macOS Terminal Series · Vol. 3

.ZSHRC НА PRO НИВО

Не "top 10 aliases от Reddit". Реален sysadmin конфиг — functions, которые пестят часове, prompt, разчитащ се на 1 поглед, и оптимизации за старт под 100ms.

zsh 5.9+
macOS 13+
без oh-my-zsh bloat
copy-paste ready
~280 реда config

Преди да копираш

Структура, инструменти и философия — защо повечето .zshrc конфиги са бавни и object.

📁 Структура
Добрата практика: раздели конфига на файлове. ~/.zshrc само source-ва останалите. ~/.zsh/aliases.zsh, ~/.zsh/functions.zsh, ~/.zsh/exports.zsh. Редактираш само релевантния файл, без да разравяш 500 реда.
~/.zshrc — skeleton структура
# ── Performance: measure startup time
# zsh -i -c exit  (виж колко ms отнема)

# ── Path — само веднъж, правилно
export PATH="/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin"
export PATH="$HOME/.local/bin:$PATH"

# ── Editor
export EDITOR='nvim'
export VISUAL='nvim'

# ── History — увеличи лимита
export HISTSIZE=100000
export SAVEHIST=100000
export HISTFILE="$HOME/.zsh_history"
setopt HIST_IGNORE_DUPS
setopt HIST_IGNORE_SPACE    # команди с интервал отпред не се пазят
setopt SHARE_HISTORY        # споделена history между терминали
setopt EXTENDED_HISTORY     # пази timestamp

# ── Load modules
source "$HOME/.zsh/exports.zsh"
source "$HOME/.zsh/aliases.zsh"
source "$HOME/.zsh/functions.zsh"
source "$HOME/.zsh/prompt.zsh"

# ── Completions (lazy load за скорост)
autoload -Uz compinit
if [[ -n "${ZDOTDIR}/.zcompdump(#qN.mh+24)" ]]; then
  compinit
else
  compinit -C  # -C = skip security check = бързо
fi
setopt HIST_IGNORE_SPACE е golden rule — ако напишеш sudo rm -rf ... с интервал отпред, командата не се записва в history. Удобно за пароли и sensitive команди.

Alias-и, дето имат смисъл

Не ls="ls -la". Alias-и, с които спестяваш реално писане всеки ден.

~/.zsh/aliases.zsh
# ══ NAVIGATION ══════════════════════════════════
alias ..  ='cd ..'
alias ... ='cd ../..'
alias ....='cd ../../..'
alias ~   ='cd ~'
alias dl  ='cd ~/Downloads'
alias dt  ='cd ~/Desktop'
alias dev ='cd ~/Development'

# ══ LS — замени с eza/exa ако имаш ══════════════
if command -v eza &>/dev/null; then
  alias ls ='eza --icons --group-directories-first'
  alias ll ='eza -la --icons --git --group-directories-first'
  alias lt ='eza --tree --level=2 --icons'
  alias lta='eza --tree --level=3 --icons --git-ignore'
else
  alias ls ='ls -G'
  alias ll ='ls -lahG'
fi

# ══ GIT ═════════════════════════════════════════
alias g  ='git'
alias gs ='git status -sb'         # компактен status
alias ga ='git add -p'             # patch mode — add само конкретни hunks
alias gc ='git commit -v'          # показва diff в commit editor-а
alias gca='git commit --amend --no-edit'
alias gp ='git push'
alias gpl='git pull --rebase'
alias gl ='git log --oneline --decorate --graph --all'
alias gd ='git diff --stat'
alias gds='git diff --staged'
alias grh='git reset HEAD~1'      # undo последния commit, запази промените
alias gst='git stash'
alias gsp='git stash pop'
alias gco='git checkout'
alias gb ='git branch -vv'        # branches с upstream info

# ══ NETWORK ══════════════════════════════════════
alias myip  ='curl -s https://api.ipify.org && echo'
alias localip='ipconfig getifaddr en0'
alias flush ='sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder'
alias ports ='sudo lsof -i -n -P | grep LISTEN'
alias wget  ='wget --no-check-certificate'   # за dev

# ══ MACOS SPECIFIC ═══════════════════════════════
alias o   ='open .'
alias show='defaults write com.apple.finder AppleShowAllFiles YES && killall Finder'
alias hide='defaults write com.apple.finder AppleShowAllFiles NO  && killall Finder'
alias dsclean='find . -name ".DS_Store" -delete'
alias sleepnow='pmset sleepnow'
alias trash='osascript -e "tell application \"Finder\" to empty trash"'

# ══ SHORTCUTS ════════════════════════════════════
alias zr  ='source ~/.zshrc'       # reload config
alias ze  ='$EDITOR ~/.zshrc'     # редактирай config
alias h   ='history | tail -50'
alias hg  ='history | grep'       # hg "docker"
alias c   ='clear'
alias q   ='exit'
alias path='echo $PATH | tr ":" "\n" | nl'  # numbered list на PATH

# ══ SAFETY NET ═══════════════════════════════════
alias rm ='rm -i'               # пита преди изтриване
alias mv ='mv -i'
alias cp ='cp -i'

# ══ DOCKER ═══════════════════════════════════════
alias dk  ='docker'
alias dkc ='docker compose'
alias dkps='docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"'
alias dkrm='docker rm $(docker ps -aq)'     # изтрий всички спрени
alias dki ='docker images'
alias dkx ='docker exec -it'    # dkx container_name bash
ga = git add -p е game-changer. Вместо да add-ваш целия файл, избираш конкретни hunks интерактивно. Commit историята става атомарна и четима.

Functions — истинската сила

Alias-ите имат ограничения — не приемат аргументи. Functions нямат.

mkcd — Създай директория и влез в нея
utility
function mkcd() {
  mkdir -p "$1" && cd "$1"
}
extract — Разархивирай всичко
utility
function extract() {
  if [ -f $1 ]; then
    case $1 in
      *.tar.bz2) tar xjf  $1 ;;
      *.tar.gz)  tar xzf  $1 ;;
      *.tar.xz)  tar xJf  $1 ;;
      *.bz2)     bunzip2   $1 ;;
      *.gz)      gunzip    $1 ;;
      *.tar)     tar xf   $1 ;;
      *.zip)     unzip     $1 ;;
      *.7z)      7z x      $1 ;;
      *.rar)     unrar x   $1 ;;
      *)         echo "'$1' - не знам как да разархивирам" ;;
    esac
  else
    echo "'$1' не е файл"
  fi
}
Ползваш: extract archive.tar.gz — без да помниш кой флаг е за кой формат.
serve — HTTP сървър в текущата директория
network
function serve() {
  local port="${1:-8000}"
  local ip=$(ipconfig getifaddr en0 2>/dev/null || echo "localhost")
  echo "Serving at: http://${ip}:${port}"
  python3 -m http.server $port
}
serve — порт 8000. serve 3000 — порт 3000. Показва local IP за достъп от телефон.
gcl — Изтрий merged branches
git
function gcl() {
  local main_branch="${1:-main}"
  git branch --merged $main_branch \
    | grep -v "^\*\|$main_branch\|develop\|master" \
    | xargs -n 1 git branch -d
  echo "Merged branches cleaned."
}
gcl или gcl main. Изтрива всички локални branch-ове, merged в main — освен самия main/master/develop.
killport — Kill процеса на порт
system
function killport() {
  if [ -z "$1" ]; then
    echo "Usage: killport "; return 1
  fi
  local pid=$(lsof -ti :$1)
  if [ -n "$pid" ]; then
    echo "Killing PID $pid on port $1"
    kill -9 $pid
  else
    echo "Nothing on port $1"
  fi
}
killport 3000 — find & kill. Край на "port already in use" разровяния.
sshcopy — Копирай SSH ключ на сървър
network
function sshcopy() {
  local key="${2:-$HOME/.ssh/id_ed25519.pub}"
  cat "$key" | ssh "$1" \
    'mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys'
  echo "Key copied to $1"
}
sshcopy user@server — по-добър ssh-copy-id, работи дори без инсталиран пакет.
weather — Времето в терминала
network
function weather() {
  local city="${1:-Sofia}"
  curl -s "wttr.in/${city}?format=3"
}

function weatherfull() {
  local city="${1:-Sofia}"
  curl -s "wttr.in/${city}"
}
weather → "Sofia: ⛅ +8°C" на един ред. weatherfull Varna → пълна 3-дневна прогноза в ASCII.
fcd — Fuzzy cd с fzf
utility
# Изисква: brew install fzf fd
function fcd() {
  local dir
  dir=$(fd --type d --hidden --follow --exclude .git \
         "${1:-.}" 2>/dev/null \
         | fzf --preview 'eza --tree --level=2 --icons {}' \
               --height 60% --border) \
  && cd "$dir"
}

# Ctrl+F → fuzzy cd навсякъде
bindkey -s '^f' 'fcd\n'
Ctrl+F отваря fuzzy finder за директории с preview. Намираш и влизаш в nested директория за 2 секунди.

Prompt без oh-my-zsh

Информативен, бърз prompt — git branch, exit code, virtualenv. Без framework overhead.

✦ alex ~/dev/project ⎇ main ✚2
user · директория · git branch + промени · prompt символ
~/.zsh/prompt.zsh
# ── Git info function
function _git_info() {
  local branch dirty
  branch=$(git symbolic-ref --short HEAD 2>/dev/null) || return
  if [[ -n $(git status --porcelain 2>/dev/null) ]]; then
    dirty=' ✚'
  fi
  echo " ⎇ ${branch}${dirty}"
}

# ── Virtualenv info
function _venv_info() {
  [[ -n $VIRTUAL_ENV ]] && echo " ($(basename $VIRTUAL_ENV))"
}

# ── Colors
autoload -U colors && colors
setopt PROMPT_SUBST

# ── Prompt lines
NEWLINE=$'\n'

PROMPT='
%F{magenta}╭─[%f%F{cyan}%n%f%F{magenta}]%f %F{blue}%~%f%F{yellow}$(_git_info)%f%F{green}$(_venv_info)%f
%F{magenta}╰─%f%F{white}❯%f '

# ── Right prompt: timestamp + exit code
RPROMPT='%(?..%F{red}✘ %?%f )%F{239}%T%f'

# ── Simpler alt version (powerline style)
# PROMPT='%K{magenta}%F{white} %n %f%k%K{blue}%F{magenta}❯%f%F{white} %~ %f%k%F{blue}❯%f%F{yellow}$(_git_info)%f %F{white}❯%f '
RPROMPT показва exit code само когато е грешка (✘ 127) и timestamp вдясно. Виждаш веднага дали командата е минала или се е счупила — без да четеш output.

Старт под 100ms

Типичните zsh проблеми и как се оправят без да жертваш функционалност.

🕐 Измери
time zsh -i -c exit — виж колко ms отнема стартирането. Над 300ms = имаш проблем. Цел: под 80ms.
Проблем Причина Решение
compinit всеки път Rebuild на completion cache при всеки старт Проверявай дали cache е стар с -mh+24 (само 1x на 24h)
nvm load time nvm source е бавен (~300ms) Lazy load: заредени само когато пишеш node/npm
Много plugins oh-my-zsh зарежда 50+ файла Използвай само нужните; zsh-autosuggestions е достатъчен
git prompt бавен git status при всеки prompt render Добави timeout или disable в голям repo
brew shellenv eval "$(brew shellenv)" е бавен Hardcode пътищата вместо eval
lazy load за nvm — спести ~300ms
# Вместо: source ~/.nvm/nvm.sh  (бавно!)

# Lazy load — зарежда nvm само при нужда
export NVM_DIR="$HOME/.nvm"

function _load_nvm() {
  [ -s "$NVM_DIR/nvm.sh" ] && source "$NVM_DIR/nvm.sh"
  [ -s "$NVM_DIR/bash_completion" ] && source "$NVM_DIR/bash_completion"
}

# Intercept node/npm/nvm — зарежда само при извикване
for cmd in node npm npx nvm; do
  eval "function $cmd() { unfunction $cmd; _load_nvm; $cmd \"\$@\"; }"
done
Startup time пада от ~400ms на ~60ms само с тази промяна. nvm се зарежда при първото node или npm извикване — без разлика в ползването.

Всичко заедно

Copy-paste ready — един файл с всичко. Адаптирай пътищата за твоя setup.

⚠ Преди да замениш
Backup: cp ~/.zshrc ~/.zshrc.bak — после може да върнеш с cp ~/.zshrc.bak ~/.zshrc.
~/.zshrc — пълен конфиг ~280 реда
# ════════════════════════════════════════════════════
# .zshrc — Pro Setup
# ════════════════════════════════════════════════════

# ── PATH
export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:$HOME/.local/bin:$PATH"

# ── Core exports
export EDITOR='nvim'
export VISUAL='nvim'
export LANG='en_US.UTF-8'
export LC_ALL='en_US.UTF-8'
export TERM='xterm-256color'

# ── History
export HISTSIZE=100000
export SAVEHIST=100000
export HISTFILE="$HOME/.zsh_history"
setopt HIST_IGNORE_DUPS HIST_IGNORE_SPACE SHARE_HISTORY EXTENDED_HISTORY

# ── Zsh options
setopt AUTO_CD           # cd без cd
setopt CORRECT           # предложи корекция при typo
setopt GLOB_DOTS         # включи dotfiles в glob
setopt NO_BEEP           # без звук при грешка

# ── Completions (cached)
autoload -Uz compinit
if [[ -n "${ZDOTDIR:-$HOME}/.zcompdump(#qN.mh+24)" ]]; then
  compinit; else compinit -C
fi
zstyle ':completion:*' menu select
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}'  # case insensitive

# ── Plugins (manual — без oh-my-zsh)
if [ -f /opt/homebrew/share/zsh-autosuggestions/zsh-autosuggestions.zsh ]; then
  source /opt/homebrew/share/zsh-autosuggestions/zsh-autosuggestions.zsh
  export ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE='fg=238'
  bindkey '^]' autosuggest-accept   # Ctrl+] приема suggestion
fi

if [ -f /opt/homebrew/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh ]; then
  source /opt/homebrew/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh
fi

# ── Key bindings
bindkey '^[[A' history-search-backward  # ↑ търси в history
bindkey '^[[B' history-search-forward
bindkey '^A'   beginning-of-line
bindkey '^E'   end-of-line
bindkey '^[[H' beginning-of-line       # Home
bindkey '^[[F' end-of-line             # End

# ── FZF integration
[ -f ~/.fzf.zsh ] && source ~/.fzf.zsh
export FZF_DEFAULT_OPTS='--height 40% --border --reverse --color=fg:#8892a4,bg:#080b12,hl:#ff2d78'
export FZF_DEFAULT_COMMAND='fd --type f --hidden --follow --exclude .git'
export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND"

# ── Aliases
alias ..  ='cd ..'
alias ... ='cd ../..'
alias dl  ='cd ~/Downloads'
alias dev ='cd ~/Development'
alias ls  ='eza --icons --group-directories-first 2>/dev/null || ls -G'
alias ll  ='eza -la --icons --git 2>/dev/null || ls -lahG'
alias g   ='git'
alias gs  ='git status -sb'
alias ga  ='git add -p'
alias gc  ='git commit -v'
alias gca ='git commit --amend --no-edit'
alias gp  ='git push'
alias gpl ='git pull --rebase'
alias gl  ='git log --oneline --decorate --graph --all'
alias gd  ='git diff --stat'
alias grh ='git reset HEAD~1'
alias gst ='git stash'
alias gsp ='git stash pop'
alias gb  ='git branch -vv'
alias myip='curl -s https://api.ipify.org && echo'
alias flush='sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder'
alias ports='sudo lsof -i -n -P | grep LISTEN'
alias o   ='open .'
alias show='defaults write com.apple.finder AppleShowAllFiles YES && killall Finder'
alias hide='defaults write com.apple.finder AppleShowAllFiles NO  && killall Finder'
alias dsclean='find . -name ".DS_Store" -delete'
alias zr  ='source ~/.zshrc'
alias ze  ='$EDITOR ~/.zshrc'
alias h   ='history | tail -50'
alias hg  ='history | grep'
alias c   ='clear'
alias path='echo $PATH | tr ":" "\n" | nl'
alias rm  ='rm -i'
alias dk  ='docker'
alias dkps='docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"'

# ── Functions
function mkcd()     { mkdir -p "$1" && cd "$1"; }
function serve()    { python3 -m http.server "${1:-8000}"; }
function weather()  { curl -s "wttr.in/${1:-Sofia}?format=3"; }

function killport() {
  local pid=$(lsof -ti :$1)
  [ -n "$pid" ] && kill -9 $pid && echo "Killed PID $pid" || echo "Nothing on :$1"
}

function extract() {
  [ -f "$1" ] || { echo "File not found: $1"; return 1; }
  case "$1" in
    *.tar.gz|*.tgz) tar xzf "$1" ;;
    *.tar.bz2)      tar xjf "$1" ;;
    *.tar.xz)       tar xJf "$1" ;;
    *.zip)          unzip "$1"   ;;
    *.gz)           gunzip "$1"  ;;
    *.7z)           7z x "$1"   ;;
    *)              echo "Cannot extract: $1" ;;
  esac
}

function gcl() {
  git branch --merged "${1:-main}" \
    | grep -v "^\*\|main\|master\|develop" \
    | xargs -n 1 git branch -d
}

# ── NVM Lazy Load
export NVM_DIR="$HOME/.nvm"
function _load_nvm() {
  [ -s "$NVM_DIR/nvm.sh" ] && source "$NVM_DIR/nvm.sh"
}
for cmd in node npm npx nvm; do
  eval "function $cmd() { unfunction $cmd; _load_nvm; $cmd \"\$@\"; }"
done

# ── Prompt
autoload -U colors && colors
setopt PROMPT_SUBST

function _git_info() {
  local b=$(git symbolic-ref --short HEAD 2>/dev/null) || return
  local d=$([[ -n $(git status --porcelain 2>/dev/null) ]] && echo ' ✚')
  echo " ⎇ ${b}${d}"
}

PROMPT='
%F{magenta}╭─[%f%F{cyan}%n%f%F{magenta}]%f %F{blue}%~%f%F{yellow}$(_git_info)%f
%F{magenta}╰─%f%F{white}❯%f '
RPROMPT='%(?..%F{red}✘%?%f )%F{239}%T%f'

# ── END ──────────────────────────────────────────
Инсталирай prerequisites: brew install eza fzf fd zsh-autosuggestions zsh-syntax-highlighting После: source ~/.zshrc или отвори нов терминал.