The one-line mistake that makes bash scripts fail silently | Comuniq
This site requires Javascript to work properly. Please enable Javascript in your browser.
-->
/Programming
Learn to develop things, get out of debt, find answers and take part in coding and programming challenges.
Members: 8 Join
Moderated by: mozzapp
The one-line mistake that makes bash scripts fail silently
Manon_code<br>1784142790<br>[Programming]<br>1 comments
It's three in the morning and the script that runs the database backup just failed. It didn't tell anyone. It didn't produce any useful log. It simply stopped halfway through, left a two-gigabyte temporary file sitting on disk, and moved on as if nothing had happened, because the next command in the script ran anyway. Nobody noticed until the next morning, when someone went to restore a backup and found that the most recent one was three weeks old.
If you've written bash for more than a few months, you've probably lived some version of this story. And it's not because of carelessness. Bash was designed at a time when scripts were short sequences of commands typed by hand, not pieces of infrastructure that run on their own in cron, in CI pipelines, inside containers, sometimes for years without anyone rereading the code. The language hasn't changed to match how we use it today, so it's up to whoever writes the script to make up for that.
## Why bash fools even experienced people
The root of almost every problem of this kind is simple to state and easy to forget: by default, bash keeps executing after a command fails. If line three of a script errors out, line four runs normally, as if everything were fine. In a script that deletes temporary files, spins up containers, or moves data around, that's the difference between a handled error and a silent disaster.
Comparing this to other languages makes it stand out: in Python an unhandled exception stops the program, while in bash a failing command only stops the program if you explicitly say that's what you want. And that's exactly where most of the scripts that "work on my machine" and then fall apart in production come from.
## The three flags every script should have
The first line after the shebang, in most serious scripts, should be this:
```bash<br>#!/usr/bin/env bash<br>set -euo pipefail<br>```
The `-e` makes the script stop as soon as a command returns an error. The `-u` makes the script stop if you try to use a variable that was never defined, which prevents that classic case of `rm -rf $DIR/` turning into `rm -rf /` because `$DIR` was empty. The `-o pipefail` makes sure that in a pipe like `command1 | command2`, the script notices if `command1` failed, even if `command2` ran fine afterward.
That said, these three flags have limits worth knowing, because trusting them blindly creates a false sense of safety. `set -e` doesn't stop the script if the failing command is inside an `if` condition, because in that context bash assumes you want to test the result, not halt everything. It also doesn't work inside functions called in certain contexts, nor in command substitution used as the value of an assignment. In practice, this means these flags are the foundation, not the full solution, and every critical part of the script still deserves explicit checking.
## Trap: making sure the mess gets cleaned up even when everything goes wrong
Every script that creates a temporary file, opens a lock, or spins up a child process will, sooner or later, get interrupted mid-run, whether because of an error, because someone hit Ctrl+C, or because the parent process was killed. The question that matters is what's left behind afterward.
```bash<br>tmpfile=$(mktemp)<br>trap 'rm -f "$tmpfile"' EXIT
# the rest of the script uses $tmpfile normally<br>```
The `trap` tied to the `EXIT` signal guarantees that cleanup command runs no matter how the script ends, whether it succeeds, errors out, or gets interrupted manually. That's the difference between a server that accumulates junk from months of failed runs and one that cleans up after itself.
## Quoting variables: the silliest mistake that breaks the most things in production
Forgetting to quote a variable looks like a style detail, but it's probably the most common cause of bizarre bugs in bash. Without quotes, bash splits the variable's value into pieces separated by whitespace and then expands anything that looks like a filename pattern, which means a filename with a space in it, or an empty value, can make a loop behave completely differently than expected.
```bash<br># dangerous: if $file has a space or is empty, this breaks<br>for f in $(ls $dir); do<br>rm $f<br>done
# safe<br>for f in "$dir"/*; do<br>rm -- "$f"<br>done<br>```
The practical rule is: every variable that holds a path, a filename, or any text coming from outside the script should be in double quotes every time it's used, no exceptions. And `"$@"` always quoted, never `$@` unquoted, because only the quoted version preserves each argument as a separate...