From 77aeb6a2a88af77077cc1c42b8a11c6b2a59d744 Mon Sep 17 00:00:00 2001 From: Johannes Altmanninger Date: Sun, 8 Oct 2023 23:22:27 +0200 Subject: [PATCH] Port execution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop support for history file version 1. ParseExecutionContext no longer contains an OperationContext because in my first implementation, ParseExecutionContext didn't have interior mutability. We should probably try to add it back. Add a few to-do style comments. Search for "todo!" and "PORTING". Co-authored-by: Xiretza (complete, wildcard, expand, history, history/file) Co-authored-by: Henrik Hørlück Berg <36937807+henrikhorluck@users.noreply.github.com> (builtins/set) --- CMakeLists.txt | 25 +- fish-rust/build.rs | 36 +- fish-rust/src/abbrs.rs | 4 +- fish-rust/src/ast.rs | 110 +- fish-rust/src/autoload.rs | 390 +++ fish-rust/src/builtins/abbr.rs | 17 +- fish-rust/src/builtins/argparse.rs | 58 +- fish-rust/src/builtins/bg.rs | 117 +- fish-rust/src/builtins/bind.rs | 28 + fish-rust/src/builtins/block.rs | 57 +- fish-rust/src/builtins/builtin.rs | 12 +- fish-rust/src/builtins/cd.rs | 23 +- fish-rust/src/builtins/command.rs | 11 +- fish-rust/src/builtins/commandline.rs | 5 + fish-rust/src/builtins/complete.rs | 644 +++++ fish-rust/src/builtins/contains.rs | 4 +- fish-rust/src/builtins/count.rs | 2 +- fish-rust/src/builtins/disown.rs | 126 + fish-rust/src/builtins/echo.rs | 4 +- fish-rust/src/builtins/emit.rs | 2 +- fish-rust/src/builtins/eval.rs | 79 + fish-rust/src/builtins/exit.rs | 4 +- fish-rust/src/builtins/fg.rs | 177 ++ fish-rust/src/builtins/function.rs | 81 +- fish-rust/src/builtins/functions.rs | 38 +- fish-rust/src/builtins/history.rs | 362 +++ fish-rust/src/builtins/jobs.rs | 267 ++ fish-rust/src/builtins/math.rs | 4 +- fish-rust/src/builtins/mod.rs | 18 +- fish-rust/src/builtins/path.rs | 54 +- fish-rust/src/builtins/printf.rs | 12 +- fish-rust/src/builtins/pwd.rs | 11 +- fish-rust/src/builtins/random.rs | 4 +- fish-rust/src/builtins/read.rs | 7 + fish-rust/src/builtins/realpath.rs | 8 +- fish-rust/src/builtins/return.rs | 18 +- fish-rust/src/builtins/set.rs | 1007 +++++++ fish-rust/src/builtins/set_color.rs | 6 +- fish-rust/src/builtins/shared.rs | 1054 +++++-- fish-rust/src/builtins/source.rs | 132 + fish-rust/src/builtins/status.rs | 66 +- fish-rust/src/builtins/string.rs | 45 +- fish-rust/src/builtins/string/collect.rs | 6 +- fish-rust/src/builtins/string/escape.rs | 2 +- fish-rust/src/builtins/string/join.rs | 6 +- fish-rust/src/builtins/string/length.rs | 2 +- fish-rust/src/builtins/string/match.rs | 4 +- fish-rust/src/builtins/string/pad.rs | 2 +- fish-rust/src/builtins/string/repeat.rs | 2 +- fish-rust/src/builtins/string/replace.rs | 2 +- fish-rust/src/builtins/string/shorten.rs | 6 +- fish-rust/src/builtins/string/split.rs | 15 +- fish-rust/src/builtins/string/sub.rs | 2 +- fish-rust/src/builtins/string/transform.rs | 2 +- fish-rust/src/builtins/string/trim.rs | 2 +- fish-rust/src/builtins/string/unescape.rs | 2 +- fish-rust/src/builtins/test.rs | 17 +- fish-rust/src/builtins/tests/string_tests.rs | 23 +- fish-rust/src/builtins/tests/test_tests.rs | 18 +- fish-rust/src/builtins/type.rs | 32 +- fish-rust/src/builtins/ulimit.rs | 16 + fish-rust/src/builtins/wait.rs | 43 +- fish-rust/src/cfg/spawn.c | 1 + fish-rust/src/cfg/w_exitcode.cpp | 2 + fish-rust/src/common.rs | 47 +- fish-rust/src/compat.c | 22 + fish-rust/src/compat.rs | 16 + fish-rust/src/complete.rs | 2586 +++++++++++++++++- fish-rust/src/env/env_ffi.rs | 158 +- fish-rust/src/env/environment.rs | 135 +- fish-rust/src/env/environment_impl.rs | 108 +- fish-rust/src/env/mod.rs | 2 +- fish-rust/src/env/var.rs | 24 - fish-rust/src/env_dispatch.rs | 24 +- fish-rust/src/env_universal_common.rs | 1142 ++++++++ fish-rust/src/event.rs | 138 +- fish-rust/src/exec.rs | 1533 +++++++++++ fish-rust/src/expand.rs | 1530 ++++++++++- fish-rust/src/fds.rs | 42 +- fish-rust/src/ffi.rs | 303 +- fish-rust/src/fish.rs | 170 +- fish-rust/src/flog.rs | 2 +- fish-rust/src/fork_exec/mod.rs | 9 +- fish-rust/src/fork_exec/postfork.rs | 12 +- fish-rust/src/fork_exec/spawn.rs | 53 +- fish-rust/src/function.rs | 89 +- fish-rust/src/future_feature_flags.rs | 2 +- fish-rust/src/global_safety.rs | 29 +- fish-rust/src/highlight.rs | 1847 ++++++++++++- fish-rust/src/history.rs | 2400 +++++++++++++++- fish-rust/src/history/file.rs | 558 ++++ fish-rust/src/input.rs | 4 + fish-rust/src/input_common.rs | 21 + fish-rust/src/io.rs | 234 +- fish-rust/src/job_group.rs | 93 +- fish-rust/src/lib.rs | 21 +- fish-rust/src/null_terminated_array.rs | 48 +- fish-rust/src/operation_context.rs | 205 +- fish-rust/src/output.rs | 4 +- fish-rust/src/parse_constants.rs | 12 +- fish-rust/src/parse_execution.rs | 2089 ++++++++++++++ fish-rust/src/parse_tree.rs | 14 + fish-rust/src/parse_util.rs | 83 +- fish-rust/src/parser.rs | 1559 +++++++++++ fish-rust/src/path.rs | 28 +- fish-rust/src/pointer.rs | 36 + fish-rust/src/proc.rs | 1877 +++++++++++++ fish-rust/src/reader.rs | 327 ++- fish-rust/src/redirection.rs | 38 +- fish-rust/src/signal.rs | 33 +- fish-rust/src/termsize.rs | 90 +- fish-rust/src/tests/complete.rs | 616 +++++ fish-rust/src/tests/env.rs | 105 + fish-rust/src/tests/env_universal_common.rs | 294 ++ fish-rust/src/tests/expand.rs | 447 +++ fish-rust/src/tests/highlight.rs | 637 +++++ fish-rust/src/tests/history.rs | 602 ++++ fish-rust/src/tests/mod.rs | 39 + fish-rust/src/tests/parser.rs | 347 +++ fish-rust/src/tests/redirection.rs | 43 + fish-rust/src/tests/string_escape.rs | 2 +- fish-rust/src/tests/topic_monitor.rs | 71 + fish-rust/src/tokenizer.rs | 2 +- fish-rust/src/topic_monitor.rs | 197 +- fish-rust/src/trace.rs | 24 +- fish-rust/src/util.rs | 22 + fish-rust/src/wait_handle.rs | 122 - fish-rust/src/wchar_ffi.rs | 25 + fish-rust/src/wcstringutil.rs | 73 +- fish-rust/src/wgetopt.rs | 2 +- fish-rust/src/wildcard.rs | 36 +- fish-rust/src/wutil/dir_iter.rs | 4 +- fish-rust/src/wutil/mod.rs | 11 +- fish-rust/src/wutil/wcstoi.rs | 2 +- src/ast.h | 3 + src/autoload.cpp | 246 -- src/autoload.h | 113 - src/builtin.cpp | 549 +--- src/builtin.h | 73 +- src/builtins/bind.cpp | 84 +- src/builtins/bind.h | 9 +- src/builtins/commandline.cpp | 82 +- src/builtins/commandline.h | 8 +- src/builtins/complete.cpp | 476 ---- src/builtins/complete.h | 11 - src/builtins/disown.cpp | 111 - src/builtins/disown.h | 11 - src/builtins/eval.cpp | 84 - src/builtins/eval.h | 11 - src/builtins/fg.cpp | 139 - src/builtins/fg.h | 11 - src/builtins/history.cpp | 332 --- src/builtins/history.h | 11 - src/builtins/jobs.cpp | 245 -- src/builtins/jobs.h | 14 - src/builtins/read.cpp | 134 +- src/builtins/read.h | 8 +- src/builtins/set.cpp | 850 ------ src/builtins/set.h | 10 - src/builtins/source.cpp | 123 - src/builtins/source.h | 11 - src/builtins/ulimit.cpp | 51 +- src/builtins/ulimit.h | 8 +- src/color.h | 11 + src/common.cpp | 39 +- src/common.h | 20 +- src/complete.cpp | 1967 ------------- src/complete.h | 249 +- src/env.cpp | 93 +- src/env.h | 102 +- src/env_dispatch.h | 16 + src/env_fwd.h | 2 + src/env_universal_common.cpp | 832 +----- src/env_universal_common.h | 200 +- src/event.cpp | 7 +- src/event.h | 6 +- src/exec.cpp | 1328 --------- src/exec.h | 52 +- src/expand.cpp | 1296 +-------- src/expand.h | 154 +- src/ffi_baggage.h | 44 +- src/fish.cpp | 2 +- src/fish_indent.cpp | 19 +- src/fish_indent_common.cpp | 5 +- src/fish_key_reader.cpp | 5 +- src/fish_tests.cpp | 2527 +---------------- src/flog.h | 2 + src/function.cpp | 24 - src/function.h | 18 - src/highlight.cpp | 1339 +-------- src/highlight.h | 229 +- src/history.cpp | 1589 ----------- src/history.h | 319 +-- src/history_file.cpp | 604 ---- src/history_file.h | 82 - src/input.cpp | 20 +- src/input.h | 8 +- src/input_common.cpp | 21 +- src/input_common.h | 6 +- src/io.cpp | 450 --- src/io.h | 265 +- src/operation_context.cpp | 29 - src/operation_context.h | 63 +- src/output.cpp | 3 +- src/output.h | 3 +- src/pager.cpp | 25 +- src/pager.h | 9 +- src/parse_execution.cpp | 1672 ----------- src/parse_execution.h | 188 -- src/parse_util.cpp | 181 +- src/parse_util.h | 3 + src/parser.cpp | 878 ------ src/parser.h | 517 +--- src/parser_keywords.cpp | 72 - src/path.cpp | 9 +- src/path.h | 10 +- src/proc.cpp | 1040 ------- src/proc.h | 606 +--- src/reader.cpp | 669 +++-- src/reader.h | 77 +- src/redirection.h | 2 +- src/screen.cpp | 17 +- src/screen.h | 11 +- src/signals.cpp | 47 - src/signals.h | 23 +- src/topic_monitor.h | 25 - src/wait_handle.h | 12 - src/wildcard.cpp | 13 +- src/wildcard.h | 44 +- src/wutil.cpp | 2 + src/wutil.h | 12 + 231 files changed, 27733 insertions(+), 25155 deletions(-) create mode 100644 fish-rust/src/autoload.rs create mode 100644 fish-rust/src/builtins/bind.rs create mode 100644 fish-rust/src/builtins/commandline.rs create mode 100644 fish-rust/src/builtins/complete.rs create mode 100644 fish-rust/src/builtins/disown.rs create mode 100644 fish-rust/src/builtins/eval.rs create mode 100644 fish-rust/src/builtins/fg.rs create mode 100644 fish-rust/src/builtins/history.rs create mode 100644 fish-rust/src/builtins/jobs.rs create mode 100644 fish-rust/src/builtins/read.rs create mode 100644 fish-rust/src/builtins/set.rs create mode 100644 fish-rust/src/builtins/source.rs create mode 100644 fish-rust/src/builtins/ulimit.rs create mode 100644 fish-rust/src/cfg/spawn.c create mode 100644 fish-rust/src/cfg/w_exitcode.cpp create mode 100644 fish-rust/src/env_universal_common.rs create mode 100644 fish-rust/src/exec.rs create mode 100644 fish-rust/src/history/file.rs create mode 100644 fish-rust/src/input.rs create mode 100644 fish-rust/src/input_common.rs create mode 100644 fish-rust/src/parse_execution.rs create mode 100644 fish-rust/src/parser.rs create mode 100644 fish-rust/src/pointer.rs create mode 100644 fish-rust/src/proc.rs create mode 100644 fish-rust/src/tests/complete.rs create mode 100644 fish-rust/src/tests/env.rs create mode 100644 fish-rust/src/tests/env_universal_common.rs create mode 100644 fish-rust/src/tests/expand.rs create mode 100644 fish-rust/src/tests/highlight.rs create mode 100644 fish-rust/src/tests/history.rs create mode 100644 fish-rust/src/tests/parser.rs create mode 100644 fish-rust/src/tests/redirection.rs create mode 100644 fish-rust/src/tests/topic_monitor.rs delete mode 100644 src/autoload.cpp delete mode 100644 src/builtins/complete.cpp delete mode 100644 src/builtins/complete.h delete mode 100644 src/builtins/disown.cpp delete mode 100644 src/builtins/disown.h delete mode 100644 src/builtins/eval.cpp delete mode 100644 src/builtins/eval.h delete mode 100644 src/builtins/fg.cpp delete mode 100644 src/builtins/fg.h delete mode 100644 src/builtins/history.cpp delete mode 100644 src/builtins/history.h delete mode 100644 src/builtins/jobs.cpp delete mode 100644 src/builtins/jobs.h delete mode 100644 src/builtins/set.cpp delete mode 100644 src/builtins/set.h delete mode 100644 src/builtins/source.cpp delete mode 100644 src/builtins/source.h delete mode 100644 src/complete.cpp create mode 100644 src/env_dispatch.h create mode 100644 src/env_fwd.h delete mode 100644 src/exec.cpp delete mode 100644 src/function.cpp delete mode 100644 src/history.cpp delete mode 100644 src/history_file.cpp delete mode 100644 src/history_file.h delete mode 100644 src/io.cpp delete mode 100644 src/operation_context.cpp mode change 100644 => 100755 src/output.h delete mode 100644 src/parse_execution.cpp delete mode 100644 src/parse_execution.h delete mode 100644 src/parser.cpp delete mode 100644 src/parser_keywords.cpp delete mode 100644 src/proc.cpp delete mode 100644 src/signals.cpp delete mode 100644 src/topic_monitor.h delete mode 100644 src/wait_handle.h diff --git a/CMakeLists.txt b/CMakeLists.txt index d37e41d7a..e209aadb2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -99,63 +99,40 @@ endif() # List of sources for builtin functions. set(FISH_BUILTIN_SRCS - src/builtin.cpp src/builtins/bind.cpp src/builtins/commandline.cpp - src/builtins/complete.cpp - src/builtins/disown.cpp - src/builtins/eval.cpp - src/builtins/fg.cpp - src/builtins/history.cpp - src/builtins/jobs.cpp src/builtins/read.cpp - src/builtins/set.cpp - src/builtins/source.cpp src/builtins/ulimit.cpp ) - # List of other sources. set(FISH_SRCS src/ast.cpp - src/autoload.cpp + src/builtin.cpp src/color.cpp src/common.cpp - src/complete.cpp src/env.cpp src/env_universal_common.cpp src/event.cpp - src/exec.cpp src/expand.cpp src/fallback.cpp src/fds.cpp src/fish_indent_common.cpp src/fish_version.cpp src/flog.cpp - src/function.cpp src/highlight.cpp - src/history.cpp - src/history_file.cpp src/input_common.cpp src/input.cpp - src/io.cpp src/null_terminated_array.cpp - src/operation_context.cpp src/output.cpp src/pager.cpp - src/parse_execution.cpp - src/parser.cpp - src/parser_keywords.cpp src/parse_util.cpp src/path.cpp - src/proc.cpp src/reader.cpp src/rustffi.cpp src/screen.cpp - src/signals.cpp src/utf8.cpp src/wcstringutil.cpp src/wgetopt.cpp - src/wildcard.cpp src/wutil.cpp ) diff --git a/fish-rust/build.rs b/fish-rust/build.rs index b6f0fa74d..13c1c44eb 100644 --- a/fish-rust/build.rs +++ b/fish-rust/build.rs @@ -20,6 +20,14 @@ fn main() { ) .compile("libcompat.a"); + if compiles("fish-rust/src/cfg/w_exitcode.cpp") { + println!("cargo:rustc-cfg=HAVE_WAITSTATUS_SIGNAL_RET"); + } + + if compiles("fish-rust/src/cfg/spawn.c") { + println!("cargo:rustc-cfg=FISH_USE_POSIX_SPAWN"); + } + let rust_dir = env!("CARGO_MANIFEST_DIR"); let target_dir = std::env::var("FISH_RUST_TARGET_DIR").unwrap_or(format!("{}/{}", rust_dir, "target/")); @@ -58,30 +66,38 @@ fn main() { "fish-rust/src/abbrs.rs", "fish-rust/src/ast.rs", "fish-rust/src/builtins/shared.rs", - "fish-rust/src/builtins/function.rs", "fish-rust/src/common.rs", - "fish-rust/src/env/env_ffi.rs", + "fish-rust/src/complete.rs", "fish-rust/src/env_dispatch.rs", + "fish-rust/src/env/env_ffi.rs", + "fish-rust/src/env_universal_common.rs", "fish-rust/src/event.rs", + "fish-rust/src/exec.rs", + "fish-rust/src/expand.rs", "fish-rust/src/fd_monitor.rs", "fish-rust/src/fd_readable_set.rs", "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/fork_exec/spawn.rs", + "fish-rust/src/fish.rs", "fish-rust/src/function.rs", "fish-rust/src/future_feature_flags.rs", "fish-rust/src/highlight.rs", + "fish-rust/src/history.rs", + "fish-rust/src/io.rs", "fish-rust/src/job_group.rs", "fish-rust/src/kill.rs", "fish-rust/src/null_terminated_array.rs", + "fish-rust/src/operation_context.rs", "fish-rust/src/output.rs", "fish-rust/src/parse_constants.rs", + "fish-rust/src/parser.rs", "fish-rust/src/parse_tree.rs", "fish-rust/src/parse_util.rs", "fish-rust/src/print_help.rs", + "fish-rust/src/proc.rs", + "fish-rust/src/reader.rs", "fish-rust/src/redirection.rs", "fish-rust/src/signal.rs", "fish-rust/src/smoke.rs", @@ -89,10 +105,8 @@ fn main() { "fish-rust/src/threads.rs", "fish-rust/src/timer.rs", "fish-rust/src/tokenizer.rs", - "fish-rust/src/topic_monitor.rs", "fish-rust/src/trace.rs", "fish-rust/src/util.rs", - "fish-rust/src/wait_handle.rs", "fish-rust/src/wildcard.rs", ]; cxx_build::bridges(&source_files) @@ -149,6 +163,16 @@ fn detect_features(target: Target) { } } +fn compiles(file: &str) -> bool { + rsconf::rebuild_if_path_changed(file); + let mut command = cc::Build::new() + .flag("-fsyntax-only") + .get_compiler() + .to_command(); + command.arg(file); + command.status().unwrap().success() +} + /// Detect if we're being compiled for a BSD-derived OS, allowing targeting code conditionally with /// `#[cfg(feature = "bsd")]`. /// diff --git a/fish-rust/src/abbrs.rs b/fish-rust/src/abbrs.rs index a9220e8e0..611780665 100644 --- a/fish-rust/src/abbrs.rs +++ b/fish-rust/src/abbrs.rs @@ -194,9 +194,11 @@ fn matches_position(&self, position: Position) -> bool { } /// The result of an abbreviation expansion. +#[derive(Debug, Eq, PartialEq)] pub struct Replacer { /// The string to use to replace the incoming token, either literal or as a function name. - replacement: WString, + /// Exposed for testing. + pub replacement: WString, /// If true, treat 'replacement' as the name of a function. is_function: bool, diff --git a/fish-rust/src/ast.rs b/fish-rust/src/ast.rs index e2f3a903f..102da72e7 100644 --- a/fish-rust/src/ast.rs +++ b/fish-rust/src/ast.rs @@ -148,12 +148,11 @@ fn source<'s>(&self, orig: &'s wstr) -> &'s wstr { fn pointer_eq(&self, rhs: &dyn Node) -> bool { std::ptr::eq(self.as_ptr(), rhs.as_ptr()) } + fn as_node(&self) -> &dyn Node; } /// NodeMut is a mutable node. -trait NodeMut: Node + AcceptorMut + ConcreteNodeMut { - fn as_node(&self) -> &dyn Node; -} +trait NodeMut: Node + AcceptorMut + ConcreteNodeMut {} pub trait ConcreteNode { // Cast to any sub-trait. @@ -401,7 +400,7 @@ pub trait Leaf: Node { fn has_source(&self) -> bool { self.range().is_some() } - fn leaf_as_node_ffi(&self) -> &dyn Node; + fn leaf_as_node(&self) -> &dyn Node; } // A token node is a node which contains a token, which must be one of a fixed set. @@ -439,6 +438,13 @@ fn count(&self) -> usize { fn is_empty(&self) -> bool { self.contents().is_empty() } + /// Iteration support. + fn iter(&self) -> std::slice::Iter> { + self.contents().iter() + } + fn get(&self, index: usize) -> Option<&Self::ContentsNode> { + self.contents().get(index).map(|b| &**b) + } } /// Implement the node trait. @@ -476,12 +482,11 @@ fn try_source_range(&self) -> Option { fn as_ptr(&self) -> *const () { (self as *const $name).cast() } - } - impl NodeMut for $name { fn as_node(&self) -> &dyn Node { self } } + impl NodeMut for $name {} }; } @@ -495,7 +500,7 @@ fn range(&self) -> Option { fn range_mut(&mut self) -> &mut Option { &mut self.range } - fn leaf_as_node_ffi(&self) -> &dyn Node { + fn leaf_as_node(&self) -> &dyn Node { self } } @@ -621,13 +626,11 @@ fn contents_mut(&mut self) -> &mut Vec> { &mut self.list_contents } } - impl $name { - /// Iteration support. - pub fn iter(&self) -> impl Iterator::ContentsNode> { - self.contents().iter().map(|b| &**b) - } - pub fn get(&self, index: usize) -> Option<&$contents> { - self.contents().get(index).map(|b| &**b) + impl<'a> IntoIterator for &'a $name { + type Item = &'a Box<$contents>; + type IntoIter = std::slice::Iter<'a, Box<$contents>>; + fn into_iter(self) -> Self::IntoIter { + self.contents().into_iter() } } impl Index for $name { @@ -1948,6 +1951,20 @@ pub fn typ(&self) -> Type { pub fn try_source_range(&self) -> Option { self.embedded_node().try_source_range() } + + fn as_argument(&self) -> Option<&Argument> { + match self { + ArgumentOrRedirectionVariant::Argument(node) => Some(node), + _ => None, + } + } + fn as_redirection(&self) -> Option<&Redirection> { + match self { + ArgumentOrRedirectionVariant::Redirection(redirection) => Some(redirection), + _ => None, + } + } + fn embedded_node(&self) -> &dyn NodeMut { match self { ArgumentOrRedirectionVariant::Argument(node) => node, @@ -2044,6 +2061,38 @@ pub fn typ(&self) -> Type { pub fn try_source_range(&self) -> Option { self.embedded_node().try_source_range() } + + pub fn as_not_statement(&self) -> Option<&NotStatement> { + match self { + StatementVariant::NotStatement(node) => Some(node), + _ => None, + } + } + pub fn as_block_statement(&self) -> Option<&BlockStatement> { + match self { + StatementVariant::BlockStatement(node) => Some(node), + _ => None, + } + } + pub fn as_if_statement(&self) -> Option<&IfStatement> { + match self { + StatementVariant::IfStatement(node) => Some(node), + _ => None, + } + } + pub fn as_switch_statement(&self) -> Option<&SwitchStatement> { + match self { + StatementVariant::SwitchStatement(node) => Some(node), + _ => None, + } + } + pub fn as_decorated_statement(&self) -> Option<&DecoratedStatement> { + match self { + StatementVariant::DecoratedStatement(node) => Some(node), + _ => None, + } + } + fn embedded_node(&self) -> &dyn NodeMut { match self { StatementVariant::None => panic!("cannot visit null statement"), @@ -2131,6 +2180,32 @@ pub fn typ(&self) -> Type { pub fn try_source_range(&self) -> Option { self.embedded_node().try_source_range() } + + pub fn as_for_header(&self) -> Option<&ForHeader> { + match self { + BlockStatementHeaderVariant::ForHeader(node) => Some(node), + _ => None, + } + } + pub fn as_while_header(&self) -> Option<&WhileHeader> { + match self { + BlockStatementHeaderVariant::WhileHeader(node) => Some(node), + _ => None, + } + } + pub fn as_function_header(&self) -> Option<&FunctionHeader> { + match self { + BlockStatementHeaderVariant::FunctionHeader(node) => Some(node), + _ => None, + } + } + pub fn as_begin_header(&self) -> Option<&BeginHeader> { + match self { + BlockStatementHeaderVariant::BeginHeader(node) => Some(node), + _ => None, + } + } + fn embedded_node(&self) -> &dyn NodeMut { match self { BlockStatementHeaderVariant::None => panic!("cannot visit null block header"), @@ -2325,7 +2400,7 @@ pub fn errored(&self) -> bool { /// \return a textual representation of the tree. /// Pass the original source as \p orig. - fn dump(&self, orig: &wstr) -> WString { + pub fn dump(&self, orig: &wstr) -> WString { let mut result = WString::new(); for node in self.walk() { @@ -4417,6 +4492,11 @@ unsafe impl ExternType for Ast { type Kind = cxx::kind::Opaque; } +unsafe impl ExternType for DecoratedStatement { + type Id = type_id!("DecoratedStatement"); + type Kind = cxx::kind::Opaque; +} + impl Ast { fn top_ffi(&self) -> Box { Box::new(NodeFfi::new(self.top.as_node())) diff --git a/fish-rust/src/autoload.rs b/fish-rust/src/autoload.rs new file mode 100644 index 000000000..03da67dc6 --- /dev/null +++ b/fish-rust/src/autoload.rs @@ -0,0 +1,390 @@ +//! The classes responsible for autoloading functions and completions. + +use crate::common::{escape, ScopeGuard}; +use crate::env::Environment; +use crate::io::IoChain; +use crate::parser::Parser; +use crate::wchar::{wstr, WString, L}; +use crate::wutil::{file_id_for_path, FileId, INVALID_FILE_ID}; +use lru::LruCache; +use std::collections::{HashMap, HashSet}; +use std::num::NonZeroUsize; +use std::time; + +/// autoload_t is a class that knows how to autoload .fish files from a list of directories. This +/// is used by autoloading functions and completions. It maintains a file cache, which is +/// responsible for potentially cached accesses of files, and then a list of files that have +/// actually been autoloaded. A client may request a file to autoload given a command name, and may +/// be returned a path which it is expected to source. +/// autoload_t does not have any internal locks; it is the responsibility of the caller to lock +/// it. +#[derive(Default)] +pub struct Autoload { + /// The environment variable whose paths we observe. + env_var_name: &'static wstr, + + /// A map from command to the files we have autoloaded. + autoloaded_files: HashMap, + + /// The list of commands that we are currently autoloading. + current_autoloading: HashSet, + + /// The autoload cache. + /// This is a unique_ptr because want to change it if the value of our environment variable + /// changes. This is never null (but it may be a cache with no paths). + cache: Box, +} + +impl Autoload { + /// Construct an autoloader that loads from the paths given by \p env_var_name. + pub fn new(env_var_name: &'static wstr) -> Self { + Self { + env_var_name, + ..Default::default() + } + } + + /// Given a command, get a path to autoload. + /// For example, if the environment variable is 'fish_function_path' and the command is 'foo', + /// this will look for a file 'foo.fish' in one of the directories given by fish_function_path. + /// If there is no such file, OR if the file has been previously resolved and is now unchanged, + /// this will return none. But if the file is either new or changed, this will return the path. + /// After returning a path, the command is marked in-progress until the caller calls + /// mark_autoload_finished() with the same command. Note this does not actually execute any + /// code; it is the caller's responsibility to load the file. + pub fn resolve_command(&mut self, cmd: &wstr, env: &dyn Environment) -> Option { + if let Some(var) = env.get(self.env_var_name) { + self.resolve_command_impl(cmd, var.as_list()) + } else { + self.resolve_command_impl(cmd, &[]) + } + } + + /// Helper to actually perform an autoload. + /// This is a static function because it executes fish script, and so must be called without + /// holding any particular locks. + pub fn perform_autoload(path: &wstr, parser: &Parser) { + // We do the useful part of what exec_subshell does ourselves + // - we source the file. + // We don't create a buffer or check ifs or create a read_limit + + let script_source = L!("source ").to_owned() + &escape(path)[..]; + let prev_statuses = parser.get_last_statuses(); + let _put_back = ScopeGuard::new((), |()| parser.set_last_statuses(prev_statuses)); + parser.eval(&script_source, &IoChain::new()); + } + + /// Mark that a command previously returned from path_to_autoload is finished autoloading. + pub fn mark_autoload_finished(&mut self, cmd: &wstr) { + let removed = self.current_autoloading.remove(cmd); + assert!(removed, "cmd was not being autoloaded"); + } + + /// \return whether a command is currently being autoloaded. + pub fn autoload_in_progress(&self, cmd: &wstr) -> bool { + self.current_autoloading.contains(cmd) + } + + /// \return whether a command could potentially be autoloaded. + /// This does not actually mark the command as being autoloaded. + pub fn can_autoload(&mut self, cmd: &wstr) -> bool { + self.cache.check(cmd, true /* allow stale */).is_some() + } + + /// \return whether autoloading has been attempted for a command. + pub fn has_attempted_autoload(&self, cmd: &wstr) -> bool { + self.cache.is_cached(cmd) + } + + /// \return the names of all commands that have been autoloaded. Note this includes "in-flight" + /// commands. + pub fn get_autoloaded_commands(&self) -> Vec { + let mut result = Vec::with_capacity(self.autoloaded_files.len()); + for k in self.autoloaded_files.keys() { + result.push(k.to_owned()); + } + // Sort the output to make it easier to test. + result.sort(); + result + } + + /// Mark that all autoloaded files have been forgotten. + /// Future calls to path_to_autoload() will return previously-returned paths. + pub fn clear(&mut self) { + // Note there is no reason to invalidate the cache here. + self.autoloaded_files.clear(); + } + + /// Invalidate any underlying cache. + /// This is exposed for testing. + fn invalidate_cache(&mut self) { + self.cache = Box::new(AutoloadFileCache::with_dirs(self.cache.dirs().to_owned())); + } + + /// Like resolve_autoload(), but accepts the paths directly. + /// This is exposed for testing. + fn resolve_command_impl(&mut self, cmd: &wstr, paths: &[WString]) -> Option { + // Are we currently in the process of autoloading this? + if self.current_autoloading.contains(cmd) { + return None; + } + + // Check to see if our paths have changed. If so, replace our cache. + // Note we don't have to modify autoloadable_files_. We'll naturally detect if those have + // changed when we query the cache. + if paths != self.cache.dirs() { + self.cache = Box::new(AutoloadFileCache::with_dirs(paths.to_owned())); + } + + // Do we have an entry to load? + let Some(file) = self.cache.check(cmd, false) else { + return None; + }; + + // Is this file the same as what we previously autoloaded? + if let Some(loaded_file) = self.autoloaded_files.get(cmd) { + if *loaded_file == file.file_id { + // The file has been autoloaded and is unchanged. + return None; + } + } + + // We're going to (tell our caller to) autoload this command. + self.current_autoloading.insert(cmd.to_owned()); + self.autoloaded_files.insert(cmd.to_owned(), file.file_id); + Some(file.path) + } +} + +/// The time before we'll recheck an autoloaded file. +const AUTOLOAD_STALENESS_INTERVALL: u64 = 15; + +/// Represents a file that we might want to autoload. +#[derive(Clone)] +struct AutoloadableFile { + /// The path to the file. + path: WString, + /// The metadata for the file. + file_id: FileId, +} + +// A timestamp is a monotonic point in time. +type Timestamp = time::Instant; +type MissesLruCache = LruCache; + +struct KnownFile { + file: AutoloadableFile, + last_checked: Timestamp, +} + +/// Class representing a cache of files that may be autoloaded. +/// This is responsible for performing cached accesses to a set of paths. +struct AutoloadFileCache { + /// The directories from which to load. + dirs: Vec, + + /// Our LRU cache of checks that were misses. + /// The key is the command, the value is the time of the check. + misses_cache: MissesLruCache, + + /// The set of files that we have returned to the caller, along with the time of the check. + /// The key is the command (not the path). + known_files: HashMap, +} + +impl Default for AutoloadFileCache { + fn default() -> Self { + Self::new() + } +} + +impl AutoloadFileCache { + /// Initialize with a set of directories. + fn with_dirs(dirs: Vec) -> Self { + Self { + dirs, + misses_cache: MissesLruCache::new(NonZeroUsize::new(1024).unwrap()), + known_files: HashMap::new(), + } + } + + /// Initialize with empty directories. + fn new() -> Self { + Self::with_dirs(vec![]) + } + + /// \return the directories. + fn dirs(&self) -> &[WString] { + &self.dirs + } + + /// Check if a command \p cmd can be loaded. + /// If \p allow_stale is true, allow stale entries; otherwise discard them. + /// This returns an autoloadable file, or none() if there is no such file. + fn check(&mut self, cmd: &wstr, allow_stale: bool) -> Option { + // Check hits. + if let Some(value) = self.known_files.get(cmd) { + if allow_stale || Self::is_fresh(value.last_checked, Self::current_timestamp()) { + // Re-use this cached hit. + return Some(value.file.clone()); + } + // The file is stale, remove it. + self.known_files.remove(cmd); + } + + // Check misses. + if let Some(miss) = self.misses_cache.get(cmd) { + if allow_stale || Self::is_fresh(*miss, Self::current_timestamp()) { + // Re-use this cached miss. + return None; + } + // The miss is stale, remove it. + self.misses_cache.pop(cmd); + } + + // We couldn't satisfy this request from the cache. Hit the disk. + let file = self.locate_file(cmd); + if let Some(file) = file.as_ref() { + let old_value = self.known_files.insert( + cmd.to_owned(), + KnownFile { + file: file.clone(), + last_checked: Self::current_timestamp(), + }, + ); + assert!( + old_value.is_none(), + "Known files cache should not have contained this cmd" + ); + } else { + let old_value = self + .misses_cache + .put(cmd.to_owned(), Self::current_timestamp()); + assert!( + old_value.is_none(), + "Misses cache should not have contained this cmd", + ); + } + file + } + + /// \return true if a command is cached (either as a hit or miss). + fn is_cached(&self, cmd: &wstr) -> bool { + self.known_files.contains_key(cmd) || self.misses_cache.contains(cmd) + } + + /// \return the current timestamp. + fn current_timestamp() -> Timestamp { + Timestamp::now() + } + + /// \return whether a timestamp is fresh enough to use. + fn is_fresh(then: Timestamp, now: Timestamp) -> bool { + let seconds = now.duration_since(then).as_secs(); + seconds < AUTOLOAD_STALENESS_INTERVALL + } + + /// Attempt to find an autoloadable file by searching our path list for a given command. + /// \return the file, or none() if none. + fn locate_file(&self, cmd: &wstr) -> Option { + // If the command is empty or starts with NULL (i.e. is empty as a path) + // we'd try to source the *directory*, which exists. + // So instead ignore these here. + if cmd.is_empty() { + return None; + } + if cmd.as_char_slice()[0] == '\0' { + return None; + } + // Re-use the storage for path. + let mut path; + for dir in self.dirs() { + // Construct the path as dir/cmd.fish + path = dir.to_owned(); + path.push('/'); + path.push_utfstr(cmd); + path.push_str(".fish"); + + let file_id = file_id_for_path(&path); + if file_id != INVALID_FILE_ID { + // Found it. + return Some(AutoloadableFile { path, file_id }); + } + } + None + } +} + +use crate::ffi_tests::add_test; +#[widestring_suffix::widestrs] +add_test!("test_autoload", || { + use crate::common::{charptr2wcstring, wcs2zstring, write_loop}; + use crate::fds::wopen_cloexec; + use crate::wutil::sprintf; + use libc::{O_CREAT, O_RDWR}; + + macro_rules! run { + ( $fmt:expr $(, $arg:expr )* $(,)? ) => { + let cmd = wcs2zstring(&sprintf!($fmt $(, $arg)*)); + let status = unsafe { libc::system(cmd.as_ptr()) }; + assert!(status == 0); + }; + } + + fn touch_file(path: &wstr) { + let fd = wopen_cloexec(path, O_RDWR | O_CREAT, 0o666); + assert!(fd >= 0); + write_loop(&fd, "Hello".as_bytes()).unwrap(); + unsafe { libc::close(fd) }; + } + + let mut t1 = "/tmp/fish_test_autoload.XXXXXX\0".as_bytes().to_vec(); + let p1 = charptr2wcstring(unsafe { libc::mkdtemp(t1.as_mut_ptr().cast()) }); + let mut t2 = "/tmp/fish_test_autoload.XXXXXX\0".as_bytes().to_vec(); + let p2 = charptr2wcstring(unsafe { libc::mkdtemp(t2.as_mut_ptr().cast()) }); + + let paths = &[p1.clone(), p2.clone()]; + let mut autoload = Autoload::new("test_var"L); + assert!(autoload.resolve_command_impl("file1"L, paths).is_none()); + assert!(autoload.resolve_command_impl("nothing"L, paths).is_none()); + assert!(autoload.get_autoloaded_commands().is_empty()); + + run!("touch %ls/file1.fish", p1); + run!("touch %ls/file2.fish", p2); + autoload.invalidate_cache(); + + assert!(!autoload.autoload_in_progress("file1"L)); + assert!(autoload.resolve_command_impl("file1"L, paths).is_some()); + assert!(autoload.resolve_command_impl("file1"L, paths).is_none()); + assert!(autoload.autoload_in_progress("file1"L)); + assert!(autoload.get_autoloaded_commands() == vec!["file1"L]); + autoload.mark_autoload_finished("file1"L); + assert!(!autoload.autoload_in_progress("file1"L)); + assert!(autoload.get_autoloaded_commands() == vec!["file1"L]); + + assert!(autoload.resolve_command_impl("file1"L, paths).is_none()); + assert!(autoload.resolve_command_impl("nothing"L, paths).is_none()); + assert!(autoload.resolve_command_impl("file2"L, paths).is_some()); + assert!(autoload.resolve_command_impl("file2"L, paths).is_none()); + autoload.mark_autoload_finished("file2"L); + assert!(autoload.resolve_command_impl("file2"L, paths).is_none()); + assert!((autoload.get_autoloaded_commands() == vec!["file1"L, "file2"L])); + + autoload.clear(); + assert!(autoload.resolve_command_impl("file1"L, paths).is_some()); + autoload.mark_autoload_finished("file1"L); + assert!(autoload.resolve_command_impl("file1"L, paths).is_none()); + assert!(autoload.resolve_command_impl("nothing"L, paths).is_none()); + assert!(autoload.resolve_command_impl("file2"L, paths).is_some()); + assert!(autoload.resolve_command_impl("file2"L, paths).is_none()); + autoload.mark_autoload_finished("file2"L); + + assert!(autoload.resolve_command_impl("file1"L, paths).is_none()); + touch_file(&sprintf!("%ls/file1.fish", p1)); + autoload.invalidate_cache(); + assert!(autoload.resolve_command_impl("file1"L, paths).is_some()); + autoload.mark_autoload_finished("file1"L); + + run!("rm -Rf %ls"L, p1); + run!("rm -Rf %ls"L, p2); +}); diff --git a/fish-rust/src/builtins/abbr.rs b/fish-rust/src/builtins/abbr.rs index ec8e019f2..7951b6442 100644 --- a/fish-rust/src/builtins/abbr.rs +++ b/fish-rust/src/builtins/abbr.rs @@ -1,8 +1,9 @@ use super::prelude::*; use crate::abbrs::{self, Abbreviation, Position}; use crate::common::{escape, escape_string, valid_func_name, EscapeStringStyle}; -use crate::env::status::{ENV_NOT_FOUND, ENV_OK}; -use crate::env::EnvMode; +use crate::env::{EnvMode, EnvStackSetResult}; +use crate::io::IoStreams; +use crate::parser::Parser; use crate::re::{regex_make_anchored, to_boxed_chars}; use pcre2::utf32::{Regex, RegexBuilder}; @@ -391,7 +392,7 @@ fn abbr_add(opts: &Options, streams: &mut IoStreams) -> Option { } // Erase the named abbreviations. -fn abbr_erase(opts: &Options, parser: &mut Parser) -> Option { +fn abbr_erase(opts: &Options, parser: &Parser) -> Option { if opts.args.is_empty() { // This has historically been a silent failure. return STATUS_CMD_ERROR; @@ -402,15 +403,15 @@ fn abbr_erase(opts: &Options, parser: &mut Parser) -> Option { let mut result = STATUS_CMD_OK; for arg in &opts.args { if !abbrs.erase(arg) { - result = Some(ENV_NOT_FOUND); + result = Some(EnvStackSetResult::ENV_NOT_FOUND.into()); } // Erase the old uvar - this makes `abbr -e` work. let esc_src = escape(arg); if !esc_src.is_empty() { let var_name = WString::from_str("_fish_abbr_") + esc_src.as_utfstr(); - let ret = parser.remove_var(&var_name, EnvMode::UNIVERSAL.into()); + let ret = parser.vars().remove(&var_name, EnvMode::UNIVERSAL); - if ret == autocxx::c_int(ENV_OK) { + if ret == EnvStackSetResult::ENV_OK { result = STATUS_CMD_OK }; } @@ -419,7 +420,7 @@ fn abbr_erase(opts: &Options, parser: &mut Parser) -> Option { }) } -pub fn abbr(parser: &mut Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Option { +pub fn abbr(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Option { let mut argv_read = Vec::with_capacity(argv.len()); argv_read.extend_from_slice(argv); @@ -539,7 +540,7 @@ pub fn abbr(parser: &mut Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> cmd, argv_read[w.woptind - 1] )); - builtin_print_error_trailer(parser, streams, cmd); + builtin_print_error_trailer(parser, streams.err, cmd); } 'h' => { builtin_print_help(parser, streams, cmd); diff --git a/fish-rust/src/builtins/argparse.rs b/fish-rust/src/builtins/argparse.rs index c650aebd5..564c4c3f5 100644 --- a/fish-rust/src/builtins/argparse.rs +++ b/fish-rust/src/builtins/argparse.rs @@ -3,6 +3,7 @@ use super::prelude::*; use crate::env::{EnvMode, EnvStack}; +use crate::exec::exec_subshell; use crate::wcstringutil::split_string; use crate::wutil::fish_iswalnum; @@ -81,29 +82,6 @@ fn new() -> Self { wopt(L!("max-args"), woption_argument_t::required_argument, 'X'), ]; -fn exec_subshell( - cmd: &wstr, - parser: &mut Parser, - outputs: &mut Vec, - apply_exit_status: bool, -) -> Option { - use crate::ffi::exec_subshell_ffi; - use crate::wchar_ffi::wcstring_list_ffi_t; - - let mut cmd_output: cxx::UniquePtr = wcstring_list_ffi_t::create(); - let retval = Some( - exec_subshell_ffi( - cmd.to_ffi().as_ref().unwrap(), - parser.pin(), - cmd_output.pin_mut(), - apply_exit_status, - ) - .into(), - ); - *outputs = cmd_output.as_mut().unwrap().from_ffi(); - retval -} - // Check if any pair of mutually exclusive options was seen. Note that since every option must have // a short name we only need to check those. fn check_for_mutually_exclusive_flags( @@ -499,7 +477,7 @@ fn parse_cmd_opts<'args>( optind: &mut usize, argc: usize, args: &mut [&'args wstr], - parser: &mut Parser, + parser: &Parser, streams: &mut IoStreams, ) -> Option { let cmd = args[0]; @@ -583,7 +561,7 @@ fn parse_cmd_opts<'args>( // If no name has been given, we default to the function name. // If any error happens, the backtrace will show which argparse it was. opts.name = parser - .get_func_name(1) + .get_function_name(1) .unwrap_or_else(|| L!("argparse").to_owned()); } @@ -624,7 +602,7 @@ fn populate_option_strings<'args>( } fn validate_arg<'opts>( - parser: &mut Parser, + parser: &Parser, opts_name: &wstr, opt_spec: &mut OptionSpec<'opts>, is_long_flag: bool, @@ -636,7 +614,7 @@ fn validate_arg<'opts>( return STATUS_CMD_OK; } - let vars = parser.get_vars(); + let vars = parser.vars(); vars.push(true /* new_scope */); let env_mode = EnvMode::LOCAL | EnvMode::EXPORT; @@ -659,13 +637,19 @@ fn validate_arg<'opts>( let mut cmd_output = Vec::new(); - let retval = exec_subshell(opt_spec.validation_command, parser, &mut cmd_output, false); + let retval = exec_subshell( + opt_spec.validation_command, + parser, + Some(&mut cmd_output), + false, + ); for output in cmd_output { - streams.err.appendln(output); + streams.err.append(output); + streams.err.append_char('\n'); } vars.pop(); - return retval; + Some(retval) } /// \return whether the option 'opt' is an implicit integer option. @@ -681,7 +665,7 @@ fn is_implicit_int(opts: &ArgParseCmdOpts, val: &wstr) -> bool { // Store this value under the implicit int option. fn validate_and_store_implicit_int<'args>( - parser: &mut Parser, + parser: &Parser, opts: &mut ArgParseCmdOpts<'args>, val: &'args wstr, w: &mut wgetopter_t, @@ -705,7 +689,7 @@ fn validate_and_store_implicit_int<'args>( } fn handle_flag<'args>( - parser: &mut Parser, + parser: &Parser, opts: &mut ArgParseCmdOpts<'args>, opt: char, is_long_flag: bool, @@ -754,7 +738,7 @@ fn handle_flag<'args>( } fn argparse_parse_flags<'args>( - parser: &mut Parser, + parser: &Parser, opts: &mut ArgParseCmdOpts<'args>, argc: usize, args: &mut [&'args wstr], @@ -855,7 +839,7 @@ fn argparse_parse_args<'args>( opts: &mut ArgParseCmdOpts<'args>, args: &mut [&'args wstr], argc: usize, - parser: &mut Parser, + parser: &Parser, streams: &mut IoStreams, ) -> Option { if argc <= 1 { @@ -943,7 +927,7 @@ fn set_argparse_result_vars(vars: &EnvStack, opts: &ArgParseCmdOpts) { /// an external command also means its output has to be in a form that can be eval'd. Because our /// version is a builtin it can directly set variables local to the current scope (e.g., a /// function). It doesn't need to write anything to stdout that then needs to be eval'd. -pub fn argparse(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { +pub fn argparse(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { let cmd = args[0]; let argc = args.len(); @@ -954,7 +938,7 @@ pub fn argparse(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr] // This is an error in argparse usage, so we append the error trailer with a stack trace. // The other errors are an error in using *the command* that is using argparse, // so our help doesn't apply. - builtin_print_error_trailer(parser, streams, cmd); + builtin_print_error_trailer(parser, streams.err, cmd); return retval; } @@ -987,7 +971,7 @@ pub fn argparse(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr] return retval; } - set_argparse_result_vars(&parser.get_vars(), &opts); + set_argparse_result_vars(parser.vars(), &opts); return retval; } diff --git a/fish-rust/src/builtins/bg.rs b/fish-rust/src/builtins/bg.rs index 3292e7ebe..8bc4545cd 100644 --- a/fish-rust/src/builtins/bg.rs +++ b/fish-rust/src/builtins/bg.rs @@ -1,53 +1,52 @@ // Implementation of the bg builtin. -use std::pin::Pin; +use libc::pid_t; use super::prelude::*; /// Helper function for builtin_bg(). fn send_to_bg( - parser: &mut Parser, + parser: &Parser, streams: &mut IoStreams, cmd: &wstr, job_pos: usize, ) -> Option { - let job = parser.get_jobs()[job_pos] - .as_ref() - .expect("job_pos must be valid"); - if !job.wants_job_control() { - let err = wgettext_fmt!( - "%ls: Can't put job %d, '%ls' to background because it is not under job control\n", - cmd, - job.job_id().0, - job.command().from_ffi() - ); - ffi::builtin_print_help( - parser.pin(), - streams.ffi_ref(), - c_str!(cmd), - err.to_ffi().as_ref()?, - ); - return STATUS_CMD_ERROR; + { + let jobs = parser.jobs(); + if !jobs[job_pos].wants_job_control() { + let err = { + let job = &jobs[job_pos]; + wgettext_fmt!( + "%ls: Can't put job %s, '%ls' to background because it is not under job control\n", + cmd, + job.job_id().to_wstring(), + job.command() + ) + }; + builtin_print_help_error(parser, streams, cmd, &err); + return STATUS_CMD_ERROR; + } + + let job = &jobs[job_pos]; + streams.err.append(wgettext_fmt!( + "Send job %s '%ls' to background\n", + job.job_id().to_wstring(), + job.command() + )); + + job.group().set_is_foreground(false); + + if !job.resume() { + return STATUS_CMD_ERROR; + } } - - streams.err.append(wgettext_fmt!( - "Send job %d '%ls' to background\n", - job.job_id().0, - job.command().from_ffi() - )); - - job.get_job_group().set_is_foreground(false); - - if !job.ffi_resume() { - return STATUS_CMD_ERROR; - } - parser.pin().job_promote_at(job_pos); + parser.job_promote_at(job_pos); return STATUS_CMD_OK; } /// Builtin for putting a job in the background. -pub fn bg(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { +pub fn bg(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { let opts = match HelpOnlyCmdOpts::parse(args, parser, streams) { Ok(opts) => opts, Err(err @ Some(_)) if err != STATUS_CMD_OK => return err, @@ -62,14 +61,11 @@ pub fn bg(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> O if opts.optind == args.len() { // No jobs were specified so use the most recent (i.e., last) job. - let jobs = parser.get_jobs(); - let job_pos = jobs.iter().position(|job| { - if let Some(job) = job.as_ref() { - return job.is_stopped() && job.wants_job_control() && !job.is_completed(); - } - - false - }); + let job_pos = { + let jobs = parser.jobs(); + jobs.iter() + .position(|job| job.is_stopped() && job.wants_job_control() && !job.is_completed()) + }; let Some(job_pos) = job_pos else { streams @@ -82,25 +78,23 @@ pub fn bg(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> O } // The user specified at least one job to be backgrounded. - let mut pids: Vec = Vec::new(); - // If one argument is not a valid pid (i.e. integer >= 0), fail without backgrounding anything, // but still print errors for all of them. let mut retval: Option = STATUS_CMD_OK; - for arg in &args[opts.optind..] { - let pid = fish_wcstoi(arg); - #[allow(clippy::unnecessary_unwrap)] - if pid.is_err() || pid.unwrap() < 0 { - streams.err.append(wgettext_fmt!( - "%ls: '%ls' is not a valid job specifier\n", - cmd, - arg - )); - retval = STATUS_INVALID_ARGS; - } else { - pids.push(pid.unwrap()); - } - } + let pids: Vec = args[opts.optind..] + .iter() + .map(|arg| { + fish_wcstoi(arg).unwrap_or_else(|_| { + streams.err.append(wgettext_fmt!( + "%ls: '%ls' is not a valid job specifier\n", + cmd, + arg + )); + retval = STATUS_INVALID_ARGS; + 0 + }) + }) + .collect(); if retval != STATUS_CMD_OK { return retval; @@ -109,14 +103,7 @@ pub fn bg(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> O // Background all existing jobs that match the pids. // Non-existent jobs aren't an error, but information about them is useful. for pid in pids { - let mut job_pos = 0; - let job = unsafe { - parser - .job_get_from_pid1(autocxx::c_int(pid), Pin::new(&mut job_pos)) - .as_ref() - }; - - if job.is_some() { + if let Some((job_pos, _job)) = parser.job_get_with_index_from_pid(pid) { send_to_bg(parser, streams, cmd, job_pos); } else { streams diff --git a/fish-rust/src/builtins/bind.rs b/fish-rust/src/builtins/bind.rs new file mode 100644 index 000000000..e2daa34d8 --- /dev/null +++ b/fish-rust/src/builtins/bind.rs @@ -0,0 +1,28 @@ +//! Implementation of the bind builtin. + +use super::prelude::*; + +const BIND_INSERT: c_int = 0; +const BIND_ERASE: c_int = 1; +const BIND_KEY_NAMES: c_int = 2; +const BIND_FUNCTION_NAMES: c_int = 3; + +struct Options { + all: bool, + bind_mode_given: bool, + list_modes: bool, + print_help: bool, + silent: bool, + use_terminfo: bool, + have_user: bool, + user: bool, + have_preset: bool, + preset: bool, + mode: c_int, + bind_mode: &'static wstr, + sets_bind_mode: &'static wstr, +} + +pub fn bind(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { + run_builtin_ffi(crate::ffi::builtin_bind, parser, streams, args) +} diff --git a/fish-rust/src/builtins/block.rs b/fish-rust/src/builtins/block.rs index 2a146247e..10348e8a6 100644 --- a/fish-rust/src/builtins/block.rs +++ b/fish-rust/src/builtins/block.rs @@ -1,3 +1,5 @@ +use std::sync::atomic::Ordering; + // Implementation of the block builtin. use super::prelude::*; @@ -23,7 +25,7 @@ struct Options { fn parse_options( args: &mut [&wstr], - parser: &mut Parser, + parser: &Parser, streams: &mut IoStreams, ) -> Result<(Options, usize), Option> { let cmd = args[0]; @@ -71,7 +73,7 @@ fn parse_options( } /// The block builtin, used for temporarily blocking events. -pub fn block(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { +pub fn block(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { let cmd = args[0]; let opts = match parse_options(args, parser, streams) { @@ -94,49 +96,52 @@ pub fn block(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) - return STATUS_INVALID_ARGS; } - if parser.ffi_global_event_blocks() == 0 { + if parser.global_event_blocks.load(Ordering::Relaxed) == 0 { streams .err .append(wgettext_fmt!("%ls: No blocks defined\n", cmd)); return STATUS_CMD_ERROR; } - parser.pin().ffi_decr_global_event_blocks(); + parser.global_event_blocks.fetch_sub(1, Ordering::Relaxed); return STATUS_CMD_OK; } let mut block_idx = 0; - let mut block = unsafe { parser.pin().block_at_index1(block_idx).as_mut() }; + let have_block = { + let mut block = parser.block_at_index(block_idx); - match opts.scope { - Scope::Local => { - // If this is the outermost block, then we're global - if block_idx + 1 >= parser.ffi_blocks_size() { + match opts.scope { + Scope::Local => { + // If this is the outermost block, then we're global + if block_idx + 1 >= parser.blocks_size() { + block = None; + } + } + Scope::Global => { block = None; } - } - Scope::Global => { - block = None; - } - Scope::Unset => { - loop { - block = if let Some(block) = block.as_mut() { - if !block.is_function_call() { + Scope::Unset => { + loop { + block = if let Some(block) = block.as_mut() { + if !block.is_function_call() { + break; + } + // Set it in function scope + block_idx += 1; + parser.block_at_index(block_idx) + } else { break; } - // Set it in function scope - block_idx += 1; - unsafe { parser.pin().block_at_index1(block_idx).as_mut() } - } else { - break; } } } - } + block.is_some() + }; - if let Some(block) = block.as_mut() { - block.pin().ffi_incr_event_blocks(); + if have_block { + parser.block_at_index_mut(block_idx).unwrap().event_blocks += 1; } else { - parser.pin().ffi_incr_global_event_blocks(); + parser.global_event_blocks.fetch_add(1, Ordering::Relaxed); } return STATUS_CMD_OK; diff --git a/fish-rust/src/builtins/builtin.rs b/fish-rust/src/builtins/builtin.rs index caf753775..722426b2c 100644 --- a/fish-rust/src/builtins/builtin.rs +++ b/fish-rust/src/builtins/builtin.rs @@ -1,5 +1,5 @@ use super::prelude::*; -use crate::ffi::{builtin_exists, builtin_get_names_ffi}; +use crate::builtins::shared::{builtin_exists, builtin_get_names}; #[derive(Default)] struct builtin_cmd_opts_t { @@ -7,11 +7,7 @@ struct builtin_cmd_opts_t { list_names: bool, } -pub fn r#builtin( - parser: &mut Parser, - streams: &mut IoStreams, - argv: &mut [&wstr], -) -> Option { +pub fn r#builtin(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Option { let cmd = argv[0]; let argc = argv.len(); let print_hints = false; @@ -67,7 +63,7 @@ pub fn r#builtin( if opts.query { let optind = w.woptind; for arg in argv.iter().take(argc).skip(optind) { - if builtin_exists(&arg.to_ffi()) { + if builtin_exists(arg) { return STATUS_CMD_OK; } } @@ -76,7 +72,7 @@ pub fn r#builtin( if opts.list_names { // List is guaranteed to be sorted by name. - let names: Vec = builtin_get_names_ffi().from_ffi(); + let names = builtin_get_names(); for name in names { streams.out.appendln(name); } diff --git a/fish-rust/src/builtins/cd.rs b/fish-rust/src/builtins/cd.rs index 6e29eae33..982adb348 100644 --- a/fish-rust/src/builtins/cd.rs +++ b/fish-rust/src/builtins/cd.rs @@ -9,10 +9,11 @@ }; use errno::{self, Errno}; use libc::{fchdir, EACCES, ELOOP, ENOENT, ENOTDIR, EPERM, O_RDONLY}; +use std::sync::Arc; // The cd builtin. Changes the current directory to the one specified or to $HOME if none is // specified. The directory can be relative to any directory in the CDPATH variable. -pub fn cd(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { +pub fn cd(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { let cmd = args[0]; let opts = match HelpOnlyCmdOpts::parse(args, parser, streams) { @@ -26,7 +27,7 @@ pub fn cd(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> O return STATUS_CMD_OK; } - let vars = parser.get_vars(); + let vars = parser.vars(); let tmpstr; let dir_in: &wstr = if args.len() > opts.optind { @@ -54,14 +55,14 @@ pub fn cd(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> O dir_in )); if !parser.is_interactive() { - streams.err.append(parser.pin().current_line().from_ffi()); + streams.err.append(parser.current_line()); }; return STATUS_CMD_ERROR; } let pwd = vars.get_pwd_slash(); - let dirs = path_apply_cdpath(dir_in, &pwd, vars.as_ref()); + let dirs = path_apply_cdpath(dir_in, &pwd, vars); if dirs.is_empty() { streams.err.append(wgettext_fmt!( "%ls: The directory '%ls' does not exist\n", @@ -70,7 +71,7 @@ pub fn cd(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> O )); if !parser.is_interactive() { - streams.err.append(parser.pin().current_line().from_ffi()); + streams.err.append(parser.current_line()); } return STATUS_CMD_ERROR; @@ -86,7 +87,7 @@ pub fn cd(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> O errno::set_errno(Errno(0)); // We need to keep around the fd for this directory, in the parser. - let mut dir_fd = AutoCloseFd::new(wopen_cloexec(&norm_dir, O_RDONLY, 0)); + let dir_fd = Arc::new(AutoCloseFd::new(wopen_cloexec(&norm_dir, O_RDONLY, 0))); if !(dir_fd.is_valid() && unsafe { fchdir(dir_fd.fd()) } == 0) { // Some errors we skip and only report if nothing worked. @@ -113,13 +114,9 @@ pub fn cd(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> O } // Stash the fd for the cwd in the parser. - parser.pin().set_cwd_fd(autocxx::c_int(dir_fd.acquire())); + parser.libdata_mut().cwd_fd = Some(dir_fd); - parser.pin().set_var_and_fire( - &L!("PWD").to_ffi(), - EnvMode::EXPORT.bits() | EnvMode::GLOBAL.bits(), - norm_dir, - ); + parser.set_var_and_fire(L!("PWD"), EnvMode::EXPORT | EnvMode::GLOBAL, vec![norm_dir]); return STATUS_CMD_OK; } @@ -165,7 +162,7 @@ pub fn cd(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> O } if !parser.is_interactive() { - streams.err.append(parser.pin().current_line().from_ffi()); + streams.err.append(parser.current_line()); } return STATUS_CMD_ERROR; diff --git a/fish-rust/src/builtins/command.rs b/fish-rust/src/builtins/command.rs index ffaccbe3e..d237204d8 100644 --- a/fish-rust/src/builtins/command.rs +++ b/fish-rust/src/builtins/command.rs @@ -8,11 +8,7 @@ struct command_cmd_opts_t { find_path: bool, } -pub fn r#command( - parser: &mut Parser, - streams: &mut IoStreams, - argv: &mut [&wstr], -) -> Option { +pub fn r#command(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Option { let cmd = argv[0]; let argc = argv.len(); let print_hints = false; @@ -63,14 +59,13 @@ pub fn r#command( let optind = w.woptind; for arg in argv.iter().take(argc).skip(optind) { let paths = if opts.all { - path_get_paths(arg, &*parser.get_vars()) + path_get_paths(arg, parser.vars()) } else { - match path_get_path(arg, &*parser.get_vars()) { + match path_get_path(arg, parser.vars()) { Some(p) => vec![p], None => vec![], } }; - for path in paths.iter() { res = true; if opts.quiet { diff --git a/fish-rust/src/builtins/commandline.rs b/fish-rust/src/builtins/commandline.rs new file mode 100644 index 000000000..f9df0c827 --- /dev/null +++ b/fish-rust/src/builtins/commandline.rs @@ -0,0 +1,5 @@ +use super::prelude::*; + +pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { + run_builtin_ffi(crate::ffi::builtin_commandline, parser, streams, args) +} diff --git a/fish-rust/src/builtins/complete.rs b/fish-rust/src/builtins/complete.rs new file mode 100644 index 000000000..3ff6a9634 --- /dev/null +++ b/fish-rust/src/builtins/complete.rs @@ -0,0 +1,644 @@ +use super::prelude::*; +use crate::common::{ + unescape_string, unescape_string_in_place, ScopeGuard, UnescapeFlags, UnescapeStringStyle, +}; +use crate::complete::{complete_add_wrapper, complete_remove_wrapper, CompletionRequestOptions}; +use crate::ffi; +use crate::highlight::colorize; +use crate::highlight::highlight_shell; +use crate::parse_constants::ParseErrorList; +use crate::parse_util::parse_util_detect_errors_in_argument_list; +use crate::parse_util::{parse_util_detect_errors, parse_util_token_extent}; +use crate::wcstringutil::string_suffixes_string; +use crate::{ + common::str2wcstring, + complete::{ + complete_add, complete_print, complete_remove, complete_remove_all, CompleteFlags, + CompleteOptionType, CompletionMode, + }, +}; +use libc::STDOUT_FILENO; + +// builtin_complete_* are a set of rather silly looping functions that make sure that all the proper +// combinations of complete_add or complete_remove get called. This is needed since complete allows +// you to specify multiple switches on a single commandline, like 'complete -s a -s b -s c', but the +// complete_add function only accepts one short switch and one long switch. + +/// Silly function. +fn builtin_complete_add2( + cmd: &wstr, + cmd_is_path: bool, + short_opt: &wstr, + gnu_opts: &[WString], + old_opts: &[WString], + result_mode: CompletionMode, + condition: &[WString], + comp: &wstr, + desc: &wstr, + flags: CompleteFlags, +) { + for short_opt in short_opt.chars() { + complete_add( + cmd.to_owned(), + cmd_is_path, + WString::from(&[short_opt][..]), + CompleteOptionType::Short, + result_mode, + condition.to_vec(), + comp.to_owned(), + desc.to_owned(), + flags, + ); + } + + for gnu_opt in gnu_opts { + complete_add( + cmd.to_owned(), + cmd_is_path, + gnu_opt.to_owned(), + CompleteOptionType::DoubleLong, + result_mode, + condition.to_vec(), + comp.to_owned(), + desc.to_owned(), + flags, + ); + } + + for old_opt in old_opts { + complete_add( + cmd.to_owned(), + cmd_is_path, + old_opt.to_owned(), + CompleteOptionType::SingleLong, + result_mode, + condition.to_vec(), + comp.to_owned(), + desc.to_owned(), + flags, + ); + } + + if old_opts.is_empty() && gnu_opts.is_empty() && short_opt.is_empty() { + complete_add( + cmd.to_owned(), + cmd_is_path, + WString::new(), + CompleteOptionType::ArgsOnly, + result_mode, + condition.to_vec(), + comp.to_owned(), + desc.to_owned(), + flags, + ); + } +} + +/// Sily function. +fn builtin_complete_add( + cmds: &[WString], + paths: &[WString], + short_opt: &wstr, + gnu_opt: &[WString], + old_opt: &[WString], + result_mode: CompletionMode, + condition: &[WString], + comp: &wstr, + desc: &wstr, + flags: CompleteFlags, +) { + for cmd in cmds { + builtin_complete_add2( + cmd, + false, /* not path */ + short_opt, + gnu_opt, + old_opt, + result_mode, + condition, + comp, + desc, + flags, + ); + } + for path in paths { + builtin_complete_add2( + path, + true, /* is path */ + short_opt, + gnu_opt, + old_opt, + result_mode, + condition, + comp, + desc, + flags, + ); + } +} + +fn builtin_complete_remove_cmd( + cmd: &WString, + cmd_is_path: bool, + short_opt: &wstr, + gnu_opt: &[WString], + old_opt: &[WString], +) { + let mut removed = false; + for s in short_opt.chars() { + complete_remove( + cmd.to_owned(), + cmd_is_path, + wstr::from_char_slice(&[s]), + CompleteOptionType::Short, + ); + removed = true; + } + + for opt in old_opt { + complete_remove( + cmd.to_owned(), + cmd_is_path, + opt, + CompleteOptionType::SingleLong, + ); + removed = true; + } + + for opt in gnu_opt { + complete_remove( + cmd.to_owned(), + cmd_is_path, + opt, + CompleteOptionType::DoubleLong, + ); + removed = true; + } + + if !removed { + // This means that all loops were empty. + complete_remove_all(cmd.to_owned(), cmd_is_path); + } +} + +fn builtin_complete_remove( + cmds: &[WString], + paths: &[WString], + short_opt: &wstr, + gnu_opt: &[WString], + old_opt: &[WString], +) { + for cmd in cmds { + builtin_complete_remove_cmd(cmd, false /* not path */, short_opt, gnu_opt, old_opt); + } + + for path in paths { + builtin_complete_remove_cmd(path, true /* is path */, short_opt, gnu_opt, old_opt); + } +} + +fn builtin_complete_print(cmd: &wstr, streams: &mut IoStreams, parser: &Parser) { + let repr = complete_print(cmd); + + // colorize if interactive + if !streams.out_is_redirected && unsafe { libc::isatty(STDOUT_FILENO) } != 0 { + let mut colors = vec![]; + highlight_shell(&repr, &mut colors, &parser.context(), false, None); + streams + .out + .append(str2wcstring(&colorize(&repr, &colors, parser.vars()))); + } else { + streams.out.append(repr); + } +} + +/// Values used for long-only options. +const OPT_ESCAPE: char = '\x01'; + +/// The complete builtin. Used for specifying programmable tab-completions. Calls the functions in +/// complete.cpp for any heavy lifting. +pub fn complete(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Option { + let cmd = argv[0]; + let argc = argv.len(); + let mut result_mode = CompletionMode::default(); + let mut remove = false; + let mut short_opt = WString::new(); + // todo!("these whould be Vec<&wstr>"); + let mut gnu_opt = vec![]; + let mut old_opt = vec![]; + let mut subcommand = vec![]; + let mut comp = WString::new(); + let mut desc = WString::new(); + let mut condition = vec![]; + let mut do_complete = false; + let mut do_complete_param = None; + let mut cmd_to_complete = vec![]; + let mut path = vec![]; + let mut wrap_targets = vec![]; + let mut preserve_order = false; + let mut unescape_output = true; + + const short_options: &wstr = L!(":a:c:p:s:l:o:d:fFrxeuAn:C::w:hk"); + const long_options: &[woption] = &[ + wopt(L!("exclusive"), woption_argument_t::no_argument, 'x'), + wopt(L!("no-files"), woption_argument_t::no_argument, 'f'), + wopt(L!("force-files"), woption_argument_t::no_argument, 'F'), + wopt( + L!("require-parameter"), + woption_argument_t::no_argument, + 'r', + ), + wopt(L!("path"), woption_argument_t::required_argument, 'p'), + wopt(L!("command"), woption_argument_t::required_argument, 'c'), + wopt( + L!("short-option"), + woption_argument_t::required_argument, + 's', + ), + wopt( + L!("long-option"), + woption_argument_t::required_argument, + 'l', + ), + wopt(L!("old-option"), woption_argument_t::required_argument, 'o'), + wopt(L!("subcommand"), woption_argument_t::required_argument, 'S'), + wopt( + L!("description"), + woption_argument_t::required_argument, + 'd', + ), + wopt(L!("arguments"), woption_argument_t::required_argument, 'a'), + wopt(L!("erase"), woption_argument_t::no_argument, 'e'), + wopt(L!("unauthoritative"), woption_argument_t::no_argument, 'u'), + wopt(L!("authoritative"), woption_argument_t::no_argument, 'A'), + wopt(L!("condition"), woption_argument_t::required_argument, 'n'), + wopt(L!("wraps"), woption_argument_t::required_argument, 'w'), + wopt( + L!("do-complete"), + woption_argument_t::optional_argument, + 'C', + ), + wopt(L!("help"), woption_argument_t::no_argument, 'h'), + wopt(L!("keep-order"), woption_argument_t::no_argument, 'k'), + wopt(L!("escape"), woption_argument_t::no_argument, OPT_ESCAPE), + ]; + + let mut have_x = false; + + let mut w = wgetopter_t::new(short_options, long_options, argv); + while let Some(opt) = w.wgetopt_long() { + match opt { + 'x' => { + result_mode.no_files = true; + result_mode.requires_param = true; + // Needed to print an error later; + have_x = true; + } + 'f' => { + result_mode.no_files = true; + } + 'F' => { + result_mode.force_files = true; + } + 'r' => { + result_mode.requires_param = true; + } + 'k' => { + preserve_order = true; + } + 'p' | 'c' => { + if let Some(tmp) = unescape_string( + w.woptarg.unwrap(), + UnescapeStringStyle::Script(UnescapeFlags::SPECIAL), + ) { + if opt == 'p' { + path.push(tmp); + } else { + cmd_to_complete.push(tmp); + } + } else { + streams.err.append(wgettext_fmt!( + "%ls: Invalid token '%ls'\n", + cmd, + w.woptarg.unwrap() + )); + return STATUS_INVALID_ARGS; + } + } + 'd' => { + desc = w.woptarg.unwrap().to_owned(); + } + 'u' => { + // This option was removed in commit 1911298 and is now a no-op. + } + 'A' => { + // This option was removed in commit 1911298 and is now a no-op. + } + 's' => { + let arg = w.woptarg.unwrap(); + short_opt.extend(arg.chars()); + if arg.is_empty() { + streams + .err + .append(wgettext_fmt!("%ls: -s requires a non-empty string\n", cmd,)); + return STATUS_INVALID_ARGS; + } + } + 'l' => { + let arg = w.woptarg.unwrap(); + gnu_opt.push(arg.to_owned()); + if arg.is_empty() { + streams + .err + .append(wgettext_fmt!("%ls: -l requires a non-empty string\n", cmd,)); + return STATUS_INVALID_ARGS; + } + } + 'o' => { + let arg = w.woptarg.unwrap(); + old_opt.push(arg.to_owned()); + if arg.is_empty() { + streams + .err + .append(wgettext_fmt!("%ls: -o requires a non-empty string\n", cmd,)); + return STATUS_INVALID_ARGS; + } + } + 'S' => { + let arg = w.woptarg.unwrap(); + subcommand.push(arg.to_owned()); + if arg.is_empty() { + streams + .err + .append(wgettext_fmt!("%ls: -S requires a non-empty string\n", cmd,)); + return STATUS_INVALID_ARGS; + } + } + 'a' => { + comp = w.woptarg.unwrap().to_owned(); + } + 'e' => remove = true, + 'n' => { + condition.push(w.woptarg.unwrap().to_owned()); + } + 'w' => { + wrap_targets.push(w.woptarg.unwrap().to_owned()); + } + 'C' => { + do_complete = true; + if let Some(s) = w.woptarg { + do_complete_param = Some(s.to_owned()); + } + } + OPT_ESCAPE => { + unescape_output = false; + } + 'h' => { + builtin_print_help(parser, streams, cmd); + return STATUS_CMD_OK; + } + ':' => { + builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1], true); + return STATUS_INVALID_ARGS; + } + '?' => { + builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1], true); + return STATUS_INVALID_ARGS; + } + _ => panic!("unexpected retval from wgetopt_long"), + } + } + + if result_mode.no_files && result_mode.force_files { + if !have_x { + streams.err.append(wgettext_fmt!( + BUILTIN_ERR_COMBO2, + "complete", + "'--no-files' and '--force-files'" + )); + } else { + // The reason for us not wanting files is `-x`, + // which is short for `-rf`. + streams.err.append(wgettext_fmt!( + BUILTIN_ERR_COMBO2, + "complete", + "'--exclusive' and '--force-files'" + )); + } + return STATUS_INVALID_ARGS; + } + + if w.woptind != argc { + // Use one left-over arg as the do-complete argument + // to enable `complete -C "git check"`. + if do_complete && do_complete_param.is_none() && argc == w.woptind + 1 { + do_complete_param = Some(argv[argc - 1].to_owned()); + } else if !do_complete && cmd_to_complete.is_empty() && argc == w.woptind + 1 { + // Or use one left-over arg as the command to complete + cmd_to_complete.push(argv[argc - 1].to_owned()); + } else { + streams + .err + .append(wgettext_fmt!(BUILTIN_ERR_TOO_MANY_ARGUMENTS, cmd)); + builtin_print_error_trailer(parser, streams.err, cmd); + return STATUS_INVALID_ARGS; + } + } + + for condition_string in &condition { + let mut errors = ParseErrorList::new(); + if parse_util_detect_errors(condition_string, Some(&mut errors), false).is_err() { + for error in errors { + let prefix = cmd.to_owned() + L!(": -n '") + &condition_string[..] + L!("': "); + streams.err.append(error.describe_with_prefix( + condition_string, + &prefix, + parser.is_interactive(), + false, + )); + streams.err.push('\n'); + } + return STATUS_CMD_ERROR; + } + } + + if !comp.is_empty() { + let mut prefix = WString::new(); + prefix.push_utfstr(cmd); + prefix.push_str(": "); + + if let Err(err_text) = parse_util_detect_errors_in_argument_list(&comp, &prefix) { + streams.err.append(wgettext_fmt!( + "%ls: %ls: contains a syntax error\n", + cmd, + comp + )); + streams.err.append(err_text); + streams.err.push('\n'); + return STATUS_CMD_ERROR; + } + } + + if do_complete { + let have_do_complete_param = do_complete_param.is_some(); + let do_complete_param = match do_complete_param { + None => { + // No argument given, try to use the current commandline. + if !ffi::commandline_get_state_initialized_ffi() { + // This corresponds to using 'complete -C' in non-interactive mode. + // See #2361 . + builtin_missing_argument(parser, streams, cmd, L!("-C"), true); + return STATUS_INVALID_ARGS; + } + ffi::commandline_get_state_text_ffi().from_ffi() + } + Some(param) => param, + }; + + let mut token = 0..0; + parse_util_token_extent( + &do_complete_param, + do_complete_param.len(), + &mut token, + None, + ); + + // Create a scoped transient command line, so that builtin_commandline will see our + // argument, not the reader buffer. + parser + .libdata_mut() + .transient_commandlines + .push(do_complete_param.clone()); + let _remove_transient = ScopeGuard::new((), |()| { + parser.libdata_mut().transient_commandlines.pop(); + }); + + // Prevent accidental recursion (see #6171). + if !parser.libdata().pods.builtin_complete_current_commandline { + if !have_do_complete_param { + parser + .libdata_mut() + .pods + .builtin_complete_current_commandline = true; + } + + let (mut comp, _needs_load) = crate::complete::complete( + &do_complete_param, + CompletionRequestOptions::normal(), + &parser.context(), + ); + + // Apply the same sort and deduplication treatment as pager completions + crate::complete::sort_and_prioritize(&mut comp, CompletionRequestOptions::default()); + + for next in comp { + // Make a fake commandline, and then apply the completion to it. + let faux_cmdline = &do_complete_param[token.clone()]; + let mut tmp_cursor = faux_cmdline.len(); + let mut faux_cmdline_with_completion = ffi::completion_apply_to_command_line( + &next.completion.to_ffi(), + unsafe { std::mem::transmute(next.flags) }, + &faux_cmdline.to_ffi(), + &mut tmp_cursor, + false, + ) + .from_ffi(); + + // completion_apply_to_command_line will append a space unless COMPLETE_NO_SPACE + // is set. We don't want to set COMPLETE_NO_SPACE because that won't close + // quotes. What we want is to close the quote, but not append the space. So we + // just look for the space and clear it. + if !next.flags.contains(CompleteFlags::NO_SPACE) + && string_suffixes_string(L!(" "), &faux_cmdline_with_completion) + { + faux_cmdline_with_completion.truncate(faux_cmdline_with_completion.len() - 1); + } + + if unescape_output { + // The input data is meant to be something like you would have on the command + // line, e.g. includes backslashes. The output should be raw, i.e. unescaped. So + // we need to unescape the command line. See #1127. + unescape_string_in_place( + &mut faux_cmdline_with_completion, + UnescapeStringStyle::Script(UnescapeFlags::default()), + ); + } + + // Append any description. + if !next.description.is_empty() { + faux_cmdline_with_completion + .reserve(faux_cmdline_with_completion.len() + 2 + next.description.len()); + faux_cmdline_with_completion.push('\t'); + faux_cmdline_with_completion.push_utfstr(&next.description); + } + faux_cmdline_with_completion.push('\n'); + streams.out.append(faux_cmdline_with_completion); + } + + parser + .libdata_mut() + .pods + .builtin_complete_current_commandline = false; + } + } else if path.is_empty() + && gnu_opt.is_empty() + && short_opt.is_empty() + && old_opt.is_empty() + && !remove + && comp.is_empty() + && desc.is_empty() + && condition.is_empty() + && wrap_targets.is_empty() + && !result_mode.no_files + && !result_mode.force_files + && !result_mode.requires_param + { + // No arguments that would add or remove anything specified, so we print the definitions of + // all matching completions. + if cmd_to_complete.is_empty() { + builtin_complete_print(L!(""), streams, parser); + } else { + for cmd in cmd_to_complete { + builtin_complete_print(&cmd, streams, parser); + } + } + } else { + let mut flags = CompleteFlags::AUTO_SPACE; + // HACK: Don't escape tildes because at the beginning of a token they probably mean + // $HOME, for example as produced by a recursive call to "complete -C". + flags |= CompleteFlags::DONT_ESCAPE_TILDES; + if preserve_order { + flags |= CompleteFlags::DONT_SORT; + } + + if remove { + builtin_complete_remove(&cmd_to_complete, &path, &short_opt, &gnu_opt, &old_opt); + } else { + builtin_complete_add( + &cmd_to_complete, + &path, + &short_opt, + &gnu_opt, + &old_opt, + result_mode, + &condition, + &comp, + &desc, + flags, + ); + } + + // Handle wrap targets (probably empty). We only wrap commands, not paths. + for wrap_target in wrap_targets { + for i in &cmd_to_complete { + if remove { + complete_remove_wrapper(i.clone(), &wrap_target); + } else { + complete_add_wrapper(i.clone(), wrap_target.clone()); + } + } + } + } + + STATUS_CMD_OK +} diff --git a/fish-rust/src/builtins/contains.rs b/fish-rust/src/builtins/contains.rs index a4e1e0ba5..5ea927343 100644 --- a/fish-rust/src/builtins/contains.rs +++ b/fish-rust/src/builtins/contains.rs @@ -9,7 +9,7 @@ struct Options { fn parse_options( args: &mut [&wstr], - parser: &mut Parser, + parser: &Parser, streams: &mut IoStreams, ) -> Result<(Options, usize), Option> { let cmd = args[0]; @@ -46,7 +46,7 @@ fn parse_options( /// Implementation of the builtin contains command, used to check if a specified string is part of /// a list. -pub fn contains(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { +pub fn contains(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { let cmd = args[0]; let (opts, optind) = match parse_options(args, parser, streams) { diff --git a/fish-rust/src/builtins/count.rs b/fish-rust/src/builtins/count.rs index d82c97fe8..63298e02d 100644 --- a/fish-rust/src/builtins/count.rs +++ b/fish-rust/src/builtins/count.rs @@ -5,7 +5,7 @@ const COUNT_CHUNK_SIZE: usize = 512 * 256; /// Implementation of the builtin count command, used to count the number of arguments sent to it. -pub fn count(_parser: &mut Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Option { +pub fn count(_parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Option { // Always add the size of argv (minus 0, which is "count"). // That means if you call `something | count a b c`, you'll get the count of something _plus 3_. let mut numargs = argv.len() - 1; diff --git a/fish-rust/src/builtins/disown.rs b/fish-rust/src/builtins/disown.rs new file mode 100644 index 000000000..93c6d25b1 --- /dev/null +++ b/fish-rust/src/builtins/disown.rs @@ -0,0 +1,126 @@ +// Implementation of the disown builtin. + +use super::shared::{builtin_print_help, STATUS_CMD_ERROR, STATUS_INVALID_ARGS}; +use crate::io::IoStreams; +use crate::parser::Parser; +use crate::proc::{add_disowned_job, Job}; +use crate::{ + builtins::shared::{HelpOnlyCmdOpts, STATUS_CMD_OK}, + wchar::wstr, + wutil::{fish_wcstoi, wgettext_fmt}, +}; +use libc::c_int; +use libc::{self, SIGCONT}; + +/// Helper for builtin_disown. +fn disown_job(cmd: &wstr, streams: &mut IoStreams, j: &Job) { + // Nothing to do if already disowned. + if j.flags().disown_requested { + return; + } + + // Stopped disowned jobs must be manually signaled; explain how to do so. + let pgid = j.get_pgid(); + if j.is_stopped() { + if let Some(pgid) = pgid { + unsafe { + libc::killpg(pgid, SIGCONT); + } + } + streams.err.append(wgettext_fmt!( + "%ls: job %d ('%ls') was stopped and has been signalled to continue.\n", + cmd, + j.job_id(), + j.command() + )); + } + + // We cannot directly remove the job from the jobs() list as `disown` might be called + // within the context of a subjob which will cause the parent job to crash in exec_job(). + // Instead, we set a flag and the parser removes the job from the jobs list later. + j.mut_flags().disown_requested = true; + add_disowned_job(j); +} + +/// Builtin for removing jobs from the job list. +pub fn disown(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { + let opts = match HelpOnlyCmdOpts::parse(args, parser, streams) { + Ok(opts) => opts, + Err(err @ Some(_)) if err != STATUS_CMD_OK => return err, + Err(err) => panic!("Illogical exit code from parse_options(): {err:?}"), + }; + + let cmd = args[0]; + if opts.print_help { + builtin_print_help(parser, streams, cmd); + return STATUS_CMD_OK; + } + + let mut retval; + if opts.optind == args.len() { + // Select last constructed job (ie first job in the job queue) that is possible to disown. + // Stopped jobs can be disowned (they will be continued). + // Foreground jobs can be disowned. + // Even jobs that aren't under job control can be disowned! + let mut job = None; + for j in &parser.jobs()[..] { + if j.is_constructed() && !j.is_completed() { + job = Some(j.clone()); + break; + } + } + + if let Some(job) = job { + disown_job(cmd, streams, &job); + retval = STATUS_CMD_OK; + } else { + streams + .err + .append(wgettext_fmt!("%ls: There are no suitable jobs\n", cmd)); + retval = STATUS_CMD_ERROR; + } + } else { + let mut jobs = vec![]; + + retval = STATUS_CMD_OK; + + // If one argument is not a valid pid (i.e. integer >= 0), fail without disowning anything, + // but still print errors for all of them. + // Non-existent jobs aren't an error, but information about them is useful. + // Multiple PIDs may refer to the same job; include the job only once by using a set. + for arg in &args[1..] { + match fish_wcstoi(arg) + .ok() + .and_then(|pid| if pid < 0 { None } else { Some(pid) }) + { + None => { + streams.err.append(wgettext_fmt!( + "%ls: '%ls' is not a valid job specifier\n", + cmd, + arg + )); + retval = STATUS_INVALID_ARGS; + } + Some(pid) => { + if let Some(j) = parser.job_get_from_pid(pid) { + jobs.push(j); + } else { + streams + .err + .append(wgettext_fmt!("%ls: Could not find job '%d'\n")); + } + } + } + } + if retval != STATUS_CMD_OK { + return retval; + } + + // Disown all target jobs. + for j in jobs { + disown_job(cmd, streams, &j); + } + } + + retval +} diff --git a/fish-rust/src/builtins/echo.rs b/fish-rust/src/builtins/echo.rs index 86a8c8c0e..638a3b469 100644 --- a/fish-rust/src/builtins/echo.rs +++ b/fish-rust/src/builtins/echo.rs @@ -22,7 +22,7 @@ fn default() -> Self { fn parse_options( args: &mut [&wstr], - parser: &mut Parser, + parser: &Parser, streams: &mut IoStreams, ) -> Result<(Options, usize), Option> { let cmd = args[0]; @@ -135,7 +135,7 @@ fn parse_numeric_sequence(chars: I) -> Option<(usize, u8)> /// /// Bash only respects `-n` if it's the first argument. We'll do the same. We also support a new, /// fish specific, option `-s` to mean "no spaces". -pub fn echo(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { +pub fn echo(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { let (opts, optind) = match parse_options(args, parser, streams) { Ok((opts, optind)) => (opts, optind), Err(err @ Some(_)) if err != STATUS_CMD_OK => return err, diff --git a/fish-rust/src/builtins/emit.rs b/fish-rust/src/builtins/emit.rs index b7c5eed80..6d1c25a2f 100644 --- a/fish-rust/src/builtins/emit.rs +++ b/fish-rust/src/builtins/emit.rs @@ -2,7 +2,7 @@ use crate::event; #[widestrs] -pub fn emit(parser: &mut Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Option { +pub fn emit(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Option { let cmd = argv[0]; let opts = match HelpOnlyCmdOpts::parse(argv, parser, streams) { diff --git a/fish-rust/src/builtins/eval.rs b/fish-rust/src/builtins/eval.rs new file mode 100644 index 000000000..b8012434c --- /dev/null +++ b/fish-rust/src/builtins/eval.rs @@ -0,0 +1,79 @@ +//! The eval builtin. + +use super::prelude::*; +use crate::io::IoBufferfill; +use crate::parser::BlockType; +use crate::wcstringutil::join_strings; +use libc::{STDERR_FILENO, STDOUT_FILENO}; + +pub fn eval(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { + let argc = args.len(); + if argc <= 1 { + return STATUS_CMD_OK; + } + + let new_cmd = join_strings(&args[1..], ' '); + + // Copy the full io chain; we may append bufferfills. + let mut ios = unsafe { &*streams.io_chain }.clone(); + + // If stdout is piped, then its output must go to the streams, not to the io_chain in our + // streams, because the pipe may be intended to be consumed by a process which + // is not yet launched (#6806). If stdout is NOT redirected, it must see the tty (#6955). So + // create a bufferfill for stdout if and only if stdout is piped. + // Note do not do this if stdout is merely redirected (say, to a file); we don't want to + // buffer in that case. + let mut stdout_fill = None; + if streams.out_is_piped { + match IoBufferfill::create_opts(parser.libdata().pods.read_limit, STDOUT_FILENO) { + None => { + // We were unable to create a pipe, probably fd exhaustion. + return STATUS_CMD_ERROR; + } + Some(buffer) => { + stdout_fill = Some(buffer.clone()); + ios.push(buffer); + } + } + } + + // Of course the same applies to stderr. + let mut stderr_fill = None; + if streams.err_is_piped { + match IoBufferfill::create_opts(parser.libdata().pods.read_limit, STDERR_FILENO) { + None => { + // We were unable to create a pipe, probably fd exhaustion. + return STATUS_CMD_ERROR; + } + Some(buffer) => { + stderr_fill = Some(buffer.clone()); + ios.push(buffer); + } + } + } + + let res = parser.eval_with(&new_cmd, &ios, streams.job_group.as_ref(), BlockType::top); + let status = if res.was_empty { + // Issue #5692, in particular, to catch `eval ""`, `eval "begin; end;"`, etc. + // where we have an argument but nothing is executed. + STATUS_CMD_OK + } else { + Some(res.status.status_value()) + }; + + // Finish the bufferfills - exhaust and close our pipes. + // Copy the output from the bufferfill back to the streams. + // Note it is important that we hold no other references to the bufferfills here - they need to + // deallocate to close. + ios.clear(); + if let Some(stdout) = stdout_fill { + let output = IoBufferfill::finish(stdout); + streams.out.append_narrow_buffer(&output); + } + if let Some(stderr) = stderr_fill { + let errput = IoBufferfill::finish(stderr); + streams.err.append_narrow_buffer(&errput); + } + + status +} diff --git a/fish-rust/src/builtins/exit.rs b/fish-rust/src/builtins/exit.rs index 841617d3b..d9bc042de 100644 --- a/fish-rust/src/builtins/exit.rs +++ b/fish-rust/src/builtins/exit.rs @@ -2,7 +2,7 @@ use super::r#return::parse_return_value; /// Function for handling the exit builtin. -pub fn exit(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { +pub fn exit(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { let retval = match parse_return_value(args, parser, streams) { Ok(v) => v, Err(e) => return e, @@ -12,7 +12,7 @@ pub fn exit(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> // TODO: in concurrent mode this won't successfully exit a pipeline, as there are other parsers // involved. That is, `exit | sleep 1000` may not exit as hoped. Need to rationalize what // behavior we want here. - parser.libdata_pod().exit_current_script = true; + parser.libdata_mut().pods.exit_current_script = true; return Some(retval); } diff --git a/fish-rust/src/builtins/fg.rs b/fish-rust/src/builtins/fg.rs new file mode 100644 index 000000000..b9527091d --- /dev/null +++ b/fish-rust/src/builtins/fg.rs @@ -0,0 +1,177 @@ +//! Implementation of the fg builtin. + +use crate::fds::make_fd_blocking; +use crate::reader::reader_write_title; +use crate::tokenizer::tok_command; +use crate::wutil::perror; +use crate::{env::EnvMode, proc::TtyTransfer}; +use libc::{STDERR_FILENO, STDIN_FILENO, TCSADRAIN}; + +use super::prelude::*; + +/// Builtin for putting a job in the foreground. +pub fn fg(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Option { + let opts = match HelpOnlyCmdOpts::parse(argv, parser, streams) { + Ok(opts) => opts, + Err(err @ Some(_)) if err != STATUS_CMD_OK => return err, + Err(err) => panic!("Illogical exit code from parse_options(): {err:?}"), + }; + + let cmd = argv[0]; + if opts.print_help { + builtin_print_help(parser, streams, cmd); + return STATUS_CMD_OK; + } + + let job; + let job_pos; + let optind = opts.optind; + if optind == argv.len() { + // Select last constructed job (i.e. first job in the job queue) that can be brought + // to the foreground. + let jobs = parser.jobs(); + match jobs.iter().enumerate().find(|(_pos, job)| { + job.is_constructed() + && !job.is_completed() + && ((job.is_stopped() || !job.is_foreground()) && job.wants_job_control()) + }) { + None => { + streams + .err + .append(wgettext_fmt!("%ls: There are no suitable jobs\n", cmd)); + return STATUS_INVALID_ARGS; + } + Some((pos, j)) => { + job_pos = Some(pos); + job = Some(j.clone()); + } + } + } else if optind + 1 < argv.len() { + // Specifying more than one job to put to the foreground is a syntax error, we still + // try to locate the job $argv[1], since we need to determine which error message to + // emit (ambigous job specification vs malformed job id). + let mut found_job = false; + match fish_wcstoi(argv[optind]) { + Ok(pid) if pid > 0 => found_job = parser.job_get_from_pid(pid).is_some(), + _ => (), + }; + + if found_job { + streams + .err + .append(wgettext_fmt!("%ls: Ambiguous job\n", cmd)); + } else { + streams.err.append(wgettext_fmt!( + "%ls: '%ls' is not a job\n", + cmd, + argv[optind] + )); + } + + builtin_print_error_trailer(parser, streams.err, cmd); + job_pos = None; + job = None; + } else { + match fish_wcstoi(argv[optind]) { + Err(_) => { + streams + .err + .append(wgettext_fmt!(BUILTIN_ERR_NOT_NUMBER, cmd, argv[optind])); + job_pos = None; + job = None; + builtin_print_error_trailer(parser, streams.err, cmd); + } + Ok(pid) => { + let pid = pid.abs(); + let j = parser.job_get_with_index_from_pid(pid); + if j.as_ref() + .map_or(true, |(_pos, j)| !j.is_constructed() || j.is_completed()) + { + streams + .err + .append(wgettext_fmt!("%ls: No suitable job: %d\n", cmd, pid)); + job_pos = None; + job = None + } else { + let (pos, j) = j.unwrap(); + job_pos = Some(pos); + job = if !j.wants_job_control() { + streams.err.append(wgettext_fmt!( + concat!( + "%ls: Can't put job %d, '%ls' to foreground because ", + "it is not under job control\n" + ), + cmd, + pid, + j.command() + )); + None + } else { + Some(j) + }; + } + } + } + }; + + let Some(job) = job else { + return STATUS_INVALID_ARGS; + }; + let job_pos = job_pos.unwrap(); + + if streams.err_is_redirected { + streams + .err + .append(wgettext_fmt!(FG_MSG, job.job_id(), job.command())); + } else { + // If we aren't redirecting, send output to real stderr, since stuff in sb_err won't get + // printed until the command finishes. + fwprintf!( + STDERR_FILENO, + "%s", + wgettext_fmt!(FG_MSG, job.job_id(), job.command()) + ); + } + + let ft = tok_command(job.command()); + if !ft.is_empty() { + // Provide value for `status current-command` + parser.libdata_mut().status_vars.command = ft.clone(); + // Also provide a value for the deprecated fish 2.0 $_ variable + parser.set_var_and_fire(L!("_"), EnvMode::EXPORT, vec![ft]); + // Provide value for `status current-commandline` + parser.libdata_mut().status_vars.commandline = job.command().to_owned(); + } + reader_write_title(job.command(), parser, true); + + // Note if tty transfer fails, we still try running the job. + parser.job_promote_at(job_pos); + let _ = make_fd_blocking(STDIN_FILENO); + { + let job_group = job.group(); + job_group.set_is_foreground(true); + let tmodes = job_group.tmodes.borrow(); + if job_group.wants_terminal() && tmodes.is_some() { + let termios = tmodes.as_ref().unwrap(); + let res = unsafe { libc::tcsetattr(STDIN_FILENO, TCSADRAIN, termios) }; + if res < 0 { + perror("tcsetattr"); + } + } + } + let mut transfer = TtyTransfer::new(); + transfer.to_job_group(job.group.as_ref().unwrap()); + let resumed = job.resume(); + if resumed { + job.continue_job(parser); + } + if job.is_stopped() { + transfer.save_tty_modes(); + } + transfer.reclaim(); + if resumed { + STATUS_CMD_OK + } else { + STATUS_CMD_ERROR + } +} diff --git a/fish-rust/src/builtins/function.rs b/fish-rust/src/builtins/function.rs index e889a004b..9eb8c8d7d 100644 --- a/fish-rust/src/builtins/function.rs +++ b/fish-rust/src/builtins/function.rs @@ -1,17 +1,16 @@ use super::prelude::*; use crate::ast::BlockStatement; use crate::common::{valid_func_name, valid_var_name}; +use crate::complete::complete_add_wrapper; use crate::env::environment::Environment; use crate::event::{self, EventDescription, EventHandler}; -use crate::ffi::IoStreams as io_streams_ffi_t; use crate::function; use crate::global_safety::RelaxedAtomicBool; +use crate::io::IoStreams; use crate::parse_tree::NodeRef; -use crate::parse_tree::ParsedSourceRefFFI; +use crate::parser::Parser; use crate::parser_keywords::parser_keywords_is_reserved; use crate::signal::Signal; -use crate::wchar_ffi::{wcstring_list_ffi_t, WCharFromFFI, WCharToFFI}; -use std::pin::Pin; use std::sync::Arc; struct FunctionCmdOpts { @@ -60,7 +59,7 @@ fn default() -> Self { /// This looks through both active and finished jobs. fn job_id_for_pid(pid: i32, parser: &Parser) -> Option { if let Some(job) = parser.job_get_from_pid(pid) { - Some(job.get_internal_job_id()) + Some(job.internal_job_id) } else { parser .get_wait_handles() @@ -75,7 +74,7 @@ fn parse_cmd_opts( opts: &mut FunctionCmdOpts, optind: &mut usize, argv: &mut [&wstr], - parser: &mut Parser, + parser: &Parser, streams: &mut IoStreams, ) -> Option { let cmd = L!("function"); @@ -134,9 +133,9 @@ fn parse_cmd_opts( let woptarg = w.woptarg.unwrap(); let e: EventDescription; if opt == 'j' && woptarg == "caller" { - let libdata = parser.ffi_libdata_pod_const(); - let caller_id = if libdata.is_subshell { - libdata.caller_id + let libdata = parser.libdata(); + let caller_id = if libdata.pods.is_subshell { + libdata.pods.caller_id } else { 0 }; @@ -252,7 +251,7 @@ fn validate_function_name( /// function. Note this isn't strictly a "builtin": it is called directly from parse_execution. /// That is why its signature is different from the other builtins. pub fn function( - parser: &mut Parser, + parser: &Parser, streams: &mut IoStreams, c_args: &mut [&wstr], func_node: NodeRef, @@ -281,7 +280,7 @@ pub fn function( } if opts.print_help { - builtin_print_error_trailer(parser, streams, cmd); + builtin_print_error_trailer(parser, streams.err, cmd); return STATUS_CMD_OK; } @@ -308,8 +307,7 @@ pub fn function( } // Extract the current filename. - let definition_file = unsafe { parser.pin().libdata().get_current_filename().as_ref() } - .map(|s| Arc::new(s.from_ffi())); + let definition_file = parser.libdata().current_filename.clone(); // Ensure inherit_vars is unique and then populate it. opts.inherit_vars.sort_unstable(); @@ -319,7 +317,7 @@ pub fn function( .inherit_vars .into_iter() .filter_map(|name| { - let vals = parser.get_vars().get(&name)?.as_list().to_vec(); + let vals = parser.vars().get(&name)?.as_list().to_vec(); Some((name, vals)) }) .collect(); @@ -343,7 +341,7 @@ pub fn function( // Handle wrap targets by creating the appropriate completions. for wt in opts.wrap_targets.into_iter() { - ffi::complete_add_wrapper(&function_name.to_ffi(), &wt.to_ffi()); + complete_add_wrapper(function_name.clone(), wt.clone()); } // Add any event handlers. @@ -375,56 +373,3 @@ pub fn function( STATUS_CMD_OK } - -fn builtin_function_ffi( - parser: Pin<&mut Parser>, - streams: Pin<&mut io_streams_ffi_t>, - c_args: &wcstring_list_ffi_t, - source_u8: *const u8, // unowned ParsedSourceRefFFI - func_node: &BlockStatement, -) -> i32 { - let storage = c_args.from_ffi(); - let mut args = truncate_args_on_nul(&storage); - let node = unsafe { - let source_ref: &ParsedSourceRefFFI = &*(source_u8.cast()); - NodeRef::from_parts( - source_ref - .0 - .as_ref() - .expect("Should have parsed source") - .clone(), - func_node, - ) - }; - function( - parser.unpin(), - &mut IoStreams::new(streams), - args.as_mut_slice(), - node, - ) - .expect("function builtin should always return a non-None status") -} - -#[cxx::bridge] -mod builtin_function { - extern "C++" { - include!("ast.h"); - include!("parser.h"); - include!("io.h"); - type Parser = crate::ffi::Parser; - type IoStreams = crate::ffi::IoStreams; - type wcstring_list_ffi_t = crate::ffi::wcstring_list_ffi_t; - - type BlockStatement = crate::ast::BlockStatement; - } - - extern "Rust" { - fn builtin_function_ffi( - parser: Pin<&mut Parser>, - streams: Pin<&mut IoStreams>, - c_args: &wcstring_list_ffi_t, - source: *const u8, // unowned ParsedSourceRefFFI - func_node: &BlockStatement, - ) -> i32; - } -} diff --git a/fish-rust/src/builtins/functions.rs b/fish-rust/src/builtins/functions.rs index 2b3c278a0..7f19b013d 100644 --- a/fish-rust/src/builtins/functions.rs +++ b/fish-rust/src/builtins/functions.rs @@ -1,11 +1,14 @@ use super::prelude::*; use crate::common::escape_string; use crate::common::reformat_for_screen; +use crate::common::str2wcstring; use crate::common::valid_func_name; use crate::common::{EscapeFlags, EscapeStringStyle}; use crate::event::{self}; -use crate::ffi::colorize_shell; use crate::function; +use crate::highlight::colorize; +use crate::highlight::highlight_shell; +use crate::parser::Parser; use crate::parser_keywords::parser_keywords_is_reserved; use crate::termsize::termsize_last; @@ -68,7 +71,7 @@ fn parse_cmd_opts<'args>( opts: &mut FunctionsCmdOpts<'args>, optind: &mut usize, argv: &mut [&'args wstr], - parser: &mut Parser, + parser: &Parser, streams: &mut IoStreams, ) -> Option { let cmd = L!("functions"); @@ -111,11 +114,7 @@ fn parse_cmd_opts<'args>( STATUS_CMD_OK } -pub fn functions( - parser: &mut Parser, - streams: &mut IoStreams, - args: &mut [&wstr], -) -> Option { +pub fn functions(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { let cmd = args[0]; let mut opts = FunctionsCmdOpts::default(); @@ -128,7 +127,7 @@ pub fn functions( let args = &args[optind..]; if opts.print_help { - builtin_print_error_trailer(parser, streams, cmd); + builtin_print_error_trailer(parser, streams.err, cmd); return STATUS_CMD_OK; } @@ -140,13 +139,13 @@ pub fn functions( > 1 { streams.err.append(wgettext_fmt!(BUILTIN_ERR_COMBO, cmd)); - builtin_print_error_trailer(parser, streams, cmd); + builtin_print_error_trailer(parser, streams.err, cmd); return STATUS_INVALID_ARGS; } if opts.report_metadata && opts.no_metadata { streams.err.append(wgettext_fmt!(BUILTIN_ERR_COMBO, cmd)); - builtin_print_error_trailer(parser, streams, cmd); + builtin_print_error_trailer(parser, streams.err, cmd); return STATUS_INVALID_ARGS; } @@ -164,7 +163,7 @@ pub fn functions( "%ls: Expected exactly one function name\n", cmd )); - builtin_print_error_trailer(parser, streams, cmd); + builtin_print_error_trailer(parser, streams.err, cmd); return STATUS_INVALID_ARGS; } let current_func = args[0]; @@ -175,7 +174,7 @@ pub fn functions( cmd, current_func )); - builtin_print_error_trailer(parser, streams, cmd); + builtin_print_error_trailer(parser, streams.err, cmd); return STATUS_CMD_ERROR; } @@ -304,7 +303,7 @@ pub fn functions( "%ls: Expected exactly two names (current function name, and new function name)\n", cmd )); - builtin_print_error_trailer(parser, streams, cmd); + builtin_print_error_trailer(parser, streams.err, cmd); return STATUS_INVALID_ARGS; } let current_func = args[0]; @@ -316,7 +315,7 @@ pub fn functions( cmd, current_func )); - builtin_print_error_trailer(parser, streams, cmd); + builtin_print_error_trailer(parser, streams.err, cmd); return STATUS_CMD_ERROR; } @@ -326,7 +325,7 @@ pub fn functions( cmd, new_func )); - builtin_print_error_trailer(parser, streams, cmd); + builtin_print_error_trailer(parser, streams.err, cmd); return STATUS_INVALID_ARGS; } @@ -337,7 +336,7 @@ pub fn functions( new_func, current_func )); - builtin_print_error_trailer(parser, streams, cmd); + builtin_print_error_trailer(parser, streams.err, cmd); return STATUS_CMD_ERROR; } if function::copy(current_func, new_func.into(), parser) { @@ -410,8 +409,11 @@ pub fn functions( } if streams.out_is_terminal() { - let col = colorize_shell(&def.to_ffi(), parser.pin()).from_ffi(); - streams.out.append(col); + let mut colors = vec![]; + highlight_shell(&def, &mut colors, &parser.context(), false, None); + streams + .out + .append(str2wcstring(&colorize(&def, &colors, parser.vars()))); } else { streams.out.append(def); } diff --git a/fish-rust/src/builtins/history.rs b/fish-rust/src/builtins/history.rs new file mode 100644 index 000000000..e954c5a7e --- /dev/null +++ b/fish-rust/src/builtins/history.rs @@ -0,0 +1,362 @@ +//! Implementation of the history builtin. + +use crate::ffi::{self}; +use crate::history::{self, history_session_id, History}; +use crate::history::{in_private_mode, HistorySharedPtr}; + +use super::prelude::*; + +#[derive(Default, Eq, PartialEq)] +enum HistCmd { + HIST_SEARCH = 1, + HIST_DELETE, + HIST_CLEAR, + HIST_MERGE, + HIST_SAVE, + #[default] + HIST_UNDEF, + HIST_CLEAR_SESSION, +} + +impl HistCmd { + fn to_wstr(&self) -> &'static wstr { + match self { + HistCmd::HIST_SEARCH => L!("search"), + HistCmd::HIST_DELETE => L!("delete"), + HistCmd::HIST_CLEAR => L!("clear"), + HistCmd::HIST_MERGE => L!("merge"), + HistCmd::HIST_SAVE => L!("save"), + HistCmd::HIST_UNDEF => panic!(), + HistCmd::HIST_CLEAR_SESSION => L!("clear-session"), + } + } +} + +impl TryFrom<&wstr> for HistCmd { + type Error = (); + fn try_from(val: &wstr) -> Result { + match val { + _ if val == "search" => Ok(HistCmd::HIST_SEARCH), + _ if val == "delete" => Ok(HistCmd::HIST_DELETE), + _ if val == "clear" => Ok(HistCmd::HIST_CLEAR), + _ if val == "merge" => Ok(HistCmd::HIST_MERGE), + _ if val == "save" => Ok(HistCmd::HIST_SAVE), + _ if val == "clear-session" => Ok(HistCmd::HIST_CLEAR_SESSION), + _ => Err(()), + } + } +} + +#[derive(Default)] +struct HistoryCmdOpts { + hist_cmd: HistCmd, + search_type: Option, + show_time_format: Option, + max_items: Option, + print_help: bool, + case_sensitive: bool, + null_terminate: bool, + reverse: bool, +} + +/// Note: Do not add new flags that represent subcommands. We're encouraging people to switch to +/// the non-flag subcommand form. While many of these flags are deprecated they must be +/// supported at least until fish 3.0 and possibly longer to avoid breaking everyones +/// config.fish and other scripts. +const short_options: &wstr = L!(":CRcehmn:pt::z"); +const longopts: &[woption] = &[ + wopt(L!("prefix"), woption_argument_t::no_argument, 'p'), + wopt(L!("contains"), woption_argument_t::no_argument, 'c'), + wopt(L!("help"), woption_argument_t::no_argument, 'h'), + wopt(L!("show-time"), woption_argument_t::optional_argument, 't'), + wopt(L!("exact"), woption_argument_t::no_argument, 'e'), + wopt(L!("max"), woption_argument_t::required_argument, 'n'), + wopt(L!("null"), woption_argument_t::no_argument, 'z'), + wopt(L!("case-sensitive"), woption_argument_t::no_argument, 'C'), + wopt(L!("delete"), woption_argument_t::no_argument, '\x01'), + wopt(L!("search"), woption_argument_t::no_argument, '\x02'), + wopt(L!("save"), woption_argument_t::no_argument, '\x03'), + wopt(L!("clear"), woption_argument_t::no_argument, '\x04'), + wopt(L!("merge"), woption_argument_t::no_argument, '\x05'), + wopt(L!("reverse"), woption_argument_t::no_argument, 'R'), +]; + +/// Remember the history subcommand and disallow selecting more than one history subcommand. +fn set_hist_cmd( + cmd: &wstr, + hist_cmd: &mut HistCmd, + sub_cmd: HistCmd, + streams: &mut IoStreams, +) -> bool { + if *hist_cmd != HistCmd::HIST_UNDEF { + streams.err.append(wgettext_fmt!( + BUILTIN_ERR_COMBO2_EXCLUSIVE, + cmd, + hist_cmd.to_wstr(), + sub_cmd.to_wstr() + )); + return false; + } + *hist_cmd = sub_cmd; + true +} + +fn check_for_unexpected_hist_args( + opts: &HistoryCmdOpts, + cmd: &wstr, + args: &[&wstr], + streams: &mut IoStreams, +) -> bool { + if opts.search_type.is_some() || opts.show_time_format.is_some() || opts.null_terminate { + let subcmd_str = opts.hist_cmd.to_wstr(); + streams.err.append(wgettext_fmt!( + "%ls: %ls: subcommand takes no options\n", + cmd, + subcmd_str + )); + return true; + } + if !args.is_empty() { + let subcmd_str = opts.hist_cmd.to_wstr(); + streams.err.append(wgettext_fmt!( + BUILTIN_ERR_ARG_COUNT2, + cmd, + subcmd_str, + 0, + args.len() + )); + return true; + } + false +} + +fn parse_cmd_opts( + opts: &mut HistoryCmdOpts, + optind: &mut usize, + argv: &mut [&wstr], + parser: &Parser, + streams: &mut IoStreams, +) -> Option { + let cmd = argv[0]; + let mut w = wgetopter_t::new(short_options, longopts, argv); + while let Some(opt) = w.wgetopt_long() { + match opt { + '\x01' => { + if !set_hist_cmd(cmd, &mut opts.hist_cmd, HistCmd::HIST_DELETE, streams) { + return STATUS_CMD_ERROR; + } + } + '\x02' => { + if !set_hist_cmd(cmd, &mut opts.hist_cmd, HistCmd::HIST_SEARCH, streams) { + return STATUS_CMD_ERROR; + } + } + '\x03' => { + if !set_hist_cmd(cmd, &mut opts.hist_cmd, HistCmd::HIST_SAVE, streams) { + return STATUS_CMD_ERROR; + } + } + '\x04' => { + if !set_hist_cmd(cmd, &mut opts.hist_cmd, HistCmd::HIST_CLEAR, streams) { + return STATUS_CMD_ERROR; + } + } + '\x05' => { + if !set_hist_cmd(cmd, &mut opts.hist_cmd, HistCmd::HIST_MERGE, streams) { + return STATUS_CMD_ERROR; + } + } + 'C' => { + opts.case_sensitive = true; + } + 'R' => { + opts.reverse = true; + } + 'p' => { + opts.search_type = Some(history::SearchType::PrefixGlob); + } + 'c' => { + opts.search_type = Some(history::SearchType::ContainsGlob); + } + 'e' => { + opts.search_type = Some(history::SearchType::Exact); + } + 't' => { + opts.show_time_format = Some(w.woptarg.unwrap_or(L!("# %c%n")).to_string()); + } + 'n' => match fish_wcstol(w.woptarg.unwrap()) { + Ok(x) => opts.max_items = Some(x as _), // todo!("historical behavior is to cast") + Err(_) => { + streams.err.append(wgettext_fmt!( + BUILTIN_ERR_NOT_NUMBER, + cmd, + w.woptarg.unwrap() + )); + return STATUS_INVALID_ARGS; + } + }, + 'z' => { + opts.null_terminate = true; + } + 'h' => { + opts.print_help = true; + } + ':' => { + builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1], true); + return STATUS_INVALID_ARGS; + } + '?' => { + // Try to parse it as a number; e.g., "-123". + match fish_wcstol(&w.argv[w.woptind - 1][1..]) { + Ok(x) => opts.max_items = Some(x as _), // todo!("historical behavior is to cast") + Err(_) => { + builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1], true); + return STATUS_INVALID_ARGS; + } + } + w.nextchar = L!(""); + } + _ => { + panic!("unexpected retval from wgetopt_long"); + } + } + } + + *optind = w.woptind; + STATUS_CMD_OK +} + +/// Manipulate history of interactive commands executed by the user. +pub fn history(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { + let mut opts = HistoryCmdOpts::default(); + let mut optind = 0; + let retval = parse_cmd_opts(&mut opts, &mut optind, args, parser, streams); + if retval != STATUS_CMD_OK { + return retval; + } + let cmd = &args[0]; + + if opts.print_help { + builtin_print_help(parser, streams, cmd); + return STATUS_CMD_OK; + } + + // Use the default history if we have none (which happens if invoked non-interactively, e.g. + // from webconfig.py. + let history = ffi::commandline_get_state_history_ffi(); + let history = if history.is_null() { + History::with_name(&history_session_id(parser.vars())) + } else { + { + *unsafe { + Box::from_raw(ffi::commandline_get_state_history_ffi() as *mut HistorySharedPtr) + } + } + .0 + }; + + // If a history command hasn't already been specified via a flag check the first word. + // Note that this can be simplified after we eliminate allowing subcommands as flags. + // See the TODO above regarding the `long_options` array. + if optind < args.len() { + if let Ok(subcmd) = HistCmd::try_from(args[optind]) { + if !set_hist_cmd(cmd, &mut opts.hist_cmd, subcmd, streams) { + return STATUS_INVALID_ARGS; + } + optind += 1; + } + } + + // Every argument that we haven't consumed already is an argument for a subcommand (e.g., a + // search term). + let args = &args[optind..]; + + // Establish appropriate defaults. + if opts.hist_cmd == HistCmd::HIST_UNDEF { + opts.hist_cmd = HistCmd::HIST_SEARCH; + } + if opts.search_type.is_none() { + if opts.hist_cmd == HistCmd::HIST_SEARCH { + opts.search_type = Some(history::SearchType::ContainsGlob); + } + if opts.hist_cmd == HistCmd::HIST_DELETE { + opts.search_type = Some(history::SearchType::Exact); + } + } + + let mut status = STATUS_CMD_OK; + match opts.hist_cmd { + HistCmd::HIST_SEARCH => { + if !history.search( + opts.search_type.unwrap(), + args, + opts.show_time_format.as_deref(), + opts.max_items.unwrap_or(usize::MAX), + opts.case_sensitive, + opts.null_terminate, + opts.reverse, + &parser.context().cancel_checker, + streams, + ) { + status = STATUS_CMD_ERROR; + } + } + HistCmd::HIST_DELETE => { + // TODO: Move this code to the history module and support the other search types + // including case-insensitive matches. At this time we expect the non-exact deletions to + // be handled only by the history function's interactive delete feature. + if opts.search_type.unwrap() != history::SearchType::Exact { + streams + .err + .append(wgettext!("builtin history delete only supports --exact\n")); + return STATUS_INVALID_ARGS; + } + if !opts.case_sensitive { + streams.err.append(wgettext!( + "builtin history delete --exact requires --case-sensitive\n" + )); + return STATUS_INVALID_ARGS; + } + for delete_string in args.iter().copied() { + history.remove(delete_string.to_owned()); + } + } + HistCmd::HIST_CLEAR => { + if check_for_unexpected_hist_args(&opts, cmd, args, streams) { + return STATUS_INVALID_ARGS; + } + history.clear(); + history.save(); + } + HistCmd::HIST_CLEAR_SESSION => { + if check_for_unexpected_hist_args(&opts, cmd, args, streams) { + return STATUS_INVALID_ARGS; + } + history.clear_session(); + history.save(); + } + HistCmd::HIST_MERGE => { + if check_for_unexpected_hist_args(&opts, cmd, args, streams) { + return STATUS_INVALID_ARGS; + } + + if in_private_mode(parser.vars()) { + streams.err.append(wgettext_fmt!( + "%ls: can't merge history in private mode\n", + cmd + )); + return STATUS_INVALID_ARGS; + } + history.incorporate_external_changes(); + } + HistCmd::HIST_SAVE => { + if check_for_unexpected_hist_args(&opts, cmd, args, streams) { + return STATUS_INVALID_ARGS; + } + history.save(); + } + HistCmd::HIST_UNDEF => panic!("Unexpected HIST_UNDEF seen"), + } + + status +} diff --git a/fish-rust/src/builtins/jobs.rs b/fish-rust/src/builtins/jobs.rs new file mode 100644 index 000000000..5c99025ca --- /dev/null +++ b/fish-rust/src/builtins/jobs.rs @@ -0,0 +1,267 @@ +// Functions for executing the jobs builtin. + +use super::shared::{ + builtin_missing_argument, builtin_print_help, builtin_unknown_option, STATUS_CMD_ERROR, + STATUS_INVALID_ARGS, +}; +use crate::common::{escape_string, timef, EscapeFlags, EscapeStringStyle}; +use crate::io::IoStreams; +use crate::job_group::{JobId, MaybeJobId}; +use crate::parser::Parser; +use crate::proc::{clock_ticks_to_seconds, have_proc_stat, proc_get_jiffies, Job, INVALID_PID}; +use crate::wchar_ext::WExt; +use crate::wgetopt::{wgetopter_t, wopt, woption, woption_argument_t}; +use crate::wutil::wgettext; +use crate::{ + builtins::shared::STATUS_CMD_OK, + wchar::{wstr, WString, L}, + wutil::{fish_wcstoi, wgettext_fmt}, +}; +use libc::c_int; +use libc::{self}; +use printf_compat::sprintf; +use std::num::NonZeroU32; +use std::sync::atomic::Ordering; + +/// Print modes for the jobs builtin. + +#[derive(Clone, Copy, Eq, PartialEq)] +enum JobsPrintMode { + Default, // print lots of general info + PrintPid, // print pid of each process in job + PrintCommand, // print command name of each process in job + PrintGroup, // print group id of job + PrintNothing, // print nothing (exit status only) +} + +/// Calculates the cpu usage (as a fraction of 1) of the specified job. +/// This may exceed 1 if there are multiple CPUs! +fn cpu_use(j: &Job) -> f64 { + let mut u = 0.0; + for p in j.processes() { + let now = timef(); + let jiffies = proc_get_jiffies(p.pid.load(Ordering::Relaxed)); + let last_jiffies = p.last_times.get().jiffies; + let since = now - last_jiffies as f64; + if since > 0.0 && jiffies > last_jiffies { + u += clock_ticks_to_seconds(jiffies - last_jiffies) / since; + } + } + u +} + +/// Print information about the specified job. +fn builtin_jobs_print(j: &Job, mode: JobsPrintMode, header: bool, streams: &mut IoStreams) { + let mut pgid = INVALID_PID; + { + if let Some(job_pgid) = j.get_pgid() { + pgid = job_pgid; + } + } + + let mut out = WString::new(); + match mode { + JobsPrintMode::PrintNothing => (), + JobsPrintMode::Default => { + if header { + // Print table header before first job. + out += wgettext!("Job\tGroup\t"); + if have_proc_stat() { + out += wgettext!("CPU\t"); + } + out += wgettext!("State\tCommand\n"); + } + + sprintf!(=> &mut out, "%d\t%d\t", j.job_id(), pgid); + + if have_proc_stat() { + sprintf!(=> &mut out, "%.0f%%\t", 100.0 * cpu_use(j)); + } + + out += if j.is_stopped() { + wgettext!("stopped") + } else { + wgettext!("running") + }; + out += "\t"; + + let cmd = escape_string( + j.command(), + EscapeStringStyle::Script(EscapeFlags::NO_PRINTABLES), + ); + out += &cmd[..]; + + out += "\n"; + streams.out.append(out); + } + JobsPrintMode::PrintGroup => { + if header { + // Print table header before first job. + out += &wgettext!("Group\n")[..]; + } + out += &sprintf!("%d\n", pgid)[..]; + streams.out.append(out); + } + JobsPrintMode::PrintPid => { + if header { + // Print table header before first job. + out += &wgettext!("Process\n")[..]; + } + + for p in j.processes() { + out += &sprintf!("%d\n", p.pid.load(Ordering::Relaxed))[..]; + } + streams.out.append(out); + } + JobsPrintMode::PrintCommand => { + if header { + // Print table header before first job. + out += &wgettext!("Command\n")[..]; + } + + for p in j.processes() { + out += &sprintf!("%ls\n", p.argv0().unwrap())[..]; + } + streams.out.append(out); + } + }; +} + +const SHORT_OPTIONS: &wstr = L!(":cghlpq"); +const LONG_OPTIONS: &[woption] = &[ + wopt(L!("command"), woption_argument_t::no_argument, 'c'), + wopt(L!("group"), woption_argument_t::no_argument, 'g'), + wopt(L!("help"), woption_argument_t::no_argument, 'h'), + wopt(L!("last"), woption_argument_t::no_argument, 'l'), + wopt(L!("pid"), woption_argument_t::no_argument, 'p'), + wopt(L!("quiet"), woption_argument_t::no_argument, 'q'), + wopt(L!("query"), woption_argument_t::no_argument, 'q'), +]; + +/// The jobs builtin. Used for printing running jobs. Defined in builtin_jobs.c. +pub fn jobs(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Option { + let cmd = argv[0]; + let argc = argv.len(); + let mut found = false; + let mut mode = JobsPrintMode::Default; + let mut print_last = false; + + let mut w = wgetopter_t::new(SHORT_OPTIONS, LONG_OPTIONS, argv); + while let Some(c) = w.wgetopt_long() { + match c { + 'p' => { + mode = JobsPrintMode::PrintPid; + } + 'q' => { + mode = JobsPrintMode::PrintNothing; + } + 'c' => { + mode = JobsPrintMode::PrintCommand; + } + 'g' => { + mode = JobsPrintMode::PrintGroup; + } + 'l' => { + print_last = true; + } + 'h' => { + builtin_print_help(parser, streams, cmd); + return STATUS_CMD_OK; + } + ':' => { + builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1], true); + return STATUS_INVALID_ARGS; + } + '?' => { + builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1], true); + return STATUS_INVALID_ARGS; + } + _ => panic!("unexpected retval from wgetopt_long"), + } + } + + if print_last { + // Ignore unconstructed jobs, i.e. ourself. + for j in &parser.jobs()[..] { + if j.is_visible() { + builtin_jobs_print(j, mode, !streams.out_is_redirected, streams); + return STATUS_CMD_OK; + } + } + return STATUS_CMD_ERROR; + } + + if w.woptind < argc { + for arg in &w.argv[w.woptind..] { + let j; + if arg.char_at(0) == '%' { + match fish_wcstoi(&arg[1..]).ok().filter(|&job_id| job_id >= 0) { + None => { + streams.err.append(wgettext_fmt!( + "%ls: '%ls' is not a valid job id\n", + cmd, + arg + )); + return STATUS_INVALID_ARGS; + } + Some(job_id) => { + let job_id = if job_id == 0 { + JobId::NONE + } else { + let job_id = u32::try_from(job_id).unwrap(); + let job_id = NonZeroU32::try_from(job_id).unwrap(); + MaybeJobId(Some(JobId::new(job_id))) + }; + j = parser.job_with_id(job_id); + } + } + } else { + match fish_wcstoi(arg).ok().filter(|&pid| pid >= 0) { + None => { + streams.err.append(wgettext_fmt!( + "%ls: '%ls' is not a valid process id\n", + cmd, + arg + )); + return STATUS_INVALID_ARGS; + } + Some(job_id) => { + j = parser.job_get_from_pid(job_id); + } + } + } + + if let Some(j) = j.filter(|j| !j.is_completed() && j.is_constructed()) { + builtin_jobs_print(&j, mode, false, streams); + found = true; + } else { + if mode != JobsPrintMode::PrintNothing { + streams + .err + .append(wgettext_fmt!("%ls: No suitable job: %ls\n", cmd, arg)); + } + return STATUS_CMD_ERROR; + } + } + } else { + for j in &parser.jobs()[..] { + // Ignore unconstructed jobs, i.e. ourself. + if j.is_visible() { + builtin_jobs_print(j, mode, !found && !streams.out_is_redirected, streams); + found = true; + } + } + } + + if !found { + // Do not babble if not interactive. + if !streams.out_is_redirected && mode != JobsPrintMode::PrintNothing { + streams + .out + .append(wgettext_fmt!("%ls: There are no jobs\n", argv[0])); + } + return STATUS_CMD_ERROR; + } + + STATUS_CMD_OK +} diff --git a/fish-rust/src/builtins/math.rs b/fish-rust/src/builtins/math.rs index d6a55cbca..df556748e 100644 --- a/fish-rust/src/builtins/math.rs +++ b/fish-rust/src/builtins/math.rs @@ -17,7 +17,7 @@ struct Options { #[widestrs] fn parse_cmd_opts( args: &mut [&wstr], - parser: &mut Parser, + parser: &Parser, streams: &mut IoStreams, ) -> Result<(Options, usize), Option> { const cmd: &wstr = "math"L; @@ -219,7 +219,7 @@ fn evaluate_expression( /// The math builtin evaluates math expressions. #[widestrs] -pub fn math(parser: &mut Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Option { +pub fn math(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Option { let cmd = argv[0]; let (opts, mut optind) = match parse_cmd_opts(argv, parser, streams) { diff --git a/fish-rust/src/builtins/mod.rs b/fish-rust/src/builtins/mod.rs index 79c2bbf71..352a4ccd1 100644 --- a/fish-rust/src/builtins/mod.rs +++ b/fish-rust/src/builtins/mod.rs @@ -3,29 +3,41 @@ pub mod abbr; pub mod argparse; pub mod bg; +pub mod bind; pub mod block; pub mod builtin; pub mod cd; pub mod command; +pub mod commandline; +pub mod complete; pub mod contains; pub mod count; +pub mod disown; pub mod echo; pub mod emit; +pub mod eval; pub mod exit; +pub mod fg; pub mod function; pub mod functions; +pub mod history; +pub mod jobs; pub mod math; pub mod path; pub mod printf; pub mod pwd; pub mod random; +pub mod read; pub mod realpath; pub mod r#return; +pub mod set; pub mod set_color; +pub mod source; pub mod status; pub mod string; pub mod test; pub mod r#type; +pub mod ulimit; pub mod wait; // Note these tests will NOT run with cfg(test). @@ -34,9 +46,13 @@ mod prelude { pub use super::shared::*; pub use libc::c_int; + pub use std::borrow::Cow; + #[allow(unused_imports)] pub(crate) use crate::{ - ffi::{self, separation_type_t, Parser, Repin}, + flog::{FLOG, FLOGF}, + io::{IoStreams, SeparationType}, + parser::Parser, wchar::prelude::*, wchar_ffi::{c_str, AsWstr, WCharFromFFI, WCharToFFI}, wgetopt::{ diff --git a/fish-rust/src/builtins/path.rs b/fish-rust/src/builtins/path.rs index 4cd243e48..8af1009d9 100644 --- a/fish-rust/src/builtins/path.rs +++ b/fish-rust/src/builtins/path.rs @@ -28,9 +28,9 @@ macro_rules! path_error { }; } -fn path_unknown_option(parser: &mut Parser, streams: &mut IoStreams, subcmd: &wstr, opt: &wstr) { +fn path_unknown_option(parser: &Parser, streams: &mut IoStreams, subcmd: &wstr, opt: &wstr) { path_error!(streams, BUILTIN_ERR_UNKNOWN, subcmd, opt); - builtin_print_error_trailer(parser, streams, L!("path")); + builtin_print_error_trailer(parser, streams.err, L!("path")); } // How many bytes we read() at once. @@ -161,7 +161,7 @@ fn path_out(streams: &mut IoStreams, opts: &Options<'_>, s: impl AsRef) { if !opts.null_out { streams .out - .append_with_separation(s, separation_type_t::explicitly, true); + .append_with_separation(s, SeparationType::explicitly, true); } else { let mut output = WString::with_capacity(s.len() + 1); output.push_utfstr(s); @@ -221,7 +221,7 @@ fn parse_opts<'args>( optind: &mut usize, n_req_args: usize, args: &mut [&'args wstr], - parser: &mut Parser, + parser: &Parser, streams: &mut IoStreams, ) -> Option { let cmd = args[0]; @@ -352,7 +352,7 @@ fn parse_opts<'args>( } // At this point we should not have optional args and be reading args from stdin. - if streams.stdin_is_directly_redirected() && args.len() > *optind { + if streams.stdin_is_directly_redirected && args.len() > *optind { path_error!(streams, BUILTIN_ERR_TOO_MANY_ARGUMENTS, cmd); return STATUS_INVALID_ARGS; } @@ -361,7 +361,7 @@ fn parse_opts<'args>( } fn path_transform( - parser: &mut Parser, + parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr], func: impl Fn(&wstr) -> WString, @@ -402,15 +402,11 @@ fn path_transform( } } -fn path_basename( - parser: &mut Parser, - streams: &mut IoStreams, - args: &mut [&wstr], -) -> Option { +fn path_basename(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { path_transform(parser, streams, args, |s| wbasename(s).to_owned()) } -fn path_dirname(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { +fn path_dirname(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { path_transform(parser, streams, args, |s| wdirname(s).to_owned()) } @@ -422,15 +418,11 @@ fn normalize_help(path: &wstr) -> WString { np } -fn path_normalize( - parser: &mut Parser, - streams: &mut IoStreams, - args: &mut [&wstr], -) -> Option { +fn path_normalize(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { path_transform(parser, streams, args, normalize_help) } -fn path_mtime(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { +fn path_mtime(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { let mut opts = Options::default(); opts.relative_valid = true; let mut optind = 0; @@ -514,11 +506,7 @@ fn test_find_extension() { } } -fn path_extension( - parser: &mut Parser, - streams: &mut IoStreams, - args: &mut [&wstr], -) -> Option { +fn path_extension(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { let mut opts = Options::default(); let mut optind = 0; let retval = parse_opts(&mut opts, &mut optind, 0, args, parser, streams); @@ -556,7 +544,7 @@ fn path_extension( } fn path_change_extension( - parser: &mut Parser, + parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr], ) -> Option { @@ -604,7 +592,7 @@ fn path_change_extension( } } -fn path_resolve(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { +fn path_resolve(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { let mut opts = Options::default(); let mut optind = 0; let retval = parse_opts(&mut opts, &mut optind, 0, args, parser, streams); @@ -626,7 +614,7 @@ fn path_resolve(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr] let mut next = arg.into_owned(); // First add $PWD if we're relative if !next.is_empty() && next.char_at(0) != '/' { - next = path_apply_working_directory(&next, &parser.get_vars().get_pwd_slash()); + next = path_apply_working_directory(&next, &parser.vars().get_pwd_slash()); } let mut rest = wbasename(&next).to_owned(); let mut real = None; @@ -669,7 +657,7 @@ fn path_resolve(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr] } } -fn path_sort(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { +fn path_sort(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { let mut opts = Options::default(); opts.reverse_valid = true; opts.unique_valid = true; @@ -835,7 +823,7 @@ fn filter_path(opts: &Options, path: &wstr) -> bool { } fn path_filter_maybe_is( - parser: &mut Parser, + parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr], is_is: bool, @@ -894,16 +882,16 @@ fn path_filter_maybe_is( } } -fn path_filter(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { +fn path_filter(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { path_filter_maybe_is(parser, streams, args, false) } -fn path_is(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { +fn path_is(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { path_filter_maybe_is(parser, streams, args, true) } /// The path builtin, for handling paths. -pub fn path(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { +pub fn path(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { let cmd = args[0]; let argc = args.len(); @@ -911,7 +899,7 @@ pub fn path(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> streams .err .append(wgettext_fmt!(BUILTIN_ERR_MISSING_SUBCMD, cmd)); - builtin_print_error_trailer(parser, streams, cmd); + builtin_print_error_trailer(parser, streams.err, cmd); return STATUS_INVALID_ARGS; } @@ -937,7 +925,7 @@ pub fn path(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> streams .err .append(wgettext_fmt!(BUILTIN_ERR_INVALID_SUBCMD, cmd, subcmd_name)); - builtin_print_error_trailer(parser, streams, cmd); + builtin_print_error_trailer(parser, streams.err, cmd); return STATUS_INVALID_ARGS; } }; diff --git a/fish-rust/src/builtins/printf.rs b/fish-rust/src/builtins/printf.rs index a87c4e7fa..27250e470 100644 --- a/fish-rust/src/builtins/printf.rs +++ b/fish-rust/src/builtins/printf.rs @@ -77,9 +77,9 @@ fn iswxdigit(c: char) -> bool { c.is_ascii_hexdigit() } -struct builtin_printf_state_t<'a> { +struct builtin_printf_state_t<'a, 'b> { // Out and err streams. Note this is a captured reference! - streams: &'a mut IoStreams, + streams: &'a mut IoStreams<'b>, // The status of the operation. exit_code: c_int, @@ -203,7 +203,7 @@ fn string_to_scalar_type( } } -impl<'a> builtin_printf_state_t<'a> { +impl<'a, 'b> builtin_printf_state_t<'a, 'b> { #[allow(clippy::partialeq_to_none)] fn verify_numeric(&mut self, s: &wstr, end: &wstr, errcode: Option) { // This check matches the historic `errcode != EINVAL` check from C++. @@ -579,7 +579,7 @@ fn nonfatal_error>(&mut self, errstr: Str) { self.streams.err.append(errstr); if !errstr.ends_with('\n') { - self.streams.err.append1('\n'); + self.streams.err.push('\n'); } // We set the exit code to error, because one occurred, @@ -603,7 +603,7 @@ fn fatal_error>(&mut self, errstr: Str) { self.streams.err.append(errstr); if !errstr.ends_with('\n') { - self.streams.err.append1('\n'); + self.streams.err.push('\n'); } self.exit_code = STATUS_CMD_ERROR.unwrap(); @@ -763,7 +763,7 @@ fn append_output(&mut self, c: char) { } /// The printf builtin. -pub fn printf(_parser: &mut Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Option { +pub fn printf(_parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Option { let mut argc = argv.len(); // Rebind argv as immutable slice (can't rearrange its elements), skipping the command name. diff --git a/fish-rust/src/builtins/pwd.rs b/fish-rust/src/builtins/pwd.rs index 68dafa6d6..70243fbb8 100644 --- a/fish-rust/src/builtins/pwd.rs +++ b/fish-rust/src/builtins/pwd.rs @@ -2,7 +2,7 @@ use errno::errno; use super::prelude::*; -use crate::{env::EnvMode, wutil::wrealpath}; +use crate::{env::Environment, wutil::wrealpath}; // The pwd builtin. Respect -P to resolve symbolic links. Respect -L to not do that (the default). const short_options: &wstr = L!("LPh"); @@ -12,7 +12,7 @@ wopt(L!("physical"), no_argument, 'P'), ]; -pub fn pwd(parser: &mut Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Option { +pub fn pwd(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Option { let cmd = argv[0]; let argc = argv.len(); let mut resolve_symlinks = false; @@ -41,11 +41,8 @@ pub fn pwd(parser: &mut Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> } let mut pwd = WString::new(); - let tmp = parser - .vars1() - .get_or_null(&L!("PWD").to_ffi(), EnvMode::default().bits()); - if !tmp.is_null() { - pwd = tmp.as_string().from_ffi(); + if let Some(tmp) = parser.vars().get(L!("PWD")) { + pwd = tmp.as_string(); } if resolve_symlinks { if let Some(real_pwd) = wrealpath(&pwd) { diff --git a/fish-rust/src/builtins/random.rs b/fish-rust/src/builtins/random.rs index bbdc25aec..37f61b721 100644 --- a/fish-rust/src/builtins/random.rs +++ b/fish-rust/src/builtins/random.rs @@ -8,7 +8,7 @@ static RNG: Lazy> = Lazy::new(|| Mutex::new(SmallRng::from_entropy())); -pub fn random(parser: &mut Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Option { +pub fn random(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Option { let cmd = argv[0]; let argc = argv.len(); let print_hints = false; @@ -47,7 +47,7 @@ pub fn random(parser: &mut Parser, streams: &mut IoStreams, argv: &mut [&wstr]) if arg_count == 1 { streams .err - .append(wgettext_fmt!("%ls: nothing to choose from\n", cmd,)); + .append(wgettext_fmt!("%ls: nothing to choose from\n", cmd)); return STATUS_INVALID_ARGS; } diff --git a/fish-rust/src/builtins/read.rs b/fish-rust/src/builtins/read.rs new file mode 100644 index 000000000..5f9c4c33a --- /dev/null +++ b/fish-rust/src/builtins/read.rs @@ -0,0 +1,7 @@ +//! Implementation of the read builtin. + +use super::prelude::*; + +pub fn read(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { + run_builtin_ffi(crate::ffi::builtin_read, parser, streams, args) +} diff --git a/fish-rust/src/builtins/realpath.rs b/fish-rust/src/builtins/realpath.rs index 04b3c40af..5605ae9fc 100644 --- a/fish-rust/src/builtins/realpath.rs +++ b/fish-rust/src/builtins/realpath.rs @@ -3,6 +3,8 @@ use errno::errno; use super::prelude::*; +use crate::env::Environment; +use crate::io::IoStreams; use crate::{ path::path_apply_working_directory, wutil::{normalize_path, wrealpath}, @@ -22,7 +24,7 @@ struct Options { fn parse_options( args: &mut [&wstr], - parser: &mut Parser, + parser: &Parser, streams: &mut IoStreams, ) -> Result<(Options, usize), Option> { let cmd = args[0]; @@ -53,7 +55,7 @@ fn parse_options( /// An implementation of the external realpath command. Doesn't support any options. /// In general scripts shouldn't invoke this directly. They should just use `realpath` which /// will fallback to this builtin if an external command cannot be found. -pub fn realpath(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { +pub fn realpath(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { let cmd = args[0]; let (opts, optind) = match parse_options(args, parser, streams) { Ok((opts, optind)) => (opts, optind), @@ -105,7 +107,7 @@ pub fn realpath(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr] } } else { // We need to get the *physical* pwd here. - let realpwd = wrealpath(parser.vars1().get_pwd_slash().as_wstr()); + let realpwd = wrealpath(&parser.vars().get_pwd_slash()); if let Some(realpwd) = realpwd { let absolute_arg = if arg.starts_with(L!("/")) { diff --git a/fish-rust/src/builtins/return.rs b/fish-rust/src/builtins/return.rs index f4ef91445..77d46232e 100644 --- a/fish-rust/src/builtins/return.rs +++ b/fish-rust/src/builtins/return.rs @@ -11,7 +11,7 @@ struct Options { fn parse_options( args: &mut [&wstr], - parser: &mut Parser, + parser: &Parser, streams: &mut IoStreams, ) -> Result<(Options, usize), Option> { let cmd = args[0]; @@ -46,13 +46,13 @@ fn parse_options( } /// Function for handling the return builtin. -pub fn r#return(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { +pub fn r#return(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { let mut retval = match parse_return_value(args, parser, streams) { Ok(v) => v, Err(e) => return e, }; - let has_function_block = parser.ffi_has_funtion_block(); + let has_function_block = parser.blocks().iter().any(|b| b.is_function_call()); // *nix does not support negative return values, but our `return` builtin happily accepts being // called with negative literals (e.g. `return -1`). @@ -65,7 +65,7 @@ pub fn r#return(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr] // If we're not in a function, exit the current script (but not an interactive shell). if !has_function_block { - let ld = parser.libdata_pod(); + let ld = &mut parser.libdata_mut().pods; if !ld.is_interactive { ld.exit_current_script = true; } @@ -73,14 +73,14 @@ pub fn r#return(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr] } // Mark a return in the libdata. - parser.libdata_pod().returning = true; + parser.libdata_mut().pods.returning = true; return Some(retval); } pub fn parse_return_value( args: &mut [&wstr], - parser: &mut Parser, + parser: &Parser, streams: &mut IoStreams, ) -> Result> { let cmd = args[0]; @@ -97,11 +97,11 @@ pub fn parse_return_value( streams .err .append(wgettext_fmt!(BUILTIN_ERR_TOO_MANY_ARGUMENTS, cmd)); - builtin_print_error_trailer(parser, streams, cmd); + builtin_print_error_trailer(parser, streams.err, cmd); return Err(STATUS_INVALID_ARGS); } if optind == args.len() { - Ok(parser.get_last_status().into()) + Ok(parser.get_last_status()) } else { match fish_wcstoi(args[optind]) { Ok(i) => Ok(i), @@ -109,7 +109,7 @@ pub fn parse_return_value( streams .err .append(wgettext_fmt!(BUILTIN_ERR_NOT_NUMBER, cmd, args[1])); - builtin_print_error_trailer(parser, streams, cmd); + builtin_print_error_trailer(parser, streams.err, cmd); return Err(STATUS_INVALID_ARGS); } } diff --git a/fish-rust/src/builtins/set.rs b/fish-rust/src/builtins/set.rs new file mode 100644 index 000000000..02b754ced --- /dev/null +++ b/fish-rust/src/builtins/set.rs @@ -0,0 +1,1007 @@ +use super::prelude::*; +use crate::common::escape; +use crate::common::escape_string; +use crate::common::get_ellipsis_char; +use crate::common::get_ellipsis_str; +use crate::common::valid_var_name; +use crate::common::EscapeFlags; +use crate::common::EscapeStringStyle; +use crate::env::EnvStackSetResult; +use crate::env::EnvVarFlags; +use crate::env::INHERITED_VARS; +use crate::event; +use crate::event::Event; +use crate::expand::expand_escape_string; +use crate::expand::expand_escape_variable; +use crate::history::history_session_id; +use crate::history::History; +use crate::wchar_ext::WExt; +use crate::{ + env::{EnvMode, EnvVar, Environment}, + wutil::wcstoi::wcstoi_partial, +}; + +// FIXME: (once localization works in Rust) These should separate `%ls: ` and the trailing `\n`, like in builtins/string +const MISMATCHED_ARGS: &str = "%ls: given %d indexes but %d values\n"; +const ARRAY_BOUNDS_ERR: &str = "%ls: array index out of bounds\n"; +const UVAR_ERR: &str = + "%ls: successfully set universal '%ls'; but a global by that name shadows it\n"; + +#[derive(Debug, Clone)] +struct Options { + print_help: bool, + show: bool, + local: bool, + function: bool, + global: bool, + exportv: bool, + erase: bool, + list: bool, + unexport: bool, + pathvar: bool, + unpathvar: bool, + universal: bool, + query: bool, + shorten_ok: bool, + append: bool, + prepend: bool, + preserve_failure_exit_status: bool, +} + +impl Default for Options { + fn default() -> Self { + Self { + print_help: false, + show: false, + local: false, + function: false, + global: false, + exportv: false, + erase: false, + list: false, + unexport: false, + pathvar: false, + unpathvar: false, + universal: false, + query: false, + shorten_ok: true, + append: false, + prepend: false, + preserve_failure_exit_status: true, + } + } +} + +impl Options { + fn scope(&self) -> EnvMode { + let mut scope = EnvMode::USER; + for (is_mode, mode) in [ + (self.local, EnvMode::LOCAL), + (self.function, EnvMode::FUNCTION), + (self.global, EnvMode::GLOBAL), + (self.exportv, EnvMode::EXPORT), + (self.unexport, EnvMode::UNEXPORT), + (self.universal, EnvMode::UNIVERSAL), + (self.pathvar, EnvMode::PATHVAR), + (self.unpathvar, EnvMode::UNPATHVAR), + ] { + if is_mode { + scope |= mode; + } + } + scope + } + + fn parse( + cmd: &wstr, + args: &mut [&wstr], + parser: &Parser, + streams: &mut IoStreams, + ) -> Result<(Options, usize), Option> { + /// Values used for long-only options. + const PATH_ARG: char = 1 as char; + const UNPATH_ARG: char = 2 as char; + // Variables used for parsing the argument list. This command is atypical in using the "+" + // (REQUIRE_ORDER) option for flag parsing. This is not typical of most fish commands. It means + // we stop scanning for flags when the first non-flag argument is seen. + const SHORT_OPTS: &wstr = L!("+:LSUaefghlnpqux"); + const LONG_OPTS: &[woption] = &[ + wopt(L!("export"), no_argument, 'x'), + wopt(L!("global"), no_argument, 'g'), + wopt(L!("function"), no_argument, 'f'), + wopt(L!("local"), no_argument, 'l'), + wopt(L!("erase"), no_argument, 'e'), + wopt(L!("names"), no_argument, 'n'), + wopt(L!("unexport"), no_argument, 'u'), + wopt(L!("universal"), no_argument, 'U'), + wopt(L!("long"), no_argument, 'L'), + wopt(L!("query"), no_argument, 'q'), + wopt(L!("show"), no_argument, 'S'), + wopt(L!("append"), no_argument, 'a'), + wopt(L!("prepend"), no_argument, 'p'), + wopt(L!("path"), no_argument, PATH_ARG), + wopt(L!("unpath"), no_argument, UNPATH_ARG), + wopt(L!("help"), no_argument, 'h'), + ]; + + let mut opts = Self::default(); + + let mut w = wgetopter_t::new(SHORT_OPTS, LONG_OPTS, args); + while let Some(c) = w.wgetopt_long() { + match c { + 'a' => opts.append = true, + 'e' => { + opts.erase = true; + opts.preserve_failure_exit_status = false; + } + 'f' => opts.function = true, + 'g' => opts.global = true, + 'h' => opts.print_help = true, + 'l' => opts.local = true, + 'n' => { + opts.list = true; + opts.preserve_failure_exit_status = false; + } + 'p' => opts.prepend = true, + 'q' => { + opts.query = true; + opts.preserve_failure_exit_status = false; + } + 'x' => opts.exportv = true, + 'u' => opts.unexport = true, + PATH_ARG => opts.pathvar = true, + UNPATH_ARG => opts.unpathvar = true, + 'U' => opts.universal = true, + 'L' => opts.shorten_ok = false, + 'S' => { + opts.show = true; + opts.preserve_failure_exit_status = false; + } + ':' => { + builtin_missing_argument(parser, streams, cmd, args[w.woptind - 1], false); + return Err(STATUS_INVALID_ARGS); + } + '?' => { + // Specifically detect `set -o` because people might be bringing over bashisms. + let optind = w.woptind; + // implicit drop(w); here + if args[optind - 1].starts_with("-o") { + // TODO: translate this + streams.err.appendln(L!( + "Fish does not have shell options. See `help fish-for-bash-users`." + )); + if optind < args.len() { + if args[optind] == "vi" { + // Tell the vi users how to get what they need. + streams + .err + .appendln(L!("To enable vi-mode, run `fish_vi_key_bindings`.")); + } else if args[optind] == "ed" { + // This should be enough for make ed users feel at home + streams.err.append(L!("?\n?\n?\n")); + } + } + } + + builtin_unknown_option(parser, streams, cmd, args[optind - 1], false); + return Err(STATUS_INVALID_ARGS); + } + _ => { + panic!("unexpected retval from wgetopt_long"); + } + } + } + + let optind = w.woptind; + + if opts.print_help { + builtin_print_help(parser, streams, cmd); + return Err(STATUS_CMD_OK); + } + + Self::validate(&opts, cmd, args, parser, streams)?; + + Ok((opts, optind)) + } + + fn validate( + opts: &Self, + cmd: &wstr, + args: &[&wstr], + parser: &Parser, + streams: &mut IoStreams, + ) -> Result<(), Option> { + // Can't query and erase or list. + if opts.query && (opts.erase || opts.list) { + streams.err.append(wgettext_fmt!(BUILTIN_ERR_COMBO, cmd)); + builtin_print_error_trailer(parser, streams.err, cmd); + return Err(STATUS_INVALID_ARGS); + } + + // We can't both list and erase variables. + if opts.erase && opts.list { + streams.err.append(wgettext_fmt!(BUILTIN_ERR_COMBO, cmd)); + builtin_print_error_trailer(parser, streams.err, cmd); + return Err(STATUS_INVALID_ARGS); + } + + // Variables can only have one scope... + if [opts.local, opts.function, opts.global, opts.universal] + .into_iter() + .filter(|b| *b) + .count() + > 1 + // ..unless we are erasing a variable, in which case we can erase from several in one go. + && !opts.erase + { + streams.err.append(wgettext_fmt!(BUILTIN_ERR_GLOCAL, cmd)); + builtin_print_error_trailer(parser, streams.err, cmd); + return Err(STATUS_INVALID_ARGS); + } + + // Variables can only have one export status. + if opts.exportv && opts.unexport { + streams.err.append(wgettext_fmt!(BUILTIN_ERR_EXPUNEXP, cmd)); + builtin_print_error_trailer(parser, streams.err, cmd); + return Err(STATUS_INVALID_ARGS); + } + + // Variables can only have one path status. + if opts.pathvar && opts.unpathvar { + streams + .err + .append(wgettext_fmt!(BUILTIN_ERR_PATHUNPATH, cmd)); + builtin_print_error_trailer(parser, streams.err, cmd); + return Err(STATUS_INVALID_ARGS); + } + + // Trying to erase and (un)export at the same time doesn't make sense. + if opts.erase && (opts.exportv || opts.unexport) { + streams.err.append(wgettext_fmt!(BUILTIN_ERR_COMBO, cmd)); + builtin_print_error_trailer(parser, streams.err, cmd); + return Err(STATUS_INVALID_ARGS); + } + + // The --show flag cannot be combined with any other flag. + if opts.show + && (opts.local + || opts.function + || opts.global + || opts.erase + || opts.list + || opts.exportv + || opts.universal) + { + streams.err.append(wgettext_fmt!(BUILTIN_ERR_COMBO, cmd)); + builtin_print_error_trailer(parser, streams.err, cmd); + return Err(STATUS_INVALID_ARGS); + } + + if args.is_empty() && opts.erase { + streams.err.append(wgettext_fmt!(BUILTIN_ERR_MISSING, cmd)); + builtin_print_error_trailer(parser, streams.err, cmd); + return Err(STATUS_INVALID_ARGS); + } + + Ok(()) + } +} + +// Check if we are setting a uvar and a global of the same name exists. See +// https://github.com/fish-shell/fish-shell/issues/806 +fn warn_if_uvar_shadows_global( + cmd: &wstr, + opts: &Options, + dest: &wstr, + streams: &mut IoStreams, + parser: &Parser, +) { + if opts.universal && parser.is_interactive() { + if parser.vars().getf(dest, EnvMode::GLOBAL).is_some() { + streams.err.append(wgettext_fmt!(UVAR_ERR, cmd, dest)); + } + } +} + +fn handle_env_return(retval: EnvStackSetResult, cmd: &wstr, key: &wstr, streams: &mut IoStreams) { + match retval { + EnvStackSetResult::ENV_OK => (), + EnvStackSetResult::ENV_PERM => { + streams.err.append(wgettext_fmt!( + "%ls: Tried to change the read-only variable '%ls'\n", + cmd, + key + )); + } + EnvStackSetResult::ENV_SCOPE => { + streams.err.append(wgettext_fmt!( + "%ls: Tried to modify the special variable '%ls' with the wrong scope\n", + cmd, + key + )); + } + EnvStackSetResult::ENV_INVALID => { + streams.err.append(wgettext_fmt!( + "%ls: Tried to modify the special variable '%ls' to an invalid value\n", + cmd, + key + )); + } + EnvStackSetResult::ENV_NOT_FOUND => { + streams.err.append(wgettext_fmt!( + "%ls: The variable '%ls' does not exist\n", + cmd, + key + )); + } + _ => panic!("unexpected vars.set() ret val"), + } +} + +/// Call vars.set. If this is a path variable, e.g. PATH, validate the elements. On error, print a +/// description of the problem to stderr. +fn env_set_reporting_errors( + cmd: &wstr, + key: &wstr, + scope: EnvMode, + list: Vec, + streams: &mut IoStreams, + parser: &Parser, +) -> EnvStackSetResult { + let retval = parser.set_var_and_fire(key, scope | EnvMode::USER, list); + // If this returned OK, the parser already fired the event. + handle_env_return(retval, cmd, key, streams); + retval +} + +// PORTING: maybe just add `thiserror`? +enum EnvArrayParseError { + InvalidIndex(WString), +} + +impl std::fmt::Display for EnvArrayParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!( + f, + "{}", + match self { + EnvArrayParseError::InvalidIndex(varname) => + wgettext_fmt!("%ls: Invalid index starting at '%ls'\n", "set", varname) + .to_string(), + } + ) + } +} + +#[derive(Debug, Default)] +struct SplitVar<'a> { + varname: &'a wstr, + var: Option, + indexes: Vec, +} + +impl<'a> SplitVar<'a> { + /// \return the number of elements in our variable, or 0 if missing. + fn varsize(&self) -> usize { + self.var.as_ref().map_or(0, |var| var.as_list().len()) + } +} + +/// Extract indexes from an argument of the form `var_name[index1 index2...]`. +/// The argument \p arg is split into a variable name and list of indexes, which is returned by +/// reference. Indexes are "expanded" in the sense that range expressions .. and negative values are +/// handled. +/// +/// Returns: +/// a split var on success, none() on error, in which case an error will have been printed. +/// If no index is found, this leaves indexes empty. +fn split_var_and_indexes<'a>( + arg: &'a wstr, + mode: EnvMode, + vars: &dyn Environment, + streams: &mut IoStreams, +) -> Option> { + match split_var_and_indexes_internal(arg, mode, vars) { + Ok(split) => Some(split), + Err(EnvArrayParseError::InvalidIndex(varname)) => { + streams.err.append(wgettext_fmt!( + "%ls: Invalid index starting at '%ls'\n", + "set", + &varname, + )); + None + } + } +} + +fn split_var_and_indexes_internal<'a>( + arg: &'a wstr, + mode: EnvMode, + vars: &dyn Environment, +) -> Result, EnvArrayParseError> { + // PORTING: this should probably be made reusable in some way? + + let mut res = SplitVar::default(); + let open_bracket = arg.find_char('['); + res.varname = open_bracket.map(|b| &arg[..b]).unwrap_or(arg); + res.var = vars.getf(res.varname, mode); + let Some(open_bracket) = open_bracket else { + // Common case of no bracket + return Ok(res); + }; + + // PORTING: this should return an error if there is no var, + // we need the length of the array to validate the indexes + let len = res + .var + .as_ref() + .map(|v| v.as_list().len()) + .unwrap_or_default(); + let mut c = arg.slice_from(open_bracket + 1); + + while c.char_at(0) != ']' { + let mut consumed = 0; + let l_ind: isize; + if res.indexes.is_empty() && c.char_at(0) == '.' && c.char_at(1) == '.' { + // If we are at the first index expression, a missing start-index means the range starts + // at the first item. + l_ind = 1; // first index + } else { + l_ind = wcstoi_partial(c, crate::wutil::Options::default(), &mut consumed) + .map_err(|_| EnvArrayParseError::InvalidIndex(res.varname.to_owned()))?; + c = c.slice_from(consumed); + // Skip trailing whitespace. + while !c.is_empty() && c.char_at(0).is_whitespace() { + c = c.slice_from(1); + } + } + + // Convert negative index to a positive index. + let signed_to_unsigned = |i: isize| -> Result { + match i.try_into() { + Ok(idx) => Ok(idx), + Err(_) => { + let Some(idx) = len.checked_add_signed(i).map(|i| i + 1) else { + // PORTING: this was not handled in C++, l_ind was just kept as an `i64` + // this should have a separate error + // also: in the case of `var[1 1]` we should probably either de-duplicate + // or make that a hard error. + // the behavior of `..` is also somewhat weird + return Err(EnvArrayParseError::InvalidIndex(res.varname.to_owned())); + }; + Ok(idx) + } + } + }; + + let l_ind: usize = signed_to_unsigned(l_ind)?; + + if c.char_at(0) == '.' && c.char_at(1) == '.' { + // If we are at the last index range expression, a missing end-index means the range + // spans until the last item. + c = c.slice_from(2); + let l_ind2: isize; + // If we are at the last index range expression, a missing end-index means the range + // spans until the last item. + if res.indexes.is_empty() && c.char_at(0) == ']' { + l_ind2 = -1; + } else { + l_ind2 = wcstoi_partial(c, crate::wutil::Options::default(), &mut consumed) + .map_err(|_| EnvArrayParseError::InvalidIndex(res.varname.to_owned()))?; + c = c.slice_from(consumed); + // Skip trailing whitespace. + while !c.is_empty() && c.char_at(0).is_whitespace() { + c = c.slice_from(1); + } + } + + let l_ind2: usize = signed_to_unsigned(l_ind2)?; + if l_ind < l_ind2 { + res.indexes.extend(l_ind..=l_ind2); + } else { + res.indexes.extend((l_ind2..=l_ind).rev()); + } + } else { + res.indexes.push(l_ind); + } + } + + Ok(res) +} + +/// Given a list of values and 1-based indexes, return a new list with those elements removed. +/// Note this deliberately accepts both args by value, as it modifies them both. +fn erased_at_indexes(mut input: Vec, mut indexes: Vec) -> Vec { + // Sort our indexes into *descending* order. + indexes.sort_by_key(|&index| std::cmp::Reverse(index)); + + // Remove duplicates. + indexes.dedup(); + + // Now when we walk indexes, we encounter larger indexes first. + for idx in indexes { + if idx > 0 && idx <= input.len() { + // One-based indexing! + input.remove(idx - 1); + } + } + input +} + +/// Print the names of all environment variables in the scope. It will include the values unless the +/// `set --names` flag was used. +fn list(opts: &Options, parser: &Parser, streams: &mut IoStreams) -> Option { + let names_only = opts.list; + let mut names = parser.vars().get_names(opts.scope()); + names.sort(); + + for key in names { + let mut out = key.clone(); + + if !names_only { + let mut val = WString::new(); + if opts.shorten_ok && key == "history" { + let history = History::with_name(&history_session_id(parser.vars())); + for i in 1..history.size() { + if val.len() >= 64 { + break; + } + if i > 1 { + val.push(' '); + } + val += &expand_escape_string(history.item_at_index(i).unwrap().str())[..] + } + } else if let Some(var) = parser.vars().getf_unless_empty(&key, opts.scope()) { + val = expand_escape_variable(&var); + } + if !val.is_empty() { + let mut shorten = false; + if opts.shorten_ok && val.len() > 64 { + shorten = true; + val.truncate(60); + } + out.push(' '); + out.push_utfstr(&val); + + if shorten { + out.push(get_ellipsis_char()); + } + } + } + + out.push('\n'); + streams.out.append(out); + } + + STATUS_CMD_OK +} + +fn query( + cmd: &wstr, + opts: &Options, + parser: &Parser, + streams: &mut IoStreams, + args: &[&wstr], +) -> Option { + let mut retval = 0; + let scope = opts.scope(); + + // No variables given, this is an error. + // 255 is the maximum return code we allow. + if args.is_empty() { + return Some(255); + } + + for arg in args { + let Some(split) = split_var_and_indexes(arg, scope, parser.vars(), streams) else { + builtin_print_error_trailer(parser, streams.err, cmd); + return STATUS_CMD_ERROR; + }; + + if split.indexes.is_empty() { + // No indexes, just increment if our variable is missing. + if split.var.is_none() { + retval += 1; + } + } else { + // Increment for every index out of range. + let varsize = split.varsize(); + for idx in split.indexes { + if idx < 1 || idx > varsize { + retval += 1; + } + } + } + } + + Some(retval) +} + +fn show_scope(var_name: &wstr, scope: EnvMode, streams: &mut IoStreams, vars: &dyn Environment) { + let scope_name = match scope { + EnvMode::LOCAL => L!("local"), + EnvMode::GLOBAL => L!("global"), + EnvMode::UNIVERSAL => L!("universal"), + _ => panic!("invalid scope"), + }; + let Some(var) = vars.getf(var_name, scope) else { + return; + }; + + let exportv = if var.exports() { + wgettext!("exported") + } else { + wgettext!("unexported") + }; + let pathvarv = if var.is_pathvar() { + wgettext!(" a path variable") + } else { + L!("") + }; + let vals = var.as_list(); + streams.out.append(wgettext_fmt!( + "$%ls: set in %ls scope, %ls,%ls with %d elements", + var_name, + scope_name, + exportv, + pathvarv, + vals.len() + )); + // HACK: PWD can be set, depending on how you ask. + // For our purposes it's read-only. + if EnvVar::flags_for(var_name).contains(EnvVarFlags::READ_ONLY) { + streams.out.append(wgettext!(" (read-only)\n")); + } else { + streams.out.push('\n'); + } + + for i in 0..vals.len() { + if vals.len() > 100 { + if i == 50 { + // try to print a mid-line ellipsis because we are eliding lines not words + streams.out.append(if u32::from(get_ellipsis_char()) > 256 { + L!("\u{22EF}") + } else { + get_ellipsis_str() + }); + streams.out.push('\n'); + } + if i >= 50 && i < vals.len() - 50 { + continue; + } + } + let value = &vals[i]; + let escaped_val = escape_string( + value, + EscapeStringStyle::Script(EscapeFlags::NO_PRINTABLES | EscapeFlags::NO_QUOTED), + ); + streams.out.append(wgettext_fmt!( + "$%ls[%d]: |%ls|\n", + var_name, + i + 1, + &escaped_val + )); + } +} + +/// Show mode. Show information about the named variable(s). +fn show(cmd: &wstr, parser: &Parser, streams: &mut IoStreams, args: &[&wstr]) -> Option { + let vars = parser.vars(); + if args.is_empty() { + // show all vars + let mut names = vars.get_names(EnvMode::USER); + names.sort(); + for name in names { + if name == "history" { + continue; + } + show_scope(&name, EnvMode::LOCAL, streams, vars); + show_scope(&name, EnvMode::GLOBAL, streams, vars); + show_scope(&name, EnvMode::UNIVERSAL, streams, vars); + + // Show the originally imported value as a debugging aid. + if let Some(inherited) = INHERITED_VARS.get().unwrap().get(&name) { + let escaped_val = escape_string( + inherited, + EscapeStringStyle::Script(EscapeFlags::NO_PRINTABLES | EscapeFlags::NO_QUOTED), + ); + streams.out.append(wgettext_fmt!( + "$%ls: originally inherited as |%ls|\n", + name, + escaped_val + )); + } + } + } else { + for arg in args.iter().copied() { + if !valid_var_name(arg) { + streams + .err + .append(wgettext_fmt!(BUILTIN_ERR_VARNAME, cmd, arg)); + builtin_print_error_trailer(parser, streams.err, cmd); + return STATUS_INVALID_ARGS; + } + + if arg.contains('[') { + streams.err.append(wgettext_fmt!( + "%ls: `set --show` does not allow slices with the var names\n", + cmd + )); + builtin_print_error_trailer(parser, streams.err, cmd); + return STATUS_CMD_ERROR; + } + + show_scope(arg, EnvMode::LOCAL, streams, vars); + show_scope(arg, EnvMode::GLOBAL, streams, vars); + show_scope(arg, EnvMode::UNIVERSAL, streams, vars); + if let Some(inherited) = INHERITED_VARS.get().unwrap().get(arg) { + let escaped_val = escape_string( + inherited, + EscapeStringStyle::Script(EscapeFlags::NO_PRINTABLES | EscapeFlags::NO_QUOTED), + ); + streams.out.append(wgettext_fmt!( + "$%ls: originally inherited as |%ls|\n", + arg, + escaped_val + )); + } + } + } + + STATUS_CMD_OK +} + +fn erase( + cmd: &wstr, + opts: &Options, + parser: &Parser, + streams: &mut IoStreams, + args: &[&wstr], +) -> Option { + let mut ret = STATUS_CMD_OK; + let scopes = opts.scope(); + // `set -e` is allowed to be called with multiple scopes. + for bit in (0..).take_while(|bit| 1 << bit <= EnvMode::USER.bits()) { + let scope = scopes.intersection(EnvMode::from_bits(1 << bit).unwrap()); + if scope.bits() == 0 || (scope == EnvMode::USER && scopes != EnvMode::USER) { + continue; + } + for arg in args { + let Some(split) = split_var_and_indexes(arg, scope, parser.vars(), streams) else { + builtin_print_error_trailer(parser, streams.err, cmd); + return STATUS_CMD_ERROR; + }; + + if !valid_var_name(split.varname) { + streams + .err + .append(wgettext_fmt!(BUILTIN_ERR_VARNAME, cmd, split.varname)); + builtin_print_error_trailer(parser, streams.err, cmd); + return STATUS_INVALID_ARGS; + } + let retval; + if split.indexes.is_empty() { + // unset the var + retval = parser.vars().remove(split.varname, scope); + // When a non-existent-variable is unset, return ENV_NOT_FOUND as $status + // but do not emit any errors at the console as a compromise between user + // friendliness and correctness. + if retval != EnvStackSetResult::ENV_NOT_FOUND { + handle_env_return(retval, cmd, split.varname, streams); + } + if retval == EnvStackSetResult::ENV_OK { + event::fire(parser, Event::variable_erase(split.varname.to_owned())); + } + } else { + // remove just the specified indexes of the var + let Some(var) = split.var else { + return STATUS_CMD_ERROR; + }; + let result = erased_at_indexes(var.as_list().to_owned(), split.indexes); + retval = + env_set_reporting_errors(cmd, split.varname, scope, result, streams, parser); + } + + // Set $status to the last error value. + // This is cheesy, but I don't expect this to be checked often. + if retval != EnvStackSetResult::ENV_OK { + ret = env_result_to_status(retval); + } + } + } + ret +} + +fn env_result_to_status(retval: EnvStackSetResult) -> Option { + Some(match retval { + EnvStackSetResult::ENV_OK => 0, + EnvStackSetResult::ENV_PERM => 1, + EnvStackSetResult::ENV_SCOPE => 2, + EnvStackSetResult::ENV_INVALID => 3, + EnvStackSetResult::ENV_NOT_FOUND => 4, + _ => panic!(), + }) +} + +/// Return a list of new values for the variable \p varname, respecting the \p opts. +/// This handles the simple case where there are no indexes. +fn new_var_values( + varname: &wstr, + opts: &Options, + argv: &[&wstr], + vars: &dyn Environment, +) -> Vec { + let mut result = vec![]; + if !opts.prepend && !opts.append { + // Not prepending or appending. + result.extend(argv.iter().copied().map(|s| s.to_owned())); + } else { + // Note: when prepending or appending, we always use default scoping when fetching existing + // values. For example: + // set --global var 1 2 + // set --local --append var 3 4 + // This starts with the existing global variable, appends to it, and sets it locally. + // So do not use the given variable: we must re-fetch it. + // TODO: this races under concurrent execution. + if let Some(existing) = vars.get(varname) { + result = existing.as_list().to_owned(); + } + + if opts.prepend { + result.splice(0..0, argv.iter().copied().map(|s| s.to_owned())); + } + + if opts.append { + result.extend(argv.iter().copied().map(|s| s.to_owned())); + } + } + result +} + +/// This handles the more difficult case of setting individual slices of a var. +fn new_var_values_by_index(split: &SplitVar, argv: &[&wstr]) -> Vec { + assert!( + argv.len() == split.indexes.len(), + "Must have the same number of indexes as arguments" + ); + + // Inherit any existing values. + // Note unlike the append/prepend case, we start with a variable in the same scope as we are + // setting. + let mut result = vec![]; + if let Some(var) = split.var.as_ref() { + result = var.as_list().to_owned(); + } + + // For each (index, argument) pair, set the element in our \p result to the replacement string. + // Extend the list with empty strings as needed. The indexes are 1-based. + for (i, arg) in argv.iter().copied().enumerate() { + let lidx = split.indexes[i]; + assert!(lidx >= 1, "idx should have been verified in range already"); + // Convert from 1-based to 0-based. + let idx = lidx - 1; + // Extend as needed with empty strings. + if idx >= result.len() { + result.resize(idx + 1, WString::new()); + } + result[idx] = arg.into(); + } + result +} + +/// Set a variable. +fn set_internal( + cmd: &wstr, + opts: &Options, + parser: &Parser, + + streams: &mut IoStreams, + argv: &[&wstr], +) -> Option { + if argv.is_empty() { + streams + .err + .append(wgettext_fmt!(BUILTIN_ERR_MIN_ARG_COUNT1, cmd, 1)); + builtin_print_error_trailer(parser, streams.err, cmd); + return STATUS_INVALID_ARGS; + } + + let scope = opts.scope(); + let var_expr = argv[0]; + let argv = &argv[1..]; + + let Some(split) = split_var_and_indexes(var_expr, scope, parser.vars(), streams) else { + builtin_print_error_trailer(parser, streams.err, cmd); + return STATUS_INVALID_ARGS; + }; + + // Is the variable valid? + if !valid_var_name(split.varname) { + streams + .err + .append(wgettext_fmt!(BUILTIN_ERR_VARNAME, cmd, split.varname)); + if let Some(pos) = split.varname.chars().position(|c| c == '=') { + streams.err.append(wgettext_fmt!( + "%ls: Did you mean `set %ls %ls`?", + cmd, + &escape(&split.varname[..pos]), + &escape(&split.varname[pos + 1..]) + )); + } + builtin_print_error_trailer(parser, streams.err, cmd); + return STATUS_INVALID_ARGS; + } + + // Setting with explicit indexes like `set foo[3] ...` has additional error handling. + if !split.indexes.is_empty() { + // Indexes must be > 0. (Note split_var_and_indexes negates negative values). + + // Append and prepend are disallowed. + if opts.append || opts.prepend { + streams.err.append(wgettext_fmt!( + "%ls: Cannot use --append or --prepend when assigning to a slice", + cmd + )); + builtin_print_error_trailer(parser, streams.err, cmd); + return STATUS_INVALID_ARGS; + } + + // Argument count and index count must agree. + if split.indexes.len() != argv.len() { + streams.err.append(wgettext_fmt!( + MISMATCHED_ARGS, + cmd, + split.indexes.len(), + argv.len() + )); + return STATUS_INVALID_ARGS; + } + } + + let new_values = if split.indexes.is_empty() { + // Handle the simple, common, case. Set the var to the specified values. + new_var_values(split.varname, opts, argv, parser.vars()) + } else { + // Handle the uncommon case of setting specific slices of a var. + new_var_values_by_index(&split, argv) + }; + + // Set the value back in the variable stack and fire any events. + let retval = env_set_reporting_errors(cmd, split.varname, scope, new_values, streams, parser); + + if retval == EnvStackSetResult::ENV_OK { + warn_if_uvar_shadows_global(cmd, opts, split.varname, streams, parser); + } + env_result_to_status(retval) +} + +/// The set builtin creates, updates, and erases (removes, deletes) variables. +pub fn set(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { + let cmd = args[0]; + let (opts, optind) = match Options::parse(cmd, args, parser, streams) { + Ok(res) => res, + Err(err) => return err, + }; + + let args = &args[optind..]; + + let retval = if opts.query { + query(cmd, &opts, parser, streams, args) + } else if opts.erase { + erase(cmd, &opts, parser, streams, args) + } else if opts.list { + list(&opts, parser, streams) + } else if opts.show { + show(cmd, parser, streams, args) + } else if args.is_empty() { + list(&opts, parser, streams) + } else { + set_internal(cmd, &opts, parser, streams, args) + }; + + if retval == STATUS_CMD_OK && opts.preserve_failure_exit_status { + return None; + } + + return retval; +} diff --git a/fish-rust/src/builtins/set_color.rs b/fish-rust/src/builtins/set_color.rs index db01e88e8..65f555d32 100644 --- a/fish-rust/src/builtins/set_color.rs +++ b/fish-rust/src/builtins/set_color.rs @@ -116,11 +116,7 @@ fn print_colors( ]; /// set_color builtin. -pub fn set_color( - parser: &mut Parser, - streams: &mut IoStreams, - argv: &mut [&wstr], -) -> Option { +pub fn set_color(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Option { // Variables used for parsing the argument list. let argc = argv.len(); diff --git a/fish-rust/src/builtins/shared.rs b/fish-rust/src/builtins/shared.rs index 0eeed629e..9b73c00f3 100644 --- a/fish-rust/src/builtins/shared.rs +++ b/fish-rust/src/builtins/shared.rs @@ -1,51 +1,57 @@ -use crate::builtins::{printf, wait}; -use crate::common::str2wcstring; -use crate::ffi::separation_type_t; -use crate::ffi::{self, wcstring_list_ffi_t, Parser, Repin, RustBuiltin}; +use super::prelude::*; +use crate::builtins::*; +use crate::common::{escape, get_by_sorted_name, str2wcstring, Named}; +use crate::ffi; +use crate::ffi::Repin; +use crate::io::{IoChain, IoFd, OutputStream, OutputStreamFfi}; +use crate::parse_constants::UNKNOWN_BUILTIN_ERR_MSG; +use crate::parse_util::parse_util_argument_is_help; +use crate::parser::{Block, BlockType, LoopStatus}; +use crate::proc::{no_exec, ProcStatus}; use crate::wchar::{wstr, WString, L}; -use crate::wchar_ffi::{c_str, empty_wstring, ToCppWString, WCharFromFFI}; use crate::wgetopt::{wgetopter_t, wopt, woption, woption_argument_t}; -use cxx::{type_id, ExternType}; -use libc::c_int; -use libc::isatty; -use libc::STDOUT_FILENO; +use cxx::CxxWString; +use errno::errno; +use libc::{c_int, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO}; use std::borrow::Cow; use std::fs::File; use std::io::{BufRead, BufReader, Read}; -use std::os::fd::{FromRawFd, RawFd}; +use std::os::fd::FromRawFd; use std::pin::Pin; +use std::sync::Arc; +use widestring_suffix::widestrs; -pub type BuiltinCmd = fn(&mut Parser, &mut IoStreams, &mut [&wstr]) -> Option; +pub type BuiltinCmd = fn(&Parser, &mut IoStreams, &mut [&wstr]) -> Option; -#[cxx::bridge] -mod builtins_ffi { - extern "C++" { - include!("wutil.h"); - include!("parser.h"); - include!("builtin.h"); +/// The default prompt for the read command. +pub const DEFAULT_READ_PROMPT: &wstr = + L!("set_color green; echo -n read; set_color normal; echo -n \"> \""); - type wcharz_t = crate::ffi::wcharz_t; - type wcstring_list_ffi_t = crate::ffi::wcstring_list_ffi_t; - type Parser = crate::ffi::Parser; - type IoStreams = crate::ffi::IoStreams; - type RustBuiltin = crate::ffi::RustBuiltin; - } - extern "Rust" { - fn rust_run_builtin( - parser: Pin<&mut Parser>, - streams: Pin<&mut IoStreams>, - cpp_args: &wcstring_list_ffi_t, - builtin: RustBuiltin, - status_code: &mut i32, - ) -> bool; - } -} +/// Error message on missing argument. +pub const BUILTIN_ERR_MISSING: &str = "%ls: %ls: option requires an argument\n"; -unsafe impl ExternType for IoStreams { - type Id = type_id!("IoStreams"); - type Kind = cxx::kind::Opaque; -} +/// Error message on missing man page. +pub const BUILTIN_ERR_MISSING_HELP: &str = concat!( + "fish: %ls: missing man page\nDocumentation may not be installed.\n`help %ls` will ", + "show an online version\n" +); + +/// Error message on multiple scope levels for variables. +pub const BUILTIN_ERR_GLOCAL: &str = + "%ls: scope can be only one of: universal function global local\n"; + +/// Error message for specifying both export and unexport to set/read. +pub const BUILTIN_ERR_EXPUNEXP: &str = "%ls: cannot both export and unexport\n"; + +/// Error message for specifying both path and unpath to set/read. +pub const BUILTIN_ERR_PATHUNPATH: &str = "%ls: cannot both path and unpath\n"; + +/// Error message for unknown switch. +pub const BUILTIN_ERR_UNKNOWN: &str = "%ls: %ls: unknown option\n"; + +/// Error message for invalid bind mode name. +pub const BUILTIN_ERR_BIND_MODE: &str = "%ls: %ls: invalid mode name. See `help identifiers`\n"; /// Error message when too many arguments are supplied to a builtin. pub const BUILTIN_ERR_TOO_MANY_ARGUMENTS: &str = "%ls: too many arguments\n"; @@ -53,15 +59,10 @@ unsafe impl ExternType for IoStreams { /// Error message when integer expected pub const BUILTIN_ERR_NOT_NUMBER: &str = "%ls: %ls: invalid integer\n"; +/// Command that requires a subcommand was invoked without a recognized subcommand. pub const BUILTIN_ERR_MISSING_SUBCMD: &str = "%ls: missing subcommand\n"; pub const BUILTIN_ERR_INVALID_SUBCMD: &str = "%ls: %ls: invalid subcommand\n"; -/// Error message for unknown switch. -pub const BUILTIN_ERR_UNKNOWN: &str = "%ls: %ls: unknown option\n"; - -pub const BUILTIN_ERR_MISSING_HELP: &str = "fish: %ls: missing man page\nDocumentation may not be installed.\n`help %ls` will show an online version\n"; -pub const BUILTIN_ERR_MISSING: &str = "%ls: %ls: option requires an argument\n"; - /// Error messages for unexpected args. pub const BUILTIN_ERR_ARG_COUNT0: &str = "%ls: missing argument\n"; pub const BUILTIN_ERR_ARG_COUNT1: &str = "%ls: expected %d arguments; got %d\n"; @@ -77,6 +78,9 @@ unsafe impl ExternType for IoStreams { pub const BUILTIN_ERR_COMBO2: &str = "%ls: invalid option combination, %ls\n"; pub const BUILTIN_ERR_COMBO2_EXCLUSIVE: &str = "%ls: %ls %ls: options cannot be used together\n"; +/// The send stuff to foreground message. +pub const FG_MSG: &str = "Send job %d (%ls) to foreground\n"; + // Return values (`$status` values for fish scripts) for various situations. /// The status code used for normal exit in a command. @@ -103,95 +107,269 @@ unsafe impl ExternType for IoStreams { /// The status code when an expansion fails, for example, "$foo[" pub const STATUS_EXPAND_ERROR: Option = Some(121); -/// A wrapper around output_stream_t. -pub struct output_stream_t(*mut ffi::output_stream_t); - -impl output_stream_t { - /// \return the underlying output_stream_t. - fn ffi(&mut self) -> Pin<&mut ffi::output_stream_t> { - unsafe { (*self.0).pin() } - } - - /// Append a &wstr or WString. - pub fn append>(&mut self, s: Str) -> bool { - self.ffi().append(&s.as_ref().into_cpp()) - } - - /// Append a &wstr or WString with a newline - pub fn appendln(&mut self, s: impl Into) -> bool { - let s = s.into() + L!("\n"); - self.ffi().append(&s.into_cpp()) - } - - /// Append a char. - pub fn append1(&mut self, c: char) -> bool { - self.append(wstr::from_char_slice(&[c])) - } - - pub fn append_with_separation( - &mut self, - s: impl AsRef, - sep: separation_type_t, - want_newline: bool, - ) -> bool { - self.ffi() - .append_with_separation(&s.as_ref().into_cpp(), sep, want_newline) - } - - pub fn flush_and_check_error(&mut self) -> c_int { - self.ffi().flush_and_check_error().into() - } +/// Data structure to describe a builtin. +struct BuiltinData { + // Name of the builtin. + name: &'static wstr, + // Function pointer to the builtin implementation. + func: BuiltinCmd, } -// Convenience wrappers around C++ IoStreams. -pub struct IoStreams { - streams: *mut builtins_ffi::IoStreams, - pub out: output_stream_t, - pub err: output_stream_t, - pub out_is_redirected: bool, - pub err_is_redirected: bool, -} +// Data about all the builtin commands in fish. +// Functions that are bound to builtin_generic are handled directly by the parser. +// NOTE: These must be kept in sorted order! +#[widestrs] +const BUILTIN_DATAS: &[BuiltinData] = &[ + BuiltinData { + name: "."L, + func: source::source, + }, + BuiltinData { + name: ":"L, + func: builtin_true, + }, + BuiltinData { + name: "["L, // ] + func: test::test, + }, + BuiltinData { + name: "_"L, + func: builtin_gettext, + }, + BuiltinData { + name: "abbr"L, + func: abbr::abbr, + }, + BuiltinData { + name: "and"L, + func: builtin_generic, + }, + BuiltinData { + name: "argparse"L, + func: argparse::argparse, + }, + BuiltinData { + name: "begin"L, + func: builtin_generic, + }, + BuiltinData { + name: "bg"L, + func: bg::bg, + }, + BuiltinData { + name: "bind"L, + func: bind::bind, + }, + BuiltinData { + name: "block"L, + func: block::block, + }, + BuiltinData { + name: "break"L, + func: builtin_break_continue, + }, + BuiltinData { + name: "breakpoint"L, + func: builtin_breakpoint, + }, + BuiltinData { + name: "builtin"L, + func: builtin::builtin, + }, + BuiltinData { + name: "case"L, + func: builtin_generic, + }, + BuiltinData { + name: "cd"L, + func: cd::cd, + }, + BuiltinData { + name: "command"L, + func: command::command, + }, + BuiltinData { + name: "commandline"L, + func: commandline::commandline, + }, + BuiltinData { + name: "complete"L, + func: complete::complete, + }, + BuiltinData { + name: "contains"L, + func: contains::contains, + }, + BuiltinData { + name: "continue"L, + func: builtin_break_continue, + }, + BuiltinData { + name: "count"L, + func: count::count, + }, + BuiltinData { + name: "disown"L, + func: disown::disown, + }, + BuiltinData { + name: "echo"L, + func: echo::echo, + }, + BuiltinData { + name: "else"L, + func: builtin_generic, + }, + BuiltinData { + name: "emit"L, + func: emit::emit, + }, + BuiltinData { + name: "end"L, + func: builtin_generic, + }, + BuiltinData { + name: "eval"L, + func: eval::eval, + }, + BuiltinData { + name: "exec"L, + func: builtin_generic, + }, + BuiltinData { + name: "exit"L, + func: exit::exit, + }, + BuiltinData { + name: "false"L, + func: builtin_false, + }, + BuiltinData { + name: "fg"L, + func: fg::fg, + }, + BuiltinData { + name: "for"L, + func: builtin_generic, + }, + BuiltinData { + name: "function"L, + func: builtin_generic, + }, + BuiltinData { + name: "functions"L, + func: functions::functions, + }, + BuiltinData { + name: "history"L, + func: history::history, + }, + BuiltinData { + name: "if"L, + func: builtin_generic, + }, + BuiltinData { + name: "jobs"L, + func: jobs::jobs, + }, + BuiltinData { + name: "math"L, + func: math::math, + }, + BuiltinData { + name: "not"L, + func: builtin_generic, + }, + BuiltinData { + name: "or"L, + func: builtin_generic, + }, + BuiltinData { + name: "path"L, + func: path::path, + }, + BuiltinData { + name: "printf"L, + func: printf::printf, + }, + BuiltinData { + name: "pwd"L, + func: pwd::pwd, + }, + BuiltinData { + name: "random"L, + func: random::random, + }, + BuiltinData { + name: "read"L, + func: read::read, + }, + BuiltinData { + name: "realpath"L, + func: realpath::realpath, + }, + BuiltinData { + name: "return"L, + func: r#return::r#return, + }, + BuiltinData { + name: "set"L, + func: set::set, + }, + BuiltinData { + name: "set_color"L, + func: set_color::set_color, + }, + BuiltinData { + name: "source"L, + func: source::source, + }, + BuiltinData { + name: "status"L, + func: status::status, + }, + BuiltinData { + name: "string"L, + func: string::string, + }, + BuiltinData { + name: "switch"L, + func: builtin_generic, + }, + BuiltinData { + name: "test"L, + func: test::test, + }, + BuiltinData { + name: "time"L, + func: builtin_generic, + }, + BuiltinData { + name: "true"L, + func: builtin_true, + }, + BuiltinData { + name: "type"L, + func: r#type::r#type, + }, + BuiltinData { + name: "ulimit"L, + func: ulimit::ulimit, + }, + BuiltinData { + name: "wait"L, + func: wait::wait, + }, + BuiltinData { + name: "while"L, + func: builtin_generic, + }, +]; +assert_sorted_by_name!(BUILTIN_DATAS); -impl IoStreams { - pub fn new(mut streams: Pin<&mut builtins_ffi::IoStreams>) -> IoStreams { - let out = output_stream_t(streams.as_mut().get_out().unpin()); - let err = output_stream_t(streams.as_mut().get_err().unpin()); - let out_is_redirected = streams.as_mut().get_out_redirected(); - let err_is_redirected = streams.as_mut().get_err_redirected(); - let streams = streams.unpin(); - IoStreams { - streams, - out, - err, - out_is_redirected, - err_is_redirected, - } - } - - pub fn ffi_pin(&mut self) -> Pin<&mut builtins_ffi::IoStreams> { - unsafe { Pin::new_unchecked(&mut *self.streams) } - } - - pub fn ffi_ref(&self) -> &builtins_ffi::IoStreams { - unsafe { &*self.streams } - } - - pub fn out_is_terminal(&self) -> bool { - !self.out_is_redirected && unsafe { isatty(STDOUT_FILENO) == 1 } - } - - pub fn stdin_is_directly_redirected(&self) -> bool { - self.ffi_ref().ffi_stdin_is_directly_redirected() - } - - pub fn stdin_fd(&self) -> Option { - let ret = self.ffi_ref().ffi_stdin_fd().0; - - if ret < 0 { - None - } else { - Some(ret) - } +impl Named for BuiltinData { + fn name(&self) -> &'static wstr { + self.name } } @@ -199,116 +377,278 @@ pub fn stdin_fd(&self) -> Option { /// We truncate on NUL for backwards-compatibility reasons. /// This used to be passed as c-strings (`wchar_t *`), /// and so we act like it for now. -pub fn truncate_args_on_nul(args: &[WString]) -> Vec<&wstr> { - args.iter() - .map(|s| match s.chars().position(|c| c == '\0') { - Some(i) => &s[..i], - None => &s[..], - }) - .collect() +pub fn truncate_at_nul(s: &wstr) -> &wstr { + &s[..s.chars().position(|c| c == '\x00').unwrap_or(s.len())] } -fn rust_run_builtin( - parser: Pin<&mut Parser>, - streams: Pin<&mut builtins_ffi::IoStreams>, - cpp_args: &wcstring_list_ffi_t, - builtin: RustBuiltin, - status_code: &mut i32, -) -> bool { - let storage: Vec = cpp_args.from_ffi(); - let mut args: Vec<&wstr> = truncate_args_on_nul(&storage); - let streams = &mut IoStreams::new(streams); +fn builtin_lookup(name: &wstr) -> Option<&'static BuiltinData> { + get_by_sorted_name(name, BUILTIN_DATAS) +} - match run_builtin(parser.unpin(), streams, args.as_mut_slice(), builtin) { - None => false, - Some(status) => { - *status_code = status; - true +/// Is there a builtin command with the given name? +pub fn builtin_exists(name: &wstr) -> bool { + builtin_lookup(name).is_some() +} + +/// Is the command a keyword we need to special-case the handling of `-h` and `--help`. +#[widestrs] +fn cmd_needs_help(cmd: &wstr) -> bool { + [ + "for"L, + "while"L, + "function"L, + "if"L, + "end"L, + "switch"L, + "case"L, + ] + .contains(&cmd) +} + +/// Execute a builtin command +pub fn builtin_run(parser: &Parser, argv: &mut [&wstr], streams: &mut IoStreams) -> ProcStatus { + if argv.is_empty() { + return ProcStatus::from_exit_code(STATUS_INVALID_ARGS.unwrap()); + } + + // We can be handed a keyword by the parser as if it was a command. This happens when the user + // follows the keyword by `-h` or `--help`. Since it isn't really a builtin command we need to + // handle displaying help for it here. + if argv.len() == 2 && parse_util_argument_is_help(argv[1]) && cmd_needs_help(argv[0]) { + builtin_print_help(parser, streams, argv[0]); + return ProcStatus::from_exit_code(STATUS_CMD_OK.unwrap()); + } + + let Some(builtin) = builtin_lookup(argv[0]) else { + FLOGF!(error, "%s", wgettext_fmt!(UNKNOWN_BUILTIN_ERR_MSG, argv[0])); + return ProcStatus::from_exit_code(STATUS_CMD_ERROR.unwrap()); + }; + + let builtin_ret = (builtin.func)(parser, streams, argv); + + // Flush our out and error streams, and check for their errors. + let out_ret = streams.out.flush_and_check_error(); + let err_ret = streams.err.flush_and_check_error(); + + // Resolve our status code. + // If the builtin itself produced an error, use that error. + // Otherwise use any errors from writing to out and writing to err, in that order. + let mut code = builtin_ret.unwrap_or(0); + if code == 0 { + code = out_ret; + } + if code == 0 { + code = err_ret; + } + + // The exit code is cast to an 8-bit unsigned integer, so saturate to 255. Otherwise, + // multiples of 256 are reported as 0. + if code > 255 { + code = 255; + } + + // Handle the case of an empty status. + if code == 0 && builtin_ret.is_none() { + return ProcStatus::empty(); + } + if code < 0 { + // If the code is below 0, constructing a proc_status_t + // would assert() out, which is a terrible failure mode + // So instead, what we do is we get a positive code, + // and we avoid 0. + code = ((256 + code) % 256).abs(); + if code == 0 { + code = 255; } + FLOGF!( + warning, + "builtin %ls returned invalid exit code %d", + argv[0], + code + ); } + + ProcStatus::from_exit_code(code) } -pub fn run_builtin( - parser: &mut Parser, - streams: &mut IoStreams, - args: &mut [&wstr], - builtin: RustBuiltin, -) -> Option { - match builtin { - RustBuiltin::Abbr => super::abbr::abbr(parser, streams, args), - RustBuiltin::Argparse => super::argparse::argparse(parser, streams, args), - RustBuiltin::Bg => super::bg::bg(parser, streams, args), - RustBuiltin::Block => super::block::block(parser, streams, args), - RustBuiltin::Builtin => super::builtin::builtin(parser, streams, args), - RustBuiltin::Cd => super::cd::cd(parser, streams, args), - RustBuiltin::Contains => super::contains::contains(parser, streams, args), - RustBuiltin::Command => super::command::command(parser, streams, args), - RustBuiltin::Count => super::count::count(parser, streams, args), - RustBuiltin::Echo => super::echo::echo(parser, streams, args), - RustBuiltin::Emit => super::emit::emit(parser, streams, args), - RustBuiltin::Exit => super::exit::exit(parser, streams, args), - RustBuiltin::Functions => super::functions::functions(parser, streams, args), - RustBuiltin::Math => super::math::math(parser, streams, args), - RustBuiltin::Path => super::path::path(parser, streams, args), - RustBuiltin::Pwd => super::pwd::pwd(parser, streams, args), - RustBuiltin::Random => super::random::random(parser, streams, args), - RustBuiltin::Realpath => super::realpath::realpath(parser, streams, args), - RustBuiltin::Return => super::r#return::r#return(parser, streams, args), - RustBuiltin::SetColor => super::set_color::set_color(parser, streams, args), - RustBuiltin::Status => super::status::status(parser, streams, args), - RustBuiltin::String => super::string::string(parser, streams, args), - RustBuiltin::Test => super::test::test(parser, streams, args), - RustBuiltin::Type => super::r#type::r#type(parser, streams, args), - RustBuiltin::Wait => wait::wait(parser, streams, args), - RustBuiltin::Printf => printf::printf(parser, streams, args), - } +/// Returns a list of all builtin names. +pub fn builtin_get_names() -> impl Iterator { + BUILTIN_DATAS.iter().map(|builtin| builtin.name) } -// Covers of these functions that take care of the pinning, etc. -// These all return STATUS_INVALID_ARGS. -pub fn builtin_missing_argument( - parser: &mut Parser, +/// Return a one-line description of the specified builtin. +pub fn builtin_get_desc(name: &wstr) -> Option<&'static wstr> { + let desc = match name { + _ if name == "." => wgettext!("Evaluate contents of file"), + _ if name == ":" => wgettext!("Return a successful result"), + _ if name == "[" => wgettext!("Test a condition"), // ] + _ if name == "_" => wgettext!("Translate a string"), + _ if name == "abbr" => wgettext!("Manage abbreviations"), + _ if name == "and" => wgettext!("Run command if last command succeeded"), + _ if name == "argparse" => wgettext!("Parse options in fish script"), + _ if name == "begin" => wgettext!("Create a block of code"), + _ if name == "bg" => wgettext!("Send job to background"), + _ if name == "bind" => wgettext!("Handle fish key bindings"), + _ if name == "block" => wgettext!("Temporarily block delivery of events"), + _ if name == "break" => wgettext!("Stop the innermost loop"), + _ if name == "breakpoint" => wgettext!("Halt execution and start debug prompt"), + _ if name == "builtin" => wgettext!("Run a builtin specifically"), + _ if name == "case" => wgettext!("Block of code to run conditionally"), + _ if name == "cd" => wgettext!("Change working directory"), + _ if name == "command" => wgettext!("Run a command specifically"), + _ if name == "commandline" => wgettext!("Set or get the commandline"), + _ if name == "complete" => wgettext!("Edit command specific completions"), + _ if name == "contains" => wgettext!("Search for a specified string in a list"), + _ if name == "continue" => wgettext!("Skip over remaining innermost loop"), + _ if name == "count" => wgettext!("Count the number of arguments"), + _ if name == "disown" => wgettext!("Remove job from job list"), + _ if name == "echo" => wgettext!("Print arguments"), + _ if name == "else" => wgettext!("Evaluate block if condition is false"), + _ if name == "emit" => wgettext!("Emit an event"), + _ if name == "end" => wgettext!("End a block of commands"), + _ if name == "eval" => wgettext!("Evaluate a string as a statement"), + _ if name == "exec" => wgettext!("Run command in current process"), + _ if name == "exit" => wgettext!("Exit the shell"), + _ if name == "false" => wgettext!("Return an unsuccessful result"), + _ if name == "fg" => wgettext!("Send job to foreground"), + _ if name == "for" => wgettext!("Perform a set of commands multiple times"), + _ if name == "function" => wgettext!("Define a new function"), + _ if name == "functions" => wgettext!("List or remove functions"), + _ if name == "history" => wgettext!("History of commands executed by user"), + _ if name == "if" => wgettext!("Evaluate block if condition is true"), + _ if name == "jobs" => wgettext!("Print currently running jobs"), + _ if name == "math" => wgettext!("Evaluate math expressions"), + _ if name == "not" => wgettext!("Negate exit status of job"), + _ if name == "or" => wgettext!("Execute command if previous command failed"), + _ if name == "path" => wgettext!("Handle paths"), + _ if name == "printf" => wgettext!("Prints formatted text"), + _ if name == "pwd" => wgettext!("Print the working directory"), + _ if name == "random" => wgettext!("Generate random number"), + _ if name == "read" => wgettext!("Read a line of input into variables"), + _ if name == "realpath" => wgettext!("Show absolute path sans symlinks"), + _ if name == "return" => wgettext!("Stop the currently evaluated function"), + _ if name == "set" => wgettext!("Handle environment variables"), + _ if name == "set_color" => wgettext!("Set the terminal color"), + _ if name == "source" => wgettext!("Evaluate contents of file"), + _ if name == "status" => wgettext!("Return status information about fish"), + _ if name == "string" => wgettext!("Manipulate strings"), + _ if name == "switch" => wgettext!("Conditionally run blocks of code"), + _ if name == "test" => wgettext!("Test a condition"), + _ if name == "time" => wgettext!("Measure how long a command or block takes"), + _ if name == "true" => wgettext!("Return a successful result"), + _ if name == "type" => wgettext!("Check if a thing is a thing"), + _ if name == "ulimit" => wgettext!("Get/set resource usage limits"), + _ if name == "wait" => wgettext!("Wait for background processes completed"), + _ if name == "while" => wgettext!("Perform a command multiple times"), + _ => return None, + }; + Some(desc) +} + +/// Display help/usage information for the specified builtin or function from manpage +/// +/// @param name +/// builtin or function name to get up help for +/// +/// Process and print help for the specified builtin or function. +pub fn builtin_print_help(parser: &Parser, streams: &mut IoStreams, cmd: &wstr) { + builtin_print_help_error(parser, streams, cmd, L!("")) +} + +pub fn builtin_print_help_error( + parser: &Parser, streams: &mut IoStreams, cmd: &wstr, - opt: &wstr, - print_hints: bool, + error_message: &wstr, ) { - ffi::builtin_missing_argument( - parser.pin(), - streams.ffi_pin(), - c_str!(cmd), - c_str!(opt), - print_hints, - ); + // This won't ever work if no_exec is set. + if no_exec() { + return; + } + let name_esc = escape(cmd); + let mut cmd = sprintf!("__fish_print_help %ls ", &name_esc); + let mut ios = IoChain::new(); + if !error_message.is_empty() { + cmd.push_utfstr(&escape(error_message)); + // If it's an error, redirect the output of __fish_print_help to stderr + ios.push(Arc::new(IoFd::new(STDOUT_FILENO, STDERR_FILENO))); + } + let res = parser.eval(&cmd, &ios); + if res.status.normal_exited() && res.status.exit_code() == 2 { + streams + .err + .append(wgettext_fmt!(BUILTIN_ERR_MISSING_HELP, name_esc, name_esc)); + } } +/// Perform error reporting for encounter with unknown option. pub fn builtin_unknown_option( - parser: &mut Parser, + parser: &Parser, streams: &mut IoStreams, cmd: &wstr, opt: &wstr, - print_hints: bool, + print_hints: bool, /*=true*/ ) { - ffi::builtin_unknown_option( - parser.pin(), - streams.ffi_pin(), - c_str!(cmd), - c_str!(opt), - print_hints, - ); + streams + .err + .append(wgettext_fmt!(BUILTIN_ERR_UNKNOWN, cmd, opt)); + if print_hints { + builtin_print_error_trailer(parser, streams.err, cmd); + } } -pub fn builtin_print_help(parser: &mut Parser, streams: &IoStreams, cmd: &wstr) { - ffi::builtin_print_help( - parser.pin(), - streams.ffi_ref(), - c_str!(cmd), - empty_wstring(), - ); +/// Perform error reporting for encounter with missing argument. +pub fn builtin_missing_argument( + parser: &Parser, + streams: &mut IoStreams, + cmd: &wstr, + mut opt: &wstr, + print_hints: bool, /*=true*/ +) { + if opt.char_at(0) == '-' && opt.char_at(1) != '-' { + // if c in -qc '-qc' is missing the argument, now opt is just 'c' + opt = &opt[opt.len() - 1..]; + streams.err.append(wgettext_fmt!( + BUILTIN_ERR_MISSING, + cmd, + L!("-").to_owned() + opt + )); + } else { + streams + .err + .append(wgettext_fmt!(BUILTIN_ERR_MISSING, cmd, opt)); + } + if print_hints { + builtin_print_error_trailer(parser, streams.err, cmd); + } } -pub fn builtin_print_error_trailer(parser: &mut Parser, streams: &mut IoStreams, cmd: &wstr) { - ffi::builtin_print_error_trailer(parser.pin(), streams.err.ffi(), c_str!(cmd)); +/// Print the backtrace and call for help that we use at the end of error messages. +pub fn builtin_print_error_trailer(parser: &Parser, b: &mut OutputStream, cmd: &wstr) { + b.push('\n'); + let stacktrace = parser.current_line(); + // Don't print two empty lines if we don't have a stacktrace. + if !stacktrace.is_empty() { + b.append(stacktrace); + b.push('\n'); + } + b.append(wgettext_fmt!( + "(Type 'help %ls' for related documentation)\n", + cmd + )); +} + +/// This function works like perror, but it prints its result into the streams.err string instead +/// to stderr. Used by the builtin commands. +pub fn builtin_wperror(program_name: &wstr, streams: &mut IoStreams) { + let err = errno(); + streams.err.append(program_name); + streams.err.append(L!(": ")); + if err.0 != 0 { + let werr = WString::from_str(&err.to_string()); + streams.err.append(werr); + streams.err.push('\n'); + } } pub struct HelpOnlyCmdOpts { @@ -319,7 +659,7 @@ pub struct HelpOnlyCmdOpts { impl HelpOnlyCmdOpts { pub fn parse( args: &mut [&wstr], - parser: &mut Parser, + parser: &Parser, streams: &mut IoStreams, ) -> Result> { let cmd = args[0]; @@ -403,11 +743,9 @@ pub fn new( streams: &mut IoStreams, chunk_size: usize, ) -> Self { - let reader = streams.stdin_is_directly_redirected().then(|| { - let stdin_fd = streams - .stdin_fd() - .filter(|&fd| fd >= 0) - .expect("should have a valid fd"); + let reader = streams.stdin_is_directly_redirected.then(|| { + let stdin_fd = streams.stdin_fd; + assert!(stdin_fd >= 0, "should have a valid fd"); // safety: this should be a valid fd, and already open let fd = unsafe { File::from_raw_fd(stdin_fd) }; BufReader::with_capacity(chunk_size, fd) @@ -501,3 +839,275 @@ fn next(&mut self) -> Option { return Some(retval); } } + +/// A generic builtin that only supports showing a help message. This is only a placeholder that +/// prints the help message. Useful for commands that live in the parser. +fn builtin_generic(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Option { + let argc = argv.len(); + let opts = match HelpOnlyCmdOpts::parse(argv, parser, streams) { + Ok(opts) => opts, + Err(err) => return err, + }; + + if opts.print_help { + builtin_print_help(parser, streams, argv[0]); + return STATUS_CMD_OK; + } + + // Hackish - if we have no arguments other than the command, we are a "naked invocation" and we + // just print help. + if argc == 1 || argv[0] == L!("time") { + builtin_print_help(parser, streams, argv[0]); + return STATUS_INVALID_ARGS; + } + + STATUS_CMD_ERROR +} + +/// This function handles both the 'continue' and the 'break' builtins that are used for loop +/// control. +fn builtin_break_continue( + parser: &Parser, + streams: &mut IoStreams, + argv: &mut [&wstr], +) -> Option { + let is_break = argv[0] == L!("break"); + let argc = argv.len(); + + if argc != 1 { + let error_message = wgettext_fmt!(BUILTIN_ERR_UNKNOWN, argv[0], argv[1]); + builtin_print_help_error(parser, streams, argv[0], &error_message); + return STATUS_INVALID_ARGS; + } + + // Paranoia: ensure we have a real loop. + // This is checked in the AST but we may be invoked dynamically, e.g. just via "eval break". + let mut has_loop = false; + for b in parser.blocks().iter().rev() { + if [BlockType::while_block, BlockType::for_block].contains(&b.typ()) { + has_loop = true; + break; + } + if b.is_function_call() { + break; + } + } + if !has_loop { + let error_message = wgettext_fmt!("%ls: Not inside of loop\n", argv[0]); + builtin_print_help_error(parser, streams, argv[0], &error_message); + return STATUS_CMD_ERROR; + } + + // Mark the status in the libdata. + parser.libdata_mut().pods.loop_status = if is_break { + LoopStatus::breaks + } else { + LoopStatus::continues + }; + STATUS_CMD_OK +} + +/// Implementation of the builtin breakpoint command, used to launch the interactive debugger. +fn builtin_breakpoint( + parser: &Parser, + streams: &mut IoStreams, + argv: &mut [&wstr], +) -> Option { + let cmd = argv[0]; + if argv.len() != 1 { + streams.err.append(wgettext_fmt!( + BUILTIN_ERR_ARG_COUNT1, + cmd, + 0, + argv.len() - 1 + )); + return STATUS_INVALID_ARGS; + } + + // If we're not interactive then we can't enter the debugger. So treat this command as a no-op. + if !parser.is_interactive() { + return STATUS_CMD_ERROR; + } + + // Ensure we don't allow creating a breakpoint at an interactive prompt. There may be a simpler + // or clearer way to do this but this works. + { + if parser + .block_at_index(1) + .map_or(true, |b| b.typ() == BlockType::breakpoint) + { + streams.err.append(wgettext_fmt!( + "%ls: Command not valid at and interactive prompt\n", + cmd, + )); + return STATUS_ILLEGAL_CMD; + } + } + + let bpb = parser.push_block(Block::breakpoint_block()); + let mut empty_io_chain = IoChain::new(); + let io_chain = if streams.io_chain.is_null() { + &mut empty_io_chain + } else { + unsafe { &mut *streams.io_chain } + }; + ffi::reader_read_ffi( + parser as *const Parser as *const autocxx::c_void, + autocxx::c_int(STDIN_FILENO), + &io_chain as *const _ as *const autocxx::c_void, + ); + parser.pop_block(bpb); + Some(parser.get_last_status()) +} + +fn builtin_true(_parser: &Parser, _streams: &mut IoStreams, _argv: &mut [&wstr]) -> Option { + STATUS_CMD_OK +} + +fn builtin_false(_parser: &Parser, _streams: &mut IoStreams, _argv: &mut [&wstr]) -> Option { + STATUS_CMD_ERROR +} + +fn builtin_gettext(_parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Option { + for arg in &argv[1..] { + streams.out.append(wgettext_str(arg)); + } + STATUS_CMD_OK +} + +#[cxx::bridge] +#[allow(clippy::needless_lifetimes)] +mod builtins_ffi { + extern "C++" { + include!("io.h"); + include!("parser.h"); + type IoStreams<'a> = crate::io::IoStreams<'a>; + type OutputStreamFfi<'a> = crate::io::OutputStreamFfi<'a>; + type Parser = crate::parser::Parser; + } + extern "Rust" { + #[cxx_name = "builtin_print_help"] + unsafe fn builtin_print_help_ffi<'a>( + parser: &Parser, + streams: Pin<&mut IoStreams<'a>>, + name: &CxxWString, + ); + #[cxx_name = "builtin_print_help_error"] + unsafe fn builtin_print_help_error_ffi<'a>( + parser: &Parser, + streams: Pin<&mut IoStreams<'a>>, + name: &CxxWString, + error_message: &CxxWString, + ); + #[cxx_name = "builtin_unknown_option"] + unsafe fn builtin_unknown_option_ffi<'a>( + parser: &Parser, + streams: Pin<&mut IoStreams<'a>>, + cmd: &CxxWString, + opt: &CxxWString, + print_hints: bool, + ); + #[cxx_name = "builtin_missing_argument"] + fn builtin_missing_argument_ffi( + parser: &Parser, + streams: Pin<&mut IoStreams>, + cmd: &CxxWString, + opt: &CxxWString, + print_hints: bool, + ); + #[cxx_name = "builtin_print_error_trailer"] + unsafe fn builtin_print_error_trailer_ffi<'a>( + parser: &Parser, + b: Pin<&mut OutputStreamFfi<'a>>, + cmd: &CxxWString, + ); + } +} + +pub fn run_builtin_ffi( + builtin_fn: fn( + *const autocxx::c_void, + *mut autocxx::c_void, + *mut autocxx::c_void, + ) -> autocxx::c_int, + parser: &Parser, + streams: &mut IoStreams, + args: &mut [&wstr], +) -> Option { + let mut zstrings = vec![]; + for arg in args { + let mut zstring: Vec = arg.chars().collect(); + zstring.push('\0'); + zstrings.push(zstring); + } + let mut zstrs = vec![]; + for zstring in &zstrings { + zstrs.push(zstring.as_ptr()); + } + zstrs.push(std::ptr::null()); + let args = zstrs.as_mut_ptr(); + let ret = (builtin_fn)( + parser as *const Parser as *const autocxx::c_void, + streams as *mut IoStreams as *mut autocxx::c_void, + args.cast(), + ); + Some(i32::from(ret)) +} + +fn builtin_print_help_ffi(parser: &Parser, streams: Pin<&mut IoStreams>, name: &CxxWString) { + builtin_print_help(parser, streams.unpin(), name.as_wstr()) +} + +fn builtin_print_help_error_ffi( + parser: &Parser, + streams: Pin<&mut IoStreams>, + name: &CxxWString, + error_message: &CxxWString, +) { + builtin_print_help_error( + parser, + streams.unpin(), + name.as_wstr(), + error_message.as_wstr(), + ) +} + +fn builtin_unknown_option_ffi( + parser: &Parser, + streams: Pin<&mut IoStreams>, + cmd: &CxxWString, + opt: &CxxWString, + print_hints: bool, +) { + builtin_unknown_option( + parser, + streams.unpin(), + cmd.as_wstr(), + opt.as_wstr(), + print_hints, + ); +} + +fn builtin_missing_argument_ffi( + parser: &Parser, + streams: Pin<&mut IoStreams>, + cmd: &CxxWString, + opt: &CxxWString, + print_hints: bool, +) { + builtin_missing_argument( + parser, + streams.unpin(), + cmd.as_wstr(), + opt.as_wstr(), + print_hints, + ); +} + +fn builtin_print_error_trailer_ffi( + parser: &Parser, + b: Pin<&mut OutputStreamFfi>, + cmd: &CxxWString, +) { + builtin_print_error_trailer(parser, b.unpin().0, cmd.as_wstr()) +} diff --git a/fish-rust/src/builtins/source.rs b/fish-rust/src/builtins/source.rs new file mode 100644 index 000000000..080553f15 --- /dev/null +++ b/fish-rust/src/builtins/source.rs @@ -0,0 +1,132 @@ +use crate::{ + common::{escape, scoped_push_replacer, FilenameRef}, + fds::{wopen_cloexec, AutoCloseFd}, + ffi::reader_read_ffi, + io::IoChain, + parser::Block, +}; +use libc::{c_int, O_RDONLY, S_IFMT, S_IFREG}; + +use super::prelude::*; + +/// The source builtin, sometimes called `.`. Evaluates the contents of a file in the current +/// context. +pub fn source(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { + let argc = args.len(); + + let opts = match HelpOnlyCmdOpts::parse(args, parser, streams) { + Ok(opts) => opts, + Err(err) => return err, + }; + let cmd = args[0]; + + if opts.print_help { + builtin_print_help(parser, streams, cmd); + return STATUS_CMD_OK; + } + + // If we open a file, this ensures we close it. + let opened_fd; + + // The fd that we read from, either from opened_fd or stdin. + let fd; + let func_filename; + let optind = opts.optind; + + if argc == optind || args[optind] == L!("-") { + if streams.stdin_fd < 0 { + streams + .err + .append(wgettext_fmt!("%ls: stdin is closed\n", cmd)); + return STATUS_CMD_ERROR; + } + // Either a bare `source` which means to implicitly read from stdin or an explicit `-`. + if argc == optind && unsafe { libc::isatty(streams.stdin_fd) } != 0 { + // Don't implicitly read from the terminal. + return STATUS_CMD_ERROR; + } + func_filename = FilenameRef::new(L!("-").to_owned()); + fd = streams.stdin_fd; + } else { + opened_fd = AutoCloseFd::new(wopen_cloexec(args[optind], O_RDONLY, 0)); + if !opened_fd.is_valid() { + let esc = escape(args[optind]); + streams.err.append(wgettext_fmt!( + "%ls: Error encountered while sourcing file '%ls':\n", + cmd, + &esc + )); + builtin_wperror(cmd, streams); + return STATUS_CMD_ERROR; + } + + fd = opened_fd.fd(); + let mut buf: libc::stat = unsafe { std::mem::zeroed() }; + if unsafe { libc::fstat(fd, &mut buf) } == -1 { + let esc = escape(args[optind]); + streams.err.append(wgettext_fmt!( + "%ls: Error encountered while sourcing file '%ls':\n", + cmd, + &esc + )); + return STATUS_CMD_ERROR; + } + + if buf.st_mode & S_IFMT != S_IFREG { + let esc = escape(args[optind]); + streams + .err + .append(wgettext_fmt!("%ls: '%ls' is not a file\n", cmd, esc)); + return STATUS_CMD_ERROR; + } + + func_filename = FilenameRef::new(args[optind].to_owned()); + } + + assert!(fd >= 0, "Should have a valid fd"); + + let sb = parser.push_block(Block::source_block(func_filename.clone())); + let _filename_push = scoped_push_replacer( + |new_value| std::mem::replace(&mut parser.libdata_mut().current_filename, new_value), + Some(func_filename.clone()), + ); + + // Construct argv from our null-terminated list. + // This is slightly subtle. If this is a bare `source` with no args then `argv + optind` already + // points to the end of argv. Otherwise we want to skip the file name to get to the args if any. + let mut argv_list: Vec = vec![]; + let remaining_args = &args[optind + if argc == optind { 0 } else { 1 }..]; + for arg in remaining_args.iter().copied() { + argv_list.push(arg.to_owned()); + } + parser.vars().set_argv(argv_list); + + let empty_io_chain = IoChain::new(); + let retval = reader_read_ffi( + parser as *const Parser as *const autocxx::c_void, + unsafe { std::mem::transmute(fd) }, + if !streams.io_chain.is_null() { + unsafe { &*streams.io_chain } + } else { + &empty_io_chain + } as *const _ as *const autocxx::c_void, + ); + let mut retval: c_int = unsafe { std::mem::transmute(retval) }; + + parser.pop_block(sb); + + if retval != STATUS_CMD_OK.unwrap() { + let esc = escape(&func_filename); + streams.err.append(wgettext_fmt!( + "%ls: Error while reading file '%ls'\n", + cmd, + if esc == "-" { L!("") } else { &esc } + )); + } else { + retval = parser.get_last_status(); + } + + // Do not close fd after calling reader_read. reader_read automatically closes it before calling + // eval. + Some(retval) +} diff --git a/fish-rust/src/builtins/status.rs b/fish-rust/src/builtins/status.rs index 8e1baa722..fef875aed 100644 --- a/fish-rust/src/builtins/status.rs +++ b/fish-rust/src/builtins/status.rs @@ -2,10 +2,10 @@ use super::prelude::*; use crate::common::{get_executable_path, str2wcstring, PROGRAM_NAME}; -use crate::ffi::{ - get_job_control_mode, get_login, is_interactive_session, job_control_t, set_job_control_mode, -}; use crate::future_feature_flags::{self as features, feature_test}; +use crate::proc::{ + get_job_control_mode, get_login, is_interactive_session, set_job_control_mode, JobControl, +}; use crate::wutil::{waccess, wbasename, wdirname, wrealpath, Error}; use libc::F_OK; use nix::errno::Errno; @@ -114,7 +114,7 @@ enum TestFeatureRetVal { struct StatusCmdOpts { level: i32, - new_job_control_mode: Option, + new_job_control_mode: Option, status_cmd: Option, print_help: bool, } @@ -191,7 +191,7 @@ fn parse_cmd_opts( opts: &mut StatusCmdOpts, optind: &mut usize, args: &mut [&wstr], - parser: &mut Parser, + parser: &Parser, streams: &mut IoStreams, ) -> Option { let cmd = args[0]; @@ -305,7 +305,7 @@ fn parse_cmd_opts( return STATUS_CMD_OK; } -pub fn status(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { +pub fn status(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { let cmd = args[0]; let argc = args.len(); @@ -357,14 +357,14 @@ pub fn status(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) streams.out.append(wgettext!("This is not a login shell\n")); } let job_control_mode = match get_job_control_mode() { - job_control_t::interactive => wgettext!("Only on interactive jobs"), - job_control_t::none => wgettext!("Never"), - job_control_t::all => wgettext!("Always"), + JobControl::interactive => wgettext!("Only on interactive jobs"), + JobControl::none => wgettext!("Never"), + JobControl::all => wgettext!("Always"), }; streams .out .append(wgettext_fmt!("Job control: %ls\n", job_control_mode)); - streams.out.append(parser.stack_trace().as_wstr()); + streams.out.append(parser.stack_trace()); return STATUS_CMD_OK; }; @@ -447,17 +447,18 @@ pub fn status(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) } match s { STATUS_BASENAME | STATUS_DIRNAME | STATUS_FILENAME => { - let res = parser.current_filename_ffi().from_ffi(); - let f = match (res.is_empty(), s) { - (false, STATUS_DIRNAME) => wdirname(&res), - (false, STATUS_BASENAME) => wbasename(&res), + let res = parser.current_filename(); + let function = res.unwrap_or_default(); + let f = match (function.is_empty(), s) { + (false, STATUS_DIRNAME) => wdirname(&function), + (false, STATUS_BASENAME) => wbasename(&function), (true, _) => wgettext!("Standard input"), - (false, _) => &res, + (false, _) => &function, }; streams.out.appendln(f); } STATUS_FUNCTION => { - let f = match parser.get_func_name(opts.level) { + let f = match parser.get_function_name(opts.level) { Some(f) => f, None => wgettext!("Not a function").to_owned(), }; @@ -467,7 +468,9 @@ pub fn status(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) // TBD is how to interpret the level argument when fetching the line number. // See issue #4161. // streams.out.append_format(L"%d\n", parser.get_lineno(opts.level)); - streams.out.appendln(parser.get_lineno().0.to_wstring()); + streams + .out + .appendln(parser.get_lineno().unwrap_or(0).to_wstring()); } STATUS_IS_INTERACTIVE => { if is_interactive_session() { @@ -477,7 +480,7 @@ pub fn status(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) } } STATUS_IS_COMMAND_SUB => { - if parser.libdata_pod().is_subshell { + if parser.libdata().pods.is_subshell { return STATUS_CMD_OK; } else { return STATUS_CMD_ERROR; @@ -505,44 +508,42 @@ pub fn status(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) } } STATUS_IS_FULL_JOB_CTRL => { - if get_job_control_mode() == job_control_t::all { + if get_job_control_mode() == JobControl::all { return STATUS_CMD_OK; } else { return STATUS_CMD_ERROR; } } STATUS_IS_INTERACTIVE_JOB_CTRL => { - if get_job_control_mode() == job_control_t::interactive { + if get_job_control_mode() == JobControl::interactive { return STATUS_CMD_OK; } else { return STATUS_CMD_ERROR; } } STATUS_IS_NO_JOB_CTRL => { - if get_job_control_mode() == job_control_t::none { + if get_job_control_mode() == JobControl::none { return STATUS_CMD_OK; } else { return STATUS_CMD_ERROR; } } STATUS_STACK_TRACE => { - streams.out.append(parser.stack_trace().as_wstr()); + streams.out.append(parser.stack_trace()); } STATUS_CURRENT_CMD => { - let var = parser.pin().libdata().get_status_vars_command().from_ffi(); - if !var.is_empty() { - streams.out.appendln(var); + let command = &parser.libdata().status_vars.command; + if !command.is_empty() { + streams.out.append(command); } else { streams.out.appendln(*PROGRAM_NAME.get().unwrap()); } + streams.out.append_char('\n'); } STATUS_CURRENT_COMMANDLINE => { - let var = parser - .pin() - .libdata() - .get_status_vars_commandline() - .from_ffi(); - streams.out.appendln(var); + let commandline = &parser.libdata().status_vars.commandline; + streams.out.append(commandline); + streams.out.append_char('\n'); } STATUS_FISH_PATH => { let path = get_executable_path("fish"); @@ -563,7 +564,8 @@ pub fn status(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) _ => path, }; - streams.out.appendln(real); + streams.out.append(real); + streams.out.append_char('\n'); } else { // This is a relative path, we can't canonicalize it let path = str2wcstring(path.as_os_str().as_bytes()); diff --git a/fish-rust/src/builtins/string.rs b/fish-rust/src/builtins/string.rs index 2a5342864..3f25141ff 100644 --- a/fish-rust/src/builtins/string.rs +++ b/fish-rust/src/builtins/string.rs @@ -1,7 +1,6 @@ use crate::wcstringutil::fish_wcwidth_visible; // Forward some imports to make subcmd implementations easier use super::prelude::*; -use crate::ffi::separation_type_t; mod collect; mod escape; @@ -31,9 +30,9 @@ macro_rules! string_error { } use string_error; -fn string_unknown_option(parser: &mut Parser, streams: &mut IoStreams, subcmd: &wstr, opt: &wstr) { +fn string_unknown_option(parser: &Parser, streams: &mut IoStreams, subcmd: &wstr, opt: &wstr) { string_error!(streams, BUILTIN_ERR_UNKNOWN, subcmd, opt); - builtin_print_error_trailer(parser, streams, L!("string")); + builtin_print_error_trailer(parser, streams.err, L!("string")); } trait StringSubCommand<'args> { @@ -51,7 +50,7 @@ fn parse_opt( fn parse_opts( &mut self, args: &mut [&'args wstr], - parser: &mut Parser, + parser: &Parser, streams: &mut IoStreams, ) -> Result> { let cmd = args[0]; @@ -97,7 +96,7 @@ fn take_args( /// Perform the business logic of the command. fn handle( &mut self, - parser: &mut Parser, + parser: &Parser, streams: &mut IoStreams, optind: &mut usize, args: &[&'args wstr], @@ -105,7 +104,7 @@ fn handle( fn run( &mut self, - parser: &mut Parser, + parser: &Parser, streams: &mut IoStreams, args: &mut [&'args wstr], ) -> Option { @@ -127,7 +126,7 @@ fn run( return retval; } - if streams.stdin_is_directly_redirected() && args.len() > optind { + if streams.stdin_is_directly_redirected && args.len() > optind { string_error!(streams, BUILTIN_ERR_TOO_MANY_ARGUMENTS, args[0]); return STATUS_INVALID_ARGS; } @@ -199,7 +198,7 @@ impl StringError { fn print_error( &self, args: &[&wstr], - parser: &mut Parser, + parser: &Parser, streams: &mut IoStreams, optarg: Option<&wstr>, optind: usize, @@ -291,7 +290,7 @@ fn arguments<'iter, 'args>( } /// The string builtin, for manipulating strings. -pub fn string(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { +pub fn string(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { let cmd = args[0]; let argc = args.len(); @@ -299,7 +298,7 @@ pub fn string(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) streams .err .append(wgettext_fmt!(BUILTIN_ERR_MISSING_SUBCMD, cmd)); - builtin_print_error_trailer(parser, streams, cmd); + builtin_print_error_trailer(parser, streams.err, cmd); return STATUS_INVALID_ARGS; } @@ -320,13 +319,11 @@ pub fn string(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) cmd.run(parser, streams, args) } "length" => length::Length::default().run(parser, streams, args), - "lower" => { - let mut cmd = transform::Transform { - quiet: false, - func: wstr::to_lowercase, - }; - cmd.run(parser, streams, args) + "lower" => transform::Transform { + quiet: false, + func: wstr::to_lowercase, } + .run(parser, streams, args), "match" => r#match::Match::default().run(parser, streams, args), "pad" => pad::Pad::default().run(parser, streams, args), "repeat" => repeat::Repeat::default().run(parser, streams, args), @@ -341,19 +338,17 @@ pub fn string(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) "sub" => sub::Sub::default().run(parser, streams, args), "trim" => trim::Trim::default().run(parser, streams, args), "unescape" => unescape::Unescape::default().run(parser, streams, args), - "upper" => { - let mut cmd = transform::Transform { - quiet: false, - func: wstr::to_uppercase, - }; - cmd.run(parser, streams, args) + "upper" => transform::Transform { + quiet: false, + func: wstr::to_uppercase, } + .run(parser, streams, args), _ => { streams .err - .append(wgettext_fmt!(BUILTIN_ERR_INVALID_SUBCMD, cmd, subcmd_name)); - builtin_print_error_trailer(parser, streams, cmd); - STATUS_INVALID_ARGS + .append(wgettext_fmt!(BUILTIN_ERR_INVALID_SUBCMD, cmd, args[0])); + builtin_print_error_trailer(parser, streams.err, cmd); + return STATUS_INVALID_ARGS; } } } diff --git a/fish-rust/src/builtins/string/collect.rs b/fish-rust/src/builtins/string/collect.rs index 55cef1749..a50b56f34 100644 --- a/fish-rust/src/builtins/string/collect.rs +++ b/fish-rust/src/builtins/string/collect.rs @@ -24,7 +24,7 @@ fn parse_opt(&mut self, _n: &wstr, c: char, _arg: Option<&wstr>) -> Result<(), S fn handle( &mut self, - _parser: &mut Parser, + _parser: &Parser, streams: &mut IoStreams, optind: &mut usize, args: &[&wstr], @@ -43,7 +43,7 @@ fn handle( streams .out - .append_with_separation(arg, separation_type_t::explicitly, want_newline); + .append_with_separation(arg, SeparationType::explicitly, want_newline); appended += arg.len(); } @@ -54,7 +54,7 @@ fn handle( if self.allow_empty && appended == 0 { streams.out.append_with_separation( L!(""), - separation_type_t::explicitly, + SeparationType::explicitly, true, /* historical behavior is to always print a newline */ ); } diff --git a/fish-rust/src/builtins/string/escape.rs b/fish-rust/src/builtins/string/escape.rs index 7a13e6c6f..f6c1edd6c 100644 --- a/fish-rust/src/builtins/string/escape.rs +++ b/fish-rust/src/builtins/string/escape.rs @@ -30,7 +30,7 @@ fn parse_opt(&mut self, name: &wstr, c: char, arg: Option<&wstr>) -> Result<(), fn handle( &mut self, - _parser: &mut Parser, + _parser: &Parser, streams: &mut IoStreams, optind: &mut usize, args: &[&wstr], diff --git a/fish-rust/src/builtins/string/join.rs b/fish-rust/src/builtins/string/join.rs index 2587f9916..355aa3c4c 100644 --- a/fish-rust/src/builtins/string/join.rs +++ b/fish-rust/src/builtins/string/join.rs @@ -56,7 +56,7 @@ fn take_args( fn handle( &mut self, - _parser: &mut Parser, + _parser: &Parser, streams: &mut IoStreams, optind: &mut usize, args: &[&wstr], @@ -84,9 +84,9 @@ fn handle( if nargs > 0 && !self.quiet { if self.is_join0 { - streams.out.append1('\0'); + streams.out.append_char('\0'); } else if print_trailing_newline { - streams.out.append1('\n'); + streams.out.append_char('\n'); } } diff --git a/fish-rust/src/builtins/string/length.rs b/fish-rust/src/builtins/string/length.rs index f42faa1d7..bfe862eaa 100644 --- a/fish-rust/src/builtins/string/length.rs +++ b/fish-rust/src/builtins/string/length.rs @@ -26,7 +26,7 @@ fn parse_opt(&mut self, _n: &wstr, c: char, _arg: Option<&wstr>) -> Result<(), S fn handle( &mut self, - _parser: &mut Parser, + _parser: &Parser, streams: &mut IoStreams, optind: &mut usize, args: &[&wstr], diff --git a/fish-rust/src/builtins/string/match.rs b/fish-rust/src/builtins/string/match.rs index f95d48332..c670cc3ca 100644 --- a/fish-rust/src/builtins/string/match.rs +++ b/fish-rust/src/builtins/string/match.rs @@ -67,7 +67,7 @@ fn take_args( fn handle( &mut self, - parser: &mut Parser, + parser: &Parser, streams: &mut IoStreams, optind: &mut usize, args: &[&wstr], @@ -125,7 +125,7 @@ fn handle( .. }) = matcher { - let vars = parser.get_vars(); + let vars = parser.vars(); for (name, vals) in first_match_captures.into_iter() { vars.set(&WString::from(name), EnvMode::default(), vals); } diff --git a/fish-rust/src/builtins/string/pad.rs b/fish-rust/src/builtins/string/pad.rs index 9bd4e7ed4..a7283043d 100644 --- a/fish-rust/src/builtins/string/pad.rs +++ b/fish-rust/src/builtins/string/pad.rs @@ -65,7 +65,7 @@ fn parse_opt(&mut self, name: &wstr, c: char, arg: Option<&wstr>) -> Result<(), fn handle<'args>( &mut self, - _parser: &mut Parser, + _parser: &Parser, streams: &mut IoStreams, optind: &mut usize, args: &[&'args wstr], diff --git a/fish-rust/src/builtins/string/repeat.rs b/fish-rust/src/builtins/string/repeat.rs index 47102cfc5..eab03eee4 100644 --- a/fish-rust/src/builtins/string/repeat.rs +++ b/fish-rust/src/builtins/string/repeat.rs @@ -39,7 +39,7 @@ fn parse_opt(&mut self, name: &wstr, c: char, arg: Option<&wstr>) -> Result<(), fn handle( &mut self, - _parser: &mut Parser, + _parser: &Parser, streams: &mut IoStreams, optind: &mut usize, args: &[&wstr], diff --git a/fish-rust/src/builtins/string/replace.rs b/fish-rust/src/builtins/string/replace.rs index edcafddd0..175bdb1cd 100644 --- a/fish-rust/src/builtins/string/replace.rs +++ b/fish-rust/src/builtins/string/replace.rs @@ -62,7 +62,7 @@ fn take_args( fn handle( &mut self, - _parser: &mut Parser, + _parser: &Parser, streams: &mut IoStreams, optind: &mut usize, args: &[&wstr], diff --git a/fish-rust/src/builtins/string/shorten.rs b/fish-rust/src/builtins/string/shorten.rs index 553a1630c..85c42679a 100644 --- a/fish-rust/src/builtins/string/shorten.rs +++ b/fish-rust/src/builtins/string/shorten.rs @@ -64,7 +64,7 @@ fn parse_opt( fn handle( &mut self, - _parser: &mut Parser, + _parser: &Parser, streams: &mut IoStreams, optind: &mut usize, args: &[&wstr], @@ -100,7 +100,7 @@ fn handle( Direction::Left => splits.last(), } .unwrap(); - s.push_utfstr(self.ellipsis); + s.push_utfstr(&self.ellipsis); let width = width_without_escapes(&s, 0); if width > 0 && width < min_width { @@ -125,7 +125,7 @@ fn handle( // truncating instead. (L!(""), 0) } else { - (self.ellipsis, self.ellipsis_width) + (&self.ellipsis[..], self.ellipsis_width) }; let mut nsub = 0usize; diff --git a/fish-rust/src/builtins/string/split.rs b/fish-rust/src/builtins/string/split.rs index ef9684ace..99fe5c831 100644 --- a/fish-rust/src/builtins/string/split.rs +++ b/fish-rust/src/builtins/string/split.rs @@ -1,4 +1,3 @@ -use std::borrow::Cow; use std::ops::Deref; use super::*; @@ -162,7 +161,7 @@ fn take_args( fn handle( &mut self, - _parser: &mut Parser, + _parser: &Parser, streams: &mut IoStreams, optind: &mut usize, args: &[&'args wstr], @@ -259,18 +258,16 @@ fn handle( } for field in self.fields.iter() { if let Some(val) = splits.get(*field) { - streams.out.append_with_separation( - val, - separation_type_t::explicitly, - true, - ); + streams + .out + .append_with_separation(val, SeparationType::explicitly, true); } } } else { - for split in &splits { + for split in splits { streams .out - .append_with_separation(split, separation_type_t::explicitly, true); + .append_with_separation(&split, SeparationType::explicitly, true); } } } diff --git a/fish-rust/src/builtins/string/sub.rs b/fish-rust/src/builtins/string/sub.rs index 9fe94aada..ad6defa2e 100644 --- a/fish-rust/src/builtins/string/sub.rs +++ b/fish-rust/src/builtins/string/sub.rs @@ -49,7 +49,7 @@ fn parse_opt(&mut self, name: &wstr, c: char, arg: Option<&wstr>) -> Result<(), fn handle( &mut self, - _parser: &mut Parser, + _parser: &Parser, streams: &mut IoStreams, optind: &mut usize, args: &[&wstr], diff --git a/fish-rust/src/builtins/string/transform.rs b/fish-rust/src/builtins/string/transform.rs index 3c2ca93d1..a3d76bb66 100644 --- a/fish-rust/src/builtins/string/transform.rs +++ b/fish-rust/src/builtins/string/transform.rs @@ -18,7 +18,7 @@ fn parse_opt(&mut self, _n: &wstr, c: char, _arg: Option<&wstr>) -> Result<(), S fn handle( &mut self, - _parser: &mut Parser, + _parser: &Parser, streams: &mut IoStreams, optind: &mut usize, args: &[&wstr], diff --git a/fish-rust/src/builtins/string/trim.rs b/fish-rust/src/builtins/string/trim.rs index f40d77505..f565775e8 100644 --- a/fish-rust/src/builtins/string/trim.rs +++ b/fish-rust/src/builtins/string/trim.rs @@ -46,7 +46,7 @@ fn parse_opt( fn handle( &mut self, - _parser: &mut Parser, + _parser: &Parser, streams: &mut IoStreams, optind: &mut usize, args: &[&wstr], diff --git a/fish-rust/src/builtins/string/unescape.rs b/fish-rust/src/builtins/string/unescape.rs index f39a057b5..ee08b75b2 100644 --- a/fish-rust/src/builtins/string/unescape.rs +++ b/fish-rust/src/builtins/string/unescape.rs @@ -32,7 +32,7 @@ fn parse_opt(&mut self, name: &wstr, c: char, arg: Option<&wstr>) -> Result<(), fn handle( &mut self, - _parser: &mut Parser, + _parser: &Parser, streams: &mut IoStreams, optind: &mut usize, args: &[&wstr], diff --git a/fish-rust/src/builtins/test.rs b/fish-rust/src/builtins/test.rs index 109eb18a5..240d8b89d 100644 --- a/fish-rust/src/builtins/test.rs +++ b/fish-rust/src/builtins/test.rs @@ -82,7 +82,7 @@ pub(super) fn new(base: i64, delta: f64) -> Self { } // Return true if the number is a tty(). - fn isatty(&self, streams: &IoStreams) -> bool { + fn isatty(&self, streams: &mut IoStreams) -> bool { fn istty(fd: libc::c_int) -> bool { // Safety: isatty cannot crash. unsafe { libc::isatty(fd) > 0 } @@ -92,7 +92,10 @@ fn istty(fd: libc::c_int) -> bool { } let bint = self.base as i32; if bint == 0 { - streams.stdin_fd().map(istty).unwrap_or(false) + match streams.stdin_fd { + -1 => false, + fd => istty(fd), + } } else if bint == 1 { !streams.out_is_redirected && istty(libc::STDOUT_FILENO) } else if bint == 2 { @@ -1003,7 +1006,7 @@ fn stat_and(arg: &wstr, f: F) -> bool /// Evaluate a conditional expression given the arguments. For POSIX conformance this /// supports a more limited range of functionality. /// Return status is the final shell status, i.e. 0 for true, 1 for false and 2 for error. -pub fn test(parser: &mut Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Option { +pub fn test(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Option { // The first argument should be the name of the command ('test'). if argv.is_empty() { return STATUS_INVALID_ARGS; @@ -1025,7 +1028,7 @@ pub fn test(parser: &mut Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> streams .err .appendln(wgettext!("[: the last argument must be ']'")); - builtin_print_error_trailer(parser, streams, program_name); + builtin_print_error_trailer(parser, streams.err, program_name); return STATUS_INVALID_ARGS; } } @@ -1053,7 +1056,7 @@ pub fn test(parser: &mut Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> let expr = test_expressions::TestParser::parse_args(args, &mut err, program_name); let Some(expr) = expr else { streams.err.append(err); - streams.err.append(parser.pin().current_line().as_wstr()); + streams.err.append(parser.current_line()); return STATUS_CMD_ERROR; }; @@ -1062,11 +1065,11 @@ pub fn test(parser: &mut Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> if !eval_errors.is_empty() { if !common::should_suppress_stderr_for_tests() { for eval_error in eval_errors { - streams.err.appendln(eval_error); + streams.err.appendln(&eval_error); } // Add a backtrace but not the "see help" message // because this isn't about passing the wrong options. - streams.err.append(parser.pin().current_line().as_wstr()); + streams.err.append(parser.current_line()); } return STATUS_INVALID_ARGS; } diff --git a/fish-rust/src/builtins/tests/string_tests.rs b/fish-rust/src/builtins/tests/string_tests.rs index 00c25d320..273108577 100644 --- a/fish-rust/src/builtins/tests/string_tests.rs +++ b/fish-rust/src/builtins/tests/string_tests.rs @@ -1,11 +1,11 @@ +use super::super::prelude::*; +use crate::common::escape; use crate::ffi_tests::add_test; +use crate::io::{OutputStream, StringOutputStream}; add_test! {"test_string", || { - use crate::ffi::Parser; - use crate::ffi; + use crate::parser::Parser; use crate::builtins::string::string; - use crate::wchar_ffi::WCharFromFFI; - use crate::common::{EscapeStringStyle, escape_string}; use crate::wchar::wstr; use crate::wchar::L; use crate::builtins::shared::{STATUS_CMD_ERROR,STATUS_CMD_OK, STATUS_INVALID_ARGS}; @@ -20,17 +20,18 @@ macro_rules! test_cases { // TODO: these should be individual tests, not all in one, port when we can run these with `cargo test` fn string_test(mut args: Vec<&wstr>, expected_rc: Option, expected_out: &wstr) { - let parser: &mut Parser = unsafe { &mut *Parser::principal_parser_ffi() }; - let mut streams = ffi::make_test_io_streams_ffi(); - let mut io = crate::builtins::shared::IoStreams::new(streams.pin_mut()); + let parser: &Parser = Parser::principal_parser(); + let mut outs = OutputStream::String(StringOutputStream::new()); + let mut errs = OutputStream::Null; + let mut streams = IoStreams::new(&mut outs, &mut errs); + streams.stdin_is_directly_redirected = false; // read from argv instead of stdin - let rc = string(parser, &mut io, args.as_mut_slice()).expect("string failed"); + let rc = string(parser, &mut streams, args.as_mut_slice()).expect("string failed"); assert_eq!(expected_rc.unwrap(), rc, "string builtin returned unexpected return code"); - let string_stream_contents = &ffi::get_test_output_ffi(&streams); - let actual = escape_string(&string_stream_contents.from_ffi(), EscapeStringStyle::default()); - let expected = escape_string(expected_out, EscapeStringStyle::default()); + let actual = escape(outs.contents()); + let expected = escape(expected_out); assert_eq!(expected, actual, "string builtin returned unexpected output"); } diff --git a/fish-rust/src/builtins/tests/test_tests.rs b/fish-rust/src/builtins/tests/test_tests.rs index 0c6bee568..da1a38ce5 100644 --- a/fish-rust/src/builtins/tests/test_tests.rs +++ b/fish-rust/src/builtins/tests/test_tests.rs @@ -1,10 +1,9 @@ use crate::builtins::prelude::*; use crate::builtins::test::test as builtin_test; - -use crate::ffi::make_null_io_streams_ffi; +use crate::io::OutputStream; fn run_one_test_test_mbracket(expected: i32, lst: &[&str], bracket: bool) -> bool { - let parser: &mut Parser = unsafe { &mut *Parser::principal_parser_ffi() }; + let parser = Parser::principal_parser(); let mut argv = Vec::new(); if bracket { argv.push(L!("[").to_owned()); @@ -20,9 +19,10 @@ fn run_one_test_test_mbracket(expected: i32, lst: &[&str], bracket: bool) -> boo // Convert to &[&wstr]. let mut argv = argv.iter().map(|s| s.as_ref()).collect::>(); + let mut out = OutputStream::Null; + let mut err = OutputStream::Null; + let mut streams = IoStreams::new(&mut out, &mut err); - let mut streams_ffi = make_null_io_streams_ffi(); - let mut streams = IoStreams::new(streams_ffi.as_mut().unwrap()); let result: Option = builtin_test(parser, &mut streams, &mut argv); if result != Some(expected) { @@ -48,9 +48,11 @@ fn run_test_test(expected: i32, lst: &[&str]) -> bool { #[widestrs] fn test_test_brackets() { // Ensure [ knows it needs a ]. - let parser: &mut Parser = unsafe { &mut *Parser::principal_parser_ffi() }; - let mut streams_ffi = make_null_io_streams_ffi(); - let mut streams = IoStreams::new(streams_ffi.as_mut().unwrap()); + let parser = Parser::principal_parser(); + + let mut out = OutputStream::Null; + let mut err = OutputStream::Null; + let mut streams = IoStreams::new(&mut out, &mut err); let args1 = &mut ["["L, "foo"L]; assert_eq!( diff --git a/fish-rust/src/builtins/type.rs b/fish-rust/src/builtins/type.rs index 1c83e03d4..653445e40 100644 --- a/fish-rust/src/builtins/type.rs +++ b/fish-rust/src/builtins/type.rs @@ -1,6 +1,8 @@ use super::prelude::*; -use crate::ffi::{builtin_exists, colorize_shell}; +use crate::builtins::shared::builtin_exists; +use crate::common::str2wcstring; use crate::function; +use crate::highlight::{colorize, highlight_shell}; use crate::path::{path_get_path, path_get_paths}; @@ -16,7 +18,7 @@ struct type_cmd_opts_t { query: bool, } -pub fn r#type(parser: &mut Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Option { +pub fn r#type(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Option { let cmd = argv[0]; let argc = argv.len(); let print_hints = false; @@ -87,7 +89,7 @@ pub fn r#type(parser: &mut Parser, streams: &mut IoStreams, argv: &mut [&wstr]) if path.is_empty() { comment.push_utfstr(&wgettext_fmt!("Defined interactively")); - } else if path == "-" { + } else if path == L!("-") { comment.push_utfstr(&wgettext_fmt!("Defined via `source`")); } else { let lineno: i32 = props.definition_lineno(); @@ -102,10 +104,10 @@ pub fn r#type(parser: &mut Parser, streams: &mut IoStreams, argv: &mut [&wstr]) let path = props.copy_definition_file().unwrap_or(L!("")); if path.is_empty() { comment.push_utfstr(&wgettext_fmt!(", copied interactively")); - } else if path == "-" { + } else if path == L!("-") { comment.push_utfstr(&wgettext_fmt!(", copied via `source`")); } else { - let lineno: i32 = props.copy_definition_lineno(); + let lineno = props.copy_definition_lineno(); comment.push_utfstr(&wgettext_fmt!( ", copied in %ls @ line %d", path, @@ -130,7 +132,15 @@ pub fn r#type(parser: &mut Parser, streams: &mut IoStreams, argv: &mut [&wstr]) )); if streams.out_is_terminal() { - let col = colorize_shell(&def.to_ffi(), parser.pin()).from_ffi(); + let mut color = vec![]; + highlight_shell( + &def, + &mut color, + &parser.context(), + /*io_ok=*/ false, + /*cursor=*/ None, + ); + let col = str2wcstring(&colorize(&def, &color, parser.vars())); streams.out.append(col); } else { streams.out.append(def); @@ -140,7 +150,7 @@ pub fn r#type(parser: &mut Parser, streams: &mut IoStreams, argv: &mut [&wstr]) streams.out.append(wgettext_fmt!(" (%ls)\n", comment)); } } else if opts.get_type { - streams.out.appendln("function"); + streams.out.appendln(L!("function")); } if !opts.all { continue; @@ -148,7 +158,7 @@ pub fn r#type(parser: &mut Parser, streams: &mut IoStreams, argv: &mut [&wstr]) } } - if !opts.force_path && builtin_exists(&arg.to_ffi()) { + if !opts.force_path && builtin_exists(arg) { found += 1; res = true; if opts.query { @@ -165,9 +175,9 @@ pub fn r#type(parser: &mut Parser, streams: &mut IoStreams, argv: &mut [&wstr]) } let paths = if opts.all { - path_get_paths(arg, &*parser.get_vars()) + path_get_paths(arg, parser.vars()) } else { - match path_get_path(arg, &*parser.get_vars()) { + match path_get_path(arg, parser.vars()) { Some(p) => vec![p], None => vec![], } @@ -186,7 +196,7 @@ pub fn r#type(parser: &mut Parser, streams: &mut IoStreams, argv: &mut [&wstr]) streams.out.append(wgettext_fmt!("%ls is %ls\n", arg, path)); } } else if opts.get_type { - streams.out.appendln("file"); + streams.out.appendln(L!("file")); break; } if !opts.all { diff --git a/fish-rust/src/builtins/ulimit.rs b/fish-rust/src/builtins/ulimit.rs new file mode 100644 index 000000000..aa2b0621a --- /dev/null +++ b/fish-rust/src/builtins/ulimit.rs @@ -0,0 +1,16 @@ +use super::prelude::*; + +pub fn ulimit(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option { + run_builtin_ffi(crate::ffi::builtin_ulimit, parser, streams, args) +} + +/// Struct describing a resource limit. +struct Resource { + resource: c_int, // resource ID + desc: &'static wstr, // description of resource + switch_char: char, // switch used on commandline to specify resource + multiplier: c_int, // the implicit multiplier used when setting getting values +} + +/// Array of resource_t structs, describing all known resource types. +const resource_arr: &[Resource] = &[]; diff --git a/fish-rust/src/builtins/wait.rs b/fish-rust/src/builtins/wait.rs index f8b6e838a..690aaa58d 100644 --- a/fish-rust/src/builtins/wait.rs +++ b/fish-rust/src/builtins/wait.rs @@ -1,13 +1,13 @@ use libc::pid_t; use super::prelude::*; -use crate::ffi::{job_t, proc_wait_any, Parser}; +use crate::proc::{proc_wait_any, Job}; use crate::signal::SigChecker; use crate::wait_handle::{WaitHandleRef, WaitHandleStore}; use crate::wutil; /// \return true if we can wait on a job. -fn can_wait_on_job(j: &cxx::SharedPtr) -> bool { +fn can_wait_on_job(j: &Job) -> bool { j.is_constructed() && !j.is_foreground() && !j.is_stopped() } @@ -35,13 +35,13 @@ enum WaitHandleQuery<'a> { /// \return true if we found a matching job (even if not waitable), false if not. fn find_wait_handles( query: WaitHandleQuery<'_>, - parser: &mut Parser, + parser: &Parser, handles: &mut Vec, ) -> bool { // Has a job already completed? // TODO: we can avoid traversing this list if searching by pid. let mut matched = false; - let wait_handles: &mut WaitHandleStore = parser.get_wait_handles_mut(); + let wait_handles: &mut WaitHandleStore = &mut parser.mut_wait_handles(); for wh in wait_handles.iter() { if wait_handle_matches(query, wh) { handles.push(wh.clone()); @@ -50,15 +50,12 @@ fn find_wait_handles( } // Is there a running job match? - for j in parser.get_jobs() { + for j in &*parser.jobs() { // We want to set 'matched' to true if we could have matched, even if the job was stopped. let provide_handle = can_wait_on_job(j); - for proc in j.get_procs() { - let wh = proc - .pin_mut() - .unpin() - .make_wait_handle(j.get_internal_job_id()); - let Some(wh) = wh else { + let internal_job_id = j.internal_job_id; + for proc in j.processes().iter() { + let Some(wh) = proc.make_wait_handle(internal_job_id) else { continue; }; if wait_handle_matches(query, &wh) { @@ -77,13 +74,13 @@ fn get_all_wait_handles(parser: &Parser) -> Vec { let mut result = parser.get_wait_handles().get_list(); // Get wait handles for running jobs. - for j in parser.get_jobs() { + for j in &*parser.jobs() { if !can_wait_on_job(j) { continue; } - for proc_ptr in j.get_procs().iter_mut() { - let proc = proc_ptr.pin_mut().unpin(); - if let Some(wh) = proc.make_wait_handle(j.get_internal_job_id()) { + let internal_job_id = j.internal_job_id; + for proc in j.processes().iter() { + if let Some(wh) = proc.make_wait_handle(internal_job_id) { result.push(wh); } } @@ -98,11 +95,7 @@ fn is_completed(wh: &WaitHandleRef) -> bool { /// Wait for the given wait handles to be marked as completed. /// If \p any_flag is set, wait for the first one; otherwise wait for all. /// \return a status code. -fn wait_for_completion( - parser: &mut Parser, - whs: &[WaitHandleRef], - any_flag: bool, -) -> Option { +fn wait_for_completion(parser: &Parser, whs: &[WaitHandleRef], any_flag: bool) -> Option { if whs.is_empty() { return Some(0); } @@ -119,7 +112,7 @@ fn wait_for_completion( // Remove completed wait handles (at most 1 if any_flag is set). for wh in whs { if is_completed(wh) { - parser.get_wait_handles_mut().remove(wh); + parser.mut_wait_handles().remove(wh); if any_flag { break; } @@ -130,12 +123,12 @@ fn wait_for_completion( if sigint.check() { return Some(128 + libc::SIGINT); } - proc_wait_any(parser.pin()); + proc_wait_any(parser); } } #[widestrs] -pub fn wait(parser: &mut Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Option { +pub fn wait(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Option { let cmd = argv[0]; let argc = argv.len(); let mut any_flag = false; // flag for -n option @@ -158,11 +151,11 @@ pub fn wait(parser: &mut Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> print_help = true; } ':' => { - builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1], print_hints); + builtin_missing_argument(parser, streams, cmd, &argv[w.woptind - 1], print_hints); return STATUS_INVALID_ARGS; } '?' => { - builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1], print_hints); + builtin_unknown_option(parser, streams, cmd, &argv[w.woptind - 1], print_hints); return STATUS_INVALID_ARGS; } _ => { diff --git a/fish-rust/src/cfg/spawn.c b/fish-rust/src/cfg/spawn.c new file mode 100644 index 000000000..e6233bcb9 --- /dev/null +++ b/fish-rust/src/cfg/spawn.c @@ -0,0 +1 @@ +#include diff --git a/fish-rust/src/cfg/w_exitcode.cpp b/fish-rust/src/cfg/w_exitcode.cpp new file mode 100644 index 000000000..87bf484ef --- /dev/null +++ b/fish-rust/src/cfg/w_exitcode.cpp @@ -0,0 +1,2 @@ +#include +static_assert(WEXITSTATUS(0x007f) == 0x7f, ""); diff --git a/fish-rust/src/common.rs b/fish-rust/src/common.rs index 8b17ed70f..5e2d0449b 100644 --- a/fish-rust/src/common.rs +++ b/fish-rust/src/common.rs @@ -6,7 +6,7 @@ PROCESS_EXPAND_SELF, PROCESS_EXPAND_SELF_STR, VARIABLE_EXPAND, VARIABLE_EXPAND_SINGLE, }; use crate::fallback::fish_wcwidth; -use crate::ffi::{self}; +use crate::ffi; use crate::flog::FLOG; use crate::future_feature_flags::{feature_test, FeatureFlag}; use crate::global_safety::RelaxedAtomicBool; @@ -1004,7 +1004,7 @@ fn debug_thread_error() { } /// Exits without invoking destructors (via _exit), useful for code after fork. -pub fn exit_without_destructors(code: i32) -> ! { +pub fn exit_without_destructors(code: libc::c_int) -> ! { unsafe { libc::_exit(code) }; } @@ -1074,7 +1074,8 @@ pub fn has_working_tty_timestamps() -> bool { /// A function type to check for cancellation. /// \return true if execution should cancel. -pub type CancelChecker = dyn Fn() -> bool; +/// todo!("Maybe remove the box? It is only needed for get_bg_context.") +pub type CancelChecker = Box bool>; /// Converts the narrow character string \c in into its wide equivalent, and return it. /// @@ -1200,6 +1201,7 @@ pub fn wcs2osstring(input: &wstr) -> OsString { OsString::from_vec(result) } +/// Same as [`wcs2string`]. Meant to be used when we need a zero-terminated string to feed legacy APIs. pub fn wcs2zstring(input: &wstr) -> CString { if input.is_empty() { return CString::default(); @@ -1311,7 +1313,11 @@ pub fn format_size(mut sz: i64) -> WString { } /// Writes out a long safely. -pub fn format_llong_safe>(buff: &mut [CharT; 64], val: i64) { +pub fn format_llong_safe, I64>(buff: &mut [CharT; 64], val: I64) +where + i64: From, +{ + let val = i64::from(val); let uval = val.unsigned_abs(); if val >= 0 { format_safe_impl(buff, 64, uval); @@ -1659,6 +1665,10 @@ fn slice_contains_slice(a: &[T], b: &[T]) -> bool { a.windows(b.len()).any(|aw| aw == b) } +pub fn subslice_position(a: &[T], b: &[T]) -> Option { + a.windows(b.len()).position(|aw| aw == b) +} + /// Determines if we are running under Microsoft's Windows Subsystem for Linux to work around /// some known limitations and/or bugs. /// @@ -1898,6 +1908,22 @@ pub fn scoped_push( ScopeGuard::new(ctx, restore_saved) } +/// Similar to scoped_push but takes a function like "std::mem::replace" instead of a function +/// that returns a mutable reference. +pub fn scoped_push_replacer( + replacer: Replacer, + new_value: T, +) -> impl ScopeGuarding +where + Replacer: Fn(T) -> T, +{ + let saved = replacer(new_value); + let restore_saved = move |_ctx: &mut ()| { + replacer(saved); + }; + ScopeGuard::new((), restore_saved) +} + pub const fn assert_send() {} pub const fn assert_sync() {} @@ -2119,11 +2145,10 @@ macro_rules! err { } } -#[allow(unused_macros)] macro_rules! fwprintf { - ($fd:expr, $format:literal $(, $arg:expr)*) => { + ($fd:expr, $format:expr $(, $arg:expr)*) => { { - let wide = crate::wutil::sprintf!($format $(, $arg )*); + let wide = crate::wutil::sprintf!($format, $( $arg ),*); crate::wutil::wwrite_to_fd(&wide, $fd); } } @@ -2141,7 +2166,7 @@ mod common_ffi { type escape_string_style_t = crate::ffi::escape_string_style_t; } extern "Rust" { - #[cxx_name = "rust_unescape_string"] + #[cxx_name = "unescape_string"] fn unescape_string_ffi( input: *const wchar_t, len: usize, @@ -2149,17 +2174,17 @@ fn unescape_string_ffi( style: escape_string_style_t, ) -> UniquePtr; - #[cxx_name = "rust_escape_string_script"] + #[cxx_name = "escape_string_script"] fn escape_string_script_ffi( input: *const wchar_t, len: usize, flags: u32, ) -> UniquePtr; - #[cxx_name = "rust_escape_string_url"] + #[cxx_name = "escape_string_url"] fn escape_string_url_ffi(input: *const wchar_t, len: usize) -> UniquePtr; - #[cxx_name = "rust_escape_string_var"] + #[cxx_name = "escape_string_var"] fn escape_string_var_ffi(input: *const wchar_t, len: usize) -> UniquePtr; } diff --git a/fish-rust/src/compat.c b/fish-rust/src/compat.c index d28252cb1..c0c21e158 100644 --- a/fish-rust/src/compat.c +++ b/fish-rust/src/compat.c @@ -1,7 +1,9 @@ #include "config.h" +#include #include #include +#include #include #include #include @@ -51,6 +53,26 @@ uint64_t C_MNT_LOCAL() { #endif } +const char* C_PATH_BSHELL() { return _PATH_BSHELL; } + +int C_PC_CASE_SENSITIVE() { +#ifdef _PC_CASE_SENSITIVE + return _PC_CASE_SENSITIVE; +#else + return 0; +#endif +} + +FILE* stdout_stream() { return stdout; } + +int C_O_EXLOCK() { +#ifdef O_EXLOCK + return O_EXLOCK; +#else + return 0; +#endif +} + static const bool uvar_file_set_mtime_hack = #ifdef UVAR_FILE_SET_MTIME_HACK true; diff --git a/fish-rust/src/compat.rs b/fish-rust/src/compat.rs index 1d7d033f4..1a0de1e76 100644 --- a/fish-rust/src/compat.rs +++ b/fish-rust/src/compat.rs @@ -1,3 +1,8 @@ +use std::sync::atomic::AtomicPtr; + +use libc::c_int; +use once_cell::sync::Lazy; + #[allow(non_snake_case)] pub fn MB_CUR_MAX() -> usize { unsafe { C_MB_CUR_MAX() } @@ -22,6 +27,12 @@ pub fn _CS_PATH() -> i32 { unsafe { C_CS_PATH() } } +#[allow(non_snake_case)] +pub static _PATH_BSHELL: AtomicPtr = AtomicPtr::new(std::ptr::null_mut()); + +#[allow(non_snake_case)] +pub static _PC_CASE_SENSITIVE: Lazy = Lazy::new(|| unsafe { C_PC_CASE_SENSITIVE() }); + extern "C" { fn C_MB_CUR_MAX() -> usize; fn has_cur_term() -> bool; @@ -33,4 +44,9 @@ pub(crate) fn confstr( buf: *mut libc::c_char, len: libc::size_t, ) -> libc::size_t; + pub fn C_PATH_BSHELL() -> *const i8; + fn C_PC_CASE_SENSITIVE() -> c_int; + pub fn C_O_EXLOCK() -> c_int; + pub fn stdout_stream() -> *mut libc::FILE; + pub fn UVAR_FILE_SET_MTIME_HACK() -> bool; } diff --git a/fish-rust/src/complete.rs b/fish-rust/src/complete.rs index c11af3f40..43ab5e3a4 100644 --- a/fish-rust/src/complete.rs +++ b/fish-rust/src/complete.rs @@ -1,16 +1,99 @@ -/// Prototypes for functions related to tab-completion. -/// -/// These functions are used for storing and retrieving tab-completion data, as well as for -/// performing tab-completion. -use crate::wchar::prelude::*; -use crate::wcstringutil::StringFuzzyMatch; -use bitflags::bitflags; +use std::{ + cmp::Ordering, + collections::{BTreeMap, HashMap, HashSet}, + mem, + pin::Pin, + sync::{ + atomic::{self, AtomicUsize}, + Mutex, + }, + time::{Duration, Instant}, +}; -#[derive(Default, Debug)] +use crate::{ + common::charptr2wcstring, + util::wcsfilecmp, + wchar_ffi::{AsWstr, WCharFromFFI, WCharToFFI}, +}; +use bitflags::bitflags; +use cxx::{CxxWString, UniquePtr}; +use once_cell::sync::Lazy; +use printf_compat::sprintf; +use widestring::U32CString; + +use crate::{ + abbrs::with_abbrs, + autoload::Autoload, + builtins::shared::{builtin_exists, builtin_get_desc, builtin_get_names}, + common::{ + escape, unescape_string, valid_var_name_char, ScopeGuard, UnescapeFlags, + UnescapeStringStyle, + }, + env::{EnvMode, EnvStack, Environment}, + exec::exec_subshell, + expand::{ + expand_escape_string, expand_escape_variable, expand_one, expand_string, + expand_to_receiver, ExpandFlags, ExpandResultCode, + }, + flog::{FLOG, FLOGF}, + function, + history::{history_session_id, History}, + operation_context::OperationContext, + parse_constants::SourceRange, + parse_util::{ + parse_util_cmdsubst_extent, parse_util_process_extent, parse_util_unescape_wildcards, + }, + parser::{Block, Parser}, + parser_keywords::parser_keywords_is_subcommand, + path::{path_get_path, path_try_get_path}, + tokenizer::{variable_assignment_equals_pos, Tok, TokFlags, TokenType, Tokenizer}, + wchar::{wstr, WString, L}, + wchar_ext::WExt, + wcstringutil::{ + string_fuzzy_match_string, string_prefixes_string, string_prefixes_string_case_insensitive, + StringFuzzyMatch, + }, + wildcard::{wildcard_complete, wildcard_has, wildcard_match}, + wutil::{gettext::wgettext_impl_do_not_use_directly, wgettext, wrealpath}, +}; + +// Completion description strings, mostly for different types of files, such as sockets, block +// devices, etc. +// +// There are a few more completion description strings defined in expand.rs. Maybe all completion +// description strings should be defined in the same file? + +/// Description for ~USER completion. +static COMPLETE_USER_DESC: Lazy<&wstr> = Lazy::new(|| wgettext!("Home for %ls")); + +/// Description for short variables. The value is concatenated to this description. +static COMPLETE_VAR_DESC_VAL: Lazy<&wstr> = Lazy::new(|| wgettext!("Variable: %ls")); + +/// Description for abbreviations. +static ABBR_DESC: Lazy<&wstr> = Lazy::new(|| wgettext!("Abbreviation: %ls")); + +/// The special cased translation macro for completions. The empty string needs to be special cased, +/// since it can occur, and should not be translated. (Gettext returns the version information as +/// the response). +#[allow(non_snake_case)] +fn C_(s: &wstr) -> &'static wstr { + if s.is_empty() { + L!("") + } else { + wgettext_impl_do_not_use_directly( + U32CString::from_ustr(s) + .expect("translation string without NUL bytes") + .as_slice_with_nul(), + ) + } +} + +#[derive(Clone, Copy, Default, PartialEq, Eq, Debug)] pub struct CompletionMode { /// If set, skip file completions. pub no_files: bool, pub force_files: bool, + /// If set, require a parameter after completion. pub requires_param: bool, } @@ -42,11 +125,27 @@ pub struct CompleteFlags: u8 { } } -#[derive(Debug)] +/// Function which accepts a completion string and returns its description. +pub type DescriptionFunc = Box WString>; + +/// Helper to return a [`DescriptionFunc`] for a constant string. +pub fn const_desc(s: &wstr) -> DescriptionFunc { + let s = s.to_owned(); + Box::new(move |_| s.clone()) +} + +pub type CompletionList = Vec; + +/// This is an individual completion entry, i.e. the result of an expansion of a completion rule. +#[derive(Clone, Debug, Eq, PartialEq)] pub struct Completion { + /// The completion string. pub completion: WString, + /// The description for this completion. pub description: WString, + /// The type of fuzzy match. pub r#match: StringFuzzyMatch, + /// Flags determining the completion behavior. pub flags: CompleteFlags, } @@ -71,37 +170,85 @@ fn from(completion: WString) -> Completion { } impl Completion { - /// \return whether this replaces its token. + pub fn new( + completion: WString, + description: WString, + r#match: StringFuzzyMatch, + flags: CompleteFlags, + ) -> Self { + let flags = resolve_auto_space(&completion, flags); + Self { + completion, + description, + r#match, + flags, + } + } + + pub fn from_completion(completion: WString) -> Self { + Self::with_desc(completion, WString::new()) + } + + pub fn with_desc(completion: WString, description: WString) -> Self { + Self::new( + completion, + description, + StringFuzzyMatch::exact_match(), + CompleteFlags::empty(), + ) + } + + /// Returns whether this replaces its token. pub fn replaces_token(&self) -> bool { self.flags.contains(CompleteFlags::REPLACES_TOKEN) } - /// \return whether this replaces the entire commandline. + + /// Returns whether this replaces the entire commandline. pub fn replaces_commandline(&self) -> bool { self.flags.contains(CompleteFlags::REPLACES_COMMANDLINE) } - /// \return the completion's match rank. Lower ranks are better completions. + /// Returns the completion's match rank. Lower ranks are better completions. pub fn rank(&self) -> u32 { self.r#match.rank() } /// If this completion replaces the entire token, prepend a prefix. Otherwise do nothing. - pub fn prepend_token_prefix(&mut self, prefix: impl AsRef) { + pub fn prepend_token_prefix(&mut self, prefix: &wstr) { if self.flags.contains(CompleteFlags::REPLACES_TOKEN) { - self.completion.insert_utfstr(0, prefix.as_ref()); + self.completion.insert_utfstr(0, prefix) } } } -/// A completion receiver accepts completions. It is essentially a wrapper around Vec with +impl CompletionRequestOptions { + /// Options for an autosuggestion. + pub fn autosuggest() -> Self { + Self { + autosuggestion: true, + descriptions: false, + fuzzy_match: false, + } + } + + /// Options for a "normal" completion. + pub fn normal() -> Self { + Self { + autosuggestion: false, + descriptions: true, + fuzzy_match: true, + } + } +} + +/// A completion receiver accepts completions. It is essentially a wrapper around `Vec` with /// some conveniences. -#[derive(Default, Debug)] pub struct CompletionReceiver { /// Our list of completions. completions: Vec, /// The maximum number of completions to add. If our list length exceeds this, then new /// completions are not added. Note 0 has no special significance here - use - /// usize::MAX instead. + /// `usize::MAX` instead. limit: usize, } @@ -122,13 +269,19 @@ fn deref_mut(&mut self) -> &mut Self::Target { } impl CompletionReceiver { + /// Construct as empty, with a limit. pub fn new(limit: usize) -> Self { Self { + completions: vec![], limit, - ..Default::default() } } + /// Acquire an existing list, with a limit. + pub fn from_list(completions: Vec, limit: usize) -> Self { + Self { completions, limit } + } + /// Add a completion. /// \return true on success, false if this would overflow the limit. #[must_use] @@ -140,8 +293,8 @@ pub fn add(&mut self, comp: impl Into) -> bool { return true; } - /// Add a list of completions. - /// \return true on success, false if this would overflow the limit. + /// Adds a completion with the given string, and default other properties. Returns `true` on + /// success, `false` if this would overflow the limit. #[must_use] pub fn extend( &mut self, @@ -157,21 +310,42 @@ pub fn extend( self.completions.len() <= self.limit, "ExactSizeIterator returned more items than it should" ); + true } - /// Clear the list of completions. This retains the storage inside completions_ which can be + /// Clear the list of completions. This retains the storage inside `completions` which can be /// useful to prevent allocations. pub fn clear(&mut self) { self.completions.clear(); } - /// \return the list of completions, clearing it. + /// Returns whether our completion list is empty. + pub fn empty(&self) -> bool { + self.completions.is_empty() + } + + /// Returns how many completions we have stored. + pub fn size(&self) -> usize { + self.completions.len() + } + + /// Returns the list of completions. + pub fn get_list(&self) -> &[Completion] { + &self.completions + } + + /// Returns the list of completions. + pub fn get_list_mut(&mut self) -> &mut [Completion] { + &mut self.completions + } + + /// Returns the list of completions, clearing it. pub fn take(&mut self) -> Vec { std::mem::take(&mut self.completions) } - /// \return a new, empty receiver whose limit is our remaining capacity. + /// Returns a new, empty receiver whose limit is our remaining capacity. /// This is useful for e.g. recursive calls when you want to act on the result before adding it. pub fn subreceiver(&self) -> Self { let remaining_capacity = self @@ -181,3 +355,2369 @@ pub fn subreceiver(&self) -> Self { Self::new(remaining_capacity) } } + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum CompleteOptionType { + /// no option + ArgsOnly, + /// `-x` + Short, + /// `-foo` + SingleLong, + /// `--foo` + DoubleLong, +} + +/// Struct describing a completion rule for options to a command. +/// +/// If option is empty, the comp field must not be empty and contains a list of arguments to the +/// command. +/// +/// The type field determines how the option is to be interpreted: either empty (args_only) or +/// short, single-long ("old") or double-long ("GNU"). An invariant is that the option is empty if +/// and only if the type is args_only. +/// +/// If option is non-empty, it specifies a switch for the command. If \c comp is also not empty, it +/// contains a list of non-switch arguments that may only follow directly after the specified +/// switch. +#[derive(Clone, Debug)] +struct CompleteEntryOpt { + /// Text of the option (like 'foo'). + option: WString, + /// Arguments to the option; may be a subshell expression expanded at evaluation time. + comp: WString, + /// Description of the completion. + desc: WString, + /// Conditions under which to use the option, expanded and evaluated at completion time. + conditions: Vec, + /// Type of the option: `ArgsOnly`, `Short`, `SingleLong`, or `DoubleLong`. + typ: CompleteOptionType, + /// Determines how completions should be performed on the argument after the switch. + result_mode: CompletionMode, + /// Completion flags. + flags: CompleteFlags, +} + +impl CompleteEntryOpt { + pub fn localized_desc(&self) -> &'static wstr { + C_(&self.desc) + } + + pub fn expected_dash_count(&self) -> usize { + match self.typ { + CompleteOptionType::ArgsOnly => 0, + CompleteOptionType::Short | CompleteOptionType::SingleLong => 1, + CompleteOptionType::DoubleLong => 2, + } + } +} + +/// Last value used in the order field of [`CompletionEntry`]. +static complete_order: AtomicUsize = AtomicUsize::new(0); + +struct CompletionEntry { + /// List of all options. + options: Vec, + /// Order for when this completion was created. This aids in outputting completions sorted by + /// time. + order: usize, +} + +impl CompletionEntry { + pub fn new() -> Self { + Self { + options: vec![], + order: complete_order.fetch_add(1, atomic::Ordering::Relaxed), + } + } + + /// Getters for option list. + pub fn get_options(&self) -> &[CompleteEntryOpt] { + &self.options + } + + /// Adds an option. + pub fn add_option(&mut self, opt: CompleteEntryOpt) { + self.options.push(opt) + } + + /// Remove all completion options in the specified entry that match the specified short / long + /// option strings. Returns true if it is now empty and should be deleted, false if it's not + /// empty. + pub fn remove_option(&mut self, option: &wstr, typ: CompleteOptionType) -> bool { + self.options + .retain(|opt| opt.option != option || opt.typ != typ); + self.options.is_empty() + } +} + +/// Set of all completion entries. Keyed by the command name, and whether it is a path. +#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)] +struct CompletionEntryIndex { + name: WString, + is_path: bool, +} +type CompletionEntryMap = BTreeMap; +static COMPLETION_MAP: Mutex = Mutex::new(BTreeMap::new()); + +/// Completion "wrapper" support. The map goes from wrapping-command to wrapped-command-list. +type WrapperMap = HashMap>; +static wrapper_map: Lazy> = Lazy::new(|| Mutex::new(HashMap::new())); + +/// Clear the [`CompleteFlags::AUTO_SPACE`] flag, and set [`CompleteFlags::NO_SPACE`] appropriately +/// depending on the suffix of the string. +fn resolve_auto_space(comp: &wstr, mut flags: CompleteFlags) -> CompleteFlags { + if flags.contains(CompleteFlags::AUTO_SPACE) { + flags -= CompleteFlags::AUTO_SPACE; + if let Some('/' | '=' | '@' | ':' | '.' | ',' | '-') = comp.as_char_slice().last() { + flags |= CompleteFlags::NO_SPACE; + } + } + + flags +} + +// If these functions aren't force inlined, it is actually faster to call +// stable_sort twice rather than to iterate once performing all comparisons in one go! + +#[inline(always)] +fn natural_compare_completions(a: &Completion, b: &Completion) -> Ordering { + if (a.flags & b.flags).contains(CompleteFlags::DONT_SORT) { + // Both completions are from a source with the --keep-order flag. + return Ordering::Equal; + } + wcsfilecmp(&a.completion, &b.completion) +} + +#[inline(always)] +fn compare_completions_by_duplicate_arguments(a: &Completion, b: &Completion) -> Ordering { + let ad = a.flags.contains(CompleteFlags::DUPLICATES_ARGUMENT); + let bd = b.flags.contains(CompleteFlags::DUPLICATES_ARGUMENT); + + ad.cmp(&bd) +} + +#[inline(always)] +fn compare_completions_by_tilde(a: &Completion, b: &Completion) -> Ordering { + if a.completion.is_empty() || b.completion.is_empty() { + return Ordering::Equal; + } + + let at = a.completion.ends_with('~'); + let bt = b.completion.ends_with('~'); + + at.cmp(&bt) +} + +/// Unique the list of completions, without perturbing their order. +fn unique_completions_retaining_order(comps: &mut Vec) { + let mut seen = HashSet::with_capacity(comps.len()); + + let pred = |c: &Completion| { + // Keep (return true) if insertion succeeds. + // todo!("don't clone here"); + seen.insert(c.completion.to_owned()) + }; + + comps.retain(pred); +} + +/// Sorts and removes any duplicate completions in the completion list, then puts them in priority +/// order. +pub fn sort_and_prioritize(comps: &mut Vec, flags: CompletionRequestOptions) { + if comps.is_empty() { + return; + } + + // Find the best rank. + let best_rank = comps.iter().map(Completion::rank).min().unwrap(); + + // Throw out completions of worse ranks. + comps.retain(|c| c.rank() == best_rank); + + // Deduplicate both sorted and unsorted results. + unique_completions_retaining_order(comps); + + // Sort, provided DONT_SORT isn't set. + // Here we do not pass suppress_exact, so that exact matches appear first. + comps.sort_by(natural_compare_completions); + + // Lastly, if this is for an autosuggestion, prefer to avoid completions that duplicate + // arguments, and penalize files that end in tilde - they're frequently autosave files from e.g. + // emacs. Also prefer samecase to smartcase. + if flags.autosuggestion { + comps.sort_by(|a, b| { + a.r#match + .case_fold + .cmp(&b.r#match.case_fold) + .then_with(|| compare_completions_by_duplicate_arguments(a, b)) + .then_with(|| compare_completions_by_tilde(a, b)) + }) + } +} + +/// Bag of data to support expanding a command's arguments using custom completions, including +/// the wrap chain. +struct CustomArgData<'a> { + /// The unescaped argument before the argument which is being completed, or empty if none. + previous_argument: WString, + /// The unescaped argument which is being completed, or empty if none. + current_argument: WString, + /// Whether a -- has been encountered, which suppresses options. + had_ddash: bool, + /// Whether to perform file completions. + /// This is an "out" parameter of the wrap chain walk: if any wrapped command suppresses file + /// completions this gets set to false. + do_file: bool, + /// Depth in the wrap chain. + wrap_depth: usize, + /// The list of variable assignments: escaped strings of the form VAR=VAL. + /// This may be temporarily appended to as we explore the wrap chain. + /// When completing, variable assignments are really set in a local scope. + var_assignments: &'a mut Vec, + /// The set of wrapped commands which we have visited, and so should not be explored again. + visited_wrapped_commands: HashSet, +} + +impl<'a> CustomArgData<'a> { + pub fn new(var_assignments: &'a mut Vec) -> Self { + Self { + previous_argument: WString::new(), + current_argument: WString::new(), + had_ddash: false, + do_file: true, + wrap_depth: 0, + var_assignments, + visited_wrapped_commands: HashSet::new(), + } + } +} + +/// Class representing an attempt to compute completions. +struct Completer<'ctx> { + /// The operation context for this completion. + ctx: &'ctx OperationContext<'ctx>, + /// Flags associated with the completion request. + flags: CompletionRequestOptions, + /// The output completions. + completions: CompletionReceiver, + /// Commands which we would have tried to load, if we had a parser. + needs_load: Vec, + /// Table of completions conditions that have already been tested and the corresponding test + /// results. + condition_cache: HashMap, +} + +static completion_autoloader: Lazy> = + Lazy::new(|| Mutex::new(Autoload::new(L!("fish_complete_path")))); + +impl<'ctx> Completer<'ctx> { + pub fn new(ctx: &'ctx OperationContext<'ctx>, flags: CompletionRequestOptions) -> Self { + Self { + ctx, + flags, + completions: CompletionReceiver::new(ctx.expansion_limit), + needs_load: vec![], + condition_cache: HashMap::new(), + } + } + + pub fn perform_for_commandline(&mut self, cmdline: WString) { + // Limit recursion, in case a user-defined completion has cycles, or the completion for "x" + // wraps "A=B x" (#3474, #7344). No need to do that when there is no parser: this happens only + // for autosuggestions where we don't evaluate command substitutions or variable assignments. + let _decrement = if let Some(parser) = self.ctx.maybe_parser() { + let level = &mut parser.libdata_mut().pods.complete_recursion_level; + if *level >= 24 { + FLOG!( + error, + wgettext!("completion reached maximum recursion depth, possible cycle?"), + ); + return; + } + *level += 1; + + Some(ScopeGuard::new((), |()| { + let level = &mut parser.libdata_mut().pods.complete_recursion_level; + *level -= 1; + })) + } else { + None + }; + + let cursor_pos = cmdline.len(); + let is_autosuggest = self.flags.autosuggestion; + + // Find the process to operate on. The cursor may be past it (#1261), so backtrack + // until we know we're no longer in a space. But the space may actually be part of the + // argument (#2477). + let mut position_in_statement = cursor_pos; + while position_in_statement > 0 && cmdline.char_at(position_in_statement - 1) == ' ' { + position_in_statement -= 1; + } + + // Get all the arguments. + let mut tokens = Vec::new(); + parse_util_process_extent(&cmdline, position_in_statement, Some(&mut tokens)); + let actual_token_count = tokens.len(); + + // Hack: fix autosuggestion by removing prefixing "and"s #6249. + if is_autosuggest { + tokens.retain(|token| !parser_keywords_is_subcommand(token.get_source(&cmdline))); + } + + // Consume variable assignments in tokens strictly before the cursor. + // This is a list of (escaped) strings of the form VAR=VAL. + // TODO: filter_drain + let mut var_assignments = Vec::new(); + for tok in &tokens { + if tok.location_in_or_at_end_of_source_range(cursor_pos) { + break; + } + let tok_src = tok.get_source(&cmdline); + if variable_assignment_equals_pos(tok_src).is_none() { + break; + } + var_assignments.push(tok_src.to_owned()); + } + tokens.drain(..var_assignments.len()); + + // Empty process (cursor is after one of ;, &, |, \n, &&, || modulo whitespace). + let [first_token, ..] = tokens.as_slice() else { + // Don't autosuggest anything based on the empty string (generalizes #1631). + if is_autosuggest { + return; + } + self.complete_cmd(WString::new()); + self.complete_abbr(WString::new()); + return; + }; + + let effective_cmdline = if tokens.len() == actual_token_count { + &cmdline + } else { + &cmdline[first_token.offset()..] + }; + + if tokens.last().unwrap().type_ == TokenType::comment { + return; + } + tokens.retain(|tok| tok.type_ != TokenType::comment); + assert!(!tokens.is_empty()); + + let cmd_tok = tokens.first().unwrap(); + let cur_tok = tokens.last().unwrap(); + + // Since fish does not currently support redirect in command position, we return here. + if cmd_tok.type_ != TokenType::string { + return; + } + if cur_tok.type_ == TokenType::error { + return; + } + for tok in &tokens { + // If there was an error, it was in the last token. + assert!(matches!(tok.type_, TokenType::string | TokenType::redirect)); + } + // If we are completing a variable name or a tilde expansion user name, we do that and + // return. No need for any other completions. + let current_token = cur_tok.get_source(&cmdline); + if cur_tok.location_in_or_at_end_of_source_range(cursor_pos) + && (self.try_complete_variable(current_token) || self.try_complete_user(current_token)) + { + return; + } + + if cmd_tok.location_in_or_at_end_of_source_range(cursor_pos) { + let equals_sign_pos = variable_assignment_equals_pos(current_token); + if equals_sign_pos.is_some() { + self.complete_param_expand( + current_token, + true, /* do_file */ + false, /* handle_as_special_cd */ + ); + return; + } + // Complete command filename. + let current_token = current_token.to_owned(); + self.complete_cmd(current_token.clone()); + self.complete_abbr(current_token); + return; + } + // See whether we are in an argument, in a redirection or in the whitespace in between. + let mut in_redirection = cur_tok.type_ == TokenType::redirect; + + let mut had_ddash = false; + let mut current_argument = L!(""); + let mut previous_argument = L!(""); + if cur_tok.type_ == TokenType::string + && cur_tok.location_in_or_at_end_of_source_range(position_in_statement) + { + // If the cursor is in whitespace, then the "current" argument is empty and the + // previous argument is the matching one. But if the cursor was in or at the end + // of the argument, then the current argument is the matching one, and the + // previous argument is the one before it. + let cursor_in_whitespace = !cur_tok.location_in_or_at_end_of_source_range(cursor_pos); + if cursor_in_whitespace { + previous_argument = current_token; + } else { + current_argument = current_token; + if tokens.len() >= 2 { + let prev_tok = &tokens[tokens.len() - 2]; + if prev_tok.type_ == TokenType::string { + previous_argument = prev_tok.get_source(&cmdline); + } + in_redirection = prev_tok.type_ == TokenType::redirect; + } + } + + // Check to see if we have a preceding double-dash. + for tok in &tokens[..tokens.len() - 1] { + if tok.get_source(&cmdline) == L!("--") { + had_ddash = true; + break; + } + } + } + + let mut do_file = false; + let mut handle_as_special_cd = false; + if in_redirection { + do_file = true; + } else { + // Try completing as an argument. + let mut arg_data = CustomArgData::new(&mut var_assignments); + arg_data.had_ddash = had_ddash; + + let bias = cmdline.len() - effective_cmdline.len(); + let command_range = SourceRange::new(cmd_tok.offset() - bias, cmd_tok.length()); + + let mut exp_command = cmd_tok.get_source(&cmdline).to_owned(); + let mut prev = None; + let mut cur = None; + if expand_command_token(self.ctx, &mut exp_command) { + prev = unescape_string(previous_argument, UnescapeStringStyle::default()); + cur = unescape_string( + current_argument, + UnescapeStringStyle::Script(UnescapeFlags::INCOMPLETE), + ); + } + if let (Some(prev), Some(cur)) = (prev, cur) { + arg_data.previous_argument = prev; + arg_data.current_argument = cur; + // Have to walk over the command and its entire wrap chain. If any command + // disables do_file, then they all do. + self.walk_wrap_chain( + &exp_command, + effective_cmdline, + command_range, + &mut arg_data, + ); + do_file = arg_data.do_file; + + // If we're autosuggesting, and the token is empty, don't do file suggestions. + if is_autosuggest && arg_data.current_argument.is_empty() { + do_file = false; + } + } + + // Hack. If we're cd, handle it specially (issue #1059, others). + handle_as_special_cd = + exp_command == L!("cd") || arg_data.visited_wrapped_commands.contains(L!("cd")); + } + + // Maybe apply variable assignments. + let _restore_vars = + self.apply_var_assignments(var_assignments.iter().map(|s| s.as_utfstr())); + if self.ctx.check_cancel() { + return; + } + + // This function wants the unescaped string. + self.complete_param_expand(current_argument, do_file, handle_as_special_cd); + + // Escape '[' in the argument before completing it. + self.escape_opening_brackets(current_argument); + + // Lastly mark any completions that appear to already be present in arguments. + self.mark_completions_duplicating_arguments(&cmdline, current_token, tokens); + } + + pub fn acquire_completions(&mut self) -> Vec { + self.completions.take() + } + + pub fn acquire_needs_load(&mut self) -> Vec { + mem::take(&mut self.needs_load) + } + + /// Test if the specified script returns zero. The result is cached, so that if multiple completions + /// use the same condition, it needs only be evaluated once. condition_cache_clear must be called + /// after a completion run to make sure that there are no stale completions. + fn condition_test(&mut self, condition: &wstr) -> bool { + if condition.is_empty() { + return true; + } + let Some(parser) = self.ctx.maybe_parser() else { + return false; + }; + + let cached_entry = self.condition_cache.get(condition); + if let Some(&entry) = cached_entry { + // Use the old value. + entry + } else { + // Compute new value and reinsert it. + let test_res = exec_subshell( + condition, parser, None, false, /* don't apply exit status */ + ) == 0; + self.condition_cache.insert(condition.to_owned(), test_res); + test_res + } + } + + fn conditions_test(&mut self, conditions: &[WString]) -> bool { + conditions.iter().all(|c| self.condition_test(c)) + } + + /// Copy any strings in `possible_comp` which have the specified prefix to the + /// completer's completion array. The prefix may contain wildcards. The output + /// will consist of [`Completion`] structs. + /// + /// There are three ways to specify descriptions for each completion. Firstly, + /// if a description has already been added to the completion, it is _not_ + /// replaced. Secondly, if the `desc_func` function is specified, use it to + /// determine a dynamic completion. Thirdly, if none of the above are available, + /// the `desc` string is used as a description. + /// + /// - `wc_escaped`: the prefix, possibly containing wildcards. The wildcard should not have + /// been unescaped, i.e. '*' should be used for any string, not the `ANY_STRING` character. + /// - `desc_func`: the function that generates a description for those completions without an + /// embedded description + /// - `possible_comp`: the list of possible completions to iterate over + /// - `flags`: The flags controlling completion + /// - `extra_expand_flags`: Additional flags controlling expansion. + fn complete_strings( + &mut self, + wc_escaped: &wstr, + desc_func: &DescriptionFunc, + possible_comp: &[Completion], + flags: CompleteFlags, + extra_expand_flags: ExpandFlags, + ) { + let mut tmp = wc_escaped.to_owned(); + if !expand_one( + &mut tmp, + self.expand_flags() + | extra_expand_flags + | ExpandFlags::SKIP_CMDSUBST + | ExpandFlags::SKIP_WILDCARDS, + self.ctx, + None, + ) { + return; + } + + let wc = parse_util_unescape_wildcards(&tmp); + for comp in possible_comp { + let comp_str = &comp.completion; + if !comp_str.is_empty() { + let expand_flags = self.expand_flags() | extra_expand_flags; + wildcard_complete( + comp_str, + &wc, + Some(desc_func), + Some(&mut self.completions), + expand_flags, + flags, + ); + } + } + } + + fn expand_flags(&self) -> ExpandFlags { + let mut result = ExpandFlags::empty(); + result.set(ExpandFlags::SKIP_CMDSUBST, self.flags.autosuggestion); + result.set(ExpandFlags::FUZZY_MATCH, self.flags.fuzzy_match); + result.set(ExpandFlags::GEN_DESCRIPTIONS, self.flags.descriptions); + result + } + + /// If command to complete is short enough, substitute the description with the whatis information + /// for the executable. + fn complete_cmd_desc(&mut self, s: &wstr) { + let Some(parser) = self.ctx.maybe_parser() else { + return; + }; + + let cmd = if let Some(pos) = s.chars().rposition(|c| c == '/') { + if pos + 1 > s.len() { + return; + } + &s[pos + 1..] + } else { + s + }; + + // Using apropos with a single-character search term produces far too many results - require at + // least two characters if we don't know the location of the whatis-database. + if cmd.len() < 2 { + return; + } + + if wildcard_has(cmd) { + return; + } + + let keep_going = + self.completions.get_list().iter().any(|c| { + c.completion.is_empty() || c.completion.as_char_slice().last() != Some(&'/') + }); + if !keep_going { + return; + } + + let lookup_cmd: WString = [L!("__fish_describe_command "), &escape(cmd)] + .into_iter() + .collect(); + + // First locate a list of possible descriptions using a single call to apropos or a direct + // search if we know the location of the whatis database. This can take some time on slower + // systems with a large set of manuals, but it should be ok since apropos is only called once. + let mut list = vec![]; + exec_subshell( + &lookup_cmd, + parser, + Some(&mut list), + false, /* don't apply exit status */ + ); + + // Then discard anything that is not a possible completion and put the result into a + // hashtable with the completion as key and the description as value. + let mut lookup = HashMap::new(); + // A typical entry is the command name, followed by a tab, followed by a description. + for elstr in &mut list { + // Skip keys that are too short. + if elstr.len() < cmd.len() { + continue; + } + + // Skip cases without a tab, or without a description, or bizarre cases where the tab is + // part of the command. + let Some(tab_idx) = elstr.find_char('\t') else { + continue; + }; + if tab_idx + 1 >= elstr.len() || tab_idx < cmd.len() { + continue; + } + + // Make the key. This is the stuff after the command. + // For example: + // elstr = lsmod + // cmd = ls + // key = mod + // Note an empty key is common and natural, if 'cmd' were already valid. + let (key, val) = elstr.split_at_mut(cmd.len()); + let val = &mut val[1..]; + assert!( + !val.is_empty(), + "tab index should not have been at the end." + ); + + // And once again I make sure the first character is uppercased because I like it that + // way, and I get to decide these things. + let mut upper_chars = val.as_char_slice()[0].to_uppercase(); + if let (Some(c), None) = (upper_chars.next(), upper_chars.next()) { + val.as_char_slice_mut()[0] = c; + } + lookup.insert(&*key, &*val); + } + + // Then do a lookup on every completion and if a match is found, change to the new + // description. + for completion in self.completions.get_list_mut() { + let el = &completion.completion; + if let Some(&desc) = lookup.get(el.as_utfstr()) { + completion.description = desc.to_owned(); + } + } + } + + /// Complete the specified command name. Search for executables in the path, executables defined + /// using an absolute path, functions, builtins and directories for implicit cd commands. + /// + /// \param str_cmd the command string to find completions for + fn complete_cmd(&mut self, str_cmd: WString) { + // Append all possible executables + let result = { + let expand_flags = self.expand_flags() + | ExpandFlags::SPECIAL_FOR_COMMAND + | ExpandFlags::FOR_COMPLETIONS + | ExpandFlags::PRESERVE_HOME_TILDES + | ExpandFlags::EXECUTABLES_ONLY; + expand_to_receiver( + str_cmd.clone(), + &mut self.completions, + expand_flags, + self.ctx, + None, + ) + .result + }; + if result == ExpandResultCode::cancel { + return; + } + if result == ExpandResultCode::ok && self.flags.descriptions { + self.complete_cmd_desc(&str_cmd); + } + + // We don't really care if this succeeds or fails. If it succeeds this->completions will be + // updated with choices for the user. + let _ = { + // Append all matching directories + let expand_flags = self.expand_flags() + | ExpandFlags::FOR_COMPLETIONS + | ExpandFlags::PRESERVE_HOME_TILDES + | ExpandFlags::DIRECTORIES_ONLY; + expand_to_receiver( + str_cmd.clone(), + &mut self.completions, + expand_flags, + self.ctx, + None, + ) + }; + + if str_cmd.is_empty() || (!str_cmd.contains('/') && str_cmd.as_char_slice()[0] != '~') { + let include_hidden = str_cmd.as_char_slice().first() == Some(&'_'); + // Append all known matching functions + let possible_comp: Vec<_> = function::get_names(include_hidden) + .into_iter() + .map(Completion::from_completion) + .collect(); + + self.complete_strings( + &str_cmd, + &{ Box::new(complete_function_desc) as DescriptionFunc }, + &possible_comp, + CompleteFlags::empty(), + ExpandFlags::empty(), + ); + + // Append all matching builtins + let possible_comp: Vec<_> = builtin_get_names() + .map(wstr::to_owned) + .map(Completion::from_completion) + .collect(); + + self.complete_strings( + &str_cmd, + &{ Box::new(|name| builtin_get_desc(name).unwrap_or(L!("")).to_owned()) }, + &possible_comp, + CompleteFlags::empty(), + ExpandFlags::empty(), + ); + } + } + + /// Attempt to complete an abbreviation for the given string. + fn complete_abbr(&mut self, cmd: WString) { + // Copy the list of names and descriptions so as not to hold the lock across the call to + // complete_strings. + let mut possible_comp = Vec::new(); + let mut descs = HashMap::new(); + with_abbrs(|set| { + for abbr in set.list() { + if !abbr.is_regex() { + possible_comp.push(Completion::from_completion(abbr.key.clone())); + descs.insert(abbr.key.clone(), abbr.replacement.clone()); + } + } + }); + + let desc_func = move |key: &wstr| { + let replacement = descs.get(key).expect("Abbreviation not found"); + sprintf!(*ABBR_DESC, replacement) + }; + self.complete_strings( + &cmd, + &{ Box::new(desc_func) as _ }, + &possible_comp, + CompleteFlags::NO_SPACE, + ExpandFlags::empty(), + ); + } + + /// Evaluate the argument list (as supplied by `complete -a`) and insert any + /// return matching completions. Matching is done using `copy_strings_with_prefix`, + /// meaning the completion may contain wildcards. + /// Logically, this is not always the right thing to do, but I have yet to come + /// up with a case where this matters. + /// + /// - `str`: The string to complete. + /// - `args`: The list of option arguments to be evaluated. + /// - `desc`: Description of the completion + /// - `flags`: The flags + fn complete_from_args(&mut self, s: &wstr, args: &wstr, desc: &wstr, flags: CompleteFlags) { + let is_autosuggest = self.flags.autosuggestion; + + let saved_state = if let Some(parser) = self.ctx.maybe_parser() { + let saved_interactive = parser.libdata().pods.is_interactive; + parser.libdata_mut().pods.is_interactive = false; + + Some((saved_interactive, parser.get_last_statuses())) + } else { + None + }; + + let eflags = if is_autosuggest { + ExpandFlags::SKIP_CMDSUBST + } else { + ExpandFlags::empty() + }; + + let possible_comp = Parser::expand_argument_list(args, eflags, self.ctx); + + if let Some(parser) = self.ctx.maybe_parser() { + let (saved_interactive, status) = saved_state.unwrap(); + parser.libdata_mut().pods.is_interactive = saved_interactive; + parser.set_last_statuses(status); + } + + // Allow leading dots - see #3707. + self.complete_strings( + &escape(s), + &const_desc(desc), + &possible_comp, + flags, + ExpandFlags::ALLOW_NONLITERAL_LEADING_DOT, + ); + } + + /// complete_param: Given a command, find completions for the argument `s` of command `cmd_orig` + /// with previous option `popt`. If file completions should be disabled, then mark + /// `out_do_file` as `false`. + /// + /// Returns `true` if successful, `false` if there's an error. + /// + /// Examples in format (cmd, popt, str): + /// + /// ```text + /// echo hello world -> ("echo", "world", "") + /// echo hello world -> ("echo", "hello", "world") + /// ``` + fn complete_param_for_command( + &mut self, + cmd_orig: &wstr, + popt: &wstr, + s: &wstr, + use_switches: bool, + out_do_file: &mut bool, + ) -> bool { + let mut use_files = true; + let mut has_force = false; + + let CmdString { cmd, path } = parse_cmd_string(cmd_orig, self.ctx.vars()); + + // Don't use cmd_orig here for paths. It's potentially pathed, + // so that command might exist, but the completion script + // won't be using it. + let cmd_exists = builtin_exists(&cmd) + || function::exists_no_autoload(&cmd) + || path_get_path(&cmd, self.ctx.vars()).is_some(); + if !cmd_exists { + // Do not load custom completions if the command does not exist + // This prevents errors caused during the execution of completion providers for + // tools that do not exist. Applies to both manual completions ("cm", "cmd ") + // and automatic completions ("gi" autosuggestion provider -> git) + FLOG!(complete, "Skipping completions for non-existent command"); + } else if let Some(parser) = self.ctx.maybe_parser() { + complete_load(&cmd, parser); + } else if !completion_autoloader + .lock() + .unwrap() + .has_attempted_autoload(&cmd) + { + self.needs_load.push(cmd.clone()); + } + + // Make a list of lists of all options that we care about. + let all_options: Vec> = COMPLETION_MAP + .lock() + .unwrap() + .iter() + .filter_map(|(idx, completion)| { + let r#match = if idx.is_path { &path } else { &cmd }; + if wildcard_match(r#match, &idx.name, false) { + // Copy all of their options into our list. Oof, this is a lot of copying. + let mut options = completion.get_options().to_vec(); + // We have to copy them in reverse order to preserve legacy behavior (#9221). + options.reverse(); + Some(options) + } else { + None + } + }) + .collect(); + + // Now release the lock and test each option that we captured above. We have to do this outside + // the lock because callouts (like the condition) may add or remove completions. See issue #2. + for options in all_options { + let short_opt_pos = short_option_pos(s, &options); + // We want last_option_requires_param to default to false but distinguish between when + // a previous completion has set it to false and when it has its default value. + let mut last_option_requires_param = None; + let mut use_common = true; + if use_switches { + if s.char_at(0) == '-' { + // Check if we are entering a combined option and argument (like --color=auto or + // -I/usr/include). + for o in &options { + let arg = if o.typ == CompleteOptionType::Short { + let Some(short_opt_pos) = short_opt_pos else { + continue; + }; + if o.option.char_at(0) != s.char_at(short_opt_pos) { + continue; + } + Some(s.slice_from(short_opt_pos + 1)) + } else { + param_match2(o, s) + }; + + if self.conditions_test(&o.conditions) { + if o.typ == CompleteOptionType::Short { + // Only override a true last_option_requires_param value with a false + // one + *last_option_requires_param + .get_or_insert(o.result_mode.requires_param) &= + o.result_mode.requires_param; + } + if let Some(arg) = arg { + if o.result_mode.requires_param { + use_common = false; + } + if o.result_mode.no_files { + use_files = false; + } + if o.result_mode.force_files { + has_force = true; + } + self.complete_from_args(arg, &o.comp, o.localized_desc(), o.flags); + } + } + } + } else if popt.char_at(0) == '-' { + // Set to true if we found a matching old-style switch. + // Here we are testing the previous argument, + // to see how we should complete the current argument + let mut old_style_match = false; + + // If we are using old style long options, check for them first. + for o in &options { + if o.typ == CompleteOptionType::SingleLong + && param_match(o, popt) + && self.conditions_test(&o.conditions) + { + old_style_match = false; + if o.result_mode.requires_param { + use_common = false; + } + if o.result_mode.no_files { + use_files = false; + } + if o.result_mode.force_files { + has_force = true; + } + self.complete_from_args(s, &o.comp, o.localized_desc(), o.flags); + } + } + + // No old style option matched, or we are not using old style options. We check if + // any short (or gnu style) options do. + if !old_style_match { + let prev_short_opt_pos = short_option_pos(popt, &options); + for o in &options { + // Gnu-style options with _optional_ arguments must be specified as a single + // token, so that it can be differed from a regular argument. + // Here we are testing the previous argument for a GNU-style match, + // to see how we should complete the current argument + if !o.result_mode.requires_param { + continue; + } + + let mut r#match = false; + if o.typ == CompleteOptionType::Short { + if let Some(prev_short_opt_pos) = prev_short_opt_pos { + r#match = prev_short_opt_pos + 1 == popt.len() + && o.option.char_at(0) == popt.char_at(prev_short_opt_pos); + } + } else if o.typ == CompleteOptionType::DoubleLong { + r#match = param_match(o, popt); + } + if r#match && self.conditions_test(&o.conditions) { + if o.result_mode.requires_param { + use_common = false; + } + if o.result_mode.no_files { + use_files = false; + } + if o.result_mode.force_files { + has_force = true; + } + self.complete_from_args(s, &o.comp, o.localized_desc(), o.flags); + } + } + } + } + } + + if !use_common { + continue; + } + + // Set a default value for last_option_requires_param only if one hasn't been set + let last_option_requires_param = last_option_requires_param.unwrap_or(false); + + // Now we try to complete an option itself + for o in &options { + // If this entry is for the base command, check if any of the arguments match. + if !self.conditions_test(&o.conditions) { + continue; + } + if o.option.is_empty() { + use_files &= !o.result_mode.no_files; + has_force |= o.result_mode.force_files; + self.complete_from_args(s, &o.comp, o.localized_desc(), o.flags); + } + + if !use_switches || s.is_empty() { + continue; + } + + // Check if the short style option matches. + if o.typ == CompleteOptionType::Short { + let optchar = o.option.char_at(0); + if let Some(short_opt_pos) = short_opt_pos { + // Only complete when the last short option has no parameter yet.. + if short_opt_pos + 1 != s.len() { + continue; + } + // .. and it does not require one .. + if last_option_requires_param { + continue; + } + // .. and the option is not already there. + if s.contains(optchar) { + continue; + } + } else { + // str has no short option at all (but perhaps it is the + // prefix of a single long option). + // Only complete short options if there is no character after the dash. + + if s != L!("-") { + continue; + } + } + // It's a match. + let desc = o.localized_desc(); + // Append a short-style option + if !self + .completions + .add(Completion::with_desc(o.option.clone(), desc.to_owned())) + { + return false; + } + } + + // Check if the long style option matches. + if o.typ != CompleteOptionType::SingleLong + && o.typ != CompleteOptionType::DoubleLong + { + continue; + } + + let whole_opt = L!("-").repeat(o.expected_dash_count()) + o.option.as_utfstr(); + if whole_opt.len() < s.len() { + continue; + } + let r#match = string_prefixes_string(s, &whole_opt); + if !r#match { + let match_no_case = string_prefixes_string_case_insensitive(s, &whole_opt); + if !match_no_case { + continue; + } + } + + let mut offset = 0; + let mut flags = CompleteFlags::empty(); + + if r#match { + offset = s.len(); + } else { + flags = CompleteFlags::REPLACES_TOKEN; + } + + // does this switch have any known arguments + let has_arg = !o.comp.is_empty(); + // does this switch _require_ an argument + let req_arg = o.result_mode.requires_param; + + if o.typ == CompleteOptionType::DoubleLong && (has_arg && !req_arg) { + // Optional arguments to a switch can only be handled using the '=', so we add it as + // a completion. By default we avoid using '=' and instead rely on '--switch + // switch-arg', since it is more commonly supported by homebrew getopt-like + // functions. + let completion = sprintf!("%ls=", whole_opt.slice_from(offset)); + + // Append a long-style option with a mandatory trailing equal sign + if !self.completions.add(Completion::new( + completion, + o.localized_desc().to_owned(), + StringFuzzyMatch::exact_match(), + flags | CompleteFlags::NO_SPACE, + )) { + return false; + } + } + + // Append a long-style option + if !self.completions.add(Completion::new( + whole_opt.slice_from(offset).to_owned(), + o.localized_desc().to_owned(), + StringFuzzyMatch::exact_match(), + flags, + )) { + return false; + } + } + } + + if has_force { + *out_do_file = true; + } else if !use_files { + *out_do_file = false; + } + + true + } + + /// Perform generic (not command-specific) expansions on the specified string. + fn complete_param_expand(&mut self, s: &wstr, do_file: bool, handle_as_special_cd: bool) { + if self.ctx.check_cancel() { + return; + } + let mut flags = self.expand_flags() + | ExpandFlags::SKIP_CMDSUBST + | ExpandFlags::FOR_COMPLETIONS + | ExpandFlags::PRESERVE_HOME_TILDES; + if !do_file { + flags |= ExpandFlags::SKIP_WILDCARDS; + } + + if handle_as_special_cd && do_file { + if self.flags.autosuggestion { + flags |= ExpandFlags::SPECIAL_FOR_CD_AUTOSUGGESTION; + } + flags |= ExpandFlags::DIRECTORIES_ONLY; + flags |= ExpandFlags::SPECIAL_FOR_CD; + } + + // Squelch file descriptions per issue #254. + if self.flags.autosuggestion || do_file { + flags -= ExpandFlags::GEN_DESCRIPTIONS; + } + + // We have the following cases: + // + // --foo=bar => expand just bar + // -foo=bar => expand just bar + // foo=bar => expand the whole thing, and also just bar + // + // We also support colon separator (#2178). If there's more than one, prefer the last one. + let sep_index = s.chars().rposition(|c| c == '=' || c == ':'); + let complete_from_start = sep_index.is_none() || !string_prefixes_string(L!("-"), s); + + if let Some(sep_index) = sep_index { + // FIXME: This just cuts the token, + // so any quoting or braces gets lost. + // See #4954. + let sep_string = s.slice_from(sep_index + 1); + let mut local_completions = Vec::new(); + if expand_string( + sep_string.to_owned(), + &mut local_completions, + flags, + self.ctx, + None, + ) + .result + == ExpandResultCode::error + { + FLOGF!(complete, "Error while expanding string '%ls'", sep_string); + } + + // Any COMPLETE_REPLACES_TOKEN will also stomp the separator. We need to "repair" them by + // inserting our separator and prefix. + let prefix_with_sep = s.as_char_slice()[..sep_index + 1].into(); + for comp in &mut local_completions { + comp.prepend_token_prefix(prefix_with_sep); + } + if !self.completions.extend(local_completions) { + return; + } + } + + if complete_from_start { + // Don't do fuzzy matching for files if the string begins with a dash (issue #568). We could + // consider relaxing this if there was a preceding double-dash argument. + if string_prefixes_string(L!("-"), s) { + flags -= ExpandFlags::FUZZY_MATCH; + } + + if expand_to_receiver(s.to_owned(), &mut self.completions, flags, self.ctx, None).result + == ExpandResultCode::error + { + FLOGF!(complete, "Error while expanding string '%ls'", s); + } + } + } + + /// Complete the specified string as an environment variable. + /// Returns `true` if this was a variable, so we should stop completion. + fn complete_variable(&mut self, s: &wstr, start_offset: usize) -> bool { + let whole_var = s; + let var = whole_var.slice_from(start_offset); + let varlen = s.len() - start_offset; + let mut res = false; + + for env_name in self.ctx.vars().get_names(EnvMode::empty()) { + let anchor_start = !self.flags.fuzzy_match; + let Some(r#match) = string_fuzzy_match_string(var, &env_name, anchor_start) else { + continue; + }; + + let (comp, flags) = if !r#match.requires_full_replacement() { + // Take only the suffix. + ( + env_name.slice_from(varlen).to_owned(), + CompleteFlags::empty(), + ) + } else { + let comp = whole_var.slice_to(start_offset).to_owned() + env_name.as_utfstr(); + let flags = CompleteFlags::REPLACES_TOKEN | CompleteFlags::DONT_ESCAPE; + (comp, flags) + }; + + let mut desc = WString::new(); + if self.flags.descriptions && self.flags.autosuggestion { + // $history can be huge, don't put all of it in the completion description; see + // #6288. + if env_name == L!("history") { + let history = History::with_name(&history_session_id(self.ctx.vars())); + for i in 1..std::cmp::min(history.size(), 64) { + if i > 1 { + desc.push(' '); + } + desc.push_utfstr(&expand_escape_string( + history.item_at_index(i).unwrap().str(), + )); + } + } else { + // Can't use ctx.vars() here, it could be any variable. + let Some(var) = self.ctx.vars().get(&env_name) else { + continue; + }; + + let value = expand_escape_variable(&var); + desc = sprintf!(*COMPLETE_VAR_DESC_VAL, value); + } + } + + // Append matching environment variables + // TODO: need to propagate overflow here. + let _ = self + .completions + .add(Completion::new(comp, desc, r#match, flags)); + + res = true; + } + + res + } + + fn try_complete_variable(&mut self, s: &wstr) -> bool { + #[derive(PartialEq, Eq)] + enum Mode { + Unquoted, + SingleQuoted, + DoubleQuoted, + } + use Mode::*; + + let mut mode = Unquoted; + + // Get the position of the dollar heading a (possibly empty) run of valid variable characters. + let mut variable_start = None; + + let mut skip_next = false; + for (in_pos, c) in s.chars().enumerate() { + if skip_next { + skip_next = false; + continue; + } + + if !valid_var_name_char(c) { + // This character cannot be in a variable, reset the dollar. + variable_start = None; + } + + match c { + '\\' => skip_next = true, + '$' => { + if mode == Unquoted || mode == DoubleQuoted { + variable_start = Some(in_pos); + } + } + '\'' => { + if mode == SingleQuoted { + mode = Unquoted; + } else if mode == Unquoted { + mode = SingleQuoted; + } + } + '"' => { + if mode == DoubleQuoted { + mode = Unquoted; + } else if mode == Unquoted { + mode = DoubleQuoted; + } + } + _ => { + // all other chars ignored here + } + } + } + + // Now complete if we have a variable start. Note the variable text may be empty; in that case + // don't generate an autosuggestion, but do allow tab completion. + let allow_empty = !self.flags.autosuggestion; + let text_is_empty = variable_start == Some(s.len() - 1); + if let Some(variable_start) = variable_start { + if allow_empty || !text_is_empty { + return self.complete_variable(s, variable_start + 1); + } + } + + false + } + + /// Try to complete the specified string as a username. This is used by `~USER` type expansion. + /// + /// Returns `false` if unable to complete, `true` otherwise + fn try_complete_user(&mut self, s: &wstr) -> bool { + #[cfg(target_os = "android")] + { + // The getpwent() function does not exist on Android. A Linux user on Android isn't + // really a user - each installed app gets an UID assigned. Listing all UID:s is not + // possible without root access, and doing a ~USER type expansion does not make sense + // since every app is sandboxed and can't access eachother. + return false; + } + #[cfg(not(target_os = "android"))] + { + static s_setpwent_lock: Mutex<()> = Mutex::new(()); + + if s.char_at(0) != '~' || s.contains('/') { + return false; + } + + let user_name = s.slice_from(1); + if user_name.contains('~') { + return false; + } + + let start_time = Instant::now(); + let mut result = false; + let name_len = s.len() - 1; + + fn getpwent_name() -> Option { + let ptr = unsafe { libc::getpwent() }; + if ptr.is_null() { + return None; + } + let pw = unsafe { ptr.read() }; + Some(charptr2wcstring(pw.pw_name)) + } + + let _guard = s_setpwent_lock.lock().unwrap(); + + unsafe { libc::setpwent() }; + while let Some(pw_name) = getpwent_name() { + if self.ctx.check_cancel() { + break; + } + + if string_prefixes_string(user_name, &pw_name) { + let desc = sprintf!(*COMPLETE_USER_DESC, &pw_name); + // Append a user name. + // TODO: propagate overflow? + let _ = self.completions.add(Completion::new( + pw_name.slice_from(name_len).to_owned(), + desc, + StringFuzzyMatch::exact_match(), + CompleteFlags::NO_SPACE, + )); + result = true; + } else if string_prefixes_string_case_insensitive(user_name, &pw_name) { + let name = sprintf!("~%ls", &pw_name); + let desc = sprintf!(*COMPLETE_USER_DESC, &pw_name); + + // Append a user name + // TODO: propagate overflow? + let _ = self.completions.add(Completion::new( + name, + desc, + StringFuzzyMatch::exact_match(), + CompleteFlags::REPLACES_TOKEN + | CompleteFlags::DONT_ESCAPE + | CompleteFlags::NO_SPACE, + )); + result = true; + } + + // If we've spent too much time (more than 200 ms) doing this give up. + if start_time.elapsed() > Duration::from_millis(200) { + break; + } + } + unsafe { libc::endpwent() }; + + result + } + } + + /// If we have variable assignments, attempt to apply them in our parser. As soon as the return + /// value goes out of scope, the variables will be removed from the parser. + fn apply_var_assignments<'a>( + &mut self, + var_assignments: impl IntoIterator, + ) -> Option> { + if !self.ctx.has_parser() { + return None; + } + let parser = self.ctx.parser(); + + let mut var_assignments = var_assignments.into_iter().peekable(); + var_assignments.peek()?; + + let vars = parser.vars(); + assert_eq!( + self.ctx.vars() as *const _ as *const (), + vars as *const _ as *const (), + "Don't know how to tab complete with a parser but a different variable set" + ); + + // clone of parse_execution_context_t::apply_variable_assignments. + // Crucially do NOT expand subcommands: + // VAR=(launch_missiles) cmd + // should not launch missiles. + // Note we also do NOT send --on-variable events. + let expand_flags = ExpandFlags::SKIP_CMDSUBST; + let block = parser.push_block(Block::variable_assignment_block()); + for var_assign in var_assignments { + let equals_pos = variable_assignment_equals_pos(var_assign) + .expect("All variable assignments should have equals position"); + let variable_name = var_assign.as_char_slice()[..equals_pos].into(); + let expression = var_assign.slice_from(equals_pos + 1); + + let mut expression_expanded = Vec::new(); + let expand_ret = expand_string( + expression.to_owned(), + &mut expression_expanded, + expand_flags, + self.ctx, + None, + ); + // If expansion succeeds, set the value; if it fails (e.g. it has a cmdsub) set an empty + // value anyways. + let vals = if expand_ret.result == ExpandResultCode::ok { + expression_expanded + .into_iter() + .map(|c| c.completion) + .collect() + } else { + Vec::new() + }; + parser + .vars() + .set(variable_name, EnvMode::LOCAL | EnvMode::EXPORT, vals); + if self.ctx.check_cancel() { + break; + } + } + + let parser_ref = self.ctx.parser().shared(); + Some(ScopeGuard::new((), move |_| parser_ref.pop_block(block))) + } + + /// Complete a command by invoking user-specified completions. + fn complete_custom(&mut self, cmd: &wstr, cmdline: &wstr, ad: &mut CustomArgData) { + if self.ctx.check_cancel() { + return; + } + + let is_autosuggest = self.flags.autosuggestion; + // Perhaps set a transient commandline so that custom completions + // builtin_commandline will refer to the wrapped command. But not if + // we're doing autosuggestions. + let mut _remove_transient = None; + let wants_transient = + (ad.wrap_depth > 0 || !ad.var_assignments.is_empty()) && !is_autosuggest; + if wants_transient { + let parser_ref = self.ctx.parser().shared(); + parser_ref + .libdata_mut() + .transient_commandlines + .push(cmdline.to_owned()); + _remove_transient = Some(ScopeGuard::new((), move |_| { + parser_ref.libdata_mut().transient_commandlines.pop(); + })); + } + + // Maybe apply variable assignments. + let _restore_vars = + self.apply_var_assignments(ad.var_assignments.iter().map(WString::as_utfstr)); + if self.ctx.check_cancel() { + return; + } + + // Invoke any custom completions for this command. + self.complete_param_for_command( + cmd, + &ad.previous_argument, + &ad.current_argument, + !ad.had_ddash, + &mut ad.do_file, + ); + } + + // Invoke command-specific completions given by \p arg_data. + // Then, for each target wrapped by the given command, update the command + // line with that target and invoke this recursively. + // The command whose completions to use is given by \p cmd. The full command line is given by \p + // cmdline and the command's range in it is given by \p cmdrange. Note: the command range + // may have a different length than the command itself, because the command is unescaped (i.e. + // quotes removed). + fn walk_wrap_chain( + &mut self, + cmd: &wstr, + cmdline: &wstr, + cmdrange: SourceRange, + ad: &mut CustomArgData, + ) { + // Limit our recursion depth. This prevents cycles in the wrap chain graph from overflowing. + if ad.wrap_depth > 24 { + return; + } + if self.ctx.check_cancel() { + return; + } + + // Extract command from the command line and invoke the receiver with it. + self.complete_custom(cmd, cmdline, ad); + + let targets = complete_get_wrap_targets(cmd); + let wrap_depth = ad.wrap_depth; + let mut ad = ScopeGuard::new(ad, |ad| ad.wrap_depth = wrap_depth); + ad.wrap_depth += 1; + + for wt in targets { + // We may append to the variable assignment list; ensure we restore it. + let saved_var_count = ad.var_assignments.len(); + let mut ad = ScopeGuard::new(&mut ad, |ad| { + assert!( + ad.var_assignments.len() >= saved_var_count, + "Should not delete var assignments" + ); + ad.var_assignments.truncate(saved_var_count); + }); + + // Separate the wrap target into any variable assignments VAR=... and the command itself. + let mut wrapped_command = None; + let mut wrapped_command_offset_in_wt = None; + let tokenizer = Tokenizer::new(&wt, TokFlags(0)); + for tok in tokenizer { + let mut tok_src = tok.get_source(&wt).to_owned(); + if variable_assignment_equals_pos(&tok_src).is_some() { + ad.var_assignments.push(tok_src); + } else { + expand_command_token(self.ctx, &mut tok_src); + + wrapped_command_offset_in_wt = Some(tok.offset()); + wrapped_command = Some(tok_src); + + break; + } + } + + // Skip this wrapped command if empty, or if we've seen it before. + let Some((wrapped_command, wrapped_command_offset_in_wt)) = + Option::zip(wrapped_command, wrapped_command_offset_in_wt) + else { + continue; + }; + + if !ad.visited_wrapped_commands.insert(wrapped_command.clone()) { + continue; + } + + // Construct a fake command line containing the wrap target. + // https://github.com/starkat99/widestring-rs/issues/37 + let mut faux_commandline = cmdline.as_char_slice().to_vec(); + faux_commandline.splice(std::ops::Range::from(cmdrange), wt.chars()); + let faux_commandline = WString::from(faux_commandline); + + // Recurse with our new command and command line. + let faux_source_range = SourceRange::new( + cmdrange.start() + wrapped_command_offset_in_wt, + wrapped_command.len(), + ); + self.walk_wrap_chain( + &wrapped_command, + &faux_commandline, + faux_source_range, + ***ad, + ); + } + } + + /// If the argument contains a '[' typed by the user, completion by appending to the argument might + /// produce an invalid token (#5831). + /// + /// Check if there is any unescaped, unquoted '['; if yes, make the completions replace the entire + /// argument instead of appending, so '[' will be escaped. + fn escape_opening_brackets(&mut self, argument: &wstr) { + let mut have_unquoted_unescaped_bracket = false; + let mut quote = None; + let mut escaped = false; + for c in argument.chars() { + have_unquoted_unescaped_bracket |= c == '[' && quote.is_none() && !escaped; + if escaped { + escaped = false; + } else if c == '\\' { + escaped = true; + } else if c == '\'' || c == '"' { + if quote == Some(c) { + // Closing a quote. + quote = None; + } else if quote.is_none() { + // Opening a quote. + quote = Some(c); + } + } + } + if !have_unquoted_unescaped_bracket { + return; + } + + // Since completion_apply_to_command_line will escape the completion, we need to provide an + // unescaped version. + let Some(unescaped_argument) = unescape_string( + argument, + UnescapeStringStyle::Script(UnescapeFlags::INCOMPLETE), + ) else { + return; + }; + for comp in self.completions.get_list_mut() { + if comp.flags.contains(CompleteFlags::REPLACES_TOKEN) { + continue; + } + comp.flags |= CompleteFlags::REPLACES_TOKEN; + comp.flags |= CompleteFlags::DONT_ESCAPE_TILDES; // See #9073. + + // We are grafting a completion that is expected to be escaped later. This will break + // if the original completion doesn't want escaping. Happily, this is only the case + // for username completion and variable name completion. They shouldn't end up here + // anyway because they won't contain '['. + if comp.flags.contains(CompleteFlags::DONT_ESCAPE) { + FLOG!(warning, "unexpected completion flag"); + } + comp.completion.insert_utfstr(0, &unescaped_argument); + } + } + + /// Set the `DUPLICATES_ARG` flag in any completion that duplicates an argument. + fn mark_completions_duplicating_arguments( + &mut self, + cmd: &wstr, + prefix: &wstr, + args: impl IntoIterator, + ) { + // Get all the arguments, unescaped, into an array that we're going to bsearch. + let mut arg_strs: Vec<_> = args + .into_iter() + .map(|arg| arg.get_source(cmd)) + .filter_map(|argstr| unescape_string(argstr, UnescapeStringStyle::default())) + .collect(); + arg_strs.sort(); + + let mut comp_str; + for comp in self.completions.get_list_mut() { + comp_str = comp.completion.clone(); + if !comp.flags.contains(CompleteFlags::REPLACES_TOKEN) { + comp_str.insert_utfstr(0, prefix); + } + if arg_strs.binary_search(&comp_str).is_ok() { + comp.flags |= CompleteFlags::DUPLICATES_ARGUMENT; + } + } + } +} + +struct CmdString { + cmd: WString, + path: WString, +} + +/// Find the full path and commandname from a command string `s`. +fn parse_cmd_string(s: &wstr, vars: &dyn Environment) -> CmdString { + let path_result = path_try_get_path(s, vars); + let found = path_result.err.is_none(); + let mut path = path_result.path; + // Resolve commands that use relative paths because we compare full paths with "complete -p". + if found && !path.is_empty() && path.as_char_slice().first() != Some(&'/') { + if let Some(full_path) = wrealpath(&path) { + path = full_path; + } + } + + // Make sure the path is not included in the command. + let cmd = if let Some(last_slash) = s.chars().rposition(|c| c == '/') { + &s[last_slash + 1..] + } else { + s + } + .to_owned(); + + CmdString { cmd, path } +} + +/// Returns a description for the specified function, or an empty string if none. +fn complete_function_desc(f: &wstr) -> WString { + if let Some(props) = function::get_props(f) { + props.description.clone() + } else { + WString::new() + } +} + +fn leading_dash_count(s: &wstr) -> usize { + s.chars().take_while(|&c| c == '-').count() +} + +/// Match a parameter. +fn param_match(e: &CompleteEntryOpt, optstr: &wstr) -> bool { + if e.typ == CompleteOptionType::ArgsOnly { + false + } else { + let dashes = leading_dash_count(optstr); + dashes == e.expected_dash_count() && e.option == optstr[dashes..] + } +} + +/// Test if a string is an option with an argument, like --color=auto or -I/usr/include. +fn param_match2<'s>(e: &CompleteEntryOpt, optstr: &'s wstr) -> Option<&'s wstr> { + // We may get a complete_entry_opt_t with no options if it's just arguments. + if e.option.is_empty() { + return None; + } + + // Verify leading dashes. + let mut cursor = leading_dash_count(optstr); + if cursor != e.expected_dash_count() { + return None; + } + + // Verify options match. + if !optstr.slice_from(cursor).starts_with(&e.option) { + return None; + } + cursor += e.option.len(); + + // Short options are like -DNDEBUG. Long options are like --color=auto. So check for an equal + // sign for long options. + assert!(e.typ != CompleteOptionType::Short); + if optstr.char_at(cursor) != '=' { + return None; + } + cursor += 1; + Some(optstr.slice_from(cursor)) +} + +/// Parses a token of short options plus one optional parameter like +/// '-xzPARAM', where x and z are short options. +/// +/// Returns the position of the last option character (e.g. the position of z which is 2). +/// Everything after that is assumed to be part of the parameter. +/// Returns wcstring::npos if there is no valid short option. +fn short_option_pos(arg: &wstr, options: &[CompleteEntryOpt]) -> Option { + if arg.len() <= 1 || leading_dash_count(arg) != 1 { + return None; + } + for (pos, arg_char) in arg.chars().enumerate().skip(1) { + let r#match = options + .iter() + .find(|o| o.typ == CompleteOptionType::Short && o.option.char_at(0) == arg_char); + if let Some(r#match) = r#match { + if r#match.result_mode.requires_param { + return Some(pos); + } + } else { + // The first character after the dash is not a valid option. + if pos == 1 { + return None; + } + return Some(pos - 1); + } + } + + Some(arg.len() - 1) +} + +fn expand_command_token(ctx: &OperationContext<'_>, cmd_tok: &mut WString) -> bool { + // TODO: we give up if the first token expands to more than one argument. We could handle + // that case by propagating arguments. + // Also we could expand wildcards. + expand_one( + cmd_tok, + ExpandFlags::SKIP_CMDSUBST | ExpandFlags::SKIP_WILDCARDS, + ctx, + None, + ) +} + +/// Create a new completion entry. +/// +/// \param completions The array of completions to append to +/// \param comp The completion string +/// \param desc The description of the completion +/// \param flags completion flags +#[deprecated = "Use Vec::push()"] +pub fn append_completion( + completions: &mut Vec, + comp: WString, + desc: WString, + flags: CompleteFlags, + r#match: StringFuzzyMatch, +) { + completions.push(Completion::new(comp, desc, r#match, flags)) +} + +/// Add an unexpanded completion "rule" to generate completions from for a command. +/// +/// # Examples +/// +/// The command 'gcc -o' requires that a file follows it, so the `requires_param` mode is suitable. +/// This can be done using the following line: +/// +/// ``` +/// complete -c gcc -s o -r +/// ``` +/// +/// The command 'grep -d' required that one of the strings 'read', 'skip' or 'recurse' is used. As +/// such, it is suitable to specify that a completion requires one of them. This can be done using +/// the following line: +/// +/// ``` +/// complete -c grep -s d -x -a "read skip recurse" +/// ``` +/// +/// - `cmd`: Command to complete. +/// - `cmd_is_path`: If `true`, cmd will be interpreted as the absolute +/// path of the program (optionally containing wildcards), otherwise it +/// will be interpreted as the command name. +/// - `option`: The name of an option. +/// - `option_type`: The type of option: can be option_type_short (-x), +/// option_type_single_long (-foo), option_type_double_long (--bar). +/// - `result_mode`: Controls how to search further completions when this completion has been +/// successfully matched. +/// - `comp`: A space separated list of completions which may contain subshells. +/// - `desc`: A description of the completion. +/// - `condition`: a command to be run to check it this completion should be used. If `condition` +/// is empty, the completion is always used. +/// - `flags`: A set of completion flags +#[allow(clippy::too_many_arguments)] +pub fn complete_add( + cmd: WString, + cmd_is_path: bool, + option: WString, + option_type: CompleteOptionType, + result_mode: CompletionMode, + condition: Vec, + comp: WString, + desc: WString, + flags: CompleteFlags, +) { + // option should be empty iff the option type is arguments only. + assert!(option.is_empty() == (option_type == CompleteOptionType::ArgsOnly)); + + // Lock the lock that allows us to edit the completion entry list. + let mut completion_map = COMPLETION_MAP.lock().expect("mutex poisoned"); + let c = &mut completion_map + .entry(CompletionEntryIndex { + name: cmd, + is_path: cmd_is_path, + }) + .or_insert_with(CompletionEntry::new); + + // Create our new option. + let opt = CompleteEntryOpt { + option, + typ: option_type, + result_mode, + comp, + desc, + conditions: condition, + flags, + }; + c.add_option(opt); +} + +/// Remove a previously defined completion. +pub fn complete_remove(cmd: WString, cmd_is_path: bool, option: &wstr, typ: CompleteOptionType) { + let mut completion_map = COMPLETION_MAP.lock().expect("mutex poisoned"); + let idx = CompletionEntryIndex { + name: cmd, + is_path: cmd_is_path, + }; + if let Some(c) = completion_map.get_mut(&idx) { + let delete_it = c.remove_option(option, typ); + if delete_it { + completion_map.remove(&idx); + } + } +} + +/// Removes all completions for a given command. +pub fn complete_remove_all(cmd: WString, cmd_is_path: bool) { + let mut completion_map = COMPLETION_MAP.lock().expect("mutex poisoned"); + completion_map.remove(&CompletionEntryIndex { + name: cmd, + is_path: cmd_is_path, + }); +} + +/// Returns all completions of the command cmd. +/// If `ctx` contains a parser, this will autoload functions and completions as needed. +/// If it does not contain a parser, then any completions which need autoloading will be returned. +pub fn complete( + cmd_with_subcmds: &wstr, + flags: CompletionRequestOptions, + ctx: &OperationContext, +) -> (Vec, Vec) { + // Determine the innermost subcommand. + let cmdsubst = parse_util_cmdsubst_extent(cmd_with_subcmds, cmd_with_subcmds.len()); + let cmd = cmd_with_subcmds[cmdsubst].to_owned(); + let mut completer = Completer::new(ctx, flags); + completer.perform_for_commandline(cmd); + + ( + completer.acquire_completions(), + completer.acquire_needs_load(), + ) +} + +/// Print the short switch `opt`, and the argument `arg` to the specified +/// [`WString`], but only if `argument` isn't an empty string. +fn append_switch_short_arg(out: &mut WString, opt: char, arg: &wstr) { + if arg.is_empty() { + return; + } + + sprintf!(=> out, " -%lc %ls", opt, escape(arg)); +} +fn append_switch_long_arg(out: &mut WString, opt: &wstr, arg: &wstr) { + if arg.is_empty() { + return; + } + + sprintf!(=> out, " --%ls %ls", opt, escape(arg)); +} +fn append_switch_short(out: &mut WString, opt: char) { + sprintf!(=> out, " -%lc", opt); +} +fn append_switch_long(out: &mut WString, opt: &wstr) { + sprintf!(=> out, " --%ls", opt); +} + +fn completion2string(index: &CompletionEntryIndex, o: &CompleteEntryOpt) -> WString { + let mut out = WString::from(L!("complete")); + + if o.flags.contains(CompleteFlags::DONT_SORT) { + append_switch_short(&mut out, 'k'); + } + + if o.result_mode.no_files && o.result_mode.requires_param { + append_switch_long(&mut out, L!("exclusive")); + } else if o.result_mode.no_files { + append_switch_long(&mut out, L!("no-files")); + } else if o.result_mode.force_files { + append_switch_long(&mut out, L!("force-files")); + } else if o.result_mode.requires_param { + append_switch_long(&mut out, L!("require-parameter")); + } + + if index.is_path { + append_switch_short_arg(&mut out, 'p', &index.name); + } else { + out.push(' '); + out.push_utfstr(&escape(&index.name)); + } + + match o.typ { + CompleteOptionType::ArgsOnly => {} + CompleteOptionType::Short => append_switch_short_arg(&mut out, 's', &o.option[..1]), + CompleteOptionType::SingleLong => append_switch_short_arg(&mut out, 'o', &o.option), + CompleteOptionType::DoubleLong => append_switch_short_arg(&mut out, 'l', &o.option), + } + + append_switch_short_arg(&mut out, 'd', o.localized_desc()); + append_switch_short_arg(&mut out, 'a', &o.comp); + for c in &o.conditions { + append_switch_short_arg(&mut out, 'n', c); + } + out.push('\n'); + + out +} + +/// Load command-specific completions for the specified command. +/// Returns `true` if something new was loaded, `false` if not. +pub fn complete_load(cmd: &wstr, parser: &Parser) -> bool { + let mut loaded_new = false; + + // We have to load this as a function, since it may define a --wraps or signature. + // See issue #2466. + if function::load(cmd, parser) { + // We autoloaded something; check if we have a --wraps. + loaded_new |= !complete_get_wrap_targets(cmd).is_empty(); + } + + // It's important to NOT hold the lock around completion loading. + // We need to take the lock to decide what to load, drop it to perform the load, then reacquire + // it. + // Note we only look at the global fish_function_path and fish_complete_path. + let path_to_load = completion_autoloader + .lock() + .expect("mutex poisoned") + .resolve_command(cmd, &**EnvStack::globals()); + if let Some(path_to_load) = path_to_load { + Autoload::perform_autoload(&path_to_load, parser); + completion_autoloader + .lock() + .expect("mutex poisoned") + .mark_autoload_finished(cmd); + loaded_new = true; + } + loaded_new +} + +/// Return a list of all current completions. +/// Used by the bare `complete`, loaded completions are printed out as commands +pub fn complete_print(cmd: &wstr) -> WString { + let mut out = WString::new(); + + // Get references to our completions and sort them by order. + let completions = COMPLETION_MAP.lock().expect("poisoned mutex"); + let mut completion_refs: Vec<_> = completions.iter().collect(); + completion_refs.sort_by_key(|(_, c)| c.order); + + for (key, entry) in completion_refs { + if !cmd.is_empty() && key.name != cmd { + continue; + } + + // Output in reverse order to preserve legacy behavior (see #9221). + for o in entry.get_options().iter().rev() { + out.push_utfstr(&completion2string(key, o)); + } + } + + // Append wraps. + let wrappers = wrapper_map.lock().expect("poisoned mutex"); + for (src, targets) in wrappers.iter() { + if !cmd.is_empty() && src != cmd { + continue; + } + for target in targets { + out.push_utfstr(L!("complete ")); + out.push_utfstr(&escape(src)); + append_switch_long_arg(&mut out, L!("wraps"), target); + out.push_utfstr(L!("\n")); + } + } + + out +} + +/// Observes that fish_complete_path has changed. +pub fn complete_invalidate_path() { + // TODO: here we unload all completions for commands that are loaded by the autoloader. We also + // unload any completions that the user may specified on the command line. We should in + // principle track those completions loaded by the autoloader alone. + + let cmds = completion_autoloader + .lock() + .expect("mutex poisoned") + .get_autoloaded_commands(); + for cmd in cmds { + complete_remove_all(cmd, false /* not a path */); + } +} + +/// Adds a "wrap target." A wrap target is a command that completes like another command. +pub fn complete_add_wrapper(command: WString, new_target: WString) -> bool { + if command.is_empty() || new_target.is_empty() { + return false; + } + + // If the command and the target are the same, + // there's no point in following the wrap-chain because we'd only complete the same thing. + // TODO: This should maybe include full cycle detection. + if command == new_target { + return false; + } + + let mut wrappers = wrapper_map.lock().expect("poisoned mutex"); + let targets = wrappers.entry(command).or_default(); + // If it's already present, we do nothing. + if !targets.contains(&new_target) { + targets.push(new_target); + } + + true +} + +/// Removes a wrap target. +pub fn complete_remove_wrapper(command: WString, target_to_remove: &wstr) -> bool { + if command.is_empty() || target_to_remove.is_empty() { + return false; + } + + let mut wrappers = wrapper_map.lock().expect("poisoned mutex"); + let mut result = false; + for targets in wrappers.values_mut() { + if let Some(pos) = targets.iter().position(|t| t == target_to_remove) { + targets.remove(pos); + result = true; + } + } + + result +} + +/// Returns a list of wrap targets for a given command. +pub fn complete_get_wrap_targets(command: &wstr) -> Vec { + if command.is_empty() { + return vec![]; + } + + let wrappers = wrapper_map.lock().expect("poisoned mutex"); + wrappers.get(command).cloned().unwrap_or_default() +} + +pub struct CompletionListFfi(pub CompletionList); + +pub use complete_ffi::CompletionRequestOptions; + +#[cxx::bridge] +mod complete_ffi { + extern "C++" { + include!("complete.h"); + include!("parser.h"); + type Parser = crate::parser::Parser; + type OperationContext<'a> = crate::operation_context::OperationContext<'a>; + type wcstring_list_ffi_t = crate::ffi::wcstring_list_ffi_t; + } + extern "Rust" { + type Completion; + type CompletionListFfi; + + fn new_completion() -> Box; + fn new_completion_with( + completion: &CxxWString, + description: &CxxWString, + flags: u8, + ) -> Box; + fn completion(self: &Completion) -> UniquePtr; + fn description(self: &Completion) -> UniquePtr; + fn flags(self: &Completion) -> u8; + fn set_flags(self: &mut Completion, value: u8); + fn replaces_commandline(self: &Completion) -> bool; + fn match_is_exact_or_prefix(self: &Completion) -> bool; + fn completion_erase(self: &mut Completion, begin: usize, end: usize); + fn rank(self: &Completion) -> u32; + #[cxx_name = "clone"] + fn clone_ffi(self: &Completion) -> Box; + + fn new_completion_list() -> Box; + fn size(self: &CompletionListFfi) -> usize; + fn empty(self: &CompletionListFfi) -> bool; + fn at(self: &CompletionListFfi, i: usize) -> &Completion; + fn at_mut(self: &mut CompletionListFfi, i: usize) -> &mut Completion; + fn clear(self: &mut CompletionListFfi); + fn complete_invalidate_path(); + fn reverse(self: &mut CompletionListFfi); + fn push_back(self: &mut CompletionListFfi, completion: &Completion); + fn sort_and_prioritize(self: &mut CompletionListFfi, flags: CompletionRequestOptions); + #[cxx_name = "complete_load"] + fn complete_load_ffi(cmd: &CxxWString, parser: &Parser) -> bool; + #[cxx_name = "complete"] + fn complete_ffi( + search_string: &CxxWString, + complete_flags: CompletionRequestOptions, + ctx: &OperationContext<'static>, + needs_load: &mut UniquePtr, + ) -> Box; + #[cxx_name = "append_completion"] + fn append_completion_ffi(completions: Pin<&mut CompletionListFfi>, comp: &CxxWString); + } + + #[derive(Clone, Copy)] + pub struct CompletionRequestOptions { + /// Requesting autosuggestion + pub autosuggestion: bool, + /// Make descriptions + pub descriptions: bool, + /// If set, we do not require a prefix match + pub fuzzy_match: bool, + } + + extern "Rust" { + fn completion_request_options_autosuggest() -> CompletionRequestOptions; + fn completion_request_options_normal() -> CompletionRequestOptions; + } +} + +fn complete_ffi( + search_string: &CxxWString, + complete_flags: CompletionRequestOptions, + ctx: &OperationContext<'static>, + needs_load: &mut UniquePtr, +) -> Box { + let (completions, to_load) = complete(search_string.as_wstr(), complete_flags, ctx); + if !needs_load.is_null() { + *needs_load = to_load.to_ffi(); + } + Box::new(CompletionListFfi(completions)) +} + +fn completion_request_options_autosuggest() -> CompletionRequestOptions { + CompletionRequestOptions::autosuggest() +} +fn completion_request_options_normal() -> CompletionRequestOptions { + CompletionRequestOptions::normal() +} + +impl Default for CompletionRequestOptions { + fn default() -> Self { + Self { + autosuggestion: false, + descriptions: false, + fuzzy_match: false, + } + } +} + +unsafe impl cxx::ExternType for CompletionListFfi { + type Id = cxx::type_id!("CompletionListFfi"); + type Kind = cxx::kind::Opaque; +} + +fn new_completion() -> Box { + Box::new(Completion::new( + "".into(), + "".into(), + StringFuzzyMatch::exact_match(), + CompleteFlags::default(), + )) +} +fn new_completion_with( + completion: &CxxWString, + description: &CxxWString, + flags: u8, +) -> Box { + Box::new(Completion::new( + completion.from_ffi(), + description.from_ffi(), + StringFuzzyMatch::exact_match(), + CompleteFlags::from_bits(flags).unwrap(), + )) +} +fn new_completion_list() -> Box { + Box::new(CompletionListFfi(CompletionList::new())) +} +fn append_completion_ffi(completions: Pin<&mut CompletionListFfi>, comp: &CxxWString) { + completions.get_mut().0.push(Completion::new( + comp.from_ffi(), + "".into(), + StringFuzzyMatch::exact_match(), + CompleteFlags::default(), + )); +} + +impl Completion { + fn completion(&self) -> UniquePtr { + self.completion.to_ffi() + } + fn description(&self) -> UniquePtr { + self.description.to_ffi() + } + fn flags(&self) -> u8 { + self.flags.bits() + } + fn set_flags(&mut self, value: u8) { + self.flags = CompleteFlags::from_bits(value).unwrap(); + } + fn clone_ffi(&self) -> Box { + Box::new(self.clone()) + } + fn match_is_exact_or_prefix(&self) -> bool { + self.r#match.is_exact_or_prefix() + } + fn completion_erase(&mut self, begin: usize, end: usize) { + self.completion.replace_range(begin..end, L!("")) + } +} +impl CompletionListFfi { + fn size(&self) -> usize { + self.0.len() + } + fn empty(&self) -> bool { + self.0.is_empty() + } + fn at(&self, i: usize) -> &Completion { + &self.0[i] + } + fn at_mut(&mut self, i: usize) -> &mut Completion { + &mut self.0[i] + } + fn reverse(&mut self) { + self.0.reverse(); + } + fn clear(&mut self) { + self.0.clear(); + } + fn push_back(&mut self, completion: &Completion) { + self.0.push(completion.clone()); + } + fn sort_and_prioritize(&mut self, flags: CompletionRequestOptions) { + sort_and_prioritize(&mut self.0, flags); + } +} + +fn complete_load_ffi(cmd: &CxxWString, parser: &Parser) -> bool { + complete_load(cmd.as_wstr(), parser) +} diff --git a/fish-rust/src/env/env_ffi.rs b/fish-rust/src/env/env_ffi.rs index 3b9bb321e..a440324af 100644 --- a/fish-rust/src/env/env_ffi.rs +++ b/fish-rust/src/env/env_ffi.rs @@ -1,22 +1,40 @@ -use super::environment::{self, EnvNull, EnvStack, EnvStackRef, Environment}; +use super::environment::{self, EnvDyn, EnvNull, EnvStack, EnvStackRef, Environment}; use super::var::{ElectricVar, EnvVar, EnvVarFlags, Statuses}; use crate::env::EnvMode; -use crate::event::Event; -use crate::ffi::{event_list_ffi_t, wchar_t, wcharz_t, wcstring_list_ffi_t}; -use crate::function::FunctionPropertiesRefFFI; +use crate::ffi::{wchar_t, wcharz_t, wcstring_list_ffi_t}; use crate::null_terminated_array::OwningNullTerminatedArrayRefFFI; use crate::signal::Signal; use crate::wchar_ffi::WCharToFFI; use crate::wchar_ffi::{AsWstr, WCharFromFFI}; +use core::ffi::c_char; use cxx::{CxxVector, CxxWString, UniquePtr}; +use lazy_static::lazy_static; +use std::ffi::c_int; use std::pin::Pin; +use crate::env::misc_init; + +impl From for c_int { + fn from(r: EnvStackSetResult) -> Self { + match r { + EnvStackSetResult::ENV_OK => 0, + EnvStackSetResult::ENV_PERM => 1, + EnvStackSetResult::ENV_SCOPE => 2, + EnvStackSetResult::ENV_INVALID => 3, + EnvStackSetResult::ENV_NOT_FOUND => 4, + _ => panic!(), + } + } +} + #[allow(clippy::module_inception)] #[cxx::bridge] mod env_ffi { + /// Return values for `EnvStack::set()`. #[repr(u8)] #[cxx_name = "env_stack_set_result_t"] + #[derive(Debug)] enum EnvStackSetResult { ENV_OK, ENV_PERM, @@ -27,15 +45,9 @@ enum EnvStackSetResult { extern "C++" { include!("env.h"); - include!("null_terminated_array.h"); include!("wutil.h"); - type event_list_ffi_t = super::event_list_ffi_t; type wcstring_list_ffi_t = super::wcstring_list_ffi_t; type wcharz_t = super::wcharz_t; - type function_properties_t = super::FunctionPropertiesRefFFI; - - type OwningNullTerminatedArrayRefFFI = - crate::null_terminated_array::OwningNullTerminatedArrayRefFFI; } extern "Rust" { @@ -92,6 +104,9 @@ fn env_var_create_from_name_ffi( #[cxx_name = "get_status"] fn get_status_ffi(&self) -> i32; + #[cxx_name = "statuses_just"] + fn statuses_just_ffi(s: i32) -> Box; + #[cxx_name = "get_pipestatus"] fn get_pipestatus_ffi(&self) -> &Vec; @@ -102,6 +117,7 @@ fn env_var_create_from_name_ffi( extern "Rust" { #[cxx_name = "EnvDyn"] type EnvDynFFI; + fn get(&self, name: &CxxWString) -> *mut EnvVar; fn getf(&self, name: &CxxWString, mode: u16) -> *mut EnvVar; fn get_names(&self, flags: u16, out: Pin<&mut wcstring_list_ffi_t>); } @@ -109,7 +125,13 @@ fn env_var_create_from_name_ffi( extern "Rust" { #[cxx_name = "EnvStackRef"] type EnvStackRefFFI; + fn env_stack_globals() -> &'static EnvStackRefFFI; + fn env_stack_principal() -> &'static EnvStackRefFFI; + fn set_one(&self, name: &CxxWString, flags: u16, value: &CxxWString) -> EnvStackSetResult; + fn get(&self, name: &CxxWString) -> *mut EnvVar; fn getf(&self, name: &CxxWString, mode: u16) -> *mut EnvVar; + fn get_unless_empty(&self, name: &CxxWString) -> *mut EnvVar; + fn getf_unless_empty(&self, name: &CxxWString, flags: u16) -> *mut EnvVar; fn get_names(&self, flags: u16, out: Pin<&mut wcstring_list_ffi_t>); fn is_principal(&self) -> bool; fn get_last_statuses(&self) -> Box; @@ -124,11 +146,12 @@ fn set( fn get_pwd_slash(&self) -> UniquePtr; fn set_pwd_from_getcwd(&self); - fn push(&mut self, new_scope: bool); - fn pop(&mut self); + fn push(&self, new_scope: bool); + fn pop(&self); - // Returns a ``Box.into_raw()``. - fn export_array(&self) -> *mut OwningNullTerminatedArrayRefFFI; + // Returns a Box.into_raw() cast to a void*. + // This is because we can't use the same C++ bindings to a Rust type from two different bridges. + fn export_array(&self) -> *mut c_char; fn snapshot(&self) -> Box; @@ -138,10 +161,6 @@ fn set( // Access the principal variable stack. fn env_get_principal_ffi() -> Box; - - fn universal_sync(&self, always: bool, out_events: Pin<&mut event_list_ffi_t>); - - fn apply_inherited_ffi(&self, props: &function_properties_t); } extern "Rust" { @@ -151,6 +170,8 @@ fn set( #[cxx_name = "rust_env_init"] fn rust_env_init_ffi(do_uvars: bool); + fn misc_init(); + #[cxx_name = "env_flags_for"] fn env_flags_for_ffi(name: wcharz_t) -> u8; } @@ -212,24 +233,64 @@ fn env_null_create_ffi() -> Box { Box::new(EnvNull::new()) } -/// FFI wrapper around dyn Environment. -pub struct EnvDynFFI(Box); +/// FFI wrapper around EnvDyn +pub struct EnvDynFFI(pub EnvDyn); impl EnvDynFFI { + fn get(&self, name: &CxxWString) -> *mut EnvVar { + EnvironmentFFI::getf_ffi(&self.0, name, 0) + } fn getf(&self, name: &CxxWString, mode: u16) -> *mut EnvVar { - EnvironmentFFI::getf_ffi(&*self.0, name, mode) + EnvironmentFFI::getf_ffi(&self.0, name, mode) } fn get_names(&self, flags: u16, out: Pin<&mut wcstring_list_ffi_t>) { - EnvironmentFFI::get_names_ffi(&*self.0, flags, out) + EnvironmentFFI::get_names_ffi(&self.0, flags, out) } } +unsafe impl cxx::ExternType for EnvDynFFI { + type Id = cxx::type_id!("EnvDyn"); // CXX name! + type Kind = cxx::kind::Opaque; +} /// FFI wrapper around EnvStackRef. +#[derive(Clone)] pub struct EnvStackRefFFI(pub EnvStackRef); +lazy_static! { + static ref GLOBALS: EnvStackRefFFI = EnvStackRefFFI(EnvStack::globals().clone()); +} +lazy_static! { + static ref PRINCIPAL_STACK: EnvStackRefFFI = EnvStackRefFFI(EnvStack::principal().clone()); +} + +fn env_stack_globals() -> &'static EnvStackRefFFI { + &GLOBALS +} +fn env_stack_principal() -> &'static EnvStackRefFFI { + &PRINCIPAL_STACK +} + impl EnvStackRefFFI { + fn set_one(&self, name: &CxxWString, flags: u16, value: &CxxWString) -> EnvStackSetResult { + self.0.set_one( + name.as_wstr(), + EnvMode::from_bits(flags).unwrap(), + value.from_ffi(), + ) + } + + fn get(&self, name: &CxxWString) -> *mut EnvVar { + EnvironmentFFI::getf_ffi(&*self.0, name, 0) + } fn getf(&self, name: &CxxWString, mode: u16) -> *mut EnvVar { EnvironmentFFI::getf_ffi(&*self.0, name, mode) } + fn get_unless_empty(&self, name: &CxxWString) -> *mut EnvVar { + EnvironmentFFI::getf_unless_empty_ffi(&*self.0, name, 0) + } + fn getf_unless_empty(&self, name: &CxxWString, mode: u16) -> *mut EnvVar { + EnvironmentFFI::getf_unless_empty_ffi(&*self.0, name, mode) + } + fn get_names(&self, flags: u16, out: Pin<&mut wcstring_list_ffi_t>) { EnvironmentFFI::get_names_ffi(&*self.0, flags, out) } @@ -280,39 +341,29 @@ fn remove(&self, name: &CxxWString, flags: u16) -> EnvStackSetResult { self.0.remove(name.as_wstr(), mode) } - fn export_array(&self) -> *mut OwningNullTerminatedArrayRefFFI { + fn export_array(&self) -> *mut c_char { Box::into_raw(Box::new(OwningNullTerminatedArrayRefFFI( self.0.export_array(), ))) + .cast() } fn snapshot(&self) -> Box { Box::new(EnvDynFFI(self.0.snapshot())) } - - fn universal_sync( - self: &EnvStackRefFFI, - always: bool, - mut out_events: Pin<&mut event_list_ffi_t>, - ) { - let events: Vec> = self.0.universal_sync(always); - for event in events { - out_events.as_mut().push(Box::into_raw(event).cast()); - } - } - - fn apply_inherited_ffi(&self, props: &FunctionPropertiesRefFFI) { - // Ported from C++: - // for (const auto &kv : props.inherit_vars) { - // vars.set(kv.first, ENV_LOCAL | ENV_USER, kv.second); - // } - for (name, vals) in props.0.inherit_vars() { - self.0 - .set(name, EnvMode::LOCAL | EnvMode::USER, vals.clone()); - } - } +} +unsafe impl cxx::ExternType for EnvStackRefFFI { + type Id = cxx::type_id!("EnvStackRef"); // CXX name! + type Kind = cxx::kind::Opaque; } +unsafe impl cxx::ExternType for Statuses { + type Id = cxx::type_id!("Statuses"); + type Kind = cxx::kind::Opaque; +} +fn statuses_just_ffi(s: i32) -> Box { + Box::new(Statuses::just(s)) +} impl Statuses { fn get_status_ffi(&self) -> i32 { self.status @@ -361,6 +412,21 @@ fn getf_ffi(&self, name: &CxxWString, mode: u16) -> *mut EnvVar { Some(var) => Box::into_raw(Box::new(var)), } } + fn getf_unless_empty_ffi(&self, name: &CxxWString, mode: u16) -> *mut EnvVar { + match self.getf( + name.as_wstr(), + EnvMode::from_bits(mode).expect("Invalid mode bits"), + ) { + None => std::ptr::null_mut(), + Some(var) => { + if var.is_empty() { + std::ptr::null_mut() + } else { + Box::into_raw(Box::new(var)) + } + } + } + } fn get_names_ffi(&self, mode: u16, mut out: Pin<&mut wcstring_list_ffi_t>) { let names = self.get_names(EnvMode::from_bits(mode).expect("Invalid mode bits")); for name in names { @@ -369,7 +435,7 @@ fn get_names_ffi(&self, mode: u16, mut out: Pin<&mut wcstring_list_ffi_t>) { } } -impl EnvironmentFFI for T {} +impl EnvironmentFFI for T {} fn var_is_electric_ffi(name: &CxxWString) -> bool { ElectricVar::for_name(name.as_wstr()).is_some() diff --git a/fish-rust/src/env/environment.rs b/fish-rust/src/env/environment.rs index 9025aea19..afdffaadc 100644 --- a/fish-rust/src/env/environment.rs +++ b/fish-rust/src/env/environment.rs @@ -5,10 +5,12 @@ use super::{ConfigPaths, ElectricVar}; use crate::abbrs::{abbrs_get_set, Abbreviation, Position}; use crate::common::{str2wcstring, unescape_string, wcs2zstring, UnescapeStringStyle}; +use crate::compat::{stdout_stream, C_PATH_BSHELL, _PATH_BSHELL}; use crate::env::{EnvMode, EnvStackSetResult, EnvVar, Statuses}; use crate::env_dispatch::{env_dispatch_init, env_dispatch_var_change}; +use crate::env_universal_common::{CallbackDataList, EnvUniversal}; use crate::event::Event; -use crate::ffi::{self, env_universal_t, universal_notifier_t}; +use crate::ffi; use crate::flog::FLOG; use crate::global_safety::RelaxedAtomicBool; use crate::null_terminated_array::OwningNullTerminatedArray; @@ -16,21 +18,22 @@ path_emit_config_directory_messages, path_get_config, path_get_data, path_make_canonical, paths_are_same_file, }; +use crate::proc::is_interactive_session; use crate::termsize; use crate::wchar::prelude::*; -use crate::wchar_ffi::{AsWstr, WCharFromFFI}; use crate::wcstringutil::join_strings; use crate::wutil::{fish_wcstol, wgetcwd, wgettext}; +use std::sync::atomic::Ordering; -use autocxx::WithinUniquePtr; -use cxx::UniquePtr; use lazy_static::lazy_static; -use libc::c_int; +use libc::{c_int, STDOUT_FILENO, _IONBF}; use once_cell::sync::OnceCell; use std::collections::HashMap; use std::ffi::CStr; +use std::io::Write; use std::mem::MaybeUninit; use std::os::unix::prelude::*; +use std::pin::Pin; use std::sync::{Arc, Mutex}; /// TODO: migrate to history once ported. @@ -38,21 +41,12 @@ // Universal variables instance. lazy_static! { - static ref UVARS: Mutex> = Mutex::new(env_universal_t::new_unique()); + static ref UVARS: Mutex = Mutex::new(EnvUniversal::new()); } /// Set when a universal variable has been modified but not yet been written to disk via sync(). static UVARS_LOCALLY_MODIFIED: RelaxedAtomicBool = RelaxedAtomicBool::new(false); -/// Convert an EnvVar to an FFI env_var_t. -pub fn env_var_to_ffi(var: Option) -> cxx::UniquePtr { - if let Some(var) = var { - ffi::env_var_t::new_ffi(Box::into_raw(Box::from(var)).cast()).within_unique_ptr() - } else { - cxx::UniquePtr::null() - } -} - /// An environment is read-only access to variable values. pub trait Environment { /// Get a variable by name using default flags. @@ -114,6 +108,32 @@ fn get_names(&self, _flags: EnvMode) -> Vec { } } +/// A helper type for wrapping a type-erased Environment. +pub struct EnvDyn { + inner: Box, +} + +impl EnvDyn { + // Exposed for testing. + pub fn new(inner: Box) -> Self { + Self { inner } + } +} + +impl Environment for EnvDyn { + fn getf(&self, key: &wstr, mode: EnvMode) -> Option { + self.inner.getf(key, mode) + } + + fn get_names(&self, flags: EnvMode) -> Vec { + self.inner.get_names(flags) + } + + fn get_pwd_slash(&self) -> WString { + self.inner.get_pwd_slash() + } +} + /// An immutable environment, used in snapshots. pub struct EnvScoped { inner: EnvMutex, @@ -136,7 +156,7 @@ pub struct EnvStack { } impl EnvStack { - fn new() -> EnvStack { + pub fn new() -> EnvStack { EnvStack { inner: EnvStackImpl::new(), } @@ -148,7 +168,7 @@ fn lock(&self) -> EnvMutexGuard { /// \return whether we are the principal stack. pub fn is_principal(&self) -> bool { - self as *const Self == Arc::as_ptr(&*PRINCIPAL_STACK) + std::ptr::eq(self, Self::principal().as_ref().get_ref()) } /// Helpers to get and set the proc statuses. @@ -279,9 +299,9 @@ pub fn export_array(&self) -> Arc { /// Snapshot this environment. This means returning a read-only copy. Local variables are copied /// but globals are shared (i.e. changes to global will be visible to this snapshot). - pub fn snapshot(&self) -> Box { + pub fn snapshot(&self) -> EnvDyn { let scoped = EnvScoped::from_impl(self.lock().base.snapshot()); - Box::new(scoped) + EnvDyn::new(Box::new(scoped) as Box) } /// Synchronizes universal variable changes. @@ -289,7 +309,7 @@ pub fn snapshot(&self) -> Box { /// instance (that is, look for changes from other fish instances). /// \return a list of events for changed variables. #[allow(clippy::vec_box)] - pub fn universal_sync(&self, always: bool) -> Vec> { + pub fn universal_sync(&self, always: bool) -> Vec { if UVAR_SCOPE_IS_GLOBAL.load() { return Vec::new(); } @@ -298,25 +318,23 @@ pub fn universal_sync(&self, always: bool) -> Vec> { } UVARS_LOCALLY_MODIFIED.store(false); - let mut unused = autocxx::c_int(0); - let sync_res_ptr = uvars().as_mut().unwrap().sync_ffi().within_unique_ptr(); - let sync_res = sync_res_ptr.as_ref().unwrap(); - if sync_res.get_changed() { - universal_notifier_t::default_notifier_ffi(std::pin::Pin::new(&mut unused)) - .post_notification(); + let mut callbacks = CallbackDataList::new(); + let changed = uvars().sync(&mut callbacks); + if changed { + ffi::env_universal_notifier_t_default_notifier_post_notification_ffi(); } // React internally to changes to special variables like LANG, and populate on-variable events. let mut result = Vec::new(); #[allow(unreachable_code)] - for idx in 0..sync_res.count() { - let name = sync_res.get_key(idx).from_ffi(); + for callback in callbacks { + let name = callback.key; env_dispatch_var_change(&name, self); - let evt = if sync_res.get_is_erase(idx) { + let evt = if callback.val.is_none() { Event::variable_erase(name) } else { Event::variable_set(name) }; - result.push(Box::new(evt)); + result.push(evt); } result } @@ -331,6 +349,10 @@ pub fn globals() -> &'static EnvStackRef { pub fn principal() -> &'static EnvStackRef { &PRINCIPAL_STACK } + + pub fn set_argv(&self, argv: Vec) { + self.set(L!("argv"), EnvMode::LOCAL, argv); + } } impl Environment for EnvScoped { @@ -365,17 +387,17 @@ fn get_pwd_slash(&self) -> WString { } } -pub type EnvStackRef = Arc; +pub type EnvStackRef = Pin>; // A variable stack that only represents globals. // Do not push or pop from this. lazy_static! { - static ref GLOBALS: EnvStackRef = Arc::new(EnvStack::new()); + static ref GLOBALS: EnvStackRef = Arc::pin(EnvStack::new()); } // Our singleton "principal" stack. lazy_static! { - static ref PRINCIPAL_STACK: EnvStackRef = Arc::new(EnvStack::new()); + static ref PRINCIPAL_STACK: EnvStackRef = Arc::pin(EnvStack::new()); } /// Some configuration path environment variables. @@ -647,7 +669,7 @@ pub fn env_init(paths: Option<&ConfigPaths>, do_uvars: bool, default_paths: bool // Set up SHLVL variable. Not we can't use vars.get() because SHLVL is read-only, and therefore // was not inherited from the environment. - if ffi::is_interactive_session() { + if is_interactive_session() { let nshlvl_str = if let Some(shlvl_var) = std::env::var_os("SHLVL") { // TODO: Figure out how to handle invalid numbers better. Shouldn't we issue a // diagnostic? @@ -718,33 +740,23 @@ pub fn env_init(paths: Option<&ConfigPaths>, do_uvars: bool, default_paths: bool if !do_uvars { UVAR_SCOPE_IS_GLOBAL.store(true); } else { + // let vars = EnvStack::principal(); + // Set up universal variables using the default path. - let callbacks = uvars() - .as_mut() - .unwrap() - .initialize_ffi() - .within_unique_ptr(); - let vars = EnvStack::principal(); - let callbacks = callbacks.as_ref().unwrap(); - for idx in 0..callbacks.count() { - let name = callbacks.get_key(idx).from_ffi(); - env_dispatch_var_change(&name, vars); + let mut callbacks = CallbackDataList::new(); + uvars().initialize(&mut callbacks); + for callback in callbacks { + env_dispatch_var_change(&callback.key, vars); } // Do not import variables that have the same name and value as // an exported universal variable. See issues #5258 and #5348. - let mut table = uvars() - .as_ref() - .unwrap() - .get_table_ffi() - .within_unique_ptr(); - for idx in 0..table.count() { - // autocxx gets confused when a value goes Rust -> Cxx -> Rust. - let uvar = table.as_mut().unwrap().get_var(idx).from_ffi(); + let uvars_locked = uvars(); + let table = uvars_locked.get_table(); + for (name, uvar) in table { if !uvar.exports() { continue; } - let name: &wstr = table.get_name(idx).as_wstr(); // Look for a global exported variable with the same name. let global = EnvStack::globals().getf(name, EnvMode::GLOBAL | EnvMode::EXPORT); @@ -759,15 +771,13 @@ pub fn env_init(paths: Option<&ConfigPaths>, do_uvars: bool, default_paths: bool let prefix_len = prefix.char_count(); let from_universal = true; let mut abbrs = abbrs_get_set(); - for idx in 0..table.count() { - let name: &wstr = table.get_name(idx).as_wstr(); + for (name, uvar) in table { if !name.starts_with(prefix) { continue; } let escaped_name = name.slice_from(prefix_len); if let Some(name) = unescape_string(escaped_name, UnescapeStringStyle::Var) { let key = name.clone(); - let uvar = table.get_var(idx).from_ffi(); let replacement: WString = join_strings(uvar.as_list(), ' '); abbrs.add(Abbreviation::new( name, @@ -780,3 +790,16 @@ pub fn env_init(paths: Option<&ConfigPaths>, do_uvars: bool, default_paths: bool } } } + +/// Various things we need to initialize at run-time that don't really fit any of the other init +/// routines. +pub fn misc_init() { + // If stdout is open on a tty ensure stdio is unbuffered. That's because those functions might + // be intermixed with `write()` calls and we need to ensure the writes are not reordered. See + // issue #3748. + if unsafe { libc::isatty(STDOUT_FILENO) } != 0 { + let _ = std::io::stdout().flush(); + unsafe { libc::setvbuf(stdout_stream(), std::ptr::null_mut(), _IONBF, 0) }; + } + _PATH_BSHELL.store(unsafe { C_PATH_BSHELL().cast_mut() }, Ordering::SeqCst); +} diff --git a/fish-rust/src/env/environment_impl.rs b/fish-rust/src/env/environment_impl.rs index 53d634c18..03c95804b 100644 --- a/fish-rust/src/env/environment_impl.rs +++ b/fish-rust/src/env/environment_impl.rs @@ -3,17 +3,17 @@ is_read_only, ElectricVar, EnvMode, EnvStackSetResult, EnvVar, EnvVarFlags, Statuses, VarTable, ELECTRIC_VARIABLES, PATH_ARRAY_SEP, }; -use crate::ffi::{self, env_universal_t}; +use crate::env_universal_common::EnvUniversal; +use crate::ffi; use crate::flog::FLOG; use crate::global_safety::RelaxedAtomicBool; +use crate::kill::kill_entries; use crate::null_terminated_array::OwningNullTerminatedArray; use crate::threads::{is_forked_child, is_main_thread}; use crate::wchar::prelude::*; use crate::wchar_ffi::{WCharFromFFI, WCharToFFI}; use crate::wutil::fish_wcstol_radix; -use autocxx::WithinUniquePtr; -use cxx::UniquePtr; use lazy_static::lazy_static; use std::cell::{RefCell, UnsafeCell}; use std::collections::HashSet; @@ -29,38 +29,23 @@ // Universal variables instance. lazy_static! { - static ref UVARS: Mutex> = Mutex::new(env_universal_t::new_unique()); + static ref UVARS: Mutex = Mutex::new(EnvUniversal::new()); } /// Getter for universal variables. /// This is typically initialized in env_init(), and is considered empty before then. -pub fn uvars() -> MutexGuard<'static, UniquePtr> { +pub fn uvars() -> MutexGuard<'static, EnvUniversal> { UVARS.lock().unwrap() } /// Whether we were launched with no_config; in this case setting a uvar instead sets a global. pub static UVAR_SCOPE_IS_GLOBAL: RelaxedAtomicBool = RelaxedAtomicBool::new(false); -/// Helper to get the kill ring. -fn get_kill_ring_entries() -> Vec { - crate::kill::kill_entries() -} - /// Helper to get the history for a session ID. fn get_history_var_text(history_session_id: &wstr) -> Vec { ffi::get_history_variable_text_ffi(&history_session_id.to_ffi()).from_ffi() } -/// Convert an FFI env_var_t to our EnvVar. -impl ffi::env_var_t { - #[allow(clippy::wrong_self_convention)] - pub fn from_ffi(&self) -> EnvVar { - let var_ptr: *const EnvVar = self.ffi_ptr().cast(); - let var: &EnvVar = unsafe { &*var_ptr }; - var.clone() - } -} - /// Apply the pathvar behavior, splitting about colons. pub fn colon_split>(val: &[T]) -> Vec { let mut split_val = Vec::new(); @@ -70,11 +55,6 @@ pub fn colon_split>(val: &[T]) -> Vec { split_val } -/// Convert an EnvVar to an FFI env_var_t. -fn env_var_to_ffi(var: EnvVar) -> cxx::UniquePtr { - ffi::env_var_t::new_ffi(Box::into_raw(Box::from(var)).cast()).within_unique_ptr() -} - /// Return true if a variable should become a path variable by default. See #436. fn variable_should_auto_pathvar(name: &wstr) -> bool { name.ends_with("PATH") @@ -392,10 +372,7 @@ fn try_get_computed(&self, key: &wstr) -> Option { let vals = get_history_var_text(history_session_id); return Some(EnvVar::new_from_name_vec("history"L, vals)); } else if key == "fish_killring"L { - Some(EnvVar::new_from_name_vec( - "fish_killring"L, - get_kill_ring_entries(), - )) + Some(EnvVar::new_from_name_vec("fish_killring"L, kill_entries())) } else if key == "pipestatus"L { let js = &self.perproc_data.statuses; let mut result = Vec::new(); @@ -473,12 +450,7 @@ fn try_get_global(&self, key: &wstr) -> Option { } fn try_get_universal(&self, key: &wstr) -> Option { - return uvars() - .as_ref() - .expect("Should have non-null uvars in this function") - .get_ffi(&key.to_ffi()) - .as_ref() - .map(|v| v.from_ffi()); + return uvars().get(key); } pub fn getf(&self, key: &wstr, mode: EnvMode) -> Option { @@ -547,11 +519,7 @@ pub fn get_names(&self, flags: EnvMode) -> Vec { } if query.universal { - let uni_list = uvars() - .as_ref() - .expect("Should have non-null uvars in this function") - .get_names_ffi(query.exports, query.unexports) - .from_ffi(); + let uni_list = uvars().get_names(query.exports, query.unexports); names.extend(uni_list); } names.into_iter().collect() @@ -559,13 +527,29 @@ pub fn get_names(&self, flags: EnvMode) -> Vec { /// Slightly optimized implementation. pub fn get_pwd_slash(&self) -> WString { - let mut pwd = self.perproc_data.pwd.clone(); + // Return "/" if PWD is missing. + // See https://github.com/fish-shell/fish-shell/issues/5080 + let mut pwd; + pwd = self.perproc_data.pwd.clone(); if !pwd.ends_with('/') { pwd.push('/'); } pwd } + // todo!("these two are clones from the trait") + fn get_unless_empty(&self, name: &wstr) -> Option { + self.getf_unless_empty(name, EnvMode::default()) + } + + fn getf_unless_empty(&self, name: &wstr, mode: EnvMode) -> Option { + let var = self.getf(name, mode)?; + if !var.is_empty() { + return Some(var); + } + None + } + /// Return a copy of self, with copied locals but shared globals. pub fn snapshot(&self) -> EnvMutex { EnvMutex::new(EnvScopedImpl { @@ -587,7 +571,7 @@ fn enumerate_generations(&self, mut func: F) { // Our uvars generation count doesn't come from next_export_generation(), so always supply // it even if it's 0. - func(uvars().as_ref().unwrap().get_export_generation()); + func(uvars().get_export_generation()); if self.globals.borrow().exports() { func(self.globals.borrow().export_gen); } @@ -648,19 +632,9 @@ fn create_export_array(&self) -> Arc { Self::get_exported(&self.globals, &mut vals); Self::get_exported(&self.locals, &mut vals); - let uni = uvars() - .as_ref() - .unwrap() - .get_names_ffi(true, false) - .from_ffi(); + let uni = uvars().get_names(true, false); for key in uni { - let var = uvars() - .as_ref() - .unwrap() - .get_ffi(&key.to_ffi()) - .as_ref() - .map(|v| v.from_ffi()) - .expect("Variable should be present in uvars"); + let var = uvars().get(&key).unwrap(); // Only insert if not already present, as uvars have lowest precedence. // TODO: a longstanding bug is that an unexported local variable will not mask an exported uvar. vals.entry(key).or_insert(var); @@ -692,7 +666,6 @@ pub fn export_array(&mut self) -> Arc { // Have to pull this into a local to satisfy the borrow checker. let mut generations = std::mem::take(&mut self.export_array_generations); - generations.clear(); self.enumerate_generations(|gen| generations.push(gen)); self.export_array_generations = generations; } @@ -789,7 +762,6 @@ pub fn set(&mut self, key: &wstr, mode: EnvMode, mut val: Vec) -> ModRe result.uvar_modified = true; } else if query.global || (query.universal && UVAR_SCOPE_IS_GLOBAL.load()) { Self::set_in_node(&mut self.base.globals, key, val, flags); - result.global_modified = true; } else if query.local { assert!( !self.base.locals.ptr_eq(&self.base.globals), @@ -824,13 +796,7 @@ pub fn set(&mut self, key: &wstr, mode: EnvMode, mut val: Vec) -> ModRe // Existing global variable. Self::set_in_node(&mut node, key, val, flags); result.global_modified = true; - } else if uvars() - .as_ref() - .unwrap() - .get_ffi(&key.to_ffi()) - .as_ref() - .is_some() - { + } else if !UVAR_SCOPE_IS_GLOBAL.load() && uvars().get(key).is_some() { // Existing universal variable. self.set_universal(key, val, query); result.uvar_modified = true; @@ -864,7 +830,7 @@ fn remove_from_chain(node: &mut EnvNodeRef, key: &wstr) -> EnvStackSetResult { if query.has_scope { // The user requested erasing from a particular scope. if query.universal { - if uvars().as_mut().unwrap().remove(&key.to_ffi()) { + if uvars().remove(key) { result.status = EnvStackSetResult::ENV_OK; } else { result.status = EnvStackSetResult::ENV_NOT_FOUND; @@ -892,11 +858,7 @@ fn remove_from_chain(node: &mut EnvNodeRef, key: &wstr) -> EnvStackSetResult { // pass } else if Self::remove_from_chain(&mut self.base.globals, key) { result.global_modified = true; - } else if uvars() - .as_mut() - .expect("Should have non-null uvars in this function") - .remove(&key.to_ffi()) - { + } else if uvars().remove(key) { result.uvar_modified = true; } else { result.status = EnvStackSetResult::ENV_NOT_FOUND; @@ -1038,10 +1000,7 @@ fn try_set_electric( /// Set a universal variable, inheriting as applicable from the given old variable. fn set_universal(&mut self, key: &wstr, mut val: Vec, query: Query) { let mut locked_uvars = uvars(); - let uv = locked_uvars - .as_mut() - .expect("Should have non-null uvars in this function"); - let oldvar = uv.get_ffi(&key.to_ffi()).as_ref().map(|v| v.from_ffi()); + let oldvar = locked_uvars.get(key); let oldvar = oldvar.as_ref(); // Resolve whether or not to export. @@ -1074,7 +1033,7 @@ fn set_universal(&mut self, key: &wstr, mut val: Vec, query: Query) { varflags.set(EnvVarFlags::PATHVAR, pathvar); let new_var = EnvVar::new_vec(val, varflags); - uv.set(&key.to_ffi(), &env_var_to_ffi(new_var)); + locked_uvars.set(key, new_var); } /// Set a variable in a given node \p node. @@ -1202,6 +1161,7 @@ pub fn lock(&self) -> EnvMutexGuard { // Safety: we use a global lock. unsafe impl Sync for EnvMutex {} +unsafe impl Send for EnvMutex {} #[test] fn test_colon_split() { diff --git a/fish-rust/src/env/mod.rs b/fish-rust/src/env/mod.rs index 09297271d..c6610a8d7 100644 --- a/fish-rust/src/env/mod.rs +++ b/fish-rust/src/env/mod.rs @@ -4,7 +4,7 @@ pub mod var; use crate::common::ToCString; -pub use env_ffi::{EnvStackRefFFI, EnvStackSetResult}; +pub use env_ffi::{EnvDynFFI, EnvStackRefFFI, EnvStackSetResult}; pub use environment::*; use std::sync::atomic::{AtomicBool, AtomicUsize}; pub use var::*; diff --git a/fish-rust/src/env/var.rs b/fish-rust/src/env/var.rs index a2dfda4c7..a045216a4 100644 --- a/fish-rust/src/env/var.rs +++ b/fish-rust/src/env/var.rs @@ -52,30 +52,6 @@ fn from(val: EnvMode) -> Self { } } -/// Return values for `env_stack_t::set()`. -pub mod status { - pub const ENV_OK: i32 = 0; - pub const ENV_PERM: i32 = 1; - pub const ENV_SCOPE: i32 = 2; - pub const ENV_INVALID: i32 = 3; - pub const ENV_NOT_FOUND: i32 = 4; -} - -/// Return values for `EnvStack::set()`. -pub enum EnvStackSetResult { - ENV_OK, - ENV_PERM, - ENV_SCOPE, - ENV_INVALID, - ENV_NOT_FOUND, -} - -impl Default for EnvStackSetResult { - fn default() -> Self { - EnvStackSetResult::ENV_OK - } -} - /// A struct of configuration directories, determined in main() that fish will optionally pass to /// env_init. #[derive(Default)] diff --git a/fish-rust/src/env_dispatch.rs b/fish-rust/src/env_dispatch.rs index b58196495..5572da44c 100644 --- a/fish-rust/src/env_dispatch.rs +++ b/fish-rust/src/env_dispatch.rs @@ -1,12 +1,15 @@ use crate::common::ToCString; +use crate::complete::complete_invalidate_path; use crate::curses::{self, Term}; use crate::env::{setenv_lock, unsetenv_lock, EnvMode, EnvStack, Environment}; use crate::env::{CURSES_INITIALIZED, READ_BYTE_LIMIT, TERM_HAS_XN}; -use crate::ffi::is_interactive_session; use crate::flog::FLOG; use crate::function; +use crate::input_common::{update_wait_on_escape_ms, update_wait_on_sequence_key_ms}; use crate::output::ColorSupport; +use crate::proc::is_interactive_session; use crate::wchar::prelude::*; +use crate::wchar_ffi::WCharToFFI; use crate::wutil::fish_wcstoi; use std::borrow::Cow; use std::collections::HashMap; @@ -103,18 +106,6 @@ struct VarDispatchTable { table: HashMap<&'static wstr, EnvCallback>, } -// TODO: Delete this after input_common is ported (and pass the input_function function directly). -fn update_wait_on_escape_ms(vars: &EnvStack) { - let fish_escape_delay_ms = vars.get_unless_empty(L!("fish_escape_delay_ms")); - let var = crate::env::environment::env_var_to_ffi(fish_escape_delay_ms); - crate::ffi::update_wait_on_escape_ms_ffi(var); -} -fn update_wait_on_sequence_key_ms(vars: &EnvStack) { - let fish_sequence_key_delay_ms = vars.get_unless_empty(L!("fish_sequence_key_delay_ms")); - let var = crate::env::environment::env_var_to_ffi(fish_sequence_key_delay_ms); - crate::ffi::update_wait_on_sequence_key_ms_ffi(var); -} - impl VarDispatchTable { /// Add a callback for the variable `name`. We must not already be observing this variable. pub fn add(&mut self, name: &'static wstr, callback: NamedEnvCallback) { @@ -249,9 +240,8 @@ fn handle_term_size_change(vars: &EnvStack) { } fn handle_fish_history_change(vars: &EnvStack) { - let fish_history = vars.get(L!("fish_history")); - let var = crate::env::env_var_to_ffi(fish_history); - crate::ffi::reader_change_history(&crate::ffi::history_session_id(var)); + let session_id = crate::history::history_session_id(vars); + crate::ffi::reader_change_history(&session_id.to_ffi()); } fn handle_fish_cursor_selection_mode_change(vars: &EnvStack) { @@ -286,7 +276,7 @@ fn handle_function_path_change(_: &EnvStack) { } fn handle_complete_path_change(_: &EnvStack) { - crate::ffi::complete_invalidate_path(); + complete_invalidate_path() } fn handle_tz_change(var_name: &wstr, vars: &EnvStack) { diff --git a/fish-rust/src/env_universal_common.rs b/fish-rust/src/env_universal_common.rs new file mode 100644 index 000000000..5e9d4ed48 --- /dev/null +++ b/fish-rust/src/env_universal_common.rs @@ -0,0 +1,1142 @@ +use crate::common::{ + read_loop, str2wcstring, timef, unescape_string, valid_var_name, wcs2zstring, write_loop, + UnescapeFlags, UnescapeStringStyle, +}; +use crate::compat::{C_O_EXLOCK, UVAR_FILE_SET_MTIME_HACK}; +use crate::env::{EnvVar, EnvVarFlags, VarTable}; +use crate::fallback::fish_mkstemp_cloexec; +use crate::fds::AutoCloseFd; +use crate::fds::{open_cloexec, wopen_cloexec}; +use crate::flog::{FLOG, FLOGF}; +use crate::path::path_get_config; +use crate::path::{path_get_config_remoteness, DirRemoteness}; +use crate::wchar::prelude::*; +use crate::wchar::{wstr, WString}; +use crate::wcstringutil::{join_strings, split_string, string_suffixes_string, LineIterator}; +use crate::wutil::{ + file_id_for_fd, file_id_for_path, file_id_for_path_narrow, wdirname, wrealpath, wrename, wstat, + wunlink, FileId, INVALID_FILE_ID, +}; +use errno::{errno, Errno}; +use libc::{ + CLOCK_REALTIME, EINTR, ENOTSUP, EOPNOTSUPP, LOCK_EX, O_CREAT, O_RDONLY, O_RDWR, UTIME_OMIT, +}; +use std::collections::hash_map::Entry; +use std::collections::HashSet; +use std::ffi::CString; +use std::mem::MaybeUninit; +use std::os::fd::RawFd; +use std::os::unix::prelude::MetadataExt; + +/// Callback data, reflecting a change in universal variables. +pub struct CallbackData { + // The name of the variable. + pub key: WString, + + // The value of the variable, or none if it is erased. + pub val: Option, +} +impl CallbackData { + /// Construct from a key and maybe a value. + pub fn new(key: WString, val: Option) -> Self { + Self { key, val } + } + /// \return whether this callback represents an erased variable. + pub fn is_erase(&self) -> bool { + self.val.is_none() + } +} + +pub type CallbackDataList = Vec; + +// List of fish universal variable formats. +// This is exposed for testing. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum UvarFormat { + fish_2_x, + fish_3_0, + future, +} + +/// Class representing universal variables. +pub struct EnvUniversal { + // Path that we save to. This is set in initialize(). If empty, initialize has not been called. + vars_path: WString, + narrow_vars_path: CString, + + // The table of variables. + vars: VarTable, + + // Keys that have been modified, and need to be written. A value here that is not present in + // vars indicates a deleted value. + modified: HashSet, + + // A generation count which is incremented every time an exported variable is modified. + export_generation: u64, + + // Whether it's OK to save. This may be set to false if we discover that a future version of + // fish wrote the uvars contents. + ok_to_save: bool, + + // If true, attempt to flock the uvars file. + // This latches to false if the file is found to be remote, where flock may hang. + do_flock: bool, + + // File id from which we last read. + last_read_file: FileId, +} + +impl EnvUniversal { + // Construct an empty universal variables. + pub fn new() -> Self { + Self { + vars_path: Default::default(), + narrow_vars_path: Default::default(), + vars: Default::default(), + modified: Default::default(), + export_generation: 1, + ok_to_save: true, + do_flock: true, + last_read_file: INVALID_FILE_ID, + } + } + // Get the value of the variable with the specified name. + pub fn get(&self, name: &wstr) -> Option { + self.vars.get(name).cloned() + } + // \return flags from the variable with the given name. + pub fn get_flags(&self, name: &wstr) -> Option { + self.vars.get(name).map(|var| var.get_flags()) + } + // Sets a variable. + pub fn set(&mut self, key: &wstr, var: EnvVar) { + let exports = var.exports(); + match self.vars.entry(key.to_owned()) { + Entry::Occupied(mut entry) => { + if entry.get() == &var { + return; + } + entry.insert(var); + } + Entry::Vacant(entry) => { + entry.insert(var); + } + }; + self.modified.insert(key.to_owned()); + if exports { + self.export_generation += 1; + } + } + // Removes a variable. Returns true if it was found, false if not. + pub fn remove(&mut self, key: &wstr) -> bool { + if let Some(var) = self.vars.remove(key) { + if var.exports() { + self.export_generation += 1; + } + self.modified.insert(key.to_owned()); + return true; + } + false + } + + // Gets variable names. + pub fn get_names(&self, show_exported: bool, show_unexported: bool) -> Vec { + let mut result = vec![]; + for (key, var) in &self.vars { + if (var.exports() && show_exported) || (!var.exports() && show_unexported) { + result.push(key.clone()); + } + } + result + } + + /// Get a view on the universal variable table. + pub fn get_table(&self) -> &VarTable { + &self.vars + } + + /// Initialize this uvars for the default path. + /// This should be called at most once on any given instance. + pub fn initialize(&mut self, callbacks: &mut CallbackDataList) { + // Set do_flock to false immediately if the default variable path is on a remote filesystem. + // See #7968. + if path_get_config_remoteness() == DirRemoteness::remote { + self.do_flock = false; + } + self.initialize_at_path(callbacks, default_vars_path()); + } + + /// Initialize a this uvars for a given path. + /// This is exposed for testing only. + pub fn initialize_at_path(&mut self, callbacks: &mut CallbackDataList, path: WString) { + if path.is_empty() { + return; + } + assert!(!self.initialized(), "Already initialized"); + self.vars_path = path; + + if self.load_from_path(callbacks) { + // Successfully loaded from our normal path. + } + } + + /// Reads and writes variables at the correct path. Returns true if modified variables were + /// written. + pub fn sync(&mut self, callbacks: &mut CallbackDataList) -> bool { + if !self.initialized() { + return false; + } + + FLOG!(uvar_file, "universal log sync"); + // Our saving strategy: + // + // 1. Open the file, producing an fd. + // 2. Lock the file (may be combined with step 1 on systems with O_EXLOCK) + // 3. After taking the lock, check if the file at the given path is different from what we + // opened. If so, start over. + // 4. Read from the file. This can be elided if its dev/inode is unchanged since the last read + // 5. Open an adjacent temporary file + // 6. Write our changes to an adjacent file + // 7. Move the adjacent file into place via rename. This is assumed to be atomic. + // 8. Release the lock and close the file + // + // Consider what happens if Process 1 and 2 both do this simultaneously. Can there be data loss? + // Process 1 opens the file and then attempts to take the lock. Now, either process 1 will see + // the original file, or process 2's new file. If it sees the new file, we're OK: it's going to + // read from the new file, and so there's no data loss. If it sees the old file, then process 2 + // must have locked it (if process 1 locks it, switch their roles). The lock will block until + // process 2 reaches step 7; at that point process 1 will reach step 2, notice that the file has + // changed, and then start over. + // + // It's possible that the underlying filesystem does not support locks (lockless NFS). In this + // case, we risk data loss if two shells try to write their universal variables simultaneously. + // In practice this is unlikely, since uvars are usually written interactively. + // + // Prior versions of fish used a hard link scheme to support file locking on lockless NFS. The + // risk here is that if the process crashes or is killed while holding the lock, future + // instances of fish will not be able to obtain it. This seems to be a greater risk than that of + // data loss on lockless NFS. Users who put their home directory on lockless NFS are playing + // with fire anyways. + // If we have no changes, just load. + if self.modified.is_empty() { + self.load_from_path_narrow(callbacks); + FLOG!(uvar_file, "universal log no modifications"); + return false; + } + + let directory = wdirname(&self.vars_path).to_owned(); + + FLOG!(uvar_file, "universal log performing full sync"); + + // Open the file. + let vars_fd = self.open_and_acquire_lock(); + if !vars_fd.is_valid() { + FLOG!(uvar_file, "universal log open_and_acquire_lock() failed"); + return false; + } + + // Read from it. + assert!(vars_fd.is_valid()); + self.load_from_fd(vars_fd.fd(), callbacks); + + if self.ok_to_save { + self.save(&directory) + } else { + true + } + } + + /// Populate a variable table \p out_vars from a \p s string. + /// This is exposed for testing only. + /// \return the format of the file that we read. + pub fn populate_variables(s: &[u8], out_vars: &mut VarTable) -> UvarFormat { + // Decide on the format. + let format = Self::format_for_contents(s); + + let iter = LineIterator::new(s); + let mut wide_line = WString::new(); + let mut storage = WString::new(); + for line in iter { + // Skip empties and constants. + if line.is_empty() || line[0] == b'#' { + continue; + } + + // Convert to UTF8. + wide_line.clear(); + let Ok(line) = std::str::from_utf8(line) else { + continue; + }; + wide_line = WString::from_str(line); + + match format { + UvarFormat::fish_2_x => { + Self::parse_message_2x_internal(&wide_line, out_vars, &mut storage); + } + UvarFormat::fish_3_0 => { + Self::parse_message_30_internal(&wide_line, out_vars, &mut storage); + } + // For future formats, just try with the most recent one. + UvarFormat::future => { + Self::parse_message_30_internal(&wide_line, out_vars, &mut storage); + } + } + } + format + } + + /// Guess a file format. Exposed for testing only. + /// \return the format corresponding to file contents \p s. + pub fn format_for_contents(s: &[u8]) -> UvarFormat { + // Walk over leading comments, looking for one like '# version' + let iter = LineIterator::new(s); + for line in iter { + if line.is_empty() { + continue; + } + if line[0] != b'#' { + // Exhausted leading comments. + break; + } + // Note scanf %s is max characters to write; add 1 for null terminator. + let mut versionbuf: MaybeUninit<[u8; 64 + 1]> = MaybeUninit::uninit(); + // Safety: test-only + let cstr = CString::new(line).unwrap(); + if unsafe { + libc::sscanf( + cstr.as_ptr(), + b"# VERSION: %64s\0".as_ptr().cast(), + versionbuf.as_mut_ptr(), + ) + } != 1 + { + continue; + } + + // Try reading the version. + let versionbuf = unsafe { versionbuf.assume_init() }; + return if versionbuf.starts_with(UVARS_VERSION_3_0) + && versionbuf[UVARS_VERSION_3_0.len()] == b'\0' + { + UvarFormat::fish_3_0 + } else { + UvarFormat::future + }; + } + // No version found, assume 2.x + return UvarFormat::fish_2_x; + } + + /// Serialize a variable list. Exposed for testing only. + pub fn serialize_with_vars(vars: &VarTable) -> Vec { + let mut contents = vec![]; + contents.extend_from_slice(SAVE_MSG); + contents.extend_from_slice(b"# VERSION: "); + contents.extend_from_slice(UVARS_VERSION_3_0); + contents.push(b'\n'); + + // Preserve legacy behavior by sorting the values first + let mut cloned: Vec<(&wstr, &EnvVar)> = + vars.iter().map(|(key, var)| (key.as_ref(), var)).collect(); + cloned.sort_by(|(lkey, _), (rkey, _)| lkey.cmp(rkey)); + + for (key, var) in cloned { + // Append the entry. Note that append_file_entry may fail, but that only affects one + // variable; soldier on. + append_file_entry( + var.get_flags(), + key, + &encode_serialized(var.as_list()), + &mut contents, + ); + } + contents + } + + /// Exposed for testing only. + pub fn is_ok_to_save(&self) -> bool { + self.ok_to_save + } + + /// Access the export generation. + pub fn get_export_generation(&self) -> u64 { + self.export_generation + } + + /// \return whether we are initialized. + fn initialized(&self) -> bool { + !self.vars_path.is_empty() + } + + fn load_from_path(&mut self, callbacks: &mut CallbackDataList) -> bool { + self.narrow_vars_path = wcs2zstring(&self.vars_path); + self.load_from_path_narrow(callbacks) + } + + fn load_from_path_narrow(&mut self, callbacks: &mut CallbackDataList) -> bool { + // Check to see if the file is unchanged. We do this again in load_from_fd, but this avoids + // opening the file unnecessarily. + if self.last_read_file != INVALID_FILE_ID + && file_id_for_path_narrow(&self.narrow_vars_path) == self.last_read_file + { + FLOG!(uvar_file, "universal log sync elided based on fast stat()"); + return true; + } + + let mut result = false; + let fd = AutoCloseFd::new(open_cloexec(&self.narrow_vars_path, O_RDONLY, 0)); + if fd.is_valid() { + FLOG!(uvar_file, "universal log reading from file"); + self.load_from_fd(fd.fd(), callbacks); + result = true; + } + result + } + + fn load_from_fd(&mut self, fd: RawFd, callbacks: &mut CallbackDataList) { + assert!(fd >= 0); + // Get the dev / inode. + let current_file = file_id_for_fd(fd); + if current_file == self.last_read_file { + FLOG!(uvar_file, "universal log sync elided based on fstat()"); + } else { + // Read a variables table from the file. + let mut new_vars = VarTable::new(); + let format = Self::read_message_internal(fd, &mut new_vars); + + // Hacky: if the read format is in the future, avoid overwriting the file: never try to + // save. + if format == UvarFormat::future { + self.ok_to_save = false; + } + + // Announce changes and update our exports generation. + self.generate_callbacks_and_update_exports(&new_vars, callbacks); + + // Acquire the new variables. + self.acquire_variables(new_vars); + self.last_read_file = current_file; + } + } + + // Functions concerned with saving. + fn open_and_acquire_lock(&mut self) -> AutoCloseFd { + // Attempt to open the file for reading at the given path, atomically acquiring a lock. On BSD, + // we can use O_EXLOCK. On Linux, we open the file, take a lock, and then compare fstat() to + // stat(); if they match, it means that the file was not replaced before we acquired the lock. + // + // We pass O_RDONLY with O_CREAT; this creates a potentially empty file. We do this so that we + // have something to lock on. + let mut locked_by_open = false; + let mut flags = O_RDWR | O_CREAT; + + #[allow(non_snake_case)] + let O_EXLOCK = unsafe { C_O_EXLOCK() }; + if O_EXLOCK != 0 && self.do_flock { + flags |= O_EXLOCK; + locked_by_open = true; + } + + let mut fd = AutoCloseFd::empty(); + while !fd.is_valid() { + fd = AutoCloseFd::new(wopen_cloexec(&self.vars_path, flags, 0o644)); + + if !fd.is_valid() { + let err = errno(); + if err.0 == EINTR { + continue; // signaled; try again + } + + if O_EXLOCK != 0 { + if (flags & O_EXLOCK) != 0 && [ENOTSUP, EOPNOTSUPP].contains(&err.0) { + // Filesystem probably does not support locking. Give up on locking. + // Note that on Linux the two errno symbols have the same value but on BSD they're + // different. + flags &= !O_EXLOCK; + self.do_flock = false; + locked_by_open = false; + continue; + } + } + FLOG!( + error, + wgettext_fmt!( + "Unable to open universal variable file '%s': %s", + &self.vars_path, + err.to_string() + ) + ); + break; + } + + assert!(fd.is_valid(), "Should have a valid fd here"); + + // Lock if we want to lock and open() didn't do it for us. + // If flock fails, give up on locking forever. + if self.do_flock && !locked_by_open { + if !flock_uvar_file(fd.fd()) { + self.do_flock = false; + } + } + + // Hopefully we got the lock. However, it's possible the file changed out from under us + // while we were waiting for the lock. Make sure that didn't happen. + if file_id_for_fd(fd.fd()) != file_id_for_path(&self.vars_path) { + // Oops, it changed! Try again. + fd.close(); + } + } + + fd + } + + fn open_temporary_file(&mut self, directory: &wstr, out_path: &mut WString) -> AutoCloseFd { + // Create and open a temporary file for writing within the given directory. Try to create a + // temporary file, up to 10 times. We don't use mkstemps because we want to open it CLO_EXEC. + // This should almost always succeed on the first try. + assert!(!string_suffixes_string(L!("/"), directory)); + + let mut saved_errno = Errno(0); + let tmp_name_template = directory.to_owned() + L!("/fishd.tmp.XXXXXX"); + let mut result = AutoCloseFd::empty(); + let mut narrow_str = CString::default(); + for _attempt in 0..10 { + if result.is_valid() { + break; + } + let (fd, tmp_name) = fish_mkstemp_cloexec(wcs2zstring(&tmp_name_template)); + result.reset(fd); + narrow_str = tmp_name; + saved_errno = errno(); + } + *out_path = str2wcstring(narrow_str.as_bytes()); + + if !result.is_valid() { + FLOG!( + error, + wgettext_fmt!( + "Unable to open temporary file '%ls': %s", + out_path, + saved_errno.to_string() + ) + ); + } + result + } + /// Writes our state to the fd. path is provided only for error reporting. + fn write_to_fd(&mut self, fd: RawFd, path: &wstr) -> bool { + assert!(fd >= 0); + let mut success = true; + let contents = Self::serialize_with_vars(&self.vars); + if let Err(err) = write_loop(&fd, &contents) { + let error = Errno(err.raw_os_error().unwrap()); + FLOG!( + error, + wgettext_fmt!( + "Unable to write to universal variables file '%ls': %s", + path, + error.to_string() + ), + ); + success = false; + } + + // Since we just wrote out this file, it matches our internal state; pretend we read from it. + self.last_read_file = file_id_for_fd(fd); + + // We don't close the file. + success + } + + fn move_new_vars_file_into_place(&mut self, src: &wstr, dst: &wstr) -> bool { + let ret = wrename(src, dst); + if ret != 0 { + let error = errno(); + FLOG!( + error, + wgettext_fmt!( + "Unable to rename file from '%ls' to '%ls': %s", + src, + dst, + error.to_string() + ) + ); + } + ret == 0 + } + + // Given a variable table, generate callbacks representing the difference between our vars and + // the new vars. Also update our exports generation count as necessary. + fn generate_callbacks_and_update_exports( + &mut self, + new_vars: &VarTable, + callbacks: &mut CallbackDataList, + ) { + // Construct callbacks for erased values. + for (key, value) in &self.vars { + // Skip modified values. + if self.modified.contains(key) { + continue; + } + + // If the value is not present in new_vars, it has been erased. + if !new_vars.contains_key(key) { + callbacks.push(CallbackData::new(key.clone(), None)); + if value.exports() { + self.export_generation += 1; + } + } + } + + // Construct callbacks for newly inserted or changed values. + for (key, new_entry) in new_vars { + // Skip modified values. + if self.modified.contains(key) { + continue; + } + + let existing = self.vars.get(key); + + // See if the value has changed. + let old_exports = existing.map_or(false, |v| v.exports()); + let export_changed = old_exports != new_entry.exports(); + let value_changed = existing.map_or(false, |v| v != new_entry); + if export_changed || value_changed { + self.export_generation += 1; + } + if existing.is_none() || export_changed || value_changed { + // Value is set for the first time, or has changed. + callbacks.push(CallbackData::new(key.clone(), Some(new_entry.clone()))); + } + } + } + + // Given a variable table, copy unmodified values into self. + fn acquire_variables(&mut self, mut vars_to_acquire: VarTable) { + // Copy modified values from existing vars to vars_to_acquire. + for key in &self.modified { + match self.vars.get(key) { + None => { + /* The value has been deleted. */ + vars_to_acquire.remove(key); + } + Some(src) => { + // The value has been modified. Copy it over. Note we can destructively modify the + // source entry in vars since we are about to get rid of this->vars entirely. + vars_to_acquire.insert(key.clone(), src.clone()); + } + } + } + + // We have constructed all the callbacks and updated vars_to_acquire. Acquire it! + self.vars = vars_to_acquire; + } + + fn populate_1_variable( + input: &wstr, + flags: EnvVarFlags, + vars: &mut VarTable, + storage: &mut WString, + ) -> bool { + let s = skip_spaces(input); + let Some(colon) = s.chars().position(|c| c == ':') else { + return false; + }; + + // Parse out the value into storage, and decode it into a variable. + storage.clear(); + let Some(unescaped) = unescape_string( + &s[colon + 1..], + UnescapeStringStyle::Script(UnescapeFlags::default()), + ) else { + return false; + }; + *storage = unescaped; + let var = EnvVar::new_vec(decode_serialized(&*storage), flags); + + // Parse out the key and write into the map. + *storage = s[..colon].to_owned(); + let key = &*storage; + (*vars).insert(key.clone(), var); + true + } + /// Parse message msg per fish 3.0 format. + fn parse_message_30_internal(msg: &wstr, vars: &mut VarTable, storage: &mut WString) { + use fish3_uvars as f3; + if msg.starts_with(L!("#")) { + return; + } + + let mut cursor = msg; + if !r#match(&mut cursor, f3::SETUVAR) { + FLOGF!(warning, PARSE_ERR, msg); + return; + } + // Parse out flags. + let mut flags = EnvVarFlags::default(); + loop { + cursor = skip_spaces(cursor); + if cursor.char_at(0) != '-' { + break; + } + if r#match(&mut cursor, f3::EXPORT) { + flags |= EnvVarFlags::EXPORT; + } else if r#match(&mut cursor, f3::PATH) { + flags |= EnvVarFlags::PATHVAR; + } else { + // Skip this unknown flag, for future proofing. + while !cursor.is_empty() && !matches!(cursor.char_at(0), ' ' | '\t') { + cursor = &cursor[1..]; + } + } + } + + // Populate the variable with these flags. + if !Self::populate_1_variable(cursor, flags, vars, storage) { + FLOGF!(warning, PARSE_ERR, msg); + } + } + + /// Parse message msg per fish 2.x format. + fn parse_message_2x_internal(msg: &wstr, vars: &mut VarTable, storage: &mut WString) { + use fish2x_uvars as f2x; + let mut cursor = msg; + + if cursor.char_at(0) == '#' { + return; + } + let mut flags = EnvVarFlags::default(); + if r#match(&mut cursor, f2x::SET_EXPORT) { + flags |= EnvVarFlags::EXPORT; + } else if r#match(&mut cursor, f2x::SET) { + } else { + FLOGF!(warning, PARSE_ERR, msg); + return; + } + + if !Self::populate_1_variable(cursor, flags, vars, storage) { + FLOGF!(warning, PARSE_ERR, msg); + } + } + + fn read_message_internal(fd: RawFd, vars: &mut VarTable) -> UvarFormat { + // Read everything from the fd. Put a sane limit on it. + let mut contents = vec![]; + let mut buffer = [0_u8; 4096]; + while contents.len() < MAX_READ_SIZE { + match read_loop(&fd, &mut buffer) { + Ok(0) | Err(_) => break, + Ok(amt) => contents.extend_from_slice(&buffer[..amt]), + } + } + + // Handle overlong files. + if contents.len() > MAX_READ_SIZE { + contents.truncate(MAX_READ_SIZE); + // Back up to a newline. + let newline = contents.iter().rposition(|c| *c == b'\n').unwrap_or(0); + contents.truncate(newline); + } + + Self::populate_variables(&contents, vars) + } + + // Write our file contents. + // \return true on success, false on failure. + fn save(&mut self, directory: &wstr) -> bool { + assert!(self.ok_to_save, "It's not OK to save"); + + let mut private_file_path = WString::new(); + + // Open adjacent temporary file. + let private_fd = self.open_temporary_file(directory, &mut private_file_path); + let mut success = private_fd.is_valid(); + + if !success { + FLOG!(uvar_file, "universal log open_temporary_file() failed"); + } + + // Write to it. + if success { + assert!(private_fd.is_valid()); + success = self.write_to_fd(private_fd.fd(), &private_file_path); + if !success { + FLOG!(uvar_file, "universal log write_to_fd() failed"); + } + } + + if success { + let real_path = wrealpath(&self.vars_path).unwrap_or_else(|| self.vars_path.clone()); + + // Ensure we maintain ownership and permissions (#2176). + // let mut sbuf : libc::stat = MaybeUninit::uninit(); + if let Ok(md) = wstat(&real_path) { + if unsafe { libc::fchown(private_fd.fd(), md.uid(), md.gid()) } == -1 { + FLOG!(uvar_file, "universal log fchown() failed"); + } + #[allow(clippy::useless_conversion)] + let mode: libc::mode_t = md.mode().try_into().unwrap(); + if unsafe { libc::fchmod(private_fd.fd(), mode) } == -1 { + FLOG!(uvar_file, "universal log fchmod() failed"); + } + } + + // Linux by default stores the mtime with low precision, low enough that updates that occur + // in quick succession may result in the same mtime (even the nanoseconds field). So + // manually set the mtime of the new file to a high-precision clock. Note that this is only + // necessary because Linux aggressively reuses inodes, causing the ABA problem; on other + // platforms we tend to notice the file has changed due to a different inode (or file size!) + // + // The current time within the Linux kernel is cached, and generally only updated on a timer + // interrupt. So if the timer interrupt is running at 10 milliseconds, the cached time will + // only be updated once every 10 milliseconds. + // + // It's probably worth finding a simpler solution to this. The tests ran into this, but it's + // unlikely to affect users. + if unsafe { UVAR_FILE_SET_MTIME_HACK() } { + let mut times: [libc::timespec; 2] = unsafe { std::mem::zeroed() }; + times[0].tv_nsec = UTIME_OMIT; // don't change ctime + if unsafe { libc::clock_gettime(CLOCK_REALTIME, &mut times[1]) } != 0 { + unsafe { + libc::futimens(private_fd.fd(), ×[0]); + } + } + } + + // Apply new file. + success = self.move_new_vars_file_into_place(&private_file_path, &real_path); + if !success { + FLOG!( + uvar_file, + "universal log move_new_vars_file_into_place() failed" + ); + } + } + + if success { + // Since we moved the new file into place, clear the path so we don't try to unlink it. + private_file_path.clear(); + } + + // Clean up. + if !private_file_path.is_empty() { + wunlink(&private_file_path); + } + if success { + // All of our modified variables have now been written out. + self.modified.clear(); + } + success + } +} + +/// \return the default variable path, or an empty string on failure. +fn default_vars_path() -> WString { + if let Some(mut path) = default_vars_path_directory() { + path.push_str("/fish_variables"); + return path; + } + WString::new() +} + +pub enum NotifierStrategy { + // Poll on shared memory. + strategy_shmem_polling, + + // Mac-specific notify(3) implementation. + strategy_notifyd, + + // Strategy that uses a named pipe. Somewhat complex, but portable and doesn't require + // polling most of the time. + strategy_named_pipe, +} + +/// The "universal notifier" is an object responsible for broadcasting and receiving universal +/// variable change notifications. These notifications do not contain the change, but merely +/// indicate that the uvar file has changed. It is up to the uvar subsystem to re-read the file. +/// +/// We support a few notification strategies. Not all strategies are supported on all platforms. +/// +/// Notifiers may request polling, and/or provide a file descriptor to be watched for readability in +/// select(). +/// +/// To request polling, the notifier overrides usec_delay_between_polls() to return a positive +/// value. That value will be used as the timeout in select(). When select returns, the loop invokes +/// poll(). poll() should return true to indicate that the file may have changed. +/// +/// To provide a file descriptor, the notifier overrides notification_fd() to return a non-negative +/// fd. This will be added to the "read" file descriptor list in select(). If the fd is readable, +/// notification_fd_became_readable() will be called; that function should be overridden to return +/// true if the file may have changed. +pub trait UniversalNotifier { + // Does a fast poll(). Returns true if changed. + fn poll(&self) -> bool; + + // Triggers a notification. + fn post_notification(&self); + + // Recommended delay between polls. A value of 0 means no polling required (so no timeout). + fn usec_delay_between_polls(&self) -> u64; + + // Returns the fd from which to watch for events, or -1 if none. + fn notification_fd(&self) -> RawFd; + + // The notification_fd is readable; drain it. Returns true if a notification is considered to + // have been posted. + fn notification_fd_became_readable(&self, fd: RawFd) -> bool; +} + +fn resolve_default_strategy() -> NotifierStrategy { + todo!("universal notifier"); +} + +// Default instance. Other instances are possible for testing. +pub fn default_notifier() -> &'static dyn UniversalNotifier { + todo!("universal notifier"); +} + +/// Factory constructor. +fn new_notifier_for_strategy(_strat: NotifierStrategy, _test_path: Option<&wstr>) { + todo!("universal notifier"); +} + +/// Error message. +const PARSE_ERR: &wstr = L!("Unable to parse universal variable message: '%ls'"); + +/// Small note about not editing ~/.fishd manually. Inserted at the top of all .fishd files. +const SAVE_MSG: &[u8] = b"# This file contains fish universal variable definitions.\n"; + +/// Version for fish 3.0 +const UVARS_VERSION_3_0: &[u8] = b"3.0"; + +// Maximum file size we'll read. +const MAX_READ_SIZE: usize = 16 * 1024 * 1024; + +// Fields used in fish 2.x uvars. + +mod fish2x_uvars { + pub const SET: &[u8] = b"SET"; + pub const SET_EXPORT: &[u8] = b"SET_EXPORT"; +} +// Fields used in fish 3.0 uvars +mod fish3_uvars { + pub const SETUVAR: &[u8] = b"SETUVAR"; + pub const EXPORT: &[u8] = b"--export"; + pub const PATH: &[u8] = b"--path"; +} + +/// The different types of messages found in the fishd file. +enum UvarMessageType { + set, + set_export, +} + +/// \return the default variable path, or an empty string on failure. +fn default_vars_path_directory() -> Option { + path_get_config() +} + +/// Test if the message msg contains the command cmd. +/// On success, updates the cursor to just past the command. +fn r#match(inout_cursor: &mut &wstr, cmd: &[u8]) -> bool { + let cursor = *inout_cursor; + if !cmd + .iter() + .copied() + .map(char::from) + .eq(cursor.chars().take(cmd.len())) + { + return false; + } + let len = cmd.len(); + if cursor.len() != len && !matches!(cursor.char_at(len), ' ' | '\t') { + return false; + } + *inout_cursor = &cursor[len..]; + true +} + +/// The universal variable format has some funny escaping requirements; here we try to be safe. +fn is_universal_safe_to_encode_directly(c: char) -> bool { + if !(32..=128).contains(&u32::from(c)) { + return false; + } + + c.is_alphanumeric() || matches!(c, '/' | '_') +} + +/// Escape specified string. +fn full_escape(input: &wstr) -> WString { + let mut out = WString::new(); + for c in input.chars() { + if is_universal_safe_to_encode_directly(c) { + out.push(c); + } else if c.is_ascii() { + sprintf!(=> &mut out, "\\x%.2x", u32::from(c)); + } else if u32::from(c) < 65536 { + sprintf!(=> &mut out, "\\u%.4x", u32::from(c)); + } else { + sprintf!(=> &mut out, "\\U%.8x", u32::from(c)); + } + } + out +} + +/// Converts input to UTF-8 and appends it to receiver. +fn append_utf8(input: &wstr, receiver: &mut Vec) { + // Notably we convert between wide and narrow strings without decoding our private-use + // characters. + receiver.extend_from_slice(input.to_string().as_bytes()); +} + +/// Creates a file entry like "SET fish_color_cwd:FF0". Appends the result to *result (as UTF8). +/// Returns true on success. storage may be used for temporary storage, to avoid allocations. +fn append_file_entry( + flags: EnvVarFlags, + key_in: &wstr, + val_in: &wstr, + result: &mut Vec, +) -> bool { + use fish3_uvars as f3; + + // Record the length on entry, in case we need to back up. + let mut success = true; + let result_length_on_entry = result.len(); + + // Append SETVAR header. + result.extend_from_slice(f3::SETUVAR); + result.push(b' '); + + // Append flags. + if flags.contains(EnvVarFlags::EXPORT) { + result.extend_from_slice(f3::EXPORT); + result.push(b' '); + } + if flags.contains(EnvVarFlags::PATHVAR) { + result.extend_from_slice(f3::PATH); + result.push(b' '); + } + + // Append variable name like "fish_color_cwd". + if !valid_var_name(key_in) { + FLOGF!(error, "Illegal variable name: '%ls'", key_in); + success = false; + } + if success { + append_utf8(key_in, result); + } + + // Append ":". + if success { + result.push(b':'); + } + + // Append value. + if success { + append_utf8(&full_escape(val_in), result); + } + + // Append newline. + if success { + result.push(b'\n'); + } + + // Don't modify result on failure. It's sufficient to simply resize it since all we ever did was + // append to it. + if !success { + result.truncate(result_length_on_entry); + } + + success +} + +/// Encoding of a null string. +const ENV_NULL: &wstr = L!("\x1d"); + +/// Character used to separate arrays in universal variables file. +/// This is 30, the ASCII record separator. +const UVAR_ARRAY_SEP: char = '\x1e'; + +/// Decode a serialized universal variable value into a list. +fn decode_serialized(val: &wstr) -> Vec { + if val == ENV_NULL { + return vec![]; + } + split_string(val, UVAR_ARRAY_SEP) +} + +/// Decode a a list into a serialized universal variable value. +fn encode_serialized(vals: &[WString]) -> WString { + if vals.is_empty() { + return ENV_NULL.to_owned(); + } + join_strings(vals, UVAR_ARRAY_SEP) +} + +/// Try locking the file. +/// \return true on success, false on error. +fn flock_uvar_file(fd: RawFd) -> bool { + let start_time = timef(); + while unsafe { libc::flock(fd, LOCK_EX) } == -1 { + if errno().0 != EINTR { + return false; // do nothing per issue #2149 + } + } + let duration = timef() - start_time; + if duration > 0.25 { + FLOG!( + warning, + wgettext_fmt!( + "Locking the universal var file took too long (%.3f seconds).", + duration + ) + ); + return false; + } + true +} + +fn skip_spaces(mut s: &wstr) -> &wstr { + while s.starts_with(L!(" ")) || s.starts_with(L!("\t")) { + s = &s[1..]; + } + s +} + +pub struct UniversalNotifierFFI(pub &'static dyn UniversalNotifier); + +#[cxx::bridge] +mod env_universal_common_ffi { + extern "Rust" { + type UniversalNotifierFFI; + #[cxx_name = "default_notifier"] + fn ffi_default_notifier() -> Box; + fn poll(&self) -> bool; + fn post_notification(&self); + fn usec_delay_between_polls(&self) -> u64; + fn notification_fd(&self) -> i32; + fn notification_fd_became_readable(&self, fd: i32) -> bool; + } +} + +fn ffi_default_notifier() -> Box { + Box::new(UniversalNotifierFFI(default_notifier())) +} + +impl UniversalNotifierFFI { + fn poll(&self) -> bool { + todo!("universal notifier") + } + fn post_notification(&self) { + todo!("universal notifier") + } + fn usec_delay_between_polls(&self) -> u64 { + todo!("universal notifier") + } + fn notification_fd(&self) -> RawFd { + todo!("universal notifier") + } + fn notification_fd_became_readable(&self, _fd: RawFd) -> bool { + todo!("universal notifier") + } +} diff --git a/fish-rust/src/event.rs b/fish-rust/src/event.rs index 6460dc096..8d190845d 100644 --- a/fish-rust/src/event.rs +++ b/fish-rust/src/event.rs @@ -4,19 +4,19 @@ //! defined when these functions produce output or perform memory allocations, since such functions //! may not be safely called by signal handlers. -use autocxx::WithinUniquePtr; -use cxx::{CxxVector, CxxWString, UniquePtr}; +use crate::ffi::wcstring_list_ffi_t; +use cxx::{CxxWString, UniquePtr}; use libc::pid_t; use std::num::NonZeroU32; use std::pin::Pin; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::{Arc, Mutex}; -use crate::builtins::shared::IoStreams; -use crate::common::{escape_string, scoped_push, EscapeFlags, EscapeStringStyle, ScopeGuard}; -use crate::ffi::{self, block_t, Parser, Repin}; +use crate::common::{escape, scoped_push_replacer, ScopeGuard}; use crate::flog::FLOG; +use crate::io::{IoChain, IoStreams}; use crate::job_group::{JobId, MaybeJobId}; +use crate::parser::{Block, Parser}; use crate::signal::{signal_check_cancel, signal_handle, Signal}; use crate::termsize; use crate::wchar::prelude::*; @@ -29,8 +29,9 @@ mod event_ffi { include!("parser.h"); include!("io.h"); type wcharz_t = crate::ffi::wcharz_t; - type Parser = crate::ffi::Parser; - type IoStreams = crate::ffi::IoStreams; + type Parser = crate::parser::Parser; + type IoStreams<'a> = crate::io::IoStreams<'a>; + type wcstring_list_ffi_t = crate::ffi::wcstring_list_ffi_t; } enum event_type_t { @@ -77,16 +78,12 @@ struct event_description_t { fn set_removed(self: &mut EventHandler); fn event_fire_generic_ffi( - parser: Pin<&mut Parser>, + parser: &Parser, name: &CxxWString, - arguments: &CxxVector, + arguments: &wcstring_list_ffi_t, ); - #[cxx_name = "event_get_desc"] - fn event_get_desc_ffi(parser: &Parser, evt: &Event) -> UniquePtr; #[cxx_name = "event_fire_delayed"] - fn event_fire_delayed_ffi(parser: Pin<&mut Parser>); - #[cxx_name = "event_fire"] - fn event_fire_ffi(parser: Pin<&mut Parser>, event: &Event); + fn fire_delayed(parser: &Parser); #[cxx_name = "event_print"] fn event_print_ffi(streams: Pin<&mut IoStreams>, type_filter: &CxxWString); @@ -408,16 +405,14 @@ pub fn caller_exit(internal_job_id: u64, job_id: MaybeJobId) -> Self { } /// Test if specified event is blocked. - fn is_blocked(&self, parser: &mut Parser) -> bool { - let mut i = 0; - while let Some(block) = parser.get_block_at_index(i) { - i += 1; - if block.ffi_event_blocks() != 0 { + fn is_blocked(&self, parser: &Parser) -> bool { + for block in parser.blocks().iter().rev() { + if block.event_blocks != 0 { return true; } } - parser.ffi_global_event_blocks() != 0 + parser.global_event_blocks.load(Ordering::Relaxed) != 0 } } @@ -573,11 +568,7 @@ pub fn get_desc(parser: &Parser, evt: &Event) -> WString { EventDescription::ProcessExit { pid } => format!("exit handler for process {pid}"), EventDescription::JobExit { pid, .. } => { if let Some(job) = parser.job_get_from_pid(*pid) { - format!( - "exit handler for job {}, '{}'", - job.job_id().0, - job.command() - ) + format!("exit handler for job {}, '{}'", job.job_id(), job.command()) } else { format!("exit handler for job with pid {pid}") } @@ -592,10 +583,6 @@ pub fn get_desc(parser: &Parser, evt: &Event) -> WString { WString::from_str(&s) } -fn event_get_desc_ffi(parser: &Parser, evt: &Event) -> UniquePtr { - get_desc(parser, evt).to_ffi() -} - /// Add an event handler. pub fn add_handler(eh: EventHandler) { if let EventDescription::Signal { signal } = eh.desc { @@ -661,22 +648,25 @@ fn event_get_function_handler_descs_ffi(name: &CxxWString) -> Vec= 0, + parser.libdata().pods.is_event >= 0, "is_event should not be negative" ); // Suppress fish_trace during events. - let is_event = parser.libdata_pod().is_event; - let mut parser = scoped_push( - parser, - |parser| &mut parser.libdata_pod().is_event, + let is_event = parser.libdata().pods.is_event; + let _inc_event = scoped_push_replacer( + |new_value| std::mem::replace(&mut parser.libdata_mut().pods.is_event, new_value), is_event + 1, ); - let mut parser = scoped_push( - &mut *parser, - |parser| &mut parser.libdata_pod().suppress_fish_trace, + let _suppress_trace = scoped_push_replacer( + |new_value| { + std::mem::replace( + &mut parser.libdata_mut().pods.suppress_fish_trace, + new_value, + ) + }, true, ); @@ -702,20 +692,17 @@ fn fire_internal(parser: &mut Parser, event: &Event) { let mut buffer = handler.function_name.clone(); for arg in &event.arguments { buffer.push(' '); - buffer.push_utfstr(&escape_string( - arg, - EscapeStringStyle::Script(EscapeFlags::default()), - )); + buffer.push_utfstr(&escape(arg)); } // Event handlers are not part of the main flow of code, so they are marked as // non-interactive. let saved_is_interactive = - std::mem::replace(&mut parser.libdata_pod().is_interactive, false); - let saved_statuses = parser.get_last_statuses().within_unique_ptr(); - let mut parser = ScopeGuard::new(&mut *parser, |parser| { - parser.pin().set_last_statuses(saved_statuses); - parser.libdata_pod().is_interactive = saved_is_interactive; + std::mem::replace(&mut parser.libdata_mut().pods.is_interactive, false); + let saved_statuses = parser.get_last_statuses(); + let _cleanup = ScopeGuard::new((), |()| { + parser.set_last_statuses(saved_statuses); + parser.libdata_mut().pods.is_interactive = saved_is_interactive; }); FLOG!( @@ -727,14 +714,9 @@ fn fire_internal(parser: &mut Parser, event: &Event) { "'" ); - let b = (*parser) - .pin() - .push_block(block_t::event_block((event as *const Event).cast()).within_unique_ptr()); - (*parser) - .pin() - .eval_string_ffi1(&buffer.to_ffi()) - .within_unique_ptr(); - (*parser).pin().pop_block(b); + let b = parser.push_block(Block::event_block(event.clone())); + parser.eval(&buffer, &IoChain::new()); + parser.pop_block(b); handler.fired.store(true, Ordering::Relaxed); fired_one_shot |= handler.is_one_shot(); @@ -746,13 +728,16 @@ fn fire_internal(parser: &mut Parser, event: &Event) { } /// Fire all delayed events attached to the given parser. -pub fn fire_delayed(parser: &mut Parser) { - let ld = parser.libdata_pod(); +pub fn fire_delayed(parser: &Parser) { + { + let ld = &parser.libdata().pods; + + // Do not invoke new event handlers from within event handlers. + if ld.is_event != 0 { + return; + }; + } - // Do not invoke new event handlers from within event handlers. - if ld.is_event != 0 { - return; - }; // Do not invoke new event handlers if we are unwinding (#6649). if signal_check_cancel() != 0 { return; @@ -760,7 +745,7 @@ pub fn fire_delayed(parser: &mut Parser) { // We unfortunately can't keep this locked until we're done with it because the SIGWINCH handler // code might call back into here and we would delay processing of the events, leading to a test - // failure under CI. (Yes, the `&mut Parser` is a lie.) + // failure under CI. let mut to_send = std::mem::take(&mut *BLOCKED_EVENTS.lock().expect("Mutex poisoned!")); // Append all signal events to to_send. @@ -799,10 +784,6 @@ pub fn fire_delayed(parser: &mut Parser) { } } -fn event_fire_delayed_ffi(parser: Pin<&mut Parser>) { - fire_delayed(parser.unpin()) -} - /// Enqueue a signal event. Invoked from a signal handler. pub fn enqueue_signal(signal: libc::c_int) { // Beware, we are in a signal handler @@ -810,7 +791,7 @@ pub fn enqueue_signal(signal: libc::c_int) { } /// Fire the specified event event, executing it on `parser`. -pub fn fire(parser: &mut Parser, event: Event) { +pub fn fire(parser: &Parser, event: Event) { // Fire events triggered by signals. fire_delayed(parser); @@ -821,10 +802,6 @@ pub fn fire(parser: &mut Parser, event: Event) { } } -fn event_fire_ffi(parser: Pin<&mut Parser>, event: &Event) { - fire(parser.unpin(), event.clone()) -} - #[widestrs] pub const EVENT_FILTER_NAMES: [&wstr; 7] = [ "signal"L, @@ -892,13 +869,12 @@ pub fn print(streams: &mut IoStreams, type_filter: &wstr) { } } -fn event_print_ffi(streams: Pin<&mut ffi::IoStreams>, type_filter: &CxxWString) { - let mut streams = IoStreams::new(streams); - print(&mut streams, type_filter.as_wstr()); +fn event_print_ffi(streams: Pin<&mut IoStreams>, type_filter: &CxxWString) { + print(streams.get_mut(), type_filter.as_wstr()); } /// Fire a generic event with the specified name. -pub fn fire_generic(parser: &mut Parser, name: WString, arguments: Vec) { +pub fn fire_generic(parser: &Parser, name: WString, arguments: Vec) { fire( parser, Event { @@ -908,14 +884,6 @@ pub fn fire_generic(parser: &mut Parser, name: WString, arguments: Vec) ) } -fn event_fire_generic_ffi( - parser: Pin<&mut Parser>, - name: &CxxWString, - arguments: &CxxVector, -) { - fire_generic( - parser.unpin(), - name.from_ffi(), - arguments.iter().map(WString::from).collect(), - ); +fn event_fire_generic_ffi(parser: &Parser, name: &CxxWString, arguments: &wcstring_list_ffi_t) { + fire_generic(parser, name.from_ffi(), arguments.from_ffi()); } diff --git a/fish-rust/src/exec.rs b/fish-rust/src/exec.rs new file mode 100644 index 000000000..064edb829 --- /dev/null +++ b/fish-rust/src/exec.rs @@ -0,0 +1,1533 @@ +// Functions for executing a program. +// +// Some of the code in this file is based on code from the Glibc manual, though the changes +// performed have been massive. + +use crate::builtins::shared::{ + builtin_run, truncate_at_nul, STATUS_CMD_ERROR, STATUS_CMD_OK, STATUS_CMD_UNKNOWN, + STATUS_NOT_EXECUTABLE, STATUS_READ_TOO_MUCH, +}; +use crate::common::{ + exit_without_destructors, scoped_push_replacer, str2wcstring, wcs2string, wcs2zstring, + write_loop, ScopeGuard, +}; +use crate::compat::_PATH_BSHELL; +use crate::env::{EnvMode, EnvStack, Environment, Statuses, READ_BYTE_LIMIT}; +use crate::env_dispatch::use_posix_spawn; +use crate::fds::make_fd_blocking; +use crate::fds::{make_autoclose_pipes, open_cloexec, AutoCloseFd, AutoClosePipes, PIPE_ERROR}; +use crate::ffi::{self, wcstring_list_ffi_t}; +use crate::flog::FLOGF; +use crate::fork_exec::blocked_signals_for_job; +use crate::fork_exec::postfork::{ + child_setup_process, execute_fork, execute_setpgid, report_setpgid_error, + safe_report_exec_error, +}; +#[cfg(FISH_USE_POSIX_SPAWN)] +use crate::fork_exec::spawn::PosixSpawner; +use crate::function::{self, FunctionProperties}; +use crate::io::{ + BufferedOutputStream, FdOutputStream, IoBufferfill, IoChain, IoClose, IoMode, IoPipe, + IoStreams, OutputStream, SeparatedBuffer, StringOutputStream, +}; +use crate::null_terminated_array::{ + null_terminated_array_length, AsNullTerminatedArray, OwningNullTerminatedArray, +}; +use crate::parser::{Block, BlockId, BlockType, EvalRes, Parser}; +use crate::proc::{ + hup_jobs, is_interactive_session, jobs_requiring_warning_on_exit, no_exec, + print_exit_warning_for_jobs, InternalProc, Job, JobGroupRef, ProcStatus, Process, ProcessType, + TtyTransfer, INVALID_PID, +}; +use crate::reader::reader_run_count; +use crate::redirection::{dup2_list_resolve_chain, Dup2List}; +use crate::threads::{iothread_perform_cant_wait, is_forked_child}; +use crate::timer::push_timer; +use crate::trace::trace_if_enabled_with_args; +use crate::wchar::{wstr, WString, L}; +use crate::wchar_ext::ToWString; +use crate::wchar_ffi::AsWstr; +use crate::wchar_ffi::WCharToFFI; +use crate::wutil::{fish_wcstol, perror}; +use crate::wutil::{wgettext, wgettext_fmt}; +use cxx::{CxxWString, UniquePtr}; +use errno::{errno, set_errno}; +use libc::c_int; +use libc::{ + c_char, isatty, EACCES, ENOENT, ENOEXEC, ENOTDIR, EPIPE, EXIT_FAILURE, EXIT_SUCCESS, O_NOCTTY, + O_RDONLY, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO, +}; +use std::ffi::CStr; +use std::io::{Read, Write}; +use std::os::fd::{FromRawFd, RawFd}; +use std::slice; +use std::sync::atomic::Ordering; +use std::sync::{atomic::AtomicUsize, Arc}; +use widestring_suffix::widestrs; + +/// Execute the processes specified by \p j in the parser \p. +/// On a true return, the job was successfully launched and the parser will take responsibility for +/// cleaning it up. On a false return, the job could not be launched and the caller must clean it +/// up. +pub fn exec_job(parser: &Parser, job: &Job, block_io: IoChain) -> bool { + // If fish was invoked with -n or --no-execute, then no_exec will be set and we do nothing. + if no_exec() { + return true; + } + + // Handle an exec call. + if job.processes()[0].typ == ProcessType::exec { + // If we are interactive, perhaps disallow exec if there are background jobs. + if !allow_exec_with_background_jobs(parser) { + for p in job.processes().iter() { + p.mark_aborted_before_launch(); + } + return false; + } + + // Apply foo=bar variable assignments + for assignment in &job.processes()[0].variable_assignments { + parser.vars().set( + &assignment.variable_name, + EnvMode::LOCAL | EnvMode::EXPORT, + assignment.values.clone(), + ); + } + + internal_exec(parser.vars(), job, block_io); + // internal_exec only returns if it failed to set up redirections. + // In case of an successful exec, this code is not reached. + let status = if job.flags().negate { 0 } else { 1 }; + parser.set_last_statuses(Statuses::just(status)); + + // A false return tells the caller to remove the job from the list. + for p in job.processes().iter() { + p.mark_aborted_before_launch(); + } + return false; + } + let _timer = push_timer(job.wants_timing() && !no_exec()); + + // Get the deferred process, if any. We will have to remember its pipes. + let mut deferred_pipes = AutoClosePipes::default(); + let deferred_process = get_deferred_process(job); + + // We may want to transfer tty ownership to the pgroup leader. + let mut transfer = TtyTransfer::new(); + + // This loop loops over every process_t in the job, starting it as appropriate. This turns out + // to be rather complex, since a process_t can be one of many rather different things. + // + // The loop also has to handle pipelining between the jobs. + // + // We can have up to three pipes "in flight" at a time: + // + // 1. The pipe the current process should read from (courtesy of the previous process) + // 2. The pipe that the current process should write to + // 3. The pipe that the next process should read from (courtesy of us) + // + // Lastly, a process may experience a pipeline-aborting error, which prevents launching + // further processes in the pipeline. + let mut pipe_next_read = AutoCloseFd::empty(); + let mut aborted_pipeline = false; + let mut procs_launched = 0; + for i in 0..job.processes().len() { + let p = &job.processes()[i]; + // proc_pipes is the pipes applied to this process. That is, it is the read end + // containing the output of the previous process (if any), plus the write end that will + // output to the next process (if any). + let mut proc_pipes = AutoClosePipes::default(); + std::mem::swap(&mut proc_pipes.read, &mut pipe_next_read); + if !p.is_last_in_job { + let Some(pipes) = make_autoclose_pipes() else { + FLOGF!(warning, "%ls", wgettext!(PIPE_ERROR)); + perror("pipe"); + aborted_pipeline = true; + abort_pipeline_from(job, i); + break; + }; + pipe_next_read = pipes.read; + proc_pipes.write = pipes.write; + + // Save any deferred process for last. By definition, the deferred process can + // never be the last process in the job, so it's safe to nest this in the outer + // `if (!p->is_last_in_job)` block, which makes it clear that `proc_next_read` will + // always be assigned when we `continue` the loop. + if Some(i) == deferred_process { + deferred_pipes = proc_pipes; + continue; + } + } + + // Regular process. + if exec_process_in_job( + parser, + p, + job, + block_io.clone(), + proc_pipes, + &deferred_pipes, + false, + ) + .is_err() + { + aborted_pipeline = true; + abort_pipeline_from(job, i); + break; + } + procs_launched += 1; + + // Transfer tty? + if p.leads_pgrp && job.group().wants_terminal() { + transfer.to_job_group(job.group.as_ref().unwrap()); + } + } + pipe_next_read.close(); + + // If our pipeline was aborted before any process was successfully launched, then there is + // nothing to reap, and we can perform an early return. + // Note we must never return false if we have launched even one process, since it will not be + // properly reaped; see #7038. + if aborted_pipeline && procs_launched == 0 { + return false; + } + + // Ok, at least one thing got launched. + // Handle any deferred process. + if let Some(dp) = deferred_process { + if + // Some other process already aborted our pipeline. + aborted_pipeline + // The deferred proc itself failed to launch. + || exec_process_in_job( + parser, + &job.processes()[dp], + job, + block_io, + deferred_pipes, + &AutoClosePipes::default(), + true, + ) + .is_err() + { + job.processes()[dp].mark_aborted_before_launch(); + } + } + + FLOGF!( + exec_job_exec, + "Executed job %d from command '%ls'", + job.job_id(), + job.command() + ); + + job.mark_constructed(); + + // If exec_error then a backgrounded job would have been terminated before it was ever assigned + // a pgroup, so error out before setting last_pid. + if !job.is_foreground() { + if let Some(last_pid) = job.get_last_pid() { + parser + .vars() + .set_one(L!("last_pid"), EnvMode::GLOBAL, last_pid.to_wstring()); + } else { + parser.vars().set_empty(L!("last_pid"), EnvMode::GLOBAL); + } + } + + if !job.is_initially_background() { + job.continue_job(parser); + } + + if job.is_stopped() { + transfer.save_tty_modes(); + } + transfer.reclaim(); + true +} + +/// Evaluate a command. +/// +/// \param cmd the command to execute +/// \param parser the parser with which to execute code +/// \param outputs if set, the list to insert output into. +/// \param apply_exit_status if set, update $status within the parser, otherwise do not. +/// +/// \return a value appropriate for populating $status. +pub fn exec_subshell( + cmd: &wstr, + parser: &Parser, + outputs: Option<&mut Vec>, + apply_exit_status: bool, +) -> libc::c_int { + let mut break_expand = false; + exec_subshell_internal( + cmd, + parser, + None, + outputs, + &mut break_expand, + apply_exit_status, + false, + ) +} + +/// Like exec_subshell, but only returns expansion-breaking errors. That is, a zero return means +/// "success" (even though the command may have failed), a non-zero return means that we should +/// halt expansion. If the \p pgid is supplied, then any spawned external commands should join that +/// pgroup. +pub fn exec_subshell_for_expand( + cmd: &wstr, + parser: &Parser, + job_group: Option<&JobGroupRef>, + outputs: &mut Vec, +) -> libc::c_int { + parser.assert_can_execute(); + let mut break_expand = true; + let ret = exec_subshell_internal( + cmd, + parser, + job_group, + Some(outputs), + &mut break_expand, + true, + true, + ); + // Only return an error code if we should break expansion. + if break_expand { + ret + } else { + STATUS_CMD_OK.unwrap() + } +} + +/// Number of calls to fork() or posix_spawn(). +static FORK_COUNT: AtomicUsize = AtomicUsize::new(0); + +/// A launch_result_t indicates when a process failed to launch, and therefore the rest of the +/// pipeline should be aborted. This includes failed redirections, fd exhaustion, fork() failures, +/// etc. +type LaunchResult = Result<(), ()>; + +/// Given an error \p err returned from either posix_spawn or exec, \return a process exit code. +fn exit_code_from_exec_error(err: libc::c_int) -> libc::c_int { + assert!(err != 0, "Zero is success, not an error"); + match err { + ENOENT | ENOTDIR => { + // This indicates either the command was not found, or a file redirection was not found. + // We do not use posix_spawn file redirections so this is always command-not-found. + STATUS_CMD_UNKNOWN.unwrap() + } + EACCES | ENOEXEC => { + // The file is not executable for various reasons. + STATUS_NOT_EXECUTABLE.unwrap() + } + #[cfg(target_os = "macos")] + libc::EBADARCH => { + // This is for e.g. running ARM app on Intel Mac. + STATUS_NOT_EXECUTABLE.unwrap() + } + _ => { + // Generic failure. + EXIT_FAILURE + } + } +} + +/// This is a 'looks like text' check. +/// \return true if either there is no NUL byte, or there is a line containing a lowercase letter +/// before the first NUL byte. +fn is_thompson_shell_payload(p: &[u8]) -> bool { + if !p.contains(&b'\0') { + return true; + }; + let mut haslower = false; + for c in p { + if c.is_ascii_lowercase() || *c == b'$' || *c == b'`' { + haslower = true; + } + if haslower && *c == b'\n' { + return true; + } + } + false +} + +/// This function checks the beginning of a file to see if it's safe to +/// pass to the system interpreter when execve() returns ENOEXEC. +/// +/// The motivation is to be able to run classic shell scripts which +/// didn't have shebang, while protecting the user from accidentally +/// running a binary file which may corrupt terminal driver state. We +/// check for lowercase letters because the ASCII magic of binary files +/// is usually uppercase, e.g. PNG, JFIF, MZ, etc. These rules are also +/// flexible enough to permit scripts with concatenated binary content, +/// such as Actually Portable Executable. +/// N.B.: this is called after fork, it must not allocate heap memory. +pub fn is_thompson_shell_script(path: &CStr) -> bool { + // Paths ending in ".fish" are never considered Thompson shell scripts. + if path.to_bytes().ends_with(".fish".as_bytes()) { + return false; + } + let e = errno(); + let mut res = false; + let fd = open_cloexec(path, O_RDONLY | O_NOCTTY, 0); + if fd != -1 { + let mut file = unsafe { std::fs::File::from_raw_fd(fd) }; + let mut buf = [b'\0'; 256]; + if let Ok(got) = file.read(&mut buf) { + if is_thompson_shell_payload(&buf[..got]) { + res = true; + } + } + } + set_errno(e); + res +} + +/// This function is executed by the child process created by a call to fork(). It should be called +/// after \c child_setup_process. It calls execve to replace the fish process image with the command +/// specified in \c p. It never returns. Called in a forked child! Do not allocate memory, etc. +fn safe_launch_process( + _p: &Process, + actual_cmd: &CStr, + argv: &impl AsNullTerminatedArray, + envv: &impl AsNullTerminatedArray, +) -> ! { + // This function never returns, so we take certain liberties with constness. + + unsafe { libc::execve(actual_cmd.as_ptr(), argv.get(), envv.get()) }; + let err = errno(); + + // The shebang wasn't introduced until UNIX Seventh Edition, so if + // the kernel won't run the binary we hand it off to the interpreter + // after performing a binary safety check, recommended by POSIX: a + // line needs to exist before the first \0 with a lowercase letter + + if err.0 == ENOEXEC && is_thompson_shell_script(actual_cmd) { + // Construct new argv. + // We must not allocate memory, so only 128 args are supported. + const maxargs: usize = 128; + let nargs = null_terminated_array_length(argv.get()); + let argv = unsafe { slice::from_raw_parts(argv.get(), nargs) }; + if nargs <= maxargs { + // +1 for /bin/sh, +1 for terminating nullptr + let mut argv2 = [std::ptr::null(); 1 + maxargs + 1]; + argv2[0] = _PATH_BSHELL.load(Ordering::Relaxed); + argv2[1..argv.len() + 1].copy_from_slice(argv); + // The command to call should use the full path, + // not what we would pass as argv0. + argv2[1] = actual_cmd.as_ptr(); + unsafe { + libc::execve(_PATH_BSHELL.load(Ordering::Relaxed), &argv2[0], envv.get()); + } + } + } + + set_errno(err); + safe_report_exec_error(errno().0, actual_cmd.as_ptr(), argv.get(), envv.get()); + exit_without_destructors(exit_code_from_exec_error(err.0)); +} + +/// This function is similar to launch_process, except it is not called after a fork (i.e. it only +/// calls exec) and therefore it can allocate memory. +fn launch_process_nofork(vars: &EnvStack, p: &Process) -> ! { + assert!(!is_forked_child()); + + // Construct argv. Ensure the strings stay alive for the duration of this function. + let narrow_strings = p.argv().iter().map(|s| wcs2zstring(s)).collect(); + let argv = OwningNullTerminatedArray::new(narrow_strings); + + // Construct envp. + let envp = vars.export_array(); + let actual_cmd = wcs2zstring(&p.actual_cmd); + + // Ensure the terminal modes are what they were before we changed them. + ffi::restore_term_mode(); + // Bounce to launch_process. This never returns. + safe_launch_process(p, &actual_cmd, &argv, &*envp); +} + +// Returns whether we can use posix spawn for a given process in a given job. +// +// To avoid the race between the caller calling tcsetpgrp() and the client checking the +// foreground process group, we don't use posix_spawn if we're going to foreground the process. (If +// we use fork(), we can call tcsetpgrp after the fork, before the exec, and avoid the race). +fn can_use_posix_spawn_for_job(job: &Job, dup2s: &Dup2List) -> bool { + // Is it globally disabled? + if !use_posix_spawn() { + return false; + } + + // Hack - do not use posix_spawn if there are self-fd redirections. + // For example if you were to write: + // cmd 6< /dev/null + // it is possible that the open() of /dev/null would result in fd 6. Here even if we attempted + // to add a dup2 action, it would be ignored and the CLO_EXEC bit would remain. So don't use + // posix_spawn in this case; instead we'll call fork() and clear the CLO_EXEC bit manually. + for action in dup2s.get_actions() { + if action.src == action.target { + return false; + } + } + // If this job will be foregrounded, we will call tcsetpgrp(), therefore do not use + // posix_spawn. + let wants_terminal = job.group().wants_terminal(); + !wants_terminal +} + +#[widestrs] +fn internal_exec(vars: &EnvStack, j: &Job, block_io: IoChain) { + // Do a regular launch - but without forking first... + let mut all_ios = block_io; + if !all_ios.append_from_specs(j.processes()[0].redirection_specs(), &vars.get_pwd_slash()) { + return; + } + + let mut blocked_signals: libc::sigset_t = unsafe { std::mem::zeroed() }; + unsafe { libc::sigemptyset(&mut blocked_signals) }; + let blocked_signals = if blocked_signals_for_job(j, &mut blocked_signals) { + Some(&blocked_signals) + } else { + None + }; + + // child_setup_process makes sure signals are properly set up. + let redirs = dup2_list_resolve_chain(&all_ios); + if child_setup_process( + 0, /* not claim_tty */ + blocked_signals, + false, /* not is_forked */ + &redirs, + ) == 0 + { + // Decrement SHLVL as we're removing ourselves from the shell "stack". + if is_interactive_session() { + let shlvl_var = vars.getf("SHLVL"L, EnvMode::GLOBAL | EnvMode::EXPORT); + let mut shlvl_str = "0"L.to_owned(); + if let Some(shlvl_var) = shlvl_var { + if let Ok(shlvl) = fish_wcstol(&shlvl_var.as_string()) { + if shlvl > 0 { + shlvl_str = (shlvl - 1).to_wstring(); + } + } + } + vars.set_one("SHLVL"L, EnvMode::GLOBAL | EnvMode::EXPORT, shlvl_str); + } + + // launch_process _never_ returns. + launch_process_nofork(vars, &j.processes()[0]); + } +} + +/// Construct an internal process for the process p. In the background, write the data \p outdata to +/// stdout and \p errdata to stderr, respecting the io chain \p ios. For example if target_fd is 1 +/// (stdout), and there is a dup2 3->1, then we need to write to fd 3. Then exit the internal +/// process. +fn run_internal_process(p: &Process, outdata: Vec, errdata: Vec, ios: &IoChain) { + p.check_generations_before_launch(); + + // We want both the dup2s and the io_chain_ts to be kept alive by the background thread, because + // they may own an fd that we want to write to. Move them all to a shared_ptr. The strings as + // well (they may be long). + // Construct a little helper struct to make it simpler to move into our closure without copying. + struct WriteFields { + src_outfd: RawFd, + outdata: Vec, + + src_errfd: RawFd, + errdata: Vec, + + ios: IoChain, + dup2s: Dup2List, + internal_proc: Arc, + + success_status: ProcStatus, + } + impl WriteFields { + fn skip_out(&self) -> bool { + self.outdata.is_empty() || self.src_outfd < 0 + } + fn skip_err(&self) -> bool { + self.errdata.is_empty() || self.src_errfd < 0 + } + } + + // Construct and assign the internal process to the real process. + let internal_proc = Arc::new(InternalProc::new()); + let old = p.internal_proc.replace(Some(internal_proc.clone())); + assert!( + old.is_none(), + "Replaced p.internal_proc, but it already had a value!" + ); + let mut f = Box::new(WriteFields { + src_outfd: -1, + outdata, + + src_errfd: -1, + errdata, + + ios: IoChain::default(), + dup2s: Dup2List::new(), + internal_proc: internal_proc.clone(), + + success_status: ProcStatus::default(), + }); + + FLOGF!( + proc_internal_proc, + "Created internal proc %llu to write output for proc '%ls'", + internal_proc.get_id(), + p.argv0().unwrap() + ); + + // Resolve the IO chain. + // Note it's important we do this even if we have no out or err data, because we may have been + // asked to truncate a file (e.g. `echo -n '' > /tmp/truncateme.txt'). The open() in the dup2 + // list resolution will ensure this happens. + f.dup2s = dup2_list_resolve_chain(ios); + + // Figure out which source fds to write to. If they are closed (unlikely) we just exit + // successfully. + f.src_outfd = f.dup2s.fd_for_target_fd(STDOUT_FILENO); + f.src_errfd = f.dup2s.fd_for_target_fd(STDERR_FILENO); + + // If we have nothing to write we can elide the thread. + // TODO: support eliding output to /dev/null. + if f.skip_out() && f.skip_err() { + internal_proc.mark_exited(&p.status); + return; + } + + // Ensure that ios stays alive, it may own fds. + f.ios = ios.clone(); + + // If our process is a builtin, it will have already set its status value. Make sure we + // propagate that if our I/O succeeds and don't read it on a background thread. TODO: have + // builtin_run provide this directly, rather than setting it in the process. + f.success_status = p.status.clone(); + + iothread_perform_cant_wait(move || { + let mut status = f.success_status.clone(); + if !f.skip_out() { + if let Err(err) = write_loop(&f.src_outfd, &f.outdata) { + if err.raw_os_error().unwrap() != EPIPE { + perror("write"); + } + if status.is_success() { + status = ProcStatus::from_exit_code(1); + } + } + } + if !f.skip_err() { + if let Err(err) = write_loop(&f.src_errfd, &f.errdata) { + if err.raw_os_error().unwrap() != EPIPE { + perror("write"); + } + if status.is_success() { + status = ProcStatus::from_exit_code(1); + } + } + } + f.internal_proc.mark_exited(&status); + }); +} + +/// If \p outdata or \p errdata are both empty, then mark the process as completed immediately. +/// Otherwise, run an internal process. +fn run_internal_process_or_short_circuit( + parser: &Parser, + j: &Job, + p: &Process, + outdata: Vec, + errdata: Vec, + ios: &IoChain, +) { + if outdata.is_empty() && errdata.is_empty() { + p.completed.store(true); + if p.is_last_in_job { + FLOGF!( + exec_job_status, + "Set status of job %d (%ls) to %d using short circuit", + j.job_id(), + j.preview(), + p.status.status_value() + ); + if let Some(statuses) = j.get_statuses() { + parser.set_last_statuses(statuses); + parser.libdata_mut().pods.status_count += 1; + } else if j.flags().negate { + // Special handling for `not set var (substitution)`. + // If there is no status, but negation was requested, + // take the last status and negate it. + let mut last_statuses = parser.get_last_statuses(); + last_statuses.status = if last_statuses.status == 0 { 1 } else { 0 }; + parser.set_last_statuses(last_statuses); + } + } + } else { + run_internal_process(p, outdata, errdata, ios); + } +} + +/// Call fork() as part of executing a process \p p in a job \j. Execute \p child_action in the +/// context of the child. +fn fork_child_for_process( + job: &Job, + p: &Process, + dup2s: &Dup2List, + fork_type: &wstr, + child_action: impl FnOnce(&Process), +) -> LaunchResult { + // Claim the tty from fish, if the job wants it and we are the pgroup leader. + let claim_tty_from = if p.leads_pgrp && job.group().wants_terminal() { + unsafe { libc::getpgrp() } + } else { + INVALID_PID + }; + + // Decide if the job wants to set a custom sigmask. + let mut blocked_signals: libc::sigset_t = unsafe { std::mem::zeroed() }; + unsafe { libc::sigemptyset(&mut blocked_signals) }; + let blocked_signals = if blocked_signals_for_job(job, &mut blocked_signals) { + Some(&blocked_signals) + } else { + None + }; + + // Narrow the command name for error reporting before fork, + // to avoid allocations in the forked child. + let narrow_cmd = wcs2zstring(job.command()); + let narrow_argv0 = wcs2zstring(p.argv0().unwrap_or_default()); + + let pid = execute_fork(); + if pid < 0 { + return Err(()); + } + let is_parent = pid > 0; + + // Record the pgroup if this is the leader. + // Both parent and child attempt to send the process to its new group, to resolve the race. + p.set_pid(if is_parent { + pid + } else { + unsafe { libc::getpid() } + }); + if p.leads_pgrp { + job.group().set_pgid(pid); + } + { + if let Some(pgid) = job.group().get_pgid() { + let err = execute_setpgid(p.pid(), pgid, is_parent); + if err != 0 { + report_setpgid_error( + err, + is_parent, + p.pid(), + pgid, + job.job_id().as_num(), + narrow_cmd.as_ptr(), + narrow_argv0.as_ptr(), + ); + } + } + } + + if !is_parent { + // Child process. + child_setup_process(claim_tty_from, blocked_signals, true, dup2s); + child_action(p); + panic!("Child process returned control to fork_child lambda!"); + } + + let count = FORK_COUNT.fetch_add(1, Ordering::Relaxed) + 1; + FLOGF!( + exec_fork, + "Fork #%d, pid %d: %s for '%ls'", + count, + pid, + fork_type, + p.argv0().unwrap() + ); + Ok(()) +} + +/// \return an newly allocated output stream for the given fd, which is typically stdout or stderr. +/// This inspects the io_chain and decides what sort of output stream to return. +/// If \p piped_output_needs_buffering is set, and if the output is going to a pipe, then the other +/// end then synchronously writing to the pipe risks deadlock, so we must buffer it. +fn create_output_stream_for_builtin( + fd: RawFd, + io_chain: &IoChain, + piped_output_needs_buffering: bool, +) -> OutputStream { + let Some(io) = io_chain.io_for_fd(fd) else { + // Common case of no redirections. + // Just write to the fd directly. + return OutputStream::Fd(FdOutputStream::new(fd)); + }; + match io.io_mode() { + IoMode::bufferfill => { + // Our IO redirection is to an internal buffer, e.g. a command substitution. + // We will write directly to it. + let buffer = io.as_bufferfill().unwrap().buffer_ref(); + OutputStream::Buffered(BufferedOutputStream::new(buffer.clone())) + } + IoMode::close => { + // Like 'echo foo >&-' + OutputStream::Null + } + IoMode::file => { + // Output is to a file which has been opened. + OutputStream::Fd(FdOutputStream::new(io.source_fd())) + } + IoMode::pipe => { + // Output is to a pipe. We may need to buffer. + if piped_output_needs_buffering { + OutputStream::String(StringOutputStream::new()) + } else { + OutputStream::Fd(FdOutputStream::new(io.source_fd())) + } + } + IoMode::fd => { + // This is a case like 'echo foo >&5' + // It's uncommon and unclear what should happen. + OutputStream::String(StringOutputStream::new()) + } + } +} + +/// Handle output from a builtin, by printing the contents of builtin_io_streams to the redirections +/// given in io_chain. + +fn handle_builtin_output( + parser: &Parser, + j: &Job, + p: &Process, + io_chain: &IoChain, + out: &OutputStream, + err: &OutputStream, +) { + assert!(p.typ == ProcessType::builtin, "Process is not a builtin"); + + // Figure out any data remaining to write. We may have none, in which case we can short-circuit. + let outbuff = wcs2string(out.contents()); + let errbuff = wcs2string(err.contents()); + + // Some historical behavior. + if !outbuff.is_empty() { + let _ = std::io::stdout().flush(); + } + if !errbuff.is_empty() { + let _ = std::io::stderr().flush(); + } + + // Construct and run our background process. + run_internal_process_or_short_circuit(parser, j, p, outbuff, errbuff, io_chain); +} + +/// Executes an external command. +/// An error return here indicates that the process failed to launch, and the rest of +/// the pipeline should be cancelled. +fn exec_external_command( + parser: &Parser, + j: &Job, + p: &Process, + proc_io_chain: &IoChain, +) -> LaunchResult { + assert!(p.typ == ProcessType::external, "Process is not external"); + // Get argv and envv before we fork. + let narrow_argv = p.argv().iter().map(|s| wcs2zstring(s)).collect(); + let argv = OwningNullTerminatedArray::new(narrow_argv); + + // Convert our IO chain to a dup2 sequence. + let dup2s = dup2_list_resolve_chain(proc_io_chain); + + // Ensure that stdin is blocking before we hand it off (see issue #176). + // Note this will also affect stdout and stderr if they refer to the same tty. + let _ = make_fd_blocking(STDIN_FILENO); + + let envv = parser.vars().export_array(); + + let actual_cmd = wcs2zstring(&p.actual_cmd); + + #[cfg(FISH_USE_POSIX_SPAWN)] + // Prefer to use posix_spawn, since it's faster on some systems like OS X. + if can_use_posix_spawn_for_job(j, &dup2s) { + let file = &parser.libdata().current_filename; + let count = FORK_COUNT.fetch_add(1, Ordering::Relaxed) + 1; // spawn counts as a fork+exec + + let pid = PosixSpawner::new(j, &dup2s).and_then(|mut spawner| { + spawner.spawn(actual_cmd.as_ptr(), argv.get_mut(), envv.get_mut()) + }); + let pid = match pid { + Ok(pid) => pid, + Err(err) => { + safe_report_exec_error(err.0, actual_cmd.as_ptr(), argv.get(), envv.get()); + p.status + .update(&ProcStatus::from_exit_code(exit_code_from_exec_error( + err.0, + ))); + return Err(()); + } + }; + assert!(pid > 0, "Should have either a valid pid, or an error"); + + // This usleep can be used to test for various race conditions + // (https://github.com/fish-shell/fish-shell/issues/360). + // usleep(10000); + + FLOGF!( + exec_fork, + "Fork #%d, pid %d: spawn external command '%s' from '%ls'", + count, + pid, + p.actual_cmd, + file.as_ref() + .map(|s| s.as_utfstr()) + .unwrap_or(L!("")) + ); + + // these are all things do_fork() takes care of normally (for forked processes): + p.pid.store(pid, Ordering::Relaxed); + if p.leads_pgrp { + j.group().set_pgid(pid); + // posix_spawn should in principle set the pgid before returning. + // In glibc, posix_spawn uses fork() and the pgid group is set on the child side; + // therefore the parent may not have seen it be set yet. + // Ensure it gets set. See #4715, also https://github.com/Microsoft/WSL/issues/2997. + execute_setpgid(pid, pid, true /* is parent */); + } + return Ok(()); + } + + fork_child_for_process(j, p, &dup2s, L!("external command"), |p| { + safe_launch_process(p, &actual_cmd, &argv, &*envv) + }) +} + +// Given that we are about to execute a function, push a function block and set up the +// variable environment. +fn function_prepare_environment( + parser: &Parser, + mut argv: Vec, + props: &FunctionProperties, +) -> BlockId { + // Extract the function name and remaining arguments. + let mut func_name = WString::new(); + if !argv.is_empty() { + // Extract and remove the function name from argv. + func_name = argv.remove(0); + } + + let fb = parser.push_block(Block::function_block( + func_name, + argv.clone(), + props.shadow_scope, + )); + let vars = parser.vars(); + + // Setup the environment for the function. There are three components of the environment: + // 1. named arguments + // 2. inherited variables + // 3. argv + + for (idx, named_arg) in props.named_arguments.iter().enumerate() { + if idx < argv.len() { + vars.set_one(named_arg, EnvMode::LOCAL | EnvMode::USER, argv[idx].clone()); + } else { + vars.set_empty(named_arg, EnvMode::LOCAL | EnvMode::USER); + } + } + + for (key, value) in &*props.inherit_vars { + vars.set(key, EnvMode::LOCAL | EnvMode::USER, value.clone()); + } + + vars.set_argv(argv); + fb +} + +// Given that we are done executing a function, restore the environment. +fn function_restore_environment(parser: &Parser, block: BlockId) { + parser.pop_block(block); + + // If we returned due to a return statement, then stop returning now. + parser.libdata_mut().pods.returning = false; +} + +// The "performer" function of a block or function process. +// This accepts a place to execute as \p parser and then executes the result, returning a status. +// This is factored out in this funny way in preparation for concurrent execution. +type ProcPerformer = dyn FnOnce( + &Parser, + &Process, + Option<&mut OutputStream>, + Option<&mut OutputStream>, +) -> ProcStatus; + +// \return a function which may be to run the given process \p. +// May return an empty std::function in the rare case that the to-be called fish function no longer +// exists. This is just a dumb artifact of the fact that we only capture the functions name, not its +// properties, when creating the job; thus a race could delete the function before we fetch its +// properties. +fn get_performer_for_process( + p: &Process, + job: &Job, + io_chain: &IoChain, +) -> Option> { + assert!( + [ProcessType::function, ProcessType::block_node].contains(&p.typ), + "Unexpected process type" + ); + // We want to capture the job group. + let job_group = job.group.clone(); + let io_chain = io_chain.clone(); + + if p.typ == ProcessType::block_node { + Some(Box::new(move |parser: &Parser, p: &Process, _out, _err| { + let source = p + .block_node_source + .as_ref() + .expect("Process is missing source info"); + let node = p + .internal_block_node + .as_ref() + .expect("Process is missing node info"); + parser + .eval_node( + source, + unsafe { node.as_ref() }, + &io_chain, + job_group.as_ref(), + BlockType::top, + ) + .status + })) + } else { + assert!(p.typ == ProcessType::function); + let Some(props) = function::get_props(p.argv0().unwrap()) else { + FLOGF!( + error, + "%ls", + wgettext_fmt!("Unknown function '%ls'", p.argv0().unwrap()) + ); + return None; + }; + Some(Box::new(move |parser: &Parser, p: &Process, _out, _err| { + let argv = p.argv(); + // Pull out the job list from the function. + let body = &props.func_node.jobs; + let fb = function_prepare_environment(parser, argv.clone(), &props); + let parsed_source = props.func_node.parsed_source_ref(); + let mut res = parser.eval_node( + &parsed_source, + body, + &io_chain, + job_group.as_ref(), + BlockType::top, + ); + function_restore_environment(parser, fb); + + // If the function did not execute anything, treat it as success. + if res.was_empty { + res = EvalRes::new(ProcStatus::from_exit_code(EXIT_SUCCESS)); + } + res.status + })) + } +} + +/// Execute a block node or function "process". +/// \p piped_output_needs_buffering if true, buffer the output. +fn exec_block_or_func_process( + parser: &Parser, + j: &Job, + p: &Process, + mut io_chain: IoChain, + piped_output_needs_buffering: bool, +) -> LaunchResult { + // Create an output buffer if we're piping to another process. + let mut block_output_bufferfill = None; + if piped_output_needs_buffering { + // Be careful to handle failure, e.g. too many open fds. + match IoBufferfill::create() { + Some(tmp) => { + // Teach the job about its bufferfill, and add it to our io chain. + io_chain.push(tmp.clone()); + block_output_bufferfill = Some(tmp); + } + None => return Err(()), + } + } + + // Get the process performer, and just execute it directly. + // Do it in this scoped way so that the performer function can be eagerly deallocating releasing + // its captured io chain. + if let Some(performer) = get_performer_for_process(p, j, &io_chain) { + p.status.update(&performer(parser, p, None, None)); + } else { + return Err(()); + } + + // If we have a block output buffer, populate it now. + let mut buffer_contents = vec![]; + if let Some(block_output_bufferfill) = block_output_bufferfill { + // Remove our write pipe and forget it. This may close the pipe, unless another thread has + // claimed it (background write) or another process has inherited it. + io_chain.remove(&*block_output_bufferfill); + buffer_contents = IoBufferfill::finish(block_output_bufferfill).newline_serialized(); + } + + run_internal_process_or_short_circuit( + parser, + j, + p, + buffer_contents, + vec![], /* errdata */ + &io_chain, + ); + + Ok(()) +} + +fn get_performer_for_builtin(p: &Process, j: &Job, io_chain: &IoChain) -> Box { + assert!(p.typ == ProcessType::builtin, "Process must be a builtin"); + + // Determine if we have a "direct" redirection for stdin. + let mut stdin_is_directly_redirected = false; + if !p.is_first_in_job { + // We must have a pipe + stdin_is_directly_redirected = true; + } else { + // We are not a pipe. Check if there is a redirection local to the process + // that's not io_mode_t::close. + for redir in p.redirection_specs() { + if redir.fd == STDIN_FILENO && !redir.is_close() { + stdin_is_directly_redirected = true; + break; + } + } + } + + // Pull out some fields which we want to copy. We don't want to store the process or job in the + // returned closure. + let argv = p.argv().clone(); + let job_group = j.group.clone(); + let mut io_chain = io_chain.clone(); + + // Be careful to not capture p or j by value, as the intent is that this may be run on another + // thread. + Box::new( + move |parser: &Parser, + _p: &Process, + output_stream: Option<&mut OutputStream>, + errput_stream: Option<&mut OutputStream>| { + let output_stream = output_stream.unwrap(); + let errput_stream = errput_stream.unwrap(); + let out_io = io_chain.io_for_fd(STDOUT_FILENO); + let err_io = io_chain.io_for_fd(STDERR_FILENO); + + // Figure out what fd to use for the builtin's stdin. + let mut local_builtin_stdin = STDIN_FILENO; + if let Some(inp) = io_chain.io_for_fd(STDIN_FILENO) { + // Ignore fd redirections from an fd other than the + // standard ones. e.g. in source <&3 don't actually read from fd 3, + // which is internal to fish. We still respect this redirection in + // that we pass it on as a block IO to the code that source runs, + // and therefore this is not an error. + let ignore_redirect = inp.io_mode() == IoMode::fd && inp.source_fd() >= 3; + if !ignore_redirect { + local_builtin_stdin = inp.source_fd(); + } + } + + // Populate our IoStreams. This is a bag of information for the builtin. + let mut streams = IoStreams::new(output_stream, errput_stream); + streams.job_group = job_group; + streams.stdin_fd = local_builtin_stdin; + streams.stdin_is_directly_redirected = stdin_is_directly_redirected; + streams.out_is_redirected = out_io.is_some(); + streams.err_is_redirected = err_io.is_some(); + streams.out_is_piped = out_io + .map(|io| io.io_mode() == IoMode::pipe) + .unwrap_or(false); + streams.err_is_piped = err_io + .map(|io| io.io_mode() == IoMode::pipe) + .unwrap_or(false); + streams.io_chain = &mut io_chain; + + // Execute the builtin. + let mut shim_argv: Vec<&wstr> = + argv.iter().map(|s| truncate_at_nul(s.as_ref())).collect(); + builtin_run(parser, &mut shim_argv, &mut streams) + }, + ) +} + +/// Executes a builtin "process". +fn exec_builtin_process( + parser: &Parser, + j: &Job, + p: &Process, + io_chain: &IoChain, + piped_output_needs_buffering: bool, +) -> LaunchResult { + assert!(p.typ == ProcessType::builtin, "Process is not a builtin"); + let mut out = + create_output_stream_for_builtin(STDOUT_FILENO, io_chain, piped_output_needs_buffering); + let mut err = + create_output_stream_for_builtin(STDERR_FILENO, io_chain, piped_output_needs_buffering); + + let performer = get_performer_for_builtin(p, j, io_chain); + let status = performer(parser, p, Some(&mut out), Some(&mut err)); + p.status.update(&status); + handle_builtin_output(parser, j, p, io_chain, &out, &err); + Ok(()) +} + +/// Executes a process \p \p in \p job, using the pipes \p pipes (which may have invalid fds if this +/// is the first or last process). +/// \p deferred_pipes represents the pipes from our deferred process; if set ensure they get closed +/// in any child. If \p is_deferred_run is true, then this is a deferred run; this affects how +/// certain buffering works. +/// An error return here indicates that the process failed to launch, and the rest of +/// the pipeline should be cancelled. +fn exec_process_in_job( + parser: &Parser, + p: &Process, + j: &Job, + block_io: IoChain, + pipes: AutoClosePipes, + deferred_pipes: &AutoClosePipes, + is_deferred_run: bool, +) -> LaunchResult { + // The write pipe (destined for stdout) needs to occur before redirections. For example, + // with a redirection like this: + // + // `foo 2>&1 | bar` + // + // what we want to happen is this: + // + // dup2(pipe, stdout) + // dup2(stdout, stderr) + // + // so that stdout and stderr both wind up referencing the pipe. + // + // The read pipe (destined for stdin) is more ambiguous. Imagine a pipeline like this: + // + // echo alpha | cat < beta.txt + // + // Should cat output alpha or beta? bash and ksh output 'beta', tcsh gets it right and + // complains about ambiguity, and zsh outputs both (!). No shells appear to output 'alpha', + // so we match bash here. That would mean putting the pipe first, so that it gets trumped by + // the file redirection. + // + // However, eval does this: + // + // echo "begin; $argv "\n" ;end <&3 3<&-" | source 3<&0 + // + // which depends on the redirection being evaluated before the pipe. So the write end of the + // pipe comes first, the read pipe of the pipe comes last. See issue #966. + + // Maybe trace this process. + // TODO: 'and' and 'or' will not show. + trace_if_enabled_with_args(parser, L!(""), p.argv()); + + // The IO chain for this process. + let mut process_net_io_chain = block_io; + + if pipes.write.is_valid() { + process_net_io_chain.push(Arc::new(IoPipe::new( + p.pipe_write_fd, + false, /* not input */ + pipes.write, + ))); + } + + // Append IOs from the process's redirection specs. + // This may fail, e.g. a failed redirection. + if !process_net_io_chain + .append_from_specs(p.redirection_specs(), &parser.vars().get_pwd_slash()) + { + return Err(()); + } + + // Read pipe goes last. + if pipes.read.is_valid() { + let pipe_read = Arc::new(IoPipe::new(STDIN_FILENO, true /* input */, pipes.read)); + process_net_io_chain.push(pipe_read); + } + + // If we have stashed pipes, make sure those get closed in the child. + for afd in [&deferred_pipes.read, &deferred_pipes.write] { + if afd.is_valid() { + process_net_io_chain.push(Arc::new(IoClose::new(afd.fd()))); + } + } + + if p.typ != ProcessType::block_node { + // A simple `begin ... end` should not be considered an execution of a command. + parser.libdata_mut().pods.exec_count += 1; + } + + let mut block_id = None; + if !p.variable_assignments.is_empty() { + block_id = Some(parser.push_block(Block::variable_assignment_block())); + } + let _pop_block = ScopeGuard::new((), |()| { + if let Some(block_id) = block_id { + parser.pop_block(block_id); + } + }); + for assignment in &p.variable_assignments { + parser.vars().set( + &assignment.variable_name, + EnvMode::LOCAL | EnvMode::EXPORT, + assignment.values.clone(), + ); + } + + // Decide if outputting to a pipe may deadlock. + // This happens if fish pipes from an internal process into another internal process: + // echo $big | string match... + // Here fish will only run one process at a time, so the pipe buffer may overfill. + // It may also happen when piping internal -> external: + // echo $big | external_proc + // fish wants to run `echo` before launching external_proc, so the pipe may deadlock. + // However if we are a deferred run, it means that we are piping into an external process + // which got launched before us! + let piped_output_needs_buffering = !p.is_last_in_job && !is_deferred_run; + + // Execute the process. + p.check_generations_before_launch(); + match p.typ { + ProcessType::function | ProcessType::block_node => exec_block_or_func_process( + parser, + j, + p, + process_net_io_chain, + piped_output_needs_buffering, + ), + ProcessType::builtin => exec_builtin_process( + parser, + j, + p, + &process_net_io_chain, + piped_output_needs_buffering, + ), + ProcessType::external => { + exec_external_command(parser, j, p, &process_net_io_chain)?; + // It's possible (though unlikely) that this is a background process which recycled a + // pid from another, previous background process. Forget any such old process. + parser.mut_wait_handles().remove_by_pid(p.pid()); + Ok(()) + } + ProcessType::exec => { + // We should have handled exec up above. + panic!("process_type_t::exec process found in pipeline, where it should never be. Aborting."); + } + } +} + +// Do we have a fish internal process that pipes into a real process? If so, we are going to +// launch it last (if there's more than one, just the last one). That is to prevent buffering +// from blocking further processes. See #1396. +// Example: +// for i in (seq 1 5); sleep 1; echo $i; end | cat +// This should show the output as it comes, not buffer until the end. +// Any such process (only one per job) will be called the "deferred" process. +fn get_deferred_process(j: &Job) -> Option { + // Common case is no deferred proc. + if j.processes().len() <= 1 { + return None; + } + + // Skip execs, which can only appear at the front. + if j.processes()[0].typ == ProcessType::exec { + return None; + } + + // Find the last non-external process, and return it if it pipes into an extenal process. + for (i, p) in j.processes().iter().enumerate().rev() { + if p.typ != ProcessType::external { + return if p.is_last_in_job { None } else { Some(i) }; + } + } + None +} + +/// Given that we failed to execute process \p failed_proc in job \p job, mark that process and +/// every subsequent process in the pipeline as aborted before launch. +fn abort_pipeline_from(job: &Job, offset: usize) { + for p in job.processes().iter().skip(offset) { + p.mark_aborted_before_launch(); + } +} + +// Given that we are about to execute an exec() call, check if the parser is interactive and there +// are extant background jobs. If so, warn the user and do not exec(). +// \return true if we should allow exec, false to disallow it. +fn allow_exec_with_background_jobs(parser: &Parser) -> bool { + // If we're not interactive, we cannot warn. + if !parser.is_interactive() { + return true; + } + + // Construct the list of running background jobs. + let bgs = jobs_requiring_warning_on_exit(parser); + if bgs.is_empty() { + return true; + } + + // Compare run counts, so we only warn once. + let current_run_count = reader_run_count(); + let last_exec_run_count = &mut parser.libdata_mut().pods.last_exec_run_counter; + if unsafe { isatty(STDIN_FILENO) } != 0 && current_run_count - 1 != *last_exec_run_count { + print_exit_warning_for_jobs(&bgs); + *last_exec_run_count = current_run_count; + false + } else { + hup_jobs(&parser.jobs()); + true + } +} + +/// Populate \p lst with the output of \p buffer, perhaps splitting lines according to \p split. +fn populate_subshell_output(lst: &mut Vec, buffer: &SeparatedBuffer, split: bool) { + // Walk over all the elements. + for elem in buffer.elements() { + let data = &elem.contents; + if elem.is_explicitly_separated() { + // Just append this one. + lst.push(str2wcstring(data)); + continue; + } + + // Not explicitly separated. We have to split it explicitly. + assert!( + !elem.is_explicitly_separated(), + "should not be explicitly separated" + ); + if split { + let mut cursor = 0; + while cursor < data.len() { + // Look for the next separator. + let stop = data[cursor..].iter().position(|c| *c == b'\n'); + let hit_separator = stop.is_some(); + // If it's not found, just use the end. + let stop = stop.map(|rel| cursor + rel).unwrap_or(data.len()); + // Stop now points at the first character we do not want to copy. + lst.push(str2wcstring(&data[cursor..stop])); + + // If we hit a separator, skip over it; otherwise we're at the end. + cursor = stop + if hit_separator { 1 } else { 0 }; + } + } else { + // We're not splitting output, but we still want to trim off a trailing newline. + let trailing_newline = if data.last() == Some(&b'\n') { 1 } else { 0 }; + lst.push(str2wcstring(&data[..data.len() - trailing_newline])); + } + } +} + +/// Execute \p cmd in a subshell in \p parser. If \p lst is not null, populate it with the output. +/// Return $status in \p out_status. +/// If \p job_group is set, any spawned commands should join that job group. +/// If \p apply_exit_status is false, then reset $status back to its original value. +/// \p is_subcmd controls whether we apply a read limit. +/// \p break_expand is used to propagate whether the result should be "expansion breaking" in the +/// sense that subshells used during string expansion should halt that expansion. \return the value +/// of $status. +fn exec_subshell_internal( + cmd: &wstr, + parser: &Parser, + job_group: Option<&JobGroupRef>, + lst: Option<&mut Vec>, + break_expand: &mut bool, + apply_exit_status: bool, + is_subcmd: bool, +) -> libc::c_int { + parser.assert_can_execute(); + let _is_subshell = scoped_push_replacer( + |new_value| std::mem::replace(&mut parser.libdata_mut().pods.is_subshell, new_value), + true, + ); + let _read_limit = scoped_push_replacer( + |new_value| std::mem::replace(&mut parser.libdata_mut().pods.read_limit, new_value), + if is_subcmd { + READ_BYTE_LIMIT.load(Ordering::Relaxed) + } else { + 0 + }, + ); + + let prev_statuses = parser.get_last_statuses(); + let _put_back = ScopeGuard::new((), |()| { + if !apply_exit_status { + parser.set_last_statuses(prev_statuses); + } + }); + + let split_output = parser.vars().get_unless_empty(L!("IFS")).is_some(); + + // IO buffer creation may fail (e.g. if we have too many open files to make a pipe), so this may + // be null. + let Some(bufferfill) = + IoBufferfill::create_opts(parser.libdata().pods.read_limit, STDOUT_FILENO) + else { + *break_expand = true; + return STATUS_CMD_ERROR.unwrap(); + }; + + let mut io_chain = IoChain::new(); + io_chain.push(bufferfill.clone()); + let eval_res = parser.eval_with(cmd, &io_chain, job_group, BlockType::subst); + let buffer = IoBufferfill::finish(bufferfill); + if buffer.discarded() { + *break_expand = true; + return STATUS_READ_TOO_MUCH.unwrap(); + } + + if eval_res.break_expand { + *break_expand = true; + return eval_res.status.status_value(); + } + + if let Some(lst) = lst { + populate_subshell_output(lst, &buffer, split_output); + } + *break_expand = false; + eval_res.status.status_value() +} + +#[cxx::bridge] +mod exec_ffi { + extern "C++" { + include!("wutil.h"); + include!("parser.h"); + type wcstring_list_ffi_t = crate::ffi::wcstring_list_ffi_t; + type Parser = crate::parser::Parser; + } + extern "Rust" { + #[cxx_name = "exec_subshell"] + fn exec_subshell_ffi( + cmd: &CxxWString, + parser: &Parser, + outputs: &mut UniquePtr, + apply_exit_status: bool, + ) -> i32; + } +} + +fn exec_subshell_ffi( + cmd: &CxxWString, + parser: &Parser, + outputs: &mut UniquePtr, + apply_exit_status: bool, +) -> c_int { + let mut tmp = vec![]; + let ret = exec_subshell(cmd.as_wstr(), parser, Some(&mut tmp), apply_exit_status); + *outputs = tmp.to_ffi(); + ret +} diff --git a/fish-rust/src/expand.rs b/fish-rust/src/expand.rs index d1b80d678..db27ad23d 100644 --- a/fish-rust/src/expand.rs +++ b/fish-rust/src/expand.rs @@ -1,9 +1,34 @@ -use crate::common::{char_offset, EXPAND_RESERVED_BASE, EXPAND_RESERVED_END}; -use crate::env::Environment; +//! String expansion functions. These functions perform several kinds of parameter expansion. There +//! are a lot of issues with regards to memory allocation. Overall, these functions would benefit +//! from using a more clever memory allocation scheme, perhaps an evil combination of talloc, +//! string buffers and reference counting. + +use crate::builtins::shared::{ + STATUS_CMD_ERROR, STATUS_CMD_UNKNOWN, STATUS_EXPAND_ERROR, STATUS_ILLEGAL_CMD, + STATUS_INVALID_ARGS, STATUS_NOT_EXECUTABLE, STATUS_READ_TOO_MUCH, STATUS_UNMATCHED_WILDCARD, +}; +use crate::common::{ + char_offset, charptr2wcstring, escape, escape_string_for_double_quotes, unescape_string, + valid_var_name_char, wcs2zstring, UnescapeFlags, UnescapeStringStyle, EXPAND_RESERVED_BASE, + EXPAND_RESERVED_END, +}; +use crate::complete::{CompleteFlags, Completion, CompletionList, CompletionReceiver}; +use crate::env::{EnvStackRefFFI, EnvVar, Environment}; +use crate::exec::exec_subshell_for_expand; +use crate::history::{history_session_id, History}; use crate::operation_context::OperationContext; -use crate::parse_constants::ParseErrorList; +use crate::parse_constants::{ParseError, ParseErrorCode, ParseErrorList, SOURCE_LOCATION_UNKNOWN}; +use crate::parse_util::{parse_util_expand_variable_error, parse_util_locate_cmdsubst_range}; +use crate::path::path_apply_working_directory; +use crate::util::wcsfilecmp_glob; use crate::wchar::prelude::*; +use crate::wchar_ffi::{WCharFromFFI, WCharToFFI}; +use crate::wcstringutil::{join_strings, trim}; +use crate::wildcard::{wildcard_expand_string, wildcard_has_internal}; +use crate::wildcard::{WildcardResult, ANY_CHAR, ANY_STRING, ANY_STRING_RECURSIVE}; +use crate::wutil::{normalize_path, wcstoi_partial, Options}; use bitflags::bitflags; +use cxx::{CxxWString, UniquePtr}; bitflags! { /// Set of flags controlling expansions. @@ -74,23 +99,23 @@ pub struct ExpandFlags : u16 { "Characters used in expansions must stay within private use area" ); -#[derive(Copy, Clone, Eq, PartialEq)] -pub enum ExpandResultCode { - /// There was an error, for example, unmatched braces. - error, - /// Expansion succeeded. - ok, - /// Expansion was cancelled (e.g. control-C). - cancel, - /// Expansion succeeded, but a wildcard in the string matched no files, - /// so the output is empty. - wildcard_no_match, -} +pub use expand_ffi::{ExpandResult, ExpandResultCode}; -/// These are the possible return values for expand_string. -pub struct ExpandResult { - // todo! - pub result: ExpandResultCode, +impl ExpandResult { + pub fn new(result: ExpandResultCode) -> Self { + Self { result, status: 0 } + } + pub fn ok() -> Self { + Self::new(ExpandResultCode::ok) + } + /// Make an error value with the given status. + pub fn make_error(status: libc::c_int) -> Self { + assert!(status != 0, "status cannot be 0 for an error result"); + Self { + result: ExpandResultCode::error, + status, + } + } } impl PartialEq for ExpandResult { @@ -103,6 +128,49 @@ fn eq(&self, other: &ExpandResultCode) -> bool { #[widestrs] pub const PROCESS_EXPAND_SELF_STR: &wstr = "%self"L; +/// Perform various forms of expansion on in, such as tilde expansion (\~USER becomes the users home +/// directory), variable expansion (\$VAR_NAME becomes the value of the environment variable +/// VAR_NAME), cmdsubst expansion and wildcard expansion. The results are inserted into the list +/// out. +/// +/// If the parameter does not need expansion, it is copied into the list out. +/// +/// \param input The parameter to expand +/// \param output The list to which the result will be appended. +/// \param flags Specifies if any expansion pass should be skipped. Legal values are any combination +/// of skip_cmdsubst skip_variables and skip_wildcards +/// \param ctx The parser, variables, and cancellation checker for this operation. The parser may +/// be null. \param errors Resulting errors, or nullptr to ignore +/// +/// \return An expand_result_t. +/// wildcard_no_match and wildcard_match are normal exit conditions used only on +/// strings containing wildcards to tell if the wildcard produced any matches. +pub fn expand_string( + input: WString, + out_completions: &mut CompletionList, + flags: ExpandFlags, + ctx: &OperationContext, + errors: Option<&mut ParseErrorList>, +) -> ExpandResult { + let mut completions = vec![]; + std::mem::swap(&mut completions, out_completions); + let mut recv = CompletionReceiver::from_list(completions, ctx.expansion_limit); + let result = expand_to_receiver(input, &mut recv, flags, ctx, errors); + *out_completions = recv.take(); + result +} + +/// Variant of string that inserts its results into a completion_receiver_t. +pub fn expand_to_receiver( + input: WString, + out_completions: &mut CompletionReceiver, + flags: ExpandFlags, + ctx: &OperationContext, + errors: Option<&mut ParseErrorList>, +) -> ExpandResult { + Expander::expand_string(input, out_completions, flags, ctx, errors) +} + /// expand_one is identical to expand_string, except it will fail if in expands to more than one /// string. This is used for expanding command names. /// @@ -110,17 +178,32 @@ fn eq(&self, other: &ExpandResultCode) -> bool { /// \param flags Specifies if any expansion pass should be skipped. Legal values are any combination /// of skip_cmdsubst skip_variables and skip_wildcards /// \param ctx The parser, variables, and cancellation checker for this operation. The parser may be -/// null. \param errors Resulting errors, or nullptr to ignore +/// null. +/// \param errors Resulting errors, or nullptr to ignore /// /// \return Whether expansion succeeded. -#[allow(unused_variables)] pub fn expand_one( s: &mut WString, flags: ExpandFlags, ctx: &OperationContext, errors: Option<&mut ParseErrorList>, ) -> bool { - todo!() + let mut completions = CompletionList::new(); + + if !flags.contains(ExpandFlags::FOR_COMPLETIONS) && expand_is_clean(s) { + return true; + } + + let mut tmp = WString::new(); + std::mem::swap(s, &mut tmp); + if expand_string(tmp, &mut completions, flags, ctx, errors) == ExpandResultCode::ok + && completions.len() == 1 + { + std::mem::swap(s, &mut completions[0].completion); + return true; + } + + false } /// Expand a command string like $HOME/bin/cmd into a command and list of arguments. @@ -130,25 +213,1412 @@ pub fn expand_one( /// expansion resulting in no command (e.g. unset variable). /// If \p skip_wildcards is true, then do not do wildcard expansion /// \return an expand error. -#[allow(unused_variables)] pub fn expand_to_command_and_args( instr: &wstr, - ctx: &OperationContext, + ctx: &OperationContext<'_>, out_cmd: &mut WString, - out_args: Option<&Vec>, - errors: &mut ParseErrorList, + mut out_args: Option<&mut Vec>, + errors: Option<&mut ParseErrorList>, skip_wildcards: bool, ) -> ExpandResult { - todo!() + // Fast path. + if expand_is_clean(instr) { + *out_cmd = instr.to_owned(); + return ExpandResult::ok(); + } + + let mut eflags = ExpandFlags::SKIP_CMDSUBST; + if skip_wildcards { + eflags |= ExpandFlags::SKIP_WILDCARDS; + } + + let mut completions = CompletionList::new(); + let expand_err = expand_string(instr.to_owned(), &mut completions, eflags, ctx, errors); + if expand_err == ExpandResultCode::ok { + // The first completion is the command, any remaining are arguments. + let mut completions = completions.into_iter(); + if let Some(comp) = completions.next() { + *out_cmd = comp.completion; + } + if let Some(ref mut out_args) = out_args { + for comp in completions { + out_args.push(comp.completion); + } + } + } + + expand_err +} + +/// Convert the variable value to a human readable form, i.e. escape things, handle arrays, etc. +/// Suitable for pretty-printing. +pub fn expand_escape_variable(var: &EnvVar) -> WString { + let mut buff = WString::new(); + + let lst = var.as_list(); + for el in lst { + if !buff.is_empty() { + buff.push_str(" "); + } + + // We want to use quotes if we have more than one string, or the string contains a space. + let prefer_quotes = lst.len() > 1 || el.contains(' '); + if prefer_quotes && is_quotable(el) { + buff.push('\''); + buff.push_utfstr(el); + buff.push('\''); + } else { + buff.push_utfstr(&escape(el)); + } + } + buff +} + +/// Convert a string value to a human readable form, i.e. escape things, handle arrays, etc. +/// Suitable for pretty-printing. +pub fn expand_escape_string(el: &wstr) -> WString { + let mut buff = WString::new(); + let prefer_quotes = el.contains(' '); + if prefer_quotes && is_quotable(el) { + buff.push('\''); + buff.push_utfstr(el); + buff.push('\''); + } else { + buff.push_utfstr(&escape(el)); + } + buff } /// Perform tilde expansion and nothing else on the specified string, which is modified in place. /// /// \param input the string to tilde expand -pub fn expand_tilde(input: &mut WString, _vars: &dyn Environment) { +pub fn expand_tilde(input: &mut WString, vars: &dyn Environment) { if input.chars().next() == Some('~') { input.replace_range(0..1, wstr::from_char_slice(&[HOME_DIRECTORY])); - todo!(); - // expand_home_directory(input, vars); + expand_home_directory(input, vars); } } + +/// Perform the opposite of tilde expansion on the string, which is modified in place. +#[widestrs] +pub fn replace_home_directory_with_tilde(s: &wstr, vars: &dyn Environment) -> WString { + let mut result = s.to_owned(); + // Only absolute paths get this treatment. + if result.starts_with("/"L) { + let mut home_directory = "~"L.to_owned(); + expand_tilde(&mut home_directory, vars); + if !home_directory.ends_with("/"L) { + home_directory.push('/'); + } + + // Now check if the home_directory prefixes the string. + if result.starts_with(&home_directory) { + // Success + result.replace_range(0..home_directory.len(), "~/"L); + } + } + result +} + +/// Characters which make a string unclean if they are the first character of the string. See \c +/// expand_is_clean(). +const UNCLEAN_FIRST: &wstr = L!("~%"); +/// Unclean characters. See \c expand_is_clean(). +const UNCLEAN: &wstr = L!("$*?\\\"'({})"); + +/// Test if the specified argument is clean, i.e. it does not contain any tokens which need to be +/// expanded or otherwise altered. Clean strings can be passed through expand_string and expand_one +/// without changing them. About two thirds of all strings are clean, so skipping expansion on them +/// actually does save a small amount of time, since it avoids multiple memory allocations during +/// the expansion process. +/// +/// \param in the string to test +fn expand_is_clean(input: &wstr) -> bool { + if input.is_empty() { + return true; + } + + // Test characters that have a special meaning in the first character position. + if UNCLEAN_FIRST.contains(input.as_char_slice()[0]) { + return false; + } + + // Test characters that have a special meaning in any character position. + !input.chars().any(|c| UNCLEAN.contains(c)) +} + +/// Append a syntax error to the given error list. +macro_rules! append_syntax_error { + ( + $errors:expr, $source_start:expr, + $fmt:expr $(, $arg:expr )* $(,)? + ) => { + if let Some(ref mut errors) = $errors { + let mut error = ParseError::default(); + error.source_start = $source_start; + error.source_length = 0; + error.code = ParseErrorCode::syntax; + error.text = wgettext_fmt!($fmt $(, $arg)*); + errors.push(error); + } + } +} + +/// Append a cmdsub error to the given error list. But only do so if the error hasn't already been +/// recorded. This is needed because command substitution is a recursive process and some errors +/// could consequently be recorded more than once. +macro_rules! append_cmdsub_error { + ( + $errors:expr, $source_start:expr, $source_end:expr, + $fmt:expr $(, $arg:expr )* $(,)? + ) => { + append_cmdsub_error_formatted!( + $errors, $source_start, $source_end, + wgettext_fmt!($fmt $(, $arg)*)); + } +} + +macro_rules! append_cmdsub_error_formatted { + ( + $errors:expr, $source_start:expr, $source_end:expr, + $text:expr $(,)? + ) => { + if let Some(ref mut errors) = $errors { + let mut error = ParseError::default(); + error.source_start = $source_start; + error.source_length = $source_end - $source_start + 1; + error.code = ParseErrorCode::cmdsubst; + error.text = $text; + if !errors.iter().any(|e| e.text == error.text) { + errors.push(error); + } + } + }; +} + +/// Append an overflow error, when expansion produces too much data. +fn append_overflow_error( + errors: &mut Option<&mut ParseErrorList>, + source_start: Option, +) -> ExpandResult { + if let Some(ref mut errors) = errors { + let mut error = ParseError::default(); + error.source_start = source_start.unwrap_or(SOURCE_LOCATION_UNKNOWN); + error.source_length = 0; + error.code = ParseErrorCode::generic; + error.text = wgettext!("Expansion produced too many results").to_owned(); + errors.push(error); + } + ExpandResult::make_error(STATUS_EXPAND_ERROR.unwrap()) +} + +/// Test if the specified string does not contain character which can not be used inside a quoted +/// string. +fn is_quotable(s: &wstr) -> bool { + !s.chars().any(|c| "\n\t\r\x08\x1B".contains(c)) +} + +enum ParseSliceError { + none, + zero_index, + invalid_index, +} + +/// Parse an array slicing specification Returns 0 on success. If a parse error occurs, returns the +/// index of the bad token. Note that 0 can never be a bad index because the string always starts +/// with [. +fn parse_slice( + input: &wstr, + idx: &mut Vec, + array_size: usize, +) -> Result { + let size = i64::try_from(array_size).unwrap(); + let mut pos = 1; // skip past the opening square bracket + + loop { + while input.char_at(pos).is_whitespace() || input.char_at(pos) == INTERNAL_SEPARATOR { + pos += 1; + } + if input.char_at(pos) == ']' { + pos += 1; + break; + } + + let tmp = if idx.is_empty() && input.char_at(pos) == '.' && input.char_at(pos + 1) == '.' { + // If we are at the first index expression, a missing start-index means the range starts + // at the first item. + 1 // first index + } else { + let mut consumed = 0; + match wcstoi_partial(&input[pos..], Options::default(), &mut consumed) { + Ok(tmp) => { + if tmp == 0 { + // Explicitly refuse $foo[0] as valid syntax, regardless of whether or + // not we're going to show an error if the index ultimately evaluates + // to zero. This will help newcomers to fish avoid a common off-by-one + // error. See #4862. + return Err((pos, ParseSliceError::zero_index)); + } + pos += consumed; + // Skip trailing whitespace. + pos += input[pos..] + .chars() + .take_while(|c| c.is_whitespace()) + .count(); + tmp + } + Err(_error) => { + // We don't test `*end` as is typically done because we expect it to not + // be the null char. Ignore the case of errno==-1 because it means the end + // char wasn't the null char. + return Err((pos, ParseSliceError::invalid_index)); + } + } + }; + + let mut i1 = if tmp > -1 { tmp } else { size + tmp + 1 }; + while input.char_at(pos) == INTERNAL_SEPARATOR { + pos += 1; + } + if input.char_at(pos) == '.' && input.char_at(pos + 1) == '.' { + pos += 2; + while input.char_at(pos) == INTERNAL_SEPARATOR { + pos += 1; + } + while input.char_at(pos).is_whitespace() { + pos += 1; // Allow the space in "[.. ]". + } + + // If we are at the last index range expression then a missing end-index means the + // range spans until the last item. + let tmp1 = if input.char_at(pos) == ']' { + -1 // last index + } else { + let mut consumed = 0; + match wcstoi_partial(&input[pos..], Options::default(), &mut consumed) { + Ok(tmp) => { + if tmp == 0 { + return Err((pos, ParseSliceError::zero_index)); + } + pos += consumed; + // Skip trailing whitespace. + pos += input[pos..] + .chars() + .take_while(|c| c.is_whitespace()) + .count(); + tmp + } + Err(_error) => { + return Err((pos, ParseSliceError::invalid_index)); + } + } + }; + + let mut i2 = if tmp1 > -1 { tmp1 } else { size + tmp1 + 1 }; + // Skip sequences that are entirely outside. + // This means "17..18" expands to nothing if there are less than 17 elements. + if i1 > size && i2 > size { + continue; + } + let mut direction = if i2 < i1 { -1 } else { 1 }; + // If only the beginning is negative, always go reverse. + // If only the end, always go forward. + // Prevents `[x..-1]` from going reverse if less than x elements are there. + if (tmp1 > -1) != (tmp > -1) { + direction = if tmp1 > -1 { -1 } else { 1 }; + } else { + // Clamp to array size when not forcing direction + // - otherwise "2..-1" clamps both to 1 and then becomes "1..1". + i1 = i1.min(size); + i2 = i2.min(size); + } + let mut jjj = i1; + while jjj * direction <= i2 * direction { + idx.push(jjj); + jjj += direction; + } + continue; + } + + idx.push(i1); + } + + Ok(pos) +} + +/// Expand all environment variables in the string *ptr. +/// +/// This function is slow, fragile and complicated. There are lots of little corner cases, like +/// $$foo should do a double expansion, $foo$bar should not double expand bar, etc. +/// +/// This function operates on strings backwards, starting at last_idx. +/// +/// Note: last_idx is considered to be where it previously finished processing. This means it +/// actually starts operating on last_idx-1. As such, to process a string fully, pass string.size() +/// as last_idx instead of string.size()-1. +/// +/// \return the result of expansion. +fn expand_variables( + instr: WString, + out: &mut CompletionReceiver, + last_idx: usize, + vars: &dyn Environment, + errors: &mut Option<&mut ParseErrorList>, +) -> ExpandResult { + // last_idx may be 1 past the end of the string, but no further. + assert!(last_idx <= instr.len(), "Invalid last_idx"); + if last_idx == 0 { + if !out.add(instr) { + return append_overflow_error(errors, None); + } + return ExpandResult::ok(); + } + + // Locate the last VARIABLE_EXPAND or VARIABLE_EXPAND_SINGLE + let mut is_single = false; + let mut varexp_char_idx = last_idx; + loop { + let done = varexp_char_idx == 0; + varexp_char_idx = varexp_char_idx.wrapping_sub(1); + if done { + break; + } + let c = instr.as_char_slice()[varexp_char_idx]; + if [VARIABLE_EXPAND, VARIABLE_EXPAND_SINGLE].contains(&c) { + is_single = c == VARIABLE_EXPAND_SINGLE; + break; + } + } + if varexp_char_idx == usize::MAX { + // No variable expand char, we're done. + if !out.add(instr) { + return append_overflow_error(errors, None); + } + return ExpandResult::ok(); + } + + // Get the variable name. + let var_name_start = varexp_char_idx + 1; + let mut var_name_stop = var_name_start; + while var_name_stop < instr.len() { + let nc = instr.as_char_slice()[var_name_stop]; + if nc == VARIABLE_EXPAND_EMPTY { + var_name_stop += 1; + break; + } + if !valid_var_name_char(nc) { + break; + } + var_name_stop += 1; + } + assert!( + var_name_stop >= var_name_start, + "Bogus variable name indexes" + ); + + // Get the variable name as a string, then try to get the variable from env. + let var_name = &instr[var_name_start..var_name_stop]; + + // It's an error if the name is empty. + if var_name.is_empty() { + if let Some(ref mut errors) = errors { + parse_util_expand_variable_error( + &instr, + 0, /* global_token_pos */ + varexp_char_idx, + errors, + ); + } + return ExpandResult::make_error(STATUS_EXPAND_ERROR.unwrap()); + } + + // Do a dirty hack to make sliced history fast (#4650). We expand from either a variable, or a + // history_t. Note that "history" is read only in env.cpp so it's safe to special-case it in + // this way (it cannot be shadowed, etc). + let mut history = None; + let mut var = None; + if var_name == L!("history") { + history = Some(History::with_name(&history_session_id(vars))); + } else if var_name.as_char_slice() != [VARIABLE_EXPAND_EMPTY] { + var = vars.get(var_name); + } + + // Parse out any following slice. + // Record the end of the variable name and any following slice. + let mut var_name_and_slice_stop = var_name_stop; + let mut all_values = true; + let slice_start = var_name_stop; + let mut var_idx_list = vec![]; + + if instr.as_char_slice().get(slice_start) == Some(&'[') { + all_values = false; + // If a variable is missing, behave as though we have one value, so that $var[1] always + // works. + let mut effective_val_count = 1; + if let Some(ref var) = var { + effective_val_count = var.as_list().len(); + } else if let Some(ref history) = history { + effective_val_count = history.size(); + } + match parse_slice( + &instr[slice_start..], + &mut var_idx_list, + effective_val_count, + ) { + Ok(offset) => { + var_name_and_slice_stop = slice_start + offset; + } + Err((bad_pos, error)) => { + match error { + ParseSliceError::none => { + panic!("bad_pos != 0 but parse_slice_error_t::none!"); + } + ParseSliceError::zero_index => { + append_syntax_error!( + errors, + slice_start + bad_pos, + "array indices start at 1, not 0." + ); + } + ParseSliceError::invalid_index => { + append_syntax_error!(errors, slice_start + bad_pos, "Invalid index value"); + } + } + return ExpandResult::make_error(STATUS_EXPAND_ERROR.unwrap()); + } + } + } + let var_idx_list = var_idx_list.iter().filter_map(|&n| n.try_into().ok()); + + if var.is_none() && history.is_none() { + // Expanding a non-existent variable. + if !is_single { + // Normal expansions of missing variables successfully expand to nothing. + return ExpandResult::ok(); + } else { + // Expansion to single argument. + // Replace the variable name and slice with VARIABLE_EXPAND_EMPTY. + let mut res = instr[..varexp_char_idx].to_owned(); + if res.as_char_slice().last() == Some(&VARIABLE_EXPAND_SINGLE) { + res.push(VARIABLE_EXPAND_EMPTY); + } + res.push_utfstr(&instr[var_name_and_slice_stop..]); + return expand_variables(res, out, varexp_char_idx, vars, errors); + } + } + + // Ok, we have a variable or a history. Let's expand it. + // Start by respecting the sliced elements. + assert!( + var.is_some() || history.is_some(), + "Should have variable or history here", + ); + let mut var_item_list = vec![]; + if all_values { + var_item_list = if let Some(ref history) = history { + history.get_history() + } else { + var.as_ref().unwrap().as_list().to_vec() + }; + } else { + // We have to respect the slice. + if let Some(ref history) = history { + // Ask history to map indexes to item strings. + // Note this may have missing entries for out-of-bounds. + let item_map = history.items_at_indexes(var_idx_list.clone()); + for item_index in var_idx_list { + if let Some(item) = item_map.get(&item_index) { + var_item_list.push(item.clone()); + } + } + } else { + let all_var_items = var.as_ref().unwrap().as_list(); + for item_index in var_idx_list { + // Check that we are within array bounds. If not, skip the element. Note: + // Negative indices (`echo $foo[-1]`) are already converted to positive ones + // here, So tmp < 1 means it's definitely not in. + // Note we are 1-based. + if item_index >= 1 && item_index <= all_var_items.len() { + var_item_list.push(all_var_items[item_index - 1].to_owned()); + } + } + } + } + + if is_single { + // Quoted expansion. Here we expect the variable's delimiter. + // Note history always has a space delimiter. + let delimit = if history.is_some() { + ' ' + } else { + var.as_ref().unwrap().get_delimiter() + }; + let mut res = instr[..varexp_char_idx].to_owned(); + if !res.is_empty() { + if res.as_char_slice().last() != Some(&VARIABLE_EXPAND_SINGLE) { + res.push(INTERNAL_SEPARATOR); + } else if var_item_list.is_empty() || var_item_list[0].is_empty() { + // First expansion is empty, but we need to recursively expand. + res.push(VARIABLE_EXPAND_EMPTY); + } + } + + // Append all entries in var_item_list, separated by the delimiter. + res.push_utfstr(&join_strings(&var_item_list, delimit)); + res.push_utfstr(&instr[var_name_and_slice_stop..]); + return expand_variables(res, out, varexp_char_idx, vars, errors); + } else { + // Normal cartesian-product expansion. + for item in var_item_list { + if varexp_char_idx == 0 && var_name_and_slice_stop == instr.len() { + if !out.add(item) { + return append_overflow_error(errors, None); + } + } else { + let mut new_in = instr[..varexp_char_idx].to_owned(); + if !new_in.is_empty() { + if new_in.as_char_slice().last() != Some(&VARIABLE_EXPAND) { + new_in.push(INTERNAL_SEPARATOR); + } else if item.is_empty() { + new_in.push(VARIABLE_EXPAND_EMPTY); + } + } + new_in.push_utfstr(&item); + new_in.push_utfstr(&instr[var_name_and_slice_stop..]); + let res = expand_variables(new_in, out, varexp_char_idx, vars, errors); + if res.result != ExpandResultCode::ok { + return res; + } + } + } + } + + ExpandResult::ok() +} + +/// Perform brace expansion, placing the expanded strings into \p out. +fn expand_braces( + input: WString, + flags: ExpandFlags, + out: &mut CompletionReceiver, + errors: &mut Option<&mut ParseErrorList>, +) -> ExpandResult { + let mut syntax_error = false; + let mut brace_count = 0; + + let mut brace_begin = None; + let mut brace_end = None; + let mut last_sep = None; + + // Locate the first non-nested brace pair. + for (pos, c) in input.chars().enumerate() { + match c { + BRACE_BEGIN => { + if brace_count == 0 { + brace_begin = Some(pos); + } + brace_count += 1; + } + BRACE_END => { + brace_count -= 1; + #[allow(clippy::comparison_chain)] + if brace_count < 0 { + syntax_error = true; + } else if brace_count == 0 { + brace_end = Some(pos); + } + } + BRACE_SEP => { + if brace_count == 1 { + last_sep = Some(pos); + } + } + _ => { + // we ignore all other characters here + } + } + } + + if brace_count > 0 { + if !flags.contains(ExpandFlags::FOR_COMPLETIONS) { + syntax_error = true; + } else { + // The user hasn't typed an end brace yet; make one up and append it, then expand + // that. + let mut synth = WString::new(); + if let Some(last_sep) = last_sep { + synth.push_utfstr(&input[..brace_begin.unwrap() + 1]); + synth.push_utfstr(&input[last_sep + 1..]); + synth.push(BRACE_END); + } else { + synth.push_utfstr(&input); + synth.push(BRACE_END); + } + + // Note: this code looks very fishy, apparently it has never worked. + return expand_braces(synth, ExpandFlags::SKIP_CMDSUBST, out, errors); + } + } + + if syntax_error { + append_syntax_error!(errors, SOURCE_LOCATION_UNKNOWN, "Mismatched braces"); + return ExpandResult::make_error(STATUS_EXPAND_ERROR.unwrap()); + } + + let Some(brace_begin) = brace_begin else { + // No more brace expansions left; we can return the value as-is. + if !out.add(input) { + return append_overflow_error(errors, None); + } + return ExpandResult::ok(); + }; + let brace_end = brace_end.unwrap(); + + let length_preceding_braces = brace_begin; + let length_following_braces = input.len() - brace_end - 1; + let tot_len = length_preceding_braces + length_following_braces; + let mut item_begin = brace_begin + 1; + for (pos, c) in input.chars().enumerate().skip(brace_begin + 1) { + if brace_count == 0 && (c == BRACE_SEP || pos == brace_end) { + assert!(pos >= item_begin); + let item_len = pos - item_begin; + let item = input[item_begin..pos].to_owned(); + let mut item = trim(item, Some(wstr::from_char_slice(&[BRACE_SPACE, '\0']))); + for c in item.as_char_slice_mut() { + if *c == BRACE_SPACE { + *c = ' '; + } + } + + // `whole_item` is a whitespace- and brace-stripped member of a single pass of brace + // expansion, e.g. in `{ alpha , b,{c, d }}`, `alpha`, `b`, and `c, d` will, in the + // first round of expansion, each in turn be a `whole_item` (with recursive commas + // replaced by special placeholders). + // We recursively call `expand_braces` with each item until it's been fully expanded. + let mut whole_item = WString::new(); + whole_item.reserve(tot_len + item_len + 2); + whole_item.push_utfstr(&input[..length_preceding_braces]); + whole_item.push_utfstr(&item); + whole_item.push_utfstr(&input[brace_end + 1..]); + let _ = expand_braces(whole_item, flags, out, errors); + + item_begin = pos + 1; + if pos == brace_end { + break; + } + } + + if c == BRACE_BEGIN { + brace_count += 1; + } + + if c == BRACE_END { + brace_count -= 1; + } + } + + ExpandResult::ok() +} + +/// Expand a command substitution \p input, executing on \p ctx, and inserting the results into +/// \p out_list, or any errors into \p errors. \return an expand result. +pub fn expand_cmdsubst( + input: WString, + ctx: &OperationContext, + out: &mut CompletionReceiver, + errors: &mut Option<&mut ParseErrorList>, +) -> ExpandResult { + assert!(ctx.has_parser(), "Cannot expand without a parser"); + let mut cursor = 0; + let mut paren_begin = 0; + let mut paren_end = 0; + let mut subcmd = L!(""); + + let mut is_quoted = false; + let mut has_dollar = false; + match parse_util_locate_cmdsubst_range( + &input, + &mut cursor, + Some(&mut subcmd), + &mut paren_begin, + &mut paren_end, + false, + Some(&mut is_quoted), + Some(&mut has_dollar), + ) { + -1 => { + append_syntax_error!(errors, SOURCE_LOCATION_UNKNOWN, "Mismatched parenthesis"); + return ExpandResult::make_error(STATUS_EXPAND_ERROR.unwrap()); + } + 0 => { + if !out.add(input) { + return append_overflow_error(errors, None); + } + return ExpandResult::ok(); + } + 1 => {} + _ => panic!(), + } + + let mut sub_res = vec![]; + let job_group = ctx.job_group.clone(); + let subshell_status = + exec_subshell_for_expand(subcmd, ctx.parser(), job_group.as_ref(), &mut sub_res); + if subshell_status != 0 { + // TODO: Ad-hoc switch, how can we enumerate the possible errors more safely? + let err = match subshell_status { + _ if subshell_status == STATUS_READ_TOO_MUCH.unwrap() => { + wgettext!("Too much data emitted by command substitution so it was discarded") + } + // TODO: STATUS_CMD_ERROR is overused and too generic. We shouldn't have to test things + // to figure out what error to show after we've already been given an error code. + _ if subshell_status == STATUS_CMD_ERROR.unwrap() => { + if ctx.parser().is_eval_depth_exceeded() { + wgettext!("Unable to evaluate string substitution") + } else { + wgettext!("Too many active file descriptors") + } + } + _ if subshell_status == STATUS_CMD_UNKNOWN.unwrap() => { + wgettext!("Unknown command") + } + _ if subshell_status == STATUS_ILLEGAL_CMD.unwrap() => { + wgettext!("Commandname was invalid") + } + _ if subshell_status == STATUS_NOT_EXECUTABLE.unwrap() => { + wgettext!("Command not executable") + } + _ if subshell_status == STATUS_INVALID_ARGS.unwrap() => { + // TODO: Also overused + // This is sent for: + // invalid redirections or pipes (like `<&foo`), + // invalid variables (invalid name or read-only) for for-loops, + // switch $foo if $foo expands to more than one argument + // time in a background job. + wgettext!("Invalid arguments") + } + _ if subshell_status == STATUS_EXPAND_ERROR.unwrap() => { + // Sent in `for $foo in ...` if $foo expands to more than one word + wgettext!("Expansion error") + } + _ if subshell_status == STATUS_UNMATCHED_WILDCARD.unwrap() => { + // Sent in `for $foo in ...` if $foo expands to more than one word + wgettext!("Unmatched wildcard") + } + _ => { + wgettext!("Unknown error while evaluating command substitution") + } + }; + append_cmdsub_error_formatted!(errors, paren_begin, paren_end, err.to_owned()); + return ExpandResult::make_error(subshell_status); + } + + // Expand slices like (cat /var/words)[1] + let mut tail_begin = paren_end + 1; + if input.as_char_slice().get(tail_begin) == Some(&'[') { + let mut slice_idx = vec![]; + let slice_begin = tail_begin; + let slice_end = match parse_slice(&input[slice_begin..], &mut slice_idx, sub_res.len()) { + Ok(offset) => slice_begin + offset, + Err((bad_pos, error)) => { + match error { + ParseSliceError::none => { + panic!("bad_pos != 0 but parse_slice_error_t::none!"); + } + ParseSliceError::zero_index => { + append_syntax_error!( + errors, + slice_begin + bad_pos, + "array indices start at 1, not 0." + ); + } + ParseSliceError::invalid_index => { + append_syntax_error!(errors, slice_begin + bad_pos, "Invalid index value"); + } + } + return ExpandResult::make_error(STATUS_EXPAND_ERROR.unwrap()); + } + }; + + let mut sub_res2 = vec![]; + tail_begin = slice_end; + for idx in slice_idx { + if idx as usize > sub_res.len() || idx < 1 { + continue; + } + // -1 to convert from 1-based slice index to 0-based vector index. + sub_res2.push(sub_res[idx as usize - 1].to_owned()); + } + sub_res = sub_res2; + } + + // Recursively call ourselves to expand any remaining command substitutions. The result of this + // recursive call using the tail of the string is inserted into the tail_expand array list + let mut tail_expand_recv = out.subreceiver(); + let mut tail = input[tail_begin..].to_owned(); + // A command substitution inside double quotes magically closes the quoted string. + // Reopen the quotes just after the command substitution. + if is_quoted { + tail.insert(0, '"'); + } + + let _ = expand_cmdsubst(tail, ctx, &mut tail_expand_recv, errors); // TODO: offset error locations + let tail_expand = tail_expand_recv.take(); + + // Combine the result of the current command substitution with the result of the recursive tail + // expansion. + + if is_quoted { + // Awkwardly reconstruct the command output. + let approx_size = sub_res.iter().map(|sub_item| sub_item.len() + 1).sum(); + let mut sub_res_joined = WString::new(); + sub_res_joined.reserve(approx_size); + for line in sub_res { + sub_res_joined.push_utfstr(&escape_string_for_double_quotes(&line)); + sub_res_joined.push('\n'); + } + // Mimic POSIX shells by stripping all trailing newlines. + if !sub_res_joined.is_empty() { + let mut i = sub_res_joined.len(); + while i > 0 && sub_res_joined.as_char_slice()[i - 1] == '\n' { + i -= 1; + } + sub_res_joined.truncate(i); + } + // Instead of performing cartesian product expansion, we directly insert the command + // substitution output into the current expansion results. + for tail_item in tail_expand { + let mut whole_item = WString::new(); + whole_item + .reserve(paren_begin + 1 + sub_res_joined.len() + 1 + tail_item.completion.len()); + whole_item.push_utfstr(&input[..paren_begin - if has_dollar { 1 } else { 0 }]); + whole_item.push(INTERNAL_SEPARATOR); + whole_item.push_utfstr(&sub_res_joined); + whole_item.push(INTERNAL_SEPARATOR); + whole_item.push_utfstr(&tail_item.completion["\"".len()..]); + if !out.add(whole_item) { + return append_overflow_error(errors, None); + } + } + + return ExpandResult::ok(); + } + + for sub_item in sub_res { + let sub_item2 = escape(&sub_item); + for tail_item in &*tail_expand { + let mut whole_item = WString::new(); + whole_item.reserve(paren_begin + 1 + sub_item2.len() + 1 + tail_item.completion.len()); + whole_item.push_utfstr(&input[..paren_begin - if has_dollar { 1 } else { 0 }]); + whole_item.push(INTERNAL_SEPARATOR); + whole_item.push_utfstr(&sub_item2); + whole_item.push(INTERNAL_SEPARATOR); + whole_item.push_utfstr(&tail_item.completion); + if !out.add(whole_item) { + return append_overflow_error(errors, None); + } + } + } + + ExpandResult::ok() +} + +// Given that input[0] is HOME_DIRECTORY or tilde (ugh), return the user's name. Return the empty +// string if it is just a tilde. Also return by reference the index of the first character of the +// remaining part of the string (e.g. the subsequent slash). +fn get_home_directory_name<'a>(input: &'a wstr, out_tail_idx: &mut usize) -> &'a wstr { + assert!([HOME_DIRECTORY, '~'].contains(&input.as_char_slice()[0])); + // We get the position of the /, but we need to remove it as well. + if let Some(pos) = input.chars().position(|c| c == '/') { + *out_tail_idx = pos; + &input[1..pos] + } else { + *out_tail_idx = input.len(); + &input[1..] + } +} + +/// Attempts tilde expansion of the string specified, modifying it in place. +fn expand_home_directory(input: &mut WString, vars: &dyn Environment) { + if input.as_char_slice().first() != Some(&HOME_DIRECTORY) { + return; + } + + let mut tail_idx = usize::MAX; + let username = get_home_directory_name(input, &mut tail_idx); + let mut home = None; + if username.is_empty() { + // Current users home directory. + match vars.get_unless_empty(L!("HOME")) { + None => { + input.clear(); + return; + } + Some(home_var) => { + home = Some(home_var.as_string()); + tail_idx = 1; + } + }; + } else { + // Some other user's home directory. + let name_cstr = wcs2zstring(username); + let mut userinfo: libc::passwd = unsafe { std::mem::zeroed() }; + let mut result: *mut libc::passwd = std::ptr::null_mut(); + let mut buf = [0 as libc::c_char; 8192]; + let retval = unsafe { + libc::getpwnam_r( + name_cstr.as_ptr(), + &mut userinfo, + &mut buf[0], + std::mem::size_of_val(&buf), + &mut result, + ) + }; + if retval == 0 && !result.is_null() { + home = Some(charptr2wcstring(userinfo.pw_dir)); + } + } + + if let Some(home) = home { + input.replace_range(..tail_idx, &normalize_path(&home, true)); + } else { + input.replace_range(0..1, L!("~")); + } +} + +/// Expand the %self escape. Note this can only come at the beginning of the string. +fn expand_percent_self(input: &mut WString) { + if input.as_char_slice().first() == Some(&PROCESS_EXPAND_SELF) { + input.replace_range(0..1, &unsafe { libc::getpid() }.to_wstring()); + } +} + +/// Remove any internal separators. Also optionally convert wildcard characters to regular +/// equivalents. This is done to support skip_wildcards. +fn remove_internal_separator(s: &mut WString, conv: bool) { + // Remove all instances of INTERNAL_SEPARATOR. + s.retain(|c| c != INTERNAL_SEPARATOR); + + // If conv is true, replace all instances of ANY_STRING with '*', + // ANY_STRING_RECURSIVE with '*'. + if conv { + for idx in s.as_char_slice_mut() { + match *idx { + ANY_CHAR => { + *idx = '?'; + } + ANY_STRING | ANY_STRING_RECURSIVE => { + *idx = '*'; + } + _ => { + // we ignore all other characters + } + } + } + } +} + +/// A type that knows how to perform expansions. +struct Expander<'a, 'b, 'c> { + /// Operation context for this expansion. + ctx: &'c OperationContext<'b>, + + /// Flags to use during expansion. + flags: ExpandFlags, + + /// List to receive any errors generated during expansion, or null to ignore errors. + errors: &'c mut Option<&'a mut ParseErrorList>, +} + +impl<'a, 'b, 'c> Expander<'a, 'b, 'c> { + fn new( + ctx: &'c OperationContext<'b>, + flags: ExpandFlags, + errors: &'c mut Option<&'a mut ParseErrorList>, + ) -> Self { + Self { ctx, flags, errors } + } + + fn expand_string( + input: WString, + out_completions: &'a mut CompletionReceiver, + flags: ExpandFlags, + ctx: &'a OperationContext<'b>, + mut errors: Option<&'a mut ParseErrorList>, + ) -> ExpandResult { + assert!( + flags.contains(ExpandFlags::SKIP_CMDSUBST) || ctx.has_parser(), + "Must have a parser if not skipping command substitutions" + ); + // Early out. If we're not completing, and there's no magic in the input, we're done. + if !flags.contains(ExpandFlags::FOR_COMPLETIONS) && expand_is_clean(&input) { + if !out_completions.add(input) { + return append_overflow_error(&mut errors, None); + } + return ExpandResult::ok(); + } + + let mut expand = Expander::new(ctx, flags, &mut errors); + + // Our expansion stages. + // An expansion stage is a member function pointer. + // It accepts the input string (transferring ownership) and returns the list of output + // completions by reference. It may return an error, which halts expansion. + let stages = [ + Expander::stage_cmdsubst, + Expander::stage_variables, + Expander::stage_braces, + Expander::stage_home_and_self, + Expander::stage_wildcards, + ]; + + // Load up our single initial completion. + let mut completions = vec![Completion::from_completion(input.clone())]; + + let mut total_result = ExpandResult::ok(); + let mut output_storage = out_completions.subreceiver(); + for stage in stages { + for comp in completions { + if expand.ctx.check_cancel() { + total_result = ExpandResult::new(ExpandResultCode::cancel); + break; + } + let this_result = (stage)(&mut expand, comp.completion, &mut output_storage); + total_result = this_result; + if total_result == ExpandResultCode::error { + break; + } + } + + // Output becomes our next stage's input. + completions = output_storage.take(); + if total_result == ExpandResultCode::error { + break; + } + } + + // This is a little tricky: if one wildcard failed to match but we still got output, it + // means that a previous expansion resulted in multiple strings. For example: + // set dirs ./a ./b + // echo $dirs/*.txt + // Here if ./a/*.txt matches and ./b/*.txt does not, then we don't want to report a failed + // wildcard. So swallow failed-wildcard errors if we got any output. + if total_result == ExpandResultCode::wildcard_no_match && !completions.is_empty() { + total_result = ExpandResult::ok(); + } + + if total_result == ExpandResultCode::ok { + // Unexpand tildes if we want to preserve them (see #647). + if flags.contains(ExpandFlags::PRESERVE_HOME_TILDES) { + expand.unexpand_tildes(&input, &mut completions); + } + if !out_completions.extend(completions) { + total_result = append_overflow_error(expand.errors, None); + } + } + + total_result + } + + fn stage_cmdsubst(&mut self, input: WString, out: &mut CompletionReceiver) -> ExpandResult { + if self.flags.contains(ExpandFlags::SKIP_CMDSUBST) { + let mut cursor = 0; + let mut start = 0; + let mut end = 0; + match parse_util_locate_cmdsubst_range( + &input, + &mut cursor, + None, + &mut start, + &mut end, + true, + None, + None, + ) { + 0 => { + if !out.add(input) { + return append_overflow_error(self.errors, None); + } + return ExpandResult::ok(); + } + cmdsub => { + if cmdsub == 1 { + append_cmdsub_error!( + self.errors, + start, + end, + "command substitutions not allowed here" + ); + } + return ExpandResult::make_error(STATUS_EXPAND_ERROR.unwrap()); + } + } + } else { + assert!( + self.ctx.has_parser(), + "Must have a parser to expand command substitutions" + ); + expand_cmdsubst(input, self.ctx, out, self.errors) + } + } + + // We pass by value to match other stages. NOLINTNEXTLINE(performance-unnecessary-value-param) + fn stage_variables(&mut self, input: WString, out: &mut CompletionReceiver) -> ExpandResult { + // We accept incomplete strings here, since complete uses expand_string to expand incomplete + // strings from the commandline. + let mut next = unescape_string( + &input, + UnescapeStringStyle::Script(UnescapeFlags::SPECIAL | UnescapeFlags::INCOMPLETE), + ) + .unwrap_or_default(); + + if self.flags.contains(ExpandFlags::SKIP_VARIABLES) { + for i in next.as_char_slice_mut() { + if [VARIABLE_EXPAND, VARIABLE_EXPAND_SINGLE].contains(i) { + *i = '$'; + } + } + if !out.add(next) { + return append_overflow_error(self.errors, None); + } + ExpandResult::ok() + } else { + let size = next.len(); + expand_variables(next, out, size, self.ctx.vars(), self.errors) + } + } + + fn stage_braces(&mut self, input: WString, out: &mut CompletionReceiver) -> ExpandResult { + expand_braces(input, self.flags, out, self.errors) + } + + fn stage_home_and_self( + &mut self, + mut input: WString, + out: &mut CompletionReceiver, + ) -> ExpandResult { + expand_home_directory(&mut input, self.ctx.vars()); + expand_percent_self(&mut input); + if !out.add(input) { + return append_overflow_error(self.errors, None); + } + ExpandResult::ok() + } + + fn stage_wildcards( + &mut self, + mut path_to_expand: WString, + out: &mut CompletionReceiver, + ) -> ExpandResult { + let mut result = ExpandResult::ok(); + + remove_internal_separator( + &mut path_to_expand, + self.flags.contains(ExpandFlags::SKIP_WILDCARDS), + ); + let has_wildcard = wildcard_has_internal(&path_to_expand); // e.g. ANY_STRING + let for_completions = self.flags.contains(ExpandFlags::FOR_COMPLETIONS); + let skip_wildcards = self.flags.contains(ExpandFlags::SKIP_WILDCARDS); + + if has_wildcard && self.flags.contains(ExpandFlags::EXECUTABLES_ONLY) { + // don't do wildcard expansion for executables, see issue #785 + } else if (for_completions && !skip_wildcards) || has_wildcard { + // We either have a wildcard, or we don't have a wildcard but we're doing completion + // expansion (so we want to get the completion of a file path). Note that if + // skip_wildcards is set, we stomped wildcards in remove_internal_separator above, so + // there actually aren't any. + // + // So we're going to treat this input as a file path. Compute the "working directories", + // which may be CDPATH if the special flag is set. + let working_dir = self.ctx.vars().get_pwd_slash(); + let mut effective_working_dirs = vec![]; + let for_cd = self.flags.contains(ExpandFlags::SPECIAL_FOR_CD); + let for_command = self.flags.contains(ExpandFlags::SPECIAL_FOR_COMMAND); + if !for_cd && !for_command { + // Common case. + effective_working_dirs.push(working_dir); + } else { + // Either special_for_command or special_for_cd. We can handle these + // mostly the same. There's the following differences: + // + // 1. An empty CDPATH should be treated as '.', but an empty PATH should be left empty + // (no commands can be found). Also, an empty element in either is treated as '.' for + // consistency with POSIX shells. Note that we rely on the latter by having called + // `munge_colon_delimited_array()` for these special env vars. Thus we do not + // special-case them here. + // + // 2. PATH is only "one level," while CDPATH is multiple levels. That is, input like + // 'foo/bar' should resolve against CDPATH, but not PATH. + // + // In either case, we ignore the path if we start with ./ or /. Also ignore it if we are + // doing command completion and we contain a slash, per IEEE 1003.1, chapter 8 under + // PATH. + if path_to_expand.starts_with(L!("/")) + || path_to_expand.starts_with(L!("./")) + || path_to_expand.starts_with(L!("../")) + || (for_command && path_to_expand.contains('/')) + { + effective_working_dirs.push(working_dir); + } else { + // Get the PATH/CDPATH and CWD. Perhaps these should be passed in. An empty CDPATH + // implies just the current directory, while an empty PATH is left empty. + let mut paths = self + .ctx + .vars() + .get(if for_cd { L!("CDPATH") } else { L!("PATH") }) + .map(|var| var.as_list().to_owned()) + .unwrap_or_default(); + + // The current directory is always valid. + paths.push(if for_cd { L!(".") } else { L!("") }.to_owned()); + for next_path in paths { + effective_working_dirs + .push(path_apply_working_directory(&next_path, &working_dir)); + } + } + } + + result = ExpandResult::new(ExpandResultCode::wildcard_no_match); + let mut expanded_recv = out.subreceiver(); + for effective_working_dir in effective_working_dirs { + let expand_res = wildcard_expand_string( + &path_to_expand, + &effective_working_dir, + self.flags, + &*self.ctx.cancel_checker, + &mut expanded_recv, + ); + match expand_res { + WildcardResult::Match => result = ExpandResult::ok(), + WildcardResult::NoMatch => (), + WildcardResult::Overflow => return append_overflow_error(self.errors, None), + WildcardResult::Cancel => return ExpandResult::new(ExpandResultCode::cancel), + } + } + + let mut expanded = expanded_recv.take(); + expanded.sort_by(|a, b| wcsfilecmp_glob(&a.completion, &b.completion)); + if !out.extend(expanded) { + result = ExpandResult::new(ExpandResultCode::error); + } + } else { + // Can't fully justify this check. I think it's that SKIP_WILDCARDS is used when completing + // to mean don't do file expansions, so if we're not doing file expansions, just drop this + // completion on the floor. + #[allow(clippy::collapsible_if)] + if !self.flags.contains(ExpandFlags::FOR_COMPLETIONS) { + if !out.add(path_to_expand) { + return append_overflow_error(self.errors, None); + } + } + } + result + } + + // Given an original input string, if it starts with a tilde, "unexpand" the expanded home + // directory. Note this may be just a tilde or a user name like ~foo/. + fn unexpand_tildes(&self, input: &wstr, completions: &mut CompletionList) { + // If input begins with tilde, then try to replace the corresponding string in each completion + // with the tilde. If it does not, there's nothing to do. + if input.as_char_slice().first() != Some(&'~') { + return; + } + + // This is a subtle kludge. We need to decide whether to unexpand tildes for all + // completions, or only those which replace their tokens. The problem is that we're sloppy + // about setting the COMPLETE_REPLACES_TOKEN flag, except when we're completing in the + // wildcard stage, because no other clients of string expansion care. Example: + // HOME=/foo + // mkdir ~/foo # makes /foo/foo + // cd ~/ + // Here we are likely to get a completion 'foo' which may match $HOME, but it extends its token + // instead of replacing it, so we don't modify it (it will just be appended to the original ~/). + // + // However if we are not completing, just expanding, then expansion just produces the full paths + // so we should unconditionally unexpand tildes. + let only_replacers = self.flags.contains(ExpandFlags::FOR_COMPLETIONS); + + // Helper to decide whether to process a completion. + let should_process = |c: &Completion| !only_replacers || c.replaces_token(); + + // Early out if none qualify. + if !completions.iter().any(should_process) { + return; + } + + // Get the username_with_tilde (like ~bert) and expand it into a home directory. + let mut tail_idx = usize::MAX; + let username_with_tilde = + WString::from_str("~") + get_home_directory_name(input, &mut tail_idx); + let mut home = username_with_tilde.clone(); + expand_tilde(&mut home, self.ctx.vars()); + + // Now for each completion that starts with home, replace it with the username_with_tilde. + for comp in completions { + if should_process(comp) && comp.completion.starts_with(&home) { + comp.completion + .replace_range(..home.len(), &username_with_tilde); + + // And mark that our tilde is literal, so it doesn't try to escape it. + comp.flags |= CompleteFlags::DONT_ESCAPE_TILDES; + } + } + } +} + +#[cxx::bridge] +mod expand_ffi { + extern "C++" { + include!("operation_context.h"); + include!("parse_constants.h"); + include!("env.h"); + include!("complete.h"); + type OperationContext<'a> = crate::operation_context::OperationContext<'a>; + type ParseErrorListFfi = crate::parse_constants::ParseErrorListFfi; + #[cxx_name = "EnvDyn"] + type EnvDynFFI = crate::env::EnvDynFFI; + #[cxx_name = "EnvStackRef"] + type EnvStackRefFFI = crate::env::EnvStackRefFFI; + type CompletionListFfi = crate::complete::CompletionListFfi; + } + + #[derive(Copy, Clone, Debug, Eq, PartialEq)] + pub enum ExpandResultCode { + /// There was an error, for example, unmatched braces. + error, + /// Expansion succeeded. + ok, + /// Expansion was cancelled (e.g. control-C). + cancel, + /// Expansion succeeded, but a wildcard in the string matched no files, + /// so the output is empty. + wildcard_no_match, + } + + /// These are the possible return values for expand_string. + #[must_use] + #[derive(Debug)] + pub struct ExpandResult { + /// The result of expansion. + pub result: ExpandResultCode, + + /// If expansion resulted in an error, this is an appropriate value with which to populate + /// $status. + // todo!("should be c_int?"); + pub status: i32, + } + + extern "Rust" { + #[cxx_name = "expand_home_directory"] + fn expand_home_directory_ffi( + input: &CxxWString, + vars: &EnvStackRefFFI, + ) -> UniquePtr; + } +} + +fn expand_home_directory_ffi(input: &CxxWString, vars: &EnvStackRefFFI) -> UniquePtr { + let mut s = input.from_ffi(); + expand_home_directory(&mut s, &*vars.0); + s.to_ffi() +} diff --git a/fish-rust/src/fds.rs b/fish-rust/src/fds.rs index dd3696b6c..6d148a8cf 100644 --- a/fish-rust/src/fds.rs +++ b/fish-rust/src/fds.rs @@ -2,14 +2,13 @@ use crate::ffi; use crate::wchar::prelude::*; use crate::wutil::perror; -use libc::EINTR; -use libc::{fcntl, F_GETFL, F_SETFL, O_CLOEXEC, O_NONBLOCK}; +use libc::{c_int, EINTR, FD_CLOEXEC, F_GETFD, F_GETFL, F_SETFD, F_SETFL, O_CLOEXEC, O_NONBLOCK}; use nix::unistd; use std::ffi::CStr; use std::io::{self, Read, Write}; use std::os::unix::prelude::*; -pub const PIPE_ERROR: &wstr = L!("An error occurred while setting up pipe"); +pub const PIPE_ERROR: &str = "An error occurred while setting up pipe"; /// The first "high fd", which is considered outside the range of valid user-specified redirections /// (like >&5). @@ -164,6 +163,27 @@ pub fn make_autoclose_pipes() -> Option { } } +/// Sets CLO_EXEC on a given fd according to the value of \p should_set. +pub fn set_cloexec(fd: RawFd, should_set: bool) -> c_int { + // Note we don't want to overwrite existing flags like O_NONBLOCK which may be set. So fetch the + // existing flags and modify them. + let flags = unsafe { libc::fcntl(fd, F_GETFD, 0) }; + if flags < 0 { + return -1; + } + let mut new_flags = flags; + if should_set { + new_flags |= FD_CLOEXEC; + } else { + new_flags &= !FD_CLOEXEC; + } + if flags == new_flags { + 0 + } else { + unsafe { libc::fcntl(fd, F_SETFD, new_flags) } + } +} + /// Wide character version of open() that also sets the close-on-exec flag (atomically when /// possible). pub fn wopen_cloexec(pathname: &wstr, flags: i32, mode: libc::c_int) -> RawFd { @@ -190,12 +210,12 @@ pub fn exec_close(fd: RawFd) { /// Mark an fd as nonblocking pub fn make_fd_nonblocking(fd: RawFd) -> Result<(), io::Error> { - let flags = unsafe { fcntl(fd, F_GETFL, 0) }; + let flags = unsafe { libc::fcntl(fd, F_GETFL, 0) }; let nonblocking = (flags & O_NONBLOCK) == O_NONBLOCK; if !nonblocking { - match unsafe { fcntl(fd, F_SETFL, flags | O_NONBLOCK) } { - 0 => return Ok(()), - _ => return Err(io::Error::last_os_error()), + match unsafe { libc::fcntl(fd, F_SETFL, flags | O_NONBLOCK) } { + -1 => return Err(io::Error::last_os_error()), + _ => return Ok(()), }; } Ok(()) @@ -203,12 +223,12 @@ pub fn make_fd_nonblocking(fd: RawFd) -> Result<(), io::Error> { /// Mark an fd as blocking pub fn make_fd_blocking(fd: RawFd) -> Result<(), io::Error> { - let flags = unsafe { fcntl(fd, F_GETFL, 0) }; + let flags = unsafe { libc::fcntl(fd, F_GETFL, 0) }; let nonblocking = (flags & O_NONBLOCK) == O_NONBLOCK; if nonblocking { - match unsafe { fcntl(fd, F_SETFL, flags & !O_NONBLOCK) } { - 0 => return Ok(()), - _ => return Err(io::Error::last_os_error()), + match unsafe { libc::fcntl(fd, F_SETFL, flags & !O_NONBLOCK) } { + -1 => return Err(io::Error::last_os_error()), + _ => return Ok(()), }; } Ok(()) diff --git a/fish-rust/src/ffi.rs b/fish-rust/src/ffi.rs index bcaf3f89a..1a5a0eeed 100644 --- a/fish-rust/src/ffi.rs +++ b/fish-rust/src/ffi.rs @@ -1,29 +1,24 @@ -use crate::wchar_ffi::WCharToFFI; +use crate::io::{IoStreams, OutputStreamFfi}; +use crate::wchar; #[rustfmt::skip] use ::std::pin::Pin; #[rustfmt::skip] use ::std::slice; -use crate::env::{EnvMode, EnvStackRef, EnvStackRefFFI}; -use crate::job_group::JobGroup; -pub use crate::wait_handle::{ - WaitHandleRef, WaitHandleRefFFI, WaitHandleStore, WaitHandleStoreFFI, -}; +pub use crate::wait_handle::{WaitHandleRef, WaitHandleStore}; use crate::wchar::prelude::*; use crate::wchar_ffi::WCharFromFFI; use autocxx::prelude::*; -use cxx::SharedPtr; -use libc::pid_t; // autocxx has been hacked up to know about this. pub type wchar_t = u32; include_cpp! { #include "autoload.h" - #include "builtin.h" #include "color.h" #include "common.h" #include "complete.h" #include "env.h" + #include "env_dispatch.h" #include "env_universal_common.h" #include "event.h" #include "exec.h" @@ -47,9 +42,10 @@ #include "tokenizer.h" #include "wutil.h" - // We need to block these types so when exposing C++ to Rust. - block!("WaitHandleStoreFFI") - block!("WaitHandleRefFFI") + #include "builtins/bind.h" + #include "builtins/commandline.h" + #include "builtins/read.h" + #include "builtins/ulimit.h" safety!(unsafe_ffi) @@ -58,34 +54,31 @@ generate!("wperror") generate!("set_inheriteds_ffi") - generate!("proc_init") - generate!("misc_init") generate!("reader_init") + generate!("reader_run_count") generate!("term_copy_modes") generate!("set_profiling_active") generate!("reader_read_ffi") + generate!("fish_is_unwinding_for_exit") generate!("restore_term_mode") - generate!("parse_util_detect_errors_ffi") + generate!("update_wait_on_escape_ms_ffi") + generate!("read_generation_count") 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!("env_universal_notifier_t_default_notifier_post_notification_ffi") + + generate!("builtin_bind") + generate!("builtin_commandline") + generate!("builtin_read") + generate!("builtin_ulimit") generate!("init_input") generate_pod!("pipes_ffi_t") - generate!("environment_t") - generate!("env_stack_t") - generate!("env_var_t") - generate!("env_universal_t") - generate!("env_universal_sync_result_t") - generate!("callback_data_t") - generate!("universal_notifier_t") - generate!("var_table_ffi_t") - - generate!("event_list_ffi_t") generate!("make_pipes_ffi") @@ -93,232 +86,39 @@ generate!("wgettext_ptr") - generate!("block_t") - generate!("Parser") - - generate!("job_t") - generate!("job_control_t") - 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") - - generate!("highlighter_t") - - generate!("proc_wait_any") - - generate!("output_stream_t") - generate!("IoStreams") - generate!("make_null_io_streams_ffi") - generate!("make_test_io_streams_ffi") - generate!("get_test_output_ffi") - - generate_pod!("RustFFIJobList") - generate_pod!("RustFFIProcList") - generate_pod!("RustBuiltin") - - generate!("builtin_exists") - generate!("builtin_missing_argument") - generate!("builtin_unknown_option") - generate!("builtin_print_help") - generate!("builtin_print_error_trailer") - generate!("builtin_get_names_ffi") - generate!("pretty_printer_t") - generate!("escape_string") - generate!("fd_event_signaller_t") - generate!("block_t") - generate!("block_type_t") - generate!("statuses_t") - generate!("io_chain_t") - - generate!("env_var_t") - - generate!("exec_subshell_ffi") + generate!("highlight_role_t") + generate!("highlight_spec_t") generate!("rgb_color_t") generate_pod!("color24_t") - generate!("colorize_shell") generate!("reader_status_count") + generate!("reader_write_title_ffi") + generate!("reader_push_ffi") + generate!("reader_readline_ffi") + generate!("reader_pop") + generate!("commandline_get_state_history_ffi") + generate!("commandline_set_buffer_ffi") + generate!("commandline_get_state_initialized_ffi") + generate!("commandline_get_state_text_ffi") + generate!("completion_apply_to_command_line") generate!("get_history_variable_text_ffi") - generate!("is_interactive_session") - generate!("set_interactive_session") + generate_pod!("escape_string_style_t") + + generate!("screen_set_midnight_commander_hack") generate!("screen_clear_layout_cache_ffi") generate!("escape_code_length_ffi") 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") - generate!("complete_add_wrapper") - generate!("update_wait_on_escape_ms_ffi") generate!("update_wait_on_sequence_key_ms_ffi") - generate!("autoload_t") - generate!("make_autoload_ffi") - generate!("perform_autoload_ffi") - generate!("complete_get_wrap_targets_ffi") - - generate!("is_thompson_shell_script") -} - -impl Parser { - pub fn get_wait_handles_mut(&mut self) -> &mut WaitHandleStore { - let ptr = self.get_wait_handles_void() as *mut Box; - assert!(!ptr.is_null()); - unsafe { (*ptr).from_ffi_mut() } - } - - pub fn get_wait_handles(&self) -> &WaitHandleStore { - let ptr = self.get_wait_handles_void() as *const Box; - assert!(!ptr.is_null()); - unsafe { (*ptr).from_ffi() } - } - - pub fn get_block_at_index(&self, i: usize) -> Option<&block_t> { - let b = self.block_at_index(i); - unsafe { b.as_ref() } - } - - pub fn get_jobs(&self) -> &[SharedPtr] { - let ffi_jobs = self.ffi_jobs(); - unsafe { slice::from_raw_parts(ffi_jobs.jobs, ffi_jobs.count) } - } - - pub fn libdata_pod(&mut self) -> &mut library_data_pod_t { - let libdata = self.pin().ffi_libdata_pod(); - - unsafe { &mut *libdata } - } - - pub fn remove_var(&mut self, var: &wstr, flags: c_int) -> c_int { - self.pin().remove_var_ffi(&var.to_ffi(), flags) - } - - pub fn job_get_from_pid(&self, pid: pid_t) -> Option<&job_t> { - let job = self.ffi_job_get_from_pid(pid.into()); - unsafe { job.as_ref() } - } - - pub fn get_vars(&mut self) -> EnvStackRef { - self.pin().vars().from_ffi() - } - - pub fn get_func_name(&mut self, level: i32) -> Option { - let name = self.pin().get_function_name_ffi(c_int(level)); - name.as_ref() - .map(|s| s.from_ffi()) - .filter(|s| !s.is_empty()) - } -} - -unsafe impl Send for env_universal_t {} - -impl env_stack_t { - /// Access the underlying Rust environment stack. - #[allow(clippy::borrowed_box)] - pub fn from_ffi(&self) -> EnvStackRef { - // Safety: get_impl_ffi returns a pointer to a Box. - let envref = self.get_impl_ffi(); - assert!(!envref.is_null()); - let env: &Box = unsafe { &*(envref.cast()) }; - env.0.clone() - } -} - -impl environment_t { - /// Helper to get a variable as a string, using the default flags. - pub fn get_as_string(&self, name: &wstr) -> Option { - self.get_as_string_flags(name, EnvMode::default()) - } - - /// Helper to get a variable as a string, using the given flags. - pub fn get_as_string_flags(&self, name: &wstr, flags: EnvMode) -> Option { - self.get_or_null(&name.to_ffi(), flags.bits()) - .as_ref() - .map(|s| s.as_string().from_ffi()) - } -} - -impl env_stack_t { - /// Helper to get a variable as a string, using the default flags. - pub fn get_as_string(&self, name: &wstr) -> Option { - self.get_as_string_flags(name, EnvMode::default()) - } - - /// Helper to get a variable as a string, using the given flags. - pub fn get_as_string_flags(&self, name: &wstr, flags: EnvMode) -> Option { - self.get_or_null(&name.to_ffi(), flags.bits()) - .as_ref() - .map(|s| s.as_string().from_ffi()) - } - - /// Helper to set a value. - pub fn set_var, U: AsRef>( - &mut self, - name: T, - value: &[U], - flags: EnvMode, - ) -> libc::c_int { - use crate::wchar_ffi::{wstr_to_u32string, W0String}; - let strings: Vec = value.iter().map(wstr_to_u32string).collect(); - let ptrs: Vec<*const u32> = strings.iter().map(|s| s.as_ptr()).collect(); - self.pin() - .set_ffi( - &name.as_ref().to_ffi(), - flags.bits(), - ptrs.as_ptr() as *const c_void, - ptrs.len(), - ) - .into() - } -} - -impl job_t { - #[allow(clippy::mut_from_ref)] - pub fn get_procs(&self) -> &mut [UniquePtr] { - let ffi_procs = self.ffi_processes(); - unsafe { slice::from_raw_parts_mut(ffi_procs.procs, ffi_procs.count) } - } - - pub fn get_job_group(&self) -> &JobGroup { - unsafe { ::std::mem::transmute::<&job_group_t, &JobGroup>(self.ffi_group()) } - } -} - -impl process_t { - /// \return the wait handle for the process, if it exists. - pub fn get_wait_handle(&self) -> Option { - let handle_ptr = self.get_wait_handle_void() as *const Box; - if handle_ptr.is_null() { - None - } else { - let handle: &WaitHandleRefFFI = unsafe { &*handle_ptr }; - Some(handle.from_ffi().clone()) - } - } - - /// \return the wait handle for the process, creating it if necessary. - pub fn make_wait_handle(&mut self, jid: u64) -> Option { - let handle_ref = self.pin().make_wait_handle_void(jid) as *const Box; - if handle_ref.is_null() { - None - } else { - let handle: &WaitHandleRefFFI = unsafe { &*handle_ref }; - Some(handle.from_ffi().clone()) - } - } } /// Allow wcharz_t to be "into" wstr. @@ -339,6 +139,17 @@ fn from(w: wcharz_t) -> Self { } } +/// Allow wcstring_list_ffi_t to be "into" Vec. +impl From<&wcstring_list_ffi_t> for Vec { + fn from(w: &wcstring_list_ffi_t) -> Self { + let mut result = Vec::with_capacity(w.size()); + for i in 0..w.size() { + result.push(w.at(i).from_ffi()); + } + result + } +} + /// A bogus trait for turning &mut Foo into Pin<&mut Foo>. /// autocxx enforces that non-const methods must be called through Pin, /// but this means we can't pass around mutable references to types like Parser. @@ -357,16 +168,10 @@ fn unpin(self: Pin<&mut Self>) -> &mut Self { } // Implement Repin for our types. -impl Repin for autoload_t {} -impl Repin for block_t {} -impl Repin for env_stack_t {} -impl Repin for env_universal_t {} -impl Repin for IoStreams {} -impl Repin for job_t {} -impl Repin for output_stream_t {} -impl Repin for Parser {} -impl Repin for process_t {} +impl Repin for IoStreams<'_> {} impl Repin for wcstring_list_ffi_t {} +impl Repin for rgb_color_t {} +impl Repin for OutputStreamFfi<'_> {} pub use autocxx::c_int; pub use ffi::*; @@ -423,19 +228,3 @@ fn from(value: void_ptr) -> Self { value.0 as *const _ } } - -impl TryFrom<&wstr> for job_control_t { - type Error = (); - - fn try_from(value: &wstr) -> Result { - if value == "full" { - Ok(job_control_t::all) - } else if value == "interactive" { - Ok(job_control_t::interactive) - } else if value == "none" { - Ok(job_control_t::none) - } else { - Err(()) - } - } -} diff --git a/fish-rust/src/fish.rs b/fish-rust/src/fish.rs index b598f7835..17195b4df 100644 --- a/fish-rust/src/fish.rs +++ b/fish-rust/src/fish.rs @@ -25,28 +25,36 @@ BUILTIN_ERR_MISSING, BUILTIN_ERR_UNKNOWN, STATUS_CMD_OK, STATUS_CMD_UNKNOWN, }, common::{ - escape, exit_without_destructors, get_executable_path, str2wcstring, wcs2string, - PROFILING_ACTIVE, PROGRAM_NAME, + escape, exit_without_destructors, get_executable_path, save_term_foreground_process_group, + scoped_push_replacer, str2wcstring, wcs2string, PROFILING_ACTIVE, PROGRAM_NAME, }, + env::Statuses, env::{ environment::{env_init, EnvStack, Environment}, ConfigPaths, EnvMode, }, event::{self, Event}, - ffi::{self, Repin}, + ffi::{self}, flog::{self, activate_flog_categories_by_pattern, set_flog_file_fd, FLOG, FLOGF}, - function, future_feature_flags as features, + function, future_feature_flags as features, history, history::start_private_mode, - parse_constants::{ParseErrorList, ParseErrorListFfi, ParseTreeFlags}, - parse_tree::{ParsedSource, ParsedSourceRefFFI}, + io::IoChain, + parse_constants::{ParseErrorList, ParseTreeFlags}, + parse_tree::ParsedSource, + parse_util::parse_util_detect_errors_in_ast, + parser::{BlockType, Parser}, path::path_get_config, + proc::{ + get_login, is_interactive_session, mark_login, mark_no_exec, proc_init, + set_interactive_session, + }, signal::{signal_clear_cancel, signal_unblock_all}, threads::{self, asan_maybe_exit}, topic_monitor, wchar::prelude::*, - wchar_ffi::{WCharFromFFI, WCharToFFI}, wutil::waccess, }; +use libc::STDERR_FILENO; use std::env; use std::ffi::{CString, OsStr, OsString}; use std::fs::File; @@ -264,7 +272,7 @@ fn determine_config_directory_paths(argv0: impl AsRef) -> ConfigPaths { } // Source the file config.fish in the given directory. -fn source_config_in_directory(parser: &mut ffi::Parser, dir: &wstr) { +fn source_config_in_directory(parser: &Parser, 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. @@ -286,17 +294,13 @@ fn source_config_in_directory(parser: &mut ffi::Parser, dir: &wstr) { 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; + parser.libdata_mut().pods.within_fish_init = true; + let _ = parser.eval(&cmd, &IoChain::new()); + parser.libdata_mut().pods.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, paths: &ConfigPaths) { +fn read_init(parser: &Parser, 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())); @@ -308,7 +312,7 @@ fn read_init(parser: &mut ffi::Parser, paths: &ConfigPaths) { } } -fn run_command_list(parser: &mut ffi::Parser, cmds: &[OsString]) -> i32 { +fn run_command_list(parser: &Parser, cmds: &[OsString]) -> i32 { let mut retval = STATUS_CMD_OK; for cmd in cmds { let cmd_wcs = str2wcstring(cmd.as_bytes()); @@ -316,44 +320,17 @@ fn run_command_list(parser: &mut ffi::Parser, cmds: &[OsString]) -> i32 { 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 + parse_util_detect_errors_in_ast(&ast, &cmd_wcs, Some(&mut errors)).is_err() }; 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(); + let ps = Arc::new(ParsedSource::new(cmd_wcs, ast)); + let _ = parser.eval_parsed_source(&ps, &IoChain::new(), None, BlockType::top); 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()); + let backtrace = parser.get_backtrace(&cmd_wcs, &errors); + fwprintf!(STDERR_FILENO, "%s", backtrace); // XXX: Why is this the return for "unknown command"? retval = STATUS_CMD_UNKNOWN; } @@ -362,7 +339,7 @@ fn run_command_list(parser: &mut ffi::Parser, cmds: &[OsString]) -> i32 { retval.unwrap() } -fn fish_parse_opt(args: &mut [&wstr], opts: &mut FishCmdOpts) -> usize { +fn fish_parse_opt(args: &mut [WString], opts: &mut FishCmdOpts) -> usize { use crate::wgetopt::{wgetopter_t, wopt, woption, woption_argument_t::*}; const RUSAGE_ARG: char = 1 as char; @@ -398,7 +375,8 @@ fn fish_parse_opt(args: &mut [&wstr], opts: &mut FishCmdOpts) -> usize { wopt(L!("version"), no_argument, 'v'), ]; - let mut w = wgetopter_t::new(SHORT_OPTS, LONG_OPTS, args); + let mut shim_args: Vec<&wstr> = args.iter().map(|s| s.as_ref()).collect(); + let mut w = wgetopter_t::new(SHORT_OPTS, LONG_OPTS, &mut shim_args); while let Some(c) = w.wgetopt_long() { match c { 'c' => opts @@ -498,7 +476,7 @@ fn fish_parse_opt(args: &mut [&wstr], opts: &mut FishCmdOpts) -> usize { && optind == args.len() && unsafe { libc::isatty(libc::STDIN_FILENO) != 0 } { - ffi::set_interactive_session(true); + set_interactive_session(true); } optind @@ -554,13 +532,8 @@ fn main() -> i32 { 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; + my_optind = fish_parse_opt(&mut args, &mut opts); // Direct any debug output right away. // --debug-output takes precedence, otherwise $FISH_DEBUG_OUTPUT is used. @@ -625,13 +598,13 @@ fn main() -> i32 { // Apply our options if opts.is_login { - ffi::mark_login(); + mark_login(); } if opts.no_exec { - ffi::mark_no_exec(); + mark_no_exec(); } if opts.is_interactive_session { - ffi::set_interactive_session(true); + set_interactive_session(true); } if opts.enable_private_mode { start_private_mode(EnvStack::globals()); @@ -639,9 +612,8 @@ fn main() -> i32 { // 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(); + if is_interactive_session() { + save_term_foreground_process_group(); } let mut paths: Option = None; @@ -649,7 +621,7 @@ fn main() -> i32 { if !opts.no_exec { // PORTING: C++ had not converted, we must revert paths = Some(determine_config_directory_paths(OsString::from_vec( - wcs2string(args[0]), + wcs2string(&args[0]), ))); env_init( paths.as_ref(), @@ -667,20 +639,20 @@ fn main() -> i32 { } } features::set_from_string(opts.features.as_utfstr()); - ffi::proc_init(); - ffi::misc_init(); + proc_init(); + crate::env::misc_init(); ffi::reader_init(); - let parser = unsafe { &mut *ffi::Parser::principal_parser_ffi() }; - parser.pin().set_syncs_uvars(!opts.no_config); + let parser = Parser::principal_parser(); + parser.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 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( + parser.vars().set_one( L!("fish_key_bindings"), EnvMode::UNEXPORT, L!("fish_default_key_bindings").to_owned(), @@ -694,19 +666,16 @@ fn main() -> i32 { 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()); + parser.set_last_statuses(Statuses::just(STATUS_CMD_OK.unwrap())); // 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()); + parser.emit_profiling(s.as_bytes()); // If we are profiling both, ensure the startup data only // ends up in the startup file. - parser.pin().clear_profiling(); + parser.clear_profiling(); } PROFILING_ACTIVE.store(opts.profile_output.is_some()); @@ -722,22 +691,21 @@ fn main() -> i32 { if !opts.batch_cmds.is_empty() { // Run the commands specified as arguments, if any. - if ffi::get_login() { + 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. - // PORTING: the args were converted to WString here in C++ let list = &args[my_optind..]; - parser.get_vars().set( + parser.vars().set( L!("argv"), EnvMode::default(), - list.iter().map(|&s| s.to_owned()).collect(), + list.iter().map(|s| s.to_owned()).collect(), ); res = run_command_list(parser, &opts.batch_cmds); - parser.libdata_pod().exit_current_script = false; + parser.libdata_mut().pods.exit_current_script = false; } else if my_optind == args.len() { // Implicitly interactive mode. if opts.no_exec && unsafe { libc::isatty(libc::STDIN_FILENO) != 0 } { @@ -748,10 +716,15 @@ fn main() -> i32 { // above line should always exit return libc::EXIT_FAILURE; } - res = ffi::reader_read_ffi(parser.pin(), c_int(libc::STDIN_FILENO)).into(); + res = ffi::reader_read_ffi( + parser as *const Parser as *const autocxx::c_void, + c_int(libc::STDIN_FILENO), + &IoChain::new() as *const _ as *const autocxx::c_void, + ) + .into(); } else { // C++ had not converted at this point, we must undo - let n = wcs2string(args[my_optind]); + let n = wcs2string(&args[my_optind]); let path = OsStr::from_bytes(&n); my_optind += 1; // Rust sets cloexec by default, see above @@ -768,16 +741,24 @@ fn main() -> i32 { Ok(f) => { // PORTING: the args were converted to WString here in C++ let list = &args[my_optind..]; - parser.get_vars().set( + parser.vars().set( L!("argv"), EnvMode::default(), - list.iter().map(|&s| s.to_owned()).collect(), + 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(); + let _filename_push = scoped_push_replacer( + |new_value| { + std::mem::replace(&mut parser.libdata_mut().current_filename, new_value) + }, + Some(Arc::new(rel_filename.to_owned())), + ); + res = ffi::reader_read_ffi( + parser as *const Parser as *const autocxx::c_void, + c_int(f.as_raw_fd()), + &IoChain::new() as *const _ as *const autocxx::c_void, + ) + .into(); if res != 0 { FLOGF!( warning, @@ -785,7 +766,6 @@ fn main() -> i32 { path.to_string_lossy() ); } - parser.pin().set_filename_ffi(old_filename.to_ffi()); } } } @@ -793,7 +773,7 @@ fn main() -> i32 { let exit_status = if res != 0 { STATUS_CMD_UNKNOWN.unwrap() } else { - parser.pin().get_last_status().into() + parser.get_last_status() }; event::fire( @@ -814,10 +794,10 @@ fn main() -> i32 { if let Some(profile_output) = opts.profile_output { let s = cstr_from_osstr(&profile_output); - parser.pin().emit_profiling(s.as_ptr()); + parser.emit_profiling(s.as_bytes()); } - ffi::history_save_all(); + history::save_all(); if opts.print_rusage_self { print_rusage_self(); } @@ -845,7 +825,7 @@ fn escape_single_quoted_hack_hack_hack_hack(s: &wstr) -> OsString { return result; } -fn fish_xdm_login_hack_hack_hack_hack(cmds: &mut Vec, args: &[&wstr]) -> bool { +fn fish_xdm_login_hack_hack_hack_hack(cmds: &mut Vec, args: &[WString]) -> bool { if cmds.len() != 1 { return false; } diff --git a/fish-rust/src/flog.rs b/fish-rust/src/flog.rs index 13a9798d0..8cef1f063 100644 --- a/fish-rust/src/flog.rs +++ b/fish-rust/src/flog.rs @@ -191,7 +191,7 @@ macro_rules! FLOG { macro_rules! FLOGF { ($category:ident, $fmt: expr, $($elem:expr),+ $(,)*) => { - crate::flog::FLOG!($category, sprintf!($fmt, $($elem),*)); + crate::flog::FLOG!($category, crate::wutil::sprintf!($fmt, $($elem),*)) } } diff --git a/fish-rust/src/fork_exec/mod.rs b/fish-rust/src/fork_exec/mod.rs index 4cb5ef839..aa25b9ec3 100644 --- a/fish-rust/src/fork_exec/mod.rs +++ b/fish-rust/src/fork_exec/mod.rs @@ -5,16 +5,17 @@ mod flog_safe; pub mod postfork; pub mod spawn; -use crate::ffi::job_t; +use crate::proc::Job; +use libc::{SIGINT, SIGQUIT}; /// Get the list of signals which should be blocked for a given job. /// Return true if at least one signal was set. -fn blocked_signals_for_job(job: &job_t, sigmask: &mut libc::sigset_t) -> bool { +pub fn blocked_signals_for_job(job: &Job, sigmask: &mut libc::sigset_t) -> bool { // Block some signals in background jobs for which job control is turned off (#6828). if !job.is_foreground() && !job.wants_job_control() { unsafe { - libc::sigaddset(sigmask, libc::SIGINT); - libc::sigaddset(sigmask, libc::SIGQUIT); + libc::sigaddset(sigmask, SIGINT); + libc::sigaddset(sigmask, SIGQUIT); } return true; } diff --git a/fish-rust/src/fork_exec/postfork.rs b/fish-rust/src/fork_exec/postfork.rs index 3720c45f1..867596e0a 100644 --- a/fish-rust/src/fork_exec/postfork.rs +++ b/fish-rust/src/fork_exec/postfork.rs @@ -50,7 +50,7 @@ pub fn report_setpgid_error( is_parent: bool, pid: pid_t, desired_pgid: pid_t, - job_id: c_int, + job_id: i64, command_str: *const c_char, argv0_str: *const c_char, ) { @@ -100,7 +100,7 @@ pub fn report_setpgid_error( /// Execute setpgid, moving pid into the given pgroup. /// Return the result of setpgid. -fn execute_setpgid(pid: pid_t, pgroup: pid_t, is_parent: bool) -> i32 { +pub fn execute_setpgid(pid: pid_t, pgroup: pid_t, is_parent: bool) -> i32 { // There is a comment "Historically we have looped here to support WSL." // TODO: stop looping. let mut eperm_count = 0; @@ -145,7 +145,7 @@ fn execute_setpgid(pid: pid_t, pgroup: pid_t, is_parent: bool) -> i32 { } /// Set up redirections and signal handling in the child process. -fn child_setup_process( +pub fn child_setup_process( claim_tty_from: pid_t, sigmask: Option<&libc::sigset_t>, is_forked: bool, @@ -204,7 +204,7 @@ fn child_setup_process( /// This function is a wrapper around fork. If the fork calls fails with EAGAIN, it is retried /// FORK_LAPS times, with a very slight delay between each lap. If fork fails even then, the process /// will exit with an error message. -fn execute_fork() -> pid_t { +pub fn execute_fork() -> pid_t { let mut err = 0; for i in 0..FORK_LAPS { let pid = unsafe { libc::fork() }; @@ -242,7 +242,7 @@ fn execute_fork() -> pid_t { exit_without_destructors(1) } -fn safe_report_exec_error( +pub fn safe_report_exec_error( err: i32, actual_cmd: *const c_char, argvv: *const *const c_char, @@ -596,7 +596,7 @@ pub extern "C" fn report_setpgid_error( is_parent, pid, desired_pgid, - job_id, + job_id.try_into().unwrap(), command_str, argv0_str, ) diff --git a/fish-rust/src/fork_exec/spawn.rs b/fish-rust/src/fork_exec/spawn.rs index 98021b0fc..59d9fdaca 100644 --- a/fish-rust/src/fork_exec/spawn.rs +++ b/fish-rust/src/fork_exec/spawn.rs @@ -1,7 +1,8 @@ //! Wrappers around posix_spawn. use super::blocked_signals_for_job; -use crate::ffi::{self, job_t}; +use crate::exec::is_thompson_shell_script; +use crate::proc::Job; use crate::redirection::Dup2List; use crate::signal::get_signals_with_handlers; use errno::{self, set_errno, Errno}; @@ -96,15 +97,15 @@ pub struct PosixSpawner { } impl PosixSpawner { - pub fn new(j: &job_t, dup2s: &Dup2List) -> Result { + pub fn new(j: &Job, dup2s: &Dup2List) -> Result { let mut attr = Attr::new()?; let mut actions = FileActions::new()?; // desired_pgid tracks the pgroup for the process. If it is none, the pgroup is left unchanged. // If it is zero, create a new pgroup from the pid. If it is >0, join that pgroup. - let desired_pgid = if let Some(pgid) = j.get_job_group().get_pgid() { + let desired_pgid = if let Some(pgid) = j.get_pgid() { Some(pgid) - } else if j.get_procs()[0].as_ref().unwrap().get_leads_pgrp() { + } else if j.processes()[0].leads_pgrp { Some(0) } else { None @@ -179,14 +180,15 @@ pub fn spawn( // the kernel won't run the binary we hand it off to the interpreter // after performing a binary safety check, recommended by POSIX: a // line needs to exist before the first \0 with a lowercase letter. - if spawn_err.0 == libc::ENOEXEC && ffi::is_thompson_shell_script(cmd) { + let cmdcstr = unsafe { CStr::from_ptr(cmd) }; + if spawn_err.0 == libc::ENOEXEC && is_thompson_shell_script(cmdcstr) { // Create a new argv with /bin/sh prepended. let interp = get_path_bshell(); let mut argv2 = vec![interp.as_ptr() as *mut c_char]; // The command to call should use the full path, // not what we would pass as argv0. - let cmd2: CString = CString::new(unsafe { CStr::from_ptr(cmd).to_bytes() }).unwrap(); + let cmd2: CString = CString::new(cmdcstr.to_bytes()).unwrap(); argv2.push(cmd2.as_ptr() as *mut c_char); for i in 1.. { let ptr = unsafe { argv.offset(i).read() }; @@ -218,20 +220,6 @@ fn get_path_bshell() -> CString { CString::new("/bin/sh").unwrap() } -/// Returns a Box::into_raw(), or nullptr on error, in which case errno is set. -/// j is an unowned pointer to a job_t, dup2s is an unowned pointer to a Dup2List. -fn new_spawner_ffi(j: *const u8, dup2s: *const u8) -> *mut PosixSpawner { - let j: &job_t = unsafe { &*j.cast() }; - let dup2s: &Dup2List = unsafe { &*dup2s.cast() }; - match PosixSpawner::new(j, dup2s) { - Ok(spawner) => Box::into_raw(Box::new(spawner)), - Err(err) => { - set_errno(err); - std::ptr::null_mut() - } - } -} - impl Drop for PosixSpawner { fn drop(&mut self) { // Necessary to define this for FFI purposes, to avoid link errors. @@ -255,28 +243,3 @@ fn spawn_ffi( } } } - -fn force_cxx_to_generate_box_do_not_call() -> Box { - unimplemented!("Do not call, for linking help only"); -} - -#[cxx::bridge] -mod posix_spawn_ffi { - extern "Rust" { - type PosixSpawner; - - // Required to force cxx to generate a Box destructor, to avoid link errors. - fn force_cxx_to_generate_box_do_not_call() -> Box; - - #[cxx_name = "new_spawner"] - fn new_spawner_ffi(j: *const u8, dup2s: *const u8) -> *mut PosixSpawner; - - #[cxx_name = "spawn"] - fn spawn_ffi( - &mut self, - cmd: *const c_char, - argv: *const *mut c_char, - envp: *const *mut c_char, - ) -> i32; - } -} diff --git a/fish-rust/src/function.rs b/fish-rust/src/function.rs index f1b611d2a..fa37baf27 100644 --- a/fish-rust/src/function.rs +++ b/fish-rust/src/function.rs @@ -3,12 +3,14 @@ // the parser and to some degree the builtin handling library. use crate::ast::{self, Node}; +use crate::autoload::Autoload; use crate::common::{assert_sync, escape, valid_func_name, FilenameRef}; +use crate::complete::complete_get_wrap_targets; use crate::env::{EnvStack, Environment}; use crate::event::{self, EventDescription}; -use crate::ffi::{self, Parser, Repin}; use crate::global_safety::RelaxedAtomicBool; use crate::parse_tree::{NodeRef, ParsedSourceRefFFI}; +use crate::parser::Parser; use crate::parser_keywords::parser_keywords_is_reserved; use crate::wchar::prelude::*; use crate::wchar_ffi::wcstring_list_ffi_t; @@ -68,7 +70,7 @@ struct FunctionSet { autoload_tombstones: HashSet, /// The autoloader for our functions. - autoloader: cxx::UniquePtr, + autoloader: Autoload, } impl FunctionSet { @@ -105,16 +107,16 @@ fn allow_autoload(&self, name: &wstr) -> bool { Mutex::new(FunctionSet { funcs: HashMap::new(), autoload_tombstones: HashSet::new(), - autoloader: ffi::make_autoload_ffi(L!("fish_function_path").to_ffi()), + autoloader: Autoload::new(L!("fish_function_path")), }) }); -/// Necessary until autoloader has been ported to Rust. +// Safety: global lock. unsafe impl Send for FunctionSet {} /// Make sure that if the specified function is a dynamically loaded function, it has been fully /// loaded. Note this executes fish script code. -fn load(name: &wstr, parser: &mut Parser) -> bool { +pub fn load(name: &wstr, parser: &Parser) -> bool { parser.assert_can_execute(); let mut path_to_autoload: Option = None; // Note we can't autoload while holding the funcset lock. @@ -122,13 +124,10 @@ fn load(name: &wstr, parser: &mut Parser) -> bool { { let mut funcset: std::sync::MutexGuard = FUNCTION_SET.lock().unwrap(); if funcset.allow_autoload(name) { - let path = funcset + if let Some(path) = funcset .autoloader - .as_mut() - .unwrap() - .resolve_command_ffi(&name.to_ffi() /* Environment::globals() */) - .from_ffi(); - if !path.is_empty() { + .resolve_command(name, EnvStack::globals().as_ref().get_ref()) + { path_to_autoload = Some(path); } } @@ -137,14 +136,12 @@ fn load(name: &wstr, parser: &mut Parser) -> bool { // Release the lock and perform any autoload, then reacquire the lock and clean up. if let Some(path_to_autoload) = path_to_autoload.as_ref() { // Crucially, the lock is acquired after perform_autoload(). - ffi::perform_autoload_ffi(&path_to_autoload.to_ffi(), parser.pin()); + Autoload::perform_autoload(path_to_autoload, parser); FUNCTION_SET .lock() .unwrap() .autoloader - .as_mut() - .unwrap() - .mark_autoload_finished(&name.to_ffi()); + .mark_autoload_finished(name); } path_to_autoload.is_some() } @@ -199,7 +196,7 @@ pub fn add(name: WString, props: Arc) { // Check if this is a function that we are autoloading. props .is_autoload - .store(funcset.autoloader.autoload_in_progress(&name.to_ffi())); + .store(funcset.autoloader.autoload_in_progress(&name)); // Create and store a new function. let existing = funcset.funcs.insert(name, props); @@ -219,7 +216,7 @@ pub fn get_props(name: &wstr) -> Option> { } /// \return the properties for a function, or None, perhaps triggering autoloading. -pub fn get_props_autoload(name: &wstr, parser: &mut Parser) -> Option> { +pub fn get_props_autoload(name: &wstr, parser: &Parser) -> Option> { parser.assert_can_execute(); if parser_keywords_is_reserved(name) { return None; @@ -230,7 +227,7 @@ pub fn get_props_autoload(name: &wstr, parser: &mut Parser) -> Option bool { +pub fn exists(cmd: &wstr, parser: &Parser) -> bool { parser.assert_can_execute(); if !valid_func_name(cmd) { return false; @@ -249,12 +246,7 @@ pub fn exists_no_autoload(cmd: &wstr) -> bool { } let mut funcset = FUNCTION_SET.lock().unwrap(); // Check if we either have the function, or it could be autoloaded. - funcset.get_props(cmd).is_some() - || funcset - .autoloader - .as_mut() - .unwrap() - .can_autoload(&cmd.to_ffi()) + funcset.get_props(cmd).is_some() || funcset.autoloader.can_autoload(cmd) } /// Remove the function with the specified name. @@ -291,7 +283,7 @@ fn get_function_body_source(props: &FunctionProperties) -> &wstr { /// Sets the description of the function with the name \c name. /// This triggers autoloading. -pub fn set_desc(name: &wstr, desc: WString, parser: &mut Parser) { +pub(crate) fn set_desc(name: &wstr, desc: WString, parser: &Parser) { parser.assert_can_execute(); load(name, parser); let mut funcset = FUNCTION_SET.lock().unwrap(); @@ -307,7 +299,7 @@ pub fn set_desc(name: &wstr, desc: WString, parser: &mut Parser) { /// Creates a new function using the same definition as the specified function. Returns true if copy /// is successful. pub fn copy(name: &wstr, new_name: WString, parser: &Parser) -> bool { - let filename = parser.current_filename_ffi().from_ffi(); + let filename = parser.current_filename(); let lineno = parser.get_lineno(); let mut funcset = FUNCTION_SET.lock().unwrap(); @@ -319,8 +311,8 @@ pub fn copy(name: &wstr, new_name: WString, parser: &Parser) -> bool { let mut new_props = props.as_ref().clone(); new_props.is_autoload.store(false); new_props.is_copy = true; - new_props.copy_definition_file = Some(Arc::new(filename)); - new_props.copy_definition_lineno = lineno.into(); + new_props.copy_definition_file = filename.clone(); + new_props.copy_definition_lineno = lineno.unwrap_or(0) as i32; // Note this will NOT overwrite an existing function with the new name. // TODO: rationalize if this behavior is desired. @@ -350,7 +342,7 @@ pub fn invalidate_path() { // Remove all autoloaded functions and update the autoload path. let mut funcset = FUNCTION_SET.lock().unwrap(); funcset.funcs.retain(|_, props| !props.is_autoload.load()); - funcset.autoloader.as_mut().unwrap().clear(); + funcset.autoloader.clear(); } impl FunctionProperties { @@ -427,7 +419,7 @@ pub fn annotated_definition(&self, name: &wstr) -> WString { } // Output wrap targets. - for wrap in ffi::complete_get_wrap_targets_ffi(&name.to_ffi()).from_ffi() { + for wrap in complete_get_wrap_targets(name) { out.push_str(" --wraps="); out.push_utfstr(&escape(&wrap)); } @@ -588,9 +580,9 @@ fn function_get_props_ffi(name: &CxxWString) -> *mut FunctionPropertiesRefFFI { fn function_get_props_autoload_ffi( name: &CxxWString, - parser: Pin<&mut Parser>, + parser: &Parser, ) -> *mut FunctionPropertiesRefFFI { - let props = get_props_autoload(name.as_wstr(), parser.unpin()); + let props = get_props_autoload(name.as_wstr(), parser); if let Some(props) = props { Box::into_raw(Box::new(FunctionPropertiesRefFFI(props))) } else { @@ -598,16 +590,16 @@ fn function_get_props_autoload_ffi( } } -fn function_load_ffi(name: &CxxWString, parser: Pin<&mut Parser>) -> bool { - load(name.as_wstr(), parser.unpin()) +fn function_load_ffi(name: &CxxWString, parser: &Parser) -> bool { + load(name.as_wstr(), parser) } -fn function_set_desc_ffi(name: &CxxWString, desc: &CxxWString, parser: Pin<&mut Parser>) { - set_desc(name.as_wstr(), desc.from_ffi(), parser.unpin()); +fn function_set_desc_ffi(name: &CxxWString, desc: &CxxWString, parser: &Parser) { + set_desc(name.as_wstr(), desc.from_ffi(), parser); } -fn function_exists_ffi(cmd: &CxxWString, parser: Pin<&mut Parser>) -> bool { - exists(cmd.as_wstr(), parser.unpin()) +fn function_exists_ffi(cmd: &CxxWString, parser: &Parser) -> bool { + exists(cmd.as_wstr(), parser) } fn function_exists_no_autoload_ffi(cmd: &CxxWString) -> bool { @@ -621,8 +613,8 @@ fn function_get_names_ffi(get_hidden: bool, mut out: Pin<&mut wcstring_list_ffi_ } } -fn function_copy_ffi(name: &CxxWString, new_name: &CxxWString, parser: Pin<&mut Parser>) -> bool { - copy(name.as_wstr(), new_name.from_ffi(), parser.unpin()) +fn function_copy_ffi(name: &CxxWString, new_name: &CxxWString, parser: &Parser) -> bool { + copy(name.as_wstr(), new_name.from_ffi(), parser) } #[cxx::bridge] @@ -632,7 +624,7 @@ mod function_ffi { include!("parse_tree.h"); include!("parser.h"); include!("wutil.h"); - type Parser = crate::ffi::Parser; + type Parser = crate::parser::Parser; type wcstring_list_ffi_t = crate::ffi::wcstring_list_ffi_t; } @@ -675,18 +667,17 @@ mod function_ffi { #[cxx_name = "function_get_props_autoload_raw"] fn function_get_props_autoload_ffi( name: &CxxWString, - parser: Pin<&mut Parser>, + parser: &Parser, ) -> *mut FunctionPropertiesRefFFI; #[cxx_name = "function_load"] - fn function_load_ffi(name: &CxxWString, parser: Pin<&mut Parser>) -> bool; + fn function_load_ffi(name: &CxxWString, parser: &Parser) -> bool; #[cxx_name = "function_set_desc"] - fn function_set_desc_ffi(name: &CxxWString, desc: &CxxWString, parser: Pin<&mut Parser>); + fn function_set_desc_ffi(name: &CxxWString, desc: &CxxWString, parser: &Parser); #[cxx_name = "function_exists"] - fn function_exists_ffi(cmd: &CxxWString, parser: Pin<&mut Parser>) -> bool; - + fn function_exists_ffi(cmd: &CxxWString, parser: &Parser) -> bool; #[cxx_name = "function_exists_no_autoload"] fn function_exists_no_autoload_ffi(cmd: &CxxWString) -> bool; @@ -694,11 +685,7 @@ fn function_get_props_autoload_ffi( fn function_get_names_ffi(get_hidden: bool, out: Pin<&mut wcstring_list_ffi_t>); #[cxx_name = "function_copy"] - fn function_copy_ffi( - name: &CxxWString, - new_name: &CxxWString, - parser: Pin<&mut Parser>, - ) -> bool; + fn function_copy_ffi(name: &CxxWString, new_name: &CxxWString, parser: &Parser) -> bool; #[cxx_name = "function_invalidate_path"] fn invalidate_path(); diff --git a/fish-rust/src/future_feature_flags.rs b/fish-rust/src/future_feature_flags.rs index 2014bb074..76fd8adb8 100644 --- a/fish-rust/src/future_feature_flags.rs +++ b/fish-rust/src/future_feature_flags.rs @@ -129,7 +129,7 @@ pub fn test(flag: FeatureFlag) -> bool { /// Set a flag. #[cfg(any(test, feature = "fish-ffi-tests"))] -fn set(flag: FeatureFlag, value: bool) { +pub fn set(flag: FeatureFlag, value: bool) { LOCAL_FEATURES.with(|fc| fc.borrow().as_ref().unwrap_or(&FEATURES).set(flag, value)); } diff --git a/fish-rust/src/global_safety.rs b/fish-rust/src/global_safety.rs index f3c8788ff..963df1f6e 100644 --- a/fish-rust/src/global_safety.rs +++ b/fish-rust/src/global_safety.rs @@ -1,9 +1,10 @@ use crate::flog::FLOG; -use std::cell::{Ref, RefMut}; +use std::cell::{Ref, RefCell, RefMut}; +use std::rc::{Rc, Weak}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::MutexGuard; -#[derive(Debug)] +#[derive(Debug, Default)] pub struct RelaxedAtomicBool(AtomicBool); impl RelaxedAtomicBool { @@ -27,6 +28,30 @@ fn clone(&self) -> Self { } } +pub struct SharedFromThisBase { + weak: RefCell>, +} + +impl SharedFromThisBase { + pub fn new() -> SharedFromThisBase { + SharedFromThisBase { + weak: RefCell::new(Weak::new()), + } + } + + pub fn initialize(&self, r: &Rc) { + *self.weak.borrow_mut() = Rc::downgrade(r); + } +} + +pub trait SharedFromThis { + fn get_base(&self) -> &SharedFromThisBase; + + fn shared_from_this(&self) -> Rc { + self.get_base().weak.borrow().upgrade().unwrap() + } +} + pub struct DebugRef<'a, T>(Ref<'a, T>); impl<'a, T> DebugRef<'a, T> { diff --git a/fish-rust/src/highlight.rs b/fish-rust/src/highlight.rs index eb0c3fa65..edc9de95a 100644 --- a/fish-rust/src/highlight.rs +++ b/fish-rust/src/highlight.rs @@ -1,81 +1,1467 @@ +//! Functions for syntax highlighting. +use crate::abbrs::{self, with_abbrs}; use crate::ast::{ - Argument, Ast, BlockStatement, BlockStatementHeaderVariant, DecoratedStatement, Keyword, Node, - NodeFfi, NodeVisitor, Redirection, Token, Type, VariableAssignment, + self, Argument, Ast, BlockStatement, BlockStatementHeaderVariant, DecoratedStatement, Keyword, + Leaf, List, Node, NodeVisitor, Redirection, Token, Type, VariableAssignment, }; -use crate::ffi::highlighter_t; -use crate::parse_constants::ParseTokenType; +use crate::builtins::shared::builtin_exists; +use crate::color::{self, RgbColor}; +use crate::common::{ + unescape_string_in_place, valid_var_name, valid_var_name_char, UnescapeFlags, ASCII_MAX, + EXPAND_RESERVED_BASE, EXPAND_RESERVED_END, +}; +use crate::compat::_PC_CASE_SENSITIVE; +use crate::env::{EnvStackRefFFI, Environment}; +use crate::expand::{ + expand_one, expand_tilde, expand_to_command_and_args, ExpandFlags, ExpandResultCode, + HOME_DIRECTORY, PROCESS_EXPAND_SELF_STR, +}; +use crate::expand::{ + BRACE_BEGIN, BRACE_END, BRACE_SEP, INTERNAL_SEPARATOR, PROCESS_EXPAND_SELF, VARIABLE_EXPAND, + VARIABLE_EXPAND_SINGLE, +}; +use crate::ffi::rgb_color_t; +use crate::function; +use crate::future_feature_flags::{feature_test, FeatureFlag}; +use crate::history::{all_paths_are_valid, HistoryItem}; +use crate::operation_context::OperationContext; +use crate::output::{parse_color, Outputter}; +use crate::parse_constants::{ + ParseKeyword, ParseTokenType, ParseTreeFlags, SourceRange, StatementDecoration, +}; +use crate::parse_util::{parse_util_locate_cmdsubst_range, parse_util_slice_length}; +use crate::path::{ + path_apply_working_directory, path_as_implicit_cd, path_get_cdpath, path_get_path, + paths_are_same_file, +}; +use crate::redirection::RedirectionMode; +use crate::threads::assert_is_background_thread; +use crate::tokenizer::{variable_assignment_equals_pos, PipeOrRedir}; +use crate::wchar::{wstr, WString, L}; +use crate::wchar_ext::WExt; +use crate::wchar_ffi::AsWstr; +use crate::wcstringutil::{ + string_prefixes_string, string_prefixes_string_case_insensitive, string_suffixes_string, +}; +use crate::wildcard::{ANY_CHAR, ANY_STRING, ANY_STRING_RECURSIVE}; +use crate::wutil::dir_iter::DirIter; +use crate::wutil::fish_wcstoi; +use crate::wutil::{normalize_path, waccess, wstat}; +use crate::wutil::{wbasename, wdirname}; +use bitflags::bitflags; +use cxx::{CxxWString, SharedPtr}; +use libc::{ENOENT, PATH_MAX, R_OK, W_OK}; +use std::collections::hash_map::Entry; +use std::collections::{HashMap, HashSet}; +use std::os::fd::RawFd; use std::pin::Pin; -struct Highlighter<'a> { - companion: Pin<&'a mut highlighter_t>, - ast: &'a Ast, +impl HighlightSpec { + pub fn new() -> Self { + Self::default() + } + pub fn with_fg(fg: HighlightRole) -> Self { + Self::with_fg_bg(fg, HighlightRole::normal) + } + pub fn with_fg_bg(fg: HighlightRole, bg: HighlightRole) -> Self { + Self { + foreground: fg, + background: bg, + ..Default::default() + } + } + pub fn with_bg(bg: HighlightRole) -> Self { + Self::with_fg_bg(HighlightRole::normal, bg) + } } -impl<'a> Highlighter<'a> { + +/// Given a string and list of colors of the same size, return the string with ANSI escape sequences +/// representing the colors. +pub fn colorize(text: &wstr, colors: &[HighlightSpec], vars: &dyn Environment) -> Vec { + assert!(colors.len() == text.len()); + let mut rv = HighlightColorResolver::new(); + let mut outp = Outputter::new_buffering(); + + let mut last_color = HighlightSpec::with_fg(HighlightRole::normal); + for (i, c) in text.chars().enumerate() { + let color = colors[i]; + if color != last_color { + outp.set_color(rv.resolve_spec(&color, false, vars), RgbColor::NORMAL); + last_color = color; + } + outp.writech(c); + } + outp.set_color(RgbColor::NORMAL, RgbColor::NORMAL); + outp.contents().to_owned() // TODO should move +} + +/// Perform syntax highlighting for the shell commands in buff. The result is stored in the color +/// array as a color_code from the HIGHLIGHT_ enum for each character in buff. +/// +/// \param buffstr The buffer on which to perform syntax highlighting +/// \param color The array in which to store the color codes. The first 8 bits are used for fg +/// color, the next 8 bits for bg color. +/// \param ctx The variables and cancellation check for this operation. +/// \param io_ok If set, allow IO which may block. This means that e.g. invalid commands may be +/// detected. +/// \param cursor The position of the cursor in the commandline. +pub fn highlight_shell( + buff: &wstr, + color: &mut Vec, + ctx: &OperationContext<'_>, + io_ok: bool, /* = false */ + cursor: Option, +) { + let working_directory = ctx.vars().get_pwd_slash(); + let mut highlighter = Highlighter::new(buff, cursor, ctx, working_directory, io_ok); + *color = highlighter.highlight(); +} + +/// highlight_color_resolver_t resolves highlight specs (like "a command") to actual RGB colors. +/// It maintains a cache with no invalidation mechanism. The lifetime of these should typically be +/// one screen redraw. +#[derive(Default)] +pub struct HighlightColorResolver { + fg_cache: HashMap, + bg_cache: HashMap, +} + +/// highlight_color_resolver_t resolves highlight specs (like "a command") to actual RGB colors. +/// It maintains a cache with no invalidation mechanism. The lifetime of these should typically be +/// one screen redraw. +impl HighlightColorResolver { + fn new() -> Self { + Default::default() + } + /// \return an RGB color for a given highlight spec. + fn resolve_spec( + &mut self, + highlight: &HighlightSpec, + is_background: bool, + vars: &dyn Environment, + ) -> RgbColor { + let cache = if is_background { + &mut self.bg_cache + } else { + &mut self.fg_cache + }; + match cache.entry(*highlight) { + Entry::Occupied(e) => *e.get(), + Entry::Vacant(e) => { + let color = Self::resolve_spec_uncached(highlight, is_background, vars); + e.insert(color); + color + } + } + } + pub fn resolve_spec_uncached( + highlight: &HighlightSpec, + is_background: bool, + vars: &dyn Environment, + ) -> RgbColor { + let mut result = RgbColor::NORMAL; + let role = if is_background { + highlight.background + } else { + highlight.foreground + }; + + let var = vars + .get_unless_empty(get_highlight_var_name(role)) + .or_else(|| vars.get_unless_empty(get_highlight_var_name(get_fallback(role)))) + .or_else(|| vars.get(get_highlight_var_name(HighlightRole::normal))); + if let Some(var) = var { + result = parse_color(&var, is_background); + } + + // Handle modifiers. + if !is_background && highlight.valid_path { + if let Some(var2) = vars.get(L!("fish_color_valid_path")) { + let result2 = parse_color(&var2, is_background); + if result.is_normal() { + result = result2; + } else if !result2.is_normal() { + // Valid path has an actual color, use it and merge the modifiers. + let mut rescol = result2; + rescol.set_bold(result.is_bold() || result2.is_bold()); + rescol.set_underline(result.is_underline() || result2.is_underline()); + rescol.set_italics(result.is_italics() || result2.is_italics()); + rescol.set_dim(result.is_dim() || result2.is_dim()); + rescol.set_reverse(result.is_reverse() || result2.is_reverse()); + result = rescol; + } else { + if result2.is_bold() { + result.set_bold(true) + }; + if result2.is_underline() { + result.set_underline(true) + }; + if result2.is_italics() { + result.set_italics(true) + }; + if result2.is_dim() { + result.set_dim(true) + }; + if result2.is_reverse() { + result.set_reverse(true) + }; + } + } + } + + if !is_background && highlight.force_underline { + result.set_underline(true); + } + + result + } +} + +fn command_is_valid( + cmd: &wstr, + decoration: StatementDecoration, + working_directory: &wstr, + vars: &dyn Environment, +) -> bool { + // Determine which types we check, based on the decoration. + let mut builtin_ok = true; + let mut function_ok = true; + let mut abbreviation_ok = true; + let mut command_ok = true; + let mut implicit_cd_ok = true; + if matches!( + decoration, + StatementDecoration::command | StatementDecoration::exec + ) { + builtin_ok = false; + function_ok = false; + abbreviation_ok = false; + command_ok = true; + implicit_cd_ok = false; + } else if decoration == StatementDecoration::builtin { + builtin_ok = true; + function_ok = false; + abbreviation_ok = false; + command_ok = false; + implicit_cd_ok = false; + } + + // Check them. + let mut is_valid = false; + + // Builtins + if !is_valid && builtin_ok { + is_valid = builtin_exists(cmd) + }; + + // Functions + if !is_valid && function_ok { + is_valid = function::exists_no_autoload(cmd) + }; + + // Abbreviations + if !is_valid && abbreviation_ok { + is_valid = with_abbrs(|set| set.has_match(cmd, abbrs::Position::Command)) + }; + + // Regular commands + if !is_valid && command_ok { + is_valid = path_get_path(cmd, vars).is_some() + }; + + // Implicit cd + if !is_valid && implicit_cd_ok { + is_valid = path_as_implicit_cd(cmd, working_directory, vars).is_some(); + } + + // Return what we got. + return is_valid; +} + +fn has_expand_reserved(s: &wstr) -> bool { + for wc in s.chars() { + if (EXPAND_RESERVED_BASE..=EXPAND_RESERVED_END).contains(&wc) { + return true; + } + } + false +} + +// Parse a command line. Return by reference the first command, and the first argument to that +// command (as a string), if any. This is used to validate autosuggestions. +fn autosuggest_parse_command( + buff: &wstr, + ctx: &OperationContext<'_>, +) -> Option<(WString, WString)> { + let ast = Ast::parse( + buff, + ParseTreeFlags::CONTINUE_AFTER_ERROR | ParseTreeFlags::ACCEPT_INCOMPLETE_TOKENS, + None, + ); + + // Find the first statement. + let Some(jc) = ast.top().as_job_list().unwrap().get(0) else { + return None; + }; + let Some(first_statement) = jc.job.statement.contents.as_decorated_statement() else { + return None; + }; + + if let Some(expanded_command) = statement_get_expanded_command(buff, first_statement, ctx) { + let mut arg = WString::new(); + // Check if the first argument or redirection is, in fact, an argument. + if let Some(arg_or_redir) = first_statement.args_or_redirs.get(0) { + if arg_or_redir.is_argument() { + arg = arg_or_redir.argument().source(buff).to_owned(); + } + } + + return Some((expanded_command, arg)); + } + + None +} + +/// Given an item \p item from the history which is a proposed autosuggestion, return whether the +/// autosuggestion is valid. It may not be valid if e.g. it is attempting to cd into a directory +/// which does not exist. +pub fn autosuggest_validate_from_history( + item: &HistoryItem, + working_directory: &wstr, + ctx: &OperationContext<'_>, +) -> bool { + assert_is_background_thread(); + + // Parse the string. + let Some((parsed_command, mut cd_dir)) = autosuggest_parse_command(item.str(), ctx) else { + // This is for autosuggestions which are not decorated commands, e.g. function declarations. + return true; + }; + + // We handle cd specially. + if parsed_command == L!("cd") && !cd_dir.is_empty() { + if expand_one(&mut cd_dir, ExpandFlags::SKIP_CMDSUBST, ctx, None) { + if string_prefixes_string(&cd_dir, L!("--help")) + || string_prefixes_string(&cd_dir, L!("-h")) + { + // cd --help is always valid. + return true; + } else { + // Check the directory target, respecting CDPATH. + // Permit the autosuggestion if the path is valid and not our directory. + let path = path_get_cdpath(&cd_dir, working_directory, ctx.vars()); + return path + .map(|p| !paths_are_same_file(working_directory, &p)) + .unwrap_or(false); + } + } + } + + // Not handled specially. Is the command valid? + let cmd_ok = builtin_exists(&parsed_command) + || function::exists_no_autoload(&parsed_command) + || path_get_path(&parsed_command, ctx.vars()).is_some(); + if !cmd_ok { + return false; + } + + // Did the historical command have arguments that look like paths, which aren't paths now? + let paths = item.get_required_paths(); + if !all_paths_are_valid(paths.iter().cloned(), ctx) { + return false; + } + + true +} + +// Highlights the variable starting with 'in', setting colors within the 'colors' array. Returns the +// number of characters consumed. +fn color_variable(inp: &wstr, colors: &mut [HighlightSpec]) -> usize { + assert!(inp.char_at(0) == '$'); + + // Handle an initial run of $s. + let mut idx = 0; + let mut dollar_count = 0; + while inp.char_at(idx) == '$' { + // Our color depends on the next char. + let next = inp.char_at(idx + 1); + if next == '$' || valid_var_name_char(next) { + colors[idx] = HighlightSpec::with_fg(HighlightRole::operat); + } else if next == '(' { + colors[idx] = HighlightSpec::with_fg(HighlightRole::operat); + return idx + 1; + } else { + colors[idx] = HighlightSpec::with_fg(HighlightRole::error); + } + idx += 1; + dollar_count += 1; + } + + // Handle a sequence of variable characters. + // It may contain an escaped newline - see #8444. + loop { + if valid_var_name_char(inp.char_at(idx)) { + colors[idx] = HighlightSpec::with_fg(HighlightRole::operat); + idx += 1; + } else if inp.char_at(idx) == '\\' && inp.char_at(idx + 1) == '\n' { + colors[idx] = HighlightSpec::with_fg(HighlightRole::operat); + idx += 1; + colors[idx] = HighlightSpec::with_fg(HighlightRole::operat); + idx += 1; + } else { + break; + } + } + + // Handle a slice, up to dollar_count of them. Note that we currently don't do any validation of + // the slice's contents, e.g. $foo[blah] will not show an error even though it's invalid. + for _slice_count in 0..dollar_count { + match parse_util_slice_length(&inp[idx..]) { + Some(slice_len) if slice_len > 0 => { + colors[idx] = HighlightSpec::with_fg(HighlightRole::operat); + colors[idx + slice_len - 1] = HighlightSpec::with_fg(HighlightRole::operat); + idx += slice_len; + } + Some(_slice_len) => { + // not a slice + break; + } + None => { + // Syntax error. Normally the entire token is colored red for us, but inside a + // double-quoted string that doesn't happen. As such, color the variable + the slice + // start red. Coloring any more than that looks bad, unless we're willing to try and + // detect where the double-quoted string ends, and I'd rather not do that. + colors[..idx + 1].fill(HighlightSpec::with_fg(HighlightRole::error)); + break; + } + } + } + idx +} + +/// This function is a disaster badly in need of refactoring. It colors an argument or command, +/// without regard to command substitutions. +fn color_string_internal(buffstr: &wstr, base_color: HighlightSpec, colors: &mut [HighlightSpec]) { + // Clarify what we expect. + assert!( + [ + HighlightSpec::with_fg(HighlightRole::param), + HighlightSpec::with_fg(HighlightRole::option), + HighlightSpec::with_fg(HighlightRole::command) + ] + .contains(&base_color), + "Unexpected base color" + ); + let buff_len = buffstr.len(); + colors.fill(base_color); + + // Hacky support for %self which must be an unquoted literal argument. + if buffstr == PROCESS_EXPAND_SELF_STR { + colors[..PROCESS_EXPAND_SELF_STR.len()].fill(HighlightSpec::with_fg(HighlightRole::operat)); + return; + } + + #[derive(Eq, PartialEq)] + enum Mode { + unquoted, + single_quoted, + double_quoted, + } + let mut mode = Mode::unquoted; + let mut unclosed_quote_offset = None; + let mut bracket_count = 0; + let mut in_pos = 0; + while in_pos < buff_len { + let c = buffstr.as_char_slice()[in_pos]; + match mode { + Mode::unquoted => { + if c == '\\' { + let mut fill_color = HighlightRole::escape; // may be set to highlight_error + let backslash_pos = in_pos; + let mut fill_end = backslash_pos; + + // Move to the escaped character. + in_pos += 1; + let escaped_char = if in_pos < buff_len { + buffstr.as_char_slice()[in_pos] + } else { + '\0' + }; + + if escaped_char == '\0' { + fill_end = in_pos; + fill_color = HighlightRole::error; + } else if matches!(escaped_char, '~' | '%') { + if in_pos == 1 { + fill_end = in_pos + 1; + } + } else if escaped_char == ',' { + if bracket_count != 0 { + fill_end = in_pos + 1; + } + } else if "abefnrtv*?$(){}[]'\"<>^ \\#;|&".contains(escaped_char) { + fill_end = in_pos + 1; + } else if escaped_char == 'c' { + // Like \ci. So highlight three characters. + fill_end = in_pos + 1; + } else if "uUxX01234567".contains(escaped_char) { + let mut res = 0; + let mut chars = 2; + let mut base = 16; + let mut max_val = ASCII_MAX; + + match escaped_char { + 'u' => { + chars = 4; + max_val = '\u{FFFF}'; // UCS2_MAX + in_pos += 1; + } + 'U' => { + chars = 8; + // Don't exceed the largest Unicode code point - see #1107. + max_val = '\u{10FFFF}'; + in_pos += 1; + } + 'x' | 'X' => { + max_val = 0xFF as char; + in_pos += 1; + } + _ => { + // a digit like \12 + base = 8; + chars = 3; + } + } + + // Consume + for _i in 0..chars { + if in_pos == buff_len { + break; + } + let Some(d) = buffstr.as_char_slice()[in_pos].to_digit(base) else { + break; + }; + res = (res * base) + d; + in_pos += 1; + } + // in_pos is now at the first character that could not be converted (or + // buff_len). + assert!((backslash_pos..=buff_len).contains(&in_pos)); + fill_end = in_pos; + + // It's an error if we exceeded the max value. + if res > u32::from(max_val) { + fill_color = HighlightRole::error; + } + + // Subtract one from in_pos, so that the increment in the loop will move to + // the next character. + in_pos -= 1; + } + assert!(fill_end >= backslash_pos); + colors[backslash_pos..fill_end].fill(HighlightSpec::with_fg(fill_color)); + } else { + // Not a backslash. + match c { + '~' => { + if in_pos == 0 { + colors[in_pos] = HighlightSpec::with_fg(HighlightRole::operat); + } + } + '$' => { + assert!(in_pos < buff_len); + in_pos += color_variable(&buffstr[in_pos..], &mut colors[in_pos..]); + // Subtract one to account for the upcoming loop increment. + in_pos -= 1; + } + '?' => { + if !feature_test(FeatureFlag::qmark_noglob) { + colors[in_pos] = HighlightSpec::with_fg(HighlightRole::operat); + } + } + '*' | '(' | ')' => { + colors[in_pos] = HighlightSpec::with_fg(HighlightRole::operat); + } + '{' => { + colors[in_pos] = HighlightSpec::with_fg(HighlightRole::operat); + bracket_count += 1; + } + '}' => { + colors[in_pos] = HighlightSpec::with_fg(HighlightRole::operat); + bracket_count -= 1; + } + ',' => { + if bracket_count > 0 { + colors[in_pos] = HighlightSpec::with_fg(HighlightRole::operat); + } + } + '\'' => { + colors[in_pos] = HighlightSpec::with_fg(HighlightRole::quote); + unclosed_quote_offset = Some(in_pos); + mode = Mode::single_quoted; + } + '"' => { + colors[in_pos] = HighlightSpec::with_fg(HighlightRole::quote); + unclosed_quote_offset = Some(in_pos); + mode = Mode::double_quoted; + } + _ => (), // we ignore all other characters + } + } + } + // Mode 1 means single quoted string, i.e 'foo'. + Mode::single_quoted => { + colors[in_pos] = HighlightSpec::with_fg(HighlightRole::quote); + if c == '\\' { + // backslash + if in_pos + 1 < buff_len { + let escaped_char = buffstr.as_char_slice()[in_pos + 1]; + if matches!(escaped_char, '\\' | '\'') { + colors[in_pos] = HighlightSpec::with_fg(HighlightRole::escape); // backslash + colors[in_pos + 1] = HighlightSpec::with_fg(HighlightRole::escape); // escaped char + in_pos += 1; // skip over backslash + } + } + } else if c == '\'' { + mode = Mode::unquoted; + } + } + // Mode 2 means double quoted string, i.e. "foo". + Mode::double_quoted => { + // Slices are colored in advance, past `in_pos`, and we don't want to overwrite + // that. + if colors[in_pos] == base_color { + colors[in_pos] = HighlightSpec::with_fg(HighlightRole::quote); + } + match c { + '"' => { + mode = Mode::unquoted; + } + '\\' => { + // Backslash + if in_pos + 1 < buff_len { + let escaped_char = buffstr.as_char_slice()[in_pos + 1]; + if matches!(escaped_char, '\\' | '"' | '\n' | '$') { + colors[in_pos] = HighlightSpec::with_fg(HighlightRole::escape); // backslash + colors[in_pos + 1] = HighlightSpec::with_fg(HighlightRole::escape); // escaped char + in_pos += 1; // skip over backslash + } + } + } + '$' => { + in_pos += color_variable(&buffstr[in_pos..], &mut colors[in_pos..]); + // Subtract one to account for the upcoming increment in the loop. + in_pos -= 1; + } + _ => (), // we ignore all other characters + } + } + } + in_pos += 1; + } + + // Error on unclosed quotes. + if mode != Mode::unquoted { + colors[unclosed_quote_offset.unwrap()] = HighlightSpec::with_fg(HighlightRole::error); + } +} + +/// Indicates whether the source range of the given node forms a valid path in the given +/// working_directory. +fn range_is_potential_path( + src: &wstr, + range: SourceRange, + at_cursor: bool, + ctx: &OperationContext, + working_directory: &wstr, +) -> bool { + // Skip strings exceeding PATH_MAX. See #7837. + // Note some paths may exceed PATH_MAX, but this is just for highlighting. + if range.length() > (PATH_MAX as usize) { + return false; + } + // Get the node source, unescape it, and then pass it to is_potential_path along with the + // working directory (as a one element list). + let mut result = false; + let mut token = (src[range.start()..range.end()]).to_owned(); + if unescape_string_in_place( + &mut token, + crate::common::UnescapeStringStyle::Script(UnescapeFlags::SPECIAL), + ) { + // Big hack: is_potential_path expects a tilde, but unescape_string gives us HOME_DIRECTORY. + // Put it back. + if token.char_at(0) == HOME_DIRECTORY { + token.as_char_slice_mut()[0] = '~'; + } + + result = is_potential_path( + &token, + at_cursor, + &[working_directory.to_owned()], + ctx, + PathFlags::PATH_EXPAND_TILDE, + ); + } + result +} + +// Tests whether the specified string cpath is the prefix of anything we could cd to. directories is +// a list of possible parent directories (typically either the working directory, or the cdpath). +// This does I/O! +// +// This is used only internally to this file, and is exposed only for testing. +bitflags! { + #[derive(Clone, Copy, Default)] + pub struct PathFlags: u8 { + // The path must be to a directory. + const PATH_REQUIRE_DIR = 1 << 0; + // Expand any leading tilde in the path. + const PATH_EXPAND_TILDE = 1 << 1; + // Normalize directories before resolving, as "cd". + const PATH_FOR_CD = 1 << 2; + } +} + +/// Tests whether the specified string cpath is the prefix of anything we could cd to. directories +/// is a list of possible parent directories (typically either the working directory, or the +/// cdpath). This does I/O! +/// +/// Hack: if out_suggested_cdpath is not NULL, it returns the autosuggestion for cd. This descends +/// the deepest unique directory hierarchy. +/// +/// We expect the path to already be unescaped. +pub fn is_potential_path( + potential_path_fragment: &wstr, + at_cursor: bool, + directories: &[WString], + ctx: &OperationContext<'_>, + flags: PathFlags, +) -> bool { + assert_is_background_thread(); + + if ctx.check_cancel() { + return false; + } + + let require_dir = flags.contains(PathFlags::PATH_REQUIRE_DIR); + let mut clean_potential_path_fragment = WString::new(); + let mut has_magic = false; + + let mut path_with_magic = potential_path_fragment.to_owned(); + if flags.contains(PathFlags::PATH_EXPAND_TILDE) { + expand_tilde(&mut path_with_magic, ctx.vars()); + } + + for c in path_with_magic.chars() { + match c { + PROCESS_EXPAND_SELF + | VARIABLE_EXPAND + | VARIABLE_EXPAND_SINGLE + | BRACE_BEGIN + | BRACE_END + | BRACE_SEP + | ANY_CHAR + | ANY_STRING + | ANY_STRING_RECURSIVE => { + has_magic = true; + } + INTERNAL_SEPARATOR => (), + _ => clean_potential_path_fragment.push(c), + } + } + + if has_magic || clean_potential_path_fragment.is_empty() { + return false; + } + + // Don't test the same path multiple times, which can happen if the path is absolute and the + // CDPATH contains multiple entries. + let mut checked_paths = HashSet::new(); + + // Keep a cache of which paths / filesystems are case sensitive. + let mut case_sensitivity_cache = CaseSensitivityCache::new(); + + for wd in directories { + if ctx.check_cancel() { + return false; + } + let mut abs_path = path_apply_working_directory(&clean_potential_path_fragment, wd); + let must_be_full_dir = abs_path.chars().last() == Some('/'); + if flags.contains(PathFlags::PATH_FOR_CD) { + abs_path = normalize_path(&abs_path, /*allow_leading_double_slashes=*/ true); + } + + // Skip this if it's empty or we've already checked it. + if abs_path.is_empty() || checked_paths.contains(&abs_path) { + continue; + } + checked_paths.insert(abs_path.clone()); + + // If the user is still typing the argument, we want to highlight it if it's the prefix + // of a valid path. This means we need to potentially walk all files in some directory. + // There are two easy cases where we can skip this: + // 1. If the argument ends with a slash, it must be a valid directory, no prefix. + // 2. If the cursor is not at the argument, it means the user is definitely not typing it, + // so we can skip the prefix-match. + if must_be_full_dir || !at_cursor { + if let Ok(md) = wstat(&abs_path) { + if !at_cursor || md.file_type().is_dir() { + return true; + } + } + } else { + // We do not end with a slash; it does not have to be a directory. + let dir_name = wdirname(&abs_path); + let filename_fragment = wbasename(&abs_path); + if dir_name == L!("/") && filename_fragment == L!("/") { + // cd ///.... No autosuggestion. + return true; + } + + if let Ok(mut dir) = DirIter::new(dir_name) { + // Check if we're case insensitive. + let do_case_insensitive = + fs_is_case_insensitive(dir_name, dir.fd(), &mut case_sensitivity_cache); + + // We opened the dir_name; look for a string where the base name prefixes it. + while let Some(entry) = dir.next() { + let Ok(entry) = entry else { continue }; + if ctx.check_cancel() { + return false; + } + + // Maybe skip directories. + if require_dir && !entry.is_dir() { + continue; + } + + if string_prefixes_string(filename_fragment, &entry.name) + || (do_case_insensitive + && string_prefixes_string_case_insensitive( + filename_fragment, + &entry.name, + )) + { + return true; + } + } + } + } + } + false +} + +// Given a string, return whether it prefixes a path that we could cd into. Return that path in +// out_path. Expects path to be unescaped. +fn is_potential_cd_path( + path: &wstr, + at_cursor: bool, + working_directory: &wstr, + ctx: &OperationContext<'_>, + flags: PathFlags, +) -> bool { + let mut directories = vec![]; + + if string_prefixes_string(L!("./"), path) { + // Ignore the CDPATH in this case; just use the working directory. + directories.push(working_directory.to_owned()); + } else { + // Get the CDPATH. + let cdpath = ctx.vars().get_unless_empty(L!("CDPATH")); + let mut pathsv = match cdpath { + None => vec![L!(".").to_owned()], + Some(cdpath) => cdpath.as_list().to_vec(), + }; + // The current $PWD is always valid. + pathsv.push(L!(".").to_owned()); + + for mut next_path in pathsv { + if next_path.is_empty() { + next_path = L!(".").to_owned(); + } + // Ensure that we use the working directory for relative cdpaths like ".". + directories.push(path_apply_working_directory(&next_path, working_directory)); + } + } + + // Call is_potential_path with all of these directories. + is_potential_path( + path, + at_cursor, + &directories, + ctx, + flags | PathFlags::PATH_REQUIRE_DIR | PathFlags::PATH_FOR_CD, + ) +} + +pub type ColorArray = Vec; + +/// Syntax highlighter helper. +struct Highlighter<'s> { + // The string we're highlighting. Note this is a reference member variable (to avoid copying)! + buff: &'s wstr, + // The position of the cursor within the string. + cursor: Option, + // The operation context. + ctx: &'s OperationContext<'s>, + // Whether it's OK to do I/O. + io_ok: bool, + // Working directory. + working_directory: WString, + // The resulting colors. + color_array: ColorArray, + // A stack of variables that the current commandline probably defines. We mark redirections + // as valid if they use one of these variables, to avoid marking valid targets as error. + // TODO This should be &'s wstr + pending_variables: Vec, + done: bool, +} + +impl<'s> Highlighter<'s> { + pub fn new( + buff: &'s wstr, + cursor: Option, + ctx: &'s OperationContext<'s>, + working_directory: WString, + can_do_io: bool, + ) -> Self { + Self { + buff, + cursor, + ctx, + io_ok: can_do_io, + working_directory, + color_array: vec![], + pending_variables: vec![], + done: false, + } + } + + pub fn highlight(&mut self) -> ColorArray { + assert!(!self.done); + self.done = true; + + // If we are doing I/O, we must be in a background thread. + if self.io_ok { + assert_is_background_thread(); + } + + self.color_array + .resize(self.buff.len(), HighlightSpec::default()); + + // Flags we use for AST parsing. + let ast_flags = ParseTreeFlags::CONTINUE_AFTER_ERROR + | ParseTreeFlags::INCLUDE_COMMENTS + | ParseTreeFlags::ACCEPT_INCOMPLETE_TOKENS + | ParseTreeFlags::LEAVE_UNTERMINATED + | ParseTreeFlags::SHOW_EXTRA_SEMIS; + let ast = Ast::parse(self.buff, ast_flags, None); + + self.visit_children(ast.top()); + if self.ctx.check_cancel() { + return std::mem::take(&mut self.color_array); + } + + // Color every comment. + let extras = &ast.extras; + for range in &extras.comments { + self.color_range(*range, HighlightSpec::with_fg(HighlightRole::comment)); + } + + // Color every extra semi. + for range in &extras.semis { + self.color_range( + *range, + HighlightSpec::with_fg(HighlightRole::statement_terminator), + ); + } + + // Color every error range. + for range in &extras.errors { + self.color_range(*range, HighlightSpec::with_fg(HighlightRole::error)); + } + + std::mem::take(&mut self.color_array) + } + + /// \return a substring of our buffer. + pub fn get_source(&self, r: SourceRange) -> &'s wstr { + assert!(r.end() >= r.start(), "Overflow"); + assert!(r.end() <= self.buff.len(), "Out of range"); + &self.buff[r.start()..r.end()] + } + + fn io_still_ok(&self) -> bool { + self.io_ok && !self.ctx.check_cancel() + } + + // Color a command. + fn color_command(&mut self, node: &ast::String_) { + let source_range = node.source_range(); + let cmd_str = self.get_source(source_range); + + let arg_start = source_range.start(); + color_string_internal( + cmd_str, + HighlightSpec::with_fg(HighlightRole::command), + &mut self.color_array[arg_start..], + ); + } + // Color a node as if it were an argument. + fn color_as_argument(&mut self, node: &dyn ast::Node, options_allowed: bool /* = true */) { + // node does not necessarily have type symbol_argument here. + let source_range = node.source_range(); + let arg_str = self.get_source(source_range); + + let arg_start = source_range.start(); + + // Color this argument without concern for command substitutions. + if options_allowed && arg_str.char_at(0) == '-' { + color_string_internal( + arg_str, + HighlightSpec::with_fg(HighlightRole::option), + &mut self.color_array[arg_start..], + ); + } else { + color_string_internal( + arg_str, + HighlightSpec::with_fg(HighlightRole::param), + &mut self.color_array[arg_start..], + ); + } + + // Now do command substitutions. + let mut cmdsub_cursor = 0; + let mut cmdsub_start = 0; + let mut cmdsub_end = 0; + let mut cmdsub_contents = L!(""); + let mut is_quoted = false; + + while parse_util_locate_cmdsubst_range( + arg_str, + &mut cmdsub_cursor, + Some(&mut cmdsub_contents), + &mut cmdsub_start, + &mut cmdsub_end, + /*accept_incomplete=*/ true, + Some(&mut is_quoted), + None, + ) > 0 + { + // The cmdsub_start is the open paren. cmdsub_end is either the close paren or the end of + // the string. cmdsub_contents extends from one past cmdsub_start to cmdsub_end. + assert!(cmdsub_end > cmdsub_start); + assert!(cmdsub_end - cmdsub_start - 1 == cmdsub_contents.len()); + + // Found a command substitution. Compute the position of the start and end of the cmdsub + // contents, within our overall src. + let arg_subcmd_start = arg_start + cmdsub_start; + let arg_subcmd_end = arg_start + cmdsub_end; + + // Highlight the parens. The open paren must exist; the closed paren may not if it was + // incomplete. + assert!(cmdsub_start < arg_str.len()); + self.color_array[arg_subcmd_start] = HighlightSpec::with_fg(HighlightRole::operat); + if arg_subcmd_end < self.buff.len() { + self.color_array[arg_subcmd_end] = HighlightSpec::with_fg(HighlightRole::operat); + } + + // Highlight it recursively. + let arg_cursor = self.cursor.map(|c| c.wrapping_sub(arg_subcmd_start)); + let mut cmdsub_highlighter = Highlighter::new( + cmdsub_contents, + arg_cursor, + self.ctx, + self.working_directory.clone(), + self.io_still_ok(), + ); + let subcolors = cmdsub_highlighter.highlight(); + + // Copy out the subcolors back into our array. + assert!(subcolors.len() == cmdsub_contents.len()); + self.color_array[arg_subcmd_start + 1..arg_subcmd_end].copy_from_slice(&subcolors); + } + } + // Colors the source range of a node with a given color. + fn color_node(&mut self, node: &dyn ast::Node, color: HighlightSpec) { + self.color_range(node.source_range(), color) + } + // Colors a range with a given color. + fn color_range(&mut self, range: SourceRange, color: HighlightSpec) { + assert!(range.end() <= self.color_array.len(), "Range out of bounds"); + self.color_array[range.start()..range.end()].fill(color); + } + // Visit the children of a node. - fn visit_children(&mut self, node: &'a dyn Node) { + fn visit_children(&mut self, node: &dyn Node) { node.accept(self, false); } // AST visitor implementations. fn visit_keyword(&mut self, node: &dyn Keyword) { - let ffi_node = NodeFfi::new(node.leaf_as_node_ffi()); - self.companion - .as_mut() - .visit_keyword((&ffi_node as *const NodeFfi<'_>).cast()); + let mut role = HighlightRole::normal; + match node.keyword() { + ParseKeyword::kw_begin + | ParseKeyword::kw_builtin + | ParseKeyword::kw_case + | ParseKeyword::kw_command + | ParseKeyword::kw_else + | ParseKeyword::kw_end + | ParseKeyword::kw_exec + | ParseKeyword::kw_for + | ParseKeyword::kw_function + | ParseKeyword::kw_if + | ParseKeyword::kw_in + | ParseKeyword::kw_switch + | ParseKeyword::kw_while => role = HighlightRole::keyword, + ParseKeyword::kw_and + | ParseKeyword::kw_or + | ParseKeyword::kw_not + | ParseKeyword::kw_exclam + | ParseKeyword::kw_time => role = HighlightRole::operat, + ParseKeyword::none => (), + _ => panic!(), + }; + self.color_node(node.leaf_as_node(), HighlightSpec::with_fg(role)); } - fn visit_token(&mut self, node: &dyn Token) { - let ffi_node = NodeFfi::new(node.leaf_as_node_ffi()); - self.companion - .as_mut() - .visit_token((&ffi_node as *const NodeFfi<'_>).cast()); + fn visit_token(&mut self, tok: &dyn Token) { + let mut role = HighlightRole::normal; + match tok.token_type() { + ParseTokenType::end | ParseTokenType::pipe | ParseTokenType::background => { + role = HighlightRole::statement_terminator + } + ParseTokenType::andand | ParseTokenType::oror => role = HighlightRole::operat, + ParseTokenType::string => { + // Assume all strings are params. This handles e.g. the variables a for header or + // function header. Other strings (like arguments to commands) need more complex + // handling, which occurs in their respective overrides of visit(). + role = HighlightRole::param; + } + _ => (), + } + self.color_node(tok.leaf_as_node(), HighlightSpec::with_fg(role)); } - fn visit_argument(&mut self, node: &Argument) { - self.companion - .as_mut() - .visit_argument((node as *const Argument).cast(), false, true); + // Visit an argument, perhaps knowing that our command is cd. + fn visit_argument(&mut self, arg: &Argument, cmd_is_cd: bool, options_allowed: bool) { + self.color_as_argument(arg.as_node(), options_allowed); + if !self.io_still_ok() { + return; + } + // Underline every valid path. + let mut is_valid_path = false; + let at_cursor = self + .cursor + .map_or(false, |c| arg.source_range().contains_inclusive(c)); + if cmd_is_cd { + // Mark this as an error if it's not 'help' and not a valid cd path. + let mut param = arg.source(self.buff).to_owned(); + if expand_one(&mut param, ExpandFlags::SKIP_CMDSUBST, self.ctx, None) { + let is_help = string_prefixes_string(¶m, L!("--help")) + || string_prefixes_string(¶m, L!("-h")); + if !is_help { + is_valid_path = is_potential_cd_path( + ¶m, + at_cursor, + &self.working_directory, + self.ctx, + PathFlags::PATH_EXPAND_TILDE, + ); + if !is_valid_path { + self.color_node( + arg.as_node(), + HighlightSpec::with_fg(HighlightRole::error), + ); + } + } + } + } else if range_is_potential_path( + self.buff, + arg.range().unwrap(), + at_cursor, + self.ctx, + &self.working_directory, + ) { + is_valid_path = true; + } + if is_valid_path { + for i in arg.range().unwrap().start()..arg.range().unwrap().end() { + self.color_array[i].valid_path = true; + } + } } - fn visit_redirection(&mut self, node: &Redirection) { - self.companion - .as_mut() - .visit_redirection((node as *const Redirection).cast()); + fn visit_redirection(&mut self, redir: &Redirection) { + // like 2> + let oper = PipeOrRedir::try_from(redir.oper.source(self.buff)) + .expect("Should have successfully parsed a pipe_or_redir_t since it was in our ast"); + let mut target = redir.target.source(self.buff).to_owned(); // like &1 or file path + + // Color the > part. + // It may have parsed successfully yet still be invalid (e.g. 9999999999999>&1) + // If so, color the whole thing invalid and stop. + if !oper.is_valid() { + self.color_node( + redir.as_node(), + HighlightSpec::with_fg(HighlightRole::error), + ); + return; + } + + // Color the operator part like 2>. + self.color_node( + &redir.oper, + HighlightSpec::with_fg(HighlightRole::redirection), + ); + + // Color the target part. + // Check if the argument contains a command substitution. If so, highlight it as a param + // even though it's a command redirection, and don't try to do any other validation. + if has_cmdsub(&target) { + self.color_as_argument(redir.target.leaf_as_node(), true); + } else { + // No command substitution, so we can highlight the target file or fd. For example, + // disallow redirections into a non-existent directory. + let target_is_valid; + if !self.io_still_ok() { + // I/O is disallowed, so we don't have much hope of catching anything but gross + // errors. Assume it's valid. + target_is_valid = true; + } else if contains_pending_variable(&self.pending_variables, &target) { + target_is_valid = true; + } else if !expand_one(&mut target, ExpandFlags::SKIP_CMDSUBST, self.ctx, None) { + // Could not be expanded. + target_is_valid = false; + } else { + // Ok, we successfully expanded our target. Now verify that it works with this + // redirection. We will probably need it as a path (but not in the case of fd + // redirections). Note that the target is now unescaped. + let target_path = path_apply_working_directory(&target, &self.working_directory); + match oper.mode { + RedirectionMode::fd => { + if target == L!("-") { + target_is_valid = true; + } else { + target_is_valid = match fish_wcstoi(&target) { + Ok(fd) => fd >= 0, + Err(_) => false, + }; + } + } + RedirectionMode::input => { + // Input redirections must have a readable non-directory. + target_is_valid = waccess(&target_path, R_OK) == 0 + && match wstat(&target_path) { + Ok(md) => !md.file_type().is_dir(), + Err(_) => false, + }; + } + RedirectionMode::overwrite + | RedirectionMode::append + | RedirectionMode::noclob => { + // Test whether the file exists, and whether it's writable (possibly after + // creating it). access() returns failure if the file does not exist. + let file_exists; + let file_is_writable; + + if string_suffixes_string(L!("/"), &target) { + // Redirections to things that are directories is definitely not + // allowed. + file_exists = false; + file_is_writable = false; + } else { + match wstat(&target_path) { + Ok(md) => { + // No err. We can write to it if it's not a directory and we have + // permission. + file_exists = true; + file_is_writable = !md.file_type().is_dir() + && waccess(&target_path, W_OK) == 0; + } + Err(err) => { + if err.raw_os_error() == Some(ENOENT) { + // File does not exist. Check if its parent directory is writable. + let mut parent = wdirname(&target_path).to_owned(); + + // Ensure that the parent ends with the path separator. This will ensure + // that we get an error if the parent directory is not really a + // directory. + if !string_suffixes_string(L!("/"), &parent) { + parent.push('/'); + } + + // Now the file is considered writable if the parent directory is + // writable. + file_exists = false; + file_is_writable = waccess(&parent, W_OK) == 0; + } else { + // Other errors we treat as not writable. This includes things like + // ENOTDIR. + file_exists = false; + file_is_writable = false; + } + } + } + } + + // NOCLOB means that we must not overwrite files that exist. + target_is_valid = file_is_writable + && !(file_exists && oper.mode == RedirectionMode::noclob); + } + _ => panic!(), + } + } + self.color_node( + redir.target.leaf_as_node(), + HighlightSpec::with_fg(if target_is_valid { + HighlightRole::redirection + } else { + HighlightRole::error + }), + ); + } } - fn visit_variable_assignment(&mut self, node: &VariableAssignment) { - self.companion - .as_mut() - .visit_variable_assignment((node as *const VariableAssignment).cast()); + fn visit_variable_assignment(&mut self, varas: &VariableAssignment) { + self.color_as_argument(varas, true); + // Highlight the '=' in variable assignments as an operator. + if let Some(offset) = variable_assignment_equals_pos(varas.source(self.buff)) { + let equals_loc = varas.source_range().start() + offset; + self.color_array[equals_loc] = HighlightSpec::with_fg(HighlightRole::operat); + let var_name = &varas.source(self.buff)[..offset]; + self.pending_variables.push(var_name.to_owned()); + } } fn visit_semi_nl(&mut self, node: &dyn Node) { - let ffi_node = NodeFfi::new(node); - self.companion - .as_mut() - .visit_semi_nl((&ffi_node as *const NodeFfi<'_>).cast()); + self.color_node( + node, + HighlightSpec::with_fg(HighlightRole::statement_terminator), + ) } - fn visit_decorated_statement(&mut self, node: &DecoratedStatement) { - self.companion - .as_mut() - .visit_decorated_statement((node as *const DecoratedStatement).cast()); + fn visit_decorated_statement(&mut self, stmt: &DecoratedStatement) { + // Color any decoration. + if let Some(decoration) = stmt.opt_decoration.as_ref() { + self.visit_keyword(decoration); + } + + // Color the command's source code. + // If we get no source back, there's nothing to color. + if stmt.command.try_source_range().is_none() { + return; + } + let cmd = stmt.command.source(self.buff); + + let mut expanded_cmd = WString::new(); + let mut is_valid_cmd = false; + if !self.io_still_ok() { + // We cannot check if the command is invalid, so just assume it's valid. + is_valid_cmd = true; + } else if variable_assignment_equals_pos(cmd).is_some() { + is_valid_cmd = true; + } else { + // Check to see if the command is valid. + // Try expanding it. If we cannot, it's an error. + if let Some(expanded) = statement_get_expanded_command(self.buff, stmt, self.ctx) { + expanded_cmd = expanded; + if !has_expand_reserved(&expanded_cmd) { + is_valid_cmd = command_is_valid( + &expanded_cmd, + stmt.decoration(), + &self.working_directory, + self.ctx.vars(), + ); + } + } + } + + // Color our statement. + if is_valid_cmd { + self.color_command(&stmt.command); + } else { + self.color_node(&stmt.command, HighlightSpec::with_fg(HighlightRole::error)) + } + + // Color arguments and redirections. + // Except if our command is 'cd' we have special logic for how arguments are colored. + let is_cd = expanded_cmd == L!("cd"); + let mut is_set = expanded_cmd == L!("set"); + // If we have seen a "--" argument, color all options from then on as normal arguments. + let mut have_dashdash = false; + for v in &stmt.args_or_redirs { + if v.is_argument() { + if is_set { + let arg = v.argument().source(self.buff); + if valid_var_name(arg) { + self.pending_variables.push(arg.to_owned()); + is_set = false; + } + } + self.visit_argument(v.argument(), is_cd, !have_dashdash); + if v.argument().source(self.buff) == L!("--") { + have_dashdash = true; + } + } else { + self.visit_redirection(v.redirection()); + } + } } - fn visit_block_statement(&mut self, node: &'a BlockStatement) { - match &*node.header { + fn visit_block_statement(&mut self, block: &BlockStatement) { + match &*block.header { BlockStatementHeaderVariant::None => panic!(), BlockStatementHeaderVariant::ForHeader(node) => self.visit(node), BlockStatementHeaderVariant::WhileHeader(node) => self.visit(node), BlockStatementHeaderVariant::FunctionHeader(node) => self.visit(node), BlockStatementHeaderVariant::BeginHeader(node) => self.visit(node), } - self.visit(&node.args_or_redirs); - let pending_variables_count = self - .companion - .as_mut() - .visit_block_statement1((node as *const BlockStatement).cast()); - self.visit(&node.jobs); - self.visit(&node.end); - self.companion - .as_mut() - .visit_block_statement2(pending_variables_count); + self.visit(&block.args_or_redirs); + let pending_variables_count = self.pending_variables.len(); + if let Some(fh) = block.header.as_for_header() { + let var_name = fh.var_name.source(self.buff); + self.pending_variables.push(var_name.to_owned()); + } + self.visit(&block.jobs); + self.visit(&block.end); + self.pending_variables.truncate(pending_variables_count); } } -impl<'a> NodeVisitor<'a> for Highlighter<'a> { +/// \return whether a string contains a command substitution. +fn has_cmdsub(src: &wstr) -> bool { + let mut cursor = 0; + let mut start = 0; + let mut end = 0; + parse_util_locate_cmdsubst_range( + src, + &mut cursor, + None, + &mut start, + &mut end, + true, + None, + None, + ) != 0 +} + +fn contains_pending_variable(pending_variables: &[WString], haystack: &wstr) -> bool { + for var_name in pending_variables { + let mut nextpos = 0; + while let Some(relpos) = &haystack[nextpos..] + .as_char_slice() + .windows(var_name.len()) + .position(|w| w == var_name.as_char_slice()) + { + let pos = nextpos + relpos; + nextpos = pos + 1; + if pos == 0 || haystack.as_char_slice()[pos - 1] != '$' { + continue; + } + let end = pos + var_name.len(); + if end < haystack.len() && valid_var_name_char(haystack.as_char_slice()[end]) { + continue; + } + return true; + } + } + false +} + +impl<'s, 'a> NodeVisitor<'a> for Highlighter<'s> { fn visit(&mut self, node: &'a dyn Node) { if let Some(keyword) = node.as_keyword() { return self.visit_keyword(keyword); @@ -89,7 +1475,7 @@ fn visit(&mut self, node: &'a dyn Node) { return; } match node.typ() { - Type::argument => self.visit_argument(node.as_argument().unwrap()), + Type::argument => self.visit_argument(node.as_argument().unwrap(), false, true), Type::redirection => self.visit_redirection(node.as_redirection().unwrap()), Type::variable_assignment => { self.visit_variable_assignment(node.as_variable_assignment().unwrap()) @@ -104,36 +1490,337 @@ fn visit(&mut self, node: &'a dyn Node) { } } -#[cxx::bridge] -#[allow(clippy::needless_lifetimes)] // false positive -mod highlighter_ffi { - extern "C++" { - include!("ast.h"); - include!("highlight.h"); - include!("parse_constants.h"); - type highlighter_t = crate::ffi::highlighter_t; - type Ast = crate::ast::Ast; - type NodeFfi<'a> = crate::ast::NodeFfi<'a>; - } - extern "Rust" { - type Highlighter<'a>; - unsafe fn new_highlighter<'a>( - companion: Pin<&'a mut highlighter_t>, - ast: &'a Ast, - ) -> Box>; - #[cxx_name = "visit_children"] - unsafe fn visit_children_ffi<'a>(self: &mut Highlighter<'a>, node: &'a NodeFfi<'a>); +// Given a plain statement node in a parse tree, get the command and return it, expanded +// appropriately for commands. If we succeed, return true. +fn statement_get_expanded_command( + src: &wstr, + stmt: &ast::DecoratedStatement, + ctx: &OperationContext<'_>, +) -> Option { + // Get the command. Try expanding it. If we cannot, it's an error. + let Some(cmd) = stmt.command.try_source(src) else { + return None; + }; + let mut out_cmd = WString::new(); + let err = expand_to_command_and_args(cmd, ctx, &mut out_cmd, None, None, false); + (err == ExpandResultCode::ok).then_some(out_cmd) +} + +fn get_highlight_var_name(role: HighlightRole) -> &'static wstr { + match role { + HighlightRole::normal => L!("fish_color_normal"), + HighlightRole::error => L!("fish_color_error"), + HighlightRole::command => L!("fish_color_command"), + HighlightRole::keyword => L!("fish_color_keyword"), + HighlightRole::statement_terminator => L!("fish_color_end"), + HighlightRole::param => L!("fish_color_param"), + HighlightRole::option => L!("fish_color_option"), + HighlightRole::comment => L!("fish_color_comment"), + HighlightRole::search_match => L!("fish_color_search_match"), + HighlightRole::operat => L!("fish_color_operator"), + HighlightRole::escape => L!("fish_color_escape"), + HighlightRole::quote => L!("fish_color_quote"), + HighlightRole::redirection => L!("fish_color_redirection"), + HighlightRole::autosuggestion => L!("fish_color_autosuggestion"), + HighlightRole::selection => L!("fish_color_selection"), + HighlightRole::pager_progress => L!("fish_pager_color_progress"), + HighlightRole::pager_background => L!("fish_pager_color_background"), + HighlightRole::pager_prefix => L!("fish_pager_color_prefix"), + HighlightRole::pager_completion => L!("fish_pager_color_completion"), + HighlightRole::pager_description => L!("fish_pager_color_description"), + HighlightRole::pager_secondary_background => L!("fish_pager_color_secondary_background"), + HighlightRole::pager_secondary_prefix => L!("fish_pager_color_secondary_prefix"), + HighlightRole::pager_secondary_completion => L!("fish_pager_color_secondary_completion"), + HighlightRole::pager_secondary_description => L!("fish_pager_color_secondary_description"), + HighlightRole::pager_selected_background => L!("fish_pager_color_selected_background"), + HighlightRole::pager_selected_prefix => L!("fish_pager_color_selected_prefix"), + HighlightRole::pager_selected_completion => L!("fish_pager_color_selected_completion"), + HighlightRole::pager_selected_description => L!("fish_pager_color_selected_description"), + _ => unreachable!(), } } -fn new_highlighter<'a>( - companion: Pin<&'a mut highlighter_t>, - ast: &'a Ast, -) -> Box> { - Box::new(Highlighter { companion, ast }) -} -impl<'a> Highlighter<'a> { - fn visit_children_ffi(&mut self, node: &'a NodeFfi<'a>) { - self.visit_children(node.as_node()); +// Table used to fetch fallback highlights in case the specified one +// wasn't set. +fn get_fallback(role: HighlightRole) -> HighlightRole { + match role { + HighlightRole::normal + | HighlightRole::error + | HighlightRole::command + | HighlightRole::statement_terminator + | HighlightRole::param + | HighlightRole::search_match + | HighlightRole::comment + | HighlightRole::operat + | HighlightRole::escape + | HighlightRole::quote + | HighlightRole::redirection + | HighlightRole::autosuggestion + | HighlightRole::selection + | HighlightRole::pager_progress + | HighlightRole::pager_background + | HighlightRole::pager_prefix + | HighlightRole::pager_completion + | HighlightRole::pager_description => HighlightRole::normal, + HighlightRole::keyword => HighlightRole::command, + HighlightRole::option => HighlightRole::param, + HighlightRole::pager_secondary_background => HighlightRole::pager_background, + HighlightRole::pager_secondary_prefix | HighlightRole::pager_selected_prefix => { + HighlightRole::pager_prefix + } + HighlightRole::pager_secondary_completion | HighlightRole::pager_selected_completion => { + HighlightRole::pager_completion + } + HighlightRole::pager_secondary_description | HighlightRole::pager_selected_description => { + HighlightRole::pager_description + } + HighlightRole::pager_selected_background => HighlightRole::search_match, + _ => unreachable!(), } } + +/// Determine if the filesystem containing the given fd is case insensitive for lookups regardless +/// of whether it preserves the case when saving a pathname. +/// +/// Returns: +/// false: the filesystem is not case insensitive +/// true: the file system is case insensitive +pub type CaseSensitivityCache = HashMap; +fn fs_is_case_insensitive( + path: &wstr, + fd: RawFd, + case_sensitivity_cache: &mut CaseSensitivityCache, +) -> bool { + let mut result = false; + if *_PC_CASE_SENSITIVE != 0 { + // Try the cache first. + match case_sensitivity_cache.entry(path.to_owned()) { + Entry::Occupied(e) => { + /* Use the cached value */ + result = *e.get(); + } + Entry::Vacant(e) => { + // Ask the system. A -1 value means error (so assume case sensitive), a 1 value means case + // sensitive, and a 0 value means case insensitive. + let ret = unsafe { libc::fpathconf(fd, *_PC_CASE_SENSITIVE) }; + result = ret == 0; + e.insert(result); + } + } + } + result +} + +pub use highlight_ffi::{HighlightRole, HighlightSpec}; + +impl Default for HighlightRole { + fn default() -> Self { + Self::normal + } +} + +impl Default for HighlightSpec { + fn default() -> Self { + Self { + foreground: Default::default(), + background: Default::default(), + valid_path: Default::default(), + force_underline: Default::default(), + } + } +} + +#[cxx::bridge] +mod highlight_ffi { + /// Describes the role of a span of text. + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] + pub enum HighlightRole { + normal, // normal text + error, // error + command, // command + keyword, + statement_terminator, // process separator + param, // command parameter (argument) + option, // argument starting with "-", up to a "--" + comment, // comment + search_match, // search match + operat, // operator + escape, // escape sequences + quote, // quoted string + redirection, // redirection + autosuggestion, // autosuggestion + selection, + + // Pager support. + // NOTE: pager.cpp relies on these being in this order. + pager_progress, + pager_background, + pager_prefix, + pager_completion, + pager_description, + pager_secondary_background, + pager_secondary_prefix, + pager_secondary_completion, + pager_secondary_description, + pager_selected_background, + pager_selected_prefix, + pager_selected_completion, + pager_selected_description, + } + + /// Simply value type describing how a character should be highlighted.. + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] + pub struct HighlightSpec { + pub foreground: HighlightRole, + pub background: HighlightRole, + pub valid_path: bool, + pub force_underline: bool, + } + + extern "Rust" { + #[cxx_name = "clone"] + fn clone_ffi(self: &HighlightSpec) -> Box; + fn new_highlight_spec() -> Box; + } + + extern "C++" { + include!("highlight.h"); + include!("history.h"); + include!("color.h"); + include!("operation_context.h"); + type HistoryItem = crate::history::HistoryItem; + type OperationContext<'a> = crate::operation_context::OperationContext<'a>; + type rgb_color_t = crate::ffi::rgb_color_t; + #[cxx_name = "EnvDyn"] + type EnvDynFFI = crate::env::EnvDynFFI; + #[cxx_name = "EnvStackRef"] + type EnvStackRefFFI = crate::env::EnvStackRefFFI; + } + extern "Rust" { + #[cxx_name = "autosuggest_validate_from_history"] + fn autosuggest_validate_from_history_ffi( + item: &HistoryItem, + working_directory: &CxxWString, + ctx: &OperationContext<'static>, + ) -> bool; + } + extern "Rust" { + type HighlightColorResolver; + fn new_highlight_color_resolver() -> Box; + #[cxx_name = "resolve_spec"] + fn resolve_spec_ffi( + &mut self, + highlight: &HighlightSpec, + is_background: bool, + vars: &EnvStackRefFFI, + out: Pin<&mut rgb_color_t>, + ); + } + extern "Rust" { + type HighlightSpecListFFI; + fn highlight_shell_ffi( + bff: &CxxWString, + ctx: &OperationContext<'_>, + io_ok: bool, + cursor: SharedPtr, + ) -> Box; + fn size(&self) -> usize; + fn at(&self, index: usize) -> &HighlightSpec; + #[cxx_name = "colorize"] + fn colorize_ffi( + text: &CxxWString, + colors: &HighlightSpecListFFI, + vars: &EnvStackRefFFI, + ) -> Vec; + } +} + +fn colorize_ffi( + text: &CxxWString, + colors: &HighlightSpecListFFI, + vars: &EnvStackRefFFI, +) -> Vec { + colorize(text.as_wstr(), &colors.0, &*vars.0) +} + +struct HighlightSpecListFFI(Vec); + +impl HighlightSpecListFFI { + fn size(&self) -> usize { + self.0.len() + } + fn at(&self, index: usize) -> &HighlightSpec { + &self.0[index] + } +} +fn highlight_shell_ffi( + buff: &CxxWString, + ctx: &OperationContext<'_>, + io_ok: bool, + cursor: SharedPtr, +) -> Box { + let cursor = cursor.as_ref().cloned(); + let mut color = vec![]; + highlight_shell(buff.as_wstr(), &mut color, ctx, io_ok, cursor); + Box::new(HighlightSpecListFFI(color)) +} + +impl HighlightColorResolver { + fn resolve_spec_ffi( + &mut self, + highlight: &HighlightSpec, + is_background: bool, + vars: &EnvStackRefFFI, + mut out: Pin<&mut rgb_color_t>, + ) { + let color = self.resolve_spec(highlight, is_background, &*vars.0); + match color.typ { + color::Type::None => (), + color::Type::Named { idx } => { + out.as_mut().set_is_named(); + out.as_mut().set_name_idx(idx); + } + color::Type::Rgb(color) => { + out.as_mut().set_is_rgb(); + out.as_mut().set_color(color.r, color.g, color.b); + } + color::Type::Normal => out.as_mut().set_is_normal(), + color::Type::Reset => out.as_mut().set_is_reset(), + } + if color.flags.bold { + out.as_mut().set_bold(true); + } + if color.flags.underline { + out.as_mut().set_underline(true); + } + if color.flags.italics { + out.as_mut().set_italics(true); + } + if color.flags.dim { + out.as_mut().set_dim(true); + } + if color.flags.reverse { + out.as_mut().set_reverse(true); + } + } +} + +fn autosuggest_validate_from_history_ffi( + item: &HistoryItem, + working_directory: &CxxWString, + ctx: &OperationContext<'static>, +) -> bool { + autosuggest_validate_from_history(item, working_directory.as_wstr(), ctx) +} + +fn new_highlight_color_resolver() -> Box { + Box::new(HighlightColorResolver::new()) +} +impl HighlightSpec { + fn clone_ffi(&self) -> Box { + Box::new(*self) + } +} +fn new_highlight_spec() -> Box { + Box::default() +} diff --git a/fish-rust/src/history.rs b/fish-rust/src/history.rs index 49f84af68..12c7527ea 100644 --- a/fish-rust/src/history.rs +++ b/fish-rust/src/history.rs @@ -1,8 +1,2400 @@ -use crate::env::{EnvMode, EnvStack}; -use crate::wchar::prelude::*; +//! Fish supports multiple shells writing to history at once. Here is its strategy: +//! +//! 1. All history files are append-only. Data, once written, is never modified. +//! +//! 2. A history file may be re-written ("vacuumed"). This involves reading in the file and writing a +//! new one, while performing maintenance tasks: discarding items in an LRU fashion until we reach +//! the desired maximum count, removing duplicates, and sorting them by timestamp (eventually, not +//! implemented yet). The new file is atomically moved into place via rename(). +//! +//! 3. History files are mapped in via mmap(). Before the file is mapped, the file takes a fcntl read +//! lock. The purpose of this lock is to avoid seeing a transient state where partial data has been +//! written to the file. +//! +//! 4. History is appended to under a fcntl write lock. +//! +//! 5. The chaos_mode boolean can be set to true to do things like lower buffer sizes which can +//! trigger race conditions. This is useful for testing. + +use crate::{common::cstr2wcstring, wcstringutil::trim}; +use std::{ + borrow::Cow, + collections::{BTreeMap, HashMap, HashSet, VecDeque}, + ffi::CString, + io::{BufRead, BufReader, Read, Write}, + mem, + num::NonZeroUsize, + ops::ControlFlow, + os::fd::RawFd, + sync::{Arc, Mutex, MutexGuard}, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; + +use bitflags::bitflags; +use cxx::{CxxWString, UniquePtr}; +use libc::{ + fchmod, fchown, flock, fstat, ftruncate, lseek, LOCK_EX, LOCK_SH, LOCK_UN, O_APPEND, O_CREAT, + O_RDONLY, O_WRONLY, SEEK_SET, +}; +use lru::LruCache; +use rand::Rng; +use widestring_suffix::widestrs; + +use crate::{ + ast::{ast_ffi::StatementDecoration, Ast, Node}, + common::{ + str2wcstring, unescape_string, valid_var_name, wcs2zstring, write_loop, CancelChecker, + UnescapeStringStyle, + }, + env::{EnvDyn, EnvMode, EnvStack, EnvStackRefFFI, Environment}, + expand::{expand_one, ExpandFlags}, + fallback::fish_mkstemp_cloexec, + fds::{wopen_cloexec, AutoCloseFd}, + ffi::wcstring_list_ffi_t, + flog::{FLOG, FLOGF}, + global_safety::RelaxedAtomicBool, + history::file::{append_history_item_to_buffer, HistoryFileContents}, + io::IoStreams, + operation_context::{OperationContext, EXPANSION_LIMIT_BACKGROUND}, + parse_constants::ParseTreeFlags, + parse_util::{parse_util_detect_errors, parse_util_unescape_wildcards}, + path::{ + path_get_config, path_get_data, path_get_data_remoteness, path_is_valid, DirRemoteness, + }, + signal::signal_check_cancel, + threads::{assert_is_background_thread, iothread_perform}, + util::find_subslice, + wchar::prelude::*, + wchar_ext::WExt, + wchar_ffi::{AsWstr, WCharFromFFI, WCharToFFI}, + wcstringutil::subsequence_in_string, + wildcard::{wildcard_match, ANY_STRING}, + wutil::{ + file_id_for_fd, file_id_for_path, wgettext_fmt, wrealpath, wrename, wstat, wunlink, FileId, + INVALID_FILE_ID, + }, +}; + +mod file; + +#[cxx::bridge] +mod history_ffi { + extern "C++" { + include!("wutil.h"); + include!("io.h"); + include!("env.h"); + include!("operation_context.h"); + + type IoStreams<'a> = crate::io::IoStreams<'a>; + #[cxx_name = "EnvDyn"] + type EnvDynFFI = crate::env::EnvDynFFI; + #[cxx_name = "EnvStackRef"] + type EnvStackRefFFI = crate::env::EnvStackRefFFI; + type OperationContext<'a> = crate::operation_context::OperationContext<'a>; + type wcstring_list_ffi_t = crate::ffi::wcstring_list_ffi_t; + } + + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + enum SearchType { + /// Search for commands exactly matching the given string. + Exact, + /// Search for commands containing the given string. + Contains, + /// Search for commands starting with the given string. + Prefix, + /// Search for commands containing the given glob pattern. + ContainsGlob, + /// Search for commands starting with the given glob pattern. + PrefixGlob, + /// Search for commands containing the given string as a subsequence + ContainsSubsequence, + /// Matches everything. + MatchEverything, + } + + /// Ways that a history item may be written to disk (or omitted). + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + enum PersistenceMode { + /// The history item is written to disk normally + Disk, + /// The history item is stored in-memory only, not written to disk + Memory, + /// The history item is stored in-memory and deleted when a new item is added + Ephemeral, + } + + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub enum SearchDirection { + Forward, + Backward, + } + + pub enum HistoryPagerInvocation { + Anew, + Advance, + Refresh, + } + + extern "Rust" { + #[cxx_name = "history_save_all"] + fn save_all(); + #[cxx_name = "history_session_id"] + fn rust_session_id(vars: &EnvStackRefFFI) -> UniquePtr; + fn rust_expand_and_detect_paths( + paths: &wcstring_list_ffi_t, + vars: &EnvStackRefFFI, + ) -> UniquePtr; + fn rust_all_paths_are_valid( + paths: &wcstring_list_ffi_t, + ctx: &OperationContext<'_>, + ) -> bool; + #[rust_name = "start_private_mode_ffi"] + fn start_private_mode(vars: &EnvStackRefFFI); + #[rust_name = "in_private_mode_ffi"] + fn in_private_mode(vars: &EnvStackRefFFI) -> bool; + #[cxx_name = "all_paths_are_valid"] + fn all_paths_are_valid_ffi(paths: &wcstring_list_ffi_t, ctx: &OperationContext<'_>) + -> bool; + } + + extern "Rust" { + type ItemIndexes; + + fn get(&self, index: usize) -> UniquePtr; + } + + extern "Rust" { + type HistoryItem; + + fn rust_history_item_new( + s: &CxxWString, + when: i64, + ident: u64, + persist_mode: PersistenceMode, + ) -> Box; + #[rust_name = "str_ffi"] + fn str(&self) -> UniquePtr; + fn is_empty(&self) -> bool; + #[rust_name = "matches_search_ffi"] + fn matches_search(&self, term: &CxxWString, typ: SearchType, case_sensitive: bool) -> bool; + #[rust_name = "timestamp_ffi"] + fn timestamp(&self) -> i64; + fn should_write_to_disk(&self) -> bool; + #[rust_name = "get_required_paths_ffi"] + fn get_required_paths(&self) -> UniquePtr; + #[rust_name = "set_required_paths_ffi"] + fn set_required_paths(&mut self, paths: &wcstring_list_ffi_t); + } + + extern "Rust" { + type HistorySharedPtr; + fn history_with_name(name: &CxxWString) -> Box; + fn is_default(&self) -> bool; + fn is_empty(&self) -> bool; + fn remove(&self, s: &CxxWString); + fn remove_ephemeral_items(&self); + fn resolve_pending(&self); + fn save(&self); + fn clear(&self); + fn clear_session(&self); + fn populate_from_config_path(&self); + fn populate_from_bash(&self, filename: &CxxWString); + fn incorporate_external_changes(&self); + fn get_history(&self) -> UniquePtr; + fn items_at_indexes(&self, indexes: &[isize]) -> Box; + fn item_at_index(&self, idx: usize) -> Box; + fn size(&self) -> usize; + fn clone(&self) -> Box; + #[cxx_name = "add_pending_with_file_detection"] + fn add_pending_with_file_detection_ffi( + &self, + s: &CxxWString, + vars: &EnvStackRefFFI, + persist_mode: PersistenceMode, + ); + } + + extern "Rust" { + type HistorySearch; + fn rust_history_search_new( + hist: &HistorySharedPtr, + s: &CxxWString, + search_type: SearchType, + flags: u32, + starting_index: usize, + ) -> Box; + #[rust_name = "original_term_ffi"] + fn original_term(&self) -> UniquePtr; + fn go_to_next_match(&mut self, direction: SearchDirection) -> bool; + fn current_item(&self) -> &HistoryItem; + #[rust_name = "current_string_ffi"] + fn current_string(&self) -> UniquePtr; + fn current_index(&self) -> usize; + fn ignores_case(&self) -> bool; + } +} + +use self::file::time_to_seconds; +pub use self::history_ffi::{PersistenceMode, SearchDirection, SearchType}; + +// Our history format is intended to be valid YAML. Here it is: +// +// - cmd: ssh blah blah blah +// when: 2348237 +// paths: +// - /path/to/something +// - /path/to/something_else +// +// Newlines are replaced by \n. Backslashes are replaced by \\. + +/// This is the history session ID we use by default if the user has not set env var fish_history. +const DFLT_FISH_HISTORY_SESSION_ID: &wstr = L!("fish"); + +/// When we rewrite the history, the number of items we keep. +// FIXME: https://github.com/rust-lang/rust/issues/67441 +const HISTORY_SAVE_MAX: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(1024 * 256) }; + +/// Default buffer size for flushing to the history file. +const HISTORY_OUTPUT_BUFFER_SIZE: usize = 64 * 1024; + +/// The file access mode we use for creating history files +const HISTORY_FILE_MODE: i32 = 0o600; + +/// How many times we retry to save +/// Saving may fail if the file is modified in between our opening +/// the file and taking the lock +const MAX_SAVE_TRIES: usize = 1024; + +/// If the size of `buffer` is at least `min_size`, output the contents `buffer` to `fd`, +/// and clear the string. +fn flush_to_fd(buffer: &mut Vec, fd: RawFd, min_size: usize) -> std::io::Result<()> { + if buffer.is_empty() || buffer.len() < min_size { + return Ok(()); + } + + write_loop(&fd, buffer)?; + buffer.clear(); + return Ok(()); +} + +struct TimeProfiler { + what: &'static str, + start: SystemTime, +} + +impl TimeProfiler { + fn new(what: &'static str) -> Self { + let start = SystemTime::now(); + Self { what, start } + } +} + +impl Drop for TimeProfiler { + fn drop(&mut self) { + if let Ok(duration) = self.start.elapsed() { + FLOGF!( + profile_history, + "%s: %d.%06d ms", + self.what, + duration.as_millis() as u64, // todo!("remove cast") + (duration.as_nanos() / 1000) as u64 // todo!("remove cast") + ) + } else { + FLOGF!(profile_history, "%s: ??? ms", self.what) + } + } +} + +trait LruCacheExt { + /// Function to add a history item. + fn add_item(&mut self, item: HistoryItem); +} + +impl LruCacheExt for LruCache { + fn add_item(&mut self, item: HistoryItem) { + // Skip empty items. + if item.is_empty() { + return; + } + + // See if it's in the cache. If it is, update the timestamp. If not, we create a new node + // and add it. Note that calling get_node promotes the node to the front. + let key = item.str(); + if let Some(node) = self.get_mut(key) { + node.creation_timestamp = SystemTime::max(node.timestamp(), item.timestamp()); + // What to do about paths here? Let's just ignore them. + } else { + self.put(key.to_owned(), item); + } + } +} + +/// Returns the path for the history file for the given `session_id`, or `None` if it could not be +/// loaded. If `suffix` is provided, append that suffix to the path; this is used for temporary files. +#[widestrs] +fn history_filename(session_id: &wstr, suffix: &wstr) -> Option { + if session_id.is_empty() { + return None; + } + + let Some(mut result) = path_get_data() else { + return None; + }; + + result.push('/'); + result.push_utfstr(session_id); + result.push_utfstr("_history"L); + result.push_utfstr(suffix); + Some(result) +} + +pub type PathList = Vec; +pub type HistoryIdentifier = u64; + +#[derive(Clone, Debug)] +pub struct HistoryItem { + /// The actual contents of the entry. + contents: WString, + /// Original creation time for the entry. + creation_timestamp: SystemTime, + /// Paths that we require to be valid for this item to be autosuggested. + required_paths: Vec, + /// Sometimes unique identifier used for hinting. + identifier: HistoryIdentifier, + /// Whether to write this item to disk. + persist_mode: PersistenceMode, +} + +impl HistoryItem { + /// Construct from a text, timestamp, and optional identifier. + /// If `persist_mode` is not [`PersistenceMode::Disk`], then do not write this item to disk. + pub fn new( + s: WString, + when: SystemTime, /*=0*/ + ident: HistoryIdentifier, /*=0*/ + persist_mode: PersistenceMode, /*=Disk*/ + ) -> Self { + Self { + contents: s, + creation_timestamp: when, + required_paths: vec![], + identifier: ident, + persist_mode, + } + } + + /// Returns the text as a string. + pub fn str(&self) -> &wstr { + &self.contents + } + + fn str_ffi(&self) -> UniquePtr { + self.str().to_ffi() + } + + /// Returns whether the text is empty. + pub fn is_empty(&self) -> bool { + self.contents.is_empty() + } + + /// Returns whether our contents matches a search term. + pub fn matches_search(&self, term: &wstr, typ: SearchType, case_sensitive: bool) -> bool { + // Note that 'term' has already been lowercased when constructing the + // search object if we're doing a case insensitive search. + let content_to_match = if case_sensitive { + Cow::Borrowed(&self.contents) + } else { + Cow::Owned(self.contents.to_lowercase()) + }; + + match typ { + SearchType::Exact => term == *content_to_match, + SearchType::Contains => { + find_subslice(term.as_slice(), content_to_match.as_slice()).is_some() + } + SearchType::Prefix => content_to_match.as_slice().starts_with(term.as_slice()), + SearchType::ContainsGlob => { + let mut pat = parse_util_unescape_wildcards(term); + if !pat.starts_with(ANY_STRING) { + pat.insert(0, ANY_STRING); + } + if !pat.ends_with(ANY_STRING) { + pat.push(ANY_STRING); + } + wildcard_match(content_to_match.as_ref(), &pat, false) + } + SearchType::PrefixGlob => { + let mut pat = parse_util_unescape_wildcards(term); + if !pat.ends_with(ANY_STRING) { + pat.push(ANY_STRING); + } + wildcard_match(content_to_match.as_ref(), &pat, false) + } + SearchType::ContainsSubsequence => subsequence_in_string(term, &content_to_match), + SearchType::MatchEverything => true, + _ => unreachable!("invalid SearchType"), + } + } + + fn matches_search_ffi(&self, term: &CxxWString, typ: SearchType, case_sensitive: bool) -> bool { + self.matches_search(&term.from_ffi(), typ, case_sensitive) + } + + /// Returns the timestamp for creating this history item. + pub fn timestamp(&self) -> SystemTime { + self.creation_timestamp + } + + fn timestamp_ffi(&self) -> i64 { + match self.timestamp().duration_since(UNIX_EPOCH) { + Ok(d) => { + // after epoch + i64::try_from(d.as_secs()).unwrap() + } + Err(e) => { + // before epoch + -i64::try_from(e.duration().as_secs()).unwrap() + } + } + } + + /// Returns whether this item should be persisted (written to disk). + pub fn should_write_to_disk(&self) -> bool { + self.persist_mode == PersistenceMode::Disk + } + + /// Get the list of arguments which referred to files. + /// This is used for autosuggestion hinting. + pub fn get_required_paths(&self) -> &[WString] { + &self.required_paths + } + + fn get_required_paths_ffi(&self) -> UniquePtr { + self.get_required_paths().to_ffi() + } + + /// Set the list of arguments which referred to files. + /// This is used for autosuggestion hinting. + pub fn set_required_paths(&mut self, paths: Vec) { + self.required_paths = paths; + } + + fn set_required_paths_ffi(&mut self, paths: &wcstring_list_ffi_t) { + self.set_required_paths(paths.from_ffi()) + } + + /// We can merge two items if they are the same command. We use the more recent timestamp, more + /// recent identifier, and the longer list of required paths. + fn merge(&mut self, item: &HistoryItem) -> bool { + // We can only merge items if they agree on their text and persistence mode. + if self.contents != item.contents || self.persist_mode != item.persist_mode { + return false; + } + + // Ok, merge this item. + self.creation_timestamp = self.creation_timestamp.max(item.creation_timestamp); + if self.required_paths.len() < item.required_paths.len() { + self.required_paths = item.required_paths.clone(); + } + if self.identifier < item.identifier { + self.identifier = item.identifier; + } + true + } +} + +static HISTORIES: Mutex>> = Mutex::new(BTreeMap::new()); + +struct HistoryImpl { + /// The name of this list. Used for picking a suitable filename and for switching modes. + name: WString, + /// New items. Note that these are NOT discarded on save. We need to keep these around so we can + /// distinguish between items in our history and items in the history of other shells that were + /// started after we were started. + new_items: Vec, + /// The index of the first new item that we have not yet written. + first_unwritten_new_item_index: usize, // 0 + /// Whether we have a pending item. If so, the most recently added item is ignored by + /// item_at_index. + has_pending_item: bool, // false + /// Whether we should disable saving to the file for a time. + disable_automatic_save_counter: u32, // 0 + /// Deleted item contents. + /// Boolean describes if it should be deleted only in this session or in all + /// (used in deduplication). + deleted_items: HashMap, + /// The buffer containing the history file contents. + file_contents: Option, + /// The file ID of the history file. + history_file_id: FileId, // INVALID_FILE_ID + /// The boundary timestamp distinguishes old items from new items. Items whose timestamps are <= + /// the boundary are considered "old". Items whose timestemps are > the boundary are new, and are + /// ignored by this instance (unless they came from this instance). The timestamp may be adjusted + /// by incorporate_external_changes(). + boundary_timestamp: SystemTime, + /// The most recent "unique" identifier for a history item. + last_identifier: HistoryIdentifier, // 0 + /// How many items we add until the next vacuum. Initially a random value. + countdown_to_vacuum: Option, // -1 + /// Whether we've loaded old items. + loaded_old: bool, // false + /// List of old items, as offsets into out mmap data. + old_item_offsets: VecDeque, +} + +/// If set, we gave up on file locking because it took too long. +/// Note this is shared among all history instances. +static ABANDONED_LOCKING: RelaxedAtomicBool = RelaxedAtomicBool::new(false); + +impl HistoryImpl { + /// Add a new history item to the end. If `pending` is set, the item will not be returned by + /// `item_at_index()` until a call to `resolve_pending()`. Pending items are tracked with an + /// offset into the array of new items, so adding a non-pending item has the effect of resolving + /// all pending items. + fn add( + &mut self, + item: HistoryItem, + pending: bool, /*=false*/ + do_save: bool, /*=true*/ + ) { + // We use empty items as sentinels to indicate the end of history. + // Do not allow them to be added (#6032). + if item.contents.is_empty() { + return; + } + + // Try merging with the last item. + if let Some(last) = self.new_items.last_mut() { + if last.merge(&item) { + // We merged, so we don't have to add anything. Maybe this item was pending, but it just got + // merged with an item that is not pending, so pending just becomes false. + self.has_pending_item = false; + return; + } + } + + // We have to add a new item. + self.new_items.push(item); + self.has_pending_item = pending; + if do_save { + self.save_unless_disabled(); + } + } + + /// Internal function. + fn clear_file_state(&mut self) { + // Erase everything we know about our file. + self.file_contents = None; + self.loaded_old = false; + self.old_item_offsets.clear(); + } + + /// Returns a timestamp for new items - see the implementation for a subtlety. + fn timestamp_now(&self) -> SystemTime { + let mut when = SystemTime::now(); + // Big hack: do not allow timestamps equal to our boundary date. This is because we include + // items whose timestamps are equal to our boundary when reading old history, so we can catch + // "just closed" items. But this means that we may interpret our own items, that we just wrote, + // as old items, if we wrote them in the same second as our birthdate. + if when.duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()) + == self + .boundary_timestamp + .duration_since(UNIX_EPOCH) + .ok() + .map(|d| d.as_secs()) + { + when += Duration::from_secs(1); + } + when + } + + /// Returns a new item identifier, incrementing our counter. + fn next_identifier(&mut self) -> HistoryIdentifier { + self.last_identifier += 1; + self.last_identifier + } + + /// Figure out the offsets of our file contents. + fn populate_from_file_contents(&mut self) { + self.old_item_offsets.clear(); + if let Some(file_contents) = &self.file_contents { + let mut cursor = 0; + while let Some(offset) = + file_contents.offset_of_next_item(&mut cursor, Some(self.boundary_timestamp)) + { + // Remember this item. + self.old_item_offsets.push_back(offset); + } + } + + FLOGF!(history, "Loaded %lu old items", self.old_item_offsets.len()); + } + + /// Loads old items if necessary. + fn load_old_if_needed(&mut self) { + if self.loaded_old { + return; + } + self.loaded_old = true; + + let _profiler = TimeProfiler::new("load_old"); + if let Some(filename) = history_filename(&self.name, L!("")) { + let file = AutoCloseFd::new(wopen_cloexec(&filename, O_RDONLY, 0)); + let fd = file.fd(); + if fd >= 0 { + // Take a read lock to guard against someone else appending. This is released after + // getting the file's length. We will read the file after releasing the lock, but that's + // not a problem, because we never modify already written data. In short, the purpose of + // this lock is to ensure we don't see the file size change mid-update. + // + // We may fail to lock (e.g. on lockless NFS - see issue #685. In that case, we proceed + // as if it did not fail. The risk is that we may get an incomplete history item; this + // is unlikely because we only treat an item as valid if it has a terminating newline. + let locked = unsafe { Self::maybe_lock_file(fd, LOCK_SH) }; + self.file_contents = HistoryFileContents::create(fd); + self.history_file_id = if self.file_contents.is_some() { + file_id_for_fd(fd) + } else { + INVALID_FILE_ID + }; + if locked { + unsafe { + Self::unlock_file(fd); + } + } + + let _profiler = TimeProfiler::new("populate_from_file_contents"); + self.populate_from_file_contents(); + } + } + } + + /// Deletes duplicates in new_items. + fn compact_new_items(&mut self) { + // Keep only the most recent items with the given contents. + let mut seen = HashSet::new(); + for idx in (0..self.new_items.len()).rev() { + let item = &self.new_items[idx]; + + // Only compact persisted items. + if !item.should_write_to_disk() { + continue; + } + + if !seen.insert(item.contents.to_owned()) { + // This item was not inserted because it was already in the set, so delete the item at + // this index. + self.new_items.remove(idx); + + if idx < self.first_unwritten_new_item_index { + // Decrement first_unwritten_new_item_index if we are deleting a previously written + // item. + self.first_unwritten_new_item_index -= 1; + } + } + } + } + + /// Removes trailing ephemeral items. + /// Ephemeral items have leading spaces, and can only be retrieved immediately; adding any item + /// removes them. + fn remove_ephemeral_items(&mut self) { + while matches!( + self.new_items.last(), + Some(&HistoryItem { + persist_mode: PersistenceMode::Ephemeral, + .. + }) + ) { + self.new_items.pop(); + } + + self.first_unwritten_new_item_index = + usize::min(self.first_unwritten_new_item_index, self.new_items.len()); + } + + /// Given the fd of an existing history file, write a new history file to `dst_fd`. + /// Returns false on error, true on success + fn rewrite_to_temporary_file(&self, existing_fd: Option, dst_fd: RawFd) -> bool { + // We are reading FROM existing_fd and writing TO dst_fd + // dst_fd must be valid + assert!(dst_fd >= 0); + + // Make an LRU cache to save only the last N elements. + let mut lru = LruCache::new(HISTORY_SAVE_MAX); + + // Read in existing items (which may have changed out from underneath us, so don't trust our + // old file contents). + if let Some(existing_fd) = existing_fd { + if let Some(local_file) = HistoryFileContents::create(existing_fd) { + let mut cursor = 0; + while let Some(offset) = local_file.offset_of_next_item(&mut cursor, None) { + // Try decoding an old item. + let Some(old_item) = local_file.decode_item(offset) else { + continue; + }; + + // If old item is newer than session always erase if in deleted. + if old_item.timestamp() > self.boundary_timestamp { + if old_item.is_empty() || self.deleted_items.contains_key(old_item.str()) { + continue; + } + lru.add_item(old_item); + } else { + // If old item is older and in deleted items don't erase if added by + // clear_session. + if old_item.is_empty() + || self.deleted_items.get(old_item.str()) == Some(&false) + { + continue; + } + // Add this old item. + lru.add_item(old_item); + } + } + } + } + + // Insert any unwritten new items + for item in self + .new_items + .iter() + .skip(self.first_unwritten_new_item_index) + { + if item.should_write_to_disk() { + lru.add_item(item.clone()); + } + } + + // Stable-sort our items by timestamp + // This is because we may have read "old" items with a later timestamp than our "new" items + // This is the essential step that roughly orders items by history + let mut items: Vec<_> = lru.into_iter().map(|(_key, item)| item).collect(); + items.sort_by_key(HistoryItem::timestamp); + + // Write them out. + let mut err = None; + let mut buffer = Vec::with_capacity(HISTORY_OUTPUT_BUFFER_SIZE + 128); + for item in items { + append_history_item_to_buffer(&item, &mut buffer); + if let Err(e) = flush_to_fd(&mut buffer, dst_fd, HISTORY_OUTPUT_BUFFER_SIZE) { + err = Some(e); + break; + } + } + if let Some(err) = err { + FLOGF!( + history_file, + "Error %d when writing to temporary history file", + err.raw_os_error().unwrap_or_default() + ); + + false + } else { + flush_to_fd(&mut buffer, dst_fd, 0).is_ok() + } + } + + /// Saves history by rewriting the file. + fn save_internal_via_rewrite(&mut self) { + FLOGF!( + history, + "Saving %lu items via rewrite", + self.new_items.len() - self.first_unwritten_new_item_index + ); + + // We want to rewrite the file, while holding the lock for as briefly as possible + // To do this, we speculatively write a file, and then lock and see if our original file changed + // Repeat until we succeed or give up + let Some(possibly_indirect_target_name) = history_filename(&self.name, L!("")) else { + return; + }; + let Some(tmp_name_template) = history_filename(&self.name, L!(".XXXXXX")) else { + return; + }; + + // If the history file is a symlink, we want to rewrite the real file so long as we can find it. + let target_name = + wrealpath(&possibly_indirect_target_name).unwrap_or(possibly_indirect_target_name); + + // Make our temporary file + // Remember that we have to close this fd! + let Some((tmp_file, tmp_name)) = create_temporary_file(&tmp_name_template) else { + return; + }; + let tmp_fd = tmp_file.fd(); + assert!(tmp_fd >= 0); + let mut done = false; + for _i in 0..MAX_SAVE_TRIES { + if done { + break; + } + // Open any target file, but do not lock it right away + let mut target_fd_before = AutoCloseFd::new(wopen_cloexec( + &target_name, + O_RDONLY | O_CREAT, + HISTORY_FILE_MODE, + )); + let orig_file_id = file_id_for_fd(target_fd_before.fd()); // possibly invalid + if !self.rewrite_to_temporary_file( + if target_fd_before.is_valid() { + Some(target_fd_before.fd()) + } else { + None + }, + tmp_fd, + ) { + // Failed to write, no good + break; + } + target_fd_before.close(); + + // The crux! We rewrote the history file; see if the history file changed while we + // were rewriting it. Make an effort to take the lock before checking, to avoid racing. + // If the open fails, then proceed; this may be because there is no current history + let mut new_file_id = INVALID_FILE_ID; + let target_fd_after = AutoCloseFd::new(wopen_cloexec(&target_name, O_RDONLY, 0)); + if target_fd_after.is_valid() { + // critical to take the lock before checking file IDs, + // and hold it until after we are done replacing. + // Also critical to check the file at the path, NOT based on our fd. + // It's only OK to replace the file while holding the lock. + // Note any lock is released when target_fd_after is closed. + unsafe { + Self::maybe_lock_file(target_fd_after.fd(), LOCK_EX); + } + new_file_id = file_id_for_path(&target_name); + } + let can_replace_file = new_file_id == orig_file_id || new_file_id == INVALID_FILE_ID; + if !can_replace_file { + // The file has changed, so we're going to re-read it + // Truncate our tmp_fd so we can reuse it + if unsafe { ftruncate(tmp_fd, 0) } == -1 + || unsafe { lseek(tmp_fd, 0, SEEK_SET) } == -1 + { + FLOGF!( + history_file, + "Error %d when truncating temporary history file", + errno::errno().0 + ); + } + } else { + // The file is unchanged, or the new file doesn't exist or we can't read it + // We also attempted to take the lock, so we feel confident in replacing it + + // Ensure we maintain the ownership and permissions of the original (#2355). If the + // stat fails, we assume (hope) our default permissions are correct. This + // corresponds to e.g. someone running sudo -E as the very first command. If they + // did, it would be tricky to set the permissions correctly. (bash doesn't get this + // case right either). + let mut sbuf: libc::stat = unsafe { mem::zeroed() }; + if target_fd_after.is_valid() + && unsafe { fstat(target_fd_after.fd(), &mut sbuf) } >= 0 + { + if unsafe { fchown(tmp_fd, sbuf.st_uid, sbuf.st_gid) } == -1 { + FLOGF!( + history_file, + "Error %d when changing ownership of history file", + errno::errno().0 + ); + } + if unsafe { fchmod(tmp_fd, sbuf.st_mode) } == -1 { + FLOGF!( + history_file, + "Error %d when changing mode of history file", + errno::errno().0, + ); + } + } + + // Slide it into place + if wrename(&tmp_name, &target_name) == -1 { + FLOG!( + error, + wgettext_fmt!( + "Error when renaming history file: %s", + errno::errno().to_string() + ) + ); + } + + // We did it + done = true; + } + } + + // Ensure we never leave the old file around + let _ = wunlink(&tmp_name); + + if done { + // We've saved everything, so we have no more unsaved items. + self.first_unwritten_new_item_index = self.new_items.len(); + + // We deleted our deleted items. + self.deleted_items.clear(); + + // Our history has been written to the file, so clear our state so we can re-reference the + // file. + self.clear_file_state(); + } + } + + /// Saves history by appending to the file. + fn save_internal_via_appending(&mut self) -> bool { + FLOGF!( + history, + "Saving %lu items via appending", + self.new_items.len() - self.first_unwritten_new_item_index + ); + // No deleting allowed. + assert!(self.deleted_items.is_empty()); + + let mut ok = false; + + // If the file is different (someone vacuumed it) then we need to update our mmap. + let mut file_changed = false; + + // Get the path to the real history file. + let Some(history_path) = history_filename(&self.name, L!("")) else { + return true; + }; + + // We are going to open the file, lock it, append to it, and then close it + // After locking it, we need to stat the file at the path; if there is a new file there, it + // means the file was replaced and we have to try again. + // Limit our max tries so we don't do this forever. + let mut history_fd = AutoCloseFd::new(-1); + for _i in 0..MAX_SAVE_TRIES { + let fd = AutoCloseFd::new(wopen_cloexec(&history_path, O_WRONLY | O_APPEND, 0)); + if !fd.is_valid() { + // can't open, we're hosed + break; + } + // Exclusive lock on the entire file. This is released when we close the file (below). This + // may fail on (e.g.) lockless NFS. If so, proceed as if it did not fail; the risk is that + // we may get interleaved history items, which is considered better than no history, or + // forcing everything through the slow copy-move mode. We try to minimize this possibility + // by writing with O_APPEND. + unsafe { + Self::maybe_lock_file(fd.fd(), LOCK_EX); + } + let file_id = file_id_for_fd(fd.fd()); + if file_id_for_path(&history_path) == file_id { + // File IDs match, so the file we opened is still at that path + // We're going to use this fd + if file_id != self.history_file_id { + file_changed = true; + } + history_fd = fd; + break; + } + } + + if history_fd.is_valid() { + // We (hopefully successfully) took the exclusive lock. Append to the file. + // Note that this is sketchy for a few reasons: + // - Another shell may have appended its own items with a later timestamp, so our file may + // no longer be sorted by timestamp. + // - Another shell may have appended the same items, so our file may now contain + // duplicates. + // + // We cannot modify any previous parts of our file, because other instances may be reading + // those portions. We can only append. + // + // Originally we always rewrote the file on saving, which avoided both of these problems. + // However, appending allows us to save history after every command, which is nice! + // + // Periodically we "clean up" the file by rewriting it, so that most of the time it doesn't + // have duplicates, although we don't yet sort by timestamp (the timestamp isn't really used + // for much anyways). + + // So far so good. Write all items at or after first_unwritten_new_item_index. Note that we + // write even a pending item - pending items are ignored by history within the command + // itself, but should still be written to the file. + // TODO: consider filling the buffer ahead of time, so we can just lock, splat, and unlock? + let mut res = Ok(()); + // Use a small buffer size for appending, we usually only have 1 item + let mut buffer = Vec::new(); + while self.first_unwritten_new_item_index < self.new_items.len() { + let item = &self.new_items[self.first_unwritten_new_item_index]; + if item.should_write_to_disk() { + append_history_item_to_buffer(item, &mut buffer); + res = flush_to_fd(&mut buffer, history_fd.fd(), HISTORY_OUTPUT_BUFFER_SIZE); + if res.is_err() { + break; + } + } + // We wrote or skipped this item, hooray. + self.first_unwritten_new_item_index += 1; + } + + if res.is_ok() { + res = flush_to_fd(&mut buffer, history_fd.fd(), 0); + } + + // Since we just modified the file, update our history_file_id to match its current state + // Otherwise we'll think the file has been changed by someone else the next time we go to + // write. + // We don't update the mapping since we only appended to the file, and everything we + // appended remains in our new_items + self.history_file_id = file_id_for_fd(history_fd.fd()); + + ok = res.is_ok(); + } + history_fd.close(); + + // If someone has replaced the file, forget our file state. + if file_changed { + self.clear_file_state(); + } + + ok + } + + /// Saves history. + fn save(&mut self, vacuum: bool) { + // Nothing to do if there's no new items. + if self.first_unwritten_new_item_index >= self.new_items.len() + && self.deleted_items.is_empty() + { + return; + } + + if history_filename(&self.name, L!("")).is_none() { + // We're in the "incognito" mode. Pretend we've saved the history. + self.first_unwritten_new_item_index = self.new_items.len(); + self.deleted_items.clear(); + self.clear_file_state(); + } + + // Compact our new items so we don't have duplicates. + self.compact_new_items(); + + // Try saving. If we have items to delete, we have to rewrite the file. If we do not, we can + // append to it. + let mut ok = false; + if !vacuum && self.deleted_items.is_empty() { + // Try doing a fast append. + ok = self.save_internal_via_appending(); + if !ok { + FLOG!(history, "Appending failed"); + } + } + if !ok { + // We did not or could not append; rewrite the file ("vacuum" it). + self.save_internal_via_rewrite(); + } + } + + /// Saves history unless doing so is disabled. + fn save_unless_disabled(&mut self) { + // Respect disable_automatic_save_counter. + if self.disable_automatic_save_counter > 0 { + return; + } + + // We may or may not vacuum. We try to vacuum every kVacuumFrequency items, but start the + // countdown at a random number so that even if the user never runs more than 25 commands, we'll + // eventually vacuum. If countdown_to_vacuum is None, it means we haven't yet picked a value for + // the counter. + let vacuum_frequency = 25; + let countdown_to_vacuum = self + .countdown_to_vacuum + .get_or_insert_with(|| rand::thread_rng().gen_range(0..vacuum_frequency)); + + // Determine if we're going to vacuum. + let mut vacuum = false; + if *countdown_to_vacuum == 0 { + *countdown_to_vacuum = vacuum_frequency; + vacuum = true; + } + + // Update our countdown. + assert!(*countdown_to_vacuum > 0); + *countdown_to_vacuum -= 1; + + // This might be a good candidate for moving to a background thread. + let _profiler = TimeProfiler::new(if vacuum { + "save vacuum" + } else { + "save no vacuum" + }); + self.save(vacuum); + } + + fn new(name: WString) -> Self { + Self { + name, + new_items: vec![], + first_unwritten_new_item_index: 0, + has_pending_item: false, + disable_automatic_save_counter: 0, + deleted_items: HashMap::new(), + file_contents: None, + history_file_id: INVALID_FILE_ID, + boundary_timestamp: SystemTime::now(), + last_identifier: 0, + countdown_to_vacuum: None, + loaded_old: false, + old_item_offsets: VecDeque::new(), + } + } + + /// Returns whether this is using the default name. + fn is_default(&self) -> bool { + self.name == DFLT_FISH_HISTORY_SESSION_ID + } + + /// Determines whether the history is empty. Unfortunately this cannot be const, since it may + /// require populating the history. + fn is_empty(&mut self) -> bool { + // If we have new items, we're not empty. + if !self.new_items.is_empty() { + return false; + } + + if self.loaded_old { + // If we've loaded old items, see if we have any offsets. + self.old_item_offsets.is_empty() + } else { + // If we have not loaded old items, don't actually load them (which may be expensive); just + // stat the file and see if it exists and is nonempty. + let Some(where_) = history_filename(&self.name, L!("")) else { + return true; + }; + + if let Ok(md) = wstat(&where_) { + // We're empty if the file is empty. + md.len() == 0 + } else { + // Access failed, assume missing. + true + } + } + } + + /// Remove a history item. + fn remove(&mut self, str_to_remove: WString) { + // Add to our list of deleted items. + self.deleted_items.insert(str_to_remove.clone(), false); + + for idx in (0..self.new_items.len()).rev() { + let matched = self.new_items[idx].str() == str_to_remove; + if matched { + self.new_items.remove(idx); + // If this index is before our first_unwritten_new_item_index, then subtract one from + // that index so it stays pointing at the same item. If it is equal to or larger, then + // we have not yet written this item, so we don't have to adjust the index. + if idx < self.first_unwritten_new_item_index { + self.first_unwritten_new_item_index -= 1; + } + } + } + assert!(self.first_unwritten_new_item_index <= self.new_items.len()); + } + + /// Resolves any pending history items, so that they may be returned in history searches. + fn resolve_pending(&mut self) { + self.has_pending_item = false; + } + + /// Enable / disable automatic saving. Main thread only! + fn disable_automatic_saving(&mut self) { + self.disable_automatic_save_counter += 1; + assert!(self.disable_automatic_save_counter != 0); // overflow! + } + + fn enable_automatic_saving(&mut self) { + assert!(self.disable_automatic_save_counter > 0); // negative overflow! + self.disable_automatic_save_counter -= 1; + self.save_unless_disabled(); + } + + /// Irreversibly clears history. + fn clear(&mut self) { + self.new_items.clear(); + self.deleted_items.clear(); + self.first_unwritten_new_item_index = 0; + self.old_item_offsets.clear(); + if let Some(filename) = history_filename(&self.name, L!("")) { + wunlink(&filename); + } + self.clear_file_state(); + } + + /// Clears only session. + fn clear_session(&mut self) { + for item in &self.new_items { + self.deleted_items.insert(item.str().to_owned(), true); + } + + self.new_items.clear(); + self.first_unwritten_new_item_index = 0; + } + + /// Populates from older location (in config path, rather than data path). + /// This is accomplished by clearing ourselves, and copying the contents of the old history + /// file to the new history file. + /// The new contents will automatically be re-mapped later. + fn populate_from_config_path(&mut self) { + let Some(new_file) = history_filename(&self.name, L!("")) else { + return; + }; + + let Some(mut old_file) = path_get_config() else { + return; + }; + + old_file.push('/'); + old_file.push_utfstr(&self.name); + old_file.push_str("_history"); + + let mut src_fd = AutoCloseFd::new(wopen_cloexec(&old_file, O_RDONLY, 0)); + if src_fd.is_valid() { + // Clear must come after we've retrieved the new_file name, and before we open + // destination file descriptor, since it destroys the name and the file. + self.clear(); + + let mut dst_fd = AutoCloseFd::new(wopen_cloexec( + &new_file, + O_WRONLY | O_CREAT, + HISTORY_FILE_MODE, + )); + + let mut buf = [0; libc::BUFSIZ as usize]; + while let Ok(n) = src_fd.read(&mut buf) { + if n == 0 { + break; + } + if dst_fd.write(&buf[..n]).is_err() { + // This message does not have high enough priority to be shown by default. + FLOG!(history_file, "Error when writing history file"); + break; + } + } + } + } + + /// Import a bash command history file. Bash's history format is very simple: just lines with + /// `#`s for comments. Ignore a few commands that are bash-specific. It makes no attempt to + /// handle multiline commands. We can't actually parse bash syntax and the bash history file + /// does not unambiguously encode multiline commands. + fn populate_from_bash(&mut self, contents: R) { + // Process the entire history file until EOF is observed. + // Pretend all items were created at this time. + let when = self.timestamp_now(); + for line in contents.split(b'\n') { + let Ok(line) = line else { + break; + }; + let wide_line = trim(str2wcstring(&line), None); + // Add this line if it doesn't contain anything we know we can't handle. + if should_import_bash_history_line(&wide_line) { + self.add( + HistoryItem::new(wide_line, when, 0, PersistenceMode::Disk), + /*pending=*/ false, + /*do_save=*/ false, + ); + } + } + self.save_unless_disabled(); + } + + /// Incorporates the history of other shells into this history. + fn incorporate_external_changes(&mut self) { + // To incorporate new items, we simply update our timestamp to now, so that items from previous + // instances get added. We then clear the file state so that we remap the file. Note that this + // is somewhat expensive because we will be going back over old items. An optimization would be + // to preserve old_item_offsets so that they don't have to be recomputed. (However, then items + // *deleted* in other instances would not show up here). + let new_timestamp = SystemTime::now(); + + // If for some reason the clock went backwards, we don't want to start dropping items; therefore + // we only do work if time has progressed. This also makes multiple calls cheap. + if new_timestamp > self.boundary_timestamp { + self.boundary_timestamp = new_timestamp; + self.clear_file_state(); + + // We also need to erase new items, since we go through those first, and that means we + // will not properly interleave them with items from other instances. + // We'll pick them up from the file (#2312) + // TODO: this will drop items that had no_persist set, how can we avoid that while still + // properly interleaving? + self.save(false); + self.new_items.clear(); + self.first_unwritten_new_item_index = 0; + } + } + + /// Gets all the history into a list. This is intended for the $history environment variable. + /// This may be long! + fn get_history(&mut self) -> Vec { + let mut result = vec![]; + // If we have a pending item, we skip the first encountered (i.e. last) new item. + let mut next_is_pending = self.has_pending_item; + let mut seen = HashSet::new(); + + // Append new items. + for item in self.new_items.iter().rev() { + // Skip a pending item if we have one. + if next_is_pending { + next_is_pending = false; + continue; + } + + if seen.insert(item.str().to_owned()) { + result.push(item.str().to_owned()) + } + } + + // Append old items. + self.load_old_if_needed(); + for &offset in self.old_item_offsets.iter().rev() { + let Some(item) = self.file_contents.as_ref().unwrap().decode_item(offset) else { + continue; + }; + if seen.insert(item.str().to_owned()) { + result.push(item.str().to_owned()); + } + } + result + } + + /// Let indexes be a list of one-based indexes into the history, matching the interpretation of + /// $history. That is, $history[1] is the most recently executed command. Values less than one + /// are skipped. Return a mapping from index to history item text. + fn items_at_indexes( + &mut self, + indexes: impl IntoIterator, + ) -> HashMap { + let mut result = HashMap::new(); + for idx in indexes { + // If this is the first time the index is encountered, we have to go fetch the item. + #[allow(clippy::map_entry)] // looks worse + if !result.contains_key(&idx) { + // New key. + let contents = match self.item_at_index(idx) { + None => WString::new(), + Some(Cow::Borrowed(HistoryItem { contents, .. })) => contents.clone(), + Some(Cow::Owned(HistoryItem { contents, .. })) => contents, + }; + result.insert(idx, contents); + } + } + result + } + + /// Sets the valid file paths for the history item with the given identifier. + fn set_valid_file_paths(&mut self, valid_file_paths: Vec, ident: HistoryIdentifier) { + // 0 identifier is used to mean "not necessary". + if ident == 0 { + return; + } + + // Look for an item with the given identifier. It is likely to be at the end of new_items. + for item in self.new_items.iter_mut().rev() { + if item.identifier == ident { + // found it + item.required_paths = valid_file_paths; + break; + } + } + } + + /// Return the specified history at the specified index. 0 is the index of the current + /// commandline. (So the most recent item is at index 1.) + fn item_at_index(&mut self, mut idx: usize) -> Option> { + // 0 is considered an invalid index. + assert!(idx > 0); + idx -= 1; + + // Determine how many "resolved" (non-pending) items we have. We can have at most one pending + // item, and it's always the last one. + let mut resolved_new_item_count = self.new_items.len(); + if self.has_pending_item && resolved_new_item_count > 0 { + resolved_new_item_count -= 1; + } + + // idx == 0 corresponds to the last resolved item. + if idx < resolved_new_item_count { + return Some(Cow::Borrowed( + &self.new_items[resolved_new_item_count - idx - 1], + )); + } + + // Now look in our old items. + idx -= resolved_new_item_count; + self.load_old_if_needed(); + if let Some(file_contents) = &self.file_contents { + let old_item_count = self.old_item_offsets.len(); + if idx < old_item_count { + // idx == 0 corresponds to last item in old_item_offsets. + let offset = self.old_item_offsets[old_item_count - idx - 1]; + return file_contents.decode_item(offset).map(Cow::Owned); + } + } + + // Index past the valid range, so return None. + return None; + } + + /// Return the number of history entries. + fn size(&mut self) -> usize { + let mut new_item_count = self.new_items.len(); + if self.has_pending_item && new_item_count > 0 { + new_item_count -= 1; + } + self.load_old_if_needed(); + let old_item_count = self.old_item_offsets.len(); + return new_item_count + old_item_count; + } + + /// Maybe lock a history file. + /// Returns `true` if successful, `false` if locking was skipped. + /// + /// # Safety + /// + /// `fd` and `lock_type` must be valid arguments to `flock(2)`. + unsafe fn maybe_lock_file(fd: RawFd, lock_type: libc::c_int) -> bool { + assert!(lock_type & LOCK_UN == 0, "Do not use lock_file to unlock"); + + // Don't lock if it took too long before, if we are simulating a failing lock, or if our history + // is on a remote filesystem. + if ABANDONED_LOCKING.load() { + return false; + } + if CHAOS_MODE.load() { + return false; + } + if path_get_data_remoteness() == DirRemoteness::remote { + return false; + } + + let start_time = SystemTime::now(); + let retval = unsafe { flock(fd, lock_type) }; + if let Ok(duration) = start_time.elapsed() { + if duration > Duration::from_millis(250) { + FLOG!( + warning, + wgettext_fmt!( + "Locking the history file took too long (%.3f seconds).", + duration.as_secs_f64() + ) + ); + ABANDONED_LOCKING.store(true); + } + } + retval != -1 + } + + /// Unlock a history file. + /// + /// # Safety + /// + /// `fd` must be a valid argument to `flock(2)` with `LOCK_UN`. + unsafe fn unlock_file(fd: RawFd) { + unsafe { + libc::flock(fd, LOCK_UN); + } + } +} + +// Returns the fd of an opened temporary file, or None on failure. +fn create_temporary_file(name_template: &wstr) -> Option<(AutoCloseFd, WString)> { + for _attempt in 0..10 { + let narrow_str = wcs2zstring(name_template); + let (fd, narrow_str) = fish_mkstemp_cloexec(narrow_str); + let out_fd = AutoCloseFd::new(fd); + if out_fd.is_valid() { + return Some((out_fd, str2wcstring(narrow_str.to_bytes()))); + } + } + None +} + +fn string_could_be_path(potential_path: &wstr) -> bool { + // Assume that things with leading dashes aren't paths. + return !(potential_path.is_empty() || potential_path.starts_with('-')); +} + +/// Perform a search of `hist` for `search_string`. Invoke a function `func` for each match. If +/// `func` returns [`ControlFlow::Break`], stop the search. +fn do_1_history_search( + hist: Arc, + search_type: SearchType, + search_string: WString, + case_sensitive: bool, + mut func: impl FnMut(&HistoryItem) -> ControlFlow<(), ()>, + cancel_check: &CancelChecker, +) { + let mut searcher = HistorySearch::new_with( + hist, + search_string, + search_type, + if case_sensitive { + SearchFlags::empty() + } else { + SearchFlags::IGNORE_CASE + }, + 0, + ); + while !cancel_check() && searcher.go_to_next_match(SearchDirection::Backward) { + if let ControlFlow::Break(()) = func(searcher.current_item()) { + break; + } + } +} + +/// Formats a single history record, including a trailing newline. +/// +/// Returns nothing. The only possible failure involves formatting the timestamp. If that happens we +/// simply omit the timestamp from the output. +fn format_history_record( + item: &HistoryItem, + show_time_format: Option<&str>, + null_terminate: bool, +) -> WString { + let mut result = WString::new(); + let seconds = time_to_seconds(item.timestamp()); + let seconds = seconds as libc::time_t; + let mut timestamp: libc::tm = unsafe { std::mem::zeroed() }; + if let Some(show_time_format) = show_time_format.and_then(|s| CString::new(s).ok()) { + if !unsafe { libc::localtime_r(&seconds, &mut timestamp).is_null() } { + const max_tstamp_length: usize = 100; + let mut timestamp_str = [0_u8; max_tstamp_length]; + // The libc crate fails to declare strftime on BSD. + #[cfg(feature = "bsd")] + extern "C" { + fn strftime( + buf: *mut libc::c_char, + maxsize: usize, + format: *const libc::c_char, + timeptr: *const libc::tm, + ) -> usize; + } + #[cfg(not(feature = "bsd"))] + use libc::strftime; + if unsafe { + strftime( + &mut timestamp_str[0] as *mut u8 as *mut libc::c_char, + max_tstamp_length, + show_time_format.as_ptr(), + ×tamp, + ) + } != 0 + { + result.push_utfstr(&cstr2wcstring(×tamp_str[..])); + } + } + } + + result.push_utfstr(item.str()); + result.push(if null_terminate { '\0' } else { '\n' }); + result +} + +/// Decide whether we ought to import a bash history line into fish. This is a very crude heuristic. +fn should_import_bash_history_line(line: &wstr) -> bool { + if line.is_empty() { + return false; + } + + // The following are Very naive tests! + + // Skip comments. + if line.starts_with('#') { + return false; + } + + // Skip lines with backticks because we don't have that syntax, + // Skip brace expansions and globs because they don't work like ours + // Skip lines with literal tabs since we don't handle them well and we don't know what they + // mean. It could just be whitespace or it's actually passed somewhere (like e.g. `sed`). + // Skip lines that end with a backslash. We do not handle multiline commands from bash history. + if line + .chars() + .any(|c| matches!(c, '`' | '{' | '*' | '\t' | '\\')) + { + return false; + } + + // Skip lines with [[...]] and ((...)) since we don't handle those constructs. + // "<<" here is a proxy for heredocs (and herestrings). + for seq in [L!("[["), L!("]]"), L!("(("), L!("))"), L!("<<")] { + if find_subslice(seq, line.as_char_slice()).is_some() { + return false; + } + } + + if Ast::parse(line, ParseTreeFlags::empty(), None).errored() { + return false; + } + + // In doing this test do not allow incomplete strings. Hence the "false" argument. + let mut errors = Vec::new(); + let _ = parse_util_detect_errors(line, Some(&mut errors), false); + errors.is_empty() +} + +pub struct History(Mutex); + +impl History { + fn imp(&self) -> MutexGuard { + self.0.lock().unwrap() + } + + /// Privately add an item. If pending, the item will not be returned by history searches until a + /// call to resolve_pending. Any trailing ephemeral items are dropped. + /// Exposed for testing. + pub fn add(&self, item: HistoryItem, pending: bool /*=false*/) { + self.imp().add(item, pending, true) + } + + /// Exposed for testing. + pub fn add_commandline(&self, s: WString) { + let mut imp = self.imp(); + let when = imp.timestamp_now(); + let item = HistoryItem::new(s, when, 0, PersistenceMode::Disk); + imp.add(item, false, true) + } + + pub fn new(name: &wstr) -> Arc { + Arc::new(Self(Mutex::new(HistoryImpl::new(name.to_owned())))) + } + + /// Returns history with the given name, creating it if necessary. + pub fn with_name(name: &wstr) -> Arc { + let mut histories = HISTORIES.lock().unwrap(); + + if let Some(hist) = histories.get(name) { + Arc::clone(hist) + } else { + let hist = Self::new(name); + histories.insert(name.to_owned(), Arc::clone(&hist)); + hist + } + } + + /// Returns whether this is using the default name. + pub fn is_default(&self) -> bool { + self.imp().is_default() + } + + /// Determines whether the history is empty. + pub fn is_empty(&self) -> bool { + self.imp().is_empty() + } + + /// Remove a history item. + pub fn remove(&self, s: WString) { + self.imp().remove(s) + } + + /// Remove any trailing ephemeral items. + pub fn remove_ephemeral_items(&self) { + self.imp().remove_ephemeral_items() + } + + /// Add a new pending history item to the end, and then begin file detection on the items to + /// determine which arguments are paths. Arguments may be expanded (e.g. with PWD and variables) + /// using the given `vars`. The item has the given `persist_mode`. + pub fn add_pending_with_file_detection( + self: Arc, + s: &wstr, + vars: EnvDyn, + persist_mode: PersistenceMode, /*=disk*/ + ) { + // We use empty items as sentinels to indicate the end of history. + // Do not allow them to be added (#6032). + if s.is_empty() { + return; + } + + // Find all arguments that look like they could be file paths. + let mut needs_sync_write = false; + let ast = Ast::parse(s, ParseTreeFlags::empty(), None); + + let mut potential_paths = Vec::new(); + for node in ast.walk() { + if let Some(arg) = node.as_argument() { + let potential_path = arg.source(s); + if string_could_be_path(potential_path) { + potential_paths.push(potential_path.to_owned()); + } + } else if let Some(stmt) = node.as_decorated_statement() { + // Hack hack hack - if the command is likely to trigger an exit, then don't do + // background file detection, because we won't be able to write it to our history file + // before we exit. + // Also skip it for 'echo'. This is because echo doesn't take file paths, but also + // because the history file test wants to find the commands in the history file + // immediately after running them, so it can't tolerate the asynchronous file detection. + if stmt.decoration() == StatementDecoration::exec { + needs_sync_write = true; + } + + let source = stmt.command.source(s); + let command = unescape_string(source, UnescapeStringStyle::default()); + let command = command.as_deref().unwrap_or(source); + if [L!("exit"), L!("reboot"), L!("restart"), L!("echo")].contains(&command) { + needs_sync_write = true; + } + } + } + + // If we got a path, we'll perform file detection for autosuggestion hinting. + let wants_file_detection = !potential_paths.is_empty() && !needs_sync_write; + let mut imp = self.imp(); + + // Make our history item. + let when = imp.timestamp_now(); + let identifier = imp.next_identifier(); + let item = HistoryItem::new(s.to_owned(), when, identifier, persist_mode); + + if wants_file_detection { + imp.disable_automatic_saving(); + + // Add the item. Then check for which paths are valid on a background thread, + // and unblock the item. + // Don't hold the lock while we perform this file detection. + imp.add(item, /*pending=*/ true, /*do_save=*/ true); + drop(imp); + iothread_perform(move || { + // Don't hold the lock while we perform this file detection. + let validated_paths = expand_and_detect_paths(potential_paths, &vars); + let mut imp = self.imp(); + imp.set_valid_file_paths(validated_paths, identifier); + imp.enable_automatic_saving(); + }); + } else { + // Add the item. + // If we think we're about to exit, save immediately, regardless of any disabling. This may + // cause us to lose file hinting for some commands, but it beats losing history items. + imp.add(item, /*pending=*/ true, /*do_save=*/ true); + if needs_sync_write { + imp.save(false); + } + } + } + + /// Resolves any pending history items, so that they may be returned in history searches. + pub fn resolve_pending(&self) { + self.imp().resolve_pending() + } + + /// Saves history. + pub fn save(&self) { + self.imp().save(false) + } + + /// Searches history. + #[allow(clippy::too_many_arguments)] + pub fn search( + self: &Arc, + search_type: SearchType, + search_args: &[&wstr], + show_time_format: Option<&str>, + max_items: usize, + case_sensitive: bool, + null_terminate: bool, + reverse: bool, + cancel_check: &CancelChecker, + streams: &mut IoStreams, + ) -> bool { + let mut remaining = max_items; + let mut collected = Vec::new(); + let mut output_error = false; + + // The function we use to act on each item. + let mut func = |item: &HistoryItem| { + if remaining == 0 { + return ControlFlow::Break(()); + } + remaining -= 1; + let formatted_record = format_history_record(item, show_time_format, null_terminate); + if reverse { + // We need to collect this for later. + collected.push(formatted_record); + } else { + // We can output this immediately. + if !streams.out.append(formatted_record) { + // This can happen if the user hit Ctrl-C to abort (maybe after the first page?). + output_error = true; + return ControlFlow::Break(()); + } + } + ControlFlow::Continue(()) + }; + + if search_args.is_empty() { + // The user had no search terms; just append everything. + do_1_history_search( + Arc::clone(self), + SearchType::MatchEverything, + WString::new(), + false, + &mut func, + cancel_check, + ); + } else { + for search_string in search_args.iter().copied() { + if search_string.is_empty() { + streams + .err + .append(L!("Searching for the empty string isn't allowed")); + return false; + } + do_1_history_search( + Arc::clone(self), + search_type, + search_string.to_owned(), + case_sensitive, + &mut func, + cancel_check, + ); + } + } + + // Output any items we collected (which only happens in reverse). + for item in collected.into_iter().rev() { + if output_error { + break; + } + + if !streams.out.append(item) { + // Don't force an error if output was aborted (typically via Ctrl-C/SIGINT); just don't + // try writing any more. + output_error = true; + } + } + + // We are intentionally not returning false in case of an output error, as the user aborting the + // output early (the most common case) isn't a reason to exit w/ a non-zero status code. + true + } + + /// Irreversibly clears history. + pub fn clear(&self) { + self.imp().clear() + } + + /// Irreversibly clears history for the current session. + pub fn clear_session(&self) { + self.imp().clear_session() + } + + /// Populates from older location (in config path, rather than data path). + pub fn populate_from_config_path(&self) { + self.imp().populate_from_config_path() + } + + /// Populates from a bash history file. + pub fn populate_from_bash(&self, contents: R) { + self.imp().populate_from_bash(contents) + } + + /// Incorporates the history of other shells into this history. + pub fn incorporate_external_changes(&self) { + self.imp().incorporate_external_changes() + } + + /// Gets all the history into a list. This is intended for the $history environment variable. + /// This may be long! + pub fn get_history(&self) -> Vec { + self.imp().get_history() + } + + /// Let indexes be a list of one-based indexes into the history, matching the interpretation of + /// `$history`. That is, `$history[1]` is the most recently executed command. + /// Returns a mapping from index to history item text. + pub fn items_at_indexes( + &self, + indexes: impl IntoIterator, + ) -> HashMap { + self.imp().items_at_indexes(indexes) + } + + /// Return the specified history at the specified index. 0 is the index of the current + /// commandline. (So the most recent item is at index 1.) + pub fn item_at_index(&self, idx: usize) -> Option { + self.imp().item_at_index(idx).map(Cow::into_owned) + } + + /// Return the number of history entries. + pub fn size(&self) -> usize { + self.imp().size() + } +} + +bitflags! { + /// Flags for history searching. + #[derive(Clone, Copy, Default)] + pub struct SearchFlags: u32 { + /// If set, ignore case. + const IGNORE_CASE = 1 << 0; + /// If set, do not deduplicate, which can help performance. + const NO_DEDUP = 1 << 1; + } +} + +/// Support for searching a history backwards. +/// Note this does NOT de-duplicate; it is the caller's responsibility to do so. +pub struct HistorySearch { + /// The history in which we are searching. + history: Arc, + /// The original search term. + orig_term: WString, + /// The (possibly lowercased) search term. + canon_term: WString, + /// Our search type. + search_type: SearchType, // history_search_type_t::contains + /// Our flags. + flags: SearchFlags, // 0 + /// The current history item. + current_item: Option, + /// Index of the current history item. + current_index: usize, // 0 + /// If deduping, the items we've seen. + deduper: HashSet, +} + +impl HistorySearch { + pub fn new(hist: Arc, s: WString) -> Self { + Self::new_with(hist, s, SearchType::Contains, SearchFlags::default(), 0) + } + pub fn new_with_type(hist: Arc, s: WString, search_type: SearchType) -> Self { + Self::new_with(hist, s, search_type, SearchFlags::default(), 0) + } + pub fn new_with_flags(hist: Arc, s: WString, flags: SearchFlags) -> Self { + Self::new_with(hist, s, SearchType::Contains, flags, 0) + } + /// Constructs a new history search. + pub fn new_with( + hist: Arc, + s: WString, + search_type: SearchType, + flags: SearchFlags, + starting_index: usize, + ) -> Self { + let mut search = Self { + history: hist, + orig_term: s.clone(), + canon_term: s, + search_type, + flags, + current_item: None, + current_index: starting_index, + deduper: HashSet::new(), + }; + + if search.ignores_case() { + search.canon_term = search.canon_term.to_lowercase(); + } + + search + } + + /// Returns the original search term. + pub fn original_term(&self) -> &wstr { + &self.orig_term + } + + fn original_term_ffi(&self) -> UniquePtr { + self.original_term().to_ffi() + } + + /// Finds the next search result. Returns `true` if one was found. + pub fn go_to_next_match(&mut self, direction: SearchDirection) -> bool { + let invalid_index = match direction { + SearchDirection::Backward => usize::MAX, + SearchDirection::Forward => 0, + _ => unreachable!(), + }; + + if self.current_index == invalid_index { + return false; + } + + let mut index = self.current_index; + loop { + // Backwards means increasing our index. + match direction { + SearchDirection::Backward => index += 1, + SearchDirection::Forward => index -= 1, + _ => unreachable!(), + }; + + if self.current_index == invalid_index { + return false; + } + + // We're done if it's empty or we cancelled. + let Some(item) = self.history.item_at_index(index) else { + return false; + }; + + assert!(!item.is_empty()); + + // Look for an item that matches and (if deduping) that we haven't seen before. + if !item.matches_search(&self.canon_term, self.search_type, !self.ignores_case()) { + continue; + } + + // Skip if deduplicating. + if self.dedup() && !self.deduper.insert(item.str().to_owned()) { + continue; + } + + // This is our new item. + self.current_item = Some(item); + self.current_index = index; + return true; + } + } + + /// Returns the current search result item. + /// + /// # Panics + /// + /// This function panics if there is no current item. + pub fn current_item(&self) -> &HistoryItem { + self.current_item.as_ref().expect("No current item") + } + + /// Returns the current search result item contents. + /// + /// # Panics + /// + /// This function panics if there is no current item. + pub fn current_string(&self) -> &wstr { + self.current_item().str() + } + + fn current_string_ffi(&self) -> UniquePtr { + self.current_string().to_ffi() + } + + /// Returns the index of the current history item. + pub fn current_index(&self) -> usize { + self.current_index + } + + /// Returns whether we are case insensitive. + pub fn ignores_case(&self) -> bool { + self.flags.contains(SearchFlags::IGNORE_CASE) + } + + /// Returns whether we deduplicate items. + fn dedup(&self) -> bool { + !self.flags.contains(SearchFlags::NO_DEDUP) + } +} + +/// Saves the new history to disk. +pub fn save_all() { + for hist in HISTORIES.lock().unwrap().values() { + hist.save(); + } +} + +/// Return the prefix for the files to be used for command and read history. +pub fn history_session_id(vars: &dyn Environment) -> WString { + let Some(var) = vars.get(L!("fish_history")) else { + return DFLT_FISH_HISTORY_SESSION_ID.to_owned(); + }; + + let session_id = var.as_string(); + if session_id.is_empty() || valid_var_name(&session_id) { + session_id + } else { + FLOG!( + error, + wgettext_fmt!( + "History session ID '%ls' is not a valid variable name. Falling back to `%ls`.", + &session_id, + DFLT_FISH_HISTORY_SESSION_ID + ), + ); + DFLT_FISH_HISTORY_SESSION_ID.to_owned() + } +} + +/// Given a list of proposed paths and a context, perform variable and home directory expansion, +/// and detect if the result expands to a value which is also the path to a file. +/// Wildcard expansions are suppressed - see implementation comments for why. +/// +/// This is used for autosuggestion hinting. If we add an item to history, and one of its arguments +/// refers to a file, then we only want to suggest it if there is a valid file there. +/// This does disk I/O and may only be called in a background thread. +pub fn expand_and_detect_paths>( + paths: P, + vars: &dyn Environment, +) -> Vec { + assert_is_background_thread(); + let working_directory = vars.get_pwd_slash(); + let ctx = OperationContext::background(vars, EXPANSION_LIMIT_BACKGROUND); + let mut result = Vec::new(); + for path in paths { + // Suppress cmdsubs since we are on a background thread and don't want to execute fish + // script. + // Suppress wildcards because we want to suggest e.g. `rm *` even if the directory + // is empty (and so rm will fail); this is nevertheless a useful command because it + // confirms the directory is empty. + let mut expanded_path = path.clone(); + if expand_one( + &mut expanded_path, + ExpandFlags::SKIP_CMDSUBST | ExpandFlags::SKIP_WILDCARDS, + &ctx, + None, + ) && path_is_valid(&expanded_path, &working_directory) + { + // Note we return the original (unexpanded) path. + result.push(path); + } + } + + result +} + +/// Given a list of proposed paths and a context, expand each one and see if it refers to a file. +/// Wildcard expansions are suppressed. +/// Returns `true` if `paths` is empty or every path is valid. +pub fn all_paths_are_valid>( + paths: P, + ctx: &OperationContext<'_>, +) -> bool { + assert_is_background_thread(); + let working_directory = ctx.vars().get_pwd_slash(); + for mut path in paths { + if ctx.check_cancel() { + return false; + } + if !expand_one( + &mut path, + ExpandFlags::SKIP_CMDSUBST | ExpandFlags::SKIP_WILDCARDS, + ctx, + None, + ) { + return false; + } + if !path_is_valid(&path, &working_directory) { + return false; + } + } + true +} + +fn all_paths_are_valid_ffi(paths: &wcstring_list_ffi_t, ctx: &OperationContext<'_>) -> bool { + all_paths_are_valid(paths.from_ffi(), ctx) +} /// Sets private mode on. Once in private mode, it cannot be turned off. pub fn start_private_mode(vars: &EnvStack) { - vars.set_one(L!("fish_history"), EnvMode::GLOBAL, "".into()); - vars.set_one(L!("fish_private_mode"), EnvMode::GLOBAL, "1".into()); + vars.set_one(L!("fish_history"), EnvMode::GLOBAL, L!("").to_owned()); + vars.set_one(L!("fish_private_mode"), EnvMode::GLOBAL, L!("1").to_owned()); +} + +/// Queries private mode status. +pub fn in_private_mode(vars: &dyn Environment) -> bool { + vars.get_unless_empty(L!("fish_private_mode")).is_some() +} + +/// Whether to force the read path instead of mmap. This is useful for testing. +static NEVER_MMAP: RelaxedAtomicBool = RelaxedAtomicBool::new(false); + +/// Whether we're in maximum chaos mode, useful for testing. +/// This causes things like locks to fail. +pub static CHAOS_MODE: RelaxedAtomicBool = RelaxedAtomicBool::new(false); + +// ======== +// FFI crud +// ======== + +struct ItemIndexes(HashMap); + +impl ItemIndexes { + fn get(&self, index: usize) -> UniquePtr { + self.0 + .get(&index) + .map(|s| s.to_ffi()) + .unwrap_or_else(UniquePtr::null) + } +} + +fn rust_history_item_new( + s: &CxxWString, + when: i64, + ident: u64, + persist_mode: PersistenceMode, +) -> Box { + let s = s.from_ffi(); + let when = if when < 0 { + UNIX_EPOCH - Duration::from_secs(u64::try_from(-when).unwrap()) + } else { + UNIX_EPOCH + Duration::from_secs(u64::try_from(when).unwrap()) + }; + Box::new(HistoryItem::new(s, when, ident, persist_mode)) +} + +pub struct HistorySharedPtr(pub Arc); + +impl HistorySharedPtr { + fn is_default(&self) -> bool { + self.0.is_default() + } + fn is_empty(&self) -> bool { + self.0.is_empty() + } + fn remove(&self, s: &CxxWString) { + self.0.remove(s.from_ffi()) + } + fn remove_ephemeral_items(&self) { + self.0.remove_ephemeral_items() + } + fn resolve_pending(&self) { + self.0.resolve_pending() + } + fn save(&self) { + self.0.save() + } + #[allow(clippy::too_many_arguments)] + fn search( + &self, + search_type: SearchType, + search_args: &wcstring_list_ffi_t, + show_time_format: &UniquePtr, + max_items: usize, + case_sensitive: bool, + null_terminate: bool, + reverse: bool, + cancel_on_signal: bool, + streams: &mut IoStreams, + ) -> bool { + let show_time_format = if show_time_format.is_null() { + None + } else { + Some(show_time_format.from_ffi().to_string()) + }; + let search_args = search_args.from_ffi(); + let search_args: Vec<&wstr> = search_args.iter().map(|s| s.as_ref()).collect(); + let cancel_checker = move || { + if cancel_on_signal { + signal_check_cancel() != 0 + } else { + false + } + }; + Arc::clone(&self.0).search( + search_type, + &search_args, + show_time_format.as_deref(), + max_items, + case_sensitive, + null_terminate, + reverse, + &{ Box::new(cancel_checker) as _ }, + streams, + ) + } + fn clear(&self) { + self.0.clear() + } + fn clear_session(&self) { + self.0.clear_session() + } + fn populate_from_config_path(&self) { + self.0.populate_from_config_path() + } + fn populate_from_bash(&self, filename: &CxxWString) { + let file = AutoCloseFd::new(wopen_cloexec(&filename.from_ffi(), O_RDONLY, 0)); + if !file.is_valid() { + return; + } + self.0.populate_from_bash(BufReader::new(file)) + } + fn incorporate_external_changes(&self) { + self.0.incorporate_external_changes() + } + fn get_history(&self) -> UniquePtr { + self.0.get_history().to_ffi() + } + fn items_at_indexes(&self, indexes: &[isize]) -> Box { + Box::new(ItemIndexes(self.0.items_at_indexes( + indexes.iter().filter_map(|&n| n.try_into().ok()), + ))) + } + fn item_at_index(&self, idx: usize) -> Box { + Box::new(self.0.item_at_index(idx).unwrap_or_else(|| HistoryItem { + contents: WString::new(), + creation_timestamp: UNIX_EPOCH, + required_paths: vec![], + identifier: 0, + persist_mode: PersistenceMode::Disk, + })) + } + fn size(&self) -> usize { + self.0.size() + } + fn clone(&self) -> Box { + Box::new(Self(Arc::clone(&self.0))) + } + fn add_pending_with_file_detection_ffi( + &self, + s: &CxxWString, + vars: &EnvStackRefFFI, + persist_mode: PersistenceMode, + ) { + Arc::clone(&self.0).add_pending_with_file_detection( + s.as_wstr(), + vars.0.snapshot(), + persist_mode, + ) + } +} + +fn history_with_name(name: &CxxWString) -> Box { + Box::new(HistorySharedPtr(History::with_name(&name.from_ffi()))) +} + +fn rust_history_search_new( + hist: &HistorySharedPtr, + s: &CxxWString, + search_type: SearchType, + flags: u32, + starting_index: usize, +) -> Box { + Box::new(HistorySearch::new_with( + Arc::clone(&hist.0), + s.from_ffi(), + search_type, + SearchFlags::from_bits(flags).unwrap(), + starting_index, + )) +} + +fn rust_session_id(vars: &EnvStackRefFFI) -> UniquePtr { + history_session_id(&*vars.0).to_ffi() +} + +fn rust_expand_and_detect_paths( + paths: &wcstring_list_ffi_t, + vars: &EnvStackRefFFI, +) -> UniquePtr { + expand_and_detect_paths(paths.from_ffi(), &*vars.0).to_ffi() +} + +fn rust_all_paths_are_valid(paths: &wcstring_list_ffi_t, ctx: &OperationContext<'_>) -> bool { + all_paths_are_valid(paths.from_ffi(), ctx) +} + +fn start_private_mode_ffi(vars: &EnvStackRefFFI) { + start_private_mode(&vars.0) +} + +fn in_private_mode_ffi(vars: &EnvStackRefFFI) -> bool { + in_private_mode(&*vars.0) +} + +unsafe impl cxx::ExternType for HistoryItem { + type Id = cxx::type_id!("HistoryItem"); + type Kind = cxx::kind::Opaque; } diff --git a/fish-rust/src/history/file.rs b/fish-rust/src/history/file.rs new file mode 100644 index 000000000..80d1db3e6 --- /dev/null +++ b/fish-rust/src/history/file.rs @@ -0,0 +1,558 @@ +//! Implemention of history files. + +use std::{ + io::Write, + ops::{Deref, DerefMut}, + os::fd::RawFd, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; + +use errno::errno; +use libc::{ + c_void, lseek, mmap, munmap, EINTR, MAP_ANONYMOUS, MAP_FAILED, MAP_PRIVATE, PROT_READ, + PROT_WRITE, SEEK_END, SEEK_SET, +}; + +use crate::{ + common::{str2wcstring, subslice_position, wcs2string}, + flog::FLOG, + path::{path_get_config_remoteness, DirRemoteness}, +}; + +use super::{history_ffi::PersistenceMode, HistoryItem}; + +/// History file types. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum HistoryFileType { + Fish2_0, + Fish1_x, +} + +/// A type wrapping up the logic around mmap and munmap. +struct MmapRegion { + ptr: *mut u8, + len: usize, +} + +impl MmapRegion { + /// Creates a new mmap'ed region. + /// + /// # Safety + /// + /// `ptr` must be the result of a successful `mmap()` call with length `len`. + unsafe fn new(ptr: *mut u8, len: usize) -> Self { + assert!(ptr.cast() != MAP_FAILED); + assert!(len > 0); + Self { ptr, len } + } + + /// Map a region `[0, len)` from an `fd`. + /// Returns [`None`] on failure. + pub fn map_file(fd: RawFd, len: usize) -> Option { + if len == 0 { + return None; + } + + let ptr = unsafe { mmap(std::ptr::null_mut(), len, PROT_READ, MAP_PRIVATE, fd, 0) }; + if ptr == MAP_FAILED { + return None; + } + + // SAFETY: mmap of `len` was successful and returned `ptr` + Some(unsafe { Self::new(ptr.cast(), len) }) + } + + /// Map anonymous memory of a given length. + /// Returns [`None`] on failure. + pub fn map_anon(len: usize) -> Option { + if len == 0 { + return None; + } + + let ptr = unsafe { + mmap( + std::ptr::null_mut(), + len, + PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, + -1, + 0, + ) + }; + if ptr == MAP_FAILED { + return None; + } + + // SAFETY: mmap of `len` was successful and returned `ptr` + Some(unsafe { Self::new(ptr.cast(), len) }) + } +} + +// SAFETY: MmapRegion has exclusive mutable access to the region +unsafe impl Send for MmapRegion {} +// SAFETY: MmapRegion does not offer interior mutability +unsafe impl Sync for MmapRegion {} + +impl Deref for MmapRegion { + type Target = [u8]; + + fn deref(&self) -> &[u8] { + unsafe { std::slice::from_raw_parts(self.ptr, self.len) } + } +} + +impl DerefMut for MmapRegion { + fn deref_mut(&mut self) -> &mut [u8] { + unsafe { std::slice::from_raw_parts_mut(self.ptr, self.len) } + } +} + +impl Drop for MmapRegion { + fn drop(&mut self) { + unsafe { munmap(self.ptr.cast(), self.len) }; + } +} + +/// HistoryFileContents holds the read-only contents of a file. +pub struct HistoryFileContents { + region: MmapRegion, +} + +impl HistoryFileContents { + /// Construct a history file contents from a file descriptor. The file descriptor is not closed. + pub fn create(fd: RawFd) -> Option { + // Check that the file is seekable, and its size. + let len = unsafe { lseek(fd, 0, SEEK_END) }; + let Ok(len) = usize::try_from(len) else { + return None; + }; + let mmap_file_directly = should_mmap(); + let mut region = if mmap_file_directly { + MmapRegion::map_file(fd, len)? + } else { + MmapRegion::map_anon(len)? + }; + + // If we mapped anonymous memory, we have to read from the file. + if !mmap_file_directly { + if unsafe { lseek(fd, 0, SEEK_SET) } != 0 { + return None; + } + if !read_from_fd(fd, region.as_mut()) { + return None; + } + } + + region.try_into().ok() + } + + /// Decode an item at a given offset. + pub fn decode_item(&self, offset: usize) -> Option { + let contents = &self.region[offset..]; + decode_item_fish_2_0(contents) + } + + /// Support for iterating item offsets. + /// The cursor should initially be 0. + /// If cutoff is given, skip items whose timestamp is newer than cutoff. + /// Returns the offset of the next item, or [`None`] on end. + pub fn offset_of_next_item( + &self, + cursor: &mut usize, + cutoff: Option, + ) -> Option { + offset_of_next_item_fish_2_0(self.contents(), cursor, cutoff) + } + + /// Returns a view of the file contents. + pub fn contents(&self) -> &[u8] { + &self.region + } +} + +/// Try to infer the history file type based on inspecting the data. +fn infer_file_type(contents: &[u8]) -> HistoryFileType { + assert!(!contents.is_empty(), "File should never be empty"); + if contents[0] == b'#' { + HistoryFileType::Fish1_x + } else { + // assume new fish + HistoryFileType::Fish2_0 + } +} + +impl TryFrom for HistoryFileContents { + type Error = (); + + fn try_from(region: MmapRegion) -> Result { + let type_ = infer_file_type(®ion); + if type_ == HistoryFileType::Fish1_x { + FLOG!(error, "unsupported history file format 1.x"); + return Err(()); + } + Ok(Self { region }) + } +} + +/// Append a history item to a buffer, in preparation for outputting it to the history file. +pub fn append_history_item_to_buffer(item: &HistoryItem, buffer: &mut Vec) { + assert!(item.should_write_to_disk(), "Item should not be persisted"); + + let mut cmd = wcs2string(item.str()); + escape_yaml_fish_2_0(&mut cmd); + buffer.extend(b"- cmd: "); + buffer.extend(&cmd); + buffer.push(b'\n'); + writeln!(buffer, " when: {}", time_to_seconds(item.timestamp())).unwrap(); + + let paths = item.get_required_paths(); + if !paths.is_empty() { + writeln!(buffer, " paths:").unwrap(); + for path in paths { + let mut path = wcs2string(path); + escape_yaml_fish_2_0(&mut path); + buffer.extend(b" - "); + buffer.extend(&path); + buffer.push(b'\n'); + } + } +} + +/// Check if we should mmap the fd. +/// Don't try mmap() on non-local filesystems. +fn should_mmap() -> bool { + if super::NEVER_MMAP.load() { + return false; + } + + // mmap only if we are known not-remote. + return path_get_config_remoteness() == DirRemoteness::local; +} + +/// Read from `fd` to fill `dest`, zeroing any unused space. +// Return true on success, false on failure. +fn read_from_fd(fd: RawFd, dest: &mut [u8]) -> bool { + let mut remaining = dest.len(); + let mut nread = 0; + while remaining > 0 { + let amt = + unsafe { libc::read(fd, (&mut dest[nread]) as *mut u8 as *mut c_void, remaining) }; + if amt < 0 { + if errno().0 != EINTR { + return false; + } + } else if amt == 0 { + break; + } else { + remaining -= amt as usize; + nread += amt as usize; + } + } + dest[nread..].fill(0u8); + true +} + +fn replace_all(s: &mut Vec, needle: &[u8], replacement: &[u8]) { + let mut offset = 0; + while let Some(relpos) = subslice_position(&s[offset..], needle) { + offset += relpos; + s.splice(offset..(offset + needle.len()), replacement.iter().copied()); + offset += replacement.len(); + } +} + +/// Support for escaping and unescaping the nonstandard "yaml" format introduced in fish 2.0. +fn escape_yaml_fish_2_0(s: &mut Vec) { + replace_all(s, b"\\", b"\\\\"); // replace one backslash with two + replace_all(s, b"\n", b"\\n"); // replace newline with backslash + literal n +} + +/// This function is called frequently, so it ought to be fast. +fn unescape_yaml_fish_2_0(s: &mut Vec) { + let mut cursor = 0; + while cursor < s.len() { + // Look for a backslash. + let Some(backslash) = s[cursor..].iter().position(|&c| c == b'\\') else { + // No more backslashes + break; + }; + + // Add back the start offset + let backslash = backslash + cursor; + + // Backslash found. Maybe we'll do something about it. + let Some(escaped_char) = s.get(backslash + 1) else { + // Backslash was final character + break; + }; + + match escaped_char { + b'\\' => { + // Two backslashes in a row. Delete the second one. + s.remove(backslash + 1); + } + b'n' => { + // Backslash + n. Replace with a newline. + s.splice(backslash..(backslash + 2), [b'\n']); + } + _ => { + // Unknown backslash escape, keep as-is + } + }; + + // The character at index backslash has now been made whole; start at the next + // character. + cursor = backslash + 1; + } + + debug_assert!(std::str::from_utf8(s).is_ok()); +} + +/// Read one line, stripping off any newline, returning the number of bytes consumed. +fn read_line(data: &[u8]) -> (usize, &[u8]) { + // Locate the newline. + if let Some(newline) = data.iter().position(|&c| c == b'\n') { + // we found a newline + let line = &data[..newline]; + // Return the amount to advance the cursor; skip over the newline. + (newline + 1, line) + } else { + // We ran off the end. + (data.len(), b"") + } +} + +fn trim_start(s: &[u8]) -> &[u8] { + &s[s.iter().take_while(|c| c.is_ascii_whitespace()).count()..] +} + +/// Trims leading spaces in the given string, returning how many there were. +fn trim_leading_spaces(s: &[u8]) -> (usize, &[u8]) { + let count = s.iter().take_while(|c| **c == b' ').count(); + (count, &s[count..]) +} + +fn extract_prefix_and_unescape_yaml(line: &[u8]) -> Option<(Vec, Vec)> { + let mut split = line.splitn(2, |c| *c == b':'); + let key = split.next().unwrap(); + let value = split.next()?; + assert!(split.next().is_none()); + + let mut key = key.to_owned(); + // Skip a space after the : if necessary. + let mut value = trim_start(value).to_owned(); + + unescape_yaml_fish_2_0(&mut key); + unescape_yaml_fish_2_0(&mut value); + Some((key, value)) +} + +/// Decode an item via the fish 2.0 format. +fn decode_item_fish_2_0(mut data: &[u8]) -> Option { + let (advance, line) = read_line(data); + let line = trim_start(line); + let Some((key, value)) = extract_prefix_and_unescape_yaml(line) else { + return None; + }; + + if key != b"- cmd" { + return None; + } + + data = &data[advance..]; + let cmd = str2wcstring(&value); + + // Read the remaining lines. + let mut indent = None; + let mut when = UNIX_EPOCH; + let mut paths = Vec::new(); + loop { + let (advance, line) = read_line(data); + + let (this_indent, line) = trim_leading_spaces(line); + let indent = *indent.get_or_insert(this_indent); + if this_indent == 0 || indent != this_indent { + break; + } + + let Some((key, value)) = extract_prefix_and_unescape_yaml(line) else { + break; + }; + + // We are definitely going to consume this line. + data = &data[advance..]; + + if key == b"when" { + // Parse an int from the timestamp. Should this fail, 0 is acceptable. + when = time_from_seconds( + std::str::from_utf8(&value) + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(0), + ); + } else if key == b"paths" { + // Read lines starting with " - " until we can't read any more. + loop { + let (advance, line) = read_line(data); + let (leading_spaces, line) = trim_leading_spaces(line); + if leading_spaces <= indent { + break; + } + + let Some(line) = line.strip_prefix(b"- ") else { + break; + }; + + // We're going to consume this line. + data = &data[advance..]; + + let mut line = line.to_owned(); + unescape_yaml_fish_2_0(&mut line); + paths.push(str2wcstring(&line)); + } + } + } + + let mut result = HistoryItem::new(cmd, when, 0, PersistenceMode::Disk); + result.set_required_paths(paths); + Some(result) +} + +fn time_from_seconds(offset: i64) -> SystemTime { + if let Ok(n) = u64::try_from(offset) { + UNIX_EPOCH + Duration::from_secs(n) + } else { + UNIX_EPOCH - Duration::from_secs(offset.unsigned_abs()) + } +} + +pub fn time_to_seconds(ts: SystemTime) -> i64 { + match ts.duration_since(UNIX_EPOCH) { + Ok(d) => { + // after epoch + i64::try_from(d.as_secs()).unwrap() + } + Err(e) => { + // before epoch + -i64::try_from(e.duration().as_secs()).unwrap() + } + } +} + +/// Parse a timestamp line that looks like this: spaces, "when:", spaces, timestamp, newline +/// We know the string contains a newline, so stop when we reach it. +fn parse_timestamp(s: &[u8]) -> Option { + let s = trim_start(s); + + let Some(s) = s.strip_prefix(b"when:") else { + return None; + }; + + let s = trim_start(s); + + std::str::from_utf8(s) + .ok() + .and_then(|s| s.parse().ok()) + .map(time_from_seconds) +} + +fn complete_lines(s: &[u8]) -> impl Iterator { + let mut lines = s.split(|&c| c == b'\n'); + // Remove either the last empty element (in case last line is newline-terminated) or the + // trailing non-newline-terminated line + lines.next_back(); + lines +} + +/// Support for iteratively locating the offsets of history items. +/// Pass the file contents and a mutable reference to a `cursor`, initially 0. +/// If `cutoff_timestamp` is given, skip items created at or after that timestamp. +/// Returns [`None`] when done. +fn offset_of_next_item_fish_2_0( + contents: &[u8], + cursor: &mut usize, + cutoff_timestamp: Option, +) -> Option { + let mut lines = complete_lines(&contents[*cursor..]).peekable(); + while let Some(mut line) = lines.next() { + // Skip lines with a leading space, since these are in the interior of one of our items. + if line.starts_with(b" ") { + continue; + } + + // Try to be a little YAML compatible. Skip lines with leading %, ---, or ... + if line.starts_with(b"%") || line.starts_with(b"---") || line.starts_with(b"...") { + continue; + } + + // Hackish: fish 1.x rewriting a fish 2.0 history file can produce lines with lots of + // leading "- cmd: - cmd: - cmd:". Trim all but one leading "- cmd:". + while line.starts_with(b"- cmd: - cmd: ") { + // Skip over just one of the - cmd. In the end there will be just one left. + line = line.strip_prefix(b"- cmd: ").unwrap(); + } + + // Hackish: fish 1.x rewriting a fish 2.0 history file can produce commands like "when: + // 123456". Ignore those. + if line.starts_with(b"- cmd: when:") { + continue; + } + + // At this point, we know `line` is at the beginning of an item. But maybe we want to + // skip this item because of timestamps. A `None` cutoff means we don't care; if we do care, + // then try parsing out a timestamp. + if let Some(cutoff_timestamp) = cutoff_timestamp { + // Hackish fast way to skip items created after our timestamp. This is the mechanism by + // which we avoid "seeing" commands from other sessions that started after we started. + // We try hard to ensure that our items are sorted by their timestamps, so in theory we + // could just break, but I don't think that works well if (for example) the clock + // changes. So we'll read all subsequent items. + // Walk over lines that we think are interior. These lines are not null terminated, but + // are guaranteed to contain a newline. + let mut timestamp = None; + loop { + let Some(interior_line) = lines.next_if(|l| l.starts_with(b" ")) else { + // If the first character is not a space, it's not an interior line, so we're done. + break; + }; + + // Try parsing a timestamp from this line. If we succeed, the loop will break. + timestamp = parse_timestamp(interior_line); + if timestamp.is_some() { + break; + } + } + + // Skip this item if the timestamp is past our cutoff. + if let Some(timestamp) = timestamp { + if timestamp > cutoff_timestamp { + continue; + } + } + } + + // We made it through the gauntlet. + + /// # Safety + /// + /// Both `from` and `to` must be derived from the same slice. + unsafe fn offset(from: &[u8], to: &[u8]) -> usize { + let from = from.as_ptr(); + let to = to.as_ptr(); + // SAFETY: from and to are derived from the same slice, slices can't be longer than + // isize::MAX + let offset = unsafe { to.offset_from(from) }; + offset.try_into().unwrap() + } + + // Advance the cursor past the last line of this entry + *cursor = match lines.next() { + Some(next_line) => unsafe { offset(contents, next_line) }, + None => contents.len(), + }; + + return Some(unsafe { offset(contents, line) }); + } + + None +} diff --git a/fish-rust/src/input.rs b/fish-rust/src/input.rs new file mode 100644 index 000000000..e158ab36b --- /dev/null +++ b/fish-rust/src/input.rs @@ -0,0 +1,4 @@ +use crate::wchar::{wstr, L}; + +pub const FISH_BIND_MODE_VAR: &wstr = L!("fish_bind_mode"); +pub const DEFAULT_BIND_MODE: &wstr = L!("default"); diff --git a/fish-rust/src/input_common.rs b/fish-rust/src/input_common.rs new file mode 100644 index 000000000..963e0584f --- /dev/null +++ b/fish-rust/src/input_common.rs @@ -0,0 +1,21 @@ +use crate::env::{EnvStack, Environment}; +use crate::wchar::prelude::*; +use crate::wchar_ffi::WCharToFFI; + +pub fn update_wait_on_escape_ms(vars: &EnvStack) { + let fish_escape_delay_ms = vars.get_unless_empty(L!("fish_escape_delay_ms")); + let is_empty = fish_escape_delay_ms.is_none(); + let value = fish_escape_delay_ms + .map(|s| s.as_string().to_ffi()) + .unwrap_or(L!("").to_ffi()); + crate::ffi::update_wait_on_escape_ms_ffi(is_empty, &value); +} + +pub fn update_wait_on_sequence_key_ms(vars: &EnvStack) { + let fish_sequence_key_delay_ms = vars.get_unless_empty(L!("fish_sequence_key_delay_ms")); + let is_empty = fish_sequence_key_delay_ms.is_none(); + let value = fish_sequence_key_delay_ms + .map(|s| s.as_string().to_ffi()) + .unwrap_or(L!("").to_ffi()); + crate::ffi::update_wait_on_sequence_key_ms_ffi(is_empty, &value); +} diff --git a/fish-rust/src/io.rs b/fish-rust/src/io.rs index 1dbdf92b3..403ad171a 100644 --- a/fish-rust/src/io.rs +++ b/fish-rust/src/io.rs @@ -8,18 +8,24 @@ }; use crate::flog::{should_flog, FLOG, FLOGF}; use crate::global_safety::RelaxedAtomicBool; -use crate::job_group::JobGroup; use crate::path::path_apply_working_directory; +use crate::proc::JobGroupRef; use crate::redirection::{RedirectionMode, RedirectionSpecList}; use crate::signal::SigChecker; use crate::topic_monitor::topic_t; use crate::wchar::prelude::*; +use crate::wchar_ffi::WCharFromFFI; use crate::wutil::{perror, perror_io, wdirname, wstat, wwrite_to_fd}; +use cxx::CxxWString; use errno::Errno; -use libc::{EAGAIN, EEXIST, EINTR, ENOENT, ENOTDIR, EPIPE, EWOULDBLOCK, O_EXCL, STDERR_FILENO}; -use std::cell::UnsafeCell; -use std::sync::{Arc, Condvar, Mutex, MutexGuard, RwLock, RwLockReadGuard}; -use std::{os::fd::RawFd, rc::Rc}; +use libc::{ + EAGAIN, EEXIST, EINTR, ENOENT, ENOTDIR, EPIPE, EWOULDBLOCK, O_EXCL, STDERR_FILENO, + STDOUT_FILENO, +}; +use std::cell::{RefCell, UnsafeCell}; +use std::os::fd::RawFd; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{Arc, Condvar, Mutex, MutexGuard}; /// separated_buffer_t represents a buffer of output from commands, prepared to be turned into a /// variable. For example, command substitutions output into one of these. Most commands just @@ -167,7 +173,7 @@ fn try_add_size(&mut self, delta: usize) -> bool { } /// Describes what type of IO operation an io_data_t represents. -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum IoMode { file, pipe, @@ -188,8 +194,29 @@ pub trait IoData { fn print(&self); // The address of the object, for comparison. fn as_ptr(&self) -> *const (); + fn as_bufferfill(&self) -> Option<&IoBufferfill> { + None + } } +// todo!("this should be safe because of how it's used. Rationalize this better.") +pub trait IoDataSync: IoData + Send + Sync {} +unsafe impl Send for IoClose {} +unsafe impl Send for IoFd {} +unsafe impl Send for IoFile {} +unsafe impl Send for IoPipe {} +unsafe impl Send for IoBufferfill {} +unsafe impl Sync for IoClose {} +unsafe impl Sync for IoFd {} +unsafe impl Sync for IoFile {} +unsafe impl Sync for IoPipe {} +unsafe impl Sync for IoBufferfill {} +impl IoDataSync for IoClose {} +impl IoDataSync for IoFd {} +impl IoDataSync for IoFile {} +impl IoDataSync for IoPipe {} +impl IoDataSync for IoBufferfill {} + pub struct IoClose { fd: RawFd, } @@ -327,14 +354,19 @@ pub struct IoBufferfill { write_fd: AutoCloseFd, /// The receiving buffer. - buffer: Arc>, + buffer: Arc, } impl IoBufferfill { + /// Create an io_bufferfill_t which, when written from, fills a buffer with the contents. + /// \returns nullptr on failure, e.g. too many open fds. + pub fn create() -> Option> { + Self::create_opts(0, STDOUT_FILENO) + } /// Create an io_bufferfill_t which, when written from, fills a buffer with the contents. /// \returns nullptr on failure, e.g. too many open fds. /// /// \param target the fd which this will be dup2'd to - typically stdout. - pub fn create(buffer_limit: usize, target: RawFd) -> Option> { + pub fn create_opts(buffer_limit: usize, target: RawFd) -> Option> { assert!(target >= 0, "Invalid target fd"); // Construct our pipes. @@ -351,31 +383,33 @@ pub fn create(buffer_limit: usize, target: RawFd) -> Option> { } } // Our fillthread gets the read end of the pipe; out_pipe gets the write end. - let buffer = Arc::new(RwLock::new(IoBuffer::new(buffer_limit))); + let buffer = Arc::new(IoBuffer::new(buffer_limit)); begin_filling(&buffer, pipes.read); assert!(pipes.write.is_valid(), "fd is not valid"); - Some(Rc::new(IoBufferfill { + Some(Arc::new(IoBufferfill { target, write_fd: pipes.write, buffer, })) } - pub fn buffer(&self) -> RwLockReadGuard<'_, IoBuffer> { - self.buffer.read().unwrap() + pub fn buffer_ref(&self) -> &Arc { + &self.buffer + } + + pub fn buffer(&self) -> &IoBuffer { + &self.buffer } /// Reset the receiver (possibly closing the write end of the pipe), and complete the fillthread /// of the buffer. \return the buffer. - pub fn finish(filler: IoBufferfill) -> SeparatedBuffer { + pub fn finish(filler: Arc) -> SeparatedBuffer { // The io filler is passed in. This typically holds the only instance of the write side of the // pipe used by the buffer's fillthread (except for that side held by other processes). Get the // buffer out of the bufferfill and clear the shared_ptr; this will typically widow the pipe. // Then allow the buffer to finish. filler .buffer - .write() - .unwrap() .complete_background_fillthread_and_take_buffer() } } @@ -400,6 +434,9 @@ fn print(&self) { fn as_ptr(&self) -> *const () { (self as *const Self).cast() } + fn as_bufferfill(&self) -> Option<&IoBufferfill> { + Some(self) + } } /// An io_buffer_t is a buffer which can populate itself by reading from an fd. @@ -413,19 +450,24 @@ pub struct IoBuffer { /// A promise, allowing synchronization with the background fill operation. /// The operation has a reference to this as well, and fulfills this promise when it exits. - fill_waiter: Option, Condvar)>>, + #[allow(clippy::type_complexity)] + fill_waiter: RefCell, Condvar)>>>, /// The item id of our background fillthread fd monitor item. - item_id: FdMonitorItemId, + item_id: AtomicU64, } +// safety: todo!("rationalize why fill_waiter is safe") +unsafe impl Send for IoBuffer {} +unsafe impl Sync for IoBuffer {} + impl IoBuffer { pub fn new(limit: usize) -> Self { IoBuffer { buffer: Mutex::new(SeparatedBuffer::new(limit)), shutdown_fillthread: RelaxedAtomicBool::new(false), - fill_waiter: None, - item_id: FdMonitorItemId::from(0), + fill_waiter: RefCell::new(None), + item_id: AtomicU64::new(0), } } @@ -473,27 +515,28 @@ pub fn read_once(fd: RawFd, buffer: &mut MutexGuard<'_, SeparatedBuffer>) -> isi } /// End the background fillthread operation, and return the buffer, transferring ownership. - pub fn complete_background_fillthread_and_take_buffer(&mut self) -> SeparatedBuffer { + pub fn complete_background_fillthread_and_take_buffer(&self) -> SeparatedBuffer { // Mark that our fillthread is done, then wake it up. assert!(self.fillthread_running(), "Should have a fillthread"); assert!( - self.item_id != FdMonitorItemId::from(0), + self.item_id.load(Ordering::SeqCst) != 0, "Should have a valid item ID" ); self.shutdown_fillthread.store(true); - fd_monitor().poke_item(self.item_id); + fd_monitor().poke_item(FdMonitorItemId::from(self.item_id.load(Ordering::SeqCst))); // Wait for the fillthread to fulfill its promise, and then clear the future so we know we no // longer have one. - let (mutex, condvar) = &**(self.fill_waiter.as_ref().unwrap()); + let mut promise = self.fill_waiter.borrow_mut(); + let (mutex, condvar) = &**promise.as_ref().unwrap(); { let mut done = mutex.lock().unwrap(); while !*done { done = condvar.wait(done).unwrap(); } } - self.fill_waiter = None; + *promise = None; // Return our buffer, transferring ownership. let mut locked_buff = self.buffer.lock().unwrap(); @@ -505,16 +548,13 @@ pub fn complete_background_fillthread_and_take_buffer(&mut self) -> SeparatedBuf /// Helper to return whether the fillthread is running. pub fn fillthread_running(&self) -> bool { - return self.fill_waiter.is_some(); + return self.fill_waiter.borrow().is_some(); } } /// Begin the fill operation, reading from the given fd in the background. -fn begin_filling(iobuffer: &Arc>, fd: AutoCloseFd) { - assert!( - !iobuffer.read().unwrap().fillthread_running(), - "Already have a fillthread" - ); +fn begin_filling(iobuffer: &Arc, fd: AutoCloseFd) { + assert!(!iobuffer.fillthread_running(), "Already have a fillthread"); // We want to fill buffer_ by reading from fd. fd is the read end of a pipe; the write end is // owned by another process, or something else writing in fish. @@ -536,8 +576,7 @@ fn begin_filling(iobuffer: &Arc>, fd: AutoCloseFd) { // complete_background_fillthread(). Note that TSan complains if the promise's dtor races with // the future's call to wait(), so we store the promise, not just its future (#7681). let promise = Arc::new((Mutex::new(false), Condvar::new())); - iobuffer.write().unwrap().fill_waiter = Some(promise.clone()); - + iobuffer.fill_waiter.replace(Some(promise.clone())); // Run our function to read until the receiver is closed. // It's OK to capture 'buffer' because 'this' waits for the promise in its dtor. let item_callback: Option = { @@ -551,16 +590,14 @@ fn begin_filling(iobuffer: &Arc>, fd: AutoCloseFd) { let mut done = false; if reason == ItemWakeReason::Readable { // select() reported us as readable; read a bit. - let iobuf = iobuffer.write().unwrap(); - let mut buf = iobuf.buffer.lock().unwrap(); + let mut buf = iobuffer.buffer.lock().unwrap(); let ret = IoBuffer::read_once(fd.fd(), &mut buf); done = ret == 0 || (ret < 0 && ![EAGAIN, EWOULDBLOCK].contains(&errno::errno().0)); - } else if iobuffer.read().unwrap().shutdown_fillthread.load() { + } else if iobuffer.shutdown_fillthread.load() { // Here our caller asked us to shut down; read while we keep getting data. // This will stop when the fd is closed or if we get EAGAIN. - let iobuf = iobuffer.write().unwrap(); - let mut buf = iobuf.buffer.lock().unwrap(); + let mut buf = iobuffer.buffer.lock().unwrap(); loop { let ret = IoBuffer::read_once(fd.fd(), &mut buf); if ret <= 0 { @@ -572,34 +609,45 @@ fn begin_filling(iobuffer: &Arc>, fd: AutoCloseFd) { if done { fd.close(); let (mutex, condvar) = &*promise; - let mut done = mutex.lock().unwrap(); - *done = true; + { + let mut done = mutex.lock().unwrap(); + *done = true; + } condvar.notify_one(); } }, )) }; - iobuffer.write().unwrap().item_id = - fd_monitor().add(FdMonitorItem::new(fd, None, item_callback)); + let item_id = fd_monitor().add(FdMonitorItem::new(fd, None, item_callback)); + iobuffer.item_id.store(u64::from(item_id), Ordering::SeqCst); } -pub type IoDataRef = Rc; +pub type IoDataRef = Arc; -#[derive(Default)] +#[derive(Clone, Default)] pub struct IoChain(pub Vec); +unsafe impl cxx::ExternType for IoChain { + type Id = cxx::type_id!("IoChain"); + type Kind = cxx::kind::Opaque; +} + impl IoChain { pub fn new() -> Self { Default::default() } - pub fn remove(&mut self, element: &IoDataRef) { - let element = Rc::as_ptr(element) as *const (); + pub fn remove(&mut self, element: &dyn IoDataSync) { + let element = element as *const _; + let element = element as *const (); self.0.retain(|e| { - let e = Rc::as_ptr(e) as *const (); + let e = Arc::as_ptr(e) as *const (); !std::ptr::eq(e, element) }); } + pub fn clear(&mut self) { + self.0.clear() + } pub fn push(&mut self, element: IoDataRef) { self.0.push(element); } @@ -623,12 +671,12 @@ pub fn append_from_specs(&mut self, specs: &RedirectionSpecList, pwd: &wstr) -> match spec.mode { RedirectionMode::fd => { if spec.is_close() { - self.push(Rc::new(IoClose::new(spec.fd))); + self.push(Arc::new(IoClose::new(spec.fd))); } else { let target_fd = spec .get_target_as_fd() .expect("fd redirection should have been validated already"); - self.push(Rc::new(IoFd::new(spec.fd, target_fd))); + self.push(Arc::new(IoFd::new(spec.fd, target_fd))); } } _ => { @@ -677,11 +725,11 @@ pub fn append_from_specs(&mut self, specs: &RedirectionSpecList, pwd: &wstr) -> // If opening a file fails, insert a closed FD instead of the file redirection // and return false. This lets execution potentially recover and at least gives // the shell a chance to gracefully regain control of the shell (see #7038). - self.push(Rc::new(IoClose::new(spec.fd))); + self.push(Arc::new(IoClose::new(spec.fd))); have_error = true; continue; } - self.push(Rc::new(IoFile::new(spec.fd, file))); + self.push(Arc::new(IoFile::new(spec.fd, file))); } } } @@ -916,7 +964,7 @@ fn flush_and_check_error(&mut self) -> libc::c_int { } } -pub struct NativeIoStreams<'a> { +pub struct IoStreams<'a> { // Streams for out and err. pub out: &'a mut OutputStream, pub err: &'a mut OutputStream, @@ -941,17 +989,22 @@ pub struct NativeIoStreams<'a> { pub err_is_redirected: bool, // Actual IO redirections. This is only used by the source builtin. Unowned. - io_chain: *const IoChain, + pub io_chain: *mut IoChain, // The job group of the job, if any. This enables builtins which run more code like eval() to // share pgid. // FIXME: this is awkwardly placed. - job_group: Option>, + pub job_group: Option, } -impl<'a> NativeIoStreams<'a> { +unsafe impl cxx::ExternType for IoStreams<'_> { + type Id = cxx::type_id!("IoStreams"); + type Kind = cxx::kind::Opaque; +} + +impl<'a> IoStreams<'a> { pub fn new(out: &'a mut OutputStream, err: &'a mut OutputStream) -> Self { - NativeIoStreams { + IoStreams { out, err, stdin_fd: -1, @@ -960,10 +1013,13 @@ pub fn new(out: &'a mut OutputStream, err: &'a mut OutputStream) -> Self { err_is_piped: false, out_is_redirected: false, err_is_redirected: false, - io_chain: std::ptr::null(), + io_chain: std::ptr::null_mut(), job_group: None, } } + pub fn out_is_terminal(&self) -> bool { + !self.out_is_redirected && unsafe { libc::isatty(STDOUT_FILENO) == 1 } + } } /// File redirection error message. @@ -985,3 +1041,69 @@ fn fd_monitor() -> &'static mut FdMonitor { let ptr: *mut FdMonitor = unsafe { (*FDM).get() }; unsafe { &mut *ptr } } + +#[cxx::bridge] +#[allow(clippy::needless_lifetimes)] +mod io_ffi { + extern "Rust" { + type IoChain; + type IoStreams<'a>; + type OutputStreamFfi<'a>; + + fn new_io_chain() -> Box; + + #[cxx_name = "out"] + unsafe fn out_ffi<'a>(self: &'a mut IoStreams<'a>) -> Box>; + #[cxx_name = "err"] + unsafe fn err_ffi<'a>(self: &'a mut IoStreams<'a>) -> Box>; + #[cxx_name = "out_is_redirected"] + unsafe fn out_is_redirected_ffi<'a>(self: &IoStreams<'a>) -> bool; + #[cxx_name = "stdin_is_directly_redirected"] + unsafe fn stdin_is_directly_redirected_ffi<'a>(self: &IoStreams<'a>) -> bool; + #[cxx_name = "stdin_fd"] + unsafe fn stdin_fd_ffi<'a>(self: &IoStreams<'a>) -> i32; + + #[cxx_name = "append"] + unsafe fn append_ffi<'a>(self: &mut OutputStreamFfi<'a>, s: &CxxWString) -> bool; + #[cxx_name = "push"] + unsafe fn push_ffi<'a>(self: &mut OutputStreamFfi<'a>, s: u32) -> bool; + } +} + +impl<'a> IoStreams<'a> { + fn out_ffi(&'a mut self) -> Box> { + Box::new(OutputStreamFfi(self.out)) + } + fn err_ffi(&'a mut self) -> Box> { + Box::new(OutputStreamFfi(self.err)) + } + unsafe fn out_is_redirected_ffi(&self) -> bool { + self.out_is_redirected + } + unsafe fn stdin_is_directly_redirected_ffi(&self) -> bool { + self.stdin_is_directly_redirected + } + unsafe fn stdin_fd_ffi(&self) -> i32 { + self.stdin_fd + } +} + +pub struct OutputStreamFfi<'a>(pub &'a mut OutputStream); + +unsafe impl cxx::ExternType for OutputStreamFfi<'_> { + type Id = cxx::type_id!("OutputStreamFfi"); + type Kind = cxx::kind::Opaque; +} + +impl<'a> OutputStreamFfi<'a> { + fn append_ffi(&mut self, s: &CxxWString) -> bool { + self.0.append(s.from_ffi()) + } + fn push_ffi(&mut self, s: u32) -> bool { + self.0.append_char(char::from_u32(s).unwrap()) + } +} + +fn new_io_chain() -> Box { + Box::new(IoChain::new()) +} diff --git a/fish-rust/src/job_group.rs b/fish-rust/src/job_group.rs index b8fedb595..7fbad0d1c 100644 --- a/fish-rust/src/job_group.rs +++ b/fish-rust/src/job_group.rs @@ -1,13 +1,14 @@ use self::ffi::pgid_t; -use crate::common::{assert_send, assert_sync}; use crate::global_safety::RelaxedAtomicBool; +use crate::proc::JobGroupRef; use crate::signal::Signal; use crate::wchar::prelude::*; -use crate::wchar_ffi::{WCharFromFFI, WCharToFFI}; +use crate::wchar_ffi::WCharToFFI; use cxx::{CxxWString, UniquePtr}; +use std::cell::RefCell; use std::num::NonZeroU32; use std::sync::atomic::{AtomicI32, Ordering}; -use std::sync::Mutex; +use std::sync::{Arc, Mutex}; #[cxx::bridge] mod ffi { @@ -42,34 +43,15 @@ struct pgid_t { // a SharedPtr/UniquePtr/Box and won't let us pass/return them by value/reference, either. unsafe fn get_modes_ffi(&self, size: usize) -> *const u8; /* actually `* const libc::termios` */ unsafe fn set_modes_ffi(&mut self, modes: *const u8, size: usize); /* actually `* const libc::termios` */ - - // The C++ code uses `shared_ptr` but cxx bridge doesn't support returning a - // `SharedPtr` nor does it implement `Arc` so we return a box and then - // convert `rust::box` to `std::shared_ptr` with `box_to_shared_ptr()` (from ffi.h). - fn create_job_group_ffi(command: &CxxWString, wants_job_id: bool) -> Box; - fn create_job_group_with_job_control_ffi( - command: &CxxWString, - wants_term: bool, - ) -> Box; } } -fn create_job_group_ffi(command: &CxxWString, wants_job_id: bool) -> Box { - let job_group = JobGroup::create(command.from_ffi(), wants_job_id); - Box::new(job_group) -} - -fn create_job_group_with_job_control_ffi(command: &CxxWString, wants_term: bool) -> Box { - let job_group = JobGroup::create_with_job_control(command.from_ffi(), wants_term); - Box::new(job_group) -} - /// A job id, corresponding to what is printed by `jobs`. 1 is the first valid job id. -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] #[repr(transparent)] pub struct JobId(NonZeroU32); -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct MaybeJobId(pub Option); impl std::ops::Deref for MaybeJobId { @@ -80,30 +62,27 @@ fn deref(&self) -> &Self::Target { } } +impl MaybeJobId { + pub fn as_num(&self) -> i64 { + self.0.map(|j| i64::from(u32::from(j.0))).unwrap_or(-1) + } +} + impl std::fmt::Display for MaybeJobId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0 - .map(|j| i64::from(u32::from(j.0))) - .unwrap_or(-1) - .fmt(f) + self.as_num().fmt(f) } } impl ToWString for MaybeJobId { fn to_wstring(&self) -> WString { - self.0 - .map(|j| i64::from(u32::from(j.0))) - .unwrap_or(-1) - .to_wstring() + self.as_num().to_wstring() } } impl<'a> printf_compat::args::ToArg<'a> for MaybeJobId { fn to_arg(self) -> printf_compat::args::Arg<'a> { - self.0 - .map(|j| i64::from(u32::from(j.0))) - .unwrap_or(-1) - .to_arg() + self.as_num().to_arg() } } @@ -120,7 +99,7 @@ fn to_arg(self) -> printf_compat::args::Arg<'a> { pub struct JobGroup { /// If set, the saved terminal modes of this job. This needs to be saved so that we can restore /// the terminal to the same state when resuming a stopped job. - pub tmodes: Option, + pub tmodes: RefCell>, /// Whether job control is enabled in this `JobGroup` or not. /// /// If this is set, then the first process in the root job must be external, as it will become @@ -133,7 +112,7 @@ pub struct JobGroup { pub is_foreground: RelaxedAtomicBool, /// The pgid leading our group. This is only ever set if [`job_control`](Self::JobControl) is /// true. We ensure the value (when set) is always non-negative. - pgid: Option, + pgid: RefCell>, /// The original command which produced this job tree. pub command: WString, /// Our job id, if any. `None` here should evaluate to `-1` for ffi purposes. @@ -144,8 +123,9 @@ pub struct JobGroup { signal: AtomicI32, } -const _: () = assert_send::(); -const _: () = assert_sync::(); +// safety: all fields without interior mutabillity are only written to once +unsafe impl Send for JobGroup {} +unsafe impl Sync for JobGroup {} impl JobGroup { /// Whether this job wants job control. @@ -223,26 +203,26 @@ pub fn cancel_with_signal_ffi(&self, signal: i32) { /// /// As such, this method takes `&mut self` rather than `&self` to enforce that this operation is /// only available during initial construction/initialization. - pub fn set_pgid(&mut self, pgid: libc::pid_t) { + pub fn set_pgid(&self, pgid: libc::pid_t) { assert!( self.wants_job_control(), "Should not set a pgid for a group that doesn't want job control!" ); assert!(pgid >= 0, "Invalid pgid!"); - assert!(self.pgid.is_none(), "JobGroup::pgid already set!"); + assert!(self.pgid.borrow().is_none(), "JobGroup::pgid already set!"); - self.pgid = Some(pgid); + self.pgid.replace(Some(pgid)); } /// Returns the value of [`JobGroup::pgid`]. This is never fish's own pgid! pub fn get_pgid(&self) -> Option { - self.pgid + *self.pgid.borrow() } /// Returns the value of [`JobGroup::pgid`] in a `UniquePtr` to take the place of an /// `Option` for ffi purposes. A null `UniquePtr` is equivalent to `None`. pub fn get_pgid_ffi(&self) -> cxx::UniquePtr { - match self.pgid { + match *self.pgid.borrow() { Some(value) => UniquePtr::new(pgid_t { value }), None => UniquePtr::null(), } @@ -257,6 +237,7 @@ unsafe fn get_modes_ffi(&self, size: usize) -> *const u8 { ); self.tmodes + .borrow() .as_ref() // Really cool that type inference works twice in a row here. The first `_` is deduced // from the left and the second `_` is deduced from the right (the return type). @@ -279,9 +260,9 @@ unsafe fn set_modes_ffi(&mut self, modes: *const u8, size: usize) { let modes = modes as *const libc::termios; if modes.is_null() { - self.tmodes = None; + self.tmodes.replace(None); } else { - self.tmodes = Some(*modes); + self.tmodes.replace(Some(*modes)); } } } @@ -294,7 +275,7 @@ unsafe fn set_modes_ffi(&mut self, modes: *const u8, size: usize) { static CONSUMED_JOB_IDS: Mutex> = Mutex::new(Vec::new()); impl JobId { - const NONE: MaybeJobId = MaybeJobId(None); + pub const NONE: MaybeJobId = MaybeJobId(None); pub fn new(value: NonZeroU32) -> Self { JobId(value) @@ -346,18 +327,18 @@ pub fn new(command: WString, id: MaybeJobId, job_control: bool, wants_term: bool job_control, wants_term, command, - tmodes: None, + tmodes: RefCell::default(), signal: 0.into(), is_foreground: RelaxedAtomicBool::new(false), - pgid: None, + pgid: RefCell::default(), } } /// Return a new `JobGroup` with the provided `command`. The `JobGroup` is only assigned a /// `JobId` if `wants_job_id` is true and is created with job control disabled and /// [`JobGroup::wants_term`] set to false. - pub fn create(command: WString, wants_job_id: bool) -> JobGroup { - JobGroup::new( + pub fn create(command: WString, wants_job_id: bool) -> JobGroupRef { + Arc::new(JobGroup::new( command, if wants_job_id { MaybeJobId(Some(JobId::acquire())) @@ -366,19 +347,19 @@ pub fn create(command: WString, wants_job_id: bool) -> JobGroup { }, false, /* job_control */ false, /* wants_term */ - ) + )) } /// Return a new `JobGroup` with the provided `command` with job control enabled. A [`JobId`] is /// automatically acquired and assigned. If `wants_term` is true then [`JobGroup::wants_term`] /// is also set to `true` accordingly. - pub fn create_with_job_control(command: WString, wants_term: bool) -> JobGroup { - JobGroup::new( + pub fn create_with_job_control(command: WString, wants_term: bool) -> JobGroupRef { + Arc::new(JobGroup::new( command, MaybeJobId(Some(JobId::acquire())), true, /* job_control */ wants_term, - ) + )) } } diff --git a/fish-rust/src/lib.rs b/fish-rust/src/lib.rs index 722ded5cc..7350e8dc1 100644 --- a/fish-rust/src/lib.rs +++ b/fish-rust/src/lib.rs @@ -2,13 +2,22 @@ #![allow(dead_code)] #![allow(non_upper_case_globals)] #![allow(clippy::bool_assert_comparison)] +#![allow(clippy::box_default)] +#![allow(clippy::collapsible_if)] +#![allow(clippy::comparison_chain)] #![allow(clippy::derivable_impls)] #![allow(clippy::field_reassign_with_default)] +#![allow(clippy::if_same_then_else)] #![allow(clippy::manual_is_ascii_check)] +#![allow(clippy::mut_from_ref)] #![allow(clippy::needless_return)] #![allow(clippy::option_map_unit_fn)] #![allow(clippy::ptr_arg)] +#![allow(clippy::redundant_slicing)] +#![allow(clippy::too_many_arguments)] #![allow(clippy::uninlined_format_args)] +#![allow(clippy::unnecessary_to_owned)] +#![allow(clippy::unnecessary_unwrap)] pub const BUILD_VERSION: &str = match option_env!("FISH_BUILD_VERSION") { Some(v) => v, @@ -20,6 +29,7 @@ mod abbrs; mod ast; +mod autoload; mod builtins; mod color; mod compat; @@ -27,7 +37,9 @@ mod curses; mod env; mod env_dispatch; +mod env_universal_common; mod event; +mod exec; mod expand; mod fallback; mod fd_monitor; @@ -50,6 +62,8 @@ mod global_safety; mod highlight; mod history; +mod input; +mod input_common; mod io; mod job_group; mod kill; @@ -59,11 +73,15 @@ mod operation_context; mod output; mod parse_constants; +mod parse_execution; mod parse_tree; mod parse_util; +mod parser; mod parser_keywords; mod path; +mod pointer; mod print_help; +mod proc; mod re; mod reader; mod redirection; @@ -87,5 +105,6 @@ mod wildcard; mod wutil; -// Don't use `#[cfg(test)]` here to make sure ffi tests are built and tested +#[cfg(any(test, feature = "fish-ffi-tests"))] +#[allow(unused_imports)] // Easy way to suppress warnings while we have two testing modes. mod tests; diff --git a/fish-rust/src/null_terminated_array.rs b/fish-rust/src/null_terminated_array.rs index 9f97acc41..df5d06509 100644 --- a/fish-rust/src/null_terminated_array.rs +++ b/fish-rust/src/null_terminated_array.rs @@ -20,6 +20,31 @@ fn c_str(&self) -> *const c_char { } } +pub trait AsNullTerminatedArray { + type CharType; + fn get(&self) -> *mut *const Self::CharType; + fn iter(&self) -> NullTerminatedArrayIterator { + NullTerminatedArrayIterator { ptr: self.get() } + } +} + +// TODO This should expose strings as CStr. +pub struct NullTerminatedArrayIterator { + ptr: *mut *const CharType, +} +impl Iterator for NullTerminatedArrayIterator { + type Item = *const CharType; + fn next(&mut self) -> Option<*const CharType> { + let result = unsafe { *self.ptr }; + if result.is_null() { + None + } else { + self.ptr = unsafe { self.ptr.add(1) }; + Some(result) + } + } +} + /// This supports the null-terminated array of NUL-terminated strings consumed by exec. /// Given a list of strings, construct a vector of pointers to those strings contents. /// This is used for building null-terminated arrays of null-terminated strings. @@ -28,7 +53,8 @@ pub struct NullTerminatedArray<'p, T: NulTerminatedString + ?Sized> { _phantom: PhantomData<&'p T>, } -impl<'p, Str: NulTerminatedString + ?Sized> NullTerminatedArray<'p, Str> { +impl<'p, Str: NulTerminatedString + ?Sized> AsNullTerminatedArray for NullTerminatedArray<'p, Str> { + type CharType = Str::CharType; /// Return the list of pointers, appropriate for envp or argv. /// Note this returns a mutable array of const strings. The caller may rearrange the strings but /// not modify their contents. @@ -42,7 +68,8 @@ fn get(&self) -> *mut *const Str::CharType { ); self.pointers.as_ptr() as *mut *const Str::CharType } - +} +impl<'p, Str: NulTerminatedString + ?Sized> NullTerminatedArray<'p, Str> { /// Construct from a list of "strings". /// This holds pointers into the strings. pub fn new>(strs: &'p [S]) -> Self { @@ -76,12 +103,18 @@ pub struct OwningNullTerminatedArray { const _: () = assert_send::(); const _: () = assert_sync::(); -impl OwningNullTerminatedArray { +impl AsNullTerminatedArray for OwningNullTerminatedArray { + type CharType = c_char; /// Cover over null_terminated_array.get(). fn get(&self) -> *mut *const c_char { self.null_terminated_array.get() } +} +impl OwningNullTerminatedArray { + pub fn get_mut(&self) -> *mut *mut c_char { + self.get().cast() + } /// Construct, taking ownership of a list of strings. pub fn new(strs: Vec) -> Self { let strings = strs.into_boxed_slice(); @@ -174,6 +207,15 @@ fn test_null_terminated_array() { assert_eq!(*ptr.offset(2), ptr::null()); } } +#[test] +fn test_null_terminated_array_iter() { + let owned_strs = &[CString::new("foo").unwrap(), CString::new("bar").unwrap()]; + let strs: Vec<_> = owned_strs.iter().map(|s| s.as_c_str()).collect(); + let arr = NullTerminatedArray::new(&strs); + let v1: Vec<_> = arr.iter().collect(); + let v2: Vec<_> = owned_strs.iter().map(|s| s.as_ptr()).collect(); + assert_eq!(v1, v2); +} #[test] fn test_owning_null_terminated_array() { diff --git a/fish-rust/src/operation_context.rs b/fish-rust/src/operation_context.rs index 2875fe12d..584874029 100644 --- a/fish-rust/src/operation_context.rs +++ b/fish-rust/src/operation_context.rs @@ -1,7 +1,204 @@ -pub struct OperationContext {} +use crate::common::CancelChecker; +use crate::env::{EnvDyn, EnvDynFFI}; +use crate::env::{EnvStack, EnvStackRef, Environment}; +use crate::parser::{Parser, ParserRef}; +use crate::proc::JobGroupRef; +use once_cell::sync::Lazy; +use std::sync::Arc; -impl OperationContext { - pub fn empty() -> OperationContext { - todo!() +/// A common helper which always returns false. +pub fn no_cancel() -> bool { + false +} + +// Default limits for expansion. +/// The default maximum number of items from expansion. +pub const EXPANSION_LIMIT_DEFAULT: usize = 512 * 1024; +/// A smaller limit for background operations like syntax highlighting. +pub const EXPANSION_LIMIT_BACKGROUND: usize = 512; + +enum Vars<'a> { + // The parser, if this is a foreground operation. If this is a background operation, this may be + // nullptr. + Parser(ParserRef), + // A set of variables. + Vars(&'a dyn Environment), + + TestOnly(ParserRef, &'a dyn Environment), +} + +/// A operation_context_t is a simple property bag which wraps up data needed for highlighting, +/// expansion, completion, and more. +pub struct OperationContext<'a> { + vars: Vars<'a>, + + // The limit in the number of expansions which should be produced. + pub expansion_limit: usize, + + /// The job group of the parental job. + /// This is used only when expanding command substitutions. If this is set, any jobs created + /// by the command substitutions should use this tree. + pub job_group: Option, + + // A function which may be used to poll for cancellation. + pub cancel_checker: CancelChecker, +} + +static nullenv: Lazy = Lazy::new(|| Arc::pin(EnvStack::new())); + +impl<'a> OperationContext<'a> { + pub fn vars(&self) -> &dyn Environment { + match &self.vars { + Vars::Parser(parser) => &*parser.variables, + Vars::Vars(vars) => *vars, + Vars::TestOnly(_, vars) => *vars, + } + } + + // \return an "empty" context which contains no variables, no parser, and never cancels. + pub fn empty() -> OperationContext<'static> { + OperationContext::background(&**nullenv, EXPANSION_LIMIT_DEFAULT) + } + + // \return an operation context that contains only global variables, no parser, and never + // cancels. + pub fn globals() -> OperationContext<'static> { + OperationContext::background(&**EnvStack::globals(), EXPANSION_LIMIT_DEFAULT) + } + + /// Construct from a full set of properties. + pub fn foreground( + parser: ParserRef, + cancel_checker: CancelChecker, + expansion_limit: usize, + ) -> OperationContext<'a> { + OperationContext { + vars: Vars::Parser(parser), + expansion_limit, + job_group: None, + cancel_checker, + } + } + + pub fn test_only_foreground( + parser: ParserRef, + vars: &'a dyn Environment, + cancel_checker: CancelChecker, + ) -> OperationContext<'a> { + OperationContext { + vars: Vars::TestOnly(parser, vars), + expansion_limit: EXPANSION_LIMIT_DEFAULT, + job_group: None, + cancel_checker, + } + } + + /// Construct from vars alone. + pub fn background(vars: &'a dyn Environment, expansion_limit: usize) -> OperationContext<'a> { + OperationContext { + vars: Vars::Vars(vars), + expansion_limit, + job_group: None, + cancel_checker: Box::new(no_cancel), + } + } + + pub fn background_with_cancel_checker( + vars: &'a dyn Environment, + cancel_checker: CancelChecker, + expansion_limit: usize, + ) -> OperationContext<'a> { + OperationContext { + vars: Vars::Vars(vars), + expansion_limit, + job_group: None, + cancel_checker, + } + } + + pub fn has_parser(&self) -> bool { + matches!(self.vars, Vars::Parser(_) | Vars::TestOnly(_, _)) + } + pub fn maybe_parser(&self) -> Option<&Parser> { + match &self.vars { + Vars::Parser(parser) => Some(parser), + Vars::Vars(_) => None, + Vars::TestOnly(parser, _) => Some(parser), + } + } + pub fn parser(&self) -> &Parser { + match &self.vars { + Vars::Parser(parser) => parser, + Vars::Vars(_) => panic!(), + Vars::TestOnly(parser, _) => parser, + } + } + // Invoke the cancel checker. \return if we should cancel. + pub fn check_cancel(&self) -> bool { + (self.cancel_checker)() } } + +/// \return an operation context for a background operation.. +/// Crucially the operation context itself does not contain a parser. +/// It is the caller's responsibility to ensure the environment lives as long as the result. +pub fn get_bg_context(env: &EnvDyn, generation_count: u32) -> OperationContext { + let cancel_checker = move || { + // Cancel if the generation count changed. + generation_count != crate::ffi::read_generation_count() + }; + OperationContext::background_with_cancel_checker( + env, + Box::new(cancel_checker), + EXPANSION_LIMIT_BACKGROUND, + ) +} + +#[cxx::bridge] +#[allow(clippy::needless_lifetimes)] +mod operation_context_ffi { + extern "C++" { + include!("env_fwd.h"); + include!("parser.h"); + #[cxx_name = "EnvStackRef"] + type EnvStackRefFFI = crate::env::EnvStackRefFFI; + #[cxx_name = "EnvDyn"] + type EnvDynFFI = crate::env::EnvDynFFI; + type Parser = crate::parser::Parser; + } + extern "Rust" { + type OperationContext<'a>; + + fn empty_operation_context() -> Box>; + fn operation_context_globals() -> Box>; + fn check_cancel(&self) -> bool; + + #[cxx_name = "get_bg_context"] + unsafe fn get_bg_context_ffi( + env: &EnvDynFFI, + generation_count: u32, + ) -> Box>; + #[cxx_name = "parser_context"] + fn parser_context_ffi(parser: &Parser) -> Box>; + } +} + +fn get_bg_context_ffi(env: &EnvDynFFI, generation_count: u32) -> Box> { + Box::new(get_bg_context(&env.0, generation_count)) +} + +fn parser_context_ffi(parser: &Parser) -> Box> { + Box::new(parser.context()) +} + +unsafe impl cxx::ExternType for OperationContext<'_> { + type Id = cxx::type_id!("OperationContext"); + type Kind = cxx::kind::Opaque; +} + +fn empty_operation_context() -> Box> { + Box::new(OperationContext::empty()) +} +fn operation_context_globals() -> Box> { + Box::new(OperationContext::globals()) +} diff --git a/fish-rust/src/output.rs b/fish-rust/src/output.rs index 168afb16d..f5f068620 100644 --- a/fish-rust/src/output.rs +++ b/fish-rust/src/output.rs @@ -523,7 +523,7 @@ pub fn best_color(candidates: &[RgbColor], support: ColorSupport) -> RgbColor { /// TODO: This code should be refactored to enable sharing with builtin_set_color. /// In particular, the argument parsing still isn't fully capable. #[allow(clippy::collapsible_else_if)] -fn parse_color(var: &EnvVar, is_background: bool) -> RgbColor { +pub fn parse_color(var: &EnvVar, is_background: bool) -> RgbColor { let mut is_bold = false; let mut is_underline = false; let mut is_italics = false; @@ -604,7 +604,7 @@ fn make_buffering_outputter_ffi() -> Box { Box::new(Outputter::new_buffering()) } -type RgbColorFFI = crate::ffi::rgb_color_t; +pub type RgbColorFFI = crate::ffi::rgb_color_t; use crate::wchar_ffi::AsWstr; impl Outputter { fn set_color_ffi(&mut self, fg: &RgbColorFFI, bg: &RgbColorFFI) { diff --git a/fish-rust/src/parse_constants.rs b/fish-rust/src/parse_constants.rs index 507d82540..0edf31904 100644 --- a/fish-rust/src/parse_constants.rs +++ b/fish-rust/src/parse_constants.rs @@ -1,9 +1,9 @@ //! Constants used in the programmatic representation of fish code. use crate::fallback::{fish_wcswidth, fish_wcwidth}; +use crate::ffi::wcharz_t; use crate::tokenizer::variable_assignment_equals_pos; use crate::wchar::prelude::*; -use crate::wchar_ffi::wcharz_t; use crate::wchar_ffi::{AsWstr, WCharFromFFI, WCharToFFI}; use bitflags::bitflags; use cxx::{type_id, ExternType}; @@ -110,6 +110,7 @@ pub enum ParseKeyword { } // Statement decorations like 'command' or 'exec'. + #[derive(Clone, Copy, Eq, PartialEq)] pub enum StatementDecoration { none, command, @@ -118,6 +119,7 @@ pub enum StatementDecoration { } // Parse error code list. + #[derive(Debug)] pub enum ParseErrorCode { none, @@ -248,6 +250,12 @@ fn contains_inclusive_ffi(&self, loc: u32) -> bool { } } +impl From for std::ops::Range { + fn from(value: SourceRange) -> Self { + value.start()..value.end() + } +} + impl Default for ParseTokenType { fn default() -> Self { ParseTokenType::invalid @@ -349,7 +357,7 @@ fn default() -> Self { } } -#[derive(Clone, Default)] +#[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct ParseError { /// Text of the error. pub text: WString, diff --git a/fish-rust/src/parse_execution.rs b/fish-rust/src/parse_execution.rs new file mode 100644 index 000000000..3f5cac6f4 --- /dev/null +++ b/fish-rust/src/parse_execution.rs @@ -0,0 +1,2089 @@ +//! Provides the "linkage" between an ast and actual execution structures (job_t, etc.). + +use crate::ast::{ + self, BlockStatementHeaderVariant, Keyword, Leaf, List, Node, StatementVariant, Token, +}; +use crate::builtins; +use crate::builtins::shared::{ + builtin_exists, truncate_at_nul, BUILTIN_ERR_VARNAME, STATUS_CMD_ERROR, STATUS_CMD_OK, + STATUS_CMD_UNKNOWN, STATUS_EXPAND_ERROR, STATUS_ILLEGAL_CMD, STATUS_INVALID_ARGS, + STATUS_NOT_EXECUTABLE, STATUS_UNMATCHED_WILDCARD, +}; +use crate::common::{ + escape, scoped_push_replacer, should_suppress_stderr_for_tests, valid_var_name, ScopeGuard, + ScopeGuarding, +}; +use crate::complete::CompletionList; +use crate::env::{EnvMode, EnvStackSetResult, EnvVar, EnvVarFlags, Environment, Statuses}; +use crate::event::{self, Event}; +use crate::exec::exec_job; +use crate::expand::{ + expand_one, expand_string, expand_to_command_and_args, ExpandFlags, ExpandResultCode, +}; +use crate::flog::FLOG; +use crate::function; +use crate::io::{IoChain, IoStreams, OutputStream, StringOutputStream}; +use crate::job_group::JobGroup; +use crate::operation_context::OperationContext; +use crate::parse_constants::{ + parse_error_offset_source_start, ParseError, ParseErrorCode, ParseErrorList, ParseKeyword, + ParseTokenType, StatementDecoration, CALL_STACK_LIMIT_EXCEEDED_ERR_MSG, + ERROR_NO_BRACE_GROUPING, ERROR_TIME_BACKGROUND, FAILED_EXPANSION_VARIABLE_NAME_ERR_MSG, + ILLEGAL_FD_ERR_MSG, INFINITE_FUNC_RECURSION_ERR_MSG, WILDCARD_ERR_MSG, +}; +use crate::parse_tree::{NodeRef, ParsedSourceRef}; +use crate::parse_util::parse_util_unescape_wildcards; +use crate::parser::{Block, BlockId, BlockType, LoopStatus, Parser, ProfileItem}; +use crate::path::{path_as_implicit_cd, path_try_get_path}; +use crate::pointer::ConstPointer; +use crate::proc::{ + get_job_control_mode, job_reap, no_exec, ConcreteAssignment, Job, JobControl, JobProperties, + JobRef, Process, ProcessList, ProcessType, +}; +use crate::reader::fish_is_unwinding_for_exit; +use crate::redirection::{RedirectionMode, RedirectionSpec, RedirectionSpecList}; +use crate::signal::Signal; +use crate::timer::push_timer; +use crate::tokenizer::{variable_assignment_equals_pos, PipeOrRedir}; +use crate::trace::{trace_if_enabled, trace_if_enabled_with_args}; +use crate::wchar::{wstr, WString, L}; +use crate::wchar_ext::WExt; +use crate::wildcard::wildcard_match; +use crate::wutil::{wgettext, wgettext_fmt}; +use libc::{c_int, ENOTDIR, EXIT_SUCCESS, STDERR_FILENO, STDOUT_FILENO}; +use std::cell::RefCell; +use std::io::ErrorKind; +use std::rc::Rc; +use std::sync::atomic::Ordering; + +/// An eval_result represents evaluation errors including wildcards which failed to match, syntax +/// errors, or other expansion errors. It also tracks when evaluation was skipped due to signal +/// cancellation. Note it does not track the exit status of commands. +#[derive(Eq, PartialEq)] +pub enum EndExecutionReason { + /// Evaluation was successfull. + ok, + + /// Evaluation was skipped due to control flow (break or return). + control_flow, + + /// Evaluation was cancelled, e.g. because of a signal or exit. + cancelled, + + /// A parse error or failed expansion (but not an error exit status from a command). + error, +} + +#[derive(Default)] +pub struct ParseExecutionContext { + pstree: RefCell>, + + // If set, one of our processes received a cancellation signal (INT or QUIT) so we are + // unwinding. + cancel_signal: RefCell>, + + // The currently executing job node, used to indicate the line number. + // todo!("use NonNull instead of ConstPointer?"); + executing_job_node: RefCell>>, + + // Cached line number information. + cached_lineno: RefCell, + + /// The block IO chain. + /// For example, in `begin; foo ; end < file.txt` this would have the 'file.txt' IO. + block_io: RefCell, +} + +#[derive(Default)] +struct CachedLineno { + offset: usize, + count: usize, +} + +impl ParseExecutionContext { + pub fn swap(left: &Self, right: Box) -> Box { + left.pstree.swap(&right.pstree); + left.cancel_signal.swap(&right.cancel_signal); + left.executing_job_node.swap(&right.executing_job_node); + left.cached_lineno.swap(&right.cached_lineno); + left.block_io.swap(&right.block_io); + right + } +} + +// Report an error, setting $status to \p status. Always returns +// 'end_execution_reason_t::error'. +macro_rules! report_error { + ( $self:ident, $ctx:expr, $status:expr, $node:expr, $fmt:expr $(, $arg:expr )* $(,)? ) => { + report_error_formatted!($self, $ctx, $status, $node, wgettext_fmt!($fmt $(, $arg )*)) + }; +} +macro_rules! report_error_formatted { + ( $self:ident, $ctx:expr, $status:expr, $node:expr, $text:expr $(,)? ) => {{ + let r = $node.source_range(); + // Create an error. + let mut error = ParseError::default(); + error.source_start = r.start(); + error.source_length = r.length(); + error.code = ParseErrorCode::syntax; // hackish + error.text = $text; + $self.report_errors($ctx, $status, &vec![error]) + }}; +} + +impl<'a> ParseExecutionContext { + /// Construct a context in preparation for evaluating a node in a tree, with the given block_io. + /// The execution context may access the parser and parent job group (if any) through ctx. + pub fn new(pstree: ParsedSourceRef, block_io: IoChain) -> Self { + Self { + pstree: RefCell::new(Some(pstree)), + cancel_signal: RefCell::default(), + executing_job_node: RefCell::default(), + cached_lineno: RefCell::default(), + block_io: RefCell::new(block_io), + } + } + + pub fn pstree(&self) -> ParsedSourceRef { + // todo!("don't clone but expose a Ref<'_, ParsedSourceRef> or similar") + self.pstree.borrow().as_ref().unwrap().clone() + } + + /// Returns the current line number, indexed from 1. Updates cached line ranges. + pub fn get_current_line_number(&self) -> Option { + let line_offset = self.line_offset_of_executing_node()?; + // The offset is 0 based; the number is 1 based. + Some(line_offset + 1) + } + + /// Returns the source offset, or -1. + pub fn get_current_source_offset(&self) -> Option { + self.executing_job_node + .borrow() + .and_then(|job| job.try_source_range()) + .map(|range| range.start()) + } + + /// Returns the source string. + pub fn get_source(&self) -> WString { + // todo!("don't clone"); + self.pstree().src.clone() + } + + pub fn eval_node( + &self, + ctx: &OperationContext<'_>, + node: &dyn Node, + associated_block: Option, + ) -> EndExecutionReason { + match node.typ() { + ast::Type::statement => { + self.eval_statement(ctx, node.as_statement().unwrap(), associated_block) + } + ast::Type::job_list => { + self.eval_job_list(ctx, node.as_job_list().unwrap(), associated_block.unwrap()) + } + _ => unreachable!(), + } + } + + /// Start executing at the given node. Returns 0 if there was no error, 1 if there was an + /// error. + fn eval_statement( + &self, + ctx: &OperationContext<'_>, + statement: &'a ast::Statement, + associated_block: Option, + ) -> EndExecutionReason { + // Note we only expect block-style statements here. No not statements. + let contents = &statement.contents; + match &**contents { + StatementVariant::BlockStatement(block) => { + self.run_block_statement(ctx, block, associated_block) + } + StatementVariant::IfStatement(ifstat) => { + self.run_if_statement(ctx, ifstat, associated_block) + } + StatementVariant::SwitchStatement(switchstat) => { + self.run_switch_statement(ctx, switchstat) + } + StatementVariant::DecoratedStatement(_) + | StatementVariant::NotStatement(_) + | StatementVariant::None => panic!(), + } + } + + fn eval_job_list( + &self, + ctx: &OperationContext<'_>, + job_list: &'a ast::JobList, + associated_block: BlockId, + ) -> EndExecutionReason { + // Check for infinite recursion: a function which immediately calls itself.. + let mut func_name = WString::new(); + if let Some(infinite_recursive_node) = + self.infinite_recursive_statement_in_job_list(ctx, job_list, &mut func_name) + { + // We have an infinite recursion. + return report_error!( + self, + ctx, + STATUS_CMD_ERROR.unwrap(), + infinite_recursive_node, + INFINITE_FUNC_RECURSION_ERR_MSG, + func_name + ); + } + + // Check for stack overflow in case of function calls (regular stack overflow) or string + // substitution blocks, which can be recursively called with eval (issue #9302). + let block_type = { + let blocks = ctx.parser().blocks(); + blocks.get(associated_block).unwrap().typ() + }; + if (block_type == BlockType::top && ctx.parser().function_stack_is_overflowing()) + || (block_type == BlockType::subst && ctx.parser().is_eval_depth_exceeded()) + { + return report_error!( + self, + ctx, + STATUS_CMD_ERROR.unwrap(), + job_list, + CALL_STACK_LIMIT_EXCEEDED_ERR_MSG + ); + } + self.run_job_list(ctx, job_list, Some(associated_block)) + } + + // Check to see if we should end execution. + // \return the eval result to end with, or none() to continue on. + // This will never return end_execution_reason_t::ok. + fn check_end_execution(&self, ctx: &OperationContext<'_>) -> Option { + // If one of our jobs ended with SIGINT, we stop execution. + // Likewise if fish itself got a SIGINT, or if something ran exit, etc. + if self.cancel_signal.borrow().is_some() + || ctx.check_cancel() + || fish_is_unwinding_for_exit() + { + return Some(EndExecutionReason::cancelled); + } + let parser = ctx.parser(); + let ld = &parser.libdata().pods; + if ld.exit_current_script { + return Some(EndExecutionReason::cancelled); + } + if ld.returning { + return Some(EndExecutionReason::control_flow); + } + if ld.loop_status != LoopStatus::normals { + return Some(EndExecutionReason::control_flow); + } + None + } + + fn report_errors( + &self, + ctx: &OperationContext<'_>, + status: c_int, + error_list: &ParseErrorList, + ) -> EndExecutionReason { + if !ctx.check_cancel() { + if error_list.is_empty() { + FLOG!(error, "Error reported but no error text found."); + } + + // Get a backtrace. + let backtrace_and_desc = ctx.parser().get_backtrace(&self.pstree().src, error_list); + + // Print it. + if !should_suppress_stderr_for_tests() { + fwprintf!(STDERR_FILENO, "%ls", backtrace_and_desc); + } + + // Mark status. + ctx.parser().set_last_statuses(Statuses::just(status)); + } + EndExecutionReason::error + } + + /// Command not found support. + fn handle_command_not_found( + &self, + ctx: &OperationContext<'_>, + cmd: &wstr, + statement: &ast::DecoratedStatement, + err: std::io::Error, + ) -> EndExecutionReason { + // We couldn't find the specified command. This is a non-fatal error. We want to set the exit + // status to 127, which is the standard number used by other shells like bash and zsh. + + if err.kind() != ErrorKind::NotFound { + // TODO: We currently handle all errors here the same, + // but this mainly applies to EACCES. We could also feasibly get: + // ELOOP + // ENAMETOOLONG + if err.raw_os_error() == Some(ENOTDIR) { + // If the original command did not include a "/", assume we found it via $PATH. + let src = self.node_source(&statement.command); + if !src.contains('/') { + return report_error!( + self, + ctx, + STATUS_NOT_EXECUTABLE.unwrap(), + &statement.command, + concat!( + "Unknown command. A component of '%ls' is not a ", + "directory. Check your $PATH." + ), + cmd + ); + } else { + return report_error!( + self, + ctx, + STATUS_NOT_EXECUTABLE.unwrap(), + &statement.command, + "Unknown command. A component of '%ls' is not a directory.", + cmd + ); + } + } + + return report_error!( + self, + ctx, + STATUS_NOT_EXECUTABLE.unwrap(), + &statement.command, + "Unknown command. '%ls' exists but is not an executable file.", + cmd + ); + } + + // Handle unrecognized commands with standard command not found handler that can make better + // error messages. + let mut event_args = vec![]; + { + let args = Self::get_argument_nodes_no_redirs(&statement.args_or_redirs); + let arg_result = + self.expand_arguments_from_nodes(ctx, &args, &mut event_args, Globspec::failglob); + if arg_result != EndExecutionReason::ok { + return arg_result; + } + + event_args.insert(0, cmd.to_owned()); + } + + let mut error = WString::new(); + + // Redirect to stderr + let mut io = IoChain::new(); + let mut list = RedirectionSpecList::new(); + list.push(RedirectionSpec::new( + STDOUT_FILENO, + RedirectionMode::fd, + L!("2").to_owned(), + )); + io.append_from_specs(&list, L!("")); + + if function::exists(L!("fish_command_not_found"), ctx.parser()) { + let mut buffer = L!("fish_command_not_found").to_owned(); + for arg in &event_args { + buffer.push(' '); + buffer.push_utfstr(&escape(arg)); + } + let parser = ctx.parser(); + let prev_statuses = parser.get_last_statuses(); + + let event = Event::generic(L!("fish_command_not_found").to_owned()); + let b = parser.push_block(Block::event_block(event)); + parser.eval(&buffer, &io); + parser.pop_block(b); + parser.set_last_statuses(prev_statuses); + } else { + // If we have no handler, just print it as a normal error. + error = wgettext!("Unknown command:").to_owned(); + if !event_args.is_empty() { + error.push(' '); + error.push_utfstr(&escape(&event_args[0])); + } + } + + if cmd.as_char_slice().first() == Some(&'{' /*}*/) { + error.push_utfstr(&wgettext!(ERROR_NO_BRACE_GROUPING)); + } + + // Here we want to report an error (so it shows a backtrace). + // If the handler printed text, that's already shown, so error will be empty. + report_error_formatted!( + self, + ctx, + STATUS_CMD_UNKNOWN.unwrap(), + &statement.command, + error + ) + } + + // Utilities + fn node_source(&self, node: &dyn ast::Node) -> WString { + // todo!("maybe don't copy") + node.source(&self.pstree().src).to_owned() + } + + fn infinite_recursive_statement_in_job_list<'b>( + &self, + ctx: &OperationContext<'_>, + jobs: &'b ast::JobList, + out_func_name: &mut WString, + ) -> Option<&'b ast::DecoratedStatement> { + // This is a bit fragile. It is a test to see if we are inside of function call, but + // not inside a block in that function call. If, in the future, the rules for what + // block scopes are pushed on function invocation changes, then this check will break. + let parser = ctx.parser(); + let parent = { + match (parser.block_at_index(0), parser.block_at_index(1)) { + (Some(current), Some(parent)) + if current.typ() == BlockType::top && parent.is_function_call() => + { + parent + } + _ => return None, // Not within function call. + } + }; + + // Get the function name of the immediate block. + let forbidden_function_name = &parent.function_name; + + // Get the first job in the job list. + let jc = &jobs.get(0)?; + let job = &jc.job; + + // Helper to return if a statement is infinitely recursive in this function. + let statement_recurses = |stat: &'b ast::Statement| -> Option<&'b ast::DecoratedStatement> { + // Ignore non-decorated statements like `if`, etc. + let StatementVariant::DecoratedStatement(dc) = &*stat.contents else { + return None; + }; + + // Ignore statements with decorations like 'builtin' or 'command', since those + // are not infinite recursion. In particular that is what enables 'wrapper functions'. + if dc.decoration() != StatementDecoration::none { + return None; + } + + // Check the command. + let mut cmd = self.node_source(&dc.command); + let forbidden = !cmd.is_empty() + && expand_one( + &mut cmd, + ExpandFlags::SKIP_CMDSUBST | ExpandFlags::SKIP_VARIABLES, + ctx, + None, + ) + && &cmd == forbidden_function_name; + if forbidden { + Some(dc) + } else { + None + } + }; + + // Check main statement. + let infinite_recursive_statement = statement_recurses(&jc.job.statement) + // Check piped remainder. + .or_else(|| { + for c in &job.continuation { + let s = statement_recurses(&c.statement); + if s.is_some() { + return s; + } + } + None + }); + + if infinite_recursive_statement.is_some() { + *out_func_name = forbidden_function_name.to_owned(); + } + + // may be none + infinite_recursive_statement + } + + // Expand a command which may contain variables, producing an expand command and possibly + // arguments. Prints an error message on error. + fn expand_command( + &self, + ctx: &OperationContext<'_>, + statement: &ast::DecoratedStatement, + out_cmd: &mut WString, + out_args: &mut Vec, + ) -> EndExecutionReason { + // Here we're expanding a command, for example $HOME/bin/stuff or $randomthing. The first + // completion becomes the command itself, everything after becomes arguments. Command + // substitutions are not supported. + let mut errors = ParseErrorList::new(); + + // Get the unexpanded command string. We expect to always get it here. + // todo!("remove clone") + let unexp_cmd = self.node_source(&statement.command); + let pos_of_command_token = statement.command.range().unwrap().start(); + + // Expand the string to produce completions, and report errors. + let expand_err = expand_to_command_and_args( + &unexp_cmd, + ctx, + out_cmd, + Some(out_args), + Some(&mut errors), + false, + ); + if expand_err == ExpandResultCode::error { + // Issue #5812 - the expansions were done on the command token, + // excluding prefixes such as " " or "if ". + // This means that the error positions are relative to the beginning + // of the token; we need to make them relative to the original source. + parse_error_offset_source_start(&mut errors, pos_of_command_token); + return self.report_errors(ctx, STATUS_ILLEGAL_CMD.unwrap(), &errors); + } else if expand_err == ExpandResultCode::wildcard_no_match { + return report_error!( + self, + ctx, + STATUS_UNMATCHED_WILDCARD.unwrap(), + statement, + WILDCARD_ERR_MSG, + &self.node_source(statement) + ); + } + assert!(expand_err == ExpandResultCode::ok); + + // Complain if the resulting expansion was empty, or expanded to an empty string. + // For no-exec it's okay, as we can't really perform the expansion. + if out_cmd.is_empty() && !no_exec() { + return report_error!( + self, + ctx, + STATUS_ILLEGAL_CMD.unwrap(), + &statement.command, + "The expanded command was empty." + ); + } + EndExecutionReason::ok + } + + /// Indicates whether a job is a simple block (one block, no redirections). + fn job_is_simple_block(&self, job: &ast::JobPipeline) -> bool { + // Must be no pipes. + if !job.continuation.is_empty() { + return false; + } + + // Helper to check if an argument_or_redirection_list_t has no redirections. + let no_redirs = + |list: &ast::ArgumentOrRedirectionList| !list.iter().any(|val| val.is_redirection()); + + // Check if we're a block statement with redirections. We do it this obnoxious way to preserve + // type safety (in case we add more specific statement types). + match &*job.statement.contents { + StatementVariant::BlockStatement(stmt) => no_redirs(&stmt.args_or_redirs), + StatementVariant::SwitchStatement(stmt) => no_redirs(&stmt.args_or_redirs), + StatementVariant::IfStatement(stmt) => no_redirs(&stmt.args_or_redirs), + StatementVariant::NotStatement(_) | StatementVariant::DecoratedStatement(_) => { + // not block statements + false + } + StatementVariant::None => panic!(), + } + } + + fn process_type_for_command( + &self, + ctx: &OperationContext<'_>, + statement: &ast::DecoratedStatement, + cmd: &wstr, + ) -> ProcessType { + // Determine the process type, which depends on the statement decoration (command, builtin, + // etc). + match statement.decoration() { + StatementDecoration::exec => ProcessType::exec, + StatementDecoration::command => ProcessType::external, + StatementDecoration::builtin => ProcessType::builtin, + StatementDecoration::none => { + if function::exists(cmd, ctx.parser()) { + ProcessType::function + } else if builtin_exists(cmd) { + ProcessType::builtin + } else { + ProcessType::external + } + } + _ => unreachable!(), + } + } + + fn apply_variable_assignments( + &self, + ctx: &OperationContext<'_>, + mut proc: Option<&mut Process>, + variable_assignment_list: &ast::VariableAssignmentList, + block: &mut Option, + ) -> EndExecutionReason { + if variable_assignment_list.is_empty() { + return EndExecutionReason::ok; + } + *block = Some(ctx.parser().push_block(Block::variable_assignment_block())); + for variable_assignment in variable_assignment_list { + let source = self.node_source(&**variable_assignment); + let equals_pos = variable_assignment_equals_pos(&source).unwrap(); + let variable_name = &source[..equals_pos]; + let expression = &source[equals_pos + 1..]; + let mut expression_expanded = vec![]; + let mut errors = ParseErrorList::new(); + // TODO this is mostly copied from expand_arguments_from_nodes, maybe extract to function + let expand_ret = expand_string( + expression.to_owned(), + &mut expression_expanded, + ExpandFlags::default(), + ctx, + Some(&mut errors), + ); + parse_error_offset_source_start( + &mut errors, + variable_assignment.range().unwrap().start() + equals_pos + 1, + ); + match expand_ret.result { + ExpandResultCode::error => { + return self.report_errors(ctx, expand_ret.status, &errors); + } + ExpandResultCode::cancel => { + return EndExecutionReason::cancelled; + } + ExpandResultCode::wildcard_no_match // nullglob (equivalent to set) + | ExpandResultCode::ok => {} + _ => unreachable!(), + } + let vals: Vec<_> = expression_expanded + .into_iter() + .map(|comp| comp.completion) + .collect(); + if let Some(proc) = &mut proc { + proc.variable_assignments.push(ConcreteAssignment::new( + variable_name.to_owned(), + vals.clone(), + )); + } + ctx.parser() + .set_var_and_fire(variable_name, EnvMode::LOCAL | EnvMode::EXPORT, vals); + } + EndExecutionReason::ok + } + + // These create process_t structures from statements. + fn populate_job_process( + &self, + ctx: &OperationContext<'_>, + job: &mut Job, + proc: &mut Process, + statement: &ast::Statement, + variable_assignments: &ast::VariableAssignmentList, + ) -> EndExecutionReason { + // Get the "specific statement" which is boolean / block / if / switch / decorated. + let specific_statement = &statement.contents; + + let mut block = None; + let result = + self.apply_variable_assignments(ctx, Some(proc), variable_assignments, &mut block); + let _scope = ScopeGuard::new((), |()| { + if let Some(block) = block { + ctx.parser().pop_block(block); + } + }); + if result != EndExecutionReason::ok { + return result; + } + + match &**specific_statement { + StatementVariant::NotStatement(not_statement) => { + self.populate_not_process(ctx, job, proc, not_statement) + } + StatementVariant::BlockStatement(_) + | StatementVariant::IfStatement(_) + | StatementVariant::SwitchStatement(_) => { + self.populate_block_process(ctx, proc, statement, specific_statement) + } + StatementVariant::DecoratedStatement(decorated_statement) => { + self.populate_plain_process(ctx, proc, decorated_statement) + } + StatementVariant::None => panic!(), + } + } + + fn populate_not_process( + &self, + ctx: &OperationContext<'_>, + job: &mut Job, + proc: &mut Process, + not_statement: &ast::NotStatement, + ) -> EndExecutionReason { + { + let mut flags = job.mut_flags(); + flags.negate = !flags.negate; + } + self.populate_job_process( + ctx, + job, + proc, + ¬_statement.contents, + ¬_statement.variables, + ) + } + + /// Creates a 'normal' (non-block) process. + fn populate_plain_process( + &self, + ctx: &OperationContext<'_>, + proc: &mut Process, + statement: &ast::DecoratedStatement, + ) -> EndExecutionReason { + // We may decide that a command should be an implicit cd. + let mut use_implicit_cd = false; + + // Get the command and any arguments due to expanding the command. + let mut cmd = WString::new(); + let mut args_from_cmd_expansion = vec![]; + let ret = self.expand_command(ctx, statement, &mut cmd, &mut args_from_cmd_expansion); + if ret != EndExecutionReason::ok { + return ret; + } + + // For no-exec, having an empty command is okay. We can't do anything more with it tho. + if no_exec() { + return EndExecutionReason::ok; + } + + assert!( + !cmd.is_empty(), + "expand_command should not produce an empty command", + ); + + // Determine the process type. + let mut process_type = self.process_type_for_command(ctx, statement, &cmd); + let external_cmd = if [ProcessType::external, ProcessType::exec].contains(&process_type) { + let parser = ctx.parser(); + // Determine the actual command. This may be an implicit cd. + let external_cmd = path_try_get_path(&cmd, parser.vars()); + let has_command = external_cmd.err.is_none(); + + let mut path = WString::new(); + if has_command { + path = external_cmd.path; + } else { + // If the specified command does not exist, and is undecorated, try using an implicit cd. + if statement.decoration() == StatementDecoration::none { + // Implicit cd requires an empty argument and redirection list. + if statement.args_or_redirs.is_empty() { + // Ok, no arguments or redirections; check to see if the command is a directory. + use_implicit_cd = path_as_implicit_cd( + &cmd, + &parser.vars().get_pwd_slash(), + parser.vars(), + ) + .is_some(); + } + } + + if !use_implicit_cd { + return self.handle_command_not_found( + ctx, + if external_cmd.path.is_empty() { + &cmd + } else { + &external_cmd.path + }, + statement, + std::io::Error::from_raw_os_error(external_cmd.err.unwrap().into()), + ); + } + }; + path + } else { + WString::new() + }; + + // Produce the full argument list and the set of IO redirections. + let mut cmd_args = vec![]; + let mut redirections = RedirectionSpecList::new(); + if use_implicit_cd { + // Implicit cd is simple. + cmd_args = vec![L!("cd").to_owned(), cmd]; + + // If we have defined a wrapper around cd, use it, otherwise use the cd builtin. + process_type = if function::exists(L!("cd"), ctx.parser()) { + ProcessType::function + } else { + ProcessType::builtin + }; + } else { + // Not implicit cd. + let glob_behavior = if [L!("set"), L!("count"), L!("path")].contains(&&cmd[..]) { + Globspec::nullglob + } else { + Globspec::failglob + }; + // Form the list of arguments. The command is the first argument, followed by any arguments + // from expanding the command, followed by the argument nodes themselves. E.g. if the + // command is '$gco foo' and $gco is git checkout. + cmd_args.push(cmd); + cmd_args.extend_from_slice(&args_from_cmd_expansion); + let arg_nodes = Self::get_argument_nodes_no_redirs(&statement.args_or_redirs); + let arg_result = + self.expand_arguments_from_nodes(ctx, &arg_nodes, &mut cmd_args, glob_behavior); + if arg_result != EndExecutionReason::ok { + return arg_result; + } + + // The set of IO redirections that we construct for the process. + let reason = + self.determine_redirections(ctx, &statement.args_or_redirs, &mut redirections); + if reason != EndExecutionReason::ok { + return reason; + } + } + + // Populate the process. + proc.typ = process_type; + proc.set_argv(cmd_args); + proc.set_redirection_specs(redirections); + proc.actual_cmd = external_cmd; + EndExecutionReason::ok + } + + fn populate_block_process( + &self, + ctx: &OperationContext<'_>, + proc: &mut Process, + statement: &ast::Statement, + specific_statement: &ast::StatementVariant, + ) -> EndExecutionReason { + // We handle block statements by creating process_type_t::block_node, that will bounce back to + // us when it's time to execute them. + // Get the argument or redirections list. + // TODO: args_or_redirs should be available without resolving the statement type. + let args_or_redirs = match specific_statement { + StatementVariant::BlockStatement(block_statement) => &block_statement.args_or_redirs, + StatementVariant::IfStatement(if_statement) => &if_statement.args_or_redirs, + StatementVariant::SwitchStatement(switch_statement) => &switch_statement.args_or_redirs, + _ => panic!("Unexpected block node type"), + }; + + let mut redirections = RedirectionSpecList::new(); + let reason = self.determine_redirections(ctx, args_or_redirs, &mut redirections); + if reason == EndExecutionReason::ok { + proc.typ = ProcessType::block_node; + proc.block_node_source = Some(self.pstree().clone()); + proc.internal_block_node = Some(statement.into()); + proc.set_redirection_specs(redirections); + } + reason + } + + // These encapsulate the actual logic of various (block) statements. + fn run_block_statement( + &self, + ctx: &OperationContext<'_>, + statement: &'a ast::BlockStatement, + associated_block: Option, + ) -> EndExecutionReason { + let bh = &statement.header; + let contents = &statement.jobs; + match &**bh { + BlockStatementHeaderVariant::ForHeader(fh) => self.run_for_statement(ctx, fh, contents), + BlockStatementHeaderVariant::WhileHeader(wh) => { + self.run_while_statement(ctx, wh, contents, associated_block) + } + BlockStatementHeaderVariant::FunctionHeader(fh) => { + self.run_function_statement(ctx, statement, fh) + } + BlockStatementHeaderVariant::BeginHeader(_bh) => { + self.run_begin_statement(ctx, contents) + } + BlockStatementHeaderVariant::None => panic!(), + } + } + + fn run_for_statement( + &self, + ctx: &OperationContext<'_>, + header: &'a ast::ForHeader, + block_contents: &'a ast::JobList, + ) -> EndExecutionReason { + // Get the variable name: `for var_name in ...`. We expand the variable name. It better result + // in just one. + let mut for_var_name = self.node_source(&header.var_name); + if !expand_one(&mut for_var_name, ExpandFlags::default(), ctx, None) { + return report_error!( + self, + ctx, + STATUS_EXPAND_ERROR.unwrap(), + &header.var_name, + FAILED_EXPANSION_VARIABLE_NAME_ERR_MSG, + for_var_name + ); + } + + if !valid_var_name(&for_var_name) { + return report_error!( + self, + ctx, + STATUS_INVALID_ARGS.unwrap(), + header.var_name, + BUILTIN_ERR_VARNAME, + "for", + for_var_name + ); + } + + // Get the contents to iterate over. + let mut arguments = vec![]; + let arg_nodes = Self::get_argument_nodes(&header.args); + let ret = + self.expand_arguments_from_nodes(ctx, &arg_nodes, &mut arguments, Globspec::nullglob); + if ret != EndExecutionReason::ok { + return ret; + } + let var = ctx.parser().vars().get(&for_var_name); + if EnvVar::flags_for(&for_var_name).contains(EnvVarFlags::READ_ONLY) { + return report_error!( + self, + ctx, + STATUS_INVALID_ARGS.unwrap(), + header.var_name, + "%ls: %ls: cannot overwrite read-only variable", + "for", + for_var_name + ); + } + + let retval = ctx.parser().vars().set( + &for_var_name, + EnvMode::LOCAL | EnvMode::USER, + var.map_or(vec![], |var| var.as_list().to_owned()), + ); + assert!(retval == EnvStackSetResult::ENV_OK); + + trace_if_enabled_with_args(ctx.parser(), L!("for"), &arguments); + let fb = ctx.parser().push_block(Block::for_block()); + + // We fire the same event over and over again, just construct it once. + let evt = Event::variable_set(for_var_name.clone()); + + // Now drive the for loop. + let mut ret = EndExecutionReason::ok; + for val in arguments { + if let Some(reason) = self.check_end_execution(ctx) { + ret = reason; + break; + } + + let retval = ctx + .parser() + .vars() + .set(&for_var_name, EnvMode::USER, vec![val]); + assert!( + retval == EnvStackSetResult::ENV_OK, + "for loop variable should have been successfully set" + ); + event::fire(ctx.parser(), evt.clone()); + + ctx.parser().libdata_mut().pods.loop_status = LoopStatus::normals; + self.run_job_list(ctx, block_contents, Some(fb)); + + if self.check_end_execution(ctx) == Some(EndExecutionReason::control_flow) { + // Handle break or continue. + let do_break = ctx.parser().libdata().pods.loop_status == LoopStatus::breaks; + ctx.parser().libdata_mut().pods.loop_status = LoopStatus::normals; + if do_break { + break; + } + } + } + + ctx.parser().pop_block(fb); + trace_if_enabled(ctx.parser(), L!("end for")); + ret + } + + fn run_if_statement( + &self, + ctx: &OperationContext<'_>, + statement: &'a ast::IfStatement, + associated_block: Option, + ) -> EndExecutionReason { + let mut result = EndExecutionReason::ok; + + // We have a sequence of if clauses, with a final else, resulting in a single job list that we + // execute. + let mut job_list_to_execute = None; + let mut if_clause = &statement.if_clause; + + // Index of the *next* elseif_clause to test. + let elseif_clauses = &statement.elseif_clauses; + let mut next_elseif_idx = 0; + + // We start with the 'if'. + trace_if_enabled(ctx.parser(), L!("if")); + + loop { + if let Some(ret) = self.check_end_execution(ctx) { + result = ret; + break; + } + + // An if condition has a job and a "tail" of andor jobs, e.g. "foo ; and bar; or baz". + // Check the condition and the tail. We treat end_execution_reason_t::error here as failure, + // in accordance with historic behavior. + let mut cond_ret = + self.run_job_conjunction(ctx, &if_clause.condition, associated_block); + if cond_ret == EndExecutionReason::ok { + cond_ret = self.run_andor_job_list(ctx, &if_clause.andor_tail, associated_block); + } + let take_branch = cond_ret == EndExecutionReason::ok + && ctx.parser().get_last_status() == EXIT_SUCCESS; + + if take_branch { + // Condition succeeded. + job_list_to_execute = Some(&if_clause.body); + break; + } + + // See if we have an elseif. + next_elseif_idx += 1; + if let Some(elseif_clause) = elseif_clauses.get(next_elseif_idx - 1) { + trace_if_enabled(ctx.parser(), L!("else if")); + if_clause = &elseif_clause.if_clause; + } else { + break; + } + } + + if job_list_to_execute.is_none() { + // our ifs and elseifs failed. + // Check our else body. + if let Some(else_clause) = statement.else_clause.as_ref() { + trace_if_enabled(ctx.parser(), L!("else")); + job_list_to_execute = Some(&else_clause.body); + } + } + + match job_list_to_execute { + None => { + // 'if' condition failed, no else clause, return 0, we're done. + // No job list means no successful conditions, so return 0 (issue #1443). + ctx.parser() + .set_last_statuses(Statuses::just(STATUS_CMD_OK.unwrap())); + } + Some(job_list_to_execute) => { + // Execute the job list we got. + let ib = ctx.parser().push_block(Block::if_block()); + self.run_job_list(ctx, job_list_to_execute, Some(ib)); + if let Some(ret) = self.check_end_execution(ctx) { + result = ret; + } + ctx.parser().pop_block(ib); + } + } + trace_if_enabled(ctx.parser(), L!("end if")); + + // It's possible there's a last-minute cancellation (issue #1297). + if let Some(ret) = self.check_end_execution(ctx) { + result = ret; + } + + // Otherwise, take the exit status of the job list. Reversal of issue #1061. + result + } + + fn run_switch_statement( + &self, + ctx: &OperationContext<'_>, + statement: &'a ast::SwitchStatement, + ) -> EndExecutionReason { + // Get the switch variable. + let switch_value = self.node_source(&statement.argument); + + // Expand it. We need to offset any errors by the position of the string. + let mut switch_values_expanded = vec![]; + let mut errors = ParseErrorList::new(); + let expand_ret = expand_string( + switch_value, + &mut switch_values_expanded, + ExpandFlags::default(), + ctx, + Some(&mut errors), + ); + parse_error_offset_source_start(&mut errors, statement.argument.range().unwrap().start()); + + match expand_ret.result { + ExpandResultCode::error => { + return self.report_errors(ctx, expand_ret.status, &errors); + } + ExpandResultCode::cancel => { + return EndExecutionReason::cancelled; + } + ExpandResultCode::wildcard_no_match => { + return report_error!( + self, + ctx, + STATUS_UNMATCHED_WILDCARD.unwrap(), + &statement.argument, + WILDCARD_ERR_MSG, + &self.node_source(&statement.argument) + ); + } + ExpandResultCode::ok => { + if switch_values_expanded.len() > 1 { + return report_error!( + self, + ctx, + STATUS_INVALID_ARGS.unwrap(), + &statement.argument, + "switch: Expected at most one argument, got %lu\n", + switch_values_expanded.len() + ); + } + } + _ => unreachable!(), + } + + // If we expanded to nothing, match the empty string. + assert!( + switch_values_expanded.len() <= 1, + "Should have at most one expansion" + ); + let switch_value_expanded = if switch_values_expanded.is_empty() { + WString::new() + } else { + switch_values_expanded.remove(0).completion + }; + + let mut result = EndExecutionReason::ok; + + trace_if_enabled_with_args(ctx.parser(), L!("switch"), &[&switch_value_expanded]); + let sb = ctx.parser().push_block(Block::switch_block()); + + // Expand case statements. + let mut matching_case_item = None; + for case_item in &statement.cases { + if let Some(ret) = self.check_end_execution(ctx) { + result = ret; + break; + } + + // Expand arguments. A case item list may have a wildcard that fails to expand to + // anything. We also report case errors, but don't stop execution; i.e. a case item that + // contains an unexpandable process will report and then fail to match. + let arg_nodes = Self::get_argument_nodes(&case_item.arguments); + let mut case_args = vec![]; + let case_result = self.expand_arguments_from_nodes( + ctx, + &arg_nodes, + &mut case_args, + Globspec::failglob, + ); + if case_result == EndExecutionReason::ok { + for arg in case_args { + // Unescape wildcards so they can be expanded again. + let unescaped_arg = parse_util_unescape_wildcards(&arg); + if wildcard_match(&switch_value_expanded, &unescaped_arg, false) { + // If this matched, we're done. + matching_case_item = Some(case_item); + break; + } + } + } + if matching_case_item.is_some() { + break; + } + } + + if let Some(case_item) = matching_case_item { + // Success, evaluate the job list. + assert!(result == EndExecutionReason::ok, "Expected success"); + result = self.run_job_list(ctx, &case_item.body, Some(sb)); + } + + ctx.parser().pop_block(sb); + trace_if_enabled(ctx.parser(), L!("end switch")); + result + } + + fn run_while_statement( + &self, + ctx: &OperationContext<'_>, + header: &'a ast::WhileHeader, + contents: &'a ast::JobList, + associated_block: Option, + ) -> EndExecutionReason { + let mut ret = EndExecutionReason::ok; + + // "The exit status of the while loop shall be the exit status of the last compound-list-2 + // executed, or zero if none was executed." + // Here are more detailed requirements: + // - If we execute the loop body zero times, or the loop body is empty, the status is success. + // - An empty loop body is treated as true, both in the loop condition and after loop exit. + // - The exit status of the last command is visible in the loop condition. (i.e. do not set the + // exit status to true BEFORE executing the loop condition). + // We achieve this by restoring the status if the loop condition fails, plus a special + // affordance for the first condition. + let mut first_cond_check = true; + + trace_if_enabled(ctx.parser(), L!("while")); + + // Run while the condition is true. + loop { + // Save off the exit status if it came from the loop body. We'll restore it if the condition + // is false. + let cond_saved_status = if first_cond_check { + Statuses::just(EXIT_SUCCESS) + } else { + ctx.parser().get_last_statuses() + }; + first_cond_check = false; + + // Check the condition. + let mut cond_ret = self.run_job_conjunction(ctx, &header.condition, associated_block); + if cond_ret == EndExecutionReason::ok { + cond_ret = self.run_andor_job_list(ctx, &header.andor_tail, associated_block); + } + + // If the loop condition failed to execute, then exit the loop without modifying the exit + // status. If the loop condition executed with a failure status, restore the status and then + // exit the loop. + if cond_ret != EndExecutionReason::ok { + break; + } else if ctx.parser().get_last_status() != EXIT_SUCCESS { + ctx.parser().set_last_statuses(cond_saved_status); + break; + } + + // Check cancellation. + if let Some(reason) = self.check_end_execution(ctx) { + ret = reason; + break; + } + + // Push a while block and then check its cancellation reason. + ctx.parser().libdata_mut().pods.loop_status = LoopStatus::normals; + + let wb = ctx.parser().push_block(Block::while_block()); + self.run_job_list(ctx, contents, Some(wb)); + let cancel_reason = self.check_end_execution(ctx); + ctx.parser().pop_block(wb); + + if cancel_reason == Some(EndExecutionReason::control_flow) { + // Handle break or continue. + let do_break = ctx.parser().libdata().pods.loop_status == LoopStatus::breaks; + ctx.parser().libdata_mut().pods.loop_status = LoopStatus::normals; + if do_break { + break; + } else { + continue; + } + } + + // no_exec means that fish was invoked with -n or --no-execute. If set, we allow the loop to + // not-execute once so its contents can be checked, and then break. + if no_exec() { + break; + } + } + trace_if_enabled(ctx.parser(), L!("end while")); + ret + } + + // Define a function. + fn run_function_statement( + &self, + ctx: &OperationContext<'_>, + statement: &ast::BlockStatement, + header: &ast::FunctionHeader, + ) -> EndExecutionReason { + // Get arguments. + let mut arguments = vec![]; + let mut arg_nodes = Self::get_argument_nodes(&header.args); + arg_nodes.insert(0, &header.first_arg); + let result = + self.expand_arguments_from_nodes(ctx, &arg_nodes, &mut arguments, Globspec::failglob); + + if result != EndExecutionReason::ok { + return result; + } + + trace_if_enabled_with_args(ctx.parser(), L!("function"), &arguments); + let mut outs = OutputStream::Null; + let mut errs = OutputStream::String(StringOutputStream::new()); + let mut streams = IoStreams::new(&mut outs, &mut errs); + let mut shim_arguments: Vec<&wstr> = arguments + .iter() + .map(|s| truncate_at_nul(s.as_ref())) + .collect(); + let err_code = builtins::function::function( + ctx.parser(), + &mut streams, + &mut shim_arguments, + NodeRef::new(self.pstree(), statement as *const ast::BlockStatement), + ); + let err_code = err_code.unwrap(); + ctx.parser().libdata_mut().pods.status_count += 1; + ctx.parser().set_last_statuses(Statuses::just(err_code)); + + let errtext = errs.contents(); + if !errtext.is_empty() { + report_error!(self, ctx, err_code, header, "%ls", errtext); + } + result + } + + fn run_begin_statement( + &self, + ctx: &OperationContext<'_>, + contents: &'a ast::JobList, + ) -> EndExecutionReason { + // Basic begin/end block. Push a scope block, run jobs, pop it + trace_if_enabled(ctx.parser(), L!("begin")); + let sb = ctx + .parser() + .push_block(Block::scope_block(BlockType::begin)); + let ret = self.run_job_list(ctx, contents, Some(sb)); + ctx.parser().pop_block(sb); + trace_if_enabled(ctx.parser(), L!("end begin")); + ret + } + + fn get_argument_nodes(args: &ast::ArgumentList) -> AstArgsList<'_> { + let mut result = AstArgsList::new(); + for arg in args { + result.push(&**arg); + } + result + } + + fn get_argument_nodes_no_redirs(args: &ast::ArgumentOrRedirectionList) -> AstArgsList<'_> { + let mut result = AstArgsList::new(); + for arg in args { + if arg.is_argument() { + result.push(arg.argument()); + } + } + result + } + + fn expand_arguments_from_nodes( + &self, + ctx: &OperationContext<'_>, + argument_nodes: &AstArgsList<'_>, + out_arguments: &mut Vec, + glob_behavior: Globspec, + ) -> EndExecutionReason { + // Get all argument nodes underneath the statement. We guess we'll have that many arguments (but + // may have more or fewer, if there are wildcards involved). + out_arguments.reserve(argument_nodes.len()); + for arg_node in argument_nodes { + // Expect all arguments to have source. + assert!(arg_node.has_source(), "Argument should have source"); + + // Expand this string. + let mut errors = ParseErrorList::new(); + let mut arg_expanded = CompletionList::new(); + let expand_ret = expand_string( + self.node_source(*arg_node), + &mut arg_expanded, + ExpandFlags::default(), + ctx, + Some(&mut errors), + ); + parse_error_offset_source_start(&mut errors, arg_node.range().unwrap().start()); + match expand_ret.result { + ExpandResultCode::error => { + return self.report_errors(ctx, expand_ret.status, &errors); + } + ExpandResultCode::cancel => { + return EndExecutionReason::cancelled; + } + ExpandResultCode::wildcard_no_match => { + if glob_behavior == Globspec::failglob { + // For no_exec, ignore the error - this might work at runtime. + if no_exec() { + return EndExecutionReason::ok; + } + // Report the unmatched wildcard error and stop processing. + return report_error!( + self, + ctx, + STATUS_UNMATCHED_WILDCARD.unwrap(), + arg_node, + WILDCARD_ERR_MSG, + &self.node_source(*arg_node) + ); + } + } + ExpandResultCode::ok => {} + _ => unreachable!(), + } + + // Now copy over any expanded arguments. Use std::move() to avoid extra allocations; this + // is called very frequently. + if let Some(additional) = + (out_arguments.len() + arg_expanded.len()).checked_sub(out_arguments.capacity()) + { + out_arguments.reserve(additional); + } + for new_arg in arg_expanded { + out_arguments.push(new_arg.completion); + } + } + + // We may have received a cancellation during this expansion. + if let Some(ret) = self.check_end_execution(ctx) { + return ret; + } + + EndExecutionReason::ok + } + + // Determines the list of redirections for a node. + fn determine_redirections( + &self, + ctx: &OperationContext<'_>, + list: &ast::ArgumentOrRedirectionList, + out_redirections: &mut RedirectionSpecList, + ) -> EndExecutionReason { + // Get all redirection nodes underneath the statement. + for arg_or_redir in list { + if !arg_or_redir.is_redirection() { + continue; + } + let redir_node = arg_or_redir.redirection(); + + let oper = match PipeOrRedir::try_from(&self.node_source(&redir_node.oper)[..]) { + Ok(oper) if oper.is_valid() => oper, + _ => { + // TODO: figure out if this can ever happen. If so, improve this error message. + return report_error!( + self, + ctx, + STATUS_INVALID_ARGS.unwrap(), + redir_node, + "Invalid redirection: %ls", + &self.node_source(redir_node) + ); + } + }; + + // PCA: I can't justify this skip_variables flag. It was like this when I got here. + let mut target = self.node_source(&redir_node.target); + let target_expanded = expand_one( + &mut target, + if no_exec() { + ExpandFlags::SKIP_VARIABLES + } else { + ExpandFlags::default() + }, + ctx, + None, + ); + if !target_expanded || target.is_empty() { + // TODO: Improve this error message. + return report_error!( + self, + ctx, + STATUS_INVALID_ARGS.unwrap(), + redir_node, + "Invalid redirection target: %ls", + target + ); + } + + // Make a redirection spec from the redirect token. + assert!(oper.is_valid(), "expected to have a valid redirection"); + let spec = RedirectionSpec::new(oper.fd, oper.mode, target); + + // Validate this spec. + if spec.mode == RedirectionMode::fd + && !spec.is_close() + && spec.get_target_as_fd().is_none() + { + return report_error!( + self, + ctx, + STATUS_INVALID_ARGS.unwrap(), + redir_node, + "Requested redirection to '%ls', which is not a valid file descriptor", + &spec.target + ); + } + out_redirections.push(spec); + + if oper.stderr_merge { + // This was a redirect like &> which also modifies stderr. + // Also redirect stderr to stdout. + out_redirections.push(get_stderr_merge()); + } + } + EndExecutionReason::ok + } + + fn run_1_job( + &self, + ctx: &OperationContext<'_>, + job_node: &'a ast::JobPipeline, + associated_block: Option, + ) -> EndExecutionReason { + if let Some(ret) = self.check_end_execution(ctx) { + return ret; + } + + // We definitely do not want to execute anything if we're told we're --no-execute! + if no_exec() { + return EndExecutionReason::ok; + } + + // Increment the eval_level for the duration of this command. + let _saved_eval_level = scoped_push_replacer( + |new_value| ctx.parser().eval_level.swap(new_value, Ordering::Relaxed), + ctx.parser().eval_level.load(Ordering::Relaxed) + 1, + ); + + // Save the node index. + let _saved_node = scoped_push_replacer( + |new_value| std::mem::replace(&mut self.executing_job_node.borrow_mut(), new_value), + Some(ConstPointer::from(job_node)), + ); + + // Profiling support. + let profile_item_id = ctx.parser().create_profile_item(); + let start_time = if profile_item_id.is_some() { + ProfileItem::now() + } else { + 0 + }; + + // When we encounter a block construct (e.g. while loop) in the general case, we create a "block + // process" containing its node. This allows us to handle block-level redirections. + // However, if there are no redirections, then we can just jump into the block directly, which + // is significantly faster. + if self.job_is_simple_block(job_node) { + let do_time = job_node.time.is_some(); + let _timer = push_timer(do_time); + let mut block = None; + let mut result = + self.apply_variable_assignments(ctx, None, &job_node.variables, &mut block); + let _scope = ScopeGuard::new((), |()| { + if let Some(block) = block { + ctx.parser().pop_block(block); + } + }); + + let specific_statement = &job_node.statement.contents; + assert!(specific_statement_type_is_redirectable_block( + specific_statement + )); + if result == EndExecutionReason::ok { + result = match &**specific_statement { + StatementVariant::BlockStatement(block_statement) => { + self.run_block_statement(ctx, block_statement, associated_block) + } + StatementVariant::IfStatement(ifstmt) => { + self.run_if_statement(ctx, ifstmt, associated_block) + } + StatementVariant::SwitchStatement(switchstmt) => { + self.run_switch_statement(ctx, switchstmt) + } + // Other types should be impossible due to the + // specific_statement_type_is_redirectable_block check. + StatementVariant::NotStatement(_) + | StatementVariant::DecoratedStatement(_) + | StatementVariant::None => panic!(), + }; + } + + if let Some(profile_item_id) = profile_item_id { + let parser = ctx.parser(); + let mut profile_items = parser.profile_items_mut(); + let profile_item = &mut profile_items[profile_item_id]; + profile_item.duration = ProfileItem::now() - start_time; + profile_item.level = ctx.parser().eval_level.load(Ordering::Relaxed); + profile_item.cmd = + profiling_cmd_name_for_redirectable_block(specific_statement, &self.pstree()); + profile_item.skipped = false; + } + + return result; + } + + let mut props = JobProperties::default(); + props.initial_background = job_node.bg.is_some(); + { + let parser = ctx.parser(); + let ld = &parser.libdata().pods; + props.skip_notification = + ld.is_subshell || parser.is_block() || ld.is_event != 0 || !parser.is_interactive(); + props.from_event_handler = ld.is_event != 0; + props.wants_timing = job_node_wants_timing(job_node); + + // It's an error to have 'time' in a background job. + if props.wants_timing && props.initial_background { + return report_error!( + self, + &ctx, + STATUS_INVALID_ARGS.unwrap(), + job_node, + ERROR_TIME_BACKGROUND + ); + } + } + + let mut job = Job::new(props, self.node_source(job_node)); + + // We are about to populate a job. One possible argument to the job is a command substitution + // which may be interested in the job that's populating it, via '--on-job-exit caller'. Record + // the job ID here. + let _caller_id = scoped_push_replacer( + |new_value| { + std::mem::replace(&mut ctx.parser().libdata_mut().pods.caller_id, new_value) + }, + job.internal_job_id, + ); + + // Populate the job. This may fail for reasons like command_not_found. If this fails, an error + // will have been printed. + let pop_result = self.populate_job_from_job_node(ctx, &mut job, job_node, associated_block); + ScopeGuarding::commit(_caller_id); + + // Clean up the job on failure or cancellation. + if pop_result == EndExecutionReason::ok { + self.setup_group(ctx, &mut job); + assert!(job.group.is_some(), "Should have a group"); + } + + // Now that we're done mutating the Job, we can stick it in an Arc + let job = Rc::new(job); + + if pop_result == EndExecutionReason::ok { + // Give the job to the parser - it will clean it up. + { + let parser = ctx.parser(); + parser.job_add(job.clone()); + + // Actually execute the job. + let block_io = self.block_io.borrow().clone(); + if !exec_job(parser, &job, block_io) { + // No process in the job successfully launched. + // Ensure statuses are set (#7540). + if let Some(statuses) = job.get_statuses() { + parser.set_last_statuses(statuses); + parser.libdata_mut().pods.status_count += 1; + } + remove_job(parser, &job); + } + + // Update universal variables on external commands. + // We only incorporate external changes if we had an external proc, for hysterical raisins. + parser.sync_uvars_and_fire(job.has_external_proc() /* always */); + } + + // If the job got a SIGINT or SIGQUIT, then we're going to start unwinding. + let mut cancel_signal = self.cancel_signal.borrow_mut(); + if cancel_signal.is_none() { + *cancel_signal = job.group().get_cancel_signal(); + } + } + + if let Some(profile_item_id) = profile_item_id { + let parser = ctx.parser(); + let mut profile_items = parser.profile_items_mut(); + let profile_item = &mut profile_items[profile_item_id]; + profile_item.duration = ProfileItem::now() - start_time; + profile_item.level = ctx.parser().eval_level.load(Ordering::Relaxed); + profile_item.cmd = job.command().to_owned(); + profile_item.skipped = pop_result != EndExecutionReason::ok; + } + + job_reap(ctx.parser(), false); // clean up jobs + pop_result + } + + fn test_and_run_1_job_conjunction( + &self, + ctx: &OperationContext<'_>, + jc: &'a ast::JobConjunction, + associated_block: Option, + ) -> EndExecutionReason { + // Test this job conjunction if it has an 'and' or 'or' decorator. + // If it passes, then run it. + if let Some(reason) = self.check_end_execution(ctx) { + return reason; + } + // Maybe skip the job if it has a leading and/or. + let mut skip = false; + if let Some(deco) = &jc.decorator { + let last_status = ctx.parser().get_last_status(); + match deco.keyword() { + ParseKeyword::kw_and => { + // AND. Skip if the last job failed. + skip = last_status != 0; + } + ParseKeyword::kw_or => { + // OR. Skip if the last job succeeded. + skip = last_status == 0; + } + _ => unreachable!(), + } + } + // Skipping is treated as success. + if skip { + EndExecutionReason::ok + } else { + self.run_job_conjunction(ctx, jc, associated_block) + } + } + + fn run_job_conjunction( + &self, + ctx: &OperationContext<'_>, + job_expr: &'a ast::JobConjunction, + associated_block: Option, + ) -> EndExecutionReason { + if let Some(reason) = self.check_end_execution(ctx) { + return reason; + } + let mut result = self.run_1_job(ctx, &job_expr.job, associated_block); + for jc in &job_expr.continuations { + if result != EndExecutionReason::ok { + return result; + } + if let Some(reason) = self.check_end_execution(ctx) { + return reason; + } + // Check the conjunction type. + let last_status = ctx.parser().get_last_status(); + let skip = match jc.conjunction.token_type() { + ParseTokenType::andand => { + // AND. Skip if the last job failed. + last_status != 0 + } + ParseTokenType::oror => { + // OR. Skip if the last job succeeded. + last_status == 0 + } + _ => unreachable!(), + }; + if !skip { + result = self.run_1_job(ctx, &jc.job, associated_block); + } + } + result + } + + fn run_job_list( + &self, + ctx: &OperationContext<'_>, + job_list_node: &'a ast::JobList, + associated_block: Option, + ) -> EndExecutionReason { + let mut result = EndExecutionReason::ok; + for jc in job_list_node { + result = self.test_and_run_1_job_conjunction(ctx, jc, associated_block); + } + // Returns the result of the last job executed or skipped. + result + } + + fn run_andor_job_list( + &self, + ctx: &OperationContext<'_>, + job_list_node: &'a ast::AndorJobList, + associated_block: Option, + ) -> EndExecutionReason { + let mut result = EndExecutionReason::ok; + for aoj in job_list_node { + result = self.test_and_run_1_job_conjunction(ctx, &aoj.job, associated_block); + } + // Returns the result of the last job executed or skipped. + result + } + + fn populate_job_from_job_node( + &self, + ctx: &OperationContext<'_>, + j: &mut Job, + job_node: &ast::JobPipeline, + _associated_block: Option, + ) -> EndExecutionReason { + // We are going to construct process_t structures for every statement in the job. + // Create processes. Each one may fail. + let mut processes = ProcessList::new(); + processes.push(Box::new(Process::new())); + let mut result = self.populate_job_process( + ctx, + j, + &mut processes[0], + &job_node.statement, + &job_node.variables, + ); + + // Construct process_ts for job continuations (pipelines). + for jc in &job_node.continuation { + if result != EndExecutionReason::ok { + break; + } + // Handle the pipe, whose fd may not be the obvious stdout. + let parsed_pipe = PipeOrRedir::try_from(&self.node_source(&jc.pipe)[..]) + .expect("Failed to parse valid pipe"); + if !parsed_pipe.is_valid() { + result = report_error!( + self, + ctx, + STATUS_INVALID_ARGS.unwrap(), + &jc.pipe, + ILLEGAL_FD_ERR_MSG, + &self.node_source(&jc.pipe) + ); + break; + } + { + let proc = processes.last_mut().unwrap(); + proc.pipe_write_fd = parsed_pipe.fd; + if parsed_pipe.stderr_merge { + // This was a pipe like &| which redirects both stdout and stderr. + // Also redirect stderr to stdout. + let specs = proc.redirection_specs_mut(); + specs.push(get_stderr_merge()); + } + } + + // Store the new process (and maybe with an error). + processes.push(Box::new(Process::new())); + result = self.populate_job_process( + ctx, + j, + processes.last_mut().unwrap(), + &jc.statement, + &jc.variables, + ); + } + + // Inform our processes of who is first and last + processes.first_mut().unwrap().is_first_in_job = true; + processes.last_mut().unwrap().is_last_in_job = true; + + // Return what happened. + if result == EndExecutionReason::ok { + // Link up the processes. + assert!(!processes.is_empty()); + *j.processes_mut() = processes; + } + result + } + + // Assign a job group to the given job. + fn setup_group(&self, ctx: &OperationContext<'_>, j: &mut Job) { + // We can use the parent group if it's compatible and we're not backgrounded. + if ctx.job_group.as_ref().map_or(false, |job_group| { + job_group.has_job_id() || !j.wants_job_id() + }) && !j.is_initially_background() + { + j.group = ctx.job_group.clone(); + return; + } + + if j.processes()[0].is_internal() || !self.use_job_control(ctx) { + // This job either doesn't have a pgroup (e.g. a simple block), or lives in fish's pgroup. + j.group = Some(JobGroup::create(j.command().to_owned(), j.wants_job_id())); + } else { + // This is a "real job" that gets its own pgroup. + j.processes_mut()[0].leads_pgrp = true; + let wants_terminal = ctx.parser().libdata().pods.is_event == 0; + j.group = Some(JobGroup::create_with_job_control( + j.command().to_owned(), + wants_terminal, + )); + } + j.group().is_foreground.store(!j.is_initially_background()); + j.mut_flags().is_group_root = true; + } + + // \return whether we should apply job control to our processes. + fn use_job_control(&self, ctx: &OperationContext<'_>) -> bool { + if ctx.parser().is_command_substitution() { + return false; + } + match get_job_control_mode() { + JobControl::all => true, + JobControl::interactive => ctx.parser().is_interactive(), + JobControl::none => false, + } + } + + // Returns the line number of the current node. + fn line_offset_of_executing_node(&self) -> Option { + // If we're not executing anything, return nothing. + let node = self.executing_job_node.borrow(); + let node = node.as_ref()?; + + // If for some reason we're executing a node without source, return nothing. + let range = node.try_source_range()?; + + Some(self.line_offset_of_character_at_offset(range.start())) + } + + fn line_offset_of_character_at_offset(&self, offset: usize) -> usize { + // Count the number of newlines, leveraging our cache. + assert!(offset <= self.pstree().src.len()); + + // Easy hack to handle 0. + if offset == 0 { + return 0; + } + + // We want to return (one plus) the number of newlines at offsets less than the given offset. + let src = &self.pstree().src; + let mut cached_lineno = self.cached_lineno.borrow_mut(); + if offset > cached_lineno.offset { + // Add one for every newline we find in the range [cached_lineno.offset, offset). + let offset = std::cmp::min(offset, src.len()); + let i = src[cached_lineno.offset..offset] + .chars() + .filter(|c| *c == '\n') + .count(); + cached_lineno.count += i; + cached_lineno.offset = offset; + } else if offset < cached_lineno.offset { + // Subtract one for every newline we find in the range [offset, cached_range.start). + cached_lineno.count -= src[offset..cached_lineno.offset] + .chars() + .filter(|c| *c == '\n') + .count(); + cached_lineno.offset = offset; + } + cached_lineno.count + } +} + +#[derive(Eq, PartialEq)] +enum Globspec { + failglob, + nullglob, +} +type AstArgsList<'a> = Vec<&'a ast::Argument>; + +/// These are the specific statement types that support redirections. +fn type_is_redirectable_block(typ: ast::Type) -> bool { + [ + ast::Type::block_statement, + ast::Type::if_statement, + ast::Type::switch_statement, + ] + .contains(&typ) +} + +fn specific_statement_type_is_redirectable_block(node: &ast::StatementVariant) -> bool { + type_is_redirectable_block(node.typ()) +} + +/// Get the name of a redirectable block, for profiling purposes. +fn profiling_cmd_name_for_redirectable_block( + node: &ast::StatementVariant, + pstree: &ParsedSourceRef, +) -> WString { + assert!(specific_statement_type_is_redirectable_block(node)); + + let source_range = node.try_source_range().expect("No source range for block"); + + let src_end = match node { + StatementVariant::BlockStatement(block_statement) => { + let block_header = &block_statement.header; + match &**block_header { + BlockStatementHeaderVariant::ForHeader(for_header) => { + for_header.semi_nl.source_range().start() + } + BlockStatementHeaderVariant::WhileHeader(while_header) => { + while_header.condition.source_range().start() + } + BlockStatementHeaderVariant::FunctionHeader(function_header) => { + function_header.semi_nl.source_range().start() + } + BlockStatementHeaderVariant::BeginHeader(begin_header) => { + begin_header.kw_begin.source_range().start() + } + BlockStatementHeaderVariant::None => panic!("Unexpected block header type"), + } + } + StatementVariant::IfStatement(ifstmt) => { + ifstmt.if_clause.condition.job.source_range().end() + } + StatementVariant::SwitchStatement(switchstmt) => switchstmt.semi_nl.source_range().start(), + _ => { + panic!("Not a redirectable block_type"); + } + }; + + assert!(src_end >= source_range.start(), "Invalid source end"); + + // Get the source for the block, and cut it at the next statement terminator. + let mut result = pstree.src[source_range.start()..src_end].to_owned(); + result.push_utfstr(L!("...")); + result +} + +/// Get a redirection from stderr to stdout (i.e. 2>&1). +fn get_stderr_merge() -> RedirectionSpec { + let stdout_fileno_str = L!("1").to_owned(); + RedirectionSpec::new(STDERR_FILENO, RedirectionMode::fd, stdout_fileno_str) +} + +/// Decide if a job node should be 'time'd. +/// For historical reasons the 'not' and 'time' prefix are "inside out". That is, it's +/// 'not time cmd'. Note that a time appearing anywhere in the pipeline affects the whole job. +/// `sleep 1 | not time true` will time the whole job! +fn job_node_wants_timing(job_node: &ast::JobPipeline) -> bool { + // Does our job have the job-level time prefix? + if job_node.time.is_some() { + return true; + } + + // Helper to return true if a node is 'not time ...' or 'not not time...' or... + let is_timed_not_statement = |mut stat: &ast::Statement| loop { + match &*stat.contents { + StatementVariant::NotStatement(ns) => { + if ns.time.is_some() { + return true; + } + stat = &ns.contents; + } + _ => return false, + } + }; + + // Do we have a 'not time ...' anywhere in our pipeline? + if is_timed_not_statement(&job_node.statement) { + return true; + } + for jc in &job_node.continuation { + if is_timed_not_statement(&jc.statement) { + return true; + } + } + + false +} + +fn remove_job(parser: &Parser, job: &JobRef) -> bool { + let mut jobs = parser.jobs_mut(); + let num_jobs = jobs.len(); + for i in 0..num_jobs { + if Rc::ptr_eq(&jobs[i], job) { + jobs.remove(i); + return true; + } + } + false +} diff --git a/fish-rust/src/parse_tree.rs b/fish-rust/src/parse_tree.rs index 85bb7df62..cee22a4f0 100644 --- a/fish-rust/src/parse_tree.rs +++ b/fish-rust/src/parse_tree.rs @@ -132,6 +132,15 @@ pub struct NodeRef { node: *const NodeType, } +impl NodeRef { + pub fn new(parsed_source: ParsedSourceRef, node: *const NodeType) -> Self { + NodeRef { + parsed_source: Pin::new(parsed_source), + node, + } + } +} + impl Clone for NodeRef { fn clone(&self) -> Self { NodeRef { @@ -188,6 +197,11 @@ pub fn parse_source( pub struct ParsedSourceRefFFI(pub Option); +unsafe impl cxx::ExternType for ParsedSourceRefFFI { + type Id = cxx::type_id!("ParsedSourceRefFFI"); + type Kind = cxx::kind::Opaque; +} + #[cxx::bridge] mod parse_tree_ffi { extern "C++" { diff --git a/fish-rust/src/parse_util.rs b/fish-rust/src/parse_util.rs index d88a0e4a8..d7014a2bc 100644 --- a/fish-rust/src/parse_util.rs +++ b/fish-rust/src/parse_util.rs @@ -1,5 +1,6 @@ //! Various mostly unrelated utility functions related to parsing, loading and evaluating fish code. use crate::ast::{self, Ast, Keyword, Leaf, List, Node, NodeVisitor}; +use crate::builtins::shared::builtin_exists; use crate::common::{ escape_string, unescape_string, valid_var_name, valid_var_name_char, EscapeFlags, EscapeStringStyle, UnescapeFlags, UnescapeStringStyle, @@ -8,24 +9,23 @@ expand_one, expand_to_command_and_args, ExpandFlags, ExpandResultCode, BRACE_BEGIN, BRACE_END, BRACE_SEP, INTERNAL_SEPARATOR, VARIABLE_EXPAND, VARIABLE_EXPAND_EMPTY, VARIABLE_EXPAND_SINGLE, }; -use crate::ffi; use crate::ffi_tests::add_test; use crate::future_feature_flags::{feature_test, FeatureFlag}; use crate::operation_context::OperationContext; use crate::parse_constants::{ - parse_error_offset_source_start, ParseError, ParseErrorCode, ParseErrorList, ParseKeyword, - ParseTokenType, ParseTreeFlags, ParserTestErrorBits, PipelinePosition, StatementDecoration, - ERROR_BAD_VAR_CHAR1, ERROR_BRACKETED_VARIABLE1, ERROR_BRACKETED_VARIABLE_QUOTED1, - ERROR_NOT_ARGV_AT, ERROR_NOT_ARGV_COUNT, ERROR_NOT_ARGV_STAR, ERROR_NOT_PID, ERROR_NOT_STATUS, - ERROR_NO_VAR_NAME, INVALID_BREAK_ERR_MSG, INVALID_CONTINUE_ERR_MSG, - INVALID_PIPELINE_CMD_ERR_MSG, UNKNOWN_BUILTIN_ERR_MSG, + parse_error_offset_source_start, ParseError, ParseErrorCode, ParseErrorList, ParseErrorListFfi, + ParseKeyword, ParseTokenType, ParseTreeFlags, ParserTestErrorBits, PipelinePosition, + StatementDecoration, ERROR_BAD_VAR_CHAR1, ERROR_BRACKETED_VARIABLE1, + ERROR_BRACKETED_VARIABLE_QUOTED1, ERROR_NOT_ARGV_AT, ERROR_NOT_ARGV_COUNT, ERROR_NOT_ARGV_STAR, + ERROR_NOT_PID, ERROR_NOT_STATUS, ERROR_NO_VAR_NAME, INVALID_BREAK_ERR_MSG, + INVALID_CONTINUE_ERR_MSG, INVALID_PIPELINE_CMD_ERR_MSG, UNKNOWN_BUILTIN_ERR_MSG, }; use crate::tokenizer::{ comment_end, is_token_delimiter, quote_end, Tok, TokenType, Tokenizer, TOK_ACCEPT_UNFINISHED, TOK_SHOW_COMMENTS, }; use crate::wchar::prelude::*; -use crate::wchar_ffi::{WCharFromFFI, WCharToFFI}; +use crate::wchar_ffi::{AsWstr, WCharFromFFI}; use crate::wcstringutil::truncate; use crate::wildcard::{ANY_CHAR, ANY_STRING, ANY_STRING_RECURSIVE}; use cxx::CxxWString; @@ -90,7 +90,7 @@ pub fn parse_util_slice_length(input: &wstr) -> Option { pub fn parse_util_locate_cmdsubst_range<'a>( s: &'a wstr, inout_cursor_offset: &mut usize, - mut out_contents: Option<&'a wstr>, + mut out_contents: Option<&mut &'a wstr>, out_start: &mut usize, out_end: &mut usize, accept_incomplete: bool, @@ -98,7 +98,7 @@ pub fn parse_util_locate_cmdsubst_range<'a>( out_has_dollar: Option<&mut bool>, ) -> i32 { // Clear the return values. - out_contents.as_mut().map(|s| *s = L!("")); + out_contents.as_mut().map(|s| **s = L!("")); *out_start = 0; *out_end = s.len(); @@ -121,9 +121,11 @@ pub fn parse_util_locate_cmdsubst_range<'a>( return ret; } + // Assign the substring to the out_contents. + let interior_begin = *out_start + 1; out_contents .as_mut() - .map(|contents| *contents = &s[*out_start..*out_end]); + .map(|contents| **contents = &s[interior_begin..*out_end]); // Update the inout_cursor_offset. Note this may cause it to exceed str.size(), though // overflow is not likely. @@ -200,7 +202,6 @@ fn parse_util_locate_cmdsub( let mut last_dollar = None; let mut paran_begin = None; let mut paran_end = None; - fn process_opening_quote( input: &[char], inout_is_quoted: &mut Option<&mut bool>, @@ -269,7 +270,7 @@ fn process_opening_quote( paran_begin = Some(pos); out_has_dollar .as_mut() - .map(|has_dollar| **has_dollar = last_dollar == Some(pos - 1)); + .map(|has_dollar| **has_dollar = last_dollar == Some(pos.wrapping_sub(1))); } paran_count += 1; @@ -968,7 +969,7 @@ fn visit(&mut self, node: &'a dyn Node) { pub fn parse_util_detect_errors( buff_src: &wstr, mut out_errors: Option<&mut ParseErrorList>, - allow_incomplete: bool, + allow_incomplete: bool, /*=false*/ ) -> Result<(), ParserTestErrorBits> { // Whether there's an unclosed quote or subshell, and therefore unfinished. This is only set if // allow_incomplete is set. @@ -1054,7 +1055,7 @@ pub fn parse_util_detect_errors_in_ast( if let Some(jc) = node.as_job_continuation() { // Somewhat clumsy way of checking for a statement without source in a pipeline. // See if our pipe has source but our statement does not. - if jc.pipe.has_source() && jc.statement.try_source_range().is_some() { + if jc.pipe.has_source() && jc.statement.try_source_range().is_none() { has_unclosed_pipe = true; } } else if let Some(jcc) = node.as_job_conjunction_continuation() { @@ -1271,7 +1272,7 @@ pub fn parse_util_detect_errors_in_argument( let mut cursor = 0; let mut checked = 0; - let subst = L!(""); + let mut subst = L!(""); let mut do_loop = true; let mut is_quoted = false; @@ -1282,7 +1283,7 @@ pub fn parse_util_detect_errors_in_argument( match parse_util_locate_cmdsubst_range( arg_src, &mut cursor, - Some(subst), + Some(&mut subst), &mut paren_begin, &mut paren_end, false, @@ -1508,7 +1509,7 @@ fn detect_errors_in_decorated_statement( &OperationContext::empty(), &mut command, None, - &mut new_errors, + Some(&mut new_errors), true, /* skip wildcards */ ) == ExpandResultCode::error { @@ -1583,7 +1584,7 @@ fn detect_errors_in_decorated_statement( Some(pe) => Some(pe), None => None, }, - ) && !ffi::builtin_exists(&unexp_command.to_ffi()) + ) && !builtin_exists(unexp_command) { errored = append_syntax_error!( parse_errors, @@ -1982,11 +1983,55 @@ macro_rules! validate { #[cxx::bridge] mod parse_util_ffi { + extern "C++" { + include!("parse_constants.h"); + include!("parse_tree.h"); + include!("ast.h"); + type ParseErrorListFfi = crate::parse_constants::ParseErrorListFfi; + type DecoratedStatement = crate::ast::DecoratedStatement; + } extern "Rust" { fn parse_util_compute_indents_ffi(src: &CxxWString) -> Vec; + #[cxx_name = "detect_errors_in_decorated_statement"] + // Getting weird linker errors when using pointers. + fn detect_errors_in_decorated_statement_ffi( + buff_src: &CxxWString, + dst: usize, + out_errors: usize, + ) -> bool; } } +fn detect_errors_in_decorated_statement_ffi( + buff_src: &CxxWString, + dst: usize, + out_errors: usize, +) -> bool { + let dst = unsafe { &*(dst as *const ast::DecoratedStatement) }; + let out_errors = out_errors as *mut ParseErrorListFfi; + let mut out_errors = if out_errors.is_null() { + None + } else { + Some(unsafe { &mut (*out_errors).0 }) + }; + detect_errors_in_decorated_statement(buff_src.as_wstr(), dst, &mut out_errors) +} + fn parse_util_compute_indents_ffi(src: &CxxWString) -> Vec { parse_util_compute_indents(&src.from_ffi()) } + +fn parse_util_detect_errors_ffi( + buff_src: &CxxWString, + out_errors: *mut ParseErrorListFfi, + allow_incomplete: bool, +) -> u8 { + let out_errors = if out_errors.is_null() { + None + } else { + Some(unsafe { &mut (*out_errors).0 }) + }; + parse_util_detect_errors(buff_src.as_wstr(), out_errors, allow_incomplete) + .err() + .map_or(0, |error_bits| error_bits.bits()) +} diff --git a/fish-rust/src/parser.rs b/fish-rust/src/parser.rs new file mode 100644 index 000000000..78047dec0 --- /dev/null +++ b/fish-rust/src/parser.rs @@ -0,0 +1,1559 @@ +// The fish parser. Contains functions for parsing and evaluating code. + +use crate::ast::{Ast, List, Node}; +use crate::builtins::shared::STATUS_ILLEGAL_CMD; +use crate::common::{ + escape_string, scoped_push_replacer, CancelChecker, EscapeFlags, EscapeStringStyle, + FilenameRef, ScopeGuarding, PROFILING_ACTIVE, +}; +use crate::complete::CompletionList; +use crate::env::{ + EnvMode, EnvStack, EnvStackRef, EnvStackRefFFI, EnvStackSetResult, Environment, Statuses, +}; +use crate::event::{self, Event}; +use crate::expand::{ + expand_string, replace_home_directory_with_tilde, ExpandFlags, ExpandResultCode, +}; +use crate::fds::{open_cloexec, AutoCloseFd}; +use crate::ffi::{self, wcstring_list_ffi_t}; +use crate::flog::FLOGF; +use crate::function; +use crate::global_safety::{RelaxedAtomicBool, SharedFromThis, SharedFromThisBase}; +use crate::io::IoChain; +use crate::job_group::MaybeJobId; +use crate::operation_context::{OperationContext, EXPANSION_LIMIT_DEFAULT}; +use crate::parse_constants::{ + ParseError, ParseErrorList, ParseTreeFlags, FISH_MAX_EVAL_DEPTH, FISH_MAX_STACK_DEPTH, + SOURCE_LOCATION_UNKNOWN, +}; +use crate::parse_execution::{EndExecutionReason, ParseExecutionContext}; +use crate::parse_tree::{parse_source, ParsedSourceRef, ParsedSourceRefFFI}; +use crate::proc::{job_reap, JobGroupRef, JobList, JobRef, ProcStatus}; +use crate::signal::{signal_check_cancel, signal_clear_cancel, Signal}; +use crate::threads::assert_is_main_thread; +use crate::util::get_time; +use crate::wait_handle::WaitHandleStore; +use crate::wchar::{wstr, WString, L}; +use crate::wchar_ffi::{AsWstr, WCharFromFFI, WCharToFFI}; +use crate::wutil::{perror, wgettext, wgettext_fmt}; +use cxx::{CxxWString, UniquePtr}; +use libc::c_int; +use libc::{O_RDONLY, STDERR_FILENO}; +use once_cell::sync::Lazy; +pub use parser_ffi::{library_data_pod_t, BlockType, LoopStatus}; +use printf_compat::sprintf; +use std::cell::{Ref, RefCell, RefMut}; +use std::ffi::{CStr, OsStr}; +use std::os::fd::{AsRawFd, RawFd}; +use std::os::unix::prelude::OsStrExt; +use std::pin::Pin; +use std::rc::Rc; +use std::sync::{ + atomic::{AtomicIsize, AtomicU64, Ordering}, + Arc, +}; +use widestring_suffix::widestrs; + +use self::parser_ffi::ParseErrorListFfi; + +/// block_t represents a block of commands. +#[derive(Default)] +pub struct Block { + /// If this is a function block, the function name. Otherwise empty. + pub function_name: WString, + + /// List of event blocks. + pub event_blocks: u64, + + /// If this is a function block, the function args. Otherwise empty. + pub function_args: Vec, + + /// Name of file that created this block. + pub src_filename: Option, + + // If this is an event block, the event. Otherwise ignored. + pub event: Option>, + + // If this is a source block, the source'd file, interned. + // Otherwise nothing. + pub sourced_file: Option, + + /// Line number where this block was created. + pub src_lineno: Option, + + /// Type of block. + block_type: BlockType, + + /// Whether we should pop the environment variable stack when we're popped off of the block + /// stack. + pub wants_pop_env: bool, +} + +impl Default for BlockType { + fn default() -> Self { + BlockType::top + } +} + +impl Block { + /// Construct from a block type. + pub fn new(block_type: BlockType) -> Self { + Self { + block_type, + ..Default::default() + } + } + + /// Description of the block, for debugging. + #[widestrs] + pub fn description(&self) -> WString { + let mut result = match self.typ() { + BlockType::while_block => "while"L, + BlockType::for_block => "for"L, + BlockType::if_block => "if"L, + BlockType::function_call => "function_call"L, + BlockType::function_call_no_shadow => "function_call_no_shadow"L, + BlockType::switch_block => "switch"L, + BlockType::subst => "substitution"L, + BlockType::top => "top"L, + BlockType::begin => "begin"L, + BlockType::source => "source"L, + BlockType::event => "event"L, + BlockType::breakpoint => "breakpoint"L, + BlockType::variable_assignment => "variable_assignment"L, + _ => panic!(), + } + .to_owned(); + + if let Some(src_lineno) = self.src_lineno { + result.push_utfstr(&sprintf!(" (line %d)", src_lineno)); + } + if let Some(src_filename) = &self.src_filename { + result.push_utfstr(&sprintf!(" (file %ls)", src_filename)); + } + result + } + + pub fn typ(&self) -> BlockType { + self.block_type + } + + /// \return if we are a function call (with or without shadowing). + pub fn is_function_call(&self) -> bool { + [BlockType::function_call, BlockType::function_call_no_shadow].contains(&self.typ()) + } + + /// Entry points for creating blocks. + pub fn if_block() -> Block { + Block::new(BlockType::if_block) + } + pub fn event_block(event: Event) -> Block { + let mut b = Block::new(BlockType::event); + b.event = Some(Rc::new(event)); + b + } + pub fn function_block(name: WString, args: Vec, shadows: bool) -> Block { + let mut b = Block::new(if shadows { + BlockType::function_call + } else { + BlockType::function_call_no_shadow + }); + b.function_name = name; + b.function_args = args; + b + } + pub fn source_block(src: FilenameRef) -> Block { + let mut b = Block::new(BlockType::source); + b.sourced_file = Some(src); + b + } + pub fn for_block() -> Block { + Block::new(BlockType::for_block) + } + pub fn while_block() -> Block { + Block::new(BlockType::while_block) + } + pub fn switch_block() -> Block { + Block::new(BlockType::switch_block) + } + pub fn scope_block(typ: BlockType) -> Block { + assert!( + [BlockType::begin, BlockType::top, BlockType::subst].contains(&typ), + "Invalid scope type" + ); + Block::new(typ) + } + pub fn breakpoint_block() -> Block { + Block::new(BlockType::breakpoint) + } + pub fn variable_assignment_block() -> Block { + Block::new(BlockType::variable_assignment) + } +} + +type Microseconds = i64; + +#[derive(Default)] +pub struct ProfileItem { + /// Time spent executing the command, including nested blocks. + pub duration: Microseconds, + + /// The block level of the specified command. Nested blocks and command substitutions both + /// increase the block level. + pub level: isize, + + /// If the execution of this command was skipped. + pub skipped: bool, + + /// The command string. + pub cmd: WString, +} + +impl ProfileItem { + pub fn new() -> Self { + Default::default() + } + /// \return the current time as a microsecond timestamp since the epoch. + pub fn now() -> Microseconds { + get_time() + } +} + +/// Miscellaneous data used to avoid recursion and others. +#[derive(Default)] +pub struct LibraryData { + pub pods: library_data_pod_t, + + /// The current filename we are evaluating, either from builtin source or on the command line. + pub current_filename: Option, + + /// A stack of fake values to be returned by builtin_commandline. This is used by the completion + /// machinery when wrapping: e.g. if `tig` wraps `git` then git completions need to see git on + /// the command line. + pub transient_commandlines: Vec, + + /// A file descriptor holding the current working directory, for use in openat(). + /// This is never null and never invalid. + pub cwd_fd: Option>, + + pub status_vars: StatusVars, +} + +impl LibraryData { + pub fn new() -> Self { + Self { + pods: library_data_pod_t { + last_exec_run_counter: u64::MAX, + ..Default::default() + }, + ..Default::default() + } + } +} + +impl Default for LoopStatus { + fn default() -> Self { + LoopStatus::normals + } +} + +/// Status variables set by the main thread as jobs are parsed and read by various consumers. +#[derive(Default)] +pub struct StatusVars { + /// Used to get the head of the current job (not the current command, at least for now) + /// for `status current-command`. + pub command: WString, + /// Used to get the full text of the current job for `status current-commandline`. + pub commandline: WString, +} + +/// The result of Parser::eval family. +#[derive(Default)] +pub struct EvalRes { + /// The value for $status. + pub status: ProcStatus, + + /// If set, there was an error that should be considered a failed expansion, such as + /// command-not-found. For example, `touch (not-a-command)` will not invoke 'touch' because + /// command-not-found will mark break_expand. + pub break_expand: bool, + + /// If set, no commands were executed and there we no errors. + pub was_empty: bool, + + /// If set, no commands produced a $status value. + pub no_status: bool, +} + +impl EvalRes { + pub fn new(status: ProcStatus) -> Self { + Self { + status, + ..Default::default() + } + } +} + +pub enum ParserStatusVar { + current_command, + current_commandline, + count_, +} + +pub type BlockId = usize; + +pub type ParserRef = Rc; + +pub struct Parser { + base: SharedFromThisBase, + + /// The current execution context. + execution_context: RefCell>>, + + /// The jobs associated with this parser. + job_list: RefCell, + + /// Our store of recorded wait-handles. These are jobs that finished in the background, + /// and have been reaped, but may still be wait'ed on. + wait_handles: RefCell, + + /// The list of blocks. + /// This is a stack; the topmost block is at the end. This is to avoid invalidating block + /// indexes during recursive evaluation. + block_list: RefCell>, + + /// The 'depth' of the fish call stack. + pub eval_level: AtomicIsize, + + /// Set of variables for the parser. + pub variables: EnvStackRef, + variables_ffi: EnvStackRefFFI, + + /// Miscellaneous library data. + library_data: RefCell, + + /// If set, we synchronize universal variables after external commands, + /// including sending on-variable change events. + syncs_uvars: RelaxedAtomicBool, + + /// If set, we are the principal parser. + is_principal: RelaxedAtomicBool, + + /// List of profile items. + profile_items: RefCell>, + + /// Global event blocks. + pub global_event_blocks: AtomicU64, +} + +impl SharedFromThis for Parser { + fn get_base(&self) -> &SharedFromThisBase { + &self.base + } +} + +impl Parser { + /// Create a parser + pub fn new(variables: EnvStackRef, is_principal: bool) -> ParserRef { + let variables_ffi = EnvStackRefFFI(variables.clone()); + let result = Rc::new(Self { + base: SharedFromThisBase::new(), + execution_context: RefCell::default(), + job_list: RefCell::default(), + wait_handles: RefCell::new(WaitHandleStore::new()), + block_list: RefCell::default(), + eval_level: AtomicIsize::new(-1), + variables, + variables_ffi, + library_data: RefCell::new(LibraryData::new()), + syncs_uvars: RelaxedAtomicBool::new(false), + is_principal: RelaxedAtomicBool::new(is_principal), + profile_items: RefCell::default(), + global_event_blocks: AtomicU64::new(0), + }); + let cwd = open_cloexec(CStr::from_bytes_with_nul(b".\0").unwrap(), O_RDONLY, 0); + if cwd < 0 { + perror("Unable to open the current working directory"); + } else { + result.libdata_mut().cwd_fd = Some(Arc::new(AutoCloseFd::new(cwd))); + } + result.base.initialize(&result); + result + } + + fn execution_context(&self) -> Ref<'_, Option>> { + self.execution_context.borrow() + } + + /// Adds a job to the beginning of the job list. + pub fn job_add(&self, job: JobRef) { + assert!(!job.processes().is_empty()); + self.jobs_mut().insert(0, job); + } + + /// \return whether we are currently evaluating a function. + pub fn is_function(&self) -> bool { + let blocks = self.blocks(); + for b in blocks.iter().rev() { + if b.is_function_call() { + return true; + } else if b.typ() == BlockType::source { + // If a function sources a file, don't descend further. + break; + } + } + false + } + + /// \return whether we are currently evaluating a command substitution. + pub fn is_command_substitution(&self) -> bool { + let blocks = self.blocks(); + for b in blocks.iter().rev() { + if b.typ() == BlockType::subst { + return true; + } else if b.typ() == BlockType::source { + // If a function sources a file, don't descend further. + break; + } + } + false + } + + /// Get the "principal" parser, whatever that is. + pub fn principal_parser() -> &'static Parser { + static mut PRINCIPAL: Lazy = + Lazy::new(|| Parser::new(EnvStack::principal().clone(), true)); + unsafe { + PRINCIPAL.assert_can_execute(); + &PRINCIPAL + } + } + + /// Assert that this parser is allowed to execute on the current thread. + pub fn assert_can_execute(&self) { + assert_is_main_thread(); + } + + pub fn eval(&self, cmd: &wstr, io: &IoChain) -> EvalRes { + self.eval_with(cmd, io, None, BlockType::top) + } + + /// Evaluate the expressions contained in cmd. + /// + /// \param cmd the string to evaluate + /// \param io io redirections to perform on all started jobs + /// \param job_group if set, the job group to give to spawned jobs. + /// \param block_type The type of block to push on the block stack, which must be either 'top' + /// or 'subst'. + /// \return the result of evaluation. + pub fn eval_with( + &self, + cmd: &wstr, + io: &IoChain, + job_group: Option<&JobGroupRef>, + block_type: BlockType, + ) -> EvalRes { + // Parse the source into a tree, if we can. + let mut error_list = ParseErrorList::new(); + if let Some(ps) = parse_source( + cmd.to_owned(), + ParseTreeFlags::empty(), + Some(&mut error_list), + ) { + return self.eval_parsed_source(&ps, io, job_group, block_type); + } + + // Get a backtrace. This includes the message. + let backtrace_and_desc = self.get_backtrace(cmd, &error_list); + + // Print it. + fwprintf!(STDERR_FILENO, "%ls\n", backtrace_and_desc); + + // Set a valid status. + self.set_last_statuses(Statuses::just(STATUS_ILLEGAL_CMD.unwrap())); + let break_expand = true; + EvalRes { + status: ProcStatus::from_exit_code(STATUS_ILLEGAL_CMD.unwrap()), + break_expand, + ..Default::default() + } + } + + /// Evaluate the parsed source ps. + /// Because the source has been parsed, a syntax error is impossible. + pub fn eval_parsed_source( + &self, + ps: &ParsedSourceRef, + io: &IoChain, + job_group: Option<&JobGroupRef>, + block_type: BlockType, + ) -> EvalRes { + assert!([BlockType::top, BlockType::subst].contains(&block_type)); + let job_list = ps.ast.top().as_job_list().unwrap(); + if !job_list.is_empty() { + // Execute the top job list. + self.eval_node(ps, job_list, io, job_group, block_type) + } else { + let status = ProcStatus::from_exit_code(self.get_last_status()); + EvalRes { + status, + break_expand: false, + was_empty: true, + no_status: true, + } + } + } + + /// Evaluates a node. + /// The node type must be ast::Statement or ast::JobList. + pub fn eval_node( + &self, + ps: &ParsedSourceRef, + node: &T, + block_io: &IoChain, + job_group: Option<&JobGroupRef>, + block_type: BlockType, + ) -> EvalRes { + // Only certain blocks are allowed. + assert!( + [BlockType::top, BlockType::subst].contains(&block_type), + "Invalid block type" + ); + + // If fish itself got a cancel signal, then we want to unwind back to the principal parser. + // If we are the principal parser and our block stack is empty, then we want to clear the + // signal. + // Note this only happens in interactive sessions. In non-interactive sessions, SIGINT will + // cause fish to exit. + let sig = signal_check_cancel(); + if sig != 0 { + if self.is_principal.load() && self.block_list.borrow().is_empty() { + signal_clear_cancel(); + } else { + return EvalRes::new(ProcStatus::from_signal(Signal::new(sig))); + } + } + + // A helper to detect if we got a signal. + // This includes both signals sent to fish (user hit control-C while fish is foreground) and + // signals from the job group (e.g. some external job terminated with SIGQUIT). + let jg = job_group.cloned(); + let check_cancel_signal = move || { + // Did fish itself get a signal? + let sig = signal_check_cancel(); + if sig != 0 { + return Some(Signal::new(sig)); + } + // Has this job group been cancelled? + jg.as_ref().and_then(|jg| jg.get_cancel_signal()) + }; + + // If we have a job group which is cancelled, then do nothing. + if let Some(sig) = check_cancel_signal() { + return EvalRes::new(ProcStatus::from_signal(sig)); + } + + job_reap(self, false); // not sure why we reap jobs here + + // Start it up + let mut op_ctx = self.context(); + let scope_block = self.push_block(Block::scope_block(block_type)); + + // Propagate our job group. + op_ctx.job_group = job_group.cloned(); + + // Replace the context's cancel checker with one that checks the job group's signal. + let cancel_checker: CancelChecker = Box::new(move || check_cancel_signal().is_some()); + op_ctx.cancel_checker = cancel_checker; + + // Create and set a new execution context. + let exc = scoped_push_replacer( + |new_value| { + if self.execution_context.borrow().is_none() || new_value.is_none() { + // Outermost node. + std::mem::replace(&mut self.execution_context.borrow_mut(), new_value) + } else { + Some(ParseExecutionContext::swap( + self.execution_context.borrow().as_ref().unwrap(), + new_value.unwrap(), + )) + } + }, + Some(Box::new(ParseExecutionContext::new( + ps.clone(), + block_io.clone(), + ))), + ); + + // Check the exec count so we know if anything got executed. + let prev_exec_count = self.libdata().pods.exec_count; + let prev_status_count = self.libdata().pods.status_count; + let reason = + self.execution_context() + .as_ref() + .unwrap() + .eval_node(&op_ctx, node, Some(scope_block)); + let new_exec_count = self.libdata().pods.exec_count; + let new_status_count = self.libdata().pods.status_count; + + ScopeGuarding::commit(exc); + self.pop_block(scope_block); + + job_reap(self, false); // reap again + + let sig = signal_check_cancel(); + if sig != 0 { + EvalRes::new(ProcStatus::from_signal(Signal::new(sig))) + } else { + let status = ProcStatus::from_exit_code(self.get_last_status()); + let break_expand = reason == EndExecutionReason::error; + EvalRes { + status, + break_expand, + was_empty: !break_expand && prev_exec_count == new_exec_count, + no_status: prev_status_count == new_status_count, + } + } + } + + /// Evaluate line as a list of parameters, i.e. tokenize it and perform parameter expansion and + /// cmdsubst execution on the tokens. Errors are ignored. If a parser is provided, it is used + /// for command substitution expansion. + pub fn expand_argument_list( + arg_list_src: &wstr, + flags: ExpandFlags, + ctx: &OperationContext<'_>, + ) -> CompletionList { + // Parse the string as an argument list. + let ast = Ast::parse_argument_list(arg_list_src, ParseTreeFlags::default(), None); + if ast.errored() { + // Failed to parse. Here we expect to have reported any errors in test_args. + return vec![]; + } + + // Get the root argument list and extract arguments from it. + let mut result = vec![]; + let list = ast.top().as_freestanding_argument_list().unwrap(); + for arg in &list.arguments { + let arg_src = arg.source(arg_list_src); + if expand_string(arg_src.to_owned(), &mut result, flags, ctx, None) + == ExpandResultCode::error + { + break; // failed to expand a string + } + } + result + } + + /// Returns a string describing the current parser position in the format 'FILENAME (line + /// LINE_NUMBER): LINE'. Example: + /// + /// init.fish (line 127): ls|grep pancake + pub fn current_line(&self) -> WString { + if self.execution_context().is_none() { + return WString::new(); + }; + let Some(source_offset) = self + .execution_context() + .as_ref() + .unwrap() + .get_current_source_offset() + else { + return WString::new(); + }; + + let lineno = self.get_lineno().unwrap_or(0); + let file = self.current_filename(); + + let mut prefix = WString::new(); + + // If we are not going to print a stack trace, at least print the line number and filename. + if !self.is_interactive() || self.is_function() { + if let Some(file) = file { + prefix.push_utfstr(&wgettext_fmt!( + "%ls (line %d): ", + &user_presentable_path(&file, self.vars()), + lineno + )); + } else if self.libdata().pods.within_fish_init { + prefix.push_utfstr(&wgettext_fmt!("Startup (line %d): ", lineno)); + } else { + prefix.push_utfstr(&wgettext_fmt!("Standard input (line %d): ", lineno)); + } + } + + let skip_caret = self.is_interactive() && !self.is_function(); + + // Use an error with empty text. + let mut empty_error = ParseError::default(); + empty_error.source_start = source_offset; + + let mut line_info = empty_error.describe_with_prefix( + &self.execution_context().as_ref().unwrap().get_source(), + &prefix, + self.is_interactive(), + skip_caret, + ); + if !line_info.is_empty() { + line_info.push('\n'); + } + + line_info.push_utfstr(&self.stack_trace()); + line_info + } + + /// Returns the current line number. + pub fn get_lineno(&self) -> Option { + self.execution_context() + .as_ref() + .and_then(|ctx| ctx.get_current_line_number()) + } + + /// \return whether we are currently evaluating a "block" such as an if statement. + /// This supports 'status is-block'. + pub fn is_block(&self) -> bool { + // Note historically this has descended into 'source', unlike 'is_function'. + let blocks = self.blocks(); + for b in blocks.iter().rev() { + if ![BlockType::top, BlockType::subst].contains(&b.typ()) { + return true; + } + } + false + } + + /// \return whether we have a breakpoint block. + pub fn is_breakpoint(&self) -> bool { + let blocks = self.blocks(); + for b in blocks.iter().rev() { + if b.typ() == BlockType::breakpoint { + return true; + } + } + false + } + + /// Return the list of blocks. The first block is at the top. + /// todo!("this RAII object should only be used for iterating over it (in reverse). Maybe enforce this") + pub fn blocks(&self) -> Ref<'_, Vec> { + self.block_list.borrow() + } + pub fn block_at_index(&self, index: usize) -> Option> { + let block_list = self.blocks(); + if index >= block_list.len() { + None + } else { + Some(Ref::map(block_list, |bl| &bl[bl.len() - 1 - index])) + } + } + pub fn block_at_index_mut(&self, index: usize) -> Option> { + let block_list = self.block_list.borrow_mut(); + if index >= block_list.len() { + None + } else { + Some(RefMut::map(block_list, |bl| { + let len = bl.len(); + &mut bl[len - 1 - index] + })) + } + } + + pub fn blocks_size(&self) -> usize { + self.block_list.borrow().len() + } + + /// Get the list of jobs. + pub fn jobs(&self) -> Ref<'_, JobList> { + self.job_list.borrow() + } + pub fn jobs_mut(&self) -> RefMut<'_, JobList> { + self.job_list.borrow_mut() + } + + /// Get the variables. + pub fn vars(&self) -> &EnvStack { + &self.variables + } + + /// Get the library data. + pub fn libdata(&self) -> Ref<'_, LibraryData> { + self.library_data.borrow() + } + pub fn libdata_mut(&self) -> RefMut<'_, LibraryData> { + self.library_data.borrow_mut() + } + + /// Get our wait handle store. + pub fn get_wait_handles(&self) -> Ref<'_, WaitHandleStore> { + self.wait_handles.borrow() + } + pub fn mut_wait_handles(&self) -> RefMut<'_, WaitHandleStore> { + self.wait_handles.borrow_mut() + } + + /// Get and set the last proc statuses. + pub fn get_last_status(&self) -> c_int { + self.vars().get_last_status() + } + pub fn get_last_statuses(&self) -> Statuses { + self.vars().get_last_statuses() + } + pub fn set_last_statuses(&self, s: Statuses) { + self.vars().set_last_statuses(s) + } + + /// Cover of vars().set(), which also fires any returned event handlers. + /// \return a value like ENV_OK. + pub fn set_var_and_fire( + &self, + key: &wstr, + mode: EnvMode, + vals: Vec, + ) -> EnvStackSetResult { + let res = self.vars().set(key, mode, vals); + if res == EnvStackSetResult::ENV_OK { + event::fire(self, Event::variable_set(key.to_owned())); + } + res + } + + /// Update any universal variables and send event handlers. + /// If \p always is set, then do it even if we have no pending changes (that is, look for + /// changes from other fish instances); otherwise only sync if this instance has changed uvars. + pub fn sync_uvars_and_fire(&self, always: bool) { + if self.syncs_uvars.load() { + let evts = self.vars().universal_sync(always); + for evt in evts { + event::fire(self, evt); + } + } + } + + /// Pushes a new block. Returns a pointer to the block, stored in the parser. + pub fn push_block(&self, mut block: Block) -> BlockId { + block.src_lineno = self.get_lineno(); + block.src_filename = self.current_filename(); + if block.typ() != BlockType::top { + let new_scope = block.typ() == BlockType::function_call; + self.vars().push(new_scope); + block.wants_pop_env = true; + } + + let mut block_list = self.block_list.borrow_mut(); + block_list.push(block); + block_list.len() - 1 + } + + /// Remove the outermost block, asserting it's the given one. + pub fn pop_block(&self, expected: BlockId) { + let block = { + let mut block_list = self.block_list.borrow_mut(); + assert!(expected == block_list.len() - 1); + block_list.pop().unwrap() + }; + if block.wants_pop_env { + self.vars().pop(); + } + } + + /// Return the function name for the specified stack frame. Default is one (current frame). + pub fn get_function_name(&self, level: i32) -> Option { + if level == 0 { + // Return the function name for the level preceding the most recent breakpoint. If there + // isn't one return the function name for the current level. + // Walk until we find a breakpoint, then take the next function. + let mut found_breakpoint = false; + let blocks = self.blocks(); + for b in blocks.iter().rev() { + if b.typ() == BlockType::breakpoint { + found_breakpoint = true; + } else if found_breakpoint && b.is_function_call() { + return Some(b.function_name.clone()); + } + } + return None; // couldn't find a breakpoint frame + } + + // Level 1 is the topmost function call. Level 2 is its caller. Etc. + let mut funcs_seen = 0; + let blocks = self.blocks(); + for b in blocks.iter().rev() { + if b.is_function_call() { + funcs_seen += 1; + if funcs_seen == level { + return Some(b.function_name.clone()); + } + } else if b.typ() == BlockType::source && level == 1 { + // Historical: If we want the topmost function, but we are really in a file sourced by a + // function, don't consider ourselves to be in a function. + break; + } + } + None + } + + /// Promotes a job to the front of the list. + pub fn job_promote_at(&self, job_pos: usize) { + // Move the job to the beginning. + self.jobs_mut().rotate_left(job_pos); + } + + /// Return the job with the specified job id. If id is 0 or less, return the last job used. + pub fn job_with_id(&self, job_id: MaybeJobId) -> Option { + for job in self.jobs().iter() { + if job_id.is_none() || job_id == job.job_id() { + return Some(job.clone()); + } + } + None + } + + /// Returns the job with the given pid. + pub fn job_get_from_pid(&self, pid: libc::pid_t) -> Option { + self.job_get_with_index_from_pid(pid).map(|t| t.1) + } + + /// Returns the job and job index with the given pid. + pub fn job_get_with_index_from_pid(&self, pid: libc::pid_t) -> Option<(usize, JobRef)> { + for (i, job) in self.jobs().iter().enumerate() { + for p in job.processes().iter() { + if p.pid.load(Ordering::Relaxed) == pid { + return Some((i, job.clone())); + } + } + } + None + } + + /// Returns a new profile item if profiling is active. The caller should fill it in. + /// The Parser will deallocate it. + /// If profiling is not active, this returns nullptr. + pub fn create_profile_item(&self) -> Option { + if PROFILING_ACTIVE.load() { + let mut profile_items = self.profile_items.borrow_mut(); + profile_items.push(ProfileItem::new()); + return Some(profile_items.len() - 1); + } + None + } + + pub fn profile_items_mut(&self) -> RefMut<'_, Vec> { + self.profile_items.borrow_mut() + } + + /// Remove the profiling items. + pub fn clear_profiling(&self) { + self.profile_items.borrow_mut().clear(); + } + + /// Output profiling data to the given filename. + pub fn emit_profiling(&self, path: &[u8]) { + // Save profiling information. OK to not use CLO_EXEC here because this is called while fish is + // exiting (and hence will not fork). + let f = match std::fs::File::create(OsStr::from_bytes(path)) { + Ok(f) => f, + Err(err) => { + FLOGF!( + warning, + "%s", + &wgettext_fmt!( + "Could not write profiling information to file '%s': %s", + &String::from_utf8_lossy(path), + format!("{}", err) + ) + ); + return; + } + }; + fwprintf!(f.as_raw_fd(), "Time\tSum\tCommand\n"); + print_profile(&self.profile_items.borrow(), f.as_raw_fd()); + } + + pub fn get_backtrace(&self, src: &wstr, errors: &ParseErrorList) -> WString { + let Some(err) = errors.first() else { + return WString::new(); + }; + + // Determine if we want to try to print a caret to point at the source error. The + // err.source_start() <= src.size() check is due to the nasty way that slices work, which is + // by rewriting the source. + let mut which_line = 0; + let mut skip_caret = true; + if err.source_start != SOURCE_LOCATION_UNKNOWN && err.source_start <= src.len() { + // Determine which line we're on. + which_line = 1 + src[..err.source_start] + .chars() + .filter(|c| *c == '\n') + .count(); + + // Don't include the caret if we're interactive, this is the first line of text, and our + // source is at its beginning, because then it's obvious. + skip_caret = self.is_interactive() && which_line == 1 && err.source_start == 0; + } + + let prefix = if let Some(filename) = self.current_filename() { + if which_line > 0 { + wgettext_fmt!( + "%ls (line %lu): ", + user_presentable_path(&filename, self.vars()), + which_line + ) + } else { + wgettext_fmt!("%ls: ", user_presentable_path(&filename, self.vars())) + } + } else { + L!("fish: ").to_owned() + }; + + let mut output = err.describe_with_prefix(src, &prefix, self.is_interactive(), skip_caret); + if !output.is_empty() { + output.push('\n'); + } + output.push_utfstr(&self.stack_trace()); + output + } + + /// Returns the file currently evaluated by the parser. This can be different than + /// reader_current_filename, e.g. if we are evaluating a function defined in a different file + /// than the one currently read. + pub fn current_filename(&self) -> Option { + let blocks = self.blocks(); + for b in blocks.iter().rev() { + if b.is_function_call() { + return function::get_props(&b.function_name) + .and_then(|props| props.definition_file.clone()); + } else if b.typ() == BlockType::source { + return b.sourced_file.clone(); + } + } + // Fall back to the file being sourced. + self.libdata().current_filename.clone() + } + + /// Return if we are interactive, which means we are executing a command that the user typed in + /// (and not, say, a prompt). + pub fn is_interactive(&self) -> bool { + self.libdata().pods.is_interactive + } + + /// Return a string representing the current stack trace. + pub fn stack_trace(&self) -> WString { + let mut trace = WString::new(); + let blocks = self.blocks(); + for b in blocks.iter().rev() { + append_block_description_to_stack_trace(self, b, &mut trace); + + // Stop at event handler. No reason to believe that any other code is relevant. + // + // It might make sense in the future to continue printing the stack trace of the code + // that invoked the event, if this is a programmatic event, but we can't currently + // detect that. + if b.typ() == BlockType::event { + break; + } + } + trace + } + + /// \return whether the number of functions in the stack exceeds our stack depth limit. + pub fn function_stack_is_overflowing(&self) -> bool { + // We are interested in whether the count of functions on the stack exceeds + // FISH_MAX_STACK_DEPTH. We don't separately track the number of functions, but we can have a + // fast path through the eval_level. If the eval_level is in bounds, so must be the stack depth. + if self.eval_level.load(Ordering::Relaxed) <= isize::try_from(FISH_MAX_STACK_DEPTH).unwrap() + { + return false; + } + // Count the functions. + let mut depth = 0; + let blocks = self.blocks(); + for b in blocks.iter().rev() { + depth += if b.is_function_call() { 1 } else { 0 }; + } + depth > FISH_MAX_STACK_DEPTH + } + + /// Mark whether we should sync universal variables. + pub fn set_syncs_uvars(&self, flag: bool) { + self.syncs_uvars.store(flag); + } + + /// \return a shared pointer reference to this parser. + pub fn shared(&self) -> ParserRef { + self.shared_from_this() + } + + /// \return the operation context for this parser. + pub fn context(&self) -> OperationContext<'static> { + OperationContext::foreground( + self.shared(), + Box::new(|| signal_check_cancel() != 0), + EXPANSION_LIMIT_DEFAULT, + ) + } + + /// Checks if the max eval depth has been exceeded + pub fn is_eval_depth_exceeded(&self) -> bool { + self.eval_level.load(Ordering::Relaxed) >= isize::try_from(FISH_MAX_EVAL_DEPTH).unwrap() + } +} + +// Given a file path, return something nicer. Currently we just "unexpand" tildes. +fn user_presentable_path(path: &wstr, vars: &dyn Environment) -> WString { + replace_home_directory_with_tilde(path, vars) +} + +/// Print profiling information to the specified stream. +fn print_profile(items: &[ProfileItem], out: RawFd) { + for (idx, item) in items.iter().enumerate() { + if item.skipped || item.cmd.is_empty() { + continue; + } + + let total_time = item.duration; + + // Compute the self time as the total time, minus the total time consumed by subsequent + // items exactly one eval level deeper. + let mut self_time = item.duration; + for nested_item in items[idx + 1..].iter() { + if nested_item.skipped { + continue; + } + + // If the eval level is not larger, then we have exhausted nested items. + if nested_item.level <= item.level { + break; + } + + // If the eval level is exactly one more than our level, it is a directly nested item. + if nested_item.level == item.level + 1 { + self_time -= nested_item.duration; + } + } + + fwprintf!(out, "%lld\t%lld\t", self_time, total_time); + for _i in 0..item.level { + fwprintf!(out, "-"); + } + + fwprintf!(out, "> %ls\n", item.cmd); + } +} + +/// Append stack trace info for the block \p b to \p trace. +fn append_block_description_to_stack_trace(parser: &Parser, b: &Block, trace: &mut WString) { + let mut print_call_site = false; + match b.typ() { + BlockType::function_call | BlockType::function_call_no_shadow => { + trace.push_utfstr(&wgettext_fmt!("in function '%ls'", &b.function_name)); + // Print arguments on the same line. + let mut args_str = WString::new(); + for arg in &b.function_args { + if !args_str.is_empty() { + args_str.push(' '); + } + // We can't quote the arguments because we print this in quotes. + // As a special-case, add the empty argument as "". + if !arg.is_empty() { + args_str.push_utfstr(&escape_string( + arg, + EscapeStringStyle::Script(EscapeFlags::NO_QUOTED), + )) + } else { + args_str.push_str("\"\""); + } + } + if !args_str.is_empty() { + // TODO: Escape these. + trace.push_utfstr(&wgettext_fmt!(" with arguments '%ls'", args_str)); + } + trace.push('\n'); + print_call_site = true; + } + BlockType::subst => { + trace.push_utfstr(&wgettext!("in command substitution\n")); + print_call_site = true; + } + BlockType::source => { + let source_dest = b.sourced_file.as_ref().unwrap(); + trace.push_utfstr(&wgettext_fmt!( + "from sourcing file %ls\n", + &user_presentable_path(source_dest, parser.vars()) + )); + print_call_site = true; + } + BlockType::event => { + let description = + event::get_desc(parser, b.event.as_ref().expect("Should have an event")); + trace.push_utfstr(&wgettext_fmt!("in event handler: %ls\n", &description)); + print_call_site = true; + } + BlockType::top + | BlockType::begin + | BlockType::switch_block + | BlockType::while_block + | BlockType::for_block + | BlockType::if_block + | BlockType::breakpoint + | BlockType::variable_assignment => {} + _ => unreachable!(), + } + + if print_call_site { + // Print where the function is called. + if let Some(file) = b.src_filename.as_ref() { + trace.push_utfstr(&sprintf!( + "\tcalled on line %d of file %ls\n", + b.src_lineno.unwrap_or(0), + user_presentable_path(file, parser.vars()) + )); + } else if parser.libdata().pods.within_fish_init { + trace.push_str("\tcalled during startup\n"); + } + } +} + +#[cxx::bridge] +mod parser_ffi { + /// Types of blocks. + #[derive(Debug)] + #[cxx_name = "block_type_t"] + pub enum BlockType { + /// While loop block + while_block, + /// For loop block + for_block, + /// If block + if_block, + /// Function invocation block + function_call, + /// Function invocation block with no variable shadowing + function_call_no_shadow, + /// Switch block + switch_block, + /// Command substitution scope + subst, + /// Outermost block + top, + /// Unconditional block + begin, + /// Block created by the . (source) builtin + source, + /// Block created on event notifier invocation + event, + /// Breakpoint block + breakpoint, + /// Variable assignment before a command + variable_assignment, + } + + /// Possible states for a loop. + pub enum LoopStatus { + /// current loop block executed as normal + normals, + /// current loop block should be removed + breaks, + /// current loop block should be skipped + continues, + } + + /// Plain-Old-Data components of `struct library_data_t` that can be shared over FFI + #[derive(Default)] + pub struct library_data_pod_t { + /// A counter incremented every time a command executes. + pub exec_count: u64, + + /// A counter incremented every time a command produces a $status. + pub status_count: u64, + + /// Last reader run count. + pub last_exec_run_counter: u64, + + /// Number of recursive calls to the internal completion function. + pub complete_recursion_level: u32, + + /// If set, we are currently within fish's initialization routines. + pub within_fish_init: bool, + + /// If we're currently repainting the commandline. + /// Useful to stop infinite loops. + pub is_repaint: bool, + + /// Whether we called builtin_complete -C without parameter. + pub builtin_complete_current_commandline: bool, + + /// Whether we are currently cleaning processes. + pub is_cleaning_procs: bool, + + /// The internal job id of the job being populated, or 0 if none. + /// This supports the '--on-job-exit caller' feature. + pub caller_id: u64, // TODO should be InternalJobId + + /// Whether we are running a subshell command. + pub is_subshell: bool, + + /// Whether we are running an event handler. This is not a bool because we keep count of the + /// event nesting level. + pub is_event: i32, + + /// Whether we are currently interactive. + pub is_interactive: bool, + + /// Whether to suppress fish_trace output. This occurs in the prompt, event handlers, and key + /// bindings. + pub suppress_fish_trace: bool, + + /// Whether we should break or continue the current loop. + /// This is set by the 'break' and 'continue' commands. + pub loop_status: LoopStatus, + + /// Whether we should return from the current function. + /// This is set by the 'return' command. + pub returning: bool, + + /// Whether we should stop executing. + /// This is set by the 'exit' command, and unset after 'reader_read'. + /// Note this only exits up to the "current script boundary." That is, a call to exit within a + /// 'source' or 'read' command will only exit up to that command. + pub exit_current_script: bool, + + /// The read limit to apply to captured subshell output, or 0 for none. + pub read_limit: usize, + } + + extern "C++" { + include!("operation_context.h"); + include!("wutil.h"); + include!("env.h"); + include!("io.h"); + include!("proc.h"); + include!("parse_tree.h"); + include!("parse_constants.h"); + type IoChain = crate::io::IoChain; + type JobRefFfi = crate::proc::JobRefFfi; + type JobGroupRefFfi = crate::proc::JobGroupRefFfi; + type ParsedSourceRefFFI = crate::parse_tree::ParsedSourceRefFFI; + type ParseErrorListFfi = crate::parse_constants::ParseErrorListFfi; + #[cxx_name = "EnvStackRef"] + type EnvStackRefFFI = crate::env::EnvStackRefFFI; + #[cxx_name = "env_stack_set_result_t"] + type EnvStackSetResult = crate::env::EnvStackSetResult; + type wcstring_list_ffi_t = crate::ffi::wcstring_list_ffi_t; + type OperationContext<'a> = crate::operation_context::OperationContext<'a>; + type Statuses = crate::env::Statuses; + } + extern "Rust" { + type Block; + } + extern "Rust" { + type LibraryData; + fn exec_count(&self) -> u64; + #[cxx_name = "is_repaint"] + fn is_repaint_ffi(&self) -> bool; + fn set_status_vars_command(&mut self, s: &CxxWString); + fn set_status_vars_commandline(&mut self, s: &CxxWString); + + #[cxx_name = "transient_commandlines_empty"] + fn transient_commandlines_empty_ffi(&self) -> bool; + #[cxx_name = "transient_commandlines_back"] + fn transient_commandlines_back_ffi(&self) -> UniquePtr; + #[cxx_name = "transient_commandlines_push"] + fn transient_commandlines_push_ffi(&mut self, s: &CxxWString); + #[cxx_name = "transient_commandlines_pop"] + fn transient_commandlines_pop_ffi(&mut self); + } + extern "Rust" { + type EvalRes; + fn no_status(&self) -> bool; + } + extern "Rust" { + type Parser; + + fn get_last_status(&self) -> i32; + fn assert_can_execute(&self); + fn is_interactive(&self) -> bool; + #[cxx_name = "eval"] + fn ffi_eval(&self, cmd: &CxxWString, io: &IoChain) -> Box; + #[cxx_name = "eval_with"] + #[cxx_name = "eval_parsed_source"] + fn ffi_eval_parsed_source(&self, ps: &ParsedSourceRefFFI, io: &IoChain); + #[cxx_name = "is_breakpoint"] + fn ffi_is_breakpoint(&self) -> bool; + #[cxx_name = "libdata"] + fn ffi_libdata(&self) -> &LibraryData; + #[cxx_name = "libdata_mut"] + #[allow(clippy::mut_from_ref)] + fn ffi_libdata_mut(&self) -> &mut LibraryData; + #[cxx_name = "libdata_pods"] + fn ffi_libdata_pods(&self) -> &library_data_pod_t; + #[cxx_name = "libdata_pods_mut"] + #[allow(clippy::mut_from_ref)] + fn ffi_libdata_pods_mut(&self) -> &mut library_data_pod_t; + #[cxx_name = "get_backtrace"] + fn ffi_get_backtrace( + &self, + src: &CxxWString, + errors: &ParseErrorListFfi, + ) -> UniquePtr; + #[cxx_name = "set_last_statuses"] + fn ffi_set_last_statuses(&self, s: &Statuses); + #[cxx_name = "vars"] + fn ffi_vars(&self) -> &EnvStackRefFFI; + #[cxx_name = "vars_boxed"] + fn ffi_vars_boxed(&self) -> *mut u8; + fn sync_uvars_and_fire(&self, always: bool); + #[cxx_name = "parser_principal_parser"] + fn ffi_parser_principal_parser() -> Box; + #[cxx_name = "shared"] + fn ffi_shared(&self) -> Box; + #[cxx_name = "set_var_and_fire"] + fn ffi_set_var_and_fire( + &self, + key: &CxxWString, + mode: u16, + vals: &wcstring_list_ffi_t, + ) -> i32; + fn parser_expand_argument_list_ffi( + arg_list_src: &CxxWString, + flags: u16, + ctx: &OperationContext<'_>, + out: Pin<&mut wcstring_list_ffi_t>, + ); + } + extern "Rust" { + #[cxx_name = "ParserRef"] + type ParserRefFFI; + fn vars(&self) -> &EnvStackRefFFI; + fn deref(&self) -> &Parser; + } +} + +impl LibraryData { + fn exec_count(&self) -> u64 { + self.pods.exec_count + } + fn is_repaint_ffi(&self) -> bool { + self.pods.is_repaint + } + fn set_status_vars_command(&mut self, s: &CxxWString) { + self.status_vars.command = s.from_ffi() + } + fn set_status_vars_commandline(&mut self, s: &CxxWString) { + self.status_vars.commandline = s.from_ffi() + } + fn transient_commandlines_empty_ffi(&self) -> bool { + self.transient_commandlines.is_empty() + } + fn transient_commandlines_back_ffi(&self) -> UniquePtr { + self.transient_commandlines.last().unwrap().to_ffi() + } + fn transient_commandlines_push_ffi(&mut self, s: &CxxWString) { + self.transient_commandlines.push(s.from_ffi()); + } + fn transient_commandlines_pop_ffi(&mut self) { + self.transient_commandlines.pop(); + } +} + +impl EvalRes { + fn no_status(&self) -> bool { + self.no_status + } +} + +unsafe impl cxx::ExternType for Parser { + type Id = cxx::type_id!("Parser"); + type Kind = cxx::kind::Opaque; +} + +pub struct ParserRefFFI(pub ParserRef); + +unsafe impl cxx::ExternType for ParserRefFFI { + type Id = cxx::type_id!("ParserRef"); // CXX name! + type Kind = cxx::kind::Opaque; +} + +fn ffi_parser_principal_parser() -> Box { + Box::new(ParserRefFFI(Parser::principal_parser().shared())) +} + +impl Parser { + fn ffi_shared(&self) -> Box { + Box::new(ParserRefFFI(self.shared())) + } + fn ffi_set_var_and_fire( + &self, + key: &CxxWString, + mode: u16, + vals: &ffi::wcstring_list_ffi_t, + ) -> i32 { + let val: u8 = unsafe { + std::mem::transmute(self.set_var_and_fire( + key.as_wstr(), + EnvMode::from_bits(mode).unwrap(), + vals.from_ffi(), + )) + }; + val as _ + } + pub fn ffi_eval(&self, cmd: &CxxWString, io: &IoChain) -> Box { + Box::new(self.eval(cmd.as_wstr(), io)) + } + fn ffi_eval_parsed_source(&self, ps: &ParsedSourceRefFFI, io: &IoChain) { + self.eval_parsed_source(ps.0.as_ref().unwrap(), io, None, BlockType::top); + } + fn ffi_is_breakpoint(&self) -> bool { + self.is_breakpoint() + } + fn ffi_vars(&self) -> &EnvStackRefFFI { + &self.variables_ffi + } + fn ffi_vars_boxed(&self) -> *mut u8 { + Box::into_raw(Box::new(self.variables_ffi.clone())).cast() + } + fn ffi_libdata(&self) -> &LibraryData { + unsafe { self.library_data.try_borrow_unguarded() }.unwrap() + } + fn ffi_libdata_mut(&self) -> &mut LibraryData { + unsafe { &mut *self.library_data.as_ptr() } + } + fn ffi_libdata_pods(&self) -> &library_data_pod_t { + &unsafe { &*self.library_data.as_ptr() }.pods + } + fn ffi_libdata_pods_mut(&self) -> &mut library_data_pod_t { + &mut unsafe { &mut *self.library_data.as_ptr() }.pods + } + fn ffi_get_backtrace( + &self, + src: &CxxWString, + errors: &ParseErrorListFfi, + ) -> UniquePtr { + self.get_backtrace(&src.from_ffi(), &errors.0).to_ffi() + } + fn ffi_set_last_statuses(&self, s: &Statuses) { + self.set_last_statuses(s.clone()) + } +} + +fn parser_expand_argument_list_ffi( + arg_list_src: &CxxWString, + flags: u16, + ctx: &OperationContext<'_>, + mut out: Pin<&mut wcstring_list_ffi_t>, +) { + let arg_list_src = arg_list_src.as_wstr(); + let flags = ExpandFlags::from_bits(flags).unwrap(); + for c in Parser::expand_argument_list(arg_list_src, flags, ctx) { + out.as_mut().push(c.completion); + } +} + +impl ParserRefFFI { + fn deref(&self) -> &Parser { + let ptr = self.0.as_ref() as *const _; + unsafe { &*ptr } + } + fn vars(&self) -> &EnvStackRefFFI { + &self.0.variables_ffi + } +} diff --git a/fish-rust/src/path.rs b/fish-rust/src/path.rs index 81595f624..5ccb999f4 100644 --- a/fish-rust/src/path.rs +++ b/fish-rust/src/path.rs @@ -201,8 +201,8 @@ pub fn path_get_path(cmd: &wstr, vars: &dyn Environment) -> Option { /// If no candidate path is found, path will be empty and err will be set to ENOENT. /// Possible err values are taken from access(). pub struct GetPathResult { - err: Option, - path: WString, + pub err: Option, + pub path: WString, } impl GetPathResult { fn new(err: Option, path: WString) -> Self { @@ -218,16 +218,18 @@ pub fn path_try_get_path(cmd: &wstr, vars: &dyn Environment) -> GetPathResult { } } -fn path_is_executable(path: &wstr) -> bool { - let narrow = wcs2zstring(path); - if unsafe { libc::access(narrow.as_ptr(), X_OK) } != 0 { - return false; +fn path_check_executable(path: &wstr) -> Result<(), std::io::Error> { + if waccess(path, X_OK) != 0 { + return Err(std::io::Error::last_os_error()); + } + + let buff = wstat(path)?; + + if buff.file_type().is_file() { + Ok(()) + } else { + Err(ErrorKind::PermissionDenied.into()) } - let narrow: Vec = narrow.into(); - let Ok(md) = std::fs::metadata(OsStr::from_bytes(&narrow)) else { - return false; - }; - md.is_file() } /// Return all the paths that match the given command. @@ -237,7 +239,7 @@ pub fn path_get_paths(cmd: &wstr, vars: &dyn Environment) -> Vec { // If the command has a slash, it must be an absolute or relative path and thus we don't bother // looking for matching commands in the PATH var. - if cmd.contains('/') && path_is_executable(cmd) { + if cmd.contains('/') && path_check_executable(cmd).is_ok() { paths.push(cmd.to_owned()); return paths; } @@ -251,7 +253,7 @@ pub fn path_get_paths(cmd: &wstr, vars: &dyn Environment) -> Vec { } let mut path = path.clone(); append_path_component(&mut path, cmd); - if path_is_executable(&path) { + if path_check_executable(&path).is_ok() { paths.push(path); } } diff --git a/fish-rust/src/pointer.rs b/fish-rust/src/pointer.rs new file mode 100644 index 000000000..e9ada1634 --- /dev/null +++ b/fish-rust/src/pointer.rs @@ -0,0 +1,36 @@ +use std::ops::Deref; + +/// Raw pointer that implements Default. +/// Additionally it implements Deref so it's more ergonomic than Option. +#[derive(Debug)] +pub struct ConstPointer(pub *const T); + +impl From<&T> for ConstPointer { + fn from(value: &T) -> Self { + Self(value) + } +} + +impl Default for ConstPointer { + fn default() -> Self { + Self(std::ptr::null()) + } +} + +#[allow(clippy::incorrect_clone_impl_on_copy_type)] +impl Clone for ConstPointer { + fn clone(&self) -> Self { + Self(self.0) + } +} + +impl Copy for ConstPointer {} + +impl Deref for ConstPointer { + type Target = T; + + fn deref(&self) -> &Self::Target { + assert!(!self.0.is_null()); + unsafe { &*self.0 } + } +} diff --git a/fish-rust/src/proc.rs b/fish-rust/src/proc.rs new file mode 100644 index 000000000..1af166d32 --- /dev/null +++ b/fish-rust/src/proc.rs @@ -0,0 +1,1877 @@ +//! Utilities for keeping track of jobs, processes and subshells, as well as signal handling +//! functions for tracking children. These functions do not themselves launch new processes, +//! the exec library will call proc to create representations of the running jobs as needed. + +use crate::ast; +use crate::common::{ + charptr2wcstring, escape, fputws, redirect_tty_output, scoped_push_replacer, timef, Timepoint, +}; +use crate::compat::cur_term; +use crate::env::Statuses; +use crate::event::{self, Event}; +use crate::flog::{FLOG, FLOGF}; +use crate::global_safety::RelaxedAtomicBool; +use crate::io::IoChain; +use crate::job_group::{JobGroup, MaybeJobId}; +use crate::parse_tree::ParsedSourceRef; +use crate::parser::{Block, Parser}; +use crate::reader::{fish_is_unwinding_for_exit, reader_schedule_prompt_repaint}; +use crate::redirection::RedirectionSpecList; +use crate::signal::{signal_set_handlers_once, Signal}; +use crate::topic_monitor::{topic_monitor_principal, topic_t, GenerationsList}; +use crate::wait_handle::{InternalJobId, WaitHandle, WaitHandleRef, WaitHandleStore}; +use crate::wchar::{wstr, WString, L}; +use crate::wchar_ext::ToWString; +use crate::wutil::{perror, wbasename, wgettext, wperror}; +use libc::{ + EBADF, EINVAL, ENOTTY, EPERM, EXIT_SUCCESS, SIGABRT, SIGBUS, SIGCONT, SIGFPE, SIGHUP, SIGILL, + SIGINT, SIGPIPE, SIGQUIT, SIGSEGV, SIGSYS, SIGTTOU, SIG_DFL, SIG_IGN, STDIN_FILENO, + STDOUT_FILENO, WCONTINUED, WEXITSTATUS, WIFCONTINUED, WIFEXITED, WIFSIGNALED, WIFSTOPPED, + WNOHANG, WTERMSIG, WUNTRACED, _SC_CLK_TCK, +}; +use once_cell::sync::Lazy; +use printf_compat::sprintf; +use std::cell::{Cell, Ref, RefCell, RefMut}; +use std::fs; +use std::io::{Read, Write}; +use std::os::fd::RawFd; +use std::rc::Rc; +use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU64, AtomicU8, Ordering}; +use std::sync::{Arc, Mutex}; +use widestring_suffix::widestrs; + +/// Types of processes. +#[derive(Default, Eq, PartialEq)] +pub enum ProcessType { + /// A regular external command. + #[default] + external, + /// A builtin command. + builtin, + /// A shellscript function. + function, + /// A block of commands, represented as a node. + block_node, + /// The exec builtin. + exec, +} + +#[repr(u8)] +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum JobControl { + all, + interactive, + none, +} + +impl TryFrom<&wstr> for JobControl { + type Error = (); + + fn try_from(value: &wstr) -> Result { + if value == "full" { + Ok(JobControl::all) + } else if value == "interactive" { + Ok(JobControl::interactive) + } else if value == "none" { + Ok(JobControl::none) + } else { + Err(()) + } + } +} + +/// A number of clock ticks. +pub type ClockTicks = u64; + +/// \return clock ticks in seconds, or 0 on failure. +/// This uses sysconf(_SC_CLK_TCK) to convert to seconds. +pub fn clock_ticks_to_seconds(ticks: ClockTicks) -> f64 { + let clock_ticks_per_sec = unsafe { libc::sysconf(_SC_CLK_TCK) }; + if clock_ticks_per_sec > 0 { + return ticks as f64 / clock_ticks_per_sec as f64; + } + 0.0 +} + +pub type JobGroupRef = Arc; + +/// A proc_status_t is a value type that encapsulates logic around exited vs stopped vs signaled, +/// etc. +/// +/// It contains two fields packed into an AtomicU64 to allow interior mutability, `status: i32` and +/// `empty: bool`. +#[derive(Default)] +pub struct ProcStatus { + value: AtomicU64, +} + +impl Clone for ProcStatus { + fn clone(&self) -> Self { + Self { + value: AtomicU64::new(self.value.load(Ordering::Relaxed)), + } + } +} + +impl ProcStatus { + fn new(status: i32, empty: bool) -> Self { + ProcStatus { + value: Self::to_u64(status, empty).into(), + } + } + + /// Returns the raw `i32` status value. + fn status(&self) -> i32 { + Self::from_u64(self.value.load(Ordering::Relaxed)).0 + } + + /// Returns the `empty` field. + /// + /// If `empty` is `true` then there is no actual status to report (e.g. background or variable + /// assignment). + pub fn is_empty(&self) -> bool { + Self::from_u64(self.value.load(Ordering::Relaxed)).1 + } + + /// Replace the current `ProcStatus` with that of `other`. + pub fn update(&self, other: &ProcStatus) { + self.value + .store(other.value.load(Ordering::Relaxed), Ordering::Relaxed); + } + + fn set_status(&self, status: i32) { + let value = Self::to_u64(status, self.is_empty()); + self.value.store(value, Ordering::Relaxed); + } + + fn set_empty(&self, empty: bool) { + let value = Self::to_u64(self.status(), empty); + self.value.store(value, Ordering::Relaxed); + } + + fn to_u64(status: i32, empty: bool) -> u64 { + (u64::from(empty) << 32) | u64::from(status as u32) + } + + fn from_u64(bits: u64) -> (i32, bool) { + let status = bits as u32 as i32; + let empty = (bits >> 32) != 0; + (status, empty) + } + + /// Encode a return value \p ret and signal \p sig into a status value like waitpid() does. + const fn w_exitcode(ret: i32, sig: i32) -> i32 { + #[cfg(HAVE_WAITSTATUS_SIGNAL_RET)] + // It's encoded signal and then status + // The return status is in the lower byte. + return (sig << 8) | ret; + #[cfg(not(HAVE_WAITSTATUS_SIGNAL_RET))] + // The status is encoded in the upper byte. + // This should be W_EXITCODE(ret, sig) but that's not available everywhere. + return (ret << 8) | sig; + } + + /// Construct from a status returned from a waitpid call. + pub fn from_waitpid(status: i32) -> ProcStatus { + ProcStatus::new(status, false) + } + + /// Construct directly from an exit code. + pub fn from_exit_code(ret: i32) -> ProcStatus { + assert!( + ret >= 0, + "trying to create proc_status_t from failed waitid()/waitpid() call \ + or invalid builtin exit code!" + ); + + // Some paranoia. + const zerocode: i32 = ProcStatus::w_exitcode(0, 0); + const _: () = assert!( + WIFEXITED(zerocode), + "Synthetic exit status not reported as exited" + ); + + assert!(ret < 256); + ProcStatus::new(Self::w_exitcode(ret, 0 /* sig */), false) + } + + /// Construct directly from a signal. + pub fn from_signal(signal: Signal) -> ProcStatus { + ProcStatus::new(Self::w_exitcode(0 /* ret */, signal.code()), false) + } + + /// Construct an empty status_t (e.g. `set foo bar`). + pub fn empty() -> ProcStatus { + let empty = true; + ProcStatus::new(0, empty) + } + + /// \return if we are stopped (as in SIGSTOP). + pub fn stopped(&self) -> bool { + WIFSTOPPED(self.status()) + } + + /// \return if we are continued (as in SIGCONT). + pub fn continued(&self) -> bool { + WIFCONTINUED(self.status()) + } + + /// \return if we exited normally (not a signal). + pub fn normal_exited(&self) -> bool { + WIFEXITED(self.status()) + } + + /// \return if we exited because of a signal. + pub fn signal_exited(&self) -> bool { + WIFSIGNALED(self.status()) + } + + /// \return the signal code, given that we signal exited. + pub fn signal_code(&self) -> libc::c_int { + assert!(self.signal_exited(), "Process is not signal exited"); + WTERMSIG(self.status()) + } + + /// \return the exit code, given that we normal exited. + pub fn exit_code(&self) -> libc::c_int { + assert!(self.normal_exited(), "Process is not normal exited"); + WEXITSTATUS(self.status()) + } + + /// \return if this status represents success. + pub fn is_success(&self) -> bool { + self.normal_exited() && self.exit_code() == EXIT_SUCCESS + } + + /// \return the value appropriate to populate $status. + pub fn status_value(&self) -> i32 { + if self.signal_exited() { + 128 + self.signal_code() + } else if self.normal_exited() { + self.exit_code() + } else { + panic!("Process is not exited") + } + } +} + +/// A structure representing a "process" internal to fish. This is backed by a pthread instead of a +/// separate process. +pub struct InternalProc { + /// An identifier for internal processes. + /// This is used for logging purposes only. + internal_proc_id: u64, + + /// Whether the process has exited. + exited: AtomicBool, + + /// If the process has exited, its status code. + status: ProcStatus, +} + +impl InternalProc { + pub fn new() -> Self { + static NEXT_PROC_ID: AtomicU64 = AtomicU64::new(0); + Self { + internal_proc_id: NEXT_PROC_ID.fetch_add(1, Ordering::SeqCst), + exited: AtomicBool::new(false), + status: ProcStatus::default(), + } + } + + /// \return if this process has exited. + pub fn exited(&self) -> bool { + self.exited.load(Ordering::Acquire) + } + + /// Mark this process as having exited with the given `status`. + pub fn mark_exited(&self, status: &ProcStatus) { + assert!(!self.exited(), "Process is already exited"); + self.status.update(status); + self.exited.store(true, Ordering::Release); + topic_monitor_principal().post(topic_t::internal_exit); + FLOG!( + proc_internal_proc, + "Internal proc", + self.internal_proc_id, + "exited with status", + status.status_value() + ); + } + + pub fn get_status(&self) -> ProcStatus { + assert!(self.exited(), "Process is not exited"); + self.status.clone() + } + + pub fn get_id(&self) -> u64 { + self.internal_proc_id + } +} + +/// 0 should not be used; although it is not a valid PGID in userspace, +/// the Linux kernel will use it for kernel processes. +/// -1 should not be used; it is a possible return value of the getpgid() +/// function +pub const INVALID_PID: i32 = -2; + +// Allows transferring the tty to a job group, while it runs. +#[derive(Default)] +pub struct TtyTransfer { + // The job group which owns the tty, or empty if none. + owner: Option, +} + +impl TtyTransfer { + pub fn new() -> Self { + Default::default() + } + /// Transfer to the given job group, if it wants to own the terminal. + #[allow(clippy::wrong_self_convention)] + pub fn to_job_group(&mut self, jg: &JobGroupRef) { + assert!(self.owner.is_none(), "Terminal already transferred"); + if TtyTransfer::try_transfer(jg) { + self.owner = Some(jg.clone()); + } + } + + /// Reclaim the tty if we transferred it. + pub fn reclaim(&mut self) { + if self.owner.is_some() { + FLOG!(proc_pgroup, "fish reclaiming terminal"); + if unsafe { libc::tcsetpgrp(STDIN_FILENO, libc::getpgrp()) } == -1 { + FLOG!(warning, "Could not return shell to foreground"); + perror("tcsetpgrp"); + } + self.owner = None; + } + } + + /// Save the current tty modes into the owning job group, if we are transferred. + pub fn save_tty_modes(&mut self) { + if let Some(ref mut owner) = self.owner { + let mut tmodes: libc::termios = unsafe { std::mem::zeroed() }; + if unsafe { libc::tcgetattr(STDIN_FILENO, &mut tmodes) } == 0 { + owner.tmodes.replace(Some(tmodes)); + } else if errno::errno().0 != ENOTTY { + perror("tcgetattr"); + } + } + } + + fn try_transfer(jg: &JobGroup) -> bool { + if !jg.wants_terminal() { + // The job doesn't want the terminal. + return false; + } + + // Get the pgid; we must have one if we want the terminal. + let pgid = jg.get_pgid().unwrap(); + assert!(pgid >= 0, "Invalid pgid"); + + // It should never be fish's pgroup. + let fish_pgrp = unsafe { libc::getpgrp() }; + assert!(pgid != fish_pgrp, "Job should not have fish's pgroup"); + + // Ok, we want to transfer to the child. + // Note it is important to be very careful about calling tcsetpgrp()! + // fish ignores SIGTTOU which means that it has the power to reassign the tty even if it doesn't + // own it. This means that other processes may get SIGTTOU and become zombies. + // Check who own the tty now. There's four cases of interest: + // 1. There is no tty at all (tcgetpgrp() returns -1). For example running from a pure script. + // Of course do not transfer it in that case. + // 2. The tty is owned by the process. This comes about often, as the process will call + // tcsetpgrp() on itself between fork ane exec. This is the essential race inherent in + // tcsetpgrp(). In this case we want to reclaim the tty, but do not need to transfer it + // ourselves since the child won the race. + // 3. The tty is owned by a different process. This may come about if fish is running in the + // background with job control enabled. Do not transfer it. + // 4. The tty is owned by fish. In that case we want to transfer the pgid. + let current_owner = unsafe { libc::tcgetpgrp(STDIN_FILENO) }; + if current_owner < 0 { + // Case 1. + return false; + } else if current_owner == pgid { + // Case 2. + return true; + } else if current_owner != pgid && current_owner != fish_pgrp { + // Case 3. + return false; + } + // Case 4 - we do want to transfer it. + + // The tcsetpgrp(2) man page says that EPERM is thrown if "pgrp has a supported value, but + // is not the process group ID of a process in the same session as the calling process." + // Since we _guarantee_ that this isn't the case (the child calls setpgid before it calls + // SIGSTOP, and the child was created in the same session as us), it seems that EPERM is + // being thrown because of an caching issue - the call to tcsetpgrp isn't seeing the + // newly-created process group just yet. On this developer's test machine (WSL running Linux + // 4.4.0), EPERM does indeed disappear on retry. The important thing is that we can + // guarantee the process isn't going to exit while we wait (which would cause us to possibly + // block indefinitely). + while unsafe { libc::tcsetpgrp(STDIN_FILENO, pgid) } != 0 { + FLOGF!(proc_termowner, "tcsetpgrp failed: %d", errno::errno().0); + + // Before anything else, make sure that it's even necessary to call tcsetpgrp. + // Since it usually _is_ necessary, we only check in case it fails so as to avoid the + // unnecessary syscall and associated context switch, which profiling has shown to have + // a significant cost when running process groups in quick succession. + let getpgrp_res = unsafe { libc::tcgetpgrp(STDIN_FILENO) }; + if getpgrp_res < 0 { + match errno::errno().0 { + ENOTTY => { + // stdin is not a tty. This may come about if job control is enabled but we are + // not a tty - see #6573. + return false; + } + EBADF => { + // stdin has been closed. Workaround a glibc bug - see #3644. + redirect_tty_output(); + return false; + } + _ => { + perror("tcgetpgrp"); + return false; + } + } + } + if getpgrp_res == pgid { + FLOGF!( + proc_termowner, + "Process group %d already has control of terminal", + pgid + ); + return true; + } + + let pgroup_terminated; + if errno::errno().0 == EINVAL { + // OS X returns EINVAL if the process group no longer lives. Probably other OSes, + // too. Unlike EPERM below, EINVAL can only happen if the process group has + // terminated. + pgroup_terminated = true; + } else if errno::errno().0 == EPERM { + // Retry so long as this isn't because the process group is dead. + let mut result: libc::c_int = 0; + let wait_result = unsafe { libc::waitpid(-pgid, &mut result, WNOHANG) }; + if wait_result == -1 { + // Note that -1 is technically an "error" for waitpid in the sense that an + // invalid argument was specified because no such process group exists any + // longer. This is the observed behavior on Linux 4.4.0. a "success" result + // would mean processes from the group still exist but is still running in some + // state or the other. + pgroup_terminated = true; + } else { + // Debug the original tcsetpgrp error (not the waitpid errno) to the log, and + // then retry until not EPERM or the process group has exited. + FLOGF!( + proc_termowner, + "terminal_give_to_job(): EPERM with pgid %d.", + pgid + ); + continue; + } + } else if errno::errno().0 == ENOTTY { + // stdin is not a TTY. In general we expect this to be caught via the tcgetpgrp + // call's EBADF handler above. + return false; + } else { + FLOGF!( + warning, + "Could not send job %d ('%ls') with pgid %d to foreground", + jg.job_id.to_wstring(), + jg.command, + pgid + ); + perror("tcsetpgrp"); + return false; + } + + if pgroup_terminated { + // All processes in the process group has exited. + // Since we delay reaping any processes in a process group until all members of that + // job/group have been started, the only way this can happen is if the very last + // process in the group terminated and didn't need to access the terminal, otherwise + // it would have hung waiting for terminal IO (SIGTTIN). We can safely ignore this. + FLOGF!( + proc_termowner, + "tcsetpgrp called but process group %d has terminated.\n", + pgid + ); + return false; + } + + break; + } + true + } +} + +/// The destructor will assert if reclaim() has not been called. +impl Drop for TtyTransfer { + fn drop(&mut self) { + assert!(self.owner.is_none(), "Forgot to reclaim() the tty"); + } +} + +/// A structure representing a single fish process. Contains variables for tracking process state +/// and the process argument list. Actually, a fish process can be either a regular external +/// process, an internal builtin which may or may not spawn a fake IO process during execution, a +/// shellscript function or a block of commands to be evaluated by calling eval. Lastly, this +/// process can be the result of an exec command. The role of this process_t is determined by the +/// type field, which can be one of process_type_t::external, process_type_t::builtin, +/// process_type_t::function, process_type_t::exec. +/// +/// The process_t contains information on how the process should be started, such as command name +/// and arguments, as well as runtime information on the status of the actual physical process which +/// represents it. Shellscript functions, builtins and blocks of code may all need to spawn an +/// external process that handles the piping and redirecting of IO for them. +/// +/// If the process is of type process_type_t::external or process_type_t::exec, argv is the argument +/// array and actual_cmd is the absolute path of the command to execute. +/// +/// If the process is of type process_type_t::builtin, argv is the argument vector, and argv[0] is +/// the name of the builtin command. +/// +/// If the process is of type process_type_t::function, argv is the argument vector, and argv[0] is +/// the name of the shellscript function. +#[derive(Default)] +pub struct Process { + /// Note whether we are the first and/or last in the job + pub is_first_in_job: bool, + pub is_last_in_job: bool, + + /// Type of process. + pub typ: ProcessType, + + /// For internal block processes only, the node of the statement. + /// This is always either block, ifs, or switchs, never boolean or decorated. + pub block_node_source: Option, + pub internal_block_node: Option>, + + /// The expanded variable assignments for this process, as specified by the `a=b cmd` syntax. + pub variable_assignments: Vec, + + /// Actual command to pass to exec in case of process_type_t::external or process_type_t::exec. + pub actual_cmd: WString, + + /// Generation counts for reaping. + pub gens: GenerationsList, + + /// Process ID, represented as an AtomicI32. This is actually an Option with a + /// value of zero representing `None`. + pub pid: AtomicI32, + + /// If we are an "internal process," that process. + pub internal_proc: RefCell>>, + + /// File descriptor that pipe output should bind to. + pub pipe_write_fd: RawFd, + + /// True if process has completed. + pub completed: RelaxedAtomicBool, + + /// True if process has stopped. + pub stopped: RelaxedAtomicBool, + + /// If set, this process is (or will become) the pgroup leader. + /// This is only meaningful for external processes. + pub leads_pgrp: bool, + + /// Whether we have generated a proc_exit event. + pub posted_proc_exit: RelaxedAtomicBool, + + /// Reported status value. + pub status: ProcStatus, + + pub last_times: Cell, + + argv: Vec, + proc_redirection_specs: RedirectionSpecList, + + // The wait handle. This is constructed lazily, and cached. + // This may be null. + wait_handle: RefCell>, +} + +#[derive(Default, Clone, Copy)] +pub struct ProcTimes { + /// Last time of cpu time check, in seconds (per timef). + pub time: Timepoint, + /// Number of jiffies spent in process at last cpu time check. + pub jiffies: ClockTicks, +} + +pub struct ConcreteAssignment { + pub variable_name: WString, + pub values: Vec, +} + +impl ConcreteAssignment { + pub fn new(variable_name: WString, values: Vec) -> Self { + Self { + variable_name, + values, + } + } +} + +impl Process { + pub fn new() -> Self { + Default::default() + } + + /// Retrieves the associated [`libc::pid_t`] or panics if no pid has been set (not yet set or + /// process does not get a pid). + /// + /// See [`Process::has_pid()]` to safely check if the process has a pid. + pub fn pid(&self) -> libc::pid_t { + let value = self.pid.load(Ordering::Relaxed); + assert!(value != 0, "Process::pid() called but pid not set!"); + #[allow(clippy::useless_conversion)] + value.into() + } + + pub fn has_pid(&self) -> bool { + let value = self.pid.load(Ordering::Relaxed); + value != 0 + } + + /// Sets the process' pid. Panics if a pid has already been set. + pub fn set_pid(&self, pid: libc::pid_t) { + assert!(pid != 0, "Invalid pid of 0 passed to Process::set_pid()"); + assert!(pid >= 0); + let old = self.pid.swap(pid, Ordering::Relaxed); + assert!(old == 0, "Process::set_pid() called more than once!"); + } + + /// Sets argv. + pub fn set_argv(&mut self, argv: Vec) { + self.argv = argv; + } + + /// Returns argv. + pub fn argv(&self) -> &Vec { + &self.argv + } + + /// Returns argv[0]. + pub fn argv0(&self) -> Option<&wstr> { + self.argv.get(0).map(|s| s.as_utfstr()) + } + + /// Redirection list getter and setter. + pub fn redirection_specs(&self) -> &RedirectionSpecList { + &self.proc_redirection_specs + } + pub fn redirection_specs_mut(&mut self) -> &mut RedirectionSpecList { + &mut self.proc_redirection_specs + } + pub fn set_redirection_specs(&mut self, specs: RedirectionSpecList) { + self.proc_redirection_specs = specs; + } + + /// Store the current topic generations. That is, right before the process is launched, record + /// the generations of all topics; then we can tell which generation values have changed after + /// launch. This helps us avoid spurious waitpid calls. + pub fn check_generations_before_launch(&self) { + self.gens + .update(&topic_monitor_principal().current_generations()); + } + + /// Mark that this process was part of a pipeline which was aborted. + /// The process was never successfully launched; give it a status of EXIT_FAILURE. + pub fn mark_aborted_before_launch(&self) { + self.completed.store(true); + // The status may have already been set to e.g. STATUS_NOT_EXECUTABLE. + // Only stomp a successful status. + if self.status.is_success() { + self.status + .set_status(ProcStatus::from_exit_code(libc::EXIT_FAILURE).status()) + } + } + + /// \return whether this process type is internal (block, function, or builtin). + pub fn is_internal(&self) -> bool { + match self.typ { + ProcessType::builtin | ProcessType::function | ProcessType::block_node => true, + ProcessType::external | ProcessType::exec => false, + } + } + + /// \return the wait handle for the process, if it exists. + pub fn get_wait_handle(&self) -> Option { + self.wait_handle.borrow().clone() + } + + pub fn is_stopped(&self) -> bool { + self.stopped.load() + } + + pub fn is_completed(&self) -> bool { + self.completed.load() + } + + /// Create a wait handle for the process. + /// As a process does not know its job id, we pass it in. + /// Note this will return null if the process is not waitable (has no pid). + pub fn make_wait_handle(&self, jid: InternalJobId) -> Option { + if self.typ != ProcessType::external || self.pid.load(Ordering::Relaxed) <= 0 { + // Not waitable. + None + } else { + if self.wait_handle.borrow().is_none() { + self.wait_handle.replace(Some(WaitHandle::new( + self.pid(), + jid, + wbasename(&self.actual_cmd.clone()).to_owned(), + ))); + } + self.get_wait_handle() + } + } +} + +pub type ProcessPtr = Box; +pub type ProcessList = Vec; + +/// A set of jobs properties. These are immutable: they do not change for the lifetime of the +/// job. +#[derive(Default, Clone, Copy)] +pub struct JobProperties { + /// Whether the specified job is a part of a subshell, event handler or some other form of + /// special job that should not be reported. + pub skip_notification: bool, + + /// Whether the job had the background ampersand when constructed, e.g. /bin/echo foo & + /// Note that a job may move between foreground and background; this just describes what the + /// initial state should be. + pub initial_background: bool, + + /// Whether the job has the 'time' prefix and so we should print timing for this job. + pub wants_timing: bool, + + /// Whether this job was created as part of an event handler. + pub from_event_handler: bool, +} + +/// Flags associated with the job. +#[derive(Default)] +pub struct JobFlags { + /// Whether the specified job is completely constructed: every process in the job has been + /// forked, etc. + pub constructed: bool, + + /// Whether the user has been notified that this job is stopped (if it is). + pub notified_of_stop: bool, + + /// Whether the exit status should be negated. This flag can only be set by the not builtin. + /// Two "not" prefixes on a single job cancel each other out. + pub negate: bool, + + /// This job is disowned, and should be removed from the active jobs list. + pub disown_requested: bool, + + // Indicates that we are the "group root." Any other jobs using this tree are nested. + pub is_group_root: bool, +} + +/// A struct representing a job. A job is a pipeline of one or more processes. +#[derive(Default)] +pub struct Job { + /// Set of immutable job properties. + properties: JobProperties, + + /// The original command which led to the creation of this job. It is used for displaying + /// messages about job status on the terminal. + command_str: WString, + + /// All the processes in this job. + pub processes: ProcessList, + + // The group containing this job. + // This is never cleared. + pub group: Option, + + /// A non-user-visible, never-recycled job ID. + pub internal_job_id: InternalJobId, + + /// Flags associated with the job. + pub job_flags: RefCell, +} + +impl Job { + pub fn new(properties: JobProperties, command_str: WString) -> Self { + static NEXT_INTERNAL_JOB_ID: AtomicU64 = AtomicU64::new(0); + Job { + properties, + command_str, + internal_job_id: NEXT_INTERNAL_JOB_ID.fetch_add(1, Ordering::Relaxed), + ..Default::default() + } + } + + pub fn group(&self) -> &JobGroup { + self.group.as_ref().unwrap() + } + + /// Returns the command. + pub fn command(&self) -> &wstr { + &self.command_str + } + + /// Borrow the job's process list. Only read-only or interior mutability actions may be + /// performed on the processes in the list. + pub fn processes(&self) -> &ProcessList { + &self.processes + } + + /// Get mutable access to the job's process list. + /// Only available with a mutable reference `&mut Job`. + pub fn processes_mut(&mut self) -> &mut ProcessList { + &mut self.processes + } + + /// \return whether it is OK to reap a given process. Sometimes we want to defer reaping a + /// process if it is the group leader and the job is not yet constructed, because then we might + /// also reap the process group and then we cannot add new processes to the group. + pub fn can_reap(&self, p: &ProcessPtr) -> bool { + !( + // Can't reap twice. + p.is_completed() || + // Can't reap the group leader in an under-construction job. + (p.has_pid() && !self.is_constructed() && self.get_pgid() == Some(p.pid())) + ) + } + + /// Returns a truncated version of the job string. Used when a message has already been emitted + /// containing the full job string and job id, but using the job id alone would be confusing + /// due to reuse of freed job ids. Prevents overloading the debug comments with the full, + /// untruncated job string when we don't care what the job is, only which of the currently + /// running jobs it is. + #[widestrs] + pub fn preview(&self) -> WString { + if self.processes().is_empty() { + return ""L.to_owned(); + } + // Note argv0 may be empty in e.g. a block process. + let procs = self.processes(); + let result = procs.first().unwrap().argv0().unwrap_or("null"L); + result.to_owned() + "..."L + } + + /// \return our pgid, or none if we don't have one, or are internal to fish + /// This never returns fish's own pgroup. + pub fn get_pgid(&self) -> Option { + self.group().get_pgid() + } + + /// \return the pid of the last external process in the job. + /// This may be none if the job consists of just internal fish functions or builtins. + /// This will never be fish's own pid. + pub fn get_last_pid(&self) -> Option { + self.processes() + .iter() + .rev() + .find(|proc| proc.has_pid()) + .map(|proc| proc.pid()) + } + + /// The id of this job. + /// This is user-visible, is recycled, and may be -1. + pub fn job_id(&self) -> MaybeJobId { + self.group().job_id + } + + /// Access the job flags. + pub fn flags(&self) -> Ref { + self.job_flags.borrow() + } + + /// Access mutable job flags. + pub fn mut_flags(&self) -> RefMut { + self.job_flags.borrow_mut() + } + + // \return whether we should print timing information. + pub fn wants_timing(&self) -> bool { + self.properties.wants_timing + } + + /// \return if we want job control. + pub fn wants_job_control(&self) -> bool { + self.group().wants_job_control() + } + + /// \return whether this job is initially going to run in the background, because & was + /// specified. + pub fn is_initially_background(&self) -> bool { + self.properties.initial_background + } + + /// Mark this job as constructed. The job must not have previously been marked as constructed. + pub fn mark_constructed(&self) { + assert!(!self.is_constructed(), "Job was already constructed"); + self.mut_flags().constructed = true; + } + + /// \return whether we have internal or external procs, respectively. + /// Internal procs are builtins, blocks, and functions. + /// External procs include exec and external. + pub fn has_external_proc(&self) -> bool { + self.processes().iter().any(|p| !p.is_internal()) + } + + /// \return whether this job, when run, will want a job ID. + /// Jobs that are only a single internal block do not get a job ID. + pub fn wants_job_id(&self) -> bool { + self.processes().len() > 1 + || !self.processes()[0].is_internal() + || self.is_initially_background() + } + + // Helper functions to check presence of flags on instances of jobs + /// The job has been fully constructed, i.e. all its member processes have been launched + pub fn is_constructed(&self) -> bool { + self.flags().constructed + } + /// The job is complete, i.e. all its member processes have been reaped + /// Return true if all processes in the job have completed. + pub fn is_completed(&self) -> bool { + assert!(!self.processes().is_empty()); + self.processes().iter().all(|p| p.is_completed()) + } + /// The job is in a stopped state + /// Return true if all processes in the job are stopped or completed, and there is at least one + /// stopped process. + pub fn is_stopped(&self) -> bool { + let mut has_stopped = false; + for p in self.processes().iter() { + if !p.is_completed() && !p.is_stopped() { + return false; + } + has_stopped |= p.is_stopped(); + } + has_stopped + } + /// The job is OK to be externally visible, e.g. to the user via `jobs` + pub fn is_visible(&self) -> bool { + !self.is_completed() && self.is_constructed() && !self.flags().disown_requested + } + pub fn skip_notification(&self) -> bool { + self.properties.skip_notification + } + #[allow(clippy::wrong_self_convention)] + pub fn from_event_handler(&self) -> bool { + self.properties.from_event_handler + } + + /// \return whether this job's group is in the foreground. + pub fn is_foreground(&self) -> bool { + self.group().is_foreground() + } + + /// \return whether we should post job_exit events. + pub fn posts_job_exit_events(&self) -> bool { + // Only report root job exits. + // For example in `ls | begin ; cat ; end` we don't need to report the cat sub-job. + if !self.flags().is_group_root { + return false; + } + + // Only jobs with external processes post job_exit events. + self.has_external_proc() + } + + /// Run ourselves. Returning once we complete or stop. + pub fn continue_job(&self, parser: &Parser) { + FLOGF!( + proc_job_run, + "Run job %d (%ls), %ls, %ls", + self.job_id(), + self.command(), + if self.is_completed() { + "COMPLETED" + } else { + "UNCOMPLETED" + }, + if parser.libdata().pods.is_interactive { + "INTERACTIVE" + } else { + "NON-INTERACTIVE" + } + ); + + // Wait for the status of our own job to change. + while !fish_is_unwinding_for_exit() && !self.is_stopped() && !self.is_completed() { + process_mark_finished_children(parser, true); + } + if self.is_completed() { + // Set $status only if we are in the foreground and the last process in the job has + // finished. + let procs = self.processes(); + let p = procs.last().unwrap(); + if p.status.normal_exited() || p.status.signal_exited() { + if let Some(statuses) = self.get_statuses() { + parser.set_last_statuses(statuses); + parser.libdata_mut().pods.status_count += 1; + } + } + } + } + + /// Prepare to resume a stopped job by sending SIGCONT and clearing the stopped flag. + /// \return true on success, false if we failed to send the signal. + pub fn resume(&self) -> bool { + self.mut_flags().notified_of_stop = false; + if !self.signal(SIGCONT) { + FLOGF!( + proc_pgroup, + "Failed to send SIGCONT to procs in job %ls", + self.command() + ); + return false; + } + + // Reset the status of each process instance + for p in self.processes.iter() { + p.stopped.store(false); + } + true + } + + /// Send the specified signal to all processes in this job. + /// \return true on success, false on failure. + pub fn signal(&self, signal: i32) -> bool { + if let Some(pgid) = self.group().get_pgid() { + if unsafe { libc::killpg(pgid, signal) } == -1 { + let strsignal = unsafe { libc::strsignal(signal) }; + let strsignal = if strsignal.is_null() { + L!("(nil)").to_owned() + } else { + charptr2wcstring(strsignal) + }; + wperror(&sprintf!("killpg(%d, %s)", pgid, strsignal)); + return false; + } + } else { + // This job lives in fish's pgroup and we need to signal procs individually. + for p in self.processes().iter() { + if !p.is_completed() && p.has_pid() && unsafe { libc::kill(p.pid(), signal) } == -1 + { + return false; + } + } + } + true + } + + /// \returns the statuses for this job. + pub fn get_statuses(&self) -> Option { + let mut st = Statuses::default(); + let mut has_status = false; + let mut laststatus = 0; + st.pipestatus.resize(self.processes().len(), 0); + for (i, p) in self.processes().iter().enumerate() { + let status = &p.status; + if status.is_empty() { + // Corner case for if a variable assignment is part of a pipeline. + // e.g. `false | set foo bar | true` will push 1 in the second spot, + // for a complete pipestatus of `1 1 0`. + st.pipestatus[i] = laststatus; + continue; + } + if status.signal_exited() { + st.kill_signal = Some(Signal::new(status.signal_code())); + } + laststatus = status.status_value(); + has_status = true; + st.pipestatus[i] = status.status_value(); + } + if !has_status { + return None; + } + st.status = if self.flags().negate { + if laststatus == 0 { + 1 + } else { + 0 + } + } else { + laststatus + }; + Some(st) + } +} + +pub type JobRef = Rc; + +/// Whether this shell is attached to a tty. +pub fn is_interactive_session() -> bool { + IS_INTERACTIVE_SESSION.load() +} +pub fn set_interactive_session(flag: bool) { + IS_INTERACTIVE_SESSION.store(flag) +} +static IS_INTERACTIVE_SESSION: RelaxedAtomicBool = RelaxedAtomicBool::new(false); + +/// Whether we are a login shell. +pub fn get_login() -> bool { + IS_LOGIN.load() +} +pub fn mark_login() { + IS_LOGIN.store(true) +} +static IS_LOGIN: RelaxedAtomicBool = RelaxedAtomicBool::new(false); + +/// If this flag is set, fish will never fork or run execve. It is used to put fish into a syntax +/// verifier mode where fish tries to validate the syntax of a file but doesn't actually do +/// anything. +pub fn no_exec() -> bool { + IS_NO_EXEC.load() +} +pub fn mark_no_exec() { + IS_NO_EXEC.store(true) +} +static IS_NO_EXEC: RelaxedAtomicBool = RelaxedAtomicBool::new(false); + +// List of jobs. +pub type JobList = Vec; + +/// The current job control mode. +/// +/// Must be one of job_control_t::all, job_control_t::interactive and job_control_t::none. +pub fn get_job_control_mode() -> JobControl { + unsafe { std::mem::transmute(JOB_CONTROL_MODE.load(Ordering::Relaxed)) } +} +pub fn set_job_control_mode(mode: JobControl) { + JOB_CONTROL_MODE.store(mode as u8, Ordering::Relaxed); + + // HACK: when fish (or any shell) launches a job with job control, it will put the job into its + // own pgroup and call tcsetpgrp() to allow that pgroup to own the terminal (making fish a + // background process). When the job finishes, fish will try to reclaim the terminal via + // tcsetpgrp(), but as fish is now a background process it will receive SIGTTOU and stop! Ensure + // that doesn't happen by ignoring SIGTTOU. + // Note that if we become interactive, we also ignore SIGTTOU. + if mode == JobControl::all { + unsafe { + libc::signal(SIGTTOU, SIG_IGN); + } + } +} +static JOB_CONTROL_MODE: AtomicU8 = AtomicU8::new(JobControl::interactive as u8); + +/// Notify the user about stopped or terminated jobs, and delete completed jobs from the job list. +/// If \p interactive is set, allow removing interactive jobs; otherwise skip them. +/// \return whether text was printed to stdout. +pub fn job_reap(parser: &Parser, allow_interactive: bool) -> bool { + parser.assert_can_execute(); + + // Early out for the common case that there are no jobs. + if parser.jobs().is_empty() { + return false; + } + + process_mark_finished_children(parser, false /* not block_ok */); + process_clean_after_marking(parser, allow_interactive) +} + +/// \return the list of background jobs which we should warn the user about, if the user attempts to +/// exit. An empty result (common) means no such jobs. +pub fn jobs_requiring_warning_on_exit(parser: &Parser) -> JobList { + let mut result = vec![]; + for job in parser.jobs().iter() { + if !job.is_foreground() && job.is_constructed() && !job.is_completed() { + result.push(job.clone()); + } + } + result +} + +/// Print the exit warning for the given jobs, which should have been obtained via +/// jobs_requiring_warning_on_exit(). +#[widestrs] +pub fn print_exit_warning_for_jobs(jobs: &JobList) { + fputws(wgettext!("There are still jobs active:\n"), STDOUT_FILENO); + fputws(wgettext!("\n PID Command\n"), STDOUT_FILENO); + for j in jobs { + fwprintf!( + STDOUT_FILENO, + "%6d %ls\n", + j.processes()[0].pid(), + j.command() + ); + } + fputws("\n"L, STDOUT_FILENO); + fputws( + wgettext!("A second attempt to exit will terminate them.\n"), + STDOUT_FILENO, + ); + fputws( + wgettext!("Use 'disown PID' to remove jobs from the list without terminating them.\n"), + STDOUT_FILENO, + ); + reader_schedule_prompt_repaint(); +} + +/// Use the procfs filesystem to look up how many jiffies of cpu time was used by a given pid. This +/// function is only available on systems with the procfs file entry 'stat', i.e. Linux. +pub fn proc_get_jiffies(inpid: libc::pid_t) -> ClockTicks { + if inpid <= 0 || !have_proc_stat() { + return 0; + } + + let filename = format!("/proc/{}/stat", inpid); + let Ok(mut f) = fs::File::open(filename) else { + return 0; + }; + + let mut buf = vec![]; + if f.read_to_end(&mut buf).is_err() { + return 0; + } + + let mut timesstrs = buf.split(|c| *c == b' ').skip(13); + let mut sum = 0; + for _ in 0..4 { + let Some(timestr) = timesstrs.next() else { + return 0; + }; + let Ok(timestr) = std::str::from_utf8(timestr) else { + return 0; + }; + let Ok(time) = str::parse::(timestr) else { + return 0; + }; + sum += time; + } + sum +} + +/// Update process time usage for all processes by calling the proc_get_jiffies function for every +/// process of every job. +pub fn proc_update_jiffies(parser: &Parser) { + for job in parser.jobs().iter() { + for p in job.processes.iter() { + p.last_times.replace(ProcTimes { + time: timef(), + jiffies: proc_get_jiffies(p.pid.load(Ordering::Relaxed)), + }); + } + } +} + +/// Initializations. +pub fn proc_init() { + signal_set_handlers_once(false); +} + +/// Set the status of \p proc to \p status. +fn handle_child_status(job: &Job, proc: &Process, status: &ProcStatus) { + proc.status.update(status); + if status.stopped() { + proc.stopped.store(true); + } else if status.continued() { + proc.stopped.store(false); + } else { + proc.completed.store(true); + } + + // If the child was killed by SIGINT or SIGQUIT, then cancel the entire group if interactive. If + // not interactive, we have historically re-sent the signal to ourselves; however don't do that + // if the signal is trapped (#6649). + // Note the asymmetry: if the fish process gets SIGINT we will run SIGINT handlers. If a child + // process gets SIGINT we do not run SIGINT handlers; we just don't exit. This should be + // rationalized. + if status.signal_exited() { + let sig = status.signal_code(); + if [SIGINT, SIGQUIT].contains(&sig) { + if is_interactive_session() { + // Mark the job group as cancelled. + job.group().cancel_with_signal(Signal::new(sig)); + } else if !event::is_signal_observed(sig) { + // Deliver the SIGINT or SIGQUIT signal to ourself since we're not interactive. + let mut act: libc::sigaction = unsafe { std::mem::zeroed() }; + unsafe { libc::sigemptyset(&mut act.sa_mask) }; + act.sa_flags = 0; + act.sa_sigaction = SIG_DFL; + unsafe { + libc::sigaction(sig, &act, std::ptr::null_mut()); + libc::kill(libc::getpid(), sig); + } + } + } + } +} + +/// Wait for any process finishing, or receipt of a signal. +pub fn proc_wait_any(parser: &Parser) { + process_mark_finished_children(parser, true /*block_ok*/); + let is_interactive = parser.libdata().pods.is_interactive; + process_clean_after_marking(parser, is_interactive); +} + +/// Send SIGHUP to the list \p jobs, excepting those which are in fish's pgroup. +pub fn hup_jobs(jobs: &JobList) { + let fish_pgrp = unsafe { libc::getpgrp() }; + for j in jobs { + let Some(pgid) = j.get_pgid() else { continue }; + if pgid != fish_pgrp && !j.is_completed() { + if j.is_stopped() { + j.signal(SIGCONT); + } + j.signal(SIGHUP); + } + } +} + +/// Add a job to the list of PIDs/PGIDs we wait on even though they are not associated with any +/// jobs. Used to avoid zombie processes after disown. +pub fn add_disowned_job(j: &Job) { + let mut disowned_pids = unsafe { DISOWNED_PIDS.lock().unwrap() }; + for process in j.processes().iter() { + if process.has_pid() { + disowned_pids.push(process.pid()); + } + } +} + +// Reap any pids in our disowned list that have exited. This is used to avoid zombies. +fn reap_disowned_pids() { + let mut disowned_pids = unsafe { DISOWNED_PIDS.lock().unwrap() }; + // waitpid returns 0 iff the PID/PGID in question has not changed state; remove the pid/pgid + // if it has changed or an error occurs (presumably ECHILD because the child does not exist) + disowned_pids.retain(|pid| { + let mut status: libc::c_int = 0; + let ret = unsafe { libc::waitpid(*pid, &mut status, WNOHANG) }; + if ret > 0 { + FLOGF!(proc_reap_external, "Reaped disowned PID or PGID %d", pid); + } + ret == 0 + }); +} + +/// A list of pids that have been disowned. They are kept around until either they exit or +/// we exit. Poll these from time-to-time to prevent zombie processes from happening (#5342). +static mut DISOWNED_PIDS: Mutex> = Mutex::new(vec![]); + +/// See if any reapable processes have exited, and mark them accordingly. +/// \param block_ok if no reapable processes have exited, block until one is (or until we receive a +/// signal). +fn process_mark_finished_children(parser: &Parser, block_ok: bool) { + parser.assert_can_execute(); + + // Get the exit and signal generations of all reapable processes. + // The exit generation tells us if we have an exit; the signal generation allows for detecting + // SIGHUP and SIGINT. + // Go through each process and figure out if and how it wants to be reaped. + let mut reapgens = GenerationsList::invalid(); + for j in parser.jobs().iter() { + for proc in j.processes().iter() { + if !j.can_reap(proc) { + continue; + } + + if proc.has_pid() { + // Reaps with a pid. + reapgens.set_min_from(topic_t::sigchld, &proc.gens); + reapgens.set_min_from(topic_t::sighupint, &proc.gens); + } + if proc.internal_proc.borrow().is_some() { + // Reaps with an internal process. + reapgens.set_min_from(topic_t::internal_exit, &proc.gens); + reapgens.set_min_from(topic_t::sighupint, &proc.gens); + } + } + } + + // Now check for changes, optionally waiting. + if !topic_monitor_principal().check(&reapgens, block_ok) { + // Nothing changed. + return; + } + + // We got some changes. Since we last checked we received SIGCHLD, and or HUP/INT. + // Update the hup/int generations and reap any reapable processes. + // We structure this as two loops for some simplicity. + // First reap all pids. + for j in parser.jobs().iter() { + for proc in j.processes.iter() { + // Does this proc have a pid that is reapable? + if proc.pid.load(Ordering::Relaxed) <= 0 || !j.can_reap(proc) { + continue; + } + + // Always update the signal hup/int gen. + proc.gens.sighupint.set(reapgens.sighupint.get()); + + // Nothing to do if we did not get a new sigchld. + if proc.gens.sigchld == reapgens.sigchld { + continue; + } + proc.gens.sigchld.set(reapgens.sigchld.get()); + + // Ok, we are reapable. Run waitpid()! + let mut statusv: libc::c_int = -1; + let pid = unsafe { + libc::waitpid(proc.pid(), &mut statusv, WNOHANG | WUNTRACED | WCONTINUED) + }; + assert!(pid <= 0 || pid == proc.pid(), "Unexpcted waitpid() return"); + if pid <= 0 { + continue; + } + + // The process has stopped or exited! Update its status. + let status = ProcStatus::from_waitpid(statusv); + handle_child_status(j, proc, &status); + if status.stopped() { + j.group().set_is_foreground(false); + } + if status.continued() { + j.mut_flags().notified_of_stop = false; + } + if status.normal_exited() || status.signal_exited() { + FLOGF!( + proc_reap_external, + "Reaped external process '%ls' (pid %d, status %d)", + proc.argv0().unwrap(), + pid, + proc.status.status_value() + ); + } else { + assert!(status.stopped() || status.continued()); + FLOGF!( + proc_reap_external, + "External process '%ls' (pid %d, %s)", + proc.argv0().unwrap(), + proc.pid(), + if proc.status.stopped() { + "stopped" + } else { + "continued" + } + ); + } + } + } + + // We are done reaping pids. + // Reap internal processes. + for j in parser.jobs().iter() { + for proc in j.processes.iter() { + // Does this proc have an internal process that is reapable? + if proc.internal_proc.borrow().is_none() || !j.can_reap(proc) { + continue; + } + + // Always update the signal hup/int gen. + proc.gens.sighupint.set(reapgens.sighupint.get()); + + // Nothing to do if we did not get a new internal exit. + if proc.gens.internal_exit == reapgens.internal_exit { + continue; + } + proc.gens.internal_exit.set(reapgens.internal_exit.get()); + + // Keep the borrow so we don't keep borrowing again and again and unwrapping again and + // again below. + let borrow = proc.internal_proc.borrow(); + let internal_proc = borrow.as_ref().unwrap(); + // Has the process exited? + if !internal_proc.exited() { + continue; + } + + // The process gets the status from its internal proc. + let status = internal_proc.get_status(); + handle_child_status(j, proc, &status); + FLOGF!( + proc_reap_internal, + "Reaped internal process '%ls' (id %llu, status %d)", + proc.argv0().unwrap(), + internal_proc.get_id(), + proc.status.status_value(), + ); + } + } + + // Remove any zombies. + reap_disowned_pids(); +} + +/// Generate process_exit events for any completed processes in \p j. +fn generate_process_exit_events(j: &Job, out_evts: &mut Vec) { + // Historically we have avoided generating events for foreground jobs from event handlers, as an + // event handler may itself produce a new event. + if !j.from_event_handler() || !j.is_foreground() { + for p in j.processes().iter() { + if p.has_pid() && p.is_completed() && !p.posted_proc_exit.load() { + p.posted_proc_exit.store(true); + out_evts.push(Event::process_exit(p.pid(), p.status.status_value())); + } + } + } +} + +/// Given a job that has completed, generate job_exit and caller_exit events. +fn generate_job_exit_events(j: &Job, out_evts: &mut Vec) { + // Generate proc and job exit events, except for foreground jobs originating in event handlers. + if !j.from_event_handler() || !j.is_foreground() { + // job_exit events. + if j.posts_job_exit_events() { + if let Some(last_pid) = j.get_last_pid() { + out_evts.push(Event::job_exit(last_pid, j.internal_job_id)); + } + } + } + // Generate caller_exit events. + out_evts.push(Event::caller_exit(j.internal_job_id, j.job_id())); +} + +/// \return whether to emit a fish_job_summary call for a process. +fn proc_wants_summary(j: &Job, p: &Process) -> bool { + // Are we completed with a pid? + if !p.is_completed() || !p.has_pid() { + return false; + } + + // Did we die due to a signal other than SIGPIPE? + let s = &p.status; + if !s.signal_exited() || s.signal_code() == SIGPIPE { + return false; + } + + // Does the job want to suppress notifications? + // Note we always report crashes. + if j.skip_notification() && !CRASHSIGNALS.contains(&s.signal_code()) { + return false; + } + + true +} + +/// \return whether to emit a fish_job_summary call for a job as a whole. We may also emit this for +/// its individual processes. +fn job_wants_summary(j: &Job) -> bool { + // Do we just skip notifications? + if j.skip_notification() { + return false; + } + + // Do we have a single process which will also report? If so then that suffices for us. + if j.processes().len() == 1 && proc_wants_summary(j, &j.processes()[0]) { + return false; + } + + // Are we foreground? + // The idea here is to not print status messages for jobs that execute in the foreground (i.e. + // without & and without being `bg`). + if j.is_foreground() { + return false; + } + + true +} + +/// \return whether we want to emit a fish_job_summary call for a job or any of its processes. +fn job_or_proc_wants_summary(j: &Job) -> bool { + job_wants_summary(j) || j.processes().iter().any(|p| proc_wants_summary(j, p)) +} + +/// Invoke the fish_job_summary function by executing the given command. +fn call_job_summary(parser: &Parser, cmd: &wstr) { + let event = Event::generic(L!("fish_job_summary").to_owned()); + let b = parser.push_block(Block::event_block(event)); + let saved_status = parser.get_last_statuses(); + parser.eval(cmd, &IoChain::new()); + parser.set_last_statuses(saved_status); + parser.pop_block(b); +} + +// \return a command which invokes fish_job_summary. +// The process pointer may be null, in which case it represents the entire job. +// Note this implements the arguments which fish_job_summary expects. +#[widestrs] +fn summary_command(j: &Job, p: Option<&Process>) -> WString { + let mut buffer = "fish_job_summary"L.to_owned(); + + // Job id. + buffer += &sprintf!(" %s", j.job_id().to_wstring())[..]; + + // 1 if foreground, 0 if background. + buffer += &sprintf!(" %d", if j.is_foreground() { 1 } else { 0 })[..]; + + // Command. + buffer.push(' '); + buffer += &escape(j.command())[..]; + + match p { + None => { + // No process, we are summarizing the whole job. + buffer += if j.is_stopped() { + " STOPPED"L + } else { + " ENDED"L + }; + } + Some(p) => { + // We are summarizing a process which exited with a signal. + // Arguments are the signal name and description. + let sig = Signal::new(p.status.signal_code()); + buffer.push(' '); + buffer += &escape(&sig.name())[..]; + + buffer.push(' '); + buffer += &escape(&sig.desc())[..]; + + // If we have multiple processes, we also append the pid and argv. + if j.processes().len() > 1 { + buffer += &sprintf!(" %d", p.pid())[..]; + + buffer.push(' '); + buffer += &escape(p.argv0().unwrap())[..]; + } + } + } + buffer +} + +// Summarize a list of jobs, by emitting calls to fish_job_summary. +// Note the given list must NOT be the parser's own job list, since the call to fish_job_summary +// could modify it. +fn summarize_jobs(parser: &Parser, jobs: &[JobRef]) -> bool { + if jobs.is_empty() { + return false; + } + + for j in jobs { + if j.is_stopped() { + call_job_summary(parser, &summary_command(j, None)); + } else { + // Completed job. + for p in j.processes().iter() { + if proc_wants_summary(j, p) { + call_job_summary(parser, &summary_command(j, Some(p))); + } + } + + // Overall status for the job. + if job_wants_summary(j) { + call_job_summary(parser, &summary_command(j, None)); + } + } + } + true +} + +/// Remove all disowned jobs whose job chain is fully constructed (that is, do not erase disowned +/// jobs that still have an in-flight parent job). Note we never print statuses for such jobs. +fn remove_disowned_jobs(jobs: &mut JobList) { + jobs.retain(|j| !j.flags().disown_requested || !j.is_constructed()); +} + +/// Given that a job has completed, check if it may be wait'ed on; if so add it to the wait handle +/// store. Then mark all wait handles as complete. +fn save_wait_handle_for_completed_job(job: &Job, store: &mut WaitHandleStore) { + assert!(job.is_completed(), "Job not completed"); + // Are we a background job? + if !job.is_foreground() { + for proc in job.processes().iter() { + if let Some(wh) = proc.make_wait_handle(job.internal_job_id) { + store.add(wh); + } + } + } + + // Mark all wait handles as complete (but don't create just for this). + for proc in job.processes().iter() { + if let Some(wh) = proc.get_wait_handle() { + wh.set_status_and_complete(proc.status.status_value()); + } + } +} + +/// Remove completed jobs from the job list, printing status messages as appropriate. +/// \return whether something was printed. +fn process_clean_after_marking(parser: &Parser, allow_interactive: bool) -> bool { + parser.assert_can_execute(); + + // This function may fire an event handler, we do not want to call ourselves recursively (to + // avoid infinite recursion). + if parser.libdata().pods.is_cleaning_procs { + return false; + } + + let _cleaning = scoped_push_replacer( + |new_value| std::mem::replace(&mut parser.libdata_mut().pods.is_cleaning_procs, new_value), + true, + ); + + // This may be invoked in an exit handler, after the TERM has been torn down + // Don't try to print in that case (#3222) + let interactive = allow_interactive && cur_term(); + + // Remove all disowned jobs. + remove_disowned_jobs(&mut parser.jobs_mut()); + + // Accumulate exit events into a new list, which we fire after the list manipulation is + // complete. + let mut exit_events = vec![]; + + // Defer processing under-construction jobs or jobs that want a message when we are not + // interactive. + let should_process_job = |j: &Job| { + // Do not attempt to process jobs which are not yet constructed. + // Do not attempt to process jobs that need to print a status message, + // unless we are interactive, in which case printing is OK. + j.is_constructed() && (interactive || !job_or_proc_wants_summary(j)) + }; + + // The list of jobs to summarize. Some of these jobs are completed and are removed from the + // parser's job list, others are stopped and remain in the list. + let mut jobs_to_summarize = vec![]; + + // Handle stopped jobs. These stay in our list. + for j in parser.jobs().iter() { + if j.is_stopped() + && !j.flags().notified_of_stop + && should_process_job(j) + && job_wants_summary(j) + { + j.mut_flags().notified_of_stop = true; + jobs_to_summarize.push(j.clone()); + } + } + + // Generate process_exit events for finished processes. + for j in parser.jobs().iter() { + generate_process_exit_events(j, &mut exit_events); + } + + // Remove completed, processable jobs from our job list. + let mut completed_jobs = vec![]; + parser.jobs_mut().retain(|j| { + if !should_process_job(j) || !j.is_completed() { + return true; + } + // We are committed to removing this job. + // Remember it for summary later, generate exit events, maybe save its wait handle if it + // finished in the background. + if job_or_proc_wants_summary(j) { + jobs_to_summarize.push(j.clone()); + } + generate_job_exit_events(j, &mut exit_events); + completed_jobs.push(j.clone()); + false + }); + for j in completed_jobs { + save_wait_handle_for_completed_job(&j, &mut parser.mut_wait_handles()); + } + + // Emit calls to fish_job_summary. + let printed = summarize_jobs(parser, &jobs_to_summarize); + + // Post pending exit events. + for evt in exit_events { + event::fire(parser, evt); + } + + if printed { + let _ = std::io::stdout().lock().flush(); + } + + printed +} + +pub fn have_proc_stat() -> bool { + // Check for /proc/self/stat to see if we are running with Linux-style procfs. + static HAVE_PROC_STAT_RESULT: Lazy = + Lazy::new(|| fs::metadata("/proc/self/stat").is_ok()); + *HAVE_PROC_STAT_RESULT +} + +/// The signals that signify crashes to us. +const CRASHSIGNALS: [libc::c_int; 6] = [SIGABRT, SIGBUS, SIGFPE, SIGILL, SIGSEGV, SIGSYS]; + +pub struct JobRefFfi(JobRef); + +unsafe impl cxx::ExternType for JobRefFfi { + type Id = cxx::type_id!("JobRefFfi"); + type Kind = cxx::kind::Opaque; +} + +pub struct JobGroupRefFfi(JobGroupRef); + +unsafe impl cxx::ExternType for JobGroupRefFfi { + type Id = cxx::type_id!("JobGroupRefFfi"); + type Kind = cxx::kind::Opaque; +} + +#[cxx::bridge] +mod proc_ffi { + extern "C++" { + include!("parser.h"); + type Parser = crate::parser::Parser; + } + extern "Rust" { + fn set_interactive_session(flag: bool); + fn get_login() -> bool; + fn mark_login(); + fn mark_no_exec(); + fn proc_init(); + + type JobRefFfi; + type JobGroupRefFfi; + + fn job_reap(parser: &Parser, allow_interactive: bool) -> bool; + fn is_interactive_session() -> bool; + fn have_proc_stat() -> bool; + fn proc_update_jiffies(parser: &Parser); + } + extern "Rust" { + type TtyTransfer; + fn new_tty_transfer() -> Box; + #[cxx_name = "to_job_group"] + fn to_job_group_ffi(&mut self, jg: &JobGroupRefFfi); + fn save_tty_modes(&mut self); + fn reclaim(&mut self); + fn no_exec() -> bool; + } + extern "Rust" { + type JobListFFI; + #[cxx_name = "jobs_requiring_warning_on_exit"] + fn jobs_requiring_warning_on_exit_ffi(parser: &Parser) -> Box; + #[cxx_name = "print_exit_warning_for_jobs"] + fn print_exit_warning_for_jobs_ffi(jobs: &JobListFFI); + fn empty(&self) -> bool; + #[cxx_name = "hup_jobs"] + fn hup_jobs_ffi(parser: &Parser); + } +} +struct JobListFFI(JobList); +fn jobs_requiring_warning_on_exit_ffi(parser: &Parser) -> Box { + Box::new(JobListFFI(jobs_requiring_warning_on_exit(parser))) +} +fn print_exit_warning_for_jobs_ffi(jobs: &JobListFFI) { + print_exit_warning_for_jobs(&jobs.0) +} + +fn hup_jobs_ffi(parser: &Parser) { + hup_jobs(&parser.jobs()) +} +impl JobListFFI { + fn empty(&self) -> bool { + self.0.is_empty() + } +} + +fn new_tty_transfer() -> Box { + Box::new(TtyTransfer::new()) +} + +impl TtyTransfer { + #[allow(clippy::wrong_self_convention)] + fn to_job_group_ffi(&mut self, jg: &JobGroupRefFfi) { + self.to_job_group(&jg.0); + } +} diff --git a/fish-rust/src/reader.rs b/fish-rust/src/reader.rs index 15ea8ac07..dcb0d9203 100644 --- a/fish-rust/src/reader.rs +++ b/fish-rust/src/reader.rs @@ -1,5 +1,127 @@ +use std::sync::atomic::AtomicI32; + +use crate::common::{escape_string, EscapeFlags, EscapeStringStyle}; +use crate::complete::{CompleteFlags, CompletionList}; use crate::env::Environment; -use crate::wchar::L; +use crate::expand::{expand_string, ExpandFlags, ExpandResultCode}; +use crate::ffi; +use crate::global_safety::RelaxedAtomicBool; +use crate::operation_context::OperationContext; +use crate::parser::Parser; +use crate::signal::signal_check_cancel; +use crate::wchar::prelude::*; +use crate::wchar_ffi::{WCharFromFFI, WCharToFFI}; +use crate::wcstringutil::count_preceding_backslashes; +use crate::wildcard::wildcard_has; +use cxx::{CxxWString, UniquePtr}; +use libc::SIGINT; +use std::os::fd::RawFd; +use std::sync::atomic::Ordering; + +#[derive(Default)] +pub struct ReaderConfig { + /// Left prompt command, typically fish_prompt. + pub left_prompt_cmd: WString, + + /// Right prompt command, typically fish_right_prompt. + pub right_prompt_cmd: WString, + + /// Name of the event to trigger once we're set up. + pub event: &'static wstr, + + /// Whether tab completion is OK. + pub complete_ok: bool, + + /// Whether to perform syntax highlighting. + pub highlight_ok: bool, + + /// Whether to perform syntax checking before returning. + pub syntax_check_ok: bool, + + /// Whether to allow autosuggestions. + pub autosuggest_ok: bool, + + /// Whether to expand abbreviations. + pub expand_abbrev_ok: bool, + + /// Whether to exit on interrupt (^C). + pub exit_on_interrupt: bool, + + /// If set, do not show what is typed. + pub in_silent_mode: bool, + + /// The fd for stdin, default to actual stdin. + pub inputfd: RawFd, +} + +pub fn reader_push(parser: &Parser, history_name: &wstr, conf: ReaderConfig) { + ffi::reader_push_ffi( + parser as *const Parser as *const autocxx::c_void, + &history_name.to_ffi(), + &conf as *const ReaderConfig as *const autocxx::c_void, + ); +} + +pub fn reader_readline(nchars: i32) -> Option { + let mut line = L!("").to_ffi(); + if ffi::reader_readline_ffi(line.pin_mut(), autocxx::c_int(nchars)) { + Some(line.from_ffi()) + } else { + None + } +} + +pub fn reader_pop() { + ffi::reader_pop() +} + +pub fn reader_write_title(cmd: &wstr, parser: &Parser, reset_cursor_position: bool /*=true*/) { + ffi::reader_write_title_ffi( + &cmd.to_ffi(), + parser as *const Parser as *const autocxx::c_void, + reset_cursor_position, + ); +} + +/// This variable is set to a signal by the signal handler when ^C is pressed. +static INTERRUPTED: AtomicI32 = AtomicI32::new(0); + +/// The readers interrupt signal handler. Cancels all currently running blocks. +/// This is called from a signal handler! +pub fn reader_handle_sigint() { + INTERRUPTED.store(SIGINT, Ordering::Relaxed); +} + +/// Clear the interrupted flag unconditionally without handling anything. The flag could have been +/// set e.g. when an interrupt arrived just as we were ending an earlier \c reader_readline +/// invocation but before the \c is_interactive_read flag was cleared. +pub fn reader_reset_interrupted() { + INTERRUPTED.store(0, Ordering::Relaxed); +} + +/// Return the value of the interrupted flag, which is set by the sigint handler, and clear it if it +/// was set. In practice this will return 0 or SIGINT. +fn reader_test_and_clear_interrupted() -> i32 { + let res = INTERRUPTED.load(Ordering::Relaxed); + if res != 0 { + INTERRUPTED.store(0, Ordering::Relaxed); + }; + res +} + +/// If set, SIGHUP has been received. This latches to true. +/// This is set from a signal handler. +static SIGHUP_RECEIVED: RelaxedAtomicBool = RelaxedAtomicBool::new(false); + +/// Mark that we encountered SIGHUP and must (soon) exit. This is invoked from a signal handler. +pub fn reader_sighup() { + // Beware, we may be in a signal handler. + SIGHUP_RECEIVED.store(true); +} + +fn reader_received_sighup() -> bool { + SIGHUP_RECEIVED.load() +} #[repr(u8)] pub enum CursorSelectionMode { @@ -13,3 +135,206 @@ pub fn check_autosuggestion_enabled(vars: &dyn Environment) -> bool { .map(|v| v != L!("0")) .unwrap_or(true) } + +pub fn reader_schedule_prompt_repaint() { + crate::ffi::reader_schedule_prompt_repaint() +} + +/// \return whether fish is currently unwinding the stack in preparation to exit. +pub fn fish_is_unwinding_for_exit() -> bool { + crate::ffi::fish_is_unwinding_for_exit() +} + +pub fn reader_run_count() -> u64 { + crate::ffi::reader_run_count() +} + +/// When tab-completing with a wildcard, we expand the wildcard up to this many results. +/// If expansion would exceed this many results, beep and do nothing. +const TAB_COMPLETE_WILDCARD_MAX_EXPANSION: usize = 256; + +/// Given that the user is tab-completing a token \p wc whose cursor is at \p pos in the token, +/// try expanding it as a wildcard, populating \p result with the expanded string. +fn try_expand_wildcard( + parser: &Parser, + wc: WString, + position: usize, + result: &mut WString, +) -> ExpandResultCode { + // Hacky from #8593: only expand if there are wildcards in the "current path component." + // Find the "current path component" by looking for an unescaped slash before and after + // our position. + // This is quite naive; for example it mishandles brackets. + let is_path_sep = + |offset| wc.char_at(offset) == '/' && count_preceding_backslashes(&wc, offset) % 2 == 0; + + let mut comp_start = position; + while comp_start > 0 && !is_path_sep(comp_start - 1) { + comp_start -= 1; + } + let mut comp_end = position; + while comp_end < wc.len() && !is_path_sep(comp_end) { + comp_end += 1; + } + if !wildcard_has(&wc[comp_start..comp_end]) { + return ExpandResultCode::wildcard_no_match; + } + result.clear(); + // Have a low limit on the number of matches, otherwise we will overwhelm the command line. + + let ctx = OperationContext::background_with_cancel_checker( + &*parser.variables, + Box::new(|| signal_check_cancel() != 0), + TAB_COMPLETE_WILDCARD_MAX_EXPANSION, + ); + + // We do wildcards only. + + let flags = ExpandFlags::SKIP_CMDSUBST + | ExpandFlags::SKIP_VARIABLES + | ExpandFlags::PRESERVE_HOME_TILDES; + let mut expanded = CompletionList::new(); + let ret = expand_string(wc, &mut expanded, flags, &ctx, None); + if ret.result != ExpandResultCode::ok { + return ret.result; + } + + // Insert all matches (escaped) and a trailing space. + let mut joined = WString::new(); + for r#match in expanded { + if r#match.flags.contains(CompleteFlags::DONT_ESCAPE) { + joined.push_utfstr(&r#match.completion); + } else { + let tildeflag = if r#match.flags.contains(CompleteFlags::DONT_ESCAPE_TILDES) { + EscapeFlags::NO_TILDE + } else { + EscapeFlags::default() + }; + joined.push_utfstr(&escape_string( + &r#match.completion, + EscapeStringStyle::Script(EscapeFlags::NO_QUOTED | tildeflag), + )); + } + joined.push(' '); + } + + *result = joined; + ExpandResultCode::ok +} + +pub fn completion_apply_to_command_line( + val_str: &wstr, + flags: CompleteFlags, + command_line: &wstr, + inout_cursor_pos: &mut usize, + append_only: bool, +) -> WString { + ffi::completion_apply_to_command_line( + &val_str.to_ffi(), + flags.bits(), + &command_line.to_ffi(), + inout_cursor_pos, + append_only, + ) + .from_ffi() +} + +#[cxx::bridge] +mod reader_ffi { + extern "C++" { + include!("operation_context.h"); + include!("env.h"); + include!("parser.h"); + #[cxx_name = "EnvDyn"] + type EnvDynFFI = crate::env::EnvDynFFI; + type Parser = crate::parser::Parser; + } + extern "Rust" { + fn reader_reset_interrupted(); + fn reader_handle_sigint(); + fn reader_test_and_clear_interrupted() -> i32; + fn reader_sighup(); + fn reader_received_sighup() -> bool; + } + extern "Rust" { + #[cxx_name = "try_expand_wildcard"] + fn try_expand_wildcard_ffi( + parser: &Parser, + wc: &CxxWString, + position: usize, + result: &mut UniquePtr, + ) -> u8; + } + extern "Rust" { + type ReaderConfig; + #[cxx_name = "left_prompt_cmd"] + fn left_prompt_cmd_ffi(&self) -> UniquePtr; + #[cxx_name = "right_prompt_cmd"] + fn right_prompt_cmd_ffi(&self) -> UniquePtr; + #[cxx_name = "event"] + fn event_ffi(&self) -> UniquePtr; + #[cxx_name = "complete_ok"] + fn complete_ok_ffi(&self) -> bool; + #[cxx_name = "highlight_ok"] + fn highlight_ok_ffi(&self) -> bool; + #[cxx_name = "syntax_check_ok"] + fn syntax_check_ok_ffi(&self) -> bool; + #[cxx_name = "autosuggest_ok"] + fn autosuggest_ok_ffi(&self) -> bool; + #[cxx_name = "expand_abbrev_ok"] + fn expand_abbrev_ok_ffi(&self) -> bool; + #[cxx_name = "exit_on_interrupt"] + fn exit_on_interrupt_ffi(&self) -> bool; + #[cxx_name = "in_silent_mode"] + fn in_silent_mode_ffi(&self) -> bool; + #[cxx_name = "inputfd"] + fn inputfd_ffi(&self) -> i32; + } +} + +impl ReaderConfig { + fn left_prompt_cmd_ffi(&self) -> UniquePtr { + self.left_prompt_cmd.to_ffi() + } + fn right_prompt_cmd_ffi(&self) -> UniquePtr { + self.right_prompt_cmd.to_ffi() + } + fn event_ffi(&self) -> UniquePtr { + self.event.to_ffi() + } + fn complete_ok_ffi(&self) -> bool { + self.complete_ok + } + fn highlight_ok_ffi(&self) -> bool { + self.highlight_ok + } + fn syntax_check_ok_ffi(&self) -> bool { + self.syntax_check_ok + } + fn autosuggest_ok_ffi(&self) -> bool { + self.autosuggest_ok + } + fn expand_abbrev_ok_ffi(&self) -> bool { + self.expand_abbrev_ok + } + fn exit_on_interrupt_ffi(&self) -> bool { + self.exit_on_interrupt + } + fn in_silent_mode_ffi(&self) -> bool { + self.in_silent_mode + } + fn inputfd_ffi(&self) -> i32 { + self.inputfd as _ + } +} +fn try_expand_wildcard_ffi( + parser: &Parser, + wc: &CxxWString, + position: usize, + result: &mut UniquePtr, +) -> u8 { + let mut rust_result = WString::new(); + let result_code = try_expand_wildcard(parser, wc.from_ffi(), position, &mut rust_result); + *result = rust_result.to_ffi(); + unsafe { std::mem::transmute(result_code) } +} diff --git a/fish-rust/src/redirection.rs b/fish-rust/src/redirection.rs index b27dd35a1..410f75a71 100644 --- a/fish-rust/src/redirection.rs +++ b/fish-rust/src/redirection.rs @@ -1,6 +1,7 @@ //! This file supports specifying and applying redirections. use crate::ffi::wcharz_t; +use crate::io::IoChain; use crate::wchar::prelude::*; use crate::wchar_ffi::WCharToFFI; use crate::wutil::fish_wcstoi; @@ -59,6 +60,7 @@ struct Dup2Action { } /// A class representing a sequence of basic redirections. + #[derive(Default)] struct Dup2List { /// The list of actions. pub actions: Vec, @@ -87,6 +89,12 @@ pub fn oflags(self) -> Option { } } +impl Dup2Action { + pub fn new(src: RawFd, target: RawFd) -> Self { + Self { src, target } + } +} + /// A struct which represents a redirection specification from the user. /// Here the file descriptors don't represent open files - it's purely textual. #[derive(Clone)] @@ -106,6 +114,9 @@ pub struct RedirectionSpec { } impl RedirectionSpec { + pub fn new(fd: RawFd, mode: RedirectionMode, target: WString) -> Self { + Self { fd, mode, target } + } /// \return if this is a close-type redirection. pub fn is_close(&self) -> bool { self.mode == RedirectionMode::fd && self.target == L!("-") @@ -178,7 +189,19 @@ fn clone(&self) -> Box { /// Produce a dup_fd_list_t from an io_chain. This may not be called before fork(). /// The result contains the list of fd actions (dup2 and close), as well as the list /// of fds opened. -fn dup2_list_resolve_chain(io_chain: &Vec) -> Dup2List { +pub fn dup2_list_resolve_chain(io_chain: &IoChain) -> Dup2List { + let mut result = Dup2List { actions: vec![] }; + for io in &io_chain.0 { + if io.source_fd() < 0 { + result.add_close(io.fd()) + } else { + result.add_dup2(io.source_fd(), io.fd()) + } + } + result +} + +fn dup2_list_resolve_chain_ffi(io_chain: &CxxVector) -> Dup2List { let mut result = Dup2List { actions: vec![] }; for io in io_chain { if io.src < 0 { @@ -190,11 +213,10 @@ fn dup2_list_resolve_chain(io_chain: &Vec) -> Dup2List { result } -fn dup2_list_resolve_chain_ffi(io_chain: &CxxVector) -> Dup2List { - dup2_list_resolve_chain(&io_chain.iter().cloned().collect()) -} - impl Dup2List { + pub fn new() -> Self { + Default::default() + } /// \return the list of dup2 actions. pub fn get_actions(&self) -> &[Dup2Action] { &self.actions @@ -203,7 +225,7 @@ pub fn get_actions(&self) -> &[Dup2Action] { /// \return the fd ultimately dup'd to a target fd, or -1 if the target is closed. /// For example, if target fd is 1, and we have a dup2 chain 5->3 and 3->1, then we will /// return 5. If the target is not referenced in the chain, returns target. - fn fd_for_target_fd(&self, target: RawFd) -> RawFd { + pub fn fd_for_target_fd(&self, target: RawFd) -> RawFd { // Paranoia. if target < 0 { return target; @@ -224,7 +246,7 @@ fn fd_for_target_fd(&self, target: RawFd) -> RawFd { } /// Append a dup2 action. - fn add_dup2(&mut self, src: RawFd, target: RawFd) { + pub fn add_dup2(&mut self, src: RawFd, target: RawFd) { assert!(src >= 0 && target >= 0, "Invalid fd in add_dup2"); // Note: record these even if src and target is the same. // This is a note that we must clear the CLO_EXEC bit. @@ -232,7 +254,7 @@ fn add_dup2(&mut self, src: RawFd, target: RawFd) { } /// Append a close action. - fn add_close(&mut self, fd: RawFd) { + pub fn add_close(&mut self, fd: RawFd) { assert!(fd >= 0, "Invalid fd in add_close"); self.actions.push(Dup2Action { src: fd, diff --git a/fish-rust/src/signal.rs b/fish-rust/src/signal.rs index 3911c7ca1..8314c16c1 100644 --- a/fish-rust/src/signal.rs +++ b/fish-rust/src/signal.rs @@ -2,8 +2,9 @@ use crate::common::{exit_without_destructors, restore_term_foreground_process_group_for_exit}; use crate::event::{enqueue_signal, is_signal_observed}; -use crate::termsize::termsize_handle_winch; -use crate::topic_monitor::{generation_t, invalid_generations, topic_monitor_principal, topic_t}; +use crate::reader::{reader_handle_sigint, reader_sighup}; +use crate::termsize::TermsizeContainer; +use crate::topic_monitor::{generation_t, topic_monitor_principal, topic_t, GenerationsList}; use crate::wchar::prelude::*; use crate::wchar_ffi::{AsWstr, WCharToFFI}; use crate::wutil::{fish_wcstoi, perror}; @@ -34,6 +35,11 @@ mod signal_ffi { fn signal_reset_handlers(); } + extern "Rust" { + type SigChecker; + fn new_sighupint_checker() -> Box; + fn check(&mut self) -> bool; + } } fn sig2wcs_ffi(sig: i32) -> UniquePtr { @@ -102,13 +108,6 @@ pub fn signal_check_cancel() -> i32 { CANCELLATION_SIGNAL.load(Ordering::Relaxed) } -// Declare these as an extern C functions and call them directly, -// in case the autocxx ffi allocates or does something else signal-unfriendly. -extern "C" { - fn reader_sighup(); - fn reader_handle_sigint(); -} - /// The single signal handler. By centralizing signal handling we ensure that we can never install /// the "wrong" signal handler (see #5969). extern "C" fn fish_signal_handler( @@ -135,12 +134,12 @@ extern "C" fn fish_signal_handler( match sig { libc::SIGWINCH => { // Respond to a winch signal by telling the termsize container. - termsize_handle_winch(); + TermsizeContainer::handle_winch(); } libc::SIGHUP => { // Exit unless the signal was trapped. if !observed { - unsafe { reader_sighup() }; + reader_sighup(); } topic_monitor_principal().post(topic_t::sighupint); } @@ -160,7 +159,7 @@ extern "C" fn fish_signal_handler( if !observed { CANCELLATION_SIGNAL.store(libc::SIGINT, Ordering::Relaxed); } - unsafe { reader_handle_sigint() }; + reader_handle_sigint(); topic_monitor_principal().post(topic_t::sighupint); } libc::SIGCHLD => { @@ -391,9 +390,9 @@ pub fn check(&mut self) -> bool { /// Wait until a sigint is delivered. pub fn wait(&self) { let tm = topic_monitor_principal(); - let mut gens = invalid_generations(); - *gens.at_mut(self.topic) = self.gen; - tm.check(&mut gens, true /* wait */); + let gens = GenerationsList::invalid(); + gens.set(self.topic, self.gen); + tm.check(&gens, true /* wait */); } } @@ -591,6 +590,10 @@ fn from(value: Signal) -> Self { assert_eq!(sig.name(), "SIGINT"); }); +fn new_sighupint_checker() -> Box { + Box::new(SigChecker::new_sighupint()) +} + #[rustfmt::skip] add_test!("test_signal_parse", || { assert_eq!(Signal::parse(L!("SIGHUP")), Some(Signal::new(libc::SIGHUP))); diff --git a/fish-rust/src/termsize.rs b/fish-rust/src/termsize.rs index ef63ac999..87337451a 100644 --- a/fish-rust/src/termsize.rs +++ b/fish-rust/src/termsize.rs @@ -1,10 +1,8 @@ // Support for exposing the terminal size. use crate::common::assert_sync; -use crate::env::{EnvMode, EnvStackRefFFI, EnvVar, Environment}; -use crate::ffi::{Parser, Repin}; +use crate::env::{EnvMode, EnvVar, Environment}; use crate::flog::FLOG; use crate::wchar::prelude::*; -use crate::wchar_ffi::WCharToFFI; use crate::wutil::fish_wcstoi; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::Mutex; @@ -23,13 +21,19 @@ pub struct Termsize { pub height: isize, } + extern "C++" { + include!("env.h"); + include!("parser.h"); + #[cxx_name = "EnvStackRef"] + type EnvStackRefFFI = crate::env::EnvStackRefFFI; + type Parser = crate::parser::Parser; + } + extern "Rust" { pub fn termsize_default() -> Termsize; pub fn termsize_last() -> Termsize; - pub fn termsize_initialize_ffi(vars: *const u8) -> Termsize; pub fn termsize_invalidate_tty(); - pub fn termsize_update_ffi(parser: *mut u8) -> Termsize; - pub fn termsize_handle_winch(); + pub fn termsize_update(parser: &Parser) -> Termsize; } } pub use termsize_ffi::Termsize; @@ -179,7 +183,7 @@ pub fn initialize(&self, vars: &dyn Environment) -> Termsize { /// registered for COLUMNS and LINES. /// This requires a shared reference so it can work from a static. /// \return the updated termsize. - pub fn updating(&self, parser: &mut Parser) -> Termsize { + pub fn updating(&self, parser: &Parser) -> Termsize { let new_size; let prev_size; @@ -208,18 +212,10 @@ pub fn updating(&self, parser: &mut Parser) -> Termsize { new_size } - fn set_columns_lines_vars(&self, val: Termsize, parser: &mut Parser) { + fn set_columns_lines_vars(&self, val: Termsize, parser: &Parser) { let saved = self.setting_env_vars.swap(true, Ordering::Relaxed); - parser.pin().set_var_and_fire( - &L!("COLUMNS").to_ffi(), - EnvMode::GLOBAL.bits(), - val.width.to_wstring().to_ffi(), - ); - parser.pin().set_var_and_fire( - &L!("LINES").to_ffi(), - EnvMode::GLOBAL.bits(), - val.height.to_wstring().to_ffi(), - ); + parser.set_var_and_fire(L!("COLUMNS"), EnvMode::GLOBAL, vec![val.width.to_wstring()]); + parser.set_var_and_fire(L!("LINES"), EnvMode::GLOBAL, vec![val.height.to_wstring()]); self.setting_env_vars.store(saved, Ordering::Relaxed); } @@ -250,31 +246,6 @@ fn handle_columns_lines_var_change(&self, vars: &dyn Environment) { .mark_override_from_env(new_termsize); } - /// Note that COLUMNS and/or LINES global variables changed. - fn handle_columns_lines_var_change_ffi(&self, vars: &dyn Environment) { - // Do nothing if we are the ones setting it. - if self.setting_env_vars.load(Ordering::Relaxed) { - return; - } - // Construct a new termsize from COLUMNS and LINES, then set it in our data. - let new_termsize = Termsize { - width: var_to_int_or( - vars.getf(L!("COLUMNS"), EnvMode::GLOBAL), - Termsize::DEFAULT_WIDTH, - ), - height: var_to_int_or( - vars.getf(L!("LINES"), EnvMode::GLOBAL), - Termsize::DEFAULT_HEIGHT, - ), - }; - - // Store our termsize as an environment override. - self.data - .lock() - .unwrap() - .mark_override_from_env(new_termsize); - } - /// Note that a WINCH signal is received. /// Naturally this may be called from within a signal handler. pub fn handle_winch() { @@ -309,36 +280,21 @@ pub fn handle_columns_lines_var_change(vars: &dyn Environment) { SHARED_CONTAINER.handle_columns_lines_var_change(vars); } -/// Called to initialize the termsize. -/// The pointer is to a Box, but has the wrong type to satisfy cxx. -#[allow(clippy::borrowed_box)] -pub fn termsize_initialize_ffi(vars_ptr: *const u8) -> Termsize { - assert!(!vars_ptr.is_null()); - let vars: &Box = unsafe { &*(vars_ptr.cast()) }; - SHARED_CONTAINER.initialize(&*vars.0) -} - -/// Called to update termsize. -pub fn termsize_update_ffi(parser_ptr: *mut u8) -> Termsize { - assert!(!parser_ptr.is_null()); - let parser: &mut Parser = unsafe { &mut *(parser_ptr as *mut Parser) }; +fn termsize_update(parser: &Parser) -> Termsize { SHARED_CONTAINER.updating(parser) } -/// FFI bridge for WINCH. -pub fn termsize_handle_winch() { - TermsizeContainer::handle_winch(); -} - pub fn termsize_invalidate_tty() { TermsizeContainer::invalidate_tty(); } use crate::ffi_tests::add_test; + +use self::termsize_ffi::Parser; add_test!("test_termsize", || { let env_global = EnvMode::GLOBAL; - let parser: &mut Parser = unsafe { &mut *Parser::principal_parser_ffi() }; - let vars = parser.get_vars(); + let parser = Parser::principal_parser(); + let vars = parser.vars(); // Use a static variable so we can pretend we're the kernel exposing a terminal size. static STUBBY_TERMSIZE: Mutex> = Mutex::new(None); @@ -375,13 +331,13 @@ fn stubby_termsize() -> Option { // Now the tty's termsize doesn't matter. vars.set_one(L!("COLUMNS"), env_global, L!("75").to_owned()); vars.set_one(L!("LINES"), env_global, L!("150").to_owned()); - ts.handle_columns_lines_var_change(&*parser.get_vars()); + ts.handle_columns_lines_var_change(parser.vars()); assert_eq!(ts.last(), Termsize::new(75, 150)); assert_eq!(vars.get(L!("COLUMNS")).unwrap().as_string(), "75"); assert_eq!(vars.get(L!("LINES")).unwrap().as_string(), "150"); vars.set_one(L!("COLUMNS"), env_global, L!("33").to_owned()); - ts.handle_columns_lines_var_change(&*parser.get_vars()); + ts.handle_columns_lines_var_change(parser.vars()); assert_eq!(ts.last(), Termsize::new(33, 150)); // Oh it got SIGWINCH, now the tty matters again. @@ -394,7 +350,7 @@ fn stubby_termsize() -> Option { // Test initialize(). vars.set_one(L!("COLUMNS"), env_global, L!("83").to_owned()); vars.set_one(L!("LINES"), env_global, L!("38").to_owned()); - ts.initialize(&*vars); + ts.initialize(vars); assert_eq!(ts.last(), Termsize::new(83, 38)); // initialize() even beats the tty reader until a sigwinch. @@ -403,7 +359,7 @@ fn stubby_termsize() -> Option { setting_env_vars: AtomicBool::new(false), tty_size_reader: stubby_termsize, }; - ts.initialize(&*parser.get_vars()); + ts.initialize(parser.vars()); ts2.updating(parser); assert_eq!(ts.last(), Termsize::new(83, 38)); TermsizeContainer::handle_winch(); diff --git a/fish-rust/src/tests/complete.rs b/fish-rust/src/tests/complete.rs new file mode 100644 index 000000000..8e454523d --- /dev/null +++ b/fish-rust/src/tests/complete.rs @@ -0,0 +1,616 @@ +use crate::abbrs::{self, with_abbrs_mut, Abbreviation}; +use crate::complete::{ + complete, complete_add, complete_add_wrapper, complete_get_wrap_targets, + complete_remove_wrapper, sort_and_prioritize, CompleteFlags, CompleteOptionType, + CompletionList, CompletionMode, CompletionRequestOptions, +}; +use crate::env::{EnvMode, Environment}; +use crate::ffi_tests::add_test; +use crate::io::IoChain; +use crate::operation_context::{ + no_cancel, OperationContext, EXPANSION_LIMIT_BACKGROUND, EXPANSION_LIMIT_DEFAULT, +}; +use crate::parser::Parser; +use crate::reader::completion_apply_to_command_line; +use crate::tests::prelude::*; +use crate::wchar::prelude::*; +use crate::wcstringutil::join_strings; +use std::collections::HashMap; +use std::ffi::CString; + +/// Joins a std::vector via commas. +fn comma_join(lst: Vec) -> WString { + join_strings(&lst, ',') +} + +add_test!("test_complete", || { + let vars = PwdEnvironment { + parent: TestEnvironment { + vars: HashMap::from([ + (WString::from_str("Foo1"), WString::new()), + (WString::from_str("Foo2"), WString::new()), + (WString::from_str("Foo3"), WString::new()), + (WString::from_str("Bar1"), WString::new()), + (WString::from_str("Bar2"), WString::new()), + (WString::from_str("Bar3"), WString::new()), + (WString::from_str("alpha"), WString::new()), + (WString::from_str("ALPHA!"), WString::new()), + (WString::from_str("gamma1"), WString::new()), + (WString::from_str("GAMMA2"), WString::new()), + ]), + }, + }; + + let parser = Parser::principal_parser().shared(); + let ctx = OperationContext::test_only_foreground(parser.clone(), &vars, Box::new(no_cancel)); + + let do_complete = |cmd: &wstr, flags: CompletionRequestOptions| complete(cmd, flags, &ctx).0; + + let mut completions = do_complete(L!("$"), CompletionRequestOptions::default()); + sort_and_prioritize(&mut completions, CompletionRequestOptions::default()); + assert_eq!( + completions + .into_iter() + .map(|c| c.completion.to_string()) + .collect::>(), + [ + "alpha", "ALPHA!", "Bar1", "Bar2", "Bar3", "Foo1", "Foo2", "Foo3", "gamma1", "GAMMA2", + "PWD" + ] + .into_iter() + .map(|s| s.to_owned()) + .collect::>() + ); + + // Smartcase test. Lowercase inputs match both lowercase and uppercase. + let mut completions = do_complete(L!("$a"), CompletionRequestOptions::default()); + sort_and_prioritize(&mut completions, CompletionRequestOptions::default()); + + assert_eq!(completions.len(), 2); + assert_eq!(completions[0].completion, L!("$ALPHA!")); + assert_eq!(completions[1].completion, L!("lpha")); + + let mut completions = do_complete(L!("$F"), CompletionRequestOptions::default()); + sort_and_prioritize(&mut completions, CompletionRequestOptions::default()); + assert_eq!(completions.len(), 3); + assert_eq!(completions[0].completion, L!("oo1")); + assert_eq!(completions[1].completion, L!("oo2")); + assert_eq!(completions[2].completion, L!("oo3")); + + completions = do_complete(L!("$1"), CompletionRequestOptions::default()); + sort_and_prioritize(&mut completions, CompletionRequestOptions::default()); + assert_eq!(completions, vec![]); + + let mut fuzzy_options = CompletionRequestOptions::default(); + fuzzy_options.fuzzy_match = true; + let mut completions = do_complete(L!("$1"), fuzzy_options); + sort_and_prioritize(&mut completions, fuzzy_options); + assert_eq!(completions.len(), 3); + assert_eq!(completions[0].completion, L!("$Bar1")); + assert_eq!(completions[1].completion, L!("$Foo1")); + assert_eq!(completions[2].completion, L!("$gamma1")); + + std::fs::create_dir_all("test/complete_test").unwrap(); + std::fs::write("test/complete_test/has space", []).unwrap(); + std::fs::write("test/complete_test/bracket[abc]", []).unwrap(); + #[cfg(not(windows))] // Square brackets are not legal path characters on WIN32/CYGWIN + std::fs::write(r"test/complete_test/gnarlybracket\[abc]", []).unwrap(); + std::fs::write("test/complete_test/testfile", []).unwrap(); + let testfile = CString::new("test/complete_test/testfile").unwrap(); + assert_eq!(unsafe { libc::chmod(testfile.as_ptr(), 0o700,) }, 0); + std::fs::create_dir_all("test/complete_test/foo1").unwrap(); + std::fs::create_dir_all("test/complete_test/foo2").unwrap(); + std::fs::create_dir_all("test/complete_test/foo3").unwrap(); + + completions = do_complete( + L!("echo (test/complete_test/testfil"), + CompletionRequestOptions::default(), + ); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].completion, L!("e")); + + completions = do_complete( + L!("echo (ls test/complete_test/testfil"), + CompletionRequestOptions::default(), + ); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].completion, L!("e")); + + completions = do_complete( + L!("echo (command ls test/complete_test/testfil"), + CompletionRequestOptions::default(), + ); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].completion, L!("e")); + + // Completing after spaces - see #2447 + completions = do_complete( + L!("echo (ls test/complete_test/has\\ "), + CompletionRequestOptions::default(), + ); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].completion, L!("space")); + + // Brackets - see #5831 + completions = do_complete( + L!("echo (ls test/complete_test/bracket["), + CompletionRequestOptions::default(), + ); + assert_eq!(completions.len(), 1); + assert_eq!( + completions[0].completion, + L!("test/complete_test/bracket[abc]") + ); + + let mut cmdline = L!("touch test/complete_test/bracket["); + completions = do_complete(cmdline, CompletionRequestOptions::default()); + assert_eq!(completions.len(), 1); + assert_eq!( + completions[0].completion, + L!("test/complete_test/bracket[abc]") + ); + let mut cursor = cmdline.len(); + let newcmdline = completion_apply_to_command_line( + &completions[0].completion, + completions[0].flags, + cmdline, + &mut cursor, + false, + ); + assert_eq!(newcmdline, L!("touch test/complete_test/bracket\\[abc\\] ")); + + // #8820 + let mut cursor_pos = 11; + let mut newcmdline = completion_apply_to_command_line( + L!("Debug/"), + CompleteFlags::REPLACES_TOKEN | CompleteFlags::NO_SPACE, + L!("mv debug debug"), + &mut cursor_pos, + true, + ); + assert_eq!(newcmdline, L!("mv debug Debug/")); + + #[cfg(not(windows))] // Square brackets are not legal path characters on WIN32/CYGWIN + { + cmdline = L!(r"touch test/complete_test/gnarlybracket\\["); + completions = do_complete(cmdline, CompletionRequestOptions::default()); + assert_eq!(completions.len(), 1); + assert_eq!( + completions[0].completion, + L!(r"test/complete_test/gnarlybracket\[abc]") + ); + let mut cursor = cmdline.len(); + newcmdline = completion_apply_to_command_line( + &completions[0].completion, + completions[0].flags, + cmdline, + &mut cursor, + false, + ); + assert_eq!( + newcmdline, + L!(r"touch test/complete_test/gnarlybracket\\\[abc\] ") + ); + } + + // Add a function and test completing it in various ways. + parser.eval(L!("function scuttlebutt; end"), &IoChain::new()); + + // Complete a function name. + completions = do_complete(L!("echo (scuttlebut"), CompletionRequestOptions::default()); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].completion, L!("t")); + + // But not with the command prefix. + completions = do_complete( + L!("echo (command scuttlebut"), + CompletionRequestOptions::default(), + ); + assert_eq!(&completions, &[]); + + // Not with the builtin prefix. + let completions = do_complete( + L!("echo (builtin scuttlebut"), + CompletionRequestOptions::default(), + ); + assert_eq!(&completions, &[]); + + // Not after a redirection. + let completions = do_complete( + L!("echo hi > scuttlebut"), + CompletionRequestOptions::default(), + ); + assert_eq!(&completions, &[]); + + // Trailing spaces (#1261). + let mut no_files = CompletionMode::default(); + no_files.no_files = true; + complete_add( + L!("foobarbaz").into(), + false, + WString::new(), + CompleteOptionType::ArgsOnly, + no_files, + vec![], + L!("qux").into(), + WString::new(), + CompleteFlags::AUTO_SPACE, + ); + let completions = do_complete(L!("foobarbaz "), CompletionRequestOptions::default()); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].completion, L!("qux")); + + // Don't complete variable names in single quotes (#1023). + let completions = do_complete(L!("echo '$Foo"), CompletionRequestOptions::default()); + assert_eq!(completions, vec![]); + let completions = do_complete(L!("echo \\$Foo"), CompletionRequestOptions::default()); + assert_eq!(completions, vec![]); + + // File completions. + let completions = do_complete( + L!("cat test/complete_test/te"), + CompletionRequestOptions::default(), + ); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].completion, L!("stfile")); + let completions = do_complete( + L!("echo sup > test/complete_test/te"), + CompletionRequestOptions::default(), + ); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].completion, L!("stfile")); + let completions = do_complete( + L!("echo sup > test/complete_test/te"), + CompletionRequestOptions::default(), + ); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].completion, L!("stfile")); + + pushd("test/complete_test"); + let completions = do_complete(L!("cat te"), CompletionRequestOptions::default()); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].completion, L!("stfile")); + assert!(!(completions[0].flags.contains(CompleteFlags::REPLACES_TOKEN))); + assert!( + !(completions[0] + .flags + .contains(CompleteFlags::DUPLICATES_ARGUMENT)) + ); + let completions = do_complete(L!("cat testfile te"), CompletionRequestOptions::default()); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].completion, L!("stfile")); + assert!(completions[0] + .flags + .contains(CompleteFlags::DUPLICATES_ARGUMENT)); + let completions = do_complete(L!("cat testfile TE"), CompletionRequestOptions::default()); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].completion, L!("testfile")); + assert!(completions[0].flags.contains(CompleteFlags::REPLACES_TOKEN)); + assert!(completions[0] + .flags + .contains(CompleteFlags::DUPLICATES_ARGUMENT)); + let completions = do_complete( + L!("something --abc=te"), + CompletionRequestOptions::default(), + ); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].completion, L!("stfile")); + let completions = do_complete(L!("something -abc=te"), CompletionRequestOptions::default()); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].completion, L!("stfile")); + let completions = do_complete(L!("something abc=te"), CompletionRequestOptions::default()); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].completion, L!("stfile")); + let completions = do_complete( + L!("something abc=stfile"), + CompletionRequestOptions::default(), + ); + assert_eq!(&completions, &[]); + let completions = do_complete(L!("something abc=stfile"), fuzzy_options); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].completion, L!("abc=testfile")); + + // Zero escapes can cause problems. See issue #1631. + let completions = do_complete(L!("cat foo\\0"), CompletionRequestOptions::default()); + assert_eq!(&completions, &[]); + let completions = do_complete(L!("cat foo\\0bar"), CompletionRequestOptions::default()); + assert_eq!(&completions, &[]); + let completions = do_complete(L!("cat \\0"), CompletionRequestOptions::default()); + assert_eq!(&completions, &[]); + let mut completions = do_complete(L!("cat te\\0"), CompletionRequestOptions::default()); + assert_eq!(&completions, &[]); + + popd(); + completions.clear(); + + // Test abbreviations. + parser.eval( + L!("function testabbrsonetwothreefour; end"), + &IoChain::new(), + ); + with_abbrs_mut(|abbrset| { + abbrset.add(Abbreviation::new( + L!("somename").into(), + L!("testabbrsonetwothreezero").into(), + L!("expansion").into(), + abbrs::Position::Command, + false, + )) + }); + + let completions = complete( + L!("testabbrsonetwothree"), + CompletionRequestOptions::default(), + &parser.context(), + ) + .0; + assert_eq!(completions.len(), 2); + assert_eq!(completions[0].completion, L!("four")); + assert!(!completions[0].flags.contains(CompleteFlags::NO_SPACE)); + // Abbreviations should not have a space after them. + assert_eq!(completions[1].completion, L!("zero")); + assert!(completions[1].flags.contains(CompleteFlags::NO_SPACE)); + with_abbrs_mut(|abbrset| { + abbrset.erase(L!("testabbrsonetwothreezero")); + }); + + // Test wraps. + assert!(comma_join(complete_get_wrap_targets(L!("wrapper1"))).is_empty()); + complete_add_wrapper(L!("wrapper1").into(), L!("wrapper2").into()); + assert_eq!( + comma_join(complete_get_wrap_targets(L!("wrapper1"))), + L!("wrapper2") + ); + complete_add_wrapper(L!("wrapper2").into(), L!("wrapper3").into()); + assert_eq!( + comma_join(complete_get_wrap_targets(L!("wrapper1"))), + L!("wrapper2") + ); + assert_eq!( + comma_join(complete_get_wrap_targets(L!("wrapper2"))), + L!("wrapper3") + ); + complete_add_wrapper(L!("wrapper3").into(), L!("wrapper1").into()); // loop! + assert_eq!( + comma_join(complete_get_wrap_targets(L!("wrapper1"))), + L!("wrapper2") + ); + assert_eq!( + comma_join(complete_get_wrap_targets(L!("wrapper2"))), + L!("wrapper3") + ); + assert_eq!( + comma_join(complete_get_wrap_targets(L!("wrapper3"))), + L!("wrapper1") + ); + complete_remove_wrapper(L!("wrapper1").into(), L!("wrapper2")); + assert!(comma_join(complete_get_wrap_targets(L!("wrapper1"))).is_empty()); + assert_eq!( + comma_join(complete_get_wrap_targets(L!("wrapper2"))), + L!("wrapper3") + ); + assert_eq!( + comma_join(complete_get_wrap_targets(L!("wrapper3"))), + L!("wrapper1") + ); + + // Test cd wrapping chain + pushd("test/complete_test"); + + complete_add_wrapper(L!("cdwrap1").into(), L!("cd").into()); + complete_add_wrapper(L!("cdwrap2").into(), L!("cdwrap1").into()); + + let mut cd_compl = do_complete(L!("cd "), CompletionRequestOptions::default()); + sort_and_prioritize(&mut cd_compl, CompletionRequestOptions::default()); + + let mut cdwrap1_compl = do_complete(L!("cdwrap1 "), CompletionRequestOptions::default()); + sort_and_prioritize(&mut cdwrap1_compl, CompletionRequestOptions::default()); + + let mut cdwrap2_compl = do_complete(L!("cdwrap2 "), CompletionRequestOptions::default()); + sort_and_prioritize(&mut cdwrap2_compl, CompletionRequestOptions::default()); + + let min_compl_size = cd_compl + .len() + .min(cdwrap1_compl.len().min(cdwrap2_compl.len())); + + assert_eq!(cd_compl.len(), min_compl_size); + assert_eq!(cdwrap1_compl.len(), min_compl_size); + assert_eq!(cdwrap2_compl.len(), min_compl_size); + for i in 0..min_compl_size { + assert_eq!(cd_compl[i].completion, cdwrap1_compl[i].completion); + assert_eq!(cdwrap1_compl[i].completion, cdwrap2_compl[i].completion); + } + + complete_remove_wrapper(L!("cdwrap1").into(), L!("cd")); + complete_remove_wrapper(L!("cdwrap2").into(), L!("cdwrap1")); + popd(); +}); + +// Testing test_autosuggest_suggest_special, in particular for properly handling quotes and +// backslashes. +add_test!("test_autosuggest_suggest_special", || { + macro_rules! perform_one_autosuggestion_cd_test { + ($command:literal, $expected:literal, $vars:expr) => { + let mut comps = complete( + L!($command), + CompletionRequestOptions::autosuggest(), + &OperationContext::background($vars, EXPANSION_LIMIT_BACKGROUND), + ) + .0; + + let expects_error = $expected == ""; + + assert_eq!(expects_error, comps.is_empty()); + if !expects_error { + sort_and_prioritize(&mut comps, CompletionRequestOptions::default()); + let suggestion = &comps[0]; + assert_eq!(suggestion.completion, L!($expected)); + } + }; + } + + macro_rules! perform_one_completion_cd_test { + ($command:literal, $expected:literal) => { + let mut comps = complete( + L!($command), + CompletionRequestOptions::default(), + &OperationContext::foreground( + Parser::principal_parser().shared(), + Box::new(no_cancel), + EXPANSION_LIMIT_DEFAULT, + ), + ) + .0; + + let expects_error = $expected == ""; + + assert_eq!(expects_error, comps.is_empty()); + if !expects_error { + sort_and_prioritize(&mut comps, CompletionRequestOptions::default()); + let suggestion = &comps[0]; + assert_eq!(suggestion.completion, L!($expected)); + } + }; + } + + // // We execute LSAN with use_tls=0 under CI to avoid a SIGSEGV crash in LSAN itself. + // // Unfortunately, this causes it to incorrectly flag a memory leak here that doesn't reproduce + // // locally with use_tls=1. + // #ifdef FISH_CI_SAN + // __lsan::ScopedDisabler disable_leak_detection{}; + // #endif + + std::fs::create_dir_all("test/autosuggest_test/0foobar").unwrap(); + std::fs::create_dir_all("test/autosuggest_test/1foo bar").unwrap(); + std::fs::create_dir_all("test/autosuggest_test/2foo bar").unwrap(); + // Cygwin disallows backslashes in filenames. + #[cfg(not(windows))] + std::fs::create_dir_all("test/autosuggest_test/3foo\\bar").unwrap(); + // a path with a single quote + std::fs::create_dir_all("test/autosuggest_test/4foo'bar").unwrap(); + // a path with a double quote + std::fs::create_dir_all("test/autosuggest_test/5foo\"bar").unwrap(); + // This is to ensure tilde expansion is handled. See the `cd ~/test_autosuggest_suggest_specia` + // test below. + // Fake out the home directory + Parser::principal_parser().vars().set_one( + L!("HOME"), + EnvMode::LOCAL | EnvMode::EXPORT, + L!("test/test-home").to_owned(), + ); + std::fs::create_dir_all("test/test-home/test_autosuggest_suggest_special/").unwrap(); + std::fs::create_dir_all("test/autosuggest_test/start/unique2/unique3/multi4").unwrap(); + std::fs::create_dir_all("test/autosuggest_test/start/unique2/unique3/multi42").unwrap(); + std::fs::create_dir_all("test/autosuggest_test/start/unique2/.hiddenDir/moreStuff").unwrap(); + + // Ensure symlink don't cause us to chase endlessly. + std::fs::create_dir_all("test/autosuggest_test/has_loop/loopy").unwrap(); + let _ = std::fs::remove_file("test/autosuggest_test/has_loop/loopy/loop"); + std::os::unix::fs::symlink("../loopy", "test/autosuggest_test/has_loop/loopy/loop").unwrap(); + + let wd = "test/autosuggest_test"; + + let mut vars = PwdEnvironment::default(); + vars.parent.vars.insert( + L!("HOME").into(), + Parser::principal_parser() + .vars() + .get(L!("HOME")) + .unwrap() + .as_string(), + ); + + perform_one_autosuggestion_cd_test!("cd test/autosuggest_test/0", "foobar/", &vars); + perform_one_autosuggestion_cd_test!("cd \"test/autosuggest_test/0", "foobar/", &vars); + perform_one_autosuggestion_cd_test!("cd 'test/autosuggest_test/0", "foobar/", &vars); + perform_one_autosuggestion_cd_test!("cd test/autosuggest_test/1", "foo bar/", &vars); + perform_one_autosuggestion_cd_test!("cd \"test/autosuggest_test/1", "foo bar/", &vars); + perform_one_autosuggestion_cd_test!("cd 'test/autosuggest_test/1", "foo bar/", &vars); + perform_one_autosuggestion_cd_test!("cd test/autosuggest_test/2", "foo bar/", &vars); + perform_one_autosuggestion_cd_test!("cd \"test/autosuggest_test/2", "foo bar/", &vars); + perform_one_autosuggestion_cd_test!("cd 'test/autosuggest_test/2", "foo bar/", &vars); + #[cfg(not(windows))] + { + perform_one_autosuggestion_cd_test!("cd test/autosuggest_test/3", "foo\\bar/", &vars); + perform_one_autosuggestion_cd_test!("cd \"test/autosuggest_test/3", "foo\\bar/", &vars); + perform_one_autosuggestion_cd_test!("cd 'test/autosuggest_test/3", "foo\\bar/", &vars); + } + perform_one_autosuggestion_cd_test!("cd test/autosuggest_test/4", "foo'bar/", &vars); + perform_one_autosuggestion_cd_test!("cd \"test/autosuggest_test/4", "foo'bar/", &vars); + perform_one_autosuggestion_cd_test!("cd 'test/autosuggest_test/4", "foo'bar/", &vars); + perform_one_autosuggestion_cd_test!("cd test/autosuggest_test/5", "foo\"bar/", &vars); + perform_one_autosuggestion_cd_test!("cd \"test/autosuggest_test/5", "foo\"bar/", &vars); + perform_one_autosuggestion_cd_test!("cd 'test/autosuggest_test/5", "foo\"bar/", &vars); + + vars.parent + .vars + .insert(L!("AUTOSUGGEST_TEST_LOC").to_owned(), WString::from_str(wd)); + perform_one_autosuggestion_cd_test!("cd $AUTOSUGGEST_TEST_LOC/0", "foobar/", &vars); + perform_one_autosuggestion_cd_test!("cd ~/test_autosuggest_suggest_specia", "l/", &vars); + + perform_one_autosuggestion_cd_test!( + "cd test/autosuggest_test/start/", + "unique2/unique3/", + &vars + ); + + perform_one_autosuggestion_cd_test!("cd test/autosuggest_test/has_loop/", "loopy/loop/", &vars); + + pushd(wd); + perform_one_autosuggestion_cd_test!("cd 0", "foobar/", &vars); + perform_one_autosuggestion_cd_test!("cd \"0", "foobar/", &vars); + perform_one_autosuggestion_cd_test!("cd '0", "foobar/", &vars); + perform_one_autosuggestion_cd_test!("cd 1", "foo bar/", &vars); + perform_one_autosuggestion_cd_test!("cd \"1", "foo bar/", &vars); + perform_one_autosuggestion_cd_test!("cd '1", "foo bar/", &vars); + perform_one_autosuggestion_cd_test!("cd 2", "foo bar/", &vars); + perform_one_autosuggestion_cd_test!("cd \"2", "foo bar/", &vars); + perform_one_autosuggestion_cd_test!("cd '2", "foo bar/", &vars); + #[cfg(not(windows))] + { + perform_one_autosuggestion_cd_test!("cd 3", "foo\\bar/", &vars); + perform_one_autosuggestion_cd_test!("cd \"3", "foo\\bar/", &vars); + perform_one_autosuggestion_cd_test!("cd '3", "foo\\bar/", &vars); + } + perform_one_autosuggestion_cd_test!("cd 4", "foo'bar/", &vars); + perform_one_autosuggestion_cd_test!("cd \"4", "foo'bar/", &vars); + perform_one_autosuggestion_cd_test!("cd '4", "foo'bar/", &vars); + perform_one_autosuggestion_cd_test!("cd 5", "foo\"bar/", &vars); + perform_one_autosuggestion_cd_test!("cd \"5", "foo\"bar/", &vars); + perform_one_autosuggestion_cd_test!("cd '5", "foo\"bar/", &vars); + + // A single quote should defeat tilde expansion. + perform_one_autosuggestion_cd_test!("cd '~/test_autosuggest_suggest_specia'", "", &vars); + + // Don't crash on ~ (issue #2696). Note this is cwd dependent. + std::fs::create_dir_all("~absolutelynosuchuser/path1/path2/").unwrap(); + perform_one_autosuggestion_cd_test!("cd ~absolutelynosuchus", "er/path1/path2/", &vars); + perform_one_autosuggestion_cd_test!("cd ~absolutelynosuchuser/", "path1/path2/", &vars); + perform_one_completion_cd_test!("cd ~absolutelynosuchus", "er/"); + perform_one_completion_cd_test!("cd ~absolutelynosuchuser/", "path1/"); + + Parser::principal_parser() + .vars() + .remove(L!("HOME"), EnvMode::LOCAL | EnvMode::EXPORT); + popd(); +}); + +add_test!("test_autosuggestion_ignores", || { + // Testing scenarios that should produce no autosuggestions + macro_rules! perform_one_autosuggestion_should_ignore_test { + ($command:literal) => { + let comps = complete( + L!($command), + CompletionRequestOptions::autosuggest(), + &OperationContext::empty(), + ) + .0; + assert_eq!(&comps, &[]); + }; + } + // Do not do file autosuggestions immediately after certain statement terminators - see #1631. + perform_one_autosuggestion_should_ignore_test!("echo PIPE_TEST|"); + perform_one_autosuggestion_should_ignore_test!("echo PIPE_TEST&"); + perform_one_autosuggestion_should_ignore_test!("echo PIPE_TEST#comment"); + perform_one_autosuggestion_should_ignore_test!("echo PIPE_TEST;"); +}); diff --git a/fish-rust/src/tests/env.rs b/fish-rust/src/tests/env.rs new file mode 100644 index 000000000..6cc9f4064 --- /dev/null +++ b/fish-rust/src/tests/env.rs @@ -0,0 +1,105 @@ +use crate::env::{EnvMode, EnvVar, EnvVarFlags, Environment}; +use crate::ffi_tests::add_test; +use crate::parser::Parser; +use crate::wchar::prelude::*; +use crate::wutil::wgetcwd; +use std::collections::HashMap; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use widestring_suffix::widestrs; + +/// An environment built around an std::map. +#[derive(Clone, Default)] +pub struct TestEnvironment { + pub vars: HashMap, +} +impl TestEnvironment { + pub fn new() -> Self { + Self::default() + } +} +impl Environment for TestEnvironment { + fn getf(&self, name: &wstr, _mode: EnvMode) -> Option { + self.vars + .get(name) + .map(|value| EnvVar::new(value.clone(), EnvVarFlags::default())) + } + fn get_names(&self, _flags: EnvMode) -> Vec { + self.vars.keys().cloned().collect() + } +} + +/// A test environment that knows about PWD. +#[derive(Default)] +pub struct PwdEnvironment { + pub parent: TestEnvironment, +} +impl PwdEnvironment { + pub fn new() -> Self { + Self::default() + } +} +#[widestrs] +impl Environment for PwdEnvironment { + fn getf(&self, name: &wstr, mode: EnvMode) -> Option { + if name == "PWD"L { + return Some(EnvVar::new(wgetcwd(), EnvVarFlags::default())); + } + self.parent.getf(name, mode) + } + + fn get_names(&self, flags: EnvMode) -> Vec { + let mut res = self.parent.get_names(flags); + if !res.iter().any(|n| n == "PWD"L) { + res.push("PWD"L.to_owned()); + } + res + } +} + +/// Helper for test_timezone_env_vars(). +fn return_timezone_hour(tstamp: SystemTime, timezone: &wstr) -> libc::c_int { + let vars = Parser::principal_parser().vars(); + + vars.set_one(L!("TZ"), EnvMode::EXPORT, timezone.to_owned()); + + let _var = vars.get(L!("TZ")); + + let tstamp: libc::time_t = tstamp + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() + .try_into() + .unwrap(); + let mut local_time: libc::tm = unsafe { std::mem::zeroed() }; + unsafe { libc::localtime_r(&tstamp, &mut local_time) }; + + local_time.tm_hour +} + +/// Verify that setting TZ calls tzset() in the current shell process. +fn test_timezone_env_vars() { + // Confirm changing the timezone affects fish's idea of the local time. + let tstamp = SystemTime::now(); + + let first_tstamp = return_timezone_hour(tstamp, L!("UTC-1")); + let second_tstamp = return_timezone_hour(tstamp, L!("UTC-2")); + let delta = second_tstamp - first_tstamp; + assert!(delta == 1 || delta == -23); +} + +// Verify that setting special env vars have the expected effect on the current shell process. +add_test!("test_env_vars", || { + test_timezone_env_vars(); + // TODO: Add tests for the locale and ncurses vars. + + let v1 = EnvVar::new(L!("abc").to_owned(), EnvVarFlags::EXPORT); + let v2 = EnvVar::new_vec(vec![L!("abc").to_owned()], EnvVarFlags::EXPORT); + let v3 = EnvVar::new_vec(vec![L!("abc").to_owned()], EnvVarFlags::empty()); + let v4 = EnvVar::new_vec( + vec![L!("abc").to_owned(), L!("def").to_owned()], + EnvVarFlags::EXPORT, + ); + assert!(v1 == v2 && !(v1 != v2)); + assert!(v1 != v3 && !(v1 == v3)); + assert!(v1 != v4 && !(v1 == v4)); +}); diff --git a/fish-rust/src/tests/env_universal_common.rs b/fish-rust/src/tests/env_universal_common.rs new file mode 100644 index 000000000..f0cc81157 --- /dev/null +++ b/fish-rust/src/tests/env_universal_common.rs @@ -0,0 +1,294 @@ +use crate::common::wcs2osstring; +use crate::env::{EnvVar, EnvVarFlags, VarTable}; +use crate::env_universal_common::{CallbackDataList, EnvUniversal, UvarFormat}; +use crate::ffi_tests::add_test; +use crate::flog::FLOG; +use crate::threads::{iothread_drain_all, iothread_perform}; +use crate::wchar::prelude::*; +use crate::wutil::file_id_for_path; +use crate::wutil::INVALID_FILE_ID; + +const UVARS_PER_THREAD: usize = 8; +const UVARS_TEST_PATH: &wstr = L!("test/fish_uvars_test/varsfile.txt"); + +fn test_universal_helper(x: usize) { + let mut callbacks = CallbackDataList::new(); + let mut uvars = EnvUniversal::new(); + uvars.initialize_at_path(&mut callbacks, UVARS_TEST_PATH.to_owned()); + + for j in 0..UVARS_PER_THREAD { + let key = sprintf!("key_%d_%d", x, j); + let val = sprintf!("val_%d_%d", x, j); + uvars.set(&key, EnvVar::new(val, EnvVarFlags::empty())); + let synced = uvars.sync(&mut callbacks); + assert!( + synced, + "Failed to sync universal variables after modification" + ); + } + + // Last step is to delete the first key. + uvars.remove(&sprintf!("key_%d_%d", x, 0)); + let synced = uvars.sync(&mut callbacks); + assert!(synced, "Failed to sync universal variables after deletion"); +} + +add_test!("test_universal", || { + let _ = std::fs::remove_dir_all("test/fish_uvars_test/"); + std::fs::create_dir_all("test/fish_uvars_test/").unwrap(); + + let threads = 1; + for i in 0..threads { + iothread_perform(move || test_universal_helper(i)); + } + unsafe { iothread_drain_all() }; + + let mut uvars = EnvUniversal::new(); + let mut callbacks = CallbackDataList::new(); + uvars.initialize_at_path(&mut callbacks, UVARS_TEST_PATH.to_owned()); + + for i in 0..threads { + for j in 0..UVARS_PER_THREAD { + let key = sprintf!("key_%d_%d", i, j); + let expected_val = if j == 0 { + None + } else { + Some(EnvVar::new( + sprintf!("val_%d_%d", i, j), + EnvVarFlags::empty(), + )) + }; + let var = uvars.get(&key); + assert_eq!(var, expected_val); + } + } + + std::fs::remove_dir_all("test/fish_uvars_test/").unwrap(); +}); + +add_test!("test_universal_output", || { + let flag_export = EnvVarFlags::EXPORT; + let flag_pathvar = EnvVarFlags::PATHVAR; + + let mut vars = VarTable::new(); + vars.insert( + L!("varA").to_owned(), + EnvVar::new_vec( + vec![L!("ValA1").to_owned(), L!("ValA2").to_owned()], + EnvVarFlags::empty(), + ), + ); + vars.insert( + L!("varB").to_owned(), + EnvVar::new_vec(vec![L!("ValB1").to_owned()], flag_export), + ); + vars.insert( + L!("varC").to_owned(), + EnvVar::new_vec(vec![L!("ValC1").to_owned()], EnvVarFlags::empty()), + ); + vars.insert( + L!("varD").to_owned(), + EnvVar::new_vec(vec![L!("ValD1").to_owned()], flag_export | flag_pathvar), + ); + vars.insert( + L!("varE").to_owned(), + EnvVar::new_vec( + vec![L!("ValE1").to_owned(), L!("ValE2").to_owned()], + flag_pathvar, + ), + ); + + let text = EnvUniversal::serialize_with_vars(&vars); + let expected = concat!( + "# This file contains fish universal variable definitions.\n", + "# VERSION: 3.0\n", + "SETUVAR varA:ValA1\\x1eValA2\n", + "SETUVAR --export varB:ValB1\n", + "SETUVAR varC:ValC1\n", + "SETUVAR --export --path varD:ValD1\n", + "SETUVAR --path varE:ValE1\\x1eValE2\n", + ) + .as_bytes(); + assert_eq!(text, expected); +}); + +fn test_universal_parsing() { + let input = concat!( + "# This file contains fish universal variable definitions.\n", + "# VERSION: 3.0\n", + "SETUVAR varA:ValA1\\x1eValA2\n", + "SETUVAR --export varB:ValB1\n", + "SETUVAR --nonsenseflag varC:ValC1\n", + "SETUVAR --export --path varD:ValD1\n", + "SETUVAR --path --path varE:ValE1\\x1eValE2\n", + ) + .as_bytes(); + + let flag_export = EnvVarFlags::EXPORT; + let flag_pathvar = EnvVarFlags::PATHVAR; + + let mut vars = VarTable::new(); + + vars.insert( + L!("varA").to_owned(), + EnvVar::new_vec( + vec![L!("ValA1").to_owned(), L!("ValA2").to_owned()], + EnvVarFlags::empty(), + ), + ); + vars.insert( + L!("varB").to_owned(), + EnvVar::new_vec(vec![L!("ValB1").to_owned()], flag_export), + ); + vars.insert( + L!("varC").to_owned(), + EnvVar::new_vec(vec![L!("ValC1").to_owned()], EnvVarFlags::empty()), + ); + vars.insert( + L!("varD").to_owned(), + EnvVar::new_vec(vec![L!("ValD1").to_owned()], flag_export | flag_pathvar), + ); + vars.insert( + L!("varE").to_owned(), + EnvVar::new_vec( + vec![L!("ValE1").to_owned(), L!("ValE2").to_owned()], + flag_pathvar, + ), + ); + + let mut parsed_vars = VarTable::new(); + EnvUniversal::populate_variables(input, &mut parsed_vars); + assert_eq!(vars, parsed_vars); +} + +add_test!("test_universal_parsing_legacy", || { + let input = concat!( + "# This file contains fish universal variable definitions.\n", + "SET varA:ValA1\\x1eValA2\n", + "SET_EXPORT varB:ValB1\n", + ) + .as_bytes(); + + let mut vars = VarTable::new(); + vars.insert( + L!("varA").to_owned(), + EnvVar::new_vec( + vec![L!("ValA1").to_owned(), L!("ValA2").to_owned()], + EnvVarFlags::empty(), + ), + ); + vars.insert( + L!("varB").to_owned(), + EnvVar::new(L!("ValB1").to_owned(), EnvVarFlags::EXPORT), + ); + + let mut parsed_vars = VarTable::new(); + EnvUniversal::populate_variables(input, &mut parsed_vars); + assert_eq!(vars, parsed_vars); +}); + +add_test!("test_universal_callbacks", || { + std::fs::create_dir_all("test/fish_uvars_test/").unwrap(); + let mut callbacks = CallbackDataList::new(); + let mut uvars1 = EnvUniversal::new(); + let mut uvars2 = EnvUniversal::new(); + uvars1.initialize_at_path(&mut callbacks, UVARS_TEST_PATH.to_owned()); + uvars2.initialize_at_path(&mut callbacks, UVARS_TEST_PATH.to_owned()); + + let noflags = EnvVarFlags::empty(); + + // Put some variables into both. + uvars1.set(L!("alpha"), EnvVar::new(L!("1").to_owned(), noflags)); // + uvars1.set(L!("beta"), EnvVar::new(L!("1").to_owned(), noflags)); // + uvars1.set(L!("delta"), EnvVar::new(L!("1").to_owned(), noflags)); // + uvars1.set(L!("epsilon"), EnvVar::new(L!("1").to_owned(), noflags)); // + uvars1.set(L!("lambda"), EnvVar::new(L!("1").to_owned(), noflags)); // + uvars1.set(L!("kappa"), EnvVar::new(L!("1").to_owned(), noflags)); // + uvars1.set(L!("omicron"), EnvVar::new(L!("1").to_owned(), noflags)); // + + uvars1.sync(&mut callbacks); + uvars2.sync(&mut callbacks); + + // Change uvars1. + uvars1.set(L!("alpha"), EnvVar::new(L!("2").to_owned(), noflags)); // changes value + uvars1.set( + L!("beta"), + EnvVar::new(L!("1").to_owned(), EnvVarFlags::EXPORT), + ); // changes export + uvars1.remove(L!("delta")); // erases value + uvars1.set(L!("epsilon"), EnvVar::new(L!("1").to_owned(), noflags)); // changes nothing + uvars1.sync(&mut callbacks); + + // Change uvars2. It should treat its value as correct and ignore changes from uvars1. + uvars2.set(L!("lambda"), EnvVar::new(L!("1").to_owned(), noflags)); // same value + uvars2.set(L!("kappa"), EnvVar::new(L!("2").to_owned(), noflags)); // different value + + // Now see what uvars2 sees. + callbacks.clear(); + uvars2.sync(&mut callbacks); + + // Sort them to get them in a predictable order. + callbacks.sort_by(|a, b| a.key.cmp(&b.key)); + + // Should see exactly three changes. + assert_eq!(callbacks.len(), 3); + assert_eq!(callbacks[0].key, L!("alpha")); + assert_eq!(callbacks[0].val.as_ref().unwrap().as_string(), L!("2")); + assert_eq!(callbacks[1].key, L!("beta")); + assert_eq!(callbacks[1].val.as_ref().unwrap().as_string(), L!("1")); + assert_eq!(callbacks[2].key, L!("delta")); + assert_eq!(callbacks[2].val, None); + std::fs::remove_dir_all("test/fish_uvars_test/").unwrap(); +}); + +add_test!("test_universal_formats", || { + macro_rules! validate { + ( $version_line:literal, $expected_format:expr ) => { + assert_eq!( + EnvUniversal::format_for_contents($version_line), + $expected_format + ); + }; + } + validate!(b"# VERSION: 3.0", UvarFormat::fish_3_0); + validate!(b"# version: 3.0", UvarFormat::fish_2_x); + validate!(b"# blah blahVERSION: 3.0", UvarFormat::fish_2_x); + validate!(b"stuff\n# blah blahVERSION: 3.0", UvarFormat::fish_2_x); + validate!(b"# blah\n# VERSION: 3.0", UvarFormat::fish_3_0); + validate!(b"# blah\n#VERSION: 3.0", UvarFormat::fish_3_0); + validate!(b"# blah\n#VERSION:3.0", UvarFormat::fish_3_0); + validate!(b"# blah\n#VERSION:3.1", UvarFormat::future); +}); + +add_test!("test_universal_ok_to_save", || { + // Ensure we don't try to save after reading from a newer fish. + std::fs::create_dir_all("test/fish_uvars_test/").unwrap(); + let contents = b"# VERSION: 99999.99\n"; + std::fs::write(wcs2osstring(UVARS_TEST_PATH), contents).unwrap(); + + let before_id = file_id_for_path(UVARS_TEST_PATH); + assert_ne!( + before_id, INVALID_FILE_ID, + "UVARS_TEST_PATH should be readable" + ); + + let mut cbs = CallbackDataList::new(); + let mut uvars = EnvUniversal::new(); + uvars.initialize_at_path(&mut cbs, UVARS_TEST_PATH.to_owned()); + assert!(!uvars.is_ok_to_save(), "Should not be OK to save"); + uvars.sync(&mut cbs); + cbs.clear(); + assert!(!uvars.is_ok_to_save(), "Should still not be OK to save"); + uvars.set( + L!("SOMEVAR"), + EnvVar::new(L!("SOMEVALUE").to_owned(), EnvVarFlags::empty()), + ); + + // Ensure file is same. + let after_id = file_id_for_path(UVARS_TEST_PATH); + assert_eq!( + before_id, after_id, + "UVARS_TEST_PATH should not have changed", + ); + std::fs::remove_dir_all("test/fish_uvars_test/").unwrap(); +}); diff --git a/fish-rust/src/tests/expand.rs b/fish-rust/src/tests/expand.rs new file mode 100644 index 000000000..cd14f511f --- /dev/null +++ b/fish-rust/src/tests/expand.rs @@ -0,0 +1,447 @@ +use crate::abbrs::Abbreviation; +use crate::abbrs::{self}; +use crate::abbrs::{with_abbrs, with_abbrs_mut}; +use crate::complete::{CompletionList, CompletionReceiver}; +use crate::env::{EnvMode, EnvStackSetResult}; +use crate::expand::{expand_to_receiver, ExpandResultCode}; +use crate::ffi_tests::add_test; +use crate::operation_context::{no_cancel, EXPANSION_LIMIT_DEFAULT}; +use crate::parse_constants::ParseErrorList; +use crate::parser::Parser; +use crate::tests::prelude::*; +use crate::wildcard::ANY_STRING; +use crate::{ + expand::{expand_string, ExpandFlags}, + operation_context::OperationContext, + wchar::prelude::*, +}; +use std::collections::hash_map::RandomState; +use std::collections::HashSet; + +fn expand_test_impl( + input: &wstr, + flags: ExpandFlags, + expected: Vec, + error_message: Option<&str>, +) { + let mut output = CompletionList::new(); + let mut errors = ParseErrorList::new(); + let pwd = PwdEnvironment::default(); + let ctx = OperationContext::test_only_foreground( + Parser::principal_parser().shared(), + &pwd, + Box::new(no_cancel), + ); + + if expand_string( + input.to_owned(), + &mut output, + flags, + &ctx, + Some(&mut errors), + ) == ExpandResultCode::error + { + assert_ne!( + errors, + vec![], + "Bug: Parse error reported but no error text found." + ); + panic!( + "{}", + errors[0].describe(input, ctx.parser().is_interactive()) + ); + } + + let expected_set: HashSet = HashSet::from_iter(expected); + let output_set = HashSet::from_iter(output.into_iter().map(|c| c.completion)); + assert_eq!( + expected_set, + output_set, + "{}", + error_message.unwrap_or("expand mismatch") + ); +} + +// Test globbing and other parameter expansion. +add_test!("test_expand", || { + /// Perform parameter expansion and test if the output equals the zero-terminated parameter list /// supplied. + /// + /// \param in the string to expand + /// \param flags the flags to send to expand_string + /// \param ... A zero-terminated parameter list of values to test. + /// After the zero terminator comes one more arg, a string, which is the error + /// message to print if the test fails. + macro_rules! expand_test { + ($input:expr, $flags:expr, ( $($expected:expr),* $(,)? )) => { + expand_test_impl(L!($input), $flags, vec![$( $expected.into(), )*], None) + }; + ($input:expr, $flags:expr, ( $($expected:expr),* $(,)? ), $error:literal) => { + expand_test_impl(L!($input), $flags, vec![$( $expected.into(), )*], Some($error)) + }; + ($input:expr, $flags:expr, $expected:expr) => { + expand_test_impl(L!($input), $flags, vec![ $expected.into() ], None) + }; + ($input:expr, $flags:expr, $expected:expr, $error:literal) => { + expand_test_impl(L!($input), $flags, vec![ $expected.into() ], Some($error)) + }; + } + + // Testing parameter expansion + let noflags = ExpandFlags::default(); + + expand_test!("foo", noflags, "foo", "Strings do not expand to themselves"); + + expand_test!( + "a{b,c,d}e", + noflags, + ("abe", "ace", "ade"), + "Bracket expansion is broken" + ); + expand_test!( + "a*", + ExpandFlags::SKIP_WILDCARDS, + "a*", + "Cannot skip wildcard expansion" + ); + expand_test!( + "/bin/l\\0", + ExpandFlags::FOR_COMPLETIONS, + (), + "Failed to handle null escape in expansion" + ); + expand_test!( + "foo\\$bar", + ExpandFlags::SKIP_VARIABLES, + "foo$bar", + "Failed to handle dollar sign in variable-skipping expansion" + ); + + // bb + // x + // bar + // baz + // xxx + // yyy + // bax + // xxx + // lol + // nub + // q + // .foo + // aaa + // aaa2 + // x + std::fs::create_dir_all("test/fish_expand_test/").unwrap(); + std::fs::create_dir_all("test/fish_expand_test/bb/").unwrap(); + std::fs::create_dir_all("test/fish_expand_test/baz/").unwrap(); + std::fs::create_dir_all("test/fish_expand_test/bax/").unwrap(); + std::fs::create_dir_all("test/fish_expand_test/lol/nub/").unwrap(); + std::fs::create_dir_all("test/fish_expand_test/aaa/").unwrap(); + std::fs::create_dir_all("test/fish_expand_test/aaa2/").unwrap(); + std::fs::write("test/fish_expand_test/.foo", []).unwrap(); + std::fs::write("test/fish_expand_test/bb/x", []).unwrap(); + std::fs::write("test/fish_expand_test/bar", []).unwrap(); + std::fs::write("test/fish_expand_test/bax/xxx", []).unwrap(); + std::fs::write("test/fish_expand_test/baz/xxx", []).unwrap(); + std::fs::write("test/fish_expand_test/baz/yyy", []).unwrap(); + std::fs::write("test/fish_expand_test/lol/nub/q", []).unwrap(); + std::fs::write("test/fish_expand_test/aaa2/x", []).unwrap(); + + // This is checking that .* does NOT match . and .. + // (https://github.com/fish-shell/fish-shell/issues/270). But it does have to match literal + // components (e.g. "./*" has to match the same as "*". + expand_test!( + "test/fish_expand_test/.*", + noflags, + "test/fish_expand_test/.foo", + "Expansion not correctly handling dotfiles" + ); + + expand_test!( + "test/fish_expand_test/./.*", + noflags, + "test/fish_expand_test/./.foo", + "Expansion not correctly handling literal path components in dotfiles" + ); + + expand_test!( + "test/fish_expand_test/*/xxx", + noflags, + ( + "test/fish_expand_test/bax/xxx", + "test/fish_expand_test/baz/xxx" + ), + "Glob did the wrong thing 1" + ); + + expand_test!( + "test/fish_expand_test/*z/xxx", + noflags, + "test/fish_expand_test/baz/xxx", + "Glob did the wrong thing 2" + ); + + expand_test!( + "test/fish_expand_test/**z/xxx", + noflags, + "test/fish_expand_test/baz/xxx", + "Glob did the wrong thing 3" + ); + + expand_test!( + "test/fish_expand_test////baz/xxx", + noflags, + "test/fish_expand_test////baz/xxx", + "Glob did the wrong thing 3" + ); + + expand_test!( + "test/fish_expand_test/b**", + noflags, + ( + "test/fish_expand_test/bb", + "test/fish_expand_test/bb/x", + "test/fish_expand_test/bar", + "test/fish_expand_test/bax", + "test/fish_expand_test/bax/xxx", + "test/fish_expand_test/baz", + "test/fish_expand_test/baz/xxx", + "test/fish_expand_test/baz/yyy" + ), + "Glob did the wrong thing 4" + ); + + // A trailing slash should only produce directories. + expand_test!( + "test/fish_expand_test/b*/", + noflags, + ( + "test/fish_expand_test/bb/", + "test/fish_expand_test/baz/", + "test/fish_expand_test/bax/" + ), + "Glob did the wrong thing 5" + ); + + expand_test!( + "test/fish_expand_test/b**/", + noflags, + ( + "test/fish_expand_test/bb/", + "test/fish_expand_test/baz/", + "test/fish_expand_test/bax/" + ), + "Glob did the wrong thing 6" + ); + + expand_test!( + "test/fish_expand_test/**/q", + noflags, + "test/fish_expand_test/lol/nub/q", + "Glob did the wrong thing 7" + ); + + expand_test!( + "test/fish_expand_test/BA", + ExpandFlags::FOR_COMPLETIONS, + ( + "test/fish_expand_test/bar", + "test/fish_expand_test/bax/", + "test/fish_expand_test/baz/" + ), + "Case insensitive test did the wrong thing" + ); + + expand_test!( + "test/fish_expand_test/BA", + ExpandFlags::FOR_COMPLETIONS, + ( + "test/fish_expand_test/bar", + "test/fish_expand_test/bax/", + "test/fish_expand_test/baz/" + ), + "Case insensitive test did the wrong thing" + ); + + expand_test!( + "test/fish_expand_test/bb/yyy", + ExpandFlags::FOR_COMPLETIONS, + (), /* nothing! */ + "Wrong fuzzy matching 1" + ); + + expand_test!( + "test/fish_expand_test/bb/x", + ExpandFlags::FOR_COMPLETIONS | ExpandFlags::FUZZY_MATCH, + "", + // we just expect the empty string since this is an exact match + "Wrong fuzzy matching 2" + ); + + // Some vswprintfs refuse to append ANY_STRING in a format specifiers, so don't use + // format_string here. + let fuzzy_comp = ExpandFlags::FOR_COMPLETIONS | ExpandFlags::FUZZY_MATCH; + let any_str_str = ANY_STRING.to_string(); + expand_test!( + "test/fish_expand_test/b/xx*", + fuzzy_comp, + ( + (String::from("test/fish_expand_test/bax/xx") + &any_str_str), + (String::from("test/fish_expand_test/baz/xx") + &any_str_str) + ), + "Wrong fuzzy matching 3" + ); + + expand_test!( + "test/fish_expand_test/b/yyy", + fuzzy_comp, + "test/fish_expand_test/baz/yyy", + "Wrong fuzzy matching 4" + ); + + expand_test!( + "test/fish_expand_test/aa/x", + fuzzy_comp, + "test/fish_expand_test/aaa2/x", + "Wrong fuzzy matching 5" + ); + + expand_test!( + "test/fish_expand_test/aaa/x", + fuzzy_comp, + (), + "Wrong fuzzy matching 6 - shouldn't remove valid directory names (#3211)" + ); + + // Dotfiles + expand_test!( + "test/fish_expand_test/.*", + noflags, + "test/fish_expand_test/.foo", + "" + ); + + // Literal path components in dotfiles. + expand_test!( + "test/fish_expand_test/./.*", + noflags, + "test/fish_expand_test/./.foo", + "" + ); + + pushd("test/fish_expand_test"); + + expand_test!( + "b/xx", + fuzzy_comp, + ("bax/xxx", "baz/xxx"), + "Wrong fuzzy matching 5" + ); + + // multiple slashes with fuzzy matching - #3185 + expand_test!("l///n", fuzzy_comp, "lol///nub/", "Wrong fuzzy matching 6"); + + popd(); +}); + +add_test!("test_expand_overflow", || { + // Testing overflowing expansions + // Ensure that we have sane limits on number of expansions - see #7497. + + // Make a list of 64 elements, then expand it cartesian-style 64 times. + // This is far too large to expand. + let vals: Vec = (1..=64).map(|i| i.to_wstring()).collect(); + let expansion = WString::from_str(&str::repeat("$bigvar", 64)); + + let parser = Parser::principal_parser().shared(); + parser.vars().push(true); + let set = parser.vars().set(L!("bigvar"), EnvMode::LOCAL, vals); + assert_eq!(set, EnvStackSetResult::ENV_OK); + + let mut errors = ParseErrorList::new(); + let ctx = + OperationContext::foreground(parser.clone(), Box::new(no_cancel), EXPANSION_LIMIT_DEFAULT); + + // We accept only 1024 completions. + let mut output = CompletionReceiver::new(1024); + + let res = expand_to_receiver( + expansion, + &mut output, + ExpandFlags::default(), + &ctx, + Some(&mut errors), + ); + assert_ne!(errors, vec![]); + assert_eq!(res, ExpandResultCode::error); + + parser.vars().pop(); +}); + +add_test!("test_abbreviations", || { + // Testing abbreviations + + with_abbrs_mut(|abbrset| { + abbrset.add(Abbreviation::new( + L!("gc").to_owned(), + L!("gc").to_owned(), + L!("git checkout").to_owned(), + abbrs::Position::Command, + false, + )); + abbrset.add(Abbreviation::new( + L!("foo").to_owned(), + L!("foo").to_owned(), + L!("bar").to_owned(), + abbrs::Position::Command, + false, + )); + abbrset.add(Abbreviation::new( + L!("gx").to_owned(), + L!("gx").to_owned(), + L!("git checkout").to_owned(), + abbrs::Position::Command, + false, + )); + abbrset.add(Abbreviation::new( + L!("yin").to_owned(), + L!("yin").to_owned(), + L!("yang").to_owned(), + abbrs::Position::Anywhere, + false, + )); + }); + + // Helper to expand an abbreviation, enforcing we have no more than one result. + let abbr_expand_1 = |token, pos| -> Option { + let result = with_abbrs(|abbrset| abbrset.r#match(token, pos)); + if result.is_empty() { + return None; + } + assert_eq!( + &result[1..], + &[], + "abbreviation expansion for {token} returned more than 1 result" + ); + Some(result.into_iter().next().unwrap().replacement) + }; + + let cmd = abbrs::Position::Command; + assert!( + abbr_expand_1(L!(""), cmd).is_none(), + "Unexpected success with empty abbreviation" + ); + assert!( + abbr_expand_1(L!("nothing"), cmd).is_none(), + "Unexpected success with missing abbreviation" + ); + + assert_eq!( + abbr_expand_1(L!("gc"), cmd), + Some(L!("git checkout").into()) + ); + + assert_eq!(abbr_expand_1(L!("foo"), cmd), Some(L!("bar").into())); + + // todo!("port the rest"); +}); diff --git a/fish-rust/src/tests/highlight.rs b/fish-rust/src/tests/highlight.rs new file mode 100644 index 000000000..1b826546c --- /dev/null +++ b/fish-rust/src/tests/highlight.rs @@ -0,0 +1,637 @@ +use crate::common::ScopeGuard; +use crate::env::EnvMode; +use crate::ffi_tests::add_test; +use crate::future_feature_flags::{self, FeatureFlag}; +use crate::parser::Parser; +use crate::tests::prelude::*; +use crate::wchar::prelude::*; +use crate::{ + env::EnvStack, + highlight::{ + highlight_shell, is_potential_path, HighlightColorResolver, HighlightRole, HighlightSpec, + PathFlags, + }, + operation_context::{OperationContext, EXPANSION_LIMIT_BACKGROUND, EXPANSION_LIMIT_DEFAULT}, +}; +use libc::PATH_MAX; + +// Helper to return a string whose length greatly exceeds PATH_MAX. +fn get_overlong_path() -> String { + let path_max = usize::try_from(PATH_MAX).unwrap(); + let mut longpath = String::with_capacity(path_max * 2 + 10); + while longpath.len() <= path_max * 2 { + longpath += "/overlong"; + } + longpath +} + +add_test!("test_is_potential_path", || { + // Directories + std::fs::create_dir_all("test/is_potential_path_test/alpha/").unwrap(); + std::fs::create_dir_all("test/is_potential_path_test/beta/").unwrap(); + + // Files + std::fs::write("test/is_potential_path_test/aardvark", []).unwrap(); + std::fs::write("test/is_potential_path_test/gamma", []).unwrap(); + + let wd = L!("test/is_potential_path_test/").to_owned(); + let wds = vec![L!(".").to_owned(), wd]; + + let vars = EnvStack::principal().clone(); + let ctx = OperationContext::background(&*vars, EXPANSION_LIMIT_DEFAULT); + + assert!(is_potential_path( + L!("al"), + true, + &wds[..], + &ctx, + PathFlags::PATH_REQUIRE_DIR + )); + + assert!(is_potential_path( + L!("alpha/"), + true, + &wds[..], + &ctx, + PathFlags::PATH_REQUIRE_DIR + )); + assert!(is_potential_path( + L!("aard"), + true, + &wds[..], + &ctx, + PathFlags::empty() + )); + assert!(!is_potential_path( + L!("aard"), + false, + &wds[..], + &ctx, + PathFlags::empty() + )); + assert!(!is_potential_path( + L!("alp/"), + true, + &wds[..], + &ctx, + PathFlags::PATH_REQUIRE_DIR | PathFlags::PATH_FOR_CD + )); + + assert!(!is_potential_path( + L!("balpha/"), + true, + &wds[..], + &ctx, + PathFlags::PATH_REQUIRE_DIR + )); + assert!(!is_potential_path( + L!("aard"), + true, + &wds[..], + &ctx, + PathFlags::PATH_REQUIRE_DIR + )); + assert!(!is_potential_path( + L!("aarde"), + true, + &wds[..], + &ctx, + PathFlags::PATH_REQUIRE_DIR + )); + assert!(!is_potential_path( + L!("aarde"), + true, + &wds[..], + &ctx, + PathFlags::empty() + )); + + assert!(is_potential_path( + L!("test/is_potential_path_test/aardvark"), + true, + &wds[..], + &ctx, + PathFlags::empty() + )); + assert!(is_potential_path( + L!("test/is_potential_path_test/al"), + true, + &wds[..], + &ctx, + PathFlags::PATH_REQUIRE_DIR + )); + assert!(is_potential_path( + L!("test/is_potential_path_test/aardv"), + true, + &wds[..], + &ctx, + PathFlags::empty() + )); + + assert!(!is_potential_path( + L!("test/is_potential_path_test/aardvark"), + true, + &wds[..], + &ctx, + PathFlags::PATH_REQUIRE_DIR + )); + assert!(!is_potential_path( + L!("test/is_potential_path_test/al/"), + true, + &wds[..], + &ctx, + PathFlags::empty() + )); + assert!(!is_potential_path( + L!("test/is_potential_path_test/ar"), + true, + &wds[..], + &ctx, + PathFlags::empty() + )); + assert!(is_potential_path( + L!("/usr"), + true, + &wds[..], + &ctx, + PathFlags::PATH_REQUIRE_DIR + )); +}); + +add_test!("test_highlighting", || { + // Testing syntax highlighting + pushd("test/fish_highlight_test/"); + let _popd = ScopeGuard::new((), |_| popd()); + std::fs::create_dir_all("dir").unwrap(); + std::fs::create_dir_all("cdpath-entry/dir-in-cdpath").unwrap(); + std::fs::write("foo", []).unwrap(); + std::fs::write("bar", []).unwrap(); + + // Here are the components of our source and the colors we expect those to be. + #[derive(Debug)] + struct HighlightComponent<'a> { + text: &'a str, + color: HighlightSpec, + nospace: bool, + } + + macro_rules! component { + ( ( $text:expr, $color:expr) ) => { + HighlightComponent { + text: $text, + color: $color, + nospace: false, + } + }; + ( ( $text:literal, $color:expr, ns ) ) => { + HighlightComponent { + text: $text, + color: $color, + nospace: true, + } + }; + } + + macro_rules! validate { + ( $($comp:tt),* $(,)? ) => { + let components = [ + $( + component!($comp), + )* + ]; + let vars = Parser::principal_parser().vars(); + // Generate the text. + let mut text = WString::new(); + let mut expected_colors = vec![]; + for comp in &components { + if !text.is_empty() && !comp.nospace { + text.push(' '); + expected_colors.push(HighlightSpec::new()); + } + text.push_str(comp.text); + expected_colors.resize(text.len(), comp.color); + } + assert_eq!(text.len(), expected_colors.len()); + + let mut colors = vec![]; + highlight_shell( + &text, + &mut colors, + &OperationContext::background(vars, EXPANSION_LIMIT_BACKGROUND), + true, /* io_ok */ + Some(text.len()), + ); + assert_eq!(colors.len(), expected_colors.len()); + + for (i, c) in text.chars().enumerate() { + // Hackish space handling. We don't care about the colors in spaces. + if c == ' ' { + continue; + } + + assert_eq!(colors[i], expected_colors[i], "Failed at position {i}, char {c}"); + } + }; + } + + let mut param_valid_path = HighlightSpec::with_fg(HighlightRole::param); + param_valid_path.valid_path = true; + + let saved_flag = future_feature_flags::test(FeatureFlag::ampersand_nobg_in_token); + future_feature_flags::set(FeatureFlag::ampersand_nobg_in_token, true); + let _restore_saved_flag = ScopeGuard::new((), |_| { + future_feature_flags::set(FeatureFlag::ampersand_nobg_in_token, saved_flag); + }); + + let fg = HighlightSpec::with_fg; + + // Verify variables and wildcards in commands using /bin/cat. + let vars = Parser::principal_parser().vars(); + vars.set_one( + L!("CDPATH"), + EnvMode::LOCAL, + L!("./cdpath-entry").to_owned(), + ); + + vars.set_one( + L!("VARIABLE_IN_COMMAND"), + EnvMode::LOCAL, + L!("a").to_owned(), + ); + vars.set_one( + L!("VARIABLE_IN_COMMAND2"), + EnvMode::LOCAL, + L!("at").to_owned(), + ); + + let _cleanup = ScopeGuard::new((), |_| { + vars.remove(L!("VARIABLE_IN_COMMAND"), EnvMode::default()); + vars.remove(L!("VARIABLE_IN_COMMAND2"), EnvMode::default()); + }); + + validate!( + ("echo", fg(HighlightRole::command)), + ("./foo", param_valid_path), + ("&", fg(HighlightRole::statement_terminator)), + ); + + validate!( + ("command", fg(HighlightRole::keyword)), + ("echo", fg(HighlightRole::command)), + ("abc", fg(HighlightRole::param)), + ("foo", param_valid_path), + ("&", fg(HighlightRole::statement_terminator)), + ); + + validate!( + ("echo", fg(HighlightRole::command)), + ("foo&bar", fg(HighlightRole::param)), + ("foo", fg(HighlightRole::param), ns), + ("&", fg(HighlightRole::statement_terminator)), + ("echo", fg(HighlightRole::command)), + ("&>", fg(HighlightRole::redirection)), + ); + + validate!( + ("if command", fg(HighlightRole::keyword)), + ("ls", fg(HighlightRole::command)), + ("; ", fg(HighlightRole::statement_terminator)), + ("echo", fg(HighlightRole::command)), + ("abc", fg(HighlightRole::param)), + ("; ", fg(HighlightRole::statement_terminator)), + ("/bin/definitely_not_a_command", fg(HighlightRole::error)), + ("; ", fg(HighlightRole::statement_terminator)), + ("end", fg(HighlightRole::keyword)), + ); + + // Verify that cd shows errors for non-directories. + validate!( + ("cd", fg(HighlightRole::command)), + ("dir", param_valid_path), + ); + + validate!( + ("cd", fg(HighlightRole::command)), + ("foo", fg(HighlightRole::error)), + ); + + validate!( + ("cd", fg(HighlightRole::command)), + ("--help", fg(HighlightRole::option)), + ("-h", fg(HighlightRole::option)), + ("definitely_not_a_directory", fg(HighlightRole::error)), + ); + + validate!( + ("cd", fg(HighlightRole::command)), + ("dir-in-cdpath", param_valid_path), + ); + + // Command substitutions. + validate!( + ("echo", fg(HighlightRole::command)), + ("param1", fg(HighlightRole::param)), + ("-l", fg(HighlightRole::option)), + ("--", fg(HighlightRole::option)), + ("-l", fg(HighlightRole::param)), + ("(", fg(HighlightRole::operat)), + ("ls", fg(HighlightRole::command)), + ("-l", fg(HighlightRole::option)), + ("--", fg(HighlightRole::option)), + ("-l", fg(HighlightRole::param)), + ("param2", fg(HighlightRole::param)), + (")", fg(HighlightRole::operat)), + ("|", fg(HighlightRole::statement_terminator)), + ("cat", fg(HighlightRole::command)), + ); + validate!( + ("true", fg(HighlightRole::command)), + ("$(", fg(HighlightRole::operat)), + ("true", fg(HighlightRole::command)), + (")", fg(HighlightRole::operat)), + ); + validate!( + ("true", fg(HighlightRole::command)), + ("\"before", fg(HighlightRole::quote)), + ("$(", fg(HighlightRole::operat)), + ("true", fg(HighlightRole::command)), + ("param1", fg(HighlightRole::param)), + (")", fg(HighlightRole::operat)), + ("after\"", fg(HighlightRole::quote)), + ("param2", fg(HighlightRole::param)), + ); + validate!( + ("true", fg(HighlightRole::command)), + ("\"", fg(HighlightRole::error)), + ("unclosed quote", fg(HighlightRole::quote)), + ("$(", fg(HighlightRole::operat)), + ("true", fg(HighlightRole::command)), + (")", fg(HighlightRole::operat)), + ); + + // Redirections substitutions. + validate!( + ("echo", fg(HighlightRole::command)), + ("param1", fg(HighlightRole::param)), + // Input redirection. + ("<", fg(HighlightRole::redirection)), + ("/bin/echo", fg(HighlightRole::redirection)), + // Output redirection to a valid fd. + ("1>&2", fg(HighlightRole::redirection)), + // Output redirection to an invalid fd. + ("2>&", fg(HighlightRole::redirection)), + ("LO", fg(HighlightRole::error)), + // Just a param, not a redirection. + ("test/blah", fg(HighlightRole::param)), + // Input redirection from directory. + ("<", fg(HighlightRole::redirection)), + ("test/", fg(HighlightRole::error)), + // Output redirection to an invalid path. + ("3>", fg(HighlightRole::redirection)), + ("/not/a/valid/path/nope", fg(HighlightRole::error)), + // Output redirection to directory. + ("3>", fg(HighlightRole::redirection)), + ("test/nope/", fg(HighlightRole::error)), + // Redirections to overflow fd. + ("99999999999999999999>&2", fg(HighlightRole::error)), + ("2>&", fg(HighlightRole::redirection)), + ("99999999999999999999", fg(HighlightRole::error)), + // Output redirection containing a command substitution. + ("4>", fg(HighlightRole::redirection)), + ("(", fg(HighlightRole::operat)), + ("echo", fg(HighlightRole::command)), + ("test/somewhere", fg(HighlightRole::param)), + (")", fg(HighlightRole::operat)), + // Just another param. + ("param2", fg(HighlightRole::param)), + ); + + validate!( + ("for", fg(HighlightRole::keyword)), + ("x", fg(HighlightRole::param)), + ("in", fg(HighlightRole::keyword)), + ("set-by-for-1", fg(HighlightRole::param)), + ("set-by-for-2", fg(HighlightRole::param)), + (";", fg(HighlightRole::statement_terminator)), + ("echo", fg(HighlightRole::command)), + (">", fg(HighlightRole::redirection)), + ("$x", fg(HighlightRole::redirection)), + (";", fg(HighlightRole::statement_terminator)), + ("end", fg(HighlightRole::keyword)), + ); + + validate!( + ("set", fg(HighlightRole::command)), + ("x", fg(HighlightRole::param)), + ("set-by-set", fg(HighlightRole::param)), + (";", fg(HighlightRole::statement_terminator)), + ("echo", fg(HighlightRole::command)), + (">", fg(HighlightRole::redirection)), + ("$x", fg(HighlightRole::redirection)), + ("2>", fg(HighlightRole::redirection)), + ("$totally_not_x", fg(HighlightRole::error)), + ("<", fg(HighlightRole::redirection)), + ("$x_but_its_an_impostor", fg(HighlightRole::error)), + ); + + validate!( + ("x", fg(HighlightRole::param), ns), + ("=", fg(HighlightRole::operat), ns), + ("set-by-variable-override", fg(HighlightRole::param), ns), + ("echo", fg(HighlightRole::command)), + (">", fg(HighlightRole::redirection)), + ("$x", fg(HighlightRole::redirection)), + ); + + validate!( + ("end", fg(HighlightRole::error)), + (";", fg(HighlightRole::statement_terminator)), + ("if", fg(HighlightRole::keyword)), + ("end", fg(HighlightRole::error)), + ); + + validate!( + ("echo", fg(HighlightRole::command)), + ("'", fg(HighlightRole::error)), + ("single_quote", fg(HighlightRole::quote)), + ("$stuff", fg(HighlightRole::quote)), + ); + + validate!( + ("echo", fg(HighlightRole::command)), + ("\"", fg(HighlightRole::error)), + ("double_quote", fg(HighlightRole::quote)), + ("$stuff", fg(HighlightRole::operat)), + ); + + validate!( + ("echo", fg(HighlightRole::command)), + ("$foo", fg(HighlightRole::operat)), + ("\"", fg(HighlightRole::quote)), + ("$bar", fg(HighlightRole::operat)), + ("\"", fg(HighlightRole::quote)), + ("$baz[", fg(HighlightRole::operat)), + ("1 2..3", fg(HighlightRole::param)), + ("]", fg(HighlightRole::operat)), + ); + + validate!( + ("for", fg(HighlightRole::keyword)), + ("i", fg(HighlightRole::param)), + ("in", fg(HighlightRole::keyword)), + ("1 2 3", fg(HighlightRole::param)), + (";", fg(HighlightRole::statement_terminator)), + ("end", fg(HighlightRole::keyword)), + ); + + validate!( + ("echo", fg(HighlightRole::command)), + ("$$foo[", fg(HighlightRole::operat)), + ("1", fg(HighlightRole::param)), + ("][", fg(HighlightRole::operat)), + ("2", fg(HighlightRole::param)), + ("]", fg(HighlightRole::operat)), + ("[3]", fg(HighlightRole::param)), // two dollar signs, so last one is not an expansion + ); + + validate!( + ("cat", fg(HighlightRole::command)), + ("/dev/null", param_valid_path), + ("|", fg(HighlightRole::statement_terminator)), + // This is bogus, but we used to use "less" here and that doesn't have to be installed. + ("cat", fg(HighlightRole::command)), + ("2>", fg(HighlightRole::redirection)), + ); + + // Highlight path-prefixes only at the cursor. + validate!( + ("cat", fg(HighlightRole::command)), + ("/dev/nu", fg(HighlightRole::param)), + ("/dev/nu", param_valid_path), + ); + + validate!( + ("if", fg(HighlightRole::keyword)), + ("true", fg(HighlightRole::command)), + ("&&", fg(HighlightRole::operat)), + ("false", fg(HighlightRole::command)), + (";", fg(HighlightRole::statement_terminator)), + ("or", fg(HighlightRole::operat)), + ("false", fg(HighlightRole::command)), + ("||", fg(HighlightRole::operat)), + ("true", fg(HighlightRole::command)), + (";", fg(HighlightRole::statement_terminator)), + ("and", fg(HighlightRole::operat)), + ("not", fg(HighlightRole::operat)), + ("!", fg(HighlightRole::operat)), + ("true", fg(HighlightRole::command)), + (";", fg(HighlightRole::statement_terminator)), + ("end", fg(HighlightRole::keyword)), + ); + + validate!( + ("echo", fg(HighlightRole::command)), + ("%self", fg(HighlightRole::operat)), + ("not%self", fg(HighlightRole::param)), + ("self%not", fg(HighlightRole::param)), + ); + + validate!( + ("false", fg(HighlightRole::command)), + ("&|", fg(HighlightRole::statement_terminator)), + ("true", fg(HighlightRole::command)), + ); + + validate!( + ("HOME", fg(HighlightRole::param)), + ("=", fg(HighlightRole::operat), ns), + (".", fg(HighlightRole::param), ns), + ("VAR1", fg(HighlightRole::param)), + ("=", fg(HighlightRole::operat), ns), + ("VAL1", fg(HighlightRole::param), ns), + ("VAR", fg(HighlightRole::param)), + ("=", fg(HighlightRole::operat), ns), + ("false", fg(HighlightRole::command)), + ("|&", fg(HighlightRole::error)), + ("true", fg(HighlightRole::command)), + ("stuff", fg(HighlightRole::param)), + ); + + validate!( + ("echo", fg(HighlightRole::command)), // ( + (")", fg(HighlightRole::error)), + ); + + validate!( + ("echo", fg(HighlightRole::command)), + ("stuff", fg(HighlightRole::param)), + ("# comment", fg(HighlightRole::comment)), + ); + + validate!( + ("echo", fg(HighlightRole::command)), + ("--", fg(HighlightRole::option)), + ("-s", fg(HighlightRole::param)), + ); + + // Overlong paths don't crash (#7837). + let overlong = get_overlong_path(); + validate!( + ("touch", fg(HighlightRole::command)), + (&overlong, fg(HighlightRole::param)), + ); + + validate!( + ("a", fg(HighlightRole::param)), + ("=", fg(HighlightRole::operat), ns), + ); + + // Highlighting works across escaped line breaks (#8444). + validate!( + ("echo", fg(HighlightRole::command)), + ("$FISH_\\\n", fg(HighlightRole::operat)), + ("VERSION", fg(HighlightRole::operat), ns), + ); + + validate!( + ("/bin/ca", fg(HighlightRole::command), ns), + ("*", fg(HighlightRole::operat), ns) + ); + + validate!( + ("/bin/c", fg(HighlightRole::command), ns), + ("*", fg(HighlightRole::operat), ns) + ); + + validate!( + ("/bin/c", fg(HighlightRole::command), ns), + ("{$VARIABLE_IN_COMMAND}", fg(HighlightRole::operat), ns), + ("*", fg(HighlightRole::operat), ns) + ); + + validate!( + ("/bin/c", fg(HighlightRole::command), ns), + ("$VARIABLE_IN_COMMAND2", fg(HighlightRole::operat), ns) + ); + + validate!(("$EMPTY_VARIABLE", fg(HighlightRole::error))); + validate!(("\"$EMPTY_VARIABLE\"", fg(HighlightRole::error))); + + validate!( + ("echo", fg(HighlightRole::command)), + ("\\UFDFD", fg(HighlightRole::escape)), + ); + validate!( + ("echo", fg(HighlightRole::command)), + ("\\U10FFFF", fg(HighlightRole::escape)), + ); + validate!( + ("echo", fg(HighlightRole::command)), + ("\\U110000", fg(HighlightRole::error)), + ); + + validate!( + (">", fg(HighlightRole::error)), + ("echo", fg(HighlightRole::error)), + ); +}); diff --git a/fish-rust/src/tests/history.rs b/fish-rust/src/tests/history.rs new file mode 100644 index 000000000..3179c5b85 --- /dev/null +++ b/fish-rust/src/tests/history.rs @@ -0,0 +1,602 @@ +use crate::common::{ + cstr2wcstring, is_windows_subsystem_for_linux, str2wcstring, wcs2osstring, wcs2string, +}; +use crate::env::EnvDyn; +use crate::fds::{wopen_cloexec, AutoCloseFd}; +use crate::ffi_tests::add_test; +use crate::history::{self, History, HistoryItem, HistorySearch, PathList, SearchDirection}; +use crate::path::path_get_data; +use crate::tests::prelude::*; +use crate::tests::string_escape::ESCAPE_TEST_CHAR; +use crate::wchar::prelude::*; +use crate::wcstringutil::{string_prefixes_string, string_prefixes_string_case_insensitive}; +use libc::{O_RDONLY, STDERR_FILENO}; +use rand::random; +use std::collections::VecDeque; +use std::ffi::{CString, OsStr, OsString}; +use std::io::BufReader; +use std::time::SystemTime; +use std::time::UNIX_EPOCH; + +fn history_contains(history: &History, txt: &wstr) -> bool { + for i in 1.. { + let Some(item) = history.item_at_index(i) else { + break; + }; + + if item.str() == txt { + return true; + } + } + + false +} + +fn random_string() -> WString { + let mut result = WString::new(); + let max = 1 + random::() % 32; + for _ in 0..max { + let c = char::from_u32(u32::try_from(1 + random::() % ESCAPE_TEST_CHAR).unwrap()) + .unwrap(); + result.push(c); + } + result +} + +add_test!("test_history", || { + macro_rules! test_history_matches { + ($search:expr, $expected:expr) => { + let expected: Vec<&wstr> = $expected; + let mut found = vec![]; + while $search.go_to_next_match(SearchDirection::Backward) { + found.push($search.current_string().to_owned()); + } + assert_eq!(expected, found); + }; + } + + let items = [ + L!("Gamma"), + L!("beta"), + L!("BetA"), + L!("Beta"), + L!("alpha"), + L!("AlphA"), + L!("Alpha"), + L!("alph"), + L!("ALPH"), + L!("ZZZ"), + ]; + let nocase = history::SearchFlags::IGNORE_CASE; + + // Populate a history. + let history = History::with_name(L!("test_history")); + history.clear(); + for s in items { + history.add_commandline(s.to_owned()); + } + + // Helper to set expected items to those matching a predicate, in reverse order. + let set_expected = |filt: fn(&wstr) -> bool| { + let mut expected = vec![]; + for s in items { + if filt(s) { + expected.push(s); + } + } + expected.reverse(); + expected + }; + + // Items matching "a", case-sensitive. + let mut searcher = HistorySearch::new(history.clone(), L!("a").to_owned()); + let expected = set_expected(|s| s.contains('a')); + test_history_matches!(searcher, expected); + + // Items matching "alpha", case-insensitive. + let mut searcher = + HistorySearch::new_with_flags(history.clone(), L!("AlPhA").to_owned(), nocase); + let expected = set_expected(|s| s.to_lowercase().find(L!("alpha")).is_some()); + test_history_matches!(searcher, expected); + + // Items matching "et", case-sensitive. + let mut searcher = HistorySearch::new(history.clone(), L!("et").to_owned()); + let expected = set_expected(|s| s.find(L!("et")).is_some()); + test_history_matches!(searcher, expected); + + // Items starting with "be", case-sensitive. + let mut searcher = HistorySearch::new_with_type( + history.clone(), + L!("be").to_owned(), + history::SearchType::Prefix, + ); + let expected = set_expected(|s| string_prefixes_string(L!("be"), s)); + test_history_matches!(searcher, expected); + + // Items starting with "be", case-insensitive. + let mut searcher = HistorySearch::new_with( + history.clone(), + L!("be").to_owned(), + history::SearchType::Prefix, + nocase, + 0, + ); + let expected = set_expected(|s| string_prefixes_string_case_insensitive(L!("be"), s)); + test_history_matches!(searcher, expected); + + // Items exactly matching "alph", case-sensitive. + let mut searcher = HistorySearch::new_with_type( + history.clone(), + L!("alph").to_owned(), + history::SearchType::Exact, + ); + let expected = set_expected(|s| s == L!("alph")); + test_history_matches!(searcher, expected); + + // Items exactly matching "alph", case-insensitive. + let mut searcher = HistorySearch::new_with( + history.clone(), + L!("alph").to_owned(), + history::SearchType::Exact, + nocase, + 0, + ); + let expected = set_expected(|s| s.to_lowercase() == L!("alph")); + test_history_matches!(searcher, expected); + + // Test item removal case-sensitive. + let mut searcher = HistorySearch::new(history.clone(), L!("Alpha").to_owned()); + test_history_matches!(searcher, vec![L!("Alpha")]); + history.remove(L!("Alpha").to_owned()); + let mut searcher = HistorySearch::new(history.clone(), L!("Alpha").to_owned()); + test_history_matches!(searcher, vec![]); + + // Test history escaping and unescaping, yaml, etc. + let mut before: VecDeque = VecDeque::new(); + let mut after: VecDeque = VecDeque::new(); + history.clear(); + let max = 100; + for i in 1..=max { + // Generate a value. + let mut value = WString::from_str("test item ") + &i.to_wstring()[..]; + + // Maybe add some backslashes. + if i % 3 == 0 { + value += L!("(slashies \\\\\\ slashies)"); + } + + // Generate some paths. + let mut paths = PathList::new(); + for _ in 0..random::() % 6 { + paths.push(random_string()); + } + + // Record this item. + let mut item = + HistoryItem::new(value, SystemTime::now(), 0, history::PersistenceMode::Disk); + item.set_required_paths(paths); + before.push_back(item.clone()); + history.add(item, false); + } + history.save(); + + // Empty items should just be dropped (#6032). + history.add_commandline(L!("").into()); + assert!(!history.item_at_index(1).unwrap().is_empty()); + + // Read items back in reverse order and ensure they're the same. + for i in (1..=100).rev() { + after.push_back(history.item_at_index(i).unwrap()); + } + assert_eq!(before.len(), after.len()); + for i in 0..before.len() { + let bef = &before[i]; + let aft = &after[i]; + assert_eq!(bef.str(), aft.str()); + assert_eq!(bef.timestamp(), aft.timestamp()); + assert_eq!(bef.get_required_paths(), aft.get_required_paths()); + } + + // Clean up after our tests. + history.clear(); +}); + +// Wait until the next second. +fn time_barrier() { + let start = SystemTime::now(); + loop { + std::thread::sleep(std::time::Duration::from_millis(1)); + if SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() + != start.duration_since(UNIX_EPOCH).unwrap().as_secs() + { + break; + } + } +} + +fn generate_history_lines(item_count: usize, idx: usize) -> Vec { + let mut result = Vec::with_capacity(item_count); + for i in 0..item_count { + result.push(sprintf!("%lu %lu", idx, i)); + } + result +} + +fn test_history_races_pound_on_history(item_count: usize, idx: usize) { + // Called in child thread to modify history. + let hist = History::new(L!("race_test")); + let hist_lines = generate_history_lines(item_count, idx); + for line in hist_lines { + hist.add_commandline(line); + hist.save(); + } +} + +add_test!("test_history_races", || { + // This always fails under WSL + if is_windows_subsystem_for_linux() { + return; + } + + // This fails too often on Github Actions, + // leading to a bunch of spurious test failures on unrelated PRs. + // For now it's better to disable it. + // TODO: Figure out *why* it does that and fix it. + if std::env::var_os("CI").is_some() { + return; + } + + // Testing history race conditions + + // Test concurrent history writing. + // How many concurrent writers we have + const RACE_COUNT: usize = 4; + + // How many items each writer makes + const ITEM_COUNT: usize = 256; + + // Ensure history is clear. + History::new(L!("race_test")).clear(); + + // history::CHAOS_MODE.store(true); + + let mut children = Vec::with_capacity(RACE_COUNT); + for i in 0..RACE_COUNT { + children.push(std::thread::spawn(move || { + test_history_races_pound_on_history(ITEM_COUNT, i); + })); + } + + // Wait for all children. + for child in children { + child.join().unwrap(); + } + + // Compute the expected lines. + let mut expected_lines: [Vec; RACE_COUNT] = + std::array::from_fn(|i| generate_history_lines(ITEM_COUNT, i)); + + // Ensure we consider the lines that have been outputted as part of our history. + time_barrier(); + + // Ensure that we got sane, sorted results. + let hist = History::new(L!("race_test")); + history::CHAOS_MODE.store(false); + + // History is enumerated from most recent to least + // Every item should be the last item in some array + let mut hist_idx = 0; + loop { + hist_idx += 1; + let Some(item) = hist.item_at_index(hist_idx) else { + break; + }; + + let mut found = false; + for list in &mut expected_lines { + let Some(position) = list.iter().position(|elem| *elem == item.str()) else { + continue; + }; + + // Remove everything from this item on + let removed = list.splice(position.., []); + for line in removed.into_iter().rev() { + err!("Item dropped from history: {line}"); + } + + found = true; + break; + } + if !found { + err!( + "Line '{}' found in history, but not found in some array", + item.str() + ); + for list in &expected_lines { + if !list.is_empty() { + fwprintf!(STDERR_FILENO, "\tRemaining: %ls\n", list.last().unwrap()) + } + } + } + } + + // +1 to account for history's 1-based offset + let expected_idx = RACE_COUNT * ITEM_COUNT + 1; + assert_eq!(hist_idx, expected_idx); + + for list in expected_lines { + assert_eq!(list, Vec::::new(), "Lines still left in the array"); + } + hist.clear(); +}); + +add_test!("test_history_merge", || { + // In a single fish process, only one history is allowed to exist with the given name But it's + // common to have multiple history instances with the same name active in different processes, + // e.g. when you have multiple shells open. We try to get that right and merge all their history + // together. Test that case. + const COUNT: usize = 3; + let name = L!("merge_test"); + let hists = [History::new(name), History::new(name), History::new(name)]; + let texts = [L!("History 1"), L!("History 2"), L!("History 3")]; + let alt_texts = [ + L!("History Alt 1"), + L!("History Alt 2"), + L!("History Alt 3"), + ]; + + // Make sure history is clear. + for hist in &hists { + hist.clear(); + } + + // Make sure we don't add an item in the same second as we created the history. + time_barrier(); + + // Add a different item to each. + for i in 0..COUNT { + hists[i].add_commandline(texts[i].to_owned()); + } + + // Save them. + for hist in &hists { + hist.save() + } + + // Make sure each history contains what it ought to, but they have not leaked into each other. + #[allow(clippy::needless_range_loop)] + for i in 0..COUNT { + for j in 0..COUNT { + let does_contain = history_contains(&hists[i], texts[j]); + let should_contain = i == j; + assert_eq!(should_contain, does_contain); + } + } + + // Make a new history. It should contain everything. The time_barrier() is so that the timestamp + // is newer, since we only pick up items whose timestamp is before the birth stamp. + time_barrier(); + let everything = History::new(name); + for text in texts { + assert!(history_contains(&everything, text)); + } + + // Tell all histories to merge. Now everybody should have everything. + for hist in &hists { + hist.incorporate_external_changes(); + } + + // Everyone should also have items in the same order (#2312) + let hist_vals1 = hists[0].get_history(); + for hist in &hists { + assert_eq!(hist_vals1, hist.get_history()); + } + + // Add some more per-history items. + for i in 0..COUNT { + hists[i].add_commandline(alt_texts[i].to_owned()); + } + // Everybody should have old items, but only one history should have each new item. + #[allow(clippy::needless_range_loop)] + for i in 0..COUNT { + for j in 0..COUNT { + // Old item. + assert!(history_contains(&hists[i], texts[j])); + + // New item. + let does_contain = history_contains(&hists[i], alt_texts[j]); + let should_contain = i == j; + assert_eq!(should_contain, does_contain); + } + } + + // Make sure incorporate_external_changes doesn't drop items! (#3496) + let writer = &hists[0]; + let reader = &hists[1]; + let more_texts = [ + L!("Item_#3496_1"), + L!("Item_#3496_2"), + L!("Item_#3496_3"), + L!("Item_#3496_4"), + L!("Item_#3496_5"), + L!("Item_#3496_6"), + ]; + for i in 0..more_texts.len() { + // time_barrier because merging will ignore items that may be newer + if i > 0 { + time_barrier(); + } + writer.add_commandline(more_texts[i].to_owned()); + writer.incorporate_external_changes(); + reader.incorporate_external_changes(); + for text in more_texts.iter().take(i) { + assert!(history_contains(reader, text)); + } + } + everything.clear(); +}); + +add_test!("test_history_path_detection", || { + // Regression test for #7582. + let tmpdirbuff = CString::new("/tmp/fish_test_history.XXXXXX").unwrap(); + let tmpdir = unsafe { libc::mkdtemp(tmpdirbuff.into_raw()) }; + let tmpdir = unsafe { CString::from_raw(tmpdir) }; + let mut tmpdir = str2wcstring(tmpdir.to_bytes()); + if !tmpdir.ends_with('/') { + tmpdir.push('/'); + } + + // Place one valid file in the directory. + let filename = L!("testfile"); + std::fs::write(wcs2osstring(&(tmpdir.clone() + &filename[..])), []).unwrap(); + + let mut test_vars = TestEnvironment::default(); + test_vars.vars.insert(L!("PWD").to_owned(), tmpdir.clone()); + test_vars.vars.insert(L!("HOME").to_owned(), tmpdir.clone()); + let vars = || EnvDyn::new(Box::new(test_vars.clone())); + + let history = History::with_name(L!("path_detection")); + history.clear(); + assert_eq!(history.size(), 0); + history.clone().add_pending_with_file_detection( + L!("cmd0 not/a/valid/path"), + vars(), + history::PersistenceMode::Disk, + ); + history.clone().add_pending_with_file_detection( + &(L!("cmd1 ").to_owned() + filename), + vars(), + history::PersistenceMode::Disk, + ); + history.clone().add_pending_with_file_detection( + &(L!("cmd2 ").to_owned() + &tmpdir[..] + L!("/") + filename), + vars(), + history::PersistenceMode::Disk, + ); + history.clone().add_pending_with_file_detection( + &(L!("cmd3 $HOME/").to_owned() + filename), + vars(), + history::PersistenceMode::Disk, + ); + history.clone().add_pending_with_file_detection( + L!("cmd4 $HOME/notafile"), + vars(), + history::PersistenceMode::Disk, + ); + history.clone().add_pending_with_file_detection( + &(L!("cmd5 ~/").to_owned() + filename), + vars(), + history::PersistenceMode::Disk, + ); + history.clone().add_pending_with_file_detection( + L!("cmd6 ~/notafile"), + vars(), + history::PersistenceMode::Disk, + ); + history.clone().add_pending_with_file_detection( + L!("cmd7 ~/*f*"), + vars(), + history::PersistenceMode::Disk, + ); + history.clone().add_pending_with_file_detection( + L!("cmd8 ~/*zzz*"), + vars(), + history::PersistenceMode::Disk, + ); + history.resolve_pending(); + + const hist_size: usize = 9; + assert_eq!(history.size(), 9); + + // Expected sets of paths. + let expected_paths = [ + vec![], // cmd0 + vec![filename.to_owned()], // cmd1 + vec![tmpdir + L!("/") + filename], // cmd2 + vec![L!("$HOME/").to_owned() + filename], // cmd3 + vec![], // cmd4 + vec![L!("~/").to_owned() + filename], // cmd5 + vec![], // cmd6 + vec![], // cmd7 - we do not expand globs + vec![], // cmd8 + ]; + + let maxlap = 128; + for _lap in 0..maxlap { + let mut failures = 0; + for i in 1..=hist_size { + if history.item_at_index(i).unwrap().get_required_paths() + != expected_paths[hist_size - i] + { + failures += 1; + } + } + if failures == 0 { + break; + } + // The file detection takes a little time since it occurs in the background. + // Loop until the test passes. + std::thread::sleep(std::time::Duration::from_millis(2)); + } + history.clear(); +}); + +fn install_sample_history(name: &wstr) { + let path = path_get_data().expect("Failed to get data directory"); + std::fs::copy( + wcs2osstring(&(L!("tests/").to_owned() + &name[..])), + wcs2osstring(&(path + L!("/") + &name[..] + L!("_history"))), + ) + .unwrap(); +} + +add_test!("test_history_formats", || { + // Test inferring and reading legacy and bash history formats. + let name = L!("history_sample_fish_2_0"); + install_sample_history(name); + let expected: Vec = vec![ + "echo this has\\\nbackslashes".into(), + "function foo\necho bar\nend".into(), + "echo alpha".into(), + ]; + let test_history_imported = History::with_name(name); + assert_eq!(test_history_imported.get_history(), expected); + test_history_imported.clear(); + + // Test bash import + // The results are in the reverse order that they appear in the bash history file. + // We don't expect whitespace to be elided (#4908: except for leading/trailing whitespace) + let expected: Vec = vec![ + "EOF".into(), + "sleep 123".into(), + "posix_cmd_sub $(is supported but only splits on newlines)".into(), + "posix_cmd_sub \"$(is supported)\"".into(), + "a && echo valid construct".into(), + "final line".into(), + "echo supsup".into(), + "export XVAR='exported'".into(), + "history --help".into(), + "echo foo".into(), + ]; + let test_history_imported_from_bash = History::with_name(L!("bash_import")); + let file = AutoCloseFd::new(wopen_cloexec(L!("tests/history_sample_bash"), O_RDONLY, 0)); + assert!(file.is_valid()); + test_history_imported_from_bash.populate_from_bash(BufReader::new(file)); + assert_eq!(test_history_imported_from_bash.get_history(), expected); + test_history_imported_from_bash.clear(); + + let name = L!("history_sample_corrupt1"); + install_sample_history(name); + // We simply invoke get_string_representation. If we don't die, the test is a success. + let test_history_imported_from_corrupted = History::with_name(name); + let expected: Vec = vec![ + "no_newline_at_end_of_file".into(), + "corrupt_prefix".into(), + "this_command_is_ok".into(), + ]; + assert_eq!(test_history_imported_from_corrupted.get_history(), expected); + test_history_imported_from_corrupted.clear(); +}); diff --git a/fish-rust/src/tests/mod.rs b/fish-rust/src/tests/mod.rs index 091baf458..bcdcb8dea 100644 --- a/fish-rust/src/tests/mod.rs +++ b/fish-rust/src/tests/mod.rs @@ -1,7 +1,46 @@ +use crate::wchar::prelude::*; + #[cfg(test)] mod common; +mod complete; +mod env; +mod env_universal_common; +mod expand; mod fd_monitor; +mod highlight; +mod history; +mod parser; #[cfg(test)] +mod redirection; mod string_escape; #[cfg(test)] mod tokenizer; +mod topic_monitor; + +mod prelude { + use crate::env::EnvStack; + pub use crate::tests::env::{PwdEnvironment, TestEnvironment}; + use crate::wutil::wgetcwd; + use once_cell::sync::Lazy; + use std::sync::Mutex; + + static PUSHED_DIRS: Lazy>> = Lazy::new(Mutex::default); + /// Helper to chdir and then update $PWD. + pub fn pushd(path: &str) { + let cwd = wgetcwd(); + PUSHED_DIRS.lock().unwrap().push(cwd.to_string()); + + // We might need to create the directory. We don't care if this fails due to the directory + // already being present. + std::fs::create_dir_all(path).unwrap(); + + std::env::set_current_dir(path).unwrap(); + EnvStack::principal().set_pwd_from_getcwd(); + } + + pub fn popd() { + let old_cwd = PUSHED_DIRS.lock().unwrap().pop().unwrap(); + std::env::set_current_dir(old_cwd).unwrap(); + EnvStack::principal().set_pwd_from_getcwd(); + } +} diff --git a/fish-rust/src/tests/parser.rs b/fish-rust/src/tests/parser.rs new file mode 100644 index 000000000..224fedd32 --- /dev/null +++ b/fish-rust/src/tests/parser.rs @@ -0,0 +1,347 @@ +use crate::ast::{Ast, List, Node}; +use crate::io::{IoBufferfill, IoChain}; +use crate::parse_constants::{ParseTreeFlags, ParserTestErrorBits}; +use crate::parse_util::{parse_util_detect_errors, parse_util_detect_errors_in_argument}; +use crate::parser::Parser; +use crate::reader::reader_reset_interrupted; +use crate::signal::{signal_clear_cancel, signal_reset_handlers, signal_set_handlers}; +use crate::threads::{iothread_drain_all, iothread_perform}; +use crate::wchar::prelude::*; +use libc::SIGINT; +use std::time::Duration; + +use crate::ffi_tests::add_test; +add_test!("test_parser", || { + macro_rules! detect_errors { + ($src:literal) => { + parse_util_detect_errors(L!($src), None, true /* accept incomplete */) + }; + } + + fn detect_argument_errors(src: &str) -> Result<(), ParserTestErrorBits> { + let src = WString::from_str(src); + let ast = Ast::parse_argument_list(&src, ParseTreeFlags::default(), None); + if ast.errored() { + return Err(ParserTestErrorBits::ERROR); + } + let args = &ast.top().as_freestanding_argument_list().unwrap().arguments; + let first_arg = args.get(0).expect("Failed to parse an argument"); + let mut errors = None; + parse_util_detect_errors_in_argument(first_arg, first_arg.source(&src), &mut errors) + } + + // Testing block nesting + assert!( + detect_errors!("if; end").is_err(), + "Incomplete if statement undetected" + ); + assert!( + detect_errors!("if test; echo").is_err(), + "Missing end undetected" + ); + assert!( + detect_errors!("if test; end; end").is_err(), + "Unbalanced end undetected" + ); + + // Testing detection of invalid use of builtin commands + assert!( + detect_errors!("case foo").is_err(), + "'case' command outside of block context undetected" + ); + assert!( + detect_errors!("switch ggg; if true; case foo;end;end").is_err(), + "'case' command outside of switch block context undetected" + ); + assert!( + detect_errors!("else").is_err(), + "'else' command outside of conditional block context undetected" + ); + assert!( + detect_errors!("else if").is_err(), + "'else if' command outside of conditional block context undetected" + ); + assert!( + detect_errors!("if false; else if; end").is_err(), + "'else if' missing command undetected" + ); + + assert!( + detect_errors!("break").is_err(), + "'break' command outside of loop block context undetected" + ); + + assert!( + detect_errors!("break --help").is_ok(), + "'break --help' incorrectly marked as error" + ); + + assert!( + detect_errors!("while false ; function foo ; break ; end ; end ").is_err(), + "'break' command inside function allowed to break from loop outside it" + ); + + assert!( + detect_errors!("exec ls|less").is_err() && detect_errors!("echo|return").is_err(), + "Invalid pipe command undetected" + ); + + assert!( + detect_errors!("for i in foo ; switch $i ; case blah ; break; end; end ").is_ok(), + "'break' command inside switch falsely reported as error" + ); + + assert!( + detect_errors!("or cat | cat").is_ok() && detect_errors!("and cat | cat").is_ok(), + "boolean command at beginning of pipeline falsely reported as error" + ); + + assert!( + detect_errors!("cat | and cat").is_err(), + "'and' command in pipeline not reported as error" + ); + + assert!( + detect_errors!("cat | or cat").is_err(), + "'or' command in pipeline not reported as error" + ); + + assert!( + detect_errors!("cat | exec").is_err() && detect_errors!("exec | cat").is_err(), + "'exec' command in pipeline not reported as error" + ); + + assert!( + detect_errors!("begin ; end arg").is_err(), + "argument to 'end' not reported as error" + ); + + assert!( + detect_errors!("switch foo ; end arg").is_err(), + "argument to 'end' not reported as error" + ); + + assert!( + detect_errors!("if true; else if false ; end arg").is_err(), + "argument to 'end' not reported as error" + ); + + assert!( + detect_errors!("if true; else ; end arg").is_err(), + "argument to 'end' not reported as error" + ); + + assert!( + detect_errors!("begin ; end 2> /dev/null").is_ok(), + "redirection after 'end' wrongly reported as error" + ); + + assert!( + detect_errors!("true | ") == Err(ParserTestErrorBits::INCOMPLETE), + "unterminated pipe not reported properly" + ); + + assert!( + detect_errors!("echo (\nfoo\n bar") == Err(ParserTestErrorBits::INCOMPLETE), + "unterminated multiline subshell not reported properly" + ); + + assert!( + detect_errors!("begin ; true ; end | ") == Err(ParserTestErrorBits::INCOMPLETE), + "unterminated pipe not reported properly" + ); + + assert!( + detect_errors!(" | true ") == Err(ParserTestErrorBits::ERROR), + "leading pipe not reported properly" + ); + + assert!( + detect_errors!("true | # comment") == Err(ParserTestErrorBits::INCOMPLETE), + "comment after pipe not reported as incomplete" + ); + + assert!( + detect_errors!("true | # comment \n false ").is_ok(), + "comment and newline after pipe wrongly reported as error" + ); + + assert!( + detect_errors!("true | ; false ") == Err(ParserTestErrorBits::ERROR), + "semicolon after pipe not detected as error" + ); + + assert!( + detect_argument_errors("foo").is_ok(), + "simple argument reported as error" + ); + + assert!( + detect_argument_errors("''").is_ok(), + "Empty string reported as error" + ); + + assert!( + detect_argument_errors("foo$$") + .unwrap_err() + .contains(ParserTestErrorBits::ERROR), + "Bad variable expansion not reported as error" + ); + + assert!( + detect_argument_errors("foo$@") + .unwrap_err() + .contains(ParserTestErrorBits::ERROR), + "Bad variable expansion not reported as error" + ); + + // Within command substitutions, we should be able to detect everything that + // parse_util_detect_errors! can detect. + assert!( + detect_argument_errors("foo(cat | or cat)") + .unwrap_err() + .contains(ParserTestErrorBits::ERROR), + "Bad command substitution not reported as error" + ); + + assert!( + detect_errors!("false & ; and cat").is_err(), + "'and' command after background not reported as error" + ); + + assert!( + detect_errors!("true & ; or cat").is_err(), + "'or' command after background not reported as error" + ); + + assert!( + detect_errors!("true & ; not cat").is_ok(), + "'not' command after background falsely reported as error" + ); + + assert!( + detect_errors!("if true & ; end").is_err(), + "backgrounded 'if' conditional not reported as error" + ); + + assert!( + detect_errors!("if false; else if true & ; end").is_err(), + "backgrounded 'else if' conditional not reported as error" + ); + + assert!( + detect_errors!("while true & ; end").is_err(), + "backgrounded 'while' conditional not reported as error" + ); + + assert!( + detect_errors!("true | || false").is_err(), + "bogus boolean statement error not detected" + ); + + assert!( + detect_errors!("|| false").is_err(), + "bogus boolean statement error not detected" + ); + + assert!( + detect_errors!("&& false").is_err(), + "bogus boolean statement error not detected" + ); + + assert!( + detect_errors!("true ; && false").is_err(), + "bogus boolean statement error not detected" + ); + + assert!( + detect_errors!("true ; || false").is_err(), + "bogus boolean statement error not detected" + ); + + assert!( + detect_errors!("true || && false").is_err(), + "bogus boolean statement error not detected" + ); + + assert!( + detect_errors!("true && || false").is_err(), + "bogus boolean statement error not detected" + ); + + assert!( + detect_errors!("true && && false").is_err(), + "bogus boolean statement error not detected" + ); + + assert!( + detect_errors!("true && ") == Err(ParserTestErrorBits::INCOMPLETE), + "unterminated conjunction not reported properly" + ); + + assert!( + detect_errors!("true && \n true").is_ok(), + "newline after && reported as error" + ); + + assert!( + detect_errors!("true || \n") == Err(ParserTestErrorBits::INCOMPLETE), + "unterminated conjunction not reported properly" + ); +}); + +fn test_1_cancellation(src: &wstr) { + let filler = IoBufferfill::create().unwrap(); + let delay = Duration::from_millis(500); + let thread = unsafe { libc::pthread_self() }; + iothread_perform(move || { + // Wait a while and then SIGINT the main thread. + std::thread::sleep(delay); + unsafe { + libc::pthread_kill(thread, SIGINT); + } + }); + let mut io = IoChain::new(); + io.push(filler.clone()); + let res = Parser::principal_parser().eval(src, &io); + let buffer = IoBufferfill::finish(filler); + assert_eq!( + buffer.len(), + 0, + "Expected 0 bytes in out_buff, but instead found {} bytes, for command {}", + buffer.len(), + src + ); + assert!(res.status.signal_exited() && res.status.signal_code() == SIGINT); + unsafe { + iothread_drain_all(); + } +} + +add_test!("test_cancellation", || { + println!("Testing Ctrl-C cancellation. If this hangs, that's a bug!"); + + // Enable fish's signal handling here. + signal_set_handlers(true); + + // This tests that we can correctly ctrl-C out of certain loop constructs, and that nothing gets + // printed if we do. + + // Here the command substitution is an infinite loop. echo never even gets its argument, so when + // we cancel we expect no output. + test_1_cancellation(L!("echo (while true ; echo blah ; end)")); + + // Nasty infinite loop that doesn't actually execute anything. + test_1_cancellation(L!( + "echo (while true ; end) (while true ; end) (while true ; end)" + )); + test_1_cancellation(L!("while true ; end")); + test_1_cancellation(L!("while true ; echo nothing > /dev/null; end")); + test_1_cancellation(L!("for i in (while true ; end) ; end")); + + signal_reset_handlers(); + + // Ensure that we don't think we should cancel. + reader_reset_interrupted(); + signal_clear_cancel(); +}); diff --git a/fish-rust/src/tests/redirection.rs b/fish-rust/src/tests/redirection.rs new file mode 100644 index 000000000..744af966d --- /dev/null +++ b/fish-rust/src/tests/redirection.rs @@ -0,0 +1,43 @@ +use crate::io::{IoChain, IoClose, IoFd}; +use crate::redirection::dup2_list_resolve_chain; +use std::sync::Arc; + +#[test] +fn test_dup2s() { + let mut chain = IoChain::new(); + chain.push(Arc::new(IoClose::new(17))); + chain.push(Arc::new(IoFd::new(3, 19))); + let list = dup2_list_resolve_chain(&chain); + assert_eq!(list.get_actions().len(), 2); + + let act1 = list.get_actions()[0]; + assert_eq!(act1.src, 17); + assert_eq!(act1.target, -1); + + let act2 = list.get_actions()[1]; + assert_eq!(act2.src, 19); + assert_eq!(act2.target, 3); +} + +#[test] +fn test_dup2s_fd_for_target_fd() { + let mut chain = IoChain::new(); + // note io_fd_t params are backwards from dup2. + chain.push(Arc::new(IoClose::new(10))); + chain.push(Arc::new(IoFd::new(9, 10))); + chain.push(Arc::new(IoFd::new(5, 8))); + chain.push(Arc::new(IoFd::new(1, 4))); + chain.push(Arc::new(IoFd::new(3, 5))); + let list = dup2_list_resolve_chain(&chain); + + assert_eq!(list.fd_for_target_fd(3), 8); + assert_eq!(list.fd_for_target_fd(5), 8); + assert_eq!(list.fd_for_target_fd(8), 8); + assert_eq!(list.fd_for_target_fd(1), 4); + assert_eq!(list.fd_for_target_fd(4), 4); + assert_eq!(list.fd_for_target_fd(100), 100); + assert_eq!(list.fd_for_target_fd(0), 0); + assert_eq!(list.fd_for_target_fd(-1), -1); + assert_eq!(list.fd_for_target_fd(9), -1); + assert_eq!(list.fd_for_target_fd(10), -1); +} diff --git a/fish-rust/src/tests/string_escape.rs b/fish-rust/src/tests/string_escape.rs index e3b745561..b84b3cdc4 100644 --- a/fish-rust/src/tests/string_escape.rs +++ b/fish-rust/src/tests/string_escape.rs @@ -164,7 +164,7 @@ fn test_escape_no_printables() { /// The average length of strings to unescape. const ESCAPE_TEST_LENGTH: usize = 100; /// The highest character number of character to try and escape. -const ESCAPE_TEST_CHAR: usize = 4000; +pub const ESCAPE_TEST_CHAR: usize = 4000; /// Helper to convert a narrow string to a sequence of hex digits. fn str2hex(input: &[u8]) -> String { diff --git a/fish-rust/src/tests/topic_monitor.rs b/fish-rust/src/tests/topic_monitor.rs new file mode 100644 index 000000000..7eb58828d --- /dev/null +++ b/fish-rust/src/tests/topic_monitor.rs @@ -0,0 +1,71 @@ +use crate::ffi_tests::add_test; +use crate::topic_monitor::{topic_monitor_t, topic_t, GenerationsList}; +use std::sync::{ + atomic::{AtomicU32, AtomicU64, Ordering}, + Arc, +}; + +add_test!("test_topic_monitor", || { + let monitor = topic_monitor_t::default(); + let gens = GenerationsList::new(); + let t = topic_t::sigchld; + gens.sigchld.set(0); + assert_eq!(monitor.generation_for_topic(t), 0); + let changed = monitor.check(&gens, false /* wait */); + assert!(!changed); + assert_eq!(gens.sigchld.get(), 0); + + monitor.post(t); + let changed = monitor.check(&gens, true /* wait */); + assert!(changed); + assert_eq!(gens.get(t), 1); + assert_eq!(monitor.generation_for_topic(t), 1); + + monitor.post(t); + assert_eq!(monitor.generation_for_topic(t), 2); + let changed = monitor.check(&gens, true /* wait */); + assert!(changed); + assert_eq!(gens.sigchld.get(), 2); +}); + +add_test!("test_topic_monitor_torture", || { + let monitor = Arc::new(topic_monitor_t::default()); + const THREAD_COUNT: usize = 64; + let t1 = topic_t::sigchld; + let t2 = topic_t::sighupint; + let mut gens_list = vec![GenerationsList::invalid(); THREAD_COUNT]; + let post_count = Arc::new(AtomicU64::new(0)); + for gen in &mut gens_list { + *gen = monitor.current_generations(); + post_count.fetch_add(1, Ordering::Relaxed); + monitor.post(t1); + } + + let completed = Arc::new(AtomicU32::new(0)); + let mut threads = vec![]; + + for gens in gens_list { + let monitor = Arc::downgrade(&monitor); + let post_count = Arc::downgrade(&post_count); + let completed = Arc::downgrade(&completed); + threads.push(std::thread::spawn(move || { + for _ in 0..1 << 11 { + let before = gens.clone(); + let _changed = monitor.upgrade().unwrap().check(&gens, true /* wait */); + assert!(before.get(t1) < gens.get(t1)); + assert!(gens.get(t1) <= post_count.upgrade().unwrap().load(Ordering::Relaxed)); + assert_eq!(gens.get(t2), 0); + } + let _amt = completed.upgrade().unwrap().fetch_add(1, Ordering::Relaxed); + })); + } + + while completed.load(Ordering::Relaxed) < THREAD_COUNT.try_into().unwrap() { + post_count.fetch_add(1, Ordering::Relaxed); + monitor.post(t1); + std::thread::yield_now(); + } + for t in threads { + t.join().unwrap(); + } +}); diff --git a/fish-rust/src/tokenizer.rs b/fish-rust/src/tokenizer.rs index 8c1d3b9f5..48fb9acec 100644 --- a/fish-rust/src/tokenizer.rs +++ b/fish-rust/src/tokenizer.rs @@ -1153,7 +1153,7 @@ pub fn is_valid(&self) -> bool { } // \return the token type for this redirection. - fn token_type(&self) -> TokenType { + pub fn token_type(&self) -> TokenType { if self.is_pipe { TokenType::pipe } else { diff --git a/fish-rust/src/topic_monitor.rs b/fish-rust/src/topic_monitor.rs index efb7d4050..a0069f0b2 100644 --- a/fish-rust/src/topic_monitor.rs +++ b/fish-rust/src/topic_monitor.rs @@ -27,86 +27,74 @@ use crate::wutil::perror; use nix::errno::Errno; use nix::unistd; -use std::cell::UnsafeCell; +use std::cell::{Cell, UnsafeCell}; use std::mem; use std::pin::Pin; -use std::sync::{ - atomic::{AtomicU8, Ordering}, - Condvar, Mutex, MutexGuard, -}; +use std::sync::atomic::{AtomicU8, Ordering}; +use std::sync::{Condvar, Mutex, MutexGuard}; +use widestring_suffix::widestrs; -#[cxx::bridge] -mod topic_monitor_ffi { - /// Simple value type containing the values for a topic. - /// This should be kept in sync with topic_t. - #[derive(Default, Copy, Clone, Debug, PartialEq, Eq)] - pub struct generation_list_t { - pub sighupint: u64, - pub sigchld: u64, - pub internal_exit: u64, - } +/// The list of topics which may be observed. +#[repr(u8)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum topic_t { + sighupint = 0, // Corresponds to both SIGHUP and SIGINT signals. + sigchld = 1, // Corresponds to SIGCHLD signal. + internal_exit = 2, // Corresponds to an internal process exit. +} - extern "Rust" { - fn invalid_generations() -> generation_list_t; - fn set_min_from(self: &mut generation_list_t, topic: topic_t, other: &generation_list_t); - fn at(self: &generation_list_t, topic: topic_t) -> u64; - fn at_mut(self: &mut generation_list_t, topic: topic_t) -> &mut u64; - //fn describe(self: &generation_list_t) -> UniquePtr; - } +// XXX: Is it correct to use the default or should the default be invalid_generation? +#[derive(Clone, Default, PartialEq, PartialOrd, Eq, Ord)] +pub struct GenerationsList { + pub sighupint: Cell, + pub sigchld: Cell, + pub internal_exit: Cell, +} - /// The list of topics which may be observed. - #[repr(u8)] - #[derive(Copy, Clone, Debug, PartialEq, Eq, Ord)] - pub enum topic_t { - sighupint, // Corresponds to both SIGHUP and SIGINT signals. - sigchld, // Corresponds to SIGCHLD signal. - internal_exit, // Corresponds to an internal process exit. - } - - extern "Rust" { - type topic_monitor_t; - fn new_topic_monitor() -> Box; - - fn topic_monitor_principal() -> &'static topic_monitor_t; - fn post(self: &topic_monitor_t, topic: topic_t); - fn current_generations(self: &topic_monitor_t) -> generation_list_t; - fn generation_for_topic(self: &topic_monitor_t, topic: topic_t) -> u64; - fn check(self: &topic_monitor_t, gens: *mut generation_list_t, wait: bool) -> bool; +/// Simple value type containing the values for a topic. +/// This should be kept in sync with topic_t. +impl GenerationsList { + /// Update `self` gen counts to match those of `other`. + pub fn update(&self, other: &Self) { + self.sighupint.set(other.sighupint.get()); + self.sigchld.set(other.sigchld.get()); + self.internal_exit.set(other.internal_exit.get()); } } -// FIXME: #derive-ing this currently makes clippy complain -// about https://rust-lang.github.io/rust-clippy/master/index.html#/incorrect_partial_ord_impl_on_ord_type -impl PartialOrd for topic_t { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -pub use topic_monitor_ffi::{generation_list_t, topic_t}; pub type generation_t = u64; impl FloggableDebug for topic_t {} /// A generation value which indicates the topic is not of interest. -pub const invalid_generation: generation_t = std::u64::MAX; +pub const INVALID_GENERATION: generation_t = std::u64::MAX; pub fn all_topics() -> [topic_t; 3] { [topic_t::sighupint, topic_t::sigchld, topic_t::internal_exit] } -impl generation_list_t { +#[widestrs] +impl GenerationsList { pub fn new() -> Self { Self::default() } + /// Generation list containing invalid generations only. + pub fn invalid() -> GenerationsList { + GenerationsList { + sighupint: INVALID_GENERATION.into(), + sigchld: INVALID_GENERATION.into(), + internal_exit: INVALID_GENERATION.into(), + } + } + fn describe(&self) -> WString { let mut result = WString::new(); for gen in self.as_array() { if !result.is_empty() { result.push(','); } - if gen == invalid_generation { + if gen == INVALID_GENERATION { result.push_str("-1"); } else { result.push_str(&gen.to_string()); @@ -115,67 +103,55 @@ fn describe(&self) -> WString { return result; } - /// \return the a mutable reference to the value for a topic. - pub fn at_mut(&mut self, topic: topic_t) -> &mut generation_t { + /// Sets the generation for `topic` to `value`. + pub fn set(&self, topic: topic_t, value: generation_t) { match topic { - topic_t::sighupint => &mut self.sighupint, - topic_t::sigchld => &mut self.sigchld, - topic_t::internal_exit => &mut self.internal_exit, - _ => panic!("invalid topic"), + topic_t::sighupint => self.sighupint.set(value), + topic_t::sigchld => self.sigchld.set(value), + topic_t::internal_exit => self.internal_exit.set(value), } } /// \return the value for a topic. - pub fn at(&self, topic: topic_t) -> generation_t { + pub fn get(&self, topic: topic_t) -> generation_t { match topic { - topic_t::sighupint => self.sighupint, - topic_t::sigchld => self.sigchld, - topic_t::internal_exit => self.internal_exit, - _ => panic!("invalid topic"), + topic_t::sighupint => self.sighupint.get(), + topic_t::sigchld => self.sigchld.get(), + topic_t::internal_exit => self.internal_exit.get(), } } /// \return ourselves as an array. pub fn as_array(&self) -> [generation_t; 3] { - [self.sighupint, self.sigchld, self.internal_exit] + [ + self.sighupint.get(), + self.sigchld.get(), + self.internal_exit.get(), + ] } /// Set the value of \p topic to the smaller of our value and the value in \p other. - pub fn set_min_from(&mut self, topic: topic_t, other: &generation_list_t) { - if self.at(topic) > other.at(topic) { - *self.at_mut(topic) = other.at(topic); + pub fn set_min_from(&mut self, topic: topic_t, other: &Self) { + if self.get(topic) > other.get(topic) { + self.set(topic, other.get(topic)); } } /// \return whether a topic is valid. pub fn is_valid(&self, topic: topic_t) -> bool { - self.at(topic) != invalid_generation + self.get(topic) != INVALID_GENERATION } /// \return whether any topic is valid. pub fn any_valid(&self) -> bool { let mut valid = false; for gen in self.as_array() { - if gen != invalid_generation { + if gen != INVALID_GENERATION { valid = true; } } valid } - - /// Generation list containing invalid generations only. - pub fn invalids() -> generation_list_t { - generation_list_t { - sighupint: invalid_generation, - sigchld: invalid_generation, - internal_exit: invalid_generation, - } - } -} - -/// CXX wrapper as it does not support member functions. -pub fn invalid_generations() -> generation_list_t { - generation_list_t::invalids() } /// A simple binary semaphore. @@ -214,11 +190,11 @@ pub fn new() -> binary_semaphore_t { assert!(pipes.is_some(), "Failed to make pubsub pipes"); pipes_ = pipes.unwrap(); - // Whoof. Thread Sanitizer swallows signals and replays them at its leisure, at the point - // where instrumented code makes certain blocking calls. But tsan cannot interrupt a signal - // call, so if we're blocked in read() (like the topic monitor wants to be!), we'll never - // receive SIGCHLD and so deadlock. So if tsan is enabled, we mark our fd as non-blocking - // (so reads will never block) and use select() to poll it. + // Whoof. Thread Sanitizer swallows signals and replays them at its leisure, at the + // point where instrumented code makes certain blocking calls. But tsan cannot interrupt + // a signal call, so if we're blocked in read() (like the topic monitor wants to be!), + // we'll never receive SIGCHLD and so deadlock. So if tsan is enabled, we mark our fd as + // non-blocking (so reads will never block) and use select() to poll it. if cfg!(feature = "FISH_TSAN_WORKAROUNDS") { let _ = make_fd_nonblocking(pipes_.read.fd()); } @@ -338,14 +314,14 @@ fn default() -> Self { type topic_bitmask_t = u8; fn topic_to_bit(t: topic_t) -> topic_bitmask_t { - 1 << t.repr + 1 << (t as u8) } // Some stuff that needs to be protected by the same lock. #[derive(Default)] struct data_t { /// The current values. - current: generation_list_t, + current: GenerationsList, /// A flag indicating that there is a current reader. /// The 'reader' is responsible for calling sema_.wait(). @@ -379,6 +355,9 @@ pub struct topic_monitor_t { sema_: binary_semaphore_t, } +// safety: this is only needed for tests +unsafe impl Sync for topic_monitor_t {} + /// The principal topic monitor. /// Do not attempt to move this into a lazy_static, it must be accessed from a signal handler. static mut s_principal: *const topic_monitor_t = std::ptr::null(); @@ -444,7 +423,7 @@ pub fn post(&self, topic: topic_t) { /// Apply any pending updates to the data. /// This accepts data because it must be locked. /// \return the updated generation list. - fn updated_gens_in_data(&self, data: &mut MutexGuard) -> generation_list_t { + fn updated_gens_in_data(&self, data: &mut MutexGuard) -> GenerationsList { // Atomically acquire the pending updates, swapping in 0. // If there are no pending updates (likely) or a thread is waiting, just return. // Otherwise CAS in 0 and update our topics. @@ -454,7 +433,7 @@ fn updated_gens_in_data(&self, data: &mut MutexGuard) -> generation_list while !cas_success { changed_topic_bits = self.status_.load(relaxed); if changed_topic_bits == 0 || changed_topic_bits == STATUS_NEEDS_WAKEUP { - return data.current; + return data.current.clone(); } cas_success = self .status_ @@ -469,35 +448,35 @@ fn updated_gens_in_data(&self, data: &mut MutexGuard) -> generation_list // Update the current generation with our topics and return it. for topic in all_topics() { if changed_topic_bits & topic_to_bit(topic) != 0 { - *data.current.at_mut(topic) += 1; + data.current.set(topic, data.current.get(topic) + 1); FLOG!( topic_monitor, "Updating topic", topic, "to", - data.current.at(topic) + data.current.get(topic) ); } } // Report our change. self.data_notifier_.notify_all(); - return data.current; + return data.current.clone(); } /// \return the current generation list, opportunistically applying any pending updates. - fn updated_gens(&self) -> generation_list_t { + fn updated_gens(&self) -> GenerationsList { let mut data = self.data_.lock().unwrap(); return self.updated_gens_in_data(&mut data); } /// Access the current generations. - pub fn current_generations(self: &topic_monitor_t) -> generation_list_t { + pub fn current_generations(self: &topic_monitor_t) -> GenerationsList { self.updated_gens() } /// Access the generation for a topic. pub fn generation_for_topic(self: &topic_monitor_t, topic: topic_t) -> generation_t { - self.current_generations().at(topic) + self.current_generations().get(topic) } /// Given a list of input generations, attempt to update them to something newer. @@ -507,7 +486,7 @@ pub fn generation_for_topic(self: &topic_monitor_t, topic: topic_t) -> generatio /// indicating we should become the reader. Now it is our responsibility to wait on the /// semaphore and notify on a change via the condition variable. If \p gens is current, and /// there is already a reader, then wait until the reader notifies us and try again. - fn try_update_gens_maybe_becoming_reader(&self, gens: &mut generation_list_t) -> bool { + fn try_update_gens_maybe_becoming_reader(&self, gens: &mut GenerationsList) -> bool { let mut become_reader = false; let mut data = self.data_.lock().unwrap(); loop { @@ -563,9 +542,9 @@ fn try_update_gens_maybe_becoming_reader(&self, gens: &mut generation_list_t) -> /// Wait for some entry in the list of generations to change. /// \return the new gens. - fn await_gens(&self, input_gens: &generation_list_t) -> generation_list_t { - let mut gens = *input_gens; - while gens == *input_gens { + fn await_gens(&self, input_gens: &GenerationsList) -> GenerationsList { + let mut gens = input_gens.clone(); + while &gens == input_gens { let become_reader = self.try_update_gens_maybe_becoming_reader(&mut gens); if become_reader { // Now we are the reader. Read from the pipe, and then update with any changes. @@ -581,7 +560,7 @@ fn await_gens(&self, input_gens: &generation_list_t) -> generation_list_t { // We are finished waiting. We must stop being the reader, and post on the condition // variable to wake up any other threads waiting for us to finish reading. let mut data = self.data_.lock().unwrap(); - gens = data.current; + gens = data.current.clone(); // FLOG(topic_monitor, "TID", thread_id(), "local", input_gens.describe(), // "read() complete, current is", gens.describe()); assert!(data.has_reader, "We should be the reader"); @@ -597,25 +576,23 @@ fn await_gens(&self, input_gens: &generation_list_t) -> generation_list_t { /// If \p wait is set, then wait if there are no changes; otherwise return immediately. /// \return true if some topic changed, false if none did. /// On a true return, this updates the generation list \p gens. - pub fn check(&self, gens: *mut generation_list_t, wait: bool) -> bool { - assert!(!gens.is_null(), "gens must not be null"); - let gens = unsafe { &mut *gens }; + pub fn check(&self, gens: &GenerationsList, wait: bool) -> bool { if !gens.any_valid() { return false; } - let mut current: generation_list_t = self.updated_gens(); + let mut current: GenerationsList = self.updated_gens(); let mut changed = false; loop { // Load the topic list and see if anything has changed. for topic in all_topics() { if gens.is_valid(topic) { assert!( - gens.at(topic) <= current.at(topic), + gens.get(topic) <= current.get(topic), "Incoming gen count exceeded published count" ); - if gens.at(topic) < current.at(topic) { - *gens.at_mut(topic) = current.at(topic); + if gens.get(topic) < current.get(topic) { + gens.set(topic, current.get(topic)); changed = true; } } diff --git a/fish-rust/src/trace.rs b/fish-rust/src/trace.rs index 8f734ed52..2251046f5 100644 --- a/fish-rust/src/trace.rs +++ b/fish-rust/src/trace.rs @@ -1,6 +1,7 @@ +use crate::parser::Parser; use crate::{ common::escape, - ffi::{self, wcharz_t, wcstring_list_ffi_t, Parser}, + ffi::{self, wcharz_t, wcstring_list_ffi_t}, global_safety::RelaxedAtomicBool, wchar::prelude::*, wchar_ffi::{WCharFromFFI, WCharToFFI}, @@ -13,7 +14,7 @@ mod trace_ffi { include!("parser.h"); type wcstring_list_ffi_t = super::wcstring_list_ffi_t; type wcharz_t = super::wcharz_t; - type Parser = super::Parser; + type Parser = crate::parser::Parser; } extern "Rust" { @@ -32,7 +33,7 @@ pub fn trace_set_enabled(do_enable: bool) { /// return whether tracing is enabled. pub fn trace_enabled(parser: &Parser) -> bool { - let ld = parser.ffi_libdata_pod_const(); + let ld = &parser.libdata().pods; if ld.suppress_fish_trace { return false; } @@ -67,8 +68,21 @@ pub fn trace_argv>(parser: &Parser, command: &wstr, args: &[S]) { ffi::log_extra_to_flog_file(&trace_text.to_ffi()); } -/// Convenience helper to trace a single string if tracing is enabled. -pub fn trace_if_enabled>(parser: &Parser, command: &wstr, args: &[S]) { +pub fn trace_if_enabled_ffi>(parser: &Parser, command: &wstr, args: &[S]) { + if trace_enabled(parser) { + trace_argv(parser, command, args); + } +} + +/// Convenience helper to trace a single command if tracing is enabled. +pub fn trace_if_enabled(parser: &Parser, command: &wstr) { + if trace_enabled(parser) { + let argv: &[&'static wstr] = &[]; + trace_argv(parser, command, argv); + } +} +/// Convenience helper to trace a single command and arguments if tracing is enabled. +pub fn trace_if_enabled_with_args>(parser: &Parser, command: &wstr, args: &[S]) { if trace_enabled(parser) { trace_argv(parser, command, args); } diff --git a/fish-rust/src/util.rs b/fish-rust/src/util.rs index 3611b2920..6e009c5cd 100644 --- a/fish-rust/src/util.rs +++ b/fish-rust/src/util.rs @@ -258,6 +258,28 @@ fn wcsfilecmp_leading_digits(a: &wstr, b: &wstr) -> (Ordering, usize, usize) { (ret, ai, bi) } +/// Finds `needle` in a `haystack` and returns the index of the first matching element, if any. +/// +/// # Examples +/// +/// ``` +/// let haystack = b"ABC ABCDAB ABCDABCDABDE"; +/// +/// assert_eq!(find_subslice(b"ABCDABD", haystack), Some(15)); +/// assert_eq!(find_subslice(b"ABCDE", haystack), None); +/// ``` +pub fn find_subslice( + needle: impl AsRef<[T]>, + haystack: impl AsRef<[T]>, +) -> Option { + let needle = needle.as_ref(); + if needle.is_empty() { + return Some(0); + } + let haystack = haystack.as_ref(); + haystack.windows(needle.len()).position(|w| w == needle) +} + /// Verify the behavior of the `wcsfilecmp()` function. #[test] fn test_wcsfilecmp() { diff --git a/fish-rust/src/wait_handle.rs b/fish-rust/src/wait_handle.rs index 2a8ad6d6f..e66e1675e 100644 --- a/fish-rust/src/wait_handle.rs +++ b/fish-rust/src/wait_handle.rs @@ -1,130 +1,8 @@ use crate::wchar::prelude::*; -use crate::wchar_ffi::WCharFromFFI; -use cxx::CxxWString; use libc::pid_t; use std::cell::Cell; use std::rc::Rc; -#[cxx::bridge] -mod wait_handle_ffi { - extern "Rust" { - type WaitHandleRefFFI; - fn new_wait_handle_ffi( - pid: i32, - internal_job_id: u64, - base_name: &CxxWString, - ) -> Box; - #[cxx_name = "set_status_and_complete"] - fn set_status_and_complete_ffi(self: &mut WaitHandleRefFFI, status: i32); - - type WaitHandleStoreFFI; - fn new_wait_handle_store_ffi() -> Box; - fn remove_by_pid(self: &mut WaitHandleStoreFFI, pid: i32); - fn get_job_id_by_pid(self: &WaitHandleStoreFFI, pid: i32) -> u64; - - fn try_get_status_and_job_id( - self: &WaitHandleStoreFFI, - pid: i32, - only_if_complete: bool, - status: &mut i32, - job_id: &mut u64, - ) -> bool; - - fn add(self: &mut WaitHandleStoreFFI, wh: *const Box); - } -} - -pub struct WaitHandleRefFFI(WaitHandleRef); - -impl WaitHandleRefFFI { - #[allow(clippy::wrong_self_convention)] - pub fn from_ffi(&self) -> &WaitHandleRef { - &self.0 - } - - #[allow(clippy::wrong_self_convention)] - pub fn from_ffi_mut(&mut self) -> &mut WaitHandleRef { - &mut self.0 - } - - fn set_status_and_complete_ffi(self: &mut WaitHandleRefFFI, status: i32) { - self.from_ffi().set_status_and_complete(status) - } -} - -pub struct WaitHandleStoreFFI(WaitHandleStore); - -impl WaitHandleStoreFFI { - #[allow(clippy::wrong_self_convention)] - pub fn from_ffi_mut(&mut self) -> &mut WaitHandleStore { - &mut self.0 - } - - #[allow(clippy::wrong_self_convention)] - pub fn from_ffi(&self) -> &WaitHandleStore { - &self.0 - } - - /// \return the job ID for a pid, or 0 if None. - fn get_job_id_by_pid(&self, pid: i32) -> u64 { - self.from_ffi() - .get_by_pid(pid) - .map(|wh| wh.internal_job_id) - .unwrap_or(0) - } - - /// Try getting the status and job ID of a job. - /// \return true if the job was found. - /// If only_if_complete is true, then only return true if the job is completed. - fn try_get_status_and_job_id( - self: &WaitHandleStoreFFI, - pid: i32, - only_if_complete: bool, - status: &mut i32, - job_id: &mut u64, - ) -> bool { - let whs = self.from_ffi(); - let Some(wh) = whs.get_by_pid(pid) else { - return false; - }; - if only_if_complete && !wh.is_completed() { - return false; - } - *status = wh.status.get().unwrap_or(0); - *job_id = wh.internal_job_id; - true - } - - /// Remove the wait handle for a pid, if present in this store. - fn remove_by_pid(&mut self, pid: i32) { - self.from_ffi_mut().remove_by_pid(pid); - } - - fn add(self: &mut WaitHandleStoreFFI, wh: *const Box) { - if wh.is_null() { - return; - } - let wh = unsafe { (*wh).from_ffi() }; - self.from_ffi_mut().add(wh.clone()); - } -} - -fn new_wait_handle_store_ffi() -> Box { - Box::new(WaitHandleStoreFFI(WaitHandleStore::new())) -} - -fn new_wait_handle_ffi( - pid: i32, - internal_job_id: u64, - base_name: &CxxWString, -) -> Box { - Box::new(WaitHandleRefFFI(WaitHandle::new( - pid as pid_t, - internal_job_id, - base_name.from_ffi(), - ))) -} - /// The non user-visible, never-recycled job ID. /// Every job has a unique positive value for this. pub type InternalJobId = u64; diff --git a/fish-rust/src/wchar_ffi.rs b/fish-rust/src/wchar_ffi.rs index f81cac2c0..5ee11fb39 100644 --- a/fish-rust/src/wchar_ffi.rs +++ b/fish-rust/src/wchar_ffi.rs @@ -35,6 +35,15 @@ pub fn chars(&self) -> &[char] { } } +/// W0String can be cheaply converted to a wcharz_t (but be mindful that W0String is kept alive). +impl From<&W0String> for wcharz_t { + fn from(w0: &W0String) -> Self { + wcharz_t { + str_: w0.as_ptr() as *const wchar_t, + } + } +} + /// Convert wcharz_t to an WString. impl From<&wcharz_t> for WString { fn from(wcharz: &wcharz_t) -> Self { @@ -69,6 +78,16 @@ macro_rules! wcharz { }; } +/// Convert a CxxVector of wcharz_t to a Vec. +pub fn wcharzs_to_vec(wcharz_vec: cxx::UniquePtr>) -> Vec { + wcharz_vec + .as_ref() + .expect("UniquePtr was null") + .iter() + .map(|s| s.into()) + .collect() +} + pub(crate) use c_str; pub(crate) use wcharz; @@ -190,6 +209,12 @@ fn from_ffi(self) -> Vec { } } +impl WCharFromFFI for &wcharz_t { + fn from_ffi(self) -> WString { + self.into() + } +} + /// Convert wcstring_list_ffi_t to Vec. impl WCharFromFFI> for &wcstring_list_ffi_t { fn from_ffi(self) -> Vec { diff --git a/fish-rust/src/wcstringutil.rs b/fish-rust/src/wcstringutil.rs index c23d58aa4..3a5535eb0 100644 --- a/fish-rust/src/wcstringutil.rs +++ b/fish-rust/src/wcstringutil.rs @@ -21,6 +21,16 @@ pub fn string_suffixes_string_case_insensitive(proposed_suffix: &wstr, value: &w && wcscasecmp(&value[value.len() - suffix_size..], proposed_suffix).is_eq() } +/// Test if a string prefixes another. Returns true if a is a prefix of b. +pub fn string_prefixes_string(proposed_prefix: &wstr, value: &wstr) -> bool { + value.as_slice().starts_with(proposed_prefix.as_slice()) +} + +/// Test if a string is a suffix of another. +pub fn string_suffixes_string(proposed_suffix: &wstr, value: &wstr) -> bool { + value.as_slice().ends_with(proposed_suffix.as_slice()) +} + /// Test if a string matches a subsequence of another. /// Note subsequence is not substring: "foo" is a subsequence of "follow" for example. pub fn subsequence_in_string(needle: &wstr, haystack: &wstr) -> bool { @@ -34,15 +44,16 @@ pub fn subsequence_in_string(needle: &wstr, haystack: &wstr) -> bool { return true; } - let mut ni = needle.chars(); - let mut nc = ni.next(); - for hc in haystack.chars() { - if nc == Some(hc) { - nc = ni.next(); + let mut needle_it = needle.chars().peekable(); + for c in haystack.chars() { + needle_it.next_if_eq(&c); + + if needle_it.peek().is_none() { + return true; } } // We succeeded if we exhausted our sequence. - nc.is_none() + needle_it.peek().is_none() } /// Case-insensitive string search, modeled after std::string::find(). @@ -87,7 +98,7 @@ pub enum ContainType { } // The case-folding required for the match. -#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] pub enum CaseFold { /// exact match: foobar matches foobar samecase, @@ -297,10 +308,7 @@ fn wcs2string_bad_char(c: char) { /// Split a string by a separator character. pub fn split_string(val: &wstr, sep: char) -> Vec { - val.as_char_slice() - .split(|c| *c == sep) - .map(WString::from_chars) - .collect() + val.split(sep).map(wstr::to_owned).collect() } /// Split a string by runs of any of the separator characters provided in \p seps. @@ -347,6 +355,7 @@ pub fn split_string_tok<'val>( } /// Joins strings with a separator. +/// This supports both &[&wstr] and &[&WString]. pub fn join_strings>(strs: &[S], sep: char) -> WString { if strs.is_empty() { return WString::new(); @@ -363,10 +372,7 @@ pub fn join_strings>(strs: &[S], sep: char) -> WString { } pub fn bool_from_string(x: &wstr) -> bool { - if x.is_empty() { - return false; - } - matches!(x.chars().next().unwrap(), 'Y' | 'T' | 'y' | 't' | '1') + matches!(x.chars().next(), Some('Y' | 'T' | 'y' | 't' | '1')) } /// Given iterators into a string (forward or reverse), splits the haystack iterators @@ -378,8 +384,8 @@ pub fn bool_from_string(x: &wstr) -> bool { pub fn split_about<'haystack>( haystack: &'haystack wstr, needle: &wstr, - max: usize, - no_empty: bool, + max: usize, /*=usize::MAX*/ + no_empty: bool, /*=false*/ ) -> Vec<&'haystack wstr> { let mut output = vec![]; let mut remaining = max; @@ -464,38 +470,38 @@ pub fn trim(input: WString, any_of: Option<&wstr>) -> WString { /// \p idx may be "one past the end." pub fn count_preceding_backslashes(text: &wstr, idx: usize) -> usize { assert!(idx <= text.len(), "Out of bounds"); - let mut backslashes = 0; - while backslashes < idx && text.char_at(idx - backslashes - 1) == '\\' { - backslashes += 1; - } - backslashes + text.chars() + .take(idx) + .rev() + .take_while(|&c| c == '\\') + .count() } /// Support for iterating over a newline-separated string. pub struct LineIterator<'a> { // The string we're iterating. - coll: &'a str, + coll: &'a [u8], // The current location in the iteration. current: usize, } impl<'a> LineIterator<'a> { - pub fn new(coll: &'a str) -> Self { + pub fn new(coll: &'a [u8]) -> Self { Self { coll, current: 0 } } } impl<'a> Iterator for LineIterator<'a> { - type Item = &'a str; + type Item = &'a [u8]; fn next(&mut self) -> Option { if self.current == self.coll.len() { return None; } let newline_or_end = self.coll[self.current..] - .bytes() - .position(|b| b == b'\n') + .iter() + .position(|b| *b == b'\n') .map(|pos| self.current + pos) .unwrap_or(self.coll.len()); let result = &self.coll[self.current..newline_or_end]; @@ -615,11 +621,20 @@ fn test_join_strings() { #[test] fn test_line_iterator() { - let text = "Alpha\nBeta\nGamma\n\nDelta\n"; + let text = b"Alpha\nBeta\nGamma\n\nDelta\n"; let mut lines = vec![]; let iter = LineIterator::new(text); for line in iter { lines.push(line); } - assert_eq!(lines, vec!["Alpha", "Beta", "Gamma", "", "Delta"]); + assert_eq!( + lines, + vec![ + &b"Alpha"[..], + &b"Beta"[..], + &b"Gamma"[..], + &b""[..], + &b"Delta"[..] + ] + ); } diff --git a/fish-rust/src/wgetopt.rs b/fish-rust/src/wgetopt.rs index bda2041bd..514a536f3 100644 --- a/fish-rust/src/wgetopt.rs +++ b/fish-rust/src/wgetopt.rs @@ -69,7 +69,7 @@ fn empty_wstr() -> &'static wstr { pub struct wgetopter_t<'opts, 'args, 'argarray> { /// Argv. - argv: &'argarray mut [&'args wstr], + pub argv: &'argarray mut [&'args wstr], /// For communication from `getopt` to the caller. When `getopt` finds an option that takes an /// argument, the argument value is returned here. Also, when `ordering` is RETURN_IN_ORDER, each diff --git a/fish-rust/src/wildcard.rs b/fish-rust/src/wildcard.rs index c60fb6f67..12a01746e 100644 --- a/fish-rust/src/wildcard.rs +++ b/fish-rust/src/wildcard.rs @@ -59,6 +59,7 @@ pub enum WildcardResult { Overflow, } +// This does something horrible refactored from an even more horrible function. fn resolve_description<'f>( full_completion: &wstr, completion: &mut &wstr, @@ -171,12 +172,12 @@ fn wildcard_complete_internal( } else { flags }; - if !out.add(Completion { - completion: out_completion.to_owned(), - description: out_desc, - flags: local_flags, - r#match: m, - }) { + if !out.add(Completion::new( + out_completion.to_owned(), + out_desc, + m, + local_flags, + )) { return WildcardResult::Overflow; } return WildcardResult::Match; @@ -302,6 +303,7 @@ pub fn wildcard_complete<'f>( /// Obtain a description string for the file specified by the filename. /// /// It assumes the file exists and won't run stat() to confirm. +/// It assumes the file exists and won't run stat() to confirm. /// The returned value is a string constant and should not be free'd. /// /// \param filename The file for which to find a description string @@ -894,7 +896,7 @@ fn add_expansion_result(&mut self, result: WString) { // // The result does not have a leading slash, but does have a trailing slash if non-empty. fn descend_unique_hierarchy(&mut self, start_point: &mut WString) -> WString { - assert!(!start_point.is_empty() && !start_point.starts_with('/')); + assert!(!start_point.is_empty() && start_point.starts_with('/')); let mut unique_hierarchy = WString::new(); let abs_unique_hierarchy = start_point; @@ -1184,7 +1186,7 @@ pub fn wildcard_match( // Check if the string has any unescaped wildcards (e.g. ANY_STRING). #[inline] #[must_use] -fn wildcard_has_internal(s: impl AsRef) -> bool { +pub fn wildcard_has_internal(s: impl AsRef) -> bool { s.as_ref() .chars() .any(|c| matches!(c, ANY_STRING | ANY_STRING_RECURSIVE | ANY_CHAR)) @@ -1192,7 +1194,7 @@ fn wildcard_has_internal(s: impl AsRef) -> bool { /// Check if the specified string contains wildcards (e.g. *). #[must_use] -fn wildcard_has(s: impl AsRef) -> bool { +pub fn wildcard_has(s: impl AsRef) -> bool { let s = s.as_ref(); let qmark_is_wild = !feature_test(FeatureFlag::qmark_noglob); // Fast check for * or ?; if none there is no wildcard. @@ -1240,12 +1242,8 @@ mod ffi { } extern "Rust" { - #[cxx_name = "wildcard_match_ffi"] - fn wildcard_match_ffi( - str: &CxxWString, - wc: &CxxWString, - leading_dots_fail_to_match: bool, - ) -> bool; + #[cxx_name = "wildcard_match"] + fn wildcard_match_ffi(str: &CxxWString, wc: &CxxWString) -> bool; #[cxx_name = "wildcard_has"] fn wildcard_has_ffi(s: &CxxWString) -> bool; @@ -1255,8 +1253,12 @@ fn wildcard_match_ffi( } } -fn wildcard_match_ffi(str: &CxxWString, wc: &CxxWString, leading_dots_fail_to_match: bool) -> bool { - wildcard_match(str.from_ffi(), wc.from_ffi(), leading_dots_fail_to_match) +fn wildcard_match_ffi(str: &CxxWString, wc: &CxxWString) -> bool { + wildcard_match( + str.from_ffi(), + wc.from_ffi(), + /*leading_dots_fail_to_match*/ false, + ) } fn wildcard_has_ffi(s: &CxxWString) -> bool { diff --git a/fish-rust/src/wutil/dir_iter.rs b/fish-rust/src/wutil/dir_iter.rs index b6c36e5d2..1dc57bac9 100644 --- a/fish-rust/src/wutil/dir_iter.rs +++ b/fish-rust/src/wutil/dir_iter.rs @@ -139,7 +139,7 @@ fn dirent_type_to_entry_type(dt: u8) -> Option { DT_REG => Some(DirEntryType::reg), DT_LNK => Some(DirEntryType::lnk), DT_SOCK => Some(DirEntryType::sock), - // todo! whiteout + // todo!("whiteout") _ => None, } } @@ -154,7 +154,7 @@ fn stat_mode_to_entry_type(m: libc::mode_t) -> Option { S_IFLNK => Some(DirEntryType::lnk), S_IFSOCK => Some(DirEntryType::sock), _ => { - // todo! whiteout + // todo!("whiteout") None } } diff --git a/fish-rust/src/wutil/mod.rs b/fish-rust/src/wutil/mod.rs index b9d848ed8..5f3381d9e 100644 --- a/fish-rust/src/wutil/mod.rs +++ b/fish-rust/src/wutil/mod.rs @@ -18,9 +18,10 @@ use crate::wchar::{wstr, WString, L}; use crate::wchar_ext::WExt; use crate::wcstringutil::{join_strings, split_string, wcs2string_callback}; +use errno::errno; pub(crate) use gettext::{wgettext, wgettext_fmt, wgettext_str}; pub(crate) use printf::sprintf; -use std::ffi::OsStr; +use std::ffi::{CStr, OsStr}; use std::fs::{self, canonicalize}; use std::io::{self, Write}; use std::os::unix::prelude::*; @@ -66,7 +67,7 @@ pub fn wperror(s: &wstr) { /// Port of the wide-string wperror from `src/wutil.cpp` but for rust `&str`. pub fn perror(s: &str) { - let e = errno::errno().0; + let e = errno().0; let mut stderr = std::io::stderr().lock(); if !s.is_empty() { let _ = write!(stderr, "{s}: "); @@ -574,15 +575,17 @@ pub fn file_id_for_autoclose_fd(fd: &AutoCloseFd) -> FileId { } pub fn file_id_for_path(path: &wstr) -> FileId { + file_id_for_path_narrow(&wcs2zstring(path)) +} + +pub fn file_id_for_path_narrow(path: &CStr) -> FileId { let mut result = INVALID_FILE_ID; - let path = wcs2zstring(path); let mut buf: libc::stat = unsafe { std::mem::zeroed() }; if unsafe { libc::stat(path.as_ptr(), &mut buf) } == 0 { result = FileId::from_stat(&buf); } result } - /// Given that \p cursor is a pointer into \p base, return the offset in characters. /// This emulates C pointer arithmetic: /// `wstr_offset_in(cursor, base)` is equivalent to C++ `cursor - base`. diff --git a/fish-rust/src/wutil/wcstoi.rs b/fish-rust/src/wutil/wcstoi.rs index 7fca517d7..c257c0027 100644 --- a/fish-rust/src/wutil/wcstoi.rs +++ b/fish-rust/src/wutil/wcstoi.rs @@ -311,7 +311,7 @@ pub fn fish_wcstoul(mut src: &wstr) -> Result { }; let mut consumed = 0; let result = wcstoi_partial(src, options, &mut consumed)?; - // Skip trailling whitespace. + // Skip trailing whitespace. src = src.slice_from(consumed); while !src.is_empty() && src.char_at(0).is_whitespace() { src = src.slice_from(1); diff --git a/src/ast.h b/src/ast.h index c171c7bf4..2d67d740f 100644 --- a/src/ast.h +++ b/src/ast.h @@ -82,6 +82,9 @@ struct keyword_base_t; #endif +using DecoratedStatement = ast::decorated_statement_t; +using BlockStatement = ast::block_statement_t; + namespace ast { using node_t = ::NodeFfi; } diff --git a/src/autoload.cpp b/src/autoload.cpp deleted file mode 100644 index 9656c3141..000000000 --- a/src/autoload.cpp +++ /dev/null @@ -1,246 +0,0 @@ -// The classes responsible for autoloading functions and completions. -#include "config.h" // IWYU pragma: keep - -#include "autoload.h" - -#include -#include -#include -#include -#include - -#include "common.h" -#include "env.h" -#include "io.h" -#include "lru.h" -#include "parser.h" -#include "wutil.h" // IWYU pragma: keep - -/// The time before we'll recheck an autoloaded file. -static const int kAutoloadStalenessInterval = 15; - -/// Represents a file that we might want to autoload. -namespace { -struct autoloadable_file_t { - /// The path to the file. - wcstring path; - - /// The metadata for the file. - file_id_t file_id; -}; -} // namespace - -/// Class representing a cache of files that may be autoloaded. -/// This is responsible for performing cached accesses to a set of paths. -class autoload_file_cache_t { - /// A timestamp is a monotonic point in time. - using timestamp_t = std::chrono::time_point; - - /// The directories from which to load. - const std::vector dirs_{}; - - /// Our LRU cache of checks that were misses. - /// The key is the command, the value is the time of the check. - using misses_lru_cache_t = lru_cache_t; - misses_lru_cache_t misses_cache_; - - /// The set of files that we have returned to the caller, along with the time of the check. - /// The key is the command (not the path). - struct known_file_t { - autoloadable_file_t file; - timestamp_t last_checked; - }; - std::unordered_map known_files_; - - /// \return the current timestamp. - static timestamp_t current_timestamp() { return std::chrono::steady_clock::now(); } - - /// \return whether a timestamp is fresh enough to use. - static bool is_fresh(timestamp_t then, timestamp_t now); - - /// Attempt to find an autoloadable file by searching our path list for a given command. - /// \return the file, or none() if none. - maybe_t locate_file(const wcstring &cmd) const; - - public: - /// Initialize with a set of directories. - explicit autoload_file_cache_t(std::vector dirs) : dirs_(std::move(dirs)) {} - - /// Initialize with empty directories. - autoload_file_cache_t() = default; - - /// \return the directories. - const std::vector &dirs() const { return dirs_; } - - /// Check if a command \p cmd can be loaded. - /// If \p allow_stale is true, allow stale entries; otherwise discard them. - /// This returns an autoloadable file, or none() if there is no such file. - maybe_t check(const wcstring &cmd, bool allow_stale = false); - - /// \return true if a command is cached (either as a hit or miss). - bool is_cached(const wcstring &cmd) const; -}; - -maybe_t autoload_file_cache_t::locate_file(const wcstring &cmd) const { - // If the command is empty or starts with NULL (i.e. is empty as a path) - // we'd try to source the *directory*, which exists. - // So instead ignore these here. - if (cmd.empty()) return none(); - if (cmd[0] == L'\0') return none(); - // Re-use the storage for path. - wcstring path; - for (const wcstring &dir : dirs()) { - // Construct the path as dir/cmd.fish - path = dir; - path += L"/"; - path += cmd; - path += L".fish"; - - file_id_t file_id = file_id_for_path(path); - if (file_id != kInvalidFileID) { - // Found it. - autoloadable_file_t result; - result.path = std::move(path); - result.file_id = file_id; - return result; - } - } - return none(); -} - -bool autoload_file_cache_t::is_fresh(timestamp_t then, timestamp_t now) { - auto seconds = std::chrono::duration_cast(now - then); - return seconds.count() < kAutoloadStalenessInterval; -} - -maybe_t autoload_file_cache_t::check(const wcstring &cmd, bool allow_stale) { - // Check hits. - auto iter = known_files_.find(cmd); - if (iter != known_files_.end()) { - if (allow_stale || is_fresh(iter->second.last_checked, current_timestamp())) { - // Re-use this cached hit. - return iter->second.file; - } - // The file is stale, remove it. - known_files_.erase(iter); - } - - // Check misses. - if (timestamp_t *miss = misses_cache_.get(cmd)) { - if (allow_stale || is_fresh(*miss, current_timestamp())) { - // Re-use this cached miss. - return none(); - } - // The miss is stale, remove it. - misses_cache_.evict_node(cmd); - } - - // We couldn't satisfy this request from the cache. Hit the disk. - maybe_t file = locate_file(cmd); - if (file.has_value()) { - auto ins = known_files_.emplace(cmd, known_file_t{*file, current_timestamp()}); - assert(ins.second && "Known files cache should not have contained this cmd"); - (void)ins; - } else { - bool ins = misses_cache_.insert(cmd, current_timestamp()); - assert(ins && "Misses cache should not have contained this cmd"); - (void)ins; - } - return file; -} - -bool autoload_file_cache_t::is_cached(const wcstring &cmd) const { - return known_files_.count(cmd) > 0 || misses_cache_.contains(cmd); -} - -autoload_t::autoload_t(wcstring env_var_name) - : env_var_name_(std::move(env_var_name)), cache_(make_unique()) {} - -autoload_t::autoload_t(autoload_t &&) noexcept = default; -autoload_t::~autoload_t() = default; - -void autoload_t::invalidate_cache() { - auto cache = make_unique(cache_->dirs()); - cache_ = std::move(cache); -} - -bool autoload_t::can_autoload(const wcstring &cmd) { - return cache_->check(cmd, true /* allow stale */).has_value(); -} - -bool autoload_t::has_attempted_autoload(const wcstring &cmd) { return cache_->is_cached(cmd); } - -std::vector autoload_t::get_autoloaded_commands() const { - std::vector result; - result.reserve(autoloaded_files_.size()); - for (const auto &kv : autoloaded_files_) { - result.push_back(kv.first); - } - // Sort the output to make it easier to test. - std::sort(result.begin(), result.end()); - return result; -} - -maybe_t autoload_t::resolve_command(const wcstring &cmd, const environment_t &env) { - if (maybe_t mvar = env.get(env_var_name_)) { - return resolve_command(cmd, mvar->as_list()); - } else { - return resolve_command(cmd, std::vector{}); - } -} - -wcstring autoload_t::resolve_command_ffi(const wcstring &cmd) { - if (auto res = resolve_command(cmd, env_stack_t::globals())) { - return std::move(*res); - } else { - return wcstring(); - } -} - -maybe_t autoload_t::resolve_command(const wcstring &cmd, - const std::vector &paths) { - // Are we currently in the process of autoloading this? - if (current_autoloading_.count(cmd) > 0) return none(); - - // Check to see if our paths have changed. If so, replace our cache. - // Note we don't have to modify autoloadable_files_. We'll naturally detect if those have - // changed when we query the cache. - if (paths != cache_->dirs()) { - cache_ = make_unique(paths); - } - - // Do we have an entry to load? - auto mfile = cache_->check(cmd); - if (!mfile) return none(); - - // Is this file the same as what we previously autoloaded? - auto iter = autoloaded_files_.find(cmd); - if (iter != autoloaded_files_.end() && iter->second == mfile->file_id) { - // The file has been autoloaded and is unchanged. - return none(); - } - - // We're going to (tell our caller to) autoload this command. - current_autoloading_.insert(cmd); - autoloaded_files_[cmd] = mfile->file_id; - return std::move(mfile->path); -} - -void autoload_t::perform_autoload(const wcstring &path, parser_t &parser) { - // We do the useful part of what exec_subshell does ourselves - // - we source the file. - // We don't create a buffer or check ifs or create a read_limit - - wcstring script_source = L"source " + escape_string(path); - auto prev_statuses = parser.get_last_statuses(); - const cleanup_t put_back([&] { parser.set_last_statuses(prev_statuses); }); - parser.eval(script_source, io_chain_t{}); -} - -std::unique_ptr make_autoload_ffi(wcstring env_var_name) { - return make_unique(std::move(env_var_name)); -} - -void perform_autoload_ffi(const wcstring &path, parser_t &parser) { - autoload_t::perform_autoload(path, parser); -} diff --git a/src/autoload.h b/src/autoload.h index b52bf5730..8b1378917 100644 --- a/src/autoload.h +++ b/src/autoload.h @@ -1,114 +1 @@ -// The classes responsible for autoloading functions and completions. -#ifndef FISH_AUTOLOAD_H -#define FISH_AUTOLOAD_H -#include "config.h" // IWYU pragma: keep - -#include -#include -#include -#include -#include - -#include "common.h" -#include "maybe.h" -#include "wutil.h" - -class autoload_file_cache_t; -class environment_t; -class Parser; using parser_t = Parser; -struct autoload_tester_t; - -/// autoload_t is a class that knows how to autoload .fish files from a list of directories. This -/// is used by autoloading functions and completions. It maintains a file cache, which is -/// responsible for potentially cached accesses of files, and then a list of files that have -/// actually been autoloaded. A client may request a file to autoload given a command name, and may -/// be returned a path which it is expected to source. -/// autoload_t does not have any internal locks; it is the responsibility of the caller to lock -/// it. -class autoload_t { - /// The environment variable whose paths we observe. - const wcstring env_var_name_; - - /// A map from command to the files we have autoloaded. - std::unordered_map autoloaded_files_; - - /// The list of commands that we are currently autoloading. - std::unordered_set current_autoloading_; - - /// The autoload cache. - /// This is a unique_ptr because want to change it if the value of our environment variable - /// changes. This is never null (but it may be a cache with no paths). - std::unique_ptr cache_; - - /// Invalidate any underlying cache. - /// This is exposed for testing. - void invalidate_cache(); - - /// Like resolve_autoload(), but accepts the paths directly. - /// This is exposed for testing. - maybe_t resolve_command(const wcstring &cmd, const std::vector &paths); - - friend autoload_tester_t; - - public: - /// Construct an autoloader that loads from the paths given by \p env_var_name. - explicit autoload_t(wcstring env_var_name); - - autoload_t(autoload_t &&); - ~autoload_t(); - - /// Given a command, get a path to autoload. - /// For example, if the environment variable is 'fish_function_path' and the command is 'foo', - /// this will look for a file 'foo.fish' in one of the directories given by fish_function_path. - /// If there is no such file, OR if the file has been previously resolved and is now unchanged, - /// this will return none. But if the file is either new or changed, this will return the path. - /// After returning a path, the command is marked in-progress until the caller calls - /// mark_autoload_finished() with the same command. Note this does not actually execute any - /// code; it is the caller's responsibility to load the file. - maybe_t resolve_command(const wcstring &cmd, const environment_t &env); - - /// FFI cover. This always uses globals, and returns an empty string instead of None. - wcstring resolve_command_ffi(const wcstring &cmd); - - /// Helper to actually perform an autoload. - /// This is a static function because it executes fish script, and so must be called without - /// holding any particular locks. - static void perform_autoload(const wcstring &path, parser_t &parser); - - /// Mark that a command previously returned from path_to_autoload is finished autoloading. - void mark_autoload_finished(const wcstring &cmd) { - size_t amt = current_autoloading_.erase(cmd); - assert(amt > 0 && "cmd was not being autoloaded"); - (void)amt; - } - - /// \return whether a command is currently being autoloaded. - bool autoload_in_progress(const wcstring &cmd) const { - return current_autoloading_.count(cmd) > 0; - } - - /// \return whether a command could potentially be autoloaded. - /// This does not actually mark the command as being autoloaded. - bool can_autoload(const wcstring &cmd); - - /// \return whether autoloading has been attempted for a command. - bool has_attempted_autoload(const wcstring &cmd); - - /// \return the names of all commands that have been autoloaded. Note this includes "in-flight" - /// commands. - std::vector get_autoloaded_commands() const; - - /// Mark that all autoloaded files have been forgotten. - /// Future calls to path_to_autoload() will return previously-returned paths. - void clear() { - // Note there is no reason to invalidate the cache here. - autoloaded_files_.clear(); - } -}; - -/// FFI helpers. -std::unique_ptr make_autoload_ffi(wcstring env_var_name); -void perform_autoload_ffi(const wcstring &path, parser_t &parser); - -#endif diff --git a/src/builtin.cpp b/src/builtin.cpp index 7fff068c7..741a6686e 100644 --- a/src/builtin.cpp +++ b/src/builtin.cpp @@ -1,67 +1,9 @@ -// Functions for executing builtin functions. -// -// How to add a new builtin function: -// -// 1). Create a function in builtin.c with the following signature: -// -// static maybe_t builtin_NAME(parser_t &parser, io_streams_t &streams, wchar_t -// **argv) -// -// where NAME is the name of the builtin, and args is a zero-terminated list of arguments. -// -// 2). Add a line like { L"NAME", &builtin_NAME, N_(L"Bla bla bla") }, to the builtin_data_t -// variable. The description is used by the completion system. Note that this array is sorted. -// -// 3). Create a file doc_src/NAME.rst, containing the manual for the builtin in -// reStructuredText-format. Check the other builtin manuals for proper syntax. -// -// 4). Use 'git add doc_src/NAME.txt' to start tracking changes to the documentation file. #include "config.h" // IWYU pragma: keep #include "builtin.h" -#include - -#include -#include -#include -#include -#include -#include - -#include "builtins/bind.h" -#include "builtins/commandline.h" -#include "builtins/complete.h" -#include "builtins/disown.h" -#include "builtins/eval.h" -#include "builtins/fg.h" -#include "builtins/history.h" -#include "builtins/jobs.h" -#include "builtins/read.h" -#include "builtins/set.h" -#include "builtins/shared.rs.h" -#include "builtins/source.h" -#include "builtins/ulimit.h" -#include "complete.h" -#include "cxx.h" -#include "cxxgen.h" -#include "fallback.h" // IWYU pragma: keep -#include "ffi.h" -#include "flog.h" -#include "io.h" -#include "null_terminated_array.h" -#include "parse_constants.h" -#include "parse_util.h" -#include "parser.h" -#include "proc.h" -#include "reader.h" -#include "wgetopt.h" #include "wutil.h" // IWYU pragma: keep -static maybe_t try_get_rust_builtin(const wcstring &cmd); -static maybe_t builtin_run_rust(parser_t &parser, io_streams_t &streams, - const std::vector &argv, RustBuiltin builtin); - /// Counts the number of arguments in the specified null-terminated array int builtin_count_args(const wchar_t *const *argv) { int argc; @@ -78,495 +20,12 @@ int builtin_count_args(const wchar_t *const *argv) { void builtin_wperror(const wchar_t *program_name, io_streams_t &streams) { char *err = std::strerror(errno); if (program_name != nullptr) { - streams.err.append(program_name); - streams.err.append(L": "); + streams.err()->append(program_name); + streams.err()->append(L": "); } if (err != nullptr) { const wcstring werr = str2wcstring(err); - streams.err.append(werr); - streams.err.push(L'\n'); - } -} - -static const wchar_t *const short_options = L"+:h"; -static const struct woption long_options[] = {{L"help", no_argument, 'h'}, {}}; - -int parse_help_only_cmd_opts(struct help_only_cmd_opts_t &opts, int *optind, int argc, - const wchar_t **argv, parser_t &parser, io_streams_t &streams) { - const wchar_t *cmd = argv[0]; - int opt; - wgetopter_t w; - while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, nullptr)) != -1) { - switch (opt) { //!OCLINT(too few branches) - case 'h': { - opts.print_help = true; - break; - } - case ':': { - builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]); - return STATUS_INVALID_ARGS; - } - case '?': { - builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]); - return STATUS_INVALID_ARGS; - } - default: { - DIE("unexpected retval from wgetopt_long"); - } - } - } - - *optind = w.woptind; - return STATUS_CMD_OK; -} - -/// Display help/usage information for the specified builtin or function from manpage -/// -/// @param name -/// builtin or function name to get up help for -/// -/// Process and print help for the specified builtin or function. -void builtin_print_help(parser_t &parser, const io_streams_t &streams, const wchar_t *name, - const wcstring &error_message) { - // This won't ever work if no_exec is set. - if (no_exec()) return; - const wcstring name_esc = escape_string(name); - wcstring cmd = format_string(L"__fish_print_help %ls ", name_esc.c_str()); - io_chain_t ios; - if (!error_message.empty()) { - cmd.append(escape_string(error_message)); - // If it's an error, redirect the output of __fish_print_help to stderr - ios.push_back(std::make_shared(STDOUT_FILENO, STDERR_FILENO)); - } - auto res = parser.eval(cmd, ios); - if (res.status.normal_exited() && res.status.exit_code() == 2) { - streams.err.append_format(BUILTIN_ERR_MISSING_HELP, name_esc.c_str(), name_esc.c_str()); - } -} - -/// Perform error reporting for encounter with unknown option. -void builtin_unknown_option(parser_t &parser, io_streams_t &streams, const wchar_t *cmd, - const wchar_t *opt, bool print_hints) { - streams.err.append_format(BUILTIN_ERR_UNKNOWN, cmd, opt); - if (print_hints) { - builtin_print_error_trailer(parser, streams.err, cmd); - } -} - -/// Perform error reporting for encounter with missing argument. -void builtin_missing_argument(parser_t &parser, io_streams_t &streams, const wchar_t *cmd, - const wchar_t *opt, bool print_hints) { - if (opt[0] == L'-' && opt[1] != L'-') { - // if c in -qc '-qc' is missing the argument, now opt is just 'c' - opt += std::wcslen(opt) - 1; - // now prepend - to output -c - streams.err.append_format(BUILTIN_ERR_MISSING, cmd, wcstring(L"-").append(opt).c_str()); - } else - streams.err.append_format(BUILTIN_ERR_MISSING, cmd, opt); - - if (print_hints) { - builtin_print_error_trailer(parser, streams.err, cmd); - } -} - -/// Print the backtrace and call for help that we use at the end of error messages. -void builtin_print_error_trailer(parser_t &parser, output_stream_t &b, const wchar_t *cmd) { - b.append(L"\n"); - const wcstring stacktrace = parser.current_line(); - // Don't print two empty lines if we don't have a stacktrace. - if (!stacktrace.empty()) { - b.append(stacktrace); - b.append(L"\n"); - } - b.append_format(_(L"(Type 'help %ls' for related documentation)\n"), cmd); -} - -/// A generic builtin that only supports showing a help message. This is only a placeholder that -/// prints the help message. Useful for commands that live in the parser. -static maybe_t builtin_generic(parser_t &parser, io_streams_t &streams, const wchar_t **argv) { - const wchar_t *cmd = argv[0]; - int argc = builtin_count_args(argv); - help_only_cmd_opts_t opts; - int optind; - int retval = parse_help_only_cmd_opts(opts, &optind, argc, argv, parser, streams); - if (retval != STATUS_CMD_OK) return retval; - - if (opts.print_help) { - builtin_print_help(parser, streams, cmd); - return STATUS_CMD_OK; - } - - // Hackish - if we have no arguments other than the command, we are a "naked invocation" and we - // just print help. - if (argc == 1 || wcscmp(cmd, L"time") == 0) { - builtin_print_help(parser, streams, cmd); - return STATUS_INVALID_ARGS; - } - - return STATUS_CMD_ERROR; -} - -static maybe_t implemented_in_rust(parser_t &, io_streams_t &, const wchar_t **) { - DIE("builtin is implemented in Rust, this should not be called"); -} - -/// This function handles both the 'continue' and the 'break' builtins that are used for loop -/// control. -static maybe_t builtin_break_continue(parser_t &parser, io_streams_t &streams, - const wchar_t **argv) { - int is_break = (std::wcscmp(argv[0], L"break") == 0); - int argc = builtin_count_args(argv); - - if (argc != 1) { - wcstring error_message = format_string(BUILTIN_ERR_UNKNOWN, argv[0], argv[1]); - builtin_print_help(parser, streams, argv[0], error_message); - return STATUS_INVALID_ARGS; - } - - // Paranoia: ensure we have a real loop. - // This is checked in the AST but we may be invoked dynamically, e.g. just via "eval break". - bool has_loop = false; - for (const auto &b : parser.blocks()) { - if (b.type() == block_type_t::while_block || b.type() == block_type_t::for_block) { - has_loop = true; - break; - } - if (b.is_function_call()) break; - } - if (!has_loop) { - wcstring error_message = format_string(_(L"%ls: Not inside of loop\n"), argv[0]); - builtin_print_help(parser, streams, argv[0], error_message); - return STATUS_CMD_ERROR; - } - - // Mark the status in the libdata. - parser.libdata().loop_status = is_break ? loop_status_t::breaks : loop_status_t::continues; - return STATUS_CMD_OK; -} - -/// Implementation of the builtin breakpoint command, used to launch the interactive debugger. -static maybe_t builtin_breakpoint(parser_t &parser, io_streams_t &streams, - const wchar_t **argv) { - const wchar_t *cmd = argv[0]; - if (argv[1] != nullptr) { - streams.err.append_format(BUILTIN_ERR_ARG_COUNT1, cmd, 0, builtin_count_args(argv) - 1); - return STATUS_INVALID_ARGS; - } - - // If we're not interactive then we can't enter the debugger. So treat this command as a no-op. - if (!parser.is_interactive()) { - return STATUS_CMD_ERROR; - } - - // Ensure we don't allow creating a breakpoint at an interactive prompt. There may be a simpler - // or clearer way to do this but this works. - const block_t *block1 = parser.block_at_index(1); - if (!block1 || block1->type() == block_type_t::breakpoint) { - streams.err.append_format(_(L"%ls: Command not valid at an interactive prompt\n"), cmd); - return STATUS_ILLEGAL_CMD; - } - - const block_t *bpb = parser.push_block(block_t::breakpoint_block()); - reader_read(parser, STDIN_FILENO, streams.io_chain ? *streams.io_chain : io_chain_t()); - parser.pop_block(bpb); - return parser.get_last_status(); -} - -static maybe_t builtin_true(parser_t &parser, io_streams_t &streams, const wchar_t **argv) { - UNUSED(parser); - UNUSED(streams); - UNUSED(argv); - return STATUS_CMD_OK; -} - -static maybe_t builtin_false(parser_t &parser, io_streams_t &streams, const wchar_t **argv) { - UNUSED(parser); - UNUSED(streams); - UNUSED(argv); - return STATUS_CMD_ERROR; -} - -static maybe_t builtin_gettext(parser_t &parser, io_streams_t &streams, const wchar_t **argv) { - UNUSED(parser); - for (int i = 1; i < builtin_count_args(argv); i++) { - streams.out.append(_(argv[i])); - } - return STATUS_CMD_OK; -} - -// END OF BUILTIN COMMANDS -// Below are functions for handling the builtin commands. -// THESE MUST BE SORTED BY NAME! Completion lookup uses binary search. - -// Data about all the builtin commands in fish. -// Functions that are bound to builtin_generic are handled directly by the parser. -// NOTE: These must be kept in sorted order! -static constexpr builtin_data_t builtin_datas[] = { - {L".", &builtin_source, N_(L"Evaluate contents of file")}, - {L":", &builtin_true, N_(L"Return a successful result")}, - {L"[", &implemented_in_rust, N_(L"Test a condition")}, - {L"_", &builtin_gettext, N_(L"Translate a string")}, - {L"abbr", &implemented_in_rust, N_(L"Manage abbreviations")}, - {L"and", &builtin_generic, N_(L"Run command if last command succeeded")}, - {L"argparse", &implemented_in_rust, N_(L"Parse options in fish script")}, - {L"begin", &builtin_generic, N_(L"Create a block of code")}, - {L"bg", &implemented_in_rust, N_(L"Send job to background")}, - {L"bind", &builtin_bind, N_(L"Handle fish key bindings")}, - {L"block", &implemented_in_rust, N_(L"Temporarily block delivery of events")}, - {L"break", &builtin_break_continue, N_(L"Stop the innermost loop")}, - {L"breakpoint", &builtin_breakpoint, N_(L"Halt execution and start debug prompt")}, - {L"builtin", &implemented_in_rust, N_(L"Run a builtin specifically")}, - {L"case", &builtin_generic, N_(L"Block of code to run conditionally")}, - {L"cd", &implemented_in_rust, N_(L"Change working directory")}, - {L"command", &implemented_in_rust, N_(L"Run a command specifically")}, - {L"commandline", &builtin_commandline, N_(L"Set or get the commandline")}, - {L"complete", &builtin_complete, N_(L"Edit command specific completions")}, - {L"contains", &implemented_in_rust, N_(L"Search for a specified string in a list")}, - {L"continue", &builtin_break_continue, N_(L"Skip over remaining innermost loop")}, - {L"count", &implemented_in_rust, N_(L"Count the number of arguments")}, - {L"disown", &builtin_disown, N_(L"Remove job from job list")}, - {L"echo", &implemented_in_rust, N_(L"Print arguments")}, - {L"else", &builtin_generic, N_(L"Evaluate block if condition is false")}, - {L"emit", &implemented_in_rust, N_(L"Emit an event")}, - {L"end", &builtin_generic, N_(L"End a block of commands")}, - {L"eval", &builtin_eval, N_(L"Evaluate a string as a statement")}, - {L"exec", &builtin_generic, N_(L"Run command in current process")}, - {L"exit", &implemented_in_rust, N_(L"Exit the shell")}, - {L"false", &builtin_false, N_(L"Return an unsuccessful result")}, - {L"fg", &builtin_fg, N_(L"Send job to foreground")}, - {L"for", &builtin_generic, N_(L"Perform a set of commands multiple times")}, - {L"function", &builtin_generic, N_(L"Define a new function")}, - {L"functions", &implemented_in_rust, N_(L"List or remove functions")}, - {L"history", &builtin_history, N_(L"History of commands executed by user")}, - {L"if", &builtin_generic, N_(L"Evaluate block if condition is true")}, - {L"jobs", &builtin_jobs, N_(L"Print currently running jobs")}, - {L"math", &implemented_in_rust, N_(L"Evaluate math expressions")}, - {L"not", &builtin_generic, N_(L"Negate exit status of job")}, - {L"or", &builtin_generic, N_(L"Execute command if previous command failed")}, - {L"path", &implemented_in_rust, N_(L"Handle paths")}, - {L"printf", &implemented_in_rust, N_(L"Prints formatted text")}, - {L"pwd", &implemented_in_rust, N_(L"Print the working directory")}, - {L"random", &implemented_in_rust, N_(L"Generate random number")}, - {L"read", &builtin_read, N_(L"Read a line of input into variables")}, - {L"realpath", &implemented_in_rust, N_(L"Show absolute path sans symlinks")}, - {L"return", &implemented_in_rust, N_(L"Stop the currently evaluated function")}, - {L"set", &builtin_set, N_(L"Handle environment variables")}, - {L"set_color", &implemented_in_rust, N_(L"Set the terminal color")}, - {L"source", &builtin_source, N_(L"Evaluate contents of file")}, - {L"status", &implemented_in_rust, N_(L"Return status information about fish")}, - {L"string", &implemented_in_rust, N_(L"Manipulate strings")}, - {L"switch", &builtin_generic, N_(L"Conditionally run blocks of code")}, - {L"test", &implemented_in_rust, N_(L"Test a condition")}, - {L"time", &builtin_generic, N_(L"Measure how long a command or block takes")}, - {L"true", &builtin_true, N_(L"Return a successful result")}, - {L"type", &implemented_in_rust, N_(L"Check if a thing is a thing")}, - {L"ulimit", &builtin_ulimit, N_(L"Get/set resource usage limits")}, - {L"wait", &implemented_in_rust, N_(L"Wait for background processes completed")}, - {L"while", &builtin_generic, N_(L"Perform a command multiple times")}, -}; -ASSERT_SORTED_BY_NAME(builtin_datas); - -#define BUILTIN_COUNT (sizeof builtin_datas / sizeof *builtin_datas) - -/// Look up a builtin_data_t for a specified builtin -/// -/// @param name -/// Name of the builtin -/// -/// @return -/// Pointer to a builtin_data_t -/// -static const builtin_data_t *builtin_lookup(const wcstring &name) { - return get_by_sorted_name(name.c_str(), builtin_datas); -} - -/// Is there a builtin command with the given name? -bool builtin_exists(const wcstring &cmd) { return static_cast(builtin_lookup(cmd)); } - -/// Is the command a keyword we need to special-case the handling of `-h` and `--help`. -static const wchar_t *const help_builtins[] = {L"for", L"while", L"function", L"if", - L"end", L"switch", L"case"}; -static bool cmd_needs_help(const wcstring &cmd) { return contains(help_builtins, cmd); } - -/// Execute a builtin command -proc_status_t builtin_run(parser_t &parser, const std::vector &argv, - io_streams_t &streams) { - if (argv.empty()) return proc_status_t::from_exit_code(STATUS_INVALID_ARGS); - const wcstring &cmdname = argv.front(); - - // We can be handed a keyword by the parser as if it was a command. This happens when the user - // follows the keyword by `-h` or `--help`. Since it isn't really a builtin command we need to - // handle displaying help for it here. - if (argv.size() == 2 && parse_util_argument_is_help(argv[1]) && cmd_needs_help(cmdname)) { - builtin_print_help(parser, streams, cmdname.c_str()); - return proc_status_t::from_exit_code(STATUS_CMD_OK); - } - - maybe_t builtin_ret; - - auto rust_builtin = try_get_rust_builtin(cmdname); - if (rust_builtin.has_value()) { - builtin_ret = builtin_run_rust(parser, streams, argv, *rust_builtin); - } else if (const builtin_data_t *data = builtin_lookup(cmdname)) { - // Construct the permutable argv array which the builtin expects, and execute the builtin. - null_terminated_array_t argv_arr(argv); - builtin_ret = data->func(parser, streams, argv_arr.get()); - } else { - FLOGF(error, UNKNOWN_BUILTIN_ERR_MSG, cmdname.c_str()); - return proc_status_t::from_exit_code(STATUS_CMD_ERROR); - } - - // Flush our out and error streams, and check for their errors. - int out_ret = streams.out.flush_and_check_error(); - int err_ret = streams.err.flush_and_check_error(); - - // Resolve our status code. - // If the builtin itself produced an error, use that error. - // Otherwise use any errors from writing to out and writing to err, in that order. - int code = builtin_ret.has_value() ? *builtin_ret : 0; - if (code == 0) code = out_ret; - if (code == 0) code = err_ret; - - // The exit code is cast to an 8-bit unsigned integer, so saturate to 255. Otherwise, - // multiples of 256 are reported as 0. - if (code > 255) code = 255; - - // Handle the case of an empty status. - if (code == 0 && !builtin_ret.has_value()) { - return proc_status_t::empty(); - } - if (code < 0) { - // If the code is below 0, constructing a proc_status_t - // would assert() out, which is a terrible failure mode - // So instead, what we do is we get a positive code, - // and we avoid 0. - code = abs((256 + code) % 256); - if (code == 0) code = 255; - FLOGF(warning, "builtin %ls returned invalid exit code %d", cmdname.c_str(), code); - } - return proc_status_t::from_exit_code(code); -} - -/// Returns a list of all builtin names. -std::vector builtin_get_names() { - std::vector result; - result.reserve(BUILTIN_COUNT); - for (const auto &builtin_data : builtin_datas) { - result.push_back(builtin_data.name); - } - return result; -} - -wcstring_list_ffi_t builtin_get_names_ffi() { return builtin_get_names(); } - -/// Insert all builtin names into list. -void builtin_get_names(completion_list_t *list) { - assert(list != nullptr); - list->reserve(list->size() + BUILTIN_COUNT); - for (const auto &builtin_data : builtin_datas) { - append_completion(list, builtin_data.name); - } -} - -/// Return a one-line description of the specified builtin. -const wchar_t *builtin_get_desc(const wcstring &name) { - const wchar_t *result = L""; - const builtin_data_t *builtin = builtin_lookup(name); - if (builtin) { - result = _(builtin->desc); - } - return result; -} - -static maybe_t try_get_rust_builtin(const wcstring &cmd) { - if (cmd == L"abbr") { - return RustBuiltin::Abbr; - } - if (cmd == L"argparse") { - return RustBuiltin::Argparse; - } - if (cmd == L"bg") { - return RustBuiltin::Bg; - } - if (cmd == L"block") { - return RustBuiltin::Block; - } - if (cmd == L"builtin") { - return RustBuiltin::Builtin; - } - if (cmd == L"cd") { - return RustBuiltin::Cd; - } - if (cmd == L"contains") { - return RustBuiltin::Contains; - } - if (cmd == L"command") { - return RustBuiltin::Command; - } - if (cmd == L"count") { - return RustBuiltin::Count; - } - if (cmd == L"echo") { - return RustBuiltin::Echo; - } - if (cmd == L"emit") { - return RustBuiltin::Emit; - } - if (cmd == L"exit") { - return RustBuiltin::Exit; - } - if (cmd == L"functions") { - return RustBuiltin::Functions; - } - if (cmd == L"math") { - return RustBuiltin::Math; - } - if (cmd == L"pwd") { - return RustBuiltin::Pwd; - } - if (cmd == L"random") { - return RustBuiltin::Random; - } - if (cmd == L"realpath") { - return RustBuiltin::Realpath; - } - if (cmd == L"set_color") { - return RustBuiltin::SetColor; - } - if (cmd == L"status") { - return RustBuiltin::Status; - } - if (cmd == L"string") { - return RustBuiltin::String; - } - if (cmd == L"test" || cmd == L"[") { - return RustBuiltin::Test; - } - if (cmd == L"type") { - return RustBuiltin::Type; - } - if (cmd == L"wait") { - return RustBuiltin::Wait; - } - if (cmd == L"path") { - return RustBuiltin::Path; - } - if (cmd == L"printf") { - return RustBuiltin::Printf; - } - if (cmd == L"return") { - return RustBuiltin::Return; - } - return none(); -} - -static maybe_t builtin_run_rust(parser_t &parser, io_streams_t &streams, - const std::vector &argv, RustBuiltin builtin) { - int status_code; - bool update_status = rust_run_builtin(parser, streams, argv, builtin, status_code); - if (update_status) { - return status_code; - } else { - return none(); + streams.err()->append(werr); + streams.err()->push(L'\n'); } } diff --git a/src/builtin.h b/src/builtin.h index dbd5730b6..1c2d6acb5 100644 --- a/src/builtin.h +++ b/src/builtin.h @@ -9,18 +9,24 @@ #include "maybe.h" #include "wutil.h" -class Parser; using parser_t = Parser; +struct Parser; +struct IoStreams; + +using parser_t = Parser; +using io_streams_t = IoStreams; + class proc_status_t; -class output_stream_t; -class IoStreams; using io_streams_t = IoStreams; -using completion_list_t = std::vector; +struct OutputStreamFfi; +using output_stream_t = OutputStreamFfi; +struct CompletionListFfi; +using completion_list_t = CompletionListFfi; /// Data structure to describe a builtin. struct builtin_data_t { // Name of the builtin. const wchar_t *name; // Function pointer to the builtin implementation. - maybe_t (*func)(parser_t &parser, io_streams_t &streams, const wchar_t **argv); + maybe_t (*func)(const parser_t &parser, io_streams_t &streams, const wchar_t **argv); // Description of what the builtin does. const wchar_t *desc; }; @@ -78,65 +84,8 @@ struct builtin_data_t { /// The send stuff to foreground message. #define FG_MSG _(L"Send job %d (%ls) to foreground\n") -bool builtin_exists(const wcstring &cmd); - -proc_status_t builtin_run(parser_t &parser, const std::vector &argv, - io_streams_t &streams); - -std::vector builtin_get_names(); -wcstring_list_ffi_t builtin_get_names_ffi(); -void builtin_get_names(completion_list_t *list); -const wchar_t *builtin_get_desc(const wcstring &name); - -wcstring builtin_help_get(parser_t &parser, const wchar_t *cmd); - -void builtin_print_help(parser_t &parser, const io_streams_t &streams, const wchar_t *name, - const wcstring &error_message = {}); int builtin_count_args(const wchar_t *const *argv); -void builtin_unknown_option(parser_t &parser, io_streams_t &streams, const wchar_t *cmd, - const wchar_t *opt, bool print_hints = true); - -void builtin_missing_argument(parser_t &parser, io_streams_t &streams, const wchar_t *cmd, - const wchar_t *opt, bool print_hints = true); - -void builtin_print_error_trailer(parser_t &parser, output_stream_t &b, const wchar_t *cmd); - void builtin_wperror(const wchar_t *program_name, io_streams_t &streams); -struct help_only_cmd_opts_t { - bool print_help = false; -}; -int parse_help_only_cmd_opts(help_only_cmd_opts_t &opts, int *optind, int argc, - const wchar_t **argv, parser_t &parser, io_streams_t &streams); - -/// An enum of the builtins implemented in Rust. -enum class RustBuiltin : int32_t { - Abbr, - Argparse, - Bg, - Block, - Builtin, - Cd, - Contains, - Command, - Count, - Echo, - Emit, - Exit, - Functions, - Math, - Path, - Printf, - Pwd, - Random, - Realpath, - Return, - SetColor, - Status, - String, - Test, - Type, - Wait, -}; #endif diff --git a/src/builtins/bind.cpp b/src/builtins/bind.cpp index 5b04fa22a..53231e552 100644 --- a/src/builtins/bind.cpp +++ b/src/builtins/bind.cpp @@ -21,6 +21,7 @@ #include "../parser.h" #include "../wgetopt.h" #include "../wutil.h" // IWYU pragma: keep +#include "builtins/shared.rs.h" enum { BIND_INSERT, BIND_ERASE, BIND_KEY_NAMES, BIND_FUNCTION_NAMES }; struct bind_cmd_opts_t { @@ -42,7 +43,7 @@ struct bind_cmd_opts_t { namespace { class builtin_bind_t { public: - maybe_t builtin_bind(parser_t &parser, io_streams_t &streams, const wchar_t **argv); + maybe_t builtin_bind(const parser_t &parser, io_streams_t &streams, const wchar_t **argv); builtin_bind_t() : input_mappings_(input_mappings()) {} @@ -54,7 +55,7 @@ class builtin_bind_t { /// lock again. acquired_lock input_mappings_; - void list(const wchar_t *bind_mode, bool user, parser_t &parser, io_streams_t &streams); + void list(const wchar_t *bind_mode, bool user, const parser_t &parser, io_streams_t &streams); void key_names(bool all, io_streams_t &streams); void function_names(io_streams_t &streams); bool add(const wcstring &seq, const wchar_t *const *cmds, size_t cmds_len, const wchar_t *mode, @@ -62,19 +63,19 @@ class builtin_bind_t { bool erase(const wchar_t *const *seq, bool all, const wchar_t *mode, bool use_terminfo, bool user, io_streams_t &streams); bool get_terminfo_sequence(const wcstring &seq, wcstring *out_seq, io_streams_t &streams) const; - bool insert(int optind, int argc, const wchar_t **argv, parser_t &parser, + bool insert(int optind, int argc, const wchar_t **argv, const parser_t &parser, io_streams_t &streams); void list_modes(io_streams_t &streams); - bool list_one(const wcstring &seq, const wcstring &bind_mode, bool user, parser_t &parser, + bool list_one(const wcstring &seq, const wcstring &bind_mode, bool user, const parser_t &parser, io_streams_t &streams); bool list_one(const wcstring &seq, const wcstring &bind_mode, bool user, bool preset, - parser_t &parser, io_streams_t &streams); + const parser_t &parser, io_streams_t &streams); }; /// List a single key binding. /// Returns false if no binding with that sequence and mode exists. bool builtin_bind_t::list_one(const wcstring &seq, const wcstring &bind_mode, bool user, - parser_t &parser, io_streams_t &streams) { + const parser_t &parser, io_streams_t &streams) { std::vector ecmds; wcstring sets_mode, out; @@ -117,12 +118,13 @@ bool builtin_bind_t::list_one(const wcstring &seq, const wcstring &bind_mode, bo } out.push_back(L'\n'); - if (!streams.out_is_redirected && isatty(STDOUT_FILENO)) { - std::vector colors; - highlight_shell(out, colors, parser.context()); - streams.out.append(str2wcstring(colorize(out, colors, parser.vars()))); + if (!streams.out_is_redirected() && isatty(STDOUT_FILENO)) { + auto ffi_colors = highlight_shell_ffi(out, *parser_context(parser), false, {}); + auto ffi_colored = colorize(out, *ffi_colors, parser.vars()); + std::string colored{ffi_colored.begin(), ffi_colored.end()}; + streams.out()->append(str2wcstring(std::move(colored))); } else { - streams.out.append(out); + streams.out()->append(out); } return true; @@ -131,7 +133,7 @@ bool builtin_bind_t::list_one(const wcstring &seq, const wcstring &bind_mode, bo // Overload with both kinds of bindings. // Returns false only if neither exists. bool builtin_bind_t::list_one(const wcstring &seq, const wcstring &bind_mode, bool user, - bool preset, parser_t &parser, io_streams_t &streams) { + bool preset, const parser_t &parser, io_streams_t &streams) { bool retval = false; if (preset) { retval |= list_one(seq, bind_mode, false, parser, streams); @@ -143,7 +145,7 @@ bool builtin_bind_t::list_one(const wcstring &seq, const wcstring &bind_mode, bo } /// List all current key bindings. -void builtin_bind_t::list(const wchar_t *bind_mode, bool user, parser_t &parser, +void builtin_bind_t::list(const wchar_t *bind_mode, bool user, const parser_t &parser, io_streams_t &streams) { const std::vector lst = input_mappings_->get_names(user); @@ -163,8 +165,8 @@ void builtin_bind_t::list(const wchar_t *bind_mode, bool user, parser_t &parser, void builtin_bind_t::key_names(bool all, io_streams_t &streams) { const std::vector names = input_terminfo_get_names(!all); for (const wcstring &name : names) { - streams.out.append(name); - streams.out.push(L'\n'); + streams.out()->append(name); + streams.out()->push(L'\n'); } } @@ -174,7 +176,7 @@ void builtin_bind_t::function_names(io_streams_t &streams) { for (const auto &name : names) { auto seq = name.c_str(); - streams.out.append_format(L"%ls\n", seq); + streams.out()->append(format_string(L"%ls\n", seq)); } } @@ -188,14 +190,15 @@ bool builtin_bind_t::get_terminfo_sequence(const wcstring &seq, wcstring *out_se wcstring eseq = escape_string(seq, ESCAPE_NO_PRINTABLES); if (!opts->silent) { if (errno == ENOENT) { - streams.err.append_format(_(L"%ls: No key with name '%ls' found\n"), L"bind", - eseq.c_str()); + streams.err()->append( + format_string(_(L"%ls: No key with name '%ls' found\n"), L"bind", eseq.c_str())); } else if (errno == EILSEQ) { - streams.err.append_format(_(L"%ls: Key with name '%ls' does not have any mapping\n"), - L"bind", eseq.c_str()); + streams.err()->append(format_string( + _(L"%ls: Key with name '%ls' does not have any mapping\n"), L"bind", eseq.c_str())); } else { - streams.err.append_format(_(L"%ls: Unknown error trying to bind to key named '%ls'\n"), - L"bind", eseq.c_str()); + streams.err()->append( + format_string(_(L"%ls: Unknown error trying to bind to key named '%ls'\n"), L"bind", + eseq.c_str())); } } return false; @@ -258,7 +261,7 @@ bool builtin_bind_t::erase(const wchar_t *const *seq, bool all, const wchar_t *m return res; } -bool builtin_bind_t::insert(int optind, int argc, const wchar_t **argv, parser_t &parser, +bool builtin_bind_t::insert(int optind, int argc, const wchar_t **argv, const parser_t &parser, io_streams_t &streams) { const wchar_t *cmd = argv[0]; int arg_count = argc - optind; @@ -272,7 +275,8 @@ bool builtin_bind_t::insert(int optind, int argc, const wchar_t **argv, parser_t } else { // Inserting both on the other hand makes no sense. if (opts->have_preset && opts->have_user) { - streams.err.append_format(BUILTIN_ERR_COMBO2_EXCLUSIVE, cmd, L"--preset", "--user"); + streams.err()->append( + format_string(BUILTIN_ERR_COMBO2_EXCLUSIVE, cmd, L"--preset", "--user")); return true; } } @@ -301,11 +305,11 @@ bool builtin_bind_t::insert(int optind, int argc, const wchar_t **argv, parser_t wcstring eseq = escape_string(argv[optind], ESCAPE_NO_PRINTABLES); if (!opts->silent) { if (opts->use_terminfo) { - streams.err.append_format(_(L"%ls: No binding found for key '%ls'\n"), cmd, - eseq.c_str()); + streams.err()->append(format_string(_(L"%ls: No binding found for key '%ls'\n"), + cmd, eseq.c_str())); } else { - streams.err.append_format(_(L"%ls: No binding found for sequence '%ls'\n"), cmd, - eseq.c_str()); + streams.err()->append(format_string( + _(L"%ls: No binding found for sequence '%ls'\n"), cmd, eseq.c_str())); } } return true; @@ -338,12 +342,13 @@ void builtin_bind_t::list_modes(io_streams_t &streams) { modes.insert(binding.mode); } for (const auto &mode : modes) { - streams.out.append_format(L"%ls\n", mode.c_str()); + streams.out()->append(format_string(L"%ls\n", mode.c_str())); } } static int parse_cmd_opts(bind_cmd_opts_t &opts, int *optind, //!OCLINT(high ncss method) - int argc, const wchar_t **argv, parser_t &parser, io_streams_t &streams) { + int argc, const wchar_t **argv, const parser_t &parser, + io_streams_t &streams) { const wchar_t *cmd = argv[0]; static const wchar_t *const short_options = L":aehkKfM:Lm:s"; static const struct woption long_options[] = {{L"all", no_argument, 'a'}, @@ -394,7 +399,7 @@ static int parse_cmd_opts(bind_cmd_opts_t &opts, int *optind, //!OCLINT(high nc } case L'M': { if (!valid_var_name(w.woptarg)) { - streams.err.append_format(BUILTIN_ERR_BIND_MODE, cmd, w.woptarg); + streams.err()->append(format_string(BUILTIN_ERR_BIND_MODE, cmd, w.woptarg)); return STATUS_INVALID_ARGS; } opts.bind_mode = w.woptarg; @@ -403,7 +408,7 @@ static int parse_cmd_opts(bind_cmd_opts_t &opts, int *optind, //!OCLINT(high nc } case L'm': { if (!valid_var_name(w.woptarg)) { - streams.err.append_format(BUILTIN_ERR_BIND_MODE, cmd, w.woptarg); + streams.err()->append(format_string(BUILTIN_ERR_BIND_MODE, cmd, w.woptarg)); return STATUS_INVALID_ARGS; } opts.sets_bind_mode = w.woptarg; @@ -424,11 +429,11 @@ static int parse_cmd_opts(bind_cmd_opts_t &opts, int *optind, //!OCLINT(high nc break; } case ':': { - builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]); + builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1], true); return STATUS_INVALID_ARGS; } case L'?': { - builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]); + builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1], true); return STATUS_INVALID_ARGS; } default: { @@ -444,7 +449,7 @@ static int parse_cmd_opts(bind_cmd_opts_t &opts, int *optind, //!OCLINT(high nc } // namespace /// The bind builtin, used for setting character sequences. -maybe_t builtin_bind_t::builtin_bind(parser_t &parser, io_streams_t &streams, +maybe_t builtin_bind_t::builtin_bind(const parser_t &parser, io_streams_t &streams, const wchar_t **argv) { const wchar_t *cmd = argv[0]; int argc = builtin_count_args(argv); @@ -499,7 +504,7 @@ maybe_t builtin_bind_t::builtin_bind(parser_t &parser, io_streams_t &stream break; } default: { - streams.err.append_format(_(L"%ls: Invalid state\n"), cmd); + streams.err()->append(format_string(_(L"%ls: Invalid state\n"), cmd)); return STATUS_CMD_ERROR; } } @@ -507,7 +512,10 @@ maybe_t builtin_bind_t::builtin_bind(parser_t &parser, io_streams_t &stream return STATUS_CMD_OK; } -maybe_t builtin_bind(parser_t &parser, io_streams_t &streams, const wchar_t **argv) { +int builtin_bind(const void *_parser, void *_streams, void *_argv) { + const auto &parser = *static_cast(_parser); + auto &streams = *static_cast(_streams); + auto argv = static_cast(_argv); builtin_bind_t bind; - return bind.builtin_bind(parser, streams, argv); + return *bind.builtin_bind(parser, streams, argv); } diff --git a/src/builtins/bind.h b/src/builtins/bind.h index 3b3aa7608..b30aaa764 100644 --- a/src/builtins/bind.h +++ b/src/builtins/bind.h @@ -4,8 +4,11 @@ #include "../maybe.h" -class Parser; using parser_t = Parser; -class IoStreams; using io_streams_t = IoStreams; -maybe_t builtin_bind(parser_t &parser, io_streams_t &streams, const wchar_t **argv); +struct Parser; +struct IoStreams; +using parser_t = Parser; +using io_streams_t = IoStreams; + +int builtin_bind(const void *parser, void *streams, void *argv); #endif diff --git a/src/builtins/commandline.cpp b/src/builtins/commandline.cpp index cb954d64f..4585aff07 100644 --- a/src/builtins/commandline.cpp +++ b/src/builtins/commandline.cpp @@ -23,6 +23,7 @@ #include "../tokenizer.h" #include "../wgetopt.h" #include "../wutil.h" // IWYU pragma: keep +#include "builtins/shared.rs.h" /// Which part of the comandbuffer are we operating on. enum { @@ -109,25 +110,30 @@ static void write_part(const wchar_t *begin, const wchar_t *end, int cut_at_curs if (token->type_ == token_type_t::string) { wcstring tmp = *tok->text_of(*token); - unescape_string_in_place(&tmp, UNESCAPE_INCOMPLETE); - out.append(tmp); + auto maybe_unescaped = unescape_string(tmp.c_str(), tmp.size(), UNESCAPE_INCOMPLETE, + STRING_STYLE_SCRIPT); + assert(maybe_unescaped); + out.append(*maybe_unescaped); out.push_back(L'\n'); } } - streams.out.append(out); + streams.out()->append(out); } else { if (cut_at_cursor) { - streams.out.append(begin, pos); + streams.out()->append(wcstring{begin, pos}); } else { - streams.out.append(begin, end - begin); + streams.out()->append(wcstring{begin, end}); } - streams.out.push(L'\n'); + streams.out()->push(L'\n'); } } /// The commandline builtin. It is used for specifying a new value for the commandline. -maybe_t builtin_commandline(parser_t &parser, io_streams_t &streams, const wchar_t **argv) { +int builtin_commandline(const void *_parser, void *_streams, void *_argv) { + const auto &parser = *static_cast(_parser); + auto &streams = *static_cast(_streams); + auto argv = static_cast(_argv); const commandline_state_t rstate = commandline_get_state(); const wchar_t *cmd = argv[0]; int buffer_part = 0; @@ -268,11 +274,11 @@ maybe_t builtin_commandline(parser_t &parser, io_streams_t &streams, const return STATUS_CMD_OK; } case ':': { - builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]); + builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1], true); return STATUS_INVALID_ARGS; } case L'?': { - builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]); + builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1], true); return STATUS_INVALID_ARGS; } default: { @@ -287,13 +293,13 @@ maybe_t builtin_commandline(parser_t &parser, io_streams_t &streams, const // Check for invalid switch combinations. if (buffer_part || cut_at_cursor || append_mode || tokenize || cursor_mode || line_mode || search_mode || paging_mode || selection_start_mode || selection_end_mode) { - streams.err.append_format(BUILTIN_ERR_COMBO, argv[0]); - builtin_print_error_trailer(parser, streams.err, cmd); + streams.err()->append(format_string(BUILTIN_ERR_COMBO, argv[0])); + builtin_print_error_trailer(parser, *streams.err(), cmd); return STATUS_INVALID_ARGS; } if (argc == w.woptind) { - builtin_missing_argument(parser, streams, cmd, argv[0]); + builtin_missing_argument(parser, streams, cmd, argv[0], true); return STATUS_INVALID_ARGS; } @@ -303,7 +309,7 @@ maybe_t builtin_commandline(parser_t &parser, io_streams_t &streams, const // Don't enqueue a repaint if we're currently in the middle of one, // because that's an infinite loop. if (mc == rl::repaint_mode || mc == rl::force_repaint || mc == rl::repaint) { - if (ld.is_repaint) continue; + if (ld.is_repaint()) continue; } // HACK: Execute these right here and now so they can affect any insertions/changes @@ -318,8 +324,9 @@ maybe_t builtin_commandline(parser_t &parser, io_streams_t &streams, const reader_queue_ch(*mc); } } else { - streams.err.append_format(_(L"%ls: Unknown input function '%ls'"), cmd, argv[i]); - builtin_print_error_trailer(parser, streams.err, cmd); + streams.err()->append( + format_string(_(L"%ls: Unknown input function '%ls'"), cmd, argv[i])); + builtin_print_error_trailer(parser, *streams.err(), cmd); return STATUS_INVALID_ARGS; } } @@ -329,22 +336,22 @@ maybe_t builtin_commandline(parser_t &parser, io_streams_t &streams, const if (selection_mode) { if (rstate.selection) { - streams.out.append(rstate.text.c_str() + rstate.selection->start, - rstate.selection->length); + streams.out()->append( + {rstate.text.c_str() + rstate.selection->start, rstate.selection->length}); } return STATUS_CMD_OK; } // Check for invalid switch combinations. if ((selection_start_mode || selection_end_mode) && (argc - w.woptind)) { - streams.err.append_format(BUILTIN_ERR_TOO_MANY_ARGUMENTS, argv[0]); - builtin_print_error_trailer(parser, streams.err, cmd); + streams.err()->append(format_string(BUILTIN_ERR_TOO_MANY_ARGUMENTS, argv[0])); + builtin_print_error_trailer(parser, *streams.err(), cmd); return STATUS_INVALID_ARGS; } if ((search_mode || line_mode || cursor_mode || paging_mode) && (argc - w.woptind > 1)) { - streams.err.append_format(BUILTIN_ERR_TOO_MANY_ARGUMENTS, argv[0]); - builtin_print_error_trailer(parser, streams.err, cmd); + streams.err()->append(format_string(BUILTIN_ERR_TOO_MANY_ARGUMENTS, argv[0])); + builtin_print_error_trailer(parser, *streams.err(), cmd); return STATUS_INVALID_ARGS; } @@ -352,16 +359,16 @@ maybe_t builtin_commandline(parser_t &parser, io_streams_t &streams, const (cursor_mode || line_mode || search_mode || paging_mode || paging_full_mode) && // Special case - we allow to get/set cursor position relative to the process/job/token. !(buffer_part && cursor_mode)) { - streams.err.append_format(BUILTIN_ERR_COMBO, argv[0]); - builtin_print_error_trailer(parser, streams.err, cmd); + streams.err()->append(format_string(BUILTIN_ERR_COMBO, argv[0])); + builtin_print_error_trailer(parser, *streams.err(), cmd); return STATUS_INVALID_ARGS; } if ((tokenize || cut_at_cursor) && (argc - w.woptind)) { - streams.err.append_format( + streams.err()->append(format_string( BUILTIN_ERR_COMBO2, cmd, - L"--cut-at-cursor and --tokenize can not be used when setting the commandline"); - builtin_print_error_trailer(parser, streams.err, cmd); + L"--cut-at-cursor and --tokenize can not be used when setting the commandline")); + builtin_print_error_trailer(parser, *streams.err(), cmd); return STATUS_INVALID_ARGS; } @@ -380,7 +387,8 @@ maybe_t builtin_commandline(parser_t &parser, io_streams_t &streams, const } if (line_mode) { - streams.out.append_format(L"%d\n", parse_util_lineno(rstate.text, rstate.cursor_pos)); + streams.out()->append( + format_string(L"%d\n", parse_util_lineno(rstate.text, rstate.cursor_pos))); return STATUS_CMD_OK; } @@ -402,7 +410,7 @@ maybe_t builtin_commandline(parser_t &parser, io_streams_t &streams, const return STATUS_CMD_ERROR; } source_offset_t start = rstate.selection->start; - streams.out.append_format(L"%lu\n", static_cast(start)); + streams.out()->append(format_string(L"%lu\n", static_cast(start))); return STATUS_CMD_OK; } @@ -411,7 +419,7 @@ maybe_t builtin_commandline(parser_t &parser, io_streams_t &streams, const return STATUS_CMD_ERROR; } source_offset_t end = rstate.selection->end(); - streams.out.append_format(L"%lu\n", static_cast(end)); + streams.out()->append(format_string(L"%lu\n", static_cast(end))); return STATUS_CMD_OK; } @@ -425,8 +433,8 @@ maybe_t builtin_commandline(parser_t &parser, io_streams_t &streams, const if (override_buffer) { current_buffer = override_buffer; current_cursor_pos = std::wcslen(current_buffer); - } else if (!ld.transient_commandlines.empty() && !cursor_mode) { - transient = ld.transient_commandlines.back(); + } else if (!ld.transient_commandlines_empty() && !cursor_mode) { + transient = *ld.transient_commandlines_back(); current_buffer = transient.c_str(); current_cursor_pos = transient.size(); } else if (rstate.initialized) { @@ -436,9 +444,9 @@ maybe_t builtin_commandline(parser_t &parser, io_streams_t &streams, const // There is no command line, either because we are not interactive, or because we are // interactive and are still reading init files (in which case we silently ignore this). if (!is_interactive_session()) { - streams.err.append(cmd); - streams.err.append(L": Can not set commandline in non-interactive mode\n"); - builtin_print_error_trailer(parser, streams.err, cmd); + streams.err()->append(cmd); + streams.err()->append(L": Can not set commandline in non-interactive mode\n"); + builtin_print_error_trailer(parser, *streams.err(), cmd); } return STATUS_CMD_ERROR; } @@ -481,8 +489,8 @@ maybe_t builtin_commandline(parser_t &parser, io_streams_t &streams, const if (argc - w.woptind) { long new_pos = fish_wcstol(argv[w.woptind]) + (begin - current_buffer); if (errno) { - streams.err.append_format(BUILTIN_ERR_NOT_NUMBER, cmd, argv[w.woptind]); - builtin_print_error_trailer(parser, streams.err, cmd); + streams.err()->append(format_string(BUILTIN_ERR_NOT_NUMBER, cmd, argv[w.woptind])); + builtin_print_error_trailer(parser, *streams.err(), cmd); } new_pos = @@ -490,7 +498,7 @@ maybe_t builtin_commandline(parser_t &parser, io_streams_t &streams, const commandline_set_buffer(current_buffer, static_cast(new_pos)); } else { size_t pos = current_cursor_pos - (begin - current_buffer); - streams.out.append_format(L"%lu\n", static_cast(pos)); + streams.out()->append(format_string(L"%lu\n", static_cast(pos))); } return STATUS_CMD_OK; } diff --git a/src/builtins/commandline.h b/src/builtins/commandline.h index 2189d370c..3ef03cf6f 100644 --- a/src/builtins/commandline.h +++ b/src/builtins/commandline.h @@ -4,8 +4,10 @@ #include "../maybe.h" -class Parser; using parser_t = Parser; -class IoStreams; using io_streams_t = IoStreams; +struct Parser; +using parser_t = Parser; +struct IoStreams; +using io_streams_t = IoStreams; -maybe_t builtin_commandline(parser_t &parser, io_streams_t &streams, const wchar_t **argv); +int builtin_commandline(const void *parser, void *streams, void *argv); #endif diff --git a/src/builtins/complete.cpp b/src/builtins/complete.cpp deleted file mode 100644 index 700862526..000000000 --- a/src/builtins/complete.cpp +++ /dev/null @@ -1,476 +0,0 @@ -// Functions used for implementing the complete builtin. -#include "config.h" // IWYU pragma: keep - -#include "complete.h" - -#include - -#include -#include -#include -#include - -#include "../builtin.h" -#include "../common.h" -#include "../complete.h" -#include "../env.h" -#include "../fallback.h" // IWYU pragma: keep -#include "../highlight.h" -#include "../io.h" -#include "../maybe.h" -#include "../parse_constants.h" -#include "../parse_util.h" -#include "../parser.h" -#include "../reader.h" -#include "../wcstringutil.h" -#include "../wgetopt.h" -#include "../wutil.h" // IWYU pragma: keep - -// builtin_complete_* are a set of rather silly looping functions that make sure that all the proper -// combinations of complete_add or complete_remove get called. This is needed since complete allows -// you to specify multiple switches on a single commandline, like 'complete -s a -s b -s c', but the -// complete_add function only accepts one short switch and one long switch. - -/// Silly function. -static void builtin_complete_add2(const wcstring &cmd, bool cmd_is_path, const wchar_t *short_opt, - const std::vector &gnu_opts, - const std::vector &old_opts, - completion_mode_t result_mode, - const std::vector &condition, const wchar_t *comp, - const wchar_t *desc, complete_flags_t flags) { - for (const wchar_t *s = short_opt; *s; s++) { - complete_add(cmd, cmd_is_path, wcstring{*s}, option_type_short, result_mode, condition, - comp, desc, flags); - } - - for (const wcstring &gnu_opt : gnu_opts) { - complete_add(cmd, cmd_is_path, gnu_opt, option_type_double_long, result_mode, condition, - comp, desc, flags); - } - - for (const wcstring &old_opt : old_opts) { - complete_add(cmd, cmd_is_path, old_opt, option_type_single_long, result_mode, condition, - comp, desc, flags); - } - - if (old_opts.empty() && gnu_opts.empty() && short_opt[0] == L'\0') { - complete_add(cmd, cmd_is_path, wcstring(), option_type_args_only, result_mode, condition, - comp, desc, flags); - } -} - -/// Silly function. -static void builtin_complete_add(const std::vector &cmds, - const std::vector &paths, const wchar_t *short_opt, - const std::vector &gnu_opt, - const std::vector &old_opt, - completion_mode_t result_mode, - const std::vector &condition, const wchar_t *comp, - const wchar_t *desc, complete_flags_t flags) { - for (const wcstring &cmd : cmds) { - builtin_complete_add2(cmd, false /* not path */, short_opt, gnu_opt, old_opt, result_mode, - condition, comp, desc, flags); - } - - for (const wcstring &path : paths) { - builtin_complete_add2(path, true /* is path */, short_opt, gnu_opt, old_opt, result_mode, - condition, comp, desc, flags); - } -} - -static void builtin_complete_remove_cmd(const wcstring &cmd, bool cmd_is_path, - const wchar_t *short_opt, - const std::vector &gnu_opt, - const std::vector &old_opt) { - bool removed = false; - for (const wchar_t *s = short_opt; *s; s++) { - complete_remove(cmd, cmd_is_path, wcstring{*s}, option_type_short); - removed = true; - } - - for (const wcstring &opt : old_opt) { - complete_remove(cmd, cmd_is_path, opt, option_type_single_long); - removed = true; - } - - for (const wcstring &opt : gnu_opt) { - complete_remove(cmd, cmd_is_path, opt, option_type_double_long); - removed = true; - } - - if (!removed) { - // This means that all loops were empty. - complete_remove_all(cmd, cmd_is_path); - } -} - -static void builtin_complete_remove(const std::vector &cmds, - const std::vector &paths, const wchar_t *short_opt, - const std::vector &gnu_opt, - const std::vector &old_opt) { - for (const wcstring &cmd : cmds) { - builtin_complete_remove_cmd(cmd, false /* not path */, short_opt, gnu_opt, old_opt); - } - - for (const wcstring &path : paths) { - builtin_complete_remove_cmd(path, true /* is path */, short_opt, gnu_opt, old_opt); - } -} - -static void builtin_complete_print(const wcstring &cmd, io_streams_t &streams, parser_t &parser) { - const wcstring repr = complete_print(cmd); - - // colorize if interactive - if (!streams.out_is_redirected && isatty(STDOUT_FILENO)) { - std::vector colors; - highlight_shell(repr, colors, parser.context()); - streams.out.append(str2wcstring(colorize(repr, colors, parser.vars()))); - } else { - streams.out.append(repr); - } -} - -/// Values used for long-only options. -enum { - opt_escape = 1, -}; -/// The complete builtin. Used for specifying programmable tab-completions. Calls the functions in -// complete.cpp for any heavy lifting. -maybe_t builtin_complete(parser_t &parser, io_streams_t &streams, const wchar_t **argv) { - const wchar_t *cmd = argv[0]; - int argc = builtin_count_args(argv); - completion_mode_t result_mode{}; - int remove = 0; - wcstring short_opt; - std::vector gnu_opt, old_opt, subcommand; - const wchar_t *comp = L"", *desc = L""; - std::vector condition; - bool do_complete = false; - bool have_do_complete_param = false; - wcstring do_complete_param; - std::vector cmd_to_complete; - std::vector path; - std::vector wrap_targets; - bool preserve_order = false; - bool unescape_output = true; - - static const wchar_t *const short_options = L":a:c:p:s:l:o:d:fFrxeuAn:C::w:hk"; - static const struct woption long_options[] = {{L"exclusive", no_argument, 'x'}, - {L"no-files", no_argument, 'f'}, - {L"force-files", no_argument, 'F'}, - {L"require-parameter", no_argument, 'r'}, - {L"path", required_argument, 'p'}, - {L"command", required_argument, 'c'}, - {L"short-option", required_argument, 's'}, - {L"long-option", required_argument, 'l'}, - {L"old-option", required_argument, 'o'}, - {L"subcommand", required_argument, 'S'}, - {L"description", required_argument, 'd'}, - {L"arguments", required_argument, 'a'}, - {L"erase", no_argument, 'e'}, - {L"unauthoritative", no_argument, 'u'}, - {L"authoritative", no_argument, 'A'}, - {L"condition", required_argument, 'n'}, - {L"wraps", required_argument, 'w'}, - {L"do-complete", optional_argument, 'C'}, - {L"help", no_argument, 'h'}, - {L"keep-order", no_argument, 'k'}, - {L"escape", no_argument, opt_escape}, - {}}; - - bool have_x = false; - - int opt; - wgetopter_t w; - while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, nullptr)) != -1) { - switch (opt) { - case 'x': { - result_mode.no_files = true; - result_mode.requires_param = true; - // Needed to print an error later. - have_x = true; - break; - } - case 'f': { - result_mode.no_files = true; - break; - } - case 'F': { - result_mode.force_files = true; - break; - } - case 'r': { - result_mode.requires_param = true; - break; - } - case 'k': { - preserve_order = true; - break; - } - case 'p': - case 'c': { - if (auto tmp = unescape_string(w.woptarg, UNESCAPE_SPECIAL)) { - if (opt == 'p') - path.push_back(*tmp); - else - cmd_to_complete.push_back(*tmp); - } else { - streams.err.append_format(_(L"%ls: Invalid token '%ls'\n"), cmd, w.woptarg); - return STATUS_INVALID_ARGS; - } - break; - } - case 'd': { - desc = w.woptarg; - assert(desc); - break; - } - case 'u': { - // This option was removed in commit 1911298 and is now a no-op. - break; - } - case 'A': { - // This option was removed in commit 1911298 and is now a no-op. - break; - } - case 's': { - short_opt.append(w.woptarg); - if (w.woptarg[0] == '\0') { - streams.err.append_format(_(L"%ls: -s requires a non-empty string\n"), cmd); - return STATUS_INVALID_ARGS; - } - break; - } - case 'l': { - gnu_opt.push_back(w.woptarg); - if (w.woptarg[0] == '\0') { - streams.err.append_format(_(L"%ls: -l requires a non-empty string\n"), cmd); - return STATUS_INVALID_ARGS; - } - break; - } - case 'o': { - old_opt.push_back(w.woptarg); - if (w.woptarg[0] == '\0') { - streams.err.append_format(_(L"%ls: -o requires a non-empty string\n"), cmd); - return STATUS_INVALID_ARGS; - } - break; - } - case 'S': { - subcommand.push_back(w.woptarg); - if (w.woptarg[0] == '\0') { - streams.err.append_format(_(L"%ls: -S requires a non-empty string\n"), cmd); - return STATUS_INVALID_ARGS; - } - break; - } - case 'a': { - comp = w.woptarg; - assert(comp); - break; - } - case 'e': { - remove = 1; - break; - } - case 'n': { - condition.push_back(w.woptarg); - assert(w.woptarg); - break; - } - case 'w': { - wrap_targets.push_back(w.woptarg); - break; - } - case 'C': { - do_complete = true; - have_do_complete_param = w.woptarg != nullptr; - if (have_do_complete_param) do_complete_param = w.woptarg; - break; - } - case opt_escape: { - unescape_output = false; - break; - } - case 'h': { - builtin_print_help(parser, streams, cmd); - return STATUS_CMD_OK; - } - case ':': { - builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]); - return STATUS_INVALID_ARGS; - } - case '?': { - builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]); - return STATUS_INVALID_ARGS; - } - default: { - DIE("unexpected retval from wgetopt_long"); - } - } - } - - if (result_mode.no_files && result_mode.force_files) { - if (!have_x) { - streams.err.append_format(BUILTIN_ERR_COMBO2, L"complete", - L"'--no-files' and '--force-files'"); - } else { - // The reason for us not wanting files is `-x`, - // which is short for `-rf`. - streams.err.append_format(BUILTIN_ERR_COMBO2, L"complete", - L"'--exclusive' and '--force-files'"); - } - return STATUS_INVALID_ARGS; - } - - if (w.woptind != argc) { - // Use one left-over arg as the do-complete argument - // to enable `complete -C "git check"`. - if (do_complete && !have_do_complete_param && argc == w.woptind + 1) { - do_complete_param = argv[argc - 1]; - have_do_complete_param = true; - } else if (!do_complete && cmd_to_complete.empty() && argc == w.woptind + 1) { - // Or use one left-over arg as the command to complete - cmd_to_complete.push_back(argv[argc - 1]); - } else { - streams.err.append_format(BUILTIN_ERR_TOO_MANY_ARGUMENTS, cmd); - builtin_print_error_trailer(parser, streams.err, cmd); - return STATUS_INVALID_ARGS; - } - } - - for (const auto &condition_string : condition) { - auto errors = new_parse_error_list(); - if (parse_util_detect_errors(condition_string, &*errors)) { - for (size_t i = 0; i < errors->size(); i++) { - wcstring prefix(wcstring(cmd) + L": -n '" + condition_string + L"': "); - streams.err.append(*errors->at(i)->describe_with_prefix( - condition_string, prefix, parser.is_interactive(), false)); - streams.err.push(L'\n'); - } - return STATUS_CMD_ERROR; - } - } - - if (comp && *comp) { - wcstring prefix; - prefix.append(cmd); - prefix.append(L": "); - - if (maybe_t err_text = parse_util_detect_errors_in_argument_list(comp, prefix)) { - streams.err.append_format(L"%ls: %ls: contains a syntax error\n", cmd, comp); - streams.err.append(*err_text); - streams.err.push(L'\n'); - return STATUS_CMD_ERROR; - } - } - - if (do_complete) { - if (!have_do_complete_param) { - // No argument given, try to use the current commandline. - auto state = commandline_get_state(); - if (!state.initialized) { - // This corresponds to using 'complete -C' in non-interactive mode. - // See #2361 . - builtin_missing_argument(parser, streams, cmd, L"-C"); - return STATUS_INVALID_ARGS; - } - do_complete_param = std::move(state.text); - } - const wchar_t *token; - - parse_util_token_extent(do_complete_param.c_str(), do_complete_param.size(), &token, - nullptr, nullptr, nullptr); - - // Create a scoped transient command line, so that builtin_commandline will see our - // argument, not the reader buffer. - parser.libdata().transient_commandlines.push_back(do_complete_param); - cleanup_t remove_transient([&] { parser.libdata().transient_commandlines.pop_back(); }); - - // Prevent accidental recursion (see #6171). - if (!parser.libdata().builtin_complete_current_commandline) { - if (!have_do_complete_param) { - parser.libdata().builtin_complete_current_commandline = true; - } - - completion_list_t comp = complete( - do_complete_param, completion_request_options_t::normal(), parser.context()); - - // Apply the same sort and deduplication treatment as pager completions - completions_sort_and_prioritize(&comp); - - for (const auto &next : comp) { - // Make a fake commandline, and then apply the completion to it. - const wcstring faux_cmdline = token; - size_t tmp_cursor = faux_cmdline.size(); - wcstring faux_cmdline_with_completion = completion_apply_to_command_line( - next.completion, next.flags, faux_cmdline, &tmp_cursor, false); - - // completion_apply_to_command_line will append a space unless COMPLETE_NO_SPACE - // is set. We don't want to set COMPLETE_NO_SPACE because that won't close - // quotes. What we want is to close the quote, but not append the space. So we - // just look for the space and clear it. - if (!(next.flags & COMPLETE_NO_SPACE) && - string_suffixes_string(L" ", faux_cmdline_with_completion)) { - faux_cmdline_with_completion.resize(faux_cmdline_with_completion.size() - 1); - } - - if (unescape_output) { - // The input data is meant to be something like you would have on the command - // line, e.g. includes backslashes. The output should be raw, i.e. unescaped. So - // we need to unescape the command line. See #1127. - unescape_string_in_place(&faux_cmdline_with_completion, UNESCAPE_DEFAULT); - } - - // Append any description. - if (!next.description.empty()) { - faux_cmdline_with_completion.reserve(faux_cmdline_with_completion.size() + 2 + - next.description.size()); - faux_cmdline_with_completion.push_back(L'\t'); - faux_cmdline_with_completion.append(next.description); - } - faux_cmdline_with_completion.push_back(L'\n'); - streams.out.append(faux_cmdline_with_completion); - } - - parser.libdata().builtin_complete_current_commandline = false; - } - } else if (path.empty() && gnu_opt.empty() && short_opt.empty() && old_opt.empty() && !remove && - !*comp && !*desc && condition.empty() && wrap_targets.empty() && - !result_mode.no_files && !result_mode.force_files && !result_mode.requires_param) { - // No arguments that would add or remove anything specified, so we print the definitions of - // all matching completions. - if (cmd_to_complete.empty()) { - builtin_complete_print(L"", streams, parser); - } else { - for (auto &cmd : cmd_to_complete) { - builtin_complete_print(cmd, streams, parser); - } - } - } else { - int flags = COMPLETE_AUTO_SPACE; - // HACK: Don't escape tildes because at the beginning of a token they probably mean - // $HOME, for example as produced by a recursive call to "complete -C". - flags |= COMPLETE_DONT_ESCAPE_TILDES; - if (preserve_order) { - flags |= COMPLETE_DONT_SORT; - } - - if (remove) { - builtin_complete_remove(cmd_to_complete, path, short_opt.c_str(), gnu_opt, old_opt); - } else { - builtin_complete_add(cmd_to_complete, path, short_opt.c_str(), gnu_opt, old_opt, - result_mode, condition, comp, desc, flags); - } - - // Handle wrap targets (probably empty). We only wrap commands, not paths. - for (const auto &wrap_target : wrap_targets) { - for (const auto &i : cmd_to_complete) { - (remove ? complete_remove_wrapper : complete_add_wrapper)(i, wrap_target); - } - } - } - - return STATUS_CMD_OK; -} diff --git a/src/builtins/complete.h b/src/builtins/complete.h deleted file mode 100644 index dcbed7c4d..000000000 --- a/src/builtins/complete.h +++ /dev/null @@ -1,11 +0,0 @@ -// Prototypes for functions for executing builtin_complete functions. -#ifndef FISH_BUILTIN_COMPLETE_H -#define FISH_BUILTIN_COMPLETE_H - -#include "../maybe.h" - -class Parser; using parser_t = Parser; -class IoStreams; using io_streams_t = IoStreams; - -maybe_t builtin_complete(parser_t &parser, io_streams_t &streams, const wchar_t **argv); -#endif diff --git a/src/builtins/disown.cpp b/src/builtins/disown.cpp deleted file mode 100644 index 8ae8bc0c8..000000000 --- a/src/builtins/disown.cpp +++ /dev/null @@ -1,111 +0,0 @@ -// Implementation of the disown builtin. -#include "config.h" // IWYU pragma: keep - -#include "disown.h" - -#include -#include -#include -#include -#include - -#include "../builtin.h" -#include "../common.h" -#include "../fallback.h" // IWYU pragma: keep -#include "../io.h" -#include "../maybe.h" -#include "../parser.h" -#include "../proc.h" -#include "../wutil.h" // IWYU pragma: keep - -/// Helper for builtin_disown. -static void disown_job(const wchar_t *cmd, io_streams_t &streams, job_t *j) { - assert(j && "Null job"); - - // Nothing to do if already disowned. - if (j->flags().disown_requested) return; - - // Stopped disowned jobs must be manually signaled; explain how to do so. - auto pgid = j->get_pgid(); - if (j->is_stopped()) { - if (pgid.has_value()) killpg(*pgid, SIGCONT); - const wchar_t *fmt = - _(L"%ls: job %d ('%ls') was stopped and has been signalled to continue.\n"); - streams.err.append_format(fmt, cmd, j->job_id(), j->command_wcstr()); - } - - // We cannot directly remove the job from the jobs() list as `disown` might be called - // within the context of a subjob which will cause the parent job to crash in exec_job(). - // Instead, we set a flag and the parser removes the job from the jobs list later. - j->mut_flags().disown_requested = true; - add_disowned_job(j); -} - -/// Builtin for removing jobs from the job list. -maybe_t builtin_disown(parser_t &parser, io_streams_t &streams, const wchar_t **argv) { - const wchar_t *cmd = argv[0]; - int argc = builtin_count_args(argv); - help_only_cmd_opts_t opts; - - int optind; - int retval = parse_help_only_cmd_opts(opts, &optind, argc, argv, parser, streams); - if (retval != STATUS_CMD_OK) return retval; - - if (opts.print_help) { - builtin_print_help(parser, streams, cmd); - return STATUS_CMD_OK; - } - - if (argv[1] == nullptr) { - // Select last constructed job (ie first job in the job queue) that is possible to disown. - // Stopped jobs can be disowned (they will be continued). - // Foreground jobs can be disowned. - // Even jobs that aren't under job control can be disowned! - job_t *job = nullptr; - for (const auto &j : parser.jobs()) { - if (j->is_constructed() && (!j->is_completed())) { - job = j.get(); - break; - } - } - - if (job) { - disown_job(cmd, streams, job); - retval = STATUS_CMD_OK; - } else { - streams.err.append_format(_(L"%ls: There are no suitable jobs\n"), cmd); - retval = STATUS_CMD_ERROR; - } - } else { - std::vector jobs; - - // If one argument is not a valid pid (i.e. integer >= 0), fail without disowning anything, - // but still print errors for all of them. - // Non-existent jobs aren't an error, but information about them is useful. - // Multiple PIDs may refer to the same job; include the job only once by using a set. - for (int i = 1; argv[i]; i++) { - int pid = fish_wcstoi(argv[i]); - if (errno || pid < 0) { - streams.err.append_format(_(L"%ls: '%ls' is not a valid job specifier\n"), cmd, - argv[i]); - retval = STATUS_INVALID_ARGS; - } else { - if (job_t *j = parser.job_get_from_pid(pid)) { - jobs.push_back(j); - } else { - streams.err.append_format(_(L"%ls: Could not find job '%d'\n"), cmd, pid); - } - } - } - if (retval != STATUS_CMD_OK) { - return retval; - } - - // Disown all target jobs. - for (job_t *j : jobs) { - disown_job(cmd, streams, j); - } - } - - return retval; -} diff --git a/src/builtins/disown.h b/src/builtins/disown.h deleted file mode 100644 index e41fb617b..000000000 --- a/src/builtins/disown.h +++ /dev/null @@ -1,11 +0,0 @@ -// Prototypes for executing builtin_disown function. -#ifndef FISH_BUILTIN_DISOWN_H -#define FISH_BUILTIN_DISOWN_H - -#include "../maybe.h" - -class Parser; using parser_t = Parser; -class IoStreams; using io_streams_t = IoStreams; - -maybe_t builtin_disown(parser_t &parser, io_streams_t &streams, const wchar_t **argv); -#endif diff --git a/src/builtins/eval.cpp b/src/builtins/eval.cpp deleted file mode 100644 index 9ba68f119..000000000 --- a/src/builtins/eval.cpp +++ /dev/null @@ -1,84 +0,0 @@ -// Functions for executing the eval builtin. -#include "config.h" // IWYU pragma: keep - -#include - -#include -#include - -#include "../builtin.h" -#include "../common.h" -#include "../fallback.h" // IWYU pragma: keep -#include "../io.h" -#include "../maybe.h" -#include "../parser.h" -#include "../proc.h" -#include "../wutil.h" // IWYU pragma: keep - -/// Implementation of eval builtin. -maybe_t builtin_eval(parser_t &parser, io_streams_t &streams, const wchar_t **argv) { - int argc = builtin_count_args(argv); - if (argc <= 1) { - return STATUS_CMD_OK; - } - - wcstring new_cmd; - for (int i = 1; i < argc; ++i) { - if (i > 1) new_cmd += L' '; - new_cmd += argv[i]; - } - - // Copy the full io chain; we may append bufferfills. - io_chain_t ios = *streams.io_chain; - - // If stdout is piped, then its output must go to the streams, not to the io_chain in our - // streams, because the pipe may be intended to be consumed by a process which - // is not yet launched (#6806). If stdout is NOT redirected, it must see the tty (#6955). So - // create a bufferfill for stdout if and only if stdout is piped. - // Note do not do this if stdout is merely redirected (say, to a file); we don't want to - // buffer in that case. - shared_ptr stdout_fill{}; - if (streams.out_is_piped) { - stdout_fill = io_bufferfill_t::create(parser.libdata().read_limit, STDOUT_FILENO); - if (!stdout_fill) { - // We were unable to create a pipe, probably fd exhaustion. - return STATUS_CMD_ERROR; - } - ios.push_back(stdout_fill); - } - - // Of course the same applies to stderr. - shared_ptr stderr_fill{}; - if (streams.err_is_piped) { - stderr_fill = io_bufferfill_t::create(parser.libdata().read_limit, STDERR_FILENO); - if (!stderr_fill) { - return STATUS_CMD_ERROR; - } - ios.push_back(stderr_fill); - } - - int status = STATUS_CMD_OK; - auto res = parser.eval_with(new_cmd, ios, streams.job_group, block_type_t::top); - if (res.was_empty) { - // Issue #5692, in particular, to catch `eval ""`, `eval "begin; end;"`, etc. - // where we have an argument but nothing is executed. - status = STATUS_CMD_OK; - } else { - status = res.status.status_value(); - } - - // Finish the bufferfills - exhaust and close our pipes. - // Copy the output from the bufferfill back to the streams. - // Note it is important that we hold no other references to the bufferfills here - they need to - // deallocate to close. - ios.clear(); - if (stdout_fill) { - separated_buffer_t output = io_bufferfill_t::finish(std::move(stdout_fill)); - streams.out.append_narrow_buffer(std::move(output)); - } - if (stderr_fill) { - separated_buffer_t errput = io_bufferfill_t::finish(std::move(stderr_fill)); - streams.err.append_narrow_buffer(std::move(errput)); - } - return status; -} diff --git a/src/builtins/eval.h b/src/builtins/eval.h deleted file mode 100644 index b6feb1fc1..000000000 --- a/src/builtins/eval.h +++ /dev/null @@ -1,11 +0,0 @@ -// Prototypes for executing builtin_eval function. -#ifndef FISH_BUILTIN_EVAL_H -#define FISH_BUILTIN_EVAL_H - -#include "../maybe.h" - -class Parser; using parser_t = Parser; -class IoStreams; using io_streams_t = IoStreams; - -maybe_t builtin_eval(parser_t &parser, io_streams_t &streams, const wchar_t **argv); -#endif diff --git a/src/builtins/fg.cpp b/src/builtins/fg.cpp deleted file mode 100644 index 6a2605a4b..000000000 --- a/src/builtins/fg.cpp +++ /dev/null @@ -1,139 +0,0 @@ -// Implementation of the fg builtin. -#include "config.h" // IWYU pragma: keep - -#include "fg.h" - -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include "../builtin.h" -#include "../common.h" -#include "../env.h" -#include "../fallback.h" // IWYU pragma: keep -#include "../fds.h" -#include "../io.h" -#include "../maybe.h" -#include "../parser.h" -#include "../proc.h" -#include "../reader.h" -#include "../tokenizer.h" -#include "../wutil.h" // IWYU pragma: keep -#include "job_group.rs.h" - -/// Builtin for putting a job in the foreground. -maybe_t builtin_fg(parser_t &parser, io_streams_t &streams, const wchar_t **argv) { - const wchar_t *cmd = argv[0]; - int argc = builtin_count_args(argv); - help_only_cmd_opts_t opts; - - int optind; - int retval = parse_help_only_cmd_opts(opts, &optind, argc, argv, parser, streams); - if (retval != STATUS_CMD_OK) return retval; - - if (opts.print_help) { - builtin_print_help(parser, streams, cmd); - return STATUS_CMD_OK; - } - - job_t *job = nullptr; - if (optind == argc) { - // Select last constructed job (i.e. first job in the job queue) that can be brought - // to the foreground. - - for (const auto &j : parser.jobs()) { - if (j->is_constructed() && (!j->is_completed()) && - ((j->is_stopped() || (!j->is_foreground())) && j->wants_job_control())) { - job = j.get(); - break; - } - } - if (!job) { - streams.err.append_format(_(L"%ls: There are no suitable jobs\n"), cmd); - } - } else if (optind + 1 < argc) { - // Specifying more than one job to put to the foreground is a syntax error, we still - // try to locate the job $argv[1], since we need to determine which error message to - // emit (ambigous job specification vs malformed job id). - bool found_job = false; - int pid = fish_wcstoi(argv[optind]); - if (errno == 0 && pid > 0) { - found_job = (parser.job_get_from_pid(pid) != nullptr); - } - - if (found_job) { - streams.err.append_format(_(L"%ls: Ambiguous job\n"), cmd); - } else { - streams.err.append_format(_(L"%ls: '%ls' is not a job\n"), cmd, argv[optind]); - } - - job = nullptr; - builtin_print_error_trailer(parser, streams.err, cmd); - } else { - int pid = abs(fish_wcstoi(argv[optind])); - if (errno) { - streams.err.append_format(BUILTIN_ERR_NOT_NUMBER, cmd, argv[optind]); - builtin_print_error_trailer(parser, streams.err, cmd); - } else { - job = parser.job_get_from_pid(pid); - if (!job || !job->is_constructed() || job->is_completed()) { - streams.err.append_format(_(L"%ls: No suitable job: %d\n"), cmd, pid); - job = nullptr; - } else if (!job->wants_job_control()) { - streams.err.append_format(_(L"%ls: Can't put job %d, '%ls' to foreground because " - L"it is not under job control\n"), - cmd, pid, job->command_wcstr()); - job = nullptr; - } - } - } - - if (!job) { - return STATUS_INVALID_ARGS; - } - - if (streams.err_is_redirected) { - streams.err.append_format(FG_MSG, job->job_id(), job->command_wcstr()); - } else { - // If we aren't redirecting, send output to real stderr, since stuff in sb_err won't get - // printed until the command finishes. - std::fwprintf(stderr, FG_MSG, job->job_id(), job->command_wcstr()); - } - - wcstring ft = *tok_command(job->command()); - if (!ft.empty()) { - // Provide value for `status current-command` - parser.libdata().status_vars.command = ft; - // Also provide a value for the deprecated fish 2.0 $_ variable - parser.set_var_and_fire(L"_", ENV_EXPORT, std::move(ft)); - // Provide value for `status current-commandline` - parser.libdata().status_vars.commandline = job->command(); - } - reader_write_title(job->command(), parser); - - // Note if tty transfer fails, we still try running the job. - parser.job_promote(job); - make_fd_blocking(STDIN_FILENO); - job->group->set_is_foreground(true); - if (job->group->wants_terminal() && (job->group->get_modes_ffi(sizeof(termios)) != nullptr)) { - auto *termios = (struct termios *)job->group->get_modes_ffi(sizeof(struct termios)); - int res = tcsetattr(STDIN_FILENO, TCSADRAIN, termios); - if (res < 0) wperror(L"tcsetattr"); - } - tty_transfer_t transfer; - transfer.to_job_group(job->group); - bool resumed = job->resume(); - if (resumed) { - job->continue_job(parser); - } - if (job->is_stopped()) transfer.save_tty_modes(); - transfer.reclaim(); - return resumed ? STATUS_CMD_OK : STATUS_CMD_ERROR; -} diff --git a/src/builtins/fg.h b/src/builtins/fg.h deleted file mode 100644 index ccbb4d040..000000000 --- a/src/builtins/fg.h +++ /dev/null @@ -1,11 +0,0 @@ -// Prototypes for executing builtin_fg function. -#ifndef FISH_BUILTIN_FG_H -#define FISH_BUILTIN_FG_H - -#include "../maybe.h" - -class Parser; using parser_t = Parser; -class IoStreams; using io_streams_t = IoStreams; - -maybe_t builtin_fg(parser_t &parser, io_streams_t &streams, const wchar_t **argv); -#endif diff --git a/src/builtins/history.cpp b/src/builtins/history.cpp deleted file mode 100644 index cc4384506..000000000 --- a/src/builtins/history.cpp +++ /dev/null @@ -1,332 +0,0 @@ -// Implementation of the history builtin. -#include "config.h" // IWYU pragma: keep - -#include "history.h" - -#include -#include -#include -#include -#include - -#include "../builtin.h" -#include "../common.h" -#include "../enum_map.h" -#include "../env.h" -#include "../fallback.h" // IWYU pragma: keep -#include "../history.h" -#include "../io.h" -#include "../maybe.h" -#include "../parser.h" -#include "../reader.h" -#include "../wgetopt.h" -#include "../wutil.h" // IWYU pragma: keep - -enum hist_cmd_t { - HIST_SEARCH = 1, - HIST_DELETE, - HIST_CLEAR, - HIST_MERGE, - HIST_SAVE, - HIST_UNDEF, - HIST_CLEAR_SESSION -}; - -// Must be sorted by string, not enum or random. -static const enum_map hist_enum_map[] = { - {HIST_CLEAR, L"clear"}, {HIST_CLEAR_SESSION, L"clear-session"}, - {HIST_DELETE, L"delete"}, {HIST_MERGE, L"merge"}, - {HIST_SAVE, L"save"}, {HIST_SEARCH, L"search"}, - {HIST_UNDEF, nullptr}, -}; - -struct history_cmd_opts_t { - hist_cmd_t hist_cmd = HIST_UNDEF; - history_search_type_t search_type = static_cast(-1); - const wchar_t *show_time_format = nullptr; - size_t max_items = SIZE_MAX; - bool print_help = false; - bool history_search_type_defined = false; - bool case_sensitive = false; - bool null_terminate = false; - bool reverse = false; -}; - -/// Note: Do not add new flags that represent subcommands. We're encouraging people to switch to -/// the non-flag subcommand form. While many of these flags are deprecated they must be -/// supported at least until fish 3.0 and possibly longer to avoid breaking everyones -/// config.fish and other scripts. -static const wchar_t *const short_options = L":CRcehmn:pt::z"; -static const struct woption long_options[] = {{L"prefix", no_argument, 'p'}, - {L"contains", no_argument, 'c'}, - {L"help", no_argument, 'h'}, - {L"show-time", optional_argument, 't'}, - {L"exact", no_argument, 'e'}, - {L"max", required_argument, 'n'}, - {L"null", no_argument, 'z'}, - {L"case-sensitive", no_argument, 'C'}, - {L"delete", no_argument, 1}, - {L"search", no_argument, 2}, - {L"save", no_argument, 3}, - {L"clear", no_argument, 4}, - {L"merge", no_argument, 5}, - {L"reverse", no_argument, 'R'}, - {}}; - -/// Remember the history subcommand and disallow selecting more than one history subcommand. -static bool set_hist_cmd(const wchar_t *cmd, hist_cmd_t *hist_cmd, hist_cmd_t sub_cmd, - io_streams_t &streams) { - if (*hist_cmd != HIST_UNDEF) { - streams.err.append_format(BUILTIN_ERR_COMBO2_EXCLUSIVE, cmd, - enum_to_str(*hist_cmd, hist_enum_map), - enum_to_str(sub_cmd, hist_enum_map)); - return false; - } - - *hist_cmd = sub_cmd; - return true; -} - -static bool check_for_unexpected_hist_args(const history_cmd_opts_t &opts, const wchar_t *cmd, - const std::vector &args, - io_streams_t &streams) { - if (opts.history_search_type_defined || opts.show_time_format || opts.null_terminate) { - const wchar_t *subcmd_str = enum_to_str(opts.hist_cmd, hist_enum_map); - streams.err.append_format(_(L"%ls: %ls: subcommand takes no options\n"), cmd, subcmd_str); - return true; - } - if (!args.empty()) { - const wchar_t *subcmd_str = enum_to_str(opts.hist_cmd, hist_enum_map); - streams.err.append_format(BUILTIN_ERR_ARG_COUNT2, cmd, subcmd_str, 0, args.size()); - return true; - } - return false; -} - -static int parse_cmd_opts(history_cmd_opts_t &opts, int *optind, //!OCLINT(high ncss method) - int argc, const wchar_t **argv, parser_t &parser, io_streams_t &streams) { - const wchar_t *cmd = argv[0]; - int opt; - wgetopter_t w; - while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, nullptr)) != -1) { - switch (opt) { - case 1: { - if (!set_hist_cmd(cmd, &opts.hist_cmd, HIST_DELETE, streams)) { - return STATUS_CMD_ERROR; - } - break; - } - case 2: { - if (!set_hist_cmd(cmd, &opts.hist_cmd, HIST_SEARCH, streams)) { - return STATUS_CMD_ERROR; - } - break; - } - case 3: { - if (!set_hist_cmd(cmd, &opts.hist_cmd, HIST_SAVE, streams)) { - return STATUS_CMD_ERROR; - } - break; - } - case 4: { - if (!set_hist_cmd(cmd, &opts.hist_cmd, HIST_CLEAR, streams)) { - return STATUS_CMD_ERROR; - } - break; - } - case 5: { - if (!set_hist_cmd(cmd, &opts.hist_cmd, HIST_MERGE, streams)) { - return STATUS_CMD_ERROR; - } - break; - } - case 'C': { - opts.case_sensitive = true; - break; - } - case 'R': { - opts.reverse = true; - break; - } - case 'p': { - opts.search_type = history_search_type_t::prefix_glob; - opts.history_search_type_defined = true; - break; - } - case 'c': { - opts.search_type = history_search_type_t::contains_glob; - opts.history_search_type_defined = true; - break; - } - case 'e': { - opts.search_type = history_search_type_t::exact; - opts.history_search_type_defined = true; - break; - } - case 't': { - opts.show_time_format = w.woptarg ? w.woptarg : L"# %c%n"; - break; - } - case 'n': { - long x = fish_wcstol(w.woptarg); - if (errno) { - streams.err.append_format(BUILTIN_ERR_NOT_NUMBER, cmd, w.woptarg); - return STATUS_INVALID_ARGS; - } - opts.max_items = static_cast(x); - break; - } - case 'z': { - opts.null_terminate = true; - break; - } - case 'h': { - opts.print_help = true; - break; - } - case ':': { - builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]); - return STATUS_INVALID_ARGS; - } - case '?': { - // Try to parse it as a number; e.g., "-123". - opts.max_items = fish_wcstol(argv[w.woptind - 1] + 1); - if (errno) { - builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]); - return STATUS_INVALID_ARGS; - } - w.nextchar = nullptr; - break; - } - default: { - DIE("unexpected retval from wgetopt_long"); - } - } - } - - *optind = w.woptind; - return STATUS_CMD_OK; -} - -/// Manipulate history of interactive commands executed by the user. -maybe_t builtin_history(parser_t &parser, io_streams_t &streams, const wchar_t **argv) { - const wchar_t *cmd = argv[0]; - int argc = builtin_count_args(argv); - history_cmd_opts_t opts; - - int optind; - int retval = parse_cmd_opts(opts, &optind, argc, argv, parser, streams); - if (retval != STATUS_CMD_OK) return retval; - - if (opts.print_help) { - builtin_print_help(parser, streams, cmd); - return STATUS_CMD_OK; - } - - // Use the default history if we have none (which happens if invoked non-interactively, e.g. - // from webconfig.py. - std::shared_ptr history = commandline_get_state().history; - if (!history) history = history_t::with_name(history_session_id(parser.vars())); - - // If a history command hasn't already been specified via a flag check the first word. - // Note that this can be simplified after we eliminate allowing subcommands as flags. - // See the TODO above regarding the `long_options` array. - if (optind < argc) { - constexpr size_t hist_enum_map_len = sizeof hist_enum_map / sizeof *hist_enum_map; - hist_cmd_t subcmd = str_to_enum(argv[optind], hist_enum_map, hist_enum_map_len); - if (subcmd != HIST_UNDEF) { - if (!set_hist_cmd(cmd, &opts.hist_cmd, subcmd, streams)) { - return STATUS_INVALID_ARGS; - } - optind++; - } - } - - // Every argument that we haven't consumed already is an argument for a subcommand (e.g., a - // search term). - const std::vector args(argv + optind, argv + argc); - - // Establish appropriate defaults. - if (opts.hist_cmd == HIST_UNDEF) opts.hist_cmd = HIST_SEARCH; - if (!opts.history_search_type_defined) { - if (opts.hist_cmd == HIST_SEARCH) opts.search_type = history_search_type_t::contains_glob; - if (opts.hist_cmd == HIST_DELETE) opts.search_type = history_search_type_t::exact; - } - - int status = STATUS_CMD_OK; - switch (opts.hist_cmd) { - case HIST_SEARCH: { - if (!history->search(opts.search_type, args, opts.show_time_format, opts.max_items, - opts.case_sensitive, opts.null_terminate, opts.reverse, - parser.cancel_checker(), streams)) { - status = STATUS_CMD_ERROR; - } - break; - } - case HIST_DELETE: { - // TODO: Move this code to the history module and support the other search types - // including case-insensitive matches. At this time we expect the non-exact deletions to - // be handled only by the history function's interactive delete feature. - if (opts.search_type != history_search_type_t::exact) { - streams.err.append_format(_(L"builtin history delete only supports --exact\n")); - status = STATUS_INVALID_ARGS; - break; - } - if (!opts.case_sensitive) { - streams.err.append_format( - _(L"builtin history delete --exact requires --case-sensitive\n")); - status = STATUS_INVALID_ARGS; - break; - } - for (const wcstring &delete_string : args) { - history->remove(delete_string); - } - break; - } - case HIST_CLEAR: { - if (check_for_unexpected_hist_args(opts, cmd, args, streams)) { - status = STATUS_INVALID_ARGS; - break; - } - history->clear(); - history->save(); - break; - } - case HIST_CLEAR_SESSION: { - if (check_for_unexpected_hist_args(opts, cmd, args, streams)) { - status = STATUS_INVALID_ARGS; - break; - } - history->clear_session(); - history->save(); - break; - } - case HIST_MERGE: { - if (check_for_unexpected_hist_args(opts, cmd, args, streams)) { - status = STATUS_INVALID_ARGS; - break; - } - - if (in_private_mode(parser.vars())) { - streams.err.append_format(_(L"%ls: can't merge history in private mode\n"), cmd); - status = STATUS_INVALID_ARGS; - break; - } - history->incorporate_external_changes(); - break; - } - case HIST_SAVE: { - if (check_for_unexpected_hist_args(opts, cmd, args, streams)) { - status = STATUS_INVALID_ARGS; - break; - } - history->save(); - break; - } - case HIST_UNDEF: { - DIE("Unexpected HIST_UNDEF seen"); - } - } - - return status; -} diff --git a/src/builtins/history.h b/src/builtins/history.h deleted file mode 100644 index 97bb2408f..000000000 --- a/src/builtins/history.h +++ /dev/null @@ -1,11 +0,0 @@ -// Prototypes for executing builtin_history function. -#ifndef FISH_BUILTIN_HISTORY_H -#define FISH_BUILTIN_HISTORY_H - -#include "../maybe.h" - -class Parser; using parser_t = Parser; -class IoStreams; using io_streams_t = IoStreams; - -maybe_t builtin_history(parser_t &parser, io_streams_t &streams, const wchar_t **argv); -#endif diff --git a/src/builtins/jobs.cpp b/src/builtins/jobs.cpp deleted file mode 100644 index d319dd05f..000000000 --- a/src/builtins/jobs.cpp +++ /dev/null @@ -1,245 +0,0 @@ -// Functions for executing the jobs builtin. -#include "config.h" // IWYU pragma: keep - -#include -#include -#include - -#include "../builtin.h" -#include "../common.h" -#include "../fallback.h" // IWYU pragma: keep -#include "../io.h" -#include "../maybe.h" -#include "../parser.h" -#include "../proc.h" -#include "../wgetopt.h" -#include "../wutil.h" // IWYU pragma: keep - -/// Print modes for the jobs builtin. -enum { - JOBS_DEFAULT, // print lots of general info - JOBS_PRINT_PID, // print pid of each process in job - JOBS_PRINT_COMMAND, // print command name of each process in job - JOBS_PRINT_GROUP, // print group id of job - JOBS_PRINT_NOTHING, // print nothing (exit status only) -}; - -/// Calculates the cpu usage (as a fraction of 1) of the specified job. -/// This may exceed 1 if there are multiple CPUs! -static double cpu_use(const job_t *j) { - double u = 0; - for (const process_ptr_t &p : j->processes) { - timepoint_t now = timef(); - clock_ticks_t jiffies = proc_get_jiffies(p->pid); - double since = now - p->last_time; - if (since > 0 && jiffies > p->last_jiffies) { - u += clock_ticks_to_seconds(jiffies - p->last_jiffies) / since; - } - } - return u; -} - -/// Print information about the specified job. -static void builtin_jobs_print(const job_t *j, int mode, int header, io_streams_t &streams) { - int pgid = INVALID_PID; - { - auto job_pgid = j->get_pgid(); - if (job_pgid.has_value()) { - pgid = *job_pgid; - } - } - - wcstring out; - switch (mode) { - case JOBS_PRINT_NOTHING: { - break; - } - case JOBS_DEFAULT: { - if (header) { - // Print table header before first job. - out.append(_(L"Job\tGroup\t")); - if (have_proc_stat()) { - out.append(_(L"CPU\t")); - } - out.append(_(L"State\tCommand\n")); - } - - append_format(out, L"%d\t%d\t", j->job_id(), pgid); - - if (have_proc_stat()) { - append_format(out, L"%.0f%%\t", 100. * cpu_use(j)); - } - - out.append(j->is_stopped() ? _(L"stopped") : _(L"running")); - out.append(L"\t"); - - const wcstring cmd = escape_string(j->command(), ESCAPE_NO_PRINTABLES); - out.append(cmd); - - out.append(L"\n"); - streams.out.append(out); - break; - } - case JOBS_PRINT_GROUP: { - if (header) { - // Print table header before first job. - out.append(_(L"Group\n")); - } - append_format(out, L"%d\n", pgid); - streams.out.append(out); - break; - } - case JOBS_PRINT_PID: { - if (header) { - // Print table header before first job. - out.append(_(L"Process\n")); - } - - for (const process_ptr_t &p : j->processes) { - append_format(out, L"%d\n", p->pid); - } - streams.out.append(out); - break; - } - case JOBS_PRINT_COMMAND: { - if (header) { - // Print table header before first job. - out.append(_(L"Command\n")); - } - - for (const process_ptr_t &p : j->processes) { - append_format(out, L"%ls\n", p->argv0()); - } - streams.out.append(out); - break; - } - default: { - DIE("unexpected mode"); - } - } -} - -/// The jobs builtin. Used for printing running jobs. Defined in builtin_jobs.c. -maybe_t builtin_jobs(parser_t &parser, io_streams_t &streams, const wchar_t **argv) { - const wchar_t *cmd = argv[0]; - int argc = builtin_count_args(argv); - bool found = false; - int mode = JOBS_DEFAULT; - bool print_last = false; - - static const wchar_t *const short_options = L":cghlpq"; - static const struct woption long_options[] = { - {L"command", no_argument, 'c'}, {L"group", no_argument, 'g'}, - {L"help", no_argument, 'h'}, {L"last", no_argument, 'l'}, - {L"pid", no_argument, 'p'}, {L"quiet", no_argument, 'q'}, - {L"query", no_argument, 'q'}, {}}; - - int opt; - wgetopter_t w; - while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, nullptr)) != -1) { - switch (opt) { - case 'p': { - mode = JOBS_PRINT_PID; - break; - } - case 'q': { - mode = JOBS_PRINT_NOTHING; - break; - } - case 'c': { - mode = JOBS_PRINT_COMMAND; - break; - } - case 'g': { - mode = JOBS_PRINT_GROUP; - break; - } - case 'l': { - print_last = true; - break; - } - case 'h': { - builtin_print_help(parser, streams, cmd); - return STATUS_CMD_OK; - } - case ':': { - builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]); - return STATUS_INVALID_ARGS; - } - case '?': { - builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]); - return STATUS_INVALID_ARGS; - } - default: { - DIE("unexpected retval from wgetopt_long"); - } - } - } - - if (print_last) { - // Ignore unconstructed jobs, i.e. ourself. - for (const auto &j : parser.jobs()) { - if (j->is_visible()) { - builtin_jobs_print(j.get(), mode, !streams.out_is_redirected, streams); - return STATUS_CMD_OK; - } - } - return STATUS_CMD_ERROR; - - } else { - if (w.woptind < argc) { - int i; - - for (i = w.woptind; i < argc; i++) { - const job_t *j = nullptr; - - if (argv[i][0] == L'%') { - int job_id = fish_wcstoi(argv[i] + 1); - if (errno || job_id < 0) { - streams.err.append_format(_(L"%ls: '%ls' is not a valid job id\n"), cmd, - argv[i]); - return STATUS_INVALID_ARGS; - } - j = parser.job_with_id(job_id); - } else { - int pid = fish_wcstoi(argv[i]); - if (errno || pid < 0) { - streams.err.append_format(_(L"%ls: '%ls' is not a valid process id\n"), cmd, - argv[i]); - return STATUS_INVALID_ARGS; - } - j = parser.job_get_from_pid(pid); - } - - if (j && !j->is_completed() && j->is_constructed()) { - builtin_jobs_print(j, mode, false, streams); - found = true; - } else { - if (mode != JOBS_PRINT_NOTHING) { - streams.err.append_format(_(L"%ls: No suitable job: %ls\n"), cmd, argv[i]); - } - return STATUS_CMD_ERROR; - } - } - } else { - for (const auto &j : parser.jobs()) { - // Ignore unconstructed jobs, i.e. ourself. - if (j->is_visible()) { - builtin_jobs_print(j.get(), mode, !found && !streams.out_is_redirected, - streams); - found = true; - } - } - } - } - - if (!found) { - // Do not babble if not interactive. - if (!streams.out_is_redirected && mode != JOBS_PRINT_NOTHING) { - streams.out.append_format(_(L"%ls: There are no jobs\n"), argv[0]); - } - return STATUS_CMD_ERROR; - } - - return STATUS_CMD_OK; -} diff --git a/src/builtins/jobs.h b/src/builtins/jobs.h deleted file mode 100644 index 85f579d23..000000000 --- a/src/builtins/jobs.h +++ /dev/null @@ -1,14 +0,0 @@ -// Prototypes for functions for executing builtin_jobs functions. -#ifndef FISH_BUILTIN_JOBS_H -#define FISH_BUILTIN_JOBS_H - -#include -#include - -#include "../io.h" -#include "../maybe.h" - -class Parser; using parser_t = Parser; - -maybe_t builtin_jobs(parser_t &parser, io_streams_t &streams, const wchar_t **argv); -#endif diff --git a/src/builtins/read.cpp b/src/builtins/read.cpp index 50172a9d0..2f78dd409 100644 --- a/src/builtins/read.cpp +++ b/src/builtins/read.cpp @@ -26,6 +26,7 @@ #include "../wcstringutil.h" #include "../wgetopt.h" #include "../wutil.h" // IWYU pragma: keep +#include "builtins/shared.rs.h" namespace { struct read_cmd_opts_t { @@ -75,7 +76,8 @@ static const struct woption long_options[] = {{L"array", no_argument, 'a'}, {}}; static int parse_cmd_opts(read_cmd_opts_t &opts, int *optind, //!OCLINT(high ncss method) - int argc, const wchar_t **argv, parser_t &parser, io_streams_t &streams) { + int argc, const wchar_t **argv, const parser_t &parser, + io_streams_t &streams) { const wchar_t *cmd = argv[0]; int opt; wgetopter_t w; @@ -95,9 +97,10 @@ static int parse_cmd_opts(read_cmd_opts_t &opts, int *optind, //!OCLINT(high nc break; } case 'i': { - streams.err.append_format(_(L"%ls: usage of -i for --silent is deprecated. Please " - L"use -s or --silent instead.\n"), - cmd); + streams.err()->append( + format_string(_(L"%ls: usage of -i for --silent is deprecated. Please " + L"use -s or --silent instead.\n"), + cmd)); return STATUS_INVALID_ARGS; } case L'f': { @@ -124,14 +127,14 @@ static int parse_cmd_opts(read_cmd_opts_t &opts, int *optind, //!OCLINT(high nc opts.nchars = fish_wcstoi(w.woptarg); if (errno) { if (errno == ERANGE) { - streams.err.append_format(_(L"%ls: Argument '%ls' is out of range\n"), cmd, - w.woptarg); - builtin_print_error_trailer(parser, streams.err, cmd); + streams.err()->append(format_string( + _(L"%ls: Argument '%ls' is out of range\n"), cmd, w.woptarg)); + builtin_print_error_trailer(parser, *streams.err(), cmd); return STATUS_INVALID_ARGS; } - streams.err.append_format(BUILTIN_ERR_NOT_NUMBER, cmd, w.woptarg); - builtin_print_error_trailer(parser, streams.err, cmd); + streams.err()->append(format_string(BUILTIN_ERR_NOT_NUMBER, cmd, w.woptarg)); + builtin_print_error_trailer(parser, *streams.err(), cmd); return STATUS_INVALID_ARGS; } break; @@ -177,11 +180,11 @@ static int parse_cmd_opts(read_cmd_opts_t &opts, int *optind, //!OCLINT(high nc break; } case ':': { - builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]); + builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1], true); return STATUS_INVALID_ARGS; } case L'?': { - builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]); + builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1], true); return STATUS_INVALID_ARGS; } default: { @@ -196,8 +199,8 @@ static int parse_cmd_opts(read_cmd_opts_t &opts, int *optind, //!OCLINT(high nc /// Read from the tty. This is only valid when the stream is stdin and it is attached to a tty and /// we weren't asked to split on null characters. -static int read_interactive(parser_t &parser, wcstring &buff, int nchars, bool shell, bool silent, - const wchar_t *prompt, const wchar_t *right_prompt, +static int read_interactive(const parser_t &parser, wcstring &buff, int nchars, bool shell, + bool silent, const wchar_t *prompt, const wchar_t *right_prompt, const wchar_t *commandline, int in) { int exit_res = STATUS_CMD_OK; @@ -224,7 +227,7 @@ static int read_interactive(parser_t &parser, wcstring &buff, int nchars, bool s reader_push(parser, wcstring{}, std::move(conf)); commandline_set_buffer(commandline, std::wcslen(commandline)); - scoped_push interactive{&parser.libdata().is_interactive, true}; + scoped_push interactive{&parser.libdata_pods_mut().is_interactive, true}; auto mline = reader_readline(nchars); interactive.restore(); @@ -352,22 +355,24 @@ static int read_one_char_at_a_time(int fd, wcstring &buff, int nchars, bool spli /// Validate the arguments given to `read` and provide defaults where needed. static int validate_read_args(const wchar_t *cmd, read_cmd_opts_t &opts, int argc, - const wchar_t *const *argv, parser_t &parser, io_streams_t &streams) { + const wchar_t *const *argv, const parser_t &parser, + io_streams_t &streams) { if (opts.prompt && opts.prompt_str) { - streams.err.append_format(_(L"%ls: Options %ls and %ls cannot be used together\n"), cmd, - L"-p", L"-P"); - builtin_print_error_trailer(parser, streams.err, cmd); + streams.err()->append(format_string( + _(L"%ls: Options %ls and %ls cannot be used together\n"), cmd, L"-p", L"-P")); + builtin_print_error_trailer(parser, *streams.err(), cmd); return STATUS_INVALID_ARGS; } if (opts.have_delimiter && opts.one_line) { - streams.err.append_format(_(L"%ls: Options %ls and %ls cannot be used together\n"), cmd, - L"--delimiter", L"--line"); + streams.err()->append( + format_string(_(L"%ls: Options %ls and %ls cannot be used together\n"), cmd, + L"--delimiter", L"--line")); return STATUS_INVALID_ARGS; } if (opts.one_line && opts.split_null) { - streams.err.append_format(_(L"%ls: Options %ls and %ls cannot be used together\n"), cmd, - L"-z", L"--line"); + streams.err()->append(format_string( + _(L"%ls: Options %ls and %ls cannot be used together\n"), cmd, L"-z", L"--line")); return STATUS_INVALID_ARGS; } @@ -379,55 +384,57 @@ static int validate_read_args(const wchar_t *cmd, read_cmd_opts_t &opts, int arg } if ((opts.place & ENV_UNEXPORT) && (opts.place & ENV_EXPORT)) { - streams.err.append_format(BUILTIN_ERR_EXPUNEXP, cmd); - builtin_print_error_trailer(parser, streams.err, cmd); + streams.err()->append(format_string(BUILTIN_ERR_EXPUNEXP, cmd)); + builtin_print_error_trailer(parser, *streams.err(), cmd); return STATUS_INVALID_ARGS; } if ((opts.place & ENV_LOCAL ? 1 : 0) + (opts.place & ENV_FUNCTION ? 1 : 0) + (opts.place & ENV_GLOBAL ? 1 : 0) + (opts.place & ENV_UNIVERSAL ? 1 : 0) > 1) { - streams.err.append_format(BUILTIN_ERR_GLOCAL, cmd); - builtin_print_error_trailer(parser, streams.err, cmd); + streams.err()->append(format_string(BUILTIN_ERR_GLOCAL, cmd)); + builtin_print_error_trailer(parser, *streams.err(), cmd); return STATUS_INVALID_ARGS; } if (!opts.array && argc < 1 && !opts.to_stdout) { - streams.err.append_format(BUILTIN_ERR_MIN_ARG_COUNT1, cmd, 1, argc); + streams.err()->append(format_string(BUILTIN_ERR_MIN_ARG_COUNT1, cmd, 1, argc)); return STATUS_INVALID_ARGS; } if (opts.array && argc != 1) { - streams.err.append_format(BUILTIN_ERR_ARG_COUNT1, cmd, 1, argc); + streams.err()->append(format_string(BUILTIN_ERR_ARG_COUNT1, cmd, 1, argc)); return STATUS_INVALID_ARGS; } if (opts.to_stdout && argc > 0) { - streams.err.append_format(BUILTIN_ERR_MAX_ARG_COUNT1, cmd, 0, argc); + streams.err()->append(format_string(BUILTIN_ERR_MAX_ARG_COUNT1, cmd, 0, argc)); return STATUS_INVALID_ARGS; } if (opts.tokenize && opts.have_delimiter) { - streams.err.append_format(BUILTIN_ERR_COMBO2_EXCLUSIVE, cmd, L"--delimiter", L"--tokenize"); + streams.err()->append( + format_string(BUILTIN_ERR_COMBO2_EXCLUSIVE, cmd, L"--delimiter", L"--tokenize")); return STATUS_INVALID_ARGS; } if (opts.tokenize && opts.one_line) { - streams.err.append_format(BUILTIN_ERR_COMBO2_EXCLUSIVE, cmd, L"--line", L"--tokenize"); + streams.err()->append( + format_string(BUILTIN_ERR_COMBO2_EXCLUSIVE, cmd, L"--line", L"--tokenize")); return STATUS_INVALID_ARGS; } // Verify all variable names. for (int i = 0; i < argc; i++) { if (!valid_var_name(argv[i])) { - streams.err.append_format(BUILTIN_ERR_VARNAME, cmd, argv[i]); - builtin_print_error_trailer(parser, streams.err, cmd); + streams.err()->append(format_string(BUILTIN_ERR_VARNAME, cmd, argv[i])); + builtin_print_error_trailer(parser, *streams.err(), cmd); return STATUS_INVALID_ARGS; } - if (env_var_t::flags_for(argv[i]) & env_var_t::flag_read_only) { - streams.err.append_format(_(L"%ls: %ls: cannot overwrite read-only variable"), cmd, - argv[i]); - builtin_print_error_trailer(parser, streams.err, cmd); + if (env_flags_for(argv[i]) & env_var_flag_read_only) { + streams.err()->append( + format_string(_(L"%ls: %ls: cannot overwrite read-only variable"), cmd, argv[i])); + builtin_print_error_trailer(parser, *streams.err(), cmd); return STATUS_INVALID_ARGS; } } @@ -436,9 +443,12 @@ static int validate_read_args(const wchar_t *cmd, read_cmd_opts_t &opts, int arg } /// The read builtin. Reads from stdin and stores the values in environment variables. -maybe_t builtin_read(parser_t &parser, io_streams_t &streams, const wchar_t **argv) { - const wchar_t *cmd = argv[0]; +int builtin_read(const void *_parser, void *_streams, void *_argv) { + const auto &parser = *static_cast(_parser); + auto &streams = *static_cast(_streams); + auto argv = static_cast(_argv); int argc = builtin_count_args(argv); + const wchar_t *cmd = argv[0]; wcstring buff; int exit_res = STATUS_CMD_OK; read_cmd_opts_t opts; @@ -464,8 +474,8 @@ maybe_t builtin_read(parser_t &parser, io_streams_t &streams, const wchar_t if (retval != STATUS_CMD_OK) return retval; // stdin may have been explicitly closed - if (streams.stdin_fd < 0) { - streams.err.append_format(_(L"%ls: stdin is closed\n"), cmd); + if (streams.stdin_fd() < 0) { + streams.err()->append(format_string(_(L"%ls: stdin is closed\n"), cmd)); return STATUS_CMD_ERROR; } @@ -481,7 +491,7 @@ maybe_t builtin_read(parser_t &parser, io_streams_t &streams, const wchar_t auto vars_left = [&]() { return argv + argc - var_ptr; }; auto clear_remaining_vars = [&]() { while (vars_left()) { - parser.vars().set_empty(*var_ptr, opts.place); + parser.vars().set(*var_ptr, opts.place, std::vector{}); ++var_ptr; } }; @@ -492,19 +502,19 @@ maybe_t builtin_read(parser_t &parser, io_streams_t &streams, const wchar_t do { buff.clear(); - int stream_stdin_is_a_tty = isatty(streams.stdin_fd); + int stream_stdin_is_a_tty = isatty(streams.stdin_fd()); if (stream_stdin_is_a_tty && !opts.split_null) { // Read interactively using reader_readline(). This does not support splitting on null. exit_res = read_interactive(parser, buff, opts.nchars, opts.shell, opts.silent, opts.prompt, - opts.right_prompt, opts.commandline, streams.stdin_fd); + opts.right_prompt, opts.commandline, streams.stdin_fd()); } else if (!opts.nchars && !stream_stdin_is_a_tty && // "one_line" is implemented as reading n-times to a new line, // if we're chunking we could get multiple lines so we would have to advance // more than 1 per run through the loop. Let's skip that for now. !opts.one_line && - (streams.stdin_is_directly_redirected || - lseek(streams.stdin_fd, 0, SEEK_CUR) != -1)) { + (streams.stdin_is_directly_redirected() || + lseek(streams.stdin_fd(), 0, SEEK_CUR) != -1)) { // We read in chunks when we either can seek (so we put the bytes back), // or we have the bytes to ourselves (because it's directly redirected). // @@ -512,11 +522,11 @@ maybe_t builtin_read(parser_t &parser, io_streams_t &streams, const wchar_t // under the assumption that the stream will be closed soon anyway. // You don't rewind VHS tapes before throwing them in the trash. // TODO: Do this when nchars is set by seeking back. - exit_res = read_in_chunks(streams.stdin_fd, buff, opts.split_null, - !streams.stdin_is_directly_redirected); + exit_res = read_in_chunks(streams.stdin_fd(), buff, opts.split_null, + !streams.stdin_is_directly_redirected()); } else { exit_res = - read_one_char_at_a_time(streams.stdin_fd, buff, opts.nchars, opts.split_null); + read_one_char_at_a_time(streams.stdin_fd(), buff, opts.nchars, opts.split_null); } if (exit_res != STATUS_CMD_OK) { @@ -525,7 +535,7 @@ maybe_t builtin_read(parser_t &parser, io_streams_t &streams, const wchar_t } if (opts.to_stdout) { - streams.out.append(buff); + streams.out()->append(buff); return exit_res; } @@ -536,7 +546,8 @@ maybe_t builtin_read(parser_t &parser, io_streams_t &streams, const wchar_t std::vector tokens; while (auto t = tok->next()) { auto text = *tok->text_of(*t); - if (auto out = unescape_string(text, UNESCAPE_DEFAULT)) { + if (auto out = unescape_string(text.c_str(), text.length(), UNESCAPE_DEFAULT, + STRING_STYLE_SCRIPT)) { tokens.push_back(*out); } else { tokens.push_back(text); @@ -548,17 +559,21 @@ maybe_t builtin_read(parser_t &parser, io_streams_t &streams, const wchar_t std::unique_ptr t; while ((vars_left() - 1 > 0) && (t = tok->next())) { auto text = *tok->text_of(*t); - if (auto out = unescape_string(text, UNESCAPE_DEFAULT)) { - parser.set_var_and_fire(*var_ptr++, opts.place, *out); + if (auto out = unescape_string(text.c_str(), text.length(), UNESCAPE_DEFAULT, + STRING_STYLE_SCRIPT)) { + parser.set_var_and_fire(*var_ptr++, opts.place, + std::vector{*out}); } else { - parser.set_var_and_fire(*var_ptr++, opts.place, text); + parser.set_var_and_fire(*var_ptr++, opts.place, + std::vector{text}); } } // If we still have tokens, set the last variable to them. if ((t = tok->next())) { wcstring rest = wcstring(buff, t->offset); - parser.set_var_and_fire(*var_ptr++, opts.place, std::move(rest)); + parser.set_var_and_fire(*var_ptr++, opts.place, + std::vector{std::move(rest)}); } } // The rest of the loop is other split-modes, we don't care about those. @@ -567,7 +582,7 @@ maybe_t builtin_read(parser_t &parser, io_streams_t &streams, const wchar_t if (!opts.have_delimiter) { auto ifs = parser.vars().get_unless_empty(L"IFS"); - if (ifs) opts.delimiter = ifs->as_string(); + if (ifs) opts.delimiter = *ifs->as_string(); } if (opts.delimiter.empty()) { @@ -596,7 +611,7 @@ maybe_t builtin_read(parser_t &parser, io_streams_t &streams, const wchar_t // Not array mode: assign each char to a separate var with the remainder being // assigned to the last var. for (const auto &c : chars) { - parser.set_var_and_fire(*var_ptr++, opts.place, c); + parser.set_var_and_fire(*var_ptr++, opts.place, std::vector{c}); } } } else if (opts.array) { @@ -630,7 +645,8 @@ maybe_t builtin_read(parser_t &parser, io_streams_t &streams, const wchar_t if (val_idx < var_vals.size()) { val = std::move(var_vals.at(val_idx++)); } - parser.set_var_and_fire(*var_ptr++, opts.place, std::move(val)); + parser.set_var_and_fire(*var_ptr++, opts.place, + std::vector{std::move(val)}); } } else { // We're using a delimiter provided by the user so use the `string split` behavior. @@ -641,7 +657,7 @@ maybe_t builtin_read(parser_t &parser, io_streams_t &streams, const wchar_t &splits, argc - 1); assert(splits.size() <= static_cast(vars_left())); for (const auto &split : splits) { - parser.set_var_and_fire(*var_ptr++, opts.place, split); + parser.set_var_and_fire(*var_ptr++, opts.place, std::vector{split}); } } } diff --git a/src/builtins/read.h b/src/builtins/read.h index 1f5492e85..2e2bc6262 100644 --- a/src/builtins/read.h +++ b/src/builtins/read.h @@ -4,8 +4,10 @@ #include "../maybe.h" -class Parser; using parser_t = Parser; -class IoStreams; using io_streams_t = IoStreams; +struct Parser; +struct IoStreams; +using parser_t = Parser; +using io_streams_t = IoStreams; -maybe_t builtin_read(parser_t &parser, io_streams_t &streams, const wchar_t **argv); +int builtin_read(const void *parser, void *streams, void *argv); #endif diff --git a/src/builtins/set.cpp b/src/builtins/set.cpp deleted file mode 100644 index 4c806d4e0..000000000 --- a/src/builtins/set.cpp +++ /dev/null @@ -1,850 +0,0 @@ -// Functions used for implementing the set builtin. -#include "config.h" // IWYU pragma: keep - -#include "set.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../builtin.h" -#include "../common.h" -#include "../env.h" -#include "../event.h" -#include "../expand.h" -#include "../fallback.h" // IWYU pragma: keep -#include "../history.h" -#include "../io.h" -#include "../maybe.h" -#include "../parser.h" -#include "../wgetopt.h" -#include "../wutil.h" // IWYU pragma: keep - -struct set_cmd_opts_t { - bool print_help = false; - bool show = false; - bool local = false; - bool function = false; - bool global = false; - bool exportv = false; - bool erase = false; - bool list = false; - bool unexport = false; - bool pathvar = false; - bool unpathvar = false; - bool universal = false; - bool query = false; - bool shorten_ok = true; - bool append = false; - bool prepend = false; - bool preserve_failure_exit_status = true; -}; - -/// Values used for long-only options. -enum { - opt_path = 1, - opt_unpath = 2, -}; - -// Variables used for parsing the argument list. This command is atypical in using the "+" -// (REQUIRE_ORDER) option for flag parsing. This is not typical of most fish commands. It means -// we stop scanning for flags when the first non-flag argument is seen. -static const wchar_t *const short_options = L"+:LSUaefghlnpqux"; -static const struct woption long_options[] = {{L"export", no_argument, 'x'}, - {L"global", no_argument, 'g'}, - {L"function", no_argument, 'f'}, - {L"local", no_argument, 'l'}, - {L"erase", no_argument, 'e'}, - {L"names", no_argument, 'n'}, - {L"unexport", no_argument, 'u'}, - {L"universal", no_argument, 'U'}, - {L"long", no_argument, 'L'}, - {L"query", no_argument, 'q'}, - {L"show", no_argument, 'S'}, - {L"append", no_argument, 'a'}, - {L"prepend", no_argument, 'p'}, - {L"path", no_argument, opt_path}, - {L"unpath", no_argument, opt_unpath}, - {L"help", no_argument, 'h'}, - {}}; - -// Hint for invalid path operation with a colon. -#define BUILTIN_SET_MISMATCHED_ARGS _(L"%ls: given %d indexes but %d values\n") -#define BUILTIN_SET_ARRAY_BOUNDS_ERR _(L"%ls: array index out of bounds\n") -#define BUILTIN_SET_UVAR_ERR \ - _(L"%ls: successfully set universal '%ls'; but a global by that name shadows it\n") - -static int parse_cmd_opts(set_cmd_opts_t &opts, int *optind, //!OCLINT(high ncss method) - int argc, const wchar_t **argv, parser_t &parser, io_streams_t &streams) { - const wchar_t *cmd = argv[0]; - - int opt; - wgetopter_t w; - while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, nullptr)) != -1) { - switch (opt) { - case 'a': { - opts.append = true; - break; - } - case 'e': { - opts.erase = true; - opts.preserve_failure_exit_status = false; - break; - } - case 'f': { - opts.function = true; - break; - } - case 'g': { - opts.global = true; - break; - } - case 'h': { - opts.print_help = true; - break; - } - case 'l': { - opts.local = true; - break; - } - case 'n': { - opts.list = true; - opts.preserve_failure_exit_status = false; - break; - } - case 'p': { - opts.prepend = true; - break; - } - case 'q': { - opts.query = true; - opts.preserve_failure_exit_status = false; - break; - } - case 'x': { - opts.exportv = true; - break; - } - case 'u': { - opts.unexport = true; - break; - } - case opt_path: { - opts.pathvar = true; - break; - } - case opt_unpath: { - opts.unpathvar = true; - break; - } - case 'U': { - opts.universal = true; - break; - } - case 'L': { - opts.shorten_ok = false; - break; - } - case 'S': { - opts.show = true; - opts.preserve_failure_exit_status = false; - break; - } - case ':': { - builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]); - return STATUS_INVALID_ARGS; - } - case '?': { - // Specifically detect `set -o` because people might be bringing over bashisms. - if (wcsncmp(argv[w.woptind - 1], L"-o", 2) == 0) { - streams.err.append( - L"Fish does not have shell options. See `help fish-for-bash-users`.\n"); - if (w.woptind < argc) { - if (wcscmp(argv[w.woptind], L"vi") == 0) { - // Tell the vi users how to get what they need. - streams.err.append(L"To enable vi-mode, run `fish_vi_key_bindings`.\n"); - } else if (wcscmp(argv[w.woptind], L"ed") == 0) { - // This should be enough for make ed users feel at home - streams.err.append(L"?\n?\n?\n"); - } - } - } - builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]); - return STATUS_INVALID_ARGS; - } - default: { - DIE("unexpected retval from wgetopt_long"); - } - } - } - - *optind = w.woptind; - return STATUS_CMD_OK; -} - -static int validate_cmd_opts(const wchar_t *cmd, const set_cmd_opts_t &opts, int argc, - const wchar_t *argv[], parser_t &parser, io_streams_t &streams) { - // Can't query and erase or list. - if (opts.query && (opts.erase || opts.list)) { - streams.err.append_format(BUILTIN_ERR_COMBO, cmd); - builtin_print_error_trailer(parser, streams.err, cmd); - return STATUS_INVALID_ARGS; - } - - // We can't both list and erase variables. - if (opts.erase && opts.list) { - streams.err.append_format(BUILTIN_ERR_COMBO, cmd); - builtin_print_error_trailer(parser, streams.err, cmd); - return STATUS_INVALID_ARGS; - } - - // Variables can only have one scope... - if (opts.local + opts.function + opts.global + opts.universal > 1) { - // ..unless we are erasing a variable, in which case we can erase from several in one go. - if (!opts.erase) { - streams.err.append_format(BUILTIN_ERR_GLOCAL, cmd); - builtin_print_error_trailer(parser, streams.err, cmd); - return STATUS_INVALID_ARGS; - } - } - - // Variables can only have one export status. - if (opts.exportv && opts.unexport) { - streams.err.append_format(BUILTIN_ERR_EXPUNEXP, cmd); - builtin_print_error_trailer(parser, streams.err, cmd); - return STATUS_INVALID_ARGS; - } - - // Variables can only have one path status. - if (opts.pathvar && opts.unpathvar) { - streams.err.append_format(BUILTIN_ERR_EXPUNEXP, cmd); - builtin_print_error_trailer(parser, streams.err, cmd); - return STATUS_INVALID_ARGS; - } - - // Trying to erase and (un)export at the same time doesn't make sense. - if (opts.erase && (opts.exportv || opts.unexport)) { - streams.err.append_format(BUILTIN_ERR_COMBO, cmd); - builtin_print_error_trailer(parser, streams.err, cmd); - return STATUS_INVALID_ARGS; - } - - // The --show flag cannot be combined with any other flag. - if (opts.show && (opts.local || opts.function || opts.global || opts.erase || opts.list || - opts.exportv || opts.universal)) { - streams.err.append_format(BUILTIN_ERR_COMBO, cmd); - builtin_print_error_trailer(parser, streams.err, cmd); - return STATUS_INVALID_ARGS; - } - - if (argc == 0 && opts.erase) { - streams.err.append_format(BUILTIN_ERR_MISSING, cmd, argv[-1]); - builtin_print_error_trailer(parser, streams.err, cmd); - return STATUS_INVALID_ARGS; - } - - return STATUS_CMD_OK; -} - -// Check if we are setting a uvar and a global of the same name exists. See -// https://github.com/fish-shell/fish-shell/issues/806 -static void warn_if_uvar_shadows_global(const wchar_t *cmd, const set_cmd_opts_t &opts, - const wcstring &dest, io_streams_t &streams, - const parser_t &parser) { - if (opts.universal && parser.is_interactive()) { - if (parser.vars().get(dest, ENV_GLOBAL).has_value()) { - streams.err.append_format(BUILTIN_SET_UVAR_ERR, cmd, dest.c_str()); - } - } -} - -static void handle_env_return(int retval, const wchar_t *cmd, const wcstring &key, - io_streams_t &streams) { - switch (retval) { - case ENV_OK: { - break; - } - case ENV_PERM: { - streams.err.append_format(_(L"%ls: Tried to change the read-only variable '%ls'\n"), - cmd, key.c_str()); - break; - } - case ENV_SCOPE: { - streams.err.append_format( - _(L"%ls: Tried to modify the special variable '%ls' with the wrong scope\n"), cmd, - key.c_str()); - break; - } - case ENV_INVALID: { - streams.err.append_format( - _(L"%ls: Tried to modify the special variable '%ls' to an invalid value\n"), cmd, - key.c_str()); - break; - } - case ENV_NOT_FOUND: { - streams.err.append_format(_(L"%ls: The variable '%ls' does not exist\n"), cmd, - key.c_str()); - break; - } - default: { - DIE("unexpected vars.set() ret val"); - } - } -} - -/// Call vars.set. If this is a path variable, e.g. PATH, validate the elements. On error, print a -/// description of the problem to stderr. -static int env_set_reporting_errors(const wchar_t *cmd, const wcstring &key, int scope, - std::vector list, io_streams_t &streams, - parser_t &parser) { - int retval = parser.set_var_and_fire(key, scope | ENV_USER, std::move(list)); - // If this returned OK, the parser already fired the event. - handle_env_return(retval, cmd, key, streams); - - return retval; -} - -namespace { -/// A helper type returned by split_var_and_indexes. -struct split_var_t { - wcstring varname; // name of the variable - maybe_t var{}; // value of the variable, or none if missing - std::vector indexes{}; // list of requested indexes - - /// \return the number of elements in our variable, or 0 if missing. - long varsize() const { return var ? static_cast(var->as_list().size()) : 0L; } -}; -} // namespace - -/// Extract indexes from an argument of the form `var_name[index1 index2...]`. -/// The argument \p arg is split into a variable name and list of indexes, which is returned by -/// reference. Indexes are "expanded" in the sense that range expressions .. and negative values are -/// handled. -/// -/// Returns: -/// a split var on success, none() on error, in which case an error will have been printed. -/// If no index is found, this leaves indexes empty. -static maybe_t split_var_and_indexes(const wchar_t *arg, env_mode_flags_t mode, - const environment_t &vars, - io_streams_t &streams) { - split_var_t res{}; - wcstring argstr{arg}; - auto open_bracket = argstr.find(L'['); - res.varname.assign(arg, open_bracket == wcstring::npos ? argstr.size() : open_bracket); - res.var = vars.get(res.varname, mode); - if (open_bracket == wcstring::npos) { - // Common case of no bracket. - return res; - } - - long varsize = res.varsize(); - const wchar_t *p = arg + open_bracket + 1; - while (*p != L']') { - const wchar_t *end; - long l_ind; - if (res.indexes.empty() && p[0] == L'.' && p[1] == L'.') { - // If we are at the first index expression, a missing start-index means the range starts - // at the first item. - l_ind = 1; // first index - } else { - const wchar_t *end = nullptr; - l_ind = fish_wcstol(p, &end); - if (errno > 0) { // ignore errno == -1 meaning the int did not end with a '\0' - streams.err.append_format(_(L"%ls: Invalid index starting at '%ls'\n"), L"set", - res.varname.c_str()); - return none(); - } - p = end; - } - - // Convert negative index to a positive index. - if (l_ind < 0) l_ind = varsize + l_ind + 1; - - if (p[0] == L'.' && p[1] == L'.') { - p += 2; - long l_ind2; - // If we are at the last index range expression, a missing end-index means the range - // spans until the last item. - if (res.indexes.empty() && *p == L']') { - l_ind2 = -1; - } else { - l_ind2 = fish_wcstol(p, &end); - if (errno > 0) { // ignore errno == -1 meaning there was text after the int - return none(); - } - p = end; - } - - // Convert negative index to a positive index. - if (l_ind2 < 0) l_ind2 = varsize + l_ind2 + 1; - - int direction = l_ind2 < l_ind ? -1 : 1; - for (long jjj = l_ind; jjj * direction <= l_ind2 * direction; jjj += direction) { - res.indexes.push_back(jjj); - } - } else { - res.indexes.push_back(l_ind); - } - } - return res; -} - -/// Given a list of values and 1-based indexes, return a new list with those elements removed. -/// Note this deliberately accepts both args by value, as it modifies them both. -static std::vector erased_at_indexes(std::vector input, - std::vector indexes) { - // Sort our indexes into *descending* order. - std::sort(indexes.begin(), indexes.end(), std::greater()); - - // Remove duplicates. - indexes.erase(std::unique(indexes.begin(), indexes.end()), indexes.end()); - - // Now when we walk indexes, we encounter larger indexes first. - for (long idx : indexes) { - if (idx > 0 && static_cast(idx) <= input.size()) { - // One-based indexing! - input.erase(input.begin() + idx - 1); - } - } - return input; -} - -static env_mode_flags_t compute_scope(const set_cmd_opts_t &opts) { - int scope = ENV_USER; - if (opts.local) scope |= ENV_LOCAL; - if (opts.function) scope |= ENV_FUNCTION; - if (opts.global) scope |= ENV_GLOBAL; - if (opts.exportv) scope |= ENV_EXPORT; - if (opts.unexport) scope |= ENV_UNEXPORT; - if (opts.universal) scope |= ENV_UNIVERSAL; - if (opts.pathvar) scope |= ENV_PATHVAR; - if (opts.unpathvar) scope |= ENV_UNPATHVAR; - return scope; -} - -/// Print the names of all environment variables in the scope. It will include the values unless the -/// `set --names` flag was used. -static int builtin_set_list(const wchar_t *cmd, set_cmd_opts_t &opts, int argc, - const wchar_t **argv, parser_t &parser, io_streams_t &streams) { - UNUSED(cmd); - UNUSED(argc); - UNUSED(argv); - UNUSED(parser); - - bool names_only = opts.list; - std::vector names = parser.vars().get_names(compute_scope(opts)); - sort(names.begin(), names.end()); - - for (const auto &key : names) { - wcstring out; - out.append(key); - - if (!names_only) { - wcstring val; - if (opts.shorten_ok && key == L"history") { - std::shared_ptr history = - history_t::with_name(history_session_id(parser.vars())); - for (size_t i = 1; i < history->size() && val.size() < 64; i++) { - if (i > 1) val += L' '; - val += expand_escape_string(history->item_at_index(i).str()); - } - } else { - auto var = parser.vars().get_unless_empty(key, compute_scope(opts)); - if (var) { - val = expand_escape_variable(*var); - } - } - if (!val.empty()) { - bool shorten = false; - if (opts.shorten_ok && val.length() > 64) { - shorten = true; - val.resize(60); - } - out.push_back(L' '); - out.append(val); - - if (shorten) out.push_back(get_ellipsis_char()); - } - } - - out.push_back(L'\n'); - streams.out.append(out); - } - - return STATUS_CMD_OK; -} - -// Query mode. Return the number of variables that do NOT exist out of the specified variables. -static int builtin_set_query(const wchar_t *cmd, set_cmd_opts_t &opts, int argc, - const wchar_t **argv, parser_t &parser, io_streams_t &streams) { - int retval = 0; - env_mode_flags_t scope = compute_scope(opts); - - // No variables given, this is an error. - // 255 is the maximum return code we allow. - if (argc == 0) return 255; - - for (int i = 0; i < argc; i++) { - auto split = split_var_and_indexes(argv[i], scope, parser.vars(), streams); - if (!split) { - builtin_print_error_trailer(parser, streams.err, cmd); - return STATUS_CMD_ERROR; - } - - if (split->indexes.empty()) { - // No indexes, just increment if our variable is missing. - if (!split->var) retval++; - } else { - // Increment for every index out of range. - long varsize = split->varsize(); - for (long idx : split->indexes) { - if (idx < 1 || idx > varsize) retval++; - } - } - } - - return retval; -} - -static void show_scope(const wchar_t *var_name, int scope, io_streams_t &streams, - const environment_t &vars) { - const wchar_t *scope_name; - switch (scope) { - case ENV_LOCAL: { - scope_name = L"local"; - break; - } - case ENV_GLOBAL: { - scope_name = L"global"; - break; - } - case ENV_UNIVERSAL: { - scope_name = L"universal"; - break; - } - default: { - DIE("invalid scope"); - } - } - - const auto var = vars.get(var_name, scope); - if (!var) { - return; - } - - const wchar_t *exportv = var->exports() ? _(L"exported") : _(L"unexported"); - const wchar_t *pathvarv = var->is_pathvar() ? _(L" a path variable") : L""; - std::vector vals = var->as_list(); - streams.out.append_format(_(L"$%ls: set in %ls scope, %ls,%ls with %d elements"), var_name, - scope_name, exportv, pathvarv, vals.size()); - // HACK: PWD can be set, depending on how you ask. - // For our purposes it's read-only. - if (env_var_t::flags_for(var_name) & env_var_t::flag_read_only) { - streams.out.append(_(L" (read-only)\n")); - } else - streams.out.push(L'\n'); - - for (size_t i = 0; i < vals.size(); i++) { - if (vals.size() > 100) { - if (i == 50) { - // try to print a mid-line ellipsis because we are eliding lines not words - streams.out.append(get_ellipsis_char() > 256 ? L"\u22EF" : get_ellipsis_str()); - streams.out.push(L'\n'); - } - if (i >= 50 && i < vals.size() - 50) continue; - } - const wcstring value = vals[i]; - const wcstring escaped_val = escape_string(value, ESCAPE_NO_PRINTABLES | ESCAPE_NO_QUOTED); - streams.out.append_format(_(L"$%ls[%d]: |%ls|\n"), var_name, i + 1, escaped_val.c_str()); - } -} - -/// Show mode. Show information about the named variable(s). -static int builtin_set_show(const wchar_t *cmd, const set_cmd_opts_t &opts, int argc, - const wchar_t **argv, parser_t &parser, io_streams_t &streams) { - UNUSED(opts); - const auto &vars = parser.vars(); - auto inheriteds = env_get_inherited(); - if (argc == 0) { // show all vars - std::vector names = vars.get_names(ENV_USER); - sort(names.begin(), names.end()); - for (const auto &name : names) { - if (name == L"history") continue; - show_scope(name.c_str(), ENV_LOCAL, streams, vars); - show_scope(name.c_str(), ENV_GLOBAL, streams, vars); - show_scope(name.c_str(), ENV_UNIVERSAL, streams, vars); - - // Show the originally imported value as a debugging aid. - auto inherited = inheriteds.find(name); - if (inherited != inheriteds.end()) { - const wcstring escaped_val = - escape_string(inherited->second, ESCAPE_NO_PRINTABLES | ESCAPE_NO_QUOTED); - streams.out.append_format(_(L"$%ls: originally inherited as |%ls|\n"), name.c_str(), - escaped_val.c_str()); - } - } - } else { - for (int i = 0; i < argc; i++) { - const wchar_t *arg = argv[i]; - - if (!valid_var_name(arg)) { - streams.err.append_format(BUILTIN_ERR_VARNAME, cmd, arg); - builtin_print_error_trailer(parser, streams.err, cmd); - return STATUS_INVALID_ARGS; - } - - if (std::wcschr(arg, L'[')) { - streams.err.append_format( - _(L"%ls: `set --show` does not allow slices with the var names\n"), cmd); - builtin_print_error_trailer(parser, streams.err, cmd); - return STATUS_CMD_ERROR; - } - - show_scope(arg, ENV_LOCAL, streams, vars); - show_scope(arg, ENV_GLOBAL, streams, vars); - show_scope(arg, ENV_UNIVERSAL, streams, vars); - auto inherited = inheriteds.find(arg); - if (inherited != inheriteds.end()) { - const wcstring escaped_val = - escape_string(inherited->second, ESCAPE_NO_PRINTABLES | ESCAPE_NO_QUOTED); - streams.out.append_format(_(L"$%ls: originally inherited as |%ls|\n"), arg, - escaped_val.c_str()); - } - } - } - - return STATUS_CMD_OK; -} - -/// Erase a variable. -static int builtin_set_erase(const wchar_t *cmd, set_cmd_opts_t &opts, int argc, - const wchar_t **argv, parser_t &parser, io_streams_t &streams) { - int ret = STATUS_CMD_OK; - env_mode_flags_t scopes = compute_scope(opts); - // `set -e` is allowed to be called with multiple scopes. - for (int bit = 0; 1 << bit <= ENV_USER; ++bit) { - int scope = scopes & 1 << bit; - if (scope == 0 || (scope == ENV_USER && scopes != ENV_USER)) continue; - for (int i = 0; i < argc; i++) { - auto split = split_var_and_indexes(argv[i], scope, parser.vars(), streams); - if (!split) { - builtin_print_error_trailer(parser, streams.err, cmd); - return STATUS_CMD_ERROR; - } - - if (!valid_var_name(split->varname)) { - streams.err.append_format(BUILTIN_ERR_VARNAME, cmd, split->varname.c_str()); - builtin_print_error_trailer(parser, streams.err, cmd); - return STATUS_INVALID_ARGS; - } - - int retval = STATUS_CMD_OK; - if (split->indexes.empty()) { // unset the var - retval = parser.vars().remove(split->varname, scope); - // When a non-existent-variable is unset, return ENV_NOT_FOUND as $status - // but do not emit any errors at the console as a compromise between user - // friendliness and correctness. - if (retval != ENV_NOT_FOUND) { - handle_env_return(retval, cmd, split->varname, streams); - } - if (retval == ENV_OK) { - event_fire(parser, *new_event_variable_erase(split->varname)); - } - } else { // remove just the specified indexes of the var - if (!split->var) return STATUS_CMD_ERROR; - std::vector result = - erased_at_indexes(split->var->as_list(), split->indexes); - retval = env_set_reporting_errors(cmd, split->varname, scope, std::move(result), - streams, parser); - } - - // Set $status to the last error value. - // This is cheesy, but I don't expect this to be checked often. - if (retval != STATUS_CMD_OK) { - ret = retval; - } - } - } - return ret; -} - -/// Return a list of new values for the variable \p varname, respecting the \p opts. -/// The arguments are given as the argc, argv pair. -/// This handles the simple case where there are no indexes. -static std::vector new_var_values(const wcstring &varname, const set_cmd_opts_t &opts, - int argc, const wchar_t *const *argv, - const environment_t &vars) { - std::vector result; - if (!opts.prepend && !opts.append) { - // Not prepending or appending. - result.assign(argv, argv + argc); - } else { - // Note: when prepending or appending, we always use default scoping when fetching existing - // values. For example: - // set --global var 1 2 - // set --local --append var 3 4 - // This starts with the existing global variable, appends to it, and sets it locally. - // So do not use the given variable: we must re-fetch it. - // TODO: this races under concurrent execution. - if (auto existing = vars.get(varname, ENV_DEFAULT)) { - result = existing->as_list(); - } - - if (opts.prepend) { - result.insert(result.begin(), argv, argv + argc); - } - - if (opts.append) { - result.insert(result.end(), argv, argv + argc); - } - } - return result; -} - -/// This handles the more difficult case of setting individual slices of a var. -static std::vector new_var_values_by_index(const split_var_t &split, int argc, - const wchar_t *const *argv) { - assert(static_cast(argc) == split.indexes.size() && - "Must have the same number of indexes as arguments"); - - // Inherit any existing values. - // Note unlike the append/prepend case, we start with a variable in the same scope as we are - // setting. - std::vector result; - if (split.var) result = split.var->as_list(); - - // For each (index, argument) pair, set the element in our \p result to the replacement string. - // Extend the list with empty strings as needed. The indexes are 1-based. - for (int i = 0; i < argc; i++) { - long lidx = split.indexes.at(i); - assert(lidx >= 1 && "idx should have been verified in range already"); - // Convert from 1-based to 0-based. - auto idx = static_cast(lidx - 1); - // Extend as needed with empty strings. - if (idx >= result.size()) result.resize(idx + 1); - result.at(idx) = argv[i]; - } - return result; -} - -/// Set a variable. -static int builtin_set_set(const wchar_t *cmd, set_cmd_opts_t &opts, int argc, const wchar_t **argv, - parser_t &parser, io_streams_t &streams) { - if (argc == 0) { - streams.err.append_format(BUILTIN_ERR_MIN_ARG_COUNT1, cmd, 1); - builtin_print_error_trailer(parser, streams.err, cmd); - return STATUS_INVALID_ARGS; - } - - env_mode_flags_t scope = compute_scope(opts); - const wchar_t *var_expr = argv[0]; - argv++; - argc--; - - auto split = split_var_and_indexes(var_expr, scope, parser.vars(), streams); - if (!split) { - builtin_print_error_trailer(parser, streams.err, cmd); - return STATUS_INVALID_ARGS; - } - - // Is the variable valid? - if (!valid_var_name(split->varname)) { - streams.err.append_format(BUILTIN_ERR_VARNAME, cmd, split->varname.c_str()); - auto pos = split->varname.find(L'='); - if (pos != wcstring::npos) { - streams.err.append_format(L"%ls: Did you mean `set %ls %ls`?", cmd, - escape_string(split->varname.substr(0, pos)).c_str(), - escape_string(split->varname.substr(pos + 1)).c_str()); - } - builtin_print_error_trailer(parser, streams.err, cmd); - return STATUS_INVALID_ARGS; - } - - // Setting with explicit indexes like `set foo[3] ...` has additional error handling. - if (!split->indexes.empty()) { - // Indexes must be > 0. (Note split_var_and_indexes negates negative values). - for (long v : split->indexes) { - if (v <= 0) { - streams.err.append_format(BUILTIN_SET_ARRAY_BOUNDS_ERR, cmd); - return STATUS_INVALID_ARGS; - } - } - - // Append and prepend are disallowed. - if (opts.append || opts.prepend) { - streams.err.append_format( - L"%ls: Cannot use --append or --prepend when assigning to a slice", cmd); - builtin_print_error_trailer(parser, streams.err, cmd); - return STATUS_INVALID_ARGS; - } - - // Argument count and index count must agree. - if (split->indexes.size() != static_cast(argc)) { - streams.err.append_format(BUILTIN_SET_MISMATCHED_ARGS, cmd, split->indexes.size(), - argc); - return STATUS_INVALID_ARGS; - } - } - - std::vector new_values; - if (split->indexes.empty()) { - // Handle the simple, common, case. Set the var to the specified values. - new_values = new_var_values(split->varname, opts, argc, argv, parser.vars()); - } else { - // Handle the uncommon case of setting specific slices of a var. - new_values = new_var_values_by_index(*split, argc, argv); - } - - // Set the value back in the variable stack and fire any events. - int retval = env_set_reporting_errors(cmd, split->varname, scope, std::move(new_values), - streams, parser); - - if (retval == ENV_OK) { - warn_if_uvar_shadows_global(cmd, opts, split->varname, streams, parser); - } - return retval; -} - -/// The set builtin creates, updates, and erases (removes, deletes) variables. -maybe_t builtin_set(parser_t &parser, io_streams_t &streams, const wchar_t **argv) { - const wchar_t *cmd = argv[0]; - int argc = builtin_count_args(argv); - set_cmd_opts_t opts; - - int optind; - int retval = parse_cmd_opts(opts, &optind, argc, argv, parser, streams); - if (retval != STATUS_CMD_OK) return retval; - argv += optind; - argc -= optind; - - if (opts.print_help) { - builtin_print_help(parser, streams, cmd); - return STATUS_CMD_OK; - } - - retval = validate_cmd_opts(cmd, opts, argc, argv, parser, streams); - if (retval != STATUS_CMD_OK) return retval; - - if (opts.query) { - retval = builtin_set_query(cmd, opts, argc, argv, parser, streams); - } else if (opts.erase) { - retval = builtin_set_erase(cmd, opts, argc, argv, parser, streams); - } else if (opts.list) { // explicit list the vars we know about - retval = builtin_set_list(cmd, opts, argc, argv, parser, streams); - } else if (opts.show) { - retval = builtin_set_show(cmd, opts, argc, argv, parser, streams); - } else if (argc == 0) { // implicit list the vars we know about - retval = builtin_set_list(cmd, opts, argc, argv, parser, streams); - } else { - retval = builtin_set_set(cmd, opts, argc, argv, parser, streams); - } - - if (retval == STATUS_CMD_OK && opts.preserve_failure_exit_status) return none(); - return retval; -} diff --git a/src/builtins/set.h b/src/builtins/set.h deleted file mode 100644 index 3c80f2a39..000000000 --- a/src/builtins/set.h +++ /dev/null @@ -1,10 +0,0 @@ -// Prototypes for functions for executing builtin_set functions. -#ifndef FISH_BUILTIN_SET_H -#define FISH_BUILTIN_SET_H - -#include "../maybe.h" - -class Parser; using parser_t = Parser; -class IoStreams; using io_streams_t = IoStreams; -maybe_t builtin_set(parser_t &parser, io_streams_t &streams, const wchar_t **argv); -#endif diff --git a/src/builtins/source.cpp b/src/builtins/source.cpp deleted file mode 100644 index 789c1d835..000000000 --- a/src/builtins/source.cpp +++ /dev/null @@ -1,123 +0,0 @@ -// Implementation of the source builtin. -#include "config.h" // IWYU pragma: keep - -#include "source.h" - -#include -#include -#include - -#include -#include -#include -#include - -#include "../builtin.h" -#include "../common.h" -#include "../env.h" -#include "../fallback.h" // IWYU pragma: keep -#include "../fds.h" -#include "../io.h" -#include "../maybe.h" -#include "../null_terminated_array.h" -#include "../parser.h" -#include "../reader.h" -#include "../wutil.h" // IWYU pragma: keep - -/// The source builtin, sometimes called `.`. Evaluates the contents of a file in the current -/// context. -maybe_t builtin_source(parser_t &parser, io_streams_t &streams, const wchar_t **argv) { - const wchar_t *cmd = argv[0]; - int argc = builtin_count_args(argv); - help_only_cmd_opts_t opts; - - int optind; - int retval = parse_help_only_cmd_opts(opts, &optind, argc, argv, parser, streams); - if (retval != STATUS_CMD_OK) return retval; - - if (opts.print_help) { - builtin_print_help(parser, streams, cmd); - return STATUS_CMD_OK; - } - - // If we open a file, this ensures we close it. - autoclose_fd_t opened_fd; - - // The fd that we read from, either from opened_fd or stdin. - int fd = -1; - - struct stat buf; - filename_ref_t func_filename{}; - - if (argc == optind || std::wcscmp(argv[optind], L"-") == 0) { - if (streams.stdin_fd < 0) { - streams.err.append_format(_(L"%ls: stdin is closed\n"), cmd); - return STATUS_CMD_ERROR; - } - // Either a bare `source` which means to implicitly read from stdin or an explicit `-`. - if (argc == optind && isatty(streams.stdin_fd)) { - // Don't implicitly read from the terminal. - return STATUS_CMD_ERROR; - } - func_filename = std::make_shared(L"-"); - fd = streams.stdin_fd; - } else { - opened_fd = autoclose_fd_t(wopen_cloexec(argv[optind], O_RDONLY)); - if (!opened_fd.valid()) { - wcstring esc = escape_string(argv[optind]); - streams.err.append_format(_(L"%ls: Error encountered while sourcing file '%ls':\n"), - cmd, esc.c_str()); - builtin_wperror(cmd, streams); - return STATUS_CMD_ERROR; - } - - fd = opened_fd.fd(); - if (fstat(fd, &buf) == -1) { - wcstring esc = escape_string(argv[optind]); - streams.err.append_format(_(L"%ls: Error encountered while sourcing file '%ls':\n"), - cmd, esc.c_str()); - builtin_wperror(L"source", streams); - return STATUS_CMD_ERROR; - } - - if (!S_ISREG(buf.st_mode)) { - wcstring esc = escape_string(argv[optind]); - streams.err.append_format(_(L"%ls: '%ls' is not a file\n"), cmd, esc.c_str()); - return STATUS_CMD_ERROR; - } - - func_filename = std::make_shared(argv[optind]); - } - assert(fd >= 0 && "Should have a valid fd"); - assert(func_filename && "Should have valid function filename"); - - const block_t *sb = parser.push_block(block_t::source_block(func_filename)); - auto &ld = parser.libdata(); - scoped_push filename_push{&ld.current_filename, func_filename}; - - // Construct argv from our null-terminated list. - // This is slightly subtle. If this is a bare `source` with no args then `argv + optind` already - // points to the end of argv. Otherwise we want to skip the file name to get to the args if any. - std::vector argv_list; - const wchar_t *const *remaining_args = argv + optind + (argc == optind ? 0 : 1); - for (size_t i = 0, len = null_terminated_array_length(remaining_args); i < len; i++) { - argv_list.push_back(remaining_args[i]); - } - parser.vars().set_argv(std::move(argv_list)); - - retval = reader_read(parser, fd, streams.io_chain ? *streams.io_chain : io_chain_t()); - - parser.pop_block(sb); - - if (retval != STATUS_CMD_OK) { - wcstring esc = escape_string(*func_filename); - streams.err.append_format(_(L"%ls: Error while reading file '%ls'\n"), cmd, - esc == L"-" ? L"" : esc.c_str()); - } else { - retval = parser.get_last_status(); - } - - // Do not close fd after calling reader_read. reader_read automatically closes it before calling - // eval. - return retval; -} diff --git a/src/builtins/source.h b/src/builtins/source.h deleted file mode 100644 index a7587980c..000000000 --- a/src/builtins/source.h +++ /dev/null @@ -1,11 +0,0 @@ -// Prototypes for executing builtin_source function. -#ifndef FISH_BUILTIN_SOURCE_H -#define FISH_BUILTIN_SOURCE_H - -#include "../maybe.h" - -class Parser; using parser_t = Parser; -class IoStreams; using io_streams_t = IoStreams; - -maybe_t builtin_source(parser_t &parser, io_streams_t &streams, const wchar_t **argv); -#endif diff --git a/src/builtins/ulimit.cpp b/src/builtins/ulimit.cpp index c974895c0..c6ce01076 100644 --- a/src/builtins/ulimit.cpp +++ b/src/builtins/ulimit.cpp @@ -16,8 +16,8 @@ #include "../maybe.h" #include "../wgetopt.h" #include "../wutil.h" // IWYU pragma: keep - -class Parser; using parser_t = Parser; +#include "builtins/shared.rs.h" +#include "builtins/ulimit.h" /// Struct describing a resource limit. struct resource_t { @@ -108,9 +108,9 @@ static void print(int resource, int hard, io_streams_t &streams) { rlim_t l = get(resource, hard); if (l == RLIM_INFINITY) - streams.out.append(L"unlimited\n"); + streams.out()->append(format_string(L"unlimited\n")); else - streams.out.append_format(L"%lu\n", l / get_multiplier(resource)); + streams.out()->append(format_string(L"%lu\n", l / get_multiplier(resource))); } /// Print values of all resource limits. @@ -133,13 +133,14 @@ static void print_all(int hard, io_streams_t &streams) { ? L"(seconds, " : (get_multiplier(resource_arr[i].resource) == 1 ? L"(" : L"(kB, ")); - streams.out.append_format(L"%-*ls %10ls-%lc) ", w, resource_arr[i].desc, unit, - resource_arr[i].switch_char); + streams.out()->append(format_string(L"%-*ls %10ls-%lc) ", w, resource_arr[i].desc, unit, + resource_arr[i].switch_char)); if (l == RLIM_INFINITY) { - streams.out.append(L"unlimited\n"); + streams.out()->append(format_string(L"unlimited\n")); } else { - streams.out.append_format(L"%lu\n", l / get_multiplier(resource_arr[i].resource)); + streams.out()->append( + format_string(L"%lu\n", l / get_multiplier(resource_arr[i].resource))); } } } @@ -175,9 +176,9 @@ static int set_limit(int resource, int hard, int soft, rlim_t value, io_streams_ if (setrlimit(resource, &ls)) { if (errno == EPERM) { - streams.err.append_format( - L"ulimit: Permission denied when changing resource of type '%ls'\n", - get_desc(resource)); + streams.err()->append( + format_string(L"ulimit: Permission denied when changing resource of type '%ls'\n", + get_desc(resource))); } else { builtin_wperror(L"ulimit", streams); } @@ -187,7 +188,10 @@ static int set_limit(int resource, int hard, int soft, rlim_t value, io_streams_ } /// The ulimit builtin, used for setting resource limits. -maybe_t builtin_ulimit(parser_t &parser, io_streams_t &streams, const wchar_t **argv) { +int builtin_ulimit(const void *_parser, void *_streams, void *_argv) { + const auto &parser = *static_cast(_parser); + auto &streams = *static_cast(_streams); + auto argv = static_cast(_argv); const wchar_t *cmd = argv[0]; int argc = builtin_count_args(argv); bool report_all = false; @@ -379,11 +383,11 @@ maybe_t builtin_ulimit(parser_t &parser, io_streams_t &streams, const wchar return STATUS_CMD_OK; } case ':': { - builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]); + builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1], true); return STATUS_INVALID_ARGS; } case '?': { - builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]); + builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1], true); return STATUS_INVALID_ARGS; } default: { @@ -398,9 +402,9 @@ maybe_t builtin_ulimit(parser_t &parser, io_streams_t &streams, const wchar } if (what == RLIMIT_UNKNOWN) { - streams.err.append_format( - _(L"%ls: Resource limit not available on this operating system\n"), cmd); - builtin_print_error_trailer(parser, streams.err, cmd); + streams.err()->append( + format_string(_(L"%ls: Resource limit not available on this operating system\n"), cmd)); + builtin_print_error_trailer(parser, *streams.err(), cmd); return STATUS_INVALID_ARGS; } @@ -410,8 +414,8 @@ maybe_t builtin_ulimit(parser_t &parser, io_streams_t &streams, const wchar print(what, hard, streams); return STATUS_CMD_OK; } else if (arg_count != 1) { - streams.err.append_format(BUILTIN_ERR_TOO_MANY_ARGUMENTS, cmd); - builtin_print_error_trailer(parser, streams.err, cmd); + streams.err()->append(format_string(BUILTIN_ERR_TOO_MANY_ARGUMENTS, cmd)); + builtin_print_error_trailer(parser, *streams.err(), cmd); return STATUS_INVALID_ARGS; } @@ -423,8 +427,8 @@ maybe_t builtin_ulimit(parser_t &parser, io_streams_t &streams, const wchar rlim_t new_limit; if (*argv[w.woptind] == L'\0') { - streams.err.append_format(_(L"%ls: New limit cannot be an empty string\n"), cmd); - builtin_print_error_trailer(parser, streams.err, cmd); + streams.err()->append(format_string(_(L"%ls: New limit cannot be an empty string\n"), cmd)); + builtin_print_error_trailer(parser, *streams.err(), cmd); return STATUS_INVALID_ARGS; } else if (wcscasecmp(argv[w.woptind], L"unlimited") == 0) { new_limit = RLIM_INFINITY; @@ -435,8 +439,9 @@ maybe_t builtin_ulimit(parser_t &parser, io_streams_t &streams, const wchar } else { new_limit = fish_wcstol(argv[w.woptind]); if (errno) { - streams.err.append_format(_(L"%ls: Invalid limit '%ls'\n"), cmd, argv[w.woptind]); - builtin_print_error_trailer(parser, streams.err, cmd); + streams.err()->append( + format_string(_(L"%ls: Invalid limit '%ls'\n"), cmd, argv[w.woptind])); + builtin_print_error_trailer(parser, *streams.err(), cmd); return STATUS_INVALID_ARGS; } new_limit *= get_multiplier(what); diff --git a/src/builtins/ulimit.h b/src/builtins/ulimit.h index 1e329e7f7..1707542f8 100644 --- a/src/builtins/ulimit.h +++ b/src/builtins/ulimit.h @@ -4,8 +4,10 @@ #include "../maybe.h" -class Parser; using parser_t = Parser; -class IoStreams; using io_streams_t = IoStreams; +struct Parser; +struct IoStreams; +using parser_t = Parser; +using io_streams_t = IoStreams; -maybe_t builtin_ulimit(parser_t &parser, io_streams_t &streams, const wchar_t **argv); +int builtin_ulimit(const void *parser, void *streams, void *argv); #endif diff --git a/src/color.h b/src/color.h index 08c581fb8..937afe48c 100644 --- a/src/color.h +++ b/src/color.h @@ -76,6 +76,17 @@ class rgb_color_t { /// Returns whether the color is the normal special color. bool is_normal(void) const { return type == type_normal; } + void set_is_named() { type = type_named; } + void set_is_rgb() { type = type_rgb; } + void set_is_normal() { type = type_normal; } + void set_is_reset() { type = type_reset; } + void set_name_idx(uint8_t idx) { data.name_idx = idx; } + void set_color(uint8_t r, uint8_t g, uint8_t b) { + data.color.rgb[0] = r; + data.color.rgb[1] = g; + data.color.rgb[2] = b; + } + /// Returns whether the color is the reset special color. bool is_reset(void) const { return type == type_reset; } diff --git a/src/common.cpp b/src/common.cpp index 6607dbba0..feab40d4a 100644 --- a/src/common.cpp +++ b/src/common.cpp @@ -33,7 +33,9 @@ #include #include "common.h" +#if INCLUDE_RUST_HEADERS #include "common.rs.h" +#endif #include "expand.h" #include "fallback.h" // IWYU pragma: keep #include "flog.h" @@ -42,7 +44,6 @@ #include "iothread.h" #include "signals.h" #include "termsize.h" -#include "topic_monitor.h" #include "wcstringutil.h" #include "wildcard.h" #include "wutil.h" // IWYU pragma: keep @@ -660,7 +661,7 @@ void narrow_string_safe(char buff[64], const wchar_t *s) { /// Escape a string in a fashion suitable for using as a URL. Store the result in out_str. static void escape_string_url(const wcstring &in, wcstring &out) { - auto result = rust_escape_string_url(in.c_str(), in.size()); + auto result = escape_string_url(in.c_str(), in.size()); if (result) { out = *result; } @@ -668,7 +669,7 @@ static void escape_string_url(const wcstring &in, wcstring &out) { /// Escape a string in a fashion suitable for using as a fish var name. Store the result in out_str. static void escape_string_var(const wcstring &in, wcstring &out) { - auto result = rust_escape_string_var(in.c_str(), in.size()); + auto result = escape_string_var(in.c_str(), in.size()); if (result) { out = *result; } @@ -693,7 +694,7 @@ wcstring escape_string_for_double_quotes(wcstring in) { /// Escape a string in a fashion suitable for using in fish script. Store the result in out_str. static void escape_string_script(const wchar_t *orig_in, size_t in_len, wcstring &out, escape_flags_t flags) { - auto result = rust_escape_string_script(orig_in, in_len, flags); + auto result = escape_string_script(orig_in, in_len, flags); if (result) { out = *result; } @@ -977,32 +978,6 @@ maybe_t read_unquoted_escape(const wchar_t *input, wcstring *result, boo return in_pos; } -bool unescape_string_in_place(wcstring *str, unescape_flags_t escape_special) { - assert(str != nullptr); - wcstring output; - if (auto unescaped = unescape_string(str->c_str(), str->size(), escape_special)) { - *str = *unescaped; - return true; - } - return false; -} - -std::unique_ptr unescape_string(const wchar_t *input, unescape_flags_t escape_special, - escape_string_style_t style) { - return unescape_string(input, std::wcslen(input), escape_special, style); -} - -std::unique_ptr unescape_string(const wchar_t *input, size_t len, - unescape_flags_t escape_special, - escape_string_style_t style) { - return rust_unescape_string(input, len, escape_special, style); -} - -std::unique_ptr unescape_string(const wcstring &input, unescape_flags_t escape_special, - escape_string_style_t style) { - return unescape_string(input.c_str(), input.size(), escape_special, style); -} - wcstring format_size(long long sz) { wcstring result; const wchar_t *sz_name[] = {L"kB", L"MB", L"GB", L"TB", L"PB", L"EB", L"ZB", L"YB", nullptr}; @@ -1294,7 +1269,5 @@ bool is_console_session() { /// can be init even if the rust version of the function is called instead. This is easier than /// declaring all those variables as extern, which I'll do in a separate PR. extern "C" { - void fish_setlocale_ffi() { - fish_setlocale(); - } +void fish_setlocale_ffi() { fish_setlocale(); } } diff --git a/src/common.h b/src/common.h index b1fb270b9..d13abf6cd 100644 --- a/src/common.h +++ b/src/common.h @@ -502,22 +502,6 @@ wcstring escape_string_for_double_quotes(wcstring in); maybe_t read_unquoted_escape(const wchar_t *input, wcstring *result, bool allow_incomplete, bool unescape_special); -/// Unescapes a string in-place. A true result indicates the string was unescaped, a false result -/// indicates the string was unmodified. -bool unescape_string_in_place(wcstring *str, unescape_flags_t escape_special); - -/// Reverse the effects of calling `escape_string`. Returns the unescaped value by reference. On -/// failure, the output is set to an empty string. -std::unique_ptr unescape_string(const wchar_t *input, unescape_flags_t escape_special, - escape_string_style_t style = STRING_STYLE_SCRIPT); - -std::unique_ptr unescape_string(const wchar_t *input, size_t len, - unescape_flags_t escape_special, - escape_string_style_t style = STRING_STYLE_SCRIPT); - -std::unique_ptr unescape_string(const wcstring &input, unescape_flags_t escape_special, - escape_string_style_t style = STRING_STYLE_SCRIPT); - /// Return the number of seconds from the UNIX epoch, with subsecond precision. This function uses /// the gettimeofday function and will have the same precision as that function. using timepoint_t = double; @@ -694,4 +678,8 @@ __attribute__((always_inline)) bool inline iswdigit(const wchar_t c) { return c >= L'0' && c <= L'9'; } +#if INCLUDE_RUST_HEADERS +#include "common.rs.h" +#endif + #endif // FISH_COMMON_H diff --git a/src/complete.cpp b/src/complete.cpp deleted file mode 100644 index 64bbffa09..000000000 --- a/src/complete.cpp +++ /dev/null @@ -1,1967 +0,0 @@ -/// Functions related to tab-completion. -/// -/// These functions are used for storing and retrieving tab-completion data, as well as for -/// performing tab-completion. -/// -#include "config.h" // IWYU pragma: keep - -#include "complete.h" - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "abbrs.h" -#include "autoload.h" -#include "builtin.h" -#include "common.h" -#include "enum_set.h" -#include "env.h" -#include "exec.h" -#include "expand.h" -#include "fallback.h" // IWYU pragma: keep -#include "flog.h" -#include "function.h" -#include "global_safety.h" -#include "history.h" -#include "maybe.h" -#include "operation_context.h" -#include "parse_constants.h" -#include "parse_util.h" -#include "parser.h" -#include "parser_keywords.h" -#include "path.h" -#include "tokenizer.h" -#include "util.h" -#include "wcstringutil.h" -#include "wildcard.h" -#include "wutil.h" // IWYU pragma: keep - -// Completion description strings, mostly for different types of files, such as sockets, block -// devices, etc. -// -// There are a few more completion description strings defined in expand.c. Maybe all completion -// description strings should be defined in the same file? - -/// Description for ~USER completion. -#define COMPLETE_USER_DESC _(L"Home for %ls") - -/// Description for short variables. The value is concatenated to this description. -#define COMPLETE_VAR_DESC_VAL _(L"Variable: %ls") - -/// Description for abbreviations. -#define ABBR_DESC _(L"Abbreviation: %ls") - -/// The special cased translation macro for completions. The empty string needs to be special cased, -/// since it can occur, and should not be translated. (Gettext returns the version information as -/// the response). -#ifdef HAVE_GETTEXT -static const wcstring &C_(const wcstring &s) { - return s.empty() ? g_empty_string : wgettext(s.c_str()); -} -#else -static const wcstring &C_(const wcstring &s) { return s; } -#endif - -/// Struct describing a completion rule for options to a command. -/// -/// If option is empty, the comp field must not be empty and contains a list of arguments to the -/// command. -/// -/// The type field determines how the option is to be interpreted: either empty (args_only) or -/// short, single-long ("old") or double-long ("GNU"). An invariant is that the option is empty if -/// and only if the type is args_only. -/// -/// If option is non-empty, it specifies a switch for the command. If \c comp is also not empty, it -/// contains a list of non-switch arguments that may only follow directly after the specified -/// switch. -namespace { -struct complete_entry_opt_t { - /// Text of the option (like 'foo'). - wcstring option; - // Arguments to the option; may be a subshell expression expanded at evaluation time. - wcstring comp; - /// Description of the completion. - wcstring desc; - // Conditions under which to use the option, expanded and evaluated at completion time. - std::vector conditions; - /// Type of the option: args_only, short, single_long, or double_long. - complete_option_type_t type; - /// Determines how completions should be performed on the argument after the switch. - completion_mode_t result_mode; - /// Completion flags. - complete_flags_t flags; - - const wcstring &localized_desc() const { return C_(desc); } - - size_t expected_dash_count() const { - switch (this->type) { - case option_type_args_only: - return 0; - case option_type_short: - case option_type_single_long: - return 1; - case option_type_double_long: - return 2; - } - DIE("unreachable"); - } -}; - -/// Last value used in the order field of completion_entry_t. -static relaxed_atomic_t k_complete_order{0}; - -/// Struct describing a command completion. -using option_list_t = std::vector; -class completion_entry_t { - public: - /// List of all options. - option_list_t options; - - /// Order for when this completion was created. This aids in outputting completions sorted by - /// time. - const unsigned int order; - - /// Getters for option list. - const option_list_t &get_options() const { return options; } - - /// Adds an option. - void add_option(complete_entry_opt_t &&opt) { options.push_back(std::move(opt)); } - - /// Remove all completion options in the specified entry that match the specified short / long - /// option strings. Returns true if it is now empty and should be deleted, false if it's not - /// empty. - bool remove_option(const wcstring &option, complete_option_type_t type) { - options.erase(std::remove_if(options.begin(), options.end(), - [&](const complete_entry_opt_t &opt) { - return opt.option == option && opt.type == type; - }), - options.end()); - return this->options.empty(); - } - - completion_entry_t() : order(++k_complete_order) {} -}; -} // namespace - -/// Set of all completion entries. Keyed by the command name, and whether it is a path. -using completion_key_t = std::pair; -using completion_entry_map_t = std::map; -static owning_lock s_completion_map; - -/// Completion "wrapper" support. The map goes from wrapping-command to wrapped-command-list. -using wrapper_map_t = std::unordered_map>; -static owning_lock wrapper_map; - -description_func_t const_desc(const wcstring &s) { - return [=](const wcstring &ignored) { - UNUSED(ignored); - return s; - }; -} - -/// Clear the COMPLETE_AUTO_SPACE flag, and set COMPLETE_NO_SPACE appropriately depending on the -/// suffix of the string. -static complete_flags_t resolve_auto_space(const wcstring &comp, complete_flags_t flags) { - complete_flags_t new_flags = flags; - if (flags & COMPLETE_AUTO_SPACE) { - new_flags &= ~COMPLETE_AUTO_SPACE; - size_t len = comp.size(); - if (len > 0 && (std::wcschr(L"/=@:.,-", comp.at(len - 1)) != nullptr)) - new_flags |= COMPLETE_NO_SPACE; - } - return new_flags; -} - -/// completion_t functions. Note that the constructor resolves flags! -completion_t::completion_t(wcstring comp, wcstring desc, string_fuzzy_match_t mat, - complete_flags_t flags_val) - : completion(std::move(comp)), - description(std::move(desc)), - match(mat), - flags(resolve_auto_space(completion, flags_val)) {} - -completion_t::completion_t(const completion_t &) = default; -completion_t::completion_t(completion_t &&) = default; -completion_t &completion_t::operator=(const completion_t &) = default; -completion_t &completion_t::operator=(completion_t &&) = default; -completion_t::~completion_t() = default; - -__attribute__((always_inline)) static inline bool natural_compare_completions( - const completion_t &a, const completion_t &b) { - // For this to work, stable_sort must be used because results aren't interchangeable. - if (a.flags & b.flags & COMPLETE_DONT_SORT) { - // Both completions are from a source with the --keep-order flag. - return false; - } - return wcsfilecmp(a.completion.c_str(), b.completion.c_str()) < 0; -} - -void completion_t::prepend_token_prefix(const wcstring &prefix) { - if (this->flags & COMPLETE_REPLACES_TOKEN) { - this->completion.insert(0, prefix); - } -} - -bool completion_receiver_t::add(completion_t &&comp) { - if (this->completions_.size() >= limit_) { - return false; - } - this->completions_.push_back(std::move(comp)); - return true; -} - -bool completion_receiver_t::add(wcstring &&comp) { return this->add(std::move(comp), wcstring{}); } - -bool completion_receiver_t::add(wcstring &&comp, wcstring desc, complete_flags_t flags, - string_fuzzy_match_t match) { - return this->add(completion_t(std::move(comp), std::move(desc), match, flags)); -} - -bool completion_receiver_t::add_list(completion_list_t &&lst) { - size_t total_size = lst.size() + this->size(); - if (total_size < this->size() || total_size > limit_) { - return false; - } - - if (completions_.empty()) { - completions_ = std::move(lst); - } else { - completions_.reserve(completions_.size() + lst.size()); - std::move(lst.begin(), lst.end(), std::back_inserter(completions_)); - } - return true; -} - -completion_list_t completion_receiver_t::take() { - completion_list_t res{}; - std::swap(res, this->completions_); - return res; -} - -completion_receiver_t completion_receiver_t::subreceiver() const { - size_t remaining_capacity = limit_ < size() ? 0 : limit_ - size(); - return completion_receiver_t(remaining_capacity); -} - -// If these functions aren't force inlined, it is actually faster to call -// stable_sort twice rather than to iterate once performing all comparisons in one go! -__attribute__((always_inline)) static inline bool compare_completions_by_duplicate_arguments( - const completion_t &a, const completion_t &b) { - bool ad = a.flags & COMPLETE_DUPLICATES_ARGUMENT; - bool bd = b.flags & COMPLETE_DUPLICATES_ARGUMENT; - return ad < bd; -} - -__attribute__((always_inline)) static inline bool compare_completions_by_tilde( - const completion_t &a, const completion_t &b) { - if (a.completion.empty() || b.completion.empty()) { - return false; - } - return ((a.completion.back() == L'~') < (b.completion.back() == L'~')); -} - -/// Unique the list of completions, without perturbing their order. -static void unique_completions_retaining_order(completion_list_t *comps) { - std::unordered_set seen; - seen.reserve(comps->size()); - auto pred = [&seen](const completion_t &c) { - // Remove (return true) if insertion fails. - bool inserted = seen.insert(c.completion).second; - return !inserted; - }; - comps->erase(std::remove_if(comps->begin(), comps->end(), pred), comps->end()); -} - -void completions_sort_and_prioritize(completion_list_t *comps, completion_request_options_t flags) { - if (comps->empty()) return; - - // Find the best rank. - uint32_t best_rank = UINT32_MAX; - for (const auto &comp : *comps) { - best_rank = std::min(best_rank, comp.rank()); - } - - // Throw out completions of worse ranks. - comps->erase(std::remove_if(comps->begin(), comps->end(), - [=](const completion_t &comp) { return comp.rank() > best_rank; }), - comps->end()); - - // Deduplicate both sorted and unsorted results. - unique_completions_retaining_order(comps); - - // Sort, provided COMPLETE_DONT_SORT isn't set. - // Here we do not pass suppress_exact, so that exact matches appear first. - stable_sort(comps->begin(), comps->end(), natural_compare_completions); - - // Lastly, if this is for an autosuggestion, prefer to avoid completions that duplicate - // arguments, and penalize files that end in tilde - they're frequently autosave files from e.g. - // emacs. Also prefer samecase to smartcase. - if (flags.autosuggestion) { - stable_sort(comps->begin(), comps->end(), [](const completion_t &a, const completion_t &b) { - if (a.match.case_fold != b.match.case_fold) { - return a.match.case_fold < b.match.case_fold; - } - return compare_completions_by_duplicate_arguments(a, b) || - compare_completions_by_tilde(a, b); - }); - } -} - -namespace { -/// Class representing an attempt to compute completions. -class completer_t { - /// The operation context for this completion. - const operation_context_t &ctx; - - /// Flags associated with the completion request. - const completion_request_options_t flags; - - /// The output completions. - completion_receiver_t completions; - - /// Commands which we would have tried to load, if we had a parser. - std::vector needs_load; - - /// Table of completions conditions that have already been tested and the corresponding test - /// results. - using condition_cache_t = std::unordered_map; - condition_cache_t condition_cache; - - bool try_complete_variable(const wcstring &str); - bool try_complete_user(const wcstring &str); - - bool complete_param_for_command(const wcstring &cmd_orig, const wcstring &popt, - const wcstring &str, bool use_switches, bool *out_do_file); - - void complete_param_expand(const wcstring &str, bool do_file, - bool handle_as_special_cd = false); - - void complete_cmd(const wcstring &str); - - /// Attempt to complete an abbreviation for the given string. - void complete_abbr(const wcstring &cmd); - - void complete_from_args(const wcstring &str, const wcstring &args, const wcstring &desc, - complete_flags_t flags); - - void complete_cmd_desc(const wcstring &str); - - bool complete_variable(const wcstring &str, size_t start_offset); - - bool condition_test(const wcstring &condition); - bool conditions_test(const std::vector &conditions); - - void complete_strings(const wcstring &wc_escaped, const description_func_t &desc_func, - const completion_list_t &possible_comp, complete_flags_t flags, - expand_flags_t extra_expand_flags = {}); - - expand_flags_t expand_flags() const { - expand_flags_t result{}; - if (flags.autosuggestion) result |= expand_flag::skip_cmdsubst; - if (flags.fuzzy_match) result |= expand_flag::fuzzy_match; - if (flags.descriptions) result |= expand_flag::gen_descriptions; - return result; - } - - // Bag of data to support expanding a command's arguments using custom completions, including - // the wrap chain. - struct custom_arg_data_t { - explicit custom_arg_data_t(std::vector *vars) : var_assignments(vars) { - assert(vars); - } - - // The unescaped argument before the argument which is being completed, or empty if none. - wcstring previous_argument{}; - - // The unescaped argument which is being completed, or empty if none. - wcstring current_argument{}; - - // Whether a -- has been encountered, which suppresses options. - bool had_ddash{false}; - - // Whether to perform file completions. - // This is an "out" parameter of the wrap chain walk: if any wrapped command suppresses file - // completions this gets set to false. - bool do_file{true}; - - // Depth in the wrap chain. - size_t wrap_depth{0}; - - // The list of variable assignments: escaped strings of the form VAR=VAL. - // This may be temporarily appended to as we explore the wrap chain. - // When completing, variable assignments are really set in a local scope. - std::vector *var_assignments; - - // The set of wrapped commands which we have visited, and so should not be explored again. - std::set visited_wrapped_commands{}; - }; - - void complete_custom(const wcstring &cmd, const wcstring &cmdline, custom_arg_data_t *ad); - - void walk_wrap_chain(const wcstring &cmd, const wcstring &cmdline, source_range_t cmdrange, - custom_arg_data_t *ad); - - cleanup_t apply_var_assignments(const std::vector &var_assignments); - - bool empty() const { return completions.empty(); } - - void escape_opening_brackets(const wcstring &argument); - - void mark_completions_duplicating_arguments(const wcstring &cmd, const wcstring &prefix, - const std::vector &args); - - public: - completer_t(const operation_context_t &ctx, completion_request_options_t f) - : ctx(ctx), flags(f), completions(ctx.expansion_limit) {} - - void perform_for_commandline(wcstring cmdline); - - completion_list_t acquire_completions() { return completions.take(); } - - std::vector acquire_needs_load() { return std::move(needs_load); } -}; - -// Autoloader for completions. -static owning_lock completion_autoloader{autoload_t(L"fish_complete_path")}; - -/// Test if the specified script returns zero. The result is cached, so that if multiple completions -/// use the same condition, it needs only be evaluated once. condition_cache_clear must be called -/// after a completion run to make sure that there are no stale completions. -bool completer_t::condition_test(const wcstring &condition) { - if (condition.empty()) { - // std::fwprintf( stderr, L"No condition specified\n" ); - return true; - } - if (!ctx.parser) { - return false; - } - - bool test_res; - auto cached_entry = condition_cache.find(condition); - if (cached_entry == condition_cache.end()) { - // Compute new value and reinsert it. - test_res = - (0 == exec_subshell(condition, *ctx.parser, false /* don't apply exit status */)); - condition_cache[condition] = test_res; - } else { - // Use the old value. - test_res = cached_entry->second; - } - return test_res; -} - -bool completer_t::conditions_test(const std::vector &conditions) { - for (const auto &c : conditions) { - if (!condition_test(c)) return false; - } - return true; -} - -/// Find the full path and commandname from a command string 'str'. -static void parse_cmd_string(const wcstring &str, wcstring *path, wcstring *cmd, - const environment_t &vars) { - auto path_result = path_try_get_path(str, vars); - bool found = (path_result.err == 0); - *path = std::move(path_result.path); - // Resolve commands that use relative paths because we compare full paths with "complete -p". - if (found && !str.empty() && str.at(0) != L'/') { - if (auto full_path = wrealpath(*path)) { - path->assign(full_path.acquire()); - } - } - - // Make sure the path is not included in the command. - size_t last_slash = str.find_last_of(L'/'); - if (last_slash != wcstring::npos) { - *cmd = str.substr(last_slash + 1); - } else { - *cmd = str; - } -} - -/// Copy any strings in possible_comp which have the specified prefix to the -/// completer's completion array. The prefix may contain wildcards. The output -/// will consist of completion_t structs. -/// -/// There are three ways to specify descriptions for each completion. Firstly, -/// if a description has already been added to the completion, it is _not_ -/// replaced. Secondly, if the desc_func function is specified, use it to -/// determine a dynamic completion. Thirdly, if none of the above are available, -/// the desc string is used as a description. -/// -/// @param wc_escaped -/// the prefix, possibly containing wildcards. The wildcard should not have -/// been unescaped, i.e. '*' should be used for any string, not the -/// ANY_STRING character. -/// @param desc_func -/// the function that generates a description for those completions without an -/// embedded description -/// @param possible_comp -/// the list of possible completions to iterate over -/// @param flags -/// The flags controlling completion -/// @param extra_expand_flags -/// Additional flags controlling expansion. -void completer_t::complete_strings(const wcstring &wc_escaped, const description_func_t &desc_func, - const completion_list_t &possible_comp, complete_flags_t flags, - expand_flags_t extra_expand_flags) { - wcstring tmp = wc_escaped; - if (!expand_one(tmp, - this->expand_flags() | extra_expand_flags | expand_flag::skip_cmdsubst | - expand_flag::skip_wildcards, - ctx)) - return; - - const wcstring wc = parse_util_unescape_wildcards(tmp); - - for (const auto &comp : possible_comp) { - const wcstring &comp_str = comp.completion; - if (!comp_str.empty()) { - wildcard_complete(comp_str, wc.c_str(), desc_func, &this->completions, - this->expand_flags() | extra_expand_flags, flags); - } - } -} - -/// If command to complete is short enough, substitute the description with the whatis information -/// for the executable. -void completer_t::complete_cmd_desc(const wcstring &str) { - if (!ctx.parser) return; - - wcstring cmd; - size_t pos = str.find_last_of(L'/'); - if (pos != std::string::npos) { - if (pos + 1 > str.length()) return; - cmd = wcstring(str, pos + 1); - } else { - cmd = str; - } - - // Using apropos with a single-character search term produces far to many results - require at - // least two characters if we don't know the location of the whatis-database. - if (cmd.length() < 2) return; - - if (wildcard_has(cmd)) { - return; - } - - bool skip = true; - for (const auto &c : completions.get_list()) { - if (c.completion.empty() || (c.completion.back() != L'/')) { - skip = false; - break; - } - } - - if (skip) { - return; - } - - wcstring lookup_cmd(L"__fish_describe_command "); - lookup_cmd.append(escape_string(cmd)); - - // First locate a list of possible descriptions using a single call to apropos or a direct - // search if we know the location of the whatis database. This can take some time on slower - // systems with a large set of manuals, but it should be ok since apropos is only called once. - std::vector list; - (void)exec_subshell(lookup_cmd, *ctx.parser, list, false /* don't apply exit status */); - - // Then discard anything that is not a possible completion and put the result into a - // hashtable with the completion as key and the description as value. - std::unordered_map lookup; - // A typical entry is the command name, followed by a tab, followed by a description. - for (const wcstring &elstr : list) { - // Skip keys that are too short. - if (elstr.size() < cmd.size()) continue; - - // Skip cases without a tab, or without a description, or bizarre cases where the tab is - // part of the command. - size_t tab_idx = elstr.find(L'\t'); - if (tab_idx == wcstring::npos || tab_idx + 1 >= elstr.size() || tab_idx < cmd.size()) - continue; - - // Make the key. This is the stuff after the command. - // For example: - // elstr = lsmod - // cmd = ls - // key = mod - // Note an empty key is common and natural, if 'cmd' were already valid. - wcstring key(elstr, cmd.size(), tab_idx - cmd.size()); - wcstring val(elstr, tab_idx + 1); - assert(!val.empty() && "tab index should not have been at the end."); - - // And once again I make sure the first character is uppercased because I like it that - // way, and I get to decide these things. - val.at(0) = towupper(val.at(0)); - lookup.emplace(std::move(key), std::move(val)); - } - - // Then do a lookup on every completion and if a match is found, change to the new - // description. - for (auto &completion : completions.get_list()) { - const wcstring &el = completion.completion; - auto new_desc_iter = lookup.find(el); - if (new_desc_iter != lookup.end()) completion.description = new_desc_iter->second; - } -} - -/// Returns a description for the specified function, or an empty string if none. -static wcstring complete_function_desc(const wcstring &fn) { - if (auto props = function_get_props(fn)) { - std::unique_ptr desc = (*props)->get_description(); - return std::move(*desc); - } - return wcstring{}; -} - -/// Complete the specified command name. Search for executables in the path, executables defined -/// using an absolute path, functions, builtins and directories for implicit cd commands. -/// -/// \param str_cmd the command string to find completions for -void completer_t::complete_cmd(const wcstring &str_cmd) { - completion_list_t possible_comp; - - // Append all possible executables - expand_result_t result = expand_string( - str_cmd, &this->completions, - this->expand_flags() | expand_flag::special_for_command | expand_flag::for_completions | - expand_flag::preserve_home_tildes | expand_flag::executables_only, - ctx); - if (result == expand_result_t::cancel) { - return; - } - if (result == expand_result_t::ok && this->flags.descriptions) { - this->complete_cmd_desc(str_cmd); - } - - // We don't really care if this succeeds or fails. If it succeeds this->completions will be - // updated with choices for the user. - expand_result_t ignore = - // Append all matching directories - expand_string(str_cmd, &this->completions, - this->expand_flags() | expand_flag::for_completions | - expand_flag::preserve_home_tildes | expand_flag::directories_only, - ctx); - UNUSED(ignore); - - if (str_cmd.empty() || (str_cmd.find(L'/') == wcstring::npos && str_cmd.at(0) != L'~')) { - bool include_hidden = !str_cmd.empty() && str_cmd.at(0) == L'_'; - wcstring_list_ffi_t names{}; - function_get_names(include_hidden, names); - for (wcstring &name : names.vals) { - // Append all known matching functions - append_completion(&possible_comp, std::move(name)); - } - - this->complete_strings(str_cmd, complete_function_desc, possible_comp, 0); - - possible_comp.clear(); - - // Append all matching builtins - builtin_get_names(&possible_comp); - this->complete_strings(str_cmd, builtin_get_desc, possible_comp, 0); - } -} - -void completer_t::complete_abbr(const wcstring &cmd) { - // Copy the list of names and descriptions so as not to hold the lock across the call to - // complete_strings. - completion_list_t possible_comp; - std::unordered_map descs; - { - auto abbrs = abbrs_list(); - for (const auto &abbr : abbrs) { - if (!abbr.is_regex) { - possible_comp.emplace_back(*abbr.key); - descs[*abbr.key] = *abbr.replacement; - } - } - } - - auto desc_func = [&](const wcstring &key) { - auto iter = descs.find(key); - assert(iter != descs.end() && "Abbreviation not found"); - return format_string(ABBR_DESC, iter->second.c_str()); - }; - this->complete_strings(cmd, desc_func, possible_comp, COMPLETE_NO_SPACE); -} - -/// Evaluate the argument list (as supplied by complete -a) and insert any -/// return matching completions. Matching is done using @c -/// copy_strings_with_prefix, meaning the completion may contain wildcards. -/// Logically, this is not always the right thing to do, but I have yet to come -/// up with a case where this matters. -/// -/// @param str -/// The string to complete. -/// @param args -/// The list of option arguments to be evaluated. -/// @param desc -/// Description of the completion -/// @param flags -/// The flags -/// -void completer_t::complete_from_args(const wcstring &str, const wcstring &args, - const wcstring &desc, complete_flags_t flags) { - const bool is_autosuggest = this->flags.autosuggestion; - - bool saved_interactive = false; - statuses_t status; - if (ctx.parser) { - saved_interactive = ctx.parser->libdata().is_interactive; - ctx.parser->libdata().is_interactive = false; - status = ctx.parser->get_last_statuses(); - } - - expand_flags_t eflags{}; - if (is_autosuggest) { - eflags |= expand_flag::skip_cmdsubst; - } - - completion_list_t possible_comp = parser_t::expand_argument_list(args, eflags, ctx); - - if (ctx.parser) { - ctx.parser->libdata().is_interactive = saved_interactive; - ctx.parser->set_last_statuses(status); - } - - // Allow leading dots - see #3707. - this->complete_strings(escape_string(str), const_desc(desc), possible_comp, flags, - expand_flag::allow_nonliteral_leading_dot); -} - -static size_t leading_dash_count(const wchar_t *str) { - size_t cursor = 0; - while (str[cursor] == L'-') { - cursor++; - } - return cursor; -} - -/// Match a parameter. -static bool param_match(const complete_entry_opt_t *e, const wchar_t *optstr) { - bool result = false; - if (e->type != option_type_args_only) { - size_t dashes = leading_dash_count(optstr); - result = (dashes == e->expected_dash_count() && e->option == &optstr[dashes]); - } - return result; -} - -/// Test if a string is an option with an argument, like --color=auto or -I/usr/include. -static const wchar_t *param_match2(const complete_entry_opt_t *e, const wchar_t *optstr) { - // We may get a complete_entry_opt_t with no options if it's just arguments. - if (e->option.empty()) { - return nullptr; - } - - // Verify leading dashes. - size_t cursor = leading_dash_count(optstr); - if (cursor != e->expected_dash_count()) { - return nullptr; - } - - // Verify options match. - if (!string_prefixes_string(e->option, &optstr[cursor])) { - return nullptr; - } - cursor += e->option.length(); - - // Short options are like -DNDEBUG. Long options are like --color=auto. So check for an equal - // sign for long options. - assert(e->type != option_type_short); - if (optstr[cursor] != L'=') { - return nullptr; - } - cursor += 1; - return &optstr[cursor]; -} - -/// Parses a token of short options plus one optional parameter like -/// '-xzPARAM', where x and z are short options. -/// -/// Returns the position of the last option character (e.g. the position of z which is 2). -/// Everything after that is assumed to be part of the parameter. -/// Returns wcstring::npos if there is no valid short option. -static size_t short_option_pos(const wcstring &arg, const option_list_t &options) { - if (arg.size() <= 1 || leading_dash_count(arg.c_str()) != 1) { - return wcstring::npos; - } - for (size_t pos = 1; pos < arg.size(); pos++) { - wchar_t arg_char = arg.at(pos); - const complete_entry_opt_t *match = nullptr; - for (const complete_entry_opt_t &o : options) { - if (o.type == option_type_short && o.option.at(0) == arg_char) { - match = &o; - break; - } - } - if (match == nullptr) { - // The first character after the dash is not a valid option. - if (pos == 1) return wcstring::npos; - return pos - 1; - } - if (match->result_mode.requires_param) { - return pos; - } - } - return arg.size() - 1; -} - -/// complete_param: Given a command, find completions for the argument str of command cmd_orig with -/// previous option popt. If file completions should be disabled, then mark *out_do_file as false. -/// -/// \return true if successful, false if there's an error. -/// -/// Examples in format (cmd, popt, str): -/// -/// echo hello world -> ("echo", "world", "") -/// echo hello world -> ("echo", "hello", "world") -/// -bool completer_t::complete_param_for_command(const wcstring &cmd_orig, const wcstring &popt, - const wcstring &str, bool use_switches, - bool *out_do_file) { - bool use_files = true, has_force = false; - - wcstring cmd, path; - parse_cmd_string(cmd_orig, &path, &cmd, ctx.vars); - - // Don't use cmd_orig here for paths. It's potentially pathed, - // so that command might exist, but the completion script - // won't be using it. - bool cmd_exists = builtin_exists(cmd) || function_exists_no_autoload(cmd) || - path_get_path(cmd, ctx.vars).has_value(); - if (!cmd_exists) { - // Do not load custom completions if the command does not exist - // This prevents errors caused during the execution of completion providers for - // tools that do not exist. Applies to both manual completions ("cm", "cmd ") - // and automatic completions ("gi" autosuggestion provider -> git) - FLOG(complete, "Skipping completions for non-existent command"); - } else if (ctx.parser) { - complete_load(cmd, *ctx.parser); - } else if (!completion_autoloader.acquire()->has_attempted_autoload(cmd)) { - needs_load.push_back(cmd); - } - - // Make a list of lists of all options that we care about. - std::vector all_options; - { - auto completion_map = s_completion_map.acquire(); - for (const auto &kv : *completion_map) { - const completion_key_t &key = kv.first; - bool cmd_is_path = key.second; - const wcstring &match = cmd_is_path ? path : cmd; - if (wildcard_match(match, key.first)) { - // Copy all of their options into our list. Oof, this is a lot of copying. - // We have to copy them in reverse order to preserve legacy behavior (#9221). - const auto &options = kv.second.get_options(); - all_options.emplace_back(options.rbegin(), options.rend()); - } - } - } - - // Now release the lock and test each option that we captured above. We have to do this outside - // the lock because callouts (like the condition) may add or remove completions. See issue 2. - for (const auto &options : all_options) { - size_t short_opt_pos = short_option_pos(str, options); - // We want last_option_requires_param to default to false but distinguish between when - // a previous completion has set it to false and when it has its default value. - maybe_t last_option_requires_param{}; - bool use_common = true; - if (use_switches) { - if (str[0] == L'-') { - // Check if we are entering a combined option and argument (like --color=auto or - // -I/usr/include). - for (const complete_entry_opt_t &o : options) { - const wchar_t *arg; - if (o.type == option_type_short) { - if (short_opt_pos == wcstring::npos) continue; - if (o.option.at(0) != str.at(short_opt_pos)) continue; - arg = str.c_str() + short_opt_pos + 1; - } else { - arg = param_match2(&o, str.c_str()); - } - - if (this->conditions_test(o.conditions)) { - if (o.type == option_type_short) { - // Only override a true last_option_requires_param value with a false - // one - if (last_option_requires_param.has_value()) { - last_option_requires_param = - *last_option_requires_param && o.result_mode.requires_param; - } else { - last_option_requires_param = o.result_mode.requires_param; - } - } - if (arg != nullptr) { - if (o.result_mode.requires_param) use_common = false; - if (o.result_mode.no_files) use_files = false; - if (o.result_mode.force_files) has_force = true; - complete_from_args(arg, o.comp, o.localized_desc(), o.flags); - } - } - } - } else if (popt[0] == L'-') { - // Set to true if we found a matching old-style switch. - // Here we are testing the previous argument, - // to see how we should complete the current argument - bool old_style_match = false; - - // If we are using old style long options, check for them first. - for (const complete_entry_opt_t &o : options) { - if (o.type == option_type_single_long && param_match(&o, popt.c_str()) && - this->conditions_test(o.conditions)) { - old_style_match = true; - if (o.result_mode.requires_param) use_common = false; - if (o.result_mode.no_files) use_files = false; - if (o.result_mode.force_files) has_force = true; - complete_from_args(str, o.comp, o.localized_desc(), o.flags); - } - } - - // No old style option matched, or we are not using old style options. We check if - // any short (or gnu style) options do. - if (!old_style_match) { - size_t prev_short_opt_pos = short_option_pos(popt, options); - for (const complete_entry_opt_t &o : options) { - // Gnu-style options with _optional_ arguments must be specified as a single - // token, so that it can be differed from a regular argument. - // Here we are testing the previous argument for a GNU-style match, - // to see how we should complete the current argument - if (!o.result_mode.requires_param) continue; - - bool match = false; - if (o.type == option_type_short) { - match = prev_short_opt_pos != wcstring::npos && - // Only if the option was the last char in the token, - // i.e. there is no parameter yet. - prev_short_opt_pos + 1 == popt.size() && - o.option.at(0) == popt.at(prev_short_opt_pos); - } else if (o.type == option_type_double_long) { - match = param_match(&o, popt.c_str()); - } - if (match && this->conditions_test(o.conditions)) { - if (o.result_mode.requires_param) use_common = false; - if (o.result_mode.no_files) use_files = false; - if (o.result_mode.force_files) has_force = true; - complete_from_args(str, o.comp, o.localized_desc(), o.flags); - } - } - } - } - } - - if (!use_common) { - continue; - } - - // Set a default value for last_option_requires_param only if one hasn't been set - if (!last_option_requires_param.has_value()) { - last_option_requires_param = false; - } - - // Now we try to complete an option itself - for (const complete_entry_opt_t &o : options) { - // If this entry is for the base command, check if any of the arguments match. - if (!this->conditions_test(o.conditions)) continue; - if (o.option.empty()) { - use_files = use_files && (!(o.result_mode.no_files)); - has_force = has_force || o.result_mode.force_files; - complete_from_args(str, o.comp, o.localized_desc(), o.flags); - } - - if (!use_switches || str.empty()) { - continue; - } - - // Check if the short style option matches. - if (o.type == option_type_short) { - wchar_t optchar = o.option.at(0); - if (short_opt_pos == wcstring::npos) { - // str has no short option at all (but perhaps it is the - // prefix of a single long option). - // Only complete short options if there is no character after the dash. - if (str != L"-") continue; - } else { - // Only complete when the last short option has no parameter yet.. - if (short_opt_pos + 1 != str.size()) continue; - // .. and it does not require one .. - if (*last_option_requires_param) continue; - // .. and the option is not already there. - if (str.find(optchar) != wcstring::npos) continue; - } - // It's a match. - wcstring desc = o.localized_desc(); - // Append a short-style option - if (!this->completions.add(wcstring{o.option}, std::move(desc), 0)) { - return false; - } - } - - // Check if the long style option matches. - if (o.type != option_type_single_long && o.type != option_type_double_long) { - continue; - } - - wcstring whole_opt(o.expected_dash_count(), L'-'); - whole_opt.append(o.option); - - if (whole_opt.length() < str.length()) { - continue; - } - int match = string_prefixes_string(str, whole_opt); - if (!match) { - bool match_no_case = wcsncasecmp(str.c_str(), whole_opt.c_str(), str.length()) == 0; - if (!match_no_case) { - continue; - } - } - - int has_arg = 0; // does this switch have any known arguments - int req_arg = 0; // does this switch _require_ an argument - size_t offset = 0; - complete_flags_t flags = 0; - - if (match) { - offset = str.length(); - } else { - flags = COMPLETE_REPLACES_TOKEN; - } - - has_arg = !o.comp.empty(); - req_arg = o.result_mode.requires_param; - - if (o.type == option_type_double_long && (has_arg && !req_arg)) { - // Optional arguments to a switch can only be handled using the '=', so we add it as - // a completion. By default we avoid using '=' and instead rely on '--switch - // switch-arg', since it is more commonly supported by homebrew getopt-like - // functions. - wcstring completion = format_string(L"%ls=", whole_opt.c_str() + offset); - // Append a long-style option with a mandatory trailing equal sign - if (!this->completions.add(std::move(completion), C_(o.desc), - flags | COMPLETE_NO_SPACE)) { - return false; - } - } - - // Append a long-style option - if (!this->completions.add(whole_opt.substr(offset), C_(o.desc), flags)) { - return false; - } - } - } - - if (has_force) { - *out_do_file = true; - } else if (!use_files) { - *out_do_file = false; - } - return true; -} - -/// Perform generic (not command-specific) expansions on the specified string. -void completer_t::complete_param_expand(const wcstring &str, bool do_file, - bool handle_as_special_cd) { - if (ctx.check_cancel()) return; - expand_flags_t flags = this->expand_flags() | expand_flag::skip_cmdsubst | - expand_flag::for_completions | expand_flag::preserve_home_tildes; - - if (!do_file) flags |= expand_flag::skip_wildcards; - - if (handle_as_special_cd && do_file) { - if (this->flags.autosuggestion) { - flags |= expand_flag::special_for_cd_autosuggestion; - } - flags |= expand_flag::directories_only; - flags |= expand_flag::special_for_cd; - } - - // Squelch file descriptions per issue #254. - if (this->flags.autosuggestion || do_file) flags.clear(expand_flag::gen_descriptions); - - // We have the following cases: - // - // --foo=bar => expand just bar - // -foo=bar => expand just bar - // foo=bar => expand the whole thing, and also just bar - // - // We also support colon separator (#2178). If there's more than one, prefer the last one. - size_t sep_index = str.find_last_of(L"=:"); - bool complete_from_separator = (sep_index != wcstring::npos); - bool complete_from_start = !complete_from_separator || !string_prefixes_string(L"-", str); - - if (complete_from_separator) { - // FIXME: This just cuts the token, - // so any quoting or braces gets lost. - // See #4954. - const wcstring sep_string = wcstring(str, sep_index + 1); - completion_list_t local_completions; - if (expand_string(sep_string, &local_completions, flags, ctx) == expand_result_t::error) { - FLOGF(complete, L"Error while expanding string '%ls'", sep_string.c_str()); - } - - // Any COMPLETE_REPLACES_TOKEN will also stomp the separator. We need to "repair" them by - // inserting our separator and prefix. - const wcstring prefix_with_sep = wcstring(str, 0, sep_index + 1); - for (completion_t &comp : local_completions) { - comp.prepend_token_prefix(prefix_with_sep); - } - if (!this->completions.add_list(std::move(local_completions))) { - return; - } - } - - if (complete_from_start) { - // Don't do fuzzy matching for files if the string begins with a dash (issue #568). We could - // consider relaxing this if there was a preceding double-dash argument. - if (string_prefixes_string(L"-", str)) flags.clear(expand_flag::fuzzy_match); - - if (expand_string(str, &this->completions, flags, ctx) == expand_result_t::error) { - FLOGF(complete, L"Error while expanding string '%ls'", str.c_str()); - } - } -} - -/// Complete the specified string as an environment variable. -/// \return true if this was a variable, so we should stop completion. -bool completer_t::complete_variable(const wcstring &str, size_t start_offset) { - const wchar_t *const whole_var = str.c_str(); - const wchar_t *var = &whole_var[start_offset]; - size_t varlen = str.length() - start_offset; - bool res = false; - - for (const wcstring &env_name : ctx.vars.get_names(0)) { - bool anchor_start = !this->flags.fuzzy_match; - maybe_t match = - string_fuzzy_match_string(var, env_name, anchor_start); - if (!match) continue; - - wcstring comp; - complete_flags_t flags = 0; - - if (!match->requires_full_replacement()) { - // Take only the suffix. - comp.append(env_name.c_str() + varlen); - } else { - comp.append(whole_var, start_offset); - comp.append(env_name); - flags = COMPLETE_REPLACES_TOKEN | COMPLETE_DONT_ESCAPE; - } - - wcstring desc; - if (this->flags.descriptions) { - if (this->flags.autosuggestion) { - // $history can be huge, don't put all of it in the completion description; see - // #6288. - if (env_name == L"history") { - std::shared_ptr history = - history_t::with_name(history_session_id(ctx.vars)); - for (size_t i = 1; i < history->size() && desc.size() < 64; i++) { - if (i > 1) desc += L' '; - desc += expand_escape_string(history->item_at_index(i).str()); - } - } else { - // Can't use ctx.vars here, it could be any variable. - auto var = ctx.vars.get(env_name); - if (!var) continue; - - wcstring value = expand_escape_variable(*var); - desc = format_string(COMPLETE_VAR_DESC_VAL, value.c_str()); - } - } - } - - // Append matching environment variables - // TODO: need to propagate overflow here. - ignore_result(this->completions.add(std::move(comp), std::move(desc), flags, *match)); - - res = true; - } - - return res; -} - -bool completer_t::try_complete_variable(const wcstring &str) { - enum { e_unquoted, e_single_quoted, e_double_quoted } mode = e_unquoted; - const size_t len = str.size(); - - // Get the position of the dollar heading a (possibly empty) run of valid variable characters. - // npos means none. - size_t variable_start = wcstring::npos; - - for (size_t in_pos = 0; in_pos < len; in_pos++) { - wchar_t c = str.at(in_pos); - if (!valid_var_name_char(c)) { - // This character cannot be in a variable, reset the dollar. - variable_start = wcstring::npos; - } - - switch (c) { - case L'\\': { - in_pos++; - break; - } - case L'$': { - if (mode == e_unquoted || mode == e_double_quoted) { - variable_start = in_pos; - } - break; - } - case L'\'': { - if (mode == e_single_quoted) { - mode = e_unquoted; - } else if (mode == e_unquoted) { - mode = e_single_quoted; - } - break; - } - case L'"': { - if (mode == e_double_quoted) { - mode = e_unquoted; - } else if (mode == e_unquoted) { - mode = e_double_quoted; - } - break; - } - default: { - break; // all other chars ignored here - } - } - } - - // Now complete if we have a variable start. Note the variable text may be empty; in that case - // don't generate an autosuggestion, but do allow tab completion. - bool allow_empty = !this->flags.autosuggestion; - bool text_is_empty = (variable_start == len - 1); - bool result = false; - if (variable_start != wcstring::npos && (allow_empty || !text_is_empty)) { - result = this->complete_variable(str, variable_start + 1); - } - return result; -} - -/// Try to complete the specified string as a username. This is used by ~USER type expansion. -/// -/// \return false if unable to complete, true otherwise -bool completer_t::try_complete_user(const wcstring &str) { -#ifndef HAVE_GETPWENT - // The getpwent() function does not exist on Android. A Linux user on Android isn't - // really a user - each installed app gets an UID assigned. Listing all UID:s is not - // possible without root access, and doing a ~USER type expansion does not make sense - // since every app is sandboxed and can't access eachother. - return false; -#else - const wchar_t *cmd = str.c_str(); - const wchar_t *first_char = cmd; - - if (*first_char != L'~' || std::wcschr(first_char, L'/')) return false; - - const wchar_t *user_name = first_char + 1; - const wchar_t *name_end = std::wcschr(user_name, L'~'); - if (name_end) return false; - - double start_time = timef(); - bool result = false; - size_t name_len = str.length() - 1; - - static std::mutex s_setpwent_lock; - scoped_lock locker(s_setpwent_lock); - setpwent(); - // cppcheck-suppress getpwentCalled - while (struct passwd *pw = getpwent()) { - if (ctx.check_cancel()) { - break; - } - const wcstring pw_name_str = str2wcstring(pw->pw_name); - const wchar_t *pw_name = pw_name_str.c_str(); - if (std::wcsncmp(user_name, pw_name, name_len) == 0) { - wcstring desc = format_string(COMPLETE_USER_DESC, pw_name); - // Append a user name. - // TODO: propagate overflow? - ignore_result( - this->completions.add(&pw_name[name_len], std::move(desc), COMPLETE_NO_SPACE)); - result = true; - } else if (wcsncasecmp(user_name, pw_name, name_len) == 0) { - wcstring name = format_string(L"~%ls", pw_name); - wcstring desc = format_string(COMPLETE_USER_DESC, pw_name); - // Append a user name - ignore_result(this->completions.add( - std::move(name), std::move(desc), - COMPLETE_REPLACES_TOKEN | COMPLETE_DONT_ESCAPE | COMPLETE_NO_SPACE)); - result = true; - } - - // If we've spent too much time (more than 200 ms) doing this give up. - if (timef() - start_time > 0.2) break; - } - - endpwent(); - return result; -#endif -} - -// If we have variable assignments, attempt to apply them in our parser. As soon as the return -// value goes out of scope, the variables will be removed from the parser. -cleanup_t completer_t::apply_var_assignments(const std::vector &var_assignments) { - if (!ctx.parser || var_assignments.empty()) return cleanup_t{[] {}}; - env_stack_t &vars = ctx.parser->vars(); - assert(&vars == &ctx.vars && - "Don't know how to tab complete with a parser but a different variable set"); - - // clone of parse_execution_context_t::apply_variable_assignments. - // Crucially do NOT expand subcommands: - // VAR=(launch_missiles) cmd - // should not launch missiles. - // Note we also do NOT send --on-variable events. - const expand_flags_t expand_flags = expand_flag::skip_cmdsubst; - const block_t *block = ctx.parser->push_block(block_t::variable_assignment_block()); - for (const wcstring &var_assign : var_assignments) { - auto equals_pos = variable_assignment_equals_pos(var_assign); - assert(equals_pos && "All variable assignments should have equals position"); - const wcstring variable_name = var_assign.substr(0, *equals_pos); - const wcstring expression = var_assign.substr(*equals_pos + 1); - - completion_list_t expression_expanded; - auto expand_ret = expand_string(expression, &expression_expanded, expand_flags, ctx); - // If expansion succeeds, set the value; if it fails (e.g. it has a cmdsub) set an empty - // value anyways. - std::vector vals; - if (expand_ret == expand_result_t::ok) { - for (auto &completion : expression_expanded) { - vals.emplace_back(std::move(completion.completion)); - } - } - ctx.parser->vars().set(variable_name, ENV_LOCAL | ENV_EXPORT, std::move(vals)); - if (ctx.check_cancel()) break; - } - return cleanup_t([=] { ctx.parser->pop_block(block); }); -} - -// Complete a command by invoking user-specified completions. -void completer_t::complete_custom(const wcstring &cmd, const wcstring &cmdline, - custom_arg_data_t *ad) { - if (ctx.check_cancel()) return; - - bool is_autosuggest = this->flags.autosuggestion; - // Perhaps set a transient commandline so that custom completions - // builtin_commandline will refer to the wrapped command. But not if - // we're doing autosuggestions. - maybe_t remove_transient{}; - bool wants_transient = (ad->wrap_depth > 0 || ad->var_assignments) && !is_autosuggest; - if (wants_transient) { - ctx.parser->libdata().transient_commandlines.push_back(cmdline); - remove_transient.emplace([=] { ctx.parser->libdata().transient_commandlines.pop_back(); }); - } - - // Maybe apply variable assignments. - cleanup_t restore_vars{apply_var_assignments(*ad->var_assignments)}; - if (ctx.check_cancel()) return; - - // Invoke any custom completions for this command. - (void)complete_param_for_command(cmd, ad->previous_argument, ad->current_argument, - !ad->had_ddash, &ad->do_file); -} - -static bool expand_command_token(const operation_context_t &ctx, wcstring &cmd_tok) { - // TODO: we give up if the first token expands to more than one argument. We could handle - // that case by propagating arguments. - // Also we could expand wildcards. - return expand_one(cmd_tok, {expand_flag::skip_cmdsubst, expand_flag::skip_wildcards}, ctx, - nullptr); -} - -// Invoke command-specific completions given by \p arg_data. -// Then, for each target wrapped by the given command, update the command -// line with that target and invoke this recursively. -// The command whose completions to use is given by \p cmd. The full command line is given by \p -// cmdline and the command's range in it is given by \p cmdrange. Note: the command range -// may have a different length than the command itself, because the command is unescaped (i.e. -// quotes removed). -void completer_t::walk_wrap_chain(const wcstring &cmd, const wcstring &cmdline, - source_range_t cmdrange, custom_arg_data_t *ad) { - // Limit our recursion depth. This prevents cycles in the wrap chain graph from overflowing. - if (ad->wrap_depth > 24) return; - if (ctx.cancel_checker()) return; - - // Extract command from the command line and invoke the receiver with it. - complete_custom(cmd, cmdline, ad); - - std::vector targets = complete_get_wrap_targets(cmd); - scoped_push saved_depth(&ad->wrap_depth, ad->wrap_depth + 1); - - for (const wcstring &wt : targets) { - // We may append to the variable assignment list; ensure we restore it. - const size_t saved_var_count = ad->var_assignments->size(); - cleanup_t restore_vars([=] { - assert(ad->var_assignments->size() >= saved_var_count && - "Should not delete var assignments"); - ad->var_assignments->resize(saved_var_count); - }); - - // Separate the wrap target into any variable assignments VAR=... and the command itself. - wcstring wrapped_command; - auto tokenizer = new_tokenizer(wt.c_str(), 0); - size_t wrapped_command_offset_in_wt = wcstring::npos; - while (auto tok = tokenizer->next()) { - wcstring tok_src = *tok->get_source(wt); - if (variable_assignment_equals_pos(tok_src)) { - ad->var_assignments->push_back(std::move(tok_src)); - } else { - wrapped_command_offset_in_wt = tok->offset; - wrapped_command = std::move(tok_src); - expand_command_token(ctx, wrapped_command); - break; - } - } - - // Skip this wrapped command if empty, or if we've seen it before. - if (wrapped_command.empty() || - !ad->visited_wrapped_commands.insert(wrapped_command).second) { - continue; - } - - // Construct a fake command line containing the wrap target. - wcstring faux_commandline = cmdline; - faux_commandline.replace(cmdrange.start, cmdrange.length, wt); - - // Recurse with our new command and command line. - source_range_t faux_source_range{uint32_t(cmdrange.start + wrapped_command_offset_in_wt), - uint32_t(wrapped_command.size())}; - walk_wrap_chain(wrapped_command, faux_commandline, faux_source_range, ad); - } -} - -/// If the argument contains a '[' typed by the user, completion by appending to the argument might -/// produce an invalid token (#5831). -/// -/// Check if there is any unescaped, unquoted '['; if yes, make the completions replace the entire -/// argument instead of appending, so '[' will be escaped. -void completer_t::escape_opening_brackets(const wcstring &argument) { - bool have_unquoted_unescaped_bracket = false; - wchar_t quote = L'\0'; - bool escaped = false; - for (wchar_t c : argument) { - have_unquoted_unescaped_bracket |= (c == L'[') && !quote && !escaped; - if (escaped) { - escaped = false; - } else if (c == L'\\') { - escaped = true; - } else if (c == L'\'' || c == L'"') { - if (quote == c) { - // Closing a quote. - quote = L'\0'; - } else if (quote == L'\0') { - // Opening a quote. - quote = c; - } - } - } - if (!have_unquoted_unescaped_bracket) return; - // Since completion_apply_to_command_line will escape the completion, we need to provide an - // unescaped version. - auto unescaped_argument = unescape_string(argument, UNESCAPE_INCOMPLETE); - if (!unescaped_argument) return; - for (completion_t &comp : completions.get_list()) { - if (comp.flags & COMPLETE_REPLACES_TOKEN) continue; - comp.flags |= COMPLETE_REPLACES_TOKEN; - comp.flags |= COMPLETE_DONT_ESCAPE_TILDES; // See #9073. - // We are grafting a completion that is expected to be escaped later. This will break - // if the original completion doesn't want escaping. Happily, this is only the case - // for username completion and variable name completion. They shouldn't end up here - // anyway because they won't contain '['. - if (comp.flags & COMPLETE_DONT_ESCAPE) { - FLOG(warning, L"unexpected completion flag"); - } - comp.completion = *unescaped_argument + comp.completion; - } -} - -/// Set the DUPLICATES_ARG flag in any completion that duplicates an argument. -void completer_t::mark_completions_duplicating_arguments(const wcstring &cmd, - const wcstring &prefix, - const std::vector &args) { - // Get all the arguments, unescaped, into an array that we're going to bsearch. - std::vector arg_strs; - for (const auto &arg : args) { - wcstring argstr = *arg.get_source(cmd); - if (auto argstr_unesc = unescape_string(argstr, UNESCAPE_DEFAULT)) { - arg_strs.push_back(std::move(*argstr_unesc)); - } - } - std::sort(arg_strs.begin(), arg_strs.end()); - - wcstring comp_str; - for (completion_t &comp : completions.get_list()) { - comp_str = comp.completion; - if (!(comp.flags & COMPLETE_REPLACES_TOKEN)) { - comp_str.insert(0, prefix); - } - if (std::binary_search(arg_strs.begin(), arg_strs.end(), comp_str)) { - comp.flags |= COMPLETE_DUPLICATES_ARGUMENT; - } - } -} - -void completer_t::perform_for_commandline(wcstring cmdline) { - // Limit recursion, in case a user-defined completion has cycles, or the completion for "x" - // wraps "A=B x" (#3474, #7344). No need to do that when there is no parser: this happens only - // for autosuggestions where we don't evaluate command substitutions or variable assignments. - if (ctx.parser) { - if (ctx.parser->libdata().complete_recursion_level >= 24) { - FLOGF(error, _(L"completion reached maximum recursion depth, possible cycle?"), - cmdline.c_str()); - return; - } - ++ctx.parser->libdata().complete_recursion_level; - }; - cleanup_t decrement{[this]() { - if (ctx.parser) --ctx.parser->libdata().complete_recursion_level; - }}; - - const size_t cursor_pos = cmdline.size(); - const bool is_autosuggest = flags.autosuggestion; - - // Find the process to operate on. The cursor may be past it (#1261), so backtrack - // until we know we're no longer in a space. But the space may actually be part of the - // argument (#2477). - size_t position_in_statement = cursor_pos; - while (position_in_statement > 0 && cmdline.at(position_in_statement - 1) == L' ') { - position_in_statement--; - } - - // Get all the arguments. - std::vector tokens; - parse_util_process_extent(cmdline.c_str(), position_in_statement, nullptr, nullptr, &tokens); - size_t actual_token_count = tokens.size(); - - // Hack: fix autosuggestion by removing prefixing "and"s #6249. - if (is_autosuggest) { - tokens.erase( - std::remove_if(tokens.begin(), tokens.end(), - [&cmdline](const tok_t &token) { - return parser_keywords_is_subcommand(*token.get_source(cmdline)); - }), - tokens.end()); - } - - // Consume variable assignments in tokens strictly before the cursor. - // This is a list of (escaped) strings of the form VAR=VAL. - std::vector var_assignments; - for (const tok_t &tok : tokens) { - if (tok.location_in_or_at_end_of_source_range(cursor_pos)) break; - wcstring tok_src = *tok.get_source(cmdline); - if (!variable_assignment_equals_pos(tok_src)) break; - var_assignments.push_back(std::move(tok_src)); - } - tokens.erase(tokens.begin(), tokens.begin() + var_assignments.size()); - - // Empty process (cursor is after one of ;, &, |, \n, &&, || modulo whitespace). - if (tokens.empty()) { - // Don't autosuggest anything based on the empty string (generalizes #1631). - if (is_autosuggest) return; - - complete_cmd(L""); - complete_abbr(L""); - return; - } - - wcstring *effective_cmdline, effective_cmdline_buf; - if (tokens.size() == actual_token_count) { - effective_cmdline = &cmdline; - } else { - effective_cmdline_buf.assign(cmdline, tokens.front().offset, wcstring::npos); - effective_cmdline = &effective_cmdline_buf; - } - - if (tokens.back().type_ == token_type_t::comment) { - return; - } - tokens.erase( - std::remove_if(tokens.begin(), tokens.end(), - [](const tok_t &tok) { return tok.type_ == token_type_t::comment; }), - tokens.end()); - assert(!tokens.empty()); - - const tok_t &cmd_tok = tokens.front(); - const tok_t &cur_tok = tokens.back(); - - // Since fish does not currently support redirect in command position, we return here. - if (cmd_tok.type_ != token_type_t::string) return; - if (cur_tok.type_ == token_type_t::error) return; - for (const auto &tok : tokens) { // If there was an error, it was in the last token. - assert(tok.type_ == token_type_t::string || tok.type_ == token_type_t::redirect); - } - // If we are completing a variable name or a tilde expansion user name, we do that and - // return. No need for any other completions. - const wcstring current_token = *cur_tok.get_source(cmdline); - if (cur_tok.location_in_or_at_end_of_source_range(cursor_pos)) { - if (try_complete_variable(current_token) || try_complete_user(current_token)) { - return; - } - } - - if (cmd_tok.location_in_or_at_end_of_source_range(cursor_pos)) { - auto equal_sign_pos = variable_assignment_equals_pos(current_token); - if (equal_sign_pos) { - complete_param_expand(current_token, true /* do_file */); - return; - } - // Complete command filename. - complete_cmd(current_token); - complete_abbr(current_token); - return; - } - // See whether we are in an argument, in a redirection or in the whitespace in between. - bool in_redirection = cur_tok.type_ == token_type_t::redirect; - - bool had_ddash = false; - wcstring current_argument, previous_argument; - if (cur_tok.type_ == token_type_t::string && - cur_tok.location_in_or_at_end_of_source_range(position_in_statement)) { - // If the cursor is in whitespace, then the "current" argument is empty and the - // previous argument is the matching one. But if the cursor was in or at the end - // of the argument, then the current argument is the matching one, and the - // previous argument is the one before it. - bool cursor_in_whitespace = !cur_tok.location_in_or_at_end_of_source_range(cursor_pos); - if (cursor_in_whitespace) { - current_argument.clear(); - previous_argument = current_token; - } else { - current_argument = current_token; - if (tokens.size() >= 2) { - tok_t prev_tok = tokens.at(tokens.size() - 2); - if (prev_tok.type_ == token_type_t::string) - previous_argument = *prev_tok.get_source(cmdline); - in_redirection = prev_tok.type_ == token_type_t::redirect; - } - } - - // Check to see if we have a preceding double-dash. - for (size_t i = 0; i < tokens.size() - 1; i++) { - if (*tokens.at(i).get_source(cmdline) == L"--") { - had_ddash = true; - break; - } - } - } - - bool do_file = false, handle_as_special_cd = false; - if (in_redirection) { - do_file = true; - } else { - // Try completing as an argument. - custom_arg_data_t arg_data{&var_assignments}; - arg_data.had_ddash = had_ddash; - - source_offset_t bias = cmdline.size() - effective_cmdline->size(); - source_range_t command_range = {cmd_tok.offset - bias, cmd_tok.length}; - - wcstring exp_command = *cmd_tok.get_source(cmdline); - std::unique_ptr prev; - std::unique_ptr cur; - bool unescaped = expand_command_token(ctx, exp_command) && - (prev = unescape_string(previous_argument, UNESCAPE_DEFAULT)) && - (cur = unescape_string(current_argument, UNESCAPE_INCOMPLETE)); - if (unescaped) { - arg_data.previous_argument = *prev; - arg_data.current_argument = *cur; - // Have to walk over the command and its entire wrap chain. If any command - // disables do_file, then they all do. - walk_wrap_chain(exp_command, *effective_cmdline, command_range, &arg_data); - do_file = arg_data.do_file; - - // If we're autosuggesting, and the token is empty, don't do file suggestions. - if (is_autosuggest && arg_data.current_argument.empty()) { - do_file = false; - } - } - - // Hack. If we're cd, handle it specially (issue #1059, others). - handle_as_special_cd = - (exp_command == L"cd") || arg_data.visited_wrapped_commands.count(L"cd"); - } - - // Maybe apply variable assignments. - cleanup_t restore_vars{apply_var_assignments(var_assignments)}; - if (ctx.check_cancel()) return; - - // This function wants the unescaped string. - complete_param_expand(current_argument, do_file, handle_as_special_cd); - - // Escape '[' in the argument before completing it. - escape_opening_brackets(current_argument); - - // Lastly mark any completions that appear to already be present in arguments. - mark_completions_duplicating_arguments(cmdline, current_token, tokens); -} - -} // namespace - -/// Create a new completion entry. -void append_completion(completion_list_t *completions, wcstring comp, wcstring desc, - complete_flags_t flags, string_fuzzy_match_t match) { - completions->emplace_back(std::move(comp), std::move(desc), match, flags); -} - -void complete_add(const wcstring &cmd, bool cmd_is_path, const wcstring &option, - complete_option_type_t option_type, completion_mode_t result_mode, - std::vector condition, const wchar_t *comp, const wchar_t *desc, - complete_flags_t flags) { - // option should be empty iff the option type is arguments only. - assert(option.empty() == (option_type == option_type_args_only)); - - // Lock the lock that allows us to edit the completion entry list. - auto completion_map = s_completion_map.acquire(); - completion_entry_t &c = (*completion_map)[std::make_pair(cmd, cmd_is_path)]; - - // Create our new option. - complete_entry_opt_t opt; - opt.option = option; - opt.type = option_type; - opt.result_mode = result_mode; - - if (comp) opt.comp = comp; - opt.conditions = std::move(condition); - if (desc) opt.desc = desc; - opt.flags = flags; - - c.add_option(std::move(opt)); -} - -void complete_remove(const wcstring &cmd, bool cmd_is_path, const wcstring &option, - complete_option_type_t type) { - auto completion_map = s_completion_map.acquire(); - auto iter = completion_map->find(std::make_pair(cmd, cmd_is_path)); - if (iter != completion_map->end()) { - bool delete_it = iter->second.remove_option(option, type); - if (delete_it) { - completion_map->erase(iter); - } - } -} - -void complete_remove_all(const wcstring &cmd, bool cmd_is_path) { - auto completion_map = s_completion_map.acquire(); - completion_map->erase(std::make_pair(cmd, cmd_is_path)); -} - -completion_list_t complete(const wcstring &cmd_with_subcmds, completion_request_options_t flags, - const operation_context_t &ctx, std::vector *out_needs_loads) { - // Determine the innermost subcommand. - const wchar_t *cmdsubst_begin, *cmdsubst_end; - parse_util_cmdsubst_extent(cmd_with_subcmds.c_str(), cmd_with_subcmds.size(), &cmdsubst_begin, - &cmdsubst_end); - assert(cmdsubst_begin != nullptr && cmdsubst_end != nullptr && cmdsubst_end >= cmdsubst_begin); - wcstring cmd = wcstring(cmdsubst_begin, cmdsubst_end - cmdsubst_begin); - completer_t completer(ctx, flags); - completer.perform_for_commandline(std::move(cmd)); - if (out_needs_loads) { - *out_needs_loads = completer.acquire_needs_load(); - } - return completer.acquire_completions(); -} - -/// Print the short switch \c opt, and the argument \c arg to the specified -/// wcstring, but only if \c argument isn't an empty string. -static void append_switch(wcstring &out, wchar_t opt, const wcstring &arg) { - if (arg.empty()) return; - append_format(out, L" -%lc %ls", opt, escape_string(arg).c_str()); -} -static void append_switch(wcstring &out, const wcstring &opt, const wcstring &arg) { - if (arg.empty()) return; - append_format(out, L" --%ls %ls", opt.c_str(), escape_string(arg).c_str()); -} -static void append_switch(wcstring &out, wchar_t opt) { append_format(out, L" -%lc", opt); } -static void append_switch(wcstring &out, const wcstring &opt) { - append_format(out, L" --%ls", opt.c_str()); -} - -static wcstring completion2string(const completion_key_t &key, const complete_entry_opt_t &o) { - const wcstring &cmd = key.first; - bool is_path = key.second; - wcstring out = L"complete"; - - if (o.flags & COMPLETE_DONT_SORT) append_switch(out, L'k'); - - if (o.result_mode.no_files && o.result_mode.requires_param) { - append_switch(out, L"exclusive"); - } else if (o.result_mode.no_files) { - append_switch(out, L"no-files"); - } else if (o.result_mode.force_files) { - append_switch(out, L"force-files"); - } else if (o.result_mode.requires_param) { - append_switch(out, L"require-parameter"); - } - - if (is_path) - append_switch(out, L'p', cmd); - else { - out.append(L" "); - out.append(escape_string(cmd)); - } - - switch (o.type) { - case option_type_args_only: { - break; - } - case option_type_short: { - append_switch(out, L's', wcstring(1, o.option.at(0))); - break; - } - case option_type_single_long: - case option_type_double_long: { - append_switch(out, o.type == option_type_single_long ? L'o' : L'l', o.option); - break; - } - } - - append_switch(out, L'd', C_(o.desc)); - append_switch(out, L'a', o.comp); - for (const auto &c : o.conditions) { - append_switch(out, L'n', c); - } - out.append(L"\n"); - return out; -} - -bool complete_load(const wcstring &cmd, parser_t &parser) { - bool loaded_new = false; - - // We have to load this as a function, since it may define a --wraps or signature. - // See issue #2466. - if (function_load(cmd, parser)) { - // We autoloaded something; check if we have a --wraps. - loaded_new |= complete_get_wrap_targets(cmd).size() > 0; - } - - // It's important to NOT hold the lock around completion loading. - // We need to take the lock to decide what to load, drop it to perform the load, then reacquire - // it. - // Note we only look at the global fish_function_path and fish_complete_path. - maybe_t path_to_load = - completion_autoloader.acquire()->resolve_command(cmd, env_stack_t::globals()); - if (path_to_load) { - autoload_t::perform_autoload(*path_to_load, parser); - completion_autoloader.acquire()->mark_autoload_finished(cmd); - loaded_new = true; - } - return loaded_new; -} - -/// Use by the bare `complete`, loaded completions are printed out as commands -wcstring complete_print(const wcstring &cmd) { - wcstring out; - - // Get references to our completions and sort them by order. - auto completions = s_completion_map.acquire(); - using comp_ref_t = std::reference_wrapper; - std::vector completion_refs(completions->begin(), completions->end()); - std::sort(completion_refs.begin(), completion_refs.end(), [](comp_ref_t a, comp_ref_t b) { - return a.get().second.order < b.get().second.order; - }); - - for (const comp_ref_t &cr : completion_refs) { - const completion_key_t &key = cr.get().first; - const completion_entry_t &entry = cr.get().second; - if (!cmd.empty() && key.first != cmd) continue; - const option_list_t &options = entry.get_options(); - // Output in reverse order to preserve legacy behavior (see #9221). - for (auto o = options.rbegin(); o != options.rend(); ++o) { - out.append(completion2string(key, *o)); - } - } - - // Append wraps. - auto locked_wrappers = wrapper_map.acquire(); - for (const auto &entry : *locked_wrappers) { - const wcstring &src = entry.first; - if (!cmd.empty() && src != cmd) continue; - for (const wcstring &target : entry.second) { - out.append(L"complete "); - out.append(escape_string(src)); - append_switch(out, L"wraps", target); - out.append(L"\n"); - } - } - return out; -} - -void complete_invalidate_path() { - // TODO: here we unload all completions for commands that are loaded by the autoloader. We also - // unload any completions that the user may specified on the command line. We should in - // principle track those completions loaded by the autoloader alone. - std::vector cmds = completion_autoloader.acquire()->get_autoloaded_commands(); - for (const wcstring &cmd : cmds) { - complete_remove_all(cmd, false /* not a path */); - } -} - -/// Add a new target that wraps a command. Example: __fish_XYZ (function) wraps XYZ (target). -bool complete_add_wrapper(const wcstring &command, const wcstring &new_target) { - if (command.empty() || new_target.empty()) { - return false; - } - - // If the command and the target are the same, - // there's no point in following the wrap-chain because we'd only complete the same thing. - // TODO: This should maybe include full cycle detection. - if (command == new_target) return false; - - auto locked_map = wrapper_map.acquire(); - wrapper_map_t &wraps = *locked_map; - std::vector *targets = &wraps[command]; - // If it's already present, we do nothing. - if (!contains(*targets, new_target)) { - targets->push_back(new_target); - } - return true; -} - -bool complete_remove_wrapper(const wcstring &command, const wcstring &target_to_remove) { - if (command.empty() || target_to_remove.empty()) { - return false; - } - - auto locked_map = wrapper_map.acquire(); - wrapper_map_t &wraps = *locked_map; - bool result = false; - auto current_targets_iter = wraps.find(command); - if (current_targets_iter != wraps.end()) { - std::vector *targets = ¤t_targets_iter->second; - auto where = std::find(targets->begin(), targets->end(), target_to_remove); - if (where != targets->end()) { - targets->erase(where); - result = true; - } - } - return result; -} - -std::vector complete_get_wrap_targets(const wcstring &command) { - if (command.empty()) { - return {}; - } - auto locked_map = wrapper_map.acquire(); - wrapper_map_t &wraps = *locked_map; - auto iter = wraps.find(command); - if (iter == wraps.end()) return {}; - return iter->second; -} - -wcstring_list_ffi_t complete_get_wrap_targets_ffi(const wcstring &command) { - return complete_get_wrap_targets(command); -} diff --git a/src/complete.h b/src/complete.h index ccc8782bd..4b871f6f2 100644 --- a/src/complete.h +++ b/src/complete.h @@ -13,8 +13,9 @@ #include #include -// #include "expand.h" #include "common.h" +#include "expand.h" +#include "parser.h" #include "wcstringutil.h" struct completion_mode_t { @@ -29,8 +30,6 @@ struct completion_mode_t { /// Character that separates the completion and description on programmable completions. #define PROG_COMPLETE_SEP L'\t' -class Parser; using parser_t = Parser; - enum { /// Do not insert space afterwards if this is the only completion. (The default is to try insert /// a space). @@ -53,240 +52,16 @@ enum { }; using complete_flags_t = uint8_t; -/// std::function which accepts a completion string and returns its description. -using description_func_t = std::function; +#if INCLUDE_RUST_HEADERS +#include "complete.rs.h" +#else +struct CompletionListFfi; +struct Completion; +struct CompletionRequestOptions; +#endif -/// Helper to return a description_func_t for a constant string. -description_func_t const_desc(const wcstring &s); - -/// This is an individual completion entry, i.e. the result of an expansion of a completion rule. -class completion_t { - private: - // No public default constructor. - completion_t(); - - public: - // Destructor. Not inlining it saves code size. - ~completion_t(); - - /// The completion string. - wcstring completion; - /// The description for this completion. - wcstring description; - /// The type of fuzzy match. - string_fuzzy_match_t match; - /// Flags determining the completion behavior. - complete_flags_t flags; - - // Construction. - explicit completion_t(wcstring comp, wcstring desc = wcstring(), - string_fuzzy_match_t match = string_fuzzy_match_t::exact_match(), - complete_flags_t flags_val = 0); - completion_t(const completion_t &); - completion_t &operator=(const completion_t &); - - // noexcepts are required for push_back to use the move ctor. - completion_t(completion_t &&) noexcept; - completion_t &operator=(completion_t &&) noexcept; - - /// \return whether this replaces its token. - bool replaces_token() const { return flags & COMPLETE_REPLACES_TOKEN; } - - /// \return whether this replaces the entire commandline. - bool replaces_commandline() const { return flags & COMPLETE_REPLACES_COMMANDLINE; } - - /// \return the completion's match rank. Lower ranks are better completions. - uint32_t rank() const { return match.rank(); } - - // If this completion replaces the entire token, prepend a prefix. Otherwise do nothing. - void prepend_token_prefix(const wcstring &prefix); -}; - -using completion_list_t = std::vector; - -struct completion_request_options_t { - bool autosuggestion{}; // requesting autosuggestion - bool descriptions{}; // make descriptions - bool fuzzy_match{}; // if set, we do not require a prefix match - - // Options for an autosuggestion. - static completion_request_options_t autosuggest() { - completion_request_options_t res{}; - res.autosuggestion = true; - res.descriptions = false; - res.fuzzy_match = false; - return res; - } - - // Options for a "normal" completion. - static completion_request_options_t normal() { - completion_request_options_t res{}; - res.autosuggestion = false; - res.descriptions = true; - res.fuzzy_match = true; - return res; - } -}; - -using completion_list_t = std::vector; - -/// A completion receiver accepts completions. It is essentially a wrapper around std::vector with -/// some conveniences. -class completion_receiver_t { - public: - /// Construct as empty, with a limit. - explicit completion_receiver_t(size_t limit) : limit_(limit) {} - - /// Acquire an existing list, with a limit. - explicit completion_receiver_t(completion_list_t &&v, size_t limit) - : completions_(std::move(v)), limit_(limit) {} - - /// Add a completion. - /// \return true on success, false if this would overflow the limit. - __warn_unused bool add(completion_t &&comp); - - /// Add a completion with the given string, and default other properties. - /// \return true on success, false if this would overflow the limit. - __warn_unused bool add(wcstring &&comp); - - /// Add a completion with the given string, description, flags, and fuzzy match. - /// \return true on success, false if this would overflow the limit. - /// The 'desc' parameter is not && because if gettext is not enabled, then we end - /// up passing a 'const wcstring &' here. - __warn_unused bool add(wcstring &&comp, wcstring desc, complete_flags_t flags = 0, - string_fuzzy_match_t match = string_fuzzy_match_t::exact_match()); - - /// Add a list of completions. - /// \return true on success, false if this would overflow the limit. - __warn_unused bool add_list(completion_list_t &&lst); - - /// Swap our completions with a new list. - void swap(completion_list_t &lst) { std::swap(completions_, lst); } - - /// Clear the list of completions. This retains the storage inside completions_ which can be - /// useful to prevent allocations. - void clear() { completions_.clear(); } - - /// \return whether our completion list is empty. - bool empty() const { return completions_.empty(); } - - /// \return how many completions we have stored. - size_t size() const { return completions_.size(); } - - /// \return a completion at an index. - completion_t &at(size_t idx) { return completions_.at(idx); } - const completion_t &at(size_t idx) const { return completions_.at(idx); } - - /// \return the list of completions. Do not modify the size of the list via this function, as it - /// may exceed our completion limit. - const completion_list_t &get_list() const { return completions_; } - completion_list_t &get_list() { return completions_; } - - /// \return the list of completions, clearing it. - completion_list_t take(); - - /// \return a new, empty receiver whose limit is our remaining capacity. - /// This is useful for e.g. recursive calls when you want to act on the result before adding it. - completion_receiver_t subreceiver() const; - - private: - // Our list of completions. - completion_list_t completions_; - - // The maximum number of completions to add. If our list length exceeds this, then new - // completions are not added. Note 0 has no special significance here - use - // numeric_limits::max() instead. - const size_t limit_; -}; - -enum complete_option_type_t : uint8_t { - option_type_args_only, // no option - option_type_short, // -x - option_type_single_long, // -foo - option_type_double_long // --foo -}; - -/// Sorts and remove any duplicate completions in the completion list, then puts them in priority -/// order. -void completions_sort_and_prioritize(completion_list_t *comps, - completion_request_options_t flags = {}); - -/// Add an unexpanded completion "rule" to generate completions from for a command. -/// -/// Examples: -/// -/// The command 'gcc -o' requires that a file follows it, so the requires_param mode is suitable. -/// This can be done using the following line: -/// -/// complete -c gcc -s o -r -/// -/// The command 'grep -d' required that one of the strings 'read', 'skip' or 'recurse' is used. As -/// such, it is suitable to specify that a completion requires one of them. This can be done using -/// the following line: -/// -/// complete -c grep -s d -x -a "read skip recurse" -/// -/// \param cmd Command to complete. -/// \param cmd_is_path If cmd_is_path is true, cmd will be interpreted as the absolute -/// path of the program (optionally containing wildcards), otherwise it -/// will be interpreted as the command name. -/// \param option The name of an option. -/// \param option_type The type of option: can be option_type_short (-x), -/// option_type_single_long (-foo), option_type_double_long (--bar). -/// \param result_mode Controls how to search further completions when this completion has been -/// successfully matched. -/// \param comp A space separated list of completions which may contain subshells. -/// \param desc A description of the completion. -/// \param condition a command to be run to check it this completion should be used. If \c condition -/// is empty, the completion is always used. -/// \param flags A set of completion flags -void complete_add(const wcstring &cmd, bool cmd_is_path, const wcstring &option, - complete_option_type_t option_type, completion_mode_t result_mode, - std::vector condition, const wchar_t *comp, const wchar_t *desc, - complete_flags_t flags); - -/// Remove a previously defined completion. -void complete_remove(const wcstring &cmd, bool cmd_is_path, const wcstring &option, - complete_option_type_t type); - -/// Removes all completions for a given command. -void complete_remove_all(const wcstring &cmd, bool cmd_is_path); - -/// Load command-specific completions for the specified command. -/// \return true if something new was loaded, false if not. -bool complete_load(const wcstring &cmd, parser_t &parser); - -/// \return all completions of the command cmd. -/// If \p ctx contains a parser, this will autoload functions and completions as needed. -/// If it does not contain a parser, then any completions which need autoloading will be returned in -/// \p needs_load, if not null. -class operation_context_t; -completion_list_t complete(const wcstring &cmd, completion_request_options_t flags, - const operation_context_t &ctx, - std::vector *out_needs_load = nullptr); - -/// Return a list of all current completions. -wcstring complete_print(const wcstring &cmd = L""); - -/// Create a new completion entry. -/// -/// \param completions The array of completions to append to -/// \param comp The completion string -/// \param desc The description of the completion -/// \param flags completion flags -void append_completion(completion_list_t *completions, wcstring comp, wcstring desc = wcstring(), - complete_flags_t flags = 0, - string_fuzzy_match_t match = string_fuzzy_match_t::exact_match()); - -/// Support for "wrap targets." A wrap target is a command that completes like another command. -bool complete_add_wrapper(const wcstring &command, const wcstring &new_target); -bool complete_remove_wrapper(const wcstring &command, const wcstring &target_to_remove); - -/// Returns a list of wrap targets for a given command. -std::vector complete_get_wrap_targets(const wcstring &command); -wcstring_list_ffi_t complete_get_wrap_targets_ffi(const wcstring &command); - -// Observes that fish_complete_path has changed. -void complete_invalidate_path(); +using completion_t = Completion; +using completion_request_options_t = CompletionRequestOptions; +using completion_list_t = CompletionListFfi; #endif diff --git a/src/env.cpp b/src/env.cpp index d2bc06bc7..eda32253d 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -3,40 +3,12 @@ #include "env.h" -#include #include -#include -#include -#include -#include #include -#include -#include -#include -#include -#include -#include - -#include "abbrs.h" -#include "common.h" -#include "env_dispatch.rs.h" -#include "env_universal_common.h" -#include "event.h" -#include "fallback.h" // IWYU pragma: keep -#include "fish_version.h" -#include "flog.h" -#include "global_safety.h" #include "history.h" -#include "input.h" -#include "null_terminated_array.h" #include "path.h" -#include "proc.h" #include "reader.h" -#include "termsize.h" -#include "threads.rs.h" -#include "wcstringutil.h" -#include "wutil.h" // IWYU pragma: keep /// At init, we read all the environment variables from this array. extern char **environ; @@ -136,17 +108,7 @@ std::vector null_environment_t::get_names(env_mode_flags_t flags) cons return std::move(names.vals); } -/// Various things we need to initialize at run-time that don't really fit any of the other init -/// routines. -void misc_init() { - // If stdout is open on a tty ensure stdio is unbuffered. That's because those functions might - // be intermixed with `write()` calls and we need to ensure the writes are not reordered. See - // issue #3748. - if (isatty(STDOUT_FILENO)) { - fflush(stdout); - setvbuf(stdout, nullptr, _IONBF, 0); - } -} +bool env_stack_t::is_principal() const { return impl_->is_principal(); } extern "C" { void env_cpp_init() { @@ -183,34 +145,6 @@ void set_inheriteds_ffi() { } } -bool env_stack_t::is_principal() const { return impl_->is_principal(); } - -std::vector> env_stack_t::universal_sync(bool always) { - event_list_ffi_t result; - impl_->universal_sync(always, result); - return std::move(result.events); -} - -void env_stack_t::apply_inherited_ffi(const function_properties_t &props) { - impl_->apply_inherited_ffi(props); -} - -statuses_t env_stack_t::get_last_statuses() const { - auto statuses_ffi = impl_->get_last_statuses(); - statuses_t res{}; - res.status = statuses_ffi->get_status(); - res.kill_signal = statuses_ffi->get_kill_signal(); - auto &pipestatus = statuses_ffi->get_pipestatus(); - res.pipestatus.assign(pipestatus.begin(), pipestatus.end()); - return res; -} - -int env_stack_t::get_last_status() const { return get_last_statuses().status; } - -void env_stack_t::set_last_statuses(statuses_t s) { - return impl_->set_last_statuses(s.status, s.kill_signal, s.pipestatus); -} - /// Update the PWD variable directory from the result of getcwd(). void env_stack_t::set_pwd_from_getcwd() { impl_->set_pwd_from_getcwd(); } @@ -251,15 +185,6 @@ int env_stack_t::remove(const wcstring &key, int mode) { return static_cast(impl_->remove(key, mode)); } -std::shared_ptr env_stack_t::export_arr() { - // export_array() returns a rust::Box. - // Acquire ownership. - OwningNullTerminatedArrayRefFFI *ptr = impl_->export_array(); - assert(ptr && "Null pointer"); - return std::make_shared( - rust::Box::from_raw(ptr)); -} - maybe_t env_dyn_t::get(const wcstring &key, env_mode_flags_t mode) const { if (auto *ptr = impl_->getf(key, mode)) { return env_var_t::new_ffi(ptr); @@ -302,6 +227,8 @@ const std::shared_ptr &env_stack_t::principal_ref() { env_stack_t::~env_stack_t() = default; env_stack_t::env_stack_t(env_stack_t &&) = default; env_stack_t::env_stack_t(rust::Box imp) : impl_(std::move(imp)) {} +env_stack_t::env_stack_t(uint8_t *imp) + : impl_(rust::Box::from_raw(reinterpret_cast(imp))) {} #if defined(__APPLE__) || defined(__CYGWIN__) static int check_runtime_path(const char *path) { @@ -385,7 +312,7 @@ void unsetenv_lock(const char *name) { wcstring_list_ffi_t get_history_variable_text_ffi(const wcstring &fish_history_val) { wcstring_list_ffi_t out{}; - std::shared_ptr history = commandline_get_state().history; + maybe_t> history = commandline_get_state().history; if (!history) { // Effective duplication of history_session_id(). wcstring session_id{}; @@ -402,18 +329,12 @@ wcstring_list_ffi_t get_history_variable_text_ffi(const wcstring &fish_history_v // Valid session. session_id = fish_history_val; } - history = history_t::with_name(session_id); + history = history_with_name(session_id); } if (history) { - history->get_history(out.vals); + out = *(*history)->get_history(); } return out; } -event_list_ffi_t::event_list_ffi_t() = default; - -void event_list_ffi_t::push(void *event_vp) { - auto event = static_cast(event_vp); - assert(event && "Null event"); - events.push_back(rust::Box::from_raw(event)); -} +const EnvStackRef &env_stack_t::get_impl_ffi() const { return *impl_; } diff --git a/src/env.h b/src/env.h index 83d7a8f8c..193f334de 100644 --- a/src/env.h +++ b/src/env.h @@ -17,17 +17,23 @@ #include "maybe.h" #include "wutil.h" -struct event_list_ffi_t; -struct function_properties_t; - #if INCLUDE_RUST_HEADERS #include "env/env_ffi.rs.h" #else struct EnvVar; struct EnvNull; +struct EnvStack; struct EnvStackRef; +struct EnvDyn; +enum class env_stack_set_result_t : uint8_t; +struct Statuses; #endif +struct event_list_ffi_t; +struct function_properties_t; + +using statuses_t = Statuses; + /// FFI helper for events. struct Event; struct event_list_ffi_t { @@ -44,6 +50,16 @@ struct event_list_ffi_t { struct owning_null_terminated_array_t; +#if INCLUDE_RUST_HEADERS +#include "env/env_ffi.rs.h" +#else +struct EnvVar; +struct EnvNull; +struct EnvStackRef; +#endif + +struct owning_null_terminated_array_t; + extern "C" { extern bool CURSES_INITIALIZED; @@ -53,6 +69,8 @@ extern bool TERM_HAS_XN; extern size_t READ_BYTE_LIMIT; } +struct Event; + // Flags that may be passed as the 'mode' in env_stack_t::set() / environment_t::get(). enum : uint16_t { /// Default mode. Used with `env_stack_t::get()` to indicate the caller doesn't care what scope @@ -84,31 +102,6 @@ using env_mode_flags_t = uint16_t; /// Return values for `env_stack_t::set()`. enum { ENV_OK, ENV_PERM, ENV_SCOPE, ENV_INVALID, ENV_NOT_FOUND }; -/// A collection of status and pipestatus. -struct statuses_t { - /// Status of the last job to exit. - int status{0}; - - /// Signal from the most recent process in the last job that was terminated by a signal. - /// 0 if all processes exited normally. - int kill_signal{0}; - - /// Pipestatus value. - std::vector pipestatus{}; - - /// Return a statuses for a single process status. - static statuses_t just(int s) { - statuses_t result{}; - result.status = s; - result.pipestatus.push_back(s); - return result; - } -}; - -/// Various things we need to initialize at run-time that don't really fit any of the other init -/// routines. -void misc_init(); - /// env_var_t is an immutable value-type data structure representing the value of an environment /// variable. This wraps the EnvVar type from Rust. class env_var_t { @@ -163,7 +156,6 @@ class env_var_t { rust::Box impl_; }; -typedef std::unordered_map var_table_t; /// An environment is read-only access to variable values. class environment_t { @@ -201,7 +193,7 @@ class null_environment_t : public environment_t { /// A mutable environment which allows scopes to be pushed and popped. class env_stack_t final : public environment_t { - friend class Parser; + friend struct Parser; /// \return whether we are the principal stack. bool is_principal() const; @@ -209,6 +201,8 @@ class env_stack_t final : public environment_t { public: ~env_stack_t() override; env_stack_t(env_stack_t &&); + /* implicit */ env_stack_t(rust::Box imp); + /* implicit */ env_stack_t(uint8_t *imp); /// Implementation of environment_t. maybe_t get(const wcstring &key, env_mode_flags_t mode = ENV_DEFAULT) const override; @@ -248,21 +242,12 @@ class env_stack_t final : public environment_t { /// Pop the variable stack. Used for implementing local variables for functions and for-loops. void pop(); - /// Returns an array containing all exported variables in a format suitable for execv. - std::shared_ptr export_arr(); - /// Snapshot this environment. This means returning a read-only copy. Local variables are copied /// but globals are shared (i.e. changes to global will be visible to this snapshot). This /// returns a shared_ptr for convenience, since the most common reason to snapshot is because /// you want to read from another thread. std::shared_ptr snapshot() const; - /// Helpers to get and set the proc statuses. - /// These correspond to $status and $pipestatus. - statuses_t get_last_statuses() const; - int get_last_status() const; - void set_last_statuses(statuses_t s); - /// Sets up argv as the given list of strings. void set_argv(std::vector argv); @@ -275,15 +260,6 @@ class env_stack_t final : public environment_t { return environment_t::get_or_null(key, mode); } - /// Synchronizes universal variable changes. - /// If \p always is set, perform synchronization even if there's no pending changes from this - /// instance (that is, look for changes from other fish instances). - /// \return a list of events for changed variables. - std::vector> universal_sync(bool always); - - /// Applies inherited variables in preparation for executing a function. - void apply_inherited_ffi(const function_properties_t &props); - // Compatibility hack; access the "environment stack" from back when there was just one. static const std::shared_ptr &principal_ref(); static env_stack_t &principal() { return *principal_ref(); } @@ -294,11 +270,9 @@ class env_stack_t final : public environment_t { /// Access the underlying Rust implementation. /// This returns a const rust::Box *, or in Rust terms, a *const Box. - const void *get_impl_ffi() const { return &impl_; } + const EnvStackRef &get_impl_ffi() const; private: - env_stack_t(rust::Box imp); - /// The implementation. Do not access this directly. rust::Box impl_; }; @@ -318,6 +292,32 @@ class env_dyn_t final : public environment_t { }; #endif +/// A struct of configuration directories, determined in main() that fish will optionally pass to +/// env_init. +struct config_paths_t { + wcstring data; // e.g., /usr/local/share + wcstring sysconf; // e.g., /usr/local/etc + wcstring doc; // e.g., /usr/local/share/doc/fish + wcstring bin; // e.g., /usr/local/bin +}; + +/// Initialize environment variable data. +void env_init(const struct config_paths_t *paths = nullptr, bool do_uvars = true, + bool default_paths = false); + +using env_var_flags_t = uint8_t; +enum { + env_var_flag_export = 1 << 0, // whether the variable is exported + env_var_flag_read_only = 1 << 1, // whether the variable is read only + env_var_flag_pathvar = 1 << 2, // whether the variable is a path variable +}; + +/// A mutable environment which allows scopes to be pushed and popped. + +#if INCLUDE_RUST_HEADERS +struct EnvDyn; +#endif + /// Gets a path appropriate for runtime storage wcstring env_get_runtime_path(); diff --git a/src/env_dispatch.h b/src/env_dispatch.h new file mode 100644 index 000000000..e9038f230 --- /dev/null +++ b/src/env_dispatch.h @@ -0,0 +1,16 @@ +// Prototypes for functions that react to environment variable changes +#ifndef FISH_ENV_DISPATCH_H +#define FISH_ENV_DISPATCH_H + +#include "config.h" // IWYU pragma: keep + +#include "common.h" +#include "env.h" + +/// Initialize variable dispatch. +void env_dispatch_init(const environment_t &vars); + +/// React to changes in variables like LANG which require running some code. +void env_dispatch_var_change(const wcstring &key, env_stack_t &vars); + +#endif diff --git a/src/env_fwd.h b/src/env_fwd.h new file mode 100644 index 000000000..771b4562a --- /dev/null +++ b/src/env_fwd.h @@ -0,0 +1,2 @@ +struct EnvStackRef; +struct EnvDyn; diff --git a/src/env_universal_common.cpp b/src/env_universal_common.cpp index 4682bbb5c..5377a66a9 100644 --- a/src/env_universal_common.cpp +++ b/src/env_universal_common.cpp @@ -55,830 +55,6 @@ #include #endif // Haiku -/// Error message. -#define PARSE_ERR L"Unable to parse universal variable message: '%ls'" - -/// Small note about not editing ~/.fishd manually. Inserted at the top of all .fishd files. -#define SAVE_MSG "# This file contains fish universal variable definitions.\n" - -/// Version for fish 3.0 -#define UVARS_VERSION_3_0 "3.0" - -// Maximum file size we'll read. -static constexpr size_t k_max_read_size = 16 * 1024 * 1024; - -// Fields used in fish 2.x uvars. -namespace fish2x_uvars { -namespace { -constexpr const char *SET = "SET"; -constexpr const char *SET_EXPORT = "SET_EXPORT"; -} // namespace -} // namespace fish2x_uvars - -// Fields used in fish 3.0 uvars -namespace fish3_uvars { -namespace { -constexpr const char *SETUVAR = "SETUVAR"; -constexpr const char *EXPORT = "--export"; -constexpr const char *PATH = "--path"; -} // namespace -} // namespace fish3_uvars - -/// The different types of messages found in the fishd file. -enum class uvar_message_type_t { set, set_export }; - -static maybe_t default_vars_path_directory() { - wcstring path; - if (!path_get_config(path)) return none(); - return path; -} - -/// \return the default variable path, or an empty string on failure. -static wcstring default_vars_path() { - if (auto path = default_vars_path_directory()) { - path->append(L"/fish_variables"); - return path.acquire(); - } - return wcstring{}; -} - -/// Test if the message msg contains the command cmd. -/// On success, updates the cursor to just past the command. -static bool match(const wchar_t **inout_cursor, const char *cmd) { - const wchar_t *cursor = *inout_cursor; - size_t len = std::strlen(cmd); - if (!std::equal(cmd, cmd + len, cursor)) { - return false; - } - if (cursor[len] && cursor[len] != L' ' && cursor[len] != L'\t') return false; - *inout_cursor = cursor + len; - return true; -} - -/// The universal variable format has some funny escaping requirements; here we try to be safe. -static bool is_universal_safe_to_encode_directly(wchar_t c) { - if (c < 32 || c > 128) return false; - - return iswalnum(c) || std::wcschr(L"/_", c); -} - -/// Escape specified string. -static wcstring full_escape(const wcstring &in) { - wcstring out; - for (wchar_t c : in) { - if (is_universal_safe_to_encode_directly(c)) { - out.push_back(c); - } else if (c <= static_cast(ASCII_MAX)) { - // See #1225 for discussion of use of ASCII_MAX here. - append_format(out, L"\\x%.2x", c); - } else if (c < 65536) { - append_format(out, L"\\u%.4x", c); - } else { - append_format(out, L"\\U%.8x", c); - } - } - return out; -} - -/// Converts input to UTF-8 and appends it to receiver, using storage as temp storage. -static bool append_utf8(const wcstring &input, std::string *receiver, std::string *storage) { - bool result = false; - if (wchar_to_utf8_string(input, storage)) { - receiver->append(*storage); - result = true; - } - return result; -} - -/// Creates a file entry like "SET fish_color_cwd:FF0". Appends the result to *result (as UTF8). -/// Returns true on success. storage may be used for temporary storage, to avoid allocations. -static bool append_file_entry(env_var_t::env_var_flags_t flags, const wcstring &key_in, - const wcstring &val_in, std::string *result, std::string *storage) { - namespace f3 = fish3_uvars; - assert(storage != nullptr); - assert(result != nullptr); - - // Record the length on entry, in case we need to back up. - bool success = true; - const size_t result_length_on_entry = result->size(); - - // Append SETVAR header. - result->append(f3::SETUVAR); - result->push_back(' '); - - // Append flags. - if (flags & env_var_t::flag_export) { - result->append(f3::EXPORT); - result->push_back(' '); - } - if (flags & env_var_t::flag_pathvar) { - result->append(f3::PATH); - result->push_back(' '); - } - - // Append variable name like "fish_color_cwd". - if (!valid_var_name(key_in)) { - FLOGF(error, L"Illegal variable name: '%ls'", key_in.c_str()); - success = false; - } - if (success && !append_utf8(key_in, result, storage)) { - FLOGF(error, L"Could not convert %ls to narrow character string", key_in.c_str()); - success = false; - } - - // Append ":". - if (success) { - result->push_back(':'); - } - - // Append value. - if (success && !append_utf8(full_escape(val_in), result, storage)) { - FLOGF(error, L"Could not convert %ls to narrow character string", val_in.c_str()); - success = false; - } - - // Append newline. - if (success) { - result->push_back('\n'); - } - - // Don't modify result on failure. It's sufficient to simply resize it since all we ever did was - // append to it. - if (!success) { - result->resize(result_length_on_entry); - } - - return success; -} - -/// Encoding of a null string. -static const wchar_t *const ENV_NULL = L"\x1d"; - -/// Character used to separate arrays in universal variables file. -/// This is 30, the ASCII record separator. -static const wchar_t UVAR_ARRAY_SEP = 0x1e; - -/// Decode a serialized universal variable value into a list. -static std::vector decode_serialized(const wcstring &val) { - if (val == ENV_NULL) return {}; - return split_string(val, UVAR_ARRAY_SEP); -} - -/// Decode a a list into a serialized universal variable value. -static wcstring encode_serialized(const std::vector &vals) { - if (vals.empty()) return ENV_NULL; - return join_strings(vals, UVAR_ARRAY_SEP); -} - -maybe_t env_universal_t::get(const wcstring &name) const { - auto where = vars.find(name); - if (where != vars.end()) return where->second; - return none(); -} - -std::unique_ptr env_universal_t::get_ffi(const wcstring &name) const { - if (auto var = this->get(name)) { - return make_unique(var.acquire()); - } else { - return nullptr; - } -} - -maybe_t env_universal_t::get_flags(const wcstring &name) const { - auto where = vars.find(name); - if (where != vars.end()) { - return where->second.get_flags(); - } - return none(); -} - -void env_universal_t::set(const wcstring &key, const env_var_t &var) { - bool new_entry = vars.count(key) == 0; - env_var_t &entry = vars[key]; - if (new_entry || entry != var) { - entry = var; - this->modified.insert(key); - if (entry.exports()) export_generation += 1; - } -} - -bool env_universal_t::remove(const wcstring &key) { - auto iter = this->vars.find(key); - if (iter != this->vars.end()) { - if (iter->second.exports()) export_generation += 1; - this->vars.erase(iter); - this->modified.insert(key); - return true; - } - return false; -} - -std::vector env_universal_t::get_names(bool show_exported, bool show_unexported) const { - std::vector result; - for (const auto &kv : vars) { - const wcstring &key = kv.first; - const env_var_t &var = kv.second; - if ((var.exports() && show_exported) || (!var.exports() && show_unexported)) { - result.push_back(key); - } - } - return result; -} - -// Given a variable table, generate callbacks representing the difference between our vars and the -// new vars. Update our exports generation. -void env_universal_t::generate_callbacks_and_update_exports(const var_table_t &new_vars, - callback_data_list_t &callbacks) { - // Construct callbacks for erased values. - for (const auto &kv : this->vars) { - const wcstring &key = kv.first; - // Skip modified values. - if (this->modified.count(key)) { - continue; - } - - // If the value is not present in new_vars, it has been erased. - if (new_vars.count(key) == 0) { - callbacks.push_back(callback_data_t(key, none())); - if (kv.second.exports()) export_generation += 1; - } - } - - // Construct callbacks for newly inserted or changed values. - for (const auto &kv : new_vars) { - const wcstring &key = kv.first; - - // Skip modified values. - if (this->modified.find(key) != this->modified.end()) { - continue; - } - - // See if the value has changed. - const env_var_t &new_entry = kv.second; - var_table_t::const_iterator existing = this->vars.find(key); - - bool old_exports = (existing != this->vars.end() && existing->second.exports()); - bool export_changed = (old_exports != new_entry.exports()); - bool value_changed = existing != this->vars.end() && existing->second != new_entry; - if (export_changed || value_changed) { - export_generation += 1; - } - if (existing == this->vars.end() || export_changed || value_changed) { - // Value is set for the first time, or has changed. - callbacks.push_back(callback_data_t(key, new_entry)); - } - } -} - -void env_universal_t::acquire_variables(var_table_t &&vars_to_acquire) { - // Copy modified values from existing vars to vars_to_acquire. - for (const auto &key : this->modified) { - auto src_iter = this->vars.find(key); - if (src_iter == this->vars.end()) { - /* The value has been deleted. */ - vars_to_acquire.erase(key); - } else { - // The value has been modified. Copy it over. Note we can destructively modify the - // source entry in vars since we are about to get rid of this->vars entirely. - env_var_t &src = src_iter->second; - env_var_t &dst = vars_to_acquire[key]; - dst = src; - } - } - - // We have constructed all the callbacks and updated vars_to_acquire. Acquire it! - this->vars = std::move(vars_to_acquire); -} - -void env_universal_t::load_from_fd(int fd, callback_data_list_t &callbacks) { - assert(fd >= 0); - // Get the dev / inode. - const file_id_t current_file = file_id_for_fd(fd); - if (current_file == last_read_file) { - FLOGF(uvar_file, L"universal log sync elided based on fstat()"); - } else { - // Read a variables table from the file. - var_table_t new_vars; - uvar_format_t format = this->read_message_internal(fd, &new_vars); - - // Hacky: if the read format is in the future, avoid overwriting the file: never try to - // save. - if (format == uvar_format_t::future) { - ok_to_save = false; - } - - // Announce changes and update our exports generation. - this->generate_callbacks_and_update_exports(new_vars, callbacks); - - // Acquire the new variables. - this->acquire_variables(std::move(new_vars)); - last_read_file = current_file; - } -} - -bool env_universal_t::load_from_path(const wcstring &path, callback_data_list_t &callbacks) { - return load_from_path(wcs2zstring(path), callbacks); -} - -bool env_universal_t::load_from_path(const std::string &path, callback_data_list_t &callbacks) { - // Check to see if the file is unchanged. We do this again in load_from_fd, but this avoids - // opening the file unnecessarily. - if (last_read_file != kInvalidFileID && file_id_for_path(path) == last_read_file) { - FLOGF(uvar_file, L"universal log sync elided based on fast stat()"); - return true; - } - - bool result = false; - autoclose_fd_t fd{open_cloexec(path, O_RDONLY)}; - if (fd.valid()) { - FLOGF(uvar_file, L"universal log reading from file"); - this->load_from_fd(fd.fd(), callbacks); - result = true; - } - return result; -} - -/// Serialize the contents to a string. -std::string env_universal_t::serialize_with_vars(const var_table_t &vars) { - std::string storage; - std::string contents; - contents.append(SAVE_MSG); - contents.append("# VERSION: " UVARS_VERSION_3_0 "\n"); - - // Preserve legacy behavior by sorting the values first - using env_pair_t = - std::pair, std::reference_wrapper>; - std::vector cloned(vars.begin(), vars.end()); - std::sort(cloned.begin(), cloned.end(), [](const env_pair_t &p1, const env_pair_t &p2) { - return p1.first.get() < p2.first.get(); - }); - - for (const auto &kv : cloned) { - // Append the entry. Note that append_file_entry may fail, but that only affects one - // variable; soldier on. - const wcstring &key = kv.first; - const env_var_t &var = kv.second; - append_file_entry(var.get_flags(), key, encode_serialized(var.as_list()), &contents, - &storage); - } - return contents; -} - -/// Writes our state to the fd. path is provided only for error reporting. -bool env_universal_t::write_to_fd(int fd, const wcstring &path) { - assert(fd >= 0); - bool success = true; - std::string contents = serialize_with_vars(vars); - if (write_loop(fd, contents.data(), contents.size()) < 0) { - const char *error = std::strerror(errno); - FLOGF(error, _(L"Unable to write to universal variables file '%ls': %s"), path.c_str(), - error); - success = false; - } - - // Since we just wrote out this file, it matches our internal state; pretend we read from it. - this->last_read_file = file_id_for_fd(fd); - - // We don't close the file. - return success; -} - -bool env_universal_t::move_new_vars_file_into_place(const wcstring &src, const wcstring &dst) { - int ret = wrename(src, dst); - if (ret != 0) { - const char *error = std::strerror(errno); - FLOGF(error, _(L"Unable to rename file from '%ls' to '%ls': %s"), src.c_str(), dst.c_str(), - error); - } - return ret == 0; -} - -void env_universal_t::initialize_at_path(callback_data_list_t &callbacks, wcstring path) { - if (path.empty()) return; - assert(!initialized() && "Already initialized"); - vars_path_ = std::move(path); - narrow_vars_path_ = wcs2zstring(vars_path_); - - if (load_from_path(narrow_vars_path_, callbacks)) { - // Successfully loaded from our normal path. - return; - } -} - -void env_universal_t::initialize(callback_data_list_t &callbacks) { - // Set do_flock to false immediately if the default variable path is on a remote filesystem. - // See #7968. - if (path_get_config_remoteness() == dir_remoteness_t::remote) do_flock = false; - this->initialize_at_path(callbacks, default_vars_path()); -} - -autoclose_fd_t env_universal_t::open_temporary_file(const wcstring &directory, wcstring *out_path) { - // Create and open a temporary file for writing within the given directory. Try to create a - // temporary file, up to 10 times. We don't use mkstemps because we want to open it CLO_EXEC. - // This should almost always succeed on the first try. - assert(!string_suffixes_string(L"/", directory)); //!OCLINT(multiple unary operator) - - int saved_errno = 0; - const wcstring tmp_name_template = directory + L"/fishd.tmp.XXXXXX"; - autoclose_fd_t result; - std::string narrow_str; - for (size_t attempt = 0; attempt < 10 && !result.valid(); attempt++) { - narrow_str = wcs2zstring(tmp_name_template); - result.reset(fish_mkstemp_cloexec(&narrow_str[0])); - saved_errno = errno; - } - *out_path = str2wcstring(narrow_str); - - if (!result.valid()) { - const char *error = std::strerror(saved_errno); - FLOGF(error, _(L"Unable to open temporary file '%ls': %s"), out_path->c_str(), error); - } - return result; -} - -/// Try locking the file. -/// \return true on success, false on error. -static bool flock_uvar_file(int fd) { - double start_time = timef(); - while (flock(fd, LOCK_EX) == -1) { - if (errno != EINTR) return false; // do nothing per issue #2149 - } - double duration = timef() - start_time; - if (duration > 0.25) { - FLOGF(warning, _(L"Locking the universal var file took too long (%.3f seconds)."), - duration); - return false; - } - return true; -} - -bool env_universal_t::open_and_acquire_lock(const wcstring &path, autoclose_fd_t *out_fd) { - // Attempt to open the file for reading at the given path, atomically acquiring a lock. On BSD, - // we can use O_EXLOCK. On Linux, we open the file, take a lock, and then compare fstat() to - // stat(); if they match, it means that the file was not replaced before we acquired the lock. - // - // We pass O_RDONLY with O_CREAT; this creates a potentially empty file. We do this so that we - // have something to lock on. - bool locked_by_open = false; - int flags = O_RDWR | O_CREAT; - -#ifdef O_EXLOCK - if (do_flock) { - flags |= O_EXLOCK; - locked_by_open = true; - } -#endif - - autoclose_fd_t fd{}; - while (!fd.valid()) { - fd = autoclose_fd_t{wopen_cloexec(path, flags, 0644)}; - - if (!fd.valid()) { - int err = errno; - if (err == EINTR) continue; // signaled; try again - -#ifdef O_EXLOCK - if ((flags & O_EXLOCK) && (err == ENOTSUP || err == EOPNOTSUPP)) { - // Filesystem probably does not support locking. Give up on locking. - // Note that on Linux the two errno symbols have the same value but on BSD they're - // different. - flags &= ~O_EXLOCK; - do_flock = false; - locked_by_open = false; - continue; - } -#endif - FLOGF(error, _(L"Unable to open universal variable file '%s': %s"), path.c_str(), - std::strerror(err)); - break; - } - - assert(fd.valid() && "Should have a valid fd here"); - - // Lock if we want to lock and open() didn't do it for us. - // If flock fails, give up on locking forever. - if (do_flock && !locked_by_open) { - if (!flock_uvar_file(fd.fd())) do_flock = false; - } - - // Hopefully we got the lock. However, it's possible the file changed out from under us - // while we were waiting for the lock. Make sure that didn't happen. - if (file_id_for_fd(fd.fd()) != file_id_for_path(path)) { - // Oops, it changed! Try again. - fd.close(); - } - } - - *out_fd = std::move(fd); - return out_fd->valid(); -} - -// Returns true if modified variables were written, false if not. (There may still be variable -// changes due to other processes on a false return). -bool env_universal_t::sync(callback_data_list_t &callbacks) { - if (!initialized()) return false; - - FLOGF(uvar_file, L"universal log sync"); - // Our saving strategy: - // - // 1. Open the file, producing an fd. - // 2. Lock the file (may be combined with step 1 on systems with O_EXLOCK) - // 3. After taking the lock, check if the file at the given path is different from what we - // opened. If so, start over. - // 4. Read from the file. This can be elided if its dev/inode is unchanged since the last read - // 5. Open an adjacent temporary file - // 6. Write our changes to an adjacent file - // 7. Move the adjacent file into place via rename. This is assumed to be atomic. - // 8. Release the lock and close the file - // - // Consider what happens if Process 1 and 2 both do this simultaneously. Can there be data loss? - // Process 1 opens the file and then attempts to take the lock. Now, either process 1 will see - // the original file, or process 2's new file. If it sees the new file, we're OK: it's going to - // read from the new file, and so there's no data loss. If it sees the old file, then process 2 - // must have locked it (if process 1 locks it, switch their roles). The lock will block until - // process 2 reaches step 7; at that point process 1 will reach step 2, notice that the file has - // changed, and then start over. - // - // It's possible that the underlying filesystem does not support locks (lockless NFS). In this - // case, we risk data loss if two shells try to write their universal variables simultaneously. - // In practice this is unlikely, since uvars are usually written interactively. - // - // Prior versions of fish used a hard link scheme to support file locking on lockless NFS. The - // risk here is that if the process crashes or is killed while holding the lock, future - // instances of fish will not be able to obtain it. This seems to be a greater risk than that of - // data loss on lockless NFS. Users who put their home directory on lockless NFS are playing - // with fire anyways. - // If we have no changes, just load. - if (modified.empty()) { - this->load_from_path(narrow_vars_path_, callbacks); - FLOGF(uvar_file, L"universal log no modifications"); - return false; - } - - const wcstring directory = wdirname(vars_path_); - autoclose_fd_t vars_fd{}; - - FLOGF(uvar_file, L"universal log performing full sync"); - - // Open the file. - if (!this->open_and_acquire_lock(vars_path_, &vars_fd)) { - FLOGF(uvar_file, L"universal log open_and_acquire_lock() failed"); - return false; - } - - // Read from it. - assert(vars_fd.valid()); - this->load_from_fd(vars_fd.fd(), callbacks); - - if (ok_to_save) { - return this->save(directory, vars_path_); - } else { - return true; - } -} - -// Write our file contents. -// \return true on success, false on failure. -bool env_universal_t::save(const wcstring &directory, const wcstring &vars_path) { - assert(ok_to_save && "It's not OK to save"); - - wcstring private_file_path; - - // Open adjacent temporary file. - autoclose_fd_t private_fd = this->open_temporary_file(directory, &private_file_path); - bool success = private_fd.valid(); - - if (!success) FLOGF(uvar_file, L"universal log open_temporary_file() failed"); - - // Write to it. - if (success) { - assert(private_fd.valid()); - success = this->write_to_fd(private_fd.fd(), private_file_path); - if (!success) FLOGF(uvar_file, L"universal log write_to_fd() failed"); - } - - if (success) { - wcstring real_path; - if (auto maybe_real_path = wrealpath(vars_path)) { - real_path = *maybe_real_path; - } else { - real_path = vars_path; - } - - // Ensure we maintain ownership and permissions (#2176). - struct stat sbuf; - if (wstat(real_path, &sbuf) >= 0) { - if (fchown(private_fd.fd(), sbuf.st_uid, sbuf.st_gid) == -1) - FLOGF(uvar_file, L"universal log fchown() failed"); - if (fchmod(private_fd.fd(), sbuf.st_mode) == -1) - FLOGF(uvar_file, L"universal log fchmod() failed"); - } - - // Linux by default stores the mtime with low precision, low enough that updates that occur - // in quick succession may result in the same mtime (even the nanoseconds field). So - // manually set the mtime of the new file to a high-precision clock. Note that this is only - // necessary because Linux aggressively reuses inodes, causing the ABA problem; on other - // platforms we tend to notice the file has changed due to a different inode (or file size!) - // - // The current time within the Linux kernel is cached, and generally only updated on a timer - // interrupt. So if the timer interrupt is running at 10 milliseconds, the cached time will - // only be updated once every 10 milliseconds. - // - // It's probably worth finding a simpler solution to this. The tests ran into this, but it's - // unlikely to affect users. -#if defined(UVAR_FILE_SET_MTIME_HACK) - struct timespec times[2] = {}; - times[0].tv_nsec = UTIME_OMIT; // don't change ctime - if (0 == clock_gettime(CLOCK_REALTIME, ×[1])) { - futimens(private_fd.fd(), times); - } -#endif - - // Apply new file. - success = this->move_new_vars_file_into_place(private_file_path, real_path); - if (!success) FLOGF(uvar_file, L"universal log move_new_vars_file_into_place() failed"); - } - - if (success) { - // Since we moved the new file into place, clear the path so we don't try to unlink it. - private_file_path.clear(); - } - - // Clean up. - if (!private_file_path.empty()) { - wunlink(private_file_path); - } - if (success) { - // All of our modified variables have now been written out. - modified.clear(); - } - return success; -} - -uvar_format_t env_universal_t::read_message_internal(int fd, var_table_t *vars) { - // Read everything from the fd. Put a sane limit on it. - std::string contents; - while (contents.size() < k_max_read_size) { - char buffer[4096]; - ssize_t amt = read_loop(fd, buffer, sizeof buffer); - if (amt <= 0) { - break; - } - contents.append(buffer, amt); - } - - // Handle overlong files. - if (contents.size() >= k_max_read_size) { - contents.resize(k_max_read_size); - // Back up to a newline. - size_t newline = contents.rfind('\n'); - contents.resize(newline == wcstring::npos ? 0 : newline); - } - - return populate_variables(contents, vars); -} - -/// \return the format corresponding to file contents \p s. -uvar_format_t env_universal_t::format_for_contents(const std::string &s) { - // Walk over leading comments, looking for one like '# version' - line_iterator_t iter{s}; - while (iter.next()) { - const std::string &line = iter.line(); - if (line.empty()) continue; - if (line.front() != L'#') { - // Exhausted leading comments. - break; - } - // Note scanf %s is max characters to write; add 1 for null terminator. - char versionbuf[64 + 1]; - if (sscanf(line.c_str(), "# VERSION: %64s", versionbuf) != 1) continue; - - // Try reading the version. - if (!std::strcmp(versionbuf, UVARS_VERSION_3_0)) { - return uvar_format_t::fish_3_0; - } else { - // Unknown future version. - return uvar_format_t::future; - } - } - // No version found, assume 2.x - return uvar_format_t::fish_2_x; -} - -uvar_format_t env_universal_t::populate_variables(const std::string &s, var_table_t *out_vars) { - // Decide on the format. - const uvar_format_t format = format_for_contents(s); - - line_iterator_t iter{s}; - wcstring wide_line; - wcstring storage; - while (iter.next()) { - const std::string &line = iter.line(); - // Skip empties and constants. - if (line.empty() || line.front() == L'#') continue; - - // Convert to UTF8. - wide_line.clear(); - if (!utf8_to_wchar(line.data(), line.size(), &wide_line, 0)) continue; - - switch (format) { - case uvar_format_t::fish_2_x: - env_universal_t::parse_message_2x_internal(wide_line, out_vars, &storage); - break; - case uvar_format_t::fish_3_0: - // For future formats, just try with the most recent one. - case uvar_format_t::future: - env_universal_t::parse_message_30_internal(wide_line, out_vars, &storage); - break; - } - } - return format; -} - -static const wchar_t *skip_spaces(const wchar_t *str) { - while (*str == L' ' || *str == L'\t') str++; - return str; -} - -bool env_universal_t::populate_1_variable(const wchar_t *input, env_var_t::env_var_flags_t flags, - var_table_t *vars, wcstring *storage) { - const wchar_t *str = skip_spaces(input); - const wchar_t *colon = std::wcschr(str, L':'); - if (!colon) return false; - - // Parse out the value into storage, and decode it into a variable. - storage->clear(); - auto unescaped = unescape_string(colon + 1, 0); - if (!unescaped) { - return false; - } - *storage = *unescaped; - env_var_t var{decode_serialized(*storage), flags}; - - // Parse out the key and write into the map. - storage->assign(str, colon - str); - const wcstring &key = *storage; - (*vars)[key] = std::move(var); - return true; -} - -/// Parse message msg per fish 3.0 format. -void env_universal_t::parse_message_30_internal(const wcstring &msgstr, var_table_t *vars, - wcstring *storage) { - namespace f3 = fish3_uvars; - const wchar_t *const msg = msgstr.c_str(); - if (msg[0] == L'#') return; - - const wchar_t *cursor = msg; - if (!match(&cursor, f3::SETUVAR)) { - FLOGF(warning, PARSE_ERR, msg); - return; - } - // Parse out flags. - env_var_t::env_var_flags_t flags = 0; - for (;;) { - cursor = skip_spaces(cursor); - if (*cursor != L'-') break; - if (match(&cursor, f3::EXPORT)) { - flags |= env_var_t::flag_export; - } else if (match(&cursor, f3::PATH)) { - flags |= env_var_t::flag_pathvar; - } else { - // Skip this unknown flag, for future proofing. - while (*cursor && *cursor != L' ' && *cursor != L'\t') cursor++; - } - } - - // Populate the variable with these flags. - if (!populate_1_variable(cursor, flags, vars, storage)) { - FLOGF(warning, PARSE_ERR, msg); - } -} - -/// Parse message msg per fish 2.x format. -void env_universal_t::parse_message_2x_internal(const wcstring &msgstr, var_table_t *vars, - wcstring *storage) { - namespace f2x = fish2x_uvars; - const wchar_t *const msg = msgstr.c_str(); - const wchar_t *cursor = msg; - - if (cursor[0] == L'#') return; - - env_var_t::env_var_flags_t flags = 0; - if (match(&cursor, f2x::SET_EXPORT)) { - flags |= env_var_t::flag_export; - } else if (match(&cursor, f2x::SET)) { - flags |= 0; - } else { - FLOGF(warning, PARSE_ERR, msg); - return; - } - - if (!populate_1_variable(cursor, flags, vars, storage)) { - FLOGF(warning, PARSE_ERR, msg); - } -} - namespace { class universal_notifier_shmem_poller_t final : public universal_notifier_t { #ifdef __CYGWIN__ @@ -1420,10 +596,6 @@ bool universal_notifier_t::notification_fd_became_readable(int fd) { return false; } -var_table_ffi_t::var_table_ffi_t(const var_table_t &table) { - for (const auto &kv : table) { - this->names.push_back(kv.first); - this->vars.push_back(kv.second); - } +void env_universal_notifier_t_default_notifier_post_notification_ffi() { + return universal_notifier_t::default_notifier().post_notification(); } -var_table_ffi_t::~var_table_ffi_t() = default; diff --git a/src/env_universal_common.h b/src/env_universal_common.h index 420de3e88..d68baadb2 100644 --- a/src/env_universal_common.h +++ b/src/env_universal_common.h @@ -15,201 +15,9 @@ #include "maybe.h" #include "wutil.h" -/// Callback data, reflecting a change in universal variables. -struct callback_data_t { - // The name of the variable. - wcstring key; - - // The value of the variable, or none if it is erased. - maybe_t val; - - /// Construct from a key and maybe a value. - callback_data_t(wcstring k, maybe_t v) : key(std::move(k)), val(std::move(v)) {} - - /// \return whether this callback represents an erased variable. - bool is_erase() const { return !val.has_value(); } -}; -using callback_data_list_t = std::vector; - -/// Wrapper type for ffi purposes. -struct env_universal_sync_result_t { - // List of callbacks. - callback_data_list_t list; - - // Return value of sync(). - bool changed; - - bool get_changed() const { return changed; } - - size_t count() const { return list.size(); } - const wcstring &get_key(size_t idx) const { return list.at(idx).key; } - bool get_is_erase(size_t idx) const { return list.at(idx).is_erase(); } -}; - -/// FFI helper to import our var_table into Rust. -/// Parallel names of strings and environment variables. -struct var_table_ffi_t { - std::vector names; - std::vector vars; - - size_t count() const { return names.size(); } - const wcstring &get_name(size_t idx) const { return names.at(idx); } - const env_var_t &get_var(size_t idx) const { return vars.at(idx); } - - explicit var_table_ffi_t(const var_table_t &table); - ~var_table_ffi_t(); -}; - -// List of fish universal variable formats. -// This is exposed for testing. -enum class uvar_format_t { fish_2_x, fish_3_0, future }; - -/// Class representing universal variables. -class env_universal_t { - public: - // Construct an empty universal variables. - env_universal_t() = default; - - // Construct inside a unique_ptr. - static std::unique_ptr new_unique() { - return std::unique_ptr(new env_universal_t()); - } - - // Get the value of the variable with the specified name. - maybe_t get(const wcstring &name) const; - - // Cover over get() for FFI purposes. - std::unique_ptr get_ffi(const wcstring &name) const; - - // \return flags from the variable with the given name. - maybe_t get_flags(const wcstring &name) const; - - // Sets a variable. - void set(const wcstring &key, const env_var_t &var); - - // Removes a variable. Returns true if it was found, false if not. - bool remove(const wcstring &key); - - // Gets variable names. - std::vector get_names(bool show_exported, bool show_unexported) const; - - // Cover over get_names for FFI. - wcstring_list_ffi_t get_names_ffi(bool show_exported, bool show_unexported) const { - return get_names(show_exported, show_unexported); - } - - /// Get a view on the universal variable table. - const var_table_t &get_table() const { return vars; } - var_table_ffi_t get_table_ffi() const { return var_table_ffi_t(vars); } - - /// Initialize this uvars for the default path. - /// This should be called at most once on any given instance. - void initialize(callback_data_list_t &callbacks); - - /// Initialize a this uvars for a given path. - /// This is exposed for testing only. - void initialize_at_path(callback_data_list_t &callbacks, wcstring path); - - /// FFI helpers. - env_universal_sync_result_t initialize_ffi() { - env_universal_sync_result_t res{}; - initialize(res.list); - return res; - } - - env_universal_sync_result_t initialize_at_path_ffi(wcstring path) { - env_universal_sync_result_t res{}; - initialize_at_path(res.list, std::move(path)); - return res; - } - - /// Reads and writes variables at the correct path. Returns true if modified variables were - /// written. - bool sync(callback_data_list_t &callbacks); - - /// FFI helper. - env_universal_sync_result_t sync_ffi() { - callback_data_list_t callbacks; - bool changed = sync(callbacks); - return env_universal_sync_result_t{std::move(callbacks), changed}; - } - - /// Populate a variable table \p out_vars from a \p s string. - /// This is exposed for testing only. - /// \return the format of the file that we read. - static uvar_format_t populate_variables(const std::string &s, var_table_t *out_vars); - - /// Guess a file format. Exposed for testing only. - static uvar_format_t format_for_contents(const std::string &s); - - /// Serialize a variable list. Exposed for testing only. - static std::string serialize_with_vars(const var_table_t &vars); - - /// Exposed for testing only. - bool is_ok_to_save() const { return ok_to_save; } - - /// Access the export generation. - uint64_t get_export_generation() const { return export_generation; } - - private: - // Path that we save to. This is set in initialize(). If empty, initialize has not been called. - wcstring vars_path_; - std::string narrow_vars_path_; - - // The table of variables. - var_table_t vars; - - // Keys that have been modified, and need to be written. A value here that is not present in - // vars indicates a deleted value. - std::unordered_set modified; - - // A generation count which is incremented every time an exported variable is modified. - uint64_t export_generation{1}; - - // Whether it's OK to save. This may be set to false if we discover that a future version of - // fish wrote the uvars contents. - bool ok_to_save{true}; - - // If true, attempt to flock the uvars file. - // This latches to false if the file is found to be remote, where flock may hang. - bool do_flock{true}; - - // File id from which we last read. - file_id_t last_read_file = kInvalidFileID; - - /// \return whether we are initialized. - bool initialized() const { return !vars_path_.empty(); } - - bool load_from_path(const wcstring &path, callback_data_list_t &callbacks); - bool load_from_path(const std::string &path, callback_data_list_t &callbacks); - - void load_from_fd(int fd, callback_data_list_t &callbacks); - - // Functions concerned with saving. - bool open_and_acquire_lock(const wcstring &path, autoclose_fd_t *out_fd); - autoclose_fd_t open_temporary_file(const wcstring &directory, wcstring *out_path); - bool write_to_fd(int fd, const wcstring &path); - bool move_new_vars_file_into_place(const wcstring &src, const wcstring &dst); - - // Given a variable table, generate callbacks representing the difference between our vars and - // the new vars. Also update our exports generation count as necessary. - void generate_callbacks_and_update_exports(const var_table_t &new_vars, - callback_data_list_t &callbacks); - - // Given a variable table, copy unmodified values into self. - void acquire_variables(var_table_t &&vars_to_acquire); - - static bool populate_1_variable(const wchar_t *input, env_var_t::env_var_flags_t flags, - var_table_t *vars, wcstring *storage); - - static void parse_message_2x_internal(const wcstring &msg, var_table_t *vars, - wcstring *storage); - static void parse_message_30_internal(const wcstring &msg, var_table_t *vars, - wcstring *storage); - static uvar_format_t read_message_internal(int fd, var_table_t *vars); - - bool save(const wcstring &directory, const wcstring &vars_path); -}; +#if INCLUDE_RUST_HEADERS +#include "env_universal_common.rs.h" +#endif /// The "universal notifier" is an object responsible for broadcasting and receiving universal /// variable change notifications. These notifications do not contain the change, but merely @@ -281,4 +89,6 @@ class universal_notifier_t { wcstring get_runtime_path(); +void env_universal_notifier_t_default_notifier_post_notification_ffi(); + #endif diff --git a/src/event.cpp b/src/event.cpp index ed0736054..a472e494b 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -28,8 +28,9 @@ #include "wcstringutil.h" #include "wutil.h" // IWYU pragma: keep -void event_fire_generic(parser_t &parser, const wcstring &name, const std::vector &args) { - std::vector ffi_args; - for (const auto &arg : args) ffi_args.push_back(arg.c_str()); +void event_fire_generic(const parser_t &parser, const wcstring &name, + const std::vector &args) { + wcstring_list_ffi_t ffi_args; + for (const auto &arg : args) ffi_args.push(arg); event_fire_generic_ffi(parser, name, ffi_args); } diff --git a/src/event.h b/src/event.h index b6d233799..30df347d1 100644 --- a/src/event.h +++ b/src/event.h @@ -14,9 +14,9 @@ #include "common.h" #include "cxx.h" #include "global_safety.h" +#include "parser.h" #include "wutil.h" -class Parser; using parser_t = Parser; #if INCLUDE_RUST_HEADERS #include "event.rs.h" #else @@ -32,9 +32,7 @@ struct Event; // TODO: Remove after porting functions.cpp extern const wchar_t *const event_filter_names[]; -class Parser; using parser_t = Parser; - -void event_fire_generic(parser_t &parser, const wcstring &name, +void event_fire_generic(const parser_t &parser, const wcstring &name, const std::vector &args = g_empty_string_list); #endif diff --git a/src/exec.cpp b/src/exec.cpp deleted file mode 100644 index bd0133386..000000000 --- a/src/exec.cpp +++ /dev/null @@ -1,1328 +0,0 @@ -// Functions for executing a program. -// -// Some of the code in this file is based on code from the Glibc manual, though the changes -// performed have been massive. -#include "config.h" - -#include -#include -#include - -#include "trace.rs.h" -#ifdef HAVE_SIGINFO_H -#include -#endif -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "ast.h" -#include "builtin.h" -#include "common.h" -#include "cxx.h" -#include "env.h" -#include "env_dispatch.rs.h" -#include "exec.h" -#include "fallback.h" // IWYU pragma: keep -#include "fds.h" -#include "ffi.h" -#include "flog.h" -#include "fork_exec/spawn.rs.h" -#include "function.h" -#include "global_safety.h" -#include "io.h" -#include "iothread.h" -#include "job_group.rs.h" -#include "maybe.h" -#include "null_terminated_array.h" -#include "parse_tree.h" -#include "parser.h" -#include "proc.h" -#include "reader.h" -#include "redirection.h" -#include "timer.rs.h" -#include "trace.rs.h" -#include "wcstringutil.h" -#include "wutil.h" // IWYU pragma: keep - -/// Number of calls to fork() or posix_spawn(). -static relaxed_atomic_t s_fork_count{0}; - -/// A launch_result_t indicates when a process failed to launch, and therefore the rest of the -/// pipeline should be aborted. This includes failed redirections, fd exhaustion, fork() failures, -/// etc. -enum class launch_result_t { - ok, - failed, -} __warn_unused_type; - -/// Given an error \p err returned from either posix_spawn or exec, \return a process exit code. -static int exit_code_from_exec_error(int err) { - assert(err && "Zero is success, not an error"); - switch (err) { - case ENOENT: - case ENOTDIR: - // This indicates either the command was not found, or a file redirection was not found. - // We do not use posix_spawn file redirections so this is always command-not-found. - return STATUS_CMD_UNKNOWN; - - case EACCES: - case ENOEXEC: - // The file is not executable for various reasons. - return STATUS_NOT_EXECUTABLE; - -#ifdef EBADARCH - case EBADARCH: - // This is for e.g. running ARM app on Intel Mac. - return STATUS_NOT_EXECUTABLE; -#endif - default: - // Generic failure. - return EXIT_FAILURE; - } -} - -/// This is a 'looks like text' check. -/// \return true if either there is no NUL byte, or there is a line containing a lowercase letter -/// before the first NUL byte. -static bool is_thompson_shell_payload(const char *p, size_t n) { - if (!memchr(p, '\0', n)) return true; - bool haslower = false; - for (; *p; p++) { - if (islower(*p) || *p == '$' || *p == '`') { - haslower = true; - } - if (haslower && *p == '\n') { - return true; - } - } - return false; -} - -/// This function checks the beginning of a file to see if it's safe to -/// pass to the system interpreter when execve() returns ENOEXEC. -/// -/// The motivation is to be able to run classic shell scripts which -/// didn't have shebang, while protecting the user from accidentally -/// running a binary file which may corrupt terminal driver state. We -/// check for lowercase letters because the ASCII magic of binary files -/// is usually uppercase, e.g. PNG, JFIF, MZ, etc. These rules are also -/// flexible enough to permit scripts with concatenated binary content, -/// such as Actually Portable Executable. -/// N.B.: this is called after fork, it must not allocate heap memory. -bool is_thompson_shell_script(const char *path) { - // Paths ending in ".fish" are never considered Thompson shell scripts. - if (const char *lastdot = strrchr(path, '.')) { - if (0 == strcmp(lastdot, ".fish")) { - return false; - } - } - int e = errno; - bool res = false; - int fd = open_cloexec(path, O_RDONLY | O_NOCTTY); - if (fd != -1) { - char buf[256]; - ssize_t got = read(fd, buf, sizeof(buf)); - close(fd); - if (got >= 0 && is_thompson_shell_payload(buf, static_cast(got))) { - res = true; - } - } - errno = e; - return res; -} - -// Implemented in postfork.rs. We don't use the FFI bridge to avoid the risk of allocations. -extern "C" { -void safe_report_exec_error(int err, const char *actual_cmd, const char *const *argv, - const char *const *envv); - -int child_setup_process(pid_t claim_tty_from, const sigset_t *sigmask, bool is_forked, - const dup2_list_t *dup2s); - -pid_t execute_fork(); - -int execute_setpgid(pid_t pid, pid_t pgroup, bool is_parent); - -void report_setpgid_error(int err, bool is_parent, pid_t pid, pid_t desired_pgid, job_id_t job_id, - const char *cmd, const char *argv0); -} - -/// This function is executed by the child process created by a call to fork(). It should be called -/// after \c child_setup_process. It calls execve to replace the fish process image with the command -/// specified in \c p. It never returns. Called in a forked child! Do not allocate memory, etc. -[[noreturn]] static void safe_launch_process(process_t *p, const char *actual_cmd, - const char *const *cargv, const char *const *cenvv) { - UNUSED(p); - int err; - - // This function never returns, so we take certain liberties with constness. - auto envv = const_cast(cenvv); - auto argv = const_cast(cargv); - auto cmd2 = const_cast(actual_cmd); - - execve(actual_cmd, argv, envv); - err = errno; - - // The shebang wasn't introduced until UNIX Seventh Edition, so if - // the kernel won't run the binary we hand it off to the interpreter - // after performing a binary safety check, recommended by POSIX: a - // line needs to exist before the first \0 with a lowercase letter - if (err == ENOEXEC && is_thompson_shell_script(actual_cmd)) { - // Construct new argv. - // We must not allocate memory, so only 128 args are supported. - constexpr size_t maxargs = 128; - size_t nargs = 0; - while (argv[nargs]) nargs++; - if (nargs <= maxargs) { - char *argv2[1 + maxargs + 1]; // +1 for /bin/sh, +1 for terminating nullptr - char interp[] = _PATH_BSHELL; - argv2[0] = interp; - std::copy_n(argv, 1 + nargs, &argv2[1]); // +1 to copy terminating nullptr - // The command to call should use the full path, - // not what we would pass as argv0. - argv2[1] = cmd2; - execve(_PATH_BSHELL, argv2, envv); - } - } - - errno = err; - safe_report_exec_error(errno, actual_cmd, argv, envv); - exit_without_destructors(exit_code_from_exec_error(err)); -} - -/// This function is similar to launch_process, except it is not called after a fork (i.e. it only -/// calls exec) and therefore it can allocate memory. -[[noreturn]] static void launch_process_nofork(env_stack_t &vars, process_t *p) { - ASSERT_IS_NOT_FORKED_CHILD(); - - // Construct argv. Ensure the strings stay alive for the duration of this function. - std::vector narrow_strings = wide_string_list_to_narrow(p->argv()); - null_terminated_array_t narrow_argv(narrow_strings); - const char **argv = narrow_argv.get(); - - // Construct envp. - auto export_vars = vars.export_arr(); - const char **envp = export_vars->get(); - std::string actual_cmd = wcs2zstring(p->actual_cmd); - - // Ensure the terminal modes are what they were before we changed them. - restore_term_mode(); - // Bounce to launch_process. This never returns. - safe_launch_process(p, actual_cmd.c_str(), argv, envp); -} - -// Returns whether we can use posix spawn for a given process in a given job. -// -// To avoid the race between the caller calling tcsetpgrp() and the client checking the -// foreground process group, we don't use posix_spawn if we're going to foreground the process. (If -// we use fork(), we can call tcsetpgrp after the fork, before the exec, and avoid the race). -static bool can_use_posix_spawn_for_job(const std::shared_ptr &job, - const dup2_list_t &dup2s) { - // Is it globally disabled? - if (!use_posix_spawn()) return false; - - // Hack - do not use posix_spawn if there are self-fd redirections. - // For example if you were to write: - // cmd 6< /dev/null - // it is possible that the open() of /dev/null would result in fd 6. Here even if we attempted - // to add a dup2 action, it would be ignored and the CLO_EXEC bit would remain. So don't use - // posix_spawn in this case; instead we'll call fork() and clear the CLO_EXEC bit manually. - for (const auto &action : dup2s.get_actions()) { - if (action.src == action.target) return false; - } - if (job->group->wants_terminal()) { - // This job will be foregrounded, so we will call tcsetpgrp(), therefore do not use - // posix_spawn. - return false; - } - return true; -} - -static void internal_exec(env_stack_t &vars, job_t *j, const io_chain_t &block_io) { - // Do a regular launch - but without forking first... - process_t *p = j->processes.front().get(); - io_chain_t all_ios = block_io; - if (!all_ios.append_from_specs(p->redirection_specs(), vars.get_pwd_slash())) { - return; - } - - sigset_t blocked_signals_storage; - sigemptyset(&blocked_signals_storage); - const sigset_t *blocked_signals = nullptr; - if (blocked_signals_for_job(*j, &blocked_signals_storage)) { - blocked_signals = &blocked_signals_storage; - } - - // child_setup_process makes sure signals are properly set up. - dup2_list_t redirs = dup2_list_resolve_chain_shim(all_ios); - if (child_setup_process(false /* not claim_tty */, blocked_signals, false /* not is_forked */, - &redirs) == 0) { - // Decrement SHLVL as we're removing ourselves from the shell "stack". - if (is_interactive_session()) { - auto shlvl_var = vars.get(L"SHLVL", ENV_GLOBAL | ENV_EXPORT); - wcstring shlvl_str = L"0"; - if (shlvl_var) { - long shlvl = fish_wcstol(shlvl_var->as_string().c_str()); - if (!errno && shlvl > 0) { - shlvl_str = to_string(shlvl - 1); - } - } - vars.set_one(L"SHLVL", ENV_GLOBAL | ENV_EXPORT, std::move(shlvl_str)); - } - - // launch_process _never_ returns. - launch_process_nofork(vars, p); - } -} - -/// Construct an internal process for the process p. In the background, write the data \p outdata to -/// stdout and \p errdata to stderr, respecting the io chain \p ios. For example if target_fd is 1 -/// (stdout), and there is a dup2 3->1, then we need to write to fd 3. Then exit the internal -/// process. -static void run_internal_process(process_t *p, std::string &&outdata, std::string &&errdata, - const io_chain_t &ios) { - p->check_generations_before_launch(); - - // We want both the dup2s and the io_chain_ts to be kept alive by the background thread, because - // they may own an fd that we want to write to. Move them all to a shared_ptr. The strings as - // well (they may be long). - // Construct a little helper struct to make it simpler to move into our closure without copying. - struct write_fields_t { - int src_outfd{-1}; - std::string outdata{}; - - int src_errfd{-1}; - std::string errdata{}; - - io_chain_t ios{}; - maybe_t dup2s{}; - std::shared_ptr internal_proc{}; - - proc_status_t success_status{}; - - bool skip_out() const { return outdata.empty() || src_outfd < 0; } - - bool skip_err() const { return errdata.empty() || src_errfd < 0; } - }; - - auto f = std::make_shared(); - f->outdata = std::move(outdata); - f->errdata = std::move(errdata); - - // Construct and assign the internal process to the real process. - p->internal_proc_ = std::make_shared(); - f->internal_proc = p->internal_proc_; - - FLOGF(proc_internal_proc, L"Created internal proc %llu to write output for proc '%ls'", - p->internal_proc_->get_id(), p->argv0()); - - // Resolve the IO chain. - // Note it's important we do this even if we have no out or err data, because we may have been - // asked to truncate a file (e.g. `echo -n '' > /tmp/truncateme.txt'). The open() in the dup2 - // list resolution will ensure this happens. - f->dup2s = dup2_list_resolve_chain_shim(ios); - - // Figure out which source fds to write to. If they are closed (unlikely) we just exit - // successfully. - f->src_outfd = f->dup2s->fd_for_target_fd(STDOUT_FILENO); - f->src_errfd = f->dup2s->fd_for_target_fd(STDERR_FILENO); - - // If we have nothing to write we can elide the thread. - // TODO: support eliding output to /dev/null. - if (f->skip_out() && f->skip_err()) { - f->internal_proc->mark_exited(p->status); - return; - } - - // Ensure that ios stays alive, it may own fds. - f->ios = ios; - - // If our process is a builtin, it will have already set its status value. Make sure we - // propagate that if our I/O succeeds and don't read it on a background thread. TODO: have - // builtin_run provide this directly, rather than setting it in the process. - f->success_status = p->status; - - iothread_perform_cantwait([f]() { - proc_status_t status = f->success_status; - if (!f->skip_out()) { - ssize_t ret = write_loop(f->src_outfd, f->outdata.data(), f->outdata.size()); - if (ret < 0) { - if (errno != EPIPE) { - wperror(L"write"); - } - if (status.is_success()) { - status = proc_status_t::from_exit_code(1); - } - } - } - if (!f->skip_err()) { - ssize_t ret = write_loop(f->src_errfd, f->errdata.data(), f->errdata.size()); - if (ret < 0) { - if (errno != EPIPE) { - wperror(L"write"); - } - if (status.is_success()) { - status = proc_status_t::from_exit_code(1); - } - } - } - f->internal_proc->mark_exited(status); - }); -} - -/// If \p outdata or \p errdata are both empty, then mark the process as completed immediately. -/// Otherwise, run an internal process. -static void run_internal_process_or_short_circuit(parser_t &parser, const std::shared_ptr &j, - process_t *p, std::string &&outdata, - std::string &&errdata, const io_chain_t &ios) { - if (outdata.empty() && errdata.empty()) { - p->completed = true; - if (p->is_last_in_job) { - FLOGF(exec_job_status, L"Set status of job %d (%ls) to %d using short circuit", - j->job_id(), j->preview().c_str(), p->status); - auto statuses = j->get_statuses(); - if (statuses) { - parser.set_last_statuses(statuses.value()); - parser.libdata().status_count++; - } else if (j->flags().negate) { - // Special handling for `not set var (substitution)`. - // If there is no status, but negation was requested, - // take the last status and negate it. - auto last_statuses = parser.get_last_statuses(); - last_statuses.status = !last_statuses.status; - parser.set_last_statuses(last_statuses); - } - } - } else { - run_internal_process(p, std::move(outdata), std::move(errdata), ios); - } -} - -bool blocked_signals_for_job(const job_t &job, sigset_t *sigmask) { - // Block some signals in background jobs for which job control is turned off (#6828). - if (!job.is_foreground() && !job.wants_job_control()) { - sigaddset(sigmask, SIGINT); - sigaddset(sigmask, SIGQUIT); - return true; - } - return false; -} - -/// Call fork() as part of executing a process \p p in a job \j. Execute \p child_action in the -/// context of the child. -static launch_result_t fork_child_for_process(const std::shared_ptr &job, process_t *p, - const dup2_list_t &dup2s, const char *fork_type, - const std::function &child_action) { - // Claim the tty from fish, if the job wants it and we are the pgroup leader. - pid_t claim_tty_from = - (p->leads_pgrp && job->group->wants_terminal()) ? getpgrp() : INVALID_PID; - - // Decide if the job wants to set a custom sigmask. - sigset_t blocked_signals_storage; - sigemptyset(&blocked_signals_storage); - const sigset_t *blocked_signals = nullptr; - if (blocked_signals_for_job(*job, &blocked_signals_storage)) { - blocked_signals = &blocked_signals_storage; - } - - // Narrow the command name for error reporting before fork, - // to avoid allocations in the forked child. - std::string narrow_cmd = wcs2string(job->command_wcstr()); - std::string narrow_argv0 = wcs2string(p->argv0()); - job_id_t job_id = job->job_id(); - - pid_t pid = execute_fork(); - if (pid < 0) { - return launch_result_t::failed; - } - const bool is_parent = (pid > 0); - - // Record the pgroup if this is the leader. - // Both parent and child attempt to send the process to its new group, to resolve the race. - p->pid = is_parent ? pid : getpid(); - if (p->leads_pgrp) { - job->group->set_pgid(p->pid); - } - - { - auto pgid = job->group->get_pgid(); - if (pgid) { - if (int err = execute_setpgid(p->pid, pgid->value, is_parent)) { - report_setpgid_error(err, is_parent, p->pid, pgid->value, job_id, - narrow_cmd.c_str(), narrow_argv0.c_str()); - } - } - } - - if (!is_parent) { - // Child process. - child_setup_process(claim_tty_from, blocked_signals, true, &dup2s); - child_action(); - DIE("Child process returned control to fork_child lambda!"); - } - - ++s_fork_count; - FLOGF(exec_fork, L"Fork #%d, pid %d: %s for '%ls'", int(s_fork_count), pid, fork_type, - p->argv0()); - return launch_result_t::ok; -} - -/// \return an newly allocated output stream for the given fd, which is typically stdout or stderr. -/// This inspects the io_chain and decides what sort of output stream to return. -/// If \p piped_output_needs_buffering is set, and if the output is going to a pipe, then the other -/// end then synchronously writing to the pipe risks deadlock, so we must buffer it. -static std::shared_ptr create_output_stream_for_builtin( - int fd, const io_chain_t &io_chain, bool piped_output_needs_buffering) { - using std::make_shared; - const shared_ptr io = io_chain.io_for_fd(fd); - if (io == nullptr) { - // Common case of no redirections. - // Just write to the fd directly. - return make_shared(fd); - } - switch (io->io_mode) { - case io_mode_t::bufferfill: { - // Our IO redirection is to an internal buffer, e.g. a command substitution. - // We will write directly to it. - std::shared_ptr buffer = - std::static_pointer_cast(io)->buffer(); - return make_unique(buffer); - } - - case io_mode_t::close: - // Like 'echo foo >&-' - return make_shared(); - - case io_mode_t::file: - // Output is to a file which has been opened. - return make_shared(io->source_fd); - - case io_mode_t::pipe: - // Output is to a pipe. We may need to buffer. - if (piped_output_needs_buffering) { - return make_shared(); - } else { - return make_shared(io->source_fd); - } - - case io_mode_t::fd: - // This is a case like 'echo foo >&5' - // It's uncommon and unclear what should happen. - return make_shared(); - } - DIE("Unreachable"); -} - -/// Handle output from a builtin, by printing the contents of builtin_io_streams to the redirections -/// given in io_chain. -static void handle_builtin_output(parser_t &parser, const std::shared_ptr &j, process_t *p, - const io_chain_t &io_chain, const output_stream_t &out, - const output_stream_t &err) { - assert(p->type == process_type_t::builtin && "Process is not a builtin"); - - // Figure out any data remaining to write. We may have none, in which case we can short-circuit. - std::string outbuff = wcs2string(out.contents()); - std::string errbuff = wcs2string(err.contents()); - - // Some historical behavior. - if (!outbuff.empty()) fflush(stdout); - if (!errbuff.empty()) fflush(stderr); - - // Construct and run our background process. - run_internal_process_or_short_circuit(parser, j, p, std::move(outbuff), std::move(errbuff), - io_chain); -} - -/// Executes an external command. -/// An error return here indicates that the process failed to launch, and the rest of -/// the pipeline should be cancelled. -static launch_result_t exec_external_command(parser_t &parser, const std::shared_ptr &j, - process_t *p, const io_chain_t &proc_io_chain) { - assert(p->type == process_type_t::external && "Process is not external"); - // Get argv and envv before we fork. - const std::vector narrow_argv = wide_string_list_to_narrow(p->argv()); - null_terminated_array_t argv_array(narrow_argv); - - // Convert our IO chain to a dup2 sequence. - auto dup2s = dup2_list_resolve_chain_shim(proc_io_chain); - - // Ensure that stdin is blocking before we hand it off (see issue #176). - // Note this will also affect stdout and stderr if they refer to the same tty. - make_fd_blocking(STDIN_FILENO); - - auto export_arr = parser.vars().export_arr(); - const char *const *argv = argv_array.get(); - const char *const *envv = export_arr->get(); - - std::string actual_cmd_str = wcs2zstring(p->actual_cmd); - const char *actual_cmd = actual_cmd_str.c_str(); - filename_ref_t file = parser.libdata().current_filename; - -#if HAVE_SPAWN_H - // Prefer to use posix_spawn, since it's faster on some systems like OS X. - if (can_use_posix_spawn_for_job(j, dup2s)) { - ++s_fork_count; // spawn counts as a fork+exec - - int err = 0; - maybe_t pid = none(); - PosixSpawner *raw_spawner = - new_spawner(reinterpret_cast(j.get()), reinterpret_cast(&dup2s)); - if (raw_spawner == nullptr) { - err = errno; - } else { - auto spawner = rust::Box::from_raw(raw_spawner); - auto pid_or_neg = spawner->spawn(actual_cmd, const_cast(argv), - const_cast(envv)); - if (pid_or_neg > 0) { - pid = pid_or_neg; - } else { - err = errno; - } - } - if (err) { - safe_report_exec_error(err, actual_cmd, argv, envv); - p->status = proc_status_t::from_exit_code(exit_code_from_exec_error(err)); - return launch_result_t::failed; - } - assert(pid.has_value() && *pid > 0 && "Should have either a valid pid, or an error"); - - // This usleep can be used to test for various race conditions - // (https://github.com/fish-shell/fish-shell/issues/360). - // usleep(10000); - - FLOGF(exec_fork, L"Fork #%d, pid %d: spawn external command '%s' from '%ls'", - int(s_fork_count), *pid, actual_cmd, file ? file->c_str() : L""); - - // these are all things do_fork() takes care of normally (for forked processes): - p->pid = *pid; - if (p->leads_pgrp) { - j->group->set_pgid(p->pid); - // posix_spawn should in principle set the pgid before returning. - // In glibc, posix_spawn uses fork() and the pgid group is set on the child side; - // therefore the parent may not have seen it be set yet. - // Ensure it gets set. See #4715, also https://github.com/Microsoft/WSL/issues/2997. - execute_setpgid(p->pid, p->pid, true /* is parent */); - } - return launch_result_t::ok; - } else -#endif - { - return fork_child_for_process(j, p, dup2s, "external command", - [&] { safe_launch_process(p, actual_cmd, argv, envv); }); - } -} - -// Given that we are about to execute a function, push a function block and set up the -// variable environment. -static block_t *function_prepare_environment(parser_t &parser, std::vector argv, - const function_properties_t &props) { - // Extract the function name and remaining arguments. - wcstring func_name; - if (!argv.empty()) { - // Extract and remove the function name from argv. - func_name = std::move(*argv.begin()); - argv.erase(argv.begin()); - } - block_t *fb = parser.push_block(block_t::function_block(func_name, argv, props.shadow_scope())); - auto &vars = parser.vars(); - - // Setup the environment for the function. There are three components of the environment: - // 1. named arguments - // 2. inherited variables - // 3. argv - - size_t idx = 0; - auto args = props.named_arguments(); - for (const wcstring &named_arg : args->vals) { - if (idx < argv.size()) { - vars.set_one(named_arg, ENV_LOCAL | ENV_USER, argv.at(idx)); - } else { - vars.set_empty(named_arg, ENV_LOCAL | ENV_USER); - } - idx++; - } - - vars.apply_inherited_ffi(props); - // for (const auto &kv : props.inherit_vars) { - // vars.set(kv.first, ENV_LOCAL | ENV_USER, kv.second); - // } - - vars.set_argv(std::move(argv)); - return fb; -} - -// Given that we are done executing a function, restore the environment. -static void function_restore_environment(parser_t &parser, const block_t *block) { - parser.pop_block(block); - - // If we returned due to a return statement, then stop returning now. - parser.libdata().returning = false; -} - -// The "performer" function of a block or function process. -// This accepts a place to execute as \p parser and then executes the result, returning a status. -// This is factored out in this funny way in preparation for concurrent execution. -using proc_performer_t = std::function; - -// \return a function which may be to run the given process \p. -// May return an empty std::function in the rare case that the to-be called fish function no longer -// exists. This is just a dumb artifact of the fact that we only capture the functions name, not its -// properties, when creating the job; thus a race could delete the function before we fetch its -// properties. -static proc_performer_t get_performer_for_process(process_t *p, job_t *job, - const io_chain_t &io_chain) { - assert((p->type == process_type_t::function || p->type == process_type_t::block_node) && - "Unexpected process type"); - // We want to capture the job group. - job_group_ref_t job_group = job->group; - - if (p->type == process_type_t::block_node) { - const parsed_source_ref_t &source = *p->block_node_source; - const ast::statement_t *node = p->internal_block_node; - assert(source.has_value() && node && "Process is missing node info"); - // The lambda will convert into a std::function which requires copyability. A Box can't - // be copied, so add another indirection. - auto source_box = std::make_shared>(source.clone()); - return [=](parser_t &parser) { - return parser.eval_node(**source_box, *node, io_chain, job_group).status; - }; - } else { - assert(p->type == process_type_t::function); - auto props = function_get_props(p->argv0()); - if (!props) { - FLOGF(error, _(L"Unknown function '%ls'"), p->argv0()); - return proc_performer_t{}; - } - auto props_box = std::make_shared>(props.acquire()); - const std::vector &argv = p->argv(); - return [=](parser_t &parser) { - // Pull out the job list from the function. - const auto *func = reinterpret_cast( - (*props_box)->get_block_statement_node()); - const ast::job_list_t &body = func->jobs(); - const block_t *fb = function_prepare_environment(parser, argv, **props_box); - const auto parsed_source_raw = (*props_box)->parsed_source(); - const auto parsed_source_box = rust::Box::from_raw( - reinterpret_cast(parsed_source_raw)); - auto res = parser.eval_node(*parsed_source_box, body, io_chain, job_group); - function_restore_environment(parser, fb); - - // If the function did not execute anything, treat it as success. - if (res.was_empty) { - res = proc_status_t::from_exit_code(EXIT_SUCCESS); - } - return res.status; - }; - } -} - -/// Execute a block node or function "process". -/// \p piped_output_needs_buffering if true, buffer the output. -static launch_result_t exec_block_or_func_process(parser_t &parser, const std::shared_ptr &j, - process_t *p, io_chain_t io_chain, - bool piped_output_needs_buffering) { - // Create an output buffer if we're piping to another process. - shared_ptr block_output_bufferfill{}; - if (piped_output_needs_buffering) { - // Be careful to handle failure, e.g. too many open fds. - block_output_bufferfill = io_bufferfill_t::create(); - if (!block_output_bufferfill) { - return launch_result_t::failed; - } - // Teach the job about its bufferfill, and add it to our io chain. - io_chain.push_back(block_output_bufferfill); - } - - // Get the process performer, and just execute it directly. - // Do it in this scoped way so that the performer function can be eagerly deallocating releasing - // its captured io chain. - if (proc_performer_t performer = get_performer_for_process(p, j.get(), io_chain)) { - p->status = performer(parser); - } else { - return launch_result_t::failed; - } - - // If we have a block output buffer, populate it now. - std::string buffer_contents; - if (block_output_bufferfill) { - // Remove our write pipe and forget it. This may close the pipe, unless another thread has - // claimed it (background write) or another process has inherited it. - io_chain.remove(block_output_bufferfill); - buffer_contents = - io_bufferfill_t::finish(std::move(block_output_bufferfill)).newline_serialized(); - } - - run_internal_process_or_short_circuit(parser, j, p, std::move(buffer_contents), - {} /* errdata */, io_chain); - return launch_result_t::ok; -} - -static proc_performer_t get_performer_for_builtin( - process_t *p, job_t *job, const io_chain_t &io_chain, - const std::shared_ptr &output_stream, - const std::shared_ptr &errput_stream) { - assert(p->type == process_type_t::builtin && "Process must be a builtin"); - - // Determine if we have a "direct" redirection for stdin. - bool stdin_is_directly_redirected = false; - if (!p->is_first_in_job) { - // We must have a pipe - stdin_is_directly_redirected = true; - } else { - // We are not a pipe. Check if there is a redirection local to the process - // that's not io_mode_t::close. - for (size_t i = 0; i < p->redirection_specs().size(); i++) { - const auto *redir = p->redirection_specs().at(i); - if (redir->fd() == STDIN_FILENO && !redir->is_close()) { - stdin_is_directly_redirected = true; - break; - } - } - } - - // Pull out some fields which we want to copy. We don't want to store the process or job in the - // returned closure. - job_group_ref_t job_group = job->group; - const std::vector &argv = p->argv(); - - // Be careful to not capture p or j by value, as the intent is that this may be run on another - // thread. - return [=](parser_t &parser) { - auto out_io = io_chain.io_for_fd(STDOUT_FILENO); - auto err_io = io_chain.io_for_fd(STDERR_FILENO); - - // Figure out what fd to use for the builtin's stdin. - int local_builtin_stdin = STDIN_FILENO; - if (const auto in = io_chain.io_for_fd(STDIN_FILENO)) { - // Ignore fd redirections from an fd other than the - // standard ones. e.g. in source <&3 don't actually read from fd 3, - // which is internal to fish. We still respect this redirection in - // that we pass it on as a block IO to the code that source runs, - // and therefore this is not an error. - bool ignore_redirect = in->io_mode == io_mode_t::fd && in->source_fd >= 3; - if (!ignore_redirect) { - local_builtin_stdin = in->source_fd; - } - } - - // Populate our io_streams_t. This is a bag of information for the builtin. - io_streams_t streams{*output_stream, *errput_stream}; - streams.job_group = job_group; - streams.stdin_fd = local_builtin_stdin; - streams.stdin_is_directly_redirected = stdin_is_directly_redirected; - streams.out_is_redirected = out_io != nullptr; - streams.err_is_redirected = err_io != nullptr; - streams.out_is_piped = (out_io && out_io->io_mode == io_mode_t::pipe); - streams.err_is_piped = (err_io && err_io->io_mode == io_mode_t::pipe); - streams.io_chain = &io_chain; - - // Execute the builtin. - return builtin_run(parser, argv, streams); - }; -} - -/// Executes a builtin "process". -static launch_result_t exec_builtin_process(parser_t &parser, const std::shared_ptr &j, - process_t *p, const io_chain_t &io_chain, - bool piped_output_needs_buffering) { - assert(p->type == process_type_t::builtin && "Process is not a builtin"); - std::shared_ptr out = - create_output_stream_for_builtin(STDOUT_FILENO, io_chain, piped_output_needs_buffering); - std::shared_ptr err = - create_output_stream_for_builtin(STDERR_FILENO, io_chain, piped_output_needs_buffering); - - if (proc_performer_t performer = get_performer_for_builtin(p, j.get(), io_chain, out, err)) { - p->status = performer(parser); - } else { - return launch_result_t::failed; - } - handle_builtin_output(parser, j, p, io_chain, *out, *err); - return launch_result_t::ok; -} - -/// Executes a process \p \p in \p job, using the pipes \p pipes (which may have invalid fds if this -/// is the first or last process). -/// \p deferred_pipes represents the pipes from our deferred process; if set ensure they get closed -/// in any child. If \p is_deferred_run is true, then this is a deferred run; this affects how -/// certain buffering works. -/// An error return here indicates that the process failed to launch, and the rest of -/// the pipeline should be cancelled. -static launch_result_t exec_process_in_job(parser_t &parser, process_t *p, - const std::shared_ptr &j, - const io_chain_t &block_io, autoclose_pipes_t pipes, - const autoclose_pipes_t &deferred_pipes, - bool is_deferred_run = false) { - // The write pipe (destined for stdout) needs to occur before redirections. For example, - // with a redirection like this: - // - // `foo 2>&1 | bar` - // - // what we want to happen is this: - // - // dup2(pipe, stdout) - // dup2(stdout, stderr) - // - // so that stdout and stderr both wind up referencing the pipe. - // - // The read pipe (destined for stdin) is more ambiguous. Imagine a pipeline like this: - // - // echo alpha | cat < beta.txt - // - // Should cat output alpha or beta? bash and ksh output 'beta', tcsh gets it right and - // complains about ambiguity, and zsh outputs both (!). No shells appear to output 'alpha', - // so we match bash here. That would mean putting the pipe first, so that it gets trumped by - // the file redirection. - // - // However, eval does this: - // - // echo "begin; $argv "\n" ;end <&3 3<&-" | source 3<&0 - // - // which depends on the redirection being evaluated before the pipe. So the write end of the - // pipe comes first, the read pipe of the pipe comes last. See issue #966. - - // Maybe trace this process. - // TODO: 'and' and 'or' will not show. - trace_if_enabled(parser, L"", p->argv()); - - // The IO chain for this process. - io_chain_t process_net_io_chain = block_io; - - if (pipes.write.valid()) { - process_net_io_chain.push_back(std::make_shared( - p->pipe_write_fd, false /* not input */, std::move(pipes.write))); - } - - // Append IOs from the process's redirection specs. - // This may fail, e.g. a failed redirection. - if (!process_net_io_chain.append_from_specs(p->redirection_specs(), - parser.vars().get_pwd_slash())) { - return launch_result_t::failed; - } - - // Read pipe goes last. - shared_ptr pipe_read{}; - if (pipes.read.valid()) { - pipe_read = - std::make_shared(STDIN_FILENO, true /* input */, std::move(pipes.read)); - process_net_io_chain.push_back(pipe_read); - } - - // If we have stashed pipes, make sure those get closed in the child. - for (const autoclose_fd_t *afd : {&deferred_pipes.read, &deferred_pipes.write}) { - if (afd->valid()) { - process_net_io_chain.push_back(std::make_shared(afd->fd())); - } - } - - if (p->type != process_type_t::block_node) { - // A simple `begin ... end` should not be considered an execution of a command. - parser.libdata().exec_count++; - } - - const block_t *block = nullptr; - cleanup_t pop_block([&]() { - if (block) parser.pop_block(block); - }); - if (!p->variable_assignments.empty()) { - block = parser.push_block(block_t::variable_assignment_block()); - } - for (const auto &assignment : p->variable_assignments) { - parser.vars().set(assignment.variable_name, ENV_LOCAL | ENV_EXPORT, assignment.values); - } - - // Decide if outputting to a pipe may deadlock. - // This happens if fish pipes from an internal process into another internal process: - // echo $big | string match... - // Here fish will only run one process at a time, so the pipe buffer may overfill. - // It may also happen when piping internal -> external: - // echo $big | external_proc - // fish wants to run `echo` before launching external_proc, so the pipe may deadlock. - // However if we are a deferred run, it means that we are piping into an external process - // which got launched before us! - bool piped_output_needs_buffering = !p->is_last_in_job && !is_deferred_run; - - // Execute the process. - p->check_generations_before_launch(); - switch (p->type) { - case process_type_t::function: - case process_type_t::block_node: { - if (exec_block_or_func_process(parser, j, p, process_net_io_chain, - piped_output_needs_buffering) == - launch_result_t::failed) { - return launch_result_t::failed; - } - break; - } - - case process_type_t::builtin: { - if (exec_builtin_process(parser, j, p, process_net_io_chain, - piped_output_needs_buffering) == launch_result_t::failed) { - return launch_result_t::failed; - } - break; - } - - case process_type_t::external: { - if (exec_external_command(parser, j, p, process_net_io_chain) == - launch_result_t::failed) { - return launch_result_t::failed; - } - // It's possible (though unlikely) that this is a background process which recycled a - // pid from another, previous background process. Forget any such old process. - parser.get_wait_handles_ffi()->remove_by_pid(p->pid); - break; - } - - case process_type_t::exec: { - // We should have handled exec up above. - DIE("process_type_t::exec process found in pipeline, where it should never be. " - "Aborting."); - } - } - return launch_result_t::ok; -} - -// Do we have a fish internal process that pipes into a real process? If so, we are going to -// launch it last (if there's more than one, just the last one). That is to prevent buffering -// from blocking further processes. See #1396. -// Example: -// for i in (seq 1 5); sleep 1; echo $i; end | cat -// This should show the output as it comes, not buffer until the end. -// Any such process (only one per job) will be called the "deferred" process. -static process_t *get_deferred_process(const shared_ptr &j) { - // Common case is no deferred proc. - if (j->processes.size() <= 1) return nullptr; - - // Skip execs, which can only appear at the front. - if (j->processes.front()->type == process_type_t::exec) return nullptr; - - // Find the last non-external process, and return it if it pipes into an extenal process. - for (auto i = j->processes.rbegin(); i != j->processes.rend(); ++i) { - process_t *p = i->get(); - if (p->type != process_type_t::external) { - return p->is_last_in_job ? nullptr : p; - } - } - return nullptr; -} - -/// Given that we failed to execute process \p failed_proc in job \p job, mark that process and -/// every subsequent process in the pipeline as aborted before launch. -static void abort_pipeline_from(const shared_ptr &job, const process_t *failed_proc) { - bool found = false; - for (process_ptr_t &p : job->processes) { - found = found || (p.get() == failed_proc); - if (found) p->mark_aborted_before_launch(); - } - assert(found && "Process not present in job"); -} - -// Given that we are about to execute an exec() call, check if the parser is interactive and there -// are extant background jobs. If so, warn the user and do not exec(). -// \return true if we should allow exec, false to disallow it. -static bool allow_exec_with_background_jobs(parser_t &parser) { - // If we're not interactive, we cannot warn. - if (!parser.is_interactive()) return true; - - // Construct the list of running background jobs. - job_list_t bgs = jobs_requiring_warning_on_exit(parser); - if (bgs.empty()) return true; - - // Compare run counts, so we only warn once. - uint64_t current_run_count = reader_run_count(); - uint64_t &last_exec_run_count = parser.libdata().last_exec_run_counter; - if (isatty(STDIN_FILENO) && current_run_count - 1 != last_exec_run_count) { - print_exit_warning_for_jobs(bgs); - last_exec_run_count = current_run_count; - return false; - } else { - hup_jobs(parser.jobs()); - return true; - } -} - -bool exec_job(parser_t &parser, const shared_ptr &j, const io_chain_t &block_io) { - assert(j && "null job_t passed to exec_job!"); - - // If fish was invoked with -n or --no-execute, then no_exec will be set and we do nothing. - if (no_exec()) { - return true; - } - - // Handle an exec call. - if (j->processes.front()->type == process_type_t::exec) { - // If we are interactive, perhaps disallow exec if there are background jobs. - if (!allow_exec_with_background_jobs(parser)) { - for (const auto &p : j->processes) { - p->mark_aborted_before_launch(); - } - return false; - } - - // Apply foo=bar variable assignments - for (const auto &assignment : j->processes.front()->variable_assignments) { - parser.vars().set(assignment.variable_name, ENV_LOCAL | ENV_EXPORT, assignment.values); - } - - internal_exec(parser.vars(), j.get(), block_io); - // internal_exec only returns if it failed to set up redirections. - // In case of an successful exec, this code is not reached. - int status = j->flags().negate ? 0 : 1; - parser.set_last_statuses(statuses_t::just(status)); - - // A false return tells the caller to remove the job from the list. - for (const auto &p : j->processes) { - p->mark_aborted_before_launch(); - } - return false; - } - auto timer = push_timer(j->wants_timing() && !no_exec()); - - // Get the deferred process, if any. We will have to remember its pipes. - autoclose_pipes_t deferred_pipes; - process_t *const deferred_process = get_deferred_process(j); - - // We may want to transfer tty ownership to the pgroup leader. - tty_transfer_t transfer{}; - - // This loop loops over every process_t in the job, starting it as appropriate. This turns out - // to be rather complex, since a process_t can be one of many rather different things. - // - // The loop also has to handle pipelining between the jobs. - // - // We can have up to three pipes "in flight" at a time: - // - // 1. The pipe the current process should read from (courtesy of the previous process) - // 2. The pipe that the current process should write to - // 3. The pipe that the next process should read from (courtesy of us) - // - // Lastly, a process may experience a pipeline-aborting error, which prevents launching - // further processes in the pipeline. - autoclose_fd_t pipe_next_read; - bool aborted_pipeline = false; - size_t procs_launched = 0; - for (const auto &procptr : j->processes) { - process_t *p = procptr.get(); - - // proc_pipes is the pipes applied to this process. That is, it is the read end - // containing the output of the previous process (if any), plus the write end that will - // output to the next process (if any). - autoclose_pipes_t proc_pipes; - proc_pipes.read = std::move(pipe_next_read); - if (!p->is_last_in_job) { - auto pipes = make_autoclose_pipes(); - if (!pipes) { - FLOGF(warning, PIPE_ERROR); - wperror(L"pipe"); - aborted_pipeline = true; - abort_pipeline_from(j, p); - break; - } - pipe_next_read = std::move(pipes->read); - proc_pipes.write = std::move(pipes->write); - - // Save any deferred process for last. By definition, the deferred process can never be - // the last process in the job, so it's safe to nest this in the outer - // `if (!p->is_last_in_job)` block, which makes it clear that `proc_next_read` will - // always be assigned when we `continue` the loop. - if (p == deferred_process) { - deferred_pipes = std::move(proc_pipes); - continue; - } - } - - // Regular process. - if (exec_process_in_job(parser, p, j, block_io, std::move(proc_pipes), deferred_pipes) == - launch_result_t::failed) { - aborted_pipeline = true; - abort_pipeline_from(j, p); - break; - } - procs_launched += 1; - - // Transfer tty? - if (p->leads_pgrp && j->group->wants_terminal()) { - transfer.to_job_group(j->group); - } - } - pipe_next_read.close(); - - // If our pipeline was aborted before any process was successfully launched, then there is - // nothing to reap, and we can perform an early return. - // Note we must never return false if we have launched even one process, since it will not be - // properly reaped; see #7038. - if (aborted_pipeline && procs_launched == 0) { - return false; - } - - // Ok, at least one thing got launched. - // Handle any deferred process. - if (deferred_process) { - if (aborted_pipeline) { - // Some other process already aborted our pipeline. - deferred_process->mark_aborted_before_launch(); - } else if (exec_process_in_job(parser, deferred_process, j, block_io, - std::move(deferred_pipes), {}, - true) == launch_result_t::failed) { - // The deferred proc itself failed to launch. - deferred_process->mark_aborted_before_launch(); - } - } - - FLOGF(exec_job_exec, L"Executed job %d from command '%ls'", j->job_id(), j->command_wcstr()); - - j->mark_constructed(); - - // If exec_error then a backgrounded job would have been terminated before it was ever assigned - // a pgroup, so error out before setting last_pid. - if (!j->is_foreground()) { - maybe_t last_pid = j->get_last_pid(); - if (last_pid.has_value()) { - parser.vars().set_one(L"last_pid", ENV_GLOBAL, to_string(*last_pid)); - } else { - parser.vars().set_empty(L"last_pid", ENV_GLOBAL); - } - } - - if (!j->is_initially_background()) { - j->continue_job(parser); - } - - if (j->is_stopped()) transfer.save_tty_modes(); - transfer.reclaim(); - return true; -} - -/// Populate \p lst with the output of \p buffer, perhaps splitting lines according to \p split. -static void populate_subshell_output(std::vector *lst, const separated_buffer_t &buffer, - bool split) { - // Walk over all the elements. - for (const auto &elem : buffer.elements()) { - if (elem.is_explicitly_separated()) { - // Just append this one. - lst->push_back(str2wcstring(elem.contents)); - continue; - } - - // Not explicitly separated. We have to split it explicitly. - assert(!elem.is_explicitly_separated() && "should not be explicitly separated"); - const char *begin = elem.contents.data(); - const char *end = begin + elem.contents.size(); - if (split) { - const char *cursor = begin; - while (cursor < end) { - // Look for the next separator. - auto stop = static_cast(std::memchr(cursor, '\n', end - cursor)); - const bool hit_separator = (stop != nullptr); - if (!hit_separator) { - // If it's not found, just use the end. - stop = end; - } - // Stop now points at the first character we do not want to copy. - lst->push_back(str2wcstring(cursor, stop - cursor)); - - // If we hit a separator, skip over it; otherwise we're at the end. - cursor = stop + (hit_separator ? 1 : 0); - } - } else { - // We're not splitting output, but we still want to trim off a trailing newline. - if (end != begin && end[-1] == '\n') { - --end; - } - lst->push_back(str2wcstring(begin, end - begin)); - } - } -} - -/// Execute \p cmd in a subshell in \p parser. If \p lst is not null, populate it with the output. -/// Return $status in \p out_status. -/// If \p job_group is set, any spawned commands should join that job group. -/// If \p apply_exit_status is false, then reset $status back to its original value. -/// \p is_subcmd controls whether we apply a read limit. -/// \p break_expand is used to propagate whether the result should be "expansion breaking" in the -/// sense that subshells used during string expansion should halt that expansion. \return the value -/// of $status. -static int exec_subshell_internal(const wcstring &cmd, parser_t &parser, - const job_group_ref_t &job_group, std::vector *lst, - bool *break_expand, bool apply_exit_status, bool is_subcmd) { - parser.assert_can_execute(); - auto &ld = parser.libdata(); - - scoped_push is_subshell(&ld.is_subshell, true); - scoped_push read_limit(&ld.read_limit, is_subcmd ? READ_BYTE_LIMIT : 0); - - auto prev_statuses = parser.get_last_statuses(); - const cleanup_t put_back([&] { - if (!apply_exit_status) { - parser.set_last_statuses(prev_statuses); - } - }); - - const bool split_output = parser.vars().get_unless_empty(L"IFS").has_value(); - - // IO buffer creation may fail (e.g. if we have too many open files to make a pipe), so this may - // be null. - auto bufferfill = io_bufferfill_t::create(ld.read_limit); - if (!bufferfill) { - *break_expand = true; - return STATUS_CMD_ERROR; - } - eval_res_t eval_res = - parser.eval_with(cmd, io_chain_t{bufferfill}, job_group, block_type_t::subst); - separated_buffer_t buffer = io_bufferfill_t::finish(std::move(bufferfill)); - if (buffer.discarded()) { - *break_expand = true; - return STATUS_READ_TOO_MUCH; - } - - if (eval_res.break_expand) { - *break_expand = true; - return eval_res.status.status_value(); - } - - if (lst) { - populate_subshell_output(lst, buffer, split_output); - } - *break_expand = false; - return eval_res.status.status_value(); -} - -int exec_subshell_for_expand(const wcstring &cmd, parser_t &parser, - const job_group_ref_t &job_group, std::vector &outputs) { - parser.assert_can_execute(); - bool break_expand = false; - int ret = exec_subshell_internal(cmd, parser, job_group, &outputs, &break_expand, true, true); - // Only return an error code if we should break expansion. - return break_expand ? ret : STATUS_CMD_OK; -} - -int exec_subshell(const wcstring &cmd, parser_t &parser, bool apply_exit_status) { - bool break_expand = false; - return exec_subshell_internal(cmd, parser, nullptr, nullptr, &break_expand, apply_exit_status, - false); -} - -int exec_subshell(const wcstring &cmd, parser_t &parser, std::vector &outputs, - bool apply_exit_status) { - bool break_expand = false; - return exec_subshell_internal(cmd, parser, nullptr, &outputs, &break_expand, apply_exit_status, - false); -} - -int exec_subshell_ffi(const wcstring &cmd, parser_t &parser, wcstring_list_ffi_t &outputs, - bool apply_exit_status) { - return exec_subshell(cmd, parser, outputs.vals, apply_exit_status); -} diff --git a/src/exec.h b/src/exec.h index e9212c363..0ae87dea8 100644 --- a/src/exec.h +++ b/src/exec.h @@ -1,51 +1,3 @@ -// Prototypes for functions for executing a program. -#ifndef FISH_EXEC_H -#define FISH_EXEC_H - -#include "config.h" - -#include -#include - -#include "flog.h" -#include "io.h" -#include "proc.h" - -class Parser; using parser_t = Parser; - -/// Execute the processes specified by \p j in the parser \p. -/// On a true return, the job was successfully launched and the parser will take responsibility for -/// cleaning it up. On a false return, the job could not be launched and the caller must clean it -/// up. -__warn_unused bool exec_job(parser_t &parser, const std::shared_ptr &j, - const io_chain_t &block_io); - -/// Evaluate a command. -/// -/// \param cmd the command to execute -/// \param parser the parser with which to execute code -/// \param outputs the list to insert output into. -/// \param apply_exit_status if set, update $status within the parser, otherwise do not. -/// -/// \return a value appropriate for populating $status. -int exec_subshell(const wcstring &cmd, parser_t &parser, bool apply_exit_status); -int exec_subshell(const wcstring &cmd, parser_t &parser, std::vector &outputs, - bool apply_exit_status); -int exec_subshell_ffi(const wcstring &cmd, parser_t &parser, wcstring_list_ffi_t &outputs, - bool apply_exit_status); - -/// Like exec_subshell, but only returns expansion-breaking errors. That is, a zero return means -/// "success" (even though the command may have failed), a non-zero return means that we should -/// halt expansion. If the \p pgid is supplied, then any spawned external commands should join that -/// pgroup. -int exec_subshell_for_expand(const wcstring &cmd, parser_t &parser, - const job_group_ref_t &job_group, std::vector &outputs); - -/// Add signals that should be masked for external processes in this job. -bool blocked_signals_for_job(const job_t &job, sigset_t *sigmask); - -/// This function checks the beginning of a file to see if it's safe to -/// pass to the system interpreter when execve() returns ENOEXEC. -bool is_thompson_shell_script(const char *path); - +#if INCLUDE_RUST_HEADERS +#include "exec.rs.h" #endif diff --git a/src/expand.cpp b/src/expand.cpp index 328a937de..c65f7dfec 100644 --- a/src/expand.cpp +++ b/src/expand.cpp @@ -1,1301 +1,11 @@ -// String expansion functions. These functions perform several kinds of parameter expansion. -#include "config.h" // IWYU pragma: keep - -#include -#include -#include -#include -#include - -#include -#include - -#ifdef SunOS -#include -#endif - -#include -#include -#include -#include -#include -#include - -#include "common.h" -#include "complete.h" -#include "env.h" -#include "exec.h" #include "expand.h" -#include "fallback.h" // IWYU pragma: keep -#include "history.h" -#include "operation_context.h" -#include "parse_constants.h" -#include "parse_util.h" -#include "parser.h" -#include "path.h" -#include "threads.rs.h" -#include "util.h" -#include "wcstringutil.h" -#include "wildcard.h" -#include "wutil.h" // IWYU pragma: keep -/// Characters which make a string unclean if they are the first character of the string. See \c -/// expand_is_clean(). -#define UNCLEAN_FIRST L"~%" -/// Unclean characters. See \c expand_is_clean(). -#define UNCLEAN L"$*?\\\"'({})" - -static void remove_internal_separator(wcstring *s, bool conv); - -/// Test if the specified argument is clean, i.e. it does not contain any tokens which need to be -/// expanded or otherwise altered. Clean strings can be passed through expand_string and expand_one -/// without changing them. About two thirds of all strings are clean, so skipping expansion on them -/// actually does save a small amount of time, since it avoids multiple memory allocations during -/// the expansion process. -/// -/// \param in the string to test -static bool expand_is_clean(const wcstring &in) { - if (in.empty()) return true; - - // Test characters that have a special meaning in the first character position. - if (std::wcschr(UNCLEAN_FIRST, in.at(0)) != nullptr) return false; - - // Test characters that have a special meaning in any character position. - return in.find_first_of(UNCLEAN) == wcstring::npos; -} - -/// Append a syntax error to the given error list. -static void append_syntax_error(parse_error_list_t *errors, size_t source_start, const wchar_t *fmt, - ...) { - if (!errors) return; - - parse_error_t error; - error.source_start = source_start; - error.source_length = 0; - error.code = parse_error_code_t::syntax; - - va_list va; - va_start(va, fmt); - error.text = std::make_unique(vformat_string(fmt, va)); - va_end(va); - - errors->push_back(std::move(error)); -} - -/// Append a cmdsub error to the given error list. But only do so if the error hasn't already been -/// recorded. This is needed because command substitution is a recursive process and some errors -/// could consequently be recorded more than once. -static void append_cmdsub_error(parse_error_list_t *errors, size_t source_start, size_t source_end, - const wchar_t *fmt, ...) { - if (!errors) return; - - parse_error_t error; - error.source_start = source_start; - error.source_length = source_end - source_start + 1; - error.code = parse_error_code_t::cmdsubst; - - va_list va; - va_start(va, fmt); - error.text = std::make_unique(vformat_string(fmt, va)); - va_end(va); - - for (size_t i = 0; i < errors->size(); i++) { - if (*error.text == *errors->at(i)->text()) return; - } - - errors->push_back(std::move(error)); -} - -/// Append an overflow error, when expansion produces too much data. -static expand_result_t append_overflow_error(parse_error_list_t *errors, - size_t source_start = SOURCE_LOCATION_UNKNOWN) { - if (errors) { - parse_error_t error; - error.source_start = source_start; - error.source_length = 0; - error.code = parse_error_code_t::generic; - error.text = std::make_unique(_(L"Expansion produced too many results")); - errors->push_back(std::move(error)); - } - return expand_result_t::make_error(STATUS_EXPAND_ERROR); -} - -/// Test if the specified string does not contain character which can not be used inside a quoted -/// string. -static bool is_quotable(const wcstring &str) { - return str.find_first_of(L"\n\t\r\b\x1B") == wcstring::npos; -} - -wcstring expand_escape_variable(const env_var_t &var) { - wcstring buff; - const std::vector &lst = var.as_list(); - - for (size_t j = 0; j < lst.size(); j++) { - const wcstring &el = lst.at(j); - if (j) buff.append(L" "); - - // We want to use quotes if we have more than one string, or the string contains a space. - bool prefer_quotes = lst.size() > 1 || el.find(L' ') != wcstring::npos; - if (prefer_quotes && is_quotable(el)) { - buff.append(L"'"); - buff.append(el); - buff.append(L"'"); - } else { - buff.append(escape_string(el)); - } - } - - return buff; -} - -wcstring expand_escape_string(const wcstring &el) { - wcstring buff; - bool prefer_quotes = el.find(L' ') != wcstring::npos; - if (prefer_quotes && is_quotable(el)) { - buff.append(L"'"); - buff.append(el); - buff.append(L"'"); - } else { - buff.append(escape_string(el)); - } - return buff; -} - -enum class parse_slice_error_t { - none, - zero_index, - invalid_index, -}; - -/// Parse an array slicing specification Returns 0 on success. If a parse error occurs, returns the -/// index of the bad token. Note that 0 can never be a bad index because the string always starts -/// with [. -static size_t parse_slice(const wchar_t *const in, wchar_t **const end_ptr, std::vector &idx, - size_t array_size, parse_slice_error_t *const error) { - const long size = static_cast(array_size); - size_t pos = 1; // skip past the opening square bracket - - *error = parse_slice_error_t::none; - - while (true) { - while (iswspace(in[pos]) || (in[pos] == INTERNAL_SEPARATOR)) pos++; - if (in[pos] == L']') { - pos++; - break; - } - - const wchar_t *end; - long tmp; - if (idx.empty() && in[pos] == L'.' && in[pos + 1] == L'.') { - // If we are at the first index expression, a missing start-index means the range starts - // at the first item. - tmp = 1; // first index - end = &in[pos]; - } else { - tmp = fish_wcstol(&in[pos], &end); - if (errno > 0) { - // We don't test `*end` as is typically done because we expect it to not be the null - // char. Ignore the case of errno==-1 because it means the end char wasn't the null - // char. - *error = parse_slice_error_t::invalid_index; - return pos; - } else if (tmp == 0) { - // Explicitly refuse $foo[0] as valid syntax, regardless of whether or not we're - // going to show an error if the index ultimately evaluates to zero. This will help - // newcomers to fish avoid a common off-by-one error. See #4862. - *error = parse_slice_error_t::zero_index; - return pos; - } - } - - long i1 = tmp > -1 ? tmp : size + tmp + 1; - pos = end - in; - while (in[pos] == INTERNAL_SEPARATOR) pos++; - if (in[pos] == L'.' && in[pos + 1] == L'.') { - pos += 2; - while (in[pos] == INTERNAL_SEPARATOR) pos++; - while (iswspace(in[pos])) pos++; // Allow the space in "[.. ]". - - long tmp1; - // If we are at the last index range expression then a missing end-index means the - // range spans until the last item. - if (in[pos] == L']') { - tmp1 = -1; // last index - end = &in[pos]; - } else { - tmp1 = fish_wcstol(&in[pos], &end); - // Ignore the case of errno==-1 because it means the end char wasn't the null char. - if (errno > 0) { - *error = parse_slice_error_t::invalid_index; - return pos; - } else if (tmp1 == 0) { - *error = parse_slice_error_t::zero_index; - return pos; - } - } - pos = end - in; - - long i2 = tmp1 > -1 ? tmp1 : size + tmp1 + 1; - // Skip sequences that are entirely outside. - // This means "17..18" expands to nothing if there are less than 17 elements. - if (i1 > size && i2 > size) { - continue; - } - short direction = i2 < i1 ? -1 : 1; - // If only the beginning is negative, always go reverse. - // If only the end, always go forward. - // Prevents `[x..-1]` from going reverse if less than x elements are there. - if ((tmp1 > -1) != (tmp > -1)) { - direction = tmp1 > -1 ? -1 : 1; - } else { - // Clamp to array size when not forcing direction - // - otherwise "2..-1" clamps both to 1 and then becomes "1..1". - i1 = i1 < size ? i1 : size; - i2 = i2 < size ? i2 : size; - } - for (long jjj = i1; jjj * direction <= i2 * direction; jjj += direction) { - // FLOGF(error, L"Expand range [subst]: %i\n", jjj); - idx.push_back(jjj); - } - continue; - } - - idx.push_back(i1); - } - - if (end_ptr) { - *end_ptr = const_cast(in + pos); - } - - return 0; -} - -/// Expand all environment variables in the string *ptr. -/// -/// This function is slow, fragile and complicated. There are lots of little corner cases, like -/// $$foo should do a double expansion, $foo$bar should not double expand bar, etc. -/// -/// This function operates on strings backwards, starting at last_idx. -/// -/// Note: last_idx is considered to be where it previously finished processing. This means it -/// actually starts operating on last_idx-1. As such, to process a string fully, pass string.size() -/// as last_idx instead of string.size()-1. -/// -/// \return the result of expansion. -static expand_result_t expand_variables(wcstring instr, completion_receiver_t *out, size_t last_idx, - const environment_t &vars, parse_error_list_t *errors) { - const size_t insize = instr.size(); - - // last_idx may be 1 past the end of the string, but no further. - assert(last_idx <= insize && "Invalid last_idx"); - if (last_idx == 0) { - if (!out->add(std::move(instr))) { - return append_overflow_error(errors); - } - return expand_result_t::ok; - } - - // Locate the last VARIABLE_EXPAND or VARIABLE_EXPAND_SINGLE - bool is_single = false; - size_t varexp_char_idx = last_idx; - while (varexp_char_idx--) { - const wchar_t c = instr.at(varexp_char_idx); - if (c == VARIABLE_EXPAND || c == VARIABLE_EXPAND_SINGLE) { - is_single = (c == VARIABLE_EXPAND_SINGLE); - break; - } - } - if (varexp_char_idx >= instr.size()) { - // No variable expand char, we're done. - if (!out->add(std::move(instr))) { - return append_overflow_error(errors); - } - return expand_result_t::ok; - } - - // Get the variable name. - const size_t var_name_start = varexp_char_idx + 1; - size_t var_name_stop = var_name_start; - while (var_name_stop < insize) { - const wchar_t nc = instr.at(var_name_stop); - if (nc == VARIABLE_EXPAND_EMPTY) { - var_name_stop++; - break; - } - if (!valid_var_name_char(nc)) break; - var_name_stop++; - } - assert(var_name_stop >= var_name_start && "Bogus variable name indexes"); - const size_t var_name_len = var_name_stop - var_name_start; - - // It's an error if the name is empty. - if (var_name_len == 0) { - if (errors) { - parse_util_expand_variable_error(instr, 0 /* global_token_pos */, varexp_char_idx, - errors); - } - return expand_result_t::make_error(STATUS_EXPAND_ERROR); - } - - // Get the variable name as a string, then try to get the variable from env. - const wcstring var_name(instr, var_name_start, var_name_len); - // Do a dirty hack to make sliced history fast (#4650). We expand from either a variable, or a - // history_t. Note that "history" is read only in env.cpp so it's safe to special-case it in - // this way (it cannot be shadowed, etc). - std::shared_ptr history{}; - maybe_t var{}; - if (var_name == L"history") { - if (is_main_thread()) { - history = history_t::with_name(history_session_id(vars)); - } - } else if (var_name != wcstring{VARIABLE_EXPAND_EMPTY}) { - var = vars.get(var_name); - } - - // Parse out any following slice. - // Record the end of the variable name and any following slice. - size_t var_name_and_slice_stop = var_name_stop; - bool all_values = true; - const size_t slice_start = var_name_stop; - std::vector var_idx_list; - if (slice_start < insize && instr.at(slice_start) == L'[') { - all_values = false; - const wchar_t *in = instr.c_str(); - wchar_t *slice_end; - // If a variable is missing, behave as though we have one value, so that $var[1] always - // works. - size_t effective_val_count = 1; - if (var) { - effective_val_count = var->as_list().size(); - } else if (history) { - effective_val_count = history->size(); - } - parse_slice_error_t parse_error; - size_t bad_pos = parse_slice(in + slice_start, &slice_end, var_idx_list, - effective_val_count, &parse_error); - if (bad_pos != 0) { - switch (parse_error) { - case parse_slice_error_t::none: - assert(false && "bad_pos != 0 but parse_slice_error_t::none!"); - break; - case parse_slice_error_t::zero_index: - append_syntax_error(errors, slice_start + bad_pos, - L"array indices start at 1, not 0."); - break; - case parse_slice_error_t::invalid_index: - append_syntax_error(errors, slice_start + bad_pos, L"Invalid index value"); - break; - } - return expand_result_t::make_error(STATUS_EXPAND_ERROR); - } - var_name_and_slice_stop = (slice_end - in); - } - - if (!var && !history) { - // Expanding a non-existent variable. - if (!is_single) { - // Normal expansions of missing variables successfully expand to nothing. - return expand_result_t::ok; - } else { - // Expansion to single argument. - // Replace the variable name and slice with VARIABLE_EXPAND_EMPTY. - wcstring res(instr, 0, varexp_char_idx); - if (!res.empty() && res.back() == VARIABLE_EXPAND_SINGLE) { - res.push_back(VARIABLE_EXPAND_EMPTY); - } - res.append(instr, var_name_and_slice_stop, wcstring::npos); - return expand_variables(std::move(res), out, varexp_char_idx, vars, errors); - } - } - - // Ok, we have a variable or a history. Let's expand it. - // Start by respecting the sliced elements. - assert((var || history) && "Should have variable or history here"); - std::vector var_item_list; - if (all_values) { - if (history) { - history->get_history(var_item_list); - } else { - var->to_list(var_item_list); - } - } else { - // We have to respect the slice. - if (history) { - // Ask history to map indexes to item strings. - // Note this may have missing entries for out-of-bounds. - auto item_map = history->items_at_indexes(var_idx_list); - for (long item_index : var_idx_list) { - auto iter = item_map.find(item_index); - if (iter != item_map.end()) { - var_item_list.push_back(iter->second); - } - } - } else { - const std::vector &all_var_items = var->as_list(); - for (long item_index : var_idx_list) { - // Check that we are within array bounds. If not, skip the element. Note: - // Negative indices (`echo $foo[-1]`) are already converted to positive ones - // here, So tmp < 1 means it's definitely not in. - // Note we are 1-based. - if (item_index >= 1 && size_t(item_index) <= all_var_items.size()) { - var_item_list.push_back(all_var_items.at(item_index - 1)); - } - } - } - } - - if (is_single) { - // Quoted expansion. Here we expect the variable's delimiter. - // Note history always has a space delimiter. - wchar_t delimit = history ? L' ' : var->get_delimiter(); - wcstring res(instr, 0, varexp_char_idx); - if (!res.empty()) { - if (res.back() != VARIABLE_EXPAND_SINGLE) { - res.push_back(INTERNAL_SEPARATOR); - } else if (var_item_list.empty() || var_item_list.front().empty()) { - // First expansion is empty, but we need to recursively expand. - res.push_back(VARIABLE_EXPAND_EMPTY); - } - } - - // Append all entries in var_item_list, separated by the delimiter. - res.append(join_strings(var_item_list, delimit)); - res.append(instr, var_name_and_slice_stop, wcstring::npos); - return expand_variables(std::move(res), out, varexp_char_idx, vars, errors); - } else { - // Normal cartesian-product expansion. - for (wcstring &item : var_item_list) { - if (varexp_char_idx == 0 && var_name_and_slice_stop == insize) { - if (!out->add(std::move(item))) { - return append_overflow_error(errors); - } - } else { - wcstring new_in(instr, 0, varexp_char_idx); - if (!new_in.empty()) { - if (new_in.back() != VARIABLE_EXPAND) { - new_in.push_back(INTERNAL_SEPARATOR); - } else if (item.empty()) { - new_in.push_back(VARIABLE_EXPAND_EMPTY); - } - } - new_in.append(item); - new_in.append(instr, var_name_and_slice_stop, wcstring::npos); - auto res = expand_variables(std::move(new_in), out, varexp_char_idx, vars, errors); - if (res.result != expand_result_t::ok) { - return res; - } - } - } - } - return expand_result_t::ok; -} - -/// Perform brace expansion, placing the expanded strings into \p out. -static expand_result_t expand_braces(wcstring &&instr, expand_flags_t flags, - completion_receiver_t *out, parse_error_list_t *errors) { - bool syntax_error = false; - int brace_count = 0; - - const wchar_t *brace_begin = nullptr, *brace_end = nullptr; - const wchar_t *last_sep = nullptr; - - const wchar_t *item_begin; - size_t length_preceding_braces, length_following_braces, tot_len; - - const wchar_t *const in = instr.c_str(); - - // Locate the first non-nested brace pair. - for (const wchar_t *pos = in; (*pos) && !syntax_error; pos++) { - switch (*pos) { - case BRACE_BEGIN: { - if (brace_count == 0) brace_begin = pos; - brace_count++; - break; - } - case BRACE_END: { - brace_count--; - if (brace_count < 0) { - syntax_error = true; - } else if (brace_count == 0) { - brace_end = pos; - } - break; - } - case BRACE_SEP: { - if (brace_count == 1) last_sep = pos; - break; - } - default: { - break; // we ignore all other characters here - } - } - } - - if (brace_count > 0) { - if (!(flags & expand_flag::for_completions)) { - syntax_error = true; - } else { - // The user hasn't typed an end brace yet; make one up and append it, then expand - // that. - wcstring mod; - if (last_sep) { - mod.append(in, brace_begin - in + 1); - mod.append(last_sep + 1); - mod.push_back(BRACE_END); - } else { - mod.append(in); - mod.push_back(BRACE_END); - } - - // Note: this code looks very fishy, apparently it has never worked. - return expand_braces(std::move(mod), expand_flag::skip_cmdsubst, out, errors); - } - } - - if (syntax_error) { - append_syntax_error(errors, SOURCE_LOCATION_UNKNOWN, _(L"Mismatched braces")); - return expand_result_t::make_error(STATUS_EXPAND_ERROR); - } - - if (brace_begin == nullptr) { - // No more brace expansions left; we can return the value as-is. - if (!out->add(std::move(instr))) { - return expand_result_t::error; - } - return expand_result_t::ok; - } - - length_preceding_braces = (brace_begin - in); - length_following_braces = instr.size() - (brace_end - in) - 1; - tot_len = length_preceding_braces + length_following_braces; - item_begin = brace_begin + 1; - for (const wchar_t *pos = (brace_begin + 1); true; pos++) { - if (brace_count == 0 && ((*pos == BRACE_SEP) || (pos == brace_end))) { - assert(pos >= item_begin); - size_t item_len = pos - item_begin; - wcstring item = wcstring(item_begin, item_len); - item = trim(item, (const wchar_t[]){BRACE_SPACE, L'\0'}); - for (auto &c : item) { - if (c == BRACE_SPACE) { - c = ' '; - } - } - - // `whole_item` is a whitespace- and brace-stripped member of a single pass of brace - // expansion, e.g. in `{ alpha , b,{c, d }}`, `alpha`, `b`, and `c, d` will, in the - // first round of expansion, each in turn be a `whole_item` (with recursive commas - // replaced by special placeholders). - // We recursively call `expand_braces` with each item until it's been fully expanded. - wcstring whole_item; - whole_item.reserve(tot_len + item_len + 2); - whole_item.append(in, length_preceding_braces); - whole_item.append(item.begin(), item.end()); - whole_item.append(brace_end + 1); - expand_braces(std::move(whole_item), flags, out, errors); - - item_begin = pos + 1; - if (pos == brace_end) break; - } - - if (*pos == BRACE_BEGIN) { - brace_count++; - } - - if (*pos == BRACE_END) { - brace_count--; - } - } - return expand_result_t::ok; -} - -/// Expand a command substitution \p input, executing on \p ctx, and inserting the results into -/// \p out_list, or any errors into \p errors. \return an expand result. -static expand_result_t expand_cmdsubst(wcstring input, const operation_context_t &ctx, - completion_receiver_t *out, parse_error_list_t *errors) { - assert(ctx.parser && "Cannot expand without a parser"); - size_t cursor = 0; - size_t paren_begin = 0; - size_t paren_end = 0; - wcstring subcmd; - - bool is_quoted = false; - bool has_dollar = false; - switch (parse_util_locate_cmdsubst_range(input, &cursor, &subcmd, &paren_begin, &paren_end, - false, &is_quoted, &has_dollar)) { - case -1: { - append_syntax_error(errors, SOURCE_LOCATION_UNKNOWN, L"Mismatched parenthesis"); - return expand_result_t::make_error(STATUS_EXPAND_ERROR); - } - case 0: { - if (!out->add(std::move(input))) { - return append_overflow_error(errors); - } - return expand_result_t::ok; - } - case 1: { - break; - } - default: { - DIE("unhandled parse_ret value"); - } - } - - std::vector sub_res; - int subshell_status = exec_subshell_for_expand(subcmd, *ctx.parser, ctx.job_group, sub_res); - if (subshell_status != 0) { - // TODO: Ad-hoc switch, how can we enumerate the possible errors more safely? - const wchar_t *err; - switch (subshell_status) { - case STATUS_READ_TOO_MUCH: - err = L"Too much data emitted by command substitution so it was discarded"; - break; - // TODO: STATUS_CMD_ERROR is overused and too generic. We shouldn't have to test things - // to figure out what error to show after we've already been given an error code. - case STATUS_CMD_ERROR: - err = L"Too many active file descriptors"; - if (ctx.parser->is_eval_depth_exceeded()) { - err = L"Unable to evaluate string substitution"; - } - break; - case STATUS_CMD_UNKNOWN: - err = L"Unknown command"; - break; - case STATUS_ILLEGAL_CMD: - err = L"Commandname was invalid"; - break; - case STATUS_NOT_EXECUTABLE: - err = L"Command not executable"; - break; - case STATUS_INVALID_ARGS: - // TODO: Also overused - // This is sent for: - // invalid redirections or pipes (like `<&foo`), - // invalid variables (invalid name or read-only) for for-loops, - // switch $foo if $foo expands to more than one argument - // time in a background job. - err = L"Invalid arguments"; - break; - case STATUS_EXPAND_ERROR: - // Sent in `for $foo in ...` if $foo expands to more than one word - err = L"Expansion error"; - break; - case STATUS_UNMATCHED_WILDCARD: - // Sent in `for $foo in ...` if $foo expands to more than one word - err = L"Unmatched wildcard"; - break; - default: - err = L"Unknown error while evaluating command substitution"; - break; - } - append_cmdsub_error(errors, paren_begin, paren_end, _(err)); - return expand_result_t::make_error(subshell_status); - } - - // Expand slices like (cat /var/words)[1] - size_t tail_begin = paren_end + 1; - if (tail_begin < input.size() && input.at(tail_begin) == L'[') { - const wchar_t *in = input.c_str(); - std::vector slice_idx; - const wchar_t *const slice_begin = in + tail_begin; - wchar_t *slice_end = nullptr; - parse_slice_error_t parse_error; - size_t bad_pos = - parse_slice(slice_begin, &slice_end, slice_idx, sub_res.size(), &parse_error); - if (bad_pos != 0) { - switch (parse_error) { - case parse_slice_error_t::none: - assert(false && "bad_pos != 0 but parse_slice_error_t::none!"); - break; - case parse_slice_error_t::zero_index: - append_syntax_error(errors, slice_begin - in + bad_pos, - L"array indices start at 1, not 0."); - break; - case parse_slice_error_t::invalid_index: - append_syntax_error(errors, slice_begin - in + bad_pos, L"Invalid index value"); - break; - } - return expand_result_t::make_error(STATUS_EXPAND_ERROR); - } - - std::vector sub_res2; - tail_begin = slice_end - in; - for (long idx : slice_idx) { - if (static_cast(idx) > sub_res.size() || idx < 1) { - continue; - } - // -1 to convert from 1-based slice index to C++ 0-based vector index. - sub_res2.push_back(sub_res.at(idx - 1)); - } - sub_res = std::move(sub_res2); - } - - // Recursively call ourselves to expand any remaining command substitutions. The result of this - // recursive call using the tail of the string is inserted into the tail_expand array list - completion_receiver_t tail_expand_recv = out->subreceiver(); - wcstring tail = input.substr(tail_begin); - // A command substitution inside double quotes magically closes the quoted string. - // Reopen the quotes just after the command substitution. - if (is_quoted) { - tail.insert(0, L"\""); - } - expand_cmdsubst(std::move(tail), ctx, &tail_expand_recv, - errors); // TODO: offset error locations - completion_list_t tail_expand = tail_expand_recv.take(); - - // Combine the result of the current command substitution with the result of the recursive tail - // expansion. - - if (is_quoted) { - // Awkwardly reconstruct the command output. - size_t approx_size = 0; - for (const wcstring &sub_item : sub_res) { - approx_size += sub_item.size() + 1; - } - wcstring sub_res_joined; - sub_res_joined.reserve(approx_size); - for (wcstring &line : sub_res) { - sub_res_joined.append(escape_string_for_double_quotes(std::move(line))); - sub_res_joined.push_back(L'\n'); - } - // Mimic POSIX shells by stripping all trailing newlines. - if (!sub_res_joined.empty()) { - size_t i; - for (i = sub_res_joined.size(); i > 0; i--) { - if (sub_res_joined[i - 1] != L'\n') break; - } - sub_res_joined.erase(i); - } - // Instead of performing cartesian product expansion, we directly insert the command - // substitution output into the current expansion results. - for (const completion_t &tail_item : tail_expand) { - wcstring whole_item; - whole_item.reserve(paren_begin + 1 + sub_res_joined.size() + 1 + - tail_item.completion.size()); - whole_item.append(input, 0, paren_begin - has_dollar); - whole_item.push_back(INTERNAL_SEPARATOR); - whole_item.append(sub_res_joined); - whole_item.push_back(INTERNAL_SEPARATOR); - whole_item.append(tail_item.completion.substr(const_strlen(L"\""))); - if (!out->add(std::move(whole_item))) { - return append_overflow_error(errors); - } - } - - return expand_result_t::ok; - } - - for (const wcstring &sub_item : sub_res) { - wcstring sub_item2 = escape_string(sub_item); - for (const completion_t &tail_item : tail_expand) { - wcstring whole_item; - whole_item.reserve(paren_begin + 1 + sub_item2.size() + 1 + - tail_item.completion.size()); - whole_item.append(input, 0, paren_begin - has_dollar); - whole_item.push_back(INTERNAL_SEPARATOR); - whole_item.append(sub_item2); - whole_item.push_back(INTERNAL_SEPARATOR); - whole_item.append(tail_item.completion); - if (!out->add(std::move(whole_item))) { - return append_overflow_error(errors); - } - } - } - - return expand_result_t::ok; -} - -// Given that input[0] is HOME_DIRECTORY or tilde (ugh), return the user's name. Return the empty -// string if it is just a tilde. Also return by reference the index of the first character of the -// remaining part of the string (e.g. the subsequent slash). -static wcstring get_home_directory_name(const wcstring &input, size_t *out_tail_idx) { - assert(input[0] == HOME_DIRECTORY || input[0] == L'~'); - - auto pos = input.find_first_of(L'/'); - // We get the position of the /, but we need to remove it as well. - if (pos != wcstring::npos) { - *out_tail_idx = pos; - pos -= 1; - } else { - *out_tail_idx = input.length(); - } - - return input.substr(1, pos); -} - -/// Attempts tilde expansion of the string specified, modifying it in place. -static void expand_home_directory(wcstring &input, const environment_t &vars) { - if (!input.empty() && input.at(0) == HOME_DIRECTORY) { - size_t tail_idx; - wcstring username = get_home_directory_name(input, &tail_idx); - - maybe_t home; - if (username.empty()) { - // Current users home directory. - auto home_var = vars.get_unless_empty(L"HOME"); - if (!home_var) { - input.clear(); - return; - } - home = home_var->as_string(); - tail_idx = 1; - } else { - // Some other user's home directory. - std::string name_cstr = wcs2zstring(username); - struct passwd userinfo; - struct passwd *result; - char buf[8192]; - int retval = getpwnam_r(name_cstr.c_str(), &userinfo, buf, sizeof(buf), &result); - if (!retval && result) { - home = str2wcstring(userinfo.pw_dir); - } - } - - if (home) { - input.replace(input.begin(), input.begin() + tail_idx, normalize_path(*home)); - } else { - input[0] = L'~'; - } - } -} - -/// Expand the %self escape. Note this can only come at the beginning of the string. -static void expand_percent_self(wcstring &input) { - if (!input.empty() && input.front() == PROCESS_EXPAND_SELF) { - input.replace(0, 1, to_string(getpid())); - } -} - -void expand_tilde(wcstring &input, const environment_t &vars) { +/// \param input the string to tilde expand +void expand_tilde(wcstring &input, const env_stack_t &vars) { // Avoid needless COW behavior by ensuring we use const at. const wcstring &tmp = input; if (!tmp.empty() && tmp.at(0) == L'~') { input.at(0) = HOME_DIRECTORY; - expand_home_directory(input, vars); + input = *expand_home_directory(input, vars.get_impl_ffi()); } } - -// If the given path contains the user's home directory, replace that with a tilde. We don't try to -// be smart about case insensitivity, etc. -wcstring replace_home_directory_with_tilde(const wcstring &str, const environment_t &vars) { - // Only absolute paths get this treatment. - wcstring result = str; - if (string_prefixes_string(L"/", result)) { - wcstring home_directory = L"~"; - expand_tilde(home_directory, vars); - if (!string_suffixes_string(L"/", home_directory)) { - home_directory.push_back(L'/'); - } - - // Now check if the home_directory prefixes the string. - if (string_prefixes_string(home_directory, result)) { - // Success - result.replace(0, home_directory.size(), L"~/"); - } - } - return result; -} - -/// Remove any internal separators. Also optionally convert wildcard characters to regular -/// equivalents. This is done to support skip_wildcards. -static void remove_internal_separator(wcstring *str, bool conv) { - // Remove all instances of INTERNAL_SEPARATOR. - str->erase(std::remove(str->begin(), str->end(), static_cast(INTERNAL_SEPARATOR)), - str->end()); - - // If conv is true, replace all instances of ANY_STRING with '*', - // ANY_STRING_RECURSIVE with '*'. - if (conv) { - for (auto &idx : *str) { - switch (idx) { - case ANY_CHAR: { - idx = L'?'; - break; - } - case ANY_STRING: - case ANY_STRING_RECURSIVE: { - idx = L'*'; - break; - } - default: { - break; // we ignore all other characters - } - } - } - } -} - -namespace { -/// A type that knows how to perform expansions. -class expander_t { - /// Operation context for this expansion. - const operation_context_t &ctx; - - /// Flags to use during expansion. - const expand_flags_t flags; - - /// List to receive any errors generated during expansion, or null to ignore errors. - parse_error_list_t *const errors; - - /// An expansion stage is a member function pointer. - /// It accepts the input string (transferring ownership) and returns the list of output - /// completions by reference. It may return an error, which halts expansion. - using stage_t = expand_result_t (expander_t::*)(wcstring, completion_receiver_t *); - - expand_result_t stage_cmdsubst(wcstring input, completion_receiver_t *out); - expand_result_t stage_variables(wcstring input, completion_receiver_t *out); - expand_result_t stage_braces(wcstring input, completion_receiver_t *out); - expand_result_t stage_home_and_self(wcstring input, completion_receiver_t *out); - expand_result_t stage_wildcards(wcstring path_to_expand, completion_receiver_t *out); - - expander_t(const operation_context_t &ctx, expand_flags_t flags, parse_error_list_t *errors) - : ctx(ctx), flags(flags), errors(errors) {} - - // Given an original input string, if it starts with a tilde, "unexpand" the expanded home - // directory. Note this may be just a tilde or a user name like ~foo/. - void unexpand_tildes(const wcstring &input, completion_list_t *completions) const; - - public: - static expand_result_t expand_string(wcstring input, completion_receiver_t *out_completions, - expand_flags_t flags, const operation_context_t &ctx, - parse_error_list_t *errors); -}; - -expand_result_t expander_t::stage_cmdsubst(wcstring input, completion_receiver_t *out) { - if (flags & expand_flag::skip_cmdsubst) { - size_t cur = 0, start = 0, end; - switch (parse_util_locate_cmdsubst_range(input, &cur, nullptr, &start, &end, true)) { - case 0: - if (!out->add(std::move(input))) { - return append_overflow_error(errors); - } - return expand_result_t::ok; - case 1: - append_cmdsub_error(errors, start, end, - L"command substitutions not allowed here"); // clang-format off - __fallthrough__ - case -1: - // clang-format on - default: - return expand_result_t::make_error(STATUS_EXPAND_ERROR); - } - } else { - assert(ctx.parser && "Must have a parser to expand command substitutions"); - return expand_cmdsubst(std::move(input), ctx, out, errors); - } -} - -// We pass by value to match other stages. NOLINTNEXTLINE(performance-unnecessary-value-param) -expand_result_t expander_t::stage_variables(wcstring input, completion_receiver_t *out) { - // We accept incomplete strings here, since complete uses expand_string to expand incomplete - // strings from the commandline. - wcstring next; - if (auto unescaped = unescape_string(input, UNESCAPE_SPECIAL | UNESCAPE_INCOMPLETE)) - next = *unescaped; - - if (flags & expand_flag::skip_variables) { - for (auto &i : next) { - if (i == VARIABLE_EXPAND || i == VARIABLE_EXPAND_SINGLE) { - i = L'$'; - } - } - if (!out->add(std::move(next))) { - return append_overflow_error(errors); - } - return expand_result_t::ok; - } else { - size_t size = next.size(); - return expand_variables(std::move(next), out, size, ctx.vars, errors); - } -} - -expand_result_t expander_t::stage_braces(wcstring input, completion_receiver_t *out) { - return expand_braces(std::move(input), flags, out, errors); -} - -expand_result_t expander_t::stage_home_and_self(wcstring input, completion_receiver_t *out) { - expand_home_directory(input, ctx.vars); - expand_percent_self(input); - if (!out->add(std::move(input))) { - return append_overflow_error(errors); - } - return expand_result_t::ok; -} - -expand_result_t expander_t::stage_wildcards(wcstring path_to_expand, completion_receiver_t *out) { - expand_result_t result = expand_result_t::ok; - - remove_internal_separator(&path_to_expand, flags & expand_flag::skip_wildcards); - const bool has_wildcard = wildcard_has_internal(path_to_expand); // e.g. ANY_STRING - const bool for_completions = flags & expand_flag::for_completions; - const bool skip_wildcards = flags & expand_flag::skip_wildcards; - - if (has_wildcard && (flags & expand_flag::executables_only)) { - // don't do wildcard expansion for executables, see issue #785 - } else if ((for_completions && !skip_wildcards) || has_wildcard) { - // We either have a wildcard, or we don't have a wildcard but we're doing completion - // expansion (so we want to get the completion of a file path). Note that if - // skip_wildcards is set, we stomped wildcards in remove_internal_separator above, so - // there actually aren't any. - // - // So we're going to treat this input as a file path. Compute the "working directories", - // which may be CDPATH if the special flag is set. - const wcstring working_dir = ctx.vars.get_pwd_slash(); - std::vector effective_working_dirs; - bool for_cd = flags & expand_flag::special_for_cd; - bool for_command = flags & expand_flag::special_for_command; - if (!for_cd && !for_command) { - // Common case. - effective_working_dirs.push_back(working_dir); - } else { - // Either special_for_command or special_for_cd. We can handle these - // mostly the same. There's the following differences: - // - // 1. An empty CDPATH should be treated as '.', but an empty PATH should be left empty - // (no commands can be found). Also, an empty element in either is treated as '.' for - // consistency with POSIX shells. Note that we rely on the latter by having called - // `munge_colon_delimited_array()` for these special env vars. Thus we do not - // special-case them here. - // - // 2. PATH is only "one level," while CDPATH is multiple levels. That is, input like - // 'foo/bar' should resolve against CDPATH, but not PATH. - // - // In either case, we ignore the path if we start with ./ or /. Also ignore it if we are - // doing command completion and we contain a slash, per IEEE 1003.1, chapter 8 under - // PATH. - if (string_prefixes_string(L"/", path_to_expand) || - string_prefixes_string(L"./", path_to_expand) || - string_prefixes_string(L"../", path_to_expand) || - (for_command && path_to_expand.find(L'/') != wcstring::npos)) { - effective_working_dirs.push_back(working_dir); - } else { - // Get the PATH/CDPATH and CWD. Perhaps these should be passed in. An empty CDPATH - // implies just the current directory, while an empty PATH is left empty. - std::vector paths; - if (auto paths_var = ctx.vars.get(for_cd ? L"CDPATH" : L"PATH")) { - paths = paths_var->as_list(); - } - - // The current directory is always valid. - paths.emplace_back(for_cd ? L"." : L""); - for (const wcstring &next_path : paths) { - effective_working_dirs.push_back( - path_apply_working_directory(next_path, working_dir)); - } - } - } - - result = expand_result_t::wildcard_no_match; - completion_receiver_t expanded_recv = out->subreceiver(); - for (const auto &effective_working_dir : effective_working_dirs) { - wildcard_result_t expand_res = wildcard_expand_string( - path_to_expand, effective_working_dir, flags, ctx.cancel_checker, &expanded_recv); - switch (expand_res) { - case wildcard_result_t::match: - result = expand_result_t::ok; - break; - case wildcard_result_t::no_match: - break; - case wildcard_result_t::overflow: - return append_overflow_error(errors); - case wildcard_result_t::cancel: - return expand_result_t::cancel; - } - } - - completion_list_t expanded = expanded_recv.take(); - std::sort(expanded.begin(), expanded.end(), - [&](const completion_t &a, const completion_t &b) { - return wcsfilecmp_glob(a.completion.c_str(), b.completion.c_str()) < 0; - }); - if (!out->add_list(std::move(expanded))) { - result = expand_result_t::error; - } - } else { - // Can't fully justify this check. I think it's that SKIP_WILDCARDS is used when completing - // to mean don't do file expansions, so if we're not doing file expansions, just drop this - // completion on the floor. - if (!(flags & expand_flag::for_completions)) { - if (!out->add(std::move(path_to_expand))) { - return append_overflow_error(errors); - } - } - } - return result; -} - -void expander_t::unexpand_tildes(const wcstring &input, completion_list_t *completions) const { - // If input begins with tilde, then try to replace the corresponding string in each completion - // with the tilde. If it does not, there's nothing to do. - if (input.empty() || input.at(0) != L'~') return; - - // This is a subtle kludge. We need to decide whether to unexpand tildes for all - // completions, or only those which replace their tokens. The problem is that we're sloppy - // about setting the COMPLETE_REPLACES_TOKEN flag, except when we're completing in the - // wildcard stage, because no other clients of string expansion care. Example: - // HOME=/foo - // mkdir ~/foo # makes /foo/foo - // cd ~/ - // Here we are likely to get a completion 'foo' which may match $HOME, but it extends its token - // instead of replacing it, so we don't modify it (it will just be appended to the original ~/). - // - // However if we are not completing, just expanding, then expansion just produces the full paths - // so we should unconditionally unexpand tildes. - bool only_replacers = bool(flags & expand_flag::for_completions); - - // Helper to decide whether to process a completion. - auto should_process = [=](const completion_t &c) { - return only_replacers ? c.replaces_token() : true; - }; - - // Early out if none qualify. - if (std::none_of(completions->begin(), completions->end(), should_process)) return; - - // Get the username_with_tilde (like ~bert) and expand it into a home directory. - size_t tail_idx; - wcstring username_with_tilde = L"~" + get_home_directory_name(input, &tail_idx); - wcstring home = username_with_tilde; - expand_tilde(home, ctx.vars); - - // Now for each completion that starts with home, replace it with the username_with_tilde. - for (auto &comp : *completions) { - if (should_process(comp) && string_prefixes_string(home, comp.completion)) { - comp.completion.replace(0, home.size(), username_with_tilde); - - // And mark that our tilde is literal, so it doesn't try to escape it. - comp.flags |= COMPLETE_DONT_ESCAPE_TILDES; - } - } -} - -expand_result_t expander_t::expand_string(wcstring input, completion_receiver_t *out_completions, - expand_flags_t flags, const operation_context_t &ctx, - parse_error_list_t *errors) { - assert(((flags & expand_flag::skip_cmdsubst) || ctx.parser) && - "Must have a parser if not skipping command substitutions"); - // Early out. If we're not completing, and there's no magic in the input, we're done. - if (!(flags & expand_flag::for_completions) && expand_is_clean(input)) { - if (!out_completions->add(std::move(input))) { - return append_overflow_error(errors); - } - return expand_result_t::ok; - } - - expander_t expand(ctx, flags, errors); - - // Our expansion stages. - const stage_t stages[] = {&expander_t::stage_cmdsubst, &expander_t::stage_variables, - &expander_t::stage_braces, &expander_t::stage_home_and_self, - &expander_t::stage_wildcards}; - - // Load up our single initial completion. - completion_list_t completions; - append_completion(&completions, input); - - completion_receiver_t output_storage = out_completions->subreceiver(); - expand_result_t total_result = expand_result_t::ok; - for (stage_t stage : stages) { - for (completion_t &comp : completions) { - if (ctx.check_cancel()) { - total_result = expand_result_t::cancel; - break; - } - expand_result_t this_result = - (expand.*stage)(std::move(comp.completion), &output_storage); - total_result = this_result; - if (total_result == expand_result_t::error) { - break; - } - } - - // Output becomes our next stage's input. - output_storage.swap(completions); - output_storage.clear(); - if (total_result == expand_result_t::error) { - break; - } - } - - // This is a little tricky: if one wildcard failed to match but we still got output, it - // means that a previous expansion resulted in multiple strings. For example: - // set dirs ./a ./b - // echo $dirs/*.txt - // Here if ./a/*.txt matches and ./b/*.txt does not, then we don't want to report a failed - // wildcard. So swallow failed-wildcard errors if we got any output. - if (total_result == expand_result_t::wildcard_no_match && !completions.empty()) { - total_result = expand_result_t::ok; - } - - if (total_result == expand_result_t::ok) { - // Unexpand tildes if we want to preserve them (see #647). - if (flags.get(expand_flag::preserve_home_tildes)) { - expand.unexpand_tildes(input, &completions); - } - if (!out_completions->add_list(std::move(completions))) { - total_result = append_overflow_error(errors); - } - } - return total_result; -} -} // namespace - -expand_result_t expand_string(wcstring input, completion_list_t *out_completions, - expand_flags_t flags, const operation_context_t &ctx, - parse_error_list_t *errors) { - completion_receiver_t recv(std::move(*out_completions), ctx.expansion_limit); - auto res = expand_string(std::move(input), &recv, flags, ctx, errors); - *out_completions = recv.take(); - return res; -} - -expand_result_t expand_string(wcstring input, completion_receiver_t *out_completions, - expand_flags_t flags, const operation_context_t &ctx, - parse_error_list_t *errors) { - return expander_t::expand_string(std::move(input), out_completions, flags, ctx, errors); -} - -bool expand_one(wcstring &string, expand_flags_t flags, const operation_context_t &ctx, - parse_error_list_t *errors) { - completion_list_t completions; - - if (!flags.get(expand_flag::for_completions) && expand_is_clean(string)) { - return true; - } - - if (expand_string(std::move(string), &completions, flags, ctx, errors) == expand_result_t::ok && - completions.size() == 1) { - string = std::move(completions.at(0).completion); - return true; - } - return false; -} - -expand_result_t expand_to_command_and_args(const wcstring &instr, const operation_context_t &ctx, - wcstring *out_cmd, std::vector *out_args, - parse_error_list_t *errors, bool skip_wildcards) { - // Fast path. - if (expand_is_clean(instr)) { - *out_cmd = instr; - return expand_result_t::ok; - } - - expand_flags_t eflags{expand_flag::skip_cmdsubst}; - if (skip_wildcards) { - eflags.set(expand_flag::skip_wildcards); - } - - completion_list_t completions; - expand_result_t expand_err = expand_string(instr, &completions, eflags, ctx, errors); - if (expand_err == expand_result_t::ok) { - // The first completion is the command, any remaning are arguments. - bool first = true; - for (auto &comp : completions) { - if (first) { - if (out_cmd) *out_cmd = std::move(comp.completion); - first = false; - } else { - if (out_args) out_args->push_back(std::move(comp.completion)); - } - } - } - return expand_err; -} diff --git a/src/expand.h b/src/expand.h index 9708bb478..38147b44e 100644 --- a/src/expand.h +++ b/src/expand.h @@ -14,64 +14,51 @@ #include "common.h" #include "enum_set.h" +#include "env.h" #include "maybe.h" +#include "operation_context.h" #include "parse_constants.h" -class env_var_t; -class environment_t; -class operation_context_t; - /// Set of flags controlling expansions. -enum class expand_flag { +enum expand_flag { /// Skip command substitutions. - skip_cmdsubst, + skip_cmdsubst = 1 << 0, /// Skip variable expansion. - skip_variables, + skip_variables = 1 << 1, /// Skip wildcard expansion. - skip_wildcards, + skip_wildcards = 1 << 2, /// The expansion is being done for tab or auto completions. Returned completions may have the /// wildcard as a prefix instead of a match. - for_completions, + for_completions = 1 << 3, /// Only match files that are executable by the current user. - executables_only, + executables_only = 1 << 4, /// Only match directories. - directories_only, + directories_only = 1 << 5, /// Generate descriptions, stored in the description field of completions. - gen_descriptions, + gen_descriptions = 1 << 6, /// Un-expand home directories to tildes after. - preserve_home_tildes, + preserve_home_tildes = 1 << 7, /// Allow fuzzy matching. - fuzzy_match, + fuzzy_match = 1 << 8, /// Disallow directory abbreviations like /u/l/b for /usr/local/bin. Only applicable if /// fuzzy_match is set. - no_fuzzy_directories, + no_fuzzy_directories = 1 << 9, /// Allows matching a leading dot even if the wildcard does not contain one. /// By default, wildcards only match a leading dot literally; this is why e.g. '*' does not /// match hidden files. - allow_nonliteral_leading_dot, + allow_nonliteral_leading_dot = 1 << 10, /// Do expansions specifically to support cd. This means using CDPATH as a list of potential /// working directories, and to use logical instead of physical paths. - special_for_cd, + special_for_cd = 1 << 11, /// Do expansions specifically for cd autosuggestion. This is to differentiate between cd /// completions and cd autosuggestions. - special_for_cd_autosuggestion, + special_for_cd_autosuggestion = 1 << 12, /// Do expansions specifically to support external command completions. This means using PATH as /// a list of potential working directories. - special_for_command, - - COUNT, + special_for_command = 1 << 13, }; -template <> -struct enum_info_t { - static constexpr auto count = expand_flag::COUNT; -}; - -using expand_flags_t = enum_set_t; - -class completion_t; -using completion_list_t = std::vector; -class completion_receiver_t; +using expand_flags_t = uint64_t; enum : wchar_t { /// Character representing a home directory. @@ -100,111 +87,16 @@ enum : wchar_t { EXPAND_SENTINEL }; -/// These are the possible return values for expand_string. -struct expand_result_t { - enum result_t { - /// There was an error, for example, unmatched braces. - error, - /// Expansion succeeded. - ok, - /// Expansion was cancelled (e.g. control-C). - cancel, - /// Expansion succeeded, but a wildcard in the string matched no files, - /// so the output is empty. - wildcard_no_match, - }; - - /// The result of expansion. - result_t result; - - /// If expansion resulted in an error, this is an appropriate value with which to populate - /// $status. - int status{0}; - - /* implicit */ expand_result_t(result_t result) : result(result) {} - - /// operator== allows for comparison against result_t values. - bool operator==(result_t rhs) const { return result == rhs; } - bool operator!=(result_t rhs) const { return !(*this == rhs); } - - /// Make an error value with the given status. - static expand_result_t make_error(int status) { - assert(status != 0 && "status cannot be 0 for an error result"); - expand_result_t result(error); - result.status = status; - return result; - } -}; - /// The string represented by PROCESS_EXPAND_SELF #define PROCESS_EXPAND_SELF_STR L"%self" #define PROCESS_EXPAND_SELF_STR_LEN 5 -/// Perform various forms of expansion on in, such as tilde expansion (\~USER becomes the users home -/// directory), variable expansion (\$VAR_NAME becomes the value of the environment variable -/// VAR_NAME), cmdsubst expansion and wildcard expansion. The results are inserted into the list -/// out. -/// -/// If the parameter does not need expansion, it is copied into the list out. -/// -/// \param input The parameter to expand -/// \param output The list to which the result will be appended. -/// \param flags Specifies if any expansion pass should be skipped. Legal values are any combination -/// of skip_cmdsubst skip_variables and skip_wildcards -/// \param ctx The parser, variables, and cancellation checker for this operation. The parser may -/// be null. \param errors Resulting errors, or nullptr to ignore -/// -/// \return An expand_result_t. -/// wildcard_no_match and wildcard_match are normal exit conditions used only on -/// strings containing wildcards to tell if the wildcard produced any matches. -__warn_unused expand_result_t expand_string(wcstring input, completion_list_t *output, - expand_flags_t flags, const operation_context_t &ctx, - parse_error_list_t *errors = nullptr); +#if INCLUDE_RUST_HEADERS +#include "expand.rs.h" +using expand_result_t = ExpandResult; +#endif -/// Variant of string that inserts its results into a completion_receiver_t. -__warn_unused expand_result_t expand_string(wcstring input, completion_receiver_t *output, - expand_flags_t flags, const operation_context_t &ctx, - parse_error_list_t *errors = nullptr); - -/// expand_one is identical to expand_string, except it will fail if in expands to more than one -/// string. This is used for expanding command names. -/// -/// \param inout_str The parameter to expand in-place -/// \param flags Specifies if any expansion pass should be skipped. Legal values are any combination -/// of skip_cmdsubst skip_variables and skip_wildcards -/// \param ctx The parser, variables, and cancellation checker for this operation. The parser may be -/// null. \param errors Resulting errors, or nullptr to ignore -/// -/// \return Whether expansion succeeded. -bool expand_one(wcstring &string, expand_flags_t flags, const operation_context_t &ctx, - parse_error_list_t *errors = nullptr); - -/// Expand a command string like $HOME/bin/cmd into a command and list of arguments. -/// Return the command and arguments by reference. -/// If the expansion resulted in no or an empty command, the command will be an empty string. Note -/// that API does not distinguish between expansion resulting in an empty command (''), and -/// expansion resulting in no command (e.g. unset variable). -/// If \p skip_wildcards is true, then do not do wildcard expansion -/// \return an expand error. -expand_result_t expand_to_command_and_args(const wcstring &instr, const operation_context_t &ctx, - wcstring *out_cmd, std::vector *out_args, - parse_error_list_t *errors = nullptr, - bool skip_wildcards = false); - -/// Convert the variable value to a human readable form, i.e. escape things, handle arrays, etc. -/// Suitable for pretty-printing. -wcstring expand_escape_variable(const env_var_t &var); - -/// Convert a string value to a human readable form, i.e. escape things, handle arrays, etc. -/// Suitable for pretty-printing. -wcstring expand_escape_string(const wcstring &el); - -/// Perform tilde expansion and nothing else on the specified string, which is modified in place. -/// /// \param input the string to tilde expand -void expand_tilde(wcstring &input, const environment_t &vars); - -/// Perform the opposite of tilde expansion on the string, which is modified in place. -wcstring replace_home_directory_with_tilde(const wcstring &str, const environment_t &vars); +void expand_tilde(wcstring &input, const env_stack_t &vars); #endif diff --git a/src/ffi_baggage.h b/src/ffi_baggage.h index 4f82afb3e..90fb825c6 100644 --- a/src/ffi_baggage.h +++ b/src/ffi_baggage.h @@ -1,9 +1,49 @@ +#include "builtin.h" +#include "builtins/bind.h" +#include "builtins/commandline.h" +#include "builtins/read.h" +#include "builtins/ulimit.h" +#include "event.h" +#include "fds.h" #include "fish_indent_common.h" +#include "highlight.h" +#include "input.h" +#include "parse_util.h" +#include "reader.h" +#include "screen.h" // Symbols that get autocxx bindings but are not used in a given binary, will cause "undefined // reference" when trying to link that binary. Work around this by marking them as used in // all binaries. -void mark_as_used() { - // +void mark_as_used(const parser_t& parser, env_stack_t& env_stack) { + wcstring s; + + escape_code_length_ffi({}); + event_fire_generic(parser, {}); + event_fire_generic(parser, {}, {}); + expand_tilde(s, env_stack); + get_history_variable_text_ffi({}); + highlight_spec_t{}; + init_input(); + make_pipes_ffi(); pretty_printer_t({}, {}); + reader_change_cursor_selection_mode(cursor_selection_mode_t::exclusive); + reader_change_history({}); + reader_read_ffi({}, {}, {}); + reader_schedule_prompt_repaint(); + reader_set_autosuggestion_enabled_ffi({}); + reader_status_count(); + restore_term_mode(); + rgb_color_t{}; + screen_clear_layout_cache_ffi(); + screen_set_midnight_commander_hack(); + setenv_lock({}, {}, {}); + set_inheriteds_ffi(); + term_copy_modes(); + unsetenv_lock({}); + + builtin_bind({}, {}, {}); + builtin_commandline({}, {}, {}); + builtin_read({}, {}, {}); + builtin_ulimit({}, {}, {}); } diff --git a/src/fish.cpp b/src/fish.cpp index 1845ecd5b..a2619c3f0 100644 --- a/src/fish.cpp +++ b/src/fish.cpp @@ -17,8 +17,8 @@ along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ #include "common.h" -#include "fish.rs.h" #include "ffi_baggage.h" +#include "fish.rs.h" int main() { program_name = L"fish"; diff --git a/src/fish_indent.cpp b/src/fish_indent.cpp index 92fc267ee..b4b8cc1bf 100644 --- a/src/fish_indent.cpp +++ b/src/fish_indent.cpp @@ -129,9 +129,8 @@ static const char *highlight_role_to_string(highlight_role_t role) { // 3,7,command static std::string make_pygments_csv(const wcstring &src) { const size_t len = src.size(); - std::vector colors; - highlight_shell(src, colors, operation_context_t::globals()); - assert(colors.size() == len && "Colors and src should have same size"); + auto colors = highlight_shell_ffi(src, *operation_context_globals(), false, {}); + assert(colors->size() == len && "Colors and src should have same size"); struct token_range_t { unsigned long start; @@ -141,7 +140,7 @@ static std::string make_pygments_csv(const wcstring &src) { std::vector token_ranges; for (size_t i = 0; i < len; i++) { - highlight_role_t role = colors.at(i).foreground; + highlight_role_t role = colors->at(i).foreground; // See if we can extend the last range. if (!token_ranges.empty()) { auto &last = token_ranges.back(); @@ -183,7 +182,7 @@ static wcstring prettify(const wcstring &src, bool do_indent) { /// for the various colors. static const wchar_t *html_class_name_for_color(highlight_spec_t spec) { #define P(x) L"fish_color_" #x - switch (spec.foreground) { + switch (spec->foreground) { case highlight_role_t::normal: { return P(normal); } @@ -450,8 +449,10 @@ int main(int argc, char *argv[]) { // Maybe colorize. std::vector colors; + maybe_t> ffi_colors; if (output_type != output_type_plain_text) { - highlight_shell(output_wtext, colors, operation_context_t::globals()); + highlight_shell(output_wtext, colors, *operation_context_globals()); + ffi_colors = highlight_shell_ffi(output_wtext, *operation_context_globals(), false, {}); } std::string colored_output; @@ -473,7 +474,11 @@ int main(int argc, char *argv[]) { break; } case output_type_ansi: { - colored_output = colorize(output_wtext, colors, env_stack_t::globals()); + auto ffi_colored = + colorize(output_wtext, **ffi_colors, env_stack_t::globals().get_impl_ffi()); + for (uint8_t c : ffi_colored) { + colored_output.push_back(c); + } break; } case output_type_html: { diff --git a/src/fish_indent_common.cpp b/src/fish_indent_common.cpp index a2fa42dc2..0cea07aab 100644 --- a/src/fish_indent_common.cpp +++ b/src/fish_indent_common.cpp @@ -252,8 +252,9 @@ wcstring pretty_printer_t::clean_text(const wcstring &input) { // Unescape the string - this leaves special markers around if there are any // expansions or anything. We specifically tell it to not compute backslash-escapes // like \U or \x, because we want to leave them intact. - wcstring unescaped = input; - unescape_string_in_place(&unescaped, UNESCAPE_SPECIAL | UNESCAPE_NO_BACKSLASHES); + wcstring unescaped = + *unescape_string(input.c_str(), input.size(), UNESCAPE_SPECIAL | UNESCAPE_NO_BACKSLASHES, + STRING_STYLE_SCRIPT); // Remove INTERNAL_SEPARATOR because that's a quote. auto quote = [](wchar_t ch) { return ch == INTERNAL_SEPARATOR; }; diff --git a/src/fish_key_reader.cpp b/src/fish_key_reader.cpp index 650032e82..1e0bc4698 100644 --- a/src/fish_key_reader.cpp +++ b/src/fish_key_reader.cpp @@ -279,8 +279,9 @@ static void process_input(bool continuous_mode, bool verbose) { rust_init(); rust_env_init(true); reader_init(); - parser_t &parser = parser_t::principal_parser(); - scoped_push interactive{&parser.libdata().is_interactive, true}; + auto parser_box = parser_principal_parser(); + const parser_t &parser = parser_box->deref(); + scoped_push interactive{&parser.libdata_pods_mut().is_interactive, true}; signal_set_handlers(true); // We need to set the shell-modes for ICRNL, // in fish-proper this is done once a command is run. diff --git a/src/fish_tests.cpp b/src/fish_tests.cpp index 99fea0b37..b39b65020 100644 --- a/src/fish_tests.cpp +++ b/src/fish_tests.cpp @@ -52,7 +52,6 @@ #include "abbrs.h" #include "ast.h" #include "autoload.h" -#include "builtin.h" #include "color.h" #include "common.h" #include "complete.h" @@ -98,7 +97,6 @@ #include "termsize.h" #include "threads.rs.h" #include "tokenizer.h" -#include "topic_monitor.h" #include "utf8.h" #include "util.h" #include "wcstringutil.h" @@ -109,11 +107,6 @@ static const char *const *s_arguments; static int s_test_run_count = 0; -#define system_assert(command) \ - if (system(command)) { \ - err(L"Non-zero result on line %d: %s", __LINE__, command); \ - } - // Indicate if we should test the given function. Either we test everything (all arguments) or we // run only tests that have a prefix in s_arguments. // If \p default_on is set, then allow no args to run this test by default. @@ -138,8 +131,6 @@ static bool should_test_function(const char *func_name, bool default_on = true) #define ESCAPE_TEST_COUNT 100000 /// The average length of strings to unescape. #define ESCAPE_TEST_LENGTH 100 -/// The highest character number of character to try and escape. -#define ESCAPE_TEST_CHAR 4000 /// Number of encountered errors. static int err_count = 0; @@ -171,7 +162,7 @@ static void err(const wchar_t *blah, ...) { } /// Joins a std::vector via commas. -static wcstring comma_join(const std::vector &lst) { +wcstring comma_join(const std::vector &lst) { wcstring result; for (size_t i = 0; i < lst.size(); i++) { if (i > 0) { @@ -203,7 +194,7 @@ static bool pushd(const char *path) { return false; } - env_stack_t::principal().set_pwd_from_getcwd(); + env_stack_principal().set_pwd_from_getcwd(); return true; } @@ -213,7 +204,7 @@ static void popd() { err(L"chdir(\"%s\") from popd() failed: errno = %d", old_cwd.c_str(), errno); } pushed_dirs.pop_back(); - env_stack_t::principal().set_pwd_from_getcwd(); + env_stack_principal().set_pwd_from_getcwd(); } // Helper to return a string whose length greatly exceeds PATH_MAX. @@ -238,15 +229,6 @@ wcstring get_overlong_path() { } \ } while (0) -#define do_test_from(e, from) \ - do { \ - if (e) { \ - ; \ - } else { \ - err(L"Test failed on line %lu (from %lu): %s", __LINE__, from, #e); \ - } \ - } while (0) - #define do_test1(e, msg) \ do { \ if (e) { \ @@ -857,70 +839,26 @@ static void test_parser() { // Ensure that we don't crash on infinite self recursion and mutual recursion. These must use // the principal parser because we cannot yet execute jobs on other parsers. - auto parser = parser_t::principal_parser().shared(); + auto parser = parser_principal_parser()->deref().shared(); say(L"Testing recursion detection"); - parser->eval(L"function recursive ; recursive ; end ; recursive; ", io_chain_t()); + parser->deref().eval(L"function recursive ; recursive ; end ; recursive; ", *new_io_chain()); - parser->eval( + parser->deref().eval( L"function recursive1 ; recursive2 ; end ; " L"function recursive2 ; recursive1 ; end ; recursive1; ", - io_chain_t()); + *new_io_chain()); say(L"Testing empty function name"); - parser->eval(L"function '' ; echo fail; exit 42 ; end ; ''", io_chain_t()); + parser->deref().eval(L"function '' ; echo fail; exit 42 ; end ; ''", *new_io_chain()); say(L"Testing eval_args"); - completion_list_t comps = parser_t::expand_argument_list(L"alpha 'beta gamma' delta", - expand_flags_t{}, parser->context()); + wcstring_list_ffi_t comps; + parser_expand_argument_list_ffi(L"alpha 'beta gamma' delta", expand_flags_t{}, + *parser_context(parser->deref()), comps); do_test(comps.size() == 3); - do_test(comps.at(0).completion == L"alpha"); - do_test(comps.at(1).completion == L"beta gamma"); - do_test(comps.at(2).completion == L"delta"); -} - -static void test_1_cancellation(const wchar_t *src) { - auto filler = io_bufferfill_t::create(); - pthread_t thread = pthread_self(); - double delay = 0.50 /* seconds */; - iothread_perform([=]() { - /// Wait a while and then SIGINT the main thread. - usleep(delay * 1E6); - pthread_kill(thread, SIGINT); - }); - eval_res_t res = parser_t::principal_parser().eval(src, io_chain_t{filler}); - separated_buffer_t buffer = io_bufferfill_t::finish(std::move(filler)); - if (buffer.size() != 0) { - err(L"Expected 0 bytes in out_buff, but instead found %lu bytes, for command %ls\n", - buffer.size(), src); - } - do_test(res.status.signal_exited() && res.status.signal_code() == SIGINT); - iothread_drain_all(); -} - -static void test_cancellation() { - say(L"Testing Ctrl-C cancellation. If this hangs, that's a bug!"); - - // Enable fish's signal handling here. - signal_set_handlers(true); - - // This tests that we can correctly ctrl-C out of certain loop constructs, and that nothing gets - // printed if we do. - - // Here the command substitution is an infinite loop. echo never even gets its argument, so when - // we cancel we expect no output. - test_1_cancellation(L"echo (while true ; echo blah ; end)"); - - // Nasty infinite loop that doesn't actually execute anything. - test_1_cancellation(L"echo (while true ; end) (while true ; end) (while true ; end)"); - test_1_cancellation(L"while true ; end"); - test_1_cancellation(L"while true ; echo nothing > /dev/null; end"); - test_1_cancellation(L"for i in (while true ; end) ; end"); - - signal_reset_handlers(); - - // Ensure that we don't think we should cancel. - reader_reset_interrupted(); - signal_clear_cancel(); + do_test(comps.at(0) == L"alpha"); + do_test(comps.at(1) == L"beta gamma"); + do_test(comps.at(2) == L"delta"); } static void test_const_strlen() { @@ -950,33 +888,6 @@ static void test_const_strcmp() { static_assert(const_strcmp("b", "aa") > 0, "const_strcmp failure"); } -static void test_is_sorted_by_name() { - struct named_t { - const wchar_t *name; - }; - - static constexpr named_t sorted[] = { - {L"a"}, {L"aa"}, {L"aaa"}, {L"aaa"}, {L"aaa"}, {L"aazz"}, {L"aazzzz"}, - }; - static_assert(is_sorted_by_name(sorted), "is_sorted_by_name failure"); - do_test(get_by_sorted_name(L"", sorted) == nullptr); - do_test(get_by_sorted_name(L"nope", sorted) == nullptr); - do_test(get_by_sorted_name(L"aaaaaaaaaaa", sorted) == nullptr); - wcstring last; - for (const auto &v : sorted) { - // We have multiple items with the same name; only test the first. - if (last != v.name) { - last = v.name; - do_test(get_by_sorted_name(last, sorted) == &v); - } - } - - static constexpr named_t not_sorted[] = { - {L"a"}, {L"aa"}, {L"aaa"}, {L"q"}, {L"aazz"}, {L"aazz"}, {L"aazz"}, {L"aazzzz"}, - }; - static_assert(!is_sorted_by_name(not_sorted), "is_sorted_by_name failure"); -} - void test_dir_iter() { dir_iter_t baditer(L"/definitely/not/a/valid/directory/for/sure"); do_test(!baditer.valid()); @@ -1068,7 +979,6 @@ static void test_utility_functions() { say(L"Testing utility functions"); test_const_strlen(); test_const_strcmp(); - test_is_sorted_by_name(); } // UTF8 tests taken from Alexey Vatchenko's utf8 library. See http://www.bsdua.org/libbsdua.html. @@ -1381,295 +1291,6 @@ static void test_lru() { do_test(cache.size() == 0); } -/// An environment built around an std::map. -struct test_environment_t : public environment_t { - std::map vars; - - maybe_t get(const wcstring &key, - env_mode_flags_t mode = ENV_DEFAULT) const override { - UNUSED(mode); - auto iter = vars.find(key); - if (iter != vars.end()) { - return env_var_t(iter->second, ENV_DEFAULT); - } - return none(); - } - - std::vector get_names(env_mode_flags_t flags) const override { - UNUSED(flags); - std::vector result; - for (const auto &kv : vars) { - result.push_back(kv.first); - } - return result; - } -}; - -/// A test environment that knows about PWD. -struct pwd_environment_t : public test_environment_t { - maybe_t get(const wcstring &key, - env_mode_flags_t mode = ENV_DEFAULT) const override { - if (key == L"PWD") { - return env_var_t{wgetcwd(), 0}; - } - return test_environment_t::get(key, mode); - } - - std::vector get_names(env_mode_flags_t flags) const override { - auto res = test_environment_t::get_names(flags); - res.clear(); - if (std::count(res.begin(), res.end(), L"PWD") == 0) { - res.emplace_back(L"PWD"); - } - return res; - } -}; - -/// Perform parameter expansion and test if the output equals the zero-terminated parameter list -/// supplied. -/// -/// \param in the string to expand -/// \param flags the flags to send to expand_string -/// \param ... A zero-terminated parameter list of values to test. -/// After the zero terminator comes one more arg, a string, which is the error -/// message to print if the test fails. -static bool expand_test(const wchar_t *in, expand_flags_t flags, ...) { - completion_list_t output; - va_list va; - bool res = true; - wchar_t *arg; - auto errors = new_parse_error_list(); - pwd_environment_t pwd{}; - operation_context_t ctx{parser_t::principal_parser().shared(), pwd, no_cancel}; - - if (expand_string(in, &output, flags, ctx, &*errors) == expand_result_t::error) { - if (errors->empty()) { - err(L"Bug: Parse error reported but no error text found."); - } else { - err(L"%ls", errors->at(0)->describe(in, ctx.parser->is_interactive())->c_str()); - } - return false; - } - - std::vector expected; - - va_start(va, flags); - while ((arg = va_arg(va, wchar_t *)) != nullptr) { - expected.emplace_back(arg); - } - va_end(va); - - std::set remaining(expected.begin(), expected.end()); - completion_list_t::const_iterator out_it = output.begin(), out_end = output.end(); - for (; out_it != out_end; ++out_it) { - if (!remaining.erase(out_it->completion)) { - res = false; - break; - } - } - if (!remaining.empty()) { - res = false; - } - - if (!res) { - arg = va_arg(va, wchar_t *); - if (arg) { - wcstring msg = L"Expected ["; - bool first = true; - for (const wcstring &exp : expected) { - if (!first) msg += L", "; - first = false; - msg += '"'; - msg += exp; - msg += '"'; - } - msg += L"], found ["; - first = true; - for (const auto &completion : output) { - if (!first) msg += L", "; - first = false; - msg += '"'; - msg += completion.completion; - msg += '"'; - } - msg += L"]"; - err(L"%ls\n%ls", arg, msg.c_str()); - } - } - - va_end(va); - - return res; -} - -/// Test globbing and other parameter expansion. -static void test_expand() { - say(L"Testing parameter expansion"); - const expand_flags_t noflags{}; - const wchar_t *const wnull = nullptr; - - expand_test(L"foo", noflags, L"foo", wnull, L"Strings do not expand to themselves"); - expand_test(L"a{b,c,d}e", noflags, L"abe", L"ace", L"ade", wnull, - L"Bracket expansion is broken"); - expand_test(L"a*", expand_flag::skip_wildcards, L"a*", wnull, - L"Cannot skip wildcard expansion"); - expand_test(L"/bin/l\\0", expand_flag::for_completions, wnull, - L"Failed to handle null escape in expansion"); - expand_test(L"foo\\$bar", expand_flag::skip_variables, L"foo$bar", wnull, - L"Failed to handle dollar sign in variable-skipping expansion"); - - // bb - // x - // bar - // baz - // xxx - // yyy - // bax - // xxx - // lol - // nub - // q - // .foo - // aaa - // aaa2 - // x - if (system("mkdir -p test/fish_expand_test/")) err(L"mkdir failed"); - if (system("mkdir -p test/fish_expand_test/bb/")) err(L"mkdir failed"); - if (system("mkdir -p test/fish_expand_test/baz/")) err(L"mkdir failed"); - if (system("mkdir -p test/fish_expand_test/bax/")) err(L"mkdir failed"); - if (system("mkdir -p test/fish_expand_test/lol/nub/")) err(L"mkdir failed"); - if (system("mkdir -p test/fish_expand_test/aaa/")) err(L"mkdir failed"); - if (system("mkdir -p test/fish_expand_test/aaa2/")) err(L"mkdir failed"); - if (system("touch test/fish_expand_test/.foo")) err(L"touch failed"); - if (system("touch test/fish_expand_test/bb/x")) err(L"touch failed"); - if (system("touch test/fish_expand_test/bar")) err(L"touch failed"); - if (system("touch test/fish_expand_test/bax/xxx")) err(L"touch failed"); - if (system("touch test/fish_expand_test/baz/xxx")) err(L"touch failed"); - if (system("touch test/fish_expand_test/baz/yyy")) err(L"touch failed"); - if (system("touch test/fish_expand_test/lol/nub/q")) err(L"touch failed"); - if (system("touch test/fish_expand_test/aaa2/x")) err(L"touch failed"); - - // This is checking that .* does NOT match . and .. - // (https://github.com/fish-shell/fish-shell/issues/270). But it does have to match literal - // components (e.g. "./*" has to match the same as "*". - expand_test(L"test/fish_expand_test/.*", noflags, L"test/fish_expand_test/.foo", wnull, - L"Expansion not correctly handling dotfiles"); - - expand_test(L"test/fish_expand_test/./.*", noflags, L"test/fish_expand_test/./.foo", wnull, - L"Expansion not correctly handling literal path components in dotfiles"); - - expand_test(L"test/fish_expand_test/*/xxx", noflags, L"test/fish_expand_test/bax/xxx", - L"test/fish_expand_test/baz/xxx", wnull, L"Glob did the wrong thing 1"); - - expand_test(L"test/fish_expand_test/*z/xxx", noflags, L"test/fish_expand_test/baz/xxx", wnull, - L"Glob did the wrong thing 2"); - - expand_test(L"test/fish_expand_test/**z/xxx", noflags, L"test/fish_expand_test/baz/xxx", wnull, - L"Glob did the wrong thing 3"); - - expand_test(L"test/fish_expand_test////baz/xxx", noflags, L"test/fish_expand_test////baz/xxx", - wnull, L"Glob did the wrong thing 3"); - - expand_test(L"test/fish_expand_test/b**", noflags, L"test/fish_expand_test/bb", - L"test/fish_expand_test/bb/x", L"test/fish_expand_test/bar", - L"test/fish_expand_test/bax", L"test/fish_expand_test/bax/xxx", - L"test/fish_expand_test/baz", L"test/fish_expand_test/baz/xxx", - L"test/fish_expand_test/baz/yyy", wnull, L"Glob did the wrong thing 4"); - - // A trailing slash should only produce directories. - expand_test(L"test/fish_expand_test/b*/", noflags, L"test/fish_expand_test/bb/", - L"test/fish_expand_test/baz/", L"test/fish_expand_test/bax/", wnull, - L"Glob did the wrong thing 5"); - - expand_test(L"test/fish_expand_test/b**/", noflags, L"test/fish_expand_test/bb/", - L"test/fish_expand_test/baz/", L"test/fish_expand_test/bax/", wnull, - L"Glob did the wrong thing 6"); - - expand_test(L"test/fish_expand_test/**/q", noflags, L"test/fish_expand_test/lol/nub/q", wnull, - L"Glob did the wrong thing 7"); - - expand_test(L"test/fish_expand_test/BA", expand_flag::for_completions, - L"test/fish_expand_test/bar", L"test/fish_expand_test/bax/", - L"test/fish_expand_test/baz/", wnull, L"Case insensitive test did the wrong thing"); - - expand_test(L"test/fish_expand_test/BA", expand_flag::for_completions, - L"test/fish_expand_test/bar", L"test/fish_expand_test/bax/", - L"test/fish_expand_test/baz/", wnull, L"Case insensitive test did the wrong thing"); - - expand_test(L"test/fish_expand_test/bb/yyy", expand_flag::for_completions, - /* nothing! */ wnull, L"Wrong fuzzy matching 1"); - - expand_test(L"test/fish_expand_test/bb/x", - expand_flags_t{expand_flag::for_completions, expand_flag::fuzzy_match}, L"", - wnull, // we just expect the empty string since this is an exact match - L"Wrong fuzzy matching 2"); - - // Some vswprintfs refuse to append ANY_STRING in a format specifiers, so don't use - // format_string here. - const expand_flags_t fuzzy_comp{expand_flag::for_completions, expand_flag::fuzzy_match}; - const wcstring any_str_str(1, ANY_STRING); - expand_test(L"test/fish_expand_test/b/xx*", fuzzy_comp, - (L"test/fish_expand_test/bax/xx" + any_str_str).c_str(), - (L"test/fish_expand_test/baz/xx" + any_str_str).c_str(), wnull, - L"Wrong fuzzy matching 3"); - - expand_test(L"test/fish_expand_test/b/yyy", fuzzy_comp, L"test/fish_expand_test/baz/yyy", wnull, - L"Wrong fuzzy matching 4"); - - expand_test(L"test/fish_expand_test/aa/x", fuzzy_comp, L"test/fish_expand_test/aaa2/x", wnull, - L"Wrong fuzzy matching 5"); - - expand_test(L"test/fish_expand_test/aaa/x", fuzzy_comp, wnull, - L"Wrong fuzzy matching 6 - shouldn't remove valid directory names (#3211)"); - - if (!expand_test(L"test/fish_expand_test/.*", noflags, L"test/fish_expand_test/.foo", 0)) { - err(L"Expansion not correctly handling dotfiles"); - } - if (!expand_test(L"test/fish_expand_test/./.*", noflags, L"test/fish_expand_test/./.foo", 0)) { - err(L"Expansion not correctly handling literal path components in dotfiles"); - } - - if (!pushd("test/fish_expand_test")) return; - - expand_test(L"b/xx", fuzzy_comp, L"bax/xxx", L"baz/xxx", wnull, L"Wrong fuzzy matching 5"); - - // multiple slashes with fuzzy matching - #3185 - expand_test(L"l///n", fuzzy_comp, L"lol///nub/", wnull, L"Wrong fuzzy matching 6"); - - popd(); -} - -static void test_expand_overflow() { - say(L"Testing overflowing expansions"); - // Ensure that we have sane limits on number of expansions - see #7497. - - // Make a list of 64 elements, then expand it cartesian-style 64 times. - // This is far too large to expand. - std::vector vals; - wcstring expansion; - for (int i = 1; i <= 64; i++) { - vals.push_back(to_string(i)); - expansion.append(L"$bigvar"); - } - - auto parser = parser_t::principal_parser().shared(); - parser->vars().push(true); - int set = parser->vars().set(L"bigvar", ENV_LOCAL, std::move(vals)); - do_test(set == ENV_OK); - - auto errors = new_parse_error_list(); - operation_context_t ctx{parser, parser->vars(), no_cancel}; - - // We accept only 1024 completions. - completion_receiver_t output{1024}; - - auto res = expand_string(expansion, &output, expand_flags_t{}, ctx, &*errors); - do_test(!errors->empty()); - do_test(res == expand_result_t::error); - - parser->vars().pop(); -} - static void test_abbreviations() { say(L"Testing abbreviations"); { @@ -1708,7 +1329,7 @@ static void test_abbreviations() { auto expand_abbreviation_in_command = [](const wcstring &cmdline, maybe_t cursor_pos = {}) -> maybe_t { if (auto replacement = reader_expand_abbreviation_at_cursor( - cmdline, cursor_pos.value_or(cmdline.size()), parser_t::principal_parser())) { + cmdline, cursor_pos.value_or(cmdline.size()), parser_principal_parser()->deref())) { wcstring cmdline_expanded = cmdline; std::vector colors{cmdline_expanded.size()}; apply_edit(&cmdline_expanded, &colors, edit_t{replacement->range, *replacement->text}); @@ -1775,13 +1396,13 @@ static void test_pager_navigation() { // columns (7 * 12 - 2 = 82). // // You can simulate this test by creating 19 files named "file00.txt" through "file_18.txt". - completion_list_t completions; + auto completions = new_completion_list(); for (size_t i = 0; i < 19; i++) { - append_completion(&completions, L"abcdefghij"); + append_completion(*completions, L"abcdefghij"); } pager_t pager; - pager.set_completions(completions); + pager.set_completions(*completions); pager.set_term_size(termsize_default()); page_rendering_t render = pager.render(); @@ -1906,8 +1527,10 @@ static void test_pager_layout() { pager_t pager; // These test cases have equal completions and descriptions - const completion_t c1(L"abcdefghij", L"1234567890"); - pager.set_completions(completion_list_t(1, c1)); + auto c1 = new_completion_with(L"abcdefghij", L"1234567890", 0); + auto c1s = new_completion_list(); + c1s->push_back(*c1); + pager.set_completions(*c1s); const pager_layout_testcase_t testcases1[] = { {26, L"abcdefghij (1234567890)"}, {25, L"abcdefghij (1234567890)"}, {24, L"abcdefghij (1234567890)"}, {23, L"abcdefghij (12345678…)"}, @@ -1921,8 +1544,10 @@ static void test_pager_layout() { } // These test cases have heavyweight completions - const completion_t c2(L"abcdefghijklmnopqrs", L"1"); - pager.set_completions(completion_list_t(1, c2)); + auto c2 = new_completion_with(L"abcdefghijklmnopqrs", L"1", 0); + auto c2s = new_completion_list(); + c2s->push_back(*c2); + pager.set_completions(*c2s); const pager_layout_testcase_t testcases2[] = { {26, L"abcdefghijklmnopqrs (1)"}, {25, L"abcdefghijklmnopqrs (1)"}, {24, L"abcdefghijklmnopqrs (1)"}, {23, L"abcdefghijklmnopq… (1)"}, @@ -1936,8 +1561,10 @@ static void test_pager_layout() { } // These test cases have no descriptions - const completion_t c3(L"abcdefghijklmnopqrst", L""); - pager.set_completions(completion_list_t(1, c3)); + auto c3 = new_completion_with(L"abcdefghijklmnopqrst", L"", 0); + auto c3s = new_completion_list(); + c3s->push_back(*c3); + pager.set_completions(*c3s); const pager_layout_testcase_t testcases3[] = { {26, L"abcdefghijklmnopqrst"}, {25, L"abcdefghijklmnopqrst"}, {24, L"abcdefghijklmnopqrst"}, {23, L"abcdefghijklmnopqrst"}, @@ -2058,45 +1685,6 @@ static void test_word_motion() { L"^a-b-c^\n\nd-e-f^ "); } -/// Test is_potential_path. -static void test_is_potential_path() { - say(L"Testing is_potential_path"); - - // Directories - if (system("mkdir -p test/is_potential_path_test/alpha/")) err(L"mkdir failed"); - if (system("mkdir -p test/is_potential_path_test/beta/")) err(L"mkdir failed"); - - // Files - if (system("touch test/is_potential_path_test/aardvark")) err(L"touch failed"); - if (system("touch test/is_potential_path_test/gamma")) err(L"touch failed"); - - const wcstring wd = L"test/is_potential_path_test/"; - const std::vector wds({L".", wd}); - - operation_context_t ctx{env_stack_t::principal()}; - do_test(is_potential_path(L"al", true, wds, ctx, PATH_REQUIRE_DIR)); - do_test(is_potential_path(L"alpha/", true, wds, ctx, PATH_REQUIRE_DIR)); - do_test(is_potential_path(L"aard", true, wds, ctx, 0)); - do_test(!is_potential_path(L"aard", false, wds, ctx, 0)); - do_test(!is_potential_path(L"alp/", true, wds, ctx, PATH_REQUIRE_DIR | PATH_FOR_CD)); - - do_test(!is_potential_path(L"balpha/", true, wds, ctx, PATH_REQUIRE_DIR)); - do_test(!is_potential_path(L"aard", true, wds, ctx, PATH_REQUIRE_DIR)); - do_test(!is_potential_path(L"aarde", true, wds, ctx, PATH_REQUIRE_DIR)); - do_test(!is_potential_path(L"aarde", true, wds, ctx, 0)); - - do_test(is_potential_path(L"test/is_potential_path_test/aardvark", true, wds, ctx, 0)); - do_test(is_potential_path(L"test/is_potential_path_test/al", true, wds, ctx, PATH_REQUIRE_DIR)); - do_test(is_potential_path(L"test/is_potential_path_test/aardv", true, wds, ctx, 0)); - - do_test(!is_potential_path(L"test/is_potential_path_test/aardvark", true, wds, ctx, - PATH_REQUIRE_DIR)); - do_test(!is_potential_path(L"test/is_potential_path_test/al/", true, wds, ctx, 0)); - do_test(!is_potential_path(L"test/is_potential_path_test/ar", true, wds, ctx, 0)); - - do_test(is_potential_path(L"/usr", true, wds, ctx, PATH_REQUIRE_DIR)); -} - static void test_wcstod() { say(L"Testing fish_wcstod"); auto tod_test = [](const wchar_t *a, const char *b) { @@ -2115,46 +1703,6 @@ static void test_wcstod() { tod_test(L"nope", "nope"); } -static void test_dup2s() { - using std::make_shared; - io_chain_t chain; - chain.push_back(make_shared(17)); - chain.push_back(make_shared(3, 19)); - auto list = dup2_list_resolve_chain_shim(chain); - do_test(list.get_actions().size() == 2); - - auto act1 = list.get_actions().at(0); - do_test(act1.src == 17); - do_test(act1.target == -1); - - auto act2 = list.get_actions().at(1); - do_test(act2.src == 19); - do_test(act2.target == 3); -} - -static void test_dup2s_fd_for_target_fd() { - using std::make_shared; - io_chain_t chain; - // note io_fd_t params are backwards from dup2. - chain.push_back(make_shared(10)); - chain.push_back(make_shared(9, 10)); - chain.push_back(make_shared(5, 8)); - chain.push_back(make_shared(1, 4)); - chain.push_back(make_shared(3, 5)); - auto list = dup2_list_resolve_chain_shim(chain); - - do_test(list.fd_for_target_fd(3) == 8); - do_test(list.fd_for_target_fd(5) == 8); - do_test(list.fd_for_target_fd(8) == 8); - do_test(list.fd_for_target_fd(1) == 4); - do_test(list.fd_for_target_fd(4) == 4); - do_test(list.fd_for_target_fd(100) == 100); - do_test(list.fd_for_target_fd(0) == 0); - do_test(list.fd_for_target_fd(-1) == -1); - do_test(list.fd_for_target_fd(9) == -1); - do_test(list.fd_for_target_fd(10) == -1); -} - /// Testing colors. static void test_colors() { say(L"Testing colors"); @@ -2171,364 +1719,6 @@ static void test_colors() { do_test(rgb_color_t(L"mooganta").is_none()); } -// This class allows accessing private bits of autoload_t. -struct autoload_tester_t { - static void run(const wchar_t *fmt, ...) { - va_list va; - va_start(va, fmt); - wcstring cmd = vformat_string(fmt, va); - va_end(va); - - int status = system(wcs2zstring(cmd).c_str()); - do_test(status == 0); - } - - static void touch_file(const wcstring &path) { - int fd = wopen_cloexec(path, O_RDWR | O_CREAT, 0666); - do_test(fd >= 0); - write_loop(fd, "Hello", 5); - close(fd); - } - - static void run_test() { - char t1[] = "/tmp/fish_test_autoload.XXXXXX"; - wcstring p1 = str2wcstring(mkdtemp(t1)); - char t2[] = "/tmp/fish_test_autoload.XXXXXX"; - wcstring p2 = str2wcstring(mkdtemp(t2)); - - const std::vector paths = {p1, p2}; - - autoload_t autoload(L"test_var"); - do_test(!autoload.resolve_command(L"file1", paths)); - do_test(!autoload.resolve_command(L"nothing", paths)); - do_test(autoload.get_autoloaded_commands().empty()); - - run(L"touch %ls/file1.fish", p1.c_str()); - run(L"touch %ls/file2.fish", p2.c_str()); - autoload.invalidate_cache(); - - do_test(!autoload.autoload_in_progress(L"file1")); - do_test(autoload.resolve_command(L"file1", paths)); - do_test(!autoload.resolve_command(L"file1", paths)); - do_test(autoload.autoload_in_progress(L"file1")); - do_test(autoload.get_autoloaded_commands() == std::vector{L"file1"}); - autoload.mark_autoload_finished(L"file1"); - do_test(!autoload.autoload_in_progress(L"file1")); - do_test(autoload.get_autoloaded_commands() == std::vector{L"file1"}); - - do_test(!autoload.resolve_command(L"file1", paths)); - do_test(!autoload.resolve_command(L"nothing", paths)); - do_test(autoload.resolve_command(L"file2", paths)); - do_test(!autoload.resolve_command(L"file2", paths)); - autoload.mark_autoload_finished(L"file2"); - do_test(!autoload.resolve_command(L"file2", paths)); - do_test((autoload.get_autoloaded_commands() == std::vector{L"file1", L"file2"})); - - autoload.clear(); - do_test(autoload.resolve_command(L"file1", paths)); - autoload.mark_autoload_finished(L"file1"); - do_test(!autoload.resolve_command(L"file1", paths)); - do_test(!autoload.resolve_command(L"nothing", paths)); - do_test(autoload.resolve_command(L"file2", paths)); - do_test(!autoload.resolve_command(L"file2", paths)); - autoload.mark_autoload_finished(L"file2"); - - do_test(!autoload.resolve_command(L"file1", paths)); - touch_file(format_string(L"%ls/file1.fish", p1.c_str())); - autoload.invalidate_cache(); - do_test(autoload.resolve_command(L"file1", paths)); - autoload.mark_autoload_finished(L"file1"); - - run(L"rm -Rf %ls", p1.c_str()); - run(L"rm -Rf %ls", p2.c_str()); - } -}; - -static void test_autoload() { - say(L"Testing autoload"); - autoload_tester_t::run_test(); -} - -static void test_complete() { - say(L"Testing complete"); - - struct test_complete_vars_t : environment_t { - std::vector get_names(env_mode_flags_t flags) const override { - UNUSED(flags); - return {L"Foo1", L"Foo2", L"Foo3", L"Bar1", L"Bar2", - L"Bar3", L"alpha", L"ALPHA!", L"gamma1", L"GAMMA2"}; - } - - maybe_t get(const wcstring &key, - env_mode_flags_t mode = ENV_DEFAULT) const override { - UNUSED(mode); - if (key == L"PWD") { - return env_var_t{wgetcwd(), 0}; - } - return {}; - } - }; - test_complete_vars_t vars; - - auto parser = parser_t::principal_parser().shared(); - - auto do_complete = [&](const wcstring &cmd, completion_request_options_t flags) { - return complete(cmd, flags, operation_context_t{parser, vars, no_cancel}); - }; - - completion_list_t completions; - - completions = do_complete(L"$", {}); - completions_sort_and_prioritize(&completions); - do_test(completions.size() == 10); - do_test(completions.at(0).completion == L"alpha"); - do_test(completions.at(1).completion == L"ALPHA!"); - do_test(completions.at(2).completion == L"Bar1"); - do_test(completions.at(3).completion == L"Bar2"); - do_test(completions.at(4).completion == L"Bar3"); - do_test(completions.at(5).completion == L"Foo1"); - do_test(completions.at(6).completion == L"Foo2"); - do_test(completions.at(7).completion == L"Foo3"); - do_test(completions.at(8).completion == L"gamma1"); - do_test(completions.at(9).completion == L"GAMMA2"); - - // Smartcase test. Lowercase inputs match both lowercase and uppercase. - completions = do_complete(L"$a", {}); - completions_sort_and_prioritize(&completions); - do_test(completions.size() == 2); - do_test(completions.at(0).completion == L"$ALPHA!"); - do_test(completions.at(1).completion == L"lpha"); - - completions = do_complete(L"$F", {}); - completions_sort_and_prioritize(&completions); - do_test(completions.size() == 3); - do_test(completions.at(0).completion == L"oo1"); - do_test(completions.at(1).completion == L"oo2"); - do_test(completions.at(2).completion == L"oo3"); - - completions = do_complete(L"$1", {}); - completions_sort_and_prioritize(&completions); - do_test(completions.empty()); - - completion_request_options_t fuzzy_options{}; - fuzzy_options.fuzzy_match = true; - completions = do_complete(L"$1", fuzzy_options); - completions_sort_and_prioritize(&completions); - do_test(completions.size() == 3); - do_test(completions.at(0).completion == L"$Bar1"); - do_test(completions.at(1).completion == L"$Foo1"); - do_test(completions.at(2).completion == L"$gamma1"); - - if (system("mkdir -p 'test/complete_test'")) err(L"mkdir failed"); - if (system("touch 'test/complete_test/has space'")) err(L"touch failed"); - if (system("touch 'test/complete_test/bracket[abc]'")) err(L"touch failed"); -#ifndef __CYGWIN__ // Square brackets are not legal path characters on WIN32/CYGWIN - if (system(R"(touch 'test/complete_test/gnarlybracket\[abc]')")) err(L"touch failed"); -#endif - if (system("touch 'test/complete_test/testfile'")) err(L"touch failed"); - if (system("chmod 700 'test/complete_test/testfile'")) err(L"chmod failed"); - if (system("mkdir -p 'test/complete_test/foo1'")) err(L"mkdir failed"); - if (system("mkdir -p 'test/complete_test/foo2'")) err(L"mkdir failed"); - if (system("mkdir -p 'test/complete_test/foo3'")) err(L"mkdir failed"); - - completions = do_complete(L"echo (test/complete_test/testfil", {}); - do_test(completions.size() == 1); - do_test(completions.at(0).completion == L"e"); - - completions = do_complete(L"echo (ls test/complete_test/testfil", {}); - do_test(completions.size() == 1); - do_test(completions.at(0).completion == L"e"); - - completions = do_complete(L"echo (command ls test/complete_test/testfil", {}); - do_test(completions.size() == 1); - do_test(completions.at(0).completion == L"e"); - - // Completing after spaces - see #2447 - completions = do_complete(L"echo (ls test/complete_test/has\\ ", {}); - do_test(completions.size() == 1); - do_test(completions.at(0).completion == L"space"); - - // Brackets - see #5831 - completions = do_complete(L"echo (ls test/complete_test/bracket[", {}); - do_test(completions.size() == 1); - do_test(completions.at(0).completion == L"test/complete_test/bracket[abc]"); - - wcstring cmdline = L"touch test/complete_test/bracket["; - completions = do_complete(cmdline, {}); - do_test(completions.size() == 1); - do_test(completions.front().completion == L"test/complete_test/bracket[abc]"); - size_t where = cmdline.size(); - wcstring newcmdline = completion_apply_to_command_line( - completions.front().completion, completions.front().flags, cmdline, &where, false); - do_test(newcmdline == L"touch test/complete_test/bracket\\[abc\\] "); - - // #8820 - size_t cursor_pos = 11; - newcmdline = - completion_apply_to_command_line(L"Debug/", COMPLETE_REPLACES_TOKEN | COMPLETE_NO_SPACE, - L"mv debug debug", &cursor_pos, true); - do_test(newcmdline == L"mv debug Debug/"); - -#ifndef __CYGWIN__ // Square brackets are not legal path characters on WIN32/CYGWIN - cmdline = LR"(touch test/complete_test/gnarlybracket\\[)"; - completions = do_complete(cmdline, {}); - do_test(completions.size() == 1); - do_test(completions.front().completion == LR"(test/complete_test/gnarlybracket\[abc])"); - where = cmdline.size(); - newcmdline = completion_apply_to_command_line( - completions.front().completion, completions.front().flags, cmdline, &where, false); - do_test(newcmdline == LR"(touch test/complete_test/gnarlybracket\\\[abc\] )"); -#endif - - // Add a function and test completing it in various ways. - parser->eval(L"function scuttlebutt; end", {}); - - // Complete a function name. - completions = do_complete(L"echo (scuttlebut", {}); - do_test(completions.size() == 1); - do_test(completions.at(0).completion == L"t"); - - // But not with the command prefix. - completions = do_complete(L"echo (command scuttlebut", {}); - do_test(completions.empty()); - - // Not with the builtin prefix. - completions = do_complete(L"echo (builtin scuttlebut", {}); - do_test(completions.empty()); - - // Not after a redirection. - completions = do_complete(L"echo hi > scuttlebut", {}); - do_test(completions.empty()); - - // Trailing spaces (#1261). - completion_mode_t no_files{}; - no_files.no_files = true; - complete_add(L"foobarbaz", false, wcstring(), option_type_args_only, no_files, {}, L"qux", - nullptr, COMPLETE_AUTO_SPACE); - completions = do_complete(L"foobarbaz ", {}); - do_test(completions.size() == 1); - do_test(completions.at(0).completion == L"qux"); - - // Don't complete variable names in single quotes (#1023). - completions = do_complete(L"echo '$Foo", {}); - do_test(completions.empty()); - completions = do_complete(L"echo \\$Foo", {}); - do_test(completions.empty()); - - // File completions. - completions = do_complete(L"cat test/complete_test/te", {}); - do_test(completions.size() == 1); - do_test(completions.at(0).completion == L"stfile"); - completions = do_complete(L"echo sup > test/complete_test/te", {}); - do_test(completions.size() == 1); - do_test(completions.at(0).completion == L"stfile"); - completions = do_complete(L"echo sup > test/complete_test/te", {}); - do_test(completions.size() == 1); - do_test(completions.at(0).completion == L"stfile"); - - if (!pushd("test/complete_test")) return; - completions = do_complete(L"cat te", {}); - do_test(completions.size() == 1); - do_test(completions.at(0).completion == L"stfile"); - do_test(!(completions.at(0).flags & COMPLETE_REPLACES_TOKEN)); - do_test(!(completions.at(0).flags & COMPLETE_DUPLICATES_ARGUMENT)); - completions = do_complete(L"cat testfile te", {}); - do_test(completions.size() == 1); - do_test(completions.at(0).completion == L"stfile"); - do_test(completions.at(0).flags & COMPLETE_DUPLICATES_ARGUMENT); - completions = do_complete(L"cat testfile TE", {}); - do_test(completions.size() == 1); - do_test(completions.at(0).completion == L"testfile"); - do_test(completions.at(0).flags & COMPLETE_REPLACES_TOKEN); - do_test(completions.at(0).flags & COMPLETE_DUPLICATES_ARGUMENT); - completions = do_complete(L"something --abc=te", {}); - do_test(completions.size() == 1); - do_test(completions.at(0).completion == L"stfile"); - completions = do_complete(L"something -abc=te", {}); - do_test(completions.size() == 1); - do_test(completions.at(0).completion == L"stfile"); - completions = do_complete(L"something abc=te", {}); - do_test(completions.size() == 1); - do_test(completions.at(0).completion == L"stfile"); - completions = do_complete(L"something abc=stfile", {}); - do_test(completions.empty()); - completions = do_complete(L"something abc=stfile", fuzzy_options); - do_test(completions.size() == 1); - do_test(completions.at(0).completion == L"abc=testfile"); - - // Zero escapes can cause problems. See issue #1631. - completions = do_complete(L"cat foo\\0", {}); - do_test(completions.empty()); - completions = do_complete(L"cat foo\\0bar", {}); - do_test(completions.empty()); - completions = do_complete(L"cat \\0", {}); - do_test(completions.empty()); - completions = do_complete(L"cat te\\0", {}); - do_test(completions.empty()); - - popd(); - completions.clear(); - - // Test abbreviations. - parser->eval(L"function testabbrsonetwothreefour; end", {}); - abbrs_get_set()->add(L"somename", L"testabbrsonetwothreezero", L"expansion", - abbrs_position_t::command, false); - completions = complete(L"testabbrsonetwothree", {}, parser->context()); - do_test(completions.size() == 2); - do_test(completions.at(0).completion == L"four"); - do_test((completions.at(0).flags & COMPLETE_NO_SPACE) == 0); - // Abbreviations should not have a space after them. - do_test(completions.at(1).completion == L"zero"); - do_test((completions.at(1).flags & COMPLETE_NO_SPACE) != 0); - abbrs_get_set()->erase(L"testabbrsonetwothreezero"); - - // Test wraps. - do_test(comma_join(complete_get_wrap_targets(L"wrapper1")).empty()); - complete_add_wrapper(L"wrapper1", L"wrapper2"); - do_test(comma_join(complete_get_wrap_targets(L"wrapper1")) == L"wrapper2"); - complete_add_wrapper(L"wrapper2", L"wrapper3"); - do_test(comma_join(complete_get_wrap_targets(L"wrapper1")) == L"wrapper2"); - do_test(comma_join(complete_get_wrap_targets(L"wrapper2")) == L"wrapper3"); - complete_add_wrapper(L"wrapper3", L"wrapper1"); // loop! - do_test(comma_join(complete_get_wrap_targets(L"wrapper1")) == L"wrapper2"); - do_test(comma_join(complete_get_wrap_targets(L"wrapper2")) == L"wrapper3"); - do_test(comma_join(complete_get_wrap_targets(L"wrapper3")) == L"wrapper1"); - complete_remove_wrapper(L"wrapper1", L"wrapper2"); - do_test(comma_join(complete_get_wrap_targets(L"wrapper1")).empty()); - do_test(comma_join(complete_get_wrap_targets(L"wrapper2")) == L"wrapper3"); - do_test(comma_join(complete_get_wrap_targets(L"wrapper3")) == L"wrapper1"); - - // Test cd wrapping chain - if (!pushd("test/complete_test")) err(L"pushd(\"test/complete_test\") failed"); - - complete_add_wrapper(L"cdwrap1", L"cd"); - complete_add_wrapper(L"cdwrap2", L"cdwrap1"); - - completion_list_t cd_compl = do_complete(L"cd ", {}); - completions_sort_and_prioritize(&cd_compl); - - completion_list_t cdwrap1_compl = do_complete(L"cdwrap1 ", {}); - completions_sort_and_prioritize(&cdwrap1_compl); - - completion_list_t cdwrap2_compl = do_complete(L"cdwrap2 ", {}); - completions_sort_and_prioritize(&cdwrap2_compl); - - size_t min_compl_size = - std::min(cd_compl.size(), std::min(cdwrap1_compl.size(), cdwrap2_compl.size())); - - do_test(cd_compl.size() == min_compl_size); - do_test(cdwrap1_compl.size() == min_compl_size); - do_test(cdwrap2_compl.size() == min_compl_size); - for (size_t i = 0; i < min_compl_size; ++i) { - do_test(cd_compl[i].completion == cdwrap1_compl[i].completion); - do_test(cdwrap1_compl[i].completion == cdwrap2_compl[i].completion); - } - - complete_remove_wrapper(L"cdwrap1", L"cd"); - complete_remove_wrapper(L"cdwrap2", L"cdwrap1"); - popd(); -} - static void test_1_completion(wcstring line, const wcstring &completion, complete_flags_t flags, bool append_only, wcstring expected, long source_line) { // str is given with a caret, which we use to represent the cursor position. Find it. @@ -2585,232 +1775,6 @@ static void test_completion_insertions() { TEST_1_COMPLETION(L": (:^ ''", L"", 0, false, L": (: ^''"); } -static void perform_one_autosuggestion_cd_test(const wcstring &command, const wcstring &expected, - const environment_t &vars, long line) { - completion_list_t comps = - complete(command, completion_request_options_t::autosuggest(), operation_context_t{vars}); - - bool expects_error = (expected == L""); - - if (comps.empty() && !expects_error) { - std::fwprintf(stderr, L"line %ld: autosuggest_suggest_special() failed for command %ls\n", - line, command.c_str()); - do_test_from(!comps.empty(), line); - return; - } else if (!comps.empty() && expects_error) { - std::fwprintf(stderr, - L"line %ld: autosuggest_suggest_special() was expected to fail but did not, " - L"for command %ls\n", - line, command.c_str()); - do_test_from(comps.empty(), line); - } - - if (!comps.empty()) { - completions_sort_and_prioritize(&comps); - const completion_t &suggestion = comps.at(0); - - if (suggestion.completion != expected) { - std::fwprintf( - stderr, - L"line %ld: complete() for cd returned the wrong expected string for command %ls\n", - line, command.c_str()); - std::fwprintf(stderr, L" actual: %ls\n", suggestion.completion.c_str()); - std::fwprintf(stderr, L"expected: %ls\n", expected.c_str()); - do_test_from(suggestion.completion == expected, line); - } - } -} - -static void perform_one_completion_cd_test(const wcstring &command, const wcstring &expected, - const environment_t &vars, long line) { - completion_list_t comps = complete( - command, {}, operation_context_t{parser_t::principal_parser().shared(), vars, no_cancel}); - - bool expects_error = (expected == L""); - - if (comps.empty() && !expects_error) { - std::fwprintf(stderr, L"line %ld: autosuggest_suggest_special() failed for command %ls\n", - line, command.c_str()); - do_test_from(!comps.empty(), line); - return; - } else if (!comps.empty() && expects_error) { - std::fwprintf(stderr, - L"line %ld: autosuggest_suggest_special() was expected to fail but did not, " - L"for command %ls\n", - line, command.c_str()); - do_test_from(comps.empty(), line); - } - - if (!comps.empty()) { - completions_sort_and_prioritize(&comps); - const completion_t &suggestion = comps.at(0); - - if (suggestion.completion != expected) { - std::fwprintf(stderr, - L"line %ld: complete() for cd tab completion returned the wrong expected " - L"string for command %ls\n", - line, command.c_str()); - std::fwprintf(stderr, L" actual: %ls\n", suggestion.completion.c_str()); - std::fwprintf(stderr, L"expected: %ls\n", expected.c_str()); - do_test_from(suggestion.completion == expected, line); - } - } -} - -// Testing test_autosuggest_suggest_special, in particular for properly handling quotes and -// backslashes. -static void test_autosuggest_suggest_special() { - // We execute LSAN with use_tls=0 under CI to avoid a SIGSEGV crash in LSAN itself. - // Unfortunately, this causes it to incorrectly flag a memory leak here that doesn't reproduce - // locally with use_tls=1. -#ifdef FISH_CI_SAN - __lsan::ScopedDisabler disable_leak_detection{}; -#endif - - if (system("mkdir -p 'test/autosuggest_test/0foobar'")) err(L"mkdir failed"); - if (system("mkdir -p 'test/autosuggest_test/1foo bar'")) err(L"mkdir failed"); - if (system("mkdir -p 'test/autosuggest_test/2foo bar'")) err(L"mkdir failed"); -#ifndef __CYGWIN__ - // Cygwin disallows backslashes in filenames. - if (system("mkdir -p 'test/autosuggest_test/3foo\\bar'")) err(L"mkdir failed"); -#endif - if (system("mkdir -p test/autosuggest_test/4foo\\'bar")) { - err(L"mkdir failed"); // a path with a single quote - } - if (system("mkdir -p test/autosuggest_test/5foo\\\"bar")) { - err(L"mkdir failed"); // a path with a double quote - } - // This is to ensure tilde expansion is handled. See the `cd ~/test_autosuggest_suggest_specia` - // test below. - // Fake out the home directory - parser_t::principal_parser().vars().set_one(L"HOME", ENV_LOCAL | ENV_EXPORT, L"test/test-home"); - if (system("mkdir -p test/test-home/test_autosuggest_suggest_special/")) { - err(L"mkdir failed"); - } - if (system("mkdir -p test/autosuggest_test/start/unique2/unique3/multi4")) { - err(L"mkdir failed"); - } - if (system("mkdir -p test/autosuggest_test/start/unique2/unique3/multi42")) { - err(L"mkdir failed"); - } - if (system("mkdir -p test/autosuggest_test/start/unique2/.hiddenDir/moreStuff")) { - err(L"mkdir failed"); - } - - // Ensure symlink don't cause us to chase endlessly. - if (system("mkdir -p test/autosuggest_test/has_loop/loopy")) { - err(L"mkdir failed"); - } - (void)unlink("test/autosuggest_test/has_loop/loopy/loop"); - if (symlink("../loopy", "test/autosuggest_test/has_loop/loopy/loop")) { - err(L"symlink failed"); - } - - const wcstring wd = L"test/autosuggest_test"; - - pwd_environment_t vars{}; - vars.vars[L"HOME"] = parser_t::principal_parser().vars().get(L"HOME")->as_string(); - - perform_one_autosuggestion_cd_test(L"cd test/autosuggest_test/0", L"foobar/", vars, __LINE__); - perform_one_autosuggestion_cd_test(L"cd \"test/autosuggest_test/0", L"foobar/", vars, __LINE__); - perform_one_autosuggestion_cd_test(L"cd 'test/autosuggest_test/0", L"foobar/", vars, __LINE__); - perform_one_autosuggestion_cd_test(L"cd test/autosuggest_test/1", L"foo bar/", vars, __LINE__); - perform_one_autosuggestion_cd_test(L"cd \"test/autosuggest_test/1", L"foo bar/", vars, - __LINE__); - perform_one_autosuggestion_cd_test(L"cd 'test/autosuggest_test/1", L"foo bar/", vars, __LINE__); - perform_one_autosuggestion_cd_test(L"cd test/autosuggest_test/2", L"foo bar/", vars, __LINE__); - perform_one_autosuggestion_cd_test(L"cd \"test/autosuggest_test/2", L"foo bar/", vars, - __LINE__); - perform_one_autosuggestion_cd_test(L"cd 'test/autosuggest_test/2", L"foo bar/", vars, - __LINE__); -#ifndef __CYGWIN__ - perform_one_autosuggestion_cd_test(L"cd test/autosuggest_test/3", L"foo\\bar/", vars, __LINE__); - perform_one_autosuggestion_cd_test(L"cd \"test/autosuggest_test/3", L"foo\\bar/", vars, - __LINE__); - perform_one_autosuggestion_cd_test(L"cd 'test/autosuggest_test/3", L"foo\\bar/", vars, - __LINE__); -#endif - perform_one_autosuggestion_cd_test(L"cd test/autosuggest_test/4", L"foo'bar/", vars, __LINE__); - perform_one_autosuggestion_cd_test(L"cd \"test/autosuggest_test/4", L"foo'bar/", vars, - __LINE__); - perform_one_autosuggestion_cd_test(L"cd 'test/autosuggest_test/4", L"foo'bar/", vars, __LINE__); - perform_one_autosuggestion_cd_test(L"cd test/autosuggest_test/5", L"foo\"bar/", vars, __LINE__); - perform_one_autosuggestion_cd_test(L"cd \"test/autosuggest_test/5", L"foo\"bar/", vars, - __LINE__); - perform_one_autosuggestion_cd_test(L"cd 'test/autosuggest_test/5", L"foo\"bar/", vars, - __LINE__); - - vars.vars[L"AUTOSUGGEST_TEST_LOC"] = wd; - perform_one_autosuggestion_cd_test(L"cd $AUTOSUGGEST_TEST_LOC/0", L"foobar/", vars, __LINE__); - perform_one_autosuggestion_cd_test(L"cd ~/test_autosuggest_suggest_specia", L"l/", vars, - __LINE__); - - perform_one_autosuggestion_cd_test(L"cd test/autosuggest_test/start/", L"unique2/unique3/", - vars, __LINE__); - - perform_one_autosuggestion_cd_test(L"cd test/autosuggest_test/has_loop/", L"loopy/loop/", vars, - __LINE__); - - if (!pushd(wcs2zstring(wd).c_str())) return; - perform_one_autosuggestion_cd_test(L"cd 0", L"foobar/", vars, __LINE__); - perform_one_autosuggestion_cd_test(L"cd \"0", L"foobar/", vars, __LINE__); - perform_one_autosuggestion_cd_test(L"cd '0", L"foobar/", vars, __LINE__); - perform_one_autosuggestion_cd_test(L"cd 1", L"foo bar/", vars, __LINE__); - perform_one_autosuggestion_cd_test(L"cd \"1", L"foo bar/", vars, __LINE__); - perform_one_autosuggestion_cd_test(L"cd '1", L"foo bar/", vars, __LINE__); - perform_one_autosuggestion_cd_test(L"cd 2", L"foo bar/", vars, __LINE__); - perform_one_autosuggestion_cd_test(L"cd \"2", L"foo bar/", vars, __LINE__); - perform_one_autosuggestion_cd_test(L"cd '2", L"foo bar/", vars, __LINE__); -#ifndef __CYGWIN__ - perform_one_autosuggestion_cd_test(L"cd 3", L"foo\\bar/", vars, __LINE__); - perform_one_autosuggestion_cd_test(L"cd \"3", L"foo\\bar/", vars, __LINE__); - perform_one_autosuggestion_cd_test(L"cd '3", L"foo\\bar/", vars, __LINE__); -#endif - perform_one_autosuggestion_cd_test(L"cd 4", L"foo'bar/", vars, __LINE__); - perform_one_autosuggestion_cd_test(L"cd \"4", L"foo'bar/", vars, __LINE__); - perform_one_autosuggestion_cd_test(L"cd '4", L"foo'bar/", vars, __LINE__); - perform_one_autosuggestion_cd_test(L"cd 5", L"foo\"bar/", vars, __LINE__); - perform_one_autosuggestion_cd_test(L"cd \"5", L"foo\"bar/", vars, __LINE__); - perform_one_autosuggestion_cd_test(L"cd '5", L"foo\"bar/", vars, __LINE__); - - // A single quote should defeat tilde expansion. - perform_one_autosuggestion_cd_test(L"cd '~/test_autosuggest_suggest_specia'", L"", vars, - __LINE__); - - // Don't crash on ~ (issue #2696). Note this is cwd dependent. - if (system("mkdir -p '~absolutelynosuchuser/path1/path2/'")) err(L"mkdir failed"); - perform_one_autosuggestion_cd_test(L"cd ~absolutelynosuchus", L"er/path1/path2/", vars, - __LINE__); - perform_one_autosuggestion_cd_test(L"cd ~absolutelynosuchuser/", L"path1/path2/", vars, - __LINE__); - perform_one_completion_cd_test(L"cd ~absolutelynosuchus", L"er/", vars, __LINE__); - perform_one_completion_cd_test(L"cd ~absolutelynosuchuser/", L"path1/", vars, __LINE__); - - parser_t::principal_parser().vars().remove(L"HOME", ENV_LOCAL | ENV_EXPORT); - popd(); -} - -static void perform_one_autosuggestion_should_ignore_test(const wcstring &command, long line) { - completion_list_t comps = complete(command, completion_request_options_t::autosuggest(), - operation_context_t::empty()); - do_test(comps.empty()); - if (!comps.empty()) { - const wcstring &suggestion = comps.front().completion; - std::fwprintf(stderr, L"line %ld: complete() expected to return nothing for %ls\n", line, - command.c_str()); - std::fwprintf(stderr, L" instead got: %ls\n", suggestion.c_str()); - } -} - -static void test_autosuggestion_ignores() { - say(L"Testing scenarios that should produce no autosuggestions"); - // Do not do file autosuggestions immediately after certain statement terminators - see #1631. - perform_one_autosuggestion_should_ignore_test(L"echo PIPE_TEST|", __LINE__); - perform_one_autosuggestion_should_ignore_test(L"echo PIPE_TEST&", __LINE__); - perform_one_autosuggestion_should_ignore_test(L"echo PIPE_TEST#comment", __LINE__); - perform_one_autosuggestion_should_ignore_test(L"echo PIPE_TEST;", __LINE__); -} - static void test_autosuggestion_combining() { say(L"Testing autosuggestion combining"); do_test(combine_command_and_autosuggestion(L"alpha", L"alphabeta") == L"alphabeta"); @@ -2826,41 +1790,9 @@ static void test_autosuggestion_combining() { do_test(combine_command_and_autosuggestion(L"alpha", L"ALPHA") == L"alpha"); } -static void test_history_matches(history_search_t &search, const std::vector &expected, - unsigned from_line) { - std::vector found; - while (search.go_to_next_match(history_search_direction_t::backward)) { - found.push_back(search.current_string()); - } - do_test_from(expected == found, from_line); - if (expected != found) { - fprintf(stderr, "Expected %ls, found %ls\n", comma_join(expected).c_str(), - comma_join(found).c_str()); - } -} - -static bool history_contains(history_t *history, const wcstring &txt) { - bool result = false; - size_t i; - for (i = 1;; i++) { - history_item_t item = history->item_at_index(i); - if (item.empty()) break; - - if (item.str() == txt) { - result = true; - break; - } - } - return result; -} - -static bool history_contains(const std::shared_ptr &history, const wcstring &txt) { - return history_contains(history.get(), txt); -} - static void test_input() { say(L"Testing input"); - inputter_t input{parser_t::principal_parser()}; + inputter_t input{parser_principal_parser()->deref()}; // Ensure sequences are order independent. Here we add two bindings where the first is a prefix // of the second, and then emit the second key list. The second binding should be invoked, not // the first! @@ -2941,240 +1873,8 @@ static void test_undo() { do_test(line.text() == L"abc"); } -#define UVARS_PER_THREAD 8 #define UVARS_TEST_PATH L"test/fish_uvars_test/varsfile.txt" -static int test_universal_helper(int x) { - callback_data_list_t callbacks; - env_universal_t uvars; - uvars.initialize_at_path(callbacks, UVARS_TEST_PATH); - for (int j = 0; j < UVARS_PER_THREAD; j++) { - const wcstring key = format_string(L"key_%d_%d", x, j); - const wcstring val = format_string(L"val_%d_%d", x, j); - uvars.set(key, env_var_t{val, 0}); - bool synced = uvars.sync(callbacks); - if (!synced) { - err(L"Failed to sync universal variables after modification"); - } - } - - // Last step is to delete the first key. - uvars.remove(format_string(L"key_%d_%d", x, 0)); - bool synced = uvars.sync(callbacks); - if (!synced) { - err(L"Failed to sync universal variables after deletion"); - } - return 0; -} - -static void test_universal() { - say(L"Testing universal variables"); - if (system("mkdir -p test/fish_uvars_test/")) err(L"mkdir failed"); - - const int threads = 1; - for (int i = 0; i < threads; i++) { - iothread_perform([=]() { test_universal_helper(i); }); - } - iothread_drain_all(); - - env_universal_t uvars; - callback_data_list_t callbacks; - uvars.initialize_at_path(callbacks, UVARS_TEST_PATH); - for (int i = 0; i < threads; i++) { - for (int j = 0; j < UVARS_PER_THREAD; j++) { - const wcstring key = format_string(L"key_%d_%d", i, j); - maybe_t expected_val; - if (j == 0) { - expected_val = none(); - } else { - expected_val = env_var_t(format_string(L"val_%d_%d", i, j), 0); - } - const maybe_t var = uvars.get(key); - if (j == 0) assert(!expected_val); - if (var != expected_val) { - const wchar_t *missing_desc = L""; - err(L"Wrong value for key %ls: expected %ls, got %ls\n", key.c_str(), - (expected_val ? expected_val->as_string().c_str() : missing_desc), - (var ? var->as_string().c_str() : missing_desc)); - } - } - } - system_assert("rm -Rf test/fish_uvars_test/"); -} - -static void test_universal_output() { - say(L"Testing universal variable output"); - - const env_var_t::env_var_flags_t flag_export = env_var_t::flag_export; - const env_var_t::env_var_flags_t flag_pathvar = env_var_t::flag_pathvar; - - var_table_t vars; - vars[L"varA"] = env_var_t(std::vector{L"ValA1", L"ValA2"}, 0); - vars[L"varB"] = env_var_t(std::vector{L"ValB1"}, flag_export); - vars[L"varC"] = env_var_t(std::vector{L"ValC1"}, 0); - vars[L"varD"] = env_var_t(std::vector{L"ValD1"}, flag_export | flag_pathvar); - vars[L"varE"] = env_var_t(std::vector{L"ValE1", L"ValE2"}, flag_pathvar); - - std::string text = env_universal_t::serialize_with_vars(vars); - const char *expected = - "# This file contains fish universal variable definitions.\n" - "# VERSION: 3.0\n" - "SETUVAR varA:ValA1\\x1eValA2\n" - "SETUVAR --export varB:ValB1\n" - "SETUVAR varC:ValC1\n" - "SETUVAR --export --path varD:ValD1\n" - "SETUVAR --path varE:ValE1\\x1eValE2\n"; - do_test(text == expected); -} - -static void test_universal_parsing() { - say(L"Testing universal variable parsing"); - const char *input = - "# This file contains fish universal variable definitions.\n" - "# VERSION: 3.0\n" - "SETUVAR varA:ValA1\\x1eValA2\n" - "SETUVAR --export varB:ValB1\n" - "SETUVAR --nonsenseflag varC:ValC1\n" - "SETUVAR --export --path varD:ValD1\n" - "SETUVAR --path --path varE:ValE1\\x1eValE2\n"; - - const env_var_t::env_var_flags_t flag_export = env_var_t::flag_export; - const env_var_t::env_var_flags_t flag_pathvar = env_var_t::flag_pathvar; - - var_table_t vars; - vars[L"varA"] = env_var_t(std::vector{L"ValA1", L"ValA2"}, 0); - vars[L"varB"] = env_var_t(std::vector{L"ValB1"}, flag_export); - vars[L"varC"] = env_var_t(std::vector{L"ValC1"}, 0); - vars[L"varD"] = env_var_t(std::vector{L"ValD1"}, flag_export | flag_pathvar); - vars[L"varE"] = env_var_t(std::vector{L"ValE1", L"ValE2"}, flag_pathvar); - - var_table_t parsed_vars; - env_universal_t::populate_variables(input, &parsed_vars); - do_test(vars == parsed_vars); -} - -static void test_universal_parsing_legacy() { - say(L"Testing universal variable legacy parsing"); - const char *input = - "# This file contains fish universal variable definitions.\n" - "SET varA:ValA1\\x1eValA2\n" - "SET_EXPORT varB:ValB1\n"; - - var_table_t vars; - vars[L"varA"] = env_var_t(std::vector{L"ValA1", L"ValA2"}, 0); - vars[L"varB"] = env_var_t(std::vector{L"ValB1"}, env_var_t::flag_export); - - var_table_t parsed_vars; - env_universal_t::populate_variables(input, &parsed_vars); - do_test(vars == parsed_vars); -} - -static bool callback_data_less_than(const callback_data_t &a, const callback_data_t &b) { - return a.key < b.key; -} - -static void test_universal_callbacks() { - say(L"Testing universal callbacks"); - if (system("mkdir -p test/fish_uvars_test/")) err(L"mkdir failed"); - callback_data_list_t callbacks; - env_universal_t uvars1; - env_universal_t uvars2; - uvars1.initialize_at_path(callbacks, UVARS_TEST_PATH); - uvars2.initialize_at_path(callbacks, UVARS_TEST_PATH); - - env_var_t::env_var_flags_t noflags = 0; - - // Put some variables into both. - uvars1.set(L"alpha", env_var_t{L"1", noflags}); - uvars1.set(L"beta", env_var_t{L"1", noflags}); - uvars1.set(L"delta", env_var_t{L"1", noflags}); - uvars1.set(L"epsilon", env_var_t{L"1", noflags}); - uvars1.set(L"lambda", env_var_t{L"1", noflags}); - uvars1.set(L"kappa", env_var_t{L"1", noflags}); - uvars1.set(L"omicron", env_var_t{L"1", noflags}); - - uvars1.sync(callbacks); - uvars2.sync(callbacks); - - // Change uvars1. - uvars1.set(L"alpha", env_var_t{L"2", noflags}); // changes value - uvars1.set(L"beta", env_var_t{L"1", env_var_t::flag_export}); // changes export - uvars1.remove(L"delta"); // erases value - uvars1.set(L"epsilon", env_var_t{L"1", noflags}); // changes nothing - uvars1.sync(callbacks); - - // Change uvars2. It should treat its value as correct and ignore changes from uvars1. - uvars2.set(L"lambda", {L"1", noflags}); // same value - uvars2.set(L"kappa", {L"2", noflags}); // different value - - // Now see what uvars2 sees. - callbacks.clear(); - uvars2.sync(callbacks); - - // Sort them to get them in a predictable order. - std::sort(callbacks.begin(), callbacks.end(), callback_data_less_than); - - // Should see exactly three changes. - do_test(callbacks.size() == 3); - do_test(callbacks.at(0).key == L"alpha"); - do_test(callbacks.at(0).val->as_string() == L"2"); - do_test(callbacks.at(1).key == L"beta"); - do_test(callbacks.at(1).val->as_string() == L"1"); - do_test(callbacks.at(2).key == L"delta"); - do_test(callbacks.at(2).val == none()); - system_assert("rm -Rf test/fish_uvars_test/"); -} - -static void test_universal_formats() { - say(L"Testing universal format detection"); - const struct { - const char *str; - uvar_format_t format; - } tests[] = { - {"# VERSION: 3.0", uvar_format_t::fish_3_0}, - {"# version: 3.0", uvar_format_t::fish_2_x}, - {"# blah blahVERSION: 3.0", uvar_format_t::fish_2_x}, - {"stuff\n# blah blahVERSION: 3.0", uvar_format_t::fish_2_x}, - {"# blah\n# VERSION: 3.0", uvar_format_t::fish_3_0}, - {"# blah\n#VERSION: 3.0", uvar_format_t::fish_3_0}, - {"# blah\n#VERSION:3.0", uvar_format_t::fish_3_0}, - {"# blah\n#VERSION:3.1", uvar_format_t::future}, - }; - for (const auto &test : tests) { - uvar_format_t format = env_universal_t::format_for_contents(test.str); - do_test(format == test.format); - } -} - -static void test_universal_ok_to_save() { - // Ensure we don't try to save after reading from a newer fish. - say(L"Testing universal Ok to save"); - if (system("mkdir -p test/fish_uvars_test/")) err(L"mkdir failed"); - constexpr const char contents[] = "# VERSION: 99999.99\n"; - FILE *fp = fopen(wcs2zstring(UVARS_TEST_PATH).c_str(), "w"); - assert(fp && "Failed to open UVARS_TEST_PATH for writing"); - fwrite(contents, const_strlen(contents), 1, fp); - fclose(fp); - - file_id_t before_id = file_id_for_path(UVARS_TEST_PATH); - do_test(before_id != kInvalidFileID && "UVARS_TEST_PATH should be readable"); - - callback_data_list_t cbs; - env_universal_t uvars; - uvars.initialize_at_path(cbs, UVARS_TEST_PATH); - do_test(!uvars.is_ok_to_save() && "Should not be OK to save"); - uvars.sync(cbs); - cbs.clear(); - do_test(!uvars.is_ok_to_save() && "Should still not be OK to save"); - uvars.set(L"SOMEVAR", env_var_t{wcstring{L"SOMEVALUE"}, 0}); - uvars.sync(cbs); - - // Ensure file is same. - file_id_t after_id = file_id_for_path(UVARS_TEST_PATH); - do_test(before_id == after_id && "UVARS_TEST_PATH should not have changed"); - system_assert("rm -Rf test/fish_uvars_test/"); -} - bool poll_notifier(const std::unique_ptr ¬e) { if (note->poll()) return true; @@ -3266,572 +1966,6 @@ static void test_universal_notifiers() { test_notifiers_with_strategy(strategy); } -class history_tests_t { - public: - static void test_history(); - static void test_history_merge(); - static void test_history_path_detection(); - static void test_history_formats(); - static void test_history_races(); - static void test_history_races_pound_on_history(size_t item_count, size_t idx); -}; - -static wcstring random_string() { - wcstring result; - size_t max = 1 + random() % 32; - while (max--) { - wchar_t c = 1 + random() % ESCAPE_TEST_CHAR; - result.push_back(c); - } - return result; -} - -void history_tests_t::test_history() { - history_search_t searcher; - say(L"Testing history"); - - const std::vector items = {L"Gamma", L"beta", L"BetA", L"Beta", L"alpha", - L"AlphA", L"Alpha", L"alph", L"ALPH", L"ZZZ"}; - const history_search_flags_t nocase = history_search_ignore_case; - - // Populate a history. - std::shared_ptr history = history_t::with_name(L"test_history"); - history->clear(); - for (const wcstring &s : items) { - history->add(s); - } - - // Helper to set expected items to those matching a predicate, in reverse order. - std::vector expected; - auto set_expected = [&](const std::function &filt) { - expected.clear(); - for (const auto &s : items) { - if (filt(s)) expected.push_back(s); - } - std::reverse(expected.begin(), expected.end()); - }; - - // Items matching "a", case-sensitive. - searcher = history_search_t(history, L"a"); - set_expected([](const wcstring &s) { return s.find(L'a') != wcstring::npos; }); - test_history_matches(searcher, expected, __LINE__); - - // Items matching "alpha", case-insensitive. - searcher = history_search_t(history, L"AlPhA", history_search_type_t::contains, nocase); - set_expected([](const wcstring &s) { return wcstolower(s).find(L"alpha") != wcstring::npos; }); - test_history_matches(searcher, expected, __LINE__); - - // Items matching "et", case-sensitive. - searcher = history_search_t(history, L"et"); - set_expected([](const wcstring &s) { return s.find(L"et") != wcstring::npos; }); - test_history_matches(searcher, expected, __LINE__); - - // Items starting with "be", case-sensitive. - searcher = history_search_t(history, L"be", history_search_type_t::prefix, 0); - set_expected([](const wcstring &s) { return string_prefixes_string(L"be", s); }); - test_history_matches(searcher, expected, __LINE__); - - // Items starting with "be", case-insensitive. - searcher = history_search_t(history, L"be", history_search_type_t::prefix, nocase); - set_expected( - [](const wcstring &s) { return string_prefixes_string_case_insensitive(L"be", s); }); - test_history_matches(searcher, expected, __LINE__); - - // Items exactly matching "alph", case-sensitive. - searcher = history_search_t(history, L"alph", history_search_type_t::exact, 0); - set_expected([](const wcstring &s) { return s == L"alph"; }); - test_history_matches(searcher, expected, __LINE__); - - // Items exactly matching "alph", case-insensitive. - searcher = history_search_t(history, L"alph", history_search_type_t::exact, nocase); - set_expected([](const wcstring &s) { return wcstolower(s) == L"alph"; }); - test_history_matches(searcher, expected, __LINE__); - - // Test item removal case-sensitive. - searcher = history_search_t(history, L"Alpha"); - test_history_matches(searcher, {L"Alpha"}, __LINE__); - history->remove(L"Alpha"); - searcher = history_search_t(history, L"Alpha"); - test_history_matches(searcher, {}, __LINE__); - - // Test history escaping and unescaping, yaml, etc. - history_item_list_t before, after; - history->clear(); - size_t i, max = 100; - for (i = 1; i <= max; i++) { - // Generate a value. - wcstring value = wcstring(L"test item ") + to_string(i); - - // Maybe add some backslashes. - if (i % 3 == 0) value.append(L"(slashies \\\\\\ slashies)"); - - // Generate some paths. - path_list_t paths; - size_t count = random() % 6; - while (count--) { - paths.push_back(random_string()); - } - - // Record this item. - history_item_t item(value, time(nullptr)); - item.required_paths = paths; - before.push_back(item); - history->add(std::move(item)); - } - history->save(); - - // Empty items should just be dropped (#6032). - history->add(L""); - do_test(!history->item_at_index(1).contents.empty()); - - // Read items back in reverse order and ensure they're the same. - for (i = 100; i >= 1; i--) { - history_item_t item = history->item_at_index(i); - do_test(!item.empty()); - after.push_back(item); - } - do_test(before.size() == after.size()); - for (size_t i = 0; i < before.size(); i++) { - const history_item_t &bef = before.at(i), &aft = after.at(i); - do_test(bef.contents == aft.contents); - do_test(bef.creation_timestamp == aft.creation_timestamp); - do_test(bef.required_paths == aft.required_paths); - } - - // Clean up after our tests. - history->clear(); -} -// Wait until the next second. -static void time_barrier() { - time_t start = time(nullptr); - do { - usleep(1000); - } while (time(nullptr) == start); -} - -static std::vector generate_history_lines(size_t item_count, size_t idx) { - std::vector result; - result.reserve(item_count); - for (unsigned long i = 0; i < item_count; i++) { - result.push_back(format_string(L"%ld %lu", (unsigned long)idx, (unsigned long)i)); - } - return result; -} - -void history_tests_t::test_history_races_pound_on_history(size_t item_count, size_t idx) { - // Called in child thread to modify history. - history_t hist(L"race_test"); - const std::vector hist_lines = generate_history_lines(item_count, idx); - for (const wcstring &line : hist_lines) { - hist.add(line); - hist.save(); - } -} - -void history_tests_t::test_history_races() { - // This always fails under WSL - if (is_windows_subsystem_for_linux()) { - return; - } - - // This fails too often on Github Actions, - // leading to a bunch of spurious test failures on unrelated PRs. - // For now it's better to disable it. - // TODO: Figure out *why* it does that and fix it. - if (getenv("CI")) { - return; - } - - say(L"Testing history race conditions"); - - // Test concurrent history writing. - // How many concurrent writers we have - constexpr size_t RACE_COUNT = 4; - - // How many items each writer makes - constexpr size_t ITEM_COUNT = 256; - - // Ensure history is clear. - history_t(L"race_test").clear(); - - // hist.chaos_mode = true; - - std::thread children[RACE_COUNT]; - for (size_t i = 0; i < RACE_COUNT; i++) { - children[i] = std::thread([=] { test_history_races_pound_on_history(ITEM_COUNT, i); }); - } - - // Wait for all children. - for (std::thread &child : children) { - child.join(); - } - - // Compute the expected lines. - std::array, RACE_COUNT> expected_lines; - for (size_t i = 0; i < RACE_COUNT; i++) { - expected_lines[i] = generate_history_lines(ITEM_COUNT, i); - } - - // Ensure we consider the lines that have been outputted as part of our history. - time_barrier(); - - // Ensure that we got sane, sorted results. - history_t hist(L"race_test"); - hist.chaos_mode = !true; - - // History is enumerated from most recent to least - // Every item should be the last item in some array - size_t hist_idx; - for (hist_idx = 1;; hist_idx++) { - history_item_t item = hist.item_at_index(hist_idx); - if (item.empty()) break; - - bool found = false; - for (std::vector &list : expected_lines) { - auto iter = std::find(list.begin(), list.end(), item.contents); - if (iter != list.end()) { - found = true; - - // Remove everything from this item on - auto cursor = list.end(); - if (cursor + 1 != list.end()) { - while (--cursor != iter) { - err(L"Item dropped from history: %ls", cursor->c_str()); - } - } - list.erase(iter, list.end()); - break; - } - } - if (!found) { - err(L"Line '%ls' found in history, but not found in some array", item.str().c_str()); - for (std::vector &list : expected_lines) { - if (!list.empty()) { - fprintf(stderr, "\tRemaining: %ls\n", list.back().c_str()); - } - } - } - } - - // +1 to account for history's 1-based offset - size_t expected_idx = RACE_COUNT * ITEM_COUNT + 1; - if (hist_idx != expected_idx) { - err(L"Expected %lu items, but instead got %lu items", expected_idx, hist_idx); - } - - // See if anything is left in the arrays - for (const std::vector &list : expected_lines) { - for (const wcstring &str : list) { - err(L"Line '%ls' still left in the array", str.c_str()); - } - } - hist.clear(); -} - -void history_tests_t::test_history_merge() { - // In a single fish process, only one history is allowed to exist with the given name But it's - // common to have multiple history instances with the same name active in different processes, - // e.g. when you have multiple shells open. We try to get that right and merge all their history - // together. Test that case. - say(L"Testing history merge"); - const size_t count = 3; - const wcstring name = L"merge_test"; - std::shared_ptr hists[count] = {std::make_shared(name), - std::make_shared(name), - std::make_shared(name)}; - const wcstring texts[count] = {L"History 1", L"History 2", L"History 3"}; - const wcstring alt_texts[count] = {L"History Alt 1", L"History Alt 2", L"History Alt 3"}; - - // Make sure history is clear. - for (auto &hist : hists) { - hist->clear(); - } - - // Make sure we don't add an item in the same second as we created the history. - time_barrier(); - - // Add a different item to each. - for (size_t i = 0; i < count; i++) { - hists[i]->add(texts[i]); - } - - // Save them. - for (auto &hist : hists) { - hist->save(); - } - - // Make sure each history contains what it ought to, but they have not leaked into each other. - for (size_t i = 0; i < count; i++) { - for (size_t j = 0; j < count; j++) { - bool does_contain = history_contains(hists[i], texts[j]); - bool should_contain = (i == j); - do_test(should_contain == does_contain); - } - } - - // Make a new history. It should contain everything. The time_barrier() is so that the timestamp - // is newer, since we only pick up items whose timestamp is before the birth stamp. - time_barrier(); - std::shared_ptr everything = std::make_shared(name); - for (const auto &text : texts) { - do_test(history_contains(everything, text)); - } - - // Tell all histories to merge. Now everybody should have everything. - for (auto &hist : hists) { - hist->incorporate_external_changes(); - } - - // Everyone should also have items in the same order (#2312) - std::vector hist_vals1; - hists[0]->get_history(hist_vals1); - for (const auto &hist : hists) { - std::vector hist_vals2; - hist->get_history(hist_vals2); - do_test(hist_vals1 == hist_vals2); - } - - // Add some more per-history items. - for (size_t i = 0; i < count; i++) { - hists[i]->add(alt_texts[i]); - } - // Everybody should have old items, but only one history should have each new item. - for (size_t i = 0; i < count; i++) { - for (size_t j = 0; j < count; j++) { - // Old item. - do_test(history_contains(hists[i], texts[j])); - - // New item. - bool does_contain = history_contains(hists[i], alt_texts[j]); - bool should_contain = (i == j); - do_test(should_contain == does_contain); - } - } - - // Make sure incorporate_external_changes doesn't drop items! (#3496) - history_t *const writer = hists[0].get(); - history_t *const reader = hists[1].get(); - const wcstring more_texts[] = {L"Item_#3496_1", L"Item_#3496_2", L"Item_#3496_3", - L"Item_#3496_4", L"Item_#3496_5", L"Item_#3496_6"}; - for (size_t i = 0; i < sizeof more_texts / sizeof *more_texts; i++) { - // time_barrier because merging will ignore items that may be newer - if (i > 0) time_barrier(); - writer->add(more_texts[i]); - writer->incorporate_external_changes(); - reader->incorporate_external_changes(); - for (size_t j = 0; j < i; j++) { - do_test(history_contains(reader, more_texts[j])); - } - } - everything->clear(); -} - -void history_tests_t::test_history_path_detection() { - // Regression test for #7582. - say(L"Testing history path detection"); - char tmpdirbuff[] = "/tmp/fish_test_history.XXXXXX"; - wcstring tmpdir = str2wcstring(mkdtemp(tmpdirbuff)); - if (!string_suffixes_string(L"/", tmpdir)) { - tmpdir.push_back(L'/'); - } - - // Place one valid file in the directory. - wcstring filename = L"testfile"; - std::string path = wcs2zstring(tmpdir + filename); - FILE *f = fopen(path.c_str(), "w"); - if (!f) { - err(L"Failed to open test file from history path detection"); - return; - } - fclose(f); - - std::shared_ptr vars = std::make_shared(); - vars->vars[L"PWD"] = tmpdir; - vars->vars[L"HOME"] = tmpdir; - - std::shared_ptr history = history_t::with_name(L"path_detection"); - history_t::add_pending_with_file_detection(history, L"cmd0 not/a/valid/path", vars); - history_t::add_pending_with_file_detection(history, L"cmd1 " + filename, vars); - history_t::add_pending_with_file_detection(history, L"cmd2 " + tmpdir + L"/" + filename, vars); - history_t::add_pending_with_file_detection(history, L"cmd3 $HOME/" + filename, vars); - history_t::add_pending_with_file_detection(history, L"cmd4 $HOME/notafile", vars); - history_t::add_pending_with_file_detection(history, L"cmd5 ~/" + filename, vars); - history_t::add_pending_with_file_detection(history, L"cmd6 ~/notafile", vars); - history_t::add_pending_with_file_detection(history, L"cmd7 ~/*f*", vars); - history_t::add_pending_with_file_detection(history, L"cmd8 ~/*zzz*", vars); - history->resolve_pending(); - - constexpr size_t hist_size = 9; - if (history->size() != hist_size) { - err(L"history has wrong size: %lu but expected %lu", (unsigned long)history->size(), - (unsigned long)hist_size); - history->clear(); - return; - } - - // Expected sets of paths. - std::vector expected[hist_size] = { - {}, // cmd0 - {filename}, // cmd1 - {tmpdir + L"/" + filename}, // cmd2 - {L"$HOME/" + filename}, // cmd3 - {}, // cmd4 - {L"~/" + filename}, // cmd5 - {}, // cmd6 - {}, // cmd7 - we do not expand globs - {}, // cmd8 - }; - - size_t lap; - const size_t maxlap = 128; - for (lap = 0; lap < maxlap; lap++) { - int failures = 0; - bool last = (lap + 1 == maxlap); - for (size_t i = 1; i <= hist_size; i++) { - if (history->item_at_index(i).required_paths != expected[hist_size - i]) { - failures += 1; - if (last) { - err(L"Wrong detected paths for item %lu", (unsigned long)i); - } - } - } - if (failures == 0) { - break; - } - // The file detection takes a little time since it occurs in the background. - // Loop until the test passes. - usleep(1E6 / 500); // 1 msec - } - // fprintf(stderr, "History saving took %lu laps\n", (unsigned long)lap); - history->clear(); -} - -static bool install_sample_history(const wchar_t *name) { - wcstring path; - if (!path_get_data(path)) { - err(L"Failed to get data directory"); - return false; - } - char command[512]; - snprintf(command, sizeof command, "cp tests/%ls %ls/%ls_history", name, path.c_str(), name); - if (system(command)) { - err(L"Failed to copy sample history"); - return false; - } - return true; -} - -/// Indicates whether the history is equal to the given null-terminated array of strings. -static bool history_equals(const shared_ptr &hist, const wchar_t *const *strings) { - // Count our expected items. - size_t expected_count = 0; - while (strings[expected_count]) { - expected_count++; - } - - // Ensure the contents are the same. - size_t history_idx = 1; - size_t array_idx = 0; - for (;;) { - const wchar_t *expected = strings[array_idx]; - history_item_t item = hist->item_at_index(history_idx); - if (expected == nullptr) { - if (!item.empty()) { - err(L"Expected empty item at history index %lu, instead found: %ls", history_idx, - item.str().c_str()); - } - break; - } else { - if (item.str() != expected) { - err(L"Expected '%ls', found '%ls' at index %lu", expected, item.str().c_str(), - history_idx); - } - } - history_idx++; - array_idx++; - } - - return true; -} - -void history_tests_t::test_history_formats() { - const wchar_t *name; - - // Test inferring and reading legacy and bash history formats. - name = L"history_sample_fish_1_x"; - say(L"Testing %ls", name); - if (!install_sample_history(name)) { - err(L"Couldn't open file tests/%ls", name); - } else { - // Note: This is backwards from what appears in the file. - const wchar_t *const expected[] = { - L"#def", L"echo #abc", L"function yay\necho hi\nend", L"cd foobar", L"ls /", nullptr}; - - auto test_history = history_t::with_name(name); - if (!history_equals(test_history, expected)) { - err(L"test_history_formats failed for %ls\n", name); - } - test_history->clear(); - } - - name = L"history_sample_fish_2_0"; - say(L"Testing %ls", name); - if (!install_sample_history(name)) { - err(L"Couldn't open file tests/%ls", name); - } else { - const wchar_t *const expected[] = {L"echo this has\\\nbackslashes", - L"function foo\necho bar\nend", L"echo alpha", nullptr}; - - auto test_history = history_t::with_name(name); - if (!history_equals(test_history, expected)) { - err(L"test_history_formats failed for %ls\n", name); - } - test_history->clear(); - } - - say(L"Testing bash import"); - FILE *f = fopen("tests/history_sample_bash", "r"); - if (!f) { - err(L"Couldn't open file tests/history_sample_bash"); - } else { - // The results are in the reverse order that they appear in the bash history file. - // We don't expect whitespace to be elided (#4908: except for leading/trailing whitespace) - const wchar_t *expected[] = {L"EOF", - L"sleep 123", - L"posix_cmd_sub $(is supported but only splits on newlines)", - L"posix_cmd_sub \"$(is supported)\"", - L"a && echo valid construct", - L"final line", - L"echo supsup", - L"export XVAR='exported'", - L"history --help", - L"echo foo", - nullptr}; - auto test_history = history_t::with_name(L"bash_import"); - test_history->populate_from_bash(f); - if (!history_equals(test_history, expected)) { - err(L"test_history_formats failed for bash import\n"); - } - test_history->clear(); - fclose(f); - } - - name = L"history_sample_corrupt1"; - say(L"Testing %ls", name); - if (!install_sample_history(name)) { - err(L"Couldn't open file tests/%ls", name); - } else { - // We simply invoke get_string_representation. If we don't die, the test is a success. - auto test_history = history_t::with_name(name); - const wchar_t *expected[] = {L"no_newline_at_end_of_file", L"corrupt_prefix", - L"this_command_is_ok", nullptr}; - if (!history_equals(test_history, expected)) { - err(L"test_history_formats failed for %ls\n", name); - } - test_history->clear(); - } -} - static void test_new_parser_correctness() { say(L"Testing parser correctness"); const struct parser_test_t { @@ -4243,456 +2377,6 @@ static void test_error_messages() { } } -static void test_highlighting() { - say(L"Testing syntax highlighting"); - if (!pushd("test/fish_highlight_test/")) return; - cleanup_t pop{[] { popd(); }}; - if (system("mkdir -p dir")) err(L"mkdir failed"); - if (system("mkdir -p cdpath-entry/dir-in-cdpath")) err(L"mkdir failed"); - if (system("touch foo")) err(L"touch failed"); - if (system("touch bar")) err(L"touch failed"); - - // Here are the components of our source and the colors we expect those to be. - struct highlight_component_t { - const wchar_t *txt; - highlight_spec_t color; - bool nospace; - highlight_component_t(const wchar_t *txt, highlight_spec_t color, bool nospace = false) - : txt(txt), color(color), nospace(nospace) {} - }; - const bool ns = true; - - using highlight_component_list_t = std::vector; - std::vector highlight_tests; - - highlight_spec_t param_valid_path{highlight_role_t::param}; - param_valid_path.valid_path = true; - - highlight_tests.push_back({{L"echo", highlight_role_t::command}, - {L"./foo", param_valid_path}, - {L"&", highlight_role_t::statement_terminator}}); - - highlight_tests.push_back({ - {L"command", highlight_role_t::keyword}, - {L"echo", highlight_role_t::command}, - {L"abc", highlight_role_t::param}, - {L"foo", param_valid_path}, - {L"&", highlight_role_t::statement_terminator}, - }); - - highlight_tests.push_back({ - {L"echo", highlight_role_t::command}, - {L"foo&bar", highlight_role_t::param}, - {L"foo", highlight_role_t::param, /*nospace=*/true}, - {L"&", highlight_role_t::statement_terminator}, - {L"echo", highlight_role_t::command}, - {L"&>", highlight_role_t::redirection}, - }); - - highlight_tests.push_back({ - {L"if command", highlight_role_t::keyword}, - {L"ls", highlight_role_t::command}, - {L"; ", highlight_role_t::statement_terminator}, - {L"echo", highlight_role_t::command}, - {L"abc", highlight_role_t::param}, - {L"; ", highlight_role_t::statement_terminator}, - {L"/bin/definitely_not_a_command", highlight_role_t::error}, - {L"; ", highlight_role_t::statement_terminator}, - {L"end", highlight_role_t::keyword}, - }); - - // Verify that cd shows errors for non-directories. - highlight_tests.push_back({ - {L"cd", highlight_role_t::command}, - {L"dir", param_valid_path}, - }); - - highlight_tests.push_back({ - {L"cd", highlight_role_t::command}, - {L"foo", highlight_role_t::error}, - }); - - highlight_tests.push_back({ - {L"cd", highlight_role_t::command}, - {L"--help", highlight_role_t::option}, - {L"-h", highlight_role_t::option}, - {L"definitely_not_a_directory", highlight_role_t::error}, - }); - - highlight_tests.push_back({ - {L"cd", highlight_role_t::command}, - {L"dir-in-cdpath", param_valid_path}, - }); - - // Command substitutions. - highlight_tests.push_back({ - {L"echo", highlight_role_t::command}, - {L"param1", highlight_role_t::param}, - {L"-l", highlight_role_t::option}, - {L"--", highlight_role_t::option}, - {L"-l", highlight_role_t::param}, - {L"(", highlight_role_t::operat}, - {L"ls", highlight_role_t::command}, - {L"-l", highlight_role_t::option}, - {L"--", highlight_role_t::option}, - {L"-l", highlight_role_t::param}, - {L"param2", highlight_role_t::param}, - {L")", highlight_role_t::operat}, - {L"|", highlight_role_t::statement_terminator}, - {L"cat", highlight_role_t::command}, - }); - highlight_tests.push_back({ - {L"true", highlight_role_t::command}, - {L"$(", highlight_role_t::operat}, - {L"true", highlight_role_t::command}, - {L")", highlight_role_t::operat}, - }); - highlight_tests.push_back({ - {L"true", highlight_role_t::command}, - {L"\"before", highlight_role_t::quote}, - {L"$(", highlight_role_t::operat}, - {L"true", highlight_role_t::command}, - {L"param1", highlight_role_t::param}, - {L")", highlight_role_t::operat}, - {L"after\"", highlight_role_t::quote}, - {L"param2", highlight_role_t::param}, - }); - highlight_tests.push_back({ - {L"true", highlight_role_t::command}, - {L"\"", highlight_role_t::error}, - {L"unclosed quote", highlight_role_t::quote}, - {L"$(", highlight_role_t::operat}, - {L"true", highlight_role_t::command}, - {L")", highlight_role_t::operat}, - }); - - // Redirections substitutions. - highlight_tests.push_back({ - {L"echo", highlight_role_t::command}, - {L"param1", highlight_role_t::param}, - - // Input redirection. - {L"<", highlight_role_t::redirection}, - {L"/bin/echo", highlight_role_t::redirection}, - - // Output redirection to a valid fd. - {L"1>&2", highlight_role_t::redirection}, - - // Output redirection to an invalid fd. - {L"2>&", highlight_role_t::redirection}, - {L"LOL", highlight_role_t::error}, - - // Just a param, not a redirection. - {L"test/blah", highlight_role_t::param}, - - // Input redirection from directory. - {L"<", highlight_role_t::redirection}, - {L"test/", highlight_role_t::error}, - - // Output redirection to an invalid path. - {L"3>", highlight_role_t::redirection}, - {L"/not/a/valid/path/nope", highlight_role_t::error}, - - // Output redirection to directory. - {L"3>", highlight_role_t::redirection}, - {L"test/nope/", highlight_role_t::error}, - - // Redirections to overflow fd. - {L"99999999999999999999>&2", highlight_role_t::error}, - {L"2>&", highlight_role_t::redirection}, - {L"99999999999999999999", highlight_role_t::error}, - - // Output redirection containing a command substitution. - {L"4>", highlight_role_t::redirection}, - {L"(", highlight_role_t::operat}, - {L"echo", highlight_role_t::command}, - {L"test/somewhere", highlight_role_t::param}, - {L")", highlight_role_t::operat}, - - // Just another param. - {L"param2", highlight_role_t::param}, - }); - - highlight_tests.push_back({ - {L"for", highlight_role_t::keyword}, - {L"x", highlight_role_t::param}, - {L"in", highlight_role_t::keyword}, - {L"set-by-for-1", highlight_role_t::param}, - {L"set-by-for-2", highlight_role_t::param}, - {L";", highlight_role_t::statement_terminator}, - {L"echo", highlight_role_t::command}, - {L">", highlight_role_t::redirection}, - {L"$x", highlight_role_t::redirection}, - {L";", highlight_role_t::statement_terminator}, - {L"end", highlight_role_t::keyword}, - }); - - highlight_tests.push_back({ - {L"set", highlight_role_t::command}, - {L"x", highlight_role_t::param}, - {L"set-by-set", highlight_role_t::param}, - {L";", highlight_role_t::statement_terminator}, - {L"echo", highlight_role_t::command}, - {L">", highlight_role_t::redirection}, - {L"$x", highlight_role_t::redirection}, - {L"2>", highlight_role_t::redirection}, - {L"$totally_not_x", highlight_role_t::error}, - {L"<", highlight_role_t::redirection}, - {L"$x_but_its_an_impostor", highlight_role_t::error}, - }); - - highlight_tests.push_back({ - {L"x", highlight_role_t::param, ns}, - {L"=", highlight_role_t::operat, ns}, - {L"set-by-variable-override", highlight_role_t::param, ns}, - {L"echo", highlight_role_t::command}, - {L">", highlight_role_t::redirection}, - {L"$x", highlight_role_t::redirection}, - }); - - highlight_tests.push_back({ - {L"end", highlight_role_t::error}, - {L";", highlight_role_t::statement_terminator}, - {L"if", highlight_role_t::keyword}, - {L"end", highlight_role_t::error}, - }); - - highlight_tests.push_back({ - {L"echo", highlight_role_t::command}, - {L"'", highlight_role_t::error}, - {L"single_quote", highlight_role_t::quote}, - {L"$stuff", highlight_role_t::quote}, - }); - - highlight_tests.push_back({ - {L"echo", highlight_role_t::command}, - {L"\"", highlight_role_t::error}, - {L"double_quote", highlight_role_t::quote}, - {L"$stuff", highlight_role_t::operat}, - }); - - highlight_tests.push_back({ - {L"echo", highlight_role_t::command}, - {L"$foo", highlight_role_t::operat}, - {L"\"", highlight_role_t::quote}, - {L"$bar", highlight_role_t::operat}, - {L"\"", highlight_role_t::quote}, - {L"$baz[", highlight_role_t::operat}, - {L"1 2..3", highlight_role_t::param}, - {L"]", highlight_role_t::operat}, - }); - - highlight_tests.push_back({ - {L"for", highlight_role_t::keyword}, - {L"i", highlight_role_t::param}, - {L"in", highlight_role_t::keyword}, - {L"1 2 3", highlight_role_t::param}, - {L";", highlight_role_t::statement_terminator}, - {L"end", highlight_role_t::keyword}, - }); - - highlight_tests.push_back({ - {L"echo", highlight_role_t::command}, - {L"$$foo[", highlight_role_t::operat}, - {L"1", highlight_role_t::param}, - {L"][", highlight_role_t::operat}, - {L"2", highlight_role_t::param}, - {L"]", highlight_role_t::operat}, - {L"[3]", highlight_role_t::param}, // two dollar signs, so last one is not an expansion - }); - - highlight_tests.push_back({ - {L"cat", highlight_role_t::command}, - {L"/dev/null", param_valid_path}, - {L"|", highlight_role_t::statement_terminator}, - // This is bogus, but we used to use "less" here and that doesn't have to be installed. - {L"cat", highlight_role_t::command}, - {L"2>", highlight_role_t::redirection}, - }); - - // Highlight path-prefixes only at the cursor. - highlight_tests.push_back({ - {L"cat", highlight_role_t::command}, - {L"/dev/nu", highlight_role_t::param}, - {L"/dev/nu", param_valid_path}, - }); - - highlight_tests.push_back({ - {L"if", highlight_role_t::keyword}, - {L"true", highlight_role_t::command}, - {L"&&", highlight_role_t::operat}, - {L"false", highlight_role_t::command}, - {L";", highlight_role_t::statement_terminator}, - {L"or", highlight_role_t::operat}, - {L"false", highlight_role_t::command}, - {L"||", highlight_role_t::operat}, - {L"true", highlight_role_t::command}, - {L";", highlight_role_t::statement_terminator}, - {L"and", highlight_role_t::operat}, - {L"not", highlight_role_t::operat}, - {L"!", highlight_role_t::operat}, - {L"true", highlight_role_t::command}, - {L";", highlight_role_t::statement_terminator}, - {L"end", highlight_role_t::keyword}, - }); - - highlight_tests.push_back({ - {L"echo", highlight_role_t::command}, - {L"%self", highlight_role_t::operat}, - {L"not%self", highlight_role_t::param}, - {L"self%not", highlight_role_t::param}, - }); - - highlight_tests.push_back({ - {L"false", highlight_role_t::command}, - {L"&|", highlight_role_t::statement_terminator}, - {L"true", highlight_role_t::command}, - }); - - highlight_tests.push_back({ - {L"HOME", highlight_role_t::param}, - {L"=", highlight_role_t::operat, ns}, - {L".", highlight_role_t::param, ns}, - {L"VAR1", highlight_role_t::param}, - {L"=", highlight_role_t::operat, ns}, - {L"VAL1", highlight_role_t::param, ns}, - {L"VAR", highlight_role_t::param}, - {L"=", highlight_role_t::operat, ns}, - {L"false", highlight_role_t::command}, - {L"|&", highlight_role_t::error}, - {L"true", highlight_role_t::command}, - {L"stuff", highlight_role_t::param}, - }); - - highlight_tests.push_back({ - {L"echo", highlight_role_t::command}, - {L")", highlight_role_t::error}, - }); - - highlight_tests.push_back({ - {L"echo", highlight_role_t::command}, - {L"stuff", highlight_role_t::param}, - {L"# comment", highlight_role_t::comment}, - }); - - highlight_tests.push_back({ - {L"echo", highlight_role_t::command}, - {L"--", highlight_role_t::option}, - {L"-s", highlight_role_t::param}, - }); - - // Overlong paths don't crash (#7837). - const wcstring overlong = get_overlong_path(); - highlight_tests.push_back({ - {L"touch", highlight_role_t::command}, - {overlong.c_str(), highlight_role_t::param}, - }); - - highlight_tests.push_back({ - {L"a", highlight_role_t::param}, - {L"=", highlight_role_t::operat, ns}, - }); - - // Highlighting works across escaped line breaks (#8444). - highlight_tests.push_back({ - {L"echo", highlight_role_t::command}, - {L"$FISH_\\\n", highlight_role_t::operat}, - {L"VERSION", highlight_role_t::operat, ns}, - }); - - auto &vars = parser_t::principal_parser().vars(); - // Verify variables and wildcards in commands using /bin/cat. - vars.set(L"VARIABLE_IN_COMMAND", ENV_LOCAL, {L"a"}); - vars.set(L"VARIABLE_IN_COMMAND2", ENV_LOCAL, {L"at"}); - vars.set(L"CDPATH", ENV_LOCAL, {L"./cdpath-entry"}); - highlight_tests.push_back( - {{L"/bin/ca", highlight_role_t::command, ns}, {L"*", highlight_role_t::operat, ns}}); - - highlight_tests.push_back({{L"/bin/c", highlight_role_t::command, ns}, - {L"{$VARIABLE_IN_COMMAND}", highlight_role_t::operat, ns}, - {L"*", highlight_role_t::operat, ns}}); - - highlight_tests.push_back({{L"/bin/c", highlight_role_t::command, ns}, - {L"{$VARIABLE_IN_COMMAND}", highlight_role_t::operat, ns}, - {L"*", highlight_role_t::operat, ns}}); - - highlight_tests.push_back({{L"/bin/c", highlight_role_t::command, ns}, - {L"$VARIABLE_IN_COMMAND2", highlight_role_t::operat, ns}}); - - highlight_tests.push_back({{L"$EMPTY_VARIABLE", highlight_role_t::error}}); - highlight_tests.push_back({{L"\"$EMPTY_VARIABLE\"", highlight_role_t::error}}); - - highlight_tests.push_back({ - {L"echo", highlight_role_t::command}, - {L"\\UFDFD", highlight_role_t::escape}, - }); -#if WCHAR_T_BITS > 16 - highlight_tests.push_back({ - {L"echo", highlight_role_t::command}, - {L"\\U10FFFF", highlight_role_t::escape}, - }); - highlight_tests.push_back({ - {L"echo", highlight_role_t::command}, - {L"\\U110000", highlight_role_t::error}, - }); -#endif - - highlight_tests.push_back({ - {L">", highlight_role_t::error}, - {L"echo", highlight_role_t::error}, - }); - - bool saved_flag = feature_test(feature_flag_t::ampersand_nobg_in_token); - feature_set(feature_flag_t::ampersand_nobg_in_token, true); - for (const highlight_component_list_t &components : highlight_tests) { - // Generate the text. - wcstring text; - std::vector expected_colors; - for (const highlight_component_t &comp : components) { - if (!text.empty() && !comp.nospace) { - text.push_back(L' '); - expected_colors.emplace_back(); - } - text.append(comp.txt); - expected_colors.resize(text.size(), comp.color); - } - do_test(expected_colors.size() == text.size()); - - std::vector colors(text.size()); - highlight_shell(text, colors, operation_context_t{vars}, true /* io_ok */, text.size()); - - if (expected_colors.size() != colors.size()) { - err(L"Color vector has wrong size! Expected %lu, actual %lu", expected_colors.size(), - colors.size()); - } - do_test(expected_colors.size() == colors.size()); - for (size_t i = 0; i < text.size(); i++) { - // Hackish space handling. We don't care about the colors in spaces. - if (text.at(i) == L' ') continue; - - if (expected_colors.at(i) != colors.at(i)) { - // Make a fancy caret under the token - auto e_col = expected_colors.at(i); - auto a_col = colors.at(i); - auto j = i + 1; - while (j < colors.size() && expected_colors.at(j) == e_col && colors.at(j) == a_col) - j++; - if (j == colors.size() - 1) j++; - const wcstring spaces(i, L' '); - const wcstring carets(j - i, L'^'); - err(L"Wrong color in test at index %lu-%lu in text (expected %#x, actual " - L"%#x):\n%ls\n%ls%ls", - i, j - 1, expected_colors.at(i), colors.at(i), text.c_str(), spaces.c_str(), - carets.c_str()); - i = j; - } - } - } - feature_set(feature_flag_t::ampersand_nobg_in_token, saved_flag); - vars.remove(L"VARIABLE_IN_COMMAND", ENV_DEFAULT); - vars.remove(L"VARIABLE_IN_COMMAND2", ENV_DEFAULT); -} - static void test_wwrite_to_fd() { say(L"Testing wwrite_to_fd"); char t[] = "/tmp/fish_test_wwrite.XXXXXX"; @@ -4741,7 +2425,7 @@ static void test_wwrite_to_fd() { /// Helper for test_timezone_env_vars(). long return_timezone_hour(time_t tstamp, const wchar_t *timezone) { - auto &vars = parser_t::principal_parser().vars(); + env_stack_t vars{parser_principal_parser()->deref().vars_boxed()}; struct tm ltime; char ltime_str[3]; char *str_ptr; @@ -4761,45 +2445,18 @@ long return_timezone_hour(time_t tstamp, const wchar_t *timezone) { return strtol(ltime_str, &str_ptr, 10); } -/// Verify that setting special env vars have the expected effect on the current shell process. -static void test_timezone_env_vars() { - // Confirm changing the timezone affects fish's idea of the local time. - time_t tstamp = time(nullptr); - - long first_tstamp = return_timezone_hour(tstamp, L"UTC-1"); - long second_tstamp = return_timezone_hour(tstamp, L"UTC-2"); - long delta = second_tstamp - first_tstamp; - if (delta != 1 && delta != -23) { - err(L"expected a one hour timezone delta got %ld", delta); - } -} - -/// Verify that setting special env vars have the expected effect on the current shell process. -static void test_env_vars() { - test_timezone_env_vars(); - // TODO: Add tests for the locale and ncurses vars. - - env_var_t v1 = {L"abc", env_var_t::flag_export}; - env_var_t v2 = {std::vector{L"abc"}, env_var_t::flag_export}; - env_var_t v3 = {std::vector{L"abc"}, 0}; - env_var_t v4 = {std::vector{L"abc", L"def"}, env_var_t::flag_export}; - do_test(v1 == v2 && !(v1 != v2)); - do_test(v1 != v3 && !(v1 == v3)); - do_test(v1 != v4 && !(v1 == v4)); -} - static void test_env_snapshot() { if (system("mkdir -p test/fish_env_snapshot_test/")) err(L"mkdir failed"); bool pushed = pushd("test/fish_env_snapshot_test"); do_test(pushed); - auto &vars = parser_t::principal_parser().vars(); + env_stack_t vars{parser_principal_parser()->deref().vars_boxed()}; vars.push(true); wcstring before_pwd = vars.get(L"PWD")->as_string(); - vars.set(L"test_env_snapshot_var", 0, {L"before"}); + vars.set(L"test_env_snapshot_var", 0, std::vector{L"before"}); const auto snapshot = vars.snapshot(); - vars.set(L"PWD", 0, {L"/newdir"}); - vars.set(L"test_env_snapshot_var", 0, {L"after"}); - vars.set(L"test_env_snapshot_var_2", 0, {L"after"}); + vars.set(L"PWD", 0, std::vector{L"/newdir"}); + vars.set(L"test_env_snapshot_var", 0, std::vector{L"after"}); + vars.set(L"test_env_snapshot_var_2", 0, std::vector{L"after"}); // vars should be unaffected by the snapshot do_test(vars.get(L"PWD")->as_string() == L"/newdir"); @@ -4812,7 +2469,7 @@ static void test_env_snapshot() { do_test(snapshot->get(L"test_env_snapshot_var_2") == none()); // snapshots see global var changes except for perproc like PWD - vars.set(L"test_env_snapshot_var_3", ENV_GLOBAL, {L"reallyglobal"}); + vars.set(L"test_env_snapshot_var_3", ENV_GLOBAL, std::vector{L"reallyglobal"}); do_test(vars.get(L"test_env_snapshot_var_3")->as_string() == L"reallyglobal"); do_test(snapshot->get(L"test_env_snapshot_var_3")->as_string() == L"reallyglobal"); @@ -4842,11 +2499,11 @@ static void test_illegal_command_exit_code() { {L"abc?def", STATUS_UNMATCHED_WILDCARD}, }; - const io_chain_t empty_ios; - parser_t &parser = parser_t::principal_parser(); + auto empty_ios = new_io_chain(); + const parser_t &parser = parser_principal_parser()->deref(); for (const auto &test : tests) { - parser.eval(test.txt, empty_ios); + parser.eval(test.txt, *empty_ios); int exit_status = parser.get_last_status(); if (exit_status != test.result) { @@ -5123,74 +2780,6 @@ void test_dirname_basename() { do_test(wbasename(longpath) == L"overlong"); } -static void test_topic_monitor() { - say(L"Testing topic monitor"); - auto monitor_box = new_topic_monitor(); - topic_monitor_t &monitor = *monitor_box; - generation_list_t gens{}; - constexpr auto t = topic_t::sigchld; - gens.sigchld = 0; - do_test(monitor.generation_for_topic(t) == 0); - auto changed = monitor.check(&gens, false /* wait */); - do_test(!changed); - do_test(gens.sigchld == 0); - - monitor.post(t); - changed = monitor.check(&gens, true /* wait */); - do_test(changed); - do_test(gens.at(t) == 1); - do_test(monitor.generation_for_topic(t) == 1); - - monitor.post(t); - do_test(monitor.generation_for_topic(t) == 2); - changed = monitor.check(&gens, true /* wait */); - do_test(changed); - do_test(gens.sigchld == 2); -} - -static void test_topic_monitor_torture() { - say(L"Torture-testing topic monitor"); - auto monitor_box = new_topic_monitor(); - topic_monitor_t &monitor = *monitor_box; - const size_t thread_count = 64; - constexpr auto t1 = topic_t::sigchld; - constexpr auto t2 = topic_t::sighupint; - std::vector gens; - gens.resize(thread_count, invalid_generations()); - std::atomic post_count{}; - for (auto &gen : gens) { - gen = monitor.current_generations(); - post_count += 1; - monitor.post(t1); - } - - std::atomic completed{}; - std::vector threads; - for (size_t i = 0; i < thread_count; i++) { - threads.emplace_back( - [&](size_t i) { - for (size_t j = 0; j < (1 << 11); j++) { - auto before = gens[i]; - auto changed = monitor.check(&gens[i], true /* wait */); - (void)changed; - do_test(before.at(t1) < gens[i].at(t1)); - do_test(gens[i].at(t1) <= post_count); - do_test(gens[i].at(t2) == 0); - } - auto amt = completed.fetch_add(1, std::memory_order_relaxed); - (void)amt; - }, - i); - } - - while (completed.load(std::memory_order_relaxed) < thread_count) { - post_count += 1; - monitor.post(t1); - std::this_thread::yield(); - } - for (auto &t : threads) t.join(); -} - static void test_pipes() { say(L"Testing pipes"); // Here we just test that each pipe has CLOEXEC set and is in the high range. @@ -5309,13 +2898,14 @@ static const test_t s_tests[]{ {TEST_GROUP("utility_functions"), test_utility_functions}, {TEST_GROUP("dir_iter"), test_dir_iter}, {TEST_GROUP("wwrite_to_fd"), test_wwrite_to_fd}, - {TEST_GROUP("env_vars"), test_env_vars}, {TEST_GROUP("env"), test_env_snapshot}, {TEST_GROUP("str_to_num"), test_str_to_num}, {TEST_GROUP("enum"), test_enum_set}, {TEST_GROUP("enum"), test_enum_array}, - {TEST_GROUP("highlighting"), test_highlighting}, + {TEST_GROUP("autosuggestion"), test_autosuggestion_combining}, {TEST_GROUP("new_parser_ll2"), test_new_parser_ll2}, + {TEST_GROUP("test_abbreviations"), test_abbreviations}, + {TEST_GROUP("test_escape_sequences"), test_escape_sequences}, {TEST_GROUP("new_parser_fuzzing"), test_new_parser_fuzzing}, {TEST_GROUP("new_parser_correctness"), test_new_parser_correctness}, {TEST_GROUP("new_parser_ad_hoc"), test_new_parser_ad_hoc}, @@ -5331,50 +2921,23 @@ static const test_t s_tests[]{ {TEST_GROUP("debounce"), test_debounce}, {TEST_GROUP("debounce"), test_debounce_timeout}, {TEST_GROUP("parser"), test_parser}, - {TEST_GROUP("cancellation"), test_cancellation}, {TEST_GROUP("utf8"), test_utf8}, - {TEST_GROUP("escape_sequences"), test_escape_sequences}, {TEST_GROUP("lru"), test_lru}, - {TEST_GROUP("expand"), test_expand}, - {TEST_GROUP("expand"), test_expand_overflow}, - {TEST_GROUP("abbreviations"), test_abbreviations}, {TEST_GROUP("wcstod"), test_wcstod}, - {TEST_GROUP("dup2s"), test_dup2s}, - {TEST_GROUP("dup2s"), test_dup2s_fd_for_target_fd}, {TEST_GROUP("pager_navigation"), test_pager_navigation}, {TEST_GROUP("pager_layout"), test_pager_layout}, {TEST_GROUP("word_motion"), test_word_motion}, - {TEST_GROUP("is_potential_path"), test_is_potential_path}, {TEST_GROUP("colors"), test_colors}, - {TEST_GROUP("complete"), test_complete}, - {TEST_GROUP("autoload"), test_autoload}, {TEST_GROUP("input"), test_input}, {TEST_GROUP("undo"), test_undo}, - {TEST_GROUP("universal"), test_universal}, - {TEST_GROUP("universal"), test_universal_output}, - {TEST_GROUP("universal"), test_universal_parsing}, - {TEST_GROUP("universal"), test_universal_parsing_legacy}, - {TEST_GROUP("universal"), test_universal_callbacks}, - {TEST_GROUP("universal"), test_universal_formats}, - {TEST_GROUP("universal"), test_universal_ok_to_save}, {TEST_GROUP("universal"), test_universal_notifiers}, {TEST_GROUP("completion_insertions"), test_completion_insertions}, - {TEST_GROUP("autosuggestion_ignores"), test_autosuggestion_ignores}, - {TEST_GROUP("autosuggestion_combining"), test_autosuggestion_combining}, - {TEST_GROUP("autosuggest_suggest_special"), test_autosuggest_suggest_special}, - {TEST_GROUP("history"), history_tests_t::test_history}, - {TEST_GROUP("history_merge"), history_tests_t::test_history_merge}, - {TEST_GROUP("history_paths"), history_tests_t::test_history_path_detection}, - {TEST_GROUP("history_races"), history_tests_t::test_history_races}, - {TEST_GROUP("history_formats"), history_tests_t::test_history_formats}, {TEST_GROUP("illegal_command_exit_code"), test_illegal_command_exit_code}, {TEST_GROUP("maybe"), test_maybe}, {TEST_GROUP("layout_cache"), test_layout_cache}, {TEST_GROUP("prompt"), test_prompt_truncation}, {TEST_GROUP("normalize"), test_normalize_path}, {TEST_GROUP("dirname"), test_dirname_basename}, - {TEST_GROUP("topics"), test_topic_monitor}, - {TEST_GROUP("topics"), test_topic_monitor_torture}, {TEST_GROUP("pipes"), test_pipes}, {TEST_GROUP("fd_event"), test_fd_event_signaller}, {TEST_GROUP("wgetopt"), test_wgetopt}, @@ -5442,7 +3005,7 @@ int main(int argc, char **argv) { signal_reset_handlers(); // Set PWD from getcwd - fixes #5599 - env_stack_t::principal().set_pwd_from_getcwd(); + env_stack_principal().set_pwd_from_getcwd(); for (const auto &test : s_tests) { if (should_test_function(test.group)) { diff --git a/src/flog.h b/src/flog.h index 72821c0da..9dbd25a96 100644 --- a/src/flog.h +++ b/src/flog.h @@ -110,6 +110,8 @@ class category_list_t { category_t screen{L"screen", L"Screen repaints"}; category_t abbrs{L"abbrs", L"Abbreviation expansion"}; + + category_t refcell{L"refcell", L"Refcell dynamic borrowing"}; }; /// The class responsible for logging. diff --git a/src/function.cpp b/src/function.cpp deleted file mode 100644 index 2f2b6df9b..000000000 --- a/src/function.cpp +++ /dev/null @@ -1,24 +0,0 @@ -// Functions for storing and retrieving function information. These functions also take care of -// autoloading functions in the $fish_function_path. Actual function evaluation is taken care of by -// the parser and to some degree the builtin handling library. -// -#include "config.h" // IWYU pragma: keep - -#include "function.h" - -#include "common.h" - -maybe_t> function_get_props(const wcstring &name) { - if (auto *ptr = function_get_props_raw(name)) { - return rust::Box::from_raw(ptr); - } - return none(); -} - -maybe_t> function_get_props_autoload(const wcstring &name, - parser_t &parser) { - if (auto *ptr = function_get_props_autoload_raw(name, parser)) { - return rust::Box::from_raw(ptr); - } - return none(); -} diff --git a/src/function.h b/src/function.h index 03dec55c1..275d30109 100644 --- a/src/function.h +++ b/src/function.h @@ -1,21 +1,3 @@ -// Prototypes for functions for storing and retrieving function information. These functions also -// take care of autoloading functions in the $fish_function_path. Actual function evaluation is -// taken care of by the parser and to some degree the builtin handling library. -#ifndef FISH_FUNCTION_H -#define FISH_FUNCTION_H - -#include "cxx.h" -#include "maybe.h" - -struct function_properties_t; -class Parser; using parser_t = Parser; - #if INCLUDE_RUST_HEADERS #include "function.rs.h" #endif - -maybe_t> function_get_props(const wcstring &name); -maybe_t> function_get_props_autoload(const wcstring &name, - parser_t &parser); - -#endif diff --git a/src/highlight.cpp b/src/highlight.cpp index 2fbeeeb46..800d2ad06 100644 --- a/src/highlight.cpp +++ b/src/highlight.cpp @@ -1,1326 +1,35 @@ -// Functions for syntax highlighting. -#include "config.h" // IWYU pragma: keep - #include "highlight.h" -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "abbrs.h" -#include "ast.h" -#include "builtin.h" -#include "color.h" -#include "common.h" -#include "env.h" -#include "expand.h" -#include "fallback.h" // IWYU pragma: keep -#include "function.h" -#include "future_feature_flags.h" -#include "highlight.rs.h" -#include "history.h" -#include "maybe.h" -#include "operation_context.h" -#include "output.h" -#include "parse_constants.h" -#include "parse_util.h" -#include "parser.h" -#include "path.h" -#include "redirection.h" -#include "threads.rs.h" -#include "tokenizer.h" -#include "wcstringutil.h" -#include "wildcard.h" -#include "wutil.h" // IWYU pragma: keep - -static const wchar_t *get_highlight_var_name(highlight_role_t role) { - switch (role) { - case highlight_role_t::normal: - return L"fish_color_normal"; - case highlight_role_t::error: - return L"fish_color_error"; - case highlight_role_t::command: - return L"fish_color_command"; - case highlight_role_t::keyword: - return L"fish_color_keyword"; - case highlight_role_t::statement_terminator: - return L"fish_color_end"; - case highlight_role_t::param: - return L"fish_color_param"; - case highlight_role_t::option: - return L"fish_color_option"; - case highlight_role_t::comment: - return L"fish_color_comment"; - case highlight_role_t::search_match: - return L"fish_color_search_match"; - case highlight_role_t::operat: - return L"fish_color_operator"; - case highlight_role_t::escape: - return L"fish_color_escape"; - case highlight_role_t::quote: - return L"fish_color_quote"; - case highlight_role_t::redirection: - return L"fish_color_redirection"; - case highlight_role_t::autosuggestion: - return L"fish_color_autosuggestion"; - case highlight_role_t::selection: - return L"fish_color_selection"; - case highlight_role_t::pager_progress: - return L"fish_pager_color_progress"; - case highlight_role_t::pager_background: - return L"fish_pager_color_background"; - case highlight_role_t::pager_prefix: - return L"fish_pager_color_prefix"; - case highlight_role_t::pager_completion: - return L"fish_pager_color_completion"; - case highlight_role_t::pager_description: - return L"fish_pager_color_description"; - case highlight_role_t::pager_secondary_background: - return L"fish_pager_color_secondary_background"; - case highlight_role_t::pager_secondary_prefix: - return L"fish_pager_color_secondary_prefix"; - case highlight_role_t::pager_secondary_completion: - return L"fish_pager_color_secondary_completion"; - case highlight_role_t::pager_secondary_description: - return L"fish_pager_color_secondary_description"; - case highlight_role_t::pager_selected_background: - return L"fish_pager_color_selected_background"; - case highlight_role_t::pager_selected_prefix: - return L"fish_pager_color_selected_prefix"; - case highlight_role_t::pager_selected_completion: - return L"fish_pager_color_selected_completion"; - case highlight_role_t::pager_selected_description: - return L"fish_pager_color_selected_description"; - } - DIE("invalid highlight role"); +highlight_spec_t::highlight_spec_t() : val(new_highlight_spec()) {} +highlight_spec_t::highlight_spec_t(const highlight_spec_t &other) : val(new_highlight_spec()) { + *this = other; +} +highlight_spec_t &highlight_spec_t::operator=(const highlight_spec_t &other) { + this->val = other.val->clone(); + return *this; } -// Table used to fetch fallback highlights in case the specified one -// wasn't set. -static highlight_role_t get_fallback(highlight_role_t role) { - switch (role) { - case highlight_role_t::normal: - case highlight_role_t::error: - case highlight_role_t::command: - case highlight_role_t::statement_terminator: - case highlight_role_t::param: - case highlight_role_t::search_match: - case highlight_role_t::comment: - case highlight_role_t::operat: - case highlight_role_t::escape: - case highlight_role_t::quote: - case highlight_role_t::redirection: - case highlight_role_t::autosuggestion: - case highlight_role_t::selection: - case highlight_role_t::pager_progress: - case highlight_role_t::pager_background: - case highlight_role_t::pager_prefix: - case highlight_role_t::pager_completion: - case highlight_role_t::pager_description: - return highlight_role_t::normal; - case highlight_role_t::keyword: - return highlight_role_t::command; - case highlight_role_t::option: - return highlight_role_t::param; - case highlight_role_t::pager_secondary_background: - return highlight_role_t::pager_background; - case highlight_role_t::pager_secondary_prefix: - case highlight_role_t::pager_selected_prefix: - return highlight_role_t::pager_prefix; - case highlight_role_t::pager_secondary_completion: - case highlight_role_t::pager_selected_completion: - return highlight_role_t::pager_completion; - case highlight_role_t::pager_secondary_description: - case highlight_role_t::pager_selected_description: - return highlight_role_t::pager_description; - case highlight_role_t::pager_selected_background: - return highlight_role_t::search_match; - } - DIE("invalid highlight role"); +highlight_spec_t::highlight_spec_t(HighlightSpec other) : val(other.clone()) {} +highlight_spec_t::highlight_spec_t(highlight_role_t fg, highlight_role_t bg) + : val(new_highlight_spec()) { + val->foreground = fg; + val->background = bg; } -/// Determine if the filesystem containing the given fd is case insensitive for lookups regardless -/// of whether it preserves the case when saving a pathname. -/// -/// Returns: -/// false: the filesystem is not case insensitive -/// true: the file system is case insensitive -using case_sensitivity_cache_t = std::unordered_map; -static bool fs_is_case_insensitive(const wcstring &path, int fd, - case_sensitivity_cache_t &case_sensitivity_cache) { - bool result = false; -#ifdef _PC_CASE_SENSITIVE - // Try the cache first. - auto cache = case_sensitivity_cache.find(path); - if (cache != case_sensitivity_cache.end()) { - /* Use the cached value */ - result = cache->second; - } else { - // Ask the system. A -1 value means error (so assume case sensitive), a 1 value means case - // sensitive, and a 0 value means case insensitive. - long ret = fpathconf(fd, _PC_CASE_SENSITIVE); - result = (ret == 0); - case_sensitivity_cache[path] = result; - } -#else - // Silence lint tools about the unused parameters. - UNUSED(path); - UNUSED(fd); - UNUSED(case_sensitivity_cache); -#endif - return result; +highlight_spec_t::operator HighlightSpec() const { return *val; } + +highlight_spec_t highlight_spec_t::make_background(highlight_role_t bg_role) { + return highlight_spec_t{highlight_role_t::normal, bg_role}; } -/// Tests whether the specified string cpath is the prefix of anything we could cd to. directories -/// is a list of possible parent directories (typically either the working directory, or the -/// cdpath). This does I/O! -/// -/// Hack: if out_suggested_cdpath is not NULL, it returns the autosuggestion for cd. This descends -/// the deepest unique directory hierarchy. -/// -/// We expect the path to already be unescaped. -bool is_potential_path(const wcstring &potential_path_fragment, bool at_cursor, - const std::vector &directories, const operation_context_t &ctx, - path_flags_t flags) { - ASSERT_IS_BACKGROUND_THREAD(); - - if (ctx.check_cancel()) return false; - - const bool require_dir = static_cast(flags & PATH_REQUIRE_DIR); - wcstring clean_potential_path_fragment; - bool has_magic = false; - - wcstring path_with_magic(potential_path_fragment); - if (flags & PATH_EXPAND_TILDE) expand_tilde(path_with_magic, ctx.vars); - - for (auto c : path_with_magic) { - switch (c) { - case PROCESS_EXPAND_SELF: - case VARIABLE_EXPAND: - case VARIABLE_EXPAND_SINGLE: - case BRACE_BEGIN: - case BRACE_END: - case BRACE_SEP: - case ANY_CHAR: - case ANY_STRING: - case ANY_STRING_RECURSIVE: { - has_magic = true; - break; - } - case INTERNAL_SEPARATOR: { - break; - } - default: { - clean_potential_path_fragment.push_back(c); - break; - } - } - } - - if (has_magic || clean_potential_path_fragment.empty()) { - return false; - } - - // Don't test the same path multiple times, which can happen if the path is absolute and the - // CDPATH contains multiple entries. - std::unordered_set checked_paths; - - // Keep a cache of which paths / filesystems are case sensitive. - case_sensitivity_cache_t case_sensitivity_cache; - - for (const wcstring &wd : directories) { - if (ctx.check_cancel()) return false; - wcstring abs_path = path_apply_working_directory(clean_potential_path_fragment, wd); - bool must_be_full_dir = abs_path.at(abs_path.size() - 1) == L'/'; - if (flags & PATH_FOR_CD) { - abs_path = normalize_path(abs_path); - } - - // Skip this if it's empty or we've already checked it. - if (abs_path.empty() || checked_paths.count(abs_path)) continue; - checked_paths.insert(abs_path); - - // If the user is still typing the argument, we want to highlight it if it's the prefix - // of a valid path. This means we need to potentially walk all files in some directory. - // There are two easy cases where we can skip this: - // 1. If the argument ends with a slash, it must be a valid directory, no prefix. - // 2. If the cursor is not at the argument, it means the user is definitely not typing it, - // so we can skip the prefix-match. - if (must_be_full_dir || !at_cursor) { - struct stat buf; - if (0 == wstat(abs_path, &buf) && (!at_cursor || S_ISDIR(buf.st_mode))) { - return true; - } - } else { - // We do not end with a slash; it does not have to be a directory. - const wcstring dir_name = wdirname(abs_path); - const wcstring filename_fragment = wbasename(abs_path); - if (dir_name == L"/" && filename_fragment == L"/") { - // cd ///.... No autosuggestion. - return true; - } - - dir_iter_t dir(dir_name); - if (dir.valid()) { - // Check if we're case insensitive. - const bool do_case_insensitive = - fs_is_case_insensitive(dir_name, dir.fd(), case_sensitivity_cache); - - // We opened the dir_name; look for a string where the base name prefixes it. - while (const auto *entry = dir.next()) { - if (ctx.check_cancel()) return false; - - // Maybe skip directories. - if (require_dir && !entry->is_dir()) { - continue; - } - - if (string_prefixes_string(filename_fragment, entry->name) || - (do_case_insensitive && - string_prefixes_string_case_insensitive(filename_fragment, entry->name))) { - return true; - } - } - } - } - } - - return false; +bool highlight_spec_t::operator==(const highlight_spec_t &other) const { + return *this->val == *other.val; } +bool highlight_spec_t::operator!=(const highlight_spec_t &other) const { return !(*this == other); } -// Given a string, return whether it prefixes a path that we could cd into. Return that path in -// out_path. Expects path to be unescaped. -static bool is_potential_cd_path(const wcstring &path, bool at_cursor, - const wcstring &working_directory, const operation_context_t &ctx, - path_flags_t flags) { - std::vector directories; - - if (string_prefixes_string(L"./", path)) { - // Ignore the CDPATH in this case; just use the working directory. - directories.push_back(working_directory); - } else { - // Get the CDPATH. - auto cdpath = ctx.vars.get_unless_empty(L"CDPATH"); - std::vector pathsv = !cdpath ? std::vector{L"."} : cdpath->as_list(); - // The current $PWD is always valid. - pathsv.push_back(L"."); - - for (auto next_path : pathsv) { - if (next_path.empty()) next_path = L"."; - // Ensure that we use the working directory for relative cdpaths like ".". - directories.push_back(path_apply_working_directory(next_path, working_directory)); - } - } - - // Call is_potential_path with all of these directories. - return is_potential_path(path, at_cursor, directories, ctx, - flags | PATH_REQUIRE_DIR | PATH_FOR_CD); -} - -// Given a plain statement node in a parse tree, get the command and return it, expanded -// appropriately for commands. If we succeed, return true. -static bool statement_get_expanded_command(const wcstring &src, - const ast::decorated_statement_t &stmt, - const operation_context_t &ctx, wcstring *out_cmd) { - // Get the command. Try expanding it. If we cannot, it's an error. - maybe_t cmd = stmt.command().source(src); - if (!cmd) return false; - expand_result_t err = expand_to_command_and_args(*cmd, ctx, out_cmd, nullptr); - return err == expand_result_t::ok; -} - -rgb_color_t highlight_color_resolver_t::resolve_spec_uncached(const highlight_spec_t &highlight, - bool is_background, - const environment_t &vars) const { - rgb_color_t result = rgb_color_t::normal(); - highlight_role_t role = is_background ? highlight.background : highlight.foreground; - - auto var = vars.get_unless_empty(get_highlight_var_name(role)); - if (!var) var = vars.get_unless_empty(get_highlight_var_name(get_fallback(role))); - if (!var) var = vars.get(get_highlight_var_name(highlight_role_t::normal)); - if (var) result = parse_color(*var, is_background); - - // Handle modifiers. - if (!is_background && highlight.valid_path) { - auto var2 = vars.get(L"fish_color_valid_path"); - if (var2) { - rgb_color_t result2 = parse_color(*var2, is_background); - if (result.is_normal()) { - result = result2; - } else if (!result2.is_normal()) { - // Valid path has an actual color, use it and merge the modifiers. - auto rescol = result2; - rescol.set_bold(result.is_bold() || result2.is_bold()); - rescol.set_underline(result.is_underline() || result2.is_underline()); - rescol.set_italics(result.is_italics() || result2.is_italics()); - rescol.set_dim(result.is_dim() || result2.is_dim()); - rescol.set_reverse(result.is_reverse() || result2.is_reverse()); - result = rescol; - } else { - if (result2.is_bold()) result.set_bold(true); - if (result2.is_underline()) result.set_underline(true); - if (result2.is_italics()) result.set_italics(true); - if (result2.is_dim()) result.set_dim(true); - if (result2.is_reverse()) result.set_reverse(true); - } - } - } - - if (!is_background && highlight.force_underline) { - result.set_underline(true); - } - - return result; -} - -rgb_color_t highlight_color_resolver_t::resolve_spec(const highlight_spec_t &highlight, - bool is_background, - const environment_t &vars) { - auto &cache = is_background ? bg_cache_ : fg_cache_; - auto p = cache.emplace(highlight, rgb_color_t{}); - auto iter = p.first; - bool did_insert = p.second; - if (did_insert) { - // Insertion happened, meaning the cache needs to be populated. - iter->second = resolve_spec_uncached(highlight, is_background, vars); - } - return iter->second; -} - -static bool command_is_valid(const wcstring &cmd, statement_decoration_t decoration, - const wcstring &working_directory, const environment_t &vars); - -static bool has_expand_reserved(const wcstring &str) { - bool result = false; - for (auto wc : str) { - if (wc >= EXPAND_RESERVED_BASE && wc <= EXPAND_RESERVED_END) { - result = true; - break; - } - } - return result; -} - -// Parse a command line. Return by reference the first command, and the first argument to that -// command (as a string), if any. This is used to validate autosuggestions. -static void autosuggest_parse_command(const wcstring &buff, const operation_context_t &ctx, - wcstring *out_expanded_command, wcstring *out_arg) { - auto ast = - ast_parse(buff, parse_flag_continue_after_error | parse_flag_accept_incomplete_tokens); - - // Find the first statement. - const ast::decorated_statement_t *first_statement = nullptr; - if (const ast::job_conjunction_t *jc = ast->top()->as_job_list().at(0)) { - first_statement = jc->job().statement().contents().ptr()->try_as_decorated_statement(); - } - - if (first_statement && - statement_get_expanded_command(buff, *first_statement, ctx, out_expanded_command)) { - // Check if the first argument or redirection is, in fact, an argument. - if (const auto *arg_or_redir = first_statement->args_or_redirs().at(0)) { - if (arg_or_redir && arg_or_redir->is_argument()) { - *out_arg = *arg_or_redir->argument().source(buff); - } - } - } -} - -bool autosuggest_validate_from_history(const history_item_t &item, - const wcstring &working_directory, - const operation_context_t &ctx) { - ASSERT_IS_BACKGROUND_THREAD(); - - // Parse the string. - wcstring parsed_command; - wcstring cd_dir; - autosuggest_parse_command(item.str(), ctx, &parsed_command, &cd_dir); - - // This is for autosuggestions which are not decorated commands, e.g. function declarations. - if (parsed_command.empty()) { - return true; - } - - // We handle cd specially. - if (parsed_command == L"cd" && !cd_dir.empty()) { - if (expand_one(cd_dir, expand_flag::skip_cmdsubst, ctx)) { - if (string_prefixes_string(cd_dir, L"--help") || - string_prefixes_string(cd_dir, L"-h")) { - // cd --help is always valid. - return true; - } else { - // Check the directory target, respecting CDPATH. - // Permit the autosuggestion if the path is valid and not our directory. - auto path = path_get_cdpath(cd_dir, working_directory, ctx.vars); - return path && !paths_are_same_file(working_directory, *path); - } - } - } - - // Not handled specially. Is the command valid? - bool cmd_ok = builtin_exists(parsed_command) || function_exists_no_autoload(parsed_command) || - path_get_path(parsed_command, ctx.vars).has_value(); - if (!cmd_ok) { - return false; - } - - // Did the historical command have arguments that look like paths, which aren't paths now? - if (!all_paths_are_valid(item.get_required_paths(), ctx)) { - return false; - } - - return true; -} - -// Highlights the variable starting with 'in', setting colors within the 'colors' array. Returns the -// number of characters consumed. -static size_t color_variable(const wchar_t *in, size_t in_len, - std::vector::iterator colors) { - assert(in_len > 0); - assert(in[0] == L'$'); - - // Handle an initial run of $s. - size_t idx = 0; - size_t dollar_count = 0; - while (in[idx] == '$') { - // Our color depends on the next char. - wchar_t next = in[idx + 1]; - if (next == L'$' || valid_var_name_char(next)) { - colors[idx] = highlight_role_t::operat; - } else if (next == L'(') { - colors[idx] = highlight_role_t::operat; - return idx + 1; - } else { - colors[idx] = highlight_role_t::error; - } - idx++; - dollar_count++; - } - - // Handle a sequence of variable characters. - // It may contain an escaped newline - see #8444. - for (;;) { - if (valid_var_name_char(in[idx])) { - colors[idx++] = highlight_role_t::operat; - } else if (in[idx] == L'\\' && in[idx + 1] == L'\n') { - colors[idx++] = highlight_role_t::operat; - colors[idx++] = highlight_role_t::operat; - } else { - break; - } - } - - // Handle a slice, up to dollar_count of them. Note that we currently don't do any validation of - // the slice's contents, e.g. $foo[blah] will not show an error even though it's invalid. - for (size_t slice_count = 0; slice_count < dollar_count; slice_count++) { - long slice_len = parse_util_slice_length(in + idx); - if (slice_len > 0) { - auto slice_ulen = static_cast(slice_len); - colors[idx] = highlight_role_t::operat; - colors[idx + slice_ulen - 1] = highlight_role_t::operat; - idx += slice_ulen; - } else if (slice_len == 0) { - // not a slice - break; - } else { - assert(slice_len < 0); - // Syntax error. Normally the entire token is colored red for us, but inside a - // double-quoted string that doesn't happen. As such, color the variable + the slice - // start red. Coloring any more than that looks bad, unless we're willing to try and - // detect where the double-quoted string ends, and I'd rather not do that. - std::fill(colors, colors + idx + 1, highlight_role_t::error); - break; - } - } - return idx; -} - -/// This function is a disaster badly in need of refactoring. It colors an argument or command, -/// without regard to command substitutions. -static void color_string_internal(const wcstring &buffstr, highlight_spec_t base_color, - std::vector::iterator colors) { - // Clarify what we expect. - assert((base_color == highlight_role_t::param || base_color == highlight_role_t::option || - base_color == highlight_role_t::command) && - "Unexpected base color"); - const size_t buff_len = buffstr.size(); - std::fill(colors, colors + buff_len, base_color); - - // Hacky support for %self which must be an unquoted literal argument. - if (buffstr == PROCESS_EXPAND_SELF_STR) { - std::fill_n(colors, std::wcslen(PROCESS_EXPAND_SELF_STR), highlight_role_t::operat); - return; - } - - enum { e_unquoted, e_single_quoted, e_double_quoted } mode = e_unquoted; - // For some ungodly reason making this a maybe makes gcc grumpy - auto unclosed_quote_offset = std::numeric_limits::max(); - int bracket_count = 0; - for (size_t in_pos = 0; in_pos < buff_len; in_pos++) { - const wchar_t c = buffstr.at(in_pos); - switch (mode) { - case e_unquoted: { - if (c == L'\\') { - auto fill_color = highlight_role_t::escape; // may be set to highlight_error - const size_t backslash_pos = in_pos; - size_t fill_end = backslash_pos; - - // Move to the escaped character. - in_pos++; - const wchar_t escaped_char = (in_pos < buff_len ? buffstr.at(in_pos) : L'\0'); - - if (escaped_char == L'\0') { - fill_end = in_pos; - fill_color = highlight_role_t::error; - } else if (std::wcschr(L"~%", escaped_char)) { - if (in_pos == 1) { - fill_end = in_pos + 1; - } - } else if (escaped_char == L',') { - if (bracket_count) { - fill_end = in_pos + 1; - } - } else if (std::wcschr(L"abefnrtv*?$(){}[]'\"<>^ \\#;|&", escaped_char)) { - fill_end = in_pos + 1; - } else if (std::wcschr(L"c", escaped_char)) { - // Like \ci. So highlight three characters. - fill_end = in_pos + 1; - } else if (std::wcschr(L"uUxX01234567", escaped_char)) { - long long res = 0; - int chars = 2; - int base = 16; - wchar_t max_val = ASCII_MAX; - - switch (escaped_char) { - case L'u': { - chars = 4; - max_val = UCS2_MAX; - in_pos++; - break; - } - case L'U': { - chars = 8; - max_val = WCHAR_MAX; - // Don't exceed the largest Unicode code point - see #1107. - if (0x10FFFF < max_val) max_val = static_cast(0x10FFFF); - in_pos++; - break; - } - case L'x': - case L'X': { - max_val = BYTE_MAX; - in_pos++; - break; - } - default: { - // a digit like \12 - base = 8; - chars = 3; - break; - } - } - - // Consume - for (int i = 0; i < chars && in_pos < buff_len; i++) { - long d = convert_digit(buffstr.at(in_pos), base); - if (d < 0) break; - res = (res * base) + d; - in_pos++; - } - // in_pos is now at the first character that could not be converted (or - // buff_len). - assert(in_pos >= backslash_pos && in_pos <= buff_len); - fill_end = in_pos; - - // It's an error if we exceeded the max value. - if (res > max_val) fill_color = highlight_role_t::error; - - // Subtract one from in_pos, so that the increment in the loop will move to - // the next character. - in_pos--; - } - assert(fill_end >= backslash_pos); - std::fill(colors + backslash_pos, colors + fill_end, fill_color); - } else { - // Not a backslash. - switch (c) { - case L'~': { - if (in_pos == 0) { - colors[in_pos] = highlight_role_t::operat; - } - break; - } - case L'$': { - assert(in_pos < buff_len); - in_pos += color_variable(buffstr.c_str() + in_pos, buff_len - in_pos, - colors + in_pos); - // Subtract one to account for the upcoming loop increment. - in_pos -= 1; - break; - } - case L'?': { - if (!feature_test(feature_flag_t::qmark_noglob)) { - colors[in_pos] = highlight_role_t::operat; - } - break; - } - case L'*': - case L'(': - case L')': { - colors[in_pos] = highlight_role_t::operat; - break; - } - case L'{': { - colors[in_pos] = highlight_role_t::operat; - bracket_count++; - break; - } - case L'}': { - colors[in_pos] = highlight_role_t::operat; - bracket_count--; - break; - } - case L',': { - if (bracket_count > 0) { - colors[in_pos] = highlight_role_t::operat; - } - break; - } - case L'\'': { - colors[in_pos] = highlight_role_t::quote; - unclosed_quote_offset = in_pos; - mode = e_single_quoted; - break; - } - case L'\"': { - colors[in_pos] = highlight_role_t::quote; - unclosed_quote_offset = in_pos; - mode = e_double_quoted; - break; - } - default: { - break; // we ignore all other characters - } - } - } - break; - } - // Mode 1 means single quoted string, i.e 'foo'. - case e_single_quoted: { - colors[in_pos] = highlight_role_t::quote; - if (c == L'\\') { - // backslash - if (in_pos + 1 < buff_len) { - const wchar_t escaped_char = buffstr.at(in_pos + 1); - if (escaped_char == L'\\' || escaped_char == L'\'') { - colors[in_pos] = highlight_role_t::escape; // backslash - colors[in_pos + 1] = highlight_role_t::escape; // escaped char - in_pos += 1; // skip over backslash - } - } - } else if (c == L'\'') { - mode = e_unquoted; - } - break; - } - // Mode 2 means double quoted string, i.e. "foo". - case e_double_quoted: { - // Slices are colored in advance, past `in_pos`, and we don't want to overwrite - // that. - if (colors[in_pos] == base_color) { - colors[in_pos] = highlight_role_t::quote; - } - switch (c) { - case L'"': { - mode = e_unquoted; - break; - } - case L'\\': { - // Backslash - if (in_pos + 1 < buff_len) { - const wchar_t escaped_char = buffstr.at(in_pos + 1); - if (std::wcschr(L"\\\"\n$", escaped_char)) { - colors[in_pos] = highlight_role_t::escape; // backslash - colors[in_pos + 1] = highlight_role_t::escape; // escaped char - in_pos += 1; // skip over backslash - } - } - break; - } - case L'$': { - in_pos += color_variable(buffstr.c_str() + in_pos, buff_len - in_pos, - colors + in_pos); - // Subtract one to account for the upcoming increment in the loop. - in_pos -= 1; - break; - } - default: { - break; // we ignore all other characters - } - } - break; - } - } - } - - // Error on unclosed quotes. - if (mode != e_unquoted) { - colors[unclosed_quote_offset] = highlight_role_t::error; - } -} - -highlighter_t::highlighter_t(const wcstring &str, maybe_t cursor, - const operation_context_t &ctx, wcstring wd, bool can_do_io) - : buff(str), - cursor(cursor), - ctx(ctx), - io_ok(can_do_io), - working_directory(std::move(wd)), - ast(ast_parse(buff, ast_flags)), - highlighter(new_highlighter(*this, *ast)) {} - -bool highlighter_t::io_still_ok() const { return io_ok && !ctx.check_cancel(); } - -wcstring highlighter_t::get_source(source_range_t r) const { - assert(r.start + r.length >= r.start && "Overflow"); - assert(r.start + r.length <= this->buff.size() && "Out of range"); - return this->buff.substr(r.start, r.length); -} - -void highlighter_t::color_node(const ast::node_t &node, highlight_spec_t color) { - color_range(node.source_range(), color); -} - -void highlighter_t::color_range(source_range_t range, highlight_spec_t color) { - assert(range.start + range.length <= this->color_array.size() && "Range out of bounds"); - std::fill_n(this->color_array.begin() + range.start, range.length, color); -} - -void highlighter_t::color_command(const ast::string_t &node) { - source_range_t source_range = node.source_range(); - const wcstring cmd_str = get_source(source_range); - - // Get an iterator to the colors associated with the argument. - const size_t arg_start = source_range.start; - const color_array_t::iterator colors = color_array.begin() + arg_start; - color_string_internal(cmd_str, highlight_role_t::command, colors); -} - -// node does not necessarily have type symbol_argument here. -void highlighter_t::color_as_argument(const ast::node_t &node, bool options_allowed) { - auto source_range = node.source_range(); - const wcstring arg_str = get_source(source_range); - - // Get an iterator to the colors associated with the argument. - const size_t arg_start = source_range.start; - const color_array_t::iterator arg_colors = color_array.begin() + arg_start; - - // Color this argument without concern for command substitutions. - if (options_allowed && arg_str[0] == L'-') { - color_string_internal(arg_str, highlight_role_t::option, arg_colors); - } else { - color_string_internal(arg_str, highlight_role_t::param, arg_colors); - } - - // Now do command substitutions. - size_t cmdsub_cursor = 0, cmdsub_start = 0, cmdsub_end = 0; - wcstring cmdsub_contents; - bool is_quoted = false; - while (parse_util_locate_cmdsubst_range(arg_str, &cmdsub_cursor, &cmdsub_contents, - &cmdsub_start, &cmdsub_end, - true /* accept incomplete */, &is_quoted) > 0) { - // The cmdsub_start is the open paren. cmdsub_end is either the close paren or the end of - // the string. cmdsub_contents extends from one past cmdsub_start to cmdsub_end. - assert(cmdsub_end > cmdsub_start); - assert(cmdsub_end - cmdsub_start - 1 == cmdsub_contents.size()); - - // Found a command substitution. Compute the position of the start and end of the cmdsub - // contents, within our overall src. - const size_t arg_subcmd_start = arg_start + cmdsub_start, - arg_subcmd_end = arg_start + cmdsub_end; - - // Highlight the parens. The open paren must exist; the closed paren may not if it was - // incomplete. - assert(cmdsub_start < arg_str.size()); - this->color_array.at(arg_subcmd_start) = highlight_role_t::operat; - if (arg_subcmd_end < this->buff.size()) - this->color_array.at(arg_subcmd_end) = highlight_role_t::operat; - - // Highlight it recursively. - maybe_t arg_cursor; - if (cursor.has_value()) { - arg_cursor = *cursor - arg_subcmd_start; - } - highlighter_t cmdsub_highlighter(cmdsub_contents, arg_cursor, this->ctx, - this->working_directory, this->io_still_ok()); - color_array_t subcolors = cmdsub_highlighter.highlight(); - - // Copy out the subcolors back into our array. - assert(subcolors.size() == cmdsub_contents.size()); - std::copy(subcolors.begin(), subcolors.end(), - this->color_array.begin() + arg_subcmd_start + 1); - } -} - -/// Indicates whether the source range of the given node forms a valid path in the given -/// working_directory. -static bool range_is_potential_path(const wcstring &src, const source_range_t &range, - bool at_cursor, const operation_context_t &ctx, - const wcstring &working_directory) { - // Skip strings exceeding PATH_MAX. See #7837. - // Note some paths may exceed PATH_MAX, but this is just for highlighting. - if (range.length > PATH_MAX) { - return false; - } - // Get the node source, unescape it, and then pass it to is_potential_path along with the - // working directory (as a one element list). - bool result = false; - wcstring token = src.substr(range.start, range.length); - if (unescape_string_in_place(&token, UNESCAPE_SPECIAL)) { - // Big hack: is_potential_path expects a tilde, but unescape_string gives us HOME_DIRECTORY. - // Put it back. - if (!token.empty() && token.at(0) == HOME_DIRECTORY) token.at(0) = L'~'; - - const std::vector working_directory_list(1, working_directory); - result = - is_potential_path(token, at_cursor, working_directory_list, ctx, PATH_EXPAND_TILDE); - } - return result; -} - -void highlighter_t::visit_keyword(const ast::node_t *kw) { - highlight_role_t role = highlight_role_t::normal; - switch (kw->keyword()) { - case parse_keyword_t::kw_begin: - case parse_keyword_t::kw_builtin: - case parse_keyword_t::kw_case: - case parse_keyword_t::kw_command: - case parse_keyword_t::kw_else: - case parse_keyword_t::kw_end: - case parse_keyword_t::kw_exec: - case parse_keyword_t::kw_for: - case parse_keyword_t::kw_function: - case parse_keyword_t::kw_if: - case parse_keyword_t::kw_in: - case parse_keyword_t::kw_switch: - case parse_keyword_t::kw_while: - role = highlight_role_t::keyword; - break; - - case parse_keyword_t::kw_and: - case parse_keyword_t::kw_or: - case parse_keyword_t::kw_not: - case parse_keyword_t::kw_exclam: - case parse_keyword_t::kw_time: - role = highlight_role_t::operat; - break; - - case parse_keyword_t::none: - break; - } - color_node(*kw, role); -} - -void highlighter_t::visit_token(const ast::node_t *tok) { - maybe_t role = highlight_role_t::normal; - switch (tok->token_type()) { - case parse_token_type_t::end: - case parse_token_type_t::pipe: - case parse_token_type_t::background: - role = highlight_role_t::statement_terminator; - break; - - case parse_token_type_t::andand: - case parse_token_type_t::oror: - role = highlight_role_t::operat; - break; - - case parse_token_type_t::string: - // Assume all strings are params. This handles e.g. the variables a for header or - // function header. Other strings (like arguments to commands) need more complex - // handling, which occurs in their respective overrides of visit(). - role = highlight_role_t::param; - - default: - break; - } - if (role) color_node(*tok, *role); -} - -void highlighter_t::visit_semi_nl(const ast::node_t *semi_nl) { - color_node(*semi_nl, highlight_role_t::statement_terminator); -} - -void highlighter_t::visit_argument(const void *arg_, bool cmd_is_cd, bool options_allowed) { - const auto &arg = *static_cast(arg_); - color_as_argument(*arg.ptr(), options_allowed); - if (!io_still_ok()) { - return; - } - // Underline every valid path. - bool is_valid_path = false; - bool at_cursor = cursor.has_value() && arg.source_range().contains_inclusive(*cursor); - if (cmd_is_cd) { - // Mark this as an error if it's not 'help' and not a valid cd path. - wcstring param = *arg.source(this->buff); - if (expand_one(param, expand_flag::skip_cmdsubst, ctx)) { - bool is_help = - string_prefixes_string(param, L"--help") || string_prefixes_string(param, L"-h"); - if (!is_help) { - is_valid_path = is_potential_cd_path(param, at_cursor, working_directory, ctx, - PATH_EXPAND_TILDE); - if (!is_valid_path) { - this->color_node(*arg.ptr(), highlight_role_t::error); - } - } - } - } else if (range_is_potential_path(buff, arg.range(), at_cursor, ctx, working_directory)) { - is_valid_path = true; - } - if (is_valid_path) - for (size_t i = arg.range().start, end = arg.range().start + arg.range().length; i < end; - i++) - this->color_array.at(i).valid_path = true; -} - -void highlighter_t::visit_variable_assignment(const void *varas_) { - const auto &varas = *static_cast(varas_); - color_as_argument(*varas.ptr()); - // Highlight the '=' in variable assignments as an operator. - auto where = variable_assignment_equals_pos(*varas.source(this->buff)); - if (where) { - size_t equals_loc = varas.source_range().start + *where; - this->color_array.at(equals_loc) = highlight_role_t::operat; - auto var_name = varas.source(this->buff)->substr(0, *where); - this->pending_variables.push_back(std::move(var_name)); - } -} - -void highlighter_t::visit_decorated_statement(const void *stmt_) { - const auto &stmt = *static_cast(stmt_); - // Color any decoration. - if (stmt.has_opt_decoration()) { - auto decoration = stmt.opt_decoration().ptr(); - this->visit_keyword(&*decoration); - } - - // Color the command's source code. - // If we get no source back, there's nothing to color. - if (!stmt.command().try_source_range()) return; - wcstring cmd = *stmt.command().source(this->buff); - - wcstring expanded_cmd; - bool is_valid_cmd = false; - if (!this->io_still_ok()) { - // We cannot check if the command is invalid, so just assume it's valid. - is_valid_cmd = true; - } else if (variable_assignment_equals_pos(cmd)) { - is_valid_cmd = true; - } else { - // Check to see if the command is valid. - // Try expanding it. If we cannot, it's an error. - bool expanded = statement_get_expanded_command(buff, stmt, ctx, &expanded_cmd); - if (expanded && !has_expand_reserved(expanded_cmd)) { - is_valid_cmd = - command_is_valid(expanded_cmd, stmt.decoration(), working_directory, ctx.vars); - } - } - - // Color our statement. - if (is_valid_cmd) { - this->color_command(stmt.command()); - } else { - this->color_node(*stmt.command().ptr(), highlight_role_t::error); - } - - // Color arguments and redirections. - // Except if our command is 'cd' we have special logic for how arguments are colored. - bool is_cd = (expanded_cmd == L"cd"); - bool is_set = (expanded_cmd == L"set"); - // If we have seen a "--" argument, color all options from then on as normal arguments. - bool have_dashdash = false; - for (size_t i = 0; i < stmt.args_or_redirs().count(); i++) { - const auto &v = *stmt.args_or_redirs().at(i); - if (v.is_argument()) { - if (is_set) { - auto arg = *v.argument().source(this->buff); - if (valid_var_name(arg)) { - this->pending_variables.push_back(std::move(arg)); - is_set = false; - } - } - this->visit_argument(&v.argument(), is_cd, !have_dashdash); - if (*v.argument().source(this->buff) == L"--") have_dashdash = true; - } else { - this->visit_redirection(&v.redirection()); - } - } -} - -size_t highlighter_t::visit_block_statement1(const void *block_) { - const auto &block = *static_cast(block_); - auto bh = block.header().ptr(); - size_t pending_variables_count = this->pending_variables.size(); - if (const auto *fh = bh->try_as_for_header()) { - auto var_name = *fh->var_name().source(this->buff); - pending_variables.push_back(std::move(var_name)); - } - return pending_variables_count; -} - -void highlighter_t::visit_block_statement2(size_t pending_variables_count) { - pending_variables.resize(pending_variables_count); -} - -/// \return whether a string contains a command substitution. -static bool has_cmdsub(const wcstring &src) { - size_t cursor = 0; - size_t start = 0; - size_t end = 0; - return parse_util_locate_cmdsubst_range(src, &cursor, nullptr, &start, &end, true) != 0; -} - -static bool contains_pending_variable(const std::vector &pending_variables, - const wcstring &haystack) { - for (const auto &var_name : pending_variables) { - size_t pos = -1; - while ((pos = haystack.find(var_name, pos + 1)) != wcstring::npos) { - if (pos == 0 || haystack.at(pos - 1) != L'$') continue; - size_t end = pos + var_name.size(); - if (end < haystack.size() && valid_var_name_char(haystack.at(end))) continue; - return true; - } - } - return false; -} - -void highlighter_t::visit_redirection(const void *redir_) { - const auto &redir = *static_cast(redir_); - auto oper = pipe_or_redir_from_string(redir.oper().source(this->buff)->c_str()); // like 2> - wcstring target = *redir.target().source(this->buff); // like &1 or file path - - assert(oper && "Should have successfully parsed a pipe_or_redir_t since it was in our ast"); - - // Color the > part. - // It may have parsed successfully yet still be invalid (e.g. 9999999999999>&1) - // If so, color the whole thing invalid and stop. - if (!oper->is_valid()) { - this->color_node(*redir.ptr(), highlight_role_t::error); - return; - } - - // Color the operator part like 2>. - this->color_node(*redir.oper().ptr(), highlight_role_t::redirection); - - // Color the target part. - // Check if the argument contains a command substitution. If so, highlight it as a param - // even though it's a command redirection, and don't try to do any other validation. - if (has_cmdsub(target)) { - this->color_as_argument(*redir.target().ptr()); - } else { - // No command substitution, so we can highlight the target file or fd. For example, - // disallow redirections into a non-existent directory. - bool target_is_valid = true; - if (!this->io_still_ok()) { - // I/O is disallowed, so we don't have much hope of catching anything but gross - // errors. Assume it's valid. - target_is_valid = true; - } else if (contains_pending_variable(this->pending_variables, target)) { - target_is_valid = true; - } else if (!expand_one(target, expand_flag::skip_cmdsubst, ctx)) { - // Could not be expanded. - target_is_valid = false; - } else { - // Ok, we successfully expanded our target. Now verify that it works with this - // redirection. We will probably need it as a path (but not in the case of fd - // redirections). Note that the target is now unescaped. - const wcstring target_path = - path_apply_working_directory(target, this->working_directory); - switch (oper->mode) { - case redirection_mode_t::fd: { - if (target == L"-") { - target_is_valid = true; - } else { - int fd = fish_wcstoi(target.c_str()); - target_is_valid = !errno && fd >= 0; - } - break; - } - case redirection_mode_t::input: { - // Input redirections must have a readable non-directory. - struct stat buf = {}; - target_is_valid = !waccess(target_path, R_OK) && !wstat(target_path, &buf) && - !S_ISDIR(buf.st_mode); - break; - } - case redirection_mode_t::overwrite: - case redirection_mode_t::append: - case redirection_mode_t::noclob: { - // Test whether the file exists, and whether it's writable (possibly after - // creating it). access() returns failure if the file does not exist. - bool file_exists = false, file_is_writable = false; - int err = 0; - - struct stat buf = {}; - if (wstat(target_path, &buf) < 0) { - err = errno; - } - - if (string_suffixes_string(L"/", target)) { - // Redirections to things that are directories is definitely not - // allowed. - file_exists = false; - file_is_writable = false; - } else if (err == 0) { - // No err. We can write to it if it's not a directory and we have - // permission. - file_exists = true; - file_is_writable = !S_ISDIR(buf.st_mode) && !waccess(target_path, W_OK); - } else if (err == ENOENT) { - // File does not exist. Check if its parent directory is writable. - wcstring parent = wdirname(target_path); - - // Ensure that the parent ends with the path separator. This will ensure - // that we get an error if the parent directory is not really a - // directory. - if (!string_suffixes_string(L"/", parent)) parent.push_back(L'/'); - - // Now the file is considered writable if the parent directory is - // writable. - file_exists = false; - file_is_writable = (0 == waccess(parent, W_OK)); - } else { - // Other errors we treat as not writable. This includes things like - // ENOTDIR. - file_exists = false; - file_is_writable = false; - } - - // NOCLOB means that we must not overwrite files that exist. - target_is_valid = file_is_writable && - !(file_exists && oper->mode == redirection_mode_t::noclob); - break; - } - } - } - this->color_node(*redir.target().ptr(), - target_is_valid ? highlight_role_t::redirection : highlight_role_t::error); - } -} - -highlighter_t::color_array_t highlighter_t::highlight() { - // If we are doing I/O, we must be in a background thread. - if (io_ok) { - ASSERT_IS_BACKGROUND_THREAD(); - } - - this->color_array.resize(this->buff.size()); - std::fill(this->color_array.begin(), this->color_array.end(), highlight_spec_t{}); - - this->highlighter->visit_children(*ast->top()); - if (ctx.check_cancel()) return std::move(color_array); - - // Color every comment. - auto extras = ast->extras(); - for (const source_range_t &r : extras->comments()) { - this->color_range(r, highlight_role_t::comment); - } - - // Color every extra semi. - for (const source_range_t &r : extras->semis()) { - this->color_range(r, highlight_role_t::statement_terminator); - } - - // Color every error range. - for (const source_range_t &r : extras->errors()) { - this->color_range(r, highlight_role_t::error); - } - - return std::move(color_array); -} - -/// Determine if a command is valid. -static bool command_is_valid(const wcstring &cmd, statement_decoration_t decoration, - const wcstring &working_directory, const environment_t &vars) { - // Determine which types we check, based on the decoration. - bool builtin_ok = true, function_ok = true, abbreviation_ok = true, command_ok = true, - implicit_cd_ok = true; - if (decoration == statement_decoration_t::command || - decoration == statement_decoration_t::exec) { - builtin_ok = false; - function_ok = false; - abbreviation_ok = false; - command_ok = true; - implicit_cd_ok = false; - } else if (decoration == statement_decoration_t::builtin) { - builtin_ok = true; - function_ok = false; - abbreviation_ok = false; - command_ok = false; - implicit_cd_ok = false; - } - - // Check them. - bool is_valid = false; - - // Builtins - if (!is_valid && builtin_ok) is_valid = builtin_exists(cmd); - - // Functions - if (!is_valid && function_ok) is_valid = function_exists_no_autoload(cmd); - - // Abbreviations - if (!is_valid && abbreviation_ok) is_valid = abbrs_has_match(cmd, abbrs_position_t::command); - - // Regular commands - if (!is_valid && command_ok) is_valid = path_get_path(cmd, vars).has_value(); - - // Implicit cd - if (!is_valid && implicit_cd_ok) { - is_valid = path_as_implicit_cd(cmd, working_directory, vars).has_value(); - } - - // Return what we got. - return is_valid; -} - -std::string colorize(const wcstring &text, const std::vector &colors, - const environment_t &vars) { - assert(colors.size() == text.size()); - highlight_color_resolver_t rv; - rust::Box outp = make_buffering_outputter(); - - highlight_spec_t last_color = highlight_role_t::normal; - for (size_t i = 0; i < text.size(); i++) { - highlight_spec_t color = colors.at(i); - if (color != last_color) { - outp->set_color(rv.resolve_spec(color, false, vars), rgb_color_t::normal()); - last_color = color; - } - outp->writech(text.at(i)); - } - outp->set_color(rgb_color_t::normal(), rgb_color_t::normal()); - auto contents = outp->contents(); - return std::string(contents.begin(), contents.end()); -} - -void highlight_shell(const wcstring &buff, std::vector &color, - const operation_context_t &ctx, bool io_ok, maybe_t cursor) { - const wcstring working_directory = ctx.vars.get_pwd_slash(); - highlighter_t highlighter(buff, cursor, ctx, working_directory, io_ok); - color = highlighter.highlight(); -} - -wcstring colorize_shell(const wcstring &text, parser_t &parser) { - std::vector colors; - highlight_shell(text, colors, parser.context()); - return str2wcstring(colorize(text, colors, parser.vars())); +void highlight_shell(const wcstring &buff, std::vector &colors, + const operation_context_t &ctx, bool io_ok, std::shared_ptr cursor) { + auto ffi_colors = highlight_shell_ffi(buff, ctx, io_ok, cursor); + colors.resize(ffi_colors->size()); + for (size_t i = 0; i < ffi_colors->size(); i++) colors[i] = ffi_colors->at(i); } diff --git a/src/highlight.h b/src/highlight.h index cfa809620..b037c3240 100644 --- a/src/highlight.h +++ b/src/highlight.h @@ -14,221 +14,48 @@ #include "ast.h" #include "color.h" #include "cxx.h" +#include "env.h" #include "flog.h" +#include "history.h" #include "maybe.h" +#include "operation_context.h" +#include "parser.h" -struct Highlighter; +struct highlight_spec_t; -class environment_t; +#if INCLUDE_RUST_HEADERS +#include "highlight.rs.h" +#else +struct HighlightSpec; +enum class HighlightRole : uint8_t; +#endif -/// Describes the role of a span of text. -enum class highlight_role_t : uint8_t { - normal = 0, // normal text - error, // error - command, // command - keyword, - statement_terminator, // process separator - param, // command parameter (argument) - option, // argument starting with "-", up to a "--" - comment, // comment - search_match, // search match - operat, // operator - escape, // escape sequences - quote, // quoted string - redirection, // redirection - autosuggestion, // autosuggestion - selection, +using highlight_role_t = HighlightRole; +// using highlight_spec_t = HighlightSpec; - // Pager support. - // NOTE: pager.cpp relies on these being in this order. - pager_progress, - pager_background, - pager_prefix, - pager_completion, - pager_description, - pager_secondary_background, - pager_secondary_prefix, - pager_secondary_completion, - pager_secondary_description, - pager_selected_background, - pager_selected_prefix, - pager_selected_completion, - pager_selected_description, -}; - -/// Simply value type describing how a character should be highlighted.. struct highlight_spec_t { - highlight_role_t foreground{highlight_role_t::normal}; - highlight_role_t background{highlight_role_t::normal}; - bool valid_path{false}; - bool force_underline{false}; + rust::Box val; - highlight_spec_t() = default; + highlight_spec_t(); + highlight_spec_t(const highlight_spec_t &other); + highlight_spec_t &operator=(const highlight_spec_t &other); - /* implicit */ highlight_spec_t(highlight_role_t fg, - highlight_role_t bg = highlight_role_t::normal) - : foreground(fg), background(bg) {} + highlight_spec_t(HighlightSpec other); + highlight_spec_t(highlight_role_t fg, highlight_role_t bg = {}); - bool operator==(const highlight_spec_t &rhs) const { - return foreground == rhs.foreground && background == rhs.background && - valid_path == rhs.valid_path && force_underline == rhs.force_underline; - } + bool operator==(const highlight_spec_t &other) const; + bool operator!=(const highlight_spec_t &other) const; - bool operator!=(const highlight_spec_t &rhs) const { return !(*this == rhs); } + HighlightSpec *operator->() { return &*this->val; } + const HighlightSpec *operator->() const { return &*this->val; } - static highlight_spec_t make_background(highlight_role_t bg_role) { - return highlight_spec_t{highlight_role_t::normal, bg_role}; - } + operator HighlightSpec() const; + + static highlight_spec_t make_background(highlight_role_t bg_role); }; -namespace std { -template <> -struct hash { - std::size_t operator()(const highlight_spec_t &v) const { - const size_t vals[4] = {static_cast(v.foreground), - static_cast(v.background), v.valid_path, - v.force_underline}; - return (vals[0] << 0) + (vals[1] << 6) + (vals[2] << 12) + (vals[3] << 18); - } -}; -} // namespace std - -class history_item_t; -class operation_context_t; - -/// Given a string and list of colors of the same size, return the string with ANSI escape sequences -/// representing the colors. -std::string colorize(const wcstring &text, const std::vector &colors, - const environment_t &vars); - -/// Perform syntax highlighting for the shell commands in buff. The result is stored in the color -/// array as a color_code from the HIGHLIGHT_ enum for each character in buff. -/// -/// \param buffstr The buffer on which to perform syntax highlighting -/// \param color The array in which to store the color codes. The first 8 bits are used for fg -/// color, the next 8 bits for bg color. -/// \param ctx The variables and cancellation check for this operation. -/// \param io_ok If set, allow IO which may block. This means that e.g. invalid commands may be -/// detected. -/// \param cursor The position of the cursor in the commandline. -void highlight_shell(const wcstring &buffstr, std::vector &color, +void highlight_shell(const wcstring &buff, std::vector &colors, const operation_context_t &ctx, bool io_ok = false, - maybe_t cursor = {}); - -class Parser; using parser_t = Parser; -/// Wrapper around colorize(highlight_shell) -wcstring colorize_shell(const wcstring &text, parser_t &parser); - -/// highlight_color_resolver_t resolves highlight specs (like "a command") to actual RGB colors. -/// It maintains a cache with no invalidation mechanism. The lifetime of these should typically be -/// one screen redraw. -struct highlight_color_resolver_t { - /// \return an RGB color for a given highlight spec. - rgb_color_t resolve_spec(const highlight_spec_t &highlight, bool is_background, - const environment_t &vars); - - private: - std::unordered_map fg_cache_; - std::unordered_map bg_cache_; - rgb_color_t resolve_spec_uncached(const highlight_spec_t &highlight, bool is_background, - const environment_t &vars) const; -}; - -/// Given an item \p item from the history which is a proposed autosuggestion, return whether the -/// autosuggestion is valid. It may not be valid if e.g. it is attempting to cd into a directory -/// which does not exist. -bool autosuggest_validate_from_history(const history_item_t &item, - const wcstring &working_directory, - const operation_context_t &ctx); - -// Tests whether the specified string cpath is the prefix of anything we could cd to. directories is -// a list of possible parent directories (typically either the working directory, or the cdpath). -// This does I/O! -// -// This is used only internally to this file, and is exposed only for testing. -enum { - // The path must be to a directory. - PATH_REQUIRE_DIR = 1 << 0, - // Expand any leading tilde in the path. - PATH_EXPAND_TILDE = 1 << 1, - // Normalize directories before resolving, as "cd". - PATH_FOR_CD = 1 << 2, -}; -typedef unsigned int path_flags_t; -bool is_potential_path(const wcstring &potential_path_fragment, bool at_cursor, - const std::vector &directories, const operation_context_t &ctx, - path_flags_t flags); - -/// Syntax highlighter helper. -class highlighter_t { - // The string we're highlighting. Note this is a reference member variable (to avoid copying)! - // We must not outlive this! - const wcstring &buff; - // The position of the cursor within the string. - const maybe_t cursor; - // The operation context. Again, a reference member variable! - const operation_context_t &ctx; - // Whether it's OK to do I/O. - const bool io_ok; - // Working directory. - const wcstring working_directory; - // The ast we produced. - rust::Box ast; - rust::Box highlighter; - // The resulting colors. - using color_array_t = std::vector; - color_array_t color_array; - // A stack of variables that the current commandline probably defines. We mark redirections - // as valid if they use one of these variables, to avoid marking valid targets as error. - std::vector pending_variables; - - // Flags we use for AST parsing. - static constexpr parse_tree_flags_t ast_flags = - parse_flag_continue_after_error | parse_flag_include_comments | - parse_flag_accept_incomplete_tokens | parse_flag_leave_unterminated | - parse_flag_show_extra_semis; - - bool io_still_ok() const; - -#if INCLUDE_RUST_HEADERS - // Declaring methods with forward-declared opaque Rust types like "ast::node_t" will cause - // undefined reference errors. - // Color a command. - void color_command(const ast::string_t &node); - // Color a node as if it were an argument. - void color_as_argument(const ast::node_t &node, bool options_allowed = true); - // Colors the source range of a node with a given color. - void color_node(const ast::node_t &node, highlight_spec_t color); - // Colors a range with a given color. - void color_range(source_range_t range, highlight_spec_t color); -#endif - - public: - /// \return a substring of our buffer. - wcstring get_source(source_range_t r) const; - - // AST visitor implementations. - void visit_keyword(const ast::node_t *kw); - void visit_token(const ast::node_t *tok); - void visit_argument(const void *arg, bool cmd_is_cd, bool options_allowed); - void visit_redirection(const void *redir); - void visit_variable_assignment(const void *varas); - void visit_semi_nl(const ast::node_t *semi_nl); - void visit_decorated_statement(const void *stmt); - size_t visit_block_statement1(const void *block); - void visit_block_statement2(size_t pending_variables_count); - -#if INCLUDE_RUST_HEADERS - // Visit an argument, perhaps knowing that our command is cd. - void visit(const ast::argument_t &arg, bool cmd_is_cd = false, bool options_allowed = true); -#endif - - // Constructor - highlighter_t(const wcstring &str, maybe_t cursor, const operation_context_t &ctx, - wcstring wd, bool can_do_io); - - // Perform highlighting, returning an array of colors. - color_array_t highlight(); -}; + std::shared_ptr cursor = {}); #endif diff --git a/src/history.cpp b/src/history.cpp deleted file mode 100644 index 583968130..000000000 --- a/src/history.cpp +++ /dev/null @@ -1,1589 +0,0 @@ -// History functions, part of the user interface. -#include "config.h" // IWYU pragma: keep - -#include -#include -#include - -#include -// We need the sys/file.h for the flock() declaration on Linux but not OS X. -#include // IWYU pragma: keep -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "ast.h" -#include "common.h" -#include "env.h" -#include "expand.h" -#include "fallback.h" // IWYU pragma: keep -#include "fds.h" -#include "flog.h" -#include "global_safety.h" -#include "history.h" -#include "history_file.h" -#include "io.h" -#include "iothread.h" -#include "lru.h" -#include "operation_context.h" -#include "parse_constants.h" -#include "parse_util.h" -#include "path.h" -#include "wcstringutil.h" -#include "wildcard.h" // IWYU pragma: keep -#include "wutil.h" // IWYU pragma: keep - -// Our history format is intended to be valid YAML. Here it is: -// -// - cmd: ssh blah blah blah -// when: 2348237 -// paths: -// - /path/to/something -// - /path/to/something_else -// -// Newlines are replaced by \n. Backslashes are replaced by \\. - -// This is the history session ID we use by default if the user has not set env var fish_history. -#define DFLT_FISH_HISTORY_SESSION_ID L"fish" - -// When we rewrite the history, the number of items we keep. -#define HISTORY_SAVE_MAX (1024 * 256) - -// Default buffer size for flushing to the history file. -#define HISTORY_OUTPUT_BUFFER_SIZE (64 * 1024) - -// The file access mode we use for creating history files -static constexpr int history_file_mode = 0600; - -// How many times we retry to save -// Saving may fail if the file is modified in between our opening -// the file and taking the lock -static constexpr int max_save_tries = 1024; - -namespace { - -/// If the size of \p buffer is at least \p min_size, output the contents of a string \p str to \p -/// fd, and clear the string. \return 0 on success, an error code on failure. -int flush_to_fd(std::string *buffer, int fd, size_t min_size) { - if (buffer->empty() || buffer->size() < min_size) { - return 0; - } - if (write_loop(fd, buffer->data(), buffer->size()) < 0) { - return errno; - } - buffer->clear(); - return 0; -} - -class time_profiler_t { - const char *what; - double start; - - public: - explicit time_profiler_t(const char *w) { - what = w; - start = timef(); - } - - ~time_profiler_t() { - double end = timef(); - FLOGF(profile_history, "%s: %.0f ms", what, (end - start) * 1000); - } -}; - -/// \return the path for the history file for the given \p session_id, or none() if it could not be -/// loaded. If suffix is provided, append that suffix to the path; this is used for temporary files. -maybe_t history_filename(const wcstring &session_id, const wcstring &suffix = {}) { - if (session_id.empty()) return none(); - - wcstring result; - if (!path_get_data(result)) return none(); - - result.append(L"/"); - result.append(session_id); - result.append(L"_history"); - result.append(suffix); - return result; -} - -} // anonymous namespace - -class history_lru_cache_t : public lru_cache_t { - public: - explicit history_lru_cache_t(size_t max) : lru_cache_t(max) {} - - /// Function to add a history item. - void add_item(history_item_t item) { - // Skip empty items. - if (item.empty()) return; - - // See if it's in the cache. If it is, update the timestamp. If not, we create a new node - // and add it. Note that calling get_node promotes the node to the front. - wcstring key = item.str(); - history_item_t *node = this->get(key); - if (node == nullptr) { - this->insert(std::move(key), std::move(item)); - } else { - node->creation_timestamp = std::max(node->timestamp(), item.timestamp()); - // What to do about paths here? Let's just ignore them. - } - } -}; - -/// We can merge two items if they are the same command. We use the more recent timestamp, more -/// recent identifier, and the longer list of required paths. -bool history_item_t::merge(const history_item_t &item) { - // We can only merge items if they agree on their text and persistence mode. - if (this->contents != item.contents || this->persist_mode != item.persist_mode) { - return false; - } - - // Ok, merge this item. - this->creation_timestamp = std::max(this->creation_timestamp, item.creation_timestamp); - if (this->required_paths.size() < item.required_paths.size()) { - this->required_paths = item.required_paths; - } - if (this->identifier < item.identifier) { - this->identifier = item.identifier; - } - return true; -} - -history_item_t::history_item_t(wcstring str, time_t when, history_identifier_t ident, - history_persistence_mode_t persist_mode) - : contents(std::move(str)), - creation_timestamp(when), - identifier(ident), - persist_mode(persist_mode) {} - -bool history_item_t::matches_search(const wcstring &term, enum history_search_type_t type, - bool case_sensitive) const { - // Note that 'term' has already been lowercased when constructing the - // search object if we're doing a case insensitive search. - wcstring contents_lower; - if (!case_sensitive) { - contents_lower = wcstolower(contents); - } - const wcstring &content_to_match = case_sensitive ? contents : contents_lower; - - switch (type) { - case history_search_type_t::exact: { - return term == content_to_match; - } - case history_search_type_t::contains: { - return content_to_match.find(term) != wcstring::npos; - } - case history_search_type_t::prefix: { - return string_prefixes_string(term, content_to_match); - } - case history_search_type_t::contains_glob: { - wcstring wcpattern1 = parse_util_unescape_wildcards(term); - if (wcpattern1.front() != ANY_STRING) wcpattern1.insert(0, 1, ANY_STRING); - if (wcpattern1.back() != ANY_STRING) wcpattern1.push_back(ANY_STRING); - return wildcard_match(content_to_match, wcpattern1); - } - case history_search_type_t::prefix_glob: { - wcstring wcpattern2 = parse_util_unescape_wildcards(term); - if (wcpattern2.back() != ANY_STRING) wcpattern2.push_back(ANY_STRING); - return wildcard_match(content_to_match, wcpattern2); - } - case history_search_type_t::contains_subsequence: { - return subsequence_in_string(term, content_to_match); - } - case history_search_type_t::match_everything: { - return true; - } - } - DIE("unexpected history_search_type_t value"); -} - -struct history_impl_t { - // Add a new history item to the end. If pending is set, the item will not be returned by - // item_at_index until a call to resolve_pending(). Pending items are tracked with an offset - // into the array of new items, so adding a non-pending item has the effect of resolving all - // pending items. - void add(history_item_t &&item, bool pending = false, bool do_save = true); - - // Internal function. - void clear_file_state(); - - // The name of this list. Used for picking a suitable filename and for switching modes. - const wcstring name; - - // New items. Note that these are NOT discarded on save. We need to keep these around so we can - // distinguish between items in our history and items in the history of other shells that were - // started after we were started. - history_item_list_t new_items; - - // The index of the first new item that we have not yet written. - size_t first_unwritten_new_item_index{0}; - - // Whether we have a pending item. If so, the most recently added item is ignored by - // item_at_index. - bool has_pending_item{false}; - - // Whether we should disable saving to the file for a time. - uint32_t disable_automatic_save_counter{0}; - - // Deleted item contents. - // Boolean describes if it should be deleted only in this session or in all - // (used in deduplication). - std::unordered_map deleted_items{}; - - // The buffer containing the history file contents. - std::unique_ptr file_contents{}; - - // The file ID of the history file. - file_id_t history_file_id = kInvalidFileID; - - // The boundary timestamp distinguishes old items from new items. Items whose timestamps are <= - // the boundary are considered "old". Items whose timestemps are > the boundary are new, and are - // ignored by this instance (unless they came from this instance). The timestamp may be adjusted - // by incorporate_external_changes(). - time_t boundary_timestamp{}; - - /// The most recent "unique" identifier for a history item. - history_identifier_t last_identifier{0}; - - // How many items we add until the next vacuum. Initially a random value. - int countdown_to_vacuum{-1}; - - // Whether we've loaded old items. - bool loaded_old{false}; - - // List of old items, as offsets into out mmap data. - std::deque old_item_offsets{}; - - // If set, we gave up on file locking because it took too long. - // Note this is shared among all history instances. - static relaxed_atomic_bool_t abandoned_locking; - - /// \return a timestamp for new items - see the implementation for a subtlety. - time_t timestamp_now() const; - - /// \return a new item identifier, incrementing our counter. - history_identifier_t next_identifier() { return ++last_identifier; } - - // Figure out the offsets of our file contents. - void populate_from_file_contents(); - - // Loads old items if necessary. - void load_old_if_needed(); - - // Deletes duplicates in new_items. - void compact_new_items(); - - // Removes trailing ephemeral items. - // Ephemeral items have leading spaces, and can only be retrieved immediately; adding any item - // removes them. - void remove_ephemeral_items(); - - // Attempts to rewrite the existing file to a target temporary file - // Returns false on error, true on success - bool rewrite_to_temporary_file(int existing_fd, int dst_fd) const; - - // Saves history by rewriting the file. - bool save_internal_via_rewrite(); - - // Saves history by appending to the file. - bool save_internal_via_appending(); - - // Saves history. - void save(bool vacuum = false); - - // Saves history unless doing so is disabled. - void save_unless_disabled(); - - explicit history_impl_t(wcstring name) - : name(std::move(name)), boundary_timestamp(time(nullptr)) {} - - history_impl_t(history_impl_t &&) = default; - ~history_impl_t() = default; - - /// Returns whether this is using the default name. - bool is_default() const; - - // Determines whether the history is empty. Unfortunately this cannot be const, since it may - // require populating the history. - bool is_empty(); - - // Remove a history item. - void remove(const wcstring &str); - - // Resolves any pending history items, so that they may be returned in history searches. - void resolve_pending(); - - // Enable / disable automatic saving. Main thread only! - void disable_automatic_saving(); - void enable_automatic_saving(); - - // Irreversibly clears history. - void clear(); - - // Clears only session. - void clear_session(); - - // Populates from older location ()in config path, rather than data path). - void populate_from_config_path(); - - // Populates from a bash history file. - void populate_from_bash(FILE *stream); - - // Incorporates the history of other shells into this history. - void incorporate_external_changes(); - - // Gets all the history into a list. This is intended for the $history environment variable. - // This may be long! - void get_history(std::vector &result); - - // Let indexes be a list of one-based indexes into the history, matching the interpretation of - // $history. That is, $history[1] is the most recently executed command. Values less than one - // are skipped. Return a mapping from index to history item text. - std::unordered_map items_at_indexes(const std::vector &idxs); - - // Sets the valid file paths for the history item with the given identifier. - void set_valid_file_paths(std::vector &&valid_file_paths, history_identifier_t ident); - - // Return the specified history at the specified index. 0 is the index of the current - // commandline. (So the most recent item is at index 1.) - history_item_t item_at_index(size_t idx); - - // Return the number of history entries. - size_t size(); - - // Maybe lock a history file. - // \return true if successful, false if locking was skipped. - static bool maybe_lock_file(int fd, int lock_type); - static void unlock_file(int fd); -}; - -relaxed_atomic_bool_t history_impl_t::abandoned_locking{false}; - -// static -bool history_impl_t::maybe_lock_file(int fd, int lock_type) { - assert(!(lock_type & LOCK_UN) && "Do not use lock_file to unlock"); - - // Don't lock if it took too long before, if we are simulating a failing lock, or if our history - // is on a remote filesystem. - if (abandoned_locking) return false; - if (history_t::chaos_mode) return false; - if (path_get_data_remoteness() == dir_remoteness_t::remote) return false; - - double start_time = timef(); - int retval = flock(fd, lock_type); - double duration = timef() - start_time; - if (duration > 0.25) { - FLOGF(warning, _(L"Locking the history file took too long (%.3f seconds)."), duration); - abandoned_locking = true; - } - return retval != -1; -} - -// static -void history_impl_t::unlock_file(int fd) { flock(fd, LOCK_UN); } - -void history_impl_t::add(history_item_t &&item, bool pending, bool do_save) { - assert(item.timestamp() != 0 && "Should not add an item with a 0 timestamp"); - // We use empty items as sentinels to indicate the end of history. - // Do not allow them to be added (#6032). - if (item.contents.empty()) { - return; - } - - // Try merging with the last item. - if (!new_items.empty() && new_items.back().merge(item)) { - // We merged, so we don't have to add anything. Maybe this item was pending, but it just got - // merged with an item that is not pending, so pending just becomes false. - this->has_pending_item = false; - } else { - // We have to add a new item. - new_items.push_back(item); - this->has_pending_item = pending; - if (do_save) save_unless_disabled(); - } -} - -void history_impl_t::save_unless_disabled() { - // Respect disable_automatic_save_counter. - if (disable_automatic_save_counter > 0) { - return; - } - - // We may or may not vacuum. We try to vacuum every kVacuumFrequency items, but start the - // countdown at a random number so that even if the user never runs more than 25 commands, we'll - // eventually vacuum. If countdown_to_vacuum is -1, it means we haven't yet picked a value for - // the counter. - const int kVacuumFrequency = 25; - if (countdown_to_vacuum < 0) { - // Generate a number in the range [0, kVacuumFrequency). - std::uniform_int_distribution dist{0, kVacuumFrequency - 1}; - unsigned seed = - static_cast(std::chrono::system_clock::now().time_since_epoch().count()); - std::minstd_rand gen{seed}; - countdown_to_vacuum = dist(gen); - } - - // Determine if we're going to vacuum. - bool vacuum = false; - if (countdown_to_vacuum == 0) { - countdown_to_vacuum = kVacuumFrequency; - vacuum = true; - } - - // This might be a good candidate for moving to a background thread. - time_profiler_t profiler(vacuum ? "save vacuum" //!OCLINT(unused var) - : "save no vacuum"); //!OCLINT(side-effect) - this->save(vacuum); - - // Update our countdown. - assert(countdown_to_vacuum > 0); - countdown_to_vacuum--; -} - -// Remove matching history entries from our list of new items. This only supports literal, -// case-sensitive, matches. -void history_impl_t::remove(const wcstring &str_to_remove) { - // Add to our list of deleted items. - deleted_items.insert(std::pair(str_to_remove, false)); - - size_t idx = new_items.size(); - while (idx--) { - bool matched = new_items.at(idx).str() == str_to_remove; - if (matched) { - new_items.erase(new_items.begin() + idx); - // If this index is before our first_unwritten_new_item_index, then subtract one from - // that index so it stays pointing at the same item. If it is equal to or larger, then - // we have not yet written this item, so we don't have to adjust the index. - if (idx < first_unwritten_new_item_index) { - first_unwritten_new_item_index--; - } - } - } - assert(first_unwritten_new_item_index <= new_items.size()); -} - -void history_impl_t::set_valid_file_paths(std::vector &&valid_file_paths, - history_identifier_t ident) { - // 0 identifier is used to mean "not necessary". - if (ident == 0) { - return; - } - - // Look for an item with the given identifier. It is likely to be at the end of new_items. - for (auto iter = new_items.rbegin(); iter != new_items.rend(); ++iter) { - if (iter->identifier == ident) { // found it - iter->required_paths = std::move(valid_file_paths); - break; - } - } -} - -void history_impl_t::get_history(std::vector &result) { - // If we have a pending item, we skip the first encountered (i.e. last) new item. - bool next_is_pending = this->has_pending_item; - std::unordered_set seen; - - // Append new items. - for (auto iter = new_items.crbegin(); iter < new_items.crend(); ++iter) { - // Skip a pending item if we have one. - if (next_is_pending) { - next_is_pending = false; - continue; - } - - if (seen.insert(iter->str()).second) result.push_back(iter->str()); - } - - // Append old items. - load_old_if_needed(); - for (auto iter = old_item_offsets.crbegin(); iter != old_item_offsets.crend(); ++iter) { - size_t offset = *iter; - const history_item_t item = file_contents->decode_item(offset); - if (seen.insert(item.str()).second) result.push_back(item.str()); - } -} - -size_t history_impl_t::size() { - size_t new_item_count = new_items.size(); - if (this->has_pending_item && new_item_count > 0) new_item_count -= 1; - load_old_if_needed(); - size_t old_item_count = old_item_offsets.size(); - return new_item_count + old_item_count; -} - -history_item_t history_impl_t::item_at_index(size_t idx) { - // 0 is considered an invalid index. - assert(idx > 0); - idx--; - - // Determine how many "resolved" (non-pending) items we have. We can have at most one pending - // item, and it's always the last one. - size_t resolved_new_item_count = new_items.size(); - if (this->has_pending_item && resolved_new_item_count > 0) { - resolved_new_item_count -= 1; - } - - // idx == 0 corresponds to the last resolved item. - if (idx < resolved_new_item_count) { - return new_items.at(resolved_new_item_count - idx - 1); - } - - // Now look in our old items. - idx -= resolved_new_item_count; - load_old_if_needed(); - size_t old_item_count = old_item_offsets.size(); - if (idx < old_item_count) { - // idx == 0 corresponds to last item in old_item_offsets. - size_t offset = old_item_offsets.at(old_item_count - idx - 1); - return file_contents->decode_item(offset); - } - - // Index past the valid range, so return an empty history item. - return history_item_t{}; -} - -std::unordered_map history_impl_t::items_at_indexes(const std::vector &idxs) { - std::unordered_map result; - for (long idx : idxs) { - if (idx <= 0) { - // Skip non-positive entries. - continue; - } - // Insert an empty string to see if this is the first time the index is encountered. If so, - // we have to go fetch the item. - auto iter_inserted = result.emplace(idx, wcstring{}); - if (iter_inserted.second) { - // New key. - auto item = item_at_index(size_t(idx)); - iter_inserted.first->second = std::move(item.contents); - } - } - return result; -} - -time_t history_impl_t::timestamp_now() const { - time_t when = time(nullptr); - // Big hack: do not allow timestamps equal to our boundary date. This is because we include - // items whose timestamps are equal to our boundary when reading old history, so we can catch - // "just closed" items. But this means that we may interpret our own items, that we just wrote, - // as old items, if we wrote them in the same second as our birthdate. - if (when == this->boundary_timestamp) { - when++; - } - return when; -} - -void history_impl_t::populate_from_file_contents() { - old_item_offsets.clear(); - if (file_contents) { - size_t cursor = 0; - maybe_t offset; - while ((offset = file_contents->offset_of_next_item(&cursor, boundary_timestamp)) - .has_value()) { - // Remember this item. - old_item_offsets.push_back(*offset); - } - } - - FLOGF(history, "Loaded %lu old items", old_item_offsets.size()); -} - -void history_impl_t::load_old_if_needed() { - if (loaded_old) return; - loaded_old = true; - - time_profiler_t profiler("load_old"); //!OCLINT(side-effect) - if (maybe_t filename = history_filename(name)) { - autoclose_fd_t file{wopen_cloexec(*filename, O_RDONLY)}; - int fd = file.fd(); - if (fd >= 0) { - // Take a read lock to guard against someone else appending. This is released after - // getting the file's length. We will read the file after releasing the lock, but that's - // not a problem, because we never modify already written data. In short, the purpose of - // this lock is to ensure we don't see the file size change mid-update. - // - // We may fail to lock (e.g. on lockless NFS - see issue #685. In that case, we proceed - // as if it did not fail. The risk is that we may get an incomplete history item; this - // is unlikely because we only treat an item as valid if it has a terminating newline. - bool locked = maybe_lock_file(fd, LOCK_SH); - file_contents = history_file_contents_t::create(fd); - this->history_file_id = file_contents ? file_id_for_fd(fd) : kInvalidFileID; - if (locked) unlock_file(fd); - - time_profiler_t profiler("populate_from_file_contents"); //!OCLINT(side-effect) - this->populate_from_file_contents(); - } - } -} - -bool history_search_t::go_to_next_match(history_search_direction_t direction) { - // Backwards means increasing our index. - size_t invalid_index; - ssize_t increment; - - if (direction == history_search_direction_t::backward) { - invalid_index = static_cast(-1); - increment = 1; - } else { - assert(direction == history_search_direction_t::forward); - invalid_index = 0; - increment = -1; - } - - if (current_index_ == invalid_index) return false; - - size_t index = current_index_; - while ((index += increment) != invalid_index) { - history_item_t item = history_->item_at_index(index); - - // We're done if it's empty or we cancelled. - if (item.empty()) { - return false; - } - - // Look for an item that matches and (if deduping) that we haven't seen before. - if (!item.matches_search(canon_term_, search_type_, !ignores_case())) { - continue; - } - - // Skip if deduplicating. - if (dedup() && !deduper_.insert(item.str()).second) { - continue; - } - - // This is our new item. - current_item_ = std::move(item); - current_index_ = index; - return true; - } - return false; -} - -const history_item_t &history_search_t::current_item() const { - assert(current_item_ && "No current item"); - return *current_item_; -} - -const wcstring &history_search_t::current_string() const { return this->current_item().str(); } - -size_t history_search_t::current_index() const { return this->current_index_; } - -void history_impl_t::clear_file_state() { - // Erase everything we know about our file. - file_contents.reset(); - loaded_old = false; - old_item_offsets.clear(); -} - -void history_impl_t::compact_new_items() { - // Keep only the most recent items with the given contents. - std::unordered_set seen; - size_t idx = new_items.size(); - while (idx--) { - const history_item_t &item = new_items[idx]; - - // Only compact persisted items. - if (!item.should_write_to_disk()) continue; - - if (!seen.insert(item.contents).second) { - // This item was not inserted because it was already in the set, so delete the item at - // this index. - new_items.erase(new_items.begin() + idx); - - if (idx < first_unwritten_new_item_index) { - // Decrement first_unwritten_new_item_index if we are deleting a previously written - // item. - first_unwritten_new_item_index--; - } - } - } -} - -void history_impl_t::remove_ephemeral_items() { - while (!new_items.empty() && - new_items.back().persist_mode == history_persistence_mode_t::ephemeral) { - new_items.pop_back(); - } - first_unwritten_new_item_index = std::min(first_unwritten_new_item_index, new_items.size()); -} - -// Given the fd of an existing history file, or -1 if none, write -// a new history file to temp_fd. Returns true on success, false -// on error -bool history_impl_t::rewrite_to_temporary_file(int existing_fd, int dst_fd) const { - // We are reading FROM existing_fd and writing TO dst_fd - // dst_fd must be valid; existing_fd does not need to be - assert(dst_fd >= 0); - - // Make an LRU cache to save only the last N elements. - history_lru_cache_t lru(HISTORY_SAVE_MAX); - - // Read in existing items (which may have changed out from underneath us, so don't trust our - // old file contents). - if (auto local_file = history_file_contents_t::create(existing_fd)) { - size_t cursor = 0; - maybe_t offset; - while ((offset = local_file->offset_of_next_item(&cursor, 0)).has_value()) { - // Try decoding an old item. - history_item_t old_item = local_file->decode_item(*offset); - - // If old item is newer than session always erase if in deleted. - if (old_item.timestamp() > boundary_timestamp) { - if (old_item.empty() || deleted_items.count(old_item.str()) > 0) { - continue; - } - lru.add_item(std::move(old_item)); - } else { - // If old item is older and in deleted items don't erase if added by - // clear_session. - if (old_item.empty() || (deleted_items.count(old_item.str()) > 0 && - !deleted_items.at(old_item.str()))) { - continue; - } - // Add this old item. - lru.add_item(std::move(old_item)); - } - } - } - - // Insert any unwritten new items - for (auto iter = new_items.cbegin() + this->first_unwritten_new_item_index; - iter != new_items.cend(); ++iter) { - if (iter->should_write_to_disk()) { - lru.add_item(*iter); - } - } - - // Stable-sort our items by timestamp - // This is because we may have read "old" items with a later timestamp than our "new" items - // This is the essential step that roughly orders items by history - lru.stable_sort([](const history_item_t &item1, const history_item_t &item2) { - return item1.timestamp() < item2.timestamp(); - }); - - // Write them out. - int err = 0; - std::string buffer; - buffer.reserve(HISTORY_OUTPUT_BUFFER_SIZE + 128); - for (const auto key_item : lru) { - append_history_item_to_buffer(key_item.second, &buffer); - err = flush_to_fd(&buffer, dst_fd, HISTORY_OUTPUT_BUFFER_SIZE); - if (err) break; - } - if (!err) { - err = flush_to_fd(&buffer, dst_fd, 0); - } - if (err) { - FLOGF(history_file, L"Error %d when writing to temporary history file", err); - } - - return err == 0; -} - -// Returns the fd of an opened temporary file, or an invalid fd on failure. -static autoclose_fd_t create_temporary_file(const wcstring &name_template, wcstring *out_path) { - for (int attempt = 0; attempt < 10; attempt++) { - std::string narrow_str = wcs2zstring(name_template); - autoclose_fd_t out_fd{fish_mkstemp_cloexec(&narrow_str[0])}; - if (out_fd.valid()) { - *out_path = str2wcstring(narrow_str); - return out_fd; - } - } - return autoclose_fd_t{}; -} - -bool history_impl_t::save_internal_via_rewrite() { - FLOGF(history, "Saving %lu items via rewrite", - new_items.size() - first_unwritten_new_item_index); - bool ok = false; - - // We want to rewrite the file, while holding the lock for as briefly as possible - // To do this, we speculatively write a file, and then lock and see if our original file changed - // Repeat until we succeed or give up - const maybe_t possibly_indirect_target_name = history_filename(name); - const maybe_t tmp_name_template = history_filename(name, L".XXXXXX"); - if (!possibly_indirect_target_name.has_value() || !tmp_name_template.has_value()) { - return false; - } - - // If the history file is a symlink, we want to rewrite the real file so long as we can find it. - wcstring target_name; - if (auto maybe_real_path = wrealpath(*possibly_indirect_target_name)) { - target_name = *maybe_real_path; - } else { - target_name = *possibly_indirect_target_name; - } - - // Make our temporary file - // Remember that we have to close this fd! - wcstring tmp_name; - autoclose_fd_t tmp_file = create_temporary_file(*tmp_name_template, &tmp_name); - if (!tmp_file.valid()) { - return false; - } - const int tmp_fd = tmp_file.fd(); - bool done = false; - for (int i = 0; i < max_save_tries && !done; i++) { - // Open any target file, but do not lock it right away - autoclose_fd_t target_fd_before{ - wopen_cloexec(target_name, O_RDONLY | O_CREAT, history_file_mode)}; - file_id_t orig_file_id = file_id_for_fd(target_fd_before.fd()); // possibly invalid - bool wrote = this->rewrite_to_temporary_file(target_fd_before.fd(), tmp_fd); - target_fd_before.close(); - if (!wrote) { - // Failed to write, no good - break; - } - - // The crux! We rewrote the history file; see if the history file changed while we - // were rewriting it. Make an effort to take the lock before checking, to avoid racing. - // If the open fails, then proceed; this may be because there is no current history - file_id_t new_file_id = kInvalidFileID; - autoclose_fd_t target_fd_after{wopen_cloexec(target_name, O_RDONLY)}; - if (target_fd_after.valid()) { - // critical to take the lock before checking file IDs, - // and hold it until after we are done replacing. - // Also critical to check the file at the path, NOT based on our fd. - // It's only OK to replace the file while holding the lock. - // Note any lock is released when target_fd_after is closed. - (void)maybe_lock_file(target_fd_after.fd(), LOCK_EX); - new_file_id = file_id_for_path(target_name); - } - bool can_replace_file = (new_file_id == orig_file_id || new_file_id == kInvalidFileID); - if (!can_replace_file) { - // The file has changed, so we're going to re-read it - // Truncate our tmp_fd so we can reuse it - if (ftruncate(tmp_fd, 0) == -1 || lseek(tmp_fd, 0, SEEK_SET) == -1) { - FLOGF(history_file, L"Error %d when truncating temporary history file", errno); - } - } else { - // The file is unchanged, or the new file doesn't exist or we can't read it - // We also attempted to take the lock, so we feel confident in replacing it - - // Ensure we maintain the ownership and permissions of the original (#2355). If the - // stat fails, we assume (hope) our default permissions are correct. This - // corresponds to e.g. someone running sudo -E as the very first command. If they - // did, it would be tricky to set the permissions correctly. (bash doesn't get this - // case right either). - struct stat sbuf; - if (target_fd_after.valid() && fstat(target_fd_after.fd(), &sbuf) >= 0) { - if (fchown(tmp_fd, sbuf.st_uid, sbuf.st_gid) == -1) { - FLOGF(history_file, L"Error %d when changing ownership of history file", errno); - } - if (fchmod(tmp_fd, sbuf.st_mode) == -1) { - FLOGF(history_file, L"Error %d when changing mode of history file", errno); - } - } - - // Slide it into place - if (wrename(tmp_name, target_name) == -1) { - const char *error = std::strerror(errno); - FLOGF(error, _(L"Error when renaming history file: %s"), error); - } - - // We did it - done = true; - } - } - - // Ensure we never leave the old file around - wunlink(tmp_name); - - if (done) { - // We've saved everything, so we have no more unsaved items. - this->first_unwritten_new_item_index = new_items.size(); - - // We deleted our deleted items. - this->deleted_items.clear(); - - // Our history has been written to the file, so clear our state so we can re-reference the - // file. - this->clear_file_state(); - } - - return ok; -} - -// Function called to save our unwritten history file by appending to the existing history file -// Returns true on success, false on failure. -bool history_impl_t::save_internal_via_appending() { - FLOGF(history, "Saving %lu items via appending", - new_items.size() - first_unwritten_new_item_index); - // No deleting allowed. - assert(deleted_items.empty()); - - bool ok = false; - - // If the file is different (someone vacuumed it) then we need to update our mmap. - bool file_changed = false; - - // Get the path to the real history file. - maybe_t maybe_history_path = history_filename(name); - if (!maybe_history_path) { - return true; - } - wcstring history_path = maybe_history_path.acquire(); - - // We are going to open the file, lock it, append to it, and then close it - // After locking it, we need to stat the file at the path; if there is a new file there, it - // means the file was replaced and we have to try again. - // Limit our max tries so we don't do this forever. - autoclose_fd_t history_fd{}; - for (int i = 0; i < max_save_tries; i++) { - autoclose_fd_t fd{wopen_cloexec(history_path, O_WRONLY | O_APPEND)}; - if (!fd.valid()) { - // can't open, we're hosed - break; - } - // Exclusive lock on the entire file. This is released when we close the file (below). This - // may fail on (e.g.) lockless NFS. If so, proceed as if it did not fail; the risk is that - // we may get interleaved history items, which is considered better than no history, or - // forcing everything through the slow copy-move mode. We try to minimize this possibility - // by writing with O_APPEND. - maybe_lock_file(fd.fd(), LOCK_EX); - const file_id_t file_id = file_id_for_fd(fd.fd()); - if (file_id_for_path(history_path) == file_id) { - // File IDs match, so the file we opened is still at that path - // We're going to use this fd - if (file_id != this->history_file_id) { - file_changed = true; - } - history_fd = std::move(fd); - break; - } - } - - if (history_fd.valid()) { - // We (hopefully successfully) took the exclusive lock. Append to the file. - // Note that this is sketchy for a few reasons: - // - Another shell may have appended its own items with a later timestamp, so our file may - // no longer be sorted by timestamp. - // - Another shell may have appended the same items, so our file may now contain - // duplicates. - // - // We cannot modify any previous parts of our file, because other instances may be reading - // those portions. We can only append. - // - // Originally we always rewrote the file on saving, which avoided both of these problems. - // However, appending allows us to save history after every command, which is nice! - // - // Periodically we "clean up" the file by rewriting it, so that most of the time it doesn't - // have duplicates, although we don't yet sort by timestamp (the timestamp isn't really used - // for much anyways). - - // So far so good. Write all items at or after first_unwritten_new_item_index. Note that we - // write even a pending item - pending items are ignored by history within the command - // itself, but should still be written to the file. - // TODO: consider filling the buffer ahead of time, so we can just lock, splat, and unlock? - int err = 0; - // Use a small buffer size for appending, we usually only have 1 item - std::string buffer; - while (first_unwritten_new_item_index < new_items.size()) { - const history_item_t &item = new_items.at(first_unwritten_new_item_index); - if (item.should_write_to_disk()) { - append_history_item_to_buffer(item, &buffer); - err = flush_to_fd(&buffer, history_fd.fd(), HISTORY_OUTPUT_BUFFER_SIZE); - if (err) break; - } - // We wrote or skipped this item, hooray. - first_unwritten_new_item_index++; - } - - if (!err) { - err = flush_to_fd(&buffer, history_fd.fd(), 0); - } - - // Since we just modified the file, update our mmap_file_id to match its current state - // Otherwise we'll think the file has been changed by someone else the next time we go to - // write. - // We don't update the mapping since we only appended to the file, and everything we - // appended remains in our new_items - this->history_file_id = file_id_for_fd(history_fd.fd()); - - ok = (err == 0); - } - history_fd.close(); - - // If someone has replaced the file, forget our file state. - if (file_changed) { - this->clear_file_state(); - } - - return ok; -} - -/// Save the specified mode to file; optionally also vacuums. -void history_impl_t::save(bool vacuum) { - // Nothing to do if there's no new items. - if (first_unwritten_new_item_index >= new_items.size() && deleted_items.empty()) return; - - if (!history_filename(name).has_value()) { - // We're in the "incognito" mode. Pretend we've saved the history. - this->first_unwritten_new_item_index = new_items.size(); - this->deleted_items.clear(); - this->clear_file_state(); - } - - // Compact our new items so we don't have duplicates. - this->compact_new_items(); - - // Try saving. If we have items to delete, we have to rewrite the file. If we do not, we can - // append to it. - bool ok = false; - if (!vacuum && deleted_items.empty()) { - // Try doing a fast append. - ok = save_internal_via_appending(); - if (!ok) { - FLOGF(history, "Appending failed"); - } - } - if (!ok) { - // We did not or could not append; rewrite the file ("vacuum" it). - this->save_internal_via_rewrite(); - } -} - -// Formats a single history record, including a trailing newline. -// -// Returns nothing. The only possible failure involves formatting the timestamp. If that happens we -// simply omit the timestamp from the output. -static void format_history_record(const history_item_t &item, const wchar_t *show_time_format, - bool null_terminate, wcstring *result) { - result->clear(); - if (show_time_format) { - const time_t seconds = item.timestamp(); - struct tm timestamp; - if (localtime_r(&seconds, ×tamp)) { - const int max_tstamp_length = 100; - wchar_t timestamp_string[max_tstamp_length + 1]; - if (std::wcsftime(timestamp_string, max_tstamp_length, show_time_format, ×tamp) != - 0) { - result->append(timestamp_string); - } - } - } - - result->append(item.str()); - result->push_back(null_terminate ? L'\0' : L'\n'); -} - -void history_impl_t::disable_automatic_saving() { - disable_automatic_save_counter++; - assert(disable_automatic_save_counter != 0); // overflow! -} - -void history_impl_t::enable_automatic_saving() { - assert(disable_automatic_save_counter > 0); // underflow - disable_automatic_save_counter--; - save_unless_disabled(); -} - -void history_impl_t::clear() { - new_items.clear(); - deleted_items.clear(); - first_unwritten_new_item_index = 0; - old_item_offsets.clear(); - if (maybe_t filename = history_filename(name)) { - wunlink(*filename); - } - this->clear_file_state(); -} - -void history_impl_t::clear_session() { - for (const auto &item : new_items) { - deleted_items.insert(std::pair(item.str(), true)); - } - - new_items.clear(); - first_unwritten_new_item_index = 0; -} - -bool history_impl_t::is_default() const { return name == DFLT_FISH_HISTORY_SESSION_ID; } - -bool history_impl_t::is_empty() { - // If we have new items, we're not empty. - if (!new_items.empty()) return false; - - bool empty = false; - if (loaded_old) { - // If we've loaded old items, see if we have any offsets. - empty = old_item_offsets.empty(); - } else { - // If we have not loaded old items, don't actually load them (which may be expensive); just - // stat the file and see if it exists and is nonempty. - const maybe_t where = history_filename(name); - if (!where.has_value()) { - return true; - } - - struct stat buf = {}; - if (wstat(*where, &buf) != 0) { - // Access failed, assume missing. - empty = true; - } else { - // We're empty if the file is empty. - empty = (buf.st_size == 0); - } - } - return empty; -} - -void history_t::add(wcstring str) { - auto imp = this->impl(); - time_t when = imp->timestamp_now(); - imp->add(history_item_t(std::move(str), when)); -} - -/// Populates from older location (in config path, rather than data path) This is accomplished by -/// clearing ourselves, and copying the contents of the old history file to the new history file. -/// The new contents will automatically be re-mapped later. -void history_impl_t::populate_from_config_path() { - maybe_t new_file = history_filename(name); - if (!new_file.has_value()) { - return; - } - - wcstring old_file; - if (path_get_config(old_file)) { - old_file.append(L"/"); - old_file.append(name); - old_file.append(L"_history"); - autoclose_fd_t src_fd{wopen_cloexec(old_file, O_RDONLY, 0)}; - if (src_fd.valid()) { - // Clear must come after we've retrieved the new_file name, and before we open - // destination file descriptor, since it destroys the name and the file. - this->clear(); - - autoclose_fd_t dst_fd{wopen_cloexec(*new_file, O_WRONLY | O_CREAT, history_file_mode)}; - char buf[BUFSIZ]; - ssize_t size; - while ((size = read(src_fd.fd(), buf, BUFSIZ)) > 0) { - ssize_t written = write(dst_fd.fd(), buf, static_cast(size)); - if (written < 0) { - // This message does not have high enough priority to be shown by default. - FLOGF(history_file, L"Error when writing history file"); - break; - } - } - } - } -} - -/// Decide whether we ought to import a bash history line into fish. This is a very crude heuristic. -static bool should_import_bash_history_line(const wcstring &line) { - if (line.empty()) return false; - - // The following are Very naive tests! - - // Skip comments. - if (line[0] == '#') return false; - - // Skip lines with backticks because we don't have that syntax, - // Skip brace expansions and globs because they don't work like ours - // Skip lines with literal tabs since we don't handle them well and we don't know what they - // mean. It could just be whitespace or it's actually passed somewhere (like e.g. `sed`). - // Skip lines that end with a backslash. We do not handle multiline commands from bash history. - if (line.find_first_of(L"`{*\t\\") != std::string::npos) return false; - - // Skip lines with [[...]] and ((...)) since we don't handle those constructs. - if (line.find(L"[[") != std::string::npos) return false; - if (line.find(L"]]") != std::string::npos) return false; - if (line.find(L"((") != std::string::npos) return false; - if (line.find(L"))") != std::string::npos) return false; - // "<<" here is a proxy for heredocs (and herestrings). - if (line.find(L"<<") != std::string::npos) return false; - - if (ast_parse(line)->errored()) return false; - - // In doing this test do not allow incomplete strings. Hence the "false" argument. - auto errors = new_parse_error_list(); - parse_util_detect_errors(line, &*errors); - return errors->empty(); -} - -/// Import a bash command history file. Bash's history format is very simple: just lines with #s for -/// comments. Ignore a few commands that are bash-specific. It makes no attempt to handle multiline -/// commands. We can't actually parse bash syntax and the bash history file does not unambiguously -/// encode multiline commands. -void history_impl_t::populate_from_bash(FILE *stream) { - // Process the entire history file until EOF is observed. - // Pretend all items were created at this time. - const auto when = this->timestamp_now(); - bool eof = false; - while (!eof) { - auto line = std::string(); - - // Loop until we've read a line or EOF is observed. - while (true) { - char buff[128]; - if (!fgets(buff, sizeof buff, stream)) { - eof = true; - break; - } - - // Deal with the newline if present. - char *a_newline = std::strchr(buff, '\n'); - if (a_newline) *a_newline = '\0'; - line.append(buff); - if (a_newline) break; - } - - wcstring wide_line = trim(str2wcstring(line)); - // Add this line if it doesn't contain anything we know we can't handle. - if (should_import_bash_history_line(wide_line)) { - this->add(history_item_t(std::move(wide_line), when), false /* pending */, - false /* do_save */); - } - } - this->save_unless_disabled(); -} - -void history_impl_t::incorporate_external_changes() { - // To incorporate new items, we simply update our timestamp to now, so that items from previous - // instances get added. We then clear the file state so that we remap the file. Note that this - // is somewhat expensive because we will be going back over old items. An optimization would be - // to preserve old_item_offsets so that they don't have to be recomputed. (However, then items - // *deleted* in other instances would not show up here). - time_t new_timestamp = time(nullptr); - - // If for some reason the clock went backwards, we don't want to start dropping items; therefore - // we only do work if time has progressed. This also makes multiple calls cheap. - if (new_timestamp > this->boundary_timestamp) { - this->boundary_timestamp = new_timestamp; - this->clear_file_state(); - - // We also need to erase new items, since we go through those first, and that means we - // will not properly interleave them with items from other instances. - // We'll pick them up from the file (#2312) - // TODO: this will drop items that had no_persist set, how can we avoid that while still - // properly interleaving? - this->save(false); - this->new_items.clear(); - this->first_unwritten_new_item_index = 0; - } -} - -/// Return the prefix for the files to be used for command and read history. -wcstring history_session_id(std::unique_ptr fish_history) { - wcstring result = DFLT_FISH_HISTORY_SESSION_ID; - - const auto var = std::move(fish_history); - if (var) { - wcstring session_id = var->as_string(); - if (session_id.empty()) { - result.clear(); - } else if (valid_var_name(session_id)) { - result = session_id; - } else { - FLOGF(error, - _(L"History session ID '%ls' is not a valid variable name. " - L"Falling back to `%ls`."), - session_id.c_str(), result.c_str()); - } - } - - return result; -} - -wcstring history_session_id(const environment_t &vars) { - auto fish_history = vars.get(L"fish_history"); - auto var = - fish_history ? std::make_unique(*fish_history) : std::unique_ptr{}; - return history_session_id(std::move(var)); -} - -path_list_t expand_and_detect_paths(const path_list_t &paths, const environment_t &vars) { - ASSERT_IS_BACKGROUND_THREAD(); - std::vector result; - wcstring working_directory = vars.get_pwd_slash(); - operation_context_t ctx(vars, kExpansionLimitBackground); - for (const wcstring &path : paths) { - // Suppress cmdsubs since we are on a background thread and don't want to execute fish - // script. - // Suppress wildcards because we want to suggest e.g. `rm *` even if the directory - // is empty (and so rm will fail); this is nevertheless a useful command because it - // confirms the directory is empty. - wcstring expanded_path = path; - if (expand_one(expanded_path, {expand_flag::skip_cmdsubst, expand_flag::skip_wildcards}, - ctx)) { - if (path_is_valid(expanded_path, working_directory)) { - // Note we return the original (unexpanded) path. - result.push_back(path); - } - } - } - return result; -} - -bool all_paths_are_valid(const path_list_t &paths, const operation_context_t &ctx) { - ASSERT_IS_BACKGROUND_THREAD(); - wcstring working_directory = ctx.vars.get_pwd_slash(); - for (const wcstring &path : paths) { - if (ctx.cancel_checker()) { - return false; - } - wcstring expanded_path = path; - if (!expand_one(expanded_path, {expand_flag::skip_cmdsubst, expand_flag::skip_wildcards}, - ctx)) { - return false; - } - if (!path_is_valid(expanded_path, working_directory)) { - return false; - } - } - return true; -} - -static bool string_could_be_path(const wcstring &potential_path) { - // Assume that things with leading dashes aren't paths. - return !(potential_path.empty() || potential_path.at(0) == L'-'); -} - -/// impl_wrapper_t is used to avoid forming owning_lock in -/// the .h file; see #7023. -struct history_t::impl_wrapper_t { - owning_lock impl; - explicit impl_wrapper_t(wcstring &&name) : impl(history_impl_t(std::move(name))) {} -}; - -/// Very simple, just mark that we have no more pending items. -void history_impl_t::resolve_pending() { this->has_pending_item = false; } - -bool history_t::chaos_mode = false; - -/* OpenBSD's mmap is not synchronized with other file operations. In particular it appears we may - * write() a file, fsync() it, close it, mmap() it, and call msync(), and we still may not see the - * newly written data. Just don't try mmap here. */ -#if defined(__OpenBSD__) -bool history_t::never_mmap = true; -#else -bool history_t::never_mmap = false; -#endif - -history_t::history_t(wcstring name) : wrap_(make_unique(std::move(name))) {} - -history_t::~history_t() = default; - -acquired_lock history_t::impl() { return wrap_->impl.acquire(); } - -acquired_lock history_t::impl() const { return wrap_->impl.acquire(); } - -bool history_t::is_default() const { return impl()->is_default(); } - -bool history_t::is_empty() { return impl()->is_empty(); } - -void history_t::add(history_item_t &&item, bool pending) { impl()->add(std::move(item), pending); } - -void history_t::remove(const wcstring &str) { impl()->remove(str); } - -void history_t::remove_ephemeral_items() { impl()->remove_ephemeral_items(); } - -// static -void history_t::add_pending_with_file_detection(const std::shared_ptr &self, - const wcstring &str, - const std::shared_ptr &vars, - history_persistence_mode_t persist_mode) { - assert(self && "Null history"); - - // We use empty items as sentinels to indicate the end of history. - // Do not allow them to be added (#6032). - if (str.empty()) { - return; - } - - // Find all arguments that look like they could be file paths. - bool needs_sync_write = false; - using namespace ast; - auto ast = ast_parse(str); - - path_list_t potential_paths; - for (auto ast_traversal = new_ast_traversal(*ast->top());;) { - auto node = ast_traversal->next(); - if (!node->has_value()) break; - if (const argument_t *arg = node->try_as_argument()) { - wcstring potential_path = *arg->source(str); - if (string_could_be_path(potential_path)) { - potential_paths.push_back(std::move(potential_path)); - } - } else if (const decorated_statement_t *stmt = node->try_as_decorated_statement()) { - // Hack hack hack - if the command is likely to trigger an exit, then don't do - // background file detection, because we won't be able to write it to our history file - // before we exit. - // Also skip it for 'echo'. This is because echo doesn't take file paths, but also - // because the history file test wants to find the commands in the history file - // immediately after running them, so it can't tolerate the asynchronous file detection. - if (stmt->decoration() == statement_decoration_t::exec) { - needs_sync_write = true; - } - - wcstring command = *stmt->command().source(str); - unescape_string_in_place(&command, UNESCAPE_DEFAULT); - if (command == L"exit" || command == L"reboot" || command == L"restart" || - command == L"echo") { - needs_sync_write = true; - } - } - } - - // If we got a path, we'll perform file detection for autosuggestion hinting. - bool wants_file_detection = !potential_paths.empty() && !needs_sync_write; - auto imp = self->impl(); - - // Make our history item. - time_t when = imp->timestamp_now(); - history_identifier_t identifier = imp->next_identifier(); - history_item_t item{str, when, identifier, persist_mode}; - - if (wants_file_detection) { - imp->disable_automatic_saving(); - - // Add the item. Then check for which paths are valid on a background thread, - // and unblock the item. - // Don't hold the lock while we perform this file detection. - imp->add(std::move(item), true /* pending */); - iothread_perform([=]() { - // Don't hold the lock while we perform this file detection. - auto validated_paths = expand_and_detect_paths(potential_paths, *vars); - auto imp = self->impl(); - imp->set_valid_file_paths(std::move(validated_paths), identifier); - imp->enable_automatic_saving(); - }); - } else { - // Add the item. - // If we think we're about to exit, save immediately, regardless of any disabling. This may - // cause us to lose file hinting for some commands, but it beats losing history items. - imp->add(std::move(item), true /* pending */); - if (needs_sync_write) { - imp->save(); - } - } -} -void history_t::resolve_pending() { impl()->resolve_pending(); } - -void history_t::save() { impl()->save(); } - -/// Perform a search of \p hist for \p search_string. Invoke a function \p func for each match. If -/// \p func returns true, continue the search; else stop it. -static void do_1_history_search(history_t *hist, history_search_type_t search_type, - const wcstring &search_string, bool case_sensitive, - const std::function &func, - const cancel_checker_t &cancel_check) { - history_search_t searcher = history_search_t(hist, search_string, search_type, - case_sensitive ? 0 : history_search_ignore_case); - while (!cancel_check() && searcher.go_to_next_match(history_search_direction_t::backward)) { - if (!func(searcher.current_item())) { - break; - } - } -} - -// Searches history. -bool history_t::search(history_search_type_t search_type, const std::vector &search_args, - const wchar_t *show_time_format, size_t max_items, bool case_sensitive, - bool null_terminate, bool reverse, const cancel_checker_t &cancel_check, - io_streams_t &streams) { - std::vector collected; - wcstring formatted_record; - size_t remaining = max_items; - bool output_error = false; - - // The function we use to act on each item. The return value indicates whether the search should - // continue (true) or stop (on false). - std::function func = [&](const history_item_t &item) -> bool { - if (remaining == 0) return false; - remaining -= 1; - format_history_record(item, show_time_format, null_terminate, &formatted_record); - if (reverse) { - // We need to collect this for later. - collected.push_back(std::move(formatted_record)); - } else { - // We can output this immediately. - if (!streams.out.append(formatted_record)) { - // This can happen if the user hit Ctrl-C to abort (maybe after the first page?). - output_error = true; - return false; - } - } - return true; - }; - - if (search_args.empty()) { - // The user had no search terms; just append everything. - do_1_history_search(this, history_search_type_t::match_everything, {}, false, func, - cancel_check); - } else { - for (const wcstring &search_string : search_args) { - if (search_string.empty()) { - streams.err.append_format(L"Searching for the empty string isn't allowed"); - return false; - } - do_1_history_search(this, search_type, search_string, case_sensitive, func, - cancel_check); - } - } - - // Output any items we collected (which only happens in reverse). - for (auto iter = collected.rbegin(); !output_error && iter != collected.rend(); ++iter) { - if (!streams.out.append(*iter)) { - // Don't force an error if output was aborted (typically via Ctrl-C/SIGINT); just don't - // try writing any more. - output_error = true; - } - } - - // We are intentionally not returning false in case of an output error, as the user aborting the - // output early (the most common case) isn't a reason to exit w/ a non-zero status code. - return true; -} - -void history_t::clear() { impl()->clear(); } - -void history_t::clear_session() { impl()->clear_session(); } - -void history_t::populate_from_config_path() { impl()->populate_from_config_path(); } - -void history_t::populate_from_bash(FILE *f) { impl()->populate_from_bash(f); } - -void history_t::incorporate_external_changes() { impl()->incorporate_external_changes(); } - -void history_t::get_history(std::vector &result) { impl()->get_history(result); } - -std::unordered_map history_t::items_at_indexes(const std::vector &idxs) { - return impl()->items_at_indexes(idxs); -} - -history_item_t history_t::item_at_index(size_t idx) { return impl()->item_at_index(idx); } - -size_t history_t::size() { return impl()->size(); } - -/// The set of all histories. -static owning_lock>> s_histories; - -void history_save_all() { - auto histories = s_histories.acquire(); - for (auto &p : *histories) { - p.second->save(); - } -} - -std::shared_ptr history_t::with_name(const wcstring &name) { - auto hs = s_histories.acquire(); - std::shared_ptr &hist = (*hs)[name]; - if (!hist) { - hist = std::make_shared(name); - } - return hist; -} - -bool in_private_mode(const environment_t &vars) { - return vars.get_unless_empty(L"fish_private_mode").has_value(); -} diff --git a/src/history.h b/src/history.h index eae4fd595..c254e3af5 100644 --- a/src/history.h +++ b/src/history.h @@ -20,211 +20,29 @@ #include "maybe.h" #include "wutil.h" // IWYU pragma: keep -class IoStreams; using io_streams_t = IoStreams; -class env_stack_t; -class environment_t; -class operation_context_t; - -/** -Fish supports multiple shells writing to history at once. Here is its strategy: - -1. All history files are append-only. Data, once written, is never modified. - -2. A history file may be re-written ("vacuumed"). This involves reading in the file and writing a -new one, while performing maintenance tasks: discarding items in an LRU fashion until we reach -the desired maximum count, removing duplicates, and sorting them by timestamp (eventually, not -implemented yet). The new file is atomically moved into place via rename(). - -3. History files are mapped in via mmap(). Before the file is mapped, the file takes a fcntl read -lock. The purpose of this lock is to avoid seeing a transient state where partial data has been -written to the file. - -4. History is appended to under a fcntl write lock. - -5. The chaos_mode boolean can be set to true to do things like lower buffer sizes which can -trigger race conditions. This is useful for testing. -*/ using path_list_t = std::vector; - -enum class history_search_type_t { - /// Search for commands exactly matching the given string. - exact, - /// Search for commands containing the given string. - contains, - /// Search for commands starting with the given string. - prefix, - /// Search for commands containing the given glob pattern. - contains_glob, - /// Search for commands starting with the given glob pattern. - prefix_glob, - /// Search for commands containing the given string as a subsequence - contains_subsequence, - /// Matches everything. - match_everything, -}; - using history_identifier_t = uint64_t; -/// Ways that a history item may be written to disk (or omitted). -enum class history_persistence_mode_t : uint8_t { - disk, // the history item is written to disk normally - memory, // the history item is stored in-memory only, not written to disk - ephemeral, // the history item is stored in-memory and deleted when a new item is added -}; +#if INCLUDE_RUST_HEADERS -class history_item_t { - public: - /// Construct from a text, timestamp, and optional identifier. - /// If \p persist_mode is ::ephemeral, then do not write this item to disk. - explicit history_item_t( - wcstring str = {}, time_t when = 0, history_identifier_t ident = 0, - history_persistence_mode_t persist_mode = history_persistence_mode_t::disk); +#include "history.rs.h" - /// \return the text as a string. - const wcstring &str() const { return contents; } +#else - /// \return whether the text is empty. - bool empty() const { return contents.empty(); } +struct HistoryItem; - /// \return whether our contents matches a search term. - bool matches_search(const wcstring &term, enum history_search_type_t type, - bool case_sensitive) const; +class HistorySharedPtr; +enum class PersistenceMode; +enum class SearchDirection; +enum class SearchType; - /// \return the timestamp for creating this history item. - time_t timestamp() const { return creation_timestamp; } +#endif // INCLUDE_RUST_HEADERS - /// \return whether this item should be persisted (written to disk). - bool should_write_to_disk() const { return persist_mode == history_persistence_mode_t::disk; } - - /// Get and set the list of arguments which referred to files. - /// This is used for autosuggestion hinting. - const path_list_t &get_required_paths() const { return required_paths; } - void set_required_paths(path_list_t paths) { required_paths = std::move(paths); } - - private: - /// Attempts to merge two compatible history items together. - bool merge(const history_item_t &item); - - /// The actual contents of the entry. - wcstring contents; - - /// Original creation time for the entry. - time_t creation_timestamp; - - /// Paths that we require to be valid for this item to be autosuggested. - path_list_t required_paths; - - /// Sometimes unique identifier used for hinting. - history_identifier_t identifier; - - /// If set, do not write this item to disk. - history_persistence_mode_t persist_mode; - - friend class history_t; - friend struct history_impl_t; - friend class history_lru_cache_t; - friend class history_tests_t; -}; - -using history_item_list_t = std::deque; - -struct history_impl_t; - -enum class history_search_direction_t { forward, backward }; - -class history_t : noncopyable_t, nonmovable_t { - friend class history_tests_t; - struct impl_wrapper_t; - const std::unique_ptr wrap_; - - acquired_lock impl(); - acquired_lock impl() const; - - /// Privately add an item. If pending, the item will not be returned by history searches until a - /// call to resolve_pending. Any trailing ephemeral items are dropped. - void add(history_item_t &&item, bool pending = false); - - /// Add a new history item with text \p str to the end of history. - void add(wcstring str); - - public: - explicit history_t(wcstring name); - ~history_t(); - - /// Whether we're in maximum chaos mode, useful for testing. - /// This causes things like locks to fail. - static bool chaos_mode; - - /// Whether to force the read path instead of mmap. This is useful for testing. - static bool never_mmap; - - /// Returns history with the given name, creating it if necessary. - static std::shared_ptr with_name(const wcstring &name); - - /// Returns whether this is using the default name. - bool is_default() const; - - /// Determines whether the history is empty. Unfortunately this cannot be const, since it may - /// require populating the history. - bool is_empty(); - - /// Remove a history item. - void remove(const wcstring &str); - - /// Remove any trailing ephemeral items. - void remove_ephemeral_items(); - - /// Add a new pending history item to the end, and then begin file detection on the items to - /// determine which arguments are paths. Arguments may be expanded (e.g. with PWD and variables) - /// using the given \p vars. The item has the given \p persist_mode. - static void add_pending_with_file_detection( - const std::shared_ptr &self, const wcstring &str, - const std::shared_ptr &vars, - history_persistence_mode_t persist_mode = history_persistence_mode_t::disk); - - /// Resolves any pending history items, so that they may be returned in history searches. - void resolve_pending(); - - /// Saves history. - void save(); - - /// Searches history. - bool search(history_search_type_t search_type, const std::vector &search_args, - const wchar_t *show_time_format, size_t max_items, bool case_sensitive, - bool null_terminate, bool reverse, const cancel_checker_t &cancel_check, - io_streams_t &streams); - - /// Irreversibly clears history. - void clear(); - - /// Irreversibly clears history for the current session. - void clear_session(); - - /// Populates from older location (in config path, rather than data path). - void populate_from_config_path(); - - /// Populates from a bash history file. - void populate_from_bash(FILE *f); - - /// Incorporates the history of other shells into this history. - void incorporate_external_changes(); - - /// Gets all the history into a list. This is intended for the $history environment variable. - /// This may be long! - void get_history(std::vector &result); - - /// Let indexes be a list of one-based indexes into the history, matching the interpretation of - /// $history. That is, $history[1] is the most recently executed command. Values less than one - /// are skipped. Return a mapping from index to history item text. - std::unordered_map items_at_indexes(const std::vector &idxs); - - /// Return the specified history at the specified index. 0 is the index of the current - /// commandline. (So the most recent item is at index 1.) - history_item_t item_at_index(size_t idx); - - /// Return the number of history entries. - size_t size(); -}; +using history_item_t = HistoryItem; +using history_persistence_mode_t = PersistenceMode; +using history_search_direction_t = SearchDirection; +using history_search_type_t = SearchType; +using history_search_flags_t = uint32_t; /// Flags for history searching. enum { @@ -236,113 +54,4 @@ enum { }; using history_search_flags_t = uint32_t; -/// Support for searching a history backwards. -/// Note this does NOT de-duplicate; it is the caller's responsibility to do so. -class history_search_t { - private: - /// The history in which we are searching. - /// TODO: this should be a shared_ptr. - history_t *history_; - - /// The original search term. - wcstring orig_term_; - - /// The (possibly lowercased) search term. - wcstring canon_term_; - - /// Our search type. - enum history_search_type_t search_type_ { history_search_type_t::contains }; - - /// Our flags. - history_search_flags_t flags_{0}; - - /// The current history item. - maybe_t current_item_; - - /// Index of the current history item. - size_t current_index_{0}; - - /// If deduping, the items we've seen. - std::unordered_set deduper_; - - /// return whether we deduplicate items. - bool dedup() const { return !(flags_ & history_search_no_dedup); } - - public: - /// Gets the original search term. - const wcstring &original_term() const { return orig_term_; } - - // Finds the next search result. Returns true if one was found. - bool go_to_next_match(history_search_direction_t direction); - - /// Returns the current search result item. asserts if there is no current item. - const history_item_t ¤t_item() const; - - /// Returns the current search result item contents. asserts if there is no current item. - const wcstring ¤t_string() const; - - /// Returns the index of the current history item. - size_t current_index() const; - - /// return whether we are case insensitive. - bool ignores_case() const { return flags_ & history_search_ignore_case; } - - /// Construct from a history pointer; the caller is responsible for ensuring the history stays - /// alive. - history_search_t(history_t *hist, const wcstring &str, - enum history_search_type_t type = history_search_type_t::contains, - history_search_flags_t flags = 0, size_t starting_index = 0) - : history_(hist), - orig_term_(str), - canon_term_(str), - search_type_(type), - flags_(flags), - current_index_(starting_index) { - if (ignores_case()) { - std::transform(canon_term_.begin(), canon_term_.end(), canon_term_.begin(), towlower); - } - } - - /// Construct from a shared_ptr. TODO: this should be the only constructor. - history_search_t(const std::shared_ptr &hist, const wcstring &str, - enum history_search_type_t type = history_search_type_t::contains, - history_search_flags_t flags = 0, size_t starting_index = 0) - : history_search_t(hist.get(), str, type, flags, starting_index) {} - - /** Default constructor. */ - history_search_t() = default; -}; - -/** Saves the new history to disk. */ -void history_save_all(); - -#if INCLUDE_RUST_HEADERS -/** Return the prefix for the files to be used for command and read history. */ -wcstring history_session_id(const environment_t &vars); -#endif - -/** FFI version of above **/ -class env_var_t; -wcstring history_session_id(std::unique_ptr fish_history); - -/** - Given a list of proposed paths and a context, perform variable and home directory expansion, - and detect if the result expands to a value which is also the path to a file. - Wildcard expansions are suppressed - see implementation comments for why. - This is used for autosuggestion hinting. If we add an item to history, and one of its arguments - refers to a file, then we only want to suggest it if there is a valid file there. - This does disk I/O and may only be called in a background thread. -*/ -path_list_t expand_and_detect_paths(const path_list_t &paths, const environment_t &vars); - -/** - Given a list of proposed paths and a context, expand each one and see if it refers to a file. - Wildcard expansions are suppressed. - \return true if \p paths is empty or every path is valid. -*/ -bool all_paths_are_valid(const path_list_t &paths, const operation_context_t &ctx); - -/** Queries private mode status. */ -bool in_private_mode(const environment_t &vars); - #endif diff --git a/src/history_file.cpp b/src/history_file.cpp deleted file mode 100644 index 2b06a5368..000000000 --- a/src/history_file.cpp +++ /dev/null @@ -1,604 +0,0 @@ -#include "config.h" // IWYU pragma: keep - -#include "history_file.h" - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include "common.h" -#include "history.h" -#include "path.h" -#include "wutil.h" - -// Some forward declarations. -static history_item_t decode_item_fish_2_0(const char *base, size_t len); -static history_item_t decode_item_fish_1_x(const char *begin, size_t length); - -static maybe_t offset_of_next_item_fish_2_0(const history_file_contents_t &contents, - size_t *inout_cursor, time_t cutoff_timestamp); -static maybe_t offset_of_next_item_fish_1_x(const char *begin, size_t mmap_length, - size_t *inout_cursor); - -// Check if we should mmap the fd. -// Don't try mmap() on non-local filesystems. -static bool should_mmap() { - if (history_t::never_mmap) return false; - - // mmap only if we are known not-remote. - return path_get_config_remoteness() == dir_remoteness_t::local; -} - -// Read up to len bytes from fd into address, zeroing the rest. -// Return true on success, false on failure. -static bool read_from_fd(int fd, void *address, size_t len) { - size_t remaining = len; - auto ptr = static_cast(address); - while (remaining > 0) { - ssize_t amt = read(fd, ptr, remaining); - if (amt < 0) { - if (errno != EINTR) { - return false; - } - } else if (amt == 0) { - break; - } else { - remaining -= amt; - ptr += amt; - } - } - std::memset(ptr, 0, remaining); - return true; -} - -static void replace_all(std::string *str, const char *needle, const char *replacement) { - size_t needle_len = std::strlen(needle), replacement_len = std::strlen(replacement); - size_t offset = 0; - while ((offset = str->find(needle, offset)) != std::string::npos) { - str->replace(offset, needle_len, replacement); - offset += replacement_len; - } -} - -// Support for escaping and unescaping the nonstandard "yaml" format introduced in fish 2.0. -static void escape_yaml_fish_2_0(std::string *str) { - replace_all(str, "\\", "\\\\"); // replace one backslash with two - replace_all(str, "\n", "\\n"); // replace newline with backslash + literal n -} - -/// This function is called frequently, so it ought to be fast. -static void unescape_yaml_fish_2_0(std::string *str) { - size_t cursor = 0, size = str->size(); - while (cursor < size) { - // Operate on a const version of str, to avoid needless COWs that at() does. - const std::string &const_str = *str; - - // Look for a backslash. - size_t backslash = const_str.find('\\', cursor); - if (backslash == std::string::npos || backslash + 1 >= size) { - // Either not found, or found as the last character. - break; - } else { - // Backslash found. Maybe we'll do something about it. Be sure to invoke the const - // version of at(). - char escaped_char = const_str.at(backslash + 1); - if (escaped_char == '\\') { - // Two backslashes in a row. Delete the second one. - str->erase(backslash + 1, 1); - size--; - } else if (escaped_char == 'n') { - // Backslash + n. Replace with a newline. - str->replace(backslash, 2, "\n"); - size--; - } - // The character at index backslash has now been made whole; start at the next - // character. - cursor = backslash + 1; - } - } -} - -// A type wrapping up a region allocated via mmap(). -struct history_file_contents_t::mmap_region_t : noncopyable_t, nonmovable_t { - void *const ptr; - const size_t len; - - mmap_region_t(void *ptr, size_t len) : ptr(ptr), len(len) { - assert(ptr != MAP_FAILED && len > 0 && "Invalid params"); - } - - ~mmap_region_t() { (void)munmap(ptr, len); } - - /// Map a region [0, len) from an fd. - /// \return nullptr on failure. - static std::unique_ptr map_file(int fd, size_t len) { - if (len == 0) return nullptr; - void *ptr = mmap(nullptr, size_t(len), PROT_READ, MAP_PRIVATE, fd, 0); - if (ptr == MAP_FAILED) return nullptr; - return make_unique(ptr, len); - } - - /// Map anonymous memory of a given length. - /// \return nullptr on failure. - static std::unique_ptr map_anon(size_t len) { - if (len == 0) return nullptr; - void *ptr = -#ifdef MAP_ANON - mmap(nullptr, size_t(len), PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); -#else - mmap(0, size_t(len), PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); -#endif - if (ptr == MAP_FAILED) return nullptr; - return make_unique(ptr, len); - } -}; - -history_file_contents_t::~history_file_contents_t() = default; - -history_file_contents_t::history_file_contents_t(std::unique_ptr region) - : region_(std::move(region)), start_(static_cast(region_->ptr)), length_(region_->len) { - assert(region_ && start_ && length_ > 0 && "Invalid params"); -} - -history_file_contents_t::history_file_contents_t(const char *start, size_t length) - : start_(start), length_(length) { - // Construct from explicit data, not backed by a file. This is used in tests. -} - -/// Try to infer the history file type based on inspecting the data. -bool history_file_contents_t::infer_file_type() { - assert(length_ > 0 && "File should never be empty"); - if (start_[0] == '#') { - this->type_ = history_type_fish_1_x; - } else { // assume new fish - this->type_ = history_type_fish_2_0; - } - return true; -} - -std::unique_ptr history_file_contents_t::create(int fd) { - // Check that the file is seekable, and its size. - off_t len = lseek(fd, 0, SEEK_END); - if (len <= 0 || static_cast(len) >= static_cast(SIZE_MAX)) return nullptr; - - bool mmap_file_directly = should_mmap(); - std::unique_ptr region = - mmap_file_directly ? mmap_region_t::map_file(fd, len) : mmap_region_t::map_anon(len); - if (!region) return nullptr; - - // If we mapped anonymous memory, we have to read from the file. - if (!mmap_file_directly) { - if (lseek(fd, 0, SEEK_SET) != 0) return nullptr; - if (!read_from_fd(fd, region->ptr, region->len)) return nullptr; - } - - std::unique_ptr result(new history_file_contents_t(std::move(region))); - - // Check the file type. - if (!result->infer_file_type()) return nullptr; - return result; -} - -history_item_t history_file_contents_t::decode_item(size_t offset) const { - const char *base = address_at(offset); - size_t len = this->length() - offset; - switch (this->type()) { - case history_type_fish_2_0: - return decode_item_fish_2_0(base, len); - case history_type_fish_1_x: - return decode_item_fish_1_x(base, len); - } - return history_item_t{}; -} - -maybe_t history_file_contents_t::offset_of_next_item(size_t *cursor, time_t cutoff) const { - switch (this->type()) { - case history_type_fish_2_0: - return offset_of_next_item_fish_2_0(*this, cursor, cutoff); - case history_type_fish_1_x: - return offset_of_next_item_fish_1_x(this->begin(), this->length(), cursor); - } - return none(); -} - -/// Read one line, stripping off any newline, and updating cursor. Note that our input string is NOT -/// null terminated; it's just a memory mapped file. -static size_t read_line(const char *base, size_t cursor, size_t len, std::string &result) { - // Locate the newline. - assert(cursor <= len); - const char *start = base + cursor; - auto a_newline = static_cast(std::memchr(start, '\n', len - cursor)); - if (a_newline != nullptr) { // we found a newline - result.assign(start, a_newline - start); - // Return the amount to advance the cursor; skip over the newline. - return a_newline - start + 1; - } - - // We ran off the end. - result.clear(); - return len - cursor; -} - -/// Trims leading spaces in the given string, returning how many there were. -static size_t trim_leading_spaces(std::string &str) { - size_t i = 0, max = str.size(); - while (i < max && str[i] == ' ') i++; - str.erase(0, i); - return i; -} - -static bool extract_prefix_and_unescape_yaml(std::string *key, std::string *value, - const std::string &line) { - size_t where = line.find(':'); - if (where != std::string::npos) { - key->assign(line, 0, where); - - // Skip a space after the : if necessary. - size_t val_start = where + 1; - if (val_start < line.size() && line.at(val_start) == ' ') val_start++; - value->assign(line, val_start, line.size() - val_start); - - unescape_yaml_fish_2_0(key); - unescape_yaml_fish_2_0(value); - } - return where != std::string::npos; -} - -/// Decode an item via the fish 2.0 format. -static history_item_t decode_item_fish_2_0(const char *base, size_t len) { - wcstring cmd; - time_t when = 0; - path_list_t paths; - - size_t indent = 0, cursor = 0; - std::string key, value, line; - - // Read the "- cmd:" line. - size_t advance = read_line(base, cursor, len, line); - trim_leading_spaces(line); - if (!extract_prefix_and_unescape_yaml(&key, &value, line) || key != "- cmd") { - goto done; //!OCLINT(goto is the cleanest way to handle bad input) - } - - cursor += advance; - cmd = str2wcstring(value); - - // Read the remaining lines. - for (;;) { - size_t advance = read_line(base, cursor, len, line); - - size_t this_indent = trim_leading_spaces(line); - if (indent == 0) indent = this_indent; - - if (this_indent == 0 || indent != this_indent) break; - - if (!extract_prefix_and_unescape_yaml(&key, &value, line)) break; - - // We are definitely going to consume this line. - cursor += advance; - - if (key == "when") { - // Parse an int from the timestamp. Should this fail, strtol returns 0; that's - // acceptable. - char *end = nullptr; - long tmp = strtol(value.c_str(), &end, 0); - when = tmp; - } else if (key == "paths") { - // Read lines starting with " - " until we can't read any more. - for (;;) { - size_t advance = read_line(base, cursor, len, line); - if (trim_leading_spaces(line) <= indent) break; - - if (std::strncmp(line.c_str(), "- ", 2) != 0) break; - - // We're going to consume this line. - cursor += advance; - - // Skip the leading dash-space and then store this path it. - line.erase(0, 2); - unescape_yaml_fish_2_0(&line); - paths.push_back(str2wcstring(line)); - } - } - } - -done: - history_item_t result(cmd, when); - result.set_required_paths(std::move(paths)); - return result; -} - -/// Parse a timestamp line that looks like this: spaces, "when:", spaces, timestamp, newline -/// The string is NOT null terminated; however we do know it contains a newline, so stop when we -/// reach it. -static bool parse_timestamp(const char *str, time_t *out_when) { - const char *cursor = str; - // Advance past spaces. - while (*cursor == ' ') cursor++; - - // Look for "when:". - size_t when_len = 5; - if (std::strncmp(cursor, "when:", when_len) != 0) return false; - cursor += when_len; - - // Advance past spaces. - while (*cursor == ' ') cursor++; - - // Try to parse a timestamp. - long timestamp = 0; - if (isdigit(*cursor) && (timestamp = strtol(cursor, nullptr, 0)) > 0) { - *out_when = static_cast(timestamp); - return true; - } - return false; -} - -/// Returns a pointer to the start of the next line, or NULL. The next line must itself end with a -/// newline. Note that the string is not null terminated. -static const char *next_line(const char *start, const char *end) { - // Handle the hopeless case. - if (end == start) return nullptr; - - // Skip past the next newline. - const char *nextline = std::find(start, end, '\n'); - if (nextline == end) { - return nullptr; - } - - // Skip past the newline character itself. - if (++nextline >= end) { - return nullptr; - } - - // Make sure this new line is itself "newline terminated". If it's not, return NULL. - const char *next_newline = std::find(nextline, end, '\n'); - if (next_newline == end) { - return nullptr; - } - - return nextline; -} - -/// Support for iteratively locating the offsets of history items. -/// Pass the file contents and a pointer to a cursor size_t, initially 0. -/// If custoff_timestamp is nonzero, skip items created at or after that timestamp. -/// Returns (size_t)-1 when done. -static maybe_t offset_of_next_item_fish_2_0(const history_file_contents_t &contents, - size_t *inout_cursor, time_t cutoff_timestamp) { - size_t cursor = *inout_cursor; - const size_t length = contents.length(); - const char *const begin = contents.begin(); - const char *const end = contents.end(); - while (cursor < length) { - const char *line_start = contents.address_at(cursor); - - // Advance the cursor to the next line. - auto a_newline = static_cast(std::memchr(line_start, '\n', length - cursor)); - if (a_newline == nullptr) break; - - // Advance the cursor past this line. +1 is for the newline. - cursor = a_newline - begin + 1; - - // Skip lines with a leading space, since these are in the interior of one of our items. - if (line_start[0] == ' ') continue; - - // Skip very short lines to make one of the checks below easier. - if (a_newline - line_start < 3) continue; - - // Try to be a little YAML compatible. Skip lines with leading %, ---, or ... - if (!std::memcmp(line_start, "%", 1) || !std::memcmp(line_start, "---", 3) || - !std::memcmp(line_start, "...", 3)) - continue; - - // Hackish: fish 1.x rewriting a fish 2.0 history file can produce lines with lots of - // leading "- cmd: - cmd: - cmd:". Trim all but one leading "- cmd:". - constexpr const char double_cmd[] = "- cmd: - cmd: "; - constexpr const size_t double_cmd_len = const_strlen(double_cmd); - while (static_cast(a_newline - line_start) > double_cmd_len && - !std::memcmp(line_start, double_cmd, double_cmd_len)) { - // Skip over just one of the - cmd. In the end there will be just one left. - line_start += const_strlen("- cmd: "); - } - - // Hackish: fish 1.x rewriting a fish 2.0 history file can produce commands like "when: - // 123456". Ignore those. - constexpr const char cmd_when[] = "- cmd: when:"; - constexpr const size_t cmd_when_len = const_strlen(cmd_when); - if (static_cast(a_newline - line_start) >= cmd_when_len && - !std::memcmp(line_start, cmd_when, cmd_when_len)) { - continue; - } - - // At this point, we know line_start is at the beginning of an item. But maybe we want to - // skip this item because of timestamps. A 0 cutoff means we don't care; if we do care, then - // try parsing out a timestamp. - if (cutoff_timestamp != 0) { - // Hackish fast way to skip items created after our timestamp. This is the mechanism by - // which we avoid "seeing" commands from other sessions that started after we started. - // We try hard to ensure that our items are sorted by their timestamps, so in theory we - // could just break, but I don't think that works well if (for example) the clock - // changes. So we'll read all subsequent items. - // Walk over lines that we think are interior. These lines are not null terminated, but - // are guaranteed to contain a newline. - bool has_timestamp = false; - time_t timestamp = 0; - const char *interior_line; - - for (interior_line = next_line(line_start, end); - interior_line != nullptr && !has_timestamp; - interior_line = next_line(interior_line, end)) { - // If the first character is not a space, it's not an interior line, so we're done. - if (interior_line[0] != ' ') break; - - // Hackish optimization: since we just stepped over some interior line, update the - // cursor so we don't have to look at these lines next time. - cursor = interior_line - begin; - - // Try parsing a timestamp from this line. If we succeed, the loop will break. - has_timestamp = parse_timestamp(interior_line, ×tamp); - } - - // Skip this item if the timestamp is past our cutoff. - if (has_timestamp && timestamp > cutoff_timestamp) { - continue; - } - } - - // We made it through the gauntlet. - *inout_cursor = cursor; - return line_start - begin; - } - return none(); -} - -void append_history_item_to_buffer(const history_item_t &item, std::string *buffer) { - assert(item.should_write_to_disk() && "Item should not be persisted"); - auto append = [=](const char *a, const char *b = nullptr, const char *c = nullptr) { - if (a) buffer->append(a); - if (b) buffer->append(b); - if (c) buffer->append(c); - }; - - std::string cmd = wcs2string(item.str()); - escape_yaml_fish_2_0(&cmd); - append("- cmd: ", cmd.c_str(), "\n"); - append(" when: ", std::to_string(item.timestamp()).c_str(), "\n"); - const path_list_t &paths = item.get_required_paths(); - if (!paths.empty()) { - append(" paths:\n"); - - for (const auto &wpath : paths) { - std::string path = wcs2string(wpath); - escape_yaml_fish_2_0(&path); - append(" - ", path.c_str(), "\n"); - } - } -} - -/// Remove backslashes from all newlines. This makes a string from the history file better formated -/// for on screen display. -static wcstring history_unescape_newlines_fish_1_x(const wcstring &in_str) { - wcstring out; - for (const wchar_t *in = in_str.c_str(); *in; in++) { - if (*in == L'\\') { - if (*(in + 1) != L'\n') { - out.push_back(*in); - } - } else { - out.push_back(*in); - } - } - return out; -} - -/// Decode an item via the fish 1.x format. Adapted from fish 1.x's item_get(). -static history_item_t decode_item_fish_1_x(const char *begin, size_t length) { - const char *end = begin + length; - const char *pos = begin; - wcstring out; - bool was_backslash = false; - bool first_char = true; - bool timestamp_mode = false; - time_t timestamp = 0; - - while (true) { - wchar_t c; - size_t res; - mbstate_t state = {}; - - if (MB_CUR_MAX == 1) { // single-byte locale - c = static_cast(*pos); - res = 1; - } else { - res = std::mbrtowc(&c, pos, end - pos, &state); - } - - if (res == static_cast(-1)) { - pos++; - continue; - } else if (res == static_cast(-2)) { - break; - } else if (res == static_cast(0)) { - pos++; - continue; - } - pos += res; - - if (c == L'\n') { - if (timestamp_mode) { - const wchar_t *time_string = out.c_str(); - while (*time_string && !iswdigit(*time_string)) time_string++; - - if (*time_string) { - auto tm = static_cast(fish_wcstol(time_string)); - if (!errno && tm >= 0) { - timestamp = tm; - } - } - - out.clear(); - timestamp_mode = false; - continue; - } - if (!was_backslash) break; - } - - if (first_char) { - first_char = false; - if (c == L'#') timestamp_mode = true; - } - - out.push_back(c); - was_backslash = (c == L'\\') && !was_backslash; - } - - out = history_unescape_newlines_fish_1_x(out); - return history_item_t(out, timestamp); -} - -/// Same as offset_of_next_item_fish_2_0, but for fish 1.x (pre fishfish). -/// Adapted from history_populate_from_mmap in history.c -static maybe_t offset_of_next_item_fish_1_x(const char *begin, size_t mmap_length, - size_t *inout_cursor) { - if (mmap_length == 0 || *inout_cursor >= mmap_length) return none(); - - const char *end = begin + mmap_length; - const char *pos; - bool ignore_newline = false; - bool do_push = true; - bool all_done = false; - size_t result = *inout_cursor; - - for (pos = begin + *inout_cursor; pos < end && !all_done; pos++) { - if (do_push) { - ignore_newline = (*pos == '#'); - do_push = false; - } - - if (*pos == '\\') { - pos++; - } else if (*pos == '\n') { - if (!ignore_newline) { - // pos will be left pointing just after this newline, because of the ++ in the loop. - all_done = true; - } - ignore_newline = false; - } - } - - if (pos == end && !all_done) { - // No trailing newline, treat this item as incomplete and ignore it. - return none(); - } - - *inout_cursor = (pos - begin); - return result; -} diff --git a/src/history_file.h b/src/history_file.h deleted file mode 100644 index 2c05a4110..000000000 --- a/src/history_file.h +++ /dev/null @@ -1,82 +0,0 @@ -#ifndef FISH_HISTORY_FILE_H -#define FISH_HISTORY_FILE_H - -#include "config.h" // IWYU pragma: keep - -#include - -#include -#include -#include - -#include "common.h" -#include "maybe.h" - -class history_item_t; - -// History file types. -enum history_file_type_t { history_type_fish_2_0, history_type_fish_1_x }; - -/// history_file_contents_t holds the read-only contents of a file. -class history_file_contents_t : noncopyable_t, nonmovable_t { - public: - /// Construct a history file contents from a file descriptor. The file descriptor is not closed. - static std::unique_ptr create(int fd); - - /// Decode an item at a given offset. - history_item_t decode_item(size_t offset) const; - - /// Support for iterating item offsets. - /// The cursor should initially be 0. - /// If cutoff is nonzero, skip items whose timestamp is newer than cutoff. - /// \return the offset of the next item, or none() on end. - maybe_t offset_of_next_item(size_t *cursor, time_t cutoff) const; - - /// Get the file type. - history_file_type_t type() const { return type_; } - - /// Get the size of the contents. - size_t length() const { return length_; } - - /// Return a pointer to the beginning. - const char *begin() const { return address_at(0); } - - /// Return a pointer to one-past-the-end. - const char *end() const { return address_at(length_); } - - /// Access the address at a given offset. - const char *address_at(size_t offset) const { - assert(offset <= length_ && "Invalid offset"); - return start_ + offset; - } - - ~history_file_contents_t(); - - private: - // A type wrapping up the logic around mmap and munmap. - struct mmap_region_t; - const std::unique_ptr region_; - - // The memory mapped pointer and length. - // The ptr aliases our region. The length may be slightly smaller, if there is a trailing - // incomplete history item. - const char *const start_; - const size_t length_; - - // The type of the mapped file. - // This is set at construction and not changed after. - history_file_type_t type_{}; - - // Private constructors; use the static create() function. - explicit history_file_contents_t(std::unique_ptr region); - history_file_contents_t(const char *start, size_t length); - - // Try to infer the file type to populate type_. - // \return true on success, false on error. - bool infer_file_type(); -}; - -/// Append a history item to a buffer, in preparation for outputting it to the history file. -void append_history_item_to_buffer(const history_item_t &item, std::string *buffer); - -#endif diff --git a/src/input.cpp b/src/input.cpp index 14f65d243..c995e0cd3 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -206,13 +206,13 @@ static wcstring input_get_bind_mode(const environment_t &vars) { } /// Set the current bind mode. -static void input_set_bind_mode(parser_t &parser, const wcstring &bm) { +static void input_set_bind_mode(const parser_t &parser, const wcstring &bm) { // Only set this if it differs to not execute variable handlers all the time. // modes may not be empty - empty is a sentinel value meaning to not change the mode assert(!bm.empty()); - if (input_get_bind_mode(parser.vars()) != bm) { + if (input_get_bind_mode(env_stack_t{parser.vars_boxed()}) != bm) { // Must send events here - see #6653. - parser.set_var_and_fire(FISH_BIND_MODE_VAR, ENV_GLOBAL, bm); + parser.set_var_and_fire(FISH_BIND_MODE_VAR, ENV_GLOBAL, wcstring_list_ffi_t{{bm}}); } } @@ -321,12 +321,12 @@ void init_input() { } } -inputter_t::inputter_t(parser_t &parser, int in) +inputter_t::inputter_t(const parser_t &parser, int in) : input_event_queue_t(in), parser_(parser.shared()) {} void inputter_t::prepare_to_select() /* override */ { // Fire any pending events and reap stray processes, including printing exit status messages. - auto &parser = *this->parser_; + auto &parser = this->parser_->deref(); event_fire_delayed(parser); if (job_reap(parser, true)) reader_schedule_prompt_repaint(); } @@ -337,7 +337,7 @@ void inputter_t::select_interrupted() /* override */ { signal_clear_cancel(); // Fire any pending events and reap stray processes, including printing exit status messages. - auto &parser = *this->parser_; + auto &parser = this->parser_->deref(); event_fire_delayed(parser); if (job_reap(parser, true)) reader_schedule_prompt_repaint(); @@ -353,7 +353,7 @@ void inputter_t::select_interrupted() /* override */ { } void inputter_t::uvar_change_notified() /* override */ { - this->parser_->sync_uvars_and_fire(true /* always */); + this->parser_->deref().sync_uvars_and_fire(true /* always */); } void inputter_t::function_push_arg(wchar_t arg) { input_function_args_.push_back(arg); } @@ -411,7 +411,7 @@ void inputter_t::mapping_execute(const input_mapping_t &m, // !has_functions && !has_commands: only set bind mode if (!has_commands && !has_functions) { - if (!m.sets_mode.empty()) input_set_bind_mode(*parser_, m.sets_mode); + if (!m.sets_mode.empty()) input_set_bind_mode(parser_->deref(), m.sets_mode); return; } @@ -441,7 +441,7 @@ void inputter_t::mapping_execute(const input_mapping_t &m, } // Empty bind mode indicates to not reset the mode (#2871) - if (!m.sets_mode.empty()) input_set_bind_mode(*parser_, m.sets_mode); + if (!m.sets_mode.empty()) input_set_bind_mode(parser_->deref(), m.sets_mode); } void inputter_t::queue_char(const char_event_t &ch) { @@ -617,7 +617,7 @@ static bool try_peek_sequence(event_queue_peeker_t *peeker, const wcstring &str) /// interrupted by a readline event. maybe_t inputter_t::find_mapping(event_queue_peeker_t *peeker) { const input_mapping_t *generic = nullptr; - const auto &vars = parser_->vars(); + env_stack_t vars{parser_->deref().vars_boxed()}; const wcstring bind_mode = input_get_bind_mode(vars); const input_mapping_t *escape = nullptr; diff --git a/src/input.h b/src/input.h index a0826ce8d..a89367134 100644 --- a/src/input.h +++ b/src/input.h @@ -13,12 +13,12 @@ #include "common.h" #include "input_common.h" #include "maybe.h" +#include "parser.h" #define FISH_BIND_MODE_VAR L"fish_bind_mode" #define DEFAULT_BIND_MODE L"default" class event_queue_peeker_t; -class Parser; using parser_t = Parser; wcstring describe_char(wint_t c); @@ -30,7 +30,7 @@ struct input_mapping_t; class inputter_t final : private input_event_queue_t { public: /// Construct from a parser, and the fd from which to read. - explicit inputter_t(parser_t &parser, int in = STDIN_FILENO); + explicit inputter_t(const parser_t &parser, int in = STDIN_FILENO); /// Read a character from stdin. Try to convert some escape sequences into character constants, /// but do not permanently block the escape character. @@ -74,8 +74,10 @@ class inputter_t final : private input_event_queue_t { maybe_t find_mapping(event_queue_peeker_t *peeker); char_event_t read_characters_no_readline(); +#if INCLUDE_RUST_HEADERS // We need a parser to evaluate bindings. - const std::shared_ptr parser_; + const rust::Box parser_; +#endif std::vector input_function_args_{}; bool function_status_{false}; diff --git a/src/input_common.cpp b/src/input_common.cpp index 7e2a40f59..27094ce71 100644 --- a/src/input_common.cpp +++ b/src/input_common.cpp @@ -143,26 +143,25 @@ void update_wait_on_escape_ms(const environment_t& vars) { } } -void update_wait_on_escape_ms_ffi(std::unique_ptr fish_escape_delay_ms) { - if (!fish_escape_delay_ms) { +void update_wait_on_escape_ms_ffi(bool empty, const wcstring& fish_escape_delay_ms) { + if (empty) { wait_on_escape_ms = WAIT_ON_ESCAPE_DEFAULT; return; } - long tmp = fish_wcstol(fish_escape_delay_ms->as_string().c_str()); + long tmp = fish_wcstol(fish_escape_delay_ms.c_str()); if (errno || tmp < 10 || tmp >= 5000) { std::fwprintf(stderr, L"ignoring fish_escape_delay_ms: value '%ls' " L"is not an integer or is < 10 or >= 5000 ms\n", - fish_escape_delay_ms->as_string().c_str()); + fish_escape_delay_ms.c_str()); } else { wait_on_escape_ms = static_cast(tmp); } } - -// Update the wait_on_sequence_key_ms value in response to the fish_sequence_key_delay_ms user variable being -// set. +// Update the wait_on_sequence_key_ms value in response to the fish_sequence_key_delay_ms user +// variable being set. void update_wait_on_sequence_key_ms(const environment_t& vars) { auto sequence_key_time_ms = vars.get_unless_empty(L"fish_sequence_key_delay_ms"); if (!sequence_key_time_ms) { @@ -181,18 +180,18 @@ void update_wait_on_sequence_key_ms(const environment_t& vars) { } } -void update_wait_on_sequence_key_ms_ffi(std::unique_ptr fish_sequence_key_delay_ms) { - if (!fish_sequence_key_delay_ms) { +void update_wait_on_sequence_key_ms_ffi(bool empty, const wcstring& fish_sequence_key_delay_ms) { + if (empty) { wait_on_sequence_key_ms = WAIT_ON_SEQUENCE_KEY_INFINITE; return; } - long tmp = fish_wcstol(fish_sequence_key_delay_ms->as_string().c_str()); + long tmp = fish_wcstol(fish_sequence_key_delay_ms.c_str()); if (errno || tmp < 10 || tmp >= 5000) { std::fwprintf(stderr, L"ignoring fish_sequence_key_delay_ms: value '%ls' " L"is not an integer or is < 10 or >= 5000 ms\n", - fish_sequence_key_delay_ms->as_string().c_str()); + fish_sequence_key_delay_ms.c_str()); } else { wait_on_sequence_key_ms = static_cast(tmp); } diff --git a/src/input_common.h b/src/input_common.h index 9e89a08cc..14ca14a58 100644 --- a/src/input_common.h +++ b/src/input_common.h @@ -10,6 +10,7 @@ #include #include "common.h" +#include "env.h" #include "maybe.h" enum class readline_cmd_t { @@ -187,12 +188,11 @@ class char_event_t { }; /// Adjust the escape timeout. -class environment_t; void update_wait_on_escape_ms(const environment_t &vars); -void update_wait_on_escape_ms_ffi(std::unique_ptr fish_escape_delay_ms); +void update_wait_on_escape_ms_ffi(bool empty, const wcstring &fish_escape_delay_ms); void update_wait_on_sequence_key_ms(const environment_t &vars); -void update_wait_on_sequence_key_ms_ffi(std::unique_ptr fish_sequence_key_delay_ms); +void update_wait_on_sequence_key_ms_ffi(bool empty, const wcstring &fish_sequence_key_delay_ms); /// A class which knows how to produce a stream of input events. /// This is a base class; you may subclass it for its override points. diff --git a/src/io.cpp b/src/io.cpp deleted file mode 100644 index 56fd07b89..000000000 --- a/src/io.cpp +++ /dev/null @@ -1,450 +0,0 @@ -// Utilities for io redirection. -#include "config.h" // IWYU pragma: keep - -#include "io.h" - -#include -#include -#include -#include -#include - -#include -#include - -#include "common.h" -#include "fallback.h" // IWYU pragma: keep -#include "fd_monitor.rs.h" -#include "fds.h" -#include "fds.rs.h" -#include "flog.h" -#include "maybe.h" -#include "path.h" -#include "redirection.h" -#include "threads.rs.h" -#include "wutil.h" // IWYU pragma: keep - -/// File redirection error message. -#define FILE_ERROR _(L"An error occurred while redirecting file '%ls'") -#define NOCLOB_ERROR _(L"The file '%ls' already exists") - -/// Base open mode to pass to calls to open. -#define OPEN_MASK 0666 - -/// Provide the fd monitor used for background fillthread operations. -static fd_monitor_t &fd_monitor() { - // Deliberately leaked to avoid shutdown dtors. - static auto fdm = make_fd_monitor_t(); - return *fdm; -} - -io_data_t::~io_data_t() = default; -io_pipe_t::~io_pipe_t() = default; -io_fd_t::~io_fd_t() = default; -io_close_t::~io_close_t() = default; -io_file_t::~io_file_t() = default; -io_bufferfill_t::~io_bufferfill_t() = default; - -void io_close_t::print() const { std::fwprintf(stderr, L"close %d\n", fd); } - -void io_fd_t::print() const { std::fwprintf(stderr, L"FD map %d -> %d\n", source_fd, fd); } - -void io_file_t::print() const { std::fwprintf(stderr, L"file %d -> %d\n", file_fd_.fd(), fd); } - -void io_pipe_t::print() const { - std::fwprintf(stderr, L"pipe {%d} (input: %s) -> %d\n", source_fd, is_input_ ? "yes" : "no", - fd); -} - -void io_bufferfill_t::print() const { - std::fwprintf(stderr, L"bufferfill %d -> %d\n", write_fd_.fd(), fd); -} - -ssize_t io_buffer_t::read_once(int fd, acquired_lock &buffer) { - assert(fd >= 0 && "Invalid fd"); - errno = 0; - char bytes[4096 * 4]; - - // We want to swallow EINTR only; in particular EAGAIN needs to be returned back to the caller. - ssize_t amt; - do { - amt = read(fd, bytes, sizeof bytes); - } while (amt < 0 && errno == EINTR); - if (amt < 0 && errno != EAGAIN && errno != EWOULDBLOCK) { - wperror(L"read"); - } else if (amt > 0) { - buffer->append(bytes, static_cast(amt)); - } - return amt; -} - -struct callback_args_t { - io_buffer_t *instance; - std::shared_ptr> promise; -}; - -extern "C" { -static void item_callback_trampoline(autoclose_fd_t2 &fd, item_wake_reason_t reason, - callback_args_t *args) { - (args->instance)->item_callback(fd, (uint8_t)reason, args); -} -} - -void io_buffer_t::begin_filling(autoclose_fd_t fd) { - assert(!fillthread_running() && "Already have a fillthread"); - - // We want to fill buffer_ by reading from fd. fd is the read end of a pipe; the write end is - // owned by another process, or something else writing in fish. - // Pass fd to an fd_monitor. It will add fd to its select() loop, and give us a callback when - // the fd is readable, or when our item is poked. The usual path is that we will get called - // back, read a bit from the fd, and append it to the buffer. Eventually the write end of the - // pipe will be closed - probably the other process exited - and fd will be widowed; read() will - // then return 0 and we will stop reading. - // In exotic circumstances the write end of the pipe will not be closed; this may happen in - // e.g.: - // cmd ( background & ; echo hi ) - // Here the background process will inherit the write end of the pipe and hold onto it forever. - // In this case, when complete_background_fillthread() is called, the callback will be invoked - // with item_wake_reason_t::poke, and we will notice that the shutdown flag is set (this - // indicates that the command substitution is done); in this case we will read until we get - // EAGAIN and then give up. - - // Construct a promise. We will fulfill it in our fill thread, and wait for it in - // complete_background_fillthread(). Note that TSan complains if the promise's dtor races with - // the future's call to wait(), so we store the promise, not just its future (#7681). - auto promise = std::make_shared>(); - this->fill_waiter_ = promise; - - // Run our function to read until the receiver is closed. - // It's OK to capture 'this' by value because 'this' waits for the promise in its dtor. - auto args = new callback_args_t; - args->instance = this; - args->promise = std::move(promise); - - item_id_ = fd_monitor().add_item(fd.acquire(), kNoTimeout, (uint8_t *)item_callback_trampoline, - (uint8_t *)args); -} - -/// This is a hack to work around the difficulties in passing a capturing lambda across FFI -/// boundaries. A static function that takes a generic/untyped callback parameter is easy to -/// marshall with the basic C ABI. -void io_buffer_t::item_callback(autoclose_fd_t2 &fd, uint8_t r, callback_args_t *args) { - item_wake_reason_t reason = (item_wake_reason_t)r; - auto &promise = *args->promise; - - // Only check the shutdown flag if we timed out or were poked. - // It's important that if select() indicated we were readable, that we call select() again - // allowing it to time out. Note the typical case is that the fd will be closed, in which - // case select will return immediately. - bool done = false; - if (reason == item_wake_reason_t::Readable) { - // select() reported us as readable; read a bit. - auto buffer = buffer_.acquire(); - ssize_t ret = read_once(fd.fd(), buffer); - done = (ret == 0 || (ret < 0 && errno != EAGAIN && errno != EWOULDBLOCK)); - } else if (shutdown_fillthread_) { - // Here our caller asked us to shut down; read while we keep getting data. - // This will stop when the fd is closed or if we get EAGAIN. - auto buffer = buffer_.acquire(); - ssize_t ret; - do { - ret = read_once(fd.fd(), buffer); - } while (ret > 0); - done = true; - } - if (done) { - fd.close(); - promise.set_value(); - // When we close the fd, we signal to the caller that the fd should be removed from its set - // and that this callback should never be called again. - // Manual memory management is not nice but this is just during the cpp-to-rust transition. - delete args; - } -}; - -separated_buffer_t io_buffer_t::complete_background_fillthread_and_take_buffer() { - // Mark that our fillthread is done, then wake it up. - assert(fillthread_running() && "Should have a fillthread"); - assert(this->item_id_ > 0 && "Should have a valid item ID"); - shutdown_fillthread_ = true; - fd_monitor().poke_item(this->item_id_); - - // Wait for the fillthread to fulfill its promise, and then clear the future so we know we no - // longer have one. - fill_waiter_->get_future().wait(); - fill_waiter_.reset(); - - // Return our buffer, transferring ownership. - auto locked_buff = buffer_.acquire(); - separated_buffer_t result = std::move(*locked_buff); - locked_buff->clear(); - return result; -} - -shared_ptr io_bufferfill_t::create(size_t buffer_limit, int target) { - assert(target >= 0 && "Invalid target fd"); - - // Construct our pipes. - auto pipes = make_autoclose_pipes(); - if (!pipes) { - return nullptr; - } - // Our buffer will read from the read end of the pipe. This end must be non-blocking. This is - // because our fillthread needs to poll to decide if it should shut down, and also accept input - // from direct buffer transfers. - if (make_fd_nonblocking(pipes->read.fd())) { - FLOGF(warning, PIPE_ERROR); - wperror(L"fcntl"); - return nullptr; - } - // Our fillthread gets the read end of the pipe; out_pipe gets the write end. - auto buffer = std::make_shared(buffer_limit); - buffer->begin_filling(std::move(pipes->read)); - return std::make_shared(target, std::move(pipes->write), buffer); -} - -separated_buffer_t io_bufferfill_t::finish(std::shared_ptr &&filler) { - // The io filler is passed in. This typically holds the only instance of the write side of the - // pipe used by the buffer's fillthread (except for that side held by other processes). Get the - // buffer out of the bufferfill and clear the shared_ptr; this will typically widow the pipe. - // Then allow the buffer to finish. - assert(filler && "Null pointer in finish"); - auto buffer = filler->buffer(); - filler.reset(); - return buffer->complete_background_fillthread_and_take_buffer(); -} - -io_buffer_t::~io_buffer_t() { - assert(!fillthread_running() && "io_buffer_t destroyed with outstanding fillthread"); -} - -void io_chain_t::remove(const shared_ptr &element) { - // See if you can guess why std::find doesn't work here. - for (auto iter = this->begin(); iter != this->end(); ++iter) { - if (*iter == element) { - this->erase(iter); - break; - } - } -} - -void io_chain_t::push_back(io_data_ref_t element) { - // Ensure we never push back NULL. - assert(element.get() != nullptr); - std::vector::push_back(std::move(element)); -} - -bool io_chain_t::append(const io_chain_t &chain) { - assert(&chain != this && "Cannot append self to self"); - this->insert(this->end(), chain.begin(), chain.end()); - return true; -} - -bool io_chain_t::append_from_specs(const redirection_spec_list_t &specs, const wcstring &pwd) { - bool have_error = false; - for (size_t i = 0; i < specs.size(); i++) { - const redirection_spec_t *spec = specs.at(i); - switch (spec->mode()) { - case redirection_mode_t::fd: { - if (spec->is_close()) { - this->push_back(make_unique(spec->fd())); - } else { - auto target_fd = spec->get_target_as_fd(); - assert(target_fd && "fd redirection should have been validated already"); - this->push_back(make_unique(spec->fd(), *target_fd)); - } - break; - } - default: { - // We have a path-based redireciton. Resolve it to a file. - // Mark it as CLO_EXEC because we don't want it to be open in any child. - wcstring path = path_apply_working_directory(*spec->target(), pwd); - int oflags = spec->oflags(); - autoclose_fd_t file{wopen_cloexec(path, oflags, OPEN_MASK)}; - if (!file.valid()) { - if ((oflags & O_EXCL) && (errno == EEXIST)) { - FLOGF(warning, NOCLOB_ERROR, spec->target()->c_str()); - } else { - if (should_flog(warning)) { - FLOGF(warning, FILE_ERROR, spec->target()->c_str()); - auto err = errno; - // If the error is that the file doesn't exist - // or there's a non-directory component, - // find the first problematic component for a better message. - if (err == ENOENT || err == ENOTDIR) { - auto dname = *spec->target(); - struct stat buf; - - while (!dname.empty()) { - auto next = wdirname(dname); - if (!wstat(next, &buf)) { - if (!S_ISDIR(buf.st_mode)) { - FLOGF(warning, _(L"Path '%ls' is not a directory"), - next.c_str()); - } else { - FLOGF(warning, _(L"Path '%ls' does not exist"), - dname.c_str()); - } - break; - } - dname = next; - } - } else { - wperror(L"open"); - } - } - } - // If opening a file fails, insert a closed FD instead of the file redirection - // and return false. This lets execution potentially recover and at least gives - // the shell a chance to gracefully regain control of the shell (see #7038). - this->push_back(make_unique(spec->fd())); - have_error = true; - break; - } - this->push_back(std::make_shared(spec->fd(), std::move(file))); - break; - } - } - } - return !have_error; -} - -void io_chain_t::print() const { - if (this->empty()) { - std::fwprintf(stderr, L"Empty chain %p\n", this); - return; - } - - std::fwprintf(stderr, L"Chain %p (%ld items):\n", this, static_cast(this->size())); - for (size_t i = 0; i < this->size(); i++) { - const auto &io = this->at(i); - if (io == nullptr) { - std::fwprintf(stderr, L"\t(null)\n"); - } else { - std::fwprintf(stderr, L"\t%lu: fd:%d, ", static_cast(i), io->fd); - io->print(); - } - } -} - -shared_ptr io_chain_t::io_for_fd(int fd) const { - for (auto iter = rbegin(); iter != rend(); ++iter) { - const auto &data = *iter; - if (data->fd == fd) { - return data; - } - } - return nullptr; -} - -dup2_list_t dup2_list_resolve_chain_shim(const io_chain_t &io_chain) { - ASSERT_IS_NOT_FORKED_CHILD(); - std::vector chain; - for (const auto &io_data : io_chain) { - chain.push_back(dup2_action_t{io_data->source_fd, io_data->fd}); - } - return dup2_list_resolve_chain(chain); -} - -bool output_stream_t::append_narrow_buffer(const separated_buffer_t &buffer) { - for (const auto &rhs_elem : buffer.elements()) { - if (!append_with_separation(str2wcstring(rhs_elem.contents), rhs_elem.separation, false)) { - return false; - } - } - return true; -} - -bool output_stream_t::append_with_separation(const wchar_t *s, size_t len, separation_type_t type, - bool want_newline) { - if (type == separation_type_t::explicitly && want_newline) { - // Try calling "append" less - it might write() to an fd - wcstring buf{s, len}; - buf.push_back(L'\n'); - return append(buf); - } else { - return append(s, len); - } -} - -const wcstring &output_stream_t::contents() const { return g_empty_string; } - -int output_stream_t::flush_and_check_error() { return STATUS_CMD_OK; } - -fd_output_stream_t::fd_output_stream_t(int fd) : fd_(fd), sigcheck_(topic_t::sighupint) { - assert(fd_ >= 0 && "Invalid fd"); -} - -bool fd_output_stream_t::append(const wchar_t *s, size_t amt) { - if (errored_) return false; - int res = wwrite_to_fd(s, amt, this->fd_); - if (res < 0) { - // Some of our builtins emit multiple screens worth of data sent to a pager (the primary - // example being the `history` builtin) and receiving SIGINT should be considered normal and - // non-exceptional (user request to abort via Ctrl-C), meaning we shouldn't print an error. - if (errno == EINTR && sigcheck_.check()) { - // We have two options here: we can either return false without setting errored_ to - // true (*this* write will be silently aborted but the onus is on the caller to check - // the return value and skip future calls to `append()`) or we can flag the entire - // output stream as errored, causing us to both return false and skip any future writes. - // We're currently going with the latter, especially seeing as no callers currently - // check the result of `append()` (since it was always a void function before). - } else if (errno != EPIPE) { - wperror(L"write"); - } - errored_ = true; - } - return !errored_; -} - -int fd_output_stream_t::flush_and_check_error() { - // Return a generic 1 on any write failure. - return errored_ ? STATUS_CMD_ERROR : STATUS_CMD_OK; -} - -bool null_output_stream_t::append(const wchar_t *, size_t) { return true; } - -std::unique_ptr make_null_io_streams_ffi() { - // Temporary test helper. - static null_output_stream_t *null = new null_output_stream_t(); - return std::make_unique(*null, *null); -} - -std::unique_ptr make_test_io_streams_ffi() { - // Temporary test helper. - auto streams = std::make_unique(); - streams->stdin_is_directly_redirected = false; // read from argv instead of stdin - return streams; -} - -wcstring get_test_output_ffi(const io_streams_t &streams) { - string_output_stream_t *out = static_cast(&streams.out); - if (out == nullptr) { - return wcstring(); - } - return out->contents(); -} - -bool string_output_stream_t::append(const wchar_t *s, size_t amt) { - contents_.append(s, amt); - return true; -} - -const wcstring &string_output_stream_t::contents() const { return contents_; } - -bool buffered_output_stream_t::append(const wchar_t *s, size_t amt) { - return buffer_->append(wcs2string(s, amt)); -} - -bool buffered_output_stream_t::append_with_separation(const wchar_t *s, size_t len, - separation_type_t type, bool want_newline) { - UNUSED(want_newline); - return buffer_->append(wcs2string(s, len), type); -} - -int buffered_output_stream_t::flush_and_check_error() { - if (buffer_->discarded()) { - return STATUS_READ_TOO_MUCH; - } - return 0; -} diff --git a/src/io.h b/src/io.h index f60c3f4c1..a68795591 100644 --- a/src/io.h +++ b/src/io.h @@ -13,11 +13,22 @@ #include #include "common.h" +#include "cxx.h" #include "fds.h" #include "global_safety.h" #include "redirection.h" #include "signals.h" -#include "topic_monitor.h" +#if INCLUDE_RUST_HEADERS +#include "io.rs.h" +#else +struct IoChain; +struct IoStreams; +struct OutputStreamFfi; +#endif +using output_stream_t = OutputStreamFfi; +using io_streams_t = IoStreams; + +// null_output_stream_t using std::shared_ptr; @@ -278,260 +289,10 @@ class io_bufferfill_t final : public io_data_t { struct callback_args_t; struct autoclose_fd_t2; -/// An io_buffer_t is a buffer which can populate itself by reading from an fd. -/// It is not an io_data_t. -class io_buffer_t { - public: - explicit io_buffer_t(size_t limit) : buffer_(limit) {} - - ~io_buffer_t(); - - /// Append a string to the buffer. - bool append(std::string &&str, separation_type_t type = separation_type_t::inferred) { - return buffer_.acquire()->append(std::move(str), type); - } - - /// \return true if output was discarded due to exceeding the read limit. - bool discarded() { return buffer_.acquire()->discarded(); } - - /// FFI callback workaround. - void item_callback(autoclose_fd_t2 &fd, uint8_t reason, callback_args_t *args); - - private: - /// Read some, filling the buffer. The buffer is passed in to enforce that the append lock is - /// held. \return positive on success, 0 if closed, -1 on error (in which case errno will be - /// set). - ssize_t read_once(int fd, acquired_lock &buff); - - /// Begin the fill operation, reading from the given fd in the background. - void begin_filling(autoclose_fd_t readfd); - - /// End the background fillthread operation, and return the buffer, transferring ownership. - separated_buffer_t complete_background_fillthread_and_take_buffer(); - - /// Helper to return whether the fillthread is running. - bool fillthread_running() const { return fill_waiter_.get() != nullptr; } - - /// Buffer storing what we have read. - owning_lock buffer_; - - /// Atomic flag indicating our fillthread should shut down. - relaxed_atomic_bool_t shutdown_fillthread_{false}; - - /// A promise, allowing synchronization with the background fill operation. - /// The operation has a reference to this as well, and fulfills this promise when it exits. - std::shared_ptr> fill_waiter_{}; - - /// The item id of our background fillthread fd monitor item. - uint64_t item_id_{0}; - - friend io_bufferfill_t; -}; - using io_data_ref_t = std::shared_ptr; -class io_chain_t : public std::vector { - public: - using std::vector::vector; - // user-declared ctor to allow const init. Do not default this, it will break the build. - io_chain_t() {} - - /// autocxx falls over with this so hide it. -#if INCLUDE_RUST_HEADERS - void remove(const io_data_ref_t &element); - void push_back(io_data_ref_t element); -#endif - bool append(const io_chain_t &chain); - - /// \return the last io redirection in the chain for the specified file descriptor, or nullptr - /// if none. -#if INCLUDE_RUST_HEADERS - io_data_ref_t io_for_fd(int fd) const; -#endif - - /// Attempt to resolve a list of redirection specs to IOs, appending to 'this'. - /// \return true on success, false on error, in which case an error will have been printed. - bool append_from_specs(const redirection_spec_list_t &specs, const wcstring &pwd); - - /// Output debugging information to stderr. - void print() const; -}; +using io_chain_t = IoChain; dup2_list_t dup2_list_resolve_chain_shim(const io_chain_t &io_chain); -/// Base class representing the output that a builtin can generate. -/// This has various subclasses depending on the ultimate output destination. -class output_stream_t : noncopyable_t, nonmovable_t { - public: - /// Required override point. The output stream receives a string \p s with \p amt chars. - virtual bool append(const wchar_t *s, size_t amt) = 0; - - /// \return any internally buffered contents. - /// This is only implemented for a string_output_stream; others flush data to their underlying - /// receiver (fd, or separated buffer) immediately and so will return an empty string here. - virtual const wcstring &contents() const; - - /// Flush any unwritten data to the underlying device, and return an error code. - /// A 0 code indicates success. The base implementation returns 0. - virtual int flush_and_check_error(); - - /// An optional override point. This is for explicit separation. - /// \param want_newline this is true if the output item should be ended with a newline. This - /// is only relevant if we are printing the output to a stream, - virtual bool append_with_separation(const wchar_t *s, size_t len, separation_type_t type, - bool want_newline = true); - - /// The following are all convenience overrides. - bool append_with_separation(const wcstring &s, separation_type_t type, - bool want_newline = true) { - return append_with_separation(s.data(), s.size(), type, want_newline); - } - - /// Append a string. - bool append(const wcstring &s) { return append(s.data(), s.size()); } - bool append(const wchar_t *s) { return append(s, std::wcslen(s)); } - - /// Append a char. - bool push(wchar_t s) { return append(&s, 1); } - - // Append data from a narrow buffer, widening it. - bool append_narrow_buffer(const separated_buffer_t &buffer); - - /// Append a format string. - bool append_format(const wchar_t *format, ...) { - va_list va; - va_start(va, format); - bool r = append_formatv(format, va); - va_end(va); - - return r; - } - - bool append_formatv(const wchar_t *format, va_list va) { - return append(vformat_string(format, va)); - } - - output_stream_t() = default; - virtual ~output_stream_t() = default; -}; - -/// A null output stream which ignores all writes. -class null_output_stream_t final : public output_stream_t { - virtual bool append(const wchar_t *s, size_t amt) override; -}; - -/// An output stream for builtins which outputs to an fd. -/// Note the fd may be something like stdout; there is no ownership implied here. -class fd_output_stream_t final : public output_stream_t { - public: - /// Construct from a file descriptor, which must be nonegative. - explicit fd_output_stream_t(int fd); - - int flush_and_check_error() override; - - bool append(const wchar_t *s, size_t amt) override; - - private: - /// The file descriptor to write to. - const int fd_; - - /// Used to check if a SIGINT has been received when EINTR is encountered - sigchecker_t sigcheck_; - - /// Whether we have received an error. - bool errored_{false}; -}; - -/// A simple output stream which buffers into a wcstring. -class string_output_stream_t final : public output_stream_t { - public: - string_output_stream_t() = default; - bool append(const wchar_t *s, size_t amt) override; - - /// \return the wcstring containing the output. - const wcstring &contents() const override; - - private: - wcstring contents_; -}; - -/// An output stream for builtins which writes into a separated buffer. -class buffered_output_stream_t final : public output_stream_t { - public: - explicit buffered_output_stream_t(std::shared_ptr buffer) - : buffer_(std::move(buffer)) { - assert(buffer_ && "Buffer must not be null"); - } - - bool append(const wchar_t *s, size_t amt) override; - bool append_with_separation(const wchar_t *s, size_t len, separation_type_t type, - bool want_newline) override; - int flush_and_check_error() override; - - private: - /// The buffer we are filling. - std::shared_ptr buffer_; -}; - -class IoStreams; -using io_streams_t = IoStreams; - -class IoStreams : noncopyable_t { - public: - // Streams for out and err. - output_stream_t &out; - output_stream_t &err; - - // fd representing stdin. This is not closed by the destructor. - // Note: if stdin is explicitly closed by `<&-` then this is -1! - int stdin_fd{-1}; - - // Whether stdin is "directly redirected," meaning it is the recipient of a pipe (foo | cmd) or - // direct redirection (cmd < foo.txt). An "indirect redirection" would be e.g. - // begin ; cmd ; end < foo.txt - // If stdin is closed (cmd <&-) this is false. - bool stdin_is_directly_redirected{false}; - - // Indicates whether stdout and stderr are specifically piped. - // If this is set, then the is_redirected flags must also be set. - bool out_is_piped{false}; - bool err_is_piped{false}; - - // Indicates whether stdout and stderr are at all redirected (e.g. to a file or piped). - bool out_is_redirected{false}; - bool err_is_redirected{false}; - - // Actual IO redirections. This is only used by the source builtin. Unowned. - const io_chain_t *io_chain{nullptr}; - - // The job group of the job, if any. This enables builtins which run more code like eval() to - // share pgid. - // FIXME: this is awkwardly placed. - std::shared_ptr job_group{}; - - IoStreams(output_stream_t &out, output_stream_t &err) : out(out), err(err) {} - virtual ~IoStreams() = default; - - /// autocxx junk. - output_stream_t &get_out() { return out; }; - output_stream_t &get_err() { return err; }; - IoStreams(const io_streams_t &) = delete; - bool get_out_redirected() { return out_is_redirected; }; - bool get_err_redirected() { return err_is_redirected; }; - bool ffi_stdin_is_directly_redirected() const { return stdin_is_directly_redirected; }; - int ffi_stdin_fd() const { return stdin_fd; }; -}; - -/// FFI helper. -class owning_io_streams_t : public io_streams_t { - public: - string_output_stream_t out_storage; - null_output_stream_t err_storage; - owning_io_streams_t() : io_streams_t(out_storage, err_storage) {} -}; - -std::unique_ptr make_null_io_streams_ffi(); -std::unique_ptr make_test_io_streams_ffi(); -wcstring get_test_output_ffi(const io_streams_t &streams); - #endif diff --git a/src/operation_context.cpp b/src/operation_context.cpp deleted file mode 100644 index c91952918..000000000 --- a/src/operation_context.cpp +++ /dev/null @@ -1,29 +0,0 @@ -// Utilities for io redirection. -#include "config.h" // IWYU pragma: keep - -#include "operation_context.h" - -#include - -#include "env.h" - -bool no_cancel() { return false; } - -operation_context_t::operation_context_t(std::shared_ptr parser, - const environment_t &vars, cancel_checker_t cancel_checker, - size_t expansion_limit) - : parser(std::move(parser)), - vars(vars), - expansion_limit(expansion_limit), - cancel_checker(std::move(cancel_checker)) {} - -operation_context_t operation_context_t::empty() { - static const null_environment_t nullenv{}; - return operation_context_t{nullenv}; -} - -operation_context_t operation_context_t::globals() { - return operation_context_t{env_stack_t::globals()}; -} - -operation_context_t::~operation_context_t() = default; diff --git a/src/operation_context.h b/src/operation_context.h index ac8b91ac4..fa991c4c7 100644 --- a/src/operation_context.h +++ b/src/operation_context.h @@ -1,18 +1,15 @@ #ifndef FISH_OPERATION_CONTEXT_H #define FISH_OPERATION_CONTEXT_H -#include -#include -#include +#include -#include "common.h" +struct OperationContext; +#if INCLUDE_RUST_HEADERS +#include "operation_context.rs.h" +#else +#endif -class environment_t; -class Parser; using parser_t = Parser; -struct job_group_t; - -/// A common helper which always returns false. -bool no_cancel(); +using operation_context_t = OperationContext; /// Default limits for expansion. enum expansion_limit_t : size_t { @@ -23,50 +20,4 @@ enum expansion_limit_t : size_t { kExpansionLimitBackground = 512, }; -/// A operation_context_t is a simple property bag which wraps up data needed for highlighting, -/// expansion, completion, and more. -class operation_context_t { - public: - // The parser, if this is a foreground operation. If this is a background operation, this may be - // nullptr. - std::shared_ptr parser; - - // The set of variables. It is the creator's responsibility to ensure this lives as log as the - // context itself. - const environment_t &vars; - - // The limit in the number of expansions which should be produced. - const size_t expansion_limit; - - /// The job group of the parental job. - /// This is used only when expanding command substitutions. If this is set, any jobs created by - /// the command substitutions should use this tree. - std::shared_ptr job_group{}; - - // A function which may be used to poll for cancellation. - cancel_checker_t cancel_checker; - - // Invoke the cancel checker. \return if we should cancel. - bool check_cancel() const { return cancel_checker(); } - - // \return an "empty" context which contains no variables, no parser, and never cancels. - static operation_context_t empty(); - - // \return an operation context that contains only global variables, no parser, and never - // cancels. - static operation_context_t globals(); - - /// Construct from a full set of properties. - operation_context_t(std::shared_ptr parser, const environment_t &vars, - cancel_checker_t cancel_checker, - size_t expansion_limit = kExpansionLimitDefault); - - /// Construct from vars alone. - explicit operation_context_t(const environment_t &vars, - size_t expansion_limit = kExpansionLimitDefault) - : operation_context_t(nullptr, vars, no_cancel, expansion_limit) {} - - ~operation_context_t(); -}; - #endif diff --git a/src/output.cpp b/src/output.cpp index a291f907d..b6054c30d 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -83,7 +83,8 @@ rgb_color_t parse_color(const env_var_t &var, bool is_background) { bool next_is_background = false; wcstring color_name; - for (const wcstring &next : var.as_list()) { + auto vals = var.as_list(); + for (const wcstring &next : vals) { color_name.clear(); if (is_background) { if (color_name.empty() && next_is_background) { diff --git a/src/output.h b/src/output.h old mode 100644 new mode 100755 index c5aac58ed..6bad4c248 --- a/src/output.h +++ b/src/output.h @@ -16,7 +16,8 @@ struct outputter_t; #endif -class env_var_t; +#include "env.h" + rgb_color_t parse_color(const env_var_t &var, bool is_background); /// Sets what colors are supported. diff --git a/src/pager.cpp b/src/pager.cpp index 452e80add..560d9a604 100644 --- a/src/pager.cpp +++ b/src/pager.cpp @@ -29,6 +29,19 @@ using comp_t = pager_t::comp_t; using comp_info_list_t = std::vector; +comp_t &comp_t::operator=(const comp_t &other) { + if (this == &other) return *this; + comp = other.comp; + desc = other.desc; + representative = other.representative->clone(); + colors = other.colors; + comp_width = other.comp_width; + desc_width = other.desc_width; + return *this; +} + +comp_t::comp_t(const comp_t &other) { *this = other; } + /// The minimum width (in characters) the terminal must to show completions at all. #define PAGER_MIN_WIDTH 16 @@ -333,7 +346,7 @@ static comp_info_list_t process_completions_into_infos(const completion_list_t & // Append the single completion string. We may later merge these into multiple. comp_info->comp.push_back(escape_string( - comp.completion, ESCAPE_NO_PRINTABLES | ESCAPE_NO_QUOTED | ESCAPE_SYMBOLIC)); + *comp.completion(), ESCAPE_NO_PRINTABLES | ESCAPE_NO_QUOTED | ESCAPE_SYMBOLIC)); if (comp.replaces_commandline() // HACK We want to render a full shell command, with syntax highlighting. Above we // escape nonprintables, which might make the rendered command longer than the original @@ -346,16 +359,16 @@ static comp_info_list_t process_completions_into_infos(const completion_list_t & // then writing a variant of escape_string() that adjusts highlighting according // so it matches the escaped string. && MB_CUR_MAX > 1) { - highlight_shell(comp.completion, comp_info->colors, operation_context_t::empty()); + highlight_shell(*comp.completion(), comp_info->colors, *empty_operation_context()); assert(comp_info->comp.back().size() >= comp_info->colors.size()); } // Append the mangled description. - comp_info->desc = comp.description; + comp_info->desc = std::move(*comp.description()); mangle_1_completion_description(&comp_info->desc); // Set the representative completion. - comp_info->representative = comp; + comp_info->representative = comp.clone(); } return result; } @@ -578,7 +591,7 @@ bool pager_t::completion_try_print(size_t cols, const wcstring &prefix, const co // We limit the width to term_width - 1. highlight_spec_t underline{}; - underline.force_underline = true; + underline->force_underline = true; size_t search_field_remaining = term_width - 1; search_field_remaining -= print_max(SEARCH_FIELD_PROMPT, highlight_role_t::normal, @@ -878,7 +891,7 @@ const completion_t *pager_t::selected_completion(const page_rendering_t &renderi const completion_t *result = nullptr; size_t idx = visual_selected_completion_index(rendering.rows, rendering.cols); if (idx != PAGER_SELECTION_NONE) { - result = &completion_infos.at(idx).representative; + result = &*completion_infos.at(idx).representative; } return result; } diff --git a/src/pager.h b/src/pager.h index e5e428132..f57e34be1 100644 --- a/src/pager.h +++ b/src/pager.h @@ -9,6 +9,7 @@ #include "common.h" #include "complete.h" +#include "cxx.h" #include "highlight.h" #include "reader.h" #include "screen.h" @@ -87,7 +88,7 @@ class pager_t { /// The description. wcstring desc{}; /// The representative completion. - completion_t representative{L""}; + rust::Box representative = new_completion(); /// The per-character highlighting, used when this is a full shell command. std::vector colors{}; /// On-screen width of the completion string. @@ -95,6 +96,12 @@ class pager_t { /// On-screen width of the description information. size_t desc_width{0}; + comp_t() = default; + comp_t(const comp_t &other); + comp_t &operator=(const comp_t &other); + comp_t(comp_t &&) = default; + comp_t &operator=(comp_t &&) = default; + // Our text looks like this: // completion (description) // Two spaces separating, plus parens, yields 4 total extra space diff --git a/src/parse_execution.cpp b/src/parse_execution.cpp deleted file mode 100644 index 85a5285b2..000000000 --- a/src/parse_execution.cpp +++ /dev/null @@ -1,1672 +0,0 @@ -// Provides the "linkage" between an ast and actual execution structures (job_t, etc.) -#include "config.h" // IWYU pragma: keep - -#include "parse_execution.h" - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include "ast.h" -#include "builtin.h" -#include "builtins/function.rs.h" -#include "common.h" -#include "complete.h" -#include "env.h" -#include "event.h" -#include "exec.h" -#include "expand.h" -#include "ffi.h" -#include "flog.h" -#include "function.h" -#include "io.h" -#include "job_group.rs.h" -#include "maybe.h" -#include "operation_context.h" -#include "parse_constants.h" -#include "parse_util.h" -#include "parser.h" -#include "path.h" -#include "proc.h" -#include "reader.h" -#include "timer.rs.h" -#include "tokenizer.h" -#include "trace.rs.h" -#include "wildcard.h" -#include "wutil.h" - -/// These are the specific statement types that support redirections. -static constexpr bool type_is_redirectable_block(ast::type_t type) { - using t = ast::type_t; - return type == t::block_statement || type == t::if_statement || type == t::switch_statement; -} - -static bool specific_statement_type_is_redirectable_block(const ast::node_t &node) { - return type_is_redirectable_block(node.typ()); -} - -/// Get the name of a redirectable block, for profiling purposes. -static wcstring profiling_cmd_name_for_redirectable_block(const ast::node_t &node, - const parsed_source_ref_t &pstree) { - using namespace ast; - assert(specific_statement_type_is_redirectable_block(node)); - - assert(node.try_source_range() && "No source range for block"); - auto source_range = node.source_range(); - - size_t src_end = 0; - switch (node.typ()) { - case type_t::block_statement: { - auto block_header = node.as_block_statement().header().ptr(); - switch (block_header->typ()) { - case type_t::for_header: - src_end = block_header->as_for_header().semi_nl().source_range().start; - break; - - case type_t::while_header: - src_end = - block_header->as_while_header().condition().ptr()->source_range().end(); - break; - - case type_t::function_header: - src_end = block_header->as_function_header().semi_nl().source_range().start; - break; - - case type_t::begin_header: - src_end = - block_header->as_begin_header().kw_begin().ptr()->source_range().end(); - break; - - default: - DIE("Unexpected block header type"); - } - } break; - - case type_t::if_statement: - src_end = - node.as_if_statement().if_clause().condition().job().ptr()->source_range().end(); - break; - - case type_t::switch_statement: - src_end = node.as_switch_statement().semi_nl().source_range().start; - break; - - default: - DIE("Not a redirectable block type"); - break; - } - - assert(src_end >= source_range.start && "Invalid source end"); - - // Get the source for the block, and cut it at the next statement terminator. - wcstring result = pstree.src().substr(source_range.start, src_end - source_range.start); - result.append(L"..."); - return result; -} - -/// Get a redirection from stderr to stdout (i.e. 2>&1). -static rust::Box get_stderr_merge() { - const wchar_t *stdout_fileno_str = L"1"; - return new_redirection_spec(STDERR_FILENO, redirection_mode_t::fd, stdout_fileno_str); -} - -parse_execution_context_t::parse_execution_context_t(rust::Box pstree, - const operation_context_t &ctx, - io_chain_t block_io) - : pstree(std::move(pstree)), - parser(ctx.parser.get()), - ctx(ctx), - block_io(std::move(block_io)) {} - -// Utilities - -wcstring parse_execution_context_t::get_source(const ast::node_t &node) const { - return *node.source(pstree->src()); -} - -const ast::decorated_statement_t * -parse_execution_context_t::infinite_recursive_statement_in_job_list(const ast::job_list_t &jobs, - wcstring *out_func_name) const { - // This is a bit fragile. It is a test to see if we are inside of function call, but not inside - // a block in that function call. If, in the future, the rules for what block scopes are pushed - // on function invocation changes, then this check will break. - const block_t *current = parser->block_at_index(0), *parent = parser->block_at_index(1); - bool is_within_function_call = - (current && parent && current->type() == block_type_t::top && parent->is_function_call()); - if (!is_within_function_call) { - return nullptr; - } - - // Get the function name of the immediate block. - const wcstring &forbidden_function_name = parent->function_name; - - // Get the first job in the job list. - const ast::job_conjunction_t *jc = jobs.at(0); - if (!jc) return nullptr; - const ast::job_pipeline_t *job = &jc->job(); - - // Helper to return if a statement is infinitely recursive in this function. - auto statement_recurses = - [&](const ast::statement_t &stat) -> const ast::decorated_statement_t * { - // Ignore non-decorated statements like `if`, etc. - const ast::decorated_statement_t *dc = - stat.contents().ptr()->try_as_decorated_statement() - ? &stat.contents().ptr()->as_decorated_statement() - : nullptr; - if (!dc) return nullptr; - - // Ignore statements with decorations like 'builtin' or 'command', since those - // are not infinite recursion. In particular that is what enables 'wrapper functions'. - if (dc->decoration() != statement_decoration_t::none) return nullptr; - - // Check the command. - wcstring cmd = *dc->command().source(pstree->src()); - bool forbidden = - !cmd.empty() && - expand_one(cmd, {expand_flag::skip_cmdsubst, expand_flag::skip_variables}, ctx) && - cmd == forbidden_function_name; - return forbidden ? dc : nullptr; - }; - - const ast::decorated_statement_t *infinite_recursive_statement = nullptr; - - // Check main statement. - infinite_recursive_statement = statement_recurses(jc->job().statement()); - - // Check piped remainder. - if (!infinite_recursive_statement) { - for (size_t i = 0; i < job->continuation().count(); i++) { - const ast::job_continuation_t &c = *job->continuation().at(i); - if (const auto *s = statement_recurses(c.statement())) { - infinite_recursive_statement = s; - break; - } - } - } - - if (infinite_recursive_statement && out_func_name) { - *out_func_name = forbidden_function_name; - } - // may be null - return infinite_recursive_statement; -} - -process_type_t parse_execution_context_t::process_type_for_command( - const ast::decorated_statement_t &statement, const wcstring &cmd) const { - enum process_type_t process_type = process_type_t::external; - - // Determine the process type, which depends on the statement decoration (command, builtin, - // etc). - switch (statement.decoration()) { - case statement_decoration_t::exec: - process_type = process_type_t::exec; - break; - case statement_decoration_t::command: - process_type = process_type_t::external; - break; - case statement_decoration_t::builtin: - process_type = process_type_t::builtin; - break; - case statement_decoration_t::none: - if (function_exists(cmd, *parser)) { - process_type = process_type_t::function; - } else if (builtin_exists(cmd)) { - process_type = process_type_t::builtin; - } else { - process_type = process_type_t::external; - } - break; - } - - return process_type; -} - -maybe_t parse_execution_context_t::check_end_execution() const { - // If one of our jobs ended with SIGINT, we stop execution. - // Likewise if fish itself got a SIGINT, or if something ran exit, etc. - if (cancel_signal || ctx.check_cancel() || fish_is_unwinding_for_exit()) { - return end_execution_reason_t::cancelled; - } - const auto &ld = parser->libdata(); - if (ld.exit_current_script) { - return end_execution_reason_t::cancelled; - } - if (ld.returning) { - return end_execution_reason_t::control_flow; - } - if (ld.loop_status != loop_status_t::normals) { - return end_execution_reason_t::control_flow; - } - return none(); -} - -/// Return whether the job contains a single statement, of block type, with no redirections. -bool parse_execution_context_t::job_is_simple_block(const ast::job_pipeline_t &job) const { - using namespace ast; - // Must be no pipes. - if (!job.continuation().empty()) { - return false; - } - - // Helper to check if an argument_or_redirection_list_t has no redirections. - auto no_redirs = [](const argument_or_redirection_list_t &list) -> bool { - for (size_t i = 0; i < list.count(); i++) { - const argument_or_redirection_t &val = *list.at(i); - if (val.is_redirection()) return false; - } - return true; - }; - - // Check if we're a block statement with redirections. We do it this obnoxious way to preserve - // type safety (in case we add more specific statement types). - const auto ss = job.statement().contents().ptr(); - switch (ss->typ()) { - case type_t::block_statement: - return no_redirs(ss->as_block_statement().args_or_redirs()); - case type_t::switch_statement: - return no_redirs(ss->as_switch_statement().args_or_redirs()); - case type_t::if_statement: - return no_redirs(ss->as_if_statement().args_or_redirs()); - case type_t::not_statement: - case type_t::decorated_statement: - // not block statements - return false; - default: - assert(0 && "Unexpected child block type"); - return false; - } -} - -end_execution_reason_t parse_execution_context_t::run_if_statement( - const ast::if_statement_t &statement, const block_t *associated_block) { - using namespace ast; - using job_list_t = ast::job_list_t; - end_execution_reason_t result = end_execution_reason_t::ok; - - // We have a sequence of if clauses, with a final else, resulting in a single job list that we - // execute. - const job_list_t *job_list_to_execute = nullptr; - const if_clause_t *if_clause = &statement.if_clause(); - - // Index of the *next* elseif_clause to test. - const elseif_clause_list_t &elseif_clauses = statement.elseif_clauses(); - size_t next_elseif_idx = 0; - - // We start with the 'if'. - trace_if_enabled(*parser, L"if"); - - for (;;) { - if (auto ret = check_end_execution()) { - result = *ret; - break; - } - - // An if condition has a job and a "tail" of andor jobs, e.g. "foo ; and bar; or baz". - // Check the condition and the tail. We treat end_execution_reason_t::error here as failure, - // in accordance with historic behavior. - end_execution_reason_t cond_ret = - run_job_conjunction(if_clause->condition(), associated_block); - if (cond_ret == end_execution_reason_t::ok) { - cond_ret = run_job_list(if_clause->andor_tail(), associated_block); - } - const bool take_branch = - (cond_ret == end_execution_reason_t::ok) && parser->get_last_status() == EXIT_SUCCESS; - - if (take_branch) { - // Condition succeeded. - job_list_to_execute = &if_clause->body(); - break; - } - - // See if we have an elseif. - const auto *elseif_clause = elseif_clauses.at(next_elseif_idx++); - if (elseif_clause) { - trace_if_enabled(*parser, L"else if"); - if_clause = &elseif_clause->if_clause(); - } else { - break; - } - } - - if (!job_list_to_execute) { - // our ifs and elseifs failed. - // Check our else body. - if (statement.has_else_clause()) { - trace_if_enabled(*parser, L"else"); - job_list_to_execute = &statement.else_clause().body(); - } - } - - if (!job_list_to_execute) { - // 'if' condition failed, no else clause, return 0, we're done. - // No job list means no successful conditions, so return 0 (issue #1443). - parser->set_last_statuses(statuses_t::just(STATUS_CMD_OK)); - } else { - // Execute the job list we got. - block_t *ib = parser->push_block(block_t::if_block()); - run_job_list(*job_list_to_execute, ib); - if (auto ret = check_end_execution()) { - result = *ret; - } - parser->pop_block(ib); - } - trace_if_enabled(*parser, L"end if"); - - // It's possible there's a last-minute cancellation (issue #1297). - if (auto ret = check_end_execution()) { - result = *ret; - } - - // Otherwise, take the exit status of the job list. Reversal of issue #1061. - return result; -} - -end_execution_reason_t parse_execution_context_t::run_begin_statement( - const ast::job_list_t &contents) { - // Basic begin/end block. Push a scope block, run jobs, pop it - trace_if_enabled(*parser, L"begin"); - block_t *sb = parser->push_block(block_t::scope_block(block_type_t::begin)); - end_execution_reason_t ret = run_job_list(contents, sb); - parser->pop_block(sb); - trace_if_enabled(*parser, L"end begin"); - return ret; -} - -// Define a function. -end_execution_reason_t parse_execution_context_t::run_function_statement( - const ast::block_statement_t &statement, const ast::function_header_t &header) { - using namespace ast; - // Get arguments. - wcstring_list_ffi_t arguments; - ast_args_list_t arg_nodes = get_argument_nodes(header.args()); - arg_nodes.insert(arg_nodes.begin(), &header.first_arg()); - end_execution_reason_t result = - this->expand_arguments_from_nodes(arg_nodes, &arguments.vals, failglob); - - if (result != end_execution_reason_t::ok) { - return result; - } - - trace_if_enabled(*parser, L"function", arguments.vals); - null_output_stream_t outs; - string_output_stream_t errs; - io_streams_t streams(outs, errs); - int err_code = builtin_function_ffi(*parser, streams, arguments, - reinterpret_cast(&*pstree), statement); - - parser->libdata().status_count++; - parser->set_last_statuses(statuses_t::just(err_code)); - - const wcstring &errtext = errs.contents(); - if (!errtext.empty()) { - return this->report_error(err_code, *header.ptr(), L"%ls", errtext.c_str()); - } - return result; -} - -end_execution_reason_t parse_execution_context_t::run_block_statement( - const ast::block_statement_t &statement, const block_t *associated_block) { - auto bh = statement.header().ptr(); - const ast::job_list_t &contents = statement.jobs(); - end_execution_reason_t ret = end_execution_reason_t::ok; - if (const auto *fh = bh->try_as_for_header()) { - ret = run_for_statement(*fh, contents); - } else if (const auto *wh = bh->try_as_while_header()) { - ret = run_while_statement(*wh, contents, associated_block); - } else if (const auto *fh = bh->try_as_function_header()) { - ret = run_function_statement(statement, *fh); - } else if (bh->try_as_begin_header()) { - ret = run_begin_statement(contents); - } else { - FLOGF(error, L"Unexpected block header: %ls\n", bh->describe()->c_str()); - PARSER_DIE(); - } - return ret; -} - -end_execution_reason_t parse_execution_context_t::run_for_statement( - const ast::for_header_t &header, const ast::job_list_t &block_contents) { - // Get the variable name: `for var_name in ...`. We expand the variable name. It better result - // in just one. - wcstring for_var_name = *header.var_name().source(get_source()); - if (!expand_one(for_var_name, expand_flags_t{}, ctx)) { - return report_error(STATUS_EXPAND_ERROR, *header.var_name().ptr(), - FAILED_EXPANSION_VARIABLE_NAME_ERR_MSG, for_var_name.c_str()); - } - - if (!valid_var_name(for_var_name)) { - return report_error(STATUS_INVALID_ARGS, *header.var_name().ptr(), BUILTIN_ERR_VARNAME, - L"for", for_var_name.c_str()); - } - - // Get the contents to iterate over. - std::vector arguments; - ast_args_list_t arg_nodes = get_argument_nodes(header.args()); - end_execution_reason_t ret = this->expand_arguments_from_nodes(arg_nodes, &arguments, nullglob); - if (ret != end_execution_reason_t::ok) { - return ret; - } - - auto var = parser->vars().get(for_var_name, ENV_DEFAULT); - if (env_var_t::flags_for(for_var_name.c_str()) & env_var_t::flag_read_only) { - return report_error(STATUS_INVALID_ARGS, *header.var_name().ptr(), - _(L"%ls: %ls: cannot overwrite read-only variable"), L"for", - for_var_name.c_str()); - } - - auto &vars = parser->vars(); - int retval; - retval = vars.set(for_var_name, ENV_LOCAL | ENV_USER, - var ? var->as_list() : std::vector{}); - assert(retval == ENV_OK); - - trace_if_enabled(*parser, L"for", arguments); - block_t *fb = parser->push_block(block_t::for_block()); - - // We fire the same event over and over again, just construct it once. - auto evt = new_event_variable_set(for_var_name); - - // Now drive the for loop. - for (const wcstring &val : arguments) { - if (auto reason = check_end_execution()) { - ret = *reason; - break; - } - - retval = vars.set(for_var_name, ENV_DEFAULT | ENV_USER, {val}); - assert(retval == ENV_OK && "for loop variable should have been successfully set"); - (void)retval; - event_fire(*parser, *evt); - - auto &ld = parser->libdata(); - ld.loop_status = loop_status_t::normals; - this->run_job_list(block_contents, fb); - - if (check_end_execution() == end_execution_reason_t::control_flow) { - // Handle break or continue. - bool do_break = (ld.loop_status == loop_status_t::breaks); - ld.loop_status = loop_status_t::normals; - if (do_break) { - break; - } - } - } - - parser->pop_block(fb); - trace_if_enabled(*parser, L"end for"); - return ret; -} - -end_execution_reason_t parse_execution_context_t::run_switch_statement( - const ast::switch_statement_t &statement) { - // Get the switch variable. - const wcstring switch_value = get_source(*statement.argument().ptr()); - - // Expand it. We need to offset any errors by the position of the string. - completion_list_t switch_values_expanded; - auto errors = new_parse_error_list(); - auto expand_ret = - expand_string(switch_value, &switch_values_expanded, expand_flags_t{}, ctx, &*errors); - errors->offset_source_start(statement.argument().range().start); - - switch (expand_ret.result) { - case expand_result_t::error: - return report_errors(expand_ret.status, *errors); - - case expand_result_t::cancel: - return end_execution_reason_t::cancelled; - - case expand_result_t::wildcard_no_match: - return report_error(STATUS_UNMATCHED_WILDCARD, *statement.argument().ptr(), - WILDCARD_ERR_MSG, get_source(*statement.argument().ptr()).c_str()); - - case expand_result_t::ok: - if (switch_values_expanded.size() > 1) { - return report_error(STATUS_INVALID_ARGS, *statement.argument().ptr(), - _(L"switch: Expected at most one argument, got %lu\n"), - switch_values_expanded.size()); - } - break; - } - - // If we expanded to nothing, match the empty string. - assert(switch_values_expanded.size() <= 1 && "Should have at most one expansion"); - wcstring switch_value_expanded; - if (!switch_values_expanded.empty()) { - switch_value_expanded = std::move(switch_values_expanded.front().completion); - } - - end_execution_reason_t result = end_execution_reason_t::ok; - - trace_if_enabled(*parser, L"switch", {switch_value_expanded}); - block_t *sb = parser->push_block(block_t::switch_block()); - - // Expand case statements. - const ast::case_item_t *matching_case_item = nullptr; - for (size_t i = 0; i < statement.cases().count(); i++) { - const ast::case_item_t &case_item = *statement.cases().at(i); - if (auto ret = check_end_execution()) { - result = *ret; - break; - } - - // Expand arguments. A case item list may have a wildcard that fails to expand to - // anything. We also report case errors, but don't stop execution; i.e. a case item that - // contains an unexpandable process will report and then fail to match. - ast_args_list_t arg_nodes = get_argument_nodes(case_item.arguments()); - std::vector case_args; - end_execution_reason_t case_result = - this->expand_arguments_from_nodes(arg_nodes, &case_args, failglob); - if (case_result == end_execution_reason_t::ok) { - for (const wcstring &arg : case_args) { - // Unescape wildcards so they can be expanded again. - wcstring unescaped_arg = parse_util_unescape_wildcards(arg); - bool match = wildcard_match(switch_value_expanded, unescaped_arg); - - // If this matched, we're done. - if (match) { - matching_case_item = &case_item; - break; - } - } - } - if (matching_case_item) break; - } - - if (matching_case_item) { - // Success, evaluate the job list. - assert(result == end_execution_reason_t::ok && "Expected success"); - result = this->run_job_list(matching_case_item->body(), sb); - } - - parser->pop_block(sb); - trace_if_enabled(*parser, L"end switch"); - return result; -} - -end_execution_reason_t parse_execution_context_t::run_while_statement( - const ast::while_header_t &header, const ast::job_list_t &contents, - const block_t *associated_block) { - end_execution_reason_t ret = end_execution_reason_t::ok; - - // "The exit status of the while loop shall be the exit status of the last compound-list-2 - // executed, or zero if none was executed." - // Here are more detailed requirements: - // - If we execute the loop body zero times, or the loop body is empty, the status is success. - // - An empty loop body is treated as true, both in the loop condition and after loop exit. - // - The exit status of the last command is visible in the loop condition. (i.e. do not set the - // exit status to true BEFORE executing the loop condition). - // We achieve this by restoring the status if the loop condition fails, plus a special - // affordance for the first condition. - bool first_cond_check = true; - - trace_if_enabled(*parser, L"while"); - - // Run while the condition is true. - for (;;) { - // Save off the exit status if it came from the loop body. We'll restore it if the condition - // is false. - auto cond_saved_status = - first_cond_check ? statuses_t::just(EXIT_SUCCESS) : parser->get_last_statuses(); - first_cond_check = false; - - // Check the condition. - end_execution_reason_t cond_ret = - this->run_job_conjunction(header.condition(), associated_block); - if (cond_ret == end_execution_reason_t::ok) { - cond_ret = run_job_list(header.andor_tail(), associated_block); - } - - // If the loop condition failed to execute, then exit the loop without modifying the exit - // status. If the loop condition executed with a failure status, restore the status and then - // exit the loop. - if (cond_ret != end_execution_reason_t::ok) { - break; - } else if (parser->get_last_status() != EXIT_SUCCESS) { - parser->set_last_statuses(cond_saved_status); - break; - } - - // Check cancellation. - if (auto reason = check_end_execution()) { - ret = *reason; - break; - } - - // Push a while block and then check its cancellation reason. - auto &ld = parser->libdata(); - ld.loop_status = loop_status_t::normals; - - block_t *wb = parser->push_block(block_t::while_block()); - this->run_job_list(contents, wb); - auto cancel_reason = this->check_end_execution(); - parser->pop_block(wb); - - if (cancel_reason == end_execution_reason_t::control_flow) { - // Handle break or continue. - bool do_break = (ld.loop_status == loop_status_t::breaks); - ld.loop_status = loop_status_t::normals; - if (do_break) { - break; - } else { - continue; - } - } - - // no_exec means that fish was invoked with -n or --no-execute. If set, we allow the loop to - // not-execute once so its contents can be checked, and then break. - if (no_exec()) { - break; - } - } - trace_if_enabled(*parser, L"end while"); - return ret; -} - -// Reports an error. Always returns end_execution_reason_t::error. -end_execution_reason_t parse_execution_context_t::report_error(int status, const ast::node_t &node, - const wchar_t *fmt, ...) const { - auto r = node.source_range(); - - // Create an error. - auto error_list = new_parse_error_list(); - parse_error_t error; - error.source_start = r.start; - error.source_length = r.length; - error.code = parse_error_code_t::syntax; // hackish - - va_list va; - va_start(va, fmt); - error.text = std::make_unique(vformat_string(fmt, va)); - va_end(va); - - error_list->push_back(std::move(error)); - - return this->report_errors(status, *error_list); -} - -end_execution_reason_t parse_execution_context_t::report_errors( - int status, const parse_error_list_t &error_list) const { - if (!ctx.check_cancel()) { - if (error_list.empty()) { - FLOG(error, L"Error reported but no error text found."); - } - - // Get a backtrace. - wcstring backtrace_and_desc; - parser->get_backtrace(pstree->src(), error_list, backtrace_and_desc); - - // Print it. - if (!should_suppress_stderr_for_tests()) { - std::fwprintf(stderr, L"%ls", backtrace_and_desc.c_str()); - } - - // Mark status. - parser->set_last_statuses(statuses_t::just(status)); - } - return end_execution_reason_t::error; -} - -// static -parse_execution_context_t::ast_args_list_t parse_execution_context_t::get_argument_nodes( - const ast::argument_list_t &args) { - ast_args_list_t result; - for (size_t i = 0; i < args.count(); i++) { - const ast::argument_t &arg = *args.at(i); - result.push_back(&arg); - } - return result; -} - -// static -parse_execution_context_t::ast_args_list_t parse_execution_context_t::get_argument_nodes( - const ast::argument_or_redirection_list_t &args) { - ast_args_list_t result; - for (size_t i = 0; i < args.count(); i++) { - const ast::argument_or_redirection_t &v = *args.at(i); - if (v.is_argument()) result.push_back(&v.argument()); - } - return result; -} - -/// Handle the case of command not found. -end_execution_reason_t parse_execution_context_t::handle_command_not_found( - const wcstring &cmd_str, const ast::decorated_statement_t &statement, int err_code) { - // We couldn't find the specified command. This is a non-fatal error. We want to set the exit - // status to 127, which is the standard number used by other shells like bash and zsh. - - const wchar_t *const cmd = cmd_str.c_str(); - if (err_code != ENOENT) { - // TODO: We currently handle all errors here the same, - // but this mainly applies to EACCES. We could also feasibly get: - // ELOOP - // ENAMETOOLONG - if (err_code == ENOTDIR) { - // If the original command did not include a "/", assume we found it via $PATH. - auto src = get_source(*statement.command().ptr()); - if (src.find(L"/") == wcstring::npos) { - return this->report_error(STATUS_NOT_EXECUTABLE, *statement.command().ptr(), - _(L"Unknown command. A component of '%ls' is not a " - L"directory. Check your $PATH."), - cmd); - } else { - return this->report_error( - STATUS_NOT_EXECUTABLE, *statement.command().ptr(), - _(L"Unknown command. A component of '%ls' is not a directory."), cmd); - } - } - - return this->report_error( - STATUS_NOT_EXECUTABLE, *statement.command().ptr(), - _(L"Unknown command. '%ls' exists but is not an executable file."), cmd); - } - - // Handle unrecognized commands with standard command not found handler that can make better - // error messages. - std::vector event_args; - { - ast_args_list_t args = get_argument_nodes(statement.args_or_redirs()); - end_execution_reason_t arg_result = - this->expand_arguments_from_nodes(args, &event_args, failglob); - - if (arg_result != end_execution_reason_t::ok) { - return arg_result; - } - - event_args.insert(event_args.begin(), cmd_str); - } - - wcstring buffer; - wcstring error; - - // Redirect to stderr - auto io = io_chain_t{}; - auto list = new_redirection_spec_list(); - list->push_back(new_redirection_spec(STDOUT_FILENO, redirection_mode_t::fd, L"2")); - io.append_from_specs(*list, L""); - - if (function_exists(L"fish_command_not_found", *parser)) { - buffer = L"fish_command_not_found"; - for (const wcstring &arg : event_args) { - buffer.push_back(L' '); - buffer.append(escape_string(arg)); - } - auto prev_statuses = parser->get_last_statuses(); - - auto event = new_event_generic(L"fish_command_not_found"); - block_t *b = parser->push_block(block_t::event_block(&*event)); - parser->eval(buffer, io); - parser->pop_block(b); - parser->set_last_statuses(std::move(prev_statuses)); - } else { - // If we have no handler, just print it as a normal error. - error = _(L"Unknown command:"); - if (!event_args.empty()) { - error.push_back(L' '); - error.append(escape_string(event_args[0])); - } - } - - if (!cmd_str.empty() && cmd_str.at(0) == L'{') { - error.append(ERROR_NO_BRACE_GROUPING); - } - - // Here we want to report an error (so it shows a backtrace). - // If the handler printed text, that's already shown, so error will be empty. - return this->report_error(STATUS_CMD_UNKNOWN, *statement.command().ptr(), error.c_str()); -} - -end_execution_reason_t parse_execution_context_t::expand_command( - const ast::decorated_statement_t &statement, wcstring *out_cmd, - std::vector *out_args) const { - // Here we're expanding a command, for example $HOME/bin/stuff or $randomthing. The first - // completion becomes the command itself, everything after becomes arguments. Command - // substitutions are not supported. - auto errors = new_parse_error_list(); - - // Get the unexpanded command string. We expect to always get it here. - wcstring unexp_cmd = get_source(*statement.command().ptr()); - size_t pos_of_command_token = statement.command().range().start; - - // Expand the string to produce completions, and report errors. - expand_result_t expand_err = - expand_to_command_and_args(unexp_cmd, ctx, out_cmd, out_args, &*errors); - if (expand_err == expand_result_t::error) { - // Issue #5812 - the expansions were done on the command token, - // excluding prefixes such as " " or "if ". - // This means that the error positions are relative to the beginning - // of the token; we need to make them relative to the original source. - errors->offset_source_start(pos_of_command_token); - return report_errors(STATUS_ILLEGAL_CMD, *errors); - } else if (expand_err == expand_result_t::wildcard_no_match) { - return report_error(STATUS_UNMATCHED_WILDCARD, *statement.ptr(), WILDCARD_ERR_MSG, - get_source(*statement.ptr()).c_str()); - } - assert(expand_err == expand_result_t::ok); - - // Complain if the resulting expansion was empty, or expanded to an empty string. - // For no-exec it's okay, as we can't really perform the expansion. - if (out_cmd->empty() && !no_exec()) { - return this->report_error(STATUS_ILLEGAL_CMD, *statement.command().ptr(), - _(L"The expanded command was empty.")); - } - return end_execution_reason_t::ok; -} - -/// Creates a 'normal' (non-block) process. -end_execution_reason_t parse_execution_context_t::populate_plain_process( - process_t *proc, const ast::decorated_statement_t &statement) { - assert(proc != nullptr); - - // We may decide that a command should be an implicit cd. - bool use_implicit_cd = false; - - // Get the command and any arguments due to expanding the command. - wcstring cmd; - std::vector args_from_cmd_expansion; - auto ret = expand_command(statement, &cmd, &args_from_cmd_expansion); - if (ret != end_execution_reason_t::ok) { - return ret; - } - // For no-exec, having an empty command is okay. We can't do anything more with it tho. - if (no_exec()) return end_execution_reason_t::ok; - assert(!cmd.empty() && "expand_command should not produce an empty command"); - - // Determine the process type. - enum process_type_t process_type = process_type_for_command(statement, cmd); - - get_path_result_t external_cmd{}; - if (process_type == process_type_t::external || process_type == process_type_t::exec) { - // Determine the actual command. This may be an implicit cd. - external_cmd = path_try_get_path(cmd, parser->vars()); - bool has_command = external_cmd.err == 0; - - // If the specified command does not exist, and is undecorated, try using an implicit cd. - if (!has_command && statement.decoration() == statement_decoration_t::none) { - // Implicit cd requires an empty argument and redirection list. - if (statement.args_or_redirs().empty()) { - // Ok, no arguments or redirections; check to see if the command is a directory. - use_implicit_cd = - path_as_implicit_cd(cmd, parser->vars().get_pwd_slash(), parser->vars()) - .has_value(); - } - } - - if (!has_command && !use_implicit_cd) { - return this->handle_command_not_found( - external_cmd.path.empty() ? cmd : external_cmd.path, statement, external_cmd.err); - } - } - - // Produce the full argument list and the set of IO redirections. - std::vector cmd_args; - auto redirections = new_redirection_spec_list(); - if (use_implicit_cd) { - // Implicit cd is simple. - cmd_args = {L"cd", cmd}; - external_cmd = get_path_result_t{}; - - // If we have defined a wrapper around cd, use it, otherwise use the cd builtin. - process_type = - function_exists(L"cd", *parser) ? process_type_t::function : process_type_t::builtin; - } else { - // Not implicit cd. - const globspec_t glob_behavior = - (cmd == L"set" || cmd == L"count" || cmd == L"path") ? nullglob : failglob; - // Form the list of arguments. The command is the first argument, followed by any arguments - // from expanding the command, followed by the argument nodes themselves. E.g. if the - // command is '$gco foo' and $gco is git checkout. - cmd_args.push_back(cmd); - vec_append(cmd_args, std::move(args_from_cmd_expansion)); - - ast_args_list_t arg_nodes = get_argument_nodes(statement.args_or_redirs()); - end_execution_reason_t arg_result = - this->expand_arguments_from_nodes(arg_nodes, &cmd_args, glob_behavior); - if (arg_result != end_execution_reason_t::ok) { - return arg_result; - } - - // The set of IO redirections that we construct for the process. - auto reason = this->determine_redirections(statement.args_or_redirs(), &*redirections); - if (reason != end_execution_reason_t::ok) { - return reason; - } - } - - // Populate the process. - proc->type = process_type; - proc->set_argv(std::move(cmd_args)); - proc->set_redirection_specs(std::move(redirections)); - proc->actual_cmd = std::move(external_cmd.path); - return end_execution_reason_t::ok; -} - -// Determine the list of arguments, expanding stuff. Reports any errors caused by expansion. If we -// have a wildcard that could not be expanded, report the error and continue. -end_execution_reason_t parse_execution_context_t::expand_arguments_from_nodes( - const ast_args_list_t &argument_nodes, std::vector *out_arguments, - globspec_t glob_behavior) { - // Get all argument nodes underneath the statement. We guess we'll have that many arguments (but - // may have more or fewer, if there are wildcards involved). - out_arguments->reserve(out_arguments->size() + argument_nodes.size()); - completion_list_t arg_expanded; - for (const ast::argument_t *arg_node : argument_nodes) { - // Expect all arguments to have source. - assert(arg_node->ptr()->has_source() && "Argument should have source"); - - // Expand this string. - auto errors = new_parse_error_list(); - arg_expanded.clear(); - auto expand_ret = expand_string(get_source(*arg_node->ptr()), &arg_expanded, - expand_flags_t{}, ctx, &*errors); - errors->offset_source_start(arg_node->range().start); - switch (expand_ret.result) { - case expand_result_t::error: { - return this->report_errors(expand_ret.status, *errors); - } - - case expand_result_t::cancel: { - return end_execution_reason_t::cancelled; - } - case expand_result_t::wildcard_no_match: { - if (glob_behavior == failglob) { - // For no_exec, ignore the error - this might work at runtime. - if (no_exec()) return end_execution_reason_t::ok; - // Report the unmatched wildcard error and stop processing. - return report_error(STATUS_UNMATCHED_WILDCARD, *arg_node->ptr(), - WILDCARD_ERR_MSG, get_source(*arg_node->ptr()).c_str()); - } - break; - } - case expand_result_t::ok: { - break; - } - default: { - DIE("unexpected expand_string() return value"); - } - } - - // Now copy over any expanded arguments. Use std::move() to avoid extra allocations; this - // is called very frequently. - out_arguments->reserve(out_arguments->size() + arg_expanded.size()); - for (completion_t &new_arg : arg_expanded) { - out_arguments->push_back(std::move(new_arg.completion)); - } - } - - // We may have received a cancellation during this expansion. - if (auto ret = check_end_execution()) { - return *ret; - } - - return end_execution_reason_t::ok; -} - -end_execution_reason_t parse_execution_context_t::determine_redirections( - const ast::argument_or_redirection_list_t &list, redirection_spec_list_t *out_redirections) { - // Get all redirection nodes underneath the statement. - for (size_t i = 0; i < list.count(); i++) { - const ast::argument_or_redirection_t &arg_or_redir = *list.at(i); - if (!arg_or_redir.is_redirection()) continue; - const ast::redirection_t &redir_node = arg_or_redir.redirection(); - - auto oper = pipe_or_redir_from_string(get_source(*redir_node.oper().ptr()).c_str()); - if (!oper || !oper->is_valid()) { - // TODO: figure out if this can ever happen. If so, improve this error message. - return report_error(STATUS_INVALID_ARGS, *redir_node.ptr(), - _(L"Invalid redirection: %ls"), - get_source(*redir_node.ptr()).c_str()); - } - - // PCA: I can't justify this skip_variables flag. It was like this when I got here. - wcstring target = get_source(*redir_node.target().ptr()); - bool target_expanded = - expand_one(target, no_exec() ? expand_flag::skip_variables : expand_flags_t{}, ctx); - if (!target_expanded || target.empty()) { - // TODO: Improve this error message. - return report_error(STATUS_INVALID_ARGS, *redir_node.ptr(), - _(L"Invalid redirection target: %ls"), target.c_str()); - } - - // Make a redirection spec from the redirect token. - assert(oper && oper->is_valid() && "expected to have a valid redirection"); - auto spec = new_redirection_spec(oper->fd, oper->mode, target.c_str()); - - // Validate this spec. - if (spec->mode() == redirection_mode_t::fd && !spec->is_close() && - !spec->get_target_as_fd()) { - const wchar_t *fmt = - _(L"Requested redirection to '%ls', which is not a valid file descriptor"); - return report_error(STATUS_INVALID_ARGS, *redir_node.ptr(), fmt, - spec->target()->c_str()); - } - out_redirections->push_back(std::move(spec)); - - if (oper->stderr_merge) { - // This was a redirect like &> which also modifies stderr. - // Also redirect stderr to stdout. - out_redirections->push_back(get_stderr_merge()); - } - } - return end_execution_reason_t::ok; -} - -end_execution_reason_t parse_execution_context_t::populate_not_process( - job_t *job, process_t *proc, const ast::not_statement_t ¬_statement) { - auto &flags = job->mut_flags(); - flags.negate = !flags.negate; - return this->populate_job_process(job, proc, not_statement.contents(), - not_statement.variables()); -} - -template -end_execution_reason_t parse_execution_context_t::populate_block_process( - process_t *proc, const ast::statement_t &statement, const Type &specific_statement) { - using namespace ast; - // We handle block statements by creating process_type_t::block_node, that will bounce back to - // us when it's time to execute them. - static_assert(std::is_same::value || - std::is_same::value || - std::is_same::value, - "Invalid block process"); - - // Get the argument or redirections list. - // TODO: args_or_redirs should be available without resolving the statement type. - const argument_or_redirection_list_t *args_or_redirs = nullptr; - - // Upcast to permit dropping the 'template' keyword. - const auto ss = specific_statement.ptr(); - switch (ss->typ()) { - case type_t::block_statement: - args_or_redirs = &ss->as_block_statement().args_or_redirs(); - break; - case type_t::if_statement: - args_or_redirs = &ss->as_if_statement().args_or_redirs(); - break; - case type_t::switch_statement: - args_or_redirs = &ss->as_switch_statement().args_or_redirs(); - break; - default: - DIE("Unexpected block node type"); - } - assert(args_or_redirs && "Should have args_or_redirs"); - - auto redirections = new_redirection_spec_list(); - auto reason = this->determine_redirections(*args_or_redirs, &*redirections); - if (reason == end_execution_reason_t::ok) { - proc->type = process_type_t::block_node; - proc->block_node_source = pstree->clone(); - proc->internal_block_node = &statement; - proc->set_redirection_specs(std::move(redirections)); - } - return reason; -} - -end_execution_reason_t parse_execution_context_t::apply_variable_assignments( - process_t *proc, const ast::variable_assignment_list_t &variable_assignment_list, - const block_t **block) { - if (variable_assignment_list.empty()) return end_execution_reason_t::ok; - *block = parser->push_block(block_t::variable_assignment_block()); - for (size_t i = 0; i < variable_assignment_list.count(); i++) { - const ast::variable_assignment_t &variable_assignment = *variable_assignment_list.at(i); - const wcstring &source = get_source(*variable_assignment.ptr()); - auto equals_pos = variable_assignment_equals_pos(source); - assert(equals_pos); - const wcstring variable_name = source.substr(0, *equals_pos); - const wcstring expression = source.substr(*equals_pos + 1); - completion_list_t expression_expanded; - auto errors = new_parse_error_list(); - // TODO this is mostly copied from expand_arguments_from_nodes, maybe extract to function - auto expand_ret = - expand_string(expression, &expression_expanded, expand_flags_t{}, ctx, &*errors); - errors->offset_source_start(variable_assignment.range().start + *equals_pos + 1); - switch (expand_ret.result) { - case expand_result_t::error: - return this->report_errors(expand_ret.status, *errors); - - case expand_result_t::cancel: - return end_execution_reason_t::cancelled; - - case expand_result_t::wildcard_no_match: // nullglob (equivalent to set) - case expand_result_t::ok: - break; - - default: { - DIE("unexpected expand_string() return value"); - } - } - std::vector vals; - for (auto &completion : expression_expanded) { - vals.emplace_back(std::move(completion.completion)); - } - if (proc) proc->variable_assignments.push_back({variable_name, vals}); - parser->set_var_and_fire(variable_name, ENV_LOCAL | ENV_EXPORT, std::move(vals)); - } - return end_execution_reason_t::ok; -} - -end_execution_reason_t parse_execution_context_t::populate_job_process( - job_t *job, process_t *proc, const ast::statement_t &statement, - const ast::variable_assignment_list_t &variable_assignments) { - using namespace ast; - // Get the "specific statement" which is boolean / block / if / switch / decorated. - const auto specific_statement = statement.contents().ptr(); - - const block_t *block = nullptr; - end_execution_reason_t result = - this->apply_variable_assignments(proc, variable_assignments, &block); - cleanup_t scope([&]() { - if (block) parser->pop_block(block); - }); - if (result != end_execution_reason_t::ok) return result; - - switch (specific_statement->typ()) { - case type_t::not_statement: { - result = this->populate_not_process(job, proc, specific_statement->as_not_statement()); - break; - } - case type_t::block_statement: - result = this->populate_block_process(proc, statement, - specific_statement->as_block_statement()); - break; - case type_t::if_statement: - result = this->populate_block_process(proc, statement, - specific_statement->as_if_statement()); - break; - case type_t::switch_statement: - result = this->populate_block_process(proc, statement, - specific_statement->as_switch_statement()); - break; - case type_t::decorated_statement: { - result = - this->populate_plain_process(proc, specific_statement->as_decorated_statement()); - break; - } - default: { - FLOGF(error, L"'%ls' not handled by new parser yet.", - specific_statement->describe()->c_str()); - PARSER_DIE(); - break; - } - } - - return result; -} - -end_execution_reason_t parse_execution_context_t::populate_job_from_job_node( - job_t *j, const ast::job_pipeline_t &job_node, const block_t *associated_block) { - UNUSED(associated_block); - - // We are going to construct process_t structures for every statement in the job. - // Create processes. Each one may fail. - process_list_t processes; - processes.emplace_back(new process_t()); - end_execution_reason_t result = this->populate_job_process( - j, processes.back().get(), job_node.statement(), job_node.variables()); - - // Construct process_ts for job continuations (pipelines). - for (size_t i = 0; i < job_node.continuation().count(); i++) { - const ast::job_continuation_t &jc = *job_node.continuation().at(i); - if (result != end_execution_reason_t::ok) { - break; - } - // Handle the pipe, whose fd may not be the obvious stdout. - auto parsed_pipe = pipe_or_redir_from_string(get_source(*jc.pipe().ptr()).c_str()); - assert(parsed_pipe && parsed_pipe->is_pipe && "Failed to parse valid pipe"); - if (!parsed_pipe->is_valid()) { - result = report_error(STATUS_INVALID_ARGS, *jc.pipe().ptr(), ILLEGAL_FD_ERR_MSG, - get_source(*jc.pipe().ptr()).c_str()); - break; - } - processes.back()->pipe_write_fd = parsed_pipe->fd; - if (parsed_pipe->stderr_merge) { - // This was a pipe like &| which redirects both stdout and stderr. - // Also redirect stderr to stdout. - auto specs = processes.back()->redirection_specs().clone(); - specs->push_back(get_stderr_merge()); - processes.back()->set_redirection_specs(std::move(specs)); - } - - // Store the new process (and maybe with an error). - processes.emplace_back(new process_t()); - result = - this->populate_job_process(j, processes.back().get(), jc.statement(), jc.variables()); - } - - // Inform our processes of who is first and last - processes.front()->is_first_in_job = true; - processes.back()->is_last_in_job = true; - - // Return what happened. - if (result == end_execution_reason_t::ok) { - // Link up the processes. - assert(!processes.empty()); //!OCLINT(multiple unary operator) - j->processes = std::move(processes); - } - return result; -} - -static bool remove_job(parser_t &parser, const job_t *job) { - for (auto j = parser.jobs().begin(); j != parser.jobs().end(); ++j) { - if (j->get() == job) { - parser.jobs().erase(j); - return true; - } - } - return false; -} - -/// Decide if a job node should be 'time'd. -/// For historical reasons the 'not' and 'time' prefix are "inside out". That is, it's -/// 'not time cmd'. Note that a time appearing anywhere in the pipeline affects the whole job. -/// `sleep 1 | not time true` will time the whole job! -static bool job_node_wants_timing(const ast::job_pipeline_t &job_node) { - // Does our job have the job-level time prefix? - if (job_node.has_time()) return true; - - // Helper to return true if a node is 'not time ...' or 'not not time...' or... - auto is_timed_not_statement = [](const ast::statement_t &stat) { - const auto *ns = stat.contents().ptr()->try_as_not_statement() - ? &stat.contents().ptr()->as_not_statement() - : nullptr; - while (ns) { - if (ns->has_time()) return true; - ns = ns->contents().ptr()->try_as_not_statement() - ? &ns->contents().ptr()->as_not_statement() - : nullptr; - } - return false; - }; - - // Do we have a 'not time ...' anywhere in our pipeline? - if (is_timed_not_statement(job_node.statement())) return true; - for (size_t i = 0; i < job_node.continuation().count(); i++) { - const ast::job_continuation_t &jc = *job_node.continuation().at(i); - if (is_timed_not_statement(jc.statement())) return true; - } - return false; -} - -end_execution_reason_t parse_execution_context_t::run_1_job(const ast::job_pipeline_t &job_node, - const block_t *associated_block) { - if (auto ret = check_end_execution()) { - return *ret; - } - - // We definitely do not want to execute anything if we're told we're --no-execute! - if (no_exec()) return end_execution_reason_t::ok; - - // Increment the eval_level for the duration of this command. - scoped_push saved_eval_level(&parser->eval_level, parser->eval_level + 1); - - // Save the node index. - scoped_push saved_node(&executing_job_node, &job_node); - - // Profiling support. - profile_item_t *profile_item = this->parser->create_profile_item(); - const auto start_time = profile_item ? profile_item_t::now() : 0; - - // When we encounter a block construct (e.g. while loop) in the general case, we create a "block - // process" containing its node. This allows us to handle block-level redirections. - // However, if there are no redirections, then we can just jump into the block directly, which - // is significantly faster. - if (job_is_simple_block(job_node)) { - bool do_time = job_node.has_time(); - auto timer = push_timer(do_time); - const block_t *block = nullptr; - end_execution_reason_t result = - this->apply_variable_assignments(nullptr, job_node.variables(), &block); - cleanup_t scope([&]() { - if (block) parser->pop_block(block); - }); - - const auto specific_statement = job_node.statement().contents().ptr(); - assert(specific_statement_type_is_redirectable_block(*specific_statement)); - if (result == end_execution_reason_t::ok) { - switch (specific_statement->typ()) { - case ast::type_t::block_statement: { - result = this->run_block_statement(specific_statement->as_block_statement(), - associated_block); - break; - } - case ast::type_t::if_statement: { - result = this->run_if_statement(specific_statement->as_if_statement(), - associated_block); - break; - } - case ast::type_t::switch_statement: { - result = this->run_switch_statement(specific_statement->as_switch_statement()); - break; - } - default: { - // Other types should be impossible due to the - // specific_statement_type_is_redirectable_block check. - PARSER_DIE(); - break; - } - } - } - - if (profile_item != nullptr) { - profile_item->duration = profile_item_t::now() - start_time; - profile_item->level = parser->eval_level; - profile_item->cmd = - profiling_cmd_name_for_redirectable_block(*specific_statement, *this->pstree); - profile_item->skipped = false; - } - - return result; - } - - const auto &ld = parser->libdata(); - - job_t::properties_t props{}; - props.initial_background = job_node.has_bg(); - props.skip_notification = - ld.is_subshell || parser->is_block() || ld.is_event || !parser->is_interactive(); - props.from_event_handler = ld.is_event; - props.wants_timing = job_node_wants_timing(job_node); - - // It's an error to have 'time' in a background job. - if (props.wants_timing && props.initial_background) { - return this->report_error(STATUS_INVALID_ARGS, *job_node.ptr(), ERROR_TIME_BACKGROUND); - } - - shared_ptr job = std::make_shared(props, get_source(*job_node.ptr())); - - // We are about to populate a job. One possible argument to the job is a command substitution - // which may be interested in the job that's populating it, via '--on-job-exit caller'. Record - // the job ID here. - scoped_push caller_id(&parser->libdata().caller_id, job->internal_job_id); - - // Populate the job. This may fail for reasons like command_not_found. If this fails, an error - // will have been printed. - end_execution_reason_t pop_result = - this->populate_job_from_job_node(job.get(), job_node, associated_block); - caller_id.restore(); - - // Clean up the job on failure or cancellation. - if (pop_result == end_execution_reason_t::ok) { - this->setup_group(job.get()); - assert(job->group && "Should not have a null group"); - - // Give the job to the parser - it will clean it up. - parser->job_add(job); - - // Actually execute the job. - if (!exec_job(*parser, job, block_io)) { - // No process in the job successfully launched. - // Ensure statuses are set (#7540). - if (auto statuses = job->get_statuses()) { - parser->set_last_statuses(statuses.value()); - parser->libdata().status_count++; - } - remove_job(*this->parser, job.get()); - } - - // Update universal variables on external commands. - // We only incorporate external changes if we had an external proc, for hysterical raisins. - parser->sync_uvars_and_fire(job->has_external_proc() /* always */); - - // If the job got a SIGINT or SIGQUIT, then we're going to start unwinding. - if (!cancel_signal) cancel_signal = job->group->get_cancel_signal(); - } - - if (profile_item != nullptr) { - profile_item->duration = profile_item_t::now() - start_time; - profile_item->level = parser->eval_level; - profile_item->cmd = job ? job->command() : wcstring(); - profile_item->skipped = (pop_result != end_execution_reason_t::ok); - } - - job_reap(*parser, false); // clean up jobs - return pop_result; -} - -end_execution_reason_t parse_execution_context_t::run_job_conjunction( - const ast::job_conjunction_t &job_expr, const block_t *associated_block) { - if (auto reason = check_end_execution()) { - return *reason; - } - end_execution_reason_t result = run_1_job(job_expr.job(), associated_block); - - for (size_t i = 0; i < job_expr.continuations().count(); i++) { - const ast::job_conjunction_continuation_t &jc = *job_expr.continuations().at(i); - if (result != end_execution_reason_t::ok) { - return result; - } - if (auto reason = check_end_execution()) { - return *reason; - } - // Check the conjunction type. - bool skip = false; - switch (jc.conjunction().token_type()) { - case parse_token_type_t::andand: - // AND. Skip if the last job failed. - skip = parser->get_last_status() != 0; - break; - case parse_token_type_t::oror: - // OR. Skip if the last job succeeded. - skip = parser->get_last_status() == 0; - break; - default: - DIE("Unexpected job conjunction type"); - } - if (!skip) { - result = run_1_job(jc.job(), associated_block); - } - } - return result; -} - -end_execution_reason_t parse_execution_context_t::test_and_run_1_job_conjunction( - const ast::job_conjunction_t &jc, const block_t *associated_block) { - // Test this job conjunction if it has an 'and' or 'or' decorator. - // If it passes, then run it. - if (auto reason = check_end_execution()) { - return *reason; - } - // Maybe skip the job if it has a leading and/or. - bool skip = false; - if (jc.has_decorator()) { - switch (jc.decorator().keyword()) { - case parse_keyword_t::kw_and: - // AND. Skip if the last job failed. - skip = parser->get_last_status() != 0; - break; - case parse_keyword_t::kw_or: - // OR. Skip if the last job succeeded. - skip = parser->get_last_status() == 0; - break; - default: - DIE("Unexpected keyword"); - } - } - // Skipping is treated as success. - if (skip) { - return end_execution_reason_t::ok; - } else { - return this->run_job_conjunction(jc, associated_block); - } -} - -end_execution_reason_t parse_execution_context_t::run_job_list(const ast::job_list_t &job_list_node, - const block_t *associated_block) { - auto result = end_execution_reason_t::ok; - for (size_t i = 0; i < job_list_node.count(); i++) { - const ast::job_conjunction_t *jc = job_list_node.at(i); - result = test_and_run_1_job_conjunction(*jc, associated_block); - } - // Returns the result of the last job executed or skipped. - return result; -} - -end_execution_reason_t parse_execution_context_t::run_job_list( - const ast::andor_job_list_t &job_list_node, const block_t *associated_block) { - auto result = end_execution_reason_t::ok; - for (size_t i = 0; i < job_list_node.count(); i++) { - const ast::andor_job_t *aoj = job_list_node.at(i); - result = test_and_run_1_job_conjunction(aoj->job(), associated_block); - } - // Returns the result of the last job executed or skipped. - return result; -} - -end_execution_reason_t parse_execution_context_t::eval_node(const ast::statement_t &statement, - const block_t *associated_block) { - // Note we only expect block-style statements here. No not statements. - enum end_execution_reason_t status = end_execution_reason_t::ok; - const auto contents = statement.contents().ptr(); - if (const auto *block = contents->try_as_block_statement()) { - status = this->run_block_statement(*block, associated_block); - } else if (const auto *ifstat = contents->try_as_if_statement()) { - status = this->run_if_statement(*ifstat, associated_block); - } else if (const auto *switchstat = contents->try_as_switch_statement()) { - status = this->run_switch_statement(*switchstat); - } else { - FLOGF(error, L"Unexpected node %ls found in %s", statement.describe()->c_str(), - __FUNCTION__); - abort(); - } - return status; -} - -end_execution_reason_t parse_execution_context_t::eval_node(const ast::job_list_t &job_list, - const block_t *associated_block) { - assert(associated_block && "Null block"); - - // Check for infinite recursion: a function which immediately calls itself.. - wcstring func_name; - if (const auto *infinite_recursive_node = - this->infinite_recursive_statement_in_job_list(job_list, &func_name)) { - // We have an infinite recursion. - return this->report_error(STATUS_CMD_ERROR, *infinite_recursive_node->ptr(), - INFINITE_FUNC_RECURSION_ERR_MSG, func_name.c_str()); - } - - // Check for stack overflow in case of function calls (regular stack overflow) or string - // substitution blocks, which can be recursively called with eval (issue #9302). - if ((associated_block->type() == block_type_t::top && - parser->function_stack_is_overflowing()) || - (associated_block->type() == block_type_t::subst && parser->is_eval_depth_exceeded())) { - return this->report_error(STATUS_CMD_ERROR, *job_list.ptr(), - CALL_STACK_LIMIT_EXCEEDED_ERR_MSG); - } - return this->run_job_list(job_list, associated_block); -} - -void parse_execution_context_t::setup_group(job_t *j) { - // We can use the parent group if it's compatible and we're not backgrounded. - if (ctx.job_group && (ctx.job_group->has_job_id() || !j->wants_job_id()) && - !j->is_initially_background()) { - j->group = ctx.job_group; - return; - } - - if (j->processes.front()->is_internal() || !this->use_job_control()) { - // This job either doesn't have a pgroup (e.g. a simple block), or lives in fish's pgroup. - rust::Box group = create_job_group_ffi(j->command(), j->wants_job_id()); - j->group = box_to_shared_ptr(std::move(group)); - } else { - // This is a "real job" that gets its own pgroup. - j->processes.front()->leads_pgrp = true; - bool wants_terminal = !parser->libdata().is_event; - auto group = create_job_group_with_job_control_ffi(j->command(), wants_terminal); - j->group = box_to_shared_ptr(std::move(group)); - } - j->group->set_is_foreground(!j->is_initially_background()); - j->mut_flags().is_group_root = true; -} - -bool parse_execution_context_t::use_job_control() const { - if (parser->is_command_substitution()) { - return false; - } - switch (get_job_control_mode()) { - case job_control_t::all: - return true; - case job_control_t::interactive: - return parser->is_interactive(); - case job_control_t::none: - return false; - } - DIE("Unreachable"); -} - -int parse_execution_context_t::line_offset_of_node(const ast::job_pipeline_t *node) { - // If we're not executing anything, return -1. - if (!node) { - return -1; - } - - // If for some reason we're executing a node without source, return -1. - if (!node->try_source_range()) { - return -1; - } - - return this->line_offset_of_character_at_offset(node->source_range().start); -} - -int parse_execution_context_t::line_offset_of_character_at_offset(size_t offset) { - // Count the number of newlines, leveraging our cache. - assert(offset <= pstree->src().size()); - - // Easy hack to handle 0. - if (offset == 0) { - return 0; - } - - // We want to return (one plus) the number of newlines at offsets less than the given offset. - // cached_lineno_count is the number of newlines at indexes less than cached_lineno_offset. - const wcstring &str = pstree->src(); - if (offset > cached_lineno_offset) { - size_t i; - for (i = cached_lineno_offset; i < offset && str[i] != L'\0'; i++) { - // Add one for every newline we find in the range [cached_lineno_offset, offset). - if (str[i] == L'\n') { - cached_lineno_count++; - } - } - cached_lineno_offset = - i; // note: i, not offset, in case offset is beyond the length of the string - } else if (offset < cached_lineno_offset) { - // Subtract one for every newline we find in the range [offset, cached_lineno_offset). - for (size_t i = offset; i < cached_lineno_offset; i++) { - if (str[i] == L'\n') { - cached_lineno_count--; - } - } - cached_lineno_offset = offset; - } - return cached_lineno_count; -} - -int parse_execution_context_t::get_current_line_number() { - int line_number = -1; - int line_offset = this->line_offset_of_node(this->executing_job_node); - if (line_offset >= 0) { - // The offset is 0 based; the number is 1 based. - line_number = line_offset + 1; - } - return line_number; -} - -int parse_execution_context_t::get_current_source_offset() const { - int result = -1; - if (executing_job_node) { - if (executing_job_node->try_source_range()) { - result = static_cast(executing_job_node->source_range().start); - } - } - return result; -} diff --git a/src/parse_execution.h b/src/parse_execution.h deleted file mode 100644 index 13c6eb27b..000000000 --- a/src/parse_execution.h +++ /dev/null @@ -1,188 +0,0 @@ -// Provides the "linkage" between an ast and actual execution structures (job_t, etc.). -#ifndef FISH_PARSE_EXECUTION_H -#define FISH_PARSE_EXECUTION_H - -#include - -#include - -#include "ast.h" // IWYU pragma: keep -#include "common.h" -#include "io.h" -#include "maybe.h" -#include "parse_constants.h" -#include "parse_tree.h" -#include "proc.h" -#include "redirection.h" - -class block_t; -class operation_context_t; -class Parser; using parser_t = Parser; - -/// An eval_result represents evaluation errors including wildcards which failed to match, syntax -/// errors, or other expansion errors. It also tracks when evaluation was skipped due to signal -/// cancellation. Note it does not track the exit status of commands. -enum class end_execution_reason_t { - /// Evaluation was successfull. - ok, - - /// Evaluation was skipped due to control flow (break or return). - control_flow, - - /// Evaluation was cancelled, e.g. because of a signal or exit. - cancelled, - - /// A parse error or failed expansion (but not an error exit status from a command). - error, -}; - -class parse_execution_context_t : noncopyable_t { - private: - rust::Box pstree; - parser_t *const parser; - const operation_context_t &ctx; - - // If set, one of our processes received a cancellation signal (INT or QUIT) so we are - // unwinding. - int cancel_signal{0}; - - // The currently executing job node, used to indicate the line number. - const ast::job_pipeline_t *executing_job_node{}; - - // Cached line number information. - size_t cached_lineno_offset = 0; - int cached_lineno_count = 0; - - /// The block IO chain. - /// For example, in `begin; foo ; end < file.txt` this would have the 'file.txt' IO. - io_chain_t block_io{}; - - // Check to see if we should end execution. - // \return the eval result to end with, or none() to continue on. - // This will never return end_execution_reason_t::ok. - maybe_t check_end_execution() const; - - // Report an error, setting $status to \p status. Always returns - // 'end_execution_reason_t::error'. - end_execution_reason_t report_error(int status, const ast::node_t &node, const wchar_t *fmt, - ...) const; - end_execution_reason_t report_errors(int status, const parse_error_list_t &error_list) const; - - /// Command not found support. - end_execution_reason_t handle_command_not_found(const wcstring &cmd, - const ast::decorated_statement_t &statement, - int err_code); - - // Utilities - wcstring get_source(const ast::node_t &node) const; - const ast::decorated_statement_t *infinite_recursive_statement_in_job_list( - const ast::job_list_t &jobs, wcstring *out_func_name) const; - - // Expand a command which may contain variables, producing an expand command and possibly - // arguments. Prints an error message on error. - end_execution_reason_t expand_command(const ast::decorated_statement_t &statement, - wcstring *out_cmd, std::vector *out_args) const; - - /// Indicates whether a job is a simple block (one block, no redirections). - bool job_is_simple_block(const ast::job_pipeline_t &job) const; - - enum process_type_t process_type_for_command(const ast::decorated_statement_t &statement, - const wcstring &cmd) const; - end_execution_reason_t apply_variable_assignments( - process_t *proc, const ast::variable_assignment_list_t &variable_assignment_list, - const block_t **block); - - // These create process_t structures from statements. - end_execution_reason_t populate_job_process( - job_t *job, process_t *proc, const ast::statement_t &statement, - const ast::variable_assignment_list_t &variable_assignments_list_t); - end_execution_reason_t populate_not_process(job_t *job, process_t *proc, - const ast::not_statement_t ¬_statement); - end_execution_reason_t populate_plain_process(process_t *proc, - const ast::decorated_statement_t &statement); - - template - end_execution_reason_t populate_block_process(process_t *proc, - const ast::statement_t &statement, - const Type &specific_statement); - - // These encapsulate the actual logic of various (block) statements. - end_execution_reason_t run_block_statement(const ast::block_statement_t &statement, - const block_t *associated_block); - end_execution_reason_t run_for_statement(const ast::for_header_t &header, - const ast::job_list_t &contents); - end_execution_reason_t run_if_statement(const ast::if_statement_t &statement, - const block_t *associated_block); - end_execution_reason_t run_switch_statement(const ast::switch_statement_t &statement); - end_execution_reason_t run_while_statement(const ast::while_header_t &header, - const ast::job_list_t &contents, - const block_t *associated_block); - end_execution_reason_t run_function_statement(const ast::block_statement_t &statement, - const ast::function_header_t &header); - end_execution_reason_t run_begin_statement(const ast::job_list_t &contents); - - enum globspec_t { failglob, nullglob }; - using ast_args_list_t = std::vector; - - static ast_args_list_t get_argument_nodes(const ast::argument_list_t &args); - static ast_args_list_t get_argument_nodes(const ast::argument_or_redirection_list_t &args); - - end_execution_reason_t expand_arguments_from_nodes(const ast_args_list_t &argument_nodes, - std::vector *out_arguments, - globspec_t glob_behavior); - - // Determines the list of redirections for a node. - end_execution_reason_t determine_redirections(const ast::argument_or_redirection_list_t &list, - redirection_spec_list_t *out_redirections); - - end_execution_reason_t run_1_job(const ast::job_pipeline_t &job, - const block_t *associated_block); - end_execution_reason_t test_and_run_1_job_conjunction(const ast::job_conjunction_t &jc, - const block_t *associated_block); - end_execution_reason_t run_job_conjunction(const ast::job_conjunction_t &job_expr, - const block_t *associated_block); - end_execution_reason_t run_job_list(const ast::job_list_t &job_list_node, - const block_t *associated_block); - end_execution_reason_t run_job_list(const ast::andor_job_list_t &job_list_node, - const block_t *associated_block); - end_execution_reason_t populate_job_from_job_node(job_t *j, const ast::job_pipeline_t &job_node, - const block_t *associated_block); - - // Assign a job group to the given job. - void setup_group(job_t *j); - - // \return whether we should apply job control to our processes. - bool use_job_control() const; - - // Returns the line number of the node. Not const since it touches cached_lineno_offset. - int line_offset_of_node(const ast::job_pipeline_t *node); - int line_offset_of_character_at_offset(size_t offset); - - public: - /// Construct a context in preparation for evaluating a node in a tree, with the given block_io. - /// The execution context may access the parser and parent job group (if any) through ctx. - parse_execution_context_t(rust::Box pstree, const operation_context_t &ctx, - io_chain_t block_io); - - /// Returns the current line number, indexed from 1. Not const since it touches - /// cached_lineno_offset. - int get_current_line_number(); - - /// Returns the source offset, or -1. - int get_current_source_offset() const; - - /// Returns the source string. - const wcstring &get_source() const { return pstree->src(); } - - /// Return the parsed ast. - const ast::ast_t &ast() const { return pstree->ast(); } - - /// Start executing at the given node. Returns 0 if there was no error, 1 if there was an - /// error. - end_execution_reason_t eval_node(const ast::statement_t &statement, - const block_t *associated_block); - end_execution_reason_t eval_node(const ast::job_list_t &job_list, - const block_t *associated_block); -}; - -#endif diff --git a/src/parse_util.cpp b/src/parse_util.cpp index 8b32728cf..4c1b0db3a 100644 --- a/src/parse_util.cpp +++ b/src/parse_util.cpp @@ -16,7 +16,6 @@ #include #include "ast.h" -#include "builtin.h" #include "common.h" #include "expand.h" #include "fallback.h" // IWYU pragma: keep @@ -41,9 +40,6 @@ /// Error message for arguments to 'end' #define END_ARG_ERR_MSG _(L"'end' does not take arguments. Did you forget a ';'?") -/// Error message when 'time' is in a pipeline. -#define TIME_IN_PIPELINE_ERR_MSG _(L"The 'time' command may only be at the beginning of a pipeline") - /// Maximum length of a variable name to show in error reports before truncation static constexpr int var_err_len = 16; @@ -616,13 +612,6 @@ static bool append_syntax_error(parse_error_list_t *errors, size_t source_locati return true; } -/// Returns true if the specified command is a builtin that may not be used in a pipeline. -static const wchar_t *const forbidden_pipe_commands[] = {L"exec", L"case", L"break", L"return", - L"continue"}; -static bool parser_is_pipe_forbidden(const wcstring &word) { - return contains(forbidden_pipe_commands, word); -} - bool parse_util_argument_is_help(const wcstring &s) { return s == L"-h" || s == L"--help"; } // \return a pointer to the first argument node of an argument_or_redirection_list_t, or nullptr if @@ -751,7 +740,8 @@ parser_test_error_bits_t parse_util_detect_errors_in_argument(const ast::argumen parser_test_error_bits_t err = 0; auto check_subtoken = [&arg_src, &out_errors, source_start](size_t begin, size_t end) -> int { - auto maybe_unesc = unescape_string(arg_src.c_str() + begin, end - begin, UNESCAPE_SPECIAL); + auto maybe_unesc = unescape_string(arg_src.c_str() + begin, end - begin, UNESCAPE_SPECIAL, + STRING_STYLE_SCRIPT); if (!maybe_unesc) { if (out_errors) { const wchar_t *fmt = L"Invalid token '%ls'"; @@ -898,165 +888,6 @@ static bool detect_errors_in_backgrounded_job(const ast::job_pipeline_t &job, return errored; } -/// Given a source buffer \p buff_src and decorated statement \p dst within it, return true if there -/// is an error and false if not. \p storage may be used to reduce allocations. -static bool detect_errors_in_decorated_statement(const wcstring &buff_src, - const ast::decorated_statement_t &dst, - wcstring *storage, - parse_error_list_t *parse_errors) { - using namespace ast; - bool errored = false; - auto source_start = dst.source_range().start; - auto source_length = dst.source_range().length; - const statement_decoration_t decoration = dst.decoration(); - - // Determine if the first argument is help. - bool first_arg_is_help = false; - if (const auto *arg = get_first_arg(dst.args_or_redirs())) { - wcstring arg_src = *arg->source(buff_src); - *storage = arg_src; - first_arg_is_help = parse_util_argument_is_help(arg_src); - } - - // Get the statement we are part of. - const statement_t &st = dst.ptr()->parent()->as_statement(); - - // Walk up to the job. - const ast::job_pipeline_t *job = nullptr; - for (auto cursor = dst.ptr()->parent(); job == nullptr; cursor = cursor->parent()) { - assert(cursor->has_value() && "Reached root without finding a job"); - job = cursor->try_as_job_pipeline(); - } - assert(job && "Should have found the job"); - - // Check our pipeline position. - pipeline_position_t pipe_pos; - if (job->continuation().empty()) { - pipe_pos = pipeline_position_t::none; - } else if (&job->statement() == &st) { - pipe_pos = pipeline_position_t::first; - } else { - pipe_pos = pipeline_position_t::subsequent; - } - - // Check that we don't try to pipe through exec. - bool is_in_pipeline = (pipe_pos != pipeline_position_t::none); - if (is_in_pipeline && decoration == statement_decoration_t::exec) { - errored = append_syntax_error(parse_errors, source_start, source_length, - INVALID_PIPELINE_CMD_ERR_MSG, L"exec"); - } - - // This is a somewhat stale check that 'and' and 'or' are not in pipelines, except at the - // beginning. We can't disallow them as commands entirely because we need to support 'and - // --help', etc. - if (pipe_pos == pipeline_position_t::subsequent) { - // We only reject it if we have no decoration. - // `echo foo | command time something` - // is entirely fair and valid. - // Other current decorations like "exec" - // are already forbidden. - const auto &deco = dst.decoration(); - if (deco == statement_decoration_t::none) { - // check if our command is 'and' or 'or'. This is very clumsy; we don't catch e.g. quoted - // commands. - wcstring command = *dst.command().source(buff_src); - *storage = command; - if (command == L"and" || command == L"or") { - errored = append_syntax_error(parse_errors, source_start, source_length, - INVALID_PIPELINE_CMD_ERR_MSG, command.c_str()); - } - - // Similarly for time (#8841). - if (command == L"time") { - errored = append_syntax_error(parse_errors, source_start, source_length, - TIME_IN_PIPELINE_ERR_MSG); - } - } - } - - // $status specifically is invalid as a command, - // to avoid people trying `if $status`. - // We see this surprisingly regularly. - wcstring com = *dst.command().source(buff_src); - *storage = com; - if (com == L"$status") { - errored = - append_syntax_error(parse_errors, source_start, source_length, - _(L"$status is not valid as a command. See `help conditions`")); - } - - wcstring unexp_command = *dst.command().source(buff_src); - *storage = unexp_command; - if (!unexp_command.empty()) { - // Check that we can expand the command. - // Make a new error list so we can fix the offset for just those, then append later. - wcstring command; - auto new_errors = new_parse_error_list(); - if (expand_to_command_and_args(unexp_command, operation_context_t::empty(), &command, - nullptr, &*new_errors, - true /* skip wildcards */) == expand_result_t::error) { - errored = true; - } - - // Check that pipes are sound. - if (!errored && parser_is_pipe_forbidden(command) && is_in_pipeline) { - errored = append_syntax_error(parse_errors, source_start, source_length, - INVALID_PIPELINE_CMD_ERR_MSG, command.c_str()); - } - - // Check that we don't break or continue from outside a loop. - if (!errored && (command == L"break" || command == L"continue") && !first_arg_is_help) { - // Walk up until we hit a 'for' or 'while' loop. If we hit a function first, - // stop the search; we can't break an outer loop from inside a function. - // This is a little funny because we can't tell if it's a 'for' or 'while' - // loop from the ancestor alone; we need the header. That is, we hit a - // block_statement, and have to check its header. - bool found_loop = false; - for (auto ancestor = dst.ptr(); ancestor->has_value(); ancestor = ancestor->parent()) { - const auto *block = ancestor->try_as_block_statement(); - if (!block) continue; - if (block->header().ptr()->typ() == type_t::for_header || - block->header().ptr()->typ() == type_t::while_header) { - // This is a loop header, so we can break or continue. - found_loop = true; - break; - } else if (block->header().ptr()->typ() == type_t::function_header) { - // This is a function header, so we cannot break or - // continue. We stop our search here. - found_loop = false; - break; - } - } - - if (!found_loop) { - errored = append_syntax_error( - parse_errors, source_start, source_length, - (command == L"break" ? INVALID_BREAK_ERR_MSG : INVALID_CONTINUE_ERR_MSG)); - } - } - - // Check that we don't do an invalid builtin (issue #1252). - if (!errored && decoration == statement_decoration_t::builtin) { - wcstring command = unexp_command; - if (expand_one(command, expand_flag::skip_cmdsubst, operation_context_t::empty(), - parse_errors) && - !builtin_exists(unexp_command)) { - errored = append_syntax_error(parse_errors, source_start, source_length, - UNKNOWN_BUILTIN_ERR_MSG, unexp_command.c_str()); - } - } - - if (parse_errors) { - // The expansion errors here go from the *command* onwards, - // so we need to offset them by the *command* offset, - // excluding the decoration. - new_errors->offset_source_start(dst.command().source_range().start); - parse_errors->append(&*new_errors); - } - } - return errored; -} - // Given we have a trailing argument_or_redirection_list, like `begin; end > /dev/null`, verify that // there are no arguments in the list. static bool detect_errors_in_block_redirection_list( @@ -1068,8 +899,9 @@ static bool detect_errors_in_block_redirection_list( return false; } -parser_test_error_bits_t parse_util_detect_errors_ffi(const ast::ast_t* ast, const wcstring &buff_src, - parse_error_list_t *out_errors) { +parser_test_error_bits_t parse_util_detect_errors_ffi(const ast::ast_t *ast, + const wcstring &buff_src, + parse_error_list_t *out_errors) { return parse_util_detect_errors(*ast, buff_src, out_errors); } @@ -1130,7 +962,8 @@ parser_test_error_bits_t parse_util_detect_errors(const ast::ast_t &ast, const w errored |= detect_errors_in_backgrounded_job(*job, out_errors); } } else if (const auto *stmt = node->try_as_decorated_statement()) { - errored |= detect_errors_in_decorated_statement(buff_src, *stmt, &storage, out_errors); + errored |= + detect_errors_in_decorated_statement(buff_src, (size_t)stmt, (size_t)out_errors); } else if (const auto *block = node->try_as_block_statement()) { // If our 'end' had no source, we are unsourced. if (!block->end().ptr()->has_source()) has_unclosed_block = true; diff --git a/src/parse_util.h b/src/parse_util.h index 9d231b23d..459441a6d 100644 --- a/src/parse_util.h +++ b/src/parse_util.h @@ -11,6 +11,9 @@ #include "cxx.h" #include "maybe.h" #include "parse_constants.h" +#if INCLUDE_RUST_HEADERS +#include "parse_util.rs.h" +#endif struct Tok; using tok_t = Tok; diff --git a/src/parser.cpp b/src/parser.cpp deleted file mode 100644 index 6b2a47dd0..000000000 --- a/src/parser.cpp +++ /dev/null @@ -1,878 +0,0 @@ -// The fish parser. Contains functions for parsing and evaluating code. -#include "config.h" // IWYU pragma: keep - -#include "parser.h" - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "ast.h" -#include "common.h" -#include "complete.h" -#include "env.h" -#include "event.h" -#include "expand.h" -#include "fallback.h" // IWYU pragma: keep -#include "fds.h" -#include "flog.h" -#include "function.h" -#include "job_group.rs.h" -#include "parse_constants.h" -#include "parse_execution.h" -#include "proc.h" -#include "signals.h" -#include "threads.rs.h" -#include "wutil.h" // IWYU pragma: keep - -class io_chain_t; - -// Given a file path, return something nicer. Currently we just "unexpand" tildes. -static wcstring user_presentable_path(const wcstring &path, const environment_t &vars) { - return replace_home_directory_with_tilde(path, vars); -} - -parser_t::Parser(std::shared_ptr vars, bool is_principal) - : wait_handles(new_wait_handle_store_ffi()), - variables(std::move(vars)), - is_principal_(is_principal) { - assert(variables.get() && "Null variables in parser initializer"); - int cwd = open_cloexec(".", O_RDONLY); - if (cwd < 0) { - perror("Unable to open the current working directory"); - return; - } - libdata().cwd_fd = std::make_shared(cwd); -} - -// Out of line destructor to enable forward declaration of parse_execution_context_t -parser_t::~Parser() = default; - -parser_t &parser_t::principal_parser() { - static const std::shared_ptr principal{ - new parser_t(env_stack_t::principal_ref(), true)}; - principal->assert_can_execute(); - return *principal; -} - -parser_t *parser_t::principal_parser_ffi() { return &principal_parser(); } - -void parser_t::assert_can_execute() const { ASSERT_IS_MAIN_THREAD(); } - -rust::Box &parser_t::get_wait_handles_ffi() { return wait_handles; } - -const rust::Box &parser_t::get_wait_handles_ffi() const { return wait_handles; } - -int parser_t::set_var_and_fire(const wcstring &key, env_mode_flags_t mode, - std::vector vals) { - int res = vars().set(key, mode, std::move(vals)); - if (res == ENV_OK) { - event_fire(*this, *new_event_variable_set(key)); - } - return res; -} - -int parser_t::set_var_and_fire(const wcstring &key, env_mode_flags_t mode, wcstring val) { - std::vector vals; - vals.push_back(std::move(val)); - return set_var_and_fire(key, mode, std::move(vals)); -} - -void parser_t::sync_uvars_and_fire(bool always) { - if (this->syncs_uvars_) { - auto evts = this->vars().universal_sync(always); - for (const auto &evt : evts) { - event_fire(*this, *evt); - } - } -} - -block_t *parser_t::push_block(block_t &&block) { - block.src_lineno = parser_t::get_lineno(); - block.src_filename = parser_t::current_filename(); - if (block.type() != block_type_t::top) { - bool new_scope = (block.type() == block_type_t::function_call); - vars().push(new_scope); - block.wants_pop_env = true; - } - - // Push it onto our list and return a pointer to it. - // Note that deques do not move their contents so this is safe. - this->block_list.push_front(std::move(block)); - return &this->block_list.front(); -} - -void parser_t::pop_block(const block_t *expected) { - assert(expected && expected == &this->block_list.at(0) && "Unexpected block"); - bool pop_env = expected->wants_pop_env; - block_list.pop_front(); // beware, this deallocates 'expected'. - if (pop_env) vars().pop(); -} - -const block_t *parser_t::block_at_index(size_t idx) const { - return idx < block_list.size() ? &block_list[idx] : nullptr; -} - -block_t *parser_t::block_at_index(size_t idx) { - return idx < block_list.size() ? &block_list[idx] : nullptr; -} - -/// Print profiling information to the specified stream. -static void print_profile(const std::deque &items, FILE *out) { - for (size_t idx = 0; idx < items.size(); idx++) { - const profile_item_t &item = items.at(idx); - if (item.skipped || item.cmd.empty()) continue; - - long long total_time = item.duration; - - // Compute the self time as the total time, minus the total time consumed by subsequent - // items exactly one eval level deeper. - long long self_time = item.duration; - for (size_t i = idx + 1; i < items.size(); i++) { - const profile_item_t &nested_item = items.at(i); - if (nested_item.skipped) continue; - - // If the eval level is not larger, then we have exhausted nested items. - if (nested_item.level <= item.level) break; - - // If the eval level is exactly one more than our level, it is a directly nested item. - if (nested_item.level == item.level + 1) self_time -= nested_item.duration; - } - - if (std::fwprintf(out, L"%lld\t%lld\t", self_time, total_time) < 0) { - wperror(L"fwprintf"); - return; - } - - for (size_t i = 0; i < item.level; i++) { - if (std::fwprintf(out, L"-") < 0) { - wperror(L"fwprintf"); - return; - } - } - - if (std::fwprintf(out, L"> %ls\n", item.cmd.c_str()) < 0) { - wperror(L"fwprintf"); - return; - } - } -} - -void parser_t::clear_profiling() { profile_items.clear(); } - -void parser_t::emit_profiling(const char *path) const { - // Save profiling information. OK to not use CLO_EXEC here because this is called while fish is - // exiting (and hence will not fork). - FILE *f = fopen(path, "w"); - if (!f) { - FLOGF(warning, _(L"Could not write profiling information to file '%s'"), path); - } else { - if (std::fwprintf(f, _(L"Time\tSum\tCommand\n"), profile_items.size()) < 0) { - wperror(L"fwprintf"); - } else { - print_profile(profile_items, f); - } - - if (fclose(f)) { - wperror(L"fclose"); - } - } -} - -completion_list_t parser_t::expand_argument_list(const wcstring &arg_list_src, - expand_flags_t eflags, - const operation_context_t &ctx) { - // Parse the string as an argument list. - auto ast = ast_parse_argument_list(arg_list_src); - if (ast->errored()) { - // Failed to parse. Here we expect to have reported any errors in test_args. - return {}; - } - - // Get the root argument list and extract arguments from it. - completion_list_t result; - const ast::freestanding_argument_list_t &list = ast->top()->as_freestanding_argument_list(); - for (size_t i = 0; i < list.arguments().count(); i++) { - const ast::argument_t &arg = *list.arguments().at(i); - wcstring arg_src = *arg.source(arg_list_src); - if (expand_string(arg_src, &result, eflags, ctx) == expand_result_t::error) { - break; // failed to expand a string - } - } - return result; -} - -void parser_t::set_cwd_fd(int fd) { - assert(fd >= 0 && "Invalid fd"); - this->libdata().cwd_fd = std::make_shared(fd); -} - -std::shared_ptr parser_t::shared() { return shared_from_this(); } - -cancel_checker_t parser_t::cancel_checker() const { - return [] { return signal_check_cancel() != 0; }; -} - -operation_context_t parser_t::context() { - return operation_context_t{this->shared(), this->vars(), this->cancel_checker()}; -} - -/// Append stack trace info for the block \p b to \p trace. -static void append_block_description_to_stack_trace(const parser_t &parser, const block_t &b, - wcstring &trace) { - bool print_call_site = false; - switch (b.type()) { - case block_type_t::function_call: - case block_type_t::function_call_no_shadow: { - append_format(trace, _(L"in function '%ls'"), b.function_name.c_str()); - // Print arguments on the same line. - wcstring args_str; - for (const wcstring &arg : b.function_args) { - if (!args_str.empty()) args_str.push_back(L' '); - // We can't quote the arguments because we print this in quotes. - // As a special-case, add the empty argument as "". - if (!arg.empty()) { - args_str.append(escape_string(arg, ESCAPE_NO_QUOTED)); - } else { - args_str.append(L"\"\""); - } - } - if (!args_str.empty()) { - // TODO: Escape these. - append_format(trace, _(L" with arguments '%ls'"), args_str.c_str()); - } - trace.push_back('\n'); - print_call_site = true; - break; - } - case block_type_t::subst: { - append_format(trace, _(L"in command substitution\n")); - print_call_site = true; - break; - } - case block_type_t::source: { - const filename_ref_t &source_dest = b.sourced_file; - append_format(trace, _(L"from sourcing file %ls\n"), - user_presentable_path(*source_dest, parser.vars()).c_str()); - print_call_site = true; - break; - } - case block_type_t::event: { - assert(b.event && "Should have an event"); - wcstring description = *event_get_desc(parser, **b.event); - append_format(trace, _(L"in event handler: %ls\n"), description.c_str()); - print_call_site = true; - break; - } - - case block_type_t::top: - case block_type_t::begin: - case block_type_t::switch_block: - case block_type_t::while_block: - case block_type_t::for_block: - case block_type_t::if_block: - case block_type_t::breakpoint: - case block_type_t::variable_assignment: - break; - } - - if (print_call_site) { - // Print where the function is called. - const auto &file = b.src_filename; - if (file) { - append_format(trace, _(L"\tcalled on line %d of file %ls\n"), b.src_lineno, - user_presentable_path(*file, parser.vars()).c_str()); - } else if (parser.libdata().within_fish_init) { - append_format(trace, _(L"\tcalled during startup\n")); - } - } -} - -wcstring parser_t::stack_trace() const { - wcstring trace; - for (const auto &b : blocks()) { - append_block_description_to_stack_trace(*this, b, trace); - - // Stop at event handler. No reason to believe that any other code is relevant. - // - // It might make sense in the future to continue printing the stack trace of the code - // that invoked the event, if this is a programmatic event, but we can't currently - // detect that. - if (b.type() == block_type_t::event) break; - } - return trace; -} - -bool parser_t::is_function() const { - for (const auto &b : block_list) { - if (b.is_function_call()) { - return true; - } else if (b.type() == block_type_t::source) { - // If a function sources a file, don't descend further. - break; - } - } - return false; -} - -bool parser_t::is_block() const { - // Note historically this has descended into 'source', unlike 'is_function'. - for (const auto &b : block_list) { - if (b.type() != block_type_t::top && b.type() != block_type_t::subst) { - return true; - } - } - return false; -} - -bool parser_t::is_breakpoint() const { - for (const auto &b : block_list) { - if (b.type() == block_type_t::breakpoint) { - return true; - } - } - return false; -} - -bool parser_t::is_command_substitution() const { - for (const auto &b : block_list) { - if (b.type() == block_type_t::subst) { - return true; - } else if (b.type() == block_type_t::source) { - // If a function sources a file, don't descend further. - break; - } - } - return false; -} - -wcstring parser_t::get_function_name_ffi(int level) { - auto name = get_function_name(level); - if (name.has_value()) { - return name.acquire(); - } else { - return wcstring(); - } -} - -maybe_t parser_t::get_function_name(int level) { - if (level == 0) { - // Return the function name for the level preceding the most recent breakpoint. If there - // isn't one return the function name for the current level. - // Walk until we find a breakpoint, then take the next function. - bool found_breakpoint = false; - for (const auto &b : block_list) { - if (b.type() == block_type_t::breakpoint) { - found_breakpoint = true; - } else if (found_breakpoint && b.is_function_call()) { - return b.function_name; - } - } - return none(); // couldn't find a breakpoint frame - } - - // Level 1 is the topmost function call. Level 2 is its caller. Etc. - int funcs_seen = 0; - for (const auto &b : block_list) { - if (b.is_function_call()) { - funcs_seen++; - if (funcs_seen == level) { - return b.function_name; - } - } else if (b.type() == block_type_t::source && level == 1) { - // Historical: If we want the topmost function, but we are really in a file sourced by a - // function, don't consider ourselves to be in a function. - break; - } - } - return none(); -} - -int parser_t::get_lineno() const { - int lineno = -1; - if (execution_context) { - lineno = execution_context->get_current_line_number(); - } - return lineno; -} - -filename_ref_t parser_t::current_filename() const { - for (const auto &b : block_list) { - if (b.is_function_call()) { - auto props = function_get_props(b.function_name); - return props ? (*props)->definition_file() : nullptr; - } else if (b.type() == block_type_t::source) { - return b.sourced_file; - } - } - // Fall back to the file being sourced. - return libdata().current_filename; -} - -void parser_t::set_filename_ffi(wcstring filename) { - libdata().current_filename = std::make_shared(filename); -} - -// FFI glue -wcstring parser_t::current_filename_ffi() const { - auto filename = current_filename(); - if (filename) { - return wcstring(*filename); - } else { - return wcstring(); - } -} - -bool parser_t::function_stack_is_overflowing() const { - // We are interested in whether the count of functions on the stack exceeds - // FISH_MAX_STACK_DEPTH. We don't separately track the number of functions, but we can have a - // fast path through the eval_level. If the eval_level is in bounds, so must be the stack depth. - if (eval_level <= FISH_MAX_STACK_DEPTH) { - return false; - } - // Count the functions. - int depth = 0; - for (const auto &b : block_list) { - depth += b.is_function_call(); - } - return depth > FISH_MAX_STACK_DEPTH; -} - -wcstring parser_t::current_line() { - if (!execution_context) { - return wcstring(); - } - int source_offset = execution_context->get_current_source_offset(); - if (source_offset < 0) { - return wcstring(); - } - - const int lineno = this->get_lineno(); - filename_ref_t file = this->current_filename(); - - wcstring prefix; - - // If we are not going to print a stack trace, at least print the line number and filename. - if (!is_interactive() || is_function()) { - if (file) { - append_format(prefix, _(L"%ls (line %d): "), - user_presentable_path(*file, vars()).c_str(), lineno); - } else if (libdata().within_fish_init) { - append_format(prefix, L"%ls (line %d): ", _(L"Startup"), lineno); - } else { - append_format(prefix, L"%ls (line %d): ", _(L"Standard input"), lineno); - } - } - - bool skip_caret = is_interactive() && !is_function(); - - // Use an error with empty text. - assert(source_offset >= 0); - parse_error_t empty_error = {}; - empty_error.text = std::make_unique(); - empty_error.source_start = source_offset; - - wcstring line_info = *empty_error.describe_with_prefix(execution_context->get_source(), prefix, - is_interactive(), skip_caret); - if (!line_info.empty()) { - line_info.push_back(L'\n'); - } - - line_info.append(this->stack_trace()); - return line_info; -} - -void parser_t::job_add(shared_ptr job) { - assert(job != nullptr); - assert(!job->processes.empty()); - job_list.insert(job_list.begin(), std::move(job)); -} - -void parser_t::job_promote(job_list_t::iterator job_it) { - // Move the job to the beginning. - std::rotate(job_list.begin(), job_it, std::next(job_it)); -} - -void parser_t::job_promote(const job_t *job) { - job_list_t::iterator loc; - for (loc = job_list.begin(); loc != job_list.end(); ++loc) { - if (loc->get() == job) { - break; - } - } - assert(loc != job_list.end()); - job_promote(loc); -} - -void parser_t::job_promote_at(size_t job_pos) { - assert(job_pos < job_list.size()); - job_promote(job_list.begin() + job_pos); -} - -const job_t *parser_t::job_with_id(job_id_t id) const { - for (const auto &job : job_list) { - if (id <= 0 || job->job_id() == id) return job.get(); - } - return nullptr; -} - -job_t *parser_t::job_get_from_pid(pid_t pid) const { - size_t job_pos{}; - return job_get_from_pid(pid, job_pos); -} - -job_t *parser_t::job_get_from_pid(int pid, size_t &job_pos) const { - for (auto it = job_list.begin(); it != job_list.end(); ++it) { - for (const process_ptr_t &p : (*it)->processes) { - if (p->pid == pid) { - job_pos = it - job_list.begin(); - return (*it).get(); - } - } - } - return nullptr; -} - -const wcstring *library_data_t::get_current_filename() const { - if (current_filename) { - return &*current_filename; - } else { - return nullptr; - } -} - -library_data_pod_t *parser_t::ffi_libdata_pod() { return &library_data; } - -job_t *parser_t::ffi_job_get_from_pid(int pid) const { return job_get_from_pid(pid); } -const library_data_pod_t &parser_t::ffi_libdata_pod_const() const { return library_data; } - -profile_item_t *parser_t::create_profile_item() { - if (g_profiling_active) { - profile_items.emplace_back(); - return &profile_items.back(); - } - return nullptr; -} - -eval_res_t parser_t::eval(const wcstring &cmd, const io_chain_t &io) { - return eval_with(cmd, io, {}, block_type_t::top); -} - -eval_res_t parser_t::eval_with(const wcstring &cmd, const io_chain_t &io, - const job_group_ref_t &job_group, enum block_type_t block_type) { - // Parse the source into a tree, if we can. - auto error_list = new_parse_error_list(); - auto ps = parse_source(wcstring{cmd}, parse_flag_none, &*error_list); - if (ps->has_value()) { - return this->eval_parsed_source(*ps, io, job_group, block_type); - } else { - // Get a backtrace. This includes the message. - wcstring backtrace_and_desc; - this->get_backtrace(cmd, *error_list, backtrace_and_desc); - - // Print it. - std::fwprintf(stderr, L"%ls\n", backtrace_and_desc.c_str()); - - // Set a valid status. - this->set_last_statuses(statuses_t::just(STATUS_ILLEGAL_CMD)); - bool break_expand = true; - return eval_res_t{proc_status_t::from_exit_code(STATUS_ILLEGAL_CMD), break_expand}; - } -} - -eval_res_t parser_t::eval_string_ffi1(const wcstring &cmd) { return eval(cmd, io_chain_t()); } - -eval_res_t parser_t::eval_parsed_source_ffi1(const parsed_source_ref_t *ps, - enum block_type_t block_type) { - return eval_parsed_source(*ps, io_chain_t(), {}, block_type); -} - -eval_res_t parser_t::eval_parsed_source(const parsed_source_ref_t &ps, const io_chain_t &io, - const job_group_ref_t &job_group, - enum block_type_t block_type) { - assert(block_type == block_type_t::top || block_type == block_type_t::subst); - const auto &job_list = ps.ast().top()->as_job_list(); - if (!job_list.empty()) { - // Execute the top job list. - return this->eval_node(ps, job_list, io, job_group, block_type); - } else { - auto status = proc_status_t::from_exit_code(get_last_status()); - bool break_expand = false; - bool was_empty = true; - bool no_status = true; - return eval_res_t{status, break_expand, was_empty, no_status}; - } -} - -template -eval_res_t parser_t::eval_node(const parsed_source_ref_t &ps, const T &node, - const io_chain_t &block_io, const job_group_ref_t &job_group, - block_type_t block_type) { - static_assert( - std::is_same::value || std::is_same::value, - "Unexpected node type"); - - // Only certain blocks are allowed. - assert((block_type == block_type_t::top || block_type == block_type_t::subst) && - "Invalid block type"); - - // If fish itself got a cancel signal, then we want to unwind back to the principal parser. - // If we are the principal parser and our block stack is empty, then we want to clear the - // signal. - // Note this only happens in interactive sessions. In non-interactive sessions, SIGINT will - // cause fish to exit. - if (int sig = signal_check_cancel()) { - if (is_principal_ && block_list.empty()) { - signal_clear_cancel(); - } else { - return proc_status_t::from_signal(sig); - } - } - - // A helper to detect if we got a signal. - // This includes both signals sent to fish (user hit control-C while fish is foreground) and - // signals from the job group (e.g. some external job terminated with SIGQUIT). - auto check_cancel_signal = [=] { - // Did fish itself get a signal? - int sig = signal_check_cancel(); - // Has this job group been cancelled? - if (!sig && job_group) sig = job_group->get_cancel_signal(); - return sig; - }; - - // If we have a job group which is cancelled, then do nothing. - if (int sig = check_cancel_signal()) { - return proc_status_t::from_signal(sig); - } - - job_reap(*this, false); // not sure why we reap jobs here - - // Start it up - operation_context_t op_ctx = this->context(); - block_t *scope_block = this->push_block(block_t::scope_block(block_type)); - - // Propagate our job group. - op_ctx.job_group = job_group; - - // Replace the context's cancel checker with one that checks the job group's signal. - op_ctx.cancel_checker = [=] { return check_cancel_signal() != 0; }; - - // Create and set a new execution context. - using exc_ctx_ref_t = std::unique_ptr; - scoped_push exc( - &execution_context, make_unique(ps.clone(), op_ctx, block_io)); - - // Check the exec count so we know if anything got executed. - const size_t prev_exec_count = libdata().exec_count; - const size_t prev_status_count = libdata().status_count; - end_execution_reason_t reason = execution_context->eval_node(node, scope_block); - const size_t new_exec_count = libdata().exec_count; - const size_t new_status_count = libdata().status_count; - - exc.restore(); - this->pop_block(scope_block); - - job_reap(*this, false); // reap again - - if (int sig = check_cancel_signal()) { - return proc_status_t::from_signal(sig); - } else { - auto status = proc_status_t::from_exit_code(this->get_last_status()); - bool break_expand = (reason == end_execution_reason_t::error); - bool was_empty = !break_expand && prev_exec_count == new_exec_count; - bool no_status = prev_status_count == new_status_count; - return eval_res_t{status, break_expand, was_empty, no_status}; - } -} - -// Explicit instantiations. TODO: use overloads instead? -template eval_res_t parser_t::eval_node(const parsed_source_ref_t &, const ast::statement_t &, - const io_chain_t &, const job_group_ref_t &, block_type_t); -template eval_res_t parser_t::eval_node(const parsed_source_ref_t &, const ast::job_list_t &, - const io_chain_t &, const job_group_ref_t &, block_type_t); - -void parser_t::get_backtrace_ffi(const wcstring &src, const parse_error_list_t *errors, - wcstring &output) const { - return get_backtrace(src, *errors, output); -}; -void parser_t::get_backtrace(const wcstring &src, const parse_error_list_t &errors, - wcstring &output) const { - if (!errors.empty()) { - const auto *err = errors.at(0); - - // Determine if we want to try to print a caret to point at the source error. The - // err.source_start() <= src.size() check is due to the nasty way that slices work, which is - // by rewriting the source. - size_t which_line = 0; - bool skip_caret = true; - if (err->source_start() != SOURCE_LOCATION_UNKNOWN && err->source_start() <= src.size()) { - // Determine which line we're on. - which_line = 1 + std::count(src.begin(), src.begin() + err->source_start(), L'\n'); - - // Don't include the caret if we're interactive, this is the first line of text, and our - // source is at its beginning, because then it's obvious. - skip_caret = (is_interactive() && which_line == 1 && err->source_start() == 0); - } - - wcstring prefix; - filename_ref_t filename = this->current_filename(); - if (filename) { - if (which_line > 0) { - prefix = - format_string(_(L"%ls (line %lu): "), - user_presentable_path(*filename, vars()).c_str(), which_line); - } else { - prefix = - format_string(_(L"%ls: "), user_presentable_path(*filename, vars()).c_str()); - } - } else { - prefix = L"fish: "; - } - - const wcstring description = - *err->describe_with_prefix(src, prefix, is_interactive(), skip_caret); - if (!description.empty()) { - output.append(description); - output.push_back(L'\n'); - } - output.append(this->stack_trace()); - } -} - -RustFFIJobList parser_t::ffi_jobs() const { - return RustFFIJobList{const_cast(job_list.data()), job_list.size()}; -} - -bool parser_t::ffi_has_funtion_block() const { - for (const auto &b : blocks()) { - if (b.is_function_call()) { - return true; - } - } - return false; -} - -uint64_t parser_t::ffi_global_event_blocks() const { return global_event_blocks; } -void parser_t::ffi_incr_global_event_blocks() { ++global_event_blocks; } -void parser_t::ffi_decr_global_event_blocks() { --global_event_blocks; } - -size_t parser_t::ffi_blocks_size() const { return block_list.size(); } - -block_t::block_t(block_type_t t) : block_type(t) {} - -wcstring block_t::description() const { - wcstring result; - switch (this->type()) { - case block_type_t::while_block: { - result.append(L"while"); - break; - } - case block_type_t::for_block: { - result.append(L"for"); - break; - } - case block_type_t::if_block: { - result.append(L"if"); - break; - } - case block_type_t::function_call: { - result.append(L"function_call"); - break; - } - case block_type_t::function_call_no_shadow: { - result.append(L"function_call_no_shadow"); - break; - } - case block_type_t::switch_block: { - result.append(L"switch"); - break; - } - case block_type_t::subst: { - result.append(L"substitution"); - break; - } - case block_type_t::top: { - result.append(L"top"); - break; - } - case block_type_t::begin: { - result.append(L"begin"); - break; - } - case block_type_t::source: { - result.append(L"source"); - break; - } - case block_type_t::event: { - result.append(L"event"); - break; - } - case block_type_t::breakpoint: { - result.append(L"breakpoint"); - break; - } - case block_type_t::variable_assignment: { - result.append(L"variable_assignment"); - break; - } - } - - if (this->src_lineno >= 0) { - append_format(result, L" (line %d)", this->src_lineno); - } - if (this->src_filename) { - append_format(result, L" (file %ls)", this->src_filename->c_str()); - } - return result; -} - -bool block_t::is_function_call() const { - return type() == block_type_t::function_call || type() == block_type_t::function_call_no_shadow; -} - -// Various block constructors. - -block_t block_t::if_block() { return block_t(block_type_t::if_block); } - -block_t block_t::event_block(const void *evt_) { - const auto &evt = *static_cast(evt_); - block_t b{block_type_t::event}; - b.event = - std::make_shared>(evt.clone()); // TODO Post-FFI: move instead of clone. - return b; -} - -block_t block_t::function_block(wcstring name, std::vector args, bool shadows) { - block_t b{shadows ? block_type_t::function_call : block_type_t::function_call_no_shadow}; - b.function_name = std::move(name); - b.function_args = std::move(args); - return b; -} - -block_t block_t::source_block(filename_ref_t src) { - block_t b{block_type_t::source}; - b.sourced_file = std::move(src); - return b; -} - -block_t block_t::for_block() { return block_t{block_type_t::for_block}; } -block_t block_t::while_block() { return block_t{block_type_t::while_block}; } -block_t block_t::switch_block() { return block_t{block_type_t::switch_block}; } -block_t block_t::scope_block(block_type_t type) { - assert( - (type == block_type_t::begin || type == block_type_t::top || type == block_type_t::subst) && - "Invalid scope type"); - return block_t(type); -} -block_t block_t::breakpoint_block() { return block_t(block_type_t::breakpoint); } -block_t block_t::variable_assignment_block() { return block_t(block_type_t::variable_assignment); } - -void block_t::ffi_incr_event_blocks() { ++event_blocks; } diff --git a/src/parser.h b/src/parser.h index 00695ad2a..8ad97071d 100644 --- a/src/parser.h +++ b/src/parser.h @@ -12,524 +12,17 @@ #include #include -#include "common.h" -#include "cxx.h" -#include "env.h" -#include "event.h" -#include "expand.h" -#include "maybe.h" -#include "operation_context.h" -#include "parse_constants.h" -#include "parse_tree.h" #include "proc.h" -#include "util.h" -#include "wait_handle.h" -class autoclose_fd_t; -class io_chain_t; -struct Event; -struct job_group_t; - -/// Types of blocks. -enum class block_type_t : uint8_t { - while_block, /// While loop block - for_block, /// For loop block - if_block, /// If block - function_call, /// Function invocation block - function_call_no_shadow, /// Function invocation block with no variable shadowing - switch_block, /// Switch block - subst, /// Command substitution scope - top, /// Outermost block - begin, /// Unconditional block - source, /// Block created by the . (source) builtin - event, /// Block created on event notifier invocation - breakpoint, /// Breakpoint block - variable_assignment, /// Variable assignment before a command -}; - -/// Possible states for a loop. -enum class loop_status_t { - normals, /// current loop block executed as normal - breaks, /// current loop block should be removed - continues, /// current loop block should be skipped -}; - -/// block_t represents a block of commands. -class block_t { - public: - /// Construct from a block type. - explicit block_t(block_type_t t); - - // If this is a function block, the function name. Otherwise empty. - wcstring function_name{}; - - /// List of event blocks. - uint64_t event_blocks{}; - - // If this is a function block, the function args. Otherwise empty. - std::vector function_args{}; - - /// Name of file that created this block. - filename_ref_t src_filename{}; - - // If this is an event block, the event. Otherwise ignored. - std::shared_ptr> event; - - // If this is a source block, the source'd file, interned. - // Otherwise nothing. - filename_ref_t sourced_file{}; - - /// Line number where this block was created. - int src_lineno{0}; - - private: - /// Type of block. - const block_type_t block_type; - - public: - /// Whether we should pop the environment variable stack when we're popped off of the block - /// stack. - bool wants_pop_env{false}; - - /// Description of the block, for debugging. - wcstring description() const; - - block_type_t type() const { return this->block_type; } - - /// \return if we are a function call (with or without shadowing). - bool is_function_call() const; - - /// Entry points for creating blocks. - static block_t if_block(); - static block_t event_block(const void *evt_); - static block_t function_block(wcstring name, std::vector args, bool shadows); - static block_t source_block(filename_ref_t src); - static block_t for_block(); - static block_t while_block(); - static block_t switch_block(); - static block_t scope_block(block_type_t type); - static block_t breakpoint_block(); - static block_t variable_assignment_block(); - - /// autocxx junk. - void ffi_incr_event_blocks(); - uint64_t ffi_event_blocks() const { return event_blocks; } -}; - -struct profile_item_t { - using microseconds_t = long long; - - /// Time spent executing the command, including nested blocks. - microseconds_t duration{}; - - /// The block level of the specified command. Nested blocks and command substitutions both - /// increase the block level. - size_t level{}; - - /// If the execution of this command was skipped. - bool skipped{}; - - /// The command string. - wcstring cmd{}; - - /// \return the current time as a microsecond timestamp since the epoch. - static microseconds_t now() { return get_time(); } -}; - -class parse_execution_context_t; - -/// Plain-Old-Data components of `struct library_data_t` that can be shared over FFI -struct library_data_pod_t { - /// A counter incremented every time a command executes. - uint64_t exec_count{0}; - - /// A counter incremented every time a command produces a $status. - uint64_t status_count{0}; - - /// Last reader run count. - uint64_t last_exec_run_counter{UINT64_MAX}; - - /// Number of recursive calls to the internal completion function. - uint32_t complete_recursion_level{0}; - - /// If set, we are currently within fish's initialization routines. - bool within_fish_init{false}; - - /// If we're currently repainting the commandline. - /// Useful to stop infinite loops. - bool is_repaint{false}; - - /// Whether we called builtin_complete -C without parameter. - bool builtin_complete_current_commandline{false}; - - /// Whether we are currently cleaning processes. - bool is_cleaning_procs{false}; - - /// The internal job id of the job being populated, or 0 if none. - /// This supports the '--on-job-exit caller' feature. - internal_job_id_t caller_id{0}; - - /// Whether we are running a subshell command. - bool is_subshell{false}; - - /// Whether we are running an event handler. This is not a bool because we keep count of the - /// event nesting level. - int is_event{0}; - - /// Whether we are currently interactive. - bool is_interactive{false}; - - /// Whether to suppress fish_trace output. This occurs in the prompt, event handlers, and key - /// bindings. - bool suppress_fish_trace{false}; - - /// Whether we should break or continue the current loop. - /// This is set by the 'break' and 'continue' commands. - enum loop_status_t loop_status { loop_status_t::normals }; - - /// Whether we should return from the current function. - /// This is set by the 'return' command. - bool returning{false}; - - /// Whether we should stop executing. - /// This is set by the 'exit' command, and unset after 'reader_read'. - /// Note this only exits up to the "current script boundary." That is, a call to exit within a - /// 'source' or 'read' command will only exit up to that command. - bool exit_current_script{false}; - - /// The read limit to apply to captured subshell output, or 0 for none. - size_t read_limit{0}; -}; - -/// Miscellaneous data used to avoid recursion and others. -struct library_data_t : public library_data_pod_t { - /// The current filename we are evaluating, either from builtin source or on the command line. - filename_ref_t current_filename{}; - - /// A stack of fake values to be returned by builtin_commandline. This is used by the completion - /// machinery when wrapping: e.g. if `tig` wraps `git` then git completions need to see git on - /// the command line. - std::vector transient_commandlines{}; - - /// A file descriptor holding the current working directory, for use in openat(). - /// This is never null and never invalid. - std::shared_ptr cwd_fd{}; - - /// Status variables set by the main thread as jobs are parsed and read by various consumers. - struct { - /// Used to get the head of the current job (not the current command, at least for now) - /// for `status current-command`. - wcstring command; - /// Used to get the full text of the current job for `status current-commandline`. - wcstring commandline; - } status_vars; - - public: - wcstring get_status_vars_command() const { return status_vars.command; } - wcstring get_status_vars_commandline() const { return status_vars.commandline; } - const wcstring *get_current_filename() const; // may return nullptr if None -}; - -/// The result of parser_t::eval family. -struct eval_res_t { - /// The value for $status. - proc_status_t status; - - /// If set, there was an error that should be considered a failed expansion, such as - /// command-not-found. For example, `touch (not-a-command)` will not invoke 'touch' because - /// command-not-found will mark break_expand. - bool break_expand{false}; - - /// If set, no commands were executed and there we no errors. - bool was_empty{false}; - - /// If set, no commands produced a $status value. - bool no_status{false}; - - /* implicit */ eval_res_t(proc_status_t status, bool break_expand = false, - bool was_empty = false, bool no_status = false) - : status(status), break_expand(break_expand), was_empty(was_empty), no_status(no_status) {} -}; - -enum class parser_status_var_t : uint8_t { - current_command, - current_commandline, - count_, -}; - -class Parser; +struct Parser; using parser_t = Parser; -class Parser : public std::enable_shared_from_this { - friend class parse_execution_context_t; - - private: - /// The current execution context. - std::unique_ptr execution_context; - - /// The jobs associated with this parser. - job_list_t job_list; - - /// Our store of recorded wait-handles. These are jobs that finished in the background, and have - /// been reaped, but may still be wait'ed on. - rust::Box wait_handles; - - /// The list of blocks. This is a deque because we give out raw pointers to callers, who hold - /// them across manipulating this stack. - /// This is in "reverse" order: the topmost block is at the front. This enables iteration from - /// top down using range-based for loops. - std::deque block_list; - - /// The 'depth' of the fish call stack. - int eval_level = -1; - - /// Set of variables for the parser. - const std::shared_ptr variables; - - /// Miscellaneous library data. - library_data_t library_data{}; - - /// If set, we synchronize universal variables after external commands, - /// including sending on-variable change events. - bool syncs_uvars_{false}; - - /// If set, we are the principal parser. - bool is_principal_{false}; - - /// List of profile items. - /// This must be a deque because we return pointers to them to callers, - /// who may hold them across blocks (which would cause reallocations internal - /// to profile_items). deque does not move items on reallocation. - std::deque profile_items; - - /// Adds a job to the beginning of the job list. - void job_add(std::shared_ptr job); - - /// \return whether we are currently evaluating a function. - bool is_function() const; - - /// \return whether we are currently evaluating a command substitution. - bool is_command_substitution() const; - - /// Create a parser - Parser(std::shared_ptr vars, bool is_principal = false); - - public: - // No copying allowed. - Parser(const parser_t &) = delete; - Parser &operator=(const parser_t &) = delete; - - /// Get the "principal" parser, whatever that is. - static parser_t &principal_parser(); - - /// ffi helper. Obviously this is totally bogus. - static parser_t *principal_parser_ffi(); - - /// Assert that this parser is allowed to execute on the current thread. - void assert_can_execute() const; - - /// Global event blocks. - uint64_t global_event_blocks{}; - - eval_res_t eval(const wcstring &cmd, const io_chain_t &io); - - /// Evaluate the expressions contained in cmd. - /// - /// \param cmd the string to evaluate - /// \param io io redirections to perform on all started jobs - /// \param job_group if set, the job group to give to spawned jobs. - /// \param block_type The type of block to push on the block stack, which must be either 'top' - /// or 'subst'. - /// \return the result of evaluation. - eval_res_t eval_with(const wcstring &cmd, const io_chain_t &io, - const job_group_ref_t &job_group, block_type_t block_type); - - eval_res_t eval_string_ffi1(const wcstring &cmd); - - /// Evaluate the parsed source ps. - /// Because the source has been parsed, a syntax error is impossible. - eval_res_t eval_parsed_source(const parsed_source_ref_t &ps, const io_chain_t &io, - const job_group_ref_t &job_group = {}, - block_type_t block_type = block_type_t::top); - eval_res_t eval_parsed_source_ffi1(const parsed_source_ref_t *ps, block_type_t block_type); - /// Evaluates a node. - /// The node type must be ast_t::statement_t or ast::job_list_t. - template - eval_res_t eval_node(const parsed_source_ref_t &ps, const T &node, const io_chain_t &block_io, - const job_group_ref_t &job_group, - block_type_t block_type = block_type_t::top); - - /// Evaluate line as a list of parameters, i.e. tokenize it and perform parameter expansion and - /// cmdsubst execution on the tokens. Errors are ignored. If a parser is provided, it is used - /// for command substitution expansion. - static completion_list_t expand_argument_list(const wcstring &arg_list_src, - expand_flags_t flags, - const operation_context_t &ctx); - - /// Returns a string describing the current parser position in the format 'FILENAME (line - /// LINE_NUMBER): LINE'. Example: - /// - /// init.fish (line 127): ls|grep pancake - wcstring current_line(); - - /// Returns the current line number. - int get_lineno() const; - - /// \return whether we are currently evaluating a "block" such as an if statement. - /// This supports 'status is-block'. - bool is_block() const; - - /// \return whether we have a breakpoint block. - bool is_breakpoint() const; - - /// Returns the block at the given index. 0 corresponds to the innermost block. Returns nullptr - /// when idx is at or equal to the number of blocks. - const block_t *block_at_index(size_t idx) const; - block_t *block_at_index(size_t idx); - - /// Return the list of blocks. The first block is at the top. - const std::deque &blocks() const { return block_list; } - - size_t blocks_size() const { return block_list.size(); } - - /// Get the list of jobs. - job_list_t &jobs() { return job_list; } - const job_list_t &jobs() const { return job_list; } - - /// Get the variables. - env_stack_t &vars() { return *variables; } - const env_stack_t &vars() const { return *variables; } - - int remove_var_ffi(const wcstring &key, int mode) { return vars().remove(key, mode); } - - /// Get the library data. - library_data_t &libdata() { return library_data; } - const library_data_t &libdata() const { return library_data; } - - /// Get our wait handle store. - rust::Box &get_wait_handles_ffi(); - const rust::Box &get_wait_handles_ffi() const; - - /// As get_wait_handles(), but void* pointer-to-Box to satisfy autocxx. - void *get_wait_handles_void() const { - const void *ptr = &get_wait_handles_ffi(); - return const_cast(ptr); - } - - /// Get and set the last proc statuses. - int get_last_status() const { return vars().get_last_status(); } - statuses_t get_last_statuses() const { return vars().get_last_statuses(); } - void set_last_statuses(statuses_t s) { vars().set_last_statuses(std::move(s)); } - - /// Cover of vars().set(), which also fires any returned event handlers. - /// \return a value like ENV_OK. - int set_var_and_fire(const wcstring &key, env_mode_flags_t mode, wcstring val); - int set_var_and_fire(const wcstring &key, env_mode_flags_t mode, std::vector vals); - - /// Update any universal variables and send event handlers. - /// If \p always is set, then do it even if we have no pending changes (that is, look for - /// changes from other fish instances); otherwise only sync if this instance has changed uvars. - void sync_uvars_and_fire(bool always = false); - - /// Pushes a new block. Returns a pointer to the block, stored in the parser. The pointer is - /// valid until the call to pop_block(). - block_t *push_block(block_t &&b); - - /// Remove the outermost block, asserting it's the given one. - void pop_block(const block_t *expected); - - /// Avoid maybe_t usage for ffi, sends a empty string in case of none. - wcstring get_function_name_ffi(int level); - /// Return the function name for the specified stack frame. Default is one (current frame). - maybe_t get_function_name(int level = 1); - - /// Promotes a job to the front of the list. - void job_promote(job_list_t::iterator job_it); - void job_promote(const job_t *job); - void job_promote_at(size_t job_pos); - - /// Return the job with the specified job id. If id is 0 or less, return the last job used. - const job_t *job_with_id(job_id_t job_id) const; - - /// Returns the job with the given pid. - job_t *job_get_from_pid(pid_t pid) const; - - /// Returns the job and position with the given pid. - job_t *job_get_from_pid(int pid, size_t &job_pos) const; - - /// Returns a new profile item if profiling is active. The caller should fill it in. - /// The parser_t will deallocate it. - /// If profiling is not active, this returns nullptr. - profile_item_t *create_profile_item(); - - /// Remove the profiling items. - void clear_profiling(); - - /// Output profiling data to the given filename. - void emit_profiling(const char *path) const; - - void get_backtrace_ffi(const wcstring &src, const parse_error_list_t *errors, - wcstring &output) const; - void get_backtrace(const wcstring &src, const parse_error_list_t &errors, - wcstring &output) const; - - /// Returns the file currently evaluated by the parser. This can be different than - /// reader_current_filename, e.g. if we are evaluating a function defined in a different file - /// than the one currently read. - filename_ref_t current_filename() const; - wcstring current_filename_ffi() const; - void set_filename_ffi(wcstring filename); - - /// Return if we are interactive, which means we are executing a command that the user typed in - /// (and not, say, a prompt). - bool is_interactive() const { return libdata().is_interactive; } - - /// Return a string representing the current stack trace. - wcstring stack_trace() const; - - /// \return whether the number of functions in the stack exceeds our stack depth limit. - bool function_stack_is_overflowing() const; - - /// Mark whether we should sync universal variables. - void set_syncs_uvars(bool flag) { syncs_uvars_ = flag; } - - /// Set the given file descriptor as the working directory for this parser. - /// This acquires ownership. - void set_cwd_fd(int fd); - - /// \return a shared pointer reference to this parser. - std::shared_ptr shared(); - - /// \return a cancel poller for checking if this parser has been signalled. - /// autocxx falls over with this so hide it. #if INCLUDE_RUST_HEADERS - cancel_checker_t cancel_checker() const; +#include "parser.rs.h" +#else +struct EvalRes; #endif - /// \return the operation context for this parser. - operation_context_t context(); - - /// Checks if the max eval depth has been exceeded - bool is_eval_depth_exceeded() const { return eval_level >= FISH_MAX_EVAL_DEPTH; } - - /// autocxx junk. - RustFFIJobList ffi_jobs() const; - library_data_pod_t *ffi_libdata_pod(); - job_t *ffi_job_get_from_pid(int pid) const; - const library_data_pod_t &ffi_libdata_pod_const() const; - - /// autocxx junk. - bool ffi_has_funtion_block() const; - - /// autocxx junk. - uint64_t ffi_global_event_blocks() const; - void ffi_incr_global_event_blocks(); - void ffi_decr_global_event_blocks(); - - /// autocxx junk. - size_t ffi_blocks_size() const; - - ~Parser(); -}; +using eval_res_t = EvalRes; #endif diff --git a/src/parser_keywords.cpp b/src/parser_keywords.cpp deleted file mode 100644 index e5b3ec1c5..000000000 --- a/src/parser_keywords.cpp +++ /dev/null @@ -1,72 +0,0 @@ -// Functions having to do with parser keywords, like testing if a function is a block command. -#include "config.h" // IWYU pragma: keep - -#include "parser_keywords.h" - -#include -#include -#include -#include - -#include "common.h" -#include "fallback.h" // IWYU pragma: keep - -using string_set_t = std::unordered_set; - -static const wcstring skip_keywords[]{ - L"else", - L"begin", -}; - -static const wcstring subcommand_keywords[]{L"command", L"builtin", L"while", L"exec", L"if", - L"and", L"or", L"not", L"time", L"begin"}; - -static const string_set_t block_keywords = {L"for", L"while", L"if", - L"function", L"switch", L"begin"}; - -// Don't forget to add any new reserved keywords to the documentation -static const wcstring reserved_keywords[] = { - L"end", L"case", L"else", L"return", L"continue", L"break", L"argparse", L"read", - L"string", L"set", L"status", L"test", L"[", L"_", L"eval"}; - -// The lists above are purposely implemented separately from the logic below, so that future -// maintainers may assume the contents of the list based off their names, and not off what the -// functions below require them to contain. - -static size_t list_max_length(const string_set_t &list) { - size_t result = 0; - for (const auto &w : list) { - if (w.length() > result) { - result = w.length(); - } - } - return result; -} - -bool parser_keywords_is_subcommand(const wcstring &cmd) { - const static string_set_t search_list = ([] { - string_set_t results; - results.insert(std::begin(subcommand_keywords), std::end(subcommand_keywords)); - results.insert(std::begin(skip_keywords), std::end(skip_keywords)); - return results; - })(); - - const static auto max_len = list_max_length(search_list); - const static auto not_found = search_list.end(); - - // Everything above is executed only at startup, this is the actual optimized search routine: - return cmd.length() <= max_len && search_list.find(cmd) != not_found; -} - -bool parser_keywords_is_reserved(const wcstring &word) { - const static string_set_t search_list = ([] { - string_set_t results; - results.insert(std::begin(subcommand_keywords), std::end(subcommand_keywords)); - results.insert(std::begin(skip_keywords), std::end(skip_keywords)); - results.insert(std::begin(block_keywords), std::end(block_keywords)); - results.insert(std::begin(reserved_keywords), std::end(reserved_keywords)); - return results; - })(); - const static size_t max_len = list_max_length(search_list); - return word.length() <= max_len && search_list.count(word) > 0; -} diff --git a/src/path.cpp b/src/path.cpp index 9e3a9da2b..aea864b73 100644 --- a/src/path.cpp +++ b/src/path.cpp @@ -134,7 +134,8 @@ static dir_remoteness_t path_remoteness(const wcstring &path) { } std::vector path_apply_cdpath(const wcstring &dir, const wcstring &wd, - const environment_t &env_vars) { + // todo!("houd be environment_t") + const env_stack_t &env_vars) { std::vector paths; if (dir.at(0) == L'/') { // Absolute path. @@ -174,7 +175,8 @@ std::vector path_apply_cdpath(const wcstring &dir, const wcstring &wd, } maybe_t path_get_cdpath(const wcstring &dir, const wcstring &wd, - const environment_t &env_vars) { + // todo!("should be environment_t") + const env_stack_t &env_vars) { int err = ENOENT; if (dir.empty()) return none(); assert(!wd.empty() && wd.back() == L'/'); @@ -195,7 +197,8 @@ maybe_t path_get_cdpath(const wcstring &dir, const wcstring &wd, } maybe_t path_as_implicit_cd(const wcstring &path, const wcstring &wd, - const environment_t &vars) { + // todo!("should be environment_t") + const env_stack_t &vars) { wcstring exp_path = path; expand_tilde(exp_path, vars); if (string_prefixes_string(L"/", exp_path) || string_prefixes_string(L"./", exp_path) || diff --git a/src/path.h b/src/path.h index 52d59d9db..80751eb9c 100644 --- a/src/path.h +++ b/src/path.h @@ -7,6 +7,7 @@ #include #include "common.h" +#include "env.h" #include "maybe.h" #include "parser.h" #include "wutil.h" @@ -41,12 +42,10 @@ dir_remoteness_t path_get_data_remoteness(); /// Like path_get_data_remoteness but for the config directory. dir_remoteness_t path_get_config_remoteness(); -class env_stack_t; /// Emit any errors if config directories are missing. /// Use the given environment stack to ensure this only occurs once. void path_emit_config_directory_messages(env_stack_t &vars); -class environment_t; /// Finds the path of an executable named \p cmd, by looking in $PATH taken from \p vars. /// \returns the path if found, none if not. maybe_t path_get_path(const wcstring &cmd, const environment_t &vars); @@ -75,16 +74,17 @@ get_path_result_t path_try_get_path(const wcstring &cmd, const environment_t &va /// \param vars The environment variables to use (for the CDPATH variable) /// \return the command, or none() if it could not be found. maybe_t path_get_cdpath(const wcstring &dir, const wcstring &wd, - const environment_t &vars); + // todo!("should be environment_t") + const env_stack_t &vars); /// Returns the given directory with all CDPATH components applied. std::vector path_apply_cdpath(const wcstring &dir, const wcstring &wd, - const environment_t &env_vars); + const env_stack_t &env_vars); /// Returns the path resolved as an implicit cd command, or none() if none. This requires it to /// start with one of the allowed prefixes (., .., ~) and resolve to a directory. maybe_t path_as_implicit_cd(const wcstring &path, const wcstring &wd, - const environment_t &vars); + const env_stack_t &vars); /// Check if two paths are equivalent, which means to ignore runs of multiple slashes (or trailing /// slashes). diff --git a/src/proc.cpp b/src/proc.cpp deleted file mode 100644 index 8b4422ecf..000000000 --- a/src/proc.cpp +++ /dev/null @@ -1,1040 +0,0 @@ -// Utilities for keeping track of jobs, processes and subshells, as well as signal handling -// functions for tracking children. These functions do not themselves launch new processes, the exec -// library will call proc to create representations of the running jobs as needed. -// -// Some of the code in this file is based on code from the Glibc manual. -#include "config.h" - -#include -#include -#include -#include - -#include -#include - -#if HAVE_TERM_H -#include // IWYU pragma: keep -#include -#elif HAVE_NCURSES_TERM_H -#include -#endif -#include -#ifdef HAVE_SIGINFO_H -#include -#endif -#include // IWYU pragma: keep -#include - -#include // IWYU pragma: keep -#include -#include -#include -#include -#include - -#include "common.h" -#include "env.h" -#include "event.h" -#include "fallback.h" // IWYU pragma: keep -#include "fds.h" -#include "flog.h" -#include "global_safety.h" -#include "io.h" -#include "job_group.rs.h" -#include "parser.h" -#include "proc.h" -#include "reader.h" -#include "signals.h" -#include "wutil.h" // IWYU pragma: keep - -/// The signals that signify crashes to us. -static const int crashsignals[] = {SIGABRT, SIGBUS, SIGFPE, SIGILL, SIGSEGV, SIGSYS}; - -static relaxed_atomic_bool_t s_is_interactive_session{false}; -bool is_interactive_session() { return s_is_interactive_session; } -void set_interactive_session(bool flag) { s_is_interactive_session = flag; } - -static relaxed_atomic_bool_t s_is_login{false}; -bool get_login() { return s_is_login; } -void mark_login() { s_is_login = true; } - -static relaxed_atomic_bool_t s_no_exec{false}; -bool no_exec() { return s_no_exec; } -void mark_no_exec() { s_no_exec = true; } - -bool have_proc_stat() { - // Check for /proc/self/stat to see if we are running with Linux-style procfs. - static const bool s_result = (access("/proc/self/stat", R_OK) == 0); - return s_result; -} - -static relaxed_atomic_t job_control_mode{job_control_t::interactive}; - -job_control_t get_job_control_mode() { return job_control_mode; } - -void set_job_control_mode(job_control_t mode) { - job_control_mode = mode; - - // HACK: when fish (or any shell) launches a job with job control, it will put the job into its - // own pgroup and call tcsetpgrp() to allow that pgroup to own the terminal (making fish a - // background process). When the job finishes, fish will try to reclaim the terminal via - // tcsetpgrp(), but as fish is now a background process it will receive SIGTTOU and stop! Ensure - // that doesn't happen by ignoring SIGTTOU. - // Note that if we become interactive, we also ignore SIGTTOU. - if (mode == job_control_t::all) { - signal(SIGTTOU, SIG_IGN); - } -} - -void proc_init() { signal_set_handlers_once(false); } - -/// Return true if all processes in the job are stopped or completed, and there is at least one -/// stopped process. -bool job_t::is_stopped() const { - bool has_stopped = false; - for (const process_ptr_t &p : processes) { - if (!p->completed && !p->stopped) { - return false; - } - has_stopped |= p->stopped; - } - return has_stopped; -} - -/// Return true if all processes in the job have completed. -bool job_t::is_completed() const { - assert(!processes.empty()); - for (const process_ptr_t &p : processes) { - if (!p->completed) { - return false; - } - } - return true; -} - -bool job_t::posts_job_exit_events() const { - // Only report root job exits. - // For example in `ls | begin ; cat ; end` we don't need to report the cat sub-job. - if (!flags().is_group_root) return false; - - // Only jobs with external processes post job_exit events. - return this->has_external_proc(); -} - -bool job_t::signal(int signal) { - auto pgid = group->get_pgid(); - if (pgid) { - if (killpg(pgid->value, signal) == -1) { - char buffer[512]; - snprintf(buffer, 512, "killpg(%d, %s)", pgid->value, strsignal(signal)); - wperror(str2wcstring(buffer).c_str()); - return false; - } - } else { - // This job lives in fish's pgroup and we need to signal procs individually. - for (const auto &p : processes) { - if (!p->completed && p->pid && kill(p->pid, signal) == -1) { - return false; - } - } - } - return true; -} - -maybe_t job_t::get_statuses() const { - statuses_t st{}; - bool has_status = false; - int laststatus = 0; - st.pipestatus.reserve(processes.size()); - for (const auto &p : processes) { - auto status = p->status; - if (status.is_empty()) { - // Corner case for if a variable assignment is part of a pipeline. - // e.g. `false | set foo bar | true` will push 1 in the second spot, - // for a complete pipestatus of `1 1 0`. - st.pipestatus.push_back(laststatus); - continue; - } - if (status.signal_exited()) { - st.kill_signal = status.signal_code(); - } - laststatus = status.status_value(); - has_status = true; - st.pipestatus.push_back(status.status_value()); - } - if (!has_status) { - return none(); - } - st.status = flags().negate ? !laststatus : laststatus; - return st; -} - -RustFFIProcList job_t::ffi_processes() const { - return RustFFIProcList{const_cast(processes.data()), processes.size()}; -} - -const job_group_t &job_t::ffi_group() const { return *group; } - -bool job_t::ffi_resume() const { return const_cast(this)->resume(); } - -void internal_proc_t::mark_exited(proc_status_t status) { - assert(!exited() && "Process is already exited"); - status_.store(status, std::memory_order_relaxed); - exited_.store(true, std::memory_order_release); - topic_monitor_principal().post(topic_t::internal_exit); - FLOG(proc_internal_proc, L"Internal proc", internal_proc_id_, L"exited with status", - status.status_value()); -} - -static int64_t next_proc_id() { - static std::atomic s_next{}; - return ++s_next; -} - -internal_proc_t::internal_proc_t() : internal_proc_id_(next_proc_id()) {} - -job_list_t jobs_requiring_warning_on_exit(const parser_t &parser) { - job_list_t result; - for (const auto &job : parser.jobs()) { - if (!job->is_foreground() && job->is_constructed() && !job->is_completed()) { - result.push_back(job); - } - } - return result; -} - -void print_exit_warning_for_jobs(const job_list_t &jobs) { - fputws(_(L"There are still jobs active:\n"), stdout); - fputws(_(L"\n PID Command\n"), stdout); - for (const auto &j : jobs) { - fwprintf(stdout, L"%6d %ls\n", j->processes[0]->pid, j->command_wcstr()); - } - fputws(L"\n", stdout); - fputws(_(L"A second attempt to exit will terminate them.\n"), stdout); - fputws(_(L"Use 'disown PID' to remove jobs from the list without terminating them.\n"), stdout); - reader_schedule_prompt_repaint(); -} - -/// Set the status of \p proc to \p status. -static void handle_child_status(const shared_ptr &job, process_t *proc, - proc_status_t status) { - proc->status = status; - if (status.stopped()) { - proc->stopped = true; - } else if (status.continued()) { - proc->stopped = false; - } else { - proc->completed = true; - } - - // If the child was killed by SIGINT or SIGQUIT, then cancel the entire group if interactive. If - // not interactive, we have historically re-sent the signal to ourselves; however don't do that - // if the signal is trapped (#6649). - // Note the asymmetry: if the fish process gets SIGINT we will run SIGINT handlers. If a child - // process gets SIGINT we do not run SIGINT handlers; we just don't exit. This should be - // rationalized. - if (status.signal_exited()) { - int sig = status.signal_code(); - if (sig == SIGINT || sig == SIGQUIT) { - if (is_interactive_session()) { - // Mark the job group as cancelled. - job->group->cancel_with_signal(sig); - } else if (!event_is_signal_observed(sig)) { - // Deliver the SIGINT or SIGQUIT signal to ourself since we're not interactive. - struct sigaction act; - sigemptyset(&act.sa_mask); - act.sa_flags = 0; - act.sa_handler = SIG_DFL; - sigaction(sig, &act, nullptr); - kill(getpid(), sig); - } - } - } -} - -process_t::process_t() - : block_node_source(empty_parsed_source_ref()), - proc_redirection_specs_(new_redirection_spec_list()) {} - -void process_t::check_generations_before_launch() { - gens_ = topic_monitor_principal().current_generations(); -} - -void process_t::mark_aborted_before_launch() { - this->completed = true; - // The status may have already been set to e.g. STATUS_NOT_EXECUTABLE. - // Only stomp a successful status. - if (this->status.is_success()) { - this->status = proc_status_t::from_exit_code(EXIT_FAILURE); - } -} - -bool process_t::is_internal() const { - switch (type) { - case process_type_t::builtin: - case process_type_t::function: - case process_type_t::block_node: - return true; - case process_type_t::external: - case process_type_t::exec: - return false; - default: - assert(false && - "The fish developers forgot to include a process_t. Please report a bug"); - return true; - } - assert(false && - "process_t::is_internal: Total logic failure, universe is broken. Please replace " - "universe and retry."); - return true; -} - -rust::Box *process_t::get_wait_handle_ffi() const { return wait_handle_.get(); } - -rust::Box *process_t::make_wait_handle_ffi(internal_job_id_t jid) { - if (type != process_type_t::external || pid <= 0) { - // Not waitable. - return nullptr; - } - if (!wait_handle_) { - wait_handle_ = make_unique>( - new_wait_handle_ffi(this->pid, jid, wbasename(this->actual_cmd))); - } - return wait_handle_.get(); -} - -void *process_t::get_wait_handle_void() const { return get_wait_handle_ffi(); } -void *process_t::make_wait_handle_void(internal_job_id_t jid) { return make_wait_handle_ffi(jid); } - -static uint64_t next_internal_job_id() { - static std::atomic s_next{}; - return ++s_next; -} - -job_t::job_t(const properties_t &props, wcstring command_str) - : properties(props), - command_str(std::move(command_str)), - internal_job_id(next_internal_job_id()) {} - -job_t::~job_t() = default; - -bool job_t::wants_job_control() const { return group->wants_job_control(); } - -void job_t::mark_constructed() { - assert(!is_constructed() && "Job was already constructed"); - mut_flags().constructed = true; -} - -bool job_t::has_external_proc() const { - for (const auto &p : processes) { - if (!p->is_internal()) return true; - } - return false; -} - -bool job_t::wants_job_id() const { - return processes.size() > 1 || !processes.front()->is_internal() || is_initially_background(); -} - -/// A list of pids that have been disowned. They are kept around until either they exit or -/// we exit. Poll these from time-to-time to prevent zombie processes from happening (#5342). -static owning_lock> s_disowned_pids; - -void add_disowned_job(const job_t *j) { - assert(j && "Null job"); - auto disowned_pids = s_disowned_pids.acquire(); - for (auto &process : j->processes) { - if (process->pid) { - disowned_pids->push_back(process->pid); - } - } -} - -// Reap any pids in our disowned list that have exited. This is used to avoid zombies. -static void reap_disowned_pids() { - auto disowned_pids = s_disowned_pids.acquire(); - auto try_reap1 = [](pid_t pid) { - int status; - int ret = waitpid(pid, &status, WNOHANG); - if (ret > 0) { - FLOGF(proc_reap_external, "Reaped disowned PID or PGID %d", pid); - } - return ret; - }; - // waitpid returns 0 iff the PID/PGID in question has not changed state; remove the pid/pgid - // if it has changed or an error occurs (presumably ECHILD because the child does not exist) - disowned_pids->erase(std::remove_if(disowned_pids->begin(), disowned_pids->end(), try_reap1), - disowned_pids->end()); -} - -/// See if any reapable processes have exited, and mark them accordingly. -/// \param block_ok if no reapable processes have exited, block until one is (or until we receive a -/// signal). -static void process_mark_finished_children(parser_t &parser, bool block_ok) { - parser.assert_can_execute(); - - // Get the exit and signal generations of all reapable processes. - // The exit generation tells us if we have an exit; the signal generation allows for detecting - // SIGHUP and SIGINT. - // Go through each process and figure out if and how it wants to be reaped. - generation_list_t reapgens = invalid_generations(); - for (const auto &j : parser.jobs()) { - for (const auto &proc : j->processes) { - if (!j->can_reap(proc)) continue; - - if (proc->pid > 0) { - // Reaps with a pid. - reapgens.set_min_from(topic_t::sigchld, proc->gens_); - reapgens.set_min_from(topic_t::sighupint, proc->gens_); - } - if (proc->internal_proc_) { - // Reaps with an internal process. - reapgens.set_min_from(topic_t::internal_exit, proc->gens_); - reapgens.set_min_from(topic_t::sighupint, proc->gens_); - } - } - } - - // Now check for changes, optionally waiting. - if (!topic_monitor_principal().check(&reapgens, block_ok)) { - // Nothing changed. - return; - } - - // We got some changes. Since we last checked we received SIGCHLD, and or HUP/INT. - // Update the hup/int generations and reap any reapable processes. - // We structure this as two loops for some simplicity. - // First reap all pids. - for (const auto &j : parser.jobs()) { - for (const auto &proc : j->processes) { - // Does this proc have a pid that is reapable? - if (proc->pid <= 0 || !j->can_reap(proc)) continue; - - // Always update the signal hup/int gen. - proc->gens_.sighupint = reapgens.sighupint; - - // Nothing to do if we did not get a new sigchld. - if (proc->gens_.sigchld == reapgens.sigchld) continue; - proc->gens_.sigchld = reapgens.sigchld; - - // Ok, we are reapable. Run waitpid()! - int statusv = -1; - pid_t pid = waitpid(proc->pid, &statusv, WNOHANG | WUNTRACED | WCONTINUED); - assert((pid <= 0 || pid == proc->pid) && "Unexpcted waitpid() return"); - if (pid <= 0) continue; - - // The process has stopped or exited! Update its status. - proc_status_t status = proc_status_t::from_waitpid(statusv); - handle_child_status(j, proc.get(), status); - if (status.stopped()) { - j->group->set_is_foreground(false); - } - if (status.continued()) { - j->mut_flags().notified_of_stop = false; - } - if (status.normal_exited() || status.signal_exited()) { - FLOGF(proc_reap_external, "Reaped external process '%ls' (pid %d, status %d)", - proc->argv0(), pid, proc->status.status_value()); - } else { - assert(status.stopped() || status.continued()); - FLOGF(proc_reap_external, "External process '%ls' (pid %d, %s)", proc->argv0(), - proc->pid, proc->status.stopped() ? "stopped" : "continued"); - } - } - } - - // We are done reaping pids. - // Reap internal processes. - for (const auto &j : parser.jobs()) { - for (const auto &proc : j->processes) { - // Does this proc have an internal process that is reapable? - if (!proc->internal_proc_ || !j->can_reap(proc)) continue; - - // Always update the signal hup/int gen. - proc->gens_.sighupint = reapgens.sighupint; - - // Nothing to do if we did not get a new internal exit. - if (proc->gens_.internal_exit == reapgens.internal_exit) continue; - proc->gens_.internal_exit = reapgens.internal_exit; - - // Has the process exited? - if (!proc->internal_proc_->exited()) continue; - - // The process gets the status from its internal proc. - handle_child_status(j, proc.get(), proc->internal_proc_->get_status()); - FLOGF(proc_reap_internal, "Reaped internal process '%ls' (id %llu, status %d)", - proc->argv0(), proc->internal_proc_->get_id(), proc->status.status_value()); - } - } - - // Remove any zombies. - reap_disowned_pids(); -} - -/// Generate process_exit events for any completed processes in \p j. -static void generate_process_exit_events(const job_ref_t &j, - std::vector> *out_evts) { - // Historically we have avoided generating events for foreground jobs from event handlers, as an - // event handler may itself produce a new event. - if (!j->from_event_handler() || !j->is_foreground()) { - for (const auto &p : j->processes) { - if (p->pid > 0 && p->completed && !p->posted_proc_exit) { - p->posted_proc_exit = true; - out_evts->push_back(new_event_process_exit(p->pid, p->status.status_value())); - } - } - } -} - -/// Given a job that has completed, generate job_exit and caller_exit events. -static void generate_job_exit_events(const job_ref_t &j, std::vector> *out_evts) { - // Generate proc and job exit events, except for foreground jobs originating in event handlers. - if (!j->from_event_handler() || !j->is_foreground()) { - // job_exit events. - if (j->posts_job_exit_events()) { - auto last_pid = j->get_last_pid(); - if (last_pid.has_value()) { - out_evts->push_back(new_event_job_exit(*last_pid, j->internal_job_id)); - } - } - } - // Generate caller_exit events. - out_evts->push_back(new_event_caller_exit(j->internal_job_id, j->job_id())); -} - -/// \return whether to emit a fish_job_summary call for a process. -static bool proc_wants_summary(const shared_ptr &j, const process_ptr_t &p) { - // Are we completed with a pid? - if (!p->completed || !p->pid) return false; - - // Did we die due to a signal other than SIGPIPE? - auto s = p->status; - if (!s.signal_exited() || s.signal_code() == SIGPIPE) return false; - - // Does the job want to suppress notifications? - // Note we always report crashes. - if (j->skip_notification() && !contains(crashsignals, s.signal_code())) return false; - - return true; -} - -/// \return whether to emit a fish_job_summary call for a job as a whole. We may also emit this for -/// its individual processes. -static bool job_wants_summary(const shared_ptr &j) { - // Do we just skip notifications? - if (j->skip_notification()) return false; - - // Do we have a single process which will also report? If so then that suffices for us. - if (j->processes.size() == 1 && proc_wants_summary(j, j->processes.front())) return false; - - // Are we foreground? - // The idea here is to not print status messages for jobs that execute in the foreground (i.e. - // without & and without being `bg`). - if (j->is_foreground()) return false; - - return true; -} - -/// \return whether we want to emit a fish_job_summary call for a job or any of its processes. -bool job_or_proc_wants_summary(const shared_ptr &j) { - if (job_wants_summary(j)) return true; - for (const auto &p : j->processes) { - if (proc_wants_summary(j, p)) return true; - } - return false; -} - -/// Invoke the fish_job_summary function by executing the given command. -static void call_job_summary(parser_t &parser, const wcstring &cmd) { - auto event = new_event_generic(L"fish_job_summary"); - block_t *b = parser.push_block(block_t::event_block(&*event)); - auto saved_status = parser.get_last_statuses(); - parser.eval(cmd, io_chain_t()); - parser.set_last_statuses(saved_status); - parser.pop_block(b); -} - -// \return a command which invokes fish_job_summary. -// The process pointer may be null, in which case it represents the entire job. -// Note this implements the arguments which fish_job_summary expects. -wcstring summary_command(const job_ref_t &j, const process_ptr_t &p = nullptr) { - wcstring buffer = L"fish_job_summary"; - - // Job id. - append_format(buffer, L" %d", j->job_id()); - - // 1 if foreground, 0 if background. - append_format(buffer, L" %d", static_cast(j->is_foreground())); - - // Command. - buffer.push_back(L' '); - buffer.append(escape_string(j->command())); - - if (!p) { - // No process, we are summarizing the whole job. - buffer.append(j->is_stopped() ? L" STOPPED" : L" ENDED"); - } else { - // We are summarizing a process which exited with a signal. - // Arguments are the signal name and description. - int sig = p->status.signal_code(); - buffer.push_back(L' '); - buffer.append(escape_string(std::move(*sig2wcs(sig)))); - - buffer.push_back(L' '); - buffer.append(escape_string(std::move(*signal_get_desc(sig)))); - - // If we have multiple processes, we also append the pid and argv. - if (j->processes.size() > 1) { - append_format(buffer, L" %d", p->pid); - - buffer.push_back(L' '); - buffer.append(escape_string(p->argv0())); - } - } - return buffer; -} - -// Summarize a list of jobs, by emitting calls to fish_job_summary. -// Note the given list must NOT be the parser's own job list, since the call to fish_job_summary -// could modify it. -static bool summarize_jobs(parser_t &parser, const std::vector &jobs) { - if (jobs.empty()) return false; - - for (const auto &j : jobs) { - if (j->is_stopped()) { - call_job_summary(parser, summary_command(j)); - } else { - // Completed job. - for (const auto &p : j->processes) { - if (proc_wants_summary(j, p)) { - call_job_summary(parser, summary_command(j, p)); - } - } - - // Overall status for the job. - if (job_wants_summary(j)) { - call_job_summary(parser, summary_command(j)); - } - } - } - return true; -} - -/// Remove all disowned jobs whose job chain is fully constructed (that is, do not erase disowned -/// jobs that still have an in-flight parent job). Note we never print statuses for such jobs. -static void remove_disowned_jobs(job_list_t &jobs) { - auto iter = jobs.begin(); - while (iter != jobs.end()) { - const auto &j = *iter; - if (j->flags().disown_requested && j->is_constructed()) { - iter = jobs.erase(iter); - } else { - ++iter; - } - } -} - -/// Given that a job has completed, check if it may be wait'ed on; if so add it to the wait handle -/// store. Then mark all wait handles as complete. -static void save_wait_handle_for_completed_job(const shared_ptr &job, - WaitHandleStoreFFI &store) { - assert(job && job->is_completed() && "Job null or not completed"); - // Are we a background job? - if (!job->is_foreground()) { - for (auto &proc : job->processes) { - store.add(proc->make_wait_handle_ffi(job->internal_job_id)); - } - } - - // Mark all wait handles as complete (but don't create just for this). - for (auto &proc : job->processes) { - if (auto *wh = proc->get_wait_handle_ffi()) { - (*wh)->set_status_and_complete(proc->status.status_value()); - } - } -} - -/// Remove completed jobs from the job list, printing status messages as appropriate. -/// \return whether something was printed. -static bool process_clean_after_marking(parser_t &parser, bool allow_interactive) { - parser.assert_can_execute(); - - // This function may fire an event handler, we do not want to call ourselves recursively (to - // avoid infinite recursion). - if (parser.libdata().is_cleaning_procs) { - return false; - } - const scoped_push cleaning(&parser.libdata().is_cleaning_procs, true); - - // This may be invoked in an exit handler, after the TERM has been torn down - // Don't try to print in that case (#3222) - const bool interactive = allow_interactive && cur_term != nullptr; - - // Remove all disowned jobs. - remove_disowned_jobs(parser.jobs()); - - // Accumulate exit events into a new list, which we fire after the list manipulation is - // complete. - std::vector> exit_events; - - // Defer processing under-construction jobs or jobs that want a message when we are not - // interactive. - auto should_process_job = [=](const shared_ptr &j) { - // Do not attempt to process jobs which are not yet constructed. - // Do not attempt to process jobs that need to print a status message, - // unless we are interactive, in which case printing is OK. - return j->is_constructed() && (interactive || !job_or_proc_wants_summary(j)); - }; - - // The list of jobs to summarize. Some of these jobs are completed and are removed from the - // parser's job list, others are stopped and remain in the list. - std::vector jobs_to_summarize; - - // Handle stopped jobs. These stay in our list. - for (const auto &j : parser.jobs()) { - if (j->is_stopped() && !j->flags().notified_of_stop && should_process_job(j) && - job_wants_summary(j)) { - j->mut_flags().notified_of_stop = true; - jobs_to_summarize.push_back(j); - } - } - - // Generate process_exit events for finished processes. - for (const auto &j : parser.jobs()) { - generate_process_exit_events(j, &exit_events); - } - - // Remove completed, processable jobs from our job list. - for (auto iter = parser.jobs().begin(); iter != parser.jobs().end();) { - const job_ref_t &j = *iter; - if (!should_process_job(j) || !j->is_completed()) { - ++iter; - continue; - } - // We are committed to removing this job. - // Remember it for summary later, generate exit events, maybe save its wait handle if it - // finished in the background. - if (job_or_proc_wants_summary(j)) jobs_to_summarize.push_back(j); - generate_job_exit_events(j, &exit_events); - save_wait_handle_for_completed_job(j, *parser.get_wait_handles_ffi()); - - // Remove it. - iter = parser.jobs().erase(iter); - } - - // Emit calls to fish_job_summary. - bool printed = summarize_jobs(parser, jobs_to_summarize); - - // Post pending exit events. - for (const auto &evt : exit_events) { - event_fire(parser, *evt); - } - - if (printed) { - fflush(stdout); - } - - return printed; -} - -bool job_reap(parser_t &parser, bool allow_interactive) { - parser.assert_can_execute(); - // Early out for the common case that there are no jobs. - if (parser.jobs().empty()) { - return false; - } - - process_mark_finished_children(parser, false /* not block_ok */); - return process_clean_after_marking(parser, allow_interactive); -} - -double clock_ticks_to_seconds(clock_ticks_t ticks) { - long clock_ticks_per_sec = sysconf(_SC_CLK_TCK); - if (clock_ticks_per_sec > 0) { - return ticks / static_cast(clock_ticks_per_sec); - } - return 0; -} - -/// Get the CPU time for the specified process. -clock_ticks_t proc_get_jiffies(pid_t inpid) { - if (inpid <= 0 || !have_proc_stat()) return 0; - - char state; - int pid, ppid, pgrp, session, tty_nr, tpgid, exit_signal, processor; - long int cutime, cstime, priority, nice, placeholder, itrealvalue, rss; - unsigned long int flags, minflt, cminflt, majflt, cmajflt, utime, stime, starttime, vsize, rlim, - startcode, endcode, startstack, kstkesp, kstkeip, signal, blocked, sigignore, sigcatch, - wchan, nswap, cnswap; - char comm[1024]; - - /// Maximum length of a /proc/[PID]/stat filename. - constexpr size_t FN_SIZE = 256; - char fn[FN_SIZE]; - std::snprintf(fn, FN_SIZE, "/proc/%d/stat", inpid); - // Don't use autoclose_fd here, we will fdopen() and then fclose() instead. - int fd = open_cloexec(fn, O_RDONLY); - if (fd < 0) return 0; - - FILE *f = fdopen(fd, "r"); - int count = fscanf(f, - "%9d %1023s %c %9d %9d %9d %9d %9d %9lu " - "%9lu %9lu %9lu %9lu %9lu %9lu %9ld %9ld %9ld " - "%9ld %9ld %9ld %9lu %9lu %9ld %9lu %9lu %9lu " - "%9lu %9lu %9lu %9lu %9lu %9lu %9lu %9lu %9lu " - "%9lu %9d %9d ", - &pid, comm, &state, &ppid, &pgrp, &session, &tty_nr, &tpgid, &flags, &minflt, - &cminflt, &majflt, &cmajflt, &utime, &stime, &cutime, &cstime, &priority, - &nice, &placeholder, &itrealvalue, &starttime, &vsize, &rss, &rlim, - &startcode, &endcode, &startstack, &kstkesp, &kstkeip, &signal, &blocked, - &sigignore, &sigcatch, &wchan, &nswap, &cnswap, &exit_signal, &processor); - fclose(f); - if (count < 17) return 0; - return clock_ticks_t(utime) + clock_ticks_t(stime) + clock_ticks_t(cutime) + - clock_ticks_t(cstime); -} - -/// Update the CPU time for all jobs. -void proc_update_jiffies(parser_t &parser) { - for (const auto &job : parser.jobs()) { - for (process_ptr_t &p : job->processes) { - p->last_time = timef(); - p->last_jiffies = proc_get_jiffies(p->pid); - } - } -} - -// static -bool tty_transfer_t::try_transfer(const job_group_ref_t &jg) { - assert(jg && "Null job group"); - if (!jg->wants_terminal()) { - // The job doesn't want the terminal. - return false; - } - - // Get the pgid; we must have one if we want the terminal. - pid_t pgid = jg->get_pgid()->value; - assert(pgid >= 0 && "Invalid pgid"); - - // It should never be fish's pgroup. - pid_t fish_pgrp = getpgrp(); - assert(pgid != fish_pgrp && "Job should not have fish's pgroup"); - - // Ok, we want to transfer to the child. - // Note it is important to be very careful about calling tcsetpgrp()! - // fish ignores SIGTTOU which means that it has the power to reassign the tty even if it doesn't - // own it. This means that other processes may get SIGTTOU and become zombies. - // Check who own the tty now. There's four cases of interest: - // 1. There is no tty at all (tcgetpgrp() returns -1). For example running from a pure script. - // Of course do not transfer it in that case. - // 2. The tty is owned by the process. This comes about often, as the process will call - // tcsetpgrp() on itself between fork ane exec. This is the essential race inherent in - // tcsetpgrp(). In this case we want to reclaim the tty, but do not need to transfer it - // ourselves since the child won the race. - // 3. The tty is owned by a different process. This may come about if fish is running in the - // background with job control enabled. Do not transfer it. - // 4. The tty is owned by fish. In that case we want to transfer the pgid. - pid_t current_owner = tcgetpgrp(STDIN_FILENO); - if (current_owner < 0) { - // Case 1. - return false; - } else if (current_owner == pgid) { - // Case 2. - return true; - } else if (current_owner != pgid && current_owner != fish_pgrp) { - // Case 3. - return false; - } - // Case 4 - we do want to transfer it. - - // The tcsetpgrp(2) man page says that EPERM is thrown if "pgrp has a supported value, but - // is not the process group ID of a process in the same session as the calling process." - // Since we _guarantee_ that this isn't the case (the child calls setpgid before it calls - // SIGSTOP, and the child was created in the same session as us), it seems that EPERM is - // being thrown because of an caching issue - the call to tcsetpgrp isn't seeing the - // newly-created process group just yet. On this developer's test machine (WSL running Linux - // 4.4.0), EPERM does indeed disappear on retry. The important thing is that we can - // guarantee the process isn't going to exit while we wait (which would cause us to possibly - // block indefinitely). - while (tcsetpgrp(STDIN_FILENO, pgid) != 0) { - FLOGF(proc_termowner, L"tcsetpgrp failed: %d", errno); - - // Before anything else, make sure that it's even necessary to call tcsetpgrp. - // Since it usually _is_ necessary, we only check in case it fails so as to avoid the - // unnecessary syscall and associated context switch, which profiling has shown to have - // a significant cost when running process groups in quick succession. - int getpgrp_res = tcgetpgrp(STDIN_FILENO); - if (getpgrp_res < 0) { - switch (errno) { - case ENOTTY: - // stdin is not a tty. This may come about if job control is enabled but we are - // not a tty - see #6573. - return false; - case EBADF: - // stdin has been closed. Workaround a glibc bug - see #3644. - redirect_tty_output(); - return false; - default: - wperror(L"tcgetpgrp"); - return false; - } - } - if (getpgrp_res == pgid) { - FLOGF(proc_termowner, L"Process group %d already has control of terminal", pgid); - return true; - } - - bool pgroup_terminated = false; - if (errno == EINVAL) { - // OS X returns EINVAL if the process group no longer lives. Probably other OSes, - // too. Unlike EPERM below, EINVAL can only happen if the process group has - // terminated. - pgroup_terminated = true; - } else if (errno == EPERM) { - // Retry so long as this isn't because the process group is dead. - int wait_result = waitpid(-1 * pgid, &wait_result, WNOHANG); - if (wait_result == -1) { - // Note that -1 is technically an "error" for waitpid in the sense that an - // invalid argument was specified because no such process group exists any - // longer. This is the observed behavior on Linux 4.4.0. a "success" result - // would mean processes from the group still exist but is still running in some - // state or the other. - pgroup_terminated = true; - } else { - // Debug the original tcsetpgrp error (not the waitpid errno) to the log, and - // then retry until not EPERM or the process group has exited. - FLOGF(proc_termowner, L"terminal_give_to_job(): EPERM with pgid %d.", pgid); - continue; - } - } else if (errno == ENOTTY) { - // stdin is not a TTY. In general we expect this to be caught via the tcgetpgrp - // call's EBADF handler above. - return false; - } else { - FLOGF(warning, _(L"Could not send job %d ('%ls') with pgid %d to foreground"), - jg->get_job_id(), jg->get_command()->c_str(), pgid); - wperror(L"tcsetpgrp"); - return false; - } - - if (pgroup_terminated) { - // All processes in the process group has exited. - // Since we delay reaping any processes in a process group until all members of that - // job/group have been started, the only way this can happen is if the very last - // process in the group terminated and didn't need to access the terminal, otherwise - // it would have hung waiting for terminal IO (SIGTTIN). We can safely ignore this. - FLOGF(proc_termowner, L"tcsetpgrp called but process group %d has terminated.\n", pgid); - return false; - } - - break; - } - return true; -} - -bool job_t::is_foreground() const { return group->is_foreground(); } - -maybe_t job_t::get_pgid() const { - auto pgid = group->get_pgid(); - if (!pgid) { - return none(); - } - return maybe_t{pgid->value}; -} - -maybe_t job_t::get_last_pid() const { - for (auto iter = processes.rbegin(); iter != processes.rend(); ++iter) { - const process_t *proc = iter->get(); - if (proc->pid > 0) return proc->pid; - } - return none(); -} - -job_id_t job_t::job_id() const { return group->get_job_id(); } - -bool job_t::resume() { - mut_flags().notified_of_stop = false; - if (!this->signal(SIGCONT)) { - FLOGF(proc_pgroup, "Failed to send SIGCONT to procs in job %ls", this->command_wcstr()); - return false; - } - - // reset the status of each process instance - for (auto &p : this->processes) { - p->stopped = false; - } - return true; -} - -void job_t::continue_job(parser_t &parser) { - FLOGF(proc_job_run, L"Run job %d (%ls), %ls, %ls", job_id(), command_wcstr(), - is_completed() ? L"COMPLETED" : L"UNCOMPLETED", - parser.libdata().is_interactive ? L"INTERACTIVE" : L"NON-INTERACTIVE"); - - // Wait for the status of our own job to change. - while (!fish_is_unwinding_for_exit() && !is_stopped() && !is_completed()) { - process_mark_finished_children(parser, true); - } - if (is_completed()) { - // Set $status only if we are in the foreground and the last process in the job has - // finished. - const auto &p = processes.back(); - if (p->status.normal_exited() || p->status.signal_exited()) { - auto statuses = get_statuses(); - if (statuses) { - parser.set_last_statuses(statuses.value()); - parser.libdata().status_count++; - } - } - } -} - -void proc_wait_any(parser_t &parser) { - process_mark_finished_children(parser, true /* block_ok */); - process_clean_after_marking(parser, parser.libdata().is_interactive); -} - -void hup_jobs(const job_list_t &jobs) { - pid_t fish_pgrp = getpgrp(); - for (const auto &j : jobs) { - auto pgid = j->get_pgid(); - if (pgid.has_value() && *pgid != fish_pgrp && !j->is_completed()) { - if (j->is_stopped()) { - j->signal(SIGCONT); - } - j->signal(SIGHUP); - } - } -} - -void tty_transfer_t::to_job_group(const job_group_ref_t &jg) { - assert(!owner_ && "Terminal already transferred"); - if (tty_transfer_t::try_transfer(jg)) { - owner_ = jg; - } -} - -void tty_transfer_t::save_tty_modes() { - if (owner_) { - struct termios tmodes {}; - if (tcgetattr(STDIN_FILENO, &tmodes) == 0) { - owner_->set_modes_ffi((uint8_t *)&tmodes, sizeof(struct termios)); - } else if (errno != ENOTTY) { - wperror(L"tcgetattr"); - } - } -} - -void tty_transfer_t::reclaim() { - if (this->owner_) { - FLOG(proc_pgroup, "fish reclaiming terminal"); - if (tcsetpgrp(STDIN_FILENO, getpgrp()) == -1) { - FLOGF(warning, _(L"Could not return shell to foreground")); - wperror(L"tcsetpgrp"); - } - this->owner_ = nullptr; - } -} - -tty_transfer_t::~tty_transfer_t() { assert(!this->owner_ && "Forgot to reclaim() the tty"); } diff --git a/src/proc.h b/src/proc.h index 76ae54d05..a0c3ca83a 100644 --- a/src/proc.h +++ b/src/proc.h @@ -22,609 +22,17 @@ #include "cxx.h" #include "maybe.h" #include "parse_tree.h" +#include "parser.h" #include "redirection.h" -#include "topic_monitor.h" -#include "wait_handle.h" -struct statuses_t; +struct Parser; -/// Types of processes. -enum class process_type_t : uint8_t { - /// A regular external command. - external, - /// A builtin command. - builtin, - /// A shellscript function. - function, - /// A block of commands, represented as a node. - block_node, - /// The exec builtin. - exec, -}; - -enum class job_control_t : uint8_t { - all, - interactive, - none, -}; - -/// A number of clock ticks. -using clock_ticks_t = uint64_t; - -/// \return clock ticks in seconds, or 0 on failure. -/// This uses sysconf(_SC_CLK_TCK) to convert to seconds. -double clock_ticks_to_seconds(clock_ticks_t ticks); - -struct job_group_t; -using job_group_ref_t = std::shared_ptr; - -/// A proc_status_t is a value type that encapsulates logic around exited vs stopped vs signaled, -/// etc. -class proc_status_t { - int status_{}; - - /// If set, there is no actual status to report, e.g. background or variable assignment. - bool empty_{}; - - explicit proc_status_t(int status) : status_(status), empty_(false) {} - - proc_status_t(int status, bool empty) : status_(status), empty_(empty) {} - - /// Encode a return value \p ret and signal \p sig into a status value like waitpid() does. - static constexpr int w_exitcode(int ret, int sig) { -#ifdef W_EXITCODE - return W_EXITCODE(ret, sig); -#elif HAVE_WAITSTATUS_SIGNAL_RET - // It's encoded signal and then status - // The return status is in the lower byte. - return ((sig) << 8 | (ret)); +#if INCLUDE_RUST_HEADERS +#include "proc.rs.h" #else - // The status is encoded in the upper byte. - return ((ret) << 8 | (sig)); +struct JobRefFfi; +struct JobGroupRefFfi; +struct JobListFFI; #endif - } - - public: - proc_status_t() = default; - - /// Construct from a status returned from a waitpid call. - static proc_status_t from_waitpid(int status) { return proc_status_t(status); } - - /// Construct directly from an exit code. - static proc_status_t from_exit_code(int ret) { - assert(ret >= 0 && - "trying to create proc_status_t from failed wait{,id,pid}() call" - " or invalid builtin exit code!"); - - // Some paranoia. - constexpr int zerocode = w_exitcode(0, 0); - static_assert(WIFEXITED(zerocode), "Synthetic exit status not reported as exited"); - - assert(ret < 256); - return proc_status_t(w_exitcode(ret, 0 /* sig */)); - } - - /// Construct directly from a signal. - static proc_status_t from_signal(int sig) { - return proc_status_t(w_exitcode(0 /* ret */, sig)); - } - - /// Construct an empty status_t (e.g. `set foo bar`). - static proc_status_t empty() { - bool empty = true; - return proc_status_t(0, empty); - } - - /// \return if we are stopped (as in SIGSTOP). - bool stopped() const { return WIFSTOPPED(status_); } - - /// \return if we are continued (as in SIGCONT). - bool continued() const { return WIFCONTINUED(status_); } - - /// \return if we exited normally (not a signal). - bool normal_exited() const { return WIFEXITED(status_); } - - /// \return if we exited because of a signal. - bool signal_exited() const { return WIFSIGNALED(status_); } - - /// \return the signal code, given that we signal exited. - int signal_code() const { - assert(signal_exited() && "Process is not signal exited"); - return WTERMSIG(status_); - } - - /// \return the exit code, given that we normal exited. - int exit_code() const { - assert(normal_exited() && "Process is not normal exited"); - return WEXITSTATUS(status_); - } - - /// \return if this status represents success. - bool is_success() const { return normal_exited() && exit_code() == EXIT_SUCCESS; } - - /// \return if this status is empty. - bool is_empty() const { return empty_; } - - /// \return the value appropriate to populate $status. - int status_value() const { - if (signal_exited()) { - return 128 + signal_code(); - } else if (normal_exited()) { - return exit_code(); - } else { - DIE("Process is not exited"); - } - } -}; - -/// A structure representing a "process" internal to fish. This is backed by a pthread instead of a -/// separate process. -class internal_proc_t { - /// An identifier for internal processes. - /// This is used for logging purposes only. - const uint64_t internal_proc_id_; - - /// Whether the process has exited. - std::atomic exited_{}; - - /// If the process has exited, its status code. - std::atomic status_{}; - - public: - /// \return if this process has exited. - bool exited() const { return exited_.load(std::memory_order_acquire); } - - /// Mark this process as exited, with the given status. - void mark_exited(proc_status_t status); - - proc_status_t get_status() const { - assert(exited() && "Process is not exited"); - return status_.load(std::memory_order_relaxed); - } - - uint64_t get_id() const { return internal_proc_id_; } - - internal_proc_t(); -}; - -/// 0 should not be used; although it is not a valid PGID in userspace, -/// the Linux kernel will use it for kernel processes. -/// -1 should not be used; it is a possible return value of the getpgid() -/// function -enum { INVALID_PID = -2 }; - -// Allows transferring the tty to a job group, while it runs. -class tty_transfer_t : nonmovable_t, noncopyable_t { - public: - tty_transfer_t() = default; - - /// Transfer to the given job group, if it wants to own the terminal. - void to_job_group(const job_group_ref_t &jg); - - /// Reclaim the tty if we transferred it. - void reclaim(); - - /// Save the current tty modes into the owning job group, if we are transferred. - void save_tty_modes(); - - /// The destructor will assert if reclaim() has not been called. - ~tty_transfer_t(); - - private: - // Try transferring the tty to the given job group. - // \return true if we should reclaim it. - static bool try_transfer(const job_group_ref_t &jg); - - // The job group which owns the tty, or empty if none. - job_group_ref_t owner_; -}; - -/// A structure representing a single fish process. Contains variables for tracking process state -/// and the process argument list. Actually, a fish process can be either a regular external -/// process, an internal builtin which may or may not spawn a fake IO process during execution, a -/// shellscript function or a block of commands to be evaluated by calling eval. Lastly, this -/// process can be the result of an exec command. The role of this process_t is determined by the -/// type field, which can be one of process_type_t::external, process_type_t::builtin, -/// process_type_t::function, process_type_t::exec. -/// -/// The process_t contains information on how the process should be started, such as command name -/// and arguments, as well as runtime information on the status of the actual physical process which -/// represents it. Shellscript functions, builtins and blocks of code may all need to spawn an -/// external process that handles the piping and redirecting of IO for them. -/// -/// If the process is of type process_type_t::external or process_type_t::exec, argv is the argument -/// array and actual_cmd is the absolute path of the command to execute. -/// -/// If the process is of type process_type_t::builtin, argv is the argument vector, and argv[0] is -/// the name of the builtin command. -/// -/// If the process is of type process_type_t::function, argv is the argument vector, and argv[0] is -/// the name of the shellscript function. -class process_t { - public: - process_t(); - - /// Note whether we are the first and/or last in the job - bool is_first_in_job{false}; - bool is_last_in_job{false}; - - /// Type of process. - process_type_t type{process_type_t::external}; - - /// For internal block processes only, the node of the statement. - /// This is always either block, ifs, or switchs, never boolean or decorated. - rust::Box block_node_source; - const ast::statement_t *internal_block_node{}; - - struct concrete_assignment { - wcstring variable_name; - std::vector values; - }; - /// The expanded variable assignments for this process, as specified by the `a=b cmd` syntax. - std::vector variable_assignments; - - /// Sets argv. - void set_argv(std::vector argv) { argv_ = std::move(argv); } - - /// Returns argv. - const std::vector &argv() { return argv_; } - - /// Returns argv[0], or nullptr. - const wchar_t *argv0() const { return argv_.empty() ? nullptr : argv_.front().c_str(); } - - /// Redirection list getter and setter. - const redirection_spec_list_t &redirection_specs() const { return *proc_redirection_specs_; } - - void set_redirection_specs(rust::Box specs) { - this->proc_redirection_specs_ = std::move(specs); - } - - /// Store the current topic generations. That is, right before the process is launched, record - /// the generations of all topics; then we can tell which generation values have changed after - /// launch. This helps us avoid spurious waitpid calls. - void check_generations_before_launch(); - - /// Mark that this process was part of a pipeline which was aborted. - /// The process was never successfully launched; give it a status of EXIT_FAILURE. - void mark_aborted_before_launch(); - - /// \return whether this process type is internal (block, function, or builtin). - bool is_internal() const; - - /// \return whether this process leads its process group. - bool get_leads_pgrp() const { return leads_pgrp; } - - /// \return our pid, or 0 if not an external process. - int get_pid() const { return pid; } - - /// \return the wait handle for the process, if it exists. - rust::Box *get_wait_handle_ffi() const; - - /// Create a wait handle for the process. - /// As a process does not know its job id, we pass it in. - /// Note this will return null if the process is not waitable (has no pid). - rust::Box *make_wait_handle_ffi(internal_job_id_t jid); - - /// Variants of get and make that return void*, to satisfy autocxx. - void *get_wait_handle_void() const; - void *make_wait_handle_void(internal_job_id_t jid); - - /// Actual command to pass to exec in case of process_type_t::external or process_type_t::exec. - wcstring actual_cmd; - - /// Generation counts for reaping. - generation_list_t gens_{}; - - /// Process ID - pid_t pid{0}; - - /// If we are an "internal process," that process. - std::shared_ptr internal_proc_{}; - - /// File descriptor that pipe output should bind to. - int pipe_write_fd{0}; - - /// True if process has completed. - bool completed{false}; - - /// True if process has stopped. - bool stopped{false}; - - /// If set, this process is (or will become) the pgroup leader. - /// This is only meaningful for external processes. - bool leads_pgrp{false}; - - /// Whether we have generated a proc_exit event. - bool posted_proc_exit{false}; - - /// Reported status value. - proc_status_t status{}; - - /// Last time of cpu time check, in seconds (per timef). - timepoint_t last_time{0}; - - /// Number of jiffies spent in process at last cpu time check. - clock_ticks_t last_jiffies{0}; - - process_t(process_t &&) = delete; - process_t &operator=(process_t &&) = delete; - process_t(const process_t &) = delete; - process_t &operator=(const process_t &) = delete; - - private: - std::vector argv_; - rust::Box proc_redirection_specs_; - - // The wait handle. This is constructed lazily, and cached. - // This may be null. - std::unique_ptr> wait_handle_; -}; - -using process_ptr_t = std::unique_ptr; -using process_list_t = std::vector; -class Parser; using parser_t = Parser; - -struct RustFFIProcList { - process_ptr_t *procs; - size_t count; -}; - -/// A struct representing a job. A job is a pipeline of one or more processes. -class job_t : noncopyable_t { - public: - /// A set of jobs properties. These are immutable: they do not change for the lifetime of the - /// job. - struct properties_t { - /// Whether the specified job is a part of a subshell, event handler or some other form of - /// special job that should not be reported. - bool skip_notification{}; - - /// Whether the job had the background ampersand when constructed, e.g. /bin/echo foo & - /// Note that a job may move between foreground and background; this just describes what the - /// initial state should be. - bool initial_background{}; - - /// Whether the job has the 'time' prefix and so we should print timing for this job. - bool wants_timing{}; - - /// Whether this job was created as part of an event handler. - bool from_event_handler{}; - }; - - private: - /// Set of immutable job properties. - const properties_t properties; - - /// The original command which led to the creation of this job. It is used for displaying - /// messages about job status on the terminal. - const wcstring command_str; - - public: - job_t(const properties_t &props, wcstring command_str); - ~job_t(); - - /// Autocxx needs to see this. - job_t(const job_t &) = delete; - - /// Returns the command as a wchar_t *. */ - const wchar_t *command_wcstr() const { return command_str.c_str(); } - - /// Returns the command. - const wcstring &command() const { return command_str; } - - /// \return whether it is OK to reap a given process. Sometimes we want to defer reaping a - /// process if it is the group leader and the job is not yet constructed, because then we might - /// also reap the process group and then we cannot add new processes to the group. - bool can_reap(const process_ptr_t &p) const { - if (p->completed) { - // Can't reap twice. - return false; - } else if (p->pid && !is_constructed() && this->get_pgid() == maybe_t{p->pid}) { - // p is the the group leader in an under-construction job. - return false; - } else { - return true; - } - } - - /// Returns a truncated version of the job string. Used when a message has already been emitted - /// containing the full job string and job id, but using the job id alone would be confusing - /// due to reuse of freed job ids. Prevents overloading the debug comments with the full, - /// untruncated job string when we don't care what the job is, only which of the currently - /// running jobs it is. - wcstring preview() const { - if (processes.empty()) return L""; - // Note argv0 may be empty in e.g. a block process. - const wchar_t *argv0 = processes.front()->argv0(); - wcstring result = argv0 ? argv0 : L"null"; - return result + L" ..."; - } - - /// All the processes in this job. - process_list_t processes; - - // The group containing this job. - // This is never null and not changed after construction. - job_group_ref_t group{}; - - /// \return our pgid, or none if we don't have one, or are internal to fish - /// This never returns fish's own pgroup. - maybe_t get_pgid() const; - - /// \return the pid of the last external process in the job. - /// This may be none if the job consists of just internal fish functions or builtins. - /// This will never be fish's own pid. - maybe_t get_last_pid() const; - - /// The id of this job. - /// This is user-visible, is recycled, and may be -1. - job_id_t job_id() const; - - /// A non-user-visible, never-recycled job ID. - const internal_job_id_t internal_job_id; - - /// Getter to enable ffi. - internal_job_id_t get_internal_job_id() const { return internal_job_id; } - - /// Flags associated with the job. - struct flags_t { - /// Whether the specified job is completely constructed: every process in the job has been - /// forked, etc. - bool constructed{false}; - - /// Whether the user has been notified that this job is stopped (if it is). - bool notified_of_stop{false}; - - /// Whether the exit status should be negated. This flag can only be set by the not builtin. - /// Two "not" prefixes on a single job cancel each other out. - bool negate{false}; - - /// This job is disowned, and should be removed from the active jobs list. - bool disown_requested{false}; - - // Indicates that we are the "group root." Any other jobs using this tree are nested. - bool is_group_root{false}; - - } job_flags{}; - - /// Access the job flags. - const flags_t &flags() const { return job_flags; } - - /// Access mutable job flags. - flags_t &mut_flags() { return job_flags; } - - // \return whether we should print timing information. - bool wants_timing() const { return properties.wants_timing; } - - /// \return if we want job control. - bool wants_job_control() const; - - /// \return whether this job is initially going to run in the background, because & was - /// specified. - bool is_initially_background() const { return properties.initial_background; } - - /// Mark this job as constructed. The job must not have previously been marked as constructed. - void mark_constructed(); - - /// \return whether we have internal or external procs, respectively. - /// Internal procs are builtins, blocks, and functions. - /// External procs include exec and external. - bool has_external_proc() const; - - /// \return whether this job, when run, will want a job ID. - /// Jobs that are only a single internal block do not get a job ID. - bool wants_job_id() const; - - // Helper functions to check presence of flags on instances of jobs - /// The job has been fully constructed, i.e. all its member processes have been launched - bool is_constructed() const { return flags().constructed; } - /// The job is complete, i.e. all its member processes have been reaped - bool is_completed() const; - /// The job is in a stopped state - bool is_stopped() const; - /// The job is OK to be externally visible, e.g. to the user via `jobs` - bool is_visible() const { - return !is_completed() && is_constructed() && !flags().disown_requested; - } - bool skip_notification() const { return properties.skip_notification; } - bool from_event_handler() const { return properties.from_event_handler; } - - /// \return whether this job's group is in the foreground. - bool is_foreground() const; - - /// \return whether we should post job_exit events. - bool posts_job_exit_events() const; - - /// Run ourselves. Returning once we complete or stop. - void continue_job(parser_t &parser); - - /// Prepare to resume a stopped job by sending SIGCONT and clearing the stopped flag. - /// \return true on success, false if we failed to send the signal. - bool resume(); - - /// Send the specified signal to all processes in this job. - /// \return true on success, false on failure. - bool signal(int signal); - - /// \returns the statuses for this job. - maybe_t get_statuses() const; - - /// autocxx junk. - RustFFIProcList ffi_processes() const; - - /// autocxx junk. - const job_group_t &ffi_group() const; - - /// autocxx junk. - /// The const is a lie and is only necessary since at the moment cxx's SharedPtr doesn't support - /// getting a mutable reference. - bool ffi_resume() const; -}; -using job_ref_t = std::shared_ptr; - -// Helper junk for autocxx. -struct RustFFIJobList { - job_ref_t *jobs; - size_t count; -}; - -/// Whether this shell is attached to a tty. -bool is_interactive_session(); -void set_interactive_session(bool flag); - -/// Whether we are a login shell. -bool get_login(); -void mark_login(); - -/// If this flag is set, fish will never fork or run execve. It is used to put fish into a syntax -/// verifier mode where fish tries to validate the syntax of a file but doesn't actually do -/// anything. -bool no_exec(); -void mark_no_exec(); - -// List of jobs. -using job_list_t = std::vector; - -/// The current job control mode. -/// -/// Must be one of job_control_t::all, job_control_t::interactive and job_control_t::none. -job_control_t get_job_control_mode(); -void set_job_control_mode(job_control_t mode); - -/// Notify the user about stopped or terminated jobs, and delete completed jobs from the job list. -/// If \p interactive is set, allow removing interactive jobs; otherwise skip them. -/// \return whether text was printed to stdout. -bool job_reap(parser_t &parser, bool interactive); - -/// \return the list of background jobs which we should warn the user about, if the user attempts to -/// exit. An empty result (common) means no such jobs. -job_list_t jobs_requiring_warning_on_exit(const parser_t &parser); - -/// Print the exit warning for the given jobs, which should have been obtained via -/// jobs_requiring_warning_on_exit(). -void print_exit_warning_for_jobs(const job_list_t &jobs); - -/// Use the procfs filesystem to look up how many jiffies of cpu time was used by a given pid. This -/// function is only available on systems with the procfs file entry 'stat', i.e. Linux. -clock_ticks_t proc_get_jiffies(pid_t inpid); - -/// Update process time usage for all processes by calling the proc_get_jiffies function for every -/// process of every job. -void proc_update_jiffies(parser_t &parser); - -/// Initializations. -void proc_init(); - -/// Wait for any process finishing, or receipt of a signal. -void proc_wait_any(parser_t &parser); - -/// Send SIGHUP to the list \p jobs, excepting those which are in fish's pgroup. -void hup_jobs(const job_list_t &jobs); - -/// Add a job to the list of PIDs/PGIDs we wait on even though they are not associated with any -/// jobs. Used to avoid zombie processes after disown. -void add_disowned_job(const job_t *j); - -bool have_proc_stat(); #endif diff --git a/src/reader.cpp b/src/reader.cpp index 153d117cb..e14b79adb 100644 --- a/src/reader.cpp +++ b/src/reader.cpp @@ -16,6 +16,8 @@ #include #include #include + +#include "history.rs.h" #ifdef HAVE_SIGINFO_H #include #endif @@ -116,10 +118,6 @@ /// more input without repainting. static constexpr size_t READAHEAD_MAX = 256; -/// When tab-completing with a wildcard, we expand the wildcard up to this many results. -/// If expansion would exceed this many results, beep and do nothing. -static const size_t TAB_COMPLETE_WILDCARD_MAX_EXPANSION = 256; - /// A mode for calling the reader_kill function. In this mode, the new string is appended to the /// current contents of the kill buffer. #define KILL_APPEND 0 @@ -139,7 +137,35 @@ static acquired_lock commandline_state_snapshot() { return s_state->acquire(); } -commandline_state_t commandline_get_state() { return *commandline_state_snapshot(); } +commandline_state_t commandline_get_state() { + auto s = commandline_state_snapshot(); + + commandline_state_t state{}; + state.text = s->text; + state.cursor_pos = s->cursor_pos; + state.selection = s->selection; + if (s->history) { + state.history = (*s->history)->clone(); + } + state.pager_mode = s->pager_mode; + state.pager_fully_disclosed = s->pager_fully_disclosed; + state.search_mode = s->search_mode; + state.initialized = s->initialized; + + return state; +} + +HistorySharedPtr *commandline_get_state_history_ffi() { + auto state = commandline_get_state(); + if (!state.history) return nullptr; + return state.history->into_raw(); +} + +bool commandline_get_state_initialized_ffi() { return commandline_get_state().initialized; } +wcstring commandline_get_state_text_ffi() { + // return std::make_unique(commandline_get_state().text); + return std::move(commandline_get_state().text); +} void commandline_set_buffer(wcstring text, size_t cursor_pos) { auto state = commandline_state_snapshot(); @@ -147,26 +173,16 @@ void commandline_set_buffer(wcstring text, size_t cursor_pos) { state->text = std::move(text); } +void commandline_set_buffer_ffi(const wcstring &text, size_t cursor_pos) { + commandline_set_buffer(text, cursor_pos); +} + /// Any time the contents of a buffer changes, we update the generation count. This allows for our /// background threads to notice it and skip doing work that they would otherwise have to do. static std::atomic s_generation; /// Helper to get the generation count -static inline uint32_t read_generation_count() { - return s_generation.load(std::memory_order_relaxed); -} - -/// \return an operation context for a background operation.. -/// Crucially the operation context itself does not contain a parser. -/// It is the caller's responsibility to ensure the environment lives as long as the result. -static operation_context_t get_bg_context(const std::shared_ptr &env, - uint32_t generation_count) { - cancel_checker_t cancel_checker = [generation_count] { - // Cancel if the generation count changed. - return generation_count != read_generation_count(); - }; - return operation_context_t{nullptr, *env, std::move(cancel_checker), kExpansionLimitBackground}; -} +uint32_t read_generation_count() { return s_generation.load(std::memory_order_relaxed); } /// We try to ensure that syntax highlighting completes appropriately before executing what the user /// typed. But we do not want it to block forever - e.g. it may hang on determining if an arbitrary @@ -233,6 +249,11 @@ static size_t cursor_position_after_edit(const edit_t &edit) { return cursor > removed ? cursor - removed : 0; } +void editable_line_t::set_colors(std::vector colors) { + assert(colors.size() == size()); + colors_ = std::move(colors); +} + bool editable_line_t::undo() { bool did_undo = false; maybe_t last_group_id{-1}; @@ -395,7 +416,7 @@ class reader_history_search_t { mode_t mode_{inactive}; /// Our history search itself. - history_search_t search_; + maybe_t> search_; /// The ordered list of matches. This may grow long. std::vector matches_; @@ -420,13 +441,13 @@ class reader_history_search_t { /// \return true if something was appended. bool append_matches_from_search() { auto find = [this](const wcstring &haystack, const wcstring &needle) { - if (search_.ignores_case()) { + if ((*search_)->ignores_case()) { return ifind(haystack, needle); } return haystack.find(needle); }; const size_t before = matches_.size(); - wcstring text = search_.current_string(); + auto text = *(*search_)->current_string(); const wcstring &needle = search_string(); if (mode_ == line || mode_ == prefix) { size_t offset = find(text, needle); @@ -477,7 +498,7 @@ class reader_history_search_t { } // Add more items from our search. - while (search_.go_to_next_match(history_search_direction_t::backward)) { + while ((*search_)->go_to_next_match(history_search_direction_t::Backward)) { if (append_matches_from_search()) { match_index_++; assert(match_index_ < matches_.size() && "Should have found more matches"); @@ -503,7 +524,7 @@ class reader_history_search_t { /// Move the history search in the given direction \p dir. bool move_in_direction(history_search_direction_t dir) { - return dir == history_search_direction_t::forward ? move_forwards() : move_backwards(); + return dir == history_search_direction_t::Forward ? move_forwards() : move_backwards(); } /// Go to the beginning (earliest) of the search. @@ -522,7 +543,7 @@ class reader_history_search_t { } /// \return the string we are searching for. - const wcstring &search_string() const { return search_.original_term(); } + const wcstring search_string() const { return *(*search_)->original_term(); } /// \return the range of the original search string in the new command line. maybe_t search_range_if_active() const { @@ -541,7 +562,7 @@ class reader_history_search_t { bool add_skip(const wcstring &str) { return skips_.insert(str).second; } /// Reset, beginning a new line or token mode search. - void reset_to_mode(const wcstring &text, const std::shared_ptr &hist, mode_t mode, + void reset_to_mode(const wcstring &text, const HistorySharedPtr &hist, mode_t mode, size_t token_offset) { assert(mode != inactive && "mode cannot be inactive in this setter"); skips_ = {text}; @@ -551,9 +572,10 @@ class reader_history_search_t { token_offset_ = token_offset; history_search_flags_t flags = history_search_no_dedup | smartcase_flags(text); // We can skip dedup in history_search_t because we do it ourselves in skips_. - search_ = history_search_t( - hist, text, - by_prefix() ? history_search_type_t::prefix : history_search_type_t::contains, flags); + search_ = rust_history_search_new( + hist, text.c_str(), + by_prefix() ? history_search_type_t::Prefix : history_search_type_t::Contains, flags, + 0); } /// Reset to inactive search. @@ -563,7 +585,7 @@ class reader_history_search_t { match_index_ = 0; mode_ = inactive; token_offset_ = 0; - search_ = history_search_t(); + search_ = maybe_t>{}; } }; @@ -602,7 +624,7 @@ struct highlight_result_t { }; struct history_pager_result_t { - completion_list_t matched_commands; + rust::Box matched_commands; size_t final_index; bool have_more_results; }; @@ -621,7 +643,7 @@ struct readline_loop_state_t { bool complete_did_insert{true}; /// List of completions. - completion_list_t comp; + rust::Box comp = new_completion_list(); /// Whether the loop has finished, due to reaching the character limit or through executing a /// command. @@ -690,7 +712,7 @@ class reader_data_t : public std::enable_shared_from_this { /// Configuration for the reader. reader_config_t conf; /// The parser being used. - std::shared_ptr parser_ref; + rust::Box parser_ref; /// String containing the whole current commandline. editable_line_t command_line; /// Whether the most recent modification to the command line was done by either history search @@ -723,7 +745,7 @@ class reader_data_t : public std::enable_shared_from_this { /// The source of input events. inputter_t inputter; /// The history. - std::shared_ptr history{}; + maybe_t> history{}; /// The history search. reader_history_search_t history_search{}; /// Whether the in-pager history search is active. @@ -793,14 +815,10 @@ class reader_data_t : public std::enable_shared_from_this { /// Do what we need to do whenever our command line changes. void command_line_changed(const editable_line_t *el); void maybe_refilter_pager(const editable_line_t *el); - enum class history_pager_invocation_t { - anew, - advance, - refresh, - }; + using history_pager_invocation_t = HistoryPagerInvocation; void fill_history_pager( history_pager_invocation_t why, - history_search_direction_t direction = history_search_direction_t::backward); + history_search_direction_t direction = history_search_direction_t::Backward); /// Do what we need to do whenever our pager selection changes. void pager_selection_changed(); @@ -829,22 +847,20 @@ class reader_data_t : public std::enable_shared_from_this { void paint_layout(const wchar_t *reason); /// Return the variable set used for e.g. command duration. - env_stack_t &vars() { return parser_ref->vars(); } - const env_stack_t &vars() const { return parser_ref->vars(); } + // todo!("should return a reference") + env_stack_t vars() const { return env_stack_t{parser_ref->deref().vars_boxed()}; } /// Access the parser. - parser_t &parser() { return *parser_ref; } - const parser_t &parser() const { return *parser_ref; } + const parser_t &parser() const { return parser_ref->deref(); } /// Convenience cover over exec_count(). - uint64_t exec_count() const { return parser().libdata().exec_count; } + uint64_t exec_count() const { return parser().libdata().exec_count(); } - reader_data_t(std::shared_ptr parser, std::shared_ptr hist, - reader_config_t &&conf) + reader_data_t(rust::Box parser, HistorySharedPtr &hist, reader_config_t &&conf) : conf(std::move(conf)), parser_ref(std::move(parser)), - inputter(*parser_ref, conf.in), - history(std::move(hist)) {} + inputter(parser_ref->deref(), conf.in), + history(hist.clone()) {} void update_buff_pos(editable_line_t *el, maybe_t new_pos = none_t()); @@ -877,10 +893,6 @@ class reader_data_t : public std::enable_shared_from_this { /// Compute completions and update the pager and/or commandline as needed. void compute_and_apply_completions(readline_cmd_t c, readline_loop_state_t &rls); - /// Given that the user is tab-completing a token \p wc whose cursor is at \p pos in the token, - /// try expanding it as a wildcard, populating \p result with the expanded string. - expand_result_t::result_t try_expand_wildcard(wcstring wc, size_t pos, wcstring *result); - void move_word(editable_line_t *el, bool move_right, bool erase, move_word_style_t style, bool newv); @@ -895,7 +907,7 @@ class reader_data_t : public std::enable_shared_from_this { bool handle_execute(readline_loop_state_t &rls); // Add the current command line contents to history. - void add_to_history() const; + void add_to_history(); // Expand abbreviations before execution. // Replace the command line with any abbreviations as needed. @@ -940,15 +952,12 @@ class reader_data_t : public std::enable_shared_from_this { void delete_char(bool backward = true); /// Called to update the termsize, including $COLUMNS and $LINES, as necessary. - void update_termsize() { termsize_update_ffi(reinterpret_cast(&parser())); } + void update_termsize() { termsize_update(parser()); } // Import history from older location (config path) if our current history is empty. void import_history_if_necessary(); }; -/// This variable is set to a signal by the signal handler when ^C is pressed. -static volatile sig_atomic_t interrupted = 0; - // Prototypes for a bunch of functions defined later on. static bool is_backslashed(const wcstring &str, size_t pos); static wchar_t unescaped_quote(const wcstring &str, size_t pos); @@ -1007,21 +1016,10 @@ enum class exit_state_t { }; static relaxed_atomic_t s_exit_state{exit_state_t::none}; -/// If set, SIGHUP has been received. This latches to true. -/// This is set from a signal handler. -static volatile sig_atomic_t s_sighup_received{false}; - -extern "C" { -void reader_sighup() { - // Beware, we may be in a signal handler. - s_sighup_received = true; -} -} - static void redirect_tty_after_sighup() { // If we have received SIGHUP, redirect the tty to avoid a user script triggering SIGTTIN or // SIGTTOU. - assert(s_sighup_received && "SIGHUP not received"); + assert(reader_received_sighup() && "SIGHUP not received"); static bool s_tty_redirected = false; if (!s_tty_redirected) { s_tty_redirected = true; @@ -1082,7 +1080,7 @@ bool fish_is_unwinding_for_exit() { switch (s_exit_state) { case exit_state_t::none: // Cancel if we got SIGHUP. - return s_sighup_received; + return reader_received_sighup(); case exit_state_t::running_handlers: // We intend to exit but we want to allow these handlers to run. return false; @@ -1219,7 +1217,7 @@ void reader_data_t::paint_layout(const wchar_t *reason) { } for (size_t i = data.history_search_range->start; i < end; i++) { - colors.at(i).background = highlight_role_t::search_match; + colors.at(i)->background = highlight_role_t::search_match; } } @@ -1243,7 +1241,7 @@ void reader_data_t::paint_layout(const wchar_t *reason) { // Prepend the mode prompt to the left prompt. screen.write(mode_prompt_buff + left_prompt_buff, right_prompt_buff, full_line, - cmd_line->size(), colors, indents, data.position, parser().vars(), pager, + cmd_line->size(), colors, indents, data.position, parser().vars_boxed(), pager, current_page_rendering, data.focused_on_pager); } @@ -1267,11 +1265,6 @@ void reader_data_t::kill(editable_line_t *el, size_t begin_idx, size_t length, i erase_substring(el, begin_idx, length); } -extern "C" { -// This is called from a signal handler! -void reader_handle_sigint() { interrupted = SIGINT; } -} - /// Make sure buffers are large enough to hold the current string length. void reader_data_t::command_line_changed(const editable_line_t *el) { ASSERT_IS_MAIN_THREAD(); @@ -1280,8 +1273,8 @@ void reader_data_t::command_line_changed(const editable_line_t *el) { s_generation.store(1 + read_generation_count(), std::memory_order_relaxed); } else if (el == &this->pager.search_field_line) { if (history_pager_active) { - fill_history_pager(history_pager_invocation_t::anew, - history_search_direction_t::backward); + fill_history_pager(history_pager_invocation_t::Anew, + history_search_direction_t::Backward); return; } this->pager.refilter_completions(); @@ -1297,7 +1290,7 @@ void reader_data_t::maybe_refilter_pager(const editable_line_t *el) { } } -static history_pager_result_t history_pager_search(const std::shared_ptr &history, +static history_pager_result_t history_pager_search(const HistorySharedPtr &history, history_search_direction_t direction, size_t history_index, const wcstring &search_string) { @@ -1309,29 +1302,31 @@ static history_pager_result_t history_pager_search(const std::shared_ptr completions = new_completion_list(); + rust::Box search = + rust_history_search_new(history, search_string.c_str(), history_search_type_t::Contains, + smartcase_flags(search_string), history_index); + bool next_match_found = search->go_to_next_match(direction); if (!next_match_found) { // If there were no matches, try again with subsequence search - search = - history_search_t{history, search_string, history_search_type_t::contains_subsequence, - smartcase_flags(search_string), history_index}; - next_match_found = search.go_to_next_match(direction); + search = rust_history_search_new(history, search_string.c_str(), + history_search_type_t::ContainsSubsequence, + smartcase_flags(search_string), history_index); + next_match_found = search->go_to_next_match(direction); } - while (completions.size() < page_size && next_match_found) { - const history_item_t &item = search.current_item(); - completions.push_back(completion_t{ - item.str(), L"", string_fuzzy_match_t::exact_match(), - COMPLETE_REPLACES_COMMANDLINE | COMPLETE_DONT_ESCAPE | COMPLETE_DONT_SORT}); + while (completions->size() < page_size && next_match_found) { + const history_item_t &item = search->current_item(); + completions->push_back(*new_completion_with( + *item.str(), L"", + COMPLETE_REPLACES_COMMANDLINE | COMPLETE_DONT_ESCAPE | COMPLETE_DONT_SORT)); - next_match_found = search.go_to_next_match(direction); + next_match_found = search->go_to_next_match(direction); } - size_t last_index = search.current_index(); - if (direction == history_search_direction_t::forward) - std::reverse(completions.begin(), completions.end()); - return {completions, last_index, search.go_to_next_match(direction)}; + size_t last_index = search->current_index(); + if (direction == history_search_direction_t::Forward) { + completions->reverse(); + } + return {std::move(completions), last_index, search->go_to_next_match(direction)}; } void reader_data_t::fill_history_pager(history_pager_invocation_t why, @@ -1339,19 +1334,19 @@ void reader_data_t::fill_history_pager(history_pager_invocation_t why, size_t index = -1; maybe_t old_pager_index; switch (why) { - case history_pager_invocation_t::anew: - assert(direction == history_search_direction_t::backward); + case history_pager_invocation_t::Anew: + assert(direction == history_search_direction_t::Backward); index = 0; break; - case history_pager_invocation_t::advance: - if (direction == history_search_direction_t::forward) { + case history_pager_invocation_t::Advance: + if (direction == history_search_direction_t::Forward) { index = history_pager_history_index_start; } else { - assert(direction == history_search_direction_t::backward); + assert(direction == history_search_direction_t::Backward); index = history_pager_history_index_end; } break; - case history_pager_invocation_t::refresh: + case history_pager_invocation_t::Refresh: // Redo the previous search previous direction. direction = history_pager_direction; index = history_pager_history_index_start; @@ -1361,19 +1356,19 @@ void reader_data_t::fill_history_pager(history_pager_invocation_t why, const wcstring &search_term = pager.search_field_line.text(); auto shared_this = this->shared_from_this(); std::function func = [=]() { - return history_pager_search(shared_this->history, direction, index, search_term); + return history_pager_search(**shared_this->history, direction, index, search_term); }; std::function completion = [=](const history_pager_result_t &result) { if (search_term != shared_this->pager.search_field_line.text()) return; // Stale request. - if (result.matched_commands.empty() && why == history_pager_invocation_t::advance) { + if (result.matched_commands->empty() && why == history_pager_invocation_t::Advance) { // No more matches, keep the existing ones and flash. shared_this->flash(); return; } history_pager_direction = direction; - if (direction == history_search_direction_t::forward) { + if (direction == history_search_direction_t::Forward) { shared_this->history_pager_history_index_start = result.final_index; shared_this->history_pager_history_index_end = index; } else { @@ -1382,8 +1377,8 @@ void reader_data_t::fill_history_pager(history_pager_invocation_t why, } shared_this->pager.extra_progress_text = result.have_more_results ? _(L"Search again for more results") : L""; - shared_this->pager.set_completions(result.matched_commands); - if (why == history_pager_invocation_t::refresh) { + shared_this->pager.set_completions(*result.matched_commands); + if (why == history_pager_invocation_t::Refresh) { pager.set_selected_completion_index(*old_pager_index); pager_selection_changed(); } else { @@ -1409,7 +1404,7 @@ void reader_data_t::pager_selection_changed() { new_cmd_line = this->cycle_command_line; } else { new_cmd_line = - completion_apply_to_command_line(completion->completion, completion->flags, + completion_apply_to_command_line(*completion->completion(), completion->flags(), this->cycle_command_line, &cursor_pos, false); } @@ -1422,7 +1417,7 @@ void reader_data_t::pager_selection_changed() { /// Expand an abbreviation replacer, which may mean running its function. /// \return the replacement, or none to skip it. This may run fish script! maybe_t expand_replacer(SourceRange range, const wcstring &token, - const abbrs_replacer_t &repl, parser_t &parser) { + const abbrs_replacer_t &repl, const parser_t &parser) { if (!repl.is_function) { // Literal replacement cannot fail. FLOGF(abbrs, L"Expanded literal abbreviation <%ls> -> <%ls>", token.c_str(), @@ -1435,14 +1430,17 @@ maybe_t expand_replacer(SourceRange range, const wcstring & cmd.push_back(L' '); cmd.append(escape_string(token)); - scoped_push not_interactive(&parser.libdata().is_interactive, false); + // todo!("use scoped push") + bool is_interactive = parser.libdata_pods().is_interactive; + parser.libdata_pods_mut().is_interactive = false; + cleanup_t not_interactive{[&] { parser.libdata_pods_mut().is_interactive = is_interactive; }}; - std::vector outputs{}; + auto outputs = std::make_unique(); int ret = exec_subshell(cmd, parser, outputs, false /* not apply_exit_status */); if (ret != STATUS_CMD_OK) { return none(); } - wcstring result = join_strings(outputs, L'\n'); + wcstring result = join_strings(outputs->vals, L'\n'); FLOGF(abbrs, L"Expanded function abbreviation <%ls> -> <%ls>", token.c_str(), result.c_str()); return abbrs_replacement_from(range, result, *repl.set_cursor_marker, repl.has_cursor_marker); } @@ -1512,7 +1510,7 @@ static std::vector extract_tokens(const wcstring &str) { /// \return the replacement. This does NOT inspect the current reader data. maybe_t reader_expand_abbreviation_at_cursor(const wcstring &cmdline, size_t cursor_pos, - parser_t &parser) { + const parser_t &parser) { // Find the token containing the cursor. Usually users edit from the end, so walk backwards. const auto tokens = extract_tokens(cmdline); auto iter = std::find_if(tokens.rbegin(), tokens.rend(), [&](const positioned_token_t &t) { @@ -1560,21 +1558,22 @@ bool reader_data_t::expand_abbreviation_at_cursor(size_t cursor_backtrack) { return result; } -void reader_reset_interrupted() { interrupted = 0; } - -int reader_test_and_clear_interrupted() { - int res = interrupted; - if (res) { - interrupted = 0; - } - return res; +void reader_write_title_ffi(const wcstring &cmd, const void *parser, bool reset_cursor_position) { + reader_write_title(cmd, *static_cast(parser), reset_cursor_position); } -void reader_write_title(const wcstring &cmd, parser_t &parser, bool reset_cursor_position) { +void reader_write_title(const wcstring &cmd, const parser_t &parser, bool reset_cursor_position) { if (!term_supports_setting_title()) return; - scoped_push noninteractive{&parser.libdata().is_interactive, false}; - scoped_push in_title(&parser.libdata().suppress_fish_trace, true); + // todo!("use scoped push") + bool is_interactive = parser.libdata_pods().is_interactive; + parser.libdata_pods_mut().is_interactive = false; + cleanup_t noninteractive{[&] { parser.libdata_pods_mut().is_interactive = is_interactive; }}; + // todo!("use scoped push") + bool suppress_fish_trace = parser.libdata_pods().suppress_fish_trace; + parser.libdata_pods_mut().suppress_fish_trace = false; + cleanup_t in_title{ + [&] { parser.libdata_pods_mut().suppress_fish_trace = suppress_fish_trace; }}; wcstring fish_title_command = DEFAULT_TITLE; if (function_exists(L"fish_title", parser)) { @@ -1585,11 +1584,11 @@ void reader_write_title(const wcstring &cmd, parser_t &parser, bool reset_cursor } } - std::vector lst; + auto lst = std::make_unique(); (void)exec_subshell(fish_title_command, parser, lst, false /* ignore exit status */); - if (!lst.empty()) { + if (!lst->empty()) { wcstring title_line = L"\x1B]0;"; - for (const auto &i : lst) { + for (const auto &i : lst->vals) { title_line += i; } title_line += L"\a"; @@ -1598,7 +1597,7 @@ void reader_write_title(const wcstring &cmd, parser_t &parser, bool reset_cursor } stdoutput().set_color(rgb_color_t::reset(), rgb_color_t::reset()); - if (reset_cursor_position && !lst.empty()) { + if (reset_cursor_position && !lst->empty()) { // Put the cursor back at the beginning of the line (issue #2453). ignore_result(write(STDOUT_FILENO, "\r", 1)); } @@ -1607,11 +1606,11 @@ void reader_write_title(const wcstring &cmd, parser_t &parser, bool reset_cursor void reader_data_t::exec_mode_prompt() { mode_prompt_buff.clear(); if (function_exists(MODE_PROMPT_FUNCTION_NAME, parser())) { - std::vector mode_indicator_list; + auto mode_indicator_list = std::make_unique(); exec_subshell(MODE_PROMPT_FUNCTION_NAME, parser(), mode_indicator_list, false); // We do not support multiple lines in the mode indicator, so just concatenate all of // them. - for (const auto &i : mode_indicator_list) { + for (const auto &i : mode_indicator_list->vals) { mode_prompt_buff += i; } } @@ -1624,7 +1623,11 @@ void reader_data_t::exec_prompt() { right_prompt_buff.clear(); // Suppress fish_trace while in the prompt. - scoped_push in_prompt(&parser().libdata().suppress_fish_trace, true); + // todo!("use scoped_push") + bool suppress_fish_trace = parser().libdata_pods().suppress_fish_trace; + parser().libdata_pods_mut().suppress_fish_trace = true; + cleanup_t in_prompt{ + [&] { parser().libdata_pods_mut().suppress_fish_trace = suppress_fish_trace; }}; // Update the termsize now. // This allows prompts to react to $COLUMNS. @@ -1632,13 +1635,16 @@ void reader_data_t::exec_prompt() { // If we have any prompts, they must be run non-interactively. if (!conf.left_prompt_cmd.empty() || !conf.right_prompt_cmd.empty()) { - scoped_push noninteractive{&parser().libdata().is_interactive, false}; + // todo!("use scoped push") + bool is_interactive = parser().libdata_pods().is_interactive; + parser().libdata_pods_mut().is_interactive = false; + cleanup_t interactive{[&] { parser().libdata_pods_mut().is_interactive = is_interactive; }}; exec_mode_prompt(); if (!conf.left_prompt_cmd.empty()) { // Status is ignored. - std::vector prompt_list; + auto prompt_list = std::make_unique(); // Historic compatibility hack. // If the left prompt function is deleted, then use a default prompt instead of // producing an error. @@ -1646,16 +1652,16 @@ void reader_data_t::exec_prompt() { !function_exists(conf.left_prompt_cmd, parser()); exec_subshell(left_prompt_deleted ? DEFAULT_PROMPT : conf.left_prompt_cmd, parser(), prompt_list, false); - left_prompt_buff = join_strings(prompt_list, L'\n'); + left_prompt_buff = join_strings(prompt_list->vals, L'\n'); } if (!conf.right_prompt_cmd.empty()) { if (function_exists(conf.right_prompt_cmd, parser())) { // Status is ignored. - std::vector prompt_list; + auto prompt_list = std::make_unique(); exec_subshell(conf.right_prompt_cmd, parser(), prompt_list, false); // Right prompt does not support multiple lines, so just concatenate all of them. - for (const auto &i : prompt_list) { + for (const auto &i : prompt_list->vals) { right_prompt_buff += i; } } @@ -1668,8 +1674,8 @@ void reader_data_t::exec_prompt() { reader_write_title(L"", parser(), false); // Some prompt may have requested an exit (#8033). - this->exit_loop_requested |= parser().libdata().exit_current_script; - parser().libdata().exit_current_script = false; + this->exit_loop_requested |= parser().libdata_pods().exit_current_script; + parser().libdata_pods_mut().exit_current_script = false; } void reader_init() { @@ -2002,18 +2008,21 @@ void reader_data_t::completion_insert(const wcstring &val, size_t token_end, // Returns a function that can be invoked (potentially // on a background thread) to determine the autosuggestion static std::function get_autosuggestion_performer( - parser_t &parser, const wcstring &search_string, size_t cursor_pos, - const std::shared_ptr &history) { + const parser_t &parser, const wcstring &search_string, size_t cursor_pos, + const HistorySharedPtr &history) { const uint32_t generation_count = read_generation_count(); - auto vars = parser.vars().snapshot(); - const wcstring working_directory = vars->get_pwd_slash(); + // shared_ptr to work around std::function limitations + auto vars = std::make_shared>(parser.vars().snapshot()); + const wcstring working_directory = *parser.vars().get_pwd_slash(); // TODO: suspicious use of 'history' here // This is safe because histories are immortal, but perhaps // this should use shared_ptr + const HistorySharedPtr *history_ptr = &history; return [=]() -> autosuggestion_t { ASSERT_IS_BACKGROUND_THREAD(); autosuggestion_t nothing = {}; - operation_context_t ctx = get_bg_context(vars, generation_count); + auto ctxptr = get_bg_context(**vars, generation_count); + auto &ctx = *ctxptr; if (ctx.check_cancel()) { return nothing; } @@ -2024,19 +2033,20 @@ static std::function get_autosuggestion_performer( } // Search history for a matching item. - history_search_t searcher(history.get(), search_string, history_search_type_t::prefix, - history_search_flags_t{}); + rust::Box searcher = + rust_history_search_new(*history_ptr, search_string.c_str(), + history_search_type_t::Prefix, history_search_flags_t{}, 0); while (!ctx.check_cancel() && - searcher.go_to_next_match(history_search_direction_t::backward)) { - const history_item_t &item = searcher.current_item(); + searcher->go_to_next_match(history_search_direction_t::Backward)) { + const history_item_t &item = searcher->current_item(); // Skip items with newlines because they make terrible autosuggestions. - if (item.str().find(L'\n') != wcstring::npos) continue; + if (item.str()->find(L'\n') != wcstring::npos) continue; if (autosuggest_validate_from_history(item, working_directory, ctx)) { // The command autosuggestion was handled specially, so we're done. // History items are case-sensitive, see #3978. - return autosuggestion_t{searcher.current_string(), search_string, + return autosuggestion_t{*searcher->current_string(), search_string, false /* icase */}; } } @@ -2055,20 +2065,21 @@ static std::function get_autosuggestion_performer( if (std::wcschr(L"'\"", last_char) && cursor_at_end) return nothing; // Try normal completions. - completion_request_options_t complete_flags = completion_request_options_t::autosuggest(); - std::vector needs_load; - completion_list_t completions = complete(search_string, complete_flags, ctx, &needs_load); + completion_request_options_t complete_flags = completion_request_options_autosuggest(); + auto needs_load = std::make_unique(); + auto completions_box = complete(search_string, complete_flags, ctx, needs_load); + completion_list_t &completions = *completions_box; autosuggestion_t result{}; result.search_string = search_string; - result.needs_load = std::move(needs_load); + result.needs_load = std::move(needs_load->vals); result.icase = true; // normal completions are case-insensitive. if (!completions.empty()) { - completions_sort_and_prioritize(&completions, complete_flags); + completions.sort_and_prioritize(complete_flags); const completion_t &comp = completions.at(0); size_t cursor = cursor_pos; result.text = completion_apply_to_command_line( - comp.completion, comp.flags, search_string, &cursor, true /* append only */); + *comp.completion(), comp.flags(), search_string, &cursor, true /* append only */); } return result; }; @@ -2145,7 +2156,7 @@ void reader_data_t::update_autosuggestion() { FLOG(reader_render, L"Autosuggesting"); autosuggestion.clear(); std::function performer = - get_autosuggestion_performer(parser(), el.text(), el.position(), history); + get_autosuggestion_performer(parser(), el.text(), el.position(), **history); auto shared_this = this->shared_from_this(); std::function completion = [shared_this](autosuggestion_t result) { shared_this->autosuggest_completed(std::move(result)); @@ -2269,7 +2280,8 @@ static bool reader_can_replace(const wcstring &in, complete_flags_t flags) { /// Determine the best (lowest) match rank for a set of completions. static uint32_t get_best_rank(const completion_list_t &comp) { uint32_t best_rank = UINT32_MAX; - for (const auto &c : comp) { + for (size_t i = 0; i < comp.size(); i++) { + auto &c = comp.at(i); best_rank = std::min(best_rank, c.rank()); } return best_rank; @@ -2310,8 +2322,8 @@ bool reader_data_t::handle_completions(const completion_list_t &comp, size_t tok // If this is a replacement completion, check that we know how to replace it, e.g. that // the token doesn't contain evil operators like {}. - if (!(c.flags & COMPLETE_REPLACES_TOKEN) || reader_can_replace(tok, c.flags)) { - completion_insert(c.completion, token_end, c.flags); + if (!(c.flags() & COMPLETE_REPLACES_TOKEN) || reader_can_replace(tok, c.flags())) { + completion_insert(*c.completion(), token_end, c.flags()); } done = true; success = true; @@ -2326,8 +2338,9 @@ bool reader_data_t::handle_completions(const completion_list_t &comp, size_t tok // Determine whether we are going to replace the token or not. If any commands of the best // rank do not require replacement, then ignore all those that want to use replacement. bool will_replace_token = true; - for (const completion_t &el : comp) { - if (el.rank() <= best_rank && !(el.flags & COMPLETE_REPLACES_TOKEN)) { + for (size_t i = 0; i < comp.size(); i++) { + const completion_t &el = comp.at(i); + if (el.rank() <= best_rank && !(el.flags() & COMPLETE_REPLACES_TOKEN)) { will_replace_token = false; break; } @@ -2335,22 +2348,24 @@ bool reader_data_t::handle_completions(const completion_list_t &comp, size_t tok // Decide which completions survived. There may be a lot of them; it would be nice if we could // figure out how to avoid copying them here. - completion_list_t surviving_completions; + auto surviving_completions_box = new_completion_list(); + completion_list_t &surviving_completions = *surviving_completions_box; bool all_matches_exact_or_prefix = true; - for (const completion_t &el : comp) { + for (size_t i = 0; i < comp.size(); i++) { + const completion_t &el = comp.at(i); // Ignore completions with a less suitable match rank than the best. if (el.rank() > best_rank) continue; // Only use completions that match replace_token. - bool completion_replace_token = static_cast(el.flags & COMPLETE_REPLACES_TOKEN); + bool completion_replace_token = static_cast(el.flags() & COMPLETE_REPLACES_TOKEN); if (completion_replace_token != will_replace_token) continue; // Don't use completions that want to replace, if we cannot replace them. - if (completion_replace_token && !reader_can_replace(tok, el.flags)) continue; + if (completion_replace_token && !reader_can_replace(tok, el.flags())) continue; // This completion survived. surviving_completions.push_back(el); - all_matches_exact_or_prefix = all_matches_exact_or_prefix && el.match.is_exact_or_prefix(); + all_matches_exact_or_prefix = all_matches_exact_or_prefix && el.match_is_exact_or_prefix(); } if (surviving_completions.size() == 1) { @@ -2364,8 +2379,8 @@ bool reader_data_t::handle_completions(const completion_list_t &comp, size_t tok // If this is a replacement completion, check that we know how to replace it, e.g. that // the token doesn't contain evil operators like {}. - if (!(c.flags & COMPLETE_REPLACES_TOKEN) || reader_can_replace(tok, c.flags)) { - completion_insert(c.completion, token_end, c.flags); + if (!(c.flags() & COMPLETE_REPLACES_TOKEN) || reader_can_replace(tok, c.flags())) { + completion_insert(*c.completion(), token_end, c.flags()); } return true; } @@ -2377,18 +2392,19 @@ bool reader_data_t::handle_completions(const completion_list_t &comp, size_t tok complete_flags_t flags = 0; bool prefix_is_partial_completion = false; bool first = true; - for (const completion_t &el : surviving_completions) { + for (size_t i = 0; i < surviving_completions.size(); i++) { + const completion_t &el = surviving_completions.at(i); if (first) { // First entry, use the whole string. - common_prefix = el.completion; - flags = el.flags; + common_prefix = *el.completion(); + flags = el.flags(); first = false; } else { // Determine the shared prefix length. - size_t idx, max = std::min(common_prefix.size(), el.completion.size()); + size_t idx, max = std::min(common_prefix.size(), el.completion()->size()); for (idx = 0; idx < max; idx++) { - if (common_prefix.at(idx) != el.completion.at(idx)) break; + if (common_prefix.at(idx) != el.completion()->at(idx)) break; } // idx is now the length of the new common prefix. @@ -2418,9 +2434,10 @@ bool reader_data_t::handle_completions(const completion_list_t &comp, size_t tok } if (use_prefix) { - for (completion_t &c : surviving_completions) { - c.flags &= ~COMPLETE_REPLACES_TOKEN; - c.completion.erase(0, common_prefix.size()); + for (size_t i = 0; i < surviving_completions.size(); i++) { + completion_t &c = surviving_completions.at_mut(i); + c.set_flags(c.flags() & ~COMPLETE_REPLACES_TOKEN); + c.completion_erase(0, common_prefix.size()); } } @@ -2578,7 +2595,7 @@ static void acquire_tty_or_exit(pid_t shell_pgid) { } /// Initialize data for interactive use. -static void reader_interactive_init(parser_t &parser) { +static void reader_interactive_init(const parser_t &parser) { ASSERT_IS_MAIN_THREAD(); pid_t shell_pgid = getpgrp(); @@ -2632,7 +2649,7 @@ static void reader_interactive_init(parser_t &parser) { termsize_invalidate_tty(); // Provide value for `status current-command` - parser.libdata().status_vars.command = L"fish"; + parser.libdata_mut().set_status_vars_command(L"fish"); // Also provide a value for the deprecated fish 2.0 $_ variable parser.vars().set_one(L"_", ENV_GLOBAL, L"fish"); } @@ -2764,13 +2781,13 @@ void reader_data_t::set_buffer_maintaining_pager(const wcstring &b, size_t pos, /// Run the specified command with the correct terminal modes, and while taking care to perform job /// notification, set the title, etc. -static eval_res_t reader_run_command(parser_t &parser, const wcstring &cmd) { +static rust::Box reader_run_command(const parser_t &parser, const wcstring &cmd) { wcstring ft = *tok_command(cmd); // Provide values for `status current-command` and `status current-commandline` if (!ft.empty()) { - parser.libdata().status_vars.command = ft; - parser.libdata().status_vars.commandline = cmd; + parser.libdata_mut().set_status_vars_command(ft); + parser.libdata_mut().set_status_vars_commandline(cmd); // Also provide a value for the deprecated fish 2.0 $_ variable parser.vars().set_one(L"_", ENV_GLOBAL, ft); } @@ -2781,7 +2798,7 @@ static eval_res_t reader_run_command(parser_t &parser, const wcstring &cmd) { term_donate(); timepoint_t time_before = timef(); - auto eval_res = parser.eval(cmd, io_chain_t{}); + auto eval_res = parser.eval(cmd, *new_io_chain()); job_reap(parser, true); // Update the execution duration iff a command is requested for execution @@ -2796,11 +2813,11 @@ static eval_res_t reader_run_command(parser_t &parser, const wcstring &cmd) { term_steal(); // Provide value for `status current-command` - parser.libdata().status_vars.command = program_name; + parser.libdata_mut().set_status_vars_command(program_name); // Also provide a value for the deprecated fish 2.0 $_ variable parser.vars().set_one(L"_", ENV_GLOBAL, program_name); // Provide value for `status current-commandline` - parser.libdata().status_vars.commandline = L""; + parser.libdata_mut().set_status_vars_commandline(L""); if (have_proc_stat()) { proc_update_jiffies(parser); @@ -2815,8 +2832,7 @@ static parser_test_error_bits_t reader_shell_test(const parser_t &parser, const parse_util_detect_errors(bstr, &*errors, true /* do accept incomplete */); if (res & PARSER_TEST_ERROR) { - wcstring error_desc; - parser.get_backtrace(bstr, *errors, error_desc); + wcstring error_desc = *parser.get_backtrace(bstr, *errors); // Ensure we end with a newline. Also add an initial newline, because it's likely the user // just hit enter and so there's junk on the current line. @@ -2843,16 +2859,17 @@ void reader_data_t::highlight_complete(highlight_result_t result) { // Given text and whether IO is allowed, return a function that performs highlighting. The function // may be invoked on a background thread. -static std::function get_highlight_performer(parser_t &parser, +static std::function get_highlight_performer(const parser_t &parser, const editable_line_t &el, bool io_ok) { - auto vars = parser.vars().snapshot(); + // shard_ptr to work around std::function requiring copyable types + auto vars = std::make_shared>(parser.vars().snapshot()); uint32_t generation_count = read_generation_count(); return [=]() -> highlight_result_t { if (el.text().empty()) return {}; - operation_context_t ctx = get_bg_context(vars, generation_count); + auto ctx = get_bg_context(**vars, generation_count); std::vector colors(el.text().size(), highlight_spec_t{}); - highlight_shell(el.text(), colors, ctx, io_ok, el.position()); + highlight_shell(el.text(), colors, *ctx, io_ok, std::make_shared(el.position())); return highlight_result_t{std::move(colors), el.text()}; }; } @@ -2937,9 +2954,9 @@ void reader_change_history(const wcstring &name) { // We don't need to _change_ if we're not initialized yet. reader_data_t *data = current_data_or_null(); if (data && data->history) { - data->history->save(); - data->history = history_t::with_name(name); - commandline_state_snapshot()->history = data->history; + (*data->history)->save(); + data->history = history_with_name(name.c_str()); + commandline_state_snapshot()->history = (*data->history)->clone(); } } @@ -2989,12 +3006,12 @@ void reader_set_autosuggestion_enabled_ffi(bool enable) { /// Add a new reader to the reader stack. /// \return a shared pointer to it. -static std::shared_ptr reader_push_ret(parser_t &parser, +static std::shared_ptr reader_push_ret(const parser_t &parser, const wcstring &history_name, reader_config_t &&conf) { - std::shared_ptr hist = history_t::with_name(history_name); + rust::Box hist = history_with_name(history_name.c_str()); hist->resolve_pending(); // see #6892 - auto data = std::make_shared(parser.shared(), hist, std::move(conf)); + auto data = std::make_shared(parser.shared(), *hist, std::move(conf)); reader_data_stack.push_back(data); data->command_line_changed(&data->command_line); if (reader_data_stack.size() == 1) { @@ -3005,10 +3022,28 @@ static std::shared_ptr reader_push_ret(parser_t &parser, } /// Public variant which discards the return value. -void reader_push(parser_t &parser, const wcstring &history_name, reader_config_t &&conf) { +void reader_push(const parser_t &parser, const wcstring &history_name, reader_config_t &&conf) { (void)reader_push_ret(parser, history_name, std::move(conf)); } +void reader_push_ffi(const void *_parser, const wcstring &history_name, const void *_conf_ffi) { + const auto &parser = *static_cast(_parser); + const auto &conf_ffi = *static_cast(_conf_ffi); + reader_config_t conf; + conf.left_prompt_cmd = std::move(*conf_ffi.left_prompt_cmd()); + conf.right_prompt_cmd = std::move(*conf_ffi.right_prompt_cmd()); + conf.event = std::move(*conf_ffi.event()); + conf.complete_ok = conf_ffi.complete_ok(); + conf.highlight_ok = conf_ffi.highlight_ok(); + conf.syntax_check_ok = conf_ffi.syntax_check_ok(); + conf.autosuggest_ok = conf_ffi.autosuggest_ok(); + conf.expand_abbrev_ok = conf_ffi.expand_abbrev_ok(); + conf.exit_on_interrupt = conf_ffi.exit_on_interrupt(); + conf.in_silent_mode = conf_ffi.in_silent_mode(); + conf.in = conf_ffi.inputfd(); + reader_push(parser, history_name, std::move(conf)); +} + void reader_pop() { assert(!reader_data_stack.empty() && "empty stack in reader_data_stack"); reader_data_stack.pop_back(); @@ -3024,25 +3059,20 @@ void reader_pop() { void reader_data_t::import_history_if_necessary() { // Import history from older location (config path) if our current history is empty. - if (history && history->is_empty()) { - history->populate_from_config_path(); + if (history && (*history)->is_empty()) { + (*history)->populate_from_config_path(); } // Import history from bash, etc. if our current history is still empty and is the default // history. - if (history && history->is_empty() && history->is_default()) { + if (history && (*history)->is_empty() && (*history)->is_default()) { // Try opening a bash file. We make an effort to respect $HISTFILE; this isn't very complete // (AFAIK it doesn't have to be exported), and to really get this right we ought to ask bash // itself. But this is better than nothing. const auto var = vars().get(L"HISTFILE"); wcstring path = (var ? var->as_string() : L"~/.bash_history"); expand_tilde(path, vars()); - int fd = wopen_cloexec(path, O_RDONLY); - if (fd >= 0) { - FILE *f = fdopen(fd, "r"); - history->populate_from_bash(f); - fclose(f); - } + (*history)->populate_from_bash(path.c_str()); } } @@ -3056,9 +3086,9 @@ static bool try_warn_on_background_jobs(reader_data_t *data) { if (reader_data_stack.size() > 1) return false; // Do we have background jobs? auto bg_jobs = jobs_requiring_warning_on_exit(data->parser()); - if (bg_jobs.empty()) return false; + if (bg_jobs->empty()) return false; // Print the warning! - print_exit_warning_for_jobs(bg_jobs); + print_exit_warning_for_jobs(*bg_jobs); data->did_warn_for_bg_jobs = true; return true; } @@ -3067,7 +3097,7 @@ static bool try_warn_on_background_jobs(reader_data_t *data) { /// \return true if we should exit. bool check_exit_loop_maybe_warning(reader_data_t *data) { // sighup always forces exit. - if (s_sighup_received) return true; + if (reader_received_sighup()) return true; // Check if an exit is requested. if (data && data->exit_loop_requested) { @@ -3093,7 +3123,9 @@ void reader_data_t::update_commandline_state() const { auto snapshot = commandline_state_snapshot(); snapshot->text = this->command_line.text(); snapshot->cursor_pos = this->command_line.position(); - snapshot->history = this->history; + if (this->history) { + snapshot->history = (*this->history)->clone(); + } snapshot->selection = this->get_selection(); snapshot->pager_mode = !this->pager.empty(); snapshot->pager_fully_disclosed = this->current_page_rendering.remaining_to_disclose == 0; @@ -3103,7 +3135,7 @@ void reader_data_t::update_commandline_state() const { void reader_data_t::apply_commandline_state_changes() { // Only the text and cursor position may be changed. - commandline_state_t state = *commandline_state_snapshot(); + commandline_state_t state = commandline_get_state(); if (state.text != this->command_line.text() || state.cursor_pos != this->command_line.position()) { // The commandline builtin changed our contents. @@ -3113,56 +3145,6 @@ void reader_data_t::apply_commandline_state_changes() { } } -expand_result_t::result_t reader_data_t::try_expand_wildcard(wcstring wc, size_t position, - wcstring *result) { - // Hacky from #8593: only expand if there are wildcards in the "current path component." - // Find the "current path component" by looking for an unescaped slash before and after - // our position. - // This is quite naive; for example it mishandles brackets. - auto is_path_sep = [&](size_t where) { - return wc.at(where) == L'/' && count_preceding_backslashes(wc, where) % 2 == 0; - }; - size_t comp_start = position; - while (comp_start > 0 && !is_path_sep(comp_start - 1)) { - comp_start--; - } - size_t comp_end = position; - while (comp_end < wc.size() && !is_path_sep(comp_end)) { - comp_end++; - } - if (!wildcard_has(wc.c_str() + comp_start, comp_end - comp_start)) { - return expand_result_t::wildcard_no_match; - } - - result->clear(); - - // Have a low limit on the number of matches, otherwise we will overwhelm the command line. - operation_context_t ctx{nullptr, vars(), parser().cancel_checker(), - TAB_COMPLETE_WILDCARD_MAX_EXPANSION}; - // We do wildcards only. - expand_flags_t flags{expand_flag::skip_cmdsubst, expand_flag::skip_variables, - expand_flag::preserve_home_tildes}; - completion_list_t expanded; - expand_result_t ret = expand_string(std::move(wc), &expanded, flags, ctx); - if (ret != expand_result_t::ok) return ret.result; - - // Insert all matches (escaped) and a trailing space. - wcstring joined; - for (const auto &match : expanded) { - if (match.flags & COMPLETE_DONT_ESCAPE) { - joined.append(match.completion); - } else { - complete_flags_t tildeflag = - (match.flags & COMPLETE_DONT_ESCAPE_TILDES) ? ESCAPE_NO_TILDE : 0; - joined.append(escape_string(match.completion, ESCAPE_NO_QUOTED | tildeflag)); - } - joined.push_back(L' '); - } - - *result = std::move(joined); - return expand_result_t::ok; -} - void reader_data_t::compute_and_apply_completions(readline_cmd_t c, readline_loop_state_t &rls) { assert((c == readline_cmd_t::complete || c == readline_cmd_t::complete_and_search) && "Invalid command"); @@ -3197,25 +3179,25 @@ void reader_data_t::compute_and_apply_completions(readline_cmd_t c, readline_loo // Check if we have a wildcard within this string; if so we first attempt to expand the // wildcard; if that succeeds we don't then apply user completions (#8593). - wcstring wc_expanded; - switch ( - try_expand_wildcard(wcstring(token_begin, token_end), position_in_token, &wc_expanded)) { - case expand_result_t::error: + std::unique_ptr wc_expanded; + switch (static_cast(try_expand_wildcard( + this->parser(), wcstring(token_begin, token_end), position_in_token, wc_expanded))) { + case ExpandResultCode::error: // This may come about if we exceeded the max number of matches. // Return "success" to suppress normal completions. flash(); return; - case expand_result_t::wildcard_no_match: + case ExpandResultCode::wildcard_no_match: break; - case expand_result_t::cancel: + case ExpandResultCode::cancel: // e.g. the user hit control-C. Suppress normal completions. return; - case expand_result_t::ok: - rls.comp.clear(); + case ExpandResultCode::ok: + rls.comp->clear(); rls.complete_did_insert = false; size_t tok_off = static_cast(token_begin - buff); size_t tok_len = static_cast(token_end - token_begin); - push_edit(el, edit_t{tok_off, tok_len, std::move(wc_expanded)}); + push_edit(el, edit_t{tok_off, tok_len, std::move(*wc_expanded)}); return; } @@ -3226,7 +3208,9 @@ void reader_data_t::compute_and_apply_completions(readline_cmd_t c, readline_loo // Ensure that `commandline` inside the completions gets the current state. update_commandline_state(); - rls.comp = complete(buffcpy, completion_request_options_t::normal(), parser_ref->context()); + std::unique_ptr needs_load = nullptr; + rls.comp = complete(buffcpy, completion_request_options_normal(), + *parser_context(parser_ref->deref()), needs_load); // User-supplied completions may have changed the commandline - prevent buffer // overflow. @@ -3234,13 +3218,13 @@ void reader_data_t::compute_and_apply_completions(readline_cmd_t c, readline_loo if (token_end > buff + el->text().size()) token_end = buff + el->text().size(); // Munge our completions. - completions_sort_and_prioritize(&rls.comp); + rls.comp->sort_and_prioritize(CompletionRequestOptions()); // Record our cycle_command_line. cycle_command_line = el->text(); cycle_cursor_pos = token_end - buff; - rls.complete_did_insert = handle_completions(rls.comp, token_begin - buff, token_end - buff); + rls.complete_did_insert = handle_completions(*rls.comp, token_begin - buff, token_end - buff); // Show the search field if requested and if we printed a list of completions. if (c == readline_cmd_t::complete_and_search && !rls.complete_did_insert && !pager.empty()) { @@ -3262,14 +3246,14 @@ static relaxed_atomic_t status_count{0}; uint64_t reader_status_count() { return status_count; } /// Read interactively. Read input from stdin while providing editing facilities. -static int read_i(parser_t &parser) { +static int read_i(const parser_t &parser) { ASSERT_IS_MAIN_THREAD(); parser.assert_can_execute(); reader_config_t conf; conf.complete_ok = true; conf.highlight_ok = true; conf.syntax_check_ok = true; - conf.autosuggest_ok = check_autosuggestion_enabled(parser.vars()); + conf.autosuggest_ok = check_autosuggestion_enabled(env_stack_t{parser.vars_boxed()}); conf.expand_abbrev_ok = true; conf.event = L"fish_prompt"; @@ -3282,7 +3266,7 @@ static int read_i(parser_t &parser) { } std::shared_ptr data = - reader_push_ret(parser, history_session_id(parser.vars()), std::move(conf)); + reader_push_ret(parser, *history_session_id(parser.vars()), std::move(conf)); data->import_history_if_necessary(); while (!check_exit_loop_maybe_warning(data.get())) { @@ -3300,18 +3284,18 @@ static int read_i(parser_t &parser) { event_fire_generic(parser, L"fish_preexec", {command}); auto eval_res = reader_run_command(parser, command); signal_clear_cancel(); - if (!eval_res.no_status) { + if (!eval_res->no_status()) { ++status_count; } // If the command requested an exit, then process it now and clear it. - data->exit_loop_requested |= parser.libdata().exit_current_script; - parser.libdata().exit_current_script = false; + data->exit_loop_requested |= parser.libdata_pods().exit_current_script; + parser.libdata_pods_mut().exit_current_script = false; event_fire_generic(parser, L"fish_postexec", {command}); // Allow any pending history items to be returned in the history array. if (data->history) { - data->history->resolve_pending(); + (*data->history)->resolve_pending(); } bool already_warned = data->did_warn_for_bg_jobs; @@ -3332,7 +3316,7 @@ static int read_i(parser_t &parser) { reader_pop(); // If we got SIGHUP, ensure the tty is redirected. - if (s_sighup_received) { + if (reader_received_sighup()) { // If we are the top-level reader, then we translate SIGHUP into exit_forced. redirect_tty_after_sighup(); } @@ -3343,7 +3327,7 @@ static int read_i(parser_t &parser) { s_exit_state = exit_state_t::running_handlers; event_fire_generic(parser, L"fish_exit"); s_exit_state = exit_state_t::finished_handlers; - hup_jobs(parser.jobs()); + hup_jobs(parser); } return 0; @@ -3394,13 +3378,13 @@ static bool event_is_normal_char(const char_event_t &evt) { /// Run a sequence of commands from an input binding. void reader_data_t::run_input_command_scripts(const std::vector &cmds) { - auto last_statuses = parser().get_last_statuses(); + auto last_statuses = parser().vars().get_last_statuses(); for (const wcstring &cmd : cmds) { update_commandline_state(); - parser().eval(cmd, io_chain_t{}); + parser().eval(cmd, *new_io_chain()); apply_commandline_state_changes(); } - parser().set_last_statuses(std::move(last_statuses)); + parser().set_last_statuses(*last_statuses); // Restore tty to shell modes. // Some input commands will take over the tty - see #2114 for an example where vim is invoked @@ -3517,8 +3501,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat // Repaint also changes the actual cursor position if (this->is_repaint_needed()) this->layout_and_repaint(L"cancel"); - auto fish_color_cancel = vars.get(L"fish_color_cancel"); - if (fish_color_cancel) { + if (auto fish_color_cancel = vars.get(L"fish_color_cancel")) { outp.set_color(parse_color(*fish_color_cancel, false), parse_color(*fish_color_cancel, true)); } @@ -3564,14 +3547,14 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat // This can happen e.g. if a variable triggers a repaint, // and the variable is set inside the prompt (#7324). // builtin commandline will refuse to enqueue these. - parser().libdata().is_repaint = true; + parser().libdata_pods_mut().is_repaint = true; exec_mode_prompt(); if (!mode_prompt_buff.empty()) { if (this->is_repaint_needed()) { screen.reset_line(true /* redraw prompt */); this->layout_and_repaint(L"mode"); } - parser().libdata().is_repaint = false; + parser().libdata_pods_mut().is_repaint = false; break; } // Else we repaint as normal. @@ -3579,19 +3562,19 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat } case rl::force_repaint: case rl::repaint: { - parser().libdata().is_repaint = true; + parser().libdata_pods_mut().is_repaint = true; exec_prompt(); screen.reset_line(true /* redraw prompt */); this->layout_and_repaint(L"readline"); force_exec_prompt_and_repaint = false; - parser().libdata().is_repaint = false; + parser().libdata_pods_mut().is_repaint = false; break; } case rl::complete: case rl::complete_and_search: { if (!conf.complete_ok) break; if (is_navigating_pager_contents() || - (!rls.comp.empty() && !rls.complete_did_insert && rls.last_cmd == rl::complete)) { + (!rls.comp->empty() && !rls.complete_did_insert && rls.last_cmd == rl::complete)) { // The user typed complete more than once in a row. If we are not yet fully // disclosed, then become so; otherwise cycle through our available completions. if (current_page_rendering.remaining_to_disclose > 0) { @@ -3608,8 +3591,8 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat } case rl::pager_toggle_search: { if (history_pager_active) { - fill_history_pager(history_pager_invocation_t::advance, - history_search_direction_t::forward); + fill_history_pager(history_pager_invocation_t::Advance, + history_search_direction_t::Forward); break; } if (!pager.empty()) { @@ -3728,7 +3711,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat } case rl::exit: { // This is by definition a successful exit, override the status - parser().set_last_statuses(statuses_t::just(STATUS_CMD_OK)); + parser().set_last_statuses(*statuses_just(STATUS_CMD_OK)); exit_loop_requested = true; check_exit_loop_maybe_warning(this); break; @@ -3742,7 +3725,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat delete_char(false /* backward */); } else if (c == rl::delete_or_exit && el->empty()) { // This is by definition a successful exit, override the status - parser().set_last_statuses(statuses_t::just(STATUS_CMD_OK)); + parser().set_last_statuses(*statuses_just(STATUS_CMD_OK)); exit_loop_requested = true; check_exit_loop_maybe_warning(this); } @@ -3781,15 +3764,15 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat parse_util_token_extent(buff, el->position(), &begin, &end, nullptr, nullptr); if (begin) { wcstring token(begin, end); - history_search.reset_to_mode(token, history, reader_history_search_t::token, - begin - buff); + history_search.reset_to_mode(token, **history, + reader_history_search_t::token, begin - buff); } else { // No current token, refuse to do a token search. history_search.reset(); } } else { // Searching by line. - history_search.reset_to_mode(el->text(), history, mode, 0); + history_search.reset_to_mode(el->text(), **history, mode, 0); // Skip the autosuggestion in the history unless it was truncated. const wcstring &suggest = autosuggestion.text; @@ -3803,8 +3786,8 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat history_search_direction_t dir = (c == rl::history_search_backward || c == rl::history_token_search_backward || c == rl::history_prefix_search_backward) - ? history_search_direction_t::backward - : history_search_direction_t::forward; + ? history_search_direction_t::Backward + : history_search_direction_t::Forward; bool found = history_search.move_in_direction(dir); // Signal that we've found nothing @@ -3815,7 +3798,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat break; } if (found || - (dir == history_search_direction_t::forward && history_search.is_at_end())) { + (dir == history_search_direction_t::Forward && history_search.is_at_end())) { update_command_line_from_history_search(); } } @@ -3823,8 +3806,8 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat } case rl::history_pager: { if (history_pager_active) { - fill_history_pager(history_pager_invocation_t::advance, - history_search_direction_t::backward); + fill_history_pager(history_pager_invocation_t::Advance, + history_search_direction_t::Backward); break; } @@ -3855,10 +3838,10 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat } inputter.function_set_status(true); if (auto completion = pager.selected_completion(current_page_rendering)) { - history->remove(completion->completion); - history->save(); - fill_history_pager(history_pager_invocation_t::refresh, - history_search_direction_t::backward); + (*history)->remove(*completion->completion()); + (*history)->save(); + fill_history_pager(history_pager_invocation_t::Refresh, + history_search_direction_t::Backward); } break; } @@ -3922,9 +3905,9 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat case rl::backward_bigword: case rl::prevd_or_backward_word: { if (c == rl::prevd_or_backward_word && command_line.empty()) { - auto last_statuses = parser().get_last_statuses(); - (void)parser().eval(L"prevd", io_chain_t{}); - parser().set_last_statuses(std::move(last_statuses)); + auto last_statuses = parser().vars().get_last_statuses(); + (void)parser().eval(L"prevd", *new_io_chain()); + parser().set_last_statuses(*std::move(last_statuses)); force_exec_prompt_and_repaint = true; inputter.queue_char(readline_cmd_t::repaint); break; @@ -3941,9 +3924,9 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat case rl::forward_bigword: case rl::nextd_or_forward_word: { if (c == rl::nextd_or_forward_word && command_line.empty()) { - auto last_statuses = parser().get_last_statuses(); - (void)parser().eval(L"nextd", io_chain_t{}); - parser().set_last_statuses(std::move(last_statuses)); + auto last_statuses = parser().vars().get_last_statuses(); + (void)parser().eval(L"nextd", *new_io_chain()); + parser().set_last_statuses(*std::move(last_statuses)); force_exec_prompt_and_repaint = true; inputter.queue_char(readline_cmd_t::repaint); break; @@ -4342,7 +4325,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat break; } case rl::clear_screen_and_repaint: { - parser().libdata().is_repaint = true; + parser().libdata_pods_mut().is_repaint = true; auto clear = screen_clear(); if (!clear.empty()) { // Clear the screen if we can. @@ -4359,7 +4342,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat screen.reset_line(true /* redraw prompt */); this->layout_and_repaint(L"readline"); force_exec_prompt_and_repaint = false; - parser().libdata().is_repaint = false; + parser().libdata_pods_mut().is_repaint = false; break; } // Some commands should have been handled internally by inputter_t::readch(). @@ -4372,7 +4355,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat } } -void reader_data_t::add_to_history() const { +void reader_data_t::add_to_history() { if (!history || conf.in_silent_mode) { return; } @@ -4385,21 +4368,21 @@ void reader_data_t::add_to_history() const { } // Remove ephemeral items - even if the text is empty. - history->remove_ephemeral_items(); + (*history)->remove_ephemeral_items(); if (!text.empty()) { // Mark this item as ephemeral if there is a leading space (#615). history_persistence_mode_t mode; if (text.front() == L' ') { // Leading spaces are ephemeral (#615). - mode = history_persistence_mode_t::ephemeral; - } else if (in_private_mode(this->vars())) { + mode = history_persistence_mode_t::Ephemeral; + } else if (in_private_mode(this->vars().get_impl_ffi())) { // Private mode means in-memory only. - mode = history_persistence_mode_t::memory; + mode = history_persistence_mode_t::Memory; } else { - mode = history_persistence_mode_t::disk; + mode = history_persistence_mode_t::Disk; } - history_t::add_pending_with_file_detection(history, text, this->vars().snapshot(), mode); + (*history)->add_pending_with_file_detection(text, this->vars().get_impl_ffi(), mode); } } @@ -4500,7 +4483,7 @@ maybe_t reader_data_t::readline(int nchars_or_0) { // Suppress fish_trace during executing key bindings. // This is simply to reduce noise. - scoped_push in_title(&parser().libdata().suppress_fish_trace, true); + scoped_push in_title(&parser().libdata_pods_mut().suppress_fish_trace, true); // If nchars_or_0 is positive, then that's the maximum number of chars. Otherwise keep it at // SIZE_MAX. @@ -4598,8 +4581,8 @@ maybe_t reader_data_t::readline(int nchars_or_0) { } // If we ran `exit` anywhere, exit. - exit_loop_requested |= parser().libdata().exit_current_script; - parser().libdata().exit_current_script = false; + exit_loop_requested |= parser().libdata_pods().exit_current_script; + parser().libdata_pods_mut().exit_current_script = false; if (exit_loop_requested) continue; if (!event_needing_handling || event_needing_handling->is_check_exit()) { @@ -4758,6 +4741,14 @@ maybe_t reader_readline(int nchars) { return data->readline(nchars); } +bool reader_readline_ffi(wcstring &line, int nchars) { + if (auto result = reader_readline(nchars)) { + line = std::move(*result); + return true; + } + return {}; +} + int reader_reading_interrupted() { int res = reader_test_and_clear_interrupted(); reader_data_t *data = current_data_or_null(); @@ -4794,7 +4785,7 @@ void reader_queue_ch(const char_event_t &ch) { /// Read non-interactively. Read input from stdin without displaying the prompt, using syntax /// highlighting. This is used for reading scripts and init files. /// The file is not closed. -static int read_ni(parser_t &parser, int fd, const io_chain_t &io) { +static int read_ni(const parser_t &parser, int fd, const io_chain_t &io) { struct stat buf {}; if (fstat(fd, &buf) == -1) { int err = errno; @@ -4832,6 +4823,7 @@ static int read_ni(parser_t &parser, int fd, const io_chain_t &io) { } else { // Fatal error. FLOGF(error, _(L"Unable to read input file: %s"), strerror(err)); + assert(false); // Reset buffer on error. We won't evaluate incomplete files. fd_contents.clear(); return 1; @@ -4864,18 +4856,19 @@ static int read_ni(parser_t &parser, int fd, const io_chain_t &io) { parser.eval_parsed_source(*ps, io); return 0; } else { - wcstring sb; - parser.get_backtrace(str, *errors, sb); + wcstring sb = *parser.get_backtrace(str, *errors); std::fwprintf(stderr, L"%ls", sb.c_str()); return 1; } } -int reader_read_ffi(parser_t &parser, int fd) { - return reader_read(parser, fd, {}); +int reader_read_ffi(const void *_parser, int fd, const void *_io) { + const auto &parser = *static_cast(_parser); + const auto &io = *static_cast(_io); + return reader_read(parser, fd, io); } -int reader_read(parser_t &parser, int fd, const io_chain_t &io) { +int reader_read(const parser_t &parser, int fd, const io_chain_t &io) { int res; // If reader_read is called recursively through the '.' builtin, we need to preserve @@ -4896,13 +4889,13 @@ int reader_read(parser_t &parser, int fd, const io_chain_t &io) { } } - scoped_push interactive_push{&parser.libdata().is_interactive, interactive}; + scoped_push interactive_push{&parser.libdata_pods_mut().is_interactive, interactive}; signal_set_handlers_once(interactive); res = interactive ? read_i(parser) : read_ni(parser, fd, io); // If the exit command was called in a script, only exit the script, not the program. - parser.libdata().exit_current_script = false; + parser.libdata_pods_mut().exit_current_script = false; return res; } diff --git a/src/reader.h b/src/reader.h index 78765b1d9..29b73312b 100644 --- a/src/reader.h +++ b/src/reader.h @@ -14,15 +14,16 @@ #include "common.h" #include "complete.h" +#include "env.h" #include "highlight.h" +#include "io.h" #include "maybe.h" #include "parse_constants.h" +#include "parser.h" -class env_stack_t; -class environment_t; -class history_t; -class io_chain_t; -class Parser; using parser_t = Parser; +#if INCLUDE_RUST_HEADERS +#include "reader.rs.h" +#endif /// An edit action that can be undone. struct edit_t { @@ -87,10 +88,7 @@ class editable_line_t { const wcstring &text() const { return text_; } const std::vector &colors() const { return colors_; } - void set_colors(std::vector colors) { - assert(colors.size() == size()); - colors_ = std::move(colors); - } + void set_colors(std::vector colors); size_t position() const { return position_; } void set_position(size_t position) { position_ = position; } @@ -139,15 +137,10 @@ class editable_line_t { uint32_t edit_group_id_ = -1; }; -int reader_read_ffi(parser_t &parser, int fd); +int reader_read_ffi(const void *parser, int fd, const void *io_chain); /// Read commands from \c fd until encountering EOF. /// The fd is not closed. -int reader_read(parser_t &parser, int fd, const io_chain_t &io); - -/// Mark that we encountered SIGHUP and must (soon) exit. This is invoked from a signal handler. -extern "C" { -void reader_sighup(); -} +int reader_read(const parser_t &parser, int fd, const io_chain_t &io); /// Initialize the reader. void reader_init(); @@ -186,7 +179,10 @@ void reader_set_autosuggestion_enabled_ffi(bool enabled); /// \param cmd Command line string passed to \c fish_title if is defined. /// \param parser The parser to use for autoloading fish_title. /// \param reset_cursor_position If set, issue a \r so the line driver knows where we are -void reader_write_title(const wcstring &cmd, parser_t &parser, bool reset_cursor_position = true); +void reader_write_title(const wcstring &cmd, const parser_t &parser, + bool reset_cursor_position = true); + +void reader_write_title_ffi(const wcstring &cmd, const void *parser, bool reset_cursor_position); /// Tell the reader that it needs to re-exec the prompt and repaint. /// This may be called in response to e.g. a color variable change. @@ -196,15 +192,6 @@ void reader_schedule_prompt_repaint(); class char_event_t; void reader_queue_ch(const char_event_t &ch); -/// Return the value of the interrupted flag, which is set by the sigint handler, and clear it if it -/// was set. In practice this will return 0 or SIGINT. -int reader_test_and_clear_interrupted(); - -/// Clear the interrupted flag unconditionally without handling anything. The flag could have been -/// set e.g. when an interrupt arrived just as we were ending an earlier \c reader_readline -/// invocation but before the \c is_interactive_read flag was cleared. -void reader_reset_interrupted(); - /// Return the value of the interrupted flag, which is set by the sigint handler, and clear it if it /// was set. If the current reader is interruptible, call \c reader_exit(). int reader_reading_interrupted(); @@ -216,6 +203,8 @@ int reader_reading_interrupted(); /// commandline. maybe_t reader_readline(int nchars); +bool reader_readline_ffi(wcstring &line, int nchars); + /// Configuration that we provide to a reader. struct reader_config_t { /// Left prompt command, typically fish_prompt. @@ -257,16 +246,13 @@ bool check_exit_loop_maybe_warning(reader_data_t *data); /// Push a new reader environment controlled by \p conf, using the given history name. /// If \p history_name is empty, then save history in-memory only; do not write it to disk. -void reader_push(parser_t &parser, const wcstring &history_name, reader_config_t &&conf); +void reader_push(const parser_t &parser, const wcstring &history_name, reader_config_t &&conf); + +void reader_push_ffi(const void *parser, const wcstring &history_name, const void *conf); /// Return to previous reader environment. void reader_pop(); -/// The readers interrupt signal handler. Cancels all currently running blocks. -extern "C" { -void reader_handle_sigint(); -} - /// \return whether fish is currently unwinding the stack in preparation to exit. bool fish_is_unwinding_for_exit(); @@ -282,7 +268,7 @@ wcstring combine_command_and_autosuggestion(const wcstring &cmdline, struct abbrs_replacement_t; maybe_t reader_expand_abbreviation_at_cursor(const wcstring &cmdline, size_t cursor_pos, - parser_t &parser); + const parser_t &parser); /// Apply a completion string. Exposed for testing only. wcstring completion_apply_to_command_line(const wcstring &val_str, complete_flags_t flags, @@ -291,22 +277,28 @@ wcstring completion_apply_to_command_line(const wcstring &val_str, complete_flag /// Snapshotted state from the reader. struct commandline_state_t { - wcstring text; // command line text, or empty if not interactive - size_t cursor_pos{0}; // position of the cursor, may be as large as text.size() - maybe_t selection{}; // visual selection, or none if none - std::shared_ptr history{}; // current reader history, or null if not interactive - bool pager_mode{false}; // pager is visible - bool pager_fully_disclosed{false}; // pager already shows everything if possible - bool search_mode{false}; // pager is visible and search is active - bool initialized{false}; // if false, the reader has not yet been entered + wcstring text; // command line text, or empty if not interactive + size_t cursor_pos{0}; // position of the cursor, may be as large as text.size() + maybe_t selection{}; // visual selection, or none if none + maybe_t> + history{}; // current reader history, or null if not interactive + bool pager_mode{false}; // pager is visible + bool pager_fully_disclosed{false}; // pager already shows everything if possible + bool search_mode{false}; // pager is visible and search is active + bool initialized{false}; // if false, the reader has not yet been entered }; /// Get the command line state. This may be fetched on a background thread. commandline_state_t commandline_get_state(); +HistorySharedPtr *commandline_get_state_history_ffi(); +bool commandline_get_state_initialized_ffi(); +wcstring commandline_get_state_text_ffi(); + /// Set the command line text and position. This may be called on a background thread; the reader /// will pick it up when it is done executing. void commandline_set_buffer(wcstring text, size_t cursor_pos = -1); +void commandline_set_buffer_ffi(const wcstring &text, size_t cursor_pos); /// Return the current interactive reads loop count. Useful for determining how many commands have /// been executed between invocations of code. @@ -316,4 +308,7 @@ uint64_t reader_run_count(); /// previous command produced a status. uint64_t reader_status_count(); +// For FFI +uint32_t read_generation_count(); + #endif diff --git a/src/redirection.h b/src/redirection.h index c0fabf7c2..4a7a4baeb 100644 --- a/src/redirection.h +++ b/src/redirection.h @@ -17,7 +17,7 @@ enum class RedirectionMode { noclob, }; struct Dup2Action; -class Dup2List; +struct Dup2List; struct RedirectionSpec; struct RedirectionSpecListFfi; diff --git a/src/screen.cpp b/src/screen.cpp index 1e50666e8..c22834e5d 100644 --- a/src/screen.cpp +++ b/src/screen.cpp @@ -706,12 +706,15 @@ bool screen_t::handle_soft_wrap(int x, int y) { /// Update the screen to match the desired output. void screen_t::update(const wcstring &left_prompt, const wcstring &right_prompt, - const environment_t &vars) { + const env_stack_t &vars) { // Helper function to set a resolved color, using the caching resolver. - highlight_color_resolver_t color_resolver{}; + auto color_resolver = new_highlight_color_resolver(); auto set_color = [&](highlight_spec_t c) { - this->outp().set_color(color_resolver.resolve_spec(c, false, vars), - color_resolver.resolve_spec(c, true, vars)); + rgb_color_t fg; + rgb_color_t bg; + color_resolver->resolve_spec(c, false, vars.get_impl_ffi(), fg); + color_resolver->resolve_spec(c, true, vars.get_impl_ffi(), bg); + this->outp().set_color(fg, bg); }; layout_cache_t &cached_layouts = layout_cache_t::shared; @@ -1141,8 +1144,10 @@ static screen_layout_t compute_layout(screen_t *s, size_t screen_width, void screen_t::write(const wcstring &left_prompt, const wcstring &right_prompt, const wcstring &commandline, size_t explicit_len, const std::vector &colors, const std::vector &indent, - size_t cursor_pos, const environment_t &vars, pager_t &pager, - page_rendering_t &page_rendering, bool cursor_is_within_pager) { + size_t cursor_pos, + // todo!("should be environment_t") + const env_stack_t &vars, pager_t &pager, page_rendering_t &page_rendering, + bool cursor_is_within_pager) { termsize_t curr_termsize = termsize_last(); int screen_width = curr_termsize.width; static relaxed_atomic_t s_repaints{0}; diff --git a/src/screen.h b/src/screen.h index 2338e9868..66179778a 100644 --- a/src/screen.h +++ b/src/screen.h @@ -22,11 +22,11 @@ #include #include "common.h" +#include "env.h" #include "highlight.h" #include "maybe.h" #include "wcstringutil.h" -class environment_t; class pager_t; class page_rendering_t; @@ -149,8 +149,10 @@ class screen_t { void write(const wcstring &left_prompt, const wcstring &right_prompt, const wcstring &commandline, size_t explicit_len, const std::vector &colors, const std::vector &indent, - size_t cursor_pos, const environment_t &vars, pager_t &pager, - page_rendering_t &page_rendering, bool cursor_is_within_pager); + size_t cursor_pos, + // todo!("this should be environment_t") + const env_stack_t &vars, pager_t &pager, page_rendering_t &page_rendering, + bool cursor_is_within_pager); /// Resets the screen buffer's internal knowledge about the contents of the screen, /// optionally repainting the prompt as well. @@ -238,7 +240,8 @@ class screen_t { /// Update the screen to match the desired output. void update(const wcstring &left_prompt, const wcstring &right_prompt, - const environment_t &vars); + // todo!("this should be environment_t") + const env_stack_t &vars); }; /// Issues an immediate clr_eos. diff --git a/src/signals.cpp b/src/signals.cpp deleted file mode 100644 index 3e1f2c4cf..000000000 --- a/src/signals.cpp +++ /dev/null @@ -1,47 +0,0 @@ -// The library for various signal related issues. -#include "config.h" // IWYU pragma: keep - -#include -#ifdef HAVE_SIGINFO_H -#include -#endif -#include - -#include -#include -#include - -#include "common.h" -#include "event.h" -#include "fallback.h" // IWYU pragma: keep -#include "global_safety.h" -#include "reader.h" -#include "signals.h" -#include "termsize.h" -#include "topic_monitor.h" -#include "wutil.h" // IWYU pragma: keep - -extern "C" { -void get_signals_with_handlers_ffi(sigset_t *set); -} -void get_signals_with_handlers(sigset_t *set) { get_signals_with_handlers_ffi(set); } - -sigchecker_t::sigchecker_t(topic_t signal) : topic_(signal) { - // Call check() to update our generation. - check(); -} - -bool sigchecker_t::check() { - auto &tm = topic_monitor_principal(); - generation_t gen = tm.generation_for_topic(topic_); - bool changed = this->gen_ != gen; - this->gen_ = gen; - return changed; -} - -void sigchecker_t::wait() const { - auto &tm = topic_monitor_principal(); - generation_list_t gens = invalid_generations(); - gens.at_mut(topic_) = this->gen_; - tm.check(&gens, true /* wait */); -} diff --git a/src/signals.h b/src/signals.h index 7c160d763..60f28b15c 100644 --- a/src/signals.h +++ b/src/signals.h @@ -7,26 +7,11 @@ #if INCLUDE_RUST_HEADERS #include "signal.rs.h" +#else +struct IoStreams; +struct SigChecker; #endif -/// Returns signals with non-default handlers. -void get_signals_with_handlers(sigset_t *set); - -enum class topic_t : uint8_t; -/// A sigint_detector_t can be used to check if a SIGINT (or SIGHUP) has been delivered. -class sigchecker_t { - const topic_t topic_; - uint64_t gen_{0}; - - public: - sigchecker_t(topic_t signal); - - /// Check if a sigint has been delivered since the last call to check(), or since the detector - /// was created. - bool check(); - - /// Wait until a sigint is delivered. - void wait() const; -}; +using sigchecker_t = SigChecker; #endif diff --git a/src/topic_monitor.h b/src/topic_monitor.h deleted file mode 100644 index f62cb9499..000000000 --- a/src/topic_monitor.h +++ /dev/null @@ -1,25 +0,0 @@ -#ifndef FISH_TOPIC_MONITOR_H -#define FISH_TOPIC_MONITOR_H - -#include "config.h" - -#include - -using generation_t = uint64_t; - -#if INCLUDE_RUST_HEADERS - -#include "topic_monitor.rs.h" - -#else - -// Hacks to allow us to compile without Rust headers. -struct generation_list_t { - uint64_t sighupint; - uint64_t sigchld; - uint64_t internal_exit; -}; - -#endif - -#endif diff --git a/src/wait_handle.h b/src/wait_handle.h deleted file mode 100644 index 6f92d60c5..000000000 --- a/src/wait_handle.h +++ /dev/null @@ -1,12 +0,0 @@ -#ifndef FISH_WAIT_HANDLE_H -#define FISH_WAIT_HANDLE_H - -// Hacks to allow us to compile without Rust headers. -struct WaitHandleStoreFFI; -struct WaitHandleRefFFI; - -#if INCLUDE_RUST_HEADERS -#include "wait_handle.rs.h" -#endif - -#endif diff --git a/src/wildcard.cpp b/src/wildcard.cpp index a58d9d2ff..f10563b85 100644 --- a/src/wildcard.cpp +++ b/src/wildcard.cpp @@ -242,8 +242,8 @@ wildcard_result_t wildcard_complete(const wcstring &str, const wchar_t *wc, /// \param is_dir Whether the file is a directory or not (might be behind a link) /// \param is_link Whether it's a link (that might point to a directory) /// \param definitely_executable Whether we know that it is executable, or don't know -static const wchar_t *file_get_desc(const wcstring &filename, bool is_dir, - bool is_link, bool definitely_executable) { +static const wchar_t *file_get_desc(const wcstring &filename, bool is_dir, bool is_link, + bool definitely_executable) { if (is_link) { if (is_dir) { return COMPLETE_DIRECTORY_SYMLINK_DESC; @@ -267,7 +267,8 @@ static const wchar_t *file_get_desc(const wcstring &filename, bool is_dir, /// up. Note that the filename came from a readdir() call, so we know it exists. static bool wildcard_test_flags_then_complete(const wcstring &filepath, const wcstring &filename, const wchar_t *wc, expand_flags_t expand_flags, - completion_receiver_t *out, const dir_iter_t::entry_t &entry) { + completion_receiver_t *out, + const dir_iter_t::entry_t &entry) { const bool executables_only = expand_flags & expand_flag::executables_only; const bool need_directory = expand_flags & expand_flag::directories_only; // Fast path: If we need directories, and we already know it is one, @@ -322,7 +323,8 @@ static bool wildcard_test_flags_then_complete(const wcstring &filepath, const wc } // If we have executables_only, we already checked waccess above, - // so we tell file_get_desc that this file is definitely executable so it can skip the check. + // so we tell file_get_desc that this file is definitely executable so it can skip the + // check. desc = file_get_desc(filepath, entry.is_dir(), is_link, executables_only); } @@ -553,7 +555,8 @@ void wildcard_expander_t::expand_trailing_slash(const wcstring &base_dir, const } if (!(flags & expand_flag::for_completions)) { - // Trailing slash and not accepting incomplete, e.g. `echo /xyz/`. Insert this file, we already know it exists! + // Trailing slash and not accepting incomplete, e.g. `echo /xyz/`. Insert this file, we + // already know it exists! this->add_expansion_result(wcstring{base_dir}); } else { // Trailing slashes and accepting incomplete, e.g. `echo /xyz/`. Everything is added. diff --git a/src/wildcard.h b/src/wildcard.h index d60c37fb6..a9bf652ae 100644 --- a/src/wildcard.h +++ b/src/wildcard.h @@ -8,6 +8,9 @@ #include "common.h" #include "complete.h" #include "expand.h" +#if INCLUDE_RUST_HEADERS +#include "wildcard.rs.h" +#endif /// Description for generic executable. #define COMPLETE_EXEC_DESC _(L"command") @@ -70,46 +73,5 @@ enum class wildcard_result_t { cancel, /// Expansion was cancelled (e.g. control-C). overflow, /// Expansion produced too many results. }; -wildcard_result_t wildcard_expand_string(const wcstring &wc, const wcstring &working_directory, - expand_flags_t flags, - const cancel_checker_t &cancel_checker, - completion_receiver_t *output); - -#if INCLUDE_RUST_HEADERS - -#include "wildcard.rs.h" - -#else -/// Test whether the given wildcard matches the string. Does not perform any I/O. -/// -/// \param str The string to test -/// \param wc The wildcard to test against -/// \param leading_dots_fail_to_match if set, strings with leading dots are assumed to be hidden -/// files and are not matched -/// -/// \return true if the wildcard matched -bool wildcard_match_ffi(const wcstring &str, const wcstring &wc, bool leading_dots_fail_to_match); - -// Check if the string has any unescaped wildcards (e.g. ANY_STRING). -bool wildcard_has_internal(const wcstring &s); - -/// Check if the specified string contains wildcards (e.g. *). -bool wildcard_has(const wcstring &s); - -#endif - -inline bool wildcard_match(const wcstring &str, const wcstring &wc, - bool leading_dots_fail_to_match = false) { - return wildcard_match_ffi(str, wc, leading_dots_fail_to_match); - } - -inline bool wildcard_has(const wchar_t *s, size_t len) { - return wildcard_has(wcstring(s, len)); -}; - -/// Test wildcard completion. -wildcard_result_t wildcard_complete(const wcstring &str, const wchar_t *wc, - const description_func_t &desc_func, completion_receiver_t *out, - expand_flags_t expand_flags, complete_flags_t flags); #endif diff --git a/src/wutil.cpp b/src/wutil.cpp index 391f3c65c..5e690f02d 100644 --- a/src/wutil.cpp +++ b/src/wutil.cpp @@ -40,6 +40,8 @@ using cstring = std::string; const file_id_t kInvalidFileID{}; +wcstring_list_ffi_t::~wcstring_list_ffi_t() = default; + /// Map used as cache by wgettext. static owning_lock> wgettext_map; diff --git a/src/wutil.h b/src/wutil.h index 121d8f1f1..5e11577c6 100644 --- a/src/wutil.h +++ b/src/wutil.h @@ -44,7 +44,9 @@ struct wcstring_list_ffi_t { wcstring_list_ffi_t() = default; /* implicit */ wcstring_list_ffi_t(std::vector vals) : vals(std::move(vals)) {} + ~wcstring_list_ffi_t(); + bool empty() const { return vals.empty(); } size_t size() const { return vals.size(); } const wcstring &at(size_t idx) const { return vals.at(idx); } void clear() { vals.clear(); } @@ -62,6 +64,16 @@ struct wcstring_list_ffi_t { static void check_test_data(wcstring_list_ffi_t data); }; +/// Convert an iterable of strings to a list of wcharz_t. +template +std::vector wcstring_list_to_ffi(const T &list) { + std::vector result; + for (const wcstring &str : list) { + result.push_back(str.c_str()); + } + return result; +} + class autoclose_fd_t; /// Wide character version of opendir(). Note that opendir() is guaranteed to set close-on-exec by