LFCS 122 ๐ง Shell Scripting โ Special Parameters
The previous chapter covered ordinary variables: assignment, expansion, and quoting. This chapter covers the special parameters that the shell sets automatically: the positional parameters ($1, $2, …), the argument collection ($@ and $*), the argument count ($#), the script name ($0), the exit status ($?), the process ID ($$), and the background job ID ($!). These are not ordinary variables; they cannot be assigned, and their values are determined by the invocation context. The LFCS exam tests their semantics, particularly the difference between "$@" and "$*" and the use of shift to consume arguments. The critical trap is the unquoted $@: it behaves differently from the quoted form when arguments contain spaces.
Key point: "$@" expands to the positional parameters as separate words, preserving the original argument boundaries. "$*" expands to the positional parameters as a single word, joined by the first character of IFS (usually a space). The unquoted $@ and $* are subject to word splitting and globbing, which destroys the argument boundaries. The shift builtin removes the first positional parameter and shifts the rest down.
Why special parameters require a specific mental model
The argument boundary problem. The $@ and $* appear similar in simple scripts, but they behave differently when arguments contain spaces. The "$@" form preserves the boundaries: three arguments become three words. The "$*" form joins them: three arguments become one word with spaces. The unquoted $@ form splits them again, so "hello world" as a single argument becomes two words, which is the bug that breaks file-processing scripts .
The positional parameter problem. The $1 is the first argument, $2 is the second, and so on. The $10 is ambiguous: the shell interprets it as ${1}0, not ${10}. The ${10} form is required for double-digit positional parameters. The ${#} is the argument count, but the ${var#pattern} is a string operation, and the ${var%pattern} is the suffix removal operation. The two syntaxes are distinct.
The script name problem. The $0 is the script name as it was invoked. If the script is executed as ./script.sh, the $0 is ./script.sh. If it is executed as /home/user/script.sh, the $0 is /home/user/script.sh. If it is sourced (. script.sh), the $0 is the name of the shell, not the script. The $0 is not the directory of the script; the ${0%/*} parameter expansion is the common way to extract the directory .
The exit status problem. The $? is the exit code of the most recent command. It is an integer. The value 0 means success. Any non-zero value means failure. The set -e option makes the script exit when a command returns non-zero. The $? must be captured immediately after the command, before any other command runs, because every command resets it.
The trade-off. The special parameters are convenient, but they are also a source of subtle bugs. The "$@" form is the correct way to pass arguments to another command. The "$*" form is for joining arguments into a single string. The shift builtin is for processing arguments in a loop. The exam tests the distinction because the difference between "$@" and $@ is the difference between a script that handles filenames with spaces and a script that breaks on them.
a. Positional parameters and the shift builtin
The positional parameters are the arguments passed to the script or function. The $1 is the first, $2 is the second, and so on. The $# is the count. The $0 is the script name.
#!/bin/bash
echo "Script: $0"
echo "First: $1"
echo "Second: $2"
echo "Count: $#"
Invoked as ./script.sh alpha beta, the output is:
Script: ./script.sh
First: alpha
Second: beta
Count: 2
The shift builtin removes the first positional parameter and shifts the rest down. The $1 becomes what was previously $2. The shift N removes N parameters. The $# decreases by the number of parameters shifted.
#!/bin/bash
while [ $# -gt 0 ]; do
echo "Argument: $1"
shift
done
This loop processes each argument in turn. The shift at the end of the loop removes the current $1, so the next iteration sees the next argument. Without the shift, the loop would run forever because $1 never changes.
The ${10} form is required for double-digit positional parameters. The $10 form is interpreted as $1 followed by 0.
b. The @ and * distinction
The @ and * look similar but behave differently when quoted.
#!/bin/bash
echo "Unquoted \$@:"
for arg in $@; do
echo " [$arg]"
done
echo "Quoted \"\$@\":"
for arg in "$@"; do
echo " [$arg]"
done
echo "Quoted \"\$*\":"
for arg in "$*"; do
echo " [$arg]"
done
Invoked with "hello world" alpha, the output is:
Unquoted $@:
[hello]
[world]
[alpha]
Quoted "$@":
[hello world]
[alpha]
Quoted "$*":
[hello world alpha]
The @ form preserves the original argument boundaries. The * form joins them into a single word. The unquoted $@ form splits them on whitespace, which destroys the boundary.
The "$@" is the correct form for passing arguments to another command. The cmd "$@" invocation passes the original arguments exactly as they were received.
#!/bin/bash
# Wrapper script that passes all arguments to the target
exec /usr/local/bin/target "$@"
The "$*" is for building a single string from the arguments. It is used when the arguments are intended to be a single value.
c. Exit status, process ID, and background job ID
The $? is the exit code of the most recent command. The $$ is the process ID of the current shell. The $! is the process ID of the most recently started background job.
#!/bin/bash
ls /nonexistent
echo "Exit code: $?"
# Output: Exit code: 2
The $? must be captured immediately after the command. Any intervening command resets it.
#!/bin/bash
sleep 60 &
pid=$!
echo "Background PID: $pid"
kill $pid
The $! is the PID of the background process. The kill $pid terminates it. The wait $pid waits for it to finish.
The $$ is the current shell’s PID. It is used for creating temporary files with a unique name.
tmpfile="/tmp/myscript.$$"
echo "Temporary file: $tmpfile"
trap 'rm -f "$tmpfile"' EXIT
The trap command registers a handler for the EXIT signal, which runs when the script exits. The rm -f "$tmpfile" removes the temporary file regardless of how the script exits. The $$ ensures the filename is unique to the process, which avoids collisions when multiple instances run concurrently.
Complete Example Session
# ============================================
# PART 1: POSITIONAL PARAMETERS
# ============================================
cat > args.sh <<'EOF'
#!/bin/bash
echo "Script: $0"
echo "First: $1"
echo "Second: $2"
echo "Count: $#"
EOF
chmod +x args.sh
./args.sh alpha beta
# Output:
# Script: ./args.sh
# First: alpha
# Second: beta
# Count: 2
# ============================================
# PART 2: SHIFT
# ============================================
cat > shift.sh <<'EOF'
#!/bin/bash
while [ $# -gt 0 ]; do
echo "Argument: $1"
shift
done
EOF
chmod +x shift.sh
./shift.sh one two three
# Output:
# Argument: one
# Argument: two
# Argument: three
# ============================================
# PART 3: "$@" PRESERVES BOUNDARIES
# ============================================
cat > at.sh <<'EOF'
#!/bin/bash
for arg in "$@"; do
echo "[$arg]"
done
EOF
chmod +x at.sh
./at.sh "hello world" alpha
# Output:
# [hello world]
# [alpha]
# ============================================
# PART 4: UNQUOTED $@ SPLITS
# ============================================
cat > unquoted.sh <<'EOF'
#!/bin/bash
for arg in $@; do
echo "[$arg]"
done
EOF
chmod +x unquoted.sh
./unquoted.sh "hello world" alpha
# Output:
# [hello]
# [world]
# [alpha]
# ============================================
# PART 5: "$*" JOINS
# ============================================
cat > star.sh <<'EOF'
#!/bin/bash
for arg in "$*"; do
echo "[$arg]"
done
EOF
chmod +x star.sh
./star.sh "hello world" alpha
# Output:
# [hello world alpha]
# ============================================
# PART 6: EXIT STATUS
# ============================================
ls /nonexistent
echo "Exit code: $?"
# Output: Exit code: 2
# ============================================
# PART 7: BACKGROUND PID
# ============================================
sleep 60 &
pid=$!
echo "PID: $pid"
kill $pid
# ============================================
# PART 8: TEMPORARY FILE WITH $$
# ============================================
cat > tmp.sh <<'EOF'
#!/bin/bash
tmpfile="/tmp/myscript.$$"
echo "Writing to $tmpfile"
echo "data" > "$tmpfile"
trap 'rm -f "$tmpfile"' EXIT
EOF
chmod +x tmp.sh
./tmp.sh
# ============================================
# PART 9: DOUBLE-DIGIT PARAMETER
# ============================================
cat > ten.sh <<'EOF'
#!/bin/bash
echo "Tenth argument: ${10}"
EOF
chmod +x ten.sh
./ten.sh 1 2 3 4 5 6 7 8 9 ten
# Output: Tenth argument: ten
The nine parts trace the special parameters: positional parameters, shift, "$@", unquoted $@, "$*", $?, $!, $$ with trap, and ${10}.
Quick Reference
Positional Parameters
| Parameter | Value |
|---|---|
$0 | Script name |
$1 โฆ $9 | Positional parameters |
${10} | Double-digit parameter |
$# | Argument count |
$@ | All arguments (separate) |
$* | All arguments (joined) |
Process and Exit
| Parameter | Value |
|---|---|
$? | Exit code of last command |
$$ | Current shell PID |
$! | PID of last background job |
$- | Current shell options |
shift Builtin
| Syntax | Effect |
|---|---|
shift | Remove first parameter |
shift N | Remove N parameters |
@ vs $
| Form | Behavior |
|---|---|
"$@" | Separate words, boundaries preserved |
"$*" | Single word, joined by IFS |
$@ (unquoted) | Split on whitespace |
$* (unquoted) | Split on whitespace |
Best Practices
โ Do This:
for arg in "$@"; do
echo "$arg"
done
<!-- Preserve argument boundaries --> # โ
exec /usr/local/bin/target "$@"
<!-- Pass all arguments through --> # โ
trap 'rm -f "$tmpfile"' EXIT
<!-- Cleanup on exit --> # โ
โ Don’t Do This:
for arg in $@; do
<!-- Splits arguments on whitespace --> # โ
echo "$10"
<!-- Interpreted as ${1}0 --> # โ
echo "Args: $*"
# When the argument boundaries matter
<!-- Joins into single string --> # โ
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Filename with space fails | Unquoted $@ | "$@" |
| Tenth argument wrong | Used $10 | ${10} |
| Exit code lost | Command between $? capture | Capture immediately |
| Temp file collision | Used fixed name | Use $$ |
| Loop infinite | shift missing | Add shift |
Real-World Examples
1. Script Name
echo "$0"
2. First Argument
echo "$1"
3. Argument Count
echo "$#"
4. All Arguments
for arg in "$@"; do echo "$arg"; done
5. Joined Arguments
echo "$*"
6. Shift Loop
while [ $# -gt 0 ]; do
echo "$1"
shift
done
7. Exit Code
cmd
echo "$?"
8. Background PID
cmd &
echo "$!"
9. Current PID
echo "$$"
10. Trap Cleanup
trap 'rm -f "$tmpfile"' EXIT
Visual
@ vs $
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Invocation: script.sh "hello world" alpha โ
โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ "$@" โ
โ โโโ "hello world" โ
โ โโโ "alpha" โ
โ (two words) โ
โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ "$*" โ
โ โโโ "hello world alpha" โ
โ (one word) โ
โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ $@ (unquoted) โ
โ โโโ hello โ
โ โโโ world โ
โ โโโ alpha โ
โ (three words) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Shift Loop
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ $1 = one, $2 = two, $3 = three โ
โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ echo $1 โ one โ
โ shift โ
โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ $1 = two, $2 = three โ
โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ echo $1 โ two โ
โ shift โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
| Script name | $0 |
| Positional parameters | $1, $2, …, ${10} |
| Argument count | $# |
| Separate arguments | "$@" |
| Joined arguments | "$*" |
| Exit code | $? |
| Current PID | $$ |
| Background PID | $! |
| Shift | shift or shift N |
Key takeaways:
"$@"preserves argument boundaries. The quoted form passes each argument as a separate word, with spaces preserved. The unquoted form splits on whitespace. The"$*"form joins the arguments into a single word .- The
${10}form is required for double-digit parameters. The$10is interpreted as$1followed by0. The braces make the parameter name explicit. shiftconsumes arguments. The builtin removes the first positional parameter and shifts the rest. Thewhile [ $# -gt 0 ]loop withshiftis the standard pattern for processing all arguments.$?is the exit code of the most recent command. It must be captured immediately after the command. Any intervening command resets it.$$is the current shell PID. It is used for unique temporary file names. Thetrapcommand withEXIThandles cleanup on any exit path.
Remember: The special parameters are the shell’s vocabulary for its own execution context. The $0 is the name, the $# is the count, the $@ is the list, the $? is the result. The "$@" form is the safe way to pass arguments. The shift is the way to consume them. The exam tests the difference between the quoted and unquoted forms, and the difference between "$@" and "$*".
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!