M
MeshWorld.
Zsh Oh My Zsh Terminal Shell CLI Productivity Developer Tools macOS Linux Configuration 9 min read

Zsh + Oh My Zsh: The Ultimate Terminal Setup Guide

Vishnu
By Vishnu

The default shell is fine for beginners. But if you type commands all day, you need a shell that works with you — not against you. Zsh with Oh My Zsh adds smart autocomplete, git status in your prompt, hundreds of plugins, and themes that make your terminal actually enjoyable to use.

:::note[TL;DR]

  • Zsh is the modern shell (default on macOS since Catalina)
  • Oh My Zsh is a framework with 300+ plugins and themes
  • Best theme: powerlevel10k (fast, customizable, informative)
  • Essential plugins: git, z, autosuggestions, syntax-highlighting
  • Use alias and functions to automate common tasks :::

Why Switch to Zsh?

FeatureBashZsh
Tab completionBasicSmart, menu-based, case-insensitive
Auto-suggestionsNoYes (with plugin)
Syntax highlightingNoYes (with plugin)
Git integrationManualBuilt-in, visual
Path expansion/u/l/b → error/u/l/b/usr/local/bin
Shared historyPer sessionAll sessions
GlobbingBasicAdvanced patterns

Zsh is now the default shell on macOS. If you’re still using bash, you’re working harder than you need to.

Installation

macOS

Zsh is pre-installed since macOS Catalina (10.15). Verify:

echo $SHELL
# Should show /bin/zsh

# If not, change it
chsh -s /bin/zsh

Linux

# Ubuntu/Debian
sudo apt update
sudo apt install zsh

# Fedora/RHEL
sudo dnf install zsh

# Arch
sudo pacman -S zsh

# Set as default
chsh -s $(which zsh)

Verify Installation

zsh --version
# Should be 5.8 or higher

Installing Oh My Zsh

# Via curl
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

# Via wget
sh -c "$(wget https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh -O -)"

This creates ~/.zshrc and sets up the framework. Restart your terminal or run source ~/.zshrc.

Powerlevel10k: The Best Theme

Most Oh My Zsh themes are slow or ugly. Powerlevel10k is neither.

Installation

# Clone the repository
git clone --depth=1 https://github.com/romkatv/powerlevel10k.git \
  ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/powerlevel10k

# Set theme in ~/.zshrc
ZSH_THEME="powerlevel10k/powerlevel10k"

Restart terminal, follow the configuration wizard:

p10k configure

Powerlevel10k Features

  • Instant prompt — Shell appears immediately
  • Git status — Branch, dirty state, ahead/behind
  • Command duration — Shows how long commands take
  • Error codes — Red prompt when last command failed
  • Background jobs — Indicator for running processes
  • Context aware — Different styles for SSH, root, etc.

Essential Plugins

Built-in Oh My Zsh Plugins

Enable in ~/.zshrc:

plugins=(
    git
    z
    colored-man-pages
    command-not-found
    history-substring-search
    safe-paste
    sudo
)

Must-Have External Plugins

zsh-autosuggestions

Suggests commands based on history as you type:

git clone https://github.com/zsh-users/zsh-autosuggestions \
  ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions

Add to plugins list:

plugins=(... zsh-autosuggestions)

zsh-syntax-highlighting

Highlights valid/invalid commands in real-time:

git clone https://github.com/zsh-users/zsh-syntax-highlighting.git \
  ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting

Add to plugins list (must be last):

plugins=(... zsh-syntax-highlighting)

z (Smart Directory Navigation)

Jump to frequently used directories:

z project      # Goes to ~/Projects/my-project (most frequent match)
z code src     # Goes to ~/Code/project/src

Already included with Oh My Zsh, just add z to plugins.

Custom Aliases

Add to ~/.zshrc after Oh My Zsh is loaded:

# Quick navigation
alias ..='cd ..'
alias ...='cd ../..'
alias ....='cd ../../..'
alias ~='cd ~'
alias -- -='cd -'

# List files
alias ls='ls -G'  # Colorized (macOS)
alias l='ls -lah'
alias la='ls -lAh'
alias ll='ls -lh'
alias lsa='ls -lah'

# Directory creation
alias mkdir='mkdir -p'

Git Shortcuts

Oh My Zsh provides many, but add your own:

alias g='git'
alias ga='git add'
alias gaa='git add --all'
alias gc='git commit -v'
alias gca='git commit -v -a'
alias gcam='git commit -a -m'
alias gcm='git commit -m'
alias gd='git diff'
alias gds='git diff --staged'
alias gf='git fetch'
alias gl='git pull'
alias gp='git push'
alias gco='git checkout'
alias gcb='git checkout -b'
alias gb='git branch'
alias gba='git branch -a'
alias gbd='git branch -d'
alias gst='git status'
alias glog='git log --oneline --decorate --graph'
alias glogs='git log --oneline --decorate --graph --all'

Development

# Package managers
alias nr='npm run'
alias ni='npm install'
alias nid='npm install --save-dev'
alias nrs='npm run start'
alias nrb='npm run build'
alias nrt='npm run test'

# Python
alias py='python3'
alias py2='python2'
alias pip='pip3'
alias venv='python3 -m venv venv'
alias activate='source venv/bin/activate'

# Docker
alias d='docker'
alias dc='docker compose'
alias dps='docker ps'
alias dpsa='docker ps -a'
alias di='docker images'
alias dex='docker exec -it'
alias dl='docker logs'
alias dlf='docker logs -f'

# Kubernetes
alias k='kubectl'
alias kg='kubectl get'
alias kd='kubectl describe'
alias kdel='kubectl delete'
alias ka='kubectl apply -f'
alias kgp='kubectl get pods'
alias kgs='kubectl get services'
alias kgn='kubectl get nodes'
alias kctx='kubectl config current-context'
alias kcns='kubectl config set-context --current --namespace'

# System
alias reload='source ~/.zshrc'
alias zshconfig='vim ~/.zshrc'
alias path='echo $PATH | tr ":" "\n"'
alias ports='lsof -i -P | grep LISTEN'
alias myip='curl -s https://ipinfo.io/ip'

Custom Functions

Add functions to ~/.zshrc:

Quick File Creation

# Create file and all parent directories
mkfile() {
    mkdir -p "$(dirname "$1")" && touch "$1"
}

# Create directory and cd into it
mkcd() {
    mkdir -p "$1" && cd "$1"
}

Development Helpers

# Find and replace in files
replace() {
    if [ $# -ne 3 ]; then
        echo "Usage: replace 'search' 'replace' file_pattern"
        return 1
    fi
    find . -type f -name "$3" -exec sed -i '' "s/$1/$2/g" {} +
}

# Extract any archive
extract() {
    if [ -f $1 ]; then
        case $1 in
            *.tar.bz2)   tar xjf $1   ;;
            *.tar.gz)    tar xzf $1   ;;
            *.bz2)       bunzip2 $1   ;;
            *.rar)       unrar x $1   ;;
            *.gz)        gunzip $1    ;;
            *.tar)       tar xf $1    ;;
            *.tbz2)      tar xjf $1   ;;
            *.tgz)       tar xzf $1   ;;
            *.zip)       unzip $1     ;;
            *.Z)         uncompress $1;;
            *.7z)        7z x $1      ;;
            *)           echo "'$1' cannot be extracted via extract()" ;;
        esac
    else
        echo "'\$1' is not a valid file"
    fi
}

# Start simple HTTP server
serve() {
    local port="${1:-8000}"
    python3 -m http.server "$port"
}

# Backup file with timestamp
backup() {
    cp "$1" "${1}.backup.$(date +%Y%m%d%H%M%S)"
}

Git Helpers

# Create feature branch from main
gfeat() {
    git checkout main && git pull && git checkout -b "feature/$1"
}

# Clean up merged branches
gclean() {
    git branch --merged | grep -v "\\*" | xargs -n 1 git branch -d
}

# Undo last commit (keep changes)
gundo() {
    git reset --soft HEAD~1
}

# Show commit count by author
gstats() {
    git shortlog -sn --no-merges
}

Complete .zshrc Template

# ==========================================
# Oh My Zsh Configuration
# ==========================================
export ZSH="$HOME/.oh-my-zsh"

# Theme
ZSH_THEME="powerlevel10k/powerlevel10k"

# Plugins
plugins=(
    git
    z
    colored-man-pages
    command-not-found
    history-substring-search
    safe-paste
    sudo
    zsh-autosuggestions
    zsh-syntax-highlighting
)

source $ZSH/oh-my-zsh.sh

# ==========================================
# User Configuration
# ==========================================

# Editor
export EDITOR='vim'

# Language
export LANG=en_US.UTF-8

# Paths
export PATH="/usr/local/bin:$PATH"
export PATH="$HOME/.local/bin:$PATH"

# History
HISTSIZE=10000
SAVEHIST=10000
HISTFILE=~/.zsh_history
setopt HIST_IGNORE_DUPS
setopt HIST_IGNORE_SPACE
setopt SHARE_HISTORY

# ==========================================
# Aliases
# ==========================================
# Navigation
alias ..='cd ..'
alias ...='cd ../..'
alias ~='cd ~'

# List files
alias l='ls -lah'
alias la='ls -lAh'
alias ll='ls -lh'

# Git
alias g='git'
alias gst='git status'
alias glog='git log --oneline --decorate --graph'

# Development
alias reload='source ~/.zshrc'
alias zshconfig='vim ~/.zshrc'

# ==========================================
# Functions
# ==========================================
mkcd() {
    mkdir -p "$1" && cd "$1"
}

serve() {
    python3 -m http.server "${1:-8000}"
}

# ==========================================
# Tool-specific Configuration
# ==========================================

# nvm (Node Version Manager)
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

# Pyenv
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"

# Rust (cargo)
[ -f ~/.cargo/env ] && source ~/.cargo/env

# ==========================================
# Custom (keep at bottom)
# ==========================================
[ -f ~/.zshrc.local ] && source ~/.zshrc.local

Useful Zsh Features

Globbing

# Recursive file matching
ls **/*.js          # All .js files in all subdirectories
ls **/*.ts(.)       # Regular files only (not symlinks)
ls **/*.(js|ts)     # Multiple extensions

# Qualifiers
ls -lh *(.Lm+100)   # Files larger than 100MB
ls *(m-7)           # Modified in last 7 days
ls *(@)             # Symlinks only
ls *(^@)            # Non-symlinks

# Modifiers
ls *.bak(:r)        # Remove .bak extension
ls *.txt(:e)        # Show only extension
ls *.txt(:h)        # Show only directory
ls *.txt(:t)        # Show only filename

Parameter Expansion

# Default values
${VAR:-default}     # Use default if VAR unset
${VAR:=default}     # Set VAR to default if unset
${VAR:?error}       # Show error if VAR unset

# String manipulation
${VAR#pattern}      # Remove shortest match from start
${VAR##pattern}     # Remove longest match from start
${VAR%pattern}      # Remove shortest match from end
${VAR%%pattern}     # Remove longest match from end
${VAR/pattern/rep}  # Replace first match
${VAR//pattern/rep} # Replace all matches

# Examples
file="document.txt"
echo ${file%.txt}.pdf     # document.pdf
path="/home/user/file"
echo ${path##*/}          # file

Completion System

# Case-insensitive completion
zstyle ':completion:*' matcher-list 'm:{a-zA-Z}={A-Za-z}'

# Fuzzy matching
zstyle ':completion:*' completer _complete _match _approximate
zstyle ':completion:*:match:*' original only

# Menu selection
zstyle ':completion:*' menu select

# Group results
zstyle ':completion:*' group-name ''

# Colorize completions
zstyle ':completion:*:default' list-colors ${(s.:.)LS_COLORS}

Troubleshooting

Slow Prompt

# If powerlevel10k is slow, enable instant prompt
# In ~/.p10k.zsh, uncomment:
# typeset -g POWERLEVEL9K_INSTANT_PROMPT=quiet

Plugins Not Loading

# Check for syntax errors
zsh -xv  # Verbose debugging

# Or test config
zsh -n ~/.zshrc  # Syntax check only

Right Prompt Issues

# Some terminals don't handle RPROMPT well
# In ~/.p10k.zsh:
# typeset -g POWERLEVEL9K_DISABLE_RPROMPT=true

Summary

  • Zsh — Modern shell with better defaults than bash
  • Oh My Zsh — Plugin framework (300+ plugins, themes)
  • Powerlevel10k — Fast, informative, customizable theme
  • Essential plugins — git, z, autosuggestions, syntax-highlighting
  • Aliases — Save keystrokes on commands you use 100x/day
  • Functions — Automate multi-step workflows

A well-configured shell is like a custom IDE — it fits your workflow and saves hours over time.