Skip to main content

fake_tty/
lib.rs

1//! Run a command in bash, pretending to be a tty.
2//!
3//! This means that the command will assume that terminal colors and
4//! other terminal features are available.
5//!
6//! ## Example
7//!
8//! ```
9//! let output = fake_tty::bash_command("ls").unwrap()
10//!     .output().unwrap();
11//! assert!(output.status.success());
12//!
13//! let _stdout: String = fake_tty::get_stdout(output.stdout).unwrap();
14//! ```
15
16use std::{
17    io,
18    process::{Command, Stdio},
19    string::FromUtf8Error,
20};
21
22#[cfg(test)]
23mod tests;
24
25#[cfg(not(any(
26    target_os = "android",
27    target_os = "linux",
28    target_os = "macos",
29    target_os = "freebsd"
30)))]
31compile_error!("This platform is not supported. See https://github.com/Aloso/to-html/issues/3");
32
33/// Creates a command that is executed by bash, pretending to be a tty.
34///
35/// This means that the command will assume that terminal colors and
36/// other terminal features are available.
37///
38/// This is equivalent to calling `command(cmd, Some("bash"))`
39pub fn bash_command(command: &str) -> io::Result<Command> {
40    let mut command = make_script_command(command, Some("bash"))?;
41    command.stdout(Stdio::piped()).stderr(Stdio::piped());
42
43    Ok(command)
44}
45
46/// Creates a command that is executed by a shell, pretending to be a tty.
47///
48/// This means that the command will assume that terminal colors and
49/// other terminal features are available.
50///
51/// If `shell` is `None` then it will default to `bash`
52pub fn command(command: &str, shell: Option<&str>) -> io::Result<Command> {
53    let mut command = make_script_command(command, shell)?;
54    command.stdout(Stdio::piped()).stderr(Stdio::piped());
55
56    Ok(command)
57}
58
59/// Wraps the command in the `script` command that can execute it
60/// pretending to be a tty.
61///
62/// - [Linux docs](https://man7.org/linux/man-pages/man1/script.1.html)
63/// - [FreeBSD docs](https://www.freebsd.org/cgi/man.cgi?query=script&sektion=0&manpath=FreeBSD+12.2-RELEASE+and+Ports&arch=default&format=html)
64/// - [Apple docs](https://opensource.apple.com/source/shell_cmds/shell_cmds-170/script/script.1.auto.html)
65///
66/// ## Examples
67///
68/// ```
69/// use std::process::{Command, Stdio};
70/// use fake_tty::make_script_command;
71///
72/// let output = make_script_command("ls", Some("bash")).unwrap()
73///     .stdout(Stdio::piped())
74///     .stderr(Stdio::piped())
75///     .output().unwrap();
76///
77/// assert!(output.status.success());
78/// ```
79pub fn make_script_command(c: &str, shell: Option<&str>) -> io::Result<Command> {
80    let shell = which_shell(shell.unwrap_or("bash"))?;
81
82    #[cfg(any(target_os = "linux", target_os = "android"))]
83    {
84        let mut command = Command::new("script");
85        command.args(["-qec", c, "/dev/null"]);
86        command.env("SHELL", shell.trim());
87
88        Ok(command)
89    }
90
91    #[cfg(any(target_os = "macos", target_os = "freebsd"))]
92    {
93        let mut command = Command::new("script");
94        command.args(&["-q", "/dev/null", shell.trim(), "-c", c]);
95        Ok(command)
96    }
97}
98
99/// Returns the standard output of the command.
100pub fn get_stdout(stdout: Vec<u8>) -> Result<String, FromUtf8Error> {
101    let out = String::from_utf8(stdout)?;
102
103    #[cfg(any(target_os = "linux", target_os = "android"))]
104    {
105        Ok(out.replace("\r\n", "\n"))
106    }
107
108    #[cfg(any(target_os = "macos", target_os = "freebsd"))]
109    {
110        let mut out = out.replace("\r\n", "\n");
111        if out.starts_with("^D\u{8}\u{8}") {
112            out = out["^D\u{8}\u{8}".len()..].to_string()
113        }
114        Ok(out)
115    }
116}
117
118fn which_shell(shell: &str) -> io::Result<String> {
119    let which = Command::new("which")
120        .arg(shell)
121        .stdout(Stdio::piped())
122        .output()?;
123
124    if which.status.success() {
125        Ok(String::from_utf8(which.stdout).unwrap())
126    } else {
127        Err(io::Error::other(String::from_utf8(which.stderr).unwrap()))
128    }
129}