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
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.