0

I am running into an issue in a script I have. I have the following:

~/test> echo bar > foo.txt; echo bar > foo.c
~/test> flags="--include \"*.txt\""
~/test> echo $flags
--include "*.txt"
~/test> grep -rHI $flags bar .
~/test> grep -rHI --include "*.txt" bar .
./foo.txt:bar

So, basically grep -rHI $flags bar . is not evaluating to the same as grep -rHI --include "*.txt" bar . There is a way around this (using eval), but I would be interested in understanding why $flags is being treated differently than its literal expansion in this case.


Apparently, this is being marked as duplicate. The quoted question deals with whitespace between elements, whereas, this question is about precedence of expansion as choroba pointed out. (The two seem to have the same solution though). I had found several references to using eval/array, but I didn't find a satisfactory answer as to what was the cause of the problem, and hence I was asking.

John
  • 3,400
  • 3
  • 31
  • 47
  • 5
    You *cannot* get embedded quotes in a string to work correctly for this. Don't try. Use an array. See http://mywiki.wooledge.org/BashFAQ/050 for details. – Etan Reisner Dec 16 '14 at 17:39
  • @EtanReisner fortunately in this case embedded quotes are not needed. `flags="--include *.txt"` should work as intended. – n. m. could be an AI Dec 16 '14 at 18:05
  • @n.m. Yes, if the expansion of `$flags` is quoted and grep can handle seeing `--include *.txt` as one "word" of argument that will work but that's not a generally guarantee-able thing. – Etan Reisner Dec 16 '14 at 18:09
  • @EtanReisner It's not one word. `"$flags"` would be one word. Word splitting occurs way after parameter expansion. – n. m. could be an AI Dec 16 '14 at 18:14
  • @n.m. Notice I said "if the expansion of `$flags` is quoted" there? That's important because without that the wildcard is expanded and grep doesn't see it but instead sees a list of files (which isn't the same thing). – Etan Reisner Dec 16 '14 at 18:16
  • Ah, OK. But it isn't and need not be quoted here. – n. m. could be an AI Dec 16 '14 at 18:36
  • Actually `flags="--include *.txt"` does not quite work. You'll notice the -r flag for grep. I need it to look for *.txt files recursively (I oversimplified the question I suppose). If you use `flags="include *.txt"`, then `*.txt` gets expanded to all files in the local directory, rather than to `*.txt`, which is what I would need. – John Dec 16 '14 at 18:45
  • @John you are right, my bad. – n. m. could be an AI Dec 17 '14 at 06:18

2 Answers2

2

The reason for the different behaviour is the order of expansions:

Expansion is performed on the command line after it has been split into words. [...] The order of expansions is: brace expansion, tilde expansion, parameter, variable and arithmetic expansion and command substitution (done in a left-to-right fashion), word splitting, and pathname expansion. [...] Only brace expansion, word splitting, and pathname expansion can change the number of words of the expansion; other expansions expand a single word to a single word. The only exceptions to this are the expansions of "$@" and "${name[@]}"

flags=--include=*.txt should be safe, though, unless you have a file named --include=123.txt somewhere, which you can prevent by quoting "$flags" in the command. But, as the quoted documentation says at the end, using an array is the best solution, if there are more than one flag:

flags=(--include '*.txt')
grep -rHi "${flags[@]}" bar .
choroba
  • 231,213
  • 25
  • 204
  • 289
  • Quotes aren't necessary on the assignment then either and then `"$flags"` at usage prevents the glob expansion. – Etan Reisner Dec 16 '14 at 18:18
  • It's no longer `flags`, then, is it? You're only supporting a single (one and only one -- zero won't work!) `flag` if quoting the expansion. Not quoting the expansion will work in this case if glob expansions are disabled, but it won't work in a more general case -- where, say, you might want to pass around filenames with spaces. – Charles Duffy Dec 16 '14 at 18:28
2

Using an array is the only canonically correct, generic answer for passing multiple arguments with their original boundaries preserved.

flags=( --include '*.txt' )
grep -rHi "${flags[@]}" bar .
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441