From eacbd6156d9474b544d247894a7bc7668832aac7 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: Fri, 18 Aug 2023 07:33:51 +0200 Subject: [PATCH] Port and adopt main written in Rust We don't change anything about compilation-setup, we just immediately jump to Rust, making the eventual final swap to a Rust entrypoint very easy. There are some string-usage and format-string differences that are generally quite messy. --- fish-rust/build.rs | 1 + fish-rust/src/ffi.rs | 18 + fish-rust/src/fish.rs | 875 +++++++++++++++++++++++++++++++++++ fish-rust/src/lib.rs | 1 + src/fish.cpp | 606 +----------------------- tests/checks/bad-option.fish | 2 +- 6 files changed, 899 insertions(+), 604 deletions(-) create mode 100644 fish-rust/src/fish.rs diff --git a/fish-rust/build.rs b/fish-rust/build.rs index 934585eba..e69e9c674 100644 --- a/fish-rust/build.rs +++ b/fish-rust/build.rs @@ -62,6 +62,7 @@ fn main() { "fish-rust/src/fds.rs", "fish-rust/src/ffi_init.rs", "fish-rust/src/ffi_tests.rs", + "fish-rust/src/fish.rs", "fish-rust/src/fish_indent.rs", "fish-rust/src/function.rs", "fish-rust/src/future_feature_flags.rs", diff --git a/fish-rust/src/ffi.rs b/fish-rust/src/ffi.rs index 98be7b9d8..15d5152f3 100644 --- a/fish-rust/src/ffi.rs +++ b/fish-rust/src/ffi.rs @@ -59,6 +59,21 @@ include_cpp! { generate!("wperror") generate!("set_inheriteds_ffi") + generate!("proc_init") + generate!("misc_init") + generate!("reader_init") + generate!("term_copy_modes") + generate!("set_profiling_active") + generate!("reader_read_ffi") + generate!("restore_term_mode") + generate!("parse_util_detect_errors_ffi") + generate!("set_flog_output_file_ffi") + generate!("flog_setlinebuf_ffi") + generate!("activate_flog_categories_by_pattern") + generate!("save_term_foreground_process_group") + generate!("restore_term_foreground_process_group_for_exit") + generate!("set_cloexec") + generate!("init_input") generate_pod!("pipes_ffi_t") @@ -91,6 +106,8 @@ include_cpp! { generate!("get_job_control_mode") generate!("set_job_control_mode") generate!("get_login") + generate!("mark_login") + generate!("mark_no_exec") generate!("process_t") generate!("library_data_t") generate_pod!("library_data_pod_t") @@ -146,6 +163,7 @@ include_cpp! { generate!("reader_schedule_prompt_repaint") generate!("reader_change_history") generate!("history_session_id") + generate!("history_save_all") generate!("reader_change_cursor_selection_mode") generate!("reader_set_autosuggestion_enabled_ffi") generate!("complete_invalidate_path") diff --git a/fish-rust/src/fish.rs b/fish-rust/src/fish.rs new file mode 100644 index 000000000..67fcaa2a9 --- /dev/null +++ b/fish-rust/src/fish.rs @@ -0,0 +1,875 @@ +// +// The main loop of the fish program. +/* +Copyright (C) 2005-2008 Axel Liljencrantz + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License version 2 as +published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA +*/ + +use autocxx::prelude::*; + +use crate::{ + ast::Ast, + builtins::shared::{ + BUILTIN_ERR_MISSING, BUILTIN_ERR_UNKNOWN, STATUS_CMD_OK, STATUS_CMD_UNKNOWN, + }, + common::{ + escape_string, exit_without_destructors, get_executable_path, str2wcstring, wcs2string, + EscapeStringStyle, PROFILING_ACTIVE, PROGRAM_NAME, + }, + env::{ + environment::{env_init, EnvStack, Environment}, + ConfigPaths, EnvMode, + }, + event::{self, Event}, + ffi::{self, Repin}, + flog::{self, activate_flog_categories_by_pattern, set_flog_file_fd, FLOG, FLOGF}, + function, future_feature_flags as features, + history::start_private_mode, + parse_constants::{ParseErrorList, ParseErrorListFfi, ParseTreeFlags}, + parse_tree::{ParsedSource, ParsedSourceRefFFI}, + path::path_get_config, + signal::{signal_clear_cancel, signal_unblock_all}, + threads::{self, asan_maybe_exit}, + topic_monitor, + wchar::prelude::*, + wchar_ffi::{WCharFromFFI, WCharToFFI}, + wutil::waccess, +}; +use std::env; +use std::ffi::{CString, OsStr, OsString}; +use std::fs::File; +use std::mem::MaybeUninit; +use std::os::unix::prelude::*; +use std::path::{Path, PathBuf}; +use std::sync::atomic::Ordering; +use std::sync::Arc; + +// FIXME: when the crate is actually called fish and not fish-rust, read this from cargo +// See: https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-crates +// for reference +const PACKAGE_NAME: &str = "fish"; // env!("CARGO_PKG_NAME"); + +// FIXME: the following should just use env!(), this is to make `cargo test` work without CMake for now +const DOC_DIR: &str = { + match option_env!("DOCDIR") { + Some(e) => e, + None => "(unused)", + } +}; +const DATA_DIR: &str = { + match option_env!("DATADIR") { + Some(e) => e, + None => "(unused)", + } +}; +const SYSCONF_DIR: &str = { + match option_env!("SYSCONFDIR") { + Some(e) => e, + None => "(unused)", + } +}; +const BIN_DIR: &str = { + match option_env!("BINDIR") { + Some(e) => e, + None => "(unused)", + } +}; + +// C++ had this as optional, and used CMAKE_BINARY_DIR, +// should probably be swapped to `OUT_DIR` once CMake is gone? +// const OUT_DIR: &str = env!("OUT_DIR", "OUT_DIR was not specified"); +const OUT_DIR: &str = env!("FISH_BUILD_DIR"); + +/// container to hold the options specified within the command line +#[derive(Default, Debug)] +struct FishCmdOpts { + /// Future feature flags values string + features: WString, + /// File path for debug output. + debug_output: Option, + /// File path for profiling output, or empty for none. + profile_output: Option, + profile_startup_output: Option, + /// Commands to be executed in place of interactive shell. + batch_cmds: Vec, + /// Commands to execute after the shell's config has been read. + postconfig_cmds: Vec, + /// Whether to print rusage-self stats after execution. + print_rusage_self: bool, + /// Whether no-config is set. + no_config: bool, + /// Whether no-exec is set. + no_exec: bool, + /// Whether this is a login shell. + is_login: bool, + /// Whether this is an interactive session. + is_interactive_session: bool, + /// Whether to enable private mode. + enable_private_mode: bool, +} + +/// \return a timeval converted to milliseconds. +#[allow(clippy::unnecessary_cast)] +fn tv_to_msec(tv: &libc::timeval) -> i64 { + // milliseconds per second + let mut msec = tv.tv_sec as i64 * 1000; + // microseconds per millisecond + msec += tv.tv_usec as i64 / 1000; + msec +} + +fn print_rusage_self() { + let mut rs = MaybeUninit::uninit(); + if unsafe { libc::getrusage(libc::RUSAGE_SELF, rs.as_mut_ptr()) } != 0 { + let s = CString::new("getrusage").unwrap(); + unsafe { libc::perror(s.as_ptr()) } + return; + } + let rs: libc::rusage = unsafe { rs.assume_init() }; + let rss_kb = if cfg!(target_os = "macos") { + // mac use bytes. + rs.ru_maxrss / 1024 + } else { + // Everyone else uses KB. + rs.ru_maxrss + }; + + let user_time = tv_to_msec(&rs.ru_utime); + let sys_time = tv_to_msec(&rs.ru_stime); + let total_time = user_time + sys_time; + let signals = rs.ru_nsignals; + + eprintln!(" rusage self:"); + eprintln!(" user time: {sys_time} ms"); + eprintln!(" sys time: {user_time} ms"); + eprintln!(" total time: {total_time} ms"); + eprintln!(" max rss: {rss_kb} kb"); + eprintln!(" signals: {signals}"); +} + +fn determine_config_directory_paths(argv0: impl AsRef) -> ConfigPaths { + // PORTING: why is this not just an associated method on ConfigPaths? + + let mut paths = ConfigPaths::default(); + let mut done = false; + let exec_path = get_executable_path(argv0.as_ref()); + if let Ok(exec_path) = exec_path.canonicalize() { + FLOG!( + config, + format!("exec_path: {:?}, argv[0]: {:?}", exec_path, argv0.as_ref()) + ); + // TODO: we should determine program_name from argv0 somewhere in this file + + // Detect if we're running right out of the CMAKE build directory + if exec_path.starts_with(OUT_DIR) { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + FLOG!( + config, + "Running out of target directory, using paths relative to CARGO_MANIFEST_DIR:\n", + manifest_dir.display() + ); + done = true; + paths = ConfigPaths { + data: manifest_dir.join("share"), + sysconf: manifest_dir.join("etc"), + doc: manifest_dir.join("user_doc/html"), + bin: OUT_DIR.into(), + } + } + + if !done { + // The next check is that we are in a reloctable directory tree + let installed_suffix = Path::new("/bin/fish"); + let just_a_fish = Path::new("/fish"); + let suffix = if exec_path.ends_with(installed_suffix) { + Some(installed_suffix) + } else if exec_path.ends_with(just_a_fish) { + FLOG!( + config, + "'fish' not in a 'bin/', trying paths relative to source tree" + ); + Some(just_a_fish) + } else { + None + }; + + if let Some(suffix) = suffix { + let seems_installed = suffix == installed_suffix; + + let mut base_path = exec_path; + base_path.shrink_to(base_path.as_os_str().len() - suffix.as_os_str().len()); + let base_path = base_path; + + paths = if seems_installed { + ConfigPaths { + data: base_path.join("share/fish"), + sysconf: base_path.join("etc/fish"), + doc: base_path.join("share/doc/fish"), + bin: base_path.join("bin"), + } + } else { + ConfigPaths { + data: base_path.join("share"), + sysconf: base_path.join("etc"), + doc: base_path.join("user_doc/html"), + bin: base_path, + } + }; + + if paths.data.exists() && paths.sysconf.exists() { + // The docs dir may not exist; in that case fall back to the compiled in path. + if !paths.doc.exists() { + paths.doc = PathBuf::from(DOC_DIR); + } + done = true; + } + } + } + } + + if !done { + // Fall back to what got compiled in. + FLOG!(config, "Using compiled in paths:"); + paths = ConfigPaths { + data: PathBuf::from(DATA_DIR).join("fish"), + sysconf: PathBuf::from(SYSCONF_DIR).join("fish"), + doc: DOC_DIR.into(), + bin: BIN_DIR.into(), + } + } + + FLOGF!( + config, + "determine_config_directory_paths() results:\npaths.data: %ls\npaths.sysconf: \ + %ls\npaths.doc: %ls\npaths.bin: %ls", + paths.data.display().to_string(), + paths.sysconf.display().to_string(), + paths.doc.display().to_string(), + paths.bin.display().to_string() + ); + + paths +} + +// Source the file config.fish in the given directory. +fn source_config_in_directory(parser: &mut ffi::parser_t, dir: &wstr) { + // If the config.fish file doesn't exist or isn't readable silently return. Fish versions up + // thru 2.2.0 would instead try to source the file with stderr redirected to /dev/null to deal + // with that possibility. + // + // This introduces a race condition since the readability of the file can change between this + // test and the execution of the 'source' command. However, that is not a security problem in + // this context so we ignore it. + let config_pathname = dir.to_owned() + L!("/config.fish"); + let escaped_pathname = escape_string(dir, EscapeStringStyle::default()) + L!("/config.fish"); + if waccess(&config_pathname, libc::R_OK) != 0 { + FLOGF!( + config, + "not sourcing %ls (not readable or does not exist)", + escaped_pathname + ); + return; + } + FLOG!(config, "sourcing", escaped_pathname); + + let cmd: WString = L!("builtin source ").to_owned() + escaped_pathname.as_utfstr(); + + parser.libdata_pod().within_fish_init = true; + // PORTING: you need to call `within_unique_ptr`, otherwise it is a no-op + let _ = parser + .pin() + .eval_string_ffi1(&cmd.to_ffi()) + .within_unique_ptr(); + parser.libdata_pod().within_fish_init = false; +} + +/// Parse init files. exec_path is the path of fish executable as determined by argv[0]. +fn read_init(parser: &mut ffi::parser_t, paths: &ConfigPaths) { + source_config_in_directory(parser, &str2wcstring(paths.data.as_os_str().as_bytes())); + source_config_in_directory(parser, &str2wcstring(paths.sysconf.as_os_str().as_bytes())); + + // We need to get the configuration directory before we can source the user configuration file. + // If path_get_config returns false then we have no configuration directory and no custom config + // to load. + if let Some(config_dir) = path_get_config() { + source_config_in_directory(parser, &config_dir); + } +} + +fn run_command_list(parser: &mut ffi::parser_t, cmds: &[OsString]) -> i32 { + let mut retval = STATUS_CMD_OK; + for cmd in cmds { + let cmd_wcs = str2wcstring(cmd.as_bytes()); + + let mut errors = ParseErrorList::new(); + let ast = Ast::parse(&cmd_wcs, ParseTreeFlags::empty(), Some(&mut errors)); + let errored = ast.errored() || { + // parse_util_detect_errors_in_ast is just partially ported + // parse_util_detect_errors_in_ast(&ast, &cmd_wcs, Some(&mut errors)).is_err(); + + let mut errors_ffi = ParseErrorListFfi(errors.clone()); + let res = ffi::parse_util_detect_errors_ffi( + &ast as *const Ast as *const _, + &cmd_wcs.to_ffi(), + &mut errors_ffi as *mut ParseErrorListFfi as *mut _, + ); + errors = errors_ffi.0; + res != 0 + }; + + if !errored { + // Construct a parsed source ref. + // Be careful to transfer ownership, this could be a very large string. + + let ps = ParsedSourceRefFFI(Some(Arc::new(ParsedSource::new(cmd_wcs, ast)))); + // this casting is needed since rust defines the type, so the type is incomplete when we + // read the headers + let _ = parser + .pin() + .eval_parsed_source_ffi1( + &ps as *const ParsedSourceRefFFI as *const _, + ffi::block_type_t::top, + ) + .within_unique_ptr(); + retval = STATUS_CMD_OK; + } else { + let mut sb = WString::new().to_ffi(); + let errors_ffi = ParseErrorListFfi(errors); + parser.pin().get_backtrace_ffi( + &cmd_wcs.to_ffi(), + &errors_ffi as *const ParseErrorListFfi as *const _, + sb.pin_mut(), + ); + // fwprint! does not seem to work? + eprint!("{}", sb.from_ffi()); + // XXX: Why is this the return for "unknown command"? + retval = STATUS_CMD_UNKNOWN; + } + } + + retval.unwrap() +} + +fn fish_parse_opt(args: &mut [&wstr], opts: &mut FishCmdOpts) -> usize { + use crate::wgetopt::{wgetopter_t, wopt, woption, woption_argument_t::*}; + + const RUSAGE_ARG: char = 1 as char; + const PRINT_DEBUG_CATEGORIES_ARG: char = 2 as char; + const PROFILE_STARTUP_ARG: char = 4 as char; + + const SHORT_OPTS: &wstr = L!("+hPilNnvc:C:p:d:f:D:o:"); + const LONG_OPTS: &[woption<'static>] = &[ + wopt(L!("command"), required_argument, 'c'), + wopt(L!("init-command"), required_argument, 'C'), + wopt(L!("features"), required_argument, 'f'), + wopt(L!("debug"), required_argument, 'd'), + wopt(L!("debug-output"), required_argument, 'o'), + wopt(L!("debug-stack-frames"), required_argument, 'D'), + wopt(L!("interactive"), no_argument, 'i'), + wopt(L!("login"), no_argument, 'l'), + wopt(L!("no-config"), no_argument, 'N'), + wopt(L!("no-execute"), no_argument, 'n'), + wopt(L!("print-rusage-self"), no_argument, RUSAGE_ARG), + wopt( + L!("print-debug-categories"), + no_argument, + PRINT_DEBUG_CATEGORIES_ARG, + ), + wopt(L!("profile"), required_argument, 'p'), + wopt( + L!("profile-startup"), + required_argument, + PROFILE_STARTUP_ARG, + ), + wopt(L!("private"), required_argument, 'P'), + wopt(L!("help"), no_argument, 'h'), + wopt(L!("version"), no_argument, 'v'), + ]; + + let mut w = wgetopter_t::new(SHORT_OPTS, LONG_OPTS, args); + while let Some(c) = w.wgetopt_long() { + match c { + 'c' => opts + .batch_cmds + .push(OsString::from_vec(wcs2string(w.woptarg.unwrap()))), + 'C' => opts + .postconfig_cmds + .push(OsString::from_vec(wcs2string(w.woptarg.unwrap()))), + 'd' => { + ffi::activate_flog_categories_by_pattern(w.woptarg.unwrap()); + activate_flog_categories_by_pattern(w.woptarg.unwrap()); + for cat in flog::categories::all_categories() { + if cat.enabled.load(Ordering::Relaxed) { + println!("Debug enabled for category: {}", cat.name); + } + } + } + 'o' => opts.debug_output = Some(OsString::from_vec(wcs2string(w.woptarg.unwrap()))), + 'f' => opts.features = w.woptarg.unwrap().to_owned(), + 'h' => opts.batch_cmds.push("__fish_print_help fish".into()), + 'i' => opts.is_interactive_session = true, + 'l' => opts.is_login = true, + 'N' => { + opts.no_config = true; + // --no-config implies private mode, we won't be saving history + opts.enable_private_mode = true; + } + 'n' => opts.no_exec = true, + RUSAGE_ARG => opts.print_rusage_self = true, + PRINT_DEBUG_CATEGORIES_ARG => { + let cats = flog::categories::all_categories(); + // Compute width of longest name. + let mut name_width = 0; + for cat in cats.iter() { + name_width = usize::max(name_width, cat.name.len()); + } + // A little extra space. + name_width += 2; + for cat in cats.iter() { + let desc = wgettext_str(cat.description); + // https://doc.rust-lang.org/std/fmt/#syntax + // this is left-justified + println!("{: opts.profile_output = Some(OsString::from_vec(wcs2string(w.woptarg.unwrap()))), + PROFILE_STARTUP_ARG => { + // With "--profile-startup" we immediately turn profiling on. + opts.profile_startup_output = + Some(OsString::from_vec(wcs2string(w.woptarg.unwrap()))); + PROFILING_ACTIVE.store(true); + ffi::set_profiling_active(true); + } + 'P' => opts.enable_private_mode = true, + 'v' => { + // FIXME: this was _(L"%s, version %s\n"), but rust-fwprintf! takes a literal instead of an expr + // and appears to not print anything + print!( + "{}", + wgettext_fmt!("%s, version %s\n", PACKAGE_NAME, crate::BUILD_VERSION) + ); + std::process::exit(0); + } + 'D' => { + // TODO: Option is currently useless. + // Either remove it or make it work with FLOG. + } + '?' => { + eprintln!( + "{}", + wgettext_fmt!(BUILTIN_ERR_UNKNOWN, "fish", args[w.woptind - 1]) + ); + std::process::exit(1) + } + ':' => { + eprintln!( + "{}", + wgettext_fmt!(BUILTIN_ERR_MISSING, "fish", args[w.woptind - 1]) + ); + std::process::exit(1) + } + _ => panic!("unexpected retval from wgetoptr_t"), + } + } + let optind = w.woptind; + + // If our command name begins with a dash that implies we're a login shell. + opts.is_login |= args[0].char_at(0) == '-'; + + // We are an interactive session if we have not been given an explicit + // command or file to execute and stdin is a tty. Note that the -i or + // --interactive options also force interactive mode. + if opts.batch_cmds.is_empty() + && optind == args.len() + && unsafe { libc::isatty(libc::STDIN_FILENO) != 0 } + { + ffi::set_interactive_session(true); + } + + optind +} + +fn cstr_from_osstr(s: &OsStr) -> CString { + // is there no better way to do this? + // this is + // CStr::from_bytes_until_nul(s.as_bytes()).unwrap() + // except we need to add the nul if it is not present + CString::new( + s.as_bytes() + .iter() + .cloned() + .take_while(|&c| c != b'\0') + .collect::>(), + ) + .unwrap() +} + +fn main() -> i32 { + let mut args: Vec = env::args_os() + .map(|osstr| str2wcstring(osstr.as_bytes())) + .collect(); + + PROGRAM_NAME + .set(L!("fish")) + .expect("multiple entrypoints setting PROGRAM_NAME"); + + let mut res = 1; + let mut my_optind; + + topic_monitor::topic_monitor_init(); + threads::init(); + signal_unblock_all(); + + { + let s = CString::new("").unwrap(); + unsafe { + libc::setlocale(libc::LC_ALL, s.as_ptr()); + } + } + + if args.is_empty() { + args.push("fish".into()); + } + + if let Some(debug_categories) = env::var_os("FISH_DEBUG") { + let s = str2wcstring(debug_categories.as_bytes()); + activate_flog_categories_by_pattern(&s); + ffi::activate_flog_categories_by_pattern(s); + } + + let owning_args = args; + let mut args_for_opts: Vec<&wstr> = owning_args.iter().map(WString::as_utfstr).collect(); + + let mut opts = FishCmdOpts::default(); + my_optind = fish_parse_opt(&mut args_for_opts, &mut opts); + + let args = args_for_opts; + + // Direct any debug output right away. + // --debug-output takes precedence, otherwise $FISH_DEBUG_OUTPUT is used. + // PORTING: this is a slight difference from C++, we now skip reading the env var if the argument is an empty string + if opts.debug_output.is_none() { + opts.debug_output = env::var_os("FISH_DEBUG_OUTPUT"); + } + + let mut debug_output = std::ptr::null_mut(); + if let Some(debug_path) = opts.debug_output { + let path = cstr_from_osstr(&debug_path); + let mode = CString::new("w").unwrap(); + let debug_file = unsafe { libc::fopen(path.as_ptr(), mode.as_ptr()) }; + + if debug_file.is_null() { + eprintln!("Could not open file {:?}", debug_output); + let s = CString::new("fopen").unwrap(); + unsafe { libc::perror(s.as_ptr()) } + std::process::exit(-1); + } + + unsafe { ffi::set_cloexec(c_int(libc::fileno(debug_file)), true) }; + ffi::flog_setlinebuf_ffi(debug_file as *mut _); + ffi::set_flog_output_file_ffi(debug_file as *mut _); + set_flog_file_fd(unsafe { libc::fileno(debug_file) }); + + debug_output = debug_file; + + /* TODO: just use File when C++ does not need a *mut FILE + + debug_output = match File::options() + .write(true) + .truncate(true) + .create(true) + .open(debug_path) + { + Ok(dbg_file) => { + // Rust sets O_CLOEXEC by default + // https://github.com/rust-lang/rust/blob/07438b0928c6691d6ee734a5a77823ec143be94d/library/std/src/sys/unix/fs.rs#L1059 + + flog::set_flog_file_fd(dbg_file.as_raw_fd()); + Some(dbg_file) + } + Err(e) => { + // TODO: should not be debug-print + eprintln!("Could not open file {:?}", debug_output); + eprintln!("{}", e); + std::process::exit(1); + } + }; + */ + } + + if opts.is_interactive_session && opts.no_exec { + FLOG!( + warning, + wgettext!("Can not use the no-execute mode when running an interactive session") + ); + opts.no_exec = false; + } + + // Apply our options + if opts.is_login { + ffi::mark_login(); + } + if opts.no_exec { + ffi::mark_no_exec(); + } + if opts.is_interactive_session { + ffi::set_interactive_session(true); + } + if opts.enable_private_mode { + start_private_mode(EnvStack::globals()); + } + + // Only save (and therefore restore) the fg process group if we are interactive. See issues + // #197 and #1002. + if ffi::is_interactive_session() { + // save_term_foreground_process_group(); + ffi::save_term_foreground_process_group(); + } + + let mut paths: Option = None; + // If we're not executing, there's no need to find the config. + if !opts.no_exec { + // PORTING: C++ had not converted, we must revert + paths = Some(determine_config_directory_paths(OsString::from_vec( + wcs2string(args[0]), + ))); + env_init(paths.as_ref(), !opts.no_config, opts.no_config); + } + + // Set features early in case other initialization depends on them. + // Start with the ones set in the environment, then those set on the command line (so the + // command line takes precedence). + if let Some(features_var) = EnvStack::globals().get(L!("fish_features")) { + for s in features_var.as_list() { + features::set_from_string(s.as_utfstr()); + } + } + features::set_from_string(opts.features.as_utfstr()); + ffi::proc_init(); + ffi::misc_init(); + ffi::reader_init(); + + let parser = unsafe { &mut *ffi::parser_t::principal_parser_ffi() }; + parser.pin().set_syncs_uvars(!opts.no_config); + + if !opts.no_exec && !opts.no_config { + read_init(parser, paths.as_ref().unwrap()); + } + + if ffi::is_interactive_session() && opts.no_config && !opts.no_exec { + // If we have no config, we default to the default key bindings. + parser.get_vars().set_one( + L!("fish_key_bindings"), + EnvMode::UNEXPORT, + L!("fish_default_key_bindings").to_owned(), + ); + if function::exists(L!("fish_default_key_bindings"), parser) { + run_command_list(parser, &[OsString::from("fish_default_key_bindings")]); + } + } + + // Re-read the terminal modes after config, it might have changed them. + ffi::term_copy_modes(); + + // Stomp the exit status of any initialization commands (issue #635). + // PORTING: it is actually really nice that this just compiles, assuming it works + parser + .pin() + .set_last_statuses(ffi::statuses_t::just(c_int(STATUS_CMD_OK.unwrap())).within_box()); + + // TODO: if-let-chains + if opts.profile_startup_output.is_some() && opts.profile_startup_output != opts.profile_output { + let s = cstr_from_osstr(&opts.profile_startup_output.unwrap()); + parser.pin().emit_profiling(s.as_ptr()); + + // If we are profiling both, ensure the startup data only + // ends up in the startup file. + parser.pin().clear_profiling(); + } + + PROFILING_ACTIVE.store(opts.profile_output.is_some()); + ffi::set_profiling_active(opts.profile_output.is_some()); + + // Run post-config commands specified as arguments, if any. + if !opts.postconfig_cmds.is_empty() { + res = run_command_list(parser, &opts.postconfig_cmds); + } + + // Clear signals in case we were interrupted (#9024). + signal_clear_cancel(); + + if !opts.batch_cmds.is_empty() { + // Run the commands specified as arguments, if any. + if ffi::get_login() { + // Do something nasty to support OpenSUSE assuming we're bash. This may modify cmds. + fish_xdm_login_hack_hack_hack_hack(&mut opts.batch_cmds, &args[my_optind..]); + } + + // Pass additional args as $argv. + // Note that we *don't* support setting argv[0]/$0, unlike e.g. bash. + // PORTING: the args were converted to WString here in C++ + let list = &args[my_optind..]; + parser.get_vars().set( + L!("argv"), + EnvMode::default(), + list.iter().map(|&s| s.to_owned()).collect(), + ); + res = run_command_list(parser, &opts.batch_cmds); + parser.libdata_pod().exit_current_script = false; + } else if my_optind == args.len() { + // Implicitly interactive mode. + if opts.no_exec && unsafe { libc::isatty(libc::STDIN_FILENO) != 0 } { + FLOG!( + error, + "no-execute mode enabled and no script given. Exiting" + ); + // above line should always exit + return libc::EXIT_FAILURE; + } + res = ffi::reader_read_ffi(parser.pin(), c_int(libc::STDIN_FILENO)).into(); + } else { + // C++ had not converted at this point, we must undo + let n = wcs2string(args[my_optind]); + let path = OsStr::from_bytes(&n); + my_optind += 1; + // Rust sets cloexec by default, see above + // We don't need autoclose_fd_t when we use File, it will be closed on drop. + match File::open(path) { + Err(e) => { + FLOGF!( + error, + wgettext!("Error reading script file '%s':"), + path.to_string_lossy() + ); + eprintln!("{}", e); + } + Ok(f) => { + // PORTING: the args were converted to WString here in C++ + let list = &args[my_optind..]; + parser.get_vars().set( + L!("argv"), + EnvMode::default(), + list.iter().map(|&s| s.to_owned()).collect(), + ); + let rel_filename = &args[my_optind - 1]; + // PORTING: this used to be `scoped_push` + let old_filename = parser.pin().current_filename_ffi().from_ffi(); + parser.pin().set_filename_ffi(rel_filename.to_ffi()); + res = ffi::reader_read_ffi(parser.pin(), c_int(f.as_raw_fd())).into(); + if res != 0 { + FLOGF!( + warning, + wgettext!("Error while reading file %ls\n"), + path.to_string_lossy() + ); + } + parser.pin().set_filename_ffi(old_filename.to_ffi()); + } + } + } + + let exit_status = if res != 0 { + STATUS_CMD_UNKNOWN.unwrap() + } else { + parser.pin().get_last_status().into() + }; + + event::fire( + parser, + Event::process_exit(unsafe { libc::getpid() }, exit_status), + ); + + // Trigger any exit handlers. + event::fire_generic( + parser, + L!("fire_exit").to_owned(), + vec![exit_status.to_wstring()], + ); + + ffi::restore_term_mode(); + // this is ported, but not adopted + ffi::restore_term_foreground_process_group_for_exit(); + + if let Some(profile_output) = opts.profile_output { + let s = cstr_from_osstr(&profile_output); + parser.pin().emit_profiling(s.as_ptr()); + } + + ffi::history_save_all(); + if opts.print_rusage_self { + print_rusage_self(); + } + + if !debug_output.is_null() { + unsafe { libc::fclose(debug_output) }; + } + + asan_maybe_exit(exit_status); + exit_without_destructors(exit_status) +} + +// https://github.com/fish-shell/fish-shell/issues/367 +// +// With them the Seed of Wisdom did I sow, +// And with my own hand labour'd it to grow: +// And this was all the Harvest that I reap'd--- +// "I came like Water, and like Wind I go." + +fn escape_single_quoted_hack_hack_hack_hack(s: &wstr) -> OsString { + let mut result = OsString::with_capacity(s.len() + 2); + result.push("\'"); + for c in s.chars() { + // Escape backslashes and single quotes only. + if matches!(c, '\\' | '\'') { + result.push("\\"); + } + result.push(c.to_string()) + } + result.push("\'"); + return result; +} + +fn fish_xdm_login_hack_hack_hack_hack(cmds: &mut Vec, args: &[&wstr]) -> bool { + if cmds.len() != 1 { + return false; + } + + let mut result = false; + let cmd = cmds.get(0).unwrap(); + if cmd == "exec \"${@}\"" || cmd == "exec \"$@\"" { + // We're going to construct a new command that starts with exec, and then has the + // remaining arguments escaped. + let mut new_cmd = OsString::from("exec"); + for arg in args { + new_cmd.push(" "); + new_cmd.push(escape_single_quoted_hack_hack_hack_hack(arg)); + } + + cmds[0] = new_cmd; + result = true; + } + return result; +} + +#[cxx::bridge] +mod fish_ffi { + extern "Rust" { + #[cxx_name = "rust_main"] + fn main() -> i32; + } +} diff --git a/fish-rust/src/lib.rs b/fish-rust/src/lib.rs index e4e644c24..a0e9903ea 100644 --- a/fish-rust/src/lib.rs +++ b/fish-rust/src/lib.rs @@ -40,6 +40,7 @@ mod fds; mod ffi; mod ffi_init; mod ffi_tests; +mod fish; mod fish_indent; mod flog; mod function; diff --git a/src/fish.cpp b/src/fish.cpp index 26aaa08e1..1845ecd5b 100644 --- a/src/fish.cpp +++ b/src/fish.cpp @@ -16,611 +16,11 @@ You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ -#include "config.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include "ast.h" #include "common.h" -#include "cxxgen.h" -#include "env.h" -#include "event.h" -#include "expand.h" -#include "fallback.h" // IWYU pragma: keep -#include "fds.h" +#include "fish.rs.h" #include "ffi_baggage.h" -#include "ffi_init.rs.h" -#include "fish_version.h" -#include "flog.h" -#include "function.h" -#include "future_feature_flags.h" -#include "global_safety.h" -#include "history.h" -#include "io.h" -#include "maybe.h" -#include "parse_constants.h" -#include "parse_tree.h" -#include "parse_util.h" -#include "parser.h" -#include "path.h" -#include "proc.h" -#include "reader.h" -#include "signals.h" -#include "threads.rs.h" -#include "wcstringutil.h" -#include "wutil.h" // IWYU pragma: keep - -// container to hold the options specified within the command line -class fish_cmd_opts_t { - public: - // Future feature flags values string - wcstring features; - // File path for debug output. - std::string debug_output; - // File path for profiling output, or empty for none. - std::string profile_output; - std::string profile_startup_output; - // Commands to be executed in place of interactive shell. - std::vector batch_cmds; - // Commands to execute after the shell's config has been read. - std::vector postconfig_cmds; - /// Whether to print rusage-self stats after execution. - bool print_rusage_self{false}; - /// Whether no-config is set. - bool no_config{false}; - /// Whether no-exec is set. - bool no_exec{false}; - /// Whether this is a login shell. - bool is_login{false}; - /// Whether this is an interactive session. - bool is_interactive_session{false}; - /// Whether to enable private mode. - bool enable_private_mode{false}; -}; - -/// \return a timeval converted to milliseconds. -static long long tv_to_msec(const struct timeval &tv) { - long long msec = static_cast(tv.tv_sec) * 1000; // milliseconds per second - msec += tv.tv_usec / 1000; // microseconds per millisecond - return msec; -} - -static void print_rusage_self(FILE *fp) { -#ifndef HAVE_GETRUSAGE - fprintf(fp, "getrusage() not supported on this platform"); - return; -#else - struct rusage rs; - if (getrusage(RUSAGE_SELF, &rs)) { - perror("getrusage"); - return; - } -#if defined(__APPLE__) && defined(__MACH__) - // Macs use bytes. - long rss_kb = rs.ru_maxrss / 1024; -#else - // Everyone else uses KB. - long rss_kb = rs.ru_maxrss; -#endif - fprintf(fp, " rusage self:\n"); - fprintf(fp, " user time: %lld ms\n", tv_to_msec(rs.ru_utime)); - fprintf(fp, " sys time: %lld ms\n", tv_to_msec(rs.ru_stime)); - fprintf(fp, " total time: %lld ms\n", tv_to_msec(rs.ru_utime) + tv_to_msec(rs.ru_stime)); - fprintf(fp, " max rss: %ld kb\n", rss_kb); - fprintf(fp, " signals: %ld\n", rs.ru_nsignals); -#endif -} - -static bool has_suffix(const std::string &path, const char *suffix, bool ignore_case) { - size_t pathlen = path.size(), suffixlen = std::strlen(suffix); - return pathlen >= suffixlen && - !(ignore_case ? strcasecmp : std::strcmp)(path.c_str() + pathlen - suffixlen, suffix); -} - -/// Modifies the given path by calling realpath. Returns true if realpath succeeded, false -/// otherwise. -static bool get_realpath(std::string &path) { - char buff[PATH_MAX], *ptr; - if ((ptr = realpath(path.c_str(), buff))) { - path = ptr; - } - return ptr != nullptr; -} - -static struct config_paths_t determine_config_directory_paths(const char *argv0) { - struct config_paths_t paths; - bool done = false; - std::string exec_path = get_executable_path(argv0); - if (get_realpath(exec_path)) { - FLOGF(config, L"exec_path: '%s', argv[0]: '%s'", exec_path.c_str(), argv0); - // TODO: we should determine program_name from argv0 somewhere in this file - -#ifdef CMAKE_BINARY_DIR - // Detect if we're running right out of the CMAKE build directory - if (string_prefixes_string(CMAKE_BINARY_DIR, exec_path.c_str())) { - FLOGF(config, - "Running out of build directory, using paths relative to CMAKE_SOURCE_DIR:\n %s", - CMAKE_SOURCE_DIR); - - done = true; - paths.data = wcstring{L"" CMAKE_SOURCE_DIR} + L"/share"; - paths.sysconf = wcstring{L"" CMAKE_SOURCE_DIR} + L"/etc"; - paths.doc = wcstring{L"" CMAKE_SOURCE_DIR} + L"/user_doc/html"; - paths.bin = wcstring{L"" CMAKE_BINARY_DIR}; - } -#endif - - if (!done) { - // The next check is that we are in a reloctable directory tree - const char *installed_suffix = "/bin/fish"; - const char *just_a_fish = "/fish"; - const char *suffix = nullptr; - - if (has_suffix(exec_path, installed_suffix, false)) { - suffix = installed_suffix; - } else if (has_suffix(exec_path, just_a_fish, false)) { - FLOGF(config, L"'fish' not in a 'bin/', trying paths relative to source tree"); - suffix = just_a_fish; - } - - if (suffix) { - bool seems_installed = (suffix == installed_suffix); - - wcstring base_path = str2wcstring(exec_path); - base_path.resize(base_path.size() - std::strlen(suffix)); - - paths.data = base_path + (seems_installed ? L"/share/fish" : L"/share"); - paths.sysconf = base_path + (seems_installed ? L"/etc/fish" : L"/etc"); - paths.doc = base_path + (seems_installed ? L"/share/doc/fish" : L"/user_doc/html"); - paths.bin = base_path + (seems_installed ? L"/bin" : L""); - - // Check only that the data and sysconf directories exist. Handle the doc - // directories separately. - struct stat buf; - if (0 == wstat(paths.data, &buf) && 0 == wstat(paths.sysconf, &buf)) { - // The docs dir may not exist; in that case fall back to the compiled in path. - if (0 != wstat(paths.doc, &buf)) { - paths.doc = L"" DOCDIR; - } - done = true; - } - } - } - } - - if (!done) { - // Fall back to what got compiled in. - FLOGF(config, L"Using compiled in paths:"); - paths.data = L"" DATADIR "/fish"; - paths.sysconf = L"" SYSCONFDIR "/fish"; - paths.doc = L"" DOCDIR; - paths.bin = L"" BINDIR; - } - - FLOGF(config, - L"determine_config_directory_paths() results:\npaths.data: %ls\npaths.sysconf: " - L"%ls\npaths.doc: %ls\npaths.bin: %ls", - paths.data.c_str(), paths.sysconf.c_str(), paths.doc.c_str(), paths.bin.c_str()); - return paths; -} - -// Source the file config.fish in the given directory. -static void source_config_in_directory(parser_t &parser, const wcstring &dir) { - // If the config.fish file doesn't exist or isn't readable silently return. Fish versions up - // thru 2.2.0 would instead try to source the file with stderr redirected to /dev/null to deal - // with that possibility. - // - // This introduces a race condition since the readability of the file can change between this - // test and the execution of the 'source' command. However, that is not a security problem in - // this context so we ignore it. - const wcstring config_pathname = dir + L"/config.fish"; - const wcstring escaped_pathname = escape_string(dir) + L"/config.fish"; - if (waccess(config_pathname, R_OK) != 0) { - FLOGF(config, L"not sourcing %ls (not readable or does not exist)", - escaped_pathname.c_str()); - return; - } - FLOGF(config, L"sourcing %ls", escaped_pathname.c_str()); - - const wcstring cmd = L"builtin source " + escaped_pathname; - - parser.libdata().within_fish_init = true; - parser.eval(cmd, io_chain_t()); - parser.libdata().within_fish_init = false; -} - -/// Parse init files. exec_path is the path of fish executable as determined by argv[0]. -static void read_init(parser_t &parser, const struct config_paths_t &paths) { - source_config_in_directory(parser, paths.data); - source_config_in_directory(parser, paths.sysconf); - - // We need to get the configuration directory before we can source the user configuration file. - // If path_get_config returns false then we have no configuration directory and no custom config - // to load. - wcstring config_dir; - if (path_get_config(config_dir)) { - source_config_in_directory(parser, config_dir); - } -} - -static int run_command_list(parser_t &parser, const std::vector &cmds, - const io_chain_t &io) { - int retval = STATUS_CMD_OK; - for (const auto &cmd : cmds) { - wcstring cmd_wcs = str2wcstring(cmd); - // Parse into an ast and detect errors. - auto errors = new_parse_error_list(); - auto ast = ast_parse(cmd_wcs, parse_flag_none, &*errors); - bool errored = ast->errored(); - if (!errored) { - errored = parse_util_detect_errors(*ast, cmd_wcs, &*errors); - } - if (!errored) { - // Construct a parsed source ref. - // Be careful to transfer ownership, this could be a very large string. - auto ps = new_parsed_source_ref(cmd_wcs, *ast); - parser.eval_parsed_source(*ps, io, {}, block_type_t::top); - retval = STATUS_CMD_OK; - } else { - wcstring sb; - parser.get_backtrace(cmd_wcs, *errors, sb); - std::fwprintf(stderr, L"%ls", sb.c_str()); - // XXX: Why is this the return for "unknown command"? - retval = STATUS_CMD_UNKNOWN; - } - } - - return retval; -} - -/// Parse the argument list, return the index of the first non-flag arguments. -static int fish_parse_opt(int argc, char **argv, fish_cmd_opts_t *opts) { - static const char *const short_opts = "+hPilNnvc:C:p:d:f:D:o:"; - static const struct option long_opts[] = { - {"command", required_argument, nullptr, 'c'}, - {"init-command", required_argument, nullptr, 'C'}, - {"features", required_argument, nullptr, 'f'}, - {"debug", required_argument, nullptr, 'd'}, - {"debug-output", required_argument, nullptr, 'o'}, - {"debug-stack-frames", required_argument, nullptr, 'D'}, - {"interactive", no_argument, nullptr, 'i'}, - {"login", no_argument, nullptr, 'l'}, - {"no-config", no_argument, nullptr, 'N'}, - {"no-execute", no_argument, nullptr, 'n'}, - {"print-rusage-self", no_argument, nullptr, 1}, - {"print-debug-categories", no_argument, nullptr, 2}, - {"profile", required_argument, nullptr, 'p'}, - {"profile-startup", required_argument, nullptr, 3}, - {"private", no_argument, nullptr, 'P'}, - {"help", no_argument, nullptr, 'h'}, - {"version", no_argument, nullptr, 'v'}, - {}}; - - int opt; - while ((opt = getopt_long(argc, argv, short_opts, long_opts, nullptr)) != -1) { - switch (opt) { - case 'c': { - opts->batch_cmds.emplace_back(optarg); - break; - } - case 'C': { - opts->postconfig_cmds.emplace_back(optarg); - break; - } - case 'd': { - activate_flog_categories_by_pattern(str2wcstring(optarg)); - rust_activate_flog_categories_by_pattern(str2wcstring(optarg).c_str()); - for (auto cat : get_flog_categories()) { - if (cat->enabled) { - std::fwprintf(stdout, L"Debug enabled for category: %ls\n", cat->name); - } - } - break; - } - case 'o': { - opts->debug_output = optarg; - break; - } - case 'f': { - opts->features = str2wcstring(optarg); - break; - } - case 'h': { - opts->batch_cmds.emplace_back("__fish_print_help fish"); - break; - } - case 'i': { - opts->is_interactive_session = true; - break; - } - case 'l': { - opts->is_login = true; - break; - } - case 'N': { - opts->no_config = true; - // --no-config implies private mode, we won't be saving history - opts->enable_private_mode = true; - break; - } - case 'n': { - opts->no_exec = true; - break; - } - case 1: { - opts->print_rusage_self = true; - break; - } - case 2: { - auto cats = get_flog_categories(); - // Compute width of longest name. - int name_width = 0; - for (auto cat : cats) { - name_width = std::max(name_width, static_cast(wcslen(cat->name))); - } - // A little extra space. - name_width += 2; - for (auto cat : cats) { - // Negating the name width left-justifies. - printf("%*ls %ls\n", -name_width, cat->name, _(cat->description)); - } - exit(0); - } - case 'p': { - // "--profile" - this does not activate profiling right away, - // rather it's done after startup is finished. - opts->profile_output = optarg; - break; - } - case 3: { - // With "--profile-startup" we immediately turn profiling on. - opts->profile_startup_output = optarg; - g_profiling_active = true; - break; - } - case 'P': { - opts->enable_private_mode = true; - break; - } - case 'v': { - std::fwprintf(stdout, _(L"%s, version %s\n"), PACKAGE_NAME, get_fish_version()); - exit(0); - } - case 'D': { - // TODO: Option is currently useless. - // Either remove it or make it work with FLOG. - break; - } - default: { - // We assume getopt_long() has already emitted a diagnostic msg. - exit(1); - } - } - } - - // If our command name begins with a dash that implies we're a login shell. - opts->is_login |= argv[0][0] == '-'; - - // We are an interactive session if we have not been given an explicit - // command or file to execute and stdin is a tty. Note that the -i or - // --interactive options also force interactive mode. - if (opts->batch_cmds.empty() && optind == argc && isatty(STDIN_FILENO)) { - set_interactive_session(true); - } - - return optind; -} - -int main(int argc, char **argv) { - int res = 1; - int my_optind = 0; +int main() { program_name = L"fish"; - rust_init(); - signal_unblock_all(); - - setlocale(LC_ALL, ""); - - const char *dummy_argv[2] = {"fish", nullptr}; - if (!argv[0]) { - argv = const_cast(dummy_argv); //!OCLINT(parameter reassignment) - argc = 1; //!OCLINT(parameter reassignment) - } - - // Enable debug categories set in FISH_DEBUG. - // This is in *addition* to the ones given via --debug. - if (const char *debug_categories = getenv("FISH_DEBUG")) { - activate_flog_categories_by_pattern(str2wcstring(debug_categories)); - } - - fish_cmd_opts_t opts{}; - my_optind = fish_parse_opt(argc, argv, &opts); - - // Direct any debug output right away. - // --debug-output takes precedence, otherwise $FISH_DEBUG_OUTPUT is used. - if (opts.debug_output.empty()) { - const char *var = getenv("FISH_DEBUG_OUTPUT"); - if (var) opts.debug_output = var; - } - - FILE *debug_output = nullptr; - if (!opts.debug_output.empty()) { - debug_output = fopen(opts.debug_output.c_str(), "w"); - if (!debug_output) { - fprintf(stderr, "Could not open file %s\n", opts.debug_output.c_str()); - perror("fopen"); - exit(-1); - } - set_cloexec(fileno(debug_output)); - setlinebuf(debug_output); - set_flog_output_file(debug_output); - rust_set_flog_file_fd(get_flog_file_fd()); - } - - // No-exec is prohibited when in interactive mode. - if (opts.is_interactive_session && opts.no_exec) { - FLOGF(warning, _(L"Can not use the no-execute mode when running an interactive session")); - opts.no_exec = false; - } - - // Apply our options. - if (opts.is_login) mark_login(); - if (opts.no_exec) mark_no_exec(); - if (opts.is_interactive_session) set_interactive_session(true); - if (opts.enable_private_mode) start_private_mode(env_stack_t::globals()); - - // Only save (and therefore restore) the fg process group if we are interactive. See issues - // #197 and #1002. - if (is_interactive_session()) { - save_term_foreground_process_group(); - } - - struct config_paths_t paths; - // If we're not executing, there's no need to find the config. - if (!opts.no_exec) { - paths = determine_config_directory_paths(argv[0]); - env_init(&paths, /* do uvars */ !opts.no_config, /* default paths */ opts.no_config); - } - - // Set features early in case other initialization depends on them. - // Start with the ones set in the environment, then those set on the command line (so the - // command line takes precedence). - if (auto features_var = env_stack_t::globals().get(L"fish_features")) { - for (const wcstring &s : features_var->as_list()) { - feature_set_from_string(s.c_str()); - } - } - feature_set_from_string(opts.features.c_str()); - proc_init(); - misc_init(); - reader_init(); - - parser_t &parser = parser_t::principal_parser(); - parser.set_syncs_uvars(!opts.no_config); - - if (!opts.no_exec && !opts.no_config) { - read_init(parser, paths); - } - - if (is_interactive_session() && opts.no_config && !opts.no_exec) { - // If we have no config, we default to the default key bindings. - parser.vars().set_one(L"fish_key_bindings", ENV_UNEXPORT, L"fish_default_key_bindings"); - if (function_exists(L"fish_default_key_bindings", parser)) { - run_command_list(parser, {"fish_default_key_bindings"}, {}); - } - } - - // Re-read the terminal modes after config, it might have changed them. - term_copy_modes(); - - // Stomp the exit status of any initialization commands (issue #635). - parser.set_last_statuses(statuses_t::just(STATUS_CMD_OK)); - - // If we're profiling startup to a separate file, write it now. - if (!opts.profile_startup_output.empty() && - opts.profile_startup_output != opts.profile_output) { - parser.emit_profiling(opts.profile_startup_output.c_str()); - - // If we are profiling both, ensure the startup data only - // ends up in the startup file. - parser.clear_profiling(); - } - - g_profiling_active = !opts.profile_output.empty(); - - // Run post-config commands specified as arguments, if any. - if (!opts.postconfig_cmds.empty()) { - res = run_command_list(parser, opts.postconfig_cmds, {}); - } - - // Clear signals in case we were interrupted (#9024). - signal_clear_cancel(); - - if (!opts.batch_cmds.empty()) { - // Run the commands specified as arguments, if any. - if (get_login()) { - // Do something nasty to support OpenSUSE assuming we're bash. This may modify cmds. - fish_xdm_login_hack_hack_hack_hack(&opts.batch_cmds, argc - my_optind, - argv + my_optind); - } - - // Pass additional args as $argv. - // Note that we *don't* support setting argv[0]/$0, unlike e.g. bash. - std::vector list; - for (char **ptr = argv + my_optind; *ptr; ptr++) { - list.push_back(str2wcstring(*ptr)); - } - parser.vars().set(L"argv", ENV_DEFAULT, std::move(list)); - res = run_command_list(parser, opts.batch_cmds, {}); - parser.libdata().exit_current_script = false; - } else if (my_optind == argc) { - // Implicitly interactive mode. - if (opts.no_exec && isatty(STDIN_FILENO)) { - FLOGF(error, L"no-execute mode enabled and no script given. Exiting"); - return EXIT_FAILURE; // above line should always exit - } - res = reader_read(parser, STDIN_FILENO, {}); - } else { - const char *file = *(argv + (my_optind++)); - autoclose_fd_t fd(open_cloexec(file, O_RDONLY)); - if (!fd.valid()) { - FLOGF(error, _(L"Error reading script file '%s':"), file); - perror("error"); - } else { - std::vector list; - for (char **ptr = argv + my_optind; *ptr; ptr++) { - list.push_back(str2wcstring(*ptr)); - } - parser.vars().set(L"argv", ENV_DEFAULT, std::move(list)); - - auto &ld = parser.libdata(); - filename_ref_t rel_filename = std::make_shared(str2wcstring(file)); - scoped_push filename_push{&ld.current_filename, rel_filename}; - res = reader_read(parser, fd.fd(), {}); - if (res) { - FLOGF(warning, _(L"Error while reading file %ls\n"), rel_filename->c_str()); - } - } - } - - int exit_status = res ? STATUS_CMD_UNKNOWN : parser.get_last_status(); - event_fire(parser, *new_event_process_exit(getpid(), exit_status)); - - // Trigger any exit handlers. - event_fire_generic(parser, L"fish_exit", {to_string(exit_status)}); - - restore_term_mode(); - restore_term_foreground_process_group_for_exit(); - - if (!opts.profile_output.empty()) { - parser.emit_profiling(opts.profile_output.c_str()); - } - - history_save_all(); - if (opts.print_rusage_self) { - print_rusage_self(stderr); - } - if (debug_output) { - fclose(debug_output); - } - asan_maybe_exit(exit_status); - exit_without_destructors(exit_status); - return EXIT_FAILURE; // above line should always exit + return rust_main(); } diff --git a/tests/checks/bad-option.fish b/tests/checks/bad-option.fish index afc09d0b0..df47065f6 100644 --- a/tests/checks/bad-option.fish +++ b/tests/checks/bad-option.fish @@ -1,2 +1,2 @@ #RUN: %fish -Z -#CHECKERR: {{.*fish}}: {{unrecognized option: Z|invalid option -- '?Z'?|unknown option -- Z|illegal option -- Z}} +# CHECKERR: {{.*fish}}: {{unrecognized option: Z|invalid option -- '?Z'?|unknown option -- Z|illegal option -- Z|-Z: unknown option}}