use crate::{
ast::{self, Node},
builtins::shared::STATUS_ILLEGAL_CMD,
common::{CancelChecker, PROFILING_ACTIVE},
complete::CompletionList,
env::{
EnvMode, EnvSetMode, EnvStack, EnvStackSetResult, Environment, Statuses,
FISH_TERMINAL_COLOR_THEME_VAR,
},
event::{self, Event},
expand::{expand_string, replace_home_directory_with_tilde, ExpandFlags, ExpandResultCode},
fds::{open_dir, BEST_O_SEARCH},
flog, flogf, function,
global_safety::RelaxedAtomicBool,
input_common::TerminalQuery,
io::IoChain,
job_group::MaybeJobId,
operation_context::{OperationContext, EXPANSION_LIMIT_DEFAULT},
parse_constants::{
ParseError, ParseErrorList, ParseTreeFlags, FISH_MAX_EVAL_DEPTH, FISH_MAX_STACK_DEPTH,
SOURCE_LOCATION_UNKNOWN,
},
parse_execution::{EndExecutionReason, ExecutionContext},
parse_tree::{parse_source, NodeRef, ParsedSourceRef, SourceLineCache},
portable_atomic::AtomicU64,
prelude::*,
proc::{job_reap, JobGroupRef, JobList, JobRef, Pid, ProcStatus},
signal::{signal_check_cancel, signal_clear_cancel, Signal},
wait_handle::WaitHandleStore,
wutil::perror_nix,
};
use assert_matches::assert_matches;
use fish_common::{
escape_string, EscapeFlags, EscapeStringStyle, FilenameRef, ScopeGuarding, ScopedCell,
ScopedRefCell,
};
use fish_util::get_time;
use fish_widestring::{wcs2bytes, WExt as _};
use libc::c_int;
use std::cell::{Ref, RefCell, RefMut};
use std::ffi::OsStr;
use std::fs::File;
use std::io::Write as _;
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: WString,
args: Vec<WString>,
},
Event(Rc<Event>),
Source {
file: Arc<WString>,
},
}
#[derive(Default)]
pub struct Block {
block_type: BlockType,
pub data: Option<Box<BlockData>>,
pub event_blocks: bool,
pub src_filename: Option<Arc<WString>>,
src_node: Option<NodeRef<ast::JobPipeline>>,
}
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
}
pub fn src_lineno(&self, cache: &mut SourceLineCache) -> Option<NonZeroU32> {
self.src_node.as_ref()?.lineno_with_cache(cache)
}
}
impl Block {
pub fn new(block_type: BlockType) -> Self {
Self {
block_type,
..Default::default()
}
}
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(&mut SourceLineCache::default()) {
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
}
pub fn is_function_call(&self) -> bool {
matches!(self.typ(), BlockType::function_call { .. })
}
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 {
pub duration: Microseconds,
pub level: isize,
pub skipped: bool,
pub cmd: WString,
}
impl ProfileItem {
pub fn new() -> Self {
Default::default()
}
pub fn now() -> Microseconds {
get_time()
}
}
#[derive(Copy, Clone)]
pub struct ScopedData {
pub eval_level: isize,
pub is_subshell: bool,
pub is_event: bool,
pub is_interactive: bool,
pub readonly_commandline: bool,
pub suppress_fish_trace: bool,
pub read_limit: usize,
pub is_cleaning_procs: bool,
pub caller_id: u64, }
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,
}
}
}
#[derive(Default)]
pub struct LibraryData {
pub current_filename: Option<FilenameRef>,
pub transient_commandline: Option<WString>,
pub cwd_fd: Option<Arc<OwnedFd>>,
pub status_vars: StatusVars,
pub exec_count: u64,
pub status_count: u64,
pub last_exec_run_counter: u64,
pub complete_recursion_level: u32,
pub within_fish_init: bool,
pub is_repaint: bool,
pub builtin_complete_current_commandline: bool,
pub loop_status: LoopStatus,
pub returning: bool,
pub exit_current_script: bool,
}
impl LibraryData {
pub fn new() -> Self {
Self {
last_exec_run_counter: u64::MAX,
..Default::default()
}
}
}
#[derive(Default)]
pub struct StatusVars {
pub command: WString,
pub commandline: WString,
}
#[derive(Default)]
pub struct EvalRes {
pub status: ProcStatus,
pub break_expand: bool,
pub was_empty: bool,
pub no_status: bool,
}
impl EvalRes {
pub fn new(status: ProcStatus) -> Self {
Self {
status,
..Default::default()
}
}
}
pub enum ParserStatusVar {
current_command,
current_commandline,
count_,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BlockId(usize);
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum CancelBehavior {
#[default]
Return,
Clear,
}
pub struct Parser {
pub interactive_initialized: RelaxedAtomicBool,
current_node: ScopedRefCell<Option<NodeRef<ast::JobPipeline>>>,
job_list: RefCell<JobList>,
wait_handles: RefCell<WaitHandleStore>,
block_list: RefCell<Vec<Block>>,
pub variables: EnvStack,
scoped_data: ScopedCell<ScopedData>,
pub library_data: ScopedRefCell<LibraryData>,
syncs_uvars: RelaxedAtomicBool,
cancel_behavior: CancelBehavior,
profile_items: RefCell<Vec<ProfileItem>>,
pub global_event_blocks: AtomicU64,
pub blocking_query: RefCell<Option<TerminalQuery>>,
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 {
pub fn new(variables: EnvStack, cancel_behavior: CancelBehavior) -> Parser {
let result = Self {
interactive_initialized: RelaxedAtomicBool::new(false),
current_node: ScopedRefCell::new(None),
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(err) => {
perror_nix("Unable to open the current working directory", err);
}
}
result
}
pub fn job_add(&self, job: JobRef) {
assert!(!job.processes().is_empty());
self.jobs_mut().insert(0, job);
}
pub fn is_function(&self) -> bool {
self.blocks_iter_rev()
.take_while(|b| b.typ() != BlockType::source)
.any(|b| b.is_function_call())
}
pub fn is_command_substitution(&self) -> bool {
self.blocks_iter_rev()
.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)
}
pub fn eval_with(
&self,
cmd: &wstr,
io: &IoChain,
job_group: Option<&JobGroupRef>,
block_type: BlockType,
test_only_suppress_stderr: bool,
) -> EvalRes {
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,
);
}
let backtrace_and_desc = self.get_backtrace(cmd, &error_list);
if !test_only_suppress_stderr {
eprintf!("%s\n", backtrace_and_desc);
}
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()
}
}
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() {
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);
}
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
}
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 {
assert_matches!(
block_type,
BlockType::top | BlockType::subst,
"Invalid block type"
);
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)));
}
}
let jg = job_group.cloned();
let check_cancel_signal = move || {
let sig = signal_check_cancel();
if sig != 0 {
return Some(Signal::new(sig));
}
jg.as_ref().and_then(|jg| jg.get_cancel_signal())
};
if let Some(sig) = check_cancel_signal() {
return EvalRes::new(ProcStatus::from_signal(sig));
}
job_reap(self, false, Some(block_io));
let mut op_ctx = self.context();
let scope_block = self.push_block(Block::scope_block(block_type));
op_ctx.job_group = job_group.cloned();
let cancel_checker: CancelChecker = Box::new(move || check_cancel_signal().is_some());
op_ctx.cancel_checker = cancel_checker;
let restore_current_node = self.current_node.scoped_replace(None);
let mut execution_context = ExecutionContext::new(
node.parsed_source_ref(),
block_io.clone(),
&self.current_node,
test_only_suppress_stderr,
);
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_current_node);
self.pop_block(scope_block);
job_reap(self, false, Some(block_io));
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,
}
}
}
pub fn expand_argument_list(
arg_list_src: &wstr,
flags: ExpandFlags,
ctx: &OperationContext<'_>,
) -> CompletionList {
let ast = ast::parse_argument_list(arg_list_src, ParseTreeFlags::default(), None);
if ast.errored() {
return vec![];
}
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; }
}
result
}
pub fn current_line(&self) -> WString {
let Some(node_ref) = self.current_node.borrow().clone() else {
return WString::new();
};
let Some(source_offset) = node_ref.source_offset() else {
return WString::new();
};
let lineno = self.get_lineno_for_display();
let file = self.current_filename();
let mut prefix = WString::new();
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();
let empty_error = ParseError {
source_start: source_offset,
..Default::default()
};
let mut line_info = empty_error.describe_with_prefix(
node_ref.source_str(),
&prefix,
self.is_interactive(),
skip_caret,
);
if !line_info.is_empty() {
line_info.push('\n');
}
line_info.push_utfstr(&self.stack_trace());
line_info
}
pub fn get_lineno(&self) -> Option<NonZeroU32> {
self.current_node.borrow().as_ref().and_then(|n| n.lineno())
}
pub fn get_lineno_for_display(&self) -> u32 {
self.get_lineno().map_or(0, |n| n.get())
}
pub fn current_node_ref(&self) -> Option<NodeRef<ast::JobPipeline>> {
self.current_node.borrow().clone()
}
pub fn is_block(&self) -> bool {
self.blocks_iter_rev().any(|b| {
![
BlockType::top,
BlockType::subst,
BlockType::variable_assignment,
]
.contains(&b.typ())
})
}
pub fn is_breakpoint(&self) -> bool {
self.blocks_iter_rev()
.any(|b| b.typ() == BlockType::breakpoint)
}
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()?;
Some(Ref::map(Ref::clone(&blocks), |bl| &bl[last]))
})
}
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]))
}
}
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]
}))
}
}
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()
}
pub fn jobs(&self) -> Ref<'_, JobList> {
self.job_list.borrow()
}
pub fn jobs_mut(&self) -> RefMut<'_, JobList> {
self.job_list.borrow_mut()
}
pub fn vars(&self) -> &EnvStack {
&self.variables
}
#[inline(always)]
pub fn scope(&self) -> ScopedData {
self.scoped_data.get()
}
pub fn push_scope<'a, F: FnOnce(&mut ScopedData)>(
&'a self,
modifier: F,
) -> impl ScopeGuarding + 'a {
self.scoped_data.scoped_mod(modifier)
}
pub fn libdata(&self) -> Ref<'_, LibraryData> {
self.library_data.borrow()
}
pub fn libdata_mut(&self) -> RefMut<'_, LibraryData> {
self.library_data.borrow_mut()
}
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()
}
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);
}
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())
}
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)
}
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)
}
pub fn set_empty(&self, key: &wstr, mode: ParserEnvSetMode) -> EnvStackSetResult {
let mode = self.convert_env_set_mode(mode);
self.vars().set_empty(key, mode)
}
pub fn remove_var(&self, key: &wstr, mode: ParserEnvSetMode) -> EnvStackSetResult {
let mode = self.convert_env_set_mode(mode);
self.vars().remove(key, mode)
}
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);
}
}
}
pub fn push_block(&self, mut block: Block) -> BlockId {
block.src_filename = self.current_filename();
block.src_node.clone_from(&self.current_node.borrow());
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)
}
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());
}
}
pub fn get_function_name(&self, level: i32) -> Option<WString> {
if level == 0 {
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()
.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()
}
pub fn job_promote_at(&self, job_pos: usize) {
self.jobs_mut().rotate_left(job_pos);
}
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
}
pub fn job_get_from_pid(&self, pid: Pid) -> Option<JobRef> {
self.job_get_with_index_from_pid(pid).map(|t| t.1)
}
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
}
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()
}
pub fn clear_profiling(&self) {
self.profile_items.borrow_mut().clear();
}
pub fn emit_profiling(&self, path: &OsStr) {
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();
};
let mut which_line = 0;
let mut skip_caret = true;
if err.source_start != SOURCE_LOCATION_UNKNOWN && err.source_start <= src.len() {
which_line = 1 + src[..err.source_start]
.chars()
.filter(|c| *c == '\n')
.count();
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 {
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
}
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())
}
pub fn is_interactive(&self) -> bool {
self.scope().is_interactive
}
pub fn stack_trace(&self) -> WString {
let mut line_cache = SourceLineCache::default();
self.blocks_iter_rev()
.take_while(|b| b.typ() != BlockType::event)
.fold(WString::new(), |mut trace, b| {
append_block_description_to_stack_trace(self, &b, &mut trace, &mut line_cache);
trace
})
}
pub fn function_stack_is_overflowing(&self) -> bool {
if self.scope().eval_level <= FISH_MAX_STACK_DEPTH {
return false;
}
let depth = self
.blocks_iter_rev()
.filter(|b| b.is_function_call())
.count();
depth > (FISH_MAX_STACK_DEPTH as usize)
}
pub fn set_syncs_uvars(&self, flag: bool) {
self.syncs_uvars.store(flag);
}
pub fn context(&self) -> OperationContext<'_> {
OperationContext::foreground(
self,
Box::new(|| signal_check_cancel() != 0),
EXPANSION_LIMIT_DEFAULT,
)
}
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()],
);
}
}
fn user_presentable_path(path: &wstr, vars: &dyn Environment) -> WString {
replace_home_directory_with_tilde(path, vars)
}
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;
let mut self_time = item.duration;
for nested_item in items[idx + 1..].iter() {
if nested_item.skipped {
continue;
}
if nested_item.level <= item.level {
break;
}
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");
}
}
fn append_block_description_to_stack_trace(
parser: &Parser,
b: &Block,
trace: &mut WString,
line_cache: &mut SourceLineCache,
) {
let mut print_source_location = 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));
let mut args_str = WString::new();
for arg in args {
if !args_str.is_empty() {
args_str.push(' ');
}
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() {
trace.push(' ');
trace.push_utfstr(&wgettext_fmt!("with arguments '%s'", args_str));
}
trace.push('\n');
print_source_location = true;
}
BlockType::subst => {
trace.push_utfstr(&wgettext!("in command substitution"));
trace.push('\n');
print_source_location = 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",
&user_presentable_path(source_dest, parser.vars())
));
trace.push('\n');
print_source_location = 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", &description));
trace.push('\n');
print_source_location = true;
}
BlockType::top
| BlockType::begin
| BlockType::switch_block
| BlockType::while_block
| BlockType::for_block
| BlockType::if_block
| BlockType::breakpoint
| BlockType::variable_assignment => {}
}
if print_source_location {
if let Some(file) = b.src_filename.as_ref() {
trace.push_utfstr(&sprintf!(
"\tcalled on line %d of file %s\n",
b.src_lineno(line_cache).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");
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum BlockType {
while_block,
for_block,
if_block,
function_call { shadows: bool },
switch_block,
subst,
#[default]
top,
begin,
source,
event,
breakpoint,
variable_assignment,
}
#[derive(Clone, Copy, Default, Eq, PartialEq)]
pub enum LoopStatus {
#[default]
normals,
breaks,
continues,
}
#[cfg(test)]
mod tests {
use super::{CancelBehavior, Parser};
use crate::{
ast::{self, is_same_node, Ast, JobList, Kind, Node, Traversal},
env::EnvStack,
expand::ExpandFlags,
io::{IoBufferfill, IoChain},
parse_constants::{
ParseErrorCode, ParseIssue, ParseTokenType, ParseTreeFlags, StatementDecoration,
},
parse_util::{detect_errors_in_argument, detect_parse_errors},
prelude::*,
reader::{fake_scoped_reader, reader_reset_interrupted},
signal::{signal_clear_cancel, signal_reset_handlers, signal_set_handlers},
tests::prelude::*,
};
use fish_wcstringutil::join_strings;
use fish_widestring::str2wcstring;
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 )
};
}
fn detect_argument_errors(src: &str) -> Result<(), ParseIssue> {
let src = str2wcstring(src);
let ast = ast::parse_argument_list(&src, ParseTreeFlags::default(), None);
if ast.errored() {
return ParseIssue::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)
}
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"
);
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 | "),
ParseIssue::INCOMPLETE,
"unterminated pipe not reported properly"
);
assert_eq!(
detect_errors!("echo (\nfoo\n bar"),
ParseIssue::INCOMPLETE,
"unterminated multiline subshell not reported properly"
);
assert_eq!(
detect_errors!("begin ; true ; end | "),
ParseIssue::INCOMPLETE,
"unterminated pipe not reported properly"
);
assert_eq!(
detect_errors!(" | true "),
ParseIssue::ERROR,
"leading pipe not reported properly"
);
assert_eq!(
detect_errors!("true | # comment"),
ParseIssue::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 "),
ParseIssue::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().error,
"Bad variable expansion not reported as error"
);
assert!(
detect_argument_errors("foo$@").unwrap_err().error,
"Bad variable expansion not reported as error"
);
assert!(
detect_argument_errors("foo(cat | or cat)")
.unwrap_err()
.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 && "),
ParseIssue::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"),
ParseIssue::INCOMPLETE,
"unterminated conjunction not reported properly"
);
assert_eq!(
detect_errors!("begin ; echo hi; }"),
ParseIssue::ERROR,
"closing of unopened brace statement not reported properly"
);
assert_eq!(
detect_errors!("begin {"), ParseIssue::INCOMPLETE,
"brace after begin not reported properly"
);
assert_eq!(
detect_errors!("a=b {"), ParseIssue::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!(";"),
];
let mut src = WString::new();
src.reserve(128);
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(' ');
}
(remaining_permutation == 0).then_some(out_str)
}
let max_len = 5;
for len in 0..max_len {
let mut permutation = 0;
while let Some(src) = string_for_permutation(&fuzzes, len, permutation) {
permutation += 1;
ast::parse(&src, ParseTreeFlags::default(), None);
}
}
}
#[test]
#[serial]
fn test_new_parser_ll2() {
let _cleanup = test_init();
fn test_1_parse_ll2(src: &wstr) -> Option<(WString, WString, StatementDecoration)> {
let ast = ast::parse(src, ParseTreeFlags::default(), None);
if ast.errored() {
return None;
}
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");
let out_deco = statement.decoration();
let out_cmd = statement.command.source(src).to_owned();
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
);
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();
let src = L!("switch foo ; case bar; case baz; end");
let ast = ast::parse(src, ParseTreeFlags::default(), None);
assert!(!ast.errored());
assert_eq!(
Traversal::new(ast.top())
.filter(|n| matches!(n.kind(), ast::Kind::CaseItem(_)))
.count(),
2
);
let ast = ast::parse(L!("a="), ParseTreeFlags::default(), None);
assert!(ast.errored());
let flags = ParseTreeFlags {
leave_unterminated: true,
..ParseTreeFlags::default()
};
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();
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());
};
}
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 || {
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");
signal_set_handlers(true);
test_1_cancellation(&parser, L!("echo (while true ; echo blah ; end)"));
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();
reader_reset_interrupted();
signal_clear_cancel();
}
struct TrueSemiAstTester<'a> {
ast: &'a Ast,
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");
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),
}
}
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
}
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() {
let ast = ast::parse(
TrueSemiAstTester::TRUE_SEMI,
ParseTreeFlags::default(),
None,
);
let tester = TrueSemiAstTester::new(&ast);
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]));
}
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]));
}
}
let decorated_statement = ast
.walk()
.find(|n| matches!(n.kind(), ast::Kind::DecoratedStatement(_)))
.expect("Expected decorated statement");
let expected_skip: Vec<&dyn Node> = expected
.iter()
.copied()
.filter(|&n| {
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() {
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(_)) {
traversal.skip_children(ast.top());
}
}
}
#[test]
#[should_panic]
fn test_traversal_parent_panics() {
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) {
let _ = traversal.parent(decorated_statement.unwrap());
}
}
}
}