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:
| Parameter | Description | Use Case |
|---|---|---|
$0 | The filename / invocation path of the script itself. | Generating usage / help text menus showing correct script invocation. |
$1 - $9 | Positional 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$PATHor typo).128+N— Command terminated by system signalN(e.g.130indicates 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=$?.