Course outline

Linux / Intermediate: system administration reasoning / Lesson 7

Write scripts with explicit contracts

Turn commands into maintainable Bash programs with input checks, controlled arguments, and deliberate failure handling.

About 4 min readContent reviewed
On this page

Lesson goals

  • Validate positional parameters and return meaningful statuses.
  • Use conditions, loops, functions, and arrays appropriately.
  • Handle cleanup and failures without trusting set -e alone.

Define inputs, outputs, and interpreter

A useful script states what arguments it accepts, what it writes, and how it reports failure. Choose Bash explicitly when using arrays or [[ conditions; /bin/sh may be another shell. A shebang selects an interpreter when the executable script is launched directly, but bash file selects Bash regardless of that first line. Keep data on standard output and diagnostics on standard error. Validate the number of arguments before expanding positional parameters, and document whether a missing value, empty string, or nonexistent path is acceptable.

bash A complete argument-checking script
#!/usr/bin/env bash
if (( $# != 1 )); then
  printf 'usage: greet NAME
' >&2
  exit 2
fi
printf 'Hello, %s
' "$1"

Save as greet.sh and study bash greet.sh Mina: it prints Hello, Mina. An incorrect argument count prints usage to standard error and exits 2. The name is data, not executable syntax.

Conditions test commands and values

An if statement uses the exit status of its condition command. Test a command directly with if command; then rather than reading $? after unrelated work. Bash [[ -f "$path" ]] tests for a regular file, but it does not guarantee that a later open will succeed: another process can change the filesystem between checks. Use checks for clear diagnostics while still handling the actual operation’s failure. Arithmetic conditions use (( expression )); their truth convention concerns the numeric expression, not the text printed by a command.

Preserve records and argument lists

A for loop over "${items[@]}" visits Bash array elements without splitting spaces inside them. A while IFS= read -r line loop preserves leading whitespace and backslashes in line-oriented input. Handle a final unterminated line if the input contract allows one. Do not loop over $(ls): command substitution removes trailing newlines, then unquoted expansion splits filenames. Functions group behavior and can use local variables to avoid accidentally changing the caller’s state. A function’s return value is an exit status; return text through output or an explicit variable convention.

bash Preserve two multiword arguments
#!/usr/bin/env bash
show_items() {
  local item
  for item in "$@"; do
    printf '<%s>
' "$item"
  done
}
items=('red apple' 'green pear')
show_items "${items[@]}"
Expected output
<red apple>
<green pear>

The function receives two arguments and prints two bracketed lines. The quoted array expansion preserves each element exactly instead of creating four separate words.

Make failure policy visible

set -u can expose unintended unset-variable expansion, and pipefail helps detect failing pipeline stages. set -e has context-dependent exceptions involving conditions, lists, functions, and pipelines; it is not a complete error-handling design. Use explicit if or || handling for operations whose failure changes the result. When capturing a failed status, save it before printing a diagnostic. Decide which failures are expected, which are retryable, and which should stop the script. A nonzero grep status for no matches may be a valid branch rather than a fatal incident.

Cleanup is part of correctness

Use mktemp for private temporary names rather than a predictable shared pathname. Register cleanup only after resource creation succeeds, quote every cleanup path, and make cleanup tolerate partially completed work. An EXIT trap runs when the shell exits through ordinary paths, but it cannot recover from every interruption, such as SIGKILL or power loss. Do not rely on traps to preserve the only copy of important data. Build output in a separate file, validate it, then use an appropriate same-filesystem rename strategy when atomic publication is required.

Common errors and fixes

A Bash script fails under sh

Why it happens
Its interpreter does not implement Bash-specific syntax.
How to fix it
Invoke Bash explicitly and declare the dependency.
Prevent it next time
Choose portability requirements before writing syntax.

A loop splits filenames

Why it happens
Unquoted command substitution was used as a file list.
How to fix it
Use quoted arrays or a suitable delimiter-aware interface.
Prevent it next time
Define record boundaries before choosing the loop.

set -e misses a failure

Why it happens
The command ran in a documented exception context.
How to fix it
Add explicit handling around the important operation.
Prevent it next time
Review failure paths as carefully as successful paths.

Check your understanding

1. Write the policy for a script that accepts exactly one readable report file.

Show worked solution

Reject any argument count other than one with usage and a nonzero status. Check the selected path for a useful diagnostic, attempt the read, and handle that read’s failure separately. Quote the path throughout, and keep diagnostics out of report output.

2. Why is a cleanup trap insufficient as the only backup strategy?

Show worked solution

Some failures prevent the trap from running, and the machine may stop midway through an operation. Preserve an independent source, write to a separate destination, and verify the resulting data. Cleanup handles temporary resources; backup and recovery protect durable data.

Key takeaways

  • Treat interpreter, input format, and failure behavior as a contract.
  • Preserve arguments and handle important failures explicitly.

Official references