Skip to content

Instantly share code, notes, and snippets.

@colemar
Last active December 4, 2025 14:25
Show Gist options
  • Select an option

  • Save colemar/1a2bd0f22413ef671572f83e80153c6b to your computer and use it in GitHub Desktop.

Select an option

Save colemar/1a2bd0f22413ef671572f83e80153c6b to your computer and use it in GitHub Desktop.
Named parameters processing for bash
# This function might seem like an over-engineered approach to the problem of accepting named arguments,
# but it ultimately proves to be a flexible, secure, simple-to-use, and reasonably efficient solution.
#
# Usage Protocol for the get_args helper function:
#
# A generic function func() (or main program) must adhere to the following contract:
# 1. Define Parameter Specifications: The function must declare an associative array named 'params'.
# This array serves two purposes: it acts as a whitelist of accepted parameter names and defines their default values.
# If a parameter has '?' as default value it is deemed as required. Default value '?' is forbidden for optional parameters.
# 2. Consume and shift out all initial fixed positional arguments.
# 3. Invoke the Parser: The function must execute the helper within an eval statement using command substitution:
# eval "$(get_args "$@")". Double quotes around the substitution are mandatory to prevent accidental side effects.
# 4. Variable Instantiation: Upon execution of the eval line, readonly local variables corresponding to the keys in params
# will be automatically initialized within the function's scope.
# These variables will contain either the value passed by the caller or the default value defined in the array.
# 5. The array params will be unaffected, since command substitution creates a copy for get_args().
# The array cannot be initially declared as readonly (-r), because the copy will be also readonly and get_args()
# needs to manipulate it.
#
# Arguments of func() can be specified as:
# - name=value (Standard form: sets variable 'name' to 'value')
# - name (Flag form: equivalent to name=name, useful for boolean checks like [[ $name ]])
#
# Error Handling:
# - If unexpected keyword arguments are passed, get_args prints an error for each one,
# completes parsing, then returns 1. The caller should handle this error (e.g., with || return 1).
# - If a required keyword argument is missing, get_args prints an error message and terminates the script with exit 1.
# This is a fatal programming error - the script cannot continue without required parameters.
#
# Canonical Example:
#
# func() {
# local -A params=([tmout]=10 [file]=? [retr]= [verbose]=)
# eval "$(get_args "$@")" || return 1 # Handle unexpected arguments
# echo "Processing '$file' with timeout ${tmout}s (Attempt 1/$retr)..."
# [[ $verbose ]] && date
# }
#
# func file=input.txt # Processing 'input.txt' with timeout 10s (Attempt 1/)...
# func file=input.txt verbose # Same as above + Wed Dec 3 01:41:28 AM CET 2025
# func retr=2 file=a.txt # Processing 'a.txt' with timeout 10s (Attempt 1/2)...
# func 'file=w s.txt' # Processing 'w s.txt' with timeout 10s (Attempt 1/)...
# func file='w s.txt' # Same as above.
# func fi'le=w s.txt' # Same as above.
# func file=a.txt retr=5 tmout=19 # Processing 'a.txt' with timeout 19s (Attempt 1/5)...
# func file=a.txt retr=2 foo=1 # ERROR: func() got an unexpected keyword argument 'foo' # eval exit code is 1
# func retr=2 tmout=1 # FATAL ERROR: func() missing required keyword argument 'file' # script terminates
#
get_args() {
local arg key buf= err= caller="${FUNCNAME[1]}()"
local -i n=1
for arg; do
key=${arg%%=*}
if [[ -v params[$key] ]]; then
buf+=" $key=\${$n#*=}"
unset params[$key]
else
echo "ERROR: $caller got an unexpected keyword argument '$key'" >&2
err=1
fi
((n++))
done
for key in "${!params[@]}"; do
if [[ ${params[$key]} != \? ]]; then
buf+=" $key=\${params[$key]}"
else
echo "FATAL ERROR: $caller missing required keyword argument '$key'" >&2
echo "exit 1"; return 1
fi
done
echo "${buf:+local -r}$buf${err:+$'\n'false}"
}
# Minimalist argument parser helper.
# Maps arguments of the form 'name=value' to readonly local variables prefixed with 'p_'.
#
# Usage Protocol:
# 1. Consume and shift out all initial fixed positional arguments.
# 2. Define default values for optional parameters as local variables with 'p_' prefix.
# 3. Execute: eval "$(get_params "$@")" to parse and overwrite defaults.
#
# Argument Syntax:
# - name=value -> Sets p_name to "value".
# - name -> Sets p_name to "name" (Flag mode, equivalent to name=name).
#
# Constraints & Responsibilities:
# 1. Caller: Must ensure passed argument keys are valid Bash identifiers (alphanumeric/underscore)
# to avoid syntax errors during assignment.
# 2. Function Implementation: Must NOT use local variables starting with 'p_' for internal logic,
# as this namespace is reserved for the generated parameters.
#
# Example:
# func() {
# shift 1 # Remove fixed arguments if any
# local p_timeout=10 p_verbose= # Define defaults (mutable)
# eval "$(get_params "$@")" # Parse arguments, override defaults and lock variables (readonly)
#
# echo "Timeout: $p_timeout"
# [[ $p_verbose ]] && echo "Verbose active"
# }
# func arg1 timeout=30 verbose
#
get_params() {
local arg key buf=
local -i n=1
for arg; do
key=${arg%%=*}
buf+=" p_$key=\${$n#*=}"
((n++))
done
echo "${buf:+local -r}$buf"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment