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