Apple has announced that in macOS 10.15 Catalina the default shell will be zsh
.
In this series, I will document my experiences moving bash
settings, configurations, and scripts over to zsh
.
- Part 1: Moving to zsh
- Part 2: Configuration Files
- Part 3: Shell Options (this article)
- Part 4: Aliases and Functions
- Part 5: Completions
- Part 6: Customizing the
zsh
Prompt - Part 7: Miscellanea
- Part 8: Scripting
zsh
This series has grown into a book: reworked and expanded with more detail and topics. Like my other books, I plan to update and add to it after release as well, keeping it relevant and useful. You can order it on the Apple Books Store now.
Now that we have chosen a file to configure our zsh
, we need to decide on ‘what’ to configure and ‘how.’ In this post, I want to talk about zsh
’s shell options.
As I have mentioned in the earlier posts, I am aware that there are many solutions out there that give you a pre-configured ‘shortcut’ into lots of zsh
goodness. But I am interested in learning this the ‘hard way’ without shortcuts. Call me old-fashioned. (“Uphill! In the snow! Both ways!”)
In the previous post, I listed some features that I would like to transfer from my bash
configuration. While researching how to implement these options in zsh
, I found a few, new and interesting options in zsh
.
The settings from bash
which I want in zsh
were:
- case-insensitive globbing
- command history, shared across windows and sessions
Note:
bash
in this series of posts specifically refers to the version ofbash
that comes with macOS as/bin/bash
(v3.2.57).Note 2: Mono-typed lines starting with a
%
show commands and results fromzsh
. Mono-typed lines starting with$
show commands and results inbash
What are Shell Options?
Shell options are preferences for the shell’s behavior. You are using shell options in bash
, when you enable ‘trace mode’ for scripts with the set -x
command or the bash -x
option. (Note: this also works with zsh
scripts.)
zsh
has a lot of shell options. Many of these options serve the purpose of enabling (or disabling) compatibility with other shells. There are also many options which are specific to zsh
.
You can set an option with the setopt
command. For compatibility with other shells the setopt
command and set -o
have the same effect (set an option by name). The following commands set the same option:
set -o AUTO_CD
setopt AUTO_CD
The names or labels of the options are commonly written in all capitals in the documentation but in lowercase when listed with the setopt
tool. The labels of the options are case insensitive and any underscores in the label are ignored. So, these commands set the same option:
setopt AUTO_CD
setopt autocd
setopt auto_cd
setopt autoCD
There are quite a few ways to negate or unset an option. First you can use unsetopt
or set +o
. Alternatively, you can prefix with NO
or no
to negate an option. The following commands all have the same effect of turning off the previously set option AUTO_CD
unsetopt AUTO_CD
set +o AUTO_CD
unsetopt autocd
setopt NO_AUTO_CD
setopt noautocd
Any options you change will only take effect in the current instance of zsh
. When you want to change the settings for all new shells, you have to put the commands in one of the configuration files (usually .zshrc
).
Showing the current Options
You can list the existing shell options with the setopt
command:
% setopt
combiningchars
interactive
login
monitor
shinstdin
zle
This list only shows options are changed from the default set of options for zsh
. These options are marked with <D>
(default for all shell emulations) or <Z>
(default for zsh
) in the documentation or the zshoptions
man page.
You can also get a list of all default zsh
options with the command:
% emulate -lLR zsh
Some zsh Options I use
As I have mentioned before in my posts on bash
configuration, I prefer minimal configuration changes, so I do not feel all awkward and lost when I have to work on an ‘un-configured’ Mac.
These configurations are a personal choice and you should pick and choose your own. You can find a full list of zsh
options in the zsh
Manual or with man zshoptions
.
On the other hand, exploring the options allows us to explore a few useful zsh
features.
Case Insensitive Globbing
Note: ‘Globbing’ is a unix/shell term that refers to the expansion of wildcard characters, such as *
and ?
into full file paths and names. I.e. ~/D*
is expanded into /Users/armin/Desktop /Users/armin/Documents /Users/armin/Downloads
Since the file system on macOS is (usually) case-insensitive, I prefer globbing and tab-completion to be case-insensitive as well.
The zsh
option which controls this is CASE_GLOB
. Since we want globbing to be case-insensitive, we want to turn the option off, so:
setopt NO_CASE_GLOB
You can test this in the shell:
% ls ~/d*<tab>
In zsh
tab completion will replace the wildcard with the actual result. So after the tab you will see:
% ls /Users/armin/Desktop /Users/armin/Documents /Users/armin/Downloads
Using tab completion this way to see and possibly edit the actual replacement for wildcards is a useful safety net.
In bash
hit the tab key will list possible completions, but not substitute them in the command prompt.
If you do not like this behavior in zsh
then you can change to behavior similar to bash
with:
setopt GLOB_COMPLETE
Automatic CD
Sometimes you enter the path to a directory, but forget the leading cd
:
$ Library/Preferences/
bash: Library/Preferences/: is a directory
% Library/Preferences
zsh: permission denied: Library/Preferences
With AUTO_CD
enabled in zsh
, the shell will automatically change directory:
% Library/Preferences
% pwd
/Users/armin/Library/Preferences
This works with relative and absolute paths, including the ..
:
% ..
% pwd
/Users/armin/Library
% ../Desktop
% pwd
/Users/armin/Desktop
I have an alias
in my .bash_profile
that sets the ..
command to cd ..
. Auto CD replaces that functionality and more.
Enable Auto CD with:
setopt AUTO_CD
Shell History
Shells commonly remember previously executed commands and allows you to recall them with the up and down arrow keys, search or special history commands.
Most of those keys work the same in zsh
. However, there are a few things you need to configure for zsh
history to work as you are used to with bash
on macOS.
By default, zsh
does not save its history when the shell exits. The history is ‘forgotten’ when you close a Terminal window or tab. To make zsh
save its history to a file when it exits, you need to set a variable in the shell:
HISTFILE=${ZDOTDIR:-$HOME}/.zsh_history
Note: this is not a shell option but shell variable or parameter. I will cover some more of those later, You can find a list of variables used by zsh
in the documentation.
The HISTFILE
variable tells zsh
where to store the history data. The syntax ${ZDOTDIR:-$HOME}
means it will use the value of ZDOTDIR
when it is set or default to the value of HOME
otherwise. When a user has set the ZDOTDIR
variable to group their configurations files in a specific directory, the history will be stored there as well.
By default zsh
simply writes each command in its own line in the history file. You can view the file’s contents with any text editor or list the last few commands:
% tail -n 10 ~/.zsh_history
You can make zsh
add a bit more data (timestamp in unix epoch time and elapsed time of the command) by setting the EXTENDED_HISTORY
shell option.
setopt EXTENDED_HISTORY
You can set limits on how many commands the shell should remember in the session and in the history file with the HISTSIZE
and SAVEHIST
variables:
SAVEHIST=5000
HISTSIZE=2000
When the shell reaches this limit the oldest commands will be removed from memory or the history file.
By default, when you exit zsh
(for example, by closing the window or tab) this particular instance of zsh
will overwrite an existing history file with its history. So when you have multiple Terminal windows or tabs open, they will all overwrite each others’ histories eventually.
You can tell zsh
to use a single, shared history file across the sessions and append to it rather than overwrite:
# share history across multiple zsh sessions
setopt SHARE_HISTORY
# append to history
setopt APPEND_HISTORY
Furthermore, you can tell zsh
to update the history file after every command, rather than waiting for the shell to exit:
# adds commands as they are typed, not at shell exit
setopt INC_APPEND_HISTORY
When you use a shared history file, it will grow very quickly, and you may want to use some options to clean out duplicates and blanks:
# expire duplicates first
setopt HIST_EXPIRE_DUPS_FIRST
# do not store duplications
setopt HIST_IGNORE_DUPS
#ignore duplicates when searching
setopt HIST_FIND_NO_DUPS
# removes blank lines from history
setopt HIST_REDUCE_BLANKS
(some of these are redundant)
Most of the time you will access the history with the up arrow key to recall the last command, or maybe a few more steps. You can search through the history with ctrl-R
In zsh
, you can also use the !!
history substitution, which will be replaced with the entire last command. This is most commonly used in combination with sudo
:
% systemsetup -getRemoteLogin
You need administrator access to run this tool... exiting!
% sudo !!
sudo systemsetup -getRemoteLogin
Password:
Remote Login: On
By default, the shell will show the command it is substituting before it is run. But at that point, it is too late to make any changes. When you set the HIST_VERIFY
option, zsh
will show the substituted command in the prompt instead, giving you a chance to edit or cancel it, or just confirm it.
% systemsetup -getRemoteLogin
You need administrator access to run this tool... exiting!
% sudo !!
% sudo systemsetup -getRemoteLogin
Password:
Remote Login: On
This works for other history substitutions such as !$
or !*
, as well. You can find all of zsh
’s history expansions in the documentation.
Correction
When you mistype a command or path, the shell is usually unforgiving. In zsh
you can enable correction. Then, the shell will make a guess of what you meant to type and ask whether you want do that instead:
% systemprofiler
zsh: correct 'systemprofiler' to 'system_profiler' [nyae]?
Your options are to
n
: execute as typedy
: accept and execute the suggested correctiona
: abort and do nothinge
: return to the prompt to continue editing
I have found this far less annoying and far more useful than I expected. Especially, since it works together with AUTO_CD
:
% Dekstop
zsh: correct 'Dekstop' to 'Desktop' [nyae]?
You enable zsh
correction with these options:
setopt CORRECT
setopt CORRECT_ALL
Reverting to defaults
Most of the changes mentioned here affect the interactive shell and will have little impact on zsh
scripts. However, there are some options that do affect the behavior of things like variable substitutions which will affect scripts.
You can revert the options for the current shell to the default settings with the following command:
emulate -LR zsh
We encountered this command earlier when we listed the default settings. The -l
option will list the settings rather than apply them.
If in doubt, it may be useful to add this at the beginning of your zsh
scripts.
Next
In the next part we will take a look at aliases and functions.
Just a minor correction, if you allow.
In the section on globbing, the setopt parameter is called GLOB_COMPLETE
not
GLOB_COMPLETION
At least in my version of ZSH (5.7.1)
Yes, `GLOB_COMPLETE` is correct, also on zsh 5.3 on High Sierra/Mojave. I fixed it in the post. Thanks for catching that!
I’m assuming that to make shell variables (like HISTFILE) stick, you need to use ‘export’ in zsh, like bash? Or is there some difference in zsh that makes them unnecessary?
Since zsh evaluates
~.zshrc
on pretty much every interactive launch, you don’t really need to export the variables for zsh behavior. There may be some weird edge cases, but I have not yet encountered them.If you want a variable to be available by all child processes (including non-zsh processes) then you should export.
You might note that setting SAVEHIST is not optional if you want history saved to ~/.zsh_history. By default it is set to 0.
Fair point. Though in Catalina, these are preset to a non-zero default.
How do I make the variables persistent? I’ve just entered the above HISTFILE-line at the command prompt. If I quit the terminal and open it again, the variable is still set according to ‘typeset | grep HISTFILE’. Is the variable already persistent now? In which file are the variables listed?
You put the options, variables, aliases and other configurations you want to persist into a zsh configuration file. Most common choice is
~/.zshrc
.However, this is confused by the fact that, in Catalina, the HISTFILE (and some other history related settings) are set by the
/etc/zshrc
pre-installed by Apple. On Mojave and earlier, when I wrote this post, the /etc/zshrc contained much less default configuration.Curious if you know how to use AUTO_CD to navigate quickly to an iCloud Drive? It seems that with Catalina, Apple has made access to the drive even more obscure than it has been in the past. For instance, I set the following in my .zshrc file
setopt AUTO_CD
hash -d i=”~/Library/Mobile\ Documents/com~apple~CloudDocs/”
but when issuing the command
% ~i
I just get
zsh: no such file or directory: ~/Library/Mobile\ Documents/com~apple~CloudDocs/
Very frustrating that iCloud Drive has become so difficult to access from the command line.
your has command has the leading `~` quoted. That means that it will _not_ be expanded in this context and the system is looking for a folder named `~` relative to the current working path. (which is exactly what the error message is telling you, but you have to look very closely). Use the `$HOME` variable to set the hash instead:
“`
hash -d I=”$HOME/Library/Mobile\ Documents/com~apple~CloudDocs/”
“`
see also: Advanced Quoting in Shell Scripts
Running into a weird zsh-nice-permission error. I have a LaunchAgent (user library) running a shell script with /bin/zsh on Catalina, and at some point I call an internal beep function, then immediately an internal notify function:
_beep &
_notify “foo” “bar”
(The “&” is there in case a user has set a long-playing sound file as his error sound, so there isn’t too much time between the beep and the notification.)
_beep is basically just: `osascript -e “beep” &>/dev/null`
_notify is just an osascript “display notification” thing
Now the problem is that there is no beep, and the stderr log file tells me that “nice(5) failed: operation not permitted”. I assume it has to do with the fact that zsh spawns background processes with nice, so I tried adding
unsetopt BG_NICE
to the shell script (somewhere near the beginning), but that didn’t help either.
Any way around this?
The LaunchAgent does _not_ run within a full shell environment such has you have when run from terminal. None of your zsh configuration files or settings, aliases or functions you have put in there exist when the LaunchAgent runs. Most of those only exist in an interactive terminal.
You can create a fully blown zsh script that is launched as a LaunchAgent. But even then that script will not get any settings from your configuration files except for zshenv. So you need to make sure any settings or functions are set up within the script.
Yes, I know, and was my initial question: does anyone know if there’s a specific way within a script to tell zsh to run background tasks without nice?
Putting `unsetopt BG_NICE` into the script didn’t cut it.