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 (this article) - 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.
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!”)
The default bash
prompt on macOS is quite elaborate. It shows the username, the hostname, and the current directory.
Calypso:~ armin$
On the other hand, the default bash prompt doesn’t show the previous command’s exit code, a piece of information I find very useful. I have written before how I re-configured my bash
prompt to have the information I want:
Of course, I wanted to recreate the same experience in zsh
.
The only (visual) difference to my bash
prompt is the %
instead of the $
.
Note: creating a file
~/.hushlogin
will suppress the status message at the start of each Terminal session inzsh
as well as inbash
(or any other shell).
Basic Prompt Configuration
The basic zsh
prompt configuration works similar to bash
, even though it uses a different syntax. The different placeholders are described in detail in the zsh
manual.
zsh
uses the same shell variable PS1
to store the default prompt. However, the variable names PROMPT
and prompt
are synonyms for PS1
and you will see either of those three being used in various examples. I am going to use PROMPT
.
The default prompt in zsh
is %m%#
. The %m
shows the first element of the hostname, the %#
shows a #
when the current prompt has super-user privileges (e.g. after a sudo -s
) and otherwise the %
symbol (the default zsh
prompt symbol).
The zsh
default prompt is far shorter than the bash
default, but even less useful. Since I work on the local system most of the time, the hostname bears no useful information, and repeating it every line is superfluous.
Note: you can argue that the hostname in the prompt is useful when you frequently have multiple terminal windows open to different hosts. This is true, but then the prompt is defined by the remote shell and its configuration files on the remote host. In your configuration file, you can test if the
SSH_CLIENT
variable is set and show a different prompt for remote sessions. There are more ways of showing the host in remote shell sessions, for example in the Terminal window title bar or with different window background colors.
In our first iteration, I want to show the current working directory instead of the hostname. When you look through the list of prompt placeholders in the zsh documentation, you find %d
, %/
, and %~
. The first two do exactly the same. The last substitution will display a path that starts with the user’s home directory with the ~
, so it will shorten /Users/armin/Projects/
to ~/Projects
.
Note: in the end you want to set your
PROMPT
variable in the.zshrc
file, so it will take effect in all yourzsh
sessions. For testing, however, you can just change thePROMPT
variable in the interactive shell. This will give you immediate feedback, how your current setup works.
% PROMPT='%/ %# '
/Users/armin/Projects/dotfiles/zshfunctions %
% PROMPT='%~ %# '
~/Projects/dotfiles/zshfunctions %
Note the trailing space in the prompt string, to separate the final %
or #
from the command entry.
I prefer the shorter output of the %~
option, but it can still be quite long, depending on your working directory. zsh
has a trick for this: when you insert a number n
between the %
and the ~
, then only the last n
elements of the path will be shown:
% PROMPT='%2~ %# '
dotfiles/zshfunctions %
When you do %1~
it will show only the name of the working directory or ~
if it is the home directory. (This also works with %/
, e.g. %2/
.)
Adding Color
Adding a bit of color or shades of gray to the prompt can make it more readable. In bash
you need cryptic escape codes to switch the colors. zsh
provides an easier way. To turn the directory in the path blue, you can use:
PROMPT='%F{blue}%1~%f %# '
The F
stands for ‘Foreground color.’ zsh
understands the colors black
, red
, green
, yellow
, blue
, magenta
, cyan
and white
. %F
or %f
resets to the default text color. Furthermore, Terminal.app represents itself as a 256-color terminal to the shell. You can verify this with
% echo $TERM
xterm-256color
You can access the 256 color pallet with %F{0}
through %F{255}
. There are tables showing which number maps to which color:
- 256 Colors – Cheat Sheet – Xterm, HEX, RGB, HSL
- 256 Terminal colors and their 24bit equivalent (or similar)
So, since I want a dark gray for my current working dir in my prompt, I chose 240
, I also set it to bold with the %B
code:
PROMPT='%B%F{240}%1~%f%b %# '
You can find a detailed list of the codes for visual effects in the documentation.
Dynamic Prompt
I wrote an entire post on how to get bash
to show the color-coded exit code of the last command. As it turns out, this is much easier in zsh
.
One of the prompt codes provides a ‘ternary conditional,’ which means it will show one of two expressions, depending on a condition. There are several conditions you can use. Once again the details can be found in the documentation.
There is one condition for the previous commands exit code:
%(?.<success expression>.<failure expression>)
This expression will use the <success expression>
when the previous command exited successfully (exit code zero) and <failure expression>
when the previous command failed (non-zero exit code). So it is quite easy to build an conditional prompt:
% PROMPT='%(?.√.?%?) %1~ %# '
√ ~ % false
?1 ~ %
You can get the √
character with option-V on the US or international macOS keyboard layout. The last part of the ternary ?%?
looks confusing. The first ?
will print a literal question mark, and the second part %?
will be replaced with previous command’s exit code.
You can add colors in the ternary expression as well:
PROMPT='%(?.%F{green}√.%F{red}?%?)%f %B%F{240}%1~%f%b %# '
Another interesting conditional code is !
which returns whether the shell is privileged (i.e. running as root) or not. This allows us to change the default prompt symbol from %
to something else, while maintaining the warning functionality when running as root:
% PROMPT='%1~ %(!.#.>) '
~ > sudo -s
~ # exit
~ >
Complete Prompt
Here is the complete prompt we assembled, with all the parts explained:
PROMPT='%(?.%F{green}√.%F{red}?%?)%f %B%F{240}%1~%f%b %# '
%(?.√.?%?) |
if return code ? is 0, show √ , else show ?%? |
%? |
exit code of previous command |
%1~ |
current working dir, shortening home to ~ , show only last 1 element |
%# |
# with root privileges, % otherwise |
%B %b |
start/stop bold |
%F{...} |
text (foreground) color, see table |
%f |
reset to default textcolor |
Right Sided Prompt
zsh
also offers a right sided prompt. It uses the same placeholders as the ‘normal’ prompt. Use the RPROMPT
variable to set the right side prompt:
% RPROMPT='%*'
√ zshfunctions % 11:02:55
zsh
will automatically hide the right prompt when the cursor reaches it when typing a long command. You can use all the other substitutions from the left side prompt, including colors and other visual markers in the right side prompt.
Git Integration
zsh
includes some basic integration for version control systems. Once again there is a voluminous, but hard to understand description of it in the documentation.
I found a better, more specific example in the ‘Pro git’ documentation. This example will show the current branch on the right side prompt.
I have changed the example to include the repo name and the branch, and to change the color.
autoload -Uz vcs_info
precmd_vcs_info() { vcs_info }
precmd_functions+=( precmd_vcs_info )
setopt prompt_subst
RPROMPT=\$vcs_info_msg_0_
zstyle ':vcs_info:git:*' formats '%F{240}(%b)%r%f'
zstyle ':vcs_info:*' enable git
In this case %b
and %r
are placeholders for the VCS (version control system) system for the branch and the repository name.
There are git
prompt solutions other than the built-in module, which deliver more information. There is a script in the git
repository, and many of the larger zsh
theme projects, such as ‘oh-my-zsh’ and ‘prezto’ have all kinds of git status widgets or modules or themes or what ever they call them.
Summary
You can spend (or waste) a lot of time on fine-tuning your prompt. Whether these modifications really improve your productivity is a matter of opinion.
In the next post, we will cover some miscellaneous odds and ends that haven’t yet really fit into any of preceding posts.
Thanks! This saved me a bunch of wasted time.
Very useful! I’ve switched to zsh after 25 years of bash. I did this entirely using the tips and tricks from your blog series on zsh. Excellent work, and many thanks.
Thank you, this was very useful.
I had no luck editing ~/.profile (maybe because I had not yet set ‘zsh’ as my default shell).
Could you please also mention the use of ~/.zprofile (run at login) and ~/.zshrc (run for new Terminal session).
More information is available from:
https://support.apple.com/en-au/HT208050
You mean like I did in part 2 of the article series? https://scriptingosx.com/2019/06/moving-to-zsh-part-2-configuration-files/
Hmm, Google sent me directly to this (zsh Prompt) page, and it’s not clear from this page that modifying the variables in .profile will not work as one might expect. Perhaps you could add a line in the conclusion section that mentions this and refers back to the configuration file section, but these comments are probably enough now anyway.
thank you so much this was great! with a bit of extra research wrote a prompt that looks like agnoster/powerlevel9k!
here it is :
PROMPT=”%F{yellow} %n@%m %F{blue}%S”$’\ue0b0′”%s%K{blue}%F{black} %~ %k%F{blue}”$’\ue0b0′”%f “
can i put this in .zshenv? i mean the prompt? i added it but didn’t work
macOS Catalina has a file `/etc/zshrc`, which sets the macOS default prompt to `PS1=”%n@%m %1~ %# “`. Since `/etc/zshrc` is executed _after_ your `~/.zshenv` this will overwrite the your setting. (See Part 2: Configuration Files) Since `~/.zshenv` is executed for _all_ instances of zsh, including scripts, it is recommended to use this minimally, if at all.
The recommended location for your personal settings is `~/.zshrc`.
Nice article and well explained. However if the goal is changing just path info, leaving everything everything else intact (colors, git info and potentially other customisations before this one), you could just use following:
PROMPT=${PROMPT/\%c/\%~}
See my StackOverflow answer: https://stackoverflow.com/a/62203156/4973640
Hi there. Is this a change we have to make every time we want to use the terminal? I applied the changes and it worked fine but when I relaunched terminal, it was back to the default prompt settings. Sorry if this is an obvious questions, I’m a beginner who just started coding for fun.
You need to create a configuration file, e.g. `~/.zshrc` which sets the `prompt` variable (and the other settings you want). The `.zshrc` will be read and run every time a new shell starts (Terminal window opens). See Moving to zsh, part 2: Configuration Files
Great post (and great book) — thanks!
I’m having a very strange problem — if I put the command that sets the prompt into .zshrc the prompt is set to an empty string.
I made a .zshrc file with a single line:
prompt=’xx ‘
But after the shell reads the initialization file the prompt is empty.
This is happening on two different systems (iMac and MacBook Pro), both running macOS 10.15.5, and both using Terminal.app.
Any ideas?
As we discussed in email, you were able to fix this by changing the line endings for the zshrc file! Glad you worked it out!
Pure gold my friend. This article saved me a bunch of time and also illustrated nicely what can be done. Thanks a ton!
Lovely prompt customisation hints, thanks!
Since I prefer having my prompt on a new line (initial cursor always in the same position), I’d like to add my 2 cents to /etc/zshrc:
NEWLINE=$’\n’
PROMPT=”${NEWLINE}[%n@%F{green}%m%f :: %~]${NEWLINE}# ”
which leads to:
[User@Hostname :: ~/Desktop/stuff/and/gear]
#
OMG, I was looking for that! I would never imagined that it was too simple
Thank you
Unfortunately, after setting up the git integration (show name of branch in brackets on right side) the prompt takes much longer to come back after entering any given command.
Response times improve when moving out of a directory under version control…
Can this be improved somehow?
Yes, for complex git repositories, the built-in support to determine the status can be quite ineffective. There are several solutions to speed this up. The linked post on git refers to this script: https://github.com/git/git/blob/master/contrib/completion/git-prompt.sh but there are several alternative approaches, for example powerlevel10k has a custom binary to determine the git status much faster: https://github.com/romkatv/powerlevel10k/tree/master/gitstatus
Great set of articles as an intro to zsh. Having been a csh (and its later variants / improvements) since SunOS 4, I have finally made the switch to zsh with Catalina as I am using the command line much more now than in recent years.
Anyway, just want to add my 2¢, and that using emojis can also enhance the prompt.
Here is my first iteration using your example but w/ emojis:
PROMPT=’%(?.%F✅%?.%F{red}❗️%?)%f %B%F{252}%2~%f%b %# ‘
This is damn good. Thanks!
Thanks for this series. Super helpful for someone given a Mac that has no idea why there are so many shell files and which one to edit for what!