1use 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#[derive(Clone, Debug, ValueEnum)]
20pub enum CompleteAction {
21 #[clap(name = "alias")]
23 Alias,
24 #[clap(name = "arrayvar")]
26 ArrayVar,
27 #[clap(name = "binding")]
29 Binding,
30 #[clap(name = "builtin")]
32 Builtin,
33 #[clap(name = "command")]
35 Command,
36 #[clap(name = "directory")]
38 Directory,
39 #[clap(name = "disabled")]
41 Disabled,
42 #[clap(name = "enabled")]
44 Enabled,
45 #[clap(name = "export")]
47 Export,
48 #[clap(name = "file")]
50 File,
51 #[clap(name = "function")]
53 Function,
54 #[clap(name = "group")]
56 Group,
57 #[clap(name = "helptopic")]
59 HelpTopic,
60 #[clap(name = "hostname")]
62 HostName,
63 #[clap(name = "job")]
65 Job,
66 #[clap(name = "keyword")]
68 Keyword,
69 #[clap(name = "running")]
71 Running,
72 #[clap(name = "service")]
74 Service,
75 #[clap(name = "setopt")]
77 SetOpt,
78 #[clap(name = "shopt")]
80 ShOpt,
81 #[clap(name = "signal")]
83 Signal,
84 #[clap(name = "stopped")]
86 Stopped,
87 #[clap(name = "user")]
89 User,
90 #[clap(name = "variable")]
92 Variable,
93}
94
95#[derive(Clone, Debug, Eq, Hash, PartialEq, ValueEnum)]
97pub enum CompleteOption {
98 #[clap(name = "bashdefault")]
100 BashDefault,
101 #[clap(name = "default")]
103 Default,
104 #[clap(name = "dirnames")]
106 DirNames,
107 #[clap(name = "filenames")]
109 FileNames,
110 #[clap(name = "noquote")]
112 NoQuote,
113 #[clap(name = "nosort")]
115 NoSort,
116 #[clap(name = "nospace")]
118 NoSpace,
119 #[clap(name = "plusdirs")]
121 PlusDirs,
122}
123
124#[derive(Clone, Default)]
126pub struct Config {
127 commands: HashMap<String, Spec>,
128
129 pub default: Option<Spec>,
132 pub empty_line: Option<Spec>,
134 pub initial_word: Option<Spec>,
136
137 pub current_completion_options: Option<GenerationOptions>,
140}
141
142#[derive(Clone, Debug, Default)]
144pub struct GenerationOptions {
145 pub bash_default: bool,
149 pub default: bool,
151 pub dir_names: bool,
153 pub file_names: bool,
155 pub no_quote: bool,
157 pub no_sort: bool,
159 pub no_space: bool,
161 pub plus_dirs: bool,
163}
164
165#[derive(Clone, Debug, Default)]
168pub struct Spec {
169 pub options: GenerationOptions,
173
174 pub actions: Vec<CompleteAction>,
178 pub glob_pattern: Option<String>,
180 pub word_list: Option<String>,
182 pub function_name: Option<String>,
184 pub command: Option<String>,
186
187 pub filter_pattern: Option<String>,
191 pub filter_pattern_excludes: bool,
194
195 pub prefix: Option<String>,
199 pub suffix: Option<String>,
201}
202
203#[derive(Debug)]
205pub struct Context<'a> {
206 pub token_to_complete: &'a str,
208
209 pub command_name: Option<&'a str>,
211 pub preceding_token: Option<&'a str>,
213
214 pub token_index: usize,
216
217 pub input_line: &'a str,
219 pub cursor_index: usize,
221 pub tokens: &'a [&'a brush_parser::Token],
223}
224
225impl Spec {
226 #[expect(clippy::too_many_lines)]
233 pub async fn get_completions(
234 &self,
235 shell: &mut Shell,
236 context: &Context<'_>,
237 ) -> Result<Answer, crate::error::Error> {
238 shell.completion_config.current_completion_options = Some(self.options.clone());
242
243 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, ¶ms, word_list).await?;
249 for word in words {
250 if word.starts_with(context.token_to_complete) {
251 candidates.insert(word);
252 }
253 }
254 }
255
256 if let Some(glob_pattern) = &self.glob_pattern {
257 let pattern = patterns::Pattern::from(glob_pattern.as_str())
258 .set_extended_globbing(shell.options.extended_globbing)
259 .set_case_insensitive(shell.options.case_insensitive_pathname_expansion);
260
261 let expansions = pattern.expand(
262 shell.working_dir(),
263 Some(&patterns::Pattern::accept_all_expand_filter),
264 &patterns::FilenameExpansionOptions::default(),
265 )?;
266
267 for expansion in expansions {
268 candidates.insert(expansion);
269 }
270 }
271 if let Some(function_name) = &self.function_name {
272 let call_result = self
273 .call_completion_function(shell, function_name.as_str(), context)
274 .await?;
275
276 match call_result {
277 Answer::RestartCompletionProcess => return Ok(call_result),
278 Answer::Candidates(mut new_candidates, _options) => {
279 candidates.append(&mut new_candidates);
280 }
281 }
282 }
283 if let Some(command) = &self.command {
284 let mut new_candidates = self
285 .call_completion_command(shell, command.as_str(), context)
286 .await?;
287 candidates.append(&mut new_candidates);
288 }
289
290 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 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 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 let mut dir_candidates = get_file_completions(
345 shell,
346 context.token_to_complete,
347 true,
348 )
349 .await;
350 candidates.append(&mut dir_candidates);
351 }
352
353 if candidates.is_empty() {
356 if options.bash_default {
357 tracing::debug!(target: trace_categories::COMPLETION, "unimplemented: complete -o bashdefault");
363 }
364 if options.default || options.dir_names {
365 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 if !self.options.no_sort {
377 candidates.sort();
378 }
379
380 Ok(Answer::Candidates(candidates, processing_options))
381 }
382
383 #[expect(clippy::too_many_lines)]
384 async fn generate_action_completions(
385 &self,
386 shell: &Shell,
387 context: &Context<'_>,
388 ) -> Result<IndexSet<String>, error::Error> {
389 let mut candidates = IndexSet::new();
390
391 let token = context.token_to_complete;
392
393 for action in &self.actions {
394 match action {
395 CompleteAction::Alias => {
396 for name in shell.aliases.keys() {
397 if name.starts_with(token) {
398 candidates.insert(name.clone());
399 }
400 }
401 }
402 CompleteAction::ArrayVar => {
403 for (name, var) in shell.env.iter() {
404 if var.value().is_array() && name.starts_with(token) {
405 candidates.insert(name.to_owned());
406 }
407 }
408 }
409 CompleteAction::Binding => {
410 tracing::debug!(target: trace_categories::COMPLETION, "unimplemented: complete -A binding");
411 }
412 CompleteAction::Builtin => {
413 for name in shell.builtins().keys() {
414 if name.starts_with(token) {
415 candidates.insert(name.to_owned());
416 }
417 }
418 }
419 CompleteAction::Command => {
420 let mut command_completions = get_command_completions(shell, context);
421 candidates.append(&mut command_completions);
422 }
423 CompleteAction::Directory => {
424 let mut file_completions =
425 get_file_completions(shell, context.token_to_complete, true).await;
426 candidates.append(&mut file_completions);
427 }
428 CompleteAction::Disabled => {
429 for (name, registration) in shell.builtins() {
430 if registration.disabled && name.starts_with(token) {
431 candidates.insert(name.to_owned());
432 }
433 }
434 }
435 CompleteAction::Enabled => {
436 for (name, registration) in shell.builtins() {
437 if !registration.disabled && name.starts_with(token) {
438 candidates.insert(name.to_owned());
439 }
440 }
441 }
442 CompleteAction::Export => {
443 for (key, value) in shell.env.iter() {
444 if value.is_exported() && key.starts_with(token) {
445 candidates.insert(key.to_owned());
446 }
447 }
448 }
449 CompleteAction::File => {
450 let mut file_completions =
451 get_file_completions(shell, context.token_to_complete, false).await;
452 candidates.append(&mut file_completions);
453 }
454 CompleteAction::Function => {
455 for (name, _) in shell.funcs().iter() {
456 candidates.insert(name.to_owned());
457 }
458 }
459 CompleteAction::Group => {
460 for group_name in users::get_all_groups()? {
461 if group_name.starts_with(token) {
462 candidates.insert(group_name);
463 }
464 }
465 }
466 CompleteAction::HelpTopic => {
467 for name in shell.builtins().keys() {
469 if name.starts_with(token) {
470 candidates.insert(name.to_owned());
471 }
472 }
473 }
474 CompleteAction::HostName => {
475 if let Ok(name) = sys::network::get_hostname() {
477 let name = name.to_string_lossy();
478 if name.starts_with(token) {
479 candidates.insert(name.to_string());
480 }
481 }
482 }
483 CompleteAction::Job => {
484 for job in &shell.jobs.jobs {
485 let command_name = job.command_name();
486 if command_name.starts_with(token) {
487 candidates.insert(command_name.to_owned());
488 }
489 }
490 }
491 CompleteAction::Keyword => {
492 for keyword in shell.get_keywords() {
493 if keyword.starts_with(token) {
494 candidates.insert(keyword.clone());
495 }
496 }
497 }
498 CompleteAction::Running => {
499 for job in &shell.jobs.jobs {
500 if matches!(job.state, jobs::JobState::Running) {
501 let command_name = job.command_name();
502 if command_name.starts_with(token) {
503 candidates.insert(command_name.to_owned());
504 }
505 }
506 }
507 }
508 CompleteAction::Service => {
509 tracing::debug!(target: trace_categories::COMPLETION, "unimplemented: complete -A service");
510 }
511 CompleteAction::SetOpt => {
512 for option in namedoptions::options(namedoptions::ShellOptionKind::SetO).iter()
513 {
514 if option.name.starts_with(token) {
515 candidates.insert(option.name.to_owned());
516 }
517 }
518 }
519 CompleteAction::ShOpt => {
520 for option in namedoptions::options(namedoptions::ShellOptionKind::Shopt).iter()
521 {
522 if option.name.starts_with(token) {
523 candidates.insert(option.name.to_owned());
524 }
525 }
526 }
527 CompleteAction::Signal => {
528 for signal in traps::TrapSignal::iterator() {
529 if signal.as_str().starts_with(token) {
530 candidates.insert(signal.as_str().to_string());
531 }
532 }
533 }
534 CompleteAction::Stopped => {
535 for job in &shell.jobs.jobs {
536 if matches!(job.state, jobs::JobState::Stopped) {
537 let command_name = job.command_name();
538 if command_name.starts_with(token) {
539 candidates.insert(job.command_name().to_owned());
540 }
541 }
542 }
543 }
544 CompleteAction::User => {
545 for user_name in users::get_all_users()? {
546 if user_name.starts_with(token) {
547 candidates.insert(user_name);
548 }
549 }
550 }
551 CompleteAction::Variable => {
552 for (key, _) in shell.env.iter() {
553 if key.starts_with(token) {
554 candidates.insert(key.to_owned());
555 }
556 }
557 }
558 }
559 }
560
561 Ok(candidates)
562 }
563
564 async fn call_completion_command(
565 &self,
566 shell: &Shell,
567 command_name: &str,
568 context: &Context<'_>,
569 ) -> Result<IndexSet<String>, error::Error> {
570 let mut shell = shell.clone();
572
573 let vars_and_values: Vec<(&str, ShellValueLiteral)> = vec![
574 ("COMP_LINE", context.input_line.into()),
575 ("COMP_POINT", context.cursor_index.to_string().into()),
576 ];
579
580 for (var, value) in vars_and_values {
582 shell.env.update_or_add(
583 var,
584 value,
585 |v| {
586 v.export();
587 Ok(())
588 },
589 env::EnvironmentLookup::Anywhere,
590 env::EnvironmentScope::Global,
591 )?;
592 }
593
594 let mut args = vec![
596 context.command_name.unwrap_or(""),
597 context.token_to_complete,
598 ];
599 if let Some(preceding_token) = context.preceding_token {
600 args.push(preceding_token);
601 }
602
603 let mut command_line = command_name.to_owned();
605 for arg in args {
606 command_line.push(' ');
607
608 let escaped_arg = escape::quote_if_needed(arg, escape::QuoteMode::SingleQuote);
609 command_line.push_str(escaped_arg.as_ref());
610 }
611
612 let params = shell.default_exec_params();
614 let output =
615 commands::invoke_command_in_subshell_and_get_output(&mut shell, ¶ms, command_line)
616 .await?;
617
618 let mut candidates = IndexSet::new();
620 for line in output.lines() {
621 candidates.insert(line.to_owned());
622 }
623
624 Ok(candidates)
625 }
626
627 async fn call_completion_function(
628 &self,
629 shell: &mut Shell,
630 function_name: &str,
631 context: &Context<'_>,
632 ) -> Result<Answer, error::Error> {
633 let vars_and_values: Vec<(&str, ShellValueLiteral)> = vec![
635 ("COMP_LINE", context.input_line.into()),
636 ("COMP_POINT", context.cursor_index.to_string().into()),
637 (
640 "COMP_WORDS",
641 context
642 .tokens
643 .iter()
644 .map(|t| t.to_str())
645 .collect::<Vec<_>>()
646 .into(),
647 ),
648 ("COMP_CWORD", context.token_index.to_string().into()),
649 ];
650
651 tracing::debug!(target: trace_categories::COMPLETION, "[calling completion func '{function_name}']: {}",
652 vars_and_values.iter().map(|(k, v)| std::format!("{k}={v}")).collect::<Vec<String>>().join(" "));
653
654 let mut vars_to_remove = vec![];
655 for (var, value) in vars_and_values {
656 shell.env.update_or_add(
657 var,
658 value,
659 |_| Ok(()),
660 env::EnvironmentLookup::Anywhere,
661 env::EnvironmentScope::Global,
662 )?;
663
664 vars_to_remove.push(var);
665 }
666
667 let mut args = vec![
668 context.command_name.unwrap_or(""),
669 context.token_to_complete,
670 ];
671 if let Some(preceding_token) = context.preceding_token {
672 args.push(preceding_token);
673 }
674
675 shell.traps.handler_depth += 1;
678
679 let params = shell.default_exec_params();
680 let invoke_result = shell
681 .invoke_function(function_name, args.iter(), ¶ms)
682 .await;
683 tracing::debug!(target: trace_categories::COMPLETION, "[completion function '{function_name}' returned: {invoke_result:?}]");
684
685 shell.traps.handler_depth -= 1;
686
687 for var_name in vars_to_remove {
689 let _ = shell.env.unset(var_name);
690 }
691
692 let result = invoke_result.unwrap_or_else(|e| {
693 tracing::warn!(target: trace_categories::COMPLETION, "error while running completion function '{function_name}': {e}");
694 1 });
696
697 if result == 124 {
700 Ok(Answer::RestartCompletionProcess)
701 } else {
702 if let Some(reply) = shell.env.unset("COMPREPLY")? {
703 tracing::debug!(target: trace_categories::COMPLETION, "[completion function yielded: {reply:?}]");
704
705 match reply.value() {
706 variables::ShellValue::IndexedArray(values) => {
707 return Ok(Answer::Candidates(
708 values.values().map(|v| v.to_owned()).collect(),
709 ProcessingOptions::default(),
710 ));
711 }
712 variables::ShellValue::String(s) => {
713 let mut candidates = IndexSet::new();
714 candidates.insert(s.to_owned());
715
716 return Ok(Answer::Candidates(candidates, ProcessingOptions::default()));
717 }
718 _ => (),
719 }
720 }
721
722 Ok(Answer::Candidates(
723 IndexSet::new(),
724 ProcessingOptions::default(),
725 ))
726 }
727 }
728}
729
730#[derive(Debug, Default)]
732pub struct Completions {
733 pub insertion_index: usize,
736 pub delete_count: usize,
739 pub candidates: IndexSet<String>,
741 pub options: ProcessingOptions,
743}
744
745#[derive(Debug)]
747pub struct ProcessingOptions {
748 pub treat_as_filenames: bool,
750 pub no_autoquote_filenames: bool,
752 pub no_trailing_space_at_end_of_line: bool,
754}
755
756impl Default for ProcessingOptions {
757 fn default() -> Self {
758 Self {
759 treat_as_filenames: true,
760 no_autoquote_filenames: false,
761 no_trailing_space_at_end_of_line: false,
762 }
763 }
764}
765
766pub enum Answer {
768 Candidates(IndexSet<String>, ProcessingOptions),
771 RestartCompletionProcess,
773}
774
775const EMPTY_COMMAND: &str = "_EmptycmD_";
776const DEFAULT_COMMAND: &str = "_DefaultCmD_";
777const INITIAL_WORD: &str = "_InitialWorD_";
778
779impl Config {
780 pub fn clear(&mut self) {
782 self.commands.clear();
783 self.empty_line = None;
784 self.default = None;
785 self.initial_word = None;
786 }
787
788 pub fn remove(&mut self, name: &str) -> bool {
795 match name {
796 EMPTY_COMMAND => {
797 let result = self.empty_line.is_some();
798 self.empty_line = None;
799 result
800 }
801 DEFAULT_COMMAND => {
802 let result = self.default.is_some();
803 self.default = None;
804 result
805 }
806 INITIAL_WORD => {
807 let result = self.initial_word.is_some();
808 self.initial_word = None;
809 result
810 }
811 _ => self.commands.remove(name).is_some(),
812 }
813 }
814
815 pub fn iter(&self) -> impl Iterator<Item = (&String, &Spec)> {
817 self.commands.iter()
818 }
819
820 pub fn get(&self, name: &str) -> Option<&Spec> {
826 match name {
827 EMPTY_COMMAND => self.empty_line.as_ref(),
828 DEFAULT_COMMAND => self.default.as_ref(),
829 INITIAL_WORD => self.initial_word.as_ref(),
830 _ => self.commands.get(name),
831 }
832 }
833
834 pub fn set(&mut self, name: &str, spec: Spec) {
842 match name {
843 EMPTY_COMMAND => {
844 self.empty_line = Some(spec);
845 }
846 DEFAULT_COMMAND => {
847 self.default = Some(spec);
848 }
849 INITIAL_WORD => {
850 self.initial_word = Some(spec);
851 }
852 _ => {
853 self.commands.insert(name.to_owned(), spec);
854 }
855 }
856 }
857
858 pub fn get_or_add_mut(&mut self, name: &str) -> &mut Spec {
867 match name {
868 EMPTY_COMMAND => {
869 if self.empty_line.is_none() {
870 self.empty_line = Some(Spec::default());
871 }
872 self.empty_line.as_mut().unwrap()
873 }
874 DEFAULT_COMMAND => {
875 if self.default.is_none() {
876 self.default = Some(Spec::default());
877 }
878 self.default.as_mut().unwrap()
879 }
880 INITIAL_WORD => {
881 if self.initial_word.is_none() {
882 self.initial_word = Some(Spec::default());
883 }
884 self.initial_word.as_mut().unwrap()
885 }
886 _ => self.commands.entry(name.to_owned()).or_default(),
887 }
888 }
889
890 #[expect(clippy::string_slice)]
898 pub async fn get_completions(
899 &self,
900 shell: &mut Shell,
901 input: &str,
902 position: usize,
903 ) -> Result<Completions, error::Error> {
904 const MAX_RESTARTS: u32 = 10;
905
906 let tokens = Self::tokenize_input_for_completion(shell, input);
908
909 let cursor = position;
910 let mut preceding_token = None;
911 let mut completion_prefix = "";
912 let mut insertion_index = cursor;
913 let mut completion_token_index = tokens.len();
914
915 let mut adjusted_tokens: Vec<&brush_parser::Token> = tokens.iter().collect();
918
919 for (i, token) in tokens.iter().enumerate() {
921 if cursor < token.location().start.index {
925 completion_token_index = i;
928 break;
929 }
930 else if cursor >= token.location().start.index && cursor <= token.location().end.index
936 {
937 insertion_index = token.location().start.index;
939
940 let offset_into_token = cursor - insertion_index;
942 let token_str = token.to_str();
943 completion_prefix = &token_str[..offset_into_token];
944
945 completion_token_index = i;
947
948 break;
949 }
950
951 preceding_token = Some(token);
954 }
955
956 let empty_token =
959 brush_parser::Token::Word(String::new(), brush_parser::TokenLocation::default());
960 if completion_token_index == tokens.len() {
961 adjusted_tokens.push(&empty_token);
962 }
963
964 let mut result = Answer::RestartCompletionProcess;
966 let mut restart_count = 0;
967 while matches!(result, Answer::RestartCompletionProcess) {
968 if restart_count > MAX_RESTARTS {
969 tracing::warn!("possible infinite loop detected in completion process");
970 break;
971 }
972
973 let completion_context = Context {
974 token_to_complete: completion_prefix,
975 preceding_token: preceding_token.map(|t| t.to_str()),
976 command_name: adjusted_tokens.first().map(|token| token.to_str()),
977 input_line: input,
978 token_index: completion_token_index,
979 tokens: adjusted_tokens.as_slice(),
980 cursor_index: position,
981 };
982
983 result = self
984 .get_completions_for_token(shell, completion_context)
985 .await;
986
987 restart_count += 1;
988 }
989
990 match result {
991 Answer::Candidates(candidates, options) => Ok(Completions {
992 insertion_index,
993 delete_count: completion_prefix.len(),
994 candidates,
995 options,
996 }),
997 Answer::RestartCompletionProcess => Ok(Completions {
998 insertion_index,
999 delete_count: 0,
1000 candidates: IndexSet::new(),
1001 options: ProcessingOptions::default(),
1002 }),
1003 }
1004 }
1005
1006 fn tokenize_input_for_completion(shell: &Shell, input: &str) -> Vec<brush_parser::Token> {
1007 const FALLBACK: &str = " \t\n\"\'@><=;|&(:";
1008
1009 let delimiter_str = shell
1010 .env_str("COMP_WORDBREAKS")
1011 .unwrap_or_else(|| FALLBACK.into());
1012
1013 let delimiters: Vec<_> = delimiter_str.chars().collect();
1014
1015 simple_tokenize_by_delimiters(input, delimiters.as_slice())
1016 }
1017
1018 async fn get_completions_for_token(&self, shell: &mut Shell, context: Context<'_>) -> Answer {
1019 let mut found_spec: Option<&Spec> = None;
1021
1022 if let Some(command_name) = context.command_name {
1023 if context.token_index == 0 {
1024 if let Some(spec) = &self.initial_word {
1025 found_spec = Some(spec);
1026 }
1027 } else {
1028 if let Some(spec) = shell.completion_config.commands.get(command_name) {
1029 found_spec = Some(spec);
1030 } else if let Some(file_name) = PathBuf::from(command_name).file_name() {
1031 if let Some(spec) = shell
1032 .completion_config
1033 .commands
1034 .get(&file_name.to_string_lossy().to_string())
1035 {
1036 found_spec = Some(spec);
1037 }
1038 }
1039
1040 if found_spec.is_none() {
1041 if let Some(spec) = &self.default {
1042 found_spec = Some(spec);
1043 }
1044 }
1045 }
1046 } else {
1047 if let Some(spec) = &self.empty_line {
1048 found_spec = Some(spec);
1049 }
1050 }
1051
1052 if let Some(spec) = found_spec {
1054 spec.to_owned()
1055 .get_completions(shell, &context)
1056 .await
1057 .unwrap_or_else(|_err| {
1058 Answer::Candidates(IndexSet::new(), ProcessingOptions::default())
1059 })
1060 } else {
1061 get_completions_using_basic_lookup(shell, &context).await
1063 }
1064 }
1065}
1066
1067async fn get_file_completions(
1068 shell: &Shell,
1069 token_to_complete: &str,
1070 must_be_dir: bool,
1071) -> IndexSet<String> {
1072 let mut throwaway_shell = shell.clone();
1074 let params = throwaway_shell.default_exec_params();
1075 let expanded_token = throwaway_shell
1076 .basic_expand_string(¶ms, token_to_complete)
1077 .await
1078 .unwrap_or_else(|_err| token_to_complete.to_owned());
1079
1080 let glob = std::format!("{expanded_token}*");
1081
1082 let path_filter = |path: &Path| !must_be_dir || shell.absolute_path(path).is_dir();
1083
1084 let pattern = patterns::Pattern::from(glob)
1085 .set_extended_globbing(shell.options.extended_globbing)
1086 .set_case_insensitive(shell.options.case_insensitive_pathname_expansion);
1087
1088 pattern
1089 .expand(
1090 shell.working_dir(),
1091 Some(&path_filter),
1092 &patterns::FilenameExpansionOptions::default(),
1093 )
1094 .unwrap_or_default()
1095 .into_iter()
1096 .collect()
1097}
1098
1099fn get_command_completions(shell: &Shell, context: &Context<'_>) -> IndexSet<String> {
1100 let mut candidates = IndexSet::new();
1101
1102 for path in shell.find_executables_in_path_with_prefix(
1104 context.token_to_complete,
1105 shell.options.case_insensitive_pathname_expansion,
1106 ) {
1107 if let Some(file_name) = path.file_name() {
1108 candidates.insert(file_name.to_string_lossy().to_string());
1109 }
1110 }
1111
1112 candidates.into_iter().collect()
1113}
1114
1115async fn get_completions_using_basic_lookup(shell: &Shell, context: &Context<'_>) -> Answer {
1116 let mut candidates = get_file_completions(shell, context.token_to_complete, false).await;
1117
1118 if context.token_index == 0
1123 && !context.token_to_complete.is_empty()
1124 && !context
1125 .token_to_complete
1126 .contains(std::path::MAIN_SEPARATOR)
1127 {
1128 let mut command_completions = get_command_completions(shell, context);
1130 candidates.append(&mut command_completions);
1131
1132 for (name, registration) in shell.builtins() {
1134 if !registration.disabled && name.starts_with(context.token_to_complete) {
1135 candidates.insert(name.to_owned());
1136 }
1137 }
1138
1139 for (name, _) in shell.funcs().iter() {
1141 if name.starts_with(context.token_to_complete) {
1142 candidates.insert(name.to_owned());
1143 }
1144 }
1145
1146 for name in shell.aliases.keys() {
1148 if name.starts_with(context.token_to_complete) {
1149 candidates.insert(name.to_owned());
1150 }
1151 }
1152
1153 for keyword in shell.get_keywords() {
1155 if keyword.starts_with(context.token_to_complete) {
1156 candidates.insert(keyword.clone());
1157 }
1158 }
1159
1160 candidates.sort();
1162 }
1163
1164 #[cfg(windows)]
1165 {
1166 candidates = candidates
1167 .into_iter()
1168 .map(|c| c.replace('\\', "/"))
1169 .collect();
1170 }
1171
1172 Answer::Candidates(candidates, ProcessingOptions::default())
1173}
1174
1175fn simple_tokenize_by_delimiters(input: &str, delimiters: &[char]) -> Vec<brush_parser::Token> {
1176 let mut tokens = vec![];
1181 let mut start = 0;
1182
1183 for piece in input.split_inclusive(delimiters) {
1184 let next_start = start + piece.len();
1185
1186 let piece = piece.strip_suffix(delimiters).unwrap_or(piece);
1187 let end = start + piece.len();
1188 tokens.push(brush_parser::Token::Word(
1189 piece.to_string(),
1190 brush_parser::TokenLocation {
1191 start: brush_parser::SourcePosition {
1192 index: start,
1193 line: 1,
1194 column: start + 1,
1195 }
1196 .into(),
1197 end: brush_parser::SourcePosition {
1198 index: end,
1199 line: 1,
1200 column: end + 1,
1201 }
1202 .into(),
1203 },
1204 ));
1205
1206 start = next_start;
1207 }
1208
1209 tokens
1210}
1211
1212fn completion_filter_pattern_matches(
1213 pattern: &str,
1214 candidate: &str,
1215 token_being_completed: &str,
1216 shell: &Shell,
1217) -> Result<bool, error::Error> {
1218 let pattern = replace_unescaped_ampersands(pattern, token_being_completed);
1219
1220 let pattern = patterns::Pattern::from(pattern.as_ref())
1225 .set_extended_globbing(shell.options.extended_globbing)
1226 .set_case_insensitive(shell.options.case_insensitive_pathname_expansion);
1227
1228 let matches = pattern.exactly_matches(candidate)?;
1229
1230 Ok(matches)
1231}
1232
1233fn replace_unescaped_ampersands<'a>(pattern: &'a str, replacement: &str) -> Cow<'a, str> {
1234 let mut in_escape = false;
1235 let mut insertion_points = vec![];
1236
1237 for (i, c) in pattern.char_indices() {
1238 if !in_escape && c == '&' {
1239 insertion_points.push(i);
1240 }
1241 in_escape = !in_escape && c == '\\';
1242 }
1243
1244 if insertion_points.is_empty() {
1245 return pattern.into();
1246 }
1247
1248 let mut result = pattern.to_owned();
1249 for i in insertion_points.iter().rev() {
1250 result.replace_range(*i..=*i, replacement);
1251 }
1252
1253 result.into()
1254}