3

With bash >5, I'm trying to assign a different value to variables depending on the architecture specified in a variable. I use a function to do so. This works perfectly:

# arguments:
  variable name to assign,
  value for mac arch,
  value for pi arch

create_variable_for_arch() {
  if [ "$_run_for_arch" = "mac" ]; then
    eval $1=\$2
  else
    eval $1=\$3
  fi
}

However, this breaks my script for some reason:

create_variable_for_arch() {
  if [ "$_run_for_arch" = "mac" ]; then
    declare "$1"="$2"
  else
    declare "$1"="$3"
  fi
}

Here is a snippet to demonstrate how I use create_variable_from_arch()

declare _moonlight_opt_audio
declare _arch_specific_stream_command

#
while getopts "b:fahdr:s" options; do
  case $options in
    a)
      create_variable_for_arch "_moonlight_opt_audio" \
        "--audio-on-host" "-localaudio"
      ;;
  esac
done

create_variable_for_arch "_moonlight_opt_fps" "--fps 60" "-fps 60"

start_streaming() {
  _arch_specific_options="$_moonlight_opt_resolution $_moonlight_opt_fps $_moonlight_opt_audio $_moonlight_opt_display_type $_moonlight_opt_bitrate"
  create_variable_for_arch "_arch_specific_stream_command" "$_arch_specific_options stream $_target_computer_ip $_moonlight_opt_app_name" "stream $_arch_specific_options -app $_moonlight_opt_app_name $_target_computer_ip"

  moonlight $_arch_specific_stream_command
}

The trace looks like this with eval()

+ start_streaming
+ _arch_specific_options='--resolution 1920x1080 --fps 60   --bitrate 5000'
+ create_variable_for_arch _arch_specific_stream_command '--resolution 1920x1080 --fps 60   --bitrate 5000 stream 192.168.1.30 StreamMouse' 'stream --resolution 1920x1080 --fps 60   --bitrate 5000 -app StreamMouse 192.168.1.30'
+ '[' mac = mac ']'
+ eval '_arch_specific_stream_command=$2'
++ _arch_specific_stream_command='--resolution 1920x1080 --fps 60   --bitrate 5000 stream 192.168.1.30 StreamMouse'
+ moonlight --resolution 1920x1080 --fps 60 --bitrate 5000 stream 192.168.1.30 StreamMouse
moonlight --resolution 1920x1080 --fps 60 --bitrate 5000 stream 192.168.1.30 StreamMouse

But with declare it looks like this:

    + start_streaming
    + _arch_specific_options=
    + create_variable_for_arch _arch_specific_stream_command ' stream 192.168.1.30 ' 'stream  -app  192.168.1.30'
    + '[' mac = mac ']'
    + declare '_arch_specific_stream_command= stream 192.168.1.30 '
    + echo moonlight
    moonlight

$_arch_specific_options ends up with no value. What is going on? I've tried a few different ways of quoting or not quoting variables, but I don't really understand what's doing what in terms of quotations.

2
  • 1
    in the very first code block, should it be evan $1=\$2 instead of declare $1=\$2 or something like that? – ilkkachu Mar 3 at 14:54
  • Yes, my apologies. I've corrected the first code block. Thank you – DeadBranch Mar 3 at 15:03
4

declare (like the typeset of other shells; also understood by bash as an alias for declare) declares a variable in the current scope (and can set a type and/or value).

So here, you would declare a variable that is local to the create_variable_for_arch function. When that function returns, that variable would be gone.

bash's declare/typeset has a -g option to declare the variable global), but you can't use that either as it declares the variable (and sets its type and/or value) in the outer-most scope as oppose to the scope of the caller of the function, so is pretty useless there (it's more useful in mksh/zsh/yash where it's only skipping the making it local or with ksh93 which has static scoping, see What do `declare name` and `declare -g` do? for details).

SO here, your options are either to use eval, or to use namerefs:

create_variable_for_arch() {
  if [ "$_run_for_arch" = mac ]; then
    eval "$1=\$2"
  else
    eval "$1=\$3"
  fi
}

Or, assuming $_run_for_arch is constant in your script:

if [ "$_run_for_arch" = "mac" ]; then
  create_variable_for_arch() { eval "$1=\$2"; }
else
  create_variable_for_arch() { eval "$1=\$3"; }
fi

Or with namerefs:

create_variable_for_arch() {
  typeset -n _var_name="$1"
  if [ "$_run_for_arch" = mac ]; then
    _var_name=$2
  else
    _var_name=$3
  fi
}

It's often (rightly) recommended to avoid eval for security reasons, but eval is safe when used properly. declare and namerefs would be as unsafe here when used improperly, as they can both also evaluate code.

All of:

f() { eval "$1=\$2"; }
f() { declare "$1=$2"; }
f() { declare -n v="$1"; v=$2; }

Would run the reboot command if called with:

f 'a[$(reboot)]' value

It's important to make sure the first argument is a variable name to avoid the arbitrary command execution vulnerability.

f() { declare $1=$2; }

would be much worse. As those parameter expansions are unquoted, they're subject to split+glob, so even the contents of $2 can end up being evaluated as shell code, as in:

f var 'foo a[$(reboot)]='
5
  • Thank you for taking the time to write this clear and comprehensive answer. I had to do a little research on typeset, nameref, and variable scope to get up to speed with you. So thanks for being specific and technical. Indeed, all three options you presented work. In terms of understanding why they work and what’s preferable, here’s what I’ve got on my list for research now: - declare scope and options - quoting variables and the related security implications - split+glob - security concerns in bash Thanks again. – DeadBranch Mar 3 at 15:50
  • 2
  • Thank you. After re-reviewing the links in Security implications... I've got a grasp on quotes/split+glob. Now I understand that moonlight $_arch_specific_stream_command failed whenever I tried quoting the var because I was relying on splitting during the parameter expansion to collapse the extra whitespace. I can set -f to avoid globbing. I'm going to have to re-review the discussion on security implications in a few days since your answer there introduced new concepts to me. – DeadBranch Mar 3 at 20:05
  • Regardless, I think I see that eval can be safe when there is no external input as a vector for code execution. – DeadBranch Mar 3 at 20:15
  • 1
    @DeadBranch, yes, but I'd also like to point out that eval is not the only command you need to watch out for those vectors. declare and namerefs can also run arbitrary code as seen above, but so can [ / test, read, printf, sh, sed, awk, let... See also Security Implications of using unsanitized data in Shell Arithmetic evaluation – Stéphane Chazelas Mar 4 at 6:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.