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