cmd_utils/
cmd_pipe.rs

1use std::{io, fs::File};
2use io::{BufWriter, Write, BufReader, BufRead};
3use std::process::{Command, Stdio, Output};
4
5/// Pipe **stdout** of `command` to **stdin** of `piped` command,
6/// and pipe **stdout** of `piped` to `piped_stdout`
7///
8/// # Errors
9///
10/// command_pipe can result in `std::io::Error`
11/// - when `spawn` or `wait` fail
12/// - when there is an issue with the **stdout** / **stdin** pipe (std::io::ErrorKind::BrokenPipe)
13#[allow(clippy::manual_flatten)]
14pub fn command_pipe_base<T> (
15    command: &mut Command, piped: &mut Command, piped_stdout: T
16) -> Result<Output, io::Error> where T: Into<Stdio>{
17    let process = command.stdout(Stdio::piped()).spawn();
18    match process {
19        Ok(child) => {
20            if let Some(stdout) = child.stdout {
21                let piped_process = piped
22                    .stdin(Stdio::piped())
23                    .stdout(piped_stdout)
24                    .spawn();
25                match piped_process {
26                    Ok(mut piped_child) => {
27                        if let Some(mut stdin) = piped_child.stdin.take() {
28                            let mut writer = BufWriter::new(&mut stdin);
29                            for line in BufReader::new(stdout).lines() {
30                                if let Ok(l) = line {
31                                    writer.write_all(l.as_bytes()).unwrap();
32                                    writer.write_all(&[b'\n']).unwrap();
33                                }
34                            }
35                        } else {
36                            return Err(io::Error::new(
37                                io::ErrorKind::BrokenPipe,
38                                "Could not pipe command, stdin not found"
39                            ))
40                        }
41                        if let Ok(out) = piped_child.wait_with_output() {
42                            return Ok(out)
43                        } else {
44                            return Err(io::Error::new(
45                                io::ErrorKind::BrokenPipe,
46                                "Could not wait for pipe"
47                            ))
48                        }
49                    },
50                    Err(e) => return Err(e),
51                }
52            } else {
53                return Err(io::Error::new(
54                    io::ErrorKind::BrokenPipe,
55                    "Could not pipe command, stdout not found"
56                ))
57            }
58        },
59        Err(e) => return Err(e),
60    }
61}
62
63/// Pipe **stdout** of `command` to **stdin** of `piped` command
64///
65/// # Errors
66///
67/// command_pipe can result in `std::io::Error`
68/// - when `spawn` or `wait` fail
69/// - when there is an issue with the **stdout** / **stdin** pipe (std::io::ErrorKind::BrokenPipe)
70pub fn command_pipe (
71    command: &mut Command, piped: &mut Command
72) -> Result<Output, io::Error> {
73    return command_pipe_base(command, piped, Stdio::piped());
74}
75
76/// Pipe **stdout** of `command` to **stdin** of `piped` command
77/// and pipe **stdout** of `piped` to `file`
78///
79/// # Errors
80///
81/// command_pipe_to_file can result in `std::io::Error`
82/// - when `spawn` or `wait` fail
83/// - when there is an issue with the **stdout** / **stdin** pipe (std::io::ErrorKind::BrokenPipe)
84pub fn command_pipe_to_file (
85    command: &mut Command, piped: &mut Command, file: File
86) -> Result<(), io::Error> {
87    return match command_pipe_base(command, piped, file) {
88        Ok(_) => Ok(()),
89        Err(e) => Err(e),
90    };
91}
92
93pub trait CmdPipe {
94    /// Pipe **stdout** of `self` to **stdin** of `piped_command`
95    ///
96    /// # Errors
97    ///
98    /// command.pipe(cmd) can result in `std::io::Error`
99    /// - when `spawn` or `wait` fail
100    /// - when there is an issue with the **stdout** / **stdin** pipe (std::io::ErrorKind::BrokenPipe)
101    fn pipe (&mut self, piped_command: &mut Command) -> Result<Output, io::Error>;
102
103    /// Pipe **stdout** of `self` to **stdin** of `piped_command`,
104    /// and pipe **stdout** of `piped_command` to `file`
105    /// # Errors
106    ///
107    /// command.pipe_to_file(cmd, file) can result in `std::io::Error`
108    /// - when `spawn` or `wait` fail
109    /// - when there is an issue with the **stdout** / **stdin** pipe (std::io::ErrorKind::BrokenPipe)
110    fn pipe_to_file (&mut self, piped_command: &mut Command, file: File) -> Result<(), io::Error>;
111}
112
113impl CmdPipe for Command {
114    fn pipe(&mut self, piped_command: &mut Command) -> Result<Output, io::Error> {
115        command_pipe(self, piped_command)
116    }
117
118    fn pipe_to_file (&mut self, piped_command: &mut Command, file: File) -> Result<(), io::Error> {
119        command_pipe_to_file(self, piped_command, file)
120    }
121}
122
123// TODO: allow piping multiple commands
124// pub fn pipe_commands (commands: Vec<&mut Command>) -> Result<Output, io::Error>{
125//     todo!()
126// }
127
128#[cfg(test)]
129mod test {
130    use std::fs;
131    use std::io::Read;
132    use std::process::Command;
133    use std::str::from_utf8;
134
135    use super::CmdPipe;
136
137    use super::command_pipe;
138
139    #[test]
140    fn test_command_to_pipe () {
141        let mut echo = Command::new("echo");
142        let mut wc = Command::new("wc");
143        let output = command_pipe(
144            &mut echo.args(["-n", "test"]),
145            &mut wc.arg("-c")
146        ).unwrap();
147        let res = match from_utf8(&output.stdout) {
148            Ok(s) => s,
149            Err(_) => panic!("unexpected stdout"),
150        };
151        // MacOs puts whitespace in from of wc result
152        assert_eq!(str::trim_start(res), "5\n");
153    }
154
155    #[test]
156    fn test_command_pipe_trait () {
157        let mut echo = Command::new("echo");
158        let mut wc = Command::new("wc");
159        let output = echo.args(["-n", "test"])
160            .pipe(&mut wc.arg("-c"))
161            .unwrap();
162        let res = match from_utf8(&output.stdout) {
163            Ok(s) => s,
164            Err(_) => panic!("unexpected stdout"),
165        };
166        // MacOs puts whitespace in from of wc result
167        assert_eq!(str::trim_start(res), "5\n");
168    }
169
170    #[test]
171    fn test_command_pipe_to_file () {
172        const FILE_NAME: &str = "tmp/__command_pipe_to_file";
173        let mut echo = Command::new("echo");
174        let mut wc = Command::new("wc");
175        if let Err(e) = fs::create_dir("tmp") {
176            match e.kind() {
177                std::io::ErrorKind::AlreadyExists => (),
178                _ => panic!("{}", e),
179            }
180        }
181        let file = fs::File::create(FILE_NAME).unwrap();
182        echo.args(["-n", "test"])
183            .pipe_to_file(&mut wc.arg("-c"), file)
184            .unwrap();
185
186        // read result
187        let mut read_file = fs::File::open(FILE_NAME).unwrap();
188        let mut res = String::new();
189        match read_file.read_to_string(&mut res) {
190            Err(e) => panic!("{}", e),
191            _ => (),
192        };
193
194        assert_eq!(str::trim_start(&res), "5\n");
195
196        // cleanup file
197        fs::remove_file(FILE_NAME).unwrap();
198    }
199}