Tips and tricks for high-quality bash scripting

Bash is a very popular shell, especially in Linux environments. Having a good knowledge of bash can get you far in terms of getting things done. Especially if you are a System Admin or a DevOps engineer or anyone who likes working with command line/terminal, knowledge of shell scripting is a must. The official documentation for bash is available at https://www.gnu.org/software/bash/manual/html_node/index.html

Almost all the best practices when it comes to a procedural programming language like modularity, logging, documentation, error-handling etc, apply to bash too. However, here I will outline a few techniques that are very specific to bash scripting.

Using set command effectively

set is a command in bash that lets us change the shell options' values. Full documentation for the set command is available at https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html

The most commonly used options in set command are:

  • The set -e command in Bash instructs the shell to exit immediately if any command within the script returns a non-zero exit status. This is useful to ensure that the script stops executing if an error occurs. Conversely, set +e disables this behavior, allowing the script to continue even if a command fails. This is helpful in implementing try/catch-like logic within your script. Try/catch is not available in bash out of the box. Here is a great example of the try/catch-like functions.

  • The set -x the command in bash is very helpful while debugging your script. This enables the trace mode where it prints out all the commands along with the values any variable that is used in the command might have. Conversely, set +x command suppresses printing out the commands.

Use variable quotes

It is always advisable to use quotes when using variables especially if your variable value involves space or any special characters. When variables are not enclosed in quotes, Bash performs word splitting and interprets certain characters as a command or shell syntax. Here is an example

#!/bin/bash

# Variable without quotes
name=John Smith

# Without quotes, Bash performs word splitting
echo Hello, $name
# Output: Hello,

# Variable with double quotes
name="John Smith"

# With quotes, the variable value is preserved
echo "Hello, $name"
# Output: Hello, John Smith

Sourcing other scripts within your script

If you need to access a function or content from another script, you use source command. This is a common practice. However, many times we make the mistake of sourcing the other script using a relative path. This causes problems when your script is invoked from a different path. For example, Let's consider these two scripts. sourced.sh is a script we source in a script called example_source.sh

Here is the content of sourced.sh

#! /bin/bash
function hello_world(){
   echo "Hello $(whoami)"
}

And here is example_source.sh

#! /bin/bash

source ./sourced.sh

hello_world

Let us assume we store this script in the same directory called as src If we run the bash script example_source from within src directory then the script will run fine. But if we run the script from any other path this script is bound to fail with the following error.

bash example_source.sh
bash: example_source.sh: No such file or directory

This common pitfall can be avoided with a simple trick. Now let's modify example_source.sh

#! /bin/bash
cur_dir=$(dirname $0)
source $cur_dir/sourced.sh

hello_world

This will now allow you to run the script example_source.sh from anywhere within the machine.

Handle secrets within a script

When it comes to secrets the same principles that apply to any programming language apply here too. Particularly

  • Avoid hardcoding secrets within your scripts. Use environment variables, configuration files, or even secret managers like Hashicorp's Vault, AWS Secret Manager, etc.

  • Make sure the secret values are never printed in the terminal. If required use set +x command wherever secrets are accessed and referred to.

  • Keep track of actions involving secrets and implement logging and monitoring to detect any unauthorized access attempts or unusual activity.

Error Handling

We have seen some techniques of error handling through set -/+ e commands. In this section, we will see some more techniques for error handling

  • Using return code through special variable $? - This can tell us if the command was executed successfully or not. One can combine this with if statements.
#! /bin/bash
set +e
<some command/s that errors out>
exit_status=$?

if [ $exit_status -eq 0 ]; then
    echo "Command succeeded"
else
    echo "Command failed with exit status: $exit_status"
fi
  • Using $$ and || for conditional executions.
command1 && command2    # Run command2 only if command1 succeeds
command1 || command2    # Run command2 only if command1 fails
  • Use of trap command. This can be used to define your error-handling routines. Here is an example:
handle_error() {
    echo "An error occurred. Exiting..."
    exit 1
}

trap 'handle_error' ERR
  • Effectively using echo for standard output and stderr for standard error output streams

This blog would be incomplete without the mention of code linting for the bash. There are tools like shellcheck. This is incredibly helpful when you want to lint or check your shell script for any syntax errors. It also checks your script for formatting and adherence to best practices.

These are some of the commonly used techniques for creating well-structured Bash scripts. If there are any additional techniques that you frequently use that I might have missed here, please feel free to add them in the comments section.