shellmark: every command you type, with how it ended¶
The problem¶
Shell history keeps the text of a command and nothing else. It cannot tell you
which commands failed this week, which one eats your afternoons, or what you ran
on that branch. The answers exist for a moment, in $? and the clock, and the
next prompt throws them away.
Prompt hooks can catch them, but the obvious hook gets three things wrong:
- bash logs the wrong command. bash has no "before a command" hook, so it is
faked with a
DEBUGtrap that also fires for whateverPROMPT_COMMANDruns. macOS Terminal setsPROMPT_COMMAND=update_terminal_cwd; tried live with bash 3.2, a hook that arms after its own prompt function loggedupdate_terminal_cwdin place of all four commands typed. - Pipelines hide failures.
$?ofmake test | tee out.logistee's. $( )is a subshell, in zsh too. Five per prompt measured at about 12 ms per command.
The method¶
- bash: arm once, and only for a new history line.
PROMPT_COMMANDbecomesrecord; <what was there>; arm. The trap takes a command only whenhistory 1shows a new number, so other prompt code, which adds no history line, is ignored wherever it sits, and the whole line is logged, not its first word. - Every pipeline status (
$pipestatus,PIPESTATUS) goes in a"pipe"field. bash resetsPIPESTATUSwhen the trap runs, so the trap saves it first. - No subshells: results come back in variables. The clock is zsh's
$epochtimeor bash 5's$EPOCHREALTIME, split on any non-digit because the decimal mark follows the locale. 1,000 commands: 0.7 s, against 12.7 s for the$( )version. - Branch from files: walk up to
.git; when it is a file (worktree, submodule), followgitdir:. Detached HEAD shows 12 characters. - A line typed with a leading space is never logged;
SHELLMARK=offpauses.
echo 'source /path/to/methods/shellmark/shellmark.sh' >> ~/.zshrc
$ shellmark fails
x 2 01 Jan 08:02 9.1s /src/app (main) make test
| 01 Jan 08:04 9.0s /src/app (main) make test | tee out.log
| marks a failure inside a pipeline that exited 0. Also recent, slow,
top, dirs, find REGEX, stats, csv. The log is plain NDJSON:
{"ts":1767225600,"cmd":"make test | tee out.log","cwd":"/src/app","branch":"main","ms":9000,"exit":0,"pipe":[2,0],"sid":"4182.2291830114"}
Limits¶
- bash spends one subshell per command reading
history 1; zsh none. - bash logs what history keeps: an
ignoredupsrepeat is skipped. - bash 3 and 4 time in whole seconds.
- An existing bash
DEBUGtrap wins; on bash 3.2 shellmark then records nothing, silently, because bash hides the outer trap from a sourced file. - Typed secrets land in the log unless the line starts with a space.
Code¶
methods/shellmark/ in the methods repository: the hook (shellmark.sh, zsh and bash 3.2+), the query tool (Python standard library), and a 52-check test that types at real interactive bash and zsh on a pseudo-terminal.