zsh is a superset of the venerable Bash shell. It has first class functions, modular enhancements, comes pre-installed on Mac OS X, and offers fantastic tab completion. As a jump start to getting a working zsh environment in place, Matthew pointed us toward oh-my-zsh, a framework with tremendous community support. oh-my-zsh offers modular zsh plug-ins, Git repository information in your prompt, a customizable right-side prompt, themes, and more.
By cloning the oh-my-zsh project on Github, selecting one of the themes, and changing your default shell to zsh you can have a flexible and powerful new shell in a matter of minutes. With a little judicious Googling and some experimentation it’s easy to develop a shell environment that’s tuned to your style of working. Over time I built up my prompt (the most visible portion of the environment) to display interactive version control status information from Git, Mercurial (hg), or Subversion (svn) when the current directory was under version control. My right-prompt showed the current RVM managed Ruby and Pythonvirtual environment. I could also see at a glance what user I was, on which machine, and the current directory path. Oh, and all in vibrant colors.
Over time the features of zsh that I’ve come to rely upon the most are the tab completion and some of the directory movement commands. However, recently I had become dissatisfied with the delay when opening a new shell in a tab or window. A several second delay wasn’t unusual. In my efforts to pack as much information as possible into the prompt I created a resource intensive monster that was increasingly annoying. I began to question the need for dynamic svn repository information in the prompt when I rarely worked with Subversion any more.
Additionally, I had switched away from the main oh-my-zsh project to a fork created by Steve Losh. He added a function that exposed more status states for Git repositories. And I was using a Python script of his that exposed Mercurial repository information for the prompt. My prompt was capable of informing me of many things based on the current context, but the underlying structure had grown large and burdensome. Two weeks ago I decided to pare down my zsh setup, and in doing so I hoped to achieve two goals. One, I wanted to have a much better understanding of what was actually driving my shell’s environment. Two, I wanted to have a simpler, and hopefully quicker loading, setup.
##Lobotomizing My oh-my-zsh theme
My first thoughts were to remove some of the less used information from my zsh prompt theme. I had added svn information since we use svn at work. However my role there shifted and I have no real need (at the moment) for Subversion reporting in my prompt.
It was the matter of a couple minutes work to comment out the Subversion related code from my theme file and source .zshrc. The subjective measure of my prompt’s loading time wasn’t much improved.
Next up I commented out the Mercurial aspects of the prompt. I stared out using hg a few years ago but, following the herd, I’ve moved all my projects to Git and Github. Again, my prompt didn’t really seem to be any faster.
Taking a new tack, I hunted around and found a completely new zsh theme I liked the general looks of called Fino. It had basic Git repository status information, RVM or rbenv Ruby information, and Python virtualenv reporting. With it in place in my .zshrc file the prompt was changed but I started getting error messages. zsh: command not found: rvm_ruby with every command issued.
Googling the message took me to a number of stackOverflow threads and some pages about Mac OS X’s path_helper. Something about the new theme wasn’t getting along with the rest of my zsh environment.
##A Brief Digression into path_helper
path_helper examines the contents of /etc/paths.d and adds entries to your $PATH. The consensus seems to be that while it’s a good idea, it may not be implemented in the best fashion.
While I was reading all the pages I could find about path_helper, I wasn’t seeing the forest for the trees. RVM, and rbenv, are dependent upon their libraries being first in the PATH. If the PATH is altered after those key libraries are setup, the order may no longer be correct.
Frustrated by my lack of success in either altering my theme or incorporating the Fino theme, and not getting the clue the path_finder Google results were giving me, I reverted to my original theme and changed my primary text editor from TextMate to Sublime Text 2. When in doubt, go shave a yak.
##Shaving the Sublime Text 2 Yak
I tend to collect open tabs in my browser. I usually have 30 or 40 open tabs and sometimes as many as 100. Periodically I scrub through them all and “do something” with at least some of them. Mentally stuck on the zsh: command not found error, I wandered through my open tabs and stopped on the nettuts plus Perfect Workflow in Sublime Text free course. Next thing you know I’ve dusted off my previously installed, but never really used, Sublime Text 2 application and started adding plug-ins and a color theme or two. I wasn’t solving my zsh theme issues, but I was using new and different tools to look at the reluctant code. When you can’t see the solution, change glasses, right?
With a newly configured and tweaked editor ready for use I returned to the original project.
##Struggles with RVM
There are two Ruby environment managers: RVM and rbenv. I’ve used both in the past, although RVM has been my primary one for longer. The framework I use for my Websites is Octopress, which is Ruby-based. I initially installed RVM to setup and configure the required Ruby and Gems to get my site infrastructure working.
Since the error message I was getting seemed to be pointing a finger at RVM I decided to switch to rbenv. Rather than solve the problem, I thought, I’ll just switch environment managers. Easy as pie.
Rather than jeopardize my working-if-slow-loading environment I decided to use a different machine for my rbenv experiment. I have access to an older, but still serviceable aluminum MacBook Pro. I had recently formatted its hard drive and installed Linux Mint on it just to explore that distribution. I wiped the drive and installed Mountain Lion and rebuilt the machine to match my personal machine as closely as possible with one key difference: I setup rbenv and not RVM.
At this point, with a test bed that I could afford to be risky with, I stepped back from trying to get oh-my-zsh working and considered switching to the other zsh framework I had read about.
##Looking at zshuery
zshuery is a much lighter-weight framework than oh-my-zsh. Based on the commit activity it isn’t nearly as frequently updated either. My concerns with it ran deeper than a seeming lack of support. Without digging into both oh-my-zsh and zshuery and comparing them at a nuts and bolts level I wouldn’t know what one had that the other didn’t. What would I gain with zshuery? What would I lose and have to recreate?
While starting out with oh-my-zsh was fantastic, it came with a hidden cost. Since I knew nothing about zsh prior to using the framework I had no way of knowing what was raw zsh and what was framework-added. To me, oh-my-zsh was zsh.
##We Interrupt this Program for a Brief Word About Writing Sorts
In my college internal data structures course we wrote a sort. After a lecture discussion the ins and outs of the algorithm I wrote my very own bubble sort. The following lecture the professor showed us the JCL sort utility. At first I was upset that I spent all the time and effort to reinvent the sort wheel, but later I came to realize that I had a better understand and appreciation for sorts as a result of the exercise.
My zsh experience was the opposite of my sort experience. I started zsh with the polished, design-decisions-already-made-for-you solution. If I really wanted to get to the bottom of my zsh shell slowness I would need to build my own environment, eschewing frameworks.
Github is a wonderful place as people store all sorts of things there, open to the public. In my sifting of Google zsh search results I stumbled onto a repository cheekily named ze-best-zsh-config. At first glance it looked well laid out. The various aspects of the configuration, options, exports, the prompt, history, aliases, and functions, were all isolated in their own .zsh files, and the resulting components brought together in the .zshrc file.
Here was a starting point for my own, hand-rolled zsh environment. I cloned the repository and started working through the .zsh files it contained line-by-line to understand them.
##Switching to rbenv
Installing and using rbenv isn’t any harder than installing and using RVM. Reflecting the current active Ruby in my shell’s right-prompt (RPROMPT) proved to be one of the hardest parts of this project.
I liked the code from the Fino theme for interrogating the host machine for either RVM or rbenv and then determining what Ruby was active. Copying that code to my new prompt.zsh file didn’t work however. The right-prompt either showed nothing at all, or the line of code that should have resolved to the Ruby name. After an evening of Googling and trial and error I finally posted a question on stackOverflow. The next morning I not only had a working answer, I had an explanation of how it worked. I love the Internet.
##setopts, Exports, Aliases, Functions, Colors, Completions, and History
With my prompt sorted it was time to understand all the configuration options available to me, and setup my environment. Working from the setops included in ze-best-zsh-config, I looked everything up. Some I kept, others I changed, a few I dropped. Next I read through the options defined in zshuery.
I performed similar examinations of the colors, completions, history, exports, aliases, and functions .zsh files. I merged my existing functions and aliases into the starter files, and incorporated some more from zshuery.
From zshuery I liberated the idea of testing the host machine’s operating system (uname) and setting an attribute. This allows me to have options that are OS specific.
##More path_helper Adventures
During the due diligence process I managed to break the right-prompt. Suddenly the zsh: command not found error was back. Somewhere along the line I had managed to break the path. After much searching and reading I finally created a .zshenv file and copied the code necessary to run path_helper there. I then altered this code to completely clear the path, ensuring that I had no unexpected cruft there. This article about path_helper was instrumental in my getting the problem solved.
I also placed the path and shim setup for rbenv into the .zshenv file. With this file in place the zsh: command not found error disappeared. Hopefully for good.
##Putting it All Together
My new .zshrc file is just a list of .zsh files that get sourced.
And those files each contain a portion of the overall configuration. I suppose they could all be combined into one large file, but I like breaking them up into logically related components.
Here is where we override path_helper by calling it again ourselves. The PATH is cleared first and then rebuilt. The result is far less duplication of directories. rbenv is also setup here.
These tests allow me to later isolate exports, or setopt statement based on the host machine’s operating system.
A colorful shell is a happy shell. But the escaped codes are arcane and miserable to work with. Let’s give them some readable names.
There are a dizzying array of options available. These are a start.
The PATH is altered here after having been cleared and started in .zshenv.
The visible tip of the zsh environment iceberg. Lots going on here.
The Python virtual environment, if one is active, is captured for display in the right-prompt.
The prompt character displayed is normally a $ or maybe a >. I’m using the symbols Steve Losh had in his extravagant zsh prompt
box_name is a clever way to show your preferred name for the machine you are on. Just create a ~/.box-name file and put the name you want there. Or, on Mac OS X, use the sudo scutil --set HostName <your-name-here> command.
A variety of variables are set up for Git repository status reporting, and then there are some functions to interrogate Git. This information is shown in the main prompt.
The current Ruby active is determined by interrogating either RVM or rbenv, which ever is present on the machine. This function, _update_ruby_version(), has been added to the chpwd_functions array like so: chpwd_functions+=(_update_ruby_version). This means it is only executed when the directory changes.
Finally there is a function to get the name of the current working directory.
The primary prompt shows the username, box name, current working directory, and git branch and status (if present). The prompt is preceded by a blank line, and places the prompt character on a line of its own. This makes reading the console far easier.
zsh provides an error prompt, which I’m using to spell correct commands. The export SPROMPT line handles this.
Lastly the troublesome right-prompt or RPROMPT. Here the Python virtual environment information and active Ruby information is displayed. One of the setops defined above, setopt transient_rprompt causes the RPROMPT to only appear on the current, most recent, prompt. I’m not sure yet if I like this, but it does make the console seem less cluttered.
Tab completion is like magic, and the incantations in the completions.zsh file are magic to me. I need to learn more about these.
I’ve had some of these aliases for as long as I’ve been working on *nix-based machines. They are as much a part of my muscle memory as tying my shoe or combing my hair. Since zsh tries to spell correct commands, the nocorrect section is at the top. Here is where you put commands that you don’t want spell checked every time they are used.
Create your own keybindings for fun and profit. Still more material to learn better.
Some of these are my own creation, others I’ve picked up along the way. The most recent addition, path() displays your current PATH beautifully. Immensely helpful when sorting out path issues.
The variables control how the command history is managed.
This file controls the use of the precmd, preexec and postcmd features. I’m not entirely sure I understand all there is to know about this. It’s on the list to investigate more thoroughly.
z tracks your most used directories based on ‘frequency’. By typing z and part of a directory name I can quickly jump to that part of the file system. Obviously the longer you have it installed the better it performs. I highly recommend it.
My original RVM + oh-my-zsh configuration had the path and setup for RVM contained in the .zlogin file. The new rbenv + zsh configuration has the rbenv setup in .zshenv. I need to read more on these two files and decide which ought to be used.
I need to explore the subject of bindkeys more to fully understand what they are and how I can benefit from them. I also need to learn about precmd, preexec, and postexec. There use in my setup was copied from ze-best-zsh-config. And zsh_hooks.
My testbed machine has 100 GB of free space on it’s hard drive that I am planning on using to install Linux Mint. Once that OS is setup and running I want to use my dotfiles repository, including the new zsh configuration, there. As I get things setup and working under Linux I’m sure there will be Linux-specific tweaks to add to the configuration.
In the end I’m not sure I’ve made my prompt load any faster. I certainly understand the processes and configuration behind it far better than before. I haven’t taken the step of removing all the entires from /etc/paths.d and placing them directly into my $PATH. This feels like something that would need constant minding and I’m not ready for that yet.
I’ll need to live with this setup for a while to discover things I’ve lost by leaving oh-my-zsh. Hopefully nothing to drastic or hard to configure myself. It’s been an interesting couple of weeks sorting this out. I have a greater appreciation for my shell now, and a greater understanding of what makes it tick.