The for
loop is fine here. But note that this is because the file contains machine names, which do not contain any whitespace characters or globbing characters. for x in $(cat file); do …
does not work to iterate over the lines of file
in general, because the shell first splits the output from the command cat file
anywhere there is whitespace, and then treats each word as a glob pattern so \[?*
are further expanded. You can make for x in $(cat file)
safe if you work on it:
set -f
IFS='
'
for x in $(cat file); do …
Related reading: Looping through files with spaces in the names?; How can I read line by line from a variable in bash?; Why is while IFS= read
used so often, instead of IFS=; while read..
? Note that when using while read
, the safe syntax to read lines is while IFS= read -r line; do …
.
Now let's turn to what goes wrong with your while read
attempt. The redirection from the server list file applies to the whole loop. So when ssh
runs, its standard input comes from that file. The ssh client can't know when the remote application might want to read from its standard input. So as soon as the ssh client notices some input, it sends that input to the remote side. The ssh server there is then ready to feed that input to the remote command, should it want it. In your case, the remote command never reads any input, so the data ends up discarded, but the client side doesn't know anything about that. Your attempt with echo
worked because echo
never reads any input, it leaves its standard input alone.
There are a few ways you can avoid this. You can tell ssh not to read from standard input, with the -n
option.
while read server; do
ssh -n $server "uname -a"
done < /home/kenny/list_of_servers.txt
The -n
option in fact tells ssh
to redirect its input from /dev/null
. You can do that at the shell level, and it'll work for any command.
while read server; do
ssh $server "uname -a" </dev/null
done < /home/kenny/list_of_servers.txt
A tempting method to avoid ssh's input coming from the file is to put the redirection on the read
command: while read server </home/kenny/list_of_servers.txt; do …
. This will not work, because it causes the file to be opened again each time the read
command is executed (so it would read the first line of the file over and over). The redirection needs to be on the whole while loop so that the file is opened once for the duration of the loop.
The general solution is to provide the input to the loop on a file descriptor other than standard input. The shell has constructs to ferry input and output from one descriptor number to another. Here, we open the file on file descriptor 3, and redirect the read
command's standard input from file descriptor 3. The ssh client ignores open non-standard descriptors, so all is well.
while read server <&3; do
ssh $server "uname -a"
done 3</home/kenny/list_of_servers.txt
In bash, the read
command has a specific option to read from a different file descriptor, so you can write read -u3 server
.
Related reading: File descriptors & shell scripting; When would you use an additional file descriptor?
You need to remove whitespace characters from the $IFS
parameter for read
to stop skipping leading and trailing ones (with -n1
, the whitespace character if any would be both leading and trailing, so skipped):
while IFS= read -rn1 a; do printf %s "$a"; done
But even then bash's read
will skip newline characters, which you can work around with:
while IFS= read -rn1 a; do printf %s "${a:-$'\n'}"; done
Though you could use IFS= read -d '' -rn1
instead or even better IFS= read -N1
(added in 4.1, copied from ksh93
(added in o
)) which is the command to read one character.
Note that bash's read
can't cope with NUL characters. And ksh93 has the same issues as bash.
With zsh:
while read -ku0 a; do print -rn -- "$a"; done
(zsh can cope with NUL characters).
Note that those read -k/n/N
read a number of characters, not bytes. So for multibyte characters, they may have to read multiple bytes until a full character is read. If the input contains invalid characters, you may end up with a variable that contains a sequence of bytes that doesn't form valid characters and which the shell may end up counting as several characters. For instance in a UTF-8 locale:
$ printf '\375\200\200\200\200ABC' | bash -c '
IFS= read -rN1 a; echo "${#a}"'
6
That \375
would introduce a 6-byte UTF-8 character. However, the 6th one (A
) above is invalid for a UTF-8 character. You still end-up with \375\200\200\200\200A
in $a
, which bash
counts as 6 characters though the first 5 ones are not really characters, just 5 bytes not forming part of any character.
Best Answer
The simplest way is to use
/dev/tty
as the read for keyboard input.For example:
This breaks if you don't run this on a terminal, and wouldn't allow for input to be redirected into the program, but otherwise works pretty well.
More generally, you could take a new file handle based off the original stdin, and then read from that. Note the
exec
line and theread
In both cases the program looks a bit like:
The second variation allows for input to also be redirected