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