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.help.as_deref().filter(|h| !h.is_empty()).unwrap_or("_");
247        let value = if help != "_" {
248            item.value.replace(':', "\\:")
249        } else {
250            item.value.clone()
251        };
252        format!("{}\n{}\n{}", item.completion_type, value, help)
253    }
254}
255
256// =============================================================================
257// FishComplete
258// =============================================================================
259
260/// Fish shell completion implementation.
261///
262/// Uses Fish's native completion system with `complete` command.
263#[derive(Debug, Clone, Default)]
264pub struct FishComplete;
265
266impl FishComplete {
267    /// The fish completion script template.
268    const SOURCE_TEMPLATE: &'static str = r#"
269function %(complete_func)s
270    set -l response (env %(complete_var)s=fish_complete COMP_WORDS=(commandline -cp) COMP_CWORD=(commandline -t) %(prog_name)s)
271
272    for completion in $response
273        set -l metadata (string split "," -- $completion)
274
275        if [ $metadata[1] = "dir" ]
276            __fish_complete_directories $metadata[2]
277        else if [ $metadata[1] = "file" ]
278            __fish_complete_path $metadata[2]
279        else if [ $metadata[1] = "plain" ]
280            echo $metadata[2]
281        end
282    end
283end
284
285complete -c %(prog_name)s -f -a "(%(complete_func)s)"
286"#;
287}
288
289impl ShellComplete for FishComplete {
290    fn name(&self) -> &str {
291        "fish"
292    }
293
294    fn source_template(&self) -> &str {
295        Self::SOURCE_TEMPLATE
296    }
297
298    fn get_completion_args(&self) -> CompletionArgs {
299        // Match Python Click:
300        // - COMP_WORDS is split using shell-like rules
301        // - COMP_CWORD contains the incomplete word (not an index)
302        // - remove incomplete from args if it appears as the last token
303        let comp_words = env::var("COMP_WORDS").unwrap_or_default();
304        let mut incomplete = env::var("COMP_CWORD").unwrap_or_default();
305        if !incomplete.is_empty() {
306            incomplete = split_arg_string(&incomplete)
307                .into_iter()
308                .next()
309                .unwrap_or_default();
310        }
311
312        let mut args: Vec<String> = split_arg_string(&comp_words).into_iter().skip(1).collect();
313        if !incomplete.is_empty() && args.last().is_some_and(|a| a == &incomplete) {
314            args.pop();
315        }
316
317        CompletionArgs { args, incomplete }
318    }
319
320    fn format_completion(&self, item: &CompletionItem) -> String {
321        // Match Python Click:
322        // - "type,value\\thelp" when help exists
323        // - otherwise "type,value"
324        if let Some(help) = item.help.as_deref().filter(|h| !h.is_empty()) {
325            format!("{},{}\t{}", item.completion_type, item.value, help)
326        } else {
327            format!("{},{}", item.completion_type, item.value)
328        }
329    }
330}
331
332// =============================================================================
333// Shell Registry
334// =============================================================================
335
336/// Get a shell completion implementation by name.
337///
338/// # Arguments
339///
340/// * `shell` - The shell name ("bash", "zsh", or "fish")
341///
342/// # Returns
343///
344/// Returns `Some(Box<dyn ShellComplete>)` if the shell is supported, `None` otherwise.
345///
346/// # Example
347///
348/// ```rust
349/// use click::completion::get_completion_class;
350///
351/// if let Some(completer) = get_completion_class("bash") {
352///     println!("Shell: {}", completer.name());
353/// }
354/// ```
355pub fn get_completion_class(shell: &str) -> Option<Box<dyn ShellComplete>> {
356    match shell.to_lowercase().as_str() {
357        "bash" => Some(Box::new(BashComplete)),
358        "zsh" => Some(Box::new(ZshComplete)),
359        "fish" => Some(Box::new(FishComplete)),
360        _ => None,
361    }
362}
363
364/// Detect the current shell from environment.
365///
366/// Checks `SHELL` environment variable and tries to determine the shell type.
367///
368/// # Returns
369///
370/// Returns the shell name if detected, or `None` if unknown.
371pub fn detect_shell() -> Option<String> {
372    env::var("SHELL").ok().and_then(|shell| {
373        let shell_name = shell.rsplit('/').next()?;
374        match shell_name {
375            "bash" => Some("bash".to_string()),
376            "zsh" => Some("zsh".to_string()),
377            "fish" => Some("fish".to_string()),
378            _ => None,
379        }
380    })
381}
382
383/// List all supported shell names.
384pub fn list_shells() -> Vec<&'static str> {
385    vec!["bash", "zsh", "fish"]
386}
387
388// =============================================================================
389// Completion Functions
390// =============================================================================
391
392/// Main shell completion entry point.
393///
394/// This function should be called when the completion environment variable is set.
395/// It parses the completion arguments, generates completions, and outputs them
396/// in the appropriate format for the shell.
397///
398/// # Arguments
399///
400/// * `cmd` - The root command to complete for
401/// * `prog_name` - The program name
402/// * `complete_var` - The environment variable name that triggers completion
403///
404/// # Example
405///
406/// ```no_run
407/// use click::completion::shell_complete;
408/// use click::command::Command;
409/// use std::env;
410///
411/// let cmd = Command::new("myapp").build();
412///
413/// // In your main(), check if completion is requested
414/// if let Ok(shell) = env::var("_MYAPP_COMPLETE") {
415///     shell_complete(&cmd, "myapp", "_MYAPP_COMPLETE");
416///     return;
417/// }
418/// ```
419pub fn shell_complete(cmd: &dyn CommandLike, prog_name: &str, complete_var: &str) {
420    // Get the shell type from the completion variable
421    let shell_type = match env::var(complete_var) {
422        Ok(val) => {
423            // The value is typically "bash_complete", "zsh_complete", etc.
424            val.split('_').next().unwrap_or("bash").to_string()
425        }
426        Err(_) => return,
427    };
428
429    let completer = match get_completion_class(&shell_type) {
430        Some(c) => c,
431        None => return,
432    };
433
434    // Get completion arguments from environment
435    let comp_args = completer.get_completion_args();
436
437    // Generate completions
438    let completions = get_completions(cmd, prog_name, &comp_args.args, &comp_args.incomplete);
439
440    // Output completions in shell-specific format
441    let stdout = io::stdout();
442    let mut handle = stdout.lock();
443    for item in completions {
444        let _ = writeln!(handle, "{}", completer.format_completion(&item));
445    }
446}
447
448/// Get completions for a command.
449///
450/// This is the internal completion generation function that can be used
451/// for testing or custom completion implementations.
452///
453/// # Arguments
454///
455/// * `cmd` - The command to complete for
456/// * `prog_name` - The program name
457/// * `args` - The arguments entered so far
458/// * `incomplete` - The incomplete word being typed
459///
460/// # Returns
461///
462/// A vector of completion items.
463pub fn get_completions(
464    cmd: &dyn CommandLike,
465    prog_name: &str,
466    args: &[String],
467    incomplete: &str,
468) -> Vec<CompletionItem> {
469    let mut completions = Vec::new();
470
471    // Create a resilient parsing context
472    let ctx = ContextBuilder::new()
473        .info_name(prog_name)
474        .resilient_parsing(true)
475        .build();
476
477    // Check if we're completing a subcommand
478    if let Some(group) = cmd.as_any().downcast_ref::<Group>() {
479        // First check if the first arg is a valid subcommand - if so, recurse into it
480        if !args.is_empty() {
481            if let Some(subcmd) = group.get_command(&args[0]) {
482                let remaining_args: Vec<String> = args[1..].to_vec();
483                return get_completions(subcmd, &args[0], &remaining_args, incomplete);
484            }
485        }
486
487        // Otherwise, list subcommand completions
488        for name in group.list_commands() {
489            if name.starts_with(incomplete) {
490                let subcmd = group.get_command(name);
491                let help = subcmd.map(|c| c.get_short_help());
492                let mut item = CompletionItem::new(name);
493                if let Some(h) = help {
494                    if !h.is_empty() {
495                        item = item.with_help(h);
496                    }
497                }
498                completions.push(item);
499            }
500        }
501    }
502    if let Some(collection) = cmd.as_any().downcast_ref::<CommandCollection>() {
503        if !args.is_empty() {
504            if let Some(subcmd) = collection.get_command(&args[0]) {
505                let remaining_args: Vec<String> = args[1..].to_vec();
506                return get_completions(subcmd, &args[0], &remaining_args, incomplete);
507            }
508        }
509
510        for name in collection.list_commands() {
511            if name.starts_with(incomplete) {
512                let subcmd = collection.get_command(&name);
513                let help = subcmd.map(|c| c.get_short_help());
514                let mut item = CompletionItem::new(&name);
515                if let Some(h) = help {
516                    if !h.is_empty() {
517                        item = item.with_help(h);
518                    }
519                }
520                completions.push(item);
521            }
522        }
523    }
524
525    // Complete options
526    if incomplete.starts_with('-') || completions.is_empty() {
527        if let Some(command) = cmd.as_any().downcast_ref::<Command>() {
528            completions.extend(get_option_completions(command, &ctx, args, incomplete));
529        } else if let Some(group) = cmd.as_any().downcast_ref::<Group>() {
530            completions.extend(get_option_completions(&group.command, &ctx, args, incomplete));
531        } else if let Some(collection) = cmd.as_any().downcast_ref::<CommandCollection>() {
532            completions.extend(get_option_completions(
533                &collection.base.command,
534                &ctx,
535                args,
536                incomplete,
537            ));
538        }
539    }
540
541    // Complete arguments (if not completing an option)
542    if !incomplete.starts_with('-') {
543        if let Some(command) = cmd.as_any().downcast_ref::<Command>() {
544            completions.extend(get_argument_completions(command, &ctx, args, incomplete));
545        } else if let Some(group) = cmd.as_any().downcast_ref::<Group>() {
546            completions.extend(get_argument_completions(&group.command, &ctx, args, incomplete));
547        } else if let Some(collection) = cmd.as_any().downcast_ref::<CommandCollection>() {
548            completions.extend(get_argument_completions(&collection.base.command, &ctx, args, incomplete));
549        }
550    }
551
552    completions
553}
554
555/// Get argument completions for a command.
556///
557/// Determines which argument is being completed based on the number of
558/// positional arguments already provided, then calls the argument's
559/// `get_completions` method.
560fn get_argument_completions(
561    cmd: &Command,
562    ctx: &crate::context::Context,
563    args: &[String],
564    incomplete: &str,
565) -> Vec<CompletionItem> {
566    // Count how many positional arguments have been consumed
567    // (arguments that don't start with '-' and aren't option values)
568    let positional_count = args
569        .iter()
570        .filter(|a| !a.starts_with('-'))
571        .count();
572
573    // Find the argument at that position
574    if let Some(arg) = cmd.arguments.get(positional_count) {
575        return arg.get_completions(ctx, incomplete);
576    }
577
578    // If we have variadic arguments, the last argument can complete more values
579    if let Some(last_arg) = cmd.arguments.last() {
580        if last_arg.multiple() {
581            return last_arg.get_completions(ctx, incomplete);
582        }
583    }
584
585    Vec::new()
586}
587
588/// Get option completions for a command.
589fn get_option_completions(
590    cmd: &Command,
591    ctx: &crate::context::Context,
592    args: &[String],
593    incomplete: &str,
594) -> Vec<CompletionItem> {
595    // Completing an inline option value: --name=value
596    if let Some((opt_name, value_prefix)) = incomplete.split_once('=') {
597        if opt_name.starts_with("--") {
598            if let Some(opt) = cmd
599                .options
600                .iter()
601                .find(|o| o.long.iter().any(|long| long == opt_name) && option_accepts_value(o))
602            {
603                return opt
604                    .get_completions(ctx, value_prefix)
605                    .into_iter()
606                    .map(|item| {
607                        let mut with_prefix = CompletionItem::new(format!(
608                            "{}={}",
609                            opt_name, item.value
610                        ));
611                        if let Some(help) = item.help {
612                            with_prefix = with_prefix.with_help(help);
613                        }
614                        with_prefix
615                    })
616                    .collect();
617            }
618        }
619    }
620
621    // Completing an option value provided as the next token: --name <TAB>
622    if let Some(last_arg) = args.last() {
623        if let Some(opt) = cmd.options.iter().find(|o| {
624            option_accepts_value(o)
625                && (o.long.iter().any(|long| long == last_arg)
626                    || o.short.iter().any(|short| short == last_arg))
627        }) {
628            return opt.get_completions(ctx, incomplete);
629        }
630    }
631
632    let mut completions = Vec::new();
633
634    for opt in &cmd.options {
635        // Add long options
636        for long in &opt.long {
637            if long.starts_with(incomplete) {
638                let mut item = CompletionItem::new(long);
639                if let Some(help) = opt.help() {
640                    item = item.with_help(help.to_string());
641                }
642                completions.push(item);
643            }
644        }
645
646        // Add short options
647        for short in &opt.short {
648            if short.starts_with(incomplete) {
649                let mut item = CompletionItem::new(short);
650                if let Some(help) = opt.help() {
651                    item = item.with_help(help.to_string());
652                }
653                completions.push(item);
654            }
655        }
656    }
657
658    // Add help option completion (if enabled).
659    if let Some(help_opt) = cmd.get_help_option(ctx) {
660        for long in &help_opt.long {
661            if long.starts_with(incomplete) {
662                let mut item = CompletionItem::new(long);
663                if let Some(help) = help_opt.help() {
664                    if !help.is_empty() {
665                        item = item.with_help(help.to_string());
666                    }
667                }
668                completions.push(item);
669            }
670        }
671        for short in &help_opt.short {
672            if short.starts_with(incomplete) {
673                let mut item = CompletionItem::new(short);
674                if let Some(help) = help_opt.help() {
675                    if !help.is_empty() {
676                        item = item.with_help(help.to_string());
677                    }
678                }
679                completions.push(item);
680            }
681        }
682    }
683
684    completions
685}
686
687fn option_accepts_value(opt: &crate::option::ClickOption) -> bool {
688    !opt.is_flag && !opt.count
689}
690
691/// Add the shell completion option to a command.
692///
693/// This returns options that can be added to a command to enable
694/// shell completion script generation.
695///
696/// # Example
697///
698/// ```
699/// use click::completion::make_completion_option;
700///
701/// let opt = make_completion_option("_MYAPP_COMPLETE");
702/// assert!(!opt.is_completion_requested());
703/// ```
704pub fn make_completion_option(complete_var: &str) -> CompletionOption {
705    CompletionOption {
706        complete_var: complete_var.to_string(),
707    }
708}
709
710/// Configuration for the shell completion option.
711#[derive(Debug, Clone)]
712pub struct CompletionOption {
713    /// The environment variable that triggers completion.
714    pub complete_var: String,
715}
716
717impl CompletionOption {
718    /// Check if completion is requested via environment variable.
719    pub fn is_completion_requested(&self) -> bool {
720        env::var(&self.complete_var).is_ok()
721    }
722
723    /// Get the completion shell type if completion is requested.
724    pub fn get_completion_shell(&self) -> Option<String> {
725        env::var(&self.complete_var).ok().and_then(|val| {
726            // Values are like "bash_complete", "bash_source", etc.
727            let parts: Vec<&str> = val.split('_').collect();
728            if parts.len() >= 2 {
729                Some(parts[0].to_string())
730            } else {
731                None
732            }
733        })
734    }
735
736    /// Check if this is a "source" request (print the completion script).
737    pub fn is_source_request(&self) -> bool {
738        env::var(&self.complete_var)
739            .map(|v| v.ends_with("_source"))
740            .unwrap_or(false)
741    }
742
743    /// Handle completion if requested.
744    ///
745    /// Returns `true` if completion was handled and the program should exit.
746    pub fn handle_completion(
747        &self,
748        cmd: &dyn CommandLike,
749        prog_name: &str,
750    ) -> bool {
751        if !self.is_completion_requested() {
752            return false;
753        }
754
755        if let Some(shell) = self.get_completion_shell() {
756            if self.is_source_request() {
757                // Print the completion script
758                if let Some(completer) = get_completion_class(&shell) {
759                    println!("{}", completer.get_source(prog_name, &self.complete_var));
760                }
761            } else {
762                // Generate completions
763                shell_complete(cmd, prog_name, &self.complete_var);
764            }
765            return true;
766        }
767
768        false
769    }
770}
771
772// =============================================================================
773// Tests
774// =============================================================================
775
776#[cfg(test)]
777mod tests {
778    use super::*;
779
780    #[test]
781    fn test_get_completion_class() {
782        assert!(get_completion_class("bash").is_some());
783        assert!(get_completion_class("zsh").is_some());
784        assert!(get_completion_class("fish").is_some());
785        assert!(get_completion_class("unknown").is_none());
786
787        // Case insensitive
788        assert!(get_completion_class("BASH").is_some());
789        assert!(get_completion_class("Zsh").is_some());
790    }
791
792    #[test]
793    fn test_shell_names() {
794        let bash = BashComplete;
795        assert_eq!(bash.name(), "bash");
796
797        let zsh = ZshComplete;
798        assert_eq!(zsh.name(), "zsh");
799
800        let fish = FishComplete;
801        assert_eq!(fish.name(), "fish");
802    }
803
804    #[test]
805    fn test_list_shells() {
806        let shells = list_shells();
807        assert!(shells.contains(&"bash"));
808        assert!(shells.contains(&"zsh"));
809        assert!(shells.contains(&"fish"));
810    }
811
812    #[test]
813    fn test_bash_source_template() {
814        let bash = BashComplete;
815        let source = bash.get_source("myapp", "_MYAPP_COMPLETE");
816
817        assert!(source.contains("myapp"));
818        assert!(source.contains("_MYAPP_COMPLETE"));
819        assert!(source.contains("_myapp_completion"));
820        assert!(source.contains("COMP_WORDS"));
821        assert!(source.contains("COMP_CWORD"));
822    }
823
824    #[test]
825    fn test_zsh_source_template() {
826        let zsh = ZshComplete;
827        let source = zsh.get_source("myapp", "_MYAPP_COMPLETE");
828
829        assert!(source.contains("#compdef myapp"));
830        assert!(source.contains("_MYAPP_COMPLETE"));
831        assert!(source.contains("_myapp_completion"));
832    }
833
834    #[test]
835    fn test_fish_source_template() {
836        let fish = FishComplete;
837        let source = fish.get_source("myapp", "_MYAPP_COMPLETE");
838
839        assert!(source.contains("function _myapp_completion"));
840        assert!(source.contains("_MYAPP_COMPLETE"));
841        assert!(source.contains("complete -c myapp"));
842    }
843
844    #[test]
845    fn test_bash_format_completion() {
846        let bash = BashComplete;
847
848        let item = CompletionItem::new("--help");
849        assert_eq!(bash.format_completion(&item), "plain,--help");
850
851        let item_with_help = CompletionItem::new("--name").with_help("Specify name");
852        assert_eq!(bash.format_completion(&item_with_help), "plain,--name");
853    }
854
855    #[test]
856    fn test_zsh_format_completion() {
857        let zsh = ZshComplete;
858
859        let item = CompletionItem::new("--help");
860        assert_eq!(zsh.format_completion(&item), "plain\n--help\n_");
861
862        let item_with_help = CompletionItem::new("--name").with_help("Specify name");
863        assert_eq!(
864            zsh.format_completion(&item_with_help),
865            "plain\n--name\nSpecify name"
866        );
867    }
868
869    #[test]
870    fn test_fish_format_completion() {
871        let fish = FishComplete;
872
873        let item = CompletionItem::new("--help");
874        assert_eq!(fish.format_completion(&item), "plain,--help");
875
876        let item_file = CompletionItem::with_type("path", "file");
877        assert_eq!(fish.format_completion(&item_file), "file,path");
878    }
879
880    #[test]
881    fn test_completion_args_default() {
882        let args = CompletionArgs::default();
883        assert!(args.args.is_empty());
884        assert!(args.incomplete.is_empty());
885    }
886
887    #[test]
888    fn test_get_completions_empty() {
889        let cmd = Command::new("test").build();
890        let completions = get_completions(&cmd, "test", &[], "");
891
892        // Should at least have --help
893        assert!(completions.iter().any(|c| c.value == "--help"));
894    }
895
896    #[test]
897    fn test_get_completions_options() {
898        let cmd = Command::new("test")
899            .option(
900                crate::option::ClickOption::new(&["--name", "-n"])
901                    .help("The name")
902                    .build(),
903            )
904            .build();
905
906        let completions = get_completions(&cmd, "test", &[], "--");
907
908        assert!(completions.iter().any(|c| c.value == "--name"));
909        assert!(completions.iter().any(|c| c.value == "--help"));
910    }
911
912    #[test]
913    fn test_get_completions_subcommands() {
914        let group = Group::new("cli")
915            .command(Command::new("init").help("Initialize").build())
916            .command(Command::new("build").help("Build").build())
917            .build();
918
919        let completions = get_completions(&group, "cli", &[], "");
920
921        assert!(completions.iter().any(|c| c.value == "init"));
922        assert!(completions.iter().any(|c| c.value == "build"));
923    }
924
925    #[test]
926    fn test_get_completions_subcommand_prefix() {
927        let group = Group::new("cli")
928            .command(Command::new("init").build())
929            .command(Command::new("install").build())
930            .command(Command::new("build").build())
931            .build();
932
933        let completions = get_completions(&group, "cli", &[], "in");
934
935        assert!(completions.iter().any(|c| c.value == "init"));
936        assert!(completions.iter().any(|c| c.value == "install"));
937        assert!(!completions.iter().any(|c| c.value == "build"));
938    }
939
940    #[test]
941    fn test_completion_option() {
942        let opt = make_completion_option("_TEST_COMPLETE");
943        assert_eq!(opt.complete_var, "_TEST_COMPLETE");
944    }
945
946    #[test]
947    fn test_completion_option_not_requested() {
948        // Clear any existing env var
949        env::remove_var("_TEST_COMPLETE");
950
951        let opt = make_completion_option("_TEST_COMPLETE");
952        assert!(!opt.is_completion_requested());
953        assert!(opt.get_completion_shell().is_none());
954        assert!(!opt.is_source_request());
955    }
956
957    #[test]
958    fn test_prog_name_with_dash() {
959        let bash = BashComplete;
960        let source = bash.get_source("my-app", "_MY_APP_COMPLETE");
961
962        // Function name should have underscore, not dash
963        assert!(source.contains("_my_app_completion"));
964    }
965
966    // =========================================================================
967    // Tests for argument completions
968    // =========================================================================
969
970    #[test]
971    fn test_get_completions_argument_with_choice_type() {
972        use crate::argument::Argument;
973        use crate::types::Choice;
974
975        let cmd = Command::new("test")
976            .argument(
977                Argument::new("format")
978                    .type_(Choice::new(["json", "xml", "yaml"]))
979                    .build()
980            )
981            .build();
982
983        let completions = get_completions(&cmd, "test", &[], "j");
984
985        assert_eq!(completions.len(), 1);
986        assert_eq!(completions[0].value, "json");
987    }
988
989    #[test]
990    fn test_get_completions_argument_with_custom_callback() {
991        use crate::argument::Argument;
992
993        let cmd = Command::new("test")
994            .argument(
995                Argument::new("filename")
996                    .shell_complete(|_ctx, incomplete| {
997                        vec![
998                            CompletionItem::new(format!("{}.txt", incomplete)),
999                            CompletionItem::new(format!("{}.md", incomplete)),
1000                        ]
1001                    })
1002                    .build()
1003            )
1004            .build();
1005
1006        let completions = get_completions(&cmd, "test", &[], "file");
1007
1008        assert_eq!(completions.len(), 2);
1009        assert!(completions.iter().any(|c| c.value == "file.txt"));
1010        assert!(completions.iter().any(|c| c.value == "file.md"));
1011    }
1012
1013    #[test]
1014    fn test_get_completions_second_argument() {
1015        use crate::argument::Argument;
1016        use crate::types::Choice;
1017
1018        let cmd = Command::new("test")
1019            .argument(Argument::new("first").build())
1020            .argument(
1021                Argument::new("second")
1022                    .type_(Choice::new(["a", "b", "c"]))
1023                    .build()
1024            )
1025            .build();
1026
1027        // First argument has no completions (STRING type)
1028        let completions = get_completions(&cmd, "test", &[], "x");
1029        assert!(completions.is_empty());
1030
1031        // Second argument should complete after first is provided
1032        let completions = get_completions(&cmd, "test", &["value1".to_string()], "a");
1033        assert_eq!(completions.len(), 1);
1034        assert_eq!(completions[0].value, "a");
1035    }
1036
1037    #[test]
1038    fn test_get_completions_variadic_argument() {
1039        use crate::argument::Argument;
1040        use crate::types::Choice;
1041
1042        let cmd = Command::new("test")
1043            .argument(
1044                Argument::new("files")
1045                    .multiple()
1046                    .type_(Choice::new(["foo.txt", "bar.txt", "baz.txt"]))
1047                    .build()
1048            )
1049            .build();
1050
1051        // First value
1052        let completions = get_completions(&cmd, "test", &[], "f");
1053        assert_eq!(completions.len(), 1);
1054        assert_eq!(completions[0].value, "foo.txt");
1055
1056        // Second value (variadic continues to complete)
1057        let completions = get_completions(&cmd, "test", &["foo.txt".to_string()], "b");
1058        assert_eq!(completions.len(), 2);
1059        assert!(completions.iter().any(|c| c.value == "bar.txt"));
1060        assert!(completions.iter().any(|c| c.value == "baz.txt"));
1061    }
1062
1063    #[test]
1064    fn test_get_completions_no_more_arguments() {
1065        use crate::argument::Argument;
1066
1067        let cmd = Command::new("test")
1068            .argument(Argument::new("single").build())
1069            .build();
1070
1071        // After the single argument is provided, no more completions
1072        let completions = get_completions(&cmd, "test", &["value".to_string()], "x");
1073        // Should be empty (no more arguments to complete)
1074        // Note: options might still complete if incomplete doesn't start with '-'
1075        assert!(completions.is_empty());
1076    }
1077}