書接上文,上回說到如何通過interactcli-rs四步實現一個命令行程式。但是 shell 交互模式在有些場景下用戶體驗並不是很好。比如我們要連接某個服務,比如 mysql 或者 redis 這樣的服務。如果每次交互都需要輸入地址、埠、用戶名等信息,交互起來太麻煩。通常的做法是一次性輸入和連接... ...
作者:京東科技 賈世聞
文盤Rust -- 領域交互模式如何實現
書接上文,上回說到如何通過interactcli-rs四步實現一個命令行程式。但是shell交互模式在有些場景下用戶體驗並不是很好。比如我們要連接某個服務,比如mysql或者redis這樣的服務。如果每次交互都需要輸入地址、埠、用戶名等信息,交互起來太麻煩。通常的做法是一次性輸入和連接相關的信息或者由統一配置文件進行管理,然後進入領域交互模式,所有的命令和反饋都和該領域相關。interactcli-rs 通過 -i 參數實現領域交互模式。這回我們探索一下這一模式是如何實現的。
基本原理
interactcli-rs 實現領域交互模式主要是迴圈解析輸入的每一行,通過rustyline 解析輸入的每一行命令,並交由命令解析函數處理響應邏輯
當我們調用 ‘-i’ 參數的時候 實際上是執行了 interact::run() 函數(interact -> cli -> run())。
pub fn run() {
let config = Config::builder()
.history_ignore_space(true)
.completion_type(CompletionType::List)
.output_stream(OutputStreamType::Stdout)
.build();
let h = MyHelper {
completer: get_command_completer(),
highlighter: MatchingBracketHighlighter::new(),
hinter: HistoryHinter {},
colored_prompt: "".to_owned(),
validator: MatchingBracketValidator::new(),
};
let mut rl = Editor::with_config(config);
rl.set_helper(Some(h));
if rl.load_history("/tmp/history").is_err() {
println!("No previous history.");
}
loop {
let p = format!("{}> ", "interact-rs");
rl.helper_mut().expect("No helper").colored_prompt = format!("\x1b[1;32m{}\x1b[0m", p);
let readline = rl.readline(&p);
match readline {
Ok(line) => {
if line.trim_start().is_empty() {
continue;
}
rl.add_history_entry(line.as_str());
match split(line.as_str()).as_mut() {
Ok(arg) => {
if arg[0] == "exit" {
println!("bye!");
break;
}
arg.insert(0, "clisample".to_string());
run_from(arg.to_vec())
}
Err(err) => {
println!("{}", err)
}
}
}
Err(ReadlineError::Interrupted) => {
println!("CTRL-C");
break;
}
Err(ReadlineError::Eof) => {
println!("CTRL-D");
break;
}
Err(err) => {
println!("Error: {:?}", err);
break;
}
}
}
rl.append_history("/tmp/history")
.map_err(|err| error!("{}", err))
.ok();
}
解析主邏輯
交互邏輯主要集中在 ‘loop’ 迴圈中,每次迴圈處理一次輸入請求。
處理的邏輯如下
- 定義提示符,類似 'mysql> ',提示用戶正在使用的程式
let p = format!("{}> ", "interact-rs");
rl.helper_mut().expect("No helper").colored_prompt = format!("\x1b[1;32m{}\x1b[0m", p);
-
讀取輸入行進行解析
- 將輸入的命令行加入到歷史文件,執行過的命令可以通過上下鍵回放來增強用戶體驗。
rl.add_history_entry(line.as_str());
- 將輸入的行解析為 arg 字元串,交由 cmd::run_from 函數進行命令解析和執行
match split(line.as_str()).as_mut() { Ok(arg) => { if arg[0] == "exit" { println!("bye!"); break; } arg.insert(0, "clisample".to_string()); run_from(arg.to_vec()) } Err(err) => { println!("{}", err) } }
- 解析中斷,當用戶執行 ctrl-c 或 ctrl-d 時,退出程式。
Err(ReadlineError::Interrupted) => { println!("CTRL-C"); break; } Err(ReadlineError::Eof) => { println!("CTRL-D"); break; } Err(err) => { println!("Error: {:?}", err); break; }
run 函數中其他代碼的作用
-
配置rustyline
在 run 函數最開頭 定義了一個configlet config = Config::builder() .history_ignore_space(true) .completion_type(CompletionType::List) .output_stream(OutputStreamType::Stdout) .build();
這個config 其實是rustyline的配置項,包括輸出方式歷史記錄約束,輸出方式等等。
MyHelper 用於配置命令的 autocomplete
let h = MyHelper { completer: get_command_completer(), highlighter: MatchingBracketHighlighter::new(), hinter: HistoryHinter {}, colored_prompt: "".to_owned(), validator: MatchingBracketValidator::new(), };
這裡賣個關子,下期詳細講講 autocomplete 的實現。
-
配置歷史文件
run 函數最後,我們為程式配置了歷史文件,應用於存放執行過的歷史命令。這樣即便程式退出,在此打開程式的時候還是可以利用以前的執行歷史。rl.append_history("/tmp/history") .map_err(|err| error!("{}", err)) .ok();
關於如何構建命令行的領域交互模式就說到這兒,下期詳細介紹一下 autocomplete 如何實現。