Arguments & Exit Codes

Master parameter arrays ($1, $#), whitespace boundaries, shift operations, and system exit code lookup tables.

beginner 15 min lesson hands-on task included

In production automation, scripts must accept configuration arguments dynamically and report their execution states back to scheduling systems.


Topic 7: Arguments

Arguments let you feed values into a script at the moment you run it, without any interactive prompting: ./script.sh Abdur. Inside the script, $1 refers to the first argument, $2 the second, and so on. $# tells you how many arguments were passed, and $@ refers to all of them.

Argument Array Boundaries & Whitespace:

When arguments are passed, the shell tokenizes them based on spacing unless the caller wraps arguments in quotes:

./script.sh payment-gateway service # $1 = payment-gateway, $2 = service
./script.sh "payment-gateway service" # $1 = payment-gateway service, $2 = empty

Inside your script, handling arguments requires care. Always reference $@ (all arguments) inside double quotes ("$@"). This forces the shell to expand each parameter as a separate quoted word, preserving spaces inside parameters. (Unquoted $@ or $* splits space-containing arguments into separate values).

The Shift Command:

The shift command pops the first parameter off the arguments list, shifting every subsequent parameter down by 1 index (e.g. $2 becomes $1, $3 becomes $2, $# decrements by 1). This is critical when looping through arbitrary argument arrays:

# Shift processing loop
while [ $# -gt 0 ]; do
  echo "Processing parameter: $1"
  shift
done

Special Positional Parameters Lookup Table:

ParameterDescriptionUse Case
$0The filename / invocation path of the script itself.Generating usage / help text menus showing correct script invocation.
$1 - $9Positional arguments 1 through 9.Extracting user flags, target paths, or modes.
${10}Positional arguments above 9 (must be wrapped in curly braces).Handling large parameter collections.
$#The total count of positional arguments passed.Verifying that correct number of options were provided.
$@All positional parameters as separate quoted tokens.Iterating over all target values safely.
$*All positional parameters concatenated as a single string.Logging command lines.
$$The Process ID (PID) of the current shell process.Generating unique temporary file paths (/tmp/temp_$$).

Try it yourself: Rewrite your Topic 6 script (create a folder) to instead take the folder name as $1 rather than asking interactively.


Topic 13: Exit Codes

Every command, when it finishes, secretly reports back a number: 0 means “success,” any non-zero number means “something went wrong” (different numbers can mean different specific failures). $? shows you the exit code of the last command that ran.

Exit Code System Lookup Table:

While codes 1-255 are failures, specific values carry standard meanings across Unix:

  • 0 — Success.
  • 1 — Generic execution error (e.g. invalid arithmetic, file operation failure).
  • 2 — Misuse of shell built-ins (e.g. syntax errors).
  • 126 — Command invoked cannot execute (permission problems or directory targeted).
  • 127 — Command not found (binary not in $PATH or typo).
  • 128+N — Command terminated by system signal N (e.g. 130 indicates termination by signal 2, Ctrl+C).
+-------------------------------------------------------------+
|                     Exit Status Meanings                    |
+-----------+-------------------------------------------------+
| Exit Code | Typical Meaning                                 |
+-----------+-------------------------------------------------+
|     0     | Complete Success                                |
|     1     | General Catchall Error                          |
|     2     | Syntax or Shell Built-in Misuse                 |
|    126    | Permission Denied / Cannot Execute Command      |
|    127    | Command Not Found                               |
|   128+N   | Terminated by Signal N (e.g., 130 = SIGINT)     |
+-----------+-------------------------------------------------+

Custom Script Exits:

You can set your own script’s exit code deliberately with exit 1 (failure) or exit 0 (success) — this matters once other scripts, cron, or CI/CD pipelines need to know whether your script actually succeeded.

Try it yourself: Run ls somefilethatdoesnotexist followed immediately by echo $? — see the non-zero code. Then run a command that works and check $? again.


Topic 3: Validating Arguments

Every script that takes arguments needs a guard clause before it does anything.

#!/usr/bin/env bash
set -Eeuo pipefail

readonly SCRIPT_NAME=${0##*/}

usage() {
    cat <<EOF
usage: ${SCRIPT_NAME} <environment> <artifact>

  environment   one of: dev, staging, production
  artifact      path to the file to deploy
EOF
}

(( $# == 2 )) || { usage >&2; exit 2; }

readonly ENVIRONMENT=$1
readonly ARTIFACT=$2

case $ENVIRONMENT in
    dev|staging|production) ;;
    *) echo "${SCRIPT_NAME}: invalid environment '${ENVIRONMENT}'" >&2; exit 2 ;;
esac

[[ -f $ARTIFACT ]] || { echo "${SCRIPT_NAME}: no such file: ${ARTIFACT}" >&2; exit 2; }

Three habits: count first, allow-list rather than deny-list, and exit 2 for usage errors so the caller can tell “you invoked me wrong” from “the work failed”.

Required arguments in one expansion:

readonly TARGET=${1:?target host required}
readonly PORT=${2:-22}                      # optional, with a default

${1:?message} aborts with a clear error naming the parameter. ${2:-default} supplies a fallback. Both are covered in the quoting lesson, and this is where they earn their place.

Handling an unknown number of arguments:

(( $# > 0 )) || { echo "no files given" >&2; exit 2; }

for f in "$@"; do
    [[ -r $f ]] || { echo "skipping unreadable: ${f}" >&2; continue; }
    process "$f"
done

"$@" here — never $@ or $*. A file named my report.txt becomes two arguments otherwise.


Topic 4: Producing Exit Codes

Reading them is half the job; a script that always exits 0 is a script nothing can react to.

readonly E_USAGE=2 E_CONFIG=3 E_NETWORK=4 E_VERIFY=5

[[ -r $CONFIG ]]                       || exit "$E_CONFIG"
ping -c1 -W2 "$TARGET" >/dev/null 2>&1 || exit "$E_NETWORK"
verify_checksum "$ARTIFACT"            || exit "$E_VERIFY"
exit 0

Distinct codes make the script programmable by whatever runs it:

./deploy.sh staging app.tar.gz
case $? in
    0) echo "deployed" ;;
    2) echo "bad invocation — not retrying" ;;
    4) echo "network issue — retrying in 30s"; sleep 30; exec "$0" "$@" ;;
    *) echo "failed, escalating" ;;
esac

“Exit 4 means the network was unreachable, retry” is a policy you can implement. “Exit 1” is not.

The range, and what to avoid:

exit takes 0–255. Anything larger wraps modulo 256 — exit 256 becomes 0, which reports success for a failure. Avoid 126, 127 and 128+ for your own meanings, since the shell already uses them.

exit          # exits with the status of the LAST command — often not what you meant
exit 0        # explicit

Reporting the first failure from a pipeline:

set -o pipefail
curl -fsS "$url" | jq -r '.items[]' | while read -r item; do ...; done
echo "${PIPESTATUS[@]}"        # e.g. "22 0 0" — curl failed

Without pipefail the exit status is jq’s, and a 404 from curl disappears entirely.

Try it yourself: Write a script with three distinct failure paths and three exit codes, then write a caller that branches on each with a case on $?.

Common mistake: Checking $? after something else has already run. $? is overwritten by every command, including echo and [[ ]]. Capture it immediately: status=$?.