| |

LFCS 126 ๐Ÿง Shell Scripting โ€” Arguments and Parsing

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

The previous chapters covered positional parameters, special variables, and conditionals. This chapter focuses on the practical problem of argument parsing: how a script accepts options like -v, --verbose, or -f file, validates them, and behaves accordingly. The LFCS exam tests the getopts builtin, the manual while/case/shift pattern, and the difference between short options (-v) and long options (--verbose). The critical point is that getopts handles short options only. Long options require a manual loop or an external tool like getopt (not getopts). The exam tests the getopts syntax because it is POSIX and available everywhere.

Key point: getopts is a shell builtin that parses short options. The syntax is while getopts "vf:" opt; do ... done. The first argument is the option string: letters that take no argument, and letters followed by a colon that take an argument. The second argument is the variable name that getopts sets on each iteration. The $OPTARG variable holds the argument value. The $OPTIND variable holds the index of the next argument. The getopts command returns false when there are no more options to parse.


Why argument parsing requires a specific pattern

The positional-only problem. A script that accepts only positional arguments (./script.sh arg1 arg2) has no way to express optional flags or named parameters. The caller must know the exact order, and adding a new option changes the interface for every caller. Flags (-v, -f file) are the standard interface for scripts with configurable behavior.

The manual parsing problem. A script can parse flags manually with a while/case/shift loop. This works for both short and long options, but the logic is error-prone: missing values, unknown flags, and combined flags (-vf) must be handled explicitly. The getopts builtin handles the common cases: short options, option values, and error reporting. It does not handle long options (--verbose), which require a manual loop or the external getopt command.

The validation problem. An argument may be a file path, a number, or a domain name. The script must validate the value before using it. The getopts builtin does not validate the value; it only extracts it. The validation is the script’s responsibility. The -f option that expects a filename must check that the file exists. The -n option that expects a number must check that the value is numeric.

The help problem. Every script with options should provide a -h or --help flag that prints the usage. The usage message documents the options and the expected arguments. The getopts pattern handles -h like any other flag: the script prints the usage and exits.

The trade-off. getopts is POSIX and available everywhere. It handles short options only. The external getopt command (with the --longoptions flag) handles long options but is not POSIX and has different behavior on different systems. A manual while/case/shift loop handles anything but requires more code. The exam tests getopts because it is the standard tool.


a. The getopts syntax and the option string

The getopts command is a builtin. It parses the positional parameters for options. The first argument is the option string. The second is the variable name that receives the option letter on each iteration.

#!/bin/bash
while getopts "vf:" opt; do
  case "$opt" in
    v) verbose=true ;;
    f) file="$OPTARG" ;;
    \?) echo "Invalid option: -$OPTARG" >&2; exit 1 ;;
    :) echo "Option -$OPTARG requires an argument" >&2; exit 1 ;;
  esac
done

The option string "vf:" declares two options. The v is a flag that takes no argument. The f: is an option that takes an argument. The colon after f is the marker. The option string can also start with a colon, which changes the error handling: ":vf:" makes getopts silent and sets $OPTARG to the problematic option.

The variable $OPTARG holds the value of the option’s argument. The variable $OPTIND holds the index of the next argument to be parsed. After the loop, the script can access the remaining non-option arguments with shift $((OPTIND - 1)).

shift $((OPTIND - 1))
echo "Remaining arguments: $@"

The $OPTIND starts at 1. The $((OPTIND - 1)) is the number of arguments consumed by the options. The shift removes them, leaving the non-option arguments in $@.


b. The manual loop for long options

getopts does not handle long options. The manual while/case/shift loop is the standard pattern for long options. The loop checks the first argument, processes it, and shifts.

#!/bin/bash
verbose=false
file=""

while [ $# -gt 0 ]; do
  case "$1" in
    -v|--verbose)
      verbose=true
      shift
      ;;
    -f|--file)
      file="$2"
      shift 2
      ;;
    -h|--help)
      echo "Usage: $0 [-v] [-f file]"
      exit 0
      ;;
    --)
      shift
      break
      ;;
    -*)
      echo "Unknown option: $1" >&2
      exit 1
      ;;
    *)
      break
      ;;
  esac
done

echo "Verbose: $verbose"
echo "File: $file"
echo "Remaining: $@"

The -- marker terminates option parsing. Any arguments after -- are treated as positional arguments, even if they start with -. The -* pattern catches any unrecognized option and exits with an error. The *) pattern breaks the loop when the first non-option argument is encountered.

The shift 2 for the -f option consumes both the flag and its value. The shift for the -v option consumes only the flag. The shift for the -- marker consumes the marker itself.


c. Validation and the help flag

The script must validate the values it receives. The -f option that expects a filename must check that the file exists. The -n option that expects a number must check that the value is numeric. The validation is the script’s responsibility, not getopts.

#!/bin/bash
file=""
count=0

while getopts "f:n:h" opt; do
  case "$opt" in
    f) file="$OPTARG" ;;
    n) count="$OPTARG" ;;
    h)
      echo "Usage: $0 [-f file] [-n count] [-h]"
      exit 0
      ;;
    \?) echo "Invalid option" >&2; exit 1 ;;
  esac
done

# Validate the file
if [ -n "$file" ] && [ ! -f "$file" ]; then
  echo "Error: file not found: $file" >&2
  exit 1
fi

# Validate the count
if ! [[ "$count" =~ ^[0-9]+$ ]]; then
  echo "Error: count must be a number" >&2
  exit 1
fi

echo "File: $file"
echo "Count: $count"

The file validation checks that the file exists. The count validation uses a regex to check that the value is numeric. The -h option prints the usage and exits before the validation.

The usage message should be printed to stdout for the -h flag (a successful exit) and to stderr for an error (a non-zero exit). The >&2 redirect sends the error message to stderr.


Complete Example Session

# ============================================
# PART 1: BASIC GETOPTS
# ============================================

cat > basic.sh <<'EOF'
#!/bin/bash
while getopts "vf:" opt; do
  case "$opt" in
    v) echo "Verbose mode" ;;
    f) echo "File: $OPTARG" ;;
  esac
done
EOF
chmod +x basic.sh
./basic.sh -v -f myfile.txt
# Output:
# Verbose mode
# File: myfile.txt
# ============================================
# PART 2: INVALID OPTION
# ============================================

./basic.sh -x
# Output: ./basic.sh: illegal option -- x
# ============================================
# PART 3: MISSING ARGUMENT
# ============================================

./basic.sh -f
# Output: ./basic.sh: option requires an argument -- f
# ============================================
# PART 4: SILENT MODE
# ============================================

cat > silent.sh <<'EOF'
#!/bin/bash
while getopts ":vf:" opt; do
  case "$opt" in
    v) echo "Verbose" ;;
    f) echo "File: $OPTARG" ;;
    \?) echo "Invalid: -$OPTARG" ;;
    :) echo "Missing: -$OPTARG" ;;
  esac
done
EOF
chmod +x silent.sh
./silent.sh -x
# Output: Invalid: -x
# ============================================
# PART 5: REMAINING ARGUMENTS
# ============================================

cat > remaining.sh <<'EOF'
#!/bin/bash
while getopts "v" opt; do
  case "$opt" in
    v) echo "Verbose" ;;
  esac
done
shift $((OPTIND - 1))
echo "Remaining: $@"
EOF
chmod +x remaining.sh
./remaining.sh -v file1 file2
# Output:
# Verbose
# Remaining: file1 file2
# ============================================
# PART 6: LONG OPTIONS MANUAL
# ============================================

cat > long.sh <<'EOF'
#!/bin/bash
verbose=false
file=""
while [ $# -gt 0 ]; do
  case "$1" in
    -v|--verbose) verbose=true; shift ;;
    -f|--file) file="$2"; shift 2 ;;
    -h|--help) echo "Usage: $0 [-v] [-f file]"; exit 0 ;;
    --) shift; break ;;
    -*) echo "Unknown: $1" >&2; exit 1 ;;
    *) break ;;
  esac
done
echo "Verbose: $verbose"
echo "File: $file"
EOF
chmod +x long.sh
./long.sh --verbose --file myfile.txt
# Output:
# Verbose: true
# File: myfile.txt
# ============================================
# PART 7: FILE VALIDATION
# ============================================

cat > validate.sh <<'EOF'
#!/bin/bash
file=""
while getopts "f:" opt; do
  case "$opt" in
    f) file="$OPTARG" ;;
  esac
done
if [ -n "$file" ] && [ ! -f "$file" ]; then
  echo "File not found: $file" >&2
  exit 1
fi
echo "OK"
EOF
chmod +x validate.sh
./validate.sh -f /nonexistent
# Output: File not found: /nonexistent
# ============================================
# PART 8: NUMERIC VALIDATION
# ============================================

cat > numeric.sh <<'EOF'
#!/bin/bash
count=0
while getopts "n:" opt; do
  case "$opt" in
    n) count="$OPTARG" ;;
  esac
done
if ! [[ "$count" =~ ^[0-9]+$ ]]; then
  echo "Count must be numeric" >&2
  exit 1
fi
echo "Count: $count"
EOF
chmod +x numeric.sh
./numeric.sh -n abc
# Output: Count must be numeric
# ============================================
# PART 9: HELP FLAG
# ============================================

cat > help.sh <<'EOF'
#!/bin/bash
while getopts "h" opt; do
  case "$opt" in
    h) echo "Usage: $0 [-h]"; exit 0 ;;
  esac
done
EOF
chmod +x help.sh
./help.sh -h
# Output: Usage: ./help.sh [-h]

The nine parts trace the argument parsing: basic getopts, invalid option, missing argument, silent mode, remaining arguments, long options, file validation, numeric validation, and help flag.


Quick Reference

getopts Syntax

ComponentPurpose
"vf:"Option string
vFlag (no argument)
f:Option with argument
optVariable that receives the letter
$OPTARGArgument value
$OPTINDNext argument index

Option String Modifiers

ModifierEffect
Leading :Silent error handling
Trailing :Option requires argument
No :Flag only

Manual Long Option Pattern

PatternEffect
-v|--verboseBoth forms
shiftConsume one
shift 2Consume two
--Terminate parsing
-*Unknown short option
*)First non-option

Validation

CheckMethod
File exists[ -f "$file" ]
Numeric[[ "$n" =~ ^[0-9]+$ ]]
Non-empty[ -n "$var" ]

Best Practices

โœ… Do This:

while getopts "vf:" opt; do
<!-- Use getopts for short options -->               # โœ…
shift $((OPTIND - 1))
<!-- Remove parsed options before accessing args -->  # โœ…
if ! [[ "$count" =~ ^[0-9]+$ ]]; then
<!-- Validate numeric input -->                      # โœ…

โŒ Don’t Do This:

# Use getopts for long options
<!-- getopts does not support --verbose -->          # โŒ
# Assume $OPTARG is valid
<!-- Validate before use -->                         # โŒ
# Forget to shift after getopts
<!-- Remaining args include parsed options -->        # โŒ

Common Pitfalls

PitfallWhy It HappensFix
Long options ignoredgetopts only handles shortManual loop
$OPTARG emptyMissing argumentCheck with -z
Unknown option errorNo \? caseAdd the case
Remaining args wrongNo shiftshift $((OPTIND - 1))
File not validatedNo check[ -f "$file" ]

Real-World Examples

1. Basic getopts

while getopts "vf:" opt; do case "$opt" in v) V=1;; f) F="$OPTARG";; esac; done

2. Silent Mode

while getopts ":vf:" opt; do case "$opt" in :) echo "Missing -$OPTARG";; esac; done

3. Remaining Arguments

shift $((OPTIND - 1))
echo "$@"

4. Long Options

while [ $# -gt 0 ]; do case "$1" in --verbose) V=1; shift;; esac; done

5. -- Terminator

case "$1" in --) shift; break;; esac

6. Unknown Option

case "$1" in -*) echo "Unknown"; exit 1;; esac

7. File Validation

if [ ! -f "$file" ]; then exit 1; fi

8. Numeric Validation

if ! [[ "$n" =~ ^[0-9]+$ ]]; then exit 1; fi

9. Help Flag

case "$opt" in h) echo "Usage: ..."; exit 0;; esac

10. Combined Flags

./script.sh -vf file

Visual

getopts Flow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  while getopts "vf:" opt                     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ”‚
                   โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  $opt = "v" โ†’ case "$opt" in v)              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ”‚
                   โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  $opt = "f" โ†’ $OPTARG = "file"               โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ”‚
                   โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  $opt = "?" โ†’ unknown option                 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ”‚
                   โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  getopts returns false โ†’ loop ends           โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

OPTIND Shift

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  ./script.sh -v -f file arg1 arg2            โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ”‚
                   โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  After getopts loop: $OPTIND = 5             โ”‚
โ”‚  (consumed -v, -f, file = 3 args + 1)        โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ”‚
                   โ”‚ shift $((OPTIND - 1))
                   โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  $@ = arg1 arg2                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
Short option parsergetopts
Option string"vf:"
FlagLetter without colon
Option with argLetter followed by colon
Argument value$OPTARG
Next index$OPTIND
Remaining argsshift $((OPTIND - 1))
Long optionsManual while/case
Terminator--

Key takeaways:

  • getopts handles short options. The option string declares which letters are flags and which take arguments. The $OPTARG holds the argument value. The $OPTIND tracks the position.
  • Long options require a manual loop. getopts does not support --verbose. The while/case/shift pattern handles both short and long options.
  • The -- terminator ends option parsing. Any argument after -- is treated as a positional argument, even if it starts with -.
  • Validation is the script’s responsibility. getopts extracts the value; the script must check that the value is valid. The file existence check and the numeric check are the standard validations.
  • The -h flag is the standard help. The script prints the usage and exits with 0. The usage message should be concise and list the options.

Remember: The getopts builtin is the POSIX tool for short options. The manual loop is the pattern for long options. The $OPTARG is the value. The $OPTIND is the position. The shift is the cleanup. The case is the dispatch. The exam tests the syntax and the distinction between short and long options.

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!