1. Overview
In Linux, we know that we can concatenate multiple commands conditionally with && or ||, for instance, “CMD1 && CMD2 || CMD3“.
We like using these operators when working on the command line or writing shell scripts since they’re compact and save a lot of typing.
In this tutorial, we’ll start from a script example and discuss a case we should avoid using && when writing shell scripts.
2. The my_script.sh Script
First of all, let’s say we’ve created a simple shell script:
$ cat my_script.sh
#!/bin/bash
USER_INPUT="$1"
ERROR_LOG="$2"
MONTHS="Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec"
[ -e "$ERROR_LOG" ] || { echo "Not a file: $ERROR_LOG"; exit 1; }
INPUT_VALID=$( awk -v userMonth="$1" 'BEGIN{ result = 0 }
{for(i=1; i<=NF; i++)
if ( tolower($i) == tolower(userMonth) )
result = 1
}END{print result}' <<< $MONTHS )
if [ "$INPUT_VALID" = "1" ]; then
echo "do some work....." && echo "Well Done!" || exit 8
else
echo "[Error] User input is not a Month: ($USER_INPUT)" >> "$ERROR_LOG" && exit 7
fi
As the code above shows, the my_script.sh script accepts two arguments, a string and a log filename.
First, we check if the log file exists. Then, we verify if USER_INPUT is a valid month name in the short form. Here, we’ve used the awk command to do the case-insensitive check.
Next, if USER_INPUT is valid, we’ll do some work. For simplicity, we’ve used an echo command to simulate the “work”. Otherwise, we log the error in the provided log file and exit with a special status code: 7.
Finally, let’s assume there’s another command or process to check the status code of this script to do further processing.
So the script is pretty straightforward. Let’s execute it and see if it works as expected.
3. Testing the my_script.sh Script
First, let’s pass an invalid month value and an invalid log filename:
$ ./my_script.sh "foo" "not-exists.file"
Not a file: not-exists.file
$ echo $?
1
As we can see, the script prints the expected error message. Further, it exits with the expected status code (1) as well.
Next, let’s pass a valid log filename and execute it again:
$ ./my_script.sh "foo" "err.log"
$ echo $?
7
$ cat err.log
[Error] User input is invalid: (foo)
As the output shows, this time, the script produces no output. But if we check the exit status code, it’s 7, as the user input is an invalid month. Another command or process can now perform further processing depending on the status code. Also, a corresponding log entry appears in the err.log file. All these show that the script works for this case as well.
Finally, let’s try the script by passing two valid values:
$ ./my_script.sh "aug" "err.log"
do some work.....
Well Done!
$ echo $?
0
The command produces the expected output. Further, the status code is 0, which is expected too.
We think we’ve tested all possible scenarios, and the script works. Therefore, we decide to put the script into production.
However, we’ve fallen into the pitfall of the && usage. Next, let’s figure it out.
4. Introduction to the Pitfall
To understand the problem, let’s execute the script again. However, this time, let’s change the log file to /opt/err.log:
$ ls -l /opt/err.log
-rw-r--r-- 1 root root 0 Jul 1 19:52 /opt/err.log
The script works as expected. So far, so good. Next, let’s change the first argument to an invalid one:
$ ./my_script.sh "FOO" "/opt/err.log"
./my_script.sh: line 21: /opt/err.log: Permission denied
Since only the root user can modify the /opt/err.log file, our script cannot append the error log to it. Now, let’s check its status:
$ echo $?
1
The status code is 1 instead of the expected 7. That is to say, the status code will mislead the other process, and finally, we may have some unexpected results.
It’s worth mentioning that the “Permission denied” error is just an example. It could be “disk is full” or other unpredictable I/O errors.
Next, let’s discuss why it may happen and how to fix it.
5. Looking Into the Problem
Given “CMD1 && CMD2“, we know that CMD2 will start only if CMD1 executes successfully.
The bug in our script is on the line:
echo "[Error] User input is not a Month: ($USER_INPUT)" >> "$ERROR_LOG" && exit 7
In this case, we know the user has provided an invalid input. Therefore, we would like to log this error and exit with the code 7.
We should note that we always need to execute the “exit 7” command, no matter if the logging command fails, as some other process runs depending on the status code.
Now that we understand the cause of the problem, fixing it isn’t a challenge for us. We can solve the problem in a couple of ways. For example, we can break the command combination and simply write two commands:
echo "[Error] User input is not a Month: ($USER_INPUT)" >> "$ERROR_LOG"
exit 7
In this way, even if the first echo command fails, the exit 7 command gets executed anyway.
Alternatively, we can combine the two commands using a semicolon instead of &&:
echo "[Error] User input is not a Month: ($USER_INPUT)" >> "$ERROR_LOG"; exit 7
Once we fix it, the status code will be 7, even if the previous echo command fails:
$ ./my_script.sh "FOO" "/opt/err.log"
./my_script.sh: line 21: /opt/err.log: Permission denied
$ echo $?
7
6. Conclusion
We often combine multiple commands conditionally using && or || in Linux. In this article, we’ve discussed a pitfall of using && when writing shell scripts.
If we want a command to be executed anyway, no matter if the previous command succeeds or fails, we shouldn’t use && or || to concatenate it to other commands.