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