Bash – Why Substituting Eval with Declare Results in Empty Variable

bashdeclareevalquoting

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.

Best Answer

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)]='