printable_shell_command/
printable_shell_command.rs1use 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
20impl 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 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 fn deref_mut(&mut self) -> &mut Command {
118 &mut self.command
119 }
120}
121
122impl From<Command> for PrintableShellCommand {
123 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 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 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 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 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 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 {
311 let command: &mut Command = printable_shell_command.deref_mut();
312 command.arg("to").arg("the").arg("internet");
313 }
314 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 }