Everything below was found while rebuilding hellishrc_plugins against 2.10.2 — a config framework with a 30-theme prompt engine, so it leans hard on the prompt renderer, the hook arrays and shopt. Each item was reproduced in a real pty (not just -c) before being written down, and each says what it cost downstream.
hellish, version 2.10.2 (hellish-linux-x86_64)
Ubuntu 22.04.4, xterm-256color, COLORTERM=truecolor, LANG=en_US.UTF-8
user-mode install: ~/.local/bin/hellish
The prompt work in 2.9/2.10 is genuinely good — % and \ in one PS1, the self-spacing badges, HELLISH_*_FUNCS, right prompts. Most of what follows is the sharp edges around that.
1. print -rP and the prompt renderer disagree about variable content
The most consequential one, because it makes prompt preview lie.
The prompt renderer does not re-scan what a variable expanded to. print -rP does. So the same theme renders two different ways depending on which path you go through.
$ V='50%off'; print -rP "[${V}]"
[50ff] # %o was eaten as a prompt escape
$ V='100%done'; print -rP "[${V}]"
[100/tmp/…/scratchpadone] # %d expanded to the cwd
versus the real prompt, in a pty:
PS1='[${V}] ' with V='50%off' → [50%off] # correct, left alone
PS1='[${V}] ' with V='%F{46}x%f' → [%F{46}x%f] # inert, not colour
PS1='[${V}] ' with V='x\g y' → [x\g y] # inert, not the branch
Both behaviours are defensible; having both is not. share/hellish/rc.d/40-prompt-switch.hsh ships prompt preview built on print -rP, and it already carries a comment explaining that it doubles % "because print -P always enters through the zsh frontend" — that comment is documenting this bug rather than a design.
Why it matters: any prompt whose content comes from a variable — which is every prompt with a computed segment, since $( ) is unavailable (§2) — previews as something you will never actually see. A branch called feature/50%-done or a directory called 100%done is enough.
Suggested: make print -rP treat post-expansion text as literal, the way the renderer does. Failing that, document the split loudly, because the natural assumption is that the preview renderer is the renderer.
Worked around by escaping every segment on the way into a preview and restoring it after — _hx_preview_escape in lib/prompt.hsh.
2. $( ) is never expanded in a prompt, and nothing says so
PS1='[$(echo LIVE)] ' → [$(echo LIVE)] # literal, forever
PS1='[${VAR}] ' → expands every render
PS1='[$((6*7))] ' → [42]
hellishrc.example says "Live $VAR / ${VAR} expansion happens at every render, and so does $? and the other single-character specials" — true, and it never mentions command substitution, so the reader is left to infer it from an omission. Every other shell expands $( ) in PS1, so the inference goes the wrong way.
This is not a small limitation: it is the difference between "a theme is a string" and "a theme needs a hook, a variable, and an invalidation story". The whole segment architecture in my engine exists because of it.
Suggested: either expand it (behind setopt PROMPT_SUBST, as zsh does), or state the exclusion explicitly next to the $VAR line. A prompt that silently prints $(git branch) looks like a broken config, not an unsupported feature.
3. shopt -s extglob is accepted, reports on, and then fails to parse
$ hellish -c 'shopt -s extglob; shopt extglob'
extglob on
$ hellish -c 'shopt -s extglob; case ab in @(ab|cd)) echo MATCH;; esac'
hellish: line 1: syntax error near unexpected token `(' # exit 2
This is worse than an unimplemented option, for one specific reason: the failure is at parse time, so the guard everyone writes does not work.
if shopt -q extglob; then # true
case "$x" in @(a|b)) ... ;; esac # the whole FILE fails to parse
fi
You cannot feature-detect your way out of it from inside the file — the branch never runs because the file never parses. The only safe probe is out-of-process (hellish -c '…'), which is what my capability probe ended up doing.
Suggested: make shopt -s extglob fail and report off until the lexer supports it. An option that lies about being on is strictly worse than one that refuses.
The other glob knobs are fine now, and worth noting as fixed: dotglob, nocaseglob, globstar and nullglob (arrays included) all behave correctly on 2.10.2, where they were accepted no-ops on 2.7.6.
4. PS2 is silently ignored
$ PS2='CUSTOM2> '
$ echo one \
> two # still '> ', not CUSTOM2>
PS2 is settable, readable, and has no effect. hellishrc.example and several shipped themes set it, which suggests it is expected to work. Either honour it or drop it from the examples, so nobody else spends time wondering why their continuation prompt never changes.
5. COLUMNS and LINES are never set
$ shopt -s checkwinsize; pretty on resize-aware; /bin/true
$ echo "[${COLUMNS:-unset}] [${LINES:-unset}]"
[unset] [unset]
checkwinsize is on by default and pretty on resize-aware is advertised as "track the terminal size after every command" — so the shell clearly knows the size. It just never exports it, where bash maintains both in interactive shells.
Cost: anything that needs the terminal width has to fork tput cols. In a prompt that is a process per render, which is exactly the thing a fast prompt cannot afford. My full-width-rule theme has to declare itself expensive and cache the answer for the life of the shell, so it gets the wrong width after a resize.
Suggested: set COLUMNS/LINES when checkwinsize/resize-aware is on. The tracking already exists; this is publishing it.
6. Missing variables: EPOCHREALTIME, SRANDOM
$ echo "[${EPOCHSECONDS}] [${EPOCHREALTIME:-unset}] [${SRANDOM:-unset}]"
[1789416704] [unset] [unset]
EPOCHSECONDS is there, EPOCHREALTIME is not. For prompt timing, one-second resolution is not useful — date +%s%N is a fork, which is the cost you were trying to avoid by having EPOCHSECONDS at all. SRANDOM is minor by comparison.
7. No bind, so key bindings cannot come from a config
$ hellish -c 'bind -p'
hellish: bind: command not found
set -o emacs / set -o vi work and are the only line-editing control an rc has. With complete landed in 2.10, bind is the one remaining hole where a config cannot reach the line editor at all — no history-prefix search on Up, no completion-ignore-case, no rebinding C-w.
Not a bug, but it is the highest-value missing builtin from a configuration author's point of view, and worth a line in the docs saying it is unavailable rather than leaving people to discover it.
8. Docs and examples that no longer match the binary
Small things, but each one costs a reader real time because they read as authoritative.
USER_DOC.md has no prompt section. Everything about the prompt — the bilingual PS1, the badges, the hook arrays — lives in hellishrc.example comments and wiki/interactive.md. grep -n '^#+ ' USER_DOC.md for prompt/PS1/theme returns nothing. The example file is doing the job of the manual.
wiki/interactive.md says prompt lists 29 themes. prompt is not a builtin; it is a shell function seeded into ~/.config/hellish/rc.d/40-prompt-switch.hsh. On a machine where that seed did not run, prompt is command not found and the wiki looks wrong.
\A shadows bash's 24-hour clock escape. Documented, and the right call given it shipped first — but a bash prompt pasted in gets an animation frame where it expected 14:30, with no warning at the point of use.
- The animation warning deserves the docs, not just a comment.
hellishrc.example says \A "has been reported to corrupt the prompt on some terminals … not reproducible on demand". That is important and currently only visible to someone reading the example file.
What works well, for balance
Worth saying, since a bug report is a biased sample. All verified on 2.10.2, all fixed since 2.7.6:
complete / compgen (with shopt -s progcomp) · printf %q · printf '\e' · declare -F and alias -p · subscripted array writes from a function defined in a sourced file · local -n / declare -n namerefs · [ -v VAR ] · read -n N · globstar, dotglob, nocaseglob, nullglob in arrays · process substitution <( ) · dirs as a builtin · mapfile / readarray -d · FUNCNAME with a real call stack · BASH_SOURCE · trap DEBUG
And specifically in the prompt: RPROMPT right-aligns correctly and is erased as the line grows; %F{n} %K{n} %B %~ %N<…< %(?.a.b) %(!.a.b) all match zsh; $? is correctly preserved across HELLISH_PRECMD_FUNCS, so a status badge shows your command's result and not the hook's:
PS1='[rc=%?]\S> ' with a precmd hook installed
false → [rc=1] ✘1> # correct
That last one is easy to get wrong and you got it right.
Priority, if it helps
|
|
|
| 1 |
§1 preview/renderer split |
makes the shipped prompt preview lie |
| 2 |
§3 extglob reporting on |
cannot be guarded against from inside a file |
| 3 |
§2 $( ) undocumented |
changes how every dynamic prompt must be built |
| 4 |
§5 COLUMNS unset |
forces a fork per prompt for width |
| 5 |
§4 PS2 ignored |
silent, and the shipped examples set it |
Happy to open these as separate issues, supply the pty harness, or test patches — the harness that produced every transcript above is a ~40-line pty.fork() driver and I can attach it if useful.
Everything below was found while rebuilding hellishrc_plugins against 2.10.2 — a config framework with a 30-theme prompt engine, so it leans hard on the prompt renderer, the hook arrays and
shopt. Each item was reproduced in a real pty (not just-c) before being written down, and each says what it cost downstream.The prompt work in 2.9/2.10 is genuinely good —
%and\in one PS1, the self-spacing badges,HELLISH_*_FUNCS, right prompts. Most of what follows is the sharp edges around that.1.
print -rPand the prompt renderer disagree about variable contentThe most consequential one, because it makes
prompt previewlie.The prompt renderer does not re-scan what a variable expanded to.
print -rPdoes. So the same theme renders two different ways depending on which path you go through.versus the real prompt, in a pty:
Both behaviours are defensible; having both is not.
share/hellish/rc.d/40-prompt-switch.hshshipsprompt previewbuilt onprint -rP, and it already carries a comment explaining that it doubles%"becauseprint -Palways enters through the zsh frontend" — that comment is documenting this bug rather than a design.Why it matters: any prompt whose content comes from a variable — which is every prompt with a computed segment, since
$( )is unavailable (§2) — previews as something you will never actually see. A branch calledfeature/50%-doneor a directory called100%doneis enough.Suggested: make
print -rPtreat post-expansion text as literal, the way the renderer does. Failing that, document the split loudly, because the natural assumption is that the preview renderer is the renderer.Worked around by escaping every segment on the way into a preview and restoring it after —
_hx_preview_escapeinlib/prompt.hsh.2.
$( )is never expanded in a prompt, and nothing says sohellishrc.examplesays "Live$VAR/${VAR}expansion happens at every render, and so does$?and the other single-character specials" — true, and it never mentions command substitution, so the reader is left to infer it from an omission. Every other shell expands$( )in PS1, so the inference goes the wrong way.This is not a small limitation: it is the difference between "a theme is a string" and "a theme needs a hook, a variable, and an invalidation story". The whole segment architecture in my engine exists because of it.
Suggested: either expand it (behind
setopt PROMPT_SUBST, as zsh does), or state the exclusion explicitly next to the$VARline. A prompt that silently prints$(git branch)looks like a broken config, not an unsupported feature.3.
shopt -s extglobis accepted, reportson, and then fails to parseThis is worse than an unimplemented option, for one specific reason: the failure is at parse time, so the guard everyone writes does not work.
You cannot feature-detect your way out of it from inside the file — the branch never runs because the file never parses. The only safe probe is out-of-process (
hellish -c '…'), which is what my capability probe ended up doing.Suggested: make
shopt -s extglobfail and reportoffuntil the lexer supports it. An option that lies about being on is strictly worse than one that refuses.The other glob knobs are fine now, and worth noting as fixed:
dotglob,nocaseglob,globstarandnullglob(arrays included) all behave correctly on 2.10.2, where they were accepted no-ops on 2.7.6.4.
PS2is silently ignoredPS2is settable, readable, and has no effect.hellishrc.exampleand several shipped themes set it, which suggests it is expected to work. Either honour it or drop it from the examples, so nobody else spends time wondering why their continuation prompt never changes.5.
COLUMNSandLINESare never setcheckwinsizeis on by default andpretty on resize-awareis advertised as "track the terminal size after every command" — so the shell clearly knows the size. It just never exports it, where bash maintains both in interactive shells.Cost: anything that needs the terminal width has to fork
tput cols. In a prompt that is a process per render, which is exactly the thing a fast prompt cannot afford. My full-width-rule theme has to declare itself expensive and cache the answer for the life of the shell, so it gets the wrong width after a resize.Suggested: set
COLUMNS/LINESwhencheckwinsize/resize-awareis on. The tracking already exists; this is publishing it.6. Missing variables:
EPOCHREALTIME,SRANDOMEPOCHSECONDSis there,EPOCHREALTIMEis not. For prompt timing, one-second resolution is not useful —date +%s%Nis a fork, which is the cost you were trying to avoid by havingEPOCHSECONDSat all.SRANDOMis minor by comparison.7. No
bind, so key bindings cannot come from a configset -o emacs/set -o viwork and are the only line-editing control an rc has. Withcompletelanded in 2.10,bindis the one remaining hole where a config cannot reach the line editor at all — no history-prefix search on Up, nocompletion-ignore-case, no rebindingC-w.Not a bug, but it is the highest-value missing builtin from a configuration author's point of view, and worth a line in the docs saying it is unavailable rather than leaving people to discover it.
8. Docs and examples that no longer match the binary
Small things, but each one costs a reader real time because they read as authoritative.
USER_DOC.mdhas no prompt section. Everything about the prompt — the bilingual PS1, the badges, the hook arrays — lives inhellishrc.examplecomments andwiki/interactive.md.grep -n '^#+ ' USER_DOC.mdfor prompt/PS1/theme returns nothing. The example file is doing the job of the manual.wiki/interactive.mdsayspromptlists 29 themes.promptis not a builtin; it is a shell function seeded into~/.config/hellish/rc.d/40-prompt-switch.hsh. On a machine where that seed did not run,promptiscommand not foundand the wiki looks wrong.\Ashadows bash's 24-hour clock escape. Documented, and the right call given it shipped first — but a bash prompt pasted in gets an animation frame where it expected14:30, with no warning at the point of use.hellishrc.examplesays\A"has been reported to corrupt the prompt on some terminals … not reproducible on demand". That is important and currently only visible to someone reading the example file.What works well, for balance
Worth saying, since a bug report is a biased sample. All verified on 2.10.2, all fixed since 2.7.6:
complete/compgen(withshopt -s progcomp) ·printf %q·printf '\e'·declare -Fandalias -p· subscripted array writes from a function defined in a sourced file ·local -n/declare -nnamerefs ·[ -v VAR ]·read -n N·globstar,dotglob,nocaseglob,nullglobin arrays · process substitution<( )·dirsas a builtin ·mapfile/readarray -d·FUNCNAMEwith a real call stack ·BASH_SOURCE·trap DEBUGAnd specifically in the prompt:
RPROMPTright-aligns correctly and is erased as the line grows;%F{n}%K{n}%B%~%N<…<%(?.a.b)%(!.a.b)all match zsh;$?is correctly preserved acrossHELLISH_PRECMD_FUNCS, so a status badge shows your command's result and not the hook's:That last one is easy to get wrong and you got it right.
Priority, if it helps
prompt previewlieextglobreportingon$( )undocumentedCOLUMNSunsetPS2ignoredHappy to open these as separate issues, supply the pty harness, or test patches — the harness that produced every transcript above is a ~40-line
pty.fork()driver and I can attach it if useful.