Mastering CLI Autocomplete in Rust with rustyline: A Step‑by‑Step Guide
This article explains how to implement multi‑level sub‑command autocomplete for a Rust command‑line interface using the rustyline library, detailing helper struct setup, sub‑command data structures, and the completion algorithm with concrete code examples.
Implementation Overview
The article continues the discussion on CLI domain interaction modes, focusing on how to add tab‑based autocomplete for multi‑level sub‑commands using the rustyline helper framework. It starts by defining a MyHelper struct that implements the required Completer, Hinter, Highlighter and Validator traits, and registers it with Editor::with_config.
#[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));
// ...
}The MyHelper struct holds the autocomplete logic, while rustyline 's set_helper loads it into the editor.
Sub‑command Autocompleter Details
A SubCmd struct records the command level, its name, and a list of its direct sub‑commands, enabling the completer to locate the appropriate scope.
#[derive(Debug, Clone)]
pub struct SubCmd {
pub level: usize,
pub command_name: String,
pub subcommands: Vec<String>,
}The function all_subcommand recursively walks the command tree (defined in src/cmd/rootcmd.rs) and populates a Vec<SubCmd> with every command and its children.
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);
}The core CommandCompleter struct stores the collected SubCmd list and provides several helper methods:
Retrieve all possible sub‑commands at a given level.
Retrieve sub‑commands that start with a specific prefix.
Retrieve the full list of sub‑commands for a particular command.
Retrieve prefixed sub‑commands for a particular command.
#[derive(Debug, Clone)]
pub struct CommandCompleter {
subcommands: Vec<SubCmd>,
}
impl CommandCompleter {
pub fn new(subcmds: Vec<SubCmd>) -> Self {
Self { subcommands: subcmds }
}
// Get all possible commands at a level
pub fn level_possible_cmd(&self, level: usize) -> Vec<String> {
let mut subcmds = vec![];
for iterm in self.subcommands.clone() {
if iterm.level == level {
subcmds.push(iterm.command_name.clone());
}
}
subcmds
}
// Get commands at a level that start with a prefix
pub fn level_prefix_possible_cmd(&self, level: usize, prefix: &str) -> Vec<String> {
let mut subcmds = vec![];
for iterm in self.subcommands.clone() {
if iterm.level == level && iterm.command_name.starts_with(prefix) {
subcmds.push(iterm.command_name);
}
}
subcmds
}
// Get all sub‑commands of a command at a level
pub fn level_cmd_possible_sub_cmd(&self, level: usize, cmd: String) -> Vec<String> {
let mut subcmds = vec![];
for iterm in self.subcommands.clone() {
if iterm.level == level && iterm.command_name == cmd {
subcmds = iterm.subcommands.clone();
}
}
subcmds
}
// Get prefixed sub‑commands of a command at a level
pub fn level_cmd_possible_prefix_sub_cmd(
&self,
level: usize,
cmd: String,
prefix: &str,
) -> Vec<String> {
let mut subcmds = vec![];
for iterm in self.subcommands.clone() {
if iterm.level == level && iterm.command_name == cmd {
for i in iterm.subcommands {
if i.starts_with(prefix) {
subcmds.push(i);
}
}
}
}
subcmds
}
// Main completion logic
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)
}
}The complete_cmd function analyses the current input line, determines the cursor position, and returns a list of possible completions. For example, typing root cm under a root command that has sub‑commands cmd1 and cmd2, pressing Tab will cause complete_cmd to return (7, ["cmd1", "cmd2"]).
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
JD Cloud Developers
JD Cloud Developers (Developer of JD Technology) is a JD Technology Group platform offering technical sharing and communication for AI, cloud computing, IoT and related developers. It publishes JD product technical information, industry content, and tech event news. Embrace technology and partner with developers to envision the future.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
