From 737a918959f306d182b2cf531f55ca3f1446f0cb Mon Sep 17 00:00:00 2001 From: Dmytro Konstantinov Date: Tue, 17 Dec 2024 14:42:02 +0000 Subject: [PATCH] Adds support for nested function arguments. (#31) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR reworks the default argument logic allowing it to collect arguments just for the current function, using BASH_ARGC as a boundary. It’s designed to make things more reliable in nested contexts, allowing the user to work only with the arguments relevant to the current function call. --------- Co-authored-by: Will Allan --- README.md | 5 +- lib/getopts_long.bash | 25 +++++- test/bats/github_17a.bats | 31 +++++++ test/bats/github_17b.bats | 35 ++++++++ test/bats/github_17c.bats | 31 +++++++ test/bin/getopts_long-explicit_args-silent | 80 +++++++++++++++++++ test/bin/getopts_long-explicit_args-verbose | 80 +++++++++++++++++++ test/bin/getopts_long-with_extdebug-silent | 80 +++++++++++++++++++ test/bin/getopts_long-with_extdebug-verbose | 80 +++++++++++++++++++ test/bin/getopts_long-without_extdebug-silent | 80 +++++++++++++++++++ .../bin/getopts_long-without_extdebug-verbose | 80 +++++++++++++++++++ 11 files changed, 600 insertions(+), 7 deletions(-) create mode 100644 test/bats/github_17a.bats create mode 100644 test/bats/github_17b.bats create mode 100644 test/bats/github_17c.bats create mode 100755 test/bin/getopts_long-explicit_args-silent create mode 100755 test/bin/getopts_long-explicit_args-verbose create mode 100755 test/bin/getopts_long-with_extdebug-silent create mode 100755 test/bin/getopts_long-with_extdebug-verbose create mode 100755 test/bin/getopts_long-without_extdebug-silent create mode 100755 test/bin/getopts_long-without_extdebug-verbose diff --git a/README.md b/README.md index fae31da..4d23de6 100644 --- a/README.md +++ b/README.md @@ -190,9 +190,8 @@ done Identical to `getopts`, `getopts_long` will parse options and their possible arguments. It will stop parsing on the first non-option argument (a string that doesn't begin with a hyphen (`-`) that isn't an argument for any option in front of it). It will also stop parsing when it sees the `--` (double-hyphen) as a stand-alone argument. -> ⚠️ **IMPORTANT** -> -> To support long options and enforce identical behaviour between getopts and getopts_long when handling hyphens, getopts_long provides its own implementation for `-` option. This means that the user can no longer include a hyphen (`-`) within short option OPTSPEC. +> [!IMPORTANT] +> To support long options and enforce identical behaviour between getopts and getopts_long when handling hyphens, getopts_long provides its own implementation for `-` option. This means that the user can not use hyphen (`-`) within their short option OPTSPEC, as this would override getopts_long implementation. ### Internal variables diff --git a/lib/getopts_long.bash b/lib/getopts_long.bash index 2b913ca..baabf95 100644 --- a/lib/getopts_long.bash +++ b/lib/getopts_long.bash @@ -10,9 +10,26 @@ getopts_long() { if [[ "${#}" == 0 ]]; then local args=() - while [[ ${#BASH_ARGV[@]} -gt ${#args[@]} ]]; do - local index=$(( ${#BASH_ARGV[@]} - ${#args[@]} - 1 )) - args[${#args[@]}]="${BASH_ARGV[${index}]}" + local -i start_index=0 + local -i end_index=$(( ${#BASH_ARGV[@]} - 1 )) + + # Minimise the number of times `declare -f` is executed + if [[ -n "${FUNCNAME[1]}" ]]; then + if [[ "${FUNCNAME[1]}" == "( anon )" ]] \ + || declare -f "${FUNCNAME[1]}" > /dev/null 2>&1; then + if ! shopt -q extdebug; then + echo "${BASH_SOURCE[1]}: line ${BASH_LINENO[0]}:" \ + "${FUNCNAME[0]} failed to detect supplied arguments" \ + "-- enable extdebug or pass arguments explicitly" >&2 + return 2 + fi + start_index=${BASH_ARGC[0]} + end_index=$(( start_index + BASH_ARGC[1] - 1 )) + fi + fi + + for (( i = end_index; i >= start_index; i-- )); do + args+=("${BASH_ARGV[i]}") done set -- "${args[@]}" fi @@ -22,7 +39,7 @@ getopts_long() { optspec_short="${optspec_short//-}" [[ "${!OPTIND:0:2}" == "--" ]] && optspec_short+='-:' - builtin getopts -- "${optspec_short}" "${optvar}" "${@}" || return 1 + builtin getopts -- "${optspec_short}" "${optvar}" "${@}" || return ${?} [[ "${!optvar}" == '-' ]] || return 0 printf -v "${optvar}" "%s" "${OPTARG%%=*}" diff --git a/test/bats/github_17a.bats b/test/bats/github_17a.bats new file mode 100644 index 0000000..a9bd3d1 --- /dev/null +++ b/test/bats/github_17a.bats @@ -0,0 +1,31 @@ +#!/usr/bin/env bats + +load ../test_helper +export GETOPTS_LONG_TEST_BIN='getopts_long-with_extdebug' + +@test "${FEATURE}: toggles, silent" { + compare '-t -t -- user_arg' \ + 'toggles' +} +@test "${FEATURE}: toggles, verbose" { + compare '-t -t -- user_arg' \ + 'toggles' +} + +@test "${FEATURE}: options, silent" { + compare '-o user_val1 -o user_val2 -- user_arg' \ + 'options' +} +@test "${FEATURE}: options, verbose" { + compare '-o user_val1 -o user_val2 -- user_arg' \ + 'options' +} + +@test "${FEATURE}: variables, silent" { + compare '-vuser_val1 -vuser_val2 -- user_arg' \ + 'variables' +} +@test "${FEATURE}: variables, verbose" { + compare '-vuser_val1 -vuser_val2 -- user_arg' \ + 'variables' +} diff --git a/test/bats/github_17b.bats b/test/bats/github_17b.bats new file mode 100644 index 0000000..2a740c9 --- /dev/null +++ b/test/bats/github_17b.bats @@ -0,0 +1,35 @@ +#!/usr/bin/env bats + +load ../test_helper + +# Compare in the following tests is simply used to populate +# bash_getopts and getopts_long arrays with identical results. +export GETOPTS_TEST_BIN='getopts_long-without_extdebug' +export GETOPTS_LONG_TEST_BIN='getopts_long-without_extdebug' + +@test "${FEATURE}: toggles, silent" { + compare 'toggles' 'toggles' + expect "${getopts_long[1]}" =~ "getopts_long-without_extdebug-silent: line 8: getopts_long failed" +} +@test "${FEATURE}: toggles, verbose" { + compare 'toggles' 'toggles' + expect "${getopts_long[1]}" =~ "getopts_long-without_extdebug-verbose: line 8: getopts_long failed" +} + +@test "${FEATURE}: options, silent" { + compare 'options' 'options' + expect "${getopts_long[1]}" =~ "getopts_long-without_extdebug-silent: line 8: getopts_long failed" +} +@test "${FEATURE}: options, verbose" { + compare 'options' 'options' + expect "${getopts_long[1]}" =~ "getopts_long-without_extdebug-verbose: line 8: getopts_long failed" +} + +@test "${FEATURE}: variables, silent" { + compare 'variables' 'variables' + expect "${getopts_long[1]}" =~ "getopts_long-without_extdebug-silent: line 8: getopts_long failed" +} +@test "${FEATURE}: variables, verbose" { + compare 'variables' 'variables' + expect "${getopts_long[1]}" =~ "getopts_long-without_extdebug-verbose: line 8: getopts_long failed" +} diff --git a/test/bats/github_17c.bats b/test/bats/github_17c.bats new file mode 100644 index 0000000..5466716 --- /dev/null +++ b/test/bats/github_17c.bats @@ -0,0 +1,31 @@ +#!/usr/bin/env bats + +load ../test_helper +export GETOPTS_LONG_TEST_BIN='getopts_long-explicit_args' + +@test "${FEATURE}: toggles, silent" { + compare '-t -t -- user_arg' \ + 'toggles' +} +@test "${FEATURE}: toggles, verbose" { + compare '-t -t -- user_arg' \ + 'toggles' +} + +@test "${FEATURE}: options, silent" { + compare '-o user_val1 -o user_val2 -- user_arg' \ + 'options' +} +@test "${FEATURE}: options, verbose" { + compare '-o user_val1 -o user_val2 -- user_arg' \ + 'options' +} + +@test "${FEATURE}: variables, silent" { + compare '-vuser_val1 -vuser_val2 -- user_arg' \ + 'variables' +} +@test "${FEATURE}: variables, verbose" { + compare '-vuser_val1 -vuser_val2 -- user_arg' \ + 'variables' +} diff --git a/test/bin/getopts_long-explicit_args-silent b/test/bin/getopts_long-explicit_args-silent new file mode 100755 index 0000000..355d238 --- /dev/null +++ b/test/bin/getopts_long-explicit_args-silent @@ -0,0 +1,80 @@ +#!/usr/bin/env bash + +TOPDIR="$(cd "$(dirname "${0}")"/../.. && pwd)" +# shellcheck disable=SC1090 +source "${TOPDIR}/lib/getopts_long.bash" + +getopts_function() { + while getopts_long ':to:v: toggle option: variable:' OPTKEY "$@"; do + case ${OPTKEY} in + 't'|'toggle') + printf 'toggle triggered' + ;; + 'o'|'option') + printf 'option supplied' + ;; + 'v'|'variable') + printf 'value supplied' + ;; + '?') + printf "INVALID OPTION" + ;; + ':') + printf "MISSING ARGUMENT" + ;; + *) + printf "NEVER REACHED" + ;; + esac + printf ' -- ' + declare -p OPTARG 2>&1 | grep -oe 'OPTARG.*' + done + + shift $(( OPTIND - 1 )) + + echo "OPTERR: ${OPTERR}" + echo "OPTKEY: ${OPTKEY}" + echo "OPTARG: ${OPTARG}" + echo "OPTIND: ${OPTIND}" + + local function_args=("$@") + declare -p function_args \ + | sed -e 's/declare -a function_args=/$@: /' +} + +proxy() { + getopts_function "$@" +} + +toggles() { + getopts_function -t --toggle -- user_arg +} + +options() { + getopts_function -o user_val1 --option user_val2 -- user_arg +} + +variables() { + getopts_function -vuser_val1 --variable=user_val2 -- user_arg +} + +enable_extdebug='false' +if shopt -q extdebug; then + enable_extdebug='true' + shopt -u extdebug +fi + +: "${1:?Missing required argument -- function name}" +function_name=${1} +shift + +if declare -f "$function_name" > /dev/null; then + ${function_name} "$@" +else + echo "Function not found -- ${function_name}" + exit 1 +fi + +if ${enable_extdebug}; then + shopt -s extdebug +fi diff --git a/test/bin/getopts_long-explicit_args-verbose b/test/bin/getopts_long-explicit_args-verbose new file mode 100755 index 0000000..538c5f0 --- /dev/null +++ b/test/bin/getopts_long-explicit_args-verbose @@ -0,0 +1,80 @@ +#!/usr/bin/env bash + +TOPDIR="$(cd "$(dirname "${0}")"/../.. && pwd)" +# shellcheck disable=SC1090 +source "${TOPDIR}/lib/getopts_long.bash" + +getopts_function() { + while getopts_long 'to:v: toggle option: variable:' OPTKEY "$@"; do + case ${OPTKEY} in + 't'|'toggle') + printf 'toggle triggered' + ;; + 'o'|'option') + printf 'option supplied' + ;; + 'v'|'variable') + printf 'value supplied' + ;; + '?') + printf "INVALID OPTION" + ;; + ':') + printf "MISSING ARGUMENT" + ;; + *) + printf "NEVER REACHED" + ;; + esac + printf ' -- ' + declare -p OPTARG 2>&1 | grep -oe 'OPTARG.*' + done + + shift $(( OPTIND - 1 )) + + echo "OPTERR: ${OPTERR}" + echo "OPTKEY: ${OPTKEY}" + echo "OPTARG: ${OPTARG}" + echo "OPTIND: ${OPTIND}" + + local function_args=("$@") + declare -p function_args \ + | sed -e 's/declare -a function_args=/$@: /' +} + +proxy() { + getopts_function "$@" +} + +toggles() { + getopts_function -t --toggle -- user_arg +} + +options() { + getopts_function -o user_val1 --option user_val2 -- user_arg +} + +variables() { + getopts_function -vuser_val1 --variable=user_val2 -- user_arg +} + +enable_extdebug='false' +if shopt -q extdebug; then + enable_extdebug='true' + shopt -u extdebug +fi + +: "${1:?Missing required parameter -- function name}" +function_name=${1} +shift + +if declare -f "$function_name" > /dev/null; then + ${function_name} "$@" +else + echo "Function not found -- ${function_name}" + exit 1 +fi + +if ${enable_extdebug}; then + shopt -s extdebug +fi diff --git a/test/bin/getopts_long-with_extdebug-silent b/test/bin/getopts_long-with_extdebug-silent new file mode 100755 index 0000000..45c0fda --- /dev/null +++ b/test/bin/getopts_long-with_extdebug-silent @@ -0,0 +1,80 @@ +#!/usr/bin/env bash + +TOPDIR="$(cd "$(dirname "${0}")"/../.. && pwd)" +# shellcheck disable=SC1090 +source "${TOPDIR}/lib/getopts_long.bash" + +getopts_function() { + while getopts_long ':to:v: toggle option: variable:' OPTKEY; do + case ${OPTKEY} in + 't'|'toggle') + printf 'toggle triggered' + ;; + 'o'|'option') + printf 'option supplied' + ;; + 'v'|'variable') + printf 'value supplied' + ;; + '?') + printf "INVALID OPTION" + ;; + ':') + printf "MISSING ARGUMENT" + ;; + *) + printf "NEVER REACHED" + ;; + esac + printf ' -- ' + declare -p OPTARG 2>&1 | grep -oe 'OPTARG.*' + done + + shift $(( OPTIND - 1 )) + + echo "OPTERR: ${OPTERR}" + echo "OPTKEY: ${OPTKEY}" + echo "OPTARG: ${OPTARG}" + echo "OPTIND: ${OPTIND}" + + local function_args=("$@") + declare -p function_args \ + | sed -e 's/declare -a function_args=/$@: /' +} + +proxy() { + getopts_function "$@" +} + +toggles() { + getopts_function -t --toggle -- user_arg +} + +options() { + getopts_function -o user_val1 --option user_val2 -- user_arg +} + +variables() { + getopts_function -vuser_val1 --variable=user_val2 -- user_arg +} + +disable_extdebug='false' +if ! shopt -q extdebug; then + disable_extdebug='true' + shopt -s extdebug +fi + +: "${1:?Missing required argument -- function name}" +function_name=${1} +shift + +if declare -f "$function_name" > /dev/null; then + ${function_name} "$@" +else + echo "Function not found -- ${function_name}" + exit 1 +fi + +if ${disable_extdebug}; then + shopt -u extdebug +fi diff --git a/test/bin/getopts_long-with_extdebug-verbose b/test/bin/getopts_long-with_extdebug-verbose new file mode 100755 index 0000000..1bed59a --- /dev/null +++ b/test/bin/getopts_long-with_extdebug-verbose @@ -0,0 +1,80 @@ +#!/usr/bin/env bash + +TOPDIR="$(cd "$(dirname "${0}")"/../.. && pwd)" +# shellcheck disable=SC1090 +source "${TOPDIR}/lib/getopts_long.bash" + +getopts_function() { + while getopts_long 'to:v: toggle option: variable:' OPTKEY; do + case ${OPTKEY} in + 't'|'toggle') + printf 'toggle triggered' + ;; + 'o'|'option') + printf 'option supplied' + ;; + 'v'|'variable') + printf 'value supplied' + ;; + '?') + printf "INVALID OPTION" + ;; + ':') + printf "MISSING ARGUMENT" + ;; + *) + printf "NEVER REACHED" + ;; + esac + printf ' -- ' + declare -p OPTARG 2>&1 | grep -oe 'OPTARG.*' + done + + shift $(( OPTIND - 1 )) + + echo "OPTERR: ${OPTERR}" + echo "OPTKEY: ${OPTKEY}" + echo "OPTARG: ${OPTARG}" + echo "OPTIND: ${OPTIND}" + + local function_args=("$@") + declare -p function_args \ + | sed -e 's/declare -a function_args=/$@: /' +} + +proxy() { + getopts_function "$@" +} + +toggles() { + getopts_function -t --toggle -- user_arg +} + +options() { + getopts_function -o user_val1 --option user_val2 -- user_arg +} + +variables() { + getopts_function -vuser_val1 --variable=user_val2 -- user_arg +} + +disable_extdebug='false' +if ! shopt -q extdebug; then + disable_extdebug='true' + shopt -s extdebug +fi + +: "${1:?Missing required parameter -- function name}" +function_name=${1} +shift + +if declare -f "$function_name" > /dev/null; then + ${function_name} "$@" +else + echo "Function not found -- ${function_name}" + exit 1 +fi + +if ${disable_extdebug}; then + shopt -u extdebug +fi diff --git a/test/bin/getopts_long-without_extdebug-silent b/test/bin/getopts_long-without_extdebug-silent new file mode 100755 index 0000000..2daa2b5 --- /dev/null +++ b/test/bin/getopts_long-without_extdebug-silent @@ -0,0 +1,80 @@ +#!/usr/bin/env bash + +TOPDIR="$(cd "$(dirname "${0}")"/../.. && pwd)" +# shellcheck disable=SC1090 +source "${TOPDIR}/lib/getopts_long.bash" + +getopts_function() { + while getopts_long ':to:v: toggle option: variable:' OPTKEY; do + case ${OPTKEY} in + 't'|'toggle') + printf 'toggle triggered' + ;; + 'o'|'option') + printf 'option supplied' + ;; + 'v'|'variable') + printf 'value supplied' + ;; + '?') + printf "INVALID OPTION" + ;; + ':') + printf "MISSING ARGUMENT" + ;; + *) + printf "NEVER REACHED" + ;; + esac + printf ' -- ' + declare -p OPTARG 2>&1 | grep -oe 'OPTARG.*' + done + + shift $(( OPTIND - 1 )) + + echo "OPTERR: ${OPTERR}" + echo "OPTKEY: ${OPTKEY}" + echo "OPTARG: ${OPTARG}" + echo "OPTIND: ${OPTIND}" + + local function_args=("$@") + declare -p function_args \ + | sed -e 's/declare -a function_args=/$@: /' +} + +proxy() { + getopts_function "$@" +} + +toggles() { + getopts_function -t --toggle -- user_arg +} + +options() { + getopts_function -o user_val1 --option user_val2 -- user_arg +} + +variables() { + getopts_function -vuser_val1 --variable=user_val2 -- user_arg +} + +enable_extdebug='false' +if shopt -q extdebug; then + enable_extdebug='true' + shopt -u extdebug +fi + +: "${1:?Missing required argument -- function name}" +function_name=${1} +shift + +if declare -f "$function_name" > /dev/null; then + ${function_name} "$@" +else + echo "Function not found -- ${function_name}" + exit 1 +fi + +if ${enable_extdebug}; then + shopt -s extdebug +fi diff --git a/test/bin/getopts_long-without_extdebug-verbose b/test/bin/getopts_long-without_extdebug-verbose new file mode 100755 index 0000000..7f07ef3 --- /dev/null +++ b/test/bin/getopts_long-without_extdebug-verbose @@ -0,0 +1,80 @@ +#!/usr/bin/env bash + +TOPDIR="$(cd "$(dirname "${0}")"/../.. && pwd)" +# shellcheck disable=SC1090 +source "${TOPDIR}/lib/getopts_long.bash" + +getopts_function() { + while getopts_long 'to:v: toggle option: variable:' OPTKEY; do + case ${OPTKEY} in + 't'|'toggle') + printf 'toggle triggered' + ;; + 'o'|'option') + printf 'option supplied' + ;; + 'v'|'variable') + printf 'value supplied' + ;; + '?') + printf "INVALID OPTION" + ;; + ':') + printf "MISSING ARGUMENT" + ;; + *) + printf "NEVER REACHED" + ;; + esac + printf ' -- ' + declare -p OPTARG 2>&1 | grep -oe 'OPTARG.*' + done + + shift $(( OPTIND - 1 )) + + echo "OPTERR: ${OPTERR}" + echo "OPTKEY: ${OPTKEY}" + echo "OPTARG: ${OPTARG}" + echo "OPTIND: ${OPTIND}" + + local function_args=("$@") + declare -p function_args \ + | sed -e 's/declare -a function_args=/$@: /' +} + +proxy() { + getopts_function "$@" +} + +toggles() { + getopts_function -t --toggle -- user_arg +} + +options() { + getopts_function -o user_val1 --option user_val2 -- user_arg +} + +variables() { + getopts_function -vuser_val1 --variable=user_val2 -- user_arg +} + +enable_extdebug='false' +if shopt -q extdebug; then + enable_extdebug='true' + shopt -u extdebug +fi + +: "${1:?Missing required parameter -- function name}" +function_name=${1} +shift + +if declare -f "$function_name" > /dev/null; then + ${function_name} "$@" +else + echo "Function not found -- ${function_name}" + exit 1 +fi + +if ${enable_extdebug}; then + shopt -s extdebug +fi