mirror of
https://github.com/fish-shell/fish-shell.git
synced 2024-12-19 05:13:44 +08:00
1290 lines
44 KiB
Rust
1290 lines
44 KiB
Rust
// The fish parser. Contains functions for parsing and evaluating code.
|
||
|
||
use crate::ast::{self, Ast, List, Node};
|
||
use crate::builtins::shared::STATUS_ILLEGAL_CMD;
|
||
use crate::common::{
|
||
escape_string, scoped_push_replacer, CancelChecker, EscapeFlags, EscapeStringStyle,
|
||
FilenameRef, ScopeGuarding, PROFILING_ACTIVE,
|
||
};
|
||
use crate::complete::CompletionList;
|
||
use crate::env::{EnvMode, EnvStack, EnvStackSetResult, Environment, Statuses};
|
||
use crate::event::{self, Event};
|
||
use crate::expand::{
|
||
expand_string, replace_home_directory_with_tilde, ExpandFlags, ExpandResultCode,
|
||
};
|
||
use crate::fds::{open_dir, BEST_O_SEARCH};
|
||
use crate::global_safety::RelaxedAtomicBool;
|
||
use crate::input_common::terminal_protocols_disable_ifn;
|
||
use crate::io::IoChain;
|
||
use crate::job_group::MaybeJobId;
|
||
use crate::operation_context::{OperationContext, EXPANSION_LIMIT_DEFAULT};
|
||
use crate::parse_constants::{
|
||
ParseError, ParseErrorList, ParseTreeFlags, FISH_MAX_EVAL_DEPTH, FISH_MAX_STACK_DEPTH,
|
||
SOURCE_LOCATION_UNKNOWN,
|
||
};
|
||
use crate::parse_execution::{EndExecutionReason, ExecutionContext};
|
||
use crate::parse_tree::{parse_source, LineCounter, ParsedSourceRef};
|
||
use crate::proc::{job_reap, JobGroupRef, JobList, JobRef, Pid, ProcStatus};
|
||
use crate::signal::{signal_check_cancel, signal_clear_cancel, Signal};
|
||
use crate::threads::assert_is_main_thread;
|
||
use crate::util::get_time;
|
||
use crate::wait_handle::WaitHandleStore;
|
||
use crate::wchar::{wstr, WString, L};
|
||
use crate::wutil::{perror, wgettext, wgettext_fmt};
|
||
use crate::{function, FLOG};
|
||
use libc::c_int;
|
||
#[cfg(not(target_has_atomic = "64"))]
|
||
use portable_atomic::AtomicU64;
|
||
use std::cell::{Ref, RefCell, RefMut};
|
||
use std::ffi::{CStr, OsStr};
|
||
use std::num::NonZeroU32;
|
||
use std::os::fd::{AsRawFd, OwnedFd, RawFd};
|
||
use std::rc::Rc;
|
||
#[cfg(target_has_atomic = "64")]
|
||
use std::sync::atomic::AtomicU64;
|
||
use std::sync::{
|
||
atomic::{AtomicIsize, Ordering},
|
||
Arc,
|
||
};
|
||
|
||
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 Default for BlockType {
|
||
fn default() -> Self {
|
||
BlockType::top
|
||
}
|
||
}
|
||
|
||
impl Block {
|
||
/// Construct from a block type.
|
||
pub fn new(block_type: BlockType) -> Self {
|
||
Self {
|
||
block_type,
|
||
..Default::default()
|
||
}
|
||
}
|
||
|
||
/// Description of the block, for debugging.
|
||
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 %ls)", src_filename));
|
||
}
|
||
result
|
||
}
|
||
|
||
pub fn typ(&self) -> BlockType {
|
||
self.block_type
|
||
}
|
||
|
||
/// Return if we are a function call (with or without shadowing).
|
||
pub fn is_function_call(&self) -> bool {
|
||
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()
|
||
}
|
||
}
|
||
|
||
/// 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 stack of fake values to be returned by builtin_commandline. This is used by the completion
|
||
/// machinery when wrapping: e.g. if `tig` wraps `git` then git completions need to see git on
|
||
/// the command line.
|
||
pub transient_commandlines: Vec<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>>,
|
||
|
||
pub status_vars: StatusVars,
|
||
|
||
/// A counter incremented every time a command executes.
|
||
pub exec_count: u64,
|
||
|
||
/// A counter incremented every time an external command executes.
|
||
pub exec_external_count: u64,
|
||
|
||
/// A counter incremented every time a command produces a $status.
|
||
pub status_count: u64,
|
||
|
||
/// Last reader run count.
|
||
pub last_exec_run_counter: u64,
|
||
|
||
/// Number of recursive calls to the internal completion function.
|
||
pub complete_recursion_level: u32,
|
||
|
||
/// If set, we are currently within fish's initialization routines.
|
||
pub within_fish_init: bool,
|
||
|
||
/// If we're currently repainting the commandline.
|
||
/// Useful to stop infinite loops.
|
||
pub is_repaint: bool,
|
||
|
||
/// Whether we called builtin_complete -C without parameter.
|
||
pub builtin_complete_current_commandline: bool,
|
||
|
||
/// Whether we are currently cleaning processes.
|
||
pub is_cleaning_procs: bool,
|
||
|
||
/// The internal job id of the job being populated, or 0 if none.
|
||
/// This supports the '--on-job-exit caller' feature.
|
||
pub caller_id: u64, // TODO should be InternalJobId
|
||
|
||
/// Whether we are running a subshell command.
|
||
pub is_subshell: bool,
|
||
|
||
/// Whether we are running an event handler. This is not a bool because we keep count of the
|
||
/// event nesting level.
|
||
pub is_event: i32,
|
||
|
||
/// Whether we are currently interactive.
|
||
pub is_interactive: bool,
|
||
|
||
/// Whether to suppress fish_trace output. This occurs in the prompt, event handlers, and key
|
||
/// bindings.
|
||
pub suppress_fish_trace: bool,
|
||
|
||
/// Whether we should break or continue the current loop.
|
||
/// This is set by the 'break' and 'continue' commands.
|
||
pub loop_status: LoopStatus,
|
||
|
||
/// Whether we should return from the current function.
|
||
/// This is set by the 'return' command.
|
||
pub returning: bool,
|
||
|
||
/// Whether we should stop executing.
|
||
/// This is set by the 'exit' command, and unset after 'reader_read'.
|
||
/// Note this only exits up to the "current script boundary." That is, a call to exit within a
|
||
/// 'source' or 'read' command will only exit up to that command.
|
||
pub exit_current_script: bool,
|
||
|
||
/// The read limit to apply to captured subshell output, or 0 for none.
|
||
pub read_limit: usize,
|
||
}
|
||
|
||
impl LibraryData {
|
||
pub fn new() -> Self {
|
||
Self {
|
||
last_exec_run_counter: u64::MAX,
|
||
..Default::default()
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Default for LoopStatus {
|
||
fn default() -> Self {
|
||
LoopStatus::normals
|
||
}
|
||
}
|
||
|
||
/// Status variables set by the main thread as jobs are parsed and read by various consumers.
|
||
#[derive(Default)]
|
||
pub struct StatusVars {
|
||
/// Used to get the head of the current job (not the current command, at least for now)
|
||
/// for `status current-command`.
|
||
pub command: WString,
|
||
/// Used to get the full text of the current job for `status current-commandline`.
|
||
pub commandline: WString,
|
||
}
|
||
|
||
/// The result of Parser::eval family.
|
||
#[derive(Default)]
|
||
pub struct EvalRes {
|
||
/// The value for $status.
|
||
pub status: ProcStatus,
|
||
|
||
/// If set, there was an error that should be considered a failed expansion, such as
|
||
/// command-not-found. For example, `touch (not-a-command)` will not invoke 'touch' because
|
||
/// command-not-found will mark break_expand.
|
||
pub break_expand: bool,
|
||
|
||
/// If set, no commands were executed and there we no errors.
|
||
pub was_empty: bool,
|
||
|
||
/// If set, no commands produced a $status value.
|
||
pub no_status: bool,
|
||
}
|
||
|
||
impl EvalRes {
|
||
pub fn new(status: ProcStatus) -> Self {
|
||
Self {
|
||
status,
|
||
..Default::default()
|
||
}
|
||
}
|
||
}
|
||
|
||
pub enum ParserStatusVar {
|
||
current_command,
|
||
current_commandline,
|
||
count_,
|
||
}
|
||
|
||
pub type BlockId = usize;
|
||
|
||
/// 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 {
|
||
/// 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: Rc<RefCell<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>>,
|
||
|
||
/// The 'depth' of the fish call stack.
|
||
pub eval_level: AtomicIsize,
|
||
|
||
/// Set of variables for the parser.
|
||
pub variables: Rc<EnvStack>,
|
||
|
||
/// Miscellaneous library data.
|
||
library_data: RefCell<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,
|
||
}
|
||
|
||
impl Parser {
|
||
/// Create a parser.
|
||
pub fn new(variables: Rc<EnvStack>, cancel_behavior: CancelBehavior) -> Parser {
|
||
let result = Self {
|
||
line_counter: Rc::new(RefCell::new(LineCounter::empty())),
|
||
job_list: RefCell::default(),
|
||
wait_handles: RefCell::new(WaitHandleStore::new()),
|
||
block_list: RefCell::default(),
|
||
eval_level: AtomicIsize::new(-1),
|
||
variables,
|
||
library_data: RefCell::new(LibraryData::new()),
|
||
syncs_uvars: RelaxedAtomicBool::new(false),
|
||
cancel_behavior,
|
||
profile_items: RefCell::default(),
|
||
global_event_blocks: AtomicU64::new(0),
|
||
};
|
||
|
||
match open_dir(CStr::from_bytes_with_nul(b".\0").unwrap(), 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)
|
||
}
|
||
|
||
/// Assert that this parser is allowed to execute on the current thread.
|
||
pub fn assert_can_execute(&self) {
|
||
assert_is_main_thread();
|
||
}
|
||
|
||
pub fn eval(&self, cmd: &wstr, io: &IoChain) -> EvalRes {
|
||
self.eval_with(cmd, io, None, BlockType::top)
|
||
}
|
||
|
||
/// Evaluate the expressions contained in cmd.
|
||
///
|
||
/// \param cmd the string to evaluate
|
||
/// \param io io redirections to perform on all started jobs
|
||
/// \param job_group if set, the job group to give to spawned jobs.
|
||
/// \param block_type The type of block to push on the block stack, which must be either 'top'
|
||
/// or 'subst'.
|
||
/// Return the result of evaluation.
|
||
pub fn eval_with(
|
||
&self,
|
||
cmd: &wstr,
|
||
io: &IoChain,
|
||
job_group: Option<&JobGroupRef>,
|
||
block_type: BlockType,
|
||
) -> EvalRes {
|
||
// Parse the source into a tree, if we can.
|
||
let mut error_list = ParseErrorList::new();
|
||
if let Some(ps) = parse_source(
|
||
cmd.to_owned(),
|
||
ParseTreeFlags::empty(),
|
||
Some(&mut error_list),
|
||
) {
|
||
return self.eval_parsed_source(&ps, io, job_group, block_type);
|
||
}
|
||
|
||
// Get a backtrace. This includes the message.
|
||
let backtrace_and_desc = self.get_backtrace(cmd, &error_list);
|
||
|
||
// Print it.
|
||
eprintf!("%s\n", backtrace_and_desc);
|
||
|
||
// Set a valid status.
|
||
self.set_last_statuses(Statuses::just(STATUS_ILLEGAL_CMD.unwrap()));
|
||
let break_expand = true;
|
||
EvalRes {
|
||
status: ProcStatus::from_exit_code(STATUS_ILLEGAL_CMD.unwrap()),
|
||
break_expand,
|
||
..Default::default()
|
||
}
|
||
}
|
||
|
||
/// Evaluate the parsed source ps.
|
||
/// Because the source has been parsed, a syntax error is impossible.
|
||
pub fn eval_parsed_source(
|
||
&self,
|
||
ps: &ParsedSourceRef,
|
||
io: &IoChain,
|
||
job_group: Option<&JobGroupRef>,
|
||
block_type: BlockType,
|
||
) -> EvalRes {
|
||
assert!([BlockType::top, BlockType::subst].contains(&block_type));
|
||
let job_list = ps.ast.top().as_job_list().unwrap();
|
||
if !job_list.is_empty() {
|
||
// Execute the top job list.
|
||
self.eval_node(ps, job_list, io, job_group, block_type)
|
||
} else {
|
||
let status = ProcStatus::from_exit_code(self.get_last_status());
|
||
EvalRes {
|
||
status,
|
||
break_expand: false,
|
||
was_empty: true,
|
||
no_status: true,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Evaluates a node.
|
||
/// The node type must be ast::Statement or ast::JobList.
|
||
pub fn eval_node<T: Node>(
|
||
&self,
|
||
ps: &ParsedSourceRef,
|
||
node: &T,
|
||
block_io: &IoChain,
|
||
job_group: Option<&JobGroupRef>,
|
||
block_type: BlockType,
|
||
) -> EvalRes {
|
||
// Only certain blocks are allowed.
|
||
assert!(
|
||
[BlockType::top, BlockType::subst].contains(&block_type),
|
||
"Invalid block type"
|
||
);
|
||
|
||
// If fish itself got a cancel signal, then we want to unwind back to the 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); // 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 line_counter = Rc::clone(&self.line_counter);
|
||
let scoped_line_counter =
|
||
scoped_push_replacer(|v| line_counter.replace(v), ps.line_counter());
|
||
|
||
// Create a new execution context.
|
||
let mut execution_context =
|
||
ExecutionContext::new(ps.clone(), block_io.clone(), Rc::clone(&line_counter));
|
||
|
||
terminal_protocols_disable_ifn();
|
||
|
||
// 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(scoped_line_counter);
|
||
self.pop_block(scope_block);
|
||
|
||
job_reap(self, false); // reap again
|
||
|
||
let sig = signal_check_cancel();
|
||
if sig != 0 {
|
||
EvalRes::new(ProcStatus::from_signal(Signal::new(sig)))
|
||
} else {
|
||
let status = ProcStatus::from_exit_code(self.get_last_status());
|
||
let break_expand = reason == EndExecutionReason::error;
|
||
EvalRes {
|
||
status,
|
||
break_expand,
|
||
was_empty: !break_expand && prev_exec_count == new_exec_count,
|
||
no_status: prev_status_count == new_status_count,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Evaluate line as a list of parameters, i.e. tokenize it and perform parameter expansion and
|
||
/// cmdsubst execution on the tokens. Errors are ignored. If a parser is provided, it is used
|
||
/// for command substitution expansion.
|
||
pub fn expand_argument_list(
|
||
arg_list_src: &wstr,
|
||
flags: ExpandFlags,
|
||
ctx: &OperationContext<'_>,
|
||
) -> CompletionList {
|
||
// Parse the string as an argument list.
|
||
let ast = Ast::parse_argument_list(arg_list_src, ParseTreeFlags::default(), None);
|
||
if ast.errored() {
|
||
// Failed to parse. Here we expect to have reported any errors in test_args.
|
||
return vec![];
|
||
}
|
||
|
||
// Get the root argument list and extract arguments from it.
|
||
let mut result = vec![];
|
||
let list = ast.top().as_freestanding_argument_list().unwrap();
|
||
for arg in &list.arguments {
|
||
let arg_src = arg.source(arg_list_src);
|
||
if 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!(
|
||
"%ls (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 mut empty_error = ParseError::default();
|
||
empty_error.source_start = source_offset;
|
||
|
||
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(|val| val.get()).unwrap_or(0)
|
||
}
|
||
|
||
/// 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 the list of blocks. The first block is at the top.
|
||
/// todo!("this RAII object should only be used for iterating over it (in reverse). Maybe enforce this")
|
||
pub fn blocks(&self) -> Ref<'_, Vec<Block>> {
|
||
self.block_list.borrow()
|
||
}
|
||
pub fn block_at_index(&self, index: usize) -> Option<Ref<'_, Block>> {
|
||
let block_list = self.blocks();
|
||
if index >= block_list.len() {
|
||
None
|
||
} else {
|
||
Some(Ref::map(block_list, |bl| &bl[bl.len() - 1 - index]))
|
||
}
|
||
}
|
||
pub fn block_at_index_mut(&self, index: usize) -> Option<RefMut<'_, Block>> {
|
||
let block_list = self.block_list.borrow_mut();
|
||
if index >= block_list.len() {
|
||
None
|
||
} else {
|
||
Some(RefMut::map(block_list, |bl| {
|
||
let len = bl.len();
|
||
&mut bl[len - 1 - index]
|
||
}))
|
||
}
|
||
}
|
||
|
||
pub fn blocks_size(&self) -> usize {
|
||
self.block_list.borrow().len()
|
||
}
|
||
|
||
/// Get the list of jobs.
|
||
pub fn jobs(&self) -> Ref<'_, JobList> {
|
||
self.job_list.borrow()
|
||
}
|
||
pub fn jobs_mut(&self) -> RefMut<'_, JobList> {
|
||
self.job_list.borrow_mut()
|
||
}
|
||
|
||
/// Get the variables.
|
||
pub fn vars(&self) -> &EnvStack {
|
||
&self.variables
|
||
}
|
||
|
||
/// Get the variables as an Arc.
|
||
pub fn vars_ref(&self) -> Rc<EnvStack> {
|
||
Rc::clone(&self.variables)
|
||
}
|
||
|
||
/// Get the library data.
|
||
pub fn libdata(&self) -> Ref<'_, LibraryData> {
|
||
self.library_data.borrow()
|
||
}
|
||
pub fn libdata_mut(&self) -> RefMut<'_, LibraryData> {
|
||
self.library_data.borrow_mut()
|
||
}
|
||
|
||
/// Get our wait handle store.
|
||
pub fn get_wait_handles(&self) -> Ref<'_, WaitHandleStore> {
|
||
self.wait_handles.borrow()
|
||
}
|
||
pub fn mut_wait_handles(&self) -> RefMut<'_, WaitHandleStore> {
|
||
self.wait_handles.borrow_mut()
|
||
}
|
||
|
||
/// Get and set the last proc statuses.
|
||
pub fn get_last_status(&self) -> c_int {
|
||
self.vars().get_last_status()
|
||
}
|
||
pub fn get_last_statuses(&self) -> Statuses {
|
||
self.vars().get_last_statuses()
|
||
}
|
||
pub fn set_last_statuses(&self, s: Statuses) {
|
||
self.vars().set_last_statuses(s)
|
||
}
|
||
|
||
/// Cover of vars().set(), which also fires any returned event handlers.
|
||
pub fn set_var_and_fire(
|
||
&self,
|
||
key: &wstr,
|
||
mode: EnvMode,
|
||
vals: Vec<WString>,
|
||
) -> EnvStackSetResult {
|
||
let res = self.vars().set(key, mode, vals);
|
||
if res == EnvStackSetResult::Ok {
|
||
event::fire(self, Event::variable_set(key.to_owned()));
|
||
}
|
||
res
|
||
}
|
||
|
||
/// Cover of vars().set(), without firing events
|
||
pub fn set_var(&self, key: &wstr, mode: EnvMode, vals: Vec<WString>) -> EnvStackSetResult {
|
||
self.vars().set(key, mode, vals)
|
||
}
|
||
|
||
/// 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);
|
||
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);
|
||
block_list.len() - 1
|
||
}
|
||
|
||
/// Remove the outermost block, asserting it's the given one.
|
||
pub fn pop_block(&self, expected: BlockId) {
|
||
let block = {
|
||
let mut block_list = self.block_list.borrow_mut();
|
||
assert!(expected == block_list.len() - 1);
|
||
block_list.pop().unwrap()
|
||
};
|
||
if block.wants_pop_env() {
|
||
self.vars().pop();
|
||
}
|
||
}
|
||
|
||
/// Return the function name for the specified stack frame. Default is one (current frame).
|
||
pub fn get_function_name(&self, level: i32) -> Option<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.
|
||
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.load().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 nullptr.
|
||
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 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;
|
||
}
|
||
};
|
||
fprintf!(f.as_raw_fd(), "Time\tSum\tCommand\n");
|
||
print_profile(&self.profile_items.borrow(), f.as_raw_fd());
|
||
}
|
||
|
||
pub fn get_backtrace(&self, src: &wstr, errors: &ParseErrorList) -> WString {
|
||
let Some(err) = errors.first() else {
|
||
return WString::new();
|
||
};
|
||
|
||
// Determine if we want to try to print a caret to point at the source error. The
|
||
// err.source_start() <= src.size() check is due to the nasty way that slices work, which is
|
||
// by rewriting the source.
|
||
let mut which_line = 0;
|
||
let mut skip_caret = true;
|
||
if err.source_start != SOURCE_LOCATION_UNKNOWN && err.source_start <= src.len() {
|
||
// Determine which line we're on.
|
||
which_line = 1 + src[..err.source_start]
|
||
.chars()
|
||
.filter(|c| *c == '\n')
|
||
.count();
|
||
|
||
// Don't include the caret if we're interactive, this is the first line of text, and our
|
||
// source is at its beginning, because then it's obvious.
|
||
skip_caret = self.is_interactive() && which_line == 1 && err.source_start == 0;
|
||
}
|
||
|
||
let prefix = if let Some(filename) = self.current_filename() {
|
||
if which_line > 0 {
|
||
wgettext_fmt!(
|
||
"%ls (line %lu): ",
|
||
user_presentable_path(&filename, self.vars()),
|
||
which_line
|
||
)
|
||
} else {
|
||
sprintf!("%ls: ", user_presentable_path(&filename, self.vars()))
|
||
}
|
||
} else {
|
||
L!("fish: ").to_owned()
|
||
};
|
||
|
||
let mut output = err.describe_with_prefix(src, &prefix, self.is_interactive(), skip_caret);
|
||
if !output.is_empty() {
|
||
output.push('\n');
|
||
}
|
||
output.push_utfstr(&self.stack_trace());
|
||
output
|
||
}
|
||
|
||
/// Returns the file currently evaluated by the parser. This can be different than
|
||
/// reader_current_filename, e.g. if we are evaluating a function defined in a different file
|
||
/// than the one currently read.
|
||
pub fn current_filename(&self) -> Option<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.libdata().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.eval_level.load(Ordering::Relaxed) <= isize::try_from(FISH_MAX_STACK_DEPTH).unwrap()
|
||
{
|
||
return false;
|
||
}
|
||
// Count the functions.
|
||
let depth = self
|
||
.blocks()
|
||
.iter()
|
||
.rev()
|
||
.filter(|b| b.is_function_call())
|
||
.count();
|
||
depth > FISH_MAX_STACK_DEPTH
|
||
}
|
||
|
||
/// Mark whether we should sync universal variables.
|
||
pub fn set_syncs_uvars(&self, flag: bool) {
|
||
self.syncs_uvars.store(flag);
|
||
}
|
||
|
||
/// Return 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.eval_level.load(Ordering::Relaxed) >= isize::try_from(FISH_MAX_EVAL_DEPTH).unwrap()
|
||
}
|
||
}
|
||
|
||
// Given a file path, return something nicer. Currently we just "unexpand" tildes.
|
||
fn user_presentable_path(path: &wstr, vars: &dyn Environment) -> WString {
|
||
replace_home_directory_with_tilde(path, vars)
|
||
}
|
||
|
||
/// Print profiling information to the specified stream.
|
||
fn print_profile(items: &[ProfileItem], out: RawFd) {
|
||
for (idx, item) in items.iter().enumerate() {
|
||
if item.skipped || item.cmd.is_empty() {
|
||
continue;
|
||
}
|
||
|
||
let total_time = item.duration;
|
||
|
||
// Compute the self time as the total time, minus the total time consumed by subsequent
|
||
// items exactly one eval level deeper.
|
||
let mut self_time = item.duration;
|
||
for nested_item in items[idx + 1..].iter() {
|
||
if nested_item.skipped {
|
||
continue;
|
||
}
|
||
|
||
// If the eval level is not larger, then we have exhausted nested items.
|
||
if nested_item.level <= item.level {
|
||
break;
|
||
}
|
||
|
||
// If the eval level is exactly one more than our level, it is a directly nested item.
|
||
if nested_item.level == item.level + 1 {
|
||
self_time -= nested_item.duration;
|
||
}
|
||
}
|
||
|
||
fprintf!(out, "%lld\t%lld\t", self_time, total_time);
|
||
for _i in 0..item.level {
|
||
fprintf!(out, "-");
|
||
}
|
||
|
||
fprintf!(out, "> %ls\n", item.cmd);
|
||
}
|
||
}
|
||
|
||
/// 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 '%ls'", 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 '%ls'", args_str));
|
||
}
|
||
trace.push('\n');
|
||
print_call_site = true;
|
||
}
|
||
BlockType::subst => {
|
||
trace.push_utfstr(&wgettext!("in command substitution\n"));
|
||
print_call_site = true;
|
||
}
|
||
BlockType::source => {
|
||
let Some(BlockData::Source { file, .. }) = b.data() else {
|
||
unreachable!()
|
||
};
|
||
let source_dest = file;
|
||
trace.push_utfstr(&wgettext_fmt!(
|
||
"from sourcing file %ls\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: %ls\n", &description));
|
||
print_call_site = true;
|
||
}
|
||
BlockType::top
|
||
| BlockType::begin
|
||
| BlockType::switch_block
|
||
| BlockType::while_block
|
||
| BlockType::for_block
|
||
| BlockType::if_block
|
||
| BlockType::breakpoint
|
||
| BlockType::variable_assignment => {}
|
||
}
|
||
|
||
if print_call_site {
|
||
// Print where the function is called.
|
||
if let Some(file) = b.src_filename.as_ref() {
|
||
trace.push_utfstr(&sprintf!(
|
||
"\tcalled on line %d of file %ls\n",
|
||
b.src_lineno.map(|n| n.get()).unwrap_or(0),
|
||
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, 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
|
||
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, Eq, PartialEq)]
|
||
pub enum LoopStatus {
|
||
/// current loop block executed as normal
|
||
normals,
|
||
/// current loop block should be removed
|
||
breaks,
|
||
/// current loop block should be skipped
|
||
continues,
|
||
}
|