Using Z Shell on Macs with the Learn Enough Tutorials

From Bash to Zsh (and back again)

Apr 24, 2020 • posted by Michael Hartl

This post is designed to cover aspects of Z shell for students of the Learn Enough tutorials who use macOS Catalina and later. Read on to find out what all of that means…

She sells C shells…

The first Learn Enough tutorial, Learn Enough Command Line to Be Dangerous, introduces the essential concept of a command-line shell:1 a program used to issue text commands to the computer. Although seemingly more primitive than more polished graphical user interfaces (GUIs), a command-line interface (CLI) is actually a powerful and flexible tool that should be in every technical person’s toolkit.2

images/figures/seashells

All of the Learn Enough tutorials use a shell known as Bash, which is probably the most common shell program in the world. (Its name is an acronym for “Bourne-again shell”, a reference to the earlier Bourne shell and a pun on the religious term “born again”.) Bash is standard on practically every Unix and Linux system in the known universe, and for many years was also the default on Apple macOS.

As of macOS Catalina, though, the default shell on Macs is a closely related but not entirely compatible program called Z shell (or Zsh). Although Bash is still available on macOS, some Mac users may prefer to use the default shell supported by Apple.3

images/figures/catalina

By the way, there’s nothing wrong with Bash—Apple’s decision was based on a licensing issue and is not a reflection on Bash as a technology. As noted, Bash is still available on Macs, and even if Apple decides to completely remove Bash in a future version of macOS it will still be easy enough to reinstall it using a package-management tool like Homebrew (as covered in the free tutorial Learn Enough Dev Environment to Be Dangerous).

Because aspects of this shell issue pervade the Learn Enough tutorials, it’s convenient to collect all the diffs in one place. Thus, this post—a one-stop shop for how to do all Learn Enough tutorial shell-related tasks in Z shell instead of in Bash.

Change shell

My first encounter with this macOS shell issue came immediately after upgrading to Catalina; some of you may have seen it as well. Upon firing up my terminal shell, I was greeted with the alert shown in Listing 1.

Listing 1: A macOS Bash/Zsh alert.
The default interactive shell is now zsh.
To update your account to use zsh, please run `chsh -s /bin/zsh`.
For more details, please visit https://support.apple.com/kb/HT208050.

[~]$

Hoo, boy. If you’d asked me to list the top 1000 changes that might affect the Learn Enough tutorials, “Apple changes the default shell for macOS” would not have been on it.

Now, I got the alert because I was already running Bash—if you got a Mac after Catalina came out, you’re probably running Z shell already, and won’t see the alert from Listing 1. If you want to follow the Learn Enough tutorials, that means you should either switch to Bash, as shown in Box 1 (recommended), or refer to this post every time a shell-related issue comes up. Or you can just start following any of the tutorials as written—as soon as a shell-related issue does come up, there’s a note in the text and a link to this post.

The good news is that Z shell is not all that different from Bash (and indeed in large part is based on it). Thus, the differences as far as the Learn Enough tutorials are concerned aren’t particularly big. Throughout the rest of this post, we’ll take a look at the exact changes needed to make each Learn Enough tutorial compatible with Zsh instead of Bash.

Box 1. How to change shells

This is a quick mini-tutorial on how to change your shell from Bash to Zsh (and vice versa). The main technique is to use the chsh program, which stands for “change shell”. Note that this procedure is entirely reversible (as described below), so there is no need to be concerned about damaging your system.

The first step is to confirm the identity of your current shell program using the echo command (as covered in Learn Enough Command Line):

  $ echo $SHELL
  /bin/bash

This prints out the $SHELL environment variable, which in most cases prints out the value of the current shell—in this case, Bash. (In rare cases, $SHELL may differ from the current shell, but the procedure below will still correctly change from one shell to another.) To change to Zsh, simply follow Apple’s suggestion from Listing 1 and run chsh with the -s (“newshell”) option:

  $ chsh -s /bin/zsh

You’ll almost certainly be prompted to type your system password at this point, which you should do. Then completely exit your shell program using Command-Q and relaunch it.

Once you’ve followed the steps above, you’ll be running Z shell instead of Bash, as you can confirm with echo:

  $ echo $SHELL
  /bin/zsh

If you ever want to switch back, simply use the same chsh command with bash in place of zsh:

  $ chsh -s /bin/bash

As with the previous case, type in your password and then restart your terminal program. The result will be a restoration of your previous settings:

  $ echo $SHELL
  /bin/bash

Learn Enough Command Line

Because Z shell and Bash are so similar, Learn Enough Command Line to Be Dangerous is almost identical when using Zsh. There are only two minor differences.

The smaller difference is a tiny change in the error message output when changing into a directory that doesn’t exist (an example in Section 4.4). In Bash, it looks like this:

# Bash error message when cd-ing into a nonexistent directory.
# Do not actually type; this is just an example.
$ mkdir foo
$ mv foo/ bar/
$ cd foo/
-bash: cd: foo: No such file or directory

Zsh uses a slightly different error message, which omits the name of the shell and places the name of the nonexistent directory at the end of the message instead:

# Zsh error message when cd-ing into a nonexistent directory.
$ mkdir foo
$ mv foo/ bar/
$ cd foo/
cd: no such file or directory: foo/

A more substantive difference involves the default behavior of renaming, copying, and deleting files (Section 2.3) and directories (Section 4.4). Bash systems typically alias the potentially dangerous commands rm (remove), mv (move), and cp (copy) to versions that use the -i option, which requires confirmation for any potential deletions:

# Bash example.
$ touch foo
$ rm foo
remove foo? n
$ ls foo
foo

(Note that we didn’t have to type rm -i due to the alias; rm alone is sufficient.) This behavior can be overridden by the -f (force) option:

# Bash example continued.
$ rm -f foo
$ ls foo
ls: foo: No such file or directory

The same patterns apply to the wildcard operator *:

# Bash example.
$ touch foo.baz
$ touch bar.baz
$ rm *.baz
remove bar.baz? n
remove foo.baz? n
$ rm -f *.baz
$ ls *.baz
ls: *.baz: No such file or directory

To ensure that this behavior is the same in Zsh, two changes are needed. First, we need to add the corresponding aliases, which fortunately follow the same syntax as Bash:4

# Avoid accidental deletion
alias rm='rm -i'
alias mv='mv -i'
alias cp='cp -i'

Second, by default Zsh requires confirmation when using rm with the wildcard operator * even when the -f option is present, which is really annoying since the whole point of using -f is to skip such confirmations. Fixing this involves setting an option to make star-related operations “silent”:

# Prevent rm -f from asking for confirmation on things like `rm -f *.bak`
setopt rm_star_silent

These commands go in a special Zsh configuration file called .zshrc in your shell’s home directory (often written using the tilde symbol ~). If you’re just beginning and don’t yet know how to use a text editor, you can simply run the following command to set things up properly on your system (it’s pretty advanced, so don’t worry about the details):

$ source <(curl -sL https://cdn.learnenough.com/zsh_fix_deletion)

This command simply appends the proper lines to the end of .zshrc, so it is safe to run no matter what might already be in the file.5

If you’re comfortable using a text editor, you can edit the configuration file directly by adding the relevant lines to ~/.zshrc, as shown in Listing 2.

Listing 2: Adding commands to improve Zsh’s deletion behavior. ~/.zshrc
# Avoid accidental deletion
alias rm='rm -i'
alias mv='mv -i'
alias cp='cp -i'
# Prevent rm -f from asking for confirmation on things like `rm -f *.bak`
setopt rm_star_silent
.
.
.

(Here the vertical ellipsis indicates any other content in the .zshrc file and should not be typed literally.)

Text Editor

There are three main appearances of shells in Learn Enough Text Editor to Be Dangerous. The differences between Bash and Zsh in the three cases are nonexistent, tricky, and trivial.

Aliasing

As we saw in Listing 2, the alias syntax is the same in Zsh and Bash. As a result, the example in Learn Enough Text Editor is identical as well (apart from using .zshrc in place of .bash_profile). In particular, Section 1.4 defines an alias called lr for the useful command ls -hartl, which lists files and directories using human-readable values for the sizes (e.g., 29K instead of 29592), including all of them (even hidden ones), ordered by reverse time, long form.6 The result is shown in Listing 3.

Listing 3: Defining a Bash alias. ~/.zshrc
alias lr='ls -hartl'

After running source on the file, the alias is available in the current shell:

$ source ~/.zshrc
$ lr
<files/directories listed as in ls -hartl>

Prompt

Another shell change introduced in Learn Enough Text Editor to Be Dangerous involves customizing the prompt shown at the command line. In particular, an exercise in Section 2.7.1 arranges for the prompt to show the current working directory. For example, my prompt currently looks like this:

[learnenough-news (master)]$

This is because my shell is currently in the /Users/mhartl/repos/learnenough-news directory, and the prompt is designed to show only the last part for brevity. (Meanwhile, (master) refers to the current Git branch, which we’ll discuss in the next section.)

The way to arrange this behavior in Bash appears in Listing 4. (The next two listings omit the other contents of the files for clarity.)

Listing 4: The Bash lines needed to customize the prompt. ~/.bash_profile
alias lr='ls -hartl'
# Customize prompt to show only working directory.
PS1='[\W]\$ '

Unfortunately, Z shell uses a different prompt syntax, which required a bit of googling on my part to figure out. The result appears in Listing 5 (with other lines omitted).

Listing 5: The Zsh lines needed to customize the prompt. ~/.zshrc
alias lr='ls -hartl'
# Customize prompt to show only working directory.
PS1='[%1~]$ '

With the code in Listing 5, running source should result in the desired behavior:

$ source ~/.zshrc
[~]$ cd ~/repos/website
[website]$

Shell scripts

The final shell example in Learn Enough Text Editor to Be Dangerous is defining a shell script called ekill (for “escalating kill”), which kills a Unix process using signals of escalating urgency.

The Z shell version is identical to the Bash version apart from the shebang line #!/bin/zsh, which identifies the shell to be used to execute the command. The result appears in Listing 6.

Listing 6: A custom escalating kill script. ~/bin/ekill
#!/bin/zsh

# Kill a process as safely as possible.
# Tries to kill a process using a series of signals with escalating urgency.
# usage: ekill <pid>

# Assign the process id to the first argument.
pid=$1
kill -15 $pid || kill -2 $pid || kill -1 $pid || kill -9 $pid

It’s worth noting that the name of the script defined in Listing 6 is identical to the one defined in Learn Enough Command Line. This dovetails with the philosophy espoused in the corresponding section:

Although some people would use a name like ekill.sh for shell scripts like this one… using an explicit extension on a shell script is a bad practice because the script’s name is the user interface to the program. As users of the system, we don’t care if ekill is written in Bash or Ruby or C, so calling it ekill.sh unnecessarily exposes the implementation language to the end-user. Indeed, if we wrote the first implementation in Bash but then decided to rewrite it in Ruby and then in C, every program (and programmer) using the script would have to change the name from ekill.sh to ekill.rb to ekill.c—an annoying and avoidable complication.

It’s also worth noting that the shell program used in a script like Listing 6 is orthogonal to the shell being used in your terminal. As long as the directory of the file in Listing 6 (in this case, ~/bin) is on your system path, the command will work as long as the executable in the shebang line is present on your computer. In other words, even if you’re using Bash for your terminal shell, the Zsh version of the ekill command (as defined in Listing 6) will work as long as /bin/zsh exists on your system, which it should:

$ ls /bin/zsh
/bin/zsh

Because the result of ls here is nonempty, we can be confident that ekill will continue to work even if we switch back to Bash. (Question: Was the change to #!/bin/zsh in Listing 6 necessary, or would the Bash version using #!/bin/bash have worked fine under Z shell?)7

Git

The two shell-related tasks in Learn Enough Git to Be Dangerous involve adding the Git branch to the prompt and adding tab completion for Git branches. Both are small changes but required a large amount of googling and experimentation to solve (an application of technical sophistication if ever there was one).

We’ll start with the preparation for Git tab completion, which involves downloading a git-completion.zsh file and putting it in a hidden .zsh directory:8

$ mkdir ~/.zsh
$ curl -o  ~/.zsh/git-completion.zsh \
>      -OL https://cdn.learnenough.com/git-completion.zsh

(Note here that you should type the backslash character \ in the middle line, but you shouldn’t type an angle bracket > in the final line. The \ is used for a line continuation, and after hitting return the > will be added automatically by the shell program.)

Next, download the code needed for the prompt and change its mode to x (“executable”):9

$ curl -o ~/.git-prompt.sh -OL https://cdn.learnenough.com/git-prompt.sh
$ chmod +x .git-prompt.sh

We can then achieve both goals of this section by adding the prompt and Git completion instructions to .zshrc, as shown in Listing 7.

Listing 7: Adding the Git branch name and tab completion. ~/.zshrc
.
.
.
# Prompt configuration
source ~/.git-prompt.sh
setopt PROMPT_SUBST
PS1='[%1~$(__git_ps1 " (%s)")]\$ '

# Git completion
fpath=(~/.zsh $fpath)
autoload -Uz compinit && compinit

Don’t ask me how any of that works—I have no idea.

images/figures/no_idea

Finally, source the file as usual:

$ source ~/.zshrc

If you navigate to the directory of a Git project (say, the ~/repos/website directory created in Section 1.2), you should see the branch name in the prompt and be able to check out the master branch by hitting git checkout ma⇥ (where indicates the tab key):

[~]$ cd ~/repos/website
[website (master)]$ git checkout ma⇥    # autocompletes to 'master'
Already on 'master'

That’s all, folks!

That’s it! With the changes described above, you can now follow the full Learn Enough sequence using Z shell instead of Bash, as well as switch back and forth should that ever be required.

P.S. Access to all the Learn Enough tutorials (including the Ruby on Rails Tutorial) is currently 25% off as part of our “learn to code at home” All Access subscription discount.

1. The section title is a reference both to the C programming language commonly used to write shell programs and to the famous tongue-twister “She sells seashells by the sea shore.”
2. Seashell image used under the CC BY 2.0 license.
3. Image of Santa Catalina Island is in the public domain.
4. It’s possible that Z shell on new Macs comes configured with these aliases. To test this, run touch foo && rm foo at the command line. If the rm command asks for a confirmation, the alias has already been defined and you don’t have to add those aliases to your system.
5. Run curl -OL https://cdn.learnenough.com/zsh_deletion_fragment if you’re curious about which commands are used in the script. You’ll see that it uses the append operator >> to append to the end of .zshrc.
6. Having known about ls -a and ls -rtl for a while—which together yield the suggestive command ls -artl—one day I decided to add an “h”’ (for obvious reasons). This is actually how I accidentally discovered the useful -h option some years ago.
7. Answer: The change was unnecessary because /bin/bash is still present on the system, as you can verify by running ls /bin/bash. As a result, the original Bash version of ekill would work fine even in a terminal shell running Zsh. The example in Listing 6 is included mainly for pedagogical purposes.
8. There are other ways to do this that don’t require this exact directory configuration. This is just the structure recommended in the git-completion.zsh file itself.
9. The git-prompt.sh file is identical to the one used in Learn Enough Git to Be Dangerous, so if you followed that tutorial you might already have it (in which case you can skip this step, although it does no harm to repeat it).
MORE ARTICLES LIKES THIS:
learnenough-news , tutorials