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
popdusually 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.
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
The all important PATH environment variable, needs my special bin directory.
export PATH=$HOME/bin:$HOME/.local/bin:$PATHMy 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:
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"
fiWhen the command is the name of a directory, perform the cd command to that directory:
setopt AUTO_CDMake cd push the old directory onto the directory stack so that popd always works:
setopt AUTO_PUSHDPrint the working directory after a cd, but only if that was magically expanded:
setopt NO_CD_SILENTAutomatically list choices on an ambiguous completion:
setopt AUTO_LISTAutomatically use menu completion after the second consecutive request for completion:
setopt AUTO_MENUTry to make the completion list smaller (occupying less lines) by printing the matches in columns with different widths:
setopt LIST_PACKEDOn 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.
setopt MENU_COMPLETEWhen using Homebrew on a Mac, we need to add its PATH:
if [[ -d /opt/homebrew ]]
then
eval $(/opt/homebrew/bin/brew shellenv zsh)
fiThis adds the following environment variables, along with expanding the PATH.
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:-}";The Zsh Book has a nice chapter treatment on zstyle, also, explaining in detail its various fields.
Use autoload to install compinit, the completion system:
autoload -U compinit; compinitDo I need this?
zstyle ':completion:*:*:cp:*' file-sort modification reverse
zstyle ':completion:*:*:mv:*' file-sort modification reverseSelecting options using Tab, arrows, and C-p / C-n:
zmodload zsh/complistDo I want to use hyphen-insensitive completion, so that _ and - will be interchangeable?
HYPHEN_INSENSITIVE="true"Display red dots whilst waiting for commands to complete.
COMPLETION_WAITING_DOTS="true"You can also set it to another string to have that shown instead of the default red dots.
COMPLETION_WAITING_DOTS="%F{yellow}waiting...%f"Some plugins for Zshell are nice, so let’s install Oh My Zshell:
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
Set it’s location:
export ZSH=$HOME/.oh-my-zshThe ZShell Syntax Highlighting project provides 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:
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlightingUsing the colorize plugin (see plugins section below), we customize with this variable setting:
export ZSH_COLORIZE_STYLE="coffee"Anything special for particular languages.
Not overly impressed, for to get pyenv to work, we need to add this code:
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"And call the pyenv to initialize it:
# eval "$(pyenv init --path)"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.
- colorize
- syntax-highlight file contents, so install Pygments first. Then call
ccatandcless(see Aliases). - direnv
- to support the direnv virtual environment project.
- mise
- to support the mise virtual environment and tool project (instead of direnv). See the walk-through.
- gnu-utils
- bind the GNU flavor for standard utils, like
gfindto the normal version, e.g.find. - iterm2
- while fully configured below, configures the interaction with the MacOS application, iTerm2.
- 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
diredin Emacs? quick-look- To view a file
- pyenv
- Call the
pyenv initand whatnot. - zbell
- To beep when a long running command has completed. Similar to my
beepcommand.
To have a plugin install, add its name to the plugins array variable before we source the OMZ script:
plugins=(colorize direnv gnu-utils iterm2 macos mise virtualenv zbell zsh-syntax-highlighting)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:
if hostname | grep AL33 >/dev/null
then
plugins+=(ansible argocd docker helm kubectl)
fiI 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 thought of this idea. We first need to add his plugin to the list of supplied plugins, so do this once:
git clone https://github.com/ivan-cukic/zsh-per-project-history ~/.oh-my-zsh/plugins/per-project-historyHis 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:
declare -a PER_PROJECT_HISTORY_TAGS
export PER_PROJECT_HISTORY_TAGS=(.envrc .git)
declare -r PER_PROJECT_HISTORY_TAGSwe just need to add per-history to the list of plugins:
plugins+=(per-project-history)Now that I’ve filled in the plugins variable, load OMZ and the plugins:
source $ZSH/oh-my-zsh.shDo 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
zstyle ':omz:update' mode auto
zstyle ':omz:update' frequency 13We’ll Check every 13 days.
Either keep it simple:
PS1='%(?.%F{green}.%F{red})$%f%b 'Oh use the absolute over-the-top bling associated with Oh My Zshell’s themes, like:
ZSH_THEME="robbyrussell"I keep the prompt simple since all of the gunk we typically put in a prompt is better placed in iTerm2’s Status Bar.
When using Homebrew on a Mac, we need to add its PATH and environment variables. This is typically done by running the command:
eval $(brew shellenv zsh)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:
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
fiSeems that if I want the GNU versions (instead of the old ones supplied by Apple), I have to do it myself:
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
fiAnd linking all the GNU libraries:
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\"
fiAnd pull in all the results into the ~/.zshenv file (why yes, this could be inlined):
[[ -f $HOME/.zshenv_brew ]] && source $HOME/.zshenv_brew
[[ -f $HOME/.zshenv_gnu ]] && source $HOME/.zshenv_gnu
[[ -f $HOME/.zshenv_lib ]] && source $HOME/.zshenv_libOn Mac systems, I like the iTerm2 application, and we can enable shell integration, either via the old school way, or just rely on the plugin above:
test -e "${HOME}/.iterm2_shell_integration.zsh" && source "${HOME}/.iterm2_shell_integration.zsh"Also, while use the title command to change the Terminal’s title bar, don’t let the prompt or other Zshell features do that:
DISABLE_AUTO_TITLE="true"Favorite feature is the 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.
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)
}Add the following:
function pycontext {
local version venvstr
version=$(pyenv version-name)
venvstr=$(echo $VIRTUAL_ENV | sed 's/.*.venv\///')
echo "🐍 $version:$venvstr"
}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:
bindkey -eWhere be the emacsclient? It should, at this point, be in our path.
And how should we call it?
export EMACS_SOCKET_NAME=personalWhich needs to be overwritten on my Work computer:
if hostname | grep AL33 >/dev/null
then
export EMACS_SOCKET_NAME=work
fiThe EDITOR variable that some programs use to edit files from the command line:
export EDITOR="emacsclient --tty"
export VISUAL="emacsclient --create-frame"With these variables defined, we can create simple aliases:
alias e="$EDITOR"
alias te="$EDITOR"
alias ee="emacsclient --create-frame"
alias eee="emacsclient --create-frame --no-wait"To work with VTerm in Emacs, we need to create this function:
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
}This allows us to execute Emacs commands:
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"
}For instance:
if [[ "$INSIDE_EMACS" = 'vterm' ]]
then
alias clear='vterm_printf "51;Evterm-clear-scrollback";tput clear'
vim() {
vterm_cmd find-file "$(realpath "${@:-.}")"
}
fiAssuming we’ve installed lsd, let’s make an alias for it:
if which lsd >/dev/null
then
alias ls=lsd
fiThe 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.
if whence cless >/dev/null
then
alias less=cless
alias cat=ccat
fiOther alternate improvements, like fd should be called directly.
And an abstraction for transitory endpoints over SSH:
alias ossh="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o loglevel=ERROR"And other ones that I use:
alias os=openstack
alias k=kubectlFor sensitive work-related environment variables, store them elsewhere, and load them:
test -e "${HOME}/.zshenv-work" && source "${HOME}/.zshenv-work"To let us know we read the ~/.zshrc file:
echo "🐚 ZShell Session"