32

I've been banging my head against for wall for a while with this one.

I want to SSH into a set of machines and check whether they are available (accepting connections and not being used). I have created a small script, tssh, which does just that:

#!/bin/bash

host=$1
timeout=${2:-1}

ssh -qo "ConnectTimeout $timeout" $host "[ \`who | cut -f1 | wc -l \` -eq 0 ] && exit 0 || exit 1"

This script works correctly. Returning 255 if there was a connection problem, 1 if the machine is busy and 0 if everything is good. If anyone knows a better way to do this please let me know.

So next I try and call tssh on my set of machines using a while read loop, and this is where it all goes wrong. The loop exits as soon as tssh returns 0 and never completes the full set.

while read nu ; do tssh "MYBOXES$nu" ; done < <(ruby -e '(0..20).each { |i| puts i }')

At first I thought this was a subshell problem but apparently not. Any help, along with comments on style/content, would be much appreciated! I know I'm going to kick myself when I find out why...

codeforester
  • 39,467
  • 16
  • 112
  • 140
  • I'm having a similar problem with Perl's Net::SSH::Perl, @Foo-Bah has described the problem really well, and the solution is to add an empty string as parameter to the cmd: https://metacpan.org/pod/Net::SSH::Perl#out-err-exit-ssh-cmd-cmd-stdin my($stdout, $stderr, $exit) = $ssh->cmd($cmd, ""); – Thomas BDX Nov 08 '13 at 16:54

9 Answers9

49

Chris is correct. The source of the loop breaking was SSH using stdin, however guns is correct in is usage of a looping methodology.

If you are looping through input (a file with a list of hostnames for example), and calling SSH, you need to pass the -n parameter, otherwise your loop based on input will fail.

while read host; do
  ssh -n $host "remote command" >> output.txt
done << host_list_file.txt
37

In the construct

something | 
while read x; do 
    ssh ...
done

the standard input as seen by the while loop is the output of something.

The default behavior of ssh is to read standard input. This allows you to do things like

cat id_rsa.pub | ssh new_box "cat - >> ~/.ssh/authorized_keys"

Now, with that being said, when the first value is read, the first ssh command will read the entire input from something. Then, by the time ssh finishes, there is no output left, and read stops.

The fix is ssh -n ... e.g.

cat /etc/hosts | awk '{print $2}' | while read x; do
    ssh -n $x "do_something_on_the_machine"
done
Foo Bah
  • 25,660
  • 5
  • 55
  • 79
12

Most of the answers are specific to ssh. Other commands also hijack stdin and do not have a -n option. This should address any other commands. This should also work for ssh.

while read x; do 
    # Make sure command does not hijack stdin
    echo "" | command $x
done < /path/to/some/file
rouble
  • 16,364
  • 16
  • 107
  • 102
8

I ran into this today -- rsh and/or ssh can break a while read loop due to it using stdin. I put a -n into the ssh line which stops it from trying to use stdin and it fixed the problem.

4

Don't know if it would help, but a cleaner way of writing that would be

for nu in `ruby -e '(0..20).each { |i| puts i}'`; do
  tssh "MYBOXES$nu" 
done
Paul Tomblin
  • 179,021
  • 58
  • 319
  • 408
  • 3
    Also, if you have GNU Coreutils, you can use `seq 0 20` instead of the ruby command. – Jouni K. Seppänen Dec 06 '08 at 16:34
  • @Paul Thanks, that works! Now if I only i knew why! Maybe it is a subshell issue after all. Certainly reminds of previous subshell issues I've had. @Jouni I replaced the ugly ruby part with seq. Wasn't aware of that command, thanks. @All Any insight into why this fix works? –  Dec 06 '08 at 16:42
  • 1
    @Paul It's a ssh issue. I will make a post – Foo Bah Aug 10 '11 at 22:25
  • 2
    I wish the people downvoting this would explain why. – Paul Tomblin Mar 28 '14 at 13:56
  • The `while..done` loop is also problematic with running ssh inside it. Using a `for` loop instead, usually fixes the problem. – Peter Sep 16 '14 at 11:02
3

I'm also unsure about why it fails, but i like xargs and seq:

seq 0 20 | xargs -n1 tssh MYBOXES
Johannes Schaub - litb
  • 496,577
  • 130
  • 894
  • 1,212
3

As Kaii mentioned, it's really overkill to call ruby or seq (which won't work on BSD or OSX machines) just to output a range of numbers. If you're happy with using bash you can:

for i in {0..20}; do
    # command
done

I believe this should work for bash 2.05b and up.

Foo Bah
  • 25,660
  • 5
  • 55
  • 79
guns
  • 10,550
  • 3
  • 39
  • 36
2

i cant believe it was the result of 0 that broke your loop, you can test against this by replacing your tssh command in the loop with "/bin/true" which also returns 0.

regarding style i dont understand why a simple looping shell script needs ruby, perl, seq or jot or any other binary that is not on my *BSD.

you can alternatively use the shells builtin for loop construct, which works at least in ksh, bash:

for ((i=0; $i<=20; i++)); do
    tssh "MYBOXES$i"
done
Kaii
  • 20,122
  • 3
  • 38
  • 60
2

talk about a "robbing Peter to pay Paul" problem. I'd been struggling for hours to figure out why my ssh was killing my something|while read loop.

Another way to stick with a while read loop and keep your ssh at the same time is to use the "-n" switch to make STDIN on ssh /dev/null. Works like a charm for me:

#!/bin/bash
[...]
something|while read host
   do
      ssh -nx ${host} fiddleAround
   done

(I tend to always use the "-x" too to avoid wasting time negotiating X in a tunnel.)