Skip to content

Instantly share code, notes, and snippets.

@felipe-gustavo
Last active May 28, 2024 06:17
Show Gist options
  • Save felipe-gustavo/4fedc6174ec74a17591c14a58160bf64 to your computer and use it in GitHub Desktop.
Save felipe-gustavo/4fedc6174ec74a17591c14a58160bf64 to your computer and use it in GitHub Desktop.
Parse args and options for Bash and Zsh

Argsparser Bash and Zsh Script

This script provides a robust and flexible way to parse command-line arguments in Bash or Zsh. It supports various features such as required options, array options, and next argument assignable options. The script is designed following best practices for modularity and maintainability.

Requirements

ZSH or BASH shells

  • zsh@^5
  • bash@^4.20

Usage

Function: argsparser

The argsparser function parses command-line arguments based on predefined options and variables. It handles required options, array options, and options that take their values from the next argument.

Script Structure

  1. Utility Functions:

    • array_includes_value: Checks if an array includes a specific value.
    • escape_double_quote: Escapes double quotes in a string.
    • check_var_name: Validates a variable name.
    • split_arg_value: Splits a string by = and retrieves the specified field.
    • show_formatted_required_parameters: Shows formatted required parameters.
  2. Main Function:

    • argsparser: Parses the command-line arguments.

Arguments Format

Arguments are defined with specific formats:

  • option:=variable_name for required options.
  • option+=variable_name for array options.
  • option~=variable_name for options whose value is the next argument (next argument assign doesn't work with array options).
  • option+:=variable_name for array and required options.

Never provide the first dash to single dash argument, option on definition would -option on usage. To arguments containing double dash, only provide one, -option on definition would --option on usage.

NOTE: options shouldn't contain :, + or ~ characters on its name.

Usage Helper (Optional)

This script offers you the option to show the usage description in case of the user add --help or -h argument to your script, e.g.: main --help. Also, in case of not finding an required argument on arguments provided, it'll print your usage description or, in case not provided, it'll print all required options.

Args parser won't stop the propagation in case of the user only call with the -h flag, to stop it with success (returning 0), check the variable helper_was_called, e.g.:

main() {
  local option helper_was_called

  usage="Main Function
    -o=<option>   Some Parameter to be filled"

  . ./argsparser.bash
  argsparser o=option -- "$usage"

  $helper_was_called && return 0
}

Example Usage

#!/bin/bash
# Alias to load argparser
# Define on your .bashrc, .bash_profile or .zshrc
alias load_argparser='. ./argsparser.bash'

main() {
  local require_opt next_assign_opt
  local -a array_opt args
  
  usage="main function
  -r | --required                Required argument
  --next-assign     <next-value> Next assigne value, e.g: --next-assign some-next-value
  --arr-var                      Array value"

  load_argparser
  argsparser {r,-required}:=require_var -next-assign~=next_assign_opt -arr-var+=array_opt -- "$usage"

  # Access the parsed variables
  echo "Required Option: $require_opt"
  echo "Next Assigne Option: $next_assign_opt"
  echo "Array Option: ${array_opt[@]}"
  echo "Plain Arguments: ${args[@]}"
}

main "$@"

Detailed Steps

  1. Defining Alias, Options and Variables:

    We're defining a alias to easier load when we want to use it, but your alias should be localed in one of you main files (.bashrc, .bash_profile or .zshrc), don't forget to create the file and point it correctly on the script (./argsparser.bash).

    alias load_argparser='. ./argsparser.bash'

    Define the variables that will be used to fill with the options parsed. For example:

    local require_opt next_assign_opt
    local -a array_opt args

    Notice that we'll work with plain arguments inside the var args, you can re-set the arguments positions with set -- $args right after parsing the args, e.g.:

    main() {
      local option
      local -a args
      
      load_argparser
      argsparser o=option
    
      set -- $args # re-set positional args with array values
    
      echo "Option: $option"
      echo "First Plain Argument: $1"
    }
    
    main -o some-arg
    # output:
    # Option: -o
    # First Plain Argument: some-arg
  2. Defining usage helper

Defines the usage variable containing the helper to use the function.

usage="main function
-r | --required                Required argument
--next-assign     <next-value> Next assign value, e.g: --next-assign some-next-value
--arr-var                      Array value"
  1. Loading and calling argsparser:

    Load and call the argsparser function with the rules and command-line arguments:

    . ./argsparser.bash
    argsparser {r,-required}:=require_var -next-assign~=next_assign_opt -arr-var+=array_opt -- "$usage" -- "$@"

    Take a look on the Load Args Parser to more details about why not load globally.

    Here we are defining the follow rule:

    • {r,-required}:=require_var: Required parameter that will fill the var required_var. The options availables for that will be --required and -r.
    • -next-assign~=next_assign_opt: Next argument option assignment, this will fill the var next_assign_opt. The option available for that will be --next-assign. This should be used like main --next-assign some-next-assign-value. Doesn't matter how many arguments come after, the next arg without a option will be assigned to the next argument assign option.
    • -arr-var+=array_opt: Array parameter that will fill the var array_opt. The option available for that will be -arr-var, which can be used more than once e.g.: main -arr-var=first-value -arr-var=first-value, which will populate the array assigned to the option.

    Also we are passing the usage helper for the script -- "$usage", which is optional, you can just not provide that if not needed by passing forward the parameters, e.g.: main -opt=var -- "$@"

  2. Accessing Parsed Variables:

    After calling argsparser with the options rules, you can access the parsed variables directly:

    echo "Required Option: $require_opt"
    echo "Next Assigne Option: $next_assign_opt"
    echo "Array Option: ${array_opt[@]}"
    echo "Plain Arguments: ${args[@]}"

Example Command

To run the example script with command-line arguments:

./example_script.sh -r=some-required-value --next-assign --arr-var="some-arr#1" some-next-assign --arr-var="some-arr#2" argument

In this example:

  • -r=some-required-value: Sets require_opt variable to some-required-value.
  • --next-assign: Sets next_assign_opt to some-next-assign, which comes ahead in the arguments.
  • --arr-var="some-arr#1" and --arr-var="some-arr#2": Adds some-arr#1 and some-arr#2 to the array array_opt.
  • argument: Will be added to the array variable args if declared

Zsh error handler

We recommend watching the return of argsparser since the set -e command closes the current instance of zsh, thus the usage code in zsh would be as follow example:

main {
  local option

  load_argsparser
  argsparser option=option || return $1
}

Loading Args Parser

You should always source the argsparser file in the place you wanna use it, don't load it in the main file (.bashrc, .bash_profile or `.zsh).

When the file is loaded we get the arguments used in the function without having to pass forward to the function everytime. In case of loading the file globally the arguments stored to be parsed will be the arguments passed to the main file.

Error Handling

The script includes error handling for various cases:

  • Invalid variable names.
  • Missing required options.
  • Conflicts between array and next argument assignable options.

Conclusion

The argsparser function provides a powerful way to parse command-line arguments in Bash scripts. By following the usage guidelines and example provided, you can easily integrate it into your own scripts to handle complex argument parsing needs.


This README file provides comprehensive documentation for the argsparser function, including how to define options, call the function, access parsed variables, and handle errors. It also includes a complete example script and command to demonstrate its usage.

#!/bin/bash
declare -a ARGS="$*"
# Utility function to check if an array includes a value
function array_includes_value() {
local search_value="$1"
shift
local array=("$@")
for item in "${array[@]}"; do
[[ "$item" == "$search_value" ]] && return 0
done
return 1
}
# Utility function to escape double quotes in a string
function escape_double_quote() {
echo "${1//\"/\\\"}"
}
# Utility function to check if a variable name is valid
function check_var_name() {
if [[ ! "$1" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then
echo "Error: \"$1\" is not a valid variable name!"
return 1
fi
return 0
}
# Utility function to split a string by '=' and get the specified field
function split_arg_value() {
echo "$1" | cut -d= -"f$2"
}
# Function to show formatted required parameters
function show_formatted_required_parameters() {
declare -A variables_n_options options_formatted_n_main_option
for option in "${!options_n_variables[@]}"; do
var="${options_n_variables[$option]}"
option_formatted="$([[ -n "${variables_n_options[$var]}" ]] && echo "${variables_n_options[$var]}|" || echo "")-$option"
variables_n_options[$var]=$option_formatted
options_formatted_n_main_option[$option_formatted]=$option
done
for var in "${!variables_n_options[@]}"; do
option_formatted="${variables_n_options[$var]}"
if array_includes_value "${options_formatted_n_main_option[$option_formatted]}" "${required_options[@]}"; then
echo " $option_formatted"
fi
done
}
# Main function to parse arguments
function argsparser() {
set -e
declare -A options_n_variables variables_n_values array_vars
declare -a required_options array_options next_value_options
helper_was_called=false # declare helper wasn't called to change ahead in case of called
if ! declare -p args &>/dev/null; then
declare -a args
fi
# Process options and variables
while [[ "$1" != "--" ]] && [[ "$1" != "" ]]; do
raw_option=$(split_arg_value "$1" 1)
variable=$(split_arg_value "$1" 2)
check_var_name "$variable" || return 1
if [[ -z $variable || "$variable" == "$raw_option" ]]; then
echo "Error: Variable not provided for \"$raw_option\"!"
return 1
fi
option="$raw_option"
local is_required=false is_array=false is_next_arg=false
[[ "$option" == *":"* ]] && { option="${option//:}"; is_required=true; }
[[ "$option" == *"+"* ]] && { option="${option//+}"; is_array=true; }
[[ "$option" == *"~"* ]] && { option="${option//\~}"; is_next_arg=true; }
if $is_next_arg && $is_array; then
echo "Error: \"$raw_option\" cannot be both Array and Next Argument Assignable!" >&2
return 1
fi
$is_next_arg && next_value_options+=("$option")
$is_array && array_options+=("$option")
$is_required && required_options+=("$option")
options_n_variables["$option"]="$variable"
shift
done
[[ -n $1 ]] && shift # Shift past the "--"
local usage=""
if [[ -n $1 ]]; then
usage="$1"
if array_includes_value "--help" "${ARGS[*]}" || array_includes_value "-h" "${ARGS[*]}"; then
# shellcheck disable=SC2034
helper_was_called=true
echo 'Usage:'
echo "$usage"
return 0
fi
fi
local option_to_get_next_arg_as_value=""
# Parse argument
for arg in ${ARGS[*]}; do
[[ "$arg" == "--" ]] && break
if [[ "$arg" != "-"* ]]; then
if [[ -n $option_to_get_next_arg_as_value ]]; then
variable="${options_n_variables[$option_to_get_next_arg_as_value]}"
variables_n_values[$variable]="$arg"
option_to_get_next_arg_as_value=""
continue
fi
args+=("$arg")
fi
raw_option="$arg"
if [[ "$arg" =~ "=" ]]; then
raw_option=$(split_arg_value "$arg" 1)
fi
option="${raw_option:1}"
if ! array_includes_value "$option" "${!options_n_variables[@]}"; then
continue
fi
value=$(split_arg_value "$arg" 2)
variable="${options_n_variables[$option]}"
if array_includes_value "$option" "${next_value_options[@]}" && [[ "$value" == "$raw_option" ]]; then
option_to_get_next_arg_as_value="$option"
elif array_includes_value "$option" "${array_options[@]}"; then
array_variable_name="array_option__$variable"
if ! declare -p "$array_variable_name" &>/dev/null; then
declare -a "$array_variable_name"
fi
eval "${array_variable_name}+=(\"$value\")"
array_vars["$array_variable_name"]="$variable"
else
variables_n_values["$variable"]="$value"
fi
done
# Check for required options
for required_option in "${required_options[@]}"; do
variable="${options_n_variables[$required_option]}"
if ! array_includes_value "$variable" "${!variables_n_values[@]}" && \
! array_includes_value "array_option__$variable" "${!array_vars[@]}"; then
echo -e "Error: Parameter -$required_option is required!\n"
if [[ -n $usage ]]; then
echo "Usage:"
echo "$usage"
else
echo "Required parameters:"
show_formatted_required_parameters
fi
return 1
fi
done
# Export variables
for variable_name in "${!variables_n_values[@]}"; do
eval "$variable_name=\"$(escape_double_quote "${variables_n_values[$variable_name]}")\""
done
# Export array variables
for local_array_variable_name in "${!array_vars[@]}"; do
variable_name="${array_vars[$local_array_variable_name]}"
eval "$variable_name=(\"\${${local_array_variable_name}[@]}\")"
done
}
#!/bin/zsh
declare -a ARGS
ARGS=("$@")
# Utility function to check if an array includes a value
array_includes_value() {
local search_value="$1"
shift
local array=("$@")
for item in "${array[@]}"; do
[[ "$item" == "$search_value" ]] && return 0
done
return 1
}
# Utility function to escape double quotes in a string
escape_double_quote() {
echo "${1//\"/\\\"}"
}
# Utility function to check if a variable name is valid
check_var_name() {
if [[ ! "$1" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then
echo "Error: \"$1\" is not a valid variable name!"
return 1
fi
return 0
}
# Utility function to split a string by '=' and get the specified field
split_arg_value() {
echo "$1" | cut -d= -f"$2"
}
# Function to show formatted required parameters
show_formatted_required_parameters() {
local -A variables_n_options options_formatted_n_main_option
for option in ${(k)options_n_variables}; do
var="${options_n_variables[$option]}"
option_formatted="$([[ -n "${variables_n_options[$var]}" ]] && echo "${variables_n_options[$var]}|" || echo "")-$option"
variables_n_options[$var]=$option_formatted
options_formatted_n_main_option[$option_formatted]=$option
done
for var in ${(k)variables_n_options}; do
option_formatted="${variables_n_options[$var]}"
if array_includes_value "${options_formatted_n_main_option[$option_formatted]}" "${required_options[@]}"; then
echo " $option_formatted"
fi
done
}
# Main function to parse arguments
argsparser() {
local -A options_n_variables variables_n_values array_vars
local -a required_options array_options next_value_options
if ! typeset -p helper_was_called &>/dev/null; then
local helper_was_called
fi
helper_was_called=false
if ! typeset -p args &>/dev/null; then
local -a args
fi
# Process options and variables
while [[ "$1" != "--" ]] && [[ "$1" != "" ]]; do
raw_option=$(split_arg_value "$1" 1)
variable=$(split_arg_value "$1" 2)
check_var_name "$variable" || return 1
if [[ -z $variable || "$variable" == "$raw_option" ]]; then
echo "Error: Variable not provided for \"$raw_option\"!"
return 1
fi
option="$raw_option"
local is_required=false is_array=false is_next_arg=false
[[ "$option" == *":"* ]] && { option="${option//:}"; is_required=true; }
[[ "$option" == *"+"* ]] && { option="${option//+}"; is_array=true; }
[[ "$option" == *"~"* ]] && { option="${option//\~}"; is_next_arg=true; }
if $is_next_arg && $is_array; then
echo "Error: \"$raw_option\" cannot be both Array and Next Argument Assignable!" >&2
return 1
fi
$is_next_arg && next_value_options+=("$option")
$is_array && array_options+=("$option")
$is_required && required_options+=("$option")
options_n_variables[$option]="$variable"
shift
done
[[ -n $1 ]] && shift # Shift past the "--"
local usage=""
if [[ -n $1 ]]; then
usage="$1"
if array_includes_value "--help" "${ARGS[@]}" || array_includes_value "-h" "${ARGS[@]}"; then
helper_was_called=true
echo 'Usage:'
echo "$usage"
return 0
fi
fi
local option_to_get_next_arg_as_value=""
# Parse arguments
for arg in "${ARGS[@]}"; do
[[ "$arg" == "--" ]] && break
if [[ "$arg" != "-"* ]]; then
if [[ -n $option_to_get_next_arg_as_value ]]; then
variable="${options_n_variables[$option_to_get_next_arg_as_value]}"
variables_n_values[$variable]="$arg"
option_to_get_next_arg_as_value=""
continue
fi
args+=("$arg")
fi
raw_option="$arg"
if [[ "$arg" =~ "=" ]]; then
raw_option=$(split_arg_value "$arg" 1)
fi
option="${raw_option[2,-1]}"
if ! array_includes_value "$option" "${(k)options_n_variables[@]}"; then
continue
fi
value=$(split_arg_value "$arg" 2)
variable="${options_n_variables[$option]}"
if array_includes_value "$option" "${next_value_options[@]}" && [[ "$value" == "$raw_option" ]]; then
option_to_get_next_arg_as_value="$option"
elif array_includes_value "$option" "${array_options[@]}"; then
array_variable_name="array_option__$variable"
if ! typeset -p "$array_variable_name" &>/dev/null; then
local -a "$array_variable_name"
fi
eval "${array_variable_name}+=(\"$value\")"
array_vars[$array_variable_name]="$variable"
else
variables_n_values[$variable]="$value"
fi
done
# Check for required options
for required_option in "${required_options[@]}"; do
variable="${options_n_variables[$required_option]}"
if ! array_includes_value "$variable" "${(k)variables_n_values[@]}" && \
! array_includes_value "array_option__$variable" "${(k)array_vars[@]}"; then
echo -e "Error: Parameter -$required_option is required!\n"
if [[ -n $usage ]]; then
echo "Usage:"
echo "$usage"
else
echo "Required parameters:"
show_formatted_required_parameters
fi
return 1
fi
done
# Export variables
for variable_name in "${(k)variables_n_values[@]}"; do
eval "$variable_name=\"$(escape_double_quote "${variables_n_values[$variable_name]}")\""
done
# Export array variables
for local_array_variable_name in "${(k)array_vars[@]}"; do
variable_name="${array_vars[$local_array_variable_name]}"
eval "$variable_name=(\"\${${local_array_variable_name}[@]}\")"
done
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment