├── .cirrus.yml ├── .github └── workflows │ ├── ci.yml │ └── coverage.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── bash.rs ├── check.rs ├── expect_line.rs ├── ftp.rs ├── ftp_interact.rs ├── interact.rs ├── interact_with_callback.rs ├── log.rs ├── ping.rs ├── powershell.rs ├── python.rs └── shell.rs ├── src ├── captures.rs ├── check_macros.rs ├── control_code.rs ├── error.rs ├── expect.rs ├── interact │ ├── actions │ │ ├── lookup.rs │ │ └── mod.rs │ ├── context.rs │ ├── mod.rs │ └── session.rs ├── lib.rs ├── needle.rs ├── process │ ├── mod.rs │ ├── unix.rs │ └── windows.rs ├── repl.rs ├── session │ ├── async_session.rs │ ├── mod.rs │ └── sync_session.rs ├── stream │ ├── log.rs │ ├── mod.rs │ └── stdin.rs └── waiter │ ├── blocking.rs │ ├── mod.rs │ └── wait.rs └── tests ├── actions ├── cat │ └── main.py └── echo │ └── main.py ├── check.rs ├── expect.rs ├── interact.rs ├── io.rs ├── is_matched.rs ├── log.rs ├── repl.rs ├── session.rs └── source ├── ansi.py └── colors.py /.cirrus.yml: -------------------------------------------------------------------------------- 1 | freebsd_instance: 2 | image_family: freebsd-13-1 3 | 4 | task: 5 | timeout_in: 5m 6 | install_script: 7 | # - pkg update -f 8 | - pkg upgrade -f --yes 9 | - pkg install -y rust bash python 10 | script: 11 | - cargo test 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: {} 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | check: 14 | name: Check 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | rust: [nightly, stable] 19 | platform: [ubuntu-latest, macos-latest, windows-latest] 20 | features: ["''", "polling", "async", "polling,async"] 21 | runs-on: ${{ matrix.platform }} 22 | steps: 23 | - uses: actions/checkout@v2 24 | - uses: actions-rs/toolchain@v1 25 | with: 26 | profile: minimal 27 | toolchain: ${{ matrix.rust || 'stable' }} 28 | override: true 29 | - uses: actions-rs/cargo@v1 30 | with: 31 | command: check 32 | args: --no-default-features --features ${{ matrix.features }} 33 | 34 | check-tests: 35 | name: Check Tests 36 | strategy: 37 | fail-fast: false 38 | matrix: 39 | rust: [nightly, stable] 40 | platform: [ubuntu-latest, macos-latest, windows-latest] 41 | runs-on: ${{ matrix.platform }} 42 | steps: 43 | - uses: actions/checkout@v2 44 | - uses: actions-rs/toolchain@v1 45 | with: 46 | profile: minimal 47 | toolchain: ${{ matrix.rust || 'stable' }} 48 | override: true 49 | - uses: actions-rs/cargo@v1 50 | with: 51 | command: check 52 | args: --tests 53 | 54 | test: 55 | name: Test Suite 56 | strategy: 57 | fail-fast: false 58 | matrix: 59 | platform: [ubuntu-latest, macos-latest] 60 | feauture: 61 | ["", "--features async"] 62 | runs-on: ${{ matrix.platform }} 63 | steps: 64 | - uses: actions/checkout@v2 65 | - uses: actions-rs/cargo@v1 66 | with: 67 | command: test 68 | args: --verbose ${{ matrix.feauture }} 69 | 70 | fmt: 71 | name: Rustfmt 72 | runs-on: ubuntu-latest 73 | steps: 74 | - uses: actions/checkout@v2 75 | - uses: actions-rs/toolchain@v1 76 | with: 77 | profile: minimal 78 | toolchain: stable 79 | override: true 80 | - run: rustup component add rustfmt 81 | - uses: actions-rs/cargo@v1 82 | with: 83 | command: fmt 84 | args: --all -- --check 85 | 86 | clippy: 87 | name: Clippy 88 | runs-on: ubuntu-latest 89 | steps: 90 | - uses: actions/checkout@v2 91 | - uses: actions-rs/toolchain@v1 92 | with: 93 | profile: minimal 94 | toolchain: stable 95 | override: true 96 | - run: rustup component add clippy 97 | - uses: actions-rs/cargo@v1 98 | with: 99 | command: clippy 100 | args: -- -D warnings 101 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Code Coverage 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: {} 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | coverage: 14 | name: Coveralls 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: actions-rs/toolchain@v1 19 | with: 20 | profile: minimal 21 | toolchain: stable 22 | override: true 23 | - name: Run cargo-tarpaulin 24 | uses: actions-rs/tarpaulin@v0.1 25 | with: 26 | version: "0.15.0" 27 | args: "--workspace --out Lcov --output-dir ./coverage" 28 | - name: Upload to Coveralls 29 | uses: coverallsapp/github-action@master 30 | with: 31 | github-token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | # Python cache 13 | **/*.pyc 14 | __pycache__ 15 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "expectrl" 3 | version = "0.7.1" 4 | authors = ["Maxim Zhiburt "] 5 | edition = "2021" 6 | resolver = "2" 7 | description = "A tool for automating terminal applications in Unix like Don libes expect" 8 | repository = "https://github.com/zhiburt/expectrl" 9 | homepage = "https://github.com/zhiburt/expectrl" 10 | documentation = "https://docs.rs/expectrl" 11 | license = "MIT" 12 | categories = ["development-tools::testing", "os::unix-apis", "os::windows-apis"] 13 | keywords = ["expect", "pty", "testing", "terminal", "automation"] 14 | readme = "README.md" 15 | 16 | [features] 17 | # "pooling" feature works only for not async version on UNIX 18 | polling = ["dep:polling", "dep:crossbeam-channel"] 19 | async = ["futures-lite", "futures-timer", "async-io", "blocking"] 20 | 21 | [dependencies] 22 | regex = "1.6.0" 23 | futures-lite = { version = "1.12.0", optional = true } 24 | futures-timer = { version = "3.0.2", optional = true } 25 | 26 | [target.'cfg(unix)'.dependencies] 27 | ptyprocess = "0.4.1" 28 | nix = "0.26" 29 | async-io = { version = "1.9.0", optional = true } 30 | polling = { version = "2.3.0", optional = true } 31 | 32 | [target.'cfg(windows)'.dependencies] 33 | conpty = "0.5.0" 34 | blocking = { version = "1.2.0", optional = true } 35 | crossbeam-channel = { version = "0.5.6", optional = true } 36 | 37 | [package.metadata.docs.rs] 38 | all-features = false 39 | 40 | [[target.'cfg(unix)'.example]] 41 | name = "log" 42 | path = "examples/log.rs" 43 | 44 | [[target.'cfg(windows)'.example]] 45 | name = "powershell" 46 | path = "examples/powershell.rs" 47 | 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Maxim Zhiburt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build](https://github.com/zhiburt/expectrl/actions/workflows/ci.yml/badge.svg)](https://github.com/zhiburt/expectrl/actions/workflows/ci.yml) 2 | [![coverage status](https://coveralls.io/repos/github/zhiburt/expectrl/badge.svg?branch=main)](https://coveralls.io/github/zhiburt/expectrl?branch=main) 3 | [![crate](https://img.shields.io/crates/v/expectrl)](https://crates.io/crates/expectrl) 4 | [![docs.rs](https://img.shields.io/docsrs/expectrl?color=blue)](https://docs.rs/expectrl/*/expectrl/) 5 | 6 | # expectrl 7 | 8 | Expectrl is a tool for automating terminal applications. 9 | 10 | Expectrl is a rust module for spawning child applications and controlling them and responding to expected patterns in process's output. Expectrl works like Don Libes' Expect. Expectrl allows your script to spawn a child application and control it as if a human were typing commands. 11 | 12 | Using the library you can: 13 | 14 | - Spawn process 15 | - Control process 16 | - Interact with process's IO(input/output). 17 | 18 | `expectrl` like original `expect` may shine when you're working with interactive applications. 19 | If your application is not interactive you may not find the library the best choiсe. 20 | 21 | ## Usage 22 | 23 | Add `expectrl` to your Cargo.toml. 24 | 25 | ```toml 26 | # Cargo.toml 27 | [dependencies] 28 | expectrl = "0.7" 29 | ``` 30 | 31 | An example where the program simulates a used interacting with `ftp`. 32 | 33 | ```rust 34 | use expectrl::{Regex, Eof, Error, Expect}; 35 | 36 | fn main() -> Result<(), Error> { 37 | let mut p = expectrl::spawn("ftp speedtest.tele2.net")?; 38 | p.expect(Regex("Name \\(.*\\):"))?; 39 | p.send_line("anonymous")?; 40 | p.expect("Password")?; 41 | p.send_line("test")?; 42 | p.expect("ftp>")?; 43 | p.send_line("cd upload")?; 44 | p.expect("successfully changed.\r\nftp>")?; 45 | p.send_line("pwd")?; 46 | p.expect(Regex("[0-9]+ \"/upload\""))?; 47 | p.send_line("exit")?; 48 | p.expect(Eof)?; 49 | 50 | Ok(()) 51 | } 52 | ``` 53 | 54 | The same example but the password will be read from stdin. 55 | 56 | ```rust 57 | use std::io::stdout; 58 | 59 | use expectrl::{ 60 | interact::{actions::lookup::Lookup, InteractSession}, 61 | stream::stdin::Stdin, 62 | ControlCode, Error, Expect, Regex, 63 | }; 64 | 65 | fn main() -> Result<(), Error> { 66 | let mut p = expectrl::spawn("ftp bks4-speedtest-1.tele2.net")?; 67 | 68 | let mut auth = false; 69 | let mut login_lookup = Lookup::new(); 70 | let mut stdin = Stdin::open()?; 71 | 72 | InteractSession::new(&mut p, &mut stdin, stdout(), &mut auth) 73 | .set_output_action(move |ctx| { 74 | if login_lookup 75 | .on(ctx.buf, ctx.eof, "Login successful")? 76 | .is_some() 77 | { 78 | **ctx.state = true; 79 | return Ok(true); 80 | } 81 | 82 | Ok(false) 83 | }) 84 | .spawn()?; 85 | 86 | stdin.close()?; 87 | 88 | if !auth { 89 | println!("An authentication was not passed"); 90 | return Ok(()); 91 | } 92 | 93 | p.expect("ftp>")?; 94 | p.send_line("cd upload")?; 95 | p.expect("successfully changed.")?; 96 | p.send_line("pwd")?; 97 | p.expect(Regex("[0-9]+ \"/upload\""))?; 98 | p.send(ControlCode::EndOfTransmission)?; 99 | p.expect("Goodbye.")?; 100 | 101 | Ok(()) 102 | } 103 | ``` 104 | 105 | #### [For more examples, check the examples directory.](https://github.com/zhiburt/expectrl/tree/main/examples) 106 | 107 | ## Features 108 | 109 | - It has an `async` support (To enable them you must turn on an `async` feature). 110 | - It supports logging. 111 | - It supports interact function. 112 | - It works on windows. 113 | 114 | ## Notes 115 | 116 | It was originally inspired by [philippkeller/rexpect] and [pexpect]. 117 | 118 | Licensed under [MIT License](LICENSE) 119 | 120 | [philippkeller/rexpect]: https://github.com/philippkeller/rexpect 121 | [pexpect]: https://pexpect.readthedocs.io/en/stable/overview.html 122 | -------------------------------------------------------------------------------- /examples/bash.rs: -------------------------------------------------------------------------------- 1 | // An example is based on README.md from https://github.com/philippkeller/rexpect 2 | 3 | #[cfg(unix)] 4 | use expectrl::{repl::spawn_bash, ControlCode, Expect, Regex}; 5 | 6 | #[cfg(unix)] 7 | #[cfg(not(feature = "async"))] 8 | fn main() { 9 | let mut p = spawn_bash().unwrap(); 10 | 11 | // case 1: execute 12 | let hostname = p.execute("hostname").unwrap(); 13 | println!("Current hostname: {:?}", String::from_utf8_lossy(&hostname)); 14 | 15 | // case 2: wait until done, only extract a few infos 16 | p.send_line("wc /etc/passwd").unwrap(); 17 | // `exp_regex` returns both string-before-match and match itself, discard first 18 | let lines = p.expect(Regex("[0-9]+")).unwrap(); 19 | let words = p.expect(Regex("[0-9]+")).unwrap(); 20 | let bytes = p.expect(Regex("[0-9]+")).unwrap(); 21 | p.expect_prompt().unwrap(); // go sure `wc` is really done 22 | println!( 23 | "/etc/passwd has {} lines, {} words, {} chars", 24 | String::from_utf8_lossy(&lines[0]), 25 | String::from_utf8_lossy(&words[0]), 26 | String::from_utf8_lossy(&bytes[0]), 27 | ); 28 | 29 | // case 3: read while program is still executing 30 | p.send_line("ping 8.8.8.8").unwrap(); // returns when it sees "bytes of data" in output 31 | for _ in 0..5 { 32 | // times out if one ping takes longer than 2s 33 | let duration = p.expect(Regex("[0-9. ]+ ms")).unwrap(); 34 | println!("Roundtrip time: {}", String::from_utf8_lossy(&duration[0])); 35 | } 36 | 37 | p.send(ControlCode::EOT).unwrap(); 38 | } 39 | 40 | #[cfg(unix)] 41 | #[cfg(feature = "async")] 42 | fn main() { 43 | use expectrl::AsyncExpect; 44 | use futures_lite::io::AsyncBufReadExt; 45 | 46 | futures_lite::future::block_on(async { 47 | let mut p = spawn_bash().await.unwrap(); 48 | 49 | // case 1: wait until program is done 50 | p.send_line("hostname").await.unwrap(); 51 | let mut hostname = String::new(); 52 | p.read_line(&mut hostname).await.unwrap(); 53 | p.expect_prompt().await.unwrap(); // go sure `hostname` is really done 54 | println!("Current hostname: {hostname:?}"); // it prints some undetermined characters before hostname ... 55 | 56 | // case 2: wait until done, only extract a few infos 57 | p.send_line("wc /etc/passwd").await.unwrap(); 58 | // `exp_regex` returns both string-before-match and match itself, discard first 59 | let lines = p.expect(Regex("[0-9]+")).await.unwrap(); 60 | let words = p.expect(Regex("[0-9]+")).await.unwrap(); 61 | let bytes = p.expect(Regex("[0-9]+")).await.unwrap(); 62 | p.expect_prompt().await.unwrap(); // go sure `wc` is really done 63 | println!( 64 | "/etc/passwd has {} lines, {} words, {} chars", 65 | String::from_utf8_lossy(lines.get(0).unwrap()), 66 | String::from_utf8_lossy(words.get(0).unwrap()), 67 | String::from_utf8_lossy(bytes.get(0).unwrap()), 68 | ); 69 | 70 | // case 3: read while program is still executing 71 | p.send_line("ping 8.8.8.8").await.unwrap(); // returns when it sees "bytes of data" in output 72 | for _ in 0..5 { 73 | // times out if one ping takes longer than 2s 74 | let duration = p.expect(Regex("[0-9. ]+ ms")).await.unwrap(); 75 | println!( 76 | "Roundtrip time: {}", 77 | String::from_utf8_lossy(duration.get(0).unwrap()) 78 | ); 79 | } 80 | 81 | p.send(ControlCode::EOT).await.unwrap(); 82 | }) 83 | } 84 | 85 | #[cfg(windows)] 86 | fn main() { 87 | panic!("An example doesn't supported on windows") 88 | } 89 | -------------------------------------------------------------------------------- /examples/check.rs: -------------------------------------------------------------------------------- 1 | use expectrl::{check, spawn, Error, Expect}; 2 | 3 | #[cfg(not(feature = "async"))] 4 | fn main() { 5 | let mut p = spawn("python ./tests/source/ansi.py").expect("Can't spawn a session"); 6 | 7 | loop { 8 | match check!( 9 | &mut p, 10 | _ = "Password: " => { 11 | println!("Set password to SECURE_PASSWORD"); 12 | p.send_line("SECURE_PASSWORD").unwrap(); 13 | }, 14 | _ = "Continue [y/n]:" => { 15 | println!("Stop processing"); 16 | p.send_line("n").unwrap(); 17 | }, 18 | ) { 19 | Err(Error::Eof) => break, 20 | result => result.unwrap(), 21 | }; 22 | } 23 | } 24 | 25 | #[cfg(feature = "async")] 26 | fn main() { 27 | use expectrl::AsyncExpect; 28 | 29 | futures_lite::future::block_on(async { 30 | let mut session = spawn("python ./tests/source/ansi.py").expect("Can't spawn a session"); 31 | 32 | loop { 33 | match check!( 34 | &mut session, 35 | _ = "Password: " => { 36 | println!("Set password to SECURE_PASSWORD"); 37 | session.send_line("SECURE_PASSWORD").await.unwrap(); 38 | }, 39 | _ = "Continue [y/n]:" => { 40 | println!("Stop processing"); 41 | session.send_line("n").await.unwrap(); 42 | }, 43 | ) 44 | .await 45 | { 46 | Err(Error::Eof) => break, 47 | result => result.unwrap(), 48 | }; 49 | } 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /examples/expect_line.rs: -------------------------------------------------------------------------------- 1 | use expectrl::{self, Any, Eof, Expect}; 2 | 3 | #[cfg(not(feature = "async"))] 4 | fn main() { 5 | let mut p = expectrl::spawn("ls -al").expect("Can't spawn a session"); 6 | 7 | loop { 8 | let m = p 9 | .expect(Any::boxed(vec![ 10 | Box::new("\r"), 11 | Box::new("\n"), 12 | Box::new(Eof), 13 | ])) 14 | .expect("Expect failed"); 15 | 16 | println!("{:?}", String::from_utf8_lossy(m.as_bytes())); 17 | 18 | let is_eof = m[0].is_empty(); 19 | if is_eof { 20 | break; 21 | } 22 | 23 | if m[0] == [b'\n'] { 24 | continue; 25 | } 26 | 27 | println!("{:?}", String::from_utf8_lossy(&m[0])); 28 | } 29 | } 30 | 31 | #[cfg(feature = "async")] 32 | fn main() { 33 | futures_lite::future::block_on(async { 34 | use expectrl::AsyncExpect; 35 | 36 | let mut session = expectrl::spawn("ls -al").expect("Can't spawn a session"); 37 | 38 | loop { 39 | let m = session 40 | .expect(Any::boxed(vec![ 41 | Box::new("\r"), 42 | Box::new("\n"), 43 | Box::new(Eof), 44 | ])) 45 | .await 46 | .expect("Expect failed"); 47 | 48 | let is_eof = m.get(0).unwrap().is_empty(); 49 | if is_eof { 50 | break; 51 | } 52 | 53 | if m.get(0).unwrap() == [b'\n'] { 54 | continue; 55 | } 56 | 57 | println!("{:?}", String::from_utf8_lossy(m.get(0).unwrap())); 58 | } 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /examples/ftp.rs: -------------------------------------------------------------------------------- 1 | use expectrl::{spawn, ControlCode, Error, Expect, Regex}; 2 | 3 | #[cfg(not(feature = "async"))] 4 | fn main() -> Result<(), Error> { 5 | let mut p = spawn("ftp bks4-speedtest-1.tele2.net")?; 6 | p.expect(Regex("Name \\(.*\\):"))?; 7 | p.send_line("anonymous")?; 8 | p.expect("Password")?; 9 | p.send_line("test")?; 10 | p.expect("ftp>")?; 11 | p.send_line("cd upload")?; 12 | p.expect("successfully changed.")?; 13 | p.send_line("pwd")?; 14 | p.expect(Regex("[0-9]+ \"/upload\""))?; 15 | p.send(ControlCode::EndOfTransmission)?; 16 | p.expect("Goodbye.")?; 17 | Ok(()) 18 | } 19 | 20 | #[cfg(feature = "async")] 21 | fn main() {} 22 | -------------------------------------------------------------------------------- /examples/ftp_interact.rs: -------------------------------------------------------------------------------- 1 | use expectrl::{ 2 | interact::actions::lookup::Lookup, spawn, stream::stdin::Stdin, ControlCode, Error, Expect, 3 | Regex, 4 | }; 5 | use std::io::stdout; 6 | 7 | #[cfg(not(all(windows, feature = "polling")))] 8 | #[cfg(not(feature = "async"))] 9 | fn main() -> Result<(), Error> { 10 | let mut p = spawn("ftp bks4-speedtest-1.tele2.net")?; 11 | 12 | let mut auth = false; 13 | let mut login_lookup = Lookup::new(); 14 | let mut stdin = Stdin::open()?; 15 | 16 | p.interact(&mut stdin, stdout()) 17 | .with_state(&mut auth) 18 | .set_output_action(move |ctx| { 19 | if login_lookup 20 | .on(ctx.buf, ctx.eof, "Login successful")? 21 | .is_some() 22 | { 23 | **ctx.state = true; 24 | return Ok(true); 25 | } 26 | 27 | Ok(false) 28 | }) 29 | .spawn()?; 30 | 31 | stdin.close()?; 32 | 33 | if !auth { 34 | println!("An authefication was not passed"); 35 | return Ok(()); 36 | } 37 | 38 | p.expect("ftp>")?; 39 | p.send_line("cd upload")?; 40 | p.expect("successfully changed.")?; 41 | p.send_line("pwd")?; 42 | p.expect(Regex("[0-9]+ \"/upload\""))?; 43 | p.send(ControlCode::EndOfTransmission)?; 44 | p.expect("Goodbye.")?; 45 | Ok(()) 46 | } 47 | 48 | #[cfg(any(all(windows, feature = "polling"), feature = "async"))] 49 | fn main() {} 50 | -------------------------------------------------------------------------------- /examples/interact.rs: -------------------------------------------------------------------------------- 1 | //! To run an example run `cargo run --example interact`. 2 | 3 | use expectrl::{spawn, stream::stdin::Stdin}; 4 | use std::io::stdout; 5 | 6 | #[cfg(unix)] 7 | const SHELL: &str = "sh"; 8 | 9 | #[cfg(windows)] 10 | const SHELL: &str = "powershell"; 11 | 12 | #[cfg(not(all(windows, feature = "polling")))] 13 | #[cfg(not(feature = "async"))] 14 | fn main() { 15 | let mut sh = spawn(SHELL).expect("Error while spawning sh"); 16 | 17 | println!("Now you're in interacting mode"); 18 | println!("To return control back to main type CTRL-] combination"); 19 | 20 | let mut stdin = Stdin::open().expect("Failed to create stdin"); 21 | 22 | sh.interact(&mut stdin, stdout()) 23 | .spawn() 24 | .expect("Failed to start interact"); 25 | 26 | stdin.close().expect("Failed to close a stdin"); 27 | 28 | println!("Exiting"); 29 | } 30 | 31 | #[cfg(feature = "async")] 32 | fn main() { 33 | // futures_lite::future::block_on(async { 34 | // let mut sh = spawn(SHELL).expect("Error while spawning sh"); 35 | 36 | // println!("Now you're in interacting mode"); 37 | // println!("To return control back to main type CTRL-] combination"); 38 | 39 | // let mut stdin = Stdin::open().expect("Failed to create stdin"); 40 | 41 | // sh.interact(&mut stdin, stdout()) 42 | // .spawn() 43 | // .await 44 | // .expect("Failed to start interact"); 45 | 46 | // stdin.close().expect("Failed to close a stdin"); 47 | 48 | // println!("Exiting"); 49 | // }); 50 | } 51 | 52 | #[cfg(all(windows, feature = "polling", not(feature = "async")))] 53 | fn main() {} 54 | -------------------------------------------------------------------------------- /examples/interact_with_callback.rs: -------------------------------------------------------------------------------- 1 | use expectrl::{interact::actions::lookup::Lookup, spawn, stream::stdin::Stdin, Regex}; 2 | 3 | #[derive(Debug, Default)] 4 | struct State { 5 | stutus_verification_counter: Option, 6 | wait_for_continue: Option, 7 | pressed_yes_on_continue: Option, 8 | } 9 | 10 | #[cfg(not(all(windows, feature = "polling")))] 11 | #[cfg(not(feature = "async"))] 12 | #[cfg(unix)] 13 | fn main() { 14 | let mut output_action = Lookup::new(); 15 | let mut input_action = Lookup::new(); 16 | let mut state = State::default(); 17 | 18 | let mut session = spawn("python ./tests/source/ansi.py").expect("Can't spawn a session"); 19 | 20 | let mut stdin = Stdin::open().unwrap(); 21 | let stdout = std::io::stdout(); 22 | 23 | let (is_alive, status) = { 24 | let mut interact = session.interact(&mut stdin, stdout).with_state(&mut state); 25 | interact 26 | .set_output_action(move |ctx| { 27 | let m = output_action.on(ctx.buf, ctx.eof, "Continue [y/n]:")?; 28 | if m.is_some() { 29 | ctx.state.wait_for_continue = Some(true); 30 | }; 31 | 32 | let m = output_action.on(ctx.buf, ctx.eof, Regex("status:\\s*.*\\w+.*\\r\\n"))?; 33 | if m.is_some() { 34 | ctx.state.stutus_verification_counter = 35 | Some(ctx.state.stutus_verification_counter.map_or(1, |c| c + 1)); 36 | output_action.clear(); 37 | } 38 | 39 | Ok(false) 40 | }) 41 | .set_input_action(move |ctx| { 42 | let m = input_action.on(ctx.buf, ctx.eof, "y")?; 43 | if m.is_some() { 44 | if let Some(_a @ true) = ctx.state.wait_for_continue { 45 | ctx.state.pressed_yes_on_continue = Some(true); 46 | } 47 | }; 48 | 49 | let m = input_action.on(ctx.buf, ctx.eof, "n")?; 50 | if m.is_some() { 51 | if let Some(_a @ true) = ctx.state.wait_for_continue { 52 | ctx.state.pressed_yes_on_continue = Some(false); 53 | } 54 | } 55 | 56 | Ok(false) 57 | }); 58 | 59 | let is_alive = interact.spawn().expect("Failed to start interact"); 60 | 61 | (is_alive, interact.get_status()) 62 | }; 63 | 64 | if !is_alive { 65 | println!("The process was exited"); 66 | #[cfg(unix)] 67 | println!("Status={:?}", status); 68 | } 69 | 70 | stdin.close().unwrap(); 71 | 72 | println!("RESULTS"); 73 | println!( 74 | "Number of time 'Y' was pressed = {}", 75 | state.pressed_yes_on_continue.unwrap_or_default() 76 | ); 77 | println!( 78 | "Status counter = {}", 79 | state.stutus_verification_counter.unwrap_or_default() 80 | ); 81 | } 82 | 83 | #[cfg(all(unix, feature = "async"))] 84 | fn main() { 85 | // let mut output_action = Lookup::new(); 86 | // let mut input_action = Lookup::new(); 87 | // let mut state = State::default(); 88 | 89 | // let mut session = spawn("python ./tests/source/ansi.py").expect("Can't spawn a session"); 90 | 91 | // let mut stdin = Stdin::open().unwrap(); 92 | // let stdout = std::io::stdout(); 93 | 94 | // let mut interact = session.interact(&mut stdin, stdout).with_state(&mut state); 95 | // interact 96 | // .set_output_action(|mut ctx| { 97 | // let m = output_action.on(ctx.buf, ctx.eof, "Continue [y/n]:")?; 98 | // if m.is_some() { 99 | // ctx.state.wait_for_continue = Some(true); 100 | // }; 101 | 102 | // let m = output_action.on(ctx.buf, ctx.eof, Regex("status:\\s*.*\\w+.*\\r\\n"))?; 103 | // if m.is_some() { 104 | // ctx.state.stutus_verification_counter = 105 | // Some(ctx.state.stutus_verification_counter.map_or(1, |c| c + 1)); 106 | // output_action.clear(); 107 | // } 108 | 109 | // Ok(false) 110 | // }) 111 | // .set_input_action(|mut ctx| { 112 | // let m = input_action.on(ctx.buf, ctx.eof, "y")?; 113 | // if m.is_some() { 114 | // if let Some(_a @ true) = ctx.state.wait_for_continue { 115 | // ctx.state.pressed_yes_on_continue = Some(true); 116 | // } 117 | // }; 118 | 119 | // let m = input_action.on(ctx.buf, ctx.eof, "n")?; 120 | // if m.is_some() { 121 | // if let Some(_a @ true) = ctx.state.wait_for_continue { 122 | // ctx.state.pressed_yes_on_continue = Some(false); 123 | // } 124 | // } 125 | 126 | // Ok(false) 127 | // }); 128 | 129 | // let is_alive = 130 | // futures_lite::future::block_on(interact.spawn()).expect("Failed to start interact"); 131 | 132 | // if !is_alive { 133 | // println!("The process was exited"); 134 | // #[cfg(unix)] 135 | // println!("Status={:?}", interact.get_status()); 136 | // } 137 | 138 | // stdin.close().unwrap(); 139 | 140 | // println!("RESULTS"); 141 | // println!( 142 | // "Number of time 'Y' was pressed = {}", 143 | // state.pressed_yes_on_continue.unwrap_or_default() 144 | // ); 145 | // println!( 146 | // "Status counter = {}", 147 | // state.stutus_verification_counter.unwrap_or_default() 148 | // ); 149 | } 150 | 151 | #[cfg(windows)] 152 | fn main() {} 153 | -------------------------------------------------------------------------------- /examples/log.rs: -------------------------------------------------------------------------------- 1 | use expectrl::{spawn, Error, Expect}; 2 | 3 | #[cfg(feature = "async")] 4 | use expectrl::AsyncExpect; 5 | 6 | fn main() -> Result<(), Error> { 7 | let p = spawn("cat")?; 8 | let mut p = expectrl::session::log(p, std::io::stdout())?; 9 | 10 | #[cfg(not(feature = "async"))] 11 | { 12 | p.send_line("Hello World")?; 13 | p.expect("Hello World")?; 14 | } 15 | #[cfg(feature = "async")] 16 | { 17 | futures_lite::future::block_on(async { 18 | p.send_line("Hello World").await?; 19 | p.expect("Hello World").await 20 | })?; 21 | } 22 | 23 | Ok(()) 24 | } 25 | -------------------------------------------------------------------------------- /examples/ping.rs: -------------------------------------------------------------------------------- 1 | #[cfg(unix)] 2 | use expectrl::{repl::spawn_bash, ControlCode, Error, Expect}; 3 | 4 | #[cfg(feature = "async")] 5 | use expectrl::AsyncExpect; 6 | 7 | #[cfg(unix)] 8 | #[cfg(not(feature = "async"))] 9 | fn main() -> Result<(), Error> { 10 | let mut p = spawn_bash()?; 11 | p.send_line("ping 8.8.8.8")?; 12 | p.expect("bytes of data")?; 13 | p.send(ControlCode::try_from("^Z").unwrap())?; 14 | p.expect_prompt()?; 15 | // bash writes 'ping 8.8.8.8' to stdout again to state which job was put into background 16 | p.send_line("bg")?; 17 | p.expect("ping 8.8.8.8")?; 18 | p.expect_prompt()?; 19 | p.send_line("sleep 0.5")?; 20 | p.expect_prompt()?; 21 | // bash writes 'ping 8.8.8.8' to stdout again to state which job was put into foreground 22 | p.send_line("fg")?; 23 | p.expect("ping 8.8.8.8")?; 24 | p.send(ControlCode::try_from("^D").unwrap())?; 25 | p.expect("packet loss")?; 26 | 27 | Ok(()) 28 | } 29 | 30 | #[cfg(unix)] 31 | #[cfg(feature = "async")] 32 | fn main() -> Result<(), Error> { 33 | futures_lite::future::block_on(async { 34 | let mut p = spawn_bash().await?; 35 | p.send_line("ping 8.8.8.8").await?; 36 | p.expect("bytes of data").await?; 37 | p.send(ControlCode::Substitute).await?; // CTRL_Z 38 | p.expect_prompt().await?; 39 | // bash writes 'ping 8.8.8.8' to stdout again to state which job was put into background 40 | p.send_line("bg").await?; 41 | p.expect("ping 8.8.8.8").await?; 42 | p.expect_prompt().await?; 43 | p.send_line("sleep 0.5").await?; 44 | p.expect_prompt().await?; 45 | // bash writes 'ping 8.8.8.8' to stdout again to state which job was put into foreground 46 | p.send_line("fg").await?; 47 | p.expect("ping 8.8.8.8").await?; 48 | p.send(ControlCode::EndOfText).await?; 49 | p.expect("packet loss").await?; 50 | Ok(()) 51 | }) 52 | } 53 | 54 | #[cfg(windows)] 55 | fn main() { 56 | panic!("An example doesn't supported on windows") 57 | } 58 | -------------------------------------------------------------------------------- /examples/powershell.rs: -------------------------------------------------------------------------------- 1 | #[cfg(windows)] 2 | use expectrl::{repl::spawn_powershell, ControlCode, Expect, Regex}; 3 | 4 | #[cfg(windows)] 5 | fn main() { 6 | #[cfg(feature = "async")] 7 | { 8 | use expectrl::AsyncExpect; 9 | 10 | futures_lite::future::block_on(async { 11 | let mut p = spawn_powershell().await.unwrap(); 12 | 13 | eprintln!("Current hostname",); 14 | 15 | // case 1: execute 16 | let hostname = p.execute("hostname").await.unwrap(); 17 | println!( 18 | "Current hostname: {:?}", 19 | String::from_utf8(hostname).unwrap() 20 | ); 21 | 22 | // case 2: wait until done, only extract a few infos 23 | p.send_line("type README.md | Measure-Object -line -word -character") 24 | .await 25 | .unwrap(); 26 | let lines = p.expect(Regex("[0-9]+\\s")).await.unwrap(); 27 | let words = p.expect(Regex("[0-9]+\\s")).await.unwrap(); 28 | let bytes = p.expect(Regex("([0-9]+)[^0-9]")).await.unwrap(); 29 | // go sure `wc` is really done 30 | p.expect_prompt().await.unwrap(); 31 | println!( 32 | "/etc/passwd has {} lines, {} words, {} chars", 33 | String::from_utf8_lossy(&lines[0]), 34 | String::from_utf8_lossy(&words[0]), 35 | String::from_utf8_lossy(&bytes[1]), 36 | ); 37 | 38 | // case 3: read while program is still executing 39 | p.send_line("ping 8.8.8.8 -t").await.unwrap(); 40 | for _ in 0..5 { 41 | let duration = p.expect(Regex("[0-9.]+ms")).await.unwrap(); 42 | println!( 43 | "Roundtrip time: {}", 44 | String::from_utf8_lossy(duration.get(0).unwrap()) 45 | ); 46 | } 47 | 48 | p.send(ControlCode::ETX).await.unwrap(); 49 | p.expect_prompt().await.unwrap(); 50 | }); 51 | } 52 | #[cfg(not(feature = "async"))] 53 | { 54 | let mut p = spawn_powershell().unwrap(); 55 | 56 | // case 1: execute 57 | let hostname = p.execute("hostname").unwrap(); 58 | println!( 59 | "Current hostname: {:?}", 60 | String::from_utf8(hostname).unwrap() 61 | ); 62 | 63 | // case 2: wait until done, only extract a few infos 64 | p.send_line("type README.md | Measure-Object -line -word -character") 65 | .unwrap(); 66 | let lines = p.expect(Regex("[0-9]+\\s")).unwrap(); 67 | let words = p.expect(Regex("[0-9]+\\s")).unwrap(); 68 | let bytes = p.expect(Regex("([0-9]+)[^0-9]")).unwrap(); 69 | // go sure `wc` is really done 70 | p.expect_prompt().unwrap(); 71 | println!( 72 | "/etc/passwd has {} lines, {} words, {} chars", 73 | String::from_utf8_lossy(&lines[0]), 74 | String::from_utf8_lossy(&words[0]), 75 | String::from_utf8_lossy(&bytes[1]), 76 | ); 77 | 78 | // case 3: read while program is still executing 79 | p.send_line("ping 8.8.8.8 -t").unwrap(); 80 | for _ in 0..5 { 81 | let duration = p.expect(Regex("[0-9.]+ms")).unwrap(); 82 | println!( 83 | "Roundtrip time: {}", 84 | String::from_utf8_lossy(duration.get(0).unwrap()) 85 | ); 86 | } 87 | 88 | p.send(ControlCode::ETX).unwrap(); 89 | p.expect_prompt().unwrap(); 90 | } 91 | } 92 | 93 | #[cfg(not(windows))] 94 | fn main() { 95 | panic!("An example doesn't supported on windows") 96 | } 97 | -------------------------------------------------------------------------------- /examples/python.rs: -------------------------------------------------------------------------------- 1 | use expectrl::{repl::spawn_python, Expect, Regex}; 2 | 3 | #[cfg(feature = "async")] 4 | use expectrl::AsyncExpect; 5 | 6 | #[cfg(not(feature = "async"))] 7 | fn main() { 8 | let mut p = spawn_python().unwrap(); 9 | 10 | p.execute("import platform").unwrap(); 11 | p.send_line("platform.node()").unwrap(); 12 | 13 | let found = p.expect(Regex(r"'.*'")).unwrap(); 14 | 15 | println!( 16 | "Platform {}", 17 | String::from_utf8_lossy(found.get(0).unwrap()) 18 | ); 19 | } 20 | 21 | #[cfg(feature = "async")] 22 | fn main() { 23 | futures_lite::future::block_on(async { 24 | let mut p = spawn_python().await.unwrap(); 25 | 26 | p.execute("import platform").await.unwrap(); 27 | p.send_line("platform.node()").await.unwrap(); 28 | 29 | let found = p.expect(Regex(r"'.*'")).await.unwrap(); 30 | 31 | println!( 32 | "Platform {}", 33 | String::from_utf8_lossy(found.get(0).unwrap()) 34 | ); 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /examples/shell.rs: -------------------------------------------------------------------------------- 1 | use expectrl::process::Termios; 2 | use expectrl::repl::ReplSession; 3 | use std::io::Result; 4 | 5 | #[cfg(all(unix, not(feature = "async")))] 6 | fn main() -> Result<()> { 7 | let mut p = expectrl::spawn("sh")?; 8 | p.set_echo(true)?; 9 | 10 | let mut shell = ReplSession::new(p, String::from("sh-5.1$")); 11 | shell.set_echo(true); 12 | shell.set_quit_command("exit"); 13 | shell.expect_prompt()?; 14 | 15 | let output = exec(&mut shell, "echo Hello World")?; 16 | println!("{:?}", output); 17 | 18 | let output = exec(&mut shell, "echo '2 + 3' | bc")?; 19 | println!("{:?}", output); 20 | 21 | Ok(()) 22 | } 23 | 24 | #[cfg(all(unix, not(feature = "async")))] 25 | fn exec(shell: &mut ReplSession, cmd: &str) -> Result { 26 | let buf = shell.execute(cmd)?; 27 | let mut string = String::from_utf8_lossy(&buf).into_owned(); 28 | string = string.replace("\r\n\u{1b}[?2004l\r", ""); 29 | string = string.replace("\r\n\u{1b}[?2004h", ""); 30 | 31 | Ok(string) 32 | } 33 | 34 | #[cfg(all(unix, feature = "async"))] 35 | fn main() -> Result<()> { 36 | futures_lite::future::block_on(async { 37 | let mut p = expectrl::spawn("sh")?; 38 | p.set_echo(true)?; 39 | 40 | let mut shell = ReplSession::new(p, String::from("sh-5.1$")); 41 | shell.set_echo(true); 42 | shell.set_quit_command("exit"); 43 | shell.expect_prompt().await?; 44 | 45 | let output = exec(&mut shell, "echo Hello World").await?; 46 | println!("{:?}", output); 47 | 48 | let output = exec(&mut shell, "echo '2 + 3' | bc").await?; 49 | println!("{:?}", output); 50 | 51 | Ok(()) 52 | }) 53 | } 54 | 55 | #[cfg(all(unix, feature = "async"))] 56 | async fn exec(shell: &mut ReplSession, cmd: &str) -> Result { 57 | let buf = shell.execute(cmd).await?; 58 | let mut string = String::from_utf8_lossy(&buf).into_owned(); 59 | string = string.replace("\r\n\u{1b}[?2004l\r", ""); 60 | string = string.replace("\r\n\u{1b}[?2004h", ""); 61 | 62 | Ok(string) 63 | } 64 | 65 | #[cfg(windows)] 66 | fn main() { 67 | panic!("An example doesn't supported on windows") 68 | } 69 | -------------------------------------------------------------------------------- /src/captures.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Index; 2 | 3 | use crate::needle::Match; 4 | 5 | /// Captures is a represention of matched pattern. 6 | /// 7 | /// It might represent an empty match. 8 | #[derive(Debug, Clone, PartialEq, Eq)] 9 | pub struct Captures { 10 | buf: Vec, 11 | matches: Vec, 12 | } 13 | 14 | impl Captures { 15 | /// New returns an instance of Found. 16 | pub(crate) fn new(buf: Vec, matches: Vec) -> Self { 17 | Self { buf, matches } 18 | } 19 | 20 | /// is_empty verifies if any matches were actually found. 21 | pub fn is_empty(&self) -> bool { 22 | self.matches.is_empty() 23 | } 24 | 25 | /// get returns a match by index. 26 | pub fn get(&self, index: usize) -> Option<&[u8]> { 27 | self.matches 28 | .get(index) 29 | .map(|m| &self.buf[m.start()..m.end()]) 30 | } 31 | 32 | /// Matches returns a list of matches. 33 | pub fn matches(&self) -> MatchIter<'_> { 34 | MatchIter::new(self) 35 | } 36 | 37 | /// before returns a bytes before match. 38 | pub fn before(&self) -> &[u8] { 39 | &self.buf[..self.left_most_index()] 40 | } 41 | 42 | /// as_bytes returns all bytes involved in a match, e.g. before the match and 43 | /// in a match itself. 44 | /// 45 | /// In most cases the returned value equeals to concatanted [Self::before] and [Self::matches]. 46 | /// But sometimes like in case of [crate::Regex] it may have a grouping so [Self::matches] might overlap, therefore 47 | /// it will not longer be true. 48 | pub fn as_bytes(&self) -> &[u8] { 49 | &self.buf 50 | } 51 | 52 | fn left_most_index(&self) -> usize { 53 | self.matches 54 | .iter() 55 | .map(|m| m.start()) 56 | .min() 57 | .unwrap_or_default() 58 | } 59 | 60 | pub(crate) fn right_most_index(matches: &[Match]) -> usize { 61 | matches.iter().map(|m| m.end()).max().unwrap_or_default() 62 | } 63 | } 64 | 65 | impl Index for Captures { 66 | type Output = [u8]; 67 | 68 | fn index(&self, index: usize) -> &Self::Output { 69 | let m = &self.matches[index]; 70 | &self.buf[m.start()..m.end()] 71 | } 72 | } 73 | 74 | impl<'a> IntoIterator for &'a Captures { 75 | type Item = &'a [u8]; 76 | type IntoIter = MatchIter<'a>; 77 | 78 | fn into_iter(self) -> Self::IntoIter { 79 | MatchIter::new(self) 80 | } 81 | } 82 | 83 | #[derive(Debug)] 84 | pub struct MatchIter<'a> { 85 | buf: &'a [u8], 86 | matches: std::slice::Iter<'a, Match>, 87 | } 88 | 89 | impl<'a> MatchIter<'a> { 90 | fn new(captures: &'a Captures) -> Self { 91 | Self { 92 | buf: &captures.buf, 93 | matches: captures.matches.iter(), 94 | } 95 | } 96 | } 97 | 98 | impl<'a> Iterator for MatchIter<'a> { 99 | type Item = &'a [u8]; 100 | 101 | fn next(&mut self) -> Option { 102 | self.matches.next().map(|m| &self.buf[m.start()..m.end()]) 103 | } 104 | 105 | fn size_hint(&self) -> (usize, Option) { 106 | self.matches.size_hint() 107 | } 108 | } 109 | 110 | impl ExactSizeIterator for MatchIter<'_> {} 111 | 112 | #[cfg(test)] 113 | mod tests { 114 | use super::*; 115 | 116 | #[test] 117 | fn test_captures_get() { 118 | let m = Captures::new( 119 | b"You can use iterator".to_vec(), 120 | vec![Match::new(0, 3), Match::new(4, 7)], 121 | ); 122 | 123 | assert_eq!(m.get(0), Some(b"You".as_ref())); 124 | assert_eq!(m.get(1), Some(b"can".as_ref())); 125 | assert_eq!(m.get(2), None); 126 | 127 | let m = Captures::new(b"You can use iterator".to_vec(), vec![]); 128 | assert_eq!(m.get(0), None); 129 | assert_eq!(m.get(1), None); 130 | assert_eq!(m.get(2), None); 131 | 132 | let m = Captures::new(vec![], vec![]); 133 | assert_eq!(m.get(0), None); 134 | assert_eq!(m.get(1), None); 135 | assert_eq!(m.get(2), None); 136 | } 137 | 138 | #[test] 139 | #[should_panic] 140 | fn test_captures_get_panics_on_invalid_match() { 141 | let m = Captures::new(b"Hello World".to_vec(), vec![Match::new(0, 100)]); 142 | let _ = m.get(0); 143 | } 144 | 145 | #[test] 146 | fn test_captures_index() { 147 | let m = Captures::new( 148 | b"You can use iterator".to_vec(), 149 | vec![Match::new(0, 3), Match::new(4, 7)], 150 | ); 151 | 152 | assert_eq!(&m[0], b"You".as_ref()); 153 | assert_eq!(&m[1], b"can".as_ref()); 154 | } 155 | 156 | #[test] 157 | #[should_panic] 158 | fn test_captures_index_panics_on_invalid_match() { 159 | let m = Captures::new(b"Hello World".to_vec(), vec![Match::new(0, 100)]); 160 | let _ = &m[0]; 161 | } 162 | 163 | #[test] 164 | #[should_panic] 165 | fn test_captures_index_panics_on_invalid_index() { 166 | let m = Captures::new(b"Hello World".to_vec(), vec![Match::new(0, 3)]); 167 | let _ = &m[10]; 168 | } 169 | 170 | #[test] 171 | #[should_panic] 172 | fn test_captures_index_panics_on_empty_match() { 173 | let m = Captures::new(b"Hello World".to_vec(), vec![]); 174 | let _ = &m[0]; 175 | } 176 | 177 | #[test] 178 | #[should_panic] 179 | fn test_captures_index_panics_on_empty_captures() { 180 | let m = Captures::new(Vec::new(), Vec::new()); 181 | let _ = &m[0]; 182 | } 183 | 184 | #[test] 185 | fn test_before() { 186 | let m = Captures::new(b"You can use iterator".to_vec(), vec![Match::new(4, 7)]); 187 | assert_eq!(m.before(), b"You ".as_ref()); 188 | 189 | let m = Captures::new( 190 | b"You can use iterator".to_vec(), 191 | vec![Match::new(0, 3), Match::new(4, 7)], 192 | ); 193 | assert_eq!(m.before(), b"".as_ref()); 194 | 195 | let m = Captures::new(b"You can use iterator".to_vec(), vec![]); 196 | assert_eq!(m.before(), b"".as_ref()); 197 | 198 | let m = Captures::new(vec![], vec![]); 199 | assert_eq!(m.before(), b"".as_ref()); 200 | } 201 | 202 | #[test] 203 | fn test_matches() { 204 | let m = Captures::new(b"You can use iterator".to_vec(), vec![Match::new(4, 7)]); 205 | assert_eq!(m.before(), b"You ".as_ref()); 206 | 207 | let m = Captures::new( 208 | b"You can use iterator".to_vec(), 209 | vec![Match::new(0, 3), Match::new(4, 7)], 210 | ); 211 | assert_eq!(m.before(), b"".as_ref()); 212 | 213 | let m = Captures::new(b"You can use iterator".to_vec(), vec![]); 214 | assert_eq!(m.before(), b"".as_ref()); 215 | 216 | let m = Captures::new(vec![], vec![]); 217 | assert_eq!(m.before(), b"".as_ref()); 218 | } 219 | 220 | #[test] 221 | fn test_captures_into_iter() { 222 | assert_eq!( 223 | Captures::new( 224 | b"You can use iterator".to_vec(), 225 | vec![Match::new(0, 3), Match::new(4, 7)] 226 | ) 227 | .into_iter() 228 | .collect::>(), 229 | vec![b"You", b"can"] 230 | ); 231 | 232 | assert_eq!( 233 | Captures::new(b"You can use iterator".to_vec(), vec![Match::new(4, 20)]) 234 | .into_iter() 235 | .collect::>(), 236 | vec![b"can use iterator"] 237 | ); 238 | 239 | assert_eq!( 240 | Captures::new(b"You can use iterator".to_vec(), vec![]) 241 | .into_iter() 242 | .collect::>(), 243 | Vec::<&[u8]>::new() 244 | ); 245 | } 246 | 247 | #[test] 248 | fn test_captures_matches() { 249 | assert_eq!( 250 | Captures::new( 251 | b"You can use iterator".to_vec(), 252 | vec![Match::new(0, 3), Match::new(4, 7)] 253 | ) 254 | .matches() 255 | .collect::>(), 256 | vec![b"You", b"can"] 257 | ); 258 | 259 | assert_eq!( 260 | Captures::new(b"You can use iterator".to_vec(), vec![Match::new(4, 20)]) 261 | .matches() 262 | .collect::>(), 263 | vec![b"can use iterator"] 264 | ); 265 | 266 | assert_eq!( 267 | Captures::new(b"You can use iterator".to_vec(), vec![]) 268 | .matches() 269 | .collect::>(), 270 | Vec::<&[u8]>::new() 271 | ); 272 | } 273 | 274 | #[test] 275 | #[should_panic] 276 | fn test_captures_into_iter_panics_on_invalid_match() { 277 | Captures::new(b"Hello World".to_vec(), vec![Match::new(0, 100)]) 278 | .into_iter() 279 | .for_each(|_| {}); 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /src/check_macros.rs: -------------------------------------------------------------------------------- 1 | //! This module contains a `check!` macros. 2 | 3 | /// Check macros provides a convient way to check if things are available in a stream of a process. 4 | /// 5 | /// It falls into a corresponding branch when a pattern was matched. 6 | /// It runs checks from top to bottom. 7 | /// It doesn't wait until any of them be available. 8 | /// If you want to wait until any of the input available you must use your own approach for example putting it in a loop. 9 | /// 10 | /// You can specify a default branch which will be called if nothing was matched. 11 | /// 12 | /// The macros levareges [`Expect::check`] function, so its just made for convience. 13 | /// 14 | /// # Example 15 | /// ```no_run 16 | /// # let mut session = expectrl::spawn("cat").unwrap(); 17 | /// # 18 | /// loop { 19 | /// expectrl::check!{ 20 | /// &mut session, 21 | /// world = "\r" => { 22 | /// // handle end of line 23 | /// }, 24 | /// _ = "Hello World" => { 25 | /// // handle Hello World 26 | /// }, 27 | /// default => { 28 | /// // handle no matches 29 | /// }, 30 | /// } 31 | /// .unwrap(); 32 | /// } 33 | /// ``` 34 | /// 35 | /// [`Expect::check`]: crate::Expect::check 36 | #[cfg(not(feature = "async"))] 37 | #[macro_export] 38 | macro_rules! check { 39 | (@check ($($tokens:tt)*) ($session:expr)) => { 40 | $crate::check!(@case $session, ($($tokens)*), (), ()) 41 | }; 42 | (@check ($session:expr, $($tokens:tt)*) ()) => { 43 | $crate::check!(@check ($($tokens)*) ($session)) 44 | }; 45 | (@check ($session:expr, $($tokens:tt)*) ($session2:expr)) => { 46 | compile_error!("Wrong number of session arguments") 47 | }; 48 | (@check ($($tokens:tt)*) ()) => { 49 | compile_error!("Please provide a session as a first argument") 50 | }; 51 | (@check () ($session:expr)) => { 52 | // there's no reason to run 0 checks so we issue a error. 53 | compile_error!("There's no reason in running check with no arguments. Please supply a check branches") 54 | }; 55 | (@case $session:expr, ($var:tt = $exp:expr => $body:tt, $($tail:tt)*), ($($head:tt)*), ($($default:tt)*)) => { 56 | // regular case 57 | // 58 | // note: we keep order correct by putting head at the beggining 59 | $crate::check!(@case $session, ($($tail)*), ($($head)* $var = $exp => $body, ), ($($default)*)) 60 | }; 61 | (@case $session:expr, ($var:tt = $exp:expr => $body:tt $($tail:tt)*), ($($head:tt)*), ($($default:tt)*)) => { 62 | // allow missing comma 63 | // 64 | // note: we keep order correct by putting head at the beggining 65 | $crate::check!(@case $session, ($($tail)*), ($($head)* $var = $exp => $body, ), ($($default)*)) 66 | }; 67 | (@case $session:expr, (default => $($tail:tt)*), ($($head:tt)*), ($($default:tt)+)) => { 68 | // A repeated default branch 69 | compile_error!("Only 1 default case is allowed") 70 | }; 71 | (@case $session:expr, (default => $body:tt, $($tail:tt)*), ($($head:tt)*), ()) => { 72 | // A default branch 73 | $crate::check!(@case $session, ($($tail)*), ($($head)*), ( { $body; #[allow(unreachable_code)] Ok(()) } )) 74 | }; 75 | (@case $session:expr, (default => $body:tt $($tail:tt)*), ($($head:tt)*), ()) => { 76 | // A default branch 77 | // allow missed comma `,` 78 | $crate::check!(@case $session, ($($tail)*), ($($head)*), ( { $body; Ok(()) } )) 79 | }; 80 | (@case $session:expr, (), ($($head:tt)*), ()) => { 81 | // there's no default branch 82 | // so we make up our own. 83 | $crate::check!(@case $session, (), ($($head)*), ( { Ok(()) } )) 84 | }; 85 | (@case $session:expr, (), ($($tail:tt)*), ($($default:tt)*)) => { 86 | // last point of @case 87 | // call code generation via @branch 88 | $crate::check!(@branch $session, ($($tail)*), ($($default)*)) 89 | }; 90 | // We need to use a variable for pattern mathing, 91 | // user may chose to drop var name using a placeholder '_', 92 | // in which case we can't call a method on such identificator. 93 | // 94 | // We could use an approach like was described 95 | // 96 | // ``` 97 | // Ok(_____random_var_name_which_supposedly_mustnt_be_used) if !_____random_var_name_which_supposedly_mustnt_be_used.is_empty() => 98 | // { 99 | // let $var = _____random_var_name_which_supposedly_mustnt_be_used; 100 | // } 101 | // ``` 102 | // 103 | // The question is which solution is more effichient. 104 | // I took the following approach because there's no chance we influence user's land via the variable name we pick. 105 | (@branch $session:expr, ($var:tt = $exp:expr => $body:tt, $($tail:tt)*), ($($default:tt)*)) => { 106 | match $crate::Expect::check($session, $exp) { 107 | result if result.as_ref().map(|found| !found.is_empty()).unwrap_or(false) => { 108 | let $var = result.unwrap(); 109 | $body; 110 | #[allow(unreachable_code)] 111 | Ok(()) 112 | } 113 | Ok(_) => { 114 | $crate::check!(@branch $session, ($($tail)*), ($($default)*)) 115 | } 116 | Err(err) => Err(err), 117 | } 118 | }; 119 | (@branch $session:expr, (), ($default:tt)) => { 120 | // A standart default branch 121 | $default 122 | }; 123 | (@branch $session:expr, ($($tail:tt)*), ($($default:tt)*)) => { 124 | compile_error!( 125 | concat!( 126 | "No supported syntax tail=(", 127 | stringify!($($tail,)*), 128 | ") ", 129 | "default=(", 130 | stringify!($($default,)*), 131 | ") ", 132 | )) 133 | }; 134 | // Entry point 135 | ($($tokens:tt)*) => { 136 | { 137 | let result: Result::<(), $crate::Error> = $crate::check!(@check ($($tokens)*) ()); 138 | result 139 | } 140 | }; 141 | } 142 | 143 | /// See sync version. 144 | /// 145 | /// Async version of macros use the same approach as sync. 146 | /// It doesn't use any future features. 147 | /// So it may be better better you to use you own approach how to check which one done first. 148 | /// For example you can use `futures_lite::future::race`. 149 | /// 150 | // async version completely the same as sync version expect 2 words '.await' and 'async' 151 | // meaning its a COPY && PASTE 152 | #[cfg(feature = "async")] 153 | #[macro_export] 154 | macro_rules! check { 155 | (@check ($($tokens:tt)*) ($session:expr)) => { 156 | $crate::check!(@case $session, ($($tokens)*), (), ()) 157 | }; 158 | (@check ($session:expr, $($tokens:tt)*) ()) => { 159 | $crate::check!(@check ($($tokens)*) ($session)) 160 | }; 161 | (@check ($session:expr, $($tokens:tt)*) ($session2:expr)) => { 162 | compile_error!("Wrong number of session arguments") 163 | }; 164 | (@check ($($tokens:tt)*) ()) => { 165 | compile_error!("Please provide a session as a first argument") 166 | }; 167 | (@check () ($session:expr)) => { 168 | // there's no reason to run 0 checks so we issue a error. 169 | compile_error!("There's no reason in running check with no arguments. Please supply a check branches") 170 | }; 171 | (@case $session:expr, ($var:tt = $exp:expr => $body:tt, $($tail:tt)*), ($($head:tt)*), ($($default:tt)*)) => { 172 | // regular case 173 | // 174 | // note: we keep order correct by putting head at the beggining 175 | $crate::check!(@case $session, ($($tail)*), ($($head)* $var = $exp => $body, ), ($($default)*)) 176 | }; 177 | (@case $session:expr, ($var:tt = $exp:expr => $body:tt $($tail:tt)*), ($($head:tt)*), ($($default:tt)*)) => { 178 | // allow missing comma 179 | // 180 | // note: we keep order correct by putting head at the beggining 181 | $crate::check!(@case $session, ($($tail)*), ($($head)* $var = $exp => $body, ), ($($default)*)) 182 | }; 183 | (@case $session:expr, (default => $($tail:tt)*), ($($head:tt)*), ($($default:tt)+)) => { 184 | // A repeated default branch 185 | compile_error!("Only 1 default case is allowed") 186 | }; 187 | (@case $session:expr, (default => $body:tt, $($tail:tt)*), ($($head:tt)*), ()) => { 188 | // A default branch 189 | $crate::check!(@case $session, ($($tail)*), ($($head)*), ( { $body; #[allow(unreachable_code)] Ok(()) } )) 190 | }; 191 | (@case $session:expr, (default => $body:tt $($tail:tt)*), ($($head:tt)*), ()) => { 192 | // A default branch 193 | // allow missed comma `,` 194 | $crate::check!(@case $session, ($($tail)*), ($($head)*), ( { $body; Ok(()) } )) 195 | }; 196 | (@case $session:expr, (), ($($head:tt)*), ()) => { 197 | // there's no default branch 198 | // so we make up our own. 199 | $crate::check!(@case $session, (), ($($head)*), ( { Ok(()) } )) 200 | }; 201 | (@case $session:expr, (), ($($tail:tt)*), ($($default:tt)*)) => { 202 | // last point of @case 203 | // call code generation via @branch 204 | $crate::check!(@branch $session, ($($tail)*), ($($default)*)) 205 | }; 206 | // We need to use a variable for pattern mathing, 207 | // user may chose to drop var name using a placeholder '_', 208 | // in which case we can't call a method on such identificator. 209 | // 210 | // We could use an approach like was described 211 | // 212 | // ``` 213 | // Ok(_____random_var_name_which_supposedly_mustnt_be_used) if !_____random_var_name_which_supposedly_mustnt_be_used.is_empty() => 214 | // { 215 | // let $var = _____random_var_name_which_supposedly_mustnt_be_used; 216 | // } 217 | // ``` 218 | // 219 | // The question is which solution is more effichient. 220 | // I took the following approach because there's no chance we influence user's land via the variable name we pick. 221 | (@branch $session:expr, ($var:tt = $exp:expr => $body:tt, $($tail:tt)*), ($($default:tt)*)) => { 222 | match $crate::AsyncExpect::check($session, $exp).await { 223 | Ok(found) => { 224 | if !found.is_empty() { 225 | let $var = found; 226 | $body; 227 | #[allow(unreachable_code)] 228 | return Ok(()) 229 | } 230 | 231 | $crate::check!(@branch $session, ($($tail)*), ($($default)*)) 232 | } 233 | Err(err) => Err(err), 234 | } 235 | }; 236 | (@branch $session:expr, (), ($default:tt)) => { 237 | // A standart default branch 238 | $default 239 | }; 240 | (@branch $session:expr, ($($tail:tt)*), ($($default:tt)*)) => { 241 | compile_error!( 242 | concat!( 243 | "No supported syntax tail=(", 244 | stringify!($($tail,)*), 245 | ") ", 246 | "default=(", 247 | stringify!($($default,)*), 248 | ") ", 249 | )) 250 | }; 251 | // Entry point 252 | ($($tokens:tt)*) => { 253 | async { 254 | let value: Result::<(), $crate::Error> = $crate::check!(@check ($($tokens)*) ()); 255 | value 256 | } 257 | }; 258 | } 259 | 260 | #[cfg(test)] 261 | mod tests { 262 | #[allow(unused_variables)] 263 | #[allow(unused_must_use)] 264 | #[test] 265 | #[ignore = "Testing in compile time"] 266 | fn test_check() { 267 | let mut session = crate::spawn("").unwrap(); 268 | crate::check! { 269 | &mut session, 270 | as11d = "zxc" => {}, 271 | }; 272 | crate::check! { 273 | &mut session, 274 | as11d = "zxc" => {}, 275 | asbb = "zxc123" => {}, 276 | }; 277 | crate::check! { session, }; 278 | 279 | // crate::check! { 280 | // as11d = "zxc" => {}, 281 | // asbb = "zxc123" => {}, 282 | // }; 283 | 284 | // panic on unused session 285 | // crate::check! { session }; 286 | 287 | // trailing commas 288 | crate::check! { 289 | &mut session, 290 | as11d = "zxc" => {} 291 | asbb = "zxc123" => {} 292 | }; 293 | crate::check! { 294 | &mut session, 295 | as11d = "zxc" => {} 296 | default => {} 297 | }; 298 | 299 | #[cfg(not(feature = "async"))] 300 | { 301 | crate::check! { 302 | &mut session, 303 | as11d = "zxc" => {}, 304 | } 305 | .unwrap(); 306 | (crate::check! { 307 | &mut session, 308 | as11d = "zxc" => {}, 309 | }) 310 | .unwrap(); 311 | (crate::check! { 312 | &mut session, 313 | as11d = "zxc" => {}, 314 | }) 315 | .unwrap(); 316 | (crate::check! { 317 | &mut session, 318 | as11d = "zxc" => { 319 | println!("asd") 320 | }, 321 | }) 322 | .unwrap(); 323 | } 324 | 325 | #[cfg(feature = "async")] 326 | async { 327 | crate::check! { 328 | &mut session, 329 | as11d = "zxc" => {}, 330 | } 331 | .await 332 | .unwrap(); 333 | (crate::check! { 334 | &mut session, 335 | as11d = "zxc" => {}, 336 | }) 337 | .await 338 | .unwrap(); 339 | (crate::check! { 340 | &mut session, 341 | as11d = "zxc" => {}, 342 | }) 343 | .await 344 | .unwrap(); 345 | (crate::check! { 346 | &mut session, 347 | as11d = "zxc" => { 348 | println!("asd") 349 | }, 350 | }) 351 | .await 352 | .unwrap(); 353 | }; 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /src/control_code.rs: -------------------------------------------------------------------------------- 1 | //! A module which contains [ControlCode] type. 2 | 3 | use std::convert::TryFrom; 4 | 5 | /// ControlCode represents the standard ASCII control codes [wiki] 6 | /// 7 | /// [wiki]: https://en.wikipedia.org/wiki/C0_and_C1_control_codes 8 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 9 | pub enum ControlCode { 10 | /// Often used as a string terminator, especially in the programming language C. 11 | Null, 12 | /// In message transmission, delimits the start of a message header. 13 | StartOfHeading, 14 | /// First character of message text, and may be used to terminate the message heading. 15 | StartOfText, 16 | /// Often used as a "break" character (Ctrl-C) to interrupt or terminate a program or process. 17 | EndOfText, 18 | /// Often used on Unix to indicate end-of-file on a terminal (Ctrl-D). 19 | EndOfTransmission, 20 | /// Signal intended to trigger a response at the receiving end, to see if it is still present. 21 | Enquiry, 22 | /// Response to an Enquiry, or an indication of successful receipt of a message. 23 | Acknowledge, 24 | /// Used for a beep on systems that didn't have a physical bell. 25 | Bell, 26 | /// Move the cursor one position leftwards. 27 | /// On input, this may delete the character to the left of the cursor. 28 | Backspace, 29 | /// Position to the next character tab stop. 30 | HorizontalTabulation, 31 | /// On Unix, used to mark end-of-line. 32 | /// In DOS, Windows, and various network standards, LF is used following CR as part of the end-of-line mark. 33 | LineFeed, 34 | /// Position the form at the next line tab stop. 35 | VerticalTabulation, 36 | /// It appears in some common plain text files as a page break character. 37 | FormFeed, 38 | /// Originally used to move the cursor to column zero while staying on the same line. 39 | CarriageReturn, 40 | /// Switch to an alternative character set. 41 | ShiftOut, 42 | /// Return to regular character set after ShiftOut. 43 | ShiftIn, 44 | /// May cause a limited number of contiguously following octets to be interpreted in some different way. 45 | DataLinkEscape, 46 | /// A control code which is reserved for device control. 47 | DeviceControl1, 48 | /// A control code which is reserved for device control. 49 | DeviceControl2, 50 | /// A control code which is reserved for device control. 51 | DeviceControl3, 52 | /// A control code which is reserved for device control. 53 | DeviceControl4, 54 | /// In multipoint systems, the NAK is used as the not-ready reply to a poll. 55 | NegativeAcknowledge, 56 | /// Used in synchronous transmission systems to provide a signal from which synchronous correction may be achieved. 57 | SynchronousIdle, 58 | /// Indicates the end of a transmission block of data. 59 | EndOfTransmissionBlock, 60 | /// Indicates that the data preceding it are in error or are to be disregarded. 61 | Cancel, 62 | /// May mark the end of the used portion of the physical medium. 63 | EndOfMedium, 64 | /// Sometimes used to indicate the end of file, both when typing on the terminal and in text files stored on disk. 65 | Substitute, 66 | /// The Esc key on the keyboard will cause this character to be sent on most systems. 67 | /// In systems based on ISO/IEC 2022, even if another set of C0 control codes are used, 68 | /// this octet is required to always represent the escape character. 69 | Escape, 70 | /// Can be used as delimiters to mark fields of data structures. 71 | /// Also it used for hierarchical levels; 72 | /// FS == level 4 73 | FileSeparator, 74 | /// It used for hierarchical levels; 75 | /// GS == level 3 76 | GroupSeparator, 77 | /// It used for hierarchical levels; 78 | /// RS == level 2 79 | RecordSeparator, 80 | /// It used for hierarchical levels; 81 | /// US == level 1 82 | UnitSeparator, 83 | /// Space is a graphic character. It causes the active position to be advanced by one character position. 84 | Space, 85 | /// Usually called backspace on modern machines, and does not correspond to the PC delete key. 86 | Delete, 87 | } 88 | 89 | impl ControlCode { 90 | /// See [ControlCode::Null] 91 | pub const NUL: ControlCode = ControlCode::Null; 92 | /// See [ControlCode::StartOfHeading] 93 | pub const SOH: ControlCode = ControlCode::StartOfHeading; 94 | /// See [ControlCode::StartOfText] 95 | pub const STX: ControlCode = ControlCode::StartOfText; 96 | /// See [ControlCode::EndOfText] 97 | pub const ETX: ControlCode = ControlCode::EndOfText; 98 | /// See [ControlCode::EndOfTransmission] 99 | pub const EOT: ControlCode = ControlCode::EndOfTransmission; 100 | /// See [ControlCode::Enquiry] 101 | pub const ENQ: ControlCode = ControlCode::Enquiry; 102 | /// See [ControlCode::Acknowledge] 103 | pub const ACK: ControlCode = ControlCode::Acknowledge; 104 | /// See [ControlCode::Bell] 105 | pub const BEL: ControlCode = ControlCode::Bell; 106 | /// See [ControlCode::Backspace] 107 | pub const BS: ControlCode = ControlCode::Backspace; 108 | /// See [ControlCode::HorizontalTabulation] 109 | pub const HT: ControlCode = ControlCode::HorizontalTabulation; 110 | /// See [ControlCode::LineFeed] 111 | pub const LF: ControlCode = ControlCode::LineFeed; 112 | /// See [ControlCode::VerticalTabulation] 113 | pub const VT: ControlCode = ControlCode::VerticalTabulation; 114 | /// See [ControlCode::FormFeed] 115 | pub const FF: ControlCode = ControlCode::FormFeed; 116 | /// See [ControlCode::CarriageReturn] 117 | pub const CR: ControlCode = ControlCode::CarriageReturn; 118 | /// See [ControlCode::ShiftOut] 119 | pub const SO: ControlCode = ControlCode::ShiftOut; 120 | /// See [ControlCode::ShiftIn] 121 | pub const SI: ControlCode = ControlCode::ShiftIn; 122 | /// See [ControlCode::DataLinkEscape] 123 | pub const DLE: ControlCode = ControlCode::DataLinkEscape; 124 | /// See [ControlCode::DeviceControl1] 125 | pub const DC1: ControlCode = ControlCode::DeviceControl1; 126 | /// See [ControlCode::DeviceControl2] 127 | pub const DC2: ControlCode = ControlCode::DeviceControl2; 128 | /// See [ControlCode::DeviceControl3] 129 | pub const DC3: ControlCode = ControlCode::DeviceControl3; 130 | /// See [ControlCode::DeviceControl4] 131 | pub const DC4: ControlCode = ControlCode::DeviceControl4; 132 | /// See [ControlCode::NegativeAcknowledge] 133 | pub const NAK: ControlCode = ControlCode::NegativeAcknowledge; 134 | /// See [ControlCode::SynchronousIdle] 135 | pub const SYN: ControlCode = ControlCode::SynchronousIdle; 136 | /// See [ControlCode::EndOfTransmissionBlock] 137 | pub const ETB: ControlCode = ControlCode::EndOfTransmissionBlock; 138 | /// See [ControlCode::Cancel] 139 | pub const CAN: ControlCode = ControlCode::Cancel; 140 | /// See [ControlCode::EndOfMedium] 141 | pub const EM: ControlCode = ControlCode::EndOfMedium; 142 | /// See [ControlCode::Substitute] 143 | pub const SUB: ControlCode = ControlCode::Substitute; 144 | /// See [ControlCode::Escape] 145 | pub const ESC: ControlCode = ControlCode::Escape; 146 | /// See [ControlCode::FileSeparator] 147 | pub const FS: ControlCode = ControlCode::FileSeparator; 148 | /// See [ControlCode::GroupSeparator] 149 | pub const GS: ControlCode = ControlCode::GroupSeparator; 150 | /// See [ControlCode::RecordSeparator] 151 | pub const RS: ControlCode = ControlCode::RecordSeparator; 152 | /// See [ControlCode::UnitSeparator] 153 | pub const US: ControlCode = ControlCode::UnitSeparator; 154 | /// See [ControlCode::Space] 155 | pub const SP: ControlCode = ControlCode::Space; 156 | /// See [ControlCode::Delete] 157 | pub const DEL: ControlCode = ControlCode::Delete; 158 | } 159 | 160 | impl From for u8 { 161 | fn from(val: ControlCode) -> Self { 162 | use ControlCode::*; 163 | match val { 164 | Null => 0, 165 | StartOfHeading => 1, 166 | StartOfText => 2, 167 | EndOfText => 3, 168 | EndOfTransmission => 4, 169 | Enquiry => 5, 170 | Acknowledge => 6, 171 | Bell => 7, 172 | Backspace => 8, 173 | HorizontalTabulation => 9, 174 | LineFeed => 10, 175 | VerticalTabulation => 11, 176 | FormFeed => 12, 177 | CarriageReturn => 13, 178 | ShiftOut => 14, 179 | ShiftIn => 15, 180 | DataLinkEscape => 16, 181 | DeviceControl1 => 17, 182 | DeviceControl2 => 18, 183 | DeviceControl3 => 19, 184 | DeviceControl4 => 20, 185 | NegativeAcknowledge => 21, 186 | SynchronousIdle => 22, 187 | EndOfTransmissionBlock => 23, 188 | Cancel => 24, 189 | EndOfMedium => 25, 190 | Substitute => 26, 191 | Escape => 27, 192 | FileSeparator => 28, 193 | GroupSeparator => 29, 194 | RecordSeparator => 30, 195 | UnitSeparator => 31, 196 | Space => 32, 197 | Delete => 127, 198 | } 199 | } 200 | } 201 | 202 | impl TryFrom for ControlCode { 203 | type Error = (); 204 | 205 | fn try_from(c: char) -> Result { 206 | use ControlCode::*; 207 | match c { 208 | '@' => Ok(Null), 209 | 'A' | 'a' => Ok(StartOfHeading), 210 | 'B' | 'b' => Ok(StartOfText), 211 | 'C' | 'c' => Ok(EndOfText), 212 | 'D' | 'd' => Ok(EndOfTransmission), 213 | 'E' | 'e' => Ok(Enquiry), 214 | 'F' | 'f' => Ok(Acknowledge), 215 | 'G' | 'g' => Ok(Bell), 216 | 'H' | 'h' => Ok(Backspace), 217 | 'I' | 'i' => Ok(HorizontalTabulation), 218 | 'J' | 'j' => Ok(LineFeed), 219 | 'K' | 'k' => Ok(VerticalTabulation), 220 | 'L' | 'l' => Ok(FormFeed), 221 | 'M' | 'm' => Ok(CarriageReturn), 222 | 'N' | 'n' => Ok(ShiftOut), 223 | 'O' | 'o' => Ok(ShiftIn), 224 | 'P' | 'p' => Ok(DataLinkEscape), 225 | 'Q' | 'q' => Ok(DeviceControl1), 226 | 'R' | 'r' => Ok(DeviceControl2), 227 | 'S' | 's' => Ok(DeviceControl3), 228 | 'T' | 't' => Ok(DeviceControl4), 229 | 'U' | 'u' => Ok(NegativeAcknowledge), 230 | 'V' | 'v' => Ok(SynchronousIdle), 231 | 'W' | 'w' => Ok(EndOfTransmissionBlock), 232 | 'X' | 'x' => Ok(Cancel), 233 | 'Y' | 'y' => Ok(EndOfMedium), 234 | 'Z' | 'z' => Ok(Substitute), 235 | '[' => Ok(Escape), 236 | '\\' => Ok(FileSeparator), 237 | ']' => Ok(GroupSeparator), 238 | '^' => Ok(RecordSeparator), 239 | '_' => Ok(UnitSeparator), 240 | ' ' => Ok(Space), 241 | '?' => Ok(Delete), 242 | _ => Err(()), 243 | } 244 | } 245 | } 246 | 247 | impl TryFrom<&str> for ControlCode { 248 | type Error = (); 249 | 250 | fn try_from(c: &str) -> Result { 251 | use ControlCode::*; 252 | match c { 253 | "^@" => Ok(Null), 254 | "^A" => Ok(StartOfHeading), 255 | "^B" => Ok(StartOfText), 256 | "^C" => Ok(EndOfText), 257 | "^D" => Ok(EndOfTransmission), 258 | "^E" => Ok(Enquiry), 259 | "^F" => Ok(Acknowledge), 260 | "^G" => Ok(Bell), 261 | "^H" => Ok(Backspace), 262 | "^I" => Ok(HorizontalTabulation), 263 | "^J" => Ok(LineFeed), 264 | "^K" => Ok(VerticalTabulation), 265 | "^L" => Ok(FormFeed), 266 | "^M" => Ok(CarriageReturn), 267 | "^N" => Ok(ShiftOut), 268 | "^O" => Ok(ShiftIn), 269 | "^P" => Ok(DataLinkEscape), 270 | "^Q" => Ok(DeviceControl1), 271 | "^R" => Ok(DeviceControl2), 272 | "^S" => Ok(DeviceControl3), 273 | "^T" => Ok(DeviceControl4), 274 | "^U" => Ok(NegativeAcknowledge), 275 | "^V" => Ok(SynchronousIdle), 276 | "^W" => Ok(EndOfTransmissionBlock), 277 | "^X" => Ok(Cancel), 278 | "^Y" => Ok(EndOfMedium), 279 | "^Z" => Ok(Substitute), 280 | "^[" => Ok(Escape), 281 | "^\\" => Ok(FileSeparator), 282 | "^]" => Ok(GroupSeparator), 283 | "^^" => Ok(RecordSeparator), 284 | "^_" => Ok(UnitSeparator), 285 | "^ " => Ok(Space), 286 | "^?" => Ok(Delete), 287 | _ => Err(()), 288 | } 289 | } 290 | } 291 | 292 | impl AsRef for ControlCode { 293 | fn as_ref(&self) -> &str { 294 | use ControlCode::*; 295 | match self { 296 | Null => "^@", 297 | StartOfHeading => "^A", 298 | StartOfText => "^B", 299 | EndOfText => "^C", 300 | EndOfTransmission => "^D", 301 | Enquiry => "^E", 302 | Acknowledge => "^F", 303 | Bell => "^G", 304 | Backspace => "^H", 305 | HorizontalTabulation => "^I", 306 | LineFeed => "^J", 307 | VerticalTabulation => "^K", 308 | FormFeed => "^L", 309 | CarriageReturn => "^M", 310 | ShiftOut => "^N", 311 | ShiftIn => "^O", 312 | DataLinkEscape => "^P", 313 | DeviceControl1 => "^Q", 314 | DeviceControl2 => "^R", 315 | DeviceControl3 => "^S", 316 | DeviceControl4 => "^T", 317 | NegativeAcknowledge => "^U", 318 | SynchronousIdle => "^V", 319 | EndOfTransmissionBlock => "^W", 320 | Cancel => "^X", 321 | EndOfMedium => "^Y", 322 | Substitute => "^Z", 323 | Escape => "^[", 324 | FileSeparator => "^\\", 325 | GroupSeparator => "^]", 326 | RecordSeparator => "^^", 327 | UnitSeparator => "^_", 328 | Space => " ", 329 | Delete => "^?", 330 | } 331 | } 332 | } 333 | 334 | impl AsRef<[u8]> for ControlCode { 335 | fn as_ref(&self) -> &[u8] { 336 | use ControlCode::*; 337 | match self { 338 | Null => &[0], 339 | StartOfHeading => &[1], 340 | StartOfText => &[2], 341 | EndOfText => &[3], 342 | EndOfTransmission => &[4], 343 | Enquiry => &[5], 344 | Acknowledge => &[6], 345 | Bell => &[7], 346 | Backspace => &[8], 347 | HorizontalTabulation => &[9], 348 | LineFeed => &[10], 349 | VerticalTabulation => &[11], 350 | FormFeed => &[12], 351 | CarriageReturn => &[13], 352 | ShiftOut => &[14], 353 | ShiftIn => &[15], 354 | DataLinkEscape => &[16], 355 | DeviceControl1 => &[17], 356 | DeviceControl2 => &[18], 357 | DeviceControl3 => &[19], 358 | DeviceControl4 => &[20], 359 | NegativeAcknowledge => &[21], 360 | SynchronousIdle => &[22], 361 | EndOfTransmissionBlock => &[23], 362 | Cancel => &[24], 363 | EndOfMedium => &[25], 364 | Substitute => &[26], 365 | Escape => &[27], 366 | FileSeparator => &[28], 367 | GroupSeparator => &[29], 368 | RecordSeparator => &[30], 369 | UnitSeparator => &[31], 370 | Space => &[32], 371 | Delete => &[127], 372 | } 373 | } 374 | } 375 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::error; 2 | use std::fmt; 3 | use std::fmt::Display; 4 | use std::io; 5 | 6 | #[allow(variant_size_differences)] 7 | /// An main error type used in [crate]. 8 | #[derive(Debug)] 9 | pub enum Error { 10 | /// An Error in IO operation. 11 | IO(io::Error), 12 | /// An Error in command line parsing. 13 | CommandParsing, 14 | /// An Error in regex parsing. 15 | RegexParsing, 16 | /// An timeout was reached while waiting in expect call. 17 | ExpectTimeout, 18 | /// Unhandled EOF error. 19 | Eof, 20 | /// It maybe OS specific error or a general erorr. 21 | Other { 22 | /// The reason of the erorr. 23 | message: String, 24 | /// An underlying error message. 25 | err: String, 26 | }, 27 | } 28 | 29 | impl Error { 30 | #[cfg(unix)] 31 | pub(crate) fn unknown(message: impl Into, err: impl Into) -> Error { 32 | Self::Other { 33 | message: message.into(), 34 | err: err.into(), 35 | } 36 | } 37 | } 38 | 39 | impl Display for Error { 40 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 41 | match self { 42 | Error::IO(err) => write!(f, "IO error {}", err), 43 | Error::CommandParsing => write!(f, "Can't parse a command string, please check it out"), 44 | Error::RegexParsing => write!(f, "Can't parse a regex expression"), 45 | Error::ExpectTimeout => write!(f, "Reached a timeout for expect type of command"), 46 | Error::Eof => write!(f, "EOF was reached; the read may successed later"), 47 | Error::Other { message, err } => write!(f, "Unexpected error; {}; {}", message, err), 48 | } 49 | } 50 | } 51 | 52 | impl error::Error for Error {} 53 | 54 | impl From for Error { 55 | fn from(err: io::Error) -> Self { 56 | Self::IO(err) 57 | } 58 | } 59 | 60 | impl From for io::Error { 61 | fn from(err: Error) -> Self { 62 | io::Error::new(io::ErrorKind::Other, err.to_string()) 63 | } 64 | } 65 | 66 | pub(crate) fn to_io_error(message: &'static str) -> impl FnOnce(E) -> io::Error { 67 | move |e: E| io::Error::new(io::ErrorKind::Other, format!("{}; {}", message, e)) 68 | } 69 | -------------------------------------------------------------------------------- /src/expect.rs: -------------------------------------------------------------------------------- 1 | use crate::{Captures, Error, Needle}; 2 | 3 | /// Expect trait provides common expect functions. 4 | pub trait Expect { 5 | /// Expect waits until a pattern is matched. 6 | /// 7 | /// If the method returns [`Ok`] it is guaranteed that at least 1 match was found. 8 | /// 9 | /// The match algorthm can be either 10 | /// - gready 11 | /// - lazy 12 | /// 13 | /// You can set one via [`Session::set_expect_lazy`]. 14 | /// Default version is gready. 15 | /// 16 | /// The implications are. 17 | /// Imagine you use [`crate::Regex`] `"\d+"` to find a match. 18 | /// And your process outputs `123`. 19 | /// In case of lazy approach we will match `1`. 20 | /// Where's in case of gready one we will match `123`. 21 | /// 22 | /// # Example 23 | /// 24 | #[cfg_attr(any(windows, feature = "async"), doc = "```ignore")] 25 | #[cfg_attr(not(any(windows, feature = "async")), doc = "```")] 26 | /// use expectrl::{Expect, spawn, Regex}; 27 | /// 28 | /// let mut p = spawn("echo 123").unwrap(); 29 | /// let m = p.expect(Regex("\\d+")).unwrap(); 30 | /// assert_eq!(m.get(0).unwrap(), b"123"); 31 | /// ``` 32 | /// 33 | #[cfg_attr(any(windows, feature = "async"), doc = "```ignore")] 34 | #[cfg_attr(not(any(windows, feature = "async")), doc = "```")] 35 | /// use expectrl::{Expect, spawn, Regex}; 36 | /// 37 | /// let mut p = spawn("echo 123").unwrap(); 38 | /// p.set_expect_lazy(true); 39 | /// let m = p.expect(Regex("\\d+")).unwrap(); 40 | /// assert_eq!(m.get(0).unwrap(), b"1"); 41 | /// ``` 42 | /// 43 | /// This behaviour is different from [`Expect::check`]. 44 | /// 45 | /// It returns an error if timeout is reached. 46 | /// You can specify a timeout value by [`Session::set_expect_timeout`] method. 47 | /// 48 | /// [`Session::set_expect_timeout`]: crate::Session::set_expect_timeout 49 | /// [`Session::set_expect_lazy`]: crate::Session::set_expect_lazy 50 | fn expect(&mut self, needle: N) -> Result 51 | where 52 | N: Needle; 53 | 54 | /// Check verifies if a pattern is matched. 55 | /// Returns empty found structure if nothing found. 56 | /// 57 | /// Is a non blocking version of [`Expect::expect`]. 58 | /// But its strategy of matching is different from it. 59 | /// It makes search against all bytes available. 60 | /// 61 | /// # Example 62 | /// 63 | #[cfg_attr( 64 | any(windows, target_os = "macos", feature = "async"), 65 | doc = "```ignore" 66 | )] 67 | #[cfg_attr(not(any(windows, target_os = "macos", feature = "async")), doc = "```")] 68 | /// use expectrl::{spawn, Regex, Expect}; 69 | /// use std::time::Duration; 70 | /// 71 | /// let mut p = spawn("echo 123").unwrap(); 72 | /// # 73 | /// # // wait to guarantee that check echo worked out (most likely) 74 | /// # std::thread::sleep(Duration::from_millis(500)); 75 | /// # 76 | /// let m = p.check(Regex("\\d+")).unwrap(); 77 | /// assert_eq!(m.get(0).unwrap(), b"123"); 78 | /// ``` 79 | fn check(&mut self, needle: N) -> Result 80 | where 81 | N: Needle; 82 | 83 | /// The functions checks if a pattern is matched. 84 | /// It doesn’t consumes bytes from stream. 85 | /// 86 | /// Its strategy of matching is different from the one in [`Expect::expect`]. 87 | /// It makes search agains all bytes available. 88 | /// 89 | /// If you want to get a matched result [`Expect::check`] and [`Expect::expect`] is a better option. 90 | /// Because it is not guaranteed that [`Expect::check`] or [`Expect::expect`] with the same parameters: 91 | /// - will successed even right after Expect::is_matched call. 92 | /// - will operate on the same bytes. 93 | /// 94 | /// IMPORTANT: 95 | /// 96 | /// If you call this method with [`Eof`] pattern be aware that eof 97 | /// indication MAY be lost on the next interactions. 98 | /// It depends from a process you spawn. 99 | /// So it might be better to use [`Expect::check`] or [`Expect::expect`] with Eof. 100 | /// 101 | /// # Example 102 | /// 103 | #[cfg_attr(any(windows, feature = "async"), doc = "```ignore")] 104 | #[cfg_attr(not(any(windows, feature = "async")), doc = "```")] 105 | /// use expectrl::{spawn, Regex, Expect}; 106 | /// use std::time::Duration; 107 | /// 108 | /// let mut p = spawn("cat").unwrap(); 109 | /// p.send_line("123"); 110 | /// # // wait to guarantee that check echo worked out (most likely) 111 | /// # std::thread::sleep(Duration::from_secs(1)); 112 | /// let m = p.is_matched(Regex("\\d+")).unwrap(); 113 | /// assert_eq!(m, true); 114 | /// ``` 115 | /// 116 | /// [`Eof`]: crate::Eof 117 | fn is_matched(&mut self, needle: N) -> Result 118 | where 119 | N: Needle; 120 | 121 | /// Send buffer to the stream. 122 | /// 123 | /// You may also use methods from [std::io::Write] instead. 124 | /// 125 | /// # Example 126 | /// 127 | #[cfg_attr(any(windows, feature = "async"), doc = "```ignore")] 128 | #[cfg_attr(not(any(windows, feature = "async")), doc = "```")] 129 | /// use expectrl::{spawn, ControlCode, Expect}; 130 | /// 131 | /// let mut proc = spawn("cat").unwrap(); 132 | /// 133 | /// proc.send("Hello"); 134 | /// proc.send(b"World"); 135 | /// proc.send(ControlCode::try_from("^C").unwrap()); 136 | /// ``` 137 | fn send(&mut self, buf: B) -> Result<(), Error> 138 | where 139 | B: AsRef<[u8]>; 140 | 141 | /// Send line to the stream. 142 | /// 143 | /// # Example 144 | /// 145 | #[cfg_attr(any(windows, feature = "async"), doc = "```ignore")] 146 | #[cfg_attr(not(any(windows, feature = "async")), doc = "```")] 147 | /// use expectrl::{spawn, ControlCode, Expect}; 148 | /// 149 | /// let mut proc = spawn("cat").unwrap(); 150 | /// 151 | /// proc.send_line("Hello"); 152 | /// proc.send_line(b"World"); 153 | /// proc.send_line(ControlCode::try_from("^C").unwrap()); 154 | /// ``` 155 | fn send_line(&mut self, buf: B) -> Result<(), Error> 156 | where 157 | B: AsRef<[u8]>; 158 | } 159 | 160 | impl Expect for &mut T 161 | where 162 | T: Expect, 163 | { 164 | fn expect(&mut self, needle: N) -> Result 165 | where 166 | N: Needle, 167 | { 168 | T::expect(self, needle) 169 | } 170 | 171 | fn check(&mut self, needle: N) -> Result 172 | where 173 | N: Needle, 174 | { 175 | T::check(self, needle) 176 | } 177 | 178 | fn is_matched(&mut self, needle: N) -> Result 179 | where 180 | N: Needle, 181 | { 182 | T::is_matched(self, needle) 183 | } 184 | 185 | fn send(&mut self, buf: B) -> Result<(), Error> 186 | where 187 | B: AsRef<[u8]>, 188 | { 189 | T::send(self, buf) 190 | } 191 | 192 | fn send_line(&mut self, buf: B) -> Result<(), Error> 193 | where 194 | B: AsRef<[u8]>, 195 | { 196 | T::send_line(self, buf) 197 | } 198 | } 199 | 200 | #[cfg(feature = "async")] 201 | /// Expect trait provides common expect functions. 202 | pub trait AsyncExpect { 203 | /// Expect waits until a pattern is matched. 204 | /// 205 | /// If the method returns [Ok] it is guaranteed that at least 1 match was found. 206 | /// 207 | /// The match algorthm can be either 208 | /// - gready 209 | /// - lazy 210 | /// 211 | /// You can set one via [Session::set_expect_lazy]. 212 | /// Default version is gready. 213 | /// 214 | /// The implications are. 215 | /// 216 | /// Imagine you use [crate::Regex] `"\d+"` to find a match. 217 | /// And your process outputs `123`. 218 | /// In case of lazy approach we will match `1`. 219 | /// Where's in case of gready one we will match `123`. 220 | /// 221 | /// # Example 222 | /// 223 | #[cfg_attr(windows, doc = "```no_run")] 224 | #[cfg_attr(unix, doc = "```")] 225 | /// # futures_lite::future::block_on(async { 226 | /// use expectrl::{AsyncExpect, spawn, Regex}; 227 | /// 228 | /// let mut p = spawn("echo 123").unwrap(); 229 | /// let m = p.expect(Regex("\\d+")).await.unwrap(); 230 | /// assert_eq!(m.get(0).unwrap(), b"123"); 231 | /// # }); 232 | /// ``` 233 | /// 234 | #[cfg_attr(windows, doc = "```no_run")] 235 | #[cfg_attr(unix, doc = "```")] 236 | /// # futures_lite::future::block_on(async { 237 | /// use expectrl::{AsyncExpect, spawn, Regex}; 238 | /// 239 | /// let mut p = spawn("echo 123").unwrap(); 240 | /// p.set_expect_lazy(true); 241 | /// let m = p.expect(Regex("\\d+")).await.unwrap(); 242 | /// assert_eq!(m.get(0).unwrap(), b"1"); 243 | /// # }); 244 | /// ``` 245 | /// 246 | /// This behaviour is different from [Session::check]. 247 | /// 248 | /// It returns an error if timeout is reached. 249 | /// You can specify a timeout value by [Session::set_expect_timeout] method. 250 | async fn expect(&mut self, needle: N) -> Result 251 | where 252 | N: Needle; 253 | 254 | /// Check checks if a pattern is matched. 255 | /// Returns empty found structure if nothing found. 256 | /// 257 | /// Is a non blocking version of [Session::expect]. 258 | /// But its strategy of matching is different from it. 259 | /// It makes search agains all bytes available. 260 | /// 261 | #[cfg_attr(any(target_os = "macos", windows), doc = "```no_run")] 262 | #[cfg_attr(not(any(target_os = "macos", windows)), doc = "```")] 263 | /// # futures_lite::future::block_on(async { 264 | /// use expectrl::{AsyncExpect, spawn, Regex}; 265 | /// 266 | /// let mut p = spawn("echo 123").unwrap(); 267 | /// // wait to guarantee that check will successed (most likely) 268 | /// std::thread::sleep(std::time::Duration::from_secs(1)); 269 | /// let m = p.check(Regex("\\d+")).await.unwrap(); 270 | /// assert_eq!(m.get(0).unwrap(), b"123"); 271 | /// # }); 272 | /// ``` 273 | async fn check(&mut self, needle: N) -> Result 274 | where 275 | N: Needle; 276 | 277 | /// Is matched checks if a pattern is matched. 278 | /// It doesn't consumes bytes from stream. 279 | async fn is_matched(&mut self, needle: N) -> Result 280 | where 281 | N: Needle; 282 | 283 | /// Send text to child’s STDIN. 284 | /// 285 | /// You can also use methods from [std::io::Write] instead. 286 | /// 287 | /// # Example 288 | /// 289 | /// ``` 290 | /// use expectrl::{spawn, ControlCode, AsyncExpect}; 291 | /// 292 | /// let mut proc = spawn("cat").unwrap(); 293 | /// 294 | /// # futures_lite::future::block_on(async { 295 | /// proc.send("Hello").await; 296 | /// proc.send(b"World").await; 297 | /// proc.send(ControlCode::try_from("^C").unwrap()).await; 298 | /// # }); 299 | /// ``` 300 | async fn send(&mut self, buf: B) -> Result<(), Error> 301 | where 302 | B: AsRef<[u8]>; 303 | 304 | /// Send a line to child’s STDIN. 305 | /// 306 | /// # Example 307 | /// 308 | /// ``` 309 | /// use expectrl::{spawn, ControlCode, AsyncExpect}; 310 | /// 311 | /// let mut proc = spawn("cat").unwrap(); 312 | /// 313 | /// # futures_lite::future::block_on(async { 314 | /// proc.send_line("Hello").await; 315 | /// proc.send_line(b"World").await; 316 | /// proc.send_line(ControlCode::try_from("^C").unwrap()).await; 317 | /// # }); 318 | /// ``` 319 | async fn send_line(&mut self, buf: B) -> Result<(), Error> 320 | where 321 | B: AsRef<[u8]>; 322 | } 323 | 324 | #[cfg(feature = "async")] 325 | impl AsyncExpect for &mut T 326 | where 327 | T: AsyncExpect, 328 | { 329 | async fn expect(&mut self, needle: N) -> Result 330 | where 331 | N: Needle, 332 | { 333 | T::expect(self, needle).await 334 | } 335 | 336 | async fn check(&mut self, needle: N) -> Result 337 | where 338 | N: Needle, 339 | { 340 | T::check(self, needle).await 341 | } 342 | 343 | async fn is_matched(&mut self, needle: N) -> Result 344 | where 345 | N: Needle, 346 | { 347 | T::is_matched(self, needle).await 348 | } 349 | 350 | async fn send(&mut self, buf: B) -> Result<(), Error> 351 | where 352 | B: AsRef<[u8]>, 353 | { 354 | T::send(self, buf).await 355 | } 356 | 357 | async fn send_line(&mut self, buf: B) -> Result<(), Error> 358 | where 359 | B: AsRef<[u8]>, 360 | { 361 | T::send_line(self, buf).await 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /src/interact/actions/lookup.rs: -------------------------------------------------------------------------------- 1 | //! The module contains [`Lookup`]. 2 | 3 | use crate::{Captures, Error, Needle}; 4 | 5 | /// A helper action for an [`InteractSession`] 6 | /// 7 | /// It holds a buffer to be able to run different checks using [`Needle`], 8 | /// via [`Lookup::on`]. 9 | /// 10 | /// [`InteractSession`]: crate::interact::InteractSession 11 | #[derive(Debug, Clone)] 12 | pub struct Lookup { 13 | buf: Vec, 14 | } 15 | 16 | impl Lookup { 17 | /// Create a lookup object. 18 | pub fn new() -> Self { 19 | Self { buf: Vec::new() } 20 | } 21 | 22 | /// Checks whethere the buffer will be matched and returns [`Captures`] in such case. 23 | pub fn on(&mut self, buf: &[u8], eof: bool, pattern: N) -> Result, Error> 24 | where 25 | N: Needle, 26 | { 27 | self.buf.extend(buf); 28 | check(&mut self.buf, pattern, eof) 29 | } 30 | 31 | /// Cleans internal buffer. 32 | /// 33 | /// So the next [`Lookup::on`] can be called with no other data involved. 34 | pub fn clear(&mut self) { 35 | self.buf.clear(); 36 | } 37 | } 38 | 39 | impl Default for Lookup { 40 | fn default() -> Self { 41 | Self::new() 42 | } 43 | } 44 | 45 | fn check(buf: &mut Vec, needle: N, eof: bool) -> Result, Error> 46 | where 47 | N: Needle, 48 | { 49 | // we ignore the check if buf is empty in just in case someone is matching 0 bytes. 50 | 51 | let found = needle.check(buf, eof)?; 52 | if found.is_empty() { 53 | return Ok(None); 54 | } 55 | 56 | let end_index = Captures::right_most_index(&found); 57 | let involved_bytes = buf[..end_index].to_vec(); 58 | let found = Captures::new(involved_bytes, found); 59 | let _ = buf.drain(..end_index); 60 | 61 | Ok(Some(found)) 62 | } 63 | -------------------------------------------------------------------------------- /src/interact/actions/mod.rs: -------------------------------------------------------------------------------- 1 | //! The module contains a list of helpers for callbacks in [`InteractSession`] 2 | //! 3 | //! [`InteractSession`]: crate::interact::InteractSession 4 | 5 | pub mod lookup; 6 | -------------------------------------------------------------------------------- /src/interact/context.rs: -------------------------------------------------------------------------------- 1 | /// Context provides an interface to use a [`Session`], IO streams 2 | /// and a state. 3 | /// 4 | /// It's used primarily in callbacks for [`InteractSession`]. 5 | /// 6 | /// [`InteractSession`]: crate::interact::InteractSession 7 | /// [`Session`]: crate::session::Session 8 | #[derive(Debug)] 9 | pub struct Context<'a, Session, Input, Output, State> { 10 | /// The field contains a &mut reference to a [`Session`]. 11 | /// 12 | /// [`Session`]: crate::session::Session 13 | pub session: &'a mut Session, 14 | /// The field contains an input structure which was used in [`InteractSession`]. 15 | /// 16 | /// [`InteractSession`]: crate::interact::InteractSession 17 | pub input: &'a mut Input, 18 | /// The field contains an output structure which was used in [`InteractSession`]. 19 | /// 20 | /// [`InteractSession`]: crate::interact::InteractSession 21 | pub output: &'a mut Output, 22 | /// The field contains a user defined data. 23 | pub state: &'a mut State, 24 | /// The field contains a bytes which were consumed from a user or the running process. 25 | pub buf: &'a [u8], 26 | /// A flag for EOF of a user session or running process. 27 | pub eof: bool, 28 | } 29 | 30 | impl<'a, Session, Input, Output, State> Context<'a, Session, Input, Output, State> { 31 | /// Creates a new [`Context`] structure. 32 | pub fn new( 33 | session: &'a mut Session, 34 | input: &'a mut Input, 35 | output: &'a mut Output, 36 | state: &'a mut State, 37 | buf: &'a [u8], 38 | eof: bool, 39 | ) -> Self { 40 | Self { 41 | session, 42 | input, 43 | output, 44 | buf, 45 | eof, 46 | state, 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/interact/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module contains a routines for running and utilizing an interacting session with a [`Session`]. 2 | //! 3 | #![cfg_attr(all(unix, not(feature = "async")), doc = "```no_run")] 4 | #![cfg_attr(not(all(unix, not(feature = "async"))), doc = "```ignore")] 5 | //! use expectrl::{ 6 | //! interact::actions::lookup::Lookup, 7 | //! spawn, stream::stdin::Stdin, Regex 8 | //! }; 9 | //! 10 | //! #[derive(Debug)] 11 | //! enum Answer { 12 | //! Yes, 13 | //! No, 14 | //! Unrecognized, 15 | //! } 16 | //! 17 | //! let mut session = spawn("cat").expect("Can't spawn a session"); 18 | //! 19 | //! let mut input_action = Lookup::new(); 20 | //! 21 | //! let mut stdin = Stdin::open().unwrap(); 22 | //! let stdout = std::io::stdout(); 23 | //! 24 | //! let mut term = session.interact(&mut stdin, stdout).with_state(Answer::Unrecognized); 25 | //! term.set_input_action(move |mut ctx| { 26 | //! let m = input_action.on(ctx.buf, ctx.eof, "yes")?; 27 | //! if m.is_some() { 28 | //! *ctx.state = Answer::Yes; 29 | //! }; 30 | //! 31 | //! let m = input_action.on(ctx.buf, ctx.eof, "no")?; 32 | //! if m.is_some() { 33 | //! *ctx.state = Answer::No; 34 | //! }; 35 | //! 36 | //! Ok(false) 37 | //! }); 38 | //! 39 | //! term.spawn().expect("Failed to run an interact session"); 40 | //! 41 | //! let answer = term.into_state(); 42 | //! 43 | //! stdin.close().unwrap(); 44 | //! 45 | //! println!("It was said {:?}", answer); 46 | //! ``` 47 | //! 48 | //! [`Session`]: crate::session::Session 49 | 50 | pub mod actions; 51 | mod context; 52 | mod session; 53 | 54 | pub use context::Context; 55 | pub use session::InteractSession; 56 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn( 2 | missing_docs, 3 | future_incompatible, 4 | single_use_lifetimes, 5 | trivial_casts, 6 | trivial_numeric_casts, 7 | unreachable_pub, 8 | unused_extern_crates, 9 | unused_import_braces, 10 | unused_qualifications, 11 | unused_results, 12 | unused_variables, 13 | variant_size_differences, 14 | missing_debug_implementations, 15 | rust_2018_idioms 16 | )] 17 | #![allow(clippy::uninlined_format_args)] 18 | 19 | //! # A tool for automating terminal applications on alike original expect. 20 | //! 21 | //! Using the library you can: 22 | //! 23 | //! - Spawn process 24 | //! - Control process 25 | //! - Interact with process's IO(input/output). 26 | //! 27 | //! `expectrl` like original `expect` may shine when you're working with interactive applications. 28 | //! If your application is not interactive you may not find the library the best choise. 29 | //! 30 | //! ## Feature flags 31 | //! 32 | //! - `async`: Enables a async/await public API. 33 | //! - `polling`: Enables polling backend in interact session. Be cautious to use it on windows. 34 | //! 35 | //! ## Examples 36 | //! 37 | //! ### An example for interacting via ftp. 38 | //! 39 | //! ```no_run,ignore 40 | //! use expectrl::{spawn, Regex, Eof, WaitStatus}; 41 | //! 42 | //! let mut p = spawn("ftp speedtest.tele2.net").unwrap(); 43 | //! p.expect(Regex("Name \\(.*\\):")).unwrap(); 44 | //! p.send_line("anonymous").unwrap(); 45 | //! p.expect("Password").unwrap(); 46 | //! p.send_line("test").unwrap(); 47 | //! p.expect("ftp>").unwrap(); 48 | //! p.send_line("cd upload").unwrap(); 49 | //! p.expect("successfully changed.\r\nftp>").unwrap(); 50 | //! p.send_line("pwd").unwrap(); 51 | //! p.expect(Regex("[0-9]+ \"/upload\"")).unwrap(); 52 | //! p.send_line("exit").unwrap(); 53 | //! p.expect(Eof).unwrap(); 54 | //! assert_eq!(p.wait().unwrap(), WaitStatus::Exited(p.pid(), 0)); 55 | //! ``` 56 | //! 57 | //! *The example inspired by the one in [philippkeller/rexpect].* 58 | //! 59 | //! ### An example when `Command` is used. 60 | //! 61 | //! ```no_run,ignore 62 | //! use std::{process::Command, io::prelude::*}; 63 | //! use expectrl::Session; 64 | //! 65 | //! let mut echo_hello = Command::new("sh"); 66 | //! echo_hello.arg("-c").arg("echo hello"); 67 | //! 68 | //! let mut p = Session::spawn(echo_hello).unwrap(); 69 | //! p.expect("hello").unwrap(); 70 | //! ``` 71 | //! 72 | //! ### An example of logging. 73 | //! 74 | //! ```no_run,ignore 75 | //! use std::io::{stdout, prelude::*}; 76 | //! use expectrl::{spawn, session::log}; 77 | //! 78 | //! let mut sh = log(spawn("sh").unwrap(), stdout()).unwrap(); 79 | //! 80 | //! writeln!(sh, "Hello World").unwrap(); 81 | //! ``` 82 | //! 83 | //! ### An example of `async` feature. 84 | //! 85 | //! You need to provide a `features=["async"]` flag to use it. 86 | //! 87 | //! ```no_run,ignore 88 | //! use expectrl::spawn; 89 | //! 90 | //! let mut p = spawn("cat").await.unwrap(); 91 | //! p.expect("hello").await.unwrap(); 92 | //! ``` 93 | //! 94 | //! ### An example of interact session with `STDIN` and `STDOUT` 95 | //! 96 | //! ```no_run,ignore 97 | //! use expectrl::{spawn, stream::stdin::Stdin}; 98 | //! use std::io::stdout; 99 | //! 100 | //! let mut sh = spawn("cat").expect("Failed to spawn a 'cat' process"); 101 | //! 102 | //! let mut stdin = Stdin::open().expect("Failed to create stdin"); 103 | //! 104 | //! sh.interact(&mut stdin, stdout()) 105 | //! .spawn() 106 | //! .expect("Failed to start interact session"); 107 | //! 108 | //! stdin.close().expect("Failed to close a stdin"); 109 | //! ``` 110 | //! 111 | //! [For more examples, check the examples directory.](https://github.com/zhiburt/expectrl/tree/main/examples) 112 | 113 | mod captures; 114 | mod check_macros; 115 | mod control_code; 116 | mod error; 117 | mod expect; 118 | mod needle; 119 | 120 | #[cfg(all(windows, feature = "polling"))] 121 | mod waiter; 122 | 123 | pub mod interact; 124 | pub mod process; 125 | pub mod repl; 126 | pub mod session; 127 | pub mod stream; 128 | 129 | pub use captures::Captures; 130 | pub use control_code::ControlCode; 131 | pub use error::Error; 132 | pub use needle::{Any, Eof, NBytes, Needle, Regex}; 133 | 134 | pub use expect::Expect; 135 | pub use session::Session; 136 | 137 | #[cfg(feature = "async")] 138 | pub use expect::AsyncExpect; 139 | 140 | use session::OsSession; 141 | 142 | /// Spawn spawnes a new session. 143 | /// 144 | /// It accepts a command and possibly arguments just as string. 145 | /// It doesn't parses ENV variables. For complex constrictions use [`Session::spawn`]. 146 | /// 147 | /// # Example 148 | /// 149 | /// ```no_run,ignore 150 | /// use std::{thread, time::Duration, io::{Read, Write}}; 151 | /// use expectrl::{spawn, ControlCode}; 152 | /// 153 | /// let mut p = spawn("cat").unwrap(); 154 | /// p.send_line("Hello World").unwrap(); 155 | /// 156 | /// thread::sleep(Duration::from_millis(300)); // give 'cat' some time to set up 157 | /// p.send(ControlCode::EndOfText).unwrap(); // abort: SIGINT 158 | /// 159 | /// let mut buf = String::new(); 160 | /// p.read_to_string(&mut buf).unwrap(); 161 | /// 162 | /// assert_eq!(buf, "Hello World\r\n"); 163 | /// ``` 164 | /// 165 | /// [`Session::spawn`]: ./struct.Session.html?#spawn 166 | pub fn spawn(cmd: S) -> Result 167 | where 168 | S: AsRef, 169 | { 170 | Session::spawn_cmd(cmd.as_ref()) 171 | } 172 | -------------------------------------------------------------------------------- /src/needle.rs: -------------------------------------------------------------------------------- 1 | //! A module which contains a [Needle] trait and a list of implementations. 2 | //! [Needle] is used for seach in a byte stream. 3 | //! 4 | //! The list of provided implementations can be found in the documentation. 5 | 6 | use crate::error::Error; 7 | 8 | /// Needle an interface for search of a match in a buffer. 9 | pub trait Needle { 10 | /// Function returns all matches that were occured. 11 | fn check(&self, buf: &[u8], eof: bool) -> Result, Error>; 12 | } 13 | 14 | /// Match structure represent a range of bytes where match was found. 15 | #[derive(Debug, Clone, PartialEq, Eq)] 16 | pub struct Match { 17 | start: usize, 18 | end: usize, 19 | } 20 | 21 | impl Match { 22 | /// New construct's an intanse of a Match. 23 | pub fn new(start: usize, end: usize) -> Self { 24 | Self { start, end } 25 | } 26 | 27 | /// Start returns a start index of a match. 28 | pub fn start(&self) -> usize { 29 | self.start 30 | } 31 | 32 | /// End returns an end index of a match. 33 | pub fn end(&self) -> usize { 34 | self.end 35 | } 36 | } 37 | 38 | impl From> for Match { 39 | fn from(m: regex::bytes::Match<'_>) -> Self { 40 | Self::new(m.start(), m.end()) 41 | } 42 | } 43 | 44 | /// Regex tries to look up a match by a regex. 45 | #[derive(Debug)] 46 | pub struct Regex>(pub Re); 47 | 48 | impl> Needle for Regex { 49 | fn check(&self, buf: &[u8], _: bool) -> Result, Error> { 50 | let regex = regex::bytes::Regex::new(self.0.as_ref()).map_err(|_| Error::RegexParsing)?; 51 | let matches = regex 52 | .captures_iter(buf) 53 | .flat_map(|c| c.iter().flatten().map(|m| m.into()).collect::>()) 54 | .collect(); 55 | Ok(matches) 56 | } 57 | } 58 | 59 | /// Eof consider a match when an EOF is reached. 60 | #[derive(Debug)] 61 | pub struct Eof; 62 | 63 | impl Needle for Eof { 64 | fn check(&self, buf: &[u8], eof: bool) -> Result, Error> { 65 | match eof { 66 | true => Ok(vec![Match::new(0, buf.len())]), 67 | false => Ok(Vec::new()), 68 | } 69 | } 70 | } 71 | 72 | /// NBytes matches N bytes from the stream. 73 | #[derive(Debug)] 74 | pub struct NBytes(pub usize); 75 | 76 | impl NBytes { 77 | fn count(&self) -> usize { 78 | self.0 79 | } 80 | } 81 | 82 | impl Needle for NBytes { 83 | fn check(&self, buf: &[u8], _: bool) -> Result, Error> { 84 | match buf.len() >= self.count() { 85 | true => Ok(vec![Match::new(0, self.count())]), 86 | false => Ok(Vec::new()), 87 | } 88 | } 89 | } 90 | 91 | impl Needle for [u8] { 92 | fn check(&self, buf: &[u8], _: bool) -> Result, Error> { 93 | if buf.len() < self.len() { 94 | return Ok(Vec::new()); 95 | } 96 | 97 | for l_bound in 0..buf.len() { 98 | let r_bound = l_bound + self.len(); 99 | if r_bound > buf.len() { 100 | return Ok(Vec::new()); 101 | } 102 | 103 | if self == &buf[l_bound..r_bound] { 104 | return Ok(vec![Match::new(l_bound, r_bound)]); 105 | } 106 | } 107 | 108 | Ok(Vec::new()) 109 | } 110 | } 111 | 112 | impl Needle for &[u8] { 113 | fn check(&self, buf: &[u8], eof: bool) -> Result, Error> { 114 | (*self).check(buf, eof) 115 | } 116 | } 117 | 118 | impl Needle for str { 119 | fn check(&self, buf: &[u8], eof: bool) -> Result, Error> { 120 | self.as_bytes().check(buf, eof) 121 | } 122 | } 123 | 124 | impl Needle for &str { 125 | fn check(&self, buf: &[u8], eof: bool) -> Result, Error> { 126 | self.as_bytes().check(buf, eof) 127 | } 128 | } 129 | 130 | impl Needle for String { 131 | fn check(&self, buf: &[u8], eof: bool) -> Result, Error> { 132 | self.as_bytes().check(buf, eof) 133 | } 134 | } 135 | 136 | impl Needle for u8 { 137 | fn check(&self, buf: &[u8], eof: bool) -> Result, Error> { 138 | ([*self][..]).check(buf, eof) 139 | } 140 | } 141 | 142 | impl Needle for char { 143 | fn check(&self, buf: &[u8], eof: bool) -> Result, Error> { 144 | char::to_string(self).check(buf, eof) 145 | } 146 | } 147 | 148 | /// Any matches uses all provided lookups and returns a match 149 | /// from a first successfull match. 150 | /// 151 | /// It does checks lookups in order they were provided. 152 | /// 153 | /// # Example 154 | /// 155 | /// ```no_run,ignore 156 | /// use expectrl::{spawn, Any}; 157 | /// 158 | /// let mut p = spawn("cat").unwrap(); 159 | /// p.expect(Any(["we", "are", "here"])).unwrap(); 160 | /// ``` 161 | /// 162 | /// To be able to combine different types of lookups you can call [Any::boxed]. 163 | /// 164 | /// ```no_run,ignore 165 | /// use expectrl::{spawn, Any, NBytes}; 166 | /// 167 | /// let mut p = spawn("cat").unwrap(); 168 | /// p.expect(Any::boxed(vec![Box::new("we"), Box::new(NBytes(3))])).unwrap(); 169 | /// ``` 170 | #[derive(Debug)] 171 | pub struct Any(pub I); 172 | 173 | impl Any>> { 174 | /// Boxed expectes a list of [Box]ed lookups. 175 | pub fn boxed(v: Vec>) -> Self { 176 | Self(v) 177 | } 178 | } 179 | 180 | impl Needle for Any<&[T]> 181 | where 182 | T: Needle, 183 | { 184 | fn check(&self, buf: &[u8], eof: bool) -> Result, Error> { 185 | for needle in self.0.iter() { 186 | let found = needle.check(buf, eof)?; 187 | if !found.is_empty() { 188 | return Ok(found); 189 | } 190 | } 191 | 192 | Ok(Vec::new()) 193 | } 194 | } 195 | 196 | impl Needle for Any> 197 | where 198 | T: Needle, 199 | { 200 | fn check(&self, buf: &[u8], eof: bool) -> Result, Error> { 201 | Any(self.0.as_slice()).check(buf, eof) 202 | } 203 | } 204 | 205 | impl Needle for Any<[T; N]> 206 | where 207 | T: Needle, 208 | { 209 | fn check(&self, buf: &[u8], eof: bool) -> Result, Error> { 210 | Any(&self.0[..]).check(buf, eof) 211 | } 212 | } 213 | 214 | impl Needle for Any<&'_ [T; N]> 215 | where 216 | T: Needle, 217 | { 218 | fn check(&self, buf: &[u8], eof: bool) -> Result, Error> { 219 | Any(&self.0[..]).check(buf, eof) 220 | } 221 | } 222 | 223 | impl Needle for &T { 224 | fn check(&self, buf: &[u8], eof: bool) -> Result, Error> { 225 | T::check(self, buf, eof) 226 | } 227 | } 228 | 229 | impl Needle for Box { 230 | fn check(&self, buf: &[u8], eof: bool) -> Result, Error> { 231 | self.as_ref().check(buf, eof) 232 | } 233 | } 234 | 235 | #[cfg(test)] 236 | mod tests { 237 | use super::*; 238 | 239 | #[test] 240 | fn test_regex() { 241 | assert_eq!( 242 | Regex("[0-9]+").check(b"+012345", false).unwrap(), 243 | vec![Match::new(1, 7)] 244 | ); 245 | assert_eq!( 246 | Regex(r"\w+").check(b"What's Up Boys", false).unwrap(), 247 | vec![ 248 | Match::new(0, 4), 249 | Match::new(5, 6), 250 | Match::new(7, 9), 251 | Match::new(10, 14) 252 | ] 253 | ); 254 | assert_eq!( 255 | Regex(r"((?:\w|')+)") 256 | .check(b"What's Up Boys", false) 257 | .unwrap(), 258 | vec![ 259 | Match::new(0, 6), 260 | Match::new(0, 6), 261 | Match::new(7, 9), 262 | Match::new(7, 9), 263 | Match::new(10, 14), 264 | Match::new(10, 14) 265 | ] 266 | ); 267 | assert_eq!( 268 | Regex(r"(\w+)=(\w+)").check(b"asd=123", false).unwrap(), 269 | vec![Match::new(0, 7), Match::new(0, 3), Match::new(4, 7)] 270 | ); 271 | } 272 | 273 | #[test] 274 | fn test_eof() { 275 | assert_eq!(Eof.check(b"qwe", true).unwrap(), vec![Match::new(0, 3)]); 276 | assert_eq!(Eof.check(b"qwe", false).unwrap(), vec![]); 277 | } 278 | 279 | #[test] 280 | fn test_n_bytes() { 281 | assert_eq!( 282 | NBytes(1).check(b"qwe", false).unwrap(), 283 | vec![Match::new(0, 1)] 284 | ); 285 | assert_eq!( 286 | NBytes(0).check(b"qwe", false).unwrap(), 287 | vec![Match::new(0, 0)] 288 | ); 289 | assert_eq!(NBytes(10).check(b"qwe", false).unwrap(), vec![]); 290 | } 291 | 292 | #[test] 293 | fn test_str() { 294 | assert_eq!( 295 | "wer".check(b"qwerty", false).unwrap(), 296 | vec![Match::new(1, 4)] 297 | ); 298 | assert_eq!("123".check(b"qwerty", false).unwrap(), vec![]); 299 | assert_eq!("".check(b"qwerty", false).unwrap(), vec![Match::new(0, 0)]); 300 | } 301 | 302 | #[test] 303 | fn test_bytes() { 304 | assert_eq!( 305 | b"wer".check(b"qwerty", false).unwrap(), 306 | vec![Match::new(1, 4)] 307 | ); 308 | assert_eq!(b"123".check(b"qwerty", false).unwrap(), vec![]); 309 | assert_eq!(b"".check(b"qwerty", false).unwrap(), vec![Match::new(0, 0)]); 310 | } 311 | 312 | #[allow(clippy::needless_borrow)] 313 | #[test] 314 | #[allow(clippy::needless_borrow)] 315 | fn test_bytes_ref() { 316 | assert_eq!( 317 | (&[b'q', b'w', b'e']).check(b"qwerty", false).unwrap(), 318 | vec![Match::new(0, 3)] 319 | ); 320 | assert_eq!( 321 | (&[b'1', b'2', b'3']).check(b"qwerty", false).unwrap(), 322 | vec![] 323 | ); 324 | assert_eq!( 325 | (&[]).check(b"qwerty", false).unwrap(), 326 | vec![Match::new(0, 0)] 327 | ); 328 | } 329 | 330 | #[test] 331 | fn test_byte() { 332 | assert_eq!( 333 | (b'3').check(b"1234", false).unwrap(), 334 | vec![Match::new(2, 3)] 335 | ); 336 | assert_eq!((b'3').check(b"1234", true).unwrap(), vec![Match::new(2, 3)]); 337 | } 338 | 339 | #[test] 340 | fn test_char() { 341 | for eof in [false, true] { 342 | assert_eq!( 343 | ('😘').check("😁😄😅😓😠😘😌".as_bytes(), eof).unwrap(), 344 | vec![Match::new(20, 24)] 345 | ); 346 | } 347 | } 348 | 349 | #[test] 350 | fn test_any() { 351 | assert_eq!( 352 | Any::>>(vec![Box::new("we"), Box::new(NBytes(3))]) 353 | .check(b"qwerty", false) 354 | .unwrap(), 355 | vec![Match::new(1, 3)] 356 | ); 357 | assert_eq!( 358 | Any::boxed(vec![Box::new("123"), Box::new(NBytes(100))]) 359 | .check(b"qwerty", false) 360 | .unwrap(), 361 | vec![], 362 | ); 363 | assert_eq!( 364 | Any(["123", "234", "rty"]).check(b"qwerty", false).unwrap(), 365 | vec![Match::new(3, 6)] 366 | ); 367 | assert_eq!( 368 | Any(&["123", "234", "rty"][..]) 369 | .check(b"qwerty", false) 370 | .unwrap(), 371 | vec![Match::new(3, 6)] 372 | ); 373 | assert_eq!( 374 | Any(&["123", "234", "rty"]).check(b"qwerty", false).unwrap(), 375 | vec![Match::new(3, 6)] 376 | ); 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /src/process/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module contains a platform independent abstraction over an os process. 2 | 3 | use std::io::Result; 4 | 5 | #[cfg(unix)] 6 | pub mod unix; 7 | #[cfg(windows)] 8 | pub mod windows; 9 | 10 | /// This trait represents a platform independent process which runs a program. 11 | pub trait Process: Sized { 12 | /// A command which process can run. 13 | type Command; 14 | /// A representation of IO stream of communication with a programm a process is running. 15 | type Stream; 16 | 17 | /// Spawn parses a given string as a commandline string and spawns it on a process. 18 | fn spawn(cmd: S) -> Result 19 | where 20 | S: AsRef; 21 | /// Spawn_command runs a process with a given command. 22 | fn spawn_command(command: Self::Command) -> Result; 23 | /// It opens a IO stream with a spawned process. 24 | fn open_stream(&mut self) -> Result; 25 | } 26 | 27 | #[allow(clippy::wrong_self_convention)] 28 | /// Healthcheck represents a check by which we can determine if a spawned process is still alive. 29 | pub trait Healthcheck { 30 | /// A status healthcheck can return. 31 | type Status; 32 | 33 | /// The function returns a status of a process if it still alive and it can operate. 34 | fn get_status(&self) -> Result; 35 | 36 | /// The function returns a status of a process if it still alive and it can operate. 37 | fn is_alive(&self) -> Result; 38 | } 39 | 40 | impl Healthcheck for &T 41 | where 42 | T: Healthcheck, 43 | { 44 | type Status = T::Status; 45 | 46 | fn get_status(&self) -> Result { 47 | T::get_status(self) 48 | } 49 | 50 | fn is_alive(&self) -> Result { 51 | T::is_alive(self) 52 | } 53 | } 54 | 55 | impl Healthcheck for &mut T 56 | where 57 | T: Healthcheck, 58 | { 59 | type Status = T::Status; 60 | 61 | fn get_status(&self) -> Result { 62 | T::get_status(self) 63 | } 64 | 65 | fn is_alive(&self) -> Result { 66 | T::is_alive(self) 67 | } 68 | } 69 | 70 | /// NonBlocking interface represens a [std::io::Read]er which can be turned in a non blocking mode 71 | /// so its read operations will return imideately. 72 | pub trait NonBlocking { 73 | /// Sets a [std::io::Read]er into a non/blocking mode. 74 | fn set_blocking(&mut self, on: bool) -> Result<()>; 75 | } 76 | 77 | impl NonBlocking for &mut T 78 | where 79 | T: NonBlocking, 80 | { 81 | fn set_blocking(&mut self, on: bool) -> Result<()> { 82 | T::set_blocking(self, on) 83 | } 84 | } 85 | 86 | /// Terminal configuration trait, used for IO configuration. 87 | pub trait Termios { 88 | /// Verifies whether a [`std::io::Write`] will be repeated in output stream and be read by [`std::io::Read`]. 89 | fn is_echo(&self) -> Result; 90 | /// Configure a echo logic. 91 | fn set_echo(&mut self, on: bool) -> Result; 92 | } 93 | 94 | impl Termios for &mut T 95 | where 96 | T: Termios, 97 | { 98 | fn is_echo(&self) -> Result { 99 | T::is_echo(self) 100 | } 101 | 102 | fn set_echo(&mut self, on: bool) -> Result { 103 | T::set_echo(self, on) 104 | } 105 | } 106 | 107 | #[cfg(feature = "async")] 108 | /// IntoAsyncStream interface turns a [Process::Stream] into an async version. 109 | /// To be used with `async`/`await`syntax 110 | pub trait IntoAsyncStream { 111 | /// AsyncStream type. 112 | /// Like [Process::Stream] but it represents an async IO stream. 113 | type AsyncStream; 114 | 115 | /// Turns an object into a async stream. 116 | fn into_async_stream(self) -> Result; 117 | } 118 | -------------------------------------------------------------------------------- /src/process/unix.rs: -------------------------------------------------------------------------------- 1 | //! This module contains a Unix implementation of [crate::process::Process]. 2 | 3 | use std::{ 4 | io::{self, ErrorKind, Read, Result, Write}, 5 | ops::{Deref, DerefMut}, 6 | os::unix::prelude::{AsRawFd, RawFd}, 7 | process::Command, 8 | }; 9 | 10 | use crate::{ 11 | error::to_io_error, 12 | process::{Healthcheck, NonBlocking, Process, Termios}, 13 | }; 14 | 15 | use ptyprocess::{errno::Errno, stream::Stream, PtyProcess}; 16 | 17 | #[cfg(feature = "async")] 18 | use super::IntoAsyncStream; 19 | #[cfg(feature = "async")] 20 | use futures_lite::{AsyncRead, AsyncWrite}; 21 | #[cfg(feature = "async")] 22 | use std::{ 23 | pin::Pin, 24 | task::{Context, Poll}, 25 | }; 26 | 27 | pub use ptyprocess::{Signal, WaitStatus}; 28 | 29 | /// A Unix representation of a [Process] via [PtyProcess] 30 | #[derive(Debug)] 31 | pub struct UnixProcess { 32 | proc: PtyProcess, 33 | } 34 | 35 | impl Process for UnixProcess { 36 | type Command = Command; 37 | type Stream = PtyStream; 38 | 39 | fn spawn(cmd: S) -> Result 40 | where 41 | S: AsRef, 42 | { 43 | let args = tokenize_command(cmd.as_ref()); 44 | if args.is_empty() { 45 | return Err(io_error("failed to parse a command")); 46 | } 47 | 48 | let mut command = std::process::Command::new(&args[0]); 49 | let _ = command.args(args.iter().skip(1)); 50 | 51 | Self::spawn_command(command) 52 | } 53 | 54 | fn spawn_command(command: Self::Command) -> Result { 55 | let proc = PtyProcess::spawn(command).map_err(to_io_error("Failed to spawn a command"))?; 56 | 57 | Ok(Self { proc }) 58 | } 59 | 60 | fn open_stream(&mut self) -> Result { 61 | let stream = self 62 | .proc 63 | .get_pty_stream() 64 | .map_err(to_io_error("Failed to create a stream"))?; 65 | let stream = PtyStream::new(stream); 66 | Ok(stream) 67 | } 68 | } 69 | 70 | impl Healthcheck for UnixProcess { 71 | type Status = WaitStatus; 72 | 73 | fn get_status(&self) -> Result { 74 | get_status(&self.proc) 75 | } 76 | 77 | fn is_alive(&self) -> Result { 78 | self.proc 79 | .is_alive() 80 | .map_err(to_io_error("failed to determine if process is alive")) 81 | } 82 | } 83 | 84 | impl Termios for UnixProcess { 85 | fn is_echo(&self) -> Result { 86 | let value = self.proc.get_echo()?; 87 | 88 | Ok(value) 89 | } 90 | 91 | fn set_echo(&mut self, on: bool) -> Result { 92 | let value = self.proc.set_echo(on, None)?; 93 | 94 | Ok(value) 95 | } 96 | } 97 | 98 | impl Deref for UnixProcess { 99 | type Target = PtyProcess; 100 | 101 | fn deref(&self) -> &Self::Target { 102 | &self.proc 103 | } 104 | } 105 | 106 | impl DerefMut for UnixProcess { 107 | fn deref_mut(&mut self) -> &mut Self::Target { 108 | &mut self.proc 109 | } 110 | } 111 | 112 | /// A IO stream (write/read) of [UnixProcess]. 113 | #[derive(Debug)] 114 | pub struct PtyStream { 115 | handle: Stream, 116 | } 117 | 118 | impl PtyStream { 119 | fn new(stream: Stream) -> Self { 120 | Self { handle: stream } 121 | } 122 | } 123 | 124 | impl Write for PtyStream { 125 | fn write(&mut self, buf: &[u8]) -> Result { 126 | self.handle.write(buf) 127 | } 128 | 129 | fn flush(&mut self) -> Result<()> { 130 | self.handle.flush() 131 | } 132 | 133 | fn write_vectored(&mut self, bufs: &[io::IoSlice<'_>]) -> Result { 134 | self.handle.write_vectored(bufs) 135 | } 136 | } 137 | 138 | impl Read for PtyStream { 139 | fn read(&mut self, buf: &mut [u8]) -> Result { 140 | self.handle.read(buf) 141 | } 142 | } 143 | 144 | impl NonBlocking for PtyStream { 145 | fn set_blocking(&mut self, on: bool) -> Result<()> { 146 | let fd = self.handle.as_raw_fd(); 147 | match on { 148 | true => make_non_blocking(fd, false), 149 | false => make_non_blocking(fd, true), 150 | } 151 | } 152 | } 153 | 154 | impl AsRawFd for PtyStream { 155 | fn as_raw_fd(&self) -> RawFd { 156 | self.handle.as_raw_fd() 157 | } 158 | } 159 | 160 | #[cfg(feature = "async")] 161 | impl IntoAsyncStream for PtyStream { 162 | type AsyncStream = AsyncPtyStream; 163 | 164 | fn into_async_stream(self) -> Result { 165 | AsyncPtyStream::new(self) 166 | } 167 | } 168 | 169 | /// An async version of IO stream of [UnixProcess]. 170 | #[cfg(feature = "async")] 171 | #[derive(Debug)] 172 | pub struct AsyncPtyStream { 173 | stream: async_io::Async, 174 | } 175 | 176 | #[cfg(feature = "async")] 177 | impl AsyncPtyStream { 178 | fn new(stream: PtyStream) -> Result { 179 | let stream = async_io::Async::new(stream)?; 180 | Ok(Self { stream }) 181 | } 182 | } 183 | 184 | #[cfg(feature = "async")] 185 | impl AsyncWrite for AsyncPtyStream { 186 | fn poll_write( 187 | mut self: Pin<&mut Self>, 188 | cx: &mut Context<'_>, 189 | buf: &[u8], 190 | ) -> Poll> { 191 | Pin::new(&mut self.stream).poll_write(cx, buf) 192 | } 193 | 194 | fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 195 | Pin::new(&mut self.stream).poll_flush(cx) 196 | } 197 | 198 | fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 199 | Pin::new(&mut self.stream).poll_close(cx) 200 | } 201 | } 202 | 203 | #[cfg(feature = "async")] 204 | impl AsyncRead for AsyncPtyStream { 205 | fn poll_read( 206 | mut self: Pin<&mut Self>, 207 | cx: &mut Context<'_>, 208 | buf: &mut [u8], 209 | ) -> Poll> { 210 | Pin::new(&mut self.stream).poll_read(cx, buf) 211 | } 212 | } 213 | 214 | #[cfg(feature = "polling")] 215 | impl polling::Source for PtyStream { 216 | fn raw(&self) -> RawFd { 217 | self.as_raw_fd() 218 | } 219 | } 220 | 221 | pub(crate) fn make_non_blocking(fd: RawFd, blocking: bool) -> Result<()> { 222 | use nix::fcntl::{fcntl, FcntlArg, OFlag}; 223 | 224 | let opt = fcntl(fd, FcntlArg::F_GETFL).map_err(nix_error_to_io)?; 225 | let mut opt = OFlag::from_bits_truncate(opt); 226 | opt.set(OFlag::O_NONBLOCK, blocking); 227 | let _ = fcntl(fd, FcntlArg::F_SETFL(opt)).map_err(nix_error_to_io)?; 228 | Ok(()) 229 | } 230 | 231 | fn nix_error_to_io(err: nix::Error) -> io::Error { 232 | io::Error::new(io::ErrorKind::Other, err) 233 | } 234 | 235 | /// Turn e.g. "prog arg1 arg2" into ["prog", "arg1", "arg2"] 236 | /// It takes care of single and double quotes but, 237 | /// 238 | /// It doesn't cover all edge cases. 239 | /// So it may not be compatible with real shell arguments parsing. 240 | fn tokenize_command(program: &str) -> Vec { 241 | let re = regex::Regex::new(r#""[^"]+"|'[^']+'|[^'" ]+"#).unwrap(); 242 | let mut res = vec![]; 243 | for cap in re.captures_iter(program) { 244 | res.push(cap[0].to_string()); 245 | } 246 | res 247 | } 248 | 249 | fn get_status(proc: &PtyProcess) -> std::prelude::v1::Result { 250 | match proc.status() { 251 | Ok(status) => Ok(status), 252 | Err(err) => match err { 253 | Errno::ECHILD | Errno::ESRCH => Err(io::Error::new(ErrorKind::WouldBlock, err)), 254 | err => Err(io::Error::new(ErrorKind::Other, err)), 255 | }, 256 | } 257 | } 258 | 259 | fn io_error(msg: &str) -> io::Error { 260 | io::Error::new(io::ErrorKind::Other, msg) 261 | } 262 | 263 | #[cfg(test)] 264 | mod tests { 265 | use super::*; 266 | 267 | #[cfg(unix)] 268 | #[test] 269 | fn test_tokenize_command() { 270 | let res = tokenize_command("prog arg1 arg2"); 271 | assert_eq!(vec!["prog", "arg1", "arg2"], res); 272 | 273 | let res = tokenize_command("prog -k=v"); 274 | assert_eq!(vec!["prog", "-k=v"], res); 275 | 276 | let res = tokenize_command("prog 'my text'"); 277 | assert_eq!(vec!["prog", "'my text'"], res); 278 | 279 | let res = tokenize_command(r#"prog "my text""#); 280 | assert_eq!(vec!["prog", r#""my text""#], res); 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /src/process/windows.rs: -------------------------------------------------------------------------------- 1 | //! This module contains a Windows implementation of [crate::process::Process]. 2 | 3 | use std::{ 4 | io::{self, Read, Result, Write}, 5 | ops::{Deref, DerefMut}, 6 | process::Command, 7 | }; 8 | 9 | use conpty::{ 10 | io::{PipeReader, PipeWriter}, 11 | spawn, Process, 12 | }; 13 | 14 | use super::{Healthcheck, NonBlocking, Process as ProcessTrait}; 15 | use crate::error::to_io_error; 16 | 17 | #[cfg(feature = "async")] 18 | use super::IntoAsyncStream; 19 | #[cfg(feature = "async")] 20 | use futures_lite::{AsyncRead, AsyncWrite}; 21 | #[cfg(feature = "async")] 22 | use std::{ 23 | pin::Pin, 24 | task::{Context, Poll}, 25 | }; 26 | 27 | /// A windows representation of a [Process] via [conpty::Process]. 28 | #[derive(Debug)] 29 | pub struct WinProcess { 30 | proc: Process, 31 | } 32 | 33 | impl ProcessTrait for WinProcess { 34 | type Command = Command; 35 | type Stream = ProcessStream; 36 | 37 | fn spawn>(cmd: S) -> Result { 38 | spawn(cmd.as_ref()) 39 | .map_err(to_io_error("")) 40 | .map(|proc| WinProcess { proc }) 41 | } 42 | 43 | fn spawn_command(command: Self::Command) -> Result { 44 | conpty::Process::spawn(command) 45 | .map_err(to_io_error("")) 46 | .map(|proc| WinProcess { proc }) 47 | } 48 | 49 | fn open_stream(&mut self) -> Result { 50 | let input = self.proc.input().map_err(to_io_error(""))?; 51 | let output = self.proc.output().map_err(to_io_error(""))?; 52 | Ok(Self::Stream::new(output, input)) 53 | } 54 | } 55 | 56 | impl Healthcheck for WinProcess { 57 | // todo: We could implement it by using WaitForObject and return -> u32 code 58 | type Status = (); 59 | 60 | fn get_status(&self) -> Result { 61 | Ok(()) 62 | } 63 | 64 | fn is_alive(&self) -> Result { 65 | Ok(self.proc.is_alive()) 66 | } 67 | } 68 | 69 | impl Deref for WinProcess { 70 | type Target = Process; 71 | 72 | fn deref(&self) -> &Self::Target { 73 | &self.proc 74 | } 75 | } 76 | 77 | impl DerefMut for WinProcess { 78 | fn deref_mut(&mut self) -> &mut Self::Target { 79 | &mut self.proc 80 | } 81 | } 82 | 83 | /// An IO stream of [WinProcess]. 84 | #[derive(Debug)] 85 | pub struct ProcessStream { 86 | input: PipeWriter, 87 | output: PipeReader, 88 | } 89 | 90 | impl ProcessStream { 91 | fn new(output: PipeReader, input: PipeWriter) -> Self { 92 | Self { input, output } 93 | } 94 | 95 | /// Tries to clone the stream. 96 | pub fn try_clone(&self) -> std::result::Result { 97 | Ok(Self { 98 | input: self.input.try_clone()?, 99 | output: self.output.try_clone()?, 100 | }) 101 | } 102 | } 103 | 104 | impl Write for ProcessStream { 105 | fn write(&mut self, buf: &[u8]) -> Result { 106 | self.input.write(buf) 107 | } 108 | 109 | fn flush(&mut self) -> Result<()> { 110 | self.input.flush() 111 | } 112 | 113 | fn write_vectored(&mut self, bufs: &[io::IoSlice<'_>]) -> Result { 114 | self.input.write_vectored(bufs) 115 | } 116 | } 117 | 118 | impl Read for ProcessStream { 119 | fn read(&mut self, buf: &mut [u8]) -> Result { 120 | self.output.read(buf) 121 | } 122 | } 123 | 124 | impl NonBlocking for ProcessStream { 125 | fn set_blocking(&mut self, on: bool) -> Result<()> { 126 | self.output.blocking(on); 127 | Ok(()) 128 | } 129 | } 130 | 131 | #[cfg(feature = "async")] 132 | impl IntoAsyncStream for ProcessStream { 133 | type AsyncStream = AsyncProcessStream; 134 | 135 | fn into_async_stream(self) -> Result { 136 | AsyncProcessStream::new(self) 137 | } 138 | } 139 | 140 | /// An async version of IO stream of [WinProcess]. 141 | #[cfg(feature = "async")] 142 | #[derive(Debug)] 143 | pub struct AsyncProcessStream { 144 | output: blocking::Unblock, 145 | input: blocking::Unblock, 146 | } 147 | 148 | #[cfg(feature = "async")] 149 | impl AsyncProcessStream { 150 | fn new(stream: ProcessStream) -> Result { 151 | let input = blocking::Unblock::new(stream.input); 152 | let output = blocking::Unblock::new(stream.output); 153 | Ok(Self { input, output }) 154 | } 155 | } 156 | 157 | #[cfg(feature = "async")] 158 | impl AsyncWrite for AsyncProcessStream { 159 | fn poll_write( 160 | mut self: Pin<&mut Self>, 161 | cx: &mut Context<'_>, 162 | buf: &[u8], 163 | ) -> Poll> { 164 | Pin::new(&mut self.input).poll_write(cx, buf) 165 | } 166 | 167 | fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 168 | Pin::new(&mut self.input).poll_flush(cx) 169 | } 170 | 171 | fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 172 | Pin::new(&mut self.input).poll_close(cx) 173 | } 174 | } 175 | 176 | #[cfg(feature = "async")] 177 | impl AsyncRead for AsyncProcessStream { 178 | fn poll_read( 179 | mut self: Pin<&mut Self>, 180 | cx: &mut Context<'_>, 181 | buf: &mut [u8], 182 | ) -> Poll> { 183 | Pin::new(&mut self.output).poll_read(cx, buf) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/session/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module contains a system independent [Session] representation. 2 | //! 3 | //! But it does set a default [Session] processes and stream in order to be able to use Session without generics. 4 | //! It also sets a list of other methods which are available for a platform processes. 5 | //! 6 | //! # Example 7 | //! 8 | //! ```no_run,ignore 9 | //! use std::{process::Command, io::prelude::*}; 10 | //! use expectrl::Session; 11 | //! 12 | //! let mut p = Session::spawn(Command::new("cat")).unwrap(); 13 | //! writeln!(p, "Hello World").unwrap(); 14 | //! let mut line = String::new(); 15 | //! p.read_line(&mut line).unwrap(); 16 | //! ``` 17 | 18 | #[cfg(feature = "async")] 19 | mod async_session; 20 | #[cfg(not(feature = "async"))] 21 | mod sync_session; 22 | 23 | use std::{io::Write, process::Command}; 24 | 25 | use crate::{interact::InteractSession, process::Process, stream::log::LogStream, Error}; 26 | 27 | #[cfg(not(feature = "async"))] 28 | use std::io::Read; 29 | 30 | #[cfg(feature = "async")] 31 | use crate::process::IntoAsyncStream; 32 | 33 | #[cfg(unix)] 34 | type OsProc = crate::process::unix::UnixProcess; 35 | #[cfg(windows)] 36 | type OsProc = crate::process::windows::WinProcess; 37 | 38 | #[cfg(all(unix, not(feature = "async")))] 39 | type OsProcStream = crate::process::unix::PtyStream; 40 | #[cfg(all(unix, feature = "async"))] 41 | type OsProcStream = crate::process::unix::AsyncPtyStream; 42 | #[cfg(all(windows, not(feature = "async")))] 43 | type OsProcStream = crate::process::windows::ProcessStream; 44 | #[cfg(all(windows, feature = "async"))] 45 | type OsProcStream = crate::process::windows::AsyncProcessStream; 46 | 47 | /// A type alias for OS process which can run a [`Session`] and a default one. 48 | pub type OsProcess = OsProc; 49 | /// A type alias for OS process stream which is a default one for [`Session`]. 50 | pub type OsStream = OsProcStream; 51 | /// A type alias for OS session. 52 | pub type OsSession = Session; 53 | 54 | #[cfg(feature = "async")] 55 | pub use async_session::Session; 56 | 57 | #[cfg(not(feature = "async"))] 58 | pub use sync_session::Session; 59 | 60 | impl Session { 61 | /// Spawns a session on a platform process. 62 | /// 63 | /// # Example 64 | /// 65 | /// ```no_run 66 | /// use std::process::Command; 67 | /// use expectrl::Session; 68 | /// 69 | /// let p = Session::spawn(Command::new("cat")); 70 | /// ``` 71 | pub fn spawn(command: Command) -> Result { 72 | let mut process = OsProcess::spawn_command(command)?; 73 | let stream = process.open_stream()?; 74 | 75 | #[cfg(feature = "async")] 76 | let stream = stream.into_async_stream()?; 77 | 78 | let session = Self::new(process, stream)?; 79 | 80 | Ok(session) 81 | } 82 | 83 | /// Spawns a session on a platform process. 84 | /// Using a string commandline. 85 | pub(crate) fn spawn_cmd(cmd: &str) -> Result { 86 | let mut process = OsProcess::spawn(cmd)?; 87 | let stream = process.open_stream()?; 88 | 89 | #[cfg(feature = "async")] 90 | let stream = stream.into_async_stream()?; 91 | 92 | let session = Self::new(process, stream)?; 93 | 94 | Ok(session) 95 | } 96 | } 97 | 98 | impl Session { 99 | /// Interact gives control of the child process to the interactive user (the 100 | /// human at the keyboard or a [`Read`]er implementator). 101 | /// 102 | /// You can set different callbacks to the session, see [`InteractSession`]. 103 | /// 104 | /// Keystrokes are sent to the child process, and 105 | /// the `stdout` and `stderr` output of the child process is printed. 106 | /// 107 | /// When the user types the `escape_character` this method will return control to a running process. 108 | /// The escape_character will not be transmitted. 109 | /// The default for escape_character is entered as `Ctrl-]`, the very same as BSD telnet. 110 | /// 111 | /// This simply echos the child `stdout` and `stderr` to the real `stdout` and 112 | /// it echos the real `stdin` to the child `stdin`. 113 | /// 114 | /// BEWARE that interact finishes after a process stops. 115 | /// So after the return you may not obtain a correct status of a process. 116 | /// 117 | /// In not `async` mode the default version uses a buzy loop. 118 | /// 119 | /// - On `linux` you can use a `polling` version using the corresponding feature. 120 | /// - On `windows` the feature is also present but it spawns a thread for pooling which creates a set of obsticales. 121 | /// Specifically if you're planning to call `interact()` multiple times it may not be safe. Because the previous threads may still be running. 122 | /// 123 | /// It works via polling in `async` mode on both `unix` and `windows`. 124 | /// 125 | /// # Example 126 | /// 127 | #[cfg_attr( 128 | all(unix, not(feature = "async"), not(feature = "polling")), 129 | doc = "```no_run" 130 | )] 131 | #[cfg_attr( 132 | not(all(unix, not(feature = "async"), not(feature = "polling"))), 133 | doc = "```ignore" 134 | )] 135 | /// use std::io::{stdout, Cursor}; 136 | /// 137 | /// let mut p = expectrl::spawn("cat").unwrap(); 138 | /// 139 | /// let input = Cursor::new(String::from("Some text right here")); 140 | /// 141 | /// p.interact(input, stdout()).spawn().unwrap(); 142 | /// ``` 143 | /// 144 | /// [`Read`]: std::io::Read 145 | pub fn interact(&mut self, input: I, output: O) -> InteractSession<&mut Self, I, O, ()> { 146 | InteractSession::new(self, input, output, ()) 147 | } 148 | } 149 | 150 | /// Set a logger which will write each Read/Write operation into the writter. 151 | /// 152 | /// # Example 153 | /// 154 | /// ``` 155 | /// use expectrl::{spawn, session::log}; 156 | /// 157 | /// let p = spawn("cat").unwrap(); 158 | /// let p = log(p, std::io::stdout()); 159 | /// ``` 160 | #[cfg(not(feature = "async"))] 161 | pub fn log(session: Session, dst: W) -> Result>, Error> 162 | where 163 | W: Write, 164 | S: Read, 165 | { 166 | session.swap_stream(|s| LogStream::new(s, dst)) 167 | } 168 | 169 | /// Set a logger which will write each Read/Write operation into the writter. 170 | /// 171 | /// # Example 172 | /// 173 | /// ``` 174 | /// use expectrl::{spawn, session::log}; 175 | /// 176 | /// let p = spawn("cat").unwrap(); 177 | /// let p = log(p, std::io::stdout()); 178 | /// ``` 179 | #[cfg(feature = "async")] 180 | pub fn log(session: Session, dst: W) -> Result>, Error> 181 | where 182 | W: Write, 183 | { 184 | session.swap_stream(|s| LogStream::new(s, dst)) 185 | } 186 | -------------------------------------------------------------------------------- /src/stream/log.rs: -------------------------------------------------------------------------------- 1 | //! This module container a [LogStream] 2 | //! which can wrap other streams in order to log a read/write operations. 3 | 4 | use std::{ 5 | io::{self, Read, Result, Write}, 6 | ops::{Deref, DerefMut}, 7 | }; 8 | 9 | #[cfg(feature = "async")] 10 | use futures_lite::{AsyncRead, AsyncWrite}; 11 | #[cfg(feature = "async")] 12 | use std::{ 13 | pin::Pin, 14 | task::{Context, Poll}, 15 | }; 16 | 17 | use crate::process::NonBlocking; 18 | 19 | /// LogStream a IO stream wrapper, 20 | /// which logs each write/read operation. 21 | #[derive(Debug)] 22 | pub struct LogStream { 23 | stream: S, 24 | logger: W, 25 | } 26 | 27 | impl LogStream { 28 | /// Creates a new instance of the stream. 29 | pub fn new(stream: S, logger: W) -> Self { 30 | Self { stream, logger } 31 | } 32 | } 33 | 34 | impl LogStream { 35 | fn log_write(&mut self, buf: &[u8]) { 36 | log(&mut self.logger, "write", buf); 37 | } 38 | 39 | fn log_read(&mut self, buf: &[u8]) { 40 | log(&mut self.logger, "read", buf); 41 | } 42 | } 43 | 44 | impl Write for LogStream { 45 | fn write(&mut self, buf: &[u8]) -> Result { 46 | let n = self.stream.write(buf)?; 47 | self.log_write(&buf[..n]); 48 | Ok(n) 49 | } 50 | 51 | fn flush(&mut self) -> Result<()> { 52 | self.stream.flush() 53 | } 54 | 55 | fn write_vectored(&mut self, bufs: &[io::IoSlice<'_>]) -> Result { 56 | let n = self.stream.write_vectored(bufs)?; 57 | 58 | let mut rest = n; 59 | let mut bytes = Vec::new(); 60 | for buf in bufs { 61 | let written = std::cmp::min(buf.len(), rest); 62 | rest -= written; 63 | 64 | bytes.extend(&buf.as_ref()[..written]); 65 | 66 | if rest == 0 { 67 | break; 68 | } 69 | } 70 | 71 | self.log_write(&bytes); 72 | 73 | Ok(n) 74 | } 75 | } 76 | 77 | impl Read for LogStream { 78 | fn read(&mut self, buf: &mut [u8]) -> Result { 79 | let n = self.stream.read(buf)?; 80 | self.log_read(&buf[..n]); 81 | Ok(n) 82 | } 83 | } 84 | 85 | impl NonBlocking for LogStream 86 | where 87 | S: NonBlocking, 88 | { 89 | fn set_blocking(&mut self, on: bool) -> Result<()> { 90 | self.stream.set_blocking(on) 91 | } 92 | } 93 | 94 | impl Deref for LogStream { 95 | type Target = S; 96 | 97 | fn deref(&self) -> &Self::Target { 98 | &self.stream 99 | } 100 | } 101 | 102 | impl DerefMut for LogStream { 103 | fn deref_mut(&mut self) -> &mut Self::Target { 104 | &mut self.stream 105 | } 106 | } 107 | 108 | #[cfg(feature = "async")] 109 | impl AsyncWrite for LogStream { 110 | fn poll_write( 111 | mut self: Pin<&mut Self>, 112 | cx: &mut Context<'_>, 113 | buf: &[u8], 114 | ) -> Poll> { 115 | self.log_write(buf); 116 | Pin::new(&mut self.get_mut().stream).poll_write(cx, buf) 117 | } 118 | 119 | fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 120 | Pin::new(&mut self.stream).poll_flush(cx) 121 | } 122 | 123 | fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 124 | Pin::new(&mut self.stream).poll_close(cx) 125 | } 126 | 127 | fn poll_write_vectored( 128 | mut self: Pin<&mut Self>, 129 | cx: &mut Context<'_>, 130 | bufs: &[io::IoSlice<'_>], 131 | ) -> Poll> { 132 | Pin::new(&mut self.stream).poll_write_vectored(cx, bufs) 133 | } 134 | } 135 | 136 | #[cfg(feature = "async")] 137 | impl AsyncRead for LogStream { 138 | fn poll_read( 139 | mut self: Pin<&mut Self>, 140 | cx: &mut Context<'_>, 141 | buf: &mut [u8], 142 | ) -> Poll> { 143 | let result = Pin::new(&mut self.stream).poll_read(cx, buf); 144 | if let Poll::Ready(Ok(n)) = &result { 145 | self.log_read(&buf[..*n]); 146 | } 147 | 148 | result 149 | } 150 | } 151 | 152 | fn log(mut writer: impl Write, target: &str, data: &[u8]) { 153 | let _ = match std::str::from_utf8(data) { 154 | Ok(data) => writeln!(writer, "{}: {:?}", target, data), 155 | Err(..) => writeln!(writer, "{}:(bytes): {:?}", target, data), 156 | }; 157 | } 158 | -------------------------------------------------------------------------------- /src/stream/mod.rs: -------------------------------------------------------------------------------- 1 | //! Stream module contains a set of IO (write/read) wrappers. 2 | 3 | pub mod log; 4 | pub mod stdin; 5 | -------------------------------------------------------------------------------- /src/stream/stdin.rs: -------------------------------------------------------------------------------- 1 | //! The module contains a nonblocking version of [std::io::Stdin]. 2 | 3 | use std::io; 4 | 5 | #[cfg(not(feature = "async"))] 6 | use std::io::Read; 7 | 8 | #[cfg(feature = "async")] 9 | use std::{ 10 | pin::Pin, 11 | task::{Context, Poll}, 12 | }; 13 | 14 | #[cfg(feature = "async")] 15 | use futures_lite::AsyncRead; 16 | 17 | use crate::Error; 18 | 19 | /// A non blocking version of STDIN. 20 | /// 21 | /// It's not recomended to be used directly. 22 | /// But we expose it because it cab be used with [`Session::interact`]. 23 | /// 24 | /// [`Session::interact`]: crate::session::Session::interact 25 | #[derive(Debug)] 26 | pub struct Stdin { 27 | inner: inner::StdinInner, 28 | } 29 | 30 | impl Stdin { 31 | /// Creates a new instance of Stdin. 32 | /// 33 | /// It may change terminal's STDIN state therefore, after 34 | /// it's used you must call [Stdin::close]. 35 | pub fn open() -> Result { 36 | #[cfg(not(feature = "async"))] 37 | { 38 | let mut stdin = inner::StdinInner::new().map(|inner| Self { inner })?; 39 | stdin.blocking(true)?; 40 | Ok(stdin) 41 | } 42 | 43 | #[cfg(feature = "async")] 44 | { 45 | let stdin = inner::StdinInner::new().map(|inner| Self { inner })?; 46 | Ok(stdin) 47 | } 48 | } 49 | 50 | /// Close frees a resources which were used. 51 | /// 52 | /// It must be called [Stdin] was used. 53 | /// Otherwise the STDIN might be returned to original state. 54 | pub fn close(mut self) -> Result<(), Error> { 55 | #[cfg(not(feature = "async"))] 56 | self.blocking(false)?; 57 | self.inner.close()?; 58 | Ok(()) 59 | } 60 | 61 | #[cfg(not(feature = "async"))] 62 | pub(crate) fn blocking(&mut self, on: bool) -> Result<(), Error> { 63 | self.inner.blocking(on) 64 | } 65 | } 66 | 67 | #[cfg(not(feature = "async"))] 68 | impl Read for Stdin { 69 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 70 | self.inner.read(buf) 71 | } 72 | } 73 | 74 | #[cfg(feature = "async")] 75 | impl AsyncRead for Stdin { 76 | fn poll_read( 77 | mut self: Pin<&mut Self>, 78 | cx: &mut Context<'_>, 79 | buf: &mut [u8], 80 | ) -> Poll> { 81 | AsyncRead::poll_read(Pin::new(&mut self.inner), cx, buf) 82 | } 83 | } 84 | 85 | #[cfg(unix)] 86 | impl std::os::unix::prelude::AsRawFd for Stdin { 87 | fn as_raw_fd(&self) -> std::os::unix::prelude::RawFd { 88 | self.inner.as_raw_fd() 89 | } 90 | } 91 | 92 | #[cfg(unix)] 93 | impl std::os::unix::prelude::AsRawFd for &mut Stdin { 94 | fn as_raw_fd(&self) -> std::os::unix::prelude::RawFd { 95 | self.inner.as_raw_fd() 96 | } 97 | } 98 | 99 | #[cfg(all(unix, feature = "polling"))] 100 | impl polling::Source for Stdin { 101 | fn raw(&self) -> std::os::unix::prelude::RawFd { 102 | std::os::unix::io::AsRawFd::as_raw_fd(self) 103 | } 104 | } 105 | 106 | #[cfg(unix)] 107 | mod inner { 108 | use super::*; 109 | 110 | use std::os::unix::prelude::AsRawFd; 111 | 112 | use nix::{ 113 | libc::STDIN_FILENO, 114 | sys::termios::{self, Termios}, 115 | unistd::isatty, 116 | }; 117 | use ptyprocess::set_raw; 118 | 119 | #[derive(Debug)] 120 | pub(super) struct StdinInner { 121 | orig_flags: Option, 122 | #[cfg(feature = "async")] 123 | stdin: async_io::Async, 124 | #[cfg(not(feature = "async"))] 125 | stdin: std::io::Stdin, 126 | } 127 | 128 | impl StdinInner { 129 | pub(super) fn new() -> Result { 130 | let stdin = std::io::stdin(); 131 | #[cfg(feature = "async")] 132 | let stdin = async_io::Async::new(stdin)?; 133 | 134 | #[cfg(target_os = "macos")] 135 | let orig_flags = None; 136 | 137 | #[cfg(not(target_os = "macos"))] 138 | let orig_flags = Self::prepare()?; 139 | 140 | Ok(Self { stdin, orig_flags }) 141 | } 142 | 143 | pub(super) fn prepare() -> Result, Error> { 144 | // flush buffers 145 | // self.stdin.flush()?; 146 | 147 | let mut o_pty_flags = None; 148 | 149 | // verify: possible controlling fd can be stdout and stderr as well? 150 | // https://stackoverflow.com/questions/35873843/when-setting-terminal-attributes-via-tcsetattrfd-can-fd-be-either-stdout 151 | let isatty_terminal = isatty(STDIN_FILENO) 152 | .map_err(|e| Error::unknown("failed to call isatty", e.to_string()))?; 153 | if isatty_terminal { 154 | // tcgetattr issues error if a provided fd is not a tty, 155 | // but we can work with such input as it may be redirected. 156 | o_pty_flags = termios::tcgetattr(STDIN_FILENO) 157 | .map(Some) 158 | .map_err(|e| Error::unknown("failed to call tcgetattr", e.to_string()))?; 159 | 160 | set_raw(STDIN_FILENO) 161 | .map_err(|e| Error::unknown("failed to set a raw tty", e.to_string()))?; 162 | } 163 | 164 | Ok(o_pty_flags) 165 | } 166 | 167 | pub(super) fn close(&mut self) -> Result<(), Error> { 168 | if let Some(origin_stdin_flags) = &self.orig_flags { 169 | termios::tcsetattr(STDIN_FILENO, termios::SetArg::TCSAFLUSH, origin_stdin_flags) 170 | .map_err(|e| Error::unknown("failed to call tcsetattr", e.to_string()))?; 171 | } 172 | 173 | Ok(()) 174 | } 175 | 176 | #[cfg(not(feature = "async"))] 177 | pub(crate) fn blocking(&mut self, on: bool) -> Result<(), Error> { 178 | crate::process::unix::make_non_blocking(self.as_raw_fd(), on).map_err(Error::IO) 179 | } 180 | } 181 | 182 | impl AsRawFd for StdinInner { 183 | fn as_raw_fd(&self) -> std::os::unix::prelude::RawFd { 184 | self.stdin.as_raw_fd() 185 | } 186 | } 187 | 188 | #[cfg(not(feature = "async"))] 189 | impl Read for StdinInner { 190 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 191 | self.stdin.read(buf) 192 | } 193 | } 194 | 195 | #[cfg(feature = "async")] 196 | impl AsyncRead for StdinInner { 197 | fn poll_read( 198 | mut self: Pin<&mut Self>, 199 | cx: &mut Context<'_>, 200 | buf: &mut [u8], 201 | ) -> Poll> { 202 | AsyncRead::poll_read(Pin::new(&mut self.stdin), cx, buf) 203 | } 204 | } 205 | } 206 | 207 | #[cfg(windows)] 208 | mod inner { 209 | use super::*; 210 | 211 | use conpty::console::Console; 212 | 213 | pub(super) struct StdinInner { 214 | terminal: Console, 215 | #[cfg(not(feature = "async"))] 216 | is_blocking: bool, 217 | #[cfg(not(feature = "async"))] 218 | stdin: io::Stdin, 219 | #[cfg(feature = "async")] 220 | stdin: blocking::Unblock, 221 | } 222 | 223 | impl std::fmt::Debug for StdinInner { 224 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 225 | #[cfg(not(feature = "async"))] 226 | { 227 | f.debug_struct("StdinInner") 228 | .field("terminal", &self.terminal) 229 | .field("is_blocking", &self.is_blocking) 230 | .field("stdin", &self.stdin) 231 | .field("stdin", &self.stdin) 232 | .finish() 233 | } 234 | #[cfg(feature = "async")] 235 | { 236 | f.debug_struct("StdinInner") 237 | .field("terminal", &self.terminal) 238 | .field("stdin", &self.stdin) 239 | .field("stdin", &self.stdin) 240 | .finish() 241 | } 242 | } 243 | } 244 | 245 | impl StdinInner { 246 | /// Creates a new instance of Stdin. 247 | /// 248 | /// It changes terminal's STDIN state therefore, after 249 | /// it's used please call [Stdin::close]. 250 | pub(super) fn new() -> Result { 251 | let console = conpty::console::Console::current().map_err(to_io_error)?; 252 | console.set_raw().map_err(to_io_error)?; 253 | 254 | let stdin = io::stdin(); 255 | 256 | #[cfg(feature = "async")] 257 | let stdin = blocking::Unblock::new(stdin); 258 | 259 | Ok(Self { 260 | terminal: console, 261 | #[cfg(not(feature = "async"))] 262 | is_blocking: false, 263 | stdin, 264 | }) 265 | } 266 | 267 | pub(super) fn close(&mut self) -> Result<(), Error> { 268 | self.terminal.reset().map_err(to_io_error)?; 269 | Ok(()) 270 | } 271 | 272 | #[cfg(not(feature = "async"))] 273 | pub(crate) fn blocking(&mut self, on: bool) -> Result<(), Error> { 274 | self.is_blocking = on; 275 | Ok(()) 276 | } 277 | } 278 | 279 | #[cfg(not(feature = "async"))] 280 | impl Read for StdinInner { 281 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 282 | // fixme: I am not sure why reading works on is_stdin_empty() == true 283 | // maybe rename of the method necessary 284 | if self.is_blocking && !self.terminal.is_stdin_empty().map_err(to_io_error)? { 285 | return Err(io::Error::new(io::ErrorKind::WouldBlock, "")); 286 | } 287 | 288 | self.stdin.read(buf) 289 | } 290 | } 291 | 292 | #[cfg(feature = "async")] 293 | impl AsyncRead for StdinInner { 294 | fn poll_read( 295 | mut self: Pin<&mut Self>, 296 | cx: &mut Context<'_>, 297 | buf: &mut [u8], 298 | ) -> Poll> { 299 | AsyncRead::poll_read(Pin::new(&mut self.stdin), cx, buf) 300 | } 301 | } 302 | 303 | fn to_io_error(err: impl std::error::Error) -> io::Error { 304 | io::Error::new(io::ErrorKind::Other, err.to_string()) 305 | } 306 | 307 | #[cfg(all(feature = "polling", not(feature = "async")))] 308 | impl Clone for StdinInner { 309 | fn clone(&self) -> Self { 310 | Self { 311 | terminal: self.terminal.clone(), 312 | is_blocking: self.is_blocking.clone(), 313 | stdin: std::io::stdin(), 314 | } 315 | } 316 | } 317 | } 318 | 319 | #[cfg(all(windows, feature = "polling", not(feature = "async")))] 320 | impl Clone for Stdin { 321 | fn clone(&self) -> Self { 322 | Self { 323 | inner: self.inner.clone(), 324 | } 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /src/waiter/blocking.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{Read, Result}, 3 | marker::PhantomData, 4 | thread::JoinHandle, 5 | }; 6 | 7 | use crossbeam_channel::Sender; 8 | 9 | /// Blocking implements a reading operation on a different thread. 10 | /// It stops the thread once any [`Error`] is encountered. 11 | #[derive(Debug)] 12 | pub struct Blocking { 13 | _id: usize, 14 | _thread: JoinHandle<()>, 15 | _reader: PhantomData, 16 | } 17 | 18 | impl Blocking { 19 | /// Creates a new blocking reader, spawning a new thread for it. 20 | pub fn new(id: usize, mut reader: R, sendr: Sender<(usize, Result>)>) -> Self 21 | where 22 | R: Read + Send + 'static, 23 | { 24 | let handle = std::thread::spawn(move || { 25 | let mut buffer = Vec::new(); 26 | let mut buf = [0; 1]; 27 | loop { 28 | match reader.read(&mut buf) { 29 | Ok(n) => { 30 | // try send failed tries 31 | if !buffer.is_empty() { 32 | for b in buffer.drain(..).collect::>() { 33 | try_send(id, Ok(Some(b)), &sendr, &mut buffer); 34 | } 35 | } 36 | 37 | if n == 0 { 38 | try_send(id, Ok(None), &sendr, &mut buffer); 39 | break; 40 | } else { 41 | try_send(id, Ok(Some(buf[0])), &sendr, &mut buffer); 42 | } 43 | } 44 | Err(err) => { 45 | // stopping the thread on error 46 | try_send(id, Err(err), &sendr, &mut buffer); 47 | break; 48 | } 49 | } 50 | } 51 | }); 52 | 53 | Self { 54 | _id: id, 55 | _thread: handle, 56 | _reader: PhantomData, 57 | } 58 | } 59 | 60 | pub fn join(self) -> std::thread::Result<()> { 61 | self._thread.join() 62 | } 63 | } 64 | 65 | fn try_send( 66 | id: usize, 67 | msg: Result>, 68 | sendr: &Sender<(usize, Result>)>, 69 | buf: &mut Vec, 70 | ) { 71 | match sendr.send((id, msg)) { 72 | Ok(_) => (), 73 | Err(err) => { 74 | if let Ok(Some(b)) = err.0 .1 { 75 | buf.push(b); 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/waiter/mod.rs: -------------------------------------------------------------------------------- 1 | mod blocking; 2 | mod wait; 3 | 4 | pub use wait::{Recv, Wait2}; 5 | -------------------------------------------------------------------------------- /src/waiter/wait.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{self, Read}, 3 | time::Duration, 4 | }; 5 | 6 | use crossbeam_channel::Receiver; 7 | 8 | use super::blocking::Blocking; 9 | 10 | #[derive(Debug)] 11 | pub struct Wait2 { 12 | recv: Receiver<(usize, io::Result>)>, 13 | b1: Blocking, 14 | b2: Blocking, 15 | timeout: Duration, 16 | } 17 | 18 | #[derive(Debug)] 19 | pub enum Recv { 20 | R1(io::Result>), 21 | R2(io::Result>), 22 | Timeout, 23 | } 24 | 25 | impl Wait2 { 26 | pub fn new(r1: R1, r2: R2) -> Self 27 | where 28 | R1: Send + Read + 'static, 29 | R2: Send + Read + 'static, 30 | { 31 | let (sndr, recv) = crossbeam_channel::unbounded(); 32 | 33 | let b1 = Blocking::new(0, r1, sndr.clone()); 34 | let b2 = Blocking::new(1, r2, sndr); 35 | 36 | Self { 37 | b1, 38 | b2, 39 | recv, 40 | timeout: Duration::from_secs(5), 41 | } 42 | } 43 | 44 | pub fn join(self) -> std::thread::Result<()> { 45 | self.b1.join()?; 46 | self.b2.join()?; 47 | Ok(()) 48 | } 49 | 50 | pub fn recv(&mut self) -> Result { 51 | match self.recv.recv_timeout(self.timeout) { 52 | Ok((id, result)) => match id { 53 | 0 => Ok(Recv::R1(result)), 54 | 1 => Ok(Recv::R2(result)), 55 | _ => unreachable!(), 56 | }, 57 | Err(err) => { 58 | if err.is_timeout() { 59 | Ok(Recv::Timeout) 60 | } else { 61 | Err(crossbeam_channel::RecvError) 62 | } 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/actions/cat/main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | def main(): 4 | try: 5 | for line in sys.stdin: 6 | print(line, sep=None, end="") 7 | except: 8 | exit(1) 9 | 10 | if __name__ == "__main__": 11 | main() -------------------------------------------------------------------------------- /tests/actions/echo/main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | def main(): 4 | try: 5 | print(' '.join(sys.argv[1:])) 6 | except: 7 | exit(1) 8 | 9 | if __name__ == "__main__": 10 | main() -------------------------------------------------------------------------------- /tests/check.rs: -------------------------------------------------------------------------------- 1 | #![cfg(unix)] 2 | 3 | use expectrl::{spawn, Any, Eof, Expect, NBytes, Regex}; 4 | use std::thread; 5 | use std::time::Duration; 6 | 7 | #[cfg(feature = "async")] 8 | use expectrl::AsyncExpect; 9 | 10 | #[cfg(unix)] 11 | #[cfg(not(feature = "async"))] 12 | #[test] 13 | fn check_str() { 14 | let mut session = spawn("cat").unwrap(); 15 | session.send_line("Hello World").unwrap(); 16 | session.check("Hello World").unwrap(); 17 | } 18 | 19 | #[cfg(unix)] 20 | #[cfg(feature = "async")] 21 | #[test] 22 | fn check_str() { 23 | futures_lite::future::block_on(async { 24 | let mut session = spawn("cat").unwrap(); 25 | session.send_line("Hello World").await.unwrap(); 26 | session.check("Hello World").await.unwrap(); 27 | }) 28 | } 29 | 30 | #[cfg(unix)] 31 | #[cfg(not(feature = "async"))] 32 | #[test] 33 | fn check_regex() { 34 | let mut session = spawn("cat").unwrap(); 35 | session.send_line("Hello World").unwrap(); 36 | 37 | thread::sleep(Duration::from_millis(600)); 38 | 39 | let m = session.check(Regex("lo.*")).unwrap(); 40 | assert_eq!(m.before(), b"Hel"); 41 | assert_eq!(m.get(0).unwrap(), b"lo World\r"); 42 | } 43 | 44 | #[cfg(unix)] 45 | #[cfg(feature = "async")] 46 | #[test] 47 | fn check_regex() { 48 | futures_lite::future::block_on(async { 49 | let mut session = spawn("cat").unwrap(); 50 | session.send_line("Hello World").await.unwrap(); 51 | 52 | thread::sleep(Duration::from_millis(600)); 53 | 54 | let m = session.check(Regex("lo.*")).await.unwrap(); 55 | assert_eq!(m.before(), b"Hel"); 56 | assert_eq!(m.get(0).unwrap(), b"lo World\r"); 57 | }) 58 | } 59 | 60 | #[cfg(unix)] 61 | #[cfg(not(feature = "async"))] 62 | #[test] 63 | fn check_n_bytes() { 64 | let mut session = spawn("cat").unwrap(); 65 | session.send_line("Hello World").unwrap(); 66 | 67 | thread::sleep(Duration::from_millis(600)); 68 | 69 | let m = session.check(NBytes(3)).unwrap(); 70 | assert_eq!(m.get(0).unwrap(), b"Hel"); 71 | assert_eq!(m.before(), b""); 72 | } 73 | 74 | #[cfg(unix)] 75 | #[cfg(feature = "async")] 76 | #[test] 77 | fn check_n_bytes() { 78 | futures_lite::future::block_on(async { 79 | let mut session = spawn("cat").unwrap(); 80 | session.send_line("Hello World").await.unwrap(); 81 | 82 | thread::sleep(Duration::from_millis(600)); 83 | 84 | let m = session.check(NBytes(3)).await.unwrap(); 85 | assert_eq!(m.get(0).unwrap(), b"Hel"); 86 | assert_eq!(m.before(), b""); 87 | }) 88 | } 89 | 90 | #[cfg(unix)] 91 | #[cfg(not(feature = "async"))] 92 | #[test] 93 | fn check_eof() { 94 | let mut session = spawn("echo 'Hello World'").unwrap(); 95 | 96 | thread::sleep(Duration::from_millis(600)); 97 | 98 | let m = session.check(Eof).unwrap(); 99 | assert_eq!(m.before(), b""); 100 | 101 | if m.matches().len() > 0 { 102 | let buf = m.get(0).unwrap(); 103 | #[cfg(target_os = "macos")] 104 | assert!(matches!(buf, b"" | b"'Hello World'\r\n"), "{:?}", buf); 105 | #[cfg(not(target_os = "macos"))] 106 | assert_eq!(buf, b"'Hello World'\r\n"); 107 | } 108 | } 109 | 110 | #[cfg(unix)] 111 | #[cfg(feature = "async")] 112 | #[test] 113 | fn check_eof() { 114 | futures_lite::future::block_on(async { 115 | let mut session = spawn("echo 'Hello World'").unwrap(); 116 | 117 | thread::sleep(Duration::from_millis(600)); 118 | 119 | let m = session.check(Eof).await.unwrap(); 120 | assert_eq!(m.before(), b""); 121 | 122 | if m.matches().len() > 0 { 123 | let buf = m.get(0).unwrap(); 124 | #[cfg(target_os = "macos")] 125 | assert!(matches!(buf, b"" | b"'Hello World'\r\n"), "{:?}", buf); 126 | #[cfg(not(target_os = "macos"))] 127 | assert_eq!(buf, b"'Hello World'\r\n"); 128 | } 129 | }) 130 | } 131 | 132 | #[cfg(unix)] 133 | #[cfg(not(feature = "async"))] 134 | #[test] 135 | fn read_after_check_str() { 136 | use std::io::Read; 137 | 138 | let mut session = spawn("cat").unwrap(); 139 | session.send_line("Hello World").unwrap(); 140 | 141 | thread::sleep(Duration::from_millis(600)); 142 | 143 | let f = session.check("Hello").unwrap(); 144 | assert!(!f.is_empty()); 145 | 146 | // we stop process so read operation will fail. 147 | // other wise read call would block. 148 | session.get_process_mut().exit(false).unwrap(); 149 | 150 | let mut buf = [0; 6]; 151 | session.read_exact(&mut buf).unwrap(); 152 | assert_eq!(&buf, b" World"); 153 | } 154 | 155 | #[cfg(unix)] 156 | #[cfg(feature = "async")] 157 | #[test] 158 | fn read_after_check_str() { 159 | use futures_lite::io::AsyncReadExt; 160 | 161 | futures_lite::future::block_on(async { 162 | let mut session = spawn("cat").unwrap(); 163 | session.send_line("Hello World").await.unwrap(); 164 | 165 | thread::sleep(Duration::from_millis(600)); 166 | 167 | let f = session.check("Hello").await.unwrap(); 168 | assert!(!f.is_empty()); 169 | 170 | // we stop process so read operation will fail. 171 | // other wise read call would block. 172 | session.get_process_mut().exit(false).unwrap(); 173 | 174 | let mut buf = [0; 6]; 175 | session.read_exact(&mut buf).await.unwrap(); 176 | assert_eq!(&buf, b" World"); 177 | }) 178 | } 179 | 180 | #[cfg(unix)] 181 | #[cfg(not(feature = "async"))] 182 | #[test] 183 | fn check_eof_timeout() { 184 | let mut p = spawn("sleep 3").expect("cannot run sleep 3"); 185 | match p.check(Eof) { 186 | Ok(found) if found.is_empty() => {} 187 | r => panic!("reached a timeout {r:?}"), 188 | } 189 | } 190 | 191 | #[cfg(unix)] 192 | #[cfg(feature = "async")] 193 | #[test] 194 | fn check_eof_timeout() { 195 | futures_lite::future::block_on(async { 196 | let mut p = spawn("sleep 3").expect("cannot run sleep 3"); 197 | match p.check(Eof).await { 198 | Ok(found) if found.is_empty() => {} 199 | r => panic!("reached a timeout {r:?}"), 200 | } 201 | }) 202 | } 203 | 204 | #[cfg(unix)] 205 | #[cfg(not(feature = "async"))] 206 | #[test] 207 | fn check_macro() { 208 | let mut session = spawn("cat").unwrap(); 209 | session.send_line("Hello World").unwrap(); 210 | 211 | thread::sleep(Duration::from_millis(600)); 212 | 213 | expectrl::check!( 214 | &mut session, 215 | world = "\r" => { 216 | assert_eq!(world.get(0).unwrap(), b"\r"); 217 | }, 218 | _ = "Hello World" => { 219 | panic!("Unexpected result"); 220 | }, 221 | default => { 222 | panic!("Unexpected result"); 223 | }, 224 | ) 225 | .unwrap(); 226 | } 227 | 228 | #[cfg(unix)] 229 | #[cfg(feature = "async")] 230 | #[test] 231 | fn check_macro() { 232 | let mut session = spawn("cat").unwrap(); 233 | futures_lite::future::block_on(session.send_line("Hello World")).unwrap(); 234 | 235 | thread::sleep(Duration::from_millis(600)); 236 | 237 | futures_lite::future::block_on(async { 238 | expectrl::check!( 239 | session, 240 | // world = "\r" => { 241 | // assert_eq!(world.get(0).unwrap(), b"\r"); 242 | // }, 243 | // _ = "Hello World" => { 244 | // panic!("Unexpected result"); 245 | // }, 246 | // default => { 247 | // panic!("Unexpected result"); 248 | // }, 249 | ) 250 | .await 251 | .unwrap(); 252 | }); 253 | } 254 | 255 | #[cfg(unix)] 256 | #[cfg(not(feature = "async"))] 257 | #[test] 258 | fn check_macro_doest_consume_missmatch() { 259 | let mut session = spawn("cat").unwrap(); 260 | session.send_line("Hello World").unwrap(); 261 | thread::sleep(Duration::from_millis(600)); 262 | 263 | expectrl::check!( 264 | &mut session, 265 | _ = "Something which is not inside" => { 266 | panic!("Unexpected result"); 267 | }, 268 | ) 269 | .unwrap(); 270 | 271 | session.send_line("345").unwrap(); 272 | thread::sleep(Duration::from_millis(600)); 273 | 274 | expectrl::check!( 275 | &mut session, 276 | buffer = Eof => { 277 | assert_eq!(buffer.get(0).unwrap(), b"Hello World\r\n") 278 | }, 279 | ) 280 | .unwrap(); 281 | } 282 | 283 | #[cfg(unix)] 284 | #[cfg(feature = "async")] 285 | #[test] 286 | fn check_macro_doest_consume_missmatch() { 287 | let mut session = spawn("cat").unwrap(); 288 | 289 | futures_lite::future::block_on(async { 290 | session.send_line("Hello World").await.unwrap(); 291 | thread::sleep(Duration::from_millis(600)); 292 | 293 | expectrl::check!( 294 | &mut session, 295 | _ = "Something which is not inside" => { 296 | panic!("Unexpected result"); 297 | }, 298 | ) 299 | .await 300 | .unwrap(); 301 | 302 | session.send_line("345").await.unwrap(); 303 | thread::sleep(Duration::from_millis(600)); 304 | 305 | expectrl::check!( 306 | &mut session, 307 | buffer = Eof => { 308 | assert_eq!(buffer.get(0).unwrap(), b"Hello World\r\n") 309 | }, 310 | ) 311 | .await 312 | .unwrap(); 313 | }); 314 | } 315 | 316 | #[cfg(unix)] 317 | #[cfg(not(feature = "async"))] 318 | #[test] 319 | fn check_macro_with_different_needles() { 320 | let check_input = |session: &mut _| { 321 | expectrl::check!( 322 | session, 323 | number = Any(["123", "345"]) => { 324 | assert_eq!(number.get(0).unwrap(), b"345") 325 | }, 326 | line = "\n" => { 327 | assert_eq!(line.before(), b"Hello World\r") 328 | }, 329 | default => { 330 | panic!("Unexpected result"); 331 | }, 332 | ) 333 | .unwrap(); 334 | }; 335 | 336 | let mut session = spawn("cat").unwrap(); 337 | session.send_line("Hello World").unwrap(); 338 | 339 | thread::sleep(Duration::from_millis(600)); 340 | check_input(&mut session); 341 | 342 | session.send_line("345").unwrap(); 343 | 344 | thread::sleep(Duration::from_millis(600)); 345 | check_input(&mut session); 346 | } 347 | 348 | #[cfg(unix)] 349 | #[cfg(feature = "async")] 350 | #[test] 351 | fn check_macro_with_different_needles() { 352 | futures_lite::future::block_on(async { 353 | let mut session = spawn("cat").unwrap(); 354 | session.send_line("Hello World").await.unwrap(); 355 | 356 | thread::sleep(Duration::from_millis(600)); 357 | expectrl::check!( 358 | &mut session, 359 | number = Any(["123", "345"]) => { 360 | assert_eq!(number.get(0).unwrap(), b"345") 361 | }, 362 | line = "\n" => { 363 | assert_eq!(line.before(), b"Hello World\r") 364 | }, 365 | default => { 366 | panic!("Unexpected result"); 367 | }, 368 | ) 369 | .await 370 | .unwrap(); 371 | 372 | session.send_line("345").await.unwrap(); 373 | 374 | thread::sleep(Duration::from_millis(600)); 375 | expectrl::check!( 376 | &mut session, 377 | number = Any(["123", "345"]) => { 378 | assert_eq!(number.get(0).unwrap(), b"345") 379 | }, 380 | line = "\n" => { 381 | assert_eq!(line.before(), b"Hello World\r") 382 | }, 383 | default => { 384 | panic!("Unexpected result"); 385 | }, 386 | ) 387 | .await 388 | .unwrap(); 389 | }); 390 | } 391 | -------------------------------------------------------------------------------- /tests/expect.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use expectrl::{spawn, Eof, Expect, NBytes, Regex}; 4 | 5 | #[cfg(not(feature = "async"))] 6 | use std::io::Read; 7 | 8 | #[cfg(feature = "async")] 9 | use expectrl::AsyncExpect; 10 | 11 | #[cfg(feature = "async")] 12 | use futures_lite::io::AsyncReadExt; 13 | 14 | #[cfg(unix)] 15 | #[cfg(not(feature = "async"))] 16 | #[test] 17 | fn expect_str() { 18 | let mut session = spawn("cat").unwrap(); 19 | session.send_line("Hello World").unwrap(); 20 | session.expect("Hello World").unwrap(); 21 | } 22 | 23 | #[cfg(unix)] 24 | #[cfg(feature = "async")] 25 | #[test] 26 | fn expect_str() { 27 | futures_lite::future::block_on(async { 28 | let mut session = spawn("cat").unwrap(); 29 | session.send_line("Hello World").await.unwrap(); 30 | session.expect("Hello World").await.unwrap(); 31 | }) 32 | } 33 | 34 | #[cfg(windows)] 35 | #[test] 36 | fn expect_str() { 37 | let mut session = spawn(r#"pwsh -c "python ./tests/actions/cat/main.py""#).unwrap(); 38 | 39 | #[cfg(not(feature = "async"))] 40 | { 41 | session.send_line("Hello World\n\r").unwrap(); 42 | session.expect("Hello World").unwrap(); 43 | } 44 | 45 | #[cfg(feature = "async")] 46 | { 47 | futures_lite::future::block_on(async { 48 | session.send_line("Hello World").await.unwrap(); 49 | session.expect("Hello World").await.unwrap(); 50 | }) 51 | } 52 | } 53 | 54 | #[cfg(unix)] 55 | #[cfg(not(feature = "async"))] 56 | #[test] 57 | fn expect_regex() { 58 | let mut session = spawn("cat").unwrap(); 59 | session.send_line("Hello World").unwrap(); 60 | let m = session.expect(Regex("lo.*")).unwrap(); 61 | assert_eq!(m.before(), b"Hel"); 62 | assert_eq!(m.get(0).unwrap(), b"lo World\r"); 63 | } 64 | 65 | #[cfg(unix)] 66 | #[cfg(not(feature = "async"))] 67 | #[test] 68 | fn expect_regex_lazy() { 69 | let mut session = spawn("cat").unwrap(); 70 | session.set_expect_lazy(true); 71 | session.send_line("Hello World").unwrap(); 72 | let m = session.expect(Regex("lo.*")).unwrap(); 73 | assert_eq!(m.before(), b"Hel"); 74 | assert_eq!(m.get(0).unwrap(), b"lo"); 75 | } 76 | 77 | #[cfg(unix)] 78 | #[cfg(feature = "async")] 79 | #[test] 80 | fn expect_gready_regex() { 81 | futures_lite::future::block_on(async { 82 | let mut session = spawn("cat").unwrap(); 83 | session.send_line("Hello World").await.unwrap(); 84 | let m = session.expect(Regex("lo.*")).await.unwrap(); 85 | assert_eq!(m.before(), b"Hel"); 86 | assert_eq!(m.get(0).unwrap(), b"lo World\r"); 87 | }) 88 | } 89 | 90 | #[cfg(unix)] 91 | #[cfg(feature = "async")] 92 | #[test] 93 | fn expect_lazy_regex() { 94 | futures_lite::future::block_on(async { 95 | let mut session = spawn("cat").unwrap(); 96 | session.set_expect_lazy(true); 97 | session.send_line("Hello World").await.unwrap(); 98 | let m = session.expect(Regex("lo.*")).await.unwrap(); 99 | assert_eq!(m.before(), b"Hel"); 100 | assert_eq!(m.get(0).unwrap(), b"lo"); 101 | }) 102 | } 103 | 104 | #[cfg(windows)] 105 | #[test] 106 | fn expect_regex() { 107 | let mut session = spawn("python ./tests/actions/echo/main.py Hello World").unwrap(); 108 | #[cfg(not(feature = "async"))] 109 | { 110 | let m = session.expect(Regex("lo.*")).unwrap(); 111 | assert_eq!(m.matches().count(), 1); 112 | assert_eq!(m.get(0).unwrap(), b"lo World\r"); 113 | } 114 | 115 | #[cfg(feature = "async")] 116 | { 117 | futures_lite::future::block_on(async { 118 | let m = session.expect(Regex("lo.*")).await.unwrap(); 119 | assert_eq!(m.matches().count(), 1); 120 | assert_eq!(m.get(0).unwrap(), b"lo World\r"); 121 | }) 122 | } 123 | } 124 | 125 | #[cfg(unix)] 126 | #[cfg(not(feature = "async"))] 127 | #[test] 128 | fn expect_n_bytes() { 129 | let mut session = spawn("cat").unwrap(); 130 | session.send_line("Hello World").unwrap(); 131 | let m = session.expect(NBytes(3)).unwrap(); 132 | assert_eq!(m.get(0).unwrap(), b"Hel"); 133 | assert_eq!(m.before(), b""); 134 | } 135 | 136 | #[cfg(unix)] 137 | #[cfg(feature = "async")] 138 | #[test] 139 | fn expect_n_bytes() { 140 | futures_lite::future::block_on(async { 141 | let mut session = spawn("cat").unwrap(); 142 | session.send_line("Hello World").await.unwrap(); 143 | let m = session.expect(NBytes(3)).await.unwrap(); 144 | assert_eq!(m.get(0).unwrap(), b"Hel"); 145 | assert_eq!(m.before(), b""); 146 | }) 147 | } 148 | 149 | #[cfg(windows)] 150 | #[test] 151 | fn expect_n_bytes() { 152 | use expectrl::Session; 153 | use std::process::Command; 154 | 155 | let mut session = Session::spawn(Command::new( 156 | "python ./tests/actions/echo/main.py Hello World", 157 | )) 158 | .unwrap(); 159 | #[cfg(not(feature = "async"))] 160 | { 161 | let m = session.expect(NBytes(14)).unwrap(); 162 | assert_eq!(m.matches().count(), 1); 163 | assert_eq!(m.get(0).unwrap().len(), 14); 164 | assert_eq!(m.before(), b""); 165 | } 166 | 167 | #[cfg(feature = "async")] 168 | { 169 | futures_lite::future::block_on(async { 170 | let m = session.expect(NBytes(14)).await.unwrap(); 171 | assert_eq!(m.matches().count(), 1); 172 | assert_eq!(m.get(0).unwrap().len(), 14); 173 | assert_eq!(m.before(), b""); 174 | }) 175 | } 176 | } 177 | 178 | #[cfg(unix)] 179 | #[cfg(not(feature = "async"))] 180 | #[test] 181 | fn expect_eof() { 182 | let mut session = spawn("echo 'Hello World'").unwrap(); 183 | session.set_expect_timeout(None); 184 | let m = session.expect(Eof).unwrap(); 185 | assert_eq!(m.get(0).unwrap(), b"'Hello World'\r\n"); 186 | assert_eq!(m.before(), b""); 187 | } 188 | 189 | #[cfg(unix)] 190 | #[cfg(feature = "async")] 191 | #[test] 192 | fn expect_eof() { 193 | futures_lite::future::block_on(async { 194 | let mut session = spawn("echo 'Hello World'").unwrap(); 195 | session.set_expect_timeout(None); 196 | let m = session.expect(Eof).await.unwrap(); 197 | assert_eq!(m.get(0).unwrap(), b"'Hello World'\r\n"); 198 | assert_eq!(m.before(), b""); 199 | }) 200 | } 201 | 202 | #[cfg(windows)] 203 | #[test] 204 | #[ignore = "https://stackoverflow.com/questions/68985384/does-a-conpty-reading-pipe-get-notified-on-process-termination"] 205 | fn expect_eof() { 206 | let mut session = spawn("echo 'Hello World'").unwrap(); 207 | 208 | // give shell some time 209 | std::thread::sleep(Duration::from_millis(300)); 210 | 211 | #[cfg(not(feature = "async"))] 212 | { 213 | let m = session.expect(Eof).unwrap(); 214 | assert_eq!(m.get(0).unwrap(), b"'Hello World'\r\n"); 215 | assert_eq!(m.before(), b""); 216 | } 217 | 218 | #[cfg(feature = "async")] 219 | { 220 | futures_lite::future::block_on(async { 221 | let m = session.expect(Eof).await.unwrap(); 222 | assert_eq!(m.get(0).unwrap(), b"'Hello World'\r\n"); 223 | assert_eq!(m.before(), b""); 224 | }) 225 | } 226 | } 227 | 228 | #[cfg(unix)] 229 | #[cfg(not(feature = "async"))] 230 | #[test] 231 | fn read_after_expect_str() { 232 | let mut session = spawn("cat").unwrap(); 233 | session.send_line("Hello World").unwrap(); 234 | session.expect("Hello").unwrap(); 235 | 236 | let mut buf = [0; 6]; 237 | session.read_exact(&mut buf).unwrap(); 238 | assert_eq!(&buf, b" World"); 239 | } 240 | 241 | #[cfg(unix)] 242 | #[cfg(feature = "async")] 243 | #[test] 244 | fn read_after_expect_str() { 245 | futures_lite::future::block_on(async { 246 | let mut session = spawn("cat").unwrap(); 247 | session.send_line("Hello World").await.unwrap(); 248 | session.expect("Hello").await.unwrap(); 249 | 250 | let mut buf = [0; 6]; 251 | session.read_exact(&mut buf).await.unwrap(); 252 | assert_eq!(&buf, b" World"); 253 | }) 254 | } 255 | 256 | #[cfg(windows)] 257 | #[cfg(not(feature = "async"))] 258 | #[test] 259 | fn read_after_expect_str() { 260 | let mut session = spawn("echo 'Hello World'").unwrap(); 261 | 262 | // give shell some time 263 | std::thread::sleep(Duration::from_millis(300)); 264 | 265 | session.expect("Hello").unwrap(); 266 | 267 | let mut buf = [0; 6]; 268 | session.read_exact(&mut buf).unwrap(); 269 | assert_eq!(&buf, b" World"); 270 | } 271 | 272 | #[cfg(windows)] 273 | #[cfg(feature = "async")] 274 | #[test] 275 | fn read_after_expect_str() { 276 | let mut session = spawn("echo 'Hello World'").unwrap(); 277 | 278 | // give shell some time 279 | std::thread::sleep(Duration::from_millis(300)); 280 | 281 | futures_lite::future::block_on(async { 282 | session.expect("Hello").await.unwrap(); 283 | 284 | let mut buf = [0; 6]; 285 | session.read_exact(&mut buf).await.unwrap(); 286 | assert_eq!(&buf, b" World"); 287 | }) 288 | } 289 | 290 | #[cfg(unix)] 291 | #[cfg(not(feature = "async"))] 292 | #[test] 293 | fn expect_eof_timeout() { 294 | let mut p = spawn("sleep 3").expect("cannot run sleep 3"); 295 | p.set_expect_timeout(Some(Duration::from_millis(100))); 296 | match p.expect(Eof) { 297 | Err(expectrl::Error::ExpectTimeout) => {} 298 | r => panic!("reached a timeout {r:?}"), 299 | } 300 | } 301 | 302 | #[cfg(unix)] 303 | #[cfg(feature = "async")] 304 | #[test] 305 | fn expect_eof_timeout() { 306 | futures_lite::future::block_on(async { 307 | let mut p = spawn("sleep 3").expect("cannot run sleep 3"); 308 | p.set_expect_timeout(Some(Duration::from_millis(100))); 309 | match p.expect(Eof).await { 310 | Err(expectrl::Error::ExpectTimeout) => {} 311 | r => panic!("reached a timeout {r:?}"), 312 | } 313 | }) 314 | } 315 | 316 | #[cfg(windows)] 317 | #[test] 318 | fn expect_eof_timeout() { 319 | let mut p = spawn("sleep 3").expect("cannot run sleep 3"); 320 | p.set_expect_timeout(Some(Duration::from_millis(100))); 321 | 322 | #[cfg(not(feature = "async"))] 323 | { 324 | match p.expect(Eof) { 325 | Err(expectrl::Error::ExpectTimeout) => {} 326 | r => panic!("should raise TimeOut {:?}", r), 327 | } 328 | } 329 | 330 | #[cfg(feature = "async")] 331 | { 332 | futures_lite::future::block_on(async { 333 | match p.expect(Eof).await { 334 | Err(expectrl::Error::ExpectTimeout) => {} 335 | r => panic!("should raise TimeOut {:?}", r), 336 | } 337 | }) 338 | } 339 | } 340 | -------------------------------------------------------------------------------- /tests/interact.rs: -------------------------------------------------------------------------------- 1 | #![cfg(unix)] 2 | 3 | use std::{ 4 | io::{self, Cursor, Read, Write}, 5 | time::{Duration, Instant}, 6 | }; 7 | 8 | #[cfg(not(feature = "async"))] 9 | use std::io::sink; 10 | 11 | #[cfg(not(feature = "async"))] 12 | use expectrl::{ 13 | interact::actions::lookup::Lookup, process::unix::WaitStatus, spawn, stream::stdin::Stdin, 14 | Expect, NBytes, 15 | }; 16 | 17 | #[cfg(feature = "async")] 18 | use expectrl::AsyncExpect; 19 | 20 | #[cfg(unix)] 21 | #[cfg(not(feature = "async"))] 22 | #[ignore = "It requires manual interaction; Or it's necessary to redirect an stdin of current process"] 23 | #[test] 24 | fn interact_callback() { 25 | let mut input_handle = Lookup::new(); 26 | let mut output_handle = Lookup::new(); 27 | 28 | let mut session = spawn("cat").unwrap(); 29 | 30 | let mut stdin = Stdin::open().unwrap(); 31 | 32 | session 33 | .interact(&mut stdin, sink()) 34 | .set_output_action(move |ctx| { 35 | if let Some(m) = output_handle.on(ctx.buf, ctx.eof, b'\n')? { 36 | let line = m.before(); 37 | println!("Line in output {:?}", String::from_utf8_lossy(line)); 38 | } 39 | 40 | Ok(false) 41 | }) 42 | .set_input_action(move |ctx| { 43 | if input_handle.on(ctx.buf, ctx.eof, "213")?.is_some() { 44 | ctx.session.send_line("Hello World")?; 45 | } 46 | 47 | Ok(false) 48 | }) 49 | .spawn() 50 | .unwrap(); 51 | 52 | stdin.close().unwrap(); 53 | } 54 | 55 | #[cfg(unix)] 56 | #[cfg(not(feature = "async"))] 57 | #[test] 58 | fn interact_output_callback() { 59 | use expectrl::interact::InteractSession; 60 | 61 | let mut session = expectrl::spawn("sleep 1 && echo 'Hello World'").unwrap(); 62 | 63 | let mut stdin = Stdin::open().unwrap(); 64 | let stdout = std::io::sink(); 65 | 66 | let mut state = 0; 67 | 68 | let mut lookup = Lookup::new(); 69 | 70 | InteractSession::new(&mut session, &mut stdin, stdout, &mut state) 71 | .set_output_action(move |ctx| { 72 | if lookup.on(ctx.buf, ctx.eof, "World")?.is_some() { 73 | **ctx.state += 1; 74 | } 75 | 76 | Ok(false) 77 | }) 78 | .spawn() 79 | .unwrap(); 80 | 81 | stdin.close().unwrap(); 82 | 83 | // fixme: sometimes it's 0 84 | // I guess because the process gets down to fast. 85 | 86 | assert!(matches!(state, 1 | 0), "{state:?}"); 87 | } 88 | 89 | #[cfg(unix)] 90 | #[cfg(not(feature = "async"))] 91 | #[test] 92 | fn interact_callbacks_called_after_exit() { 93 | let mut session = expectrl::spawn("echo 'Hello World'").unwrap(); 94 | 95 | assert_eq!( 96 | session.get_process().wait().unwrap(), 97 | WaitStatus::Exited(session.get_process().pid(), 0) 98 | ); 99 | 100 | let mut stdin = Stdin::open().unwrap(); 101 | let stdout = std::io::sink(); 102 | 103 | let mut state = 0; 104 | 105 | let mut lookup = Lookup::new(); 106 | session 107 | .interact(&mut stdin, stdout) 108 | .with_state(&mut state) 109 | .set_output_action(move |ctx| { 110 | if lookup.on(ctx.buf, ctx.eof, "World")?.is_some() { 111 | **ctx.state += 1; 112 | } 113 | 114 | Ok(false) 115 | }) 116 | .spawn() 117 | .unwrap(); 118 | 119 | stdin.close().unwrap(); 120 | 121 | assert_eq!(state, 0); 122 | } 123 | 124 | #[cfg(unix)] 125 | #[cfg(not(any(feature = "async", feature = "polling")))] 126 | #[test] 127 | fn interact_callbacks_with_stream_redirection() { 128 | let output_lines = vec![ 129 | "NO_MATCHED\n".to_string(), 130 | "QWE\n".to_string(), 131 | "QW123\n".to_string(), 132 | "NO_MATCHED_2\n".to_string(), 133 | ]; 134 | 135 | let reader = ListReaderWithDelayedEof::new(output_lines, Duration::from_secs(2)); 136 | let mut writer = io::Cursor::new(vec![0; 2048]); 137 | 138 | let mut session = spawn("cat").unwrap(); 139 | 140 | let mut input_handle = Lookup::new(); 141 | session 142 | .interact(reader, &mut writer) 143 | .set_input_action(move |ctx| { 144 | if input_handle.on(ctx.buf, ctx.eof, "QWE")?.is_some() { 145 | ctx.session.send_line("Hello World")?; 146 | }; 147 | 148 | Ok(false) 149 | }) 150 | .spawn() 151 | .unwrap(); 152 | 153 | let buffer = String::from_utf8_lossy(writer.get_ref()); 154 | assert!(buffer.contains("Hello World"), "{buffer:?}"); 155 | } 156 | 157 | #[cfg(unix)] 158 | #[cfg(not(any(feature = "async", feature = "polling")))] 159 | #[test] 160 | fn interact_filters() { 161 | let reader = ReaderWithDelayEof::new("1009\nNO\n", Duration::from_secs(4)); 162 | let mut writer = io::Cursor::new(vec![0; 2048]); 163 | 164 | let mut session = spawn("cat").unwrap(); 165 | session 166 | .interact(reader, &mut writer) 167 | .set_input_filter(|buf| { 168 | // ignore 0 chars 169 | let v = buf.iter().filter(|&&b| b != b'0').copied().collect(); 170 | Ok(v) 171 | }) 172 | .set_output_filter(|buf| { 173 | // Make NO -> YES 174 | let v = buf 175 | .chunks(2) 176 | .flat_map(|s| match s { 177 | &[b'N', b'O'] => &[b'Y', b'E', b'S'], 178 | other => other, 179 | }) 180 | .copied() 181 | .collect(); 182 | Ok(v) 183 | }) 184 | .spawn() 185 | .unwrap(); 186 | 187 | let buffer = String::from_utf8_lossy(writer.get_ref()); 188 | let buffer = buffer.trim_end_matches(char::from(0)); 189 | 190 | // fixme: somehow the output is duplicated which is wrong. 191 | assert_eq!(buffer, "19\r\nYES\r\n19\r\nYES\r\n"); 192 | } 193 | 194 | #[cfg(all(unix, not(any(feature = "async", feature = "polling"))))] 195 | #[test] 196 | fn interact_context() { 197 | let mut session = spawn("cat").unwrap(); 198 | 199 | let reader = ListReaderWithDelayedEof::new( 200 | vec![ 201 | "QWE\n".into(), 202 | "QWE\n".into(), 203 | "QWE\n".into(), 204 | "QWE\n".into(), 205 | ], 206 | Duration::from_secs(2), 207 | ); 208 | let mut writer = io::Cursor::new(vec![0; 2048]); 209 | 210 | let mut input_data = Lookup::new(); 211 | let mut output_data = Lookup::new(); 212 | 213 | let mut isession = session.interact(reader, &mut writer).with_state((0, 0)); 214 | isession 215 | .set_input_action(move |ctx| { 216 | if input_data.on(ctx.buf, ctx.eof, "QWE\n")?.is_some() { 217 | ctx.state.0 += 1; 218 | ctx.session.send_line("123")?; 219 | } 220 | 221 | Ok(false) 222 | }) 223 | .set_output_action(move |ctx| { 224 | if output_data.on(ctx.buf, ctx.eof, NBytes(1))?.is_some() { 225 | ctx.state.1 += 1; 226 | output_data.clear(); 227 | } 228 | 229 | Ok(false) 230 | }); 231 | 232 | let is_alive = isession.spawn().unwrap(); 233 | 234 | let state = isession.into_state(); 235 | 236 | assert!(is_alive); 237 | 238 | assert_eq!(state.0, 4); 239 | assert!(state.1 > 0, "{:?}", state.1); 240 | 241 | let buffer = String::from_utf8_lossy(writer.get_ref()); 242 | assert!(buffer.contains("123"), "{buffer:?}"); 243 | } 244 | 245 | #[cfg(all(unix, not(any(feature = "async", feature = "polling"))))] 246 | #[test] 247 | fn interact_on_output_not_matched() { 248 | // Stops interact mode after 123 being read. 249 | // Which may cause it to stay buffered in session. 250 | // Verify this buffer was cleaned and 123 won't be accessed then. 251 | 252 | let reader = ListReaderWithDelayedEof::new( 253 | vec![ 254 | "QWE\n".to_string(), 255 | "123\n".to_string(), 256 | String::from_utf8_lossy(&[29]).to_string(), 257 | "WWW\n".to_string(), 258 | ], 259 | Duration::from_secs(2), 260 | ); 261 | let mut writer = io::Cursor::new(vec![0; 2048]); 262 | 263 | let mut input = Lookup::new(); 264 | 265 | let mut session = spawn("cat").unwrap(); 266 | 267 | let mut isession = session.interact(reader, &mut writer).with_state((0, 0)); 268 | isession 269 | .set_input_action(move |ctx| { 270 | if input.on(ctx.buf, ctx.eof, "QWE\n")?.is_some() { 271 | ctx.state.0 += 1; 272 | } 273 | 274 | if input.on(ctx.buf, ctx.eof, "WWW\n")?.is_some() { 275 | ctx.state.1 += 1; 276 | } 277 | 278 | Ok(false) 279 | }) 280 | .set_output_action(|_| Ok(false)) 281 | .set_idle_action(|_ctx| { 282 | std::thread::sleep(Duration::from_millis(500)); 283 | Ok(false) 284 | }); 285 | 286 | let is_alive = isession.spawn().unwrap(); 287 | 288 | let state = isession.into_state(); 289 | 290 | assert!(is_alive); 291 | 292 | assert_eq!(state.0, 2); 293 | assert_eq!(state.1, 0); 294 | 295 | let buffer = String::from_utf8_lossy(writer.get_ref()); 296 | let buffer = buffer.trim_end_matches(char::from(0)); 297 | assert_eq!(buffer, "QWE\r\nQWE\r\n123\r\n123\r\n"); 298 | 299 | session.send_line("WWW").unwrap(); 300 | 301 | let m = session.expect("WWW\r\n").unwrap(); 302 | assert_ne!(m.before(), b"123\r\n"); 303 | assert_eq!(m.before(), b""); 304 | } 305 | 306 | // #[cfg(unix)] 307 | // #[cfg(not(feature = "polling"))] 308 | // #[cfg(not(feature = "async"))] 309 | // #[test] 310 | // fn interact_stream_redirection() { 311 | // let commands = "Hello World\nIt works :)\n"; 312 | 313 | // let mut reader = ReaderWithDelayEof::new(commands, Duration::from_secs(4)); 314 | // let mut writer = io::Cursor::new(vec![0; 1024]); 315 | 316 | // let mut session = expectrl::spawn("cat").unwrap(); 317 | // let mut opts = expectrl::interact::InteractOptions::default(); 318 | 319 | // opts.interact(&mut session, &mut reader, &mut writer) 320 | // .unwrap(); 321 | 322 | // drop(opts); 323 | 324 | // let buffer = String::from_utf8_lossy(writer.get_ref()); 325 | // let buffer = buffer.trim_end_matches(char::from(0)); 326 | 327 | // assert_eq!(buffer, "Hello World\r\nIt works :)\r\n"); 328 | // } 329 | 330 | #[cfg(unix)] 331 | #[cfg(feature = "async")] 332 | #[test] 333 | fn interact_stream_redirection() { 334 | futures_lite::future::block_on(async { 335 | let commands = "Hello World\nIt works :)\n"; 336 | 337 | let reader = ReaderWithDelayEof::new(commands, Duration::from_secs(4)); 338 | let mut writer = AsyncWriter(io::Cursor::new(vec![0; 1024])); 339 | 340 | let mut session = expectrl::spawn("cat").unwrap(); 341 | 342 | session.interact(reader, &mut writer).spawn().await.unwrap(); 343 | 344 | let buffer = String::from_utf8_lossy(writer.0.get_ref()); 345 | let buffer = buffer.trim_end_matches(char::from(0)); 346 | 347 | assert_eq!( 348 | buffer, 349 | "Hello World\r\nIt works :)\r\nHello World\r\nIt works :)\r\n" 350 | ); 351 | }); 352 | } 353 | 354 | #[cfg(feature = "async")] 355 | #[test] 356 | fn interact_output_callback() { 357 | use expectrl::{ 358 | interact::{actions::lookup::Lookup, InteractSession}, 359 | stream::stdin::Stdin, 360 | }; 361 | 362 | let mut session = expectrl::spawn("sleep 1 && echo 'Hello World'").unwrap(); 363 | 364 | let mut stdin = Stdin::open().unwrap(); 365 | let stdout = AsyncWriter(std::io::sink()); 366 | 367 | let mut interact = InteractSession::new(&mut session, &mut stdin, stdout, (0, Lookup::new())); 368 | interact.set_output_action(|ctx| { 369 | if ctx.state.1.on(ctx.buf, ctx.eof, "World")?.is_some() { 370 | ctx.state.0 += 1; 371 | } 372 | 373 | Ok(false) 374 | }); 375 | futures_lite::future::block_on(interact.spawn()).unwrap(); 376 | 377 | let (state, _) = interact.into_state(); 378 | 379 | stdin.close().unwrap(); 380 | 381 | // fixme: sometimes it's 0 382 | // I guess because the process gets down to fast. 383 | 384 | assert!(matches!(state, 1 | 0), "{state:?}"); 385 | } 386 | 387 | struct ListReaderWithDelayedEof { 388 | lines: Vec, 389 | eof_timeout: Duration, 390 | now: Option, 391 | } 392 | 393 | impl ListReaderWithDelayedEof { 394 | #[cfg(not(feature = "async"))] 395 | fn new(lines: Vec, eof_timeout: Duration) -> Self { 396 | Self { 397 | lines, 398 | eof_timeout, 399 | now: None, 400 | } 401 | } 402 | } 403 | 404 | impl Read for ListReaderWithDelayedEof { 405 | fn read(&mut self, mut buf: &mut [u8]) -> io::Result { 406 | if self.now.is_none() { 407 | self.now = Some(Instant::now()); 408 | } 409 | 410 | if !self.lines.is_empty() { 411 | let line = self.lines.remove(0); 412 | buf.write_all(line.as_bytes())?; 413 | Ok(line.as_bytes().len()) 414 | } else if self.now.unwrap().elapsed() < self.eof_timeout { 415 | Err(io::Error::new(io::ErrorKind::WouldBlock, "")) 416 | } else { 417 | Ok(0) 418 | } 419 | } 420 | } 421 | 422 | #[cfg(unix)] 423 | impl std::os::unix::io::AsRawFd for ListReaderWithDelayedEof { 424 | fn as_raw_fd(&self) -> std::os::unix::prelude::RawFd { 425 | 0 426 | } 427 | } 428 | 429 | struct ReaderWithDelayEof { 430 | inner: Cursor, 431 | fire_timeout: Duration, 432 | now: Instant, 433 | } 434 | 435 | impl ReaderWithDelayEof 436 | where 437 | T: AsRef<[u8]>, 438 | { 439 | fn new(buf: T, timeout: Duration) -> Self { 440 | Self { 441 | inner: Cursor::new(buf), 442 | now: Instant::now(), 443 | fire_timeout: timeout, 444 | } 445 | } 446 | } 447 | 448 | impl Read for ReaderWithDelayEof 449 | where 450 | T: AsRef<[u8]>, 451 | { 452 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 453 | let n = self.inner.read(buf)?; 454 | if n == 0 && self.now.elapsed() < self.fire_timeout { 455 | Err(io::Error::new(io::ErrorKind::WouldBlock, "")) 456 | } else { 457 | Ok(n) 458 | } 459 | } 460 | } 461 | 462 | #[cfg(feature = "async")] 463 | impl futures_lite::AsyncRead for ReaderWithDelayEof 464 | where 465 | T: AsRef<[u8]> + Unpin, 466 | { 467 | fn poll_read( 468 | self: std::pin::Pin<&mut Self>, 469 | _cx: &mut std::task::Context<'_>, 470 | buf: &mut [u8], 471 | ) -> std::task::Poll> { 472 | let result = self.get_mut().read(buf); 473 | std::task::Poll::Ready(result) 474 | } 475 | } 476 | 477 | #[cfg(feature = "async")] 478 | struct AsyncWriter(W); 479 | 480 | #[cfg(feature = "async")] 481 | impl futures_lite::AsyncWrite for AsyncWriter 482 | where 483 | T: Write + Unpin, 484 | { 485 | fn poll_write( 486 | self: std::pin::Pin<&mut Self>, 487 | cx: &mut std::task::Context<'_>, 488 | buf: &[u8], 489 | ) -> std::task::Poll> { 490 | std::task::Poll::Ready(self.get_mut().0.write(buf)) 491 | } 492 | 493 | fn poll_flush( 494 | self: std::pin::Pin<&mut Self>, 495 | cx: &mut std::task::Context<'_>, 496 | ) -> std::task::Poll> { 497 | std::task::Poll::Ready(self.get_mut().0.flush()) 498 | } 499 | 500 | fn poll_close( 501 | self: std::pin::Pin<&mut Self>, 502 | cx: &mut std::task::Context<'_>, 503 | ) -> std::task::Poll> { 504 | std::task::Poll::Ready(Ok(())) 505 | } 506 | } 507 | -------------------------------------------------------------------------------- /tests/is_matched.rs: -------------------------------------------------------------------------------- 1 | #![cfg(unix)] 2 | 3 | use expectrl::{process::unix::WaitStatus, spawn, Eof, Expect, NBytes, Regex}; 4 | use std::thread; 5 | use std::time::Duration; 6 | 7 | #[cfg(feature = "async")] 8 | use expectrl::AsyncExpect; 9 | 10 | #[cfg(not(feature = "async"))] 11 | #[test] 12 | fn is_matched_str() { 13 | let mut session = spawn("cat").unwrap(); 14 | session.send_line("Hello World").unwrap(); 15 | thread::sleep(Duration::from_millis(600)); 16 | assert!(session.is_matched("Hello World").unwrap()); 17 | } 18 | 19 | #[cfg(unix)] 20 | #[cfg(feature = "async")] 21 | #[test] 22 | fn is_matched_str() { 23 | futures_lite::future::block_on(async { 24 | let mut session = spawn("cat").unwrap(); 25 | session.send_line("Hello World").await.unwrap(); 26 | thread::sleep(Duration::from_millis(600)); 27 | assert!(session.is_matched("Hello World").await.unwrap()); 28 | }) 29 | } 30 | 31 | #[cfg(unix)] 32 | #[cfg(not(feature = "async"))] 33 | #[test] 34 | fn is_matched_regex() { 35 | let mut session = spawn("cat").unwrap(); 36 | session.send_line("Hello World").unwrap(); 37 | 38 | thread::sleep(Duration::from_millis(600)); 39 | 40 | assert!(session.is_matched(Regex("lo.*")).unwrap()); 41 | } 42 | 43 | #[cfg(unix)] 44 | #[cfg(feature = "async")] 45 | #[test] 46 | fn is_matched_regex() { 47 | futures_lite::future::block_on(async { 48 | let mut session = spawn("cat").unwrap(); 49 | session.send_line("Hello World").await.unwrap(); 50 | 51 | thread::sleep(Duration::from_millis(600)); 52 | 53 | assert!(session.is_matched(Regex("lo.*")).await.unwrap()); 54 | }) 55 | } 56 | 57 | #[cfg(unix)] 58 | #[cfg(not(feature = "async"))] 59 | #[test] 60 | fn is_matched_bytes() { 61 | let mut session = spawn("cat").unwrap(); 62 | session.send_line("Hello World").unwrap(); 63 | 64 | thread::sleep(Duration::from_millis(600)); 65 | 66 | assert!(session.is_matched(NBytes(3)).unwrap()); 67 | } 68 | 69 | #[cfg(unix)] 70 | #[cfg(feature = "async")] 71 | #[test] 72 | fn is_matched_n_bytes() { 73 | futures_lite::future::block_on(async { 74 | let mut session = spawn("cat").unwrap(); 75 | session.send_line("Hello World").await.unwrap(); 76 | 77 | thread::sleep(Duration::from_millis(600)); 78 | 79 | assert!(session.is_matched(NBytes(3)).await.unwrap()); 80 | }) 81 | } 82 | 83 | #[cfg(target_os = "linux")] 84 | #[cfg(not(feature = "async"))] 85 | #[test] 86 | fn is_matched_eof() { 87 | let mut session = spawn("echo 'Hello World'").unwrap(); 88 | 89 | assert_eq!( 90 | session.get_process().wait().unwrap(), 91 | WaitStatus::Exited(session.get_process().pid(), 0), 92 | ); 93 | 94 | assert!(session.is_matched(Eof).unwrap()); 95 | } 96 | 97 | #[cfg(target_os = "linux")] 98 | #[cfg(feature = "async")] 99 | #[test] 100 | fn is_matched_eof() { 101 | futures_lite::future::block_on(async { 102 | let mut session = spawn("echo 'Hello World'").unwrap(); 103 | 104 | assert_eq!( 105 | WaitStatus::Exited(session.get_process().pid(), 0), 106 | session.get_process().wait().unwrap() 107 | ); 108 | 109 | assert!(!session.is_matched(Eof).await.unwrap()); 110 | assert!(session.is_matched(Eof).await.unwrap()); 111 | }) 112 | } 113 | 114 | #[cfg(unix)] 115 | #[cfg(not(feature = "async"))] 116 | #[test] 117 | fn read_after_is_matched() { 118 | use std::io::Read; 119 | 120 | let mut session = spawn("cat").unwrap(); 121 | session.send_line("Hello World").unwrap(); 122 | 123 | thread::sleep(Duration::from_millis(600)); 124 | 125 | assert!(session.is_matched("Hello").unwrap()); 126 | 127 | // we stop process so read operation will end up with EOF. 128 | // other wise read call would block. 129 | session.get_process_mut().exit(false).unwrap(); 130 | 131 | let mut buf = [0; 128]; 132 | let n = session.read(&mut buf).unwrap(); 133 | assert_eq!(&buf[..n], b"Hello World\r\n"); 134 | } 135 | 136 | #[cfg(unix)] 137 | #[cfg(feature = "async")] 138 | #[test] 139 | fn read_after_is_matched() { 140 | use futures_lite::io::AsyncReadExt; 141 | 142 | futures_lite::future::block_on(async { 143 | let mut session = spawn("cat").unwrap(); 144 | session.send_line("Hello World").await.unwrap(); 145 | 146 | thread::sleep(Duration::from_millis(600)); 147 | 148 | assert!(session.is_matched("Hello").await.unwrap()); 149 | 150 | // we stop process so read operation will end up with EOF. 151 | // other wise read call would block. 152 | session.get_process_mut().exit(false).unwrap(); 153 | 154 | let mut buf = [0; 128]; 155 | let n = session.read(&mut buf).await.unwrap(); 156 | assert_eq!(&buf[..n], b"Hello World\r\n"); 157 | }) 158 | } 159 | 160 | #[cfg(target_os = "linux")] 161 | #[cfg(not(feature = "async"))] 162 | #[test] 163 | fn check_after_is_matched_eof() { 164 | let mut p = spawn("echo AfterSleep").expect("cannot run echo"); 165 | assert_eq!( 166 | WaitStatus::Exited(p.get_process().pid(), 0), 167 | p.get_process().wait().unwrap() 168 | ); 169 | assert!(p.is_matched(Eof).unwrap()); 170 | 171 | let m = p.check(Eof).unwrap(); 172 | 173 | #[cfg(target_os = "linux")] 174 | assert_eq!(m.get(0).unwrap(), b"AfterSleep\r\n"); 175 | 176 | #[cfg(not(target_os = "linux"))] 177 | assert_eq!(m.get(0).unwrap(), b""); 178 | } 179 | 180 | #[cfg(target_os = "linux")] 181 | #[cfg(feature = "async")] 182 | #[test] 183 | fn check_after_is_matched_eof() { 184 | futures_lite::future::block_on(async { 185 | let mut p = spawn("echo AfterSleep").expect("cannot run echo"); 186 | assert_eq!( 187 | WaitStatus::Exited(p.get_process().pid(), 0), 188 | p.get_process().wait().unwrap() 189 | ); 190 | 191 | assert!(!p.is_matched(Eof).await.unwrap()); 192 | assert!(p.is_matched(Eof).await.unwrap()); 193 | 194 | let m = p.check(Eof).await.unwrap(); 195 | 196 | #[cfg(target_os = "linux")] 197 | assert_eq!(m.get(0).unwrap(), b"AfterSleep\r\n"); 198 | 199 | #[cfg(not(target_os = "linux"))] 200 | assert!(m.matches().len() == 0); 201 | }) 202 | } 203 | 204 | #[cfg(target_os = "linux")] 205 | #[cfg(not(feature = "async"))] 206 | #[test] 207 | fn expect_after_is_matched_eof() { 208 | let mut p = spawn("echo AfterSleep").expect("cannot run echo"); 209 | assert_eq!( 210 | WaitStatus::Exited(p.get_process().pid(), 0), 211 | p.get_process().wait().unwrap() 212 | ); 213 | assert!(p.is_matched(Eof).unwrap()); 214 | 215 | let m = p.expect(Eof).unwrap(); 216 | 217 | #[cfg(target_os = "linux")] 218 | assert_eq!(m.get(0).unwrap(), b"AfterSleep\r\n"); 219 | 220 | #[cfg(not(target_os = "linux"))] 221 | assert_eq!(m.get(0).unwrap(), b""); 222 | 223 | assert!(matches!(p.expect("").unwrap_err(), expectrl::Error::Eof)); 224 | } 225 | 226 | #[cfg(target_os = "linux")] 227 | #[cfg(feature = "async")] 228 | #[test] 229 | fn expect_after_is_matched_eof() { 230 | futures_lite::future::block_on(async { 231 | let mut p = spawn("echo AfterSleep").expect("cannot run echo"); 232 | assert_eq!( 233 | WaitStatus::Exited(p.get_process().pid(), 0), 234 | p.get_process().wait().unwrap() 235 | ); 236 | 237 | assert!(!p.is_matched(Eof).await.unwrap()); 238 | assert!(p.is_matched(Eof).await.unwrap()); 239 | 240 | let m = p.expect(Eof).await.unwrap(); 241 | 242 | #[cfg(target_os = "linux")] 243 | assert_eq!(m.get(0).unwrap(), b"AfterSleep\r\n"); 244 | 245 | #[cfg(not(target_os = "linux"))] 246 | assert!(m.matches().len() == 0); 247 | 248 | assert!(matches!( 249 | p.expect("").await.unwrap_err(), 250 | expectrl::Error::Eof 251 | )); 252 | }) 253 | } 254 | -------------------------------------------------------------------------------- /tests/log.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{self, prelude::*, Cursor}, 3 | sync::{Arc, Mutex}, 4 | thread, 5 | time::Duration, 6 | }; 7 | 8 | #[cfg(feature = "async")] 9 | use futures_lite::{AsyncBufReadExt, AsyncReadExt}; 10 | 11 | use expectrl::{session, spawn, Expect}; 12 | 13 | #[cfg(feature = "async")] 14 | use expectrl::AsyncExpect; 15 | 16 | #[test] 17 | #[cfg(windows)] 18 | #[cfg(not(feature = "async"))] 19 | fn log() { 20 | let writer = StubWriter::default(); 21 | let mut session = session::log( 22 | spawn("python ./tests/actions/cat/main.py").unwrap(), 23 | writer.clone(), 24 | ) 25 | .unwrap(); 26 | 27 | thread::sleep(Duration::from_millis(300)); 28 | 29 | session.send_line("Hello World").unwrap(); 30 | 31 | thread::sleep(Duration::from_millis(300)); 32 | 33 | let mut buf = vec![0; 1024]; 34 | let _ = session.read(&mut buf).unwrap(); 35 | 36 | let bytes = writer.inner.lock().unwrap(); 37 | let log_str = String::from_utf8_lossy(bytes.get_ref()); 38 | assert!(log_str.as_ref().contains("write")); 39 | assert!(log_str.as_ref().contains("read")); 40 | } 41 | 42 | #[test] 43 | #[cfg(windows)] 44 | #[cfg(feature = "async")] 45 | fn log() { 46 | futures_lite::future::block_on(async { 47 | let writer = StubWriter::default(); 48 | let mut session = session::log( 49 | spawn("python ./tests/actions/cat/main.py").unwrap(), 50 | writer.clone(), 51 | ) 52 | .unwrap(); 53 | thread::sleep(Duration::from_millis(300)); 54 | 55 | session.send_line("Hello World").await.unwrap(); 56 | 57 | thread::sleep(Duration::from_millis(300)); 58 | 59 | let mut buf = vec![0; 1024]; 60 | let _ = session.read(&mut buf).await.unwrap(); 61 | 62 | let bytes = writer.inner.lock().unwrap(); 63 | let log_str = String::from_utf8_lossy(bytes.get_ref()); 64 | assert!(log_str.as_ref().contains("write")); 65 | assert!(log_str.as_ref().contains("read")); 66 | }); 67 | } 68 | 69 | #[test] 70 | #[cfg(unix)] 71 | fn log() { 72 | let writer = StubWriter::default(); 73 | 74 | #[cfg(feature = "async")] 75 | futures_lite::future::block_on(async { 76 | let mut session = session::log(spawn("cat").unwrap(), writer.clone()).unwrap(); 77 | 78 | session.send_line("Hello World").await.unwrap(); 79 | 80 | // give some time to cat 81 | // since sometimes we doesn't keep up to read whole string 82 | thread::sleep(Duration::from_millis(300)); 83 | 84 | let mut buf = vec![0; 1024]; 85 | let _ = session.read(&mut buf).await.unwrap(); 86 | 87 | let bytes = writer.inner.lock().unwrap(); 88 | let text = String::from_utf8_lossy(bytes.get_ref()); 89 | assert!( 90 | text.contains("read") && text.contains("write"), 91 | "unexpected output {text:?}" 92 | ); 93 | }); 94 | 95 | #[cfg(not(feature = "async"))] 96 | { 97 | let mut session = session::log(spawn("cat").unwrap(), writer.clone()).unwrap(); 98 | 99 | session.send_line("Hello World").unwrap(); 100 | 101 | // give some time to cat 102 | // since sometimes we doesn't keep up to read whole string 103 | thread::sleep(Duration::from_millis(300)); 104 | 105 | let mut buf = vec![0; 1024]; 106 | let _ = session.read(&mut buf).unwrap(); 107 | 108 | let bytes = writer.inner.lock().unwrap(); 109 | let text = String::from_utf8_lossy(bytes.get_ref()); 110 | assert!( 111 | text.contains("read") && text.contains("write"), 112 | "unexpected output {text:?}" 113 | ); 114 | } 115 | } 116 | 117 | #[test] 118 | #[cfg(unix)] 119 | fn log_read_line() { 120 | let writer = StubWriter::default(); 121 | 122 | #[cfg(feature = "async")] 123 | futures_lite::future::block_on(async { 124 | let mut session = session::log(spawn("cat").unwrap(), writer.clone()).unwrap(); 125 | 126 | session.send_line("Hello World").await.unwrap(); 127 | 128 | let mut buf = String::new(); 129 | let _ = session.read_line(&mut buf).await.unwrap(); 130 | assert_eq!(buf, "Hello World\r\n"); 131 | 132 | let bytes = writer.inner.lock().unwrap(); 133 | let text = String::from_utf8_lossy(bytes.get_ref()); 134 | assert!( 135 | text.contains("read") && text.contains("write"), 136 | "unexpected output {text:?}" 137 | ); 138 | }); 139 | 140 | #[cfg(not(feature = "async"))] 141 | { 142 | let mut session = session::log(spawn("cat").unwrap(), writer.clone()).unwrap(); 143 | 144 | session.send_line("Hello World").unwrap(); 145 | 146 | let mut buf = String::new(); 147 | let _ = session.read_line(&mut buf).unwrap(); 148 | assert_eq!(buf, "Hello World\r\n"); 149 | 150 | let bytes = writer.inner.lock().unwrap(); 151 | let text = String::from_utf8_lossy(bytes.get_ref()); 152 | if !matches!( 153 | text.as_ref(), 154 | "write: \"Hello World\\n\"\nread: \"Hello World\"\nread: \"\\r\\n\"\n" 155 | | "write: \"Hello World\\n\"\nread: \"Hello World\\r\\n\"\n" 156 | | "write: \"Hello World\"\nwrite: \"\\n\"\nread: \"Hello World\\r\\n\"\n", 157 | ) { 158 | panic!("unexpected output {text:?}"); 159 | } 160 | } 161 | } 162 | 163 | #[derive(Debug, Clone, Default)] 164 | struct StubWriter { 165 | inner: Arc>>>, 166 | } 167 | 168 | impl Write for StubWriter { 169 | fn write(&mut self, buf: &[u8]) -> io::Result { 170 | self.inner.lock().unwrap().write(buf) 171 | } 172 | 173 | fn flush(&mut self) -> io::Result<()> { 174 | self.inner.lock().unwrap().flush() 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /tests/repl.rs: -------------------------------------------------------------------------------- 1 | #![cfg(unix)] 2 | 3 | use expectrl::{ 4 | process::unix::WaitStatus, 5 | repl::{spawn_bash, spawn_python}, 6 | ControlCode, Expect, 7 | }; 8 | #[cfg(feature = "async")] 9 | use futures_lite::io::AsyncBufReadExt; 10 | #[cfg(not(feature = "async"))] 11 | use std::io::BufRead; 12 | use std::{thread, time::Duration}; 13 | 14 | #[cfg(feature = "async")] 15 | use expectrl::AsyncExpect; 16 | 17 | #[cfg(not(feature = "async"))] 18 | #[cfg(target_os = "linux")] 19 | #[test] 20 | fn bash() { 21 | let mut p = spawn_bash().unwrap(); 22 | 23 | p.send_line("echo Hello World").unwrap(); 24 | let mut msg = String::new(); 25 | p.read_line(&mut msg).unwrap(); 26 | assert!(msg.ends_with("Hello World\r\n")); 27 | 28 | p.send(ControlCode::EOT).unwrap(); 29 | 30 | p.get_session_mut().get_process_mut().exit(true).unwrap(); 31 | } 32 | 33 | #[cfg(not(feature = "async"))] 34 | #[cfg(target_os = "linux")] 35 | #[test] 36 | fn bash_with_log() { 37 | use expectrl::{repl::ReplSession, session}; 38 | 39 | let p = spawn_bash().unwrap(); 40 | let prompt = p.get_prompt().to_owned(); 41 | let quit_cmd = p 42 | .get_quit_command() 43 | .map(|c| c.to_owned()) 44 | .unwrap_or("exit".to_owned()); 45 | let is_echo = p.is_echo(); 46 | let session = session::log(p.into_session(), std::io::stderr()).unwrap(); 47 | let mut p = ReplSession::new(session, prompt); 48 | p.set_quit_command(quit_cmd); 49 | p.set_echo(is_echo); 50 | 51 | p.send_line("echo Hello World").unwrap(); 52 | let mut msg = String::new(); 53 | p.read_line(&mut msg).unwrap(); 54 | assert!(msg.ends_with("Hello World\r\n")); 55 | 56 | thread::sleep(Duration::from_millis(300)); 57 | p.send(ControlCode::EOT).unwrap(); 58 | 59 | assert_eq!( 60 | p.get_session().get_process().wait().unwrap(), 61 | WaitStatus::Exited(p.get_session().get_process().pid(), 0) 62 | ); 63 | } 64 | 65 | #[cfg(feature = "async")] 66 | #[cfg(not(target_os = "macos"))] 67 | #[test] 68 | fn bash() { 69 | futures_lite::future::block_on(async { 70 | let mut p = spawn_bash().await.unwrap(); 71 | 72 | p.send_line("echo Hello World").await.unwrap(); 73 | let mut msg = String::new(); 74 | p.read_line(&mut msg).await.unwrap(); 75 | assert!(msg.ends_with("Hello World\r\n")); 76 | 77 | thread::sleep(Duration::from_millis(300)); 78 | p.send(ControlCode::EOT).await.unwrap(); 79 | 80 | let proc = p.get_session().get_process(); 81 | assert_eq!(proc.wait().unwrap(), WaitStatus::Exited(proc.pid(), 0)); 82 | }) 83 | } 84 | 85 | #[cfg(feature = "async")] 86 | #[cfg(not(target_os = "macos"))] 87 | #[test] 88 | fn bash_with_log() { 89 | futures_lite::future::block_on(async { 90 | use expectrl::{repl::ReplSession, session}; 91 | 92 | let p = spawn_bash().await.unwrap(); 93 | let prompt = p.get_prompt().to_owned(); 94 | let quit_cmd = p 95 | .get_quit_command() 96 | .map(|c| c.to_owned()) 97 | .unwrap_or_default(); 98 | let is_echo = p.is_echo(); 99 | let session = session::log(p.into_session(), std::io::stderr()).unwrap(); 100 | let mut p = ReplSession::new(session, prompt); 101 | p.set_quit_command(quit_cmd); 102 | p.set_echo(is_echo); 103 | 104 | p.send_line("echo Hello World").await.unwrap(); 105 | let mut msg = String::new(); 106 | p.read_line(&mut msg).await.unwrap(); 107 | assert!(msg.ends_with("Hello World\r\n")); 108 | 109 | thread::sleep(Duration::from_millis(300)); 110 | p.send(ControlCode::EOT).await.unwrap(); 111 | 112 | let proc = p.get_session().get_process(); 113 | assert_eq!(proc.wait().unwrap(), WaitStatus::Exited(proc.pid(), 0)); 114 | }) 115 | } 116 | 117 | #[cfg(not(feature = "async"))] 118 | #[test] 119 | fn python() { 120 | let mut p = spawn_python().unwrap(); 121 | 122 | let prompt = p.execute("print('Hello World')").unwrap(); 123 | let prompt = String::from_utf8_lossy(&prompt); 124 | assert!(prompt.contains("Hello World"), "{prompt:?}"); 125 | 126 | thread::sleep(Duration::from_millis(300)); 127 | p.send(ControlCode::EndOfText).unwrap(); 128 | thread::sleep(Duration::from_millis(300)); 129 | 130 | let mut msg = String::new(); 131 | p.read_line(&mut msg).unwrap(); 132 | assert!(msg.contains("\r\n"), "{msg:?}"); 133 | 134 | let mut msg = String::new(); 135 | p.read_line(&mut msg).unwrap(); 136 | assert_eq!(msg, "KeyboardInterrupt\r\n"); 137 | 138 | p.expect_prompt().unwrap(); 139 | 140 | p.send(ControlCode::EndOfTransmission).unwrap(); 141 | 142 | assert_eq!( 143 | p.get_session().get_process().wait().unwrap(), 144 | WaitStatus::Exited(p.get_session().get_process().pid(), 0) 145 | ); 146 | } 147 | 148 | #[cfg(feature = "async")] 149 | #[test] 150 | fn python() { 151 | futures_lite::future::block_on(async { 152 | let mut p = spawn_python().await.unwrap(); 153 | 154 | let prompt = p.execute("print('Hello World')").await.unwrap(); 155 | let prompt = String::from_utf8_lossy(&prompt); 156 | assert!(prompt.contains("Hello World"), "{prompt:?}"); 157 | 158 | thread::sleep(Duration::from_millis(300)); 159 | p.send(ControlCode::EndOfText).await.unwrap(); 160 | thread::sleep(Duration::from_millis(300)); 161 | 162 | let mut msg = String::new(); 163 | p.read_line(&mut msg).await.unwrap(); 164 | assert!(msg.contains("\r\n"), "{msg:?}"); 165 | 166 | let mut msg = String::new(); 167 | p.read_line(&mut msg).await.unwrap(); 168 | assert_eq!(msg, "KeyboardInterrupt\r\n"); 169 | 170 | p.expect_prompt().await.unwrap(); 171 | 172 | p.send(ControlCode::EndOfTransmission).await.unwrap(); 173 | 174 | let proc = p.get_session().get_process(); 175 | assert_eq!(proc.wait().unwrap(), WaitStatus::Exited(proc.pid(), 0)); 176 | }) 177 | } 178 | 179 | #[cfg(feature = "async")] 180 | #[test] 181 | fn bash_pwd() { 182 | futures_lite::future::block_on(async { 183 | let mut p = spawn_bash().await.unwrap(); 184 | p.execute("cd /tmp/").await.unwrap(); 185 | p.send_line("pwd").await.unwrap(); 186 | let mut pwd = String::new(); 187 | p.read_line(&mut pwd).await.unwrap(); 188 | assert!(pwd.contains("/tmp\r\n")); 189 | }); 190 | } 191 | 192 | #[cfg(feature = "async")] 193 | #[test] 194 | fn bash_control_chars() { 195 | futures_lite::future::block_on(async { 196 | let mut p = spawn_bash().await.unwrap(); 197 | p.send_line("cat <(echo ready) -").await.unwrap(); 198 | thread::sleep(Duration::from_millis(100)); 199 | p.send(ControlCode::EndOfText).await.unwrap(); // abort: SIGINT 200 | p.expect_prompt().await.unwrap(); 201 | p.send_line("cat <(echo ready) -").await.unwrap(); 202 | thread::sleep(Duration::from_millis(100)); 203 | p.send(ControlCode::Substitute).await.unwrap(); // suspend:SIGTSTPcon 204 | p.expect_prompt().await.unwrap(); 205 | }); 206 | } 207 | 208 | #[cfg(not(feature = "async"))] 209 | #[test] 210 | fn bash_pwd() { 211 | let mut p = spawn_bash().unwrap(); 212 | p.execute("cd /tmp/").unwrap(); 213 | p.send_line("pwd").unwrap(); 214 | let mut pwd = String::new(); 215 | p.read_line(&mut pwd).unwrap(); 216 | assert!(pwd.contains("/tmp\r\n")); 217 | } 218 | 219 | #[cfg(not(feature = "async"))] 220 | #[test] 221 | fn bash_control_chars() { 222 | let mut p = spawn_bash().unwrap(); 223 | p.send_line("cat <(echo ready) -").unwrap(); 224 | thread::sleep(Duration::from_millis(300)); 225 | p.send(ControlCode::EndOfText).unwrap(); // abort: SIGINT 226 | p.expect_prompt().unwrap(); 227 | p.send_line("cat <(echo ready) -").unwrap(); 228 | thread::sleep(Duration::from_millis(100)); 229 | p.send(ControlCode::Substitute).unwrap(); // suspend:SIGTSTPcon 230 | p.expect_prompt().unwrap(); 231 | } 232 | -------------------------------------------------------------------------------- /tests/session.rs: -------------------------------------------------------------------------------- 1 | use expectrl::{session::OsSession, spawn, Expect}; 2 | 3 | #[cfg(feature = "async")] 4 | use futures_lite::io::{AsyncReadExt, AsyncWriteExt}; 5 | 6 | #[cfg(not(feature = "async"))] 7 | #[cfg(not(windows))] 8 | use std::io::{Read, Write}; 9 | 10 | #[cfg(feature = "async")] 11 | use expectrl::AsyncExpect; 12 | 13 | #[cfg(unix)] 14 | #[cfg(not(feature = "async"))] 15 | #[test] 16 | fn send() { 17 | let mut session = spawn("cat").unwrap(); 18 | session.send("Hello World").unwrap(); 19 | 20 | session.write_all(&[3]).unwrap(); // Ctrl+C 21 | session.flush().unwrap(); 22 | 23 | let mut buf = String::new(); 24 | session.read_to_string(&mut buf).unwrap(); 25 | 26 | // cat doesn't printed anything 27 | assert_eq!(buf, ""); 28 | } 29 | 30 | #[cfg(unix)] 31 | #[cfg(feature = "async")] 32 | #[test] 33 | fn send() { 34 | futures_lite::future::block_on(async { 35 | let mut session = spawn("cat").unwrap(); 36 | session.send("Hello World").await.unwrap(); 37 | 38 | session.write_all(&[3]).await.unwrap(); // Ctrl+C 39 | session.flush().await.unwrap(); 40 | 41 | let mut buf = String::new(); 42 | session.read_to_string(&mut buf).await.unwrap(); 43 | 44 | // cat doesn't printed anything 45 | assert_eq!(buf, ""); 46 | }) 47 | } 48 | 49 | #[cfg(windows)] 50 | #[test] 51 | fn send() { 52 | use std::io::Write; 53 | 54 | let mut session = spawn("python ./tests/actions/cat/main.py").unwrap(); 55 | #[cfg(not(feature = "async"))] 56 | { 57 | session.write(b"Hello World").unwrap(); 58 | session.expect("Hello World").unwrap(); 59 | } 60 | #[cfg(feature = "async")] 61 | { 62 | futures_lite::future::block_on(async { 63 | session.write(b"Hello World").await.unwrap(); 64 | session.expect("Hello World").await.unwrap(); 65 | }) 66 | } 67 | } 68 | 69 | #[cfg(unix)] 70 | #[cfg(not(feature = "async"))] 71 | #[test] 72 | fn send_multiline() { 73 | let mut session = spawn("cat").unwrap(); 74 | session.send("Hello World\n").unwrap(); 75 | 76 | let m = session.expect('\n').unwrap(); 77 | let buf = String::from_utf8_lossy(m.before()); 78 | 79 | assert_eq!(buf, "Hello World\r"); 80 | 81 | session.get_process_mut().exit(true).unwrap(); 82 | } 83 | 84 | #[cfg(unix)] 85 | #[cfg(feature = "async")] 86 | #[test] 87 | fn send_multiline() { 88 | futures_lite::future::block_on(async { 89 | let mut session = spawn("cat").unwrap(); 90 | session.send("Hello World\n").await.unwrap(); 91 | 92 | let m = session.expect('\n').await.unwrap(); 93 | let buf = String::from_utf8_lossy(m.before()); 94 | 95 | assert_eq!(buf, "Hello World\r"); 96 | 97 | session.get_process_mut().exit(true).unwrap(); 98 | }) 99 | } 100 | 101 | #[cfg(windows)] 102 | #[test] 103 | fn send_multiline() { 104 | let mut session = spawn("python ./tests/actions/cat/main.py").unwrap(); 105 | #[cfg(not(feature = "async"))] 106 | { 107 | session.send("Hello World\r\n").unwrap(); 108 | let m = session.expect('\n').unwrap(); 109 | let buf = String::from_utf8_lossy(m.before()); 110 | assert!(buf.contains("Hello World"), "{:?}", buf); 111 | session.get_process_mut().exit(0).unwrap(); 112 | } 113 | #[cfg(feature = "async")] 114 | { 115 | use futures_lite::{AsyncBufReadExt, StreamExt}; 116 | 117 | futures_lite::future::block_on(async { 118 | session.send("Hello World\r\n").await.unwrap(); 119 | let m = session.expect('\n').await.unwrap(); 120 | let buf = String::from_utf8_lossy(m.before()); 121 | assert!(buf.contains("Hello World"), "{:?}", buf); 122 | session.get_process_mut().exit(0).unwrap(); 123 | }) 124 | } 125 | } 126 | 127 | #[cfg(unix)] 128 | #[cfg(not(feature = "async"))] 129 | #[test] 130 | fn send_line() { 131 | let mut session = spawn("cat").unwrap(); 132 | session.send_line("Hello World").unwrap(); 133 | 134 | let m = session.expect('\n').unwrap(); 135 | let buf = String::from_utf8_lossy(m.before()); 136 | 137 | assert_eq!(buf, "Hello World\r"); 138 | 139 | session.get_process_mut().exit(true).unwrap(); 140 | } 141 | 142 | #[cfg(unix)] 143 | #[cfg(feature = "async")] 144 | #[test] 145 | fn send_line() { 146 | futures_lite::future::block_on(async { 147 | let mut session = spawn("cat").unwrap(); 148 | session.send_line("Hello World").await.unwrap(); 149 | 150 | let m = session.expect('\n').await.unwrap(); 151 | let buf = String::from_utf8_lossy(m.before()); 152 | 153 | assert_eq!(buf, "Hello World\r"); 154 | session.get_process_mut().exit(true).unwrap(); 155 | }) 156 | } 157 | 158 | #[cfg(windows)] 159 | #[test] 160 | fn send_line() { 161 | let mut session = spawn("python ./tests/actions/cat/main.py").unwrap(); 162 | #[cfg(not(feature = "async"))] 163 | { 164 | session.send_line("Hello World").unwrap(); 165 | let m = session.expect('\n').unwrap(); 166 | let buf = String::from_utf8_lossy(m.before()); 167 | assert!(buf.contains("Hello World"), "{:?}", buf); 168 | session.get_process_mut().exit(0).unwrap(); 169 | } 170 | #[cfg(feature = "async")] 171 | { 172 | use futures_lite::{AsyncBufReadExt, StreamExt}; 173 | 174 | futures_lite::future::block_on(async { 175 | session.send_line("Hello World").await.unwrap(); 176 | let m = session.expect('\n').await.unwrap(); 177 | let buf = String::from_utf8_lossy(m.before()); 178 | assert!(buf.contains("Hello World"), "{:?}", buf); 179 | session.get_process_mut().exit(0).unwrap(); 180 | }) 181 | } 182 | } 183 | 184 | #[test] 185 | fn test_spawn_no_command() { 186 | #[cfg(unix)] 187 | assert!(spawn("").is_err()); 188 | #[cfg(windows)] 189 | assert!(spawn("").is_ok()); 190 | } 191 | 192 | #[test] 193 | #[ignore = "it's a compile time check"] 194 | fn test_session_as_writer() { 195 | #[cfg(not(feature = "async"))] 196 | { 197 | let _: Box = Box::new(spawn("ls").unwrap()); 198 | let _: Box = Box::new(spawn("ls").unwrap()); 199 | let _: Box = Box::new(spawn("ls").unwrap()); 200 | 201 | fn _io_copy(mut session: OsSession) { 202 | let _ = std::io::copy(&mut std::io::empty(), &mut session).unwrap(); 203 | } 204 | } 205 | #[cfg(feature = "async")] 206 | { 207 | let _: Box = 208 | Box::new(spawn("ls").unwrap()) as Box; 209 | let _: Box = 210 | Box::new(spawn("ls").unwrap()) as Box; 211 | let _: Box = 212 | Box::new(spawn("ls").unwrap()) as Box; 213 | 214 | async fn _io_copy(mut session: OsSession) { 215 | futures_lite::io::copy(futures_lite::io::empty(), &mut session) 216 | .await 217 | .unwrap(); 218 | } 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /tests/source/ansi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # The file was developed by https://github.com/GaryBoone/ 4 | 5 | from colors import colorize, Color 6 | import sys 7 | import time 8 | import getpass 9 | 10 | NUM_LINES = 5 11 | SAME_LINE_ESC = "\033[F" 12 | 13 | def main(): 14 | """Demonstrate several kinds of terminal outputs. 15 | 16 | Examples including ANSI codes, "\r" without "\n", writing to stdin, no-echo 17 | inputs. 18 | """ 19 | 20 | # Show a color. 21 | print("status: ", colorize("good", Color.GREEN)) 22 | 23 | # Show same-line output via "\r". 24 | for i in range(NUM_LINES): 25 | sys.stdout.write(f"[{i+1}/{NUM_LINES}]: file{i}\r") 26 | time.sleep(1) 27 | print("\n") 28 | 29 | # Show same-line output via an ANSI code. 30 | for i in range(NUM_LINES): 31 | print(f"{SAME_LINE_ESC}[{i+1}/{NUM_LINES}]: file{i}") 32 | time.sleep(1) 33 | 34 | # Handle prompts which don't repeat input to stdout. 35 | print("Here is a test password prompt") 36 | print(colorize("Do not enter a real password", Color.RED)) 37 | getpass.getpass() 38 | 39 | # Handle simple input. 40 | ans = input("Continue [y/n]:") 41 | col = Color.GREEN if ans == "y" else Color.RED 42 | print(f"You said: {colorize(ans, col)}") 43 | if ans == "n" or ans == "": 44 | sys.exit(0) 45 | 46 | # Handle long-running process, like starting a server. 47 | print("[Starting long running process...]") 48 | print("[Ctrl-C to exit]") 49 | while True: 50 | print("status: ", colorize("good", Color.GREEN)) 51 | time.sleep(1) 52 | 53 | if __name__ == "__main__": 54 | main() 55 | -------------------------------------------------------------------------------- /tests/source/colors.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | class Color(enum.Enum): 4 | RESET = "\33[0m" 5 | RED = "\33[31m" 6 | GREEN = "\33[32m" 7 | YELLOW = "\33[33m" 8 | 9 | 10 | def colorize(text: str, color: Color) -> str: 11 | """Wrap `text` in terminal color directives. 12 | 13 | The return value will show up as the given color when printed in a terminal. 14 | """ 15 | return f"{color.value}{text}{Color.RESET.value}" 16 | --------------------------------------------------------------------------------