39

I am trying achieve the same effect as typing

mv ./images/*.{pdf,eps,jpg,svg} ./images/junk/  

at the command line, from inside a bash script. I have:

MYDIR="./images"
OTHERDIR="./images/junk"  
SUFFIXES='{pdf,eps,jpg,svg}'
mv "$MYDIR/"*.$SUFFIXES "$OTHERDIR/"

which, when run, gives the not unexpected error:

mv: rename ./images/*.{pdf,eps,jpg,svg} to ./images/junk/*.{pdf,eps,jpg,svg}: 
No such file or directory

What is the correct way to quote all this so that mv will actually do the desired expansion? (Yes, there are plenty of files that match the pattern in ./images/.)

Ross Duncan
  • 610
  • 1
  • 5
  • 11

3 Answers3

41

A deleted answer was on the right track. A slight modification to your attempt:

shopt -s extglob
MYDIR="./images"
OTHERDIR="./images/junk"  
SUFFIXES='@(pdf|eps|jpg|svg)'
mv "$MYDIR/"*.$SUFFIXES "$OTHERDIR/"

Brace expansion is done before variable expansion, but variable expansion is done before pathname expansion. So the braces are still braces when the variable is expanded in your original, but when the variable instead contains pathname elements, they have already been expanded when the pathname expansion gets done.

Dennis Williamson
  • 346,391
  • 90
  • 374
  • 439
  • 9
    +1, neat trick, didn't know about `@()`, I've learned something new. Just looked it up in the manpage, for anyone wondering there are `?()` patterns, `*()` patterns, `+()` patterns, `@()` patterns, and `!()` patterns. All available if `extglob` is enabled. – falstro Jan 15 '11 at 09:43
  • Brilliant, thanks! Quoting and expansion in shell scripts always causes me problems, this will help a lot. – Ross Duncan Jan 17 '11 at 16:18
  • 2
    @RossDuncan, I'd suggest http://mywiki.wooledge.org/BashParser as a place to start for a good reference on the phases involved in the shell's execution model and which order they happen in. – Charles Duffy Nov 08 '15 at 03:13
  • 1
    @YzmirRamirez: Yes. Did you try it? – Dennis Williamson Jan 20 '19 at 03:06
  • 1
    Yes...it must be my environment or some other shell option that is conflicting. Hope it works out for others though. – Yzmir Ramirez Jan 20 '19 at 03:14
  • note that this won't work unless the filename is the entire argument. for something like `dd if="$MYDIR/"*".bin"` you'd need eval – martinkunev May 10 '22 at 11:06
10

You'll need to eval that line in order for it to work, like so:

MYDIR="./images"
OTHERDIR="./images/junk"  
SUFFIXES='{pdf,eps,jpg,svg}'
eval "mv \"$MYDIR\"/*.$SUFFIXES \"$OTHERDIR/\""

Now, this has problems, in particular, if you don't trust $SUFFIXES, it might contain an injection attack, but for this simple case it should be alright.

If you are open to other solutions, you might want to experiment with find and xargs.

falstro
  • 34,597
  • 9
  • 72
  • 86
5

You can write a function:

function expand { for arg in "$@"; do [[ -f $arg ]] && echo $arg; done }

then call it with what you want to expand:

expand "$MYDIR/"*.$SUFFIXES

You can also make it a script expand.sh if you like.

Dagang
  • 24,586
  • 26
  • 88
  • 133
  • 2
    Yes, `echo` instead of `expand` in the answer, would give every match, not just files. In a general case, if you just want to expand the wildcard, and you want the result in an array, simply use: `array=($(echo /path/to/files.*))`. Or without the path: `array=($(cd /some/path && echo file-*.jpg))`. Of course be careful with special characters in filenames. – Yeti Apr 27 '20 at 07:59
  • I have no idea why but the `-f` file exists test wouldn't work until I used single braces (bash 4.4.20). The test worked but capturing the function output from the following echo was not, i.e. `FILES=$(expand *.txt)`. – jozxyqk Jun 02 '20 at 22:45