Thursday, February 8, 2024

[SOLVED] How do i iterate through an associative array in Bash where the values are arrays?

Issue

I wrote this code to loop through usernames and domains on my LAN. Sadly, the script prints nothing.

#!/bin/bash

construct_array_of_trgts() {
  declare -A usrs_n_dmns
  
  local -a guest_dmns
  local -a usrs=("j" "jim" "o" "root")
  local -a guest_dmns=("raspberrypi" "lenovo")
  for d in "${guest_dmns[@]}"; do 
    PS3="Select the users to include for sshing into $d. Q when done selecting."$'\n'
    local -a targt_usrs
    select u in "${usrs[@]}"; do
      if [[ "$u" ]]; then
        targt_usrs+=("$u")
     elif [[ "$REPLY" == 'q' ]]; then 
      break;
     fi
    done
    usrs_n_dmns["${d}"]="$targt_usrs"
  done
  
}
construct_array_of_trgts

for d in "${!usrs_n_dmns[@]}"; do
  targt_usrs=("${usrs_n_dmns["${d}"]}")
  echo "$usrs_n_dmns"
  for u in "${targt_usrs[@]}"; do
    echo "ssh ${u}@${d}" 
  done
done

Why doesn't this script print anything visible? Is it at all possible for an array to be a value in an associative array in Bash?


Solution

As @KamilCuk explained, there are no nested or multi-dimensional arrays in Bash.

Bash arrays can only store scalar or integer values.

What is possible though, is to pass a whole array as a space-delimited concatenation of individually quoted elements that are suitable to be reused as-is in a Bash declare statement.

  1. To quote values, Bash version 4.4+ provides the @Q expansion parameter.
  2. To join array elements into a single string with each element space-delimited it needs * as the array expansion such as ${array[*]}.

Both value quoting parameter and array to string join can be combined into a single expansion such as: ${array[*]@Q}

For example:

#!/usr/bin/env bash

declare -a array=(foo bar 'Hello World')

printf '(%s)\n' "${array[*]@Q}"

Fixed your code and now it works:

#!/usr/bin/env bash

construct_array_of_trgts() {
  local -a usrs=("j" "jim" "o" "root")
  local -a guest_dmns=("raspberrypi" "lenovo")
  for d in "${guest_dmns[@]}"; do
    PS3="Select the users to include for sshing into $d. Q when done selecting."$'\n'
    local -a targt_usrs=()
    while :; do
      select u in "${usrs[@]}"; do
        if [[ "$u" ]]; then
          targt_usrs+=("$u")
        elif [[ "$REPLY" == 'q' ]]; then
          break 2
        fi
      done
    done
    # Array to scalar of space-delimited quoted values
    usrs_n_dmns["${d}"]="${targt_usrs[*]@Q}"
  done
}

declare -A usrs_n_dmns

construct_array_of_trgts

for d in "${!usrs_n_dmns[@]}"; do
  # Scalar value is expanded into array delcare
  declare -a targt_usrs="(${usrs_n_dmns[$d]})"
  for u in "${targt_usrs[@]}"; do
    echo ssh "${u}@${d}"
  done
done


Answered By - Léa Gris
Answer Checked By - David Marino (WPSolving Volunteer)