1

I'm for hours trying to figure this one out.

What I would like to do is have a function that can receive N parameters which can include arrays.

Arrays can be in any parameter position, not only the last!

Order of parameters can change, for example, I can pass the first parameter as an array one time and in another call pass as second parameter!

Arrays and normal parameters must be interchangeable!

Example code (Note in this example parameter order is fixed, see following example for problem):

# $1 Menu title
# $2 List of menu options
# $3 Menu text
menu() {
        dialog --clear --stdout --title "$1" --menu "$3" 0 0 0 "$2"
}

options=(
        1 "Option 1"
        2 "Option 2"
        3 "Option 3"
        4 "Option 4")
menu "Title" ${options[@]} "Text"

The first example can be solved like this:

# $1 Menu title
# $2 List of menu options
# $3 Menu text
menu() {
        local -n a=$2
        dialog --clear --stdout --title "$1" --menu "$3" 0 0 0 "${a[@]}"
}

options=(
        1 "Option 1"
        2 "Option 2"
        3 "Option 3"
        4 "Option 4")
menu "Title" options "Text"

Problem example:

my_func() {
        my_command "$1" --something "$2" "$3" --somethingelse "$4"
}

optionsa=(
        1 "Option 1"
        2 "Option 2"
        3 "Option 3"
        4 "Option 4")

optionsb=(
        1 "Option 1"
        2 "Option 2"
        3 "Option 3"
        4 "Option 4")

my_func "Title" ${optionsa[@]} "Text" ${optionsb[@]}
# Note that I could do this and it must work too:
my_func ${optionsa[@]} "Title" ${optionsb[@]} "Text"
# This too must work:
my_func ${optionsa[@]} ${optionsb[@]} "Title" "Text"

When the array is expanded it must be possible to use the values as parameters to commands, if the value in the array has multiple words quoted ("Option 1") it must be treated as a single parameter to avoid for example passing a path as "/my dir/" and having it separated as </my> <dir/>.

How can I solve the second example, where the order or parameter can vary?

user5507535
  • 1,580
  • 1
  • 18
  • 39

1 Answers1

3

How can I solve the second example, where the order or parameter can vary?

There are two universal independent of programming language solutions to join a variadic length of a list of items together that any programmers should know about:

  1. Pass the count...

my_func() {
    local tmp optionsa title text optionsb
    tmp=$1
    shift
    while ((tmp--)); do
         optionsa+=("$1")
         shift
    done
    title="$1"
    shift
    tmp=$1
    shift
    while ((tmp--)); do
        optionsb+=("$1")
        shift
    done
    text=$1
    my_command "${optionsa[@]}" "$title" "${optionsb[@]}" "$text"
}

my_func 0 "Title" 0 "Text"
my_func 0 "Title" "${#optionsb[@]}" "${optionsb[@]}" "Text"
my_func "${#optionsa[@]}" "${optionsa[@]}" "Title" "${#optionsb[@]}" "${optionsb[@]}" "Text"
  1. Use a sentinel value.

my_func() {
    local tmp optionsa title text optionsb
    while [[ -n "$1" ]]; do
          optionsa+=("$1")
          shift
    done
    title=$1
    shift
    while [[ -n "$1" ]]; do
          optionsb+=("$1")
          shift
    done
    text=$1
    my_command "${optionsa[@]}" "$title" "${optionsb[@]}" "$text"
}
my_func "" "Title" "" "Text"
my_func "" "Title" "${optionsb[@]}" "" "Text"
my_func "${optionsa[@]}" "" "Title" "${optionsb[@]}" "" "Text"

And I see two bash specific solutions:

  1. Pass arrays as names and use namereferences.

my_func() {
    # Use unique names to avoid nameclashes
    declare -n _my_func_optionsa=$1
    local title=$2
    declare -n _my_func_optionsb=$3
    local title=$4
    my_command "${_my_func_optionsa[@]}" "$title" "${_my_func_optionsb[@]}" "$text"
}

# arrays have to exists
my_func optionsa "Title" optionsb "Text"
  1. Parse the arguments like a real man. This is actually universal solution, as it generally performs data serialization when creating list of arguments and then data deserialization when reading the arguments - the format (arguments as options) is specific to shell.

my_func() {
    local args
    # I am used to linux getopt, getopts would work as well
    if ! args=$(getopt -n "$my_func" -o "a:t:b:x:" -- "$@"); then
           echo "my_func: Invalid arguments" >&2
           return 1
    fi
    set -- "$args"
    local optionsa title optionsb text
    while (($#)); do
       case "$1" in
       -a) optionsa+=("$2"); shift; ;;
       -t) title="$2"; shift; ;;
       -b) optionsb+=("$2"); shift; ;;
       -x) text="$2"; shift; ;;
       *) echo "my_func: Error parsing argument: $1" >&2; return 1; ;;
       esac
       shift
    done
    my_command "${optionsa[@]}" "$title" "${optionsb[@]}" "$text"
}

my_func -a opta1 -a opta2 -t Title -b optb1 -b optb2 -x text

# or build the options list from arrays:
# ie. perform data serialization
args=()
for i in "${optionsa[@]}"; do
   args+=(-a "$i")
done
args+=(-t "title")
for i in "${optionsb[@]}"; do args+=(-b "$i"); done
args+=(-x "text")
my_func "${args[@]}"

Generally if a function has constant and small count of arguments, just use the arguments. If functions get complicated with more edge cases, I recommend to parse the arguments like a man - makes the function versatile and abstract, easy to expand and implement edge cases and handle corner cases and errors, easy to understand by other programmers, easily readable and parsable by human eyes.

Because your example codes may some problems, I recommend to research how does quoting work in shelll, specifically how "${array[@]}" differ from ${array[@]}, research how [@] differ from [*], how does and when word splitting expansion is performed and how it affects the parameters. All the unquoted array expansions in your code suffer from word splitting - the spaces will not be preserved, also in the first example.

KamilCuk
  • 120,984
  • 8
  • 59
  • 111