#[allow(unused_imports)]
use crate::ported::vm_helper::ShellExecutor;
#[allow(unused_imports)]
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub enum AdviceKind {
Before,
After,
Around,
}
#[derive(Debug, Clone)]
pub struct Intercept {
pub pattern: String,
pub kind: AdviceKind,
pub code: String,
pub id: u32,
}
thread_local! {
static LEX_ININTERCEPT: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
pub(crate) fn note_command_word(word: &str, quoted: bool) {
LEX_ININTERCEPT.set(word == "intercept" && !quoted);
}
pub(crate) fn wants_block() -> bool {
LEX_ININTERCEPT.get() && !crate::dash_mode::zsh_dropin()
}
pub(crate) fn disarm() {
LEX_ININTERCEPT.set(false);
}
pub(crate) fn scan_block_body<G>(mut getc: G) -> Option<String>
where
G: FnMut() -> Option<char>,
{
let mut body = String::new();
let mut depth: u32 = 1;
let mut pending_heredocs: Vec<(String, bool)> = Vec::new();
let mut at_word_start = true;
loop {
let c = getc()?;
match c {
'\\' => {
body.push(c);
body.push(getc()?);
at_word_start = false;
continue;
}
'\'' => {
body.push(c);
loop {
let q = getc()?;
body.push(q);
if q == '\'' {
break;
}
}
at_word_start = false;
continue;
}
'"' => {
body.push(c);
scan_double_quoted(&mut getc, &mut body)?;
at_word_start = false;
continue;
}
'`' => {
body.push(c);
loop {
let q = getc()?;
body.push(q);
match q {
'\\' => body.push(getc()?),
'`' => break,
_ => {}
}
}
at_word_start = false;
continue;
}
'#' if at_word_start => {
body.push(c);
loop {
match getc() {
Some('\n') => {
body.push('\n');
break;
}
Some(ch) => body.push(ch),
None => return None,
}
}
at_word_start = true;
if !pending_heredocs.is_empty() {
drain_heredocs(&mut getc, &mut body, &mut pending_heredocs)?;
}
continue;
}
'<' => {
body.push(c);
let (delim, stopped_at) = scan_heredoc_intro(&mut getc, &mut body)?;
if let Some(d) = delim {
pending_heredocs.push(d);
}
at_word_start = false;
if stopped_at == Some('\n') {
at_word_start = true;
if !pending_heredocs.is_empty() {
drain_heredocs(&mut getc, &mut body, &mut pending_heredocs)?;
}
}
continue;
}
'\n' => {
body.push(c);
at_word_start = true;
if !pending_heredocs.is_empty() {
drain_heredocs(&mut getc, &mut body, &mut pending_heredocs)?;
}
continue;
}
'{' => {
depth += 1;
body.push(c);
}
'}' => {
depth -= 1;
if depth == 0 {
return Some(body);
}
body.push(c);
}
_ => body.push(c),
}
at_word_start = c.is_whitespace() || matches!(c, ';' | '|' | '&' | '(' | ')');
}
}
fn scan_double_quoted<G>(getc: &mut G, body: &mut String) -> Option<()>
where
G: FnMut() -> Option<char>,
{
loop {
let c = getc()?;
body.push(c);
match c {
'\\' => body.push(getc()?),
'"' => return Some(()),
'$' => {
let n = getc()?;
body.push(n);
if n == '(' {
let mut depth = 1;
while depth > 0 {
let q = getc()?;
body.push(q);
match q {
'\\' => body.push(getc()?),
'(' => depth += 1,
')' => depth -= 1,
_ => {}
}
}
}
}
_ => {}
}
}
}
#[allow(clippy::type_complexity)]
fn scan_heredoc_intro<G>(
getc: &mut G,
body: &mut String,
) -> Option<(Option<(String, bool)>, Option<char>)>
where
G: FnMut() -> Option<char>,
{
let second = getc()?;
body.push(second);
if second != '<' {
return Some((None, Some(second)));
}
let mut third = getc()?;
body.push(third);
if third == '<' {
return Some((None, Some(third)));
}
let strip_tabs = third == '-';
if strip_tabs {
third = getc()?;
body.push(third);
}
let mut c = third;
while c == ' ' || c == '\t' {
c = getc()?;
body.push(c);
}
let mut delim = String::new();
let mut stopped_at: Option<char> = None;
loop {
match c {
'\'' | '"' => {
let close = c;
loop {
let q = getc()?;
body.push(q);
if q == close {
break;
}
delim.push(q);
}
}
'\\' => {
let q = getc()?;
body.push(q);
delim.push(q);
}
_ if c.is_whitespace() || c == ';' || c == '&' || c == '|' || c == ')' => {
stopped_at = Some(c);
break;
}
_ => delim.push(c),
}
match getc() {
Some(n) => {
c = n;
body.push(n);
}
None => break,
}
}
if delim.is_empty() {
Some((None, stopped_at))
} else {
Some((Some((delim, strip_tabs)), stopped_at))
}
}
fn drain_heredocs<G>(
getc: &mut G,
body: &mut String,
pending: &mut Vec<(String, bool)>,
) -> Option<()>
where
G: FnMut() -> Option<char>,
{
for (delim, strip_tabs) in pending.drain(..) {
loop {
let mut line = String::new();
let mut hit_eof = true;
while let Some(c) = getc() {
if c == '\n' {
hit_eof = false;
break;
}
line.push(c);
}
body.push_str(&line);
if !hit_eof {
body.push('\n');
}
let candidate = if strip_tabs {
line.trim_start_matches('\t')
} else {
line.as_str()
};
if candidate == delim {
break;
}
if hit_eof {
return Some(());
}
}
}
Some(())
}
pub(crate) fn intercept_matches(pattern: &str, cmd_name: &str, full_cmd: &str) -> bool {
if pattern == "*" || pattern == "all" {
return true;
}
if pattern == cmd_name {
return true;
}
if pattern.contains('*') || pattern.contains('?') {
if let Ok(pat) = glob::Pattern::new(pattern) {
return pat.matches(cmd_name) || pat.matches(full_cmd);
}
}
false
}
impl crate::ported::vm_helper::ShellExecutor {
pub(crate) fn run_intercepts(
&mut self,
cmd_name: &str,
full_cmd: &str,
args: &[String],
) -> Option<Result<i32, String>> {
let matching: Vec<Intercept> = self
.intercepts
.iter()
.filter(|i| intercept_matches(&i.pattern, cmd_name, full_cmd))
.cloned()
.collect();
if matching.is_empty() {
return None;
}
self.set_scalar("INTERCEPT_NAME".to_string(), cmd_name.to_string());
self.set_scalar("INTERCEPT_ARGS".to_string(), args.join(" "));
self.set_scalar("INTERCEPT_CMD".to_string(), full_cmd.to_string());
for advice in matching
.iter()
.filter(|i| matches!(i.kind, AdviceKind::Before))
{
let _ = self.execute_advice(&advice.code);
}
let around = matching
.iter()
.find(|i| matches!(i.kind, AdviceKind::Around));
let t0 = std::time::Instant::now();
let result = if let Some(advice) = around {
self.set_scalar("__intercept_proceed".to_string(), "0".to_string());
let advice_result = self.execute_advice(&advice.code);
let proceeded = self
.scalar("__intercept_proceed")
.map(|v| v == "1")
.unwrap_or(false);
if proceeded {
advice_result
} else {
advice_result
}
} else {
let has_after = matching.iter().any(|i| matches!(i.kind, AdviceKind::After));
if !has_after {
return None;
}
self.run_original_command(cmd_name, args)
};
let elapsed = t0.elapsed();
let ms = elapsed.as_secs_f64() * 1000.0;
self.set_scalar("INTERCEPT_MS".to_string(), format!("{:.3}", ms));
self.set_scalar("INTERCEPT_US".to_string(), format!("{:.0}", ms * 1000.0));
self.set_scalar(
"INTERCEPT_STATUS".to_string(),
match &result {
Ok(st) => st.to_string(),
Err(_) => "1".to_string(),
},
);
for advice in matching
.iter()
.filter(|i| matches!(i.kind, AdviceKind::After))
{
let _ = self.execute_advice(&advice.code);
}
self.unset_scalar("INTERCEPT_NAME");
self.unset_scalar("INTERCEPT_ARGS");
self.unset_scalar("INTERCEPT_CMD");
self.unset_scalar("INTERCEPT_MS");
self.unset_scalar("INTERCEPT_US");
self.unset_scalar("INTERCEPT_STATUS");
self.unset_scalar("__intercept_proceed");
Some(result)
}
pub(crate) fn execute_advice(&mut self, code: &str) -> Result<i32, String> {
let code = code.trim();
if code.starts_with('@') {
let stryke_code = code.trim_start_matches('@').trim();
if let Some(status) = crate::try_stryke_dispatch(stryke_code) {
self.set_last_status(status);
return Ok(status);
}
}
self.execute_script(code)
}
pub(crate) fn run_original_command(
&mut self,
cmd_name: &str,
args: &[String],
) -> Result<i32, String> {
if let Some(status) = self.dispatch_function_call(cmd_name, args) {
return Ok(status);
}
self.execute_external(cmd_name, args, &[])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn star_matches_anything() {
assert!(intercept_matches("*", "anything", "anything --here"));
assert!(intercept_matches("*", "", ""));
}
#[test]
fn all_matches_anything() {
assert!(intercept_matches("all", "ls", "ls -la"));
assert!(intercept_matches("all", "git", "git status"));
}
#[test]
fn exact_match_on_cmd_name() {
assert!(intercept_matches("git", "git", "git push"));
assert!(intercept_matches("ls", "ls", "ls -la"));
}
#[test]
fn exact_pattern_does_not_match_different_name() {
assert!(!intercept_matches("git", "svn", "svn diff"));
assert!(!intercept_matches("ls", "lsof", "lsof -p 1"));
}
#[test]
fn glob_star_matches_prefix() {
assert!(intercept_matches("git *", "git", "git push origin"));
}
#[test]
fn glob_star_underscore_prefix_matches_completion_funcs() {
assert!(intercept_matches("_*", "_files", "_files"));
assert!(intercept_matches("_*", "_describe", "_describe"));
}
#[test]
fn glob_star_does_not_match_non_prefix() {
assert!(!intercept_matches("_*", "files", "files"));
}
#[test]
fn question_mark_glob_matches_single_char() {
assert!(intercept_matches("l?", "ls", "ls"));
assert!(!intercept_matches("l?", "lsof", "lsof"));
}
#[test]
fn unmatched_pattern_without_glob_chars_returns_false() {
assert!(!intercept_matches("nope", "git", "git push"));
}
#[test]
fn invalid_glob_pattern_returns_false() {
assert!(!intercept_matches("[invalid", "git", "git push"));
}
#[test]
fn empty_pattern_does_not_match_non_empty_cmd() {
assert!(!intercept_matches("", "ls", "ls -la"));
}
#[test]
fn empty_pattern_matches_empty_cmd_exactly() {
assert!(intercept_matches("", "", ""));
}
#[test]
fn advice_kind_variants_round_trip_clone() {
let b = AdviceKind::Before;
let a = AdviceKind::After;
let r = AdviceKind::Around;
assert!(matches!(b.clone(), AdviceKind::Before));
assert!(matches!(a.clone(), AdviceKind::After));
assert!(matches!(r.clone(), AdviceKind::Around));
}
#[test]
fn intercept_struct_clone_preserves_fields() {
let i = Intercept {
pattern: "git *".into(),
kind: AdviceKind::Before,
code: "echo before".into(),
id: 42,
};
let c = i.clone();
assert_eq!(c.pattern, "git *");
assert!(matches!(c.kind, AdviceKind::Before));
assert_eq!(c.code, "echo before");
assert_eq!(c.id, 42);
}
}