command-stream 0.9.1 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,165 @@
1
+ //! Macros for ergonomic command execution
2
+ //!
3
+ //! This module provides command execution macros that offer a similar experience
4
+ //! to JavaScript's `$` tagged template literal for shell command execution.
5
+ //!
6
+ //! ## Available Macros
7
+ //!
8
+ //! - `s!` - Short, concise macro (recommended for most use cases)
9
+ //! - `sh!` - Shell macro (alternative short form)
10
+ //! - `cmd!` - Command macro (explicit name)
11
+ //! - `cs!` - Command-stream macro (another alternative)
12
+ //!
13
+ //! All macros are aliases and provide identical functionality.
14
+ //!
15
+ //! ## Usage
16
+ //!
17
+ //! ```rust,no_run
18
+ //! use command_stream::s;
19
+ //!
20
+ //! #[tokio::main]
21
+ //! async fn main() -> Result<(), Box<dyn std::error::Error>> {
22
+ //! // Simple command
23
+ //! let result = s!("echo hello world").await?;
24
+ //!
25
+ //! // With interpolation (values are automatically quoted for safety)
26
+ //! let name = "John Doe";
27
+ //! let result = s!("echo Hello, {}", name).await?;
28
+ //!
29
+ //! // Multiple arguments
30
+ //! let file = "test.txt";
31
+ //! let dir = "/tmp";
32
+ //! let result = s!("cp {} {}", file, dir).await?;
33
+ //!
34
+ //! Ok(())
35
+ //! }
36
+ //! ```
37
+
38
+ /// Build a shell command with interpolated values safely quoted
39
+ ///
40
+ /// This function is used internally by the `cmd!` macro to build
41
+ /// shell commands with properly quoted interpolated values.
42
+ pub fn build_shell_command(parts: &[&str], values: &[&str]) -> String {
43
+ let mut result = String::new();
44
+
45
+ for (i, part) in parts.iter().enumerate() {
46
+ result.push_str(part);
47
+ if i < values.len() {
48
+ result.push_str(&crate::quote::quote(values[i]));
49
+ }
50
+ }
51
+
52
+ result
53
+ }
54
+
55
+ /// Helper function to create a ProcessRunner from a command string
56
+ pub fn create_runner(command: String) -> crate::ProcessRunner {
57
+ crate::ProcessRunner::new(command, crate::RunOptions {
58
+ mirror: true,
59
+ capture: true,
60
+ ..Default::default()
61
+ })
62
+ }
63
+
64
+ /// Helper function to create a ProcessRunner with custom options
65
+ pub fn create_runner_with_options(command: String, options: crate::RunOptions) -> crate::ProcessRunner {
66
+ crate::ProcessRunner::new(command, options)
67
+ }
68
+
69
+ /// The `cmd!` macro for ergonomic shell command execution
70
+ ///
71
+ /// This macro provides a similar experience to JavaScript's `$` tagged template literal.
72
+ /// Values interpolated into the command are automatically quoted for shell safety.
73
+ ///
74
+ /// Note: Consider using the shorter `s!` or `sh!` aliases for more concise code.
75
+ ///
76
+ /// # Examples
77
+ ///
78
+ /// ```rust,no_run
79
+ /// use command_stream::s;
80
+ ///
81
+ /// # async fn example() -> Result<(), command_stream::Error> {
82
+ /// // Simple command (returns a future that can be awaited)
83
+ /// let result = s!("echo hello").await?;
84
+ ///
85
+ /// // With string interpolation
86
+ /// let name = "world";
87
+ /// let result = s!("echo hello {}", name).await?;
88
+ ///
89
+ /// // With multiple values
90
+ /// let src = "source.txt";
91
+ /// let dst = "dest.txt";
92
+ /// let result = s!("cp {} {}", src, dst).await?;
93
+ ///
94
+ /// // Values with special characters are automatically quoted
95
+ /// let filename = "file with spaces.txt";
96
+ /// let result = s!("cat {}", filename).await?; // Safely handles spaces
97
+ /// # Ok(())
98
+ /// # }
99
+ /// ```
100
+ ///
101
+ /// # Safety
102
+ ///
103
+ /// All interpolated values are automatically quoted using shell-safe quoting,
104
+ /// preventing command injection attacks.
105
+ #[macro_export]
106
+ macro_rules! cmd {
107
+ // No interpolation - just a plain command string
108
+ ($cmd:expr) => {{
109
+ async {
110
+ $crate::run($cmd).await
111
+ }
112
+ }};
113
+
114
+ // With format-style interpolation
115
+ ($fmt:expr, $($arg:expr),+ $(,)?) => {{
116
+ // Build command with quoted values
117
+ let mut result = String::new();
118
+ let values: Vec<String> = vec![$(format!("{}", $arg)),+];
119
+ let values_ref: Vec<&str> = values.iter().map(|s| s.as_str()).collect();
120
+ let fmt_parts: Vec<&str> = $fmt.split("{}").collect();
121
+ for (i, part) in fmt_parts.iter().enumerate() {
122
+ result.push_str(part);
123
+ if i < values_ref.len() {
124
+ result.push_str(&$crate::quote::quote(values_ref[i]));
125
+ }
126
+ }
127
+
128
+ async move {
129
+ $crate::run(result).await
130
+ }
131
+ }};
132
+ }
133
+
134
+ /// The `sh!` macro - alias for `cmd!`
135
+ ///
136
+ /// This is an alternative name for `cmd!` that some users may find
137
+ /// more intuitive for shell command execution.
138
+ #[macro_export]
139
+ macro_rules! sh {
140
+ ($($args:tt)*) => {
141
+ $crate::cmd!($($args)*)
142
+ };
143
+ }
144
+
145
+ /// The `s!` macro - short alias for `cmd!`
146
+ ///
147
+ /// This is a concise alternative to `cmd!` for quick shell command execution.
148
+ /// Recommended for use in documentation and examples.
149
+ #[macro_export]
150
+ macro_rules! s {
151
+ ($($args:tt)*) => {
152
+ $crate::cmd!($($args)*)
153
+ };
154
+ }
155
+
156
+ /// The `cs!` macro - alias for `cmd!`
157
+ ///
158
+ /// Short for "command-stream", this provides another alternative
159
+ /// for shell command execution.
160
+ #[macro_export]
161
+ macro_rules! cs {
162
+ ($($args:tt)*) => {
163
+ $crate::cmd!($($args)*)
164
+ };
165
+ }
@@ -0,0 +1,411 @@
1
+ //! Pipeline execution support
2
+ //!
3
+ //! This module provides pipeline functionality similar to the JavaScript
4
+ //! `$.process-runner-pipeline.mjs` module. It allows chaining commands
5
+ //! together with the output of one command becoming the input of the next.
6
+ //!
7
+ //! ## Usage
8
+ //!
9
+ //! ```rust,no_run
10
+ //! use command_stream::{Pipeline, run};
11
+ //!
12
+ //! #[tokio::main]
13
+ //! async fn main() -> Result<(), Box<dyn std::error::Error>> {
14
+ //! // Create a pipeline
15
+ //! let result = Pipeline::new()
16
+ //! .add("echo hello world")
17
+ //! .add("grep world")
18
+ //! .add("wc -l")
19
+ //! .run()
20
+ //! .await?;
21
+ //!
22
+ //! println!("Output: {}", result.stdout);
23
+ //! Ok(())
24
+ //! }
25
+ //! ```
26
+
27
+ use std::collections::HashMap;
28
+ use std::path::PathBuf;
29
+ use std::process::Stdio;
30
+ use tokio::io::{AsyncReadExt, AsyncWriteExt};
31
+ use tokio::process::Command;
32
+
33
+ use crate::trace::trace_lazy;
34
+ use crate::{CommandResult, Result, RunOptions, StdinOption};
35
+
36
+ /// A pipeline of commands to be executed sequentially
37
+ ///
38
+ /// Each command's stdout is piped to the next command's stdin.
39
+ #[derive(Debug, Clone)]
40
+ pub struct Pipeline {
41
+ /// Commands in the pipeline
42
+ commands: Vec<String>,
43
+ /// Initial stdin content (optional)
44
+ stdin: Option<String>,
45
+ /// Working directory
46
+ cwd: Option<PathBuf>,
47
+ /// Environment variables
48
+ env: Option<HashMap<String, String>>,
49
+ /// Whether to mirror output to parent stdout/stderr
50
+ mirror: bool,
51
+ /// Whether to capture output
52
+ capture: bool,
53
+ }
54
+
55
+ impl Default for Pipeline {
56
+ fn default() -> Self {
57
+ Self::new()
58
+ }
59
+ }
60
+
61
+ impl Pipeline {
62
+ /// Create a new empty pipeline
63
+ pub fn new() -> Self {
64
+ Pipeline {
65
+ commands: Vec::new(),
66
+ stdin: None,
67
+ cwd: None,
68
+ env: None,
69
+ mirror: true,
70
+ capture: true,
71
+ }
72
+ }
73
+
74
+ /// Add a command to the pipeline
75
+ pub fn add(mut self, command: impl Into<String>) -> Self {
76
+ self.commands.push(command.into());
77
+ self
78
+ }
79
+
80
+ /// Set the initial stdin content for the first command
81
+ pub fn stdin(mut self, content: impl Into<String>) -> Self {
82
+ self.stdin = Some(content.into());
83
+ self
84
+ }
85
+
86
+ /// Set the working directory for all commands
87
+ pub fn cwd(mut self, path: impl Into<PathBuf>) -> Self {
88
+ self.cwd = Some(path.into());
89
+ self
90
+ }
91
+
92
+ /// Set environment variables for all commands
93
+ pub fn env(mut self, env: HashMap<String, String>) -> Self {
94
+ self.env = Some(env);
95
+ self
96
+ }
97
+
98
+ /// Set whether to mirror output to stdout/stderr
99
+ pub fn mirror_output(mut self, mirror: bool) -> Self {
100
+ self.mirror = mirror;
101
+ self
102
+ }
103
+
104
+ /// Set whether to capture output
105
+ pub fn capture_output(mut self, capture: bool) -> Self {
106
+ self.capture = capture;
107
+ self
108
+ }
109
+
110
+ /// Execute the pipeline and return the result
111
+ pub async fn run(self) -> Result<CommandResult> {
112
+ if self.commands.is_empty() {
113
+ return Ok(CommandResult {
114
+ stdout: String::new(),
115
+ stderr: "No commands in pipeline".to_string(),
116
+ code: 1,
117
+ });
118
+ }
119
+
120
+ trace_lazy("Pipeline", || {
121
+ format!("Running pipeline with {} commands", self.commands.len())
122
+ });
123
+
124
+ let mut current_stdin = self.stdin.clone();
125
+ let mut last_result = CommandResult {
126
+ stdout: String::new(),
127
+ stderr: String::new(),
128
+ code: 0,
129
+ };
130
+ let mut accumulated_stderr = String::new();
131
+
132
+ for (i, cmd_str) in self.commands.iter().enumerate() {
133
+ let is_last = i == self.commands.len() - 1;
134
+
135
+ trace_lazy("Pipeline", || {
136
+ format!("Executing command {}/{}: {}", i + 1, self.commands.len(), cmd_str)
137
+ });
138
+
139
+ // Check if this is a virtual command
140
+ let first_word = cmd_str.split_whitespace().next().unwrap_or("");
141
+ if crate::commands::are_virtual_commands_enabled() {
142
+ if let Some(result) = self.try_virtual_command(first_word, cmd_str, &current_stdin).await {
143
+ if result.code != 0 {
144
+ return Ok(CommandResult {
145
+ stdout: result.stdout,
146
+ stderr: accumulated_stderr + &result.stderr,
147
+ code: result.code,
148
+ });
149
+ }
150
+ current_stdin = Some(result.stdout.clone());
151
+ accumulated_stderr.push_str(&result.stderr);
152
+ last_result = result;
153
+ continue;
154
+ }
155
+ }
156
+
157
+ // Execute via shell
158
+ let shell = find_available_shell();
159
+ let mut cmd = Command::new(&shell.cmd);
160
+ for arg in &shell.args {
161
+ cmd.arg(arg);
162
+ }
163
+ cmd.arg(cmd_str);
164
+
165
+ // Configure stdio
166
+ cmd.stdin(Stdio::piped());
167
+ cmd.stdout(Stdio::piped());
168
+ cmd.stderr(Stdio::piped());
169
+
170
+ // Set working directory
171
+ if let Some(ref cwd) = self.cwd {
172
+ cmd.current_dir(cwd);
173
+ }
174
+
175
+ // Set environment
176
+ if let Some(ref env_vars) = self.env {
177
+ for (key, value) in env_vars {
178
+ cmd.env(key, value);
179
+ }
180
+ }
181
+
182
+ // Spawn the process
183
+ let mut child = cmd.spawn()?;
184
+
185
+ // Write stdin if available
186
+ if let Some(ref stdin_content) = current_stdin {
187
+ if let Some(mut stdin) = child.stdin.take() {
188
+ let content = stdin_content.clone();
189
+ tokio::spawn(async move {
190
+ let _ = stdin.write_all(content.as_bytes()).await;
191
+ let _ = stdin.shutdown().await;
192
+ });
193
+ }
194
+ }
195
+
196
+ // Read stdout
197
+ let mut stdout_content = String::new();
198
+ if let Some(mut stdout) = child.stdout.take() {
199
+ stdout.read_to_string(&mut stdout_content).await?;
200
+ }
201
+
202
+ // Read stderr
203
+ let mut stderr_content = String::new();
204
+ if let Some(mut stderr) = child.stderr.take() {
205
+ stderr.read_to_string(&mut stderr_content).await?;
206
+ }
207
+
208
+ // Mirror output if enabled and this is the last command
209
+ if is_last && self.mirror {
210
+ if !stdout_content.is_empty() {
211
+ print!("{}", stdout_content);
212
+ }
213
+ if !stderr_content.is_empty() {
214
+ eprint!("{}", stderr_content);
215
+ }
216
+ }
217
+
218
+ // Wait for the process
219
+ let status = child.wait().await?;
220
+ let code = status.code().unwrap_or(-1);
221
+
222
+ accumulated_stderr.push_str(&stderr_content);
223
+
224
+ if code != 0 {
225
+ return Ok(CommandResult {
226
+ stdout: stdout_content,
227
+ stderr: accumulated_stderr,
228
+ code,
229
+ });
230
+ }
231
+
232
+ // Set up stdin for next command
233
+ current_stdin = Some(stdout_content.clone());
234
+ last_result = CommandResult {
235
+ stdout: stdout_content,
236
+ stderr: String::new(),
237
+ code,
238
+ };
239
+ }
240
+
241
+ Ok(CommandResult {
242
+ stdout: last_result.stdout,
243
+ stderr: accumulated_stderr,
244
+ code: last_result.code,
245
+ })
246
+ }
247
+
248
+ /// Try to execute a virtual command
249
+ async fn try_virtual_command(
250
+ &self,
251
+ cmd_name: &str,
252
+ full_cmd: &str,
253
+ stdin: &Option<String>,
254
+ ) -> Option<CommandResult> {
255
+ let parts: Vec<&str> = full_cmd.split_whitespace().collect();
256
+ let args: Vec<String> = parts.iter().skip(1).map(|s| s.to_string()).collect();
257
+
258
+ let ctx = crate::commands::CommandContext {
259
+ args,
260
+ stdin: stdin.clone(),
261
+ cwd: self.cwd.clone(),
262
+ env: self.env.clone(),
263
+ output_tx: None,
264
+ is_cancelled: None,
265
+ };
266
+
267
+ match cmd_name {
268
+ "echo" => Some(crate::commands::echo(ctx).await),
269
+ "pwd" => Some(crate::commands::pwd(ctx).await),
270
+ "cd" => Some(crate::commands::cd(ctx).await),
271
+ "true" => Some(crate::commands::r#true(ctx).await),
272
+ "false" => Some(crate::commands::r#false(ctx).await),
273
+ "sleep" => Some(crate::commands::sleep(ctx).await),
274
+ "cat" => Some(crate::commands::cat(ctx).await),
275
+ "ls" => Some(crate::commands::ls(ctx).await),
276
+ "mkdir" => Some(crate::commands::mkdir(ctx).await),
277
+ "rm" => Some(crate::commands::rm(ctx).await),
278
+ "touch" => Some(crate::commands::touch(ctx).await),
279
+ "cp" => Some(crate::commands::cp(ctx).await),
280
+ "mv" => Some(crate::commands::mv(ctx).await),
281
+ "basename" => Some(crate::commands::basename(ctx).await),
282
+ "dirname" => Some(crate::commands::dirname(ctx).await),
283
+ "env" => Some(crate::commands::env(ctx).await),
284
+ "exit" => Some(crate::commands::exit(ctx).await),
285
+ "which" => Some(crate::commands::which(ctx).await),
286
+ "yes" => Some(crate::commands::yes(ctx).await),
287
+ "seq" => Some(crate::commands::seq(ctx).await),
288
+ "test" => Some(crate::commands::test(ctx).await),
289
+ _ => None,
290
+ }
291
+ }
292
+ }
293
+
294
+ /// Shell configuration
295
+ #[derive(Debug, Clone)]
296
+ struct ShellConfig {
297
+ cmd: String,
298
+ args: Vec<String>,
299
+ }
300
+
301
+ /// Find an available shell
302
+ fn find_available_shell() -> ShellConfig {
303
+ let is_windows = cfg!(windows);
304
+
305
+ if is_windows {
306
+ ShellConfig {
307
+ cmd: "cmd.exe".to_string(),
308
+ args: vec!["/c".to_string()],
309
+ }
310
+ } else {
311
+ let shells = [
312
+ ("/bin/sh", "-c"),
313
+ ("/usr/bin/sh", "-c"),
314
+ ("/bin/bash", "-c"),
315
+ ];
316
+
317
+ for (cmd, arg) in shells {
318
+ if std::path::Path::new(cmd).exists() {
319
+ return ShellConfig {
320
+ cmd: cmd.to_string(),
321
+ args: vec![arg.to_string()],
322
+ };
323
+ }
324
+ }
325
+
326
+ ShellConfig {
327
+ cmd: "/bin/sh".to_string(),
328
+ args: vec!["-c".to_string()],
329
+ }
330
+ }
331
+ }
332
+
333
+ /// Extension trait to add `.pipe()` method to ProcessRunner
334
+ pub trait PipelineExt {
335
+ /// Pipe the output of this command to another command
336
+ fn pipe(self, command: impl Into<String>) -> PipelineBuilder;
337
+ }
338
+
339
+ impl PipelineExt for crate::ProcessRunner {
340
+ fn pipe(self, command: impl Into<String>) -> PipelineBuilder {
341
+ PipelineBuilder {
342
+ first: self,
343
+ additional: vec![command.into()],
344
+ }
345
+ }
346
+ }
347
+
348
+ /// Builder for piping commands together
349
+ pub struct PipelineBuilder {
350
+ first: crate::ProcessRunner,
351
+ additional: Vec<String>,
352
+ }
353
+
354
+ impl PipelineBuilder {
355
+ /// Add another command to the pipeline
356
+ pub fn pipe(mut self, command: impl Into<String>) -> Self {
357
+ self.additional.push(command.into());
358
+ self
359
+ }
360
+
361
+ /// Execute the pipeline
362
+ pub async fn run(mut self) -> Result<CommandResult> {
363
+ // First, run the initial command
364
+ let first_result = self.first.run().await?;
365
+
366
+ if first_result.code != 0 {
367
+ return Ok(first_result);
368
+ }
369
+
370
+ // Then run the rest as a pipeline
371
+ let mut current_stdin = Some(first_result.stdout);
372
+ let mut accumulated_stderr = first_result.stderr;
373
+ let mut last_result = CommandResult {
374
+ stdout: String::new(),
375
+ stderr: String::new(),
376
+ code: 0,
377
+ };
378
+
379
+ for cmd_str in &self.additional {
380
+ let mut runner = crate::ProcessRunner::new(
381
+ cmd_str.clone(),
382
+ RunOptions {
383
+ stdin: StdinOption::Content(current_stdin.take().unwrap_or_default()),
384
+ mirror: false,
385
+ capture: true,
386
+ ..Default::default()
387
+ },
388
+ );
389
+
390
+ let result = runner.run().await?;
391
+ accumulated_stderr.push_str(&result.stderr);
392
+
393
+ if result.code != 0 {
394
+ return Ok(CommandResult {
395
+ stdout: result.stdout,
396
+ stderr: accumulated_stderr,
397
+ code: result.code,
398
+ });
399
+ }
400
+
401
+ current_stdin = Some(result.stdout.clone());
402
+ last_result = result;
403
+ }
404
+
405
+ Ok(CommandResult {
406
+ stdout: last_result.stdout,
407
+ stderr: accumulated_stderr,
408
+ code: last_result.code,
409
+ })
410
+ }
411
+ }