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 #[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 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.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 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 #[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 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.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 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 ];
577
578 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 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 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 let params = shell.default_exec_params();
612 let output =
613 commands::invoke_command_in_subshell_and_get_output(&mut shell, ¶ms, command_line)
614 .await?;
615
616 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 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 (
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 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 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 });
691
692 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#[derive(Debug, Default)]
727pub struct Completions {
728 pub insertion_index: usize,
730 pub delete_count: usize,
732 pub candidates: IndexSet<String>,
734 pub options: ProcessingOptions,
736}
737
738#[derive(Debug)]
740pub struct ProcessingOptions {
741 pub treat_as_filenames: bool,
743 pub no_autoquote_filenames: bool,
745 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
759pub enum Answer {
761 Candidates(IndexSet<String>, ProcessingOptions),
764 RestartCompletionProcess,
766}
767
768const EMPTY_COMMAND: &str = "_EmptycmD_";
769const DEFAULT_COMMAND: &str = "_DefaultCmD_";
770const INITIAL_WORD: &str = "_InitialWorD_";
771
772impl Config {
773 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 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 pub fn iter(&self) -> impl Iterator<Item = (&String, &Spec)> {
810 self.commands.iter()
811 }
812
813 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 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 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 #[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 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 let mut adjusted_tokens: Vec<&brush_parser::Token> = tokens.iter().collect();
911
912 for (i, token) in tokens.iter().enumerate() {
914 if cursor < token.location().start.index {
918 completion_token_index = i;
921 break;
922 }
923 else if cursor >= token.location().start.index && cursor <= token.location().end.index
929 {
930 insertion_index = token.location().start.index;
932
933 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 completion_token_index = i;
940
941 break;
942 }
943
944 preceding_token = Some(token);
947 }
948
949 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 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 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 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 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 let mut throwaway_shell = shell.clone();
1067 let params = throwaway_shell.default_exec_params();
1068 let expanded_token = throwaway_shell
1069 .basic_expand_string(¶ms, 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 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 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 let mut command_completions = get_command_completions(shell, context);
1123 candidates.append(&mut command_completions);
1124
1125 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 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 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 for keyword in shell.get_keywords() {
1148 if keyword.starts_with(context.token_to_complete) {
1149 candidates.insert(keyword.clone());
1150 }
1151 }
1152
1153 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 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 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}