簡介#
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!("無法識別的命令 '{}'", cmd.trim());
continue;
}
};
}
let mut stmt: Statement = Statement::default();
match prepare_statement(cmd.trim(), &mut stmt) {
Ok(_) => {}
Err(PrepareError::UnrecognizedStatement) => {
println!("在'{}'的開頭無法識別的關鍵字", cmd.trim());
continue;
}
};
match execute_statement(&stmt) {
Ok(_) => {
println!("執行成功。");
}
Err(_) => {
println!("執行語句時出錯");
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!("必須是插入語句");
}
//測試各個字段是否正確
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, "無法識別的語句")
}
PrepareError::IncorrectParamNumber => {
write!(f, "參數數量不正確")
}
}
}
}
#[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, "無法識別的語句")
}
ExecuteError::Failure(s) => {
write!(f, "失敗: {}", 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!("這是我們將執行插入的地方。");
Err(ExecuteError::Failure("未實現".to_owned()))
}
fn execute_select(_stmt: &Statement) -> Result<ExecuteResult, ExecuteError> {
println!("這是我們將執行查詢的地方。");
Err(ExecuteError::Failure("未實現".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 語句的函數。