shell_builder.rs

  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}