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/// Creates a command that is executed by bash, pretending to be a tty.
23///
24/// This means that the command will assume that terminal colors and
25/// other terminal features are available.
26pub fn bash_command(command: &str) -> io::Result<Command> {
27    let mut command = make_script_command(command, Some("bash"))?;
28    command.stdout(Stdio::piped()).stderr(Stdio::piped());
29
30    Ok(command)
31}
32
33/// Creates a command that is executed by a shell, pretending to be a tty.
34///
35/// This means that the command will assume that terminal colors and
36/// other terminal features are available.
37pub fn command(command: &str, shell: Option<&str>) -> io::Result<Command> {
38    let mut command = make_script_command(command, shell)?;
39    command.stdout(Stdio::piped()).stderr(Stdio::piped());
40
41    Ok(command)
42}
43
44/// Wraps the command in the `script` command that can execute it
45/// pretending to be a tty.
46///
47/// - [Linux docs](https://man7.org/linux/man-pages/man1/script.1.html)
48/// - [FreeBSD docs](https://www.freebsd.org/cgi/man.cgi?query=script&sektion=0&manpath=FreeBSD+12.2-RELEASE+and+Ports&arch=default&format=html)
49/// - [Apple docs](https://opensource.apple.com/source/shell_cmds/shell_cmds-170/script/script.1.auto.html)
50///
51/// ## Examples
52///
53/// ```
54/// use std::process::{Command, Stdio};
55/// use fake_tty::make_script_command;
56///
57/// let output = make_script_command("ls", Some("bash")).unwrap()
58///     .stdout(Stdio::piped())
59///     .stderr(Stdio::piped())
60///     .output().unwrap();
61///
62/// assert!(output.status.success());
63/// ```
64pub fn make_script_command(c: &str, shell: Option<&str>) -> io::Result<Command> {
65    let shell = which_shell(shell.unwrap_or("bash"))?;
66
67    #[cfg(any(target_os = "linux", target_os = "android"))]
68    {
69        let mut command = Command::new("script");
70        command.args(&["-qec", c, "/dev/null"]);
71        command.env("SHELL", shell.trim());
72
73        Ok(command)
74    }
75
76    #[cfg(any(target_os = "macos", target_os = "freebsd"))]
77    {
78        let mut command = Command::new("script");
79        command.args(&["-q", "/dev/null", shell.trim(), "-c", c]);
80        Ok(command)
81    }
82
83    #[cfg(not(any(
84        target_os = "android",
85        target_os = "linux",
86        target_os = "macos",
87        target_os = "freebsd"
88    )))]
89    compile_error!("This platform is not supported. See https://github.com/Aloso/to-html/issues/3")
90}
91
92/// Returns the standard output of the command.
93pub fn get_stdout(stdout: Vec<u8>) -> Result<String, FromUtf8Error> {
94    let out = String::from_utf8(stdout)?;
95
96    #[cfg(any(target_os = "linux", target_os = "android"))]
97    {
98        Ok(out.replace("\r\n", "\n"))
99    }
100
101    #[cfg(any(target_os = "macos", target_os = "freebsd"))]
102    {
103        let mut out = out.replace("\r\n", "\n");
104        if out.starts_with("^D\u{8}\u{8}") {
105            out = out["^D\u{8}\u{8}".len()..].to_string()
106        }
107        Ok(out)
108    }
109}
110
111fn which_shell(shell: &str) -> io::Result<String> {
112    let which = Command::new("which")
113        .arg(shell)
114        .stdout(Stdio::piped())
115        .output()?;
116
117    if which.status.success() {
118        Ok(String::from_utf8(which.stdout).unwrap())
119    } else {
120        Err(io::Error::new(
121            io::ErrorKind::Other,
122            String::from_utf8(which.stderr).unwrap(),
123        ))
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    fn run(s: &str) -> String {
130        let output = crate::bash_command(s).unwrap().output().unwrap();
131        let s1 = crate::get_stdout(output.stdout).unwrap();
132
133        if crate::which_shell("zsh").is_ok() {
134            let output = crate::command(s, Some("zsh")).unwrap().output().unwrap();
135            let s2 = crate::get_stdout(output.stdout).unwrap();
136
137            assert_eq!(s1, s2);
138        }
139
140        s1
141    }
142
143    #[test]
144    fn echo() {
145        assert_eq!(run("echo hello world"), "hello world\n");
146    }
147
148    #[test]
149    fn seq() {
150        assert_eq!(run("seq 3"), "1\n2\n3\n");
151    }
152
153    #[test]
154    fn echo_quotes() {
155        assert_eq!(run(r#"echo "Hello \$\`' world!""#), "Hello $`' world!\n");
156    }
157
158    #[test]
159    fn echo_and_cat() {
160        assert_eq!(
161            run("echo 'look, bash support!' | cat"),
162            "look, bash support!\n"
163        );
164    }
165}