1

I want to implement a bash function that runs its arguments as a command, while (maybe optionally) printing the command before. Think of an installation script or test runner script.

Just using

function run () {
    echo "Running $@"
    "$@"
}

would not allow me to distinguish a call from run foo arg1 arg2 and run foo "arg1 arg2", so I need to properly escape arguments.

My best shot so far is

function run () {
    echo -n "Running"
    printf " %q" "$@"
    echo
    "$@"
}

Which works:

$ run echo "one_argument" "second argument" argument\"with\'quotes
Running echo one_argument second\ argument argument\"with\'quotes
one_argument second argument argument"with'quotes

but is not very elegant. How can I achieve an output of

$ run echo "one_argument" "second argument" argument\"with\'quotes
Running echo one_argument "second argument" "argument\"with'quotes"
one_argument second argument argument"with'quotes

i.e. how can I make printf to put quotation marks around arguments that need quoting, and properly escape quotes therein, so that the output can be copy’n’pasted correctly?

Joachim Breitner
  • 25,395
  • 6
  • 78
  • 139
  • Could you just put quotation marks around _all_ arguments, not just the ones that need it? You could just escape double quotes around all arguments, as shown here: tldp.org/LDP/Bash-Beginners-Guide/html/sect_03_03.html – kevinsa5 Jul 14 '13 at 09:03
  • That would be ugly, and would be incorrect if the the argument contained a quotation mark. For debugging output that is not so severe, but I’d still like the output to be correct. – Joachim Breitner Jul 14 '13 at 09:05
  • Ah, I see. My knowledge of BASH is not sufficient to help past that, sorry. – kevinsa5 Jul 14 '13 at 09:08
  • Take a look at this: [http://stackoverflow.com/a/1669493/1744633](http://stackoverflow.com/a/1669493/1744633) – Mohammad Javad Naderi Jul 14 '13 at 09:26
  • Also doesn’t escape quotes inside the argument. I guess I should elaborate my question. – Joachim Breitner Jul 14 '13 at 09:28

2 Answers2

3

This will quote everything:

run() {
    printf "Running:"
    for arg; do 
        printf ' "%s"' "${arg//\"/\\\"}"
    done
    echo
    "$@"
}
run echo "one_argument" "second argument" argument\"with\'quotes
Running: "echo" "one_argument" "second argument" "argument\"with'quotes"
one_argument second argument argument"with'quotes

This version only quotes arguments containing double quotes or whitespace:

run() {
    local fmt arg
    printf "Running:"
    for arg; do
        [[ $arg == *[\"[:space:]]* ]] && fmt=' "%s"' || fmt=" %s" 
        printf "$fmt" "${arg//\"/\\\"}"
    done
    echo
    "$@"
}
run echo "one_argument" "second argument" argument\"with\'quotes
Running: echo one_argument "second argument" "argument\"with'quotes"
one_argument second argument argument"with'quotes
glenn jackman
  • 238,783
  • 38
  • 220
  • 352
2

I don't think there's an elegant solution to what you want, because "$@" is handled by bash before any command ever gets to see it. You'll have to manually re-construct the command-line:

#!/bin/bash

function run() {
  echo -n "Running:"
  for arg in "$@"; do
    arg="$(sed 's/"/\\&/g' <<<$arg)"
    [[ $arg =~ [[:space:]\\\'] ]] && arg=\"arg\"
    echo -n " $arg"
  done
  echo ""

  "$@"
}

run "$@"

Output:

$ ./test.sh echo arg1 "arg 2" "arg3\"with'other\'\nstuff"
Running: echo arg1 "arg 2" "arg3\"with'other\'\nstuff"
arg1 arg 2 arg3"with'other\'\nstuff

Note that there are some corner cases where you won't get the exact input command line. This happens when you pass arguments that bash expands before passing them on, e.g.:

$ ./test.sh echo foo'bar'baz
Running: echo foobarbaz
foobarbaz
$ ./test.sh echo "foo\\bar"
Running: echo "foo\bar"
foobar
Ansgar Wiechers
  • 193,178
  • 25
  • 254
  • 328
  • This looks very similar to [the answer](http://stackoverflow.com/questions/1668649/how-to-keep-quotes-in-args/1669493#1669493) linked by @MohammadJavadNaderi in the comments; it does not seem to handle quoting special characters such as `"` itself. – Joachim Breitner Jul 14 '13 at 09:59
  • You can't `shift` the arguments away -- you won't be able to execute the command after printing it! Also, your regular expression will match one of the characters {"a", "c", "e", "p", "s", ":"} -- you need `[[:space:]]` – glenn jackman Jul 14 '13 at 11:16
  • @glennjackman That's what I get for insufficient testing. Thanks for the heads up. Fixed. – Ansgar Wiechers Jul 14 '13 at 11:30