Files
fish-shell/src/bin/fish.rs
Johannes Altmanninger d0e47cf58a Move current_filename out of LibraryData
The ScopedRefCell wrapping from library_data
is used for two things
1. to allow mutating library_data from a &Parser (for this, a RefCell would be enough)
2. to replace "current_filename" for a scope

A following commit wants to pass parser as "&mut Parser", which
voids reason 1.  It will also remove the ScopedRefCell wrapping
from LibraryData because reason 2 alone is not strong enough.  Move
"current_filename" outside of that, next to "current_node" which is
already a ScopedRefCell.  In future we could maybe consolidate them
into one field, like (or even merging with) ScopedData.
2026-05-01 18:03:13 +08:00

681 lines
24 KiB
Rust

//
// 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 fish::{
ast,
builtins::{
error::Error,
fish_indent, fish_key_reader,
shared::{STATUS_CMD_ERROR, STATUS_CMD_OK, STATUS_CMD_UNKNOWN, VERSION_STRING_TEMPLATE},
},
common::{PACKAGE_NAME, PROFILING_ACTIVE, PROGRAM_NAME},
env::{
EnvMode, Statuses,
config_paths::ConfigPaths,
environment::{EnvStack, Environment as _, env_init},
},
eprintf, err_fmt,
event::{self, Event},
fds::heightenize_fd,
flog::{self, activate_flog_categories_by_pattern, flog, flogf, set_flog_file_fd},
fprintf, function,
history::{self, start_private_mode},
io::{FdOutputStream, IoChain, OutputStream},
locale::set_libc_locales,
nix::isatty,
panic::panic_handler,
parse_constants::{ParseErrorList, ParseTreeFlags},
parse_tree::ParsedSource,
parse_util::detect_parse_errors_in_ast,
parser::{BlockType, CancelBehavior, Parser, ParserEnvSetMode},
path::path_get_config,
prelude::*,
printf,
proc::{
Pid, get_login, is_interactive_session, mark_login, mark_no_exec, proc_init,
set_interactive_session,
},
reader::{reader_exit_signal, reader_init, reader_read, term_copy_modes},
signal::{signal_clear_cancel, signal_unblock_all},
threads::{self},
topic_monitor,
wutil::waccess,
};
use fish_common::{escape, save_term_foreground_process_group};
use fish_widestring::{bytes2wcstring, osstr2wcstring, wcs2bytes};
use libc::{STDERR_FILENO, STDIN_FILENO};
use nix::{
sys::resource::{UsageWho, getrusage},
unistd::{AccessFlags, getpid},
};
use std::{
env,
ffi::{OsStr, OsString},
fs::File,
ops::ControlFlow,
os::unix::prelude::*,
path::Path,
sync::{Arc, atomic::Ordering},
};
/// 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 nix_tv_to_ms(tv: nix::sys::time::TimeVal) -> i64 {
// milliseconds per second
let mut ms = tv.tv_sec() as i64 * 1000;
// microseconds per millisecond
ms += tv.tv_usec() as i64 / 1000;
ms
}
fn print_rusage_self() {
// `getrusage` should never fail with this usage.
// If it does, it suggests a non-POSIX-compliant OS.
let usage = getrusage(UsageWho::RUSAGE_SELF).unwrap();
#[allow(non_snake_case)]
let rss_KiB = if cfg!(apple) {
// Macs use bytes,
// even though docs at https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/getrusage.2.html say otherwise.
usage.max_rss() / 1024
} else {
usage.max_rss()
};
let user_time = nix_tv_to_ms(usage.user_time());
let sys_time = nix_tv_to_ms(usage.system_time());
let total_time = user_time + sys_time;
let signals = usage.signals();
eprintf!(" rusage self:\n");
eprintf!(" user time: %s ms\n", sys_time.to_string());
eprintf!(" sys time: %s ms\n", user_time.to_string());
eprintf!(" total time: %s ms\n", total_time.to_string());
eprintf!(" max rss: %s KiB\n", rss_KiB.to_string());
eprintf!(" signals: %s\n", signals.to_string());
}
// Source the file config.fish in the given directory.
// Returns true if successful, false if not.
fn source_config_in_directory(parser: &Parser, dir: &wstr) -> bool {
// 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(dir) + L!("/config.fish");
if waccess(&config_pathname, AccessFlags::R_OK).is_err() {
flogf!(
config,
"not sourcing %s (not readable or does not exist)",
escaped_pathname
);
return false;
}
flog!(config, "sourcing", escaped_pathname);
let cmd: WString = L!("builtin source ").to_owned() + escaped_pathname.as_utfstr();
parser.libdata_mut().within_fish_init = true;
let _ = parser.eval(&cmd, &IoChain::new());
parser.libdata_mut().within_fish_init = false;
true
}
/// Parse init files. exec_path is the path of fish executable as determined by argv[0].
fn read_init(parser: &Parser, paths: &ConfigPaths) {
use fish::autoload::Asset;
let emfile = Asset::get("config.fish").expect("Embedded file not found");
let src = bytes2wcstring(&emfile.data);
parser.libdata_mut().within_fish_init = true;
let fname: Arc<WString> = Arc::new(L!("embedded:config.fish").into());
let ret = parser.eval_file_wstr(src, fname, &IoChain::new(), None);
parser.libdata_mut().within_fish_init = false;
if let Err(msg) = ret {
eprintf!("%s", msg);
}
source_config_in_directory(parser, &osstr2wcstring(&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.
if let Some(config_dir) = path_get_config() {
source_config_in_directory(parser, &config_dir);
}
}
fn run_command_list(parser: &Parser, cmds: &[OsString]) -> Result<(), libc::c_int> {
let mut retval = Ok(());
for cmd in cmds {
let cmd_wcs = osstr2wcstring(cmd);
let mut errors = ParseErrorList::new();
let ast = ast::parse(&cmd_wcs, ParseTreeFlags::default(), Some(&mut errors));
let errored = ast.errored() || {
detect_parse_errors_in_ast(&ast, &cmd_wcs, Some(&mut errors)).is_err()
};
if !errored {
// Construct a parsed source ref.
let ps = Arc::new(ParsedSource::new(cmd_wcs, ast));
let _ = parser.eval_parsed_source(&ps, &IoChain::new(), None, BlockType::Top, false);
retval = Ok(());
} else {
let backtrace = parser.get_backtrace(&cmd_wcs, &errors);
eprintf!("%s", backtrace);
// XXX: Why is this the return for "unknown command"?
retval = Err(STATUS_CMD_UNKNOWN);
}
}
retval
}
fn fish_parse_opt(args: &mut [WString], opts: &mut FishCmdOpts) -> ControlFlow<i32, usize> {
use fish_wgetopt::{ArgType::*, WGetopter, WOption, wopt};
const RUSAGE_ARG: char = 1 as char;
const PRINT_DEBUG_CATEGORIES_ARG: char = 2 as char;
const PROFILE_STARTUP_ARG: char = 3 as char;
const SHORT_OPTS: &wstr = L!("+hPilNnvc:C:p:d:f:D:o:");
const LONG_OPTS: &[WOption<'static>] = &[
wopt(L!("command"), RequiredArgument, 'c'),
wopt(L!("init-command"), RequiredArgument, 'C'),
wopt(L!("features"), RequiredArgument, 'f'),
wopt(L!("debug"), RequiredArgument, 'd'),
wopt(L!("debug-output"), RequiredArgument, 'o'),
wopt(L!("debug-stack-frames"), RequiredArgument, 'D'),
wopt(L!("interactive"), NoArgument, 'i'),
wopt(L!("login"), NoArgument, 'l'),
wopt(L!("no-config"), NoArgument, 'N'),
wopt(L!("no-execute"), NoArgument, 'n'),
wopt(L!("print-rusage-self"), NoArgument, RUSAGE_ARG),
wopt(
L!("print-debug-categories"),
NoArgument,
PRINT_DEBUG_CATEGORIES_ARG,
),
wopt(L!("profile"), RequiredArgument, 'p'),
wopt(L!("profile-startup"), RequiredArgument, PROFILE_STARTUP_ARG),
wopt(L!("private"), NoArgument, 'P'),
wopt(L!("help"), NoArgument, 'h'),
wopt(L!("version"), NoArgument, 'v'),
];
let mut shim_args: Vec<&wstr> = args.iter().map(|s| s.as_ref()).collect();
let mut w = WGetopter::new(SHORT_OPTS, LONG_OPTS, &mut shim_args);
while let Some(c) = w.next_opt() {
match c {
'c' => opts
.batch_cmds
.push(OsString::from_vec(wcs2bytes(w.woptarg.unwrap()))),
'C' => opts
.postconfig_cmds
.push(OsString::from_vec(wcs2bytes(w.woptarg.unwrap()))),
'd' => {
activate_flog_categories_by_pattern(w.woptarg.unwrap());
for cat in flog::categories::all_categories() {
if cat.enabled.load(Ordering::Relaxed) {
printf!("Debug enabled for category: %s\n", cat.name);
}
}
}
'o' => opts.debug_output = Some(OsString::from_vec(wcs2bytes(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 = cat.description.localize();
// this is left-justified
printf!("%-*s %s\n", name_width, cat.name, desc);
}
return ControlFlow::Break(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(wcs2bytes(w.woptarg.unwrap()))),
PROFILE_STARTUP_ARG => {
// With "--profile-startup" we immediately turn profiling on.
opts.profile_startup_output =
Some(OsString::from_vec(wcs2bytes(w.woptarg.unwrap())));
PROFILING_ACTIVE.store(true);
}
'P' => opts.enable_private_mode = true,
'v' => {
printf!(
"%s\n",
wgettext_fmt!(VERSION_STRING_TEMPLATE, PACKAGE_NAME, fish::BUILD_VERSION)
);
return ControlFlow::Break(0);
}
'D' => {
// TODO: Option is currently useless.
// Either remove it or make it work with flog.
}
'?' => {
err_fmt!(Error::UNKNOWN_OPT, args[w.wopt_index - 1])
.cmd(L!("fish"))
.append_to_msg('\n')
.write_to(&mut OutputStream::Fd(FdOutputStream::new(STDERR_FILENO)));
return ControlFlow::Break(1);
}
':' => {
err_fmt!(Error::MISSING_OPT_ARG, args[w.wopt_index - 1])
.cmd(L!("fish"))
.append_to_msg('\n')
.write_to(&mut OutputStream::Fd(FdOutputStream::new(STDERR_FILENO)));
return ControlFlow::Break(1);
}
';' => {
err_fmt!(Error::UNEXP_OPT_ARG, args[w.wopt_index - 1])
.cmd(L!("fish"))
.append_to_msg('\n')
.write_to(&mut OutputStream::Fd(FdOutputStream::new(STDERR_FILENO)));
return ControlFlow::Break(1);
}
_ => panic!("unexpected retval from WGetopter"),
}
}
let optind = w.wopt_index;
// 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() && isatty(STDIN_FILENO) {
set_interactive_session(true);
}
ControlFlow::Continue(optind)
}
fn main() {
// If we are called as "/path/to/fish_key_reader", become fish_key_reader.
if let Some(name) = env::args_os().next() {
let p = Path::new(&name).file_name().and_then(|x| x.to_str());
if p == Some("fish_key_reader") {
return fish_key_reader::main();
} else if p == Some("fish_indent") {
return fish_indent::main();
}
}
PROGRAM_NAME.set(L!("fish")).unwrap();
if !cfg!(small_main_stack) {
panic_handler(throwing_main);
} else {
// Create a new thread with a decent stack size to be our main thread
std::thread::scope(|scope| {
scope.spawn(|| panic_handler(throwing_main));
});
}
}
fn throwing_main() -> i32 {
let mut res = Err(STATUS_CMD_ERROR);
signal_unblock_all();
topic_monitor::topic_monitor_init();
threads::init();
// Safety: single-threaded.
unsafe {
set_libc_locales(/*log_ok=*/ false)
};
#[cfg(feature = "localize-messages")]
fish::localization::initialize_localization();
// Enable debug categories set in FISH_DEBUG.
// This is in *addition* to the ones given via --debug.
if let Some(debug_categories) = env::var_os("FISH_DEBUG") {
let s = osstr2wcstring(debug_categories);
activate_flog_categories_by_pattern(&s);
}
let mut args: Vec<WString> = env::args_os().map(osstr2wcstring).collect();
let mut opts = FishCmdOpts::default();
let mut my_optind = match fish_parse_opt(&mut args, &mut opts) {
ControlFlow::Continue(optind) => optind,
ControlFlow::Break(status) => return status,
};
// 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");
}
if let Some(debug_path) = opts.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
set_flog_file_fd(dbg_file.into_raw_fd());
}
Err(e) => {
let debug_path_string = format!("{debug_path:?}");
// TODO: should be translated
eprintf!("Could not open file %s\n", debug_path_string);
eprintf!("%s\n", e);
return 1;
}
}
}
// No-exec is prohibited when in interactive mode.
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 {
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(EnvStack::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();
}
// If we're not executing, there's no need to find the config.
let config_paths = if !opts.no_exec {
let config_paths = ConfigPaths::new();
env_init(
Some(&config_paths),
/* do uvars */ !opts.no_config,
/* default paths */ opts.no_config,
);
Some(config_paths)
} else {
None
};
// 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() {
fish_feature_flags::set_from_string(s.as_utfstr());
}
}
fish_feature_flags::set_from_string(opts.features.as_utfstr());
proc_init();
reader_init(true);
// Construct the root parser!
let env = EnvStack::globals().create_child(true /* dispatches_var_changes */);
let parser = &Parser::new(env, CancelBehavior::Clear);
parser.set_syncs_uvars(!opts.no_config);
if !opts.no_exec && !opts.no_config {
read_init(parser, config_paths.as_ref().unwrap());
}
if is_interactive_session() && opts.no_config && !opts.no_exec {
// If we have no config, we default to the default key bindings.
parser.set_one(
L!("fish_key_bindings"),
ParserEnvSetMode::new(EnvMode::UNEXPORT),
L!("fish_default_key_bindings").to_owned(),
);
if function::exists(L!("fish_default_key_bindings"), parser) {
let _ = run_command_list(parser, &[OsString::from("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::just(STATUS_CMD_OK));
// TODO(MSRV>=1.88): feature(let_chains)
if let Some(path) = &opts.profile_startup_output {
if opts.profile_startup_output != opts.profile_output {
parser.emit_profiling(path);
// If we are profiling both, ensure the startup data only
// ends up in the startup file.
parser.clear_profiling();
}
}
PROFILING_ACTIVE.store(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 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.
let list = &args[my_optind..];
parser.set_var(
L!("argv"),
ParserEnvSetMode::default(),
list.iter().map(|s| s.to_owned()).collect(),
);
res = run_command_list(parser, &opts.batch_cmds);
parser.libdata_mut().exit_current_script = false;
} else if my_optind == args.len() {
// Implicitly interactive mode.
if opts.no_exec && isatty(libc::STDIN_FILENO) {
flog!(
error,
"no-execute mode enabled and no script given. Exiting"
);
// above line should always exit
return libc::EXIT_FAILURE;
}
res = reader_read(parser, libc::STDIN_FILENO, &IoChain::new());
} else {
let filename = &args[my_optind];
let n = wcs2bytes(filename);
let path = OsStr::from_bytes(&n);
my_optind += 1;
// Rust sets cloexec by default, see above
match File::open(path) {
Err(e) => {
flogf!(
error,
wgettext!("Error reading script file '%s':"),
path.to_string_lossy()
);
eprintf!("%s\n", e);
}
Ok(f) => {
if let Ok(f) = heightenize_fd(f.into(), true).map(File::from) {
let list = &args[my_optind..];
parser.set_var(
L!("argv"),
ParserEnvSetMode::default(),
list.iter().map(|s| s.to_owned()).collect(),
);
let _filename_push = parser
.current_filename
.scoped_replace(Some(Arc::new(filename.to_owned())));
res = reader_read(parser, f.as_raw_fd(), &IoChain::new());
if res.is_err() {
flog!(
warning,
wgettext_fmt!("Error while reading file %s", path.to_string_lossy())
);
}
}
}
}
}
let exit_status = if res.is_err() {
STATUS_CMD_UNKNOWN
} else {
parser.get_last_status()
};
event::fire(
parser,
Event::process_exit(Pid::from_nix_pid_unchecked(getpid()), exit_status),
);
// Trigger any exit handlers.
event::fire_generic(
parser,
L!("fish_exit").to_owned(),
vec![exit_status.to_wstring()],
);
if let Some(profile_output) = opts.profile_output {
parser.emit_profiling(&profile_output);
}
history::save_all();
// If we deferred a fatal signal, re-raise it now so the parent sees WIFSIGNALED.
let exit_sig = reader_exit_signal();
if exit_sig != 0 {
unsafe {
libc::signal(exit_sig, libc::SIG_DFL);
libc::raise(exit_sig);
}
}
if opts.print_rusage_self {
print_rusage_self();
}
exit_status
}
// https://github.com/fish-shell/fish-shell/issues/367
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("\'");
result
}
fn fish_xdm_login_hack_hack_hack_hack(cmds: &mut [OsString], args: &[WString]) -> bool {
if cmds.len() != 1 {
return false;
}
let cmd = &cmds[0];
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[1..] {
new_cmd.push(" ");
new_cmd.push(escape_single_quoted_hack_hack_hack_hack(arg));
}
cmds[0] = new_cmd;
true
} else {
false
}
}