Bash Script – Writing Commands with Proper Quoting

bashquotingshell-script

A situation that I've run into a few times: I have a shell function that does some setup and then invokes a more-or-less arbitrary command, like this:

setup_and_run() {
    some_setup_commands
    "$@"
}

I want to alter this function to also write a script file that I can use to re-run the command after doing the same setup. I can do the following, which works when there are no arguments with spaces or other weird stuff in the command:

setup_and_run() {
    some_setup_commands
    cat >>rerun.sh <<EOF
some_setup_commands
$@
EOF
    "$@"
}

Is there a standard way to alter this so that the script gets written with proper quoting, in general, even if some arguments have spaces or other special characters?

For example, with the second definition of setup_and_run, if I run

$ setup_and_run ls "my cat's \"password\""
my cat's "password"

then rerun.sh will contain

some_setup_commands
ls my cat's "password"

whereas I want it to contain something like

some_setup_commands
ls "my cat's \"password\""

or

some_setup_commands
ls my\ cat\'s\ \"password\"

or some such thing.

In a pinch I can outsource this to something like Python's shlex.quote(), but I'm looking for something that can be done in the shell itself if possible, or at least using a lighter-weight standard UNIX tool.

Best Answer

In bash, ksh93 and zsh, you can use the %q escape for the printf builtin.

#!/bin/bash
# also works in ksh93, zsh
setup_and_run() {
    some_setup_commands
    cat >>rerun.sh <<EOF
some_setup_commands
$(printf '%q ' "$@")
EOF
    "$@"
}

In bash, you can use the printf -v to write the output to a variable rather than to standard output.

#!/bin/bash
printf -v cmd '%q ' "$@"
setup_and_run() {
    some_setup_commands
    cat >>rerun.sh <<EOF
some_setup_commands
$cmd
EOF
    "$@"
}

In zsh, there's a different method which is the q parameter expansion flag, followed by j: : to join the array elements with a space.

#!/bin/zsh
setup_and_run() {
    some_setup_commands
    cat >>rerun.sh <<EOF
some_setup_commands
${(j: :)${(q)@}}
EOF
    "$@"
}

In plain sh, it's more difficult. A reliable way to quote a word is to put single quotes around it, and replace each single quote inside by '\''. Making the replacement correctly is not very easy, involving either an external call to sed or a loop.

#!/bin/sh
quote_simple_command () {
  quoted=
  for raw in "$@"; do
    quoted="$quoted'"
    while case "$raw" in *\'*) true;; *) false;; esac; do
      quoted="$quoted${raw%%\'*}'\\''"
      raw="${raw#*\'}"
    done
    quoted="$quoted$raw' "
  done
  printf %s "${quoted% }"
}
setup_and_run() {
    some_setup_commands
    cat >>rerun.sh <<EOF
some_setup_commands
$(quote_simple_command "$@")
EOF
    "$@"
}
Related Question