Scripts become far more useful when they take arguments. Instead of editing the script, you run ./backup.sh shop --keep 14. Bash gives you positional parameters for simple cases and getopts (or a while/case loop) for proper options.

Positional parameters

#!/usr/bin/env bash
echo "Script: $0"
echo "First:  $1"
echo "Second: $2"
echo "Count:  $#"
echo "All:    $*"
./args.sh shop /var/backups
  • $1–$9 are the arguments. From the tenth on, use braces: ${10}.
  • $# is the number of arguments.
  • $0 is the script’s name as it was called.

“$@” vs “$*”

for a in "$@"; do echo "[$a]"; done

"$@" expands to each argument as a separate word, keeping spaces inside arguments intact. That’s what you want for passing arguments on to another command:

exec node server.js "$@"

"$*" joins everything into one string. Unquoted $@ and $* split on spaces. Avoid them.

Required arguments and usage messages

usage() {
  echo "usage: $(basename "$0") <database> [backup-dir]" >&2
  exit 2
}

(( $# >= 1 )) || usage
db="$1"
dest="${2:-/var/backups}"

Or in one line, with an automatic error:

db="${1:?usage: $0 <database> [dir]}"

shift

shift drops $1 and moves everything down: $2 becomes $1, and so on. shift 2 drops two.

action="$1"; shift
case "$action" in
  start) start_service "$@" ;;
  stop)  stop_service "$@" ;;
  *)     usage ;;
esac

This is the usual structure for scripts with subcommands, like ./manage.sh deploy v1.2.

Options with a while/case loop

This approach handles short and long options in any order, and is the most flexible:

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

keep=7
dry_run=0
verbose=0

usage() {
  cat >&2 <<EOF
usage: $(basename "$0") [options] <database>...
  -k, --keep DAYS   keep backups for DAYS days (default $keep)
  -n, --dry-run     show what would happen
  -v, --verbose     more output
  -h, --help        this help
EOF
  exit "${1:-2}"
}

while (( $# )); do
  case "$1" in
    -k|--keep)   keep="${2:?--keep needs a value}"; shift 2 ;;
    --keep=*)    keep="${1#*=}"; shift ;;
    -n|--dry-run) dry_run=1; shift ;;
    -v|--verbose) verbose=1; shift ;;
    -h|--help)   usage 0 ;;
    --)          shift; break ;;
    -*)          echo "unknown option: $1" >&2; usage ;;
    *)           break ;;
  esac
done

(( $# >= 1 )) || usage
for db in "$@"; do
  (( verbose )) && echo "backing up $db (keep $keep days)"
  (( dry_run )) || backup "$db"
done

-- marks the end of options. Everything after it is treated as a plain argument, even if it starts with -.

getopts (short options only)

getopts is built into the shell and handles combined flags like -nv, plus option values:

keep=7; dry_run=0
while getopts ":k:nvh" opt; do
  case "$opt" in
    k) keep="$OPTARG" ;;
    n) dry_run=1 ;;
    v) verbose=1 ;;
    h) usage 0 ;;
    :) echo "-$OPTARG needs a value" >&2; usage ;;
    \?) echo "unknown option -$OPTARG" >&2; usage ;;
  esac
done
shift $((OPTIND - 1))
  • The letters in ":k:nvh" are the allowed options. A : after a letter means it takes a value.
  • The leading : turns on silent error handling, so you can print your own messages.
  • After the loop, shift $((OPTIND - 1)) leaves only the non-option arguments.

getopts doesn’t support long options (--keep). The external getopt command (GNU) does, but it’s less portable. For most scripts, the while/case loop above is simplest.

Validate values

[[ "$keep" =~ ^[0-9]+$ ]] || { echo "--keep must be a number" >&2; exit 2; }
[[ -d "$dest" ]] || { echo "no such directory: $dest" >&2; exit 2; }

Arguments from the environment

For cron jobs and containers, environment variables with defaults are often more convenient than flags:

KEEP_DAYS="${KEEP_DAYS:-7}"
BACKUP_DIR="${BACKUP_DIR:-/var/backups}"

Run it as KEEP_DAYS=30 ./backup.sh shop. Many scripts support both, letting flags override the environment.

Exit codes

Follow conventions so other tools understand your script: 0 for success, 1 for general errors, and 2 for usage errors. See Bash error handling.

Quick reference

Syntax Meaning
$1, ${10} Positional arguments
$# Argument count
"$@" All arguments, separately quoted
shift / shift 2 Drop leading arguments
${1:-default} Default value
${1:?msg} Required, error if missing
getopts "ab:" opt Parse -a, -b value
-- End of options

Related: Bash functions, read input and if/else and case.

  • Bash
  • Scripting
  • Linux