profile picture

Run Any Project with 'r'

July 28, 2025 - intermediate cli

This is an article about fish shell customisation. Fish is an alternative to bash, zsh, or powershell. It’s powerful as both an interactive shell, and a scripting language, and I’ll be demonstrating a little bit of both of these domains.

This article demonstrates the creation of an r abbreviation in fish, that will automatically expand to the correct “run” command, based on the files in your current directory. It might be of use even if you don’t use fish as your shell. Maybe it will convince you to try it out!

The Running Man

I work with a bunch of programming languages – primarily Python, Rust & JavaScript. Running these projects is all slightly different.

Here are some examples:

# Python:
uv run a_script_or_something.py

# Rust:
cargo run

# JavaScript:
node run dev

Something I’ve been doing for a few years now is to add a Justfile or a Makefile to each of my projects. They all follow the same convention, so if I run just run or make run, it will execute whichever command is appropriate for that particular project.

So that I don’t have to type just run over and over again, I created an alias. Now … fish doesn’t really have aliases, it has functions. The alias fish command just creates a function and puts it in the right place.

# ~/.config/fish/functions/jr.fish
function jr --wraps='just run' --description 'alias jr just run'
  just run $argv; 
end

This is fine, but when I checked the alias documentation, I saw the following sentence:

If you want to ease your interactive use, to save typing, consider using an abbreviation instead.

… so I had to check out abbr.

That’s when I got a bit carried away.

Lots of abbreviations

I immediately set up a bunch of abbreviations for running different types of projects:

abbr --add jr  'just run'
abbr --add mr  'make run'
abbr --add uvr 'uv run'
abbr --add cr  'cargo run'
abbr --add nr  'node run dev'

The great thing about abbr over alias is that whereas alias executes the command when you hit enter, abbr replaces the abbreviation with the command when you hit space. That means you can view and edit the command before you run it.

Abbreviations also come with some neat features, like being able to put the cursor in the right spot after expansion. That means abbr --add gcm --set-cursor 'git commit -m "%"' will expand to git commit -m "", with the cursor already between the two quotes, ready for you to write a commit message.

But the five abbreviations above are super-wasteful. They all “run”, but I need to decide which shortcut to type out, based on the kind of project I’m working on.

Wouldn’t it be great if I could just type r and have it automatically expanded to the correct command?

If I type ‘r’, run the project

Here’s a function I wrote to echo the appropriate command-line based on the available files and folders in my project.

function _expand_run -d "Expand the 'run' abbreviation into a project-specific run command."
    
    if test -e Justfile # Test for a Justfile in the current directory
        echo just run
    else if test -e Makefile
        echo make run
    else if test -e Cargo.toml
        echo cargo run
    else if test -e package.json
        echo npm run dev
    else if test -e .venv
        echo uv run
    end
end

This function assumes it is being run from the project’s top-level directory, i.e. the directory that contains a Justfile or a Cargo.toml file. (Don’t worry, I’ll come back to this later.)

Abbr allows you to attach a function to an abbreviation with the --function parameter, like this:

# When expanding 'r', run `_expand_run` and use whatever that prints out:
abbr --add r --function _expand_run

Now, if I type ‘r’ and hit space, if I’m in a directory with a Justfile, it expands to just run. If, instead, I’m in a directory containing a Cargo.toml file, it expands to cargo run.

When you’re deep in a project

The function above worked well for me for a while, but occasionally it … wouldn’t.

As I pointed out above, a flaw in the function is that it assumes it is being called from a project’s top-level directory. Most task runners don’t require this. I can run just run or cargo run from a deeper directory, and it doesn’t matter if the Justfile or Cargo.toml file is a directory or two above where I’m working, the command will find and run them as if my current working directory is the project directory.

If I try that with the expansion function above, it will get to the end of the if-else block without matching, echo nothing, and so the line goes blank. Eventually, I got around to writing a function that recursively searches parent directories for a file or directory with a given name.

It occurred to me later that I probably could have gotten an LLM to write the following for me, but like some kind of programmer, I wrote it by hand:

function project_file \
    -d "Find a file in parent directories"
    # Find a file in the specified directory (or PWD), or any parent directory.
    #
    # Usage:
    # project_file [-p START_DIRECTORY] [-v] FILENAME
    #
    # `project_file FILENAME` will look for 'FILENAME' in the PWD or any parent directory.
    # `project_file -p START_DIRECTORY FILENAME` will look for FILENAME in START_DIRECTORY,
    # and then all parents of START_DIRECTORY.
    #
    # If -v is provided, project_file will print the path of the found file.
    # The exit code is 0 if a file was found and 1 otherwise.

    argparse v/verbose 'p/path=' -- $argv
    or return

    set filename $argv[1]
    if test -z $filename
        echo "project_file: missing FILENAME" >&2
        return -2
    end

    if set -ql _flag_p # Use the -p value, if provided
        set p $(path resolve "$_flag_p")
    else
        # Default to the working directory
        set p "$PWD"
    end

    for i in (seq 1 64) # Avoiding an infinite loop
        if test $p = /
            # File wasn't found in any parent directory
            return 1
        else if test -e "$p/$filename"
            # File found. Return
            if set -ql _flag_v
                echo "$p/$filename"
            end
            return 0
        else
            # File not found here. Check in parent.
            set p $(path dirname $p)
        end
    end
    print "project_file: possible infinite loop. aborted" >&2
    return -1
end

Why isn’t this part of a standard library?

I then hooked this into my expansion function:

function _expand_run -d "Expand the 'run' abbreviation into a project-specific run command."
    if project_file Justfile  # Test if this dir or a parent contains Justfile
        echo just run
    else if project_file Makefile
        echo make run
    else if project_file Cargo.toml
        echo cargo run
    else if project_file package.json
        echo npm run dev
    else if project_file .venv
        echo uv run
    end
end

Summary

I’ve made my full script available as a gist. It includes b and t expansions for building and testing projects, too. Feel free to drop it into your own ~/.config/fish/conf.d/ directory. Don’t forget to start a new fish session to pick up the script. You might also want to edit the expansion functions to match your own tools and conventions, too.

I’ve found r for “run”, b for “build”, and t for “test” reduces cognitive load more than I would have expected, especially when switching between projects. Don’t expect your friends to be impressed, though – it’s so fast, they won’t see it unless you explain it to them!

It’s possible to rewrite these expansions as standalone functions or scripts, but I like the way abbreviations work as a smart autocomplete. I’ve also considered extending the expansion functions to parse the Justfile or package.json for available tasks. But I’ve decided to stop for now!

The abbr command can do a lot more than I’ve shown here. I highly recommend checking out the abbr docs to see what it can do. I’ve also become intrigued by the ability to call functions based on fish events, but I think I’ll save that for future experiments.

If you found this article useful, or you have suggestions, let me know on Mastodon or Bluesky!

Hire Me!

Did you like this article? Do you think I sound like the kind of person that may be suitable for your organisation?

I’m looking for work as a Senior Software Developer, either remote or local to Edinburgh. I have many years of Python and Rust experience (among other things) at all levels. I also have excellent communications skills! (Hopefully my writing here illustrates that point.)

You can find out more about me on this site, and you can contact me at judy@judy.co.uk for a copy of my CV.