Bash Scripting 05 ๐ง Positional Parameters โ Handling command-line arguments ($1, $2, $@)
Every useful script eventually needs to accept input from the outside world. Hardcoded values work for a demonstration, but real automation demands flexibility: a backup script that receives a target directory, a deployment tool that accepts an environment name, or a log analyzer that takes a filename. The shell provides a direct mechanism for this: positional parameters. When a script is invoked, the arguments following its name are assigned to special variables numbered from one upward. $1 holds the first argument, $2 the second, and the special parameter $@ represents the complete collection. Understanding these parametersโtheir syntax, their quirks under quoting, and their role in robust argument parsingโseparates scripts that work for one specific case from scripts that work for any case.
Key point: Positional parameters are read-only variables assigned by the shell at invocation time. $1 through $9 are accessible directly, but any parameter numbered ten or higher requires braces: ${10}. Always quote "$@" to preserve argument boundaries; unquoted $@ allows word splitting that will silently corrupt arguments containing spaces.
Why positional parameters exist
The fixed-input problem.
A script without parameters is a script with a single behavior. It can only operate on the paths, filenames, and values written into its source code. This limitation forces duplication: if you need to process five different directories, you either copy the script five times or manually edit it between runs. Positional parameters eliminate this duplication by making the script’s input external to its implementation. The same backup script can target /home, /var/www, or /tmp depending on what you pass at the command line, without a single change to the script itself .
The quoting vulnerability problem.
The most common bug in shell scripts that handle arguments is unquoted expansion. When a user runs ./script.sh "my file.txt", the shell passes a single argument containing a space. If the script references $1 without quotes, the shell splits that argument into two words, my and file.txt. The script then processes two files instead of one, or crashes because neither exists. The special parameter "$@" exists specifically to preserve the original argument boundaries: each positional parameter expands as a separate word, with spaces intact . This distinction between quoted and unquoted expansion is not a minor detail; it is the difference between a script that handles real filenames and one that fails unpredictably.
The argument-counting problem.
Scripts must know how many arguments they received before processing them. A script expecting a source and destination should fail clearly if given one argument or five. The special parameter $# provides this count directly, enabling validation before any argument is accessed . Without $#, a script might proceed with empty variables, produce confusing errors, or modify files in unintended ways because a missing parameter expanded to an empty string.
a. Accessing individual parameters
The shell assigns each command-line argument to a numbered parameter starting at $1. The script’s own name is stored in $0, which is not a positional parameter in the strict sense and is not affected by the shift command .
#!/usr/bin/env bash
# Display the first three arguments individually
echo "Script name: $0" # $0 is the script path, not a positional parameter
echo "First argument: $1" # The first argument after the script name
echo "Second argument: $2" # The second argument
echo "Third argument: $3" # The third argument (empty if not provided)
Running this script with ./show_args.sh alpha beta gamma produces output identifying each argument by position. If the script receives fewer arguments than referenced, the missing variables expand to empty stringsโno error is raised. This silent behavior means scripts must validate $# before assuming an argument exists.
For positions beyond nine, braces become mandatory. The shell interprets $10 as $1 followed by the literal character 0. To access the tenth argument, write ${10} .
#!/usr/bin/env bash
# Accessing double-digit parameters requires braces
echo "Tenth argument: ${10}" # Correct: expands to the tenth positional parameter
echo "Tenth argument: $10" # Incorrect: expands $1 then appends literal "0"
This syntax rule exists because the shell’s parameter expansion grammar only recognizes a single digit immediately after $. Braces disambiguate the parameter name from any following text.
b. Collecting all arguments with $@ and $*
Bash provides two special parameters for representing the complete set of positional parameters: $@ and $*. They behave identically when unquoted, but diverge critically inside double quotes. This difference determines whether your script preserves argument boundaries or collapses them.
#!/usr/bin/env bash
# Demonstrate the difference between "$@" and "$*"
printf 'Quoted $@: '; printf '[%s] ' "$@"; echo
printf 'Quoted $*: '; printf '[%s] ' "$*"; echo
When invoked as ./collect.sh "first arg" "second arg", the quoted "$@" expands to two separate words: first arg and second arg. The quoted "$*" expands to a single word: first arg second arg, joined by the first character of the IFS variable (a space by default) .
The practical consequence is significant. "$@" is almost always what you want. It preserves the user’s original intent: each argument remains a distinct entity, even if it contains spaces. "$*" is appropriate only when you deliberately need to join all arguments into a single string, such as logging or constructing a delimited output line.
#!/usr/bin/env bash
# Iterate over arguments safely using "$@"
for arg in "$@"; do
echo "Processing: $arg" # Each iteration receives one intact argument
done
Using $@ unquoted in this loop would split arguments containing spaces, causing the loop body to execute more times than the user intended .
c. Validating and manipulating parameters
Robust scripts validate their arguments before use. The parameter $# provides the argument count, and conditional tests can enforce minimums, maximums, or exact counts.
#!/usr/bin/env bash
# Require exactly two arguments: source and destination
if [ "$#" -ne 2 ]; then
echo "Usage: $0 <source> <destination>" >&2
exit 1
fi
source="$1"
destination="$2"
echo "Copying from $source to $destination"
The shift builtin removes the first positional parameter and renumbers the remaining ones: $2 becomes $1, $3 becomes $2, and so on. This is essential for parsing options followed by operands, where options are consumed one at a time until none remain .
#!/usr/bin/env bash
# Consume options with a while loop and shift
verbose=false
while [ "$#" -gt 0 ]; do
case "$1" in
-v|--verbose)
verbose=true
shift # Remove the option from the parameter list
;;
--)
shift # End of options marker; stop parsing
break
;;
*)
break # First non-option argument; stop parsing
;;
esac
done
# Remaining parameters are now operands
echo "Verbose: $verbose, remaining args: $@"
This patternโtest $1, act, shift, repeatโis the foundation of manual argument parsing. It handles options in any order and leaves non-option arguments available for further processing. The -- convention explicitly signals the end of options, allowing filenames that begin with dashes to be processed correctly .
Complete Example Session
#!/usr/bin/env bash
# ============================================
# PART 1: THE SCRIPT HEADER AND USAGE
# ============================================
# A file processing script that demonstrates positional parameters
set -euo pipefail
usage() {
echo "Usage: $0 [-v] [-o OUTPUT] FILE [FILE...]" >&2
echo " -v Verbose output" >&2
echo " -o OUTPUT Write results to OUTPUT" >&2
exit 1
}
# ============================================
# PART 2: DEFAULTS AND OPTION PARSING
# ============================================
verbose=false
output=""
# Parse options until the first non-option argument
while [ "$#" -gt 0 ]; do
case "$1" in
-v)
verbose=true
shift
;;
-o)
if [ -z "${2:-}" ]; then
echo "Error: -o requires an argument" >&2
usage
fi
output="$2"
shift 2
;;
--)
shift
break
;;
-*)
echo "Unknown option: $1" >&2
usage
;;
*)
break
;;
esac
done
# ============================================
# PART 3: VALIDATE REMAINING OPERANDS
# ============================================
if [ "$#" -eq 0 ]; then
echo "Error: at least one FILE is required" >&2
usage
fi
# ============================================
# PART 4: PROCESS EACH REMAINING ARGUMENT
# ============================================
for file in "$@"; do
if [ ! -f "$file" ]; then
echo "Skipping non-file: $file" >&2
continue
fi
if $verbose; then
echo "Processing: $file"
fi
done
# ============================================
# PART 5: OUTPUT HANDLING
# ============================================
if [ -n "$output" ]; then
echo "Would write results to: $output"
else
echo "No output file specified"
fi
The five parts demonstrate the complete lifecycle of argument handling: define usage, parse options with shift, validate the remaining operand count, iterate safely with "$@", and act on parsed values. The quoted "$@" in the loop ensures filenames with spaces are processed as single units.
Quick Reference
Parameter Syntax
| Syntax | Meaning | When to Use |
|---|---|---|
$1โ$9 | First through ninth positional parameters | Direct access to individual arguments |
${10} | Tenth parameter (braces required) | Any parameter beyond single digits |
$# | Number of positional parameters | Validation before access |
$0 | Script name (not a positional parameter) | Usage messages |
"$@" | All parameters as separate words | Iteration, forwarding, safety |
"$*" | All parameters as one word | Joining for logs or display |
$@ | All parameters, subject to splitting | Rarely correct; avoid |
Validation Tests
| Test | Checks | Example |
|---|---|---|
[ "$#" -eq N ] | Exact count | [ "$#" -eq 2 ] |
[ "$#" -ge N ] | Minimum count | [ "$#" -ge 1 ] |
[ -z "${1:-}" ] | Argument missing or empty | Guard before use |
[ -n "${1:-}" ] | Argument present and non-empty | Proceed safely |
Shift Behavior
| Command | Effect |
|---|---|
shift | Remove $1; $2 becomes $1, etc. |
shift 2 | Remove first two parameters |
$# after shift | Decreases by number of shifts |
Best Practices
โ Do This:
# Validate count before accessing
if [ "$#" -lt 2 ]; then usage; fi # โ
# Quote "$@" in for loops
for arg in "$@"; do echo "$arg"; done # โ
# Use ${10} for double digits
echo "Tenth: ${10}" # โ
# Use ${1:-} for safe checks
if [ -z "${1:-}" ]; then echo "missing"; fi # โ
# Shift after consuming options
case "$1" in -v) verbose=true; shift ;; esac # โ
# Use $# to detect missing operands
[ "$#" -gt 0 ] || usage # โ
โ Don’t Do This:
# Access $1 without checking $#
echo "$1" # โ
# Unquoted $@ splits on spaces
for arg in $@; do echo "$arg"; done # โ
# $10 is $1 followed by "0"
echo "Tenth: $10" # โ
# Direct $1 in tests can fail with set -u
if [ -z "$1" ]; then echo "missing"; fi # โ
# Forgetting shift causes infinite loop
while [ "$#" -gt 0 ]; do case "$1" in -v) ;; esac; done # โ
# Assuming $* preserves boundaries
printf '%s\n' "$*" # โ
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Argument with spaces split into multiple | $@ used without quotes | Use "$@" |
${10} vs $10 confusion | Shell only reads one digit after $ | Always brace double digits |
set -u crashes on missing argument | $1 referenced when unset | Use ${1:-} |
| Infinite loop in option parser | Forgetting shift in case branch | Add shift after each option |
| Empty string treated as missing | [ -z "$1" ] when $1 unset | Use [ -z "${1:-}" ] |
$# checked after shift | Count changes during parsing | Validate before parsing or track remaining |
Filename beginning with - rejected | Case pattern -* catches it | Handle -- to end options |
Real-World Examples
1. Require exactly two arguments
[ "$#" -eq 2 ] || { echo "Usage: $0 SRC DST" >&2; exit 1; }
src="$1"; dst="$2"
2. Safe iteration over files
for file in "$@"; do
[ -f "$file" ] || continue
wc -l "$file"
done
3. Shift through options manually
while [ "${1:-}" = "-v" ]; do
verbose=true
shift
done
4. Access tenth argument
echo "Tenth: ${10}"
5. Check argument count before use
if [ "$#" -lt 1 ]; then
echo "Need at least one argument" >&2
exit 1
fi
6. Join all arguments for logging
echo "Invoked with: $*"
7. Forward all arguments to another command
exec other_script "$@"
8. Handle end-of-options marker
case "$1" in
--) shift; break ;;
esac
9. Default value for optional argument
target="${1:-.}"
echo "Target: $target"
10. Count operands after option parsing
shift $((OPTIND - 1))
[ "$#" -gt 0 ] || { echo "No operands"; exit 1; }
Visual
The Parameter Assignment Model
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ COMMAND LINE TO POSITIONAL PARAMETERS โ
โ โ
โ ./backup.sh /home/user "my documents" -v โ
โ โ โ โ โ โ
โ โ โ โ โโโ $3 = -v โ
โ โ โ โโโโโโโโโโโโโโโ $2 = "my documents"
โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโ $1 = /home/user โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ $0 = ./backup.shโ
โ โ
โ $# = 3 "$@" = 3 separate words โ
โ ${10} requires braces โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Quoted vs Unquoted Expansion
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ "$@" vs $@ WITH ARGUMENTS CONTAINING SPACES โ
โ โ
โ Invocation: ./script.sh "a b" c โ
โ โ
โ "$@" (quoted): โ
โ โโโโโโโโโโโ โโโโโ โ
โ โ "a b" โ โ"c"โ Two words, boundaries preserved โ
โ โโโโโโโโโโโ โโโโโ โ
โ โ
โ $@ (unquoted): โ
โ โโโโโ โโโโโ โโโโโ โ
โ โ"a"โ โ"b"โ โ"c"โ Three words, space split โ
โ โโโโโ โโโโโ โโโโโ โ
โ โ
โ Rule: Always quote "$@" โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Shift and Option Parsing Flow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ SHIFT-BASED OPTION PARSING โ
โ โ
โ Initial: $1=-v $2=-o $3=out.txt $4=file1 $5=file2 โ
โ โ
โ Step 1: case "$1" in -v) shift ;; esac โ
โ $1=-o $2=out.txt $3=file1 $4=file2 โ
โ โ
โ Step 2: case "$1" in -o) output=$2; shift 2 ;; esac โ
โ $1=file1 $2=file2 โ
โ โ
โ Step 3: Non-option encountered; break โ
โ Operands: "$@" = file1 file2 โ
โ โ
โ Each shift renumbers remaining parameters downward. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
$1โ$9 | First through ninth positional parameters; direct syntax |
${10}+ | Braces required for parameters with multiple digits |
$0 | Script name; not a positional parameter; unaffected by shift |
$# | Count of positional parameters; validates before access |
"$@" | All parameters as separate words; the correct choice for iteration |
"$*" | All parameters as one word joined by IFS; use only for display |
shift | Removes $1 and renumbers; the engine of option parsing |
-- | Conventional marker signaling end of options |
${1:-} | Safe expansion preventing errors under set -u |
$@ unquoted | Subject to word splitting; almost always a bug |
Key takeaways:
- Quote
"$@"religiously. Every unquoted expansion of$@is a latent bug that manifests when a user passes a filename containing a space. - Validate
$#first. Accessing$1without confirming at least one argument exists produces silent empty strings or crashes underset -u. - Brace double digits.
$10is not the tenth argument;${10}is. The shell’s grammar reads only one character after$. - Shift consumes and renumbers. Option parsing is a loop of test, act, shift. Forgetting the shift produces an infinite loop.
$*joins,$@separates. The distinction only matters when quoted, and that is precisely when it matters most.${1:-}is safer than$1. The colon-dash form provides a default and prevents unset-variable errors.- Handle
--explicitly. Users have legitimate reasons to pass arguments beginning with dashes; the parser must respect the end-of-options marker.
Remember: Positional parameters are the shell’s native interface between the command line and the script. They are assigned at invocation, read-only during execution, and subject to subtle expansion rules that reward precision. The script that quotes "$@", validates $#, and shifts deliberately will handle any input a user provides. The script that does not will fail the first time a path contains a space and mystify its author in the process.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!