Reputation: 537
I have written a short script that checks for the presence of an environment variable, and fails if it does not exist. The purpose of this script is for use on a CI server, which has configuration variables loaded through a console. I want to ensure that these variables have been set, and fail the job upfront if they haven't.
The execution environment for these jobs is a Docker container based on Alpine Linux. I only have access to sh
. I would like to avoid installation of another shell like bash
to keep the image size as slim as possible.
The script looks roughly like this:
#!/bin/sh
AWS_ACCESS_KEY_ID=123 # provided by CI
_fail_without() {
VAR_NAME=$1
VAR_VAL=$(eval echo "\$$VAR_NAME")
if [[ -z "${VAR_VAL}" ]]; then
echo "${VAR_NAME} not set; aborting"
exit 1
else
echo "${VAR_NAME} exists"
fi
}
_fail_without AWS_ACCESS_KEY_ID
_fail_without AWS_SECRET_ACCESS_KEY
... with the expected stdout:
AWS_ACCESS_KEY_ID exists
AWS_SECRET_ACCESS_KEY not set; aborting
As you can see, I have passed the string value of the variable name, rather than the variable itself, so that failures will be properly logged. All of this works fine. However, I'm concerned about the potential security implications of relying on eval
to access the variable value in the line VAR_VAL=$(eval echo "\$$VAR_NAME")
.
The question is: is this a viable approach, are there any security implications to be aware of, and if there is, is there a more secure or better alternative? I can't use declare
, and printf
doesn't seem to behave the same way it does in bash
either.
Upvotes: 1
Views: 899
Reputation: 141613
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.
Upvotes: 1