printable_shell_command/
printable_shell_command.rs

1use std::{
2    ffi::{OsStr, OsString},
3    ops::{Deref, DerefMut},
4    process::Command,
5    str::Utf8Error,
6};
7
8use itertools::Itertools;
9
10use crate::{
11    escape::{SimpleEscapeOptions, simple_escape},
12    shell_printable::ShellPrintable,
13};
14
15pub struct PrintableShellCommand {
16    arg_groups: Vec<Vec<OsString>>,
17    command: Command,
18}
19
20// TODO: this depends on the interface to `Command` supporting the *appending*
21// of algs but not the removal/reordering/editing of any args added so far. Is
22// it even remotely possible to fail compilation if the args in `Command` become
23// mutable like this?
24impl PrintableShellCommand {
25    pub fn new<S: AsRef<OsStr>>(program: S) -> Self {
26        Self {
27            arg_groups: vec![],
28            command: Command::new(program),
29        }
30    }
31
32    pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
33        self.adopt_args();
34        let arg = self.arg_without_adoption(arg);
35        self.command.arg(arg);
36        self
37    }
38
39    fn arg_without_adoption<S: AsRef<OsStr>>(&mut self, arg: S) -> S {
40        self.arg_groups.push(vec![(&arg).into()]);
41        arg
42    }
43
44    pub fn args<I, S>(&mut self, args: I) -> &mut Self
45    where
46        I: IntoIterator<Item = S>,
47        S: AsRef<OsStr>,
48    {
49        self.adopt_args();
50        let args = self.args_without_adoption(args);
51        self.command.args(args);
52        self
53    }
54
55    fn args_without_adoption<I, S>(&mut self, args: I) -> Vec<OsString>
56    where
57        I: IntoIterator<Item = S>,
58        S: AsRef<OsStr>,
59    {
60        let args: Vec<OsString> = args
61            .into_iter()
62            .map(|arg| std::convert::Into::<OsString>::into(&arg))
63            .collect();
64        self.arg_groups.push(args.clone());
65        args
66    }
67
68    fn args_to_adopt(&self) -> Vec<OsString> {
69        let mut to_adopt: Vec<OsString> = vec![];
70        for either_or_both in self
71            .arg_groups
72            .iter()
73            .flatten()
74            .zip_longest(self.command.get_args())
75        {
76            match either_or_both {
77                itertools::EitherOrBoth::Both(a, b) => {
78                    if a != b {
79                        panic!("Command args do not match. This should not be possible.")
80                    }
81                }
82                itertools::EitherOrBoth::Left(_) => {
83                    panic!("Command is missing a previously seen arg. This should not be possible.")
84                }
85                itertools::EitherOrBoth::Right(arg) => {
86                    to_adopt.push(arg.to_owned());
87                }
88            }
89        }
90        dbg!(&to_adopt);
91
92        to_adopt
93    }
94
95    /// Adopt any args that were added to the underlying `Command` (from a
96    /// `Deref`). Calling this function caches args instead of requiring
97    /// throwaway work when subsequently generating printable strings (which
98    /// would be inefficient when done multiple times).
99    pub fn adopt_args(&mut self) -> &mut Self {
100        for arg in self.args_to_adopt() {
101            self.arg_without_adoption(arg);
102        }
103        self
104    }
105}
106
107impl Deref for PrintableShellCommand {
108    type Target = Command;
109
110    fn deref(&self) -> &Command {
111        &self.command
112    }
113}
114
115impl DerefMut for PrintableShellCommand {
116    /// If args are added to the underlying command, they will be added as individual arg groups by `PrintableShellCommand`.
117    fn deref_mut(&mut self) -> &mut Command {
118        &mut self.command
119    }
120}
121
122impl From<Command> for PrintableShellCommand {
123    /// Adopts a `Command`, treating each arg as its own group (i.e. each arg will be printed on a separate line).
124    fn from(command: Command) -> Self {
125        let mut printable_shell_command = Self {
126            arg_groups: vec![],
127            command,
128        };
129        printable_shell_command.adopt_args();
130        printable_shell_command
131    }
132}
133
134impl ShellPrintable for PrintableShellCommand {
135    fn printable_invocation_string_lossy(&self) -> String {
136        let mut lines: Vec<String> = vec![simple_escape(
137            &self.command.get_program().to_string_lossy(),
138            SimpleEscapeOptions { is_command: true },
139        )];
140
141        // TODO: make this more efficient. (Take `&mut self` in the trait? Interior mutability?)
142        let a = self.arg_groups.iter();
143        let b: Vec<Vec<OsString>> = self
144            .args_to_adopt()
145            .into_iter()
146            .map(|arg| vec![arg])
147            .collect();
148
149        for arg_group in a.chain(&b) {
150            let mut line_parts = vec![];
151            for arg in arg_group {
152                line_parts.push(simple_escape(
153                    &arg.to_string_lossy(),
154                    SimpleEscapeOptions { is_command: false },
155                ))
156            }
157            lines.push(line_parts.join(" "))
158        }
159        lines.join(
160            " \\
161  ",
162        )
163    }
164
165    fn printable_invocation_string(&self) -> Result<String, Utf8Error> {
166        let mut lines: Vec<String> = vec![simple_escape(
167            TryInto::<&str>::try_into(self.command.get_program())?,
168            SimpleEscapeOptions { is_command: true },
169        )];
170
171        // TODO: make this more efficient. (Take `&mut self` in the trait? Interior mutability?)
172        let a = self.arg_groups.iter();
173        let b: Vec<Vec<OsString>> = self
174            .args_to_adopt()
175            .into_iter()
176            .map(|arg| vec![arg])
177            .collect();
178
179        for arg_group in a.chain(&b) {
180            let mut line_parts = vec![];
181            for arg in arg_group {
182                let s = TryInto::<&str>::try_into(arg.as_os_str())?;
183                line_parts.push(simple_escape(s, SimpleEscapeOptions { is_command: false }))
184            }
185            lines.push(line_parts.join(" "))
186        }
187        Ok(lines.join(
188            " \\
189  ",
190        ))
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use std::{ops::DerefMut, process::Command};
197
198    use crate::{PrintableShellCommand, ShellPrintable};
199
200    #[test]
201    fn echo() -> Result<(), String> {
202        let mut printable_shell_command = PrintableShellCommand::new("echo");
203        printable_shell_command.args(["#hi"]);
204        // Not printed by successful tests, but we can at least check this doesn't panic.
205        let _ = printable_shell_command.print_invocation();
206
207        assert_eq!(
208            printable_shell_command
209                .printable_invocation_string()
210                .unwrap(),
211            "echo \\
212  '#hi'"
213        );
214        assert_eq!(
215            printable_shell_command
216                .printable_invocation_string()
217                .unwrap(),
218            printable_shell_command.printable_invocation_string_lossy(),
219        );
220        Ok(())
221    }
222
223    #[test]
224    fn ffmpeg() -> Result<(), String> {
225        let mut printable_shell_command = PrintableShellCommand::new("ffmpeg");
226        printable_shell_command
227            .args(["-i", "./test/My video.mp4"])
228            .args(["-filter:v", "setpts=2.0*PTS"])
229            .args(["-filter:a", "atempo=0.5"])
230            .arg("./test/My video (slow-mo).mov");
231        // Not printed by successful tests, but we can at least check this doesn't panic.
232        let _ = printable_shell_command.print_invocation();
233
234        assert_eq!(
235            printable_shell_command
236                .printable_invocation_string()
237                .unwrap(),
238            "ffmpeg \\
239  -i './test/My video.mp4' \\
240  -filter:v 'setpts=2.0*PTS' \\
241  -filter:a atempo=0.5 \\
242  './test/My video (slow-mo).mov'"
243        );
244        assert_eq!(
245            printable_shell_command
246                .printable_invocation_string()
247                .unwrap(),
248            printable_shell_command.printable_invocation_string_lossy(),
249        );
250        Ok(())
251    }
252
253    #[test]
254    fn from_command() -> Result<(), String> {
255        let mut command = Command::new("echo");
256        command.args(["hello", "#world"]);
257        // Not printed by tests, but we can at least check this doesn't panic.
258        let mut printable_shell_command = PrintableShellCommand::from(command);
259        let _ = printable_shell_command.print_invocation();
260
261        assert_eq!(
262            printable_shell_command
263                .printable_invocation_string()
264                .unwrap(),
265            "echo \\
266  hello \\
267  '#world'"
268        );
269        Ok(())
270    }
271
272    #[test]
273    fn adoption() -> Result<(), String> {
274        let mut printable_shell_command = PrintableShellCommand::new("echo");
275
276        {
277            let command: &mut Command = printable_shell_command.deref_mut();
278            command.arg("hello");
279            command.arg("#world");
280        }
281
282        printable_shell_command
283            .printable_invocation_string()
284            .unwrap();
285        assert_eq!(
286            printable_shell_command
287                .printable_invocation_string()
288                .unwrap(),
289            "echo \\
290  hello \\
291  '#world'"
292        );
293
294        printable_shell_command.args(["wide", "web"]);
295
296        printable_shell_command
297            .printable_invocation_string()
298            .unwrap();
299        assert_eq!(
300            printable_shell_command
301                .printable_invocation_string()
302                .unwrap(),
303            "echo \\
304  hello \\
305  '#world' \\
306  wide web"
307        );
308
309        // Second adoption
310        {
311            let command: &mut Command = printable_shell_command.deref_mut();
312            command.arg("to").arg("the").arg("internet");
313        }
314        // Test adoption idempotency.
315        printable_shell_command.adopt_args();
316        printable_shell_command.adopt_args();
317        printable_shell_command.adopt_args();
318        assert_eq!(
319            printable_shell_command
320                .printable_invocation_string()
321                .unwrap(),
322            "echo \\
323  hello \\
324  '#world' \\
325  wide web \\
326  to \\
327  the \\
328  internet"
329        );
330
331        Ok(())
332    }
333
334    // TODO: test invalid UTF-8
335}