If/Then/Else logic is a fundamental to scripting, and today I learned that an assumption I was making about how a “short-hand” for if/then/else that I had been using was fundamentally flawed. I learned this thanks to “ShellCheck” which is a program that will scan your bash or sh (but sadly not zsh) scripts for language errors.
Consider These Two Methods
#!/bin/bash #The "long form" and proper way to make an if/then/else statement #Set a file or folder that we want to check if it exists doesItExist="/Users/bigmac/Downloads" if [ -e "$doesItExist" ]; then echo "Yes, this item exists." else echo "No, this item does not exist." fi
#!/bin/bash #The "short form" and potentially problematic way to make an if/then/else statement #Set a file or folder that we want to check if it exists doesItExist="/Users/bigmac/Downloads" [ -e "$doesItExist" ] && echo "Yes, this item exists." || echo "No, this item does not exist."
Both of these scripts appear to function exactly the same, and the second script has fewer characters so surely it’s “more efficient.”
The Problem with the Short Form Example
Lets break down this code into three parts:
A is our testing parameter (“does this item exist?) B is our desired action if A is true (send a message that “Yes, this item exists.”, and C is our desired action if A is false (send a message that “No, this item does not exist.”)
This code will work as an if/then/else statement in my example, but there is an assumption built in that could cause problems: In this example, if the “B” command/action fails, then “C” will run.
This is because any commands found after
|| will run if the previous command exited as false (or with a non-zero exit code). We will generally never run into this problem with my example script above, because using
echo to print such a simple message shouldn’t really fail.
So if you’re using the
A && B || C format in your scripts for if/then/else, you will get unwanted consequences when B exits with an error.
Proof of Concept
#!/bin/bash #The "short form" and potentially problematic way to make an if/then/else statement #Set a file or folder that we want to check if it exists doesItExist="/Users/bigmac/Downloads" [ -e "$doesItExist" ] && rm "$doesItExist" || echo "No, this item does not exist."
In this proof of concept script, I’m running the
rm command against a directory. This will fail, because
rm requires the
-r recursive flag in order to delete a directory. Here is the output of my script:
bigmac % ./failEvenIfExists.sh rm: /Users/bigmac/Downloads: is a directory No, this item does not exist.
You can see that my “A” statement ran (it tried to
rm the directory), and then my C statement ran as well, even though the item exists (A was true.) This is because any commands following the
|| will get processed if the preceding command failed.
You can add ShellCheck into Visual Studio Code, BBEdit, and many other text editors to check for errors or potential pitfalls in your scripts in real time as you write them.
You can also copy/paste an entire script body into https://shellcheck.net to get feedback.
I found the tip about this potential problem by using the ShellCheck extension in Visual Studio Code, and my proof of concept here doesn’t actually flag a problem when pasting it into the website (because it’s not incorrect code, it just doesn’t work how you might expect.) The Visual Studio Code ShellCheck extension warned me about this as an “informational” not a “warning” or “failure”.
This specific tip can be read about to get a detailed explanation of what I ran into today: https://www.shellcheck.net/wiki/SC2015