| |

LFCS 122 ๐Ÿง Shell Scripting โ€” Special Parameters

โฑ๏ธ Estimated read: 9 min | |

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

ParameterValue
$0Script name
$1 โ€ฆ $9Positional parameters
${10}Double-digit parameter
$#Argument count
$@All arguments (separate)
$*All arguments (joined)

Process and Exit

ParameterValue
$?Exit code of last command
$$Current shell PID
$!PID of last background job
$-Current shell options

shift Builtin

SyntaxEffect
shiftRemove first parameter
shift NRemove N parameters

@ vs $

FormBehavior
"$@"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

PitfallWhy It HappensFix
Filename with space failsUnquoted $@"$@"
Tenth argument wrongUsed $10${10}
Exit code lostCommand between $? captureCapture immediately
Temp file collisionUsed fixed nameUse $$
Loop infiniteshift missingAdd 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

ItemValue
Script name$0
Positional parameters$1, $2, …, ${10}
Argument count$#
Separate arguments"$@"
Joined arguments"$*"
Exit code$?
Current PID$$
Background PID$!
Shiftshift 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 $10 is interpreted as $1 followed by 0. The braces make the parameter name explicit.
  • shift consumes arguments. The builtin removes the first positional parameter and shifts the rest. The while [ $# -gt 0 ] loop with shift is 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. The trap command with EXIT handles 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 "$*".

Was this helpful?


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!