A tool for automating terminal applications in Unix.

Overview

Build coverage status crate docs.rs

expectrl

A tool for automating terminal applications in Unix.

Using the library you can:

  • Spawn process
  • Control process
  • Expect/Verify responces

It was inspired by philippkeller/rexpect and pexpect.

It supports async calls. To enable them you must turn on an async feature.

Usage

An example for interacting via ftp:

use expectrl::{spawn, Regex, Eof, WaitStatus};

fn main() {
    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));
}

Example bash with async feature

use expectrl::{repl::spawn_bash, Regex, Error, ControlCode};
use futures_lite::io::AsyncBufReadExt;

#[tokio::main]
fn main() -> Result<(), Error> {
    let mut p = spawn_bash().await?;

    p.send_line("hostname").await?;
    let mut hostname = String::new();
    p.read_line(&mut hostname).await?;
    p.expect_prompt().await?; // go sure `hostname` is really done
    println!("Current hostname: {:?}", hostname);

    Ok(())
}

Example with bash and job control

One frequent bitfall with sending signals is that you need to somehow ensure that the program has fully loaded, otherwise they goes into nowhere. There are 2 handy function execute for this purpouse:

  • execute - does a command and ensures that the prompt is shown again.
  • expect_prompt - ensures that the prompt is shown.
use expectrl::{repl::spawn_bash, Error, ControlCode};

fn main() -> Result<(), Error> {
    let mut p = spawn_bash()?;
    p.send_line("ping 8.8.8.8")?;
    p.expect("bytes of data")?;
    p.send_control(ControlCode::Substitute)?; // CTRL_Z
    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_control(ControlCode::EndOfText)?;
    p.expect("packet loss")?;

    Ok(())
}

Examples

For more examples, check the examples directory.

Comparison to philippkeller/rexpect

It will be fair to say that without it there would be no expectrl.

  • It has an async support.
  • It does a couple of inner things diferently.
  • It has a different interface.
  • It supports logging.
  • It supports interact function.
  • ...

Licensed under MIT License

Comments
  • Need non-blocking reads for interactive()

    Need non-blocking reads for interactive()

    I can't use expect() directly to read lines for line processing because I need non-blocking reads. Using non-blocking reads allows more control while waiting for data to be available. For example, I add an animated cursor if the data takes too long to appear.

    Here's the current code, roughly. It's loosely based on the expectrl source code using try_read here.

    The following code shows a number of useful expectrl features. Specifically, it:

    • Catches \r and \n as the command writes lines, allowing them to be rewritten. This is useful because some commands emit just \r to overwrite the previous line in a terminal rather than scrolling.
    • Allows lines to be read individually and processed.
    • Allows timeouts to be checked.
    • Allows other processing in the loop, such as starting an animated cursor or waiting message if the command is taking too long to emit any output.
        use expectrl::{Session, Signal, WaitStatus};
        use std::io::{stdout, Write};
        use std::{process::Command, thread, time::Duration, time::SystemTime};
    
        fn main() {
          let mut cmd = Command::new("ping");
          cmd.args(&["www.time.org"]);
    
          let mut session = Session::spawn(cmd).expect("failed to execute process");
          thread::sleep(Duration::from_millis(300)); // Give the process a moment to start.
    
          let mut buf = vec![0; 512]; // Line buffer.
          let mut b = [0; 1]; // Read buffer.
          let sys_time = SystemTime::now();
          loop {
            match session.try_read(&mut b) {
              Ok(0) => {
                println!("==> EoF");
                break; // EoF
              }
              Ok(_) => {
                buf.push(b[0]);
                if b[0] == 10 || b[0] == 13 {
                  stdout().write_all(&buf).unwrap();
                  stdout().flush().unwrap();
    
                  // (1) Further process the line as needed. 
                  // ...
    
                  buf.clear();
                }
              }
              Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {}
              Err(err) => {
                println!("err {}", err);
                break;
              }
            }
            // Check for timeout.
            // Start/update animated cursor if there's no output
          }
    
          match session.wait() {
            Ok(WaitStatus::Exited(pid, status)) => {
              println!("child process {} exited with: {:?}", pid, status);
            }
            // Signaled() is expected if the process is terminated.
            Ok(WaitStatus::Signaled(_, s, _)) => {
              println!("child process terminated with signal: {}", s);
            }
            Err(e) => {
              println!("error waiting for process: {:?}", e);
            }
            s => {
              println!("unhandled WaitStatus: {:?}", s);
            }
          }
        }
    

    This code works because Session provides a try_read() that doesn't block. Currently,expect() already provides many of these features, including timeouts and the Any() method to detect specific line endings. But expect() blocks. With expect() blocking, I would have to multithread cursor animation and synchronize terminal writes. Doable, but it adds complexity that the try_read() approach avoids.

    The problem: How to allow interactive typing into the terminal, with the line processing as above? There's interactive()but it can't be used in this context because it blocks.

    Possible solutions:

    1. Could there be a version of interactive() that emits lines for processing?
    2. Could there be a version of interactive() that accepts a passed-in function for line processing or other work? (eg, at (1) above).
    3. Could there be a try_interactive() allows interactivity, but checks for data written to stdout without blocking?
    opened by GaryBoone 59
  • Interact() shows passwords

    Interact() shows passwords

    Programs that ask for passwords don't repeat them as they're typed. For example,

    $ sudo ls
    Password:
    

    but doesn't show the characters typed by the user. Here's a program using expectrs to do the same:

    use expectrl::Session;
    use std::process::Command;
    
    fn main() {
        let mut cmd = Command::new("sudo");
        cmd.arg("ls");
        let mut session = Session::spawn_cmd(cmd).expect("Error while spawning");
        let status = session.interact().expect("Failed to start interact");
        println!("Exit status {:?}", status);
    }
    

    But it repeats whatever is typed.

    Running `target/debug/expectrl_test`
    Password: sdf
    

    In my bash shell, the Password: prompt is actually followed by a key symbol, which seems to be a property of bash, not sudo.The same effect is seen with read -s, which I believe is a bash built-in and might be useful for testing.

    opened by GaryBoone 21
  • session: add `expect_eager` method

    session: add `expect_eager` method

    hi, we're trying to migrate from rexpect to this crate in https://github.com/anoma/anoma/pull/1095. It was pretty smooth, thank you for this crate! We just hit couple road-blocks - one of them is that the lazy approach in Session::expect is too slow for processes with many lines of output. We get around it by adding this expect_eager variation.

    opened by tzemanovic 9
  • `interact()` seems consumes too much CPU

    `interact()` seems consumes too much CPU

    Example, following example will cause 100% CPU usage.

    fn main() {
        let shell = "bash";
        let mut sh = expectrl::spawn(shell.to_string()).expect("Error while spawning sh");
    
        println!("Connecting to {}", shell);
    
        sh.interact().unwrap();
    }
    
    opened by yinheli 8
  • doc sync session

    doc sync session

    hi again, just a small fix-up to get sync_session to show up in docs. There are bunch of other things with #[cfg(not(feature = "async"))] that get excluded, but because they share the same name in #[cfg(feature = "async")], enabling those doesn't work together :/

    opened by tzemanovic 8
  • Can't spawn() after interact()

    Can't spawn() after interact()

    Running another spawn() after calling interact() causes the spawn to fail with a Sys(EBADF) error.

    If the repro code below is run without the interact() function, it calls spawn() and prints the process info as expected:

    Running PtyProcess { master: Master { fd: PtyMaster(7) }, child_pid: Pid(97435), stream: 
    Stream { inner: File { fd: 9, path: "/dev/ptmx", read: true, write: true }, reader: BufReader { 
    reader: Reader { inner: File { fd: 10, path: "/dev/ptmx", read: true, write: true } }, buffer: 0/8192 
    } }, eof_char: 4, intr_char: 3, terminate_approach_delay: 100ms }
    

    However, if the interact() function is called, the subsequent call to spawn() fails:

    Cargo.lock	Cargo.toml	src		target
    Quiting status Exited(Pid(96414), 0)
    thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Sys(EBADF)', src/main.rs:20:42
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
    

    Repro code:

    use expectrl::Session;
    use ptyprocess::PtyProcess;
    use std::{process::Command, thread, time::Duration};
    
    fn main() {
        let cmd = Command::new("ls");
        let mut bash = Session::spawn_cmd(cmd).expect("Error while spawning bash");
        //
        // If these two lines are commented out, the spawn() below starts and the
        // process information is printed.
        //
        // If these two lines are enabled, the interact() succeeds and the exit code
        // 0 is printed, but the spawn() below panics.
        let status = bash.interact().expect("Failed to start interact");
        println!("Quiting status {:?}", status);
    
        // Now start another.
        thread::sleep(Duration::from_millis(500));   // Delay just in case.
    
        let cmd = Command::new("ls");
        let process = PtyProcess::spawn(cmd).unwrap();
        println!("Running {:?}", process);
    }
    

    Tested on macOs.

    opened by GaryBoone 8
  • Session::spawn() vs expectrl::spawn() issues with std::process::Command

    Session::spawn() vs expectrl::spawn() issues with std::process::Command

    expectrl::Session::spawn() accepts a std::process::Command, allowing some options like which directory to run in. Is it compatible with interact()? Or should expectrl::spawn also have a version that accepts a Command?

    This code works as expected, printing out the help for git diff, then exiting:

    let mut cmd = std::process::Command::new("git");
    cmd.args(["diff"]);
    cmd.current_dir("/tmp");
    let mut session = expectrl::Session::spawn(cmd).expect("Can't spawn a session");
    let mut opts = expectrl::interact::InteractOptions::terminal().unwrap();
    let res = opts.interact(&mut session).unwrap();
    

    But when a Command is used with the code in this comment of issue 13, Some unexpected behavior occurs.

    Specifically, (1) replacing

    let mut session = expectrl::spawn("./terminal_example.py").expect("Can't spawn a session");
    

    with

    let mut cmd = std::process::Command::new("git");
    let mut session = expectrl::Session::spawn(cmd).expect("Can't spawn a session");
    

    causes the output to stop partway through printing the git help.

    (2) Replacing the spawn line with

    let mut cmd = std::process::Command::new("git");
    cmd.args(["diff"]);
    let mut session = expectrl::Session::spawn(cmd).expect("Can't spawn a session");
    

    works as expected.

    (3) Replacing the spawn line with

    let mut cmd = std::process::Command::new("git");
    cmd.args(["diff"]);
    cmd.current_dir("/tmp");
    let mut session = expectrl::Session::spawn(cmd).expect("Can't spawn a session");
    

    also causes the output to stop partway through printing the git help.

    There could be several issues here:

    1. It's notable that the simple example above works as expected, but the demo code doesn't. Is there something about using on_output or on_idle that causes the problem?
    2. How do we use a spawn that accepts a Command and works with interact()?
    3. Which spawn is the right one to use with interact()? It seems confusing to have multiple functions with similar names in different namespaces, eg Session::spawn() vs expectrl::spawn().
    opened by GaryBoone 7
  • "Resource temporarily unavailable" in interact example

    To reproduce: run interact example execute this in the spawned shell: python -c "print('\n'*10000000)" (or more if it doesn't error ;))

    Error: thread 'main' panicked at 'Failed to start interact: IO(Os { code: 11, kind: WouldBlock, message: "Resource temporarily unavailable" })', examples/interact.rs:23:10

    Full backtrace: err.txt

    Relevant setup info: Linux 6.0.1, alacritty terminal

    This doesn't happen in other programs that do similar things (poetry, qmk). There everything works as expected

    PS: It also can happen in not-so-extreme scenarios, but that's a reliable way to reproduce this

    bug 
    opened by LoipesMas 5
  • repl::ReplSession::new function is private

    repl::ReplSession::new function is private

    Thanks for the wonderful library. I want to create my custom ReplSession, but ReplSession::new function is private. So how can I create ReplSession from some sessions outside of this library?

    I want to run program following:

    let session = expectrl::repl::ReplSession::new(
        expectrl::spawn("mycommand").unwrap(),
        "EXPECT_PROMPT".to_string(),
        None, false);
    let recv = session.execute("send");
    
    opened by yamaura 5
  • Session::spawn() method is hard to use across platforms

    Session::spawn() method is hard to use across platforms

    As things are now, Session::spawn() expects std::process::Command on Unix-like platforms and conpty::ProcAttr on Windows (the documentation only mentions the former). The API doesn’t expose the command type in use, this type is private. As a result, my code has to depend on conpty even though it doesn’t actually use it.

    The way I see it, expectrl could at least expose the command type publicly. But ideally it would really take the standard std::process::Command structure on all platforms and convert to conpty::ProcAttr itself.

    opened by palant 4
  • Strip bash decoded sequences.

    Strip bash decoded sequences.

    If you take a look what we print here.

    cargo run --example bash

    You will find a list of sequences which aren't expected at all ...

    https://github.com/zhiburt/expectrl/blob/6b53972e2ffd5102bbc632d3025db65b6e73c663/examples/bash.rs#L17

    help wanted bash 
    opened by zhiburt 3
  • Capture waidpid result in interact()

    Capture waidpid result in interact()

    Today when using interact the exit code is lost because is_alive will swallow it. It would be nice if the session could hold on to the status() so that it can be called again later to retrieve the first observed status.

    opened by mitsuhiko 2
  • Doesn't work on Windows in GitHub Actions

    Doesn't work on Windows in GitHub Actions

    Consider the following very simple use of expectrl:

    use expectrl::{spawn, Error};
    use std::time::Duration;
    
    fn main() -> Result<(), Error> {
        println!("Spawning ...");
        let mut p = spawn("cat")?;
        p.set_expect_timeout(Some(Duration::from_secs(3)));
        println!("Sending line ...");
        p.send_line("Hello World")?;
        println!("Expecting line ...");
        p.expect("Hello World")?;
        println!("OK!");
        Ok(())
    }
    

    If this program is run in GitHub Actions (using expectrl 0.6.0 and Rust 1.65.0), it succeeds on Ubuntu and macOS but fails on Windows; you can see such a run here. In particular, the output on Windows is:

    Spawning ...
    Sending line ...
    Expecting line ...
    Error: ExpectTimeout
    
    opened by jwodder 5
  • `check_eof` test fails on macOS

    `check_eof` test fails on macOS

    On the latest commit at the time of writing

    https://github.com/zhiburt/expectrl/blob/cbd01147d9977d2949a7f607bf424baa1daee168/tests/check.rs#L87-L106

    To reproduce:

    cargo test check_eof -- --exact
    

    Test output on macOS 13 with zsh

    running 1 test
    test check_eof ... FAILED
    
    failures:
    
    ---- check_eof stdout ----
    thread 'check_eof' panicked at 'assertion failed: `(left == right)`
      left: `[]`,
     right: `[39, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 39, 13, 10]`', tests/check.rs:103:13
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
    
    
    failures:
        check_eof
    
    test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 9 filtered out; finished in 0.62s
    
    error: test failed, to rerun pass '--test check'
    
    opened by james-chf 1
  • Interact on Mac doesn't print what's being typed

    Interact on Mac doesn't print what's being typed

    Hi, I'm trying to use expectrl to interact with a ssh password prompt on macOS (vers 12 if it matters) everything works but after the ssh session gets initialized the user input is not "printed" on the command line. Input is being passed to the ssh session, but it is invisible to the user. Example:

    netadmin@CO-90062106: ______________<--- command is not visible

    Desktop/ Documents/ Downloads/ Library/ Movies/ Music/ Pictures/ Public/

    But it produces the expected return when return is pressed.

    This code reproduces the issue

    use expectrl::{check, spawn, stream::stdin::Stdin, Error};
    use std::io::{stdout, Write};
    
    fn main() {
        let command = format!("ssh netadmin@localhost");
        let password = "123xxx";
        let mut sh = spawn("bash").expect(&format!("Unknown command: {:?}", command));
        writeln!(sh, "{}", command).unwrap();
    
        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.expect("Password:").unwrap();
        sh.send_line(password).unwrap();
    
        sh.interact(&mut stdin, stdout())
            .spawn()
            .expect("Failed to start interact");
    
        stdin.close().expect("Failed to close a stdin");
    
        println!("we keep dong things");
    }
    
    question bash 
    opened by psymole 9
Owner
Maxim Zhiburt
Maxim Zhiburt
🚧 Meta Programming language automating multilang communications in a smart way

Table of Contents Merge TLDR Manifest merge-lang Inference File Structure Compile Scheduling Execution Runtime Package Manager API Merge NOTE: Any of

camel_case 4 Oct 17, 2023
A lightweight terminal tool to manage processes in Unix machines.

TTV v0.0.1 TTV (term-task-viewer) is a lightweight tool to view and manage active processes in Unix machines. It provides an easy interface with vim-l

Caio Ishikawa 9 Aug 29, 2023
Modern file system navigation tool on Unix

monat -- Modern file system Navigator 简体中文 Introduction monat is a Unix shell auxiliary command focusing on the navigation of the file system, especia

Pavinberg 8 May 10, 2022
Pink is a command-line tool inspired by the Unix man command.

Pink is a command-line tool inspired by the Unix man command. It displays custom-formatted text pages in the terminal using a subset of HTML-like tags.

null 3 Nov 2, 2023
create and test the style and formatting of text in your terminal applications

description: create and test the style and formatting of text in your terminal applications docs: https://docs.rs/termstyle termstyle is a library tha

Rett Berg 18 Jul 3, 2021
Terminal plotting library for using in Rust CLI applications

textplots Terminal plotting library for using in Rust CLI applications. Should work well in any unicode terminal with monospaced font. It is inspired

Alexey Suslov 163 Dec 30, 2022
A Rust curses library, supports Unix platforms and Windows

pancurses pancurses is a curses library for Rust that supports both Linux and Windows by abstracting away the backend that it uses (ncurses-rs and pdc

Ilkka Halila 360 Jan 7, 2023
Reviving the Research Edition Unix speak command

This repository contains the source code of Unix speak program that appeared in the Third (1973) to Sixth (1975) Research Unix editions, slightly adjusted to run on a modern computer. Details on the code's provenance and the methods employed for reviving it can be found in this blog post.

Diomidis Spinellis 31 Jul 27, 2022
Spawn multiple concurrent unix terminals in Discord

Using this bot can be exceedingly dangerous since you're basically granting people direct access to your shell.

Simon Larsson 11 Jun 1, 2021
fcp is a significantly faster alternative to the classic Unix cp(1) command

A significantly faster alternative to the classic Unix cp(1) command, copying large files and directories in a fraction of the time.

Kevin Svetlitski 532 Jan 3, 2023
A small unix and windows lib to search for executables in PATH folders.

A small unix and windows lib to search for executables in path folders.

Robiot 2 Dec 25, 2021
A Unix shell written and implemented in rust 🦀

vsh A Blazingly fast shell made in Rust ?? Installation Copy and paste the following command and choose the appropriate installtion method for you. Yo

XMantle 89 Dec 18, 2022
FreeDesktop-compliant trasher for Unix

to-trash ?? to-trash (tt for short) is a fast, small, and hopefully FreeDesktop-compliant file trasher for Linux. Compliance tt aims to have complianc

Vinícius Miguel 22 Aug 19, 2022
xcp is a (partial) clone of the Unix cp command. It is not intended as a full replacement

xcp is a (partial) clone of the Unix cp command. It is not intended as a full replacement, but as a companion utility with some more user-friendly feedback and some optimisations that make sense under certain tasks (see below).

Steve Smith 310 Jan 5, 2023
Just a UNIX's cat copy, but less bloated and in Rust.

RAT The opposite of UNIX's cat, less bloated, and in Rust. About the project The idea of this CLI is "A CLI program that is basically UNIX's cat comma

Renan Fernandes 2 Mar 5, 2022
skyWM is an extensible tiling window manager written in Rust. skyWM has a clear and distinct focus adhering to the KISS and Unix philosophy.

Please note: skyWM is currently in heavy development and is not usable as of yet. Documentation and versions will change quickly. skyWM skyWM is an ex

MrBeeBenson 74 Dec 28, 2022
Safe Unix shell-like parameter expansion/variable substitution via cross-platform CLI or Rust API

Safe Unix shell-like parameter expansion/variable substitution for those who need a more powerful alternative to envsubst but don't want to resort to

Isak Wertwein 4 Oct 4, 2022
A unix shell written in rust

rust-shell a unix shell written in rust Features Main features has .rc file (in ~/.rstshrc) has syntax highlighting fish-like autosuggestion emacs edi

matan h 4 Jan 10, 2023
Superviseur - A simple process supervisor for UNIX-like systems

A simple process supervisor for UNIX-like systems. Currently supports non-containerized services. Containerized services and wasm services will be supported in the future.

Tsiry Sandratraina 25 Mar 28, 2023