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