From 62f945134e77816d04607d2b865991f93bfda0f6 Mon Sep 17 00:00:00 2001 From: reuben olinsky Date: Mon, 13 May 2024 09:41:06 -0700 Subject: [PATCH] Implement more of compgen (#8) Adds naive implementation for file, alias, builtin, and function. --- cli/src/main.rs | 3 + shell/src/builtins/help.rs | 9 +-- shell/src/builtins/mod.rs | 10 +++ shell/src/completion.rs | 140 ++++++++++++++++++++++--------------- shell/src/patterns.rs | 15 ++-- 5 files changed, 105 insertions(+), 72 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index e3dbd5f4..78845454 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -82,6 +82,8 @@ enum TraceEvent { Expand, #[clap(name = "parse")] Parse, + #[clap(name = "pattern")] + Pattern, #[clap(name = "tokenize")] Tokenize, } @@ -125,6 +127,7 @@ fn main() { TraceEvent::Complete => vec!["shell::completion", "shell::builtins::complete"], TraceEvent::Expand => vec![], TraceEvent::Parse => vec!["parse"], + TraceEvent::Pattern => vec!["shell::pattern"], TraceEvent::Tokenize => vec!["tokenize"], }; diff --git a/shell/src/builtins/help.rs b/shell/src/builtins/help.rs index b1e1aeb4..7241cf26 100644 --- a/shell/src/builtins/help.rs +++ b/shell/src/builtins/help.rs @@ -1,8 +1,6 @@ use crate::builtin::{BuiltinCommand, BuiltinExitCode}; use clap::Parser; -use itertools::Itertools; use std::io::Write; -use std::iter::Iterator; #[derive(Parser)] pub(crate) struct HelpCommand {} @@ -22,12 +20,7 @@ impl BuiltinCommand for HelpCommand { "The following commands are implemented as shell built-ins:" )?; - let builtin_names: Vec<_> = super::SPECIAL_BUILTINS - .iter() - .chain(super::BUILTINS.iter()) - .map(|(name, _)| *name) - .sorted() - .collect(); + let builtin_names: Vec<_> = crate::builtins::get_all_builtin_names(); const COLUMN_COUNT: usize = 3; diff --git a/shell/src/builtins/mod.rs b/shell/src/builtins/mod.rs index 770c2240..68211269 100644 --- a/shell/src/builtins/mod.rs +++ b/shell/src/builtins/mod.rs @@ -1,4 +1,5 @@ use futures::future::BoxFuture; +use itertools::Itertools; use std::collections::{HashMap, HashSet}; use std::io::Write; @@ -130,6 +131,15 @@ lazy_static::lazy_static! { pub(crate) static ref DECLARATION_BUILTINS: HashSet<&'static str> = get_declaration_builtin_names(); } +pub(crate) fn get_all_builtin_names() -> Vec { + SPECIAL_BUILTINS + .iter() + .chain(BUILTINS.iter()) + .map(|(name, _)| (*name).to_owned()) + .sorted() + .collect::>() +} + fn get_declaration_builtin_names() -> HashSet<&'static str> { let mut s = HashSet::new(); s.insert("alias"); diff --git a/shell/src/completion.rs b/shell/src/completion.rs index a8d0668c..75bbe73a 100644 --- a/shell/src/completion.rs +++ b/shell/src/completion.rs @@ -4,7 +4,7 @@ use std::{ path::{Path, PathBuf}, }; -use crate::{env, error, patterns, variables::ShellValueLiteral, Shell}; +use crate::{builtins, env, error, patterns, variables::ShellValueLiteral, Shell}; #[derive(Clone, Debug, ValueEnum)] pub enum CompleteAction { @@ -145,6 +145,7 @@ pub struct CompletionContext<'a> { } impl CompletionSpec { + #[allow(clippy::too_many_lines)] pub async fn get_completions( &self, shell: &mut Shell, @@ -177,19 +178,39 @@ impl CompletionSpec { for action in &self.actions { match action { - CompleteAction::Alias => tracing::debug!("UNIMPLEMENTED: complete -A alias"), + CompleteAction::Alias => { + for name in shell.aliases.keys() { + candidates.push(name.to_string()); + } + } CompleteAction::ArrayVar => tracing::debug!("UNIMPLEMENTED: complete -A arrayvar"), CompleteAction::Binding => tracing::debug!("UNIMPLEMENTED: complete -A binding"), - CompleteAction::Builtin => tracing::debug!("UNIMPLEMENTED: complete -A builtin"), + CompleteAction::Builtin => { + let mut builtin_names = builtins::get_all_builtin_names(); + candidates.append(&mut builtin_names); + } CompleteAction::Command => tracing::debug!("UNIMPLEMENTED: complete -A command"), CompleteAction::Directory => { tracing::debug!("UNIMPLEMENTED: complete -A directory"); } CompleteAction::Disabled => tracing::debug!("UNIMPLEMENTED: complete -A disabled"), CompleteAction::Enabled => tracing::debug!("UNIMPLEMENTED: complete -A enabled"), - CompleteAction::Export => tracing::debug!("UNIMPLEMENTED: complete -A export"), - CompleteAction::File => tracing::debug!("UNIMPLEMENTED: complete -A file"), - CompleteAction::Function => tracing::debug!("UNIMPLEMENTED: complete -A function"), + CompleteAction::Export => { + for (key, value) in shell.env.iter() { + if value.is_exported() { + candidates.push(key.to_owned()); + } + } + } + CompleteAction::File => { + let mut file_completions = get_file_completions(shell, context); + candidates.append(&mut file_completions); + } + CompleteAction::Function => { + for name in shell.funcs.keys() { + candidates.push(name.to_owned()); + } + } CompleteAction::Group => tracing::debug!("UNIMPLEMENTED: complete -A group"), CompleteAction::HelpTopic => { tracing::debug!("UNIMPLEMENTED: complete -A helptopic"); @@ -205,9 +226,9 @@ impl CompletionSpec { CompleteAction::Stopped => tracing::debug!("UNIMPLEMENTED: complete -A stopped"), CompleteAction::User => tracing::debug!("UNIMPLEMENTED: complete -A user"), CompleteAction::Variable => { - shell.env.iter().for_each(|(key, _value)| { - candidates.push(key.to_string()); - }); + for (key, _) in shell.env.iter() { + candidates.push(key.to_owned()); + } } } } @@ -502,64 +523,67 @@ impl CompletionConfig { } } - self.get_completions_using_basic_lookup(shell, &context) + get_completions_using_basic_lookup(shell, &context) } +} - #[allow(clippy::unused_self)] - fn get_completions_using_basic_lookup( - &self, - shell: &Shell, - context: &CompletionContext, - ) -> CompletionResult { - // TODO: Contextually generate different completions. - let glob = std::format!("{}*", context.token_to_complete); - // TODO: Pass through quoting. - let mut candidates = if let Ok(mut candidates) = patterns::Pattern::from(glob) - .expand(shell.working_dir.as_path(), shell.options.extended_globbing) - { - for candidate in &mut candidates { - if Path::new(candidate.as_str()).is_dir() { - candidate.push('/'); - } - } - candidates - } else { - vec![] - }; - - // If this appears to be the command token (and if there's *some* prefix without - // a path separator) then also consider whether we should search the path for - // completions too. - // TODO: Do a better job than just checking if index == 0. - if context.token_index == 0 - && !context.token_to_complete.is_empty() - && !context.token_to_complete.contains('/') - { - let glob_pattern = std::format!("{}*", context.token_to_complete); - - for path in shell.find_executables_in_path(&glob_pattern) { - if let Some(file_name) = path.file_name() { - candidates.push(file_name.to_string_lossy().to_string()); - } +fn get_file_completions(shell: &Shell, context: &CompletionContext) -> Vec { + let glob = std::format!("{}*", context.token_to_complete); + + // TODO: Pass through quoting. + if let Ok(mut candidates) = patterns::Pattern::from(glob) + .expand(shell.working_dir.as_path(), shell.options.extended_globbing) + { + for candidate in &mut candidates { + if Path::new(candidate.as_str()).is_dir() { + candidate.push('/'); } } + candidates + } else { + vec![] + } +} - if context.token_index + 1 >= context.tokens.len() { - for candidate in &mut candidates { - if !candidate.ends_with('/') { - candidate.push(' '); - } +fn get_completions_using_basic_lookup( + shell: &Shell, + context: &CompletionContext, +) -> CompletionResult { + let mut candidates = get_file_completions(shell, context); + + // TODO: Contextually generate different completions. + // If this appears to be the command token (and if there's *some* prefix without + // a path separator) then also consider whether we should search the path for + // completions too. + // TODO: Do a better job than just checking if index == 0. + if context.token_index == 0 + && !context.token_to_complete.is_empty() + && !context.token_to_complete.contains('/') + { + let glob_pattern = std::format!("{}*", context.token_to_complete); + + for path in shell.find_executables_in_path(&glob_pattern) { + if let Some(file_name) = path.file_name() { + candidates.push(file_name.to_string_lossy().to_string()); } } + } - #[cfg(windows)] - { - candidates = candidates - .into_iter() - .map(|c| c.replace("\\", "/")) - .collect(); + if context.token_index + 1 >= context.tokens.len() { + for candidate in &mut candidates { + if !candidate.ends_with('/') { + candidate.push(' '); + } } + } - CompletionResult::Candidates(candidates) + #[cfg(windows)] + { + candidates = candidates + .into_iter() + .map(|c| c.replace("\\", "/")) + .collect(); } + + CompletionResult::Candidates(candidates) } diff --git a/shell/src/patterns.rs b/shell/src/patterns.rs index a42ac7ae..a7baf989 100644 --- a/shell/src/patterns.rs +++ b/shell/src/patterns.rs @@ -75,6 +75,8 @@ impl Pattern { return Ok(vec![concatenated]); } + tracing::debug!("expanding pattern: {self:?}"); + let mut components: Vec = vec![]; for piece in &self.pieces { let mut split_result = piece @@ -100,11 +102,9 @@ impl Pattern { } let is_absolute = if let Some(first_component) = components.first() { - if let Some(first_piece) = first_component.first() { - first_piece.as_str().starts_with(std::path::MAIN_SEPARATOR) - } else { - false - } + first_component + .iter() + .all(|piece| piece.as_str().is_empty()) } else { false }; @@ -112,7 +112,8 @@ impl Pattern { let prefix_to_remove; let mut paths_so_far = if is_absolute { prefix_to_remove = None; - vec![PathBuf::new()] + // TODO: Figure out appropriate thing to do on non-Unix platforms. + vec![PathBuf::from("/")] } else { let mut working_dir_str = working_dir.to_string_lossy().to_string(); working_dir_str.push(std::path::MAIN_SEPARATOR); @@ -172,6 +173,8 @@ impl Pattern { }) .collect(); + tracing::debug!(" => results: {results:?}"); + Ok(results) }