Files
fish-shell/src/parser.rs
2026-01-25 03:19:22 +01:00

2485 lines
85 KiB
Rust

// The fish parser. Contains functions for parsing and evaluating code.
use crate::ast::{self, Node};
use crate::builtins::shared::STATUS_ILLEGAL_CMD;
use crate::common::{
CancelChecker, EscapeFlags, EscapeStringStyle, FilenameRef, PROFILING_ACTIVE, ScopeGuarding,
ScopedCell, ScopedRefCell, escape_string, wcs2bytes,
};
use crate::complete::CompletionList;
use crate::env::{
EnvMode, EnvSetMode, EnvStack, EnvStackSetResult, Environment, FISH_TERMINAL_COLOR_THEME_VAR,
Statuses,
};
use crate::event::{self, Event};
use crate::expand::{
ExpandFlags, ExpandResultCode, expand_string, replace_home_directory_with_tilde,
};
use crate::fds::{BEST_O_SEARCH, open_dir};
use crate::global_safety::RelaxedAtomicBool;
use crate::input_common::TerminalQuery;
use crate::io::IoChain;
use crate::job_group::MaybeJobId;
use crate::operation_context::{EXPANSION_LIMIT_DEFAULT, OperationContext};
use crate::parse_constants::{
FISH_MAX_EVAL_DEPTH, FISH_MAX_STACK_DEPTH, ParseError, ParseErrorList, ParseTreeFlags,
SOURCE_LOCATION_UNKNOWN,
};
use crate::parse_execution::{EndExecutionReason, ExecutionContext};
use crate::parse_tree::NodeRef;
use crate::parse_tree::{LineCounter, ParsedSourceRef, parse_source};
use crate::portable_atomic::AtomicU64;
use crate::prelude::*;
use crate::proc::{JobGroupRef, JobList, JobRef, Pid, ProcStatus, job_reap};
use crate::signal::{Signal, signal_check_cancel, signal_clear_cancel};
use crate::wait_handle::WaitHandleStore;
use crate::wutil::perror;
use crate::{flog, flogf, function};
use assert_matches::assert_matches;
use fish_util::get_time;
use fish_widestring::WExt;
use libc::c_int;
use std::cell::{Ref, RefCell, RefMut};
use std::ffi::OsStr;
use std::fs::File;
use std::io::Write;
use std::num::NonZeroU32;
use std::os::fd::OwnedFd;
use std::rc::Rc;
use std::sync::Arc;
use std::time::Duration;
pub enum BlockData {
Function {
/// Name of the function
name: WString,
/// Arguments passed to the function
args: Vec<WString>,
},
Event(Rc<Event>),
Source {
/// The sourced file
file: Arc<WString>,
},
}
/// block_t represents a block of commands.
#[derive(Default)]
pub struct Block {
/// Type of block.
block_type: BlockType,
/// [`BlockType`]-specific data.
///
/// None of these data fields are accessed on a regular basis (only for shell introspection), so
/// we store them in a `Box` to reduce the size of the `Block` itself.
pub data: Option<Box<BlockData>>,
/// Pseudo-counter of event blocks
pub event_blocks: bool,
/// Name of the file that created this block
pub src_filename: Option<Arc<WString>>,
/// Line number where this block was created, starting from 1.
pub src_lineno: Option<NonZeroU32>,
}
impl Block {
#[inline(always)]
pub fn data(&self) -> Option<&BlockData> {
self.data.as_deref()
}
#[inline(always)]
pub fn wants_pop_env(&self) -> bool {
self.typ() != 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.
pub fn description(&self) -> WString {
let mut result = match self.typ() {
BlockType::while_block => L!("while"),
BlockType::for_block => L!("for"),
BlockType::if_block => L!("if"),
BlockType::function_call { .. } => L!("function_call"),
BlockType::switch_block => L!("switch"),
BlockType::subst => L!("substitution"),
BlockType::top => L!("top"),
BlockType::begin => L!("begin"),
BlockType::source => L!("source"),
BlockType::event => L!("event"),
BlockType::breakpoint => L!("breakpoint"),
BlockType::variable_assignment => L!("variable_assignment"),
}
.to_owned();
if let Some(src_lineno) = self.src_lineno {
result.push_utfstr(&sprintf!(" (line %d)", src_lineno.get()));
}
if let Some(src_filename) = &self.src_filename {
result.push_utfstr(&sprintf!(" (file %s)", 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 {
matches!(self.typ(), BlockType::function_call { .. })
}
/// 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.data = Some(Box::new(BlockData::Event(Rc::new(event))));
b
}
pub fn function_block(name: WString, args: Vec<WString>, shadows: bool) -> Block {
let mut b = Block::new(BlockType::function_call { shadows });
b.data = Some(Box::new(BlockData::Function { name, args }));
b
}
pub fn source_block(src: FilenameRef) -> Block {
let mut b = Block::new(BlockType::source);
b.data = Some(Box::new(BlockData::Source { file: 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()
}
}
/// Data which is managed in a scoped fashion: is generally set for the duration of a block
/// of code. Note this is stored in a Cell and so must be Copy.
#[derive(Copy, Clone)]
pub struct ScopedData {
/// The 'depth' of the fish call stack.
/// -1 means nothing is executing. 0 means we are running a top-level command.
/// Larger values indicate deeper nesting.
pub eval_level: isize,
/// Whether we are running a subshell command.
pub is_subshell: bool,
/// Whether we are running an event handler.
pub is_event: bool,
/// Whether we are currently interactive.
pub is_interactive: bool,
/// Whether the command line is closed for modification from fish script.
pub readonly_commandline: bool,
/// Whether to suppress fish_trace output. This occurs in the prompt, event handlers, and key
/// bindings.
pub suppress_fish_trace: bool,
/// The read limit to apply to captured subshell output, or 0 for none.
pub read_limit: usize,
/// 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
}
impl Default for ScopedData {
fn default() -> Self {
Self {
eval_level: -1,
is_subshell: false,
is_event: false,
readonly_commandline: false,
is_interactive: false,
suppress_fish_trace: false,
read_limit: 0,
is_cleaning_procs: false,
caller_id: 0,
}
}
}
/// Miscellaneous data used to avoid recursion and others.
#[derive(Default)]
pub struct LibraryData {
/// The current filename we are evaluating, either from builtin source or on the command line.
pub current_filename: Option<FilenameRef>,
/// A fake value 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_commandline: Option<WString>,
/// A file descriptor holding the current working directory, for use in openat().
/// This is never null and never invalid.
pub cwd_fd: Option<Arc<OwnedFd>>,
/// Variables supporting the "status" builtin.
pub status_vars: StatusVars,
/// 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 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,
}
impl LibraryData {
pub fn new() -> Self {
Self {
last_exec_run_counter: u64::MAX,
..Default::default()
}
}
}
/// 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_,
}
/// A newtype for the block index.
/// This is the naive position in the block list.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BlockId(usize);
/// Controls the behavior when fish itself receives a signal and there are
/// no blocks on the stack.
/// The "outermost" parser is responsible for clearing the signal.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum CancelBehavior {
#[default]
/// Return the signal to the caller
Return,
/// Clear the signal
Clear,
}
pub struct Parser {
pub interactive_initialized: RelaxedAtomicBool,
/// A shared line counter. This is handed out to each execution context
/// so they can communicate the line number back to this Parser.
line_counter: ScopedRefCell<LineCounter<ast::JobPipeline>>,
/// The jobs associated with this parser.
job_list: RefCell<JobList>,
/// 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<WaitHandleStore>,
/// 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<Vec<Block>>,
/// Set of variables for the parser.
pub variables: EnvStack,
/// Data managed in a scoped fashion.
scoped_data: ScopedCell<ScopedData>,
/// Miscellaneous library data.
pub library_data: ScopedRefCell<LibraryData>,
/// If set, we synchronize universal variables after external commands,
/// including sending on-variable change events.
syncs_uvars: RelaxedAtomicBool,
/// The behavior when fish itself receives a signal and there are no blocks on the stack.
cancel_behavior: CancelBehavior,
/// List of profile items.
profile_items: RefCell<Vec<ProfileItem>>,
/// Global event blocks.
pub global_event_blocks: AtomicU64,
pub blocking_query: RefCell<Option<TerminalQuery>>,
// Timeout for blocking terminal queries.
pub blocking_query_timeout: RefCell<Option<Duration>>,
}
#[derive(Copy, Clone, Default)]
pub struct ParserEnvSetMode {
pub mode: EnvMode,
pub user: bool,
}
impl ParserEnvSetMode {
pub fn new(mode: EnvMode) -> Self {
Self { mode, user: false }
}
pub fn user(mode: EnvMode) -> Self {
Self { mode, user: true }
}
}
impl Parser {
/// Create a parser.
pub fn new(variables: EnvStack, cancel_behavior: CancelBehavior) -> Parser {
let result = Self {
interactive_initialized: RelaxedAtomicBool::new(false),
line_counter: ScopedRefCell::new(LineCounter::empty()),
job_list: RefCell::default(),
wait_handles: RefCell::default(),
block_list: RefCell::default(),
variables,
scoped_data: ScopedCell::new(ScopedData::default()),
library_data: ScopedRefCell::new(LibraryData::new()),
syncs_uvars: RelaxedAtomicBool::new(false),
cancel_behavior,
profile_items: RefCell::default(),
global_event_blocks: AtomicU64::new(0),
blocking_query: RefCell::new(None),
blocking_query_timeout: RefCell::new(None),
};
match open_dir(c".", BEST_O_SEARCH) {
Ok(fd) => {
result.libdata_mut().cwd_fd = Some(Arc::new(fd));
}
Err(_) => {
perror("Unable to open the current working directory");
}
}
result
}
/// 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 {
self.blocks_iter_rev()
// If a function sources a file, don't descend further.
.take_while(|b| b.typ() != BlockType::source)
.any(|b| b.is_function_call())
}
/// Return whether we are currently evaluating a command substitution.
pub fn is_command_substitution(&self) -> bool {
self.blocks_iter_rev()
// If a function sources a file, don't descend further.
.take_while(|b| b.typ() != BlockType::source)
.any(|b| b.typ() == BlockType::subst)
}
pub fn eval(&self, cmd: &wstr, io: &IoChain) -> EvalRes {
self.eval_with(cmd, io, None, BlockType::top, false)
}
/// 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,
test_only_suppress_stderr: bool,
) -> 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::default(),
Some(&mut error_list),
) {
return self.eval_parsed_source(
&ps,
io,
job_group,
block_type,
test_only_suppress_stderr,
);
}
// Get a backtrace. This includes the message.
let backtrace_and_desc = self.get_backtrace(cmd, &error_list);
if !test_only_suppress_stderr {
// Print it.
eprintf!("%s\n", backtrace_and_desc);
}
// Set a valid status.
self.set_last_statuses(Statuses::just(STATUS_ILLEGAL_CMD));
let break_expand = true;
EvalRes {
status: ProcStatus::from_exit_code(STATUS_ILLEGAL_CMD),
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,
test_only_suppress_stderr: bool,
) -> EvalRes {
assert_matches!(block_type, BlockType::top | BlockType::subst);
let job_list = ps.top_job_list();
if !job_list.is_empty() {
// Execute the top job list.
self.eval_node(
&job_list,
io,
job_group,
block_type,
test_only_suppress_stderr,
)
} else {
let status = ProcStatus::from_exit_code(self.get_last_status());
EvalRes {
status,
break_expand: false,
was_empty: true,
no_status: true,
}
}
}
pub fn eval_wstr(
&self,
src: WString,
io: &IoChain,
job_group: Option<&JobGroupRef>,
block_type: BlockType,
) -> Result<EvalRes, WString> {
use crate::parse_tree::ParsedSource;
use crate::parse_util::detect_parse_errors_in_ast;
let mut errors = vec![];
let ast = ast::parse(&src, ParseTreeFlags::default(), Some(&mut errors));
let mut errored = ast.errored();
if !errored {
errored = detect_parse_errors_in_ast(&ast, &src, Some(&mut errors)).is_err();
}
if errored {
let sb = self.get_backtrace(&src, &errors);
return Err(sb);
}
// Construct a parsed source ref.
// Be careful to transfer ownership, this could be a very large string.
let ps = Arc::new(ParsedSource::new(src, ast));
Ok(self.eval_parsed_source(&ps, io, job_group, block_type, false))
}
pub fn eval_file_wstr(
&self,
src: WString,
filename: Arc<WString>,
io: &IoChain,
job_group: Option<&JobGroupRef>,
) -> Result<EvalRes, WString> {
let _interactive_push = self.push_scope(|s| s.is_interactive = false);
let sb = self.push_block(Block::source_block(filename.clone()));
let _filename_push = self
.library_data
.scoped_set(Some(filename), |s| &mut s.current_filename);
let ret = self.eval_wstr(src, io, job_group, BlockType::top);
self.pop_block(sb);
self.libdata_mut().exit_current_script = false;
ret
}
/// Evaluates a node.
/// The node type must be ast::Statement or ast::JobList.
pub fn eval_node<T: Node>(
&self,
node: &NodeRef<T>,
block_io: &IoChain,
job_group: Option<&JobGroupRef>,
block_type: BlockType,
test_only_suppress_stderr: bool,
) -> EvalRes {
// Only certain blocks are allowed.
assert_matches!(
block_type,
BlockType::top | BlockType::subst,
"Invalid block type"
);
// If fish itself got a cancel signal, then we want to unwind back to the parser which
// has a Clear cancellation behavior.
// 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.cancel_behavior == CancelBehavior::Clear && 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, Some(block_io)); // 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;
// Restore the line counter.
let ps = node.parsed_source_ref();
let restore_line_counter = self.line_counter.scoped_replace(ps.line_counter());
// Create a new execution context.
let mut execution_context = ExecutionContext::new(
ps,
block_io.clone(),
&self.line_counter,
test_only_suppress_stderr,
);
// Check the exec count so we know if anything got executed.
let prev_exec_count = self.libdata().exec_count;
let prev_status_count = self.libdata().status_count;
let reason = execution_context.eval_node(&op_ctx, &**node, Some(scope_block));
let new_exec_count = self.libdata().exec_count;
let new_status_count = self.libdata().status_count;
ScopeGuarding::commit(restore_line_counter);
self.pop_block(scope_block);
job_reap(self, false, Some(block_io)); // 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![];
for arg in &ast.top().arguments {
let arg_src = arg.source(arg_list_src);
if matches!(
expand_string(arg_src.to_owned(), &mut result, flags, ctx, None).result,
ExpandResultCode::error | ExpandResultCode::overflow
) {
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 {
let Some(source_offset) = self.line_counter.borrow_mut().source_offset_of_node() else {
return WString::new();
};
let lineno = self.get_lineno_for_display();
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!(
"%s (line %d): ",
&user_presentable_path(&file, self.vars()),
lineno
));
} else if self.libdata().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 empty_error = ParseError {
source_start: source_offset,
..Default::default()
};
let mut line_info = empty_error.describe_with_prefix(
self.line_counter.borrow().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, indexed from 1.
pub fn get_lineno(&self) -> Option<NonZeroU32> {
// The offset is 0 based; the number is 1 based.
self.line_counter
.borrow_mut()
.line_offset_of_node()
.map(|offset| NonZeroU32::new(offset.saturating_add(1)).unwrap())
}
/// Returns the current line number, indexed from 1, or zero if not sourced.
pub fn get_lineno_for_display(&self) -> u32 {
self.get_lineno().map_or(0, |val| val.get())
}
/// 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'.
self.blocks_iter_rev().any(|b| {
![
BlockType::top,
BlockType::subst,
BlockType::variable_assignment,
]
.contains(&b.typ())
})
}
/// Return whether we have a breakpoint block.
pub fn is_breakpoint(&self) -> bool {
self.blocks_iter_rev()
.any(|b| b.typ() == BlockType::breakpoint)
}
// Return an iterator over the blocks, in reverse order.
// That is, the first block is the innermost block.
pub fn blocks_iter_rev<'a>(&'a self) -> impl Iterator<Item = Ref<'a, Block>> {
let blocks = self.block_list.borrow();
let mut indices = (0..blocks.len()).rev();
std::iter::from_fn(move || {
let last = indices.next()?;
// note this clone is cheap
Some(Ref::map(Ref::clone(&blocks), |bl| &bl[last]))
})
}
// Return the block at a given index, where 0 is the innermost block.
pub fn block_at_index(&self, index: usize) -> Option<Ref<'_, Block>> {
let block_list = self.block_list.borrow();
let block_count = block_list.len();
if index >= block_count {
None
} else {
Some(Ref::map(block_list, |bl| &bl[block_count - 1 - index]))
}
}
// Return the block at a given index, where 0 is the innermost block.
pub fn block_at_index_mut(&self, index: usize) -> Option<RefMut<'_, Block>> {
let block_list = self.block_list.borrow_mut();
let block_count = block_list.len();
if index >= block_count {
None
} else {
Some(RefMut::map(block_list, |bl| {
&mut bl[block_count - 1 - index]
}))
}
}
// Return the block with the given id, asserting it exists. Note ids are recycled.
pub fn block_with_id(&self, id: BlockId) -> Ref<'_, Block> {
Ref::map(self.block_list.borrow(), |bl| &bl[id.0])
}
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 a copy of the scoped data.
#[inline(always)]
pub fn scope(&self) -> ScopedData {
self.scoped_data.get()
}
/// Modify the scoped values for the duration of the caller's scope (or whenever the ParserScope is dropped).
/// This accepts a closure which modifies the ScopedData, and returns a ParserScope which restores the
/// data when dropped.
pub fn push_scope<'a, F: FnOnce(&mut ScopedData)>(
&'a self,
modifier: F,
) -> impl ScopeGuarding + 'a {
self.scoped_data.scoped_mod(modifier)
}
/// Get the library data.
pub fn libdata(&self) -> Ref<'_, LibraryData> {
self.library_data.borrow()
}
/// Get the library data, mutably.
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.
pub fn set_var_and_fire(
&self,
key: &wstr,
mode: ParserEnvSetMode,
vals: Vec<WString>,
) -> EnvStackSetResult {
let res = self.set_var(key, mode, vals);
if res == EnvStackSetResult::Ok {
event::fire(self, Event::variable_set(key.to_owned()));
}
res
}
pub fn is_repainting(&self) -> bool {
self.libdata().is_repaint
}
pub fn convert_env_set_mode(&self, mode: ParserEnvSetMode) -> EnvSetMode {
EnvSetMode::new_with(mode.mode, mode.user, self.is_repainting())
}
/// Cover of vars().set(), without firing events
pub fn set_var(
&self,
key: &wstr,
mode: ParserEnvSetMode,
vals: Vec<WString>,
) -> EnvStackSetResult {
let mode = self.convert_env_set_mode(mode);
self.vars().set(key, mode, vals)
}
/// Cover of vars().set_one(), without firing events
pub fn set_one(&self, key: &wstr, mode: ParserEnvSetMode, val: WString) -> EnvStackSetResult {
let mode = self.convert_env_set_mode(mode);
self.vars().set_one(key, mode, val)
}
/// Cover of vars().set_empty(), without firing events
pub fn set_empty(&self, key: &wstr, mode: ParserEnvSetMode) -> EnvStackSetResult {
let mode = self.convert_env_set_mode(mode);
self.vars().set_empty(key, mode)
}
/// Cover of vars().remove(), without firing events
pub fn remove_var(&self, key: &wstr, mode: ParserEnvSetMode) -> EnvStackSetResult {
let mode = self.convert_env_set_mode(mode);
self.vars().remove(key, mode)
}
/// Update any universal variables and send event handlers.
/// If `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, self.is_repainting());
for evt in evts {
event::fire(self, evt);
}
}
}
/// Pushes a new block. Returns an id (index) of the block, which is 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 { shadows: true };
self.vars().push(new_scope);
}
let mut block_list = self.block_list.borrow_mut();
block_list.push(block);
BlockId(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_eq!(expected.0, block_list.len() - 1);
block_list.pop().unwrap()
};
if block.wants_pop_env() {
self.vars().pop(self.is_repainting());
}
}
/// Return the function name for the specified stack frame. Default is one (current frame).
pub fn get_function_name(&self, level: i32) -> Option<WString> {
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.
return self
.blocks_iter_rev()
.skip_while(|b| b.typ() != BlockType::breakpoint)
.find_map(|b| match b.data() {
Some(BlockData::Function { name, .. }) => Some(name.clone()),
_ => None,
});
}
self.blocks_iter_rev()
// 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.
.take_while(|b| !(level == 1 && b.typ() == BlockType::source))
.map(|b| (b, 0))
.map(|(b, level)| {
if b.is_function_call() {
(b, level + 1)
} else {
(b, level)
}
})
.skip_while(|(_, l)| *l != level)
.inspect(|(b, _)| debug_assert!(b.is_function_call()))
.map(|(b, _)| {
let Some(BlockData::Function { name, .. }) = b.data() else {
unreachable!()
};
name.clone()
})
.next()
}
/// 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<JobRef> {
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: Pid) -> Option<JobRef> {
self.job_get_with_index_from_pid(pid).map(|t| t.1)
}
/// Returns the job and job index with the given pid.
/// This assumes that all external jobs have a pid.
pub fn job_get_with_index_from_pid(&self, pid: Pid) -> Option<(usize, JobRef)> {
for (i, job) in self.jobs().iter().enumerate() {
for p in job.external_procs() {
if p.pid().unwrap() == 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 None.
pub fn create_profile_item(&self) -> Option<usize> {
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<ProfileItem>> {
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: &OsStr) {
// Save profiling information. OK to not use CLO_EXEC here because this is called while fish is
// exiting (and hence will not fork).
let mut f = match std::fs::File::create(path) {
Ok(f) => f,
Err(err) => {
flog!(
warning,
wgettext_fmt!(
"Could not write profiling information to file '%s': %s",
path.to_string_lossy(),
err.to_string()
)
);
return;
}
};
print_profile(&self.profile_items.borrow(), &mut f);
}
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!(
"%s (line %u): ",
user_presentable_path(&filename, self.vars()),
which_line
)
} else {
sprintf!("%s: ", 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<FilenameRef> {
self.blocks_iter_rev()
.find_map(|b| match b.data() {
Some(BlockData::Function { name, .. }) => {
function::get_props(name).and_then(|props| props.definition_file.clone())
}
Some(BlockData::Source { file }) => Some(file.clone()),
_ => None,
})
.or_else(|| 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.scope().is_interactive
}
/// Return a string representing the current stack trace.
pub fn stack_trace(&self) -> WString {
self.blocks_iter_rev()
// 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.
.take_while(|b| b.typ() != BlockType::event)
.fold(WString::new(), |mut trace, b| {
append_block_description_to_stack_trace(self, &b, &mut trace);
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.scope().eval_level <= FISH_MAX_STACK_DEPTH {
return false;
}
// Count the functions.
let depth = self
.blocks_iter_rev()
.filter(|b| b.is_function_call())
.count();
depth > (FISH_MAX_STACK_DEPTH as usize)
}
/// Mark whether we should sync universal variables.
pub fn set_syncs_uvars(&self, flag: bool) {
self.syncs_uvars.store(flag);
}
/// Return the operation context for this parser.
pub fn context(&self) -> OperationContext<'_> {
OperationContext::foreground(
self,
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.scope().eval_level >= FISH_MAX_EVAL_DEPTH
}
pub fn set_color_theme(&self, background_color: Option<&xterm_color::Color>) {
let color_theme = match background_color.map(|c| c.perceived_lightness()) {
Some(x) if x < 0.5 => L!("dark"),
Some(_) => L!("light"),
None => L!("unknown"),
};
if self
.vars()
.get(FISH_TERMINAL_COLOR_THEME_VAR)
.is_some_and(|var| var.as_list() == [color_theme])
{
return;
}
flogf!(
reader,
"Setting %s to %s",
FISH_TERMINAL_COLOR_THEME_VAR,
color_theme
);
self.set_var_and_fire(
FISH_TERMINAL_COLOR_THEME_VAR,
ParserEnvSetMode::new(EnvMode::GLOBAL),
vec![color_theme.to_owned()],
);
}
}
// 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: &mut File) {
let col_width = 10;
let _ = out.write_all(
format!(
"{:^col_width$} {:^col_width$} Command\n",
"Time (μs)", "Sum (μs)",
)
.as_bytes(),
);
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;
}
}
let level = item.level.unsigned_abs().saturating_add(1);
let _ = out.write_all(
format!(
"{:>col_width$} {:>col_width$} {:->level$} ",
self_time, total_time, '>'
)
.as_bytes(),
);
let indentation_level = col_width + 1 + col_width + 1 + level + 1;
let indented_cmd = item.cmd.replace(
L!("\n"),
&(WString::from("\n") + &wstr::repeat(L!(" "), indentation_level)[..]),
);
let _ = out.write_all(&wcs2bytes(&indented_cmd));
let _ = out.write_all(b"\n");
}
}
/// Append stack trace info for the block `b` to `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 { .. } => {
let Some(BlockData::Function { name, args, .. }) = b.data() else {
unreachable!()
};
trace.push_utfstr(&wgettext_fmt!("in function '%s'", name));
// Print arguments on the same line.
let mut args_str = WString::new();
for arg in 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 '%s'", 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 Some(BlockData::Source { file, .. }) = b.data() else {
unreachable!()
};
let source_dest = file;
trace.push_utfstr(&wgettext_fmt!(
"from sourcing file %s\n",
&user_presentable_path(source_dest, parser.vars())
));
print_call_site = true;
}
BlockType::event => {
let Some(BlockData::Event(event)) = b.data() else {
unreachable!()
};
let description = event::get_desc(parser, event);
trace.push_utfstr(&wgettext_fmt!("in event handler: %s\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 => {}
}
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 %s\n",
b.src_lineno.map_or(0, |n| n.get()),
user_presentable_path(file, parser.vars())
));
} else if parser.libdata().within_fish_init {
trace.push_str("\tcalled during startup\n");
}
}
}
/// Types of blocks.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum BlockType {
/// While loop block
while_block,
/// For loop block
for_block,
/// If block
if_block,
/// Function invocation block
function_call { shadows: bool },
/// Switch block
switch_block,
/// Command substitution scope
subst,
/// Outermost block
#[default]
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.
#[derive(Clone, Copy, Default, Eq, PartialEq)]
pub enum LoopStatus {
/// current loop block executed as normal
#[default]
normals,
/// current loop block should be removed
breaks,
/// current loop block should be skipped
continues,
}
#[cfg(test)]
mod tests {
use super::{CancelBehavior, Parser};
use crate::ast::{
self, Ast, Castable, JobList, JobPipeline, Kind, Node, Traversal, is_same_node,
};
use crate::env::EnvStack;
use crate::expand::ExpandFlags;
use crate::io::{IoBufferfill, IoChain};
use crate::parse_constants::{
ParseErrorCode, ParseTokenType, ParseTreeFlags, ParserTestErrorBits, StatementDecoration,
};
use crate::parse_tree::{LineCounter, parse_source};
use crate::parse_util::{detect_errors_in_argument, detect_parse_errors};
use crate::prelude::*;
use crate::reader::{fake_scoped_reader, reader_reset_interrupted};
use crate::signal::{signal_clear_cancel, signal_reset_handlers, signal_set_handlers};
use crate::tests::prelude::*;
use fish_wcstringutil::join_strings;
use libc::SIGINT;
use std::time::Duration;
#[test]
#[serial]
fn test_parser() {
let _cleanup = test_init();
macro_rules! detect_errors {
($src:literal) => {
detect_parse_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().arguments;
let first_arg = args.first().expect("Failed to parse an argument");
let mut errors = None;
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_eq!(
detect_errors!("true | "),
Err(ParserTestErrorBits::INCOMPLETE),
"unterminated pipe not reported properly"
);
assert_eq!(
detect_errors!("echo (\nfoo\n bar"),
Err(ParserTestErrorBits::INCOMPLETE),
"unterminated multiline subshell not reported properly"
);
assert_eq!(
detect_errors!("begin ; true ; end | "),
Err(ParserTestErrorBits::INCOMPLETE),
"unterminated pipe not reported properly"
);
assert_eq!(
detect_errors!(" | true "),
Err(ParserTestErrorBits::ERROR),
"leading pipe not reported properly"
);
assert_eq!(
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_eq!(
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
// 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_eq!(
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_eq!(
detect_errors!("true || \n"),
Err(ParserTestErrorBits::INCOMPLETE),
"unterminated conjunction not reported properly"
);
assert_eq!(
detect_errors!("begin ; echo hi; }"),
Err(ParserTestErrorBits::ERROR),
"closing of unopened brace statement not reported properly"
);
assert_eq!(
detect_errors!("begin {"), // }
Err(ParserTestErrorBits::INCOMPLETE),
"brace after begin not reported properly"
);
assert_eq!(
detect_errors!("a=b {"), // }
Err(ParserTestErrorBits::INCOMPLETE),
"brace after variable override not reported properly"
);
}
#[test]
#[serial]
fn test_new_parser_correctness() {
let _cleanup = test_init();
macro_rules! validate {
($src:expr, $ok:expr) => {
let ast = ast::parse(L!($src), ParseTreeFlags::default(), None);
assert_eq!(ast.errored(), !$ok);
};
}
validate!("; ; ; ", true);
validate!("if ; end", false);
validate!("if true ; end", true);
validate!("if true; end ; end", false);
validate!("if end; end ; end", false);
validate!("if end", false);
validate!("end", false);
validate!("for i i", false);
validate!("for i in a b c ; end", true);
validate!("begin end", true);
validate!("begin; end", true);
validate!("begin if true; end; end;", true);
validate!("begin if true ; echo hi ; end; end", true);
validate!("true && false || false", true);
validate!("true || false; and true", true);
validate!("true || ||", false);
validate!("|| true", false);
validate!("true || \n\n false", true);
}
#[test]
#[serial]
fn test_new_parser_correctness_by_fuzzing() {
let _cleanup = test_init();
let fuzzes = [
L!("if"),
L!("else"),
L!("for"),
L!("in"),
L!("while"),
L!("begin"),
L!("function"),
L!("switch"),
L!("case"),
L!("end"),
L!("and"),
L!("or"),
L!("not"),
L!("command"),
L!("builtin"),
L!("foo"),
L!("|"),
L!("^"),
L!("&"),
L!(";"),
];
// Generate a list of strings of all keyword / token combinations.
let mut src = WString::new();
src.reserve(128);
// Given that we have an array of 'fuzz_count' strings, we wish to enumerate all permutations of
// 'len' values. We do this by incrementing an integer, interpreting it as "base fuzz_count".
fn string_for_permutation(
fuzzes: &[&wstr],
len: usize,
permutation: usize,
) -> Option<WString> {
let mut remaining_permutation = permutation;
let mut out_str = WString::new();
for _i in 0..len {
let idx = remaining_permutation % fuzzes.len();
remaining_permutation /= fuzzes.len();
out_str.push_utfstr(fuzzes[idx]);
out_str.push(' ');
}
// Return false if we wrapped.
(remaining_permutation == 0).then_some(out_str)
}
let max_len = 5;
for len in 0..max_len {
// We wish to look at all permutations of 4 elements of 'fuzzes' (with replacement).
// Construct an int and keep incrementing it.
let mut permutation = 0;
while let Some(src) = string_for_permutation(&fuzzes, len, permutation) {
permutation += 1;
ast::parse(&src, ParseTreeFlags::default(), None);
}
}
}
// Test the LL2 (two token lookahead) nature of the parser by exercising the special builtin and
// command handling. In particular, 'command foo' should be a decorated statement 'foo' but 'command
// -help' should be an undecorated statement 'command' with argument '--help', and NOT attempt to
// run a command called '--help'.
#[test]
#[serial]
fn test_new_parser_ll2() {
let _cleanup = test_init();
// Parse a statement, returning the command, args (joined by spaces), and the decoration. Returns
// true if successful.
fn test_1_parse_ll2(src: &wstr) -> Option<(WString, WString, StatementDecoration)> {
let ast = ast::parse(src, ParseTreeFlags::default(), None);
if ast.errored() {
return None;
}
// Get the statement. Should only have one.
let mut statement = None;
for n in Traversal::new(ast.top()) {
if let Kind::DecoratedStatement(tmp) = n.kind() {
assert!(
statement.is_none(),
"More than one decorated statement found in '{}'",
src
);
statement = Some(tmp);
}
}
let statement = statement.expect("No decorated statement found");
// Return its decoration and command.
let out_deco = statement.decoration();
let out_cmd = statement.command.source(src).to_owned();
// Return arguments separated by spaces.
let out_joined_args = join_strings(
&statement
.args_or_redirs
.iter()
.filter(|a| a.is_argument())
.map(|a| a.source(src))
.collect::<Vec<_>>(),
' ',
);
Some((out_cmd, out_joined_args, out_deco))
}
macro_rules! validate {
($src:expr, $cmd:expr, $args:expr, $deco:expr) => {
let (cmd, args, deco) = test_1_parse_ll2(L!($src)).unwrap();
assert_eq!(cmd, L!($cmd));
assert_eq!(args, L!($args));
assert_eq!(deco, $deco);
};
}
validate!("echo hello", "echo", "hello", StatementDecoration::None);
validate!(
"command echo hello",
"echo",
"hello",
StatementDecoration::Command
);
validate!(
"exec echo hello",
"echo",
"hello",
StatementDecoration::Exec
);
validate!(
"command command hello",
"command",
"hello",
StatementDecoration::Command
);
validate!(
"builtin command hello",
"command",
"hello",
StatementDecoration::Builtin
);
validate!(
"command --help",
"command",
"--help",
StatementDecoration::None
);
validate!("command -h", "command", "-h", StatementDecoration::None);
validate!("command", "command", "", StatementDecoration::None);
validate!("command -", "command", "-", StatementDecoration::None);
validate!("command --", "command", "--", StatementDecoration::None);
validate!(
"builtin --names",
"builtin",
"--names",
StatementDecoration::None
);
validate!("function", "function", "", StatementDecoration::None);
validate!(
"function --help",
"function",
"--help",
StatementDecoration::None
);
// Verify that 'function -h' and 'function --help' are plain statements but 'function --foo' is
// not (issue #1240).
macro_rules! check_function_help {
($src:expr, $kind:pat) => {
let ast = ast::parse(L!($src), ParseTreeFlags::default(), None);
assert!(!ast.errored());
assert_eq!(
Traversal::new(ast.top())
.filter(|n| matches!(n.kind(), $kind))
.count(),
1
);
};
}
check_function_help!("function -h", ast::Kind::DecoratedStatement(_));
check_function_help!("function --help", ast::Kind::DecoratedStatement(_));
check_function_help!("function --foo; end", ast::Kind::FunctionHeader(_));
check_function_help!("function foo; end", ast::Kind::FunctionHeader(_));
}
#[test]
#[serial]
fn test_new_parser_ad_hoc() {
let _cleanup = test_init();
// Very ad-hoc tests for issues encountered.
// Ensure that 'case' terminates a job list.
let src = L!("switch foo ; case bar; case baz; end");
let ast = ast::parse(src, ParseTreeFlags::default(), None);
assert!(!ast.errored());
// Expect two CaseItems. The bug was that we'd
// try to run a command 'case'.
assert_eq!(
Traversal::new(ast.top())
.filter(|n| matches!(n.kind(), ast::Kind::CaseItem(_)))
.count(),
2
);
// Ensure that naked variable assignments don't hang.
// The bug was that "a=" would produce an error but not be consumed,
// leading to an infinite loop.
// By itself it should produce an error.
let ast = ast::parse(L!("a="), ParseTreeFlags::default(), None);
assert!(ast.errored());
let flags = ParseTreeFlags {
leave_unterminated: true,
..ParseTreeFlags::default()
};
// If we are leaving things unterminated, this should not produce an error.
// i.e. when typing "a=" at the command line, it should be treated as valid
// because we don't want to color it as an error.
let ast = ast::parse(L!("a="), flags, None);
assert!(!ast.errored());
let mut errors = vec![];
ast::parse(L!("begin; echo ("), flags, Some(&mut errors));
assert_eq!(errors.len(), 1);
assert_eq!(
errors[0].code,
ParseErrorCode::TokenizerUnterminatedSubshell
);
errors.clear();
ast::parse(L!("for x in ("), flags, Some(&mut errors));
assert_eq!(errors.len(), 1);
assert_eq!(
errors[0].code,
ParseErrorCode::TokenizerUnterminatedSubshell
);
errors.clear();
ast::parse(L!("begin; echo '"), flags, Some(&mut errors));
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].code, ParseErrorCode::TokenizerUnterminatedQuote);
}
#[test]
#[serial]
fn test_new_parser_errors() {
let _cleanup = test_init();
macro_rules! validate {
($src:expr, $expected_code:expr) => {
let mut errors = vec![];
let ast = ast::parse(L!($src), ParseTreeFlags::default(), Some(&mut errors));
assert!(ast.errored());
assert_eq!(
errors.into_iter().map(|e| e.code).collect::<Vec<_>>(),
vec![$expected_code],
);
};
}
validate!("echo 'abc", ParseErrorCode::TokenizerUnterminatedQuote);
validate!("'", ParseErrorCode::TokenizerUnterminatedQuote);
validate!("echo (abc", ParseErrorCode::TokenizerUnterminatedSubshell);
validate!("end", ParseErrorCode::UnbalancingEnd);
validate!("echo hi ; end", ParseErrorCode::UnbalancingEnd);
validate!("else", ParseErrorCode::UnbalancingElse);
validate!("if true ; end ; else", ParseErrorCode::UnbalancingElse);
validate!("case", ParseErrorCode::UnbalancingCase);
validate!("if true ; case ; end", ParseErrorCode::UnbalancingCase);
validate!("begin ; }", ParseErrorCode::UnbalancingBrace);
validate!("true | and", ParseErrorCode::AndOrInPipeline);
validate!("a=", ParseErrorCode::BareVariableAssignment);
}
#[test]
#[serial]
fn test_eval_recursion_detection() {
let _cleanup = test_init();
// Ensure that we don't crash on infinite self recursion and mutual recursion.
let parser = TestParser::new();
parser.eval(
L!("function recursive ; recursive ; end ; recursive; "),
&IoChain::new(),
);
parser.eval(
L!(concat!(
"function recursive1 ; recursive2 ; end ; ",
"function recursive2 ; recursive1 ; end ; recursive1; ",
)),
&IoChain::new(),
);
}
#[test]
#[serial]
fn test_eval_illegal_exit_code() {
let _cleanup = test_init();
let parser = TestParser::new();
macro_rules! validate {
($cmd:expr, $result:expr) => {
parser.eval($cmd, &IoChain::new());
let exit_status = parser.get_last_status();
assert_eq!(exit_status, parser.get_last_status());
};
}
// We need to be in an empty directory so that none of the wildcards match a file that might be
// in the fish source tree. In particular we need to ensure that "?" doesn't match a file
// named by a single character. See issue #3852.
parser.pushd("test/temp");
validate!(L!("echo -n"), STATUS_CMD_OK.unwrap());
validate!(L!("pwd"), STATUS_CMD_OK.unwrap());
validate!(L!("UNMATCHABLE_WILDCARD*"), STATUS_UNMATCHED_WILDCARD);
validate!(L!("UNMATCHABLE_WILDCARD**"), STATUS_UNMATCHED_WILDCARD);
validate!(L!("?"), STATUS_UNMATCHED_WILDCARD);
validate!(L!("abc?def"), STATUS_UNMATCHED_WILDCARD);
parser.popd();
}
#[test]
#[serial]
fn test_eval_empty_function_name() {
let _cleanup = test_init();
let parser = TestParser::new();
parser.eval(
L!("function '' ; echo fail; exit 42 ; end ; ''"),
&IoChain::new(),
);
}
#[test]
#[serial]
fn test_expand_argument_list() {
let _cleanup = test_init();
let parser = TestParser::new();
let comps: Vec<WString> = Parser::expand_argument_list(
L!("alpha 'beta gamma' delta"),
ExpandFlags::default(),
&parser.context(),
)
.into_iter()
.map(|c| c.completion)
.collect();
assert_eq!(comps, &[L!("alpha"), L!("beta gamma"), L!("delta"),]);
}
fn test_1_cancellation(parser: &Parser, src: &wstr) {
let filler = IoBufferfill::create().unwrap();
let delay = Duration::from_millis(100);
#[allow(clippy::unnecessary_cast)]
let thread = unsafe { libc::pthread_self() } as usize;
std::thread::spawn(move || {
// Wait a while and then SIGINT the main thread.
std::thread::sleep(delay);
unsafe {
libc::pthread_kill(thread as libc::pthread_t, SIGINT);
}
});
let mut io = IoChain::new();
io.push(filler.clone());
let res = 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);
}
#[test]
#[serial]
fn test_cancellation() {
let _cleanup = test_init();
let parser = Parser::new(EnvStack::new(), CancelBehavior::Clear);
let _pop = fake_scoped_reader(&parser);
printf!("Testing Ctrl-C cancellation. If this hangs, that's a bug!\n");
// 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(&parser, L!("echo (while true ; echo blah ; end)"));
// Nasty infinite loop that doesn't actually execute anything.
test_1_cancellation(
&parser,
L!("echo (while true ; end) (while true ; end) (while true ; end)"),
);
test_1_cancellation(&parser, L!("while true ; end"));
test_1_cancellation(&parser, L!("while true ; echo nothing > /dev/null; end"));
test_1_cancellation(&parser, 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();
}
#[test]
fn test_line_counter() {
let src = L!("echo line1; echo still_line_1;\n\necho line3");
let ps = parse_source(src.to_owned(), ParseTreeFlags::default(), None)
.expect("Failed to parse source");
assert!(!ps.ast.errored());
let mut line_counter = ps.line_counter();
// Test line_offset_of_character_at_offset, both forwards and backwards to exercise the cache.
let mut expected = 0;
for (idx, c) in src.chars().enumerate() {
let line_offset = line_counter.line_offset_of_character_at_offset(idx);
assert_eq!(line_offset, expected);
if c == '\n' {
expected += 1;
}
}
for (idx, c) in src.chars().enumerate().rev() {
if c == '\n' {
expected -= 1;
}
let line_offset = line_counter.line_offset_of_character_at_offset(idx);
assert_eq!(line_offset, expected);
}
let pipelines: Vec<_> = ps.ast.walk().filter_map(ast::JobPipeline::cast).collect();
assert_eq!(pipelines.len(), 3);
let src_offsets = [0, 0, 2];
assert_eq!(line_counter.source_offset_of_node(), None);
assert_eq!(line_counter.line_offset_of_node(), None);
for (idx, &node) in pipelines.iter().enumerate() {
line_counter.node = std::ptr::from_ref(node);
assert_eq!(
line_counter.source_offset_of_node(),
Some(node.source_range().start())
);
assert_eq!(line_counter.line_offset_of_node(), Some(src_offsets[idx]));
}
for (idx, &node) in pipelines.iter().enumerate().rev() {
line_counter.node = std::ptr::from_ref(node);
assert_eq!(
line_counter.source_offset_of_node(),
Some(node.source_range().start())
);
assert_eq!(line_counter.line_offset_of_node(), Some(src_offsets[idx]));
}
}
#[test]
fn test_line_counter_empty() {
let mut line_counter = LineCounter::<JobPipeline>::empty();
assert_eq!(line_counter.line_offset_of_character_at_offset(0), 0);
assert_eq!(line_counter.line_offset_of_node(), None);
assert_eq!(line_counter.source_offset_of_node(), None);
}
// Helper for testing a simple ast traversal.
// The ast is always for the command 'true;'.
struct TrueSemiAstTester<'a> {
// The AST we are testing.
ast: &'a Ast,
// Expected parent-child relationships, in the order we expect to encounter them.
parent_child: Box<[(&'a dyn Node, &'a dyn Node)]>,
}
impl<'a> TrueSemiAstTester<'a> {
const TRUE_SEMI: &'static wstr = L!("true;");
fn new(ast: &'a Ast) -> Self {
let job_list: &JobList = ast.top();
let job_conjunction = &job_list[0];
let job_pipeline = &job_conjunction.job;
let variable_assignment_list = &job_pipeline.variables;
let statement = &job_pipeline.statement;
let decorated_statement = statement
.as_decorated_statement()
.expect("Expected decorated_statement");
let command = &decorated_statement.command;
let args_or_redirs = &decorated_statement.args_or_redirs;
let job_continuation = &job_pipeline.continuation;
let job_conjunction_continuation = &job_conjunction.continuations;
let semi_nl = job_conjunction.semi_nl.as_ref().expect("Expected semi_nl");
// Helpful parent-child map, such that the children are in the order that we expect to encounter them
// in the AST.
let parent_child: &[(&'a dyn Node, &'a dyn Node)] = &[
(job_list, job_conjunction),
(job_conjunction, job_pipeline),
(job_pipeline, variable_assignment_list),
(job_pipeline, statement),
(statement, decorated_statement),
(decorated_statement, command),
(decorated_statement, args_or_redirs),
(job_pipeline, job_continuation),
(job_conjunction, job_conjunction_continuation),
(job_conjunction, semi_nl),
];
Self {
ast,
parent_child: Box::from(parent_child),
}
}
// Expected nodes, in-order.
fn expected_nodes(&self) -> Vec<&'a dyn Node> {
let mut expected: Vec<&dyn Node> = vec![self.ast.top()];
expected.extend(self.parent_child.iter().map(|&(_p, c)| c));
expected
}
// Helper function to construct the parent list of a given node, such at the first entry is
// the node itself, and the last entry is the root node.
fn get_parents<'s>(
&'s self,
node: &'a dyn Node,
) -> impl Iterator<Item = &'a dyn Node> + 's {
let mut next = Some(node);
std::iter::from_fn(move || {
let out = next?;
next = self
.parent_child
.iter()
.find_map(|&(p, c)| is_same_node(c, out).then_some(p));
Some(out)
})
}
}
#[test]
fn test_ast() {
// Light testing of the AST and traversals.
let ast = ast::parse(
TrueSemiAstTester::TRUE_SEMI,
ParseTreeFlags::default(),
None,
);
let tester = TrueSemiAstTester::new(&ast);
// Walk the AST and collect all nodes.
// See is_same_node comments for why we can't use assert_eq! here.
let found = ast.walk().collect::<Vec<_>>();
let expected = tester.expected_nodes();
assert_eq!(found.len(), expected.len());
for idx in 0..found.len() {
assert!(is_same_node(found[idx], expected[idx]));
}
// Walk and check parents.
let mut traversal = ast.walk();
while let Some(node) = traversal.next() {
let expected_parents = tester.get_parents(node).collect::<Vec<_>>();
let found_parents = traversal.parent_nodes().collect::<Vec<_>>();
assert_eq!(found_parents.len(), expected_parents.len());
for idx in 0..found_parents.len() {
assert!(is_same_node(found_parents[idx], expected_parents[idx]));
}
}
// Find the decorated statement.
let decorated_statement = ast
.walk()
.find(|n| matches!(n.kind(), ast::Kind::DecoratedStatement(_)))
.expect("Expected decorated statement");
// Test the skip feature. Don't descend into the decorated_statement.
let expected_skip: Vec<&dyn Node> = expected
.iter()
.copied()
.filter(|&n| {
// Discard nodes who have the decorated_statement as a parent,
// excepting the decorated_statement itself.
tester
.get_parents(n)
.skip(1)
.all(|p| !is_same_node(p, decorated_statement))
})
.collect();
let mut found = vec![];
let mut traversal = ast.walk();
while let Some(node) = traversal.next() {
if is_same_node(node, decorated_statement) {
traversal.skip_children(node);
}
found.push(node);
}
assert_eq!(found.len(), expected_skip.len());
for idx in 0..found.len() {
assert!(is_same_node(found[idx], expected_skip[idx]));
}
}
#[test]
#[should_panic]
fn test_traversal_skip_children_panics() {
// Test that we panic if we try to skip children of a node that is not the current node.
let ast = ast::parse(L!("true;"), ParseTreeFlags::default(), None);
let mut traversal = ast.walk();
while let Some(node) = traversal.next() {
if matches!(node.kind(), ast::Kind::DecoratedStatement(_)) {
// Should panic as we can only skip the current node.
traversal.skip_children(ast.top());
}
}
}
#[test]
#[should_panic]
fn test_traversal_parent_panics() {
// Can only get the parent of nodes still on the stack.
let ast = ast::parse(L!("true;"), ParseTreeFlags::default(), None);
let mut traversal = ast.walk();
let mut decorated_statement = None;
while let Some(node) = traversal.next() {
if let Kind::DecoratedStatement(_) = node.kind() {
decorated_statement = Some(node);
} else if node.as_token().map(|t| t.token_type()) == Some(ParseTokenType::End) {
// should panic as the decorated_statement is not on the stack.
let _ = traversal.parent(decorated_statement.unwrap());
}
}
}
}