Skip to main content

brush_core/
completion.rs

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