hash runs shell code with the mvdan/sh interpreter, compared here with bash 5.2. Each line is one difference: what bash does, what hash does, and what to do about it when there is something to do. Behavior that matches bash is not listed.
- Subshell traps. bash runs a subshell's own traps (
(trap 'rm "$tmp"' EXIT; ...)) when it ends. hash sets and lists them but never runs them: the interpreter has no hook at a subshell's end. Only the shell's own EXIT trap runs, when hash exits. $?in a signal trap. bash leaves the last command's status in$?in a SIGHUP or SIGTERM trap. hash sets it to 128+signal, in the signal's trap and in the EXIT trap that follows.- A signal trap that does not exit. bash goes on after it. A hash script still ends, dying of the signal, since
it cannot resume the line the signal stopped. An
exitin the trap ends the script with its status. - Ignored signals. bash keeps any signal it was started with ignored. hash keeps SIGHUP and SIGINT ignored
(
nohup), not SIGTERM or SIGQUIT (Go keeps only the first two). An empty trap (trap '' TERM) ignores the signal in hash, but the commands hash runs do not inherit the ignore. - SIGQUIT between lines. A script's QUIT trap runs once the line ended, or before the next line when the signal
came between lines, with
$?the last line's status (bash 5.2 does the same for a USR1 trap). - INT trap timing. bash runs a script's INT trap as the command that handled Ctrl+C returns. hash runs it once the
command's line ended; a
hash -cprogram is one line, so there it runs at the end. - Interactive traps. The interactive shell keeps signal traps but runs only its EXIT trap: no INT trap (as bash
5.2), and no TERM trap where bash -i runs one (
hash -iruns a TERM trap once the line ended). - A command running at a fatal signal. bash waits for it to finish. hash sends it the same signal and kills it after 2 seconds.
- An
exitin a subshell's ERR trap ((trap 'exit 4' ERR; false; echo in)) ends the subshell in bash. In hash the subshell goes on, andexitin the shell's own ERR trap ends the shell as in bash. - ERR trap at a failing pipeline. bash runs it once; hash runs it twice, for the pipeline's last command and for the pipeline.
set -eand Ctrl+C. bash -i with errexit on exits when Ctrl+C ends a foreground pipeline. hash's interactive shell ends the line with 130 and goes on.
- Jobs run inside hash. A job (
cmd &) is not a process of its own: hash runs its shell code itself and starts the commands it runs (through itself, which keepsargv[0]as typed and sets the signals below; a few milliseconds per job). - Process groups. In the interactive shell each command a job starts leads a process group of its own, as with
bash's job control: the terminal's Ctrl+C and Ctrl+\ reach only the foreground,
kill 0in a job stays in it, and exit or Ctrl+D leaves the jobs running. A script's jobs share its process group, as in bash with no job control, and start with Ctrl+C and Ctrl+\ ignored. - No job control. Ctrl+Z,
fgandbgare not supported: hash never stops or continues a job. - A job and the terminal. The interactive shell's job's command starts with SIGTTOU and SIGTTIN ignored, since
nothing could continue it if the terminal stopped it: its
sttyworks, and its read of the terminal (read x </dev/tty, a password prompt), or one its shell code makes, fails with an I/O error, where bash stops the job. A process that command starts and that resets the signals is stopped as in bash (a child of macOS's/bin/sh, bash 3.2), untilkill %Nends it. - After an upgrade. When hash's own executable was replaced since the shell started, a job's command starts as it is, without those ignores.
- Stdin. A job's stdin is
/dev/nullunless a redirect names the terminal; its output is the terminal itself. - Hangup at the shell's end. When a signal ends the interactive shell, hash sends SIGHUP to the process group of
every job's commands still running (
nohupjobs live on), whatever the signal. - A script's exit. Neither shell waits for its jobs. A command a job started keeps running; a job's shell code that hash runs itself is cut off, where bash's goes on as a process of its own.
- Job ids.
$!names a jobg1,g2, ....killandwaittake$!,gN,%N,%%or%-, andwaitthe pidjobs -pprints for a job that is one command, running or ended (for p in $(jobs -p); do wait "$p"), where bash's takes the pid of any process of a job;%%and%-are the jobsjobsmarks+and-(%-the only one when one runs), among the running ones (bash's may name a job that ended and was not reported yet).%Nis the shell's Nth job, where bash numbers the jobs still running, and%nameis not taken.killends the job's shell code (a runawaywhile :; do :; done &too), the jobs it started, and the process groups of the commands they started (in a script, the commands themselves). wait -nwaits for the first of the shell's running jobs to end, or of the jobs it names, and takes its status; 127 when none runs. A job that ended before the call is not one unless named, where bash's takes it until reported, and-p varis not taken.disowntakes-h,-aand-r, job specs and the pidsjobs -pprints: a disowned job leavesjobs,%Nand a barewait, and with-hstays listed; the shell's hangup passes both by.$!still names a disowned job, as bash's pid does.jobslists the shell's running jobs, or the ones it names (-ladds their commands' pids;-slists none). A job a command of it stopped is still listed as Running.-pnames each job askilltakes it: by its command's pid when the job is one command (a function is not) and that command started within 20ms, else by itsgN(shell code, a{ ...; }block, a pipeline), which ends the whole job, where bash lists the pid of the job's first process. A command other thankillandwait(ps -p $(jobs -p)) cannot take agN.- Notices. The interactive shell says
[N] pidas a job starts ([N] gNwhen it starts no command within 20ms) and[N]+ Done,Exit Nor the signal's name before the next prompt once it ended, as bash -i; there is noset -bto say it at once. - Command substitutions with jobs. hash holds
$(...)open until each job it started that keeps the substitution's output open ended, as bash (pid=$(server >log 2>&1 & echo $!)returns at once), but tells that from the redirections of the job's statement and of the blocks around it, not from what its commands do later. - Tracing. With
set -x, a job traces hash's own calls (__hash_job_started N,__hash_job_run N ...) and a substitution that starts a job traces__hash_subst_beginand__hash_subst_end.
- Foreground commands on a PTY. In the interactive shell, a command whose stdin and output are the terminal runs
on a PTY of its own, so hash can keep its output for
copyand the agent. Keys typed while it runs go to it. - Keys a PTY command leaves unread. As in bash, they are the next prompt's, all of them in the order typed
(
sleep 5, thenls⏎typed during it, runsls; a line typed across the command's end is one line), a paste bigger than the PTY's input queue too. Areadlater in the same line does not see them (a later PTY command of the line does). - A long line typed during a command. On macOS a single line over about 1 KB (the PTY's line limit), typed or pasted while a command runs that does not read it, loses its tail; bash loses it the same way, at the terminal's own limit.
- A PTY command's parent. It runs under a second hash process, the PTY's session leader, which hands its unread
keys back:
$PPIDin it is that process, not the shell's$$, and it costs about 5 ms a command (that process's start, measured on an Apple Silicon Mac). When the file at hash's path is no longer the hash that runs (an upgrade replaced it), PTY commands run without it, and their unread keys are lost. - A PTY command's background children. The command runs in a process group of its own, and a child it leaves
running (
sh -c 'sleep 60 &') outlives it, as in bash, but the PTY ends with the command: the child's reads of it end and its writes to it fail from then on, where in bash they reach the terminal. On Linux, where a child that holds the PTY keeps it open, hash shows the PTY's output until it was quiet for 200 ms (2 s at the most, for a child that keeps writing), so such a command's prompt comes about 200 ms late, and what the child writes after that is lost. - Ctrl+C death. A PTY command's death of Ctrl+C ends the line, as bash ends one when a job dies of it (a command that kills itself with SIGINT too).
- Pipeline tails and redirected stdin. A command whose output is the terminal but whose stdin is not (
ls | less,prog < file) gets no PTY: its stdout and stderr are the terminal itself, as in bash. hash keeps neither:copy, the agent's context and?? explain, error learning, issue drafts and did-you-mean see its status but none of its output or error text. Passing its stderr through hash instead reorders it against its stdout.
- A command that is not found or cannot run. The interactive shell draws its own box for it (127 or 126) where
bash prints one line, but only when the command's stderr is the line's: with its stderr elsewhere
(
x=$(nosuch 2>/dev/null),2>&1 | ...) it gets bash's one line there, which2>/dev/nullsilences. - A command that cannot start. Status 126 with bash's message ("cannot execute binary file: Exec format error",
"Argument list too long", "bad interpreter"); the line goes on (
./tool || fallback). A file with no shebang that is not a binary runs as a/bin/shscript, where bash runs it with itself. - Expansion errors (
${x:?msg},set -uand an unset variable) end the line with status 1 and the interactive shell goes on; they end a script with 127. execends the shell once its command started. One that is not there (127) or cannot start (126) leaves the interactive shell running, as in bash; in a script it ends the shell, one that could not start with no EXIT trap.- Pipelines. In the bash dialect a pipeline's last command runs in a subshell, as bash runs it without
lastpipe(echo x | read vleavesvunset); in the zsh dialect it runs in the shell itself, as zsh does. - A deleted working directory. Commands run in it, as in bash, one the shell entered (
cd,pushd,popd) and deleted on the same line too. A subshell whose directory cannot be entered ((cd d && rmdir ../d && ls)) fails its commands with 126 and says why, where bash's subshell runs them there. - Messages. Errors the interpreter reports itself (a bad option,
exit 1 2) have nohash:prefix and their own wording;exit 1 2fails with 1 and the line goes on, where bash -i drops the rest of the line.
- See the configuration reference for how hash reads and filters startup files.
- A zsh file sourced in bash. bash runs
source plugin.zshas bash and fails on its zsh lines. hash, in the bash dialect, filters a zsh file (*.zsh,.zshrc) thatsourceor.reads as it filters a migrated one first: its zsh-only lines are skipped and the rest runs. - A sourced file that does not parse. Both run its statements before the syntax error, except in a startup file,
where hash runs none of it and prints one line for the file (
hash: ~/x.sh: not loaded: line 24: ...) where bash prints the error. A file the compatibility filter takes (a zsh file, a startup file of the migrated shell) runs its statements before the one the filter stops at, and the line says so (hash: ~/x.zsh: not loaded from line 24: ...). The status is 2 where bash's is 1. - zsh's setup builtins. bash has no
setopt,bindkey,zstyle,compdef,autoload,zmodload,zle,compinitoradd-zsh-hook(command not found). hash runs them as no-ops anywhere, andis-at-leastas zsh's version check. shoptof interactive options. bash takesshopt -s histappend checkwinsizeand the like. hash takes them quietly and does nothing; other options its interpreter has not still fail.shopt -qof one says it is off.$-of the interactive shell. Both haveiin$-(hash -itoo, so an rc file'scase $- in *i*)guard passes underhash -ic), in every expansion of it (${-#*e},${#-}). hash's holds only the flags it has (aefinux), none of bash'sh,m,B,H,s.[[ -o interactive ]]holds there, as in zsh (bash has no such option).set -xtraces each$-in a function as${__hash_flags[$-]}, the read-only array hash serves it from;declare -fprints the function as it was written.