• Uncategorized

About bash : Bash-Expand-braces-and-globs-with-spaces-in-filenames

Question Detail

I have some files that look like:

/path/with spaces/{a,b,c}/*.gz

And I need all files matching the glob under a subset of the a,b,c dirs to end up as arguments to a single command:

mycmd '/path/with spaces/a/1.gz' '/path/with spaces/a/2.gz' '/path/with spaces/c/3.gz' ...

The directories I care about come in as command line params and I have them in an array:

dirs=( "[email protected]" )

And I want to do something like:

mycmd "/path/with spaces/{${dirs[*]}}/"*.gz

but this doesn’t work, because bash expands braces before variables. I have tried tricks with echo and ls and even eval (*shudder*) but it’s tough to make them work with spaces in filenames. find doesn’t seem to be much help because it doesn’t do braces. I can get a separate glob for each dir in an array with:

dirs=( "${dirs[@]/#//path/with spaces/}" )
dirs=( "${dirs[@]/%//*.gz}" )

but then bash quotes the wildcards on expansion.

So: is there an elegant way to get all the files matching a variable brace and glob pattern, properly handling spaces, or am I stuck doing for loops? I’m using Bash 3 if that makes a difference.

Question Answer

To perform brace expansion and globbing on a path with spaces, you can quote the portions of the path that contain spaces, e.g.

mycmd '/path/with spaces/'{a,b,c}/*.gz

Doing brace expansion using a list of values from a variable is a little tricky since brace expansion is done before any other expansion. I don’t see any way but to use the dreaded eval.

eval mycmd "'/path/with spaces/'{a,b,c}/*.gz"

P.S. In such a case however, I would personally opt for a loop to build the argument list rather than the approach shown above. While more verbose, a loop will be a lot easier to read for the uninitiated and will avoid the need to use eval (especially when the expansion candidates are derived from user input!).

Proof of concept:

Using a dummy command (x.sh) which prints out the number of arguments and prints out each argument:

[[email protected]]$ shopt -s nullglob  # handle case where globbing returns no match

[[email protected]]$ ./x.sh 'path with space'/{a,b}/*.txt
Number of arguments = 3
- path with space/a/1.txt
- path with space/b/2.txt
- path with space/b/3.txt

[[email protected]]:~/temp$ dirs="a,b"
[[email protected]]k:~/temp$ eval ./x.sh "'path with space'/{$dirs}/*.txt"
Number of arguments = 3
- path with space/a/1.txt
- path with space/b/2.txt
- path with space/b/3.txt

Okay, so here is one using bash for the “braces” and find for the globs:

find "${dirs[@]/#//path/with spaces/}" -name '*.gz' -print0 | xargs -0 mycmd

Useful with this if you need the results in an array.

Here’s one for the GNU Parallel fans:

parallel -Xj1 mycmd {}/*.gz ::: "${dirs[@]/#//path/with spaces/}"

You may also like...

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.