3
#!/usr/bin/bash


ARGENT=("Nous devons économiser de l'argent."
"Je dois économiser de l'argent.")

BIENETRE=("Comment vas-tu?" "Tout va bien ?")

aoarrs=("${ARGENT}" "${BIENETRE}")

select arr in "${aoarrs[@]}"; do
  for el in "${arr[@]}"; do
    echo "$el"
  done
  break
done 

I want this script to print the array names to the user, ARGENT and BIENETRE, so that the user can select one of them. After the user's input the script is meant to print every element of an array selected. I want to select with select an array to loop through from an array of arrays (aoarrs). The reason why I want to use select is because in the real world my array of arrays may have many more than just two arrays in it. How might I accomplish that?

3
  • Note that ${ARGENT} is the same as ${ARGENT[0]} Commented Aug 28, 2023 at 13:01
  • You really don't want to use CAPS for shell variable names. Since, by convention, global environment variables are capitalized, it is bad practice to also use caps for your own, local variables because this can lead to naming collisions and hard to find bugs. Commented Aug 28, 2023 at 15:57
  • Please paste your script at shellcheck.net. Commented Aug 28, 2023 at 23:15

4 Answers 4

6

You'll store the array names in aoarrs, and inside the select body declare a nameref to the chosen name:

ARGENT=("Nous devons économiser de l'argent."
"Je dois économiser de l'argent.")
BIENETRE=("Comment vas-tu?" "Tout va bien ?")
aoarrs=(ARGENT BIENETRE)

PS3='Which array? '
select arr in "${aoarrs[@]}"; do 
    [[ $arr ]] || continue
    declare -n ref=$arr
    for i in "${!ref[@]}"; do 
        printf '%d\t%s\n' $i "${ref[i]}"
    done 
    break
done

Running might look like

1) ARGENT
2) BIENETRE
Which array? 3
Which array? 4
Which array? 5
Which array? 2
0   Comment vas-tu?
1   Tout va bien ?
2

You'd want a mapping of "keys" to "values", where "values" are the lists of strings, and the "keys" are ARGENT, BIENETRE

You're on the right path with aoarrs, because you could use that array as associative array:

declare -A aoarrs
aoarrs[ARGENT]=$ARGENT
aoarrs[BIENETRE]=$BIENETRE

and then just iterate over all keys in that array using something like for key in ${!aoarrs[@]}….

Sadly, bash doesn't, for whatever reason, allow lists to be elements of these associative arrays.

So, things suck. You can for example join the elements of your lists with a reserved character to split them later on (that's stupid because it means you can't have all characters in your string, or need to start escaping them), or you build your own functions that take lists of strings, append them to a large array and then you implement your own associative lookup function on that container (that would be stupid; not only would it be slow, it would also require you to write relatively much code in a relatively ill-suited language). It would look terrible. Here's an example which I write down without testing it, because it's ugly enough that I need to get it out of my head, but don't want to deal with it any further:

#!/bin/bash
###############################
#        UNTESTED STUFF       #
# If you think this code is   #
# acceptable, consider doing  #
# something good for yourself #
# once in a while             #
###############################
declare -A index
declare -A lengths
declare -a storage

# Adds an entry to our our custom container
# 
# Usage:
# add_key_values KEY "list element 1" "list element 2" …
function add_key_values() {
  local key="$1"
  shift
  local -n valuelist=$@

  # get the length of the passed list, to save it
  local lengthlist=${#valuelist[@]}


  # get the end of the current storage, that's where we start adding
  # our list
  local start_index=${#storage[@]}

  # finally, actually store the list items in the storage
  for item in "${valuelist[@]}"; do
    storage+=("${item}")
  done
  lengths["${key}"]=$lengthlist
  index["${key}"]=$start_index
}

# Retrieve a list from the storage
# Sadly, bash is not a proper programming language, because what it calls
# "functions" don't do the one thing that a function needs to do:
# return a value for an argument. There's just no "return" mechanism in bash.
# 
# Returns an empty list if the key wasn't found.
#
# So, after sobbing a bit, we just say
# Usage:
# get_values $key
# Overwrites the `ret_val` variable with the list that was saved earlier
function get_values() {
  # prepare ret_val
  declare -g -a ret_val=()

  local key=$1

  # We return (with ret_val empty) if key isn't present
  # frigging bash doesn't have a "is key present in associative array" function…
  # so this is the workaround to check whether there's $key in $index.
  # seriously.
  local teststring
  teststring="$(printf 'index[%s]' "${key}")"
  # you need quite modern bash to even get the "is defined" -v test
  [[ -v "${teststring}" ]] || return

  # let's get all the elements from storage and append them to ret_val
  local start=${index[$key]}
  local length=${lengths[$key]}
  for idx in $(seq $(( start - 1 )) $((start - 1 + length)) ); do 
    ret_val+=("${storage[idx]}")
  done
}

####################
# EXAMPLE USAGE
####################
add_key_values "ARGENT" "Nous devons économiser de l'argent." "Je dois économiser de l'argent."
add_key_values "BIENETRE" ("Comment vas-tu?" "Tout va bien ?")

for key in ${!index[@]}; do
  echo "the list for value $key contains"
  get_values "${key}"
  for element in ${ret_val[@]}; do
    echo "element: ${element}"
  done
done

The next option is magic that involves "indirect addressing" of variables by name using eval. That's kind of evil, and stupid, and there's very many posts on here that hint at "if you're at that point, then maybe use a proper programming language instead of bash".

I'd concur with that: This whole problem would literally be done in four lines of python, with the first two lines would be storing "ARGENT" and "BIENETRE" and their lists into a dict. Or really, in any other common language that's not bash (or C, for that matter), associative arrays are less bad.

1

Spanning array has names of variables only:

aoarrs=(ARGENT BIENETRE)

while :; do
        select arr in "${aoarrs[@]}" quit; do
                declare -n ref=$arr
                case $arr in
                        quit)
                                break 2;; 
                        *)
                                select item in "${ref[@]}"; do
                                        echo $item
                                        break
                                done;;
                esac
                break
        done
done

declare -n ref=$arr - make a reference to the variable named by its value.
break 2 - break 2 enclosing loops.

1

It would be easier in ksh93 (the shell bash tries to emulate) where associative arrays can have arrays (among other things) as values.

#! /bin/ksh -

typeset -A all=(
  [argent]=(
    "Nous devons économiser de l'argent."
    "Je dois économiser de l'argent."
  )

  [bien-être]=(
    "Comment vas-tu?"
    "Tout va bien ?"
  )
)

select topic in "${!all[@]}"; do
  for sentence in "${all[$topic][@]}"; do
    print -r -- "$sentence"
  done
  break
done

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.