1#![cfg_attr(feature = "derive", doc = "```no_run")]
36#![cfg_attr(not(feature = "derive"), doc = "```ignore")]
37use std::ffi::OsString;
60use std::fmt::Write as _;
61
62use winnow::stream::BStr;
63
64use crate::help::{Command, Item};
65
66pub const REQUEST: &str = "__complete_word__";
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
72#[non_exhaustive]
73pub enum Hint {
74 #[default]
79 Nothing,
80 Files,
83 Dirs,
85 Commands,
87}
88
89impl Hint {
90 const fn line(self) -> Option<&'static str> {
92 match self {
93 Hint::Files => Some("\u{1}files"),
94 Hint::Dirs => Some("\u{1}dirs"),
95 Hint::Commands => Some("\u{1}commands"),
96 Hint::Nothing => None,
97 }
98 }
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
103#[non_exhaustive]
104pub enum Shell {
105 Bash,
107 Zsh,
109 Fish,
111 Elvish,
113 PowerShell,
115}
116
117impl Shell {
118 pub const ALL: &'static [Shell] = &[
120 Shell::Bash,
121 Shell::Zsh,
122 Shell::Fish,
123 Shell::Elvish,
124 Shell::PowerShell,
125 ];
126
127 pub const fn as_str(self) -> &'static str {
129 match self {
130 Shell::Bash => "bash",
131 Shell::Zsh => "zsh",
132 Shell::Fish => "fish",
133 Shell::Elvish => "elvish",
134 Shell::PowerShell => "powershell",
135 }
136 }
137
138 pub fn from_name(name: &str) -> Option<Shell> {
140 Shell::ALL.iter().copied().find(|s| s.as_str() == name)
141 }
142}
143
144impl crate::FromArg for Shell {
146 const SPEC: &'static str = "Shell";
148
149 const CHOICES: &'static [&'static str] = &["bash", "zsh", "fish", "elvish", "powershell"];
150
151 fn from_arg(value: &BStr) -> Result<Self, crate::error::BoxError> {
152 let name = std::str::from_utf8(value).ok();
153 match name.and_then(Shell::from_name) {
154 Some(shell) => Ok(shell),
155 None => Err(Box::new(crate::ChoiceError {
156 choices: Self::CHOICES,
157 })),
158 }
159 }
160}
161
162impl std::fmt::Display for Shell {
163 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164 f.write_str(self.as_str())
165 }
166}
167
168#[derive(Debug, Clone, PartialEq, Eq)]
170pub struct Candidate {
171 pub value: String,
173 pub help: &'static str,
175}
176
177#[derive(Debug, Clone, Default, PartialEq, Eq)]
179pub struct Completions {
180 pub candidates: Vec<Candidate>,
182 pub hint: Hint,
184 pub before: String,
187}
188
189pub fn complete(root: &'static Command, words: &[&str]) -> Completions {
193 let (current, before) = match words.split_last() {
194 Some((current, before)) => (*current, before),
195 None => ("", words),
196 };
197 let mut state = Walk {
198 command: root,
199 globals: Vec::new(),
200 owing: None,
201 stopped: false,
202 position: 0,
203 };
204 for word in before {
205 state.step(word);
206 }
207 state.complete(current)
208}
209
210#[derive(Clone, Copy)]
213struct Owing {
214 item: &'static Item,
215 fixed: usize,
216 flexible: bool,
217 once: bool,
218}
219
220struct Walk {
222 command: &'static Command,
223 globals: Vec<&'static Item>,
225 owing: Option<Owing>,
227 stopped: bool,
229 position: usize,
231}
232
233impl Walk {
234 fn within(&self, sub: &'static crate::help::Sub) -> Walk {
237 let mut globals = self.globals.clone();
238 globals.extend(self.command.items.iter().filter(|i| i.global));
239 Walk {
240 command: sub.command,
241 globals,
242 owing: None,
243 stopped: false,
244 position: 0,
245 }
246 }
247
248 fn handed_over(&self) -> Option<Walk> {
251 let command = self.command;
252 let sub = command.default_target()?;
253 command.default_subcommand_flags.then(|| self.within(sub))
254 }
255
256 fn flags(&self) -> impl Iterator<Item = &'static Item> + '_ {
257 let own = self.command.items.iter().filter(|i| !i.positional);
258 own.chain(self.globals.iter().copied())
259 }
260
261 fn long(&self, name: &str) -> Option<&'static Item> {
262 self.flags()
263 .find(|i| i.long == Some(name) || i.negate == Some(name) || i.aliases.contains(&name))
264 }
265
266 fn takes_next(item: &Item) -> bool {
268 item.value_name.is_some() && !item.require_equals
269 }
270
271 fn letter(&self, letter: char, plus: bool) -> Option<&'static Item> {
272 self.flags().find(|i| {
273 if plus {
274 i.plus == Some(letter)
275 } else {
276 i.short == Some(letter) || i.more_shorts.contains(&letter)
277 }
278 })
279 }
280
281 fn plus_options(&self) -> bool {
282 self.flags().any(|i| i.plus.is_some())
283 }
284
285 fn plus_letters<'a>(&self, word: &'a str) -> Option<&'a str> {
287 let letters = word.strip_prefix('+').filter(|l| !l.is_empty())?;
288 self.plus_options().then_some(letters)
289 }
290
291 fn owe(&mut self, item: &'static Item, attached: bool) {
293 if item.value_name.is_none() || (!attached && item.require_equals) {
294 return;
295 }
296 if item.more_values {
297 self.owing = Some(Owing {
298 item,
299 fixed: 0,
300 flexible: true,
301 once: false,
302 });
303 return;
304 }
305 if !attached && item.optional_value {
306 self.owing = Some(Owing {
307 item,
308 fixed: 0,
309 flexible: true,
310 once: true,
311 });
312 return;
313 }
314 let left = item.values.max(1) - usize::from(attached);
315 if left > 0 {
316 self.owing = Some(Owing {
317 item,
318 fixed: left,
319 flexible: false,
320 once: false,
321 });
322 }
323 }
324
325 fn consume_owing(&mut self, word: &str) -> bool {
328 let Some(owing) = self.owing else {
329 return false;
330 };
331 if owing.fixed > 0 {
332 let fixed = owing.fixed - 1;
333 self.owing = (fixed > 0).then_some(Owing { fixed, ..owing });
334 return true;
335 }
336 if owing.flexible && !crate::token::is_flag_like(word.as_bytes()) {
337 if owing.once {
338 self.owing = None;
339 }
340 return true;
341 }
342 self.owing = None;
343 false
344 }
345
346 fn bundle(&mut self, letters: &str, plus: bool) {
347 for (at, letter) in letters.char_indices() {
348 let handed = || {
349 self.handed_over()
350 .filter(|walk| walk.letter(letter, plus).is_some())
351 };
352 if self.letter(letter, plus).is_none()
353 && let Some(walk) = handed()
354 {
355 *self = walk;
356 }
357 let Some(item) = self.letter(letter, plus) else {
358 break;
359 };
360 if item.value_name.is_some() {
361 if at + letter.len_utf8() == letters.len() && Self::takes_next(item) {
363 self.owe(item, false);
364 }
365 break;
366 }
367 }
368 }
369
370 fn step(&mut self, word: &str) {
371 if self.consume_owing(word) {
372 return;
373 }
374 if self.stopped {
375 self.position += 1;
376 return;
377 }
378 if word == "--" {
379 self.stopped = true;
380 } else if let Some(body) = word.strip_prefix("--") {
381 let (name, value) = match body.split_once('=') {
382 Some((name, value)) => (name, Some(value)),
383 None => (body, None),
384 };
385 let handed = || self.handed_over().filter(|walk| walk.long(name).is_some());
386 if self.long(name).is_none()
387 && let Some(walk) = handed()
388 {
389 *self = walk;
390 }
391 if let Some(item) = self.long(name) {
392 self.owe(item, value.is_some());
393 }
394 } else if let Some(letters) = word.strip_prefix('-').filter(|l| !l.is_empty()) {
395 self.bundle(letters, false);
396 } else if let Some(letters) = self.plus_letters(word) {
397 self.bundle(letters, true);
398 } else if let Some(sub) = self
399 .command
400 .subcommands
401 .iter()
402 .find(|s| s.names.contains(&word))
403 {
404 *self = self.within(sub);
405 } else if let Some(sub) = self.command.default_target() {
406 *self = self.within(sub);
408 self.position = 1;
409 } else {
410 self.position += 1;
411 }
412 }
413
414 fn complete(&self, current: &str) -> Completions {
415 let mut out = Completions::default();
416 if let Some(owing) = self.owing {
417 let as_value = owing.fixed > 0
418 || (owing.flexible && !crate::token::is_flag_like(current.as_bytes()));
419 if as_value {
420 values(owing.item, "", current, &mut out);
421 return out;
422 }
423 }
424 if !self.stopped && current.starts_with('+') && self.plus_options() {
425 self.plus_names(current, &mut out);
426 return out;
427 }
428 if !self.stopped && current.starts_with('-') {
429 if let Some((name, value)) = current
430 .strip_prefix("--")
431 .and_then(|body| body.split_once('='))
432 {
433 let handed = self.handed_over();
434 let item = self
435 .long(name)
436 .or_else(|| handed.as_ref().and_then(|walk| walk.long(name)));
437 if let Some(item) = item.filter(|i| i.value_name.is_some()) {
438 values(
439 item,
440 ¤t[..current.len() - value.len()],
441 value,
442 &mut out,
443 );
444 }
445 return out;
446 }
447 self.flag_names(current, &mut out);
448 if let Some(walk) = self.handed_over() {
451 let shorts = !current.starts_with("--");
452 let own = walk.command.items.iter().filter(|i| !i.positional);
453 for item in own.filter(|i| !i.hide && !self.takes(i)) {
454 push_names(item, shorts, current, &mut out);
455 }
456 }
457 return out;
458 }
459 if !self.stopped {
460 for sub in self.command.subcommands.iter().filter(|s| !s.hide) {
461 push(&mut out, sub.name, sub.about, current);
462 }
463 }
464 self.positional(current, &mut out);
465 if !self.stopped
467 && self.position == 0
468 && let Some(sub) = self.command.default_target()
469 {
470 self.within(sub).positional(current, &mut out);
471 }
472 out
473 }
474
475 fn positional(&self, current: &str, out: &mut Completions) {
477 let mut positionals = self.command.items.iter().filter(|i| i.positional);
478 let positional = positionals
479 .clone()
480 .nth(self.position)
481 .or_else(|| positionals.rfind(|i| i.multiple));
482 if let Some(item) = positional {
483 values(item, "", current, out);
484 }
485 }
486
487 fn takes(&self, item: &Item) -> bool {
489 item.long.is_some_and(|long| self.long(long).is_some())
490 || item.short.is_some_and(|c| self.letter(c, false).is_some())
491 }
492
493 fn flag_names(&self, current: &str, out: &mut Completions) {
494 let shorts = !current.starts_with("--");
496 for item in self.flags().filter(|i| !i.hide) {
497 push_names(item, shorts, current, out);
498 }
499 let command = self.command;
500 if command.help_flag {
501 if command.help_short && shorts {
502 push(out, "-h", "Print help", current);
503 }
504 push(out, "--help", "Print help", current);
505 }
506 if command.version.is_some() {
507 if shorts {
508 push(out, "-V", "Print version", current);
509 }
510 push(out, "--version", "Print version", current);
511 }
512 }
513
514 fn plus_names(&self, current: &str, out: &mut Completions) {
515 for item in self.flags().filter(|i| !i.hide) {
516 if let Some(letter) = item.plus {
517 push(out, &format!("+{letter}"), item.help, current);
518 }
519 }
520 }
521}
522
523fn push_names(item: &'static Item, shorts: bool, current: &str, out: &mut Completions) {
526 if let Some(long) = item.long {
527 push(out, &format!("--{long}"), item.help, current);
528 }
529 if let Some(negate) = item.negate {
530 push(out, &format!("--{negate}"), item.help, current);
531 }
532 if shorts {
533 if let Some(short) = item.short {
534 push(out, &format!("-{short}"), item.help, current);
535 }
536 for &letter in item.more_shorts {
537 push(out, &format!("-{letter}"), item.help, current);
538 }
539 }
540}
541
542fn values(item: &'static Item, prefix: &str, typed: &str, out: &mut Completions) {
545 if let Some(complete) = item.complete {
546 let fitting = complete(typed)
547 .into_iter()
548 .filter(|c| c.value.starts_with(typed));
549 out.candidates.extend(fitting.map(|c| Candidate {
550 value: format!("{prefix}{}", c.value),
551 help: c.help,
552 }));
553 return;
554 }
555 if item.choices.is_empty() {
556 out.hint = item.hint;
557 prefix.clone_into(&mut out.before);
558 return;
559 }
560 for choice in item.choices {
561 if choice.starts_with(typed) {
562 out.candidates.push(Candidate {
563 value: format!("{prefix}{choice}"),
564 help: "",
565 });
566 }
567 }
568}
569
570fn push(out: &mut Completions, value: &str, help: &'static str, typed: &str) {
571 if value.starts_with(typed) {
572 out.candidates.push(Candidate {
573 value: value.to_owned(),
574 help,
575 });
576 }
577}
578
579pub fn answer(root: &'static Command, args: &[OsString]) -> Option<String> {
590 let (first, rest) = args.split_first()?;
591 if first.to_str()? != REQUEST {
592 return None;
593 }
594 let (mut shell, mut line, mut replaced) = (None, String::new(), None);
595 let mut given: Option<Vec<String>> = None;
596 let mut rest = rest.iter().map(|a| a.to_string_lossy().into_owned());
597 while let Some(option) = rest.next() {
598 if option == "--words" {
599 given = Some(rest.by_ref().collect());
601 break;
602 }
603 let value = rest.next().unwrap_or_default();
604 match option.as_str() {
605 "--shell" => shell = Shell::from_name(&value),
606 "--line" => line = value,
607 "--bash-word" => replaced = Some(value),
608 _ => {}
609 }
610 }
611 let shell = shell?;
612 let mut words = given.unwrap_or_else(|| split_line(&line));
613 if !words.is_empty() {
614 words.remove(0);
615 }
616 if words.is_empty() {
617 words.push(String::new());
618 }
619 let words: Vec<&str> = words.iter().map(String::as_str).collect();
620 let completions = complete(root, &words);
621 Some(match (shell, replaced) {
622 (Shell::Bash, Some(replaced)) => {
623 render_bash(&completions, words.last().copied().unwrap_or(""), &replaced)
624 }
625 _ => render(&completions, shell),
626 })
627}
628
629pub fn render(completions: &Completions, shell: Shell) -> String {
634 let mut out = String::new();
635 for c in &completions.candidates {
636 match shell {
637 Shell::Bash => out.push_str(&c.value),
638 Shell::Zsh => {
639 out.push_str(&c.value.replace(':', "\\:"));
640 if !c.help.is_empty() {
641 let _ = write!(out, ":{}", first_line(c.help));
642 }
643 }
644 Shell::Fish | Shell::Elvish | Shell::PowerShell => {
645 out.push_str(&c.value);
646 if !c.help.is_empty() {
647 let _ = write!(out, "\t{}", first_line(c.help));
648 }
649 }
650 }
651 out.push('\n');
652 }
653 ask_the_shell(completions, shell, &mut out);
654 out
655}
656
657fn ask_the_shell(completions: &Completions, shell: Shell, out: &mut String) {
660 let Some(line) = completions.hint.line() else {
661 return;
662 };
663 out.push_str(line);
664 if shell != Shell::Bash {
665 out.push('\t');
666 out.push_str(&completions.before);
667 }
668 out.push('\n');
669}
670
671fn render_bash(completions: &Completions, typed: &str, replaced: &str) -> String {
674 let replaced = if replaced == "=" { "" } else { replaced };
676 let cut = typed
677 .strip_suffix(replaced)
678 .map_or(0, |before| before.len());
679 let mut out = String::new();
680 for c in &completions.candidates {
681 out.push_str(c.value.get(cut..).unwrap_or(&c.value));
682 out.push('\n');
683 }
684 ask_the_shell(completions, Shell::Bash, &mut out);
685 out
686}
687
688fn first_line(text: &str) -> &str {
689 text.lines().next().unwrap_or("")
690}
691
692fn split_line(line: &str) -> Vec<String> {
695 let mut words = Vec::new();
696 let mut word: Option<String> = None;
697 let mut quote = None;
698 let mut chars = line.chars();
699 while let Some(c) = chars.next() {
700 match (quote, c) {
701 (Some(q), c) if c == q => quote = None,
702 (Some('"') | None, '\\') => {
703 if let Some(next) = chars.next() {
704 word.get_or_insert_default().push(next);
705 }
706 }
707 (Some(_), c) => word.get_or_insert_default().push(c),
708 (None, '\'' | '"') => {
709 quote = Some(c);
710 word.get_or_insert_default();
711 }
712 (None, c) if c.is_whitespace() => words.extend(word.take()),
713 (None, c) => word.get_or_insert_default().push(c),
714 }
715 }
716 words.push(word.unwrap_or_default());
717 words
718}
719
720pub fn script(bin: &str, shell: Shell) -> String {
728 assert!(
729 !bin.is_empty()
730 && bin
731 .chars()
732 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | '+')),
733 "a completion script needs a plain program name, not {bin:?}"
734 );
735 let function: String = bin
736 .chars()
737 .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
738 .collect();
739 match shell {
740 Shell::Bash => format!(
741 r#"# Completion for {bin}, answered by `{bin} {REQUEST}`.
742_{function}_complete() {{
743 local line ask= cur="${{COMP_WORDS[COMP_CWORD]}}"
744 # Right after `--name=`, the current word is the `=` itself.
745 [[ $cur == = ]] && cur=
746 COMPREPLY=()
747 while IFS= read -r line; do
748 case $line in
749 $'\001files') ask=f ;;
750 $'\001dirs') ask=d ;;
751 $'\001commands') ask=c ;;
752 ?*) COMPREPLY+=("$line") ;;
753 esac
754 done < <(command {bin} {REQUEST} --shell bash --line "${{COMP_LINE:0:COMP_POINT}}" \
755 --bash-word "${{COMP_WORDS[COMP_CWORD]}}" 2>/dev/null)
756 if [[ -n $ask ]]; then
757 [[ $ask == c ]] || compopt -o filenames 2>/dev/null
758 local IFS=$'\n'
759 COMPREPLY+=($(compgen -$ask -- "$cur"))
760 fi
761}}
762complete -F _{function}_complete {bin}
763"#
764 ),
765 Shell::Zsh => format!(
766 r#"#compdef {bin}
767# Completion for {bin}, answered by `{bin} {REQUEST}`.
768_{function}() {{
769 local -a candidates
770 local line ask= before=
771 while IFS= read -r line; do
772 case $line in
773 $'\001files\t'*) ask=_files ;;
774 $'\001dirs\t'*) ask='_path_files -/' ;;
775 $'\001commands\t'*) ask=_command_names ;;
776 ?*) candidates+=("$line"); continue ;;
777 esac
778 before=${{line#*$'\t'}}
779 done < <(command {bin} {REQUEST} --shell zsh --line "${{BUFFER[1,CURSOR]}}" 2>/dev/null)
780 (( ${{#candidates}} )) && _describe -t values '{bin}' candidates
781 if [[ -n $ask ]]; then
782 # `--name=`, when the value is attached: not a part of what is asked for.
783 if [[ -n $before && $PREFIX == "$before"* ]]; then
784 IPREFIX+=$before
785 PREFIX=${{PREFIX#"$before"}}
786 fi
787 ${{=ask}}
788 fi
789 return 0
790}}
791if [[ $zsh_eval_context[-1] == loadautofunc ]]; then
792 _{function} "$@"
793else
794 compdef _{function} {bin}
795fi
796"#
797 ),
798 Shell::Fish => format!(
799 r#"# Completion for {bin}, answered by `{bin} {REQUEST}`.
800function __{function}_complete
801 set -l token (commandline -ct)
802 for line in (command {bin} {REQUEST} --shell fish --line (commandline -cp) 2>/dev/null)
803 if not string match -q \u0001'*' -- $line
804 test -n "$line"; and printf '%s\n' $line
805 continue
806 end
807 # The mark, then `--name=` when the value is attached to its flag.
808 set -l asked (string split -m 1 \t -- $line)
809 set -l before $asked[2]
810 set -l typed (string sub -s (math (string length -- "$before") + 1) -- $token)
811 switch $asked[1]
812 case \u0001files
813 printf '%s\n' $before(__fish_complete_path "$typed")
814 case \u0001dirs
815 printf '%s\n' $before(__fish_complete_directories "$typed")
816 case \u0001commands
817 printf '%s\n' $before(complete -C "$typed")
818 end
819 end
820end
821complete -c {bin} -f -a '(__{function}_complete)'
822"#
823 ),
824 Shell::Elvish => format!(
825 r#"# Completion for {bin}, answered by `{bin} {REQUEST}`.
826use str
827
828set edit:completion:arg-completer[{bin}] = {{|@words|
829 for answer [(e:{bin} {REQUEST} --shell elvish --words $@words 2>/dev/null | from-lines)] {{
830 if (str:has-prefix $answer "\x01") {{
831 # The mark, then `--name=` when the value is attached to its flag.
832 var asked before = (str:split &max=2 "\t" $answer)
833 var typed = (str:trim-prefix $words[-1] $before)
834 var found = []
835 if (has-value ["\x01files" "\x01dirs"] $asked) {{
836 set found = [(edit:complete-filename $typed)]
837 }} elif (eq $asked "\x01commands") {{
838 set found = [(edit:complete-sudo sudo $typed)]
839 }}
840 for one $found {{
841 if (eq (kind-of $one) string) {{
842 put $before$one
843 }} elif (or (not-eq $asked "\x01dirs") (str:has-suffix $one[stem] /)) {{
844 edit:complex-candidate $before$one[stem] &code-suffix=$one[code-suffix] &display=$one[display]
845 }}
846 }}
847 }} elif (not-eq $answer '') {{
848 var parts = [(str:split "\t" $answer)]
849 var display = $parts[0]
850 if (> (count $parts) 1) {{
851 set display = (str:join ' -- ' $parts)
852 }}
853 edit:complex-candidate $parts[0] &display=$display
854 }}
855 }}
856}}
857"#
858 ),
859 Shell::PowerShell => format!(
860 r#"# Completion for {bin}, answered by `{bin} {REQUEST}`.
861Register-ArgumentCompleter -Native -CommandName '{bin}' -ScriptBlock {{
862 param($wordToComplete, $commandAst, $cursorPosition)
863
864 # The command's text up to the cursor: `$cursorPosition` counts from the
865 # start of the whole input, the extent from its own start.
866 $extent = $commandAst.Extent
867 $offset = [Math]::Min([Math]::Max($cursorPosition - $extent.StartOffset, 0), $extent.Text.Length)
868 $line = $extent.Text.Substring(0, $offset)
869 # The extent ends at the last token: a word not yet typed needs its space.
870 if ($wordToComplete -eq '' -and -not $line.EndsWith(' ')) {{ $line += ' ' }}
871 $answers = @(& '{bin}' {REQUEST} --shell powershell --line $line 2>$null)
872
873 foreach ($answer in $answers) {{
874 if ([string]::IsNullOrEmpty($answer)) {{ continue }}
875 $parts = $answer -split "`t", 2
876 if ($answer[0] -eq [char]1) {{
877 # The mark, then `--name=` when the value is attached to its flag.
878 $before = $parts[1]
879 $typed = $wordToComplete
880 if ($before -and $typed.StartsWith($before)) {{ $typed = $typed.Substring($before.Length) }}
881 $found = switch ($parts[0].Substring(1)) {{
882 'files' {{ [System.Management.Automation.CompletionCompleters]::CompleteFilename($typed) }}
883 'dirs' {{
884 [System.Management.Automation.CompletionCompleters]::CompleteFilename($typed) |
885 Where-Object ResultType -eq 'ProviderContainer'
886 }}
887 'commands' {{ [System.Management.Automation.CompletionCompleters]::CompleteCommand($typed) }}
888 }}
889 foreach ($one in $found) {{
890 [System.Management.Automation.CompletionResult]::new(
891 $before + $one.CompletionText, $one.ListItemText, $one.ResultType, $one.ToolTip)
892 }}
893 continue
894 }}
895 $help = if ($parts.Count -gt 1 -and $parts[1]) {{ $parts[1] }} else {{ $parts[0] }}
896 [System.Management.Automation.CompletionResult]::new(
897 $parts[0], $parts[0], 'ParameterValue', $help)
898 }}
899}}
900"#
901 ),
902 }
903}
904
905#[cfg(test)]
906mod tests {
907 use super::*;
908
909 #[test]
910 fn lines_split_as_a_shell_splits_them() {
911 assert_eq!(
912 split_line("prog -o 'a b' c\\ d"),
913 ["prog", "-o", "a b", "c d"]
914 );
915 assert_eq!(split_line("prog --x="), ["prog", "--x="]);
916 assert_eq!(split_line("prog "), ["prog", ""]);
917 assert_eq!(split_line("prog \"q\\\"x"), ["prog", "q\"x"]);
918 }
919
920 #[test]
921 fn bash_gets_the_part_after_its_word_break() {
922 let c = Completions {
923 candidates: vec![Candidate {
924 value: "--color=always".into(),
925 help: "",
926 }],
927 ..Completions::default()
928 };
929 assert_eq!(render_bash(&c, "--color=al", "al"), "always\n");
930 assert_eq!(render_bash(&c, "--color=", "="), "always\n");
931 assert_eq!(render_bash(&c, "--col", "--col"), "--color=always\n");
932 }
933
934 #[test]
935 fn scripts_call_back_the_program() {
936 for &shell in Shell::ALL {
937 let script = script("my-tool", shell);
938 assert!(script.contains("my-tool __complete_word__"), "{script}");
939 }
940 }
941}