Skip to content
Go back

My shell configuration

Updated:  at  12:58 PM
Welcome to my blog

My shell configuration

For years, I kept things simple, using the built-in macOS Terminal, with oh-my-zsh, and not much else. It worked well enough day-to-day.

Over time though, I have found a handful of small improvements that add up. Quicker navigation, better defaults, and a setup that’s easy to move across machines. This post is a brief overview of how I organise my shell configuration.

It’s not meant to be prescriptive, just a practical starting point if you’re thinking, “my .zshrc is getting a bit… long”.


Terminal emulator

I’ve hopped around a few terminal emulators over the years: the default macOS Terminal app, then Warp for a while, some experimenting with iTerm2, and finally landing on WezTerm.

WezTerm is a cross-platform terminal emulator (and multiplexer) written by @wez. It’s configured in Lua, which means you can make it as minimal or as customised as you like. The initial setup is a bit of a time investment, but I’ve found it’s paid off in day-to-day productivity.

The main things I value:

My wezterm.lua is here:

If you’re new to it, the official docs are the best place to start:


Shell

With the terminal itself sorted, the rest is mostly about how I structure my zsh config.

The guiding principle is simple:

Keep .zshrc small, and source everything else from a dedicated folder.

That way, instead of one giant file, I’ve got a small set of focused scripts (aliases, exports, functions, installs). They are easier to skim and update.

The sourcing loop

At a high level, my .zshrc uses a simple loop to source each script from ~/shell:

# Run scripts defined in ~/shell directory
for file in "$HOME"/shell/*.sh; do
  if [ -f "$file" ]; then
    source "$file"
  fi
done

Nothing groundbreaking, but it keeps things modular. Each file does one job, and you can add or remove pieces without worrying about breaking the whole thing.

Glob order is typically alphabetical. If you ever need a strict load order, prefix filenames with numbers (e.g. 00_exports.sh, 10_aliases.sh).

Folder structure

My shell configuration lives in a shell directory, roughly like this:

📁 shell/
├── 📁 aliases/
├── 📄 functions
├── 📄 installs
├── 📄 exports.sh
└── 📄 load.sh

(You can browse the real thing here: https://github.com/ThomasHepworth/PersonalDevTools/tree/main/shell-configs)

A breakdown of each component

📁 Aliases

The aliases directory contains multiple files, each covering a small category. For example, I keep Git aliases separate from Docker aliases, and both separate from general utilities.

That makes it easy to:

Aliases folder:

📁 Functions

The functions directory contains custom shell functions I use often. These are mostly small helpers: shortcuts for common directories, quick Git helpers, or little wrappers around tools I use a lot.

I prefer functions for anything that:

Functions folder:

📁 Installs

The installs directory is where I keep the “setup” side of things: installing or configuring tools and plugins. I cover some of these in more detail in:

This includes things like:

Keeping installs separate helps keep the day-to-day config readable, and it makes migrating to a new machine much faster: copy the directory, run installs, and you’re most of the way there.

Installs folder:

📄 Exports.sh

exports.sh is where I define environment variables: PATH additions, tool configuration flags, and anything I want available everywhere.

I keep this file boring on purpose. It should be safe to source repeatedly, and easy to scan.

Exports file:

📄 Load

Finally, load.sh is a bit of a “utility” script. It’s responsible for loading any extra config and also includes helpers that make my setup easier to understand.

One of the things it does is provide a quick way to list the functions or aliases defined inside a file.

For example, this helper scrapes definitions out of sourced scripts:

# Function to list contents of a script file based on type (aliases or functions)
function display_script_definitions() {
  local script_file="$1"
  local content_type="$2"  # either 'aliases' or 'functions'

  if [[ "$content_type" == "aliases" ]]; then
    echo "Aliases defined in $script_file:"
    grep '^alias' "$script_file"
  elif [[ "$content_type" == "functions" ]]; then
    echo "Functions defined in $script_file:"
    grep '^function ' "$script_file" | awk '{print $2}' | cut -d '(' -f 1
  else
    echo "Invalid content type specified. Please choose 'aliases' or 'functions'."
  fi
}

That lets me run simple commands (based on filenames) to quickly see what’s available. It’s handy when you’ve not looked at a particular alias file for a while and just want a quick reminder.

For example, any aliases listed within the file aliases_git.sh can be displayed with: aliases_git in my terminal:

Calling aliases_git to list git aliases in the terminal

This is intentionally lightweight. It’s “good enough” for my own conventions, rather than trying to build a perfect parser for every shell edge case. Often I simply need a nudge or quick list, rather than a full audit.

Load file:


This modular approach has served me well. It keeps things tidy, makes it easier to find and update specific settings, and means I can get a new machine into a comfortable state quickly.

If you want to take a look at the full setup, here are the key links:


Suggest Changes

Previous Post
Deterministic vs Probabilistic Record Linkage
Next Post
The Mythical Man-Month — Notes and Highlights