OSX bash & expect

I have a script that must perform a 'sudo' operation on each of a number of hosts. I'm trying to get expect working so I only have to enter it once, and have run into a couple of issues.

First, several examples suggest to use:

/usr/bin/expect <<EOD
spawn ssh -t $host /usr/bin/sudo -v
expect "Password:"
send "$SUDOPASS\n"
EOD

However, as soon as I enter the second '<', all of the quoting gets screwed up, and my script will terminate with an unexpected end of file. This is really strange, as I use the same mechanism with cat to write out files in other scripts.

Other examples suggest:

/usr/bin/expect -c "
spawn ssh -t $host /usr/bin/sudo -v
expect "Password:"
send "$SUDOPASS\n"
"

Doing it this way (and adding -d to expect), I get errors about no tty and a repeat of my password with an 'n' appended to the end, so it isn't sending a newline (I've tried '\r' as well), but instead appears to be escaping the 'n'

Since most of the examples I'm finding are from Linux, I expect (ha, ha, pun not intended!) that they're using a GNU expect, and I'm using a BSD expect. I am reading through the man page, but the word "spawn" appears so many times, it could be quite a while before I stumble across the correct instance.

Here's a result that seems to be closest to working:

flamingo:~ jnojr$ Scripts/getinfo_expect.sh 
Assuming you have one 'sudo' password for all of your hosts, enter it now: 
expect version 5.45
spawn /usr/bin/ssh -t macbook /usr/bin/sudo -v
parent: waiting for sync byte
parent: telling child to go ahead
parent: now unsynchronized from child
spawn: returns {67358}

expect: does "" (spawn_id exp7) match glob pattern "Password:"? no
Password:
expect: does "Password:" (spawn_id exp7) match glob pattern "Password:"? yes
expect: set expect_out(0,string) "Password:"
expect: set expect_out(spawn_id) "exp7"
expect: set expect_out(buffer) "Password:"
send: sending "password\n" to { exp7 }
argv[0] = /usr/bin/expect  argv[1] = -d  argv[2] = -c  argv[3] = 
    spawn /usr/bin/ssh -t macbook /usr/bin/sudo -v
    expect Password: { send password\n }  
set argc 0
set argv0 "/usr/bin/expect"
set argv ""
sudo: no tty present and no askpass program specified

That's with leaving the '\n' outside of the quotes, as in

send "$SUDOPASS"\n

It seems to be mostly working, except skipping the '-t' option to ssh

Maybe 'ssh -tt' for a better fake terminal ?

That isn't in the man page, but I tried it anyway. No difference :frowning:

---------- Post updated 02-27-13 at 01:30 PM ---------- Previous update was 02-26-13 at 01:37 PM ----------

I found this link which gave me some useful info... removing preceding indentation from my expect lines got me going! However, now, lines where I echo text or variables interspersed with '/t' no longer contain tabs, but literal '/t' strings!

I usually write expect using autoexpect. It is very fussy! Perhaps use quoted real tabs?

The lines with the tabs are not in the expect portion. I did replace them with real tabs, and that works, but... jeez Louise! That isn't very maintainable!

Here's what I'm trying to get. In my non-expect version, I use:

  ssh $host ls /etc/cma.conf >/dev/null 2>&1
  if [ "$?" -eq 0 ]; then
    ssh -t $host 'sudo -v'
    HBSSVER=`ssh $host 'sudo cat /etc/cma.conf' | grep CMABuildNumber | cut -d'>' -f2 | cut -d '<' -f1`
  fi

That works... it returns a four-digit build number, which is exactly what I want. But that scrip prompts me for my sudo password once for each host it hits, which can be a pain. So in my expect version, I have:

  ssh $host ls /etc/cma.conf >/dev/null 2>&1
  if [ "$?" -eq 0 ]; then
/usr/bin/expect <<EOD
spawn ssh -t $host sudo -v
expect "Password:"
send "$SUDOPASS\r"
EOD
    HBSSVER=`ssh -t $host sudo cat /etc/cma.conf | grep CMABuildNumber | cut -d'>' -f2 | cut -d'<' -f1`
  fi

That is very literally the only difference between the two versions of my script. I am befuddled as to why that screws up tabs in other lines. Also, this way, my HBSSVER= line no longer works... it hangs at my cut statements. IIRC, that's why I was using 'sudo -v', so that the password could be entered and cached on the remote host, and the second line could just run without prompting.

And, yes, I know I technically could change permissions on the /etc/cma.conf file to no longer need sudo That isn't an option, either. If you've never dealt with HBSS, consider yourself lucky and just trust me :smiley:

Have you considered running these things on the root cron or a root service so they can be created with read permission to your id or your user's ids.

I guess tabs are meta to expect, and if they lose their quoting or escaping, they are just white space!

You should be able to send one block of code through, or on the command line of, one ssh session and get all your answers from the output on stdout and stderr.

echo '...'|ssh whoever@wherever -tt bash 2>&1 | sed '...'
 
ssh whoever@wherever -ntt '...' 2>&1 | while read l ; do ... done

BTW, the first actually becomes bash -c '...' on the remote side for non-trivial '...'

I am not a big fan of the here document '<<' ; there are places it is great, like plain text lists, but for shell it has less control than "echo '...' |ssh bash", where exactly what you say goes down the pipe unprocessed to a remote shell. If you need something on this side that ' blocks, slip out to ", do it , then out of " and back to '. If you want that same control over on the remote shell, ' becomes '"'"' (single-double-single-double-single). No surprises, no extra quote stripping. I try to stay away from '\' for similar reasons. They disappear with little warning.

Unfortunately, what I'm doing is more of an "on demand" type of thing. If I absolutely had to, I could use a cron job, but that's a last resort.

In any case, someone in another thread gave me the hint that pushed me over the edge...

  ssh $host ls /etc/cma.conf >/dev/null 2>&1
  if [ "$?" -eq 0 ]; then
    if [ -f /tmp/hbss.sh ]; then rm -f /tmp/hbss.sh; fi
cat >/tmp/hbss.sh <<EOF
#!/bin/bash
ssh -tt $host sudo grep CMABuildNumber /etc/cma.conf <<EOS
$SUDOPASS
EOS
EOF
    chmod 755 /tmp/hbss.sh
    HBSSVER=`/tmp/hbss.sh | grep CMABuildNumber | cut -d'>' -f2 | cut -d'<' -f1`
    #if [ -f /tmp/hbss.sh ]; then rm -f /tmp/hbss.sh; fi
  fi

It is a lot uglier than I'd like, but it works.

Ahh, the mysteries of bash!

I still think it is in too many pieces. Figure out how to do it with one ssh. The ssh time to set up is pretty long.

I guess you are removing the hbss.sh because it may not be writable. Maybe writing it in a less public dir would rmeove that possibility, or with a per-user pathname. You could make a new script *.sh.tmp without removing it, and then if you like the script you made, just mv it to the final name. That way, a partial script is never in service.