fm/modes/utils/
shell_escape.rs1use std::{borrow::Cow, ffi::OsStr};
2
3use anyhow::Result;
4
5pub trait Quote<S> {
10 fn quote(&self) -> Result<S>;
20}
21
22impl Quote<String> for String {
23 fn quote(&self) -> Result<String> {
24 self.as_str().quote()
25 }
26}
27
28impl Quote<String> for &str {
29 fn quote(&self) -> Result<String> {
30 try_quote(self)
31 }
32}
33
34impl Quote<String> for Cow<'_, str> {
35 fn quote(&self) -> Result<String> {
36 try_quote(self)
37 }
38}
39
40impl Quote<String> for &OsStr {
41 fn quote(&self) -> Result<String> {
42 self.to_string_lossy().quote()
43 }
44}
45
46fn must_quote(byte: u8) -> bool {
47 matches!(byte, b' ' | b'\'' | b'"')
48}
49
50fn try_quote(s: &str) -> Result<String> {
53 if !s.bytes().any(must_quote) {
54 Ok(s.into())
55 } else {
56 let len = s.len();
57 let mut escaped = Vec::with_capacity(len + 2);
58 escaped.push(b'\'');
59 for byte in s.bytes() {
60 match byte {
61 b'\'' | b'"' => {
62 escaped.reserve(4);
63 escaped.push(b'\'');
64 escaped.push(b'\\');
65 escaped.push(byte);
66 escaped.push(b'\'');
67 }
68 _ => escaped.push(byte),
69 }
70 }
71 escaped.push(b'\'');
72 let s = String::from_utf8(escaped)?;
73 crate::log_info!("try quote: #{s}#");
74 Ok(s)
75 }
76}
77
78pub trait JoinQuote {
80 fn join_quote(&self, sep: &str) -> String;
81}
82
83impl JoinQuote for &Vec<String> {
84 fn join_quote(&self, sep: &str) -> String {
86 self.iter()
87 .filter_map(|fp| fp.quote().ok())
88 .collect::<Vec<_>>()
89 .join(sep)
90 }
91}
92
93pub fn append_files_to_shell_command(shell_command: String, files: Option<Vec<String>>) -> String {
96 let Some(files) = &files else {
97 return shell_command;
98 };
99 shell_command + " " + &files.join_quote(" ")
100}