Skip to main content

zsh/extensions/
intercepts.rs

1//! Command intercept / advice machinery — extension; no zsh C counterpart.
2#[allow(unused_imports)]
3use crate::ported::exec::ShellExecutor;
4#[allow(unused_imports)]
5use std::collections::HashMap;
6
7/// AOP advice type — before, after, or around.
8#[derive(Debug, Clone)]
9/// Aspect-oriented advice classification.
10/// zshrs-original — no C zsh counterpart. C zsh's closest
11/// analog is the function-wrapper hook in Src/module.c
12/// (`addwrapper()`, used by `zsh/zprof`), but per-function
13/// before/after/around AOP intercepts are unique to zshrs.
14pub enum AdviceKind {
15    /// Run code before the command executes.
16    Before,
17    /// Run code after the command executes. $? and INTERCEPT_MS available.
18    After,
19    /// Wrap the command. Code must call `intercept_proceed` to run original.
20    Around,
21}
22
23/// An intercept registration.
24#[derive(Debug, Clone)]
25/// One AOP intercept registered against a function pattern.
26/// zshrs-original — no C counterpart.
27pub struct Intercept {
28    /// Pattern to match command names. Supports glob: "git *", "_*", "*".
29    pub pattern: String,
30    /// What kind of advice.
31    pub kind: AdviceKind,
32    /// Shell code to execute as advice.
33    pub code: String,
34    /// Unique ID for removal.
35    pub id: u32,
36}
37
38/// Match an intercept pattern against a command name or full command string.
39/// Supports: exact match, glob ("git *", "_*", "*"), or "all".
40pub(crate) fn intercept_matches(pattern: &str, cmd_name: &str, full_cmd: &str) -> bool {
41    if pattern == "*" || pattern == "all" {
42        return true;
43    }
44    if pattern == cmd_name {
45        return true;
46    }
47    if pattern.contains('*') || pattern.contains('?') {
48        if let Ok(pat) = glob::Pattern::new(pattern) {
49            return pat.matches(cmd_name) || pat.matches(full_cmd);
50        }
51    }
52    false
53}
54
55// ===========================================================
56// Methods moved verbatim from src/ported/exec.rs because their
57// C counterpart's source file maps 1:1 to this Rust module.
58// Phase: drift
59// ===========================================================
60
61// BEGIN moved-from-exec-rs
62impl crate::ported::exec::ShellExecutor {
63    /// Check intercepts for a command. Returns Some(result) if an around
64    /// advice fully handled the command, None to proceed normally.
65    pub(crate) fn run_intercepts(
66        &mut self,
67        cmd_name: &str,
68        full_cmd: &str,
69        args: &[String],
70    ) -> Option<Result<i32, String>> {
71        // Collect matching intercepts (clone to avoid borrow issues)
72        let matching: Vec<Intercept> = self
73            .intercepts
74            .iter()
75            .filter(|i| intercept_matches(&i.pattern, cmd_name, full_cmd))
76            .cloned()
77            .collect();
78
79        if matching.is_empty() {
80            return None;
81        }
82
83        // Set INTERCEPT_NAME and INTERCEPT_ARGS for advice code
84        self.set_scalar("INTERCEPT_NAME".to_string(), cmd_name.to_string());
85        self.set_scalar("INTERCEPT_ARGS".to_string(), args.join(" "));
86        self.set_scalar("INTERCEPT_CMD".to_string(), full_cmd.to_string());
87
88        // Run before advice
89        for advice in matching
90            .iter()
91            .filter(|i| matches!(i.kind, AdviceKind::Before))
92        {
93            let _ = self.execute_advice(&advice.code);
94        }
95
96        // Check for around advice — first match wins
97        let around = matching
98            .iter()
99            .find(|i| matches!(i.kind, AdviceKind::Around));
100
101        let t0 = std::time::Instant::now();
102
103        let result = if let Some(advice) = around {
104            // Around advice: set INTERCEPT_PROCEED flag, run advice code.
105            // If advice calls `intercept_proceed`, the original command runs.
106            self.set_scalar("__intercept_proceed".to_string(), "0".to_string());
107            let advice_result = self.execute_advice(&advice.code);
108
109            // Check if intercept_proceed was called
110            let proceeded = self
111                .scalar("__intercept_proceed")
112                .map(|v| v == "1")
113                .unwrap_or(false);
114
115            if proceeded {
116                // The original command was already executed inside the advice
117                advice_result
118            } else {
119                // Advice didn't call proceed — command was suppressed
120                advice_result
121            }
122        } else {
123            // No around advice — run the original command.
124            // We return None to let the normal dispatch continue.
125            // But we still need after advice to fire, so we can't return None here
126            // if there are after advices. Run the command ourselves.
127            let has_after = matching.iter().any(|i| matches!(i.kind, AdviceKind::After));
128            if !has_after {
129                // Only before advice, no after — let normal dispatch continue
130                return None;
131            }
132
133            // Has after advice — we must run the command and then run after advice
134            self.run_original_command(cmd_name, args)
135        };
136
137        let elapsed = t0.elapsed();
138
139        // Set timing variable for after advice
140        let ms = elapsed.as_secs_f64() * 1000.0;
141        self.set_scalar("INTERCEPT_MS".to_string(), format!("{:.3}", ms));
142        self.set_scalar("INTERCEPT_US".to_string(), format!("{:.0}", ms * 1000.0));
143
144        // Run after advice
145        for advice in matching
146            .iter()
147            .filter(|i| matches!(i.kind, AdviceKind::After))
148        {
149            let _ = self.execute_advice(&advice.code);
150        }
151
152        // Clean up
153        self.unset_scalar("INTERCEPT_NAME");
154        self.unset_scalar("INTERCEPT_ARGS");
155        self.unset_scalar("INTERCEPT_CMD");
156        self.unset_scalar("INTERCEPT_MS");
157        self.unset_scalar("INTERCEPT_US");
158        self.unset_scalar("__intercept_proceed");
159
160        Some(result)
161    }
162    /// Execute the original command (used by around/after intercept dispatch).
163    /// Execute advice code — dispatches @ prefix to stryke (fat binary),
164    /// everything else to the shell parser. No fork. Machine code speed.
165    pub(crate) fn execute_advice(&mut self, code: &str) -> Result<i32, String> {
166        let code = code.trim();
167        if code.starts_with('@') {
168            let stryke_code = code.trim_start_matches('@').trim();
169            if let Some(status) = crate::try_stryke_dispatch(stryke_code) {
170                self.set_last_status(status);
171                return Ok(status);
172            }
173            // No stryke handler (thin binary) — fall through to shell
174        }
175        self.execute_script(code)
176    }
177    pub(crate) fn run_original_command(&mut self, cmd_name: &str, args: &[String]) -> Result<i32, String> {
178        // Function dispatch via the compiled pipeline (functions_compiled
179        // first, falls back to legacy AST recompile if needed).
180        if let Some(status) = self.dispatch_function_call(cmd_name, args) {
181            return Ok(status);
182        }
183        // External command
184        self.execute_external(cmd_name, args, &[])
185    }
186}
187// END moved-from-exec-rs