1use clap::ValueEnum;
4use std::{
5 borrow::Cow,
6 collections::HashMap,
7 path::{Path, PathBuf},
8};
9use strum::IntoEnumIterator;
10
11use crate::{
12 Shell, commands, env, error, escape, expansion, extensions, interfaces, jobs, namedoptions,
13 patterns,
14 sys::{self, users},
15 trace_categories, traps,
16 variables::{self, ShellValueLiteral},
17};
18use brush_parser::unquote_str;
19
20#[derive(Clone, Debug, ValueEnum)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
23pub enum CompleteAction {
24 #[clap(name = "alias")]
26 Alias,
27 #[clap(name = "arrayvar")]
29 ArrayVar,
30 #[clap(name = "binding")]
32 Binding,
33 #[clap(name = "builtin")]
35 Builtin,
36 #[clap(name = "command")]
38 Command,
39 #[clap(name = "directory")]
41 Directory,
42 #[clap(name = "disabled")]
44 Disabled,
45 #[clap(name = "enabled")]
47 Enabled,
48 #[clap(name = "export")]
50 Export,
51 #[clap(name = "file")]
53 File,
54 #[clap(name = "function")]
56 Function,
57 #[clap(name = "group")]
59 Group,
60 #[clap(name = "helptopic")]
62 HelpTopic,
63 #[clap(name = "hostname")]
65 HostName,
66 #[clap(name = "job")]
68 Job,
69 #[clap(name = "keyword")]
71 Keyword,
72 #[clap(name = "running")]
74 Running,
75 #[clap(name = "service")]
77 Service,
78 #[clap(name = "setopt")]
80 SetOpt,
81 #[clap(name = "shopt")]
83 ShOpt,
84 #[clap(name = "signal")]
86 Signal,
87 #[clap(name = "stopped")]
89 Stopped,
90 #[clap(name = "user")]
92 User,
93 #[clap(name = "variable")]
95 Variable,
96}
97
98#[derive(Clone, Debug, Eq, Hash, PartialEq, ValueEnum)]
100pub enum CompleteOption {
101 #[clap(name = "bashdefault")]
103 BashDefault,
104 #[clap(name = "default")]
106 Default,
107 #[clap(name = "dirnames")]
109 DirNames,
110 #[clap(name = "filenames")]
112 FileNames,
113 #[clap(name = "noquote")]
115 NoQuote,
116 #[clap(name = "nosort")]
118 NoSort,
119 #[clap(name = "nospace")]
121 NoSpace,
122 #[clap(name = "plusdirs")]
124 PlusDirs,
125}
126
127#[derive(Clone, Default)]
129#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
130pub struct Config {
131 commands: HashMap<String, Spec>,
132
133 pub default: Option<Spec>,
136 pub empty_line: Option<Spec>,
138 pub initial_word: Option<Spec>,
140
141 pub current_completion_options: Option<GenerationOptions>,
144
145 pub fallback_options: FallbackOptions,
148}
149
150#[derive(Clone, Debug)]
152#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
153pub struct FallbackOptions {
154 pub mark_directories: bool,
156 pub mark_symlinked_directories: bool,
158}
159
160impl Default for FallbackOptions {
161 fn default() -> Self {
162 Self {
163 mark_directories: true,
164 mark_symlinked_directories: false,
165 }
166 }
167}
168
169#[derive(Clone, Debug, Default)]
171#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
172pub struct GenerationOptions {
173 pub bash_default: bool,
177 pub default: bool,
179 pub dir_names: bool,
181 pub file_names: bool,
183 pub no_quote: bool,
185 pub no_sort: bool,
187 pub no_space: bool,
189 pub plus_dirs: bool,
191}
192
193#[derive(Clone, Debug, Default)]
196#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
197pub struct Spec {
198 pub options: GenerationOptions,
202
203 pub actions: Vec<CompleteAction>,
207 pub glob_pattern: Option<String>,
209 pub word_list: Option<String>,
211 pub function_name: Option<String>,
213 pub command: Option<String>,
215
216 pub filter_pattern: Option<String>,
220 pub filter_pattern_excludes: bool,
223
224 pub prefix: Option<String>,
228 pub suffix: Option<String>,
230}
231
232#[derive(Clone, Copy, Debug, Default)]
234pub enum CompletionTrigger {
235 #[default]
237 InteractiveComplete,
238 Programmatic,
240}
241
242impl CompletionTrigger {
243 pub const fn comp_type(self) -> i32 {
245 match self {
246 Self::InteractiveComplete => 9, Self::Programmatic => 0,
248 }
249 }
250
251 pub const fn comp_key(self) -> i32 {
253 match self {
254 Self::InteractiveComplete => 9, Self::Programmatic => 0,
256 }
257 }
258}
259
260#[derive(Debug)]
262pub struct Context<'a> {
263 pub token_to_complete: &'a str,
265
266 pub command_name: Option<&'a str>,
268 pub preceding_token: Option<&'a str>,
270
271 pub token_index: usize,
273
274 pub input_line: &'a str,
276 pub cursor_index: usize,
278 pub tokens: &'a [&'a CompletionToken<'a>],
280
281 pub trigger: CompletionTrigger,
283}
284
285impl Spec {
286 #[expect(clippy::too_many_lines)]
293 pub async fn get_completions(
294 &self,
295 shell: &mut Shell<impl extensions::ShellExtensions>,
296 context: &Context<'_>,
297 ) -> Result<Answer, crate::error::Error> {
298 shell.completion_config_mut().current_completion_options = Some(self.options.clone());
302
303 let mut candidates = self.generate_action_completions(shell, context).await?;
305 if let Some(word_list) = &self.word_list {
306 let params = shell.default_exec_params();
307 let options = crate::expansion::ExpanderOptions {
310 pathname_expand: false,
311 ..Default::default()
312 };
313 let words = crate::expansion::full_expand_and_split_word_with_options(
314 shell, ¶ms, word_list, &options,
315 )
316 .await?;
317 for word in words {
318 if word.starts_with(context.token_to_complete) {
319 candidates.push(word);
320 }
321 }
322 }
323
324 if let Some(glob_pattern) = &self.glob_pattern {
325 let pattern = patterns::Pattern::from(glob_pattern.as_str())
326 .set_extended_globbing(shell.options().extended_globbing)
327 .set_case_insensitive(shell.options().case_insensitive_pathname_expansion);
328
329 let expansions = pattern
330 .expand(
331 shell.working_dir(),
332 Some(&patterns::Pattern::accept_all_expand_filter),
333 &patterns::FilenameExpansionOptions::default(),
334 )?
335 .into_paths();
336
337 for expansion in expansions {
338 candidates.push(expansion);
339 }
340 }
341 if let Some(function_name) = &self.function_name {
342 let call_result = self
343 .call_completion_function(shell, function_name.as_str(), context)
344 .await?;
345
346 match call_result {
347 Answer::RestartCompletionProcess => return Ok(call_result),
348 Answer::Candidates(mut new_candidates, _options) => {
349 candidates.append(&mut new_candidates);
350 }
351 }
352 }
353 if let Some(command) = &self.command {
354 let mut new_candidates = self
355 .call_completion_command(shell, command.as_str(), context)
356 .await?;
357 candidates.append(&mut new_candidates);
358 }
359
360 if let Some(filter_pattern) = &self.filter_pattern
362 && !filter_pattern.is_empty()
363 {
364 let mut updated = Vec::new();
365
366 for candidate in candidates {
367 let matches = completion_filter_pattern_matches(
368 filter_pattern.as_str(),
369 candidate.as_str(),
370 context.token_to_complete,
371 shell,
372 )?;
373
374 if self.filter_pattern_excludes != matches {
375 updated.push(candidate);
376 }
377 }
378
379 candidates = updated;
380 }
381
382 if self.prefix.is_some() || self.suffix.is_some() {
384 let empty = String::new();
385 let prefix = self.prefix.as_ref().unwrap_or(&empty);
386 let suffix = self.suffix.as_ref().unwrap_or(&empty);
387
388 let mut updated = Vec::new();
389 for candidate in candidates {
390 updated.push(std::format!("{prefix}{candidate}{suffix}"));
391 }
392
393 candidates = updated;
394 }
395
396 let options = if let Some(options) = &shell.completion_config().current_completion_options {
401 options
402 } else {
403 &self.options
404 };
405
406 let mut processing_options = ProcessingOptions {
407 treat_as_filenames: options.file_names,
408 no_autoquote_filenames: options.no_quote,
409 no_trailing_space_at_end_of_line: options.no_space,
410 };
411
412 if options.plus_dirs || options.dir_names {
413 let mut dir_candidates = get_file_completions(
415 shell,
416 context.token_to_complete,
417 true,
418 )
419 .await;
420 candidates.append(&mut dir_candidates);
421 }
422
423 if candidates.is_empty() && options.bash_default {
426 tracing::debug!(target: trace_categories::COMPLETION, "unimplemented: complete -o bashdefault");
430 }
431
432 if candidates.is_empty() && options.default {
435 let must_be_dir = options.dir_names;
438
439 let mut default_candidates =
440 get_file_completions(shell, context.token_to_complete, must_be_dir).await;
441 candidates.append(&mut default_candidates);
442
443 if shell.completion_config().fallback_options.mark_directories {
444 processing_options.treat_as_filenames = true;
445 }
446 }
447
448 if !self.options.no_sort {
450 candidates.sort();
451 }
452
453 Ok(Answer::Candidates(candidates, processing_options))
454 }
455
456 #[expect(clippy::too_many_lines)]
457 async fn generate_action_completions(
458 &self,
459 shell: &Shell<impl extensions::ShellExtensions>,
460 context: &Context<'_>,
461 ) -> Result<Vec<String>, error::Error> {
462 let mut candidates = Vec::new();
463
464 let token = context.token_to_complete;
465
466 for action in &self.actions {
467 match action {
468 CompleteAction::Alias => {
469 for name in shell.aliases().keys() {
470 if name.starts_with(token) {
471 candidates.push(name.clone());
472 }
473 }
474 }
475 CompleteAction::ArrayVar => {
476 for (name, var) in shell.env().iter() {
477 if var.value().is_array() && name.starts_with(token) {
478 candidates.push(name.to_owned());
479 }
480 }
481 }
482 CompleteAction::Binding => {
483 for input_func in interfaces::InputFunction::iter() {
484 let name: &'static str = input_func.into();
485 if name.starts_with(token) {
486 candidates.push(name.to_string());
487 }
488 }
489 }
490 CompleteAction::Builtin => {
491 for name in shell.builtins().keys() {
492 if name.starts_with(token) {
493 candidates.push(name.to_owned());
494 }
495 }
496 }
497 CompleteAction::Command => {
498 let mut command_completions =
499 get_external_command_completions(shell, context.token_to_complete);
500 candidates.append(&mut command_completions);
501 }
502 CompleteAction::Directory => {
503 let mut file_completions =
504 get_file_completions(shell, context.token_to_complete, true).await;
505 candidates.append(&mut file_completions);
506 }
507 CompleteAction::Disabled => {
508 for (name, registration) in shell.builtins() {
509 if registration.disabled && name.starts_with(token) {
510 candidates.push(name.to_owned());
511 }
512 }
513 }
514 CompleteAction::Enabled => {
515 for (name, registration) in shell.builtins() {
516 if !registration.disabled && name.starts_with(token) {
517 candidates.push(name.to_owned());
518 }
519 }
520 }
521 CompleteAction::Export => {
522 for (key, value) in shell.env().iter() {
523 if value.is_exported() && key.starts_with(token) {
524 candidates.push(key.to_owned());
525 }
526 }
527 }
528 CompleteAction::File => {
529 let mut file_completions =
530 get_file_completions(shell, context.token_to_complete, false).await;
531 candidates.append(&mut file_completions);
532 }
533 CompleteAction::Function => {
534 for (name, _) in shell.funcs().iter() {
535 candidates.push(name.to_owned());
536 }
537 }
538 CompleteAction::Group => {
539 for group_name in users::get_all_groups()? {
540 if group_name.starts_with(token) {
541 candidates.push(group_name);
542 }
543 }
544 }
545 CompleteAction::HelpTopic => {
546 for name in shell.builtins().keys() {
548 if name.starts_with(token) {
549 candidates.push(name.to_owned());
550 }
551 }
552 }
553 CompleteAction::HostName => {
554 if let Ok(name) = sys::network::get_hostname() {
556 let name = name.to_string_lossy();
557 if name.starts_with(token) {
558 candidates.push(name.to_string());
559 }
560 }
561 }
562 CompleteAction::Job => {
563 for job in &shell.jobs().jobs {
564 let command_name = job.command_name();
565 if command_name.starts_with(token) {
566 candidates.push(command_name.to_owned());
567 }
568 }
569 }
570 CompleteAction::Keyword => {
571 for keyword in shell.get_keywords() {
572 if keyword.starts_with(token) {
573 candidates.push(keyword.to_string());
574 }
575 }
576 }
577 CompleteAction::Running => {
578 for job in &shell.jobs().jobs {
579 if matches!(job.state, jobs::JobState::Running) {
580 let command_name = job.command_name();
581 if command_name.starts_with(token) {
582 candidates.push(command_name.to_owned());
583 }
584 }
585 }
586 }
587 CompleteAction::Service => {
588 tracing::debug!(target: trace_categories::COMPLETION, "unimplemented: complete -A service");
589 }
590 CompleteAction::SetOpt => {
591 for option in namedoptions::options(namedoptions::ShellOptionKind::SetO).iter()
592 {
593 if option.name.starts_with(token) {
594 candidates.push(option.name.to_owned());
595 }
596 }
597 }
598 CompleteAction::ShOpt => {
599 for option in namedoptions::options(namedoptions::ShellOptionKind::Shopt).iter()
600 {
601 if option.name.starts_with(token) {
602 candidates.push(option.name.to_owned());
603 }
604 }
605 }
606 CompleteAction::Signal => {
607 for signal in traps::TrapSignal::iterator() {
608 if signal.as_str().starts_with(token) {
609 candidates.push(signal.as_str().to_string());
610 }
611 }
612 }
613 CompleteAction::Stopped => {
614 for job in &shell.jobs().jobs {
615 if matches!(job.state, jobs::JobState::Stopped) {
616 let command_name = job.command_name();
617 if command_name.starts_with(token) {
618 candidates.push(job.command_name().to_owned());
619 }
620 }
621 }
622 }
623 CompleteAction::User => {
624 for user_name in users::get_all_users()? {
625 if user_name.starts_with(token) {
626 candidates.push(user_name);
627 }
628 }
629 }
630 CompleteAction::Variable => {
631 for (key, _) in shell.env().iter() {
632 if key.starts_with(token) {
633 candidates.push(key.to_owned());
634 }
635 }
636 }
637 }
638 }
639
640 Ok(candidates)
641 }
642
643 async fn call_completion_command(
644 &self,
645 shell: &Shell<impl extensions::ShellExtensions>,
646 command_name: &str,
647 context: &Context<'_>,
648 ) -> Result<Vec<String>, error::Error> {
649 let mut shell = shell.clone();
651
652 let vars_and_values: Vec<(&str, ShellValueLiteral)> = vec![
653 ("COMP_LINE", context.input_line.into()),
654 ("COMP_POINT", context.cursor_index.to_string().into()),
655 ("COMP_KEY", context.trigger.comp_key().to_string().into()),
656 ("COMP_TYPE", context.trigger.comp_type().to_string().into()),
657 ];
658
659 for (var, value) in vars_and_values {
661 shell.env_mut().update_or_add(
662 var,
663 value,
664 |v| {
665 v.export();
666 Ok(())
667 },
668 env::EnvironmentLookup::Anywhere,
669 env::EnvironmentScope::Global,
670 )?;
671 }
672
673 let mut args = vec![
675 context.command_name.unwrap_or(""),
676 context.token_to_complete,
677 ];
678 if let Some(preceding_token) = context.preceding_token {
679 args.push(preceding_token);
680 }
681
682 let mut command_line = command_name.to_owned();
684 for arg in args {
685 command_line.push(' ');
686
687 let escaped_arg = escape::quote_if_needed(arg, escape::QuoteMode::SingleQuote);
688 command_line.push_str(escaped_arg.as_ref());
689 }
690
691 let params = shell.default_exec_params();
693 let output =
694 commands::invoke_command_in_subshell_and_get_output(&mut shell, ¶ms, command_line)
695 .await?;
696
697 let mut candidates = Vec::new();
699 for line in output.lines() {
700 candidates.push(line.to_owned());
701 }
702
703 Ok(candidates)
704 }
705
706 async fn call_completion_function(
707 &self,
708 shell: &mut Shell<impl extensions::ShellExtensions>,
709 function_name: &str,
710 context: &Context<'_>,
711 ) -> Result<Answer, error::Error> {
712 let vars_and_values: Vec<(&str, ShellValueLiteral)> = vec![
714 ("COMP_LINE", context.input_line.into()),
715 ("COMP_POINT", context.cursor_index.to_string().into()),
716 ("COMP_KEY", context.trigger.comp_key().to_string().into()),
717 ("COMP_TYPE", context.trigger.comp_type().to_string().into()),
718 (
719 "COMP_WORDS",
720 context
721 .tokens
722 .iter()
723 .map(|t| t.text)
724 .collect::<Vec<_>>()
725 .into(),
726 ),
727 ("COMP_CWORD", context.token_index.to_string().into()),
728 ];
729
730 tracing::debug!(target: trace_categories::COMPLETION, "[calling completion func '{function_name}']: {}",
731 vars_and_values.iter().map(|(k, v)| std::format!("{k}={v}")).collect::<Vec<String>>().join(" "));
732
733 let mut vars_to_remove = Vec::with_capacity(vars_and_values.len());
734 for (var, value) in vars_and_values {
735 shell.env_mut().update_or_add(
736 var,
737 value,
738 |_| Ok(()),
739 env::EnvironmentLookup::Anywhere,
740 env::EnvironmentScope::Global,
741 )?;
742
743 vars_to_remove.push(var);
744 }
745
746 let mut args = vec![
747 context.command_name.unwrap_or(""),
748 context.token_to_complete,
749 ];
750 if let Some(preceding_token) = context.preceding_token {
751 args.push(preceding_token);
752 }
753
754 shell.enter_trap_handler(None);
757
758 let params = shell.default_exec_params();
759 let invoke_result = shell
760 .invoke_function(function_name, args.iter(), ¶ms)
761 .await;
762 tracing::debug!(target: trace_categories::COMPLETION, "[completion function '{function_name}' returned: {invoke_result:?}]");
763
764 shell.leave_trap_handler();
765
766 for var_name in vars_to_remove {
768 let _ = shell.env_mut().unset(var_name);
769 }
770
771 let result = invoke_result.unwrap_or_else(|e| {
772 tracing::warn!(target: trace_categories::COMPLETION, "error while running completion function '{function_name}': {e}");
773 1 });
775
776 if result == 124 {
779 Ok(Answer::RestartCompletionProcess)
780 } else {
781 if let Some(reply) = shell.env_mut().unset("COMPREPLY")? {
782 tracing::debug!(target: trace_categories::COMPLETION, "[completion function yielded: {reply:?}]");
783
784 match reply.value() {
785 variables::ShellValue::IndexedArray(values) => {
786 return Ok(Answer::Candidates(
787 values.values().map(|v| v.to_owned()).collect(),
788 ProcessingOptions::default(),
789 ));
790 }
791 variables::ShellValue::String(s) => {
792 let candidates = vec![s.to_owned()];
793 return Ok(Answer::Candidates(candidates, ProcessingOptions::default()));
794 }
795 _ => (),
796 }
797 }
798
799 Ok(Answer::Candidates(Vec::new(), ProcessingOptions::default()))
800 }
801 }
802}
803
804#[derive(Debug, Default)]
806pub struct Completions {
807 pub insertion_index: usize,
810 pub delete_count: usize,
813 pub candidates: Vec<String>,
815 pub options: ProcessingOptions,
817}
818
819#[derive(Debug)]
821pub struct ProcessingOptions {
822 pub treat_as_filenames: bool,
824 pub no_autoquote_filenames: bool,
826 pub no_trailing_space_at_end_of_line: bool,
828}
829
830#[derive(Debug, Clone, Copy)]
832pub struct CompletionToken<'a> {
833 pub text: &'a str,
835 pub start: usize,
837}
838
839impl CompletionToken<'_> {
840 pub const fn length(&self) -> usize {
842 self.text.len()
843 }
844
845 pub const fn end(&self) -> usize {
847 self.start + self.length()
848 }
849}
850
851impl Default for ProcessingOptions {
852 fn default() -> Self {
853 Self {
854 treat_as_filenames: true,
855 no_autoquote_filenames: false,
856 no_trailing_space_at_end_of_line: false,
857 }
858 }
859}
860
861pub enum Answer {
863 Candidates(Vec<String>, ProcessingOptions),
866 RestartCompletionProcess,
868}
869
870const EMPTY_COMMAND: &str = "_EmptycmD_";
871const DEFAULT_COMMAND: &str = "_DefaultCmD_";
872const INITIAL_WORD: &str = "_InitialWorD_";
873
874impl Config {
875 pub fn clear(&mut self) {
877 self.commands.clear();
878 self.empty_line = None;
879 self.default = None;
880 self.initial_word = None;
881 }
882
883 pub fn remove(&mut self, name: &str) -> bool {
890 match name {
891 EMPTY_COMMAND => {
892 let result = self.empty_line.is_some();
893 self.empty_line = None;
894 result
895 }
896 DEFAULT_COMMAND => {
897 let result = self.default.is_some();
898 self.default = None;
899 result
900 }
901 INITIAL_WORD => {
902 let result = self.initial_word.is_some();
903 self.initial_word = None;
904 result
905 }
906 _ => self.commands.remove(name).is_some(),
907 }
908 }
909
910 pub fn iter(&self) -> impl Iterator<Item = (&String, &Spec)> {
912 self.commands.iter()
913 }
914
915 pub fn get(&self, name: &str) -> Option<&Spec> {
921 match name {
922 EMPTY_COMMAND => self.empty_line.as_ref(),
923 DEFAULT_COMMAND => self.default.as_ref(),
924 INITIAL_WORD => self.initial_word.as_ref(),
925 _ => self.commands.get(name),
926 }
927 }
928
929 pub fn set(&mut self, name: &str, spec: Spec) {
937 match name {
938 EMPTY_COMMAND => {
939 self.empty_line = Some(spec);
940 }
941 DEFAULT_COMMAND => {
942 self.default = Some(spec);
943 }
944 INITIAL_WORD => {
945 self.initial_word = Some(spec);
946 }
947 _ => {
948 self.commands.insert(name.to_owned(), spec);
949 }
950 }
951 }
952
953 #[allow(
962 clippy::missing_panics_doc,
963 clippy::unwrap_used,
964 reason = "these unwrap calls should not fail"
965 )]
966 pub fn get_or_add_mut(&mut self, name: &str) -> &mut Spec {
967 match name {
968 EMPTY_COMMAND => {
969 if self.empty_line.is_none() {
970 self.empty_line = Some(Spec::default());
971 }
972 self.empty_line.as_mut().unwrap()
973 }
974 DEFAULT_COMMAND => {
975 if self.default.is_none() {
976 self.default = Some(Spec::default());
977 }
978 self.default.as_mut().unwrap()
979 }
980 INITIAL_WORD => {
981 if self.initial_word.is_none() {
982 self.initial_word = Some(Spec::default());
983 }
984 self.initial_word.as_mut().unwrap()
985 }
986 _ => self.commands.entry(name.to_owned()).or_default(),
987 }
988 }
989
990 #[expect(clippy::string_slice)]
998 pub async fn get_completions(
999 &self,
1000 shell: &mut Shell<impl extensions::ShellExtensions>,
1001 input: &str,
1002 position: usize,
1003 ) -> Result<Completions, error::Error> {
1004 const MAX_RESTARTS: u32 = 10;
1005
1006 let tokens = Self::tokenize_input_for_completion(shell, input);
1008
1009 let cursor = position;
1010 let mut preceding_token = None;
1011 let mut completion_prefix = "";
1012 let mut insertion_index = cursor;
1013 let mut completion_token_index = tokens.len();
1014
1015 let mut adjusted_tokens: Vec<&CompletionToken<'_>> = tokens.iter().collect();
1018
1019 for (i, token) in tokens.iter().enumerate() {
1021 if cursor < token.start {
1025 completion_token_index = i;
1028 break;
1029 }
1030 else if cursor >= token.start && cursor <= token.end() {
1036 insertion_index = token.start;
1038
1039 let offset_into_token = cursor - insertion_index;
1041 let token_str = token.text;
1042 completion_prefix = &token_str[..offset_into_token];
1043
1044 completion_token_index = i;
1046
1047 break;
1048 }
1049
1050 preceding_token = Some(token);
1053 }
1054
1055 let empty_token = CompletionToken {
1058 text: "",
1059 start: input.len(),
1060 };
1061 if completion_token_index == tokens.len() {
1062 adjusted_tokens.push(&empty_token);
1063 }
1064
1065 let mut result = Answer::RestartCompletionProcess;
1067 let mut restart_count = 0;
1068 while matches!(result, Answer::RestartCompletionProcess) {
1069 if restart_count > MAX_RESTARTS {
1070 tracing::warn!("possible infinite loop detected in completion process");
1071 break;
1072 }
1073
1074 let completion_context = Context {
1075 token_to_complete: completion_prefix,
1076 preceding_token: preceding_token.map(|t| t.text),
1077 command_name: adjusted_tokens.first().map(|token| token.text),
1078 input_line: input,
1079 token_index: completion_token_index,
1080 tokens: adjusted_tokens.as_slice(),
1081 cursor_index: position,
1082 trigger: CompletionTrigger::InteractiveComplete,
1083 };
1084
1085 result = self
1086 .get_completions_for_token(shell, completion_context)
1087 .await;
1088
1089 restart_count += 1;
1090 }
1091
1092 match result {
1093 Answer::Candidates(candidates, options) => Ok(Completions {
1094 insertion_index,
1095 delete_count: completion_prefix.len(),
1096 candidates,
1097 options,
1098 }),
1099 Answer::RestartCompletionProcess => Ok(Completions {
1100 insertion_index,
1101 delete_count: 0,
1102 candidates: Vec::new(),
1103 options: ProcessingOptions::default(),
1104 }),
1105 }
1106 }
1107
1108 fn tokenize_input_for_completion<'a>(
1109 shell: &Shell<impl extensions::ShellExtensions>,
1110 input: &'a str,
1111 ) -> Vec<CompletionToken<'a>> {
1112 const FALLBACK: &str = " \t\n\"\'@><=;|&(:";
1113
1114 let delimiter_str = shell
1115 .env_str("COMP_WORDBREAKS")
1116 .unwrap_or_else(|| FALLBACK.into());
1117
1118 let delimiters: Vec<_> = delimiter_str.chars().collect();
1119
1120 simple_tokenize_by_delimiters(input, delimiters.as_slice())
1121 }
1122
1123 async fn get_completions_for_token(
1124 &self,
1125 shell: &mut Shell<impl extensions::ShellExtensions>,
1126 context: Context<'_>,
1127 ) -> Answer {
1128 let mut found_spec: Option<&Spec> = None;
1130
1131 if let Some(command_name) = context.command_name {
1132 if context.token_index == 0 {
1133 if let Some(spec) = &self.initial_word {
1134 found_spec = Some(spec);
1135 }
1136 } else {
1137 if let Some(spec) = shell.completion_config().commands.get(command_name) {
1138 found_spec = Some(spec);
1139 } else if let Some(file_name) = PathBuf::from(command_name).file_name()
1140 && let Some(spec) = shell
1141 .completion_config()
1142 .commands
1143 .get(&file_name.to_string_lossy().to_string())
1144 {
1145 found_spec = Some(spec);
1146 }
1147
1148 if found_spec.is_none()
1149 && let Some(spec) = &self.default
1150 {
1151 found_spec = Some(spec);
1152 }
1153 }
1154 } else {
1155 if let Some(spec) = &self.empty_line {
1156 found_spec = Some(spec);
1157 }
1158 }
1159
1160 if let Some(spec) = found_spec {
1162 spec.to_owned()
1163 .get_completions(shell, &context)
1164 .await
1165 .unwrap_or_else(|_err| Answer::Candidates(Vec::new(), ProcessingOptions::default()))
1166 } else {
1167 get_completions_using_basic_lookup(shell, &context).await
1169 }
1170 }
1171}
1172
1173async fn get_file_completions(
1174 shell: &Shell<impl extensions::ShellExtensions>,
1175 token_to_complete: &str,
1176 must_be_dir: bool,
1177) -> Vec<String> {
1178 let mut throwaway_shell = shell.clone();
1180 let params = throwaway_shell.default_exec_params();
1181 let options = expansion::ExpanderOptions {
1182 execute_command_substitutions: false,
1183 ..Default::default()
1184 };
1185 let expanded_token = expansion::basic_expand_word_with_options(
1186 &mut throwaway_shell,
1187 ¶ms,
1188 &unquote_str(token_to_complete),
1189 &options,
1190 )
1191 .await
1192 .unwrap_or_else(|_err| token_to_complete.to_owned());
1193
1194 let glob = std::format!("{expanded_token}*");
1195
1196 let path_filter = |path: &Path| !must_be_dir || shell.absolute_path(path).is_dir();
1197
1198 let pattern = patterns::Pattern::from(glob)
1199 .set_extended_globbing(shell.options().extended_globbing)
1200 .set_case_insensitive(shell.options().case_insensitive_pathname_expansion);
1201
1202 pattern
1203 .expand(
1204 shell.working_dir(),
1205 Some(&path_filter),
1206 &patterns::FilenameExpansionOptions::default(),
1207 )
1208 .unwrap_or_default()
1209 .into_paths()
1210 .into_iter()
1211 .collect()
1212}
1213
1214fn get_external_command_completions(
1215 shell: &Shell<impl extensions::ShellExtensions>,
1216 prefix: &str,
1217) -> Vec<String> {
1218 let mut candidates = Vec::new();
1219
1220 for path in shell.find_executables_in_path_with_prefix(
1222 prefix,
1223 shell.options().case_insensitive_pathname_expansion,
1224 ) {
1225 if let Some(file_name) = path.file_name() {
1226 candidates.push(file_name.to_string_lossy().to_string());
1227 }
1228 }
1229
1230 candidates.into_iter().collect()
1231}
1232
1233fn try_get_variable_completions(
1242 shell: &Shell<impl extensions::ShellExtensions>,
1243 token: &str,
1244) -> Option<Answer> {
1245 let (var_prefix, use_braces) = if let Some(prefix) = token.strip_prefix("${") {
1247 if prefix.contains('}') {
1249 return None;
1250 }
1251 (prefix, true)
1252 } else if let Some(prefix) = token.strip_prefix('$') {
1253 (prefix, false)
1254 } else {
1255 return None;
1256 };
1257
1258 if var_prefix.contains(std::path::MAIN_SEPARATOR) {
1260 return None;
1261 }
1262
1263 let mut candidates: Vec<String> = shell
1265 .env()
1266 .iter()
1267 .filter(|(key, _)| key.starts_with(var_prefix))
1268 .map(|(key, _)| {
1269 if use_braces {
1270 format!("${{{key}}}")
1271 } else {
1272 format!("${key}")
1273 }
1274 })
1275 .collect();
1276 candidates.sort();
1277
1278 let options = ProcessingOptions {
1280 treat_as_filenames: false,
1281 ..ProcessingOptions::default()
1282 };
1283
1284 Some(Answer::Candidates(candidates, options))
1285}
1286
1287fn add_command_completions(
1290 shell: &Shell<impl extensions::ShellExtensions>,
1291 prefix: &str,
1292 candidates: &mut Vec<String>,
1293) {
1294 let mut command_completions = get_external_command_completions(shell, prefix);
1296 candidates.append(&mut command_completions);
1297
1298 for (name, registration) in shell.builtins() {
1300 if !registration.disabled && name.starts_with(prefix) {
1301 candidates.push(name.to_owned());
1302 }
1303 }
1304
1305 for (name, _) in shell.funcs().iter() {
1307 if name.starts_with(prefix) {
1308 candidates.push(name.to_owned());
1309 }
1310 }
1311
1312 for name in shell.aliases().keys() {
1314 if name.starts_with(prefix) {
1315 candidates.push(name.to_owned());
1316 }
1317 }
1318
1319 for keyword in shell.get_keywords() {
1321 if keyword.starts_with(prefix) {
1322 candidates.push(keyword.to_string());
1323 }
1324 }
1325}
1326
1327async fn get_completions_using_basic_lookup(
1328 shell: &Shell<impl extensions::ShellExtensions>,
1329 context: &Context<'_>,
1330) -> Answer {
1331 let token = context.token_to_complete;
1332
1333 if let Some(answer) = try_get_variable_completions(shell, token) {
1335 return answer;
1336 }
1337
1338 let mut candidates = get_file_completions(shell, token, false).await;
1340
1341 let is_command_position =
1346 context.token_index == 0 && !token.is_empty() && !token.contains(std::path::MAIN_SEPARATOR);
1347
1348 if is_command_position {
1349 add_command_completions(shell, token, &mut candidates);
1350 candidates.sort();
1351 }
1352
1353 #[cfg(windows)]
1354 {
1355 candidates = candidates
1356 .into_iter()
1357 .map(|c| c.replace('\\', "/"))
1358 .collect();
1359 }
1360
1361 Answer::Candidates(candidates, ProcessingOptions::default())
1362}
1363
1364#[allow(clippy::string_slice, reason = "used indices come from char_indices")]
1368fn simple_tokenize_by_delimiters<'a>(
1369 input: &'a str,
1370 delimiters: &[char],
1371) -> Vec<CompletionToken<'a>> {
1372 let mut tokens = vec![];
1373 let mut word_start = None;
1374 let mut word_is_delimiters = false;
1375 let mut quote_char: Option<char> = None;
1376 let mut escaped = false;
1377
1378 for (i, c) in input.char_indices() {
1379 let mut is_active_delimiter = false;
1380 if escaped {
1381 escaped = false;
1382 } else if let Some(q) = quote_char {
1383 if c == '\\' && q == '"' {
1384 escaped = true;
1386 } else if c == q {
1387 quote_char = None;
1389 }
1390 } else {
1391 if c == '\\' {
1392 escaped = true;
1393 } else if word_start.is_none() && (c == '\'' || c == '\"') {
1394 quote_char = Some(c);
1396 } else {
1397 is_active_delimiter = delimiters.contains(&c);
1398 }
1399 }
1400
1401 if is_active_delimiter {
1402 if let Some(start) = word_start {
1405 if !word_is_delimiters || c.is_ascii_whitespace() {
1406 tokens.push(CompletionToken {
1407 text: &input[start..i],
1408 start,
1409 });
1410 word_start = None;
1411 word_is_delimiters = false;
1412 }
1413
1414 if !c.is_ascii_whitespace() && word_start.is_none() {
1415 word_start = Some(i);
1416 word_is_delimiters = true;
1417 }
1418 } else if !c.is_ascii_whitespace() {
1419 if word_start.is_none() {
1421 word_start = Some(i);
1422 word_is_delimiters = true;
1423 }
1424 }
1425 } else {
1426 if word_is_delimiters && let Some(start) = word_start {
1428 tokens.push(CompletionToken {
1429 text: &input[start..i],
1430 start,
1431 });
1432 word_start = None;
1433 word_is_delimiters = false;
1434 }
1435
1436 if word_start.is_none() {
1438 word_start = Some(i);
1439 }
1440 }
1441 }
1442
1443 if let Some(start) = word_start {
1445 tokens.push(CompletionToken {
1446 text: &input[start..],
1447 start,
1448 });
1449 }
1450
1451 tokens
1452}
1453
1454fn completion_filter_pattern_matches(
1455 pattern: &str,
1456 candidate: &str,
1457 token_being_completed: &str,
1458 shell: &Shell<impl extensions::ShellExtensions>,
1459) -> Result<bool, error::Error> {
1460 let pattern = replace_unescaped_ampersands(pattern, token_being_completed);
1461
1462 let pattern = patterns::Pattern::from(pattern.as_ref())
1467 .set_extended_globbing(shell.options().extended_globbing)
1468 .set_case_insensitive(shell.options().case_insensitive_pathname_expansion);
1469
1470 let matches = pattern.exactly_matches(candidate)?;
1471
1472 Ok(matches)
1473}
1474
1475fn replace_unescaped_ampersands<'a>(pattern: &'a str, replacement: &str) -> Cow<'a, str> {
1476 let mut in_escape = false;
1477 let mut insertion_points = vec![];
1478
1479 for (i, c) in pattern.char_indices() {
1480 if !in_escape && c == '&' {
1481 insertion_points.push(i);
1482 }
1483 in_escape = !in_escape && c == '\\';
1484 }
1485
1486 if insertion_points.is_empty() {
1487 return pattern.into();
1488 }
1489
1490 let mut result = pattern.to_owned();
1491 for i in insertion_points.iter().rev() {
1492 result.replace_range(*i..=*i, replacement);
1493 }
1494
1495 result.into()
1496}
1497
1498#[cfg(test)]
1499mod tests {
1500 use super::*;
1501 use pretty_assertions::assert_matches;
1502
1503 #[test]
1504 #[allow(clippy::too_many_lines)]
1505 fn completion_tokenization() {
1506 assert_matches!(
1507 simple_tokenize_by_delimiters("one two", &[' ']).as_slice(),
1508 [
1509 CompletionToken {
1510 text: "one",
1511 start: 0,
1512 },
1513 CompletionToken {
1514 text: "two",
1515 start: 4,
1516 }
1517 ]
1518 );
1519
1520 assert_matches!(
1521 simple_tokenize_by_delimiters("one \t two", &[' ', '\t']).as_slice(),
1522 [
1523 CompletionToken {
1524 text: "one",
1525 start: 0,
1526 },
1527 CompletionToken {
1528 text: "two",
1529 start: 6,
1530 }
1531 ]
1532 );
1533
1534 assert_matches!(simple_tokenize_by_delimiters(" ", &[' ']).as_slice(), []);
1535
1536 assert_matches!(
1537 simple_tokenize_by_delimiters(":", &[':']).as_slice(),
1538 [CompletionToken {
1539 text: ":",
1540 start: 0,
1541 }]
1542 );
1543
1544 assert_matches!(
1545 simple_tokenize_by_delimiters("a:::b", &[':', ' ']).as_slice(),
1546 [
1547 CompletionToken {
1548 text: "a",
1549 start: 0,
1550 },
1551 CompletionToken {
1552 text: ":::",
1553 start: 1,
1554 },
1555 CompletionToken {
1556 text: "b",
1557 start: 4,
1558 }
1559 ]
1560 );
1561
1562 assert_matches!(
1563 simple_tokenize_by_delimiters("a: : :b", &[':', ' ']).as_slice(),
1564 [
1565 CompletionToken {
1566 text: "a",
1567 start: 0,
1568 },
1569 CompletionToken {
1570 text: ":",
1571 start: 1,
1572 },
1573 CompletionToken {
1574 text: ":",
1575 start: 3,
1576 },
1577 CompletionToken {
1578 text: ":",
1579 start: 5,
1580 },
1581 CompletionToken {
1582 text: "b",
1583 start: 6,
1584 }
1585 ]
1586 );
1587
1588 assert_matches!(
1589 simple_tokenize_by_delimiters("one two:three", &[':', ' ']).as_slice(),
1590 [
1591 CompletionToken {
1592 text: "one",
1593 start: 0,
1594 },
1595 CompletionToken {
1596 text: "two",
1597 start: 4,
1598 },
1599 CompletionToken {
1600 text: ":",
1601 start: 7,
1602 },
1603 CompletionToken {
1604 text: "three",
1605 start: 8,
1606 }
1607 ]
1608 );
1609
1610 assert_matches!(
1611 simple_tokenize_by_delimiters("one'two", &['\'']).as_slice(),
1612 [
1613 CompletionToken {
1614 text: "one",
1615 start: 0,
1616 },
1617 CompletionToken {
1618 text: "'",
1619 start: 3,
1620 },
1621 CompletionToken {
1622 text: "two",
1623 start: 4,
1624 },
1625 ]
1626 );
1627
1628 assert_matches!(
1629 simple_tokenize_by_delimiters("one 'two:three'", &[':', ' ']).as_slice(),
1630 [
1631 CompletionToken {
1632 text: "one",
1633 start: 0,
1634 },
1635 CompletionToken {
1636 text: "'two:three'",
1637 start: 4,
1638 },
1639 ]
1640 );
1641
1642 assert_matches!(
1643 simple_tokenize_by_delimiters("one \\'two \"two four\"", &[':', ' ']).as_slice(),
1644 [
1645 CompletionToken {
1646 text: "one",
1647 start: 0,
1648 },
1649 CompletionToken {
1650 text: "\\'two",
1651 start: 4,
1652 },
1653 CompletionToken {
1654 text: "\"two four\"",
1655 start: 10,
1656 },
1657 ]
1658 );
1659 }
1660}