mirror of
https://github.com/fish-shell/fish-shell.git
synced 2024-11-24 22:50:10 +08:00
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.
This commit is contained in:
parent
96e58dac21
commit
eacbd6156d
|
@ -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",
|
||||
|
|
|
@ -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")
|
||||
|
|
875
fish-rust/src/fish.rs
Normal file
875
fish-rust/src/fish.rs
Normal file
|
@ -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<OsString>,
|
||||
/// File path for profiling output, or empty for none.
|
||||
profile_output: Option<OsString>,
|
||||
profile_startup_output: Option<OsString>,
|
||||
/// Commands to be executed in place of interactive shell.
|
||||
batch_cmds: Vec<OsString>,
|
||||
/// Commands to execute after the shell's config has been read.
|
||||
postconfig_cmds: Vec<OsString>,
|
||||
/// 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<Path>) -> 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!("{:<width$} {}", cat.name, desc, width = name_width);
|
||||
}
|
||||
std::process::exit(0);
|
||||
}
|
||||
// "--profile" - this does not activate profiling right away,
|
||||
// rather it's done after startup is finished.
|
||||
'p' => 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::<Vec<_>>(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn main() -> i32 {
|
||||
let mut args: Vec<WString> = 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<ConfigPaths> = 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<OsString>, 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;
|
||||
}
|
||||
}
|
|
@ -40,6 +40,7 @@ mod fds;
|
|||
mod ffi;
|
||||
mod ffi_init;
|
||||
mod ffi_tests;
|
||||
mod fish;
|
||||
mod fish_indent;
|
||||
mod flog;
|
||||
mod function;
|
||||
|
|
606
src/fish.cpp
606
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 <fcntl.h>
|
||||
#include <getopt.h>
|
||||
#include <limits.h>
|
||||
#include <locale.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <strings.h>
|
||||
#include <sys/resource.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <cwchar>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#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<std::string> batch_cmds;
|
||||
// Commands to execute after the shell's config has been read.
|
||||
std::vector<std::string> 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<long long>(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<std::string> &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<int>(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<char **>(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<wcstring> 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<wcstring> 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<wcstring>(str2wcstring(file));
|
||||
scoped_push<filename_ref_t> 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();
|
||||
}
|
||||
|
|
|
@ -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}}
|
||||
|
|
Loading…
Reference in New Issue
Block a user