Let's face it, positional arguments such as $1 and $2 are neither descriptive nor flexible. There is a better way to supply arguments: with this simple trick you'll get --named arguments --in your-script, which is waaaay better 🤓.
Parsing script arguments
The following script declares its argument variables and lists their names in a readonly whitelist. Before assigning a value, is_allowed_argument checks that the name is both whitelisted and declared:
declare service_name="" tag="" namespace="" env=""
declare -ar allowed_arguments=(service_name tag namespace env)
is_allowed_argument() {
local candidate=$1
local allowed
for allowed in "${allowed_arguments[@]}"; do
if [[ $candidate == "$allowed" ]]; then
declare -p -- "$candidate" &>/dev/null
return
fi
done
return 1
}
die() {
printf "Script failed: %s\n" "$1" >&2
exit 1
}
while [ $# -gt 0 ]; do
if [[ $1 == "--"* ]]; then
if ! is_allowed_argument "${1#--}"; then
die "Unknown parameter: $1"
fi
if (( $# < 2 )); then
die "Missing value for $1"
fi
if [[ $2 == "--"* ]]; then
die "Missing value for $1"
fi
printf -v "${1#--}" "%s" "$2"
shift 2
else
die "Unexpected positional argument: $1"
fi
done
echo "service_name: '$service_name' env: '$env'"When we execute the script with ./test.sh --service_name catalog --env test, it returns:
$ ./test.sh --service_name catalog --env test
service_name: 'catalog' env: 'test'The parser accepts only --name value arguments whose names are declared and included in allowed_arguments. It rejects unknown options, missing values, and positional arguments. Values cannot start with --, because the parser treats them as another option. If you want to support flags without values, check Support for --help?
Argument validation
First, let's define a usage function to show how our script works:
function usage {
echo ""
echo "Deploys an ECR image to Atlas using GitOps and ArgoCD."
echo ""
echo "usage: $0 --service_name string --tag string --namespace string --env string "
echo ""
echo " --service_name string name of the service"
echo " (example: blaze-search-term-redirect-service)"
echo " --tag string tag of the image to deploy"
echo " (example: 804-a325d6a)"
echo " --namespace string namespace of the cluster"
echo " (example: eos)"
echo " --env string env to which to deploy the tag"
echo " (example: dev)"
echo ""
}Now, let's check if the parameters are supplied. If one of the parameters is missing, display usage and terminate the script. We use the -z conditional expression to check if the variable is empty:
if [[ -z $service_name ]]; then
usage
die "Missing parameter --service_name"
elif [[ -z $tag ]]; then
usage
die "Missing parameter --tag"
elif [[ -z $namespace ]]; then
usage
die "Missing parameter --namespace"
elif [[ -z $env ]]; then
usage
die "Missing parameter --env"
fiSupport for --help?
The easiest way to add --help is to handle it in the loop that turns arguments into variables:
while [ $# -gt 0 ]; do
if [[ $1 == "--help" ]]; then
usage
exit 0
elif [[ $1 == "--"* ]]; then
if ! is_allowed_argument "${1#--}"; then
die "Unknown parameter: $1"
fi
if (( $# < 2 )); then
die "Missing value for $1"
fi
if [[ $2 == "--"* ]]; then
die "Missing value for $1"
fi
printf -v "${1#--}" "%s" "$2"
shift 2
else
die "Unexpected positional argument: $1"
fi
doneAny flag that does not take a value can be added to this loop.
Write better scripts: use ShellCheck
The article Please stop writing shell scripts makes some excellent points on why writing a Bash script is hard for programmers: bash behavior is most likely not like the programming language you're familiar with. The ShellCheck extension for Visual Studio Code will help you catch some of the pitfalls. ShellCheck reports warnings with codes such as SC2086; its wiki explains what each code means:
Complete example
Putting everything together gives us the following complete script:
#!/usr/bin/env bash
set -euo pipefail
declare service_name="" tag="" namespace="" env=""
declare -ar allowed_arguments=(service_name tag namespace env)
is_allowed_argument() {
local candidate=$1
local allowed
for allowed in "${allowed_arguments[@]}"; do
if [[ $candidate == "$allowed" ]]; then
declare -p -- "$candidate" &>/dev/null
return
fi
done
return 1
}
usage() {
echo ""
echo "Deploys an ECR image to Atlas using GitOps and ArgoCD."
echo ""
echo "usage: $0 --service_name string --tag string --namespace string --env string"
echo ""
echo " --service_name string name of the service"
echo " (example: blaze-search-term-redirect-service)"
echo " --tag string tag of the image to deploy"
echo " (example: 804-a325d6a)"
echo " --namespace string namespace of the cluster"
echo " (example: eos)"
echo " --env string env to which to deploy the tag"
echo " (example: dev)"
echo " --help show this help"
echo ""
}
die() {
printf "Script failed: %s\n" "$1" >&2
exit 1
}
while [ $# -gt 0 ]; do
if [[ $1 == "--help" ]]; then
usage
exit 0
elif [[ $1 == "--"* ]]; then
if ! is_allowed_argument "${1#--}"; then
die "Unknown parameter: $1"
fi
if (( $# < 2 )); then
die "Missing value for $1"
fi
if [[ $2 == "--"* ]]; then
die "Missing value for $1"
fi
printf -v "${1#--}" "%s" "$2"
shift 2
else
die "Unexpected positional argument: $1"
fi
done
if [[ -z $service_name ]]; then
usage
die "Missing parameter --service_name"
elif [[ -z $tag ]]; then
usage
die "Missing parameter --tag"
elif [[ -z $namespace ]]; then
usage
die "Missing parameter --namespace"
elif [[ -z $env ]]; then
usage
die "Missing parameter --env"
fi
printf "Deploying %s:%s to %s/%s\n" \
"$service_name" "$tag" "$env" "$namespace"Conclusion
Adding named arguments to a bash script is pretty easy. Declaring the argument variables and listing their names in an explicit whitelist prevents mistyped options and unrelated shell variables from being changed.
Changelog
- Restricted named arguments to declared variables in an explicit allowlist, added argument validation and a strict-mode complete example, clarified parser limitations, and corrected references.
- Moved the reusable
lib.shexample to Bash script with a lib for named parameters. - Shortened parameter validation using
elif. - Initial article.