mirror of
https://github.com/fish-shell/fish-shell.git
synced 2024-11-22 12:41:08 +08:00
cd541575b4
Completion on ": {*," used to work but nowadays our attempt to wildcard-expand it fails with a syntax error and we do nothing. This behavior probably only makes sense for the overflow case, so do that.
2028 lines
76 KiB
Rust
2028 lines
76 KiB
Rust
//! Provides the "linkage" between an ast and actual execution structures (job_t, etc.).
|
|
|
|
use crate::ast::{
|
|
self, BlockStatementHeaderVariant, Keyword, Leaf, List, Node, StatementVariant, Token,
|
|
};
|
|
use crate::builtins;
|
|
use crate::builtins::shared::{
|
|
builtin_exists, BUILTIN_ERR_VARNAME, STATUS_CMD_ERROR, STATUS_CMD_OK, STATUS_CMD_UNKNOWN,
|
|
STATUS_EXPAND_ERROR, STATUS_ILLEGAL_CMD, STATUS_INVALID_ARGS, STATUS_NOT_EXECUTABLE,
|
|
STATUS_UNMATCHED_WILDCARD,
|
|
};
|
|
use crate::common::{
|
|
escape, scoped_push_replacer, should_suppress_stderr_for_tests, truncate_at_nul,
|
|
valid_var_name, ScopeGuard, ScopeGuarding,
|
|
};
|
|
use crate::complete::CompletionList;
|
|
use crate::env::{EnvMode, EnvStackSetResult, EnvVar, EnvVarFlags, Environment, Statuses};
|
|
use crate::event::{self, Event};
|
|
use crate::exec::exec_job;
|
|
use crate::expand::{
|
|
expand_one, expand_string, expand_to_command_and_args, ExpandFlags, ExpandResultCode,
|
|
};
|
|
use crate::flog::FLOG;
|
|
use crate::function;
|
|
use crate::io::{IoChain, IoStreams, OutputStream, StringOutputStream};
|
|
use crate::job_group::JobGroup;
|
|
use crate::operation_context::OperationContext;
|
|
use crate::parse_constants::{
|
|
parse_error_offset_source_start, ParseError, ParseErrorCode, ParseErrorList, ParseKeyword,
|
|
ParseTokenType, StatementDecoration, CALL_STACK_LIMIT_EXCEEDED_ERR_MSG,
|
|
ERROR_NO_BRACE_GROUPING, ERROR_TIME_BACKGROUND, FAILED_EXPANSION_VARIABLE_NAME_ERR_MSG,
|
|
ILLEGAL_FD_ERR_MSG, INFINITE_FUNC_RECURSION_ERR_MSG, WILDCARD_ERR_MSG,
|
|
};
|
|
use crate::parse_tree::{LineCounter, NodeRef, ParsedSourceRef};
|
|
use crate::parse_util::parse_util_unescape_wildcards;
|
|
use crate::parser::{Block, BlockData, BlockId, BlockType, LoopStatus, Parser, ProfileItem};
|
|
use crate::parser_keywords::parser_keywords_is_subcommand;
|
|
use crate::path::{path_as_implicit_cd, path_try_get_path};
|
|
use crate::proc::{
|
|
get_job_control_mode, job_reap, no_exec, ConcreteAssignment, Job, JobControl, JobProperties,
|
|
JobRef, Process, ProcessList, ProcessType,
|
|
};
|
|
use crate::reader::fish_is_unwinding_for_exit;
|
|
use crate::redirection::{RedirectionMode, RedirectionSpec, RedirectionSpecList};
|
|
use crate::signal::Signal;
|
|
use crate::timer::push_timer;
|
|
use crate::tokenizer::{variable_assignment_equals_pos, PipeOrRedir};
|
|
use crate::trace::{trace_if_enabled, trace_if_enabled_with_args};
|
|
use crate::wchar::{wstr, WString, L};
|
|
use crate::wchar_ext::WExt;
|
|
use crate::wildcard::wildcard_match;
|
|
use crate::wutil::{wgettext, wgettext_maybe_fmt};
|
|
use libc::{c_int, ENOTDIR, EXIT_SUCCESS, STDERR_FILENO, STDOUT_FILENO};
|
|
use std::cell::RefCell;
|
|
use std::io::ErrorKind;
|
|
use std::rc::Rc;
|
|
use std::sync::{atomic::Ordering, Arc};
|
|
|
|
/// An eval_result represents evaluation errors including wildcards which failed to match, syntax
|
|
/// errors, or other expansion errors. It also tracks when evaluation was skipped due to signal
|
|
/// cancellation. Note it does not track the exit status of commands.
|
|
#[derive(Eq, PartialEq)]
|
|
pub enum EndExecutionReason {
|
|
/// Evaluation was successfull.
|
|
ok,
|
|
|
|
/// Evaluation was skipped due to control flow (break or return).
|
|
control_flow,
|
|
|
|
/// Evaluation was cancelled, e.g. because of a signal or exit.
|
|
cancelled,
|
|
|
|
/// A parse error or failed expansion (but not an error exit status from a command).
|
|
error,
|
|
}
|
|
|
|
pub struct ExecutionContext {
|
|
// The parsed source and its AST.
|
|
pstree: ParsedSourceRef,
|
|
|
|
// If set, one of our processes received a cancellation signal (INT or QUIT) so we are
|
|
// unwinding.
|
|
cancel_signal: Option<Signal>,
|
|
|
|
// Helper to count lines.
|
|
// This is shared with the Parser so that the Parser can access the current line.
|
|
line_counter: Rc<RefCell<LineCounter<ast::JobPipeline>>>,
|
|
|
|
/// The block IO chain.
|
|
/// For example, in `begin; foo ; end < file.txt` this would have the 'file.txt' IO.
|
|
block_io: IoChain,
|
|
}
|
|
|
|
// Report an error, setting $status to `status`. Always returns
|
|
// 'end_execution_reason_t::error'.
|
|
macro_rules! report_error {
|
|
( $self:ident, $ctx:expr, $status:expr, $node:expr, $fmt:expr $(, $arg:expr )* $(,)? ) => {
|
|
report_error_formatted!($self, $ctx, $status, $node, wgettext_maybe_fmt!($fmt $(, $arg )*))
|
|
};
|
|
}
|
|
macro_rules! report_error_formatted {
|
|
( $self:ident, $ctx:expr, $status:expr, $node:expr, $text:expr $(,)? ) => {{
|
|
let r = $node.source_range();
|
|
// Create an error.
|
|
let mut error = ParseError::default();
|
|
error.source_start = r.start();
|
|
error.source_length = r.length();
|
|
error.code = ParseErrorCode::syntax; // hackish
|
|
error.text = $text;
|
|
$self.report_errors($ctx, $status, &vec![error])
|
|
}};
|
|
}
|
|
|
|
impl<'a> ExecutionContext {
|
|
/// Construct a context in preparation for evaluating a node in a tree, with the given block_io.
|
|
/// The execution context may access the parser and parent job group (if any) through ctx.
|
|
pub fn new(
|
|
pstree: ParsedSourceRef,
|
|
block_io: IoChain,
|
|
line_counter: Rc<RefCell<LineCounter<ast::JobPipeline>>>,
|
|
) -> Self {
|
|
Self {
|
|
pstree,
|
|
cancel_signal: None,
|
|
line_counter,
|
|
block_io,
|
|
}
|
|
}
|
|
|
|
pub fn pstree(&self) -> &ParsedSourceRef {
|
|
&self.pstree
|
|
}
|
|
|
|
pub fn eval_node(
|
|
&mut self,
|
|
ctx: &OperationContext<'_>,
|
|
node: &dyn Node,
|
|
associated_block: Option<BlockId>,
|
|
) -> EndExecutionReason {
|
|
match node.typ() {
|
|
ast::Type::statement => {
|
|
self.eval_statement(ctx, node.as_statement().unwrap(), associated_block)
|
|
}
|
|
ast::Type::job_list => {
|
|
self.eval_job_list(ctx, node.as_job_list().unwrap(), associated_block.unwrap())
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
/// Start executing at the given node. Returns 0 if there was no error, 1 if there was an
|
|
/// error.
|
|
fn eval_statement(
|
|
&mut self,
|
|
ctx: &OperationContext<'_>,
|
|
statement: &'a ast::Statement,
|
|
associated_block: Option<BlockId>,
|
|
) -> EndExecutionReason {
|
|
// Note we only expect block-style statements here. No not statements.
|
|
let contents = &statement.contents;
|
|
match &**contents {
|
|
StatementVariant::BlockStatement(block) => {
|
|
self.run_block_statement(ctx, block, associated_block)
|
|
}
|
|
StatementVariant::IfStatement(ifstat) => {
|
|
self.run_if_statement(ctx, ifstat, associated_block)
|
|
}
|
|
StatementVariant::SwitchStatement(switchstat) => {
|
|
self.run_switch_statement(ctx, switchstat)
|
|
}
|
|
StatementVariant::DecoratedStatement(_)
|
|
| StatementVariant::NotStatement(_)
|
|
| StatementVariant::None => panic!(),
|
|
}
|
|
}
|
|
|
|
fn eval_job_list(
|
|
&mut self,
|
|
ctx: &OperationContext<'_>,
|
|
job_list: &'a ast::JobList,
|
|
associated_block: BlockId,
|
|
) -> EndExecutionReason {
|
|
// Check for infinite recursion: a function which immediately calls itself..
|
|
let mut func_name = WString::new();
|
|
if let Some(infinite_recursive_node) =
|
|
self.infinite_recursive_statement_in_job_list(ctx, job_list, &mut func_name)
|
|
{
|
|
// We have an infinite recursion.
|
|
return report_error!(
|
|
self,
|
|
ctx,
|
|
STATUS_CMD_ERROR.unwrap(),
|
|
infinite_recursive_node,
|
|
INFINITE_FUNC_RECURSION_ERR_MSG,
|
|
func_name
|
|
);
|
|
}
|
|
|
|
// Check for stack overflow in case of function calls (regular stack overflow) or string
|
|
// substitution blocks, which can be recursively called with eval (issue #9302).
|
|
let block_type = {
|
|
let blocks = ctx.parser().blocks();
|
|
blocks.get(associated_block).unwrap().typ()
|
|
};
|
|
if (block_type == BlockType::top && ctx.parser().function_stack_is_overflowing())
|
|
|| (block_type == BlockType::subst && ctx.parser().is_eval_depth_exceeded())
|
|
{
|
|
return report_error!(
|
|
self,
|
|
ctx,
|
|
STATUS_CMD_ERROR.unwrap(),
|
|
job_list,
|
|
CALL_STACK_LIMIT_EXCEEDED_ERR_MSG
|
|
);
|
|
}
|
|
self.run_job_list(ctx, job_list, Some(associated_block))
|
|
}
|
|
|
|
// Check to see if we should end execution.
|
|
// Return the eval result to end with, or none() to continue on.
|
|
// This will never return end_execution_reason_t::ok.
|
|
fn check_end_execution(&self, ctx: &OperationContext<'_>) -> Option<EndExecutionReason> {
|
|
// If one of our jobs ended with SIGINT, we stop execution.
|
|
// Likewise if fish itself got a SIGINT, or if something ran exit, etc.
|
|
if self.cancel_signal.is_some() || ctx.check_cancel() || fish_is_unwinding_for_exit() {
|
|
return Some(EndExecutionReason::cancelled);
|
|
}
|
|
let parser = ctx.parser();
|
|
let ld = &parser.libdata();
|
|
if ld.exit_current_script {
|
|
return Some(EndExecutionReason::cancelled);
|
|
}
|
|
if ld.returning {
|
|
return Some(EndExecutionReason::control_flow);
|
|
}
|
|
if ld.loop_status != LoopStatus::normals {
|
|
return Some(EndExecutionReason::control_flow);
|
|
}
|
|
None
|
|
}
|
|
|
|
fn report_errors(
|
|
&self,
|
|
ctx: &OperationContext<'_>,
|
|
status: c_int,
|
|
error_list: &ParseErrorList,
|
|
) -> EndExecutionReason {
|
|
if !ctx.check_cancel() {
|
|
if error_list.is_empty() {
|
|
FLOG!(error, "Error reported but no error text found.");
|
|
}
|
|
|
|
// Get a backtrace.
|
|
let backtrace_and_desc = ctx.parser().get_backtrace(&self.pstree().src, error_list);
|
|
|
|
// Print it.
|
|
if !should_suppress_stderr_for_tests() {
|
|
eprintf!("%s", backtrace_and_desc);
|
|
}
|
|
|
|
// Mark status.
|
|
ctx.parser().set_last_statuses(Statuses::just(status));
|
|
}
|
|
EndExecutionReason::error
|
|
}
|
|
|
|
/// Command not found support.
|
|
fn handle_command_not_found(
|
|
&mut self,
|
|
ctx: &OperationContext<'_>,
|
|
cmd: &wstr,
|
|
statement: &ast::DecoratedStatement,
|
|
err: std::io::Error,
|
|
) -> EndExecutionReason {
|
|
// We couldn't find the specified command. This is a non-fatal error. We want to set the exit
|
|
// status to 127, which is the standard number used by other shells like bash and zsh.
|
|
|
|
if err.kind() != ErrorKind::NotFound {
|
|
// TODO: We currently handle all errors here the same,
|
|
// but this mainly applies to EACCES. We could also feasibly get:
|
|
// ELOOP
|
|
// ENAMETOOLONG
|
|
if err.raw_os_error() == Some(ENOTDIR) {
|
|
// If the original command did not include a "/", assume we found it via $PATH.
|
|
let src = self.node_source(&statement.command);
|
|
if !src.contains('/') {
|
|
return report_error!(
|
|
self,
|
|
ctx,
|
|
STATUS_NOT_EXECUTABLE.unwrap(),
|
|
&statement.command,
|
|
concat!(
|
|
"Unknown command. A component of '%ls' is not a ",
|
|
"directory. Check your $PATH."
|
|
),
|
|
cmd
|
|
);
|
|
} else {
|
|
return report_error!(
|
|
self,
|
|
ctx,
|
|
STATUS_NOT_EXECUTABLE.unwrap(),
|
|
&statement.command,
|
|
"Unknown command. A component of '%ls' is not a directory.",
|
|
cmd
|
|
);
|
|
}
|
|
}
|
|
|
|
return report_error!(
|
|
self,
|
|
ctx,
|
|
STATUS_NOT_EXECUTABLE.unwrap(),
|
|
&statement.command,
|
|
"Unknown command. '%ls' exists but is not an executable file.",
|
|
cmd
|
|
);
|
|
}
|
|
|
|
// Handle unrecognized commands with standard command not found handler that can make better
|
|
// error messages.
|
|
let mut event_args = vec![];
|
|
{
|
|
let args = Self::get_argument_nodes_no_redirs(&statement.args_or_redirs);
|
|
let arg_result =
|
|
self.expand_arguments_from_nodes(ctx, &args, &mut event_args, Globspec::failglob);
|
|
if arg_result != EndExecutionReason::ok {
|
|
return arg_result;
|
|
}
|
|
|
|
event_args.insert(0, cmd.to_owned());
|
|
}
|
|
|
|
let mut error = WString::new();
|
|
|
|
// Redirect to stderr
|
|
let mut io = IoChain::new();
|
|
let mut list = RedirectionSpecList::new();
|
|
list.push(RedirectionSpec::new(
|
|
STDOUT_FILENO,
|
|
RedirectionMode::fd,
|
|
L!("2").to_owned(),
|
|
));
|
|
io.append_from_specs(&list, L!(""));
|
|
|
|
if function::exists(L!("fish_command_not_found"), ctx.parser()) {
|
|
let mut buffer = L!("fish_command_not_found").to_owned();
|
|
for arg in &event_args {
|
|
buffer.push(' ');
|
|
buffer.push_utfstr(&escape(arg));
|
|
}
|
|
let parser = ctx.parser();
|
|
let prev_statuses = parser.get_last_statuses();
|
|
|
|
let event = Event::generic(L!("fish_command_not_found").to_owned());
|
|
let b = parser.push_block(Block::event_block(event));
|
|
parser.eval(&buffer, &io);
|
|
parser.pop_block(b);
|
|
parser.set_last_statuses(prev_statuses);
|
|
} else {
|
|
// If we have no handler, just print it as a normal error.
|
|
error = wgettext!("Unknown command:").to_owned();
|
|
if !event_args.is_empty() {
|
|
error.push(' ');
|
|
error.push_utfstr(&escape(&event_args[0]));
|
|
}
|
|
}
|
|
|
|
if cmd.as_char_slice().first() == Some(&'{' /*}*/) {
|
|
error.push_utfstr(&wgettext!(ERROR_NO_BRACE_GROUPING));
|
|
}
|
|
|
|
// Here we want to report an error (so it shows a backtrace).
|
|
// If the handler printed text, that's already shown, so error will be empty.
|
|
report_error_formatted!(
|
|
self,
|
|
ctx,
|
|
STATUS_CMD_UNKNOWN.unwrap(),
|
|
&statement.command,
|
|
error
|
|
)
|
|
}
|
|
|
|
// Utilities
|
|
fn node_source(&self, node: &dyn ast::Node) -> WString {
|
|
// todo!("maybe don't copy")
|
|
node.source(&self.pstree().src).to_owned()
|
|
}
|
|
|
|
fn infinite_recursive_statement_in_job_list<'b>(
|
|
&self,
|
|
ctx: &OperationContext<'_>,
|
|
jobs: &'b ast::JobList,
|
|
out_func_name: &mut WString,
|
|
) -> Option<&'b ast::DecoratedStatement> {
|
|
// This is a bit fragile. It is a test to see if we are inside of function call, but
|
|
// not inside a block in that function call. If, in the future, the rules for what
|
|
// block scopes are pushed on function invocation changes, then this check will break.
|
|
let parser = ctx.parser();
|
|
let parent;
|
|
let parent_fn_name = {
|
|
match (parser.block_at_index(0), parser.block_at_index(1)) {
|
|
(Some(current), Some(p)) if current.typ() == BlockType::top => {
|
|
parent = p;
|
|
match parent.data() {
|
|
Some(BlockData::Function { name, .. }) => name,
|
|
_ => return None,
|
|
}
|
|
}
|
|
_ => return None, // Not within function call.
|
|
}
|
|
};
|
|
|
|
// Get the function name of the immediate block.
|
|
let forbidden_function_name = parent_fn_name;
|
|
|
|
// Get the first job in the job list.
|
|
let jc = &jobs.get(0)?;
|
|
let job = &jc.job;
|
|
|
|
// Helper to return if a statement is infinitely recursive in this function.
|
|
let statement_recurses = |stat: &'b ast::Statement| -> Option<&'b ast::DecoratedStatement> {
|
|
// Ignore non-decorated statements like `if`, etc.
|
|
let StatementVariant::DecoratedStatement(dc) = &*stat.contents else {
|
|
return None;
|
|
};
|
|
|
|
// Ignore statements with decorations like 'builtin' or 'command', since those
|
|
// are not infinite recursion. In particular that is what enables 'wrapper functions'.
|
|
if dc.decoration() != StatementDecoration::none {
|
|
return None;
|
|
}
|
|
|
|
// Check the command.
|
|
let mut cmd = self.node_source(&dc.command);
|
|
let forbidden = !cmd.is_empty()
|
|
&& expand_one(
|
|
&mut cmd,
|
|
ExpandFlags::FAIL_ON_CMDSUBST | ExpandFlags::SKIP_VARIABLES,
|
|
ctx,
|
|
None,
|
|
)
|
|
&& &cmd == forbidden_function_name;
|
|
if forbidden {
|
|
Some(dc)
|
|
} else {
|
|
None
|
|
}
|
|
};
|
|
|
|
// Check main statement.
|
|
let infinite_recursive_statement = statement_recurses(&jc.job.statement)
|
|
// Check piped remainder.
|
|
.or_else(|| {
|
|
for c in &job.continuation {
|
|
let s = statement_recurses(&c.statement);
|
|
if s.is_some() {
|
|
return s;
|
|
}
|
|
}
|
|
None
|
|
});
|
|
|
|
if infinite_recursive_statement.is_some() {
|
|
*out_func_name = forbidden_function_name.to_owned();
|
|
}
|
|
|
|
// may be none
|
|
infinite_recursive_statement
|
|
}
|
|
|
|
// Expand a command which may contain variables, producing an expand command and possibly
|
|
// arguments. Prints an error message on error.
|
|
fn expand_command(
|
|
&mut self,
|
|
ctx: &OperationContext<'_>,
|
|
statement: &ast::DecoratedStatement,
|
|
out_cmd: &mut WString,
|
|
out_args: &mut Vec<WString>,
|
|
) -> EndExecutionReason {
|
|
// Here we're expanding a command, for example $HOME/bin/stuff or $randomthing. The first
|
|
// completion becomes the command itself, everything after becomes arguments. Command
|
|
// substitutions are not supported.
|
|
let mut errors = ParseErrorList::new();
|
|
|
|
// Get the unexpanded command string. We expect to always get it here.
|
|
// todo!("remove clone")
|
|
let unexp_cmd = self.node_source(&statement.command);
|
|
let pos_of_command_token = statement.command.range().unwrap().start();
|
|
|
|
// Expand the string to produce completions, and report errors.
|
|
let expand_err = expand_to_command_and_args(
|
|
&unexp_cmd,
|
|
ctx,
|
|
out_cmd,
|
|
Some(out_args),
|
|
Some(&mut errors),
|
|
false,
|
|
);
|
|
match expand_err.result {
|
|
ExpandResultCode::error | ExpandResultCode::overflow => {
|
|
// Issue #5812 - the expansions were done on the command token,
|
|
// excluding prefixes such as " " or "if ".
|
|
// This means that the error positions are relative to the beginning
|
|
// of the token; we need to make them relative to the original source.
|
|
parse_error_offset_source_start(&mut errors, pos_of_command_token);
|
|
return self.report_errors(ctx, STATUS_ILLEGAL_CMD.unwrap(), &errors);
|
|
}
|
|
ExpandResultCode::wildcard_no_match => {
|
|
return report_error!(
|
|
self,
|
|
ctx,
|
|
STATUS_UNMATCHED_WILDCARD.unwrap(),
|
|
statement,
|
|
WILDCARD_ERR_MSG,
|
|
&self.node_source(statement)
|
|
);
|
|
}
|
|
ExpandResultCode::cancel => {
|
|
return EndExecutionReason::cancelled;
|
|
}
|
|
ExpandResultCode::ok => {}
|
|
}
|
|
|
|
// Complain if the resulting expansion was empty, or expanded to an empty string.
|
|
// For no-exec it's okay, as we can't really perform the expansion.
|
|
if out_cmd.is_empty() && !no_exec() {
|
|
return report_error!(
|
|
self,
|
|
ctx,
|
|
STATUS_ILLEGAL_CMD.unwrap(),
|
|
&statement.command,
|
|
"The expanded command was empty."
|
|
);
|
|
}
|
|
// Complain if we've expanded to a subcommand keyword like "command" or "if",
|
|
// because these will call the corresponding *builtin*,
|
|
// which won't be doing what the user asks for
|
|
//
|
|
// (skipping in no-exec because we don't have the actual variable value)
|
|
if !no_exec() && parser_keywords_is_subcommand(out_cmd) && &unexp_cmd != out_cmd {
|
|
return report_error!(
|
|
self,
|
|
ctx,
|
|
STATUS_ILLEGAL_CMD.unwrap(),
|
|
&statement.command,
|
|
"The expanded command is a keyword."
|
|
);
|
|
}
|
|
EndExecutionReason::ok
|
|
}
|
|
|
|
/// Indicates whether a job is a simple block (one block, no redirections).
|
|
fn job_is_simple_block(&self, job: &ast::JobPipeline) -> bool {
|
|
// Must be no pipes.
|
|
if !job.continuation.is_empty() {
|
|
return false;
|
|
}
|
|
|
|
// Helper to check if an argument_or_redirection_list_t has no redirections.
|
|
let no_redirs =
|
|
|list: &ast::ArgumentOrRedirectionList| !list.iter().any(|val| val.is_redirection());
|
|
|
|
// Check if we're a block statement with redirections. We do it this obnoxious way to preserve
|
|
// type safety (in case we add more specific statement types).
|
|
match &*job.statement.contents {
|
|
StatementVariant::BlockStatement(stmt) => no_redirs(&stmt.args_or_redirs),
|
|
StatementVariant::SwitchStatement(stmt) => no_redirs(&stmt.args_or_redirs),
|
|
StatementVariant::IfStatement(stmt) => no_redirs(&stmt.args_or_redirs),
|
|
StatementVariant::NotStatement(_) | StatementVariant::DecoratedStatement(_) => {
|
|
// not block statements
|
|
false
|
|
}
|
|
StatementVariant::None => panic!(),
|
|
}
|
|
}
|
|
|
|
fn process_type_for_command(
|
|
&self,
|
|
ctx: &OperationContext<'_>,
|
|
statement: &ast::DecoratedStatement,
|
|
cmd: &wstr,
|
|
) -> ProcessType {
|
|
// Determine the process type, which depends on the statement decoration (command, builtin,
|
|
// etc).
|
|
match statement.decoration() {
|
|
StatementDecoration::exec => ProcessType::exec,
|
|
StatementDecoration::command => ProcessType::external,
|
|
StatementDecoration::builtin => ProcessType::builtin,
|
|
StatementDecoration::none => {
|
|
if function::exists(cmd, ctx.parser()) {
|
|
ProcessType::function
|
|
} else if builtin_exists(cmd) {
|
|
ProcessType::builtin
|
|
} else {
|
|
ProcessType::external
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn apply_variable_assignments(
|
|
&mut self,
|
|
ctx: &OperationContext<'_>,
|
|
mut proc: Option<&mut Process>,
|
|
variable_assignment_list: &ast::VariableAssignmentList,
|
|
block: &mut Option<BlockId>,
|
|
) -> EndExecutionReason {
|
|
if variable_assignment_list.is_empty() {
|
|
return EndExecutionReason::ok;
|
|
}
|
|
*block = Some(ctx.parser().push_block(Block::variable_assignment_block()));
|
|
for variable_assignment in variable_assignment_list {
|
|
let source = self.node_source(&**variable_assignment);
|
|
let equals_pos = variable_assignment_equals_pos(&source).unwrap();
|
|
let variable_name = &source[..equals_pos];
|
|
let expression = &source[equals_pos + 1..];
|
|
let mut expression_expanded = vec![];
|
|
let mut errors = ParseErrorList::new();
|
|
// TODO this is mostly copied from expand_arguments_from_nodes, maybe extract to function
|
|
let expand_ret = expand_string(
|
|
expression.to_owned(),
|
|
&mut expression_expanded,
|
|
ExpandFlags::default(),
|
|
ctx,
|
|
Some(&mut errors),
|
|
);
|
|
parse_error_offset_source_start(
|
|
&mut errors,
|
|
variable_assignment.range().unwrap().start() + equals_pos + 1,
|
|
);
|
|
match expand_ret.result {
|
|
ExpandResultCode::error|ExpandResultCode::overflow => {
|
|
return self.report_errors(ctx, expand_ret.status, &errors);
|
|
}
|
|
ExpandResultCode::cancel => {
|
|
return EndExecutionReason::cancelled;
|
|
}
|
|
ExpandResultCode::wildcard_no_match // nullglob (equivalent to set)
|
|
| ExpandResultCode::ok => {}
|
|
}
|
|
let vals: Vec<_> = expression_expanded
|
|
.into_iter()
|
|
.map(|comp| comp.completion)
|
|
.collect();
|
|
if let Some(proc) = &mut proc {
|
|
proc.variable_assignments.push(ConcreteAssignment::new(
|
|
variable_name.to_owned(),
|
|
vals.clone(),
|
|
));
|
|
}
|
|
ctx.parser()
|
|
.set_var_and_fire(variable_name, EnvMode::LOCAL | EnvMode::EXPORT, vals);
|
|
}
|
|
EndExecutionReason::ok
|
|
}
|
|
|
|
// These create process_t structures from statements.
|
|
fn populate_job_process(
|
|
&mut self,
|
|
ctx: &OperationContext<'_>,
|
|
job: &mut Job,
|
|
proc: &mut Process,
|
|
statement: &ast::Statement,
|
|
variable_assignments: &ast::VariableAssignmentList,
|
|
) -> EndExecutionReason {
|
|
// Get the "specific statement" which is boolean / block / if / switch / decorated.
|
|
let specific_statement = &statement.contents;
|
|
|
|
let mut block = None;
|
|
let result =
|
|
self.apply_variable_assignments(ctx, Some(proc), variable_assignments, &mut block);
|
|
let _scope = ScopeGuard::new((), |()| {
|
|
if let Some(block) = block {
|
|
ctx.parser().pop_block(block);
|
|
}
|
|
});
|
|
if result != EndExecutionReason::ok {
|
|
return result;
|
|
}
|
|
|
|
match &**specific_statement {
|
|
StatementVariant::NotStatement(not_statement) => {
|
|
self.populate_not_process(ctx, job, proc, not_statement)
|
|
}
|
|
StatementVariant::BlockStatement(_)
|
|
| StatementVariant::IfStatement(_)
|
|
| StatementVariant::SwitchStatement(_) => {
|
|
self.populate_block_process(ctx, proc, statement, specific_statement)
|
|
}
|
|
StatementVariant::DecoratedStatement(decorated_statement) => {
|
|
self.populate_plain_process(ctx, proc, decorated_statement)
|
|
}
|
|
StatementVariant::None => panic!(),
|
|
}
|
|
}
|
|
|
|
fn populate_not_process(
|
|
&mut self,
|
|
ctx: &OperationContext<'_>,
|
|
job: &mut Job,
|
|
proc: &mut Process,
|
|
not_statement: &ast::NotStatement,
|
|
) -> EndExecutionReason {
|
|
{
|
|
let mut flags = job.mut_flags();
|
|
flags.negate = !flags.negate;
|
|
}
|
|
self.populate_job_process(
|
|
ctx,
|
|
job,
|
|
proc,
|
|
¬_statement.contents,
|
|
¬_statement.variables,
|
|
)
|
|
}
|
|
|
|
/// Creates a 'normal' (non-block) process.
|
|
fn populate_plain_process(
|
|
&mut self,
|
|
ctx: &OperationContext<'_>,
|
|
proc: &mut Process,
|
|
statement: &ast::DecoratedStatement,
|
|
) -> EndExecutionReason {
|
|
// We may decide that a command should be an implicit cd.
|
|
let mut use_implicit_cd = false;
|
|
|
|
// Get the command and any arguments due to expanding the command.
|
|
let mut cmd = WString::new();
|
|
let mut args_from_cmd_expansion = vec![];
|
|
let ret = self.expand_command(ctx, statement, &mut cmd, &mut args_from_cmd_expansion);
|
|
if ret != EndExecutionReason::ok {
|
|
return ret;
|
|
}
|
|
|
|
// For no-exec, having an empty command is okay. We can't do anything more with it tho.
|
|
if no_exec() {
|
|
return EndExecutionReason::ok;
|
|
}
|
|
|
|
assert!(
|
|
!cmd.is_empty(),
|
|
"expand_command should not produce an empty command",
|
|
);
|
|
|
|
// Determine the process type.
|
|
let mut process_type = self.process_type_for_command(ctx, statement, &cmd);
|
|
let external_cmd = if [ProcessType::external, ProcessType::exec].contains(&process_type) {
|
|
let parser = ctx.parser();
|
|
// Determine the actual command. This may be an implicit cd.
|
|
let external_cmd = path_try_get_path(&cmd, parser.vars());
|
|
let has_command = external_cmd.err.is_none();
|
|
|
|
let mut path = WString::new();
|
|
if has_command {
|
|
path = external_cmd.path;
|
|
} else {
|
|
// If the specified command does not exist, and is undecorated, try using an implicit cd.
|
|
if statement.decoration() == StatementDecoration::none {
|
|
// Implicit cd requires an empty argument and redirection list.
|
|
if statement.args_or_redirs.is_empty() {
|
|
// Ok, no arguments or redirections; check to see if the command is a directory.
|
|
use_implicit_cd = path_as_implicit_cd(
|
|
&cmd,
|
|
&parser.vars().get_pwd_slash(),
|
|
parser.vars(),
|
|
)
|
|
.is_some();
|
|
}
|
|
}
|
|
|
|
if !use_implicit_cd {
|
|
return self.handle_command_not_found(
|
|
ctx,
|
|
if external_cmd.path.is_empty() {
|
|
&cmd
|
|
} else {
|
|
&external_cmd.path
|
|
},
|
|
statement,
|
|
std::io::Error::from_raw_os_error(external_cmd.err.unwrap().into()),
|
|
);
|
|
}
|
|
};
|
|
path
|
|
} else {
|
|
WString::new()
|
|
};
|
|
|
|
// Produce the full argument list and the set of IO redirections.
|
|
let mut cmd_args = vec![];
|
|
let mut redirections = RedirectionSpecList::new();
|
|
if use_implicit_cd {
|
|
// Implicit cd is simple.
|
|
cmd_args = vec![L!("cd").to_owned(), cmd];
|
|
|
|
// If we have defined a wrapper around cd, use it, otherwise use the cd builtin.
|
|
process_type = if function::exists(L!("cd"), ctx.parser()) {
|
|
ProcessType::function
|
|
} else {
|
|
ProcessType::builtin
|
|
};
|
|
} else {
|
|
// Not implicit cd.
|
|
let glob_behavior = if [L!("set"), L!("count"), L!("path")].contains(&&cmd[..]) {
|
|
Globspec::nullglob
|
|
} else {
|
|
Globspec::failglob
|
|
};
|
|
// Form the list of arguments. The command is the first argument, followed by any arguments
|
|
// from expanding the command, followed by the argument nodes themselves. E.g. if the
|
|
// command is '$gco foo' and $gco is git checkout.
|
|
cmd_args.push(cmd);
|
|
cmd_args.extend_from_slice(&args_from_cmd_expansion);
|
|
let arg_nodes = Self::get_argument_nodes_no_redirs(&statement.args_or_redirs);
|
|
let arg_result =
|
|
self.expand_arguments_from_nodes(ctx, &arg_nodes, &mut cmd_args, glob_behavior);
|
|
if arg_result != EndExecutionReason::ok {
|
|
return arg_result;
|
|
}
|
|
|
|
// The set of IO redirections that we construct for the process.
|
|
let reason =
|
|
self.determine_redirections(ctx, &statement.args_or_redirs, &mut redirections);
|
|
if reason != EndExecutionReason::ok {
|
|
return reason;
|
|
}
|
|
}
|
|
|
|
// Populate the process.
|
|
proc.typ = process_type;
|
|
proc.set_argv(cmd_args);
|
|
proc.set_redirection_specs(redirections);
|
|
proc.actual_cmd = external_cmd;
|
|
EndExecutionReason::ok
|
|
}
|
|
|
|
fn populate_block_process(
|
|
&mut self,
|
|
ctx: &OperationContext<'_>,
|
|
proc: &mut Process,
|
|
statement: &ast::Statement,
|
|
specific_statement: &ast::StatementVariant,
|
|
) -> EndExecutionReason {
|
|
// We handle block statements by creating process_type_t::block_node, that will bounce back to
|
|
// us when it's time to execute them.
|
|
// Get the argument or redirections list.
|
|
// TODO: args_or_redirs should be available without resolving the statement type.
|
|
let args_or_redirs = match specific_statement {
|
|
StatementVariant::BlockStatement(block_statement) => &block_statement.args_or_redirs,
|
|
StatementVariant::IfStatement(if_statement) => &if_statement.args_or_redirs,
|
|
StatementVariant::SwitchStatement(switch_statement) => &switch_statement.args_or_redirs,
|
|
_ => panic!("Unexpected block node type"),
|
|
};
|
|
|
|
let mut redirections = RedirectionSpecList::new();
|
|
let reason = self.determine_redirections(ctx, args_or_redirs, &mut redirections);
|
|
if reason == EndExecutionReason::ok {
|
|
proc.typ = ProcessType::block_node;
|
|
proc.block_node_source = Some(Arc::clone(self.pstree()));
|
|
proc.internal_block_node = Some(statement.into());
|
|
proc.set_redirection_specs(redirections);
|
|
}
|
|
reason
|
|
}
|
|
|
|
// These encapsulate the actual logic of various (block) statements.
|
|
fn run_block_statement(
|
|
&mut self,
|
|
ctx: &OperationContext<'_>,
|
|
statement: &'a ast::BlockStatement,
|
|
associated_block: Option<BlockId>,
|
|
) -> EndExecutionReason {
|
|
let bh = &statement.header;
|
|
let contents = &statement.jobs;
|
|
match &**bh {
|
|
BlockStatementHeaderVariant::ForHeader(fh) => self.run_for_statement(ctx, fh, contents),
|
|
BlockStatementHeaderVariant::WhileHeader(wh) => {
|
|
self.run_while_statement(ctx, wh, contents, associated_block)
|
|
}
|
|
BlockStatementHeaderVariant::FunctionHeader(fh) => {
|
|
self.run_function_statement(ctx, statement, fh)
|
|
}
|
|
BlockStatementHeaderVariant::BeginHeader(_bh) => {
|
|
self.run_begin_statement(ctx, contents)
|
|
}
|
|
BlockStatementHeaderVariant::None => panic!(),
|
|
}
|
|
}
|
|
|
|
fn run_for_statement(
|
|
&mut self,
|
|
ctx: &OperationContext<'_>,
|
|
header: &'a ast::ForHeader,
|
|
block_contents: &'a ast::JobList,
|
|
) -> EndExecutionReason {
|
|
// Get the variable name: `for var_name in ...`. We expand the variable name. It better result
|
|
// in just one.
|
|
let mut for_var_name = self.node_source(&header.var_name);
|
|
if !expand_one(&mut for_var_name, ExpandFlags::default(), ctx, None) {
|
|
return report_error!(
|
|
self,
|
|
ctx,
|
|
STATUS_EXPAND_ERROR.unwrap(),
|
|
&header.var_name,
|
|
FAILED_EXPANSION_VARIABLE_NAME_ERR_MSG,
|
|
for_var_name
|
|
);
|
|
}
|
|
|
|
if !valid_var_name(&for_var_name) {
|
|
return report_error!(
|
|
self,
|
|
ctx,
|
|
STATUS_INVALID_ARGS.unwrap(),
|
|
header.var_name,
|
|
BUILTIN_ERR_VARNAME,
|
|
"for",
|
|
for_var_name
|
|
);
|
|
}
|
|
|
|
// Get the contents to iterate over.
|
|
let mut arguments = vec![];
|
|
let arg_nodes = Self::get_argument_nodes(&header.args);
|
|
let ret =
|
|
self.expand_arguments_from_nodes(ctx, &arg_nodes, &mut arguments, Globspec::nullglob);
|
|
if ret != EndExecutionReason::ok {
|
|
return ret;
|
|
}
|
|
let var = ctx.parser().vars().get(&for_var_name);
|
|
if EnvVar::flags_for(&for_var_name).contains(EnvVarFlags::READ_ONLY) {
|
|
return report_error!(
|
|
self,
|
|
ctx,
|
|
STATUS_INVALID_ARGS.unwrap(),
|
|
header.var_name,
|
|
"%ls: %ls: cannot overwrite read-only variable",
|
|
"for",
|
|
for_var_name
|
|
);
|
|
}
|
|
|
|
let retval = ctx.parser().vars().set(
|
|
&for_var_name,
|
|
EnvMode::LOCAL | EnvMode::USER,
|
|
var.map_or(vec![], |var| var.as_list().to_owned()),
|
|
);
|
|
assert!(retval == EnvStackSetResult::Ok);
|
|
|
|
trace_if_enabled_with_args(ctx.parser(), L!("for"), &arguments);
|
|
|
|
// We fire the same event over and over again, just construct it once.
|
|
let evt = Event::variable_set(for_var_name.clone());
|
|
|
|
// Now drive the for loop.
|
|
let mut ret = EndExecutionReason::ok;
|
|
for val in arguments {
|
|
if let Some(reason) = self.check_end_execution(ctx) {
|
|
ret = reason;
|
|
break;
|
|
}
|
|
|
|
let retval = ctx
|
|
.parser()
|
|
.vars()
|
|
.set(&for_var_name, EnvMode::USER, vec![val]);
|
|
assert!(
|
|
retval == EnvStackSetResult::Ok,
|
|
"for loop variable should have been successfully set"
|
|
);
|
|
event::fire(ctx.parser(), evt.clone());
|
|
|
|
ctx.parser().libdata_mut().loop_status = LoopStatus::normals;
|
|
|
|
// Push and pop the block again and again to clear variables
|
|
let fb = ctx.parser().push_block(Block::for_block());
|
|
self.run_job_list(ctx, block_contents, Some(fb));
|
|
ctx.parser().pop_block(fb);
|
|
|
|
if self.check_end_execution(ctx) == Some(EndExecutionReason::control_flow) {
|
|
// Handle break or continue.
|
|
let do_break = ctx.parser().libdata().loop_status == LoopStatus::breaks;
|
|
ctx.parser().libdata_mut().loop_status = LoopStatus::normals;
|
|
if do_break {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
trace_if_enabled(ctx.parser(), L!("end for"));
|
|
ret
|
|
}
|
|
|
|
fn run_if_statement(
|
|
&mut self,
|
|
ctx: &OperationContext<'_>,
|
|
statement: &'a ast::IfStatement,
|
|
associated_block: Option<BlockId>,
|
|
) -> EndExecutionReason {
|
|
let mut result = EndExecutionReason::ok;
|
|
|
|
// We have a sequence of if clauses, with a final else, resulting in a single job list that we
|
|
// execute.
|
|
let mut job_list_to_execute = None;
|
|
let mut if_clause = &statement.if_clause;
|
|
|
|
// Index of the *next* elseif_clause to test.
|
|
let elseif_clauses = &statement.elseif_clauses;
|
|
let mut next_elseif_idx = 0;
|
|
|
|
// We start with the 'if'.
|
|
trace_if_enabled(ctx.parser(), L!("if"));
|
|
|
|
loop {
|
|
if let Some(ret) = self.check_end_execution(ctx) {
|
|
result = ret;
|
|
break;
|
|
}
|
|
|
|
// An if condition has a job and a "tail" of andor jobs, e.g. "foo ; and bar; or baz".
|
|
// Check the condition and the tail. We treat end_execution_reason_t::error here as failure,
|
|
// in accordance with historic behavior.
|
|
let mut cond_ret =
|
|
self.run_job_conjunction(ctx, &if_clause.condition, associated_block);
|
|
if cond_ret == EndExecutionReason::ok {
|
|
cond_ret = self.run_andor_job_list(ctx, &if_clause.andor_tail, associated_block);
|
|
}
|
|
let take_branch = cond_ret == EndExecutionReason::ok
|
|
&& ctx.parser().get_last_status() == EXIT_SUCCESS;
|
|
|
|
if take_branch {
|
|
// Condition succeeded.
|
|
job_list_to_execute = Some(&if_clause.body);
|
|
break;
|
|
}
|
|
|
|
// See if we have an elseif.
|
|
next_elseif_idx += 1;
|
|
if let Some(elseif_clause) = elseif_clauses.get(next_elseif_idx - 1) {
|
|
trace_if_enabled(ctx.parser(), L!("else if"));
|
|
if_clause = &elseif_clause.if_clause;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if job_list_to_execute.is_none() {
|
|
// our ifs and elseifs failed.
|
|
// Check our else body.
|
|
if let Some(else_clause) = statement.else_clause.as_ref() {
|
|
trace_if_enabled(ctx.parser(), L!("else"));
|
|
job_list_to_execute = Some(&else_clause.body);
|
|
}
|
|
}
|
|
|
|
match job_list_to_execute {
|
|
None => {
|
|
// 'if' condition failed, no else clause, return 0, we're done.
|
|
// No job list means no successful conditions, so return 0 (issue #1443).
|
|
ctx.parser()
|
|
.set_last_statuses(Statuses::just(STATUS_CMD_OK.unwrap()));
|
|
}
|
|
Some(job_list_to_execute) => {
|
|
// Execute the job list we got.
|
|
let ib = ctx.parser().push_block(Block::if_block());
|
|
self.run_job_list(ctx, job_list_to_execute, Some(ib));
|
|
if let Some(ret) = self.check_end_execution(ctx) {
|
|
result = ret;
|
|
}
|
|
ctx.parser().pop_block(ib);
|
|
}
|
|
}
|
|
trace_if_enabled(ctx.parser(), L!("end if"));
|
|
|
|
// It's possible there's a last-minute cancellation (issue #1297).
|
|
if let Some(ret) = self.check_end_execution(ctx) {
|
|
result = ret;
|
|
}
|
|
|
|
// Otherwise, take the exit status of the job list. Reversal of issue #1061.
|
|
result
|
|
}
|
|
|
|
fn run_switch_statement(
|
|
&mut self,
|
|
ctx: &OperationContext<'_>,
|
|
statement: &'a ast::SwitchStatement,
|
|
) -> EndExecutionReason {
|
|
// Get the switch variable.
|
|
let switch_value = self.node_source(&statement.argument);
|
|
|
|
// Expand it. We need to offset any errors by the position of the string.
|
|
let mut switch_values_expanded = vec![];
|
|
let mut errors = ParseErrorList::new();
|
|
let expand_ret = expand_string(
|
|
switch_value,
|
|
&mut switch_values_expanded,
|
|
ExpandFlags::default(),
|
|
ctx,
|
|
Some(&mut errors),
|
|
);
|
|
parse_error_offset_source_start(&mut errors, statement.argument.range().unwrap().start());
|
|
|
|
match expand_ret.result {
|
|
ExpandResultCode::error | ExpandResultCode::overflow => {
|
|
return self.report_errors(ctx, expand_ret.status, &errors);
|
|
}
|
|
ExpandResultCode::cancel => {
|
|
return EndExecutionReason::cancelled;
|
|
}
|
|
ExpandResultCode::wildcard_no_match => {
|
|
return report_error!(
|
|
self,
|
|
ctx,
|
|
STATUS_UNMATCHED_WILDCARD.unwrap(),
|
|
&statement.argument,
|
|
WILDCARD_ERR_MSG,
|
|
&self.node_source(&statement.argument)
|
|
);
|
|
}
|
|
ExpandResultCode::ok => {
|
|
if switch_values_expanded.len() > 1 {
|
|
return report_error!(
|
|
self,
|
|
ctx,
|
|
STATUS_INVALID_ARGS.unwrap(),
|
|
&statement.argument,
|
|
"switch: Expected at most one argument, got %lu\n",
|
|
switch_values_expanded.len()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we expanded to nothing, match the empty string.
|
|
assert!(
|
|
switch_values_expanded.len() <= 1,
|
|
"Should have at most one expansion"
|
|
);
|
|
let switch_value_expanded = if switch_values_expanded.is_empty() {
|
|
WString::new()
|
|
} else {
|
|
switch_values_expanded.remove(0).completion
|
|
};
|
|
|
|
let mut result = EndExecutionReason::ok;
|
|
|
|
trace_if_enabled_with_args(ctx.parser(), L!("switch"), &[&switch_value_expanded]);
|
|
let sb = ctx.parser().push_block(Block::switch_block());
|
|
|
|
// Expand case statements.
|
|
let mut matching_case_item = None;
|
|
for case_item in &statement.cases {
|
|
if let Some(ret) = self.check_end_execution(ctx) {
|
|
result = ret;
|
|
break;
|
|
}
|
|
|
|
// Expand arguments. A case item list may have a wildcard that fails to expand to
|
|
// anything. We also report case errors, but don't stop execution; i.e. a case item that
|
|
// contains an unexpandable process will report and then fail to match.
|
|
let arg_nodes = Self::get_argument_nodes(&case_item.arguments);
|
|
let mut case_args = vec![];
|
|
let case_result = self.expand_arguments_from_nodes(
|
|
ctx,
|
|
&arg_nodes,
|
|
&mut case_args,
|
|
Globspec::failglob,
|
|
);
|
|
if case_result == EndExecutionReason::ok {
|
|
for arg in case_args {
|
|
// Unescape wildcards so they can be expanded again.
|
|
let unescaped_arg = parse_util_unescape_wildcards(&arg);
|
|
if wildcard_match(&switch_value_expanded, &unescaped_arg, false) {
|
|
// If this matched, we're done.
|
|
matching_case_item = Some(case_item);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if matching_case_item.is_some() {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if let Some(case_item) = matching_case_item {
|
|
// Success, evaluate the job list.
|
|
assert!(result == EndExecutionReason::ok, "Expected success");
|
|
result = self.run_job_list(ctx, &case_item.body, Some(sb));
|
|
}
|
|
|
|
ctx.parser().pop_block(sb);
|
|
trace_if_enabled(ctx.parser(), L!("end switch"));
|
|
result
|
|
}
|
|
|
|
fn run_while_statement(
|
|
&mut self,
|
|
ctx: &OperationContext<'_>,
|
|
header: &'a ast::WhileHeader,
|
|
contents: &'a ast::JobList,
|
|
associated_block: Option<BlockId>,
|
|
) -> EndExecutionReason {
|
|
let mut ret = EndExecutionReason::ok;
|
|
|
|
// "The exit status of the while loop shall be the exit status of the last compound-list-2
|
|
// executed, or zero if none was executed."
|
|
// Here are more detailed requirements:
|
|
// - If we execute the loop body zero times, or the loop body is empty, the status is success.
|
|
// - An empty loop body is treated as true, both in the loop condition and after loop exit.
|
|
// - The exit status of the last command is visible in the loop condition. (i.e. do not set the
|
|
// exit status to true BEFORE executing the loop condition).
|
|
// We achieve this by restoring the status if the loop condition fails, plus a special
|
|
// affordance for the first condition.
|
|
let mut first_cond_check = true;
|
|
|
|
trace_if_enabled(ctx.parser(), L!("while"));
|
|
|
|
// Run while the condition is true.
|
|
loop {
|
|
// Save off the exit status if it came from the loop body. We'll restore it if the condition
|
|
// is false.
|
|
let cond_saved_status = if first_cond_check {
|
|
Statuses::just(EXIT_SUCCESS)
|
|
} else {
|
|
ctx.parser().get_last_statuses()
|
|
};
|
|
first_cond_check = false;
|
|
|
|
// Check the condition.
|
|
let mut cond_ret = self.run_job_conjunction(ctx, &header.condition, associated_block);
|
|
if cond_ret == EndExecutionReason::ok {
|
|
cond_ret = self.run_andor_job_list(ctx, &header.andor_tail, associated_block);
|
|
}
|
|
|
|
// If the loop condition failed to execute, then exit the loop without modifying the exit
|
|
// status. If the loop condition executed with a failure status, restore the status and then
|
|
// exit the loop.
|
|
if cond_ret != EndExecutionReason::ok {
|
|
break;
|
|
} else if ctx.parser().get_last_status() != EXIT_SUCCESS {
|
|
ctx.parser().set_last_statuses(cond_saved_status);
|
|
break;
|
|
}
|
|
|
|
// Check cancellation.
|
|
if let Some(reason) = self.check_end_execution(ctx) {
|
|
ret = reason;
|
|
break;
|
|
}
|
|
|
|
// Push a while block and then check its cancellation reason.
|
|
ctx.parser().libdata_mut().loop_status = LoopStatus::normals;
|
|
|
|
let wb = ctx.parser().push_block(Block::while_block());
|
|
self.run_job_list(ctx, contents, Some(wb));
|
|
let cancel_reason = self.check_end_execution(ctx);
|
|
ctx.parser().pop_block(wb);
|
|
|
|
if cancel_reason == Some(EndExecutionReason::control_flow) {
|
|
// Handle break or continue.
|
|
let do_break = ctx.parser().libdata().loop_status == LoopStatus::breaks;
|
|
ctx.parser().libdata_mut().loop_status = LoopStatus::normals;
|
|
if do_break {
|
|
break;
|
|
} else {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// no_exec means that fish was invoked with -n or --no-execute. If set, we allow the loop to
|
|
// not-execute once so its contents can be checked, and then break.
|
|
if no_exec() {
|
|
break;
|
|
}
|
|
}
|
|
trace_if_enabled(ctx.parser(), L!("end while"));
|
|
ret
|
|
}
|
|
|
|
// Define a function.
|
|
fn run_function_statement(
|
|
&mut self,
|
|
ctx: &OperationContext<'_>,
|
|
statement: &ast::BlockStatement,
|
|
header: &ast::FunctionHeader,
|
|
) -> EndExecutionReason {
|
|
// Get arguments.
|
|
let mut arguments = vec![];
|
|
let mut arg_nodes = Self::get_argument_nodes(&header.args);
|
|
arg_nodes.insert(0, &header.first_arg);
|
|
let result =
|
|
self.expand_arguments_from_nodes(ctx, &arg_nodes, &mut arguments, Globspec::failglob);
|
|
|
|
if result != EndExecutionReason::ok {
|
|
return result;
|
|
}
|
|
|
|
trace_if_enabled_with_args(ctx.parser(), L!("function"), &arguments);
|
|
let mut outs = OutputStream::Null;
|
|
let mut errs = OutputStream::String(StringOutputStream::new());
|
|
let io_chain = IoChain::new();
|
|
let mut streams = IoStreams::new(&mut outs, &mut errs, &io_chain);
|
|
let mut shim_arguments: Vec<&wstr> = arguments
|
|
.iter()
|
|
.map(|s| truncate_at_nul(s.as_ref()))
|
|
.collect();
|
|
let err_code = builtins::function::function(
|
|
ctx.parser(),
|
|
&mut streams,
|
|
&mut shim_arguments,
|
|
NodeRef::new(
|
|
Arc::clone(self.pstree()),
|
|
statement as *const ast::BlockStatement,
|
|
),
|
|
);
|
|
let err_code = err_code.unwrap();
|
|
ctx.parser().libdata_mut().status_count += 1;
|
|
ctx.parser().set_last_statuses(Statuses::just(err_code));
|
|
|
|
let errtext = errs.contents();
|
|
if !errtext.is_empty() {
|
|
report_error!(self, ctx, err_code, header, "%ls", errtext);
|
|
}
|
|
result
|
|
}
|
|
|
|
fn run_begin_statement(
|
|
&mut self,
|
|
ctx: &OperationContext<'_>,
|
|
contents: &'a ast::JobList,
|
|
) -> EndExecutionReason {
|
|
// Basic begin/end block. Push a scope block, run jobs, pop it
|
|
trace_if_enabled(ctx.parser(), L!("begin"));
|
|
let sb = ctx
|
|
.parser()
|
|
.push_block(Block::scope_block(BlockType::begin));
|
|
let ret = self.run_job_list(ctx, contents, Some(sb));
|
|
ctx.parser().pop_block(sb);
|
|
trace_if_enabled(ctx.parser(), L!("end begin"));
|
|
ret
|
|
}
|
|
|
|
fn get_argument_nodes(args: &ast::ArgumentList) -> AstArgsList<'_> {
|
|
let mut result = AstArgsList::new();
|
|
for arg in args {
|
|
result.push(&**arg);
|
|
}
|
|
result
|
|
}
|
|
|
|
fn get_argument_nodes_no_redirs(args: &ast::ArgumentOrRedirectionList) -> AstArgsList<'_> {
|
|
let mut result = AstArgsList::new();
|
|
for arg in args {
|
|
if arg.is_argument() {
|
|
result.push(arg.argument());
|
|
}
|
|
}
|
|
result
|
|
}
|
|
|
|
fn expand_arguments_from_nodes(
|
|
&mut self,
|
|
ctx: &OperationContext<'_>,
|
|
argument_nodes: &AstArgsList<'_>,
|
|
out_arguments: &mut Vec<WString>,
|
|
glob_behavior: Globspec,
|
|
) -> EndExecutionReason {
|
|
// Get all argument nodes underneath the statement. We guess we'll have that many arguments (but
|
|
// may have more or fewer, if there are wildcards involved).
|
|
out_arguments.reserve(argument_nodes.len());
|
|
for arg_node in argument_nodes {
|
|
// Expect all arguments to have source.
|
|
assert!(arg_node.has_source(), "Argument should have source");
|
|
|
|
// Expand this string.
|
|
let mut errors = ParseErrorList::new();
|
|
let mut arg_expanded = CompletionList::new();
|
|
let expand_ret = expand_string(
|
|
self.node_source(*arg_node),
|
|
&mut arg_expanded,
|
|
ExpandFlags::default(),
|
|
ctx,
|
|
Some(&mut errors),
|
|
);
|
|
parse_error_offset_source_start(&mut errors, arg_node.range().unwrap().start());
|
|
match expand_ret.result {
|
|
ExpandResultCode::error | ExpandResultCode::overflow => {
|
|
return self.report_errors(ctx, expand_ret.status, &errors);
|
|
}
|
|
ExpandResultCode::cancel => {
|
|
return EndExecutionReason::cancelled;
|
|
}
|
|
ExpandResultCode::wildcard_no_match => {
|
|
if glob_behavior == Globspec::failglob {
|
|
// For no_exec, ignore the error - this might work at runtime.
|
|
if no_exec() {
|
|
return EndExecutionReason::ok;
|
|
}
|
|
// Report the unmatched wildcard error and stop processing.
|
|
return report_error!(
|
|
self,
|
|
ctx,
|
|
STATUS_UNMATCHED_WILDCARD.unwrap(),
|
|
arg_node,
|
|
WILDCARD_ERR_MSG,
|
|
&self.node_source(*arg_node)
|
|
);
|
|
}
|
|
}
|
|
ExpandResultCode::ok => {}
|
|
}
|
|
|
|
// Now copy over any expanded arguments. Use std::move() to avoid extra allocations; this
|
|
// is called very frequently.
|
|
if let Some(additional) =
|
|
(out_arguments.len() + arg_expanded.len()).checked_sub(out_arguments.capacity())
|
|
{
|
|
out_arguments.reserve(additional);
|
|
}
|
|
for new_arg in arg_expanded {
|
|
out_arguments.push(new_arg.completion);
|
|
}
|
|
}
|
|
|
|
// We may have received a cancellation during this expansion.
|
|
if let Some(ret) = self.check_end_execution(ctx) {
|
|
return ret;
|
|
}
|
|
|
|
EndExecutionReason::ok
|
|
}
|
|
|
|
// Determines the list of redirections for a node.
|
|
fn determine_redirections(
|
|
&self,
|
|
ctx: &OperationContext<'_>,
|
|
list: &ast::ArgumentOrRedirectionList,
|
|
out_redirections: &mut RedirectionSpecList,
|
|
) -> EndExecutionReason {
|
|
// Get all redirection nodes underneath the statement.
|
|
for arg_or_redir in list {
|
|
if !arg_or_redir.is_redirection() {
|
|
continue;
|
|
}
|
|
let redir_node = arg_or_redir.redirection();
|
|
|
|
let oper = match PipeOrRedir::try_from(&self.node_source(&redir_node.oper)[..]) {
|
|
Ok(oper) if oper.is_valid() => oper,
|
|
_ => {
|
|
// TODO: figure out if this can ever happen. If so, improve this error message.
|
|
return report_error!(
|
|
self,
|
|
ctx,
|
|
STATUS_INVALID_ARGS.unwrap(),
|
|
redir_node,
|
|
"Invalid redirection: %ls",
|
|
&self.node_source(redir_node)
|
|
);
|
|
}
|
|
};
|
|
|
|
// PCA: I can't justify this skip_variables flag. It was like this when I got here.
|
|
let mut target = self.node_source(&redir_node.target);
|
|
let target_expanded = expand_one(
|
|
&mut target,
|
|
if no_exec() {
|
|
ExpandFlags::SKIP_VARIABLES
|
|
} else {
|
|
ExpandFlags::default()
|
|
},
|
|
ctx,
|
|
None,
|
|
);
|
|
if !target_expanded || target.is_empty() {
|
|
// TODO: Improve this error message.
|
|
return report_error!(
|
|
self,
|
|
ctx,
|
|
STATUS_INVALID_ARGS.unwrap(),
|
|
redir_node,
|
|
"Invalid redirection target: %ls",
|
|
target
|
|
);
|
|
}
|
|
|
|
// Make a redirection spec from the redirect token.
|
|
assert!(oper.is_valid(), "expected to have a valid redirection");
|
|
let spec = RedirectionSpec::new(oper.fd, oper.mode, target);
|
|
|
|
// Validate this spec.
|
|
if spec.mode == RedirectionMode::fd
|
|
&& !spec.is_close()
|
|
&& spec.get_target_as_fd().is_none()
|
|
{
|
|
return report_error!(
|
|
self,
|
|
ctx,
|
|
STATUS_INVALID_ARGS.unwrap(),
|
|
redir_node,
|
|
"Requested redirection to '%ls', which is not a valid file descriptor",
|
|
&spec.target
|
|
);
|
|
}
|
|
out_redirections.push(spec);
|
|
|
|
if oper.stderr_merge {
|
|
// This was a redirect like &> which also modifies stderr.
|
|
// Also redirect stderr to stdout.
|
|
out_redirections.push(get_stderr_merge());
|
|
}
|
|
}
|
|
EndExecutionReason::ok
|
|
}
|
|
|
|
fn run_1_job(
|
|
&mut self,
|
|
ctx: &OperationContext<'_>,
|
|
job_node: &'a ast::JobPipeline,
|
|
associated_block: Option<BlockId>,
|
|
) -> EndExecutionReason {
|
|
if let Some(ret) = self.check_end_execution(ctx) {
|
|
return ret;
|
|
}
|
|
|
|
// We definitely do not want to execute anything if we're told we're --no-execute!
|
|
if no_exec() {
|
|
return EndExecutionReason::ok;
|
|
}
|
|
|
|
// Increment the eval_level for the duration of this command.
|
|
let _saved_eval_level = scoped_push_replacer(
|
|
|new_value| ctx.parser().eval_level.swap(new_value, Ordering::Relaxed),
|
|
ctx.parser().eval_level.load(Ordering::Relaxed) + 1,
|
|
);
|
|
|
|
// Save the executing node.
|
|
let line_counter = Rc::clone(&self.line_counter);
|
|
let _saved_node = scoped_push_replacer(
|
|
|node| line_counter.borrow_mut().set_node(node),
|
|
Some(job_node),
|
|
);
|
|
|
|
// Profiling support.
|
|
let profile_item_id = ctx.parser().create_profile_item();
|
|
let start_time = if profile_item_id.is_some() {
|
|
ProfileItem::now()
|
|
} else {
|
|
0
|
|
};
|
|
|
|
let job_is_background = job_node.bg.is_some();
|
|
let _timer = {
|
|
let wants_timing = job_node_wants_timing(job_node);
|
|
// It's an error to have 'time' in a background job.
|
|
if wants_timing && job_is_background {
|
|
return report_error!(
|
|
self,
|
|
ctx,
|
|
STATUS_INVALID_ARGS.unwrap(),
|
|
job_node,
|
|
ERROR_TIME_BACKGROUND
|
|
);
|
|
}
|
|
wants_timing.then(push_timer)
|
|
};
|
|
|
|
// When we encounter a block construct (e.g. while loop) in the general case, we create a "block
|
|
// process" containing its node. This allows us to handle block-level redirections.
|
|
// However, if there are no redirections, then we can just jump into the block directly, which
|
|
// is significantly faster.
|
|
if self.job_is_simple_block(job_node) {
|
|
let mut block = None;
|
|
let mut result =
|
|
self.apply_variable_assignments(ctx, None, &job_node.variables, &mut block);
|
|
let _scope = ScopeGuard::new((), |()| {
|
|
if let Some(block) = block {
|
|
ctx.parser().pop_block(block);
|
|
}
|
|
});
|
|
|
|
let specific_statement = &job_node.statement.contents;
|
|
assert!(specific_statement_type_is_redirectable_block(
|
|
specific_statement
|
|
));
|
|
if result == EndExecutionReason::ok {
|
|
result = match &**specific_statement {
|
|
StatementVariant::BlockStatement(block_statement) => {
|
|
self.run_block_statement(ctx, block_statement, associated_block)
|
|
}
|
|
StatementVariant::IfStatement(ifstmt) => {
|
|
self.run_if_statement(ctx, ifstmt, associated_block)
|
|
}
|
|
StatementVariant::SwitchStatement(switchstmt) => {
|
|
self.run_switch_statement(ctx, switchstmt)
|
|
}
|
|
// Other types should be impossible due to the
|
|
// specific_statement_type_is_redirectable_block check.
|
|
StatementVariant::NotStatement(_)
|
|
| StatementVariant::DecoratedStatement(_)
|
|
| StatementVariant::None => panic!(),
|
|
};
|
|
}
|
|
|
|
if let Some(profile_item_id) = profile_item_id {
|
|
let parser = ctx.parser();
|
|
let mut profile_items = parser.profile_items_mut();
|
|
let profile_item = &mut profile_items[profile_item_id];
|
|
profile_item.duration = ProfileItem::now() - start_time;
|
|
profile_item.level = ctx.parser().eval_level.load(Ordering::Relaxed);
|
|
profile_item.cmd =
|
|
profiling_cmd_name_for_redirectable_block(specific_statement, self.pstree());
|
|
profile_item.skipped = false;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
let mut props = JobProperties::default();
|
|
props.initial_background = job_is_background;
|
|
{
|
|
let parser = ctx.parser();
|
|
let ld = &parser.libdata();
|
|
props.skip_notification =
|
|
ld.is_subshell || parser.is_block() || ld.is_event != 0 || !parser.is_interactive();
|
|
props.from_event_handler = ld.is_event != 0;
|
|
}
|
|
|
|
let mut job = Job::new(props, self.node_source(job_node));
|
|
|
|
// We are about to populate a job. One possible argument to the job is a command substitution
|
|
// which may be interested in the job that's populating it, via '--on-job-exit caller'. Record
|
|
// the job ID here.
|
|
let _caller_id = scoped_push_replacer(
|
|
|new_value| std::mem::replace(&mut ctx.parser().libdata_mut().caller_id, new_value),
|
|
job.internal_job_id,
|
|
);
|
|
|
|
// Populate the job. This may fail for reasons like command_not_found. If this fails, an error
|
|
// will have been printed.
|
|
let pop_result = self.populate_job_from_job_node(ctx, &mut job, job_node, associated_block);
|
|
ScopeGuarding::commit(_caller_id);
|
|
|
|
// Clean up the job on failure or cancellation.
|
|
if pop_result == EndExecutionReason::ok {
|
|
self.setup_group(ctx, &mut job);
|
|
assert!(job.group.is_some(), "Should have a group");
|
|
}
|
|
|
|
// Now that we're done mutating the Job, we can stick it in an Arc
|
|
let job = Rc::new(job);
|
|
|
|
if pop_result == EndExecutionReason::ok {
|
|
// Give the job to the parser - it will clean it up.
|
|
{
|
|
let parser = ctx.parser();
|
|
parser.job_add(job.clone());
|
|
|
|
// Actually execute the job.
|
|
if !exec_job(parser, &job, self.block_io.clone()) {
|
|
// No process in the job successfully launched.
|
|
// Ensure statuses are set (#7540).
|
|
if let Some(statuses) = job.get_statuses() {
|
|
parser.set_last_statuses(statuses);
|
|
parser.libdata_mut().status_count += 1;
|
|
}
|
|
remove_job(parser, &job);
|
|
}
|
|
|
|
// Update universal variables on external commands.
|
|
// We only incorporate external changes if we had an external proc, for hysterical raisins.
|
|
parser.sync_uvars_and_fire(job.has_external_proc() /* always */);
|
|
}
|
|
|
|
// If the job got a SIGINT or SIGQUIT, then we're going to start unwinding.
|
|
if self.cancel_signal.is_none() {
|
|
self.cancel_signal = job.group().get_cancel_signal();
|
|
}
|
|
}
|
|
|
|
if let Some(profile_item_id) = profile_item_id {
|
|
let parser = ctx.parser();
|
|
let mut profile_items = parser.profile_items_mut();
|
|
let profile_item = &mut profile_items[profile_item_id];
|
|
profile_item.duration = ProfileItem::now() - start_time;
|
|
profile_item.level = ctx.parser().eval_level.load(Ordering::Relaxed);
|
|
profile_item.cmd = job.command().to_owned();
|
|
profile_item.skipped = pop_result != EndExecutionReason::ok;
|
|
}
|
|
|
|
job_reap(ctx.parser(), false); // clean up jobs
|
|
pop_result
|
|
}
|
|
|
|
fn test_and_run_1_job_conjunction(
|
|
&mut self,
|
|
ctx: &OperationContext<'_>,
|
|
jc: &'a ast::JobConjunction,
|
|
associated_block: Option<BlockId>,
|
|
) -> EndExecutionReason {
|
|
// Test this job conjunction if it has an 'and' or 'or' decorator.
|
|
// If it passes, then run it.
|
|
if let Some(reason) = self.check_end_execution(ctx) {
|
|
return reason;
|
|
}
|
|
// Maybe skip the job if it has a leading and/or.
|
|
let mut skip = false;
|
|
if let Some(deco) = &jc.decorator {
|
|
let last_status = ctx.parser().get_last_status();
|
|
match deco.keyword() {
|
|
ParseKeyword::kw_and => {
|
|
// AND. Skip if the last job failed.
|
|
skip = last_status != 0;
|
|
}
|
|
ParseKeyword::kw_or => {
|
|
// OR. Skip if the last job succeeded.
|
|
skip = last_status == 0;
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
// Skipping is treated as success.
|
|
if skip {
|
|
EndExecutionReason::ok
|
|
} else {
|
|
self.run_job_conjunction(ctx, jc, associated_block)
|
|
}
|
|
}
|
|
|
|
fn run_job_conjunction(
|
|
&mut self,
|
|
ctx: &OperationContext<'_>,
|
|
job_expr: &'a ast::JobConjunction,
|
|
associated_block: Option<BlockId>,
|
|
) -> EndExecutionReason {
|
|
if let Some(reason) = self.check_end_execution(ctx) {
|
|
return reason;
|
|
}
|
|
let mut result = self.run_1_job(ctx, &job_expr.job, associated_block);
|
|
for jc in &job_expr.continuations {
|
|
if result != EndExecutionReason::ok {
|
|
return result;
|
|
}
|
|
if let Some(reason) = self.check_end_execution(ctx) {
|
|
return reason;
|
|
}
|
|
// Check the conjunction type.
|
|
let last_status = ctx.parser().get_last_status();
|
|
let skip = match jc.conjunction.token_type() {
|
|
ParseTokenType::andand => {
|
|
// AND. Skip if the last job failed.
|
|
last_status != 0
|
|
}
|
|
ParseTokenType::oror => {
|
|
// OR. Skip if the last job succeeded.
|
|
last_status == 0
|
|
}
|
|
_ => unreachable!(),
|
|
};
|
|
if !skip {
|
|
result = self.run_1_job(ctx, &jc.job, associated_block);
|
|
}
|
|
}
|
|
result
|
|
}
|
|
|
|
fn run_job_list(
|
|
&mut self,
|
|
ctx: &OperationContext<'_>,
|
|
job_list_node: &'a ast::JobList,
|
|
associated_block: Option<BlockId>,
|
|
) -> EndExecutionReason {
|
|
let mut result = EndExecutionReason::ok;
|
|
for jc in job_list_node {
|
|
result = self.test_and_run_1_job_conjunction(ctx, jc, associated_block);
|
|
}
|
|
// Returns the result of the last job executed or skipped.
|
|
result
|
|
}
|
|
|
|
fn run_andor_job_list(
|
|
&mut self,
|
|
ctx: &OperationContext<'_>,
|
|
job_list_node: &'a ast::AndorJobList,
|
|
associated_block: Option<BlockId>,
|
|
) -> EndExecutionReason {
|
|
let mut result = EndExecutionReason::ok;
|
|
for aoj in job_list_node {
|
|
result = self.test_and_run_1_job_conjunction(ctx, &aoj.job, associated_block);
|
|
}
|
|
// Returns the result of the last job executed or skipped.
|
|
result
|
|
}
|
|
|
|
fn populate_job_from_job_node(
|
|
&mut self,
|
|
ctx: &OperationContext<'_>,
|
|
j: &mut Job,
|
|
job_node: &ast::JobPipeline,
|
|
_associated_block: Option<BlockId>,
|
|
) -> EndExecutionReason {
|
|
// We are going to construct process_t structures for every statement in the job.
|
|
// Create processes. Each one may fail.
|
|
let mut processes = ProcessList::new();
|
|
processes.push(Box::new(Process::new()));
|
|
let mut result = self.populate_job_process(
|
|
ctx,
|
|
j,
|
|
&mut processes[0],
|
|
&job_node.statement,
|
|
&job_node.variables,
|
|
);
|
|
|
|
// Construct process_ts for job continuations (pipelines).
|
|
for jc in &job_node.continuation {
|
|
if result != EndExecutionReason::ok {
|
|
break;
|
|
}
|
|
// Handle the pipe, whose fd may not be the obvious stdout.
|
|
let parsed_pipe = PipeOrRedir::try_from(&self.node_source(&jc.pipe)[..])
|
|
.expect("Failed to parse valid pipe");
|
|
if !parsed_pipe.is_valid() {
|
|
result = report_error!(
|
|
self,
|
|
ctx,
|
|
STATUS_INVALID_ARGS.unwrap(),
|
|
&jc.pipe,
|
|
ILLEGAL_FD_ERR_MSG,
|
|
&self.node_source(&jc.pipe)
|
|
);
|
|
break;
|
|
}
|
|
{
|
|
let proc = processes.last_mut().unwrap();
|
|
proc.pipe_write_fd = parsed_pipe.fd;
|
|
if parsed_pipe.stderr_merge {
|
|
// This was a pipe like &| which redirects both stdout and stderr.
|
|
// Also redirect stderr to stdout.
|
|
let specs = proc.redirection_specs_mut();
|
|
specs.push(get_stderr_merge());
|
|
}
|
|
}
|
|
|
|
// Store the new process (and maybe with an error).
|
|
processes.push(Box::new(Process::new()));
|
|
result = self.populate_job_process(
|
|
ctx,
|
|
j,
|
|
processes.last_mut().unwrap(),
|
|
&jc.statement,
|
|
&jc.variables,
|
|
);
|
|
}
|
|
|
|
// Inform our processes of who is first and last
|
|
processes.first_mut().unwrap().is_first_in_job = true;
|
|
processes.last_mut().unwrap().is_last_in_job = true;
|
|
|
|
// Return what happened.
|
|
if result == EndExecutionReason::ok {
|
|
// Link up the processes.
|
|
assert!(!processes.is_empty());
|
|
*j.processes_mut() = processes;
|
|
}
|
|
result
|
|
}
|
|
|
|
// Assign a job group to the given job.
|
|
fn setup_group(&self, ctx: &OperationContext<'_>, j: &mut Job) {
|
|
// We can use the parent group if it's compatible and we're not backgrounded.
|
|
if ctx.job_group.as_ref().map_or(false, |job_group| {
|
|
job_group.has_job_id() || !j.wants_job_id()
|
|
}) && !j.is_initially_background()
|
|
{
|
|
j.group = ctx.job_group.clone();
|
|
return;
|
|
}
|
|
|
|
if j.processes()[0].is_internal() || !self.use_job_control(ctx) {
|
|
// This job either doesn't have a pgroup (e.g. a simple block), or lives in fish's pgroup.
|
|
j.group = Some(JobGroup::create(j.command().to_owned(), j.wants_job_id()));
|
|
} else {
|
|
// This is a "real job" that gets its own pgroup.
|
|
j.processes_mut()[0].leads_pgrp = true;
|
|
let wants_terminal = ctx.parser().libdata().is_event == 0;
|
|
j.group = Some(JobGroup::create_with_job_control(
|
|
j.command().to_owned(),
|
|
wants_terminal,
|
|
));
|
|
}
|
|
j.group().is_foreground.store(!j.is_initially_background());
|
|
j.mut_flags().is_group_root = true;
|
|
}
|
|
|
|
// Return whether we should apply job control to our processes.
|
|
fn use_job_control(&self, ctx: &OperationContext<'_>) -> bool {
|
|
if ctx.parser().is_command_substitution() {
|
|
return false;
|
|
}
|
|
match get_job_control_mode() {
|
|
JobControl::all => true,
|
|
JobControl::interactive => ctx.parser().is_interactive(),
|
|
JobControl::none => false,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Eq, PartialEq)]
|
|
enum Globspec {
|
|
failglob,
|
|
nullglob,
|
|
}
|
|
type AstArgsList<'a> = Vec<&'a ast::Argument>;
|
|
|
|
/// These are the specific statement types that support redirections.
|
|
fn type_is_redirectable_block(typ: ast::Type) -> bool {
|
|
[
|
|
ast::Type::block_statement,
|
|
ast::Type::if_statement,
|
|
ast::Type::switch_statement,
|
|
]
|
|
.contains(&typ)
|
|
}
|
|
|
|
fn specific_statement_type_is_redirectable_block(node: &ast::StatementVariant) -> bool {
|
|
type_is_redirectable_block(node.typ())
|
|
}
|
|
|
|
/// Get the name of a redirectable block, for profiling purposes.
|
|
fn profiling_cmd_name_for_redirectable_block(
|
|
node: &ast::StatementVariant,
|
|
pstree: &ParsedSourceRef,
|
|
) -> WString {
|
|
assert!(specific_statement_type_is_redirectable_block(node));
|
|
|
|
let source_range = node.try_source_range().expect("No source range for block");
|
|
|
|
let src_end = match node {
|
|
StatementVariant::BlockStatement(block_statement) => {
|
|
let block_header = &block_statement.header;
|
|
match &**block_header {
|
|
BlockStatementHeaderVariant::ForHeader(for_header) => {
|
|
for_header.semi_nl.source_range().start()
|
|
}
|
|
BlockStatementHeaderVariant::WhileHeader(while_header) => {
|
|
while_header.condition.source_range().start()
|
|
}
|
|
BlockStatementHeaderVariant::FunctionHeader(function_header) => {
|
|
function_header.semi_nl.source_range().start()
|
|
}
|
|
BlockStatementHeaderVariant::BeginHeader(begin_header) => {
|
|
begin_header.kw_begin.source_range().start()
|
|
}
|
|
BlockStatementHeaderVariant::None => panic!("Unexpected block header type"),
|
|
}
|
|
}
|
|
StatementVariant::IfStatement(ifstmt) => {
|
|
ifstmt.if_clause.condition.job.source_range().end()
|
|
}
|
|
StatementVariant::SwitchStatement(switchstmt) => switchstmt.semi_nl.source_range().start(),
|
|
_ => {
|
|
panic!("Not a redirectable block_type");
|
|
}
|
|
};
|
|
|
|
assert!(src_end >= source_range.start(), "Invalid source end");
|
|
|
|
// Get the source for the block, and cut it at the next statement terminator.
|
|
let mut result = pstree.src[source_range.start()..src_end].to_owned();
|
|
result.push_utfstr(L!("..."));
|
|
result
|
|
}
|
|
|
|
/// Get a redirection from stderr to stdout (i.e. 2>&1).
|
|
fn get_stderr_merge() -> RedirectionSpec {
|
|
let stdout_fileno_str = L!("1").to_owned();
|
|
RedirectionSpec::new(STDERR_FILENO, RedirectionMode::fd, stdout_fileno_str)
|
|
}
|
|
|
|
/// Decide if a job node should be 'time'd.
|
|
/// For historical reasons the 'not' and 'time' prefix are "inside out". That is, it's
|
|
/// 'not time cmd'. Note that a time appearing anywhere in the pipeline affects the whole job.
|
|
/// `sleep 1 | not time true` will time the whole job!
|
|
fn job_node_wants_timing(job_node: &ast::JobPipeline) -> bool {
|
|
// Does our job have the job-level time prefix?
|
|
if job_node.time.is_some() {
|
|
return true;
|
|
}
|
|
|
|
// Helper to return true if a node is 'not time ...' or 'not not time...' or...
|
|
let is_timed_not_statement = |mut stat: &ast::Statement| loop {
|
|
match &*stat.contents {
|
|
StatementVariant::NotStatement(ns) => {
|
|
if ns.time.is_some() {
|
|
return true;
|
|
}
|
|
stat = &ns.contents;
|
|
}
|
|
_ => return false,
|
|
}
|
|
};
|
|
|
|
// Do we have a 'not time ...' anywhere in our pipeline?
|
|
if is_timed_not_statement(&job_node.statement) {
|
|
return true;
|
|
}
|
|
for jc in &job_node.continuation {
|
|
if is_timed_not_statement(&jc.statement) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
fn remove_job(parser: &Parser, job: &JobRef) -> bool {
|
|
let mut jobs = parser.jobs_mut();
|
|
let num_jobs = jobs.len();
|
|
for i in 0..num_jobs {
|
|
if Rc::ptr_eq(&jobs[i], job) {
|
|
jobs.remove(i);
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|