Portable (Cartesian) brace expansion in your shell

printf and subshells for the win

Cartesian expansion, also known as brace expansion, is an incredibly powerful feature of most unixy shells, but despite being fundamentally simple and incredibly empowering, it’s been traditionally relegated to the dark and shadowy corners of command line hacking, employed only by greybeards looking to avoid repeating themselves at any cost. And, boy, does it really cut down on repetition.

Take for example this snippet from a Dockerfile that sets up permissions on certain directories:

mkdir -p /var/log/php /var/log/unitd /var/log/mysql
chown -R user:user /var/log/php /var/log/unitd /var/log/mysql
chmod ug+rw /var/log/php /var/log/unitd /var/log/mysql

You can immediately see how the /var/log/ prefix is a source of repetition: wasting valuable horizontal screen real estate, extra room for typos or mistakes, and diluting the signal:noise ratio of the lines in question.

In most shells,1 you can use braces ({})2 to accomplish the same with much less repetition:

mkdir -p /var/log/{php,unitd,mysql}
chown -R user:user /var/log/{php,unitd,mysql}
chmod ug+rw /var/log/{php,unitd,mysql}

..which expands to the same, effectively operating as a declarative “for loop” running for each of the items within the braces; e.g. echo {hello,goodbye}_world prints hello_world goodbye_world (mxn complexity with each braced/non-braced component being one set) while echo {hello,goodbye}_world{.com,.exe} prints hello_world.com goodbye_world.com hello_world.exe goodbye_world.exe, or mxnxo complexity with m equal to two, n equal to one, and o equal to two for four results.

Depending on the shell, this functionality can be taken a step further and used to do questionable things, like combining with glob expansion to get a list of all files with a certain extension that appear in one of n different directories with an expression like foo/{bar,baz}/*.{c,h} or anything else your heart desires.

Alas, the one issue with Cartesian/brace expansion is that it isn’t formalized in the posix sh spec, meaning you can’t use it in truly portable shell scripts or with a #!/bin/sh shebang. But fret not, because you can still accomplish the same thing with another feature: subshell output interpolation.

To jump right to it, here is portable syntax that will accomplish the same as our original demonstration snippet, except this is truly portable and you can use it in your Alpine Linux Dockerfile running sh:

mkdir -p $(printf /var/log/%s\n php unitd mysql)
chown -R user:user /var/log/$(echo php unitd mysql)
chmod ug+rw /var/log/$(printf %s\n php unitd mysql)

As you can see, there are several ways it can be used, relying on printf to perform the expansion for you directly (taking advantage of the fact that printf is spec’d to repeat the input when it is provided with more parameters than placeholders), use echo instead of printf if your $IFS configuration splits on spaces, or use printf %s\n if you want to support spaces in paths and have $IFS set to split only on new lines (or you’re smart and sane and use fish which defaults to splitting only on new lines anyway) – in which case the second example with echo instead of printf %s\n won’t work for you.

So keep it DRY3 and embrace the power of brace expansion (or Cartesian expansion in general, if you don’t have access to brace expansion in your shell of choice) the next time you’re banging out a shell script!


  1. Including, but not limited to, fish (the one true shell), bash, zsh, ksh (as of ksh88), zsh, and csh/tcsh (where this feature originated). But notably not supported by dash and not part of the posix sh spec. 

  2. These are called braces, or sometimes curly braces. They are not brackets ([]) and there is no such thing as a “curly bracket”. 

  3. Don’t Repeat Yourself 

  • Similar Posts

    Craving more? Here are some posts a vector similarity search turns up as being relevant or similar from our catalog you might also enjoy.
    1. Scripting in rust with self-interpreting source code
    2. How to import any Linux command into your Windows 10 environment
  • Leave a Reply

    Your email address will not be published. Required fields are marked *