From 7b3637cd1fbabea5c0f4c468076c20dc8ffb27e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20H=C3=B8rl=C3=BCck=20Berg?= <36937807+henrikhorluck@users.noreply.github.com> Date: Tue, 27 Jun 2023 19:05:55 +0200 Subject: [PATCH] Port builtins/status to fish - Also port tests of wdirname and wbasename, as they were bugged --- fish-rust/Cargo.lock | 16 +- fish-rust/Cargo.toml | 2 + fish-rust/src/builtins/mod.rs | 1 + fish-rust/src/builtins/shared.rs | 5 + fish-rust/src/builtins/status.rs | 633 +++++++++++++++++++++++++++++++ fish-rust/src/common.rs | 2 +- fish-rust/src/ffi.rs | 17 + fish-rust/src/path.rs | 2 +- fish-rust/src/wutil/mod.rs | 46 ++- fish-rust/src/wutil/tests.rs | 60 +++ src/builtin.cpp | 5 +- src/builtin.h | 1 + src/parser.cpp | 10 + src/parser.h | 5 + 14 files changed, 781 insertions(+), 24 deletions(-) create mode 100644 fish-rust/src/builtins/status.rs create mode 100644 fish-rust/src/wutil/tests.rs diff --git a/fish-rust/Cargo.lock b/fish-rust/Cargo.lock index 3acb7662b..b13acfc0a 100644 --- a/fish-rust/Cargo.lock +++ b/fish-rust/Cargo.lock @@ -341,6 +341,7 @@ dependencies = [ "lru", "moveit", "nix", + "num-derive", "num-traits", "once_cell", "pcre2", @@ -630,6 +631,17 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "num-traits" version = "0.2.15" @@ -648,7 +660,7 @@ checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "pcre2" version = "0.2.3" -source = "git+https://github.com/fish-shell/rust-pcre2?branch=master#824dd1460562f7b724a9acef218d4edb2ed7c289" +source = "git+https://github.com/fish-shell/rust-pcre2?branch=master#813a4267546e5ca8ff349c9c67d65e52a82172d2" dependencies = [ "libc", "log", @@ -659,7 +671,7 @@ dependencies = [ [[package]] name = "pcre2-sys" version = "0.2.4" -source = "git+https://github.com/fish-shell/rust-pcre2?branch=master#824dd1460562f7b724a9acef218d4edb2ed7c289" +source = "git+https://github.com/fish-shell/rust-pcre2?branch=master#813a4267546e5ca8ff349c9c67d65e52a82172d2" dependencies = [ "cc", "libc", diff --git a/fish-rust/Cargo.toml b/fish-rust/Cargo.toml index 317f6b174..f7de98569 100644 --- a/fish-rust/Cargo.toml +++ b/fish-rust/Cargo.toml @@ -22,6 +22,8 @@ lru = "0.10.0" moveit = "0.5.1" nix = { version = "0.25.0", default-features = false, features = [] } num-traits = "0.2.15" +# to make integer->enum conversion easier +num-derive = "0.3.3" once_cell = "1.17.0" rand = { version = "0.8.5", features = ["small_rng"] } unixstring = "0.2.7" diff --git a/fish-rust/src/builtins/mod.rs b/fish-rust/src/builtins/mod.rs index 8d579bc23..0829d89f9 100644 --- a/fish-rust/src/builtins/mod.rs +++ b/fish-rust/src/builtins/mod.rs @@ -17,6 +17,7 @@ pub mod random; pub mod realpath; pub mod r#return; pub mod set_color; +pub mod status; pub mod test; pub mod r#type; pub mod wait; diff --git a/fish-rust/src/builtins/shared.rs b/fish-rust/src/builtins/shared.rs index 983a3b523..a422be00f 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"; +pub const BUILTIN_ERR_MISSING_SUBCMD: &str = "%ls: missing subcommand\n"; +pub const BUILTIN_ERR_INVALID_SUBCMD: &str = "%ls: %ls: invalid subcommand\n"; + /// Error message for unknown switch. pub const BUILTIN_ERR_UNKNOWN: &str = "%ls: %ls: unknown option\n"; @@ -50,6 +53,7 @@ pub const BUILTIN_ERR_MAX_ARG_COUNT1: &str = "%ls: expected <= %d arguments; got /// Error message on invalid combination of options. pub const BUILTIN_ERR_COMBO: &str = "%ls: invalid option combination\n"; pub const BUILTIN_ERR_COMBO2: &str = "%ls: invalid option combination, %ls\n"; +pub const BUILTIN_ERR_COMBO2_EXCLUSIVE: &str = "%ls: %ls %ls: options cannot be used together\n"; // Return values (`$status` values for fish scripts) for various situations. @@ -197,6 +201,7 @@ pub fn run_builtin( RustBuiltin::Realpath => super::realpath::realpath(parser, streams, args), RustBuiltin::Return => super::r#return::r#return(parser, streams, args), RustBuiltin::SetColor => super::set_color::set_color(parser, streams, args), + RustBuiltin::Status => super::status::status(parser, streams, args), RustBuiltin::Test => super::test::test(parser, streams, args), RustBuiltin::Type => super::r#type::r#type(parser, streams, args), RustBuiltin::Wait => wait::wait(parser, streams, args), diff --git a/fish-rust/src/builtins/status.rs b/fish-rust/src/builtins/status.rs new file mode 100644 index 000000000..0d96ef8f5 --- /dev/null +++ b/fish-rust/src/builtins/status.rs @@ -0,0 +1,633 @@ +use std::os::unix::prelude::OsStrExt; + +use crate::builtins::shared::BUILTIN_ERR_NOT_NUMBER; +use crate::builtins::shared::{ + builtin_missing_argument, builtin_print_help, builtin_unknown_option, io_streams_t, + BUILTIN_ERR_ARG_COUNT2, BUILTIN_ERR_COMBO2_EXCLUSIVE, BUILTIN_ERR_INVALID_SUBCMD, + STATUS_CMD_ERROR, STATUS_CMD_OK, STATUS_INVALID_ARGS, +}; +use crate::common::{get_executable_path, str2wcstring}; + +use crate::ffi::get_job_control_mode; +use crate::ffi::get_login; +use crate::ffi::set_job_control_mode; +use crate::ffi::{is_interactive_session, Repin}; +use crate::ffi::{job_control_t, parser_t}; +use crate::future_feature_flags::{feature_metadata, feature_test}; +use crate::wchar::{wstr, WString, L}; + +use crate::wchar_ffi::WCharFromFFI; + +use crate::wgetopt::{wgetopter_t, wopt, woption, woption_argument_t}; +use crate::wutil::{ + fish_wcstoi, waccess, wbasename, wdirname, wgettext, wgettext_fmt, wrealpath, Error, +}; +use libc::{c_int, F_OK}; +use nix::errno::Errno; +use nix::NixPath; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive; + +macro_rules! str_enum { + ($name:ident, $(($val:ident, $str:expr)),* $(,)?) => { + impl TryFrom<&wstr> for $name { + type Error = (); + + fn try_from(s: &wstr) -> Result { + // matching on str's let's us avoid having to do binary search and friends outselves, + // this is ascii only anyways + match s.to_string().as_str() { + $($str => Ok(Self::$val)),*, + _ => Err(()), + } + } + } + + impl $name { + fn to_wstr(&self) -> WString { + // There can be multiple vals => str mappings, and that's okay + #[allow(unreachable_patterns)] + match self { + $(Self::$val => WString::from($str)),*, + } + } + } + } +} + +use once_cell::sync::Lazy; +use StatusCmd::*; +#[repr(u32)] +#[derive(Default, PartialEq, FromPrimitive, Clone)] +enum StatusCmd { + STATUS_CURRENT_CMD = 1, + STATUS_BASENAME, + STATUS_DIRNAME, + STATUS_FEATURES, + STATUS_FILENAME, + STATUS_FISH_PATH, + STATUS_FUNCTION, + STATUS_IS_BLOCK, + STATUS_IS_BREAKPOINT, + STATUS_IS_COMMAND_SUB, + STATUS_IS_FULL_JOB_CTRL, + STATUS_IS_INTERACTIVE, + STATUS_IS_INTERACTIVE_JOB_CTRL, + STATUS_IS_LOGIN, + STATUS_IS_NO_JOB_CTRL, + STATUS_LINE_NUMBER, + STATUS_SET_JOB_CONTROL, + STATUS_STACK_TRACE, + STATUS_TEST_FEATURE, + STATUS_CURRENT_COMMANDLINE, + #[default] + STATUS_UNDEF, +} + +str_enum!( + StatusCmd, + (STATUS_BASENAME, "basename"), + (STATUS_BASENAME, "current-basename"), + (STATUS_CURRENT_CMD, "current-command"), + (STATUS_CURRENT_COMMANDLINE, "current-commandline"), + (STATUS_DIRNAME, "current-dirname"), + (STATUS_FILENAME, "current-filename"), + (STATUS_FUNCTION, "current-function"), + (STATUS_LINE_NUMBER, "current-line-number"), + (STATUS_DIRNAME, "dirname"), + (STATUS_FEATURES, "features"), + (STATUS_FILENAME, "filename"), + (STATUS_FISH_PATH, "fish-path"), + (STATUS_FUNCTION, "function"), + (STATUS_IS_BLOCK, "is-block"), + (STATUS_IS_BREAKPOINT, "is-breakpoint"), + (STATUS_IS_COMMAND_SUB, "is-command-substitution"), + (STATUS_IS_FULL_JOB_CTRL, "is-full-job-control"), + (STATUS_IS_INTERACTIVE, "is-interactive"), + (STATUS_IS_INTERACTIVE_JOB_CTRL, "is-interactive-job-control"), + (STATUS_IS_LOGIN, "is-login"), + (STATUS_IS_NO_JOB_CTRL, "is-no-job-control"), + (STATUS_SET_JOB_CONTROL, "job-control"), + (STATUS_LINE_NUMBER, "line-number"), + (STATUS_STACK_TRACE, "print-stack-trace"), + (STATUS_STACK_TRACE, "stack-trace"), + (STATUS_TEST_FEATURE, "test-feature"), + // this was a nullptr in C++ + (STATUS_UNDEF, "undef"), +); + +impl StatusCmd { + fn as_char(&self) -> char { + // TODO: once unwrap is const, make LONG_OPTIONS const + let ch: StatusCmd = self.clone(); + char::from_u32(ch as u32).unwrap() + } +} + +/// Values that may be returned from the test-feature option to status. +#[repr(i32)] +enum TestFeatureRetVal { + TEST_FEATURE_ON = 0, + TEST_FEATURE_OFF, + TEST_FEATURE_NOT_RECOGNIZED, +} + +struct StatusCmdOpts { + level: i32, + new_job_control_mode: Option, + status_cmd: StatusCmd, + print_help: bool, +} + +impl Default for StatusCmdOpts { + fn default() -> Self { + Self { + level: 1, + new_job_control_mode: None, + status_cmd: StatusCmd::STATUS_UNDEF, + print_help: false, + } + } +} + +impl StatusCmdOpts { + fn set_status_cmd(&mut self, cmd: &wstr, sub_cmd: StatusCmd) -> Result<(), WString> { + if self.status_cmd != StatusCmd::STATUS_UNDEF { + return Err(wgettext_fmt!( + BUILTIN_ERR_COMBO2_EXCLUSIVE, + cmd, + self.status_cmd.to_wstr(), + sub_cmd.to_wstr(), + )); + } + self.status_cmd = sub_cmd; + Ok(()) + } +} + +const SHORT_OPTIONS: &wstr = L!(":L:cbilfnhj:t"); +static LONG_OPTIONS: Lazy<[woption; 17]> = Lazy::new(|| { + use woption_argument_t::*; + [ + wopt(L!("help"), no_argument, 'h'), + wopt(L!("current-filename"), no_argument, 'f'), + wopt(L!("current-line-number"), no_argument, 'n'), + wopt(L!("filename"), no_argument, 'f'), + wopt(L!("fish-path"), no_argument, STATUS_FISH_PATH.as_char()), + wopt(L!("is-block"), no_argument, 'b'), + wopt(L!("is-command-substitution"), no_argument, 'c'), + wopt( + L!("is-full-job-control"), + no_argument, + STATUS_IS_FULL_JOB_CTRL.as_char(), + ), + wopt(L!("is-interactive"), no_argument, 'i'), + wopt( + L!("is-interactive-job-control"), + no_argument, + STATUS_IS_INTERACTIVE_JOB_CTRL.as_char(), + ), + wopt(L!("is-login"), no_argument, 'l'), + wopt( + L!("is-no-job-control"), + no_argument, + STATUS_IS_NO_JOB_CTRL.as_char(), + ), + wopt(L!("job-control"), required_argument, 'j'), + wopt(L!("level"), required_argument, 'L'), + wopt(L!("line"), no_argument, 'n'), + wopt(L!("line-number"), no_argument, 'n'), + wopt(L!("print-stack-trace"), no_argument, 't'), + ] +}); + +/// Print the features and their values. +fn print_features(streams: &mut io_streams_t) { + // TODO: move this to features.rs + let mut max_len = i32::MIN; + for md in feature_metadata() { + max_len = max_len.max(md.name.len() as i32); + } + for md in feature_metadata() { + let set = if feature_test(md.flag) { + L!("on") + } else { + L!("off") + }; + streams.out.append(wgettext_fmt!( + "%-*ls%-3s %ls %ls\n", + max_len + 1, + md.name.from_ffi(), + set, + md.groups.from_ffi(), + md.description.from_ffi(), + )); + } +} + +fn parse_cmd_opts( + opts: &mut StatusCmdOpts, + optind: &mut usize, + args: &mut [&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 { + 'L' => { + opts.level = { + let arg = w.woptarg.expect("Option -L requires an argument"); + match fish_wcstoi(arg) { + Ok(level) if level >= 0 => level, + Err(Error::Overflow) | Ok(_) => { + streams.err.append(wgettext_fmt!( + "%ls: Invalid level value '%ls'\n", + cmd, + arg + )); + return STATUS_INVALID_ARGS; + } + _ => { + streams + .err + .append(wgettext_fmt!(BUILTIN_ERR_NOT_NUMBER, cmd, arg)); + return STATUS_INVALID_ARGS; + } + } + }; + } + 'c' => { + if let Err(e) = opts.set_status_cmd(cmd, STATUS_IS_COMMAND_SUB) { + streams.err.append(e); + return STATUS_CMD_ERROR; + } + } + 'b' => { + if let Err(e) = opts.set_status_cmd(cmd, STATUS_IS_BLOCK) { + streams.err.append(e); + return STATUS_CMD_ERROR; + } + } + 'i' => { + if let Err(e) = opts.set_status_cmd(cmd, STATUS_IS_INTERACTIVE) { + streams.err.append(e); + return STATUS_CMD_ERROR; + } + } + 'l' => { + if let Err(e) = opts.set_status_cmd(cmd, STATUS_IS_LOGIN) { + streams.err.append(e); + return STATUS_CMD_ERROR; + } + } + 'f' => { + if let Err(e) = opts.set_status_cmd(cmd, STATUS_FILENAME) { + streams.err.append(e); + return STATUS_CMD_ERROR; + } + } + 'n' => { + if let Err(e) = opts.set_status_cmd(cmd, STATUS_LINE_NUMBER) { + streams.err.append(e); + return STATUS_CMD_ERROR; + } + } + 'j' => { + if let Err(e) = opts.set_status_cmd(cmd, STATUS_SET_JOB_CONTROL) { + streams.err.append(e); + return STATUS_CMD_ERROR; + } + let Ok(job_mode) = w.woptarg.unwrap().try_into() else { + streams.err.append(wgettext_fmt!("%ls: Invalid job control mode '%ls'\n", cmd, w.woptarg.unwrap())); + return STATUS_CMD_ERROR; + }; + opts.new_job_control_mode = Some(job_mode); + } + 't' => { + if let Err(e) = opts.set_status_cmd(cmd, STATUS_STACK_TRACE) { + streams.err.append(e); + return STATUS_CMD_ERROR; + } + } + 'h' => opts.print_help = true, + ':' => { + builtin_missing_argument(parser, streams, cmd, args[w.woptind - 1], false); + return STATUS_INVALID_ARGS; + } + '?' => { + builtin_unknown_option(parser, streams, cmd, args[w.woptind - 1], false); + return STATUS_INVALID_ARGS; + } + c => { + let Some(opt_cmd) = StatusCmd::from_u32(c as u32) else { + panic!("unexpected retval from wgetopt_long") + }; + match opt_cmd { + STATUS_IS_FULL_JOB_CTRL + | STATUS_IS_INTERACTIVE_JOB_CTRL + | STATUS_IS_NO_JOB_CTRL + | STATUS_FISH_PATH => { + if let Err(e) = opts.set_status_cmd(cmd, opt_cmd) { + streams.err.append(e); + return STATUS_CMD_ERROR; + } + } + _ => panic!("unexpected retval from wgetopt_long"), + } + } + } + } + + *optind = w.woptind; + + return STATUS_CMD_OK; +} + +pub fn status( + parser: &mut parser_t, + streams: &mut io_streams_t, + args: &mut [&wstr], +) -> Option { + let cmd = args[0]; + let argc = args.len(); + + let mut opts = StatusCmdOpts::default(); + let mut optind = 0usize; + let retval = parse_cmd_opts(&mut opts, &mut optind, args, parser, streams); + if retval != STATUS_CMD_OK { + return retval; + } + + if opts.print_help { + builtin_print_help(parser, streams, cmd); + return STATUS_CMD_OK; + } + + // If a status command hasn't already been specified via a flag check the first word. + // Note that this can be simplified after we eliminate allowing subcommands as flags. + if optind < argc { + match StatusCmd::try_from(args[optind]) { + // TODO: can we replace UNDEF with wrapping in option? + Ok(STATUS_UNDEF) | Err(_) => { + streams + .err + .append(wgettext_fmt!(BUILTIN_ERR_INVALID_SUBCMD, cmd, args[1])); + return STATUS_INVALID_ARGS; + } + Ok(s) => { + if let Err(e) = opts.set_status_cmd(cmd, s) { + streams.err.append(e); + return STATUS_CMD_ERROR; + } + optind += 1; + } + } + } + // Every argument that we haven't consumed already is an argument for a subcommand. + let args = &args[optind..]; + + match opts.status_cmd { + STATUS_UNDEF => { + if !args.is_empty() { + streams.err.append(wgettext_fmt!( + BUILTIN_ERR_ARG_COUNT2, + cmd, + opts.status_cmd.to_wstr(), + 0, + args.len() + )); + return STATUS_INVALID_ARGS; + } + if get_login() { + streams.out.append(wgettext!("This is a login shell\n")); + } else { + streams.out.append(wgettext!("This is not a login shell\n")); + } + let job_control_mode = match get_job_control_mode() { + job_control_t::interactive => wgettext!("Only on interactive jobs"), + job_control_t::none => wgettext!("Never"), + job_control_t::all => wgettext!("Always"), + }; + streams + .out + .append(wgettext_fmt!("Job control: %ls\n", job_control_mode)); + streams.out.append(parser.stack_trace().from_ffi()); + } + STATUS_SET_JOB_CONTROL => { + let job_control_mode = match opts.new_job_control_mode { + Some(j) => { + // Flag form used + if !args.is_empty() { + streams.err.append(wgettext_fmt!( + BUILTIN_ERR_ARG_COUNT2, + cmd, + opts.status_cmd.to_wstr(), + 0, + args.len() + )); + return STATUS_INVALID_ARGS; + } + j + } + None => { + if args.len() != 1 { + streams.err.append(wgettext_fmt!( + BUILTIN_ERR_ARG_COUNT2, + cmd, + opts.status_cmd.to_wstr(), + 1, + args.len() + )); + return STATUS_INVALID_ARGS; + } + let Ok(new_mode)= args[0].try_into() else { + streams.err.append(wgettext_fmt!("%ls: Invalid job control mode '%ls'\n", cmd, args[0])); + return STATUS_CMD_ERROR; + }; + new_mode + } + }; + set_job_control_mode(job_control_mode); + } + STATUS_FEATURES => print_features(streams), + STATUS_TEST_FEATURE => { + if args.len() != 1 { + streams.err.append(wgettext_fmt!( + BUILTIN_ERR_ARG_COUNT2, + cmd, + opts.status_cmd.to_wstr(), + 1, + args.len() + )); + return STATUS_INVALID_ARGS; + } + use TestFeatureRetVal::*; + let mut retval = Some(TEST_FEATURE_NOT_RECOGNIZED as c_int); + for md in &feature_metadata() { + if md.name.from_ffi() == args[0] { + retval = match feature_test(md.flag) { + true => Some(TEST_FEATURE_ON as c_int), + false => Some(TEST_FEATURE_OFF as c_int), + }; + } + } + return retval; + } + ref s => { + if !args.is_empty() { + streams.err.append(wgettext_fmt!( + BUILTIN_ERR_ARG_COUNT2, + cmd, + opts.status_cmd.to_wstr(), + 0, + args.len() + )); + return STATUS_INVALID_ARGS; + } + match s { + STATUS_BASENAME | STATUS_DIRNAME | STATUS_FILENAME => { + let res = parser.current_filename_ffi().from_ffi(); + let f = match (res.is_empty(), opts.status_cmd) { + (false, STATUS_DIRNAME) => wdirname(res), + (false, STATUS_BASENAME) => wbasename(res), + (true, _) => wgettext!("Standard input").to_owned(), + (false, _) => res, + }; + streams.out.append(wgettext_fmt!("%ls\n", f)); + } + STATUS_FUNCTION => { + let f = match parser.get_func_name(opts.level) { + Some(f) => f, + None => wgettext!("Not a function").to_owned(), + }; + streams.out.append(wgettext_fmt!("%ls\n", f)); + } + STATUS_LINE_NUMBER => { + // TBD is how to interpret the level argument when fetching the line number. + // See issue #4161. + // streams.out.append_format(L"%d\n", parser.get_lineno(opts.level)); + streams + .out + .append(wgettext_fmt!("%d\n", parser.get_lineno().0)); + } + STATUS_IS_INTERACTIVE => { + if is_interactive_session() { + return STATUS_CMD_OK; + } else { + return STATUS_CMD_ERROR; + } + } + STATUS_IS_COMMAND_SUB => { + if parser.libdata_pod().is_subshell { + return STATUS_CMD_OK; + } else { + return STATUS_CMD_ERROR; + } + } + STATUS_IS_BLOCK => { + if parser.is_block() { + return STATUS_CMD_OK; + } else { + return STATUS_CMD_ERROR; + } + } + STATUS_IS_BREAKPOINT => { + if parser.is_breakpoint() { + return STATUS_CMD_OK; + } else { + return STATUS_CMD_ERROR; + } + } + STATUS_IS_LOGIN => { + if get_login() { + return STATUS_CMD_OK; + } else { + return STATUS_CMD_ERROR; + } + } + STATUS_IS_FULL_JOB_CTRL => { + if get_job_control_mode() == job_control_t::all { + return STATUS_CMD_OK; + } else { + return STATUS_CMD_ERROR; + } + } + STATUS_IS_INTERACTIVE_JOB_CTRL => { + if get_job_control_mode() == job_control_t::interactive { + return STATUS_CMD_OK; + } else { + return STATUS_CMD_ERROR; + } + } + STATUS_IS_NO_JOB_CTRL => { + if get_job_control_mode() == job_control_t::none { + return STATUS_CMD_OK; + } else { + return STATUS_CMD_ERROR; + } + } + STATUS_STACK_TRACE => { + streams.out.append(parser.stack_trace().from_ffi()); + } + STATUS_CURRENT_CMD => { + let var = parser.pin().libdata().get_status_vars_command().from_ffi(); + if !var.is_empty() { + streams.out.append(var); + } else { + // FIXME: C++ used `program_name` here, no clue where it's from + streams.out.append(L!("fish")); + } + streams.out.append1('\n'); + } + STATUS_CURRENT_COMMANDLINE => { + let var = parser + .pin() + .libdata() + .get_status_vars_commandline() + .from_ffi(); + streams.out.append(var); + streams.out.append1('\n'); + } + STATUS_FISH_PATH => { + let path = get_executable_path("fish"); + if path.is_empty() { + streams.err.append(wgettext_fmt!( + "%ls: Could not get executable path: '%s'\n", + cmd, + Errno::last().to_string() + )); + } + if path.is_absolute() { + let path = str2wcstring(path.as_os_str().as_bytes()); + // This is an absoulte path, we can canonicalize it + let real = match wrealpath(&path) { + Some(p) if waccess(&p, F_OK) == 0 => p, + // realpath did not work, just append the path + // - maybe this was obtained via $PATH? + _ => path, + }; + + streams.out.append(real); + streams.out.append1('\n'); + } else { + // This is a relative path, we can't canonicalize it + let path = str2wcstring(path.as_os_str().as_bytes()); + streams.out.append(path); + streams.out.append1('\n'); + } + } + STATUS_UNDEF | STATUS_SET_JOB_CONTROL | STATUS_FEATURES | STATUS_TEST_FEATURE => { + unreachable!("") + } + } + } + }; + + return retval; +} diff --git a/fish-rust/src/common.rs b/fish-rust/src/common.rs index b8678f108..d5aecd6de 100644 --- a/fish-rust/src/common.rs +++ b/fish-rust/src/common.rs @@ -1728,7 +1728,7 @@ pub fn valid_var_name(s: &wstr) -> bool { } /// Get the absolute path to the fish executable itself -fn get_executable_path(argv0: &str) -> PathBuf { +pub fn get_executable_path(argv0: &str) -> PathBuf { std::env::current_exe().unwrap_or_else(|_| PathBuf::from_str(argv0).unwrap()) } diff --git a/fish-rust/src/ffi.rs b/fish-rust/src/ffi.rs index a3491d94a..b5b5aff6a 100644 --- a/fish-rust/src/ffi.rs +++ b/fish-rust/src/ffi.rs @@ -84,6 +84,10 @@ include_cpp! { generate!("parser_t") generate!("job_t") + generate!("job_control_t") + generate!("get_job_control_mode") + generate!("set_job_control_mode") + generate!("get_login") generate!("process_t") generate!("library_data_t") generate_pod!("library_data_pod_t") @@ -400,3 +404,16 @@ impl core::convert::From for *const autocxx::c_void { value.0 as *const _ } } + +impl TryFrom<&wstr> for job_control_t { + type Error = (); + + fn try_from(value: &wstr) -> Result { + match value.to_string().as_str() { + "full" => Ok(job_control_t::all), + "interactive" => Ok(job_control_t::interactive), + "none" => Ok(job_control_t::none), + _ => Err(()), + } + } +} diff --git a/fish-rust/src/path.rs b/fish-rust/src/path.rs index b7e3c541d..40b13f24c 100644 --- a/fish-rust/src/path.rs +++ b/fish-rust/src/path.rs @@ -642,7 +642,7 @@ fn create_directory(d: &wstr) -> bool { } None => { if errno().0 == ENOENT { - let dir = wdirname(d.to_owned()); + let dir = wdirname(d); if create_directory(&dir) && wmkdir(d, 0o700) == 0 { return true; } diff --git a/fish-rust/src/wutil/mod.rs b/fish-rust/src/wutil/mod.rs index c8ac0938c..bffcc8563 100644 --- a/fish-rust/src/wutil/mod.rs +++ b/fish-rust/src/wutil/mod.rs @@ -3,6 +3,8 @@ pub mod errors; pub mod fileid; pub mod gettext; pub mod printf; +#[cfg(test)] +mod tests; pub mod wcstod; pub mod wcstoi; @@ -321,22 +323,24 @@ pub fn path_normalize_for_cd(wd: &wstr, path: &wstr) -> WString { /// Wide character version of dirname(). #[widestrs] -pub fn wdirname(mut path: WString) -> WString { +pub fn wdirname(path: impl AsRef) -> WString { + let path = path.as_ref(); // Do not use system-provided dirname (#7837). // On Mac it's not thread safe, and will error for paths exceeding PATH_MAX. // This follows OpenGroup dirname recipe. // 1: Double-slash stays. if path == "//"L { - return path; + return path.to_owned(); } // 2: All slashes => return slash. - if !path.is_empty() && path.chars().find(|c| *c == '/').is_none() { + if !path.is_empty() && path.chars().find(|&c| c != '/').is_none() { return "/"L.to_owned(); } // 3: Trim trailing slashes. + let mut path = path.to_owned(); while path.as_char_slice().last() == Some(&'/') { path.pop(); } @@ -347,13 +351,17 @@ pub fn wdirname(mut path: WString) -> WString { }; // 5: Remove trailing non-slashes. - path.truncate(last_slash + 1); - // 6: Skip as permitted. // 7: Remove trailing slashes again. - while path.as_char_slice().last() == Some(&'/') { - path.pop(); - } + path = path + .chars() + .rev() + .skip(last_slash + 1) + .skip_while(|&c| c == '/') + .collect::() + .chars() + .rev() + .collect(); // 8: Empty => return slash. if path.is_empty() { @@ -364,7 +372,8 @@ pub fn wdirname(mut path: WString) -> WString { /// Wide character version of basename(). #[widestrs] -pub fn wbasename(mut path: WString) -> WString { +pub fn wbasename(path: impl AsRef) -> WString { + let path = path.as_ref(); // This follows OpenGroup basename recipe. // 1: empty => allowed to return ".". This is what system impls do. if path.is_empty() { @@ -373,21 +382,20 @@ pub fn wbasename(mut path: WString) -> WString { // 2: Skip as permitted. // 3: All slashes => return slash. - if !path.is_empty() && path.chars().find(|c| *c == '/').is_none() { + if !path.is_empty() && path.chars().find(|&c| c != '/').is_none() { return "/"L.to_owned(); } // 4: Remove trailing slashes. - // while (!path.is_empty() && path.back() == '/') path.pop_back(); - while path.as_char_slice().last() == Some(&'/') { - path.pop(); - } - // 5: Remove up to and including last slash. - if let Some(last_slash) = path.chars().rev().position(|c| c == '/') { - path.truncate(last_slash + 1); - }; - path + path.chars() + .rev() + .skip_while(|&c| c == '/') + .take_while(|&c| c != '/') + .collect::() + .chars() + .rev() + .collect() } /// Wide character version of mkdir. diff --git a/fish-rust/src/wutil/tests.rs b/fish-rust/src/wutil/tests.rs new file mode 100644 index 000000000..44630e04f --- /dev/null +++ b/fish-rust/src/wutil/tests.rs @@ -0,0 +1,60 @@ +use super::*; +use libc::PATH_MAX; + +macro_rules! test_cases_wdirname_wbasename { + ($($name:ident: $test:expr),* $(,)?) => { + $( + #[test] + fn $name() { + let (path, dir, base) = $test; + let actual = wdirname(WString::from(path)); + assert_eq!(actual, WString::from(dir), "Wrong dirname for {:?}", path); + let actual = wbasename(WString::from(path)); + assert_eq!(actual, WString::from(base), "Wrong basename for {:?}", path); + } + )* + }; +} + +/// Helper to return a string whose length greatly exceeds PATH_MAX. +fn overlong_path() -> WString { + let mut longpath = WString::with_capacity((PATH_MAX * 2 + 10) as usize); + while longpath.len() < (PATH_MAX * 2) as usize { + longpath.push_str("/overlong"); + } + return longpath; +} + +test_cases_wdirname_wbasename! { + wdirname_wbasename_test_1: ("", ".", "."), + wdirname_wbasename_test_2: ("foo//", ".", "foo"), + wdirname_wbasename_test_3: ("foo//////", ".", "foo"), + wdirname_wbasename_test_4: ("/////foo", "/", "foo"), + wdirname_wbasename_test_5: ("/////foo", "/", "foo"), + wdirname_wbasename_test_6: ("//foo/////bar", "//foo", "bar"), + wdirname_wbasename_test_7: ("foo/////bar", "foo", "bar"), + // Examples given in XPG4.2. + wdirname_wbasename_test_8: ("/usr/lib", "/usr", "lib"), + wdirname_wbasename_test_9: ("usr", ".", "usr"), + wdirname_wbasename_test_10: ("/", "/", "/"), + wdirname_wbasename_test_11: (".", ".", "."), + wdirname_wbasename_test_12: ("..", ".", ".."), +} + +// Ensures strings which greatly exceed PATH_MAX still work (#7837). +#[test] +fn test_overlong_wdirname_wbasename() { + let path = overlong_path(); + let dir = { + let mut longpath_dir = path.clone(); + let last_slash = longpath_dir.chars().rev().position(|c| c == '/').unwrap(); + longpath_dir.truncate(longpath_dir.len() - last_slash - 1); + longpath_dir + }; + let base = "overlong"; + + let actual = wdirname(&path); + assert_eq!(actual, dir, "Wrong dirname for {:?}", path); + let actual = wbasename(&path); + assert_eq!(actual, base, "Wrong basename for {:?}", path); +} diff --git a/src/builtin.cpp b/src/builtin.cpp index a95d5bfd3..194393b9d 100644 --- a/src/builtin.cpp +++ b/src/builtin.cpp @@ -394,7 +394,7 @@ static constexpr builtin_data_t builtin_datas[] = { {L"set", &builtin_set, N_(L"Handle environment variables")}, {L"set_color", &implemented_in_rust, N_(L"Set the terminal color")}, {L"source", &builtin_source, N_(L"Evaluate contents of file")}, - {L"status", &builtin_status, N_(L"Return status information about fish")}, + {L"status", &implemented_in_rust, N_(L"Return status information about fish")}, {L"string", &builtin_string, N_(L"Manipulate strings")}, {L"switch", &builtin_generic, N_(L"Conditionally run blocks of code")}, {L"test", &implemented_in_rust, N_(L"Test a condition")}, @@ -565,6 +565,9 @@ static maybe_t try_get_rust_builtin(const wcstring &cmd) { if (cmd == L"set_color") { return RustBuiltin::SetColor; } + if (cmd == L"status") { + return RustBuiltin::Status; + } if (cmd == L"test" || cmd == L"[") { return RustBuiltin::Test; } diff --git a/src/builtin.h b/src/builtin.h index 1cb2281bc..fb15fe5bb 100644 --- a/src/builtin.h +++ b/src/builtin.h @@ -129,6 +129,7 @@ enum class RustBuiltin : int32_t { Realpath, Return, SetColor, + Status, Test, Type, Wait, diff --git a/src/parser.cpp b/src/parser.cpp index 727df0e42..a81104899 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -412,6 +412,16 @@ filename_ref_t parser_t::current_filename() const { return libdata().current_filename; } +// FFI glue +wcstring parser_t::current_filename_ffi() const { + auto filename = current_filename(); + if (filename) { + return wcstring(*filename); + } else { + return wcstring(); + } +} + bool parser_t::function_stack_is_overflowing() const { // We are interested in whether the count of functions on the stack exceeds // FISH_MAX_STACK_DEPTH. We don't separately track the number of functions, but we can have a diff --git a/src/parser.h b/src/parser.h index 695d70c78..11b759797 100644 --- a/src/parser.h +++ b/src/parser.h @@ -224,6 +224,10 @@ struct library_data_t : public library_data_pod_t { /// Used to get the full text of the current job for `status current-commandline`. wcstring commandline; } status_vars; + + public: + wcstring get_status_vars_command() const { return status_vars.command; } + wcstring get_status_vars_commandline() const { return status_vars.commandline; } }; /// The result of parser_t::eval family. @@ -468,6 +472,7 @@ class parser_t : public std::enable_shared_from_this { /// reader_current_filename, e.g. if we are evaluating a function defined in a different file /// than the one currently read. filename_ref_t current_filename() const; + wcstring current_filename_ffi() const; /// Return if we are interactive, which means we are executing a command that the user typed in /// (and not, say, a prompt).