Reputation: 19585
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
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