The problem is this line:
TIMESEC=$(echo blah | ( /usr/bin/time -f %e grep blah >/dev/null ) 2>&1 | awk -F. '{print $1}')
where you are redirecting the standard error to match the standard output. bash is writing its trace-messages to the standard error, and is (for instance) using its built-in echo
along with other shell constructs all in the bash process.
If you change it to something like
TIMESEC=$(echo blah | sh -c "( /usr/bin/time -f %e grep blah >/dev/null )" 2>&1 | awk -F. '{print $1}')
it will work around that problem, and perhaps be an acceptable compromise between tracing and working:
++ awk -F. '{print $1}'
++ sh -c '( /usr/bin/time -f %e grep blah >/dev/null )'
++ echo blah
+ TIMESEC=0
+ echo ABC--0--DEF
ABC--0--DEF
+ '[' 0 -eq 0 ']'
+ echo 'we are here!'
we are here!
I suspect this is the part of the sequence that's catching you:
The words that are not variable assignments or redirections are expanded (see Shell Expansions). If any words remain after expansion, the first word is taken to be the name of the command and the remaining words are the arguments
That's from the Bash reference manual in the section on Simple Command Expansion.
In the cmd=bash
example, no environment variables are set, and bash processes the command line up through parameter expansion, leaving bash -c "echo hi"
.
In the prefix=hello=hi
example, there are again no variable assignments in the first pass, so processing continues to parameter expansion, resulting in a first word of hello=hi
.
Once the variable assignments have been processed, they are not re-processed during command execution.
See the processing and its results under set -x
:
$ prefix=hello=hi
+ prefix=hello=hi
$ $prefix bash -c 'echo $hello'
+ hello=hi bash -c 'echo $hello'
-bash: hello=hi: command not found
$ hello=42 bash -c 'echo $hello'
+ hello=42
+ bash -c 'echo $hello'
42
For a safer variation of "variable expansion" -as- "environment variables" than eval
, consider wjandrea's suggestion of env
:
prefix=hello=hi
env "$prefix" bash -c 'echo "$hello"'
hi
It's not strictly a command-line variable assignment, since we're using the env
utility's main function of assigning environment variables to a command, but it accomplishes the same goal. The $prefix
variable is expanded during the processing of the command-line, providing the name=value to env
, who passes it along to bash
.
Best Answer
When you leave a variable expansion unquoted, it undergoes word splitting and filename expansion (i.e. globbing). It isn't parsed as a shell command. In general, when you dynamically construct a shell snippet to execute, the right way to execute it is
eval "$a"
wherea
contains the string to parse as shell code.In your first snippet, the value of
a
is the stringmy_cmd --verbose
. Word splitting breaks it into two wordsmy_cmd
and--verbose
. Globbing does nothing since there are no wildcards in any of the words. The command resulting from the expansion of$a
thus consists of two wordsmy_cmd
and--verbose
, so the commandmy_cmd
(which could be an alias, a function, a builtin or an executable in the PATH) is executed with the single argument--verbose
.In the second snippet, things are similar, with three words resulting from the expansion:
URL=myurl
,my_cmd
and--verbose
. This results in an attempt to execute the commandURL=myurl
with two arguments.The shell command
URL=myurl my_cmd --verbose
is parsed differently: the first word is parsed as an assignment to the variableURL
, and since there is a command name after it the assignment sets the environment variable for the duration of the command only. This is part of parsing, not something that's done after expansion, so it requires the equal sign to be part of the shell source code, the equal sign can't be the result of some expansion.Don't store a command with parameters into a string. Use an array. For a complex command, such as one where you set variables or perform redirection in addition to running the command, use a function if at all possible, and if not use
eval
(taking great care of proper quoting).