5

I have a complex script that takes variables from files and uses them to run programs (Wine specifically)

Passing options from the variables in the other file isn't working as expected:

#!/bin/bash
. settingsfile
wine $run

And in the other file:

run="run.exe -withoption \"This text\""

When I change wine $run to echo wine $run, it echos a string, which when run explicitly works fine:

#!/bin/bash
. settingsfile
wine run.exe -withoption "This text"

Edit: Running with #!/bin/bash -x shows me:

+ wine run.exe -withoption '"This' 'text"'

How do I fix this?

J V
  • 11,402
  • 10
  • 52
  • 72
  • That's strange. It *should* work as is. Try changing the shebang line to `#!/bin/bash -x` to have a better idea of how the expansion is being performed. – Eduardo Ivanec Mar 03 '12 at 15:52
  • That's a brilliant option, thanks! It shows that it's being encased in single quotes for no apparent reason... – J V Mar 03 '12 at 16:00

2 Answers2

9

The problem is that "This and text" are treated as separate arguments, each containing a double-quote, rather than as a single argument This text. You can see this if you write a function to print out one argument per line; this:

function echo_on_separate_lines ()
{
  local arg
  for arg in "$@" ; do
    echo "<< $arg >>"
  done
}
run="run.exe -withoption \"This text\""
echo_on_separate_lines $run

prints this:

<< run.exe >>
<< -withoption >>
<< "This >>
<< text" >>

rather than this:

<< run.exe >>
<< -withoption >>
<< This text >>

The simplest solution is to tack on an eval to re-process the quoting:

run="run.exe -withoption \"This text\""
wine $run     # or better yet:   wine "$run"

But a more robust solution is to have run be an array, and then you can refer to it as "${run[@]}":

run=(run.exe -withoption "This text")
wine "${run[@]}"

so that the quoting is handled properly from the get-go.

ruakh
  • 175,680
  • 26
  • 273
  • 307
  • Great explanation, but is there any other way to do it other than an array? Or would it be possible to turn the string into an array correctly before running? – J V Mar 03 '12 at 16:03
  • 1
    @JV: I'm guessing you saw a version of my answer before I edited it to mention `eval`? Well, it now mentions `eval`, which is another way to do it other than an array. But I think that an array is the safest and most robust way, if you can do it. – ruakh Mar 03 '12 at 16:08
  • I could do an array, but it's annoying for cases where `$run` is a single file. `eval` is exactly what I'm looking for, thanks! – J V Mar 03 '12 at 16:14
5

Short answer: see BashFAQ #050: I'm trying to put a command in a variable, but the complex cases always fail!

Long answer: putting quotes in a variable doesn't do anything useful, because bash parses quotes before it replaces variable with their values; this means that by the time it's replaced $run with run.exe -withoption "This text" it's already done quote parsing, and isn't going to go back and notice those double-quotes, which means they don't do what you expect.

BTW, using echo to check what's happening is extremely misleading, because echo shows you what its arguments looked like after they're parsed, but you're reading it as though it was showing them before parsing. Instead, use @Eduardo's suggestion of -x to see what's really going. (BTW, you can also use set -x to turn that on for the interesting part of the script, then set +x to turn it off.)

So how do you solve it? Generally the best way is to put your command in an array rather than a simple variable, and then use the idiom "${arrayname[@]}" to pass each array element as a separate argument:

run=(run.exe -withoption "This text")
wine "${run[@]}"
Gordon Davisson
  • 118,432
  • 16
  • 123
  • 151