Bash elif not being used it seems - bash

Just started looking at bash scripts yesterday and wanted to make a script for work where in I ping different addresses on a network and using ssh keys I login and shutdown mikrotiks/junipers/computers in order. I came up with this and it seems to work for the 'mikrotik' array but not for the 'other' elif statement. I'm testing this by just changing an IP address from the MikroTik array to the 'other' array to trigger the elif but it doesn't seem to do anything. Just goes straight to the else statement.
This is one of my first proper bash scripts and assembled this using other examples, stuck here for some reason.
#!/bin/bash
#first array to make the script turn off the nodes in the order I want.
targets=(
192.168.10.10
192.168.10.11
192.168.10.2
192.168.10.1
192.168.10.3
192.168.50.21
192.168.50.3
192.168.50.20
192.168.50.2
192.168.50.1
192.168.50.22
)
mikrotik=(192.168.10.1 192.168.10.2 192.158.50.1 192.168.50.2 192.168.10.5) #Mikrotik addresses
other=(192.168.50.3 192.168.10.3 192.168.10.10 192.168.10.11 192.168.50.21 192.168.50.20 192.168.50.22) #other addresses
for target in "${targets[#]}" #For each index of targets do...
do
ping -c1 $target > /dev/null #ping each ip address
if [[ ($? -eq 0) && (${mikrotik[*]} =~ "$target") ]] #if ping successful and target is within mikrotik array
then
echo "$target mikrotik has replied and will now shutdown"
ssh $target "system shutdown"
echo "..................................."
elif [[ ($? -eq 0) && (${other[*]} =~ "$target") ]] #if ping successful and target within the juniper or computer array
then
echo "$target device has replied and will now shutdown"
ssh $target "shutdown now"
echo "..................................."
else
echo "$target didn't reply moving onto next target"
echo "..................................."
fi
done

$? contains the last command executed. So you do:
command1
if command2 ... $? ...; then # this command has it's own exit status
# and it's nonzero because the body is not executed!
...
elif command3 ... $? ...; then # the __LAST__ command here is command2
# $? has the exit status of command2, __not__ command1
...
fi
The $? inside elif has the exit status of [[ executed in the first if. Because first if body was not executed, the [[ exited with nonzero exit status - so $? in the elif clause will always be nonzero. Generally, using $? like doing command; if (($?)); then is an antipattern - don't use it. Check the exit value of the command instead - if command; then. You want to check the exit status of ping, so test it with if:
if ping -c1 $target > /dev/null; then
# those if's could be refactored too to extract common code
if [[ ${mikrotik[*]} =~ "$target" ]]; then
name=mikrotik
command=(system shutdown)
elif [[ ${other[*]} =~ "$target" ]]; then
name=device
command=(shutdown now)
else
handle error
fi
echo "$target $name has replied and will now shutdown"
ssh "$target" "${command[#]}"
else
echo "$target didn't reply moving onto next target"
fi
echo "..................................."
If you really want to store the exit status of command for later use, save it in a temporary variable right after using it, then use that temporary variable.
ping ...
pingret=$?
if ((pingret == 0)); then something; fi

Related

Only run code if git tag exists for current commit in BASH [duplicate]

What would be the best way to check the exit status in an if statement in order to echo a specific output?
I'm thinking of it being:
if [ $? -eq 1 ]
then
echo "blah blah blah"
fi
The issue I am also having is that the exit statement is before the if statement simply because it has to have that exit code. Also, I know I'm doing something wrong since the exit would obviously exit the program.
Every command that runs has an exit status.
That check is looking at the exit status of the command that finished most recently before that line runs.
If you want your script to exit when that test returns true (the previous command failed) then you put exit 1 (or whatever) inside that if block after the echo.
That being said, if you are running the command and are wanting to test its output, using the following is often more straightforward.
if some_command; then
echo command returned true
else
echo command returned some error
fi
Or to turn that around use ! for negation
if ! some_command; then
echo command returned some error
else
echo command returned true
fi
Note though that neither of those cares what the error code is. If you know you only care about a specific error code then you need to check $? manually.
Note that exit codes != 0 are used to report errors. So, it's better to do:
retVal=$?
if [ $retVal -ne 0 ]; then
echo "Error"
fi
exit $retVal
instead of
# will fail for error codes == 1
retVal=$?
if [ $retVal -eq 1 ]; then
echo "Error"
fi
exit $retVal
An alternative to an explicit if statement
Minimally:
test $? -eq 0 || echo "something bad happened"
Complete:
EXITCODE=$?
test $EXITCODE -eq 0 && echo "something good happened" || echo "something bad happened";
exit $EXITCODE
$? is a parameter like any other. You can save its value to use before ultimately calling exit.
exit_status=$?
if [ $exit_status -eq 1 ]; then
echo "blah blah blah"
fi
exit $exit_status
For the record, if the script is run with set -e (or #!/bin/bash -e) and you therefore cannot check $? directly (since the script would terminate on any return code other than zero), but want to handle a specific code, #gboffis comment is great:
/some/command || error_code=$?
if [ "${error_code}" -eq 2 ]; then
...
Just to add to the helpful and detailed answer:
If you have to check the exit code explicitly, it is better to use the arithmetic operator, (( ... )), this way:
run_some_command
(($? != 0)) && { printf '%s\n' "Command exited with non-zero"; exit 1; }
Or, use a case statement:
run_some_command; ec=$? # grab the exit code into a variable so that it can
# be reused later, without the fear of being overwritten
case $ec in
0) ;;
1) printf '%s\n' "Command exited with non-zero"; exit 1;;
*) do_something_else;;
esac
Related answer about error handling in Bash:
Raise error in a Bash script
If you are writing a function – which is always preferred – you can propagate the error like this:
function()
{
if <command>; then
echo worked
else
return
fi
}
Now, the caller can do things like function && next as expected! This is useful if you have a lot of things to do in the if block, etc. (otherwise there are one-liners for this). It can easily be tested using the false command.
Using Z shell (zsh) you can simply use:
if [[ $(false)? -eq 1 ]]; then echo "yes" ;fi
When using Bash and set -e is on, you can use:
false || exit_code=$?
if [[ ${exit_code} -ne 0 ]]; then echo ${exit_code}; fi
This might only be useful in a limited set of use-cases, I use this specifically when I need to capture the output from a command and write it to a log file if the exit code reports that something went wrong.
RESULT=$(my_command_that_might_fail)
if (exit $?)
then
echo "everything went fine."
else
echo "ERROR: $RESULT" >> my_logfile.txt
fi
you can just add this if statement:
if [ $? -ne 0 ];
then
echo 'The previous command was not executed successfully';
fi
Below test scripts below work for
simple bash test commands
multiple test commands
bash test commands with pipe included:
if [[ $(echo -en "abc\n def" |grep -e "^abc") && ! $(echo -en "abc\n def" |grep -e "^def") ]] ; then
echo "pipe true"
else
echo "pipe false"
fi
if [[ $(echo -en "abc\n def" |grep -e "^abc") && $(echo -en "abc\n def" |grep -e "^def") ]] ; then
echo "pipe true"
else
echo "pipe false"
fi
The output is:
pipe true
pipe false

How to use multiple if statements

I have a created a bash script which touches a file in specific mounts to monitor for directory locks or storage issues. I've done this using multiple if statements, but if I use the below syntax using exit at the end of the if then this exits the full script and not continue with checking the rest of the server hosts. Can someone tell me if there's either a better way of doing this or if I can replace the exit so that the script continues with the rest of the if statements?
ssh $SERVER1 touch /apps/mount/im.alive.txt
if [ $? -ne 0 ]; then
echo "$SERVER1 is in accessible. Please escalate"
else
exit
fi
ssh $SERVER2 touch /apps/mount/im.alive.txt
if [ $? -ne 0 ]; then
echo "$SERVER2 is in accessible. Please escalate"
else
exit
fi
To elaborate on the comment by #Mighty Portk: the else part of the if statement is not mandatory, so you can just get away without it, and without the exit:
ssh $SERVER1 touch /apps/mount/im.alive.txt
if [ $? -ne 0 ]; then
echo "$SERVER1 is in accessible. Please escalate"
fi
ssh $SERVER2 touch /apps/mount/im.alive.txt
if [ $? -ne 0 ]; then
echo "$SERVER2 is in accessible. Please escalate"
fi
Or just simplify it like this:
ssh "$SERVER1" touch /apps/mount/im.alive.txt || \
echo "$SERVER1 is in accessible. Please escalate"
ssh "$SERVER2" touch /apps/mount/im.alive.txt || \
echo "$SERVER2 is in accessible. Please escalate"
Or
for S in "$SERVER1" "$SERVER2"; do
ssh "$S" touch /apps/mount/im.alive.txt || \
echo "$S is in accessible. Please escalate."
done
You can also turn it into a script:
#!/bin/sh
for S; do
ssh "$S" touch /apps/mount/im.alive.txt || \
echo "$S is in accessible. Please escalate."
done
Usage:
sh script.sh "$SERVER1" "$SERVER2"
You don't have to run "ssh" and then explicitly test its exit code. The "if" command will do that for you. This is how I would write that:
if ssh $SERVER1 touch /apps/mount/im.alive.txt
then
true # do-nothing command
else
echo "$SERVER1 is in accessible. Please escalate"
fi
if ssh $SERVER2 touch /apps/mount/im.alive.txt
then
true # do-nothing command
else
echo "$SERVER2 is in accessible. Please escalate"
fi
However, since you're performing the same set of operations on more than one SERVER, you could use a loop:
for server in $SERVER1 $SERVER2
do
if ssh $server touch /apps/mount/im.alive.txt
then
true # do-nothing command
else
echo "$server is in accessible. Please escalate"
fi
done
And finally, (after all good recommendations) it is a good practice using functions for some actions, like error messages and such. Therefore, the
#put this at the top of your script
eecho() {
echo "Error: $#" >&2
return 1
}
will function as an echo, but always write the error message to STDERR, and returns problem (non zero status) so you can do the next:
[[ some_condition ]] || eecho "some error message" || exit 1
e.g. chain it with exit. (see konsolebox's recommendation)

read returns true regardless of variable content

I have the following code snippet:
#!/bin/bash
while true; do
ls "$1"
exitstatus=$?
if [[ $exitstatus != 0 ]]; then
read -n 1 -p "Retry? (y/n)" ch
echo
if [[ ! $ch =~ [Yy] ]]; then
break
fi
fi
exit $exitstatus
done
Executing this script shows that the [[ ! $ch =~ [Yy] ]] is executed regardless of the contents of $ch.
$ ./test.sh /foo
ls: cannot access /foo: No such file or directory
Retry? (y/n)y
$ ./test.sh /foo
ls: cannot access /foo: No such file or directory
Retry? (y/n)n
$
I tried commenting out things, and this seems to show the expected behaviour:
#!/bin/bash
while true; do
#ls "$1"
#exitstatus=$?
#if [[ $exitstatus != 0 ]]; then
read -n 1 -p "Retry? (y/n)" ch
if [[ ! $ch =~ [Yy] ]]; then
break
fi
#fi
#exit $exitstatus
done
Executing above in the shell gives:
$ ./test.sh
Retry? (y/n)y
Retry? (y/n)y
Retry? (y/n)n
$
What am I doing wrong in the first case?
exit $exitstatus exits the loop after the first try. It should be outside the loop.
You have several logic problems:
(1) You asked for retry, but if the answer was y, you never read in a new filename to test against, so it would always fail.
(2) also glaring was when you asked for a retry, you then did this: You got a Y on retry, but then just dumped the user into break and you went nowhere further :)
read -n 1 -p "Retry? (y/n)" ch
echo
if [[ ! $ch =~ [Yy] ]]; then ## testing for y, then we will retry right! - NOPE, just break :(
break
(3) those nasty while true loops. Think about what your loop should break on and use that test, not some loop forever and hope to break at the right spot. Nevertheless, turning a couple of bits of logic around you can make it work like this:
#!/bin/bash
## initialize variables
ch=y
srchfile="$1"
## while loop test for Y or y
while [[ $ch == [Yy] ]]; do
# attempt listing on srchfile
ls "$srchfile"
exitstatus=$?
# if bad exit status, prompt to retry?
if [[ $exitstatus != 0 ]]; then
read -n 1 -p "Retry? (y/n)" ch
echo
# test for y or Y answer
if [[ $ch == [^Yy] ]]; then
break
fi
# if retry, read new filename to test
read -p "Enter a new name for $srchfile : " srchfile
else
# that pesky file has been found, let's do something with it!
printf "You have successfully found file : %srchfile\n"
printf "(now do something useful with with it)\n"
break
fi
done
exit $exitstatus

Checking host availability by using ping in bash scripts

I want to write a script, that would keep checking if any of the devices in network, that should be online all day long, are really online. I tried to use ping, but
if [ "`ping -c 1 some_ip_here`" ]
then
echo 1
else
echo 0
fi
gives 1 no matter if I enter valid or invalid ip address. How can I check if a specific address (or better any of devices from list of ip addresses) went offline?
Ping returns different exit codes depending on the type of error.
ping 256.256.256.256 ; echo $?
# 68
ping -c 1 127.0.0.1 ; echo $?
# 0
ping -c 1 192.168.1.5 ; echo $?
# 2
0 means host reachable
2 means unreachable
You don't need the backticks in the if statement. You can use this check
if ping -c 1 some_ip_here &> /dev/null
then
echo "success"
else
echo "error"
fi
The if command checks the exit code of the following command (the ping). If the exit code is zero (which means that the command exited successfully) the then block will be executed. If it return a non-zero exit code, then the else block will be executed.
I can think of a one liner like this to run
ping -c 1 127.0.0.1 &> /dev/null && echo success || echo fail
Replace 127.0.0.1 with IP or hostname, replace echo commands with what needs to be done in either case.
Code above will succeed, maybe try with an IP or hostname you know that is not accessible.
Like this:
ping -c 1 google.com &> /dev/null && echo success || echo fail
and this
ping -c 1 lolcatz.ninja &> /dev/null && echo success || echo fail
There is advanced version of ping - "fping", which gives possibility to define the timeout in milliseconds.
#!/bin/bash
IP='192.168.1.1'
fping -c1 -t300 $IP 2>/dev/null 1>/dev/null
if [ "$?" = 0 ]
then
echo "Host found"
else
echo "Host not found"
fi
This is a complete bash script which pings target every 5 seconds and logs errors to a file.
Enjoy!
#!/bin/bash
FILE=errors.txt
TARGET=192.168.0.1
touch $FILE
while true;
do
DATE=$(date '+%d/%m/%Y %H:%M:%S')
ping -c 1 $TARGET &> /dev/null
if [[ $? -ne 0 ]]; then
echo "ERROR "$DATE
echo $DATE >> $FILE
else
echo "OK "$DATE
fi
sleep 5
done
FYI,
I just did some test using the method above and if we use multi ping (10 requests)
ping -c10 8.8.8.8 &> /dev/null ; echo $?
the result of multi ping command will be "0" if at least one of ping result reachable,
and "1" in case where all ping requests are unreachable.
up=`fping -r 1 $1 `
if [ -z "${up}" ]; then
printf "Host $1 not responding to ping \n"
else
printf "Host $1 responding to ping \n"
fi
for i in `cat Hostlist`
do
ping -c1 -w2 $i | grep "PING" | awk '{print $2,$3}'
done
This seems to work moderately well in a terminal emulator window. It loops until there's a connection then stops.
#!/bin/bash
# ping in a loop until the net is up
declare -i s=0
declare -i m=0
while ! ping -c1 -w2 8.8.8.8 &> /dev/null ;
do
echo "down" $m:$s
sleep 10
s=s+10
if test $s -ge 60; then
s=0
m=m+1;
fi
done
echo -e "--------->> UP! (connect a speaker) <<--------" \\a
The \a at the end is trying to get a bel char on connect. I've been trying to do this in LXDE/lxpanel but everything halts until I have a network connection again. Having a time started out as a progress indicator because if you look at a window with just "down" on every line you can't even tell it's moving.
I liked the idea of checking a list like:
for i in `cat Hostlist`
do
ping -c1 -w2 $i | grep "PING" | awk '{print $2,$3}'
done
but that snippet doesn't care if a host is unreachable, so is not a great answer IMHO.
I ran with it and wrote
for i in `cat Hostlist`
do
ping -c1 -w2 $i >/dev/null 2>&1 ; echo $i $?
done
And I can then handle each accordingly.
check host every one second and send message when host is reach
while :;do ping -c 1 -w 1 -q 8.8.8.8 &>/dev/null && /root/telegram-send.sh "Host reacheble now" && break || sleep 1;done

Bash loop ping successful

I'm thinking that this needs to be changed to a while clause, at the moment it'll wait till all 10000 pings are done, I need it to return when the ping is successful. The program "say" is on OSX it makes the computer speak.
#!/bin/bash
echo begin ping
if ping -c 100000 8.8.8.8 | grep timeout;
then echo `say timeout`;
else echo `say the internet is back up`;
fi
OK I don't have rights to answer my own question so here's my answer for it after playing around:
Thanks, yeah I didn't know about $? until now. Anyway now I've gone and made this. I like that yours doesn't go forever but in my situation I didn't need it to stop until it's finished.
#!/bin/bash
intertube=0
echo "begin ping"
while [ $intertube -ne 1 ]; do
ping -c 3 google.com
if [ $? -eq 0 ]; then
echo "ping success";
say success
intertube=1;
else
echo "fail ping"
fi
done
echo "fin script"
You probably shouldn't rely on textual output of a command to decide this, especially when the ping command gives you a perfectly good return value:
The ping utility returns an exit status of zero if at least one response was heard from the specified host; a status of two if the transmission was successful but no responses were received; or another value from <sysexits.h> if an error occurred.
In other words, use something like:
((count = 60)) # Maximum number to try.
while [[ $count -ne 0 ]] ; do
ping -c 1 8.8.8.8 # Try once.
rc=$?
if [[ $rc -eq 0 ]] ; then
((count = 1)) # If okay, flag loop exit.
else
sleep 1 # Minimise network storm.
fi
((count = count - 1)) # So we don't go forever.
done
if [[ $rc -eq 0 ]] ; then # Make final determination.
echo `say The internet is back up.`
else
echo `say Timeout.`
fi
You don't need to use echo or grep. You could do this:
ping -oc 100000 8.8.8.8 > /dev/null && say "up" || say "down"
This can also be done with a timeout:
# Ping until timeout or 1 successful packet
ping -w (timeout) -c 1
I use this Bash script to test the internet status every minute on OSX
#address=192.168.1.99 # forced bad address for testing/debugging
address=23.208.224.170 # www.cisco.com
internet=1 # default to internet is up
while true;
do
# %a Day of Week, textual
# %b Month, textual, abbreviated
# %d Day, numeric
# %r Timestamp AM/PM
echo -n $(date +"%a, %b %d, %r") "-- "
ping -c 1 ${address} > /tmp/ping.$
if [[ $? -ne 0 ]]; then
if [[ ${internet} -eq 1 ]]; then # edge trigger -- was up now down
echo -n $(say "Internet down") # OSX Text-to-Speech
echo -n "Internet DOWN"
else
echo -n "... still down"
fi
internet=0
else
if [[ ${internet} -eq 0 ]]; then # edge trigger -- was down now up
echo -n $(say "Internet back up") # OSX Text-To-Speech
fi
internet=1
fi
cat /tmp/ping.$ | head -2 | tail -1
sleep 60 ; # sleep 60 seconds =1 min
done
If you use the -o option, BSD ping (which is also on macOS) will exit after receiving one reply packet.
Further reading: https://www.freebsd.org/cgi/man.cgi?query=ping
EDIT: paxdiablo makes a very good point about using ping’s exit status to your advantage. I would do something like:
#!/usr/bin/env bash
echo 'Begin ping'
if ping -oc 100000 8.8.8.8 > /dev/null; then
echo $(say 'timeout')
else
echo $(say 'the Internet is back up')
fi
ping will send up to 100,000 packets and then exit with a failure status—unless it receives one reply packet, in which case it exits with a success status. The if will then execute the appropriate statement.
Here's my one-liner solution:
screen -S internet-check -d -m -- bash -c 'while ! ping -c 1 google.com; do echo -; done; echo Google responding to ping | mail -s internet-back my-email#example.com'
This runs an infinite ping in a new screen session until there is a response, at which point it sends an e-mail to my-email#example.com. Useful in the age of e-mail sent to phones.
(You might want to check that mail is configured correctly by just running echo test | mail -s test my-email#example.com first. Of course you can do whatever you want from done; onwards, sound a bell, start a web browser, use your imagination.)
I liked paxdiablo's script, but wanted a version that ran indefinitely. This version runs ping until a connection is established and then prints a message saying so.
echo "Testing..."
PING_CMD="ping -t 3 -c 1 google.com > /dev/null 2>&1"
eval $PING_CMD
if [[ $? -eq 0 ]]; then
echo "Already connected."
else
echo -n "Waiting for connection..."
while true; do
eval $PING_CMD
if [[ $? -eq 0 ]]; then
echo
echo Connected.
break
else
sleep 0.5
echo -n .
fi
done
fi
I also have a Gist of this script which I'll update with fixes and improvements as needed.

Resources