flag-rs 0.10.0

A Cobra-inspired CLI framework with dynamic completions
Documentation
//! Shell completion script generation
//!
//! This module provides functionality to generate shell completion scripts
//! for Bash, Zsh, and Fish shells. The generated scripts integrate with the
//! dynamic completion system to provide TAB completions at runtime.

use crate::command::Command;

/// Supported shell types for completion generation
///
/// This enum represents the shells for which we can generate completion scripts.
///
/// # Examples
///
/// ```
/// use flag_rs::shell::Shell;
/// use flag_rs::Command;
///
/// let cmd = Command::new("myapp");
///
/// // Generate Bash completion script
/// let bash_script = cmd.generate_completion(Shell::Bash);
///
/// // Generate Zsh completion script
/// let zsh_script = cmd.generate_completion(Shell::Zsh);
///
/// // Generate Fish completion script
/// let fish_script = cmd.generate_completion(Shell::Fish);
/// ```
#[derive(Debug, Clone, Copy)]
pub enum Shell {
    /// Bash shell (most common on Linux)
    Bash,
    /// Zsh shell (default on macOS)
    Zsh,
    /// Fish shell (modern alternative shell)
    Fish,
}

impl Command {
    /// Generates a completion script for the specified shell
    ///
    /// The generated script should be saved to the appropriate location
    /// for your shell to load it automatically.
    ///
    /// # Arguments
    ///
    /// * `shell` - The shell to generate completions for
    ///
    /// # Returns
    ///
    /// A string containing the shell completion script
    ///
    /// # Shell-specific installation
    ///
    /// ## Bash
    /// Save to `/etc/bash_completion.d/myapp` or source from `.bashrc`:
    /// ```bash
    /// myapp completion bash > ~/.myapp-completion.bash
    /// echo "source ~/.myapp-completion.bash" >> ~/.bashrc
    /// ```
    ///
    /// ## Zsh
    /// Save to a directory in your `$fpath`:
    /// ```bash
    /// myapp completion zsh > ~/.zsh/completions/_myapp
    /// ```
    ///
    /// ## Fish
    /// Save to Fish's completion directory:
    /// ```bash
    /// myapp completion fish > ~/.config/fish/completions/myapp.fish
    /// ```
    pub fn generate_completion(&self, shell: Shell) -> String {
        match shell {
            Shell::Bash => self.generate_bash_completion(),
            Shell::Zsh => self.generate_zsh_completion(),
            Shell::Fish => self.generate_fish_completion(),
        }
    }

    fn generate_bash_completion(&self) -> String {
        let name = self.name();
        let upper = name.to_uppercase();
        format!(
            r#"# Bash completion for {name}
_{name}_complete() {{
    local cur prev words cword
    _get_comp_words_by_ref -n : cur prev words cword

    # Call our binary with special completion env var
    local IFS=$'\n'
    local response
    response=$({upper}_COMPLETE=bash "${{words[0]}}" __complete "${{words[@]:1:$((cword-1))}}" "$cur" 2>/dev/null)

    if [[ -n "$response" ]]; then
        # Use printf to handle each line separately
        local lines=()
        local help_messages=()
        while IFS= read -r line; do
            if [[ "$line" == _activehelp_* ]]; then
                # Extract help message
                help_messages+=("${{line#_activehelp_ }}")
            else
                lines+=("$line")
            fi
        done <<< "$response"
        COMPREPLY=( "${{lines[@]}}" )

        # Display help messages if any
        if [[ ${{#help_messages[@]}} -gt 0 ]]; then
            printf '\n'
            for msg in "${{help_messages[@]}}"; do
                printf '%s\n' "$msg"
            done
            printf '\n'
        fi
    fi
}}

complete -F _{name}_complete {name}
"#
        )
    }

    fn generate_zsh_completion(&self) -> String {
        let name = self.name();
        let upper = name.to_uppercase();
        format!(
            r#"#compdef -P {name}
# Zsh completion for {name}

_{name}_complete() {{
    local -a completions
    local IFS=$'\n'

    # Get the actual command from the command line
    local cmd="${{words[1]}}"
    if [[ "$cmd" != /* ]] && ! command -v "$cmd" &>/dev/null; then
        # If not found in PATH, try relative path
        if [[ -x "$cmd" ]]; then
            cmd="./$cmd"
        fi
    fi

    # Build completion arguments
    local -a comp_line
    comp_line=("__complete")

    # Add all words except the command name
    local i
    for (( i = 2; i < CURRENT; i++ )); do
        comp_line+=("${{words[$i]}}")
    done

    # Add the current word being completed
    comp_line+=("${{words[CURRENT]}}")

    # Call the command with completion environment variable
    local response
    response=$({upper}_COMPLETE=zsh "$cmd" "${{comp_line[@]}}" 2>/dev/null)

    if [[ -n "$response" ]]; then
        local -a values
        local -a descriptions
        local -a help_messages
        local line

        # Parse response lines
        while IFS= read -r line; do
            if [[ "$line" == _activehelp_::* ]]; then
                # ActiveHelp message
                help_messages+=("${{line#_activehelp_::}}")
            elif [[ "$line" == *:* ]]; then
                # Line has description
                values+=("${{line%%:*}}")
                descriptions+=("${{line#*:}}")
            else
                # No description
                values+=("$line")
                descriptions+=("")
            fi
        done <<< "$response"

        # Display ActiveHelp messages if any
        if [[ ${{#help_messages[@]}} -gt 0 ]]; then
            local formatted_help=()
            for msg in "${{help_messages[@]}}"; do
                formatted_help+=("-- $msg --")
            done
            compadd -x "${{(j: :)formatted_help}}"
        fi

        # Add completions with descriptions
        if [[ ${{#descriptions[@]}} -gt 0 ]] && [[ -n "${{descriptions[*]// }}" ]]; then
            compadd -Q -d descriptions -a values
        else
            compadd -Q -a values
        fi
    fi
}}

compdef _{name}_complete {name}
"#
        )
    }

    fn generate_fish_completion(&self) -> String {
        let name = self.name();
        let upper = name.to_uppercase();
        // Regular string (not raw): `\t` MUST be a literal tab to match the
        // `_activehelp_\t<msg>` separator that completion_format.rs emits.
        // A raw `r#"..."#` would produce literal backslash-t and silently
        // break ActiveHelp matching in fish.
        format!(
            "# Fish completion for {name}
function __{name}_complete
    set -l cmd (commandline -opc)
    set -l cursor (commandline -C)
    set -l current (commandline -ct)

    # Call our binary with special completion env var
    set -l response (env {upper}_COMPLETE=fish $cmd[1] __complete $cmd[2..-1] $current 2>/dev/null)

    # Process response and handle ActiveHelp
    set -l help_messages
    for line in $response
        if string match -q '_activehelp_*' -- $line
            # Extract help message
            set -a help_messages (string replace '_activehelp_\t' '' -- $line)
        else
            echo $line
        end
    end

    # Display help messages if any
    if test (count $help_messages) -gt 0
        for msg in $help_messages
            echo \"ยป $msg\" >&2
        end
    end
end

complete -c {name} -f -a '(__{name}_complete)'
"
        )
    }
}