Reliable Bash Scripts

Reliable Bash scripts make their assumptions visible: which shell and tools they require, what counts as an input record, which failures are expected, and what a caller can infer from the exit status. A collection of strict-looking options cannot replace that contract.

Quote data, keep command arguments separate, check consequential operations explicitly, and test failures alongside success. Use a different language when structured parsing, complex state, or recovery logic makes the shell program harder to reason about than the task itself.

TL;DR

Quick Example

This text-file illustration preserves spaces and backslashes while normalizing each input line into a private scratch file. It counts a final non-empty line even without a trailing newline. It requires Bash plus mktemp, rm, and wc; it is not a binary or NUL-delimited record processor.

Test it with a filename that starts with a dash or contains spaces and glob characters, and with input containing backslashes, percent signs, blank lines, a final line without a newline, and no input at all. Usage errors return 64, missing input returns 66, and the scratch file is removed after successful runs.

A separate local pipeline check showed false piped into true returning zero with pipefail disabled and one with it enabled. Those are actual bounded observations. No Linux distribution, other shell, injected signal, disk-full condition, permission race, arbitrary read error, or production operation was tested.

Core Concepts

Quoting keeps data inside its intended boundary

The GNU Bash manual explains quoting as removing special interpretation from characters. In ordinary command arguments, quote expansions that represent one value, and use arrays when a command needs a list of distinct arguments. Quoting is not a substitute for checking what operation those arguments request. Bash quoting.

The example uses a fixed printf format with the line supplied as data. A percent sign in the input therefore remains input rather than becoming a formatting instruction. Its file redirection uses a quoted pathname; it does not ask a command to interpret that name as an option. When a utility accepts options, use its supported end-of-options convention where needed.

Avoid passing a constructed command string to eval or another shell merely to preserve a convenient interface. That creates another interpretation boundary. Build the argument list directly and keep untrusted text out of command syntax. A filename that happens to work in a demonstration is not evidence that every generated command is correctly quoted.

Exit status needs a meaning

Decide whether a nonzero result means an expected negative condition, a failed operation, or an invalid invocation. Handle those cases where they occur. A search command finding no match and a storage operation failing should not automatically have the same application consequence.

Bash's errexit option has documented exceptions for conditional and other contexts. Pipefail changes a pipeline's result to the rightmost nonzero component status, or zero when all components succeed. Neither feature classifies the failure or supplies recovery. The set builtin.

The example uses explicit checks instead of relying on errexit. Its status values are a teaching contract, not a claim of a universal standard for all tools. A caller should also inspect whether the promised output exists and is complete before treating a zero status as the end of a larger workflow.

A trap is part of the failure policy

The EXIT handler captures the incoming status before running cleanup. It removes only the scratch path returned by mktemp, then exits with the captured status. This preserves the operation's result even if cleanup itself reports a problem. Whether cleanup failure should change the final status is a separate contract to choose deliberately.

Do not promise cleanup under every circumstance. Abrupt process or machine termination can prevent a handler from running. Design temporary resources so leftovers are identifiable and safe to handle later. Avoid broad cleanup patterns whose scope depends on an unset, empty, or unexpectedly expanded variable.

Best Practices

Define the input before selecting a loop

Decide whether records are lines, NUL-delimited paths, or structured documents. The loop is intentionally for a regular text file. It does not distinguish every possible input error from end-of-file, and its preflight readability check does not eliminate later races.

For a production task, use an input method that can meet the required framing and error contract. If that requires substantial parsing or recovery machinery, a language with suitable libraries may be easier to verify.

Test the environment you intend to support

Record Bash and external-tool versions, locale and line-ending assumptions, and required filesystem behavior. Run syntax checks and meaningful success/failure cases. Do not infer portability from one local execution or assume that a command with the same name has identical options everywhere.

Keep sensitive values out of tracing and error output. Diagnostic context should identify the failed operation without copying secrets or complete private files into logs. Test how the caller distinguishes a complete result from a partial one.

Common Mistakes

Treating strict flags as a safety certificate

Splitting input on whitespace

Losing the original exit status in cleanup

Calling one shell run portable

FAQ

Should every script enable errexit?

Choose an error-handling policy you can explain and test. Errexit can help, but its context-dependent rules do not replace explicit handling of important or expected failures. The example deliberately uses explicit checks.

Does pipefail make a pipeline recover automatically?

No. It changes the reported pipeline status. The script still needs to decide what a failure means, what evidence to retain, and whether any partial output is usable.

Does this loop support every filename or file format?

It handles a quoted regular-file pathname and the tested awkward name. Its contents are line-oriented text, not arbitrary binary or NUL-delimited data. Broader claims require a different contract and tests.

Related Topics

References