FrankWiles.com

My CLI World

When I was first starting out, I spent 60+% percent of my time ssh’ed into a remote server. So I refrained from heavily customizing my own shell because those customizations were difficult, if not impossible, to have consistently across all of my servers and situations.

Due to this habit it wasn’t until the last couple of years I realized… I don’t really ssh anymore!

Ok, to be clear, yes I still ssh into the occasional server but now 99% of my time in a shell is local on MY laptop.

So I can customize the hell out of it!

I’m going to walk you through a couple of workflows, some zsh bits, and some CLI tools I use daily. These do a few things for me:

Together these save me hours of frustration and time every week.

My shell prompt

I have to admit, I’ve never been one to heavily customize my prompt and I still don’t do much, but I have installed and really like Starship. It’s fast as it’s written in Rust, but nicely customizable to show you useful information such as the Python version in use on a per-directory basis.

Screenshot of my Starship prompt

This is my prompt when working on this blog. It’s showing the directory we’re in which is frankwiles.com it’s letting me know we’re on the git branch main and that we have untracked files. The next version number v0.0.1 isn’t useful here because it’s the “version” of my AstroJS blog package itself which will forever remain there.

The next couple of versions however are somewhat useful. It’s not SUPER clear but that icon is a dumpling or bun so Starship is letting me know that I’m using bun version 1.2.7. and I have a little Typer based CLI tool I use to create new draft posts and other blog maintenance tasks and this virtual env happens to be on Python v3.11.1.

It’s a nice to have, but honestly the weakest recommenation I’ll be making in this post. If you’re happy with your prompt, skip this one.

Starting work on something…

The most frequently used, and thus most useful, workflow hack I have set up uses a combination of direnv and zoxide.

No matter if the next task for me is dev or ops related, my first step is to move into that project folder.

If my project is in ~/src/django-test-plus I could obviously type cd ~/src/django-test-plus/. zoxide helps me track my common folders and fuzzy matches on them to quickly get me where I’m going. I just tested and I’m able to instead type z django-test-plus or z django-test and it takes me there.

Once I’m in the right place, direnv does a few things for me:

This replaces a similar setup I had before which used virtualenv-wrapper to do basically the same thing but wasted some disk space with an empty venv sometimes and was a bit more cumbersome to set up so I didn’t do it 100% of the time.

For a dev project, I might be setting env vars to determine which S3 bucket some data is in. I might need to tie this project to a specific version of Tailwind CLI or and old version of some other tool.

With ops, I set up a specific ops folder per customer or customer environment. I can type z <customer>-ops and a half dozen or more env vars are set to give me access to their AWS, GCP, or other cloud provider and with the specific CLI tool versions I need for them.

This pattern ALSO gives me a spot to drop a Justfile so I can curate a list of just recipes for common tasks for THIS client.

To show you an example, here is what happens when I move to my Politifact ops project:

Screenshot of using zoxide

This sets a env var to tell the gcloud command which GCP project I’m using and sets a client specific KUBECONFIG for their k8s clusters. I’ve also got a special bin dir here to use a specific version of FluxCD’s CLi tool flux.

Finding stuff…

We all have a ton of files. I just checked and in my ~/src folder, where I keep most projects, and there are a little over 1 million individual files.

I use these three tools heavily when looking for stuff. The first, fd is a better version of find.

Instead of having to type something like find . -name cli.py you can type fd cli.py. It’s faster and automatically ignores files in your .gitignore.

I also need to find stuff IN my files of code. I use a mixture of ripgrep and ack, but mostly I use ack.

ack foo finds me all instances of the string foo in all the files in this folder and below. I can constrain this to just Python files with ack --python foo.

I also have a shell command ackopen which will open all of the matched files as individual buffers in NeoVim below FYI.

I bet I type ack <something> 50+ times a day.

Shell History

I’ve recently switched to using Atuin to manage my shell history. It’s customizable and super fast. This allows me to just type the beginning of a command and it searches through all my history for commands that begin with that which cuts down on frequent things that aren’t quite frequent enough to make it into a Justfile.

Here is a screenshot of hitting “up arrow” (well really esc k since I rock vim mode in my shell):

Screenshot example of using Atuin shell history

If you stare at this closely you’ll see some of the commands I’ve had to type as I’ve written this post for example, like moving screenshots from my Desktop folder to this posts’ folder.

Custom Aliases

My most frequently used alias is r, it moves me back to the root of THIS git repo. It’s simple but super useful:

zsh
# Go to root of this git repo
alias r='cd $(git root)'

UPDATE: So it turns out git root doesn’t come with git, I’ve actually been getting it from git-extras in homebrew, and didn’t know it! It is just this:

zsh
git rev-parse --show-toplevel

Which you can make global with git config --global alias.root 'rev-parse --show-toplevel'. Thanks to Adam Johnson and Ned Batchelder for both pointing this out to me after I published this! Ok back to more aliases…

I also often want to look for files from the repo of this repo, so I have rd (a pun of fd) that does it for me:

zsh
# Search for files from the git root
function rd() {
    fd "$1" $(git root)
}

As I mentioned above, I’m often looking for content in various source files so have ackopen which uses ack to search through files and then opens all of the matching files in NeoVim.

zsh
# Open files with ack
ackopen() {
  nvim $(ack -l "$@")
}

One thing I like about ack is that it listens to your .gitignore, but sometimes that gets in the way because what I’m actually looking for I also don’t want in my repo. Ack supports doing this, but I can’t be bothered to memorize that so I have fullack which remembers for me.

zsh
# Search all files regardless of .gitignore etc 
fullack() {
  ack --noenv $@
}

I also never rememember the right Finder steps to tell OSX that I’m ok with it executing this file I downloaded so I have:

zsh
appleallow() {
  xattr -d com.apple.quarantine $@
}

Two other useful aliases, pulllog and pulldiff which help me see what I just pulled in with git which I wrote about a couple of months ago.

I’m exclusively using uv these days, so I added this to my ~/.config/direnv/direnvrc to automatically activate it if it exists or create it if it doesn’t yet exist:

zsh
layout_uv() {
    if [[ -d "venv" ]]; then
        VIRTUAL_ENV="$(pwd)/venv"
    fi

    if [[ -z $VIRTUAL_ENV || ! -d $VIRTUAL_ENV ]]; then
        log_status "No virtual environment exists. Executing \`uv venv\` to create one."
        uv venv venv
        VIRTUAL_ENV="$(pwd)/venv"
    fi

    PATH_add "$VIRTUAL_ENV/bin"
    export UV_ACTIVE=1  # or VENV_ACTIVE=1
    export VIRTUAL_ENV
}

Now I just add layout_uv to the top of my .envrc files when I want this behavior, which is almost always FYI.

Your Mission

The main take away from this blog post should be the desire to heavily customize your environments.

Even if some of my little tools don’t resonate for your particular working style, they hopefully have sparked a idea that would simplify something for you. So go write it!

Go write that just recipe you’ve been meaning to do.

Wrap that one tool’s cumbersome command line options with a little alias.

Craft a little shell function to handle that one situation you find yourself in once a week or month.

What common every day annoyance can you remove?

With AI you really have no excuse anymore.

Other Resources

Headshot of Frank Wiles

Frank Wiles

Founder of REVSYS and former President of the Django Software Foundation . Expert in building, scaling and maintaining complex web applications. Want to reach out? Contact me here or use the social links below.