Four .zshrc Mistakes That Quietly Break Zsh, and How to Fix Them

What came out of reviewing my .zshrc: return $(...), LC_ALL, compinit order and heavy ls aliases

* This page contains promotional content

I had been happily using the zshrc I set up in Reviewing my Zsh and NeoVim configuration as a base, but I went over the configuration again with the help of generative AI (ChatGPT-5.2).

What I improved

  • Made the broken predicate functions correct (stopped using return $(…))
  • Left the locale alone (LC_ALL in particular is easy to get wrong)
  • Made completion work from the very first shell (avoided the landmine of zinit lazy loading combined with the compinit order)
  • Isolated the heavy listings into ll/la so that everyday ls does not get slow

Background: with .zshrc, convenience and destruction are a hair apart

When it comes to improving a zshrc, there are more landmines of the “writing it the wrong way quietly breaks things” kind than there is work on appearance or adding aliases. These were the four places I got stuck this time:

  1. return $(…) in _has/_try
  2. locale (overwriting LC_ALL)
  3. the order of zinit lazy loading and compinit
  4. aliases that make the ls equivalent too heavy

1) _has/_try: return $(…) is an accident that turns output into an exit status

What return returns is a status from 0 to 255. If you feed the standard output of $(…) into it, it becomes an empty string or a string and breaks (it drifts away from the intent).

The fix is simple: just run the command and return its exit status.

_has() {
  whence -w -- "$1" >/dev/null 2>&1
}

_try() {
  eval "$*" >/dev/null 2>&1
}

Key points:

  • Always quote “$1” / “$*” (it prevents breakage from spaces and special characters)
  • Because _try is eval, where it can be used is limited (this time I leaned to the safe side without breaking the existing design)

2) locale: LC_ALL overwrites everything, so do not touch it in .zshrc

Once you export LC_ALL, all the categories such as LC_CTYPE and LC_MESSAGES are forced. As a result Japanese turns into mojibake, command output changes, and you get behaviour you did not expect.

This time I made it a policy not to set LC_ALL inside .zshrc, and only to put UTF-8 in when LANG is empty.

if [[ -z ${LANG-} ]]; then
  export LANG=ja_JP.UTF-8
fi

# NOTE:
# Avoid setting LC_ALL here. It overrides all LC_* categories and can easily
# cause mojibake. If you need C locale for a single command, do:
#   LC_ALL=C <cmd>

Operational tips:

  • For a use like “I want to sort in C only”, it is safer to write LC_ALL=C cmd for that command alone.

3) Completion: the order of zinit lazy loading and compinit causes “it only fails the first time”

If you lazy load a completion source (for example zsh-completions) and run compinit first, completion can end up being iffy on the first run only.

This time I loaded the completion related things first without delaying them, and made it lighter with compinit -C (cache).

zinit light zsh-users/zsh-completions
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}'
zstyle ':completion:*:default' menu select=1

autoload -Uz compinit && compinit -C

4) alias: isolate the heavy ls equivalents into ll/la

Piling eza options on (icons/git/time-style/all/long) feels good, but when a directory is large it is plainly slow.

So, going with “the heavy ones go to an explicit command”, I grew ll and la.

which eza > /dev/null && alias ll='eza --icons --git --time-style relative -al'
which eza > /dev/null && alias la='eza --icons --git --time-style relative -a'

A landmine I crushed along the way: smart quotes kill an alias

Slipping smart quotes in, as in alias dc=‘docker-compose’, is the kind of thing that makes just that one line ineffective. After unifying on the ASCII ‘, I made it a fallback that prefers docker v2.

if _has docker; then
  if _try "docker compose version"; then
    alias dc='docker compose'
  elif _has docker-compose; then
    alias dc='docker-compose'
  fi
fi

Verification: at least this much

  • Syntax check (no side effects)
zsh -n ~/.zshrc
  • Applying it (evaluate in a new environment)
exec zsh -l

Note: when mojibake shows up, it can be that an old shell or terminal was holding on to the old environment, so restart the terminal if necessary.


Summary

  • With .zshrc, putting not breaking ahead of convenience is what ends up feeling good to use
  • Localise LC_ALL, mind the order for completion, and let the heavy listings escape to ll/la
  • return $(…) is out of the question (output is not the exit status)

References

See also