Skip to main content
  1. Posts/

Modern CLI Tools That Replaced My Unix Classics

Nick Liu
Author
Nick Liu
Building infrastructure for Facebook Feed Ranking at Meta. Previously at Walmart, Twitter, AWS, and eBay. MS in Computer Science at Georgia Tech.
Table of Contents

I’ve been gradually replacing classic Unix tools with modern alternatives, mostly written in Rust

. After a year of daily use, these aren’t experiments anymore — they’re muscle memory.

The Replacements
#

Classic Modern Why
cat bat Syntax highlighting, line numbers, git integration
ls eza Icons, git status, tree view, color-coded
grep ripgrep 10x faster, respects .gitignore, smart case
find fd Simpler syntax, respects .gitignore, colored output
cd zoxide Learns your habits, fuzzy matching
sed sd Intuitive regex syntax, no escaping nightmare
du dust Visual directory size with a tree view
df duf Colorful, filterable disk usage
top btop Beautiful TUI with mouse support, per-core graphs
ps procs Colorized, searchable, tree view
history atuin Encrypted sync, full-text search, workspace filtering

Setting Up Aliases
#

In my .zshrc, I alias the classics to their replacements so the transition is invisible:

alias cat="bat"
alias vim="nvim"
alias vi="nvim"
alias top="btop"
alias du="dust"
alias df="duf"
alias ps="procs"

For eza, I use a Zinit plugin that configures it with sensible defaults:

zinit light z-shell/zsh-eza
# Gives me: ls, ll, la, lt all backed by eza with --git, --icons, --group-directories-first

Deep Dive: The Tools That Matter Most
#

bat — cat with Wings
#

bat isn’t just cat with colors. I use it as:

  • A pager for other tools (git diff, man pages)
  • A preview engine for FZF
  • A syntax highlighter for Atuin’s history preview
# Use bat as the FZF preview
export FZF_CTRL_T_OPTS="--preview 'bat -n --color=always --line-range :500 {}'"

ripgrep — The Search Engine
#

ripgrep is absurdly fast. My .ripgreprc configures smart defaults:

--smart-case        # Case-insensitive unless you use uppercase
--follow            # Follow symlinks
--hidden            # Search hidden files
--max-filesize=100M

I also define custom file type groups:

--type-add=web:*.{html,css,js,ts,jsx,tsx,vue,svelte}
--type-add=config:*.{json,yaml,yml,toml,ini,conf}
--type-add=shell:*.{sh,bash,zsh,fish}

Then I can search only web files: rg "useState" --type web.

Power move: rgf — ripgrep piped into FZF with a preview, opening results in Neovim.
rgf() {
  rg --color=always --line-number --no-heading "$@" |
    fzf --ansi --delimiter ':' \
        --preview 'bat --color=always {1} --highlight-line {2}' \
        --preview-window 'up,60%,border-bottom,+{2}+3/3,~3' |
    awk -F: '{print "+"$2, $1}' |
    xargs -r nvim
}

zoxide — cd That Learns
#

After using zoxide for a few months, I can’t go back. It learns which directories you visit most:

z work      # jumps to ~/workspace (most visited match)
z dot       # jumps to ~/dotfiles
zi          # interactive mode with FZF

I integrate it with FZF for even more power:

eval "$(zoxide init zsh --hook pwd)"

fd — find for Humans
#

# find hidden .env files, ignoring node_modules
find . -name ".env" -not -path "*/node_modules/*"
# same thing
fd .env

fd respects .gitignore by default and is the engine behind my FZF file finder:

export FZF_DEFAULT_COMMAND='fd --type f --strip-cwd-prefix --hidden --follow --exclude .git'

FZF: The Glue That Binds Everything
#

FZF isn’t a replacement — it’s an amplifier. It makes every other tool interactive.

fzf-tab: Tab Completion on Steroids
#

zinit light Aloxaf/fzf-tab

# Preview files and directories during tab completion
zstyle ':fzf-tab:complete:cd:*' fzf-preview 'eza -1 --color=always $realpath'
zstyle ':fzf-tab:complete:cat:*' fzf-preview 'bat --color=always $realpath'

Now when I type cd <TAB>, I get a fuzzy searchable list with directory previews. Same for cat, vim, and any other command.

Git Integration
#

# Fuzzy branch switcher
gb() {
  git branch --all --sort=-committerdate |
    fzf --preview 'git log --oneline --graph --color=always {1}' |
    sed 's/remotes\/origin\///' |
    xargs git checkout
}

Catppuccin Theme for FZF
#

Everything gets the same color treatment:

export FZF_DEFAULT_OPTS=" \
  --color=bg+:#313244,bg:#1e1e2e,spinner:#f5e0dc,hl:#f38ba8 \
  --color=fg:#cdd6f4,header:#f38ba8,info:#cba6f7,pointer:#f5e0dc \
  --color=marker:#b4befe,fg+:#cdd6f4,prompt:#cba6f7,hl+:#f38ba8 \
  --color=selected-bg:#45475a"

Atuin: Shell History Reimagined
#

Atuin replaces the basic shell history with a SQLite-backed, encrypted, cross-machine synced history.

Key features I rely on:

# ~/.config/atuin/config.toml
enter_accept = true          # Execute on Enter, not just select
keymap_mode = "auto"         # Respects my Zsh vi-mode
workspaces = true            # Filter by git repo when pressing Up
filter_mode_shell_up_key_binding = "workspace"
style = "compact"
show_preview = true
Killer feature: Workspace filtering. When I press Up in a git repo, I only see commands I ran in that repo. No more scrolling through unrelated history.

The secrets filter automatically strips AWS keys, GitHub tokens, and passwords from history:

secrets_filter = true
history_filter = [
  "^export (AWS|GITHUB|SLACK|TOKEN|SECRET|PASSWORD|KEY)",
]

Starship: The Prompt
#

Starship gives me a fast, informative prompt with minimal configuration. My setup uses Catppuccin Mocha colors with status-bar-style background segments:

  • OS icon + username
  • Current directory (truncated to 3 levels, with custom icons for ~/Documents, ~/Developer, etc.)
  • Git branch + status (ahead/behind/dirty indicators)
  • Active language versions (Python, Go, Rust, Node — only shown when relevant)
  • Command duration (only if > 45 seconds)

Installation
#

All of these tools are in my Brewfile — one brew bundle and you’re set.
# Modern CLI replacements
brew "bat"
brew "eza"
brew "ripgrep"
brew "fd"
brew "fzf"
brew "zoxide"
brew "sd"
brew "dust"
brew "duf"
brew "btop"
brew "procs"
brew "atuin"
brew "starship"

Worth the Switch?
#

Absolutely. The common thread across all these tools:

  1. Better defaults — they do what you want without flags
  2. Respect .gitignore — no more accidentally grepping node_modules
  3. Colored output — easier to scan visually
  4. Speed — most are written in Rust and noticeably faster on large codebases

The transition cost is low — alias the classics, and you’ll barely notice the switch. But you’ll definitely notice when you’re on a machine without them.

Full configs in my dotfiles repo:

Related

Managing Dotfiles Like a Pro with Yadm

Every developer eventually reaches the point where their configs become too valuable to lose. Here’s how I use yadm to manage my macOS dotfiles with automated testing, daily maintenance, and a pre-commit workflow that keeps everything in check. For me, the turning point was spending a weekend setting up a new MacBook and realizing I couldn’t reproduce my environment reliably. That’s when I started managing my dotfiles properly.