Writing More Compact Bash Code

""

In any programming language, idioms may be used that may not seem obvious from reading the manual. Often these usages of the language represent ways to make your code more compact (as in requiring fewer lines of code). Of course, some will eschew these idioms believing they represent bad style. Style, of course, is in the eyes of beholder, and this article is not intended as an exercise in defining good or bad style. So for those who may be tempted to comment on the grounds of style I would (re)direct your attention to /dev/null.

In most programming languages, non-scripting ones at least, you want to avoid uninitialized variables. In bash, using uninitialized variables can often simplify your code. For instance, you might at first be tempted to do something like this in bash:

$ cat code.bash
if test -f some_file; then
    file_exists=1
else
    file_exists=0
fi
if [[ $file_exists -eq 1 ]]; then
    do_something
fi

However, if you remember that referring to an undefined variable in bash does not cause an error, it just returns an empty string, you can simplify the above code to something like this:

$ cat code.bash
if test -f some_file; then
    file_exists=1
fi
if [[ $file_exists ]]; then
    do_something
fi

Instead of checking to see if the variable file_exists has a particular value (0 or 1), you just check to see if it has a value or not.

Something that I learned while writing this is that I had mistakenly believed that the variable in the condition in the second if statement needed to be in double quotes (i.e. [[ "$file_exists" ]]) or bash would produce an error in instances where the variable was not set; this appears not to be the case. I'm not sure if this changed at some point or if I've just always been mistaken.

Continuing with this example, another thing you can do to make this a bit more compact is to make use of the && and || (and and or) operators to simplify your if statements. The && and || operators are used in bash to separate commands, and they are short-circuiting operators, which means they don't execute the commands on their right-hand side if executing the left-hand side is enough to know what the overall exit status will be.

For example, in A and B there is no need to evaluate B if A is false since the overall expression can only be true if both are true, similarly in A or B there is no need to evaluate B if A is true since only one needs to be true for the expression to be true. When dealing with bash is true can generally be interpreted to mean that a command terminated with a zero exit status, and is false means that the command terminated with a non-zero exit status. Now you can simplify the code above into:

$ cat code.bash
test -f some_file  &&  file_exists=1
[[ $file_exists ]]  &&  do_something

Replacing if statements with command lists separated by && and || can also come in handy in loops to break out of the loop or continue to the next iteration:

$ cat code.bash
for i in *.pdf
do
    test $i == '*.pdf'  &&  break
    test ${i:0:1} == 'a'  ||  continue
done

The first test above checks to see if the loop variable equals the glob expression in the loop (this happens if there no matches), and if so, break out of the loop. The second test checks to see if the name of the matched file starts with the letter a, and if not, skips to the next iteration of the loop (we only execute the continue if the comparison fails). Obviously, in this contrived example you could have just used a*.pdf in the glob expression in the for loop.

One of the traps that you can fall into when using the && and || operators to replace simple if statements is you might think something like this would work:

$ cat code.bash
test -f some_file  &&  file_exists=1; echo 'File exists'

But it doesn't work, because the semicolon has lower precedence than && and ||, and so this executes as if you wrote:

$ cat code.bash
if test -f some_file; then
    file_exists=1
fi
echo 'File exists'

To make this work, you need to put the code after the && in braces {}:

$ cat code.bash
test -f some_file  &&  { file_exists=1; echo 'File exists'; }

Note that the braces should have whitespace around them, and the semicolon at the end of the list is mandatory (see the man page for a bit more explanation on why).

Load Disqus comments