banner

For a full list of BASHing data blog posts see the index page.     RSS


Brace expansion with variables and arrays: eval to the rescue

In a 2019 blog post I tinkered with two alternatives to BASH brace expansion. Alternatives might be needed because strings with spaces cause problems unless separately quoted, and (I thought) you can't put shell variables inside the braces because BASH does brace expansion first when executing a command.

Chris Dunlop from onthe.net.au emailed to point out that you can brace-expand variables, with the help of the shell built-in eval. This post explores details of the eval workaround, and I'm very grateful to Chris for pointing out errors I made in a draft post and for suggesting alternative commands.


Using eval. The "help" for eval says:

Combine ARGs into a single string, use the result as input to the shell,
and execute the resulting commands.

To ensure that double quotes get left alone by the shell when eval is used, those quotes (and any backslash escapes) have to be protected with single quotes or escapes:

eval1

Brace expansion with variables. BASH will happily brace-expand a pair of comma-separated strings of items, as shown for the first command in the screenshot below. If those strings are stored in variables, the variables get expanded but brace expansion fails. There's no comma-separated string of items to expand, because brace expansion has priority in BASH over variable expansion. However, if you put eval before the command (last command in screenshot), the variables get expanded before the shell does brace expansion, and it all works nicely:

eval2

Brace expansion with variables containing spaces. For this demo I'll use a couple of the personal names from the original blog post. If an item in a comma-separated list for brace expansion contains a space, the item must be quoted:

eval3

Escaping the quotes, the same job can be done with eval and variables:

eval4

A slightly sexier way to do this is to escape the space in the variable with a backslash:

eval5

I've used backslash escapes for clarity. You get the same result with single quotes:
 
f="'Ann Marie',Burt"
l="'de Jong',Jones"
eval printf '"%s\n"' {$f}'"' '"'{$l}


Brace expansion with arrays. You can also brace-expand BASH arrays. Suppose I create a couple of indexed arrays arr1 and arr2 and examine their contents:

eval6

The contents are just space-separated lists that can be converted to comma-separated ones suitable for brace expansion. One approach (with thanks to Chris Dunlop) is to change BASH's internal field separator (IFS) to a comma, but only in a subshell containing the echoed expansion of the array:

eval7

eval printf '"%s\n"' \
> {$(IFS=,;echo "${arr1[*]}")}{$(IFS=,;echo "${arr2[*]}")}
 
The change in IFS doesn't propagate to the main, working shell. It dies when the subshell $(...) finishes its work.

Another method is to convert spaces in the expanded arrays to commas with tr, store the results in variables, and then brace-expand the variables:

eval8

Brace expansion with array items containing spaces. Array items with spaces need to be quoted or have the spaces escaped:

eval9

The numbering of items in indexed arrays begins with "0".

The eval workaround for brace expansion won't work in this case when the arrays are expanded, because the escape backslash isn't "protected":

eval10

One way to protect the backslash (with thanks again to Chris Dunlop) is to use BASH string substitution:

eval11

eval printf '"%s\n"' \
{$(IFS=,;echo "${arr3a[*]//\ /\\ }")}\" \"{$(IFS=,;echo "${arr4a[*]//\ /\\ }")}
 
In the construction ${arr3a[*]//\ /\\ }, all instances of "\ " in the expanded array "arr3a" are replaced with "\\ ", where the first backslash protects the second one.

I find this a little easier to follow when the comma and backslash replacements are stored in variables:

eval12

Finally, here's the "variables" method with quoted array items:

eval13

Last update: 2020-04-22
The blog posts on this website are licensed under a
Creative Commons Attribution-NonCommercial 4.0 International License