From de3c243f5c20eaaeb5f1fe36902064d298d47742 Mon Sep 17 00:00:00 2001 From: reuben olinsky Date: Mon, 13 May 2024 12:37:17 -0700 Subject: [PATCH] Support completion of dirs (#9) --- shell/src/builtins/help.rs | 2 +- shell/src/completion.rs | 29 ++++++++++++++++++++--------- shell/src/expansion.rs | 1 + shell/src/patterns.rs | 38 +++++++++++++++++++++++++++++++------- shell/src/shell.rs | 15 +++++++++++---- 5 files changed, 64 insertions(+), 21 deletions(-) diff --git a/shell/src/builtins/help.rs b/shell/src/builtins/help.rs index 7241cf26..3a584589 100644 --- a/shell/src/builtins/help.rs +++ b/shell/src/builtins/help.rs @@ -20,7 +20,7 @@ impl BuiltinCommand for HelpCommand { "The following commands are implemented as shell built-ins:" )?; - let builtin_names: Vec<_> = crate::builtins::get_all_builtin_names(); + let builtin_names = context.shell.get_builtin_names(); const COLUMN_COUNT: usize = 3; diff --git a/shell/src/completion.rs b/shell/src/completion.rs index 75bbe73a..99ef8f61 100644 --- a/shell/src/completion.rs +++ b/shell/src/completion.rs @@ -4,7 +4,7 @@ use std::{ path::{Path, PathBuf}, }; -use crate::{builtins, env, error, patterns, variables::ShellValueLiteral, Shell}; +use crate::{env, error, patterns, variables::ShellValueLiteral, Shell}; #[derive(Clone, Debug, ValueEnum)] pub enum CompleteAction { @@ -186,12 +186,13 @@ impl CompletionSpec { CompleteAction::ArrayVar => tracing::debug!("UNIMPLEMENTED: complete -A arrayvar"), CompleteAction::Binding => tracing::debug!("UNIMPLEMENTED: complete -A binding"), CompleteAction::Builtin => { - let mut builtin_names = builtins::get_all_builtin_names(); + let mut builtin_names = shell.get_builtin_names(); candidates.append(&mut builtin_names); } CompleteAction::Command => tracing::debug!("UNIMPLEMENTED: complete -A command"), CompleteAction::Directory => { - tracing::debug!("UNIMPLEMENTED: complete -A directory"); + let mut file_completions = get_file_completions(shell, context, true); + candidates.append(&mut file_completions); } CompleteAction::Disabled => tracing::debug!("UNIMPLEMENTED: complete -A disabled"), CompleteAction::Enabled => tracing::debug!("UNIMPLEMENTED: complete -A enabled"), @@ -203,7 +204,7 @@ impl CompletionSpec { } } CompleteAction::File => { - let mut file_completions = get_file_completions(shell, context); + let mut file_completions = get_file_completions(shell, context, false); candidates.append(&mut file_completions); } CompleteAction::Function => { @@ -527,13 +528,23 @@ impl CompletionConfig { } } -fn get_file_completions(shell: &Shell, context: &CompletionContext) -> Vec { +fn get_file_completions( + shell: &Shell, + context: &CompletionContext, + must_be_dir: bool, +) -> Vec { let glob = std::format!("{}*", context.token_to_complete); + let metadata_filter = |metadata: Option<&std::fs::Metadata>| { + !must_be_dir || metadata.is_some_and(|md| md.is_dir()) + }; + // TODO: Pass through quoting. - if let Ok(mut candidates) = patterns::Pattern::from(glob) - .expand(shell.working_dir.as_path(), shell.options.extended_globbing) - { + if let Ok(mut candidates) = patterns::Pattern::from(glob).expand( + shell.working_dir.as_path(), + shell.options.extended_globbing, + Some(&metadata_filter), + ) { for candidate in &mut candidates { if Path::new(candidate.as_str()).is_dir() { candidate.push('/'); @@ -549,7 +560,7 @@ fn get_completions_using_basic_lookup( shell: &Shell, context: &CompletionContext, ) -> CompletionResult { - let mut candidates = get_file_completions(shell, context); + let mut candidates = get_file_completions(shell, context, false); // TODO: Contextually generate different completions. // If this appears to be the command token (and if there's *some* prefix without diff --git a/shell/src/expansion.rs b/shell/src/expansion.rs index 8dcc0e23..448756c0 100644 --- a/shell/src/expansion.rs +++ b/shell/src/expansion.rs @@ -490,6 +490,7 @@ impl<'a> WordExpander<'a> { .expand( self.shell.working_dir.as_path(), self.parser_options.enable_extended_globbing, + Some(&patterns::Pattern::accept_all_expand_filter), ) .map_or_else( |_err| vec![], diff --git a/shell/src/patterns.rs b/shell/src/patterns.rs index a7baf989..e55717f8 100644 --- a/shell/src/patterns.rs +++ b/shell/src/patterns.rs @@ -61,11 +61,20 @@ impl Pattern { self.pieces.iter().all(|p| p.as_str().is_empty()) } - pub(crate) fn expand( + pub(crate) fn accept_all_expand_filter(_metadata: Option<&std::fs::Metadata>) -> bool { + true + } + + #[allow(clippy::too_many_lines)] + pub(crate) fn expand( &self, working_dir: &Path, enable_extended_globbing: bool, - ) -> Result, error::Error> { + metadata_filter: Option<&MF>, + ) -> Result, error::Error> + where + MF: Fn(Option<&std::fs::Metadata>) -> bool, + { if self.is_empty() { return Ok(vec![]); } else if !self.pieces.iter().any(|piece| { @@ -140,16 +149,31 @@ impl Pattern { for current_path in current_paths { let subpattern = Pattern::from(&component); let regex = subpattern.to_regex(true, true, enable_extended_globbing)?; + + let matches_criteria = |dir_entry: &std::fs::DirEntry| { + if !regex + .is_match(dir_entry.file_name().to_string_lossy().as_ref()) + .unwrap_or(false) + { + return false; + } + + if let Some(filter) = &metadata_filter { + let metadata_result = dir_entry.metadata(); + if !filter(metadata_result.as_ref().ok()) { + return false; + } + } + + true + }; + let mut matching_paths_in_dir: Vec<_> = current_path .read_dir() .map_or_else(|_| vec![], |dir| dir.into_iter().collect()) .into_iter() .filter_map(|result| result.ok()) - .filter(|entry| { - regex - .is_match(entry.file_name().to_string_lossy().as_ref()) - .unwrap_or(false) - }) + .filter(matches_criteria) .map(|entry| entry.path()) .collect(); diff --git a/shell/src/shell.rs b/shell/src/shell.rs index f434a431..94dca819 100644 --- a/shell/src/shell.rs +++ b/shell/src/shell.rs @@ -6,7 +6,6 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use crate::env::{EnvironmentLookup, EnvironmentScope, ShellEnvironment}; -use crate::error; use crate::expansion; use crate::interp::{self, Execute, ExecutionParameters, ExecutionResult}; use crate::jobs; @@ -14,6 +13,7 @@ use crate::openfiles; use crate::options::RuntimeOptions; use crate::prompt::expand_prompt; use crate::variables::{self, ShellValue, ShellVariable}; +use crate::{builtins, error}; use crate::{commands, patterns}; use crate::{completion, users}; use crate::{context, env}; @@ -709,9 +709,11 @@ impl Shell { for dir_str in self.env.get_str("PATH").unwrap_or_default().split(':') { let pattern = std::format!("{dir_str}/{required_glob_pattern}"); // TODO: Pass through quoting. - if let Ok(entries) = patterns::Pattern::from(pattern) - .expand(&self.working_dir, self.options.extended_globbing) - { + if let Ok(entries) = patterns::Pattern::from(pattern).expand( + &self.working_dir, + self.options.extended_globbing, + Some(&patterns::Pattern::accept_all_expand_filter), + ) { for entry in entries { let path = Path::new(&entry); if path.executable() { @@ -808,4 +810,9 @@ impl Shell { writeln!(self.stderr(), "{prefix}{}", command.as_ref()) } + + #[allow(clippy::unused_self)] + pub fn get_builtin_names(&self) -> Vec { + builtins::get_all_builtin_names() + } }