flag_rs/
shell.rs

1//! Shell completion script generation
2//!
3//! This module provides functionality to generate shell completion scripts
4//! for Bash, Zsh, and Fish shells. The generated scripts integrate with the
5//! dynamic completion system to provide TAB completions at runtime.
6
7use crate::command::Command;
8use std::fmt::Write;
9
10/// Safe writeln macro that handles the rare case where writing to String fails
11/// Writing to String should virtually never fail except in extreme memory conditions
12macro_rules! safe_writeln {
13    ($dst:expr) => {
14        if writeln!($dst).is_err() {
15            eprintln!("Warning: Failed to write to completion script buffer");
16        }
17    };
18    ($dst:expr, $($arg:tt)*) => {
19        if writeln!($dst, $($arg)*).is_err() {
20            eprintln!("Warning: Failed to write to completion script buffer");
21        }
22    };
23}
24
25/// Supported shell types for completion generation
26///
27/// This enum represents the shells for which we can generate completion scripts.
28///
29/// # Examples
30///
31/// ```
32/// use flag_rs::shell::Shell;
33/// use flag_rs::Command;
34///
35/// let cmd = Command::new("myapp");
36///
37/// // Generate Bash completion script
38/// let bash_script = cmd.generate_completion(Shell::Bash);
39///
40/// // Generate Zsh completion script
41/// let zsh_script = cmd.generate_completion(Shell::Zsh);
42///
43/// // Generate Fish completion script
44/// let fish_script = cmd.generate_completion(Shell::Fish);
45/// ```
46#[derive(Debug, Clone, Copy)]
47pub enum Shell {
48    /// Bash shell (most common on Linux)
49    Bash,
50    /// Zsh shell (default on macOS)
51    Zsh,
52    /// Fish shell (modern alternative shell)
53    Fish,
54}
55
56impl Command {
57    /// Generates a completion script for the specified shell
58    ///
59    /// The generated script should be saved to the appropriate location
60    /// for your shell to load it automatically.
61    ///
62    /// # Arguments
63    ///
64    /// * `shell` - The shell to generate completions for
65    ///
66    /// # Returns
67    ///
68    /// A string containing the shell completion script
69    ///
70    /// # Shell-specific installation
71    ///
72    /// ## Bash
73    /// Save to `/etc/bash_completion.d/myapp` or source from `.bashrc`:
74    /// ```bash
75    /// myapp completion bash > ~/.myapp-completion.bash
76    /// echo "source ~/.myapp-completion.bash" >> ~/.bashrc
77    /// ```
78    ///
79    /// ## Zsh
80    /// Save to a directory in your `$fpath`:
81    /// ```bash
82    /// myapp completion zsh > ~/.zsh/completions/_myapp
83    /// ```
84    ///
85    /// ## Fish
86    /// Save to Fish's completion directory:
87    /// ```bash
88    /// myapp completion fish > ~/.config/fish/completions/myapp.fish
89    /// ```
90    pub fn generate_completion(&self, shell: Shell) -> String {
91        match shell {
92            Shell::Bash => self.generate_bash_completion(),
93            Shell::Zsh => self.generate_zsh_completion(),
94            Shell::Fish => self.generate_fish_completion(),
95        }
96    }
97
98    fn generate_bash_completion(&self) -> String {
99        let mut script = String::new();
100
101        safe_writeln!(&mut script, "# Bash completion for {}", self.name());
102        safe_writeln!(&mut script, "_{}_complete() {{", self.name());
103        safe_writeln!(&mut script, "    local cur prev words cword");
104        safe_writeln!(
105            &mut script,
106            "    _get_comp_words_by_ref -n : cur prev words cword"
107        );
108        safe_writeln!(&mut script);
109        safe_writeln!(
110            &mut script,
111            "    # Call our binary with special completion env var"
112        );
113        safe_writeln!(&mut script, "    local IFS=$'\\n'");
114        safe_writeln!(&mut script, "    local response");
115        safe_writeln!(
116            &mut script,
117            "    response=$({}_COMPLETE=bash \"${{words[0]}}\" __complete \"${{words[@]:1:$((cword-1))}}\" \"$cur\" 2>/dev/null)",
118            self.name().to_uppercase()
119        );
120        safe_writeln!(&mut script);
121        safe_writeln!(&mut script, "    if [[ -n \"$response\" ]]; then");
122        safe_writeln!(
123            &mut script,
124            "        # Use printf to handle each line separately"
125        );
126        safe_writeln!(&mut script, "        local lines=()");
127        safe_writeln!(&mut script, "        local help_messages=()");
128        safe_writeln!(&mut script, "        while IFS= read -r line; do");
129        safe_writeln!(
130            &mut script,
131            "            if [[ \"$line\" == _activehelp_* ]]; then"
132        );
133        safe_writeln!(&mut script, "                # Extract help message");
134        safe_writeln!(
135            &mut script,
136            "                help_messages+=(\"${{line#_activehelp_ }}\")"
137        );
138        safe_writeln!(&mut script, "            else");
139        safe_writeln!(&mut script, "                lines+=(\"$line\")");
140        safe_writeln!(&mut script, "            fi");
141        safe_writeln!(&mut script, "        done <<< \"$response\"");
142        safe_writeln!(&mut script, "        COMPREPLY=( \"${{lines[@]}}\" )");
143        safe_writeln!(&mut script);
144        safe_writeln!(&mut script, "        # Display help messages if any");
145        safe_writeln!(
146            &mut script,
147            "        if [[ ${{#help_messages[@]}} -gt 0 ]]; then"
148        );
149        safe_writeln!(&mut script, "            printf '\\n'");
150        safe_writeln!(
151            &mut script,
152            "            for msg in \"${{help_messages[@]}}\"; do"
153        );
154        safe_writeln!(&mut script, "                printf '%s\\n' \"$msg\"");
155        safe_writeln!(&mut script, "            done");
156        safe_writeln!(&mut script, "            printf '\\n'");
157        safe_writeln!(&mut script, "        fi");
158        safe_writeln!(&mut script, "    fi");
159        safe_writeln!(&mut script, "}}");
160        safe_writeln!(&mut script);
161        safe_writeln!(
162            &mut script,
163            "complete -F _{}_complete {}",
164            self.name(),
165            self.name()
166        );
167
168        script
169    }
170
171    fn generate_zsh_completion(&self) -> String {
172        let mut script = String::new();
173
174        safe_writeln!(&mut script, "#compdef -P {}", self.name());
175        safe_writeln!(&mut script, "# Zsh completion for {}", self.name());
176        safe_writeln!(&mut script);
177        safe_writeln!(&mut script, "_{}_complete() {{", self.name());
178        safe_writeln!(&mut script, "    local -a completions");
179        safe_writeln!(&mut script, "    local IFS=$'\\n'");
180        safe_writeln!(&mut script);
181        safe_writeln!(
182            &mut script,
183            "    # Get the actual command from the command line"
184        );
185        safe_writeln!(&mut script, "    local cmd=\"${{words[1]}}\"");
186        safe_writeln!(
187            &mut script,
188            "    if [[ \"$cmd\" != /* ]] && ! command -v \"$cmd\" &>/dev/null; then"
189        );
190        safe_writeln!(
191            &mut script,
192            "        # If not found in PATH, try relative path"
193        );
194        safe_writeln!(&mut script, "        if [[ -x \"$cmd\" ]]; then");
195        safe_writeln!(&mut script, "            cmd=\"./$cmd\"");
196        safe_writeln!(&mut script, "        fi");
197        safe_writeln!(&mut script, "    fi");
198        safe_writeln!(&mut script);
199        safe_writeln!(&mut script, "    # Build completion arguments");
200        safe_writeln!(&mut script, "    local -a comp_line");
201        safe_writeln!(&mut script, "    comp_line=(\"__complete\")");
202        safe_writeln!(&mut script);
203        safe_writeln!(&mut script, "    # Add all words except the command name");
204        safe_writeln!(&mut script, "    local i");
205        safe_writeln!(&mut script, "    for (( i = 2; i < CURRENT; i++ )); do");
206        safe_writeln!(&mut script, "        comp_line+=(\"${{words[$i]}}\")");
207        safe_writeln!(&mut script, "    done");
208        safe_writeln!(&mut script);
209        safe_writeln!(&mut script, "    # Add the current word being completed");
210        safe_writeln!(&mut script, "    comp_line+=(\"${{words[CURRENT]}}\")");
211        safe_writeln!(&mut script);
212        safe_writeln!(
213            &mut script,
214            "    # Call the command with completion environment variable"
215        );
216        safe_writeln!(&mut script, "    local response");
217        safe_writeln!(
218            &mut script,
219            "    response=$({}_COMPLETE=zsh \"$cmd\" \"${{comp_line[@]}}\" 2>/dev/null)",
220            self.name().to_uppercase()
221        );
222        safe_writeln!(&mut script);
223        safe_writeln!(&mut script, "    if [[ -n \"$response\" ]]; then");
224        safe_writeln!(&mut script, "        local -a values");
225        safe_writeln!(&mut script, "        local -a descriptions");
226        safe_writeln!(&mut script, "        local -a help_messages");
227        safe_writeln!(&mut script, "        local line");
228        safe_writeln!(&mut script);
229        safe_writeln!(&mut script, "        # Parse response lines");
230        safe_writeln!(&mut script, "        while IFS= read -r line; do");
231        safe_writeln!(
232            &mut script,
233            "            if [[ \"$line\" == _activehelp_::* ]]; then"
234        );
235        safe_writeln!(&mut script, "                # ActiveHelp message");
236        safe_writeln!(
237            &mut script,
238            "                help_messages+=(\"${{line#_activehelp_::}}\")"
239        );
240        safe_writeln!(&mut script, "            elif [[ \"$line\" == *:* ]]; then");
241        safe_writeln!(&mut script, "                # Line has description");
242        safe_writeln!(&mut script, "                values+=(\"${{line%%:*}}\")");
243        safe_writeln!(
244            &mut script,
245            "                descriptions+=(\"${{line#*:}}\")"
246        );
247        safe_writeln!(&mut script, "            else");
248        safe_writeln!(&mut script, "                # No description");
249        safe_writeln!(&mut script, "                values+=(\"$line\")");
250        safe_writeln!(&mut script, "                descriptions+=(\"\")");
251        safe_writeln!(&mut script, "            fi");
252        safe_writeln!(&mut script, "        done <<< \"$response\"");
253        safe_writeln!(&mut script);
254        safe_writeln!(&mut script, "        # Display ActiveHelp messages if any");
255        safe_writeln!(
256            &mut script,
257            "        if [[ ${{#help_messages[@]}} -gt 0 ]]; then"
258        );
259        safe_writeln!(&mut script, "            local formatted_help=()");
260        safe_writeln!(
261            &mut script,
262            "            for msg in \"${{help_messages[@]}}\"; do"
263        );
264        safe_writeln!(
265            &mut script,
266            "                formatted_help+=(\"-- $msg --\")"
267        );
268        safe_writeln!(&mut script, "            done");
269        safe_writeln!(
270            &mut script,
271            "            compadd -x \"${{(j: :)formatted_help}}\""
272        );
273        safe_writeln!(&mut script, "        fi");
274        safe_writeln!(&mut script);
275        safe_writeln!(&mut script, "        # Add completions with descriptions");
276        safe_writeln!(
277            &mut script,
278            "        if [[ ${{#descriptions[@]}} -gt 0 ]] && [[ -n \"${{descriptions[*]// }}\" ]]; then"
279        );
280        safe_writeln!(
281            &mut script,
282            "            compadd -Q -d descriptions -a values"
283        );
284        safe_writeln!(&mut script, "        else");
285        safe_writeln!(&mut script, "            compadd -Q -a values");
286        safe_writeln!(&mut script, "        fi");
287        safe_writeln!(&mut script, "    fi");
288        safe_writeln!(&mut script, "}}");
289        safe_writeln!(&mut script);
290        safe_writeln!(
291            &mut script,
292            "compdef _{}_complete {}",
293            self.name(),
294            self.name()
295        );
296
297        script
298    }
299
300    fn generate_fish_completion(&self) -> String {
301        let mut script = String::new();
302
303        safe_writeln!(&mut script, "# Fish completion for {}", self.name());
304        safe_writeln!(&mut script, "function __{}_complete", self.name());
305        safe_writeln!(&mut script, "    set -l cmd (commandline -opc)");
306        safe_writeln!(&mut script, "    set -l cursor (commandline -C)");
307        safe_writeln!(&mut script, "    set -l current (commandline -ct)");
308        safe_writeln!(&mut script);
309        safe_writeln!(
310            &mut script,
311            "    # Call our binary with special completion env var"
312        );
313        safe_writeln!(
314            &mut script,
315            "    set -l response (env {}_COMPLETE=fish $cmd[1] __complete $cmd[2..-1] $current 2>/dev/null)",
316            self.name().to_uppercase()
317        );
318        safe_writeln!(&mut script);
319        safe_writeln!(&mut script, "    # Process response and handle ActiveHelp");
320        safe_writeln!(&mut script, "    set -l help_messages");
321        safe_writeln!(&mut script, "    for line in $response");
322        safe_writeln!(
323            &mut script,
324            "        if string match -q '_activehelp_*' -- $line"
325        );
326        safe_writeln!(&mut script, "            # Extract help message");
327        safe_writeln!(
328            &mut script,
329            "            set -a help_messages (string replace '_activehelp_\t' '' -- $line)"
330        );
331        safe_writeln!(&mut script, "        else");
332        safe_writeln!(&mut script, "            echo $line");
333        safe_writeln!(&mut script, "        end");
334        safe_writeln!(&mut script, "    end");
335        safe_writeln!(&mut script);
336        safe_writeln!(&mut script, "    # Display help messages if any");
337        safe_writeln!(&mut script, "    if test (count $help_messages) -gt 0");
338        safe_writeln!(&mut script, "        for msg in $help_messages");
339        safe_writeln!(&mut script, "            echo \"ยป $msg\" >&2");
340        safe_writeln!(&mut script, "        end");
341        safe_writeln!(&mut script, "    end");
342        safe_writeln!(&mut script, "end");
343        safe_writeln!(&mut script);
344        safe_writeln!(
345            &mut script,
346            "complete -c {} -f -a '(__{}_complete)'",
347            self.name(),
348            self.name()
349        );
350
351        script
352    }
353}