#[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 {
use crate::ported::lex::LEX_LEXFLAGS;
LEX_ININTERCEPT.get()
&& LEX_LEXFLAGS.get() == 0
&& !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
}
thread_local! {
static IN_ADVICE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
fn with_advice_flag<T>(value: bool, f: impl FnOnce() -> T) -> T {
let prev = IN_ADVICE.with(|c| c.replace(value));
let out = f();
IN_ADVICE.with(|c| c.set(prev));
out
}
pub(crate) fn outside_advice<T>(f: impl FnOnce() -> T) -> T {
with_advice_flag(false, f)
}
thread_local! {
static ORIGINAL_OVERRIDE: std::cell::RefCell<Option<(String, Vec<String>)>> =
const { std::cell::RefCell::new(None) };
}
struct OriginalOverrideGuard(Option<(String, Vec<String>)>);
impl OriginalOverrideGuard {
fn set(original: Option<(String, Vec<String>)>) -> Self {
Self(ORIGINAL_OVERRIDE.with(|slot| slot.replace(original)))
}
}
impl Drop for OriginalOverrideGuard {
fn drop(&mut self) {
ORIGINAL_OVERRIDE.with(|slot| *slot.borrow_mut() = self.0.take());
}
}
pub(crate) fn alias_origin(hasalias: Option<String>) -> Option<(String, usize)> {
let name = hasalias?;
let text = crate::ported::hashtable::aliastab_lock()
.read()
.ok()?
.get(name.as_str())?
.text
.clone();
let body_words = crate::ported::hist::histsplitwords(&text, false).len();
Some((name, body_words))
}
impl crate::ported::vm_helper::ShellExecutor {
fn clear_advice_vars(&mut self) {
for name in [
"INTERCEPT_NAME",
"INTERCEPT_ARGS",
"INTERCEPT_CMD",
"INTERCEPT_MS",
"INTERCEPT_US",
"INTERCEPT_STATUS",
"__intercept_proceed",
] {
self.unset_scalar(name);
}
}
pub(crate) fn run_intercepts(
&mut self,
cmd_name: &str,
full_cmd: &str,
args: &[String],
) -> Option<Result<i32, String>> {
self.run_intercepts_with(cmd_name, full_cmd, args, None)
}
pub(crate) fn run_intercepts_with(
&mut self,
cmd_name: &str,
full_cmd: &str,
args: &[String],
original: Option<(String, Vec<String>)>,
) -> Option<Result<i32, String>> {
let _override_guard = OriginalOverrideGuard::set(original);
if IN_ADVICE.with(|c| c.get()) {
return None;
}
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 {
self.clear_advice_vars();
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.clear_advice_vars();
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);
}
}
with_advice_flag(true, || self.execute_script(code))
}
pub(crate) fn run_original_command(
&mut self,
cmd_name: &str,
args: &[String],
) -> Result<i32, String> {
let (cmd_name, args) = match ORIGINAL_OVERRIDE.with(|o| o.borrow().clone()) {
Some((name, real_args)) => (name, real_args),
None => (cmd_name.to_string(), args.to_vec()),
};
let (cmd_name, args) = (cmd_name.as_str(), args.as_slice());
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);
}
}