3

Imagine i have following bash script:

#/bin/sh
#next line will work correctly, outputting user name and current directory
start-stop-daemon --start --exec /bin/su -- root -c 'whoami; ls'

MYVAR="start-stop-daemon --start --exec /bin/su -- root -c 'whoami; ls'"
#next line will fail with following error:
#ls': -c: line 0: unexpected EOF while looking for matching `''
$MYVAR

Question - why second approach via variable does not work? How to make it work?

Sergey Alaev
  • 253
  • 1
  • 7

2 Answers2

2

Bourne (and Bourne again) shell has complicated rules from line parsing to command execution.

For the sake of simplification, let's reduce your direct command line to

su -c 'whoami; ls'

and your "embedded" ones to

MYVAR="su -c 'whoami; ls'"     
$MYVAR                   

In the first case, the command line is split in 3 tokens (words), because the space character is a metacharacter delimiting words, and also because quote escapes metacharacters (so is escaped the space into quotes). Before command execution, a quote removal stage applies, leaving 3 words and no quotes. The first word is the command name su, and the 2 others are -c and whoami; ls, the command parameters. Right.

In the second case, the call consists of a single parameter expansion. For some expansions (including parameter expansion), word splitting applies. To that aim, a special variable named IFS is used to deduce words from the expanded parameter value. By default, IFS consists of space and tab characters, and newline. That means that each of these characters are used to split bunch of characters to constitute words. Here, the expanded value is:

su -c 'whoami; ls'

and we end up with 4 words, namely: su, -c, 'whoami; and ls'. On top of that, the quote removal stage does not occur for parameter expanded values. Command is issued (with command name su and its 3 arguments), and you obtain a weird error message.

The complexity of this situation is that we have to keep a word containing a space as an argument to su, the space itself being exposed to the word splitting stage, and quotes are useless in parameter expansion.

What you can do to deal with this is to play with the IFS variable. One solution would be:

IFS=: MYVAR="su:-c:whoami; ls"
$MYVAR

IFS is here overriden to exclude the space character from word splitting, the task being assumed by the newly introduced colon character.

lledr
  • 141
  • 5
2

As @lled explained, the problem is that the shell processes quotes before expanding variables, so putting quotes in variables doesn't do anything useful. But there are a couple of alternatives, depending on why you want to store the command (rather than just executing it directly).

If you just want to define the command once, then use it repeatedly, use a function:

myfunc() {
    start-stop-daemon --start --exec /bin/su -- root -c 'whoami; ls'
}
# ...
myfunc

If you need to build/select the command in one place, then use it in another place, you can use a bash array to store it. Store each "word" of the command as an array element, and then if you reference it correctly those word breaks will be preserved:

myarray=(start-stop-daemon --start --exec /bin/su -- root -c 'whoami; ls')
# ...
"${myarray[@]}"

What happens here is that the array gets defined as having the elements "start-stop-daemon", "--start", "--exec", "/bin/su", "--", "root", "-c", and "whoami; ls". The single-quotes aren't stored as part of the array, but they have the effect of making "whoami; ls" a single array element. Then, in the expansion of the array, the [@] tells the shell to expand each array element into a separate word, and the double-quotes around it prevent any additional word splitting on the resulting values.

For more info (and a couple of other options), see BashFAQ #50:I'm trying to put a command in a variable, but the complex cases always fail!

Gordon Davisson
  • 11,036
  • 3
  • 27
  • 33