Rust中如何区分Lisp REPL的输入提示与普通输出?
如何区分Lisp REPL的输入提示与普通输出?
我正在启动一个Command,向其stdin写入数据并读取其stdout,将输出管道传输至xterm.js。这个命令是长期运行的交互式Lisp REPL(比如SBCL),代码能正常运行,但遇到一个问题:无法判断该进程是在请求输入还是仅输出内容。比如从stdout读到不带换行符的> ,可能是进程在提示用户输入,但也可能是某个长时间运行的求值过程恰好输出了> 却尚未结束,怎么区分这两种情况?
我的Rust代码如下:
// using the os_pipe crate to create a new pipe with both, stdout and stderr, // to capture both, in correct order let (reader, writer_out) = os_pipe::pipe().unwrap(); let writer_err = writer_out.try_clone().unwrap(); let mut c = Command::new("path/to/sbcl"); let mut child = c .stdin(Stdio::piped()) .stdout(writer_out) .stderr(writer_err) .spawn() .expect("command failed to start"); let (sender, receiver) = bounded::<String>(0); let mut f = BufReader::new(reader); thread::spawn(move || { let mut buf = String::new(); // using the utf8_chars crate to iterate over chars of the bufreader for c in f.chars().map(|x| x.unwrap()) { buf.push(c); if c == '\n' { sender.send(buf.clone()).expect("Failed to send."); buf.clear(); } // matches "> " or similar // if a "> " was sent without newline, I still want to capture it else if RE_PROMPT.is_match(&buf) { sender.send(buf.clone()).expect("Failed to send."); buf.clear(); } } });
解决方案
1. 使用伪终端(PTY)替代普通管道
这是最可靠的方案,因为交互式REPL通常会针对终端环境调整行为,只有连接到终端时才会正确输出输入提示,还能通过终端状态判断进程是否在等待输入。
在Rust中可以用pty crate实现:
use pty::fork; use std::io::{Read, Write}; use std::sync::mpsc::{bounded, Sender}; use std::thread; let (sender, receiver) = bounded::<String>(0); // 创建伪终端并启动子进程 let mut fork = fork().unwrap(); if fork.is_child() { // 在子进程中执行SBCL std::process::Command::new("path/to/sbcl") .status() .unwrap(); std::process::exit(0); } // 父进程读取PTY输出 let mut pty = fork.pty().unwrap(); thread::spawn(move || { let mut buf = [0u8; 1024]; let mut output = String::new(); loop { match pty.read(&mut buf) { Ok(n) if n > 0 => { let chunk = String::from_utf8_lossy(&buf[..n]); output.push_str(&chunk); // 匹配输入提示(可根据REPL实际提示调整正则,比如SBCL默认是"* ") if RE_PROMPT.is_match(&output) { // 写入空字节测试进程是否阻塞等待输入 if pty.write_all(b"\n").is_ok() { sender.send(output.clone()).expect("Failed to send"); output.clear(); } } else if output.contains('\n') { // 普通换行输出直接发送 sender.send(output.clone()).expect("Failed to send"); output.clear(); } } _ => break, } } });
2. 结合进程状态与输出上下文判断
如果不想用PTY,可以通过检查子进程状态+输出上下文辅助判断:
- 用
child.try_wait()检查进程是否处于运行且等待输入的状态 - 输入提示通常出现在行首,且前一段输出是以换行结尾的完整结果
修改代码逻辑示例:
use std::sync::{Arc, Mutex}; // 用Arc<Mutex>共享缓冲区状态 let shared_buf = Arc::new(Mutex::new(String::new())); let buf_clone = shared_buf.clone(); // 读取线程更新共享缓冲区 thread::spawn(move || { let mut f = BufReader::new(reader); let mut buf = String::new(); for c in f.chars().map(|x| x.unwrap()) { buf.push(c); *buf_clone.lock().unwrap() = buf.clone(); if c == '\n' { sender.send(buf.clone()).expect("Failed to send."); buf.clear(); *buf_clone.lock().unwrap() = String::new(); } } }); // 主线程定期检查进程状态与缓冲区 thread::spawn(move || { loop { match child.try_wait() { Ok(None) => { let current_buf = shared_buf.lock().unwrap().clone(); if RE_PROMPT.is_match(¤t_buf) { // 写入测试字符,判断进程是否响应 child.stdin.as_mut().unwrap().write_all(b" ").unwrap(); thread::sleep(std::time::Duration::from_millis(100)); // 根据后续输出判断是否为输入提示,再决定是否发送缓冲区内容 } } _ => break, } thread::sleep(std::time::Duration::from_millis(500)); } });
3. 优化正则匹配规则
针对REPL提示做更精准匹配:
- 匹配行首的
>(用^>正则,开启多行模式) - 覆盖REPL不同模式的提示格式,比如SBCL调试模式的
debug>
内容的提问来源于stack exchange,提问作者User9123
相关产品推荐
相关产品推荐

