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(
498 &self,
499 shell: &mut Shell<impl extensions::ShellExtensions>,
500 word: &str,
501 ) -> Result<Vec<String>, crate::error::Error> {
502 let word = unquote_str(word);
503 let context = Context {
504 word: &word,
505 line: LineContext {
506 input: "",
507 cursor: 0,
508 words: &words::NO_WORDS,
509 command_name: Some("compgen"),
510 comp_type: 0,
511 comp_key: 0,
512 },
513 };
514
515 let Generated::Candidates(SpecCandidates { candidates, .. }) =
516 self.generate_candidates(shell, &context).await?
517 else {
518 return Ok(Vec::new());
519 };
520
521 let (candidates, _options) = self
522 .apply_options(shell, &context, candidates, self.options.clone())
523 .await;
524 Ok(candidates)
525 }
526
527 async fn complete(
533 &self,
534 shell: &mut Shell<impl extensions::ShellExtensions>,
535 context: &Context<'_>,
536 ) -> Result<Generated<(Vec<String>, ProcessingOptions)>, crate::error::Error> {
537 let mut scope = InProgressScope::start(
538 shell,
539 InProgressCompletion {
540 options: self.options.clone(),
541 },
542 );
543
544 let Generated::Candidates(SpecCandidates {
545 candidates,
546 file_names,
547 }) = self.generate_candidates(scope.shell, context).await?
548 else {
549 return Ok(Generated::Restart);
550 };
551
552 let mut options = scope.take_options().unwrap_or_else(|| self.options.clone());
555 if file_names {
556 options.set(CompleteOption::FileNames, true);
557 }
558 let sort = !options.get(CompleteOption::NoSort);
559 let (mut candidates, processing_options) = self
560 .apply_options(scope.shell, context, candidates, options)
561 .await;
562 if sort {
563 candidates.sort();
564 }
565 Ok(Generated::Candidates((candidates, processing_options)))
566 }
567
568 async fn generate_candidates(
570 &self,
571 shell: &mut Shell<impl extensions::ShellExtensions>,
572 context: &Context<'_>,
573 ) -> Result<Generated<SpecCandidates>, crate::error::Error> {
574 let SpecCandidates {
576 mut candidates,
577 file_names,
578 } = self.generate_action_completions(shell, context).await?;
579 if let Some(word_list) = &self.word_list {
580 let params = shell.default_exec_params();
581 let unexpanded_words =
582 split_completion_word_list(word_list, &shell.ifs(), &shell.parser_options())?;
583 let options = crate::expansion::ExpanderOptions {
584 pathname_expand: false,
585 ..Default::default()
586 };
587 let mut words = vec![];
588 for word in unexpanded_words {
589 words.extend(
590 crate::expansion::full_expand_and_split_word_with_options(
591 shell, ¶ms, word, &options,
592 )
593 .await?,
594 );
595 }
596 candidates.extend(
597 words
598 .into_iter()
599 .filter(|word| word.starts_with(context.word)),
600 );
601 }
602
603 if let Some(glob_pattern) = &self.glob_pattern {
604 let pattern = shell_pattern(shell, glob_pattern.as_str());
605
606 let expansions = pattern
607 .expand(
608 shell.working_dir(),
609 Some(&patterns::Pattern::accept_all_expand_filter),
610 &patterns::FilenameExpansionOptions::default(),
611 )?
612 .into_paths();
613
614 candidates.extend(expansions);
615 }
616 if let Some(function_name) = &self.function_name {
617 match call_completion_function(shell, function_name, context).await? {
618 Generated::Candidates(new_candidates) => candidates.extend(new_candidates),
619 Generated::Restart => return Ok(Generated::Restart),
620 }
621 }
622 if let Some(command) = &self.command {
623 candidates.extend(call_completion_command(shell, command, context).await?);
624 }
625
626 if let Some(filter_pattern) = &self.filter_pattern
629 && !filter_pattern.is_empty()
630 {
631 let mut updated = Vec::new();
632
633 let (pattern, keep_matches) = match filter_pattern.strip_prefix('!') {
637 Some(rest) if !(shell.options().extended_globbing && rest.starts_with('(')) => {
638 (rest, true)
639 }
640 _ => (filter_pattern.as_str(), false),
641 };
642
643 for candidate in candidates {
644 let matches = completion_filter_pattern_matches(
645 pattern,
646 candidate.as_str(),
647 context.word,
648 shell,
649 )?;
650
651 if matches == keep_matches {
652 updated.push(candidate);
653 }
654 }
655
656 candidates = updated;
657 }
658
659 if self.prefix.is_some() || self.suffix.is_some() {
661 let empty = String::new();
662 let prefix = self.prefix.as_ref().unwrap_or(&empty);
663 let suffix = self.suffix.as_ref().unwrap_or(&empty);
664
665 let mut updated = Vec::with_capacity(candidates.len() * (prefix.len() + suffix.len()));
666 for candidate in candidates {
667 updated.push(std::format!("{prefix}{candidate}{suffix}"));
668 }
669
670 candidates = updated;
671 }
672
673 Ok(Generated::Candidates(SpecCandidates {
674 candidates,
675 file_names,
676 }))
677 }
678
679 async fn apply_options(
682 &self,
683 shell: &Shell<impl extensions::ShellExtensions>,
684 context: &Context<'_>,
685 mut candidates: Vec<String>,
686 options: GenerationOptions,
687 ) -> (Vec<String>, ProcessingOptions) {
688 let mut processing_options = ProcessingOptions {
689 treat_as_filenames: options.get(CompleteOption::FileNames),
690 no_autoquote_filenames: options.get(CompleteOption::NoQuote),
691 no_trailing_space_at_end_of_line: options.get(CompleteOption::NoSpace),
692 };
693
694 if options.get(CompleteOption::PlusDirs)
697 || (options.get(CompleteOption::DirNames) && candidates.is_empty())
698 {
699 let mut dir_candidates =
700 get_file_completions(shell, context.word, true).await;
701
702 if candidates.is_empty() && shell.completion_config().fallback_options.mark_directories
704 {
705 processing_options.treat_as_filenames = true;
706 }
707
708 candidates.append(&mut dir_candidates);
709 }
710
711 if candidates.is_empty() && options.get(CompleteOption::BashDefault) {
714 tracing::debug!(target: trace_categories::COMPLETION, "unimplemented: complete -o bashdefault");
718 }
719
720 if candidates.is_empty() && options.get(CompleteOption::Default) {
723 let must_be_dir = options.get(CompleteOption::DirNames);
726
727 let mut default_candidates =
728 get_file_completions(shell, context.word, must_be_dir).await;
729 candidates.append(&mut default_candidates);
730
731 if shell.completion_config().fallback_options.mark_directories {
732 processing_options.treat_as_filenames = true;
733 }
734 }
735
736 (candidates, processing_options)
737 }
738
739 #[expect(clippy::too_many_lines, reason = "a flat match over the actions")]
741 async fn generate_action_completions(
742 &self,
743 shell: &Shell<impl extensions::ShellExtensions>,
744 context: &Context<'_>,
745 ) -> Result<SpecCandidates, error::Error> {
746 let prefix = context.word;
747 let mut candidates = Vec::new();
748 let mut file_names = false;
749
750 for action in &self.actions {
751 let c = &mut candidates;
752 match action {
753 CompleteAction::Alias => {
756 extend_matching(c, shell.aliases().keys().sorted(), prefix);
757 }
758 CompleteAction::ArrayVar => extend_matching(
759 c,
760 shell
761 .env()
762 .iter()
763 .filter(|(_, var)| var.value().is_array())
764 .map(|(name, _)| name),
765 prefix,
766 ),
767 CompleteAction::Binding => extend_matching(
768 c,
769 interfaces::InputFunction::iter().map(<&'static str>::from),
770 prefix,
771 ),
772 CompleteAction::Builtin | CompleteAction::HelpTopic => {
774 extend_matching(c, shell.builtins().keys(), prefix);
775 }
776 CompleteAction::Command => c.extend(command_completions(shell, prefix)),
777 CompleteAction::Directory => {
778 let dirs = get_file_completions(shell, context.word, true).await;
779 file_names |= !dirs.is_empty();
780 c.extend(dirs);
781 }
782 CompleteAction::Disabled => extend_matching(
783 c,
784 shell
785 .builtins()
786 .iter()
787 .filter(|(_, registration)| registration.disabled)
788 .map(|(name, _)| name),
789 prefix,
790 ),
791 CompleteAction::Enabled => extend_matching(
792 c,
793 shell
794 .builtins()
795 .iter()
796 .filter(|(_, registration)| !registration.disabled)
797 .map(|(name, _)| name),
798 prefix,
799 ),
800 CompleteAction::Export => extend_matching(
801 c,
802 shell
803 .env()
804 .iter()
805 .filter(|(_, var)| var.is_exported())
806 .map(|(name, _)| name),
807 prefix,
808 ),
809 CompleteAction::File => {
810 file_names = true;
811 c.extend(get_file_completions(shell, context.word, false).await);
812 }
813 CompleteAction::Function => extend_matching(
814 c,
815 shell.funcs().iter().map(|(name, _)| name).sorted(),
816 prefix,
817 ),
818 CompleteAction::Group => extend_matching(c, users::get_all_groups()?, prefix),
819 CompleteAction::HostName => extend_matching(
821 c,
822 sys::network::get_hostname()
823 .ok()
824 .map(|name| name.to_string_lossy().into_owned()),
825 prefix,
826 ),
827 CompleteAction::Job => extend_matching(
828 c,
829 shell.jobs().jobs.iter().map(|job| job.command_name()),
830 prefix,
831 ),
832 CompleteAction::Keyword => extend_matching(c, shell.get_keywords(), prefix),
833 CompleteAction::Running => extend_matching(
834 c,
835 shell
836 .jobs()
837 .jobs
838 .iter()
839 .filter(|job| matches!(job.state, jobs::JobState::Running))
840 .map(|job| job.command_name()),
841 prefix,
842 ),
843 CompleteAction::Service => {
844 tracing::debug!(target: trace_categories::COMPLETION, "unimplemented: complete -A service");
845 }
846 CompleteAction::SetOpt => extend_matching(
847 c,
848 namedoptions::options(namedoptions::ShellOptionKind::SetO)
849 .iter()
850 .map(|option| option.name),
851 prefix,
852 ),
853 CompleteAction::ShOpt => extend_matching(
854 c,
855 namedoptions::options(namedoptions::ShellOptionKind::Shopt)
856 .iter()
857 .map(|option| option.name),
858 prefix,
859 ),
860 CompleteAction::Signal => extend_matching(
861 c,
862 traps::TrapSignal::iterator().map(traps::TrapSignal::as_str),
863 prefix,
864 ),
865 CompleteAction::Stopped => extend_matching(
866 c,
867 shell
868 .jobs()
869 .jobs
870 .iter()
871 .filter(|job| matches!(job.state, jobs::JobState::Stopped))
872 .map(|job| job.command_name()),
873 prefix,
874 ),
875 CompleteAction::User => extend_matching(c, users::get_all_users()?, prefix),
876 CompleteAction::Variable => {
877 extend_matching(c, shell.env().iter().map(|(name, _)| name), prefix);
878 }
879 }
880 }
881
882 Ok(SpecCandidates {
883 candidates,
884 file_names,
885 })
886 }
887}
888
889async fn call_completion_command(
892 shell: &mut Shell<impl extensions::ShellExtensions>,
893 command: &str,
894 context: &Context<'_>,
895) -> Result<Vec<String>, error::Error> {
896 let line = context.line;
897
898 let mut command_line = command.to_owned();
900 for arg in line.args(context.word) {
901 command_line.push(' ');
902 command_line.push_str(&escape::quote_if_needed(
903 arg,
904 escape::QuoteMode::SingleQuote,
905 ));
906 }
907
908 let status = shell.save_command_status();
912 let mut scoped = enter_comp_vars_scope(shell, line.comp_vars(), true)?;
913 let mut blocked = scoped.block_trap_delivery();
914 let params = blocked.default_exec_params();
915 let output =
916 commands::invoke_command_in_subshell_and_get_output(&mut blocked, ¶ms, command_line)
917 .await;
918 drop(blocked);
919 drop(scoped);
920 shell.restore_command_status(status);
921
922 Ok(output?.lines().map(str::to_owned).collect())
923}
924
925async fn call_completion_function(
929 shell: &mut Shell<impl extensions::ShellExtensions>,
930 function_name: &str,
931 context: &Context<'_>,
932) -> Result<Generated<Vec<String>>, error::Error> {
933 let line = context.line;
934 let mut vars = line.comp_vars();
935 vars.extend(line.comp_words_vars());
936
937 tracing::debug!(target: trace_categories::COMPLETION, "[calling completion func '{function_name}']: {}",
938 vars.iter().map(|(k, v)| std::format!("{k}={v:?}")).collect::<Vec<String>>().join(" "));
939
940 let names: Vec<_> = vars.iter().map(|(name, _)| *name).collect();
944 let mut scoped = enter_comp_vars_scope(shell, vars, false)?;
945 let mut blocked = scoped.block_trap_delivery();
946 let params = blocked.default_exec_params();
947 let invoke_result = blocked
948 .invoke_function(function_name, line.args(context.word).iter(), params)
949 .await
950 .map(|result| u8::from(result.exit_code));
951
952 drop(blocked);
954 drop(scoped);
955
956 for name in names {
960 let _ = shell.env_mut().unset(name);
961 }
962
963 tracing::debug!(target: trace_categories::COMPLETION, "[completion function '{function_name}' returned: {invoke_result:?}]");
964
965 let result = invoke_result.unwrap_or_else(|e| {
966 tracing::warn!(target: trace_categories::COMPLETION, "error while running completion function '{function_name}': {e}");
967 1 });
969
970 let reply = shell.env_mut().unset("COMPREPLY")?;
973
974 if result == 124 {
977 return Ok(Generated::Restart);
978 }
979
980 tracing::debug!(target: trace_categories::COMPLETION, "[completion function yielded: {reply:?}]");
981
982 let candidates = match reply.as_ref().map(|reply| reply.value()) {
983 Some(variables::ShellValue::IndexedArray(values)) => {
984 values.values().map(|v| v.to_owned()).collect()
985 }
986 Some(variables::ShellValue::String(s)) => vec![s.to_owned()],
987 _ => Vec::new(),
988 };
989 Ok(Generated::Candidates(candidates))
990}
991
992#[derive(Debug, Default)]
994pub struct Completions {
995 pub insertion_index: usize,
999 pub delete_count: usize,
1003 pub candidates: Vec<String>,
1005 pub options: ProcessingOptions,
1007}
1008
1009#[derive(Debug)]
1012pub struct ProcessingOptions {
1013 pub treat_as_filenames: bool,
1015 pub no_autoquote_filenames: bool,
1017 pub no_trailing_space_at_end_of_line: bool,
1020}
1021
1022impl Default for ProcessingOptions {
1023 fn default() -> Self {
1024 Self {
1025 treat_as_filenames: true,
1026 no_autoquote_filenames: false,
1027 no_trailing_space_at_end_of_line: false,
1028 }
1029 }
1030}
1031
1032#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, strum_macros::EnumIter)]
1034#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1035pub enum SpecialSpec {
1036 Default,
1038 EmptyLine,
1040 InitialWord,
1042}
1043
1044impl SpecialSpec {
1045 pub const fn command_name(self) -> &'static str {
1050 match self {
1051 Self::Default => "_DefaultCmD_",
1052 Self::EmptyLine => "_EmptycmD_",
1053 Self::InitialWord => "_InitialWorD_",
1054 }
1055 }
1056
1057 pub fn from_command_name(name: &str) -> Option<Self> {
1059 Self::iter().find(|special| special.command_name() == name)
1060 }
1061}
1062
1063#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
1065pub enum SpecName<'a> {
1066 Command(&'a str),
1068 Special(SpecialSpec),
1070}
1071
1072impl<'a> SpecName<'a> {
1073 pub fn parse(name: &'a str) -> Self {
1076 SpecialSpec::from_command_name(name).map_or(Self::Command(name), Self::Special)
1077 }
1078
1079 pub const fn as_str(self) -> &'a str {
1082 match self {
1083 Self::Command(command) => command,
1084 Self::Special(special) => special.command_name(),
1085 }
1086 }
1087}
1088
1089impl Config {
1090 pub fn clear(&mut self) {
1092 self.commands.clear();
1093 self.specials.clear();
1094 }
1095
1096 pub fn remove(&mut self, name: SpecName<'_>) -> bool {
1103 match name {
1104 SpecName::Command(command) => self.commands.remove(command).is_some(),
1105 SpecName::Special(special) => self.specials.remove(&special).is_some(),
1106 }
1107 }
1108
1109 pub fn iter(&self) -> impl Iterator<Item = (SpecName<'_>, &Spec)> {
1112 let commands = self
1113 .commands
1114 .iter()
1115 .map(|(command, spec)| (SpecName::Command(command), spec));
1116 let specials = self
1117 .specials
1118 .iter()
1119 .map(|(special, spec)| (SpecName::Special(*special), spec));
1120 commands.chain(specials)
1121 }
1122
1123 pub fn get(&self, name: SpecName<'_>) -> Option<&Spec> {
1129 match name {
1130 SpecName::Command(command) => self.commands.get(command),
1131 SpecName::Special(special) => self.specials.get(&special),
1132 }
1133 }
1134
1135 pub fn get_mut(&mut self, name: SpecName<'_>) -> Option<&mut Spec> {
1141 match name {
1142 SpecName::Command(command) => self.commands.get_mut(command),
1143 SpecName::Special(special) => self.specials.get_mut(&special),
1144 }
1145 }
1146
1147 pub fn set(&mut self, name: SpecName<'_>, spec: Spec) {
1155 match name {
1156 SpecName::Command(command) => {
1157 self.commands.insert(command.to_owned(), spec);
1158 }
1159 SpecName::Special(special) => {
1160 self.specials.insert(special, spec);
1161 }
1162 }
1163 }
1164
1165 fn find_spec<'l>(&self, line: &LineContext<'l>) -> Option<(&Spec, Option<&'l str>)> {
1174 let special = |special: SpecialSpec| {
1175 let spec = self.specials.get(&special)?;
1176 Some((spec, Some(special.command_name())))
1177 };
1178
1179 if line.cursor == 0 && line.input.chars().next().is_none_or(char::is_whitespace) {
1184 return special(SpecialSpec::EmptyLine);
1185 }
1186
1187 if matches!(line.words.cword, None | Some(0)) {
1188 return special(SpecialSpec::InitialWord);
1189 }
1190
1191 let command_name = line.command_name?;
1192 let spec = self
1193 .commands
1194 .get(command_name)
1195 .or_else(|| {
1196 let file_name = Path::new(command_name).file_name()?;
1197 self.commands.get(file_name.to_string_lossy().as_ref())
1198 })
1199 .or_else(|| self.specials.get(&SpecialSpec::Default))?;
1200 Some((spec, Some(command_name)))
1201 }
1202}
1203
1204pub(crate) async fn complete(
1206 shell: &mut Shell<impl extensions::ShellExtensions>,
1207 input: &str,
1208 cursor: usize,
1209) -> Result<Completions, error::Error> {
1210 const MAX_RESTARTS: u32 = 10;
1212
1213 if !input.is_char_boundary(cursor) {
1214 return Err(error::ErrorKind::InvalidCompletionPosition(cursor).into());
1215 }
1216
1217 let word_breaks = word_break_chars(shell);
1218 let word = find_completion_word(input, &word_breaks, cursor);
1219 let line_words = find_line_words(input, &word_breaks, cursor);
1220 let context = Context {
1221 word: &word.text,
1222 line: LineContext::new(input, cursor, &line_words),
1223 };
1224
1225 let mut completed = None;
1228 for _ in 0..=MAX_RESTARTS {
1229 if let Generated::Candidates(candidates_and_options) = complete_word(shell, context).await {
1230 completed = Some(candidates_and_options);
1231 break;
1232 }
1233 }
1234 let (candidates, options) = completed.unwrap_or_else(|| {
1235 tracing::warn!(target: trace_categories::COMPLETION, "completion kept restarting; giving up");
1236 (Vec::new(), ProcessingOptions::default())
1237 });
1238
1239 Ok(Completions {
1240 insertion_index: word.range.start,
1241 delete_count: word.range.len(),
1242 candidates,
1243 options,
1244 })
1245}
1246
1247fn word_break_chars(shell: &Shell<impl extensions::ShellExtensions>) -> Vec<char> {
1248 const FALLBACK: &str = " \t\n\"\'@><=;|&(:";
1249
1250 shell
1251 .env_str("COMP_WORDBREAKS")
1252 .unwrap_or_else(|| FALLBACK.into())
1253 .chars()
1254 .collect()
1255}
1256
1257async fn complete_word(
1261 shell: &mut Shell<impl extensions::ShellExtensions>,
1262 mut context: Context<'_>,
1263) -> Generated<(Vec<String>, ProcessingOptions)> {
1264 let Some((spec, command_name)) = shell.completion_config().find_spec(&context.line) else {
1267 return Generated::Candidates(get_completions_using_basic_lookup(shell, &context).await);
1268 };
1269 let spec = spec.clone();
1270 context.line.command_name = command_name;
1271
1272 spec.complete(shell, &context).await.unwrap_or_else(|err| {
1273 tracing::debug!(target: trace_categories::COMPLETION, "completion spec failed: {err}");
1274 Generated::Candidates((Vec::new(), ProcessingOptions::default()))
1275 })
1276}
1277
1278async fn get_file_completions(
1279 shell: &Shell<impl extensions::ShellExtensions>,
1280 token_to_complete: &str,
1281 must_be_dir: bool,
1282) -> Vec<String> {
1283 let mut throwaway_shell = shell.clone();
1286 let params = throwaway_shell.default_exec_params();
1287 let options = expansion::ExpanderOptions {
1288 execute_command_substitutions: false,
1289 ..Default::default()
1290 };
1291 let expanded_token = expansion::basic_expand_word_with_options(
1292 &mut throwaway_shell,
1293 ¶ms,
1294 &unquote_str(token_to_complete),
1295 &options,
1296 )
1297 .await
1298 .unwrap_or_else(|_err| token_to_complete.to_owned());
1299
1300 let expanded_token = sys::fs::normalize_path_separators(&expanded_token).into_owned();
1304
1305 let glob = std::format!("{expanded_token}*");
1306
1307 let path_filter = |path: &Path| !must_be_dir || shell.absolute_path(path).is_dir();
1308
1309 let pattern = shell_pattern(shell, glob);
1310
1311 let mut completions: Vec<String> = pattern
1312 .expand(
1313 shell.working_dir(),
1314 Some(&path_filter),
1315 &patterns::FilenameExpansionOptions::default(),
1316 )
1317 .unwrap_or_default()
1318 .into_paths()
1319 .into_iter()
1320 .map(|p| match sys::fs::normalize_path_separators(&p) {
1321 std::borrow::Cow::Borrowed(_) => p,
1322 std::borrow::Cow::Owned(normalized) => normalized,
1323 })
1324 .collect();
1325
1326 match expanded_token.as_str() {
1327 "." => {
1328 completions.push(".".into());
1329 completions.push("..".into());
1330 }
1331 ".." => {
1332 completions.push("..".into());
1333 }
1334 _ => {}
1335 }
1336
1337 completions.sort();
1338 completions.dedup();
1339 completions
1340}
1341
1342fn try_get_variable_completions(
1352 shell: &Shell<impl extensions::ShellExtensions>,
1353 token: &str,
1354) -> Option<(Vec<String>, ProcessingOptions)> {
1355 let (var_prefix, use_braces) = if let Some(prefix) = token.strip_prefix("${") {
1357 if prefix.contains('}') {
1359 return None;
1360 }
1361 (prefix, true)
1362 } else {
1363 let prefix = token.strip_prefix('$')?;
1364 (prefix, false)
1365 };
1366
1367 if sys::fs::contains_path_separator(var_prefix) {
1370 return None;
1371 }
1372
1373 let mut candidates: Vec<String> = shell
1375 .env()
1376 .iter()
1377 .filter(|(key, _)| key.starts_with(var_prefix))
1378 .map(|(key, _)| {
1379 if use_braces {
1380 format!("${{{key}}}")
1381 } else {
1382 format!("${key}")
1383 }
1384 })
1385 .collect();
1386 candidates.sort();
1387
1388 let options = ProcessingOptions {
1391 treat_as_filenames: false,
1392 ..ProcessingOptions::default()
1393 };
1394
1395 Some((candidates, options))
1396}
1397
1398fn command_completions(
1402 shell: &Shell<impl extensions::ShellExtensions>,
1403 prefix: &str,
1404) -> Vec<String> {
1405 let mut names = Vec::new();
1406
1407 extend_matching(&mut names, shell.aliases().keys().sorted(), prefix);
1410 extend_matching(&mut names, shell.get_keywords(), prefix);
1411 extend_matching(
1412 &mut names,
1413 shell.funcs().iter().map(|(name, _)| name).sorted(),
1414 prefix,
1415 );
1416 extend_matching(
1417 &mut names,
1418 shell
1419 .builtins()
1420 .iter()
1421 .filter(|(_, registration)| !registration.disabled)
1422 .map(|(name, _)| name)
1423 .sorted(),
1424 prefix,
1425 );
1426 names.extend(
1427 shell
1428 .find_executables_in_path_with_prefix(
1429 prefix,
1430 shell.options().case_insensitive_pathname_expansion,
1431 )
1432 .filter_map(|path| Some(path.file_name()?.to_string_lossy().into_owned())),
1433 );
1434
1435 names
1436}
1437
1438fn extend_matching<S: AsRef<str>>(
1440 candidates: &mut Vec<String>,
1441 names: impl IntoIterator<Item = S>,
1442 prefix: &str,
1443) {
1444 candidates.extend(
1445 names
1446 .into_iter()
1447 .filter(|name| name.as_ref().starts_with(prefix))
1448 .map(|name| name.as_ref().to_owned()),
1449 );
1450}
1451
1452async fn get_completions_using_basic_lookup(
1453 shell: &Shell<impl extensions::ShellExtensions>,
1454 context: &Context<'_>,
1455) -> (Vec<String>, ProcessingOptions) {
1456 let token = context.word;
1457
1458 if let Some(answer) = try_get_variable_completions(shell, token) {
1460 return answer;
1461 }
1462
1463 let mut candidates = get_file_completions(shell, token, false).await;
1465
1466 let is_command_position = context.line.words.cword == Some(0)
1471 && !token.is_empty()
1472 && !sys::fs::contains_path_separator(token);
1473
1474 if is_command_position {
1475 candidates.extend(command_completions(shell, token));
1476 candidates.sort();
1477 }
1478
1479 (candidates, ProcessingOptions::default())
1480}
1481
1482fn completion_filter_pattern_matches(
1483 pattern: &str,
1484 candidate: &str,
1485 token_being_completed: &str,
1486 shell: &Shell<impl extensions::ShellExtensions>,
1487) -> Result<bool, error::Error> {
1488 let pattern = replace_unescaped_ampersands(pattern, token_being_completed);
1489
1490 let pattern = shell_pattern(shell, pattern.as_ref());
1495
1496 let matches = pattern.exactly_matches(candidate)?;
1497
1498 Ok(matches)
1499}
1500
1501fn replace_unescaped_ampersands<'a>(pattern: &'a str, replacement: &str) -> Cow<'a, str> {
1502 let mut in_escape = false;
1503 let mut insertion_points = vec![];
1504
1505 for (i, c) in pattern.char_indices() {
1506 if !in_escape && c == '&' {
1507 insertion_points.push(i);
1508 }
1509 in_escape = !in_escape && c == '\\';
1510 }
1511
1512 if insertion_points.is_empty() {
1513 return pattern.into();
1514 }
1515
1516 let mut result = pattern.to_owned();
1517 for i in insertion_points.iter().rev() {
1518 result.replace_range(*i..=*i, replacement);
1519 }
1520
1521 result.into()
1522}
1523
1524fn shell_pattern(
1526 shell: &Shell<impl extensions::ShellExtensions>,
1527 pattern: impl Into<patterns::Pattern>,
1528) -> patterns::Pattern {
1529 pattern
1530 .into()
1531 .set_extended_globbing(shell.options().extended_globbing)
1532 .set_case_insensitive(shell.options().case_insensitive_pathname_expansion)
1533}
1534
1535#[cfg(test)]
1536mod tests {
1537 use super::*;
1538
1539 #[test]
1540 fn generation_options_enable_and_disable_each_option() {
1541 let mut options: GenerationOptions = std::iter::once(CompleteOption::NoSpace).collect();
1542 assert!(options.get(CompleteOption::NoSpace));
1543 assert!(!options.get(CompleteOption::NoSort));
1544
1545 options.set(CompleteOption::NoSort, true);
1546 options.set(CompleteOption::NoSpace, false);
1547 assert!(options.get(CompleteOption::NoSort));
1548 assert!(!options.get(CompleteOption::NoSpace));
1549 assert_eq!(
1550 options,
1551 std::iter::once(CompleteOption::NoSort).collect::<GenerationOptions>()
1552 );
1553 }
1554
1555 #[test]
1556 fn special_specs_have_bash_names() {
1557 assert_eq!(SpecialSpec::Default.command_name(), "_DefaultCmD_");
1558 assert_eq!(SpecialSpec::EmptyLine.command_name(), "_EmptycmD_");
1559 assert_eq!(SpecialSpec::InitialWord.command_name(), "_InitialWorD_");
1560 }
1561
1562 #[test]
1563 fn special_specs_are_each_stored_on_their_own() {
1564 let mut config = Config::default();
1565 let specials = [
1566 (SpecialSpec::Default, "d"),
1567 (SpecialSpec::EmptyLine, "e"),
1568 (SpecialSpec::InitialWord, "i"),
1569 ];
1570
1571 for (special, word) in specials {
1572 assert!(config.get(SpecName::Special(special)).is_none());
1573 let spec = Spec {
1574 word_list: Some(word.to_owned()),
1575 ..Spec::default()
1576 };
1577 config.set(SpecName::Special(special), spec);
1578 }
1579
1580 for (special, word) in specials {
1581 let spec = config.get(SpecName::Special(special));
1582 assert_eq!(spec.and_then(|spec| spec.word_list.as_deref()), Some(word));
1583
1584 if let Some(spec) = config.get_mut(SpecName::Special(special)) {
1585 spec.prefix = Some(word.to_uppercase());
1586 }
1587 let spec = config.get(SpecName::parse(special.command_name()));
1589 assert_eq!(
1590 spec.and_then(|spec| spec.prefix.clone()),
1591 Some(word.to_uppercase())
1592 );
1593 }
1594 }
1595
1596 #[test]
1597 fn spec_names_parse_bash_names_for_special_specs() {
1598 assert_eq!(
1599 SpecName::parse("_DefaultCmD_"),
1600 SpecName::Special(SpecialSpec::Default)
1601 );
1602 assert_eq!(SpecName::parse("mycmd"), SpecName::Command("mycmd"));
1603 for special in SpecialSpec::iter() {
1604 let name = SpecName::Special(special);
1605 assert_eq!(SpecName::parse(name.as_str()), name);
1606 }
1607 assert_eq!(SpecName::Command("mycmd").as_str(), "mycmd");
1608 }
1609
1610 #[test]
1611 fn special_specs_round_trip_through_bash_names() {
1612 for special in SpecialSpec::iter() {
1613 assert_eq!(
1614 SpecialSpec::from_command_name(special.command_name()),
1615 Some(special)
1616 );
1617 }
1618 assert_eq!(
1619 SpecialSpec::from_command_name("_DefaultCmD_"),
1620 Some(SpecialSpec::Default)
1621 );
1622 assert_eq!(SpecialSpec::from_command_name("mycmd"), None);
1623 }
1624
1625 #[test]
1626 fn completion_names_are_bash_names() {
1627 for action in CompleteAction::iter() {
1628 assert_eq!(<&str>::from(action).parse::<CompleteAction>(), Ok(action));
1629 }
1630 for option in CompleteOption::iter() {
1631 assert_eq!(<&str>::from(&option).parse::<CompleteOption>(), Ok(option));
1632 }
1633 assert_eq!(<&str>::from(CompleteAction::ArrayVar), "arrayvar");
1634 assert_eq!(<&str>::from(CompleteOption::BashDefault), "bashdefault");
1635 }
1636}