上次我們聊到 CLI 的領域互動模式。在領域互動模式中,可能存在多層次的子命令。在使用過程中如果全評記憶的話,命令少還好,多了真心記不住。頻繁 --help 也是個很麻煩的事情。如果每次按 'tab' 鍵就可以提示或補齊命令是不是很方便呢。這一節我們就來說說 'autocommplete' 如何實現。我們還是以interactcli-rs中的實現來解說實現過程
實現過程
其實,rustyline 已經為我們提供了基本的helper功能框架,其中包括了completer。我們來看程式碼,檔案位置src/interact/cli.rs
#[derive(Helper)]
struct MyHelper {
completer: CommandCompleter,
highlighter: MatchingBracketHighlighter,
validator: MatchingBracketValidator,
hinter: HistoryHinter,
colored_prompt: String,
}
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);
// let mut rl = Editor::<()>::new();
rl.set_helper(Some(h));
......
}
首先定義 MyHelper 結構體, 需要實現 Completer + Hinter + Highlighter + Validator trait。然後通過rustyline的set_helper函數載入我們定義好的helper。在MyHelper 結構體中,需要我們自己來實現completer的邏輯。
Sub command autocompleter實現詳解
#[derive(Debug, Clone)]
pub struct SubCmd {
pub level: usize,
pub command_name: String,
pub subcommands: Vec<String>,
}
SubCmd 結構體包含:命令級別,命令名稱,以及該命令包含的子命令資訊,以便在實現實現 autocomplete 時定位命令和子命令的範圍
pub fn all_subcommand(app: &clap_Command, beginlevel: usize, input: &mut Vec<SubCmd>) {
let nextlevel = beginlevel + 1;
let mut subcmds = vec![];
for iterm in app.get_subcommands() {
subcmds.push(iterm.get_name().to_string());
if iterm.has_subcommands() {
all_subcommand(iterm, nextlevel, input);
} else {
if beginlevel == 0 {
all_subcommand(iterm, nextlevel, input);
}
}
}
let subcommand = SubCmd {
level: beginlevel,
command_name: app.get_name().to_string(),
subcommands: subcmds,
};
input.push(subcommand);
}
#[derive(Debug, Clone)]
pub struct CommandCompleter {
subcommands: Vec<SubCmd>,
}
impl CommandCompleter {
pub fn new(subcmds: Vec<SubCmd>) -> Self {
Self {
subcommands: subcmds,
}
}
//獲取level下所有可能的子命令
pub fn level_possible_cmd(&self, level: usize) -> Vec<String> {
let mut subcmds = vec![];
let cmds = self.subcommands.clone();
for iterm in cmds {
if iterm.level == level {
subcmds.push(iterm.command_name.clone());
}
}
return subcmds;
}
//獲取level下某字串開頭的子命令
pub fn level_prefix_possible_cmd(&self, level: usize, prefix: &str) -> Vec<String> {
let mut subcmds = vec![];
let cmds = self.subcommands.clone();
for iterm in cmds {
if iterm.level == level && iterm.command_name. starts_with(prefix) {
subcmds.push(iterm.command_name);
}
}
return subcmds;
}
//獲取某level 下某subcommand的所有子命令
pub fn level_cmd_possible_sub_cmd(&self, level: usize, cmd: String) -> Vec<String> {
let mut subcmds = vec![];
let cmds = self.subcommands.clone();
for iterm in cmds {
if iterm.level == level && iterm.command_name == cmd {
subcmds = iterm.subcommands.clone();
}
}
return subcmds;
}
//獲取某level 下某subcommand的所有prefix子命令
pub fn level_cmd_possible_prefix_sub_cmd(
&self,
level: usize,
cmd: String,
prefix: &str,
) -> Vec<String> {
let mut subcmds = vec![];
let cmds = self.subcommands.clone();
for iterm in cmds {
if iterm.level == level && iterm.command_name == cmd {
for i in iterm.subcommands {
if i.starts_with(prefix) {
subcmds.push(i);
}
}
}
}
return subcmds;
}
pub fn complete_cmd(&self, line: &str, pos: usize) -> Result<(usize, Vec<Pair>)> {
let mut entries: Vec<Pair> = Vec::new();
let d: Vec<_> = line.split(' ').collect();
if d.len() == 1 {
if d.last() == Some(&"") {
for str in self.level_possible_cmd(1) {
let mut replace = str.clone();
replace.push_str(" ");
entries.push(Pair {
display: str.clone(),
replacement: replace,
});
}
return Ok((pos, entries));
}
if let Some(last) = d.last() {
for str in self.level_prefix_possible_cmd (1, *last) {
let mut replace = str.clone();
replace.push_str(" ");
entries.push(Pair {
display: str.clone(),
replacement: replace,
});
}
return Ok((pos - last.len(), entries));
}
}
if d.last() == Some(&"") {
for str in self
.level_cmd_possible_sub_cmd(d.len() - 1, d.get(d.len() - 2).unwrap().to_string())
{
let mut replace = str.clone();
replace.push_str(" ");
entries.push(Pair {
display: str.clone(),
replacement: replace,
});
}
return Ok((pos, entries));
}
if let Some(last) = d.last() {
for str in self. level_cmd_possible_prefix_sub_cmd(
d.len() - 1,
d.get(d.len() - 2).unwrap().to_string(),
*last,
) {
let mut replace = str.clone();
replace.push_str(" ");
entries.push(Pair {
display: str.clone(),
replacement: replace,
});
}
return Ok((pos - last.len(), entries));
}
Ok((pos, entries))
}
}
impl Completer for CommandCompleter {
type Candidate = Pair;
fn complete(&self, line: &str, pos: usize, _ctx: & Context<'_>) -> Result<(usize, Vec<Pair>)> {
self.complete_cmd(line, pos)
}
}
CommandCompleter 的實現部分比較多,大致包括兩個部分,前一部分包括:獲取某一級別下所有可能的子命令、獲取某級別下某字串開頭的子命令、獲取某級別下某個命令的所有子命令,等基本功能。這部分程式碼中有註釋就不一一累述。
函數complete_cmd用來計算行中的位置以及在該位置的替換內容。
輸入項是命令列的內容以及遊標所在位置,輸出項為在該位置需要替換的內容。比如,我們在提示符下輸入 "root cm" root 下包含 cmd1、cmd2 兩個子命令,此時如果按 'tab'鍵,complete_cmd 函數就會返回 (7,[cmd1,cmd2])。
作者:京東科技 賈世聞
來源:京東雲開發者社群 轉載請註明來源