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–$9are the arguments. From the tenth on, use braces:${10}.$#is the number of arguments.$0is 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.