我的 Python 解释器 Memphis 有一个 REPL(读取-评估-打印循环)!
这是旧闻了。只要你在与聪明的老猫头鹰互动时零失误,就可以随心所欲地解读。假设您不想对同一个语句求值两次,或者如果您这样做,也不介意重新输入它。而且零错误。
我对这个 REPL 非常满意。甚至激动不已。我已经写信回家谈论这个 REPL。但我老板的老板的老板要求我们为了人民的底线而改进 REPL。他们把我叫进办公室,点点头让我坐在红木办公桌对面的椅子上。 “一些用户希望退格键起作用。”让用户见鬼去吧! “向上箭头应该会显示他们的最后一个命令。”箭头键支持是如此九十年代! “你能在第五季度结束前完成这项工作吗?”我放弃了!
所以我回到办公桌前改进了 REPL。
我对其进行了很大改进,所有按键都可以使用。退格键、向上箭头、向下箭头、向左箭头,以及最后但并非最不重要的退格箭头。会计师可以使用新的 REPL 进行实地考察。勾勾勾勾勾勾勾勾勾勾勾勾勾勾勾勾勾勾勾勾。那是会计师在敲数字,而不是慢慢扩散的炸弹。
我将 REPL 发送到实验室,并告诉我的主要机械师对该订单进行加急工作。这是我说的 REPL,从他们的眼神我可以看出他们明白了。 750 毫秒后,构建完成,我们获得了箭头键支持。我把产品带回给大佬们,请求让我恢复工作,并询问他们的想法。他们运行了一些命令,打印了一些打印内容,并添加了一些添加内容。他们犯了一个错误,按下了退格键。我翻了个白眼,因为说真的,谁犯了错误,但他们似乎很满意。他们意识到他们不想运行已经输入的长命令,这就是我的生活陷入困境的地方。他们。打。 Ctrl。 C. 说真的,这是谁干的?!您知道这会结束当前进程,对吗?对吗???
“到明年年底,我们需要 Ctrl-C 支持。”这些人和他们的要求。我会添加 Ctrl-C 支持。但两年之内绝对不会。
所以我回到办公桌并添加了 Ctrl-C 支持。
我把我的整个职业和财务未来都押在“从头开始”构建东西上,所以我在这个项目的第一天就面临着困境。我选择使用 crossterm 进行关键检测主要是因为跨平台支持。老实说,crossterm 非常非常好。 API 很直观,我对 KeyModifiers 特别满意(我们需要它来处理 Ctrl-C,我认为这是不必要的,见上文)。
我们需要它,这样终端就不会为我们处理特殊的密钥。但该死的,我没有意识到它会把我们的屏幕变成一台有故障的打字机。无论如何,我必须规范所有字符串以在任何换行符之前添加回车符。效果很好,我对此感到非常兴奋。
/// When the terminal is in raw mode, we must emit a carriage return in addition to a newline, /// because that does not happen automatically. fn normalize<T: Display>(err: T) -> String { let formatted = format!("{}", err); if terminal::is_raw_mode_enabled().expect("Failed to query terminal raw mode") { formatted.replace("\n", "\n\r") } else { formatted.to_string() } } /// Print command which will normalize newlines + carriage returns before printing. fn print_raw<T: Display>(val: T) { print!("{}", normalize(val)); io::stdout().flush().expect("Failed to flush stdout"); }
还有!如果您没有在意外恐慌时禁用原始模式,您的终端会话将变得不可用。我安装了自定义恐慌处理程序来捕捉这个并玩得很好。
panic::set_hook(Box::new(|info| { // This line is critical!! The rest of this function is just debug info, but without this // line, your shell will become unusable on an unexpected panic. let _ = terminal::disable_raw_mode(); if let Some(s) = info.payload().downcast_ref::<&str>() { eprintln!("\nPanic: {s:?}"); } else if let Some(s) = info.payload().downcast_ref::<String>() { eprintln!("\nPanic: {s:?}"); } else { eprintln!("\nPanic occurred!"); } if let Some(location) = info.location() { eprintln!( " in file '{}' at line {}", location.file(), location.line() ); } else { eprintln!(" in an unknown location."); } process::exit(1); }));
在我的旧 REPL(我更喜欢它,见上文)下,我可以通过运行二进制文件并将一些 Python 代码传递到 stdin 来测试它的集成。当使用 crossterm 时,它停止工作,我认为是由于合同纠纷。老实说,我无法完全解释它,但是 event::read() 在标准输入输入提供的集成测试中会超时并失败。所以我嘲笑了它。
pub trait TerminalIO { fn read_event(&mut self) -> Result<Event, io::Error>; fn write<T: Display>(&mut self, output: T) -> io::Result<()>; fn writeln<T: Display>(&mut self, output: T) -> io::Result<()>; } /// A mock for testing that doesn't use `crossterm`. struct MockTerminalIO { /// Predefined events for testing events: Vec<Event>, /// Captured output for assertions output: Vec<String>, } impl TerminalIO for MockTerminalIO { fn read_event(&mut self) -> Result<Event, io::Error> { if self.events.is_empty() { Err(io::Error::new(io::ErrorKind::Other, "No more events")) } else { // remove from the front (semantically similar to VecDequeue::pop_front). Ok(self.events.remove(0)) } } fn write<T: Display>(&mut self, output: T) -> io::Result<()> { self.output.push(format!("{}", output)); Ok(()) } fn writeln<T: Display>(&mut self, output: T) -> io::Result<()> { self.write(output)?; self.write("\n")?; Ok(()) } }
这导致整个事情变成了单元测试?老实说我不知道。此时,如果我 a) 在另一个二进制文件中调用一个二进制文件,或者 2) 在测试中启动服务器/打开端口/侦听套接字,我将其称为集成测试。如果您想在评论中留下其他定义,请不要留下,因为这听起来很烦人。
我创建了两个实用函数来开始。
/// Run the complete flow, from input code string to return value string. If you need any Ctrl /// modifiers, do not use this! fn run_and_return(input: &str) -> String { let mut terminal = MockTerminalIO::from_str(input); Repl::new().run(&mut terminal); terminal.return_val() } /// Turn an input string into a list of crossterm events so we don't have to /// hand-compile our test. fn string_to_events(input: &str) -> Vec<Event> { input .chars() .map(|c| { let key_code = match c { '\n' => KeyCode::Enter, _ => KeyCode::Char(c), }; Event::Key(KeyEvent::new(key_code, KeyModifiers::NONE)) }) .collect() }
使用这些,我们现在可以用相当少的样板来测试这些常见场景。
#[test] fn test_repl_name_error() { let return_val = run_and_return("e\n"); assert!(return_val.contains("NameError: name 'e' is not defined")); } #[test] fn test_repl_expr() { let third_from_last = run_and_return("12345\n"); assert_eq!(third_from_last, "12345"); } #[test] fn test_repl_statement() { let return_val = run_and_return("a = 5.5\n"); // empty string because a statement does not have a return value assert_eq!(return_val, ""); } #[test] fn test_repl_function() { let code = r#" def foo(): a = 10 return 2 * a foo() "#; let return_val = run_and_return(code); assert_eq!(return_val, "20"); } #[test] fn test_repl_ctrl_c() { let mut events = string_to_events("123456789\n"); let ctrl_c = Event::Key(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)); events.insert(4, ctrl_c); let mut terminal = MockTerminalIO::new(events); Repl::new().run(&mut terminal); assert_eq!(terminal.return_val(), "56789"); }
我添加 REPL 的动机之一是因为我相信当您添加第二个入口点时,您的代码会变得更好。您实际上正在成为您的图书馆的第二个用户,这可以帮助您更接近地理解我们都在键盘上寻找的完美抽象。我是认真的说这一点。
REPL 现在位于功能标志后面,作为重新获得管理的一种方式。我在零第三方 crate 的帮助下保持解释 Python 代码的能力,这意味着 crossterm 要么需要成为例外,要么我会引入一个功能标志。现在,如果你在没有启用 REPL 的情况下编译并运行“memphis”,它会礼貌地告诉你“错误的构建,笨蛋。”
REPL 就在这里。你可以像这样运行它。如果你想买的话,那听起来就像是一个骗局。祝你好起来,尽快说话。
如果您想将更多类似的帖子直接发送到您的收件箱,您可以在这里订阅!
除了指导软件工程师之外,我还写了我作为一名成人诊断自闭症患者的经历。更少的代码和相同数量的笑话。
以上是适合胖手指打字的 REPL的详细内容。更多信息请关注PHP中文网其他相关文章!