shell_builder.rs

  1use crate::Shell;
  2
  3#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
  4enum ShellKind {
  5    #[default]
  6    Posix,
  7    Powershell,
  8    Cmd,
  9}
 10
 11impl ShellKind {
 12    fn new(program: &str) -> Self {
 13        if program == "powershell"
 14            || program.ends_with("powershell.exe")
 15            || program == "pwsh"
 16            || program.ends_with("pwsh.exe")
 17        {
 18            ShellKind::Powershell
 19        } else if program == "cmd" || program.ends_with("cmd.exe") {
 20            ShellKind::Cmd
 21        } else {
 22            // Someother shell detected, the user might install and use a
 23            // unix-like shell.
 24            ShellKind::Posix
 25        }
 26    }
 27
 28    fn to_shell_variable(&self, input: &str) -> String {
 29        match self {
 30            Self::Powershell => Self::to_powershell_variable(input),
 31            Self::Cmd => Self::to_cmd_variable(input),
 32            Self::Posix => input.to_owned(),
 33        }
 34    }
 35
 36    fn to_cmd_variable(input: &str) -> String {
 37        if let Some(var_str) = input.strip_prefix("${") {
 38            if var_str.find(':').is_none() {
 39                // If the input starts with "${", remove the trailing "}"
 40                format!("%{}%", &var_str[..var_str.len() - 1])
 41            } else {
 42                // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
 43                // which will result in the task failing to run in such cases.
 44                input.into()
 45            }
 46        } else if let Some(var_str) = input.strip_prefix('$') {
 47            // If the input starts with "$", directly append to "$env:"
 48            format!("%{}%", var_str)
 49        } else {
 50            // If no prefix is found, return the input as is
 51            input.into()
 52        }
 53    }
 54    fn to_powershell_variable(input: &str) -> String {
 55        if let Some(var_str) = input.strip_prefix("${") {
 56            if var_str.find(':').is_none() {
 57                // If the input starts with "${", remove the trailing "}"
 58                format!("$env:{}", &var_str[..var_str.len() - 1])
 59            } else {
 60                // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
 61                // which will result in the task failing to run in such cases.
 62                input.into()
 63            }
 64        } else if let Some(var_str) = input.strip_prefix('$') {
 65            // If the input starts with "$", directly append to "$env:"
 66            format!("$env:{}", var_str)
 67        } else {
 68            // If no prefix is found, return the input as is
 69            input.into()
 70        }
 71    }
 72
 73    fn args_for_shell(&self, interactive: bool, combined_command: String) -> Vec<String> {
 74        match self {
 75            ShellKind::Powershell => vec!["-C".to_owned(), combined_command],
 76            ShellKind::Cmd => vec!["/C".to_owned(), combined_command],
 77            ShellKind::Posix => interactive
 78                .then(|| "-i".to_owned())
 79                .into_iter()
 80                .chain(["-c".to_owned(), combined_command])
 81                .collect(),
 82        }
 83    }
 84}
 85
 86fn system_shell() -> String {
 87    if cfg!(target_os = "windows") {
 88        // `alacritty_terminal` uses this as default on Windows. See:
 89        // https://github.com/alacritty/alacritty/blob/0d4ab7bca43213d96ddfe40048fc0f922543c6f8/alacritty_terminal/src/tty/windows/mod.rs#L130
 90        // We could use `util::get_windows_system_shell()` here, but we are running tasks here, so leave it to `powershell.exe`
 91        // should be okay.
 92        "powershell.exe".to_string()
 93    } else {
 94        std::env::var("SHELL").unwrap_or("/bin/sh".to_string())
 95    }
 96}
 97
 98/// ShellBuilder is used to turn a user-requested task into a
 99/// program that can be executed by the shell.
100pub struct ShellBuilder {
101    /// The shell to run
102    program: String,
103    args: Vec<String>,
104    interactive: bool,
105    kind: ShellKind,
106}
107
108pub static DEFAULT_REMOTE_SHELL: &str = "\"${SHELL:-sh}\"";
109
110impl ShellBuilder {
111    /// Create a new ShellBuilder as configured.
112    pub fn new(is_local: bool, shell: &Shell) -> Self {
113        let (program, args) = match shell {
114            Shell::System => {
115                if is_local {
116                    (system_shell(), Vec::new())
117                } else {
118                    (DEFAULT_REMOTE_SHELL.to_string(), Vec::new())
119                }
120            }
121            Shell::Program(shell) => (shell.clone(), Vec::new()),
122            Shell::WithArguments { program, args, .. } => (program.clone(), args.clone()),
123        };
124        let kind = ShellKind::new(&program);
125        Self {
126            program,
127            args,
128            interactive: true,
129            kind,
130        }
131    }
132    pub fn non_interactive(mut self) -> Self {
133        self.interactive = false;
134        self
135    }
136    /// Returns the label to show in the terminal tab
137    pub fn command_label(&self, command_label: &str) -> String {
138        match self.kind {
139            ShellKind::Powershell => {
140                format!("{} -C '{}'", self.program, command_label)
141            }
142            ShellKind::Cmd => {
143                format!("{} /C '{}'", self.program, command_label)
144            }
145            ShellKind::Posix => {
146                let interactivity = self.interactive.then_some("-i ").unwrap_or_default();
147                format!("{} {interactivity}-c '{}'", self.program, command_label)
148            }
149        }
150    }
151    /// Returns the program and arguments to run this task in a shell.
152    pub fn build(
153        mut self,
154        task_command: Option<String>,
155        task_args: &Vec<String>,
156    ) -> (String, Vec<String>) {
157        if let Some(task_command) = task_command {
158            let combined_command = task_args
159                .into_iter()
160                .fold(task_command, |mut command, arg| {
161                    command.push(' ');
162                    command.push_str(&self.kind.to_shell_variable(arg));
163                    command
164                });
165
166            self.args
167                .extend(self.kind.args_for_shell(self.interactive, combined_command));
168        }
169
170        (self.program, self.args)
171    }
172}