You put `PROMPT="%~ [$(date +%H:%M)] %# "` in your ~/.zshrc and the clock in the zsh prompt is frozen at the time the shell started. Explain what happened, and what has to change for the prompt to re-evaluate that substitution before every command line.
answer
- expanded once, or expanded every time
- the quotes decide when
- an option unlocks $( ) in prompts
- PROMPT_SUBST
- or use a native %-escape
basics
~20 sThe double quotes ran the command substitution once, when ~/.zshrc was sourced, so the value was baked into the string. Assign the prompt single-quoted and enable the PROMPT_SUBST option, or use the native %D{%H:%M} prompt escape.
solid answer
~40 sThe substitution happened at *assignment* time. Double quotes let the shell expand `$(date +%H:%M)` while sourcing `~/.zshrc`, so `PROMPT` ended up holding a fixed string with the startup time in it — the prompt is doing exactly what you told it. To defer the expansion, store the text literally with single quotes and turn on the option that makes zsh expand prompt strings each time it draws them: `setopt PROMPT_SUBST` plus `PROMPT='%~ [$(date +%H:%M)] %# '`. Without `PROMPT_SUBST` the single-quoted version fails differently — prompt expansion handles `%`-escapes but not command substitution, so you would see the literal text `$(date +%H:%M)` in your prompt. For a clock specifically, the better answer avoids the subshell entirely: `%D{%H:%M}` is a built-in prompt escape that formats the current time with no fork per prompt.
code
bash · 12 lines# frozen: the subshell runs once, when ~/.zshrc is sourced
PROMPT="%~ [$(date +%H:%M)] %# "
# literal: stored unexpanded, but prompt expansion does not run $( )
PROMPT='%~ [$(date +%H:%M)] %# '
# re-evaluated before every prompt
setopt PROMPT_SUBST
PROMPT='%~ [$(date +%H:%M)] %# '
# best for a clock: a native escape, no fork at all
PROMPT='%~ [%D{%H:%M}] %# 'go deeper
Know that PROMPT (also spelled PS1) holds the prompt string, and that escapes like %~ and %n show the directory and username.
Explain the difference between expansion at assignment time and at prompt-draw time, and name PROMPT_SUBST as the option that enables substitution in the second.
Care about the cost: every $(...) in a prompt forks before each command line, so compute once in a precmd hook or use a native escape instead.
Weigh what a shared prompt is for — signalling account, host and risky context — against the latency and support burden it adds to everyone's every keystroke.
## Two different expansions, at two different times A zsh prompt goes through expansion twice in its life, and almost every prompt bug is a confusion between the two. **Assignment-time expansion** is ordinary shell expansion, done once, when the line `PROMPT=...` executes. Double quotes participate; single quotes do not. **Prompt-time expansion** is done by zsh every time it is about to draw the prompt. It always processes `%`-escapes (`%~`, `%n`, `%#`), and it processes parameter, command and arithmetic substitution *only if* the `PROMPT_SUBST` option is set. The three cases: ```zsh # 1. frozen: the subshell runs once, while ~/.zshrc is sourced PROMPT="%~ [$(date +%H:%M)] %# " # 2. literal: stored unexpanded, but prompt expansion won't run $( ) PROMPT='%~ [$(date +%H:%M)] %# ' # 3. correct: stored unexpanded, and expanded on every prompt setopt PROMPT_SUBST PROMPT='%~ [$(date +%H:%M)] %# ' ``` Case 1 is the reported symptom. Case 2 is what people get when they fix the quotes but forget the option, and it looks broken in a completely different way, which is a useful diagnostic: literal `$(...)` on screen means the option is missing, a stale value means the quoting is wrong. ## PROMPT, PS1 and RPROMPT `PROMPT` and `PS1` are the same parameter under two names, so configurations you copy will use either. `RPROMPT` (also `RPS1`) is zsh's right-hand-side prompt, drawn flush to the right margin and automatically hidden when the command line grows into it. It is the natural home for context you want visible but not in the way — host, exit status, a timestamp. ## The escapes you actually need Prompt escapes are always available (they are governed by `PROMPT_PERCENT`, which is on by default) and cost nothing: - `%n` username, `%m` short hostname, `%M` full hostname - `%~` current directory with `$HOME` shown as `~` - `%#` prints `#` for a privileged shell and `%` otherwise - `%D{...}` a strftime-formatted date or time — the right fix for a clock - `%F{red}`…`%f` foreground colour, `%B`…`%b` bold - `%(?.ok.fail)` a conditional: the `?` test picks the first text when the last command succeeded Because `%D{%H:%M}` exists, the whole subshell in the original example is unnecessary: ```zsh PROMPT='%~ [%D{%H:%M}] %# ' ``` ## The cost you have just introduced Enabling `PROMPT_SUBST` and putting `$(...)` in the prompt means forking a process — sometimes several — *before every single command line you type*. On a laptop with a warm cache you will not notice one. Inside a large repository, on a network filesystem, or with three such calls chained, you get a prompt that visibly lags, and the lag is charged to every keystroke of your day. The standard remedy is to compute at most once per prompt in a hook rather than inline: ```zsh autoload -Uz add-zsh-hook setopt PROMPT_SUBST _gk_precmd() { _gk_when=$(date +%H:%M:%S) } add-zsh-hook precmd _gk_precmd PROMPT='%~ [$_gk_when] %# ' ``` `precmd` runs immediately before each prompt is drawn; `preexec` runs after you hit return and before the command runs; `chpwd` runs on directory change. `add-zsh-hook` (autoloaded) registers a function without clobbering whatever else is already registered, which is why it is preferred over defining `precmd` directly — a plugin may already own that name. For version-control information specifically, zsh ships `vcs_info`, an autoloadable function designed to be called from `precmd`, which fills in a message parameter you then interpolate into the prompt. It exists precisely so that people stop shelling out to a VCS on every keystroke. ## Escape sequences and line wrapping If you embed raw terminal escape sequences rather than using `%F`/`%B`, wrap them in `%{`…`%}`. That tells zsh the enclosed bytes occupy zero display columns. Get it wrong and the shell miscounts the prompt width; the visible symptom is a long command line that wraps in the wrong place or overwrites part of the prompt while you edit it. Using the `%F`/`%f` and `%B`/`%b` escapes avoids the problem entirely because zsh already knows their width.
- With single quotes but no PROMPT_SUBST, what do you actually see?The literal text `$(date +%H:%M)` in your prompt. Prompt expansion always handles `%`-escapes, but command, parameter and arithmetic substitution are gated behind `PROMPT_SUBST`. That difference is a useful diagnostic: literal `$(...)` on screen means the option is missing, while a stale value means the quoting was wrong.
- What is the performance objection to putting a command substitution in PROMPT?It forks a process before every command line you type. One cheap call is fine; three of them, or one that touches a network filesystem or a large repository, produces a prompt that visibly lags. Compute in a `precmd` hook into a variable, or use a native `%`-escape that needs no subshell.
- Why use `add-zsh-hook precmd myfunc` instead of just defining a function called precmd?Because `precmd` is a single name and something else may already own it — a plugin, a framework, a prompt theme. `add-zsh-hook` (after `autoload -Uz add-zsh-hook`) appends to the array of registered functions instead of replacing whoever got there first.
- Your prompt uses raw colour escapes and long command lines start wrapping in the wrong place. Why?zsh is counting those escape bytes as visible width. Wrap any raw terminal escape in `%{`…`%}` to declare it zero-width, or better, use the `%F{...}`/`%f` and `%B`/`%b` escapes whose width zsh already knows.
saying these in an interview costs you the question
- Thinks the quoting style makes no difference here
- Believes prompt expansion always runs command substitution
- Sets PROMPT_SUBST but keeps the double quotes
- Adds a subshell per prompt without considering the cost
- Embeds raw colour escapes without marking them zero-width