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    #[allow(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.as_path(),
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    #[allow(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.to_string());
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.get_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.get_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 (name, _) in namedoptions::SET_O_OPTIONS.iter() {
513                        if name.starts_with(token) {
514                            candidates.insert((*name).to_owned());
515                        }
516                    }
517                }
518                CompleteAction::ShOpt => {
519                    for (name, _) in namedoptions::SHOPT_OPTIONS.iter() {
520                        if name.starts_with(token) {
521                            candidates.insert((*name).to_owned());
522                        }
523                    }
524                }
525                CompleteAction::Signal => {
526                    for signal in traps::TrapSignal::iterator() {
527                        if signal.as_str().starts_with(token) {
528                            candidates.insert(signal.as_str().to_string());
529                        }
530                    }
531                }
532                CompleteAction::Stopped => {
533                    for job in &shell.jobs.jobs {
534                        if matches!(job.state, jobs::JobState::Stopped) {
535                            let command_name = job.get_command_name();
536                            if command_name.starts_with(token) {
537                                candidates.insert(job.get_command_name().to_owned());
538                            }
539                        }
540                    }
541                }
542                CompleteAction::User => {
543                    for user_name in users::get_all_users()? {
544                        if user_name.starts_with(token) {
545                            candidates.insert(user_name);
546                        }
547                    }
548                }
549                CompleteAction::Variable => {
550                    for (key, _) in shell.env.iter() {
551                        if key.starts_with(token) {
552                            candidates.insert(key.to_owned());
553                        }
554                    }
555                }
556            }
557        }
558
559        Ok(candidates)
560    }
561
562    async fn call_completion_command(
563        &self,
564        shell: &Shell,
565        command_name: &str,
566        context: &Context<'_>,
567    ) -> Result<IndexSet<String>, error::Error> {
568        // Move to a subshell so we can start filling out variables.
569        let mut shell = shell.clone();
570
571        let vars_and_values: Vec<(&str, ShellValueLiteral)> = vec![
572            ("COMP_LINE", context.input_line.into()),
573            ("COMP_POINT", context.cursor_index.to_string().into()),
574            // TODO: add COMP_KEY
575            // TODO: add COMP_TYPE
576        ];
577
578        // Fill out variables.
579        for (var, value) in vars_and_values {
580            shell.env.update_or_add(
581                var,
582                value,
583                |v| {
584                    v.export();
585                    Ok(())
586                },
587                env::EnvironmentLookup::Anywhere,
588                env::EnvironmentScope::Global,
589            )?;
590        }
591
592        // Compute args.
593        let mut args = vec![
594            context.command_name.unwrap_or(""),
595            context.token_to_complete,
596        ];
597        if let Some(preceding_token) = context.preceding_token {
598            args.push(preceding_token);
599        }
600
601        // Compose the full command line.
602        let mut command_line = command_name.to_owned();
603        for arg in args {
604            command_line.push(' ');
605
606            let escaped_arg = escape::quote_if_needed(arg, escape::QuoteMode::SingleQuote);
607            command_line.push_str(escaped_arg.as_ref());
608        }
609
610        // Run the command.
611        let params = shell.default_exec_params();
612        let output =
613            commands::invoke_command_in_subshell_and_get_output(&mut shell, &params, command_line)
614                .await?;
615
616        // Split results.
617        let mut candidates = IndexSet::new();
618        for line in output.lines() {
619            candidates.insert(line.to_owned());
620        }
621
622        Ok(candidates)
623    }
624
625    async fn call_completion_function(
626        &self,
627        shell: &mut Shell,
628        function_name: &str,
629        context: &Context<'_>,
630    ) -> Result<Answer, error::Error> {
631        // TODO: Don't pollute the persistent environment with these?
632        let vars_and_values: Vec<(&str, ShellValueLiteral)> = vec![
633            ("COMP_LINE", context.input_line.into()),
634            ("COMP_POINT", context.cursor_index.to_string().into()),
635            // TODO: add COMP_KEY
636            // TODO: add COMP_TYPE
637            (
638                "COMP_WORDS",
639                context
640                    .tokens
641                    .iter()
642                    .map(|t| t.to_str())
643                    .collect::<Vec<_>>()
644                    .into(),
645            ),
646            ("COMP_CWORD", context.token_index.to_string().into()),
647        ];
648
649        tracing::debug!(target: trace_categories::COMPLETION, "[calling completion func '{function_name}']: {}",
650            vars_and_values.iter().map(|(k, v)| std::format!("{k}={v}")).collect::<Vec<String>>().join(" "));
651
652        let mut vars_to_remove = vec![];
653        for (var, value) in vars_and_values {
654            shell.env.update_or_add(
655                var,
656                value,
657                |_| Ok(()),
658                env::EnvironmentLookup::Anywhere,
659                env::EnvironmentScope::Global,
660            )?;
661
662            vars_to_remove.push(var);
663        }
664
665        let mut args = vec![
666            context.command_name.unwrap_or(""),
667            context.token_to_complete,
668        ];
669        if let Some(preceding_token) = context.preceding_token {
670            args.push(preceding_token);
671        }
672
673        // TODO: Find a more appropriate interlock here. For now we use the existing
674        // handler depth count to suppress any debug traps.
675        shell.traps.handler_depth += 1;
676
677        let invoke_result = shell.invoke_function(function_name, &args).await;
678        tracing::debug!(target: trace_categories::COMPLETION, "[completion function '{function_name}' returned: {invoke_result:?}]");
679
680        shell.traps.handler_depth -= 1;
681
682        // Make a best-effort attempt to unset the temporary variables.
683        for var_name in vars_to_remove {
684            let _ = shell.env.unset(var_name);
685        }
686
687        let result = invoke_result.unwrap_or_else(|e| {
688            tracing::warn!(target: trace_categories::COMPLETION, "error while running completion function '{function_name}': {e}");
689            1 // Report back a non-zero exit code.
690        });
691
692        // When the function returns the special value 124, then it's a request
693        // for us to restart the completion process.
694        if result == 124 {
695            Ok(Answer::RestartCompletionProcess)
696        } else {
697            if let Some(reply) = shell.env.unset("COMPREPLY")? {
698                tracing::debug!(target: trace_categories::COMPLETION, "[completion function yielded: {reply:?}]");
699
700                match reply.value() {
701                    variables::ShellValue::IndexedArray(values) => {
702                        return Ok(Answer::Candidates(
703                            values.values().map(|v| v.to_owned()).collect(),
704                            ProcessingOptions::default(),
705                        ));
706                    }
707                    variables::ShellValue::String(s) => {
708                        let mut candidates = IndexSet::new();
709                        candidates.insert(s.to_owned());
710
711                        return Ok(Answer::Candidates(candidates, ProcessingOptions::default()));
712                    }
713                    _ => (),
714                }
715            }
716
717            Ok(Answer::Candidates(
718                IndexSet::new(),
719                ProcessingOptions::default(),
720            ))
721        }
722    }
723}
724
725/// Represents a set of generated command completions.
726#[derive(Debug, Default)]
727pub struct Completions {
728    /// The index in the input line where the completions should be inserted.
729    pub insertion_index: usize,
730    /// The number of chars in the input line that should be removed before insertion.
731    pub delete_count: usize,
732    /// The ordered set of completions.
733    pub candidates: IndexSet<String>,
734    /// Options for processing the candidates.
735    pub options: ProcessingOptions,
736}
737
738/// Options governing how command completion candidates are processed after being generated.
739#[derive(Debug)]
740pub struct ProcessingOptions {
741    /// Treat completions as file names.
742    pub treat_as_filenames: bool,
743    /// Don't auto-quote completions that are file names.
744    pub no_autoquote_filenames: bool,
745    /// Don't append a trailing space to completions at the end of the input line.
746    pub no_trailing_space_at_end_of_line: bool,
747}
748
749impl Default for ProcessingOptions {
750    fn default() -> Self {
751        Self {
752            treat_as_filenames: true,
753            no_autoquote_filenames: false,
754            no_trailing_space_at_end_of_line: false,
755        }
756    }
757}
758
759/// Encapsulates a completion answer.
760pub enum Answer {
761    /// The completion process generated a set of candidates along with options
762    /// controlling how to process them.
763    Candidates(IndexSet<String>, ProcessingOptions),
764    /// The completion process needs to be restarted.
765    RestartCompletionProcess,
766}
767
768const EMPTY_COMMAND: &str = "_EmptycmD_";
769const DEFAULT_COMMAND: &str = "_DefaultCmD_";
770const INITIAL_WORD: &str = "_InitialWorD_";
771
772impl Config {
773    /// Removes all registered completion specs.
774    pub fn clear(&mut self) {
775        self.commands.clear();
776        self.empty_line = None;
777        self.default = None;
778        self.initial_word = None;
779    }
780
781    /// Ensures the named completion spec is no longer registered; returns whether a
782    /// removal operation was required.
783    ///
784    /// # Arguments
785    ///
786    /// * `name` - The name of the completion spec to remove.
787    pub fn remove(&mut self, name: &str) -> bool {
788        match name {
789            EMPTY_COMMAND => {
790                let result = self.empty_line.is_some();
791                self.empty_line = None;
792                result
793            }
794            DEFAULT_COMMAND => {
795                let result = self.default.is_some();
796                self.default = None;
797                result
798            }
799            INITIAL_WORD => {
800                let result = self.initial_word.is_some();
801                self.initial_word = None;
802                result
803            }
804            _ => self.commands.remove(name).is_some(),
805        }
806    }
807
808    /// Returns an iterator over the completion specs.
809    pub fn iter(&self) -> impl Iterator<Item = (&String, &Spec)> {
810        self.commands.iter()
811    }
812
813    /// If present, returns the completion spec for the command of the given name.
814    ///
815    /// # Arguments
816    ///
817    /// * `name` - The name of the command.
818    pub fn get(&self, name: &str) -> Option<&Spec> {
819        match name {
820            EMPTY_COMMAND => self.empty_line.as_ref(),
821            DEFAULT_COMMAND => self.default.as_ref(),
822            INITIAL_WORD => self.initial_word.as_ref(),
823            _ => self.commands.get(name),
824        }
825    }
826
827    /// If present, sets the provided completion spec to be associated with the
828    /// command of the given name.
829    ///
830    /// # Arguments
831    ///
832    /// * `name` - The name of the command.
833    /// * `spec` - The completion spec to associate with the command.
834    pub fn set(&mut self, name: &str, spec: Spec) {
835        match name {
836            EMPTY_COMMAND => {
837                self.empty_line = Some(spec);
838            }
839            DEFAULT_COMMAND => {
840                self.default = Some(spec);
841            }
842            INITIAL_WORD => {
843                self.initial_word = Some(spec);
844            }
845            _ => {
846                self.commands.insert(name.to_owned(), spec);
847            }
848        }
849    }
850
851    /// Returns a mutable reference to the completion spec for the command of the
852    /// given name; if the command already was associated with a spec, returns
853    /// a reference to that existing spec. Otherwise registers a new default
854    /// spec and returns a mutable reference to it.
855    ///
856    /// # Arguments
857    ///
858    /// * `name` - The name of the command.
859    pub fn get_or_add_mut(&mut self, name: &str) -> &mut Spec {
860        match name {
861            EMPTY_COMMAND => {
862                if self.empty_line.is_none() {
863                    self.empty_line = Some(Spec::default());
864                }
865                self.empty_line.as_mut().unwrap()
866            }
867            DEFAULT_COMMAND => {
868                if self.default.is_none() {
869                    self.default = Some(Spec::default());
870                }
871                self.default.as_mut().unwrap()
872            }
873            INITIAL_WORD => {
874                if self.initial_word.is_none() {
875                    self.initial_word = Some(Spec::default());
876                }
877                self.initial_word.as_mut().unwrap()
878            }
879            _ => self.commands.entry(name.to_owned()).or_default(),
880        }
881    }
882
883    /// Generates completions for the given input line and cursor position.
884    ///
885    /// # Arguments
886    ///
887    /// * `shell` - The shell instance to use for completion generation.
888    /// * `input` - The input line for which completions are being generated.
889    /// * `position` - The 0-based index of the cursor in the input line.
890    #[allow(clippy::cast_sign_loss)]
891    pub async fn get_completions(
892        &self,
893        shell: &mut Shell,
894        input: &str,
895        position: usize,
896    ) -> Result<Completions, error::Error> {
897        const MAX_RESTARTS: u32 = 10;
898
899        // Make a best-effort attempt to tokenize.
900        let tokens = Self::tokenize_input_for_completion(shell, input);
901
902        let cursor = i32::try_from(position)?;
903        let mut preceding_token = None;
904        let mut completion_prefix = "";
905        let mut insertion_index = cursor;
906        let mut completion_token_index = tokens.len();
907
908        // Copy a set of references to the tokens; we will adjust this list as
909        // we find we need to insert an empty token.
910        let mut adjusted_tokens: Vec<&brush_parser::Token> = tokens.iter().collect();
911
912        // Try to find which token we are in.
913        for (i, token) in tokens.iter().enumerate() {
914            // If the cursor is before the start of the token, then it's between
915            // this token and the one that preceded it (or it's before the first
916            // token if this is the first token).
917            if cursor < token.location().start.index {
918                // TODO: Should insert an empty token here; the position looks to have
919                // been between this token and the preceding one.
920                completion_token_index = i;
921                break;
922            }
923            // If the cursor is anywhere from the first char of the token up to
924            // (and including) the first char after the token, then this we need
925            // to generate completions to replace/update this token. We'll pay
926            // attention to the position to figure out the prefix that we should
927            // be completing.
928            else if cursor >= token.location().start.index && cursor <= token.location().end.index
929            {
930                // Update insertion index.
931                insertion_index = token.location().start.index;
932
933                // Update prefix.
934                let offset_into_token = (cursor - insertion_index) as usize;
935                let token_str = token.to_str();
936                completion_prefix = &token_str[..offset_into_token];
937
938                // Update token index.
939                completion_token_index = i;
940
941                break;
942            }
943
944            // Otherwise, we need to keep looking. Update what we think the
945            // preceding token may be.
946            preceding_token = Some(token);
947        }
948
949        // If the position is after the last token, then we need to insert an empty
950        // token for the new token to be generated.
951        let empty_token =
952            brush_parser::Token::Word(String::new(), brush_parser::TokenLocation::default());
953        if completion_token_index == tokens.len() {
954            adjusted_tokens.push(&empty_token);
955        }
956
957        // Get the completions.
958        let mut result = Answer::RestartCompletionProcess;
959        let mut restart_count = 0;
960        while matches!(result, Answer::RestartCompletionProcess) {
961            if restart_count > MAX_RESTARTS {
962                tracing::error!("possible infinite loop detected in completion process");
963                break;
964            }
965
966            let completion_context = Context {
967                token_to_complete: completion_prefix,
968                preceding_token: preceding_token.map(|t| t.to_str()),
969                command_name: adjusted_tokens.first().map(|token| token.to_str()),
970                input_line: input,
971                token_index: completion_token_index,
972                tokens: adjusted_tokens.as_slice(),
973                cursor_index: position,
974            };
975
976            result = self
977                .get_completions_for_token(shell, completion_context)
978                .await;
979
980            restart_count += 1;
981        }
982
983        match result {
984            Answer::Candidates(candidates, options) => Ok(Completions {
985                insertion_index: insertion_index as usize,
986                delete_count: completion_prefix.len(),
987                candidates,
988                options,
989            }),
990            Answer::RestartCompletionProcess => Ok(Completions {
991                insertion_index: insertion_index as usize,
992                delete_count: 0,
993                candidates: IndexSet::new(),
994                options: ProcessingOptions::default(),
995            }),
996        }
997    }
998
999    fn tokenize_input_for_completion(shell: &Shell, input: &str) -> Vec<brush_parser::Token> {
1000        const FALLBACK: &str = " \t\n\"\'@><=;|&(:";
1001
1002        let delimiter_str = shell
1003            .get_env_str("COMP_WORDBREAKS")
1004            .unwrap_or_else(|| FALLBACK.into());
1005
1006        let delimiters: Vec<_> = delimiter_str.chars().collect();
1007
1008        simple_tokenize_by_delimiters(input, delimiters.as_slice())
1009    }
1010
1011    async fn get_completions_for_token(&self, shell: &mut Shell, context: Context<'_>) -> Answer {
1012        // See if we can find a completion spec matching the current command.
1013        let mut found_spec: Option<&Spec> = None;
1014
1015        if let Some(command_name) = context.command_name {
1016            if context.token_index == 0 {
1017                if let Some(spec) = &self.initial_word {
1018                    found_spec = Some(spec);
1019                }
1020            } else {
1021                if let Some(spec) = shell.completion_config.commands.get(command_name) {
1022                    found_spec = Some(spec);
1023                } else if let Some(file_name) = PathBuf::from(command_name).file_name() {
1024                    if let Some(spec) = shell
1025                        .completion_config
1026                        .commands
1027                        .get(&file_name.to_string_lossy().to_string())
1028                    {
1029                        found_spec = Some(spec);
1030                    }
1031                }
1032
1033                if found_spec.is_none() {
1034                    if let Some(spec) = &self.default {
1035                        found_spec = Some(spec);
1036                    }
1037                }
1038            }
1039        } else {
1040            if let Some(spec) = &self.empty_line {
1041                found_spec = Some(spec);
1042            }
1043        }
1044
1045        // Try to generate completions.
1046        if let Some(spec) = found_spec {
1047            spec.to_owned()
1048                .get_completions(shell, &context)
1049                .await
1050                .unwrap_or_else(|_err| {
1051                    Answer::Candidates(IndexSet::new(), ProcessingOptions::default())
1052                })
1053        } else {
1054            // If we didn't find a spec, then fall back to basic completion.
1055            get_completions_using_basic_lookup(shell, &context).await
1056        }
1057    }
1058}
1059
1060async fn get_file_completions(
1061    shell: &Shell,
1062    token_to_complete: &str,
1063    must_be_dir: bool,
1064) -> IndexSet<String> {
1065    // Basic-expand the token-to-be-completed; it won't have been expanded to this point.
1066    let mut throwaway_shell = shell.clone();
1067    let params = throwaway_shell.default_exec_params();
1068    let expanded_token = throwaway_shell
1069        .basic_expand_string(&params, token_to_complete)
1070        .await
1071        .unwrap_or_else(|_err| token_to_complete.to_owned());
1072
1073    let glob = std::format!("{expanded_token}*");
1074
1075    let path_filter = |path: &Path| !must_be_dir || shell.get_absolute_path(path).is_dir();
1076
1077    let pattern = patterns::Pattern::from(glob)
1078        .set_extended_globbing(shell.options.extended_globbing)
1079        .set_case_insensitive(shell.options.case_insensitive_pathname_expansion);
1080
1081    pattern
1082        .expand(
1083            shell.working_dir.as_path(),
1084            Some(&path_filter),
1085            &patterns::FilenameExpansionOptions::default(),
1086        )
1087        .unwrap_or_default()
1088        .into_iter()
1089        .collect()
1090}
1091
1092fn get_command_completions(shell: &Shell, context: &Context<'_>) -> IndexSet<String> {
1093    let mut candidates = IndexSet::new();
1094
1095    // Look for external commands.
1096    for path in shell.find_executables_in_path_with_prefix(
1097        context.token_to_complete,
1098        shell.options.case_insensitive_pathname_expansion,
1099    ) {
1100        if let Some(file_name) = path.file_name() {
1101            candidates.insert(file_name.to_string_lossy().to_string());
1102        }
1103    }
1104
1105    candidates.into_iter().collect()
1106}
1107
1108async fn get_completions_using_basic_lookup(shell: &Shell, context: &Context<'_>) -> Answer {
1109    let mut candidates = get_file_completions(shell, context.token_to_complete, false).await;
1110
1111    // If this appears to be the command token (and if there's *some* prefix without
1112    // a path separator) then also consider whether we should search the path for
1113    // completions too.
1114    // TODO: Do a better job than just checking if index == 0.
1115    if context.token_index == 0
1116        && !context.token_to_complete.is_empty()
1117        && !context
1118            .token_to_complete
1119            .contains(std::path::MAIN_SEPARATOR)
1120    {
1121        // Add external commands.
1122        let mut command_completions = get_command_completions(shell, context);
1123        candidates.append(&mut command_completions);
1124
1125        // Add built-in commands.
1126        for (name, registration) in &shell.builtins {
1127            if !registration.disabled && name.starts_with(context.token_to_complete) {
1128                candidates.insert(name.to_owned());
1129            }
1130        }
1131
1132        // Add shell functions.
1133        for (name, _) in shell.funcs.iter() {
1134            if name.starts_with(context.token_to_complete) {
1135                candidates.insert(name.to_owned());
1136            }
1137        }
1138
1139        // Add aliases.
1140        for name in shell.aliases.keys() {
1141            if name.starts_with(context.token_to_complete) {
1142                candidates.insert(name.to_owned());
1143            }
1144        }
1145
1146        // Add keywords.
1147        for keyword in shell.get_keywords() {
1148            if keyword.starts_with(context.token_to_complete) {
1149                candidates.insert(keyword.clone());
1150            }
1151        }
1152
1153        // Sort.
1154        candidates.sort();
1155    }
1156
1157    #[cfg(windows)]
1158    {
1159        candidates = candidates
1160            .into_iter()
1161            .map(|c| c.replace('\\', "/"))
1162            .collect();
1163    }
1164
1165    Answer::Candidates(candidates, ProcessingOptions::default())
1166}
1167
1168#[allow(clippy::cast_possible_truncation)]
1169#[allow(clippy::cast_possible_wrap)]
1170fn simple_tokenize_by_delimiters(input: &str, delimiters: &[char]) -> Vec<brush_parser::Token> {
1171    //
1172    // This is an overly naive tokenization.
1173    //
1174
1175    let mut tokens = vec![];
1176    let mut start: i32 = 0;
1177
1178    for piece in input.split_inclusive(delimiters) {
1179        let next_start = start + piece.len() as i32;
1180
1181        let piece = piece.strip_suffix(delimiters).unwrap_or(piece);
1182        let end: i32 = start + piece.len() as i32;
1183        tokens.push(brush_parser::Token::Word(
1184            piece.to_string(),
1185            brush_parser::TokenLocation {
1186                start: brush_parser::SourcePosition {
1187                    index: start,
1188                    line: 1,
1189                    column: start + 1,
1190                },
1191                end: brush_parser::SourcePosition {
1192                    index: end,
1193                    line: 1,
1194                    column: end + 1,
1195                },
1196            },
1197        ));
1198
1199        start = next_start;
1200    }
1201
1202    tokens
1203}
1204
1205fn completion_filter_pattern_matches(
1206    pattern: &str,
1207    candidate: &str,
1208    token_being_completed: &str,
1209    shell: &Shell,
1210) -> Result<bool, error::Error> {
1211    let pattern = replace_unescaped_ampersands(pattern, token_being_completed);
1212
1213    //
1214    // TODO: Replace unescaped '&' with the word being completed.
1215    //
1216
1217    let pattern = patterns::Pattern::from(pattern.as_ref())
1218        .set_extended_globbing(shell.options.extended_globbing)
1219        .set_case_insensitive(shell.options.case_insensitive_pathname_expansion);
1220
1221    let matches = pattern.exactly_matches(candidate)?;
1222
1223    Ok(matches)
1224}
1225
1226fn replace_unescaped_ampersands<'a>(pattern: &'a str, replacement: &str) -> Cow<'a, str> {
1227    let mut in_escape = false;
1228    let mut insertion_points = vec![];
1229
1230    for (i, c) in pattern.char_indices() {
1231        if !in_escape && c == '&' {
1232            insertion_points.push(i);
1233        }
1234        in_escape = !in_escape && c == '\\';
1235    }
1236
1237    if insertion_points.is_empty() {
1238        return pattern.into();
1239    }
1240
1241    let mut result = pattern.to_owned();
1242    for i in insertion_points.iter().rev() {
1243        result.replace_range(*i..=*i, replacement);
1244    }
1245
1246    result.into()
1247}