Skip to main content

shrub_core/
completion.rs

1//! Implements programmable command completion support.
2//!
3//! There are two ways in:
4//!
5//! - [`Shell::complete`] completes a line at a cursor, as interactive
6//!   completion does.
7//! - [`Spec::generate`] generates a spec's candidates for a word on its own, as
8//!   the `compgen` builtin does.
9//!
10//! Completing a line follows bash and readline:
11//!
12//! 1. The word to complete (the text candidates replace, and a completion
13//!    function's `$2`) and the words of `COMP_WORDS` are found separately, and
14//!    needn't agree: after `--opt=`, the word to complete is empty, while
15//!    `COMP_WORDS` ends with `=`.
16//! 2. The spec to use is looked up: `complete -E` if there's nothing before the
17//!    cursor (and it isn't at the start of a word), `-I` for the initial word,
18//!    else the command's, else `-D`. It's looked up again each time a
19//!    completion function asks for completion to restart, so specs it
20//!    registered are used.
21//! 3. The spec generates candidates with a completion in progress on the shell,
22//!    so `compopt` can change its options. With no spec, basic completion
23//!    completes variables, file names, and commands.
24
25use std::borrow::Cow;
26use std::collections::{BTreeSet, HashMap};
27use std::path::Path;
28
29use itertools::Itertools;
30use shrub_parser::unquote_str;
31use strum::IntoEnumIterator;
32
33use crate::sys::{self, users};
34use crate::variables::{self, ShellValue, ShellVariable};
35use crate::{
36    Shell, commands, env, error, escape, expansion, extensions, interfaces, jobs, namedoptions,
37    patterns, trace_categories, traps,
38};
39
40mod quoting;
41mod words;
42
43use words::{LineWords, find_completion_word, find_line_words};
44
45// `compgen -W` splits unquoted literal IFS characters before expanding each
46// resulting word.
47fn split_completion_word_list(
48    word_list: &str,
49    ifs: &str,
50    parser_options: &shrub_parser::ParserOptions,
51) -> Result<Vec<String>, error::Error> {
52    // Like bash, quoting left open at the end runs to the end of the list.
53    let mut word_list = word_list.to_owned();
54    quoting::close(&mut word_list);
55    let word_list = word_list.as_str();
56
57    let pieces = shrub_parser::word::parse(word_list, parser_options)?;
58    let mut words = vec![];
59    let mut current_word = String::new();
60
61    for piece in pieces {
62        let source = word_list
63            .get(piece.start_index..piece.end_index)
64            .ok_or_else(|| {
65                error::ErrorKind::InternalError(String::from(
66                    "word parser returned an invalid source span",
67                ))
68            })?;
69
70        if matches!(piece.piece, shrub_parser::word::WordPiece::Text(_)) {
71            for c in source.chars() {
72                if ifs.contains(c) {
73                    if !current_word.is_empty() {
74                        words.push(std::mem::take(&mut current_word));
75                    }
76                } else {
77                    current_word.push(c);
78                }
79            }
80        } else {
81            current_word.push_str(source);
82        }
83    }
84
85    if !current_word.is_empty() {
86        words.push(current_word);
87    }
88
89    Ok(words)
90}
91
92/// Type of action to take to generate completion candidates. Each one's name
93/// (e.g. `arrayvar`) is what `complete -A` takes.
94#[derive(
95    Clone,
96    Copy,
97    Debug,
98    Eq,
99    PartialEq,
100    strum_macros::EnumIter,
101    strum_macros::EnumMessage,
102    strum_macros::EnumString,
103    strum_macros::IntoStaticStr,
104)]
105#[strum(serialize_all = "lowercase")]
106#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
107pub enum CompleteAction {
108    /// Complete with valid aliases.
109    Alias,
110    /// Complete with names of array shell variables.
111    ArrayVar,
112    /// Complete with names of key bindings.
113    Binding,
114    /// Complete with names of shell builtins.
115    Builtin,
116    /// Complete with names of executable commands.
117    Command,
118    /// Complete with directory names.
119    Directory,
120    /// Complete with names of disabled shell builtins.
121    Disabled,
122    /// Complete with names of enabled shell builtins.
123    Enabled,
124    /// Complete with names of exported shell variables.
125    Export,
126    /// Complete with filenames.
127    File,
128    /// Complete with names of shell functions.
129    Function,
130    /// Complete with valid user groups.
131    Group,
132    /// Complete with names of valid shell help topics.
133    HelpTopic,
134    /// Complete with the system's hostname(s).
135    HostName,
136    /// Complete with the command names of shell-managed jobs.
137    Job,
138    /// Complete with valid shell keywords.
139    Keyword,
140    /// Complete with the command names of running shell-managed jobs.
141    Running,
142    /// Complete with names of system services.
143    Service,
144    /// Complete with the names of options settable via set -o.
145    SetOpt,
146    /// Complete with the names of options settable via shopt.
147    ShOpt,
148    /// Complete with the names of trappable signals.
149    Signal,
150    /// Complete with the command names of stopped shell-managed jobs.
151    Stopped,
152    /// Complete with valid usernames.
153    User,
154    /// Complete with names of shell variables.
155    Variable,
156}
157
158/// Options influencing how command completions are generated. Each one's name
159/// (e.g. `nospace`) is what `complete -o` takes; they're declared in the order
160/// bash lists them.
161#[derive(
162    Clone,
163    Copy,
164    Debug,
165    Eq,
166    Hash,
167    Ord,
168    PartialEq,
169    PartialOrd,
170    strum_macros::EnumIter,
171    strum_macros::EnumMessage,
172    strum_macros::EnumString,
173    strum_macros::IntoStaticStr,
174)]
175#[strum(serialize_all = "lowercase")]
176#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
177pub enum CompleteOption {
178    /// Perform rest of default completions if no completions are generated.
179    BashDefault,
180    /// Use default filename completion if no completions are generated.
181    Default,
182    /// Treat completions as directory names.
183    DirNames,
184    /// Treat completions as filenames.
185    FileNames,
186    /// Suppress default auto-quotation of completions.
187    NoQuote,
188    /// Do not sort completions.
189    NoSort,
190    /// Do not append a trailing space to completions at the end of the input
191    /// line.
192    NoSpace,
193    /// Also generate directory completions.
194    PlusDirs,
195}
196
197/// Encapsulates the shell's programmable command completion configuration.
198#[derive(Clone, Default)]
199#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
200pub struct Config {
201    /// The specs for completing commands' arguments, by command name.
202    commands: HashMap<String, Spec>,
203    /// The specs used in place of a command's.
204    specials: HashMap<SpecialSpec, Spec>,
205
206    /// Fallback options to use when 'default' completions are requested (not to
207    /// be confused with the 'default' completion spec, nor 'bashdefault'
208    /// completions).
209    pub fallback_options: FallbackOptions,
210}
211
212/// Options for fallback completions.
213#[derive(Clone, Debug)]
214#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
215pub struct FallbackOptions {
216    /// If true, mark directory completions with a trailing slash.
217    pub mark_directories: bool,
218    /// If true, mark symlinked directory completions with a trailing slash.
219    pub mark_symlinked_directories: bool,
220}
221
222impl Default for FallbackOptions {
223    fn default() -> Self {
224        Self {
225            mark_directories: true,
226            mark_symlinked_directories: false,
227        }
228    }
229}
230
231/// Options for generating completions: which [`CompleteOption`]s are enabled.
232#[derive(Clone, Debug, Default, Eq, PartialEq)]
233#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
234pub struct GenerationOptions {
235    enabled: BTreeSet<CompleteOption>,
236}
237
238impl FromIterator<CompleteOption> for GenerationOptions {
239    /// Returns options with just the given ones enabled.
240    fn from_iter<I: IntoIterator<Item = CompleteOption>>(options: I) -> Self {
241        Self {
242            enabled: options.into_iter().collect(),
243        }
244    }
245}
246
247impl GenerationOptions {
248    /// Returns whether `option` is enabled.
249    pub fn get(&self, option: CompleteOption) -> bool {
250        self.enabled.contains(&option)
251    }
252
253    /// Enables or disables `option`.
254    pub fn set(&mut self, option: CompleteOption, enabled: bool) {
255        if enabled {
256            self.enabled.insert(option);
257        } else {
258            self.enabled.remove(&option);
259        }
260    }
261}
262
263/// Encapsulates a command completion specification; provides policy for how to
264/// generate completions for a given input.
265#[derive(Clone, Debug, Default)]
266#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
267pub struct Spec {
268    //
269    // Options
270    /// Options to use for completion.
271    pub options: GenerationOptions,
272
273    //
274    // Generators
275    /// Actions to take to generate completions.
276    pub actions: Vec<CompleteAction>,
277    /// Optionally, a glob pattern whose expansion will be used as completions.
278    pub glob_pattern: Option<String>,
279    /// Optionally, a list of words to use as completions.
280    pub word_list: Option<String>,
281    /// Optionally, the name of a shell function to invoke to generate
282    /// completions.
283    pub function_name: Option<String>,
284    /// Optionally, the name of a command to execute to generate completions.
285    pub command: Option<String>,
286
287    //
288    // Filters
289    /// Optionally, a pattern to filter completions (`complete -X`), as given:
290    /// candidates it matches are removed, or, if it starts with a `!` (that
291    /// doesn't start an extglob pattern), those it doesn't match.
292    pub filter_pattern: Option<String>,
293
294    //
295    // Transformers
296    /// Optionally, provides a prefix to be prepended to all completion
297    /// candidates.
298    pub prefix: Option<String>,
299    /// Optionally, provides a suffix to be appended to all completion
300    /// candidates.
301    pub suffix: Option<String>,
302}
303
304/// A shell's programmable completion state.
305#[derive(Clone, Default)]
306#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
307pub(crate) struct State {
308    /// The completion specs and settings.
309    pub(crate) config: Config,
310    /// The programmable completion in progress, if any.
311    pub(crate) in_progress: Option<InProgressCompletion>,
312}
313
314/// A programmable completion whose spec is generating candidates.
315///
316/// That includes while its completion function runs. Like bash, the `compopt`
317/// builtin changes its options.
318#[derive(Clone, Debug)]
319#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
320pub(crate) struct InProgressCompletion {
321    /// The options in effect, which start as the spec's.
322    pub(crate) options: GenerationOptions,
323}
324
325/// Keeps a completion in progress on a shell until dropped, then restores the
326/// one (if any) it replaced -- even if the completion is cancelled partway
327/// through, e.g. by Ctrl-C.
328struct InProgressScope<'a, SE: extensions::ShellExtensions> {
329    shell: &'a mut Shell<SE>,
330    prev: Option<InProgressCompletion>,
331}
332
333impl<'a, SE: extensions::ShellExtensions> InProgressScope<'a, SE> {
334    const fn start(shell: &'a mut Shell<SE>, in_progress: InProgressCompletion) -> Self {
335        let prev = shell.in_progress_completion_mut().replace(in_progress);
336        Self { shell, prev }
337    }
338
339    /// Ends the completion in progress early, returning its options as
340    /// `compopt` left them, if it's still in progress.
341    fn take_options(&mut self) -> Option<GenerationOptions> {
342        self.shell
343            .in_progress_completion_mut()
344            .take()
345            .map(|in_progress| in_progress.options)
346    }
347}
348
349impl<SE: extensions::ShellExtensions> Drop for InProgressScope<'_, SE> {
350    fn drop(&mut self) {
351        *self.shell.in_progress_completion_mut() = self.prev.take();
352    }
353}
354
355/// Enters a scope on `shell` holding the `COMP_*` variables `vars` for a
356/// completion function or command (exported, if `export`), until the returned
357/// guard is dropped -- even if the completion is cancelled.
358fn enter_comp_vars_scope<SE: extensions::ShellExtensions>(
359    shell: &mut Shell<SE>,
360    vars: impl IntoIterator<Item = (&'static str, ShellValue)>,
361    export: bool,
362) -> Result<impl std::ops::DerefMut<Target = Shell<SE>> + '_, error::Error> {
363    let mut scoped = shell.enter_scope(env::EnvironmentScope::Command);
364    for (name, value) in vars {
365        let mut var = ShellVariable::new(value);
366        if export {
367            var.export();
368        }
369        scoped
370            .env_mut()
371            .add(name, var, env::EnvironmentScope::Command)?;
372    }
373    Ok(scoped)
374}
375
376/// What a spec generates candidates for.
377#[derive(Clone, Copy, Debug)]
378struct Context<'a> {
379    /// The word to generate candidates for. When completing a line, this is the
380    /// word being completed; for [`Spec::generate`], it's the word given,
381    /// dequoted.
382    word: &'a str,
383    /// The line a completion function or command is told about: the one being
384    /// completed, or for [`Spec::generate`], like bash, an empty one being
385    /// completed for `compgen`.
386    line: LineContext<'a>,
387}
388
389/// A line being completed, which a completion function or command is told about
390/// in its arguments and the `COMP_*` variables.
391#[derive(Clone, Copy, Debug)]
392struct LineContext<'a> {
393    /// The line (`COMP_LINE`).
394    input: &'a str,
395    /// The cursor's byte offset in the line (`COMP_POINT`).
396    cursor: usize,
397    /// The words of `COMP_WORDS`, and which the cursor is in (`COMP_CWORD`).
398    words: &'a LineWords<'a>,
399    /// The name of the command being completed for (`$1`): the first word or,
400    /// for the empty-line and initial-word specs, bash's names for them.
401    command_name: Option<&'a str>,
402    /// The kind of completion (`COMP_TYPE`), as bash's char code for it (e.g.
403    /// Tab for normal completion), or 0 for `compgen`.
404    comp_type: u8,
405    /// The key that triggered completion (`COMP_KEY`), or 0 for `compgen`.
406    comp_key: u8,
407}
408
409impl<'a> LineContext<'a> {
410    /// Returns the line `input`, being completed at `cursor`, whose words are
411    /// `words`.
412    fn new(input: &'a str, cursor: usize, words: &'a LineWords<'a>) -> Self {
413        Self {
414            input,
415            cursor,
416            words,
417            command_name: words.command_name(),
418            comp_type: b'\t',
419            comp_key: b'\t',
420        }
421    }
422
423    /// Returns the arguments a completion function or command gets for
424    /// completing `word`: like bash, the command name, the word, and the
425    /// word before it, each empty if there's none.
426    fn args(&self, word: &'a str) -> [&'a str; 3] {
427        [
428            self.command_name.unwrap_or(""),
429            word,
430            self.words.preceding_word().unwrap_or(""),
431        ]
432    }
433
434    /// Returns the `COMP_*` variables that describe the line to a completion
435    /// function or command, except for `COMP_WORDS` and `COMP_CWORD` (see
436    /// [`Self::comp_words_vars`]).
437    fn comp_vars(&self) -> Vec<(&'static str, ShellValue)> {
438        vec![
439            ("COMP_LINE", self.input.into()),
440            ("COMP_POINT", self.cursor.to_string().into()),
441            ("COMP_KEY", self.comp_key.to_string().into()),
442            ("COMP_TYPE", self.comp_type.to_string().into()),
443        ]
444    }
445
446    /// Returns the `COMP_WORDS` and `COMP_CWORD` variables, which only
447    /// completion functions get.
448    fn comp_words_vars(&self) -> [(&'static str, ShellValue); 2] {
449        let cword = self
450            .words
451            .cword
452            .map_or_else(|| "-1".to_owned(), |i| i.to_string());
453        [
454            ("COMP_WORDS", self.words.comp_words.clone().into()),
455            ("COMP_CWORD", cword.into()),
456        ]
457    }
458}
459
460/// Candidates a spec generated (`T`), or else a completion function's request,
461/// by returning 124, that completion restart.
462enum Generated<T> {
463    /// The candidates generated.
464    Candidates(T),
465    /// A request that completion restart.
466    Restart,
467}
468
469/// The candidates a spec's generators produced.
470struct SpecCandidates {
471    /// The candidates.
472    candidates: Vec<String>,
473    /// Whether generating them makes them file names: like bash, if the spec's
474    /// `file` action ran, or its `directory` action found any (see
475    /// [`CompleteOption::FileNames`]).
476    file_names: bool,
477}
478
479impl Spec {
480    /// Generates this spec's completion candidates for `word`, a word on its
481    /// own (not part of a line being completed), as the `compgen` builtin
482    /// does: with any fallbacks its options ask for added, and like bash,
483    /// in the order generated (sorting is for showing them to a user). A
484    /// completion function or command run for `word` is told, like bash's,
485    /// about an empty line being completed for `compgen`.
486    ///
487    /// This doesn't start a completion in progress, so the `compopt` builtin
488    /// can't change the options applied to the candidates. Like bash, a
489    /// completion function that asks for completion to restart generates no
490    /// candidates.
491    ///
492    /// # Arguments
493    ///
494    /// * `shell` - The shell instance to use for completion generation.
495    /// * `word` - The word to generate candidates for, as typed (with any
496    ///   quoting).
497    pub async fn generate(
498        &self,
499        shell: &mut Shell<impl extensions::ShellExtensions>,
500        word: &str,
501    ) -> Result<Vec<String>, crate::error::Error> {
502        let word = unquote_str(word);
503        let context = Context {
504            word: &word,
505            line: LineContext {
506                input: "",
507                cursor: 0,
508                words: &words::NO_WORDS,
509                command_name: Some("compgen"),
510                comp_type: 0,
511                comp_key: 0,
512            },
513        };
514
515        let Generated::Candidates(SpecCandidates { candidates, .. }) =
516            self.generate_candidates(shell, &context).await?
517        else {
518            return Ok(Vec::new());
519        };
520
521        let (candidates, _options) = self
522            .apply_options(shell, &context, candidates, self.options.clone())
523            .await;
524        Ok(candidates)
525    }
526
527    /// Completes the word being completed in `context`'s line, with a
528    /// completion in progress for it (see [`InProgressCompletion`]) while
529    /// its candidates are generated, so the `compopt` builtin can change
530    /// the options applied to them. Returns the candidates, sorted unless
531    /// `nosort`, and how to process them.
532    async fn complete(
533        &self,
534        shell: &mut Shell<impl extensions::ShellExtensions>,
535        context: &Context<'_>,
536    ) -> Result<Generated<(Vec<String>, ProcessingOptions)>, crate::error::Error> {
537        let mut scope = InProgressScope::start(
538            shell,
539            InProgressCompletion {
540                options: self.options.clone(),
541            },
542        );
543
544        let Generated::Candidates(SpecCandidates {
545            candidates,
546            file_names,
547        }) = self.generate_candidates(scope.shell, context).await?
548        else {
549            return Ok(Generated::Restart);
550        };
551
552        // Apply the options as `compopt` left them, and as generating the
553        // candidates did.
554        let mut options = scope.take_options().unwrap_or_else(|| self.options.clone());
555        if file_names {
556            options.set(CompleteOption::FileNames, true);
557        }
558        let sort = !options.get(CompleteOption::NoSort);
559        let (mut candidates, processing_options) = self
560            .apply_options(scope.shell, context, candidates, options)
561            .await;
562        if sort {
563            candidates.sort();
564        }
565        Ok(Generated::Candidates((candidates, processing_options)))
566    }
567
568    /// Generates this spec's candidates, before its options are applied.
569    async fn generate_candidates(
570        &self,
571        shell: &mut Shell<impl extensions::ShellExtensions>,
572        context: &Context<'_>,
573    ) -> Result<Generated<SpecCandidates>, crate::error::Error> {
574        // Generate completions based on any provided actions (and on words).
575        let SpecCandidates {
576            mut candidates,
577            file_names,
578        } = self.generate_action_completions(shell, context).await?;
579        if let Some(word_list) = &self.word_list {
580            let params = shell.default_exec_params();
581            let unexpanded_words =
582                split_completion_word_list(word_list, &shell.ifs(), &shell.parser_options())?;
583            let options = crate::expansion::ExpanderOptions {
584                pathname_expand: false,
585                ..Default::default()
586            };
587            let mut words = vec![];
588            for word in unexpanded_words {
589                words.extend(
590                    crate::expansion::full_expand_and_split_word_with_options(
591                        shell, &params, word, &options,
592                    )
593                    .await?,
594                );
595            }
596            candidates.extend(
597                words
598                    .into_iter()
599                    .filter(|word| word.starts_with(context.word)),
600            );
601        }
602
603        if let Some(glob_pattern) = &self.glob_pattern {
604            let pattern = shell_pattern(shell, glob_pattern.as_str());
605
606            let expansions = pattern
607                .expand(
608                    shell.working_dir(),
609                    Some(&patterns::Pattern::accept_all_expand_filter),
610                    &patterns::FilenameExpansionOptions::default(),
611                )?
612                .into_paths();
613
614            candidates.extend(expansions);
615        }
616        if let Some(function_name) = &self.function_name {
617            match call_completion_function(shell, function_name, context).await? {
618                Generated::Candidates(new_candidates) => candidates.extend(new_candidates),
619                Generated::Restart => return Ok(Generated::Restart),
620            }
621        }
622        if let Some(command) = &self.command {
623            candidates.extend(call_completion_command(shell, command, context).await?);
624        }
625
626        // Apply filter pattern, if present. Anything the filter selects gets
627        // removed.
628        if let Some(filter_pattern) = &self.filter_pattern
629            && !filter_pattern.is_empty()
630        {
631            let mut updated = Vec::new();
632
633            // Like bash, a leading `!` (unless extglob is on and it starts a
634            // `!(...)` pattern) inverts the filter, which then
635            // keeps what it matches.
636            let (pattern, keep_matches) = match filter_pattern.strip_prefix('!') {
637                Some(rest) if !(shell.options().extended_globbing && rest.starts_with('(')) => {
638                    (rest, true)
639                }
640                _ => (filter_pattern.as_str(), false),
641            };
642
643            for candidate in candidates {
644                let matches = completion_filter_pattern_matches(
645                    pattern,
646                    candidate.as_str(),
647                    context.word,
648                    shell,
649                )?;
650
651                if matches == keep_matches {
652                    updated.push(candidate);
653                }
654            }
655
656            candidates = updated;
657        }
658
659        // Add prefix and/or suffix, if present.
660        if self.prefix.is_some() || self.suffix.is_some() {
661            let empty = String::new();
662            let prefix = self.prefix.as_ref().unwrap_or(&empty);
663            let suffix = self.suffix.as_ref().unwrap_or(&empty);
664
665            let mut updated = Vec::with_capacity(candidates.len() * (prefix.len() + suffix.len()));
666            for candidate in candidates {
667                updated.push(std::format!("{prefix}{candidate}{suffix}"));
668            }
669
670            candidates = updated;
671        }
672
673        Ok(Generated::Candidates(SpecCandidates {
674            candidates,
675            file_names,
676        }))
677    }
678
679    /// Applies `options` to the candidates this spec generated, adding any
680    /// fallbacks they ask for; returns them and how to process them.
681    async fn apply_options(
682        &self,
683        shell: &Shell<impl extensions::ShellExtensions>,
684        context: &Context<'_>,
685        mut candidates: Vec<String>,
686        options: GenerationOptions,
687    ) -> (Vec<String>, ProcessingOptions) {
688        let mut processing_options = ProcessingOptions {
689            treat_as_filenames: options.get(CompleteOption::FileNames),
690            no_autoquote_filenames: options.get(CompleteOption::NoQuote),
691            no_trailing_space_at_end_of_line: options.get(CompleteOption::NoSpace),
692        };
693
694        // plusdirs always adds directory names; dirnames only does so when
695        // nothing else matched.
696        if options.get(CompleteOption::PlusDirs)
697            || (options.get(CompleteOption::DirNames) && candidates.is_empty())
698        {
699            let mut dir_candidates =
700                get_file_completions(shell, context.word, /* must_be_dir */ true).await;
701
702            // If directories are all we have, let them be marked as such.
703            if candidates.is_empty() && shell.completion_config().fallback_options.mark_directories
704            {
705                processing_options.treat_as_filenames = true;
706            }
707
708            candidates.append(&mut dir_candidates);
709        }
710
711        // If we still have no candidates, and bashdefault completions were
712        // requested, then generate those.
713        if candidates.is_empty() && options.get(CompleteOption::BashDefault) {
714            // TODO(completions): it's not clear what default "bash" completions
715            // means. From basic testing, this doesn't seem to
716            // include basic file and directory name completion.
717            tracing::debug!(target: trace_categories::COMPLETION, "unimplemented: complete -o bashdefault");
718        }
719
720        // If we still have no candidates, and default completions were
721        // requested, then generate those.
722        if candidates.is_empty() && options.get(CompleteOption::Default) {
723            // N.B. We approximate "default" readline completion behavior by
724            // getting file and dir completions.
725            let must_be_dir = options.get(CompleteOption::DirNames);
726
727            let mut default_candidates =
728                get_file_completions(shell, context.word, must_be_dir).await;
729            candidates.append(&mut default_candidates);
730
731            if shell.completion_config().fallback_options.mark_directories {
732                processing_options.treat_as_filenames = true;
733            }
734        }
735
736        (candidates, processing_options)
737    }
738
739    /// Generates the candidates of this spec's actions.
740    #[expect(clippy::too_many_lines, reason = "a flat match over the actions")]
741    async fn generate_action_completions(
742        &self,
743        shell: &Shell<impl extensions::ShellExtensions>,
744        context: &Context<'_>,
745    ) -> Result<SpecCandidates, error::Error> {
746        let prefix = context.word;
747        let mut candidates = Vec::new();
748        let mut file_names = false;
749
750        for action in &self.actions {
751            let c = &mut candidates;
752            match action {
753                // Aliases and functions are stored unordered; bash enumerates them sorted
754                // by name.
755                CompleteAction::Alias => {
756                    extend_matching(c, shell.aliases().keys().sorted(), prefix);
757                }
758                CompleteAction::ArrayVar => extend_matching(
759                    c,
760                    shell
761                        .env()
762                        .iter()
763                        .filter(|(_, var)| var.value().is_array())
764                        .map(|(name, _)| name),
765                    prefix,
766                ),
767                CompleteAction::Binding => extend_matching(
768                    c,
769                    interfaces::InputFunction::iter().map(<&'static str>::from),
770                    prefix,
771                ),
772                // For now, we only have help topics for built-in commands.
773                CompleteAction::Builtin | CompleteAction::HelpTopic => {
774                    extend_matching(c, shell.builtins().keys(), prefix);
775                }
776                CompleteAction::Command => c.extend(command_completions(shell, prefix)),
777                CompleteAction::Directory => {
778                    let dirs = get_file_completions(shell, context.word, true).await;
779                    file_names |= !dirs.is_empty();
780                    c.extend(dirs);
781                }
782                CompleteAction::Disabled => extend_matching(
783                    c,
784                    shell
785                        .builtins()
786                        .iter()
787                        .filter(|(_, registration)| registration.disabled)
788                        .map(|(name, _)| name),
789                    prefix,
790                ),
791                CompleteAction::Enabled => extend_matching(
792                    c,
793                    shell
794                        .builtins()
795                        .iter()
796                        .filter(|(_, registration)| !registration.disabled)
797                        .map(|(name, _)| name),
798                    prefix,
799                ),
800                CompleteAction::Export => extend_matching(
801                    c,
802                    shell
803                        .env()
804                        .iter()
805                        .filter(|(_, var)| var.is_exported())
806                        .map(|(name, _)| name),
807                    prefix,
808                ),
809                CompleteAction::File => {
810                    file_names = true;
811                    c.extend(get_file_completions(shell, context.word, false).await);
812                }
813                CompleteAction::Function => extend_matching(
814                    c,
815                    shell.funcs().iter().map(|(name, _)| name).sorted(),
816                    prefix,
817                ),
818                CompleteAction::Group => extend_matching(c, users::get_all_groups()?, prefix),
819                // N.B. We only retrieve one hostname.
820                CompleteAction::HostName => extend_matching(
821                    c,
822                    sys::network::get_hostname()
823                        .ok()
824                        .map(|name| name.to_string_lossy().into_owned()),
825                    prefix,
826                ),
827                CompleteAction::Job => extend_matching(
828                    c,
829                    shell.jobs().jobs.iter().map(|job| job.command_name()),
830                    prefix,
831                ),
832                CompleteAction::Keyword => extend_matching(c, shell.get_keywords(), prefix),
833                CompleteAction::Running => extend_matching(
834                    c,
835                    shell
836                        .jobs()
837                        .jobs
838                        .iter()
839                        .filter(|job| matches!(job.state, jobs::JobState::Running))
840                        .map(|job| job.command_name()),
841                    prefix,
842                ),
843                CompleteAction::Service => {
844                    tracing::debug!(target: trace_categories::COMPLETION, "unimplemented: complete -A service");
845                }
846                CompleteAction::SetOpt => extend_matching(
847                    c,
848                    namedoptions::options(namedoptions::ShellOptionKind::SetO)
849                        .iter()
850                        .map(|option| option.name),
851                    prefix,
852                ),
853                CompleteAction::ShOpt => extend_matching(
854                    c,
855                    namedoptions::options(namedoptions::ShellOptionKind::Shopt)
856                        .iter()
857                        .map(|option| option.name),
858                    prefix,
859                ),
860                CompleteAction::Signal => extend_matching(
861                    c,
862                    traps::TrapSignal::iterator().map(traps::TrapSignal::as_str),
863                    prefix,
864                ),
865                CompleteAction::Stopped => extend_matching(
866                    c,
867                    shell
868                        .jobs()
869                        .jobs
870                        .iter()
871                        .filter(|job| matches!(job.state, jobs::JobState::Stopped))
872                        .map(|job| job.command_name()),
873                    prefix,
874                ),
875                CompleteAction::User => extend_matching(c, users::get_all_users()?, prefix),
876                CompleteAction::Variable => {
877                    extend_matching(c, shell.env().iter().map(|(name, _)| name), prefix);
878                }
879            }
880        }
881
882        Ok(SpecCandidates {
883            candidates,
884            file_names,
885        })
886    }
887}
888
889/// Runs the completion command `command` (`complete -C`) to generate candidates
890/// for the word `context` is generating them for: one per line of its output.
891async fn call_completion_command(
892    shell: &mut Shell<impl extensions::ShellExtensions>,
893    command: &str,
894    context: &Context<'_>,
895) -> Result<Vec<String>, error::Error> {
896    let line = context.line;
897
898    // Compose the full command line.
899    let mut command_line = command.to_owned();
900    for arg in line.args(context.word) {
901        command_line.push(' ');
902        command_line.push_str(&escape::quote_if_needed(
903            arg,
904            escape::QuoteMode::SingleQuote,
905        ));
906    }
907
908    // Run the command in a subshell, with the variables exported to it and
909    // traps blocked. Like a command substitution, it would set `$?`; the
910    // user didn't run it, so leave that as it was.
911    let status = shell.save_command_status();
912    let mut scoped = enter_comp_vars_scope(shell, line.comp_vars(), true)?;
913    let mut blocked = scoped.block_trap_delivery();
914    let params = blocked.default_exec_params();
915    let output =
916        commands::invoke_command_in_subshell_and_get_output(&mut blocked, &params, command_line)
917            .await;
918    drop(blocked);
919    drop(scoped);
920    shell.restore_command_status(status);
921
922    Ok(output?.lines().map(str::to_owned).collect())
923}
924
925/// Runs the completion function `function_name` (`complete -F`) to generate
926/// candidates for the word `context` is generating them for: those it leaves in
927/// `COMPREPLY`, unless it asks for completion to restart.
928async fn call_completion_function(
929    shell: &mut Shell<impl extensions::ShellExtensions>,
930    function_name: &str,
931    context: &Context<'_>,
932) -> Result<Generated<Vec<String>>, error::Error> {
933    let line = context.line;
934    let mut vars = line.comp_vars();
935    vars.extend(line.comp_words_vars());
936
937    tracing::debug!(target: trace_categories::COMPLETION, "[calling completion func '{function_name}']: {}",
938        vars.iter().map(|(k, v)| std::format!("{k}={v:?}")).collect::<Vec<String>>().join(" "));
939
940    // Set the variables in a scope of their own, left when the function returns
941    // -- or when the completion is cancelled -- and block traps while it
942    // runs.
943    let names: Vec<_> = vars.iter().map(|(name, _)| *name).collect();
944    let mut scoped = enter_comp_vars_scope(shell, vars, false)?;
945    let mut blocked = scoped.block_trap_delivery();
946    let params = blocked.default_exec_params();
947    let invoke_result = blocked
948        .invoke_function(function_name, line.args(context.word).iter(), params)
949        .await
950        .map(|result| u8::from(result.exit_code));
951
952    // The function's done: unblock traps, and unset the variables.
953    drop(blocked);
954    drop(scoped);
955
956    // Like bash, also unset any the function set again after unsetting them, or
957    // that were set before it ran: either way, they're outside the scope
958    // just left.
959    for name in names {
960        let _ = shell.env_mut().unset(name);
961    }
962
963    tracing::debug!(target: trace_categories::COMPLETION, "[completion function '{function_name}' returned: {invoke_result:?}]");
964
965    let result = invoke_result.unwrap_or_else(|e| {
966        tracing::warn!(target: trace_categories::COMPLETION, "error while running completion function '{function_name}': {e}");
967        1 // Report back a non-zero exit code.
968    });
969
970    // Like bash, take `COMPREPLY` out of the shell even if it's discarded
971    // below.
972    let reply = shell.env_mut().unset("COMPREPLY")?;
973
974    // When the function returns the special value 124, then it's a request
975    // for us to restart the completion process.
976    if result == 124 {
977        return Ok(Generated::Restart);
978    }
979
980    tracing::debug!(target: trace_categories::COMPLETION, "[completion function yielded: {reply:?}]");
981
982    let candidates = match reply.as_ref().map(|reply| reply.value()) {
983        Some(variables::ShellValue::IndexedArray(values)) => {
984            values.values().map(|v| v.to_owned()).collect()
985        }
986        Some(variables::ShellValue::String(s)) => vec![s.to_owned()],
987        _ => Vec::new(),
988    };
989    Ok(Generated::Candidates(candidates))
990}
991
992/// Represents a set of generated command completions.
993#[derive(Debug, Default)]
994pub struct Completions {
995    /// The index in the input line where the completions should be inserted.
996    /// Represented as a byte offset into the input line; must be at a clean
997    /// character boundary.
998    pub insertion_index: usize,
999    /// The number of elements in the input line that should be removed before
1000    /// insertion. Represented as a byte count; must capture an exact
1001    /// character boundary.
1002    pub delete_count: usize,
1003    /// The ordered set of completions.
1004    pub candidates: Vec<String>,
1005    /// Options for processing the candidates.
1006    pub options: ProcessingOptions,
1007}
1008
1009/// Options governing how command completion candidates are processed after
1010/// being generated.
1011#[derive(Debug)]
1012pub struct ProcessingOptions {
1013    /// Treat completions as file names.
1014    pub treat_as_filenames: bool,
1015    /// Don't auto-quote completions that are file names.
1016    pub no_autoquote_filenames: bool,
1017    /// Don't append a trailing space to completions at the end of the input
1018    /// line.
1019    pub no_trailing_space_at_end_of_line: bool,
1020}
1021
1022impl Default for ProcessingOptions {
1023    fn default() -> Self {
1024        Self {
1025            treat_as_filenames: true,
1026            no_autoquote_filenames: false,
1027            no_trailing_space_at_end_of_line: false,
1028        }
1029    }
1030}
1031
1032/// A completion spec used in place of a command's.
1033#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, strum_macros::EnumIter)]
1034#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1035pub enum SpecialSpec {
1036    /// The spec used when no command's spec applies (`complete -D`).
1037    Default,
1038    /// The spec used when the command line is empty (`complete -E`).
1039    EmptyLine,
1040    /// The spec used for the initial word of a command line (`complete -I`).
1041    InitialWord,
1042}
1043
1044impl SpecialSpec {
1045    /// Returns the name that stands in for a command's with this spec, as
1046    /// bash's does: the `complete` and `compopt` builtins accept it in
1047    /// place of a command name, and a completion function for the
1048    /// empty-line or initial-word spec gets it as `$1`.
1049    pub const fn command_name(self) -> &'static str {
1050        match self {
1051            Self::Default => "_DefaultCmD_",
1052            Self::EmptyLine => "_EmptycmD_",
1053            Self::InitialWord => "_InitialWorD_",
1054        }
1055    }
1056
1057    /// Returns the special spec that `name`, bash's name for it, names, if any.
1058    pub fn from_command_name(name: &str) -> Option<Self> {
1059        Self::iter().find(|special| special.command_name() == name)
1060    }
1061}
1062
1063/// Names a completion spec in a [`Config`].
1064#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
1065pub enum SpecName<'a> {
1066    /// The spec for completing the named command's arguments.
1067    Command(&'a str),
1068    /// One of the special specs.
1069    Special(SpecialSpec),
1070}
1071
1072impl<'a> SpecName<'a> {
1073    /// Returns the spec named `name`: a command's, or the special spec that
1074    /// bash's name for it (see [`SpecialSpec::command_name`]) names.
1075    pub fn parse(name: &'a str) -> Self {
1076        SpecialSpec::from_command_name(name).map_or(Self::Command(name), Self::Special)
1077    }
1078
1079    /// Returns the name: a command's, or for a special spec, bash's name for
1080    /// it.
1081    pub const fn as_str(self) -> &'a str {
1082        match self {
1083            Self::Command(command) => command,
1084            Self::Special(special) => special.command_name(),
1085        }
1086    }
1087}
1088
1089impl Config {
1090    /// Removes all registered completion specs.
1091    pub fn clear(&mut self) {
1092        self.commands.clear();
1093        self.specials.clear();
1094    }
1095
1096    /// Ensures the named completion spec is no longer registered; returns
1097    /// whether a removal operation was required.
1098    ///
1099    /// # Arguments
1100    ///
1101    /// * `name` - The name of the completion spec to remove.
1102    pub fn remove(&mut self, name: SpecName<'_>) -> bool {
1103        match name {
1104            SpecName::Command(command) => self.commands.remove(command).is_some(),
1105            SpecName::Special(special) => self.specials.remove(&special).is_some(),
1106        }
1107    }
1108
1109    /// Returns an iterator over the completion specs and their names, in no
1110    /// particular order.
1111    pub fn iter(&self) -> impl Iterator<Item = (SpecName<'_>, &Spec)> {
1112        let commands = self
1113            .commands
1114            .iter()
1115            .map(|(command, spec)| (SpecName::Command(command), spec));
1116        let specials = self
1117            .specials
1118            .iter()
1119            .map(|(special, spec)| (SpecName::Special(*special), spec));
1120        commands.chain(specials)
1121    }
1122
1123    /// If present, returns the named completion spec.
1124    ///
1125    /// # Arguments
1126    ///
1127    /// * `name` - The name of the completion spec.
1128    pub fn get(&self, name: SpecName<'_>) -> Option<&Spec> {
1129        match name {
1130            SpecName::Command(command) => self.commands.get(command),
1131            SpecName::Special(special) => self.specials.get(&special),
1132        }
1133    }
1134
1135    /// If present, returns a mutable reference to the named completion spec.
1136    ///
1137    /// # Arguments
1138    ///
1139    /// * `name` - The name of the completion spec.
1140    pub fn get_mut(&mut self, name: SpecName<'_>) -> Option<&mut Spec> {
1141        match name {
1142            SpecName::Command(command) => self.commands.get_mut(command),
1143            SpecName::Special(special) => self.specials.get_mut(&special),
1144        }
1145    }
1146
1147    /// Registers the provided completion spec under the given name, replacing
1148    /// any already registered there.
1149    ///
1150    /// # Arguments
1151    ///
1152    /// * `name` - The name of the completion spec.
1153    /// * `spec` - The completion spec.
1154    pub fn set(&mut self, name: SpecName<'_>, spec: Spec) {
1155        match name {
1156            SpecName::Command(command) => {
1157                self.commands.insert(command.to_owned(), spec);
1158            }
1159            SpecName::Special(special) => {
1160                self.specials.insert(special, spec);
1161            }
1162        }
1163    }
1164
1165    /// Returns the completion spec to use for completing `line`, and the name
1166    /// of the command it completes for (a completion function's `$1`): the
1167    /// empty-line spec (`complete -E`) if there's nothing before the cursor
1168    /// and it isn't at the start of a word, else the spec for the initial
1169    /// word (`complete -I`) if completing that, else its command's spec (by
1170    /// name, or by file name if the command is a path), else the default spec
1171    /// (`complete -D`). Like bash, the empty-line and initial-word specs
1172    /// complete for commands named after them.
1173    fn find_spec<'l>(&self, line: &LineContext<'l>) -> Option<(&Spec, Option<&'l str>)> {
1174        let special = |special: SpecialSpec| {
1175            let spec = self.specials.get(&special)?;
1176            Some((spec, Some(special.command_name())))
1177        };
1178
1179        // Like bash, it's the empty line only if the cursor is at its very
1180        // start, and not at the start of a word there: with whitespace
1181        // before the cursor, or a word just after it, the initial word
1182        // is completed.
1183        if line.cursor == 0 && line.input.chars().next().is_none_or(char::is_whitespace) {
1184            return special(SpecialSpec::EmptyLine);
1185        }
1186
1187        if matches!(line.words.cword, None | Some(0)) {
1188            return special(SpecialSpec::InitialWord);
1189        }
1190
1191        let command_name = line.command_name?;
1192        let spec = self
1193            .commands
1194            .get(command_name)
1195            .or_else(|| {
1196                let file_name = Path::new(command_name).file_name()?;
1197                self.commands.get(file_name.to_string_lossy().as_ref())
1198            })
1199            .or_else(|| self.specials.get(&SpecialSpec::Default))?;
1200        Some((spec, Some(command_name)))
1201    }
1202}
1203
1204/// Completes `input` at `cursor`: see [`Shell::complete`].
1205pub(crate) async fn complete(
1206    shell: &mut Shell<impl extensions::ShellExtensions>,
1207    input: &str,
1208    cursor: usize,
1209) -> Result<Completions, error::Error> {
1210    /// How many times a completion function may ask for completion to restart.
1211    const MAX_RESTARTS: u32 = 10;
1212
1213    if !input.is_char_boundary(cursor) {
1214        return Err(error::ErrorKind::InvalidCompletionPosition(cursor).into());
1215    }
1216
1217    let word_breaks = word_break_chars(shell);
1218    let word = find_completion_word(input, &word_breaks, cursor);
1219    let line_words = find_line_words(input, &word_breaks, cursor);
1220    let context = Context {
1221        word: &word.text,
1222        line: LineContext::new(input, cursor, &line_words),
1223    };
1224
1225    // Complete, restarting as often as completion functions ask to. If they
1226    // never stop asking, there's nothing to complete with.
1227    let mut completed = None;
1228    for _ in 0..=MAX_RESTARTS {
1229        if let Generated::Candidates(candidates_and_options) = complete_word(shell, context).await {
1230            completed = Some(candidates_and_options);
1231            break;
1232        }
1233    }
1234    let (candidates, options) = completed.unwrap_or_else(|| {
1235        tracing::warn!(target: trace_categories::COMPLETION, "completion kept restarting; giving up");
1236        (Vec::new(), ProcessingOptions::default())
1237    });
1238
1239    Ok(Completions {
1240        insertion_index: word.range.start,
1241        delete_count: word.range.len(),
1242        candidates,
1243        options,
1244    })
1245}
1246
1247fn word_break_chars(shell: &Shell<impl extensions::ShellExtensions>) -> Vec<char> {
1248    const FALLBACK: &str = " \t\n\"\'@><=;|&(:";
1249
1250    shell
1251        .env_str("COMP_WORDBREAKS")
1252        .unwrap_or_else(|| FALLBACK.into())
1253        .chars()
1254        .collect()
1255}
1256
1257/// Completes the word being completed in `context`'s line, with the completion
1258/// spec that applies to it if there is one, or else with basic completion.
1259/// Returns the candidates and how to process them.
1260async fn complete_word(
1261    shell: &mut Shell<impl extensions::ShellExtensions>,
1262    mut context: Context<'_>,
1263) -> Generated<(Vec<String>, ProcessingOptions)> {
1264    // Look the spec up afresh each time: a completion function that asks for
1265    // completion to restart may have registered a new one.
1266    let Some((spec, command_name)) = shell.completion_config().find_spec(&context.line) else {
1267        return Generated::Candidates(get_completions_using_basic_lookup(shell, &context).await);
1268    };
1269    let spec = spec.clone();
1270    context.line.command_name = command_name;
1271
1272    spec.complete(shell, &context).await.unwrap_or_else(|err| {
1273        tracing::debug!(target: trace_categories::COMPLETION, "completion spec failed: {err}");
1274        Generated::Candidates((Vec::new(), ProcessingOptions::default()))
1275    })
1276}
1277
1278async fn get_file_completions(
1279    shell: &Shell<impl extensions::ShellExtensions>,
1280    token_to_complete: &str,
1281    must_be_dir: bool,
1282) -> Vec<String> {
1283    // Basic-expand the token-to-be-completed; it won't have been expanded to
1284    // this point.
1285    let mut throwaway_shell = shell.clone();
1286    let params = throwaway_shell.default_exec_params();
1287    let options = expansion::ExpanderOptions {
1288        execute_command_substitutions: false,
1289        ..Default::default()
1290    };
1291    let expanded_token = expansion::basic_expand_word_with_options(
1292        &mut throwaway_shell,
1293        &params,
1294        &unquote_str(token_to_complete),
1295        &options,
1296    )
1297    .await
1298    .unwrap_or_else(|_err| token_to_complete.to_owned());
1299
1300    // Normalize path separators before building the glob pattern, because
1301    // backslash is the escape character in glob syntax and must not be
1302    // confused with a Windows path separator.
1303    let expanded_token = sys::fs::normalize_path_separators(&expanded_token).into_owned();
1304
1305    let glob = std::format!("{expanded_token}*");
1306
1307    let path_filter = |path: &Path| !must_be_dir || shell.absolute_path(path).is_dir();
1308
1309    let pattern = shell_pattern(shell, glob);
1310
1311    let mut completions: Vec<String> = pattern
1312        .expand(
1313            shell.working_dir(),
1314            Some(&path_filter),
1315            &patterns::FilenameExpansionOptions::default(),
1316        )
1317        .unwrap_or_default()
1318        .into_paths()
1319        .into_iter()
1320        .map(|p| match sys::fs::normalize_path_separators(&p) {
1321            std::borrow::Cow::Borrowed(_) => p,
1322            std::borrow::Cow::Owned(normalized) => normalized,
1323        })
1324        .collect();
1325
1326    match expanded_token.as_str() {
1327        "." => {
1328            completions.push(".".into());
1329            completions.push("..".into());
1330        }
1331        ".." => {
1332            completions.push("..".into());
1333        }
1334        _ => {}
1335    }
1336
1337    completions.sort();
1338    completions.dedup();
1339    completions
1340}
1341
1342/// Attempts to complete a variable name from the given token.
1343/// Returns the candidates and how to process them if the token looks like a
1344/// variable reference being typed, or `None` if file/command completion should
1345/// be used instead.
1346///
1347/// # Arguments
1348///
1349/// * `shell` - The shell instance to use for variable lookup.
1350/// * `token` - The token being completed. May be empty.
1351fn try_get_variable_completions(
1352    shell: &Shell<impl extensions::ShellExtensions>,
1353    token: &str,
1354) -> Option<(Vec<String>, ProcessingOptions)> {
1355    // Determine if this is a braced or unbraced variable reference
1356    let (var_prefix, use_braces) = if let Some(prefix) = token.strip_prefix("${") {
1357        // For braced: only complete if brace isn't closed yet
1358        if prefix.contains('}') {
1359            return None;
1360        }
1361        (prefix, true)
1362    } else {
1363        let prefix = token.strip_prefix('$')?;
1364        (prefix, false)
1365    };
1366
1367    // If there's a path separator, this is a path like $HOME/foo, not a
1368    // variable to complete
1369    if sys::fs::contains_path_separator(var_prefix) {
1370        return None;
1371    }
1372
1373    // Find matching variables
1374    let mut candidates: Vec<String> = shell
1375        .env()
1376        .iter()
1377        .filter(|(key, _)| key.starts_with(var_prefix))
1378        .map(|(key, _)| {
1379            if use_braces {
1380                format!("${{{key}}}")
1381            } else {
1382                format!("${key}")
1383            }
1384        })
1385        .collect();
1386    candidates.sort();
1387
1388    // Variable completions should not be treated as filenames (no escaping
1389    // needed)
1390    let options = ProcessingOptions {
1391        treat_as_filenames: false,
1392        ..ProcessingOptions::default()
1393    };
1394
1395    Some((candidates, options))
1396}
1397
1398/// Returns the names of the commands that start with `prefix`, in the order
1399/// bash lists them: aliases, keywords, functions, enabled builtins, then
1400/// executables in the path.
1401fn command_completions(
1402    shell: &Shell<impl extensions::ShellExtensions>,
1403    prefix: &str,
1404) -> Vec<String> {
1405    let mut names = Vec::new();
1406
1407    // Aliases, functions, and builtins are stored unordered; bash enumerates
1408    // each sorted by name.
1409    extend_matching(&mut names, shell.aliases().keys().sorted(), prefix);
1410    extend_matching(&mut names, shell.get_keywords(), prefix);
1411    extend_matching(
1412        &mut names,
1413        shell.funcs().iter().map(|(name, _)| name).sorted(),
1414        prefix,
1415    );
1416    extend_matching(
1417        &mut names,
1418        shell
1419            .builtins()
1420            .iter()
1421            .filter(|(_, registration)| !registration.disabled)
1422            .map(|(name, _)| name)
1423            .sorted(),
1424        prefix,
1425    );
1426    names.extend(
1427        shell
1428            .find_executables_in_path_with_prefix(
1429                prefix,
1430                shell.options().case_insensitive_pathname_expansion,
1431            )
1432            .filter_map(|path| Some(path.file_name()?.to_string_lossy().into_owned())),
1433    );
1434
1435    names
1436}
1437
1438/// Adds the `names` that start with `prefix` to `candidates`.
1439fn extend_matching<S: AsRef<str>>(
1440    candidates: &mut Vec<String>,
1441    names: impl IntoIterator<Item = S>,
1442    prefix: &str,
1443) {
1444    candidates.extend(
1445        names
1446            .into_iter()
1447            .filter(|name| name.as_ref().starts_with(prefix))
1448            .map(|name| name.as_ref().to_owned()),
1449    );
1450}
1451
1452async fn get_completions_using_basic_lookup(
1453    shell: &Shell<impl extensions::ShellExtensions>,
1454    context: &Context<'_>,
1455) -> (Vec<String>, ProcessingOptions) {
1456    let token = context.word;
1457
1458    // Try variable completion first (e.g., $HO -> $HOME, ${HO -> ${HOME})
1459    if let Some(answer) = try_get_variable_completions(shell, token) {
1460        return answer;
1461    }
1462
1463    // File completions
1464    let mut candidates = get_file_completions(shell, token, false).await;
1465
1466    // If this appears to be the command token (and if there's *some* prefix
1467    // without a path separator) then also consider whether we should search
1468    // the path for completions too.
1469    // TODO(completions): Do a better job than just checking if index == 0.
1470    let is_command_position = context.line.words.cword == Some(0)
1471        && !token.is_empty()
1472        && !sys::fs::contains_path_separator(token);
1473
1474    if is_command_position {
1475        candidates.extend(command_completions(shell, token));
1476        candidates.sort();
1477    }
1478
1479    (candidates, ProcessingOptions::default())
1480}
1481
1482fn completion_filter_pattern_matches(
1483    pattern: &str,
1484    candidate: &str,
1485    token_being_completed: &str,
1486    shell: &Shell<impl extensions::ShellExtensions>,
1487) -> Result<bool, error::Error> {
1488    let pattern = replace_unescaped_ampersands(pattern, token_being_completed);
1489
1490    //
1491    // TODO(completions): Replace unescaped '&' with the word being completed.
1492    //
1493
1494    let pattern = shell_pattern(shell, pattern.as_ref());
1495
1496    let matches = pattern.exactly_matches(candidate)?;
1497
1498    Ok(matches)
1499}
1500
1501fn replace_unescaped_ampersands<'a>(pattern: &'a str, replacement: &str) -> Cow<'a, str> {
1502    let mut in_escape = false;
1503    let mut insertion_points = vec![];
1504
1505    for (i, c) in pattern.char_indices() {
1506        if !in_escape && c == '&' {
1507            insertion_points.push(i);
1508        }
1509        in_escape = !in_escape && c == '\\';
1510    }
1511
1512    if insertion_points.is_empty() {
1513        return pattern.into();
1514    }
1515
1516    let mut result = pattern.to_owned();
1517    for i in insertion_points.iter().rev() {
1518        result.replace_range(*i..=*i, replacement);
1519    }
1520
1521    result.into()
1522}
1523
1524/// Returns `pattern`, set to match as the shell's options say.
1525fn shell_pattern(
1526    shell: &Shell<impl extensions::ShellExtensions>,
1527    pattern: impl Into<patterns::Pattern>,
1528) -> patterns::Pattern {
1529    pattern
1530        .into()
1531        .set_extended_globbing(shell.options().extended_globbing)
1532        .set_case_insensitive(shell.options().case_insensitive_pathname_expansion)
1533}
1534
1535#[cfg(test)]
1536mod tests {
1537    use super::*;
1538
1539    #[test]
1540    fn generation_options_enable_and_disable_each_option() {
1541        let mut options: GenerationOptions = std::iter::once(CompleteOption::NoSpace).collect();
1542        assert!(options.get(CompleteOption::NoSpace));
1543        assert!(!options.get(CompleteOption::NoSort));
1544
1545        options.set(CompleteOption::NoSort, true);
1546        options.set(CompleteOption::NoSpace, false);
1547        assert!(options.get(CompleteOption::NoSort));
1548        assert!(!options.get(CompleteOption::NoSpace));
1549        assert_eq!(
1550            options,
1551            std::iter::once(CompleteOption::NoSort).collect::<GenerationOptions>()
1552        );
1553    }
1554
1555    #[test]
1556    fn special_specs_have_bash_names() {
1557        assert_eq!(SpecialSpec::Default.command_name(), "_DefaultCmD_");
1558        assert_eq!(SpecialSpec::EmptyLine.command_name(), "_EmptycmD_");
1559        assert_eq!(SpecialSpec::InitialWord.command_name(), "_InitialWorD_");
1560    }
1561
1562    #[test]
1563    fn special_specs_are_each_stored_on_their_own() {
1564        let mut config = Config::default();
1565        let specials = [
1566            (SpecialSpec::Default, "d"),
1567            (SpecialSpec::EmptyLine, "e"),
1568            (SpecialSpec::InitialWord, "i"),
1569        ];
1570
1571        for (special, word) in specials {
1572            assert!(config.get(SpecName::Special(special)).is_none());
1573            let spec = Spec {
1574                word_list: Some(word.to_owned()),
1575                ..Spec::default()
1576            };
1577            config.set(SpecName::Special(special), spec);
1578        }
1579
1580        for (special, word) in specials {
1581            let spec = config.get(SpecName::Special(special));
1582            assert_eq!(spec.and_then(|spec| spec.word_list.as_deref()), Some(word));
1583
1584            if let Some(spec) = config.get_mut(SpecName::Special(special)) {
1585                spec.prefix = Some(word.to_uppercase());
1586            }
1587            // Bash's name for the spec names the same one.
1588            let spec = config.get(SpecName::parse(special.command_name()));
1589            assert_eq!(
1590                spec.and_then(|spec| spec.prefix.clone()),
1591                Some(word.to_uppercase())
1592            );
1593        }
1594    }
1595
1596    #[test]
1597    fn spec_names_parse_bash_names_for_special_specs() {
1598        assert_eq!(
1599            SpecName::parse("_DefaultCmD_"),
1600            SpecName::Special(SpecialSpec::Default)
1601        );
1602        assert_eq!(SpecName::parse("mycmd"), SpecName::Command("mycmd"));
1603        for special in SpecialSpec::iter() {
1604            let name = SpecName::Special(special);
1605            assert_eq!(SpecName::parse(name.as_str()), name);
1606        }
1607        assert_eq!(SpecName::Command("mycmd").as_str(), "mycmd");
1608    }
1609
1610    #[test]
1611    fn special_specs_round_trip_through_bash_names() {
1612        for special in SpecialSpec::iter() {
1613            assert_eq!(
1614                SpecialSpec::from_command_name(special.command_name()),
1615                Some(special)
1616            );
1617        }
1618        assert_eq!(
1619            SpecialSpec::from_command_name("_DefaultCmD_"),
1620            Some(SpecialSpec::Default)
1621        );
1622        assert_eq!(SpecialSpec::from_command_name("mycmd"), None);
1623    }
1624
1625    #[test]
1626    fn completion_names_are_bash_names() {
1627        for action in CompleteAction::iter() {
1628            assert_eq!(<&str>::from(action).parse::<CompleteAction>(), Ok(action));
1629        }
1630        for option in CompleteOption::iter() {
1631            assert_eq!(<&str>::from(&option).parse::<CompleteOption>(), Ok(option));
1632        }
1633        assert_eq!(<&str>::from(CompleteAction::ArrayVar), "arrayvar");
1634        assert_eq!(<&str>::from(CompleteOption::BashDefault), "bashdefault");
1635    }
1636}