上次我们聊到 CLI 的领域交互模式。在领域交互模式中,可能存在多层次的子命令。在使用过程中如果全评记忆的话,命令少还好,多了真心记不住。频繁 --help 也是个很麻烦的事情。如果每次按 'tab' 键就可以提示或补齐命令是不是很方便呢。这一节我们就来说说 'autocommplete' 如何实现。我们还是以interactcli-rs中的实现来解说实现过程
实现过程
其实,rustyline 已经为我们提供了基本的helper功能框架,其中包括了completer。我们来看代码,文件位置src/interact/cli.rs
1#[derive(Helper)] 2struct MyHelper { 3 completer: CommandCompleter, 4 highlighter: MatchingBracketHighlighter, 5 validator: MatchingBracketValidator, 6 hinter: HistoryHinter, 7 colored_prompt: String, 8} 9 10pub fn run() { 11 let config = Config::builder() 12 .history_ignore_space(true) 13 .completion_type(CompletionType::List) 14 .output_stream(OutputStreamType::Stdout) 15 .build(); 16 17 let h = MyHelper { 18 completer: get_command_completer(), 19 highlighter: MatchingBracketHighlighter::new(), 20 hinter: HistoryHinter {}, 21 colored_prompt: "".to_owned(), 22 validator: MatchingBracketValidator::new(), 23 }; 24 25 let mut rl = Editor::with_config(config); 26 // let mut rl = Editor::<()>::new(); 27 rl.set_helper(Some(h)); 28 29 ...... 30 31} 32 33 34
首先定义 MyHelper 结构体, 需要实现 Completer + Hinter + Highlighter + Validator trait。然后通过rustyline的set_helper函数加载我们定义好的helper。在MyHelper 结构体中,需要我们自己来实现completer的逻辑。
Sub command autocompleter实现详解
- SubCmd 结构体
1#[derive(Debug, Clone)] 2pub struct SubCmd { 3 pub level: usize, 4 pub command_name: String, 5 pub subcommands: Vec<String>, 6} 7 8 9
SubCmd 结构体包含:命令级别,命令名称,以及该命令包含的子命令信息,以便在实现实现 autocomplete 时定位命令和子命令的范围
- 在程序启动时遍历所有的command,src/cmd/rootcmd.rs 中的all_subcommand函数负责收集所有命令并转换为Vec<SubCmd>
1pub fn all_subcommand(app: &clap_Command, beginlevel: usize, input: &mut Vec<SubCmd>) { 2 let nextlevel = beginlevel + 1; 3 let mut subcmds = vec![]; 4 for iterm in app.get_subcommands() { 5 subcmds.push(iterm.get_name().to_string()); 6 if iterm.has_subcommands() { 7 all_subcommand(iterm, nextlevel, input); 8 } else { 9 if beginlevel == 0 { 10 all_subcommand(iterm, nextlevel, input); 11 } 12 } 13 } 14 let subcommand = SubCmd { 15 level: beginlevel, 16 command_name: app.get_name().to_string(), 17 subcommands: subcmds, 18 }; 19 input.push(subcommand); 20} 21 22 23
- CommandCompleter 子命令自动补充功能的核心部分
1#[derive(Debug, Clone)] 2pub struct CommandCompleter { 3 subcommands: Vec<SubCmd>, 4} 5 6impl CommandCompleter { 7 pub fn new(subcmds: Vec<SubCmd>) -> Self { 8 Self { 9 subcommands: subcmds, 10 } 11 } 12 13 //获取level下所有可能的子命令 14 pub fn level_possible_cmd(&self, level: usize) -> Vec<String> { 15 let mut subcmds = vec![]; 16 let cmds = self.subcommands.clone(); 17 for iterm in cmds { 18 if iterm.level == level { 19 subcmds.push(iterm.command_name.clone()); 20 } 21 } 22 return subcmds; 23 } 24 //获取level下某字符串开头的子命令 25 pub fn level_prefix_possible_cmd(&self, level: usize, prefix: &str) -> Vec<String> { 26 let mut subcmds = vec![]; 27 let cmds = self.subcommands.clone(); 28 for iterm in cmds { 29 if iterm.level == level && iterm.command_name. starts_with(prefix) { 30 subcmds.push(iterm.command_name); 31 } 32 } 33 return subcmds; 34 } 35 36 //获取某level 下某subcommand的所有子命令 37 pub fn level_cmd_possible_sub_cmd(&self, level: usize, cmd: String) -> Vec<String> { 38 let mut subcmds = vec![]; 39 let cmds = self.subcommands.clone(); 40 for iterm in cmds { 41 if iterm.level == level && iterm.command_name == cmd { 42 subcmds = iterm.subcommands.clone(); 43 } 44 } 45 return subcmds; 46 } 47 48 //获取某level 下某subcommand的所有prefix子命令 49 pub fn level_cmd_possible_prefix_sub_cmd( 50 &self, 51 level: usize, 52 cmd: String, 53 prefix: &str, 54 ) -> Vec<String> { 55 let mut subcmds = vec![]; 56 let cmds = self.subcommands.clone(); 57 for iterm in cmds { 58 if iterm.level == level && iterm.command_name == cmd { 59 for i in iterm.subcommands { 60 if i.starts_with(prefix) { 61 subcmds.push(i); 62 } 63 } 64 } 65 } 66 return subcmds; 67 } 68 69 pub fn complete_cmd(&self, line: &str, pos: usize) -> Result<(usize, Vec<Pair>)> { 70 let mut entries: Vec<Pair> = Vec::new(); 71 let d: Vec<_> = line.split(' ').collect(); 72 73 if d.len() == 1 { 74 if d.last() == Some(&"") { 75 for str in self.level_possible_cmd(1) { 76 let mut replace = str.clone(); 77 replace.push_str(" "); 78 entries.push(Pair { 79 display: str.clone(), 80 replacement: replace, 81 }); 82 } 83 return Ok((pos, entries)); 84 } 85 86 if let Some(last) = d.last() { 87 for str in self.level_prefix_possible_cmd (1, *last) { 88 let mut replace = str.clone(); 89 replace.push_str(" "); 90 entries.push(Pair { 91 display: str.clone(), 92 replacement: replace, 93 }); 94 } 95 return Ok((pos - last.len(), entries)); 96 } 97 } 98 99 if d.last() == Some(&"") { 100 for str in self 101 .level_cmd_possible_sub_cmd(d.len() - 1, d.get(d.len() - 2).unwrap().to_string()) 102 { 103 let mut replace = str.clone(); 104 replace.push_str(" "); 105 entries.push(Pair { 106 display: str.clone(), 107 replacement: replace, 108 }); 109 } 110 return Ok((pos, entries)); 111 } 112 113 if let Some(last) = d.last() { 114 for str in self. level_cmd_possible_prefix_sub_cmd( 115 d.len() - 1, 116 d.get(d.len() - 2).unwrap().to_string(), 117 *last, 118 ) { 119 let mut replace = str.clone(); 120 replace.push_str(" "); 121 entries.push(Pair { 122 display: str.clone(), 123 replacement: replace, 124 }); 125 } 126 return Ok((pos - last.len(), entries)); 127 } 128 129 Ok((pos, entries)) 130 } 131} 132 133impl Completer for CommandCompleter { 134 type Candidate = Pair; 135 136 fn complete(&self, line: &str, pos: usize, _ctx: & Context<'_>) -> Result<(usize, Vec<Pair>)> { 137 self.complete_cmd(line, pos) 138 } 139} 140 141 142
CommandCompleter 的实现部分比较多,大致包括两个部分,前一部分包括:获取某一级别下所有可能的子命令、获取某级别下某字符串开头的子命令、获取某级别下某个命令的所有子命令,等基本功能。这部分代码中有注释就不一一累述。
函数complete_cmd用来计算行中的位置以及在该位置的替换内容。
输入项是命令行的内容以及光标所在位置,输出项为在该位置需要替换的内容。比如,我们在提示符下输入 "root cm" root 下包含 cmd1、cmd2 两个子命令,此时如果按 'tab'键,complete_cmd 函数就会返回 (7,[cmd1,cmd2])。
作者:京东科技 贾世闻
来源:京东云开发者社区 转载请注明来源
