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