From 061ae892706638c34628871aabbe79a460e55496 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 16 Mar 2026 08:56:53 -0500 Subject: [PATCH 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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 {