1use std::borrow::Cow;
2
3use crate::shell::get_system_shell;
4use crate::shell::{
5 Shell, ShellKind, args_for_shell_option, to_shell_variable_option, try_quote_option,
6 try_quote_prefix_aware_option,
7};
8
9const POSIX_STDIN_REDIRECT_PREFIX: char = '(';
10const POSIX_STDIN_REDIRECT_SUFFIX: &str = ") </dev/null";
11const POWERSHELL_STDIN_REDIRECT_PREFIX: &str = "$null | & {";
12const POWERSHELL_STDIN_REDIRECT_SUFFIX: &str = "}";
13
14/// ShellBuilder is used to turn a user-requested task into a
15/// program that can be executed by the shell.
16pub struct ShellBuilder {
17 /// The shell to run
18 program: String,
19 args: Vec<String>,
20 interactive: bool,
21 /// Whether to redirect stdin to /dev/null for the spawned command as a subshell.
22 redirect_stdin: bool,
23 kind: Option<ShellKind>,
24}
25
26impl ShellBuilder {
27 /// Create a new ShellBuilder as configured.
28 pub fn new(shell: &Shell) -> Self {
29 let (program, args) = match shell {
30 Shell::System => (get_system_shell(), Vec::new()),
31 Shell::Program(shell) => (shell.clone(), Vec::new()),
32 Shell::WithArguments { program, args, .. } => (program.clone(), args.clone()),
33 };
34
35 let kind = ShellKind::new(&program);
36 Self {
37 program,
38 args,
39 interactive: true,
40 kind,
41 redirect_stdin: false,
42 }
43 }
44 pub fn non_interactive(mut self) -> Self {
45 self.interactive = false;
46 self
47 }
48
49 /// Returns the label to show in the terminal tab
50 pub fn command_label(&self, command_to_use_in_label: &str) -> String {
51 if command_to_use_in_label.trim().is_empty() {
52 self.program.clone()
53 } else {
54 match self.kind {
55 Some(ShellKind::PowerShell) | Some(ShellKind::Pwsh) => {
56 format!("{} -C '{}'", self.program, command_to_use_in_label)
57 }
58 #[cfg(windows)]
59 None => {
60 format!("{} -C '{}'", self.program, command_to_use_in_label)
61 }
62 Some(ShellKind::Cmd) => {
63 format!("{} /C \"{}\"", self.program, command_to_use_in_label)
64 }
65 Some(
66 ShellKind::Posix(_)
67 | ShellKind::Nushell
68 | ShellKind::Fish
69 | ShellKind::Csh
70 | ShellKind::Tcsh
71 | ShellKind::Rc
72 | ShellKind::Xonsh
73 | ShellKind::Elvish,
74 ) => {
75 let interactivity = self.interactive.then_some("-i ").unwrap_or_default();
76 format!(
77 "{PROGRAM} {interactivity}-c '{command_to_use_in_label}'",
78 PROGRAM = self.program
79 )
80 }
81 #[cfg(unix)]
82 None => {
83 let interactivity = self.interactive.then_some("-i ").unwrap_or_default();
84 format!(
85 "{PROGRAM} {interactivity}-c '{command_to_use_in_label}'",
86 PROGRAM = self.program
87 )
88 }
89 }
90 }
91 }
92
93 pub fn redirect_stdin_to_dev_null(mut self) -> Self {
94 self.redirect_stdin = true;
95 self
96 }
97
98 fn apply_stdin_redirect(&self, combined_command: &mut String) {
99 match self.kind {
100 Some(ShellKind::Fish) => {
101 combined_command.insert_str(0, "begin; ");
102 combined_command.push_str("; end </dev/null");
103 }
104 Some(
105 ShellKind::Posix(_)
106 | ShellKind::Nushell
107 | ShellKind::Csh
108 | ShellKind::Tcsh
109 | ShellKind::Rc
110 | ShellKind::Xonsh
111 | ShellKind::Elvish,
112 ) => {
113 combined_command.insert(0, POSIX_STDIN_REDIRECT_PREFIX);
114 combined_command.push_str(POSIX_STDIN_REDIRECT_SUFFIX);
115 }
116 #[cfg(unix)]
117 None => {
118 combined_command.insert(0, POSIX_STDIN_REDIRECT_PREFIX);
119 combined_command.push_str(POSIX_STDIN_REDIRECT_SUFFIX);
120 }
121 Some(ShellKind::PowerShell) | Some(ShellKind::Pwsh) => {
122 combined_command.insert_str(0, POWERSHELL_STDIN_REDIRECT_PREFIX);
123 combined_command.push_str(POWERSHELL_STDIN_REDIRECT_SUFFIX);
124 }
125 #[cfg(windows)]
126 None => {
127 combined_command.insert_str(0, POWERSHELL_STDIN_REDIRECT_PREFIX);
128 combined_command.push_str(POWERSHELL_STDIN_REDIRECT_SUFFIX);
129 }
130 Some(ShellKind::Cmd) => {
131 combined_command.push_str("< NUL");
132 }
133 }
134 }
135
136 /// Returns the program and arguments to run this task in a shell.
137 pub fn build(
138 mut self,
139 task_command: Option<String>,
140 task_args: &[String],
141 ) -> (String, Vec<String>) {
142 if let Some(task_command) = task_command {
143 let task_command = if !task_args.is_empty() {
144 match try_quote_prefix_aware_option(self.kind, &task_command) {
145 Some(task_command) => task_command.into_owned(),
146 None => task_command,
147 }
148 } else {
149 task_command
150 };
151 let mut combined_command = task_args.iter().fold(task_command, |mut command, arg| {
152 command.push(' ');
153 let shell_variable = to_shell_variable_option(self.kind, arg);
154 command.push_str(&match try_quote_option(self.kind, &shell_variable) {
155 Some(shell_variable) => shell_variable,
156 None => Cow::Owned(shell_variable),
157 });
158 command
159 });
160 if self.redirect_stdin {
161 self.apply_stdin_redirect(&mut combined_command);
162 }
163
164 self.args.extend(args_for_shell_option(
165 self.kind,
166 self.interactive,
167 combined_command,
168 ));
169 }
170
171 (self.program, self.args)
172 }
173
174 // This should not exist, but our task infra is broken beyond repair right now
175 #[doc(hidden)]
176 pub fn build_no_quote(
177 mut self,
178 task_command: Option<String>,
179 task_args: &[String],
180 ) -> (String, Vec<String>) {
181 if let Some(task_command) = task_command {
182 let mut combined_command = task_args.iter().fold(task_command, |mut command, arg| {
183 command.push(' ');
184 command.push_str(&to_shell_variable_option(self.kind, arg));
185 command
186 });
187 if self.redirect_stdin {
188 self.apply_stdin_redirect(&mut combined_command);
189 }
190
191 self.args.extend(args_for_shell_option(
192 self.kind,
193 self.interactive,
194 combined_command,
195 ));
196 }
197
198 (self.program, self.args)
199 }
200
201 /// Builds a `smol::process::Command` with the given task command and arguments.
202 ///
203 /// Prefer this over manually constructing a command with the output of `Self::build`,
204 /// as this method handles `cmd` weirdness on windows correctly.
205 pub fn build_smol_command(
206 self,
207 task_command: Option<String>,
208 task_args: &[String],
209 ) -> smol::process::Command {
210 smol::process::Command::from(self.build_std_command(task_command, task_args))
211 }
212
213 /// Builds a `std::process::Command` with the given task command and arguments.
214 ///
215 /// Prefer this over manually constructing a command with the output of `Self::build`,
216 /// as this method handles `cmd` weirdness on windows correctly.
217 pub fn build_std_command(
218 self,
219 mut task_command: Option<String>,
220 task_args: &[String],
221 ) -> std::process::Command {
222 #[cfg(windows)]
223 let kind = self.kind;
224 if task_args.is_empty() {
225 task_command = task_command
226 .as_ref()
227 .map(|cmd| try_quote_prefix_aware_option(self.kind, cmd).map(Cow::into_owned))
228 .unwrap_or(task_command);
229 }
230 let (program, args) = self.build(task_command, task_args);
231
232 let mut child = crate::command::new_std_command(program);
233
234 #[cfg(windows)]
235 if kind == Some(ShellKind::Cmd) {
236 use std::os::windows::process::CommandExt;
237
238 for arg in args {
239 child.raw_arg(arg);
240 }
241 } else {
242 child.args(args);
243 }
244
245 #[cfg(not(windows))]
246 child.args(args);
247
248 child
249 }
250
251 pub fn kind(&self) -> Option<ShellKind> {
252 self.kind
253 }
254}
255
256#[cfg(test)]
257mod test {
258 use super::*;
259
260 #[test]
261 fn test_nu_shell_variable_substitution() {
262 let shell = Shell::Program("nu".to_owned());
263 let shell_builder = ShellBuilder::new(&shell);
264
265 let (program, args) = shell_builder.build(
266 Some("echo".into()),
267 &[
268 "${hello}".to_string(),
269 "$world".to_string(),
270 "nothing".to_string(),
271 "--$something".to_string(),
272 "$".to_string(),
273 "${test".to_string(),
274 ],
275 );
276
277 assert_eq!(program, "nu");
278 assert_eq!(
279 args,
280 vec![
281 "-i",
282 "-c",
283 "echo '$env.hello' '$env.world' nothing '--($env.something)' '$' '${test'"
284 ]
285 );
286 }
287
288 #[test]
289 fn redirect_stdin_to_dev_null_precedence() {
290 let shell = Shell::Program("nu".to_owned());
291 let shell_builder = ShellBuilder::new(&shell);
292
293 let (program, args) = shell_builder
294 .redirect_stdin_to_dev_null()
295 .build(Some("echo".into()), &["nothing".to_string()]);
296
297 assert_eq!(program, "nu");
298 assert_eq!(args, vec!["-i", "-c", "(echo nothing) </dev/null"]);
299 }
300
301 #[test]
302 fn redirect_stdin_to_dev_null_fish() {
303 let shell = Shell::Program("fish".to_owned());
304 let shell_builder = ShellBuilder::new(&shell);
305
306 let (program, args) = shell_builder
307 .redirect_stdin_to_dev_null()
308 .build(Some("echo".into()), &["test".to_string()]);
309
310 assert_eq!(program, "fish");
311 assert_eq!(args, vec!["-i", "-c", "begin; echo test; end </dev/null"]);
312 }
313
314 #[test]
315 fn does_not_quote_sole_command_only() {
316 let shell = Shell::Program("fish".to_owned());
317 let shell_builder = ShellBuilder::new(&shell);
318
319 let (program, args) = shell_builder.build(Some("echo".into()), &[]);
320
321 assert_eq!(program, "fish");
322 assert_eq!(args, vec!["-i", "-c", "echo"]);
323
324 let shell = Shell::Program("fish".to_owned());
325 let shell_builder = ShellBuilder::new(&shell);
326
327 let (program, args) = shell_builder.build(Some("echo oo".into()), &[]);
328
329 assert_eq!(program, "fish");
330 assert_eq!(args, vec!["-i", "-c", "echo oo"]);
331 }
332}