Shell – How to Generate Arguments to Another Command via Command Substitution

argumentscommand-substitutionshell

Following on from:
unexpected behaviour in shell command substitution

I have a command which can take a huge list of arguments, some of which can legitimately contain spaces (and probably other things)

I wrote a script which can generate those arguments for me, with quotes, but I must copy and paste the output e.g.

./somecommand
<output on stdout with quoting>
./othercommand some_args <output from above>

I tried to streamline this by simply doing

./othercommand $(./somecommand)

and ran into the unexpected behaviour mentioned in question above. The question is — can command substitution be reliably used to generate the arguments to othercommand given that some arguments require quoting and this cannot be changed?

Best Answer

I wrote a script which can generate those arguments for me, with quotes

If the output is properly quoted for the shell, and you trust the output, then you could run eval on it.

Assuming you have a shell that supports arrays, it would be best to use one to store the arguments you get.

If ./gen_args.sh produces output like 'foo bar' '*' asdf, then we could run eval "args=( $(./gen_args.sh) )" to populate an array called args with the results. That would be the three elements foo bar, *, asdf.

We can use "${args[@]}" as usual to expand the array elements individually:

$ eval "args=( $(./gen_args.sh) )"
$ for var in "${args[@]}"; do printf ":%s:\n" "$var"; done
:foo bar:
:*:
:asdf:

(Note the quotes. "${array[@]}" expands to all elements as distinct arguments unmodified. Without quotes the array elements are subject to word splitting. See e.g. the Arrays page on BashGuide.)

However, eval will happily run any shell substitutions, so $HOME in the output would expand to your home directory, and a command substitution would actually run a command in the shell running eval. An output of "$(date >&2)" would create a single empty array element and print the current date on stdout. This is a concern if gen_args.sh gets the data from some untrusted source, like another host over the network, file names created by other users. The output could include arbitrary commands. (If get_args.sh itself was malicious, it wouldn't need to output anything, it could just run the malicious commands directly.)


An alternative to shell quoting, which is hard to parse without eval, would be to use some other character as separator in the output of your script. You'd need to pick one that is not needed in the actual arguments.

Let's choose #, and have the script output foo bar#*#asdf. Now we can use unquoted command expansion to split the output of the command to the arguments.

$ IFS='#'                          # split on '#' signs
$ set -f                           # disable globbing
$ args=( $( ./gen_args3.sh ) )     # assign the values to the array
$ for var in "${args[@]}"; do printf ":%s:\n" "$var"; done
:foo bar:
:*:
:asdf:

You'll need to set IFS back later if you depend on word splitting elsewhere in the script (unset IFS should work to make it the default), and also use set +f if you want to use globbing later.

If you're not using Bash or some other shell that has arrays, you could use the positional parameters for that. Replace args=( $(...) ) with set -- $(./gen_args.sh) and use "$@" instead of "${args[@]}" then. (Here, too, you need quotes around "$@", otherwise the positional parameters are subject to word splitting.)

Related Question