shell_builder.rs

  1use util::shell::get_system_shell;
  2
  3use crate::Shell;
  4
  5pub use util::shell::ShellKind;
  6
  7/// ShellBuilder is used to turn a user-requested task into a
  8/// program that can be executed by the shell.
  9pub struct ShellBuilder {
 10    /// The shell to run
 11    program: String,
 12    args: Vec<String>,
 13    interactive: bool,
 14    /// Whether to redirect stdin to /dev/null for the spawned command as a subshell.
 15    redirect_stdin: bool,
 16    kind: ShellKind,
 17}
 18
 19impl ShellBuilder {
 20    /// Create a new ShellBuilder as configured.
 21    pub fn new(shell: &Shell, is_windows: bool) -> Self {
 22        let (program, args) = match shell {
 23            Shell::System => (get_system_shell(), Vec::new()),
 24            Shell::Program(shell) => (shell.clone(), Vec::new()),
 25            Shell::WithArguments { program, args, .. } => (program.clone(), args.clone()),
 26        };
 27
 28        let kind = ShellKind::new(&program, is_windows);
 29        Self {
 30            program,
 31            args,
 32            interactive: true,
 33            kind,
 34            redirect_stdin: false,
 35        }
 36    }
 37    pub fn non_interactive(mut self) -> Self {
 38        self.interactive = false;
 39        self
 40    }
 41
 42    /// Returns the label to show in the terminal tab
 43    pub fn command_label(&self, command_to_use_in_label: &str) -> String {
 44        if command_to_use_in_label.trim().is_empty() {
 45            self.program.clone()
 46        } else {
 47            match self.kind {
 48                ShellKind::PowerShell => {
 49                    format!("{} -C '{}'", self.program, command_to_use_in_label)
 50                }
 51                ShellKind::Cmd => {
 52                    format!("{} /C \"{}\"", self.program, command_to_use_in_label)
 53                }
 54                ShellKind::Posix
 55                | ShellKind::Nushell
 56                | ShellKind::Fish
 57                | ShellKind::Csh
 58                | ShellKind::Tcsh
 59                | ShellKind::Rc
 60                | ShellKind::Xonsh => {
 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 mut combined_command = task_args.iter().fold(task_command, |mut command, arg| {
 84                command.push(' ');
 85                command.push_str(&self.kind.to_shell_variable(arg));
 86                command
 87            });
 88            if self.redirect_stdin {
 89                match self.kind {
 90                    ShellKind::Fish => {
 91                        combined_command.insert_str(0, "begin; ");
 92                        combined_command.push_str("; end </dev/null");
 93                    }
 94                    ShellKind::Posix
 95                    | ShellKind::Nushell
 96                    | ShellKind::Csh
 97                    | ShellKind::Tcsh
 98                    | ShellKind::Rc
 99                    | ShellKind::Xonsh => {
100                        combined_command.insert(0, '(');
101                        combined_command.push_str(") </dev/null");
102                    }
103                    ShellKind::PowerShell => {
104                        combined_command.insert_str(0, "$null | & {");
105                        combined_command.push_str("}");
106                    }
107                    ShellKind::Cmd => {
108                        combined_command.push_str("< NUL");
109                    }
110                }
111            }
112
113            self.args
114                .extend(self.kind.args_for_shell(self.interactive, combined_command));
115        }
116
117        (self.program, self.args)
118    }
119}
120
121#[cfg(test)]
122mod test {
123    use super::*;
124
125    #[test]
126    fn test_nu_shell_variable_substitution() {
127        let shell = Shell::Program("nu".to_owned());
128        let shell_builder = ShellBuilder::new(&shell, false);
129
130        let (program, args) = shell_builder.build(
131            Some("echo".into()),
132            &[
133                "${hello}".to_string(),
134                "$world".to_string(),
135                "nothing".to_string(),
136                "--$something".to_string(),
137                "$".to_string(),
138                "${test".to_string(),
139            ],
140        );
141
142        assert_eq!(program, "nu");
143        assert_eq!(
144            args,
145            vec![
146                "-i",
147                "-c",
148                "echo $env.hello $env.world nothing --($env.something) $ ${test"
149            ]
150        );
151    }
152
153    #[test]
154    fn redirect_stdin_to_dev_null_precedence() {
155        let shell = Shell::Program("nu".to_owned());
156        let shell_builder = ShellBuilder::new(&shell, false);
157
158        let (program, args) = shell_builder
159            .redirect_stdin_to_dev_null()
160            .build(Some("echo".into()), &["nothing".to_string()]);
161
162        assert_eq!(program, "nu");
163        assert_eq!(args, vec!["-i", "-c", "(echo nothing) </dev/null"]);
164    }
165
166    #[test]
167    fn redirect_stdin_to_dev_null_fish() {
168        let shell = Shell::Program("fish".to_owned());
169        let shell_builder = ShellBuilder::new(&shell);
170
171        let (program, args) = shell_builder
172            .redirect_stdin_to_dev_null()
173            .build(Some("echo".into()), &["test".to_string()]);
174
175        assert_eq!(program, "fish");
176        assert_eq!(args, vec!["-i", "-c", "begin; echo test; end </dev/null"]);
177    }
178}