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}