• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 //! Complete commands within shells
2 
3 /// Complete commands within bash
4 pub 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
complete(&self, cmd: &mut clap::Command) -> std::convert::Infallible65         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
try_complete(&self, cmd: &mut clap::Command) -> clap::error::Result<()>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
file_name(name: &str) -> String124     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 {
default() -> Self139         fn default() -> Self {
140             Self::Readline
141         }
142     }
143 
144     /// Generate code to register the dynamic completion
register( name: &str, executables: impl IntoIterator<Item = impl AsRef<str>>, completer: &str, behavior: &Behavior, buf: &mut dyn Write, ) -> Result<(), std::io::Error>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 }
194 complete 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 {
value_variants<'a>() -> &'a [Self]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         }
to_possible_value(&self) -> ::std::option::Option<clap::builder::PossibleValue>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 {
default() -> Self284         fn default() -> Self {
285             Self::Normal
286         }
287     }
288 
289     /// Complete the command specified
complete( cmd: &mut clap::Command, args: Vec<std::ffi::OsString>, arg_index: usize, _comp_type: CompType, _trailing_space: bool, current_dir: Option<&std::path::Path>, ) -> Result<Vec<std::ffi::OsString>, std::io::Error>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 
complete_arg( arg: &clap_lex::ParsedArg<'_>, cmd: &clap::Command, current_dir: Option<&std::path::Path>, pos_index: usize, is_escaped: bool, ) -> Result<Vec<std::ffi::OsString>, std::io::Error>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 
complete_arg_value( value: Result<&str, &clap_lex::RawOsStr>, arg: &clap::Arg, current_dir: Option<&std::path::Path>, ) -> Vec<OsString>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 
complete_path( value_os: &clap_lex::RawOsStr, current_dir: Option<&std::path::Path>, is_wanted: impl Fn(&std::path::Path) -> bool, ) -> Vec<OsString>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 
complete_subcommand(value: &str, cmd: &clap::Command) -> Vec<OsString>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