How to Look Up a Variable by Name with #!/Bin/Sh (Posix Sh)

How can I look up a variable by name with #!/bin/sh (POSIX sh)?

You can use eval to "nest" variable substitutions.

f1="filename1";
i=1;
eval c=\${f$i}
echo $c

sh: variable value access by string name

After some dwelling, reading posix shell manual and finding any good in posix utilities I finally settled I would use variable expansions ${var:?} and ${var?} to check if a variable is set or unset, null or not null and that I will use expr utility with BRE posix regex to check if a variable is a valid variable name.

Below are the functions that I have ended up with. A small test function and some test cases are on the end. I feel like the expr BRE matching is the most not-portable part of it all, however I couldn't find any false positives in var_is_name.

#!/bin/sh

# var ####################################################################################################

#
# Check if arguments are a valid "name" identifier in the POSIX shell contects
# @args identifiers
# @returns
# 0 - all identifiers are valid names
# 1 - any one of identifiers is not a valid name
# 2 - internal error
# 3 - even worse internal error
var_is_name() {
# 3.230 Name
# In the shell command language, a word consisting solely of underscores, digits, and alphabetics from the portable character set. The first character of a name is not a digit.
local _var_is_name_i
for _var_is_name_i; do
expr "$_var_is_name_i" : '[_a-zA-Z][_a-zA-Z0-9]*$' >/dev/null || return $?
done
}

# @args identifiers
# @returns Same as var_is_name but returns `2` in case if any of the arguments is not a valid name
var_is_name_error_on_fail() {
local _var_is_name_error_on_fail_ret
var_is_name "$@" && _var_is_name_error_on_fail_ret=$? || _var_is_name_error_on_fail_ret=$?
if [ "$_var_is_name_error_on_fail_ret" -eq 1 ]; then return 2
elif [ "$_var_is_name_error_on_fail_ret" -ne 0 ]; then return "$_var_is_name_error_on_fail_ret"
fi
}

# @args identifiers
# @returns
# 0 - if all identifiers are set
# 1 - if any of the identifiers is not set
# other - in case of error (ex. any of the identifiers is not a valid name)
var_is_set() {
var_is_name_error_on_fail "$@" || return $?
local _var_is_set_i
for _var_is_set_i; do
if ! ( eval printf %.0s "\"\${$_var_is_set_i?}\"" ) 2>/dev/null; then
return 1
fi
done
return 0
}

# @args identifiers
# @returns
# 0 - if all identifiers are null
# 1 - if any of the identifiers is not null
# other - in case of error (ex. any of the identifiers is not a valid name)
var_is_null() {
var_is_name_error_on_fail "$@" || return $?
var_is_set "$@" || return $?
local _var_is_null_i
for _var_is_null_i; do
( eval printf %.0s "\"\${$_var_is_null_i:?}\"" ) 2>/dev/null || return 0
done
return 1
}

# @args identifiers
# @returns
# 0 - if all identifiers are not null
# 1 - if any of the identifiers is null
# other - in case of error (ex. any of the identifiers is not a valid name)
var_is_not_null() {
var_is_name_error_on_fail "$@" || return $?
var_is_set "$@" || return $?
local _var_is_not_null_ret
var_is_null "$@" && _var_is_not_null_ret=$? || _var_is_not_null_ret=$?
if [ "$_var_is_not_null_ret" -eq 0 ]; then
return 1
elif [ "$_var_is_not_null_ret" -eq 1 ]; then
return 0;
fi
return "$_var_is_not_null_ret"
}

#################################################################################################################

var_test() {
local ret

var_is_name "$@" && ret=$? || ret=$?
if [ "$ret" -eq 0 ]; then printf "%s is %s\n" "$1" "name"
elif [ "$ret" -eq 1 ]; then printf "%s is not %s\n" "$1" "name"
else printf "err var_is_name %s %s\n" "$1" "$ret"; fi

var_is_set "$@" && ret=$? || ret=$?
if [ "$ret" -eq 0 ]; then printf "%s is %s\n" "$1" "set"
elif [ "$ret" -eq 1 ]; then printf "%s is not %s\n" "$1" "set"
elif [ "$ret" -eq 2 ]; then printf "var_is_set %s errored\n" "$1"
else printf "err var_is_set %s %s\n" "$1" "$ret"; fi

var_is_null "$@" && ret=$? || ret=$?
if [ "$ret" -eq 0 ]; then printf "%s is %s\n" "$1" "null"
elif [ "$ret" -eq 1 ]; then printf "%s is not %s\n" "$1" "null"
elif [ "$ret" -eq 2 ]; then printf "var_is_null %s errored\n" "$1"
else printf "err var_is_null %s %s\n" "$1" "$ret"; fi

var_is_not_null "$@" && ret=$? || ret=$?
if [ "$ret" -eq 0 ]; then printf "%s is %s\n" "$1" "not_null"
elif [ "$ret" -eq 1 ]; then printf "%s is not %s\n" "$1" "not_null"
elif [ "$ret" -eq 2 ]; then printf "var_is_not_null %s errored\n" "$1"
else printf "err var_is_not_null %s %s\n" "$1" "$ret"; fi

echo
}

var_test '$()'
var_test '$()def'
var_test 'abc$()'
var_test 'abc$()def'
echo "unset a"; var_test a
a=; echo "a=$a"; var_test a
a=""; echo "a=\"\""; var_test a
a='$(echo I will format your harddrive >&2)'; echo "a='$a'"; var_test a
a='!@$%^&*(){}:"|<>>?~'\'; echo "a='$a'"; var_test a

When run inside alpine the script will output:

# the script saved in /tmp/script.sh
$ chmod +x /tmp/script.sh
$ docker run --rm -ti -v /tmp:/mnt alpine /mnt/script.sh
$() is not name
var_is_set $() errored
var_is_null $() errored
var_is_not_null $() errored

$()def is not name
var_is_set $()def errored
var_is_null $()def errored
var_is_not_null $()def errored

abc$() is not name
var_is_set abc$() errored
var_is_null abc$() errored
var_is_not_null abc$() errored

abc$()def is not name
var_is_set abc$()def errored
var_is_null abc$()def errored
var_is_not_null abc$()def errored

unset a
a is name
a is not set
a is not null
a is not not_null

a=
a is name
a is set
a is null
a is not not_null

a=""
a is name
a is set
a is null
a is not not_null

a='$(echo I will format your harddrive >&2)'
a is name
a is set
a is not null
a is not_null

a='!@$%^&*(){}:"|<>>?~''
a is name
a is set
a is not null
a is not_null

That said, I think this is too much hassle for a simple "is a variable set or not" checking. Sometimes I just trust other they will not do a strange things, and if they do, it will break theirs computer not mine. So sometimes I would advise to just settle for simple solutions like yours - [ -n "$(eval echo "\"\${$var}\"")" ] && echo "$var is set" || echo "$var is not set is sometimes just enough when you trust your inputs.

Lookup shell variables by name, indirectly

From the man page of bash:

${!varname}

If the first character of parameter is an exclamation point, a level of
variable indirection is introduced. Bash uses the value of the variable formed from the rest of parameter as the name of the variable;
this variable is then expanded and that value is used in the rest of
the substitution, rather than the value of parameter itself. This is
known as indirect expansion.

How to show the line number in Shell (/bin/sh) script when debugging (-x)?

All the functionality discussed here is already required in the User Portability Utilities annex to the POSIX standard.

Moreover, dash, the most common non-bash /bin/sh implementation on Linux, already has the functionality built-in, as you can test below:

dash -s <<'EOF'
PS4=':$LINENO+'; set -x
echo "First line"
echo "Second line"
EOF

...correctly emits (with dash 0.5.10.2):

:2+echo First line
First line
:3+echo Second line
Second line

Extract substring in Bash

Use cut:

echo 'someletters_12345_moreleters.ext' | cut -d'_' -f 2

More generic:

INPUT='someletters_12345_moreleters.ext'
SUBSTRING=$(echo $INPUT| cut -d'_' -f 2)
echo $SUBSTRING

How to get a variable value if variable name is stored as string?

You can use ${!a}:

var1="this is the real value"
a="var1"
echo "${!a}" # outputs 'this is the real value'

This is an example of indirect parameter expansion:

The basic form of parameter expansion is ${parameter}. The value of
parameter is substituted.

If the first character of parameter is an exclamation point (!), it
introduces a level of variable indirection. Bash uses the value of the
variable formed from the rest of parameter as the name of the
variable; this variable is then expanded and that value is used in the
rest of the substitution, rather than the value of parameter itself.

Why do you need to put #!/bin/bash at the beginning of a script file?

It's a convention so the *nix shell knows what kind of interpreter to run.

For example, older flavors of ATT defaulted to sh (the Bourne shell), while older versions of BSD defaulted to csh (the C shell).

Even today (where most systems run bash, the "Bourne Again Shell"), scripts can be in bash, python, perl, ruby, PHP, etc, etc. For example, you might see #!/bin/perl or #!/bin/perl5.

PS:
The exclamation mark (!) is affectionately called "bang". The shell comment symbol (#) is sometimes called "hash".

PPS:
Remember - under *nix, associating a suffix with a file type is merely a convention, not a "rule". An executable can be a binary program, any one of a million script types and other things as well. Hence the need for #!/bin/bash.

How do I get the current user's username in Bash?

On the command line, enter

whoami

or

echo "$USER"


Related Topics



Leave a reply



Submit