Benjamin Sago / ogham / cairnrefinery / etc…

Technical notes Handle Fish universal variables and abbrs across machines

A feature fish has that other shells do not is universal variables: variables that are automatically applied to other currently-running fish sessions. Fish uses them internally for its own configuration.

I think this is a really neat feature. When I used zsh, I’d occasionally edit one of my configuration files, only to find several hours or even days later that the update hadn’t been applied to one of my long-running existing shell sessions — and then I’d have to close whatever was going on in that terminal and re-open it to stay consistent.

The downside of this is feature is that it makes it slightly more awkward to keep the entirety of your fish configuration in a Git repo, or to synchronise it between machines. Universal variables are stored in a separate file, fish_variables, unlike the other files in fish’s configuration folder, is not intended to be human-edited.

It’s natural to define abbreviations and set universal variables in the fish_config file, in the same place as where aliases are defined and global variables are set. But there are some pitfalls that are easy to encounter when you try doing it this way:

  1. If you define an abbreviation in fish_config, and later remove the line that defines it, the abbr will live on in fish_variables until you remove it manually.
  2. If you append (-a) or prepend (-p) to an array universal variable, it will be added again every time you start a new shell, making it get longer and longer, invisibly slowing your shell down each time until it finally gets slow enough for you to realise what’s happening.

On this page, I evaluate some ways to deal with this.

Global abbreviations

Fish version 3.0 allows you to define global abbreviations, rather than universal ones, with the -g flag. Global abbreviations will only apply to the shell session they’re defined in, and will not be made available in other previously-running fish sessions.

This means that you can place these abbr definitions in your fish configuration file, and they will apply to only the session that they’re loading.

config.fish
abbr -ga cb  'cargo build'
abbr -ga cbr 'cargo build --release'
abbr -ga ct  'cargo test'
abbr -ga ck  'cargo check'
abbr -ga cr  'cargo run --'

The downside of this is that it’s quite slow. Defining 100 abbreviations in my configuration file was enough to bring my shell startup time from 9ms to 60ms, which is enough to be noticeable.

A separate setup stage

The solution I settled on was to split my fish configuration in two. I have the regular fish_config file along with my function definitions and variable overrides, but I also have a setup/ folder where everything that defines universal variables goes. The files in this folder are not touched during fish startup, so I get to keep my nice fast startup time, while maintaining the fact that everything about my setup is stored inside the same Git repository.

To activate the changes in the setup/ directory, I run this function that clears out all loaded universal variables and loads them anew:

functions/fresh-fish.fish
function fresh-fish -d "Resets fish’s universal variables"

    # delete all existing universal vars
    # <https://github.com/fish-shell/fish-shell/issues/4542#issuecomment-430848765>
    set -l existing_vars \
        (set --show \
         | string replace -rf '^\$([^:[]+).*: set in universal.*' '$1' \
         | grep -E '^_?fish_')

    for v in $existing_vars
        set -Ue $v
    end

    # load new universal vars
    source $__fish_config_dir/setup/cargo-abbrs.fish
    source $__fish_config_dir/setup/common-abbrs.fish
    source $__fish_config_dir/setup/git-abbrs.fish
    source $__fish_config_dir/setup/theme.fish

    echo "[All loaded.]"
end

Then, I define my aliases throughout the abbrs files, without needing to use -g:

setup/cargo-abbrs.fish
abbr -a cb  'cargo build'
abbr -a cbr 'cargo build --release'
abbr -a ct  'cargo test'
abbr -a ck  'cargo check'
abbr -a cr  'cargo run --'

I also define my fish theme in one of these files, because it too uses universal variables:

setup/theme.fish
set -U fish_color_autosuggestion brblack
set -U fish_color_command \x2d\x2dbold
set -U fish_color_comment red
set -U fish_color_end brmagenta
set -U fish_color_error brred
set -U fish_color_escape bryellow\x1e\x2d\x2dbold
set -U fish_color_operator bryellow
set -U fish_color_param cyan
set -U fish_color_quote yellow
set -U fish_color_redirection brblue

Now, whenever I change any of the files within the setup/ directory, I simply run the fresh-fish function defined above, and it re-configures my theme and abbreviations — and even applies the new versions to running sessions without me having to close and re-open them. The obvious downside to this is that you need to remember to do it, but I’ve been doing this for several months now and it hasn’t been an issue.