Debugging Bash Scripts

This post covers the essential techniques for debugging Bash scripts, including trace mode with set -x, catching errors with set -e and set -u, checking exit codes, using echo for variable inspection, and running shellcheck for static analysis. Each method is explained with working examples so begi

Debugging Bash Scripts

Bash scripts are incredibly useful, but when something goes wrong, figuring out why can feel like searching for a needle in a haystack. The good news is that Bash comes with some powerful built-in debugging tools that make troubleshooting much more manageable. In this post, we'll walk through the most practical techniques for debugging Bash scripts, starting from the simplest and working up.

Start with set -x: Trace Mode

The single most useful debugging tool in Bash is set -x, also called trace mode. When you enable it, Bash prints each command to the terminal before executing it, with a + prefix. This lets you see exactly what's happening, in order, as your script runs.

You can add it directly inside your script:

#!/bin/bash
set -x

name="Alice"
echo "Hello, $name"

Running this produces:

+ name=Alice
+ echo 'Hello, Alice'
Hello, Alice

You can see the variable assignment and the echo command both printed before they execute. For complex scripts with loops and conditionals, this output is invaluable. To turn tracing off partway through a script, use set +x.

Catch Errors Early with set -e and set -u

Two other set options are worth using together for cleaner, safer scripts:

  • set -e: Exit immediately if any command returns a non-zero exit code (meaning it failed). Without this, Bash will happily keep running even after an error.
  • set -u: Treat unset variables as an error and exit. This catches typos in variable names before they cause strange behaviour.

A common pattern is to combine all three at the top of your script:

#!/bin/bash
set -eux

The u flag is especially helpful. Without it, a typo like $USERNMAE instead of $USERNAME would silently expand to an empty string and potentially cause all sorts of downstream problems.

Check Exit Codes Manually

Every command in Bash returns an exit code. A value of 0 means success; anything else means failure. You can check the exit code of the last command run using the special variable $?:

#!/bin/bash

mkdir /some/directory
echo "Exit code: $?"

If the directory already exists or permissions are denied, you'll see a non-zero exit code. Checking $? at key points in your script helps you understand exactly where things are failing.

Use echo and printf for Quick Inspection

Sometimes the simplest approach is the most effective. Dropping a few echo statements into your script to print variable values at key points is a classic debugging technique that works every time:

#!/bin/bash

filepath="/etc/hosts"
echo "DEBUG: filepath is $filepath"

if [ -f "$filepath" ]; then
  echo "File exists"
fi

Just remember to remove or comment out your debug lines before putting the script into production.

Run Scripts with the -x Flag Directly

You don't have to modify a script to enable tracing. You can pass the -x flag directly to Bash when running the script from the command line:

bash -x myscript.sh

This is handy when you want to debug a script without editing it, or when you're troubleshooting a script someone else wrote.

Use shellcheck for Static Analysis

shellcheck is a free, open-source tool that analyses your Bash scripts and points out common mistakes before you even run the script. It catches issues like quoting errors, unintended word splitting, deprecated syntax, and more. You can install it on most Linux systems with:

sudo apt install shellcheck   # Debian/Ubuntu
sudo dnf install shellcheck   # Fedora/RHEL

Then run it against your script:

shellcheck myscript.sh

You can also use it online at shellcheck.net without installing anything. If you're writing Bash regularly, making shellcheck part of your workflow will save you a lot of time.

What's Next

Now that you can debug your scripts with confidence, the next logical step is writing scripts that handle errors gracefully on their own. In the next post, we'll look at Bash error handling: using trap to catch errors and signals, writing cleanup functions, and building scripts that fail safely rather than silently. These techniques turn a fragile script into one you can actually rely on in production.

🔧
ShellCheck is a fantastic static analysis tool for Bash scripts — paste your script in or run it locally and it will flag unset variables, quoting issues, and logic errors before they ever cause a problem at runtime. ShellCheck, bash -n (syntax check flag) and VS Code with ShellCheck extension.