1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use std::{borrow::Cow, fmt, path::Path};
4#[cfg(windows)]
5use std::{path::PathBuf, sync::LazyLock};
6
7#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
9#[serde(rename_all = "snake_case")]
10pub enum Shell {
11 #[default]
13 System,
14 Program(String),
16 WithArguments {
18 program: String,
20 args: Vec<String>,
22 title_override: Option<String>,
24 },
25}
26
27impl Shell {
28 pub fn program(&self) -> String {
29 match self {
30 Shell::Program(program) => program.clone(),
31 Shell::WithArguments { program, .. } => program.clone(),
32 Shell::System => get_system_shell(),
33 }
34 }
35
36 pub fn program_and_args(&self) -> (String, &[String]) {
37 match self {
38 Shell::Program(program) => (program.clone(), &[]),
39 Shell::WithArguments { program, args, .. } => (program.clone(), args),
40 Shell::System => (get_system_shell(), &[]),
41 }
42 }
43
44 pub fn shell_kind(&self, is_windows: bool) -> ShellKind {
45 match self {
46 Shell::Program(program) => ShellKind::new(program, is_windows),
47 Shell::WithArguments { program, .. } => ShellKind::new(program, is_windows),
48 Shell::System => ShellKind::system(),
49 }
50 }
51}
52
53#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
54pub enum ShellKind {
55 #[default]
56 Posix,
57 Csh,
58 Tcsh,
59 Rc,
60 Fish,
61 PowerShell,
63 Pwsh,
65 Nushell,
66 Cmd,
67 Xonsh,
68 Elvish,
69}
70
71pub fn get_system_shell() -> String {
72 if cfg!(windows) {
73 get_windows_system_shell()
74 } else {
75 std::env::var("SHELL").unwrap_or("/bin/sh".to_string())
76 }
77}
78
79pub fn get_default_system_shell() -> String {
80 if cfg!(windows) {
81 get_windows_system_shell()
82 } else {
83 "/bin/sh".to_string()
84 }
85}
86
87pub fn get_default_system_shell_preferring_bash() -> String {
89 #[cfg(windows)]
90 {
91 get_windows_bash().unwrap_or_else(|| get_windows_system_shell())
92 }
93
94 #[cfg(not(windows))]
95 {
96 "/bin/sh".to_string()
97 }
98}
99
100#[cfg(windows)]
101pub fn get_windows_bash() -> Option<String> {
102 fn find_bash_in_installation(install_root: &Path) -> Option<PathBuf> {
103 if !install_root.join("git-bash.exe").is_file() {
104 return None;
105 }
106 let bash = install_root.join("bin").join("bash.exe");
107 bash.is_file().then_some(bash)
108 }
109
110 fn find_bash_in_git() -> Option<PathBuf> {
111 if let Some(bash) = std::env::var_os("GIT_INSTALL_ROOT")
112 .map(PathBuf::from)
113 .and_then(|path| find_bash_in_installation(&path))
114 {
115 return Some(bash);
116 }
117 let git = which::which("git").ok()?;
118 let binary_directory = git.parent()?;
119 let parent = binary_directory.parent()?;
120 find_bash_in_installation(parent).or_else(|| find_bash_in_installation(parent.parent()?))
121 }
122
123 static BASH: LazyLock<Option<String>> = LazyLock::new(|| {
124 let bash = find_bash_in_git().map(|p| p.to_string_lossy().into_owned());
125 if let Some(ref path) = bash {
126 log::info!("Found bash at {}", path);
127 }
128 bash
129 });
130
131 (*BASH).clone()
132}
133
134pub fn get_windows_system_shell() -> String {
135 #[cfg(windows)]
136 return gpui_util::get_windows_system_shell();
137
138 #[cfg(not(windows))]
139 return "cmd.exe".to_string();
140}
141
142impl fmt::Display for ShellKind {
143 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144 match self {
145 ShellKind::Posix => write!(f, "sh"),
146 ShellKind::Csh => write!(f, "csh"),
147 ShellKind::Tcsh => write!(f, "tcsh"),
148 ShellKind::Fish => write!(f, "fish"),
149 ShellKind::PowerShell => write!(f, "powershell"),
150 ShellKind::Pwsh => write!(f, "pwsh"),
151 ShellKind::Nushell => write!(f, "nu"),
152 ShellKind::Cmd => write!(f, "cmd"),
153 ShellKind::Rc => write!(f, "rc"),
154 ShellKind::Xonsh => write!(f, "xonsh"),
155 ShellKind::Elvish => write!(f, "elvish"),
156 }
157 }
158}
159
160impl ShellKind {
161 pub fn system() -> Self {
162 Self::new(&get_system_shell(), cfg!(windows))
163 }
164
165 pub fn supports_posix_chaining(&self) -> bool {
189 matches!(
190 self,
191 ShellKind::Posix
192 | ShellKind::Fish
193 | ShellKind::PowerShell
194 | ShellKind::Pwsh
195 | ShellKind::Cmd
196 | ShellKind::Xonsh
197 | ShellKind::Csh
198 | ShellKind::Tcsh
199 | ShellKind::Nushell
200 | ShellKind::Elvish
201 | ShellKind::Rc
202 )
203 }
204
205 pub fn new(program: impl AsRef<Path>, is_windows: bool) -> Self {
206 let program = program.as_ref();
207 let program = program
208 .file_stem()
209 .unwrap_or_else(|| program.as_os_str())
210 .to_string_lossy();
211
212 match &*program {
213 "powershell" => ShellKind::PowerShell,
214 "pwsh" => ShellKind::Pwsh,
215 "cmd" => ShellKind::Cmd,
216 "nu" => ShellKind::Nushell,
217 "fish" => ShellKind::Fish,
218 "csh" => ShellKind::Csh,
219 "tcsh" => ShellKind::Tcsh,
220 "rc" => ShellKind::Rc,
221 "xonsh" => ShellKind::Xonsh,
222 "elvish" => ShellKind::Elvish,
223 "sh" | "bash" | "zsh" => ShellKind::Posix,
224 _ if is_windows => ShellKind::PowerShell,
225 _ => ShellKind::Posix,
228 }
229 }
230
231 pub fn to_shell_variable(self, input: &str) -> String {
232 match self {
233 Self::PowerShell | Self::Pwsh => Self::to_powershell_variable(input),
234 Self::Cmd => Self::to_cmd_variable(input),
235 Self::Posix => input.to_owned(),
236 Self::Fish => input.to_owned(),
237 Self::Csh => input.to_owned(),
238 Self::Tcsh => input.to_owned(),
239 Self::Rc => input.to_owned(),
240 Self::Nushell => Self::to_nushell_variable(input),
241 Self::Xonsh => input.to_owned(),
242 Self::Elvish => input.to_owned(),
243 }
244 }
245
246 fn to_cmd_variable(input: &str) -> String {
247 if let Some(var_str) = input.strip_prefix("${") {
248 match var_str.strip_suffix('}') {
249 Some(var_name) if !var_name.is_empty() && !var_name.contains(':') => {
250 format!("%{var_name}%")
251 }
252 _ => input.into(),
255 }
256 } else if let Some(var_str) = input.strip_prefix('$') {
257 format!("%{}%", var_str)
259 } else {
260 input.into()
262 }
263 }
264
265 fn to_powershell_variable(input: &str) -> String {
266 if let Some(var_str) = input.strip_prefix("${") {
267 match var_str.strip_suffix('}') {
268 Some(var_name) if !var_name.is_empty() && !var_name.contains(':') => {
269 format!("$env:{var_name}")
270 }
271 _ => input.into(),
274 }
275 } else if let Some(var_str) = input.strip_prefix('$') {
276 format!("$env:{}", var_str)
278 } else {
279 input.into()
281 }
282 }
283
284 fn to_nushell_variable(input: &str) -> String {
285 let mut result = String::new();
286 let mut source = input;
287 let mut is_start = true;
288
289 loop {
290 match source.chars().next() {
291 None => return result,
292 Some('$') => {
293 source = Self::parse_nushell_var(&source[1..], &mut result, is_start);
294 is_start = false;
295 }
296 Some(_) => {
297 is_start = false;
298 let chunk_end = source.find('$').unwrap_or(source.len());
299 let (chunk, rest) = source.split_at(chunk_end);
300 result.push_str(chunk);
301 source = rest;
302 }
303 }
304 }
305 }
306
307 fn parse_nushell_var<'a>(source: &'a str, text: &mut String, is_start: bool) -> &'a str {
308 if source.starts_with("env.") {
309 text.push('$');
310 return source;
311 }
312
313 match source.chars().next() {
314 Some('{') => {
315 let source = &source[1..];
316 if let Some(end) = source.find('}') {
317 let var_name = &source[..end];
318 if !var_name.is_empty() {
319 if !is_start {
320 text.push_str("(");
321 }
322 text.push_str("$env.");
323 text.push_str(var_name);
324 if !is_start {
325 text.push_str(")");
326 }
327 &source[end + 1..]
328 } else {
329 text.push_str("${}");
330 &source[end + 1..]
331 }
332 } else {
333 text.push_str("${");
334 source
335 }
336 }
337 Some(c) if c.is_alphabetic() || c == '_' => {
338 let end = source
339 .find(|c: char| !c.is_alphanumeric() && c != '_')
340 .unwrap_or(source.len());
341 let var_name = &source[..end];
342 if !is_start {
343 text.push_str("(");
344 }
345 text.push_str("$env.");
346 text.push_str(var_name);
347 if !is_start {
348 text.push_str(")");
349 }
350 &source[end..]
351 }
352 _ => {
353 text.push('$');
354 source
355 }
356 }
357 }
358
359 pub fn args_for_shell(&self, interactive: bool, combined_command: String) -> Vec<String> {
360 match self {
361 ShellKind::PowerShell | ShellKind::Pwsh => vec!["-C".to_owned(), combined_command],
362 ShellKind::Cmd => vec![
363 "/S".to_owned(),
364 "/C".to_owned(),
365 format!("\"{combined_command}\""),
366 ],
367 ShellKind::Posix
368 | ShellKind::Nushell
369 | ShellKind::Fish
370 | ShellKind::Csh
371 | ShellKind::Tcsh
372 | ShellKind::Rc
373 | ShellKind::Xonsh
374 | ShellKind::Elvish => interactive
375 .then(|| "-i".to_owned())
376 .into_iter()
377 .chain(["-c".to_owned(), combined_command])
378 .collect(),
379 }
380 }
381
382 pub const fn command_prefix(&self) -> Option<char> {
383 match self {
384 ShellKind::PowerShell | ShellKind::Pwsh => Some('&'),
385 ShellKind::Nushell => Some('^'),
386 ShellKind::Posix
387 | ShellKind::Csh
388 | ShellKind::Tcsh
389 | ShellKind::Rc
390 | ShellKind::Fish
391 | ShellKind::Cmd
392 | ShellKind::Xonsh
393 | ShellKind::Elvish => None,
394 }
395 }
396
397 pub fn prepend_command_prefix<'a>(&self, command: &'a str) -> Cow<'a, str> {
398 match self.command_prefix() {
399 Some(prefix) if !command.starts_with(prefix) => {
400 Cow::Owned(format!("{prefix}{command}"))
401 }
402 _ => Cow::Borrowed(command),
403 }
404 }
405
406 pub const fn sequential_commands_separator(&self) -> char {
407 match self {
408 ShellKind::Cmd => '&',
409 ShellKind::Posix
410 | ShellKind::Csh
411 | ShellKind::Tcsh
412 | ShellKind::Rc
413 | ShellKind::Fish
414 | ShellKind::PowerShell
415 | ShellKind::Pwsh
416 | ShellKind::Nushell
417 | ShellKind::Xonsh
418 | ShellKind::Elvish => ';',
419 }
420 }
421
422 pub const fn sequential_and_commands_separator(&self) -> &'static str {
423 match self {
424 ShellKind::Cmd
425 | ShellKind::Posix
426 | ShellKind::Csh
427 | ShellKind::Tcsh
428 | ShellKind::Rc
429 | ShellKind::Fish
430 | ShellKind::Pwsh
431 | ShellKind::Xonsh => "&&",
432 ShellKind::PowerShell | ShellKind::Nushell | ShellKind::Elvish => ";",
433 }
434 }
435
436 pub fn try_quote<'a>(&self, arg: &'a str) -> Option<Cow<'a, str>> {
437 match self {
438 ShellKind::PowerShell => Some(Self::quote_powershell(arg)),
439 ShellKind::Pwsh => Some(Self::quote_pwsh(arg)),
440 ShellKind::Cmd => Some(Self::quote_cmd(arg)),
441 ShellKind::Posix
442 | ShellKind::Csh
443 | ShellKind::Tcsh
444 | ShellKind::Rc
445 | ShellKind::Fish
446 | ShellKind::Nushell
447 | ShellKind::Xonsh
448 | ShellKind::Elvish => shlex::try_quote(arg).ok(),
449 }
450 }
451
452 fn quote_windows(arg: &str, enclose: bool) -> Cow<'_, str> {
453 if arg.is_empty() {
454 return Cow::Borrowed("\"\"");
455 }
456
457 let needs_quoting = arg.chars().any(|c| c == ' ' || c == '\t' || c == '"');
458 if !needs_quoting {
459 return Cow::Borrowed(arg);
460 }
461
462 let mut result = String::with_capacity(arg.len() + 2);
463
464 if enclose {
465 result.push('"');
466 }
467
468 let chars: Vec<char> = arg.chars().collect();
469 let mut i = 0;
470
471 while i < chars.len() {
472 if chars[i] == '\\' {
473 let mut num_backslashes = 0;
474 while i < chars.len() && chars[i] == '\\' {
475 num_backslashes += 1;
476 i += 1;
477 }
478
479 if i < chars.len() && chars[i] == '"' {
480 for _ in 0..(num_backslashes * 2 + 1) {
482 result.push('\\');
483 }
484 result.push('"');
485 i += 1;
486 } else if i >= chars.len() {
487 for _ in 0..(num_backslashes * 2) {
489 result.push('\\');
490 }
491 } else {
492 for _ in 0..num_backslashes {
494 result.push('\\');
495 }
496 }
497 } else if chars[i] == '"' {
498 result.push('\\');
500 result.push('"');
501 i += 1;
502 } else {
503 result.push(chars[i]);
504 i += 1;
505 }
506 }
507
508 if enclose {
509 result.push('"');
510 }
511 Cow::Owned(result)
512 }
513
514 fn needs_quoting_powershell(s: &str) -> bool {
515 s.is_empty()
516 || s.chars().any(|c| {
517 c.is_whitespace()
518 || matches!(
519 c,
520 '"' | '`'
521 | '$'
522 | '&'
523 | '|'
524 | '<'
525 | '>'
526 | ';'
527 | '('
528 | ')'
529 | '['
530 | ']'
531 | '{'
532 | '}'
533 | ','
534 | '\''
535 | '@'
536 )
537 })
538 }
539
540 fn need_quotes_powershell(arg: &str) -> bool {
541 let mut quote_count = 0;
542 for c in arg.chars() {
543 if c == '"' {
544 quote_count += 1;
545 } else if c.is_whitespace() && (quote_count % 2 == 0) {
546 return true;
547 }
548 }
549 false
550 }
551
552 fn escape_powershell_quotes(s: &str) -> String {
553 let mut result = String::with_capacity(s.len() + 4);
554 result.push('\'');
555 for c in s.chars() {
556 if c == '\'' {
557 result.push('\'');
558 }
559 result.push(c);
560 }
561 result.push('\'');
562 result
563 }
564
565 pub fn quote_powershell(arg: &str) -> Cow<'_, str> {
566 let ps_will_quote = Self::need_quotes_powershell(arg);
567 let crt_quoted = Self::quote_windows(arg, !ps_will_quote);
568
569 if !Self::needs_quoting_powershell(arg) {
570 return crt_quoted;
571 }
572
573 Cow::Owned(Self::escape_powershell_quotes(&crt_quoted))
574 }
575
576 pub fn quote_pwsh(arg: &str) -> Cow<'_, str> {
577 if arg.is_empty() {
578 return Cow::Borrowed("''");
579 }
580
581 if !Self::needs_quoting_powershell(arg) {
582 return Cow::Borrowed(arg);
583 }
584
585 Cow::Owned(Self::escape_powershell_quotes(arg))
586 }
587
588 pub fn quote_cmd(arg: &str) -> Cow<'_, str> {
589 let crt_quoted = Self::quote_windows(arg, true);
590
591 let needs_cmd_escaping = crt_quoted.contains(['"', '%', '^', '<', '>', '&', '|', '(', ')']);
592
593 if !needs_cmd_escaping {
594 return crt_quoted;
595 }
596
597 let mut result = String::with_capacity(crt_quoted.len() * 2);
598 for c in crt_quoted.chars() {
599 match c {
600 '^' | '"' | '<' | '>' | '&' | '|' | '(' | ')' => {
601 result.push('^');
602 result.push(c);
603 }
604 '%' => {
605 result.push_str("%%cd:~,%");
606 }
607 _ => result.push(c),
608 }
609 }
610 Cow::Owned(result)
611 }
612
613 pub fn try_quote_prefix_aware<'a>(&self, arg: &'a str) -> Option<Cow<'a, str>> {
618 if let Some(char) = self.command_prefix() {
619 if let Some(arg) = arg.strip_prefix(char) {
620 for quote in ['\'', '"'] {
622 if let Some(arg) = arg
623 .strip_prefix(quote)
624 .and_then(|arg| arg.strip_suffix(quote))
625 {
626 let quoted = self.try_quote(arg)?;
631 return Some(if quoted.starts_with(['\'', '"']) {
632 Cow::Owned(self.prepend_command_prefix("ed).into_owned())
633 } else {
634 Cow::Owned(
635 self.prepend_command_prefix(&format!("{quote}{quoted}{quote}"))
636 .into_owned(),
637 )
638 });
639 }
640 }
641 return self
642 .try_quote(arg)
643 .map(|quoted| Cow::Owned(self.prepend_command_prefix("ed).into_owned()));
644 }
645 }
646 self.try_quote(arg).map(|quoted| match quoted {
647 unquoted @ Cow::Borrowed(_) => unquoted,
648 Cow::Owned(quoted) => Cow::Owned(self.prepend_command_prefix("ed).into_owned()),
649 })
650 }
651
652 pub fn split(&self, input: &str) -> Option<Vec<String>> {
653 shlex::split(input)
654 }
655
656 pub const fn activate_keyword(&self) -> &'static str {
657 match self {
658 ShellKind::Cmd => "",
659 ShellKind::Nushell => "overlay use",
660 ShellKind::PowerShell | ShellKind::Pwsh => ".",
661 ShellKind::Fish
662 | ShellKind::Csh
663 | ShellKind::Tcsh
664 | ShellKind::Posix
665 | ShellKind::Rc
666 | ShellKind::Xonsh
667 | ShellKind::Elvish => "source",
668 }
669 }
670
671 pub const fn clear_screen_command(&self) -> &'static str {
672 match self {
673 ShellKind::Cmd => "cls",
674 ShellKind::Posix
675 | ShellKind::Csh
676 | ShellKind::Tcsh
677 | ShellKind::Rc
678 | ShellKind::Fish
679 | ShellKind::PowerShell
680 | ShellKind::Pwsh
681 | ShellKind::Nushell
682 | ShellKind::Xonsh
683 | ShellKind::Elvish => "clear",
684 }
685 }
686
687 #[cfg(windows)]
688 pub const fn tty_escape_args(&self) -> bool {
691 match self {
692 ShellKind::Cmd => false,
693 ShellKind::Posix
694 | ShellKind::Csh
695 | ShellKind::Tcsh
696 | ShellKind::Rc
697 | ShellKind::Fish
698 | ShellKind::PowerShell
699 | ShellKind::Pwsh
700 | ShellKind::Nushell
701 | ShellKind::Xonsh
702 | ShellKind::Elvish => true,
703 }
704 }
705}
706
707#[cfg(test)]
708mod tests {
709 use super::*;
710
711 #[test]
722 fn test_try_quote_powershell() {
723 let shell_kind = ShellKind::PowerShell;
724 assert_eq!(
725 shell_kind
726 .try_quote("C:\\Users\\johndoe\\dev\\python\\39007\\tests\\.venv\\Scripts\\python.exe -m pytest \"test_foo.py::test_foo\"")
727 .unwrap()
728 .into_owned(),
729 "'C:\\Users\\johndoe\\dev\\python\\39007\\tests\\.venv\\Scripts\\python.exe -m pytest \\\"test_foo.py::test_foo\\\"'".to_string()
730 );
731 }
732
733 #[test]
734 fn test_try_quote_cmd() {
735 let shell_kind = ShellKind::Cmd;
736 assert_eq!(
737 shell_kind
738 .try_quote("C:\\Users\\johndoe\\dev\\python\\39007\\tests\\.venv\\Scripts\\python.exe -m pytest \"test_foo.py::test_foo\"")
739 .unwrap()
740 .into_owned(),
741 "^\"C:\\Users\\johndoe\\dev\\python\\39007\\tests\\.venv\\Scripts\\python.exe -m pytest \\^\"test_foo.py::test_foo\\^\"^\"".to_string()
742 );
743 }
744
745 #[test]
746 fn test_try_quote_powershell_edge_cases() {
747 let shell_kind = ShellKind::PowerShell;
748
749 assert_eq!(
751 shell_kind.try_quote("").unwrap().into_owned(),
752 "'\"\"'".to_string()
753 );
754
755 assert_eq!(shell_kind.try_quote("simple").unwrap(), "simple");
757
758 assert_eq!(
760 shell_kind.try_quote("hello world").unwrap().into_owned(),
761 "'hello world'".to_string()
762 );
763
764 assert_eq!(
766 shell_kind.try_quote("$variable").unwrap().into_owned(),
767 "'$variable'".to_string()
768 );
769
770 assert_eq!(
772 shell_kind.try_quote("test`command").unwrap().into_owned(),
773 "'test`command'".to_string()
774 );
775
776 assert_eq!(
778 shell_kind
779 .try_quote("test `\"$var`\" end")
780 .unwrap()
781 .into_owned(),
782 "'test `\\\"$var`\\\" end'".to_string()
783 );
784
785 assert_eq!(
787 shell_kind.try_quote("C:\\path\\to\\file").unwrap(),
788 "C:\\path\\to\\file"
789 );
790 }
791
792 #[test]
793 fn test_try_quote_cmd_edge_cases() {
794 let shell_kind = ShellKind::Cmd;
795
796 assert_eq!(
798 shell_kind.try_quote("").unwrap().into_owned(),
799 "^\"^\"".to_string()
800 );
801
802 assert_eq!(shell_kind.try_quote("simple").unwrap(), "simple");
804
805 assert_eq!(
807 shell_kind.try_quote("hello world").unwrap().into_owned(),
808 "^\"hello world^\"".to_string()
809 );
810
811 assert_eq!(
813 shell_kind.try_quote("path\\ test").unwrap().into_owned(),
814 "^\"path\\ test^\"".to_string()
815 );
816
817 assert_eq!(
819 shell_kind.try_quote("test path\\").unwrap().into_owned(),
820 "^\"test path\\\\^\"".to_string()
821 );
822
823 assert_eq!(
825 shell_kind.try_quote("test path\\\\").unwrap().into_owned(),
826 "^\"test path\\\\\\\\^\"".to_string()
827 );
828
829 assert_eq!(
831 shell_kind.try_quote("test\\\"quote").unwrap().into_owned(),
832 "^\"test\\\\\\^\"quote^\"".to_string()
833 );
834
835 assert_eq!(
837 shell_kind
838 .try_quote("test\\\\\"quote")
839 .unwrap()
840 .into_owned(),
841 "^\"test\\\\\\\\\\^\"quote^\"".to_string()
842 );
843
844 assert_eq!(
846 shell_kind.try_quote("C:\\path\\to\\file").unwrap(),
847 "C:\\path\\to\\file"
848 );
849 }
850
851 #[test]
852 fn test_try_quote_nu_command() {
853 let shell_kind = ShellKind::Nushell;
854 assert_eq!(
855 shell_kind.try_quote("'uname'").unwrap().into_owned(),
856 "\"'uname'\"".to_string()
857 );
858 assert_eq!(
859 shell_kind
860 .try_quote_prefix_aware("'uname'")
861 .unwrap()
862 .into_owned(),
863 "^\"'uname'\"".to_string()
864 );
865 assert_eq!(
866 shell_kind.try_quote("^uname").unwrap().into_owned(),
867 "'^uname'".to_string()
868 );
869 assert_eq!(
870 shell_kind
871 .try_quote_prefix_aware("^uname")
872 .unwrap()
873 .into_owned(),
874 "^uname".to_string()
875 );
876 assert_eq!(
877 shell_kind.try_quote("^'uname'").unwrap().into_owned(),
878 "'^'\"'uname\'\"".to_string()
879 );
880 assert_eq!(
881 shell_kind
882 .try_quote_prefix_aware("^'uname'")
883 .unwrap()
884 .into_owned(),
885 "^'uname'".to_string()
886 );
887 assert_eq!(
888 shell_kind.try_quote("'uname a'").unwrap().into_owned(),
889 "\"'uname a'\"".to_string()
890 );
891 assert_eq!(
892 shell_kind
893 .try_quote_prefix_aware("'uname a'")
894 .unwrap()
895 .into_owned(),
896 "^\"'uname a'\"".to_string()
897 );
898 assert_eq!(
899 shell_kind.try_quote("^'uname a'").unwrap().into_owned(),
900 "'^'\"'uname a'\"".to_string()
901 );
902 assert_eq!(
903 shell_kind
904 .try_quote_prefix_aware("^'uname a'")
905 .unwrap()
906 .into_owned(),
907 "^'uname a'".to_string()
908 );
909 assert_eq!(
910 shell_kind.try_quote("uname").unwrap().into_owned(),
911 "uname".to_string()
912 );
913 assert_eq!(
914 shell_kind
915 .try_quote_prefix_aware("uname")
916 .unwrap()
917 .into_owned(),
918 "uname".to_string()
919 );
920 }
921
922 #[test]
923 fn test_try_quote_single_quote_paths() {
924 let path_with_quote = r"C:\Temp\O'Brien\repo";
925 let shlex_shells = [
926 ShellKind::Posix,
927 ShellKind::Fish,
928 ShellKind::Csh,
929 ShellKind::Tcsh,
930 ShellKind::Rc,
931 ShellKind::Xonsh,
932 ShellKind::Elvish,
933 ShellKind::Nushell,
934 ];
935
936 for shell_kind in shlex_shells {
937 let quoted = shell_kind.try_quote(path_with_quote).unwrap().into_owned();
938 assert_ne!(quoted, path_with_quote);
939 assert_eq!(
940 shlex::split("ed),
941 Some(vec![path_with_quote.to_string()])
942 );
943
944 if shell_kind == ShellKind::Nushell {
945 let prefixed = shell_kind.prepend_command_prefix("ed);
946 assert!(prefixed.starts_with('^'));
947 }
948 }
949
950 for shell_kind in [ShellKind::PowerShell, ShellKind::Pwsh] {
951 let quoted = shell_kind.try_quote(path_with_quote).unwrap().into_owned();
952 assert!(quoted.starts_with('\''));
953 assert!(quoted.ends_with('\''));
954 assert!(quoted.contains("O''Brien"));
955 }
956 }
957
958 #[test]
959 fn test_to_shell_variable() {
960 assert_eq!(
961 ShellKind::PowerShell.to_shell_variable("${FOO}"),
962 "$env:FOO"
963 );
964 assert_eq!(ShellKind::Pwsh.to_shell_variable("${FOO}"), "$env:FOO");
965 assert_eq!(ShellKind::Cmd.to_shell_variable("${FOO}"), "%FOO%");
966 assert_eq!(ShellKind::Nushell.to_shell_variable("${FOO}"), "$env.FOO");
967 assert_eq!(ShellKind::Posix.to_shell_variable("${FOO}"), "${FOO}");
968
969 assert_eq!(ShellKind::PowerShell.to_shell_variable("$FOO"), "$env:FOO");
970 assert_eq!(
971 ShellKind::PowerShell.to_shell_variable("${日本}"),
972 "$env:日本"
973 );
974 assert_eq!(
975 ShellKind::PowerShell.to_shell_variable("${FOO:-bar}"),
976 "${FOO:-bar}"
977 );
978 }
979
980 #[test]
981 fn test_to_shell_variable_malformed_is_passed_through() {
982 for input in ["${", "${FOO", "${café", "${}", "${日本"] {
983 for shell_kind in [
984 ShellKind::PowerShell,
985 ShellKind::Pwsh,
986 ShellKind::Cmd,
987 ShellKind::Nushell,
988 ] {
989 assert_eq!(shell_kind.to_shell_variable(input), input);
990 }
991 }
992 }
993}