fish shell quickstart for converting bash scripts

After some years of bash and PowerShell, and some hours of using fish, I’ve realised that expansion & predictive typeahead are good features in a shell, whereas “be a great programming language” is less important than I thought: because there is no need to write scripts in the language of your shell.

Fish has slicker typeahead and expansions than bash or even PowerShell. But to switch to a fish shell, you do still have to convert your profile & start-up scripts. So here’s my quick-start guide for converting bash to fish.

  • Do this first: at the fish prompt type help. Behold! the fish documentation in your browser is much easier to search than man pages are.
  • Calmly accept that fish uses set var value instead of var=value. Roll your eyes if it helps.
  • Use end everywhere that bash has fi, done, esac, braces {} etc. e.g. function definition is done with function ... end. The keywords do and then are redundant everywhere, just remove them. else has a semicolon after it. case requires a leading switch(expr).
  • There is no [[ condition ]] but [ ... ] or test ... work. Type help test to see all the file and numeric tests you expect, such as if [ -f filename ] etc. string and regex conditionals are done with the string match command (see below). You can replace [[ -f this && -z that || -z other ]] with [ -f this -a -z that -o -z other ] but see below for how fish can also replace || and && constructions with or and and statements.
  • But first! type help string to see the marvels of proper built-in string commands.
  • Replace function parameters $*, $1, $2 etc with $argv, $argv[1], $argv[2] etc. If that makes you scowl, then type help argparse. See! That’s much better than kludging about in bash.
  • Remove the $ from $(subcommand) leaving just (subcommand). Inside quotes, take the subcommand outside the quote: "Today is $(date)" becomes "Today is "(date). (Recall that quotes in bash & fish don’t work at all like quotes in most programming languages. Quote marks are not token delimiters and a"bc"d is a valid single token and is parsed identically to each of abcd , "abcd", abc'd').
  • Replace heredocs with multi-line literal strings and standard piping syntax. However, note that if you pipe or read to a variable, the default multiline behaviour is to split on newline and generate an array. Defeat this by piping through string split0 – see

Search-and-replace Script Snippets

Here is my hit-list of things to search and replace to convert a bash shell to fish. These resolved almost all of my issues in converting a few hundred lines of bash script to fish.

var=valueset var value
export var=valueset -x var value
export -f functionnameredundant.Just remove it
alias abbr=’commandstring’(no change)alias syntax is accepted as an abbreviation for a function definition since fish 3
command $(subshell commmand)
command `subshell commmand`
command (subshell command)
command (subshell commmand | string split0)
Just remove the $ but keep the ()

See below for when you want to add string split0
command “$(subshell commmand)”command (subshell command)Remove both the $ and the quotes ""to make this work
if [[ condition ]] ; then this ; else that ; fiif [ condition ] ; this ; else ; that ; endSee below for more on Fish’s multine and and or syntax.
if [[ number != number ]] ; then this ; else that ; fiif [ number -ne number ] ; this ; else ; that ; endSee below for more on Fish’s multine and and or syntax.
while condition ; do something ; donewhile condition ; something ; end
$1, $2$argv[1], $argv[2]But see help argparse
if [[ testthis =~ substring ]] if string match -q ‘*substring*’ testthisstring match without -r does glob style testing
if [[ testthis =~ regexpattern ]] if string match -rq regexpattern testthisstring match with -r does regex testing
[ guardcondition ] && command
[ guardcondition ] || command
works as isBut see or and and below for when it’s more complex
var=${this:-$that}if set -q this ; set var $this ; else ; set var $that ; end
cat > outfile <<< “heredoc”
cat > outfile <<< “multiline … heredoc”
echo “multiline … heredoc” | cat > outfile no heredocs, but multiline strings are fine
NB printf is better than echo for anything complicated, in any shell.
if [[ -z $this && $that=~$pattern ]]if [ -z $this ] ; and string match -rq $pattern $that ;
content=$(curl $url)set content (curl $url | string split0)without the pipe to string split0, content will be split on newlines to an array of lines.

Fish’s multine and and or syntax

Fish has a multiline and and or syntax that may be clearer than && and || in both conditionals and guarded commands. It is less terse.

[ condition ]
and do this
or do that

That said, && and || are still valid in commands :

[ condition ] && do this || do that

Other gotchas

  • You may have to read up on how fish does parameter expansion, and especially handling spaces, differently to bash.
  • Pipe & subcommand output to multiline strings or arrays: set x (cat myfile.txt) will set x to an array of the lines of myfile.txt. To keep x as a single multine string, use string split0 : set x (cat myfile.txt | string split0)

Official tips for new fishers:

See the FAQ at

(Failing to) Copy a Time Machine Backup to a Network Drive with asr

The Apple support page for copying a Time Machine backup disk doesn’t cover the scenario when your new backup target disk is on the network. If you try to do it by hand using cp, rsync, ditto or other, you will likely fail with inscrutable errors.

Using asr may work, but failed for me after 1 ½ days, 500GB, possibly because I had some kind of network disconnected. To rely on a network being reliable for 3 days is to ignore the 8 fallacies of distributed computing, but if your TM backup is small enough this could work.

  1. Use Disk Utility -> File -> New Image -> Blank Image … to create a new sparsebundle disk image on your network drive. The arrowed options must be set correctly (well, you don’t have to use sparse bundle but it is allegedly designed specifically for efficient use across a network):


2. Mount the new disk image by double-clicking it, and also attach your existing Time Machine backup drive. Then, use  -> About This Mac -> System Report… -> Hardware/Storage and look in the column BSD Name to find the device names on which your Source and Target volumes are mounted:

3. Turn off Time Machine backup. Usually by unticking “Back Up Automatically” in the Time Machine preferences, if there is no On/Off switch.

4. Then, use asr on the command line to copy the device that hosted the volume to the device hosting the new volume. Use caffeinate at the same time to stop the computer sleeping instead of copying. In my case that was:

sudo caffeinate asr restore --source /dev/disk3 --target /dev/disk4s2 --erase --puppetstrings --allowfragmentedcatalog

I got this output, and after a few seconds had to type y to confirm:

XSTA    start   512 client
XSTA setup
Validating target…done
XSTA metadata
Validating source…done
Erase contents of /dev/disk4s2 (/Volumes/LaCie2019)? [ny]:

The --puppetstrings option means what most of us might call --progress although the output is quite limited.

Expect a speed of about 4 days per terabyte. I don’t know why. Watching the Network tab in Activity Monitor I can see that data is rarely going faster than 5MB/s. Even writing to a spinning disk across a 20 year old 100Mbps network should go faster than that. I tried adding --buffers 10 --buffersize 100MB, but that still only got me to about 3 days per terabyte.

Anyway …

For me it failed. Sorry I lost the error message. So I went to to Finder drag-n-drop. The first time this failed after a day; the second it succeeded after 3 days. 🤷‍♂️

Use NSSM to install SyncThing as a Windows service

SyncThing does what OneDrive & Google Drive can do but under your control, across your machines, with more options, and without having to touch a 3rd party data snooping provider and without having to pay 3rd party Terabyte rates. I use it on my home network both to synchronise configuration across multiple machines and as an at-home backup solution. It’s fast, simple, well-maintained and it works.

NSSM is “the Non-Sucking Service Manager” which has a simple GUI to set up commandline programs like SyncThing as a Windows Service.

Install SyncThing

To use SyncThing as a Service, avoid the GUI options such as SyncTrayzor and go for the GitHub download. Choose a directory to install to, such as your Program Files directory.

SyncTrayzor is great for your working machine, where you only need SyncThing to run when you are logged in. For a server which is hosting backups and redundant copies of your files, you want a Windows service running whenever the machine is up.

Install NSSM

NSSM also has no installer as of early 2020. Download & extract to a Program Files directory.

I then added New-Alias nssm "C:\Program Files\nssm-2\win64\nssm.exe" to my PowerShell profile

Launch NSSM

nssm without parameters will show you the commands you can use. The simplest is to use install & edit to get the GUI:
To show service installation GUI: nssm install [<servicename>]
To show service editing GUI: nssm edit <servicename>

So use:

nssm install SyncThing

And then fill in the boxes by finding the path where you installed SyncThing. I only edited the first three tabs: Application, Details, and Log On. The rest can stay as default.

What about the Parameters? See the SyncThing Docs. This is mine:

-no-console -no-browser -no-restart -gui-address=localhost:8384

-no-console -no-browser are because services run headless.
-no-restart because the Windows Service infrastructure has options for handling restarts.
-gui-address=localhost:8384 to make the gui console only available on localhost, not across the network. You may not want this.

You can now use nssm to start/stop/monitor services, not just the ones you have installed with it.

nssm start SyncThing
nssm status SyncThing

Or, you can use the standard Windows Services gui.

Where is the config?

Nssm just edits the Windows service config, which is visible in the Local Services app, which you can launch from Task Manager -> Services

SyncThing keeps config in the place noted in SyncThing Docs unless you add e.g. -home=D:\MyPath to the startup parameters

Where is the SyncThing Gui?

If you followed my example and used -gui-address=localhost:8384 then open that address in your browser and read all about at

More Options?


Yes but I want to manage it across my home network?

  1. Change the startup options to use -gui-address=
  2. Add the full path to SyncThing.exe as a firewall exception in your Windows firewall.
  3. Restart the service

This will make the browser interface accessible across the network. Then:

  1. Open the the GUI at localhost:8384.
  2. Open the Settings (under the Actions menu, top right).
  3. Open the GUI panel.
    1. Choose HTTPS
    2. Add a username and password. NB I think these are both case sensitive.

Choosing an Affordable Mechanical Keyboard

tl;dr: KeyChron are brilliant and cheaper than the alternatives.

My nephew was foolish enough to ask about the keyboard he spotted in a photo of my desktop so, armed with my Keychron K2, I explained what led me to it.

I got out kitchen scales and improvised some 2-10 gramme weights and carefully weighed the actuation force on my MacBook and on my Logitech K480. I found that the MacBook is approx 60grammes and that the K480—which I find too much hard work to type on—is about 70g. To me, the difference feels much more than that, but that’s what the scales told me. (And yes, the A-level physicists amongst you will know that by gramme I mean, milliNewton)

I deduced that any switch of 60g or below, I would be happy with.
I then noted that the Gateron Blue is allegedly noisy (fine if you don’t share a room with someone, anti-social if you do); and I examined the graphs of the actuation curves. I would have preferred the ‘clicky’ buckling spring feel of the Blue but I’ve used a noisy keyboard before and feel the noise can be a real downer. I definitely didn’t want the linear feel which the Red and Black have (allegedly gamers like it, but I don’t) so that left me with the Brown ‘Tactile’ which is in between.

I went for aluminium backlit because boo to plastic (don’t kid yourself though; I do not know whether the carbon footprint of producing the aluminium keyboard is any less than that of the plastic). Nightlife without backlighting is grim, it is must-have for me. My other must-haves are Mac keycaps, and either the “tenkeyless” keyset or the full 101 keys, because I hate not having page up/down and home/end on the main keyboard.

I agonised over K1 or K2 and went for K2 because K1 didn’t have the Brown switches. But … having become a closet Apple fanboi I am now disappointed that the K2 feels much higher off the desk that the Apple ultra-flat style and I may yet buy a K1 instead. Or a wrist rest.

I assure you however that the K2 is the best keyboard I’ve had for years. I had an IBM buckling spring keyboard for while in the 1990s, and a noisy Cherry for a while last year. The KeyChron has the same solid feel at the IBM had, but with the Brown switches is lighter and feels slightly less industrial. It is really nice, impressively priced, and I very much like it.

It will make you want to type long letters, or—should you happen to have WhatsApp Web on your computer—really really long messages to your nephew about how good your keyboard is. 🤷

Also 10% off!

I did spend a while looking at other keyboards, mostly mechanical, but wasn’t willing to pay the enormous prices. KeyChron is a half or even a third of the price of other good mechanical keyboards. Seemed a no-brainer to me.