Skip to content

Commit 9eb61dd

Browse files
bors[bot]phaer
andauthored
Merge #103
103: Add option to skip ansi escape codes r=souze a=phaer This allows users to ignore ansi escape codes, such as terminal colors and would therefore close #25. As you've noticed, i needed to add the new parameter `skip_ansi_escape_codes` in quite a few places. Maybe it would be a good idea to introduce an `Options` struct for `timeout` and `skip_ansi_escape_codes`, with default values? I could do that, but wanted to keep this PR focused. I've considered using [strip_ansi_escapes](https://docs.rs/strip-ansi-escapes/latest/strip_ansi_escapes/index.html#), mentioned in the issue. But that uses VTE which uses a state machine to interpret the escape codes. But as we are not interested in interpreting them, just skipping them and ansi escape codes are just the byte 27 inserted in a string, followed by "[", zero or more numbers separated by ";" and ending with a letter; We optionally just skip those in our non-blocking reader in order to efficently ignore them during matching without having to traverse the whole buffer a second time. A minimal test case is also added Co-authored-by: phaer <[email protected]>
2 parents eee779b + e992f99 commit 9eb61dd

File tree

3 files changed

+117
-26
lines changed

3 files changed

+117
-26
lines changed

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ pub mod reader;
7070
pub mod session;
7171

7272
pub use reader::ReadUntil;
73-
pub use session::{spawn, spawn_bash, spawn_python, spawn_stream};
73+
pub use session::{spawn, spawn_bash, spawn_python, spawn_stream, spawn_with_options};
7474

7575
// include the README.md here to test its doc
7676
#[doc = include_str!("../README.md")]

src/reader.rs

Lines changed: 72 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,18 @@ pub fn find(needle: &ReadUntil, buffer: &str, eof: bool) -> Option<(usize, usize
9898
}
9999
}
100100

101+
/// Options for NBReader
102+
///
103+
/// - timeout:
104+
/// + `None`: read_until is blocking forever. This is probably not what you want
105+
/// + `Some(millis)`: after millis milliseconds a timeout error is raised
106+
/// - strip_ansi_escape_codes: Whether to filter out escape codes, such as colors.
107+
#[derive(Default)]
108+
pub struct Options {
109+
pub timeout_ms: Option<u64>,
110+
pub strip_ansi_escape_codes: bool,
111+
}
112+
101113
/// Non blocking reader
102114
///
103115
/// Typically you'd need that to check for output of a process without blocking your thread.
@@ -116,16 +128,16 @@ impl NBReader {
116128
/// # Arguments:
117129
///
118130
/// - f: file like object
119-
/// - timeout:
120-
/// + `None`: read_until is blocking forever. This is probably not what you want
121-
/// + `Some(millis)`: after millis milliseconds a timeout error is raised
122-
pub fn new<R: Read + Send + 'static>(f: R, timeout: Option<u64>) -> NBReader {
131+
/// - options: see `Options`
132+
pub fn new<R: Read + Send + 'static>(f: R, options: Options) -> NBReader {
123133
let (tx, rx) = channel();
124134

125135
// spawn a thread which reads one char and sends it to tx
126136
thread::spawn(move || -> Result<(), Error> {
127137
let mut reader = BufReader::new(f);
128138
let mut byte = [0u8];
139+
let mut in_escape_code = false;
140+
129141
loop {
130142
match reader.read(&mut byte) {
131143
Ok(0) => {
@@ -134,8 +146,16 @@ impl NBReader {
134146
break;
135147
}
136148
Ok(_) => {
137-
tx.send(Ok(PipedChar::Char(byte[0])))
138-
.map_err(|_| Error::MpscSendError)?;
149+
if options.strip_ansi_escape_codes && byte[0] == 27 {
150+
in_escape_code = true;
151+
} else if options.strip_ansi_escape_codes && in_escape_code {
152+
if char::from(byte[0]).is_alphabetic() {
153+
in_escape_code = false;
154+
}
155+
} else {
156+
tx.send(Ok(PipedChar::Char(byte[0])))
157+
.map_err(|_| Error::MpscSendError)?;
158+
}
139159
}
140160
Err(error) => {
141161
tx.send(Err(PipeError::IO(error)))
@@ -153,7 +173,7 @@ impl NBReader {
153173
reader: rx,
154174
buffer: String::with_capacity(1024),
155175
eof: false,
156-
timeout: timeout.map(time::Duration::from_millis),
176+
timeout: options.timeout_ms.map(time::Duration::from_millis),
157177
}
158178
}
159179

@@ -204,11 +224,11 @@ impl NBReader {
204224
///
205225
/// ```
206226
/// # use std::io::Cursor;
207-
/// use rexpect::reader::{NBReader, ReadUntil, Regex};
227+
/// use rexpect::reader::{NBReader, ReadUntil, Regex, Options};
208228
/// // instead of a Cursor you would put your process output or file here
209229
/// let f = Cursor::new("Hello, miss!\n\
210230
/// What do you mean: 'miss'?");
211-
/// let mut e = NBReader::new(f, None);
231+
/// let mut e = NBReader::new(f, Options::default());
212232
///
213233
/// let (first_line, _) = e.read_until(&ReadUntil::String('\n'.to_string())).unwrap();
214234
/// assert_eq!("Hello, miss!", &first_line);
@@ -230,6 +250,7 @@ impl NBReader {
230250

231251
loop {
232252
self.read_into_buffer()?;
253+
233254
if let Some(tuple_pos) = find(needle, &self.buffer, self.eof) {
234255
let first = self.buffer.drain(..tuple_pos.0).collect();
235256
let second = self.buffer.drain(..tuple_pos.1 - tuple_pos.0).collect();
@@ -287,7 +308,7 @@ mod tests {
287308
#[test]
288309
fn test_expect_melon() {
289310
let f = io::Cursor::new("a melon\r\n");
290-
let mut r = NBReader::new(f, None);
311+
let mut r = NBReader::new(f, Options::default());
291312
assert_eq!(
292313
("a melon".to_string(), "\r\n".to_string()),
293314
r.read_until(&ReadUntil::String("\r\n".to_string()))
@@ -304,7 +325,7 @@ mod tests {
304325
#[test]
305326
fn test_regex() {
306327
let f = io::Cursor::new("2014-03-15");
307-
let mut r = NBReader::new(f, None);
328+
let mut r = NBReader::new(f, Options::default());
308329
let re = Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap();
309330
assert_eq!(
310331
("".to_string(), "2014-03-15".to_string()),
@@ -316,7 +337,7 @@ mod tests {
316337
#[test]
317338
fn test_regex2() {
318339
let f = io::Cursor::new("2014-03-15");
319-
let mut r = NBReader::new(f, None);
340+
let mut r = NBReader::new(f, Options::default());
320341
let re = Regex::new(r"-\d{2}-").unwrap();
321342
assert_eq!(
322343
("2014".to_string(), "-03-".to_string()),
@@ -328,7 +349,7 @@ mod tests {
328349
#[test]
329350
fn test_nbytes() {
330351
let f = io::Cursor::new("abcdef");
331-
let mut r = NBReader::new(f, None);
352+
let mut r = NBReader::new(f, Options::default());
332353
assert_eq!(
333354
("".to_string(), "ab".to_string()),
334355
r.read_until(&ReadUntil::NBytes(2)).expect("2 bytes")
@@ -346,7 +367,7 @@ mod tests {
346367
#[test]
347368
fn test_any_with_multiple_possible_matches() {
348369
let f = io::Cursor::new("zero one two three four five");
349-
let mut r = NBReader::new(f, None);
370+
let mut r = NBReader::new(f, Options::default());
350371

351372
let result = r
352373
.read_until(&ReadUntil::Any(vec![
@@ -361,7 +382,7 @@ mod tests {
361382
#[test]
362383
fn test_any_with_same_start_different_length() {
363384
let f = io::Cursor::new("hi hello");
364-
let mut r = NBReader::new(f, None);
385+
let mut r = NBReader::new(f, Options::default());
365386

366387
let result = r
367388
.read_until(&ReadUntil::Any(vec![
@@ -376,18 +397,52 @@ mod tests {
376397
#[test]
377398
fn test_eof() {
378399
let f = io::Cursor::new("lorem ipsum dolor sit amet");
379-
let mut r = NBReader::new(f, None);
400+
let mut r = NBReader::new(f, Options::default());
380401
r.read_until(&ReadUntil::NBytes(2)).expect("2 bytes");
381402
assert_eq!(
382403
("".to_string(), "rem ipsum dolor sit amet".to_string()),
383404
r.read_until(&ReadUntil::EOF).expect("reading until EOF")
384405
);
385406
}
386407

408+
#[test]
409+
fn test_skip_partial_ansi_code() {
410+
let f = io::Cursor::new("\x1b[31;1;4mHello\x1b[1");
411+
let mut r = NBReader::new(
412+
f,
413+
Options {
414+
timeout_ms: None,
415+
strip_ansi_escape_codes: true,
416+
},
417+
);
418+
let bytes = r
419+
.read_until(&ReadUntil::String("Hello".to_string()))
420+
.unwrap();
421+
assert_eq!(bytes, ("".to_string(), "Hello".to_string()));
422+
assert_eq!(None, r.try_read());
423+
}
424+
425+
#[test]
426+
fn test_skip_ansi_codes() {
427+
let f = io::Cursor::new("\x1b[31;1;4mHello\x1b[0m");
428+
let mut r = NBReader::new(
429+
f,
430+
Options {
431+
timeout_ms: None,
432+
strip_ansi_escape_codes: true,
433+
},
434+
);
435+
let bytes = r
436+
.read_until(&ReadUntil::String("Hello".to_string()))
437+
.unwrap();
438+
assert_eq!(bytes, ("".to_string(), "Hello".to_string()));
439+
assert_eq!(None, r.try_read());
440+
}
441+
387442
#[test]
388443
fn test_try_read() {
389444
let f = io::Cursor::new("lorem");
390-
let mut r = NBReader::new(f, None);
445+
let mut r = NBReader::new(f, Options::default());
391446
let bytes = r.read_until(&ReadUntil::NBytes(4)).unwrap();
392447
assert!(bytes.0.is_empty());
393448
assert_eq!(bytes.1, "lore");

src/session.rs

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
33
use crate::error::Error; // load error-chain
44
use crate::process::PtyProcess;
5-
pub use crate::reader::ReadUntil;
65
use crate::reader::{NBReader, Regex};
6+
pub use crate::reader::{Options, ReadUntil};
77
use std::fs::File;
88
use std::io::prelude::*;
99
use std::io::LineWriter;
@@ -17,10 +17,10 @@ pub struct StreamSession<W: Write> {
1717
}
1818

1919
impl<W: Write> StreamSession<W> {
20-
pub fn new<R: Read + Send + 'static>(reader: R, writer: W, timeout_ms: Option<u64>) -> Self {
20+
pub fn new<R: Read + Send + 'static>(reader: R, writer: W, options: Options) -> Self {
2121
Self {
2222
writer: LineWriter::new(writer),
23-
reader: NBReader::new(reader, timeout_ms),
23+
reader: NBReader::new(reader, options),
2424
}
2525
}
2626

@@ -172,11 +172,29 @@ impl DerefMut for PtySession {
172172
}
173173

174174
/// Start a process in a tty session, write and read from it
175+
///
176+
/// # Example
177+
///
178+
/// ```
179+
///
180+
/// use rexpect::spawn;
181+
/// # use rexpect::error::Error;
182+
///
183+
/// # fn main() {
184+
/// # || -> Result<(), Error> {
185+
/// let mut s = spawn("cat", Some(1000))?;
186+
/// s.send_line("hello, polly!")?;
187+
/// let line = s.read_line()?;
188+
/// assert_eq!("hello, polly!", line);
189+
/// # Ok(())
190+
/// # }().expect("test failed");
191+
/// # }
192+
/// ```
175193
impl PtySession {
176-
fn new(process: PtyProcess, timeout_ms: Option<u64>) -> Result<Self, Error> {
194+
fn new(process: PtyProcess, options: Options) -> Result<Self, Error> {
177195
let f = process.get_file_handle()?;
178196
let reader = f.try_clone()?;
179-
let stream = StreamSession::new(reader, f, timeout_ms);
197+
let stream = StreamSession::new(reader, f, options);
180198
Ok(Self { process, stream })
181199
}
182200
}
@@ -214,14 +232,25 @@ pub fn spawn(program: &str, timeout_ms: Option<u64>) -> Result<PtySession, Error
214232

215233
/// See `spawn`
216234
pub fn spawn_command(command: Command, timeout_ms: Option<u64>) -> Result<PtySession, Error> {
235+
spawn_with_options(
236+
command,
237+
Options {
238+
timeout_ms,
239+
strip_ansi_escape_codes: false,
240+
},
241+
)
242+
}
243+
244+
/// See `spawn`
245+
pub fn spawn_with_options(command: Command, options: Options) -> Result<PtySession, Error> {
217246
#[cfg(feature = "which")]
218247
{
219248
let _ = which::which(command.get_program())?;
220249
}
221250
let mut process = PtyProcess::new(command)?;
222-
process.set_kill_timeout(timeout_ms);
251+
process.set_kill_timeout(options.timeout_ms);
223252

224-
PtySession::new(process, timeout_ms)
253+
PtySession::new(process, options)
225254
}
226255

227256
/// A repl session: e.g. bash or the python shell:
@@ -407,7 +436,14 @@ pub fn spawn_stream<R: Read + Send + 'static, W: Write>(
407436
writer: W,
408437
timeout_ms: Option<u64>,
409438
) -> StreamSession<W> {
410-
StreamSession::new(reader, writer, timeout_ms)
439+
StreamSession::new(
440+
reader,
441+
writer,
442+
Options {
443+
timeout_ms,
444+
strip_ansi_escape_codes: false,
445+
},
446+
)
411447
}
412448

413449
#[cfg(test)]

0 commit comments

Comments
 (0)