149 lines
6.7 KiB
Rust
149 lines
6.7 KiB
Rust
//! This module contains functions for looking up a matching command signature from tokenized and
|
|
//! untokenized input.
|
|
use itertools::Itertools;
|
|
|
|
use super::{registry::CommandRegistry, Command};
|
|
|
|
/// Returns the highest-precedence matching `Command` signature object for the given `input`, if
|
|
/// any, along with the index of the token in `input` matched to the returned `Command`.
|
|
///
|
|
/// Subcommands take precedence over parent commands.
|
|
///
|
|
/// Note that a token in the input must have trailing whitespace (e.g. marking it as "completed")
|
|
/// to be eligible to be matched to a command signature. So, for example, if the input does not
|
|
/// contain trailing whitespace, the last token is not considered in the matching algorithm.
|
|
/// Otherwise, if one subcommand is a prefix of another subcommand, we could mistakenly eagerly
|
|
/// return the signature for the shorter subcommand even if the intent was to continue typing to
|
|
/// enter the longer subcommand.
|
|
///
|
|
/// Practically, this means that for input "test_command test_subcommand", even if there is a
|
|
/// subcommand signature for "test_subcommand", this returns the signature for "test_command",
|
|
/// because it's assumed "test_subcommand" may still be edited.
|
|
pub fn get_matching_signature_for_input<'a>(
|
|
input: &str,
|
|
registry: &'a CommandRegistry,
|
|
) -> Option<(&'a Command, usize)> {
|
|
let input_tokens = input.split_whitespace().collect_vec();
|
|
get_matching_signature_for_tokenized_input(
|
|
&input_tokens,
|
|
input.ends_with(char::is_whitespace),
|
|
registry,
|
|
)
|
|
}
|
|
|
|
/// Returns the highest-precedence matching `Command` signature object for the given tokenized
|
|
/// `input`, if any. This is equivalent to `get_matching_signature_input` above, except input is
|
|
/// tokenized (e.g. given as an array of string tokens, which were assumed to be space-delimited in
|
|
/// the original input). Because input is tokenized, the caller needs to explicitly specify whether
|
|
/// the original input had trailing whitespace to determine if the last token is eligible for use
|
|
/// in the matching algorithm.
|
|
///
|
|
/// See comments on `get_matching_signature_input` for more details.
|
|
pub fn get_matching_signature_for_tokenized_input<'a>(
|
|
input_tokens: &[&str],
|
|
has_trailing_whitespace: bool,
|
|
registry: &'a CommandRegistry,
|
|
) -> Option<(&'a Command, usize)> {
|
|
let (first_token, remaining_tokens) = input_tokens.split_first()?;
|
|
|
|
// Find the top level signature.
|
|
registry.get_signature(first_token).map(|signature| {
|
|
deepest_matching_subcommand_signature(
|
|
remaining_tokens,
|
|
&signature.command,
|
|
0,
|
|
has_trailing_whitespace,
|
|
)
|
|
})
|
|
}
|
|
|
|
/// Given a parent `command_signature`, resolves the most specific (deepest) subcommand that
|
|
/// the user has entered in `input_tokens`, skipping over any flags that appear
|
|
/// before the subcommand name (e.g. `kubectl -n kube-system get` resolves to `get`).
|
|
///
|
|
/// Returns the matched `Command` along with the index of its token in `input_tokens`.
|
|
/// If no subcommand is found, `command_signature` itself is returned at `current_token_index`.
|
|
///
|
|
/// The last token is only eligible for a subcommand match when `has_trailing_whitespace` is
|
|
/// true, i.e. the user has finished typing it.
|
|
fn deepest_matching_subcommand_signature<'a>(
|
|
input_tokens: &[&str],
|
|
command_signature: &'a Command,
|
|
mut current_token_index: usize,
|
|
has_trailing_whitespace: bool,
|
|
) -> (&'a Command, usize) {
|
|
if input_tokens.is_empty() {
|
|
return (command_signature, current_token_index);
|
|
}
|
|
|
|
// Save the starting index before we begin scanning for subcommands.
|
|
// If we skip past flags but never find a subcommand beyond them, we
|
|
// return this index so that `parse_internal_command` treats the flags
|
|
// as arguments to be parsed rather than swallowing them into the
|
|
// command name.
|
|
let subcommand_search_start_index = current_token_index;
|
|
|
|
while current_token_index < input_tokens.len() {
|
|
let is_last_token = current_token_index == input_tokens.len() - 1;
|
|
let token = input_tokens[current_token_index];
|
|
|
|
// Try to match the token against a subcommand.
|
|
let subcommand_match = command_signature.subcommands.iter().find(|subcommand| {
|
|
let token_matches_subcommand = token == subcommand.name.as_str();
|
|
if is_last_token {
|
|
// If this is the last token, treat the subcommand signature as a match
|
|
// if there is trailing whitespace, which affirms the user's intent to use
|
|
// that subcommand. If there is no trailing whitespace, the user may still
|
|
// be in the process of editing that subcommand (or specifying a different
|
|
// subcommand of which the current token is a prefix).
|
|
token_matches_subcommand && has_trailing_whitespace
|
|
} else {
|
|
token_matches_subcommand
|
|
}
|
|
});
|
|
|
|
if let Some(subcommand) = subcommand_match {
|
|
return deepest_matching_subcommand_signature(
|
|
input_tokens,
|
|
subcommand,
|
|
current_token_index + 1,
|
|
has_trailing_whitespace,
|
|
);
|
|
}
|
|
|
|
// If the token is a flag (starts with '-'), try to skip past it and its arguments
|
|
// to continue looking for subcommands. This handles cases like
|
|
// `kubectl -n kube-system get pods` where flags appear before subcommands.
|
|
if token.starts_with('-') {
|
|
if let Some(option) = command_signature
|
|
.options
|
|
.iter()
|
|
.find(|opt| opt.name.iter().any(|name| name == token))
|
|
{
|
|
// Skip the flag's arguments (non-switch options consume the next token(s)).
|
|
// Clamp to the number of argument tokens actually present to avoid
|
|
// advancing past the end of input_tokens (e.g. `kubectl -n ` with no
|
|
// namespace value).
|
|
let num_args = option.arguments.iter().filter(|arg| !arg.optional).count();
|
|
let available = input_tokens.len().saturating_sub(current_token_index + 1);
|
|
current_token_index += num_args.min(available);
|
|
}
|
|
// Advance past the flag token itself.
|
|
current_token_index += 1;
|
|
continue;
|
|
}
|
|
|
|
// Token is not a subcommand or a recognized flag; stop searching.
|
|
break;
|
|
}
|
|
|
|
// No subcommand was found beyond any skipped flags, so return the
|
|
// start index. This ensures the caller's parser still sees those
|
|
// flag tokens and can process them as flag arguments.
|
|
(command_signature, subcommand_search_start_index)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "lookup_test.rs"]
|
|
mod test;
|