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 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}