Skip to main content

ShellExecutor

Struct ShellExecutor 

Source
pub struct ShellExecutor {
Show 47 fields pub scriptname: Option<String>, pub scriptfilename: Option<String>, pub subshell_snapshots: Vec<SubshellSnapshot>, pub inline_env_stack: Vec<InlineEnvFrame>, pub current_command_glob_failed: Cell<bool>, pub jobs: JobTable, pub fpath: Vec<PathBuf>, pub history: Option<HistoryEngine>, pub completions: HashMap<String, CompSpec>, pub zstyles: Vec<zstyle_entry>, pub local_scope_depth: usize, pub pending_underscore: Option<String>, pub in_dq_context: u32, pub in_scalar_assign: u32, pub profiling_enabled: bool, pub compsys_cache: OnceCell<Option<CompsysCache>>, pub compinit_pending: Option<(Receiver<CompInitBgResult>, Instant)>, pub plugin_cache: Option<PluginCache>, pub deferred_compdefs: Vec<Vec<String>>, pub returning: Option<i32>, pub zsh_compat: bool, pub bash_compat: bool, pub posix_mode: bool, pub worker_pool: Arc<WorkerPool>, pub intercepts: Vec<Intercept>, pub async_jobs: HashMap<u32, Receiver<(i32, String)>>, pub next_async_id: u32, pub redirect_scope_stack: Vec<Vec<(i32, i32)>>, pub multios_scope_stack: Vec<Vec<(i32, JoinHandle<()>)>>, pub exec_redirs_permanent: bool, pub pipe_output_pending: bool, pub pipe_output_scope: Option<usize>, pub redirect_failed: bool, pub functions_compiled: HashMap<String, Chunk>, pub function_source: HashMap<String, String>, pub function_line_base: HashMap<String, i64>, pub function_def_file: HashMap<String, Option<String>>, pub prompt_funcstack: Vec<(String, i64, Option<String>)>, pub tied_array_to_scalar: HashMap<String, (String, String)>, pub ztest_pass_count: AtomicUsize, pub ztest_fail_count: AtomicUsize, pub ztest_skip_count: AtomicUsize, pub ztest_pass_total: AtomicUsize, pub ztest_fail_total: AtomicUsize, pub ztest_skip_total: AtomicUsize, pub ztest_run_failed: AtomicBool, pub ztest_suppress_stdout: bool, /* private fields */
}
Expand description

Port of the file-static globals + Estate chain Src/exec.c uses — execlist() (line 1349) drives every list, with execpline() (line 1668), execpline2() (line 1991), execsimple() (line 1290), and the per-WC_* execfuncs[] table (line 268) feeding off it. The Rust port collapses everything into one ShellExecutor so we don’t need thread-local globals.

Fields§

§scriptname: Option<String>

Mirrors C zsh’s file-static scriptname (Src/init.c). Used by PS4’s %N and the scriptname:line: … prefix on error messages. Inside a function, MUTATES to the function name (Src/exec.c:5903 scriptname = dupstring(name)). Init sets this in -c mode to the binary basename per init.c:479; when sourcing a file via source/bin_dot, it becomes the resolved file path; otherwise it falls back through $0$ZSH_ARGZERO.

§scriptfilename: Option<String>

Mirrors C zsh’s scriptfilename global (Src/init.c). Tracks the FILE BEING READ (vs scriptname which tracks the active function name during a call). Used by PS4’s %x and certain error-message prefixes that want the file location, NOT the function name.

At -c-mode init, scriptname == scriptfilename == “zsh” (Src/init.c:479). When entering a function, ONLY scriptname updates (exec.c:5903); scriptfilename stays at the outer file path, so %x inside a function still shows the file the function was called from.

§subshell_snapshots: Vec<SubshellSnapshot>

Stack of subshell-state snapshots. Each (…) subshell pushes a copy of variables/arrays/assoc_arrays at entry and pops/restores at exit. Without this, (x=inner; …); echo $x shows inner instead of the outer-scope value.

§inline_env_stack: Vec<InlineEnvFrame>

Stack of inline-assignment scopes — X=foo Y=bar cmd pushes a frame at the start, the assigns run inside it, and cmd returns into END_INLINE_ENV which restores both shell-vars and process-env to the pre-frame state. Each frame holds (name, prev_var, prev_env) per assigned name. zsh’s equivalent is the parser-level “addvar” list executed under addvars() (Src/exec.c) right before the command exec.

§current_command_glob_failed: Cell<bool>

Set by expand_glob’s no-match arm when nomatch is on (zsh default) — instructs the simple-command dispatcher to skip executing the current command, set last_status=1, and continue to the next command in the script. zsh’s bin_simple uses the errflag global for the same role: error printed, command suppressed, script continues. Without this we were calling process::exit(1) deep inside expand_glob, killing the whole shell on any unmatched glob even with multi-statement input. Cell because the no-match site only has a &self borrow.

§jobs: JobTable

jobs field.

§fpath: Vec<PathBuf>

fpath field.

§history: Option<HistoryEngine>

history field.

§completions: HashMap<String, CompSpec>§zstyles: Vec<zstyle_entry>§local_scope_depth: usize

Current function scope depth for local tracking.

§pending_underscore: Option<String>

Last arg of the currently-running command, deferred into $_ when the next command dispatches. zsh: $_ reflects the LAST command’s last arg, so echo hi; echo $_ prints hi (not the _ arg of echo $_ itself). Promoted in pop_args and host.exec before the command’s args are read.

§in_dq_context: u32

True while expanding inside a double-quoted context. Set by BUILTIN_EXPAND_TEXT mode 1 around expand_string calls. Used by parameter-flag application to suppress array-only flags ((o)/(O)/(n)/(i)/(M)/(u)) — zsh’s behaviour: those flags only fire in array context.

§in_scalar_assign: u32

True (>0) while expanding the RHS of a scalar assignment. Direct port of zsh’s PREFORK_SINGLE bit set by Src/exec.c::addvars line 2546 (prefork(vl, isstr ? (PREFORK_SINGLE|PREFORK_ASSIGN) : PREFORK_ASSIGN, ...)). Subst_port’s paramsubst reads this via ssub and suppresses (f) / (s:STR:) / (0) / (z) split flags per Src/subst.c:1759 + 3902, so y="${(f)x}" preserves x’s original separator (newlines) instead of re-joining with IFS-first-char (space).

§profiling_enabled: bool

profiling_enabled field.

§compsys_cache: OnceCell<Option<CompsysCache>>

compsys_cache field. SQLite mirror, opened on FIRST USE via ShellExecutor::compsys_cache.

It is a dbview/FTS mirror for inspection — the authoritative completion cache is the rkyv shards — so nothing on a normal command path touches it. Opening it in the constructor still cost every shell three file opens (compsys.db, -wal, -shm) plus WAL setup, including zshrs -f -c exit, which cannot consult it at all.

§compinit_pending: Option<(Receiver<CompInitBgResult>, Instant)>

compinit_pending field.

§plugin_cache: Option<PluginCache>

plugin_cache field.

§deferred_compdefs: Vec<Vec<String>>

deferred_compdefs field.

§returning: Option<i32>§zsh_compat: bool

zsh compatibility mode - use .zcompdump, fpath scanning, etc. Also serves as the --zsh parity-test flag: caches off, daemon off, plugin_cache replay off so every source re-runs the file fresh per Src/builtin.c:6080-6123 bin_dot semantics.

§bash_compat: bool

bash compatibility mode (--bash). Same parity-mode semantics as zsh_compat (caches/daemon/replay off) plus bash-specific behavior tweaks where bash 5.x diverges from zsh — e.g. BASH_VERSION / BASH_REMATCH exposed, [[ =~ ]] populates match indices the bash way, mapfile/readarray as builtins.

§posix_mode: bool

POSIX sh strict mode — no SQLite, no worker pool, no zsh extensions

§worker_pool: Arc<WorkerPool>

Worker thread pool for background tasks (compinit, process subs, etc.)

§intercepts: Vec<Intercept>

AOP intercept table: command/function name → advice chain. Glob patterns supported (e.g. “git ”, “”).

§async_jobs: HashMap<u32, Receiver<(i32, String)>>

Async job handles: id → receiver for (status, stdout)

§next_async_id: u32

Next async job ID

§redirect_scope_stack: Vec<Vec<(i32, i32)>>

Per-scope saved-fd stacks for Op::WithRedirectsBegin/End. Each entry is a Vec of (fd, saved_dup_fd) pairs taken from dup(fd) before the redirect was applied; with_redirects_end dup2s them back and closes.

§multios_scope_stack: Vec<Vec<(i32, JoinHandle<()>)>>

Per-scope MULTIOS tee state. Each entry is (pipe_write_fd, JoinHandle): the pipe write-end currently dup2’d onto the command’s fd, and the splitter thread that reads from the pipe read-end and writes to every collected target. Closed

  • joined by host_redirect_scope_end BEFORE the saved fds are restored so the splitter drains every byte the body wrote into the pipe. Bug #36 in docs/BUGS.md.
§exec_redirs_permanent: bool

True while applying a bare exec’s redirect list (exec 1>&-, exec 2>/dev/null — no command words). host_apply_redirect then skips pushing the saved fd into the enclosing scope so the fd change survives group/command teardown. c:Src/exec.c:3978-3986 — nullexec==1: “we specifically don’t restore the original fd’s before returning”; C’s per-execcmd save[] means exec’s redirs never enter the enclosing group’s save list either. Toggled by BUILTIN_EXEC_PERM_REDIRS.

§pipe_output_pending: bool

Set in a forked pipeline-stage child right after its stdout is dup2’d onto the pipe write-end. Consumed by the FIRST host_redirect_scope_begin (the stage command’s own redirect list) into pipe_output_scope. c:Src/exec.c:3722-3724 — addfd(forked, save, mfds, 1, output, 1, NULL): the pipe occupies mfds[1] in the SAME execcmd that processes the stage command’s redirect list.

§pipe_output_scope: Option<usize>

Index into redirect_scope_stack of the scope whose redirect list shares an execcmd with the pipeline output on fd 1. A write-side redirect of fd 1 applied at exactly this scope depth MULTIOS-splits (tees) instead of replacing — c:Src/exec.c: 2447-2480 addfd “split the stream”. Cleared when that scope ends.

§redirect_failed: bool

Set by host_apply_redirect when a redirect target couldn’t be opened (permission denied, no such directory, etc). The next builtin/command checks this at entry and short-circuits with status 1 instead of running. Mirrors zsh’s “command skip” on redirect failure.

§functions_compiled: HashMap<String, Chunk>

Compiled function bodies — name → fusevm::Chunk. Populated by BUILTIN_REGISTER_FUNCTION (from FunctionDef lowering) and lazily by ZshrsHost::call_function when only an AST exists in self.functions (autoloaded, sourced, etc.). Op::CallFunction dispatches through here.

§function_source: HashMap<String, String>

Canonical source text for functions. Populated by autoload paths (the raw file/cache body), runtime FuncDef compile (the parsed source span), and unfunction removal. Used by introspection (whence, which, typeset -f) instead of reconstructing from a ShellCommand AST. When a function is in functions_compiled but not here, introspection falls back to text::getpermtext(self.functions[name]).

§function_line_base: HashMap<String, i64>

first_body_line - 1 per compiled function — matches inner ZshCompiler::lineno_offset / zsh funcstack->flineno combined with relative $LINENO for Src/prompt.c:909 %I.

§function_def_file: HashMap<String, Option<String>>

scriptfilename when BUILTIN_REGISTER_COMPILED_FN ran — %x inside a function (prompt.c:931-934) reads funcstack->filename.

§prompt_funcstack: Vec<(String, i64, Option<String>)>

Innermost-last stack of active compiled-call frames for prompt %I / %x.

§tied_array_to_scalar: HashMap<String, (String, String)>

Scalar→(array, sep) tie table set up by typeset -T VAR var [SEP]. Array→(scalar, sep) reverse-tie table. Used by BUILTIN_SET_ARRAY to join the array elements with sep and mirror to the scalar side.

§ztest_pass_count: AtomicUsize

Per-block pass count (reset by ztest_run).

§ztest_fail_count: AtomicUsize

Per-block fail count (reset by ztest_run).

§ztest_skip_count: AtomicUsize

Per-block skip count (reset by ztest_run).

§ztest_pass_total: AtomicUsize

Cumulative pass total across the run.

§ztest_fail_total: AtomicUsize

Cumulative fail total across the run.

§ztest_skip_total: AtomicUsize

Cumulative skip total across the run.

§ztest_run_failed: AtomicBool

Sticky failure flag — set by any ztest_run that observed fails; the CLI runner reads this so a test that asserts then exits 0 still counts as a failed file.

§ztest_suppress_stdout: bool

Suppress per-assertion / lines on stderr. Set by the worker runner inside the forked child when it has already redirected fd 2 to a tmp file (we still want the lines, but only after the runner re-emits them under print_lock to avoid line-tearing).

Implementations§

Source§

impl ShellExecutor

Source

pub fn drain_compinit_bg(&mut self)

Non-blocking drain of background compinit results. Call this before any completion lookup (prompt, tab-complete, etc.). If the background thread hasn’t finished yet, this is a no-op.

Source§

impl ShellExecutor

Source

pub fn save_fd_for_scope(&mut self, fd: i32)

Apply a single redirection. The current scope’s saved-fd vec gets a dup of the original fd so it can be restored by host_redirect_scope_end. op_byte matches fusevm::op::redirect_op::*. Park the CURRENT contents of fd in the enclosing redirect scope so host_redirect_scope_end can restore them.

c:Src/exec.c:2421-2443 — addfd’s “starting a new multio” arm:

if (!forked && save[fd1] == -2) {
    if (fd1 == fd2) save[fd1] = -1;
    else {
        int fdN = movefd(fd1);
        /* fd1 may already be closed here, so
         * ignore bad file descriptor error */
        if (fdN < 0) { if (errno != EBADF) { … } }
        …
        save[fd1] = fdN;
    }
}

A save[] slot of -1 therefore means “fd1 was CLOSED before this redirection”, and fixfds (c:4522-4532) feeds it to redup(-1, i), whose first arm is zclose(y) (c:Src/utils.c: 2047-2048). So zsh CLOSES the fd again at teardown rather than leaving the redirection behind — print x 3<file; cat <&3 reports “3: bad file descriptor” in zsh.

c:Src/exec.c:3978-3986 — a bare exec (nullexec==1) “specifically doesn’t restore the original fd’s”, so nothing is parked for it.

Source

pub fn host_apply_redirect(&mut self, fd: u8, op_byte: u8, target: &str)

host_apply_redirect — see implementation.

Source

pub fn host_redirect_scope_begin(&mut self, _count: u8)

Push a fresh redirect scope. _count is informational — the actual saved fds are appended by host_apply_redirect into the top scope.

Source

pub fn unwind_redirect_scopes_to(&mut self, depth: usize)

Restore every redirect scope opened above depth.

c:Src/exec.c:4364 — fixfds(save) runs on EVERY exit path out of execcmd_exec, the one an early return takes out of a compound command carrying redirections included (while … done < f, { …; return } < f, if …; then return; fi < f). zshrs compiles return to a Jump past the body’s WithRedirectsEnd, so that scope stayed on the stack and its saved fds never came back — the caller inherited the callee’s redirected fd.

gitstatus hit this: gitstatus.plugin.zsh’s daemon sources gitstatus/install, whose _gitstatus_install_main returns out of while … done <"$gitstatus_dir"/install.info. fd 0 stayed on install.info instead of reverting to the request FIFO, so gitstatusd read EOF on startup and exited (“EOF. Exiting.”), gitstatus_start failed, VCS_STATUS_REMOTE_URL came back empty and powerlevel10k rendered the generic git icon in place of the per-forge one.

Source

pub fn host_redirect_scope_end(&mut self)

Pop the top redirect scope, restoring saved fds.

Source

pub fn host_set_pending_stdin(&mut self, content: String)

Set up content as stdin (fd 0) for the next command. Used by Op::HereDoc(idx) and Op::HereString.

c:Src/exec.c:4655 getherestr — C writes the body to a TEMP FILE (gettempfile → write_loop → close → reopen O_RDONLY → unlink), NOT a pipe. The previous pipe+writer-thread shape SIGPIPE’d the whole shell when the consumer never read the body (: <<< ${(F)x/y} — D04parameter chunk 211, flaky rc=141): the redirect-scope teardown closed the read end while the detached thread was still in write_all, and the shell’s SIGPIPE disposition is SIG_DFL. A temp file has no reader/writer coupling — matching C exactly, including lseek-ability of fd 0, which pipes don’t give.

Source

pub fn host_exec_external(&mut self, args: &[String]) -> i32

Spawn an external command using zshrs’s full dispatch logic (intercepts, command_hash, redirect handling). Used by ZshrsHost::exec so the bytecode VM’s Op::Exec and Op::CallFunction external fallback get the same semantics as the tree-walker’s execute_external rather than a plain Command::new shortcut. Returns the exit status.

Source§

impl ShellExecutor

Source

pub fn set_scalar(&mut self, name: String, value: String)

Set a scalar parameter via the canonical paramtab (Src/params.c:3350 setsparam). The single store.

Source

pub fn pparams(&self) -> Vec<String>

Read positional parameters from canonical PPARAMS Mutex<Vec<String>> (Src/init.c:pparams). The single store.

Source

pub fn set_pparams(&mut self, params: Vec<String>)

Write positional parameters to canonical PPARAMS.

Source

pub fn param_flags(&self, name: &str) -> i32

Read PM_* type flags from the paramtab Param entry. Used by SET_VAR / += arms (case-fold, integer-add, readonly guard). Returns 0 when the name isn’t in paramtab. Mirrors the C source’s direct pm->node.flags & PM_INTEGER checks.

Source

pub fn is_readonly_param(&self, name: &str) -> bool

readonly / typeset -r / read-only-by-design (LINENO, PPID, $$, $?, $!, …) — match user-side rejection in C’s assignstrvalue at Src/params.c:2699-2703 which gates on pm->node.flags & PM_READONLY where the IPDEF4 family declares PM_READONLY_SPECIAL = PM_SPECIAL | PM_READONLY | PM_RO_BY_DESIGN (all three bits set together), which init_partab_params now stamps in full. The PM_RO_BY_DESIGN arm below is therefore no longer the IPDEF4 rows’ only read-only marker — it remains because private params (c:Src/Modules/param_private.c:174) carry PM_RO_BY_DESIGN WITHOUT PM_READONLY and need the scope-gated test. Bug #418-family / test_lineno_intrinsic_readonly.

Source

pub fn last_status(&self) -> i32

Most-recent-command exit status. Reads canonical builtin::LASTVAL AtomicI32 (Src/builtin.c:6443).

Source

pub fn set_last_status(&mut self, status: i32)

Write the most-recent-command exit status. The canonical store is builtin::LASTVAL; this is the single setter. Used everywhere $? / %? / errexit / ZERR trap read.

Source

pub fn set_array(&mut self, name: String, value: Vec<String>)

Set an indexed array parameter via canonical paramtab (setaparam, Src/params.c:3595). The single store.

Source

pub fn set_assoc(&mut self, name: String, value: IndexMap<String, String>)

Set an associative array parameter via canonical sethparam (Src/params.c:3602). The single store.

Source

pub fn scalar(&self, name: &str) -> Option<String>

Read a scalar parameter. Mirrors C getsparam at Src/params.c:3076 — reads through paramtab, falls back to special-var hooks and env.

Source

pub fn array(&self, name: &str) -> Option<Vec<String>>

Read an array parameter via canonical getaparam (Src/params.c:3101).

Source

pub fn assoc(&self, name: &str) -> Option<IndexMap<String, String>>

Read an associative array parameter from canonical paramtab_hashed_storage. Mirrors C gethparam at Src/params.c:3115 — returns the typed IndexMap.

Source

pub fn has_scalar(&self, name: &str) -> bool

Test whether a scalar parameter exists in paramtab. Mirrors the C paramtab->getnode(name) != NULL check.

Source

pub fn has_array(&self, name: &str) -> bool

Test whether an array parameter exists in paramtab. Mirrors getaparam(name).is_some() (PM_ARRAY + populated u_arr, with digit-first-name rejection and nameref deref) WITHOUT cloning the backing vector — getaparam returns an owned Vec<String>, so a bare existence probe on a large array copied every element. Hot in the subscript-store dispatch (a[i]=v in a loop), so keep it a flag read.

Source

pub fn has_assoc(&self, name: &str) -> bool

Test whether an associative array parameter exists. Reads canonical paramtab_hashed_storage (Src/params.c hashed PM_HASHED slot).

Source

pub fn unset_assoc(&mut self, name: &str)

Unset an associative array parameter via canonical unsetparam (Src/params.c:3819) — PM_READONLY rejection, stdunsetfn dispatch, env clear. Also clears the zshrs-side paramtab_hashed_storage parallel IndexMap shadow.

Source

pub fn alias(&self, name: &str) -> Option<String>

Read a regular (non-global) alias value. Reads canonical aliastab (Src/hashtable.c:1186). Filters out aliases that have the ALIAS_GLOBAL flag set so the regular-alias slot is distinct from the global-alias slot, mirroring C’s two separate dispatch paths via aliasflags checks.

Source

pub fn set_alias(&mut self, name: String, value: String)

Set a regular alias. Writes canonical aliastab with ALIAS_GLOBAL bit cleared.

Source

pub fn set_global_alias(&mut self, name: String, value: String)

Set a global alias (alias -g). Writes canonical aliastab with ALIAS_GLOBAL bit set.

Source

pub fn set_suffix_alias(&mut self, name: String, value: String)

Set a suffix alias (alias -s ext=cmd). Writes canonical sufaliastab with ALIAS_SUFFIX node flag — mirrors C Src/builtin.c:4480-4481 (flags1 |= ALIAS_SUFFIX; ht = sufaliastab;) → c:4527 (createaliasnode(value, flags1)). Without ALIAS_SUFFIX in node.flags, ${saliases[k]} / ${(k)saliases} introspection (parameter.c:1953/2018) fails because both paths strict-equality-match flags == ALIAS_SUFFIX.

Source

pub fn alias_entries(&self) -> Vec<(String, String)>

Snapshot the alias map as a sorted Vec<(name, value)>, only entries WITHOUT the ALIAS_GLOBAL flag (regular aliases).

Source

pub fn global_alias_entries(&self) -> Vec<(String, String)>

Snapshot the global-alias entries (ALIAS_GLOBAL flag set).

Source

pub fn suffix_alias_entries(&self) -> Vec<(String, String)>

Snapshot the suffix-alias entries.

Source

pub fn unset_array(&mut self, name: &str)

Unset an array parameter. Direct port of unsetparam_pm for a PM_ARRAY Param. Mirrors are kept for now while the field transitions. Unset an array parameter via canonical unsetparam (Src/params.c:3819). Routes through the C-faithful port that runs PM_NAMEREF skip + PM_READONLY rejection via unsetparam_pm + stdunsetfn dispatch + pm.old scope restore. Inline tab.remove(name) skipped all four.

Source

pub fn unset_scalar(&mut self, name: &str)

Unset a scalar parameter via canonical unsetparam. Same C-faithful path as unset_array; the C unsetparam itself is type-agnostic and dispatches through PM_TYPE inside.

Source

pub fn new_worker(pool: Arc<WorkerPool>) -> Self

Lightweight executor for a POOL WORKER THREAD. Unlike [new] (a full session bootstrap that re-derives PWD, imports the environment, seeds OPTS_LIVE, and writes ~30 default params into the GLOBAL param table), this constructs ONLY the per-executor struct fields and touches NO global state. A worker shares the already-populated, RwLock-synchronized globals (params / functions / options); re-seeding them here would clobber the live main session’s values (IFS, OPTIND, $_, user options, …).

The worker pool is shared (Arc) — a worker never spins up its own pool. Per-worker SQLite caches (compsys / plugin) and the history engine are left None: a worker runs short compute bodies, not interactive editing.

Phase 1 of the in-process thread-execution model (replaces the subprocess-forking parallel builtins). The caller runs the body under ExecutorContext::enter(&mut wex) so the VM’s thread_local executor resolves on the worker thread; param writes flow to the shared globals.

Source

pub fn new() -> Self

new — see implementation.

Source

pub fn execute_script_file(&mut self, file_path: &str) -> Result<i32, String>

Execute a script file with bytecode caching — skips lex+parse+compile on cache hit. Bytecode is stored in rkyv keyed by (path, mtime).

Source

pub fn execute_script_zsh_pipeline( &mut self, script: &str, ) -> Result<i32, String>

zshrs’s script entry: lex + parse + compile + run, then the end-of-script hooks. eval, source, trap bodies and autoload registration all funnel through here.

Source

pub fn execute_zwc_program(&mut self, script: &str) -> Result<i32, String>

Run the TEXT that getpermtext reconstructed from an already-compiled .zwc program.

c:Src/init.c:1618-1622 — the compiled arm of source() is execode(prog, 1, 0, "filecode"). The wordcode runs as it stands and NOTHING is lexed; a .zwc is quote-resolved once, at zcompile time.

!!! WARNING: RUST-ONLY HELPER !!! zshrs has no execute-the-wordcode path — it deparses the program back to source (getpermtext) and lexes it again — so the round trip is lossless only while the lexer reads quotes the way the deparse writes them. untokenize (c:Src/exec.c:2134) renders EVERY quote null through ztokens[Snull - Pound], and that entry is a bare single quote (c:Src/lex.c:38), so a closing null followed by an opening one comes back out as two adjacent quotes. Under RCQUOTES the lexer reads that pair inside a quoted word as one LITERAL quote (c:Src/lex.c:1328) instead of as two delimiters, so the openshift-aliases plugin’s alias opodr='oc …=''{…}''' — which zcompile resolved with no literal quotes at all — re-lexed with two of them. The deparse spelling is by construction the DEFAULT-option spelling, so the option is cleared for the compile to restore C’s “not lexed at all” property.

It is cleared for the COMPILE ONLY. A .zwc that does setopt rcquotes (zsh-expand’s plugin entry does, at its line 39) must still set the option for real, and that setting must outlive the source — so the previous value is restored before the chunk RUNS, not after. The same split applies to alias expansion: a function or eval body the program runs is lexed at RUNTIME and must see the live alias table.

Source

pub fn execute_script_per_command( &mut self, script: &str, ) -> Result<i32, String>

Run script the way C runs a PLAIN sourced file: parse ONE event, execute it, parse the next — so lexer-time state that one line establishes is in force when the next line is lexed.

c:Src/init.c:1618-1641 — source() has two arms. A file that was already compiled (try_source_file found a .zwc) runs whole, as one program: execode(prog, 1, 0, "filecode") (c:1621). A plain file runs through the per-command loop: /* loop through the file to be sourced */ switch (loop(0, 0)) (c:1626-1627), whose body is lexinit(); parse_event(ENDINPUT); … execode(prog, 0, 0, "file") (c:155-220). This is the second arm.

The difference is observable whenever a line changes something the LEXER consults, because a whole-file compile lexes every line with the state the file STARTED with:

alias greet='print -r -- hello'   # takes effect at execution time
greet                             # …but this line is lexed after it

Same for setopt rcquotes (c:Src/lex.c:1326), unsetopt aliases, and a syntax error late in the file (C has already run the good lines).

Re-entrancy. Every nested context — $(source f), `source f`, eval "source f", a pipe stage, a ( … ) subshell, a source inside a sourced file — reaches here through the normal builtin path, so this must be safe to enter while an outer instance of itself is parked mid-file. Two properties make it so, and both are deliberate:

  • It never touches the shell’s INPUT STACK. C’s source points SHIN at the file (c:1584) and lets loop’s ingetc pull from it; doing that here would fight the outer reader for the one global. Instead the file body is installed as the lexer’s own LEX_INPUT window under strinbeg — the exact parking [parse_isolated] uses for a command-substitution body — and the outer window is saved on the Rust stack and restored on the way out. Nesting is then just stack discipline.
  • It never dispatches through execode. execode (src/ported/exec.rs) runs its program on the installed SESSION executor, which is the right one only for the top-level REPL; from inside a command substitution the live executor is the sub-VM that owns the capture. Each event is compiled and run here on self — the same executor execute_script would have used — via Self::run_chunk. $(source f) therefore captures exactly what $(…) captures from any other builtin.

Returns the file’s $?. Err only for a VM error, as Self::run_chunk reports it.

Source

pub fn execute_script(&mut self, script: &str) -> Result<i32, String>

execute_script — see implementation.

Source

pub fn execute_script_captured(&mut self, script: &str) -> (i32, String)

Run script with stdout AND stderr captured, returning (exit status, output) — the entry point for an embedder that owns the terminal (a TUI), where a stray echo corrupts the display.

A shell cannot capture its output into an in-process buffer the way a single-runtime language can: a forked child writes fd 1 directly and knows nothing about the parent’s buffers. The capture is therefore at fd level, and it differs from $(…) in the one way that matters to an embedder: Self::run_command_substitution runs on a sub-VM, as a subshell must, so a variable it sets is gone afterwards. This runs the script on THIS VM, so state persists across captured runs exactly as it does across ordinary Self::execute_script calls.

The saved fds go through movefd to land at fd >= 10 and marked FDT_INTERNAL, per zsh’s invariant that shell-internal fds never live below 10 — otherwise a script doing exec 9>&- closes the capture’s own bookkeeping. A temp file, not a pipe, receives the output: with no concurrent reader, a pipe deadlocks the moment a script writes past the 64 KiB buffer.

§Concurrency contract

While a capture is in flight, no other thread in the process may write fd 1 or fd 2. POSIX has no per-thread fd table, so pointing fd 1 at the capture points it there for every thread at once; any byte another thread writes during the window lands in the returned String instead of on the terminal. The CAPTURE_LOCK below excludes a second capture, which is all a lock can do — a thread that never calls this function (a logger, a progress meter, a test harness’s own reporter) is not excluded by anything, and its output is silently absorbed.

This is not a gap that a different capture mechanism closes. C zsh dodges it for $(…) by forking: getoutput (Src/exec.c:4816) calls zfork and only the child does redup(pipes[1], 1) (Src/exec.c:4837), so the parent’s fd 1 is never touched — but the child then runs entersubsh (Src/exec.c:4838) and a variable it sets is gone. Forking here would throw away the one property this call exists to provide (state persists on THIS VM across captured runs), so the cost is paid as a contract instead: capture from one thread, and quiesce the rest.

Source

pub fn execute_program(&mut self, program: &ZshProgram) -> i32

Run an ALREADY-PARSED program (the back half of execute_script_zsh_pipeline): compile the ZshProgram to a fusevm Chunk and run it. Used by the ported loop() REPL (Src/init.c:220 execode), which parses via parse_event and hands the program here through the execute_program exec hook. Returns the resulting $? (1 on a compile/run error).

Source

pub fn function_exists(&self, name: &str) -> bool

Whether name is a known function. Checks the compiled-functions table and the autoload-pending registry — autoload foo should make whence foo/type foo/functions foo recognize foo as a function before it’s actually loaded. Doesn’t trigger autoload itself; use maybe_autoload first if you need to load before introspecting.

Source

pub fn function_names(&self) -> Vec<String>

Sorted list of every known function name (union of compiled + source).

Source

pub fn run_function_body_only( &mut self, name: &str, args: &[String], ) -> Option<i32>

Dispatch a function by name. Thin passthru — autoload-materialize the body if needed, build a synthetic shfunc, and hand off to the canonical doshfunc port (Src/exec.c:5823src/ported/exec.rs::doshfunc). doshfunc owns ALL scope management (starttrapscope/endtrapscope, startparamscope/ endparamscope, funcdepth bump, pipestats save/restore, scriptname snapshot, BREAKS/CONTFLAG/LOOPS/RETFLAG snapshot+restore, $0 override via FUNCTIONARGZERO, etc.). The body run itself is the Rust-only adaptation passed via the body_runner closure because zshrs runs function bodies through fusevm bytecode (not C zsh’s wordcode walker via runshfunc).

Returns None when the name isn’t a known function so the caller can fall through to external dispatch. Body-only counterpart to [dispatch_function_call] — runs the function body WITHOUT wrapping in doshfunc. Used as the body_runner closure target by src/ported/ callers that already wrap their own crate::ported::exec::doshfunc(...) call (so going back through dispatch_function_call would double-wrap the scope). Mirrors C’s runshfunc(prog, wrappers, name) at exec.c:6042 from doshfunc’s perspective.

Source

pub fn dispatch_function_call( &mut self, name: &str, args: &[String], ) -> Option<i32>

Source

pub fn compsys_cache(&self) -> Option<&CompsysCache>

run_command_substitution — see implementation. The SQLite mirror, opened the first time anything asks for it.

Returns None when no cache file exists yet (or it failed to open), which is the same answer the eager constructor produced.

Source

pub fn run_command_substitution(&mut self, cmd_str: &str) -> String

Source

pub fn run_shared_state_substitution(&mut self, cmd_str: &str) -> String

ksh93 funsub ${ list; } / mksh valsub ${| list; } — capture the output of cmd_str WITHOUT the subshell isolation $( … ) applies.

ksh(1), Command Substitution: “${ command;} … the command is executed in the current shell environment”, so an assignment or a cd inside survives: ksh -c 'x=0; y=${ x=5; print -n out; }; print "x=$x y=$y"'x=5 y=out, where the same body in $( … ) leaves x at 0. mksh behaves identically for both of its forms.

Same capture machinery as $( … ) — only the parent-state snapshot/restore is skipped, which is exactly the difference the two references document.

!!! RUST-ONLY ENTRY POINT — zsh has no funsub/valsub !!!

Source§

impl ShellExecutor

Source

pub fn run_trap(&mut self, signal: &str)

Execute the trap body for a signal name from the REPL signal loop (bins/zshrs.rs CtrlC/CtrlD dispatch). Thin passthru to traps_table lookup + execute_script — kept as a method because the REPL loop owns &mut ShellExecutor and needs a single call point. The async signal-handler dispatch path goes through crate::ported::signals::dotrap instead.

Source§

impl ShellExecutor

Source

pub fn expand_glob(&self, pattern: &str) -> Vec<String>

Expand glob pattern via canonical glob_path (port of Src/glob.c::zglob). Adds executor-side current_command_glob_failed cell so the dispatch layer skips the current command on NOMATCH + looks_like_glob instead of exiting the shell.

Source§

impl ShellExecutor

Source

pub fn enter_posix_mode(&mut self)

enter_posix_mode — see implementation.

Source

pub fn enter_ksh_mode(&mut self)

enter_ksh_mode — see implementation.

Source

pub fn enter_dash_mode(&mut self)

enter_dash_mode — strict-dash (Debian Almquist Shell) runtime. Same executor setup as [enter_posix_mode] (dash IS sh for every option), but calls emulate("dash") so the Rust-only DASH_STRICT flag is raised (and NOT cleared, as emulate("sh") would). See src/extensions/dash_mode.rs.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> ArchivePointee for T

Source§

type ArchivedMetadata = ()

The archived version of the pointer metadata for this type.
Source§

fn pointer_metadata( _: &<T as ArchivePointee>::ArchivedMetadata, ) -> <T as Pointee>::Metadata

Converts some archived metadata to the pointer metadata for itself.
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<F, W, T, D> Deserialize<With<T, W>, D> for F
where W: DeserializeWith<F, T, D>, D: Fallible + ?Sized, F: ?Sized,

Source§

fn deserialize( &self, deserializer: &mut D, ) -> Result<With<T, W>, <D as Fallible>::Error>

Deserializes using the given deserializer
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<A, B, T> HttpServerConnExec<A, B> for T
where B: Body,

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> LayoutRaw for T

Source§

fn layout_raw(_: <T as Pointee>::Metadata) -> Result<Layout, LayoutError>

Gets the layout of the type.
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> Pointee for T

Source§

type Metadata = ()

The type for metadata in pointers and references to Self.
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more