Question

I have this (test) script:

#!/bin/bash

my_cmd_bad_ ( ) {
    cmd="$@"
    $cmd
}

my_cmd_good_ ( ) {
    "$@"
}

my_cmd_bad_  ls -l "file with space"
my_cmd_good_ ls -l "file with space"

The output is (the file does not exist, which is not the point of this question):

» ~/test.sh
ls: cannot access file: No such file or directory
ls: cannot access with: No such file or directory
ls: cannot access space: No such file or directory
ls: cannot access file with space: No such file or directory

I am surprised that the first version does not work as expected: the parameter is not quoted, and instead of processing one file, it processes three. Why?

How can I save the command that I want to execute, properly quoted? I need to execute it later, where I do not have "$@" anymore.

A simple rework of this test script would be appreciated.

Was it helpful?

Solution 2

You can quote any single parameter and evaluate it later:

my_cmd_bad_ ( ) {
  j=0
  for i in "$@"; do
    cmd["$j"]=\"$"$i"\"
    j=$(( $j + 1 ))
   done;
  eval ${cmd[*]}
}

OTHER TIPS

See similar question: How to pass command line parameters with quotes stored in single variable?

Use those utility functions ho save a command to a string for later execution:

bash_escape() {
  # backtick indirection strictly necessary here: we use it to strip the
  # trailing newline from sed's output, which Solaris/BSD sed *always* output
  # (unlike GNU sed, which outputs "test": printf %s test | sed -e s/dummy//)
  out=`echo "$1" | sed -e s/\\'/\\''\\\\'\\'\\'/g`
  printf \'%s\' "$out"
}
append_bash_escape() {
  printf "%s " "$1"
  bash_escape "$2"
}

your_cmd_fixed_ ( ) {
  cmd="$@"
  while [ $# -gt 0 ] ; do
    cmd=`append_bash_escape "$cmd" "$1"` ; shift
  done
  $cmd
}

You are combining three space-delimited strings "ls", "-l", and "file with space" into a single space-delimited string cmd. There's no way to know which spaces were originally quoted (in "file with space") and which spaces were introduced during the assignment to cmd.

Typically, it is not a good idea to try to build up command lines into a single string. Use functions, or isolate the actual command and leave the arguments in $@.

Rewrite the command like this:

my_cmd_bad_ () {
    cmd=$1; shift
    $cmd "$@"
}

See http://mywiki.wooledge.org/BashFAQ/050

Note that your second version is greatly preferred most of the time. The only exceptions are if you need to do something special. For example, you can't bundle an assignment or redirect or compound command into a parameter list.

The correct way to handle the quoting issue requires non-standard features. Semi-realistic example involving a template:

function myWrapper {
    typeset x IFS=$' \t\n'
    { eval "$(</dev/fd/0)"; } <<-EOF
    for x in $(printf '%q ' "$@"); do
        echo "\$x"
    done
EOF
}

myWrapper 'foo bar' $'baz\nbork'

Make sure you understand exactly what's going on here and that you really have a good reason for doing this. It requires ensuring side-effects can't affect the arguments. This specific example doesn't demonstrate a very good use case because everything is hard-coded so you're able to correctly escape things in advance and expand the arguments quoted if you wanted.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top