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::collections::HashMap;
4use std::ffi::OsString;
5use std::path::PathBuf;
6use std::process::Stdio;
7use tokio::io::AsyncWriteExt;
8
9#[derive(Debug, Clone, Default)]
10pub enum CommandStdio {
11    #[default]
12    Pipe,
13    Inherit,
14    Ignore,
15}
16
17impl CommandStdio {
18    fn native(&self) -> Stdio {
19        match self {
20            Self::Pipe => Stdio::piped(),
21            Self::Inherit => Stdio::inherit(),
22            Self::Ignore => Stdio::null(),
23        }
24    }
25}
26
27/// Reusable builder that passes every argument literally to the executable.
28#[derive(Debug, Clone)]
29pub struct CommandBuilder {
30    pub file: OsString,
31    pub arguments: Vec<OsString>,
32    cwd: Option<PathBuf>,
33    env: HashMap<OsString, OsString>,
34    stdout: CommandStdio,
35    stderr: CommandStdio,
36    stdin: StdinOption,
37}
38
39impl CommandBuilder {
40    pub fn new(file: impl Into<OsString>) -> Self {
41        Self {
42            file: file.into(),
43            arguments: Vec::new(),
44            cwd: None,
45            env: HashMap::new(),
46            stdout: CommandStdio::Pipe,
47            stderr: CommandStdio::Pipe,
48            stdin: StdinOption::Null,
49        }
50    }
51
52    pub fn arg(mut self, arg: impl Into<OsString>) -> Self {
53        self.arguments.push(arg.into());
54        self
55    }
56
57    pub fn args<I, S>(mut self, args: I) -> Self
58    where
59        I: IntoIterator<Item = S>,
60        S: Into<OsString>,
61    {
62        self.arguments.extend(args.into_iter().map(Into::into));
63        self
64    }
65
66    pub fn cwd(mut self, path: impl Into<PathBuf>) -> Self {
67        self.cwd = Some(path.into());
68        self
69    }
70    pub fn env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Self {
71        self.env.insert(key.into(), value.into());
72        self
73    }
74    pub fn stdout(mut self, mode: CommandStdio) -> Self {
75        self.stdout = mode;
76        self
77    }
78    pub fn stderr(mut self, mode: CommandStdio) -> Self {
79        self.stderr = mode;
80        self
81    }
82    pub fn stdin(mut self, input: StdinOption) -> Self {
83        self.stdin = input;
84        self
85    }
86
87    pub async fn run(&self) -> Result<CommandResult> {
88        let mut command = tokio::process::Command::new(&self.file);
89        command
90            .args(&self.arguments)
91            .envs(&self.env)
92            .stdin(match &self.stdin {
93                StdinOption::Inherit => Stdio::inherit(),
94                StdinOption::Null => Stdio::null(),
95                _ => Stdio::piped(),
96            })
97            .stdout(self.stdout.native())
98            .stderr(self.stderr.native())
99            .kill_on_drop(true);
100        if let Some(cwd) = &self.cwd {
101            command.current_dir(cwd);
102        }
103        let mut child = command.spawn()?;
104        let input = child.stdin.take();
105        let write = async {
106            if let (Some(mut input), StdinOption::Content(data)) = (input, &self.stdin) {
107                if let Err(error) = input.write_all(data.as_bytes()).await {
108                    if error.kind() != std::io::ErrorKind::BrokenPipe {
109                        return Err(error);
110                    }
111                }
112            }
113            Ok::<(), std::io::Error>(())
114        };
115        let (output, written) = tokio::join!(child.wait_with_output(), write);
116        written?;
117        let output = output?;
118        let code = output.status.code().unwrap_or(1);
119        #[cfg(unix)]
120        let code = {
121            use std::os::unix::process::ExitStatusExt;
122            output.status.signal().map_or(code, |signal| 128 + signal)
123        };
124        Ok(CommandResult::new(
125            String::from_utf8_lossy(&output.stdout).into_owned(),
126            String::from_utf8_lossy(&output.stderr).into_owned(),
127            code,
128        ))
129    }
130
131    pub async fn exit_code(&self) -> Result<i32> {
132        Ok(self.run().await?.code)
133    }
134
135    /// Render for a POSIX shell. Running the builder itself never uses this string.
136    pub fn shell_string(&self) -> String {
137        std::iter::once(&self.file)
138            .chain(&self.arguments)
139            .map(|value| format!("'{}'", value.to_string_lossy().replace('\'', "'\\''")))
140            .collect::<Vec<_>>()
141            .join(" ")
142    }
143}
144
145/// Translate a shell script into an executable JavaScript module.
146pub fn fy(source: &str) -> String {
147    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"))
148}