1pub mod ansi;
66pub mod events;
67#[doc(hidden)]
68pub mod macros;
69pub mod pipeline;
70pub mod quote;
71pub mod state;
72pub mod stream;
73pub mod terminal;
74pub mod trace;
75
76pub mod commands;
78pub mod shell_parser;
79pub mod utils;
80
81use std::collections::HashMap;
82use std::path::PathBuf;
83use std::process::Stdio;
84use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
85use tokio::process::{Child, Command};
86use tokio::sync::mpsc;
87
88pub use commands::{CommandContext, StreamChunk};
89pub use shell_parser::{needs_real_shell, parse_shell_command, ParsedCommand};
90pub use utils::{CommandResult, VirtualUtils};
91
92pub use ansi::{AnsiConfig, AnsiUtils};
94pub use events::{EventData, EventType, StreamEmitter};
95pub use pipeline::{Pipeline, PipelineBuilder, PipelineExt};
96pub use quote::quote;
97pub use state::{
98 get_shell_settings, global_state, reset_global_state, set_shell_option, unset_shell_option,
99 GlobalState, ShellSettings,
100};
101pub use stream::{AsyncIterator, IntoStream, OutputChunk, OutputStream, StreamingRunner};
102pub use trace::trace;
103
104fn resolve_spawn_cwd(cwd: Option<&PathBuf>) -> Option<PathBuf> {
116 if let Some(c) = cwd {
118 return Some(c.clone());
119 }
120
121 match std::env::current_dir() {
124 Ok(_) => None,
125 Err(e) => {
126 let fallback = std::env::var_os("HOME")
127 .or_else(|| std::env::var_os("USERPROFILE"))
128 .map(PathBuf::from)
129 .unwrap_or_else(std::env::temp_dir);
130 trace(
131 "ProcessRunner",
132 &format!(
133 "current_dir() failed ({}); spawning in fallback directory {}",
134 e,
135 fallback.display()
136 ),
137 );
138 if fallback.exists() {
139 Some(fallback)
140 } else {
141 Some(std::env::temp_dir())
142 }
143 }
144 }
145}
146
147#[derive(Debug, thiserror::Error)]
149pub enum Error {
150 #[error("IO error: {0}")]
151 Io(#[from] std::io::Error),
152
153 #[error("Command failed with exit code {code}: {message}")]
154 CommandFailed { code: i32, message: String },
155
156 #[error("Command not found: {0}")]
157 CommandNotFound(String),
158
159 #[error("Parse error: {0}")]
160 ParseError(String),
161
162 #[error("Cancelled")]
163 Cancelled,
164}
165
166pub type Result<T> = std::result::Result<T, Error>;
168
169#[derive(Debug, Clone)]
171pub struct RunOptions {
172 pub mirror: bool,
174 pub capture: bool,
176 pub stdin: StdinOption,
178 pub cwd: Option<PathBuf>,
180 pub env: Option<HashMap<String, String>>,
182 pub interactive: bool,
184 pub shell_operators: bool,
186 pub trace: bool,
188}
189
190impl Default for RunOptions {
191 fn default() -> Self {
192 RunOptions {
193 mirror: true,
194 capture: true,
195 stdin: StdinOption::Inherit,
196 cwd: None,
197 env: None,
198 interactive: false,
199 shell_operators: true,
200 trace: true,
201 }
202 }
203}
204
205#[derive(Debug, Clone)]
207pub enum StdinOption {
208 Inherit,
210 Pipe,
212 Content(String),
214 Null,
216}
217
218pub struct ProcessRunner {
220 command: String,
221 options: RunOptions,
222 child: Option<Child>,
223 result: Option<CommandResult>,
224 started: bool,
225 finished: bool,
226 cancelled: bool,
227 output_tx: Option<mpsc::Sender<StreamChunk>>,
228 output_rx: Option<mpsc::Receiver<StreamChunk>>,
229}
230
231impl ProcessRunner {
232 pub fn new(command: impl Into<String>, options: RunOptions) -> Self {
234 let (tx, rx) = mpsc::channel(1024);
235 ProcessRunner {
236 command: command.into(),
237 options,
238 child: None,
239 result: None,
240 started: false,
241 finished: false,
242 cancelled: false,
243 output_tx: Some(tx),
244 output_rx: Some(rx),
245 }
246 }
247
248 pub async fn start(&mut self) -> Result<()> {
250 if self.started {
251 return Ok(());
252 }
253 self.started = true;
254
255 utils::trace_lazy("ProcessRunner", || {
256 format!("Starting command: {}", self.command)
257 });
258
259 let first_word = self.command.split_whitespace().next().unwrap_or("");
261 if let Some(result) = self.try_virtual_command(first_word).await {
262 self.result = Some(result);
263 self.finished = true;
264 return Ok(());
265 }
266
267 let _parsed = if self.options.shell_operators && !needs_real_shell(&self.command) {
269 parse_shell_command(&self.command)
270 } else {
271 None
272 };
273
274 let shell = find_available_shell();
276
277 let mut cmd = Command::new(&shell.cmd);
278 for arg in &shell.args {
279 cmd.arg(arg);
280 }
281 cmd.arg(&self.command);
282
283 match &self.options.stdin {
285 StdinOption::Inherit => {
286 cmd.stdin(Stdio::inherit());
287 }
288 StdinOption::Pipe => {
289 cmd.stdin(Stdio::piped());
290 }
291 StdinOption::Content(_) => {
292 cmd.stdin(Stdio::piped());
293 }
294 StdinOption::Null => {
295 cmd.stdin(Stdio::null());
296 }
297 }
298
299 if self.options.capture || self.options.mirror {
301 cmd.stdout(Stdio::piped());
302 cmd.stderr(Stdio::piped());
303 } else {
304 cmd.stdout(Stdio::inherit());
305 cmd.stderr(Stdio::inherit());
306 }
307
308 if let Some(cwd) = resolve_spawn_cwd(self.options.cwd.as_ref()) {
311 cmd.current_dir(cwd);
312 }
313
314 if let Some(ref env_vars) = self.options.env {
316 for (key, value) in env_vars {
317 cmd.env(key, value);
318 }
319 }
320
321 let child = cmd.spawn()?;
323 self.child = Some(child);
324
325 Ok(())
326 }
327
328 pub async fn run(&mut self) -> Result<CommandResult> {
330 self.start().await?;
331
332 if let Some(result) = &self.result {
333 return Ok(result.clone());
334 }
335
336 let mut child = self.child.take().ok_or_else(|| {
337 Error::Io(std::io::Error::new(
338 std::io::ErrorKind::Other,
339 "Process not started",
340 ))
341 })?;
342
343 if let StdinOption::Content(ref content) = self.options.stdin {
345 if let Some(mut stdin) = child.stdin.take() {
346 let content = content.clone();
347 tokio::spawn(async move {
348 let _ = stdin.write_all(content.as_bytes()).await;
349 let _ = stdin.shutdown().await;
350 });
351 }
352 }
353
354 let mut stdout_content = String::new();
356 let mut stderr_content = String::new();
357
358 if let Some(stdout) = child.stdout.take() {
359 let mut reader = BufReader::new(stdout).lines();
360 while let Ok(Some(line)) = reader.next_line().await {
361 if self.options.mirror {
362 println!("{}", line);
363 }
364 stdout_content.push_str(&line);
365 stdout_content.push('\n');
366 }
367 }
368
369 if let Some(stderr) = child.stderr.take() {
370 let mut reader = BufReader::new(stderr).lines();
371 while let Ok(Some(line)) = reader.next_line().await {
372 if self.options.mirror {
373 eprintln!("{}", line);
374 }
375 stderr_content.push_str(&line);
376 stderr_content.push('\n');
377 }
378 }
379
380 let status = child.wait().await?;
381 let code = status.code().unwrap_or(-1);
382
383 let result = CommandResult {
384 stdout: stdout_content,
385 stderr: stderr_content,
386 code,
387 };
388
389 self.result = Some(result.clone());
390 self.finished = true;
391
392 Ok(result)
393 }
394
395 async fn try_virtual_command(&self, cmd_name: &str) -> Option<CommandResult> {
397 if !commands::are_virtual_commands_enabled() {
398 return None;
399 }
400
401 let parts: Vec<&str> = self.command.split_whitespace().collect();
403 let args: Vec<String> = parts.iter().skip(1).map(|s| s.to_string()).collect();
404
405 let ctx = CommandContext {
406 args,
407 stdin: match &self.options.stdin {
408 StdinOption::Content(s) => Some(s.clone()),
409 _ => None,
410 },
411 cwd: self.options.cwd.clone(),
412 env: self.options.env.clone(),
413 output_tx: self.output_tx.clone(),
414 is_cancelled: None,
415 };
416
417 match cmd_name {
418 "echo" => Some(commands::echo(ctx).await),
419 "pwd" => Some(commands::pwd(ctx).await),
420 "cd" => Some(commands::cd(ctx).await),
421 "true" => Some(commands::r#true(ctx).await),
422 "false" => Some(commands::r#false(ctx).await),
423 "sleep" => Some(commands::sleep(ctx).await),
424 "cat" => Some(commands::cat(ctx).await),
425 "ls" => Some(commands::ls(ctx).await),
426 "mkdir" => Some(commands::mkdir(ctx).await),
427 "rm" => Some(commands::rm(ctx).await),
428 "touch" => Some(commands::touch(ctx).await),
429 "cp" => Some(commands::cp(ctx).await),
430 "mv" => Some(commands::mv(ctx).await),
431 "basename" => Some(commands::basename(ctx).await),
432 "dirname" => Some(commands::dirname(ctx).await),
433 "env" => Some(commands::env(ctx).await),
434 "exit" => Some(commands::exit(ctx).await),
435 "which" => Some(commands::which(ctx).await),
436 "yes" => Some(commands::yes(ctx).await),
437 "seq" => Some(commands::seq(ctx).await),
438 "test" => Some(commands::test(ctx).await),
439 _ => None,
440 }
441 }
442
443 pub fn kill(&mut self) -> Result<()> {
445 self.cancelled = true;
446 if let Some(ref mut child) = self.child {
447 child.start_kill()?;
448 }
449 Ok(())
450 }
451
452 pub fn is_finished(&self) -> bool {
454 self.finished
455 }
456
457 pub fn result(&self) -> Option<&CommandResult> {
459 self.result.as_ref()
460 }
461
462 pub fn command(&self) -> &str {
464 &self.command
465 }
466
467 pub fn options(&self) -> &RunOptions {
469 &self.options
470 }
471}
472
473#[derive(Debug, Clone)]
475struct ShellConfig {
476 cmd: String,
477 args: Vec<String>,
478}
479
480fn find_available_shell() -> ShellConfig {
482 let is_windows = cfg!(windows);
483
484 if is_windows {
485 let shells = [
487 ("cmd.exe", vec!["/c"]),
488 ("powershell.exe", vec!["-Command"]),
489 ];
490
491 for (cmd, args) in shells {
492 if which::which(cmd).is_ok() {
493 return ShellConfig {
494 cmd: cmd.to_string(),
495 args: args.into_iter().map(String::from).collect(),
496 };
497 }
498 }
499
500 ShellConfig {
501 cmd: "cmd.exe".to_string(),
502 args: vec!["/c".to_string()],
503 }
504 } else {
505 let shells = [
507 ("/bin/sh", vec!["-c"]),
508 ("/usr/bin/sh", vec!["-c"]),
509 ("/bin/bash", vec!["-c"]),
510 ("sh", vec!["-c"]),
511 ];
512
513 for (cmd, args) in shells {
514 if std::path::Path::new(cmd).exists() || which::which(cmd).is_ok() {
515 return ShellConfig {
516 cmd: cmd.to_string(),
517 args: args.into_iter().map(String::from).collect(),
518 };
519 }
520 }
521
522 ShellConfig {
523 cmd: "/bin/sh".to_string(),
524 args: vec!["-c".to_string()],
525 }
526 }
527}
528
529pub async fn run(command: impl Into<String>) -> Result<CommandResult> {
534 let mut runner = ProcessRunner::new(command, RunOptions::default());
535 runner.run().await
536}
537
538pub use run as execute;
541
542pub async fn exec(command: impl Into<String>, options: RunOptions) -> Result<CommandResult> {
544 let mut runner = ProcessRunner::new(command, options);
545 runner.run().await
546}
547
548pub fn create(command: impl Into<String>, options: RunOptions) -> ProcessRunner {
550 ProcessRunner::new(command, options)
551}
552
553pub fn run_sync(command: impl Into<String>) -> Result<CommandResult> {
555 let rt = tokio::runtime::Runtime::new()?;
556 rt.block_on(run(command))
557}
558
559