Joachim Breitner
Joachim Breitner

Reputation: 25763

Shell quoting with quotation marks in bash

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?

Upvotes: 1

Views: 3420

Answers (2)

Ansgar Wiechers
Ansgar Wiechers

Reputation: 200273

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

Upvotes: 2

glenn jackman
glenn jackman

Reputation: 246799

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

Upvotes: 3

Related Questions