term_transcript/shell/
standard.rs

1//! Standard shell support.
2
3use std::{
4    ffi::OsStr,
5    io,
6    path::Path,
7    process::{Child, ChildStdin, Command},
8};
9
10use styled_str::StyledStr;
11
12use super::ShellOptions;
13use crate::{
14    ExitStatus,
15    traits::{ConfigureCommand, Echoing, SpawnShell, SpawnedShell},
16};
17
18#[derive(Debug, Clone, Copy)]
19enum StdShellType {
20    /// `sh` shell.
21    Sh,
22    /// `bash` shell.
23    Bash,
24    /// PowerShell.
25    PowerShell,
26}
27
28/// Shell interpreter that brings additional functionality for [`ShellOptions`].
29#[derive(Debug)]
30pub struct StdShell {
31    shell_type: StdShellType,
32    command: Command,
33}
34
35impl ConfigureCommand for StdShell {
36    fn current_dir(&mut self, dir: &Path) {
37        self.command.current_dir(dir);
38    }
39
40    fn env(&mut self, name: &str, value: &OsStr) {
41        self.command.env(name, value);
42    }
43}
44
45#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", ret))]
46fn check_sh_exit_code(response: StyledStr<'_>) -> Option<ExitStatus> {
47    let response = response.text();
48    response.trim().parse().ok().map(ExitStatus)
49}
50
51#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", ret))]
52fn check_ps_exit_code(response: StyledStr<'_>) -> Option<ExitStatus> {
53    let response = response.text();
54    match response.trim() {
55        "True" => Some(ExitStatus(0)),
56        "False" => Some(ExitStatus(1)),
57        _ => None,
58    }
59}
60
61impl ShellOptions<StdShell> {
62    /// Creates options for an `sh` shell.
63    pub fn sh() -> Self {
64        let this = Self::new(StdShell {
65            shell_type: StdShellType::Sh,
66            command: Command::new("sh"),
67        });
68        this.with_status_check("echo $?", check_sh_exit_code)
69    }
70
71    /// Creates options for a Bash shell.
72    pub fn bash() -> Self {
73        let this = Self::new(StdShell {
74            shell_type: StdShellType::Bash,
75            command: Command::new("bash"),
76        });
77        this.with_status_check("echo $?", check_sh_exit_code)
78    }
79
80    /// Creates options for PowerShell 6+ (the one with the `pwsh` executable).
81    pub fn pwsh() -> Self {
82        let mut command = Command::new("pwsh");
83        command.arg("-NoLogo").arg("-NoExit");
84
85        let command = StdShell {
86            shell_type: StdShellType::PowerShell,
87            command,
88        };
89        Self::new(command)
90            .with_init_command("function prompt { }")
91            .with_status_check("echo $?", check_ps_exit_code)
92    }
93
94    /// Creates an alias for the binary at `path_to_bin`, which should be an absolute path.
95    /// This allows to call the binary using this alias without complex preparations (such as
96    /// installing it globally via `cargo install`), and is more flexible than
97    /// [`Self::with_cargo_path()`].
98    ///
99    /// In integration tests, you may use [`env!("CARGO_BIN_EXE_<name>")`] to get a path
100    /// to binary targets.
101    ///
102    /// # Limitations
103    ///
104    /// - For Bash and PowerShell, `name` must be a valid name of a function. For `sh`,
105    ///   `name` must be a valid name for the `alias` command. The `name` validity
106    ///   is **not** checked.
107    ///
108    /// [`env!("CARGO_BIN_EXE_<name>")`]: https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-crates
109    #[must_use]
110    pub fn with_alias(self, name: &str, path_to_bin: &str) -> Self {
111        let alias_command = match self.command.shell_type {
112            StdShellType::Sh => {
113                format!("alias {name}=\"'{path_to_bin}'\"")
114            }
115            StdShellType::Bash => format!("{name}() {{ '{path_to_bin}' \"$@\"; }}"),
116            StdShellType::PowerShell => format!("function {name} {{ & '{path_to_bin}' @Args }}"),
117        };
118
119        self.with_init_command(alias_command)
120    }
121}
122
123impl SpawnShell for StdShell {
124    type ShellProcess = Echoing<Child>;
125    type Reader = os_pipe::PipeReader;
126    type Writer = ChildStdin;
127
128    #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", err))]
129    fn spawn_shell(&mut self) -> io::Result<SpawnedShell<Self>> {
130        let SpawnedShell {
131            shell,
132            reader,
133            writer,
134        } = self.command.spawn_shell()?;
135
136        let is_echoing = matches!(self.shell_type, StdShellType::PowerShell);
137        Ok(SpawnedShell {
138            shell: Echoing::new(shell, is_echoing),
139            reader,
140            writer,
141        })
142    }
143}