mod capture;
mod legacy;
mod pipeline;
mod pty_session;
use std::io;
use tracing::debug;
use super::parser::{Pipeline, SimpleCommand};
use super::CommandResult;
use crate::cli::jarvis::jarvis_talk;
pub fn run_pipeline(pipeline: &Pipeline) -> CommandResult {
let n = pipeline.commands.len();
debug!(pipeline_length = n, "Running pipeline");
if n == 1 {
return run_single_command(&pipeline.commands[0]);
}
pipeline::run_piped_commands(&pipeline.commands)
}
fn run_single_command(simple: &SimpleCommand) -> CommandResult {
let has_redirect = !simple.redirects.is_empty();
if has_redirect {
return legacy::run_single_command_legacy(simple);
}
match pty_session::run_single_command_pty_session(simple) {
Ok(result) => result,
Err(e) => {
debug!("PTY session failed ({e}), falling back to legacy mode");
legacy::run_single_command_legacy(simple)
}
}
}
pub fn run_pipeline_captured(pipeline: &Pipeline) -> CommandResult {
capture::run_pipeline_captured(pipeline)
}
fn spawn_error(cmd: &str, e: io::Error) -> CommandResult {
let reason = match e.kind() {
io::ErrorKind::NotFound => "command not found".to_string(),
io::ErrorKind::PermissionDenied => "permission denied".to_string(),
_ => format!("{e}"),
};
let msg = format!("{cmd}: {reason}. Something wrong, sir?");
jarvis_talk(&msg);
CommandResult::error(msg, 127)
}
fn kill_and_wait(child: &mut std::process::Child) {
let _ = child.kill();
let _ = child.wait();
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::parser::Redirect;
fn simple(cmd: &str, args: &[&str]) -> SimpleCommand {
SimpleCommand {
cmd: cmd.to_string(),
args: args.iter().map(|s| s.to_string()).collect(),
redirects: vec![],
}
}
#[test]
fn echo_stdout_capture() {
let result = run_single_command(&simple("echo", &["hello"]));
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout.trim(), "hello");
}
#[test]
fn exit_code_success() {
let result = run_single_command(&simple("true", &[]));
assert_eq!(result.exit_code, 0);
}
#[test]
fn exit_code_failure() {
let result = run_single_command(&simple("false", &[]));
assert_eq!(result.exit_code, 1);
}
#[test]
fn stderr_capture() {
let result = run_single_command(&simple("sh", &["-c", "echo err >&2"]));
assert_eq!(result.stderr.trim(), "err");
}
#[test]
fn nonexistent_command_returns_error() {
let result = run_single_command(&simple("__jarvish_nonexistent_command__", &[]));
assert_ne!(result.exit_code, 0);
assert!(!result.stderr.is_empty());
}
#[test]
fn pipeline_two_commands_piped() {
let pipeline = Pipeline {
commands: vec![
SimpleCommand {
cmd: "echo".into(),
args: vec!["hello".into()],
redirects: vec![],
},
SimpleCommand {
cmd: "cat".into(),
args: vec![],
redirects: vec![],
},
],
};
let result = run_pipeline(&pipeline);
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout.trim(), "hello");
}
#[test]
fn pipeline_three_commands_piped() {
let pipeline = Pipeline {
commands: vec![
SimpleCommand {
cmd: "printf".into(),
args: vec!["aaa\\nbbb\\nccc\\n".into()],
redirects: vec![],
},
SimpleCommand {
cmd: "grep".into(),
args: vec!["bbb".into()],
redirects: vec![],
},
SimpleCommand {
cmd: "cat".into(),
args: vec![],
redirects: vec![],
},
],
};
let result = run_pipeline(&pipeline);
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout.trim(), "bbb");
}
#[test]
fn pipeline_exit_code_from_last_command() {
let pipeline = Pipeline {
commands: vec![
SimpleCommand {
cmd: "echo".into(),
args: vec!["hello".into()],
redirects: vec![],
},
SimpleCommand {
cmd: "false".into(),
args: vec![],
redirects: vec![],
},
],
};
let result = run_pipeline(&pipeline);
assert_eq!(result.exit_code, 1);
}
#[test]
fn redirect_stdout_overwrite() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("out.txt");
let path_str = path.to_str().unwrap().to_string();
let pipeline = Pipeline {
commands: vec![SimpleCommand {
cmd: "echo".into(),
args: vec!["redirected".into()],
redirects: vec![Redirect::StdoutOverwrite(path_str)],
}],
};
let result = run_pipeline(&pipeline);
assert_eq!(result.exit_code, 0);
let contents = std::fs::read_to_string(&path).unwrap();
assert_eq!(contents.trim(), "redirected");
}
#[test]
fn redirect_stdout_append() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("out.txt");
let path_str = path.to_str().unwrap().to_string();
std::fs::write(&path, "first\n").unwrap();
let pipeline = Pipeline {
commands: vec![SimpleCommand {
cmd: "echo".into(),
args: vec!["second".into()],
redirects: vec![Redirect::StdoutAppend(path_str)],
}],
};
let result = run_pipeline(&pipeline);
assert_eq!(result.exit_code, 0);
let contents = std::fs::read_to_string(&path).unwrap();
assert!(contents.contains("first"));
assert!(contents.contains("second"));
}
#[test]
fn redirect_stdin_from_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("input.txt");
let path_str = path.to_str().unwrap().to_string();
std::fs::write(&path, "from_file\n").unwrap();
let pipeline = Pipeline {
commands: vec![SimpleCommand {
cmd: "cat".into(),
args: vec![],
redirects: vec![Redirect::StdinFrom(path_str)],
}],
};
let result = run_pipeline(&pipeline);
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout.trim(), "from_file");
}
#[test]
fn redirect_stdin_nonexistent_file_returns_error() {
let pipeline = Pipeline {
commands: vec![SimpleCommand {
cmd: "cat".into(),
args: vec![],
redirects: vec![Redirect::StdinFrom(
"/tmp/__jarvish_nonexistent_input__".into(),
)],
}],
};
let result = run_pipeline(&pipeline);
assert_ne!(result.exit_code, 0);
}
}