0

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
4
  • 1
    Isn't this a duplicate of stackoverflow.com/q/4069188/3266847? Commented Aug 15, 2019 at 2:07
  • Not really, because this question do not clearly or only marginally address the nameref type usage here: stackoverflow.com/a/55170447/7939871 I think My question/answer here can shed some light on a good use for nameref. For the record, I was refactoring an old Bash script where it gets the first index of an array using eval and cut and ugly stuffs like that. I wanted to modernize and get ride of the eval. Commented Aug 15, 2019 at 2:17
  • Worth noting that "eval" is simply one-vowel away from "evil". Anything, including a nameref that eliminates its use is a good thing. Commented Aug 15, 2019 at 3:47
  • 1
    Even namerefs are not fool-proof; better would be to use a language with proper data structures. Commented Aug 15, 2019 at 12:37

1 Answer 1

3

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
Sign up to request clarification or add additional context in comments.

3 Comments

Any particular reason for $'%q\n', rather than just '%q\n' (letting printf interpret the two-character \n sequence rather than giving it a one-character newline literal)?
Not really, just a personal taste of C-like strings I am used to and save headaches when it is time to quote constant strings I want to be sure nothing will get interpreted, expanded or globbed in any way.
It's pobably worth pointing out that this requires Bash 4.3 or newer.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.