Explanation
eval
combines all arguments into a single string, and evaluates that string as code. Thus, eval "ls" "-l" "file with space"
is exactly the same as eval ls -l file with space
or eval "ls -l file with space"
.
Best Practice (Without Pipe Support)
As given in BashFAQ #50 -- the following runs its exact argument list as the argument vector of a simple command.
run_command() {
"$@"
}
This offers numerous guarantees:
- No process substitution, command substitution, or other undesired operations will take place. Thus, arbitrary filenames can be passed through without double-expansion causing any part of their contents to be parsed as code.
- An argument will always be passed through to the program being run, not parsed by the shell. Thus, an array entry with the exact string
>foo
will be passed through, rather than causing a file named foo
to be created.
If you need to wrap a command with a pipeline, this can be done by encapsulating that pipeline in a function:
run_pipeline() { foo "$@" | bar; }
run_command run_pipeline "argument one" "argument two"
Allowing Pipes As Direct Arguments
To be clear: I do not advise using this code. By exempting |
from the usual protections provided by following best practices, it weakens the security provided by said practices. However, it does do what you ask.
run_command() {
local cmd_str='' arg arg_q
for arg; do
if [[ $arg = "|" ]]; then
cmd_str+=" | "
else
printf -v arg_q '%q' "$arg"
cmd_str+=" $arg_q"
fi
done
eval "$cmd_str"
}
In this form, an argument of |
will cause the generated string to contain a compound command, split into simple commands at the location of that argument.
On Why This Is A Bad Idea
Now -- why is trying to allow syntax elements to be processed a Bad Idea in this context? Consider the following:
echo '<hello>'
Here, the <
and >
in the string <hello>
have been quoted, and thus no longer have their original behavior. However, once you've assigned these values to an array or an argument list, as in
args=( 'echo' '<hello>' )
...metadata no longer exists about which characters were quoted or were not. Thus,
echo hello '|' world
becomes entirely indistinguishable from
echo hello | world
even though as separate commands these would have had very different behaviors.
A Concrete Example
Consider the following:
run_command rm -rf -- "$tempdir" "$pidfile"
In the "best practices" example, this is guaranteed to treat both the contents of tempdir
and pidfile
as filenames passed to rm
, no matter what those values are.
However, with the "allowing pipes" example, the above could instead invoke rm -rf -- | arbitrary-command-here
, should tempfile='|'
and pidfile=arbitrary-command-here
.
As shell variables are initialized from the set of environment variables present, and environment variables are often externally controllable -- as demonstrated by the existence of remote exploits for Shellshock -- this is not a purely theoretical or idle concern.