Brace expansion and sequences in Fish
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.