5

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.

1 Answer 1

7

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
    "$@"
}
6
  • I see that printf '%s\n' "'${somevar//\'/'\''}'" doesn't work, but why doesn't it work?
    – Wildcard
    Dec 15, 2017 at 0:52
  • @Wildcard, because you have a single-quoted string inside the ${...} and you tried to use a backslash within it, which doesn't work. (Which technically means the shell is still expecting a ", a } and a ". I think.) "'${foo//\'/\'\\\'\'}'" seems it would do what you want.
    – ilkkachu
    Dec 15, 2017 at 5:14
  • @ilkkachu, except that it doesn't. I tried that already. Bash's single quote handling in the replacement string of parameter expansion appears to be wildly erratic and insane. ...further experimentation shows, it is actually consistent (sort of). It interprets non-escaped single quotes as single quotes, but it preserves both escaped and non-escaped single quotes, along with the escape character used on single quotes. Annoying. Edit: Wait, no, it de-doubles the backslashes appearing within non-escaped single quotes, and does parameter expansion too.
    – Wildcard
    Dec 15, 2017 at 5:35
  • @ilkkachu, unix.stackexchange.com/q/410983/135943
    – Wildcard
    Dec 15, 2017 at 5:58
  • 1
    Actually, @Wildcard has identified a notable issue in the linked question's thread: older versions of bash until relatively recently were, in fact, behaving as my Busybox ash does. Sometime between versions 4.1-something and 4.3-something, bash changed how it parses "'${foo//\'/\'\\\'\'}'".
    – mtraceur
    Dec 15, 2017 at 9:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.