mirror of
https://github.com/fish-shell/fish-shell.git
synced 2025-02-11 04:17:29 +08:00
619 lines
21 KiB
Rust
619 lines
21 KiB
Rust
use super::prelude::*;
|
|
use crate::common::{unescape_string, ScopeGuard, UnescapeFlags, UnescapeStringStyle};
|
|
use crate::complete::{complete_add_wrapper, complete_remove_wrapper, CompletionRequestOptions};
|
|
use crate::highlight::colorize;
|
|
use crate::highlight::highlight_shell;
|
|
use crate::nix::isatty;
|
|
use crate::parse_constants::ParseErrorList;
|
|
use crate::parse_util::parse_util_detect_errors_in_argument_list;
|
|
use crate::parse_util::{parse_util_detect_errors, parse_util_token_extent};
|
|
use crate::reader::{commandline_get_state, completion_apply_to_command_line};
|
|
use crate::wcstringutil::string_suffixes_string;
|
|
use crate::{
|
|
common::str2wcstring,
|
|
complete::{
|
|
complete_add, complete_print, complete_remove, complete_remove_all, CompleteFlags,
|
|
CompleteOptionType, CompletionMode,
|
|
},
|
|
};
|
|
use libc::STDOUT_FILENO;
|
|
|
|
// builtin_complete_* are a set of rather silly looping functions that make sure that all the proper
|
|
// combinations of complete_add or complete_remove get called. This is needed since complete allows
|
|
// you to specify multiple switches on a single commandline, like 'complete -s a -s b -s c', but the
|
|
// complete_add function only accepts one short switch and one long switch.
|
|
|
|
/// Silly function.
|
|
fn builtin_complete_add2(
|
|
cmd: &wstr,
|
|
cmd_is_path: bool,
|
|
short_opt: &wstr,
|
|
gnu_opts: &[WString],
|
|
old_opts: &[WString],
|
|
result_mode: CompletionMode,
|
|
condition: &[WString],
|
|
comp: &wstr,
|
|
desc: &wstr,
|
|
flags: CompleteFlags,
|
|
) {
|
|
for short_opt in short_opt.chars() {
|
|
complete_add(
|
|
cmd.to_owned(),
|
|
cmd_is_path,
|
|
WString::from(&[short_opt][..]),
|
|
CompleteOptionType::Short,
|
|
result_mode,
|
|
condition.to_vec(),
|
|
comp.to_owned(),
|
|
desc.to_owned(),
|
|
flags,
|
|
);
|
|
}
|
|
|
|
for gnu_opt in gnu_opts {
|
|
complete_add(
|
|
cmd.to_owned(),
|
|
cmd_is_path,
|
|
gnu_opt.to_owned(),
|
|
CompleteOptionType::DoubleLong,
|
|
result_mode,
|
|
condition.to_vec(),
|
|
comp.to_owned(),
|
|
desc.to_owned(),
|
|
flags,
|
|
);
|
|
}
|
|
|
|
for old_opt in old_opts {
|
|
complete_add(
|
|
cmd.to_owned(),
|
|
cmd_is_path,
|
|
old_opt.to_owned(),
|
|
CompleteOptionType::SingleLong,
|
|
result_mode,
|
|
condition.to_vec(),
|
|
comp.to_owned(),
|
|
desc.to_owned(),
|
|
flags,
|
|
);
|
|
}
|
|
|
|
if old_opts.is_empty() && gnu_opts.is_empty() && short_opt.is_empty() {
|
|
complete_add(
|
|
cmd.to_owned(),
|
|
cmd_is_path,
|
|
WString::new(),
|
|
CompleteOptionType::ArgsOnly,
|
|
result_mode,
|
|
condition.to_vec(),
|
|
comp.to_owned(),
|
|
desc.to_owned(),
|
|
flags,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Sily function.
|
|
fn builtin_complete_add(
|
|
cmds: &[WString],
|
|
paths: &[WString],
|
|
short_opt: &wstr,
|
|
gnu_opt: &[WString],
|
|
old_opt: &[WString],
|
|
result_mode: CompletionMode,
|
|
condition: &[WString],
|
|
comp: &wstr,
|
|
desc: &wstr,
|
|
flags: CompleteFlags,
|
|
) {
|
|
for cmd in cmds {
|
|
builtin_complete_add2(
|
|
cmd,
|
|
false, /* not path */
|
|
short_opt,
|
|
gnu_opt,
|
|
old_opt,
|
|
result_mode,
|
|
condition,
|
|
comp,
|
|
desc,
|
|
flags,
|
|
);
|
|
}
|
|
for path in paths {
|
|
builtin_complete_add2(
|
|
path,
|
|
true, /* is path */
|
|
short_opt,
|
|
gnu_opt,
|
|
old_opt,
|
|
result_mode,
|
|
condition,
|
|
comp,
|
|
desc,
|
|
flags,
|
|
);
|
|
}
|
|
}
|
|
|
|
fn builtin_complete_remove_cmd(
|
|
cmd: &WString,
|
|
cmd_is_path: bool,
|
|
short_opt: &wstr,
|
|
gnu_opt: &[WString],
|
|
old_opt: &[WString],
|
|
) {
|
|
let mut removed = false;
|
|
for s in short_opt.chars() {
|
|
complete_remove(
|
|
cmd.to_owned(),
|
|
cmd_is_path,
|
|
wstr::from_char_slice(&[s]),
|
|
CompleteOptionType::Short,
|
|
);
|
|
removed = true;
|
|
}
|
|
|
|
for opt in old_opt {
|
|
complete_remove(
|
|
cmd.to_owned(),
|
|
cmd_is_path,
|
|
opt,
|
|
CompleteOptionType::SingleLong,
|
|
);
|
|
removed = true;
|
|
}
|
|
|
|
for opt in gnu_opt {
|
|
complete_remove(
|
|
cmd.to_owned(),
|
|
cmd_is_path,
|
|
opt,
|
|
CompleteOptionType::DoubleLong,
|
|
);
|
|
removed = true;
|
|
}
|
|
|
|
if !removed {
|
|
// This means that all loops were empty.
|
|
complete_remove_all(cmd.to_owned(), cmd_is_path);
|
|
}
|
|
}
|
|
|
|
fn builtin_complete_remove(
|
|
cmds: &[WString],
|
|
paths: &[WString],
|
|
short_opt: &wstr,
|
|
gnu_opt: &[WString],
|
|
old_opt: &[WString],
|
|
) {
|
|
for cmd in cmds {
|
|
builtin_complete_remove_cmd(cmd, false /* not path */, short_opt, gnu_opt, old_opt);
|
|
}
|
|
|
|
for path in paths {
|
|
builtin_complete_remove_cmd(path, true /* is path */, short_opt, gnu_opt, old_opt);
|
|
}
|
|
}
|
|
|
|
fn builtin_complete_print(cmd: &wstr, streams: &mut IoStreams, parser: &Parser) {
|
|
let repr = complete_print(cmd);
|
|
|
|
// colorize if interactive
|
|
if !streams.out_is_redirected && isatty(STDOUT_FILENO) {
|
|
let mut colors = vec![];
|
|
highlight_shell(&repr, &mut colors, &parser.context(), false, None);
|
|
streams
|
|
.out
|
|
.append(str2wcstring(&colorize(&repr, &colors, parser.vars())));
|
|
} else {
|
|
streams.out.append(repr);
|
|
}
|
|
}
|
|
|
|
/// Values used for long-only options.
|
|
const OPT_ESCAPE: char = '\x01';
|
|
|
|
/// The complete builtin. Used for specifying programmable tab-completions. Calls the functions in
|
|
/// complete.cpp for any heavy lifting.
|
|
pub fn complete(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Option<c_int> {
|
|
let cmd = argv[0];
|
|
let argc = argv.len();
|
|
let mut result_mode = CompletionMode::default();
|
|
let mut remove = false;
|
|
let mut short_opt = WString::new();
|
|
// todo!("these whould be Vec<&wstr>");
|
|
let mut gnu_opt = vec![];
|
|
let mut old_opt = vec![];
|
|
let mut subcommand = vec![];
|
|
let mut comp = WString::new();
|
|
let mut desc = WString::new();
|
|
let mut condition = vec![];
|
|
let mut do_complete = false;
|
|
let mut do_complete_param = None;
|
|
let mut cmd_to_complete = vec![];
|
|
let mut path = vec![];
|
|
let mut wrap_targets = vec![];
|
|
let mut preserve_order = false;
|
|
let mut unescape_output = true;
|
|
|
|
const short_options: &wstr = L!(":a:c:p:s:l:o:d:fFrxeuAn:C::w:hk");
|
|
const long_options: &[WOption] = &[
|
|
wopt(L!("exclusive"), ArgType::NoArgument, 'x'),
|
|
wopt(L!("no-files"), ArgType::NoArgument, 'f'),
|
|
wopt(L!("force-files"), ArgType::NoArgument, 'F'),
|
|
wopt(L!("require-parameter"), ArgType::NoArgument, 'r'),
|
|
wopt(L!("path"), ArgType::RequiredArgument, 'p'),
|
|
wopt(L!("command"), ArgType::RequiredArgument, 'c'),
|
|
wopt(L!("short-option"), ArgType::RequiredArgument, 's'),
|
|
wopt(L!("long-option"), ArgType::RequiredArgument, 'l'),
|
|
wopt(L!("old-option"), ArgType::RequiredArgument, 'o'),
|
|
wopt(L!("subcommand"), ArgType::RequiredArgument, 'S'),
|
|
wopt(L!("description"), ArgType::RequiredArgument, 'd'),
|
|
wopt(L!("arguments"), ArgType::RequiredArgument, 'a'),
|
|
wopt(L!("erase"), ArgType::NoArgument, 'e'),
|
|
wopt(L!("unauthoritative"), ArgType::NoArgument, 'u'),
|
|
wopt(L!("authoritative"), ArgType::NoArgument, 'A'),
|
|
wopt(L!("condition"), ArgType::RequiredArgument, 'n'),
|
|
wopt(L!("wraps"), ArgType::RequiredArgument, 'w'),
|
|
wopt(L!("do-complete"), ArgType::OptionalArgument, 'C'),
|
|
wopt(L!("help"), ArgType::NoArgument, 'h'),
|
|
wopt(L!("keep-order"), ArgType::NoArgument, 'k'),
|
|
wopt(L!("escape"), ArgType::NoArgument, OPT_ESCAPE),
|
|
];
|
|
|
|
let mut have_x = false;
|
|
|
|
let mut w = WGetopter::new(short_options, long_options, argv);
|
|
while let Some(opt) = w.next_opt() {
|
|
match opt {
|
|
'x' => {
|
|
result_mode.no_files = true;
|
|
result_mode.requires_param = true;
|
|
// Needed to print an error later;
|
|
have_x = true;
|
|
}
|
|
'f' => {
|
|
result_mode.no_files = true;
|
|
}
|
|
'F' => {
|
|
result_mode.force_files = true;
|
|
}
|
|
'r' => {
|
|
result_mode.requires_param = true;
|
|
}
|
|
'k' => {
|
|
preserve_order = true;
|
|
}
|
|
'p' | 'c' => {
|
|
if let Some(tmp) = unescape_string(
|
|
w.woptarg.unwrap(),
|
|
UnescapeStringStyle::Script(UnescapeFlags::SPECIAL),
|
|
) {
|
|
if opt == 'p' {
|
|
path.push(tmp);
|
|
} else {
|
|
cmd_to_complete.push(tmp);
|
|
}
|
|
} else {
|
|
streams.err.append(wgettext_fmt!(
|
|
"%ls: Invalid token '%ls'\n",
|
|
cmd,
|
|
w.woptarg.unwrap()
|
|
));
|
|
return STATUS_INVALID_ARGS;
|
|
}
|
|
}
|
|
'd' => {
|
|
desc = w.woptarg.unwrap().to_owned();
|
|
}
|
|
'u' => {
|
|
// This option was removed in commit 1911298 and is now a no-op.
|
|
}
|
|
'A' => {
|
|
// This option was removed in commit 1911298 and is now a no-op.
|
|
}
|
|
's' => {
|
|
let arg = w.woptarg.unwrap();
|
|
short_opt.extend(arg.chars());
|
|
if arg.is_empty() {
|
|
streams
|
|
.err
|
|
.append(wgettext_fmt!("%ls: -s requires a non-empty string\n", cmd,));
|
|
return STATUS_INVALID_ARGS;
|
|
}
|
|
}
|
|
'l' => {
|
|
let arg = w.woptarg.unwrap();
|
|
gnu_opt.push(arg.to_owned());
|
|
if arg.is_empty() {
|
|
streams
|
|
.err
|
|
.append(wgettext_fmt!("%ls: -l requires a non-empty string\n", cmd,));
|
|
return STATUS_INVALID_ARGS;
|
|
}
|
|
}
|
|
'o' => {
|
|
let arg = w.woptarg.unwrap();
|
|
old_opt.push(arg.to_owned());
|
|
if arg.is_empty() {
|
|
streams
|
|
.err
|
|
.append(wgettext_fmt!("%ls: -o requires a non-empty string\n", cmd,));
|
|
return STATUS_INVALID_ARGS;
|
|
}
|
|
}
|
|
'S' => {
|
|
let arg = w.woptarg.unwrap();
|
|
subcommand.push(arg.to_owned());
|
|
if arg.is_empty() {
|
|
streams
|
|
.err
|
|
.append(wgettext_fmt!("%ls: -S requires a non-empty string\n", cmd,));
|
|
return STATUS_INVALID_ARGS;
|
|
}
|
|
}
|
|
'a' => {
|
|
comp = w.woptarg.unwrap().to_owned();
|
|
}
|
|
'e' => remove = true,
|
|
'n' => {
|
|
condition.push(w.woptarg.unwrap().to_owned());
|
|
}
|
|
'w' => {
|
|
wrap_targets.push(w.woptarg.unwrap().to_owned());
|
|
}
|
|
'C' => {
|
|
do_complete = true;
|
|
if let Some(s) = w.woptarg {
|
|
do_complete_param = Some(s.to_owned());
|
|
}
|
|
}
|
|
OPT_ESCAPE => {
|
|
unescape_output = false;
|
|
}
|
|
'h' => {
|
|
builtin_print_help(parser, streams, cmd);
|
|
return STATUS_CMD_OK;
|
|
}
|
|
':' => {
|
|
builtin_missing_argument(parser, streams, cmd, argv[w.wopt_index - 1], true);
|
|
return STATUS_INVALID_ARGS;
|
|
}
|
|
'?' => {
|
|
builtin_unknown_option(parser, streams, cmd, argv[w.wopt_index - 1], true);
|
|
return STATUS_INVALID_ARGS;
|
|
}
|
|
_ => panic!("unexpected retval from WGetopter"),
|
|
}
|
|
}
|
|
|
|
if result_mode.no_files && result_mode.force_files {
|
|
if !have_x {
|
|
streams.err.append(wgettext_fmt!(
|
|
BUILTIN_ERR_COMBO2,
|
|
"complete",
|
|
"'--no-files' and '--force-files'"
|
|
));
|
|
} else {
|
|
// The reason for us not wanting files is `-x`,
|
|
// which is short for `-rf`.
|
|
streams.err.append(wgettext_fmt!(
|
|
BUILTIN_ERR_COMBO2,
|
|
"complete",
|
|
"'--exclusive' and '--force-files'"
|
|
));
|
|
}
|
|
return STATUS_INVALID_ARGS;
|
|
}
|
|
|
|
if w.wopt_index != argc {
|
|
// Use one left-over arg as the do-complete argument
|
|
// to enable `complete -C "git check"`.
|
|
if do_complete && do_complete_param.is_none() && argc == w.wopt_index + 1 {
|
|
do_complete_param = Some(argv[argc - 1].to_owned());
|
|
} else if !do_complete && cmd_to_complete.is_empty() && argc == w.wopt_index + 1 {
|
|
// Or use one left-over arg as the command to complete
|
|
cmd_to_complete.push(argv[argc - 1].to_owned());
|
|
} else {
|
|
streams
|
|
.err
|
|
.append(wgettext_fmt!(BUILTIN_ERR_TOO_MANY_ARGUMENTS, cmd));
|
|
builtin_print_error_trailer(parser, streams.err, cmd);
|
|
return STATUS_INVALID_ARGS;
|
|
}
|
|
}
|
|
|
|
for condition_string in &condition {
|
|
let mut errors = ParseErrorList::new();
|
|
if parse_util_detect_errors(condition_string, Some(&mut errors), false).is_err() {
|
|
for error in errors {
|
|
let prefix = cmd.to_owned() + L!(": -n '") + &condition_string[..] + L!("': ");
|
|
streams.err.append(error.describe_with_prefix(
|
|
condition_string,
|
|
&prefix,
|
|
parser.is_interactive(),
|
|
false,
|
|
));
|
|
streams.err.push('\n');
|
|
}
|
|
return STATUS_CMD_ERROR;
|
|
}
|
|
}
|
|
|
|
if !comp.is_empty() {
|
|
let mut prefix = WString::new();
|
|
prefix.push_utfstr(cmd);
|
|
prefix.push_str(": ");
|
|
|
|
if let Err(err_text) = parse_util_detect_errors_in_argument_list(&comp, &prefix) {
|
|
streams.err.append(wgettext_fmt!(
|
|
"%ls: %ls: contains a syntax error\n",
|
|
cmd,
|
|
comp
|
|
));
|
|
streams.err.append(err_text);
|
|
streams.err.push('\n');
|
|
return STATUS_CMD_ERROR;
|
|
}
|
|
}
|
|
|
|
if do_complete {
|
|
let have_do_complete_param = do_complete_param.is_some();
|
|
let do_complete_param = match do_complete_param {
|
|
None => {
|
|
// No argument given, try to use the current commandline.
|
|
let commandline_state = commandline_get_state(true);
|
|
if !commandline_state.initialized {
|
|
// This corresponds to using 'complete -C' in non-interactive mode.
|
|
// See #2361 .
|
|
builtin_missing_argument(parser, streams, cmd, L!("-C"), true);
|
|
return STATUS_INVALID_ARGS;
|
|
}
|
|
commandline_state.text
|
|
}
|
|
Some(param) => param,
|
|
};
|
|
|
|
let mut token = 0..0;
|
|
parse_util_token_extent(
|
|
&do_complete_param,
|
|
do_complete_param.len(),
|
|
&mut token,
|
|
None,
|
|
);
|
|
|
|
// Create a scoped transient command line, so that builtin_commandline will see our
|
|
// argument, not the reader buffer.
|
|
parser
|
|
.libdata_mut()
|
|
.transient_commandlines
|
|
.push(do_complete_param.clone());
|
|
let _remove_transient = ScopeGuard::new((), |()| {
|
|
parser.libdata_mut().transient_commandlines.pop();
|
|
});
|
|
|
|
// Prevent accidental recursion (see #6171).
|
|
if !parser.libdata().builtin_complete_current_commandline {
|
|
if !have_do_complete_param {
|
|
parser.libdata_mut().builtin_complete_current_commandline = true;
|
|
}
|
|
|
|
let (mut comp, _needs_load) = crate::complete::complete(
|
|
&do_complete_param,
|
|
CompletionRequestOptions::normal(),
|
|
&parser.context(),
|
|
);
|
|
|
|
// Apply the same sort and deduplication treatment as pager completions
|
|
crate::complete::sort_and_prioritize(&mut comp, CompletionRequestOptions::default());
|
|
|
|
for next in comp {
|
|
// Make a fake commandline, and then apply the completion to it.
|
|
let faux_cmdline = &do_complete_param[token.clone()];
|
|
let mut tmp_cursor = faux_cmdline.len();
|
|
let mut faux_cmdline_with_completion = completion_apply_to_command_line(
|
|
&next.completion,
|
|
next.flags,
|
|
faux_cmdline,
|
|
&mut tmp_cursor,
|
|
false,
|
|
);
|
|
|
|
// completion_apply_to_command_line will append a space unless COMPLETE_NO_SPACE
|
|
// is set. We don't want to set COMPLETE_NO_SPACE because that won't close
|
|
// quotes. What we want is to close the quote, but not append the space. So we
|
|
// just look for the space and clear it.
|
|
if !next.flags.contains(CompleteFlags::NO_SPACE)
|
|
&& string_suffixes_string(L!(" "), &faux_cmdline_with_completion)
|
|
{
|
|
faux_cmdline_with_completion.truncate(faux_cmdline_with_completion.len() - 1);
|
|
}
|
|
|
|
if unescape_output {
|
|
// The input data is meant to be something like you would have on the command
|
|
// line, e.g. includes backslashes. The output should be raw, i.e. unescaped. So
|
|
// we need to unescape the command line. See #1127.
|
|
faux_cmdline_with_completion = unescape_string(
|
|
&faux_cmdline_with_completion,
|
|
UnescapeStringStyle::Script(UnescapeFlags::default()),
|
|
)
|
|
.expect("Unescaping commandline to complete failed");
|
|
}
|
|
|
|
// Append any description.
|
|
if !next.description.is_empty() {
|
|
faux_cmdline_with_completion
|
|
.reserve(faux_cmdline_with_completion.len() + 2 + next.description.len());
|
|
faux_cmdline_with_completion.push('\t');
|
|
faux_cmdline_with_completion.push_utfstr(&next.description);
|
|
}
|
|
faux_cmdline_with_completion.push('\n');
|
|
streams.out.append(faux_cmdline_with_completion);
|
|
}
|
|
|
|
parser.libdata_mut().builtin_complete_current_commandline = false;
|
|
}
|
|
} else if path.is_empty()
|
|
&& gnu_opt.is_empty()
|
|
&& short_opt.is_empty()
|
|
&& old_opt.is_empty()
|
|
&& !remove
|
|
&& comp.is_empty()
|
|
&& desc.is_empty()
|
|
&& condition.is_empty()
|
|
&& wrap_targets.is_empty()
|
|
&& !result_mode.no_files
|
|
&& !result_mode.force_files
|
|
&& !result_mode.requires_param
|
|
{
|
|
// No arguments that would add or remove anything specified, so we print the definitions of
|
|
// all matching completions.
|
|
if cmd_to_complete.is_empty() {
|
|
builtin_complete_print(L!(""), streams, parser);
|
|
} else {
|
|
for cmd in cmd_to_complete {
|
|
builtin_complete_print(&cmd, streams, parser);
|
|
}
|
|
}
|
|
} else {
|
|
let mut flags = CompleteFlags::AUTO_SPACE;
|
|
// HACK: Don't escape tildes because at the beginning of a token they probably mean
|
|
// $HOME, for example as produced by a recursive call to "complete -C".
|
|
flags |= CompleteFlags::DONT_ESCAPE_TILDES;
|
|
if preserve_order {
|
|
flags |= CompleteFlags::DONT_SORT;
|
|
}
|
|
|
|
if remove {
|
|
builtin_complete_remove(&cmd_to_complete, &path, &short_opt, &gnu_opt, &old_opt);
|
|
} else {
|
|
builtin_complete_add(
|
|
&cmd_to_complete,
|
|
&path,
|
|
&short_opt,
|
|
&gnu_opt,
|
|
&old_opt,
|
|
result_mode,
|
|
&condition,
|
|
&comp,
|
|
&desc,
|
|
flags,
|
|
);
|
|
}
|
|
|
|
// Handle wrap targets (probably empty). We only wrap commands, not paths.
|
|
for wrap_target in wrap_targets {
|
|
for i in &cmd_to_complete {
|
|
if remove {
|
|
complete_remove_wrapper(i.clone(), &wrap_target);
|
|
} else {
|
|
complete_add_wrapper(i.clone(), wrap_target.clone());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
STATUS_CMD_OK
|
|
}
|