Created
September 12, 2023 12:49
-
-
Save OleksandrKucherenko/0473b10ea3fac797bd3a66cc77b55357 to your computer and use it in GitHub Desktop.
Parse BASH script input arguments in a simplest way
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
# shellcheck disable=SC2155,SC2034,SC2059 | |
# get script directory | |
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |
# is allowed to use macOS extensions (script can be executed in *nix environment) | |
use_macos_extensions=false | |
if [[ "$OSTYPE" == "darwin"* ]]; then use_macos_extensions=true; fi | |
# colors | |
export cl_reset=$(tput sgr0) | |
export cl_red=$(tput setaf 1) | |
export cl_green=$(tput setaf 2) | |
export cl_yellow=$(tput setaf 3) | |
export cl_blue=$(tput setaf 4) | |
export cl_purple=$(tput setaf 5) | |
export cl_cyan=$(tput setaf 6) | |
export cl_white=$(tput setaf 7) | |
export cl_grey=$(tput setaf 8) | |
export cl_lred=$(tput setaf 9) | |
export cl_lgreen=$(tput setaf 10) | |
export cl_lyellow=$(tput setaf 11) | |
export cl_lblue=$(tput setaf 12) | |
export cl_lpurple=$(tput setaf 13) | |
export cl_lcyan=$(tput setaf 14) | |
export cl_lwhite=$(tput setaf 15) | |
export cl_black=$(tput setaf 16) | |
# shellcheck disable=SC1090 source=_logger.sh | |
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_logger.sh" | |
# register own logger | |
logger common "$@" | |
function now() { | |
echo "$EPOCHREALTIME" # <~ bash 5.0 | |
#python -c 'import datetime; print datetime.datetime.now().strftime("%s.%f")' | |
} | |
# shellcheck disable=SC2155,SC2086 | |
function print_time_diff() { | |
local diff="$(now) - $1" | |
bc <<<$diff | |
} | |
# shellcheck disable=SC2086 | |
function validate_input() { | |
local variable=$1 | |
local default=${2:-""} | |
local prompt=${3:-""} | |
local user_in="" | |
# Ctrl+C during read operation force error exit | |
trap 'exit 1' SIGINT | |
# execute at least once | |
while :; do | |
# allow macOs read command extension usage (default value -i) | |
if $use_macos_extensions; then | |
[[ -z "${prompt// /}" ]] || read -e -i "${default}" -p "${cl_purple}? ${cl_reset}${prompt}${cl_blue}" -r user_in | |
[[ -n "${prompt// /}" ]] || read -e -i "${default}" -r user_in | |
else | |
[[ -z "${prompt// /}" ]] || echo "${cl_purple}? ${cl_reset}${prompt}${cl_blue}" | |
read -r user_in | |
fi | |
printf "${cl_reset}" | |
[[ -z "${user_in// /}" ]] || break | |
done | |
local __resultvar=$variable | |
eval $__resultvar="'$user_in'" | |
} | |
# shellcheck disable=SC2086,SC2059 | |
function validate_yn_input() { | |
local variable=$1 | |
local default=${2:-""} | |
local prompt=${3:-""} | |
local user_in=false | |
while true; do | |
if $use_macos_extensions; then | |
[[ -z "${prompt// /}" ]] || read -e -i "${default}" -p "${cl_purple}? ${cl_reset}${prompt}${cl_blue}" -r yn | |
[[ -n "${prompt// /}" ]] || read -e -i "${default}" -r yn | |
else | |
[[ -z "${prompt// /}" ]] || echo "${cl_purple}? ${cl_reset}${prompt}${cl_blue}" | |
read -r yn | |
fi | |
printf "${cl_reset}" | |
case $yn in | |
[Yy]*) | |
user_in=true | |
break | |
;; | |
[Nn]*) | |
user_in=false | |
break | |
;; | |
*) | |
user_in=false | |
break | |
;; | |
esac | |
done | |
local __resultvar=$variable | |
eval $__resultvar="$user_in" | |
} | |
# shellcheck disable=SC2086 | |
function env_variable_or_secret_file() { | |
# | |
# Usage: | |
# env_variable_or_secret_file "new_value" \ | |
# "GITLAB_CI_INTEGRATION_TEST" \ | |
# ".secrets/gitlab_ci_integration_test" \ | |
# "{user friendly message}" | |
# | |
local name=$1 | |
local variable=$2 | |
local file=$3 | |
local fallback=${4:-"No hints, check the documentation"} | |
local __result=$name | |
if [[ -z "${!variable}" ]]; then | |
if [[ ! -f "$file" ]]; then | |
echo "" | |
echo "${cl_red}ERROR:${cl_reset} shell environment variable '\$$variable' or file '$file' should be provided" | |
echo "" | |
echo "Hint:" | |
echo " $fallback" | |
exit 1 | |
else | |
echo "Using file: ${cl_green}$file${cl_reset} ~> $name" | |
eval $__result="'$(cat $file)'" | |
fi | |
else | |
echo "Using var : ${cl_green}\$$variable${cl_reset} ~> $name" | |
eval $__result="'${!variable}'" | |
fi | |
} | |
# shellcheck disable=SC2086 | |
function optional_env_variable_or_secret_file() { | |
# | |
# Usage: | |
# optional_env_variable_or_secret_file "new_value" \ | |
# "GITLAB_CI_INTEGRATION_TEST" \ | |
# ".secrets/gitlab_ci_integration_test" | |
# | |
local name=$1 | |
local variable=$2 | |
local file=$3 | |
local __result=$name | |
if [[ -z "${!variable}" ]]; then | |
if [[ ! -f "$file" ]]; then | |
# NO variable, NO file | |
echo "${cl_yellow}Note:${cl_reset} shell environment variable '\$$variable' or file '$file' can be provided." | |
return 0 | |
else | |
echo "Using file: ${cl_green}$file${cl_reset} ~> $name" | |
eval $__result="'$(cat $file)'" | |
return 2 | |
fi | |
else | |
echo "Using var : ${cl_green}\$$variable${cl_reset} ~> $name" | |
eval $__result="'${!variable}'" | |
return 1 | |
fi | |
} | |
function isHelp() { | |
local args=("$@") | |
if [[ "${args[*]}" =~ "--help" ]]; then echo true; else echo false; fi | |
} | |
# --cookies -> "cookies||0", | |
# --cookies=: -> "cookies||0" | |
# --cookies=first -> "first||0", | |
# --cookies=first: -> "first||0" | |
# --cookies=::1 -> "cookies||1", | |
# --cookies=:default:1 -> "cookies|default|1" | |
# --cookies=first::1 -> "first||1" | |
# --cookies=first:default -> "first|default|0" | |
# --cookies=first:default:1 -> "first|default|1" | |
function extract_output_definition() { | |
local definition=$1 | |
# extract output variable name, examples: | |
local name=${definition%%=*} | |
local name_as_value=${name//-/} | |
local output=${definition##*=} | |
local variable="" | |
local default="1" | |
local args_qt="0" | |
# extract variable name | |
if [[ "$output" == "$definition" ]]; then # simplest: --cookies | |
variable=$name_as_value | |
elif [[ "$output" == *:* ]]; then # extended: --cookies=first:*, --cookies=first:default:1, --cookies=::1, --cookies=:, --cookies=first: | |
local tmp=${output%%:*} && variable=${tmp:-"$name_as_value"} | |
else # extended: --cookies=first | |
variable=$output | |
fi | |
# extract default value | |
if [[ "$output" == *:* ]]; then default=${output#*:} && default=${default%:*}; fi | |
# extract arguments quantity | |
if [[ "$output" == *:*:* ]]; then args_qt=${output##*:}; fi | |
echo "$variable|$default|$args_qt" | |
} | |
# pattern: "{argument},-{short},--{alias}={output}:{init_value}:{args_quantity}" | |
if [ -z "$ARGS_DEFINITION" ]; then export ARGS_DEFINITION="-h,--help -v,--version=:1.0.0 --debug=DEBUG:*"; fi | |
function parse_arguments() { | |
local args=("$@") | |
# TODO (olku): trim whitespaces in $ARGS_DEFINITION, not spaces in begining or end, no double spaces | |
echoCommon "${cl_grey}Definition: $ARGS_DEFINITION${cl_reset}" >&2 | |
# extract definition of each argument, separated by space, remove last empty element | |
readarray -td ' ' definitions <<<"$ARGS_DEFINITION " && unset 'definitions[-1]' | |
# build lookup map of arguments, extract the longest name of each argument | |
declare -A lookup_arguments && lookup_arguments=() # key-to-index_of_definition. e.g. -c -> 0, --cookies -> 0 | |
declare -A index_to_outputs && index_to_outputs=() # index-to-variable_name, e.g. -c,--cookies -> 0=cookies | |
declare -A index_to_args_qt && index_to_args_qt=() # index-to-argument_qauntity, e.g. -c,--cookies -> 0="0" | |
declare -A index_to_default && index_to_default=() # index-to-argument_default, e.g. -c,--cookies -> 0="", -c=:default:1 -> 0="default" | |
# build parameters mapping | |
for i in "${!definitions[@]}"; do | |
# TODO (olku): validate the pattern format, otherwise throw an error | |
# shellcheck disable=SC2206 | |
local keys=(${definitions[i]//,/ }) | |
for key in "${keys[@]}"; do | |
local name=${key%%=*} # extract clean key name, e.g. --cookies=first -> --cookies | |
local helper=$(extract_output_definition "$key") | |
# do the mapping | |
lookup_arguments[$name]=$i | |
index_to_outputs[$i]=$(echo "$helper" | awk -F'|' '{print $1}') | |
index_to_args_qt[$i]=$(echo "$helper" | awk -F'|' '{print $3}') | |
index_to_default[$i]=$(echo "$helper" | awk -F'|' '{print $2}') | |
done | |
done | |
local index=1 # indexed input arguments without pre-flag | |
local skip_next_counter=0 # how many argument to skip from processing | |
local skip_aggregated="" # all skipped arguments placed into one array | |
local last_processed="" # last processed argument | |
local separator="" # separator between aggregated arguments | |
# parse the script arguments and resolve them to output variables | |
for i in "${!args[@]}"; do | |
local argument=${args[i]} | |
local value="" | |
# extract key and value from argument, if used format `--key=value` | |
# shellcheck disable=SC2206 | |
if [[ "$argument" == *=* ]]; then local tmp=(${argument//=/ }) && value=${tmp[1]} && argument=${tmp[0]}; fi | |
# accumulate arguments that reserved by last processed argument | |
if [ "$skip_next_counter" -gt 0 ]; then | |
skip_next_counter=$((skip_next_counter - 1)) | |
skip_aggregated="${skip_aggregated}${separator}${argument}" | |
separator=" " | |
continue | |
fi | |
# if skipped aggregated var contains value assign it to the last processed argument | |
if [ ${#skip_aggregated} -gt 0 ]; then | |
value="$skip_aggregated" && skip_aggregated="" && separator="" | |
# assign value to output variable | |
local tmp_index=${lookup_arguments[$last_processed]} | |
eval "export ${index_to_outputs[$tmp_index]}=\"$value\"" | |
fi | |
# process flags | |
if [ ${lookup_arguments[$argument]+_} ]; then | |
last_processed=$argument | |
local tmp_index=${lookup_arguments[$argument]} | |
local expected=${index_to_args_qt[$tmp_index]} | |
# if expected more arguments than provided, configure skip_next_counter | |
if [ "$expected" -gt 0 ]; then | |
skip_next_counter=$expected | |
skip_aggregated="$value" # assign current value to the skip_aggregated | |
continue | |
else | |
# assign default value to the output variable first | |
eval "export ${index_to_outputs[$tmp_index]}=\"${index_to_default[$tmp_index]}\"" | |
# default value is re-assigned by provided value | |
if [ -n "$value" ]; then | |
eval "export ${index_to_outputs[$tmp_index]}=\"$value\"" | |
fi | |
fi | |
else | |
local by_index="\$$index" | |
# process plain unnamed arguments | |
case $argument in | |
-*) echoCommon "${cl_grey}ignored: $argument ($value)${cl_reset}" >&2 ;; | |
*) | |
if [ ${lookup_arguments[$by_index]+_} ]; then | |
last_processed=$by_index | |
local tmp_index=${lookup_arguments[$by_index]} | |
eval "export ${index_to_outputs[$tmp_index]}=\"$argument\"" | |
else | |
echoCommon "${cl_grey}ignored: $argument [$by_index]${cl_reset}" >&2 | |
fi | |
index=$((index + 1)) | |
;; | |
esac | |
fi | |
done | |
# if aggregated var contains somenthing, raise error "too little arguments provided" | |
if [ ${#skip_aggregated} -gt 0 ]; then | |
if [ "$skip_next_counter" -gt 0 ]; then | |
echo "Too little arguments provided" | |
exit 1 | |
else | |
local value="$skip_aggregated" && skip_aggregated="" && separator="" | |
local tmp_index=${lookup_arguments[$last_processed]} | |
eval "export ${index_to_outputs[$tmp_index]}=\"$value\"" | |
fi | |
fi | |
# debug output | |
echoCommon "definition to output index:" | |
printfCommon '%s\n' "${!definitions[@]}" "${definitions[@]}" | pr -2t | |
echoCommon "'index', 'output variable name', 'args quantity', 'defaults':" | |
printfCommon '\"%s\"\n' "${!index_to_outputs[@]}" "${index_to_outputs[@]}" "${index_to_args_qt[@]}" "${index_to_default[@]}" | pr -4t | sort | |
for variable in "${index_to_outputs[@]}"; do | |
declare -n var_ref=$variable | |
echoCommon "${cl_grey}extracted: $variable=$var_ref${cl_reset}" | |
done | |
} | |
# array of script arguments cleaned from flags (e.g. --help) | |
export ARGS_NO_FLAGS=() | |
function exclude_flags_from_args() { | |
local args=("$@") | |
# remove all flags from call | |
for i in "${!args[@]}"; do | |
if [[ ${args[i]} == --* ]]; then unset 'args[i]'; fi | |
done | |
echoCommon "${cl_grey}Filtered args:" "$@" "~>" "${args[*]}" "$cl_reset" >&2 | |
# shellcheck disable=SC2207,SC2116 | |
ARGS_NO_FLAGS=($(echo "${args[*]}")) | |
} | |
exclude_flags_from_args "$@" | |
parse_arguments "$@" | |
# shellcheck disable=SC2154 | |
# echo "help:$help version:$version debug:$DEBUG first:$FIRST second:$SECOND servers:$servers" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
latest version in: https://github.com/OleksandrKucherenko/e-bash