1use 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#[derive(Clone, Debug, ValueEnum)]
21pub enum CompleteAction {
22 #[clap(name = "alias")]
24 Alias,
25 #[clap(name = "arrayvar")]
27 ArrayVar,
28 #[clap(name = "binding")]
30 Binding,
31 #[clap(name = "builtin")]
33 Builtin,
34 #[clap(name = "command")]
36 Command,
37 #[clap(name = "directory")]
39 Directory,
40 #[clap(name = "disabled")]
42 Disabled,
43 #[clap(name = "enabled")]
45 Enabled,
46 #[clap(name = "export")]
48 Export,
49 #[clap(name = "file")]
51 File,
52 #[clap(name = "function")]
54 Function,
55 #[clap(name = "group")]
57 Group,
58 #[clap(name = "helptopic")]
60 HelpTopic,
61 #[clap(name = "hostname")]
63 HostName,
64 #[clap(name = "job")]
66 Job,
67 #[clap(name = "keyword")]
69 Keyword,
70 #[clap(name = "running")]
72 Running,
73 #[clap(name = "service")]
75 Service,
76 #[clap(name = "setopt")]
78 SetOpt,
79 #[clap(name = "shopt")]
81 ShOpt,
82 #[clap(name = "signal")]
84 Signal,
85 #[clap(name = "stopped")]
87 Stopped,
88 #[clap(name = "user")]
90 User,
91 #[clap(name = "variable")]
93 Variable,
94}
95
96#[derive(Clone, Debug, Eq, Hash, PartialEq, ValueEnum)]
98pub enum CompleteOption {
99 #[clap(name = "bashdefault")]
101 BashDefault,
102 #[clap(name = "default")]
104 Default,
105 #[clap(name = "dirnames")]
107 DirNames,
108 #[clap(name = "filenames")]
110 FileNames,
111 #[clap(name = "noquote")]
113 NoQuote,
114 #[clap(name = "nosort")]
116 NoSort,
117 #[clap(name = "nospace")]
119 NoSpace,
120 #[clap(name = "plusdirs")]
122 PlusDirs,
123}
124
125#[derive(Clone, Default)]
127pub struct Config {
128 commands: HashMap<String, Spec>,
129
130 pub default: Option<Spec>,
133 pub empty_line: Option<Spec>,
135 pub initial_word: Option<Spec>,
137
138 pub current_completion_options: Option<GenerationOptions>,
141}
142
143#[derive(Clone, Debug, Default)]
145pub struct GenerationOptions {
146 pub bash_default: bool,
150 pub default: bool,
152 pub dir_names: bool,
154 pub file_names: bool,
156 pub no_quote: bool,
158 pub no_sort: bool,
160 pub no_space: bool,
162 pub plus_dirs: bool,
164}
165
166#[derive(Clone, Debug, Default)]
169pub struct Spec {
170 pub options: GenerationOptions,
174
175 pub actions: Vec<CompleteAction>,
179 pub glob_pattern: Option<String>,
181 pub word_list: Option<String>,
183 pub function_name: Option<String>,
185 pub command: Option<String>,
187
188 pub filter_pattern: Option<String>,
192 pub filter_pattern_excludes: bool,
195
196 pub prefix: Option<String>,
200 pub suffix: Option<String>,
202}
203
204#[derive(Debug)]
206pub struct Context<'a> {
207 pub token_to_complete: &'a str,
209
210 pub command_name: Option<&'a str>,
212 pub preceding_token: Option<&'a str>,
214
215 pub token_index: usize,
217
218 pub input_line: &'a str,
220 pub cursor_index: usize,
222 pub tokens: &'a [&'a brush_parser::Token],
224}
225
226impl Spec {
227 #[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 shell.completion_config.current_completion_options = Some(self.options.clone());
243
244 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, ¶ms, 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 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 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 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 let mut dir_candidates = get_file_completions(
346 shell,
347 context.token_to_complete,
348 true,
349 )
350 .await;
351 candidates.append(&mut dir_candidates);
352 }
353
354 if candidates.is_empty() {
357 if options.bash_default {
358 tracing::debug!(target: trace_categories::COMPLETION, "UNIMPLEMENTED: complete -o bashdefault");
364 }
365 if options.default || options.dir_names {
366 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 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 name in shell.builtins.keys() {
470 if name.starts_with(token) {
471 candidates.insert(name.to_owned());
472 }
473 }
474 }
475 CompleteAction::HostName => {
476 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 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 ];
578
579 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 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 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 let params = shell.default_exec_params();
613 let output =
614 commands::invoke_command_in_subshell_and_get_output(&mut shell, ¶ms, command_line)
615 .await?;
616
617 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 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 (
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 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 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 });
692
693 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#[derive(Debug, Default)]
728pub struct Completions {
729 pub insertion_index: usize,
731 pub delete_count: usize,
733 pub candidates: IndexSet<String>,
735 pub options: ProcessingOptions,
737}
738
739#[derive(Debug)]
741pub struct ProcessingOptions {
742 pub treat_as_filenames: bool,
744 pub no_autoquote_filenames: bool,
746 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
760pub enum Answer {
762 Candidates(IndexSet<String>, ProcessingOptions),
765 RestartCompletionProcess,
767}
768
769const EMPTY_COMMAND: &str = "_EmptycmD_";
770const DEFAULT_COMMAND: &str = "_DefaultCmD_";
771const INITIAL_WORD: &str = "_InitialWorD_";
772
773impl Config {
774 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 pub fn iter(&self) -> impl Iterator<Item = (&String, &Spec)> {
798 self.commands.iter()
799 }
800
801 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 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 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 #[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 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 let mut adjusted_tokens: Vec<&brush_parser::Token> = tokens.iter().collect();
899
900 for (i, token) in tokens.iter().enumerate() {
902 if cursor < token.location().start.index {
906 completion_token_index = i;
909 break;
910 }
911 else if cursor >= token.location().start.index && cursor <= token.location().end.index
917 {
918 insertion_index = token.location().start.index;
920
921 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 completion_token_index = i;
928
929 break;
930 }
931
932 preceding_token = Some(token);
935 }
936
937 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 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 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 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 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 let mut throwaway_shell = shell.clone();
1055 let params = throwaway_shell.default_exec_params();
1056 let expanded_token = throwaway_shell
1057 .basic_expand_string(¶ms, 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 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 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 let mut command_completions = get_command_completions(shell, context);
1109 candidates.append(&mut command_completions);
1110
1111 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 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 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 for keyword in shell.get_keywords() {
1134 if keyword.starts_with(context.token_to_complete) {
1135 candidates.insert(keyword.clone());
1136 }
1137 }
1138
1139 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 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 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}