Shell Aliases and Functions Every Rails Dev Should Have
You type bundle exec rspec spec/models/ maybe forty times a day. Over a year that’s a few hours of pure keystrokes, and more importantly it’s a few hours of tiny frictions that discourage you from running the tests as often as you should. The point of a good shell setup isn’t saving typing — it’s making the right action cheap enough that you stop hesitating before it.
Start With What You Actually Type
Before copying anyone’s dotfiles, look at your own history. The data is right there.
Example:
history \
| awk '{ $1=""; print substr($0, 2) }' \
| sort | uniq -c | sort -rn | head -30
That gives you the thirty commands you run most. Alias those. A collection of clever aliases for commands you never use is just clutter you have to remember.
For multi-word prefixes, which is where Rails work concentrates:
history | awk '{ print $2, $3, $4 }' | sort | uniq -c | sort -rn | head -20
The Core Rails Set
Example:
# ~/.zshrc
alias be="bundle exec"
alias ber="bundle exec rspec"
alias bi="bundle install"
alias bu="bundle update"
alias rc="bin/rails console"
alias rs="bin/rails server"
alias rdm="bin/rails db:migrate"
alias rdr="bin/rails db:rollback"
alias rds="bin/rails db:seed"
alias rr="bin/rails routes"
be alone is worth the whole file. Everything else composes with it — be rubocop, be sidekiq, be rake stats.
Routes, but searchable
bin/rails routes prints four hundred lines. You want one.
rg() { bin/rails routes | grep -i "$1"; }
Or use the built-in grep flag, which is faster because it filters before formatting:
alias rrg="bin/rails routes -g"
Migration status at a glance
alias rdms="bin/rails db:migrate:status | grep -v '^\s*up'"
Shows only pending migrations, which is the only part you ever care about.
Functions Beat Aliases When Arguments Are Involved
An alias appends arguments at the end. That’s fine for be, useless when the argument needs to go in the middle.
Example:
# Run one spec file, or one example by line number
t() {
if [ -z "$1" ]; then
bundle exec rspec
else
bundle exec rspec "$1"
fi
}
# Usage: t spec/models/order_spec.rb:42
Example:
# Rails console in any environment, defaulting to development
rce() {
bin/rails console -e "${1:-development}"
}
Example:
# Generate a migration and immediately open it
mig() {
local out
out=$(bin/rails generate migration "$@")
echo "$out"
local file
file=$(echo "$out" | grep -oE 'db/migrate/\S+\.rb' | head -1)
[ -n "$file" ] && ${EDITOR:-vim} "$file"
}
That last one removes the “now find the file it just made” step, which is small and constant and exactly the kind of thing worth automating.
Git, Where the Real Volume Is
Example:
alias gs="git status --short --branch"
alias gd="git diff"
alias gds="git diff --staged"
alias gco="git checkout"
alias gcb="git checkout -b"
alias gp="git push"
alias gpl="git pull --rebase"
alias gl="git log --oneline --graph --decorate -20"
git status --short is a meaningful upgrade over the default — the full output is a tutorial aimed at people who’ve never used git, and you’ve read it ten thousand times.
Branch switching without typing branch names
gb() {
local branch
branch=$(git branch --sort=-committerdate | fzf | tr -d ' *')
[ -n "$branch" ] && git checkout "$branch"
}
Requires fzf, which is the single highest-return tool you can add to a shell. Sorted by most recent commit, so the branch you want is almost always in the top three.
Clean up merged branches
gclean() {
git branch --merged main | grep -vE '^\*|main|develop' | xargs -r git branch -d
}
Read that carefully before using it — it deletes local branches already merged into main. The grep -vE guard protects the current branch and the two long-lived ones. Test with echo in place of git branch -d the first time.
Project Navigation
alias ..="cd .."
alias ...="cd ../.."
# Jump to the repo root from anywhere inside it
alias cdr='cd "$(git rev-parse --show-toplevel)"'
cdr is deceptively useful. Rails commands mostly need to run from the root, and you’re usually four directories deep in app/services/billing/.
Log and Process Wrangling
Example:
alias tlog="tail -f log/development.log"
# Tail the log but strip the asset and SQL noise
tlogc() {
tail -f log/development.log \
| grep -vE 'ActiveRecord::SchemaMigration|assets|CACHE'
}
# Kill the server that didn't shut down cleanly
alias killrails="kill -9 \$(cat tmp/pids/server.pid) 2>/dev/null || echo 'no pidfile'"
Example:
# What's on port 3000?
port() {
lsof -nP -iTCP:"${1:-3000}" -sTCP:LISTEN
}
You will use port 3000 more than you expect.
Making Them Discoverable
The failure mode of a large alias file is forgetting what’s in it. Two habits fix this.
Keep them in one file, grouped and commented, sourced from your shell config:
# ~/.zshrc
[ -f ~/.dev_aliases ] && source ~/.dev_aliases
Add a lookup:
aliases() {
grep -E "^(alias |[a-z_]+\(\))" ~/.dev_aliases | grep -i "${1:-}"
}
aliases git prints everything git-related. Thirty seconds to write, and it’s the difference between an alias file you use and one you rewrite every two years because you forgot what was in it.
Pro-Tip: Resist aliasing anything destructive to something short.
alias drop="bin/rails db:drop"will eventually be typed in the wrong terminal tab, and the tab you thought was staging was production. The aliases worth having are the ones you run constantly and that fail safely —gs,be,t,cdr. For anything that deletes data, drops a database, force-pushes, or touches a remote environment, keep the full command. The two seconds of typing are a deliberate speed bump, and the one time it makes you pause and reread what you’re about to run, it pays for every other time.
Portability and Sharing
Keep your aliases in a dotfiles repo, symlinked into place. It makes a new machine a five-minute setup rather than a week of “oh right, I had a thing for that.”
Be careful about sharing project-specific aliases with a team through their shell config — a function that assumes bin/rails exists breaks on a non-Rails repo. Project-scoped tooling belongs in the repo itself, as rake tasks or scripts in bin/, where it’s version-controlled alongside the code it operates on and everyone gets it automatically.
Conclusion
Build the set from your own history rather than someone else’s dotfiles — the aliases that matter are the commands you already type. Use functions when arguments need positioning, keep everything in one greppable file with a lookup command, and deliberately leave destructive operations un-aliased. The goal isn’t to type less; it’s to remove the small hesitations that keep you from running the tests, checking the routes, or reading the log as often as you should.
FAQs
Q1: Aliases or functions — how do I choose?
If arguments always go at the end, use an alias. If you need arguments in the middle, defaults, conditionals, or multiple commands, use a function. Functions are strictly more capable and only marginally more verbose.
Q2: Why do my aliases work in the terminal but not in scripts?
Aliases aren’t expanded in non-interactive shells by default. Use functions, or define the alias inside the script and enable shopt -s expand_aliases in bash. In general, scripts should call the real commands.
Q3: Should I alias over real commands, like alias ls="eza"?
It’s convenient, and it breaks muscle memory on any machine without your dotfiles — including production servers. Shadowing common commands is a personal call; shadowing them with something that takes different flags is where it gets painful.
Q4: How do I see what a name resolves to?
type gs in zsh or bash tells you whether it’s an alias, function, or binary and shows the definition. which is less reliable for this since behaviour varies by shell.
Q5: Where should these live — .zshrc or .zprofile?
.zshrc runs for every interactive shell, which is what you want for aliases and functions. .zprofile runs once at login and is for environment variables and PATH setup.
Check viewARU - Brand Newsletter!
Newsletter to DEVs by DEVs - boost your Personal Brand & career! 🚀