From 53d23e40c78591c3a237f457ac3f0a08fcf2109b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 02:07:34 +0000 Subject: [PATCH 01/36] chore(deps): Update Rust crate nix to 0.31 --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e968809d..ca1e9017 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -100,9 +100,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.172" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "linux-raw-sys" @@ -124,9 +124,9 @@ checksum = "5486aed0026218e61b8a01d5fbd5a0a134649abb71a0e53b7bc088529dced86e" [[package]] name = "nix" -version = "0.30.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537bc3c4a347b87fd52ac6c03a02ab1302962cfd93373c5d7a112cdc337854cc" +checksum = "225e7cfe711e0ba79a68baeddb2982723e4235247aefce1482f2f16c27865b66" dependencies = [ "bitflags 2.6.0", "cfg-if", diff --git a/Cargo.toml b/Cargo.toml index 42c253d6..e61c83fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -125,7 +125,7 @@ which = ["dep:which"] [dependencies] comma = "1.0" -nix = { version = "0.30", features = ["fs", "process", "signal", "term"] } +nix = { version = "0.31", features = ["fs", "process", "signal", "term"] } regex = "1" tempfile = "3" thiserror = "2.0.0" From 9f35c533f377d1250d4f4ed6610f92d46db7b596 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 00:53:50 +0000 Subject: [PATCH 02/36] chore(deps): Update pre-commit hook crate-ci/committed to v1.1.11 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 67c10aa2..9f7927c1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,6 +14,6 @@ repos: hooks: - id: typos - repo: https://github.com/crate-ci/committed - rev: v1.1.10 + rev: v1.1.11 hooks: - id: committed From 82bf68c05065e40b5fcc6685fd85ff637a09ab44 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 00:53:54 +0000 Subject: [PATCH 03/36] chore(deps): Update pre-commit hook crate-ci/typos to v1.44.0 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 67c10aa2..6bc4cdbf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: - id: check-case-conflict - id: detect-private-key - repo: https://github.com/crate-ci/typos - rev: v1.42.3 + rev: v1.44.0 hooks: - id: typos - repo: https://github.com/crate-ci/committed From 5cfa555c0b9fb4a02b3c0546dbd74b0b7b6d1aca Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 2 Mar 2026 15:50:57 -0600 Subject: [PATCH 04/36] chore: Bump MSRV to 1.85 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e61c83fd..6003e829 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ resolver = "2" repository = "https://github.com/rust-cli/rexpect" license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.70.0" # MSRV +rust-version = "1.85.0" # MSRV include = [ "build.rs", "src/**/*", From 42d20910fdf9b8b47ece35c0d914908ff9b4268c Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 2 Mar 2026 15:51:34 -0600 Subject: [PATCH 05/36] chore: Migrate to Edition 2024 --- Cargo.toml | 4 ++-- src/process.rs | 10 +++++----- src/reader.rs | 18 +++++++++--------- src/session.rs | 2 +- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6003e829..f14f85cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,10 @@ [workspace] -resolver = "2" +resolver = "3" [workspace.package] repository = "https://github.com/rust-cli/rexpect" license = "MIT OR Apache-2.0" -edition = "2021" +edition = "2024" rust-version = "1.85.0" # MSRV include = [ "build.rs", diff --git a/src/process.rs b/src/process.rs index 63cfb498..db1c2507 100644 --- a/src/process.rs +++ b/src/process.rs @@ -2,13 +2,13 @@ use crate::error::Error; use nix; -use nix::fcntl::{open, OFlag}; +use nix::fcntl::{OFlag, open}; use nix::libc::STDERR_FILENO; -use nix::pty::{grantpt, posix_openpt, unlockpt, PtyMaster}; +use nix::pty::{PtyMaster, grantpt, posix_openpt, unlockpt}; pub use nix::sys::{signal, wait}; use nix::sys::{stat, termios}; use nix::unistd::{ - close, dup, dup2_stderr, dup2_stdin, dup2_stdout, fork, setsid, ForkResult, Pid, + ForkResult, Pid, close, dup, dup2_stderr, dup2_stdin, dup2_stdout, fork, setsid, }; use std; use std::fs::File; @@ -68,7 +68,7 @@ use nix::pty::ptsname_r; /// instead of using a static mutex this calls ioctl with TIOCPTYGNAME directly /// based on https://blog.tarq.io/ptsname-on-osx-with-rust/ fn ptsname_r(fd: &PtyMaster) -> nix::Result { - use nix::libc::{ioctl, TIOCPTYGNAME}; + use nix::libc::{TIOCPTYGNAME, ioctl}; use std::ffi::CStr; // the buffer size on OSX is 128, defined by sys/ttycom.h @@ -207,7 +207,7 @@ impl PtyProcess { Ok(_) => {} // process was already killed before -> ignore Err(nix::errno::Errno::ESRCH) => { - return Ok(wait::WaitStatus::Exited(Pid::from_raw(0), 0)) + return Ok(wait::WaitStatus::Exited(Pid::from_raw(0), 0)); } Err(e) => return Err(Error::from(e)), } diff --git a/src/reader.rs b/src/reader.rs index 7a6a9443..a6ae6e54 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -4,7 +4,7 @@ use crate::error::Error; pub use regex::Regex; use std::io::prelude::*; use std::io::{self, BufReader}; -use std::sync::mpsc::{channel, Receiver}; +use std::sync::mpsc::{Receiver, channel}; use std::thread; use std::{fmt, time}; @@ -31,13 +31,13 @@ pub enum ReadUntil { impl fmt::Display for ReadUntil { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let printable = match self { - ReadUntil::String(ref s) if s == "\n" => "\\n (newline)".to_owned(), - ReadUntil::String(ref s) if s == "\r" => "\\r (carriage return)".to_owned(), - ReadUntil::String(ref s) => format!("\"{s}\""), - ReadUntil::Regex(ref r) => format!("Regex: \"{r}\""), + ReadUntil::String(s) if s == "\n" => "\\n (newline)".to_owned(), + ReadUntil::String(s) if s == "\r" => "\\r (carriage return)".to_owned(), + ReadUntil::String(s) => format!("\"{s}\""), + ReadUntil::Regex(r) => format!("Regex: \"{r}\""), ReadUntil::EOF => "EOF (End of File)".to_owned(), ReadUntil::NBytes(n) => format!("reading {n} bytes"), - ReadUntil::Any(ref v) => { + ReadUntil::Any(v) => { let mut res = Vec::new(); for r in v { res.push(r.to_string()); @@ -63,8 +63,8 @@ impl fmt::Display for ReadUntil { /// 2. position after match pub fn find(needle: &ReadUntil, buffer: &str, eof: bool) -> Option<(usize, usize)> { match needle { - ReadUntil::String(ref s) => buffer.find(s).map(|pos| (pos, pos + s.len())), - ReadUntil::Regex(ref pattern) => pattern.find(buffer).map(|mat| (mat.start(), mat.end())), + ReadUntil::String(s) => buffer.find(s).map(|pos| (pos, pos + s.len())), + ReadUntil::Regex(pattern) => pattern.find(buffer).map(|mat| (mat.start(), mat.end())), ReadUntil::EOF => { if eof { Some((0, buffer.len())) @@ -83,7 +83,7 @@ pub fn find(needle: &ReadUntil, buffer: &str, eof: bool) -> Option<(usize, usize None } } - ReadUntil::Any(ref anys) => anys + ReadUntil::Any(anys) => anys .iter() // Filter matching needles .filter_map(|any| find(any, buffer, eof)) diff --git a/src/session.rs b/src/session.rs index 52f2f41b..07399812 100644 --- a/src/session.rs +++ b/src/session.rs @@ -5,8 +5,8 @@ use crate::process::PtyProcess; use crate::reader::{NBReader, Regex}; pub use crate::reader::{Options, ReadUntil}; use std::fs::File; -use std::io::prelude::*; use std::io::LineWriter; +use std::io::prelude::*; use std::ops::{Deref, DerefMut}; use std::process::Command; use tempfile; From a49324aed183dc1607f86200f59f0c4f9fb68fdc Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 10 Mar 2026 08:57:31 -0500 Subject: [PATCH 06/36] docs: Update description --- .github/settings.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/settings.yml b/.github/settings.yml index f8bcac4c..9b3763e4 100644 --- a/.github/settings.yml +++ b/.github/settings.yml @@ -1,7 +1,7 @@ # These settings are synced to GitHub by https://probot.github.io/apps/settings/ repository: - description: ".github/workflows/ci.yml" + description: "Interact with unix processes/bash the same way as pexpect or Don libes expect does" homepage: "https://docs.rs/rexpect" topics: "pexpect unix pty processes" has_issues: true From a5e99dfeb9018f40f8dad71559ed78d455d620f4 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 16 Mar 2026 08:06:46 -0500 Subject: [PATCH 07/36] refactor: Move helpers to after use --- src/reader.rs | 180 ++++++++++++++++++++++++------------------------- src/session.rs | 12 ++-- 2 files changed, 96 insertions(+), 96 deletions(-) diff --git a/src/reader.rs b/src/reader.rs index a6ae6e54..2ef8a5e4 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -8,96 +8,6 @@ use std::sync::mpsc::{Receiver, channel}; use std::thread; use std::{fmt, time}; -#[derive(Debug)] -enum PipeError { - IO(io::Error), -} - -#[derive(Debug)] -#[allow(clippy::upper_case_acronyms)] -enum PipedChar { - Char(u8), - EOF, -} - -pub enum ReadUntil { - String(String), - Regex(Regex), - EOF, - NBytes(usize), - Any(Vec), -} - -impl fmt::Display for ReadUntil { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let printable = match self { - ReadUntil::String(s) if s == "\n" => "\\n (newline)".to_owned(), - ReadUntil::String(s) if s == "\r" => "\\r (carriage return)".to_owned(), - ReadUntil::String(s) => format!("\"{s}\""), - ReadUntil::Regex(r) => format!("Regex: \"{r}\""), - ReadUntil::EOF => "EOF (End of File)".to_owned(), - ReadUntil::NBytes(n) => format!("reading {n} bytes"), - ReadUntil::Any(v) => { - let mut res = Vec::new(); - for r in v { - res.push(r.to_string()); - } - res.join(", ") - } - }; - write!(f, "{printable}") - } -} - -/// find first occurrence of needle within buffer -/// -/// # Arguments: -/// -/// - buffer: the currently read buffer from a process which will still grow in the future -/// - eof: if the process already sent an EOF or a HUP -/// -/// # Return -/// -/// Tuple with match positions: -/// 1. position before match (0 in case of EOF and Nbytes) -/// 2. position after match -pub fn find(needle: &ReadUntil, buffer: &str, eof: bool) -> Option<(usize, usize)> { - match needle { - ReadUntil::String(s) => buffer.find(s).map(|pos| (pos, pos + s.len())), - ReadUntil::Regex(pattern) => pattern.find(buffer).map(|mat| (mat.start(), mat.end())), - ReadUntil::EOF => { - if eof { - Some((0, buffer.len())) - } else { - None - } - } - ReadUntil::NBytes(n) => { - if *n <= buffer.len() { - Some((0, *n)) - } else if eof && !buffer.is_empty() { - // reached almost end of buffer, return string, even though it will be - // smaller than the wished n bytes - Some((0, buffer.len())) - } else { - None - } - } - ReadUntil::Any(anys) => anys - .iter() - // Filter matching needles - .filter_map(|any| find(any, buffer, eof)) - // Return the left-most match - .min_by(|(start1, end1), (start2, end2)| { - if start1 == start2 { - end1.cmp(end2) - } else { - start1.cmp(start2) - } - }), - } -} - /// Options for `NBReader` /// /// - timeout: @@ -296,6 +206,96 @@ impl NBReader { } } +pub enum ReadUntil { + String(String), + Regex(Regex), + EOF, + NBytes(usize), + Any(Vec), +} + +impl fmt::Display for ReadUntil { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let printable = match self { + ReadUntil::String(s) if s == "\n" => "\\n (newline)".to_owned(), + ReadUntil::String(s) if s == "\r" => "\\r (carriage return)".to_owned(), + ReadUntil::String(s) => format!("\"{s}\""), + ReadUntil::Regex(r) => format!("Regex: \"{r}\""), + ReadUntil::EOF => "EOF (End of File)".to_owned(), + ReadUntil::NBytes(n) => format!("reading {n} bytes"), + ReadUntil::Any(v) => { + let mut res = Vec::new(); + for r in v { + res.push(r.to_string()); + } + res.join(", ") + } + }; + write!(f, "{printable}") + } +} + +/// find first occurrence of needle within buffer +/// +/// # Arguments: +/// +/// - buffer: the currently read buffer from a process which will still grow in the future +/// - eof: if the process already sent an EOF or a HUP +/// +/// # Return +/// +/// Tuple with match positions: +/// 1. position before match (0 in case of EOF and Nbytes) +/// 2. position after match +pub fn find(needle: &ReadUntil, buffer: &str, eof: bool) -> Option<(usize, usize)> { + match needle { + ReadUntil::String(s) => buffer.find(s).map(|pos| (pos, pos + s.len())), + ReadUntil::Regex(pattern) => pattern.find(buffer).map(|mat| (mat.start(), mat.end())), + ReadUntil::EOF => { + if eof { + Some((0, buffer.len())) + } else { + None + } + } + ReadUntil::NBytes(n) => { + if *n <= buffer.len() { + Some((0, *n)) + } else if eof && !buffer.is_empty() { + // reached almost end of buffer, return string, even though it will be + // smaller than the wished n bytes + Some((0, buffer.len())) + } else { + None + } + } + ReadUntil::Any(anys) => anys + .iter() + // Filter matching needles + .filter_map(|any| find(any, buffer, eof)) + // Return the left-most match + .min_by(|(start1, end1), (start2, end2)| { + if start1 == start2 { + end1.cmp(end2) + } else { + start1.cmp(start2) + } + }), + } +} + +#[derive(Debug)] +enum PipeError { + IO(io::Error), +} + +#[derive(Debug)] +#[allow(clippy::upper_case_acronyms)] +enum PipedChar { + Char(u8), + EOF, +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/session.rs b/src/session.rs index 07399812..65375f3b 100644 --- a/src/session.rs +++ b/src/session.rs @@ -5,8 +5,8 @@ use crate::process::PtyProcess; use crate::reader::{NBReader, Regex}; pub use crate::reader::{Options, ReadUntil}; use std::fs::File; -use std::io::LineWriter; use std::io::prelude::*; +use std::io::LineWriter; use std::ops::{Deref, DerefMut}; use std::process::Command; use tempfile; @@ -88,11 +88,6 @@ impl StreamSession { self.reader.try_read() } - // wrapper around reader::read_until to give more context for errors - fn exp(&mut self, needle: &ReadUntil) -> Result<(String, String), Error> { - self.reader.read_until(needle) - } - /// Wait until we see EOF (i.e. child process has terminated) /// Return all the yet unread output pub fn exp_eof(&mut self) -> Result { @@ -149,6 +144,11 @@ impl StreamSession { pub fn exp_any(&mut self, needles: Vec) -> Result<(String, String), Error> { self.exp(&ReadUntil::Any(needles)) } + + // wrapper around reader::read_until to give more context for errors + fn exp(&mut self, needle: &ReadUntil) -> Result<(String, String), Error> { + self.reader.read_until(needle) + } } /// Interact with a process with read/write/signals, etc. #[allow(dead_code)] From c3fbd04b43c146a986377f27effe0070c48d33a2 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 16 Mar 2026 08:15:51 -0500 Subject: [PATCH 08/36] docs(reader): Document ReadUntil --- src/reader.rs | 1 + src/session.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/reader.rs b/src/reader.rs index 2ef8a5e4..997de848 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -206,6 +206,7 @@ impl NBReader { } } +/// See [`NBReader::read_until`] pub enum ReadUntil { String(String), Regex(Regex), diff --git a/src/session.rs b/src/session.rs index 65375f3b..4ab6b788 100644 --- a/src/session.rs +++ b/src/session.rs @@ -5,8 +5,8 @@ use crate::process::PtyProcess; use crate::reader::{NBReader, Regex}; pub use crate::reader::{Options, ReadUntil}; use std::fs::File; -use std::io::prelude::*; use std::io::LineWriter; +use std::io::prelude::*; use std::ops::{Deref, DerefMut}; use std::process::Command; use tempfile; From 9f7b2bab7c0b7e59243944248382f59c6bac4bff Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 16 Mar 2026 08:16:24 -0500 Subject: [PATCH 09/36] docs(reader): Re-arrange variants --- src/reader.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/reader.rs b/src/reader.rs index 997de848..62f5e0d2 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -210,8 +210,8 @@ impl NBReader { pub enum ReadUntil { String(String), Regex(Regex), - EOF, NBytes(usize), + EOF, Any(Vec), } @@ -222,8 +222,8 @@ impl fmt::Display for ReadUntil { ReadUntil::String(s) if s == "\r" => "\\r (carriage return)".to_owned(), ReadUntil::String(s) => format!("\"{s}\""), ReadUntil::Regex(r) => format!("Regex: \"{r}\""), - ReadUntil::EOF => "EOF (End of File)".to_owned(), ReadUntil::NBytes(n) => format!("reading {n} bytes"), + ReadUntil::EOF => "EOF (End of File)".to_owned(), ReadUntil::Any(v) => { let mut res = Vec::new(); for r in v { From cc67a84e7592adc610fbd149ba4a6e888a0b62bd Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 16 Mar 2026 08:19:59 -0500 Subject: [PATCH 10/36] refactor(reader): Remove allocations from Display --- src/reader.rs | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/reader.rs b/src/reader.rs index 62f5e0d2..665b994d 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -217,22 +217,23 @@ pub enum ReadUntil { impl fmt::Display for ReadUntil { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let printable = match self { - ReadUntil::String(s) if s == "\n" => "\\n (newline)".to_owned(), - ReadUntil::String(s) if s == "\r" => "\\r (carriage return)".to_owned(), - ReadUntil::String(s) => format!("\"{s}\""), - ReadUntil::Regex(r) => format!("Regex: \"{r}\""), - ReadUntil::NBytes(n) => format!("reading {n} bytes"), - ReadUntil::EOF => "EOF (End of File)".to_owned(), + match self { + ReadUntil::String(s) if s == "\n" => write!(f, "\\n (newline)"), + ReadUntil::String(s) if s == "\r" => write!(f, "\\r (carriage return)"), + ReadUntil::String(s) => write!(f, "\"{s}\""), + ReadUntil::Regex(r) => write!(f, "Regex: \"{r}\""), + ReadUntil::NBytes(n) => write!(f, "reading {n} bytes"), + ReadUntil::EOF => write!(f, "EOF (End of File)"), ReadUntil::Any(v) => { - let mut res = Vec::new(); - for r in v { - res.push(r.to_string()); + for (i, r) in v.iter().enumerate() { + if i != 0 { + write!(f, ", ")?; + } + write!(f, "{r}")?; } - res.join(", ") + Ok(()) } - }; - write!(f, "{printable}") + } } } From beda4a1c981c53db5a9d01bd48dd4261cd1ffc80 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 16 Mar 2026 08:36:34 -0500 Subject: [PATCH 11/36] docs: Clean up --- src/process.rs | 32 +++++++++++------- src/reader.rs | 63 ++++++++++++++++++---------------- src/session.rs | 92 ++++++++++++++++++++++++++++++-------------------- 3 files changed, 107 insertions(+), 80 deletions(-) diff --git a/src/process.rs b/src/process.rs index db1c2507..bbe0683b 100644 --- a/src/process.rs +++ b/src/process.rs @@ -18,7 +18,7 @@ use std::os::unix::process::CommandExt; use std::process::Command; use std::{thread, time}; -/// Start a process in a forked tty so you can interact with it the same as you would +/// Start a process in a forked tty to interact with it like you would /// within a terminal /// /// The process and pty session are killed upon dropping `PtyProcess` @@ -144,17 +144,18 @@ impl PtyProcess { Ok(fd.into()) } - /// At the drop of `PtyProcess` the running process is killed. This is blocking forever if - /// the process does not react to a normal kill. If `kill_timeout` is set the process is - /// `kill -9`ed after duration + /// At the drop of `PtyProcess` the running process is killed (blocking). + /// + /// This is blocking forever if the process does not react to a normal kill. + /// If `kill_timeout` is set the process is `kill -9`ed after duration. pub fn set_kill_timeout(&mut self, timeout_ms: Option) { self.kill_timeout = timeout_ms.map(time::Duration::from_millis); } - /// Get status of child process, non-blocking. + /// Get status of child process (non-blocking). /// - /// This method runs waitpid on the process. - /// This means: If you ran `exit()` before or `status()` this method will + /// This method runs waitpid on the process: + /// if you ran `exit()` before or `status()` this method will /// return `None` /// /// # Example @@ -176,29 +177,34 @@ impl PtyProcess { wait::waitpid(self.child_pid, Some(wait::WaitPidFlag::WNOHANG)).ok() } - /// Wait until process has exited. This is a blocking call. + /// Wait until process has exited (non-blocking). + /// /// If the process doesn't terminate this will block forever. pub fn wait(&self) -> Result { wait::waitpid(self.child_pid, None).map_err(Error::from) } - /// Regularly exit the process, this method is blocking until the process is dead + /// Regularly exit the process (blocking). + /// + /// This method is blocking until the process is dead pub fn exit(&mut self) -> Result { self.kill(signal::SIGTERM) } - /// Non-blocking variant of `kill()` (doesn't wait for process to be killed) + /// Kill the process with a specific signal (non-blocking). pub fn signal(&mut self, sig: signal::Signal) -> Result<(), Error> { signal::kill(self.child_pid, sig).map_err(Error::from) } - /// Kill the process with a specific signal. This method blocks, until the process is dead + /// Kill the process with a specific signal (blocking). + /// + /// This method blocks until the process is dead /// - /// repeatedly sends SIGTERM to the process until it died, + /// This repeatedly sends SIGTERM to the process until it died, /// the pty session is closed upon dropping `PtyMaster`, /// so we don't need to explicitly do that here. /// - /// if `kill_timeout` is set and a repeated sending of signal does not result in the process + /// If `kill_timeout` is set and a repeated sending of signal does not result in the process /// being killed, then `kill -9` is sent after the `kill_timeout` duration has elapsed. pub fn kill(&mut self, sig: signal::Signal) -> Result { let start = time::Instant::now(); diff --git a/src/reader.rs b/src/reader.rs index 665b994d..b49c298f 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -8,15 +8,14 @@ use std::sync::mpsc::{Receiver, channel}; use std::thread; use std::{fmt, time}; -/// Options for `NBReader` -/// -/// - timeout: -/// + `None`: `read_until` is blocking forever. This is probably not what you want -/// + `Some(millis)`: after millis milliseconds a timeout error is raised -/// - `strip_ansi_escape_codes`: Whether to filter out escape codes, such as colors. +/// Options for [`NBReader`] #[derive(Default)] pub struct Options { + /// `None`: `read_until` is blocking forever. This is probably not what you want + /// + /// `Some(millis)`: after millis milliseconds a timeout error is raised pub timeout_ms: Option, + /// Whether to filter out escape codes, such as colors. pub strip_ansi_escape_codes: bool, } @@ -35,10 +34,10 @@ pub struct NBReader { impl NBReader { /// Create a new reader instance /// - /// # Arguments: + /// # Arguments /// - /// - f: file like object - /// - options: see `Options` + /// - `f`: file like object + /// - `options`: see [`Options`] pub fn new(f: R, options: Options) -> NBReader { let (tx, rx) = channel(); @@ -87,7 +86,7 @@ impl NBReader { } } - /// reads all available chars from the read channel and stores them in self.buffer + /// Reads all available chars from the read channel and stores them in [`Self::buffer`] fn read_into_buffer(&mut self) -> Result<(), Error> { if self.eof { return Ok(()); @@ -109,24 +108,13 @@ impl NBReader { Ok(()) } - /// Read until needle is found (blocking!) and return tuple with: - /// 1. yet unread string until and without needle - /// 2. matched needle + /// Read until needle is found (blocking!) /// /// This methods loops (while reading from the Cursor) until the needle is found. /// - /// There are different modes: - /// - /// - `ReadUntil::String` searches for string (use '\n'.`to_string()` to search for newline). - /// Returns not yet read data in first String, and needle in second String - /// - `ReadUntil::Regex` searches for regex - /// Returns not yet read data in first String and matched regex in second String - /// - `ReadUntil::NBytes` reads maximum n bytes - /// Returns n bytes in second String, first String is left empty - /// - `ReadUntil::EOF` reads until end of file is reached - /// Returns all bytes in second String, first is left empty - /// - /// Note that when used with a tty the lines end with \r\n + /// Returns a tuple with: + /// 1. yet unread string until and without needle + /// 2. matched needle /// /// Returns error if EOF is reached before the needle could be found. /// @@ -193,8 +181,9 @@ impl NBReader { } } - /// Try to read one char from internal buffer. Returns None if - /// no char is ready, Some(char) otherwise. This is non-blocking + /// Try to read one char from internal buffer (non-blocking). + /// + /// Returns `None` if no char is ready `Some(char)` otherwise. pub fn try_read(&mut self) -> Option { // discard eventual errors, EOF will be handled in read_until correctly let _ = self.read_into_buffer(); @@ -207,10 +196,24 @@ impl NBReader { } /// See [`NBReader::read_until`] +/// +/// Note that when used with a tty the lines end with \r\n pub enum ReadUntil { + /// Searches for string (use '\n'.`to_string()` to search for newline). + /// + /// Returns not yet read data in first String, and needle in second String String(String), + /// `ReadUntil::Regex` searches for regex + /// + /// Returns not yet read data in first String and matched regex in second String Regex(Regex), + /// `ReadUntil::NBytes` reads maximum n bytes + /// + /// Returns n bytes in second String, first String is left empty NBytes(usize), + /// `ReadUntil::EOF` reads until end of file is reached + /// + /// Returns all bytes in second String, first is left empty EOF, Any(Vec), } @@ -237,12 +240,12 @@ impl fmt::Display for ReadUntil { } } -/// find first occurrence of needle within buffer +/// Find first occurrence of needle within buffer /// /// # Arguments: /// -/// - buffer: the currently read buffer from a process which will still grow in the future -/// - eof: if the process already sent an EOF or a HUP +/// - `buffer`: the currently read buffer from a process which will still grow in the future +/// - `eof`: if the process already sent an EOF or a HUP /// /// # Return /// diff --git a/src/session.rs b/src/session.rs index 4ab6b788..8c175a2a 100644 --- a/src/session.rs +++ b/src/session.rs @@ -24,9 +24,9 @@ impl StreamSession { } } - /// sends string and a newline to process + /// Sends string and a newline to process /// - /// this is guaranteed to be flushed to the process + /// This is guaranteed to be flushed to the process /// returns number of written bytes pub fn send_line(&mut self, line: &str) -> Result { let mut len = self.send(line)?; @@ -34,8 +34,10 @@ impl StreamSession { Ok(len) } - /// Send string to process. As stdin of the process is most likely buffered, you'd - /// need to call `flush()` after `send()` to make the process actually see your input. + /// Send string to process. + /// + /// As stdin of the process is most likely buffered, + /// you'd need to call `flush()` after `send()` to make the process actually see your input. /// /// Returns number of written bytes pub fn send(&mut self, s: &str) -> Result { @@ -45,7 +47,9 @@ impl StreamSession { /// Send a control code to the running process and consume resulting output line /// (which is empty because echo is off) /// - /// E.g. `send_control('c')` sends ctrl-c. Upper/smaller case does not matter. + /// Upper/smaller case does not matter. + /// + /// E.g. `send_control('c')` sends ctrl-c. pub fn send_control(&mut self, c: char) -> Result<(), Error> { let code = match c { 'a'..='z' => c as u8 + 1 - b'a', @@ -63,13 +67,16 @@ impl StreamSession { Ok(()) } - /// Make sure all bytes written via `send()` are sent to the process + /// Make sure all bytes written via [`Self::send()`] are sent to the process pub fn flush(&mut self) -> Result<(), Error> { self.writer.flush().map_err(Error::from) } - /// Read one line (blocking!) and return line without the newline - /// (waits until \n is in the output fetches the line and removes \r at the end if present) + /// Read one line (blocking). + /// + /// Return line without the newline + /// + /// Waits until \n is in the output fetches the line and removes \r at the end if present. pub fn read_line(&mut self) -> Result { match self.exp(&ReadUntil::String('\n'.to_string())) { Ok((mut line, _)) => { @@ -82,19 +89,22 @@ impl StreamSession { } } - /// Return `Some(c)` if a char is ready in the stdout stream of the process, return `None` - /// otherwise. This is non-blocking. + /// Return `Some(c)` if a char is ready in the stdout stream of the process (non-blocking). + /// + /// Return `None` otherwise. pub fn try_read(&mut self) -> Option { self.reader.try_read() } /// Wait until we see EOF (i.e. child process has terminated) + /// /// Return all the yet unread output pub fn exp_eof(&mut self) -> Result { self.exp(&ReadUntil::EOF).map(|(_, s)| s) } /// Wait until provided regex is seen on stdout of child process. + /// /// Return a tuple: /// 1. the yet unread output /// 2. the matched regex @@ -106,6 +116,7 @@ impl StreamSession { } /// Wait until provided string is seen on stdout of child process. + /// /// Return the yet unread output (without the matched string) pub fn exp_string(&mut self, needle: &str) -> Result { self.exp(&ReadUntil::String(needle.to_owned())) @@ -113,6 +124,7 @@ impl StreamSession { } /// Wait until provided char is seen on stdout of child process. + /// /// Return the yet unread output (without the matched char) pub fn exp_char(&mut self, needle: char) -> Result { self.exp(&ReadUntil::String(needle.to_string())) @@ -150,6 +162,7 @@ impl StreamSession { self.reader.read_until(needle) } } + /// Interact with a process with read/write/signals, etc. #[allow(dead_code)] pub struct PtySession { @@ -230,7 +243,7 @@ pub fn spawn(program: &str, timeout_ms: Option) -> Result) -> Result { spawn_with_options( command, @@ -241,7 +254,7 @@ pub fn spawn_command(command: Command, timeout_ms: Option) -> Result Result { #[cfg(feature = "which")] { @@ -253,26 +266,29 @@ pub fn spawn_with_options(command: Command, options: Options) -> Result>> " for python + /// The prompt, used for `wait_for_prompt`, e.g. ">>> " for python pub prompt: String, - /// the `pty_session` you prepared before (initiating the shell, maybe set a custom prompt, etc.) - /// see `spawn_bash` for an example + /// The `pty_session` you prepared before (initiating the shell, maybe set a custom prompt, etc.) + /// + /// See [`spawn_bash`] for an example pub pty_session: PtySession, - /// if set, then the `quit_command` is called when this object is dropped + /// If set, then the `quit_command` is called when this object is dropped /// you need to provide this if the shell you're testing is not killed by just sending /// SIGTERM pub quit_command: Option, - /// set this to true if the repl has echo on (i.e. sends user input to stdout) - /// although echo is set off at pty fork (see `PtyProcess::new`) a few repls still - /// seem to be able to send output. You may need to try with true first, and if - /// tests fail set this to false. + /// Set this to true if the repl has echo on (i.e. sends user input to stdout) + /// + /// Although echo is set off at pty fork (see `PtyProcess::new`) a few repls still + /// seem to be able to send output. + /// You may need to try with true first, and if tests fail set this to false. pub echo_on: bool, } @@ -319,9 +335,11 @@ impl PtyReplSession { Ok(()) } - /// send line to repl (and flush output) and then, if `echo_on=true` wait for the - /// input to appear. - /// Return: number of bytes written + /// Send line to repl (and flush output) + /// + /// If `echo_on=true` wait for the input to appear. + /// + /// Returns number of bytes written pub fn send_line(&mut self, line: &str) -> Result { let bytes_written = self.pty_session.send_line(line)?; if self.echo_on { @@ -360,25 +378,25 @@ impl Drop for PtyReplSession { /// Spawn bash in a pty session, run programs and expect output /// +/// The difference to [`spawn`] and [`spawn_command`] is: /// -/// The difference to `spawn` and `spawn_command` is: -/// -/// - `spawn_bash` starts bash with a custom rcfile which guarantees +/// - [`spawn_bash`] starts bash with a custom rcfile which guarantees /// a certain prompt -/// - the `PtyBashSession` also provides `wait_for_prompt` and `execute` +/// - Provides [`PtyReplSession::wait_for_prompt`] and [`PtyReplSession::execute`] /// -/// timeout: the duration until which `exp_*` returns a timeout error, or None -/// additionally, when dropping the bash prompt while bash is still blocked by a program +/// `timeout`: the duration until which `exp_*` returns a timeout error, or `None`. +/// Additionally, when dropping the bash prompt while bash is still blocked by a program /// (e.g. `sleep 9999`) then the timeout is used as a timeout before a `kill -9` is issued -/// at the bash command. Use a timeout whenever possible because it makes -/// debugging a lot easier (otherwise the program just hangs and you -/// don't know where) +/// at the bash command. +/// Use a timeout whenever possible because it makes debugging a lot easier +/// (otherwise the program just hangs and you don't know where) /// -/// bash is started with echo off. That means you don't need to "read back" -/// what you wrote to bash. But what you need to do is a `wait_for_prompt` -/// after a process finished. +/// Bash is started with echo off. +/// That means you don't need to "read back" what you wrote to bash. +/// But what you need to do is a `wait_for_prompt` after a process finished. /// -/// Also: if you start a program you should use `execute` and not `send_line`. +/// Also: if you start a program you should use [`PtyReplSession::execute`] and not +/// [`PtyReplSession::send_line`]. /// /// For an example see the README pub fn spawn_bash(timeout: Option) -> Result { From 6f4463d15058a8a1b20af36cee2b86d9705e1099 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 16 Mar 2026 09:07:25 -0500 Subject: [PATCH 12/36] docs: Move doc comment onto method --- src/session.rs | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/session.rs b/src/session.rs index 8c175a2a..43b8c289 100644 --- a/src/session.rs +++ b/src/session.rs @@ -184,26 +184,26 @@ impl DerefMut for PtySession { } } -/// Start a process in a tty session, write and read from it -/// -/// # Example -/// -/// ``` -/// -/// use rexpect::spawn; -/// # use rexpect::error::Error; -/// -/// # fn main() { -/// # || -> Result<(), Error> { -/// let mut s = spawn("cat", Some(1000))?; -/// s.send_line("hello, polly!")?; -/// let line = s.read_line()?; -/// assert_eq!("hello, polly!", line); -/// # Ok(()) -/// # }().expect("test failed"); -/// # } -/// ``` impl PtySession { + /// Start a process in a tty session, write and read from it + /// + /// # Example + /// + /// ``` + /// + /// use rexpect::spawn; + /// # use rexpect::error::Error; + /// + /// # fn main() { + /// # || -> Result<(), Error> { + /// let mut s = spawn("cat", Some(1000))?; + /// s.send_line("hello, polly!")?; + /// let line = s.read_line()?; + /// assert_eq!("hello, polly!", line); + /// # Ok(()) + /// # }().expect("test failed"); + /// # } + /// ``` fn new(process: PtyProcess, options: Options) -> Result { let f = process.get_file_handle()?; let reader = f.try_clone()?; From 9c9ca5020f410a394abf6628fbbe13a317c2eb27 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 16 Mar 2026 09:07:41 -0500 Subject: [PATCH 13/36] feat: Expose PtySession::new --- src/session.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/session.rs b/src/session.rs index 43b8c289..f6037a6b 100644 --- a/src/session.rs +++ b/src/session.rs @@ -204,7 +204,7 @@ impl PtySession { /// # }().expect("test failed"); /// # } /// ``` - fn new(process: PtyProcess, options: Options) -> Result { + pub fn new(process: PtyProcess, options: Options) -> Result { let f = process.get_file_handle()?; let reader = f.try_clone()?; let stream = StreamSession::new(reader, f, options); From 07869021415f25e119fadecfa42ea307f36ed7bb Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 16 Mar 2026 09:08:39 -0500 Subject: [PATCH 14/36] refactor(session): Move split after use --- src/session.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/session.rs b/src/session.rs index f6037a6b..273cbb8c 100644 --- a/src/session.rs +++ b/src/session.rs @@ -212,12 +212,6 @@ impl PtySession { } } -/// Turn e.g. "prog arg1 arg2" into ["prog", "arg1", "arg2"] -/// Also takes care of single and double quotes -fn tokenize_command(program: &str) -> Result, Error> { - comma::parse_command(program).ok_or(Error::BadProgramArguments) -} - /// Start command in background in a pty session (pty fork) and return a struct /// with writer and buffered reader (for unblocking reads). /// @@ -243,6 +237,12 @@ pub fn spawn(program: &str, timeout_ms: Option) -> Result Result, Error> { + comma::parse_command(program).ok_or(Error::BadProgramArguments) +} + /// See [`spawn`] pub fn spawn_command(command: Command, timeout_ms: Option) -> Result { spawn_with_options( From c0a5ed04770519391cbe198694327a4f856b10d8 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 16 Mar 2026 09:01:33 -0500 Subject: [PATCH 15/36] refactor(process): Draw attention to public APIs --- src/process.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/process.rs b/src/process.rs index bbe0683b..30137a1c 100644 --- a/src/process.rs +++ b/src/process.rs @@ -5,7 +5,6 @@ use nix; use nix::fcntl::{OFlag, open}; use nix::libc::STDERR_FILENO; use nix::pty::{PtyMaster, grantpt, posix_openpt, unlockpt}; -pub use nix::sys::{signal, wait}; use nix::sys::{stat, termios}; use nix::unistd::{ ForkResult, Pid, close, dup, dup2_stderr, dup2_stdin, dup2_stdout, fork, setsid, @@ -18,6 +17,8 @@ use std::os::unix::process::CommandExt; use std::process::Command; use std::{thread, time}; +pub use nix::sys::{signal, wait}; + /// Start a process in a forked tty to interact with it like you would /// within a terminal /// From ef54ee8accb4beed99b95bf5fa8e7e03ac742695 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 16 Mar 2026 09:04:54 -0500 Subject: [PATCH 16/36] fix(process): Directly re-export needed types --- examples/exit_code.rs | 8 ++++---- src/process.rs | 26 ++++++++++++++------------ 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/examples/exit_code.rs b/examples/exit_code.rs index 67486de6..dfb57065 100644 --- a/examples/exit_code.rs +++ b/examples/exit_code.rs @@ -1,5 +1,5 @@ use rexpect::error::Error; -use rexpect::process::wait; +use rexpect::process::WaitStatus; use rexpect::spawn; /// The following code emits: @@ -9,14 +9,14 @@ use rexpect::spawn; fn main() -> Result<(), Error> { let p = spawn("cat /etc/passwd", Some(2000))?; match p.process.wait() { - Ok(wait::WaitStatus::Exited(_, 0)) => println!("cat exited with code 0, all good!"), + Ok(WaitStatus::Exited(_, 0)) => println!("cat exited with code 0, all good!"), _ => println!("cat exited with code >0, or it was killed"), } let mut p = spawn("cat /this/does/not/exist", Some(2000))?; match p.process.wait() { - Ok(wait::WaitStatus::Exited(_, 0)) => println!("cat succeeded"), - Ok(wait::WaitStatus::Exited(_, c)) => { + Ok(WaitStatus::Exited(_, 0)) => println!("cat succeeded"), + Ok(WaitStatus::Exited(_, c)) => { println!("Cat failed with exit code {c}"); println!("Output (stdout and stderr): {}", p.exp_eof()?); } diff --git a/src/process.rs b/src/process.rs index 30137a1c..1a928c5f 100644 --- a/src/process.rs +++ b/src/process.rs @@ -18,6 +18,8 @@ use std::process::Command; use std::{thread, time}; pub use nix::sys::{signal, wait}; +pub use signal::Signal; +pub use wait::WaitStatus; /// Start a process in a forked tty to interact with it like you would /// within a terminal @@ -162,7 +164,7 @@ impl PtyProcess { /// # Example /// ```rust,no_run /// - /// use rexpect::process::{self, wait::WaitStatus}; + /// use rexpect::process::{self, WaitStatus}; /// use std::process::Command; /// /// # fn main() { @@ -174,26 +176,26 @@ impl PtyProcess { /// # } /// ``` /// - pub fn status(&self) -> Option { + pub fn status(&self) -> Option { wait::waitpid(self.child_pid, Some(wait::WaitPidFlag::WNOHANG)).ok() } /// Wait until process has exited (non-blocking). /// /// If the process doesn't terminate this will block forever. - pub fn wait(&self) -> Result { + pub fn wait(&self) -> Result { wait::waitpid(self.child_pid, None).map_err(Error::from) } /// Regularly exit the process (blocking). /// /// This method is blocking until the process is dead - pub fn exit(&mut self) -> Result { + pub fn exit(&mut self) -> Result { self.kill(signal::SIGTERM) } /// Kill the process with a specific signal (non-blocking). - pub fn signal(&mut self, sig: signal::Signal) -> Result<(), Error> { + pub fn signal(&mut self, sig: Signal) -> Result<(), Error> { signal::kill(self.child_pid, sig).map_err(Error::from) } @@ -207,26 +209,26 @@ impl PtyProcess { /// /// If `kill_timeout` is set and a repeated sending of signal does not result in the process /// being killed, then `kill -9` is sent after the `kill_timeout` duration has elapsed. - pub fn kill(&mut self, sig: signal::Signal) -> Result { + pub fn kill(&mut self, sig: Signal) -> Result { let start = time::Instant::now(); loop { match signal::kill(self.child_pid, sig) { Ok(_) => {} // process was already killed before -> ignore Err(nix::errno::Errno::ESRCH) => { - return Ok(wait::WaitStatus::Exited(Pid::from_raw(0), 0)); + return Ok(WaitStatus::Exited(Pid::from_raw(0), 0)); } Err(e) => return Err(Error::from(e)), } match self.status() { - Some(status) if status != wait::WaitStatus::StillAlive => return Ok(status), + Some(status) if status != WaitStatus::StillAlive => return Ok(status), Some(_) | None => thread::sleep(time::Duration::from_millis(100)), } // kill -9 if timeout is reached if let Some(timeout) = self.kill_timeout { if start.elapsed() > timeout { - signal::kill(self.child_pid, signal::Signal::SIGKILL).map_err(Error::from)?; + signal::kill(self.child_pid, Signal::SIGKILL).map_err(Error::from)?; } } } @@ -235,7 +237,7 @@ impl PtyProcess { impl Drop for PtyProcess { fn drop(&mut self) { - if let Some(wait::WaitStatus::StillAlive) = self.status() { + if let Some(WaitStatus::StillAlive) = self.status() { self.exit().expect("cannot exit"); } } @@ -244,7 +246,7 @@ impl Drop for PtyProcess { #[cfg(test)] mod tests { use super::*; - use nix::sys::{signal, wait}; + use nix::sys::wait; use std::io::{BufRead, BufReader, LineWriter, Write}; #[test] @@ -264,7 +266,7 @@ mod tests { thread::sleep(time::Duration::from_millis(100)); writer.write_all(&[3])?; // send ^C writer.flush()?; - let should = wait::WaitStatus::Signaled(process.child_pid, signal::Signal::SIGINT, false); + let should = WaitStatus::Signaled(process.child_pid, Signal::SIGINT, false); assert_eq!(should, wait::waitpid(process.child_pid, None).unwrap()); Ok(()) } From b7c7dcfd02e11a66d6e01e8ec3b9ddcc248668bd Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 16 Mar 2026 09:34:44 -0500 Subject: [PATCH 17/36] docs(process): Encourage get_file_handle --- src/process.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/process.rs b/src/process.rs index 1a928c5f..c1db0b7c 100644 --- a/src/process.rs +++ b/src/process.rs @@ -45,8 +45,7 @@ pub use wait::WaitStatus; /// # fn main() { /// /// let mut process = PtyProcess::new(Command::new("cat")).expect("could not execute cat"); -/// let fd = dup(&process.pty).unwrap(); -/// let f = File::from(fd); +/// let f = process.get_file_handle().unwrap(); /// let mut writer = LineWriter::new(&f); /// let mut reader = BufReader::new(&f); /// process.exit().expect("could not terminate process"); From 2aa73c8ab62a57308e1220d30f0fe28f2576c9d6 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 16 Mar 2026 09:37:03 -0500 Subject: [PATCH 18/36] feat(reader): Add builder methods to Options --- src/reader.rs | 32 ++++++++++++++++++-------------- src/session.rs | 17 ++--------------- 2 files changed, 20 insertions(+), 29 deletions(-) diff --git a/src/reader.rs b/src/reader.rs index b49c298f..6b1efd09 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -19,6 +19,22 @@ pub struct Options { pub strip_ansi_escape_codes: bool, } +impl Options { + pub fn new() -> Self { + Default::default() + } + + pub fn timeout_ms(mut self, timeout_ms: Option) -> Self { + self.timeout_ms = timeout_ms; + self + } + + pub fn strip_ansi_escape_codes(mut self, yes: bool) -> Self { + self.strip_ansi_escape_codes = yes; + self + } +} + /// Non blocking reader /// /// Typically you'd need that to check for output of a process without blocking your thread. @@ -408,13 +424,7 @@ mod tests { #[test] fn test_skip_partial_ansi_code() { let f = io::Cursor::new("\x1b[31;1;4mHello\x1b[1"); - let mut r = NBReader::new( - f, - Options { - timeout_ms: None, - strip_ansi_escape_codes: true, - }, - ); + let mut r = NBReader::new(f, Options::new().strip_ansi_escape_codes(true)); let bytes = r .read_until(&ReadUntil::String("Hello".to_owned())) .unwrap(); @@ -425,13 +435,7 @@ mod tests { #[test] fn test_skip_ansi_codes() { let f = io::Cursor::new("\x1b[31;1;4mHello\x1b[0m"); - let mut r = NBReader::new( - f, - Options { - timeout_ms: None, - strip_ansi_escape_codes: true, - }, - ); + let mut r = NBReader::new(f, Options::new().strip_ansi_escape_codes(true)); let bytes = r .read_until(&ReadUntil::String("Hello".to_owned())) .unwrap(); diff --git a/src/session.rs b/src/session.rs index 273cbb8c..8698ecc0 100644 --- a/src/session.rs +++ b/src/session.rs @@ -245,13 +245,7 @@ fn tokenize_command(program: &str) -> Result, Error> { /// See [`spawn`] pub fn spawn_command(command: Command, timeout_ms: Option) -> Result { - spawn_with_options( - command, - Options { - timeout_ms, - strip_ansi_escape_codes: false, - }, - ) + spawn_with_options(command, Options::new().timeout_ms(timeout_ms)) } /// See [`spawn`] @@ -456,14 +450,7 @@ pub fn spawn_stream( writer: W, timeout_ms: Option, ) -> StreamSession { - StreamSession::new( - reader, - writer, - Options { - timeout_ms, - strip_ansi_escape_codes: false, - }, - ) + StreamSession::new(reader, writer, Options::new().timeout_ms(timeout_ms)) } #[cfg(test)] From 3c6456338d543e746e3be3ca99ea796c5f7f04a7 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 16 Mar 2026 09:38:13 -0500 Subject: [PATCH 19/36] feat(session): Add accessors to PtySession --- examples/exit_code.rs | 4 ++-- src/session.rs | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/examples/exit_code.rs b/examples/exit_code.rs index dfb57065..e341fd51 100644 --- a/examples/exit_code.rs +++ b/examples/exit_code.rs @@ -8,13 +8,13 @@ use rexpect::spawn; /// Output (stdout and stderr): cat: /this/does/not/exist: No such file or directory fn main() -> Result<(), Error> { let p = spawn("cat /etc/passwd", Some(2000))?; - match p.process.wait() { + match p.process().wait() { Ok(WaitStatus::Exited(_, 0)) => println!("cat exited with code 0, all good!"), _ => println!("cat exited with code >0, or it was killed"), } let mut p = spawn("cat /this/does/not/exist", Some(2000))?; - match p.process.wait() { + match p.process().wait() { Ok(WaitStatus::Exited(_, 0)) => println!("cat succeeded"), Ok(WaitStatus::Exited(_, c)) => { println!("Cat failed with exit code {c}"); diff --git a/src/session.rs b/src/session.rs index 8698ecc0..7cc734d6 100644 --- a/src/session.rs +++ b/src/session.rs @@ -210,6 +210,14 @@ impl PtySession { let stream = StreamSession::new(reader, f, options); Ok(Self { process, stream }) } + + pub fn process(&self) -> &PtyProcess { + &self.process + } + + pub fn process_mut(&mut self) -> &mut PtyProcess { + &mut self.process + } } /// Start command in background in a pty session (pty fork) and return a struct From 8a0b7612e14be2de0ecbffdd039ede0f67b86cec Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 16 Mar 2026 09:43:12 -0500 Subject: [PATCH 20/36] docs(session): Reorder PtyReplSession fields --- src/session.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/session.rs b/src/session.rs index 7cc734d6..5d13e06f 100644 --- a/src/session.rs +++ b/src/session.rs @@ -273,14 +273,14 @@ pub fn spawn_with_options(command: Command, options: Options) -> Result>> " for python - pub prompt: String, - /// The `pty_session` you prepared before (initiating the shell, maybe set a custom prompt, etc.) /// /// See [`spawn_bash`] for an example pub pty_session: PtySession, + /// The prompt, used for `wait_for_prompt`, e.g. ">>> " for python + pub prompt: String, + /// If set, then the `quit_command` is called when this object is dropped /// you need to provide this if the shell you're testing is not killed by just sending /// SIGTERM @@ -425,8 +425,8 @@ pub fn spawn_bash(timeout: Option) -> Result { spawn_command(c, timeout).and_then(|p| { let new_prompt = "[REXPECT_PROMPT>"; let mut pb = PtyReplSession { - prompt: new_prompt.to_owned(), pty_session: p, + prompt: new_prompt.to_owned(), quit_command: Some("quit".to_owned()), echo_on: false, }; From cd038c5b1a6bb3dc6e21c1025fe3f587b833e5b2 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 16 Mar 2026 09:47:46 -0500 Subject: [PATCH 21/36] feat(session): Provide constructor for PtyReplSession --- src/session.rs | 40 ++++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/src/session.rs b/src/session.rs index 5d13e06f..f655d0be 100644 --- a/src/session.rs +++ b/src/session.rs @@ -273,25 +273,45 @@ pub fn spawn_with_options(command: Command, options: Options) -> Result>> " for python pub prompt: String, - - /// If set, then the `quit_command` is called when this object is dropped - /// you need to provide this if the shell you're testing is not killed by just sending - /// SIGTERM pub quit_command: Option, + pub echo_on: bool, +} + +impl PtyReplSession { + /// Start a REPL session + /// + /// `prompt`: used for [`Self::wait_for_prompt`], e.g. ">>> " for python + /// + /// See [`spawn_bash`] for an example + pub fn new(pty_session: PtySession, prompt: String) -> Self { + Self { + pty_session, + prompt, + quit_command: None, + echo_on: false, + } + } + + /// Called when this object is dropped. + /// + /// You need to provide this if the shell you're testing is not killed by just sending + /// SIGTERM. + pub fn quit_command(mut self, cmd: Option) -> Self { + self.quit_command = cmd; + self + } /// Set this to true if the repl has echo on (i.e. sends user input to stdout) /// /// Although echo is set off at pty fork (see `PtyProcess::new`) a few repls still /// seem to be able to send output. /// You may need to try with true first, and if tests fail set this to false. - pub echo_on: bool, + pub fn echo_on(mut self, yes: bool) -> Self { + self.echo_on = yes; + self + } } impl PtyReplSession { From 0498828aa15f174065f845c80b58d85d19a53af4 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 16 Mar 2026 09:54:27 -0500 Subject: [PATCH 22/36] docs: Update changelog --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bb2b6b1..831a30fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] - ReleaseDate +### Feature + +- Add builder methods to `Options` +- Add `PtySession::new` +- Add `PtySession::process` +- Add `PtyReplSession::new`, `PtyReplSession::echo_on`, `PtyReplSession::quit_command` + +### Fixes + +- Make `process::wait::WaitStatus` and `process::signal::Signal` available directly in `process` + +### Compatibility + +- MSRV is 1.85 + ## [0.6.3] - 2026-01-15 ### Fixes From 7e96abeaa93b2de2fc54175cf99c398b682121fd Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 16 Mar 2026 09:54:37 -0500 Subject: [PATCH 23/36] chore: Release rexpect version 0.6.4 --- CHANGELOG.md | 5 ++++- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 831a30fd..5c2e6d01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] - ReleaseDate +## [0.6.4] - 2026-03-16 + ### Feature - Add builder methods to `Options` @@ -130,7 +132,8 @@ All `exp_*` methods now also return the yet unread string and/or the matched str - try_read was blocking when there was no char ready (!) -> fixed -[Unreleased]: https://github.com/rust-cli/rexpect/compare/v0.6.3...HEAD +[Unreleased]: https://github.com/rust-cli/rexpect/compare/v0.6.4...HEAD +[0.6.4]: https://github.com/rust-cli/rexpect/compare/v0.6.3...v0.6.4 [0.6.3]: https://github.com/rust-cli/rexpect/compare/v0.6.2...v0.6.3 [0.6.2]: https://github.com/rust-cli/rexpect/compare/v0.6.1...v0.6.2 [0.6.1]: https://github.com/rust-cli/rexpect/compare/v0.6.0...v0.6.1 diff --git a/Cargo.lock b/Cargo.lock index ca1e9017..7c517230 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "aho-corasick" @@ -192,7 +192,7 @@ checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "rexpect" -version = "0.6.3" +version = "0.6.4" dependencies = [ "comma", "nix", diff --git a/Cargo.toml b/Cargo.toml index f14f85cd..3173b50c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,7 +97,7 @@ lto = true [package] name = "rexpect" description = "Interact with unix processes/bash the same way as pexpect or Don libes expect does" -version = "0.6.3" +version = "0.6.4" categories = ["os::unix-apis"] keywords = ["pty", "automation", "testing", "expect", "pexpect"] repository.workspace = true From 061ae892706638c34628871aabbe79a460e55496 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 16 Mar 2026 08:56:53 -0500 Subject: [PATCH 24/36] fix(error)!: Make Error non-exhaustive --- src/error.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/error.rs b/src/error.rs index a64b906a..06ae2798 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,7 @@ use std::time; #[derive(Debug, thiserror::Error)] +#[non_exhaustive] pub enum Error { #[error("EOF (End of File): Expected {:?} but got EOF after reading {:?} process terminated with {:?}", .expected, .got, .exit_code.as_ref().unwrap_or(&"unknown".to_owned()))] EOF { From 642ca73c73a4491f4be19c6dd7b0a8530b4c82b2 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 16 Mar 2026 09:00:00 -0500 Subject: [PATCH 25/36] fix(process)!: Make PtyProcess fields private --- src/process.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/process.rs b/src/process.rs index c1db0b7c..ddaff8bf 100644 --- a/src/process.rs +++ b/src/process.rs @@ -57,8 +57,8 @@ pub use wait::WaitStatus; /// # } /// ``` pub struct PtyProcess { - pub pty: PtyMaster, - pub child_pid: Pid, + pty: PtyMaster, + pub(crate) child_pid: Pid, kill_timeout: Option, } From 87864790e2ba66a01231c2500fc82714aad64fbb Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 16 Mar 2026 09:05:24 -0500 Subject: [PATCH 26/36] fix(process)!: Dont re-export whole modules --- src/process.rs | 2 +- src/session.rs | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/process.rs b/src/process.rs index ddaff8bf..979bd142 100644 --- a/src/process.rs +++ b/src/process.rs @@ -5,6 +5,7 @@ use nix; use nix::fcntl::{OFlag, open}; use nix::libc::STDERR_FILENO; use nix::pty::{PtyMaster, grantpt, posix_openpt, unlockpt}; +use nix::sys::{signal, wait}; use nix::sys::{stat, termios}; use nix::unistd::{ ForkResult, Pid, close, dup, dup2_stderr, dup2_stdin, dup2_stdout, fork, setsid, @@ -17,7 +18,6 @@ use std::os::unix::process::CommandExt; use std::process::Command; use std::{thread, time}; -pub use nix::sys::{signal, wait}; pub use signal::Signal; pub use wait::WaitStatus; diff --git a/src/session.rs b/src/session.rs index f655d0be..9be349d8 100644 --- a/src/session.rs +++ b/src/session.rs @@ -484,17 +484,15 @@ pub fn spawn_stream( #[cfg(test)] mod tests { use super::*; + use nix::sys::wait; #[test] fn test_read_line() -> Result<(), Error> { let mut s = spawn("cat", Some(100000))?; s.send_line("hans")?; assert_eq!("hans", s.read_line()?); - let should = crate::process::wait::WaitStatus::Signaled( - s.process.child_pid, - crate::process::signal::Signal::SIGTERM, - false, - ); + let should = + wait::WaitStatus::Signaled(s.process.child_pid, crate::process::Signal::SIGTERM, false); assert_eq!(should, s.process.exit()?); Ok(()) } From 568837ffa007a8f052c136300e57e6dcd7139799 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 16 Mar 2026 08:10:48 -0500 Subject: [PATCH 27/36] fix(reader)!: Make find private Not seeing why the use case for this to be public. We need to better understand that to see how it should be public. --- src/reader.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reader.rs b/src/reader.rs index 6b1efd09..985968f4 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -268,7 +268,7 @@ impl fmt::Display for ReadUntil { /// Tuple with match positions: /// 1. position before match (0 in case of EOF and Nbytes) /// 2. position after match -pub fn find(needle: &ReadUntil, buffer: &str, eof: bool) -> Option<(usize, usize)> { +fn find(needle: &ReadUntil, buffer: &str, eof: bool) -> Option<(usize, usize)> { match needle { ReadUntil::String(s) => buffer.find(s).map(|pos| (pos, pos + s.len())), ReadUntil::Regex(pattern) => pattern.find(buffer).map(|mat| (mat.start(), mat.end())), From 776dbe9cfc89defcf8616f1f2e7b84f3bfa5c923 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 16 Mar 2026 08:15:02 -0500 Subject: [PATCH 28/36] fix(reader)!: Make ReadUntil non-exhaustive --- src/reader.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/reader.rs b/src/reader.rs index 985968f4..dde5b115 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -214,6 +214,7 @@ impl NBReader { /// See [`NBReader::read_until`] /// /// Note that when used with a tty the lines end with \r\n +#[non_exhaustive] pub enum ReadUntil { /// Searches for string (use '\n'.`to_string()` to search for newline). /// From 3256aa86c68ac7f58eceb48adddaa95ae65dbf4d Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 16 Mar 2026 09:22:28 -0500 Subject: [PATCH 29/36] fix(reader)!: Make `Options` opaque --- src/reader.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/reader.rs b/src/reader.rs index dde5b115..e8e3e984 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -14,9 +14,9 @@ pub struct Options { /// `None`: `read_until` is blocking forever. This is probably not what you want /// /// `Some(millis)`: after millis milliseconds a timeout error is raised - pub timeout_ms: Option, + pub(crate) timeout_ms: Option, /// Whether to filter out escape codes, such as colors. - pub strip_ansi_escape_codes: bool, + pub(crate) strip_ansi_escape_codes: bool, } impl Options { From f1dd095b6d1f703b8f8e71f2db444547f9efc2d0 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 16 Mar 2026 09:27:19 -0500 Subject: [PATCH 30/36] fix(session): Make StreamSession fields private --- src/session.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/session.rs b/src/session.rs index 9be349d8..5995e18e 100644 --- a/src/session.rs +++ b/src/session.rs @@ -12,8 +12,8 @@ use std::process::Command; use tempfile; pub struct StreamSession { - pub writer: LineWriter, - pub reader: NBReader, + writer: LineWriter, + reader: NBReader, } impl StreamSession { From 22a230d07017e63ca5bcdf62bb11c62f897b983c Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 16 Mar 2026 09:29:31 -0500 Subject: [PATCH 31/36] fix(session)!: Make PtySession fields private --- src/session.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/session.rs b/src/session.rs index 5995e18e..354585ec 100644 --- a/src/session.rs +++ b/src/session.rs @@ -166,8 +166,8 @@ impl StreamSession { /// Interact with a process with read/write/signals, etc. #[allow(dead_code)] pub struct PtySession { - pub process: PtyProcess, - pub stream: StreamSession, + process: PtyProcess, + stream: StreamSession, } // make StreamSession's methods available directly From 029796af30efe929aa57a5808da9d5d6fd02fe55 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 16 Mar 2026 09:48:45 -0500 Subject: [PATCH 32/36] fix(session)!: Make PtyReplSession fields private --- examples/repl.rs | 11 +++-------- src/session.rs | 8 ++++---- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/examples/repl.rs b/examples/repl.rs index 4a305e46..674b4cbe 100644 --- a/examples/repl.rs +++ b/examples/repl.rs @@ -5,20 +5,15 @@ use rexpect::session::PtyReplSession; use rexpect::spawn; fn ed_session() -> Result { - let mut ed = PtyReplSession { + let mut ed = PtyReplSession::new(spawn("/bin/ed -p '> '", Some(2000))?, "> ".to_owned()) // for `echo_on` you need to figure that out by trial and error. // For bash and python repl it is false - echo_on: false, - - // used for `wait_for_prompt()` - prompt: "> ".to_owned(), - pty_session: spawn("/bin/ed -p '> '", Some(2000))?, + .echo_on(false) // command which is sent when the instance of this struct is dropped // in the below example this is not needed, but if you don't explicitly // exit a REPL then rexpect tries to send a SIGTERM and depending on the repl // this does not end the repl and would end up in an error - quit_command: Some("Q".to_owned()), - }; + .quit_command(Some("Q".to_owned())); ed.wait_for_prompt()?; Ok(ed) } diff --git a/src/session.rs b/src/session.rs index 354585ec..c48ae1e2 100644 --- a/src/session.rs +++ b/src/session.rs @@ -273,10 +273,10 @@ pub fn spawn_with_options(command: Command, options: Options) -> Result, - pub echo_on: bool, + pty_session: PtySession, + prompt: String, + quit_command: Option, + echo_on: bool, } impl PtyReplSession { From 633db9fe5bad79f1b122029a9ec081b09ed7d6bd Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 16 Mar 2026 10:04:18 -0500 Subject: [PATCH 33/36] docs: Update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c2e6d01..e063d5b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] - ReleaseDate +### Breaking Changes + +- Made fields private: `PtyReplSession`, `PtySession`, `StreamSession`, `Options` +- Made `non_exhaustive`: `Options`, `Error` +- Remove from the API: `find`, `process::wait`, `process::signal` + ## [0.6.4] - 2026-03-16 ### Feature From 825e5154d6bffd2e0dfd93f3c011f8530de4c79f Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 16 Mar 2026 10:04:25 -0500 Subject: [PATCH 34/36] chore: Release rexpect version 0.7.0 --- CHANGELOG.md | 5 ++++- Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e063d5b1..6df71d83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] - ReleaseDate +## [0.7.0] - 2026-03-16 + ### Breaking Changes - Made fields private: `PtyReplSession`, `PtySession`, `StreamSession`, `Options` @@ -138,7 +140,8 @@ All `exp_*` methods now also return the yet unread string and/or the matched str - try_read was blocking when there was no char ready (!) -> fixed -[Unreleased]: https://github.com/rust-cli/rexpect/compare/v0.6.4...HEAD +[Unreleased]: https://github.com/rust-cli/rexpect/compare/v0.7.0...HEAD +[0.7.0]: https://github.com/rust-cli/rexpect/compare/v0.6.4...v0.7.0 [0.6.4]: https://github.com/rust-cli/rexpect/compare/v0.6.3...v0.6.4 [0.6.3]: https://github.com/rust-cli/rexpect/compare/v0.6.2...v0.6.3 [0.6.2]: https://github.com/rust-cli/rexpect/compare/v0.6.1...v0.6.2 diff --git a/Cargo.lock b/Cargo.lock index 7c517230..724e282a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -192,7 +192,7 @@ checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "rexpect" -version = "0.6.4" +version = "0.7.0" dependencies = [ "comma", "nix", diff --git a/Cargo.toml b/Cargo.toml index 3173b50c..383d86df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,7 +97,7 @@ lto = true [package] name = "rexpect" description = "Interact with unix processes/bash the same way as pexpect or Don libes expect does" -version = "0.6.4" +version = "0.7.0" categories = ["os::unix-apis"] keywords = ["pty", "automation", "testing", "expect", "pexpect"] repository.workspace = true From 6a5d51e9bcdf75f2b0b8262ed14e7f167f08d9bd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 01:24:40 +0000 Subject: [PATCH 35/36] chore(deps): Update j178/prek-action action to v2 --- .github/workflows/pre-commit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index ef1a54ac..1a96a38b 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -23,6 +23,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: j178/prek-action@v1 + - uses: j178/prek-action@v2 with: prek-version: '0.2.27' From 4f2c2446c06915a415e7ded2ef9c4dad2ee095cd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 01:02:16 +0000 Subject: [PATCH 36/36] chore(deps): Update pre-commit hook crate-ci/typos to v1.46.0 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4c838708..bbd1af3f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: - id: check-case-conflict - id: detect-private-key - repo: https://github.com/crate-ci/typos - rev: v1.44.0 + rev: v1.46.0 hooks: - id: typos - repo: https://github.com/crate-ci/committed