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 ofparameter
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 ofparameter
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
Difference Between Linux Loadable and Built-In Modules
Sort Across Multiple Files in Linux
Sending Keyboard Input to a Program from Command-Line
Run a Shell Script from Docker-Compose Command, Inside the Container
Download Images from Google with Command Line
How to Cut First Column (Variable Length) of a String in Shell
How to Control a User Systemd Using 'Systemctl --User' After Sudo Su - Myuser
How to Correctly Install Wkhtmltopdf on Debian 64 Bit
Call to Daemon in a /Etc/Init.D Script Is Blocking, Not Running in Background
Bash: Difference Between "Export K=1" VS. "K=1"
How to Write Linux Driver Module Call/Use Another Driver Module
Awk One Liner Select Only Rows Based on Value of a Column
How to Sort Files Numerically from Linux Command Line
Selecting the Right Linux I/O Scheduler for a Host Equipped with Nvme Ssd
Using Find - Deleting All Files/Directories (In Linux ) Except Any One
What Does Double Slash // in 'Cd //' Mean in Linux