Skip to main content

command_stream/
command_builder.rs

1//! Lazy exact-argv command descriptions, parallel to JavaScript `$.command`.
2use crate::{CommandResult, Result, StdinOption};
3use std::ffi::OsString;
4use std::path::PathBuf;
5use std::process::Stdio;
6use tokio::io::AsyncWriteExt;
7
8#[derive(Debug, Clone, Default)]
9pub enum CommandStdio {
10    #[default]
11    Pipe,
12    Inherit,
13    Ignore,
14}
15
16impl CommandStdio {
17    fn native(&self) -> Stdio {
18        match self {
19            Self::Pipe => Stdio::piped(),
20            Self::Inherit => Stdio::inherit(),
21            Self::Ignore => Stdio::null(),
22        }
23    }
24}
25
26/// Reusable builder that passes every argument literally to the executable.
27#[derive(Debug, Clone)]
28pub struct CommandBuilder {
29    pub file: OsString,
30    pub arguments: Vec<OsString>,
31    cwd: Option<PathBuf>,
32    // Replay changes in order so native Command applies platform-specific
33    // key equality (notably case-insensitive environment names on Windows).
34    env: Vec<(OsString, Option<OsString>)>,
35    clear_env: bool,
36    stdout: CommandStdio,
37    stderr: CommandStdio,
38    stdin: StdinOption,
39}
40
41impl CommandBuilder {
42    pub fn new(file: impl Into<OsString>) -> Self {
43        Self {
44            file: file.into(),
45            arguments: Vec::new(),
46            cwd: None,
47            env: Vec::new(),
48            clear_env: false,
49            stdout: CommandStdio::Pipe,
50            stderr: CommandStdio::Pipe,
51            stdin: StdinOption::Null,
52        }
53    }
54
55    pub fn arg(mut self, arg: impl Into<OsString>) -> Self {
56        self.arguments.push(arg.into());
57        self
58    }
59
60    pub fn args<I, S>(mut self, args: I) -> Self
61    where
62        I: IntoIterator<Item = S>,
63        S: Into<OsString>,
64    {
65        self.arguments.extend(args.into_iter().map(Into::into));
66        self
67    }
68
69    pub fn cwd(mut self, path: impl Into<PathBuf>) -> Self {
70        self.cwd = Some(path.into());
71        self
72    }
73    pub fn env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Self {
74        self.env.push((key.into(), Some(value.into())));
75        self
76    }
77    pub fn envs<I, K, V>(mut self, variables: I) -> Self
78    where
79        I: IntoIterator<Item = (K, V)>,
80        K: Into<OsString>,
81        V: Into<OsString>,
82    {
83        for (key, value) in variables {
84            self = self.env(key, value);
85        }
86        self
87    }
88    pub fn env_remove(mut self, key: impl Into<OsString>) -> Self {
89        self.env.push((key.into(), None));
90        self
91    }
92    pub fn env_clear(mut self) -> Self {
93        self.env.clear();
94        self.clear_env = true;
95        self
96    }
97    pub fn stdout(mut self, mode: CommandStdio) -> Self {
98        self.stdout = mode;
99        self
100    }
101    pub fn stderr(mut self, mode: CommandStdio) -> Self {
102        self.stderr = mode;
103        self
104    }
105    pub fn stdin(mut self, input: StdinOption) -> Self {
106        self.stdin = input;
107        self
108    }
109
110    /// Apply an owned builder transformation without executing the command.
111    pub fn pipe(self, transform: impl FnOnce(Self) -> Self) -> Self {
112        transform(self)
113    }
114
115    fn native_command(&self) -> std::process::Command {
116        let mut command = std::process::Command::new(&self.file);
117        command
118            .args(&self.arguments)
119            .stdin(match &self.stdin {
120                StdinOption::Inherit => Stdio::inherit(),
121                StdinOption::Null => Stdio::null(),
122                _ => Stdio::piped(),
123            })
124            .stdout(self.stdout.native())
125            .stderr(self.stderr.native());
126        if self.clear_env {
127            command.env_clear();
128        }
129        for (key, value) in &self.env {
130            if let Some(value) = value {
131                command.env(key, value);
132            } else {
133                command.env_remove(key);
134            }
135        }
136        if let Some(cwd) = &self.cwd {
137            command.current_dir(cwd);
138        }
139        command
140    }
141
142    pub async fn run(&self) -> Result<CommandResult> {
143        let mut command = tokio::process::Command::from(self.native_command());
144        command.kill_on_drop(true);
145        let mut child = command.spawn()?;
146        let input = child.stdin.take();
147        let write = async {
148            if let (Some(mut input), StdinOption::Content(data)) = (input, &self.stdin) {
149                if let Err(error) = input.write_all(data.as_bytes()).await {
150                    if error.kind() != std::io::ErrorKind::BrokenPipe {
151                        return Err(error);
152                    }
153                }
154            }
155            Ok::<(), std::io::Error>(())
156        };
157        let (output, written) = tokio::join!(child.wait_with_output(), write);
158        written?;
159        Ok(Self::result(output?))
160    }
161
162    /// Execute synchronously, draining output while supplying buffered stdin.
163    pub fn run_sync(&self) -> Result<CommandResult> {
164        let mut child = self.native_command().spawn()?;
165        let input = child.stdin.take();
166        let output = std::thread::scope(|scope| {
167            let writer = scope.spawn(|| {
168                if let (Some(mut input), StdinOption::Content(data)) = (input, &self.stdin) {
169                    use std::io::Write;
170                    if let Err(error) = input.write_all(data.as_bytes()) {
171                        if error.kind() != std::io::ErrorKind::BrokenPipe {
172                            return Err(error);
173                        }
174                    }
175                }
176                Ok::<(), std::io::Error>(())
177            });
178            let output = child.wait_with_output();
179            writer
180                .join()
181                .map_err(|_| std::io::Error::other("command stdin writer panicked"))??;
182            output
183        })?;
184        Ok(Self::result(output))
185    }
186
187    fn result(output: std::process::Output) -> CommandResult {
188        let code = output.status.code().unwrap_or(1);
189        #[cfg(unix)]
190        let code = {
191            use std::os::unix::process::ExitStatusExt;
192            output.status.signal().map_or(code, |signal| 128 + signal)
193        };
194        CommandResult::new(
195            String::from_utf8_lossy(&output.stdout).into_owned(),
196            String::from_utf8_lossy(&output.stderr).into_owned(),
197            code,
198        )
199    }
200
201    pub async fn exit_code(&self) -> Result<i32> {
202        Ok(self.run().await?.code)
203    }
204
205    /// Render for a POSIX shell. Running the builder itself never uses this string.
206    pub fn shell_string(&self) -> String {
207        std::iter::once(&self.file)
208            .chain(&self.arguments)
209            .map(|value| format!("'{}'", value.to_string_lossy().replace('\'', "'\\''")))
210            .collect::<Vec<_>>()
211            .join(" ")
212    }
213}
214
215/// Translate a shell script into an executable JavaScript module.
216pub fn fy(source: &str) -> String {
217    format!("#!/usr/bin/env node\nimport {{ command }} from 'command-stream';\nconst source = {};\nconst result = await command('sh', '-c', source, 'translated.sh', ...process.argv.slice(2)).with({{ mirror: true, capture: false, stdin: 'inherit' }}).run();\nprocess.exitCode = result.code;\n", serde_json::to_string(source).expect("string serialization"))
218}