0

So when I want to use bash to list out files starting with multiple letters I can do something like echo /home/username/{A,B,C}* which correctly echoes the filenames starting with A, B, C.

I'm trying to do the same with a bash variable with user input inside a script, say something along the lines of(say the script name is run_user_input.sh):
var=$1;echo $var;

And I run it as ./run_user_input.sh "/home/username/{A,B}*".

But this just echoes /home/username/{A,B}*.

Note that var="/home/username/A*";echo $var; still works correctly.

How can this be fixed?

tangy
  • 3,056
  • 2
  • 25
  • 42

1 Answers1

1

In double quotes, only a few things are expanded: parameter and command substitution, most importantly, but no filenames (and no brace expansion), so * within double quotes is always literal.

Then, on the right-hand side of an assignment, there is also no filename and brace expansion, so var=* will put a literal * into var, even without quotes. Your last example "works" only because you don't quote the expansion of $var, but it contains literally /home/username/A*.

The way to do it is to use an array:

fnames=(/home/username/{A,B,C}*)

This will perform both brace and filename expansion, and the array elements will contain one filename each, properly escaping spaces and glob characters. Access them with proper quoting:

echo "${fnames[0]}"

gives you the first filename, for example.

If you supply the pattern as a parameter to a script, there's probably no way around using eval:

pattern=$1
eval fnames=("$pattern")

This comes with the usual warnings about eval. If you supply as a pattern the following:

pattern='x); echo pwnd #'

eval will expand the line to

fnames=(x); echo pwnd #)

and actually run the injected command, which could be less friendly than just an echo.

Sadly, the robust method

eval "$(printf 'fnames=(%q)' "$pattern")"

doesn't work, as it prevents expansion.

To avoid this, I'd recommend rewriting your script to take multiple arguments and let the shell do the expansion:

./yourscript path/to/dir/{A,B}*

and in the script something like

for file; do <something>; done

(which is the same as for file in "$@"; do <something>; done). Now you have the benefits of expansion being taken care of by the shell, without the drawbacks of eval.

Benjamin W.
  • 46,058
  • 19
  • 106
  • 116
  • 1
    Worth noting that brace expansion is *not* performed on the result of a parameter expansion, and `bash` is exhibiting its default behavior of treating a non-matching pattern (non-matching here because of the unexpanded braces) as a literal string. – chepner Aug 09 '18 at 14:37
  • The thing is that my script takes `"/home/username/{A,B,C}*"` as customizable user input. So it worked as `echo $1` where `$1 == "/home/username/{A,B,C}*"` - I should have stated this more clearly. The above solution wont work directly with such an input right? – tangy Aug 09 '18 at 14:37
  • @tangy Something like in this question? https://stackoverflow.com/questions/43751540/how-to-store-curly-brackets-in-a-bash-variable – Benjamin W. Aug 09 '18 at 14:48
  • @tangy You need to update the question with *exactly* how your script receives that input. – chepner Aug 09 '18 at 14:49
  • @tangy You're likely to end up with needing `eval` if you want to evaluate user input that contains brace expansions. – Benjamin W. Aug 09 '18 at 14:50
  • Thanks, yes I've updated the question. And yes something like the question you have linked. – tangy Aug 09 '18 at 14:52
  • @tangy I've updated the answer, but see remark at the end. – Benjamin W. Aug 09 '18 at 15:17
  • Thanks for both the answer and the additional suggestion avoiding eval! – tangy Aug 09 '18 at 16:18