From af3fd83ef5ebe796cb6fa80d32b9a872cc5f69c0 Mon Sep 17 00:00:00 2001 From: Chris Vogel Date: Tue, 28 May 2024 12:42:52 +0200 Subject: [PATCH 1/2] Update getopts * allow positional and named parameters * re-organized code * still could be refactored again --- helpers/helpers.v1.d/getopts | 377 ++++++++++++++++++++--------------- 1 file changed, 218 insertions(+), 159 deletions(-) diff --git a/helpers/helpers.v1.d/getopts b/helpers/helpers.v1.d/getopts index f9ef5dc0b7..8658779961 100644 --- a/helpers/helpers.v1.d/getopts +++ b/helpers/helpers.v1.d/getopts @@ -1,5 +1,3 @@ -#!/bin/bash - # Internal helper design to allow helpers to use getopts to manage their arguments # # [internal] @@ -20,7 +18,7 @@ # | arg: $@ - Simply "$@" to tranfert all the positionnal arguments to the function # # This helper need an array, named "args_array" with all the arguments used by the helper -# that want to use ynh_handle_getopts_args +# that want to use ynh_handle_getopts_args # Be carreful, this array has to be an associative array, as the following example: # local -A args_array=( [a]=arg1 [b]=arg2= [c]=arg3 ) # Let's explain this array: @@ -36,180 +34,241 @@ # For the previous example, that means that $finalpath will be fill with the value given as argument for this option. # # Also, in the previous example, finalpath has a '=' at the end. That means this option need a value. -# So, the helper has to be call with --finalpath /final/path, --finalpath=/final/path or -f /final/path, the variable $finalpath will get the value /final/path +# So, the helper has to be call with --finalpath /final/path, --finalpath=/final/path or -f /final/path, +# the variable $finalpath will get the value /final/path # If there's many values for an option, -f /final /path, the value will be separated by a ';' $finalpath=/final;/path -# For an option without value, like --user in the example, the helper can be called only with --user or -u. $user will then get the value 1. +# For an option without value, like --user in the example, the helper can be called only with --user or -u. $user +# will then get the value 1. # # To keep a retrocompatibility, a package can still call a helper, using getopts, with positional arguments. -# The "legacy mode" will manage the positional arguments and fill the variable in the same order than they are given in $args_array. -# e.g. for `my_helper "val1" val2`, arg1 will be filled with val1, and arg2 with val2. +# The "legacy mode" will manage the positional arguments and fill the variable in the same order than they are given +# in $args_array. e.g. for `my_helper "val1" val2`, arg1 will be filled with val1, and arg2 with val2. + +# Positional parameters (used to be the only way to use ynh_handle_getopts_args once upon a time) can be +# used also: +# +# '--' start processing the rest of the arguments as positional parameters +# $legacy_args The arguments positional parameters will be assign to +# Needs to be composed of array keys of args_array. If a key for a predefined variable +# is used multiple times the assigned values will be concatenated delimited by ';'. +# If the long option variable to contain the data is predefined as an array (e.g. using +# `local -a arg1` then multiple values will be assigned to its cells. +# If the last positional parameter defined in legacy_args is defined as an array all +# the leftover positional parameters will be assigned to its cells. +# (it is named legacy_args, because the use of positional parameters was about to be +# deprecated before the last re-design of this sub) # # Requires YunoHost version 3.2.2 or higher. +# flohmarkt_ynh_handle_getopts_args() { ynh_handle_getopts_args() { # Manage arguments only if there's some provided set +o xtrace # set +x - if [ $# -ne 0 ]; then - # Store arguments in an array to keep each argument separated - local arguments=("$@") - - # For each option in the array, reduce to short options for getopts (e.g. for [u]=user, --user will be -u) - # And built parameters string for getopts - # ${!args_array[@]} is the list of all option_flags in the array (An option_flag is 'u' in [u]=user, user is a value) - local getopts_parameters="" - local option_flag="" - for option_flag in "${!args_array[@]}"; do - # Concatenate each option_flags of the array to build the string of arguments for getopts - # Will looks like 'abcd' for -a -b -c -d - # If the value of an option_flag finish by =, it's an option with additionnal values. (e.g. --user bob or -u bob) - # Check the last character of the value associate to the option_flag - if [ "${args_array[$option_flag]: -1}" = "=" ]; then - # For an option with additionnal values, add a ':' after the letter for getopts. - getopts_parameters="${getopts_parameters}${option_flag}:" - else - getopts_parameters="${getopts_parameters}${option_flag}" + if [ $# -eq 0 ]; then + ynh_print_warn --message="ynh_handle_getopts_args called without arguments" + return + fi + + # Store arguments in an array to keep each argument separated + local arguments=("$@") + + # For each option in the array, reduce to short options for getopts (e.g. for [u]=user, --user will be -u) + # And built parameters string for getopts + # ${!args_array[@]} is the list of all option_flags in the array (An option_flag is 'u' in [u]=user, user is a value) + local getopts_parameters="" + local option_flag="" + ## go through all possible options and replace arguments with short versions + for option_flag in "${!args_array[@]}"; do + # TODO refactor: Now I'm not sure anymore this part belongs here. To make the + # this all less hard to read and understand I'm thinking at the moment that it + # would be good to split the different things done here into their own loops: + # + # 1. build the option string $getopts_parameters + # 2. go through the arguments and add empty arguments where needed to + # allow for cases like '--arg= --value' where 'value' is a valid option, too + # 3. replace long option names by short once + # 4. (possibly add empty parameters for '-a -v' in cases where -a expects a value + # and -v is a valid option, too - but I dearly hope this will not be necessary) + # Concatenate each option_flags of the array to build the string of arguments for getopts + # Will looks like 'abcd' for -a -b -c -d + # If the value of an option_flag finish by =, it's an option with additionnal values. + # (e.g. --user bob or -u bob) + # Check the last character of the value associate to the option_flag + if [ "${args_array[$option_flag]: -1}" = "=" ]; then + # For an option with additionnal values, add a ':' after the letter for getopts. + getopts_parameters="${getopts_parameters}${option_flag}:" + else + getopts_parameters="${getopts_parameters}${option_flag}" + fi + # Check each argument given to the function + local arg="" + # ${#arguments[@]} is the size of the array + ## for one possible option: look at each argument supplied: + for arg in $(seq 0 $((${#arguments[@]} - 1))); do + # the following cases need to be taken care of + # '--arg=value' → works + # '--arg= value' → works + # '--arg=-value' → works + # '--arg= -v' or + # '--arg= --value' → works if not exists arg '[v]=value=' + # → $arg will be set to '-v' or '--value' + # but if exists '[v]=value=' this is not the expected behavior: + # → then $arg is expected to contain an empty value and '-v' or '--value' + # is expected to be interpreted as its own valid argument + # (found in use of ynh_replace_string called by ynh_add_config) + # solution: + # insert an empty arg into array arguments to be later interpreted by + # getopts as the missing value to --arg= + if [[ -v arguments[arg+1] ]] && [[ ${arguments[arg]: -1} == '=' ]]; then + # arg ends with a '=' + local this_argument=${arguments[arg]} + local next_argument=${arguments[arg+1]} + # for looking up next_argument in args_array remove optionally trailing '=' + next_argument=$( printf '%s' "$next_argument" | cut -d'=' -f1 ) + + # check if next_argument is a value in args_array + # → starts with '--' and the rest of the argument excluding optional trailing '=' + # of the string is a value in associative array args_array + # → or starts with '-' and the rest of the argument is a valid key in args_array + # (long argument could already have been replaced by short version before) + if ( [[ "${next_argument:0:2}" == '--' ]] \ + && printf '%s ' "${args_array[@]}" | fgrep -w "${next_argument:2}" > /dev/null ) \ + || ( [[ "${next_argument:0:1}" == '-' ]] \ + && printf '%s ' "${!args_array[@]}" | fgrep -w "${next_argument:1:1}" > /dev/null ) + then + # insert an empty value to array arguments to be interpreted as the value + # for argument[arg] + arguments=( ${arguments[@]:0:arg+1} '' ${arguments[@]:arg+1}) + fi fi - # Check each argument given to the function - local arg="" - # ${#arguments[@]} is the size of the array - for arg in $(seq 0 $((${#arguments[@]} - 1))); do - # Escape options' values starting with -. Otherwise the - will be considered as another option. - arguments[arg]="${arguments[arg]//--${args_array[$option_flag]}-/--${args_array[$option_flag]}\\TOBEREMOVED\\-}" - # And replace long option (value of the option_flag) by the short option, the option_flag itself - # (e.g. for [u]=user, --user will be -u) - # Replace long option with = (match the beginning of the argument) - arguments[arg]="$(printf '%s\n' "${arguments[arg]}" | sed "s/^--${args_array[$option_flag]}/-${option_flag} /")" - # And long option without = (match the whole line) - arguments[arg]="$(printf '%s\n' "${arguments[arg]}" | sed "s/^--${args_array[$option_flag]%=}$/-${option_flag} /")" - done + + # Replace long option with = (match the beginning of the argument) + arguments[arg]="$(printf '%s\n' "${arguments[arg]}" \ + | sed "s/^--${args_array[$option_flag]}/-${option_flag}/")" + # And long option without = (match the whole line) + arguments[arg]="$(printf '%s\n' "${arguments[arg]}" \ + | sed "s/^--${args_array[$option_flag]%=}$/-${option_flag}/")" done + done + + # Parse the first argument, return the number of arguments to be shifted off the arguments array + # The function call is necessary here to allow `getopts` to use $@ + parse_arg() { + # Initialize the index of getopts + OPTIND=1 + # getopts will fill $parameter with the letter of the option it has read. + local parameter="" + getopts ":$getopts_parameters" parameter || true - # Read and parse all the arguments - # Use a function here, to use standart arguments $@ and be able to use shift. - parse_arg() { - # Read all arguments, until no arguments are left - while [ $# -ne 0 ]; do - # Initialize the index of getopts - OPTIND=1 - # Parse with getopts only if the argument begin by -, that means the argument is an option - # getopts will fill $parameter with the letter of the option it has read. - local parameter="" - getopts ":$getopts_parameters" parameter || true - - if [ "$parameter" = "?" ]; then - ynh_die --message="Invalid argument: -${OPTARG:-}" - elif [ "$parameter" = ":" ]; then - ynh_die --message="-$OPTARG parameter requires an argument." - else - local shift_value=1 - # Use the long option, corresponding to the short option read by getopts, as a variable - # (e.g. for [u]=user, 'user' will be used as a variable) - # Also, remove '=' at the end of the long option - # The variable name will be stored in 'option_var' - local option_var="${args_array[$parameter]%=}" - # If this option doesn't take values - # if there's a '=' at the end of the long option name, this option takes values - if [ "${args_array[$parameter]: -1}" != "=" ]; then - # 'eval ${option_var}' will use the content of 'option_var' - eval ${option_var}=1 - else - # Read all other arguments to find multiple value for this option. - # Load args in a array - local all_args=("$@") - - # If the first argument is longer than 2 characters, - # There's a value attached to the option, in the same array cell - if [ ${#all_args[0]} -gt 2 ]; then - # Remove the option and the space, so keep only the value itself. - all_args[0]="${all_args[0]#-${parameter} }" - - # At this point, if all_args[0] start with "-", then the argument is not well formed - if [ "${all_args[0]:0:1}" == "-" ]; then - ynh_die --message="Argument \"${all_args[0]}\" not valid! Did you use a single \"-\" instead of two?" - fi - # Reduce the value of shift, because the option has been removed manually - shift_value=$((shift_value - 1)) - fi - - # Declare the content of option_var as a variable. - eval ${option_var}="" - # Then read the array value per value - local i - for i in $(seq 0 $((${#all_args[@]} - 1))); do - # If this argument is an option, end here. - if [ "${all_args[$i]:0:1}" == "-" ]; then - # Ignore the first value of the array, which is the option itself - if [ "$i" -ne 0 ]; then - break - fi - else - # Ignore empty parameters - if [ -n "${all_args[$i]}" ]; then - # Else, add this value to this option - # Each value will be separated by ';' - if [ -n "${!option_var}" ]; then - # If there's already another value for this option, add a ; before adding the new value - eval ${option_var}+="\;" - fi - - # Remove the \ that escape - at beginning of values. - all_args[i]="${all_args[i]//\\TOBEREMOVED\\/}" - - # For the record. - # We're using eval here to get the content of the variable stored itself as simple text in $option_var... - # Other ways to get that content would be to use either ${!option_var} or declare -g ${option_var} - # But... ${!option_var} can't be used as left part of an assignation. - # declare -g ${option_var} will create a local variable (despite -g !) and will not be available for the helper itself. - # So... Stop fucking arguing each time that eval is evil... Go find an other working solution if you can find one! - - eval ${option_var}+='"${all_args[$i]}"' - fi - shift_value=$((shift_value + 1)) - fi - done - fi - fi + if [ "$parameter" = "?" ]; then + ynh_die --message="Invalid argument: -${OPTARG:-}" + exit 255 + elif [ "$parameter" = ":" ]; then + ynh_die --message="-$OPTARG parameter requires an argument." + echo "-$OPTARG parameter requires an argument." + exit 255 + else + # Use the long option, corresponding to the short option read by getopts, as a variable + # (e.g. for [u]=user, 'user' will be used as a variable) + # Also, remove '=' at the end of the long option + # The variable name will be stored in 'option_var' as a nameref + option_var="${args_array[$parameter]%=}" + # if there's a '=' at the end of the long option name, this option takes values + if [ "${args_array[$parameter]: -1}" != "=" ]; then + # no argument expected for option - set option variable to '1' + option_value=1 + else + # remove leading and trailing spaces from OPTARG + OPTARG="$( printf '%s' "${OPTARG}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" + option_value="${OPTARG}" + fi + # set shift_value according to the number of options interpreted by getopts + shift_value=$(( $OPTIND - 1 )) + fi + } + + # iterate over the arguments: if first argument starts with a '-' feed arguments to getopts + # if first argument doesn't start with a '-' enter mode to read positional parameters + local argument + local positional_mode=0 # state is getopts mode at the beginning, not positional parameters + local positional_count=0 # counter for positional parameters + local option_var='' # the variable name to be filled + # Try to use legacy_args as a list of option_flag of the array args_array + # Otherwise, fill it with getopts_parameters to get the option_flag. + # (But an associative arrays isn't always sorted in the correct order...) + # Remove all ':' in getopts_parameters, if used. + legacy_args=${legacy_args:-${getopts_parameters//:/}} + while [[ -v 'arguments' ]] && [[ ${#arguments} -ne 0 ]]; do + local shift_value=0 + local option_value='' # the value to be filled into ${!option_var} + argument=${arguments[0]} + # if state once changed to positional parameter mode, all the rest of the arguments will + # be interpreted in positional parameter mode even if they start with a '-' + if [ $positional_mode == 0 ] && [ "${argument}" == '--' ];then + positional_mode=1 + shift_value=1 + elif [ $positional_mode == 0 ] && [ "${argument:0:1}" == '-' ]; then + parse_arg "${arguments[@]}" + else + positional_mode=1 # set state to positional parameter mode + + # Get the option_flag from getopts_parameters by using the option_flag according to the + # position of the argument. + option_flag=${legacy_args:$positional_count:1} + + # increment counter for legacy_args if still args left. If no args left check if the + # last arg is a predefined array and let it cells be filled. Otherwise complain and + # return. + if [[ $positional_count -le $((${#legacy_args} - 1)) ]]; then + # set counter to for next option_flag to fill + positional_count=$((positional_count+1)) - # Shift the parameter and its argument(s) - shift $shift_value - done - } - - # LEGACY MODE - # Check if there's getopts arguments - if [ "${arguments[0]:0:1}" != "-" ]; then - # If not, enter in legacy mode and manage the arguments as positionnal ones.. - # Dot not echo, to prevent to go through a helper output. But print only in the log. - set -x - echo "! Helper used in legacy mode !" >/dev/null - set +x - local i - for i in $(seq 0 $((${#arguments[@]} - 1))); do - # Try to use legacy_args as a list of option_flag of the array args_array - # Otherwise, fallback to getopts_parameters to get the option_flag. But an associative arrays isn't always sorted in the correct order... - # Remove all ':' in getopts_parameters - getopts_parameters=${legacy_args:-${getopts_parameters//:/}} - # Get the option_flag from getopts_parameters, by using the option_flag according to the position of the argument. - option_flag=${getopts_parameters:$i:1} - if [ -z "$option_flag" ]; then - ynh_print_warn --message="Too many arguments ! \"${arguments[$i]}\" will be ignored." - continue - fi # Use the long option, corresponding to the option_flag, as a variable # (e.g. for [u]=user, 'user' will be used as a variable) # Also, remove '=' at the end of the long option # The variable name will be stored in 'option_var' - local option_var="${args_array[$option_flag]%=}" + option_var="${args_array[$option_flag]%=}" + elif [[ $positional_count -ge $((${#legacy_args} - 1)) ]] && + ! declare -p ${option_var} | grep '^declare -a' + then + # no more legacy_args to fill - legacy behaviour: complain and return + ynh_print_warn --message="Too many arguments ! \"${arguments[$i]}\" will be ignored." + return + else + fi - # Store each value given as argument in the corresponding variable - # The values will be stored in the same order than $args_array - eval ${option_var}+='"${arguments[$i]}"' - done - unset legacy_args + # value to be assigned to ${!option_var} + option_value=$argument + + # shift off one positional parameter + shift_value=1 + fi + + # fill option_var with value found + # if ${!option_var} is an array, fill mutiple values as array cells + # otherwise concatenate them seperated by ';' + # nameref is used to access the variable that is named $option_var + local -n option_ref=$option_var + # this defines option_ref as a reference to the variable named "$option_var" + # any operation on option_ref will be written to the variable named "$option_var" + # 'option_ref="hello world"' will work like as if '${!option_var}="hello world"' + # would be a valid syntax + # see also: `man bash` part about commands 'declare' option '-n' + if declare -p $option_var | grep '^declare -a ' > /dev/null; then + # hurray it's an array + ${option_ref}+='("${option_value}")' + elif ! [[ -v "$option_var" ]] || [[ -z "$option_ref" ]]; then + option_ref=${option_value} else - # END LEGACY MODE - # Call parse_arg and pass the modified list of args as an array of arguments. - parse_arg "${arguments[@]}" + option_ref+=";${option_value}" fi - fi + + # shift value off arguments array + arguments=("${arguments[@]:${shift_value}}") + done + + # the former subroutine did this - no idea if it is expected somewhere + unset legacy_args + + # re-enable trace set -o xtrace # set -x } From f78775c3fe3ad93eeba38f55b7ec9b26513f38a7 Mon Sep 17 00:00:00 2001 From: Chris Vogel Date: Tue, 28 May 2024 12:43:53 +0200 Subject: [PATCH 2/2] Update getopts she-bang --- helpers/helpers.v1.d/getopts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/helpers/helpers.v1.d/getopts b/helpers/helpers.v1.d/getopts index 8658779961..23028dfcac 100644 --- a/helpers/helpers.v1.d/getopts +++ b/helpers/helpers.v1.d/getopts @@ -1,3 +1,5 @@ +#!/bin/bash + # Internal helper design to allow helpers to use getopts to manage their arguments # # [internal]