简介#
REPL(Read-Eval-Print Loop)是一种交互式编程环境,它允许用户输入代码并立即查看结果。在本篇文章中,我们将介绍如何使用 Rust 编写一个简单的 REPL,它可以读取用户输入的代码并在控制台中打印出结果。
实现步骤#
这个 REPL 主要分为 3 个步骤:读取用户输入、解析用户输入、执行用户输入。
-
读取用户输入
首先要循环读取用户的输入,当用户按下回车键后,我们就可以获取到用户在控制台中输入的内容了。如果用户没有输入任何内容,我们可以继续等待用户的输入。
-
解析用户输入
当我们获取到用户的输入后,就需要将其解析成程序可以理解的格式。我们首先判断用户输入的是不是元命令(以 "." 开头),如果是就执行相应的元命令。如果不是元命令,那么我们就需要解析出用户输入的 SQL 语句。
-
执行用户输入
当我们将用户输入的 SQL 语句解析出来后,就可以执行这个语句了。我们首先需要对输入的 SQL 语句进行预处理,然后执行这个语句。如果执行成功,我们就可以在控制台中打印出相应的结果。如果执行失败,我们就需要在控制台中打印出错误信息。
以上就是这个 REPL 的基本流程,如果你想了解更多细节,请查看代码。
代码#
main.rs#
mod executor;
mod parser;
use executor::{execute_statement, PrepareError, Statement};
use parser::{do_meta_command, prepare_statement, MetaCommandResult};
use std::io::{self, Write};
fn main() -> io::Result<()> {
loop {
print_prompt();
io::stdout().flush()?;
let mut cmd = String::new();
io::stdin().read_line(&mut cmd)?;
if cmd.trim().is_empty() {
continue;
}
if cmd.starts_with('.') {
match do_meta_command(cmd.trim()) {
MetaCommandResult::Exit => {
println!("exit, bye");
break;
}
MetaCommandResult::UnrecognizedCommand => {
println!("Unrecognized command '{}'", cmd.trim());
continue;
}
};
}
let mut stmt: Statement = Statement::default();
match prepare_statement(cmd.trim(), &mut stmt) {
Ok(_) => {}
Err(PrepareError::UnrecognizedStatement) => {
println!("Unrecognized keyword at start of '{}'", cmd.trim());
continue;
}
};
match execute_statement(&stmt) {
Ok(_) => {
println!("Executed.");
}
Err(_) => {
println!("Error executing statement");
continue;
}
}
}
Ok(())
}
fn print_prompt() {
print!("db > ")
}
将 prepare_statement
封装到 parser.rs
,是因为这个函数的作用是将用户输入的 SQL 语句解析成程序可以理解的格式,属于语法分析的范畴,因此放在 parser.rs
文件中更为合适;而 execute_statement
函数则是将解析出来的语句进行执行,是需要执行器来完成的,因此将其封装到 executor.rs
文件中。
parser.rs#
use crate::executor::{PrepareError, Statement, StatementType};
pub enum MetaCommandResult {
Exit,
UnrecognizedCommand,
}
pub fn do_meta_command(cmd: &str) -> MetaCommandResult {
if cmd == ".exit" {
MetaCommandResult::Exit
} else {
MetaCommandResult::UnrecognizedCommand
}
}
pub fn prepare_statement(cmd: &str, stmt: &mut Statement) -> Result<(), PrepareError> {
if cmd.starts_with("insert") {
stmt.statement_type = StatementType::Insert;
//将cmd解析成row
let mut iter = cmd.split_whitespace();
//判断是否有三个参数
if iter.clone().count() != 4 {
return Err(PrepareError::UnrecognizedStatement);
}
iter.next();
stmt.row_to_insert.id = iter.next().unwrap().parse::<u32>().unwrap();
stmt.row_to_insert.username = iter.next().unwrap().to_string();
stmt.row_to_insert.email = iter.next().unwrap().to_string();
println!("stmt.row_to_insert: {:?}", stmt.row_to_insert);
return Ok(());
}
if cmd.starts_with("select") {
stmt.statement_type = StatementType::Select;
return Ok(());
}
Err(PrepareError::UnrecognizedStatement)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_prepare_insert() {
let mut stmt = Statement::default();
let cmd = "insert 1 user1 [email protected]";
let _result = prepare_statement(cmd, &mut stmt);
if stmt.statement_type != StatementType::Insert {
panic!("must be insert statement");
}
//测试各个字段是否正确
assert_eq!(stmt.row_to_insert.id, 1);
assert_eq!(stmt.row_to_insert.username, "user1");
assert_eq!(stmt.row_to_insert.email, "[email protected]");
}
}
这是代码中的 parse.rs
文件,它包含了解析用户输入的函数和元命令的函数。元命令是以 "." 开头的命令,用于控制 REPL 的行为,如退出 REPL。
do_meta_command
函数用于解析元命令。如果用户输入的是 ".exit",函数返回 MetaCommandResult::Exit
,表示需要退出 REPL;如果用户输入的是其他元命令,函数返回 MetaCommandResult::UnrecognizedCommand
,表示无法识别的元命令。
prepare_statement
函数用于将用户输入的 SQL 语句解析成程序可以理解的格式。如果用户输入的是 "insert",函数将 SQL 语句解析成一个 Statement
结构体,包含了要插入的行的各个字段。如果用户输入的是 "select",函数将 SQL 语句解析成一个 Statement
结构体,不包含要查询的条件。如果用户输入的是其他语句,函数返回 PrepareError::UnrecognizedStatement
,表示无法识别的语句。
prepare_statement
函数中的代码实现了解析 "insert" 语句的逻辑。首先将 Statement
结构体的 statement_type
字段设置为 StatementType::Insert
,表示要插入一行数据。然后将 SQL 语句解析成一个迭代器,使用 split_whitespace
函数将其拆分成单词。如果单词数不是 4,函数返回 PrepareError::UnrecognizedStatement
,表示无法识别的语句。否则,函数将解析出的值赋给 stmt
结构体的 row_to_insert
字段,包括 id
、username
和 email
三个字段。
在测试中,我们使用 test_prepare_insert
测试了 prepare_statement
函数是否能够正确解析 "insert" 语句,并且将解析出的值赋给 stmt
结构体的 row_to_insert
字段。
excutor.rs#
#![allow(dead_code)]
use serde::{Deserialize, Serialize};
use std::fmt::Display;
#[derive(Debug, Eq, PartialEq)]
pub enum StatementType {
Insert,
Select,
Unrecognized,
}
#[derive(Debug, Eq, PartialEq)]
pub enum PrepareError {
UnrecognizedStatement,
IncorrectParamNumber,
}
impl Display for PrepareError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PrepareError::UnrecognizedStatement => {
write!(f, "UnrecognizedStatement")
}
PrepareError::IncorrectParamNumber => {
write!(f, "IncorrectParamNumber")
}
}
}
}
#[derive(Debug, Eq, PartialEq)]
pub enum ExecuteError {
UnrecognizedStatement,
Failure(String),
}
impl Display for ExecuteError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ExecuteError::UnrecognizedStatement => {
write!(f, "UnrecognizedStatement")
}
ExecuteError::Failure(s) => {
write!(f, "Failure: {}", s)
}
}
}
}
pub enum ExecuteResult {
Record(Vec<Row>),
Affected(u32),
}
pub struct Statement {
pub row_to_insert: Row,
pub statement_type: StatementType,
}
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
pub struct Row {
pub id: u32,
pub email: String,
pub username: String,
}
impl Default for Statement {
fn default() -> Self {
Statement {
statement_type: StatementType::Unrecognized,
row_to_insert: Row::default(),
}
}
}
pub fn execute_statement(stmt: &Statement) -> Result<ExecuteResult, ExecuteError> {
match stmt.statement_type {
StatementType::Insert => execute_insert(stmt),
StatementType::Select => execute_select(stmt),
StatementType::Unrecognized => Err(ExecuteError::UnrecognizedStatement),
}
}
fn execute_insert(_stmt: &Statement) -> Result<ExecuteResult, ExecuteError> {
println!("This is where we would do an insert.");
Err(ExecuteError::Failure("unimplemented".to_owned()))
}
fn execute_select(_stmt: &Statement) -> Result<ExecuteResult, ExecuteError> {
println!("This is where we would do a select.");
Err(ExecuteError::Failure("unimplemented".to_owned()))
}
这是代码中的 excutor.rs
文件,它包含了执行用户输入的 SQL 语句的函数。execute_statement
函数根据输入的语句类型,调用相应的函数执行语句。如果语句类型是 "insert",函数调用 execute_insert
函数执行插入操作;如果语句类型是 "select",函数调用 execute_select
函数执行查询操作;如果语句类型是其他值,函数返回 ExecuteError::UnrecognizedStatement
,表示无法识别的语句类型。
execute_insert
和 execute_select
函数都是未实现的占位函数,它们只是打印一条消息并返回一个错误。
Row
结构体表示一行数据,包含了 id
、email
和 username
三个字段。Statement
结构体表示一个 SQL 语句,包含了要执行的语句类型和要插入的行数据。StatementType
枚举表示 SQL 语句的类型,包括 "insert" 和 "select" 两种类型。PrepareError
和 ExecuteError
枚举分别表示准备语句和执行语句时可能出现的错误,包括语句无法识别和参数数量不正确等。
在 Display
trait 的实现中,我们为 PrepareError
和 ExecuteError
分别实现了 fmt
方法,用于格式化错误信息。
总结#
本文主要介绍了 REPL 的基本流程和代码实现,包括解析用户输入的 SQL 语句和执行语句等。代码中的 parser.rs
文件包含了解析用户输入的函数和元命令的函数,excutor.rs
文件包含了执行用户输入的 SQL 语句的函数。