1//! Complete commands within shells
2
3/// Complete commands within bash
4pub mod bash {
5    use std::ffi::OsString;
6    use std::io::Write;
7
8    use unicode_xid::UnicodeXID;
9
10    #[derive(clap::Subcommand)]
11    #[command(hide = true)]
12    #[allow(missing_docs)]
13    #[derive(Clone, Debug)]
14    pub enum CompleteCommand {
15        /// Register shell completions for this program
16        Complete(CompleteArgs),
17    }
18
19    #[derive(clap::Args)]
20    #[command(group = clap::ArgGroup::new("complete").multiple(true).conflicts_with("register"))]
21    #[allow(missing_docs)]
22    #[derive(Clone, Debug)]
23    pub struct CompleteArgs {
24        /// Path to write completion-registration to
25        #[arg(long, required = true)]
26        register: Option<std::path::PathBuf>,
27
28        #[arg(
29            long,
30            required = true,
31            value_name = "COMP_CWORD",
32            hide_short_help = true,
33            group = "complete"
34        )]
35        index: Option<usize>,
36
37        #[arg(long, hide_short_help = true, group = "complete")]
38        ifs: Option<String>,
39
40        #[arg(
41            long = "type",
42            required = true,
43            hide_short_help = true,
44            group = "complete"
45        )]
46        comp_type: Option<CompType>,
47
48        #[arg(long, hide_short_help = true, group = "complete")]
49        space: bool,
50
51        #[arg(
52            long,
53            conflicts_with = "space",
54            hide_short_help = true,
55            group = "complete"
56        )]
57        no_space: bool,
58
59        #[arg(raw = true, hide_short_help = true, group = "complete")]
60        comp_words: Vec<OsString>,
61    }
62
63    impl CompleteCommand {
64        /// Process the completion request
65        pub fn complete(&self, cmd: &mut clap::Command) -> std::convert::Infallible {
66            self.try_complete(cmd).unwrap_or_else(|e| e.exit());
67            std::process::exit(0)
68        }
69
70        /// Process the completion request
71        pub fn try_complete(&self, cmd: &mut clap::Command) -> clap::error::Result<()> {
72            debug!("CompleteCommand::try_complete: {:?}", self);
73            let CompleteCommand::Complete(args) = self;
74            if let Some(out_path) = args.register.as_deref() {
75                let mut buf = Vec::new();
76                let name = cmd.get_name();
77                let bin = cmd.get_bin_name().unwrap_or_else(|| cmd.get_name());
78                register(name, [bin], bin, &Behavior::default(), &mut buf)?;
79                if out_path == std::path::Path::new("-") {
80                    std::io::stdout().write_all(&buf)?;
81                } else if out_path.is_dir() {
82                    let out_path = out_path.join(file_name(name));
83                    std::fs::write(out_path, buf)?;
84                } else {
85                    std::fs::write(out_path, buf)?;
86                }
87            } else {
88                let index = args.index.unwrap_or_default();
89                let comp_type = args.comp_type.unwrap_or_default();
90                let space = match (args.space, args.no_space) {
91                    (true, false) => Some(true),
92                    (false, true) => Some(false),
93                    (true, true) => {
94                        unreachable!("`--space` and `--no-space` set, clap should prevent this")
95                    }
96                    (false, false) => None,
97                }
98                .unwrap();
99                let current_dir = std::env::current_dir().ok();
100                let completions = complete(
101                    cmd,
102                    args.comp_words.clone(),
103                    index,
104                    comp_type,
105                    space,
106                    current_dir.as_deref(),
107                )?;
108
109                let mut buf = Vec::new();
110                for (i, completion) in completions.iter().enumerate() {
111                    if i != 0 {
112                        write!(&mut buf, "{}", args.ifs.as_deref().unwrap_or("\n"))?;
113                    }
114                    write!(&mut buf, "{}", completion.to_string_lossy())?;
115                }
116                std::io::stdout().write_all(&buf)?;
117            }
118
119            Ok(())
120        }
121    }
122
123    /// The recommended file name for the registration code
124    pub fn file_name(name: &str) -> String {
125        format!("{}.bash", name)
126    }
127
128    /// Define the completion behavior
129    pub enum Behavior {
130        /// Bare bones behavior
131        Minimal,
132        /// Fallback to readline behavior when no matches are generated
133        Readline,
134        /// Customize bash's completion behavior
135        Custom(String),
136    }
137
138    impl Default for Behavior {
139        fn default() -> Self {
140            Self::Readline
141        }
142    }
143
144    /// Generate code to register the dynamic completion
145    pub fn register(
146        name: &str,
147        executables: impl IntoIterator<Item = impl AsRef<str>>,
148        completer: &str,
149        behavior: &Behavior,
150        buf: &mut dyn Write,
151    ) -> Result<(), std::io::Error> {
152        let escaped_name = name.replace('-', "_");
153        debug_assert!(
154            escaped_name.chars().all(|c| c.is_xid_continue()),
155            "`name` must be an identifier, got `{}`",
156            escaped_name
157        );
158        let mut upper_name = escaped_name.clone();
159        upper_name.make_ascii_uppercase();
160
161        let executables = executables
162            .into_iter()
163            .map(|s| shlex::quote(s.as_ref()).into_owned())
164            .collect::<Vec<_>>()
165            .join(" ");
166
167        let options = match behavior {
168            Behavior::Minimal => "-o nospace -o bashdefault",
169            Behavior::Readline => "-o nospace -o default -o bashdefault",
170            Behavior::Custom(c) => c.as_str(),
171        };
172
173        let completer = shlex::quote(completer);
174
175        let script = r#"
176_clap_complete_NAME() {
177    local IFS=$'\013'
178    local SUPPRESS_SPACE=0
179    if compopt +o nospace 2> /dev/null; then
180        SUPPRESS_SPACE=1
181    fi
182    if [[ ${SUPPRESS_SPACE} == 1 ]]; then
183        SPACE_ARG="--no-space"
184    else
185        SPACE_ARG="--space"
186    fi
187    COMPREPLY=( $("COMPLETER" complete --index ${COMP_CWORD} --type ${COMP_TYPE} ${SPACE_ARG} --ifs="$IFS" -- "${COMP_WORDS[@]}") )
188    if [[ $? != 0 ]]; then
189        unset COMPREPLY
190    elif [[ $SUPPRESS_SPACE == 1 ]] && [[ "${COMPREPLY-}" =~ [=/:]$ ]]; then
191        compopt -o nospace
192    fi
193}
194complete OPTIONS -F _clap_complete_NAME EXECUTABLES
195"#
196        .replace("NAME", &escaped_name)
197        .replace("EXECUTABLES", &executables)
198        .replace("OPTIONS", options)
199        .replace("COMPLETER", &completer)
200        .replace("UPPER", &upper_name);
201
202        writeln!(buf, "{}", script)?;
203        Ok(())
204    }
205
206    /// Type of completion attempted that caused a completion function to be called
207    #[derive(Copy, Clone, Debug, PartialEq, Eq)]
208    #[non_exhaustive]
209    pub enum CompType {
210        /// Normal completion
211        Normal,
212        /// List completions after successive tabs
213        Successive,
214        /// List alternatives on partial word completion
215        Alternatives,
216        /// List completions if the word is not unmodified
217        Unmodified,
218        /// Menu completion
219        Menu,
220    }
221
222    impl clap::ValueEnum for CompType {
223        fn value_variants<'a>() -> &'a [Self] {
224            &[
225                Self::Normal,
226                Self::Successive,
227                Self::Alternatives,
228                Self::Unmodified,
229                Self::Menu,
230            ]
231        }
232        fn to_possible_value(&self) -> ::std::option::Option<clap::builder::PossibleValue> {
233            match self {
234                Self::Normal => {
235                    let value = "9";
236                    debug_assert_eq!(b'\t'.to_string(), value);
237                    Some(
238                        clap::builder::PossibleValue::new(value)
239                            .alias("normal")
240                            .help("Normal completion"),
241                    )
242                }
243                Self::Successive => {
244                    let value = "63";
245                    debug_assert_eq!(b'?'.to_string(), value);
246                    Some(
247                        clap::builder::PossibleValue::new(value)
248                            .alias("successive")
249                            .help("List completions after successive tabs"),
250                    )
251                }
252                Self::Alternatives => {
253                    let value = "33";
254                    debug_assert_eq!(b'!'.to_string(), value);
255                    Some(
256                        clap::builder::PossibleValue::new(value)
257                            .alias("alternatives")
258                            .help("List alternatives on partial word completion"),
259                    )
260                }
261                Self::Unmodified => {
262                    let value = "64";
263                    debug_assert_eq!(b'@'.to_string(), value);
264                    Some(
265                        clap::builder::PossibleValue::new(value)
266                            .alias("unmodified")
267                            .help("List completions if the word is not unmodified"),
268                    )
269                }
270                Self::Menu => {
271                    let value = "37";
272                    debug_assert_eq!(b'%'.to_string(), value);
273                    Some(
274                        clap::builder::PossibleValue::new(value)
275                            .alias("menu")
276                            .help("Menu completion"),
277                    )
278                }
279            }
280        }
281    }
282
283    impl Default for CompType {
284        fn default() -> Self {
285            Self::Normal
286        }
287    }
288
289    /// Complete the command specified
290    pub fn complete(
291        cmd: &mut clap::Command,
292        args: Vec<std::ffi::OsString>,
293        arg_index: usize,
294        _comp_type: CompType,
295        _trailing_space: bool,
296        current_dir: Option<&std::path::Path>,
297    ) -> Result<Vec<std::ffi::OsString>, std::io::Error> {
298        cmd.build();
299
300        let raw_args = clap_lex::RawArgs::new(args.into_iter());
301        let mut cursor = raw_args.cursor();
302        let mut target_cursor = raw_args.cursor();
303        raw_args.seek(
304            &mut target_cursor,
305            clap_lex::SeekFrom::Start(arg_index as u64),
306        );
307        // As we loop, `cursor` will always be pointing to the next item
308        raw_args.next_os(&mut target_cursor);
309
310        // TODO: Multicall support
311        if !cmd.is_no_binary_name_set() {
312            raw_args.next_os(&mut cursor);
313        }
314
315        let mut current_cmd = &*cmd;
316        let mut pos_index = 1;
317        let mut is_escaped = false;
318        while let Some(arg) = raw_args.next(&mut cursor) {
319            if cursor == target_cursor {
320                return complete_arg(&arg, current_cmd, current_dir, pos_index, is_escaped);
321            }
322
323            debug!(
324                "complete::next: Begin parsing '{:?}' ({:?})",
325                arg.to_value_os(),
326                arg.to_value_os().as_raw_bytes()
327            );
328
329            if let Ok(value) = arg.to_value() {
330                if let Some(next_cmd) = current_cmd.find_subcommand(value) {
331                    current_cmd = next_cmd;
332                    pos_index = 0;
333                    continue;
334                }
335            }
336
337            if is_escaped {
338                pos_index += 1;
339            } else if arg.is_escape() {
340                is_escaped = true;
341            } else if let Some(_long) = arg.to_long() {
342            } else if let Some(_short) = arg.to_short() {
343            } else {
344                pos_index += 1;
345            }
346        }
347
348        Err(std::io::Error::new(
349            std::io::ErrorKind::Other,
350            "No completion generated",
351        ))
352    }
353
354    fn complete_arg(
355        arg: &clap_lex::ParsedArg<'_>,
356        cmd: &clap::Command,
357        current_dir: Option<&std::path::Path>,
358        pos_index: usize,
359        is_escaped: bool,
360    ) -> Result<Vec<std::ffi::OsString>, std::io::Error> {
361        debug!(
362            "complete_arg: arg={:?}, cmd={:?}, current_dir={:?}, pos_index={}, is_escaped={}",
363            arg,
364            cmd.get_name(),
365            current_dir,
366            pos_index,
367            is_escaped
368        );
369        let mut completions = Vec::new();
370
371        if !is_escaped {
372            if let Some((flag, value)) = arg.to_long() {
373                if let Ok(flag) = flag {
374                    if let Some(value) = value {
375                        if let Some(arg) = cmd.get_arguments().find(|a| a.get_long() == Some(flag))
376                        {
377                            completions.extend(
378                                complete_arg_value(value.to_str().ok_or(value), arg, current_dir)
379                                    .into_iter()
380                                    .map(|os| {
381                                        // HACK: Need better `OsStr` manipulation
382                                        format!("--{}={}", flag, os.to_string_lossy()).into()
383                                    }),
384                            )
385                        }
386                    } else {
387                        completions.extend(
388                            crate::generator::utils::longs_and_visible_aliases(cmd)
389                                .into_iter()
390                                .filter_map(|f| {
391                                    f.starts_with(flag).then(|| format!("--{}", f).into())
392                                }),
393                        );
394                    }
395                }
396            } else if arg.is_escape() || arg.is_stdio() || arg.is_empty() {
397                // HACK: Assuming knowledge of is_escape / is_stdio
398                completions.extend(
399                    crate::generator::utils::longs_and_visible_aliases(cmd)
400                        .into_iter()
401                        .map(|f| format!("--{}", f).into()),
402                );
403            }
404
405            if arg.is_empty() || arg.is_stdio() || arg.is_short() {
406                // HACK: Assuming knowledge of is_stdio
407                completions.extend(
408                    crate::generator::utils::shorts_and_visible_aliases(cmd)
409                        .into_iter()
410                        // HACK: Need better `OsStr` manipulation
411                        .map(|f| format!("{}{}", arg.to_value_os().to_str_lossy(), f).into()),
412                );
413            }
414        }
415
416        if let Some(positional) = cmd
417            .get_positionals()
418            .find(|p| p.get_index() == Some(pos_index))
419        {
420            completions.extend(complete_arg_value(arg.to_value(), positional, current_dir));
421        }
422
423        if let Ok(value) = arg.to_value() {
424            completions.extend(complete_subcommand(value, cmd));
425        }
426
427        Ok(completions)
428    }
429
430    fn complete_arg_value(
431        value: Result<&str, &clap_lex::RawOsStr>,
432        arg: &clap::Arg,
433        current_dir: Option<&std::path::Path>,
434    ) -> Vec<OsString> {
435        let mut values = Vec::new();
436        debug!("complete_arg_value: arg={:?}, value={:?}", arg, value);
437
438        if let Some(possible_values) = crate::generator::utils::possible_values(arg) {
439            if let Ok(value) = value {
440                values.extend(possible_values.into_iter().filter_map(|p| {
441                    let name = p.get_name();
442                    name.starts_with(value).then(|| name.into())
443                }));
444            }
445        } else {
446            let value_os = match value {
447                Ok(value) => clap_lex::RawOsStr::from_str(value),
448                Err(value_os) => value_os,
449            };
450            match arg.get_value_hint() {
451                clap::ValueHint::Other => {
452                    // Should not complete
453                }
454                clap::ValueHint::Unknown | clap::ValueHint::AnyPath => {
455                    values.extend(complete_path(value_os, current_dir, |_| true));
456                }
457                clap::ValueHint::FilePath => {
458                    values.extend(complete_path(value_os, current_dir, |p| p.is_file()));
459                }
460                clap::ValueHint::DirPath => {
461                    values.extend(complete_path(value_os, current_dir, |p| p.is_dir()));
462                }
463                clap::ValueHint::ExecutablePath => {
464                    use is_executable::IsExecutable;
465                    values.extend(complete_path(value_os, current_dir, |p| p.is_executable()));
466                }
467                clap::ValueHint::CommandName
468                | clap::ValueHint::CommandString
469                | clap::ValueHint::CommandWithArguments
470                | clap::ValueHint::Username
471                | clap::ValueHint::Hostname
472                | clap::ValueHint::Url
473                | clap::ValueHint::EmailAddress => {
474                    // No completion implementation
475                }
476                _ => {
477                    // Safe-ish fallback
478                    values.extend(complete_path(value_os, current_dir, |_| true));
479                }
480            }
481            values.sort();
482        }
483
484        values
485    }
486
487    fn complete_path(
488        value_os: &clap_lex::RawOsStr,
489        current_dir: Option<&std::path::Path>,
490        is_wanted: impl Fn(&std::path::Path) -> bool,
491    ) -> Vec<OsString> {
492        let mut completions = Vec::new();
493
494        let current_dir = match current_dir {
495            Some(current_dir) => current_dir,
496            None => {
497                // Can't complete without a `current_dir`
498                return Vec::new();
499            }
500        };
501        let (existing, prefix) = value_os
502            .split_once('\\')
503            .unwrap_or((clap_lex::RawOsStr::from_str(""), value_os));
504        let root = current_dir.join(existing.to_os_str());
505        debug!("complete_path: root={:?}, prefix={:?}", root, prefix);
506
507        for entry in std::fs::read_dir(&root)
508            .ok()
509            .into_iter()
510            .flatten()
511            .filter_map(Result::ok)
512        {
513            let raw_file_name = clap_lex::RawOsString::new(entry.file_name());
514            if !raw_file_name.starts_with_os(prefix) {
515                continue;
516            }
517
518            if entry.metadata().map(|m| m.is_dir()).unwrap_or(false) {
519                let path = entry.path();
520                let mut suggestion = pathdiff::diff_paths(&path, current_dir).unwrap_or(path);
521                suggestion.push(""); // Ensure trailing `/`
522                completions.push(suggestion.as_os_str().to_owned());
523            } else {
524                let path = entry.path();
525                if is_wanted(&path) {
526                    let suggestion = pathdiff::diff_paths(&path, current_dir).unwrap_or(path);
527                    completions.push(suggestion.as_os_str().to_owned());
528                }
529            }
530        }
531
532        completions
533    }
534
535    fn complete_subcommand(value: &str, cmd: &clap::Command) -> Vec<OsString> {
536        debug!(
537            "complete_subcommand: cmd={:?}, value={:?}",
538            cmd.get_name(),
539            value
540        );
541
542        let mut scs = crate::generator::utils::all_subcommands(cmd)
543            .into_iter()
544            .filter(|x| x.0.starts_with(value))
545            .map(|x| OsString::from(&x.0))
546            .collect::<Vec<_>>();
547        scs.sort();
548        scs.dedup();
549        scs
550    }
551}
552