shell_builder.rs

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