Bash – Correct quoting in eval

bashshell-script

I have a script, that does nothing useful but execute the positional arguments. (I'm aware of the security risks, and the script does not make anything useful because it's a minimal working example.)

$ cat script
> #!/usr/bin/env bash
>
> eval "${*}"


$ cat "docu ment"
> Lorem ipsum dolor sit amet

What I would like to do is call the script with ./script cat "docu ment", or ./script cat docu\ ment, but the quotes or the escape character vanishes and the script will try cat docu ment, which doesn't work. How would I fix the quoting in such a case?

EDIT: What I really want to do, is invoke a command as many times until it returns a successful exit code, or it tried n times. My script looks like this:

#!/usr/bin/env bash

# Try command several times, until it reports success (exit code 0)
# or I give up (tried n times)

tryMax=10
try=1

# Do until loop in bash
while
    eval "${@}"
    exitcode="${?}"
    [[ "${exitcode}" -ne 0 && "${try}" -lt "${tryMax}" ]]
do (( try++ ))
done

if [[ "${exitcode}" -ne 0 ]]; then
    echo -n "I tried hard, but did not manage to make this work. The exit code "
    echo "of the last iteration of this command was: ${exitcode}."
    exit "${exitcode}"
fi

Best Answer

You don't need eval here. You can just use "$@":

while
    "${@}"
    exitcode="${?}"
    [[ "${exitcode}" -ne 0 && "${try}" -lt "${tryMax}" ]]
do ...

"$@" will expand to all the arguments to the script as separate "words" - respecting the original quoting that prevented word splitting - and then leave you with the first argument as the command waiting to run (cat), and the remaining arguments as arguments to cat (docu ment).

Where this won't work:

  • If you want the command passed in to use other higher-level shell constructs, like pipes, function definitions, loops, etc. These are all processed before parameter expansion, and won't be attempted again after "$@" is expanded.
  • If the command has its return code negated ! cmd. ! is also processed at the start of handling a command, before parameter expansion.
  • If the command is multiple commands x \; y or $'x\ny' or x $'\n' y, or the same with && or ||. All those are just regular arguments.
  • If your command has variable assignments preceding it like LD_LIBRARY_PATH=/x foo. You can put them before the script name, but not the argument command.
  • If the command has redirections >foo, 3<bar in it. These may or may not be able to be affixed to the script, since the script has its own logging output.
  • If the command has here-documents or here-strings. These will only be readable one time if affixed to the script itself, so depending on exactly when the command fails you might or might not be all right. These would be very difficult to pass in suitably for eval anyway.
  • If the command is a subshell ( ... ) or command group { ... ; }. These will be treated as commands called ( and {, not as syntactic constructs.
  • If the command contains command substitution $(...) that needs to be run repeatedly. You can use that (or any other shell construct) to generate the original arguments, but they will all be fixed strings once the script starts running.
  • If the command has any other element that should be evaluated repeatedly, like $RANDOM or an arithmetic expansion $((i++)).
  • If the command is time. This is a shell reserved word, and not a built-in command, so it is also processed before parameter expansion.

Otherwise, however, you can successfully avoid eval entirely, and should probably do so. It's very fragile to construct correctly even ignoring any possible security issues.

Related Question