Selective fish function syncing with funcstore

Effortlessly managing my fish shell functions in git by selectively saving them with a new funcstore command.

Introduction

I keep my fish config synced between multiple machines. The configuration is stored in a GitHub repository, the concept known as dotfiles.

Problem

Fish shell lets you create a function on the fly and save it using one command (funcsave), which makes it available across other sessions (including future ones). The functions defined this way are stored in ~/.config/fish/functions/.

For fish, I symlink the whole ~/.config/fish/ to ~/.dotfiles/fish/, which by default would result in all my funcsaved functions being seen by git. This is not desirable since my machines have always ended up with some custom functions that I didn’t want synced.

Solution

I recently came up with a solution which is as easy to use as the built-in funcsave. In fact, it’s based on the function -> funcsave workflow.

First, I added ~/.dotfiles/fish/functions/ to my .gitignore. This way functions I define on the fly aren’t seen by git by default. Then, I instructed fish to look for functions in an additional place: ~/.config/fish/stored-functions/. I only did this for interactive sessions since I never use fish for scripting.

For convenience, I also created ~/.config/fish/stored-completions/ so I could have intelligent auto-completion for some of the saved functions.

# ~/.config/fish/config.fish
if status is-interactive
    # Commands to run in interactive sessions can go here
    set --prepend fish_function_path ~/.config/fish/stored-functions
    set --prepend fish_complete_path ~/.config/fish/stored-completions
end

Next, I created a funcstore function. The concept is to enhance the established function -> funcsave workflow in fish with a new final step: marking a function as one I want stored across machines. It moves an existing fish function defined on the fly to the newly created ~/.config/fish/stored-functions/ directory.

With the function -> funcsave -> funcstore workflow, only selected functions are visible by git instead of this being the case for all functions by default.

# ~/.config/fish/stored-functions/funcstore.fish
function funcstore
    if test (count $argv) -ne 1
        echo "Usage: funcstore <function_name>"
        echo "Moves a fish function to the stored-functions directory."
        return 1
    end
    set fname $argv[1]
    set src_path ~/.config/fish/functions/$fname.fish
    set dest_path ~/.config/fish/stored-functions/$fname.fish
    if not test -f $src_path
        echo "Error: Function '$fname' does not exist in ~/.config/fish/functions/"
        return 1
    end
    mv $src_path $dest_path
    echo "funcstore: moved $src_path to $dest_path"
end

Finally, I created a completion function that suggests the functions I might want to store upon pressing tab when my prompt says funcstore.

# ~/.config/fish/stored-completions/funcstore.fish
function __fish_complete_funcstore
    set -l func_dir ~/.config/fish/functions/
    for file in $func_dir*.fish
        echo (basename $file .fish)
    end
end
complete -c funcstore -f -a "(__fish_complete_funcstore)" -d "Move a function to stored-functions"

Example

Let’s say I wrote a useful function that lets me SSH to a machine and create (or attach to an existing) tmux session with fish as $SHELL. I called it ssht.

See how the workflow looked like:

> function ssht
      ssh -t $argv "env SHELL=`which fish` tmux new-session -A -s mbiel"
  end

> # The above created the function visible in this shell session only
> funcsave ssht
funcsave: wrote /home/mbiel/.config/fish/functions/ssht.fish

> # Now the function has been persisted on disk and is available in other sessions on this machine,
> # but still invisible to the git repository keeping track of my ~/.config/fish/
> # This is great if I don't want to track it with my other dotfiles
> funcstore ssht
funcstore: moved /home/mbiel/.config/fish/functions/ssht.fish to /home/mbiel/.config/fish/stored-functions/ssht.fish

> # Finally, the file with function definition has been moved to a directory which is not ignored by git
> # and I can stage and commit it.