Brace expansions are a shell syntax that lets you perform operations on a series of arguments with variable components without having to type each one out in full. Bash can do a lot with this that Fish can’t, so it’s taken a little work to replicate some common commands in Fish. But not much.

Argument lists

If a pair of braces (curly brackets, or {}) contains a comma-separated list, the argument is repeated with the brace replaced with each item in the list. For example, in both Bash and Fish you can use a single argument to create multiple directories:

mkdir -p project/{src,dist}

The -p creates the intermediate “project” directory and the braces expand to subdirectories of that, which produces:

.
└── project
    ├── dist
    └── src

Another handy use of this syntax comes up when copying or moving a file with a new extension. Again, this syntax works in both Bash and Fish:

mv resources/report.{txt,bak}

That will expand to mv resources/report.txt resources/report.bak, renaming the report.txt file to report.bak.

Numeric Sequences

In Bash you also get sequences, meaning you can run echo {1..5} and get 1 2 3 4 5. Fish doesn’t have range interpretation in its brace expansion, though. To do this in Fish, you need to use command substitution, in most cases using the seq command.

The following shows using both brace expansion and command substitution in a single command. The following syntax will only work in Fish:

mkdir -p Resources/20{20,21}/(seq -w 1 12)

The above creates a Resources directory containing directories 2020 and 2021 (using brace expansion), and in each of those directories a directory is created for 01 through 12 (the -w switch adds zero padding as needed):

.
└── Resources
    ├── 2020
    │   ├── 01
    │   ├── 02
    │   ├── 03
    │   ├── 04
    │   ├── 05
    [...]

The seq command has options for formatting and changing the increment as well (see man seq). If you put an integer between the start and end arguments, it will increment by that amount. To create only even numbered directories you would use:

mkdir -p Resources/(seq -w 2 2 12)

The above outputs numbers from 2 through 12 incremented by 2:

.
└── Resources
    ├── 02
    ├── 04
    ├── 06
    ├── 08
    ├── 10
    └── 12

Fish does have solid index range expansion, which makes it easy to output a specific range, multiple ranges, reverse ranges, etc., but this still has to be used with command substitution and can’t be used directly in a command the way brace expansion can.

You could use similar to create a text file for every day of every month in 2020 and 2021:

touch 20{20,21}-(seq -w 1 12)-(seq -w 1 30)-{research,meeting}.txt

This creates files like 2020-01-01-research.txt and 2020-01-01-meeting.txt for every day. Of course, the problem is that this assumes 30 days in every month, a problem you can only work around with a more complex solution…

A Brief Diversion

Here’s a Fish script for creating a text file for every day of every month for a given set of years.

for year in 20{19,20,21}
	for month in (seq -w 1 12)
		set -l days (cal $month $year | awk 'NF {DAYS = $NF}; END {print DAYS}')
		touch $year-$month-(seq -w 1 $days)-{meeting,research}.md
	end
end

This uses nested for loops for the known variables (3 years, 12 months each). Then it uses a little cal and awk hack to get the number of days for the current year/month combination in the loop. This allows us to run a touch command within the month loop, expanding to the correct number of days.

Alpha Sequences

One thing neither Fish nor seq can do (that I know of) is alphabetic sequences. In Bash, brace expansion understands {a..z} and will fill in the letters between. In order to accomplish this in Fish, you’ll need to execute something in the middle using Ruby, Perl, etc. For example, the following will do the same thing as echo a{a..d}b would in Bash:

$ echo a{(ruby -e 'print ("a".."d").to_a.join(",")')}b                                                                                                             
aab abb acb adb

The ruby command uses a range operator, output as an array and then combined with a comma. The output of this is passed to Fish within braces, meaning it’s interpreted as a list of arguments in a brace expansion.

The above example works with longer alphanumeric strings as well, such as "aab".."acc" (a function of Ruby, not of Fish, obviously). Dig into the documentation on “ranges” for your scripting tool of choice. In Ruby:

irb(main):004:0> print ("car".."cat").to_a
["car", "cas", "cat"]=> nil

Hopefully that’s all useful information for any Fish users who happen to be looking for it. And also good notes for my future self, which I’m sure I’ll find as a search engine result for my own question eventually.