start_command/
isolation_shell.rs1const SHELL_NAMES: [&str; 8] = ["bash", "zsh", "sh", "fish", "ksh", "csh", "tcsh", "dash"];
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum ShellQuotingStyle {
8 Posix,
10 PowerShell,
12}
13
14impl ShellQuotingStyle {
15 pub fn host() -> Self {
17 if cfg!(windows) {
18 ShellQuotingStyle::PowerShell
19 } else {
20 ShellQuotingStyle::Posix
21 }
22 }
23}
24
25fn is_safe_arg(arg: &str, style: ShellQuotingStyle) -> bool {
30 let extra = match style {
31 ShellQuotingStyle::Posix => "_@%+=:,./^-",
32 ShellQuotingStyle::PowerShell => "_=:./\\-",
33 };
34 !arg.is_empty()
35 && arg
36 .chars()
37 .all(|c| c.is_ascii_alphanumeric() || extra.contains(c))
38}
39
40pub fn quote_shell_arg_with(arg: &str, style: ShellQuotingStyle) -> String {
42 if is_safe_arg(arg, style) {
43 return arg.to_string();
44 }
45 match style {
46 ShellQuotingStyle::Posix => format!("'{}'", arg.replace('\'', "'\\''")),
47 ShellQuotingStyle::PowerShell => format!("'{}'", arg.replace('\'', "''")),
48 }
49}
50
51pub fn quote_shell_arg(arg: &str) -> String {
53 quote_shell_arg_with(arg, ShellQuotingStyle::host())
54}
55
56pub fn build_command_string_with(argv: &[String], style: ShellQuotingStyle) -> String {
62 match argv {
63 [] => String::new(),
64 [single] => single.clone(),
65 _ => argv
66 .iter()
67 .map(|arg| quote_shell_arg_with(arg, style))
68 .collect::<Vec<_>>()
69 .join(" "),
70 }
71}
72
73pub fn build_command_string(argv: &[String]) -> String {
75 build_command_string_with(argv, ShellQuotingStyle::host())
76}
77
78pub fn split_shell_words_with(command: &str, style: ShellQuotingStyle) -> Option<Vec<String>> {
85 let is_powershell = style == ShellQuotingStyle::PowerShell;
86 let mut words: Vec<String> = Vec::new();
87 let mut current = String::new();
88 let mut started = false;
89 let mut quote: Option<char> = None;
90 let mut chars = command.chars().peekable();
91
92 while let Some(c) = chars.next() {
93 if quote.is_none() && c.is_whitespace() {
94 if started {
95 words.push(std::mem::take(&mut current));
96 started = false;
97 }
98 continue;
99 }
100 started = true;
101 if !is_powershell && c == '\\' && quote != Some('\'') {
102 let escaped = chars.next()?;
103 current.push(escaped);
104 continue;
105 }
106 if quote.is_none() && (c == '\'' || c == '"') {
107 quote = Some(c);
108 continue;
109 }
110 if Some(c) == quote {
111 if is_powershell && chars.peek() == Some(&c) {
112 chars.next();
113 current.push(c);
114 continue;
115 }
116 quote = None;
117 continue;
118 }
119 current.push(c);
120 }
121
122 if quote.is_some() {
123 return None;
124 }
125 if started {
126 words.push(current);
127 }
128 Some(words)
129}
130
131pub fn split_shell_words(command: &str) -> Option<Vec<String>> {
133 split_shell_words_with(command, ShellQuotingStyle::host())
134}
135
136pub fn to_shell_words(command: &str) -> Vec<String> {
138 split_shell_words(command).unwrap_or_else(|| {
139 command
140 .split_whitespace()
141 .map(|part| part.to_string())
142 .collect()
143 })
144}
145
146fn basename(part: &str) -> &str {
147 part.rsplit('/').next().unwrap_or(part)
148}
149
150fn is_shell_command(parts: &[String]) -> bool {
151 parts
152 .first()
153 .is_some_and(|first| SHELL_NAMES.contains(&basename(first)))
154}
155
156pub fn is_interactive_shell_command(command: &str) -> bool {
158 let parts = to_shell_words(command);
159 is_shell_command(&parts) && !parts.iter().any(|part| part == "-c")
160}
161
162pub fn is_shell_invocation_with_args(command: &str) -> bool {
164 let parts = to_shell_words(command);
165 is_shell_command(&parts) && parts.iter().any(|part| part == "-c")
166}
167
168pub fn build_shell_with_args_cmd_args(command: &str) -> Vec<String> {
170 let parts = to_shell_words(command);
171 let Some(index) = parts.iter().position(|part| part == "-c") else {
172 return parts;
173 };
174 let script = parts[index + 1..].join(" ");
175 let mut result = parts[..=index].to_vec();
176 if !script.is_empty() {
177 result.push(script);
178 }
179 result
180}
181
182fn quote_for_display(arg: &str) -> String {
184 if is_safe_arg(arg, ShellQuotingStyle::Posix) {
187 arg.to_string()
188 } else if arg.contains('"') {
189 format!("'{}'", arg)
190 } else {
191 format!("\"{}\"", arg)
192 }
193}
194
195pub fn build_display_command(command: &str) -> String {
200 if is_shell_invocation_with_args(command) {
201 return build_shell_with_args_cmd_args(command)
202 .iter()
203 .map(|arg| quote_for_display(arg))
204 .collect::<Vec<_>>()
205 .join(" ");
206 }
207 match split_shell_words(command) {
208 Some(words) if build_command_string(&words) == command => words
209 .iter()
210 .map(|arg| quote_for_display(arg))
211 .collect::<Vec<_>>()
212 .join(" "),
213 _ => command.to_string(),
214 }
215}
216
217pub fn command_name(command: &str) -> String {
219 to_shell_words(command)
220 .into_iter()
221 .next()
222 .unwrap_or_default()
223}