diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ffcd00ce..d00920d0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -95,12 +95,14 @@ jobs: - name: "Upload test results" uses: actions/upload-artifact@v4 + if: always() with: name: test-reports path: test-results-*.xml - name: "Generate code coverage report" uses: clearlyip/code-coverage-report-action@v5 + if: always() id: "code_coverage_report" with: artifact_download_workflow_names: "CI" @@ -108,6 +110,7 @@ jobs: - name: "Upload code coverage report" uses: actions/upload-artifact@v4 + if: always() with: name: codecov-reports path: code-coverage-results.md diff --git a/Cargo.lock b/Cargo.lock index 0f88f934..6f1e8981 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -240,13 +240,13 @@ dependencies = [ "clap", "colored", "descape", + "diff", "dir-cmp", "expectrl", "glob", "indent", "junit-report", "pathdiff", - "prettydiff", "regex", "serde", "serde_yaml", @@ -600,6 +600,12 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "396a0a312bef78b5f62b0251d7162c4b8af162949b8b104d2967e41b26c1b68c" +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "difflib" version = "0.4.0" @@ -1306,21 +1312,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" -[[package]] -name = "owo-colors" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" - -[[package]] -name = "pad" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ad9b889f1b12e0b9ee24db044b5129150d5eada288edc800f789928dc8c0e3" -dependencies = [ - "unicode-width", -] - [[package]] name = "parking_lot" version = "0.12.2" @@ -1391,9 +1382,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "plotters" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +checksum = "a15b6eccb8484002195a3e44fe65a4ce8e93a625797a063735536fd59cb01cf3" dependencies = [ "num-traits", "plotters-backend", @@ -1404,15 +1395,15 @@ dependencies = [ [[package]] name = "plotters-backend" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" +checksum = "414cec62c6634ae900ea1c56128dfe87cf63e7caece0852ec76aba307cebadb7" [[package]] name = "plotters-svg" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +checksum = "81b30686a7d9c3e010b84284bdd26a29f2138574f52f5eb6f794fc0ad924e705" dependencies = [ "plotters-backend", ] @@ -1478,16 +1469,6 @@ dependencies = [ "termtree", ] -[[package]] -name = "prettydiff" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abec3fb083c10660b3854367697da94c674e9e82aa7511014dc958beeb7215e9" -dependencies = [ - "owo-colors", - "pad", -] - [[package]] name = "proc-macro2" version = "1.0.83" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 6fc14589..61668ff6 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -37,13 +37,13 @@ assert_cmd = "2.0.14" assert_fs = "1.1.1" colored = "2.1.0" descape = "1.1.2" +diff = "0.1.13" dir-cmp = "0.1.0" expectrl = "0.7.1" glob = "0.3.1" indent = "0.1.1" junit-report = "0.8.3" pathdiff = "0.2.1" -prettydiff = { version = "0.7.0", default-features = false } regex = "1.10.4" serde = { version = "1.0.202", features = ["derive"] } serde_yaml = "0.9.34" diff --git a/cli/src/main.rs b/cli/src/main.rs index 35240deb..470b86ff 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -127,7 +127,11 @@ fn main() { let targets = match event { TraceEvent::Arithmetic => vec!["parser::arithmetic"], TraceEvent::Commands => vec!["commands"], - TraceEvent::Complete => vec!["shell::completion", "shell::builtins::complete"], + TraceEvent::Complete => vec![ + "completion", + "shell::completion", + "shell::builtins::complete", + ], TraceEvent::Expand => vec![], TraceEvent::Parse => vec!["parse"], TraceEvent::Pattern => vec!["shell::pattern"], diff --git a/cli/tests/cases/builtins/compgen.yaml b/cli/tests/cases/builtins/compgen.yaml index 383ba30c..d98d2326 100644 --- a/cli/tests/cases/builtins/compgen.yaml +++ b/cli/tests/cases/builtins/compgen.yaml @@ -5,29 +5,27 @@ cases: alias myalias="ls" alias myalias2="echo hi" - compgen -A alias myalias + compgen -A alias myalias | sort - name: "compgen -A builtin" stdin: | - compgen -A builtin cd + compgen -A builtin cd | sort - name: "compgen -A directory" - known_failure: true stdin: | touch somefile mkdir somedir mkdir somedir2 - compgen -A directory some + compgen -A directory some | sort - name: "compgen -A file" - known_failure: true stdin: | touch somefile mkdir somedir mkdir somedir2 - compgen -A file some + compgen -A file some | sort - name: "compgen -A function" stdin: | @@ -39,15 +37,15 @@ cases: echo hello } - compgen -A function myfunc + compgen -A function myfunc | sort - name: "compgen -A keyword" stdin: | - compgen -A keyword esa + compgen -A keyword esa | sort - name: "compgen -A variable" stdin: | declare myvar=10 declare myvar2=11 - compgen -A variable myvar + compgen -A variable myvar | sort diff --git a/cli/tests/cases/compound_cmds/while.yaml b/cli/tests/cases/compound_cmds/while.yaml index 1743d5a4..881957bc 100644 --- a/cli/tests/cases/compound_cmds/while.yaml +++ b/cli/tests/cases/compound_cmds/while.yaml @@ -4,12 +4,12 @@ cases: stdin: | while false; do echo 'In loop'; done - # - name: "break in while loop" - # stdin: | - # while true; do - # echo 'In loop' - # break - # done + - name: "break in while loop" + stdin: | + while true; do + echo 'In loop' + break + done - name: "Arithmetic in while loop" stdin: | diff --git a/cli/tests/cases/redirection.yaml b/cli/tests/cases/redirection.yaml index b1f155b4..25111a64 100644 --- a/cli/tests/cases/redirection.yaml +++ b/cli/tests/cases/redirection.yaml @@ -41,8 +41,8 @@ cases: stdin: | ls -d . non-existent-dir &>/dev/null - # - name: "Process substitution: input + output" - # known_failure: true # Issue #151 - # stdin: | - # shopt -u -o posix - # cp <(echo hi) >(cat) + - name: "Process substitution: input + output" + known_failure: true # Issue #151 + stdin: | + shopt -u -o posix + cp <(echo hi) >(cat) diff --git a/cli/tests/integration_tests.rs b/cli/tests/integration_tests.rs index b862d03b..63459470 100644 --- a/cli/tests/integration_tests.rs +++ b/cli/tests/integration_tests.rs @@ -559,6 +559,7 @@ impl TestCaseResult { " {}", "------ Test: stdout [cleaned]------------------------".cyan() )?; + writeln!( writer, "{}", @@ -578,14 +579,7 @@ impl TestCaseResult { "------ Oracle <> Test: stdout ---------------------------------".cyan() )?; - writeln!( - writer, - "{}", - indent::indent_all_by( - 8, - prettydiff::diff_lines(o.as_str(), t.as_str()).format() - ) - )?; + write_diff(&mut writer, 8, o.as_str(), t.as_str())?; writeln!( writer, @@ -613,14 +607,7 @@ impl TestCaseResult { "------ Oracle <> Test: stderr ---------------------------------".cyan() )?; - writeln!( - writer, - "{}", - indent::indent_all_by( - 8, - prettydiff::diff_lines(o.as_str(), t.as_str()).format() - ) - )?; + write_diff(&mut writer, 8, o.as_str(), t.as_str())?; writeln!( writer, @@ -1275,6 +1262,28 @@ fn make_expectrl_output_readable>(output: S) -> String { strip_ansi_escapes::strip_str(unescaped) } +fn write_diff( + writer: &mut impl std::io::Write, + indent: usize, + left: &str, + right: &str, +) -> Result<()> { + let indent_str = " ".repeat(indent); + + let diff = diff::lines(left, right); + for d in diff { + let formatted = match d { + diff::Result::Left(l) => std::format!("{indent_str}- {l}").red(), + diff::Result::Both(l, _) => std::format!("{indent_str} {l}").bright_black(), + diff::Result::Right(r) => std::format!("{indent_str}+ {r}").green(), + }; + + writeln!(writer, "{formatted}")?; + } + + Ok(()) +} + fn main() { let unparsed_args: Vec<_> = std::env::args().collect(); let options = TestOptions::parse_from(unparsed_args); diff --git a/interactive-shell/src/interactive_shell.rs b/interactive-shell/src/interactive_shell.rs index ce564ac6..58eca296 100644 --- a/interactive-shell/src/interactive_shell.rs +++ b/interactive-shell/src/interactive_shell.rs @@ -1,5 +1,9 @@ use rustyline::validate::ValidationResult; -use std::{borrow::Cow, io::Write, path::PathBuf}; +use std::{ + borrow::Cow, + io::Write, + path::{Path, PathBuf}, +}; type Editor = rustyline::Editor; @@ -228,11 +232,33 @@ impl EditorHelper { } }; - let completions = result.unwrap_or_else(|_| shell::Completions { + let mut completions = result.unwrap_or_else(|_| shell::Completions { start: pos, candidates: vec![], + options: shell::CandidateProcessingOptions::default(), }); + // TODO: implement completion postprocessing + let completing_end_of_line = pos == line.len(); + if completions.options.treat_as_filenames { + for candidate in &mut completions.candidates { + // Check if it's a directory. + if !candidate.ends_with('/') && Path::new(candidate).is_dir() { + candidate.push('/'); + } + } + } + if completions.options.no_autoquote_filenames { + tracing::debug!(target: "completion", "don't autoquote filenames"); + } + if completing_end_of_line && !completions.options.no_trailing_space_at_end_of_line { + for candidate in &mut completions.candidates { + if !completions.options.treat_as_filenames || !candidate.ends_with('/') { + candidate.push(' '); + } + } + } + let candidates = completions .candidates .into_iter() diff --git a/shell/src/builtins/complete.rs b/shell/src/builtins/complete.rs index d90ad8b5..41a66b0d 100644 --- a/shell/src/builtins/complete.rs +++ b/shell/src/builtins/complete.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use std::fmt::Write as _; use std::io::Write; -use crate::builtin::{BuiltinCommand, BuiltinExitCode}; +use crate::builtin::{self, BuiltinCommand, BuiltinExitCode}; use crate::completion::{self, CompleteAction, CompleteOption, CompletionSpec}; use crate::error; @@ -90,14 +90,7 @@ impl CommonCompleteCommandArgs { }; let mut spec = completion::CompletionSpec { - bash_default: false, - default: false, - dir_names: false, - file_names: false, - no_quote: false, - no_sort: false, - no_space: false, - plus_dirs: false, + options: completion::CompletionOptions::default(), actions: self.resolve_actions(), glob_pattern: self.glob_pattern.clone(), word_list: self.word_list.clone(), @@ -111,14 +104,14 @@ impl CommonCompleteCommandArgs { for option in &self.options { match option { - CompleteOption::BashDefault => spec.bash_default = true, - CompleteOption::Default => spec.default = true, - CompleteOption::DirNames => spec.dir_names = true, - CompleteOption::FileNames => spec.file_names = true, - CompleteOption::NoQuote => spec.no_quote = true, - CompleteOption::NoSort => spec.no_sort = true, - CompleteOption::NoSpace => spec.no_space = true, - CompleteOption::PlusDirs => spec.plus_dirs = true, + CompleteOption::BashDefault => spec.options.bash_default = true, + CompleteOption::Default => spec.options.default = true, + CompleteOption::DirNames => spec.options.dir_names = true, + CompleteOption::FileNames => spec.options.file_names = true, + CompleteOption::NoQuote => spec.options.no_quote = true, + CompleteOption::NoSort => spec.options.no_sort = true, + CompleteOption::NoSpace => spec.options.no_space = true, + CompleteOption::PlusDirs => spec.options.plus_dirs = true, } } @@ -325,28 +318,28 @@ impl CompleteCommand { s.push_str(&piece); } - if spec.bash_default { + if spec.options.bash_default { s.push_str(" -o bashdefault"); } - if spec.default { + if spec.options.default { s.push_str(" -o default"); } - if spec.dir_names { + if spec.options.dir_names { s.push_str(" -o dirnames"); } - if spec.file_names { + if spec.options.file_names { s.push_str(" -o filenames"); } - if spec.no_quote { + if spec.options.no_quote { s.push_str(" -o noquote"); } - if spec.no_sort { + if spec.options.no_sort { s.push_str(" -o nosort"); } - if spec.no_space { + if spec.options.no_space { s.push_str(" -o nospace"); } - if spec.plus_dirs { + if spec.options.plus_dirs { s.push_str(" -o plusdirs"); } @@ -443,7 +436,7 @@ impl BuiltinCommand for CompGenCommand { .await?; match result { - completion::CompletionResult::Candidates(candidates) => { + completion::CompletionResult::Candidates(mut candidates, _options) => { for candidate in candidates { writeln!(context.stdout(), "{candidate}")?; } @@ -483,21 +476,6 @@ impl BuiltinCommand for CompOptCommand { &self, context: crate::context::CommandExecutionContext<'_>, ) -> Result { - if !self.names.is_empty() { - tracing::debug!("UNIMPLEMENTED: compopt with names"); - return error::unimp("compopt with names"); - } - - let target_spec = if self.update_default { - Some(&mut context.shell.completion_config.default) - } else if self.update_empty { - Some(&mut context.shell.completion_config.empty_line) - } else if self.update_initial_word { - Some(&mut context.shell.completion_config.initial_word) - } else { - None - }; - let mut options = HashMap::new(); for option in &self.disabled_options { options.insert(option.clone(), false); @@ -506,21 +484,97 @@ impl BuiltinCommand for CompOptCommand { options.insert(option.clone(), true); } - if let Some(Some(target_spec)) = target_spec { - for (option, value) in options { - match option { - CompleteOption::BashDefault => target_spec.bash_default = value, - CompleteOption::Default => target_spec.default = value, - CompleteOption::DirNames => target_spec.dir_names = value, - CompleteOption::FileNames => target_spec.file_names = value, - CompleteOption::NoQuote => target_spec.no_quote = value, - CompleteOption::NoSort => target_spec.no_sort = value, - CompleteOption::NoSpace => target_spec.no_space = value, - CompleteOption::PlusDirs => target_spec.plus_dirs = value, + if !self.names.is_empty() { + if self.update_default || self.update_empty || self.update_initial_word { + writeln!( + context.stderr(), + "compopt: cannot specify names with -D, -E, or -I" + )?; + return Ok(builtin::BuiltinExitCode::InvalidUsage); + } + + for name in &self.names { + if let Some(spec) = context.shell.completion_config.commands.get_mut(name) { + Self::set_options_for_spec(spec, &options); + } else { + let mut spec = CompletionSpec::default(); + Self::set_options_for_spec(&mut spec, &options); + context + .shell + .completion_config + .commands + .insert(name.to_owned(), spec); } } + } else if self.update_default { + if let Some(spec) = &mut context.shell.completion_config.default { + Self::set_options_for_spec(spec, &options); + } else { + let mut spec = CompletionSpec::default(); + Self::set_options_for_spec(&mut spec, &options); + std::mem::swap( + &mut context.shell.completion_config.default, + &mut Some(spec), + ); + } + } else if self.update_empty { + if let Some(spec) = &mut context.shell.completion_config.empty_line { + Self::set_options_for_spec(spec, &options); + } else { + let mut spec = CompletionSpec::default(); + Self::set_options_for_spec(&mut spec, &options); + std::mem::swap( + &mut context.shell.completion_config.empty_line, + &mut Some(spec), + ); + } + } else if self.update_initial_word { + if let Some(spec) = &mut context.shell.completion_config.initial_word { + Self::set_options_for_spec(spec, &options); + } else { + let mut spec = CompletionSpec::default(); + Self::set_options_for_spec(&mut spec, &options); + std::mem::swap( + &mut context.shell.completion_config.initial_word, + &mut Some(spec), + ); + } + } else { + // If we got here, then we need to apply to any completion actively in-flight. + if let Some(in_flight_options) = context + .shell + .completion_config + .current_completion_options + .as_mut() + { + Self::set_options(in_flight_options, &options); + } } Ok(BuiltinExitCode::Success) } } + +impl CompOptCommand { + fn set_options_for_spec(spec: &mut CompletionSpec, options: &HashMap) { + Self::set_options(&mut spec.options, options); + } + + fn set_options( + target_options: &mut completion::CompletionOptions, + options: &HashMap, + ) { + for (option, value) in options { + match option { + CompleteOption::BashDefault => target_options.bash_default = *value, + CompleteOption::Default => target_options.default = *value, + CompleteOption::DirNames => target_options.dir_names = *value, + CompleteOption::FileNames => target_options.file_names = *value, + CompleteOption::NoQuote => target_options.no_quote = *value, + CompleteOption::NoSort => target_options.no_sort = *value, + CompleteOption::NoSpace => target_options.no_space = *value, + CompleteOption::PlusDirs => target_options.plus_dirs = *value, + } + } + } +} diff --git a/shell/src/completion.rs b/shell/src/completion.rs index b5cbaca4..ff7ae88a 100644 --- a/shell/src/completion.rs +++ b/shell/src/completion.rs @@ -86,11 +86,13 @@ pub struct CompletionConfig { pub default: Option, pub empty_line: Option, pub initial_word: Option, + + pub current_completion_options: Option, } #[allow(clippy::module_name_repetitions)] #[derive(Clone, Debug, Default)] -pub struct CompletionSpec { +pub struct CompletionOptions { // // Options // @@ -102,6 +104,15 @@ pub struct CompletionSpec { pub no_sort: bool, pub no_space: bool, pub plus_dirs: bool, +} + +#[allow(clippy::module_name_repetitions)] +#[derive(Clone, Debug, Default)] +pub struct CompletionSpec { + // + // Options + // + pub options: CompletionOptions, // // Generators @@ -153,28 +164,10 @@ impl CompletionSpec { ) -> Result { let mut candidates: Vec = vec![]; - // Apply options - if self.bash_default { - tracing::debug!("UNIMPLEMENTED: complete -o bashdefault"); - } - if self.default { - tracing::debug!("UNIMPLEMENTED: complete -o default"); - } - if self.dir_names { - tracing::debug!("UNIMPLEMENTED: complete -o dirnames"); - } - if self.file_names { - tracing::debug!("UNIMPLEMENTED: complete -o filenames"); - } - if self.no_quote { - tracing::debug!("UNIMPLEMENTED: complete -o noquote"); - } - if self.no_space { - tracing::debug!("UNIMPLEMENTED: complete -o nospace"); - } - if self.plus_dirs { - tracing::debug!("UNIMPLEMENTED: complete -o plusdirs"); - } + // Store the current options in the shell; this is needed since the compopt + // built-in has the ability of modifying the options for an in-flight + // completion process. + shell.completion_config.current_completion_options = Some(self.options.clone()); for action in &self.actions { match action { @@ -296,7 +289,7 @@ impl CompletionSpec { .await?; match call_result { CompletionResult::RestartCompletionProcess => return Ok(call_result), - CompletionResult::Candidates(mut new_candidates) => { + CompletionResult::Candidates(mut new_candidates, _options) => { candidates.append(&mut new_candidates); } } @@ -329,12 +322,47 @@ impl CompletionSpec { } } - // Sort, if desired. - if !self.no_sort { + // + // Now apply options + // + + let options = if let Some(options) = &shell.completion_config.current_completion_options { + options + } else { + &self.options + }; + + let processing_options = CandidateProcessingOptions { + treat_as_filenames: options.file_names, + no_autoquote_filenames: options.no_quote, + no_trailing_space_at_end_of_line: options.no_space, + }; + + if candidates.is_empty() { + if options.bash_default { + // TODO: if we have no completions, then fall back to default "bash" completion + tracing::debug!("UNIMPLEMENTED: complete -o bashdefault"); + } + if options.default { + // TODO: if we have no completions, then fall back to default file name completion + tracing::debug!("UNIMPLEMENTED: complete -o default"); + } + if options.dir_names { + // TODO: if we have no completions, then fall back to performing dir name completion + tracing::debug!("UNIMPLEMENTED: complete -o dirnames"); + } + } + if options.plus_dirs { + // Also add dir name completion. + tracing::debug!("UNIMPLEMENTED: complete -o plusdirs"); + } + + // Sort, unless blocked by options. + if !self.options.no_sort { candidates.sort(); } - Ok(CompletionResult::Candidates(candidates)) + Ok(CompletionResult::Candidates(candidates, processing_options)) } async fn call_completion_function( @@ -391,12 +419,16 @@ impl CompletionSpec { crate::variables::ShellValue::IndexedArray(values) => { Ok(CompletionResult::Candidates( values.values().map(|v| v.to_owned()).collect(), + CandidateProcessingOptions::default(), )) } _ => error::unimp("unexpected COMPREPLY value type"), } } else { - Ok(CompletionResult::Candidates(vec![])) + Ok(CompletionResult::Candidates( + vec![], + CandidateProcessingOptions::default(), + )) } } } @@ -406,11 +438,22 @@ impl CompletionSpec { pub struct Completions { pub start: usize, pub candidates: Vec, + pub options: CandidateProcessingOptions, +} + +#[derive(Debug, Default)] +pub struct CandidateProcessingOptions { + /// Treat completions as file names + pub treat_as_filenames: bool, + /// Don't auto-quote completions that are file names. + pub no_autoquote_filenames: bool, + /// Don't append a trailing space to completions at the end of the input line. + pub no_trailing_space_at_end_of_line: bool, } #[allow(clippy::module_name_repetitions)] pub enum CompletionResult { - Candidates(Vec), + Candidates(Vec, CandidateProcessingOptions), RestartCompletionProcess, } @@ -505,19 +548,23 @@ impl CompletionConfig { restart_count += 1; } - let candidates = match result { - CompletionResult::Candidates(candidates) => candidates, - CompletionResult::RestartCompletionProcess => vec![], - }; - - Ok(Completions { - start: insertion_point as usize, - candidates, - }) + match result { + CompletionResult::Candidates(candidates, options) => Ok(Completions { + start: insertion_point as usize, + candidates, + options, + }), + CompletionResult::RestartCompletionProcess => Ok(Completions { + start: insertion_point as usize, + candidates: vec![], + options: CandidateProcessingOptions::default(), + }), + } } else { Ok(Completions { start: position, candidates: vec![], + options: CandidateProcessingOptions::default(), }) } } @@ -567,9 +614,11 @@ impl CompletionConfig { .to_owned() .get_completions(shell, &context) .await - .unwrap_or_else(|_err| CompletionResult::Candidates(vec![])); + .unwrap_or_else(|_err| { + CompletionResult::Candidates(vec![], CandidateProcessingOptions::default()) + }); - if !matches!(&result, CompletionResult::Candidates(candidates) if candidates.is_empty()) + if !matches!(&result, CompletionResult::Candidates(candidates, _) if candidates.is_empty()) { return result; } @@ -589,16 +638,11 @@ fn get_file_completions( let path_filter = |path: &Path| !must_be_dir || path.is_dir(); // TODO: Pass through quoting. - if let Ok(mut candidates) = patterns::Pattern::from(glob).expand( + if let Ok(candidates) = patterns::Pattern::from(glob).expand( shell.working_dir.as_path(), shell.options.extended_globbing, Some(&path_filter), ) { - for candidate in &mut candidates { - if Path::new(candidate.as_str()).is_dir() { - candidate.push('/'); - } - } candidates } else { vec![] @@ -645,7 +689,7 @@ fn get_completions_using_basic_lookup( .collect(); } - CompletionResult::Candidates(candidates) + CompletionResult::Candidates(candidates, CandidateProcessingOptions::default()) } fn split_string_using_ifs>(s: S, shell: &Shell) -> Vec { diff --git a/shell/src/lib.rs b/shell/src/lib.rs index c014d57d..4ff826bf 100644 --- a/shell/src/lib.rs +++ b/shell/src/lib.rs @@ -24,7 +24,7 @@ mod traps; mod users; mod variables; -pub use completion::Completions; +pub use completion::{CandidateProcessingOptions, Completions}; pub use error::Error; pub use interp::ExecutionResult; pub use shell::{CreateOptions, Shell};