1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
//! Public execution options and errors.
use crate::{signal, PreferLocal};
use std::collections::HashMap;
use std::path::PathBuf;
/// Error type for command-stream operations
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Command failed with exit code {code}: {message}")]
CommandFailed { code: i32, message: String },
#[error("Command not found: {0}")]
CommandNotFound(String),
#[error("Parse error: {0}")]
ParseError(String),
#[error("Cancelled")]
Cancelled,
}
impl Error {
/// Build a [`Error::CommandFailed`] for a command that exited with `code`.
pub fn command_failed(code: i32, message: impl Into<String>) -> Self {
Error::CommandFailed {
code,
message: message.into(),
}
}
/// Exit status carried by the error, when the failure has one.
///
/// Mirrors the `error.code` property of the JavaScript implementation
/// (issue #38). Failures that never reached a child process, such as parse
/// errors, report `None`.
pub fn code(&self) -> Option<i32> {
match self {
Error::CommandFailed { code, .. } => Some(*code),
// `command not found` is 127 in POSIX shells, which is also what
// the JavaScript implementation reports for a missing executable.
Error::CommandNotFound(_) => Some(127),
Error::Io(error) => match error.kind() {
std::io::ErrorKind::NotFound => Some(127),
std::io::ErrorKind::PermissionDenied => Some(126),
_ => None,
},
// A cancelled command is terminated with SIGINT (128 + 2).
Error::Cancelled => Some(130),
Error::ParseError(_) => None,
}
}
/// Alias for [`code`](Self::code).
///
/// Node.js `child_process` names this property `code`, while execa, zx,
/// nano-spawn, and Bun Shell name it `exitCode`. command-stream exposes
/// both spellings in every language (issue #38).
pub fn exit_code(&self) -> Option<i32> {
self.code()
}
}
/// Result type for command-stream operations
pub type Result<T> = std::result::Result<T, Error>;
/// Options for command execution
#[derive(Debug, Clone)]
pub struct RunOptions {
/// Mirror output to parent stdout/stderr
pub mirror: bool,
/// Capture output in result
pub capture: bool,
/// Standard input handling
pub stdin: StdinOption,
/// Working directory
pub cwd: Option<PathBuf>,
/// Environment variables
pub env: Option<HashMap<String, String>>,
/// Prefer executables from the working directory or explicit local directories.
pub prefer_local: PreferLocal,
/// Interactive mode (TTY forwarding)
pub interactive: bool,
/// Enable shell operator parsing
pub shell_operators: bool,
/// Enable tracing for this command
pub trace: bool,
/// Signal used to stop the process when it is killed without an explicit
/// signal, i.e. [`crate::ProcessRunner::kill`] (default `SIGTERM`).
///
/// Mirrors the JavaScript `killSignal` option. An explicit
/// [`crate::ProcessRunner::kill_with`] argument always overrides it.
pub kill_signal: String,
/// Milliseconds the child is given to handle the kill signal before
/// `SIGKILL` is sent (default 100).
///
/// Mirrors the JavaScript `killGrace` option. This is the window in which a
/// child running its own signal handler can shut down on its own terms.
pub kill_grace_ms: u64,
/// Stop the command once this much time has passed since it started
/// (default `None`: unbounded).
///
/// Mirrors the JavaScript `timeout` option. The child is stopped with
/// [`kill_signal`](Self::kill_signal), then `SIGKILL` after
/// [`kill_grace_ms`](Self::kill_grace_ms); the result carries the
/// `128 + signal` code (143 for `SIGTERM`) and
/// [`crate::ProcessRunner::timed_out`] reports `true`. The deadline is
/// enforced while [`crate::ProcessRunner::run`] is awaited and also bounds
/// built-in virtual commands.
pub timeout: Option<std::time::Duration>,
/// Variables removed from the child's environment, the equivalent of
/// `env -u NAME` (default empty).
///
/// Removals are applied after [`env`](Self::env), so a name listed here
/// is absent even when `env` sets it.
pub env_remove: Vec<String>,
}
impl Default for RunOptions {
fn default() -> Self {
RunOptions {
mirror: true,
capture: true,
stdin: StdinOption::Inherit,
cwd: None,
env: None,
prefer_local: PreferLocal::Off,
interactive: false,
shell_operators: true,
trace: true,
kill_signal: signal::DEFAULT_KILL_SIGNAL.to_string(),
kill_grace_ms: signal::DEFAULT_KILL_GRACE_MS,
timeout: None,
env_remove: Vec::new(),
}
}
}
/// Standard input options
#[derive(Debug, Clone)]
pub enum StdinOption {
/// Inherit from parent process
Inherit,
/// Pipe (allow writing to stdin)
Pipe,
/// Provide string content
Content(String),
/// Null device
Null,
}