Migrating to ZSH from Bash
Recent news that upcoming macOS 10.15 will use zsh as its default shell nudged me once again to look at it. I thought about giving ZSH a chance few times in the past, but was always intimidated by the prospect of migrating an already convenient bash setup to it, also dominating use of oh-my-zsh across my friends and coworkers gave an impression that zsh is impossible to use without such monstrous extensions.
Finally I decided to give it a try and see whether it is possible to make a low-effort zsh setup that would be not that much different from my bash setup, giving me a better chance to stick with it. Another important aspect is that I want such setup to be easy to understand and not to use external projects like oh-my-zsh.
Starting point: bash setup
Here's how my bash is set up and what I wanted to keep the same:
- emacs-style handling of command line (ctrl-a moves cursor to beginning of line, ctrl-e to end, etc);
- ability to cd into subdirectories from predefined set of directories (CDPATH);
- customized low-key prompt, showing exit code of previous command if it's not zero;
- prompt integration with git, mainly to display name of active branch;
Iterating over new setup
Starting with empty .zshrc
file my main priorities were getting familiar key bindings to move around the input line and more useful prompt.
While the internet was full of tutorials on how to set bindings key-by-key, it was far more easier to achieve what I wanted: man zshroadmap
(and man zshzle
) provided the correct way to do this:
bindkey -e
Having familiar key bindings in place, prompt was the next step. Here, man zshmisc
(specifically its EXPANSION OF PROMPT SEQUENCES section) was of big help.
In my bash prompt final component of the current directory is shown, followed by ¶ char, so initial step was to get it was this:
PROMPT='%1~ ¶ '
Pleasingly, zsh color abilities are much nicer to deal with than in bash, where one have to jiggle raw ascii escape sequences, so prettified version of the prompt was not that ugly:
PROMPT='%B%F{white}%1~%f%b %F{green}¶%f '
Here %B
and %b
work as bold opening and close "tags" (think of it like html's <b>
and </b>
respectively), and %F{color}
and %f
as opening and close "tags" for colors.
Next one was displaying non-zero exit code of previously executed command. Here CONDITIONAL SUBSTRINGS IN PROMPTS section of manual page was helpful, specifically ternary expression %(x.true-text.false-text)
. Applied to exit status of the last command it looks like %(?.zero-exit-code-text.non-zero-exit-code-text)
. The return status of the last command is expanded in prompt with %?
placeholder, and since zero exit code shouldn't be reported at all (empty string), final expression takes the form %(?..%? )
. With some colors sprinkled on top, prompt becomes this:
PROMPT='%B%F{red}%(?..%? )%f%F{white}%1~%f%b %F{green}¶%f '
This is almost all I need, with exception of reporting git repository branch. Here I checked /Library/Developer/CommandLineTools/usr/share/git-core/
directory, and though it had git-prompt.sh
file I used in bash, there were no corresponding file for zsh. Prepared that I'd need to come up with some scripting myself, I checked internets once again to see whether there's any alternative than resort to frameworks like oh-my-zsh, and one keyword popped up: vcs_info
. It appears that recent versions of zsh are shipped with built-in framework to work with most VCSes!
Detailed information on this framework can be found in man zshcontrib
, after reading its examples and following through descriptions of default formatting directives, I came up with the following setup mimicking what I had for bash:
autoload -Uz vcs_info
zstyle ':vcs_info:*' enable git
zstyle ':vcs_info:*' formats ' %F{cyan}(%b)%f'
zstyle ':vcs_info:*' actionformats ' %F{cyan}(%b|%a)%f'
This configures vcs_info
subsystem to only work with git repositories and skip logic for all other VCSes, and apply desired formatting. The next step was plumbing this to prompt.
Manpage example showed that vcs_info
puts details into variable that can be put as ${vcs_info_msg_0_}
into PROMPT, but expansions of such variables should be explicitly activated:
setopt PROMPT_SUBST
PROMPT='%B%F{red}%(?..%? )%f%F{white}%1~%f%b${vcs_info_msg_0_} %F{green}¶%f '
The last step tying this all together was automativally executing vcs_info
whenever needed. This was achieved by plugging it into precmd
shell hook:
precmd () { vcs_info }
That did the trick, now my prompt behaved exacly the same way as in bash.
Another quality-of-life change was having autocompletions. This was done as recommended by manpage:
autoload -Uz compinit
compinit
Other things like setting variables, some helper functions and CDPATH feature to quickly cd into subdirs of predefined directories worked exactly the same as in .bashrc
.
macOS Terminal app integration
macOS Terminal app has an option to make new windows/tabs open with the same working directory as the current window/tab. This feature only works if shell notifies Terminal app on its current working directory:
Though it states that it needs a "file:" URL with properly percent-encoded path, it seems that the following shell command works fine for this:
printf '\e]7;%s\a' $PWD
Now to make zsh issue this command each time directory changes, it can be put into chpwd
hook:
chpwd () { printf '\e]7;%s\a' $PWD }
There's one small caveat with chpwd
hook though: if new window/tab started with non-default directory and then we start another tab from it without changing directory first, that newly started tab will get default directory, as the previous tab will never run chpwd
hook as it didn't change directory. This can be resolved by running command from above not within chpwd
hook, but from precmd
hook, which is executed before each prompt. Combined with already existing vcs_info
it becomes:
precmd () { vcs_info; printf '\e]7;%s\a' $PWD; }
Resulting zsh setup
After above changes I get what I wanted: zsh setup which has all features from my bash setup, feels familiar enough to eliminate friction and allows me to stick with it and learn zsh on my own pace.
Final .zshrc
file became this:
# Correctly display UTF-8 with combining characters.
if [ "$TERM_PROGRAM" = "Apple_Terminal" ]; then
setopt combiningchars
fi
disable log
export CLICOLOR=1
export GREP_OPTIONS='--color=auto'
export LESS='-iFXc'
export LANG="en_US.UTF-8"
export LC_ALL="en_US.UTF-8"
PAGER=less
export EDITOR=vim
export VISUAL=$EDITOR
bindkey -e
autoload -Uz compinit
compinit
HISTFILE=$HOME/.zsh_history
HISTSIZE=1000
SAVEHIST=3000
setopt AUTO_CONTINUE
setopt HIST_IGNORE_DUPS
setopt APPEND_HISTORY
CDPATH=.:$HOME:$HOME/Repositories
autoload -Uz vcs_info
zstyle ':vcs_info:*' enable git
zstyle ':vcs_info:*' formats ' %F{cyan}(%b)%f'
zstyle ':vcs_info:*' actionformats ' %F{cyan}(%b|%a)%f'
precmd () { vcs_info; printf '\e]7;%s\a' $PWD; }
setopt PROMPT_SUBST
PROMPT='%B%F{red}%(?..%? )%f%F{white}%1~%f%b${vcs_info_msg_0_} %F{green}¶%f '