1use std::borrow::Cow;
26use std::collections::{BTreeSet, HashMap};
27use std::path::Path;
28
29use itertools::Itertools;
30use shrub_parser::unquote_str;
31use strum::IntoEnumIterator;
32
33use crate::sys::{self, users};
34use crate::variables::{self, ShellValue, ShellVariable};
35use crate::{
36 Shell, commands, env, error, escape, expansion, extensions, interfaces, jobs, namedoptions,
37 patterns, trace_categories, traps,
38};
39
40mod quoting;
41mod words;
42
43use words::{LineWords, find_completion_word, find_line_words};
44
45fn split_completion_word_list(
48 word_list: &str,
49 ifs: &str,
50 parser_options: &shrub_parser::ParserOptions,
51) -> Result<Vec<String>, error::Error> {
52 let mut word_list = word_list.to_owned();
54 quoting::close(&mut word_list);
55 let word_list = word_list.as_str();
56
57 let pieces = shrub_parser::word::parse(word_list, parser_options)?;
58 let mut words = vec![];
59 let mut current_word = String::new();
60
61 for piece in pieces {
62 let source = word_list
63 .get(piece.start_index..piece.end_index)
64 .ok_or_else(|| {
65 error::ErrorKind::InternalError(String::from(
66 "word parser returned an invalid source span",
67 ))
68 })?;
69
70 if matches!(piece.piece, shrub_parser::word::WordPiece::Text(_)) {
71 for c in source.chars() {
72 if ifs.contains(c) {
73 if !current_word.is_empty() {
74 words.push(std::mem::take(&mut current_word));
75 }
76 } else {
77 current_word.push(c);
78 }
79 }
80 } else {
81 current_word.push_str(source);
82 }
83 }
84
85 if !current_word.is_empty() {
86 words.push(current_word);
87 }
88
89 Ok(words)
90}
91
92#[derive(
95 Clone,
96 Copy,
97 Debug,
98 Eq,
99 PartialEq,
100 strum_macros::EnumIter,
101 strum_macros::EnumMessage,
102 strum_macros::EnumString,
103 strum_macros::IntoStaticStr,
104)]
105#[strum(serialize_all = "lowercase")]
106#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
107pub enum CompleteAction {
108 Alias,
110 ArrayVar,
112 Binding,
114 Builtin,
116 Command,
118 Directory,
120 Disabled,
122 Enabled,
124 Export,
126 File,
128 Function,
130 Group,
132 HelpTopic,
134 HostName,
136 Job,
138 Keyword,
140 Running,
142 Service,
144 SetOpt,
146 ShOpt,
148 Signal,
150 Stopped,
152 User,
154 Variable,
156}
157
158#[derive(
162 Clone,
163 Copy,
164 Debug,
165 Eq,
166 Hash,
167 Ord,
168 PartialEq,
169 PartialOrd,
170 strum_macros::EnumIter,
171 strum_macros::EnumMessage,
172 strum_macros::EnumString,
173 strum_macros::IntoStaticStr,
174)]
175#[strum(serialize_all = "lowercase")]
176#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
177pub enum CompleteOption {
178 BashDefault,
180 Default,
182 DirNames,
184 FileNames,
186 NoQuote,
188 NoSort,
190 NoSpace,
193 PlusDirs,
195}
196
197#[derive(Clone, Default)]
199#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
200pub struct Config {
201 commands: HashMap<String, Spec>,
203 specials: HashMap<SpecialSpec, Spec>,
205
206 pub fallback_options: FallbackOptions,
210}
211
212#[derive(Clone, Debug)]
214#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
215pub struct FallbackOptions {
216 pub mark_directories: bool,
218 pub mark_symlinked_directories: bool,
220}
221
222impl Default for FallbackOptions {
223 fn default() -> Self {
224 Self {
225 mark_directories: true,
226 mark_symlinked_directories: false,
227 }
228 }
229}
230
231#[derive(Clone, Debug, Default, Eq, PartialEq)]
233#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
234pub struct GenerationOptions {
235 enabled: BTreeSet<CompleteOption>,
236}
237
238impl FromIterator<CompleteOption> for GenerationOptions {
239 fn from_iter<I: IntoIterator<Item = CompleteOption>>(options: I) -> Self {
241 Self {
242 enabled: options.into_iter().collect(),
243 }
244 }
245}
246
247impl GenerationOptions {
248 pub fn get(&self, option: CompleteOption) -> bool {
250 self.enabled.contains(&option)
251 }
252
253 pub fn set(&mut self, option: CompleteOption, enabled: bool) {
255 if enabled {
256 self.enabled.insert(option);
257 } else {
258 self.enabled.remove(&option);
259 }
260 }
261}
262
263#[derive(Clone, Debug, Default)]
266#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
267pub struct Spec {
268 pub options: GenerationOptions,
272
273 pub actions: Vec<CompleteAction>,
277 pub glob_pattern: Option<String>,
279 pub word_list: Option<String>,
281 pub function_name: Option<String>,
284 pub command: Option<String>,
286
287 pub filter_pattern: Option<String>,
293
294 pub prefix: Option<String>,
299 pub suffix: Option<String>,
302}
303
304#[derive(Clone, Default)]
306#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
307pub(crate) struct State {
308 pub(crate) config: Config,
310 pub(crate) in_progress: Option<InProgressCompletion>,
312}
313
314#[derive(Clone, Debug)]
319#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
320pub(crate) struct InProgressCompletion {
321 pub(crate) options: GenerationOptions,
323}
324
325struct InProgressScope<'a, SE: extensions::ShellExtensions> {
329 shell: &'a mut Shell<SE>,
330 prev: Option<InProgressCompletion>,
331}
332
333impl<'a, SE: extensions::ShellExtensions> InProgressScope<'a, SE> {
334 const fn start(shell: &'a mut Shell<SE>, in_progress: InProgressCompletion) -> Self {
335 let prev = shell.in_progress_completion_mut().replace(in_progress);
336 Self { shell, prev }
337 }
338
339 fn take_options(&mut self) -> Option<GenerationOptions> {
342 self.shell
343 .in_progress_completion_mut()
344 .take()
345 .map(|in_progress| in_progress.options)
346 }
347}
348
349impl<SE: extensions::ShellExtensions> Drop for InProgressScope<'_, SE> {
350 fn drop(&mut self) {
351 *self.shell.in_progress_completion_mut() = self.prev.take();
352 }
353}
354
355fn enter_comp_vars_scope<SE: extensions::ShellExtensions>(
359 shell: &mut Shell<SE>,
360 vars: impl IntoIterator<Item = (&'static str, ShellValue)>,
361 export: bool,
362) -> Result<impl std::ops::DerefMut<Target = Shell<SE>> + '_, error::Error> {
363 let mut scoped = shell.enter_scope(env::EnvironmentScope::Command);
364 for (name, value) in vars {
365 let mut var = ShellVariable::new(value);
366 if export {
367 var.export();
368 }
369 scoped
370 .env_mut()
371 .add(name, var, env::EnvironmentScope::Command)?;
372 }
373 Ok(scoped)
374}
375
376#[derive(Clone, Copy, Debug)]
378struct Context<'a> {
379 word: &'a str,
383 line: LineContext<'a>,
387}
388
389#[derive(Clone, Copy, Debug)]
392struct LineContext<'a> {
393 input: &'a str,
395 cursor: usize,
397 words: &'a LineWords<'a>,
399 command_name: Option<&'a str>,
402 comp_type: u8,
405 comp_key: u8,
407}
408
409impl<'a> LineContext<'a> {
410 fn new(input: &'a str, cursor: usize, words: &'a LineWords<'a>) -> Self {
413 Self {
414 input,
415 cursor,
416 words,
417 command_name: words.command_name(),
418 comp_type: b'\t',
419 comp_key: b'\t',
420 }
421 }
422
423 fn args(&self, word: &'a str) -> [&'a str; 3] {
427 [
428 self.command_name.unwrap_or(""),
429 word,
430 self.words.preceding_word().unwrap_or(""),
431 ]
432 }
433
434 fn comp_vars(&self) -> Vec<(&'static str, ShellValue)> {
438 vec![
439 ("COMP_LINE", self.input.into()),
440 ("COMP_POINT", self.cursor.to_string().into()),
441 ("COMP_KEY", self.comp_key.to_string().into()),
442 ("COMP_TYPE", self.comp_type.to_string().into()),
443 ]
444 }
445
446 fn comp_words_vars(&self) -> [(&'static str, ShellValue); 2] {
449 let cword = self
450 .words
451 .cword
452 .map_or_else(|| "-1".to_owned(), |i| i.to_string());
453 [
454 ("COMP_WORDS", self.words.comp_words.clone().into()),
455 ("COMP_CWORD", cword.into()),
456 ]
457 }
458}
459
460enum Generated<T> {
463 Candidates(T),
465 Restart,
467}
468
469struct SpecCandidates {
471 candidates: Vec<String>,
473 file_names: bool,
477}
478
479impl Spec {
480 pub async fn generate(
504 &self,
505 shell: &mut Shell<impl extensions::ShellExtensions>,
506 word: &str,
507 ) -> Result<Vec<String>, crate::error::Error> {
508 let word = unquote_str(word);
509 let context = Context {
510 word: &word,
511 line: LineContext {
512 input: "",
513 cursor: 0,
514 words: &words::NO_WORDS,
515 command_name: Some("compgen"),
516 comp_type: 0,
517 comp_key: 0,
518 },
519 };
520
521 let Generated::Candidates(SpecCandidates { candidates, .. }) =
522 self.generate_candidates(shell, &context).await?
523 else {
524 return Ok(Vec::new());
525 };
526
527 let (candidates, _options) = self
528 .apply_options(shell, &context, candidates, self.options.clone())
529 .await;
530 Ok(candidates)
531 }
532
533 async fn complete(
539 &self,
540 shell: &mut Shell<impl extensions::ShellExtensions>,
541 context: &Context<'_>,
542 ) -> Result<Generated<(Vec<String>, ProcessingOptions)>, crate::error::Error> {
543 let mut scope = InProgressScope::start(
544 shell,
545 InProgressCompletion {
546 options: self.options.clone(),
547 },
548 );
549
550 let Generated::Candidates(SpecCandidates {
551 candidates,
552 file_names,
553 }) = self.generate_candidates(scope.shell, context).await?
554 else {
555 return Ok(Generated::Restart);
556 };
557
558 let mut options = scope.take_options().unwrap_or_else(|| self.options.clone());
561 if file_names {
562 options.set(CompleteOption::FileNames, true);
563 }
564 let sort = !options.get(CompleteOption::NoSort);
565 let (mut candidates, processing_options) = self
566 .apply_options(scope.shell, context, candidates, options)
567 .await;
568 if sort {
569 candidates.sort();
570 }
571 Ok(Generated::Candidates((candidates, processing_options)))
572 }
573
574 async fn generate_candidates(
576 &self,
577 shell: &mut Shell<impl extensions::ShellExtensions>,
578 context: &Context<'_>,
579 ) -> Result<Generated<SpecCandidates>, crate::error::Error> {
580 let SpecCandidates {
582 mut candidates,
583 file_names,
584 } = self.generate_action_completions(shell, context).await?;
585 if let Some(word_list) = &self.word_list {
586 let params = shell.default_exec_params();
587 let unexpanded_words =
588 split_completion_word_list(word_list, &shell.ifs(), &shell.parser_options())?;
589 let options = crate::expansion::ExpanderOptions {
590 pathname_expand: false,
591 ..Default::default()
592 };
593 let mut words = vec![];
594 for word in unexpanded_words {
595 words.extend(
596 crate::expansion::full_expand_and_split_word_with_options(
597 shell, ¶ms, word, &options,
598 )
599 .await?,
600 );
601 }
602 candidates.extend(
603 words
604 .into_iter()
605 .filter(|word| word.starts_with(context.word)),
606 );
607 }
608
609 if let Some(glob_pattern) = &self.glob_pattern {
610 let pattern = shell_pattern(shell, glob_pattern.as_str());
611
612 let expansions = pattern
613 .expand(
614 shell.working_dir(),
615 Some(&patterns::Pattern::accept_all_expand_filter),
616 &patterns::FilenameExpansionOptions::default(),
617 )?
618 .into_paths();
619
620 candidates.extend(expansions);
621 }
622 if let Some(function_name) = &self.function_name {
623 match call_completion_function(shell, function_name, context).await? {
624 Generated::Candidates(new_candidates) => candidates.extend(new_candidates),
625 Generated::Restart => return Ok(Generated::Restart),
626 }
627 }
628 if let Some(command) = &self.command {
629 candidates.extend(call_completion_command(shell, command, context).await?);
630 }
631
632 if let Some(filter_pattern) = &self.filter_pattern
635 && !filter_pattern.is_empty()
636 {
637 let mut updated = Vec::new();
638
639 let (pattern, keep_matches) = match filter_pattern.strip_prefix('!') {
643 Some(rest) if !(shell.options().extended_globbing && rest.starts_with('(')) => {
644 (rest, true)
645 }
646 _ => (filter_pattern.as_str(), false),
647 };
648
649 for candidate in candidates {
650 let matches = completion_filter_pattern_matches(
651 pattern,
652 candidate.as_str(),
653 context.word,
654 shell,
655 )?;
656
657 if matches == keep_matches {
658 updated.push(candidate);
659 }
660 }
661
662 candidates = updated;
663 }
664
665 if self.prefix.is_some() || self.suffix.is_some() {
667 let empty = String::new();
668 let prefix = self.prefix.as_ref().unwrap_or(&empty);
669 let suffix = self.suffix.as_ref().unwrap_or(&empty);
670
671 let mut updated = Vec::with_capacity(candidates.len() * (prefix.len() + suffix.len()));
672 for candidate in candidates {
673 updated.push(std::format!("{prefix}{candidate}{suffix}"));
674 }
675
676 candidates = updated;
677 }
678
679 Ok(Generated::Candidates(SpecCandidates {
680 candidates,
681 file_names,
682 }))
683 }
684
685 async fn apply_options(
688 &self,
689 shell: &Shell<impl extensions::ShellExtensions>,
690 context: &Context<'_>,
691 mut candidates: Vec<String>,
692 options: GenerationOptions,
693 ) -> (Vec<String>, ProcessingOptions) {
694 let mut processing_options = ProcessingOptions {
695 treat_as_filenames: options.get(CompleteOption::FileNames),
696 no_autoquote_filenames: options.get(CompleteOption::NoQuote),
697 no_trailing_space_at_end_of_line: options.get(CompleteOption::NoSpace),
698 };
699
700 if options.get(CompleteOption::PlusDirs)
703 || (options.get(CompleteOption::DirNames) && candidates.is_empty())
704 {
705 let mut dir_candidates =
706 get_file_completions(shell, context.word, true).await;
707
708 if candidates.is_empty() && shell.completion_config().fallback_options.mark_directories
710 {
711 processing_options.treat_as_filenames = true;
712 }
713
714 candidates.append(&mut dir_candidates);
715 }
716
717 if candidates.is_empty() && options.get(CompleteOption::BashDefault) {
720 tracing::debug!(target: trace_categories::COMPLETION, "unimplemented: complete -o bashdefault");
724 }
725
726 if candidates.is_empty() && options.get(CompleteOption::Default) {
729 let must_be_dir = options.get(CompleteOption::DirNames);
732
733 let mut default_candidates =
734 get_file_completions(shell, context.word, must_be_dir).await;
735 candidates.append(&mut default_candidates);
736
737 if shell.completion_config().fallback_options.mark_directories {
738 processing_options.treat_as_filenames = true;
739 }
740 }
741
742 (candidates, processing_options)
743 }
744
745 #[expect(clippy::too_many_lines, reason = "a flat match over the actions")]
747 async fn generate_action_completions(
748 &self,
749 shell: &Shell<impl extensions::ShellExtensions>,
750 context: &Context<'_>,
751 ) -> Result<SpecCandidates, error::Error> {
752 let prefix = context.word;
753 let mut candidates = Vec::new();
754 let mut file_names = false;
755
756 for action in &self.actions {
757 let c = &mut candidates;
758 match action {
759 CompleteAction::Alias => {
762 extend_matching(c, shell.aliases().keys().sorted(), prefix);
763 }
764 CompleteAction::ArrayVar => extend_matching(
765 c,
766 shell
767 .env()
768 .iter()
769 .filter(|(_, var)| var.value().is_array())
770 .map(|(name, _)| name),
771 prefix,
772 ),
773 CompleteAction::Binding => extend_matching(
774 c,
775 interfaces::InputFunction::iter().map(<&'static str>::from),
776 prefix,
777 ),
778 CompleteAction::Builtin | CompleteAction::HelpTopic => {
780 extend_matching(c, shell.builtins().keys(), prefix);
781 }
782 CompleteAction::Command => c.extend(command_completions(shell, prefix)),
783 CompleteAction::Directory => {
784 let dirs = get_file_completions(shell, context.word, true).await;
785 file_names |= !dirs.is_empty();
786 c.extend(dirs);
787 }
788 CompleteAction::Disabled => extend_matching(
789 c,
790 shell
791 .builtins()
792 .iter()
793 .filter(|(_, registration)| registration.disabled)
794 .map(|(name, _)| name),
795 prefix,
796 ),
797 CompleteAction::Enabled => extend_matching(
798 c,
799 shell
800 .builtins()
801 .iter()
802 .filter(|(_, registration)| !registration.disabled)
803 .map(|(name, _)| name),
804 prefix,
805 ),
806 CompleteAction::Export => extend_matching(
807 c,
808 shell
809 .env()
810 .iter()
811 .filter(|(_, var)| var.is_exported())
812 .map(|(name, _)| name),
813 prefix,
814 ),
815 CompleteAction::File => {
816 file_names = true;
817 c.extend(get_file_completions(shell, context.word, false).await);
818 }
819 CompleteAction::Function => extend_matching(
820 c,
821 shell.funcs().iter().map(|(name, _)| name).sorted(),
822 prefix,
823 ),
824 CompleteAction::Group => extend_matching(c, users::get_all_groups()?, prefix),
825 CompleteAction::HostName => extend_matching(
827 c,
828 sys::network::get_hostname()
829 .ok()
830 .map(|name| name.to_string_lossy().into_owned()),
831 prefix,
832 ),
833 CompleteAction::Job => extend_matching(
834 c,
835 shell.jobs().jobs.iter().map(|job| job.command_name()),
836 prefix,
837 ),
838 CompleteAction::Keyword => extend_matching(c, shell.get_keywords(), prefix),
839 CompleteAction::Running => extend_matching(
840 c,
841 shell
842 .jobs()
843 .jobs
844 .iter()
845 .filter(|job| matches!(job.state, jobs::JobState::Running))
846 .map(|job| job.command_name()),
847 prefix,
848 ),
849 CompleteAction::Service => {
850 tracing::debug!(target: trace_categories::COMPLETION, "unimplemented: complete -A service");
851 }
852 CompleteAction::SetOpt => extend_matching(
853 c,
854 namedoptions::options(namedoptions::ShellOptionKind::SetO)
855 .iter()
856 .map(|option| option.name),
857 prefix,
858 ),
859 CompleteAction::ShOpt => extend_matching(
860 c,
861 namedoptions::options(namedoptions::ShellOptionKind::Shopt)
862 .iter()
863 .map(|option| option.name),
864 prefix,
865 ),
866 CompleteAction::Signal => extend_matching(
867 c,
868 traps::TrapSignal::iterator().map(traps::TrapSignal::as_str),
869 prefix,
870 ),
871 CompleteAction::Stopped => extend_matching(
872 c,
873 shell
874 .jobs()
875 .jobs
876 .iter()
877 .filter(|job| matches!(job.state, jobs::JobState::Stopped))
878 .map(|job| job.command_name()),
879 prefix,
880 ),
881 CompleteAction::User => extend_matching(c, users::get_all_users()?, prefix),
882 CompleteAction::Variable => {
883 extend_matching(c, shell.env().iter().map(|(name, _)| name), prefix);
884 }
885 }
886 }
887
888 Ok(SpecCandidates {
889 candidates,
890 file_names,
891 })
892 }
893}
894
895async fn call_completion_command(
898 shell: &mut Shell<impl extensions::ShellExtensions>,
899 command: &str,
900 context: &Context<'_>,
901) -> Result<Vec<String>, error::Error> {
902 let line = context.line;
903
904 let mut command_line = command.to_owned();
906 for arg in line.args(context.word) {
907 command_line.push(' ');
908 command_line.push_str(&escape::quote_if_needed(
909 arg,
910 escape::QuoteMode::SingleQuote,
911 ));
912 }
913
914 let status = shell.save_command_status();
918 let mut scoped = enter_comp_vars_scope(shell, line.comp_vars(), true)?;
919 let mut blocked = scoped.block_trap_delivery();
920 let params = blocked.default_exec_params();
921 let output =
922 commands::invoke_command_in_subshell_and_get_output(&mut blocked, ¶ms, command_line)
923 .await;
924 drop(blocked);
925 drop(scoped);
926 shell.restore_command_status(status);
927
928 Ok(output?.lines().map(str::to_owned).collect())
929}
930
931async fn call_completion_function(
935 shell: &mut Shell<impl extensions::ShellExtensions>,
936 function_name: &str,
937 context: &Context<'_>,
938) -> Result<Generated<Vec<String>>, error::Error> {
939 let line = context.line;
940 let mut vars = line.comp_vars();
941 vars.extend(line.comp_words_vars());
942
943 tracing::debug!(target: trace_categories::COMPLETION, "[calling completion func '{function_name}']: {}",
944 vars.iter().map(|(k, v)| std::format!("{k}={v:?}")).collect::<Vec<String>>().join(" "));
945
946 let names: Vec<_> = vars.iter().map(|(name, _)| *name).collect();
950 let mut scoped = enter_comp_vars_scope(shell, vars, false)?;
951 let mut blocked = scoped.block_trap_delivery();
952 let params = blocked.default_exec_params();
953 let invoke_result = blocked
954 .invoke_function(function_name, line.args(context.word).iter(), params)
955 .await
956 .map(|result| u8::from(result.exit_code));
957
958 drop(blocked);
960 drop(scoped);
961
962 for name in names {
966 let _ = shell.env_mut().unset(name);
967 }
968
969 tracing::debug!(target: trace_categories::COMPLETION, "[completion function '{function_name}' returned: {invoke_result:?}]");
970
971 let result = invoke_result.unwrap_or_else(|e| {
972 tracing::warn!(target: trace_categories::COMPLETION, "error while running completion function '{function_name}': {e}");
973 1 });
975
976 let reply = shell.env_mut().unset("COMPREPLY")?;
979
980 if result == 124 {
983 return Ok(Generated::Restart);
984 }
985
986 tracing::debug!(target: trace_categories::COMPLETION, "[completion function yielded: {reply:?}]");
987
988 let candidates = match reply.as_ref().map(|reply| reply.value()) {
989 Some(variables::ShellValue::IndexedArray(values)) => {
990 values.values().map(|v| v.to_owned()).collect()
991 }
992 Some(variables::ShellValue::String(s)) => vec![s.to_owned()],
993 _ => Vec::new(),
994 };
995 Ok(Generated::Candidates(candidates))
996}
997
998#[derive(Debug, Default)]
1000pub struct Completions {
1001 pub insertion_index: usize,
1005 pub delete_count: usize,
1009 pub candidates: Vec<String>,
1011 pub options: ProcessingOptions,
1013}
1014
1015#[derive(Debug)]
1018pub struct ProcessingOptions {
1019 pub treat_as_filenames: bool,
1021 pub no_autoquote_filenames: bool,
1023 pub no_trailing_space_at_end_of_line: bool,
1026}
1027
1028impl Default for ProcessingOptions {
1029 fn default() -> Self {
1030 Self {
1031 treat_as_filenames: true,
1032 no_autoquote_filenames: false,
1033 no_trailing_space_at_end_of_line: false,
1034 }
1035 }
1036}
1037
1038#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, strum_macros::EnumIter)]
1040#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1041pub enum SpecialSpec {
1042 Default,
1044 EmptyLine,
1046 InitialWord,
1048}
1049
1050impl SpecialSpec {
1051 pub const fn command_name(self) -> &'static str {
1056 match self {
1057 Self::Default => "_DefaultCmD_",
1058 Self::EmptyLine => "_EmptycmD_",
1059 Self::InitialWord => "_InitialWorD_",
1060 }
1061 }
1062
1063 pub fn from_command_name(name: &str) -> Option<Self> {
1065 Self::iter().find(|special| special.command_name() == name)
1066 }
1067}
1068
1069#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
1071pub enum SpecName<'a> {
1072 Command(&'a str),
1074 Special(SpecialSpec),
1076}
1077
1078impl<'a> SpecName<'a> {
1079 pub fn parse(name: &'a str) -> Self {
1082 SpecialSpec::from_command_name(name).map_or(Self::Command(name), Self::Special)
1083 }
1084
1085 pub const fn as_str(self) -> &'a str {
1088 match self {
1089 Self::Command(command) => command,
1090 Self::Special(special) => special.command_name(),
1091 }
1092 }
1093}
1094
1095impl Config {
1096 pub fn clear(&mut self) {
1098 self.commands.clear();
1099 self.specials.clear();
1100 }
1101
1102 pub fn remove(&mut self, name: SpecName<'_>) -> bool {
1109 match name {
1110 SpecName::Command(command) => self.commands.remove(command).is_some(),
1111 SpecName::Special(special) => self.specials.remove(&special).is_some(),
1112 }
1113 }
1114
1115 pub fn iter(&self) -> impl Iterator<Item = (SpecName<'_>, &Spec)> {
1118 let commands = self
1119 .commands
1120 .iter()
1121 .map(|(command, spec)| (SpecName::Command(command), spec));
1122 let specials = self
1123 .specials
1124 .iter()
1125 .map(|(special, spec)| (SpecName::Special(*special), spec));
1126 commands.chain(specials)
1127 }
1128
1129 pub fn get(&self, name: SpecName<'_>) -> Option<&Spec> {
1135 match name {
1136 SpecName::Command(command) => self.commands.get(command),
1137 SpecName::Special(special) => self.specials.get(&special),
1138 }
1139 }
1140
1141 pub fn get_mut(&mut self, name: SpecName<'_>) -> Option<&mut Spec> {
1147 match name {
1148 SpecName::Command(command) => self.commands.get_mut(command),
1149 SpecName::Special(special) => self.specials.get_mut(&special),
1150 }
1151 }
1152
1153 pub fn set(&mut self, name: SpecName<'_>, spec: Spec) {
1161 match name {
1162 SpecName::Command(command) => {
1163 self.commands.insert(command.to_owned(), spec);
1164 }
1165 SpecName::Special(special) => {
1166 self.specials.insert(special, spec);
1167 }
1168 }
1169 }
1170
1171 fn find_spec<'l>(&self, line: &LineContext<'l>) -> Option<(&Spec, Option<&'l str>)> {
1180 let special = |special: SpecialSpec| {
1181 let spec = self.specials.get(&special)?;
1182 Some((spec, Some(special.command_name())))
1183 };
1184
1185 if line.cursor == 0 && line.input.chars().next().is_none_or(char::is_whitespace) {
1190 return special(SpecialSpec::EmptyLine);
1191 }
1192
1193 if matches!(line.words.cword, None | Some(0)) {
1194 return special(SpecialSpec::InitialWord);
1195 }
1196
1197 let command_name = line.command_name?;
1198 let spec = self
1199 .commands
1200 .get(command_name)
1201 .or_else(|| {
1202 let file_name = Path::new(command_name).file_name()?;
1203 self.commands.get(file_name.to_string_lossy().as_ref())
1204 })
1205 .or_else(|| self.specials.get(&SpecialSpec::Default))?;
1206 Some((spec, Some(command_name)))
1207 }
1208}
1209
1210pub(crate) async fn complete(
1212 shell: &mut Shell<impl extensions::ShellExtensions>,
1213 input: &str,
1214 cursor: usize,
1215) -> Result<Completions, error::Error> {
1216 const MAX_RESTARTS: u32 = 10;
1218
1219 if !input.is_char_boundary(cursor) {
1220 return Err(error::ErrorKind::InvalidCompletionPosition(cursor).into());
1221 }
1222
1223 let word_breaks = word_break_chars(shell);
1224 let word = find_completion_word(input, &word_breaks, cursor);
1225 let line_words = find_line_words(input, &word_breaks, cursor);
1226 let context = Context {
1227 word: &word.text,
1228 line: LineContext::new(input, cursor, &line_words),
1229 };
1230
1231 let mut completed = None;
1234 for _ in 0..=MAX_RESTARTS {
1235 if let Generated::Candidates(candidates_and_options) = complete_word(shell, context).await {
1236 completed = Some(candidates_and_options);
1237 break;
1238 }
1239 }
1240 let (candidates, options) = completed.unwrap_or_else(|| {
1241 tracing::warn!(target: trace_categories::COMPLETION, "completion kept restarting; giving up");
1242 (Vec::new(), ProcessingOptions::default())
1243 });
1244
1245 Ok(Completions {
1246 insertion_index: word.range.start,
1247 delete_count: word.range.len(),
1248 candidates,
1249 options,
1250 })
1251}
1252
1253fn word_break_chars(shell: &Shell<impl extensions::ShellExtensions>) -> Vec<char> {
1254 const FALLBACK: &str = " \t\n\"\'@><=;|&(:";
1255
1256 shell
1257 .env_str("COMP_WORDBREAKS")
1258 .unwrap_or_else(|| FALLBACK.into())
1259 .chars()
1260 .collect()
1261}
1262
1263async fn complete_word(
1267 shell: &mut Shell<impl extensions::ShellExtensions>,
1268 mut context: Context<'_>,
1269) -> Generated<(Vec<String>, ProcessingOptions)> {
1270 let Some((spec, command_name)) = shell.completion_config().find_spec(&context.line) else {
1273 return Generated::Candidates(get_completions_using_basic_lookup(shell, &context).await);
1274 };
1275 let spec = spec.clone();
1276 context.line.command_name = command_name;
1277
1278 spec.complete(shell, &context).await.unwrap_or_else(|err| {
1279 tracing::debug!(target: trace_categories::COMPLETION, "completion spec failed: {err}");
1280 Generated::Candidates((Vec::new(), ProcessingOptions::default()))
1281 })
1282}
1283
1284async fn get_file_completions(
1285 shell: &Shell<impl extensions::ShellExtensions>,
1286 token_to_complete: &str,
1287 must_be_dir: bool,
1288) -> Vec<String> {
1289 let mut throwaway_shell = shell.clone();
1292 let params = throwaway_shell.default_exec_params();
1293 let options = expansion::ExpanderOptions {
1294 execute_command_substitutions: false,
1295 ..Default::default()
1296 };
1297 let expanded_token = expansion::basic_expand_word_with_options(
1298 &mut throwaway_shell,
1299 ¶ms,
1300 &unquote_str(token_to_complete),
1301 &options,
1302 )
1303 .await
1304 .unwrap_or_else(|_err| token_to_complete.to_owned());
1305
1306 let expanded_token = sys::fs::normalize_path_separators(&expanded_token).into_owned();
1310
1311 let glob = std::format!("{expanded_token}*");
1312
1313 let path_filter = |path: &Path| !must_be_dir || shell.absolute_path(path).is_dir();
1314
1315 let pattern = shell_pattern(shell, glob);
1316
1317 let mut completions: Vec<String> = pattern
1318 .expand(
1319 shell.working_dir(),
1320 Some(&path_filter),
1321 &patterns::FilenameExpansionOptions::default(),
1322 )
1323 .unwrap_or_default()
1324 .into_paths()
1325 .into_iter()
1326 .map(|p| match sys::fs::normalize_path_separators(&p) {
1327 std::borrow::Cow::Borrowed(_) => p,
1328 std::borrow::Cow::Owned(normalized) => normalized,
1329 })
1330 .collect();
1331
1332 match expanded_token.as_str() {
1333 "." => {
1334 completions.push(".".into());
1335 completions.push("..".into());
1336 }
1337 ".." => {
1338 completions.push("..".into());
1339 }
1340 _ => {}
1341 }
1342
1343 completions.sort();
1344 completions.dedup();
1345 completions
1346}
1347
1348fn try_get_variable_completions(
1358 shell: &Shell<impl extensions::ShellExtensions>,
1359 token: &str,
1360) -> Option<(Vec<String>, ProcessingOptions)> {
1361 let (var_prefix, use_braces) = if let Some(prefix) = token.strip_prefix("${") {
1363 if prefix.contains('}') {
1365 return None;
1366 }
1367 (prefix, true)
1368 } else {
1369 let prefix = token.strip_prefix('$')?;
1370 (prefix, false)
1371 };
1372
1373 if sys::fs::contains_path_separator(var_prefix) {
1376 return None;
1377 }
1378
1379 let mut candidates: Vec<String> = shell
1381 .env()
1382 .iter()
1383 .filter(|(key, _)| key.starts_with(var_prefix))
1384 .map(|(key, _)| {
1385 if use_braces {
1386 format!("${{{key}}}")
1387 } else {
1388 format!("${key}")
1389 }
1390 })
1391 .collect();
1392 candidates.sort();
1393
1394 let options = ProcessingOptions {
1397 treat_as_filenames: false,
1398 ..ProcessingOptions::default()
1399 };
1400
1401 Some((candidates, options))
1402}
1403
1404fn command_completions(
1408 shell: &Shell<impl extensions::ShellExtensions>,
1409 prefix: &str,
1410) -> Vec<String> {
1411 let mut names = Vec::new();
1412
1413 extend_matching(&mut names, shell.aliases().keys().sorted(), prefix);
1416 extend_matching(&mut names, shell.get_keywords(), prefix);
1417 extend_matching(
1418 &mut names,
1419 shell.funcs().iter().map(|(name, _)| name).sorted(),
1420 prefix,
1421 );
1422 extend_matching(
1423 &mut names,
1424 shell
1425 .builtins()
1426 .iter()
1427 .filter(|(_, registration)| !registration.disabled)
1428 .map(|(name, _)| name)
1429 .sorted(),
1430 prefix,
1431 );
1432 names.extend(
1433 shell
1434 .find_executables_in_path_with_prefix(
1435 prefix,
1436 shell.options().case_insensitive_pathname_expansion,
1437 )
1438 .filter_map(|path| Some(path.file_name()?.to_string_lossy().into_owned())),
1439 );
1440
1441 names
1442}
1443
1444fn extend_matching<S: AsRef<str>>(
1446 candidates: &mut Vec<String>,
1447 names: impl IntoIterator<Item = S>,
1448 prefix: &str,
1449) {
1450 candidates.extend(
1451 names
1452 .into_iter()
1453 .filter(|name| name.as_ref().starts_with(prefix))
1454 .map(|name| name.as_ref().to_owned()),
1455 );
1456}
1457
1458async fn get_completions_using_basic_lookup(
1459 shell: &Shell<impl extensions::ShellExtensions>,
1460 context: &Context<'_>,
1461) -> (Vec<String>, ProcessingOptions) {
1462 let token = context.word;
1463
1464 if let Some(answer) = try_get_variable_completions(shell, token) {
1466 return answer;
1467 }
1468
1469 let mut candidates = get_file_completions(shell, token, false).await;
1471
1472 let is_command_position = context.line.words.cword == Some(0)
1477 && !token.is_empty()
1478 && !sys::fs::contains_path_separator(token);
1479
1480 if is_command_position {
1481 candidates.extend(command_completions(shell, token));
1482 candidates.sort();
1483 }
1484
1485 (candidates, ProcessingOptions::default())
1486}
1487
1488fn completion_filter_pattern_matches(
1489 pattern: &str,
1490 candidate: &str,
1491 token_being_completed: &str,
1492 shell: &Shell<impl extensions::ShellExtensions>,
1493) -> Result<bool, error::Error> {
1494 let pattern = replace_unescaped_ampersands(pattern, token_being_completed);
1495
1496 let pattern = shell_pattern(shell, pattern.as_ref());
1501
1502 let matches = pattern.exactly_matches(candidate)?;
1503
1504 Ok(matches)
1505}
1506
1507fn replace_unescaped_ampersands<'a>(pattern: &'a str, replacement: &str) -> Cow<'a, str> {
1508 let mut in_escape = false;
1509 let mut insertion_points = vec![];
1510
1511 for (i, c) in pattern.char_indices() {
1512 if !in_escape && c == '&' {
1513 insertion_points.push(i);
1514 }
1515 in_escape = !in_escape && c == '\\';
1516 }
1517
1518 if insertion_points.is_empty() {
1519 return pattern.into();
1520 }
1521
1522 let mut result = pattern.to_owned();
1523 for i in insertion_points.iter().rev() {
1524 result.replace_range(*i..=*i, replacement);
1525 }
1526
1527 result.into()
1528}
1529
1530fn shell_pattern(
1532 shell: &Shell<impl extensions::ShellExtensions>,
1533 pattern: impl Into<patterns::Pattern>,
1534) -> patterns::Pattern {
1535 pattern
1536 .into()
1537 .set_extended_globbing(shell.options().extended_globbing)
1538 .set_case_insensitive(shell.options().case_insensitive_pathname_expansion)
1539}
1540
1541#[cfg(test)]
1542mod tests {
1543 use super::*;
1544
1545 #[test]
1546 fn generation_options_enable_and_disable_each_option() {
1547 let mut options: GenerationOptions = std::iter::once(CompleteOption::NoSpace).collect();
1548 assert!(options.get(CompleteOption::NoSpace));
1549 assert!(!options.get(CompleteOption::NoSort));
1550
1551 options.set(CompleteOption::NoSort, true);
1552 options.set(CompleteOption::NoSpace, false);
1553 assert!(options.get(CompleteOption::NoSort));
1554 assert!(!options.get(CompleteOption::NoSpace));
1555 assert_eq!(
1556 options,
1557 std::iter::once(CompleteOption::NoSort).collect::<GenerationOptions>()
1558 );
1559 }
1560
1561 #[test]
1562 fn special_specs_have_bash_names() {
1563 assert_eq!(SpecialSpec::Default.command_name(), "_DefaultCmD_");
1564 assert_eq!(SpecialSpec::EmptyLine.command_name(), "_EmptycmD_");
1565 assert_eq!(SpecialSpec::InitialWord.command_name(), "_InitialWorD_");
1566 }
1567
1568 #[test]
1569 fn special_specs_are_each_stored_on_their_own() {
1570 let mut config = Config::default();
1571 let specials = [
1572 (SpecialSpec::Default, "d"),
1573 (SpecialSpec::EmptyLine, "e"),
1574 (SpecialSpec::InitialWord, "i"),
1575 ];
1576
1577 for (special, word) in specials {
1578 assert!(config.get(SpecName::Special(special)).is_none());
1579 let spec = Spec {
1580 word_list: Some(word.to_owned()),
1581 ..Spec::default()
1582 };
1583 config.set(SpecName::Special(special), spec);
1584 }
1585
1586 for (special, word) in specials {
1587 let spec = config.get(SpecName::Special(special));
1588 assert_eq!(spec.and_then(|spec| spec.word_list.as_deref()), Some(word));
1589
1590 if let Some(spec) = config.get_mut(SpecName::Special(special)) {
1591 spec.prefix = Some(word.to_uppercase());
1592 }
1593 let spec = config.get(SpecName::parse(special.command_name()));
1595 assert_eq!(
1596 spec.and_then(|spec| spec.prefix.clone()),
1597 Some(word.to_uppercase())
1598 );
1599 }
1600 }
1601
1602 #[test]
1603 fn spec_names_parse_bash_names_for_special_specs() {
1604 assert_eq!(
1605 SpecName::parse("_DefaultCmD_"),
1606 SpecName::Special(SpecialSpec::Default)
1607 );
1608 assert_eq!(SpecName::parse("mycmd"), SpecName::Command("mycmd"));
1609 for special in SpecialSpec::iter() {
1610 let name = SpecName::Special(special);
1611 assert_eq!(SpecName::parse(name.as_str()), name);
1612 }
1613 assert_eq!(SpecName::Command("mycmd").as_str(), "mycmd");
1614 }
1615
1616 #[test]
1617 fn special_specs_round_trip_through_bash_names() {
1618 for special in SpecialSpec::iter() {
1619 assert_eq!(
1620 SpecialSpec::from_command_name(special.command_name()),
1621 Some(special)
1622 );
1623 }
1624 assert_eq!(
1625 SpecialSpec::from_command_name("_DefaultCmD_"),
1626 Some(SpecialSpec::Default)
1627 );
1628 assert_eq!(SpecialSpec::from_command_name("mycmd"), None);
1629 }
1630
1631 #[test]
1632 fn completion_names_are_bash_names() {
1633 for action in CompleteAction::iter() {
1634 assert_eq!(<&str>::from(action).parse::<CompleteAction>(), Ok(action));
1635 }
1636 for option in CompleteOption::iter() {
1637 assert_eq!(<&str>::from(&option).parse::<CompleteOption>(), Ok(option));
1638 }
1639 assert_eq!(<&str>::from(CompleteAction::ArrayVar), "arrayvar");
1640 assert_eq!(<&str>::from(CompleteOption::BashDefault), "bashdefault");
1641 }
1642}