---
title: "Fish Shell Functions & Custom Commands Guide"
description: "How to create, edit, save, and autoload functions in Fish Shell. Covers event handlers, argument parsing, scope, and practical examples."
date: 2026-02-23
categories: ["vps"]
tags: ["fish-shell"]
---

import Notice from "@components/widgets/Notice.astro";
import Accordion from "@components/widgets/Accordion.astro";

Functions in Fish are how you build reusable commands. If you've used Bash functions before, the idea is the same, but Fish's implementation is cleaner — no curly braces, explicit argument handling through `$argv`, and a lazy-loading system that keeps startup fast.

I use functions for everything from quick shortcuts to project-specific tooling. This guide covers creating them, saving them permanently, and the Fish-specific features that make them more useful than Bash functions.

## Creating a basic function

```fish
function greet
    echo "Hello, $argv"
end
```

Run `greet World` and it prints `Hello, World`. The variable `$argv` contains all arguments passed to the function. You can access individual arguments with `$argv[1]`, `$argv[2]`, etc.

That function only lasts for the current shell session. Close the terminal and it's gone. I'll cover how to make functions permanent in a moment.

### Functions with named arguments

Fish doesn't have named parameters the way Python does, but `--argument-names` gives you something close:

```fish
function mkcd --argument-names dir
    mkdir -p $dir
    cd $dir
end
```

Now `mkcd projects/new-app` creates the directory and moves into it. The first argument gets assigned to `$dir`. Extra arguments are still available through `$argv`.

You can list multiple argument names:

```fish
function connect --argument-names host port
    ssh -p $port $host
end
```

### Adding a description

```fish
function ll --description "List files in long format"
    ls -la $argv
end
```

The description shows up when you run `functions` or `type ll`. It's documentation for future-you.

## Saving functions permanently

Fish has three ways to keep functions across sessions.

### Method 1: funcsave (the Fish way)

Create a function interactively, then save it:

```fish
function weather --argument-names city
    curl "wttr.in/$city?format=3"
end

funcsave weather
```

This saves the function to `~/.config/fish/functions/weather.fish`. Fish automatically loads it when you use the command in any future session — not at startup, but on demand. This lazy loading is why Fish stays fast regardless of how many saved functions you have.

### Method 2: Create the file directly

Write the function file yourself:

```fish
# ~/.config/fish/functions/weather.fish
function weather --argument-names city
    curl "wttr.in/$city?format=3"
end
```

Same result as `funcsave`. I prefer this method for functions I want to version-control with my dotfiles.

### Method 3: Put it in config.fish

```fish
# ~/.config/fish/config.fish
function weather --argument-names city
    curl "wttr.in/$city?format=3"
end
```

This works but has two downsides: the function loads on every shell startup (not lazily), and your config.fish gets longer. Use autoloading files for anything beyond trivial functions.

<Notice type="info" title="Autoloading rules">
For autoloading to work, the file name must match the function name. A function called `weather` must be in `weather.fish`. If the file contains multiple functions, only the one matching the filename will autoload.
</Notice>

## Editing functions

Fish has a built-in function editor:

```fish
funced weather
```

This opens the function in `$EDITOR` (or `$VISUAL`). When you save and close the editor, Fish loads the updated function into your current session. You can then persist it with `funcsave weather`.

To see a function's current definition without editing:

```fish
functions weather
# or
type weather
```

## Practical function examples

### Git shortcut with defaults

```fish
function gc --description "Git commit with message"
    if test (count $argv) -eq 0
        echo "Usage: gc <message>"
        return 1
    end
    git add --all
    git commit -m "$argv"
end
```

Usage: `gc "fix login redirect"` stages everything and commits with that message.

### Quick project switcher

```fish
function proj --argument-names name
    set -l base ~/projects
    if test -z "$name"
        ls $base
        return
    end
    if test -d $base/$name
        cd $base/$name
    else
        echo "Project '$name' not found in $base"
        return 1
    end
end
```

Run `proj` to list projects, or `proj myapp` to jump to `~/projects/myapp`. Add tab completions for it too:

```fish
# ~/.config/fish/completions/proj.fish
complete -c proj -f -a "(ls ~/projects)"
```

Now `proj` followed by Tab lists your project directories. See my [autocomplete guide](/fish-shell-autocomplete-suggestions/) for more on writing completions.

### Backup with timestamp

```fish
function bak --argument-names file
    if test -z "$file"
        echo "Usage: bak <file>"
        return 1
    end
    cp $file $file.bak.(date +%Y%m%d-%H%M%S)
end
```

`bak config.yaml` creates `config.yaml.bak.20260224-141500`.

### Docker cleanup

```fish
function docker-clean --description "Remove stopped containers, dangling images, unused volumes"
    echo "Removing stopped containers..."
    docker container prune -f
    echo "Removing dangling images..."
    docker image prune -f
    echo "Removing unused volumes..."
    docker volume prune -f
end
```

## Event handlers

Functions can respond to events. This is useful for running code when variables change, when a command finishes, or when Fish exits.

### Run code when a variable changes

```fish
function __on_pwd_change --on-variable PWD
    if test -f .node-version
        echo "Node version: "(cat .node-version)
    end
end
```

Every time you change directories, this checks for a `.node-version` file. The `--on-variable PWD` flag triggers the function whenever `$PWD` changes.

### Run code on Fish exit

```fish
function __on_exit --on-event fish_exit
    echo "Goodbye!"
end
```

### Run code after a command finishes

```fish
function __notify_long_command --on-event fish_postexec
    if test $CMD_DURATION -gt 10000
        echo "Command took "(math $CMD_DURATION / 1000)" seconds"
    end
end
```

This prints a notice when any command takes more than 10 seconds. `$CMD_DURATION` is a special Fish variable that holds the last command's execution time in milliseconds.

<Notice type="warning" title="Event handler naming">
Event handler functions should start with double underscores or a unique prefix to avoid name collisions. Also, event handlers in autoloaded files won't trigger until the function has been loaded once. For handlers that need to work from the start, put them in `config.fish` or `conf.d/`.
</Notice>

## Scope and variable visibility

Functions have their own local scope by default. Variables set with `set -l` inside a function aren't visible outside it:

```fish
function test_scope
    set -l secret "hidden"
    echo $secret
end
test_scope  # prints "hidden"
echo $secret  # prints nothing
```

Use `set -g` for global variables (visible everywhere in the session) or `set -U` for universal variables (persist across all Fish sessions):

```fish
set -g session_var "I last until you close this terminal"
set -U persistent_var "I survive restarts"
```

## Functions vs abbreviations vs aliases

Fish has three ways to create shortcuts. Here's when to use each:

| | Functions | Abbreviations | Aliases |
|---|---|---|---|
| Best for | Complex logic, multi-line commands | Simple command shortcuts | Simple command wrapping |
| Expansion | No expansion, runs as-is | Expands on command line before running | Wraps as a function internally |
| History shows | Function name | Expanded command | Alias name |
| Arguments | Full `$argv` handling | Limited (position/regex-based) | Pass-through `$argv` |

I use abbreviations for simple shortcuts (`gs` → `git status`), and functions for anything that needs logic. I covered abbreviations vs aliases in detail in [Fish Shell abbreviations vs aliases](/fish-shell-abbreviations-vs-aliases/).

## Managing functions

```fish
functions                    # list all defined functions
functions -n                 # list function names only
functions weather            # show a function's definition
functions -e weather         # erase a function
funcsave weather             # save to autoload file
funced weather               # edit in $EDITOR
```

To delete a saved function permanently:

```fish
functions -e weather
funcsave weather
```

The second command writes the "erased" state, removing the file from `~/.config/fish/functions/`.

## Related guides

- [Fish Shell autocomplete and suggestions](/fish-shell-autocomplete-suggestions/) — writing custom completions for your functions
- [Fish Shell abbreviations vs aliases](/fish-shell-abbreviations-vs-aliases/) — when to use each
- [Best Fish Shell plugins](/best-fish-shell-plugins/) — extend Fish with Fisher and community plugins
- [Fish Shell on macOS](/fish-shell-macos-setup/) — if you're setting up Fish on a Mac
- [Install Fish Shell on Ubuntu](/install-fish-shell-ubuntu/) — getting started on Linux