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::{
97 escape_for_double_quotes, escape_for_single_quotes, has_shell_escapes,
98 is_quote_context_enabled, quote, quote_for_context, scan_quote_context, QuoteContext,
99};
100pub use state::{
101 get_shell_settings, global_state, reset_global_state, set_shell_option, unset_shell_option,
102 GlobalState, ShellSettings,
103};
104pub use stream::{AsyncIterator, IntoStream, OutputChunk, OutputStream, StreamingRunner};
105pub use trace::trace;
106
107fn fallback_cwd() -> PathBuf {
108 std::env::var_os("HOME")
109 .or_else(|| std::env::var_os("USERPROFILE"))
110 .map(PathBuf::from)
111 .filter(|path| path.is_dir())
112 .unwrap_or_else(std::env::temp_dir)
113}
114
115fn resolve_spawn_cwd(cwd: Option<&PathBuf>) -> Option<PathBuf> {
127 if let Some(c) = cwd {
129 return Some(c.clone());
130 }
131
132 match std::env::current_dir() {
135 Ok(_) => None,
136 Err(e) => {
137 let fallback = fallback_cwd();
138 trace(
139 "ProcessRunner",
140 &format!(
141 "current_dir() failed ({}); spawning in fallback directory {}",
142 e,
143 fallback.display()
144 ),
145 );
146 Some(fallback)
147 }
148 }
149}
150
151#[derive(Debug, thiserror::Error)]
153pub enum Error {
154 #[error("IO error: {0}")]
155 Io(#[from] std::io::Error),
156
157 #[error("Command failed with exit code {code}: {message}")]
158 CommandFailed { code: i32, message: String },
159
160 #[error("Command not found: {0}")]
161 CommandNotFound(String),
162
163 #[error("Parse error: {0}")]
164 ParseError(String),
165
166 #[error("Cancelled")]
167 Cancelled,
168}
169
170pub type Result<T> = std::result::Result<T, Error>;
172
173#[derive(Debug, Clone)]
175pub struct RunOptions {
176 pub mirror: bool,
178 pub capture: bool,
180 pub stdin: StdinOption,
182 pub cwd: Option<PathBuf>,
184 pub env: Option<HashMap<String, String>>,
186 pub interactive: bool,
188 pub shell_operators: bool,
190 pub trace: bool,
192}
193
194impl Default for RunOptions {
195 fn default() -> Self {
196 RunOptions {
197 mirror: true,
198 capture: true,
199 stdin: StdinOption::Inherit,
200 cwd: None,
201 env: None,
202 interactive: false,
203 shell_operators: true,
204 trace: true,
205 }
206 }
207}
208
209#[derive(Debug, Clone)]
211pub enum StdinOption {
212 Inherit,
214 Pipe,
216 Content(String),
218 Null,
220}
221
222pub struct ProcessRunner {
224 command: String,
225 options: RunOptions,
226 child: Option<Child>,
227 result: Option<CommandResult>,
228 started: bool,
229 finished: bool,
230 cancelled: bool,
231 output_tx: Option<mpsc::Sender<StreamChunk>>,
232 #[allow(dead_code)]
237 output_rx: Option<mpsc::Receiver<StreamChunk>>,
238}
239
240impl ProcessRunner {
241 pub fn new(command: impl Into<String>, options: RunOptions) -> Self {
243 let (tx, rx) = mpsc::channel(1024);
244 ProcessRunner {
245 command: command.into(),
246 options,
247 child: None,
248 result: None,
249 started: false,
250 finished: false,
251 cancelled: false,
252 output_tx: Some(tx),
253 output_rx: Some(rx),
254 }
255 }
256
257 pub async fn start(&mut self) -> Result<()> {
259 if self.started {
260 return Ok(());
261 }
262 self.started = true;
263
264 utils::trace_lazy("ProcessRunner", || {
265 format!("Starting command: {}", self.command)
266 });
267
268 let first_word = if has_shell_escapes(&self.command) || needs_real_shell(&self.command) {
277 ""
278 } else {
279 self.command.split_whitespace().next().unwrap_or("")
280 };
281 if let Some(result) = self.try_virtual_command(first_word).await {
282 self.result = Some(result);
283 self.finished = true;
284 return Ok(());
285 }
286 let _parsed = if self.options.shell_operators && !needs_real_shell(&self.command) {
288 parse_shell_command(&self.command)
289 } else {
290 None
291 };
292
293 let shell = find_available_shell();
295
296 let mut cmd = Command::new(&shell.cmd);
297 for arg in &shell.args {
298 cmd.arg(arg);
299 }
300 cmd.arg(utils::with_exported_process_context(
301 &self.command,
302 self.options.env.as_ref(),
303 ));
304
305 match &self.options.stdin {
307 StdinOption::Inherit => {
308 cmd.stdin(Stdio::inherit());
309 }
310 StdinOption::Pipe => {
311 cmd.stdin(Stdio::piped());
312 }
313 StdinOption::Content(_) => {
314 cmd.stdin(Stdio::piped());
315 }
316 StdinOption::Null => {
317 cmd.stdin(Stdio::null());
318 }
319 }
320
321 if self.options.capture || self.options.mirror {
323 cmd.stdout(Stdio::piped());
324 cmd.stderr(Stdio::piped());
325 } else {
326 cmd.stdout(Stdio::inherit());
327 cmd.stderr(Stdio::inherit());
328 }
329
330 if let Some(cwd) = resolve_spawn_cwd(self.options.cwd.as_ref()) {
333 cmd.current_dir(cwd);
334 }
335
336 if let Some(ref env_vars) = self.options.env {
338 for (key, value) in env_vars {
339 cmd.env(key, value);
340 }
341 }
342
343 let child = cmd.spawn()?;
345 self.child = Some(child);
346
347 Ok(())
348 }
349
350 pub async fn run(&mut self) -> Result<CommandResult> {
352 self.start().await?;
353
354 if let Some(result) = &self.result {
355 return Ok(result.clone());
356 }
357
358 let mut child = self
359 .child
360 .take()
361 .ok_or_else(|| Error::Io(std::io::Error::other("Process not started")))?;
362
363 if let StdinOption::Content(ref content) = self.options.stdin {
365 if let Some(mut stdin) = child.stdin.take() {
366 let content = content.clone();
367 tokio::spawn(async move {
368 let _ = stdin.write_all(content.as_bytes()).await;
369 let _ = stdin.shutdown().await;
370 });
371 }
372 }
373
374 let mut stdout_content = String::new();
376 let mut stderr_content = String::new();
377
378 if let Some(stdout) = child.stdout.take() {
379 let mut reader = BufReader::new(stdout).lines();
380 while let Ok(Some(line)) = reader.next_line().await {
381 if self.options.mirror {
382 println!("{}", line);
383 }
384 stdout_content.push_str(&line);
385 stdout_content.push('\n');
386 }
387 }
388
389 if let Some(stderr) = child.stderr.take() {
390 let mut reader = BufReader::new(stderr).lines();
391 while let Ok(Some(line)) = reader.next_line().await {
392 if self.options.mirror {
393 eprintln!("{}", line);
394 }
395 stderr_content.push_str(&line);
396 stderr_content.push('\n');
397 }
398 }
399
400 let status = child.wait().await?;
401 let code = status.code().unwrap_or(-1);
402
403 let result = CommandResult {
404 stdout: stdout_content,
405 stderr: stderr_content,
406 code,
407 };
408
409 self.result = Some(result.clone());
410 self.finished = true;
411
412 Ok(result)
413 }
414
415 async fn try_virtual_command(&self, cmd_name: &str) -> Option<CommandResult> {
417 if !commands::are_virtual_commands_enabled() {
418 return None;
419 }
420
421 let parts: Vec<&str> = self.command.split_whitespace().collect();
423 let args: Vec<String> = parts.iter().skip(1).map(|s| s.to_string()).collect();
424
425 let ctx = CommandContext {
426 args,
427 stdin: match &self.options.stdin {
428 StdinOption::Content(s) => Some(s.clone()),
429 _ => None,
430 },
431 cwd: self.options.cwd.clone(),
432 env: self.options.env.clone(),
433 output_tx: self.output_tx.clone(),
434 is_cancelled: None,
435 };
436
437 match cmd_name {
438 "echo" => Some(commands::echo(ctx).await),
439 "pwd" => Some(commands::pwd(ctx).await),
440 "cd" => Some(commands::cd::resolve_cd(ctx).await.0),
441 "true" => Some(commands::r#true(ctx).await),
442 "false" => Some(commands::r#false(ctx).await),
443 "sleep" => Some(commands::sleep(ctx).await),
444 "cat" => Some(commands::cat(ctx).await),
445 "ls" => Some(commands::ls(ctx).await),
446 "mkdir" => Some(commands::mkdir(ctx).await),
447 "rm" => Some(commands::rm(ctx).await),
448 "touch" => Some(commands::touch(ctx).await),
449 "cp" => Some(commands::cp(ctx).await),
450 "mv" => Some(commands::mv(ctx).await),
451 "basename" => Some(commands::basename(ctx).await),
452 "dirname" => Some(commands::dirname(ctx).await),
453 "env" => Some(commands::env(ctx).await),
454 "exit" => Some(commands::exit(ctx).await),
455 "which" => Some(commands::which(ctx).await),
456 "yes" => Some(commands::yes(ctx).await),
457 "seq" => Some(commands::seq(ctx).await),
458 "test" => Some(commands::test(ctx).await),
459 _ => None,
460 }
461 }
462
463 pub fn kill(&mut self) -> Result<()> {
465 self.cancelled = true;
466 if let Some(ref mut child) = self.child {
467 child.start_kill()?;
468 }
469 Ok(())
470 }
471
472 pub fn is_finished(&self) -> bool {
474 self.finished
475 }
476
477 pub fn result(&self) -> Option<&CommandResult> {
479 self.result.as_ref()
480 }
481
482 pub fn command(&self) -> &str {
484 &self.command
485 }
486
487 pub fn options(&self) -> &RunOptions {
489 &self.options
490 }
491}
492
493#[derive(Debug, Clone)]
495struct ShellConfig {
496 cmd: String,
497 args: Vec<String>,
498}
499
500fn find_available_shell() -> ShellConfig {
502 let is_windows = cfg!(windows);
503
504 if is_windows {
505 let shells = [
507 ("cmd.exe", vec!["/c"]),
508 ("powershell.exe", vec!["-Command"]),
509 ];
510
511 for (cmd, args) in shells {
512 if which::which(cmd).is_ok() {
513 return ShellConfig {
514 cmd: cmd.to_string(),
515 args: args.into_iter().map(String::from).collect(),
516 };
517 }
518 }
519
520 ShellConfig {
521 cmd: "cmd.exe".to_string(),
522 args: vec!["/c".to_string()],
523 }
524 } else {
525 let shells = [
527 ("/bin/sh", vec!["-c"]),
528 ("/usr/bin/sh", vec!["-c"]),
529 ("/bin/bash", vec!["-c"]),
530 ("sh", vec!["-c"]),
531 ];
532
533 for (cmd, args) in shells {
534 if std::path::Path::new(cmd).exists() || which::which(cmd).is_ok() {
535 return ShellConfig {
536 cmd: cmd.to_string(),
537 args: args.into_iter().map(String::from).collect(),
538 };
539 }
540 }
541
542 ShellConfig {
543 cmd: "/bin/sh".to_string(),
544 args: vec!["-c".to_string()],
545 }
546 }
547}
548
549pub async fn run(command: impl Into<String>) -> Result<CommandResult> {
554 let mut runner = ProcessRunner::new(command, RunOptions::default());
555 runner.run().await
556}
557
558pub use run as execute;
561
562pub async fn exec(command: impl Into<String>, options: RunOptions) -> Result<CommandResult> {
564 let mut runner = ProcessRunner::new(command, options);
565 runner.run().await
566}
567
568pub fn create(command: impl Into<String>, options: RunOptions) -> ProcessRunner {
570 ProcessRunner::new(command, options)
571}
572
573pub fn run_sync(command: impl Into<String>) -> Result<CommandResult> {
575 let rt = tokio::runtime::Runtime::new()?;
576 rt.block_on(run(command))
577}
578
579