Fish Shell Tips & Tricks

7 min read Original article ↗

Contents

Introduction

I recently watched a video by Dreams of Code, which presents nice tips and tricks that can be used in the popular zsh shell. Since I use fish instead, I figured it would be a good opportunity to transfer some of those tips over, learn a bit myself, and hopefully help you as well.

This article will not describe the zsh tips (or only briefly); instead, I invite you to watch the video linked above. They have all been transposed to fish in the sections below (and some more).

Editing the Command Buffer

Similar to zsh’s edit-command-buffer widget (which must be bound manually), fish allows one to edit the command buffer in your configured $VISUAL or $EDITOR tool with the default keybinding alt + e  .

In the cast below, the buffer opens in helix, my modal editor of choice!

Undo and Redo Command Edits

When modifying the command buffer inline with shortcuts such as alt + backspace   (deleting a whole argument), it’s sometimes convenient to be able to undo the last modification.

fish has a default key assignment for this: ctrl + z  . You could have guessed that, right?

The redo command is bound to ctrl + r   by default, but since I use that to open atuin (a really awesome tool, if you don’t know!), I changed the binding:

!! for Last Command

In bash, there’s a convenient alias !! to reference the content of the previous command that was run. The same is available in zsh, but there is no equivalent (by default) in fish. However, it’s trivial to implement it with a custom function and associated abbreviation:

function last_history_item
  echo $history[1]
end
abbr -a !! --position anywhere --function last_history_item
 
A cartoon portrait of the author

The huge advantage of abbreviations is that they automatically expand when you hit the "space" or "enter" key, so they are not "blind" and allow you to inspect the command before committing. We'll go into more details about the abbr command in a later section of this article.

Prepend sudo

In the video by Dreams of Code, an annoyance mentioned is having to retype a command that was run without sudo but required it. The proposed solution is to type sudo !! to invoke the last command with sudo.

In fish, however, there’s a better way! The built-in shortcut alt + s   prepends the current command with sudo, doas, please, or run0, as available. This can even be combined with the previous tip by typing !! followed by alt + s   to re-run the last command. Alternatively, bringing up the last history item with the up arrow before hitting the shortcut also works.

Run Hook on Directory Navigation

zsh has chpwd(), a hook that runs some code any time the current directory is changed. A very similar thing can be done in fish with the following syntax:

function my_chpwd --on-variable PWD
  echo "Changed to $PWD"
end

The function runs any time the special $PWD environment variable changes. In general however, I prefer to rely on direnv for that purpose. That way, the commands to run are defined in a .envrc file which can be different for each directory.

Open Files Based on Extension

Another neat trick shown in the video is the ability to enter a filename (without a command before), and zsh applies some replacement template based on the file extension, allowing one to open the file with the desired binary automatically. The feature in question is called “suffix aliases”.

The best way to go about this is to create a function invoked when the entered command cannot be resolved. That’s what the special fish_command_not_found function does. Here, I chose to view text files with bat and open other files with different editors (those are just examples; in practice, you might want to add more extensions and programs).

function fish_command_not_found
  set -l filename $argv[1]
  if test -f $filename
    set -l ext (string split -r -m1 '.' -- $filename)[-1]
    switch $ext
      case rs js ts go py md txt
        bat $filename
      case json
        cat $filename | jaq
      case pdf
        open $filename
      case mp4 mkv avi
        vlc $filename
      case jpg png gif
        feh $filename
      case '*'
        __fish_default_command_not_found_handler $argv
    end
  else
    __fish_default_command_not_found_handler $argv
  end
end

First, the file extension is extracted from the filename, and then a different program is chosen depending on the extension. This works because entering a filename (without a command preceding it) doesn’t normally do anything in fish, resulting in a “command not found” error. We catch this error and add behavior in case we find that we have passed a valid file.

Unfortunately, I couldn’t find an easy way to have arbitrary path autocompletion in first position (command position). So the full name must be written by hand. Let me know if you find a way to enable completions for files without a command!

Abbreviations (for Commands, Arguments, Paths)

Abbreviations are one of the best features in fish. Unlike aliases, they expand to show their content, enabling customization of the command, adding parameters and so on.

Aliases can be forced to resolve only when they are in command position (first word, which is the default) or anywhere in the line as we’ll see below.

This enables some cool features inspired by the aforementioned video.

Pipe Suffixes

Since abbreviations can be usable anywhere in a command with --position anywhere, they can be used to add suffixes to commands to pipe their standard output or errors to /dev/null.

abbr -a NE --position anywhere -- "2>/dev/null"
abbr -a DN --position anywhere -- "> /dev/null"
abbr -a NUL --position anywhere -- ">/dev/null 2>&1"

Long Commands

Of course, long commands can be shortened nicely:

abbr -a gco -- "git checkout"
abbr -a gaa -- "git add --all"
abbr -a gba -- "git branch -a"

Frequently Used Directories

It can also be useful for bookmarks:

abbr -a ~pr -- "cd ~/my-projects/best-project"

However, much like Dreams of Code, I prefer to use zoxide to navigate directories, so much so that I have it aliased to cd.

Setting the Cursor Position

Even more powerful, it’s possible to indicate where the cursor should be positioned after expansion:

abbr -a gcam --set-cursor -- 'git add --all && git commit -am "%"'

The % symbol is the default marker for the cursor position (which is used if we don’t specify another marker with --set-cursor=MARKER).

Clearing the Screen (Keeping the Buffer)

Sometimes it’s useful to clear the screen while keeping whatever was already written in the command prompt. The default binding for this is ctrl + l  . In zsh, it seems this requires some widget scripting.

Copy and Paste

There are many options to copy and paste parts of the command buffer to the system clipboard or fish’s pasteboard, which is ominously called the Kill Ring.

The one I use the most is copying the whole content of the buffer into the system clipboard, which is easy enough with the ctrl + x   shortcut. To paste, a simple ctrl + v   does the trick.

ctrl + k  

puts everything from the cursor position until the end of the line into the Kill Ring, which can then be pasted with ctrl + y  

(the key stands for "yank", apparently a heritage of the Emacs edition mode). Since the pasteboard is a ring, we can put multiple things in there and then rotate between them in reverse order, after pasting, with alt + y  

(yank-pop).

Bonus: Directory History

As a bonus, I just found out about the directory history, which enables navigation to previously visited folders with the alt +    shortcut (when the command line is empty).

Similarly, one can go forward in the history with alt +   . A game-changer when switching between two projects frequently!

Conclusion

I hope you found something useful in this article. Thanks to Dreams of Code for inspiring this article. Until next time!