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?
Shell syntax
You seem to be confused regarding conditionals in shell scripts. Every shell command has an exit status, which is an integer between 0 and 255, with 0 meaning success and any other value meaning failure. Statements like if
and while
that expect boolean operands inspect the exit status of the command and treat 0 (success) as true and any other value (failure) as false.
For example, the grep
command returns 0 if the pattern is found and 1 if the pattern is not found. So
while ifconfig | grep "192.168.100." > /dev/null; do …
repeats the loop as long as the pattern 192.168.100.
is found in the output of ifconfig
. Note that the pattern 192.168.100.
matches strings like 192x168 1007
, because .
in a regular expression matches any character; to search for a literal string, pass the option -F
to grep
. To invert the condition, put !
in front.
while ! ifconfig | grep -F "192.168.100." > /dev/null; do …
Further in the script, you want to compare the value of a variable to a number. You use the -gt
operator, which is part of the syntax of the of conditional expressions understood by the test
command. The test
command returns 0 if the conditional expression is true and 1 if the conditional expression is false.
if test "$x" -gt 200; then
It is customary to use the alternate name [
for the test
command. This name expects the command to end with the parameter ]
. The two ways of writing this command are exactly equivalent.
if [ "$x" -gt 200 ]; then
Bash also offers a third way to write this command, with the special syntax [[ … ]]
. This special syntax can support a few more operators than [
, because [
is an ordinary command subject to the usual parsing rules, while [[ … ]]
is part of the shell syntax.
Again, keep in mind that [
is for conditional expressions, which are a syntax with operators like -n
, -gt
, … [
doesn't mean “boolean value”: any command has a boolean value (exit status = 0?).
Detecting that the network is up
Your way of detecting that the network is up is not robust. In particular, note that your script will be triggered as soon as any network interface acquires an IP address within the specified range. In particular, it's quite possible that DNS won't be up yet at that point, let alone any network shares mounted.
Do you really need to run these commands when someone logs in? It's easier to make a command run automatically when the network is brought up. The way to do that depends on your distribution and whether you use NetworkManager.
If you need to run these commands as part of the login scripts, then test for the resource that you really need, not for the presence of an IP address. For example, if you want to test whether /net/somenode/somedir
is mounted, use
while ! grep -q /net/somenode/somedir </proc/mounts; do
sleep 1
done
If you have upstart or systemd…
then you can use it. For example, with Upstart, mark your job as start on net-device-up eth0
(replace eth0
by the name of the interface that provides the desired network connectivity). With Systemd, see Cause a script to execute after networking has started?
Best Answer
Yes, this is possible.
This
sh
script (also works inbash
,ksh93
and other compatible shells) contains your infinite loop. After launching the job in the background, it installs a signal handler, a small routine that should be run whenever the script catches a particular signal. The signal that it catches is theINT
(interrupt) signal, which you send by pressing Ctrl+C.The script waits for the job to finish, then it sleeps for 10 minutes (as your script does). Before it sleeps, it uninstalls the signal handler so that you may interrupt the script with Ctrl+C.
If an
INT
signal comes in while the job is running, the signal handler will kill the job using its process ID. The script will then progress to uninstall the handler and sleep.If you don't exit the script during the 10 minutes of sleep, the loop continues with launching the job and installing the handler again.