expectrl-0.7.1/.cargo_vcs_info.json0000644000000001360000000000100126670ustar { "git": { "sha1": "e8b43ff1bba3e4908733a2ae37e4528ef02401b3" }, "path_in_vcs": "" }expectrl-0.7.1/.cirrus.yml000064400000000000000000000003131046102023000135640ustar 00000000000000freebsd_instance: image_family: freebsd-13-1 task: timeout_in: 5m install_script: # - pkg update -f - pkg upgrade -f --yes - pkg install -y rust bash python script: - cargo test expectrl-0.7.1/.github/workflows/ci.yml000064400000000000000000000045201046102023000161730ustar 00000000000000name: Build on: push: branches: - main pull_request: {} env: CARGO_TERM_COLOR: always jobs: check: name: Check strategy: fail-fast: false matrix: rust: [nightly, stable] platform: [ubuntu-latest, macos-latest, windows-latest] features: ["''", "polling", "async", "polling,async"] runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: ${{ matrix.rust || 'stable' }} override: true - uses: actions-rs/cargo@v1 with: command: check args: --no-default-features --features ${{ matrix.features }} check-tests: name: Check Tests strategy: fail-fast: false matrix: rust: [nightly, stable] platform: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: ${{ matrix.rust || 'stable' }} override: true - uses: actions-rs/cargo@v1 with: command: check args: --tests test: name: Test Suite strategy: fail-fast: false matrix: platform: [ubuntu-latest, macos-latest] feauture: ["", "--features async"] runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v2 - uses: actions-rs/cargo@v1 with: command: test args: --verbose ${{ matrix.feauture }} fmt: name: Rustfmt runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - run: rustup component add rustfmt - uses: actions-rs/cargo@v1 with: command: fmt args: --all -- --check clippy: name: Clippy runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - run: rustup component add clippy - uses: actions-rs/cargo@v1 with: command: clippy args: -- -D warnings expectrl-0.7.1/.github/workflows/coverage.yml000064400000000000000000000012531046102023000173730ustar 00000000000000name: Code Coverage on: push: branches: - main pull_request: {} env: CARGO_TERM_COLOR: always jobs: coverage: name: Coveralls runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - name: Run cargo-tarpaulin uses: actions-rs/tarpaulin@v0.1 with: version: "0.15.0" args: "--workspace --out Lcov --output-dir ./coverage" - name: Upload to Coveralls uses: coverallsapp/github-action@master with: github-token: ${{ secrets.GITHUB_TOKEN }}expectrl-0.7.1/.gitignore000064400000000000000000000005451046102023000134530ustar 00000000000000# Generated by Cargo # will have compiled files and executables /target/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk # Python cache **/*.pyc __pycache__ expectrl-0.7.1/Cargo.lock0000644000000301670000000000100106510ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "aho-corasick" version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" dependencies = [ "memchr", ] [[package]] name = "async-channel" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" dependencies = [ "concurrent-queue 2.1.0", "event-listener", "futures-core", ] [[package]] name = "async-io" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83e21f3a490c72b3b0cf44962180e60045de2925d8dff97918f7ee43c8f637c7" dependencies = [ "autocfg", "concurrent-queue 1.2.2", "futures-lite", "libc", "log", "once_cell", "parking", "polling", "slab", "socket2", "waker-fn", "winapi", ] [[package]] name = "async-lock" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7" dependencies = [ "event-listener", ] [[package]] name = "async-task" version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae" [[package]] name = "atomic-waker" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "debc29dde2e69f9e47506b525f639ed42300fc014a3e007832592448fa8e4599" [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "bitflags" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" [[package]] name = "blocking" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c67b173a56acffd6d2326fb7ab938ba0b00a71480e14902b2591c87bc5741e8" dependencies = [ "async-channel", "async-lock", "async-task", "atomic-waker", "fastrand", "futures-lite", ] [[package]] name = "cache-padded" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" [[package]] name = "cc" version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "concurrent-queue" version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" dependencies = [ "cache-padded", ] [[package]] name = "concurrent-queue" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c278839b831783b70278b14df4d45e1beb1aad306c07bb796637de9a0e323e8e" dependencies = [ "crossbeam-utils", ] [[package]] name = "conpty" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9dba80d29fb495d43f79e69d0f49e5c0ba05a71aea873c37d4152a33951e" dependencies = [ "windows", ] [[package]] name = "crossbeam-channel" version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf2b3e8478797446514c91ef04bafcb59faba183e621ad488df88983cc14128c" dependencies = [ "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" dependencies = [ "cfg-if", ] [[package]] name = "event-listener" version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "expectrl" version = "0.7.1" dependencies = [ "async-io", "blocking", "conpty", "crossbeam-channel", "futures-lite", "futures-timer", "nix", "polling", "ptyprocess", "regex", ] [[package]] name = "fastrand" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" dependencies = [ "instant", ] [[package]] name = "futures-core" version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" [[package]] name = "futures-io" version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" [[package]] name = "futures-lite" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" dependencies = [ "fastrand", "futures-core", "futures-io", "memchr", "parking", "pin-project-lite", "waker-fn", ] [[package]] name = "futures-timer" version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" [[package]] name = "instant" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if", ] [[package]] name = "libc" version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" [[package]] name = "log" version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ "cfg-if", ] [[package]] name = "memchr" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "memoffset" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" dependencies = [ "autocfg", ] [[package]] name = "nix" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" dependencies = [ "bitflags", "cfg-if", "libc", "memoffset", "pin-utils", "static_assertions", ] [[package]] name = "once_cell" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" [[package]] name = "parking" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" [[package]] name = "pin-project-lite" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "polling" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899b00b9c8ab553c743b3e11e87c5c7d423b2a2de229ba95b24a756344748011" dependencies = [ "autocfg", "cfg-if", "libc", "log", "wepoll-ffi", "winapi", ] [[package]] name = "ptyprocess" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e05aef7befb11a210468a2d77d978dde2c6381a0381e33beb575e91f57fe8cf" dependencies = [ "nix", ] [[package]] name = "regex" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.6.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" [[package]] name = "slab" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" [[package]] name = "socket2" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" dependencies = [ "libc", "winapi", ] [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "waker-fn" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" [[package]] name = "wepoll-ffi" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" dependencies = [ "cc", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_i686_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_x86_64_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" expectrl-0.7.1/Cargo.toml0000644000000036150000000000100106720ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "expectrl" version = "0.7.1" authors = ["Maxim Zhiburt "] description = "A tool for automating terminal applications in Unix like Don libes expect" homepage = "https://github.com/zhiburt/expectrl" documentation = "https://docs.rs/expectrl" readme = "README.md" keywords = [ "expect", "pty", "testing", "terminal", "automation", ] categories = [ "development-tools::testing", "os::unix-apis", "os::windows-apis", ] license = "MIT" repository = "https://github.com/zhiburt/expectrl" resolver = "2" [package.metadata.docs.rs] all-features = false [dependencies.futures-lite] version = "1.12.0" optional = true [dependencies.futures-timer] version = "3.0.2" optional = true [dependencies.regex] version = "1.6.0" [features] async = [ "futures-lite", "futures-timer", "async-io", "blocking", ] polling = [ "dep:polling", "dep:crossbeam-channel", ] [target."cfg(unix)".dependencies.async-io] version = "1.9.0" optional = true [target."cfg(unix)".dependencies.nix] version = "0.26" [target."cfg(unix)".dependencies.polling] version = "2.3.0" optional = true [target."cfg(unix)".dependencies.ptyprocess] version = "0.4.1" [target."cfg(windows)".dependencies.blocking] version = "1.2.0" optional = true [target."cfg(windows)".dependencies.conpty] version = "0.5.0" [target."cfg(windows)".dependencies.crossbeam-channel] version = "0.5.6" optional = true expectrl-0.7.1/Cargo.toml.orig000064400000000000000000000023401046102023000143450ustar 00000000000000[package] name = "expectrl" version = "0.7.1" authors = ["Maxim Zhiburt "] edition = "2021" resolver = "2" description = "A tool for automating terminal applications in Unix like Don libes expect" repository = "https://github.com/zhiburt/expectrl" homepage = "https://github.com/zhiburt/expectrl" documentation = "https://docs.rs/expectrl" license = "MIT" categories = ["development-tools::testing", "os::unix-apis", "os::windows-apis"] keywords = ["expect", "pty", "testing", "terminal", "automation"] readme = "README.md" [features] # "pooling" feature works only for not async version polling = ["dep:polling", "dep:crossbeam-channel"] async = ["futures-lite", "futures-timer", "async-io", "blocking"] [dependencies] regex = "1.6.0" futures-lite = { version = "1.12.0", optional = true } futures-timer = { version = "3.0.2", optional = true } [target.'cfg(unix)'.dependencies] ptyprocess = "0.4.1" nix = "0.26" async-io = { version = "1.9.0", optional = true } polling = { version = "2.3.0", optional = true } [target.'cfg(windows)'.dependencies] conpty = "0.5.0" blocking = { version = "1.2.0", optional = true } crossbeam-channel = { version = "0.5.6", optional = true } [package.metadata.docs.rs] all-features = false expectrl-0.7.1/LICENSE000064400000000000000000000020561046102023000124670ustar 00000000000000MIT License Copyright (c) 2021 Maxim Zhiburt Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. expectrl-0.7.1/README.md000064400000000000000000000066111046102023000127420ustar 00000000000000[![Build](https://github.com/zhiburt/expectrl/actions/workflows/ci.yml/badge.svg)](https://github.com/zhiburt/expectrl/actions/workflows/ci.yml) [![coverage status](https://coveralls.io/repos/github/zhiburt/expectrl/badge.svg?branch=main)](https://coveralls.io/github/zhiburt/expectrl?branch=main) [![crate](https://img.shields.io/crates/v/expectrl)](https://crates.io/crates/expectrl) [![docs.rs](https://img.shields.io/docsrs/expectrl?color=blue)](https://docs.rs/expectrl/*/expectrl/) # expectrl Expectrl is a tool for automating terminal applications. 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. Using the library you can: - Spawn process - Control process - Interact with process's IO(input/output). `expectrl` like original `expect` may shine when you're working with interactive applications. If your application is not interactive you may not find the library the best choise. ## Usage Add `expectrl` to your Cargo.toml. ```toml # Cargo.toml [dependencies] expectrl = "0.7" ``` An example where the program simulates a used interacting with `ftp`. ```rust use expectrl::{spawn, Regex, Eof, Error}; fn main() -> Result<(), Error> { let mut p = spawn("ftp speedtest.tele2.net")?; p.expect(Regex("Name \\(.*\\):"))?; p.send_line("anonymous")?; p.expect("Password")?; p.send_line("test")?; p.expect("ftp>")?; p.send_line("cd upload")?; p.expect("successfully changed.\r\nftp>")?; p.send_line("pwd")?; p.expect(Regex("[0-9]+ \"/upload\""))?; p.send_line("exit")?; p.expect(Eof)?; Ok(()) } ``` The same example but the password will be read from stdin. ```rust use std::io::stdout; use expectrl::{ interact::{actions::lookup::Lookup, InteractOptions}, spawn, stream::stdin::Stdin, ControlCode, Error, Regex, }; fn main() -> Result<(), Error> { let mut auth = false; let mut login_lookup = Lookup::new(); let opts = InteractOptions::new(&mut auth).on_output(|ctx| { if login_lookup .on(ctx.buf, ctx.eof, "Login successful")? .is_some() { **ctx.state = true; return Ok(true); } Ok(false) }); let mut p = spawn("ftp bks4-speedtest-1.tele2.net")?; let mut stdin = Stdin::open()?; p.interact(&mut stdin, stdout()).spawn(opts)?; stdin.close()?; if !auth { println!("An authefication was not passed"); return Ok(()); } p.expect("ftp>")?; p.send_line("cd upload")?; p.expect("successfully changed.")?; p.send_line("pwd")?; p.expect(Regex("[0-9]+ \"/upload\""))?; p.send(ControlCode::EndOfTransmission)?; p.expect("Goodbye.")?; Ok(()) } ``` #### [For more examples, check the examples directory.](https://github.com/zhiburt/expectrl/tree/main/examples) ## Features - It has an `async` support (To enable them you must turn on an `async` feature). - It supports logging. - It supports interact function. - It works on windows. ## Notes It was originally inspired by [philippkeller/rexpect] and [pexpect]. Licensed under [MIT License](LICENSE) [philippkeller/rexpect]: https://github.com/philippkeller/rexpect [pexpect]: https://pexpect.readthedocs.io/en/stable/overview.html expectrl-0.7.1/examples/bash.rs000064400000000000000000000064061046102023000145660ustar 00000000000000// An example is based on README.md from https://github.com/philippkeller/rexpect #[cfg(unix)] use expectrl::{repl::spawn_bash, ControlCode, Regex}; #[cfg(unix)] #[cfg(not(feature = "async"))] fn main() { let mut p = spawn_bash().unwrap(); // case 1: execute let hostname = p.execute("hostname").unwrap(); println!("Current hostname: {:?}", String::from_utf8_lossy(&hostname)); // case 2: wait until done, only extract a few infos p.send_line("wc /etc/passwd").unwrap(); // `exp_regex` returns both string-before-match and match itself, discard first let lines = p.expect(Regex("[0-9]+")).unwrap(); let words = p.expect(Regex("[0-9]+")).unwrap(); let bytes = p.expect(Regex("[0-9]+")).unwrap(); p.expect_prompt().unwrap(); // go sure `wc` is really done println!( "/etc/passwd has {} lines, {} words, {} chars", String::from_utf8_lossy(&lines[0]), String::from_utf8_lossy(&words[0]), String::from_utf8_lossy(&bytes[0]), ); // case 3: read while program is still executing p.send_line("ping 8.8.8.8").unwrap(); // returns when it sees "bytes of data" in output for _ in 0..5 { // times out if one ping takes longer than 2s let duration = p.expect(Regex("[0-9. ]+ ms")).unwrap(); println!("Roundtrip time: {}", String::from_utf8_lossy(&duration[0])); } p.send(ControlCode::EOT).unwrap(); } #[cfg(unix)] #[cfg(feature = "async")] fn main() { use futures_lite::io::AsyncBufReadExt; futures_lite::future::block_on(async { let mut p = spawn_bash().await.unwrap(); // case 1: wait until program is done p.send_line("hostname").await.unwrap(); let mut hostname = String::new(); p.read_line(&mut hostname).await.unwrap(); p.expect_prompt().await.unwrap(); // go sure `hostname` is really done println!("Current hostname: {hostname:?}"); // it prints some undetermined characters before hostname ... // case 2: wait until done, only extract a few infos p.send_line("wc /etc/passwd").await.unwrap(); // `exp_regex` returns both string-before-match and match itself, discard first let lines = p.expect(Regex("[0-9]+")).await.unwrap(); let words = p.expect(Regex("[0-9]+")).await.unwrap(); let bytes = p.expect(Regex("[0-9]+")).await.unwrap(); p.expect_prompt().await.unwrap(); // go sure `wc` is really done println!( "/etc/passwd has {} lines, {} words, {} chars", String::from_utf8_lossy(lines.get(0).unwrap()), String::from_utf8_lossy(words.get(0).unwrap()), String::from_utf8_lossy(bytes.get(0).unwrap()), ); // case 3: read while program is still executing p.send_line("ping 8.8.8.8").await.unwrap(); // returns when it sees "bytes of data" in output for _ in 0..5 { // times out if one ping takes longer than 2s let duration = p.expect(Regex("[0-9. ]+ ms")).await.unwrap(); println!( "Roundtrip time: {}", String::from_utf8_lossy(duration.get(0).unwrap()) ); } p.send(ControlCode::EOT).await.unwrap(); }) } #[cfg(windows)] fn main() { panic!("An example doesn't supported on windows") } expectrl-0.7.1/examples/check.rs000064400000000000000000000026471046102023000147310ustar 00000000000000use expectrl::{check, spawn, Error}; #[cfg(not(feature = "async"))] fn main() { let mut session = spawn("python ./tests/source/ansi.py").expect("Can't spawn a session"); loop { match check!( &mut session, _ = "Password: " => { println!("Set password to SECURE_PASSWORD"); session.send_line("SECURE_PASSWORD").unwrap(); }, _ = "Continue [y/n]:" => { println!("Stop processing"); session.send_line("n").unwrap(); }, ) { Err(Error::Eof) => break, result => result.unwrap(), }; } } #[cfg(feature = "async")] fn main() { futures_lite::future::block_on(async { let mut session = spawn("python ./tests/source/ansi.py").expect("Can't spawn a session"); loop { match check!( &mut session, _ = "Password: " => { println!("Set password to SECURE_PASSWORD"); session.send_line("SECURE_PASSWORD").await.unwrap(); }, _ = "Continue [y/n]:" => { println!("Stop processing"); session.send_line("n").await.unwrap(); }, ) .await { Err(Error::Eof) => break, result => result.unwrap(), }; } }) } expectrl-0.7.1/examples/expect_line.rs000064400000000000000000000025741046102023000161520ustar 00000000000000use expectrl::{self, Any, Eof}; #[cfg(not(feature = "async"))] fn main() { let mut session = expectrl::spawn("ls -al").expect("Can't spawn a session"); loop { let m = session .expect(Any::boxed(vec![ Box::new("\r"), Box::new("\n"), Box::new(Eof), ])) .expect("Expect failed"); println!("{:?}", String::from_utf8_lossy(m.as_bytes())); let is_eof = m[0].is_empty(); if is_eof { break; } if m[0] == [b'\n'] { continue; } println!("{:?}", String::from_utf8_lossy(&m[0])); } } #[cfg(feature = "async")] fn main() { futures_lite::future::block_on(async { let mut session = expectrl::spawn("ls -al").expect("Can't spawn a session"); loop { let m = session .expect(Any::boxed(vec![ Box::new("\r"), Box::new("\n"), Box::new(Eof), ])) .await .expect("Expect failed"); let is_eof = m.get(0).unwrap().is_empty(); if is_eof { break; } if m.get(0).unwrap() == [b'\n'] { continue; } println!("{:?}", String::from_utf8_lossy(m.get(0).unwrap())); } }) } expectrl-0.7.1/examples/ftp.rs000064400000000000000000000011111046102023000144260ustar 00000000000000use expectrl::{spawn, ControlCode, Error, Regex}; #[cfg(not(feature = "async"))] fn main() -> Result<(), Error> { let mut p = spawn("ftp bks4-speedtest-1.tele2.net")?; p.expect(Regex("Name \\(.*\\):"))?; p.send_line("anonymous")?; p.expect("Password")?; p.send_line("test")?; p.expect("ftp>")?; p.send_line("cd upload")?; p.expect("successfully changed.")?; p.send_line("pwd")?; p.expect(Regex("[0-9]+ \"/upload\""))?; p.send(ControlCode::EndOfTransmission)?; p.expect("Goodbye.")?; Ok(()) } #[cfg(feature = "async")] fn main() {} expectrl-0.7.1/examples/ftp_interact.rs000064400000000000000000000022771046102023000163350ustar 00000000000000use expectrl::{ interact::{actions::lookup::Lookup, InteractOptions}, spawn, stream::stdin::Stdin, ControlCode, Error, Regex, }; use std::io::stdout; #[cfg(not(all(windows, feature = "polling")))] #[cfg(not(feature = "async"))] fn main() -> Result<(), Error> { let mut auth = false; let mut login_lookup = Lookup::new(); let opts = InteractOptions::new(&mut auth).on_output(|ctx| { if login_lookup .on(ctx.buf, ctx.eof, "Login successful")? .is_some() { **ctx.state = true; return Ok(true); } Ok(false) }); let mut p = spawn("ftp bks4-speedtest-1.tele2.net")?; let mut stdin = Stdin::open()?; p.interact(&mut stdin, stdout()).spawn(opts)?; stdin.close()?; if !auth { println!("An authefication was not passed"); return Ok(()); } p.expect("ftp>")?; p.send_line("cd upload")?; p.expect("successfully changed.")?; p.send_line("pwd")?; p.expect(Regex("[0-9]+ \"/upload\""))?; p.send(ControlCode::EndOfTransmission)?; p.expect("Goodbye.")?; Ok(()) } #[cfg(any(all(windows, feature = "polling"), feature = "async"))] fn main() {} expectrl-0.7.1/examples/interact.rs000064400000000000000000000027311046102023000154570ustar 00000000000000//! To run an example run `cargo run --example interact`. use expectrl::{interact::InteractOptions, spawn, stream::stdin::Stdin}; use std::io::stdout; #[cfg(unix)] const SHELL: &str = "sh"; #[cfg(windows)] const SHELL: &str = "powershell"; #[cfg(not(all(windows, feature = "polling")))] #[cfg(not(feature = "async"))] fn main() { let mut sh = spawn(SHELL).expect("Error while spawning sh"); println!("Now you're in interacting mode"); println!("To return control back to main type CTRL-] combination"); let mut stdin = Stdin::open().expect("Failed to create stdin"); sh.interact(&mut stdin, stdout()) .spawn(&mut InteractOptions::default()) .expect("Failed to start interact"); stdin.close().expect("Failed to close a stdin"); println!("Exiting"); } #[cfg(feature = "async")] fn main() { futures_lite::future::block_on(async { let mut sh = spawn(SHELL).expect("Error while spawning sh"); println!("Now you're in interacting mode"); println!("To return control back to main type CTRL-] combination"); let mut stdin = Stdin::open().expect("Failed to create stdin"); sh.interact(&mut stdin, stdout()) .spawn(&mut InteractOptions::default()) .await .expect("Failed to start interact"); stdin.close().expect("Failed to close a stdin"); println!("Exiting"); }); } #[cfg(all(windows, feature = "polling", not(feature = "async")))] fn main() {} expectrl-0.7.1/examples/interact_with_callback.rs000064400000000000000000000111451046102023000203250ustar 00000000000000use expectrl::{ interact::{actions::lookup::Lookup, InteractOptions}, spawn, stream::stdin::Stdin, Regex, }; #[derive(Debug, Default)] struct State { stutus_verification_counter: Option, wait_for_continue: Option, pressed_yes_on_continue: Option, } #[cfg(not(all(windows, feature = "polling")))] #[cfg(not(feature = "async"))] fn main() { let mut output_action = Lookup::new(); let mut input_action = Lookup::new(); let mut state = State::default(); let opts = InteractOptions::new(&mut state) .on_output(|mut ctx| { let m = output_action.on(ctx.buf, ctx.eof, "Continue [y/n]:")?; if m.is_some() { ctx.state.wait_for_continue = Some(true); }; let m = output_action.on(ctx.buf, ctx.eof, Regex("status:\\s*.*\\w+.*\\r\\n"))?; if m.is_some() { ctx.state.stutus_verification_counter = Some(ctx.state.stutus_verification_counter.map_or(1, |c| c + 1)); output_action.clear(); } Ok(false) }) .on_input(|mut ctx| { let m = input_action.on(ctx.buf, ctx.eof, "y")?; if m.is_some() { if let Some(_a @ true) = ctx.state.wait_for_continue { ctx.state.pressed_yes_on_continue = Some(true); } }; let m = input_action.on(ctx.buf, ctx.eof, "n")?; if m.is_some() { if let Some(_a @ true) = ctx.state.wait_for_continue { ctx.state.pressed_yes_on_continue = Some(false); } } Ok(false) }); let mut session = spawn("python ./tests/source/ansi.py").expect("Can't spawn a session"); let mut stdin = Stdin::open().unwrap(); let stdout = std::io::stdout(); let mut interact = session.interact(&mut stdin, stdout); let is_alive = interact.spawn(opts).expect("Failed to start interact"); if !is_alive { println!("The process was exited"); #[cfg(unix)] println!("Status={:?}", interact.get_status()); } stdin.close().unwrap(); println!("RESULTS"); println!( "Number of time 'Y' was pressed = {}", state.pressed_yes_on_continue.unwrap_or_default() ); println!( "Status counter = {}", state.stutus_verification_counter.unwrap_or_default() ); } #[cfg(feature = "async")] fn main() { let mut output_action = Lookup::new(); let mut input_action = Lookup::new(); let mut state = State::default(); let opts = InteractOptions::new(&mut state) .on_output(|mut ctx| { let m = output_action.on(ctx.buf, ctx.eof, "Continue [y/n]:")?; if m.is_some() { ctx.state.wait_for_continue = Some(true); }; let m = output_action.on(ctx.buf, ctx.eof, Regex("status:\\s*.*\\w+.*\\r\\n"))?; if m.is_some() { ctx.state.stutus_verification_counter = Some(ctx.state.stutus_verification_counter.map_or(1, |c| c + 1)); output_action.clear(); } Ok(false) }) .on_input(|mut ctx| { let m = input_action.on(ctx.buf, ctx.eof, "y")?; if m.is_some() { if let Some(_a @ true) = ctx.state.wait_for_continue { ctx.state.pressed_yes_on_continue = Some(true); } }; let m = input_action.on(ctx.buf, ctx.eof, "n")?; if m.is_some() { if let Some(_a @ true) = ctx.state.wait_for_continue { ctx.state.pressed_yes_on_continue = Some(false); } } Ok(false) }); let mut session = spawn("python ./tests/source/ansi.py").expect("Can't spawn a session"); let mut stdin = Stdin::open().unwrap(); let stdout = std::io::stdout(); let mut interact = session.interact(&mut stdin, stdout); let is_alive = futures_lite::future::block_on(interact.spawn(opts)).expect("Failed to start interact"); if !is_alive { println!("The process was exited"); #[cfg(unix)] println!("Status={:?}", interact.get_status()); } stdin.close().unwrap(); println!("RESULTS"); println!( "Number of time 'Y' was pressed = {}", state.pressed_yes_on_continue.unwrap_or_default() ); println!( "Status counter = {}", state.stutus_verification_counter.unwrap_or_default() ); } #[cfg(all(windows, feature = "polling", not(feature = "async")))] fn main() {} expectrl-0.7.1/examples/log.rs000064400000000000000000000007361046102023000144320ustar 00000000000000use expectrl::{spawn, Error}; fn main() -> Result<(), Error> { let p = spawn("cat")?; let mut p = expectrl::session::log(p, std::io::stdout())?; #[cfg(not(feature = "async"))] { p.send_line("Hello World")?; p.expect("Hello World")?; } #[cfg(feature = "async")] { futures_lite::future::block_on(async { p.send_line("Hello World").await?; p.expect("Hello World").await })?; } Ok(()) } expectrl-0.7.1/examples/ping.rs000064400000000000000000000033531046102023000146040ustar 00000000000000#[cfg(unix)] use expectrl::{repl::spawn_bash, ControlCode, Error}; #[cfg(unix)] #[cfg(not(feature = "async"))] fn main() -> Result<(), Error> { let mut p = spawn_bash()?; p.send_line("ping 8.8.8.8")?; p.expect("bytes of data")?; p.send(ControlCode::try_from("^Z").unwrap())?; p.expect_prompt()?; // bash writes 'ping 8.8.8.8' to stdout again to state which job was put into background p.send_line("bg")?; p.expect("ping 8.8.8.8")?; p.expect_prompt()?; p.send_line("sleep 0.5")?; p.expect_prompt()?; // bash writes 'ping 8.8.8.8' to stdout again to state which job was put into foreground p.send_line("fg")?; p.expect("ping 8.8.8.8")?; p.send(ControlCode::try_from("^D").unwrap())?; p.expect("packet loss")?; Ok(()) } #[cfg(unix)] #[cfg(feature = "async")] fn main() -> Result<(), Error> { futures_lite::future::block_on(async { let mut p = spawn_bash().await?; p.send_line("ping 8.8.8.8").await?; p.expect("bytes of data").await?; p.send(ControlCode::Substitute).await?; // CTRL_Z p.expect_prompt().await?; // bash writes 'ping 8.8.8.8' to stdout again to state which job was put into background p.send_line("bg").await?; p.expect("ping 8.8.8.8").await?; p.expect_prompt().await?; p.send_line("sleep 0.5").await?; p.expect_prompt().await?; // bash writes 'ping 8.8.8.8' to stdout again to state which job was put into foreground p.send_line("fg").await?; p.expect("ping 8.8.8.8").await?; p.send(ControlCode::EndOfText).await?; p.expect("packet loss").await?; Ok(()) }) } #[cfg(windows)] fn main() { panic!("An example doesn't supported on windows") } expectrl-0.7.1/examples/powershell.rs000064400000000000000000000062571046102023000160410ustar 00000000000000#[cfg(windows)] fn main() { use expectrl::{repl::spawn_powershell, ControlCode, Regex}; #[cfg(feature = "async")] { futures_lite::future::block_on(async { let mut p = spawn_powershell().await.unwrap(); eprintln!("Current hostname",); // case 1: execute let hostname = p.execute("hostname").await.unwrap(); println!( "Current hostname: {:?}", String::from_utf8(hostname).unwrap() ); // case 2: wait until done, only extract a few infos p.send_line("type README.md | Measure-Object -line -word -character") .await .unwrap(); let lines = p.expect(Regex("[0-9]+\\s")).await.unwrap(); let words = p.expect(Regex("[0-9]+\\s")).await.unwrap(); let bytes = p.expect(Regex("([0-9]+)[^0-9]")).await.unwrap(); // go sure `wc` is really done p.expect_prompt().await.unwrap(); println!( "/etc/passwd has {} lines, {} words, {} chars", String::from_utf8_lossy(&lines[0]), String::from_utf8_lossy(&words[0]), String::from_utf8_lossy(&bytes[1]), ); // case 3: read while program is still executing p.send_line("ping 8.8.8.8 -t").await.unwrap(); for _ in 0..5 { let duration = p.expect(Regex("[0-9.]+ms")).await.unwrap(); println!( "Roundtrip time: {}", String::from_utf8_lossy(duration.get(0).unwrap()) ); } p.send(ControlCode::ETX).await.unwrap(); p.expect_prompt().await.unwrap(); }); } #[cfg(not(feature = "async"))] { let mut p = spawn_powershell().unwrap(); // case 1: execute let hostname = p.execute("hostname").unwrap(); println!( "Current hostname: {:?}", String::from_utf8(hostname).unwrap() ); // case 2: wait until done, only extract a few infos p.send_line("type README.md | Measure-Object -line -word -character") .unwrap(); let lines = p.expect(Regex("[0-9]+\\s")).unwrap(); let words = p.expect(Regex("[0-9]+\\s")).unwrap(); let bytes = p.expect(Regex("([0-9]+)[^0-9]")).unwrap(); // go sure `wc` is really done p.expect_prompt().unwrap(); println!( "/etc/passwd has {} lines, {} words, {} chars", String::from_utf8_lossy(&lines[0]), String::from_utf8_lossy(&words[0]), String::from_utf8_lossy(&bytes[1]), ); // case 3: read while program is still executing p.send_line("ping 8.8.8.8 -t").unwrap(); for _ in 0..5 { let duration = p.expect(Regex("[0-9.]+ms")).unwrap(); println!( "Roundtrip time: {}", String::from_utf8_lossy(duration.get(0).unwrap()) ); } p.send(ControlCode::ETX).unwrap(); p.expect_prompt().unwrap(); } } #[cfg(not(windows))] fn main() { panic!("An example doesn't supported on windows") } expectrl-0.7.1/examples/python.rs000064400000000000000000000014401046102023000151630ustar 00000000000000use expectrl::{repl::spawn_python, Regex}; #[cfg(not(feature = "async"))] fn main() { let mut p = spawn_python().unwrap(); p.execute("import platform").unwrap(); p.send_line("platform.node()").unwrap(); let found = p.expect(Regex(r"'.*'")).unwrap(); println!( "Platform {}", String::from_utf8_lossy(found.get(0).unwrap()) ); } #[cfg(feature = "async")] fn main() { futures_lite::future::block_on(async { let mut p = spawn_python().await.unwrap(); p.execute("import platform").await.unwrap(); p.send_line("platform.node()").await.unwrap(); let found = p.expect(Regex(r"'.*'")).await.unwrap(); println!( "Platform {}", String::from_utf8_lossy(found.get(0).unwrap()) ); }) } expectrl-0.7.1/examples/shell.rs000064400000000000000000000035251046102023000147570ustar 00000000000000use expectrl::repl::ReplSession; use std::io::Result; #[cfg(all(unix, not(feature = "async")))] fn main() -> Result<()> { let mut p = expectrl::spawn("sh")?; p.get_process_mut().set_echo(true, None)?; let mut shell = ReplSession::new(p, String::from("sh-5.1$"), Some(String::from("exit")), true); shell.expect_prompt()?; let output = exec(&mut shell, "echo Hello World")?; println!("{:?}", output); let output = exec(&mut shell, "echo '2 + 3' | bc")?; println!("{:?}", output); Ok(()) } #[cfg(all(unix, not(feature = "async")))] fn exec(shell: &mut ReplSession, cmd: &str) -> Result { let buf = shell.execute(cmd)?; let mut string = String::from_utf8_lossy(&buf).into_owned(); string = string.replace("\r\n\u{1b}[?2004l\r", ""); string = string.replace("\r\n\u{1b}[?2004h", ""); Ok(string) } #[cfg(all(unix, feature = "async"))] fn main() -> Result<()> { futures_lite::future::block_on(async { let mut p = expectrl::spawn("sh")?; p.get_process_mut().set_echo(true, None)?; let mut shell = ReplSession::new(p, String::from("sh-5.1$"), Some(String::from("exit")), true); shell.expect_prompt().await?; let output = exec(&mut shell, "echo Hello World").await?; println!("{:?}", output); let output = exec(&mut shell, "echo '2 + 3' | bc").await?; println!("{:?}", output); Ok(()) }) } #[cfg(all(unix, feature = "async"))] async fn exec(shell: &mut ReplSession, cmd: &str) -> Result { let buf = shell.execute(cmd).await?; let mut string = String::from_utf8_lossy(&buf).into_owned(); string = string.replace("\r\n\u{1b}[?2004l\r", ""); string = string.replace("\r\n\u{1b}[?2004h", ""); Ok(string) } #[cfg(windows)] fn main() { panic!("An example doesn't supported on windows") } expectrl-0.7.1/src/captures.rs000064400000000000000000000166411046102023000144520ustar 00000000000000use std::ops::Index; use crate::needle::Match; /// Captures is a represention of matched pattern. /// /// It might represent an empty match. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Captures { buf: Vec, matches: Vec, } impl Captures { /// New returns an instance of Found. pub(crate) fn new(buf: Vec, matches: Vec) -> Self { Self { buf, matches } } /// is_empty verifies if any matches were actually found. pub fn is_empty(&self) -> bool { self.matches.is_empty() } /// get returns a match by index. pub fn get(&self, index: usize) -> Option<&[u8]> { self.matches .get(index) .map(|m| &self.buf[m.start()..m.end()]) } /// Matches returns a list of matches. pub fn matches(&self) -> MatchIter<'_> { MatchIter::new(self) } /// before returns a bytes before match. pub fn before(&self) -> &[u8] { &self.buf[..self.left_most_index()] } /// as_bytes returns all bytes involved in a match, e.g. before the match and /// in a match itself. /// /// In most cases the returned value equeals to concatanted [Self::before] and [Self::matches]. /// But sometimes like in case of [crate::Regex] it may have a grouping so [Self::matches] might overlap, therefore /// it will not longer be true. pub fn as_bytes(&self) -> &[u8] { &self.buf } fn left_most_index(&self) -> usize { self.matches .iter() .map(|m| m.start()) .min() .unwrap_or_default() } pub(crate) fn right_most_index(matches: &[Match]) -> usize { matches.iter().map(|m| m.end()).max().unwrap_or_default() } } impl Index for Captures { type Output = [u8]; fn index(&self, index: usize) -> &Self::Output { let m = &self.matches[index]; &self.buf[m.start()..m.end()] } } impl<'a> IntoIterator for &'a Captures { type Item = &'a [u8]; type IntoIter = MatchIter<'a>; fn into_iter(self) -> Self::IntoIter { MatchIter::new(self) } } #[derive(Debug)] pub struct MatchIter<'a> { buf: &'a [u8], matches: std::slice::Iter<'a, Match>, } impl<'a> MatchIter<'a> { fn new(captures: &'a Captures) -> Self { Self { buf: &captures.buf, matches: captures.matches.iter(), } } } impl<'a> Iterator for MatchIter<'a> { type Item = &'a [u8]; fn next(&mut self) -> Option { self.matches.next().map(|m| &self.buf[m.start()..m.end()]) } fn size_hint(&self) -> (usize, Option) { self.matches.size_hint() } } impl ExactSizeIterator for MatchIter<'_> {} #[cfg(test)] mod tests { use super::*; #[test] fn test_captures_get() { let m = Captures::new( b"You can use iterator".to_vec(), vec![Match::new(0, 3), Match::new(4, 7)], ); assert_eq!(m.get(0), Some(b"You".as_ref())); assert_eq!(m.get(1), Some(b"can".as_ref())); assert_eq!(m.get(2), None); let m = Captures::new(b"You can use iterator".to_vec(), vec![]); assert_eq!(m.get(0), None); assert_eq!(m.get(1), None); assert_eq!(m.get(2), None); let m = Captures::new(vec![], vec![]); assert_eq!(m.get(0), None); assert_eq!(m.get(1), None); assert_eq!(m.get(2), None); } #[test] #[should_panic] fn test_captures_get_panics_on_invalid_match() { let m = Captures::new(b"Hello World".to_vec(), vec![Match::new(0, 100)]); let _ = m.get(0); } #[test] fn test_captures_index() { let m = Captures::new( b"You can use iterator".to_vec(), vec![Match::new(0, 3), Match::new(4, 7)], ); assert_eq!(&m[0], b"You".as_ref()); assert_eq!(&m[1], b"can".as_ref()); } #[test] #[should_panic] fn test_captures_index_panics_on_invalid_match() { let m = Captures::new(b"Hello World".to_vec(), vec![Match::new(0, 100)]); let _ = &m[0]; } #[test] #[should_panic] fn test_captures_index_panics_on_invalid_index() { let m = Captures::new(b"Hello World".to_vec(), vec![Match::new(0, 3)]); let _ = &m[10]; } #[test] #[should_panic] fn test_captures_index_panics_on_empty_match() { let m = Captures::new(b"Hello World".to_vec(), vec![]); let _ = &m[0]; } #[test] #[should_panic] fn test_captures_index_panics_on_empty_captures() { let m = Captures::new(Vec::new(), Vec::new()); let _ = &m[0]; } #[test] fn test_before() { let m = Captures::new(b"You can use iterator".to_vec(), vec![Match::new(4, 7)]); assert_eq!(m.before(), b"You ".as_ref()); let m = Captures::new( b"You can use iterator".to_vec(), vec![Match::new(0, 3), Match::new(4, 7)], ); assert_eq!(m.before(), b"".as_ref()); let m = Captures::new(b"You can use iterator".to_vec(), vec![]); assert_eq!(m.before(), b"".as_ref()); let m = Captures::new(vec![], vec![]); assert_eq!(m.before(), b"".as_ref()); } #[test] fn test_matches() { let m = Captures::new(b"You can use iterator".to_vec(), vec![Match::new(4, 7)]); assert_eq!(m.before(), b"You ".as_ref()); let m = Captures::new( b"You can use iterator".to_vec(), vec![Match::new(0, 3), Match::new(4, 7)], ); assert_eq!(m.before(), b"".as_ref()); let m = Captures::new(b"You can use iterator".to_vec(), vec![]); assert_eq!(m.before(), b"".as_ref()); let m = Captures::new(vec![], vec![]); assert_eq!(m.before(), b"".as_ref()); } #[test] fn test_captures_into_iter() { assert_eq!( Captures::new( b"You can use iterator".to_vec(), vec![Match::new(0, 3), Match::new(4, 7)] ) .into_iter() .collect::>(), vec![b"You", b"can"] ); assert_eq!( Captures::new(b"You can use iterator".to_vec(), vec![Match::new(4, 20)]) .into_iter() .collect::>(), vec![b"can use iterator"] ); assert_eq!( Captures::new(b"You can use iterator".to_vec(), vec![]) .into_iter() .collect::>(), Vec::<&[u8]>::new() ); } #[test] fn test_captures_matches() { assert_eq!( Captures::new( b"You can use iterator".to_vec(), vec![Match::new(0, 3), Match::new(4, 7)] ) .matches() .collect::>(), vec![b"You", b"can"] ); assert_eq!( Captures::new(b"You can use iterator".to_vec(), vec![Match::new(4, 20)]) .matches() .collect::>(), vec![b"can use iterator"] ); assert_eq!( Captures::new(b"You can use iterator".to_vec(), vec![]) .matches() .collect::>(), Vec::<&[u8]>::new() ); } #[test] #[should_panic] fn test_captures_into_iter_panics_on_invalid_match() { Captures::new(b"Hello World".to_vec(), vec![Match::new(0, 100)]) .into_iter() .for_each(|_| {}); } } expectrl-0.7.1/src/check_macros.rs000064400000000000000000000305131046102023000152370ustar 00000000000000//! This module contains a `check!` macros. /// Check macros provides a convient way to check if things are available in a stream of a process. /// /// It falls into a corresponding branch when a pattern was matched. /// It runs checks from top to bottom. /// It doesn't wait until any of them be available. /// If you want to wait until any of the input available you must use your own approach for example putting it in a loop. /// /// You can specify a default branch which will be called if nothing was matched. /// /// The macros levareges [crate::Session::check] function, so its just made for convience. /// /// # Example /// ```no_run /// # let mut session = expectrl::spawn("cat").unwrap(); /// # /// loop { /// expectrl::check!{ /// &mut session, /// world = "\r" => { /// // handle end of line /// }, /// _ = "Hello World" => { /// // handle Hello World /// }, /// default => { /// // handle no matches /// }, /// } /// .unwrap(); /// } /// ``` #[cfg(not(feature = "async"))] #[macro_export] macro_rules! check { (@check ($($tokens:tt)*) ($session:expr)) => { $crate::check!(@case $session, ($($tokens)*), (), ()) }; (@check ($session:expr, $($tokens:tt)*) ()) => { $crate::check!(@check ($($tokens)*) ($session)) }; (@check ($session:expr, $($tokens:tt)*) ($session2:expr)) => { compile_error!("Wrong number of session arguments") }; (@check ($($tokens:tt)*) ()) => { compile_error!("Please provide a session as a first argument") }; (@check () ($session:expr)) => { // there's no reason to run 0 checks so we issue a error. compile_error!("There's no reason in running check with no arguments. Please supply a check branches") }; (@case $session:expr, ($var:tt = $exp:expr => $body:tt, $($tail:tt)*), ($($head:tt)*), ($($default:tt)*)) => { // regular case // // note: we keep order correct by putting head at the beggining $crate::check!(@case $session, ($($tail)*), ($($head)* $var = $exp => $body, ), ($($default)*)) }; (@case $session:expr, ($var:tt = $exp:expr => $body:tt $($tail:tt)*), ($($head:tt)*), ($($default:tt)*)) => { // allow missing comma // // note: we keep order correct by putting head at the beggining $crate::check!(@case $session, ($($tail)*), ($($head)* $var = $exp => $body, ), ($($default)*)) }; (@case $session:expr, (default => $($tail:tt)*), ($($head:tt)*), ($($default:tt)+)) => { // A repeated default branch compile_error!("Only 1 default case is allowed") }; (@case $session:expr, (default => $body:tt, $($tail:tt)*), ($($head:tt)*), ()) => { // A default branch $crate::check!(@case $session, ($($tail)*), ($($head)*), ( { $body; #[allow(unreachable_code)] Ok(()) } )) }; (@case $session:expr, (default => $body:tt $($tail:tt)*), ($($head:tt)*), ()) => { // A default branch // allow missed comma `,` $crate::check!(@case $session, ($($tail)*), ($($head)*), ( { $body; Ok(()) } )) }; (@case $session:expr, (), ($($head:tt)*), ()) => { // there's no default branch // so we make up our own. $crate::check!(@case $session, (), ($($head)*), ( { Ok(()) } )) }; (@case $session:expr, (), ($($tail:tt)*), ($($default:tt)*)) => { // last point of @case // call code generation via @branch $crate::check!(@branch $session, ($($tail)*), ($($default)*)) }; // We need to use a variable for pattern mathing, // user may chose to drop var name using a placeholder '_', // in which case we can't call a method on such identificator. // // We could use an approach like was described // // ``` // Ok(_____random_var_name_which_supposedly_mustnt_be_used) if !_____random_var_name_which_supposedly_mustnt_be_used.is_empty() => // { // let $var = _____random_var_name_which_supposedly_mustnt_be_used; // } // ``` // // The question is which solution is more effichient. // I took the following approach because there's no chance we influence user's land via the variable name we pick. (@branch $session:expr, ($var:tt = $exp:expr => $body:tt, $($tail:tt)*), ($($default:tt)*)) => { match $crate::session::Session::check($session, $exp) { result if result.as_ref().map(|found| !found.is_empty()).unwrap_or(false) => { let $var = result.unwrap(); $body; #[allow(unreachable_code)] Ok(()) } Ok(_) => { $crate::check!(@branch $session, ($($tail)*), ($($default)*)) } Err(err) => Err(err), } }; (@branch $session:expr, (), ($default:tt)) => { // A standart default branch $default }; (@branch $session:expr, ($($tail:tt)*), ($($default:tt)*)) => { compile_error!( concat!( "No supported syntax tail=(", stringify!($($tail,)*), ") ", "default=(", stringify!($($default,)*), ") ", )) }; // Entry point ($($tokens:tt)*) => { { let result: Result::<(), $crate::Error> = $crate::check!(@check ($($tokens)*) ()); result } }; } /// See sync version. /// /// Async version of macros use the same approach as sync. /// It doesn't use any future features. /// So it may be better better you to use you own approach how to check which one done first. /// For example you can use `futures_lite::future::race`. /// // async version completely the same as sync version expect 2 words '.await' and 'async' // meaning its a COPY && PASTE #[cfg(feature = "async")] #[macro_export] macro_rules! check { (@check ($($tokens:tt)*) ($session:expr)) => { $crate::check!(@case $session, ($($tokens)*), (), ()) }; (@check ($session:expr, $($tokens:tt)*) ()) => { $crate::check!(@check ($($tokens)*) ($session)) }; (@check ($session:expr, $($tokens:tt)*) ($session2:expr)) => { compile_error!("Wrong number of session arguments") }; (@check ($($tokens:tt)*) ()) => { compile_error!("Please provide a session as a first argument") }; (@check () ($session:expr)) => { // there's no reason to run 0 checks so we issue a error. compile_error!("There's no reason in running check with no arguments. Please supply a check branches") }; (@case $session:expr, ($var:tt = $exp:expr => $body:tt, $($tail:tt)*), ($($head:tt)*), ($($default:tt)*)) => { // regular case // // note: we keep order correct by putting head at the beggining $crate::check!(@case $session, ($($tail)*), ($($head)* $var = $exp => $body, ), ($($default)*)) }; (@case $session:expr, ($var:tt = $exp:expr => $body:tt $($tail:tt)*), ($($head:tt)*), ($($default:tt)*)) => { // allow missing comma // // note: we keep order correct by putting head at the beggining $crate::check!(@case $session, ($($tail)*), ($($head)* $var = $exp => $body, ), ($($default)*)) }; (@case $session:expr, (default => $($tail:tt)*), ($($head:tt)*), ($($default:tt)+)) => { // A repeated default branch compile_error!("Only 1 default case is allowed") }; (@case $session:expr, (default => $body:tt, $($tail:tt)*), ($($head:tt)*), ()) => { // A default branch $crate::check!(@case $session, ($($tail)*), ($($head)*), ( { $body; #[allow(unreachable_code)] Ok(()) } )) }; (@case $session:expr, (default => $body:tt $($tail:tt)*), ($($head:tt)*), ()) => { // A default branch // allow missed comma `,` $crate::check!(@case $session, ($($tail)*), ($($head)*), ( { $body; Ok(()) } )) }; (@case $session:expr, (), ($($head:tt)*), ()) => { // there's no default branch // so we make up our own. $crate::check!(@case $session, (), ($($head)*), ( { Ok(()) } )) }; (@case $session:expr, (), ($($tail:tt)*), ($($default:tt)*)) => { // last point of @case // call code generation via @branch $crate::check!(@branch $session, ($($tail)*), ($($default)*)) }; // We need to use a variable for pattern mathing, // user may chose to drop var name using a placeholder '_', // in which case we can't call a method on such identificator. // // We could use an approach like was described // // ``` // Ok(_____random_var_name_which_supposedly_mustnt_be_used) if !_____random_var_name_which_supposedly_mustnt_be_used.is_empty() => // { // let $var = _____random_var_name_which_supposedly_mustnt_be_used; // } // ``` // // The question is which solution is more effichient. // I took the following approach because there's no chance we influence user's land via the variable name we pick. (@branch $session:expr, ($var:tt = $exp:expr => $body:tt, $($tail:tt)*), ($($default:tt)*)) => { match $crate::session::Session::check(&mut $session, $exp).await { Ok(found) => { if !found.is_empty() { let $var = found; $body; #[allow(unreachable_code)] return Ok(()) } $crate::check!(@branch $session, ($($tail)*), ($($default)*)) } Err(err) => Err(err), } }; (@branch $session:expr, (), ($default:tt)) => { // A standart default branch $default }; (@branch $session:expr, ($($tail:tt)*), ($($default:tt)*)) => { compile_error!( concat!( "No supported syntax tail=(", stringify!($($tail,)*), ") ", "default=(", stringify!($($default,)*), ") ", )) }; // Entry point ($($tokens:tt)*) => { async { let value: Result::<(), $crate::Error> = $crate::check!(@check ($($tokens)*) ()); value } }; } #[cfg(test)] mod tests { #[allow(unused_variables)] #[allow(unused_must_use)] #[test] #[ignore = "Testing in compile time"] fn test_check() { let mut session = crate::spawn("").unwrap(); crate::check! { &mut session, as11d = "zxc" => {}, }; crate::check! { &mut session, as11d = "zxc" => {}, asbb = "zxc123" => {}, }; crate::check! { session, }; // crate::check! { // as11d = "zxc" => {}, // asbb = "zxc123" => {}, // }; // panic on unused session // crate::check! { session }; // trailing commas crate::check! { &mut session, as11d = "zxc" => {} asbb = "zxc123" => {} }; crate::check! { &mut session, as11d = "zxc" => {} default => {} }; #[cfg(not(feature = "async"))] { crate::check! { &mut session, as11d = "zxc" => {}, } .unwrap(); (crate::check! { &mut session, as11d = "zxc" => {}, }) .unwrap(); (crate::check! { &mut session, as11d = "zxc" => {}, }) .unwrap(); (crate::check! { &mut session, as11d = "zxc" => { println!("asd") }, }) .unwrap(); } #[cfg(feature = "async")] async { crate::check! { session, as11d = "zxc" => {}, } .await .unwrap(); (crate::check! { session, as11d = "zxc" => {}, }) .await .unwrap(); (crate::check! { session, as11d = "zxc" => {}, }) .await .unwrap(); (crate::check! { session, as11d = "zxc" => { println!("asd") }, }) .await .unwrap(); }; } } expectrl-0.7.1/src/control_code.rs000064400000000000000000000334671046102023000153030ustar 00000000000000//! A module which contains [ControlCode] type. use std::convert::TryFrom; /// ControlCode represents the standard ASCII control codes [wiki] /// /// [wiki]: https://en.wikipedia.org/wiki/C0_and_C1_control_codes #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ControlCode { /// Often used as a string terminator, especially in the programming language C. Null, /// In message transmission, delimits the start of a message header. StartOfHeading, /// First character of message text, and may be used to terminate the message heading. StartOfText, /// Often used as a "break" character (Ctrl-C) to interrupt or terminate a program or process. EndOfText, /// Often used on Unix to indicate end-of-file on a terminal (Ctrl-D). EndOfTransmission, /// Signal intended to trigger a response at the receiving end, to see if it is still present. Enquiry, /// Response to an Enquiry, or an indication of successful receipt of a message. Acknowledge, /// Used for a beep on systems that didn't have a physical bell. Bell, /// Move the cursor one position leftwards. /// On input, this may delete the character to the left of the cursor. Backspace, /// Position to the next character tab stop. HorizontalTabulation, /// On Unix, used to mark end-of-line. /// In DOS, Windows, and various network standards, LF is used following CR as part of the end-of-line mark. LineFeed, /// Position the form at the next line tab stop. VerticalTabulation, /// It appears in some common plain text files as a page break character. FormFeed, /// Originally used to move the cursor to column zero while staying on the same line. CarriageReturn, /// Switch to an alternative character set. ShiftOut, /// Return to regular character set after ShiftOut. ShiftIn, /// May cause a limited number of contiguously following octets to be interpreted in some different way. DataLinkEscape, /// A control code which is reserved for device control. DeviceControl1, /// A control code which is reserved for device control. DeviceControl2, /// A control code which is reserved for device control. DeviceControl3, /// A control code which is reserved for device control. DeviceControl4, /// In multipoint systems, the NAK is used as the not-ready reply to a poll. NegativeAcknowledge, /// Used in synchronous transmission systems to provide a signal from which synchronous correction may be achieved. SynchronousIdle, /// Indicates the end of a transmission block of data. EndOfTransmissionBlock, /// Indicates that the data preceding it are in error or are to be disregarded. Cancel, /// May mark the end of the used portion of the physical medium. EndOfMedium, /// Sometimes used to indicate the end of file, both when typing on the terminal and in text files stored on disk. Substitute, /// The Esc key on the keyboard will cause this character to be sent on most systems. /// In systems based on ISO/IEC 2022, even if another set of C0 control codes are used, /// this octet is required to always represent the escape character. Escape, /// Can be used as delimiters to mark fields of data structures. /// Also it used for hierarchical levels; /// FS == level 4 FileSeparator, /// It used for hierarchical levels; /// GS == level 3 GroupSeparator, /// It used for hierarchical levels; /// RS == level 2 RecordSeparator, /// It used for hierarchical levels; /// US == level 1 UnitSeparator, /// Space is a graphic character. It causes the active position to be advanced by one character position. Space, /// Usually called backspace on modern machines, and does not correspond to the PC delete key. Delete, } impl ControlCode { /// See [ControlCode::Null] pub const NUL: ControlCode = ControlCode::Null; /// See [ControlCode::StartOfHeading] pub const SOH: ControlCode = ControlCode::StartOfHeading; /// See [ControlCode::StartOfText] pub const STX: ControlCode = ControlCode::StartOfText; /// See [ControlCode::EndOfText] pub const ETX: ControlCode = ControlCode::EndOfText; /// See [ControlCode::EndOfTransmission] pub const EOT: ControlCode = ControlCode::EndOfTransmission; /// See [ControlCode::Enquiry] pub const ENQ: ControlCode = ControlCode::Enquiry; /// See [ControlCode::Acknowledge] pub const ACK: ControlCode = ControlCode::Acknowledge; /// See [ControlCode::Bell] pub const BEL: ControlCode = ControlCode::Bell; /// See [ControlCode::Backspace] pub const BS: ControlCode = ControlCode::Backspace; /// See [ControlCode::HorizontalTabulation] pub const HT: ControlCode = ControlCode::HorizontalTabulation; /// See [ControlCode::LineFeed] pub const LF: ControlCode = ControlCode::LineFeed; /// See [ControlCode::VerticalTabulation] pub const VT: ControlCode = ControlCode::VerticalTabulation; /// See [ControlCode::FormFeed] pub const FF: ControlCode = ControlCode::FormFeed; /// See [ControlCode::CarriageReturn] pub const CR: ControlCode = ControlCode::CarriageReturn; /// See [ControlCode::ShiftOut] pub const SO: ControlCode = ControlCode::ShiftOut; /// See [ControlCode::ShiftIn] pub const SI: ControlCode = ControlCode::ShiftIn; /// See [ControlCode::DataLinkEscape] pub const DLE: ControlCode = ControlCode::DataLinkEscape; /// See [ControlCode::DeviceControl1] pub const DC1: ControlCode = ControlCode::DeviceControl1; /// See [ControlCode::DeviceControl2] pub const DC2: ControlCode = ControlCode::DeviceControl2; /// See [ControlCode::DeviceControl3] pub const DC3: ControlCode = ControlCode::DeviceControl3; /// See [ControlCode::DeviceControl4] pub const DC4: ControlCode = ControlCode::DeviceControl4; /// See [ControlCode::NegativeAcknowledge] pub const NAK: ControlCode = ControlCode::NegativeAcknowledge; /// See [ControlCode::SynchronousIdle] pub const SYN: ControlCode = ControlCode::SynchronousIdle; /// See [ControlCode::EndOfTransmissionBlock] pub const ETB: ControlCode = ControlCode::EndOfTransmissionBlock; /// See [ControlCode::Cancel] pub const CAN: ControlCode = ControlCode::Cancel; /// See [ControlCode::EndOfMedium] pub const EM: ControlCode = ControlCode::EndOfMedium; /// See [ControlCode::Substitute] pub const SUB: ControlCode = ControlCode::Substitute; /// See [ControlCode::Escape] pub const ESC: ControlCode = ControlCode::Escape; /// See [ControlCode::FileSeparator] pub const FS: ControlCode = ControlCode::FileSeparator; /// See [ControlCode::GroupSeparator] pub const GS: ControlCode = ControlCode::GroupSeparator; /// See [ControlCode::RecordSeparator] pub const RS: ControlCode = ControlCode::RecordSeparator; /// See [ControlCode::UnitSeparator] pub const US: ControlCode = ControlCode::UnitSeparator; /// See [ControlCode::Space] pub const SP: ControlCode = ControlCode::Space; /// See [ControlCode::Delete] pub const DEL: ControlCode = ControlCode::Delete; } impl From for u8 { fn from(val: ControlCode) -> Self { use ControlCode::*; match val { Null => 0, StartOfHeading => 1, StartOfText => 2, EndOfText => 3, EndOfTransmission => 4, Enquiry => 5, Acknowledge => 6, Bell => 7, Backspace => 8, HorizontalTabulation => 9, LineFeed => 10, VerticalTabulation => 11, FormFeed => 12, CarriageReturn => 13, ShiftOut => 14, ShiftIn => 15, DataLinkEscape => 16, DeviceControl1 => 17, DeviceControl2 => 18, DeviceControl3 => 19, DeviceControl4 => 20, NegativeAcknowledge => 21, SynchronousIdle => 22, EndOfTransmissionBlock => 23, Cancel => 24, EndOfMedium => 25, Substitute => 26, Escape => 27, FileSeparator => 28, GroupSeparator => 29, RecordSeparator => 30, UnitSeparator => 31, Space => 32, Delete => 127, } } } impl TryFrom for ControlCode { type Error = (); fn try_from(c: char) -> Result { use ControlCode::*; match c { '@' => Ok(Null), 'A' | 'a' => Ok(StartOfHeading), 'B' | 'b' => Ok(StartOfText), 'C' | 'c' => Ok(EndOfText), 'D' | 'd' => Ok(EndOfTransmission), 'E' | 'e' => Ok(Enquiry), 'F' | 'f' => Ok(Acknowledge), 'G' | 'g' => Ok(Bell), 'H' | 'h' => Ok(Backspace), 'I' | 'i' => Ok(HorizontalTabulation), 'J' | 'j' => Ok(LineFeed), 'K' | 'k' => Ok(VerticalTabulation), 'L' | 'l' => Ok(FormFeed), 'M' | 'm' => Ok(CarriageReturn), 'N' | 'n' => Ok(ShiftOut), 'O' | 'o' => Ok(ShiftIn), 'P' | 'p' => Ok(DataLinkEscape), 'Q' | 'q' => Ok(DeviceControl1), 'R' | 'r' => Ok(DeviceControl2), 'S' | 's' => Ok(DeviceControl3), 'T' | 't' => Ok(DeviceControl4), 'U' | 'u' => Ok(NegativeAcknowledge), 'V' | 'v' => Ok(SynchronousIdle), 'W' | 'w' => Ok(EndOfTransmissionBlock), 'X' | 'x' => Ok(Cancel), 'Y' | 'y' => Ok(EndOfMedium), 'Z' | 'z' => Ok(Substitute), '[' => Ok(Escape), '\\' => Ok(FileSeparator), ']' => Ok(GroupSeparator), '^' => Ok(RecordSeparator), '_' => Ok(UnitSeparator), ' ' => Ok(Space), '?' => Ok(Delete), _ => Err(()), } } } impl TryFrom<&str> for ControlCode { type Error = (); fn try_from(c: &str) -> Result { use ControlCode::*; match c { "^@" => Ok(Null), "^A" => Ok(StartOfHeading), "^B" => Ok(StartOfText), "^C" => Ok(EndOfText), "^D" => Ok(EndOfTransmission), "^E" => Ok(Enquiry), "^F" => Ok(Acknowledge), "^G" => Ok(Bell), "^H" => Ok(Backspace), "^I" => Ok(HorizontalTabulation), "^J" => Ok(LineFeed), "^K" => Ok(VerticalTabulation), "^L" => Ok(FormFeed), "^M" => Ok(CarriageReturn), "^N" => Ok(ShiftOut), "^O" => Ok(ShiftIn), "^P" => Ok(DataLinkEscape), "^Q" => Ok(DeviceControl1), "^R" => Ok(DeviceControl2), "^S" => Ok(DeviceControl3), "^T" => Ok(DeviceControl4), "^U" => Ok(NegativeAcknowledge), "^V" => Ok(SynchronousIdle), "^W" => Ok(EndOfTransmissionBlock), "^X" => Ok(Cancel), "^Y" => Ok(EndOfMedium), "^Z" => Ok(Substitute), "^[" => Ok(Escape), "^\\" => Ok(FileSeparator), "^]" => Ok(GroupSeparator), "^^" => Ok(RecordSeparator), "^_" => Ok(UnitSeparator), "^ " => Ok(Space), "^?" => Ok(Delete), _ => Err(()), } } } impl AsRef for ControlCode { fn as_ref(&self) -> &str { use ControlCode::*; match self { Null => "^@", StartOfHeading => "^A", StartOfText => "^B", EndOfText => "^C", EndOfTransmission => "^D", Enquiry => "^E", Acknowledge => "^F", Bell => "^G", Backspace => "^H", HorizontalTabulation => "^I", LineFeed => "^J", VerticalTabulation => "^K", FormFeed => "^L", CarriageReturn => "^M", ShiftOut => "^N", ShiftIn => "^O", DataLinkEscape => "^P", DeviceControl1 => "^Q", DeviceControl2 => "^R", DeviceControl3 => "^S", DeviceControl4 => "^T", NegativeAcknowledge => "^U", SynchronousIdle => "^V", EndOfTransmissionBlock => "^W", Cancel => "^X", EndOfMedium => "^Y", Substitute => "^Z", Escape => "^[", FileSeparator => "^\\", GroupSeparator => "^]", RecordSeparator => "^^", UnitSeparator => "^_", Space => " ", Delete => "^?", } } } impl AsRef<[u8]> for ControlCode { fn as_ref(&self) -> &[u8] { use ControlCode::*; match self { Null => &[0], StartOfHeading => &[1], StartOfText => &[2], EndOfText => &[3], EndOfTransmission => &[4], Enquiry => &[5], Acknowledge => &[6], Bell => &[7], Backspace => &[8], HorizontalTabulation => &[9], LineFeed => &[10], VerticalTabulation => &[11], FormFeed => &[12], CarriageReturn => &[13], ShiftOut => &[14], ShiftIn => &[15], DataLinkEscape => &[16], DeviceControl1 => &[17], DeviceControl2 => &[18], DeviceControl3 => &[19], DeviceControl4 => &[20], NegativeAcknowledge => &[21], SynchronousIdle => &[22], EndOfTransmissionBlock => &[23], Cancel => &[24], EndOfMedium => &[25], Substitute => &[26], Escape => &[27], FileSeparator => &[28], GroupSeparator => &[29], RecordSeparator => &[30], UnitSeparator => &[31], Space => &[32], Delete => &[127], } } } expectrl-0.7.1/src/error.rs000064400000000000000000000037061046102023000137530ustar 00000000000000use std::error; use std::fmt; use std::fmt::Display; use std::io; #[allow(variant_size_differences)] /// An main error type used in [crate]. #[derive(Debug)] pub enum Error { /// An Error in IO operation. IO(io::Error), /// An Error in command line parsing. CommandParsing, /// An Error in regex parsing. RegexParsing, /// An timeout was reached while waiting in expect call. ExpectTimeout, /// Unhandled EOF error. Eof, /// It maybe OS specific error or a general erorr. Other { /// The reason of the erorr. message: String, /// An underlying error message. err: String, }, } impl Error { #[cfg(unix)] pub(crate) fn unknown(message: impl Into, err: impl Into) -> Error { Self::Other { message: message.into(), err: err.into(), } } } impl Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Error::IO(err) => write!(f, "IO error {}", err), Error::CommandParsing => write!(f, "Can't parse a command string, please check it out"), Error::RegexParsing => write!(f, "Can't parse a regex expression"), Error::ExpectTimeout => write!(f, "Reached a timeout for expect type of command"), Error::Eof => write!(f, "EOF was reached; the read may successed later"), Error::Other { message, err } => write!(f, "Unexpected error; {}; {}", message, err), } } } impl error::Error for Error {} impl From for Error { fn from(err: io::Error) -> Self { Self::IO(err) } } impl From for io::Error { fn from(err: Error) -> Self { io::Error::new(io::ErrorKind::Other, err.to_string()) } } pub(crate) fn to_io_error(message: &'static str) -> impl FnOnce(E) -> io::Error { move |e: E| io::Error::new(io::ErrorKind::Other, format!("{}; {}", message, e)) } expectrl-0.7.1/src/interact/actions/lookup.rs000064400000000000000000000030221046102023000173730ustar 00000000000000//! The module contains [`Lookup`]. use crate::{Captures, Error, Needle}; /// A helper action for an [`InteractSession`] /// /// It holds a buffer to be able to run different checks using [`Needle`], /// via [`Lookup::on`]. /// /// [`InteractSession`]: crate::interact::InteractSession #[derive(Debug, Clone)] pub struct Lookup { buf: Vec, } impl Lookup { /// Create a lookup object. pub fn new() -> Self { Self { buf: Vec::new() } } /// Checks whethere the buffer will be matched and returns [`Captures`] in such case. pub fn on(&mut self, buf: &[u8], eof: bool, pattern: N) -> Result, Error> where N: Needle, { self.buf.extend(buf); check(&mut self.buf, pattern, eof) } /// Cleans internal buffer. /// /// So the next [`Lookup::on`] can be called with no other data involved. pub fn clear(&mut self) { self.buf.clear(); } } impl Default for Lookup { fn default() -> Self { Self::new() } } fn check(buf: &mut Vec, needle: N, eof: bool) -> Result, Error> where N: Needle, { // we ignore the check if buf is empty in just in case someone is matching 0 bytes. let found = needle.check(buf, eof)?; if found.is_empty() { return Ok(None); } let end_index = Captures::right_most_index(&found); let involved_bytes = buf[..end_index].to_vec(); let found = Captures::new(involved_bytes, found); let _ = buf.drain(..end_index); Ok(Some(found)) } expectrl-0.7.1/src/interact/actions/mod.rs000064400000000000000000000002361046102023000166450ustar 00000000000000//! The module contains a list of helpers for callbacks in [`InteractSession`] //! //! [`InteractSession`]: crate::interact::InteractSession pub mod lookup; expectrl-0.7.1/src/interact/context.rs000064400000000000000000000030771046102023000161200ustar 00000000000000/// Context provides an interface to use a [`Session`], IO streams /// and a state. /// /// It's used primarily in callbacks for [`InteractSession`]. /// /// [`InteractSession`]: crate::interact::InteractSession /// [`Session`]: crate::session::Session #[derive(Debug)] pub struct Context<'a, Session, Input, Output, State> { /// The field contains a &mut reference to a [`Session`]. /// /// [`Session`]: crate::session::Session pub session: &'a mut Session, /// The field contains an input structure which was used in [`InteractSession`]. /// /// [`InteractSession`]: crate::interact::InteractSession pub input: &'a mut Input, /// The field contains an output structure which was used in [`InteractSession`]. /// /// [`InteractSession`]: crate::interact::InteractSession pub output: &'a mut Output, /// The field contains a user defined data. pub state: &'a mut State, /// The field contains a bytes which were consumed from a user or the running process. pub buf: &'a [u8], /// A flag for EOF of a user session or running process. pub eof: bool, } impl<'a, Session, Input, Output, State> Context<'a, Session, Input, Output, State> { /// Creates a new [`Context`] structure. pub fn new( session: &'a mut Session, input: &'a mut Input, output: &'a mut Output, state: &'a mut State, buf: &'a [u8], eof: bool, ) -> Self { Self { session, input, output, buf, eof, state, } } } expectrl-0.7.1/src/interact/mod.rs000064400000000000000000000030221046102023000152010ustar 00000000000000//! This module contains a routines for running and utilizing an interacting session with a [`Session`]. //! #![cfg_attr(all(unix, not(feature = "async")), doc = "```no_run")] #![cfg_attr(not(all(unix, not(feature = "async"))), doc = "```ignore")] //! use expectrl::{interact::{InteractOptions, actions::lookup::Lookup}, spawn, stream::stdin::Stdin, Regex}; //! //! #[derive(Debug)] //! enum Answer { //! Yes, //! No, //! Unrecognized, //! } //! //! let mut session = spawn("cat").expect("Can't spawn a session"); //! //! let mut input_action = Lookup::new(); //! //! let mut stdin = Stdin::open().unwrap(); //! let stdout = std::io::stdout(); //! //! let mut opts = InteractOptions::new(Answer::Unrecognized) //! .on_input(|mut ctx| { //! let m = input_action.on(ctx.buf, ctx.eof, "yes")?; //! if m.is_some() { //! *ctx.state = Answer::Yes; //! }; //! //! let m = input_action.on(ctx.buf, ctx.eof, "no")?; //! if m.is_some() { //! *ctx.state = Answer::No; //! }; //! //! Ok(false) //! }); //! //! session.interact(&mut stdin, stdout) //! .spawn(&mut opts) //! .expect("Failed to run an interact session"); //! //! let answer = opts.into_inner(); //! //! stdin.close().unwrap(); //! //! println!("It was said {:?}", answer); //! ``` //! //! [`Session`]: crate::session::Session pub mod actions; mod context; mod opts; mod session; pub use context::Context; pub use opts::{InteractOptions, NoAction, NoFilter}; pub use session::InteractSession; expectrl-0.7.1/src/interact/opts.rs000064400000000000000000000131161046102023000154140ustar 00000000000000use std::borrow::Cow; use super::Context; use crate::{session::OsProcess, Error, Session}; type Result = std::result::Result; /// Interact options (aka callbacks you can set to be callled being in an interactive mode). #[derive(Debug)] pub struct InteractOptions { pub(crate) state: C, pub(crate) input_filter: Option, pub(crate) output_filter: Option, pub(crate) input_action: Option, pub(crate) output_action: Option, pub(crate) idle_action: Option, } type DefaultOps = InteractOptions< C, NoFilter, NoFilter, NoAction, I, O, C>, NoAction, I, O, C>, NoAction, I, O, C>, >; impl Default for DefaultOps { fn default() -> Self { Self::new(()) } } impl DefaultOps { /// Set a state. pub fn new(state: C) -> Self { Self { state, input_filter: None, output_filter: None, input_action: None, output_action: None, idle_action: None, } } } impl InteractOptions { /// Get a reference on state pub fn get_state(&self) -> &C { &self.state } /// Get a mut reference on state pub fn get_state_mut(&mut self) -> &mut C { &mut self.state } /// Returns a inner state. pub fn into_inner(self) -> C { self.state } /// Sets the output filter. /// The output_filter will be passed all the output from the child process. /// /// The filter isn't applied to user's `read` calls through the [`Context`] in callbacks. pub fn output_filter(self, filter: F) -> InteractOptions where F: FnMut(&[u8]) -> Result>, { InteractOptions { input_filter: self.input_filter, output_filter: Some(filter), input_action: self.input_action, output_action: self.output_action, idle_action: self.idle_action, state: self.state, } } /// Sets the input filter. /// The input_filter will be passed all the keyboard input from the user. /// /// The input_filter is run BEFORE the check for the escape_character. /// The filter is called BEFORE calling a on_input callback if it's set. pub fn input_filter(self, filter: F) -> InteractOptions where F: FnMut(&[u8]) -> Result>, { InteractOptions { input_filter: Some(filter), output_filter: self.output_filter, input_action: self.input_action, output_action: self.output_action, idle_action: self.idle_action, state: self.state, } } } impl InteractOptions, I, O, C>, OA, WA> { /// Puts a hanlder which will be called when users input is detected. /// /// Be aware that currently async version doesn't take a Session as an argument. /// See . pub fn on_input(self, action: F) -> InteractOptions where F: FnMut(Context<'_, Session, I, O, C>) -> Result, { InteractOptions { input_filter: self.input_filter, output_filter: self.output_filter, input_action: Some(action), output_action: self.output_action, idle_action: self.idle_action, state: self.state, } } } impl InteractOptions, I, O, C>, WA> { /// Puts a hanlder which will be called when process output is detected. /// /// IMPORTANT: /// /// Please be aware that your use of [Session::expect], [Session::check] and any `read` operation on session /// will cause the read bytes not to apeard in the output stream! pub fn on_output(self, action: F) -> InteractOptions where F: FnMut(Context<'_, Session, I, O, C>) -> Result, { InteractOptions { input_filter: self.input_filter, output_filter: self.output_filter, input_action: self.input_action, output_action: Some(action), idle_action: self.idle_action, state: self.state, } } } impl InteractOptions, I, O, C>> { /// Puts a handler which will be called on each interaction when no input is detected. pub fn on_idle(self, action: F) -> InteractOptions where F: FnMut(Context<'_, Session, I, O, C>) -> Result, { InteractOptions { input_filter: self.input_filter, output_filter: self.output_filter, input_action: self.input_action, output_action: self.output_action, idle_action: Some(action), state: self.state, } } } /// A helper type to set a default action to [`InteractSession`]. /// /// [`InteractSession`]: crate::interact::InteractSession pub type NoAction = fn(Context<'_, S, I, O, C>) -> Result; /// A helper type to set a default filter to [`InteractSession`]. /// /// [`InteractSession`]: crate::interact::InteractSession pub type NoFilter = fn(&[u8]) -> Result>; expectrl-0.7.1/src/interact/session.rs000064400000000000000000000653371046102023000161260ustar 00000000000000//! This module contains a [`InteractSession`] which runs an interact session with IO. use std::{ borrow::{BorrowMut, Cow}, io::{ErrorKind, Write}, }; use crate::{session::OsProcess, Error, Session}; #[cfg(not(feature = "async"))] use std::io::Read; use super::{Context, InteractOptions}; #[cfg(all(not(feature = "async"), not(feature = "polling")))] use crate::process::NonBlocking; /// InteractConfig represents options of an interactive session. #[derive(Debug)] pub struct InteractSession { session: Session, input: Input, output: Output, escape_character: u8, #[cfg(unix)] status: Option, } impl InteractSession { /// Default escape character. pub const ESCAPE: u8 = 29; // Ctrl-] /// Creates a new object of [InteractSession]. pub fn new(session: S, input: I, output: O) -> InteractSession { InteractSession { input, output, session, escape_character: Self::ESCAPE, #[cfg(unix)] status: None, } } /// Sets an escape character after seen which the interact interactions will be stopped /// and controll will be returned to a caller process. pub fn set_escape_character(mut self, c: u8) -> Self { self.escape_character = c; self } /// Returns a status of spawned session if it was exited. /// /// If [`Self::spawn`] returns false but this method returns None it means that a child process was shutdown by various reasons. /// Which sometimes happens and it's not considered to be a valid [`WaitStatus`], so None is returned. /// /// [`Self::spawn`]: crate::interact::InteractSession::spawn /// [`WaitStatus`]: crate::WaitStatus #[cfg(unix)] pub fn get_status(&self) -> Option { self.status } } #[cfg(not(any(feature = "async", feature = "polling")))] impl InteractSession<&mut Session, I, O> where I: Read, O: Write, S: NonBlocking + Write + Read, { /// Runs the session. /// /// See [`Session::interact`]. /// /// [`Session::interact`]: crate::session::Session::interact pub fn spawn(&mut self, mut ops: OPS) -> Result where OPS: BorrowMut>, IF: FnMut(&[u8]) -> Result, Error>, OF: FnMut(&[u8]) -> Result, Error>, IA: FnMut(Context<'_, Session, I, O, C>) -> Result, OA: FnMut(Context<'_, Session, I, O, C>) -> Result, WA: FnMut(Context<'_, Session, I, O, C>) -> Result, { #[cfg(unix)] { let is_echo = self .session .get_process() .get_echo() .map_err(|e| Error::unknown("failed to get echo", e.to_string()))?; if !is_echo { let _ = self.session.get_process_mut().set_echo(true, None); } self.status = None; let is_alive = interact_buzy_loop(self, ops.borrow_mut())?; if !is_echo { let _ = self.session.get_process_mut().set_echo(false, None); } Ok(is_alive) } #[cfg(windows)] { interact_buzy_loop(self, ops.borrow_mut()) } } } #[cfg(all(unix, feature = "polling", not(feature = "async")))] impl InteractSession<&mut Session, I, O> where I: Read + std::os::unix::io::AsRawFd, O: Write, S: Write + Read + std::os::unix::io::AsRawFd, { /// Runs the session. /// /// See [`Session::interact`]. /// /// [`Session::interact`]: crate::session::Session::interact pub fn spawn(&mut self, mut ops: OPS) -> Result where OPS: BorrowMut>, IF: FnMut(&[u8]) -> Result, Error>, OF: FnMut(&[u8]) -> Result, Error>, IA: FnMut(Context<'_, Session, I, O, C>) -> Result, OA: FnMut(Context<'_, Session, I, O, C>) -> Result, WA: FnMut(Context<'_, Session, I, O, C>) -> Result, { let is_echo = self .session .get_process() .get_echo() .map_err(|e| Error::unknown("failed to get echo", e.to_string()))?; if !is_echo { let _ = self.session.get_process_mut().set_echo(true, None); } self.status = None; let is_alive = interact_polling(self, ops.borrow_mut())?; if !is_echo { let _ = self.session.get_process_mut().set_echo(false, None); } Ok(is_alive) } } #[cfg(feature = "async")] impl InteractSession<&mut Session, I, O> where I: futures_lite::AsyncRead + Unpin, O: Write, S: futures_lite::AsyncRead + futures_lite::AsyncWrite + Unpin, { /// Runs the session. /// /// See [`Session::interact`]. /// /// [`Session::interact`]: crate::session::Session::interact pub async fn spawn(&mut self, mut opts: OPS) -> Result where OPS: BorrowMut>, IF: FnMut(&[u8]) -> Result, Error>, OF: FnMut(&[u8]) -> Result, Error>, IA: FnMut(Context<'_, Session, I, O, C>) -> Result, OA: FnMut(Context<'_, Session, I, O, C>) -> Result, WA: FnMut(Context<'_, Session, I, O, C>) -> Result, { #[cfg(unix)] { let is_echo = self .session .get_echo() .map_err(|e| Error::unknown("failed to get echo", e.to_string()))?; if !is_echo { let _ = self.session.set_echo(true, None); } let is_alive = interact_async(self, opts.borrow_mut()).await?; if !is_echo { let _ = self.session.set_echo(false, None); } Ok(is_alive) } #[cfg(windows)] { interact_async(self, opts.borrow_mut()).await } } } #[cfg(all(windows, feature = "polling", not(feature = "async")))] impl InteractSession<&mut Session, I, O> where I: Read + Clone + Send + 'static, O: Write, { /// Runs the session. /// /// See [`Session::interact`]. /// /// [`Session::interact`]: crate::session::Session::interact pub fn spawn(&mut self, mut ops: OPS) -> Result where OPS: BorrowMut>, IF: FnMut(&[u8]) -> Result, Error>, OF: FnMut(&[u8]) -> Result, Error>, IA: FnMut(Context<'_, Session, I, O, C>) -> Result, OA: FnMut(Context<'_, Session, I, O, C>) -> Result, WA: FnMut(Context<'_, Session, I, O, C>) -> Result, { interact_polling_on_thread(self, ops.borrow_mut()) } } #[cfg(all(not(feature = "async"), not(feature = "polling")))] fn interact_buzy_loop( interact: &mut InteractSession<&mut Session, I, O>, opts: &mut InteractOptions, ) -> Result where S: NonBlocking + Write + Read, I: Read, O: Write, IF: FnMut(&[u8]) -> Result, Error>, OF: FnMut(&[u8]) -> Result, Error>, IA: FnMut(Context<'_, Session, I, O, C>) -> Result, OA: FnMut(Context<'_, Session, I, O, C>) -> Result, WA: FnMut(Context<'_, Session, I, O, C>) -> Result, { let mut buf = [0; 512]; loop { #[cfg(unix)] { let status = get_status(interact.session)?; if !matches!(status, Some(crate::WaitStatus::StillAlive)) { interact.status = status; return Ok(false); } } #[cfg(windows)] { if !interact.session.is_alive()? { return Ok(false); } } match interact.session.try_read(&mut buf) { Ok(n) => { let eof = n == 0; let buf = &buf[..n]; let buf = call_filter(opts.output_filter.as_mut(), buf)?; let exit = call_action( opts.output_action.as_mut(), interact.session, &mut interact.input, &mut interact.output, &mut opts.state, &buf, eof, )?; if eof || exit { return Ok(true); } spin_write(&mut interact.output, &buf)?; spin_flush(&mut interact.output)?; } Err(err) if err.kind() == ErrorKind::WouldBlock => {} Err(err) => return Err(err.into()), } // We dont't print user input back to the screen. // In terminal mode it will be ECHOed back automatically. // This way we preserve terminal seetings for example when user inputs password. // The terminal must have been prepared before. match interact.input.read(&mut buf) { Ok(n) => { let eof = n == 0; let buf = &buf[..n]; let buf = call_filter(opts.input_filter.as_mut(), buf)?; let exit = call_action( opts.input_action.as_mut(), interact.session, &mut interact.input, &mut interact.output, &mut opts.state, &buf, eof, )?; if eof | exit { return Ok(true); } let escape_char_position = buf.iter().position(|c| *c == interact.escape_character); match escape_char_position { Some(pos) => { interact.session.write_all(&buf[..pos])?; return Ok(true); } None => { interact.session.write_all(&buf[..])?; } } } Err(err) if err.kind() == ErrorKind::WouldBlock => {} Err(err) => return Err(err.into()), } let exit = call_action( opts.idle_action.as_mut(), interact.session, &mut interact.input, &mut interact.output, &mut opts.state, &[], false, )?; if exit { return Ok(true); } } } #[cfg(all(unix, not(feature = "async"), feature = "polling"))] fn interact_polling( interact: &mut InteractSession<&mut Session, I, O>, opts: &mut InteractOptions, ) -> Result where S: Write + Read + std::os::unix::io::AsRawFd, I: Read + std::os::unix::io::AsRawFd, O: Write, IF: FnMut(&[u8]) -> Result, Error>, OF: FnMut(&[u8]) -> Result, Error>, IA: FnMut(Context<'_, Session, I, O, C>) -> Result, OA: FnMut(Context<'_, Session, I, O, C>) -> Result, WA: FnMut(Context<'_, Session, I, O, C>) -> Result, { use polling::{Event, Poller}; // Create a poller and register interest in readability on the socket. let poller = Poller::new()?; poller.add(interact.input.as_raw_fd(), Event::readable(0))?; poller.add( interact.session.get_stream().as_raw_fd(), Event::readable(1), )?; let mut buf = [0; 512]; // The event loop. let mut events = Vec::new(); loop { let status = get_status(interact.session)?; if !matches!(status, Some(crate::WaitStatus::StillAlive)) { interact.status = status; return Ok(false); } // Wait for at least one I/O event. events.clear(); let _ = poller.wait(&mut events, Some(std::time::Duration::from_secs(5)))?; for ev in &events { if ev.key == 0 { // We dont't print user input back to the screen. // In terminal mode it will be ECHOed back automatically. // This way we preserve terminal seetings for example when user inputs password. // The terminal must have been prepared before. match interact.input.read(&mut buf) { Ok(n) => { let eof = n == 0; let buf = &buf[..n]; let buf = call_filter(opts.input_filter.as_mut(), buf)?; let exit = call_action( opts.input_action.as_mut(), interact.session, &mut interact.input, &mut interact.output, &mut opts.state, &buf, eof, )?; if eof || exit { return Ok(true); } let escape_char_pos = buf.iter().position(|c| *c == interact.escape_character); match escape_char_pos { Some(pos) => { interact.session.write_all(&buf[..pos]).map_err(Error::IO)?; return Ok(true); } None => interact.session.write_all(&buf[..])?, } } Err(err) if err.kind() == ErrorKind::WouldBlock => {} Err(err) => return Err(err.into()), } // Set interest in the next readability event. poller.modify(interact.input.as_raw_fd(), Event::readable(0))?; } if ev.key == 1 { match interact.session.read(&mut buf) { Ok(n) => { let eof = n == 0; let buf = &buf[..n]; let buf = call_filter(opts.output_filter.as_mut(), buf)?; let exit = call_action( opts.output_action.as_mut(), interact.session, &mut interact.input, &mut interact.output, &mut opts.state, &buf, eof, )?; if eof || exit { return Ok(true); } spin_write(&mut interact.output, &buf)?; spin_flush(&mut interact.output)?; } Err(err) if err.kind() == ErrorKind::WouldBlock => {} Err(err) => return Err(err.into()), } // Set interest in the next readability event. poller.modify( interact.session.get_stream().as_raw_fd(), Event::readable(1), )?; } } let exit = call_action( opts.idle_action.as_mut(), interact.session, &mut interact.input, &mut interact.output, &mut opts.state, &[], false, )?; if exit { return Ok(true); } } } #[cfg(all(windows, not(feature = "async"), feature = "polling"))] fn interact_polling_on_thread( interact: &mut InteractSession<&mut Session, I, O>, opts: &mut InteractOptions, ) -> Result where I: Read + Clone + Send + 'static, O: Write, IF: FnMut(&[u8]) -> Result, Error>, OF: FnMut(&[u8]) -> Result, Error>, IA: FnMut(Context<'_, Session, I, O, C>) -> Result, OA: FnMut(Context<'_, Session, I, O, C>) -> Result, WA: FnMut(Context<'_, Session, I, O, C>) -> Result, { use crate::{ error::to_io_error, waiter::{Recv, Wait2}, }; // Create a poller and register interest in readability on the socket. let stream = interact .session .get_stream() .try_clone() .map_err(to_io_error(""))?; let mut poller = Wait2::new(interact.input.clone(), stream); loop { // In case where proceses exits we are trying to // fill buffer to run callbacks if there was something in. // // We ignore errors because there might be errors like EOCHILD etc. if interact.session.is_alive()? { return Ok(false); } // Wait for at least one I/O event. let event = poller.recv().map_err(to_io_error(""))?; match event { Recv::R1(b) => match b { Ok(b) => { let buf = b.map_or([0], |b| [b]); let eof = b.is_none(); let n = if eof { 0 } else { 1 }; let buf = &buf[..n]; let buf = call_filter(opts.input_filter.as_mut(), buf)?; let exit = call_action( opts.input_action.as_mut(), interact.session, &mut interact.input, &mut interact.output, &mut opts.state, &buf, eof, )?; if eof || exit { return Ok(true); } // todo: replace all of these by 1 by 1 write let escape_char_pos = buf.iter().position(|c| *c == interact.escape_character); match escape_char_pos { Some(pos) => { interact.session.write_all(&buf[..pos])?; return Ok(true); } None => interact.session.write_all(&buf[..])?, } } Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {} Err(err) => return Err(err.into()), }, Recv::R2(b) => match b { Ok(b) => { let buf = b.map_or([0], |b| [b]); let eof = b.is_none(); let n = if eof { 0 } else { 1 }; let buf = &buf[..n]; let buf = call_filter(opts.output_filter.as_mut(), buf)?; let exit = call_action( opts.output_action.as_mut(), interact.session, &mut interact.input, &mut interact.output, &mut opts.state, &buf, eof, )?; if eof || exit { return Ok(true); } interact.output.write_all(&buf)?; interact.output.flush()?; } Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {} Err(err) => return Err(err.into()), }, Recv::Timeout => { let exit = call_action( opts.idle_action.as_mut(), interact.session, &mut interact.input, &mut interact.output, &mut opts.state, &[], false, )?; if exit { return Ok(true); } } } } } #[cfg(feature = "async")] async fn interact_async( interact: &mut InteractSession<&mut Session, I, O>, opts: &mut InteractOptions, ) -> Result where S: futures_lite::AsyncRead + futures_lite::AsyncWrite + Unpin, I: futures_lite::AsyncRead + Unpin, O: Write, IF: FnMut(&[u8]) -> Result, Error>, OF: FnMut(&[u8]) -> Result, Error>, IA: FnMut(Context<'_, Session, I, O, C>) -> Result, OA: FnMut(Context<'_, Session, I, O, C>) -> Result, WA: FnMut(Context<'_, Session, I, O, C>) -> Result, { use std::io; use futures_lite::{AsyncReadExt, AsyncWriteExt}; let mut stdin_buf = [0; 512]; let mut proc_buf = [0; 512]; loop { #[cfg(unix)] { let status = get_status(interact.session)?; if !matches!(status, Some(crate::WaitStatus::StillAlive)) { interact.status = status; return Ok(false); } } #[cfg(windows)] { if !interact.session.is_alive()? { return Ok(false); } } #[derive(Debug)] enum ReadFrom { Stdin, OsProcessess, Timeout, } let read_process = async { ( ReadFrom::OsProcessess, interact.session.read(&mut proc_buf).await, ) }; let read_stdin = async { (ReadFrom::Stdin, interact.input.read(&mut stdin_buf).await) }; let timeout = async { ( ReadFrom::Timeout, async { futures_timer::Delay::new(std::time::Duration::from_secs(5)).await; io::Result::Ok(0) } .await, ) }; let read_fut = futures_lite::future::or(read_process, read_stdin); let (read_from, result) = futures_lite::future::or(read_fut, timeout).await; match read_from { ReadFrom::OsProcessess => { let n = result?; let eof = n == 0; let buf = &proc_buf[..n]; let buf = call_filter(opts.output_filter.as_mut(), buf)?; let exit = call_action( opts.output_action.as_mut(), interact.session, &mut interact.input, &mut interact.output, &mut opts.state, &buf, eof, )?; if eof || exit { return Ok(true); } spin_write(&mut interact.output, &buf)?; spin_flush(&mut interact.output)?; } ReadFrom::Stdin => { // We dont't print user input back to the screen. // In terminal mode it will be ECHOed back automatically. // This way we preserve terminal seetings for example when user inputs password. // The terminal must have been prepared before. match result { Ok(n) => { let eof = n == 0; let buf = &stdin_buf[..n]; let buf = call_filter(opts.output_filter.as_mut(), buf)?; let exit = call_action( opts.input_action.as_mut(), interact.session, &mut interact.input, &mut interact.output, &mut opts.state, &buf, eof, )?; if eof || exit { return Ok(true); } let escape_char_pos = buf.iter().position(|c| *c == interact.escape_character); match escape_char_pos { Some(pos) => { interact.session.write_all(&buf[..pos]).await?; return Ok(true); } None => interact.session.write_all(&buf[..]).await?, } } Err(err) if err.kind() == io::ErrorKind::WouldBlock => {} Err(err) => return Err(err.into()), } } ReadFrom::Timeout => { let exit = call_action( opts.idle_action.as_mut(), interact.session, &mut interact.input, &mut interact.output, &mut opts.state, &[], false, )?; if exit { return Ok(true); } } } } } fn spin_write(mut writer: W, buf: &[u8]) -> std::io::Result<()> where W: Write, { loop { match writer.write_all(buf) { Ok(_) => return Ok(()), Err(err) if err.kind() != std::io::ErrorKind::WouldBlock => return Err(err), Err(_) => (), } } } fn spin_flush(mut writer: W) -> std::io::Result<()> where W: Write, { loop { match writer.flush() { Ok(_) => return Ok(()), Err(err) if err.kind() != std::io::ErrorKind::WouldBlock => return Err(err), Err(_) => (), } } } fn call_action( action: Option, s: &mut S, r: &mut I, w: &mut O, state: &mut C, buf: &[u8], eof: bool, ) -> Result where F: FnMut(Context<'_, S, I, O, C>) -> Result, { match action { Some(mut action) => (action)(Context::new(s, r, w, state, buf, eof)), None => Ok(false), } } fn call_filter(filter: Option, buf: &[u8]) -> Result, Error> where F: FnMut(&[u8]) -> Result, Error>, { match filter { Some(mut action) => (action)(buf), None => Ok(Cow::Borrowed(buf)), } } #[cfg(unix)] fn get_status(session: &Session) -> Result, Error> { match session.get_process().status() { Ok(status) => Ok(Some(status)), Err(ptyprocess::errno::Errno::ECHILD | ptyprocess::errno::Errno::ESRCH) => Ok(None), Err(err) => Err(Error::IO(std::io::Error::new(ErrorKind::Other, err))), } } expectrl-0.7.1/src/lib.rs000064400000000000000000000106171046102023000133670ustar 00000000000000#![warn( missing_docs, future_incompatible, single_use_lifetimes, trivial_casts, trivial_numeric_casts, unreachable_pub, unused_extern_crates, unused_import_braces, unused_qualifications, unused_results, unused_variables, variant_size_differences, missing_debug_implementations, rust_2018_idioms )] #![allow(clippy::uninlined_format_args)] //! # A tool for automating terminal applications on alike original expect. //! //! Using the library you can: //! //! - Spawn process //! - Control process //! - Interact with process's IO(input/output). //! //! `expectrl` like original `expect` may shine when you're working with interactive applications. //! If your application is not interactive you may not find the library the best choise. //! //! ## Feature flags //! //! - `async`: Enables a async/await public API. //! - `polling`: Enables polling backend in interact session. Be cautious to use it on windows. //! //! ## Examples //! //! ### An example for interacting via ftp. //! //! ```no_run,ignore //! use expectrl::{spawn, Regex, Eof, WaitStatus}; //! //! let mut p = spawn("ftp speedtest.tele2.net").unwrap(); //! p.expect(Regex("Name \\(.*\\):")).unwrap(); //! p.send_line("anonymous").unwrap(); //! p.expect("Password").unwrap(); //! p.send_line("test").unwrap(); //! p.expect("ftp>").unwrap(); //! p.send_line("cd upload").unwrap(); //! p.expect("successfully changed.\r\nftp>").unwrap(); //! p.send_line("pwd").unwrap(); //! p.expect(Regex("[0-9]+ \"/upload\"")).unwrap(); //! p.send_line("exit").unwrap(); //! p.expect(Eof).unwrap(); //! assert_eq!(p.wait().unwrap(), WaitStatus::Exited(p.pid(), 0)); //! ``` //! //! *The example inspired by the one in [philippkeller/rexpect].* //! //! ### An example when `Command` is used. //! //! ```no_run,ignore //! use std::{process::Command, io::prelude::*}; //! use expectrl::Session; //! //! let mut echo_hello = Command::new("sh"); //! echo_hello.arg("-c").arg("echo hello"); //! //! let mut p = Session::spawn(echo_hello).unwrap(); //! p.expect("hello").unwrap(); //! ``` //! //! ### An example of logging. //! //! ```no_run,ignore //! use std::io::{stdout, prelude::*}; //! use expectrl::{spawn, session::log}; //! //! let mut sh = log(spawn("sh").unwrap(), stdout()).unwrap(); //! //! writeln!(sh, "Hello World").unwrap(); //! ``` //! //! ### An example of `async` feature. //! //! You need to provide a `features=["async"]` flag to use it. //! //! ```no_run,ignore //! use expectrl::spawn; //! //! let mut p = spawn("cat").await.unwrap(); //! p.expect("hello").await.unwrap(); //! ``` //! //! ### An example of interact session with `STDIN` and `STDOUT` //! //! ```no_run,ignore //! use expectrl::{spawn, stream::stdin::Stdin}; //! use std::io::stdout; //! //! let mut sh = spawn("cat").expect("Failed to spawn a 'cat' process"); //! //! let mut stdin = Stdin::open().expect("Failed to create stdin"); //! //! sh.interact(&mut stdin, stdout()) //! .spawn() //! .expect("Failed to start interact session"); //! //! stdin.close().expect("Failed to close a stdin"); //! ``` //! //! [For more examples, check the examples directory.](https://github.com/zhiburt/expectrl/tree/main/examples) mod captures; mod check_macros; mod control_code; mod error; mod needle; #[cfg(all(windows, feature = "polling"))] mod waiter; pub mod interact; pub mod process; pub mod repl; pub mod session; pub mod stream; pub use captures::Captures; pub use control_code::ControlCode; pub use error::Error; pub use needle::{Any, Eof, NBytes, Needle, Regex}; #[cfg(unix)] pub use ptyprocess::{Signal, WaitStatus}; pub use session::Session; /// Spawn spawnes a new session. /// /// It accepts a command and possibly arguments just as string. /// It doesn't parses ENV variables. For complex constrictions use [`Session::spawn`]. /// /// # Example /// /// ```no_run,ignore /// use std::{thread, time::Duration, io::{Read, Write}}; /// use expectrl::{spawn, ControlCode}; /// /// let mut p = spawn("cat").unwrap(); /// p.send_line("Hello World").unwrap(); /// /// thread::sleep(Duration::from_millis(300)); // give 'cat' some time to set up /// p.send(ControlCode::EndOfText).unwrap(); // abort: SIGINT /// /// let mut buf = String::new(); /// p.read_to_string(&mut buf).unwrap(); /// /// assert_eq!(buf, "Hello World\r\n"); /// ``` /// /// [`Session::spawn`]: ./struct.Session.html?#spawn pub fn spawn>(cmd: S) -> Result { Session::spawn_cmd(cmd.as_ref()) } expectrl-0.7.1/src/needle.rs000064400000000000000000000232021046102023000140470ustar 00000000000000//! A module which contains a [Needle] trait and a list of implementations. //! [Needle] is used for seach in a byte stream. //! //! The list of provided implementations can be found in the documentation. use crate::error::Error; /// Needle an interface for search of a match in a buffer. pub trait Needle { /// Function returns all matches that were occured. fn check(&self, buf: &[u8], eof: bool) -> Result, Error>; } /// Match structure represent a range of bytes where match was found. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Match { start: usize, end: usize, } impl Match { /// New construct's an intanse of a Match. pub fn new(start: usize, end: usize) -> Self { Self { start, end } } /// Start returns a start index of a match. pub fn start(&self) -> usize { self.start } /// End returns an end index of a match. pub fn end(&self) -> usize { self.end } } impl From> for Match { fn from(m: regex::bytes::Match<'_>) -> Self { Self::new(m.start(), m.end()) } } /// Regex tries to look up a match by a regex. #[derive(Debug)] pub struct Regex>(pub Re); impl> Needle for Regex { fn check(&self, buf: &[u8], _: bool) -> Result, Error> { let regex = regex::bytes::Regex::new(self.0.as_ref()).map_err(|_| Error::RegexParsing)?; let matches = regex .captures_iter(buf) .flat_map(|c| c.iter().flatten().map(|m| m.into()).collect::>()) .collect(); Ok(matches) } } /// Eof consider a match when an EOF is reached. #[derive(Debug)] pub struct Eof; impl Needle for Eof { fn check(&self, buf: &[u8], eof: bool) -> Result, Error> { match eof { true => Ok(vec![Match::new(0, buf.len())]), false => Ok(Vec::new()), } } } /// NBytes matches N bytes from the stream. #[derive(Debug)] pub struct NBytes(pub usize); impl NBytes { fn count(&self) -> usize { self.0 } } impl Needle for NBytes { fn check(&self, buf: &[u8], _: bool) -> Result, Error> { match buf.len() >= self.count() { true => Ok(vec![Match::new(0, self.count())]), false => Ok(Vec::new()), } } } impl Needle for [u8] { fn check(&self, buf: &[u8], _: bool) -> Result, Error> { if buf.len() < self.len() { return Ok(Vec::new()); } for l_bound in 0..buf.len() { let r_bound = l_bound + self.len(); if r_bound > buf.len() { return Ok(Vec::new()); } if self == &buf[l_bound..r_bound] { return Ok(vec![Match::new(l_bound, r_bound)]); } } Ok(Vec::new()) } } impl Needle for &[u8] { fn check(&self, buf: &[u8], eof: bool) -> Result, Error> { (*self).check(buf, eof) } } impl Needle for str { fn check(&self, buf: &[u8], eof: bool) -> Result, Error> { self.as_bytes().check(buf, eof) } } impl Needle for &str { fn check(&self, buf: &[u8], eof: bool) -> Result, Error> { self.as_bytes().check(buf, eof) } } impl Needle for String { fn check(&self, buf: &[u8], eof: bool) -> Result, Error> { self.as_bytes().check(buf, eof) } } impl Needle for u8 { fn check(&self, buf: &[u8], eof: bool) -> Result, Error> { ([*self][..]).check(buf, eof) } } impl Needle for char { fn check(&self, buf: &[u8], eof: bool) -> Result, Error> { char::to_string(self).check(buf, eof) } } /// Any matches uses all provided lookups and returns a match /// from a first successfull match. /// /// It does checks lookups in order they were provided. /// /// # Example /// /// ```no_run,ignore /// use expectrl::{spawn, Any}; /// /// let mut p = spawn("cat").unwrap(); /// p.expect(Any(["we", "are", "here"])).unwrap(); /// ``` /// /// To be able to combine different types of lookups you can call [Any::boxed]. /// /// ```no_run,ignore /// use expectrl::{spawn, Any, NBytes}; /// /// let mut p = spawn("cat").unwrap(); /// p.expect(Any::boxed(vec![Box::new("we"), Box::new(NBytes(3))])).unwrap(); /// ``` #[derive(Debug)] pub struct Any(pub I); impl Any>> { /// Boxed expectes a list of [Box]ed lookups. pub fn boxed(v: Vec>) -> Self { Self(v) } } impl Needle for Any<&[T]> where T: Needle, { fn check(&self, buf: &[u8], eof: bool) -> Result, Error> { for needle in self.0.iter() { let found = needle.check(buf, eof)?; if !found.is_empty() { return Ok(found); } } Ok(Vec::new()) } } impl Needle for Any> where T: Needle, { fn check(&self, buf: &[u8], eof: bool) -> Result, Error> { Any(self.0.as_slice()).check(buf, eof) } } impl Needle for Any<[T; N]> where T: Needle, { fn check(&self, buf: &[u8], eof: bool) -> Result, Error> { Any(&self.0[..]).check(buf, eof) } } impl Needle for Any<&'_ [T; N]> where T: Needle, { fn check(&self, buf: &[u8], eof: bool) -> Result, Error> { Any(&self.0[..]).check(buf, eof) } } impl Needle for &T { fn check(&self, buf: &[u8], eof: bool) -> Result, Error> { T::check(self, buf, eof) } } impl Needle for Box { fn check(&self, buf: &[u8], eof: bool) -> Result, Error> { self.as_ref().check(buf, eof) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_regex() { assert_eq!( Regex("[0-9]+").check(b"+012345", false).unwrap(), vec![Match::new(1, 7)] ); assert_eq!( Regex(r"\w+").check(b"What's Up Boys", false).unwrap(), vec![ Match::new(0, 4), Match::new(5, 6), Match::new(7, 9), Match::new(10, 14) ] ); assert_eq!( Regex(r"((?:\w|')+)") .check(b"What's Up Boys", false) .unwrap(), vec![ Match::new(0, 6), Match::new(0, 6), Match::new(7, 9), Match::new(7, 9), Match::new(10, 14), Match::new(10, 14) ] ); assert_eq!( Regex(r"(\w+)=(\w+)").check(b"asd=123", false).unwrap(), vec![Match::new(0, 7), Match::new(0, 3), Match::new(4, 7)] ); } #[test] fn test_eof() { assert_eq!(Eof.check(b"qwe", true).unwrap(), vec![Match::new(0, 3)]); assert_eq!(Eof.check(b"qwe", false).unwrap(), vec![]); } #[test] fn test_n_bytes() { assert_eq!( NBytes(1).check(b"qwe", false).unwrap(), vec![Match::new(0, 1)] ); assert_eq!( NBytes(0).check(b"qwe", false).unwrap(), vec![Match::new(0, 0)] ); assert_eq!(NBytes(10).check(b"qwe", false).unwrap(), vec![]); } #[test] fn test_str() { assert_eq!( "wer".check(b"qwerty", false).unwrap(), vec![Match::new(1, 4)] ); assert_eq!("123".check(b"qwerty", false).unwrap(), vec![]); assert_eq!("".check(b"qwerty", false).unwrap(), vec![Match::new(0, 0)]); } #[test] fn test_bytes() { assert_eq!( b"wer".check(b"qwerty", false).unwrap(), vec![Match::new(1, 4)] ); assert_eq!(b"123".check(b"qwerty", false).unwrap(), vec![]); assert_eq!(b"".check(b"qwerty", false).unwrap(), vec![Match::new(0, 0)]); } #[allow(clippy::needless_borrow)] #[test] #[allow(clippy::needless_borrow)] fn test_bytes_ref() { assert_eq!( (&[b'q', b'w', b'e']).check(b"qwerty", false).unwrap(), vec![Match::new(0, 3)] ); assert_eq!( (&[b'1', b'2', b'3']).check(b"qwerty", false).unwrap(), vec![] ); assert_eq!( (&[]).check(b"qwerty", false).unwrap(), vec![Match::new(0, 0)] ); } #[test] fn test_byte() { assert_eq!( (b'3').check(b"1234", false).unwrap(), vec![Match::new(2, 3)] ); assert_eq!((b'3').check(b"1234", true).unwrap(), vec![Match::new(2, 3)]); } #[test] fn test_char() { for eof in [false, true] { assert_eq!( ('😘').check("😁😄😅😓😠😘😌".as_bytes(), eof).unwrap(), vec![Match::new(20, 24)] ); } } #[test] fn test_any() { assert_eq!( Any::>>(vec![Box::new("we"), Box::new(NBytes(3))]) .check(b"qwerty", false) .unwrap(), vec![Match::new(1, 3)] ); assert_eq!( Any::boxed(vec![Box::new("123"), Box::new(NBytes(100))]) .check(b"qwerty", false) .unwrap(), vec![], ); assert_eq!( Any(["123", "234", "rty"]).check(b"qwerty", false).unwrap(), vec![Match::new(3, 6)] ); assert_eq!( Any(&["123", "234", "rty"][..]) .check(b"qwerty", false) .unwrap(), vec![Match::new(3, 6)] ); assert_eq!( Any(&["123", "234", "rty"]).check(b"qwerty", false).unwrap(), vec![Match::new(3, 6)] ); } } expectrl-0.7.1/src/process/mod.rs000064400000000000000000000035601046102023000150550ustar 00000000000000//! This module contains a platform independent abstraction over an os process. use std::io::Result; #[cfg(unix)] pub mod unix; #[cfg(windows)] pub mod windows; /// This trait represents a platform independent process which runs a program. pub trait Process: Sized { /// A command which process can run. type Command; /// A representation of IO stream of communication with a programm a process is running. type Stream; /// Spawn parses a given string as a commandline string and spawns it on a process. fn spawn>(cmd: S) -> Result; /// Spawn_command runs a process with a given command. fn spawn_command(command: Self::Command) -> Result; /// It opens a IO stream with a spawned process. fn open_stream(&mut self) -> Result; } #[allow(clippy::wrong_self_convention)] /// Healthcheck represents a check by which we can determine if a spawned process is still alive. pub trait Healthcheck { /// The function returns a status of a process if it still alive and it can operate. fn is_alive(&mut self) -> Result; } /// NonBlocking interface represens a [std::io::Read]er which can be turned in a non blocking mode /// so its read operations will return imideately. pub trait NonBlocking { /// Sets a [std::io::Read]er into a non blocking mode. fn set_non_blocking(&mut self) -> Result<()>; /// Sets a [std::io::Read]er back into a blocking mode. fn set_blocking(&mut self) -> Result<()>; } #[cfg(feature = "async")] /// IntoAsyncStream interface turns a [Process::Stream] into an async version. /// To be used with `async`/`await`syntax pub trait IntoAsyncStream { /// AsyncStream type. /// Like [Process::Stream] but it represents an async IO stream. type AsyncStream; /// Turns an object into a async stream. fn into_async_stream(self) -> Result; } expectrl-0.7.1/src/process/unix.rs000064400000000000000000000140061046102023000152560ustar 00000000000000//! This module contains a Unix implementation of [crate::process::Process]. use super::{Healthcheck, NonBlocking, Process}; use crate::error::to_io_error; use ptyprocess::{stream::Stream, PtyProcess}; #[cfg(feature = "async")] use super::IntoAsyncStream; #[cfg(feature = "async")] use futures_lite::{AsyncRead, AsyncWrite}; #[cfg(feature = "async")] use std::{ pin::Pin, task::{Context, Poll}, }; use std::{ io::{self, Read, Result, Write}, ops::{Deref, DerefMut}, os::unix::prelude::{AsRawFd, RawFd}, process::Command, }; /// A Unix representation of a [Process] via [PtyProcess] #[derive(Debug)] pub struct UnixProcess { proc: PtyProcess, } impl Process for UnixProcess { type Command = Command; type Stream = PtyStream; fn spawn>(cmd: S) -> Result { let args = tokenize_command(cmd.as_ref()); if args.is_empty() { return Err(io::Error::new( io::ErrorKind::Other, "Failed to parse a command", )); } let mut command = std::process::Command::new(&args[0]); let _ = command.args(args.iter().skip(1)); Self::spawn_command(command) } fn spawn_command(command: Self::Command) -> Result { let proc = PtyProcess::spawn(command).map_err(to_io_error("Failed to spawn a command"))?; Ok(Self { proc }) } fn open_stream(&mut self) -> Result { let stream = self .proc .get_pty_stream() .map_err(to_io_error("Failed to create a stream"))?; let stream = PtyStream::new(stream); Ok(stream) } } impl Healthcheck for UnixProcess { fn is_alive(&mut self) -> Result { self.proc .is_alive() .map_err(to_io_error("Failed to call pty.is_alive()")) } } impl Deref for UnixProcess { type Target = PtyProcess; fn deref(&self) -> &Self::Target { &self.proc } } impl DerefMut for UnixProcess { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.proc } } /// A IO stream (write/read) of [UnixProcess]. #[derive(Debug)] pub struct PtyStream { handle: Stream, } impl PtyStream { fn new(stream: Stream) -> Self { Self { handle: stream } } } impl Write for PtyStream { fn write(&mut self, buf: &[u8]) -> Result { self.handle.write(buf) } fn flush(&mut self) -> Result<()> { self.handle.flush() } fn write_vectored(&mut self, bufs: &[io::IoSlice<'_>]) -> Result { self.handle.write_vectored(bufs) } } impl Read for PtyStream { fn read(&mut self, buf: &mut [u8]) -> Result { self.handle.read(buf) } } impl NonBlocking for PtyStream { fn set_non_blocking(&mut self) -> Result<()> { let fd = self.handle.as_raw_fd(); make_non_blocking(fd, true) } fn set_blocking(&mut self) -> Result<()> { let fd = self.handle.as_raw_fd(); make_non_blocking(fd, false) } } impl AsRawFd for PtyStream { fn as_raw_fd(&self) -> RawFd { self.handle.as_raw_fd() } } #[cfg(feature = "async")] impl IntoAsyncStream for PtyStream { type AsyncStream = AsyncPtyStream; fn into_async_stream(self) -> Result { AsyncPtyStream::new(self) } } /// An async version of IO stream of [UnixProcess]. #[cfg(feature = "async")] #[derive(Debug)] pub struct AsyncPtyStream { stream: async_io::Async, } #[cfg(feature = "async")] impl AsyncPtyStream { fn new(stream: PtyStream) -> Result { let stream = async_io::Async::new(stream)?; Ok(Self { stream }) } } #[cfg(feature = "async")] impl AsyncWrite for AsyncPtyStream { fn poll_write( mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8], ) -> Poll> { Pin::new(&mut self.stream).poll_write(cx, buf) } fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::new(&mut self.stream).poll_flush(cx) } fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::new(&mut self.stream).poll_close(cx) } } #[cfg(feature = "async")] impl AsyncRead for AsyncPtyStream { fn poll_read( mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut [u8], ) -> Poll> { Pin::new(&mut self.stream).poll_read(cx, buf) } } #[cfg(feature = "polling")] impl polling::Source for PtyStream { fn raw(&self) -> RawFd { self.as_raw_fd() } } pub(crate) fn make_non_blocking(fd: RawFd, blocking: bool) -> Result<()> { use nix::fcntl::{fcntl, FcntlArg, OFlag}; let opt = fcntl(fd, FcntlArg::F_GETFL).map_err(nix_error_to_io)?; let mut opt = OFlag::from_bits_truncate(opt); opt.set(OFlag::O_NONBLOCK, blocking); let _ = fcntl(fd, FcntlArg::F_SETFL(opt)).map_err(nix_error_to_io)?; Ok(()) } fn nix_error_to_io(err: nix::Error) -> io::Error { io::Error::new(io::ErrorKind::Other, err) } /// Turn e.g. "prog arg1 arg2" into ["prog", "arg1", "arg2"] /// It takes care of single and double quotes but, /// /// It doesn't cover all edge cases. /// So it may not be compatible with real shell arguments parsing. fn tokenize_command(program: &str) -> Vec { let re = regex::Regex::new(r#""[^"]+"|'[^']+'|[^'" ]+"#).unwrap(); let mut res = vec![]; for cap in re.captures_iter(program) { res.push(cap[0].to_string()); } res } #[cfg(test)] mod tests { use super::*; #[cfg(unix)] #[test] fn test_tokenize_command() { let res = tokenize_command("prog arg1 arg2"); assert_eq!(vec!["prog", "arg1", "arg2"], res); let res = tokenize_command("prog -k=v"); assert_eq!(vec!["prog", "-k=v"], res); let res = tokenize_command("prog 'my text'"); assert_eq!(vec!["prog", "'my text'"], res); let res = tokenize_command(r#"prog "my text""#); assert_eq!(vec!["prog", r#""my text""#], res); } } expectrl-0.7.1/src/process/windows.rs000064400000000000000000000105551046102023000157720ustar 00000000000000//! This module contains a Windows implementation of [crate::process::Process]. use std::{ io::{self, Read, Result, Write}, ops::{Deref, DerefMut}, process::Command, }; use conpty::{ io::{PipeReader, PipeWriter}, spawn, Process, }; use super::{Healthcheck, NonBlocking, Process as ProcessTrait}; use crate::error::to_io_error; #[cfg(feature = "async")] use super::IntoAsyncStream; #[cfg(feature = "async")] use futures_lite::{AsyncRead, AsyncWrite}; #[cfg(feature = "async")] use std::{ pin::Pin, task::{Context, Poll}, }; /// A windows representation of a [Process] via [conpty::Process]. #[derive(Debug)] pub struct WinProcess { proc: Process, } impl ProcessTrait for WinProcess { type Command = Command; type Stream = ProcessStream; fn spawn>(cmd: S) -> Result { spawn(cmd.as_ref()) .map_err(to_io_error("")) .map(|proc| WinProcess { proc }) } fn spawn_command(command: Self::Command) -> Result { conpty::Process::spawn(command) .map_err(to_io_error("")) .map(|proc| WinProcess { proc }) } fn open_stream(&mut self) -> Result { let input = self.proc.input().map_err(to_io_error(""))?; let output = self.proc.output().map_err(to_io_error(""))?; Ok(Self::Stream::new(output, input)) } } impl Healthcheck for WinProcess { fn is_alive(&mut self) -> Result { Ok(self.proc.is_alive()) } } impl Deref for WinProcess { type Target = Process; fn deref(&self) -> &Self::Target { &self.proc } } impl DerefMut for WinProcess { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.proc } } /// An IO stream of [WinProcess]. #[derive(Debug)] pub struct ProcessStream { input: PipeWriter, output: PipeReader, } impl ProcessStream { fn new(output: PipeReader, input: PipeWriter) -> Self { Self { input, output } } /// Tries to clone the stream. pub fn try_clone(&self) -> std::result::Result { Ok(Self { input: self.input.try_clone()?, output: self.output.try_clone()?, }) } } impl Write for ProcessStream { fn write(&mut self, buf: &[u8]) -> Result { self.input.write(buf) } fn flush(&mut self) -> Result<()> { self.input.flush() } fn write_vectored(&mut self, bufs: &[io::IoSlice<'_>]) -> Result { self.input.write_vectored(bufs) } } impl Read for ProcessStream { fn read(&mut self, buf: &mut [u8]) -> Result { self.output.read(buf) } } impl NonBlocking for ProcessStream { fn set_non_blocking(&mut self) -> Result<()> { self.output.blocking(false); Ok(()) } fn set_blocking(&mut self) -> Result<()> { self.output.blocking(true); Ok(()) } } #[cfg(feature = "async")] impl IntoAsyncStream for ProcessStream { type AsyncStream = AsyncProcessStream; fn into_async_stream(self) -> Result { AsyncProcessStream::new(self) } } /// An async version of IO stream of [WinProcess]. #[cfg(feature = "async")] #[derive(Debug)] pub struct AsyncProcessStream { output: blocking::Unblock, input: blocking::Unblock, } #[cfg(feature = "async")] impl AsyncProcessStream { fn new(stream: ProcessStream) -> Result { let input = blocking::Unblock::new(stream.input); let output = blocking::Unblock::new(stream.output); Ok(Self { input, output }) } } #[cfg(feature = "async")] impl AsyncWrite for AsyncProcessStream { fn poll_write( mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8], ) -> Poll> { Pin::new(&mut self.input).poll_write(cx, buf) } fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::new(&mut self.input).poll_flush(cx) } fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::new(&mut self.input).poll_close(cx) } } #[cfg(feature = "async")] impl AsyncRead for AsyncProcessStream { fn poll_read( mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut [u8], ) -> Poll> { Pin::new(&mut self.output).poll_read(cx, buf) } } expectrl-0.7.1/src/repl.rs000064400000000000000000000257231046102023000135670ustar 00000000000000//! This module contains a list of special Sessions that can be spawned. use crate::{ error::Error, session::{OsProcess, OsProcessStream}, Captures, Session, }; use std::ops::{Deref, DerefMut}; #[cfg(not(feature = "async"))] use crate::process::NonBlocking; #[cfg(not(feature = "async"))] use std::io::{Read, Write}; #[cfg(unix)] use std::process::Command; use crate::spawn; #[cfg(feature = "async")] use futures_lite::{AsyncRead, AsyncWrite}; /// Spawn a bash session. /// /// It uses a custom prompt to be able to controll shell better. /// /// If you wan't to use [Session::interact] method it is better to use just Session. /// Because we don't handle echoes here (currently). Ideally we need to. #[cfg(unix)] #[cfg(not(feature = "async"))] pub fn spawn_bash() -> Result { const DEFAULT_PROMPT: &str = "EXPECT_PROMPT"; let mut cmd = Command::new("bash"); let _ = cmd.env("PS1", DEFAULT_PROMPT); // bind 'set enable-bracketed-paste off' turns off paste mode, // without it each command in bash starts and ends with an invisible sequence. // // We might need to turn it off optionally? let _ = cmd.env( "PROMPT_COMMAND", "PS1=EXPECT_PROMPT; unset PROMPT_COMMAND; bind 'set enable-bracketed-paste off'", ); let session = crate::session::Session::spawn(cmd)?; let mut bash = ReplSession::new( session, DEFAULT_PROMPT.to_string(), Some("quit".to_string()), false, ); // read a prompt to make it not available on next read. // // fix: somehow this line causes a different behaviour in iteract method. // the issue most likely that with this line in interact mode ENTER produces CTRL-M // when without the line it produces \r\n bash.expect_prompt()?; Ok(bash) } /// Spawn a bash session. /// /// It uses a custom prompt to be able to controll shell better. #[cfg(unix)] #[cfg(feature = "async")] pub async fn spawn_bash() -> Result { const DEFAULT_PROMPT: &str = "EXPECT_PROMPT"; let mut cmd = Command::new("bash"); let _ = cmd.env("PS1", DEFAULT_PROMPT); // bind 'set enable-bracketed-paste off' turns off paste mode, // without it each command in bash starts and ends with an invisible sequence. // // We might need to turn it off optionally? let _ = cmd.env( "PROMPT_COMMAND", "PS1=EXPECT_PROMPT; unset PROMPT_COMMAND; bind 'set enable-bracketed-paste off'", ); let session = crate::session::Session::spawn(cmd)?; let mut bash = ReplSession::new( session, DEFAULT_PROMPT.to_string(), Some("quit".to_string()), false, ); // read a prompt to make it not available on next read. bash.expect_prompt().await?; Ok(bash) } /// Spawn default python's IDLE. #[cfg(not(feature = "async"))] pub fn spawn_python() -> Result { // todo: check windows here // If we spawn it as ProcAttr::default().commandline("python") it will spawn processes endlessly.... let session = spawn("python")?; let mut idle = ReplSession::new(session, ">>> ".to_owned(), Some("quit()".to_owned()), false); idle.expect_prompt()?; Ok(idle) } /// Spawn default python's IDLE. #[cfg(feature = "async")] pub async fn spawn_python() -> Result { // todo: check windows here // If we spawn it as ProcAttr::default().commandline("python") it will spawn processes endlessly.... let session = spawn("python")?; let mut idle = ReplSession::new(session, ">>> ".to_owned(), Some("quit()".to_owned()), false); idle.expect_prompt().await?; Ok(idle) } /// Spawn a powershell session. /// /// It uses a custom prompt to be able to controll the shell. #[cfg(windows)] #[cfg(not(feature = "async"))] pub fn spawn_powershell() -> Result { const DEFAULT_PROMPT: &str = "EXPECTED_PROMPT>"; let session = spawn("pwsh -NoProfile -NonInteractive -NoLogo")?; let mut powershell = ReplSession::new( session, DEFAULT_PROMPT.to_owned(), Some("exit".to_owned()), true, ); // https://stackoverflow.com/questions/5725888/windows-powershell-changing-the-command-prompt let _ = powershell.execute(format!( r#"function prompt {{ "{}"; return " " }}"#, DEFAULT_PROMPT ))?; // https://stackoverflow.com/questions/69063656/is-it-possible-to-stop-powershell-wrapping-output-in-ansi-sequences/69063912#69063912 // https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_ansi_terminals?view=powershell-7.2#disabling-ansi-output let _ = powershell.execute(r#"[System.Environment]::SetEnvironmentVariable("TERM", "dumb")"#)?; let _ = powershell .execute(r#"[System.Environment]::SetEnvironmentVariable("TERM", "NO_COLOR")"#)?; Ok(powershell) } /// Spawn a powershell session. /// /// It uses a custom prompt to be able to controll the shell. #[cfg(windows)] #[cfg(feature = "async")] pub async fn spawn_powershell() -> Result { const DEFAULT_PROMPT: &str = "EXPECTED_PROMPT>"; let session = spawn("pwsh -NoProfile -NonInteractive -NoLogo")?; let mut powershell = ReplSession::new( session, DEFAULT_PROMPT.to_owned(), Some("exit".to_owned()), true, ); // https://stackoverflow.com/questions/5725888/windows-powershell-changing-the-command-prompt let _ = powershell .execute(format!( r#"function prompt {{ "{}"; return " " }}"#, DEFAULT_PROMPT )) .await?; // https://stackoverflow.com/questions/69063656/is-it-possible-to-stop-powershell-wrapping-output-in-ansi-sequences/69063912#69063912 // https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_ansi_terminals?view=powershell-7.2#disabling-ansi-output let _ = powershell .execute(r#"[System.Environment]::SetEnvironmentVariable("TERM", "dumb")"#) .await?; let _ = powershell .execute(r#"[System.Environment]::SetEnvironmentVariable("TERM", "NO_COLOR")"#) .await?; Ok(powershell) } /// A repl session: e.g. bash or the python shell: /// you have a prompt where a user inputs commands and the shell /// which executes them and manages IO streams. #[derive(Debug)] pub struct ReplSession

{ /// The prompt, used for `wait_for_prompt`, /// e.g. ">>> " for python. prompt: String, /// A pseudo-teletype session with a spawned process. session: Session, /// A command which will be called before termination. quit_command: Option, /// Flag to see if a echo is turned on. is_echo_on: bool, } impl ReplSession { /// Spawn function creates a repl session. /// /// The argument list is: /// - session; a spawned session which repl will wrap. /// - prompt; a string which will identify that the command was run. /// - quit_command; a command which will be called when [ReplSession] instance is dropped. /// - is_echo_on; determines whether the prompt check will be done twice. pub fn new( session: Session, prompt: String, quit_command: Option, is_echo: bool, ) -> Self { Self { session, prompt, quit_command, is_echo_on: is_echo, } } /// Get a used prompt. pub fn get_prompt(&self) -> &str { &self.prompt } /// Get a used quit command. pub fn get_quit_command(&self) -> Option<&str> { self.quit_command.as_deref() } /// Get a echo settings. pub fn is_echo(&self) -> bool { self.is_echo_on } /// Get an inner session. pub fn into_session(self) -> Session { self.session } } #[cfg(not(feature = "async"))] impl ReplSession { /// Block until prompt is found pub fn expect_prompt(&mut self) -> Result<(), Error> { let _ = self._expect_prompt()?; Ok(()) } fn _expect_prompt(&mut self) -> Result { self.session.expect(&self.prompt) } } #[cfg(feature = "async")] impl ReplSession { /// Block until prompt is found pub async fn expect_prompt(&mut self) -> Result<(), Error> { let _ = self._expect_prompt().await?; Ok(()) } async fn _expect_prompt(&mut self) -> Result { self.session.expect(&self.prompt).await } } #[cfg(not(feature = "async"))] impl ReplSession { /// Send a command to a repl and verifies that it exited. /// Returning it's output. pub fn execute + Clone>(&mut self, cmd: SS) -> Result, Error> { self.send_line(cmd)?; let found = self._expect_prompt()?; Ok(found.before().to_vec()) } /// Sends line to repl (and flush the output). /// /// If echo_on=true wait for the input to appear. #[cfg(not(feature = "async"))] pub fn send_line>(&mut self, line: Text) -> Result<(), Error> { let text = line.as_ref(); self.session.send_line(text)?; if self.is_echo_on { let _ = self.expect(line.as_ref())?; } Ok(()) } /// Send a quit command. /// /// In async version we it won't be send on Drop so, /// If you wan't it to be send you must do it yourself. pub fn exit(&mut self) -> Result<(), Error> { if let Some(quit_command) = &self.quit_command { self.session.send_line(quit_command)?; } Ok(()) } } #[cfg(feature = "async")] impl ReplSession { /// Send a command to a repl and verifies that it exited. pub async fn execute(&mut self, cmd: impl AsRef) -> Result, Error> { self.send_line(cmd).await?; let found = self._expect_prompt().await?; Ok(found.before().to_vec()) } /// Sends line to repl (and flush the output). /// /// If echo_on=true wait for the input to appear. pub async fn send_line(&mut self, line: impl AsRef) -> Result<(), Error> { self.session.send_line(line.as_ref()).await?; if self.is_echo_on { let _ = self.expect(line.as_ref()).await?; } Ok(()) } /// Send a quit command. /// /// In async version we it won't be send on Drop so, /// If you wan't it to be send you must do it yourself. pub async fn exit(&mut self) -> Result<(), Error> { if let Some(quit_command) = &self.quit_command { self.session.send_line(quit_command).await?; } Ok(()) } } impl Deref for ReplSession { type Target = Session; fn deref(&self) -> &Self::Target { &self.session } } impl DerefMut for ReplSession { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.session } } expectrl-0.7.1/src/session/async_session.rs000064400000000000000000000612171046102023000171660ustar 00000000000000//! Module contains an async version of Session structure. use std::{ io::{self, IoSliceMut}, ops::{Deref, DerefMut}, pin::Pin, task::{Context, Poll}, time::Duration, }; use futures_lite::{ ready, AsyncBufRead, AsyncBufReadExt, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, }; use crate::{process::Healthcheck, Captures, Error, Needle}; /// Session represents a spawned process and its streams. /// It controlls process and communication with it. #[derive(Debug)] pub struct Session

{ process: P, stream: Stream, } // GEt back to the solution where Logger is just dyn Write instead of all these magic with type system..... impl Session { /// Create a new session. pub fn new(process: P, stream: S) -> io::Result { Ok(Self { process, stream: Stream::new(stream), }) } /// Get a reference to original stream. pub fn get_stream(&self) -> &S { self.stream.as_ref() } /// Get a mut reference to original stream. pub fn get_stream_mut(&mut self) -> &mut S { self.stream.as_mut() } /// Get a reference to a process running program. pub fn get_process(&self) -> &P { &self.process } /// Get a mut reference to a process running program. pub fn get_process_mut(&mut self) -> &mut P { &mut self.process } /// Set the pty session's expect timeout. pub fn set_expect_timeout(&mut self, expect_timeout: Option) { self.stream.set_expect_timeout(expect_timeout); } /// Set a expect algorithm to be either gready or lazy. /// /// Default algorithm is gready. /// /// See [Session::expect]. pub fn set_expect_lazy(&mut self, is_lazy: bool) { self.stream.expect_lazy = is_lazy; } pub(crate) fn swap_stream R, R>( mut self, new_stream: F, ) -> Result, Error> { let buf = self.stream.get_available().to_owned(); let stream = self.stream.into_inner(); let stream = new_stream(stream); let mut session = Session::new(self.process, stream)?; session.stream.keep(&buf); Ok(session) } } impl Session { /// Verifies whether process is still alive. pub fn is_alive(&mut self) -> Result { self.process.is_alive().map_err(|err| err.into()) } } impl Session { /// Expect waits until a pattern is matched. /// /// If the method returns [Ok] it is guaranteed that at least 1 match was found. /// /// The match algorthm can be either /// - gready /// - lazy /// /// You can set one via [Session::set_expect_lazy]. /// Default version is gready. /// /// The implications are. /// /// Imagine you use [crate::Regex] `"\d+"` to find a match. /// And your process outputs `123`. /// In case of lazy approach we will match `1`. /// Where's in case of gready one we will match `123`. /// /// # Example /// #[cfg_attr(windows, doc = "```no_run")] #[cfg_attr(unix, doc = "```")] /// # futures_lite::future::block_on(async { /// let mut p = expectrl::spawn("echo 123").unwrap(); /// let m = p.expect(expectrl::Regex("\\d+")).await.unwrap(); /// assert_eq!(m.get(0).unwrap(), b"123"); /// # }); /// ``` /// #[cfg_attr(windows, doc = "```no_run")] #[cfg_attr(unix, doc = "```")] /// # futures_lite::future::block_on(async { /// let mut p = expectrl::spawn("echo 123").unwrap(); /// p.set_expect_lazy(true); /// let m = p.expect(expectrl::Regex("\\d+")).await.unwrap(); /// assert_eq!(m.get(0).unwrap(), b"1"); /// # }); /// ``` /// /// This behaviour is different from [Session::check]. /// /// It returns an error if timeout is reached. /// You can specify a timeout value by [Session::set_expect_timeout] method. pub async fn expect(&mut self, needle: N) -> Result { match self.stream.expect_lazy { true => self.stream.expect_lazy(needle).await, false => self.stream.expect_gready(needle).await, } } /// Check checks if a pattern is matched. /// Returns empty found structure if nothing found. /// /// Is a non blocking version of [Session::expect]. /// But its strategy of matching is different from it. /// It makes search agains all bytes available. /// #[cfg_attr(any(target_os = "macos", windows), doc = "```no_run")] #[cfg_attr(not(any(target_os = "macos", windows)), doc = "```")] /// # futures_lite::future::block_on(async { /// let mut p = expectrl::spawn("echo 123").unwrap(); /// // wait to guarantee that check will successed (most likely) /// std::thread::sleep(std::time::Duration::from_secs(1)); /// let m = p.check(expectrl::Regex("\\d+")).await.unwrap(); /// assert_eq!(m.get(0).unwrap(), b"123"); /// # }); /// ``` pub async fn check(&mut self, needle: E) -> Result { self.stream.check(needle).await } /// Is matched checks if a pattern is matched. /// It doesn't consumes bytes from stream. pub async fn is_matched(&mut self, needle: E) -> Result { self.stream.is_matched(needle).await } /// Verifyes if stream is empty or not. pub async fn is_empty(&mut self) -> io::Result { self.stream.is_empty().await } } impl Session { /// Send text to child’s STDIN. /// /// You can also use methods from [std::io::Write] instead. /// /// # Example /// /// ``` /// use expectrl::{spawn, ControlCode}; /// /// let mut proc = spawn("cat").unwrap(); /// /// # futures_lite::future::block_on(async { /// proc.send("Hello"); /// proc.send(b"World"); /// proc.send(ControlCode::try_from("^C").unwrap()); /// # }); /// ``` pub async fn send>(&mut self, buf: B) -> io::Result<()> { self.stream.write_all(buf.as_ref()).await } /// Send a line to child’s STDIN. /// /// # Example /// /// ``` /// use expectrl::{spawn, ControlCode}; /// /// let mut proc = spawn("cat").unwrap(); /// /// # futures_lite::future::block_on(async { /// proc.send_line("Hello"); /// proc.send_line(b"World"); /// proc.send_line(ControlCode::try_from("^C").unwrap()); /// # }); /// ``` pub async fn send_line>(&mut self, buf: B) -> io::Result<()> { #[cfg(windows)] const LINE_ENDING: &[u8] = b"\r\n"; #[cfg(not(windows))] const LINE_ENDING: &[u8] = b"\n"; self.stream.write_all(buf.as_ref()).await?; self.stream.write_all(LINE_ENDING).await?; Ok(()) } } impl Deref for Session { type Target = P; fn deref(&self) -> &Self::Target { &self.process } } impl DerefMut for Session { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.process } } impl AsyncWrite for Session { fn poll_write( self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8], ) -> Poll> { Pin::new(&mut self.get_mut().stream).poll_write(cx, buf) } fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::new(&mut self.stream).poll_flush(cx) } fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::new(&mut self.stream).poll_close(cx) } fn poll_write_vectored( mut self: Pin<&mut Self>, cx: &mut Context<'_>, bufs: &[io::IoSlice<'_>], ) -> Poll> { Pin::new(&mut self.stream).poll_write_vectored(cx, bufs) } } impl AsyncRead for Session { fn poll_read( mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut [u8], ) -> Poll> { Pin::new(&mut self.stream).poll_read(cx, buf) } } impl AsyncBufRead for Session { fn poll_fill_buf(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::new(&mut self.get_mut().stream).poll_fill_buf(cx) } fn consume(mut self: Pin<&mut Self>, amt: usize) { Pin::new(&mut self.stream).consume(amt); } } /// Session represents a spawned process and its streams. /// It controlls process and communication with it. #[derive(Debug)] struct Stream { stream: BufferedStream, expect_timeout: Option, expect_lazy: bool, } impl Stream { /// Creates an async IO stream. fn new(stream: S) -> Self { Self { stream: BufferedStream::new(stream), expect_timeout: Some(Duration::from_millis(10000)), expect_lazy: false, } } /// Returns a reference to original stream. fn as_ref(&self) -> &S { &self.stream.stream } /// Returns a mut reference to original stream. fn as_mut(&mut self) -> &mut S { &mut self.stream.stream } /// Set the pty session's expect timeout. fn set_expect_timeout(&mut self, expect_timeout: Option) { self.expect_timeout = expect_timeout; } /// Save a bytes in inner buffer. /// They'll be pushed to the end of the buffer. fn keep(&mut self, buf: &[u8]) { self.stream.keep(buf); } /// Get an inner buffer. fn get_available(&mut self) -> &[u8] { self.stream.buffer() } /// Returns an inner IO stream. fn into_inner(self) -> S { self.stream.stream } } impl Stream { async fn expect_gready(&mut self, needle: N) -> Result { let expect_timeout = self.expect_timeout; let expect_future = async { let mut eof = false; loop { let data = self.stream.buffer(); let found = Needle::check(&needle, data, eof)?; if !found.is_empty() { let end_index = Captures::right_most_index(&found); let involved_bytes = data[..end_index].to_vec(); self.stream.consume(end_index); return Ok(Captures::new(involved_bytes, found)); } if eof { return Err(Error::Eof); } eof = self.stream.fill().await? == 0; } }; if let Some(timeout) = expect_timeout { let timeout_future = futures_timer::Delay::new(timeout); futures_lite::future::or(expect_future, async { timeout_future.await; Err(Error::ExpectTimeout) }) .await } else { expect_future.await } } async fn expect_lazy(&mut self, needle: N) -> Result { let expect_timeout = self.expect_timeout; let expect_future = async { // We read by byte to make things as lazy as possible. // // It's chose is important in using Regex as a Needle. // Imagine we have a `\d+` regex. // Using such buffer will match string `2` imidiately eventhough right after might be other digit. // // The second reason is // if we wouldn't read by byte EOF indication could be lost. // And next blocking std::io::Read operation could be blocked forever. // // We could read all data available via `read_available` to reduce IO operations, // but in such case we would need to keep a EOF indicator internally in stream, // which is OK if EOF happens onces, but I am not sure if this is a case. let mut checked_length = 0; let mut eof = false; loop { let available = self.stream.buffer(); let is_buffer_checked = checked_length == available.len(); if is_buffer_checked { let n = self.stream.fill().await?; eof = n == 0; } // We intentinally not increase the counter // and run check one more time even though the data isn't changed. // Because it may be important for custom implementations of Needle. let available = self.stream.buffer(); if checked_length < available.len() { checked_length += 1; } let data = &available[..checked_length]; let found = Needle::check(&needle, data, eof)?; if !found.is_empty() { let end_index = Captures::right_most_index(&found); let involved_bytes = data[..end_index].to_vec(); self.stream.consume(end_index); return Ok(Captures::new(involved_bytes, found)); } if eof { return Err(Error::Eof); } } }; if let Some(timeout) = expect_timeout { let timeout_future = futures_timer::Delay::new(timeout); futures_lite::future::or(expect_future, async { timeout_future.await; Err(Error::ExpectTimeout) }) .await } else { expect_future.await } } /// Is matched checks if a pattern is matched. /// It doesn't consumes bytes from stream. async fn is_matched(&mut self, needle: E) -> Result { let eof = self.try_fill().await?; let buf = self.stream.buffer(); let found = needle.check(buf, eof)?; if !found.is_empty() { return Ok(true); } if eof { return Err(Error::Eof); } Ok(false) } /// Check checks if a pattern is matched. /// Returns empty found structure if nothing found. async fn check(&mut self, needle: E) -> Result { let eof = self.try_fill().await?; let buf = self.stream.buffer(); let found = needle.check(buf, eof)?; if !found.is_empty() { let end_index = Captures::right_most_index(&found); let involved_bytes = buf[..end_index].to_vec(); self.stream.consume(end_index); return Ok(Captures::new(involved_bytes, found)); } if eof { return Err(Error::Eof); } Ok(Captures::new(Vec::new(), Vec::new())) } /// Verifyes if stream is empty or not. async fn is_empty(&mut self) -> io::Result { match futures_lite::future::poll_once(self.read(&mut [])).await { Some(Ok(0)) => Ok(true), Some(Ok(_)) => Ok(false), Some(Err(err)) => Err(err), None => Ok(true), } } async fn try_fill(&mut self) -> Result { match futures_lite::future::poll_once(self.stream.fill()).await { Some(Ok(n)) => Ok(n == 0), Some(Err(err)) => Err(err.into()), None => Ok(false), } } } impl AsyncWrite for Stream { fn poll_write( mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8], ) -> Poll> { Pin::new(&mut *self.stream.get_mut()).poll_write(cx, buf) } fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::new(&mut *self.stream.get_mut()).poll_flush(cx) } fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::new(&mut *self.stream.get_mut()).poll_close(cx) } fn poll_write_vectored( mut self: Pin<&mut Self>, cx: &mut Context<'_>, bufs: &[io::IoSlice<'_>], ) -> Poll> { Pin::new(&mut *self.stream.get_mut()).poll_write_vectored(cx, bufs) } } impl AsyncRead for Stream { fn poll_read( mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut [u8], ) -> Poll> { Pin::new(&mut self.stream).poll_read(cx, buf) } } impl AsyncBufRead for Stream { fn poll_fill_buf(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::new(&mut self.get_mut().stream).poll_fill_buf(cx) } fn consume(mut self: Pin<&mut Self>, amt: usize) { Pin::new(&mut self.stream).consume(amt); } } /// Session represents a spawned process and its streams. /// It controlls process and communication with it. #[derive(Debug)] struct BufferedStream { stream: S, buffer: Vec, length: usize, } impl BufferedStream { fn new(stream: S) -> Self { Self { stream, buffer: Vec::new(), length: 0, } } fn keep(&mut self, buf: &[u8]) { self.buffer.extend(buf); self.length += buf.len(); } fn buffer(&self) -> &[u8] { &self.buffer[..self.length] } fn get_mut(&mut self) -> &mut S { &mut self.stream } } impl BufferedStream { async fn fill(&mut self) -> io::Result { let mut buf = [0; 128]; let n = self.stream.read(&mut buf).await?; self.keep(&buf[..n]); Ok(n) } } impl AsyncRead for BufferedStream { fn poll_read( mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut [u8], ) -> Poll> { let mut rem = ready!(self.as_mut().poll_fill_buf(cx))?; let nread = std::io::Read::read(&mut rem, buf)?; self.consume(nread); Poll::Ready(Ok(nread)) } fn poll_read_vectored( mut self: Pin<&mut Self>, cx: &mut Context<'_>, bufs: &mut [IoSliceMut<'_>], ) -> Poll> { let mut rem = ready!(self.as_mut().poll_fill_buf(cx))?; let nread = std::io::Read::read_vectored(&mut rem, bufs)?; self.consume(nread); Poll::Ready(Ok(nread)) } } impl AsyncBufRead for BufferedStream { fn poll_fill_buf(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { if self.buffer.is_empty() { let mut buf = [0; 128]; let n = ready!(Pin::new(&mut self.stream).poll_read(cx, &mut buf))?; self.keep(&buf[..n]); } let buf = self.get_mut().buffer(); Poll::Ready(Ok(buf)) } fn consume(mut self: Pin<&mut Self>, amt: usize) { let _ = self.buffer.drain(..amt); self.length -= amt; } } #[cfg(test)] mod tests { use futures_lite::AsyncWriteExt; use crate::Eof; use super::*; #[test] fn test_expect_lazy() { let buf = b"Hello World".to_vec(); let cursor = futures_lite::io::Cursor::new(buf); let mut stream = Stream::new(cursor); futures_lite::future::block_on(async { let found = stream.expect_lazy("World").await.unwrap(); assert_eq!(b"Hello ", found.before()); assert_eq!(vec![b"World"], found.matches().collect::>()); }); } #[test] fn test_expect_lazy_eof() { let buf = b"Hello World".to_vec(); let cursor = futures_lite::io::Cursor::new(buf); let mut stream = Stream::new(cursor); futures_lite::future::block_on(async { let found = stream.expect_lazy(Eof).await.unwrap(); assert_eq!(b"", found.before()); assert_eq!(vec![b"Hello World"], found.matches().collect::>()); }); let cursor = futures_lite::io::Cursor::new(Vec::new()); let mut stream = Stream::new(cursor); futures_lite::future::block_on(async { let err = stream.expect_lazy("").await.unwrap_err(); assert!(matches!(err, Error::Eof)); }); } #[test] fn test_expect_lazy_timeout() { futures_lite::future::block_on(async { let mut stream = Stream::new(NoEofReader::default()); stream.set_expect_timeout(Some(Duration::from_millis(100))); stream.write_all(b"Hello").await.unwrap(); let err = stream.expect_lazy("Hello World").await.unwrap_err(); assert!(matches!(err, Error::ExpectTimeout)); stream.write_all(b" World").await.unwrap(); let found = stream.expect_lazy("World").await.unwrap(); assert_eq!(b"Hello ", found.before()); assert_eq!(vec![b"World"], found.matches().collect::>()); }); } #[test] fn test_expect_gready() { let buf = b"Hello World".to_vec(); let cursor = futures_lite::io::Cursor::new(buf); let mut stream = Stream::new(cursor); futures_lite::future::block_on(async { let found = stream.expect_gready("World").await.unwrap(); assert_eq!(b"Hello ", found.before()); assert_eq!(vec![b"World"], found.matches().collect::>()); }); } #[test] fn test_expect_gready_eof() { let buf = b"Hello World".to_vec(); let cursor = futures_lite::io::Cursor::new(buf); let mut stream = Stream::new(cursor); futures_lite::future::block_on(async { let found = stream.expect_gready(Eof).await.unwrap(); assert_eq!(b"", found.before()); assert_eq!(vec![b"Hello World"], found.matches().collect::>()); }); let cursor = futures_lite::io::Cursor::new(Vec::new()); let mut stream = Stream::new(cursor); futures_lite::future::block_on(async { let err = stream.expect_gready("").await.unwrap_err(); assert!(matches!(err, Error::Eof)); }); } #[test] fn test_expect_gready_timeout() { futures_lite::future::block_on(async { let mut stream = Stream::new(NoEofReader::default()); stream.set_expect_timeout(Some(Duration::from_millis(100))); stream.write_all(b"Hello").await.unwrap(); let err = stream.expect_gready("Hello World").await.unwrap_err(); assert!(matches!(err, Error::ExpectTimeout)); stream.write_all(b" World").await.unwrap(); let found = stream.expect_gready("World").await.unwrap(); assert_eq!(b"Hello ", found.before()); assert_eq!(vec![b"World"], found.matches().collect::>()); }); } #[test] fn test_check() { let buf = b"Hello World".to_vec(); let cursor = futures_lite::io::Cursor::new(buf); let mut stream = Stream::new(cursor); futures_lite::future::block_on(async { let found = stream.check("World").await.unwrap(); assert_eq!(b"Hello ", found.before()); assert_eq!(vec![b"World"], found.matches().collect::>()); }); } #[test] fn test_is_matched() { let mut stream = Stream::new(NoEofReader::default()); futures_lite::future::block_on(async { stream.write_all(b"Hello World").await.unwrap(); assert!(stream.is_matched("World").await.unwrap()); assert!(!stream.is_matched("*****").await.unwrap()); let found = stream.check("World").await.unwrap(); assert_eq!(b"Hello ", found.before()); assert_eq!(vec![b"World"], found.matches().collect::>()); }); } #[derive(Debug, Default)] struct NoEofReader { data: Vec, } impl AsyncWrite for NoEofReader { fn poll_write( mut self: Pin<&mut Self>, _: &mut Context<'_>, buf: &[u8], ) -> Poll> { self.data.extend(buf); Poll::Ready(Ok(buf.len())) } fn poll_flush(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } fn poll_close(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } } impl AsyncRead for NoEofReader { fn poll_read( mut self: Pin<&mut Self>, _: &mut Context<'_>, mut buf: &mut [u8], ) -> Poll> { if self.data.is_empty() { return Poll::Pending; } let n = std::io::Write::write(&mut buf, &self.data)?; let _ = self.data.drain(..n); Poll::Ready(Ok(n)) } } } expectrl-0.7.1/src/session/mod.rs000064400000000000000000000135261046102023000150650ustar 00000000000000//! This module contains a system independent [Session] representation. //! //! But it does set a default [Session] processes and stream in order to be able to use Session without generics. //! It also sets a list of other methods which are available for a platform processes. //! //! # Example //! //! ```no_run,ignore //! use std::{process::Command, io::prelude::*}; //! use expectrl::Session; //! //! let mut p = Session::spawn(Command::new("cat")).unwrap(); //! writeln!(p, "Hello World").unwrap(); //! let mut line = String::new(); //! p.read_line(&mut line).unwrap(); //! ``` #[cfg(feature = "async")] mod async_session; #[cfg(not(feature = "async"))] mod sync_session; use std::{io::Write, process::Command}; use crate::{interact::InteractSession, process::Process, stream::log::LogStream, Error}; #[cfg(not(feature = "async"))] use std::io::Read; #[cfg(feature = "async")] use crate::process::IntoAsyncStream; #[cfg(unix)] type OsProc = crate::process::unix::UnixProcess; #[cfg(windows)] type OsProc = crate::process::windows::WinProcess; #[cfg(all(unix, not(feature = "async")))] type OsProcStream = crate::process::unix::PtyStream; #[cfg(all(unix, feature = "async"))] type OsProcStream = crate::process::unix::AsyncPtyStream; #[cfg(all(windows, not(feature = "async")))] type OsProcStream = crate::process::windows::ProcessStream; #[cfg(all(windows, feature = "async"))] type OsProcStream = crate::process::windows::AsyncProcessStream; /// A type alias for OS process which can run a [`Session`] and a default one. pub type OsProcess = OsProc; /// A type alias for OS process stream which is a default one for [`Session`]. pub type OsProcessStream = OsProcStream; #[cfg(feature = "async")] pub use async_session::Session; #[cfg(not(feature = "async"))] pub use sync_session::Session; impl Session { /// Spawns a session on a platform process. /// /// # Example /// /// ```no_run /// use std::process::Command; /// use expectrl::Session; /// /// let p = Session::spawn(Command::new("cat")); /// ``` pub fn spawn(command: Command) -> Result { let mut process = OsProcess::spawn_command(command)?; let stream = process.open_stream()?; #[cfg(feature = "async")] let stream = stream.into_async_stream()?; let session = Self::new(process, stream)?; Ok(session) } /// Spawns a session on a platform process. /// Using a string commandline. pub(crate) fn spawn_cmd(cmd: &str) -> Result { let mut process = OsProcess::spawn(cmd)?; let stream = process.open_stream()?; #[cfg(feature = "async")] let stream = stream.into_async_stream()?; let session = Self::new(process, stream)?; Ok(session) } } impl Session { /// Interact gives control of the child process to the interactive user (the /// human at the keyboard or a [`Read`]er implementator). /// /// You can set different callbacks to the session, see [`InteractSession`]. /// /// Keystrokes are sent to the child process, and /// the `stdout` and `stderr` output of the child process is printed. /// /// When the user types the `escape_character` this method will return control to a running process. /// The escape_character will not be transmitted. /// The default for escape_character is entered as `Ctrl-]`, the very same as BSD telnet. /// /// This simply echos the child `stdout` and `stderr` to the real `stdout` and /// it echos the real `stdin` to the child `stdin`. /// /// BEWARE that interact finishes after a process stops. /// So after the return you may not obtain a correct status of a process. /// /// In not `async` mode the default version uses a buzy loop. /// /// - On `linux` you can use a `polling` version using the corresponding feature. /// - On `windows` the feature is also present but it spawns a thread for pooling which creates a set of obsticales. /// Specifically if you're planning to call `interact()` multiple times it may not be safe. Because the previous threads may still be running. /// /// It works via polling in `async` mode on both `unix` and `windows`. /// /// # Example /// #[cfg_attr( all(unix, not(feature = "async"), not(feature = "polling")), doc = "```no_run" )] #[cfg_attr( not(all(unix, not(feature = "async"), not(feature = "polling"))), doc = "```ignore" )] /// use std::io::{stdout, Cursor}; /// use expectrl::{self, interact::InteractOptions}; /// /// let mut p = expectrl::spawn("cat").unwrap(); /// /// let input = Cursor::new(String::from("Some text right here")); /// /// p.interact(input, stdout()).spawn(InteractOptions::default()).unwrap(); /// ``` /// /// [`Read`]: std::io::Read pub fn interact(&mut self, input: I, output: O) -> InteractSession<&mut Self, I, O> { InteractSession::new(self, input, output) } } /// Set a logger which will write each Read/Write operation into the writter. /// /// # Example /// /// ``` /// use expectrl::{spawn, session::log}; /// /// let p = spawn("cat").unwrap(); /// let p = log(p, std::io::stdout()); /// ``` #[cfg(not(feature = "async"))] pub fn log(session: Session, dst: W) -> Result>, Error> where W: Write, S: Read, { session.swap_stream(|s| LogStream::new(s, dst)) } /// Set a logger which will write each Read/Write operation into the writter. /// /// # Example /// /// ``` /// use expectrl::{spawn, session::log}; /// /// let p = spawn("cat").unwrap(); /// let p = log(p, std::io::stdout()); /// ``` #[cfg(feature = "async")] pub fn log(session: Session, dst: W) -> Result>, Error> where W: Write, { session.swap_stream(|s| LogStream::new(s, dst)) } expectrl-0.7.1/src/session/sync_session.rs000064400000000000000000000450101046102023000170160ustar 00000000000000//! Module contains a Session structure. use std::{ io::{self, BufRead, BufReader, Read, Write}, time::{self, Duration}, }; use crate::{ error::Error, needle::Needle, process::{Healthcheck, NonBlocking}, Captures, }; /// Session represents a spawned process and its streams. /// It controlls process and communication with it. #[derive(Debug)] pub struct Session

{ proc: P, stream: TryStream, expect_timeout: Option, expect_lazy: bool, } impl Session where S: Read, { /// Creates a new session. pub fn new(process: P, stream: S) -> io::Result { let stream = TryStream::new(stream)?; Ok(Self { proc: process, stream, expect_timeout: Some(Duration::from_millis(10000)), expect_lazy: false, }) } pub(crate) fn swap_stream(mut self, new_stream: F) -> Result, Error> where F: FnOnce(S) -> R, R: Read, { self.stream.flush_in_buffer(); let buf = self.stream.get_available().to_owned(); let stream = self.stream.into_inner(); let new_stream = new_stream(stream); let mut session = Session::new(self.proc, new_stream)?; session.stream.keep_in_buffer(&buf); Ok(session) } } impl Session { /// Set the pty session's expect timeout. pub fn set_expect_timeout(&mut self, expect_timeout: Option) { self.expect_timeout = expect_timeout; } /// Set a expect algorithm to be either gready or lazy. /// /// Default algorithm is gready. /// /// See [Session::expect]. pub fn set_expect_lazy(&mut self, lazy: bool) { self.expect_lazy = lazy; } /// Get a reference to original stream. pub fn get_stream(&self) -> &S { self.stream.as_ref() } /// Get a mut reference to original stream. pub fn get_stream_mut(&mut self) -> &mut S { self.stream.as_mut() } /// Get a reference to a process running program. pub fn get_process(&self) -> &P { &self.proc } /// Get a mut reference to a process running program. pub fn get_process_mut(&mut self) -> &mut P { &mut self.proc } } impl Session { /// Verifies whether process is still alive. pub fn is_alive(&mut self) -> Result { self.proc.is_alive().map_err(|err| err.into()) } } impl Session { /// Expect waits until a pattern is matched. /// /// If the method returns [Ok] it is guaranteed that at least 1 match was found. /// /// The match algorthm can be either /// - gready /// - lazy /// /// You can set one via [Session::set_expect_lazy]. /// Default version is gready. /// /// The implications are. /// Imagine you use [crate::Regex] `"\d+"` to find a match. /// And your process outputs `123`. /// In case of lazy approach we will match `1`. /// Where's in case of gready one we will match `123`. /// /// # Example /// #[cfg_attr(windows, doc = "```no_run")] #[cfg_attr(unix, doc = "```")] /// let mut p = expectrl::spawn("echo 123").unwrap(); /// let m = p.expect(expectrl::Regex("\\d+")).unwrap(); /// assert_eq!(m.get(0).unwrap(), b"123"); /// ``` /// #[cfg_attr(windows, doc = "```no_run")] #[cfg_attr(unix, doc = "```")] /// let mut p = expectrl::spawn("echo 123").unwrap(); /// p.set_expect_lazy(true); /// let m = p.expect(expectrl::Regex("\\d+")).unwrap(); /// assert_eq!(m.get(0).unwrap(), b"1"); /// ``` /// /// This behaviour is different from [Session::check]. /// /// It returns an error if timeout is reached. /// You can specify a timeout value by [Session::set_expect_timeout] method. pub fn expect(&mut self, needle: N) -> Result where N: Needle, { match self.expect_lazy { true => self.expect_lazy(needle), false => self.expect_gready(needle), } } /// Expect which fills as much as possible to the buffer. /// /// See [Session::expect]. fn expect_gready(&mut self, needle: N) -> Result where N: Needle, { let start = time::Instant::now(); loop { let eof = self.stream.read_available()?; let data = self.stream.get_available(); let found = needle.check(data, eof)?; if !found.is_empty() { let end_index = Captures::right_most_index(&found); let involved_bytes = data[..end_index].to_vec(); self.stream.consume_available(end_index); return Ok(Captures::new(involved_bytes, found)); } if eof { return Err(Error::Eof); } if let Some(timeout) = self.expect_timeout { if start.elapsed() > timeout { return Err(Error::ExpectTimeout); } } } } /// Expect which reads byte by byte. /// /// See [Session::expect]. fn expect_lazy(&mut self, needle: N) -> Result where N: Needle, { let mut checking_data_length = 0; let mut eof = false; let start = time::Instant::now(); loop { let mut available = self.stream.get_available(); if checking_data_length == available.len() { // We read by byte to make things as lazy as possible. // // It's chose is important in using Regex as a Needle. // Imagine we have a `\d+` regex. // Using such buffer will match string `2` imidiately eventhough right after might be other digit. // // The second reason is // if we wouldn't read by byte EOF indication could be lost. // And next blocking std::io::Read operation could be blocked forever. // // We could read all data available via `read_available` to reduce IO operations, // but in such case we would need to keep a EOF indicator internally in stream, // which is OK if EOF happens onces, but I am not sure if this is a case. eof = self.stream.read_available_once(&mut [0; 1])? == Some(0); available = self.stream.get_available(); } // We intentinally not increase the counter // and run check one more time even though the data isn't changed. // Because it may be important for custom implementations of Needle. if checking_data_length < available.len() { checking_data_length += 1; } let data = &available[..checking_data_length]; let found = needle.check(data, eof)?; if !found.is_empty() { let end_index = Captures::right_most_index(&found); let involved_bytes = data[..end_index].to_vec(); self.stream.consume_available(end_index); return Ok(Captures::new(involved_bytes, found)); } if eof { return Err(Error::Eof); } if let Some(timeout) = self.expect_timeout { if start.elapsed() > timeout { return Err(Error::ExpectTimeout); } } } } /// Check verifies if a pattern is matched. /// Returns empty found structure if nothing found. /// /// Is a non blocking version of [Session::expect]. /// But its strategy of matching is different from it. /// It makes search against all bytes available. /// /// # Example /// #[cfg_attr(any(windows, target_os = "macos"), doc = "```no_run")] #[cfg_attr(not(any(target_os = "macos", windows)), doc = "```")] /// use expectrl::{spawn, Regex}; /// use std::time::Duration; /// /// let mut p = spawn("echo 123").unwrap(); /// # /// # // wait to guarantee that check echo worked out (most likely) /// # std::thread::sleep(Duration::from_millis(500)); /// # /// let m = p.check(Regex("\\d+")).unwrap(); /// assert_eq!(m.get(0).unwrap(), b"123"); /// ``` pub fn check(&mut self, needle: N) -> Result where N: Needle, { let eof = self.stream.read_available()?; let buf = self.stream.get_available(); let found = needle.check(buf, eof)?; if !found.is_empty() { let end_index = Captures::right_most_index(&found); let involved_bytes = buf[..end_index].to_vec(); self.stream.consume_available(end_index); return Ok(Captures::new(involved_bytes, found)); } if eof { return Err(Error::Eof); } Ok(Captures::new(Vec::new(), Vec::new())) } /// The functions checks if a pattern is matched. /// It doesn’t consumes bytes from stream. /// /// Its strategy of matching is different from the one in [Session::expect]. /// It makes search agains all bytes available. /// /// If you want to get a matched result [Session::check] and [Session::expect] is a better option. /// Because it is not guaranteed that [Session::check] or [Session::expect] with the same parameters: /// - will successed even right after Session::is_matched call. /// - will operate on the same bytes. /// /// IMPORTANT: /// /// If you call this method with [crate::Eof] pattern be aware that eof /// indication MAY be lost on the next interactions. /// It depends from a process you spawn. /// So it might be better to use [Session::check] or [Session::expect] with Eof. /// /// # Example /// #[cfg_attr(windows, doc = "```no_run")] #[cfg_attr(unix, doc = "```")] /// use expectrl::{spawn, Regex}; /// use std::time::Duration; /// /// let mut p = spawn("cat").unwrap(); /// p.send_line("123"); /// # // wait to guarantee that check echo worked out (most likely) /// # std::thread::sleep(Duration::from_secs(1)); /// let m = p.is_matched(Regex("\\d+")).unwrap(); /// assert_eq!(m, true); /// ``` pub fn is_matched(&mut self, needle: N) -> Result where N: Needle, { let eof = self.stream.read_available()?; let buf = self.stream.get_available(); let found = needle.check(buf, eof)?; if !found.is_empty() { return Ok(true); } if eof { return Err(Error::Eof); } Ok(false) } } impl Session { /// Send text to child’s STDIN. /// /// You can also use methods from [std::io::Write] instead. /// /// # Example /// /// ``` /// use expectrl::{spawn, ControlCode}; /// /// let mut proc = spawn("cat").unwrap(); /// /// proc.send("Hello"); /// proc.send(b"World"); /// proc.send(ControlCode::try_from("^C").unwrap()); /// ``` pub fn send>(&mut self, buf: B) -> io::Result<()> { self.stream.write_all(buf.as_ref()) } /// Send a line to child’s STDIN. /// /// # Example /// /// ``` /// use expectrl::{spawn, ControlCode}; /// /// let mut proc = spawn("cat").unwrap(); /// /// proc.send_line("Hello"); /// proc.send_line(b"World"); /// proc.send_line(ControlCode::try_from("^C").unwrap()); /// ``` pub fn send_line>(&mut self, buf: B) -> io::Result<()> { #[cfg(windows)] const LINE_ENDING: &[u8] = b"\r\n"; #[cfg(not(windows))] const LINE_ENDING: &[u8] = b"\n"; self.stream.write_all(buf.as_ref())?; self.write_all(LINE_ENDING)?; Ok(()) } } impl Session { /// Try to read in a non-blocking mode. /// /// Returns `[std::io::ErrorKind::WouldBlock]` /// in case if there's nothing to read. pub fn try_read(&mut self, buf: &mut [u8]) -> io::Result { self.stream.try_read(buf) } /// Verifyes if stream is empty or not. pub fn is_empty(&mut self) -> io::Result { self.stream.is_empty() } } impl Write for Session { fn write(&mut self, buf: &[u8]) -> std::io::Result { self.stream.write(buf) } fn flush(&mut self) -> std::io::Result<()> { self.stream.flush() } fn write_vectored(&mut self, bufs: &[io::IoSlice<'_>]) -> io::Result { self.stream.write_vectored(bufs) } } impl Read for Session { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { self.stream.read(buf) } } impl BufRead for Session { fn fill_buf(&mut self) -> std::io::Result<&[u8]> { self.stream.fill_buf() } fn consume(&mut self, amt: usize) { self.stream.consume(amt) } } #[derive(Debug)] struct TryStream { stream: ControlledReader, } impl TryStream { fn into_inner(self) -> S { self.stream.inner.into_inner().inner } fn as_ref(&self) -> &S { &self.stream.inner.get_ref().inner } fn as_mut(&mut self) -> &mut S { &mut self.stream.inner.get_mut().inner } } impl TryStream { /// The function returns a new Stream from a file. fn new(stream: S) -> io::Result { Ok(Self { stream: ControlledReader::new(stream), }) } fn flush_in_buffer(&mut self) { self.stream.flush_in_buffer(); } } impl TryStream { fn keep_in_buffer(&mut self, v: &[u8]) { self.stream.keep_in_buffer(v); } fn get_available(&mut self) -> &[u8] { self.stream.get_available() } fn consume_available(&mut self, n: usize) { self.stream.consume_available(n) } } impl TryStream { /// Try to read in a non-blocking mode. /// /// It raises io::ErrorKind::WouldBlock if there's nothing to read. fn try_read(&mut self, buf: &mut [u8]) -> io::Result { self.stream.get_mut().set_non_blocking()?; let result = self.stream.inner.read(buf); // As file is DUPed changes in one descriptor affects all ones // so we need to make blocking file after we finished. self.stream.get_mut().set_blocking()?; result } #[allow(clippy::wrong_self_convention)] fn is_empty(&mut self) -> io::Result { match self.try_read(&mut []) { Ok(0) => Ok(true), Ok(_) => Ok(false), Err(err) if err.kind() == io::ErrorKind::WouldBlock => Ok(true), Err(err) => Err(err), } } fn read_available(&mut self) -> std::io::Result { self.stream.flush_in_buffer(); let mut buf = [0; 248]; loop { match self.try_read_inner(&mut buf) { Ok(0) => break Ok(true), Ok(n) => { self.stream.keep_in_buffer(&buf[..n]); } Err(err) if err.kind() == io::ErrorKind::WouldBlock => break Ok(false), Err(err) => break Err(err), } } } fn read_available_once(&mut self, buf: &mut [u8]) -> std::io::Result> { self.stream.flush_in_buffer(); match self.try_read_inner(buf) { Ok(0) => Ok(Some(0)), Ok(n) => { self.stream.keep_in_buffer(&buf[..n]); Ok(Some(n)) } Err(err) if err.kind() == io::ErrorKind::WouldBlock => Ok(None), Err(err) => Err(err), } } // non-buffered && non-blocking read fn try_read_inner(&mut self, buf: &mut [u8]) -> io::Result { self.stream.get_mut().set_non_blocking()?; let result = self.stream.get_mut().read(buf); // As file is DUPed changes in one descriptor affects all ones // so we need to make blocking file after we finished. self.stream.get_mut().set_blocking()?; result } } impl Write for TryStream { fn write(&mut self, buf: &[u8]) -> io::Result { self.stream.inner.get_mut().inner.write(buf) } fn flush(&mut self) -> io::Result<()> { self.stream.inner.get_mut().inner.flush() } fn write_vectored(&mut self, bufs: &[io::IoSlice<'_>]) -> io::Result { self.stream.inner.get_mut().inner.write_vectored(bufs) } } impl Read for TryStream { fn read(&mut self, buf: &mut [u8]) -> io::Result { self.stream.inner.read(buf) } } impl BufRead for TryStream { fn fill_buf(&mut self) -> io::Result<&[u8]> { self.stream.inner.fill_buf() } fn consume(&mut self, amt: usize) { self.stream.inner.consume(amt) } } #[derive(Debug)] struct ControlledReader { inner: BufReader>, } impl ControlledReader { fn new(reader: R) -> Self { Self { inner: BufReader::new(BufferedReader::new(reader)), } } fn flush_in_buffer(&mut self) { // Because we have 2 buffered streams there might appear inconsistancy // in read operations and the data which was via `keep_in_buffer` function. // // To eliminate it we move BufReader buffer to our buffer. let b = self.inner.buffer().to_vec(); self.inner.consume(b.len()); self.keep_in_buffer(&b); } } impl ControlledReader { fn keep_in_buffer(&mut self, v: &[u8]) { self.inner.get_mut().buffer.extend(v); } fn get_mut(&mut self) -> &mut R { &mut self.inner.get_mut().inner } fn get_available(&mut self) -> &[u8] { &self.inner.get_ref().buffer } fn consume_available(&mut self, n: usize) { let _ = self.inner.get_mut().buffer.drain(..n); } } #[derive(Debug)] struct BufferedReader { inner: R, buffer: Vec, } impl BufferedReader { fn new(reader: R) -> Self { Self { inner: reader, buffer: Vec::new(), } } } impl Read for BufferedReader { fn read(&mut self, mut buf: &mut [u8]) -> std::io::Result { if self.buffer.is_empty() { self.inner.read(buf) } else { let n = buf.write(&self.buffer)?; let _ = self.buffer.drain(..n); Ok(n) } } } expectrl-0.7.1/src/stream/log.rs000064400000000000000000000075451046102023000147030ustar 00000000000000//! This module container a [LogStream] //! which can wrap other streams in order to log a read/write operations. use std::{ io::{self, Read, Result, Write}, ops::{Deref, DerefMut}, }; #[cfg(feature = "async")] use futures_lite::{AsyncRead, AsyncWrite}; #[cfg(feature = "async")] use std::{ pin::Pin, task::{Context, Poll}, }; use crate::process::NonBlocking; /// LogStream a IO stream wrapper, /// which logs each write/read operation. #[derive(Debug)] pub struct LogStream { stream: S, logger: W, } impl LogStream { /// Creates a new instance of the stream. pub fn new(stream: S, logger: W) -> Self { Self { stream, logger } } } impl LogStream { fn log_write(&mut self, buf: &[u8]) { log(&mut self.logger, "write", buf); } fn log_read(&mut self, buf: &[u8]) { log(&mut self.logger, "read", buf); } } impl Write for LogStream { fn write(&mut self, buf: &[u8]) -> Result { let n = self.stream.write(buf)?; self.log_write(&buf[..n]); Ok(n) } fn flush(&mut self) -> Result<()> { self.stream.flush() } fn write_vectored(&mut self, bufs: &[io::IoSlice<'_>]) -> Result { let n = self.stream.write_vectored(bufs)?; let mut rest = n; let mut bytes = Vec::new(); for buf in bufs { let written = std::cmp::min(buf.len(), rest); rest -= written; bytes.extend(&buf.as_ref()[..written]); if rest == 0 { break; } } self.log_write(&bytes); Ok(n) } } impl Read for LogStream { fn read(&mut self, buf: &mut [u8]) -> Result { let n = self.stream.read(buf)?; self.log_read(&buf[..n]); Ok(n) } } impl NonBlocking for LogStream { fn set_non_blocking(&mut self) -> Result<()> { self.stream.set_non_blocking() } fn set_blocking(&mut self) -> Result<()> { self.stream.set_blocking() } } impl Deref for LogStream { type Target = S; fn deref(&self) -> &Self::Target { &self.stream } } impl DerefMut for LogStream { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.stream } } #[cfg(feature = "async")] impl AsyncWrite for LogStream { fn poll_write( mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8], ) -> Poll> { self.log_write(buf); Pin::new(&mut self.get_mut().stream).poll_write(cx, buf) } fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::new(&mut self.stream).poll_flush(cx) } fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::new(&mut self.stream).poll_close(cx) } fn poll_write_vectored( mut self: Pin<&mut Self>, cx: &mut Context<'_>, bufs: &[io::IoSlice<'_>], ) -> Poll> { Pin::new(&mut self.stream).poll_write_vectored(cx, bufs) } } #[cfg(feature = "async")] impl AsyncRead for LogStream { fn poll_read( mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut [u8], ) -> Poll> { let result = Pin::new(&mut self.stream).poll_read(cx, buf); if let Poll::Ready(Ok(n)) = &result { self.log_read(&buf[..*n]); } result } } fn log(mut writer: impl Write, target: &str, data: &[u8]) { let _ = match std::str::from_utf8(data) { Ok(data) => writeln!(writer, "{}: {:?}", target, data), Err(..) => writeln!(writer, "{}:(bytes): {:?}", target, data), }; } expectrl-0.7.1/src/stream/mod.rs000064400000000000000000000001331046102023000146630ustar 00000000000000//! Stream module contains a set of IO (write/read) wrappers. pub mod log; pub mod stdin; expectrl-0.7.1/src/stream/stdin.rs000064400000000000000000000223641046102023000152370ustar 00000000000000//! The module contains a nonblocking version of [std::io::Stdin]. use std::io; #[cfg(not(feature = "async"))] use std::io::Read; #[cfg(feature = "async")] use std::{ pin::Pin, task::{Context, Poll}, }; #[cfg(feature = "async")] use futures_lite::AsyncRead; use crate::Error; /// A non blocking version of STDIN. /// /// It's not recomended to be used directly. /// But we expose it because it cab be used with [`Session::interact`]. /// /// [`Session::interact`]: crate::session::Session::interact #[derive(Debug)] pub struct Stdin { inner: inner::StdinInner, } impl Stdin { /// Creates a new instance of Stdin. /// /// It may change terminal's STDIN state therefore, after /// it's used you must call [Stdin::close]. pub fn open() -> Result { #[cfg(not(feature = "async"))] { let mut stdin = inner::StdinInner::new().map(|inner| Self { inner })?; stdin.blocking(true)?; Ok(stdin) } #[cfg(feature = "async")] { let stdin = inner::StdinInner::new().map(|inner| Self { inner })?; Ok(stdin) } } /// Close frees a resources which were used. /// /// It must be called [Stdin] was used. /// Otherwise the STDIN might be returned to original state. pub fn close(mut self) -> Result<(), Error> { #[cfg(not(feature = "async"))] self.blocking(false)?; self.inner.close()?; Ok(()) } #[cfg(not(feature = "async"))] pub(crate) fn blocking(&mut self, on: bool) -> Result<(), Error> { self.inner.blocking(on) } } #[cfg(not(feature = "async"))] impl Read for Stdin { fn read(&mut self, buf: &mut [u8]) -> io::Result { self.inner.read(buf) } } #[cfg(feature = "async")] impl AsyncRead for Stdin { fn poll_read( mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut [u8], ) -> Poll> { AsyncRead::poll_read(Pin::new(&mut self.inner), cx, buf) } } #[cfg(unix)] impl std::os::unix::prelude::AsRawFd for Stdin { fn as_raw_fd(&self) -> std::os::unix::prelude::RawFd { self.inner.as_raw_fd() } } #[cfg(unix)] impl std::os::unix::prelude::AsRawFd for &mut Stdin { fn as_raw_fd(&self) -> std::os::unix::prelude::RawFd { self.inner.as_raw_fd() } } #[cfg(all(unix, feature = "polling"))] impl polling::Source for Stdin { fn raw(&self) -> std::os::unix::prelude::RawFd { std::os::unix::io::AsRawFd::as_raw_fd(self) } } #[cfg(unix)] mod inner { use super::*; use std::os::unix::prelude::AsRawFd; use nix::{ libc::STDIN_FILENO, sys::termios::{self, Termios}, unistd::isatty, }; use ptyprocess::set_raw; #[derive(Debug)] pub(super) struct StdinInner { orig_flags: Option, #[cfg(feature = "async")] stdin: async_io::Async, #[cfg(not(feature = "async"))] stdin: std::io::Stdin, } impl StdinInner { pub(super) fn new() -> Result { let stdin = std::io::stdin(); #[cfg(feature = "async")] let stdin = async_io::Async::new(stdin)?; #[cfg(target_os = "macos")] let orig_flags = None; #[cfg(not(target_os = "macos"))] let orig_flags = Self::prepare()?; Ok(Self { stdin, orig_flags }) } pub(super) fn prepare() -> Result, Error> { // flush buffers // self.stdin.flush()?; let mut o_pty_flags = None; // verify: possible controlling fd can be stdout and stderr as well? // https://stackoverflow.com/questions/35873843/when-setting-terminal-attributes-via-tcsetattrfd-can-fd-be-either-stdout let isatty_terminal = isatty(STDIN_FILENO) .map_err(|e| Error::unknown("failed to call isatty", e.to_string()))?; if isatty_terminal { // tcgetattr issues error if a provided fd is not a tty, // but we can work with such input as it may be redirected. o_pty_flags = termios::tcgetattr(STDIN_FILENO) .map(Some) .map_err(|e| Error::unknown("failed to call tcgetattr", e.to_string()))?; set_raw(STDIN_FILENO) .map_err(|e| Error::unknown("failed to set a raw tty", e.to_string()))?; } Ok(o_pty_flags) } pub(super) fn close(&mut self) -> Result<(), Error> { if let Some(origin_stdin_flags) = &self.orig_flags { termios::tcsetattr(STDIN_FILENO, termios::SetArg::TCSAFLUSH, origin_stdin_flags) .map_err(|e| Error::unknown("failed to call tcsetattr", e.to_string()))?; } Ok(()) } #[cfg(not(feature = "async"))] pub(crate) fn blocking(&mut self, on: bool) -> Result<(), Error> { crate::process::unix::make_non_blocking(self.as_raw_fd(), on).map_err(Error::IO) } } impl AsRawFd for StdinInner { fn as_raw_fd(&self) -> std::os::unix::prelude::RawFd { self.stdin.as_raw_fd() } } #[cfg(not(feature = "async"))] impl Read for StdinInner { fn read(&mut self, buf: &mut [u8]) -> io::Result { self.stdin.read(buf) } } #[cfg(feature = "async")] impl AsyncRead for StdinInner { fn poll_read( mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut [u8], ) -> Poll> { AsyncRead::poll_read(Pin::new(&mut self.stdin), cx, buf) } } } #[cfg(windows)] mod inner { use super::*; use conpty::console::Console; pub(super) struct StdinInner { terminal: Console, #[cfg(not(feature = "async"))] is_blocking: bool, #[cfg(not(feature = "async"))] stdin: io::Stdin, #[cfg(feature = "async")] stdin: blocking::Unblock, } impl std::fmt::Debug for StdinInner { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { #[cfg(not(feature = "async"))] { f.debug_struct("StdinInner") .field("terminal", &self.terminal) .field("is_blocking", &self.is_blocking) .field("stdin", &self.stdin) .field("stdin", &self.stdin) .finish() } #[cfg(feature = "async")] { f.debug_struct("StdinInner") .field("terminal", &self.terminal) .field("stdin", &self.stdin) .field("stdin", &self.stdin) .finish() } } } impl StdinInner { /// Creates a new instance of Stdin. /// /// It changes terminal's STDIN state therefore, after /// it's used please call [Stdin::close]. pub(super) fn new() -> Result { let console = conpty::console::Console::current().map_err(to_io_error)?; console.set_raw().map_err(to_io_error)?; let stdin = io::stdin(); #[cfg(feature = "async")] let stdin = blocking::Unblock::new(stdin); Ok(Self { terminal: console, #[cfg(not(feature = "async"))] is_blocking: false, stdin, }) } pub(super) fn close(&mut self) -> Result<(), Error> { self.terminal.reset().map_err(to_io_error)?; Ok(()) } #[cfg(not(feature = "async"))] pub(crate) fn blocking(&mut self, on: bool) -> Result<(), Error> { self.is_blocking = on; Ok(()) } } #[cfg(not(feature = "async"))] impl Read for StdinInner { fn read(&mut self, buf: &mut [u8]) -> io::Result { // fixme: I am not sure why reading works on is_stdin_empty() == true // maybe rename of the method necessary if self.is_blocking && !self.terminal.is_stdin_empty().map_err(to_io_error)? { return Err(io::Error::new(io::ErrorKind::WouldBlock, "")); } self.stdin.read(buf) } } #[cfg(feature = "async")] impl AsyncRead for StdinInner { fn poll_read( mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut [u8], ) -> Poll> { AsyncRead::poll_read(Pin::new(&mut self.stdin), cx, buf) } } fn to_io_error(err: impl std::error::Error) -> io::Error { io::Error::new(io::ErrorKind::Other, err.to_string()) } #[cfg(all(feature = "polling", not(feature = "async")))] impl Clone for StdinInner { fn clone(&self) -> Self { Self { terminal: self.terminal.clone(), is_blocking: self.is_blocking.clone(), stdin: std::io::stdin(), } } } } #[cfg(all(windows, feature = "polling", not(feature = "async")))] impl Clone for Stdin { fn clone(&self) -> Self { Self { inner: self.inner.clone(), } } } expectrl-0.7.1/src/waiter/blocking.rs000064400000000000000000000042441046102023000157030ustar 00000000000000use std::{ io::{Read, Result}, marker::PhantomData, thread::JoinHandle, }; use crossbeam_channel::Sender; /// Blocking implements a reading operation on a different thread. /// It stops the thread once any [`Error`] is encountered. #[derive(Debug)] pub struct Blocking { _id: usize, _thread: JoinHandle<()>, _reader: PhantomData, } impl Blocking { /// Creates a new blocking reader, spawning a new thread for it. pub fn new(id: usize, mut reader: R, sendr: Sender<(usize, Result>)>) -> Self where R: Read + Send + 'static, { let handle = std::thread::spawn(move || { let mut buffer = Vec::new(); let mut buf = [0; 1]; loop { match reader.read(&mut buf) { Ok(n) => { // try send failed tries if !buffer.is_empty() { for b in buffer.drain(..).collect::>() { try_send(id, Ok(Some(b)), &sendr, &mut buffer); } } if n == 0 { try_send(id, Ok(None), &sendr, &mut buffer); break; } else { try_send(id, Ok(Some(buf[0])), &sendr, &mut buffer); } } Err(err) => { // stopping the thread on error try_send(id, Err(err), &sendr, &mut buffer); break; } } } }); Self { _id: id, _thread: handle, _reader: PhantomData, } } pub fn join(self) -> std::thread::Result<()> { self._thread.join() } } fn try_send( id: usize, msg: Result>, sendr: &Sender<(usize, Result>)>, buf: &mut Vec, ) { match sendr.send((id, msg)) { Ok(_) => (), Err(err) => { if let Ok(Some(b)) = err.0 .1 { buf.push(b); } } } } expectrl-0.7.1/src/waiter/mod.rs000064400000000000000000000000661046102023000146700ustar 00000000000000mod blocking; mod wait; pub use wait::{Recv, Wait2}; expectrl-0.7.1/src/waiter/wait.rs000064400000000000000000000027421046102023000150600ustar 00000000000000use std::{ io::{self, Read}, time::Duration, }; use crossbeam_channel::Receiver; use super::blocking::Blocking; #[derive(Debug)] pub struct Wait2 { recv: Receiver<(usize, io::Result>)>, b1: Blocking, b2: Blocking, timeout: Duration, } #[derive(Debug)] pub enum Recv { R1(io::Result>), R2(io::Result>), Timeout, } impl Wait2 { pub fn new(r1: R1, r2: R2) -> Self where R1: Send + Read + 'static, R2: Send + Read + 'static, { let (sndr, recv) = crossbeam_channel::unbounded(); let b1 = Blocking::new(0, r1, sndr.clone()); let b2 = Blocking::new(1, r2, sndr); Self { b1, b2, recv, timeout: Duration::from_secs(5), } } pub fn join(self) -> std::thread::Result<()> { self.b1.join()?; self.b2.join()?; Ok(()) } pub fn recv(&mut self) -> Result { match self.recv.recv_timeout(self.timeout) { Ok((id, result)) => match id { 0 => Ok(Recv::R1(result)), 1 => Ok(Recv::R2(result)), _ => unreachable!(), }, Err(err) => { if err.is_timeout() { Ok(Recv::Timeout) } else { Err(crossbeam_channel::RecvError) } } } } } expectrl-0.7.1/tests/actions/cat/main.py000064400000000000000000000002541046102023000163270ustar 00000000000000import sys def main(): try: for line in sys.stdin: print(line, sep=None, end="") except: exit(1) if __name__ == "__main__": main()expectrl-0.7.1/tests/actions/echo/main.py000064400000000000000000000002111046102023000164670ustar 00000000000000import sys def main(): try: print(' '.join(sys.argv[1:])) except: exit(1) if __name__ == "__main__": main()expectrl-0.7.1/tests/check.rs000064400000000000000000000232151046102023000142470ustar 00000000000000#![cfg(unix)] use expectrl::{spawn, Any, Eof, NBytes, Regex}; use std::thread; use std::time::Duration; #[cfg(unix)] #[cfg(not(feature = "async"))] #[test] fn check_str() { let mut session = spawn("cat").unwrap(); session.send_line("Hello World").unwrap(); session.check("Hello World").unwrap(); } #[cfg(unix)] #[cfg(feature = "async")] #[test] fn check_str() { futures_lite::future::block_on(async { let mut session = spawn("cat").unwrap(); session.send_line("Hello World").await.unwrap(); session.check("Hello World").await.unwrap(); }) } #[cfg(unix)] #[cfg(not(feature = "async"))] #[test] fn check_regex() { let mut session = spawn("cat").unwrap(); session.send_line("Hello World").unwrap(); thread::sleep(Duration::from_millis(600)); let m = session.check(Regex("lo.*")).unwrap(); assert_eq!(m.before(), b"Hel"); assert_eq!(m.get(0).unwrap(), b"lo World\r"); } #[cfg(unix)] #[cfg(feature = "async")] #[test] fn check_regex() { futures_lite::future::block_on(async { let mut session = spawn("cat").unwrap(); session.send_line("Hello World").await.unwrap(); thread::sleep(Duration::from_millis(600)); let m = session.check(Regex("lo.*")).await.unwrap(); assert_eq!(m.before(), b"Hel"); assert_eq!(m.get(0).unwrap(), b"lo World\r"); }) } #[cfg(unix)] #[cfg(not(feature = "async"))] #[test] fn check_n_bytes() { let mut session = spawn("cat").unwrap(); session.send_line("Hello World").unwrap(); thread::sleep(Duration::from_millis(600)); let m = session.check(NBytes(3)).unwrap(); assert_eq!(m.get(0).unwrap(), b"Hel"); assert_eq!(m.before(), b""); } #[cfg(unix)] #[cfg(feature = "async")] #[test] fn check_n_bytes() { futures_lite::future::block_on(async { let mut session = spawn("cat").unwrap(); session.send_line("Hello World").await.unwrap(); thread::sleep(Duration::from_millis(600)); let m = session.check(NBytes(3)).await.unwrap(); assert_eq!(m.get(0).unwrap(), b"Hel"); assert_eq!(m.before(), b""); }) } #[cfg(unix)] #[cfg(not(feature = "async"))] #[test] fn check_eof() { let mut session = spawn("echo 'Hello World'").unwrap(); thread::sleep(Duration::from_millis(600)); let m = session.check(Eof).unwrap(); assert_eq!(m.before(), b""); if m.matches().len() > 0 { let buf = m.get(0).unwrap(); #[cfg(target_os = "macos")] assert!(matches!(buf, b"" | b"'Hello World'\r\n"), "{:?}", buf); #[cfg(not(target_os = "macos"))] assert_eq!(buf, b"'Hello World'\r\n"); } } #[cfg(unix)] #[cfg(feature = "async")] #[test] fn check_eof() { futures_lite::future::block_on(async { let mut session = spawn("echo 'Hello World'").unwrap(); thread::sleep(Duration::from_millis(600)); let m = session.check(Eof).await.unwrap(); assert_eq!(m.before(), b""); if m.matches().len() > 0 { let buf = m.get(0).unwrap(); #[cfg(target_os = "macos")] assert!(matches!(buf, b"" | b"'Hello World'\r\n"), "{:?}", buf); #[cfg(not(target_os = "macos"))] assert_eq!(buf, b"'Hello World'\r\n"); } }) } #[cfg(unix)] #[cfg(not(feature = "async"))] #[test] fn read_after_check_str() { use std::io::Read; let mut session = spawn("cat").unwrap(); session.send_line("Hello World").unwrap(); thread::sleep(Duration::from_millis(600)); let f = session.check("Hello").unwrap(); assert!(!f.is_empty()); // we stop process so read operation will fail. // other wise read call would block. session.get_process_mut().exit(false).unwrap(); let mut buf = [0; 6]; session.read_exact(&mut buf).unwrap(); assert_eq!(&buf, b" World"); } #[cfg(unix)] #[cfg(feature = "async")] #[test] fn read_after_check_str() { use futures_lite::io::AsyncReadExt; futures_lite::future::block_on(async { let mut session = spawn("cat").unwrap(); session.send_line("Hello World").await.unwrap(); thread::sleep(Duration::from_millis(600)); let f = session.check("Hello").await.unwrap(); assert!(!f.is_empty()); // we stop process so read operation will fail. // other wise read call would block. session.get_process_mut().exit(false).unwrap(); let mut buf = [0; 6]; session.read_exact(&mut buf).await.unwrap(); assert_eq!(&buf, b" World"); }) } #[cfg(unix)] #[cfg(not(feature = "async"))] #[test] fn check_eof_timeout() { let mut p = spawn("sleep 3").expect("cannot run sleep 3"); match p.check(Eof) { Ok(found) if found.is_empty() => {} r => panic!("reached a timeout {r:?}"), } } #[cfg(unix)] #[cfg(feature = "async")] #[test] fn check_eof_timeout() { futures_lite::future::block_on(async { let mut p = spawn("sleep 3").expect("cannot run sleep 3"); match p.check(Eof).await { Ok(found) if found.is_empty() => {} r => panic!("reached a timeout {r:?}"), } }) } #[cfg(unix)] #[cfg(not(feature = "async"))] #[test] fn check_macro() { let mut session = spawn("cat").unwrap(); session.send_line("Hello World").unwrap(); thread::sleep(Duration::from_millis(600)); expectrl::check!( &mut session, world = "\r" => { assert_eq!(world.get(0).unwrap(), b"\r"); }, _ = "Hello World" => { panic!("Unexpected result"); }, default => { panic!("Unexpected result"); }, ) .unwrap(); } #[cfg(unix)] #[cfg(feature = "async")] #[test] fn check_macro() { let mut session = spawn("cat").unwrap(); futures_lite::future::block_on(session.send_line("Hello World")).unwrap(); thread::sleep(Duration::from_millis(600)); futures_lite::future::block_on(async { expectrl::check!( session, // world = "\r" => { // assert_eq!(world.get(0).unwrap(), b"\r"); // }, // _ = "Hello World" => { // panic!("Unexpected result"); // }, // default => { // panic!("Unexpected result"); // }, ) .await .unwrap(); }); } #[cfg(unix)] #[cfg(not(feature = "async"))] #[test] fn check_macro_doest_consume_missmatch() { let mut session = spawn("cat").unwrap(); session.send_line("Hello World").unwrap(); thread::sleep(Duration::from_millis(600)); expectrl::check!( &mut session, _ = "Something which is not inside" => { panic!("Unexpected result"); }, ) .unwrap(); session.send_line("345").unwrap(); thread::sleep(Duration::from_millis(600)); expectrl::check!( &mut session, buffer = Eof => { assert_eq!(buffer.get(0).unwrap(), b"Hello World\r\n") }, ) .unwrap(); } #[cfg(unix)] #[cfg(feature = "async")] #[test] fn check_macro_doest_consume_missmatch() { let mut session = spawn("cat").unwrap(); futures_lite::future::block_on(async { session.send_line("Hello World").await.unwrap(); thread::sleep(Duration::from_millis(600)); expectrl::check!( session, _ = "Something which is not inside" => { panic!("Unexpected result"); }, ) .await .unwrap(); session.send_line("345").await.unwrap(); thread::sleep(Duration::from_millis(600)); expectrl::check!( session, buffer = Eof => { assert_eq!(buffer.get(0).unwrap(), b"Hello World\r\n") }, ) .await .unwrap(); }); } #[cfg(unix)] #[cfg(not(feature = "async"))] #[test] fn check_macro_with_different_needles() { let check_input = |session: &mut _| { expectrl::check!( session, number = Any(["123", "345"]) => { assert_eq!(number.get(0).unwrap(), b"345") }, line = "\n" => { assert_eq!(line.before(), b"Hello World\r") }, default => { panic!("Unexpected result"); }, ) .unwrap(); }; let mut session = spawn("cat").unwrap(); session.send_line("Hello World").unwrap(); thread::sleep(Duration::from_millis(600)); check_input(&mut session); session.send_line("345").unwrap(); thread::sleep(Duration::from_millis(600)); check_input(&mut session); } #[cfg(unix)] #[cfg(feature = "async")] #[test] fn check_macro_with_different_needles() { futures_lite::future::block_on(async { let mut session = spawn("cat").unwrap(); session.send_line("Hello World").await.unwrap(); thread::sleep(Duration::from_millis(600)); expectrl::check!( session, number = Any(["123", "345"]) => { assert_eq!(number.get(0).unwrap(), b"345") }, line = "\n" => { assert_eq!(line.before(), b"Hello World\r") }, default => { panic!("Unexpected result"); }, ) .await .unwrap(); session.send_line("345").await.unwrap(); thread::sleep(Duration::from_millis(600)); expectrl::check!( session, number = Any(["123", "345"]) => { assert_eq!(number.get(0).unwrap(), b"345") }, line = "\n" => { assert_eq!(line.before(), b"Hello World\r") }, default => { panic!("Unexpected result"); }, ) .await .unwrap(); }); } expectrl-0.7.1/tests/expect.rs000064400000000000000000000215161046102023000144640ustar 00000000000000use expectrl::{spawn, Eof, NBytes, Regex}; use std::time::Duration; #[cfg(not(feature = "async"))] use std::io::Read; #[cfg(feature = "async")] use futures_lite::io::AsyncReadExt; #[cfg(unix)] #[cfg(not(feature = "async"))] #[test] fn expect_str() { let mut session = spawn("cat").unwrap(); session.send_line("Hello World").unwrap(); session.expect("Hello World").unwrap(); } #[cfg(unix)] #[cfg(feature = "async")] #[test] fn expect_str() { futures_lite::future::block_on(async { let mut session = spawn("cat").unwrap(); session.send_line("Hello World").await.unwrap(); session.expect("Hello World").await.unwrap(); }) } #[cfg(windows)] #[test] fn expect_str() { let mut session = spawn(r#"pwsh -c "python ./tests/actions/cat/main.py""#).unwrap(); #[cfg(not(feature = "async"))] { session.send_line("Hello World\n\r").unwrap(); session.expect("Hello World").unwrap(); } #[cfg(feature = "async")] { futures_lite::future::block_on(async { session.send_line("Hello World").await.unwrap(); session.expect("Hello World").await.unwrap(); }) } } #[cfg(unix)] #[cfg(not(feature = "async"))] #[test] fn expect_regex() { let mut session = spawn("cat").unwrap(); session.send_line("Hello World").unwrap(); let m = session.expect(Regex("lo.*")).unwrap(); assert_eq!(m.before(), b"Hel"); assert_eq!(m.get(0).unwrap(), b"lo World\r"); } #[cfg(unix)] #[cfg(not(feature = "async"))] #[test] fn expect_regex_lazy() { let mut session = spawn("cat").unwrap(); session.set_expect_lazy(true); session.send_line("Hello World").unwrap(); let m = session.expect(Regex("lo.*")).unwrap(); assert_eq!(m.before(), b"Hel"); assert_eq!(m.get(0).unwrap(), b"lo"); } #[cfg(unix)] #[cfg(feature = "async")] #[test] fn expect_gready_regex() { futures_lite::future::block_on(async { let mut session = spawn("cat").unwrap(); session.send_line("Hello World").await.unwrap(); let m = session.expect(Regex("lo.*")).await.unwrap(); assert_eq!(m.before(), b"Hel"); assert_eq!(m.get(0).unwrap(), b"lo World\r"); }) } #[cfg(unix)] #[cfg(feature = "async")] #[test] fn expect_lazy_regex() { futures_lite::future::block_on(async { let mut session = spawn("cat").unwrap(); session.set_expect_lazy(true); session.send_line("Hello World").await.unwrap(); let m = session.expect(Regex("lo.*")).await.unwrap(); assert_eq!(m.before(), b"Hel"); assert_eq!(m.get(0).unwrap(), b"lo"); }) } #[cfg(windows)] #[test] fn expect_regex() { let mut session = spawn("python ./tests/actions/echo/main.py Hello World").unwrap(); #[cfg(not(feature = "async"))] { let m = session.expect(Regex("lo.*")).unwrap(); assert_eq!(m.matches().count(), 1); assert_eq!(m.get(0).unwrap(), b"lo World\r"); } #[cfg(feature = "async")] { futures_lite::future::block_on(async { let m = session.expect(Regex("lo.*")).await.unwrap(); assert_eq!(m.matches().count(), 1); assert_eq!(m.get(0).unwrap(), b"lo World\r"); }) } } #[cfg(unix)] #[cfg(not(feature = "async"))] #[test] fn expect_n_bytes() { let mut session = spawn("cat").unwrap(); session.send_line("Hello World").unwrap(); let m = session.expect(NBytes(3)).unwrap(); assert_eq!(m.get(0).unwrap(), b"Hel"); assert_eq!(m.before(), b""); } #[cfg(unix)] #[cfg(feature = "async")] #[test] fn expect_n_bytes() { futures_lite::future::block_on(async { let mut session = spawn("cat").unwrap(); session.send_line("Hello World").await.unwrap(); let m = session.expect(NBytes(3)).await.unwrap(); assert_eq!(m.get(0).unwrap(), b"Hel"); assert_eq!(m.before(), b""); }) } #[cfg(windows)] #[test] fn expect_n_bytes() { use expectrl::Session; use std::process::Command; let mut session = Session::spawn(Command::new( "python ./tests/actions/echo/main.py Hello World", )) .unwrap(); #[cfg(not(feature = "async"))] { let m = session.expect(NBytes(14)).unwrap(); assert_eq!(m.matches().count(), 1); assert_eq!(m.get(0).unwrap().len(), 14); assert_eq!(m.before(), b""); } #[cfg(feature = "async")] { futures_lite::future::block_on(async { let m = session.expect(NBytes(14)).await.unwrap(); assert_eq!(m.matches().count(), 1); assert_eq!(m.get(0).unwrap().len(), 14); assert_eq!(m.before(), b""); }) } } #[cfg(unix)] #[cfg(not(feature = "async"))] #[test] fn expect_eof() { let mut session = spawn("echo 'Hello World'").unwrap(); session.set_expect_timeout(None); let m = session.expect(Eof).unwrap(); assert_eq!(m.get(0).unwrap(), b"'Hello World'\r\n"); assert_eq!(m.before(), b""); } #[cfg(unix)] #[cfg(feature = "async")] #[test] fn expect_eof() { futures_lite::future::block_on(async { let mut session = spawn("echo 'Hello World'").unwrap(); session.set_expect_timeout(None); let m = session.expect(Eof).await.unwrap(); assert_eq!(m.get(0).unwrap(), b"'Hello World'\r\n"); assert_eq!(m.before(), b""); }) } #[cfg(windows)] #[test] #[ignore = "https://stackoverflow.com/questions/68985384/does-a-conpty-reading-pipe-get-notified-on-process-termination"] fn expect_eof() { let mut session = spawn("echo 'Hello World'").unwrap(); // give shell some time std::thread::sleep(Duration::from_millis(300)); #[cfg(not(feature = "async"))] { let m = session.expect(Eof).unwrap(); assert_eq!(m.get(0).unwrap(), b"'Hello World'\r\n"); assert_eq!(m.before(), b""); } #[cfg(feature = "async")] { futures_lite::future::block_on(async { let m = session.expect(Eof).await.unwrap(); assert_eq!(m.get(0).unwrap(), b"'Hello World'\r\n"); assert_eq!(m.before(), b""); }) } } #[cfg(unix)] #[cfg(not(feature = "async"))] #[test] fn read_after_expect_str() { let mut session = spawn("cat").unwrap(); session.send_line("Hello World").unwrap(); session.expect("Hello").unwrap(); let mut buf = [0; 6]; session.read_exact(&mut buf).unwrap(); assert_eq!(&buf, b" World"); } #[cfg(unix)] #[cfg(feature = "async")] #[test] fn read_after_expect_str() { futures_lite::future::block_on(async { let mut session = spawn("cat").unwrap(); session.send_line("Hello World").await.unwrap(); session.expect("Hello").await.unwrap(); let mut buf = [0; 6]; session.read_exact(&mut buf).await.unwrap(); assert_eq!(&buf, b" World"); }) } #[cfg(windows)] #[cfg(not(feature = "async"))] #[test] fn read_after_expect_str() { let mut session = spawn("echo 'Hello World'").unwrap(); // give shell some time std::thread::sleep(Duration::from_millis(300)); session.expect("Hello").unwrap(); let mut buf = [0; 6]; session.read_exact(&mut buf).unwrap(); assert_eq!(&buf, b" World"); } #[cfg(windows)] #[cfg(feature = "async")] #[test] fn read_after_expect_str() { let mut session = spawn("echo 'Hello World'").unwrap(); // give shell some time std::thread::sleep(Duration::from_millis(300)); futures_lite::future::block_on(async { session.expect("Hello").await.unwrap(); let mut buf = [0; 6]; session.read_exact(&mut buf).await.unwrap(); assert_eq!(&buf, b" World"); }) } #[cfg(unix)] #[cfg(not(feature = "async"))] #[test] fn expect_eof_timeout() { let mut p = spawn("sleep 3").expect("cannot run sleep 3"); p.set_expect_timeout(Some(Duration::from_millis(100))); match p.expect(Eof) { Err(expectrl::Error::ExpectTimeout) => {} r => panic!("reached a timeout {r:?}"), } } #[cfg(unix)] #[cfg(feature = "async")] #[test] fn expect_eof_timeout() { futures_lite::future::block_on(async { let mut p = spawn("sleep 3").expect("cannot run sleep 3"); p.set_expect_timeout(Some(Duration::from_millis(100))); match p.expect(Eof).await { Err(expectrl::Error::ExpectTimeout) => {} r => panic!("reached a timeout {r:?}"), } }) } #[cfg(windows)] #[test] fn expect_eof_timeout() { let mut p = spawn("sleep 3").expect("cannot run sleep 3"); p.set_expect_timeout(Some(Duration::from_millis(100))); #[cfg(not(feature = "async"))] { match p.expect(Eof) { Err(expectrl::Error::ExpectTimeout) => {} r => panic!("should raise TimeOut {:?}", r), } } #[cfg(feature = "async")] { futures_lite::future::block_on(async { match p.expect(Eof).await { Err(expectrl::Error::ExpectTimeout) => {} r => panic!("should raise TimeOut {:?}", r), } }) } } expectrl-0.7.1/tests/interact.rs000064400000000000000000000316621046102023000150100ustar 00000000000000#![cfg(unix)] use std::{ io::{self, Cursor, Read, Write}, time::{Duration, Instant}, }; #[cfg(not(feature = "async"))] use std::io::sink; #[cfg(not(feature = "async"))] use expectrl::{interact::actions::lookup::Lookup, spawn, stream::stdin::Stdin, NBytes}; #[cfg(not(feature = "async"))] use expectrl::WaitStatus; #[cfg(unix)] #[cfg(not(feature = "async"))] #[ignore = "It requires manual interaction; Or it's necessary to redirect an stdin of current process"] #[test] fn interact_callback() { use expectrl::interact::InteractOptions; let mut input_handle = Lookup::new(); let mut output_handle = Lookup::new(); let mut session = spawn("cat").unwrap(); let mut stdin = Stdin::open().unwrap(); let opts = InteractOptions::default() .on_output(|ctx| { if let Some(m) = output_handle.on(ctx.buf, ctx.eof, b'\n')? { let line = m.before(); println!("Line in output {:?}", String::from_utf8_lossy(line)); } Ok(false) }) .on_input(|ctx| { if input_handle.on(ctx.buf, ctx.eof, "213")?.is_some() { ctx.session.send_line("Hello World")?; } Ok(false) }); session.interact(&mut stdin, sink()).spawn(opts).unwrap(); stdin.close().unwrap(); } #[cfg(unix)] #[cfg(not(feature = "async"))] #[test] fn interact_output_callback() { use expectrl::interact::{InteractOptions, InteractSession}; let mut session = expectrl::spawn("sleep 1 && echo 'Hello World'").unwrap(); let mut stdin = Stdin::open().unwrap(); let stdout = std::io::sink(); let mut state = 0; let mut lookup = Lookup::new(); let interact_opts = InteractOptions::new(&mut state).on_output(|ctx| { if lookup.on(ctx.buf, ctx.eof, "World")?.is_some() { **ctx.state += 1; } Ok(false) }); let mut interact = InteractSession::new(&mut session, &mut stdin, stdout); interact.spawn(interact_opts).unwrap(); stdin.close().unwrap(); // fixme: sometimes it's 0 // I guess because the process gets down to fast. assert!(matches!(state, 1 | 0), "{state:?}"); } #[cfg(unix)] #[cfg(not(feature = "async"))] #[test] fn interact_callbacks_called_after_exit() { use expectrl::interact::InteractOptions; let mut session = expectrl::spawn("echo 'Hello World'").unwrap(); assert_eq!( session.get_process().wait().unwrap(), WaitStatus::Exited(session.get_process().pid(), 0) ); let mut stdin = Stdin::open().unwrap(); let stdout = std::io::sink(); let mut state = 0; let mut lookup = Lookup::new(); session .interact(&mut stdin, stdout) .spawn(&mut InteractOptions::new(&mut state).on_output(|ctx| { if lookup.on(ctx.buf, ctx.eof, "World")?.is_some() { **ctx.state += 1; } Ok(false) })) .unwrap(); stdin.close().unwrap(); assert_eq!(state, 0); } #[cfg(unix)] #[cfg(not(any(feature = "async", feature = "polling")))] #[test] fn interact_callbacks_with_stream_redirection() { use expectrl::interact::InteractOptions; let output_lines = vec![ "NO_MATCHED\n".to_string(), "QWE\n".to_string(), "QW123\n".to_string(), "NO_MATCHED_2\n".to_string(), ]; let reader = ListReaderWithDelayedEof::new(output_lines, Duration::from_secs(2)); let mut writer = io::Cursor::new(vec![0; 2048]); let mut session = spawn("cat").unwrap(); let mut input_handle = Lookup::new(); session .interact(reader, &mut writer) .spawn(InteractOptions::default().on_input(|ctx| { if input_handle.on(ctx.buf, ctx.eof, "QWE")?.is_some() { ctx.session.send_line("Hello World")?; }; Ok(false) })) .unwrap(); let buffer = String::from_utf8_lossy(writer.get_ref()); assert!(buffer.contains("Hello World"), "{buffer:?}"); } #[cfg(unix)] #[cfg(not(any(feature = "async", feature = "polling")))] #[test] fn interact_filters() { use expectrl::interact::InteractOptions; let reader = ReaderWithDelayEof::new("1009\nNO\n", Duration::from_secs(4)); let mut writer = io::Cursor::new(vec![0; 2048]); let mut session = spawn("cat").unwrap(); session .interact(reader, &mut writer) .spawn( InteractOptions::default() .input_filter(|buf| { // ignore 0 chars let v = buf.iter().filter(|&&b| b != b'0').copied().collect(); Ok(v) }) .output_filter(|buf| { // Make NO -> YES let v = buf .chunks(2) .flat_map(|s| match s { &[b'N', b'O'] => &[b'Y', b'E', b'S'], other => other, }) .copied() .collect(); Ok(v) }), ) .unwrap(); let buffer = String::from_utf8_lossy(writer.get_ref()); let buffer = buffer.trim_end_matches(char::from(0)); // fixme: somehow the output is duplicated which is wrong. assert_eq!(buffer, "19\r\nYES\r\n19\r\nYES\r\n"); } #[cfg(all(unix, not(any(feature = "async", feature = "polling"))))] #[test] fn interact_context() { use expectrl::interact::InteractOptions; let mut session = spawn("cat").unwrap(); let reader = ListReaderWithDelayedEof::new( vec![ "QWE\n".into(), "QWE\n".into(), "QWE\n".into(), "QWE\n".into(), ], Duration::from_secs(2), ); let mut writer = io::Cursor::new(vec![0; 2048]); let mut input_data = Lookup::new(); let mut output_data = Lookup::new(); let mut opts = InteractOptions::new((0, 0)) .on_input(|ctx| { if input_data.on(ctx.buf, ctx.eof, "QWE\n")?.is_some() { ctx.state.0 += 1; ctx.session.send_line("123")?; } Ok(false) }) .on_output(|ctx| { if output_data.on(ctx.buf, ctx.eof, NBytes(1))?.is_some() { ctx.state.1 += 1; output_data.clear(); } Ok(false) }); let is_alive = session .interact(reader, &mut writer) .spawn(&mut opts) .unwrap(); let state = opts.into_inner(); assert!(is_alive); assert_eq!(state.0, 4); assert!(state.1 > 0, "{:?}", state.1); let buffer = String::from_utf8_lossy(writer.get_ref()); assert!(buffer.contains("123"), "{buffer:?}"); } #[cfg(all(unix, not(any(feature = "async", feature = "polling"))))] #[test] fn interact_on_output_not_matched() { // Stops interact mode after 123 being read. // Which may cause it to stay buffered in session. // Verify this buffer was cleaned and 123 won't be accessed then. use expectrl::interact::InteractOptions; let reader = ListReaderWithDelayedEof::new( vec![ "QWE\n".to_string(), "123\n".to_string(), String::from_utf8_lossy(&[29]).to_string(), "WWW\n".to_string(), ], Duration::from_secs(2), ); let mut writer = io::Cursor::new(vec![0; 2048]); let mut input = Lookup::new(); let mut session = spawn("cat").unwrap(); let mut opts = InteractOptions::new((0, 0)) .on_input(|ctx| { if input.on(ctx.buf, ctx.eof, "QWE\n")?.is_some() { ctx.state.0 += 1; } if input.on(ctx.buf, ctx.eof, "WWW\n")?.is_some() { ctx.state.1 += 1; } Ok(false) }) .on_output(|_ctx| Ok(false)) .on_idle(|_ctx| { std::thread::sleep(Duration::from_millis(500)); Ok(false) }); let is_alive = session .interact(reader, &mut writer) .spawn(&mut opts) .unwrap(); let state = opts.into_inner(); assert!(is_alive); assert_eq!(state.0, 2); assert_eq!(state.1, 0); let buffer = String::from_utf8_lossy(writer.get_ref()); let buffer = buffer.trim_end_matches(char::from(0)); assert_eq!(buffer, "QWE\r\nQWE\r\n123\r\n123\r\n"); session.send_line("WWW").unwrap(); let m = session.expect("WWW\r\n").unwrap(); assert_ne!(m.before(), b"123\r\n"); assert_eq!(m.before(), b""); } // #[cfg(unix)] // #[cfg(not(feature = "polling"))] // #[cfg(not(feature = "async"))] // #[test] // fn interact_stream_redirection() { // let commands = "Hello World\nIt works :)\n"; // let mut reader = ReaderWithDelayEof::new(commands, Duration::from_secs(4)); // let mut writer = io::Cursor::new(vec![0; 1024]); // let mut session = expectrl::spawn("cat").unwrap(); // let mut opts = expectrl::interact::InteractOptions::default(); // opts.interact(&mut session, &mut reader, &mut writer) // .unwrap(); // drop(opts); // let buffer = String::from_utf8_lossy(writer.get_ref()); // let buffer = buffer.trim_end_matches(char::from(0)); // assert_eq!(buffer, "Hello World\r\nIt works :)\r\n"); // } #[cfg(unix)] #[cfg(feature = "async")] #[test] fn interact_stream_redirection() { use expectrl::interact::InteractOptions; futures_lite::future::block_on(async { let commands = "Hello World\nIt works :)\n"; let reader = ReaderWithDelayEof::new(commands, Duration::from_secs(4)); let mut writer = io::Cursor::new(vec![0; 1024]); let mut session = expectrl::spawn("cat").unwrap(); session .interact(reader, &mut writer) .spawn(InteractOptions::default()) .await .unwrap(); let buffer = String::from_utf8_lossy(writer.get_ref()); let buffer = buffer.trim_end_matches(char::from(0)); assert_eq!( buffer, "Hello World\r\nIt works :)\r\nHello World\r\nIt works :)\r\n" ); }); } #[cfg(feature = "async")] #[test] fn interact_output_callback() { use expectrl::{ interact::{actions::lookup::Lookup, InteractOptions, InteractSession}, stream::stdin::Stdin, }; let mut session = expectrl::spawn("sleep 1 && echo 'Hello World'").unwrap(); let mut stdin = Stdin::open().unwrap(); let stdout = std::io::sink(); let mut otps = InteractOptions::new((0, Lookup::new())).on_output(|ctx| { if ctx.state.1.on(ctx.buf, ctx.eof, "World")?.is_some() { ctx.state.0 += 1; } Ok(false) }); let mut interact = InteractSession::new(&mut session, &mut stdin, stdout); futures_lite::future::block_on(interact.spawn(&mut otps)).unwrap(); let (state, _) = otps.into_inner(); stdin.close().unwrap(); // fixme: sometimes it's 0 // I guess because the process gets down to fast. assert!(matches!(state, 1 | 0), "{state:?}"); } struct ListReaderWithDelayedEof { lines: Vec, eof_timeout: Duration, now: Option, } impl ListReaderWithDelayedEof { #[cfg(not(feature = "async"))] fn new(lines: Vec, eof_timeout: Duration) -> Self { Self { lines, eof_timeout, now: None, } } } impl Read for ListReaderWithDelayedEof { fn read(&mut self, mut buf: &mut [u8]) -> io::Result { if self.now.is_none() { self.now = Some(Instant::now()); } if !self.lines.is_empty() { let line = self.lines.remove(0); buf.write_all(line.as_bytes())?; Ok(line.as_bytes().len()) } else if self.now.unwrap().elapsed() < self.eof_timeout { Err(io::Error::new(io::ErrorKind::WouldBlock, "")) } else { Ok(0) } } } #[cfg(unix)] impl std::os::unix::io::AsRawFd for ListReaderWithDelayedEof { fn as_raw_fd(&self) -> std::os::unix::prelude::RawFd { 0 } } struct ReaderWithDelayEof { inner: Cursor, fire_timeout: Duration, now: Instant, } impl ReaderWithDelayEof where T: AsRef<[u8]>, { fn new(buf: T, timeout: Duration) -> Self { Self { inner: Cursor::new(buf), now: Instant::now(), fire_timeout: timeout, } } } impl Read for ReaderWithDelayEof where T: AsRef<[u8]>, { fn read(&mut self, buf: &mut [u8]) -> io::Result { let n = self.inner.read(buf)?; if n == 0 && self.now.elapsed() < self.fire_timeout { Err(io::Error::new(io::ErrorKind::WouldBlock, "")) } else { Ok(n) } } } #[cfg(feature = "async")] impl futures_lite::AsyncRead for ReaderWithDelayEof where T: AsRef<[u8]> + Unpin, { fn poll_read( self: std::pin::Pin<&mut Self>, _cx: &mut std::task::Context<'_>, buf: &mut [u8], ) -> std::task::Poll> { let result = self.get_mut().read(buf); std::task::Poll::Ready(result) } } expectrl-0.7.1/tests/io.rs000064400000000000000000000434151046102023000136050ustar 00000000000000use expectrl::{Captures, ControlCode, Needle, Session}; use std::{process::Command, thread, time::Duration}; #[cfg(unix)] use expectrl::WaitStatus; #[cfg(feature = "async")] use futures_lite::{ future::block_on, io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt}, }; #[cfg(not(feature = "async"))] use std::io::{BufRead, Read, Write}; #[test] #[cfg(unix)] fn send_controll() { let mut proc = Session::spawn(Command::new("cat")).unwrap(); _p_send_control(&mut proc, ControlCode::EOT).unwrap(); assert_eq!( proc.get_process().wait().unwrap(), WaitStatus::Exited(proc.get_process().pid(), 0), ); } #[test] #[cfg(windows)] fn send_controll() { let mut proc = Session::spawn(Command::new("powershell -C ping localhost")).unwrap(); // give powershell a bit time thread::sleep(Duration::from_millis(100)); _p_send_control(&mut proc, ControlCode::ETX).unwrap(); assert!({ let code = proc.get_process().wait(None).unwrap(); code == 0 || code == 3221225786 }); } #[test] #[cfg(unix)] fn send() { let mut proc = Session::spawn(Command::new("cat")).unwrap(); _p_send(&mut proc, "hello cat\n").unwrap(); // give cat a time to react on input thread::sleep(Duration::from_millis(100)); let mut buf = vec![0; 128]; let n = _p_read(&mut proc, &mut buf).unwrap(); assert_eq!(&buf[..n], b"hello cat\r\n"); assert!(proc.get_process_mut().exit(true).unwrap()); } #[test] #[cfg(windows)] fn send() { let mut proc = Session::spawn(Command::new("python ./tests/actions/cat/main.py")).unwrap(); _p_send(&mut proc, "hello cat\r\n").unwrap(); _p_expect(&mut proc, "hello cat").unwrap(); proc.get_process_mut().exit(0).unwrap(); } #[test] #[cfg(unix)] fn send_line() { let mut proc = Session::spawn(Command::new("cat")).unwrap(); _p_send_line(&mut proc, "hello cat").unwrap(); // give cat a time to react on input thread::sleep(Duration::from_millis(100)); let mut buf = vec![0; 128]; let n = _p_read(&mut proc, &mut buf).unwrap(); assert_eq!(&buf[..n], b"hello cat\r\n"); assert!(proc.get_process_mut().exit(true).unwrap()); } #[test] #[cfg(windows)] fn send_line() { let mut proc = Session::spawn(Command::new("python ./tests/actions/cat/main.py")).unwrap(); _p_send_line(&mut proc, "hello cat").unwrap(); _p_expect(&mut proc, "hello cat").unwrap(); proc.get_process_mut().exit(0).unwrap(); } #[test] #[cfg(unix)] fn try_read_by_byte() { let mut proc = Session::spawn(Command::new("cat")).unwrap(); assert_eq!( _p_try_read(&mut proc, &mut [0; 1]).unwrap_err().kind(), std::io::ErrorKind::WouldBlock ); _p_send_line(&mut proc, "123").unwrap(); // give cat a time to react on input thread::sleep(Duration::from_millis(100)); let mut buf = [0; 1]; _p_try_read(&mut proc, &mut buf).unwrap(); assert_eq!(&buf, &[b'1']); _p_try_read(&mut proc, &mut buf).unwrap(); assert_eq!(&buf, &[b'2']); _p_try_read(&mut proc, &mut buf).unwrap(); assert_eq!(&buf, &[b'3']); _p_try_read(&mut proc, &mut buf).unwrap(); assert_eq!(&buf, &[b'\r']); _p_try_read(&mut proc, &mut buf).unwrap(); assert_eq!(&buf, &[b'\n']); assert_eq!( _p_try_read(&mut proc, &mut buf).unwrap_err().kind(), std::io::ErrorKind::WouldBlock ); } #[test] #[cfg(windows)] #[cfg(not(feature = "async"))] fn try_read_by_byte() { // it shows that on windows ECHO is turned on. // Mustn't it be turned down? let mut proc = Session::spawn(Command::new("powershell")).unwrap(); _p_send_line( &mut proc, "while (1) { read-host | set r; if (!$r) { break }}", ) .unwrap(); _p_read_until(&mut proc, b'}').unwrap(); _p_read_line(&mut proc).unwrap(); _p_send_line(&mut proc, "123").unwrap(); thread::sleep(Duration::from_millis(500)); _p_read_until(&mut proc, b'1').unwrap(); let mut buf = [0; 1]; _p_try_read(&mut proc, &mut buf).unwrap(); assert_eq!(&buf, &[b'2']); _p_try_read(&mut proc, &mut buf).unwrap(); assert_eq!(&buf, &[b'3']); _p_try_read(&mut proc, &mut buf).unwrap(); assert_eq!(&buf, &[b'\r']); _p_try_read(&mut proc, &mut buf).unwrap(); assert_eq!(&buf, &[b'\n']); } #[test] #[cfg(unix)] fn blocking_read_after_non_blocking() { let mut proc = Session::spawn(Command::new("cat")).unwrap(); assert!(_p_is_empty(&mut proc).unwrap()); _p_send_line(&mut proc, "123").unwrap(); // give cat a time to react on input thread::sleep(Duration::from_millis(100)); let mut buf = [0; 1]; _p_try_read(&mut proc, &mut buf).unwrap(); assert_eq!(&buf, &[b'1']); let mut buf = [0; 64]; let n = _p_read(&mut proc, &mut buf).unwrap(); assert_eq!(&buf[..n], b"23\r\n"); thread::spawn(move || { let _ = _p_read(&mut proc, &mut buf).unwrap(); // the error will be propagated in case of panic panic!("it's unnexpected that read operation will be ended") }); // give some time to read thread::sleep(Duration::from_millis(100)); } #[test] #[cfg(windows)] fn blocking_read_after_non_blocking() { let mut proc = Session::spawn(Command::new("powershell")).unwrap(); _p_send_line( &mut proc, "while (1) { read-host | set r; if (!$r) { break }}", ) .unwrap(); thread::sleep(Duration::from_millis(300)); _p_send_line(&mut proc, "123").unwrap(); thread::sleep(Duration::from_millis(1000)); assert!(do_until( || { thread::sleep(Duration::from_millis(50)); _p_try_read(&mut proc, &mut [0; 1]).is_ok() }, Duration::from_secs(3) )); let mut buf = [0; 64]; let n = _p_read(&mut proc, &mut buf).unwrap(); assert!(n > 0); } #[test] #[cfg(unix)] fn try_read() { let mut proc = Session::spawn(Command::new("cat")).unwrap(); let mut buf = vec![0; 128]; assert_eq!( _p_try_read(&mut proc, &mut buf).unwrap_err().kind(), std::io::ErrorKind::WouldBlock ); _p_send_line(&mut proc, "123").unwrap(); // give cat a time to react on input thread::sleep(Duration::from_millis(100)); assert_eq!(_p_try_read(&mut proc, &mut buf).unwrap(), 5); assert_eq!(&buf[..5], b"123\r\n"); assert_eq!( _p_try_read(&mut proc, &mut buf).unwrap_err().kind(), std::io::ErrorKind::WouldBlock ); } #[test] #[cfg(windows)] fn try_read() { let mut proc = Session::spawn(Command::new("powershell")).unwrap(); thread::sleep(Duration::from_millis(300)); _p_send_line( &mut proc, "while (1) { read-host | set r; if (!$r) { break }}", ) .unwrap(); thread::sleep(Duration::from_millis(500)); _p_send_line(&mut proc, "123").unwrap(); _p_send_line(&mut proc, "123").unwrap(); // give cat a time to react on input thread::sleep(Duration::from_millis(1500)); assert!(do_until( || { thread::sleep(Duration::from_millis(50)); let mut buf = vec![0; 128]; let _ = _p_try_read(&mut proc, &mut buf); if String::from_utf8_lossy(&buf).contains("123") { true } else { false } }, Duration::from_secs(5) )); } #[test] #[cfg(unix)] fn blocking_read_after_non_blocking_try_read() { let mut proc = Session::spawn(Command::new("cat")).unwrap(); let mut buf = vec![0; 1]; assert_eq!( _p_try_read(&mut proc, &mut buf).unwrap_err().kind(), std::io::ErrorKind::WouldBlock ); _p_send_line(&mut proc, "123").unwrap(); // give cat a time to react on input thread::sleep(Duration::from_millis(100)); assert_eq!(_p_try_read(&mut proc, &mut buf).unwrap(), 1); assert_eq!(&buf[..1], b"1"); let mut buf = [0; 64]; let n = _p_read(&mut proc, &mut buf).unwrap(); assert_eq!(&buf[..n], b"23\r\n"); thread::spawn(move || { let _ = _p_read(&mut proc, &mut buf).unwrap(); // the error will be propagated in case of panic panic!("it's unnexpected that read operation will be ended") }); // give some time to read thread::sleep(Duration::from_millis(100)); } #[cfg(unix)] #[test] fn try_read_after_eof() { let mut proc = Session::spawn(Command::new("cat")).unwrap(); _p_send_line(&mut proc, "hello").unwrap(); // give cat a time to react on input thread::sleep(Duration::from_millis(100)); let mut buf = vec![0; 128]; assert_eq!(_p_try_read(&mut proc, &mut buf).unwrap(), 7); assert_eq!( _p_try_read(&mut proc, &mut buf).unwrap_err().kind(), std::io::ErrorKind::WouldBlock ); assert!(_p_is_empty(&mut proc).unwrap()); } #[test] #[cfg(unix)] fn try_read_after_process_exit() { let mut command = Command::new("echo"); command.arg("hello cat"); let mut proc = Session::spawn(command).unwrap(); assert_eq!( proc.get_process().wait().unwrap(), WaitStatus::Exited(proc.get_process().pid(), 0) ); #[cfg(target_os = "linux")] assert_eq!(_p_try_read(&mut proc, &mut [0; 128]).unwrap(), 11); #[cfg(not(target_os = "linux"))] assert_eq!(_p_try_read(&mut proc, &mut [0; 128]).unwrap(), 0); assert_eq!(_p_try_read(&mut proc, &mut [0; 128]).unwrap(), 0); assert!(_p_is_empty(&mut proc).unwrap()); // // on macos we may not able to read after process is dead. // // I assume that kernel consumes proceses resorces without any code check of parent, // // which what is happening on linux. // // // // So we check that there may be None or Some(0) // // on macos we can't put it before read's for some reason something get blocked // // assert_eq!(proc.wait().unwrap(), WaitStatus::Exited(proc.pid(), 0)); } #[cfg(windows)] #[test] fn try_read_after_process_exit() { use std::io::ErrorKind; let mut proc = Session::spawn(Command::new("cmd /C echo hello cat")).unwrap(); assert_eq!(proc.get_process().wait(None).unwrap(), 0); let now = std::time::Instant::now(); loop { if now.elapsed() > Duration::from_secs(2) { panic!("didn't read what expected") } match _p_try_read(&mut proc, &mut [0; 128]) { Ok(n) => { assert!(n > 0); assert!(_p_try_read(&mut proc, &mut [0; 128]).is_err()); assert!(_p_try_read(&mut proc, &mut [0; 128]).is_err()); assert!(_p_is_empty(&mut proc).unwrap()); assert_eq!(proc.get_process().wait(None).unwrap(), 0); return; } Err(err) => { if err.kind() == ErrorKind::WouldBlock { continue; } panic!("unexpected error {:?}", err); } } } } #[test] #[cfg(unix)] fn try_read_to_end() { let mut cmd = Command::new("echo"); cmd.arg("Hello World"); let mut proc = Session::spawn(cmd).unwrap(); let mut buf: Vec = Vec::new(); loop { let mut b = [0; 128]; match _p_try_read(&mut proc, &mut b) { Ok(0) => break, Ok(n) => buf.extend(&b[..n]), Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {} Err(err) => Err(err).unwrap(), } } assert_eq!(&buf[..13], b"Hello World\r\n"); } #[test] #[cfg(windows)] fn try_read_to_end() { let mut proc = Session::spawn(Command::new( "python ./tests/actions/echo/main.py Hello World", )) .unwrap(); let mut buf: Vec = Vec::new(); let now = std::time::Instant::now(); while now.elapsed() < Duration::from_secs(1) { let mut b = [0; 1]; match _p_try_read(&mut proc, &mut b) { Ok(n) => buf.extend(&b[..n]), Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => (), Err(err) => Err(err).unwrap(), } } assert!(String::from_utf8_lossy(&buf).contains("Hello World")); } #[test] #[cfg(windows)] fn continues_try_reads() { let cmd = Command::new("python3 -c \"import time; print('Start Sleep'); time.sleep(0.1); print('End of Sleep'); yn=input('input');\""); let mut proc = Session::spawn(cmd).unwrap(); let mut buf = [0; 128]; loop { if !proc.is_alive().unwrap() { panic!("Most likely python is not installed"); } match _p_try_read(&mut proc, &mut buf) { Ok(n) => { if String::from_utf8_lossy(&buf[..n]).contains("input") { break; } } Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {} Err(err) => Err(err).unwrap(), } } } #[test] #[cfg(not(target_os = "macos"))] #[cfg(not(windows))] fn automatic_stop_of_interact_on_eof() { let mut p = Session::spawn(Command::new("ls")).unwrap(); _p_interact(&mut p).unwrap(); // check that second spawn works let mut p = Session::spawn(Command::new("ls")).unwrap(); _p_interact(&mut p).unwrap(); } #[test] #[cfg(not(target_os = "macos"))] #[cfg(not(windows))] fn spawn_after_interact() { let mut p = Session::spawn(Command::new("ls")).unwrap(); _p_interact(&mut p).unwrap(); let p = Session::spawn(Command::new("ls")).unwrap(); assert!(matches!( p.get_process().wait().unwrap(), WaitStatus::Exited(_, 0) )); } #[test] #[cfg(unix)] fn read_line_test() { let mut proc = Session::spawn(Command::new("cat")).unwrap(); // give cat a time to react on input thread::sleep(Duration::from_millis(100)); _p_send_line(&mut proc, "123").unwrap(); thread::sleep(Duration::from_millis(100)); let line = _p_read_line(&mut proc).unwrap(); assert_eq!(&line, "123\r\n"); proc.get_process_mut().exit(true).unwrap(); } fn _p_read(proc: &mut Session, buf: &mut [u8]) -> std::io::Result { #[cfg(not(feature = "async"))] { proc.read(buf) } #[cfg(feature = "async")] { block_on(proc.read(buf)) } } fn _p_write_all(proc: &mut Session, buf: &[u8]) -> std::io::Result<()> { #[cfg(not(feature = "async"))] { proc.write_all(buf) } #[cfg(feature = "async")] { block_on(proc.write_all(buf)) } } fn _p_flush(proc: &mut Session) -> std::io::Result<()> { #[cfg(not(feature = "async"))] { proc.flush() } #[cfg(feature = "async")] { block_on(proc.flush()) } } fn _p_send(proc: &mut Session, buf: &str) -> std::io::Result<()> { #[cfg(not(feature = "async"))] { proc.send(buf) } #[cfg(feature = "async")] { block_on(proc.send(buf)) } } fn _p_expect(proc: &mut Session, n: impl Needle) -> Result { #[cfg(not(feature = "async"))] { proc.expect(n) } #[cfg(feature = "async")] { block_on(proc.expect(n)) } } fn _p_send_line(proc: &mut Session, buf: &str) -> std::io::Result<()> { #[cfg(not(feature = "async"))] { proc.send_line(buf) } #[cfg(feature = "async")] { block_on(proc.send_line(buf)) } } fn _p_send_control(proc: &mut Session, buf: impl Into) -> std::io::Result<()> { #[cfg(not(feature = "async"))] { proc.send(buf.into()) } #[cfg(feature = "async")] { block_on(proc.send(buf.into())) } } fn _p_read_to_string(proc: &mut Session) -> std::io::Result { let mut buf = String::new(); #[cfg(not(feature = "async"))] { proc.read_to_string(&mut buf)?; } #[cfg(feature = "async")] { block_on(proc.read_to_string(&mut buf))?; } Ok(buf) } fn _p_read_to_end(proc: &mut Session) -> std::io::Result> { let mut buf = Vec::new(); #[cfg(not(feature = "async"))] { proc.read_to_end(&mut buf)?; } #[cfg(feature = "async")] { block_on(proc.read_to_end(&mut buf))?; } Ok(buf) } fn _p_read_until(proc: &mut Session, ch: u8) -> std::io::Result> { let mut buf = Vec::new(); #[cfg(not(feature = "async"))] { let n = proc.read_until(ch, &mut buf)?; buf = buf[..n].to_vec(); } #[cfg(feature = "async")] { let n = block_on(proc.read_until(ch, &mut buf))?; buf = buf[..n].to_vec(); } Ok(buf) } fn _p_read_line(proc: &mut Session) -> std::io::Result { let mut buf = String::new(); #[cfg(not(feature = "async"))] { proc.read_line(&mut buf)?; } #[cfg(feature = "async")] { block_on(proc.read_line(&mut buf))?; } Ok(buf) } fn _p_is_empty(proc: &mut Session) -> std::io::Result { #[cfg(not(feature = "async"))] { proc.is_empty() } #[cfg(feature = "async")] { block_on(proc.is_empty()) } } fn _p_try_read(proc: &mut Session, buf: &mut [u8]) -> std::io::Result { #[cfg(not(feature = "async"))] { proc.try_read(buf) } #[cfg(feature = "async")] { block_on(async { futures_lite::future::poll_once(proc.read(buf)) .await .unwrap_or(Err(std::io::Error::new(std::io::ErrorKind::WouldBlock, ""))) }) } } #[cfg(unix)] fn _p_interact(proc: &mut Session) -> Result<(), expectrl::Error> { use expectrl::{interact::InteractOptions, stream::stdin::Stdin}; use std::io::stdout; let mut stdin = Stdin::open()?; let stdout = stdout(); #[cfg(not(feature = "async"))] { proc.interact(&mut stdin, stdout) .spawn(InteractOptions::default())?; } #[cfg(feature = "async")] { block_on( proc.interact(&mut stdin, stdout) .spawn(InteractOptions::default()), )?; } stdin.close() } #[cfg(windows)] fn do_until(mut foo: impl FnMut() -> bool, timeout: Duration) -> bool { let now = std::time::Instant::now(); while now.elapsed() < timeout { if foo() { return true; } } return false; } expectrl-0.7.1/tests/is_matched.rs000064400000000000000000000145651046102023000153020ustar 00000000000000#![cfg(unix)] use expectrl::{spawn, Eof, NBytes, Regex, WaitStatus}; use std::thread; use std::time::Duration; #[cfg(unix)] #[cfg(not(feature = "async"))] #[test] fn is_matched_str() { let mut session = spawn("cat").unwrap(); session.send_line("Hello World").unwrap(); thread::sleep(Duration::from_millis(600)); assert!(session.is_matched("Hello World").unwrap()); } #[cfg(unix)] #[cfg(feature = "async")] #[test] fn is_matched_str() { futures_lite::future::block_on(async { let mut session = spawn("cat").unwrap(); session.send_line("Hello World").await.unwrap(); thread::sleep(Duration::from_millis(600)); assert!(session.is_matched("Hello World").await.unwrap()); }) } #[cfg(unix)] #[cfg(not(feature = "async"))] #[test] fn is_matched_regex() { let mut session = spawn("cat").unwrap(); session.send_line("Hello World").unwrap(); thread::sleep(Duration::from_millis(600)); assert!(session.is_matched(Regex("lo.*")).unwrap()); } #[cfg(unix)] #[cfg(feature = "async")] #[test] fn is_matched_regex() { futures_lite::future::block_on(async { let mut session = spawn("cat").unwrap(); session.send_line("Hello World").await.unwrap(); thread::sleep(Duration::from_millis(600)); assert!(session.is_matched(Regex("lo.*")).await.unwrap()); }) } #[cfg(unix)] #[cfg(not(feature = "async"))] #[test] fn is_matched_bytes() { let mut session = spawn("cat").unwrap(); session.send_line("Hello World").unwrap(); thread::sleep(Duration::from_millis(600)); assert!(session.is_matched(NBytes(3)).unwrap()); } #[cfg(unix)] #[cfg(feature = "async")] #[test] fn is_matched_n_bytes() { futures_lite::future::block_on(async { let mut session = spawn("cat").unwrap(); session.send_line("Hello World").await.unwrap(); thread::sleep(Duration::from_millis(600)); assert!(session.is_matched(NBytes(3)).await.unwrap()); }) } #[cfg(target_os = "linux")] #[cfg(not(feature = "async"))] #[test] fn is_matched_eof() { let mut session = spawn("echo 'Hello World'").unwrap(); assert_eq!( session.get_process().wait().unwrap(), WaitStatus::Exited(session.get_process().pid(), 0), ); assert!(session.is_matched(Eof).unwrap()); } #[cfg(target_os = "linux")] #[cfg(feature = "async")] #[test] fn is_matched_eof() { futures_lite::future::block_on(async { let mut session = spawn("echo 'Hello World'").unwrap(); assert_eq!( WaitStatus::Exited(session.pid(), 0), session.wait().unwrap() ); assert!(!session.is_matched(Eof).await.unwrap()); assert!(session.is_matched(Eof).await.unwrap()); }) } #[cfg(unix)] #[cfg(not(feature = "async"))] #[test] fn read_after_is_matched() { use std::io::Read; let mut session = spawn("cat").unwrap(); session.send_line("Hello World").unwrap(); thread::sleep(Duration::from_millis(600)); assert!(session.is_matched("Hello").unwrap()); // we stop process so read operation will end up with EOF. // other wise read call would block. session.get_process_mut().exit(false).unwrap(); let mut buf = [0; 128]; let n = session.read(&mut buf).unwrap(); assert_eq!(&buf[..n], b"Hello World\r\n"); } #[cfg(unix)] #[cfg(feature = "async")] #[test] fn read_after_is_matched() { use futures_lite::io::AsyncReadExt; futures_lite::future::block_on(async { let mut session = spawn("cat").unwrap(); session.send_line("Hello World").await.unwrap(); thread::sleep(Duration::from_millis(600)); assert!(session.is_matched("Hello").await.unwrap()); // we stop process so read operation will end up with EOF. // other wise read call would block. session.get_process_mut().exit(false).unwrap(); let mut buf = [0; 128]; let n = session.read(&mut buf).await.unwrap(); assert_eq!(&buf[..n], b"Hello World\r\n"); }) } #[cfg(target_os = "linux")] #[cfg(not(feature = "async"))] #[test] fn check_after_is_matched_eof() { let mut p = spawn("echo AfterSleep").expect("cannot run echo"); assert_eq!( WaitStatus::Exited(p.get_process().pid(), 0), p.get_process().wait().unwrap() ); assert!(p.is_matched(Eof).unwrap()); let m = p.check(Eof).unwrap(); #[cfg(target_os = "linux")] assert_eq!(m.get(0).unwrap(), b"AfterSleep\r\n"); #[cfg(not(target_os = "linux"))] assert_eq!(m.get(0).unwrap(), b""); } #[cfg(target_os = "linux")] #[cfg(feature = "async")] #[test] fn check_after_is_matched_eof() { futures_lite::future::block_on(async { let mut p = spawn("echo AfterSleep").expect("cannot run echo"); assert_eq!(WaitStatus::Exited(p.pid(), 0), p.wait().unwrap()); assert!(!p.is_matched(Eof).await.unwrap()); assert!(p.is_matched(Eof).await.unwrap()); let m = p.check(Eof).await.unwrap(); #[cfg(target_os = "linux")] assert_eq!(m.get(0).unwrap(), b"AfterSleep\r\n"); #[cfg(not(target_os = "linux"))] assert!(m.matches().len() == 0); }) } #[cfg(target_os = "linux")] #[cfg(not(feature = "async"))] #[test] fn expect_after_is_matched_eof() { let mut p = spawn("echo AfterSleep").expect("cannot run echo"); assert_eq!( WaitStatus::Exited(p.get_process().pid(), 0), p.get_process().wait().unwrap() ); assert!(p.is_matched(Eof).unwrap()); let m = p.expect(Eof).unwrap(); #[cfg(target_os = "linux")] assert_eq!(m.get(0).unwrap(), b"AfterSleep\r\n"); #[cfg(not(target_os = "linux"))] assert_eq!(m.get(0).unwrap(), b""); assert!(matches!(p.expect("").unwrap_err(), expectrl::Error::Eof)); } #[cfg(target_os = "linux")] #[cfg(feature = "async")] #[test] fn expect_after_is_matched_eof() { futures_lite::future::block_on(async { let mut p = spawn("echo AfterSleep").expect("cannot run echo"); assert_eq!(WaitStatus::Exited(p.pid(), 0), p.wait().unwrap()); assert!(!p.is_matched(Eof).await.unwrap()); assert!(p.is_matched(Eof).await.unwrap()); let m = p.expect(Eof).await.unwrap(); #[cfg(target_os = "linux")] assert_eq!(m.get(0).unwrap(), b"AfterSleep\r\n"); #[cfg(not(target_os = "linux"))] assert!(m.matches().len() == 0); assert!(matches!( p.expect("").await.unwrap_err(), expectrl::Error::Eof )); }) } expectrl-0.7.1/tests/log.rs000064400000000000000000000116621046102023000137560ustar 00000000000000use std::{ io::{self, prelude::*, Cursor}, sync::{Arc, Mutex}, thread, time::Duration, }; #[cfg(feature = "async")] use futures_lite::AsyncBufReadExt; #[cfg(feature = "async")] use futures_lite::AsyncReadExt; use expectrl::session; use expectrl::spawn; #[test] #[cfg(windows)] #[cfg(not(feature = "async"))] fn log() { let writer = StubWriter::default(); let mut session = session::log( spawn("python ./tests/actions/cat/main.py").unwrap(), writer.clone(), ) .unwrap(); thread::sleep(Duration::from_millis(300)); session.send_line("Hello World").unwrap(); thread::sleep(Duration::from_millis(300)); let mut buf = vec![0; 1024]; let _ = session.read(&mut buf).unwrap(); let bytes = writer.inner.lock().unwrap(); let log_str = String::from_utf8_lossy(bytes.get_ref()); assert!(log_str.as_ref().contains("write")); assert!(log_str.as_ref().contains("read")); } #[test] #[cfg(windows)] #[cfg(feature = "async")] fn log() { futures_lite::future::block_on(async { let writer = StubWriter::default(); let mut session = session::log( spawn("python ./tests/actions/cat/main.py").unwrap(), writer.clone(), ) .unwrap(); thread::sleep(Duration::from_millis(300)); session.send_line("Hello World").await.unwrap(); thread::sleep(Duration::from_millis(300)); let mut buf = vec![0; 1024]; let _ = session.read(&mut buf).await.unwrap(); let bytes = writer.inner.lock().unwrap(); let log_str = String::from_utf8_lossy(bytes.get_ref()); assert!(log_str.as_ref().contains("write")); assert!(log_str.as_ref().contains("read")); }); } #[test] #[cfg(unix)] fn log() { let writer = StubWriter::default(); #[cfg(feature = "async")] futures_lite::future::block_on(async { let mut session = session::log(spawn("cat").unwrap(), writer.clone()).unwrap(); session.send_line("Hello World").await.unwrap(); // give some time to cat // since sometimes we doesn't keep up to read whole string thread::sleep(Duration::from_millis(300)); let mut buf = vec![0; 1024]; let _ = session.read(&mut buf).await.unwrap(); let bytes = writer.inner.lock().unwrap(); let text = String::from_utf8_lossy(bytes.get_ref()); assert!( text.contains("read") && text.contains("write"), "unexpected output {text:?}" ); }); #[cfg(not(feature = "async"))] { let mut session = session::log(spawn("cat").unwrap(), writer.clone()).unwrap(); session.send_line("Hello World").unwrap(); // give some time to cat // since sometimes we doesn't keep up to read whole string thread::sleep(Duration::from_millis(300)); let mut buf = vec![0; 1024]; let _ = session.read(&mut buf).unwrap(); let bytes = writer.inner.lock().unwrap(); let text = String::from_utf8_lossy(bytes.get_ref()); assert!( text.contains("read") && text.contains("write"), "unexpected output {text:?}" ); } } #[test] #[cfg(unix)] fn log_read_line() { let writer = StubWriter::default(); #[cfg(feature = "async")] futures_lite::future::block_on(async { let mut session = session::log(spawn("cat").unwrap(), writer.clone()).unwrap(); session.send_line("Hello World").await.unwrap(); let mut buf = String::new(); let _ = session.read_line(&mut buf).await.unwrap(); assert_eq!(buf, "Hello World\r\n"); let bytes = writer.inner.lock().unwrap(); let text = String::from_utf8_lossy(bytes.get_ref()); assert!( text.contains("read") && text.contains("write"), "unexpected output {text:?}" ); }); #[cfg(not(feature = "async"))] { let mut session = session::log(spawn("cat").unwrap(), writer.clone()).unwrap(); session.send_line("Hello World").unwrap(); let mut buf = String::new(); let _ = session.read_line(&mut buf).unwrap(); assert_eq!(buf, "Hello World\r\n"); let bytes = writer.inner.lock().unwrap(); let text = String::from_utf8_lossy(bytes.get_ref()); if !matches!( text.as_ref(), "write: \"Hello World\\n\"\nread: \"Hello World\"\nread: \"\\r\\n\"\n" | "write: \"Hello World\\n\"\nread: \"Hello World\\r\\n\"\n" | "write: \"Hello World\"\nwrite: \"\\n\"\nread: \"Hello World\\r\\n\"\n", ) { panic!("unexpected output {text:?}"); } } } #[derive(Debug, Clone, Default)] struct StubWriter { inner: Arc>>>, } impl Write for StubWriter { fn write(&mut self, buf: &[u8]) -> io::Result { self.inner.lock().unwrap().write(buf) } fn flush(&mut self) -> io::Result<()> { self.inner.lock().unwrap().flush() } } expectrl-0.7.1/tests/repl.rs000064400000000000000000000144641046102023000141420ustar 00000000000000#![cfg(unix)] use expectrl::{ repl::{spawn_bash, spawn_python}, ControlCode, WaitStatus, }; #[cfg(feature = "async")] use futures_lite::io::AsyncBufReadExt; #[cfg(not(feature = "async"))] use std::io::BufRead; use std::{thread, time::Duration}; #[cfg(not(feature = "async"))] #[cfg(target_os = "linux")] #[test] fn bash() { let mut p = spawn_bash().unwrap(); p.send_line("echo Hello World").unwrap(); let mut msg = String::new(); p.read_line(&mut msg).unwrap(); assert!(msg.ends_with("Hello World\r\n")); p.send(ControlCode::EOT).unwrap(); p.get_process_mut().exit(true).unwrap(); } #[cfg(not(feature = "async"))] #[cfg(target_os = "linux")] #[test] fn bash_with_log() { use expectrl::{repl::ReplSession, session}; let p = spawn_bash().unwrap(); let prompt = p.get_prompt().to_owned(); let quit_cmd = p.get_quit_command().map(|c| c.to_owned()); let is_echo = p.is_echo(); let session = session::log(p.into_session(), std::io::stderr()).unwrap(); let mut p = ReplSession::new(session, prompt, quit_cmd, is_echo); p.send_line("echo Hello World").unwrap(); let mut msg = String::new(); p.read_line(&mut msg).unwrap(); assert!(msg.ends_with("Hello World\r\n")); thread::sleep(Duration::from_millis(300)); p.send(ControlCode::EOT).unwrap(); assert_eq!( p.get_process().wait().unwrap(), WaitStatus::Exited(p.get_process().pid(), 0) ); } #[cfg(feature = "async")] #[cfg(not(target_os = "macos"))] #[test] fn bash() { futures_lite::future::block_on(async { let mut p = spawn_bash().await.unwrap(); p.send_line("echo Hello World").await.unwrap(); let mut msg = String::new(); p.read_line(&mut msg).await.unwrap(); assert!(msg.ends_with("Hello World\r\n")); thread::sleep(Duration::from_millis(300)); p.send(ControlCode::EOT).await.unwrap(); assert_eq!(p.wait().unwrap(), WaitStatus::Exited(p.pid(), 0)); }) } #[cfg(feature = "async")] #[cfg(not(target_os = "macos"))] #[test] fn bash_with_log() { futures_lite::future::block_on(async { use expectrl::{repl::ReplSession, session}; let p = spawn_bash().await.unwrap(); let prompt = p.get_prompt().to_owned(); let quit_cmd = p.get_quit_command().map(|c| c.to_owned()); let is_echo = p.is_echo(); let session = session::log(p.into_session(), std::io::stderr()).unwrap(); let mut p = ReplSession::new(session, prompt, quit_cmd, is_echo); p.send_line("echo Hello World").await.unwrap(); let mut msg = String::new(); p.read_line(&mut msg).await.unwrap(); assert!(msg.ends_with("Hello World\r\n")); thread::sleep(Duration::from_millis(300)); p.send(ControlCode::EOT).await.unwrap(); assert_eq!(p.wait().unwrap(), WaitStatus::Exited(p.pid(), 0)); }) } #[cfg(not(feature = "async"))] #[test] fn python() { let mut p = spawn_python().unwrap(); let prompt = p.execute("print('Hello World')").unwrap(); let prompt = String::from_utf8_lossy(&prompt); assert!(prompt.contains("Hello World"), "{prompt:?}"); thread::sleep(Duration::from_millis(300)); p.send(ControlCode::EndOfText).unwrap(); thread::sleep(Duration::from_millis(300)); let mut msg = String::new(); p.read_line(&mut msg).unwrap(); assert!(msg.contains("\r\n"), "{msg:?}"); let mut msg = String::new(); p.read_line(&mut msg).unwrap(); assert_eq!(msg, "KeyboardInterrupt\r\n"); p.expect_prompt().unwrap(); p.send(ControlCode::EndOfTransmission).unwrap(); assert_eq!( p.get_process().wait().unwrap(), WaitStatus::Exited(p.get_process().pid(), 0) ); } #[cfg(feature = "async")] #[test] fn python() { futures_lite::future::block_on(async { let mut p = spawn_python().await.unwrap(); let prompt = p.execute("print('Hello World')").await.unwrap(); let prompt = String::from_utf8_lossy(&prompt); assert!(prompt.contains("Hello World"), "{prompt:?}"); thread::sleep(Duration::from_millis(300)); p.send(ControlCode::EndOfText).await.unwrap(); thread::sleep(Duration::from_millis(300)); let mut msg = String::new(); p.read_line(&mut msg).await.unwrap(); assert!(msg.contains("\r\n"), "{msg:?}"); let mut msg = String::new(); p.read_line(&mut msg).await.unwrap(); assert_eq!(msg, "KeyboardInterrupt\r\n"); p.expect_prompt().await.unwrap(); p.send(ControlCode::EndOfTransmission).await.unwrap(); assert_eq!(p.wait().unwrap(), WaitStatus::Exited(p.pid(), 0)); }) } #[cfg(feature = "async")] #[test] fn bash_pwd() { futures_lite::future::block_on(async { let mut p = spawn_bash().await.unwrap(); p.execute("cd /tmp/").await.unwrap(); p.send_line("pwd").await.unwrap(); let mut pwd = String::new(); p.read_line(&mut pwd).await.unwrap(); assert!(pwd.contains("/tmp\r\n")); }); } #[cfg(feature = "async")] #[test] fn bash_control_chars() { futures_lite::future::block_on(async { let mut p = spawn_bash().await.unwrap(); p.send_line("cat <(echo ready) -").await.unwrap(); thread::sleep(Duration::from_millis(100)); p.send(ControlCode::EndOfText).await.unwrap(); // abort: SIGINT p.expect_prompt().await.unwrap(); p.send_line("cat <(echo ready) -").await.unwrap(); thread::sleep(Duration::from_millis(100)); p.send(ControlCode::Substitute).await.unwrap(); // suspend:SIGTSTPcon p.expect_prompt().await.unwrap(); }); } #[cfg(not(feature = "async"))] #[test] fn bash_pwd() { let mut p = spawn_bash().unwrap(); p.execute("cd /tmp/").unwrap(); p.send_line("pwd").unwrap(); let mut pwd = String::new(); p.read_line(&mut pwd).unwrap(); assert!(pwd.contains("/tmp\r\n")); } #[cfg(not(feature = "async"))] #[test] fn bash_control_chars() { let mut p = spawn_bash().unwrap(); p.send_line("cat <(echo ready) -").unwrap(); thread::sleep(Duration::from_millis(300)); p.send(ControlCode::EndOfText).unwrap(); // abort: SIGINT p.expect_prompt().unwrap(); p.send_line("cat <(echo ready) -").unwrap(); thread::sleep(Duration::from_millis(100)); p.send(ControlCode::Substitute).unwrap(); // suspend:SIGTSTPcon p.expect_prompt().unwrap(); } expectrl-0.7.1/tests/session.rs000064400000000000000000000137521046102023000146620ustar 00000000000000use expectrl::{spawn, Session}; #[cfg(feature = "async")] use futures_lite::io::{AsyncReadExt, AsyncWriteExt}; #[cfg(not(feature = "async"))] #[cfg(not(windows))] use std::io::{Read, Write}; #[cfg(unix)] #[cfg(not(feature = "async"))] #[test] fn send() { let mut session = spawn("cat").unwrap(); session.send("Hello World").unwrap(); session.write_all(&[3]).unwrap(); // Ctrl+C session.flush().unwrap(); let mut buf = String::new(); session.read_to_string(&mut buf).unwrap(); // cat doesn't printed anything assert_eq!(buf, ""); } #[cfg(unix)] #[cfg(feature = "async")] #[test] fn send() { futures_lite::future::block_on(async { let mut session = spawn("cat").unwrap(); session.send("Hello World").await.unwrap(); session.write_all(&[3]).await.unwrap(); // Ctrl+C session.flush().await.unwrap(); let mut buf = String::new(); session.read_to_string(&mut buf).await.unwrap(); // cat doesn't printed anything assert_eq!(buf, ""); }) } #[cfg(windows)] #[test] fn send() { use std::io::Write; let mut session = spawn("python ./tests/actions/cat/main.py").unwrap(); #[cfg(not(feature = "async"))] { session.write(b"Hello World").unwrap(); session.expect("Hello World").unwrap(); } #[cfg(feature = "async")] { futures_lite::future::block_on(async { session.write(b"Hello World").await.unwrap(); session.expect("Hello World").await.unwrap(); }) } } #[cfg(unix)] #[cfg(not(feature = "async"))] #[test] fn send_multiline() { let mut session = spawn("cat").unwrap(); session.send("Hello World\n").unwrap(); let m = session.expect('\n').unwrap(); let buf = String::from_utf8_lossy(m.before()); assert_eq!(buf, "Hello World\r"); session.get_process_mut().exit(true).unwrap(); } #[cfg(unix)] #[cfg(feature = "async")] #[test] fn send_multiline() { futures_lite::future::block_on(async { let mut session = spawn("cat").unwrap(); session.send("Hello World\n").await.unwrap(); let m = session.expect('\n').await.unwrap(); let buf = String::from_utf8_lossy(m.before()); assert_eq!(buf, "Hello World\r"); session.get_process_mut().exit(true).unwrap(); }) } #[cfg(windows)] #[test] fn send_multiline() { let mut session = spawn("python ./tests/actions/cat/main.py").unwrap(); #[cfg(not(feature = "async"))] { session.send("Hello World\r\n").unwrap(); let m = session.expect('\n').unwrap(); let buf = String::from_utf8_lossy(m.before()); assert!(buf.contains("Hello World"), "{:?}", buf); session.get_process_mut().exit(0).unwrap(); } #[cfg(feature = "async")] { use futures_lite::{AsyncBufReadExt, StreamExt}; futures_lite::future::block_on(async { session.send("Hello World\r\n").await.unwrap(); let m = session.expect('\n').unwrap(); let buf = String::from_utf8_lossy(m.before()); assert!(buf.contains("Hello World"), "{:?}", buf); session.get_process_mut().exit(0).unwrap(); }) } } #[cfg(unix)] #[cfg(not(feature = "async"))] #[test] fn send_line() { let mut session = spawn("cat").unwrap(); session.send_line("Hello World").unwrap(); let m = session.expect('\n').unwrap(); let buf = String::from_utf8_lossy(m.before()); assert_eq!(buf, "Hello World\r"); session.get_process_mut().exit(true).unwrap(); } #[cfg(unix)] #[cfg(feature = "async")] #[test] fn send_line() { futures_lite::future::block_on(async { let mut session = spawn("cat").unwrap(); session.send_line("Hello World").await.unwrap(); let m = session.expect('\n').await.unwrap(); let buf = String::from_utf8_lossy(m.before()); assert_eq!(buf, "Hello World\r"); session.get_process_mut().exit(true).unwrap(); }) } #[cfg(windows)] #[test] fn send_line() { let mut session = spawn("python ./tests/actions/cat/main.py").unwrap(); #[cfg(not(feature = "async"))] { session.send_line("Hello World").unwrap(); let m = session.expect('\n').unwrap(); let buf = String::from_utf8_lossy(m.before()); assert!(buf.contains("Hello World"), "{:?}", buf); session.get_process_mut().exit(0).unwrap(); } #[cfg(feature = "async")] { use futures_lite::{AsyncBufReadExt, StreamExt}; futures_lite::future::block_on(async { session.send_line("Hello World").await.unwrap(); let m = session.expect('\n').unwrap(); let buf = String::from_utf8_lossy(m.before()); assert!(buf.contains("Hello World"), "{:?}", buf); session.get_process_mut().exit(0).unwrap(); }) } } #[test] fn test_spawn_no_command() { #[cfg(unix)] assert!(spawn("").is_err()); #[cfg(windows)] assert!(spawn("").is_ok()); } #[test] #[ignore = "it's a compile time check"] fn test_session_as_writer() { #[cfg(not(feature = "async"))] { let _: Box = Box::new(spawn("ls").unwrap()); let _: Box = Box::new(spawn("ls").unwrap()); let _: Box = Box::new(spawn("ls").unwrap()); fn _io_copy(mut session: Session) { let _ = std::io::copy(&mut std::io::empty(), &mut session).unwrap(); } } #[cfg(feature = "async")] { let _: Box = Box::new(spawn("ls").unwrap()) as Box; let _: Box = Box::new(spawn("ls").unwrap()) as Box; let _: Box = Box::new(spawn("ls").unwrap()) as Box; async fn _io_copy(mut session: Session) { futures_lite::io::copy(futures_lite::io::empty(), &mut session) .await .unwrap(); } } } expectrl-0.7.1/tests/source/ansi.py000064400000000000000000000026361046102023000154340ustar 00000000000000#!/usr/bin/env python3 # The file was developed by https://github.com/GaryBoone/ from colors import colorize, Color import sys import time import getpass NUM_LINES = 5 SAME_LINE_ESC = "\033[F" def main(): """Demonstrate several kinds of terminal outputs. Examples including ANSI codes, "\r" without "\n", writing to stdin, no-echo inputs. """ # Show a color. print("status: ", colorize("good", Color.GREEN)) # Show same-line output via "\r". for i in range(NUM_LINES): sys.stdout.write(f"[{i+1}/{NUM_LINES}]: file{i}\r") time.sleep(1) print("\n") # Show same-line output via an ANSI code. for i in range(NUM_LINES): print(f"{SAME_LINE_ESC}[{i+1}/{NUM_LINES}]: file{i}") time.sleep(1) # Handle prompts which don't repeat input to stdout. print("Here is a test password prompt") print(colorize("Do not enter a real password", Color.RED)) getpass.getpass() # Handle simple input. ans = input("Continue [y/n]:") col = Color.GREEN if ans == "y" else Color.RED print(f"You said: {colorize(ans, col)}") if ans == "n" or ans == "": sys.exit(0) # Handle long-running process, like starting a server. print("[Starting long running process...]") print("[Ctrl-C to exit]") while True: print("status: ", colorize("good", Color.GREEN)) time.sleep(1) if __name__ == "__main__": main() expectrl-0.7.1/tests/source/colors.py000064400000000000000000000005531046102023000157770ustar 00000000000000import enum class Color(enum.Enum): RESET = "\33[0m" RED = "\33[31m" GREEN = "\33[32m" YELLOW = "\33[33m" def colorize(text: str, color: Color) -> str: """Wrap `text` in terminal color directives. The return value will show up as the given color when printed in a terminal. """ return f"{color.value}{text}{Color.RESET.value}"