I have a command which I would like to pre-construct from constants, but there are variables that will come at play during runtime, imagine:
custom_cmd="cmd1 '$CONST1' '$var1' '$CONST2' '$var2' 2> /dev/null || cmd2 '$CONST3' '$var3'; echo $?"
The reason this is put into custom_cmd
variable and not substituted at any particular run is because I need to run this in current script's shell (using eval
), but also pass it to ssh
within bash -c
execution.
So if I were to wrap this into a function instead (with $var{1,2,3} as arguments), I would need to make the script allow for executing it standalone (with special argument, but also have it scp
over first - definitely want to avoid this) so that it is actually available for the remote bash -c
.
However, the $CONST{1,2,3} are actually loaded from a local config file (I do not want to copy that one over and in fact I do not want to parse it more than once), so I would need to be passing them in too. At that point, it becomes very repetitive for multiple ever-so-slightly different invocations anyhow - to have such function.
My issue is that while the CONST{1,2,3} are non-changing, the $var{1,2,3} are basically environment variables. So say at some point $var1
value is meant to be that of $ENV_VAR_X
on the target system, but at another it is meant to be $ENV_VAR_Y
.
In a way, I would like a two-stage substitution, something I can initialize at the beginning where the constants are known, but then only substitute the environment variable names later (when it goes to eval
or bash -c
).
Is there such a thing?
Building commands in strings is problematic. You should prefer functions instead, and as @CharlesDuffy observed in comments, that does not produce much practical difficulty for sending a function to bash -c
.
Additionally, by using indirect expansions, you can avoid having to expand some parts of the command more times than others.
Consider this:
# Optional, for convenience:
constants=("$CONST1" "$CONST2" "$CONST3")
custom_cmd() {
cmd1 "$1" "${!4}" "$2" "${!5}" 2> /dev/null || cmd2 "$3" "${!6}"
echo $?
}
# ...
# execute locally
custom_cmd "${constants[@]}" "$arg1" "$arg2" "$arg3"
# execute with bash -c
# (Declares the function and then immediately calls it, with arguments provided
# on the `bash` command line.)
bash -c "$(declare -f custom_cmd);"'custom_cmd "$@"' \
custom_cmd "${constants[@]}" "$arg1" "$arg2" "$arg3"
Note: in the bash -c
example, the argument immediately after the command string (custom_cmd
) is used as $0
in the executed shell instance. That serves mainly to position the remaining arguments, which are mapped as the shell's positional parameters. Those then become arguments to the custom_cmd
function when the custom_cmd "$@"
command inside the command string is evaluated.
That does push the "constants" as function parameters along with the other variables, and I think that's appropriate. They are variables, after all, even if they are declared read-only or simply not expected to change.
For anyone struggling to follow the logic above, here's a complete working example with a few additional constructs to improve clarity of the output that you could execute with bash -x
if it helps:
$ cat tst.sh
#!/usr/bin/env bash
CONST1='c * 1'
CONST2='c $RANDOM 2'
CONST3='c 3'
localvar='something'
arg1='RANDOM'
arg2='localvar'
arg3='TMP'
# Optional, for convenience:
constants=("$CONST1" "$CONST2" "$CONST3")
custom_cmd() {
echo "\$1=\"$1\"" "\$4=\"$4\"->\"${!4}\"" "\$2=\"$2\"" "\$5=\"$5\"->\"${!5}\"" 2> /dev/null || echo "\$3=\"$3\"" "\$6=\"$6\"->\"${!6}\""
echo "\$?=\"$?\""
}
echo 'execute locally:'
custom_cmd "${constants[@]}" "$arg1" "$arg2" "$arg3"
echo 'execute with bash -c:'
# (Declares the function and then immediately calls it, with arguments provided
# on the `bash` command line.)
bash -c "$(declare -f custom_cmd);"'custom_cmd "$@"' \
_ "${constants[@]}" "$arg1" "$arg2" "$arg3"
$ ./tst.sh
execute locally:
$1="c * 1" $4="RANDOM"->"10144" $2="c $RANDOM 2" $5="localvar"->"something"
$?="0"
execute with bash -c:
$1="c * 1" $4="RANDOM"->"289" $2="c $RANDOM 2" $5="localvar"->""
$?="0"
$ bash -x ./tst.sh
+ CONST1='c * 1'
+ CONST2='c $RANDOM 2'
+ CONST3='c 3'
+ localvar=something
+ arg1=RANDOM
+ arg2=localvar
+ arg3=TMP
+ constants=("$CONST1" "$CONST2" "$CONST3")
+ echo 'execute locally:'
execute locally:
+ custom_cmd 'c * 1' 'c $RANDOM 2' 'c 3' RANDOM localvar TMP
+ echo '$1="c * 1"' '$4="RANDOM"->"16277"' '$2="c $RANDOM 2"' '$5="localvar"->"something"'
$1="c * 1" $4="RANDOM"->"16277" $2="c $RANDOM 2" $5="localvar"->"something"
+ echo '$?="0"'
$?="0"
+ echo 'execute with bash -c:'
execute with bash -c:
++ declare -f custom_cmd
+ bash -c 'custom_cmd ()
{
echo "\$1=\"$1\"" "\$4=\"$4\"->\"${!4}\"" "\$2=\"$2\"" "\$5=\"$5\"->\"${!5}\"" 2> /dev/null || echo "\$3=\"$3\"" "\$6=\"$6\"->\"${!6}\"";
echo "\$?=\"$?\""
};custom_cmd "$@"' _ 'c * 1' 'c $RANDOM 2' 'c 3' RANDOM localvar TMP
$1="c * 1" $4="RANDOM"->"8394" $2="c $RANDOM 2" $5="localvar"->""
$?="0"