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 = self.kind.prepend_command_prefix(&task_command);
 84            let task_command = if !task_args.is_empty() {
 85                match self.kind.try_quote_prefix_aware(&task_command) {
 86                    Some(task_command) => task_command,
 87                    None => task_command,
 88                }
 89            } else {
 90                task_command
 91            };
 92            let mut combined_command =
 93                task_args
 94                    .iter()
 95                    .fold(task_command.into_owned(), |mut command, arg| {
 96                        command.push(' ');
 97                        let shell_variable = self.kind.to_shell_variable(arg);
 98                        command.push_str(&match self.kind.try_quote(&shell_variable) {
 99                            Some(shell_variable) => shell_variable,
100                            None => Cow::Owned(shell_variable),
101                        });
102                        command
103                    });
104            if self.redirect_stdin {
105                match self.kind {
106                    ShellKind::Fish => {
107                        combined_command.insert_str(0, "begin; ");
108                        combined_command.push_str("; end </dev/null");
109                    }
110                    ShellKind::Posix
111                    | ShellKind::Nushell
112                    | ShellKind::Csh
113                    | ShellKind::Tcsh
114                    | ShellKind::Rc
115                    | ShellKind::Xonsh
116                    | ShellKind::Elvish => {
117                        combined_command.insert(0, '(');
118                        combined_command.push_str(") </dev/null");
119                    }
120                    ShellKind::PowerShell | ShellKind::Pwsh => {
121                        combined_command.insert_str(0, "$null | & {");
122                        combined_command.push_str("}");
123                    }
124                    ShellKind::Cmd => {
125                        combined_command.push_str("< NUL");
126                    }
127                }
128            }
129
130            self.args
131                .extend(self.kind.args_for_shell(self.interactive, combined_command));
132        }
133
134        (self.program, self.args)
135    }
136
137    pub fn kind(&self) -> ShellKind {
138        self.kind
139    }
140}
141
142#[cfg(test)]
143mod test {
144    use super::*;
145
146    #[test]
147    fn test_nu_shell_variable_substitution() {
148        let shell = Shell::Program("nu".to_owned());
149        let shell_builder = ShellBuilder::new(&shell, false);
150
151        let (program, args) = shell_builder.build(
152            Some("echo".into()),
153            &[
154                "${hello}".to_string(),
155                "$world".to_string(),
156                "nothing".to_string(),
157                "--$something".to_string(),
158                "$".to_string(),
159                "${test".to_string(),
160            ],
161        );
162
163        assert_eq!(program, "nu");
164        assert_eq!(
165            args,
166            vec![
167                "-i",
168                "-c",
169                "^echo '$env.hello' '$env.world' nothing '--($env.something)' '$' '${test'"
170            ]
171        );
172    }
173
174    #[test]
175    fn redirect_stdin_to_dev_null_precedence() {
176        let shell = Shell::Program("nu".to_owned());
177        let shell_builder = ShellBuilder::new(&shell, false);
178
179        let (program, args) = shell_builder
180            .redirect_stdin_to_dev_null()
181            .build(Some("echo".into()), &["nothing".to_string()]);
182
183        assert_eq!(program, "nu");
184        assert_eq!(args, vec!["-i", "-c", "(^echo nothing) </dev/null"]);
185    }
186
187    #[test]
188    fn redirect_stdin_to_dev_null_fish() {
189        let shell = Shell::Program("fish".to_owned());
190        let shell_builder = ShellBuilder::new(&shell, false);
191
192        let (program, args) = shell_builder
193            .redirect_stdin_to_dev_null()
194            .build(Some("echo".into()), &["test".to_string()]);
195
196        assert_eq!(program, "fish");
197        assert_eq!(args, vec!["-i", "-c", "begin; echo test; end </dev/null"]);
198    }
199}