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(mut self, task_command: String, task_args: &Vec<String>) -> (String, Vec<String>) {
153 let combined_command = task_args
154 .into_iter()
155 .fold(task_command, |mut command, arg| {
156 command.push(' ');
157 command.push_str(&self.kind.to_shell_variable(arg));
158 command
159 });
160
161 self.args
162 .extend(self.kind.args_for_shell(self.interactive, combined_command));
163
164 (self.program, self.args)
165 }
166}