Skip to content
DevOps devops shell 5 min read

Handling Command-Line Arguments

Most useful scripts need to be told what to do. Instead of editing the script every time, you pass it information on the command line — like ./backup.sh /var/www or ./deploy.sh --env production. These extra words are called command-line arguments (the values you type after the script name). Learning to read and validate them is the difference between a one-off hack and a reusable tool you can run on any Ubuntu server.

Positional parameters: $0, $1, $2…

When you run a script, Bash automatically stores each word you typed into special variables called positional parameters (variables numbered by their position on the line). They are read-only inside the script.

  • $0 — the name of the script itself (how it was called).
  • $1 to $9 — the first through ninth arguments.
  • ${10} and beyond — for the tenth argument onward you must use braces, or Bash reads it as $1 followed by 0.

Create a file called args.sh:

#!/usr/bin/env bash
echo "Script name : $0"
echo "First arg   : $1"
echo "Second arg  : $2"

Make it executable and run it:

chmod +x args.sh
./args.sh hello world

Output:

Script name : ./args.sh
First arg   : hello
Second arg  : world

Counting and listing arguments: $#, $@, $*

Two more special variables help you handle any number of arguments without knowing the count ahead of time.

VariableMeaningWhen to use
$#The number of arguments passedCheck whether the user gave enough input
$@All arguments as a separate listLooping over arguments — almost always what you want
$*All arguments as one single stringRare; only when you truly want them joined

Always write "$@" inside double quotes. Quoted "$@" keeps each argument intact, so a filename with spaces like "my report.txt" stays as one item. Unquoted $@ or $* will split that into two broken arguments.

#!/usr/bin/env bash
echo "You passed $# argument(s)."
for file in "$@"; do
  echo "Processing: $file"
done
./args.sh one.txt "two files.txt"

Output:

You passed 2 argument(s).
Processing: one.txt
Processing: two files.txt

Shifting arguments with shift

The shift command drops $1 and moves every other argument down one position: the old $2 becomes $1, the old $3 becomes $2, and so on. $# decreases by one each time. This is handy when the first argument is a command and the rest are its options.

#!/usr/bin/env bash
action="$1"   # grab the first argument
shift         # now $@ holds only the remaining arguments
echo "Action requested: $action"
echo "Remaining items: $@"
./args.sh delete file1 file2 file3

Output:

Action requested: delete
Remaining items: file1 file2 file3

A real script: require and validate an argument

Good scripts fail fast and explain themselves. The pattern below prints a usage message (a short line showing how to call the script) and exits with a non-zero code when the input is wrong. Exit code 0 means success; anything else signals an error to whatever called the script.

Here is a script that takes one required argument — a directory to back up — and validates that it actually exists:

#!/usr/bin/env bash
set -euo pipefail

usage() {
  echo "Usage: $0 <directory>"
  echo "  Creates a timestamped tar.gz backup of <directory>."
  exit 1
}

# Require exactly one argument
if [ "$#" -ne 1 ]; then
  echo "Error: expected 1 argument, got $#." >&2
  usage
fi

target="$1"

# Validate that it is a real directory
if [ ! -d "$target" ]; then
  echo "Error: '$target' is not a directory." >&2
  exit 1
fi

stamp="$(date +%Y%m%d-%H%M%S)"
archive="/var/backups/$(basename "$target")-$stamp.tar.gz"

tar -czf "$archive" "$target"
echo "Backup created: $archive"

Run it with no argument and it stops politely:

./backup.sh

Output:

Error: expected 1 argument, got 0.
Usage: ./backup.sh <directory>
  Creates a timestamped tar.gz backup of <directory>.

Send error messages to stderr (the standard error stream) by adding >&2. This keeps errors separate from normal output, so logs and pipelines stay clean.

Parsing flags with getopts

Positional arguments work well for one or two values, but real tools use flags (short options like -v or -o file). The built-in getopts parses these for you, handles them in any order, and reports bad input.

In the string "vo:h", each letter is an option you accept. A colon after a letter (o:) means that option requires a value, which getopts places in the variable $OPTARG.

#!/usr/bin/env bash
set -euo pipefail

verbose=false
output="result.txt"

usage() {
  echo "Usage: $0 [-v] [-o output_file] <input>"
  exit 1
}

while getopts "vo:h" opt; do
  case "$opt" in
    v) verbose=true ;;
    o) output="$OPTARG" ;;
    h) usage ;;
    *) usage ;;   # unknown flag
  esac
done

# Remove the parsed flags, leaving only positional args
shift "$((OPTIND - 1))"

if [ "$#" -lt 1 ]; then
  echo "Error: missing input file." >&2
  usage
fi

input="$1"
$verbose && echo "Verbose mode on."
echo "Reading from '$input', writing to '$output'."
./tool.sh -v -o report.txt data.csv

Output:

Verbose mode on.
Reading from 'data.csv', writing to 'report.txt'.

The line shift "$((OPTIND - 1))" is essential: getopts counts how many flags it read in $OPTIND, and shifting by that amount leaves only the real positional arguments (here, data.csv) in $@.

When to use which: reach for plain $1/$2 when a script takes one or two fixed inputs. Use getopts once you have optional behaviour, on/off switches, or values that can appear in any order. For long flags like --verbose, getopts does not support them — write a manual while/case loop or use a higher-level language.

Best practices

  • Validate early: check $# and the type of each input before doing real work.
  • Always provide a usage() function and call it on bad input or -h.
  • Quote every expansion — "$1", "$@", "$OPTARG" — to survive spaces and special characters.
  • Exit with a non-zero code on errors and send messages to stderr with >&2.
  • Use getopts for flags; never hand-roll fragile string matching for short options.
  • Put set -euo pipefail near the top so undefined variables and failed commands stop the script instead of running on broken data.
Last updated June 15, 2026
Was this helpful?