Skip to main content

click/
completion.rs

1//! Shell completion support for click-rs.
2//!
3//! This module provides shell completion functionality for Bash, Zsh, and Fish shells.
4//! It generates completion scripts that can be sourced in each shell to provide
5//! tab completion for CLI applications built with click-rs.
6//!
7//! # Reference
8//!
9//! Based on Python Click's `shell_completion.py`.
10//!
11//! # Example
12//!
13//! ```no_run
14//! use click::completion::{get_completion_class, shell_complete};
15//! use click::command::Command;
16//!
17//! let cmd = Command::new("myapp").build();
18//!
19//! // Generate completion script for bash
20//! if let Some(completer) = get_completion_class("bash") {
21//!     let script = completer.source_template();
22//!     println!("{}", script.replace("%(prog_name)s", "myapp"));
23//! }
24//! ```
25
26use std::env;
27use std::io::{self, Write};
28
29use crate::command::Command;
30use crate::context::ContextBuilder;
31use crate::group::{CommandCollection, CommandLike, Group};
32use crate::parameter::Parameter;
33use crate::types::CompletionItem;
34use crate::utils::split_arg_string;
35
36// =============================================================================
37// ShellComplete Trait
38// =============================================================================
39
40/// Trait for shell-specific completion implementations.
41///
42/// Each shell has different completion mechanisms and script formats.
43/// Implementations of this trait provide the necessary integration for each shell.
44pub trait ShellComplete: Send + Sync {
45    /// Returns the name of the shell (e.g., "bash", "zsh", "fish").
46    fn name(&self) -> &str;
47
48    /// Returns the shell script template for enabling completions.
49    ///
50    /// The template can contain placeholders:
51    /// - `%(prog_name)s` - The program name
52    /// - `%(complete_func)s` - The completion function name
53    /// - `%(complete_var)s` - The completion environment variable
54    fn source_template(&self) -> &str;
55
56    /// Get completion arguments from the shell environment.
57    ///
58    /// Parses the completion environment variables set by the shell's completion
59    /// system and returns the arguments to complete.
60    fn get_completion_args(&self) -> CompletionArgs;
61
62    /// Format a completion item for output to the shell.
63    ///
64    /// Each shell expects completions in a different format.
65    fn format_completion(&self, item: &CompletionItem) -> String;
66
67    /// Get the source script with placeholders replaced.
68    fn get_source(&self, prog_name: &str, complete_var: &str) -> String {
69        let complete_func = format!("_{}_completion", prog_name.replace('-', "_"));
70        self.source_template()
71            .replace("%(prog_name)s", prog_name)
72            .replace("%(complete_func)s", &complete_func)
73            .replace("%(complete_var)s", complete_var)
74    }
75}
76
77/// Arguments parsed from the shell completion environment.
78#[derive(Debug, Clone)]
79pub struct CompletionArgs {
80    /// The arguments up to the cursor position.
81    pub args: Vec<String>,
82    /// The incomplete word being typed.
83    pub incomplete: String,
84}
85
86impl Default for CompletionArgs {
87    fn default() -> Self {
88        Self {
89            args: Vec::new(),
90            incomplete: String::new(),
91        }
92    }
93}
94
95// =============================================================================
96// BashComplete
97// =============================================================================
98
99/// Bash shell completion implementation.
100///
101/// Uses `COMP_WORDS` and `COMP_CWORD` environment variables set by bash's
102/// completion system.
103#[derive(Debug, Clone, Default)]
104pub struct BashComplete;
105
106impl BashComplete {
107    /// The bash completion script template.
108    const SOURCE_TEMPLATE: &'static str = r#"
109%(complete_func)s() {
110    local IFS=$'\n'
111    COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \
112                   COMP_CWORD=$COMP_CWORD \
113                   %(complete_var)s=bash_complete \
114                   %(prog_name)s ) )
115    return 0
116}
117
118%(complete_func)s_setup() {
119    complete -o default -F %(complete_func)s %(prog_name)s
120}
121
122%(complete_func)s_setup
123"#;
124}
125
126impl ShellComplete for BashComplete {
127    fn name(&self) -> &str {
128        "bash"
129    }
130
131    fn source_template(&self) -> &str {
132        Self::SOURCE_TEMPLATE
133    }
134
135    fn get_completion_args(&self) -> CompletionArgs {
136        // Match Python Click: parse COMP_WORDS with shell-like splitting.
137        let comp_words = env::var("COMP_WORDS").unwrap_or_default();
138        let comp_cword: usize = env::var("COMP_CWORD")
139            .ok()
140            .and_then(|s| s.parse().ok())
141            .unwrap_or(0);
142
143        let cwords = split_arg_string(&comp_words);
144
145        let args: Vec<String> = cwords
146            .iter()
147            .skip(1)
148            .take(comp_cword.saturating_sub(1))
149            .cloned()
150            .collect();
151
152        let incomplete = cwords.get(comp_cword).cloned().unwrap_or_default();
153
154        CompletionArgs { args, incomplete }
155    }
156
157    fn format_completion(&self, item: &CompletionItem) -> String {
158        // Match Python Click: "type,value"
159        format!("{},{}", item.completion_type, item.value)
160    }
161}
162
163// =============================================================================
164// ZshComplete
165// =============================================================================
166
167/// Zsh shell completion implementation.
168///
169/// Uses the `_arguments` style completion with `COMP_WORDS` and `COMP_CWORD`.
170#[derive(Debug, Clone, Default)]
171pub struct ZshComplete;
172
173impl ZshComplete {
174    /// The zsh completion script template.
175    const SOURCE_TEMPLATE: &'static str = r#"
176#compdef %(prog_name)s
177
178%(complete_func)s() {
179    local -a completions
180    local -a completions_with_descriptions
181    local -a response
182    (( ! $+commands[%(prog_name)s] )) && return 1
183
184    response=("${(@f)$(env COMP_WORDS="${words[*]}" COMP_CWORD=$((CURRENT-1)) %(complete_var)s=zsh_complete %(prog_name)s)}")
185
186    for key descr in ${(kv)response}; do
187        if [[ "$descr" == "_" ]]; then
188            completions+=("$key")
189        else
190            completions_with_descriptions+=("$key":"$descr")
191        fi
192    done
193
194    if [ -n "$completions_with_descriptions" ]; then
195        _describe -V unsorted completions_with_descriptions -U
196    fi
197
198    if [ -n "$completions" ]; then
199        compadd -U -V unsorted -a completions
200    fi
201}
202
203if [[ $zsh_eval_context[-1] == loadautofun ]]; then
204    %(complete_func)s "$@"
205else
206    compdef %(complete_func)s %(prog_name)s
207fi
208"#;
209}
210
211impl ShellComplete for ZshComplete {
212    fn name(&self) -> &str {
213        "zsh"
214    }
215
216    fn source_template(&self) -> &str {
217        Self::SOURCE_TEMPLATE
218    }
219
220    fn get_completion_args(&self) -> CompletionArgs {
221        // Match Python Click: parse COMP_WORDS with shell-like splitting.
222        let comp_words = env::var("COMP_WORDS").unwrap_or_default();
223        let comp_cword: usize = env::var("COMP_CWORD")
224            .ok()
225            .and_then(|s| s.parse().ok())
226            .unwrap_or(0);
227
228        let cwords = split_arg_string(&comp_words);
229
230        let args: Vec<String> = cwords
231            .iter()
232            .skip(1)
233            .take(comp_cword.saturating_sub(1))
234            .cloned()
235            .collect();
236
237        let incomplete = cwords.get(comp_cword).cloned().unwrap_or_default();
238
239        CompletionArgs { args, incomplete }
240    }
241
242    fn format_completion(&self, item: &CompletionItem) -> String {
243        // Match Python Click:
244        // - help is "_" when absent
245        // - escape ":" in value iff help != "_"
246        let help = item
247            .help
248            .as_deref()
249            .filter(|h| !h.is_empty())
250            .unwrap_or("_");
251        let value = if help != "_" {
252            item.value.replace(':', "\\:")
253        } else {
254            item.value.clone()
255        };
256        format!("{}\n{}\n{}", item.completion_type, value, help)
257    }
258}
259
260// =============================================================================
261// FishComplete
262// =============================================================================
263
264/// Fish shell completion implementation.
265///
266/// Uses Fish's native completion system with `complete` command.
267#[derive(Debug, Clone, Default)]
268pub struct FishComplete;
269
270impl FishComplete {
271    /// The fish completion script template.
272    const SOURCE_TEMPLATE: &'static str = r#"
273function %(complete_func)s
274    set -l response (env %(complete_var)s=fish_complete COMP_WORDS=(commandline -cp) COMP_CWORD=(commandline -t) %(prog_name)s)
275
276    for completion in $response
277        set -l metadata (string split "," -- $completion)
278
279        if [ $metadata[1] = "dir" ]
280            __fish_complete_directories $metadata[2]
281        else if [ $metadata[1] = "file" ]
282            __fish_complete_path $metadata[2]
283        else if [ $metadata[1] = "plain" ]
284            echo $metadata[2]
285        end
286    end
287end
288
289complete -c %(prog_name)s -f -a "(%(complete_func)s)"
290"#;
291}
292
293impl ShellComplete for FishComplete {
294    fn name(&self) -> &str {
295        "fish"
296    }
297
298    fn source_template(&self) -> &str {
299        Self::SOURCE_TEMPLATE
300    }
301
302    fn get_completion_args(&self) -> CompletionArgs {
303        // Match Python Click:
304        // - COMP_WORDS is split using shell-like rules
305        // - COMP_CWORD contains the incomplete word (not an index)
306        // - remove incomplete from args if it appears as the last token
307        let comp_words = env::var("COMP_WORDS").unwrap_or_default();
308        let mut incomplete = env::var("COMP_CWORD").unwrap_or_default();
309        if !incomplete.is_empty() {
310            incomplete = split_arg_string(&incomplete)
311                .into_iter()
312                .next()
313                .unwrap_or_default();
314        }
315
316        let mut args: Vec<String> = split_arg_string(&comp_words).into_iter().skip(1).collect();
317        if !incomplete.is_empty() && args.last().is_some_and(|a| a == &incomplete) {
318            args.pop();
319        }
320
321        CompletionArgs { args, incomplete }
322    }
323
324    fn format_completion(&self, item: &CompletionItem) -> String {
325        // Match Python Click:
326        // - "type,value\\thelp" when help exists
327        // - otherwise "type,value"
328        if let Some(help) = item.help.as_deref().filter(|h| !h.is_empty()) {
329            format!("{},{}\t{}", item.completion_type, item.value, help)
330        } else {
331            format!("{},{}", item.completion_type, item.value)
332        }
333    }
334}
335
336// =============================================================================
337// Shell Registry
338// =============================================================================
339
340/// Get a shell completion implementation by name.
341///
342/// # Arguments
343///
344/// * `shell` - The shell name ("bash", "zsh", or "fish")
345///
346/// # Returns
347///
348/// Returns `Some(Box<dyn ShellComplete>)` if the shell is supported, `None` otherwise.
349///
350/// # Example
351///
352/// ```rust
353/// use click::completion::get_completion_class;
354///
355/// if let Some(completer) = get_completion_class("bash") {
356///     println!("Shell: {}", completer.name());
357/// }
358/// ```
359pub fn get_completion_class(shell: &str) -> Option<Box<dyn ShellComplete>> {
360    match shell.to_lowercase().as_str() {
361        "bash" => Some(Box::new(BashComplete)),
362        "zsh" => Some(Box::new(ZshComplete)),
363        "fish" => Some(Box::new(FishComplete)),
364        _ => None,
365    }
366}
367
368/// Detect the current shell from environment.
369///
370/// Checks `SHELL` environment variable and tries to determine the shell type.
371///
372/// # Returns
373///
374/// Returns the shell name if detected, or `None` if unknown.
375pub fn detect_shell() -> Option<String> {
376    env::var("SHELL").ok().and_then(|shell| {
377        let shell_name = shell.rsplit('/').next()?;
378        match shell_name {
379            "bash" => Some("bash".to_string()),
380            "zsh" => Some("zsh".to_string()),
381            "fish" => Some("fish".to_string()),
382            _ => None,
383        }
384    })
385}
386
387/// List all supported shell names.
388pub fn list_shells() -> Vec<&'static str> {
389    vec!["bash", "zsh", "fish"]
390}
391
392// =============================================================================
393// Completion Functions
394// =============================================================================
395
396/// Main shell completion entry point.
397///
398/// This function should be called when the completion environment variable is set.
399/// It parses the completion arguments, generates completions, and outputs them
400/// in the appropriate format for the shell.
401///
402/// # Arguments
403///
404/// * `cmd` - The root command to complete for
405/// * `prog_name` - The program name
406/// * `complete_var` - The environment variable name that triggers completion
407///
408/// # Example
409///
410/// ```no_run
411/// use click::completion::shell_complete;
412/// use click::command::Command;
413/// use std::env;
414///
415/// let cmd = Command::new("myapp").build();
416///
417/// // In your main(), check if completion is requested
418/// if let Ok(shell) = env::var("_MYAPP_COMPLETE") {
419///     shell_complete(&cmd, "myapp", "_MYAPP_COMPLETE");
420///     return;
421/// }
422/// ```
423pub fn shell_complete(cmd: &dyn CommandLike, prog_name: &str, complete_var: &str) {
424    // Get the shell type from the completion variable
425    let shell_type = match env::var(complete_var) {
426        Ok(val) => {
427            // The value is typically "bash_complete", "zsh_complete", etc.
428            val.split('_').next().unwrap_or("bash").to_string()
429        }
430        Err(_) => return,
431    };
432
433    let completer = match get_completion_class(&shell_type) {
434        Some(c) => c,
435        None => return,
436    };
437
438    // Get completion arguments from environment
439    let comp_args = completer.get_completion_args();
440
441    // Generate completions
442    let completions = get_completions(cmd, prog_name, &comp_args.args, &comp_args.incomplete);
443
444    // Output completions in shell-specific format
445    let stdout = io::stdout();
446    let mut handle = stdout.lock();
447    for item in completions {
448        let _ = writeln!(handle, "{}", completer.format_completion(&item));
449    }
450}
451
452/// Get completions for a command.
453///
454/// This is the internal completion generation function that can be used
455/// for testing or custom completion implementations.
456///
457/// # Arguments
458///
459/// * `cmd` - The command to complete for
460/// * `prog_name` - The program name
461/// * `args` - The arguments entered so far
462/// * `incomplete` - The incomplete word being typed
463///
464/// # Returns
465///
466/// A vector of completion items.
467pub fn get_completions(
468    cmd: &dyn CommandLike,
469    prog_name: &str,
470    args: &[String],
471    incomplete: &str,
472) -> Vec<CompletionItem> {
473    let mut completions = Vec::new();
474
475    // Create a resilient parsing context
476    let ctx = ContextBuilder::new()
477        .info_name(prog_name)
478        .resilient_parsing(true)
479        .build();
480
481    // Check if we're completing a subcommand
482    if let Some(group) = cmd.as_any().downcast_ref::<Group>() {
483        // First check if the first arg is a valid subcommand - if so, recurse into it
484        if !args.is_empty() {
485            if let Some(subcmd) = group.get_command(&args[0]) {
486                let remaining_args: Vec<String> = args[1..].to_vec();
487                return get_completions(subcmd, &args[0], &remaining_args, incomplete);
488            }
489        }
490
491        // Otherwise, list subcommand completions
492        for name in group.list_commands() {
493            if name.starts_with(incomplete) {
494                let subcmd = group.get_command(name);
495                let help = subcmd.map(|c| c.get_short_help());
496                let mut item = CompletionItem::new(name);
497                if let Some(h) = help {
498                    if !h.is_empty() {
499                        item = item.with_help(h);
500                    }
501                }
502                completions.push(item);
503            }
504        }
505    }
506    if let Some(collection) = cmd.as_any().downcast_ref::<CommandCollection>() {
507        if !args.is_empty() {
508            if let Some(subcmd) = collection.get_command(&args[0]) {
509                let remaining_args: Vec<String> = args[1..].to_vec();
510                return get_completions(subcmd, &args[0], &remaining_args, incomplete);
511            }
512        }
513
514        for name in collection.list_commands() {
515            if name.starts_with(incomplete) {
516                let subcmd = collection.get_command(&name);
517                let help = subcmd.map(|c| c.get_short_help());
518                let mut item = CompletionItem::new(&name);
519                if let Some(h) = help {
520                    if !h.is_empty() {
521                        item = item.with_help(h);
522                    }
523                }
524                completions.push(item);
525            }
526        }
527    }
528
529    // Complete options
530    if incomplete.starts_with('-') || completions.is_empty() {
531        if let Some(command) = cmd.as_any().downcast_ref::<Command>() {
532            completions.extend(get_option_completions(command, &ctx, args, incomplete));
533        } else if let Some(group) = cmd.as_any().downcast_ref::<Group>() {
534            completions.extend(get_option_completions(
535                &group.command,
536                &ctx,
537                args,
538                incomplete,
539            ));
540        } else if let Some(collection) = cmd.as_any().downcast_ref::<CommandCollection>() {
541            completions.extend(get_option_completions(
542                &collection.base.command,
543                &ctx,
544                args,
545                incomplete,
546            ));
547        }
548    }
549
550    // Complete arguments (if not completing an option)
551    if !incomplete.starts_with('-') {
552        if let Some(command) = cmd.as_any().downcast_ref::<Command>() {
553            completions.extend(get_argument_completions(command, &ctx, args, incomplete));
554        } else if let Some(group) = cmd.as_any().downcast_ref::<Group>() {
555            completions.extend(get_argument_completions(
556                &group.command,
557                &ctx,
558                args,
559                incomplete,
560            ));
561        } else if let Some(collection) = cmd.as_any().downcast_ref::<CommandCollection>() {
562            completions.extend(get_argument_completions(
563                &collection.base.command,
564                &ctx,
565                args,
566                incomplete,
567            ));
568        }
569    }
570
571    completions
572}
573
574/// Get argument completions for a command.
575///
576/// Determines which argument is being completed based on the number of
577/// positional arguments already provided, then calls the argument's
578/// `get_completions` method.
579fn get_argument_completions(
580    cmd: &Command,
581    ctx: &crate::context::Context,
582    args: &[String],
583    incomplete: &str,
584) -> Vec<CompletionItem> {
585    // Count how many positional arguments have been consumed
586    // (arguments that don't start with '-' and aren't option values)
587    let positional_count = args.iter().filter(|a| !a.starts_with('-')).count();
588
589    // Find the argument at that position
590    if let Some(arg) = cmd.arguments.get(positional_count) {
591        return arg.get_completions(ctx, incomplete);
592    }
593
594    // If we have variadic arguments, the last argument can complete more values
595    if let Some(last_arg) = cmd.arguments.last() {
596        if last_arg.multiple() {
597            return last_arg.get_completions(ctx, incomplete);
598        }
599    }
600
601    Vec::new()
602}
603
604/// Get option completions for a command.
605fn get_option_completions(
606    cmd: &Command,
607    ctx: &crate::context::Context,
608    args: &[String],
609    incomplete: &str,
610) -> Vec<CompletionItem> {
611    // Completing an inline option value: --name=value
612    if let Some((opt_name, value_prefix)) = incomplete.split_once('=') {
613        if opt_name.starts_with("--") {
614            if let Some(opt) = cmd
615                .options
616                .iter()
617                .find(|o| o.long.iter().any(|long| long == opt_name) && option_accepts_value(o))
618            {
619                return opt
620                    .get_completions(ctx, value_prefix)
621                    .into_iter()
622                    .map(|item| {
623                        let mut with_prefix =
624                            CompletionItem::new(format!("{}={}", opt_name, item.value));
625                        if let Some(help) = item.help {
626                            with_prefix = with_prefix.with_help(help);
627                        }
628                        with_prefix
629                    })
630                    .collect();
631            }
632        }
633    }
634
635    // Completing an option value provided as the next token: --name <TAB>
636    if let Some(last_arg) = args.last() {
637        if let Some(opt) = cmd.options.iter().find(|o| {
638            option_accepts_value(o)
639                && (o.long.iter().any(|long| long == last_arg)
640                    || o.short.iter().any(|short| short == last_arg))
641        }) {
642            return opt.get_completions(ctx, incomplete);
643        }
644    }
645
646    let mut completions = Vec::new();
647
648    for opt in &cmd.options {
649        // Add long options
650        for long in &opt.long {
651            if long.starts_with(incomplete) {
652                let mut item = CompletionItem::new(long);
653                if let Some(help) = opt.help() {
654                    item = item.with_help(help.to_string());
655                }
656                completions.push(item);
657            }
658        }
659
660        // Add short options
661        for short in &opt.short {
662            if short.starts_with(incomplete) {
663                let mut item = CompletionItem::new(short);
664                if let Some(help) = opt.help() {
665                    item = item.with_help(help.to_string());
666                }
667                completions.push(item);
668            }
669        }
670    }
671
672    // Add help option completion (if enabled).
673    if let Some(help_opt) = cmd.get_help_option(ctx) {
674        for long in &help_opt.long {
675            if long.starts_with(incomplete) {
676                let mut item = CompletionItem::new(long);
677                if let Some(help) = help_opt.help() {
678                    if !help.is_empty() {
679                        item = item.with_help(help.to_string());
680                    }
681                }
682                completions.push(item);
683            }
684        }
685        for short in &help_opt.short {
686            if short.starts_with(incomplete) {
687                let mut item = CompletionItem::new(short);
688                if let Some(help) = help_opt.help() {
689                    if !help.is_empty() {
690                        item = item.with_help(help.to_string());
691                    }
692                }
693                completions.push(item);
694            }
695        }
696    }
697
698    completions
699}
700
701fn option_accepts_value(opt: &crate::option::ClickOption) -> bool {
702    !opt.is_flag && !opt.count
703}
704
705/// Add the shell completion option to a command.
706///
707/// This returns options that can be added to a command to enable
708/// shell completion script generation.
709///
710/// # Example
711///
712/// ```
713/// use click::completion::make_completion_option;
714///
715/// let opt = make_completion_option("_MYAPP_COMPLETE");
716/// assert!(!opt.is_completion_requested());
717/// ```
718pub fn make_completion_option(complete_var: &str) -> CompletionOption {
719    CompletionOption {
720        complete_var: complete_var.to_string(),
721    }
722}
723
724/// Configuration for the shell completion option.
725#[derive(Debug, Clone)]
726pub struct CompletionOption {
727    /// The environment variable that triggers completion.
728    pub complete_var: String,
729}
730
731impl CompletionOption {
732    /// Check if completion is requested via environment variable.
733    pub fn is_completion_requested(&self) -> bool {
734        env::var(&self.complete_var).is_ok()
735    }
736
737    /// Get the completion shell type if completion is requested.
738    pub fn get_completion_shell(&self) -> Option<String> {
739        env::var(&self.complete_var).ok().and_then(|val| {
740            // Values are like "bash_complete", "bash_source", etc.
741            let parts: Vec<&str> = val.split('_').collect();
742            if parts.len() >= 2 {
743                Some(parts[0].to_string())
744            } else {
745                None
746            }
747        })
748    }
749
750    /// Check if this is a "source" request (print the completion script).
751    pub fn is_source_request(&self) -> bool {
752        env::var(&self.complete_var)
753            .map(|v| v.ends_with("_source"))
754            .unwrap_or(false)
755    }
756
757    /// Handle completion if requested.
758    ///
759    /// Returns `true` if completion was handled and the program should exit.
760    pub fn handle_completion(&self, cmd: &dyn CommandLike, prog_name: &str) -> bool {
761        if !self.is_completion_requested() {
762            return false;
763        }
764
765        if let Some(shell) = self.get_completion_shell() {
766            if self.is_source_request() {
767                // Print the completion script
768                if let Some(completer) = get_completion_class(&shell) {
769                    println!("{}", completer.get_source(prog_name, &self.complete_var));
770                }
771            } else {
772                // Generate completions
773                shell_complete(cmd, prog_name, &self.complete_var);
774            }
775            return true;
776        }
777
778        false
779    }
780}
781
782// =============================================================================
783// Tests
784// =============================================================================
785
786#[cfg(test)]
787mod tests {
788    use super::*;
789
790    #[test]
791    fn test_get_completion_class() {
792        assert!(get_completion_class("bash").is_some());
793        assert!(get_completion_class("zsh").is_some());
794        assert!(get_completion_class("fish").is_some());
795        assert!(get_completion_class("unknown").is_none());
796
797        // Case insensitive
798        assert!(get_completion_class("BASH").is_some());
799        assert!(get_completion_class("Zsh").is_some());
800    }
801
802    #[test]
803    fn test_shell_names() {
804        let bash = BashComplete;
805        assert_eq!(bash.name(), "bash");
806
807        let zsh = ZshComplete;
808        assert_eq!(zsh.name(), "zsh");
809
810        let fish = FishComplete;
811        assert_eq!(fish.name(), "fish");
812    }
813
814    #[test]
815    fn test_list_shells() {
816        let shells = list_shells();
817        assert!(shells.contains(&"bash"));
818        assert!(shells.contains(&"zsh"));
819        assert!(shells.contains(&"fish"));
820    }
821
822    #[test]
823    fn test_bash_source_template() {
824        let bash = BashComplete;
825        let source = bash.get_source("myapp", "_MYAPP_COMPLETE");
826
827        assert!(source.contains("myapp"));
828        assert!(source.contains("_MYAPP_COMPLETE"));
829        assert!(source.contains("_myapp_completion"));
830        assert!(source.contains("COMP_WORDS"));
831        assert!(source.contains("COMP_CWORD"));
832    }
833
834    #[test]
835    fn test_zsh_source_template() {
836        let zsh = ZshComplete;
837        let source = zsh.get_source("myapp", "_MYAPP_COMPLETE");
838
839        assert!(source.contains("#compdef myapp"));
840        assert!(source.contains("_MYAPP_COMPLETE"));
841        assert!(source.contains("_myapp_completion"));
842    }
843
844    #[test]
845    fn test_fish_source_template() {
846        let fish = FishComplete;
847        let source = fish.get_source("myapp", "_MYAPP_COMPLETE");
848
849        assert!(source.contains("function _myapp_completion"));
850        assert!(source.contains("_MYAPP_COMPLETE"));
851        assert!(source.contains("complete -c myapp"));
852    }
853
854    #[test]
855    fn test_bash_format_completion() {
856        let bash = BashComplete;
857
858        let item = CompletionItem::new("--help");
859        assert_eq!(bash.format_completion(&item), "plain,--help");
860
861        let item_with_help = CompletionItem::new("--name").with_help("Specify name");
862        assert_eq!(bash.format_completion(&item_with_help), "plain,--name");
863    }
864
865    #[test]
866    fn test_zsh_format_completion() {
867        let zsh = ZshComplete;
868
869        let item = CompletionItem::new("--help");
870        assert_eq!(zsh.format_completion(&item), "plain\n--help\n_");
871
872        let item_with_help = CompletionItem::new("--name").with_help("Specify name");
873        assert_eq!(
874            zsh.format_completion(&item_with_help),
875            "plain\n--name\nSpecify name"
876        );
877    }
878
879    #[test]
880    fn test_fish_format_completion() {
881        let fish = FishComplete;
882
883        let item = CompletionItem::new("--help");
884        assert_eq!(fish.format_completion(&item), "plain,--help");
885
886        let item_file = CompletionItem::with_type("path", "file");
887        assert_eq!(fish.format_completion(&item_file), "file,path");
888    }
889
890    #[test]
891    fn test_completion_args_default() {
892        let args = CompletionArgs::default();
893        assert!(args.args.is_empty());
894        assert!(args.incomplete.is_empty());
895    }
896
897    #[test]
898    fn test_get_completions_empty() {
899        let cmd = Command::new("test").build();
900        let completions = get_completions(&cmd, "test", &[], "");
901
902        // Should at least have --help
903        assert!(completions.iter().any(|c| c.value == "--help"));
904    }
905
906    #[test]
907    fn test_get_completions_options() {
908        let cmd = Command::new("test")
909            .option(
910                crate::option::ClickOption::new(&["--name", "-n"])
911                    .help("The name")
912                    .build(),
913            )
914            .build();
915
916        let completions = get_completions(&cmd, "test", &[], "--");
917
918        assert!(completions.iter().any(|c| c.value == "--name"));
919        assert!(completions.iter().any(|c| c.value == "--help"));
920    }
921
922    #[test]
923    fn test_get_completions_subcommands() {
924        let group = Group::new("cli")
925            .command(Command::new("init").help("Initialize").build())
926            .command(Command::new("build").help("Build").build())
927            .build();
928
929        let completions = get_completions(&group, "cli", &[], "");
930
931        assert!(completions.iter().any(|c| c.value == "init"));
932        assert!(completions.iter().any(|c| c.value == "build"));
933    }
934
935    #[test]
936    fn test_get_completions_subcommand_prefix() {
937        let group = Group::new("cli")
938            .command(Command::new("init").build())
939            .command(Command::new("install").build())
940            .command(Command::new("build").build())
941            .build();
942
943        let completions = get_completions(&group, "cli", &[], "in");
944
945        assert!(completions.iter().any(|c| c.value == "init"));
946        assert!(completions.iter().any(|c| c.value == "install"));
947        assert!(!completions.iter().any(|c| c.value == "build"));
948    }
949
950    #[test]
951    fn test_completion_option() {
952        let opt = make_completion_option("_TEST_COMPLETE");
953        assert_eq!(opt.complete_var, "_TEST_COMPLETE");
954    }
955
956    #[test]
957    fn test_completion_option_not_requested() {
958        // Clear any existing env var
959        env::remove_var("_TEST_COMPLETE");
960
961        let opt = make_completion_option("_TEST_COMPLETE");
962        assert!(!opt.is_completion_requested());
963        assert!(opt.get_completion_shell().is_none());
964        assert!(!opt.is_source_request());
965    }
966
967    #[test]
968    fn test_prog_name_with_dash() {
969        let bash = BashComplete;
970        let source = bash.get_source("my-app", "_MY_APP_COMPLETE");
971
972        // Function name should have underscore, not dash
973        assert!(source.contains("_my_app_completion"));
974    }
975
976    // =========================================================================
977    // Tests for argument completions
978    // =========================================================================
979
980    #[test]
981    fn test_get_completions_argument_with_choice_type() {
982        use crate::argument::Argument;
983        use crate::types::Choice;
984
985        let cmd = Command::new("test")
986            .argument(
987                Argument::new("format")
988                    .type_(Choice::new(["json", "xml", "yaml"]))
989                    .build(),
990            )
991            .build();
992
993        let completions = get_completions(&cmd, "test", &[], "j");
994
995        assert_eq!(completions.len(), 1);
996        assert_eq!(completions[0].value, "json");
997    }
998
999    #[test]
1000    fn test_get_completions_argument_with_custom_callback() {
1001        use crate::argument::Argument;
1002
1003        let cmd = Command::new("test")
1004            .argument(
1005                Argument::new("filename")
1006                    .shell_complete(|_ctx, incomplete| {
1007                        vec![
1008                            CompletionItem::new(format!("{}.txt", incomplete)),
1009                            CompletionItem::new(format!("{}.md", incomplete)),
1010                        ]
1011                    })
1012                    .build(),
1013            )
1014            .build();
1015
1016        let completions = get_completions(&cmd, "test", &[], "file");
1017
1018        assert_eq!(completions.len(), 2);
1019        assert!(completions.iter().any(|c| c.value == "file.txt"));
1020        assert!(completions.iter().any(|c| c.value == "file.md"));
1021    }
1022
1023    #[test]
1024    fn test_get_completions_second_argument() {
1025        use crate::argument::Argument;
1026        use crate::types::Choice;
1027
1028        let cmd = Command::new("test")
1029            .argument(Argument::new("first").build())
1030            .argument(
1031                Argument::new("second")
1032                    .type_(Choice::new(["a", "b", "c"]))
1033                    .build(),
1034            )
1035            .build();
1036
1037        // First argument has no completions (STRING type)
1038        let completions = get_completions(&cmd, "test", &[], "x");
1039        assert!(completions.is_empty());
1040
1041        // Second argument should complete after first is provided
1042        let completions = get_completions(&cmd, "test", &["value1".to_string()], "a");
1043        assert_eq!(completions.len(), 1);
1044        assert_eq!(completions[0].value, "a");
1045    }
1046
1047    #[test]
1048    fn test_get_completions_variadic_argument() {
1049        use crate::argument::Argument;
1050        use crate::types::Choice;
1051
1052        let cmd = Command::new("test")
1053            .argument(
1054                Argument::new("files")
1055                    .multiple()
1056                    .type_(Choice::new(["foo.txt", "bar.txt", "baz.txt"]))
1057                    .build(),
1058            )
1059            .build();
1060
1061        // First value
1062        let completions = get_completions(&cmd, "test", &[], "f");
1063        assert_eq!(completions.len(), 1);
1064        assert_eq!(completions[0].value, "foo.txt");
1065
1066        // Second value (variadic continues to complete)
1067        let completions = get_completions(&cmd, "test", &["foo.txt".to_string()], "b");
1068        assert_eq!(completions.len(), 2);
1069        assert!(completions.iter().any(|c| c.value == "bar.txt"));
1070        assert!(completions.iter().any(|c| c.value == "baz.txt"));
1071    }
1072
1073    #[test]
1074    fn test_get_completions_no_more_arguments() {
1075        use crate::argument::Argument;
1076
1077        let cmd = Command::new("test")
1078            .argument(Argument::new("single").build())
1079            .build();
1080
1081        // After the single argument is provided, no more completions
1082        let completions = get_completions(&cmd, "test", &["value".to_string()], "x");
1083        // Should be empty (no more arguments to complete)
1084        // Note: options might still complete if incomplete doesn't start with '-'
1085        assert!(completions.is_empty());
1086    }
1087}