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.
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 $SHELL /bin/zsh
If you ever want to switch back, simply use the same
chsh command with
bash in place of
$ 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
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
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.
# 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.)
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.
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.
alias lr='ls -hartl'
source on the file, the alias is available in the current shell:
$ source ~/.zshrc $ lr <files/directories listed as in ls -hartl>
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:
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.)
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).
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]$
#!/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.shfor 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
ekillis written in Bash or Ruby or C, so calling it
ekill.shunnecessarily 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.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
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
$ 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.)
$ 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.
. . . # 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.
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
[~]$ cd ~/repos/website [website (master)]$ git checkout ma⇥ # autocompletes to 'master' Already on 'master'
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 fooat the command line. If the
rmcommand 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_fragmentif 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
ls -rtlfor 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
-hoption some years ago.
/bin/bashis still present on the system, as you can verify by running
ls /bin/bash. As a result, the original Bash version of
ekillwould work fine even in a terminal shell running Zsh. The example in Listing 6 is included mainly for pedagogical purposes.