use crate::compsys::ported::_comp_locale::_comp_locale;
use crate::ported::modules::zutil::lookupstyle;
use crate::ported::params::getsparam;
use std::env;
use std::io::Read;
use std::process::{Command, Output, Stdio};
pub fn _call_program(args: &[String]) -> i32 {
let _fn_scope = crate::compsys::ported::shared::FnScope::enter("_call_program");
match command_line(args) {
Some((cmdline, use_locale, line)) => run_helper(&cmdline, use_locale, line, false).1,
None => 1,
}
}
pub fn call_program_capture(args: &[String]) -> (String, i32) {
let _fn_scope = crate::compsys::ported::shared::FnScope::enter("_call_program");
match command_line(args) {
Some((cmdline, use_locale, line)) => run_helper(&cmdline, use_locale, line, true),
None => (String::new(), 1),
}
}
fn command_line(args: &[String]) -> Option<(Vec<String>, bool, u64)> {
let mut argv: Vec<String> = args.to_vec();
let mut use_locale = true;
if let Some(first) = argv.first() {
if first == "-p" {
argv.remove(0);
} else if first == "-l" {
argv.remove(0);
use_locale = false;
}
}
if argv.is_empty() {
return None;
}
let curcontext = getsparam("curcontext").unwrap_or_default();
let style_ctx = format!(":completion:{}:{}", curcontext, argv[0]);
let styled = lookupstyle(&style_ctx, "command")
.first()
.cloned()
.unwrap_or_default();
if !styled.is_empty() {
if let Some(rest) = styled.strip_prefix('-') {
let mut v: Vec<String> = vec![rest.to_string()];
if argv.len() > 1 {
v.extend(argv[1..].iter().cloned());
}
return Some((v, use_locale, 28));
}
return Some((vec![styled], use_locale, 30));
}
if argv.len() > 1 {
Some((argv[1..].to_vec(), use_locale, 33))
} else {
None
}
}
struct StderrDiscard {
saved: libc::c_int,
}
impl StderrDiscard {
fn maybe_enter() -> Option<Self> {
let debug_fd: i32 = getsparam("debug_fd")
.and_then(|s| s.parse().ok())
.unwrap_or(-1);
if debug_fd > 2 || unsafe { libc::isatty(2) } == 0 {
return None; }
let devnull = unsafe {
libc::open(
b"/dev/null\0".as_ptr() as *const libc::c_char,
libc::O_WRONLY,
)
};
if devnull < 0 {
return None;
}
let saved = unsafe { libc::dup(2) };
if saved < 0 {
unsafe { libc::close(devnull) };
return None;
}
unsafe {
libc::dup2(devnull, 2);
libc::close(devnull);
}
Some(StderrDiscard { saved })
}
}
impl Drop for StderrDiscard {
fn drop(&mut self) {
unsafe {
libc::dup2(self.saved, 2);
libc::close(self.saved);
}
}
}
struct HelperEnv {
saved: Vec<(String, Option<String>)>,
}
impl HelperEnv {
fn enter(use_locale: bool) -> Self {
let mut saved: Vec<(String, Option<String>)> = Vec::new();
saved.push(("COLUMNS".to_string(), env::var("COLUMNS").ok()));
env::set_var("COLUMNS", "999");
if use_locale {
saved.push(("LANG".to_string(), env::var("LANG").ok()));
for (k, v) in env::vars() {
if k.starts_with("LC_") {
saved.push((k, Some(v)));
}
}
if env::var("LC_CTYPE").is_err() {
saved.push(("LC_CTYPE".to_string(), None));
}
let _ = _comp_locale();
}
HelperEnv { saved }
}
}
impl Drop for HelperEnv {
fn drop(&mut self) {
for (k, v) in self.saved.iter().rev() {
match v {
Some(v) => env::set_var(k, v),
None => env::remove_var(k),
}
}
}
}
fn single_quote(s: &str) -> String {
format!("'{}'", s.replace('\'', "'\\''"))
}
fn have_shell_executor() -> bool {
if crate::fusevm_bridge::try_with_executor(|_| ()).is_some() {
return true;
}
crate::fusevm_bridge::with_session_context(|| {
crate::fusevm_bridge::try_with_executor(|_| ()).is_some()
})
}
fn run_helper(cmdline: &[String], use_locale: bool, line: u64, capture: bool) -> (String, i32) {
if let Some(policy) = crate::compsys::in_editor::exec_policy() {
return run_helper_subprocess(cmdline, use_locale, Some(policy), capture);
}
if !have_shell_executor() {
return run_helper_subprocess(cmdline, use_locale, None, capture);
}
let _env = HelperEnv::enter(use_locale); let _err = StderrDiscard::maybe_enter();
let text = cmdline.join(" ");
if !capture {
return (
String::new(),
crate::compsys::ported::shared::eval_comp(&text, line),
);
}
crate::compsys::ported::shared::set_sh_lineno(line);
let out =
crate::ported::exec::run_command_substitution(&format!("eval {}", single_quote(&text)));
let status = crate::ported::exec::cmdoutval.load(std::sync::atomic::Ordering::Relaxed);
let _ = crate::ported::params::setsparam("REPLY", &out);
(out, status)
}
fn publish(raw: &str, capture: bool) -> String {
if !capture {
if !raw.is_empty() {
use std::io::Write as _;
let mut so = std::io::stdout();
let _ = so.write_all(raw.as_bytes());
let _ = so.flush();
}
return String::new();
}
let trimmed = raw.trim_end_matches('\n').to_string();
let _ = crate::ported::params::setsparam("REPLY", &trimmed);
trimmed
}
fn run_helper_subprocess(
cmdline: &[String],
use_locale: bool,
policy: Option<(bool, std::time::Instant)>,
capture: bool,
) -> (String, i32) {
let mut cmd = Command::new("sh");
cmd.arg("-c").arg(cmdline.join(" "));
cmd.env("COLUMNS", "999"); if use_locale {
let saved_lang = env::var("LANG").ok();
let saved_ctype = env::var("LC_CTYPE").ok();
let _ = _comp_locale();
cmd.env("LANG", env::var("LANG").unwrap_or_else(|_| "C".to_string()));
if let Ok(ct) = env::var("LC_CTYPE") {
cmd.env("LC_CTYPE", ct);
}
if let Some(v) = saved_lang {
env::set_var("LANG", v);
}
if let Some(v) = saved_ctype {
env::set_var("LC_CTYPE", v);
}
}
let output = match policy {
Some((false, _)) => {
let _ = crate::ported::params::setsparam("REPLY", "");
return (String::new(), 1);
}
Some((true, deadline)) => match run_with_deadline(cmd, deadline) {
Some(o) => o,
None => {
let _ = crate::ported::params::setsparam("REPLY", "");
return (String::new(), 1);
}
},
None => match cmd.output() {
Ok(o) => o,
Err(_) => {
let _ = crate::ported::params::setsparam("REPLY", "");
return (String::new(), 1);
}
},
};
let raw = String::from_utf8_lossy(&output.stdout).to_string();
let stdout = publish(&raw, capture);
if !output.stderr.is_empty() && unsafe { libc::isatty(2) } == 0 {
use std::io::Write as _;
let mut se = std::io::stderr();
let _ = se.write_all(&output.stderr);
let _ = se.flush();
}
(stdout, output.status.code().unwrap_or(1))
}
fn run_with_deadline(mut cmd: Command, deadline: std::time::Instant) -> Option<Output> {
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::null());
cmd.stdin(Stdio::null());
let mut child = cmd.spawn().ok()?;
let stdout = child.stdout.take()?;
let reader = std::thread::spawn(move || {
let mut buf = Vec::new();
let mut stdout = stdout;
let _ = stdout.read_to_end(&mut buf);
buf
});
let status = loop {
match child.try_wait() {
Ok(Some(st)) => break st,
Ok(None) => {}
Err(_) => return None,
}
if std::time::Instant::now() >= deadline {
let _ = child.kill();
let _ = child.wait();
tracing::debug!(
target: "zshrs::compsys::in_editor",
"_call_program: helper killed at completion deadline",
);
return None;
}
std::thread::sleep(std::time::Duration::from_millis(1));
};
let stdout = reader.join().unwrap_or_default();
Some(Output {
status,
stdout,
stderr: Vec::new(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_args_returns_one() {
let _g = crate::test_util::global_state_lock();
assert_eq!(_call_program(&[]), 1);
}
#[test]
fn invokes_true_command_successfully() {
let _g = crate::test_util::global_state_lock();
let r = _call_program(&["my-style-key".to_string(), "true".to_string()]);
assert_eq!(r, 0);
}
#[test]
fn invokes_false_command_returns_one() {
let _g = crate::test_util::global_state_lock();
let r = _call_program(&["my-style-key".to_string(), "false".to_string()]);
assert_eq!(r, 1);
}
#[test]
fn plain_call_does_not_publish_reply() {
let _g = crate::test_util::global_state_lock();
let _ = crate::ported::params::setsparam("REPLY", "STALE");
let _ = _call_program(&[
"my-style-key".to_string(),
"printf".to_string(),
"hello".to_string(),
]);
assert_eq!(getsparam("REPLY").as_deref(), Some("STALE"));
}
#[test]
fn capture_entry_point_returns_the_output() {
let _g = crate::test_util::global_state_lock();
let (out, rc) = call_program_capture(&[
"my-style-key".to_string(),
"printf".to_string(),
"hello".to_string(),
]);
assert_eq!(out, "hello");
assert_eq!(rc, 0);
assert_eq!(getsparam("REPLY").as_deref(), Some("hello"));
}
#[test]
fn command_line_uses_sh33_for_a_plain_call() {
let _g = crate::test_util::global_state_lock();
let (words, locale, line) =
command_line(&["a-style-key".to_string(), "printf hi".to_string()]).unwrap();
assert_eq!(words, vec!["printf hi".to_string()]); assert!(locale, "sh:4 — `clocale` defaults to `_comp_locale;`");
assert_eq!(line, 33);
}
#[test]
fn command_line_dash_l_clears_the_locale_reset() {
let _g = crate::test_util::global_state_lock();
let (words, locale, line) = command_line(&[
"-l".to_string(),
"a-style-key".to_string(),
"true".to_string(),
])
.unwrap();
assert_eq!(words, vec!["true".to_string()]);
assert!(!locale);
assert_eq!(line, 33);
}
#[test]
fn command_line_dash_p_is_consumed() {
let _g = crate::test_util::global_state_lock();
let (words, _locale, _line) = command_line(&[
"-p".to_string(),
"a-style-key".to_string(),
"true".to_string(),
])
.unwrap();
assert_eq!(words, vec!["true".to_string()]);
}
#[test]
fn command_line_is_none_when_there_is_nothing_to_run() {
let _g = crate::test_util::global_state_lock();
assert!(command_line(&["a-style-key".to_string()]).is_none());
}
}