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(remote_system_shell: Option<&str>, shell: &Shell) -> Self {
 22        let (program, args) = match remote_system_shell {
 23            Some(program) => (program.to_string(), Vec::new()),
 24            None => match shell {
 25                Shell::System => (get_system_shell(), Vec::new()),
 26                Shell::Program(shell) => (shell.clone(), Vec::new()),
 27                Shell::WithArguments { program, args, .. } => (program.clone(), args.clone()),
 28            },
 29        };
 30
 31        let kind = ShellKind::new(&program);
 32        Self {
 33            program,
 34            args,
 35            interactive: true,
 36            kind,
 37            redirect_stdin: false,
 38        }
 39    }
 40    pub fn non_interactive(mut self) -> Self {
 41        self.interactive = false;
 42        self
 43    }
 44
 45    /// Returns the label to show in the terminal tab
 46    pub fn command_label(&self, command_to_use_in_label: &str) -> String {
 47        if command_to_use_in_label.trim().is_empty() {
 48            self.program.clone()
 49        } else {
 50            match self.kind {
 51                ShellKind::PowerShell => {
 52                    format!("{} -C '{}'", self.program, command_to_use_in_label)
 53                }
 54                ShellKind::Cmd => {
 55                    format!("{} /C '{}'", self.program, command_to_use_in_label)
 56                }
 57                ShellKind::Posix
 58                | ShellKind::Nushell
 59                | ShellKind::Fish
 60                | ShellKind::Csh
 61                | ShellKind::Tcsh
 62                | ShellKind::Rc => {
 63                    let interactivity = self.interactive.then_some("-i ").unwrap_or_default();
 64                    format!(
 65                        "{PROGRAM} {interactivity}-c '{command_to_use_in_label}'",
 66                        PROGRAM = self.program
 67                    )
 68                }
 69            }
 70        }
 71    }
 72
 73    pub fn redirect_stdin_to_dev_null(mut self) -> Self {
 74        self.redirect_stdin = true;
 75        self
 76    }
 77
 78    /// Returns the program and arguments to run this task in a shell.
 79    pub fn build(
 80        mut self,
 81        task_command: Option<String>,
 82        task_args: &[String],
 83    ) -> (String, Vec<String>) {
 84        if let Some(task_command) = task_command {
 85            let mut combined_command = task_args.iter().fold(task_command, |mut command, arg| {
 86                command.push(' ');
 87                command.push_str(&self.kind.to_shell_variable(arg));
 88                command
 89            });
 90            if self.redirect_stdin {
 91                match self.kind {
 92                    ShellKind::Posix
 93                    | ShellKind::Nushell
 94                    | ShellKind::Fish
 95                    | ShellKind::Csh
 96                    | ShellKind::Tcsh
 97                    | ShellKind::Rc => {
 98                        combined_command.insert(0, '(');
 99                        combined_command.push_str(") </dev/null");
100                    }
101                    ShellKind::PowerShell => {
102                        combined_command.insert_str(0, "$null | & {");
103                        combined_command.push_str("}");
104                    }
105                    ShellKind::Cmd => {
106                        combined_command.push_str("< NUL");
107                    }
108                }
109            }
110
111            self.args
112                .extend(self.kind.args_for_shell(self.interactive, combined_command));
113        }
114
115        (self.program, self.args)
116    }
117}
118
119#[cfg(test)]
120mod test {
121    use super::*;
122
123    #[test]
124    fn test_nu_shell_variable_substitution() {
125        let shell = Shell::Program("nu".to_owned());
126        let shell_builder = ShellBuilder::new(None, &shell);
127
128        let (program, args) = shell_builder.build(
129            Some("echo".into()),
130            &[
131                "${hello}".to_string(),
132                "$world".to_string(),
133                "nothing".to_string(),
134                "--$something".to_string(),
135                "$".to_string(),
136                "${test".to_string(),
137            ],
138        );
139
140        assert_eq!(program, "nu");
141        assert_eq!(
142            args,
143            vec![
144                "-i",
145                "-c",
146                "echo $env.hello $env.world nothing --($env.something) $ ${test"
147            ]
148        );
149    }
150
151    #[test]
152    fn redirect_stdin_to_dev_null_precedence() {
153        let shell = Shell::Program("nu".to_owned());
154        let shell_builder = ShellBuilder::new(None, &shell);
155
156        let (program, args) = shell_builder
157            .redirect_stdin_to_dev_null()
158            .build(Some("echo".into()), &["nothing".to_string()]);
159
160        assert_eq!(program, "nu");
161        assert_eq!(args, vec!["-i", "-c", "(echo nothing) </dev/null"]);
162    }
163}