Skip to content

Commit

Permalink
Adds support for nested function arguments. (#31)
Browse files Browse the repository at this point in the history
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 <billyzkid@yahoo.com>
  • Loading branch information
UrsaDK and billyzkid authored Dec 17, 2024
1 parent 9b94aad commit 737a918
Show file tree
Hide file tree
Showing 11 changed files with 600 additions and 7 deletions.
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
25 changes: 21 additions & 4 deletions lib/getopts_long.bash
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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%%=*}"
Expand Down
31 changes: 31 additions & 0 deletions test/bats/github_17a.bats
Original file line number Diff line number Diff line change
@@ -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'
}
35 changes: 35 additions & 0 deletions test/bats/github_17b.bats
Original file line number Diff line number Diff line change
@@ -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"
}
31 changes: 31 additions & 0 deletions test/bats/github_17c.bats
Original file line number Diff line number Diff line change
@@ -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'
}
80 changes: 80 additions & 0 deletions test/bin/getopts_long-explicit_args-silent
Original file line number Diff line number Diff line change
@@ -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
80 changes: 80 additions & 0 deletions test/bin/getopts_long-explicit_args-verbose
Original file line number Diff line number Diff line change
@@ -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
80 changes: 80 additions & 0 deletions test/bin/getopts_long-with_extdebug-silent
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 737a918

Please sign in to comment.