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...

1
  • 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: metacpan.org/pod/Net::SSH::Perl#out-err-exit-ssh-cmd-cmd-stdin my($stdout, $stderr, $exit) = $ssh->cmd($cmd, ""); Commented Nov 8, 2013 at 16:54

9 Answers 9

53

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
Sign up to request clarification or add additional context in comments.

Comments

38

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

1 Comment

while read x; do ...; done < <(awk '{print $2}' </etc/hosts) would be better behaved, avoiding the problems described in BashFAQ #24.
14

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

3 Comments

That's quite inefficient compared to command "$x" </dev/null. echo "" | requires a fork(), a mkfifo(), etc; and it moves command out-of-process, so even if it's a shell function, it can't set variables that persist outside the loop.
This solved my problem. In my case, using echo "" | for some reason made me unable to print the result of the command to stdout. Using @CharlesDuffy's </dev/null worked.
Also worked with sudo when running a command that eats input. Thanks!
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.

Comments

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

5 Comments

Also, if you have GNU Coreutils, you can use seq 0 20 instead of the ruby command.
@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?
@Paul It's a ssh issue. I will make a post
I wish the people downvoting this would explain why.
The while..done loop is also problematic with running ssh inside it. Using a for loop instead, usually fixes the problem.
4

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.

Comments

3

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

seq 0 20 | xargs -n1 tssh MYBOXES

Comments

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

Comments

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.)

Comments