Léa Gris
Léa Gris

Reputation: 19585

Bash function to get the keys of an arbitrary array, without using eval

I wrote a function to get the keys of an arbitrary array.

It works as intended but is using the evil eval.

How would you rewrite it without using eval?

#!/usr/bin/env bash
# shellcheck disable=2034

# Return indexes of the array name
# @Params:
# $1: Name of the array
# @Output:
# >&1: Newline delimited list of indexes
function get_keys() {
  eval echo "\${!$1[@]}" | tr ' ' $'\n'
}

# Testing the get_keys function

# A numerical indexed array
declare -a a=([5]="a" [8]="b" [10]="c" [15]="d")
printf $'Standard array a:\nIndexes\tValues\n'
while read -r k; do
  printf $'%q\t%q\n' "$k" "${a[$k]}"
done < <(get_keys a)
echo

# An associative array
declare -A b=(["foo"]="hello" ["bar"]="world")
printf $'Associative array b:\nKeys\tValues\n'
while read -r k; do
  printf $'%q\t%q\n' "$k" "${b[$k]}"
done < <(get_keys b)
echo

Output:

Standard array a:
Indexes Values
5       a
8       b
10      c
15      d

Associative array b:
Keys    Values
foo     hello
bar     world

Upvotes: 0

Views: 109

Answers (1)

L&#233;a Gris
L&#233;a Gris

Reputation: 19585

The trick to allow indirection from the function's argument, is to declare a variable to be a nameref type with the -n switch:

A variable can be assigned the nameref attribute using the -n option to the declare or local builtin commands ... A nameref is commonly used within shell functions to refer to a variable whose name is passed as an argument to the function. For instance, if a variable name is passed to a shell function as its first argument, running

          declare -n ref=$1

inside the function creates a nameref variable ref whose value is the variable name passed as the first argument.

IMPORTANT !

Bash version ≥ 4.3 is required for the nameref variable type.

The get_keys function can be rewritten like this without eval:

# Return indexes of the array name
# @Params:
# $1: Name of the array
# @Output:
# >&1: Null delimited list of indexes
function get_keys() {
  local -n ref_arr="$1" # nameref of the array name argument
  printf '%s\0' "${!ref_arr[@]}" # null delimited for arbitrary keys
}

Note that to be compatible with arbitrary keys witch may contain control characters, the list is returned null-delimited. It has to be considered while reading the output of the function.

So here is a full implementation and test of the get_keys and companion utility functions get_first_key, get_last_key and get_first_last_keys:

#!/usr/bin/env bash

# Return indexes of the array name
# @Params:
# $1: Name of the array
# @Output:
# >&1: Null delimited list of indexes
function get_keys() {
  local -n ref_arr="$1" # nameref of the array name argument
  printf '%s\0' "${!ref_arr[@]}"
}

# Return the first index of the array name
# @Params:
# $1: Name of the array
# @Output:
# >&1: the first index of the array
function get_first_key() {
  local -- first_key
  IFS= read -r -d '' first_key < <(get_keys "$1")
  printf '%s' "$first_key"
}

# Return the last index of the array name
# @Params:
# $1: Name of the array
# @Output:
# >&1: the last index of the array
function get_last_key() {
  local -- key last_key
  while IFS= read -r -d '' key && [ -n "$key" ]; do
    last_key="$key"
  done < <(get_keys "$1") # read keys until last one
  printf '%s' "$last_key"
}

# Return the first and the last indexes of the array name
# @Params:
# $1: Name of the array
# @Output:
# >&1: the first and last indexes of the array
function get_first_last_keys() {
  local -- key first_key last_key IFS=
  {
    read -r -d '' first_key # read the first key
    last_key="$first_key"   # in case there is only one key
    while IFS= read -r -d '' key && [ -n "$key" ]; do
      last_key="$key" # we'v read a new last key
    done
  } < <(get_keys "$1")
  printf '%s\0%s\0' "$first_key" "$last_key"
}

# Testing the get_keys function

# A numerical indexed array
declare -a a=([5]="a" [8]="b" [10]="c" [15]="d")
printf $"Standard array %s:\\n\\n" 'a'
typeset -p a
echo
printf '%-7s %-8s\n' $"Indexes" $"Values"
echo '----------------'

declare -i i # Array index as integer
# Iterate all array indexes returned by get_keys
while IFS= read -r -d '' i; do
  printf '%7d %-8s\n' "$i" "${a[$i]}"
done < <(get_keys a)
echo

# An associative array
unset b
declare -A b=(
  [$'\7']="First"
  [$'foo\nbar']="hello"
  ["bar baz"]="world"
  [";ls -l"]="command"
  ["No more!"]="Last one"
)
printf $"Associative array %s:\\n\\n" 'b'
typeset -p b
echo
printf '%-13s %-8s\n' $"Keys" $"Values"
echo '----------------------'
declare -- k # Array key
# Iterate all array keys returned by get_keys
while IFS= read -r -d '' k; do
  printf '%-13q %-8s\n' "$k" "${b[$k]}"
done < <(get_keys b)
echo
printf $"First key: %q\\n" "$(get_first_key b)"
printf $"Last key: %q\\n" "$(get_last_key b)"
declare -- first_key last_key
{
  IFS= read -r -d '' first_key
  IFS= read -r -d '' last_key
} < <(get_first_last_keys b)
printf $"First value: %s\\nLast value: %s\\n" "${b[$first_key]}" "${b[$last_key]}"

Output:

Standard array a:

declare -a a=([5]="a" [8]="b" [10]="c" [15]="d")

Indexes Values  
----------------
      5 a       
      8 b       
     10 c       
     15 d       

Associative array b:

declare -A b=(["No more!"]="Last one" [$'\a']="First" ["bar baz"]="world" [$'foo\nbar']="hello" [";ls -l"]="command" )

Keys          Values  
----------------------
No\ more\!    Last one
$'\a'         First   
bar\ baz      world   
$'foo\nbar'   hello   
\;ls\ -l      command 

First key: No\ more\!
Last key: \;ls\ -l
First value: Last one
Last value: command

Upvotes: 3

Related Questions