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
- Part 4: Aliases and Functions
- Part 5: Completions
- Part 6: Customizing the
zsh
Prompt - Part 7: Miscellanea (this article)
- 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.
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!”)
We have covered the general aspects of configuring your zsh
environment and enabling some of its features to make your work more productive. However, there are zsh
features that didn’t quite fit in earlier posts, but also don’t warrant a post of their own. So I am gathering them here.
multiIO
Terminal commands can take input from a file or a previous command (stdin
) and have two different outputs: stdout
and stderr
. In bash
you can redirect each of these to a single other destination.
For example, you can redirect the output of a command to a file:
% system_profiler SPHardwareDataType >hardwareinfo.txt
In zsh
you can redirect to (and from) multiple sources. In the simplest form you can write the output to two files:
% system_profiler SPHardwareDataType >hardwareinfo.txt >computerinfo.txt
This is of course not a very realistic case. Since the pipe |
is a form of redirection, you can combine output to a file with a pipe:
% system_profiler SPHardwareDataType >hardwareprofile.txt | cat
Instead of piping to cat, you can also redirect to stdout
(or &1
) as well as to a file:
% system_profiler SPHardwareDataType >&1 >hardwareprofile.txt
Note that the order of doing this is important. The construct >file.txt >&1
would redirect the output to file.txt
and then redirect the output again to where stdout
or 1
is going, so it would be redundant.
When combined with pipes and other commands multiIO can become very useful:
% system_profiler SPHardwareDataType >hardwareprofile.txt | awk '/Serial Number/ { print $4 }' >&1 >serialnumber.txt
You can use multiIO for input as well:
% sort </usr/share/calendar/calendar.freebsd </usr/share/calendar/calendar.computer
And while this not directly related, but somewhat close, in zsh
, this
% <hardwareinfo.txt
is equivalent to more hardwareinfo.txt
.
Recursive Globbing with **
You can use the **
to denote an arbitrary string that can span multiple directories in a path.
For example:
% echo Library/Preferences/**/com.apple.screensaver.*plist
Library/Preferences/ByHost/com.apple.screensaver.BBCCDDEE-AABB-CCDD-ABCD-00AABBCCDDEE.plist Library/Preferences/com.apple.screensaver.plist
In this case the **
matches nothing as well as /ByHost/
.
Note: when used on large folder structures this glob can take a while. So use with care.
Connected array Variables
We already encountered the fpath
variable in earlier posts. You can see its contents with the echo command:
% echo $fpath
/Users/armin/Projects/mac-zsh-completions/completions/ /Users/armin/Projects/dotfiles/zshfunctions /usr/local/share/zsh/site-functions /usr/share/zsh/site-functions /usr/share/zsh/5.3/functions
Interestingly enough, zsh
also has an FPATH
variable, which is a colon-separated list of directories:
% echo $FPATH
/Users/armin/Projects/mac-zsh-completions/completions/:/Users/armin/Projects/dotfiles/zshfunctions:/usr/local/share/zsh/site-functions:/usr/share/zsh/site-functions:/usr/share/zsh/5.3/functions
Since the fpath
variable is an array, I only changed the fpath
variable in my zshrc
.I never set or changed the FPATH
, yet it reflects the changes made to the fpath
variable.
When you see the type of both variables, you get an idea that something is going on:
% echo ${(t)fpath}
array-special
% echo ${(t)FPATH}
scalar-special
The fpath
and FPATH
are connected in zsh
. Changes to one affect the other. This allows use of more flexible and powerful array operations through the fpath
‘aspect’ of the value, but also provides compatibility to tools that expect the traditional colon-separated format in FPATH
.
You will not be surprised to hear that zsh
uses the same ‘magic’ with the PATH
variable and its array counterpart path
.
This means that you can continue to use path_helper
to get your PATH
from the files in /etc/paths
and /etc/paths.d
. (Well, you don’t have to, because on macOS this is done for all users in /etc/zprofile
.) But then you can manipulate the path
variable with array functions, like:
path+=~/bin
You get the useful aspects of both syntaxes.
Suffix Aliases
I learnt this one after writing the aliases part.
Suffix aliases take effect on the last part of a path, so usually the file extension. A suffix alias will assign a command to use when you just type a file path in the command line.
For example, you can a suffix alias for the txt
file extension:
alias -s txt="open -t"
When you then type a path ending with .txt
and no command, zsh
will execute open -t /path/to/file.txt
.
The open -t
command opens a file in the default application set for the txt
file extension in Finder. You probably want to set the suffix alias to bbedit
or atom
or something like that rather than open -t
.
You can use other command line tools for the suffix alias:
alias -s log="tail -f"
Then, typing /var/log/install.log
will show the last lines of that file and update the output when the file changes. If you prefer the graphical user interface, you can use the open -a
command to assign suffix aliases to applications:
alias -s log="open -a Console"
You can even create a suffix alias using a different alias:
alias pacifist="open -a Pacifist"
alias -s pkg=pacifist
Together with the AutoCD option, this can improve your application-shell interactions a lot.
Bindkey for History Search
Most of the keyboard shortcuts in zsh
work the same way as they do in bash
. I have found one change that has proven quite useful:
^[[A' up-line-or-search # up arrow bindkey
^[[B' down-line-or-search # down arrow
These two commands will change the behavior of the up and down arrow keys from just switching to the previous command, to searching
. This means that when you start typing a command and then hit the up key, rather than just replacing what you already typed with the previous command, the shell will instead search for the latest command in the history starting with what you already typed.
There are many commands or ‘widgets’ you can assign to keystrokes with the bindkey
command. You can find a list of default ‘widgets’ in the documentation.
Conclusion
This concludes the part of the series about configuring zsh
. When I set out I wanted to recreate the environment I had built in bash
. Along the way I found a few features in zsh
that seemed worth adding to my toolkit.
After nearly two months of working in zsh
, there are already some features I would miss terribly when switching back to bash
or a plain, unconfigured zsh
. Most important is the powerful tab-completion. But features like AutoCD, MultiIO, and flexible aliases, are useful tools as well.
The dynamic loading of functions from files in the fpath
was initially confusing, but it allows configurations and functions to be split out into their own, which simplifies “modularizing” and sharing.
In the next (and last) post, I will cover the changes when scripting with zsh
vs bash
.
Nice set of articles, worth mentioning as the majority of OSX users will be also using brew that there are various Brews available to extend zsh `brew search zsh`
Thank you for this article; I’ve been a long-time bash user, but it’s probably time to make the switch 🙂
I’d like to add my thanks to those above (and without doubt below) in this thread.
Moving to Catalina was a good opportunity to review and upgrade.
This was hugely helpful, thank you! I now have a lovely zsh + iTerm setup after being a bash + Terminal user for years. I appreciate how you broke down each line of “magic” in an easy-to-understand way.
Great intro to zsh. the bindkey section seems to have some duplicates and typos.
Thanks,
Stan W
Thanks, there seems to be something in the post syntax that keeps choking WordPress, I have fixed this post a few times already…
Thanks for the article, Armin. I just moved to zsh from bash after Catalina came out and your guide was the easiest and most complete guide I found.
Thanks so much for all this!
One thing… for the “Bindkey for History Search” part, it looks like the initial characters for those two lines have disappeared. They should be:
bindkey ‘^[[A’ up-line-or-search # up arrow
bindkey ‘^[[B’ down-line-or-search # down arrow
Thank you, thank you, thank you!!
I’m a total neophyte and could not figure out why I was getting errors on the bindkey lines every time I tried to ‘source .zshrc’ after adding them.
Really appreciate you spelling out what they should actually be – other comments said they were problematic without saying what they should be.
Much appreciated!