use std::borrow::Cow;
use std::collections::{BTreeSet, HashMap};
use std::path::Path;
use itertools::Itertools;
use shrub_parser::unquote_str;
use strum::IntoEnumIterator;
use crate::sys::{self, users};
use crate::variables::{self, ShellValue, ShellVariable};
use crate::{
Shell, commands, env, error, escape, expansion, extensions, interfaces, jobs, namedoptions,
patterns, trace_categories, traps,
};
mod quoting;
mod words;
use words::{LineWords, find_completion_word, find_line_words};
fn split_completion_word_list(
word_list: &str,
ifs: &str,
parser_options: &shrub_parser::ParserOptions,
) -> Result<Vec<String>, error::Error> {
let mut word_list = word_list.to_owned();
quoting::close(&mut word_list);
let word_list = word_list.as_str();
let pieces = shrub_parser::word::parse(word_list, parser_options)?;
let mut words = vec![];
let mut current_word = String::new();
for piece in pieces {
let source = word_list
.get(piece.start_index..piece.end_index)
.ok_or_else(|| {
error::ErrorKind::InternalError(String::from(
"word parser returned an invalid source span",
))
})?;
if matches!(piece.piece, shrub_parser::word::WordPiece::Text(_)) {
for c in source.chars() {
if ifs.contains(c) {
if !current_word.is_empty() {
words.push(std::mem::take(&mut current_word));
}
} else {
current_word.push(c);
}
}
} else {
current_word.push_str(source);
}
}
if !current_word.is_empty() {
words.push(current_word);
}
Ok(words)
}
#[derive(
Clone,
Copy,
Debug,
Eq,
PartialEq,
strum_macros::EnumIter,
strum_macros::EnumMessage,
strum_macros::EnumString,
strum_macros::IntoStaticStr,
)]
#[strum(serialize_all = "lowercase")]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum CompleteAction {
Alias,
ArrayVar,
Binding,
Builtin,
Command,
Directory,
Disabled,
Enabled,
Export,
File,
Function,
Group,
HelpTopic,
HostName,
Job,
Keyword,
Running,
Service,
SetOpt,
ShOpt,
Signal,
Stopped,
User,
Variable,
}
#[derive(
Clone,
Copy,
Debug,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
strum_macros::EnumIter,
strum_macros::EnumMessage,
strum_macros::EnumString,
strum_macros::IntoStaticStr,
)]
#[strum(serialize_all = "lowercase")]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum CompleteOption {
BashDefault,
Default,
DirNames,
FileNames,
NoQuote,
NoSort,
NoSpace,
PlusDirs,
}
#[derive(Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Config {
commands: HashMap<String, Spec>,
specials: HashMap<SpecialSpec, Spec>,
pub fallback_options: FallbackOptions,
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FallbackOptions {
pub mark_directories: bool,
pub mark_symlinked_directories: bool,
}
impl Default for FallbackOptions {
fn default() -> Self {
Self {
mark_directories: true,
mark_symlinked_directories: false,
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GenerationOptions {
enabled: BTreeSet<CompleteOption>,
}
impl FromIterator<CompleteOption> for GenerationOptions {
fn from_iter<I: IntoIterator<Item = CompleteOption>>(options: I) -> Self {
Self {
enabled: options.into_iter().collect(),
}
}
}
impl GenerationOptions {
pub fn get(&self, option: CompleteOption) -> bool {
self.enabled.contains(&option)
}
pub fn set(&mut self, option: CompleteOption, enabled: bool) {
if enabled {
self.enabled.insert(option);
} else {
self.enabled.remove(&option);
}
}
}
#[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Spec {
pub options: GenerationOptions,
pub actions: Vec<CompleteAction>,
pub glob_pattern: Option<String>,
pub word_list: Option<String>,
pub function_name: Option<String>,
pub command: Option<String>,
pub filter_pattern: Option<String>,
pub prefix: Option<String>,
pub suffix: Option<String>,
}
#[derive(Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub(crate) struct State {
pub(crate) config: Config,
pub(crate) in_progress: Option<InProgressCompletion>,
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub(crate) struct InProgressCompletion {
pub(crate) options: GenerationOptions,
}
struct InProgressScope<'a, SE: extensions::ShellExtensions> {
shell: &'a mut Shell<SE>,
prev: Option<InProgressCompletion>,
}
impl<'a, SE: extensions::ShellExtensions> InProgressScope<'a, SE> {
const fn start(shell: &'a mut Shell<SE>, in_progress: InProgressCompletion) -> Self {
let prev = shell.in_progress_completion_mut().replace(in_progress);
Self { shell, prev }
}
fn take_options(&mut self) -> Option<GenerationOptions> {
self.shell
.in_progress_completion_mut()
.take()
.map(|in_progress| in_progress.options)
}
}
impl<SE: extensions::ShellExtensions> Drop for InProgressScope<'_, SE> {
fn drop(&mut self) {
*self.shell.in_progress_completion_mut() = self.prev.take();
}
}
fn enter_comp_vars_scope<SE: extensions::ShellExtensions>(
shell: &mut Shell<SE>,
vars: impl IntoIterator<Item = (&'static str, ShellValue)>,
export: bool,
) -> Result<impl std::ops::DerefMut<Target = Shell<SE>> + '_, error::Error> {
let mut scoped = shell.enter_scope(env::EnvironmentScope::Command);
for (name, value) in vars {
let mut var = ShellVariable::new(value);
if export {
var.export();
}
scoped
.env_mut()
.add(name, var, env::EnvironmentScope::Command)?;
}
Ok(scoped)
}
#[derive(Clone, Copy, Debug)]
struct Context<'a> {
word: &'a str,
line: LineContext<'a>,
}
#[derive(Clone, Copy, Debug)]
struct LineContext<'a> {
input: &'a str,
cursor: usize,
words: &'a LineWords<'a>,
command_name: Option<&'a str>,
comp_type: u8,
comp_key: u8,
}
impl<'a> LineContext<'a> {
fn new(input: &'a str, cursor: usize, words: &'a LineWords<'a>) -> Self {
Self {
input,
cursor,
words,
command_name: words.command_name(),
comp_type: b'\t',
comp_key: b'\t',
}
}
fn args(&self, word: &'a str) -> [&'a str; 3] {
[
self.command_name.unwrap_or(""),
word,
self.words.preceding_word().unwrap_or(""),
]
}
fn comp_vars(&self) -> Vec<(&'static str, ShellValue)> {
vec![
("COMP_LINE", self.input.into()),
("COMP_POINT", self.cursor.to_string().into()),
("COMP_KEY", self.comp_key.to_string().into()),
("COMP_TYPE", self.comp_type.to_string().into()),
]
}
fn comp_words_vars(&self) -> [(&'static str, ShellValue); 2] {
let cword = self
.words
.cword
.map_or_else(|| "-1".to_owned(), |i| i.to_string());
[
("COMP_WORDS", self.words.comp_words.clone().into()),
("COMP_CWORD", cword.into()),
]
}
}
enum Generated<T> {
Candidates(T),
Restart,
}
struct SpecCandidates {
candidates: Vec<String>,
file_names: bool,
}
impl Spec {
pub async fn generate(
&self,
shell: &mut Shell<impl extensions::ShellExtensions>,
word: &str,
) -> Result<Vec<String>, crate::error::Error> {
let word = unquote_str(word);
let context = Context {
word: &word,
line: LineContext {
input: "",
cursor: 0,
words: &words::NO_WORDS,
command_name: Some("compgen"),
comp_type: 0,
comp_key: 0,
},
};
let Generated::Candidates(SpecCandidates { candidates, .. }) =
self.generate_candidates(shell, &context).await?
else {
return Ok(Vec::new());
};
let (candidates, _options) = self
.apply_options(shell, &context, candidates, self.options.clone())
.await;
Ok(candidates)
}
async fn complete(
&self,
shell: &mut Shell<impl extensions::ShellExtensions>,
context: &Context<'_>,
) -> Result<Generated<(Vec<String>, ProcessingOptions)>, crate::error::Error> {
let mut scope = InProgressScope::start(
shell,
InProgressCompletion {
options: self.options.clone(),
},
);
let Generated::Candidates(SpecCandidates {
candidates,
file_names,
}) = self.generate_candidates(scope.shell, context).await?
else {
return Ok(Generated::Restart);
};
let mut options = scope.take_options().unwrap_or_else(|| self.options.clone());
if file_names {
options.set(CompleteOption::FileNames, true);
}
let sort = !options.get(CompleteOption::NoSort);
let (mut candidates, processing_options) = self
.apply_options(scope.shell, context, candidates, options)
.await;
if sort {
candidates.sort();
}
Ok(Generated::Candidates((candidates, processing_options)))
}
async fn generate_candidates(
&self,
shell: &mut Shell<impl extensions::ShellExtensions>,
context: &Context<'_>,
) -> Result<Generated<SpecCandidates>, crate::error::Error> {
let SpecCandidates {
mut candidates,
file_names,
} = self.generate_action_completions(shell, context).await?;
if let Some(word_list) = &self.word_list {
let params = shell.default_exec_params();
let unexpanded_words =
split_completion_word_list(word_list, &shell.ifs(), &shell.parser_options())?;
let options = crate::expansion::ExpanderOptions {
pathname_expand: false,
..Default::default()
};
let mut words = vec![];
for word in unexpanded_words {
words.extend(
crate::expansion::full_expand_and_split_word_with_options(
shell, ¶ms, word, &options,
)
.await?,
);
}
candidates.extend(
words
.into_iter()
.filter(|word| word.starts_with(context.word)),
);
}
if let Some(glob_pattern) = &self.glob_pattern {
let pattern = shell_pattern(shell, glob_pattern.as_str());
let expansions = pattern
.expand(
shell.working_dir(),
Some(&patterns::Pattern::accept_all_expand_filter),
&patterns::FilenameExpansionOptions::default(),
)?
.into_paths();
candidates.extend(expansions);
}
if let Some(function_name) = &self.function_name {
match call_completion_function(shell, function_name, context).await? {
Generated::Candidates(new_candidates) => candidates.extend(new_candidates),
Generated::Restart => return Ok(Generated::Restart),
}
}
if let Some(command) = &self.command {
candidates.extend(call_completion_command(shell, command, context).await?);
}
if let Some(filter_pattern) = &self.filter_pattern
&& !filter_pattern.is_empty()
{
let mut updated = Vec::new();
let (pattern, keep_matches) = match filter_pattern.strip_prefix('!') {
Some(rest) if !(shell.options().extended_globbing && rest.starts_with('(')) => {
(rest, true)
}
_ => (filter_pattern.as_str(), false),
};
for candidate in candidates {
let matches = completion_filter_pattern_matches(
pattern,
candidate.as_str(),
context.word,
shell,
)?;
if matches == keep_matches {
updated.push(candidate);
}
}
candidates = updated;
}
if self.prefix.is_some() || self.suffix.is_some() {
let empty = String::new();
let prefix = self.prefix.as_ref().unwrap_or(&empty);
let suffix = self.suffix.as_ref().unwrap_or(&empty);
let mut updated = Vec::with_capacity(candidates.len() * (prefix.len() + suffix.len()));
for candidate in candidates {
updated.push(std::format!("{prefix}{candidate}{suffix}"));
}
candidates = updated;
}
Ok(Generated::Candidates(SpecCandidates {
candidates,
file_names,
}))
}
async fn apply_options(
&self,
shell: &Shell<impl extensions::ShellExtensions>,
context: &Context<'_>,
mut candidates: Vec<String>,
options: GenerationOptions,
) -> (Vec<String>, ProcessingOptions) {
let mut processing_options = ProcessingOptions {
treat_as_filenames: options.get(CompleteOption::FileNames),
no_autoquote_filenames: options.get(CompleteOption::NoQuote),
no_trailing_space_at_end_of_line: options.get(CompleteOption::NoSpace),
};
if options.get(CompleteOption::PlusDirs)
|| (options.get(CompleteOption::DirNames) && candidates.is_empty())
{
let mut dir_candidates =
get_file_completions(shell, context.word, true).await;
if candidates.is_empty() && shell.completion_config().fallback_options.mark_directories
{
processing_options.treat_as_filenames = true;
}
candidates.append(&mut dir_candidates);
}
if candidates.is_empty() && options.get(CompleteOption::BashDefault) {
tracing::debug!(target: trace_categories::COMPLETION, "unimplemented: complete -o bashdefault");
}
if candidates.is_empty() && options.get(CompleteOption::Default) {
let must_be_dir = options.get(CompleteOption::DirNames);
let mut default_candidates =
get_file_completions(shell, context.word, must_be_dir).await;
candidates.append(&mut default_candidates);
if shell.completion_config().fallback_options.mark_directories {
processing_options.treat_as_filenames = true;
}
}
(candidates, processing_options)
}
#[expect(clippy::too_many_lines, reason = "a flat match over the actions")]
async fn generate_action_completions(
&self,
shell: &Shell<impl extensions::ShellExtensions>,
context: &Context<'_>,
) -> Result<SpecCandidates, error::Error> {
let prefix = context.word;
let mut candidates = Vec::new();
let mut file_names = false;
for action in &self.actions {
let c = &mut candidates;
match action {
CompleteAction::Alias => {
extend_matching(c, shell.aliases().keys().sorted(), prefix);
}
CompleteAction::ArrayVar => extend_matching(
c,
shell
.env()
.iter()
.filter(|(_, var)| var.value().is_array())
.map(|(name, _)| name),
prefix,
),
CompleteAction::Binding => extend_matching(
c,
interfaces::InputFunction::iter().map(<&'static str>::from),
prefix,
),
CompleteAction::Builtin | CompleteAction::HelpTopic => {
extend_matching(c, shell.builtins().keys(), prefix);
}
CompleteAction::Command => c.extend(command_completions(shell, prefix)),
CompleteAction::Directory => {
let dirs = get_file_completions(shell, context.word, true).await;
file_names |= !dirs.is_empty();
c.extend(dirs);
}
CompleteAction::Disabled => extend_matching(
c,
shell
.builtins()
.iter()
.filter(|(_, registration)| registration.disabled)
.map(|(name, _)| name),
prefix,
),
CompleteAction::Enabled => extend_matching(
c,
shell
.builtins()
.iter()
.filter(|(_, registration)| !registration.disabled)
.map(|(name, _)| name),
prefix,
),
CompleteAction::Export => extend_matching(
c,
shell
.env()
.iter()
.filter(|(_, var)| var.is_exported())
.map(|(name, _)| name),
prefix,
),
CompleteAction::File => {
file_names = true;
c.extend(get_file_completions(shell, context.word, false).await);
}
CompleteAction::Function => extend_matching(
c,
shell.funcs().iter().map(|(name, _)| name).sorted(),
prefix,
),
CompleteAction::Group => extend_matching(c, users::get_all_groups()?, prefix),
CompleteAction::HostName => extend_matching(
c,
sys::network::get_hostname()
.ok()
.map(|name| name.to_string_lossy().into_owned()),
prefix,
),
CompleteAction::Job => extend_matching(
c,
shell.jobs().jobs.iter().map(|job| job.command_name()),
prefix,
),
CompleteAction::Keyword => extend_matching(c, shell.get_keywords(), prefix),
CompleteAction::Running => extend_matching(
c,
shell
.jobs()
.jobs
.iter()
.filter(|job| matches!(job.state, jobs::JobState::Running))
.map(|job| job.command_name()),
prefix,
),
CompleteAction::Service => {
tracing::debug!(target: trace_categories::COMPLETION, "unimplemented: complete -A service");
}
CompleteAction::SetOpt => extend_matching(
c,
namedoptions::options(namedoptions::ShellOptionKind::SetO)
.iter()
.map(|option| option.name),
prefix,
),
CompleteAction::ShOpt => extend_matching(
c,
namedoptions::options(namedoptions::ShellOptionKind::Shopt)
.iter()
.map(|option| option.name),
prefix,
),
CompleteAction::Signal => extend_matching(
c,
traps::TrapSignal::iterator().map(traps::TrapSignal::as_str),
prefix,
),
CompleteAction::Stopped => extend_matching(
c,
shell
.jobs()
.jobs
.iter()
.filter(|job| matches!(job.state, jobs::JobState::Stopped))
.map(|job| job.command_name()),
prefix,
),
CompleteAction::User => extend_matching(c, users::get_all_users()?, prefix),
CompleteAction::Variable => {
extend_matching(c, shell.env().iter().map(|(name, _)| name), prefix);
}
}
}
Ok(SpecCandidates {
candidates,
file_names,
})
}
}
async fn call_completion_command(
shell: &mut Shell<impl extensions::ShellExtensions>,
command: &str,
context: &Context<'_>,
) -> Result<Vec<String>, error::Error> {
let line = context.line;
let mut command_line = command.to_owned();
for arg in line.args(context.word) {
command_line.push(' ');
command_line.push_str(&escape::quote_if_needed(
arg,
escape::QuoteMode::SingleQuote,
));
}
let status = shell.save_command_status();
let mut scoped = enter_comp_vars_scope(shell, line.comp_vars(), true)?;
let mut blocked = scoped.block_trap_delivery();
let params = blocked.default_exec_params();
let output =
commands::invoke_command_in_subshell_and_get_output(&mut blocked, ¶ms, command_line)
.await;
drop(blocked);
drop(scoped);
shell.restore_command_status(status);
Ok(output?.lines().map(str::to_owned).collect())
}
async fn call_completion_function(
shell: &mut Shell<impl extensions::ShellExtensions>,
function_name: &str,
context: &Context<'_>,
) -> Result<Generated<Vec<String>>, error::Error> {
let line = context.line;
let mut vars = line.comp_vars();
vars.extend(line.comp_words_vars());
tracing::debug!(target: trace_categories::COMPLETION, "[calling completion func '{function_name}']: {}",
vars.iter().map(|(k, v)| std::format!("{k}={v:?}")).collect::<Vec<String>>().join(" "));
let names: Vec<_> = vars.iter().map(|(name, _)| *name).collect();
let mut scoped = enter_comp_vars_scope(shell, vars, false)?;
let mut blocked = scoped.block_trap_delivery();
let params = blocked.default_exec_params();
let invoke_result = blocked
.invoke_function(function_name, line.args(context.word).iter(), params)
.await
.map(|result| u8::from(result.exit_code));
drop(blocked);
drop(scoped);
for name in names {
let _ = shell.env_mut().unset(name);
}
tracing::debug!(target: trace_categories::COMPLETION, "[completion function '{function_name}' returned: {invoke_result:?}]");
let result = invoke_result.unwrap_or_else(|e| {
tracing::warn!(target: trace_categories::COMPLETION, "error while running completion function '{function_name}': {e}");
1 });
let reply = shell.env_mut().unset("COMPREPLY")?;
if result == 124 {
return Ok(Generated::Restart);
}
tracing::debug!(target: trace_categories::COMPLETION, "[completion function yielded: {reply:?}]");
let candidates = match reply.as_ref().map(|reply| reply.value()) {
Some(variables::ShellValue::IndexedArray(values)) => {
values.values().map(|v| v.to_owned()).collect()
}
Some(variables::ShellValue::String(s)) => vec![s.to_owned()],
_ => Vec::new(),
};
Ok(Generated::Candidates(candidates))
}
#[derive(Debug, Default)]
pub struct Completions {
pub insertion_index: usize,
pub delete_count: usize,
pub candidates: Vec<String>,
pub options: ProcessingOptions,
}
#[derive(Debug)]
pub struct ProcessingOptions {
pub treat_as_filenames: bool,
pub no_autoquote_filenames: bool,
pub no_trailing_space_at_end_of_line: bool,
}
impl Default for ProcessingOptions {
fn default() -> Self {
Self {
treat_as_filenames: true,
no_autoquote_filenames: false,
no_trailing_space_at_end_of_line: false,
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, strum_macros::EnumIter)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SpecialSpec {
Default,
EmptyLine,
InitialWord,
}
impl SpecialSpec {
pub const fn command_name(self) -> &'static str {
match self {
Self::Default => "_DefaultCmD_",
Self::EmptyLine => "_EmptycmD_",
Self::InitialWord => "_InitialWorD_",
}
}
pub fn from_command_name(name: &str) -> Option<Self> {
Self::iter().find(|special| special.command_name() == name)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum SpecName<'a> {
Command(&'a str),
Special(SpecialSpec),
}
impl<'a> SpecName<'a> {
pub fn parse(name: &'a str) -> Self {
SpecialSpec::from_command_name(name).map_or(Self::Command(name), Self::Special)
}
pub const fn as_str(self) -> &'a str {
match self {
Self::Command(command) => command,
Self::Special(special) => special.command_name(),
}
}
}
impl Config {
pub fn clear(&mut self) {
self.commands.clear();
self.specials.clear();
}
pub fn remove(&mut self, name: SpecName<'_>) -> bool {
match name {
SpecName::Command(command) => self.commands.remove(command).is_some(),
SpecName::Special(special) => self.specials.remove(&special).is_some(),
}
}
pub fn iter(&self) -> impl Iterator<Item = (SpecName<'_>, &Spec)> {
let commands = self
.commands
.iter()
.map(|(command, spec)| (SpecName::Command(command), spec));
let specials = self
.specials
.iter()
.map(|(special, spec)| (SpecName::Special(*special), spec));
commands.chain(specials)
}
pub fn get(&self, name: SpecName<'_>) -> Option<&Spec> {
match name {
SpecName::Command(command) => self.commands.get(command),
SpecName::Special(special) => self.specials.get(&special),
}
}
pub fn get_mut(&mut self, name: SpecName<'_>) -> Option<&mut Spec> {
match name {
SpecName::Command(command) => self.commands.get_mut(command),
SpecName::Special(special) => self.specials.get_mut(&special),
}
}
pub fn set(&mut self, name: SpecName<'_>, spec: Spec) {
match name {
SpecName::Command(command) => {
self.commands.insert(command.to_owned(), spec);
}
SpecName::Special(special) => {
self.specials.insert(special, spec);
}
}
}
fn find_spec<'l>(&self, line: &LineContext<'l>) -> Option<(&Spec, Option<&'l str>)> {
let special = |special: SpecialSpec| {
let spec = self.specials.get(&special)?;
Some((spec, Some(special.command_name())))
};
if line.cursor == 0 && line.input.chars().next().is_none_or(char::is_whitespace) {
return special(SpecialSpec::EmptyLine);
}
if matches!(line.words.cword, None | Some(0)) {
return special(SpecialSpec::InitialWord);
}
let command_name = line.command_name?;
let spec = self
.commands
.get(command_name)
.or_else(|| {
let file_name = Path::new(command_name).file_name()?;
self.commands.get(file_name.to_string_lossy().as_ref())
})
.or_else(|| self.specials.get(&SpecialSpec::Default))?;
Some((spec, Some(command_name)))
}
}
pub(crate) async fn complete(
shell: &mut Shell<impl extensions::ShellExtensions>,
input: &str,
cursor: usize,
) -> Result<Completions, error::Error> {
const MAX_RESTARTS: u32 = 10;
if !input.is_char_boundary(cursor) {
return Err(error::ErrorKind::InvalidCompletionPosition(cursor).into());
}
let word_breaks = word_break_chars(shell);
let word = find_completion_word(input, &word_breaks, cursor);
let line_words = find_line_words(input, &word_breaks, cursor);
let context = Context {
word: &word.text,
line: LineContext::new(input, cursor, &line_words),
};
let mut completed = None;
for _ in 0..=MAX_RESTARTS {
if let Generated::Candidates(candidates_and_options) = complete_word(shell, context).await {
completed = Some(candidates_and_options);
break;
}
}
let (candidates, options) = completed.unwrap_or_else(|| {
tracing::warn!(target: trace_categories::COMPLETION, "completion kept restarting; giving up");
(Vec::new(), ProcessingOptions::default())
});
Ok(Completions {
insertion_index: word.range.start,
delete_count: word.range.len(),
candidates,
options,
})
}
fn word_break_chars(shell: &Shell<impl extensions::ShellExtensions>) -> Vec<char> {
const FALLBACK: &str = " \t\n\"\'@><=;|&(:";
shell
.env_str("COMP_WORDBREAKS")
.unwrap_or_else(|| FALLBACK.into())
.chars()
.collect()
}
async fn complete_word(
shell: &mut Shell<impl extensions::ShellExtensions>,
mut context: Context<'_>,
) -> Generated<(Vec<String>, ProcessingOptions)> {
let Some((spec, command_name)) = shell.completion_config().find_spec(&context.line) else {
return Generated::Candidates(get_completions_using_basic_lookup(shell, &context).await);
};
let spec = spec.clone();
context.line.command_name = command_name;
spec.complete(shell, &context).await.unwrap_or_else(|err| {
tracing::debug!(target: trace_categories::COMPLETION, "completion spec failed: {err}");
Generated::Candidates((Vec::new(), ProcessingOptions::default()))
})
}
async fn get_file_completions(
shell: &Shell<impl extensions::ShellExtensions>,
token_to_complete: &str,
must_be_dir: bool,
) -> Vec<String> {
let mut throwaway_shell = shell.clone();
let params = throwaway_shell.default_exec_params();
let options = expansion::ExpanderOptions {
execute_command_substitutions: false,
..Default::default()
};
let expanded_token = expansion::basic_expand_word_with_options(
&mut throwaway_shell,
¶ms,
&unquote_str(token_to_complete),
&options,
)
.await
.unwrap_or_else(|_err| token_to_complete.to_owned());
let expanded_token = sys::fs::normalize_path_separators(&expanded_token).into_owned();
let glob = std::format!("{expanded_token}*");
let path_filter = |path: &Path| !must_be_dir || shell.absolute_path(path).is_dir();
let pattern = shell_pattern(shell, glob);
let mut completions: Vec<String> = pattern
.expand(
shell.working_dir(),
Some(&path_filter),
&patterns::FilenameExpansionOptions::default(),
)
.unwrap_or_default()
.into_paths()
.into_iter()
.map(|p| match sys::fs::normalize_path_separators(&p) {
std::borrow::Cow::Borrowed(_) => p,
std::borrow::Cow::Owned(normalized) => normalized,
})
.collect();
match expanded_token.as_str() {
"." => {
completions.push(".".into());
completions.push("..".into());
}
".." => {
completions.push("..".into());
}
_ => {}
}
completions.sort();
completions.dedup();
completions
}
fn try_get_variable_completions(
shell: &Shell<impl extensions::ShellExtensions>,
token: &str,
) -> Option<(Vec<String>, ProcessingOptions)> {
let (var_prefix, use_braces) = if let Some(prefix) = token.strip_prefix("${") {
if prefix.contains('}') {
return None;
}
(prefix, true)
} else {
let prefix = token.strip_prefix('$')?;
(prefix, false)
};
if sys::fs::contains_path_separator(var_prefix) {
return None;
}
let mut candidates: Vec<String> = shell
.env()
.iter()
.filter(|(key, _)| key.starts_with(var_prefix))
.map(|(key, _)| {
if use_braces {
format!("${{{key}}}")
} else {
format!("${key}")
}
})
.collect();
candidates.sort();
let options = ProcessingOptions {
treat_as_filenames: false,
..ProcessingOptions::default()
};
Some((candidates, options))
}
fn command_completions(
shell: &Shell<impl extensions::ShellExtensions>,
prefix: &str,
) -> Vec<String> {
let mut names = Vec::new();
extend_matching(&mut names, shell.aliases().keys().sorted(), prefix);
extend_matching(&mut names, shell.get_keywords(), prefix);
extend_matching(
&mut names,
shell.funcs().iter().map(|(name, _)| name).sorted(),
prefix,
);
extend_matching(
&mut names,
shell
.builtins()
.iter()
.filter(|(_, registration)| !registration.disabled)
.map(|(name, _)| name)
.sorted(),
prefix,
);
names.extend(
shell
.find_executables_in_path_with_prefix(
prefix,
shell.options().case_insensitive_pathname_expansion,
)
.filter_map(|path| Some(path.file_name()?.to_string_lossy().into_owned())),
);
names
}
fn extend_matching<S: AsRef<str>>(
candidates: &mut Vec<String>,
names: impl IntoIterator<Item = S>,
prefix: &str,
) {
candidates.extend(
names
.into_iter()
.filter(|name| name.as_ref().starts_with(prefix))
.map(|name| name.as_ref().to_owned()),
);
}
async fn get_completions_using_basic_lookup(
shell: &Shell<impl extensions::ShellExtensions>,
context: &Context<'_>,
) -> (Vec<String>, ProcessingOptions) {
let token = context.word;
if let Some(answer) = try_get_variable_completions(shell, token) {
return answer;
}
let mut candidates = get_file_completions(shell, token, false).await;
let is_command_position = context.line.words.cword == Some(0)
&& !token.is_empty()
&& !sys::fs::contains_path_separator(token);
if is_command_position {
candidates.extend(command_completions(shell, token));
candidates.sort();
}
(candidates, ProcessingOptions::default())
}
fn completion_filter_pattern_matches(
pattern: &str,
candidate: &str,
token_being_completed: &str,
shell: &Shell<impl extensions::ShellExtensions>,
) -> Result<bool, error::Error> {
let pattern = replace_unescaped_ampersands(pattern, token_being_completed);
let pattern = shell_pattern(shell, pattern.as_ref());
let matches = pattern.exactly_matches(candidate)?;
Ok(matches)
}
fn replace_unescaped_ampersands<'a>(pattern: &'a str, replacement: &str) -> Cow<'a, str> {
let mut in_escape = false;
let mut insertion_points = vec![];
for (i, c) in pattern.char_indices() {
if !in_escape && c == '&' {
insertion_points.push(i);
}
in_escape = !in_escape && c == '\\';
}
if insertion_points.is_empty() {
return pattern.into();
}
let mut result = pattern.to_owned();
for i in insertion_points.iter().rev() {
result.replace_range(*i..=*i, replacement);
}
result.into()
}
fn shell_pattern(
shell: &Shell<impl extensions::ShellExtensions>,
pattern: impl Into<patterns::Pattern>,
) -> patterns::Pattern {
pattern
.into()
.set_extended_globbing(shell.options().extended_globbing)
.set_case_insensitive(shell.options().case_insensitive_pathname_expansion)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generation_options_enable_and_disable_each_option() {
let mut options: GenerationOptions = std::iter::once(CompleteOption::NoSpace).collect();
assert!(options.get(CompleteOption::NoSpace));
assert!(!options.get(CompleteOption::NoSort));
options.set(CompleteOption::NoSort, true);
options.set(CompleteOption::NoSpace, false);
assert!(options.get(CompleteOption::NoSort));
assert!(!options.get(CompleteOption::NoSpace));
assert_eq!(
options,
std::iter::once(CompleteOption::NoSort).collect::<GenerationOptions>()
);
}
#[test]
fn special_specs_have_bash_names() {
assert_eq!(SpecialSpec::Default.command_name(), "_DefaultCmD_");
assert_eq!(SpecialSpec::EmptyLine.command_name(), "_EmptycmD_");
assert_eq!(SpecialSpec::InitialWord.command_name(), "_InitialWorD_");
}
#[test]
fn special_specs_are_each_stored_on_their_own() {
let mut config = Config::default();
let specials = [
(SpecialSpec::Default, "d"),
(SpecialSpec::EmptyLine, "e"),
(SpecialSpec::InitialWord, "i"),
];
for (special, word) in specials {
assert!(config.get(SpecName::Special(special)).is_none());
let spec = Spec {
word_list: Some(word.to_owned()),
..Spec::default()
};
config.set(SpecName::Special(special), spec);
}
for (special, word) in specials {
let spec = config.get(SpecName::Special(special));
assert_eq!(spec.and_then(|spec| spec.word_list.as_deref()), Some(word));
if let Some(spec) = config.get_mut(SpecName::Special(special)) {
spec.prefix = Some(word.to_uppercase());
}
let spec = config.get(SpecName::parse(special.command_name()));
assert_eq!(
spec.and_then(|spec| spec.prefix.clone()),
Some(word.to_uppercase())
);
}
}
#[test]
fn spec_names_parse_bash_names_for_special_specs() {
assert_eq!(
SpecName::parse("_DefaultCmD_"),
SpecName::Special(SpecialSpec::Default)
);
assert_eq!(SpecName::parse("mycmd"), SpecName::Command("mycmd"));
for special in SpecialSpec::iter() {
let name = SpecName::Special(special);
assert_eq!(SpecName::parse(name.as_str()), name);
}
assert_eq!(SpecName::Command("mycmd").as_str(), "mycmd");
}
#[test]
fn special_specs_round_trip_through_bash_names() {
for special in SpecialSpec::iter() {
assert_eq!(
SpecialSpec::from_command_name(special.command_name()),
Some(special)
);
}
assert_eq!(
SpecialSpec::from_command_name("_DefaultCmD_"),
Some(SpecialSpec::Default)
);
assert_eq!(SpecialSpec::from_command_name("mycmd"), None);
}
#[test]
fn completion_names_are_bash_names() {
for action in CompleteAction::iter() {
assert_eq!(<&str>::from(action).parse::<CompleteAction>(), Ok(action));
}
for option in CompleteOption::iter() {
assert_eq!(<&str>::from(&option).parse::<CompleteOption>(), Ok(option));
}
assert_eq!(<&str>::from(CompleteAction::ArrayVar), "arrayvar");
assert_eq!(<&str>::from(CompleteOption::BashDefault), "bashdefault");
}
}