brush_core/
completion.rs

1//! Implements programmable command completion support.
2
3use clap::ValueEnum;
4use indexmap::IndexSet;
5use std::{
6    borrow::Cow,
7    collections::HashMap,
8    path::{Path, PathBuf},
9};
10
11use crate::{
12    Shell, commands, env, error, escape, jobs, namedoptions, patterns,
13    sys::{self, users},
14    trace_categories, traps,
15    variables::{self, ShellValueLiteral},
16};
17
18/// Type of action to take to generate completion candidates.
19#[derive(Clone, Debug, ValueEnum)]
20pub enum CompleteAction {
21    /// Complete with valid aliases.
22    #[clap(name = "alias")]
23    Alias,
24    /// Complete with names of array shell variables.
25    #[clap(name = "arrayvar")]
26    ArrayVar,
27    /// Complete with names of key bindings.
28    #[clap(name = "binding")]
29    Binding,
30    /// Complete with names of shell builtins.
31    #[clap(name = "builtin")]
32    Builtin,
33    /// Complete with names of executable commands.
34    #[clap(name = "command")]
35    Command,
36    /// Complete with directory names.
37    #[clap(name = "directory")]
38    Directory,
39    /// Complete with names of disabled shell builtins.
40    #[clap(name = "disabled")]
41    Disabled,
42    /// Complete with names of enabled shell builtins.
43    #[clap(name = "enabled")]
44    Enabled,
45    /// Complete with names of exported shell variables.
46    #[clap(name = "export")]
47    Export,
48    /// Complete with filenames.
49    #[clap(name = "file")]
50    File,
51    /// Complete with names of shell functions.
52    #[clap(name = "function")]
53    Function,
54    /// Complete with valid user groups.
55    #[clap(name = "group")]
56    Group,
57    /// Complete with names of valid shell help topics.
58    #[clap(name = "helptopic")]
59    HelpTopic,
60    /// Complete with the system's hostname(s).
61    #[clap(name = "hostname")]
62    HostName,
63    /// Complete with the command names of shell-managed jobs.
64    #[clap(name = "job")]
65    Job,
66    /// Complete with valid shell keywords.
67    #[clap(name = "keyword")]
68    Keyword,
69    /// Complete with the command names of running shell-managed jobs.
70    #[clap(name = "running")]
71    Running,
72    /// Complete with names of system services.
73    #[clap(name = "service")]
74    Service,
75    /// Complete with the names of options settable via shopt.
76    #[clap(name = "setopt")]
77    SetOpt,
78    /// Complete with the names of options settable via set -o.
79    #[clap(name = "shopt")]
80    ShOpt,
81    /// Complete with the names of trappable signals.
82    #[clap(name = "signal")]
83    Signal,
84    /// Complete with the command names of stopped shell-managed jobs.
85    #[clap(name = "stopped")]
86    Stopped,
87    /// Complete with valid usernames.
88    #[clap(name = "user")]
89    User,
90    /// Complete with names of shell variables.
91    #[clap(name = "variable")]
92    Variable,
93}
94
95/// Options influencing how command completions are generated.
96#[derive(Clone, Debug, Eq, Hash, PartialEq, ValueEnum)]
97pub enum CompleteOption {
98    /// Perform rest of default completions if no completions are generated.
99    #[clap(name = "bashdefault")]
100    BashDefault,
101    /// Use default filename completion if no completions are generated.
102    #[clap(name = "default")]
103    Default,
104    /// Treat completions as directory names.
105    #[clap(name = "dirnames")]
106    DirNames,
107    /// Treat completions as filenames.
108    #[clap(name = "filenames")]
109    FileNames,
110    /// Suppress default auto-quotation of completions.
111    #[clap(name = "noquote")]
112    NoQuote,
113    /// Do not sort completions.
114    #[clap(name = "nosort")]
115    NoSort,
116    /// Do not append a trailing space to completions at the end of the input line.
117    #[clap(name = "nospace")]
118    NoSpace,
119    /// Also generate directory completions.
120    #[clap(name = "plusdirs")]
121    PlusDirs,
122}
123
124/// Encapsulates the shell's programmable command completion configuration.
125#[derive(Clone, Default)]
126pub struct Config {
127    commands: HashMap<String, Spec>,
128
129    /// Optionally, a completion spec to be used as a default, when earlier
130    /// matches yield no candidates.
131    pub default: Option<Spec>,
132    /// Optionally, a completion spec to be used when the command line is empty.
133    pub empty_line: Option<Spec>,
134    /// Optionally, a completion spec to be used for the initial word of a command line.
135    pub initial_word: Option<Spec>,
136
137    /// Optionally, stores the current completion options in effect. May be mutated
138    /// while a completion generation is in-flight.
139    pub current_completion_options: Option<GenerationOptions>,
140}
141
142/// Options for generating completions.
143#[derive(Clone, Debug, Default)]
144pub struct GenerationOptions {
145    //
146    // Options
147    /// Perform rest of default completions if no completions are generated.
148    pub bash_default: bool,
149    /// Use default readline-style filename completion if no completions are generated.
150    pub default: bool,
151    /// Treat completions as directory names.
152    pub dir_names: bool,
153    /// Treat completions as filenames.
154    pub file_names: bool,
155    /// Do not add usual quoting for completions.
156    pub no_quote: bool,
157    /// Do not sort completions.
158    pub no_sort: bool,
159    /// Do not append typical space to a completion at the end of the input line.
160    pub no_space: bool,
161    /// Also complete with directory names.
162    pub plus_dirs: bool,
163}
164
165/// Encapsulates a command completion specification; provides policy for how to
166/// generate completions for a given input.
167#[derive(Clone, Debug, Default)]
168pub struct Spec {
169    //
170    // Options
171    /// Options to use for completion.
172    pub options: GenerationOptions,
173
174    //
175    // Generators
176    /// Actions to take to generate completions.
177    pub actions: Vec<CompleteAction>,
178    /// Optionally, a glob pattern whose expansion will be used as completions.
179    pub glob_pattern: Option<String>,
180    /// Optionally, a list of words to use as completions.
181    pub word_list: Option<String>,
182    /// Optionally, the name of a shell function to invoke to generate completions.
183    pub function_name: Option<String>,
184    /// Optionally, the name of a command to execute to generate completions.
185    pub command: Option<String>,
186
187    //
188    // Filters
189    /// Optionally, a pattern to filter completions.
190    pub filter_pattern: Option<String>,
191    /// If true, completion candidates matching `filter_pattern` are removed;
192    /// otherwise, those not matching it are removed.
193    pub filter_pattern_excludes: bool,
194
195    //
196    // Transformers
197    /// Optionally, provides a prefix to be prepended to all completion candidates.
198    pub prefix: Option<String>,
199    /// Optionally, provides a suffix to be prepended to all completion candidates.
200    pub suffix: Option<String>,
201}
202
203/// Encapsulates context used during completion generation.
204#[derive(Debug)]
205pub struct Context<'a> {
206    /// The token to complete.
207    pub token_to_complete: &'a str,
208
209    /// If available, the name of the command being invoked.
210    pub command_name: Option<&'a str>,
211    /// If there was one, the token preceding the one being completed.
212    pub preceding_token: Option<&'a str>,
213
214    /// The 0-based index of the token to complete.
215    pub token_index: usize,
216
217    /// The input line.
218    pub input_line: &'a str,
219    /// The 0-based index of the cursor in the input line.
220    pub cursor_index: usize,
221    /// The tokens in the input line.
222    pub tokens: &'a [&'a brush_parser::Token],
223}
224
225impl Spec {
226    /// Generates completion candidates using this specification.
227    ///
228    /// # Arguments
229    ///
230    /// * `shell` - The shell instance to use for completion generation.
231    /// * `context` - The context in which completion is being generated.
232    #[expect(clippy::too_many_lines)]
233    pub async fn get_completions(
234        &self,
235        shell: &mut Shell,
236        context: &Context<'_>,
237    ) -> Result<Answer, crate::error::Error> {
238        // Store the current options in the shell; this is needed since the compopt
239        // built-in has the ability of modifying the options for an in-flight
240        // completion process.
241        shell.completion_config.current_completion_options = Some(self.options.clone());
242
243        // Generate completions based on any provided actions (and on words).
244        let mut candidates = self.generate_action_completions(shell, context).await?;
245        if let Some(word_list) = &self.word_list {
246            let params = shell.default_exec_params();
247            let words =
248                crate::expansion::full_expand_and_split_str(shell, &params, word_list).await?;
249            for word in words {
250                if word.starts_with(context.token_to_complete) {
251                    candidates.insert(word);
252                }
253            }
254        }
255
256        if let Some(glob_pattern) = &self.glob_pattern {
257            let pattern = patterns::Pattern::from(glob_pattern.as_str())
258                .set_extended_globbing(shell.options.extended_globbing)
259                .set_case_insensitive(shell.options.case_insensitive_pathname_expansion);
260
261            let expansions = pattern.expand(
262                shell.working_dir(),
263                Some(&patterns::Pattern::accept_all_expand_filter),
264                &patterns::FilenameExpansionOptions::default(),
265            )?;
266
267            for expansion in expansions {
268                candidates.insert(expansion);
269            }
270        }
271        if let Some(function_name) = &self.function_name {
272            let call_result = self
273                .call_completion_function(shell, function_name.as_str(), context)
274                .await?;
275
276            match call_result {
277                Answer::RestartCompletionProcess => return Ok(call_result),
278                Answer::Candidates(mut new_candidates, _options) => {
279                    candidates.append(&mut new_candidates);
280                }
281            }
282        }
283        if let Some(command) = &self.command {
284            let mut new_candidates = self
285                .call_completion_command(shell, command.as_str(), context)
286                .await?;
287            candidates.append(&mut new_candidates);
288        }
289
290        // Apply filter pattern, if present. Anything the filter selects gets removed.
291        if let Some(filter_pattern) = &self.filter_pattern {
292            if !filter_pattern.is_empty() {
293                let mut updated = IndexSet::new();
294
295                for candidate in candidates {
296                    let matches = completion_filter_pattern_matches(
297                        filter_pattern.as_str(),
298                        candidate.as_str(),
299                        context.token_to_complete,
300                        shell,
301                    )?;
302
303                    if self.filter_pattern_excludes != matches {
304                        updated.insert(candidate);
305                    }
306                }
307
308                candidates = updated;
309            }
310        }
311
312        // Add prefix and/or suffix, if present.
313        if self.prefix.is_some() || self.suffix.is_some() {
314            let empty = String::new();
315            let prefix = self.prefix.as_ref().unwrap_or(&empty);
316            let suffix = self.suffix.as_ref().unwrap_or(&empty);
317
318            let mut updated = IndexSet::new();
319            for candidate in candidates {
320                updated.insert(std::format!("{prefix}{candidate}{suffix}"));
321            }
322
323            candidates = updated;
324        }
325
326        //
327        // Now apply options
328        //
329
330        let options = if let Some(options) = &shell.completion_config.current_completion_options {
331            options
332        } else {
333            &self.options
334        };
335
336        let processing_options = ProcessingOptions {
337            treat_as_filenames: options.file_names,
338            no_autoquote_filenames: options.no_quote,
339            no_trailing_space_at_end_of_line: options.no_space,
340        };
341
342        if options.plus_dirs {
343            // Also add dir name completion.
344            let mut dir_candidates = get_file_completions(
345                shell,
346                context.token_to_complete,
347                /* must_be_dir */ true,
348            )
349            .await;
350            candidates.append(&mut dir_candidates);
351        }
352
353        // If we still haven't found any completion candidates by now, then consider whether any
354        // requests were made for fallbacks.
355        if candidates.is_empty() {
356            if options.bash_default {
357                //
358                // TODO: if we have no completions, then fall back to default "bash" completions.
359                // It's not clear what exactly this means, though. From basic testing, it doesn't
360                // seem to include basic file and directory name completion.
361                //
362                tracing::debug!(target: trace_categories::COMPLETION, "unimplemented: complete -o bashdefault");
363            }
364            if options.default || options.dir_names {
365                // N.B. We approximate "default" readline completion behavior by getting file and
366                // dir completions.
367                let must_be_dir = options.dir_names;
368
369                let mut default_candidates =
370                    get_file_completions(shell, context.token_to_complete, must_be_dir).await;
371                candidates.append(&mut default_candidates);
372            }
373        }
374
375        // Sort, unless blocked by options.
376        if !self.options.no_sort {
377            candidates.sort();
378        }
379
380        Ok(Answer::Candidates(candidates, processing_options))
381    }
382
383    #[expect(clippy::too_many_lines)]
384    async fn generate_action_completions(
385        &self,
386        shell: &Shell,
387        context: &Context<'_>,
388    ) -> Result<IndexSet<String>, error::Error> {
389        let mut candidates = IndexSet::new();
390
391        let token = context.token_to_complete;
392
393        for action in &self.actions {
394            match action {
395                CompleteAction::Alias => {
396                    for name in shell.aliases.keys() {
397                        if name.starts_with(token) {
398                            candidates.insert(name.clone());
399                        }
400                    }
401                }
402                CompleteAction::ArrayVar => {
403                    for (name, var) in shell.env.iter() {
404                        if var.value().is_array() && name.starts_with(token) {
405                            candidates.insert(name.to_owned());
406                        }
407                    }
408                }
409                CompleteAction::Binding => {
410                    tracing::debug!(target: trace_categories::COMPLETION, "unimplemented: complete -A binding");
411                }
412                CompleteAction::Builtin => {
413                    for name in shell.builtins().keys() {
414                        if name.starts_with(token) {
415                            candidates.insert(name.to_owned());
416                        }
417                    }
418                }
419                CompleteAction::Command => {
420                    let mut command_completions = get_command_completions(shell, context);
421                    candidates.append(&mut command_completions);
422                }
423                CompleteAction::Directory => {
424                    let mut file_completions =
425                        get_file_completions(shell, context.token_to_complete, true).await;
426                    candidates.append(&mut file_completions);
427                }
428                CompleteAction::Disabled => {
429                    for (name, registration) in shell.builtins() {
430                        if registration.disabled && name.starts_with(token) {
431                            candidates.insert(name.to_owned());
432                        }
433                    }
434                }
435                CompleteAction::Enabled => {
436                    for (name, registration) in shell.builtins() {
437                        if !registration.disabled && name.starts_with(token) {
438                            candidates.insert(name.to_owned());
439                        }
440                    }
441                }
442                CompleteAction::Export => {
443                    for (key, value) in shell.env.iter() {
444                        if value.is_exported() && key.starts_with(token) {
445                            candidates.insert(key.to_owned());
446                        }
447                    }
448                }
449                CompleteAction::File => {
450                    let mut file_completions =
451                        get_file_completions(shell, context.token_to_complete, false).await;
452                    candidates.append(&mut file_completions);
453                }
454                CompleteAction::Function => {
455                    for (name, _) in shell.funcs().iter() {
456                        candidates.insert(name.to_owned());
457                    }
458                }
459                CompleteAction::Group => {
460                    for group_name in users::get_all_groups()? {
461                        if group_name.starts_with(token) {
462                            candidates.insert(group_name);
463                        }
464                    }
465                }
466                CompleteAction::HelpTopic => {
467                    // For now, we only have help topics for built-in commands.
468                    for name in shell.builtins().keys() {
469                        if name.starts_with(token) {
470                            candidates.insert(name.to_owned());
471                        }
472                    }
473                }
474                CompleteAction::HostName => {
475                    // N.B. We only retrieve one hostname.
476                    if let Ok(name) = sys::network::get_hostname() {
477                        let name = name.to_string_lossy();
478                        if name.starts_with(token) {
479                            candidates.insert(name.to_string());
480                        }
481                    }
482                }
483                CompleteAction::Job => {
484                    for job in &shell.jobs.jobs {
485                        let command_name = job.command_name();
486                        if command_name.starts_with(token) {
487                            candidates.insert(command_name.to_owned());
488                        }
489                    }
490                }
491                CompleteAction::Keyword => {
492                    for keyword in shell.get_keywords() {
493                        if keyword.starts_with(token) {
494                            candidates.insert(keyword.clone());
495                        }
496                    }
497                }
498                CompleteAction::Running => {
499                    for job in &shell.jobs.jobs {
500                        if matches!(job.state, jobs::JobState::Running) {
501                            let command_name = job.command_name();
502                            if command_name.starts_with(token) {
503                                candidates.insert(command_name.to_owned());
504                            }
505                        }
506                    }
507                }
508                CompleteAction::Service => {
509                    tracing::debug!(target: trace_categories::COMPLETION, "unimplemented: complete -A service");
510                }
511                CompleteAction::SetOpt => {
512                    for option in namedoptions::options(namedoptions::ShellOptionKind::SetO).iter()
513                    {
514                        if option.name.starts_with(token) {
515                            candidates.insert(option.name.to_owned());
516                        }
517                    }
518                }
519                CompleteAction::ShOpt => {
520                    for option in namedoptions::options(namedoptions::ShellOptionKind::Shopt).iter()
521                    {
522                        if option.name.starts_with(token) {
523                            candidates.insert(option.name.to_owned());
524                        }
525                    }
526                }
527                CompleteAction::Signal => {
528                    for signal in traps::TrapSignal::iterator() {
529                        if signal.as_str().starts_with(token) {
530                            candidates.insert(signal.as_str().to_string());
531                        }
532                    }
533                }
534                CompleteAction::Stopped => {
535                    for job in &shell.jobs.jobs {
536                        if matches!(job.state, jobs::JobState::Stopped) {
537                            let command_name = job.command_name();
538                            if command_name.starts_with(token) {
539                                candidates.insert(job.command_name().to_owned());
540                            }
541                        }
542                    }
543                }
544                CompleteAction::User => {
545                    for user_name in users::get_all_users()? {
546                        if user_name.starts_with(token) {
547                            candidates.insert(user_name);
548                        }
549                    }
550                }
551                CompleteAction::Variable => {
552                    for (key, _) in shell.env.iter() {
553                        if key.starts_with(token) {
554                            candidates.insert(key.to_owned());
555                        }
556                    }
557                }
558            }
559        }
560
561        Ok(candidates)
562    }
563
564    async fn call_completion_command(
565        &self,
566        shell: &Shell,
567        command_name: &str,
568        context: &Context<'_>,
569    ) -> Result<IndexSet<String>, error::Error> {
570        // Move to a subshell so we can start filling out variables.
571        let mut shell = shell.clone();
572
573        let vars_and_values: Vec<(&str, ShellValueLiteral)> = vec![
574            ("COMP_LINE", context.input_line.into()),
575            ("COMP_POINT", context.cursor_index.to_string().into()),
576            // TODO: add COMP_KEY
577            // TODO: add COMP_TYPE
578        ];
579
580        // Fill out variables.
581        for (var, value) in vars_and_values {
582            shell.env.update_or_add(
583                var,
584                value,
585                |v| {
586                    v.export();
587                    Ok(())
588                },
589                env::EnvironmentLookup::Anywhere,
590                env::EnvironmentScope::Global,
591            )?;
592        }
593
594        // Compute args.
595        let mut args = vec![
596            context.command_name.unwrap_or(""),
597            context.token_to_complete,
598        ];
599        if let Some(preceding_token) = context.preceding_token {
600            args.push(preceding_token);
601        }
602
603        // Compose the full command line.
604        let mut command_line = command_name.to_owned();
605        for arg in args {
606            command_line.push(' ');
607
608            let escaped_arg = escape::quote_if_needed(arg, escape::QuoteMode::SingleQuote);
609            command_line.push_str(escaped_arg.as_ref());
610        }
611
612        // Run the command.
613        let params = shell.default_exec_params();
614        let output =
615            commands::invoke_command_in_subshell_and_get_output(&mut shell, &params, command_line)
616                .await?;
617
618        // Split results.
619        let mut candidates = IndexSet::new();
620        for line in output.lines() {
621            candidates.insert(line.to_owned());
622        }
623
624        Ok(candidates)
625    }
626
627    async fn call_completion_function(
628        &self,
629        shell: &mut Shell,
630        function_name: &str,
631        context: &Context<'_>,
632    ) -> Result<Answer, error::Error> {
633        // TODO: Don't pollute the persistent environment with these?
634        let vars_and_values: Vec<(&str, ShellValueLiteral)> = vec![
635            ("COMP_LINE", context.input_line.into()),
636            ("COMP_POINT", context.cursor_index.to_string().into()),
637            // TODO: add COMP_KEY
638            // TODO: add COMP_TYPE
639            (
640                "COMP_WORDS",
641                context
642                    .tokens
643                    .iter()
644                    .map(|t| t.to_str())
645                    .collect::<Vec<_>>()
646                    .into(),
647            ),
648            ("COMP_CWORD", context.token_index.to_string().into()),
649        ];
650
651        tracing::debug!(target: trace_categories::COMPLETION, "[calling completion func '{function_name}']: {}",
652            vars_and_values.iter().map(|(k, v)| std::format!("{k}={v}")).collect::<Vec<String>>().join(" "));
653
654        let mut vars_to_remove = vec![];
655        for (var, value) in vars_and_values {
656            shell.env.update_or_add(
657                var,
658                value,
659                |_| Ok(()),
660                env::EnvironmentLookup::Anywhere,
661                env::EnvironmentScope::Global,
662            )?;
663
664            vars_to_remove.push(var);
665        }
666
667        let mut args = vec![
668            context.command_name.unwrap_or(""),
669            context.token_to_complete,
670        ];
671        if let Some(preceding_token) = context.preceding_token {
672            args.push(preceding_token);
673        }
674
675        // TODO: Find a more appropriate interlock here. For now we use the existing
676        // handler depth count to suppress any debug traps.
677        shell.traps.handler_depth += 1;
678
679        let params = shell.default_exec_params();
680        let invoke_result = shell
681            .invoke_function(function_name, args.iter(), &params)
682            .await;
683        tracing::debug!(target: trace_categories::COMPLETION, "[completion function '{function_name}' returned: {invoke_result:?}]");
684
685        shell.traps.handler_depth -= 1;
686
687        // Make a best-effort attempt to unset the temporary variables.
688        for var_name in vars_to_remove {
689            let _ = shell.env.unset(var_name);
690        }
691
692        let result = invoke_result.unwrap_or_else(|e| {
693            tracing::warn!(target: trace_categories::COMPLETION, "error while running completion function '{function_name}': {e}");
694            1 // Report back a non-zero exit code.
695        });
696
697        // When the function returns the special value 124, then it's a request
698        // for us to restart the completion process.
699        if result == 124 {
700            Ok(Answer::RestartCompletionProcess)
701        } else {
702            if let Some(reply) = shell.env.unset("COMPREPLY")? {
703                tracing::debug!(target: trace_categories::COMPLETION, "[completion function yielded: {reply:?}]");
704
705                match reply.value() {
706                    variables::ShellValue::IndexedArray(values) => {
707                        return Ok(Answer::Candidates(
708                            values.values().map(|v| v.to_owned()).collect(),
709                            ProcessingOptions::default(),
710                        ));
711                    }
712                    variables::ShellValue::String(s) => {
713                        let mut candidates = IndexSet::new();
714                        candidates.insert(s.to_owned());
715
716                        return Ok(Answer::Candidates(candidates, ProcessingOptions::default()));
717                    }
718                    _ => (),
719                }
720            }
721
722            Ok(Answer::Candidates(
723                IndexSet::new(),
724                ProcessingOptions::default(),
725            ))
726        }
727    }
728}
729
730/// Represents a set of generated command completions.
731#[derive(Debug, Default)]
732pub struct Completions {
733    /// The index in the input line where the completions should be inserted. Represented
734    /// as a byte offset into the input line; must be at a clean character boundary.
735    pub insertion_index: usize,
736    /// The number of elements in the input line that should be removed before insertion.
737    /// Represented as a byte count; must capture an exact character boundary.
738    pub delete_count: usize,
739    /// The ordered set of completions.
740    pub candidates: IndexSet<String>,
741    /// Options for processing the candidates.
742    pub options: ProcessingOptions,
743}
744
745/// Options governing how command completion candidates are processed after being generated.
746#[derive(Debug)]
747pub struct ProcessingOptions {
748    /// Treat completions as file names.
749    pub treat_as_filenames: bool,
750    /// Don't auto-quote completions that are file names.
751    pub no_autoquote_filenames: bool,
752    /// Don't append a trailing space to completions at the end of the input line.
753    pub no_trailing_space_at_end_of_line: bool,
754}
755
756impl Default for ProcessingOptions {
757    fn default() -> Self {
758        Self {
759            treat_as_filenames: true,
760            no_autoquote_filenames: false,
761            no_trailing_space_at_end_of_line: false,
762        }
763    }
764}
765
766/// Encapsulates a completion answer.
767pub enum Answer {
768    /// The completion process generated a set of candidates along with options
769    /// controlling how to process them.
770    Candidates(IndexSet<String>, ProcessingOptions),
771    /// The completion process needs to be restarted.
772    RestartCompletionProcess,
773}
774
775const EMPTY_COMMAND: &str = "_EmptycmD_";
776const DEFAULT_COMMAND: &str = "_DefaultCmD_";
777const INITIAL_WORD: &str = "_InitialWorD_";
778
779impl Config {
780    /// Removes all registered completion specs.
781    pub fn clear(&mut self) {
782        self.commands.clear();
783        self.empty_line = None;
784        self.default = None;
785        self.initial_word = None;
786    }
787
788    /// Ensures the named completion spec is no longer registered; returns whether a
789    /// removal operation was required.
790    ///
791    /// # Arguments
792    ///
793    /// * `name` - The name of the completion spec to remove.
794    pub fn remove(&mut self, name: &str) -> bool {
795        match name {
796            EMPTY_COMMAND => {
797                let result = self.empty_line.is_some();
798                self.empty_line = None;
799                result
800            }
801            DEFAULT_COMMAND => {
802                let result = self.default.is_some();
803                self.default = None;
804                result
805            }
806            INITIAL_WORD => {
807                let result = self.initial_word.is_some();
808                self.initial_word = None;
809                result
810            }
811            _ => self.commands.remove(name).is_some(),
812        }
813    }
814
815    /// Returns an iterator over the completion specs.
816    pub fn iter(&self) -> impl Iterator<Item = (&String, &Spec)> {
817        self.commands.iter()
818    }
819
820    /// If present, returns the completion spec for the command of the given name.
821    ///
822    /// # Arguments
823    ///
824    /// * `name` - The name of the command.
825    pub fn get(&self, name: &str) -> Option<&Spec> {
826        match name {
827            EMPTY_COMMAND => self.empty_line.as_ref(),
828            DEFAULT_COMMAND => self.default.as_ref(),
829            INITIAL_WORD => self.initial_word.as_ref(),
830            _ => self.commands.get(name),
831        }
832    }
833
834    /// If present, sets the provided completion spec to be associated with the
835    /// command of the given name.
836    ///
837    /// # Arguments
838    ///
839    /// * `name` - The name of the command.
840    /// * `spec` - The completion spec to associate with the command.
841    pub fn set(&mut self, name: &str, spec: Spec) {
842        match name {
843            EMPTY_COMMAND => {
844                self.empty_line = Some(spec);
845            }
846            DEFAULT_COMMAND => {
847                self.default = Some(spec);
848            }
849            INITIAL_WORD => {
850                self.initial_word = Some(spec);
851            }
852            _ => {
853                self.commands.insert(name.to_owned(), spec);
854            }
855        }
856    }
857
858    /// Returns a mutable reference to the completion spec for the command of the
859    /// given name; if the command already was associated with a spec, returns
860    /// a reference to that existing spec. Otherwise registers a new default
861    /// spec and returns a mutable reference to it.
862    ///
863    /// # Arguments
864    ///
865    /// * `name` - The name of the command.
866    pub fn get_or_add_mut(&mut self, name: &str) -> &mut Spec {
867        match name {
868            EMPTY_COMMAND => {
869                if self.empty_line.is_none() {
870                    self.empty_line = Some(Spec::default());
871                }
872                self.empty_line.as_mut().unwrap()
873            }
874            DEFAULT_COMMAND => {
875                if self.default.is_none() {
876                    self.default = Some(Spec::default());
877                }
878                self.default.as_mut().unwrap()
879            }
880            INITIAL_WORD => {
881                if self.initial_word.is_none() {
882                    self.initial_word = Some(Spec::default());
883                }
884                self.initial_word.as_mut().unwrap()
885            }
886            _ => self.commands.entry(name.to_owned()).or_default(),
887        }
888    }
889
890    /// Generates completions for the given input line and cursor position.
891    ///
892    /// # Arguments
893    ///
894    /// * `shell` - The shell instance to use for completion generation.
895    /// * `input` - The input line for which completions are being generated.
896    /// * `position` - The 0-based index of the cursor in the input line.
897    #[expect(clippy::string_slice)]
898    pub async fn get_completions(
899        &self,
900        shell: &mut Shell,
901        input: &str,
902        position: usize,
903    ) -> Result<Completions, error::Error> {
904        const MAX_RESTARTS: u32 = 10;
905
906        // Make a best-effort attempt to tokenize.
907        let tokens = Self::tokenize_input_for_completion(shell, input);
908
909        let cursor = position;
910        let mut preceding_token = None;
911        let mut completion_prefix = "";
912        let mut insertion_index = cursor;
913        let mut completion_token_index = tokens.len();
914
915        // Copy a set of references to the tokens; we will adjust this list as
916        // we find we need to insert an empty token.
917        let mut adjusted_tokens: Vec<&brush_parser::Token> = tokens.iter().collect();
918
919        // Try to find which token we are in.
920        for (i, token) in tokens.iter().enumerate() {
921            // If the cursor is before the start of the token, then it's between
922            // this token and the one that preceded it (or it's before the first
923            // token if this is the first token).
924            if cursor < token.location().start.index {
925                // TODO: Should insert an empty token here; the position looks to have
926                // been between this token and the preceding one.
927                completion_token_index = i;
928                break;
929            }
930            // If the cursor is anywhere from the first char of the token up to
931            // (and including) the first char after the token, then this we need
932            // to generate completions to replace/update this token. We'll pay
933            // attention to the position to figure out the prefix that we should
934            // be completing.
935            else if cursor >= token.location().start.index && cursor <= token.location().end.index
936            {
937                // Update insertion index.
938                insertion_index = token.location().start.index;
939
940                // Update prefix.
941                let offset_into_token = cursor - insertion_index;
942                let token_str = token.to_str();
943                completion_prefix = &token_str[..offset_into_token];
944
945                // Update token index.
946                completion_token_index = i;
947
948                break;
949            }
950
951            // Otherwise, we need to keep looking. Update what we think the
952            // preceding token may be.
953            preceding_token = Some(token);
954        }
955
956        // If the position is after the last token, then we need to insert an empty
957        // token for the new token to be generated.
958        let empty_token =
959            brush_parser::Token::Word(String::new(), brush_parser::TokenLocation::default());
960        if completion_token_index == tokens.len() {
961            adjusted_tokens.push(&empty_token);
962        }
963
964        // Get the completions.
965        let mut result = Answer::RestartCompletionProcess;
966        let mut restart_count = 0;
967        while matches!(result, Answer::RestartCompletionProcess) {
968            if restart_count > MAX_RESTARTS {
969                tracing::warn!("possible infinite loop detected in completion process");
970                break;
971            }
972
973            let completion_context = Context {
974                token_to_complete: completion_prefix,
975                preceding_token: preceding_token.map(|t| t.to_str()),
976                command_name: adjusted_tokens.first().map(|token| token.to_str()),
977                input_line: input,
978                token_index: completion_token_index,
979                tokens: adjusted_tokens.as_slice(),
980                cursor_index: position,
981            };
982
983            result = self
984                .get_completions_for_token(shell, completion_context)
985                .await;
986
987            restart_count += 1;
988        }
989
990        match result {
991            Answer::Candidates(candidates, options) => Ok(Completions {
992                insertion_index,
993                delete_count: completion_prefix.len(),
994                candidates,
995                options,
996            }),
997            Answer::RestartCompletionProcess => Ok(Completions {
998                insertion_index,
999                delete_count: 0,
1000                candidates: IndexSet::new(),
1001                options: ProcessingOptions::default(),
1002            }),
1003        }
1004    }
1005
1006    fn tokenize_input_for_completion(shell: &Shell, input: &str) -> Vec<brush_parser::Token> {
1007        const FALLBACK: &str = " \t\n\"\'@><=;|&(:";
1008
1009        let delimiter_str = shell
1010            .env_str("COMP_WORDBREAKS")
1011            .unwrap_or_else(|| FALLBACK.into());
1012
1013        let delimiters: Vec<_> = delimiter_str.chars().collect();
1014
1015        simple_tokenize_by_delimiters(input, delimiters.as_slice())
1016    }
1017
1018    async fn get_completions_for_token(&self, shell: &mut Shell, context: Context<'_>) -> Answer {
1019        // See if we can find a completion spec matching the current command.
1020        let mut found_spec: Option<&Spec> = None;
1021
1022        if let Some(command_name) = context.command_name {
1023            if context.token_index == 0 {
1024                if let Some(spec) = &self.initial_word {
1025                    found_spec = Some(spec);
1026                }
1027            } else {
1028                if let Some(spec) = shell.completion_config.commands.get(command_name) {
1029                    found_spec = Some(spec);
1030                } else if let Some(file_name) = PathBuf::from(command_name).file_name() {
1031                    if let Some(spec) = shell
1032                        .completion_config
1033                        .commands
1034                        .get(&file_name.to_string_lossy().to_string())
1035                    {
1036                        found_spec = Some(spec);
1037                    }
1038                }
1039
1040                if found_spec.is_none() {
1041                    if let Some(spec) = &self.default {
1042                        found_spec = Some(spec);
1043                    }
1044                }
1045            }
1046        } else {
1047            if let Some(spec) = &self.empty_line {
1048                found_spec = Some(spec);
1049            }
1050        }
1051
1052        // Try to generate completions.
1053        if let Some(spec) = found_spec {
1054            spec.to_owned()
1055                .get_completions(shell, &context)
1056                .await
1057                .unwrap_or_else(|_err| {
1058                    Answer::Candidates(IndexSet::new(), ProcessingOptions::default())
1059                })
1060        } else {
1061            // If we didn't find a spec, then fall back to basic completion.
1062            get_completions_using_basic_lookup(shell, &context).await
1063        }
1064    }
1065}
1066
1067async fn get_file_completions(
1068    shell: &Shell,
1069    token_to_complete: &str,
1070    must_be_dir: bool,
1071) -> IndexSet<String> {
1072    // Basic-expand the token-to-be-completed; it won't have been expanded to this point.
1073    let mut throwaway_shell = shell.clone();
1074    let params = throwaway_shell.default_exec_params();
1075    let expanded_token = throwaway_shell
1076        .basic_expand_string(&params, token_to_complete)
1077        .await
1078        .unwrap_or_else(|_err| token_to_complete.to_owned());
1079
1080    let glob = std::format!("{expanded_token}*");
1081
1082    let path_filter = |path: &Path| !must_be_dir || shell.absolute_path(path).is_dir();
1083
1084    let pattern = patterns::Pattern::from(glob)
1085        .set_extended_globbing(shell.options.extended_globbing)
1086        .set_case_insensitive(shell.options.case_insensitive_pathname_expansion);
1087
1088    pattern
1089        .expand(
1090            shell.working_dir(),
1091            Some(&path_filter),
1092            &patterns::FilenameExpansionOptions::default(),
1093        )
1094        .unwrap_or_default()
1095        .into_iter()
1096        .collect()
1097}
1098
1099fn get_command_completions(shell: &Shell, context: &Context<'_>) -> IndexSet<String> {
1100    let mut candidates = IndexSet::new();
1101
1102    // Look for external commands.
1103    for path in shell.find_executables_in_path_with_prefix(
1104        context.token_to_complete,
1105        shell.options.case_insensitive_pathname_expansion,
1106    ) {
1107        if let Some(file_name) = path.file_name() {
1108            candidates.insert(file_name.to_string_lossy().to_string());
1109        }
1110    }
1111
1112    candidates.into_iter().collect()
1113}
1114
1115async fn get_completions_using_basic_lookup(shell: &Shell, context: &Context<'_>) -> Answer {
1116    let mut candidates = get_file_completions(shell, context.token_to_complete, false).await;
1117
1118    // If this appears to be the command token (and if there's *some* prefix without
1119    // a path separator) then also consider whether we should search the path for
1120    // completions too.
1121    // TODO: Do a better job than just checking if index == 0.
1122    if context.token_index == 0
1123        && !context.token_to_complete.is_empty()
1124        && !context
1125            .token_to_complete
1126            .contains(std::path::MAIN_SEPARATOR)
1127    {
1128        // Add external commands.
1129        let mut command_completions = get_command_completions(shell, context);
1130        candidates.append(&mut command_completions);
1131
1132        // Add built-in commands.
1133        for (name, registration) in shell.builtins() {
1134            if !registration.disabled && name.starts_with(context.token_to_complete) {
1135                candidates.insert(name.to_owned());
1136            }
1137        }
1138
1139        // Add shell functions.
1140        for (name, _) in shell.funcs().iter() {
1141            if name.starts_with(context.token_to_complete) {
1142                candidates.insert(name.to_owned());
1143            }
1144        }
1145
1146        // Add aliases.
1147        for name in shell.aliases.keys() {
1148            if name.starts_with(context.token_to_complete) {
1149                candidates.insert(name.to_owned());
1150            }
1151        }
1152
1153        // Add keywords.
1154        for keyword in shell.get_keywords() {
1155            if keyword.starts_with(context.token_to_complete) {
1156                candidates.insert(keyword.clone());
1157            }
1158        }
1159
1160        // Sort.
1161        candidates.sort();
1162    }
1163
1164    #[cfg(windows)]
1165    {
1166        candidates = candidates
1167            .into_iter()
1168            .map(|c| c.replace('\\', "/"))
1169            .collect();
1170    }
1171
1172    Answer::Candidates(candidates, ProcessingOptions::default())
1173}
1174
1175fn simple_tokenize_by_delimiters(input: &str, delimiters: &[char]) -> Vec<brush_parser::Token> {
1176    //
1177    // This is an overly naive tokenization.
1178    //
1179
1180    let mut tokens = vec![];
1181    let mut start = 0;
1182
1183    for piece in input.split_inclusive(delimiters) {
1184        let next_start = start + piece.len();
1185
1186        let piece = piece.strip_suffix(delimiters).unwrap_or(piece);
1187        let end = start + piece.len();
1188        tokens.push(brush_parser::Token::Word(
1189            piece.to_string(),
1190            brush_parser::TokenLocation {
1191                start: brush_parser::SourcePosition {
1192                    index: start,
1193                    line: 1,
1194                    column: start + 1,
1195                }
1196                .into(),
1197                end: brush_parser::SourcePosition {
1198                    index: end,
1199                    line: 1,
1200                    column: end + 1,
1201                }
1202                .into(),
1203            },
1204        ));
1205
1206        start = next_start;
1207    }
1208
1209    tokens
1210}
1211
1212fn completion_filter_pattern_matches(
1213    pattern: &str,
1214    candidate: &str,
1215    token_being_completed: &str,
1216    shell: &Shell,
1217) -> Result<bool, error::Error> {
1218    let pattern = replace_unescaped_ampersands(pattern, token_being_completed);
1219
1220    //
1221    // TODO: Replace unescaped '&' with the word being completed.
1222    //
1223
1224    let pattern = patterns::Pattern::from(pattern.as_ref())
1225        .set_extended_globbing(shell.options.extended_globbing)
1226        .set_case_insensitive(shell.options.case_insensitive_pathname_expansion);
1227
1228    let matches = pattern.exactly_matches(candidate)?;
1229
1230    Ok(matches)
1231}
1232
1233fn replace_unescaped_ampersands<'a>(pattern: &'a str, replacement: &str) -> Cow<'a, str> {
1234    let mut in_escape = false;
1235    let mut insertion_points = vec![];
1236
1237    for (i, c) in pattern.char_indices() {
1238        if !in_escape && c == '&' {
1239            insertion_points.push(i);
1240        }
1241        in_escape = !in_escape && c == '\\';
1242    }
1243
1244    if insertion_points.is_empty() {
1245        return pattern.into();
1246    }
1247
1248    let mut result = pattern.to_owned();
1249    for i in insertion_points.iter().rev() {
1250        result.replace_range(*i..=*i, replacement);
1251    }
1252
1253    result.into()
1254}