Gone Fishing
This weekend I explored the Fish Shell (a modern interactive Unix shell). I've been dabbling with it for a while, particularly enjoying its interactive features: command suggestions, the TAB-completion menu, the fact that it Just Works™ out of the box, and understands commands and options by groking their man
-pages.
I've wanted to port a few of my bash functions which I've been missing: my ssh-pass
to load SSH keys onto the Agent's keyring with passphrase supplied by pass
, and an alias to get my 1password passphrase from pass
as well. Also some directory navigation shortcuts that I've used since my university days. I figured these would be a good introduction to how fish's scripting works, and I was right
I started with something simple: my wtfo
function which runs wtf
with it's -o
option to include obscene abreviations. The hack is to change the error when it still can't find any expansion (“nothing inappropriate”). Here's my original bash version, listed with my list
meta-function:
[mjl@jazz:~/hax/gitlab.com/milohax/milosophical-me]$ list wtfo λ: function wtfo --> /Users/mjl/.dotfiles/source/20_env.sh:33 "Look up an abbreviation, including obscene meanings"; wtfo () { FUNCDESC="Look up an abbreviation, including obscene meanings"; wtf -o ${@} | sed 's/nothing appropriate/nothing inappropriate/' }
A few of things that I notice with this bash function:
- I keep a description of the function in
$FUNCDESC
- The
list
functiondescribe
s the function with it's$FUNCDESC
, and then prints the bash-tokenized definition of the function usingtype
- The functions arguments are available as
$1
,$2
, and so on, or${@}
for all of them strung together - Variables in bash can optionally be expanded using the
{}
brackets, which avoids expansion issues when values have spaces - I used
sed
(thes
treamed
itor) to substitute the error whichwtf
will print when it doesn't find a match, for my cute hack. Thes/something/else/
syntax is ased
command — derived fromed
— tos
ubstitute the first string between the/
s with the second string between the other/
s - The function begins with a redundant
()
to list arguments, and also the body block is surrounded by{
and}
which has overloaded, different meaning to the{}
for variable expansion
All of this is arcane, and is just a small taste of why I dislike bash, csh, zsh and so on. They use complicated syntax which dates from a time when it took a long time to print things out on paper terminals (which can't be easily edited, unlike a video terminal), and when computer cycles were precious, so command parsing had to be simple. It's clunky and awkward to use, difficult to read, and just plain unfriendly, and it's not clear what it actually means unless you're a Unix guru.
Fish, is the Friendly interactive shell. See how friendly it is with the fish version of my wtfo
function, listed using fish's built-in functions
function:
mjl@jazz ~/h/g/m/milosophical-me (fishing)> functions wtfo # Defined in /Users/mjl/.config/fish/functions/wtfo.fish @ line 1 function wtfo --wraps=wtf --description 'Look up an abbreviation, including obscene meanings' string replace 'nothing appropriate' 'nothing inappropriate' (wtf -o $argv) end
Here's what I notice about the fish function:
- Fish can tell you which file contains the function's definition
- The function's description is supplied as the argument to the
--description
option- this is used by fish during command completion, so it should be short, more on this later
- The
functions
function will list all functions, or just the specified one. When it lists, it includes the description as part of the definition - The function's arguments are available as
$argv
which is an array -- also no need to use the funky brakets because fish understands spaces in things - fish can do string manipulation with its built-in function
string
. The sub-commandreplace
will replace occurences of the first argument in the string, with the second argument - I've used command substitution (in the
()
parenthesis) for the call towtf
, passing it the function's arguments. I could have piped it like the original too. Note that the substitution syntax is different to bash's, and I like it: there's no dollar sign, because we're not doing anything with variables - The
--wraps=wtf
tells fish to use the same TAB-completions for the function's options as the command it wraps, which in turn are generated from thewtf
command'sman
-page
All the functionality (and more) which I added to bash, I get for free with fish, and with a nicer syntax that is much easier to read.
Directory changing aliases
In bash, I have these aliases:
alias ..='cd ..' alias ...='cd ../..' alias ....='cd ../../..' alias .....='cd ../../../..' alias ......='cd ../../../../..' alias .......='cd ../../../../../..' alias ........='cd ../../../../../../..' alias .........='cd ../../../../../../../..' alias ..........='cd ../../../../../../../../..' alias ...........='cd ../../../../../../../../../..'
They let me change up directories by just typing ..
to go up one dir, or ....
to go up three. I saw them at university, and thought they were neat (we used the C-shell on SunOS). Then when I became a Java developer they were essential. Now I'm just attached to them.
Fish doesn't actually have aliases. It has an alias
command that makes a function to wrap whatever you are aliasing. So the definitions aren't so compact as above. Here are a few of them (I made eight, maybe overkill...):
# Defined in /Users/mjl/.config/fish/functions/...fish @ line 1 function .. --description 'Change up 2 parent dirs' ../ end
# Defined in /Users/mjl/.config/fish/functions/....fish @ line 1 function ... --wraps=../../ --wraps=../../../ --description 'Change up 2 parent dirs' ../../ end
# Defined in /Users/mjl/.config/fish/functions/.....fish @ line 1 function .... --wraps=../../ --wraps=../../../../ --description 'Change up 3 parent dirs' ../../../ end
… and so on. All those --description
s seem verbose, but they buy me this TAB-completion:
mjl@jazz ~/h/g/m/milosophical-me (fishing)> .... .. (Change up 1 parent dir) ..... (Change up 4 parent dirs) ........ (Change up 7 parent dirs) ... (Change up 2 parent dirs) ...... (Change up 5 parent dirs) ../ (Directory, 128B) .... (Change up 3 parent dirs) ....... (Change up 6 parent dirs)
Also — and this is my favourite part — directories are executable! So you don't have to use cd
, just put a /
at the end! Finally the execute-permissions-bit has real meaning, not a double-meaning. This works for any directory, not just the ..
pointer to the parent.
Something more complex: ssh-pass
Here is my first go at a fish-version of ssh-pass
:
mjl@jazz ~/h/g/m/milosophical-me (fishing)> list ssh-pass # Defined in /Users/mjl/.config/fish/functions/ssh-pass.fish @ line 1 function ssh-pass --description 'Add specified SSH keys to the SSH Agent, from pass(1)' #Each key's passphrase is retrieved from the Unix password store (pass), and #given to ssh-add(1) via the SSH_ASKPASS mechanism. This relies upon the keys #having the same path names in both your key directory ($SSH_KEYDIR), and #your password store." if not set --query argv[1] error "$_: no SSH key specified." usage $_ "<key> [...]" return 1 end set --query DISPLAY; or set --export DISPLAY dummy pushd $SSH_KEYDIR; or return 2 set --local KEY for KEY in $argv set --export SSH_ASKPASS (mktemp -t ssh-askpassXXX) echo "\ #!/bin/sh pass ssh/$KEY|head -1 " > $SSH_ASKPASS chmod +x $SSH_ASKPASS ssh-add $SSH_KEYDIR/$KEY < /dev/null rm $SSH_ASKPASS end set --erase SSH_ASKPASS popd end
My observations:
- I made a small “alias” (function)
list
, to listfunctions
- The comments are included in the listing, which nice! I can have more extended comments to explain a function, and keep the
--description
short -
if not set --query argv[1]
, instead ofif test -z ${1}
- Clear
or
(andand
) functions, instead of weird special syntax||
and&&
- Fish doesn't have here-docs. Instead you can use multi-line values, which are even better! That's how the temporary askpass script is written
- No meaningless
$
for things which aren't variables, or{}
around things - In fish, use
$_
instead of bash's$FUNCNAME
, though this is deprecated in favor ofstatus current-command
, so I should make itset FUNCNAME (status current-command)
This function does not have shell completion for my keys, like my bash function does. I still need to look into that: for now I am happy to use fish's command suggestions that come from the shell history.
My ssh-pass
function uses two meta-functions that I also ported from bash:
mjl@jazz ~/h/g/m/milosophical-me (fishing)> list error # Defined in /Users/mjl/.config/fish/functions/error.fish @ line 1 function error --description 'Echo arguments to STDERR' isatty; and set_color red echo $argv 1>&2 isatty; and set_color normal end
and
mjl@jazz ~/h/g/m/milosophical-me (fishing)> list usage # Defined in /Users/mjl/.config/fish/functions/usage.fish @ line 1 function usage --description 'Print usage signature of a function' if not set --query argv[2] error "$_: missing function name and arguments" usage $_ "<function_name> <arguments_spec>" return 1 end isatty; and set_color yellow echo Usage: $argv isatty; and set_color normal echo Description: (describe $argv[1]) end
The best part about these is set_color
with colour names, instead of having to echo -e
some-ANSI-control-character sequence.
My usage
function uses my describe
function to pick out the function description:
mjl@jazz ~/h/g/m/milosophical-me (fishing)> list describe # Defined in /Users/mjl/.config/fish/functions/describe.fish @ line 1 function describe --description 'Print a function description' if not set --query argv[1] error "$_: no function specified" usage $_ "<function>" return 1 end functions $argv[1] | grep "\-\-description" | head -1 \ | string replace "function $argv[1] --description " "" end
Note that this uses grep
only because string match
works a line at a time. I could probably re-write this in a shell loop that escapes when --description
is found.
More fishing to be done
As you can tell by my enthusiasm, I am having such a lot of fun writing fish functions, and I think they look beautiful. I'm going to do some more, and then look into some of the extra fish tools for organizing and sharing my functions among my different computers.
Happy hacking!