pub fn is_shell_flag(flag: &str) -> bool {
flag.starts_with('-')
&& !flag.starts_with("--")
&& flag.len() > 1
&& flag.as_bytes()[1..].contains(&b'c')
}
fn env_no_filter() -> bool {
std::env::var("TOKF_NO_FILTER")
.ok()
.is_some_and(|v| matches!(v.as_str(), "1" | "true" | "yes"))
}
fn env_verbose() -> bool {
std::env::var("TOKF_VERBOSE")
.ok()
.is_some_and(|v| matches!(v.as_str(), "1" | "true" | "yes"))
}
fn restore_original_path() {
if let Ok(original) = std::env::var("TOKF_ORIGINAL_PATH") {
unsafe { std::env::set_var("PATH", &original) };
}
}
fn rewrite_and_delegate(flags: &str, command: &str, verbose: bool) -> i32 {
restore_original_path();
if env_no_filter() {
if verbose {
eprintln!("[tokf] shell: TOKF_NO_FILTER set, delegating to sh");
}
return delegate_to_real_shell(flags, command);
}
let options = tokf::rewrite::types::RewriteOptions {
no_mask_exit_code: true,
};
let rewritten = tokf::rewrite::rewrite_with_options(command, verbose, &options);
if verbose {
if rewritten == command {
eprintln!("[tokf] shell: no filter match, delegating to sh");
} else {
eprintln!("[tokf] shell: rewritten to: {rewritten}");
}
}
delegate_to_real_shell(flags, &rewritten)
}
pub fn cmd_shell(flags: &str, command: &str) -> i32 {
rewrite_and_delegate(flags, command, env_verbose())
}
fn quote_argv(args: &[String]) -> String {
args.iter()
.map(|a| format!("'{}'", a.replace('\'', "'\\''")))
.collect::<Vec<_>>()
.join(" ")
}
pub fn cmd_shell_argv(flags: &str, args: &[String]) -> i32 {
if args.is_empty() {
return 0;
}
let verbose = env_verbose();
restore_original_path();
if env_no_filter() {
if verbose {
eprintln!("[tokf] shell: TOKF_NO_FILTER set, delegating to sh");
}
return delegate_to_real_shell(flags, "e_argv(args));
}
let unquoted = args.join(" ");
let options = tokf::rewrite::types::RewriteOptions {
no_mask_exit_code: true,
};
let rewritten = tokf::rewrite::rewrite_with_options(&unquoted, verbose, &options);
if rewritten == unquoted {
if verbose {
eprintln!("[tokf] shell: no filter match, delegating to sh");
}
delegate_to_real_shell(flags, "e_argv(args))
} else {
let safe_rewritten = rewritten.replacen(&unquoted, "e_argv(args), 1);
if verbose {
eprintln!("[tokf] shell: rewritten to: {safe_rewritten}");
}
delegate_to_real_shell(flags, &safe_rewritten)
}
}
fn delegate_to_real_shell(flags: &str, command: &str) -> i32 {
match std::process::Command::new("sh")
.arg(flags)
.arg(command)
.status()
{
Ok(status) => {
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
status
.code()
.unwrap_or_else(|| status.signal().map_or(1, |s| 128 + s))
}
#[cfg(not(unix))]
{
status.code().unwrap_or(1)
}
}
Err(e) => {
eprintln!("[tokf] shell: failed to run sh: {e}");
127
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn shell_flag_c() {
assert!(is_shell_flag("-c"));
}
#[test]
fn shell_flag_cu() {
assert!(is_shell_flag("-cu"));
}
#[test]
fn shell_flag_ec() {
assert!(is_shell_flag("-ec"));
}
#[test]
fn shell_flag_ecu() {
assert!(is_shell_flag("-ecu"));
}
#[test]
fn shell_flag_not_long_flag() {
assert!(!is_shell_flag("--cache"));
assert!(!is_shell_flag("--color"));
}
#[test]
fn shell_flag_not_verbose() {
assert!(!is_shell_flag("-v"));
}
#[test]
fn shell_flag_not_empty_dash() {
assert!(!is_shell_flag("-"));
}
#[cfg(unix)]
#[test]
fn delegate_echo() {
let status = std::process::Command::new("sh")
.arg("-c")
.arg("echo hello")
.status()
.unwrap();
assert!(status.success());
}
#[test]
fn shell_unmatched_command_delegates() {
let code = cmd_shell("-c", "true");
assert_eq!(code, 0);
}
#[test]
fn shell_unmatched_failure_preserves_exit_code() {
let code = cmd_shell("-c", "false");
assert_ne!(code, 0);
}
#[test]
fn shell_compound_delegates() {
let code = cmd_shell("-c", "true && true");
assert_eq!(code, 0);
}
#[test]
fn shell_compound_failure() {
let code = cmd_shell("-c", "false && true");
assert_ne!(code, 0);
}
#[test]
fn shell_empty_command_delegates() {
let code = cmd_shell("-c", "");
assert_eq!(code, 0);
}
#[test]
fn shell_whitespace_only_delegates() {
let code = cmd_shell("-c", " ");
assert_eq!(code, 0);
}
#[test]
fn shell_pipe_delegates() {
let code = cmd_shell("-c", "echo hello | cat");
assert_eq!(code, 0);
}
#[test]
fn shell_flag_uppercase_c_does_not_match() {
assert!(!is_shell_flag("-C"));
}
#[test]
fn shell_flag_empty_string() {
assert!(!is_shell_flag(""));
}
#[test]
fn shell_flag_no_dash() {
assert!(!is_shell_flag("c"));
}
#[test]
fn shell_flag_long_c_only() {
assert!(!is_shell_flag("--c"));
}
#[test]
fn shell_argv_simple_command() {
let args: Vec<String> = vec!["true".into()];
let code = cmd_shell_argv("-c", &args);
assert_eq!(code, 0);
}
#[test]
fn shell_argv_unknown_command_fallback() {
let args: Vec<String> = vec!["false".into()];
let code = cmd_shell_argv("-c", &args);
assert_ne!(code, 0);
}
#[test]
fn shell_argv_preserves_arguments() {
let args: Vec<String> = vec!["echo".into(), "hello world".into()];
let code = cmd_shell_argv("-c", &args);
assert_eq!(code, 0);
}
#[test]
fn shell_argv_empty_args() {
let code = cmd_shell_argv("-c", &[]);
assert_eq!(code, 0);
}
#[test]
fn shell_argv_single_quotes_in_args() {
let args: Vec<String> = vec!["echo".into(), "it's".into()];
let code = cmd_shell_argv("-c", &args);
assert_eq!(code, 0);
}
#[test]
fn shell_argv_special_chars_in_args() {
let args: Vec<String> = vec!["echo".into(), "$HOME `whoami`".into()];
let code = cmd_shell_argv("-c", &args);
assert_eq!(code, 0);
}
#[test]
fn quote_argv_simple() {
let args: Vec<String> = vec!["cargo".into(), "fmt".into()];
assert_eq!(quote_argv(&args), "'cargo' 'fmt'");
}
#[test]
fn quote_argv_single_quotes() {
let args: Vec<String> = vec!["it's".into()];
assert_eq!(quote_argv(&args), "'it'\\''s'");
}
#[test]
fn quote_argv_empty() {
let args: Vec<String> = vec![];
assert_eq!(quote_argv(&args), "");
}
#[test]
fn quote_argv_spaces() {
let args: Vec<String> = vec!["hello world".into()];
assert_eq!(quote_argv(&args), "'hello world'");
}
#[test]
fn quote_argv_empty_string_element() {
let args: Vec<String> = vec![String::new()];
assert_eq!(quote_argv(&args), "''");
}
#[test]
fn quote_argv_backslashes() {
let args: Vec<String> = vec!["foo\\bar".into()];
assert_eq!(quote_argv(&args), "'foo\\bar'");
}
#[test]
fn quote_argv_newlines() {
let args: Vec<String> = vec!["line1\nline2".into()];
assert_eq!(quote_argv(&args), "'line1\nline2'");
}
#[test]
fn quote_argv_multiple_single_quotes() {
let args: Vec<String> = vec!["'''".into()];
assert_eq!(quote_argv(&args), "''\\'''\\'''\\'''");
}
#[test]
fn quote_argv_mixed_special_chars() {
let args: Vec<String> = vec!["echo".into(), "$HOME".into(), "it's".into()];
assert_eq!(quote_argv(&args), "'echo' '$HOME' 'it'\\''s'");
}
}