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)
Related
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
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
the more I learn bash the more questions I have, and the more I understand why very few people do bash. Easy is something else, but I like it.
I have managed to figure out how to test directories and there writablity, but have a problem the minute I try to do this with a remote server over ssh. The first instance testing the /tmp directory works fine, but when the second part is called, I get line 0: [: missing]'`
Now if I replace the \" with a single quote, it works, but I thought that single quotes turn of variable referencing ?? Can someone explain this to me please ? Assuming that the tmp directory does exist and is writable, here the script so far
#!/bin/bash
SshHost="hostname"
SshRsa="~/.ssh/id_rsa"
SshUser="user"
SshPort="22"
Base="/tmp"
Sub="one space/another space"
BaseBashExist="bash -c \"[ -d \"$Base\" ] && echo 0 && exit 0 || echo 1 && exit 1\""
SSHBaseExist=$( ssh -l $SshUser -i $SshRsa -p $SshPort $SshHost ${BaseBashExist} )
echo -n $Base
if [ $? -eq 0 ]
then
echo -n "...OK..."
else
echo "...FAIL"
exit 1
fi
BaseBashPerm="bash -c \"[ -w \"$Base\" ] && echo 0 && exit 0 || echo 1 && exit 1\""
SSHBaseExist=$( ssh -l $SshUser -i $SshRsa -p $SshPort $SshHost ${BaseBashPerm} )
if [ $? -eq 0 ]
then
echo "...writeable"
else
echo "...not writeable"
fi
BaseAndSub="$Base/$Sub"
BaseAndSubBashExist="bash -c \"[ -d \"$BaseAndSub\" ] && echo 0 && exit 0 || echo 1 && exit 1\""
SSHBaseAndSubExist=$( ssh -l $SshUser -i $SshRsa -p $SshPort $SshHost ${BaseAndSubBashExist} )
echo -n $BaseAndSub
if [ $? -eq 0 ]
then
echo -n "...OK..."
else
echo "...FAIL"
exit 1
fi
BaseAndSubBashPerm="bash -c \"[ -w \"$BaseAndSub\" ] && echo 0 && exit 0 || echo 1 && exit 1\""
SSHBaseAndSubPerm=$( ssh -l $SshUser -i $SshRsa -p $SshPort $SshHost ${BaseAndSubBashPerm} )
if [ $? -eq 0 ]
then
echo -n "...writeable"
else
echo "...not writeable"
fi
exit 0
The first thing you should do is refactor your code with simplicity in mind, then the quoting error will go away as well. Try:
if ssh [flags] test -w "'$file'"; then
Encapsulate your SSH flags in a ssh config to facilitate re-use, and your script will shorten dramatically.
You are fine with single quotes in this context; by the time the script is seen by the remote bash, your local bash has already substituted in the variables you want to substitute.
However, your script is a total mess. You should put the repetitive code in functions if you cannot drastically simplify it.
#!/bin/bash
remote () {
# most of the parameters here are at their default values;
# why do you feel you need to specify them?
#ssh -l "user" -i ~/.ssh/id_rsa -p 22 hostname "$#"
ssh hostname "$#"
# —---------^
# if you really actually need to wrap the remote
# commands in bash -c "..." then add that here
}
exists_and_writable () {
echo -n "$1"
if remote test -d "$1"; then
echo -n "...OK..."
else
echo "...FAIL"
exit 1
fi
if remote test -w "$1"; then
echo "...writeable"
else
echo "...not writeable"
fi
}
Base="/tmp"
# Note the need for additional quoting here
Sub="one\\ space/another\\ space"
exists_and_writable "$Base"
BaseAndSub="$Base/$Sub"
exist_and_writable "$BaseAndSub"
exit 0
ssh -qnx "useraccount#hostname"
"test -f ${file absolute path} ||
echo ${file absolute path} no such file or directory"
This may turn out to be more of a thought exercise, but I am trying to echo a newline after some command I'm executing within a conditional. For example, I have:
if ssh me#host [ -e $filename ] ; then
echo "File exists remotely"
else
echo "Does not exist remotely"
fi
And want to throw in an echo after the ssh command regardless of the outcome. The reason is formatting; that way a newline will exist after the prompt for password for ssh.
First Try
if ssh me#host [ -e $filename ] && echo ; then
Because && echo would not change the conditional outcome, but bash would not execute echo if ssh returned false. Similarly,
if ssh me#host [ -e $filename ] || (echo && false) ; then
Does not work because it will short-circuit if ssh returns true.
An answer to the problem would be
ssh me#host [ -e $filename ]
result=$?
echo
if [ $result == 0 ] ; then
but was wondering if there was some similar conditional expression to do this.
Thanks.
While this would work
if foo && echo || ! echo; then
I'd prefer putting the whole thing into a function
function addecho() {
"$#" # execute command passed as arguments (including parameters)
result= $? # store return value
echo
return $result # return stored result
}
if addecho foo; then
What about this?
if ssh me#host [ -e $filename ] && echo || echo; then
I have not thought about precedence order of && and || and surely putting some parenthesis would help, but like that it works already... you get the echo both when ssh fails and when it succeeds...
Add the "echo" before the filename test
if ssh me#host "echo; [ -e $filename ]"; then
echo "File exists remotely"
else
echo "Does not exist remotely"
fi
Need some shell scripting help, especially with my if-then-else logic. I want to combine both conditions, but not sure if the file checks will work the same? Should I be doing something like a nested if?? My script uses the if statements to do file checks to see if they exist, then do something..
There is probably a better way to do file checks part too.
Any help, critique would be appreciated. Thanks.
Here's my code, it sort of works.
if [ $# != 1 ]; then
echo "Usage: getlogs.sh <remote-host>" 2>&1
exit 1
fi
#Declare variables
STAMP=`date '+%Y%m%d-%H:%M'`
REMOTE_MYCNF=/var/log/mysoft/mysoft.log
REMOTE_GZ=/var/log/mysoft/mysoft.log.1.gz
REMOTE_DIR=/var/log/mysoft/
BACKUP_DIR=/home/mysql/dev/logs/
NEWLOG="foo-temp.log"
export REMOTE_MYCNF STAMP SHORTNAME
export REMOTE_DIR REMOTE_GZ
#Copy file over
echo "START..." 2>&1
test -f $BACKUP_DIR$1.mysoft.log
if [ $? = 0 ]; then
echo "Local log file exists, clean up for new copy..." 2>&1
/bin/rm $BACKUP_DIR$1.mysoft.log
exit 0
else
echo "File does not exist, getting a new copy..." 2>&1
fi
echo "Checking remotely in $1 for foo logfile $REMOTE_MYCNF $STAMP" 2>&1
if [ ! -f $REMOTE_MYCNF ]; then
echo "File exists remotely, creating new logfile and copy here...." 2>&1
ssh $1 "zcat $REMOTE_GZ >> $REMOTE_DIR$NEWLOG"
ssh $1 "cat $REMOTE_MYCNF >> $REMOTE_DIR$NEWLOG"
/usr/bin/scp $1:$REMOTE_DIR$NEWLOG $BACKUP_DIR$1.mysoft.log
echo "end remote copy" 2>&1
echo "Cleaning up remote files" 2>&1
ssh $1 "rm $REMOTE_DIR$NEWLOG"
exit 0
else
echo "Unable to get file" 2>&1
exit 0
fi
Updated code using help:
if [ -f $BACKUP_DIR$1.mysoft.log ]; then
echo "Local log file exists, clean up for new copy..." 2>&1
/bin/rm $BACKUP_DIR$1.mysoft.log
exit 0
else
echo "File does not exist, getting a new copy..." 2>&1
echo "Checking remotely in $1 for foo logfile $REMOTE_MYCNF $STAMP" 2>&1
if [ ! -f $REMOTE_MYCNF ]; then
echo "File exists remotely, creating new logfile and bring a copy here...." 2>&1
ssh $1 "zcat $REMOTE_GZ >> $REMOTE_DIR$NEWLOG"
ssh $1 "cat $REMOTE_MYCNF >> $REMOTE_DIR$NEWLOG"
/usr/bin/scp $1:$REMOTE_DIR$NEWLOG $BACKUP_DIR$1.mysoft.log
echo "end remote copy" 2>&1
echo "Cleaning up remote files" 2>&1
ssh $1 "rm $REMOTE_DIR$NEWLOG"
exit 0
else
echo "Unable to get file" 2>&1
exit 0
fi
fi
The file test can be combined into one statement like this:
if [ -f $BACKUP_DIR$1.mysoft.log ]; then
At a glance, it doesn't look like you need to export any of the variables.
If you intend for the if [ ! -f $REMOTE_MYCNF ]; then block to be executed within the else of the previous if, just move it within it.
if ...
then
foo
else
if ...
then
bar
else
baz
fi
fi
If you need to check two things:
if [ "$foo" = "bar" && "$baz" = "qux" ]
Always quote your variables.
In a short script it's fine to use positional parameters such as $1 directly, but it makes a longer script easier to understand if a variables with meaningful names are assigned their values near the top.
remote_host=$1
When you want to echo errors to stderr do it this way:
echo "Message" >&2
The way you have it, you're echoing the message and any errors the echo itself may produce (pretty rare) to stdout.