diff --git a/CMakeLists.txt b/CMakeLists.txt index e0937213b..35d54561c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -99,7 +99,7 @@ endif() # List of sources for builtin functions. set(FISH_BUILTIN_SRCS - src/builtin.cpp src/builtins/argparse.cpp src/builtins/bind.cpp + src/builtin.cpp src/builtins/bind.cpp src/builtins/cd.cpp src/builtins/commandline.cpp src/builtins/complete.cpp src/builtins/disown.cpp diff --git a/fish-rust/src/builtins/argparse.rs b/fish-rust/src/builtins/argparse.rs new file mode 100644 index 000000000..e841ec54e --- /dev/null +++ b/fish-rust/src/builtins/argparse.rs @@ -0,0 +1,1025 @@ +use std::array; +use std::collections::HashMap; + +use crate::builtins::shared::builtin_print_error_trailer; +use crate::builtins::shared::{ + builtin_missing_argument, builtin_print_help, builtin_unknown_option, io_streams_t, + BUILTIN_ERR_MAX_ARG_COUNT1, BUILTIN_ERR_MIN_ARG_COUNT1, BUILTIN_ERR_UNKNOWN, STATUS_CMD_ERROR, + STATUS_CMD_OK, STATUS_INVALID_ARGS, +}; +use crate::env::EnvMode; +use crate::ffi::env_stack_t; +use crate::ffi::parser_t; +use crate::ffi::Repin; +use crate::wchar::{wstr, WString, L}; +use crate::wcstringutil::split_string; +use crate::wgetopt::{wgetopter_t, wopt, woption, woption_argument_t}; +use crate::wutil::{fish_iswalnum, fish_wcstol, wgettext_fmt}; +use libc::c_int; + +const VAR_NAME_PREFIX: &wstr = L!("_flag_"); + +const BUILTIN_ERR_INVALID_OPT_SPEC: &str = "%ls: Invalid option spec '%ls' at char '%lc'\n"; + +#[derive(PartialEq)] +enum ArgCardinality { + Optional = -1isize, + None = 0, + Once = 1, + AtLeastOnce = 2, +} + +impl Default for ArgCardinality { + fn default() -> Self { + Self::None + } +} + +#[derive(Default)] +struct OptionSpec<'args> { + short_flag: char, + long_flag: &'args wstr, + validation_command: &'args wstr, + vals: Vec, + short_flag_valid: bool, + num_allowed: ArgCardinality, + num_seen: isize, +} + +impl OptionSpec<'_> { + fn new(s: char) -> Self { + Self { + short_flag: s, + short_flag_valid: true, + ..Default::default() + } + } +} + +#[derive(Default)] +struct ArgParseCmdOpts<'args> { + ignore_unknown: bool, + print_help: bool, + stop_nonopt: bool, + min_args: usize, + max_args: usize, + implicit_int_flag: char, + name: WString, + raw_exclusive_flags: Vec<&'args wstr>, + args: Vec<&'args wstr>, + options: HashMap>, + long_to_short_flag: HashMap, + exclusive_flag_sets: Vec>, +} + +impl ArgParseCmdOpts<'_> { + fn new() -> Self { + Self { + max_args: usize::MAX, + ..Default::default() + } + } +} + +const SHORT_OPTIONS: &wstr = L!("+:hn:six:N:X:"); +const LONG_OPTIONS: &[woption] = &[ + wopt(L!("stop-nonopt"), woption_argument_t::no_argument, 's'), + wopt(L!("ignore-unknown"), woption_argument_t::no_argument, 'i'), + wopt(L!("name"), woption_argument_t::required_argument, 'n'), + wopt(L!("exclusive"), woption_argument_t::required_argument, 'x'), + wopt(L!("help"), woption_argument_t::no_argument, 'h'), + wopt(L!("min-args"), woption_argument_t::required_argument, 'N'), + wopt(L!("max-args"), woption_argument_t::required_argument, 'X'), +]; + +fn exec_subshell( + cmd: &wstr, + parser: &mut parser_t, + outputs: &mut Vec, + apply_exit_status: bool, +) -> Option { + use crate::ffi::exec_subshell_ffi; + use crate::wchar_ffi::wcstring_list_ffi_t; + use crate::wchar_ffi::WCharFromFFI; + use crate::wchar_ffi::WCharToFFI; + + let mut cmd_output: cxx::UniquePtr = wcstring_list_ffi_t::create(); + let retval = Some( + exec_subshell_ffi( + cmd.to_ffi().as_ref().unwrap(), + parser.pin(), + cmd_output.pin_mut(), + apply_exit_status, + ) + .into(), + ); + *outputs = cmd_output.as_mut().unwrap().from_ffi(); + retval +} + +fn check_for_mutually_exclusive_flags( + opts: &ArgParseCmdOpts, + streams: &mut io_streams_t, +) -> Option { + for opt_spec in opts.options.values() { + if opt_spec.num_seen == 0 { + continue; + } + + // We saw this option at least once. Check all the sets of mutually exclusive options to see + // if this option appears in any of them. + for xarg_set in &opts.exclusive_flag_sets { + if xarg_set.contains(&opt_spec.short_flag) { + // Okay, this option is in a mutually exclusive set of options. Check if any of the + // other mutually exclusive options have been seen. + for xflag in xarg_set { + let Some(xopt_spec) = opts.options.get(xflag) else { + continue; + }; + + // Ignore this flag in the list of mutually exclusive flags. + if xopt_spec.short_flag == opt_spec.short_flag { + continue; + } + + // If it is a different flag check if it has been seen. + if xopt_spec.num_seen != 0 { + let mut flag1: WString = WString::new(); + if opt_spec.short_flag_valid { + flag1.push(opt_spec.short_flag); + } + if !opt_spec.long_flag.is_empty() { + if opt_spec.short_flag_valid { + flag1.push('/'); + } + flag1.push_utfstr(&opt_spec.long_flag); + } + + let mut flag2: WString = WString::new(); + if xopt_spec.short_flag_valid { + flag2.push(xopt_spec.short_flag); + } + if !xopt_spec.long_flag.is_empty() { + if xopt_spec.short_flag_valid { + flag2.push('/'); + } + flag2.push_utfstr(&xopt_spec.long_flag); + } + + // We want the flag order to be deterministic. Primarily to make unit + // testing easier. + if flag1 > flag2 { + std::mem::swap(&mut flag1, &mut flag2); + } + streams.err.append(wgettext_fmt!( + "%ls: %ls %ls: options cannot be used together\n", + opts.name, + flag1, + flag2 + )); + return STATUS_CMD_ERROR; + } + } + } + } + } + + return STATUS_CMD_OK; +} + +// This should be called after all the option specs have been parsed. At that point we have enough +// information to parse the values associated with any `--exclusive` flags. +fn parse_exclusive_args(opts: &mut ArgParseCmdOpts, streams: &mut io_streams_t) -> Option { + for raw_xflags in &opts.raw_exclusive_flags { + let xflags = split_string(raw_xflags, ','); + if xflags.len() < 2 { + streams.err.append(wgettext_fmt!( + "%ls: exclusive flag string '%ls' is not valid\n", + opts.name, + raw_xflags + )); + return STATUS_CMD_ERROR; + } + + let exclusive_set: &mut Vec = &mut vec![]; + for flag in &xflags { + if flag.len() == 1 && opts.options.contains_key(&flag.as_char_slice()[0]) { + let short = flag.as_char_slice()[0]; + // It's a short flag. + exclusive_set.push(short); + } else if let Some(short_equiv) = opts.long_to_short_flag.get(flag) { + // It's a long flag we store as its short flag equivalent. + exclusive_set.push(*short_equiv); + } else { + streams.err.append(wgettext_fmt!( + "%ls: exclusive flag '%ls' is not valid\n", + opts.name, + flag + )); + return STATUS_CMD_ERROR; + } + } + + // Store the set of exclusive flags for use when parsing the supplied set of arguments. + opts.exclusive_flag_sets.push(exclusive_set.to_vec()); + } + return STATUS_CMD_OK; +} + +fn parse_flag_modifiers<'args>( + opts: &ArgParseCmdOpts<'args>, + opt_spec: &mut OptionSpec<'args>, + option_spec: &wstr, + opt_spec_str: &mut &'args [char], + streams: &mut io_streams_t, +) -> bool { + let s = *opt_spec_str; + let mut i = 0usize; + + if opt_spec.short_flag == opts.implicit_int_flag && i < s.len() && s[i] != '!' { + streams.err.append(wgettext_fmt!( + "%ls: Implicit int short flag '%lc' does not allow modifiers like '%lc'\n", + opts.name, + opt_spec.short_flag, + s[i] + )); + return false; + } + + if Some(&'=') == s.get(i) { + i += 1; + opt_spec.num_allowed = match s.get(i) { + Some(&'?') => ArgCardinality::Optional, + Some(&'+') => ArgCardinality::AtLeastOnce, + _ => ArgCardinality::Once, + }; + if opt_spec.num_allowed != ArgCardinality::Once { + i += 1; + } + } + + if Some(&'!') == s.get(i) { + i += 1; + opt_spec.validation_command = wstr::from_char_slice(&s[i..]); + // Move cursor to the end so we don't expect a long flag. + i = s.len(); + } else if i < s.len() { + streams.err.append(wgettext_fmt!( + BUILTIN_ERR_INVALID_OPT_SPEC, + opts.name, + option_spec, + s[i] + )); + return false; + } + + // Make sure we have some validation for implicit int flags. + if opt_spec.short_flag == opts.implicit_int_flag && opt_spec.validation_command.is_empty() { + opt_spec.validation_command = L!("_validate_int"); + } + + if opts.options.contains_key(&opt_spec.short_flag) { + streams.err.append(wgettext_fmt!( + "%ls: Short flag '%lc' already defined\n", + opts.name, + opt_spec.short_flag + )); + return false; + } + + *opt_spec_str = &s[i..]; + return true; +} + +/// Parse the text following the short flag letter. +fn parse_option_spec_sep<'args>( + opts: &mut ArgParseCmdOpts<'args>, + opt_spec: &mut OptionSpec<'args>, + option_spec: &'args wstr, + opt_spec_str: &mut &'args [char], + counter: &mut u32, + streams: &mut io_streams_t, +) -> bool { + let mut s = *opt_spec_str; + let mut i = 1usize; + // C++ used -1 to check for # here, we instead adjust opt_spec_str to start one earlier + if s[i - 1] == '#' { + if s[i] != '-' { + // Long-only! + i -= 1; + opt_spec.short_flag = char::from_u32(*counter).unwrap(); + *counter += 1; + } + if opts.implicit_int_flag != '\0' { + streams.err.append(wgettext_fmt!( + "%ls: Implicit int flag '%lc' already defined\n", + opts.name, + opts.implicit_int_flag + )); + return false; + } + opts.implicit_int_flag = opt_spec.short_flag; + opt_spec.short_flag_valid = false; + i += 1; + *opt_spec_str = &s[i..]; + return true; + } + + match s[i] { + '-' => { + opt_spec.short_flag_valid = false; + i += 1; + if i == s.len() { + streams.err.append(wgettext_fmt!( + BUILTIN_ERR_INVALID_OPT_SPEC, + opts.name, + option_spec, + s[i - 1] + )); + return false; + } + } + '/' => { + i += 1; // the struct is initialized assuming short_flag_valid should be true + if i == s.len() { + streams.err.append(wgettext_fmt!( + BUILTIN_ERR_INVALID_OPT_SPEC, + opts.name, + option_spec, + s[i - 1] + )); + return false; + } + } + '#' => { + if opts.implicit_int_flag != '\0' { + streams.err.append(wgettext_fmt!( + "%ls: Implicit int flag '%lc' already defined\n", + opts.name, + opts.implicit_int_flag + )); + return false; + } + opts.implicit_int_flag = opt_spec.short_flag; + opt_spec.num_allowed = ArgCardinality::Once; + i += 1; // the struct is initialized assuming short_flag_valid should be true + } + '!' | '?' | '=' => { + // Try to parse any other flag modifiers + // parse_flag_modifiers assumes opt_spec_str starts where it should, not one earlier + s = &s[i..]; + if !parse_flag_modifiers(opts, opt_spec, option_spec, &mut s, streams) { + return false; + } + i = 0; + } + _ => { + // No short flag separator and no other modifiers, so this is a long only option. + // Since getopt needs a wchar, we have a counter that we count up. + opt_spec.short_flag_valid = false; + i -= 1; + opt_spec.short_flag = char::from_u32(*counter).unwrap(); + *counter += 1; + } + } + + *opt_spec_str = &s[i..]; + return true; +} + +fn parse_option_spec<'args>( + opts: &mut ArgParseCmdOpts<'args>, + option_spec: &'args wstr, + counter: &mut u32, + streams: &mut io_streams_t, +) -> bool { + if option_spec.is_empty() { + streams.err.append(wgettext_fmt!( + "%ls: An option spec must have at least a short or a long flag\n", + opts.name + )); + return false; + } + + let s = &mut option_spec.as_char_slice(); + let mut i = 0usize; + if !fish_iswalnum(s[i]) && s[i] != '#' { + streams.err.append(wgettext_fmt!( + "%ls: Short flag '%lc' invalid, must be alphanum or '#'\n", + opts.name, + s[i] + )); + return false; + } + + let mut opt_spec = OptionSpec::new(s[i]); + + // Try parsing stuff after the short flag. + if i + 1 < s.len() + && !parse_option_spec_sep(opts, &mut opt_spec, option_spec, s, counter, streams) + { + return false; + } + + // Collect any long flag name. + if i < s.len() { + let long_flag_end: usize = s[i..] + .iter() + .enumerate() + .find_map(|(idx, c)| { + if *c == '-' || *c == '_' || fish_iswalnum(*c) { + None + } else { + Some(idx + i) + } + }) + .unwrap_or(s.len()); + + if long_flag_end != i { + opt_spec.long_flag = wstr::from_char_slice(&s[i..long_flag_end]); + if opts.long_to_short_flag.contains_key(opt_spec.long_flag) { + streams.err.append(wgettext_fmt!( + "%ls: Long flag '%ls' already defined\n", + opts.name, + opt_spec.long_flag + )); + return false; + } + } + i = long_flag_end; + } + + *s = &s[i..]; + if !parse_flag_modifiers(opts, &mut opt_spec, option_spec, s, streams) { + return false; + } + + // Record our long flag if we have one. + if !opt_spec.long_flag.is_empty() { + let ins = opts + .long_to_short_flag + .insert(WString::from(opt_spec.long_flag), opt_spec.short_flag); + assert!(ins.is_none(), "Should have inserted long flag"); + } + + // Record our option under its short flag. + opts.options.insert(opt_spec.short_flag, opt_spec); + + return true; +} + +fn collect_option_specs<'args>( + opts: &mut ArgParseCmdOpts<'args>, + optind: &mut usize, + argc: usize, + args: &[&'args wstr], + streams: &mut io_streams_t, +) -> Option { + let cmd: &wstr = args[0]; + + // A counter to give short chars to long-only options because getopt needs that. + // Luckily we have wgetopt so we can use wchars - this is one of the private use areas so we + // have 6400 options available. + let mut counter = 0xE000u32; + + loop { + if *optind == argc { + streams + .err + .append(wgettext_fmt!("%ls: Missing -- separator\n", cmd)); + return STATUS_INVALID_ARGS; + } + + if L!("--") == args[*optind] { + *optind += 1; + break; + } + + if !parse_option_spec(opts, args[*optind], &mut counter, streams) { + return STATUS_CMD_ERROR; + } + + *optind += 1; + } + + // Check for counter overreach once at the end because this is very unlikely to ever be reached. + let counter_max = 0xF8FFu32; + + if counter > counter_max { + streams + .err + .append(wgettext_fmt!("%ls: Too many long-only options\n", cmd)); + return STATUS_INVALID_ARGS; + } + + return STATUS_CMD_OK; +} + +fn parse_cmd_opts<'args>( + opts: &mut ArgParseCmdOpts<'args>, + optind: &mut usize, + argc: usize, + args: &mut [&'args wstr], + parser: &mut parser_t, + streams: &mut io_streams_t, +) -> Option { + let cmd = args[0]; + + let mut args_read = Vec::with_capacity(args.len()); + args_read.extend_from_slice(args); + + let mut w = wgetopter_t::new(SHORT_OPTIONS, LONG_OPTIONS, args); + while let Some(c) = w.wgetopt_long() { + match c { + 'n' => opts.name = w.woptarg.unwrap().to_owned(), + 's' => opts.stop_nonopt = true, + 'i' => opts.ignore_unknown = true, + // Just save the raw string here. Later, when we have all the short and long flag + // definitions we'll parse these strings into a more useful data structure. + 'x' => opts.raw_exclusive_flags.push(w.woptarg.unwrap()), + 'h' => opts.print_help = true, + 'N' => { + opts.min_args = { + let x = fish_wcstol(w.woptarg.unwrap()).unwrap_or(-1); + if x < 0 { + streams.err.append(wgettext_fmt!( + "%ls: Invalid --min-args value '%ls'\n", + cmd, + w.woptarg.unwrap() + )); + return STATUS_INVALID_ARGS; + } + x.try_into().unwrap() + } + } + 'X' => { + opts.max_args = { + let x = fish_wcstol(w.woptarg.unwrap()).unwrap_or(-1); + if x < 0 { + streams.err.append(wgettext_fmt!( + "%ls: Invalid --max-args value '%ls'\n", + cmd, + w.woptarg.unwrap() + )); + return STATUS_INVALID_ARGS; + } + x.try_into().unwrap() + } + } + ':' => { + builtin_missing_argument( + parser, + streams, + cmd, + args[w.woptind - 1], + /* print_hints */ false, + ); + return STATUS_INVALID_ARGS; + } + '?' => { + builtin_unknown_option(parser, streams, cmd, args[w.woptind - 1], false); + return STATUS_INVALID_ARGS; + } + _ => panic!("unexpected retval from wgetopt_long"), + } + } + + if opts.print_help { + return STATUS_CMD_OK; + } + + if L!("--") == args_read[w.woptind - 1] { + w.woptind -= 1; + } + + if argc == w.woptind { + // The user didn't specify any option specs. + streams + .err + .append(wgettext_fmt!("%ls: Missing -- separator\n", cmd)); + return STATUS_INVALID_ARGS; + } + + if opts.name.is_empty() { + // If no name has been given, we default to the function name. + // If any error happens, the backtrace will show which argparse it was. + opts.name = parser + .get_func_name(1) + .unwrap_or_else(|| L!("argparse").to_owned()); + } + + *optind = w.woptind; + return collect_option_specs(opts, optind, argc, args, streams); +} + +fn populate_option_strings<'args>( + opts: &ArgParseCmdOpts<'args>, + short_options: &mut WString, + long_options: &mut Vec>, +) { + for opt_spec in opts.options.values() { + if opt_spec.short_flag_valid { + short_options.push(opt_spec.short_flag); + } + + let arg_type = match opt_spec.num_allowed { + ArgCardinality::Optional => { + if opt_spec.short_flag_valid { + short_options.push_str("::"); + } + woption_argument_t::optional_argument + } + ArgCardinality::Once | ArgCardinality::AtLeastOnce => { + if opt_spec.short_flag_valid { + short_options.push_str(":"); + } + woption_argument_t::required_argument + } + ArgCardinality::None => woption_argument_t::no_argument, + }; + + if !opt_spec.long_flag.is_empty() { + long_options.push(wopt(opt_spec.long_flag, arg_type, opt_spec.short_flag)); + } + } +} + +fn validate_arg<'opts>( + parser: &mut parser_t, + opts_name: &wstr, + opt_spec: &mut OptionSpec<'opts>, + is_long_flag: bool, + woptarg: &'opts wstr, + streams: &mut io_streams_t, +) -> Option { + // Obviously if there is no arg validation command we assume the arg is okay. + if opt_spec.validation_command.is_empty() { + return STATUS_CMD_OK; + } + + parser.get_var_stack().pin().push(true); + + let env_mode = EnvMode::LOCAL | EnvMode::EXPORT; + parser.set_var( + L!("_argparse_cmd"), + array::from_ref(&opts_name).as_slice(), + env_mode, + ); + let flag_name = WString::from(VAR_NAME_PREFIX) + "name"; + if is_long_flag { + parser.set_var( + flag_name, + array::from_ref(&opt_spec.long_flag).as_slice(), + env_mode, + ); + } else { + parser.set_var( + flag_name, + array::from_ref(&wstr::from_char_slice(&[opt_spec.short_flag])).as_slice(), + env_mode, + ); + } + parser.set_var( + WString::from(VAR_NAME_PREFIX) + "value", + array::from_ref(&woptarg).as_slice(), + env_mode, + ); + + let mut cmd_output = Vec::new(); + + let retval = exec_subshell(opt_spec.validation_command, parser, &mut cmd_output, false); + + for output in cmd_output { + streams.err.append(output); + streams.err.append1('\n'); + } + parser.get_var_stack().pin().pop(); + return retval; +} + +/// \return whether the option 'opt' is an implicit integer option. +fn is_implicit_int(opts: &ArgParseCmdOpts, val: &wstr) -> bool { + if opts.implicit_int_flag == '\0' { + // There is no implicit integer option. + return false; + } + + // We succeed if this argument can be parsed as an integer. + fish_wcstol(val).is_ok() +} + +// Store this value under the implicit int option. +fn validate_and_store_implicit_int<'args>( + parser: &mut parser_t, + opts: &mut ArgParseCmdOpts<'args>, + val: &'args wstr, + w: &mut wgetopter_t, + is_long_flag: bool, + streams: &mut io_streams_t, +) -> Option { + let opt_spec = opts.options.get_mut(&opts.implicit_int_flag).unwrap(); + let retval = validate_arg(parser, &opts.name, opt_spec, is_long_flag, val, streams); + + if retval != STATUS_CMD_OK { + return retval; + } + + // It's a valid integer so store it and return success. + opt_spec.vals.clear(); + opt_spec.vals.push(val.into()); + opt_spec.num_seen += 1; + w.nextchar = L!(""); + + return STATUS_CMD_OK; +} + +fn handle_flag<'args>( + parser: &mut parser_t, + opts: &mut ArgParseCmdOpts<'args>, + opt: char, + is_long_flag: bool, + woptarg: Option<&'args wstr>, + streams: &mut io_streams_t, +) -> Option { + let opt_spec = opts.options.get_mut(&opt).unwrap(); + + opt_spec.num_seen += 1; + if opt_spec.num_allowed == ArgCardinality::None { + // It's a boolean flag. Save the flag we saw since it might be useful to know if the + // short or long flag was given. + assert!(woptarg.is_none()); + let s = if is_long_flag { + WString::from("--") + opt_spec.long_flag + } else { + WString::from_chars(['-', opt_spec.short_flag]) + }; + opt_spec.vals.push(s); + return STATUS_CMD_OK; + } + + if let Some(woptarg) = woptarg { + let retval = validate_arg(parser, &opts.name, opt_spec, is_long_flag, woptarg, streams); + if retval != STATUS_CMD_OK { + return retval; + } + } + + match opt_spec.num_allowed { + ArgCardinality::Optional | ArgCardinality::Once => { + // We're depending on `wgetopt_long()` to report that a mandatory value is missing if + // `opt_spec->num_allowed == 1` and thus return ':' so that we don't take this branch if + // the mandatory arg is missing. + opt_spec.vals.clear(); + if let Some(arg) = woptarg { + opt_spec.vals.push(arg.into()); + } + } + _ => { + opt_spec.vals.push(woptarg.unwrap().into()); + } + } + + return STATUS_CMD_OK; +} + +fn argparse_parse_flags<'args>( + parser: &mut parser_t, + opts: &mut ArgParseCmdOpts<'args>, + argc: usize, + args: &mut [&'args wstr], + optind: &mut usize, + streams: &mut io_streams_t, +) -> Option { + let mut args_read = Vec::with_capacity(args.len()); + args_read.extend_from_slice(args); + + // "+" means stop at nonopt, "-" means give nonoptions the option character code `1`, and don't + // reorder. + let mut short_options = WString::from(if opts.stop_nonopt { L!("+:") } else { L!("-:") }); + let mut long_options = vec![]; + populate_option_strings(opts, &mut short_options, &mut long_options); + + let mut long_idx: usize = usize::MAX; + let mut w = wgetopter_t::new(&short_options, &long_options, args); + while let Some(opt) = w.wgetopt_long_idx(&mut long_idx) { + let retval = match opt { + ':' => { + builtin_missing_argument( + parser, + streams, + &opts.name, + args_read[w.woptind - 1], + false, + ); + STATUS_INVALID_ARGS + } + '?' => { + // It's not a recognized flag. See if it's an implicit int flag. + let arg_contents = &args_read[w.woptind - 1][1..]; + + if is_implicit_int(opts, arg_contents) { + validate_and_store_implicit_int( + parser, + opts, + arg_contents, + &mut w, + long_idx != usize::MAX, + streams, + ) + } else if !opts.ignore_unknown { + streams.err.append(wgettext_fmt!( + BUILTIN_ERR_UNKNOWN, + opts.name, + args_read[w.woptind - 1] + )); + STATUS_INVALID_ARGS + } else { + // Any unrecognized option is put back if ignore_unknown is used. + // This allows reusing the same argv in multiple argparse calls, + // or just ignoring the error (e.g. in completions). + opts.args.push(args_read[w.woptind - 1]); + // Work around weirdness with wgetopt, which crashes if we `continue` here. + if w.woptind == argc { + break; + } + // Explain to wgetopt that we want to skip to the next arg, + // because we can't handle this opt group. + w.nextchar = L!(""); + STATUS_CMD_OK + } + } + '\u{1}' => { + // A non-option argument. + // We use `-` as the first option-string-char to disable GNU getopt's reordering, + // otherwise we'd get ignored options first and normal arguments later. + // E.g. `argparse -i -- -t tango -w` needs to keep `-t tango -w` in $argv, not `-t -w + // tango`. + opts.args.push(args_read[w.woptind - 1]); + continue; + } + // It's a recognized flag. + _ => handle_flag( + parser, + opts, + opt, + long_idx != usize::MAX, + w.woptarg, + streams, + ), + }; + if retval != STATUS_CMD_OK { + return retval; + } + long_idx = usize::MAX; + } + + *optind = w.woptind; + return STATUS_CMD_OK; +} + +// This function mimics the `wgetopt_long()` usage found elsewhere in our other builtin commands. +// It's different in that the short and long option structures are constructed dynamically based on +// arguments provided to the `argparse` command. +fn argparse_parse_args<'args>( + opts: &mut ArgParseCmdOpts<'args>, + args: &mut [&'args wstr], + argc: usize, + parser: &mut parser_t, + streams: &mut io_streams_t, +) -> Option { + if argc <= 1 { + return STATUS_CMD_OK; + } + + let mut optind = 0usize; + let retval = argparse_parse_flags(parser, opts, argc, args, &mut optind, streams); + if retval != STATUS_CMD_OK { + return retval; + } + + let retval = check_for_mutually_exclusive_flags(opts, streams); + if retval != STATUS_CMD_OK { + return retval; + } + + opts.args.extend_from_slice(&args[optind..]); + + return STATUS_CMD_OK; +} + +fn check_min_max_args_constraints( + opts: &ArgParseCmdOpts, + streams: &mut io_streams_t, +) -> Option { + let cmd = &opts.name; + + if opts.args.len() < opts.min_args { + streams.err.append(wgettext_fmt!( + BUILTIN_ERR_MIN_ARG_COUNT1, + cmd, + opts.min_args, + opts.args.len() + )); + return STATUS_CMD_ERROR; + } + + if opts.max_args != usize::MAX && opts.args.len() > opts.max_args { + streams.err.append(wgettext_fmt!( + BUILTIN_ERR_MAX_ARG_COUNT1, + cmd, + opts.max_args, + opts.args.len() + )); + return STATUS_CMD_ERROR; + } + + return STATUS_CMD_OK; +} + +/// Put the result of parsing the supplied args into the caller environment as local vars. +fn set_argparse_result_vars(vars: &mut env_stack_t, opts: &ArgParseCmdOpts) { + for opt_spec in opts.options.values() { + if opt_spec.num_seen == 0 { + continue; + } + + if opt_spec.short_flag_valid { + let mut var_name = WString::from(VAR_NAME_PREFIX); + var_name.push(opt_spec.short_flag); + vars.set_var(&var_name, opt_spec.vals.as_slice(), EnvMode::LOCAL); + } + + if !opt_spec.long_flag.is_empty() { + // We do a simple replacement of all non alphanum chars rather than calling + // escape_string(long_flag, 0, STRING_STYLE_VAR). + let long_flag = opt_spec + .long_flag + .chars() + .map(|c| if fish_iswalnum(c) { c } else { '_' }); + let var_name_long: WString = VAR_NAME_PREFIX.chars().chain(long_flag).collect(); + vars.set_var(var_name_long, opt_spec.vals.as_slice(), EnvMode::LOCAL); + } + } + + vars.set_var(L!("argv"), opts.args.as_slice(), EnvMode::LOCAL); +} + +/// The argparse builtin. This is explicitly not compatible with the BSD or GNU version of this +/// command. That's because fish doesn't have the weird quoting problems of POSIX shells. So we +/// don't need to support flags like `--unquoted`. Similarly we don't want to support introducing +/// long options with a single dash so we don't support the `--alternative` flag. That `getopt` is +/// an external command also means its output has to be in a form that can be eval'd. Because our +/// version is a builtin it can directly set variables local to the current scope (e.g., a +/// function). It doesn't need to write anything to stdout that then needs to be eval'd. +pub fn argparse( + parser: &mut parser_t, + streams: &mut io_streams_t, + args: &mut [&wstr], +) -> Option { + let cmd = args[0]; + let argc = args.len(); + + let mut opts = ArgParseCmdOpts::new(); + let mut optind = 0usize; + let retval = parse_cmd_opts(&mut opts, &mut optind, argc, args, parser, streams); + if retval != STATUS_CMD_OK { + // This is an error in argparse usage, so we append the error trailer with a stack trace. + // The other errors are an error in using *the command* that is using argparse, + // so our help doesn't apply. + builtin_print_error_trailer(parser, streams, cmd); + return retval; + } + + if opts.print_help { + builtin_print_help(parser, streams, cmd); + return STATUS_CMD_OK; + } + + let retval = parse_exclusive_args(&mut opts, streams); + if retval != STATUS_CMD_OK { + return retval; + } + + // wgetopt expects the first argument to be the command, and skips it. + // if optind was 0 we'd already have returned. + assert!(optind > 0, "Optind is 0?"); + let retval = argparse_parse_args( + &mut opts, + &mut args[optind - 1..], + argc - optind + 1, + parser, + streams, + ); + if retval != STATUS_CMD_OK { + return retval; + } + + let retval = check_min_max_args_constraints(&opts, streams); + if retval != STATUS_CMD_OK { + return retval; + } + + set_argparse_result_vars(parser.get_var_stack(), &opts); + + return retval; +} diff --git a/fish-rust/src/builtins/mod.rs b/fish-rust/src/builtins/mod.rs index 43a3209fa..8d579bc23 100644 --- a/fish-rust/src/builtins/mod.rs +++ b/fish-rust/src/builtins/mod.rs @@ -1,6 +1,7 @@ pub mod shared; pub mod abbr; +pub mod argparse; pub mod bg; pub mod block; pub mod builtin; diff --git a/fish-rust/src/builtins/shared.rs b/fish-rust/src/builtins/shared.rs index 79e47e35b..45e23d1a8 100644 --- a/fish-rust/src/builtins/shared.rs +++ b/fish-rust/src/builtins/shared.rs @@ -37,6 +37,9 @@ pub const BUILTIN_ERR_TOO_MANY_ARGUMENTS: &str = "%ls: too many arguments\n"; /// Error message when integer expected pub const BUILTIN_ERR_NOT_NUMBER: &str = "%ls: %ls: invalid integer\n"; +/// Error message for unknown switch. +pub const BUILTIN_ERR_UNKNOWN: &str = "%ls: %ls: unknown option\n"; + /// Error messages for unexpected args. pub const BUILTIN_ERR_ARG_COUNT0: &str = "%ls: missing argument\n"; pub const BUILTIN_ERR_ARG_COUNT1: &str = "%ls: expected %d arguments; got %d\n"; @@ -170,6 +173,7 @@ pub fn run_builtin( ) -> Option { match builtin { RustBuiltin::Abbr => super::abbr::abbr(parser, streams, args), + RustBuiltin::Argparse => super::argparse::argparse(parser, streams, args), RustBuiltin::Bg => super::bg::bg(parser, streams, args), RustBuiltin::Block => super::block::block(parser, streams, args), RustBuiltin::Builtin => super::builtin::builtin(parser, streams, args), diff --git a/fish-rust/src/ffi.rs b/fish-rust/src/ffi.rs index dbe225646..f5b089b48 100644 --- a/fish-rust/src/ffi.rs +++ b/fish-rust/src/ffi.rs @@ -25,6 +25,7 @@ include_cpp! { #include "env.h" #include "env_universal_common.h" #include "event.h" + #include "exec.h" #include "fallback.h" #include "fds.h" #include "fish_indent_common.h" @@ -119,6 +120,8 @@ include_cpp! { generate!("env_var_t") + generate!("exec_subshell_ffi") + generate!("function_properties_t") generate!("function_properties_ref_t") generate!("function_get_props_autoload") @@ -203,9 +206,21 @@ impl parser_t { self.vars_env_ffi() } - pub fn set_var(&mut self, name: &wstr, value: &[&wstr], flags: EnvMode) -> libc::c_int { + pub fn set_var, U: AsRef>( + &mut self, + name: T, + value: &[U], + flags: EnvMode, + ) -> libc::c_int { self.get_var_stack().set_var(name, value, flags) } + + pub fn get_func_name(&mut self, level: i32) -> Option { + let name = self.pin().get_function_name_ffi(c_int(level)); + name.as_ref() + .map(|s| s.from_ffi()) + .filter(|s| !s.is_empty()) + } } unsafe impl Send for env_universal_t {} @@ -238,13 +253,18 @@ impl env_stack_t { } /// Helper to set a value. - pub fn set_var(&mut self, name: &wstr, value: &[&wstr], flags: EnvMode) -> libc::c_int { + pub fn set_var, U: AsRef>( + &mut self, + name: T, + value: &[U], + flags: EnvMode, + ) -> libc::c_int { use crate::wchar_ffi::{wstr_to_u32string, W0String}; let strings: Vec = value.iter().map(wstr_to_u32string).collect(); let ptrs: Vec<*const u32> = strings.iter().map(|s| s.as_ptr()).collect(); self.pin() .set_ffi( - &name.to_ffi(), + &name.as_ref().to_ffi(), flags.bits(), ptrs.as_ptr() as *const c_void, ptrs.len(), diff --git a/fish-rust/src/wgetopt.rs b/fish-rust/src/wgetopt.rs index 8fb88cfce..bc4224c98 100644 --- a/fish-rust/src/wgetopt.rs +++ b/fish-rust/src/wgetopt.rs @@ -80,7 +80,7 @@ pub struct wgetopter_t<'opts, 'args, 'argarray> { /// returned was found. This allows us to pick up the scan where we left off. /// /// If this is empty, it means resume the scan by advancing to the next ARGV-element. - nextchar: &'args wstr, + pub nextchar: &'args wstr, /// Index in ARGV of the next element to be scanned. This is used for communication to and from /// the caller and for communication between successive calls to `getopt`. diff --git a/src/builtin.cpp b/src/builtin.cpp index 8bb87ca87..a95d5bfd3 100644 --- a/src/builtin.cpp +++ b/src/builtin.cpp @@ -29,7 +29,6 @@ #include #include -#include "builtins/argparse.h" #include "builtins/bind.h" #include "builtins/cd.h" #include "builtins/commandline.h" @@ -350,7 +349,7 @@ static constexpr builtin_data_t builtin_datas[] = { {L"_", &builtin_gettext, N_(L"Translate a string")}, {L"abbr", &implemented_in_rust, N_(L"Manage abbreviations")}, {L"and", &builtin_generic, N_(L"Run command if last command succeeded")}, - {L"argparse", &builtin_argparse, N_(L"Parse options in fish script")}, + {L"argparse", &implemented_in_rust, N_(L"Parse options in fish script")}, {L"begin", &builtin_generic, N_(L"Create a block of code")}, {L"bg", &implemented_in_rust, N_(L"Send job to background")}, {L"bind", &builtin_bind, N_(L"Handle fish key bindings")}, @@ -524,6 +523,9 @@ static maybe_t try_get_rust_builtin(const wcstring &cmd) { if (cmd == L"abbr") { return RustBuiltin::Abbr; } + if (cmd == L"argparse") { + return RustBuiltin::Argparse; + } if (cmd == L"bg") { return RustBuiltin::Bg; } diff --git a/src/builtin.h b/src/builtin.h index 07e40c56b..1cb2281bc 100644 --- a/src/builtin.h +++ b/src/builtin.h @@ -113,6 +113,7 @@ int parse_help_only_cmd_opts(help_only_cmd_opts_t &opts, int *optind, int argc, /// An enum of the builtins implemented in Rust. enum class RustBuiltin : int32_t { Abbr, + Argparse, Bg, Block, Builtin, diff --git a/src/builtins/argparse.cpp b/src/builtins/argparse.cpp deleted file mode 100644 index 9549c4b81..000000000 --- a/src/builtins/argparse.cpp +++ /dev/null @@ -1,740 +0,0 @@ -// Implementation of the argparse builtin. -// -// See issue #4190 for the rationale behind the original behavior of this builtin. -#include "config.h" // IWYU pragma: keep - -#include "argparse.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../builtin.h" -#include "../common.h" -#include "../env.h" -#include "../exec.h" -#include "../fallback.h" // IWYU pragma: keep -#include "../io.h" -#include "../maybe.h" -#include "../parser.h" -#include "../wcstringutil.h" -#include "../wgetopt.h" // IWYU pragma: keep -#include "../wutil.h" // IWYU pragma: keep - -static const wcstring var_name_prefix = L"_flag_"; - -#define BUILTIN_ERR_INVALID_OPT_SPEC _(L"%ls: Invalid option spec '%ls' at char '%lc'\n") - -namespace { -struct option_spec_t { - wchar_t short_flag; - wcstring long_flag; - wcstring validation_command; - std::vector vals; - bool short_flag_valid{true}; - int num_allowed{0}; - int num_seen{0}; - - explicit option_spec_t(wchar_t s) : short_flag(s) {} -}; -using option_spec_ref_t = std::unique_ptr; - -struct argparse_cmd_opts_t { - bool ignore_unknown = false; - bool print_help = false; - bool stop_nonopt = false; - size_t min_args = 0; - size_t max_args = SIZE_MAX; - wchar_t implicit_int_flag = L'\0'; - wcstring name; - std::vector raw_exclusive_flags; - std::vector argv; - std::unordered_map options; - std::unordered_map long_to_short_flag; - std::vector> exclusive_flag_sets; -}; -} // namespace - -static const wchar_t *const short_options = L"+:hn:six:N:X:"; -static const struct woption long_options[] = { - {L"stop-nonopt", no_argument, 's'}, {L"ignore-unknown", no_argument, 'i'}, - {L"name", required_argument, 'n'}, {L"exclusive", required_argument, 'x'}, - {L"help", no_argument, 'h'}, {L"min-args", required_argument, 'N'}, - {L"max-args", required_argument, 'X'}, {}}; - -// Check if any pair of mutually exclusive options was seen. Note that since every option must have -// a short name we only need to check those. -static int check_for_mutually_exclusive_flags(const argparse_cmd_opts_t &opts, - io_streams_t &streams) { - for (const auto &kv : opts.options) { - const auto &opt_spec = kv.second; - if (opt_spec->num_seen == 0) continue; - - // We saw this option at least once. Check all the sets of mutually exclusive options to see - // if this option appears in any of them. - for (const auto &xarg_set : opts.exclusive_flag_sets) { - if (contains(xarg_set, opt_spec->short_flag)) { - // Okay, this option is in a mutually exclusive set of options. Check if any of the - // other mutually exclusive options have been seen. - for (const auto &xflag : xarg_set) { - auto xopt_spec_iter = opts.options.find(xflag); - if (xopt_spec_iter == opts.options.end()) continue; - - const auto &xopt_spec = xopt_spec_iter->second; - // Ignore this flag in the list of mutually exclusive flags. - if (xopt_spec->short_flag == opt_spec->short_flag) continue; - - // If it is a different flag check if it has been seen. - if (xopt_spec->num_seen) { - wcstring flag1; - if (opt_spec->short_flag_valid) flag1 = wcstring(1, opt_spec->short_flag); - if (!opt_spec->long_flag.empty()) { - if (opt_spec->short_flag_valid) flag1 += L"/"; - flag1 += opt_spec->long_flag; - } - wcstring flag2; - if (xopt_spec->short_flag_valid) flag2 = wcstring(1, xopt_spec->short_flag); - if (!xopt_spec->long_flag.empty()) { - if (xopt_spec->short_flag_valid) flag2 += L"/"; - flag2 += xopt_spec->long_flag; - } - // We want the flag order to be deterministic. Primarily to make unit - // testing easier. - if (flag1 > flag2) { - std::swap(flag1, flag2); - } - streams.err.append_format( - _(L"%ls: %ls %ls: options cannot be used together\n"), - opts.name.c_str(), flag1.c_str(), flag2.c_str()); - return STATUS_CMD_ERROR; - } - } - } - } - } - return STATUS_CMD_OK; -} - -// This should be called after all the option specs have been parsed. At that point we have enough -// information to parse the values associated with any `--exclusive` flags. -static int parse_exclusive_args(argparse_cmd_opts_t &opts, io_streams_t &streams) { - for (const wcstring &raw_xflags : opts.raw_exclusive_flags) { - const std::vector xflags = split_string(raw_xflags, L','); - if (xflags.size() < 2) { - streams.err.append_format(_(L"%ls: exclusive flag string '%ls' is not valid\n"), - opts.name.c_str(), raw_xflags.c_str()); - return STATUS_CMD_ERROR; - } - - std::vector exclusive_set; - for (const auto &flag : xflags) { - if (flag.size() == 1 && opts.options.find(flag[0]) != opts.options.end()) { - // It's a short flag. - exclusive_set.push_back(flag[0]); - } else { - auto x = opts.long_to_short_flag.find(flag); - if (x != opts.long_to_short_flag.end()) { - // It's a long flag we store as its short flag equivalent. - exclusive_set.push_back(x->second); - } else { - streams.err.append_format(_(L"%ls: exclusive flag '%ls' is not valid\n"), - opts.name.c_str(), flag.c_str()); - return STATUS_CMD_ERROR; - } - } - } - - // Store the set of exclusive flags for use when parsing the supplied set of arguments. - opts.exclusive_flag_sets.push_back(exclusive_set); - } - - return STATUS_CMD_OK; -} - -static bool parse_flag_modifiers(const argparse_cmd_opts_t &opts, const option_spec_ref_t &opt_spec, - const wcstring &option_spec, const wchar_t **opt_spec_str, - io_streams_t &streams) { - const wchar_t *s = *opt_spec_str; - if (opt_spec->short_flag == opts.implicit_int_flag && *s && *s != L'!') { - streams.err.append_format( - _(L"%ls: Implicit int short flag '%lc' does not allow modifiers like '%lc'\n"), - opts.name.c_str(), opt_spec->short_flag, *s); - return false; - } - - if (*s == L'=') { - s++; - if (*s == L'?') { - opt_spec->num_allowed = -1; // optional arg - s++; - } else if (*s == L'+') { - opt_spec->num_allowed = 2; // mandatory arg and can appear more than once - s++; - } else { - opt_spec->num_allowed = 1; // mandatory arg and can appear only once - } - } - - if (*s == L'!') { - s++; - opt_spec->validation_command = wcstring(s); - // Move cursor to the end so we don't expect a long flag. - while (*s) s++; - } else if (*s) { - streams.err.append_format(BUILTIN_ERR_INVALID_OPT_SPEC, opts.name.c_str(), - option_spec.c_str(), *s); - return false; - } - - // Make sure we have some validation for implicit int flags. - if (opt_spec->short_flag == opts.implicit_int_flag && opt_spec->validation_command.empty()) { - opt_spec->validation_command = L"_validate_int"; - } - - if (opts.options.find(opt_spec->short_flag) != opts.options.end()) { - streams.err.append_format(L"%ls: Short flag '%lc' already defined\n", opts.name.c_str(), - opt_spec->short_flag); - return false; - } - - *opt_spec_str = s; - return true; -} - -/// Parse the text following the short flag letter. -static bool parse_option_spec_sep(argparse_cmd_opts_t &opts, const option_spec_ref_t &opt_spec, - const wcstring &option_spec, const wchar_t **opt_spec_str, - wchar_t &counter, io_streams_t &streams) { - const wchar_t *s = *opt_spec_str; - if (*(s - 1) == L'#') { - if (*s != L'-') { - // Long-only! - s--; - opt_spec->short_flag = counter; - counter++; - } - if (opts.implicit_int_flag) { - streams.err.append_format(_(L"%ls: Implicit int flag '%lc' already defined\n"), - opts.name.c_str(), opts.implicit_int_flag); - return false; - } - opts.implicit_int_flag = opt_spec->short_flag; - opt_spec->short_flag_valid = false; - s++; - } else if (*s == L'-') { - opt_spec->short_flag_valid = false; - s++; - if (!*s) { - streams.err.append_format(BUILTIN_ERR_INVALID_OPT_SPEC, opts.name.c_str(), - option_spec.c_str(), *(s - 1)); - return false; - } - } else if (*s == L'/') { - s++; // the struct is initialized assuming short_flag_valid should be true - if (!*s) { - streams.err.append_format(BUILTIN_ERR_INVALID_OPT_SPEC, opts.name.c_str(), - option_spec.c_str(), *(s - 1)); - return false; - } - } else if (*s == L'#') { - if (opts.implicit_int_flag) { - streams.err.append_format(_(L"%ls: Implicit int flag '%lc' already defined\n"), - opts.name.c_str(), opts.implicit_int_flag); - return false; - } - opts.implicit_int_flag = opt_spec->short_flag; - opt_spec->num_allowed = 1; // mandatory arg and can appear only once - s++; // the struct is initialized assuming short_flag_valid should be true - } else { - if (*s != L'!' && *s != L'?' && *s != L'=') { - // No short flag separator and no other modifiers, so this is a long only option. - // Since getopt needs a wchar, we have a counter that we count up. - opt_spec->short_flag_valid = false; - s--; - opt_spec->short_flag = counter; - counter++; - } else { - // Try to parse any other flag modifiers - if (!parse_flag_modifiers(opts, opt_spec, option_spec, &s, streams)) return false; - } - } - - *opt_spec_str = s; - return true; -} - -/// This parses an option spec string into a struct option_spec. -static bool parse_option_spec(argparse_cmd_opts_t &opts, //!OCLINT(high npath complexity) - const wcstring &option_spec, wchar_t &counter, - io_streams_t &streams) { - if (option_spec.empty()) { - streams.err.append_format( - _(L"%ls: An option spec must have at least a short or a long flag\n"), - opts.name.c_str()); - return false; - } - - const wchar_t *s = option_spec.c_str(); - if (!iswalnum(*s) && *s != L'#') { - streams.err.append_format(_(L"%ls: Short flag '%lc' invalid, must be alphanum or '#'\n"), - opts.name.c_str(), *s); - return false; - } - - std::unique_ptr opt_spec(new option_spec_t{*s++}); - - // Try parsing stuff after the short flag. - if (*s && !parse_option_spec_sep(opts, opt_spec, option_spec, &s, counter, streams)) { - return false; - } - - // Collect any long flag name. - if (*s) { - const wchar_t *const long_flag_start = s; - while (*s && (*s == L'-' || *s == L'_' || iswalnum(*s))) s++; - if (s != long_flag_start) { - opt_spec->long_flag = wcstring(long_flag_start, s); - if (opts.long_to_short_flag.count(opt_spec->long_flag) > 0) { - streams.err.append_format(L"%ls: Long flag '%ls' already defined\n", - opts.name.c_str(), opt_spec->long_flag.c_str()); - return false; - } - } - } - if (!parse_flag_modifiers(opts, opt_spec, option_spec, &s, streams)) { - return false; - } - - // Record our long flag if we have one. - if (!opt_spec->long_flag.empty()) { - auto ins = opts.long_to_short_flag.emplace(opt_spec->long_flag, opt_spec->short_flag); - assert(ins.second && "Should have inserted long flag"); - (void)ins; - } - - // Record our option under its short flag. - opts.options[opt_spec->short_flag] = std::move(opt_spec); - return true; -} - -static int collect_option_specs(argparse_cmd_opts_t &opts, int *optind, int argc, - const wchar_t **argv, io_streams_t &streams) { - const wchar_t *cmd = argv[0]; - - // A counter to give short chars to long-only options because getopt needs that. - // Luckily we have wgetopt so we can use wchars - this is one of the private use areas so we - // have 6400 options available. - auto counter = static_cast(0xE000); - - while (true) { - if (std::wcscmp(L"--", argv[*optind]) == 0) { - ++*optind; - break; - } - - if (!parse_option_spec(opts, argv[*optind], counter, streams)) { - return STATUS_CMD_ERROR; - } - if (++*optind == argc) { - streams.err.append_format(_(L"%ls: Missing -- separator\n"), cmd); - return STATUS_INVALID_ARGS; - } - } - - // Check for counter overreach once at the end because this is very unlikely to ever be reached. - if (counter > static_cast(0xF8FF)) { - streams.err.append_format(_(L"%ls: Too many long-only options\n"), cmd); - return STATUS_INVALID_ARGS; - } - - return STATUS_CMD_OK; -} - -static int parse_cmd_opts(argparse_cmd_opts_t &opts, int *optind, //!OCLINT(high ncss method) - int argc, const wchar_t **argv, parser_t &parser, io_streams_t &streams) { - const wchar_t *cmd = argv[0]; - int opt; - wgetopter_t w; - while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, nullptr)) != -1) { - switch (opt) { - case 'n': { - opts.name = w.woptarg; - break; - } - case 's': { - opts.stop_nonopt = true; - break; - } - case 'i': { - opts.ignore_unknown = true; - break; - } - case 'x': { - // Just save the raw string here. Later, when we have all the short and long flag - // definitions we'll parse these strings into a more useful data structure. - opts.raw_exclusive_flags.push_back(w.woptarg); - break; - } - case 'h': { - opts.print_help = true; - break; - } - case 'N': { - long x = fish_wcstol(w.woptarg); - if (errno || x < 0) { - streams.err.append_format(_(L"%ls: Invalid --min-args value '%ls'\n"), cmd, - w.woptarg); - return STATUS_INVALID_ARGS; - } - opts.min_args = x; - break; - } - case 'X': { - long x = fish_wcstol(w.woptarg); - if (errno || x < 0) { - streams.err.append_format(_(L"%ls: Invalid --max-args value '%ls'\n"), cmd, - w.woptarg); - return STATUS_INVALID_ARGS; - } - opts.max_args = x; - break; - } - case ':': { - builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1], - /* print_hints */ false); - return STATUS_INVALID_ARGS; - } - case '?': { - builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1], - /* print_hints */ false); - return STATUS_INVALID_ARGS; - } - default: { - DIE("unexpected retval from wgetopt_long"); - } - } - } - - if (opts.print_help) return STATUS_CMD_OK; - - if (std::wcscmp(L"--", argv[w.woptind - 1]) == 0) { - --w.woptind; - } - - if (argc == w.woptind) { - // The user didn't specify any option specs. - streams.err.append_format(_(L"%ls: Missing -- separator\n"), cmd); - return STATUS_INVALID_ARGS; - } - - if (opts.name.empty()) { - // If no name has been given, we default to the function name. - // If any error happens, the backtrace will show which argparse it was. - if (maybe_t fn = parser.get_function_name(1)) { - opts.name = fn.acquire(); - } else { - opts.name = L"argparse"; - } - } - - *optind = w.woptind; - return collect_option_specs(opts, optind, argc, argv, streams); -} - -static void populate_option_strings(const argparse_cmd_opts_t &opts, wcstring *short_options, - std::vector *long_options) { - for (const auto &kv : opts.options) { - const auto &opt_spec = kv.second; - if (opt_spec->short_flag_valid) short_options->push_back(opt_spec->short_flag); - - woption_argument_t arg_type = no_argument; - if (opt_spec->num_allowed == -1) { - arg_type = optional_argument; - if (opt_spec->short_flag_valid) short_options->append(L"::"); - } else if (opt_spec->num_allowed > 0) { - arg_type = required_argument; - if (opt_spec->short_flag_valid) short_options->append(L":"); - } - - if (!opt_spec->long_flag.empty()) { - long_options->push_back({opt_spec->long_flag.c_str(), arg_type, opt_spec->short_flag}); - } - } - long_options->push_back(woption{}); -} - -static int validate_arg(parser_t &parser, const argparse_cmd_opts_t &opts, option_spec_t *opt_spec, - bool is_long_flag, const wchar_t *woptarg, io_streams_t &streams) { - // Obviously if there is no arg validation command we assume the arg is okay. - if (opt_spec->validation_command.empty()) return STATUS_CMD_OK; - - std::vector cmd_output; - - auto &vars = parser.vars(); - - vars.push(true); - vars.set_one(L"_argparse_cmd", ENV_LOCAL | ENV_EXPORT, opts.name); - if (is_long_flag) { - vars.set_one(var_name_prefix + L"name", ENV_LOCAL | ENV_EXPORT, opt_spec->long_flag); - } else { - vars.set_one(var_name_prefix + L"name", ENV_LOCAL | ENV_EXPORT, - wcstring(1, opt_spec->short_flag)); - } - vars.set_one(var_name_prefix + L"value", ENV_LOCAL | ENV_EXPORT, woptarg); - - int retval = exec_subshell(opt_spec->validation_command, parser, cmd_output, false); - for (const auto &output : cmd_output) { - streams.err.append(output); - streams.err.push(L'\n'); - } - vars.pop(); - return retval; -} - -/// \return whether the option 'opt' is an implicit integer option. -static bool is_implicit_int(const argparse_cmd_opts_t &opts, const wchar_t *val) { - if (opts.implicit_int_flag == L'\0') { - // There is no implicit integer option. - return false; - } - - // We succeed if this argument can be parsed as an integer. - errno = 0; - (void)fish_wcstol(val); - return errno == 0; -} - -// Store this value under the implicit int option. -static int validate_and_store_implicit_int(parser_t &parser, const argparse_cmd_opts_t &opts, - const wchar_t *val, wgetopter_t &w, int long_idx, - io_streams_t &streams) { - // See if this option passes the validation checks. - auto found = opts.options.find(opts.implicit_int_flag); - assert(found != opts.options.end()); - const auto &opt_spec = found->second; - int retval = validate_arg(parser, opts, opt_spec.get(), long_idx != -1, val, streams); - if (retval != STATUS_CMD_OK) return retval; - - // It's a valid integer so store it and return success. - opt_spec->vals.clear(); - opt_spec->vals.emplace_back(val); - opt_spec->num_seen++; - w.nextchar = nullptr; - return STATUS_CMD_OK; -} - -static int handle_flag(parser_t &parser, const argparse_cmd_opts_t &opts, option_spec_t *opt_spec, - int long_idx, const wchar_t *woptarg, io_streams_t &streams) { - opt_spec->num_seen++; - if (opt_spec->num_allowed == 0) { - // It's a boolean flag. Save the flag we saw since it might be useful to know if the - // short or long flag was given. - assert(!woptarg); - if (long_idx == -1) { - opt_spec->vals.push_back(wcstring(1, L'-') + opt_spec->short_flag); - } else { - opt_spec->vals.push_back(L"--" + opt_spec->long_flag); - } - return STATUS_CMD_OK; - } - - if (woptarg) { - int retval = validate_arg(parser, opts, opt_spec, long_idx != -1, woptarg, streams); - if (retval != STATUS_CMD_OK) return retval; - } - - if (opt_spec->num_allowed == -1 || opt_spec->num_allowed == 1) { - // We're depending on `wgetopt_long()` to report that a mandatory value is missing if - // `opt_spec->num_allowed == 1` and thus return ':' so that we don't take this branch if - // the mandatory arg is missing. - opt_spec->vals.clear(); - if (woptarg) { - opt_spec->vals.push_back(woptarg); - } - } else { - assert(woptarg); - opt_spec->vals.push_back(woptarg); - } - - return STATUS_CMD_OK; -} - -static int argparse_parse_flags(parser_t &parser, argparse_cmd_opts_t &opts, - const wchar_t *short_options, const woption *long_options, - const wchar_t *cmd, int argc, const wchar_t **argv, int *optind, - io_streams_t &streams) { - int opt; - int long_idx = -1; - wgetopter_t w; - while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, &long_idx)) != -1) { - if (opt == ':') { - builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1], - false /* print_hints */); - return STATUS_INVALID_ARGS; - } else if (opt == '?') { - // It's not a recognized flag. See if it's an implicit int flag. - const wchar_t *arg_contents = argv[w.woptind - 1] + 1; - int retval = STATUS_CMD_OK; - if (is_implicit_int(opts, arg_contents)) { - retval = validate_and_store_implicit_int(parser, opts, arg_contents, w, long_idx, - streams); - } else if (!opts.ignore_unknown) { - streams.err.append_format(BUILTIN_ERR_UNKNOWN, cmd, argv[w.woptind - 1]); - retval = STATUS_INVALID_ARGS; - } else { - // Any unrecognized option is put back if ignore_unknown is used. - // This allows reusing the same argv in multiple argparse calls, - // or just ignoring the error (e.g. in completions). - opts.argv.push_back(arg_contents - 1); - // Work around weirdness with wgetopt, which crashes if we `continue` here. - if (w.woptind == argc) break; - // Explain to wgetopt that we want to skip to the next arg, - // because we can't handle this opt group. - w.nextchar = nullptr; - } - if (retval != STATUS_CMD_OK) return retval; - long_idx = -1; - continue; - } else if (opt == 1) { - // A non-option argument. - // We use `-` as the first option-string-char to disable GNU getopt's reordering, - // otherwise we'd get ignored options first and normal arguments later. - // E.g. `argparse -i -- -t tango -w` needs to keep `-t tango -w` in $argv, not `-t -w - // tango`. - opts.argv.push_back(argv[w.woptind - 1]); - continue; - } - - // It's a recognized flag. - auto found = opts.options.find(opt); - assert(found != opts.options.end()); - - int retval = handle_flag(parser, opts, found->second.get(), long_idx, w.woptarg, streams); - if (retval != STATUS_CMD_OK) return retval; - long_idx = -1; - } - - *optind = w.woptind; - return STATUS_CMD_OK; -} - -// This function mimics the `wgetopt_long()` usage found elsewhere in our other builtin commands. -// It's different in that the short and long option structures are constructed dynamically based on -// arguments provided to the `argparse` command. -static int argparse_parse_args(argparse_cmd_opts_t &opts, const wchar_t **argv, int argc, - parser_t &parser, io_streams_t &streams) { - if (argc <= 1) return STATUS_CMD_OK; - - // "+" means stop at nonopt, "-" means give nonoptions the option character code `1`, and don't - // reorder. - wcstring short_options = opts.stop_nonopt ? L"+:" : L"-:"; - std::vector long_options; - populate_option_strings(opts, &short_options, &long_options); - - // long_options should have a "null terminator" - assert(!long_options.empty() && long_options.back().name == nullptr); - - const wchar_t *cmd = opts.name.c_str(); - - int optind; - int retval = argparse_parse_flags(parser, opts, short_options.c_str(), long_options.data(), cmd, - argc, argv, &optind, streams); - if (retval != STATUS_CMD_OK) return retval; - - retval = check_for_mutually_exclusive_flags(opts, streams); - if (retval != STATUS_CMD_OK) return retval; - - for (int i = optind; argv[i]; i++) { - opts.argv.push_back(argv[i]); - } - - return STATUS_CMD_OK; -} - -static int check_min_max_args_constraints(const argparse_cmd_opts_t &opts, const parser_t &parser, - io_streams_t &streams) { - UNUSED(parser); - const wchar_t *cmd = opts.name.c_str(); - - if (opts.argv.size() < opts.min_args) { - streams.err.append_format(BUILTIN_ERR_MIN_ARG_COUNT1, cmd, opts.min_args, opts.argv.size()); - return STATUS_CMD_ERROR; - } - if (opts.max_args != SIZE_MAX && opts.argv.size() > opts.max_args) { - streams.err.append_format(BUILTIN_ERR_MAX_ARG_COUNT1, cmd, opts.max_args, opts.argv.size()); - return STATUS_CMD_ERROR; - } - - return STATUS_CMD_OK; -} - -/// Put the result of parsing the supplied args into the caller environment as local vars. -static void set_argparse_result_vars(env_stack_t &vars, const argparse_cmd_opts_t &opts) { - for (const auto &kv : opts.options) { - const auto &opt_spec = kv.second; - if (!opt_spec->num_seen) continue; - - if (opt_spec->short_flag_valid) { - vars.set(var_name_prefix + opt_spec->short_flag, ENV_LOCAL, opt_spec->vals); - } - if (!opt_spec->long_flag.empty()) { - // We do a simple replacement of all non alphanum chars rather than calling - // escape_string(long_flag, 0, STRING_STYLE_VAR). - wcstring long_flag = opt_spec->long_flag; - for (auto &pos : long_flag) { - if (!iswalnum(pos)) pos = L'_'; - } - vars.set(var_name_prefix + long_flag, ENV_LOCAL, opt_spec->vals); - } - } - - vars.set(L"argv", ENV_LOCAL, opts.argv); -} - -/// The argparse builtin. This is explicitly not compatible with the BSD or GNU version of this -/// command. That's because fish doesn't have the weird quoting problems of POSIX shells. So we -/// don't need to support flags like `--unquoted`. Similarly we don't want to support introducing -/// long options with a single dash so we don't support the `--alternative` flag. That `getopt` is -/// an external command also means its output has to be in a form that can be eval'd. Because our -/// version is a builtin it can directly set variables local to the current scope (e.g., a -/// function). It doesn't need to write anything to stdout that then needs to be eval'd. -maybe_t builtin_argparse(parser_t &parser, io_streams_t &streams, const wchar_t **argv) { - const wchar_t *cmd = argv[0]; - int argc = builtin_count_args(argv); - argparse_cmd_opts_t opts; - - int optind; - int retval = parse_cmd_opts(opts, &optind, argc, argv, parser, streams); - if (retval != STATUS_CMD_OK) { - // This is an error in argparse usage, so we append the error trailer with a stack trace. - // The other errors are an error in using *the command* that is using argparse, - // so our help doesn't apply. - builtin_print_error_trailer(parser, streams.err, cmd); - return retval; - } - - if (opts.print_help) { - builtin_print_help(parser, streams, cmd); - return STATUS_CMD_OK; - } - - retval = parse_exclusive_args(opts, streams); - if (retval != STATUS_CMD_OK) return retval; - - // wgetopt expects the first argument to be the command, and skips it. - // if optind was 0 we'd already have returned. - assert(optind > 0 && "Optind is 0?"); - retval = argparse_parse_args(opts, &argv[optind - 1], argc - optind + 1, parser, streams); - if (retval != STATUS_CMD_OK) return retval; - - retval = check_min_max_args_constraints(opts, parser, streams); - if (retval != STATUS_CMD_OK) return retval; - - set_argparse_result_vars(parser.vars(), opts); - return retval; -} diff --git a/src/builtins/argparse.h b/src/builtins/argparse.h deleted file mode 100644 index 97272b380..000000000 --- a/src/builtins/argparse.h +++ /dev/null @@ -1,11 +0,0 @@ -// Prototypes for executing builtin_getopt function. -#ifndef FISH_BUILTIN_ARGPARSE_H -#define FISH_BUILTIN_ARGPARSE_H - -#include "../maybe.h" - -class parser_t; -struct io_streams_t; - -maybe_t builtin_argparse(parser_t &parser, io_streams_t &streams, const wchar_t **argv); -#endif diff --git a/src/exec.cpp b/src/exec.cpp index e66b0d190..ae37b53bb 100644 --- a/src/exec.cpp +++ b/src/exec.cpp @@ -1255,3 +1255,8 @@ int exec_subshell(const wcstring &cmd, parser_t &parser, std::vector & return exec_subshell_internal(cmd, parser, nullptr, &outputs, &break_expand, apply_exit_status, false); } + +int exec_subshell_ffi(const wcstring &cmd, parser_t &parser, wcstring_list_ffi_t &outputs, + bool apply_exit_status) { + return exec_subshell(cmd, parser, outputs.vals, apply_exit_status); +} diff --git a/src/exec.h b/src/exec.h index 6aa648d55..19c0741af 100644 --- a/src/exec.h +++ b/src/exec.h @@ -31,6 +31,8 @@ __warn_unused bool exec_job(parser_t &parser, const std::shared_ptr &j, int exec_subshell(const wcstring &cmd, parser_t &parser, bool apply_exit_status); int exec_subshell(const wcstring &cmd, parser_t &parser, std::vector &outputs, bool apply_exit_status); +int exec_subshell_ffi(const wcstring &cmd, parser_t &parser, wcstring_list_ffi_t &outputs, + bool apply_exit_status); /// Like exec_subshell, but only returns expansion-breaking errors. That is, a zero return means /// "success" (even though the command may have failed), a non-zero return means that we should diff --git a/src/parser.cpp b/src/parser.cpp index 58da3fbe4..727df0e42 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -349,6 +349,15 @@ bool parser_t::is_command_substitution() const { return false; } +wcstring parser_t::get_function_name_ffi(int level) { + auto name = get_function_name(level); + if (name.has_value()) { + return name.acquire(); + } else { + return wcstring(); + } +} + maybe_t parser_t::get_function_name(int level) { if (level == 0) { // Return the function name for the level preceding the most recent breakpoint. If there diff --git a/src/parser.h b/src/parser.h index 8feb28acc..87a5d80d8 100644 --- a/src/parser.h +++ b/src/parser.h @@ -434,6 +434,8 @@ class parser_t : public std::enable_shared_from_this { /// Remove the outermost block, asserting it's the given one. void pop_block(const block_t *expected); + /// Avoid maybe_t usage for ffi, sends a empty string in case of none. + wcstring get_function_name_ffi(int level); /// Return the function name for the specified stack frame. Default is one (current frame). maybe_t get_function_name(int level = 1);