1#![warn(clippy::pedantic)]
30
31use std::ffi::OsStr;
32use std::fmt::{self, Debug, Display};
33use std::process::Command;
34
35const RESERVED_COMMAND_WORDS: &[&str] = &[
36 "case", "do", "done", "elif", "else", "esac", "fi", "for", "function", "if", "in", "select", "then", "time", "until", "while", ];
41
42pub struct Unquoted<'a>(pub &'a Command);
47
48impl Debug for Unquoted<'_> {
49 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 write!(f, "`")?;
51 for (name, value_opt) in self.0.get_envs() {
52 if let Some(value) = value_opt {
53 write!(f, "{}={} ", Word(name), Word(value))?;
54 }
55 }
56
57 let program = self.0.get_program();
58 if let Some(s) = program
59 .to_str()
60 .filter(|s| RESERVED_COMMAND_WORDS.binary_search(s).is_ok())
61 {
62 write!(f, "'{}'", s)?;
63 } else {
64 write!(f, "{}", Word(program))?;
65 }
66
67 for arg in self.0.get_args() {
68 write!(f, " {}", Word(arg))?;
69 }
70 write!(f, "`")
71 }
72}
73
74struct Word<'a>(&'a OsStr);
75
76impl Display for Word<'_> {
77 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78 let s = self.0.to_string_lossy();
81
82 if s.is_empty() {
83 return write!(f, "''");
84 }
85
86 let has_single_quote = s.contains('\'');
87 let has_special_within_double = s.contains(
88 [
89 '$', '`', '\\', '"', '@', '!', ]
93 .as_slice(),
94 );
95 let has_special = has_single_quote
96 || has_special_within_double
97 || s.contains(char::is_whitespace)
98 || s.contains(char::is_control)
99 || s.contains(
100 [
101 '|', '&', ';', '<', '>', '(', ')', ' ', '\t', '\n', '*', '?', '[', '#', '~', '%', ']', '{', '}', ]
110 .as_slice(),
111 );
112
113 if has_single_quote && !has_special_within_double {
114 write!(f, r#""{}""#, s)
116 } else if has_special {
117 if has_single_quote {
119 write!(f, "'")?;
120 for c in s.chars() {
121 if c == '\'' {
122 write!(f, "'\\''")?;
123 } else {
124 write!(f, "{}", c)?;
125 }
126 }
127 write!(f, "'")
128 } else {
129 write!(f, "'{}'", s)
130 }
131 } else {
132 write!(f, "{}", s)
134 }
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use std::{ffi::OsStr, process::Command};
141
142 use crate::{Unquoted, Word, RESERVED_COMMAND_WORDS};
143
144 #[test]
145 fn command_words_sorted() {
146 assert!(RESERVED_COMMAND_WORDS.windows(2).all(|s| s[0] < s[1]));
147 }
148
149 macro_rules! assert_q {
150 ($left:expr, $right:expr) => {
151 assert_eq!(Word(OsStr::new($left)).to_string(), $right)
152 };
153 }
154
155 #[test]
156 fn quoted() {
157 assert_q!("", r"''");
158 assert_q!("meow", r"meow");
159 assert_q!("にゃー", "にゃー");
160 assert_q!("meow", "meow");
161
162 assert_q!("meow meow", "'meow meow'");
164 assert_q!("meow\tmeow", "'meow\tmeow'");
165 assert_q!("meow\nmeow", "'meow\nmeow'");
166 assert_q!("meow meow", "'meow meow'");
167
168 assert_q!("|meow", "'|meow'");
170 assert_q!("meow&", "'meow&'");
171 assert_q!("meow;", "'meow;'");
172 assert_q!("<meow", "'<meow'");
173 assert_q!(">meow", "'>meow'");
174 assert_q!("(meow", "'(meow'");
175 assert_q!("meow)", "'meow)'");
176 assert_q!("$meow", "'$meow'");
177 assert_q!("`meow`", "'`meow`'");
178 assert_q!(r"\meow", r"'\meow'");
179 assert_q!(r#""meow""#, r#"'"meow"'"#);
180 assert_q!("meow*", "'meow*'");
181 assert_q!("meow?", "'meow?'");
182 assert_q!("[meow", "'[meow'");
183 assert_q!("meow]", "'meow]'");
184 assert_q!("{meow", "'{meow'");
185 assert_q!("meow}", "'meow}'");
186 assert_q!("#meow", "'#meow'");
187 assert_q!("~meow", "'~meow'");
188 assert_q!("%meow", "'%meow'");
189 assert_q!("@meow", "'@meow'");
190 assert_q!("!meow", "'!meow'");
191
192 assert_q!("meow's", r#""meow's""#);
194 assert_q!("|meow's", r#""|meow's""#);
197 assert_q!("meow's&", r#""meow's&""#);
198 assert_q!("meow's;", r#""meow's;""#);
199 assert_q!("<meow's", r#""<meow's""#);
200 assert_q!(">meow's", r#"">meow's""#);
201 assert_q!("(meow's", r#""(meow's""#);
202 assert_q!("meow's)", r#""meow's)""#);
203 assert_q!("meow's meow", r#""meow's meow""#);
204 assert_q!("meow's\tmeow", "\"meow's\tmeow\"");
205 assert_q!("meow's\nmeow", "\"meow's\nmeow\"");
206 assert_q!("meow's*", r#""meow's*""#);
207 assert_q!("meow's?", r#""meow's?""#);
208 assert_q!("[meow's", r#""[meow's""#);
209 assert_q!("meow's]", r#""meow's]""#);
210 assert_q!("{meow's", r#""{meow's""#);
211 assert_q!("meow's}", r#""meow's}""#);
212 assert_q!("#meow's", r##""#meow's""##);
213 assert_q!("~meow's", r#""~meow's""#);
214 assert_q!("%meow's", r#""%meow's""#);
215 assert_q!("$meow's", r"'$meow'\''s'");
218 assert_q!("`meow's`", r"'`meow'\''s`'");
219 assert_q!(r"\meow's", r"'\meow'\''s'");
220 assert_q!(r#""meow's""#, r#"'"meow'\''s"'"#);
221 assert_q!("@meow's", r"'@meow'\''s'");
222 assert_q!("!meow's", r"'!meow'\''s'");
223 }
224
225 macro_rules! assert_u {
226 ($value:expr, $display:expr) => {
227 assert_eq!(format!("{:?}", Unquoted(&$value)), $display)
228 };
229 }
230
231 #[test]
232 fn program_only() {
233 assert_u!(Command::new("program"), "`program`");
234 assert_u!(Command::new("programn't"), r#"`"programn't"`"#);
235 assert_u!(Command::new("case"), "`'case'`");
236 }
237
238 #[test]
239 fn args() {
240 assert_u!(
241 Command::new("program").args(["arg1", "arg b", "arg'c", r#"arg"d"#, "arg$e"]),
242 r#"`program arg1 'arg b' "arg'c" 'arg"d' 'arg$e'`"#
243 );
244 }
245
246 #[test]
247 fn env() {
248 assert_u!(
249 Command::new("program")
250 .env("BLAH1", "blah")
251 .env("BLAH2", "\"blah's blah\"")
252 .env("BLAH3", r#"\"blah's blah\""#),
253 r#"`BLAH1=blah BLAH2='"blah'\''s blah"' BLAH3='\"blah'\''s blah\"' program`"#
254 );
255 }
256}