14

I'm trying to expand a string involving a wildcard and a collection of extensions specified within braces. Nothing seems to work as the example below illustrates. the variable firstList expands fine, but neither secondList, thirdList or fourthList expands properly. I've also tried various versions of eval but none work either. Any help would be appreciated

#!/bin/bash
touch a.ext1
touch b.ext1
firstList='*.ext1'
ls  $firstList
touch a.ext2
touch b.ext2
secondList='*.{ext1,ext2}'
ls $secondList 
ls '$secondList'
ls "$secondList"
thirdList=*.{ext1,ext2}
ls $thirdList  
ls '$thirdList'
ls "$thirdList"
fourthList="*.{ext1,ext2}"
ls $fourthList
ls '$fourthList'
ls "$fourthList"
Leo Simon
  • 453

2 Answers2

15

The shell expands * only if un-quoted, any quoting stops expansion by the shell.

Also, a brace expansion needs to be unquoted to be expanded by the shell.

This work (lets use echo to see what the shell does):

$ echo *.{ext1,ext2}
a.ext1 b.ext1 a.ext2 b.ext2

Even if there are files with some other names:

$ touch {a,b}.{ext1,ext2} {c,d}.{ext3,ext4} none
ls
a.ext1  a.ext2  b.ext1  b.ext2  c.ext3  c.ext4  d.ext3  d.ext4  none

$ echo *.{ext1,ext2}
a.ext1 b.ext1 a.ext2 b.ext2

Why that works?

It is important that we understand why that works. It is because of the order of expansion. First the "Brace expansion" and later (the last one) "Pathname Expansion" (a.k.a glob-expansion).

Brace --> Parameter (variable) --> Pathname

We can turn off "Pathname expansion" for a moment:

$ set -f
$ echo *.{ext1,ext2}
*.ext1 *.ext2

The "Pathname Expansion" receives two arguments: *.ext1 and *.ext2.

$ set +f
$ echo *.{ext1,ext2}
a.ext1 b.ext1 a.ext2 b.ext2

The problem is that we can not use a variable for the brace expansion.
It has been explained many times before for using a variable inside a "Brace Expansion"

To expand a "Brace Expansion" that is the result of a "Variable Expansion", you need to re-submit the command line to the shell with eval.

$ list={ext1,ext2}
$ eval echo '*.'"$list"

Brace --> Variable --> Glob || --> Brace --> Variable --> Glob
........ quoted here -->^^^^^^ || eval ^^^^^^^^^^^^^^^^^^^^^^^^^

Values of the file names bring no execution problem for eval:

$ touch 'a;date;.ext1'
eval echo '*.'"$list"
a;date;.ext1 a.ext1 b.ext1 a.ext2 b.ext2

But the value of $list could be unsafe. However, the value of $list is set by the script writer. The script writer is in control of eval: Just not use externally set values for $list. Try This:

#!/bin/bash
touch {a,b,c}.ext{1,2}
list=ext{1,2}
eval ls -l -- '*.'"$list"

A better alternative.

An alternative (without eval) is to use Bash "Extended Patterns":

#!/bin/bash
shopt -s extglob
list='@(ext1|ext2)'
ls -- *.$list

Note: Please be aware that both solutions (eval and patterns) (as written) are safe for filenames with spaces or new lines. But will fail for a $list with spaces, because $list is unquoted or the eval removes the quotes.

2

Consider:

secondList='*.{ext1,ext2}'
ls $secondList 

The problem is that brace expansion is done before variable expansion. That means that, in the above, brace expansion is never performed.

This is because, when bash first sees the command line, there are no braces. After secondList is expanded, it is too late.

The following will work:

$ s='*'
$ ls $s.{ext1,ext2}
a.ext1  a.ext2  b.ext1  b.ext2

Here, the command line has braces so that brace expansion can performed as the first step. After that, the value of $s is substituted in (variable expansion), and lastly pathname expansion is performed.

Documentation

man bash explains the order of expansion:

The order of expansions is: brace expansion; tilde expansion, parameter and variable expansion, arithmetic expansion, and command substitution (done in a left-to-right fashion); word splitting; and pathname expansion.

John1024
  • 74,655