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    ///
498    /// # Errors
499    ///
500    /// Returns an error if running the completion generator fails, for example
501    /// when a completion function or command cannot be executed, or when
502    /// globbing or word expansion fails.
503    pub async fn generate(
504        &self,
505        shell: &mut Shell<impl extensions::ShellExtensions>,
506        word: &str,
507    ) -> Result<Vec<String>, crate::error::Error> {
508        let word = unquote_str(word);
509        let context = Context {
510            word: &word,
511            line: LineContext {
512                input: "",
513                cursor: 0,
514                words: &words::NO_WORDS,
515                command_name: Some("compgen"),
516                comp_type: 0,
517                comp_key: 0,
518            },
519        };
520
521        let Generated::Candidates(SpecCandidates { candidates, .. }) =
522            self.generate_candidates(shell, &context).await?
523        else {
524            return Ok(Vec::new());
525        };
526
527        let (candidates, _options) = self
528            .apply_options(shell, &context, candidates, self.options.clone())
529            .await;
530        Ok(candidates)
531    }
532
533    /// Completes the word being completed in `context`'s line, with a
534    /// completion in progress for it (see [`InProgressCompletion`]) while
535    /// its candidates are generated, so the `compopt` builtin can change
536    /// the options applied to them. Returns the candidates, sorted unless
537    /// `nosort`, and how to process them.
538    async fn complete(
539        &self,
540        shell: &mut Shell<impl extensions::ShellExtensions>,
541        context: &Context<'_>,
542    ) -> Result<Generated<(Vec<String>, ProcessingOptions)>, crate::error::Error> {
543        let mut scope = InProgressScope::start(
544            shell,
545            InProgressCompletion {
546                options: self.options.clone(),
547            },
548        );
549
550        let Generated::Candidates(SpecCandidates {
551            candidates,
552            file_names,
553        }) = self.generate_candidates(scope.shell, context).await?
554        else {
555            return Ok(Generated::Restart);
556        };
557
558        // Apply the options as `compopt` left them, and as generating the
559        // candidates did.
560        let mut options = scope.take_options().unwrap_or_else(|| self.options.clone());
561        if file_names {
562            options.set(CompleteOption::FileNames, true);
563        }
564        let sort = !options.get(CompleteOption::NoSort);
565        let (mut candidates, processing_options) = self
566            .apply_options(scope.shell, context, candidates, options)
567            .await;
568        if sort {
569            candidates.sort();
570        }
571        Ok(Generated::Candidates((candidates, processing_options)))
572    }
573
574    /// Generates this spec's candidates, before its options are applied.
575    async fn generate_candidates(
576        &self,
577        shell: &mut Shell<impl extensions::ShellExtensions>,
578        context: &Context<'_>,
579    ) -> Result<Generated<SpecCandidates>, crate::error::Error> {
580        // Generate completions based on any provided actions (and on words).
581        let SpecCandidates {
582            mut candidates,
583            file_names,
584        } = self.generate_action_completions(shell, context).await?;
585        if let Some(word_list) = &self.word_list {
586            let params = shell.default_exec_params();
587            let unexpanded_words =
588                split_completion_word_list(word_list, &shell.ifs(), &shell.parser_options())?;
589            let options = crate::expansion::ExpanderOptions {
590                pathname_expand: false,
591                ..Default::default()
592            };
593            let mut words = vec![];
594            for word in unexpanded_words {
595                words.extend(
596                    crate::expansion::full_expand_and_split_word_with_options(
597                        shell, &params, word, &options,
598                    )
599                    .await?,
600                );
601            }
602            candidates.extend(
603                words
604                    .into_iter()
605                    .filter(|word| word.starts_with(context.word)),
606            );
607        }
608
609        if let Some(glob_pattern) = &self.glob_pattern {
610            let pattern = shell_pattern(shell, glob_pattern.as_str());
611
612            let expansions = pattern
613                .expand(
614                    shell.working_dir(),
615                    Some(&patterns::Pattern::accept_all_expand_filter),
616                    &patterns::FilenameExpansionOptions::default(),
617                )?
618                .into_paths();
619
620            candidates.extend(expansions);
621        }
622        if let Some(function_name) = &self.function_name {
623            match call_completion_function(shell, function_name, context).await? {
624                Generated::Candidates(new_candidates) => candidates.extend(new_candidates),
625                Generated::Restart => return Ok(Generated::Restart),
626            }
627        }
628        if let Some(command) = &self.command {
629            candidates.extend(call_completion_command(shell, command, context).await?);
630        }
631
632        // Apply filter pattern, if present. Anything the filter selects gets
633        // removed.
634        if let Some(filter_pattern) = &self.filter_pattern
635            && !filter_pattern.is_empty()
636        {
637            let mut updated = Vec::new();
638
639            // Like bash, a leading `!` (unless extglob is on and it starts a
640            // `!(...)` pattern) inverts the filter, which then
641            // keeps what it matches.
642            let (pattern, keep_matches) = match filter_pattern.strip_prefix('!') {
643                Some(rest) if !(shell.options().extended_globbing && rest.starts_with('(')) => {
644                    (rest, true)
645                }
646                _ => (filter_pattern.as_str(), false),
647            };
648
649            for candidate in candidates {
650                let matches = completion_filter_pattern_matches(
651                    pattern,
652                    candidate.as_str(),
653                    context.word,
654                    shell,
655                )?;
656
657                if matches == keep_matches {
658                    updated.push(candidate);
659                }
660            }
661
662            candidates = updated;
663        }
664
665        // Add prefix and/or suffix, if present.
666        if self.prefix.is_some() || self.suffix.is_some() {
667            let empty = String::new();
668            let prefix = self.prefix.as_ref().unwrap_or(&empty);
669            let suffix = self.suffix.as_ref().unwrap_or(&empty);
670
671            let mut updated = Vec::with_capacity(candidates.len() * (prefix.len() + suffix.len()));
672            for candidate in candidates {
673                updated.push(std::format!("{prefix}{candidate}{suffix}"));
674            }
675
676            candidates = updated;
677        }
678
679        Ok(Generated::Candidates(SpecCandidates {
680            candidates,
681            file_names,
682        }))
683    }
684
685    /// Applies `options` to the candidates this spec generated, adding any
686    /// fallbacks they ask for; returns them and how to process them.
687    async fn apply_options(
688        &self,
689        shell: &Shell<impl extensions::ShellExtensions>,
690        context: &Context<'_>,
691        mut candidates: Vec<String>,
692        options: GenerationOptions,
693    ) -> (Vec<String>, ProcessingOptions) {
694        let mut processing_options = ProcessingOptions {
695            treat_as_filenames: options.get(CompleteOption::FileNames),
696            no_autoquote_filenames: options.get(CompleteOption::NoQuote),
697            no_trailing_space_at_end_of_line: options.get(CompleteOption::NoSpace),
698        };
699
700        // plusdirs always adds directory names; dirnames only does so when
701        // nothing else matched.
702        if options.get(CompleteOption::PlusDirs)
703            || (options.get(CompleteOption::DirNames) && candidates.is_empty())
704        {
705            let mut dir_candidates =
706                get_file_completions(shell, context.word, /* must_be_dir */ true).await;
707
708            // If directories are all we have, let them be marked as such.
709            if candidates.is_empty() && shell.completion_config().fallback_options.mark_directories
710            {
711                processing_options.treat_as_filenames = true;
712            }
713
714            candidates.append(&mut dir_candidates);
715        }
716
717        // If we still have no candidates, and bashdefault completions were
718        // requested, then generate those.
719        if candidates.is_empty() && options.get(CompleteOption::BashDefault) {
720            // TODO(completions): it's not clear what default "bash" completions
721            // means. From basic testing, this doesn't seem to
722            // include basic file and directory name completion.
723            tracing::debug!(target: trace_categories::COMPLETION, "unimplemented: complete -o bashdefault");
724        }
725
726        // If we still have no candidates, and default completions were
727        // requested, then generate those.
728        if candidates.is_empty() && options.get(CompleteOption::Default) {
729            // N.B. We approximate "default" readline completion behavior by
730            // getting file and dir completions.
731            let must_be_dir = options.get(CompleteOption::DirNames);
732
733            let mut default_candidates =
734                get_file_completions(shell, context.word, must_be_dir).await;
735            candidates.append(&mut default_candidates);
736
737            if shell.completion_config().fallback_options.mark_directories {
738                processing_options.treat_as_filenames = true;
739            }
740        }
741
742        (candidates, processing_options)
743    }
744
745    /// Generates the candidates of this spec's actions.
746    #[expect(clippy::too_many_lines, reason = "a flat match over the actions")]
747    async fn generate_action_completions(
748        &self,
749        shell: &Shell<impl extensions::ShellExtensions>,
750        context: &Context<'_>,
751    ) -> Result<SpecCandidates, error::Error> {
752        let prefix = context.word;
753        let mut candidates = Vec::new();
754        let mut file_names = false;
755
756        for action in &self.actions {
757            let c = &mut candidates;
758            match action {
759                // Aliases and functions are stored unordered; bash enumerates them sorted
760                // by name.
761                CompleteAction::Alias => {
762                    extend_matching(c, shell.aliases().keys().sorted(), prefix);
763                }
764                CompleteAction::ArrayVar => extend_matching(
765                    c,
766                    shell
767                        .env()
768                        .iter()
769                        .filter(|(_, var)| var.value().is_array())
770                        .map(|(name, _)| name),
771                    prefix,
772                ),
773                CompleteAction::Binding => extend_matching(
774                    c,
775                    interfaces::InputFunction::iter().map(<&'static str>::from),
776                    prefix,
777                ),
778                // For now, we only have help topics for built-in commands.
779                CompleteAction::Builtin | CompleteAction::HelpTopic => {
780                    extend_matching(c, shell.builtins().keys(), prefix);
781                }
782                CompleteAction::Command => c.extend(command_completions(shell, prefix)),
783                CompleteAction::Directory => {
784                    let dirs = get_file_completions(shell, context.word, true).await;
785                    file_names |= !dirs.is_empty();
786                    c.extend(dirs);
787                }
788                CompleteAction::Disabled => extend_matching(
789                    c,
790                    shell
791                        .builtins()
792                        .iter()
793                        .filter(|(_, registration)| registration.disabled)
794                        .map(|(name, _)| name),
795                    prefix,
796                ),
797                CompleteAction::Enabled => extend_matching(
798                    c,
799                    shell
800                        .builtins()
801                        .iter()
802                        .filter(|(_, registration)| !registration.disabled)
803                        .map(|(name, _)| name),
804                    prefix,
805                ),
806                CompleteAction::Export => extend_matching(
807                    c,
808                    shell
809                        .env()
810                        .iter()
811                        .filter(|(_, var)| var.is_exported())
812                        .map(|(name, _)| name),
813                    prefix,
814                ),
815                CompleteAction::File => {
816                    file_names = true;
817                    c.extend(get_file_completions(shell, context.word, false).await);
818                }
819                CompleteAction::Function => extend_matching(
820                    c,
821                    shell.funcs().iter().map(|(name, _)| name).sorted(),
822                    prefix,
823                ),
824                CompleteAction::Group => extend_matching(c, users::get_all_groups()?, prefix),
825                // N.B. We only retrieve one hostname.
826                CompleteAction::HostName => extend_matching(
827                    c,
828                    sys::network::get_hostname()
829                        .ok()
830                        .map(|name| name.to_string_lossy().into_owned()),
831                    prefix,
832                ),
833                CompleteAction::Job => extend_matching(
834                    c,
835                    shell.jobs().jobs.iter().map(|job| job.command_name()),
836                    prefix,
837                ),
838                CompleteAction::Keyword => extend_matching(c, shell.get_keywords(), prefix),
839                CompleteAction::Running => extend_matching(
840                    c,
841                    shell
842                        .jobs()
843                        .jobs
844                        .iter()
845                        .filter(|job| matches!(job.state, jobs::JobState::Running))
846                        .map(|job| job.command_name()),
847                    prefix,
848                ),
849                CompleteAction::Service => {
850                    tracing::debug!(target: trace_categories::COMPLETION, "unimplemented: complete -A service");
851                }
852                CompleteAction::SetOpt => extend_matching(
853                    c,
854                    namedoptions::options(namedoptions::ShellOptionKind::SetO)
855                        .iter()
856                        .map(|option| option.name),
857                    prefix,
858                ),
859                CompleteAction::ShOpt => extend_matching(
860                    c,
861                    namedoptions::options(namedoptions::ShellOptionKind::Shopt)
862                        .iter()
863                        .map(|option| option.name),
864                    prefix,
865                ),
866                CompleteAction::Signal => extend_matching(
867                    c,
868                    traps::TrapSignal::iterator().map(traps::TrapSignal::as_str),
869                    prefix,
870                ),
871                CompleteAction::Stopped => extend_matching(
872                    c,
873                    shell
874                        .jobs()
875                        .jobs
876                        .iter()
877                        .filter(|job| matches!(job.state, jobs::JobState::Stopped))
878                        .map(|job| job.command_name()),
879                    prefix,
880                ),
881                CompleteAction::User => extend_matching(c, users::get_all_users()?, prefix),
882                CompleteAction::Variable => {
883                    extend_matching(c, shell.env().iter().map(|(name, _)| name), prefix);
884                }
885            }
886        }
887
888        Ok(SpecCandidates {
889            candidates,
890            file_names,
891        })
892    }
893}
894
895/// Runs the completion command `command` (`complete -C`) to generate candidates
896/// for the word `context` is generating them for: one per line of its output.
897async fn call_completion_command(
898    shell: &mut Shell<impl extensions::ShellExtensions>,
899    command: &str,
900    context: &Context<'_>,
901) -> Result<Vec<String>, error::Error> {
902    let line = context.line;
903
904    // Compose the full command line.
905    let mut command_line = command.to_owned();
906    for arg in line.args(context.word) {
907        command_line.push(' ');
908        command_line.push_str(&escape::quote_if_needed(
909            arg,
910            escape::QuoteMode::SingleQuote,
911        ));
912    }
913
914    // Run the command in a subshell, with the variables exported to it and
915    // traps blocked. Like a command substitution, it would set `$?`; the
916    // user didn't run it, so leave that as it was.
917    let status = shell.save_command_status();
918    let mut scoped = enter_comp_vars_scope(shell, line.comp_vars(), true)?;
919    let mut blocked = scoped.block_trap_delivery();
920    let params = blocked.default_exec_params();
921    let output =
922        commands::invoke_command_in_subshell_and_get_output(&mut blocked, &params, command_line)
923            .await;
924    drop(blocked);
925    drop(scoped);
926    shell.restore_command_status(status);
927
928    Ok(output?.lines().map(str::to_owned).collect())
929}
930
931/// Runs the completion function `function_name` (`complete -F`) to generate
932/// candidates for the word `context` is generating them for: those it leaves in
933/// `COMPREPLY`, unless it asks for completion to restart.
934async fn call_completion_function(
935    shell: &mut Shell<impl extensions::ShellExtensions>,
936    function_name: &str,
937    context: &Context<'_>,
938) -> Result<Generated<Vec<String>>, error::Error> {
939    let line = context.line;
940    let mut vars = line.comp_vars();
941    vars.extend(line.comp_words_vars());
942
943    tracing::debug!(target: trace_categories::COMPLETION, "[calling completion func '{function_name}']: {}",
944        vars.iter().map(|(k, v)| std::format!("{k}={v:?}")).collect::<Vec<String>>().join(" "));
945
946    // Set the variables in a scope of their own, left when the function returns
947    // -- or when the completion is cancelled -- and block traps while it
948    // runs.
949    let names: Vec<_> = vars.iter().map(|(name, _)| *name).collect();
950    let mut scoped = enter_comp_vars_scope(shell, vars, false)?;
951    let mut blocked = scoped.block_trap_delivery();
952    let params = blocked.default_exec_params();
953    let invoke_result = blocked
954        .invoke_function(function_name, line.args(context.word).iter(), params)
955        .await
956        .map(|result| u8::from(result.exit_code));
957
958    // The function's done: unblock traps, and unset the variables.
959    drop(blocked);
960    drop(scoped);
961
962    // Like bash, also unset any the function set again after unsetting them, or
963    // that were set before it ran: either way, they're outside the scope
964    // just left.
965    for name in names {
966        let _ = shell.env_mut().unset(name);
967    }
968
969    tracing::debug!(target: trace_categories::COMPLETION, "[completion function '{function_name}' returned: {invoke_result:?}]");
970
971    let result = invoke_result.unwrap_or_else(|e| {
972        tracing::warn!(target: trace_categories::COMPLETION, "error while running completion function '{function_name}': {e}");
973        1 // Report back a non-zero exit code.
974    });
975
976    // Like bash, take `COMPREPLY` out of the shell even if it's discarded
977    // below.
978    let reply = shell.env_mut().unset("COMPREPLY")?;
979
980    // When the function returns the special value 124, then it's a request
981    // for us to restart the completion process.
982    if result == 124 {
983        return Ok(Generated::Restart);
984    }
985
986    tracing::debug!(target: trace_categories::COMPLETION, "[completion function yielded: {reply:?}]");
987
988    let candidates = match reply.as_ref().map(|reply| reply.value()) {
989        Some(variables::ShellValue::IndexedArray(values)) => {
990            values.values().map(|v| v.to_owned()).collect()
991        }
992        Some(variables::ShellValue::String(s)) => vec![s.to_owned()],
993        _ => Vec::new(),
994    };
995    Ok(Generated::Candidates(candidates))
996}
997
998/// Represents a set of generated command completions.
999#[derive(Debug, Default)]
1000pub struct Completions {
1001    /// The index in the input line where the completions should be inserted.
1002    /// Represented as a byte offset into the input line; must be at a clean
1003    /// character boundary.
1004    pub insertion_index: usize,
1005    /// The number of elements in the input line that should be removed before
1006    /// insertion. Represented as a byte count; must capture an exact
1007    /// character boundary.
1008    pub delete_count: usize,
1009    /// The ordered set of completions.
1010    pub candidates: Vec<String>,
1011    /// Options for processing the candidates.
1012    pub options: ProcessingOptions,
1013}
1014
1015/// Options governing how command completion candidates are processed after
1016/// being generated.
1017#[derive(Debug)]
1018pub struct ProcessingOptions {
1019    /// Treat completions as file names.
1020    pub treat_as_filenames: bool,
1021    /// Don't auto-quote completions that are file names.
1022    pub no_autoquote_filenames: bool,
1023    /// Don't append a trailing space to completions at the end of the input
1024    /// line.
1025    pub no_trailing_space_at_end_of_line: bool,
1026}
1027
1028impl Default for ProcessingOptions {
1029    fn default() -> Self {
1030        Self {
1031            treat_as_filenames: true,
1032            no_autoquote_filenames: false,
1033            no_trailing_space_at_end_of_line: false,
1034        }
1035    }
1036}
1037
1038/// A completion spec used in place of a command's.
1039#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, strum_macros::EnumIter)]
1040#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1041pub enum SpecialSpec {
1042    /// The spec used when no command's spec applies (`complete -D`).
1043    Default,
1044    /// The spec used when the command line is empty (`complete -E`).
1045    EmptyLine,
1046    /// The spec used for the initial word of a command line (`complete -I`).
1047    InitialWord,
1048}
1049
1050impl SpecialSpec {
1051    /// Returns the name that stands in for a command's with this spec, as
1052    /// bash's does: the `complete` and `compopt` builtins accept it in
1053    /// place of a command name, and a completion function for the
1054    /// empty-line or initial-word spec gets it as `$1`.
1055    pub const fn command_name(self) -> &'static str {
1056        match self {
1057            Self::Default => "_DefaultCmD_",
1058            Self::EmptyLine => "_EmptycmD_",
1059            Self::InitialWord => "_InitialWorD_",
1060        }
1061    }
1062
1063    /// Returns the special spec that `name`, bash's name for it, names, if any.
1064    pub fn from_command_name(name: &str) -> Option<Self> {
1065        Self::iter().find(|special| special.command_name() == name)
1066    }
1067}
1068
1069/// Names a completion spec in a [`Config`].
1070#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
1071pub enum SpecName<'a> {
1072    /// The spec for completing the named command's arguments.
1073    Command(&'a str),
1074    /// One of the special specs.
1075    Special(SpecialSpec),
1076}
1077
1078impl<'a> SpecName<'a> {
1079    /// Returns the spec named `name`: a command's, or the special spec that
1080    /// bash's name for it (see [`SpecialSpec::command_name`]) names.
1081    pub fn parse(name: &'a str) -> Self {
1082        SpecialSpec::from_command_name(name).map_or(Self::Command(name), Self::Special)
1083    }
1084
1085    /// Returns the name: a command's, or for a special spec, bash's name for
1086    /// it.
1087    pub const fn as_str(self) -> &'a str {
1088        match self {
1089            Self::Command(command) => command,
1090            Self::Special(special) => special.command_name(),
1091        }
1092    }
1093}
1094
1095impl Config {
1096    /// Removes all registered completion specs.
1097    pub fn clear(&mut self) {
1098        self.commands.clear();
1099        self.specials.clear();
1100    }
1101
1102    /// Ensures the named completion spec is no longer registered; returns
1103    /// whether a removal operation was required.
1104    ///
1105    /// # Arguments
1106    ///
1107    /// * `name` - The name of the completion spec to remove.
1108    pub fn remove(&mut self, name: SpecName<'_>) -> bool {
1109        match name {
1110            SpecName::Command(command) => self.commands.remove(command).is_some(),
1111            SpecName::Special(special) => self.specials.remove(&special).is_some(),
1112        }
1113    }
1114
1115    /// Returns an iterator over the completion specs and their names, in no
1116    /// particular order.
1117    pub fn iter(&self) -> impl Iterator<Item = (SpecName<'_>, &Spec)> {
1118        let commands = self
1119            .commands
1120            .iter()
1121            .map(|(command, spec)| (SpecName::Command(command), spec));
1122        let specials = self
1123            .specials
1124            .iter()
1125            .map(|(special, spec)| (SpecName::Special(*special), spec));
1126        commands.chain(specials)
1127    }
1128
1129    /// If present, returns the named completion spec.
1130    ///
1131    /// # Arguments
1132    ///
1133    /// * `name` - The name of the completion spec.
1134    pub fn get(&self, name: SpecName<'_>) -> Option<&Spec> {
1135        match name {
1136            SpecName::Command(command) => self.commands.get(command),
1137            SpecName::Special(special) => self.specials.get(&special),
1138        }
1139    }
1140
1141    /// If present, returns a mutable reference to the named completion spec.
1142    ///
1143    /// # Arguments
1144    ///
1145    /// * `name` - The name of the completion spec.
1146    pub fn get_mut(&mut self, name: SpecName<'_>) -> Option<&mut Spec> {
1147        match name {
1148            SpecName::Command(command) => self.commands.get_mut(command),
1149            SpecName::Special(special) => self.specials.get_mut(&special),
1150        }
1151    }
1152
1153    /// Registers the provided completion spec under the given name, replacing
1154    /// any already registered there.
1155    ///
1156    /// # Arguments
1157    ///
1158    /// * `name` - The name of the completion spec.
1159    /// * `spec` - The completion spec.
1160    pub fn set(&mut self, name: SpecName<'_>, spec: Spec) {
1161        match name {
1162            SpecName::Command(command) => {
1163                self.commands.insert(command.to_owned(), spec);
1164            }
1165            SpecName::Special(special) => {
1166                self.specials.insert(special, spec);
1167            }
1168        }
1169    }
1170
1171    /// Returns the completion spec to use for completing `line`, and the name
1172    /// of the command it completes for (a completion function's `$1`): the
1173    /// empty-line spec (`complete -E`) if there's nothing before the cursor
1174    /// and it isn't at the start of a word, else the spec for the initial
1175    /// word (`complete -I`) if completing that, else its command's spec (by
1176    /// name, or by file name if the command is a path), else the default spec
1177    /// (`complete -D`). Like bash, the empty-line and initial-word specs
1178    /// complete for commands named after them.
1179    fn find_spec<'l>(&self, line: &LineContext<'l>) -> Option<(&Spec, Option<&'l str>)> {
1180        let special = |special: SpecialSpec| {
1181            let spec = self.specials.get(&special)?;
1182            Some((spec, Some(special.command_name())))
1183        };
1184
1185        // Like bash, it's the empty line only if the cursor is at its very
1186        // start, and not at the start of a word there: with whitespace
1187        // before the cursor, or a word just after it, the initial word
1188        // is completed.
1189        if line.cursor == 0 && line.input.chars().next().is_none_or(char::is_whitespace) {
1190            return special(SpecialSpec::EmptyLine);
1191        }
1192
1193        if matches!(line.words.cword, None | Some(0)) {
1194            return special(SpecialSpec::InitialWord);
1195        }
1196
1197        let command_name = line.command_name?;
1198        let spec = self
1199            .commands
1200            .get(command_name)
1201            .or_else(|| {
1202                let file_name = Path::new(command_name).file_name()?;
1203                self.commands.get(file_name.to_string_lossy().as_ref())
1204            })
1205            .or_else(|| self.specials.get(&SpecialSpec::Default))?;
1206        Some((spec, Some(command_name)))
1207    }
1208}
1209
1210/// Completes `input` at `cursor`: see [`Shell::complete`].
1211pub(crate) async fn complete(
1212    shell: &mut Shell<impl extensions::ShellExtensions>,
1213    input: &str,
1214    cursor: usize,
1215) -> Result<Completions, error::Error> {
1216    /// How many times a completion function may ask for completion to restart.
1217    const MAX_RESTARTS: u32 = 10;
1218
1219    if !input.is_char_boundary(cursor) {
1220        return Err(error::ErrorKind::InvalidCompletionPosition(cursor).into());
1221    }
1222
1223    let word_breaks = word_break_chars(shell);
1224    let word = find_completion_word(input, &word_breaks, cursor);
1225    let line_words = find_line_words(input, &word_breaks, cursor);
1226    let context = Context {
1227        word: &word.text,
1228        line: LineContext::new(input, cursor, &line_words),
1229    };
1230
1231    // Complete, restarting as often as completion functions ask to. If they
1232    // never stop asking, there's nothing to complete with.
1233    let mut completed = None;
1234    for _ in 0..=MAX_RESTARTS {
1235        if let Generated::Candidates(candidates_and_options) = complete_word(shell, context).await {
1236            completed = Some(candidates_and_options);
1237            break;
1238        }
1239    }
1240    let (candidates, options) = completed.unwrap_or_else(|| {
1241        tracing::warn!(target: trace_categories::COMPLETION, "completion kept restarting; giving up");
1242        (Vec::new(), ProcessingOptions::default())
1243    });
1244
1245    Ok(Completions {
1246        insertion_index: word.range.start,
1247        delete_count: word.range.len(),
1248        candidates,
1249        options,
1250    })
1251}
1252
1253fn word_break_chars(shell: &Shell<impl extensions::ShellExtensions>) -> Vec<char> {
1254    const FALLBACK: &str = " \t\n\"\'@><=;|&(:";
1255
1256    shell
1257        .env_str("COMP_WORDBREAKS")
1258        .unwrap_or_else(|| FALLBACK.into())
1259        .chars()
1260        .collect()
1261}
1262
1263/// Completes the word being completed in `context`'s line, with the completion
1264/// spec that applies to it if there is one, or else with basic completion.
1265/// Returns the candidates and how to process them.
1266async fn complete_word(
1267    shell: &mut Shell<impl extensions::ShellExtensions>,
1268    mut context: Context<'_>,
1269) -> Generated<(Vec<String>, ProcessingOptions)> {
1270    // Look the spec up afresh each time: a completion function that asks for
1271    // completion to restart may have registered a new one.
1272    let Some((spec, command_name)) = shell.completion_config().find_spec(&context.line) else {
1273        return Generated::Candidates(get_completions_using_basic_lookup(shell, &context).await);
1274    };
1275    let spec = spec.clone();
1276    context.line.command_name = command_name;
1277
1278    spec.complete(shell, &context).await.unwrap_or_else(|err| {
1279        tracing::debug!(target: trace_categories::COMPLETION, "completion spec failed: {err}");
1280        Generated::Candidates((Vec::new(), ProcessingOptions::default()))
1281    })
1282}
1283
1284async fn get_file_completions(
1285    shell: &Shell<impl extensions::ShellExtensions>,
1286    token_to_complete: &str,
1287    must_be_dir: bool,
1288) -> Vec<String> {
1289    // Basic-expand the token-to-be-completed; it won't have been expanded to
1290    // this point.
1291    let mut throwaway_shell = shell.clone();
1292    let params = throwaway_shell.default_exec_params();
1293    let options = expansion::ExpanderOptions {
1294        execute_command_substitutions: false,
1295        ..Default::default()
1296    };
1297    let expanded_token = expansion::basic_expand_word_with_options(
1298        &mut throwaway_shell,
1299        &params,
1300        &unquote_str(token_to_complete),
1301        &options,
1302    )
1303    .await
1304    .unwrap_or_else(|_err| token_to_complete.to_owned());
1305
1306    // Normalize path separators before building the glob pattern, because
1307    // backslash is the escape character in glob syntax and must not be
1308    // confused with a Windows path separator.
1309    let expanded_token = sys::fs::normalize_path_separators(&expanded_token).into_owned();
1310
1311    let glob = std::format!("{expanded_token}*");
1312
1313    let path_filter = |path: &Path| !must_be_dir || shell.absolute_path(path).is_dir();
1314
1315    let pattern = shell_pattern(shell, glob);
1316
1317    let mut completions: Vec<String> = pattern
1318        .expand(
1319            shell.working_dir(),
1320            Some(&path_filter),
1321            &patterns::FilenameExpansionOptions::default(),
1322        )
1323        .unwrap_or_default()
1324        .into_paths()
1325        .into_iter()
1326        .map(|p| match sys::fs::normalize_path_separators(&p) {
1327            std::borrow::Cow::Borrowed(_) => p,
1328            std::borrow::Cow::Owned(normalized) => normalized,
1329        })
1330        .collect();
1331
1332    match expanded_token.as_str() {
1333        "." => {
1334            completions.push(".".into());
1335            completions.push("..".into());
1336        }
1337        ".." => {
1338            completions.push("..".into());
1339        }
1340        _ => {}
1341    }
1342
1343    completions.sort();
1344    completions.dedup();
1345    completions
1346}
1347
1348/// Attempts to complete a variable name from the given token.
1349/// Returns the candidates and how to process them if the token looks like a
1350/// variable reference being typed, or `None` if file/command completion should
1351/// be used instead.
1352///
1353/// # Arguments
1354///
1355/// * `shell` - The shell instance to use for variable lookup.
1356/// * `token` - The token being completed. May be empty.
1357fn try_get_variable_completions(
1358    shell: &Shell<impl extensions::ShellExtensions>,
1359    token: &str,
1360) -> Option<(Vec<String>, ProcessingOptions)> {
1361    // Determine if this is a braced or unbraced variable reference
1362    let (var_prefix, use_braces) = if let Some(prefix) = token.strip_prefix("${") {
1363        // For braced: only complete if brace isn't closed yet
1364        if prefix.contains('}') {
1365            return None;
1366        }
1367        (prefix, true)
1368    } else {
1369        let prefix = token.strip_prefix('$')?;
1370        (prefix, false)
1371    };
1372
1373    // If there's a path separator, this is a path like $HOME/foo, not a
1374    // variable to complete
1375    if sys::fs::contains_path_separator(var_prefix) {
1376        return None;
1377    }
1378
1379    // Find matching variables
1380    let mut candidates: Vec<String> = shell
1381        .env()
1382        .iter()
1383        .filter(|(key, _)| key.starts_with(var_prefix))
1384        .map(|(key, _)| {
1385            if use_braces {
1386                format!("${{{key}}}")
1387            } else {
1388                format!("${key}")
1389            }
1390        })
1391        .collect();
1392    candidates.sort();
1393
1394    // Variable completions should not be treated as filenames (no escaping
1395    // needed)
1396    let options = ProcessingOptions {
1397        treat_as_filenames: false,
1398        ..ProcessingOptions::default()
1399    };
1400
1401    Some((candidates, options))
1402}
1403
1404/// Returns the names of the commands that start with `prefix`, in the order
1405/// bash lists them: aliases, keywords, functions, enabled builtins, then
1406/// executables in the path.
1407fn command_completions(
1408    shell: &Shell<impl extensions::ShellExtensions>,
1409    prefix: &str,
1410) -> Vec<String> {
1411    let mut names = Vec::new();
1412
1413    // Aliases, functions, and builtins are stored unordered; bash enumerates
1414    // each sorted by name.
1415    extend_matching(&mut names, shell.aliases().keys().sorted(), prefix);
1416    extend_matching(&mut names, shell.get_keywords(), prefix);
1417    extend_matching(
1418        &mut names,
1419        shell.funcs().iter().map(|(name, _)| name).sorted(),
1420        prefix,
1421    );
1422    extend_matching(
1423        &mut names,
1424        shell
1425            .builtins()
1426            .iter()
1427            .filter(|(_, registration)| !registration.disabled)
1428            .map(|(name, _)| name)
1429            .sorted(),
1430        prefix,
1431    );
1432    names.extend(
1433        shell
1434            .find_executables_in_path_with_prefix(
1435                prefix,
1436                shell.options().case_insensitive_pathname_expansion,
1437            )
1438            .filter_map(|path| Some(path.file_name()?.to_string_lossy().into_owned())),
1439    );
1440
1441    names
1442}
1443
1444/// Adds the `names` that start with `prefix` to `candidates`.
1445fn extend_matching<S: AsRef<str>>(
1446    candidates: &mut Vec<String>,
1447    names: impl IntoIterator<Item = S>,
1448    prefix: &str,
1449) {
1450    candidates.extend(
1451        names
1452            .into_iter()
1453            .filter(|name| name.as_ref().starts_with(prefix))
1454            .map(|name| name.as_ref().to_owned()),
1455    );
1456}
1457
1458async fn get_completions_using_basic_lookup(
1459    shell: &Shell<impl extensions::ShellExtensions>,
1460    context: &Context<'_>,
1461) -> (Vec<String>, ProcessingOptions) {
1462    let token = context.word;
1463
1464    // Try variable completion first (e.g., $HO -> $HOME, ${HO -> ${HOME})
1465    if let Some(answer) = try_get_variable_completions(shell, token) {
1466        return answer;
1467    }
1468
1469    // File completions
1470    let mut candidates = get_file_completions(shell, token, false).await;
1471
1472    // If this appears to be the command token (and if there's *some* prefix
1473    // without a path separator) then also consider whether we should search
1474    // the path for completions too.
1475    // TODO(completions): Do a better job than just checking if index == 0.
1476    let is_command_position = context.line.words.cword == Some(0)
1477        && !token.is_empty()
1478        && !sys::fs::contains_path_separator(token);
1479
1480    if is_command_position {
1481        candidates.extend(command_completions(shell, token));
1482        candidates.sort();
1483    }
1484
1485    (candidates, ProcessingOptions::default())
1486}
1487
1488fn completion_filter_pattern_matches(
1489    pattern: &str,
1490    candidate: &str,
1491    token_being_completed: &str,
1492    shell: &Shell<impl extensions::ShellExtensions>,
1493) -> Result<bool, error::Error> {
1494    let pattern = replace_unescaped_ampersands(pattern, token_being_completed);
1495
1496    //
1497    // TODO(completions): Replace unescaped '&' with the word being completed.
1498    //
1499
1500    let pattern = shell_pattern(shell, pattern.as_ref());
1501
1502    let matches = pattern.exactly_matches(candidate)?;
1503
1504    Ok(matches)
1505}
1506
1507fn replace_unescaped_ampersands<'a>(pattern: &'a str, replacement: &str) -> Cow<'a, str> {
1508    let mut in_escape = false;
1509    let mut insertion_points = vec![];
1510
1511    for (i, c) in pattern.char_indices() {
1512        if !in_escape && c == '&' {
1513            insertion_points.push(i);
1514        }
1515        in_escape = !in_escape && c == '\\';
1516    }
1517
1518    if insertion_points.is_empty() {
1519        return pattern.into();
1520    }
1521
1522    let mut result = pattern.to_owned();
1523    for i in insertion_points.iter().rev() {
1524        result.replace_range(*i..=*i, replacement);
1525    }
1526
1527    result.into()
1528}
1529
1530/// Returns `pattern`, set to match as the shell's options say.
1531fn shell_pattern(
1532    shell: &Shell<impl extensions::ShellExtensions>,
1533    pattern: impl Into<patterns::Pattern>,
1534) -> patterns::Pattern {
1535    pattern
1536        .into()
1537        .set_extended_globbing(shell.options().extended_globbing)
1538        .set_case_insensitive(shell.options().case_insensitive_pathname_expansion)
1539}
1540
1541#[cfg(test)]
1542mod tests {
1543    use super::*;
1544
1545    #[test]
1546    fn generation_options_enable_and_disable_each_option() {
1547        let mut options: GenerationOptions = std::iter::once(CompleteOption::NoSpace).collect();
1548        assert!(options.get(CompleteOption::NoSpace));
1549        assert!(!options.get(CompleteOption::NoSort));
1550
1551        options.set(CompleteOption::NoSort, true);
1552        options.set(CompleteOption::NoSpace, false);
1553        assert!(options.get(CompleteOption::NoSort));
1554        assert!(!options.get(CompleteOption::NoSpace));
1555        assert_eq!(
1556            options,
1557            std::iter::once(CompleteOption::NoSort).collect::<GenerationOptions>()
1558        );
1559    }
1560
1561    #[test]
1562    fn special_specs_have_bash_names() {
1563        assert_eq!(SpecialSpec::Default.command_name(), "_DefaultCmD_");
1564        assert_eq!(SpecialSpec::EmptyLine.command_name(), "_EmptycmD_");
1565        assert_eq!(SpecialSpec::InitialWord.command_name(), "_InitialWorD_");
1566    }
1567
1568    #[test]
1569    fn special_specs_are_each_stored_on_their_own() {
1570        let mut config = Config::default();
1571        let specials = [
1572            (SpecialSpec::Default, "d"),
1573            (SpecialSpec::EmptyLine, "e"),
1574            (SpecialSpec::InitialWord, "i"),
1575        ];
1576
1577        for (special, word) in specials {
1578            assert!(config.get(SpecName::Special(special)).is_none());
1579            let spec = Spec {
1580                word_list: Some(word.to_owned()),
1581                ..Spec::default()
1582            };
1583            config.set(SpecName::Special(special), spec);
1584        }
1585
1586        for (special, word) in specials {
1587            let spec = config.get(SpecName::Special(special));
1588            assert_eq!(spec.and_then(|spec| spec.word_list.as_deref()), Some(word));
1589
1590            if let Some(spec) = config.get_mut(SpecName::Special(special)) {
1591                spec.prefix = Some(word.to_uppercase());
1592            }
1593            // Bash's name for the spec names the same one.
1594            let spec = config.get(SpecName::parse(special.command_name()));
1595            assert_eq!(
1596                spec.and_then(|spec| spec.prefix.clone()),
1597                Some(word.to_uppercase())
1598            );
1599        }
1600    }
1601
1602    #[test]
1603    fn spec_names_parse_bash_names_for_special_specs() {
1604        assert_eq!(
1605            SpecName::parse("_DefaultCmD_"),
1606            SpecName::Special(SpecialSpec::Default)
1607        );
1608        assert_eq!(SpecName::parse("mycmd"), SpecName::Command("mycmd"));
1609        for special in SpecialSpec::iter() {
1610            let name = SpecName::Special(special);
1611            assert_eq!(SpecName::parse(name.as_str()), name);
1612        }
1613        assert_eq!(SpecName::Command("mycmd").as_str(), "mycmd");
1614    }
1615
1616    #[test]
1617    fn special_specs_round_trip_through_bash_names() {
1618        for special in SpecialSpec::iter() {
1619            assert_eq!(
1620                SpecialSpec::from_command_name(special.command_name()),
1621                Some(special)
1622            );
1623        }
1624        assert_eq!(
1625            SpecialSpec::from_command_name("_DefaultCmD_"),
1626            Some(SpecialSpec::Default)
1627        );
1628        assert_eq!(SpecialSpec::from_command_name("mycmd"), None);
1629    }
1630
1631    #[test]
1632    fn completion_names_are_bash_names() {
1633        for action in CompleteAction::iter() {
1634            assert_eq!(<&str>::from(action).parse::<CompleteAction>(), Ok(action));
1635        }
1636        for option in CompleteOption::iter() {
1637            assert_eq!(<&str>::from(&option).parse::<CompleteOption>(), Ok(option));
1638        }
1639        assert_eq!(<&str>::from(CompleteAction::ArrayVar), "arrayvar");
1640        assert_eq!(<&str>::from(CompleteOption::BashDefault), "bashdefault");
1641    }
1642}