普通视图

发现新文章,点击刷新页面。
昨天 — 2026年4月20日首页

Bash Strict Mode: set -euo pipefail Explained

By default, a Bash script will keep running after a command fails, treat unset variables as empty strings, and hide errors inside pipelines. That is convenient for one-off commands in an interactive shell, but inside a script it is a recipe for silent corruption: a failed backup step that still reports success, an unset path that expands to rm -rf /, a curl | sh pipeline that swallows a 500 error.

Bash strict mode is a short set of flags you put at the top of a script to make the shell fail loudly instead. This guide explains each flag in set -euo pipefail, shows what it changes with real examples, and covers the cases where you need to turn it off.

The Strict Mode Line

You will see this line at the top of many modern shell scripts:

script.shsh
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'

Each flag addresses a different class of silent failure:

  • set -e exits immediately when a command returns a non-zero status.
  • set -u treats references to unset variables as an error.
  • set -o pipefail makes a pipeline fail if any command in it fails, not just the last.
  • IFS=$'\n\t' narrows word splitting so unquoted expansions do not split on spaces.

You can enable the options separately, but in practice they are almost always used together.

set -e: Exit on Error

Without set -e, a failing command in a script does not stop execution. The script happily continues to the next line:

no-strict.shsh
#!/usr/bin/env bash
cp /does/not/exist /tmp/dest
echo "Backup complete"

Running it prints both the error and the success message:

Terminal
./no-strict.sh
output
cp: cannot stat '/does/not/exist': No such file or directory
Backup complete

The script exits with status 0, so any caller thinks the backup worked. Adding set -e changes the behavior:

strict.shsh
#!/usr/bin/env bash
set -e
cp /does/not/exist /tmp/dest
echo "Backup complete"
Terminal
./strict.sh
echo $?
output
cp: cannot stat '/does/not/exist': No such file or directory
1

The script stops at the failing cp, never prints “Backup complete”, and exits with a non-zero status. Anyone scheduling this through cron or a CI pipeline now gets a real failure signal.

Opting Out for a Single Command

Sometimes you expect a command to fail and want to handle the result yourself. Append || true to tell set -e to ignore the exit status of that one command:

sh
grep "pattern" config.txt || true

You can also use if or &&, which set -e treats as deliberate checks:

sh
if ! grep -q "pattern" config.txt; then
 echo "pattern missing"
fi

This is the canonical way to check for something without exiting the script when it is not there.

set -u: Fail on Unset Variables

A classic shell footgun is a typo in a variable name. Without strict mode, Bash silently expands the misspelled variable to an empty string:

unset.shsh
#!/usr/bin/env bash
TARGET_DIR="/var/backups"
rm -rf "$TARGE_DIR/old"

The script deletes /old because $TARGE_DIR is empty. With set -u, the same script exits before the rm runs:

unset-strict.shsh
#!/usr/bin/env bash
set -u
TARGET_DIR="/var/backups"
rm -rf "$TARGE_DIR/old"
output
./unset-strict.sh: line 4: TARGE_DIR: unbound variable

Handling Optional Variables

Environment variables that may or may not be set need a default to play nicely with set -u. The ${VAR:-default} syntax provides one without touching the original:

sh
PORT="${PORT:-8080}"
echo "Listening on $PORT"

If $PORT is unset or empty, the script uses 8080. If it is set, the original value is kept.

set -o pipefail: Catch Failures Inside Pipelines

By default, the exit status of a pipeline is the exit status of the last command. Everything to the left can fail silently:

sh
curl https://bad.example.com/data | tee data.txt

If curl fails with a 404, the pipeline still exits 0 because tee wrote its (empty) input successfully. With set -o pipefail, the pipeline adopts the first non-zero exit status from any command in it:

pipefail.shsh
#!/usr/bin/env bash
set -o pipefail
curl https://bad.example.com/data | tee data.txt
echo "Exit: $?"
output
curl: (6) Could not resolve host: bad.example.com
Exit: 6

This is the one flag that fixes the most “my script said it succeeded but clearly did not” bugs, and it is the one most often forgotten when people add just set -e.

IFS: Safer Word Splitting

The Internal Field Separator (IFS) controls how Bash splits unquoted expansions into words. The default is space, tab, and newline, which means a filename with a space in it gets split into two arguments:

sh
files="one two.txt three.txt"
for f in $files; do
 echo "$f"
done
output
one
two.txt
three.txt

Setting IFS=$'\n\t' removes the space from the separator list. You still get clean splitting on newlines (useful for reading find or ls output) and on tabs (useful for TSV data), but spaces inside values are preserved.

Even with a narrower IFS, always quote your expansions ("$var", "${array[@]}"). IFS tightening is a safety net, not a replacement for quoting.

When Strict Mode Gets in the Way

Strict mode is opinionated, and a few situations push back. The most common one is reading a file line by line with a while loop and a counter:

sh
count=0
while read -r line; do
 count=$((count + 1))
done < input.txt

That works fine, but if you increment with ((count++)) inside set -e, the script exits on the first iteration because ((count++)) returns the pre-increment value (0), which Bash treats as a failure. Use count=$((count + 1)) or ((count++)) || true to stay compatible.

Another common case is probing for a command:

sh
if ! command -v jq >/dev/null; then
 echo "jq is not installed"
 exit 1
fi

Using command -v inside an if is safe even under set -e, because set -e does not trigger on commands in conditional contexts.

If you need to temporarily disable a flag for a specific block, turn it off and back on:

sh
set +e
some_flaky_command
result=$?
set -e

This is cleaner than sprinkling || true everywhere when you have a block of commands that need softer error handling.

A Minimal Strict Script Template

Use this as a starting point for new scripts:

template.shsh
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'

# Script body goes here.

It is four lines, and it catches the majority of silent failures that cause shell scripts to misbehave in production.

Quick Reference

Flag What it does
set -e Exit when any command returns a non-zero status
set -u Treat unset variables as an error
set -o pipefail A pipeline fails if any command in it fails
IFS=$'\n\t' Word-split only on newline and tab, not on spaces
set +e / set +u Turn the matching flag back off for a block
command || true Ignore the exit status of a single command
${VAR:-default} Provide a default for optional variables

Troubleshooting

Script exits silently with no error message
A command is failing under set -e without printing anything. Run the script with bash -x script.sh to trace execution and see which line aborts.

unbound variable on a variable that sometimes is set
Replace the reference with ${VAR:-} to provide an empty default, or ${VAR:-fallback} to provide a meaningful one.

Pipeline fails on a command that is supposed to stop early
Commands like head can cause the producer to receive SIGPIPE, which pipefail treats as a failure. Either redesign the pipeline or wrap the producer in || true when the early exit is expected.

Strict mode breaks a sourced script
The set flags apply to the current shell. If you source a script that expects the old behavior, either fix the sourced script or wrap the source call between set +e and set -e.

FAQ

Should every Bash script use strict mode?
For scripts that run unattended (cron jobs, CI steps, deployment scripts), yes. For short interactive helpers, the flags are still useful, but the cost of a missed edge case is lower.

Does strict mode work in sh or dash?
set -e and set -u are POSIX, so they work in any compliant shell. set -o pipefail is a Bash extension that also works in Zsh and Ksh, but not in pure POSIX sh or dash.

Is set -e really that unreliable?
set -e has well-known edge cases, especially around functions and subshells. It is not a replacement for explicit error handling in critical paths, but combined with pipefail and -u it catches far more bugs than it causes.

How do I pass set -euo pipefail to a one-liner?
Use bash -euo pipefail -c 'your command' when invoking Bash from another program such as ssh or a Makefile.

Conclusion

set -euo pipefail and a tighter IFS catch many of the silent failures that make shell scripts hard to trust. Pair strict mode with clear error handling using Bash functions and a proper shebang line when you want scripts to fail early and predictably.

昨天以前首页

How to Parse Command-Line Options in Bash with getopts

The getopts command is a Bash built-in that provides a clean, structured way to parse command-line options in your scripts. Instead of manually looping through arguments with case and shift, getopts handles option parsing, argument extraction, and error reporting for you.

This guide explains how to use getopts to process options and arguments in Bash scripts. If you are new to command-line arguments, start with our guide on positional parameters .

Syntax

The basic syntax for getopts is:

sh
while getopts "optstring" VARNAME; do
 case $VARNAME in
 # handle each option
 esac
done
  • optstring — A string that defines which options the script accepts
  • VARNAME — A variable that holds the current option letter on each iteration

Each time the while loop runs, getopts processes the next option from the command line and stores the option letter in VARNAME. The loop ends when there are no more options to process.

Here is a simple example:

~/flags.shsh
#!/bin/bash

while getopts "vh" opt; do
 case $opt in
 v) echo "Verbose mode enabled" ;;
 h) echo "Usage: $0 [-v] [-h]"; exit 0 ;;
 esac
done

Run the script:

Terminal
./flags.sh -v
./flags.sh -h
./flags.sh -vh
output
Verbose mode enabled
Usage: ./flags.sh [-v] [-h]
Verbose mode enabled
Usage: ./flags.sh [-v] [-h]

Notice that -vh works the same as -v -h. The getopts command automatically handles combined short options.

The Option String

The option string tells getopts which option letters are valid and which ones require an argument. There are three patterns:

  • f — A simple flag (no argument). Used for boolean switches like -v for verbose.
  • f: — An option that requires an argument. The colon after the letter means the user must provide a value, such as -f filename.
  • : (leading colon) — Enables silent error mode. When placed at the very beginning of the option string, getopts suppresses its default error messages so you can handle errors yourself.

For example, the option string ":vf:o:" means:

  • : — Silent error mode
  • v — A simple flag (-v)
  • f: — An option requiring an argument (-f filename)
  • o: — An option requiring an argument (-o output)

OPTARG and OPTIND

When working with getopts, two special variables track the parsing state:

OPTARG

The OPTARG variable holds the argument value for options that require one. When you define an option with a trailing colon (e.g., f:), the value the user passes after -f is stored in OPTARG:

~/optarg_example.shsh
#!/bin/bash

while getopts "f:o:" opt; do
 case $opt in
 f) echo "Input file: $OPTARG" ;;
 o) echo "Output file: $OPTARG" ;;
 esac
done
Terminal
./optarg_example.sh -f data.csv -o results.txt
output
Input file: data.csv
Output file: results.txt

OPTIND

The OPTIND variable holds the index of the next argument to be processed. It starts at 1 and increments as getopts processes each option. After the while loop finishes, OPTIND points to the first non-option argument.

Use shift $((OPTIND - 1)) after the loop to remove all processed options, leaving only the remaining positional arguments in $@:

~/optind_example.shsh
#!/bin/bash

while getopts "v" opt; do
 case $opt in
 v) echo "Verbose mode enabled" ;;
 esac
done

shift $((OPTIND - 1))

echo "Remaining arguments: $@"
Terminal
./optind_example.sh -v file1.txt file2.txt
output
Verbose mode enabled
Remaining arguments: file1.txt file2.txt

The shift $((OPTIND - 1)) line is a common pattern. Without it, the processed options would still be part of the positional parameters, making it difficult to access the non-option arguments.

Error Handling

The getopts command has two error handling modes: verbose (default) and silent.

Verbose Mode (Default)

In verbose mode, getopts prints its own error messages when it encounters an invalid option or a missing argument:

~/verbose_errors.shsh
#!/bin/bash

while getopts "f:" opt; do
 case $opt in
 f) echo "File: $OPTARG" ;;
 esac
done
Terminal
./verbose_errors.sh -x
./verbose_errors.sh -f
output
./verbose_errors.sh: illegal option -- x
./verbose_errors.sh: option requires an argument -- f

In this mode, getopts sets opt to ? for both invalid options and missing arguments.

Silent Mode

Silent mode is enabled by adding a colon at the beginning of the option string. In this mode, getopts suppresses its default error messages and gives you more control:

  • For an invalid option, opt is set to ? and OPTARG contains the invalid option character.
  • For a missing argument, opt is set to : and OPTARG contains the option that was missing its argument.
~/silent_errors.shsh
#!/bin/bash

while getopts ":f:vh" opt; do
 case $opt in
 f) echo "File: $OPTARG" ;;
 v) echo "Verbose mode" ;;
 h) echo "Usage: $0 [-v] [-f file] [-h]"; exit 0 ;;
 \?) echo "Error: Invalid option -$OPTARG" >&2; exit 1 ;;
 :) echo "Error: Option -$OPTARG requires an argument" >&2; exit 1 ;;
 esac
done
Terminal
./silent_errors.sh -x
./silent_errors.sh -f
output
Error: Invalid option -x
Error: Option -f requires an argument

Silent mode is the recommended approach for production scripts because it allows you to write custom error messages that are more helpful to the user.

Practical Examples

Example 1: Script with Flags and Arguments

This script demonstrates a common pattern — a usage function , boolean flags, options with arguments, and input validation:

~/process.shsh
#!/bin/bash

usage() {
 echo "Usage: $0 [-v] [-o output] [-n count] file..."
 echo ""
 echo "Options:"
 echo " -v Enable verbose output"
 echo " -o output Write results to output file"
 echo " -n count Number of lines to process"
 echo " -h Show this help message"
 exit 1
}

VERBOSE=false
OUTPUT=""
COUNT=0

while getopts ":vo:n:h" opt; do
 case $opt in
 v) VERBOSE=true ;;
 o) OUTPUT="$OPTARG" ;;
 n) COUNT="$OPTARG" ;;
 h) usage ;;
 \?) echo "Error: Invalid option -$OPTARG" >&2; usage ;;
 :) echo "Error: Option -$OPTARG requires an argument" >&2; usage ;;
 esac
done

shift $((OPTIND - 1))

if [ $# -eq 0 ]; then
 echo "Error: No input files specified" >&2
 usage
fi

if [ "$VERBOSE" = true ]; then
 echo "Verbose: ON"
 echo "Output: ${OUTPUT:-stdout}"
 echo "Count: ${COUNT:-all}"
 echo "Files: $@"
 echo ""
fi

for file in "$@"; do
 if [ ! -f "$file" ]; then
 echo "Warning: '$file' not found, skipping" >&2
 continue
 fi

 if [ -n "$OUTPUT" ]; then
 if [ "$COUNT" -gt 0 ] 2>/dev/null; then
 head -n "$COUNT" "$file" >> "$OUTPUT"
 else
 cat "$file" >> "$OUTPUT"
 fi
 else
 if [ "$COUNT" -gt 0 ] 2>/dev/null; then
 head -n "$COUNT" "$file"
 else
 cat "$file"
 fi
 fi
done
Terminal
echo -e "line 1\nline 2\nline 3\nline 4\nline 5" > testfile.txt
./process.sh -v -n 3 testfile.txt
output
Verbose: ON
Output: stdout
Count: 3
Files: testfile.txt
line 1
line 2
line 3

The script parses the options first, then uses shift to access the remaining file arguments.

Example 2: Configuration Wrapper

This example shows a script that wraps another command, passing different configurations based on the options provided:

~/deploy.shsh
#!/bin/bash

ENV="staging"
DRY_RUN=false
TAG="latest"

while getopts ":e:t:dh" opt; do
 case $opt in
 e) ENV="$OPTARG" ;;
 t) TAG="$OPTARG" ;;
 d) DRY_RUN=true ;;
 h)
 echo "Usage: $0 [-e environment] [-t tag] [-d] service"
 echo ""
 echo " -e env Target environment (default: staging)"
 echo " -t tag Image tag (default: latest)"
 echo " -d Dry run mode"
 exit 0
 ;;
 \?) echo "Error: Invalid option -$OPTARG" >&2; exit 1 ;;
 :) echo "Error: Option -$OPTARG requires an argument" >&2; exit 1 ;;
 esac
done

shift $((OPTIND - 1))

SERVICE="${1:?Error: Service name required}"

echo "Deploying '$SERVICE' to $ENV with tag '$TAG'"

if [ "$DRY_RUN" = true ]; then
 echo "[DRY RUN] Would execute: docker pull myregistry/$SERVICE:$TAG"
 echo "[DRY RUN] Would execute: kubectl set image deployment/$SERVICE $SERVICE=myregistry/$SERVICE:$TAG -n $ENV"
else
 echo "Pulling image and updating deployment..."
fi
Terminal
./deploy.sh -e production -t v2.1.0 -d webapp
output
Deploying 'webapp' to production with tag 'v2.1.0'
[DRY RUN] Would execute: docker pull myregistry/webapp:v2.1.0
[DRY RUN] Would execute: kubectl set image deployment/webapp webapp=myregistry/webapp:v2.1.0 -n production

getopts vs getopt

The getopts built-in is often confused with the external getopt command. Here are the key differences:

Feature getopts (built-in) getopt (external)
Type Bash/POSIX built-in External program (/usr/bin/getopt)
Long options Not supported Supported (--verbose)
Portability Works on all POSIX shells Varies by OS (GNU vs BSD)
Whitespace handling Handles correctly GNU version handles correctly
Error handling Built-in verbose/silent modes Manual
Speed Faster (no subprocess) Slower (spawns a process)

Use getopts when you only need short options and want maximum portability. Use getopt (GNU version) when you need long options like --verbose or --output.

Troubleshooting

Options are not being parsed
Make sure your options come before any non-option arguments. The getopts command stops parsing when it encounters the first non-option argument. For example, ./script.sh file.txt -v will not parse -v because file.txt comes first.

OPTIND is not resetting between function calls
If you use getopts inside a function and call that function multiple times, you need to reset OPTIND to 1 before each call. Otherwise, getopts will start from where it left off.

Missing argument not detected
If an option requires an argument but getopts does not report an error, check that you included a colon after the option letter in the option string. For example, use "f:" instead of "f" if -f needs an argument.

Unexpected ? in the variable
In verbose mode (no leading colon), getopts sets the variable to ? for both invalid options and missing arguments. Switch to silent mode (leading colon) to distinguish between the two cases and write custom error messages.

Quick Reference

Element Description
getopts "opts" var Parse options defined in opts, store current letter in var
f in option string Simple flag, no argument
f: in option string Option that requires an argument
: (leading) Enable silent error mode
$OPTARG Holds the argument for the current option
$OPTIND Index of the next argument to process
shift $((OPTIND - 1)) Remove parsed options, keep remaining arguments
\? in case Handles invalid options
: in case Handles missing arguments (silent mode only)

FAQ

Does getopts support long options like –verbose?
No. The getopts built-in only supports single-character options (e.g., -v). For long options, use the external getopt command (GNU version) or parse them manually with a case statement and shift.

Can I combine options like -vf file?
Yes. The getopts command automatically handles combined options. When it encounters -vf file, it processes -v first, then -f with file as its argument.

What happens if I forget shift $((OPTIND - 1))?
The processed options will remain in the positional parameters. Any code that accesses $1, $2, or $@ after the getopts loop will still see the option flags instead of just the remaining arguments.

Is getopts POSIX compliant?
Yes. The getopts command is defined by the POSIX standard and works in all POSIX-compliant shells, including bash, dash, ksh, and zsh. This makes it more portable than the external getopt command.

How do I make an option’s argument optional?
The getopts built-in does not support optional arguments for options. An option either always requires an argument (using :) or never takes one. If you need optional arguments, handle the logic manually after parsing.

Conclusion

The getopts built-in provides a reliable way to parse command-line options in Bash scripts. It handles option string definitions, argument extraction, combined flags, and error reporting with minimal code.

If you have any questions, feel free to leave a comment below.

❌
❌