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 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_command(
184 self,
185 mut task_command: Option<String>,
186 task_args: &[String],
187 ) -> smol::process::Command {
188 #[cfg(windows)]
189 let kind = self.kind;
190 if task_args.is_empty() {
191 task_command = task_command
192 .as_ref()
193 .map(|cmd| self.kind.try_quote_prefix_aware(&cmd).map(Cow::into_owned))
194 .unwrap_or(task_command);
195 }
196 let (program, args) = self.build(task_command, task_args);
197
198 let mut child = crate::command::new_smol_command(program);
199
200 #[cfg(windows)]
201 if kind == ShellKind::Cmd {
202 use smol::process::windows::CommandExt;
203
204 for arg in args {
205 child.raw_arg(arg);
206 }
207 } else {
208 child.args(args);
209 }
210
211 #[cfg(not(windows))]
212 child.args(args);
213
214 child
215 }
216
217 pub fn kind(&self) -> ShellKind {
218 self.kind
219 }
220}
221
222#[cfg(test)]
223mod test {
224 use super::*;
225
226 #[test]
227 fn test_nu_shell_variable_substitution() {
228 let shell = Shell::Program("nu".to_owned());
229 let shell_builder = ShellBuilder::new(&shell, false);
230
231 let (program, args) = shell_builder.build(
232 Some("echo".into()),
233 &[
234 "${hello}".to_string(),
235 "$world".to_string(),
236 "nothing".to_string(),
237 "--$something".to_string(),
238 "$".to_string(),
239 "${test".to_string(),
240 ],
241 );
242
243 assert_eq!(program, "nu");
244 assert_eq!(
245 args,
246 vec![
247 "-i",
248 "-c",
249 "echo '$env.hello' '$env.world' nothing '--($env.something)' '$' '${test'"
250 ]
251 );
252 }
253
254 #[test]
255 fn redirect_stdin_to_dev_null_precedence() {
256 let shell = Shell::Program("nu".to_owned());
257 let shell_builder = ShellBuilder::new(&shell, false);
258
259 let (program, args) = shell_builder
260 .redirect_stdin_to_dev_null()
261 .build(Some("echo".into()), &["nothing".to_string()]);
262
263 assert_eq!(program, "nu");
264 assert_eq!(args, vec!["-i", "-c", "(echo nothing) </dev/null"]);
265 }
266
267 #[test]
268 fn redirect_stdin_to_dev_null_fish() {
269 let shell = Shell::Program("fish".to_owned());
270 let shell_builder = ShellBuilder::new(&shell, false);
271
272 let (program, args) = shell_builder
273 .redirect_stdin_to_dev_null()
274 .build(Some("echo".into()), &["test".to_string()]);
275
276 assert_eq!(program, "fish");
277 assert_eq!(args, vec!["-i", "-c", "begin; echo test; end </dev/null"]);
278 }
279
280 #[test]
281 fn does_not_quote_sole_command_only() {
282 let shell = Shell::Program("fish".to_owned());
283 let shell_builder = ShellBuilder::new(&shell, false);
284
285 let (program, args) = shell_builder.build(Some("echo".into()), &[]);
286
287 assert_eq!(program, "fish");
288 assert_eq!(args, vec!["-i", "-c", "echo"]);
289
290 let shell = Shell::Program("fish".to_owned());
291 let shell_builder = ShellBuilder::new(&shell, false);
292
293 let (program, args) = shell_builder.build(Some("echo oo".into()), &[]);
294
295 assert_eq!(program, "fish");
296 assert_eq!(args, vec!["-i", "-c", "echo oo"]);
297 }
298}