Using Z Shell on Macs with the Learn Enough Tutorials
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
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
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.
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.
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 deletions
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.
~/.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 .bashrc
and .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.
~/.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.)
~/.bashrc
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).
~/.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.
~/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 ifekill
is written in Bash or Ruby or C, so calling itekill.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 fromekill.sh
toekill.rb
toekill.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.
~/.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.
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.
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.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
.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./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.git-completion.zsh
file itself.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).