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 for name in shell.builtins().keys() {
502 if name.starts_with(token) {
503 candidates.push(name.to_owned());
504 }
505 }
506 for keyword in shell.get_keywords() {
507 if keyword.starts_with(token) {
508 candidates.push(keyword.to_string());
509 }
510 }
511 for (name, _) in shell.funcs().iter() {
512 candidates.push(name.to_owned());
513 }
514 }
515 CompleteAction::Directory => {
516 let mut file_completions =
517 get_file_completions(shell, context.token_to_complete, true).await;
518 candidates.append(&mut file_completions);
519 }
520 CompleteAction::Disabled => {
521 for (name, registration) in shell.builtins() {
522 if registration.disabled && name.starts_with(token) {
523 candidates.push(name.to_owned());
524 }
525 }
526 }
527 CompleteAction::Enabled => {
528 for (name, registration) in shell.builtins() {
529 if !registration.disabled && name.starts_with(token) {
530 candidates.push(name.to_owned());
531 }
532 }
533 }
534 CompleteAction::Export => {
535 for (key, value) in shell.env().iter() {
536 if value.is_exported() && key.starts_with(token) {
537 candidates.push(key.to_owned());
538 }
539 }
540 }
541 CompleteAction::File => {
542 let mut file_completions =
543 get_file_completions(shell, context.token_to_complete, false).await;
544 candidates.append(&mut file_completions);
545 }
546 CompleteAction::Function => {
547 for (name, _) in shell.funcs().iter() {
548 candidates.push(name.to_owned());
549 }
550 }
551 CompleteAction::Group => {
552 for group_name in users::get_all_groups()? {
553 if group_name.starts_with(token) {
554 candidates.push(group_name);
555 }
556 }
557 }
558 CompleteAction::HelpTopic => {
559 for name in shell.builtins().keys() {
561 if name.starts_with(token) {
562 candidates.push(name.to_owned());
563 }
564 }
565 }
566 CompleteAction::HostName => {
567 if let Ok(name) = sys::network::get_hostname() {
569 let name = name.to_string_lossy();
570 if name.starts_with(token) {
571 candidates.push(name.to_string());
572 }
573 }
574 }
575 CompleteAction::Job => {
576 for job in &shell.jobs().jobs {
577 let command_name = job.command_name();
578 if command_name.starts_with(token) {
579 candidates.push(command_name.to_owned());
580 }
581 }
582 }
583 CompleteAction::Keyword => {
584 for keyword in shell.get_keywords() {
585 if keyword.starts_with(token) {
586 candidates.push(keyword.to_string());
587 }
588 }
589 }
590 CompleteAction::Running => {
591 for job in &shell.jobs().jobs {
592 if matches!(job.state, jobs::JobState::Running) {
593 let command_name = job.command_name();
594 if command_name.starts_with(token) {
595 candidates.push(command_name.to_owned());
596 }
597 }
598 }
599 }
600 CompleteAction::Service => {
601 tracing::debug!(target: trace_categories::COMPLETION, "unimplemented: complete -A service");
602 }
603 CompleteAction::SetOpt => {
604 for option in namedoptions::options(namedoptions::ShellOptionKind::SetO).iter()
605 {
606 if option.name.starts_with(token) {
607 candidates.push(option.name.to_owned());
608 }
609 }
610 }
611 CompleteAction::ShOpt => {
612 for option in namedoptions::options(namedoptions::ShellOptionKind::Shopt).iter()
613 {
614 if option.name.starts_with(token) {
615 candidates.push(option.name.to_owned());
616 }
617 }
618 }
619 CompleteAction::Signal => {
620 for signal in traps::TrapSignal::iterator() {
621 if signal.as_str().starts_with(token) {
622 candidates.push(signal.as_str().to_string());
623 }
624 }
625 }
626 CompleteAction::Stopped => {
627 for job in &shell.jobs().jobs {
628 if matches!(job.state, jobs::JobState::Stopped) {
629 let command_name = job.command_name();
630 if command_name.starts_with(token) {
631 candidates.push(job.command_name().to_owned());
632 }
633 }
634 }
635 }
636 CompleteAction::User => {
637 for user_name in users::get_all_users()? {
638 if user_name.starts_with(token) {
639 candidates.push(user_name);
640 }
641 }
642 }
643 CompleteAction::Variable => {
644 for (key, _) in shell.env().iter() {
645 if key.starts_with(token) {
646 candidates.push(key.to_owned());
647 }
648 }
649 }
650 }
651 }
652
653 Ok(candidates)
654 }
655
656 async fn call_completion_command(
657 &self,
658 shell: &Shell<impl extensions::ShellExtensions>,
659 command_name: &str,
660 context: &Context<'_>,
661 ) -> Result<Vec<String>, error::Error> {
662 let mut shell = shell.clone();
664
665 let vars_and_values: Vec<(&str, ShellValueLiteral)> = vec![
666 ("COMP_LINE", context.input_line.into()),
667 ("COMP_POINT", context.cursor_index.to_string().into()),
668 ("COMP_KEY", context.trigger.comp_key().to_string().into()),
669 ("COMP_TYPE", context.trigger.comp_type().to_string().into()),
670 ];
671
672 for (var, value) in vars_and_values {
674 shell.env_mut().update_or_add(
675 var,
676 value,
677 |v| {
678 v.export();
679 Ok(())
680 },
681 env::EnvironmentLookup::Anywhere,
682 env::EnvironmentScope::Global,
683 )?;
684 }
685
686 let mut args = vec![
688 context.command_name.unwrap_or(""),
689 context.token_to_complete,
690 ];
691 if let Some(preceding_token) = context.preceding_token {
692 args.push(preceding_token);
693 }
694
695 let mut command_line = command_name.to_owned();
697 for arg in args {
698 command_line.push(' ');
699
700 let escaped_arg = escape::quote_if_needed(arg, escape::QuoteMode::SingleQuote);
701 command_line.push_str(escaped_arg.as_ref());
702 }
703
704 let params = shell.default_exec_params();
706 let output =
707 commands::invoke_command_in_subshell_and_get_output(&mut shell, ¶ms, command_line)
708 .await?;
709
710 let mut candidates = Vec::new();
712 for line in output.lines() {
713 candidates.push(line.to_owned());
714 }
715
716 Ok(candidates)
717 }
718
719 async fn call_completion_function(
720 &self,
721 shell: &mut Shell<impl extensions::ShellExtensions>,
722 function_name: &str,
723 context: &Context<'_>,
724 ) -> Result<Answer, error::Error> {
725 let vars_and_values: Vec<(&str, ShellValueLiteral)> = vec![
727 ("COMP_LINE", context.input_line.into()),
728 ("COMP_POINT", context.cursor_index.to_string().into()),
729 ("COMP_KEY", context.trigger.comp_key().to_string().into()),
730 ("COMP_TYPE", context.trigger.comp_type().to_string().into()),
731 (
732 "COMP_WORDS",
733 context
734 .tokens
735 .iter()
736 .map(|t| t.text)
737 .collect::<Vec<_>>()
738 .into(),
739 ),
740 ("COMP_CWORD", context.token_index.to_string().into()),
741 ];
742
743 tracing::debug!(target: trace_categories::COMPLETION, "[calling completion func '{function_name}']: {}",
744 vars_and_values.iter().map(|(k, v)| std::format!("{k}={v}")).collect::<Vec<String>>().join(" "));
745
746 let mut vars_to_remove = Vec::with_capacity(vars_and_values.len());
747 for (var, value) in vars_and_values {
748 shell.env_mut().update_or_add(
749 var,
750 value,
751 |_| Ok(()),
752 env::EnvironmentLookup::Anywhere,
753 env::EnvironmentScope::Global,
754 )?;
755
756 vars_to_remove.push(var);
757 }
758
759 let mut args = vec![
760 context.command_name.unwrap_or(""),
761 context.token_to_complete,
762 ];
763 if let Some(preceding_token) = context.preceding_token {
764 args.push(preceding_token);
765 }
766
767 shell.acquire_trap_delivery_block();
774
775 let params = shell.default_exec_params();
776 let invoke_result = shell
777 .invoke_function(function_name, args.iter(), ¶ms)
778 .await;
779
780 tracing::debug!(target: trace_categories::COMPLETION, "[completion function '{function_name}' returned: {invoke_result:?}]");
781
782 shell.release_trap_delivery_block();
783
784 for var_name in vars_to_remove {
786 let _ = shell.env_mut().unset(var_name);
787 }
788
789 let result = invoke_result.unwrap_or_else(|e| {
790 tracing::warn!(target: trace_categories::COMPLETION, "error while running completion function '{function_name}': {e}");
791 1 });
793
794 if result == 124 {
797 Ok(Answer::RestartCompletionProcess)
798 } else {
799 if let Some(reply) = shell.env_mut().unset("COMPREPLY")? {
800 tracing::debug!(target: trace_categories::COMPLETION, "[completion function yielded: {reply:?}]");
801
802 match reply.value() {
803 variables::ShellValue::IndexedArray(values) => {
804 return Ok(Answer::Candidates(
805 values.values().map(|v| v.to_owned()).collect(),
806 ProcessingOptions::default(),
807 ));
808 }
809 variables::ShellValue::String(s) => {
810 let candidates = vec![s.to_owned()];
811 return Ok(Answer::Candidates(candidates, ProcessingOptions::default()));
812 }
813 _ => (),
814 }
815 }
816
817 Ok(Answer::Candidates(Vec::new(), ProcessingOptions::default()))
818 }
819 }
820}
821
822#[derive(Debug, Default)]
824pub struct Completions {
825 pub insertion_index: usize,
828 pub delete_count: usize,
831 pub candidates: Vec<String>,
833 pub options: ProcessingOptions,
835}
836
837#[derive(Debug)]
839pub struct ProcessingOptions {
840 pub treat_as_filenames: bool,
842 pub no_autoquote_filenames: bool,
844 pub no_trailing_space_at_end_of_line: bool,
846}
847
848#[derive(Debug, Clone, Copy)]
850pub struct CompletionToken<'a> {
851 pub text: &'a str,
853 pub start: usize,
855}
856
857impl CompletionToken<'_> {
858 pub const fn length(&self) -> usize {
860 self.text.len()
861 }
862
863 pub const fn end(&self) -> usize {
865 self.start + self.length()
866 }
867}
868
869impl Default for ProcessingOptions {
870 fn default() -> Self {
871 Self {
872 treat_as_filenames: true,
873 no_autoquote_filenames: false,
874 no_trailing_space_at_end_of_line: false,
875 }
876 }
877}
878
879pub enum Answer {
881 Candidates(Vec<String>, ProcessingOptions),
884 RestartCompletionProcess,
886}
887
888const EMPTY_COMMAND: &str = "_EmptycmD_";
889const DEFAULT_COMMAND: &str = "_DefaultCmD_";
890const INITIAL_WORD: &str = "_InitialWorD_";
891
892impl Config {
893 pub fn clear(&mut self) {
895 self.commands.clear();
896 self.empty_line = None;
897 self.default = None;
898 self.initial_word = None;
899 }
900
901 pub fn remove(&mut self, name: &str) -> bool {
908 match name {
909 EMPTY_COMMAND => {
910 let result = self.empty_line.is_some();
911 self.empty_line = None;
912 result
913 }
914 DEFAULT_COMMAND => {
915 let result = self.default.is_some();
916 self.default = None;
917 result
918 }
919 INITIAL_WORD => {
920 let result = self.initial_word.is_some();
921 self.initial_word = None;
922 result
923 }
924 _ => self.commands.remove(name).is_some(),
925 }
926 }
927
928 pub fn iter(&self) -> impl Iterator<Item = (&String, &Spec)> {
930 self.commands.iter()
931 }
932
933 pub fn get(&self, name: &str) -> Option<&Spec> {
939 match name {
940 EMPTY_COMMAND => self.empty_line.as_ref(),
941 DEFAULT_COMMAND => self.default.as_ref(),
942 INITIAL_WORD => self.initial_word.as_ref(),
943 _ => self.commands.get(name),
944 }
945 }
946
947 pub fn set(&mut self, name: &str, spec: Spec) {
955 match name {
956 EMPTY_COMMAND => {
957 self.empty_line = Some(spec);
958 }
959 DEFAULT_COMMAND => {
960 self.default = Some(spec);
961 }
962 INITIAL_WORD => {
963 self.initial_word = Some(spec);
964 }
965 _ => {
966 self.commands.insert(name.to_owned(), spec);
967 }
968 }
969 }
970
971 #[allow(
980 clippy::missing_panics_doc,
981 clippy::unwrap_used,
982 reason = "these unwrap calls should not fail"
983 )]
984 pub fn get_or_add_mut(&mut self, name: &str) -> &mut Spec {
985 match name {
986 EMPTY_COMMAND => {
987 if self.empty_line.is_none() {
988 self.empty_line = Some(Spec::default());
989 }
990 self.empty_line.as_mut().unwrap()
991 }
992 DEFAULT_COMMAND => {
993 if self.default.is_none() {
994 self.default = Some(Spec::default());
995 }
996 self.default.as_mut().unwrap()
997 }
998 INITIAL_WORD => {
999 if self.initial_word.is_none() {
1000 self.initial_word = Some(Spec::default());
1001 }
1002 self.initial_word.as_mut().unwrap()
1003 }
1004 _ => self.commands.entry(name.to_owned()).or_default(),
1005 }
1006 }
1007
1008 #[expect(clippy::string_slice)]
1016 pub async fn get_completions(
1017 &self,
1018 shell: &mut Shell<impl extensions::ShellExtensions>,
1019 input: &str,
1020 position: usize,
1021 ) -> Result<Completions, error::Error> {
1022 const MAX_RESTARTS: u32 = 10;
1023
1024 let tokens = Self::tokenize_input_for_completion(shell, input);
1026
1027 let cursor = position;
1028 let mut preceding_token = None;
1029 let mut completion_prefix = "";
1030 let mut insertion_index = cursor;
1031 let mut completion_token_index = tokens.len();
1032
1033 let mut adjusted_tokens: Vec<&CompletionToken<'_>> = tokens.iter().collect();
1036
1037 for (i, token) in tokens.iter().enumerate() {
1039 if cursor < token.start {
1043 completion_token_index = i;
1046 break;
1047 }
1048 else if cursor >= token.start && cursor <= token.end() {
1054 insertion_index = token.start;
1056
1057 let offset_into_token = cursor - insertion_index;
1059 let token_str = token.text;
1060 completion_prefix = &token_str[..offset_into_token];
1061
1062 completion_token_index = i;
1064
1065 break;
1066 }
1067
1068 preceding_token = Some(token);
1071 }
1072
1073 let empty_token = CompletionToken {
1076 text: "",
1077 start: input.len(),
1078 };
1079 if completion_token_index == tokens.len() {
1080 adjusted_tokens.push(&empty_token);
1081 }
1082
1083 let mut result = Answer::RestartCompletionProcess;
1085 let mut restart_count = 0;
1086 while matches!(result, Answer::RestartCompletionProcess) {
1087 if restart_count > MAX_RESTARTS {
1088 tracing::warn!("possible infinite loop detected in completion process");
1089 break;
1090 }
1091
1092 let completion_context = Context {
1093 token_to_complete: completion_prefix,
1094 preceding_token: preceding_token.map(|t| t.text),
1095 command_name: adjusted_tokens.first().map(|token| token.text),
1096 input_line: input,
1097 token_index: completion_token_index,
1098 tokens: adjusted_tokens.as_slice(),
1099 cursor_index: position,
1100 trigger: CompletionTrigger::InteractiveComplete,
1101 };
1102
1103 result = self
1104 .get_completions_for_token(shell, completion_context)
1105 .await;
1106
1107 restart_count += 1;
1108 }
1109
1110 match result {
1111 Answer::Candidates(candidates, options) => Ok(Completions {
1112 insertion_index,
1113 delete_count: completion_prefix.len(),
1114 candidates,
1115 options,
1116 }),
1117 Answer::RestartCompletionProcess => Ok(Completions {
1118 insertion_index,
1119 delete_count: 0,
1120 candidates: Vec::new(),
1121 options: ProcessingOptions::default(),
1122 }),
1123 }
1124 }
1125
1126 fn tokenize_input_for_completion<'a>(
1127 shell: &Shell<impl extensions::ShellExtensions>,
1128 input: &'a str,
1129 ) -> Vec<CompletionToken<'a>> {
1130 const FALLBACK: &str = " \t\n\"\'@><=;|&(:";
1131
1132 let delimiter_str = shell
1133 .env_str("COMP_WORDBREAKS")
1134 .unwrap_or_else(|| FALLBACK.into());
1135
1136 let delimiters: Vec<_> = delimiter_str.chars().collect();
1137
1138 simple_tokenize_by_delimiters(input, delimiters.as_slice())
1139 }
1140
1141 async fn get_completions_for_token(
1142 &self,
1143 shell: &mut Shell<impl extensions::ShellExtensions>,
1144 context: Context<'_>,
1145 ) -> Answer {
1146 let mut found_spec: Option<&Spec> = None;
1148
1149 if let Some(command_name) = context.command_name {
1150 if context.token_index == 0 {
1151 if let Some(spec) = &self.initial_word {
1152 found_spec = Some(spec);
1153 }
1154 } else {
1155 if let Some(spec) = shell.completion_config().commands.get(command_name) {
1156 found_spec = Some(spec);
1157 } else if let Some(file_name) = PathBuf::from(command_name).file_name() {
1158 if let Some(spec) = shell
1159 .completion_config()
1160 .commands
1161 .get(&file_name.to_string_lossy().to_string())
1162 {
1163 found_spec = Some(spec);
1164 }
1165 }
1166
1167 if found_spec.is_none() {
1168 if let Some(spec) = &self.default {
1169 found_spec = Some(spec);
1170 }
1171 }
1172 }
1173 } else {
1174 if let Some(spec) = &self.empty_line {
1175 found_spec = Some(spec);
1176 }
1177 }
1178
1179 if let Some(spec) = found_spec {
1181 spec.to_owned()
1182 .get_completions(shell, &context)
1183 .await
1184 .unwrap_or_else(|_err| Answer::Candidates(Vec::new(), ProcessingOptions::default()))
1185 } else {
1186 get_completions_using_basic_lookup(shell, &context).await
1188 }
1189 }
1190}
1191
1192async fn get_file_completions(
1193 shell: &Shell<impl extensions::ShellExtensions>,
1194 token_to_complete: &str,
1195 must_be_dir: bool,
1196) -> Vec<String> {
1197 let mut throwaway_shell = shell.clone();
1199 let params = throwaway_shell.default_exec_params();
1200 let options = expansion::ExpanderOptions {
1201 execute_command_substitutions: false,
1202 ..Default::default()
1203 };
1204 let expanded_token = expansion::basic_expand_word_with_options(
1205 &mut throwaway_shell,
1206 ¶ms,
1207 &unquote_str(token_to_complete),
1208 &options,
1209 )
1210 .await
1211 .unwrap_or_else(|_err| token_to_complete.to_owned());
1212
1213 let expanded_token = sys::fs::normalize_path_separators(&expanded_token).into_owned();
1217
1218 let glob = std::format!("{expanded_token}*");
1219
1220 let path_filter = |path: &Path| !must_be_dir || shell.absolute_path(path).is_dir();
1221
1222 let pattern = patterns::Pattern::from(glob)
1223 .set_extended_globbing(shell.options().extended_globbing)
1224 .set_case_insensitive(shell.options().case_insensitive_pathname_expansion);
1225
1226 let mut completions: Vec<String> = pattern
1227 .expand(
1228 shell.working_dir(),
1229 Some(&path_filter),
1230 &patterns::FilenameExpansionOptions::default(),
1231 )
1232 .unwrap_or_default()
1233 .into_paths()
1234 .into_iter()
1235 .map(|p| match sys::fs::normalize_path_separators(&p) {
1236 std::borrow::Cow::Borrowed(_) => p,
1237 std::borrow::Cow::Owned(normalized) => normalized,
1238 })
1239 .collect();
1240
1241 match expanded_token.as_str() {
1242 "." => {
1243 completions.push(".".into());
1244 completions.push("..".into());
1245 }
1246 ".." => {
1247 completions.push("..".into());
1248 }
1249 _ => {}
1250 }
1251
1252 completions.sort();
1253 completions.dedup();
1254 completions
1255}
1256
1257fn get_external_command_completions(
1258 shell: &Shell<impl extensions::ShellExtensions>,
1259 prefix: &str,
1260) -> Vec<String> {
1261 let mut candidates = Vec::new();
1262
1263 for path in shell.find_executables_in_path_with_prefix(
1265 prefix,
1266 shell.options().case_insensitive_pathname_expansion,
1267 ) {
1268 if let Some(file_name) = path.file_name() {
1269 candidates.push(file_name.to_string_lossy().to_string());
1270 }
1271 }
1272
1273 candidates.into_iter().collect()
1274}
1275
1276fn try_get_variable_completions(
1285 shell: &Shell<impl extensions::ShellExtensions>,
1286 token: &str,
1287) -> Option<Answer> {
1288 let (var_prefix, use_braces) = if let Some(prefix) = token.strip_prefix("${") {
1290 if prefix.contains('}') {
1292 return None;
1293 }
1294 (prefix, true)
1295 } else if let Some(prefix) = token.strip_prefix('$') {
1296 (prefix, false)
1297 } else {
1298 return None;
1299 };
1300
1301 if sys::fs::contains_path_separator(var_prefix) {
1303 return None;
1304 }
1305
1306 let mut candidates: Vec<String> = shell
1308 .env()
1309 .iter()
1310 .filter(|(key, _)| key.starts_with(var_prefix))
1311 .map(|(key, _)| {
1312 if use_braces {
1313 format!("${{{key}}}")
1314 } else {
1315 format!("${key}")
1316 }
1317 })
1318 .collect();
1319 candidates.sort();
1320
1321 let options = ProcessingOptions {
1323 treat_as_filenames: false,
1324 ..ProcessingOptions::default()
1325 };
1326
1327 Some(Answer::Candidates(candidates, options))
1328}
1329
1330fn add_command_completions(
1333 shell: &Shell<impl extensions::ShellExtensions>,
1334 prefix: &str,
1335 candidates: &mut Vec<String>,
1336) {
1337 let mut command_completions = get_external_command_completions(shell, prefix);
1339 candidates.append(&mut command_completions);
1340
1341 for (name, registration) in shell.builtins() {
1343 if !registration.disabled && name.starts_with(prefix) {
1344 candidates.push(name.to_owned());
1345 }
1346 }
1347
1348 for (name, _) in shell.funcs().iter() {
1350 if name.starts_with(prefix) {
1351 candidates.push(name.to_owned());
1352 }
1353 }
1354
1355 for name in shell.aliases().keys() {
1357 if name.starts_with(prefix) {
1358 candidates.push(name.to_owned());
1359 }
1360 }
1361
1362 for keyword in shell.get_keywords() {
1364 if keyword.starts_with(prefix) {
1365 candidates.push(keyword.to_string());
1366 }
1367 }
1368}
1369
1370async fn get_completions_using_basic_lookup(
1371 shell: &Shell<impl extensions::ShellExtensions>,
1372 context: &Context<'_>,
1373) -> Answer {
1374 let token = context.token_to_complete;
1375
1376 if let Some(answer) = try_get_variable_completions(shell, token) {
1378 return answer;
1379 }
1380
1381 let mut candidates = get_file_completions(shell, token, false).await;
1383
1384 let is_command_position =
1389 context.token_index == 0 && !token.is_empty() && !sys::fs::contains_path_separator(token);
1390
1391 if is_command_position {
1392 add_command_completions(shell, token, &mut candidates);
1393 candidates.sort();
1394 }
1395
1396 Answer::Candidates(candidates, ProcessingOptions::default())
1397}
1398
1399#[allow(clippy::string_slice, reason = "used indices come from char_indices")]
1403fn simple_tokenize_by_delimiters<'a>(
1404 input: &'a str,
1405 delimiters: &[char],
1406) -> Vec<CompletionToken<'a>> {
1407 let mut tokens = vec![];
1408 let mut word_start = None;
1409 let mut word_is_delimiters = false;
1410 let mut quote_char: Option<char> = None;
1411 let mut escaped = false;
1412
1413 for (i, c) in input.char_indices() {
1414 let mut is_active_delimiter = false;
1415 if escaped {
1416 escaped = false;
1417 } else if let Some(q) = quote_char {
1418 if c == '\\' && q == '"' {
1419 escaped = true;
1421 } else if c == q {
1422 quote_char = None;
1424 }
1425 } else {
1426 if c == '\\' {
1427 escaped = true;
1428 } else if word_start.is_none() && (c == '\'' || c == '\"') {
1429 quote_char = Some(c);
1431 } else {
1432 is_active_delimiter = delimiters.contains(&c);
1433 }
1434 }
1435
1436 if is_active_delimiter {
1437 if let Some(start) = word_start {
1440 if !word_is_delimiters || c.is_ascii_whitespace() {
1441 tokens.push(CompletionToken {
1442 text: &input[start..i],
1443 start,
1444 });
1445 word_start = None;
1446 word_is_delimiters = false;
1447 }
1448
1449 if !c.is_ascii_whitespace() {
1450 if word_start.is_none() {
1451 word_start = Some(i);
1452 word_is_delimiters = true;
1453 }
1454 }
1455 } else if !c.is_ascii_whitespace() {
1456 if word_start.is_none() {
1458 word_start = Some(i);
1459 word_is_delimiters = true;
1460 }
1461 }
1462 } else {
1463 if word_is_delimiters {
1465 if let Some(start) = word_start {
1466 tokens.push(CompletionToken {
1467 text: &input[start..i],
1468 start,
1469 });
1470 word_start = None;
1471 word_is_delimiters = false;
1472 }
1473 }
1474
1475 if word_start.is_none() {
1477 word_start = Some(i);
1478 }
1479 }
1480 }
1481
1482 if let Some(start) = word_start {
1484 tokens.push(CompletionToken {
1485 text: &input[start..],
1486 start,
1487 });
1488 }
1489
1490 tokens
1491}
1492
1493fn completion_filter_pattern_matches(
1494 pattern: &str,
1495 candidate: &str,
1496 token_being_completed: &str,
1497 shell: &Shell<impl extensions::ShellExtensions>,
1498) -> Result<bool, error::Error> {
1499 let pattern = replace_unescaped_ampersands(pattern, token_being_completed);
1500
1501 let pattern = patterns::Pattern::from(pattern.as_ref())
1506 .set_extended_globbing(shell.options().extended_globbing)
1507 .set_case_insensitive(shell.options().case_insensitive_pathname_expansion);
1508
1509 let matches = pattern.exactly_matches(candidate)?;
1510
1511 Ok(matches)
1512}
1513
1514fn replace_unescaped_ampersands<'a>(pattern: &'a str, replacement: &str) -> Cow<'a, str> {
1515 let mut in_escape = false;
1516 let mut insertion_points = vec![];
1517
1518 for (i, c) in pattern.char_indices() {
1519 if !in_escape && c == '&' {
1520 insertion_points.push(i);
1521 }
1522 in_escape = !in_escape && c == '\\';
1523 }
1524
1525 if insertion_points.is_empty() {
1526 return pattern.into();
1527 }
1528
1529 let mut result = pattern.to_owned();
1530 for i in insertion_points.iter().rev() {
1531 result.replace_range(*i..=*i, replacement);
1532 }
1533
1534 result.into()
1535}
1536
1537#[cfg(test)]
1538mod tests {
1539 use super::*;
1540 use pretty_assertions::assert_matches;
1541
1542 #[test]
1543 #[allow(clippy::too_many_lines)]
1544 fn completion_tokenization() {
1545 assert_matches!(
1546 simple_tokenize_by_delimiters("one two", &[' ']).as_slice(),
1547 [
1548 CompletionToken {
1549 text: "one",
1550 start: 0,
1551 },
1552 CompletionToken {
1553 text: "two",
1554 start: 4,
1555 }
1556 ]
1557 );
1558
1559 assert_matches!(
1560 simple_tokenize_by_delimiters("one \t two", &[' ', '\t']).as_slice(),
1561 [
1562 CompletionToken {
1563 text: "one",
1564 start: 0,
1565 },
1566 CompletionToken {
1567 text: "two",
1568 start: 6,
1569 }
1570 ]
1571 );
1572
1573 assert_matches!(simple_tokenize_by_delimiters(" ", &[' ']).as_slice(), []);
1574
1575 assert_matches!(
1576 simple_tokenize_by_delimiters(":", &[':']).as_slice(),
1577 [CompletionToken {
1578 text: ":",
1579 start: 0,
1580 }]
1581 );
1582
1583 assert_matches!(
1584 simple_tokenize_by_delimiters("a:::b", &[':', ' ']).as_slice(),
1585 [
1586 CompletionToken {
1587 text: "a",
1588 start: 0,
1589 },
1590 CompletionToken {
1591 text: ":::",
1592 start: 1,
1593 },
1594 CompletionToken {
1595 text: "b",
1596 start: 4,
1597 }
1598 ]
1599 );
1600
1601 assert_matches!(
1602 simple_tokenize_by_delimiters("a: : :b", &[':', ' ']).as_slice(),
1603 [
1604 CompletionToken {
1605 text: "a",
1606 start: 0,
1607 },
1608 CompletionToken {
1609 text: ":",
1610 start: 1,
1611 },
1612 CompletionToken {
1613 text: ":",
1614 start: 3,
1615 },
1616 CompletionToken {
1617 text: ":",
1618 start: 5,
1619 },
1620 CompletionToken {
1621 text: "b",
1622 start: 6,
1623 }
1624 ]
1625 );
1626
1627 assert_matches!(
1628 simple_tokenize_by_delimiters("one two:three", &[':', ' ']).as_slice(),
1629 [
1630 CompletionToken {
1631 text: "one",
1632 start: 0,
1633 },
1634 CompletionToken {
1635 text: "two",
1636 start: 4,
1637 },
1638 CompletionToken {
1639 text: ":",
1640 start: 7,
1641 },
1642 CompletionToken {
1643 text: "three",
1644 start: 8,
1645 }
1646 ]
1647 );
1648
1649 assert_matches!(
1650 simple_tokenize_by_delimiters("one'two", &['\'']).as_slice(),
1651 [
1652 CompletionToken {
1653 text: "one",
1654 start: 0,
1655 },
1656 CompletionToken {
1657 text: "'",
1658 start: 3,
1659 },
1660 CompletionToken {
1661 text: "two",
1662 start: 4,
1663 },
1664 ]
1665 );
1666
1667 assert_matches!(
1668 simple_tokenize_by_delimiters("one 'two:three'", &[':', ' ']).as_slice(),
1669 [
1670 CompletionToken {
1671 text: "one",
1672 start: 0,
1673 },
1674 CompletionToken {
1675 text: "'two:three'",
1676 start: 4,
1677 },
1678 ]
1679 );
1680
1681 assert_matches!(
1682 simple_tokenize_by_delimiters("one \\'two \"two four\"", &[':', ' ']).as_slice(),
1683 [
1684 CompletionToken {
1685 text: "one",
1686 start: 0,
1687 },
1688 CompletionToken {
1689 text: "\\'two",
1690 start: 4,
1691 },
1692 CompletionToken {
1693 text: "\"two four\"",
1694 start: 10,
1695 },
1696 ]
1697 );
1698 }
1699}