Arguments and Exit Codes
Part of Essentials — Bash Scripting
Third in the Essentials Bash series. Assumes you understand Variables and Quoting. Next up: Conditionals.
A script that only works with hardcoded values isn't a tool — it's a note to yourself. Arguments make scripts reusable. Exit codes make them composable: they let calling scripts, cron jobs, and monitoring systems know whether your script succeeded.
Where You've Seen This
Windows batch files use %1, %2, %3 for positional arguments — Bash uses $1, $2, $3. Exit codes are equally universal: every program returns one when it finishes (0 = success, non-zero = failure), and you've seen this whenever an installer aborted mid-way or a build stopped on error.
Positional Arguments
Arguments passed on the command line are available as $1, $2, $3, and so on. Always assign them to named variables at the top of the script — $1 is cryptic everywhere it appears; a named variable is self-documenting:
| Using Positional Arguments | |
|---|---|
- Assign
$1to a named variable immediately — if$1appears ten lines later, nobody knows what it is. - Same for every positional argument. The rest of the script reads naturally.
| Calling the Script | |
|---|---|
- Output:
Deploying version 1.4.2 to production
The Argument Special Variables
-
$1,$2, ...${N}
Positional arguments.
$1is the first argument,$2the second. For argument 10 and above, braces are required:${10}. -
"$@"
All arguments as separate quoted strings — each one intact, regardless of content. Use when iterating over a list the caller supplies, or forwarding arguments to another command.
$@ in Practice - Each argument the caller passed arrives as one intact item — no word splitting, no surprises.
-
$#
The count of arguments. Validate it at the top of any script that requires specific input — fail immediately with a usage message rather than running with missing values.
-
$0
The script's name as called. Use it in usage messages so the error always points to the right script, even when called via a symlink or from a different directory.
Script Name in Usage Message - Output:
Usage: ./deploy.sh <environment> <version>—$0expands to the script name as called.
- Output:
\"$@\" vs \"$*\"
Almost always use "$@" — each argument stays a separate quoted string. Use "$*" only when you want all arguments joined into one string for display output, never for passing to another command.
Exit Codes
Every command is a black box to its caller — input goes in, an exit code comes out. Your script is no different. When a cron job, a monitoring agent, or another script runs yours, the exit code is the only signal they get back.
The standard codes:
- 0 — success
- 1 — general error
- 2 — misuse of the command (wrong arguments)
- 126 — command found but not executable
- 127 — command not found
- 128+N — killed by signal N
| Checking Exit Codes | |
|---|---|
- 0 if the pattern was found, 1 if not found, 2 if an error occurred.
- Non-zero — the path doesn't exist.
Setting Your Script's Exit Code
Call exit 0 only if everything worked. Call exit 1 the moment you know something went wrong — don't let the script continue:
| Returning Exit Codes | |
|---|---|
- Wrong number of arguments — exit immediately, before doing any work.
- The check failed — exit with a non-zero code so callers know.
- Technically redundant (a script that reaches the end exits 0), but explicit about intent.
Using Exit Codes in the Calling Script
| Acting on an Exit Code | |
|---|---|
- Checking
$?explicitly — works, but$?is only valid immediately after the command. Any intervening command overwrites it. - Using the command directly as the condition — cleaner and safer. This is the preferred style.
Real-World Argument Patterns
Most scripts require specific arguments. Check $# at the start and fail immediately with a usage message — never let the script run with missing input:
When arguments are optional, use default values to fall back gracefully rather than requiring the caller to always supply everything:
| Arguments with Defaults | |
|---|---|
- If no first argument, default to
staging. - If no second argument, default to 30 seconds.
Wrapper scripts take fixed arguments for themselves and forward the rest to another command. shift consumes the fixed arguments, leaving "$@" as the remainder:
| Forwarding Arguments with shift | |
|---|---|
- Removes the first two arguments. What was
$3is now$1. "$@"now contains only the caller-supplied options, forwarded intact.
When your interface grows to flag-style arguments — --environment production, --dry-run, --help — that's the natural handoff point. Python's click library handles flags, validation, and --help generation in a way $1/$@ patterns can't scale to. See My Bash Script Is Getting Out of Hand.
Practice Exercises
Exercise 1: Write an Argument Validator
Write a script called backup.sh that:
- Requires exactly two arguments: a source directory and a destination directory
- Prints a helpful usage message to stderr and exits with code 1 if the wrong number of arguments is given
- Prints
"Backing up /source to /destination"when called correctly
Exercise 2: Exit Code Chain
Write a script called preflight.sh that checks three conditions and exits with code 1 if any fails, or code 0 if all pass:
- Argument
$1is provided (a hostname) - The
curlcommand is available (usecommand -v curl) - The hostname is reachable (use
ping -c 1 $1)
Print a specific error message for each failure.
Solution
Quick Recap
- Always assign
$1,$2to named variables at the top — positional numbers are cryptic in the middle of a script $#— argument count; validate at the start, fail immediately with a usage message"$@"— all arguments, each properly quoted; use when passing arguments to another command$0— the script's name; use it in usage messages- Exit codes are your script's only signal to callers — 0 means success, non-zero means failure
- Use
if ! ./script.sh; thenrather than checking$?explicitly — cleaner and$?can be overwritten - Write error messages to stderr:
echo "Error" >&2— covered in Pipes and Redirection
Further Reading
Command References
man bash— the "Special Parameters" section documents$@,$*,$#,$?,$$, and$0help shift— documentation for theshiftbuiltin
Deep Dives
- Bash FAQ: Capture Output and Exit Status — Wooledge on storing command output and checking
$? - Process Exit Status — GNU manual on how exit status flows through pipelines
Official Documentation
Exploring Python
- My Bash Script Is Getting Out of Hand — When
$1/$@handling grows unwieldy: migrating argument-heavy scripts to Python with proper parsing and--helpoutput
What's Next?
Head to Conditionals — if/elif/else, the [[ ]] operator, and the tests that drive the logic of every real Bash script.