作者:京东科技 贾世闻
文盘 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 如何实现。