#+TITLE:  Zshell
#+AUTHOR: Howard Abrams
#+EMAIL:  howard@howardabrams.com
#+DATE:   2025-03-30 Sun
#+LASTMOD: [2025-03-30 Sun]
#+FILETAGS: technical
#+STARTUP: inlineimages

No, I haven’t eschewed my beloved Eshell, but at work, I’m often required to work with Bash snippets and code. This requires using a standard shell environment. This file includes my notes on Zshell, as well my configuration, when tangled.

Do I prefer Zshell over Bash? Just barely. Most of its bells and whistles are quite annoying for some one who spends more time in Emacs that a shell. Still, I like these features:

  - Syntax coloring :: helps flag potential errors (but I do /not/ like autocorrect, as that is more often wrong).
  - Fail/Success Dot :: At the beginning of the command prompt, you can see if the job passed.
  - Easy completion :: The plugins with OMZ often set up option completion
  - Auto CD :: This means that =popd= usually just works.
  - ZBell :: The waiting indicators and alerts on long running jobs is something I love in my eshell setup.

Regardless, I keep my shell configuration conspicuously light.
* Configuration
Let’s create the following files, and the configuration below will be injected into one of them:

  - =~/.zshenv= :: Usually run for every zsh
  - =~/.zprofile= :: Usually run for login shells (this includes the system-wide =/etc/zprofile=)
  - =~/.zshrc= :: Run for interactive shells … default file when tangling
  - =~/.zlogin= :: Run for login shells … seems to run as often as =.zshrc=

#+BEGIN_SRC zsh :exports none
  #!/usr/bin/env zsh
  #
  # My complete Zshell configuration. Don't edit this file.
  # Instead edit: zshell.org and tangle it.
  #
#+END_SRC

#+BEGIN_SRC zsh :tangle ~/.zshenv :exports none
  #!/usr/bin/env zsh
  #
  # Non-interactive, mostly easily settable environment variables. Don't
  # edit this file.  Instead edit: zshell.org and tangle.
  #
#+END_SRC

* Path
The all important =PATH= environment variable, needs my special =bin= directory.

#+BEGIN_SRC zsh :tangle ~/.zshenv
  export PATH=$HOME/bin:$HOME/.local/bin:$PATH
#+END_SRC

My Apple Macbook screws up my =PATH= by having =/etc/profile= (that runs after my =~/.zshenv=) /pre-pend/ system directories like =/bin= and =/usr/bin= /after/ I’ve set up my =PATH= environment variable. So, in my own =.zprofile= (which runs afterwards), I reverse it using the lovely =tac= program:

#+BEGIN_SRC sh :tangle ~/.zprofile :shebang #!/bin/zsh
  if [[ -f /etc/zprofile ]]
  then
      # Reverse the PATH variable
      reversed_path=$(echo $PATH | tr ':' '\n' | tac | tr '\n' ':')

      # Reset the path after removing the trailing colon:
      export PATH=${reversed_path%:}

      # Output the reversed PATH
      # echo "Reversed PATH: $reversed_path"
  fi
#+END_SRC

* Options
When the command is the name of a directory, perform the =cd= command to that directory:

#+BEGIN_SRC zsh
setopt AUTO_CD
#+END_SRC

Make =cd= push the old directory onto the directory stack so that =popd= always works:

#+BEGIN_SRC zsh
setopt AUTO_PUSHD
#+END_SRC

Print the working directory after a =cd=, but only if that was magically expanded:

#+BEGIN_SRC zsh
setopt NO_CD_SILENT
#+END_SRC

Automatically list choices on an ambiguous completion:

#+BEGIN_SRC zsh
setopt AUTO_LIST
#+END_SRC

Automatically use menu completion after the second consecutive request for completion:

#+BEGIN_SRC zsh
setopt AUTO_MENU
#+END_SRC

Try to make the completion list smaller (occupying less lines) by printing the matches in columns with different widths:

#+BEGIN_SRC zsh
setopt LIST_PACKED
#+END_SRC

On an ambiguous completion, instead of listing possibilities or beeping, insert the first match immediately. Then when completion is requested again, remove the first match and insert the second match, etc.

#+BEGIN_SRC zsh
setopt MENU_COMPLETE
#+END_SRC

* Homebrew
When using Homebrew on a Mac, we need to add its =PATH=:

#+BEGIN_SRC zsh :tangle ~/.zshenv
  if [[ -d /opt/homebrew ]]
  then
    eval $(/opt/homebrew/bin/brew shellenv zsh)
  fi
#+END_SRC

This adds the following environment variables, along with expanding the =PATH=.

#+BEGIN_SRC zsh :tangle no
  export HOMEBREW_PREFIX="/opt/homebrew";
  export HOMEBREW_CELLAR="/opt/homebrew/Cellar";
  export HOMEBREW_REPOSITORY="/opt/homebrew";
  [ -z "${MANPATH-}" ] || export MANPATH=":${MANPATH#:}";
  export INFOPATH="/opt/homebrew/share/info:${INFOPATH:-}";
#+END_SRC

* ZShell Styles
The [[http://www.bash2zsh.com/][Zsh Book]] has a nice chapter treatment on =zstyle=, also, explaining in detail its various fields.

** Tab Completion
Use [[https://thevaluable.dev/zsh-install-configure-mouseless/][autoload]] to install =compinit=, the completion system:

#+BEGIN_SRC zsh
  autoload -U compinit; compinit
#+END_SRC

Do I need this?

#+BEGIN_SRC zsh
  zstyle ':completion:*:*:cp:*' file-sort modification reverse
  zstyle ':completion:*:*:mv:*' file-sort modification reverse
#+END_SRC

Selecting options using ~Tab~, arrows, and ~C-p~ / ~C-n~:

#+BEGIN_SRC zsh
  zmodload zsh/complist
#+END_SRC

Do I want to use hyphen-insensitive completion, so that  =_= and =-= will be interchangeable?

#+BEGIN_SRC zsh :tangle ~/.zshenv
  HYPHEN_INSENSITIVE="true"
#+END_SRC

** Waiting Indication
Display red dots whilst waiting for commands to complete.

#+BEGIN_SRC zsh :tangle ~/.zshenv
  COMPLETION_WAITING_DOTS="true"
#+END_SRC

You can also set it to another string to have that shown instead of the default red dots.

#+BEGIN_SRC zsh :tangle no
  COMPLETION_WAITING_DOTS="%F{yellow}waiting...%f"
#+END_SRC

* Oh My Zshell
Some [[https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins][plugins for Zshell]] are nice, so let’s install [[https://ohmyz.sh/][Oh My Zshell]]:

#+BEGIN_SRC sh :tangle no :results replace raw :wrap example
  sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
#+END_SRC

#+RESULTS:
#+begin_example
Cloning Oh My Zsh...
branch 'master' set up to track 'origin/master' by rebasing.
/Users/howard/Library/CloudStorage/Dropbox/org/technical

Looking for an existing zsh config...
Found old .zshrc.pre-oh-my-zsh. Backing up to /Users/howard/.zshrc.pre-oh-my-zsh-2025-03-30_12-22-02
Found /Users/howard/.zshrc. Backing up to /Users/howard/.zshrc.pre-oh-my-zsh
Using the Oh My Zsh template file and adding it to /Users/howard/.zshrc.

         __                                     __
  ____  / /_     ____ ___  __  __   ____  _____/ /_
 / __ \/ __ \   / __ `__ \/ / / /  /_  / / ___/ __ \
/ /_/ / / / /  / / / / / / /_/ /    / /_(__  ) / / /
\____/_/ /_/  /_/ /_/ /_/\__, /    /___/____/_/ /_/
                        /____/                       ....is now installed!


Before you scream Oh My Zsh! look over the `.zshrc` file to select plugins, themes, and options.

• Follow us on X: https://x.com/ohmyzsh
• Join our Discord community: https://discord.gg/ohmyzsh
• Get stickers, t-shirts, coffee mugs and more: https://shop.planetargon.com/collections/oh-my-zsh

Run zsh to try it out.
#+end_example

Set it’s location:

#+BEGIN_SRC zsh :tangle ~/.zshrc
  export ZSH=$HOME/.oh-my-zsh
#+END_SRC

** Syntax Coloration

The [[https://github.com/zsh-users/zsh-syntax-highlighting][ZShell Syntax Highlighting]] project provides [[https://fishshell.com/][Fish shell]]-like syntax highlighting for ZShell. This was my killer feature for using Fish, but I need the standard Bash-compatible syntax. Now I can have both. Let’s install this project in coordination with Oh My Zshell:

#+BEGIN_SRC sh :tangle no
  git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting
#+END_SRC

Using the =colorize= plugin (see [[Plugins][plugins section below]]), we customize with this variable setting:

#+BEGIN_SRC zsh
  export ZSH_COLORIZE_STYLE="coffee"
#+END_SRC


** Language Support
Anything special for particular languages.
*** Python
Not overly impressed, for to get =pyenv= to work, we need to add this code:

#+BEGIN_SRC zsh :tangle ~/.zshenv
  export PYENV_ROOT="$HOME/.pyenv"
  export PATH="$PYENV_ROOT/bin:$PATH"
#+END_SRC

And call the =pyenv= to initialize it:

#+BEGIN_SRC zsh
  # eval "$(pyenv init --path)"
#+END_SRC

** Plugins
Configure the plugins, making sure to not use =git=, as the aliases are a pain to remember when I already have a superior Git interface in Emacs.

  - [[https://github.com/hsienjan/colorize][colorize]] :: syntax-highlight file contents, so install [[https://pygments.org/download/][Pygments]] first. Then call =ccat= and =cless= (see [[Aliases]]).
  - [[https://github.com/ptavares/zsh-direnv][direnv]] :: to support the [[https://direnv.net/][direnv]] virtual environment project.
  - [[https://github.com/ohmyzsh/ohmyzsh/blob/master/plugins/mise/mise.plugin.zsh][mise]] :: to support the [[https://mise.jdx.dev/getting-started.html][mise]] virtual environment and tool project (instead of direnv). See the [[https://mise.jdx.dev/walkthrough.html][walk-through]].
  - [[https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/gnu-utils][gnu-utils]] :: bind the GNU flavor for standard utils, like =gfind= to the normal version, e.g. =find=.
  - [[https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/iterm2][iterm2]] :: while fully configured below, configures the interaction with the MacOS application, [[https://www.iterm2.com/][iTerm2]].
  - [[https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/macos][macos]] :: adds new functions that work better with MacOS terminals and the Finder. I like:
      - =tab= :: To open a new terminal tab
      - =cdf= :: To open a directory in the Finder, meh. Why not change this to open it in =dired= in Emacs?
      - =quick-look= :: To view a file
  - [[https://github.com/ohmyzsh/ohmyzsh/blob/master/plugins/pyenv/README.md][pyenv]] :: Call the =pyenv init= and whatnot.
  - [[https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/zbell][zbell]] :: To beep when a long running command has completed. Similar to my =beep= command.

To have a plugin /install/, add its name to the =plugins= array variable /before/ we =source= the OMZ script:

#+begin_SRC zsh
  plugins=(colorize direnv gnu-utils iterm2 macos mise virtualenv zbell zsh-syntax-highlighting)
#+END_SRC

Notice the =iterm2= plugin as well as the =macos= plugins that would be nice to figure out how to make them optionally added (although I believe they check themselves).

The trick is to install some base-level plugins, and then, on my work computer, install more by appending to the =plugins= array variable:

#+BEGIN_SRC zsh
  if hostname | grep AL33 >/dev/null
  then
    plugins+=(ansible argocd docker helm kubectl)
  fi
#+END_SRC

I would like to have a history /per project/, so that when I start a session for a project, my history has where I left off /for that project/ and not everything. I guess I’m not the only one who [[https://github.com/ivan-cukic/zsh-per-project-history][thought of this idea]].  We first need to add his plugin to the list of supplied plugins, so do this once:

#+BEGIN_SRC zsh :tangle no
  git clone https://github.com/ivan-cukic/zsh-per-project-history ~/.oh-my-zsh/plugins/per-project-history
#+END_SRC

His idea is to have a variable array, =PER_PROJECT_HISTORY_TAGS= that lists files that should identify the start of a project, and while his default seems sufficient, I am not found of the spammy message, so:

#+BEGIN_SRC zsh
  declare -a PER_PROJECT_HISTORY_TAGS
  export PER_PROJECT_HISTORY_TAGS=(.envrc .git)
  declare -r PER_PROJECT_HISTORY_TAGS
#+END_SRC

we just need to add =per-history= to the list of plugins:

#+BEGIN_SRC zsh
  plugins+=(per-project-history)
#+END_SRC

Now that I’ve filled in the =plugins= variable, load OMZ and the plugins:

#+BEGIN_SRC zsh
  source $ZSH/oh-my-zsh.sh
#+END_SRC

Do we want to waste time during startup to update this? These can be:

  - =disabled= :: disable automatic updates
  - =auto= :: update automatically without asking
  - =reminder= :: remind me to update when it's time

#+BEGIN_SRC zsh
  zstyle ':omz:update' mode auto
  zstyle ':omz:update' frequency 13
#+END_SRC

We’ll Check every 13 days.
* Prompt
Either keep it simple:

#+BEGIN_SRC zsh
  PS1='%(?.%F{green}.%F{red})$%f%b '
#+END_SRC

Oh use the absolute /over-the-top/ bling associated with Oh My Zshell’s /themes/, like:

#+BEGIN_SRC zsh :tangle no
  ZSH_THEME="robbyrussell"
#+END_SRC

I keep the prompt simple since all of the /gunk/ we typically put in a prompt is better placed in [[https://iterm2.com/documentation-status-bar.html][iTerm2's Status Bar]].
* Homebrew
When using Homebrew on a Mac, we need to add its =PATH= and environment variables. This is typically done by running the command:

#+BEGIN_SRC zsh :tangle no
  eval $(brew shellenv zsh)
#+END_SRC

We want to add the path and environment variables into the =~/.zshenv= file, but this file should not contain any logic or code. So, let’s run the command /from Emacs/, and store the results in the file.

The full script to run is:

#+BEGIN_SRC zsh :tangle no :results file :file ~/.zshenv_brew
  echo '# -*- mode:sh; -*-'
  if which brew >/dev/null
  then
      if [[ -d /opt/homebrew ]]
      then
          /opt/homebrew/bin/brew shellenv zsh
      else
          brew shellenv zsh
      fi
  fi
#+END_SRC

Seems that if I want the GNU versions (instead of the old ones supplied by Apple), I have to do it myself:

#+BEGIN_SRC zsh :tangle no :results file :file ~/.zshenv_gnu
  echo '# -*- mode:sh; -*-'
  if which brew >/dev/null
  then
      for PKG in binutils gettext unzip openssl texinfo mysql-client openjdk
      do
          if PKG_INSTALL=$(brew --prefix $PKG)
          then
              echo export PATH=$PKG_INSTALL/bin:'$PATH'
          fi
      done

      for PKG in coreutils ed findutils gnu-indent gnu-sed gnu-tar grep make
      do
          if PKG_INSTALL=$(brew --prefix $PKG)
          then
              echo export PATH=$PKG_INSTALL/libexec/gnubin:'$PATH'
          fi
      done
  fi
#+END_SRC

And linking all the GNU libraries:

#+BEGIN_SRC zsh :tangle no :results file :file ~/.zshenv_lib
  echo '# -*- mode:sh; -*-'

  if which brew >/dev/null
  then
      for PKG in readline openssl xz binutils ctags libgccjit imagemagick
      do
          if PKG_INSTALL=$(brew --prefix $PKG)
             echo export LDFLAGS=\"-L$PKG_INSTALL/lib '$LDFLAGS'\"
             echo export CPPFLAGS=\"-I$PKG_INSTALL/include '$CPPFLAGS'\"
      done
      echo export LDFLAGS=\"'$LDFLAGS' -L$(brew --prefix)/lib\"
      echo export CPPFLAGS=\"'$CPPFLAGS' -I$(brew --prefix)/include\"
  fi
#+END_SRC

And pull in all the results into the =~/.zshenv= file (why yes, this could be inlined):

#+BEGIN_SRC zsh :tangle ~/.zshenv
  [[ -f $HOME/.zshenv_brew ]] && source $HOME/.zshenv_brew
  [[ -f $HOME/.zshenv_gnu  ]] && source $HOME/.zshenv_gnu
  [[ -f $HOME/.zshenv_lib  ]] && source $HOME/.zshenv_lib
#+END_SRC

* iTerm2
On Mac systems, I like the [[https://www.iterm2.com/][iTerm2 application]], and we can enable [[https://iterm2.com/documentation-shell-integration.html][shell integration]], either via the old school way, or just rely on [[https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/iterm2][the /plugin/ ]]above:

#+BEGIN_SRC zsh
  test -e "${HOME}/.iterm2_shell_integration.zsh" && source "${HOME}/.iterm2_shell_integration.zsh"
#+END_SRC

Also, while use the =title= command to change the Terminal’s title bar, don’t let the prompt or other Zshell features do that:

#+BEGIN_SRC zsh :tangle ~/.zshenv
  DISABLE_AUTO_TITLE="true"
#+END_SRC

Favorite feature is the [[https://iterm2.com/documentation-status-bar.html][Status Bar]] at the bottom of the screen that shows the Git branch, current working directory, etc. This allows my prompt to be much shorter. What other information I want has changed over the years, but I define this information with this function:

Currently, I show the currently defined Kube namespace.

#+BEGIN_SRC zsh
  function iterm2_python_version() {
      echo $(pyenv version-name):$(echo "$VIRTUAL_ENV" | sed "
                      s|^$HOME|~|
                      s|^~/src/wpc-gerrit.inday.io/||
                      s|^~/work/||
                      s|^~/.venv/||
                      s|/\.venv$||
                      s|\.venv$||")
  }

  function iterm2_print_user_vars() {
      # iterm2_set_user_var kubecontext $($ yq '.users[0].name' ~/.kube/config):$(kubectl config view --minify --output 'jsonpath={..namespace}')

      # Correct version:
      # iterm2_set_user_var kubecontext $(kubectl config current-context):$(kubectl config view --minify --output 'jsonpath={..namespace}')
      # Faster version:
      iterm2_set_user_var kubecontext $(awk '/^current-context:/{print $2;exit;}' <~/.kube/config)

      iterm2_set_user_var pycontext $(iterm2_python_version)
  }
#+END_SRC

Add the following:

#+BEGIN_SRC zsh
  function pycontext {
    local version venvstr
    version=$(pyenv version-name)
    venvstr=$(echo $VIRTUAL_ENV | sed 's/.*.venv\///')
    echo "🐍 $version:$venvstr"
  }
#+END_SRC

* Emacs
While /Oh My Zshell/ has an =emacs= plugin, I’m not crazy about it. I guess I need more control.

While it /should/ figure out (as Emacs keybindings are the default), this is how we ensure it:

#+BEGIN_SRC zsh
  bindkey -e
#+END_SRC

Where be the =emacsclient=? It should, at this point, be in our path.

And how should we call it?

#+BEGIN_SRC zsh :tangle ~/.zshenv
  export EMACS_SOCKET_NAME=personal
#+END_SRC

Which needs to be overwritten on my Work computer:

#+BEGIN_SRC zsh :tangle ~/.zshenv
  if hostname | grep AL33 >/dev/null
  then
    export EMACS_SOCKET_NAME=work
  fi
#+END_SRC

The =EDITOR= variable that some programs use to edit files from the command line:

#+BEGIN_SRC zsh :tangle ~/.zshenv
  export EDITOR="emacsclient --tty"
  export VISUAL="emacsclient --create-frame"
#+END_SRC

With these variables defined, we can create simple aliases:

#+BEGIN_SRC zsh
  alias e="$EDITOR"
  alias te="$EDITOR"
  alias ee="emacsclient --create-frame"
  alias eee="emacsclient --create-frame --no-wait"
#+END_SRC

** Vterm

To work with [[https://github.com/akermu/emacs-libvterm][VTerm in Emacs]], we need to create this function:

#+BEGIN_SRC zsh
  vterm_printf() {
      if [ -n "$TMUX" ] \
          && { [ "${TERM%%-*}" = "tmux" ] \
              || [ "${TERM%%-*}" = "screen" ]; }
      then
          # Tell tmux to pass the escape sequences through
          printf "\ePtmux;\e\e]%s\007\e\\" "$1"
      elif [ "${TERM%%-*}" = "screen" ]; then
          # GNU screen (screen, screen-256color, screen-256color-bce)
          printf "\eP\e]%s\007\e\\" "$1"
      else
          printf "\e]%s\e\\" "$1"
      fi
  }
#+END_SRC

This allows us to execute Emacs commands:

#+BEGIN_SRC zsh
  vterm_cmd() {
      local vterm_elisp
      vterm_elisp=""
      while [ $# -gt 0 ]; do
          vterm_elisp="$vterm_elisp""$(printf '"%s" ' "$(printf "%s" "$1" | sed -e 's|\\|\\\\|g' -e 's|"|\\"|g')")"
          shift
      done
      vterm_printf "51;E$vterm_elisp"
  }
#+END_SRC

For instance:

#+BEGIN_SRC zsh
  if [[ "$INSIDE_EMACS" = 'vterm' ]]
  then
      alias clear='vterm_printf "51;Evterm-clear-scrollback";tput clear'

      vim() {
          vterm_cmd find-file "$(realpath "${@:-.}")"
      }
  fi
#+END_SRC

* Aliases
Assuming we’ve installed [[https://github.com/lsd-rs/lsd][lsd]], let’s make an alias for it:

#+BEGIN_SRC zsh
  if which lsd >/dev/null
  then
    alias ls=lsd
  fi
#+END_SRC

The [[https://github.com/jtdaugherty/ccat][ccat project]] (like bat) adds syntax coloring to text files. For big files, it certainly slows down the output, and I’m wondering if I want these aliases.

#+BEGIN_SRC zsh
  if whence cless >/dev/null
  then
    alias less=cless
    alias cat=ccat
  fi
#+END_SRC

Other alternate improvements, like [[https://github.com/sharkdp/fd][fd]] should be called directly.

And an abstraction for transitory endpoints over SSH:

#+BEGIN_SRC zsh
  alias ossh="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o loglevel=ERROR"
#+END_SRC

And other ones that I use:

#+BEGIN_SRC  zsh
  alias os=openstack
  alias k=kubectl
#+END_SRC

* Final Message
For sensitive work-related environment variables, store them elsewhere, and load them:

#+BEGIN_SRC zsh :tangle ~/.zshenv
  test -e "${HOME}/.zshenv-work" && source "${HOME}/.zshenv-work"
#+END_SRC

To let us know we read the =~/.zshrc= file:

#+BEGIN_SRC zsh
  echo "🐚 ZShell Session"
#+END_SRC

#+description: A literate programming file for configuring Zshell.
#+property:    header-args:zsh :tangle ~/.zshrc
#+property:    header-args:sh :tangle no
#+property:    header-args    :results none :eval no-export :comments no mkdirp yes
#+options:     num:nil toc:t todo:nil tasks:nil tags:nil date:nil
#+options:     skip:nil author:nil email:nil creator:nil timestamp:nil
#+infojs_opt:  view:nil toc:t ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js

# Local Variables:
# eval: (add-hook 'after-save-hook #'org-babel-tangle t t)
# End:
