fish-shell/src/parse_execution.cpp

1535 lines
62 KiB
C++
Raw Normal View History

// Provides the "linkage" between a parse_node_tree_t and actual execution structures (job_t, etc.)
//
// A note on error handling: fish has two kind of errors, fatal parse errors non-fatal runtime
// errors. A fatal error prevents execution of the entire file, while a non-fatal error skips that
// job.
//
// Non-fatal errors are printed as soon as they are encountered; otherwise you would have to wait
// for the execution to finish to see them.
#include "config.h" // IWYU pragma: keep
#include "parse_execution.h"
2015-07-25 15:14:25 +00:00
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
#include <unistd.h>
#include <wctype.h>
#include <algorithm>
#include <cwchar>
#include <memory>
#include <string>
2017-02-11 02:47:02 +00:00
#include <type_traits>
2015-07-25 15:14:25 +00:00
#include <vector>
#include "builtin.h"
#include "builtin_function.h"
#include "common.h"
#include "complete.h"
2015-07-25 15:14:25 +00:00
#include "env.h"
#include "event.h"
#include "exec.h"
#include "expand.h"
2019-05-27 22:56:53 +00:00
#include "flog.h"
2015-07-25 15:14:25 +00:00
#include "function.h"
#include "io.h"
#include "maybe.h"
#include "parse_constants.h"
#include "parse_util.h"
#include "parser.h"
#include "path.h"
#include "proc.h"
#include "reader.h"
2019-12-20 04:41:53 +00:00
#include "timer.h"
#include "tnode.h"
#include "tokenizer.h"
#include "trace.h"
#include "util.h"
#include "wildcard.h"
#include "wutil.h"
namespace g = grammar;
/// These are the specific statement types that support redirections.
static constexpr bool type_is_redirectable_block(parse_token_type_t type) {
return type == symbol_block_statement || type == symbol_if_statement ||
type == symbol_switch_statement;
}
static bool specific_statement_type_is_redirectable_block(const parse_node_t &node) {
return type_is_redirectable_block(node.type);
}
/// Get the name of a redirectable block, for profiling purposes.
static wcstring profiling_cmd_name_for_redirectable_block(const parse_node_t &node,
const parse_node_tree_t &tree,
const wcstring &src) {
assert(specific_statement_type_is_redirectable_block(node));
assert(node.has_source());
2014-03-31 17:01:39 +00:00
// Get the source for the block, and cut it at the next statement terminator.
const size_t src_start = node.source_start;
2018-01-16 06:13:37 +00:00
auto term = tree.find_child<g::end_command>(node);
assert(term.has_source() && term.source_range()->start >= src_start);
size_t src_len = term.source_range()->start - src_start;
2014-03-31 17:01:39 +00:00
wcstring result = wcstring(src, src_start, src_len);
result.append(L"...");
return result;
}
/// Get a redirection from stderr to stdout (i.e. 2>&1).
static redirection_spec_t get_stderr_merge() {
const wchar_t *stdout_fileno_str = L"1";
return redirection_spec_t{STDERR_FILENO, redirection_mode_t::fd, stdout_fileno_str};
}
parse_execution_context_t::parse_execution_context_t(parsed_source_ref_t pstree, parser_t *p,
job_lineage_t lineage)
: pstree(std::move(pstree)), parser(p), lineage(std::move(lineage)) {}
// Utilities
wcstring parse_execution_context_t::get_source(const parse_node_t &node) const {
return node.get_source(pstree->src);
}
tnode_t<g::plain_statement> parse_execution_context_t::infinite_recursive_statement_in_job_list(
tnode_t<g::job_list> job_list, wcstring *out_func_name) const {
// This is a bit fragile. It is a test to see if we are inside of function call, but not inside
// a block in that function call. If, in the future, the rules for what block scopes are pushed
// on function invocation changes, then this check will break.
const block_t *current = parser->block_at_index(0), *parent = parser->block_at_index(1);
bool is_within_function_call =
2019-12-22 23:37:14 +00:00
(current && parent && current->type() == block_type_t::top && parent->is_function_call());
if (!is_within_function_call) {
return {};
}
// Get the function name of the immediate block.
const wcstring &forbidden_function_name = parent->function_name;
// Get the first job in the job list.
tnode_t<g::job> first_job = job_list.try_get_child<g::job_conjunction, 1>().child<0>();
if (!first_job) {
return {};
}
// Here's the statement node we find that's infinite recursive.
tnode_t<grammar::plain_statement> infinite_recursive_statement;
// Ignore the jobs variable assigment and "time" prefixes.
tnode_t<g::statement> statement = first_job.child<2>();
tnode_t<g::job_continuation> continuation = first_job.child<3>();
const null_environment_t nullenv{};
2018-01-16 00:33:36 +00:00
while (statement) {
// Get the list of plain statements.
// Ignore statements with decorations like 'builtin' or 'command', since those
// are not infinite recursion. In particular that is what enables 'wrapper functions'.
2018-01-16 00:33:36 +00:00
tnode_t<g::plain_statement> plain_statement =
statement.try_get_child<g::decorated_statement, 0>()
.try_get_child<g::plain_statement, 0>();
if (plain_statement) {
maybe_t<wcstring> cmd = command_for_plain_statement(plain_statement, pstree->src);
2019-04-25 18:22:17 +00:00
if (cmd &&
expand_one(*cmd, {expand_flag::skip_cmdsubst, expand_flag::skip_variables}, nullenv,
nullptr) &&
2018-01-16 00:33:36 +00:00
cmd == forbidden_function_name) {
// This is it.
infinite_recursive_statement = plain_statement;
if (out_func_name != nullptr) {
2018-01-16 00:33:36 +00:00
*out_func_name = forbidden_function_name;
}
break;
}
}
2018-01-16 00:33:36 +00:00
statement = continuation.next_in_list<g::statement>();
}
2018-01-16 00:33:36 +00:00
return infinite_recursive_statement;
}
process_type_t parse_execution_context_t::process_type_for_command(
tnode_t<grammar::plain_statement> statement, const wcstring &cmd) const {
enum process_type_t process_type = process_type_t::external;
// Determine the process type, which depends on the statement decoration (command, builtin,
// etc).
enum parse_statement_decoration_t decoration = get_decoration(statement);
switch (decoration) {
case parse_statement_decoration_exec:
process_type = process_type_t::exec;
break;
case parse_statement_decoration_command:
process_type = process_type_t::external;
break;
case parse_statement_decoration_builtin:
process_type = process_type_t::builtin;
break;
case parse_statement_decoration_none:
if (function_exists(cmd, *parser)) {
process_type = process_type_t::function;
} else if (builtin_exists(cmd)) {
process_type = process_type_t::builtin;
} else {
process_type = process_type_t::external;
}
break;
}
return process_type;
}
maybe_t<eval_result_t> parse_execution_context_t::check_end_execution() const {
if (shell_is_exiting()) {
return eval_result_t::cancelled;
}
if (parser && parser->cancellation_signal) {
return eval_result_t::cancelled;
}
const auto &ld = parser->libdata();
if (ld.returning) {
return eval_result_t::control_flow;
}
if (ld.loop_status != loop_status_t::normals) {
return eval_result_t::control_flow;
}
return none();
}
/// Return whether the job contains a single statement, of block type, with no redirections.
2018-01-16 00:00:19 +00:00
bool parse_execution_context_t::job_is_simple_block(tnode_t<g::job> job_node) const {
tnode_t<g::statement> statement = job_node.child<2>();
// Must be no pipes.
if (job_node.child<3>().try_get_child<g::tok_pipe, 0>()) {
return false;
}
// Helper to check if an argument or redirection list has no redirections.
auto is_empty = [](tnode_t<g::arguments_or_redirections_list> lst) -> bool {
return !lst.next_in_list<g::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).
const parse_node_t &specific_statement = statement.get_child_node<0>();
switch (specific_statement.type) {
case symbol_block_statement:
return is_empty(statement.require_get_child<g::block_statement, 0>().child<3>());
case symbol_switch_statement:
return is_empty(statement.require_get_child<g::switch_statement, 0>().child<5>());
case symbol_if_statement:
return is_empty(statement.require_get_child<g::if_statement, 0>().child<3>());
case symbol_not_statement:
case symbol_decorated_statement:
// not block statements
return false;
default:
assert(0 && "Unexpected child block type");
return false;
}
}
eval_result_t parse_execution_context_t::run_if_statement(tnode_t<g::if_statement> statement,
const block_t *associated_block) {
eval_result_t result = eval_result_t::ok;
// We have a sequence of if clauses, with a final else, resulting in a single job list that we
// execute.
2018-01-14 10:02:02 +00:00
tnode_t<g::job_list> job_list_to_execute;
tnode_t<g::if_clause> if_clause = statement.child<0>();
tnode_t<g::else_clause> else_clause = statement.child<1>();
// We start with the 'if'.
trace_if_enabled(*parser, L"if");
for (;;) {
if (auto ret = check_end_execution()) {
result = *ret;
break;
}
// An if condition has a job and a "tail" of andor jobs, e.g. "foo ; and bar; or baz".
tnode_t<g::job_conjunction> condition_head = if_clause.child<1>();
2018-01-14 10:02:02 +00:00
tnode_t<g::andor_job_list> condition_boolean_tail = if_clause.child<3>();
// Check the condition and the tail. We treat eval_result_t::error here as failure, in
// accordance with historic behavior.
eval_result_t cond_ret = run_job_conjunction(condition_head, associated_block);
if (cond_ret == eval_result_t::ok) {
cond_ret = run_job_list(condition_boolean_tail, associated_block);
}
const bool take_branch =
(cond_ret == eval_result_t::ok) && parser->get_last_status() == EXIT_SUCCESS;
if (take_branch) {
// Condition succeeded.
2018-01-14 10:02:02 +00:00
job_list_to_execute = if_clause.child<4>();
break;
2018-01-14 10:02:02 +00:00
}
auto else_cont = else_clause.try_get_child<g::else_continuation, 1>();
if (!else_cont) {
// 'if' condition failed, no else clause, return 0, we're done.
parser->set_last_statuses(statuses_t::just(STATUS_CMD_OK));
break;
} else {
// We have an 'else continuation' (either else-if or else).
2018-01-14 10:02:02 +00:00
if (auto maybe_if_clause = else_cont.try_get_child<g::if_clause, 0>()) {
// it's an 'else if', go to the next one.
if_clause = maybe_if_clause;
2018-01-14 10:02:02 +00:00
else_clause = else_cont.try_get_child<g::else_clause, 1>();
assert(else_clause && "Expected to have an else clause");
trace_if_enabled(*parser, L"else if");
} else {
// It's the final 'else', we're done.
2018-01-14 10:02:02 +00:00
job_list_to_execute = else_cont.try_get_child<g::job_list, 1>();
assert(job_list_to_execute && "Should have a job list");
trace_if_enabled(*parser, L"else");
break;
}
}
}
// Execute any job list we got.
2018-01-16 00:08:06 +00:00
if (job_list_to_execute) {
block_t *ib = parser->push_block(block_t::if_block());
2018-01-16 00:08:06 +00:00
run_job_list(job_list_to_execute, ib);
if (auto ret = check_end_execution()) {
result = *ret;
}
parser->pop_block(ib);
} else {
2019-10-19 01:36:03 +00:00
// No job list means no successful conditions, so return 0 (issue #1443).
parser->set_last_statuses(statuses_t::just(STATUS_CMD_OK));
}
2014-03-31 17:01:39 +00:00
trace_if_enabled(*parser, L"end if");
// It's possible there's a last-minute cancellation (issue #1297).
if (auto ret = check_end_execution()) {
result = *ret;
}
// Otherwise, take the exit status of the job list. Reversal of issue #1061.
return result;
}
eval_result_t parse_execution_context_t::run_begin_statement(tnode_t<g::job_list> contents) {
// Basic begin/end block. Push a scope block, run jobs, pop it
trace_if_enabled(*parser, L"begin");
2019-12-22 23:37:14 +00:00
block_t *sb = parser->push_block(block_t::scope_block(block_type_t::begin));
eval_result_t ret = run_job_list(contents, sb);
parser->pop_block(sb);
trace_if_enabled(*parser, L"end begin");
return ret;
}
// Define a function.
eval_result_t parse_execution_context_t::run_function_statement(
tnode_t<grammar::block_statement> statement, tnode_t<grammar::function_header> header) {
// Get arguments.
wcstring_list_t arguments;
argument_node_list_t arg_nodes = header.descendants<g::argument>();
eval_result_t result = this->expand_arguments_from_nodes(arg_nodes, &arguments, failglob);
if (result != eval_result_t::ok) {
2016-10-31 03:26:10 +00:00
return result;
}
trace_if_enabled(*parser, L"function", arguments);
io_streams_t streams(0); // no limit on the amount of output from builtin_function()
int err = builtin_function(*parser, streams, arguments, pstree, statement);
parser->set_last_statuses(statuses_t::just(err));
2016-10-31 03:26:10 +00:00
wcstring errtext = streams.err.contents();
if (!errtext.empty()) {
this->report_error(header, L"%ls", errtext.c_str());
result = eval_result_t::error;
}
2016-10-31 03:26:10 +00:00
return result;
}
eval_result_t parse_execution_context_t::run_block_statement(tnode_t<g::block_statement> statement,
const block_t *associated_block) {
2018-01-14 09:42:58 +00:00
tnode_t<g::block_header> bheader = statement.child<0>();
tnode_t<g::job_list> contents = statement.child<1>();
eval_result_t ret = eval_result_t::ok;
2018-01-14 09:42:58 +00:00
if (auto header = bheader.try_get_child<g::for_header, 0>()) {
ret = run_for_statement(header, contents);
} else if (auto header = bheader.try_get_child<g::while_header, 0>()) {
ret = run_while_statement(header, contents, associated_block);
2018-01-14 09:42:58 +00:00
} else if (auto header = bheader.try_get_child<g::function_header, 0>()) {
ret = run_function_statement(statement, header);
2018-01-14 09:42:58 +00:00
} else if (auto header = bheader.try_get_child<g::begin_header, 0>()) {
2018-01-20 22:05:34 +00:00
ret = run_begin_statement(contents);
2018-01-14 09:42:58 +00:00
} else {
FLOGF(error, L"Unexpected block header: %ls\n", bheader.node()->describe().c_str());
2018-01-14 09:42:58 +00:00
PARSER_DIE();
}
return ret;
}
eval_result_t parse_execution_context_t::run_for_statement(
2018-01-14 09:47:09 +00:00
tnode_t<grammar::for_header> header, tnode_t<grammar::job_list> block_contents) {
// Get the variable name: `for var_name in ...`. We expand the variable name. It better result
// in just one.
2018-01-14 09:47:09 +00:00
tnode_t<g::tok_string> var_name_node = header.child<1>();
wcstring for_var_name = get_source(var_name_node);
if (!expand_one(for_var_name, expand_flags_t{}, parser->vars(), parser->shared())) {
report_error(var_name_node, FAILED_EXPANSION_VARIABLE_NAME_ERR_MSG, for_var_name.c_str());
return eval_result_t::error;
}
// Get the contents to iterate over.
wcstring_list_t arguments;
eval_result_t ret = this->expand_arguments_from_nodes(get_argument_nodes(header.child<3>()),
&arguments, nullglob);
if (ret != eval_result_t::ok) {
return ret;
}
auto var = parser->vars().get(for_var_name, ENV_DEFAULT);
if (var && var->read_only()) {
report_error(var_name_node, L"You cannot use read-only variable '%ls' in a for loop",
for_var_name.c_str());
return eval_result_t::error;
}
int retval;
if (var) {
retval = parser->vars().set(for_var_name, ENV_LOCAL | ENV_USER, var->as_list());
} else {
retval = parser->vars().set_empty(for_var_name, ENV_LOCAL | ENV_USER);
}
assert(retval == ENV_OK);
if (!valid_var_name(for_var_name)) {
report_error(var_name_node, BUILTIN_ERR_VARNAME, L"for", for_var_name.c_str());
return eval_result_t::error;
}
trace_if_enabled(*parser, L"for", arguments);
block_t *fb = parser->push_block(block_t::for_block());
// Now drive the for loop.
for (const wcstring &val : arguments) {
if (auto reason = check_end_execution()) {
ret = *reason;
break;
}
int retval = parser->vars().set_one(for_var_name, ENV_DEFAULT | ENV_USER, val);
2017-12-21 21:53:18 +00:00
assert(retval == ENV_OK && "for loop variable should have been successfully set");
(void)retval;
auto &ld = parser->libdata();
ld.loop_status = loop_status_t::normals;
this->run_job_list(block_contents, fb);
if (check_end_execution() == eval_result_t::control_flow) {
// Handle break or continue.
bool do_break = (ld.loop_status == loop_status_t::breaks);
ld.loop_status = loop_status_t::normals;
if (do_break) {
break;
}
}
}
parser->pop_block(fb);
trace_if_enabled(*parser, L"end for");
return ret;
}
eval_result_t parse_execution_context_t::run_switch_statement(
2018-01-14 10:30:18 +00:00
tnode_t<grammar::switch_statement> statement) {
eval_result_t result = eval_result_t::ok;
// Get the switch variable.
2018-01-14 10:30:18 +00:00
tnode_t<grammar::argument> switch_value_n = statement.child<1>();
const wcstring switch_value = get_source(switch_value_n);
// Expand it. We need to offset any errors by the position of the string.
std::vector<completion_t> switch_values_expanded;
parse_error_list_t errors;
auto expand_ret =
expand_string(switch_value, &switch_values_expanded, expand_flag::no_descriptions,
parser->vars(), parser->shared(), &errors);
2018-01-14 10:30:18 +00:00
parse_error_offset_source_start(&errors, switch_value_n.source_range()->start);
2014-03-31 17:01:39 +00:00
switch (expand_ret) {
case expand_result_t::error: {
result = report_errors(errors);
break;
}
case expand_result_t::wildcard_no_match: {
2018-01-14 10:30:18 +00:00
result = report_unmatched_wildcard_error(switch_value_n);
break;
}
case expand_result_t::wildcard_match:
case expand_result_t::ok: {
break;
}
default: {
DIE("unexpected expand_string() return value");
break;
}
}
if (result == eval_result_t::ok && switch_values_expanded.size() > 1) {
result =
report_error(switch_value_n, _(L"switch: Expected at most one argument, got %lu\n"),
switch_values_expanded.size());
}
2014-03-31 17:01:39 +00:00
if (result != eval_result_t::ok) {
2016-10-31 03:26:10 +00:00
return result;
}
2014-03-31 17:01:39 +00:00
const wcstring &switch_value_expanded =
switch_values_expanded.size() == 1 ? switch_values_expanded.at(0).completion : L"";
block_t *sb = parser->push_block(block_t::switch_block());
2016-10-31 03:26:10 +00:00
// Expand case statements.
2018-01-14 10:30:18 +00:00
tnode_t<g::case_item_list> case_item_list = statement.child<3>();
tnode_t<g::case_item> matching_case_item{};
while (auto case_item = case_item_list.next_in_list<g::case_item>()) {
if (auto ret = check_end_execution()) {
result = *ret;
2016-10-31 03:26:10 +00:00
break;
}
2016-10-31 03:26:10 +00:00
// 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.
auto arg_nodes = get_argument_nodes(case_item.child<1>());
2016-10-31 03:26:10 +00:00
wcstring_list_t case_args;
eval_result_t case_result =
this->expand_arguments_from_nodes(arg_nodes, &case_args, failglob);
if (case_result == eval_result_t::ok) {
2018-01-14 10:30:18 +00:00
for (const wcstring &arg : case_args) {
2016-10-31 03:26:10 +00:00
// Unescape wildcards so they can be expanded again.
wcstring unescaped_arg = parse_util_unescape_wildcards(arg);
bool match = wildcard_match(switch_value_expanded, unescaped_arg);
// If this matched, we're done.
if (match) {
matching_case_item = case_item;
break;
}
}
}
2018-01-14 10:30:18 +00:00
if (matching_case_item) break;
2016-10-31 03:26:10 +00:00
}
2018-01-14 10:30:18 +00:00
if (matching_case_item) {
2016-10-31 03:26:10 +00:00
// Success, evaluate the job list.
assert(result == eval_result_t::ok && "Expected success");
2018-01-14 10:30:18 +00:00
auto job_list = matching_case_item.child<3>();
result = this->run_job_list(job_list, sb);
}
2016-10-31 03:26:10 +00:00
parser->pop_block(sb);
return result;
}
eval_result_t parse_execution_context_t::run_while_statement(tnode_t<grammar::while_header> header,
tnode_t<grammar::job_list> contents,
const block_t *associated_block) {
eval_result_t ret = eval_result_t::ok;
// "The exit status of the while loop shall be the exit status of the last compound-list-2
// executed, or zero if none was executed."
// Here are more detailed requirements:
// - If we execute the loop body zero times, or the loop body is empty, the status is success.
// - An empty loop body is treated as true, both in the loop condition and after loop exit.
// - The exit status of the last command is visible in the loop condition. (i.e. do not set the
// exit status to true BEFORE executing the loop condition).
// We achieve this by restoring the status if the loop condition fails, plus a special
// affordance for the first condition.
bool first_cond_check = true;
// The conditions of the while loop.
tnode_t<g::job_conjunction> condition_head = header.child<1>();
2018-01-14 10:41:37 +00:00
tnode_t<g::andor_job_list> condition_boolean_tail = header.child<3>();
trace_if_enabled(*parser, L"while");
// Run while the condition is true.
for (;;) {
// Save off the exit status if it came from the loop body. We'll restore it if the condition
// is false.
auto cond_saved_status =
first_cond_check ? statuses_t::just(EXIT_SUCCESS) : parser->get_last_statuses();
first_cond_check = false;
// Check the condition.
eval_result_t cond_ret = this->run_job_conjunction(condition_head, associated_block);
if (cond_ret == eval_result_t::ok) {
cond_ret = run_job_list(condition_boolean_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 != eval_result_t::ok) {
break;
} else if (parser->get_last_status() != EXIT_SUCCESS) {
parser->set_last_statuses(cond_saved_status);
break;
}
// Check cancellation.
if (auto reason = check_end_execution()) {
ret = *reason;
break;
}
// Push a while block and then check its cancellation reason.
auto &ld = parser->libdata();
ld.loop_status = loop_status_t::normals;
block_t *wb = parser->push_block(block_t::while_block());
2018-01-14 10:41:37 +00:00
this->run_job_list(contents, wb);
auto cancel_reason = this->check_end_execution();
parser->pop_block(wb);
if (cancel_reason == eval_result_t::control_flow) {
// Handle break or continue.
bool do_break = (ld.loop_status == loop_status_t::breaks);
ld.loop_status = loop_status_t::normals;
if (do_break) {
break;
} else {
continue;
}
}
// no_exec means that fish was invoked with -n or --no-execute. If set, we allow the loop to
// not-execute once so its contents can be checked, and then break.
2019-05-12 22:04:18 +00:00
if (no_exec()) {
break;
}
}
trace_if_enabled(*parser, L"end while");
return ret;
}
// Reports an error. Always returns eval_result_t::error, so you can assign the result to an
// 'errored' variable.
eval_result_t parse_execution_context_t::report_error(const parse_node_t &node, const wchar_t *fmt,
...) const {
// Create an error.
parse_error_list_t error_list = parse_error_list_t(1);
parse_error_t *error = &error_list.at(0);
error->source_start = node.source_start;
error->source_length = node.source_length;
error->code = parse_error_syntax; // hackish
va_list va;
va_start(va, fmt);
error->text = vformat_string(fmt, va);
va_end(va);
this->report_errors(error_list);
return eval_result_t::error;
}
eval_result_t parse_execution_context_t::report_errors(const parse_error_list_t &error_list) const {
if (!parser->cancellation_signal) {
if (error_list.empty()) {
FLOG(error, L"Error reported but no error text found.");
}
2014-03-31 17:01:39 +00:00
// Get a backtrace.
wcstring backtrace_and_desc;
parser->get_backtrace(pstree->src, error_list, backtrace_and_desc);
2014-03-31 17:01:39 +00:00
// Print it.
if (!should_suppress_stderr_for_tests()) {
std::fwprintf(stderr, L"%ls", backtrace_and_desc.c_str());
}
}
return eval_result_t::error;
}
/// Reports an unmatched wildcard error and returns eval_result_t::error.
eval_result_t parse_execution_context_t::report_unmatched_wildcard_error(
const parse_node_t &unmatched_wildcard) const {
parser->set_last_statuses(statuses_t::just(STATUS_UNMATCHED_WILDCARD));
report_error(unmatched_wildcard, WILDCARD_ERR_MSG, get_source(unmatched_wildcard).c_str());
return eval_result_t::error;
}
/// Handle the case of command not found.
eval_result_t parse_execution_context_t::handle_command_not_found(
const wcstring &cmd_str, tnode_t<g::plain_statement> statement, int err_code) {
// We couldn't find the specified command. This is a non-fatal error. We want to set the exit
// status to 127, which is the standard number used by other shells like bash and zsh.
const wchar_t *const cmd = cmd_str.c_str();
if (err_code != ENOENT) {
this->report_error(statement, _(L"The file '%ls' is not executable by this user"), cmd);
} else {
// Handle unrecognized commands with standard command not found handler that can make better
// error messages.
wcstring_list_t event_args;
{
auto args = get_argument_nodes(statement.child<1>());
eval_result_t arg_result =
this->expand_arguments_from_nodes(args, &event_args, failglob);
if (arg_result != eval_result_t::ok) {
return arg_result;
}
event_args.insert(event_args.begin(), cmd_str);
}
event_fire_generic(*parser, L"fish_command_not_found", &event_args);
// Here we want to report an error (so it shows a backtrace), but with no text.
this->report_error(statement, L"");
}
// Set the last proc status appropriately.
int status = err_code == ENOENT ? STATUS_CMD_UNKNOWN : STATUS_NOT_EXECUTABLE;
parser->set_last_statuses(statuses_t::just(status));
return eval_result_t::error;
}
eval_result_t parse_execution_context_t::expand_command(tnode_t<grammar::plain_statement> statement,
wcstring *out_cmd,
wcstring_list_t *out_args) const {
// Here we're expanding a command, for example $HOME/bin/stuff or $randomthing. The first
// completion becomes the command itself, everything after becomes arguments. Command
// substitutions are not supported.
parse_error_list_t errors;
// Get the unexpanded command string. We expect to always get it here.
wcstring unexp_cmd = *command_for_plain_statement(statement, pstree->src);
size_t pos_of_command_token = statement.child<0>().source_range()->start;
// Expand the string to produce completions, and report errors.
expand_result_t expand_err =
expand_to_command_and_args(unexp_cmd, parser->vars(), out_cmd, out_args, &errors);
if (expand_err == expand_result_t::error) {
parser->set_last_statuses(statuses_t::just(STATUS_ILLEGAL_CMD));
// 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.
for (auto &error : errors) error.source_start += pos_of_command_token;
return report_errors(errors);
} else if (expand_err == expand_result_t::wildcard_no_match) {
return report_unmatched_wildcard_error(statement);
}
assert(expand_err == expand_result_t::ok || expand_err == expand_result_t::wildcard_match);
// Complain if the resulting expansion was empty, or expanded to an empty string.
if (out_cmd->empty()) {
return this->report_error(statement, _(L"The expanded command was empty."));
}
return eval_result_t::ok;
}
/// Creates a 'normal' (non-block) process.
eval_result_t parse_execution_context_t::populate_plain_process(
job_t *job, process_t *proc, tnode_t<grammar::plain_statement> statement) {
assert(job != nullptr);
assert(proc != nullptr);
// We may decide that a command should be an implicit cd.
bool use_implicit_cd = false;
// Get the command and any arguments due to expanding the command.
wcstring cmd;
wcstring_list_t args_from_cmd_expansion;
auto ret = expand_command(statement, &cmd, &args_from_cmd_expansion);
if (ret != eval_result_t::ok) {
return ret;
}
assert(!cmd.empty() && "expand_command should not produce an empty command");
// Determine the process type.
enum process_type_t process_type = process_type_for_command(statement, cmd);
// Protect against exec with background processes running
if (process_type == process_type_t::exec && parser->is_interactive()) {
bool have_bg = false;
for (const auto &bg : parser->jobs()) {
// The assumption here is that if it is a foreground job,
// it's related to us.
// This stops us from asking if we're doing `exec` inside a function.
if (!bg->is_completed() && !bg->is_foreground()) {
have_bg = true;
break;
}
}
if (have_bg) {
uint64_t current_run_count = reader_run_count();
uint64_t &last_exec_run_count = parser->libdata().last_exec_run_counter;
if (isatty(STDIN_FILENO) && current_run_count - 1 != last_exec_run_count) {
reader_bg_job_warning(*parser);
last_exec_run_count = current_run_count;
return eval_result_t::error;
} else {
hup_background_jobs(*parser);
}
}
}
wcstring path_to_external_command;
if (process_type == process_type_t::external || process_type == process_type_t::exec) {
// Determine the actual command. This may be an implicit cd.
bool has_command = path_get_path(cmd, &path_to_external_command, parser->vars());
// If there was no command, then we care about the value of errno after checking for it, to
// distinguish between e.g. no file vs permissions problem.
const int no_cmd_err_code = errno;
// If the specified command does not exist, and is undecorated, try using an implicit cd.
if (!has_command && get_decoration(statement) == parse_statement_decoration_none) {
// Implicit cd requires an empty argument and redirection list.
tnode_t<g::arguments_or_redirections_list> args = statement.child<1>();
if (args_from_cmd_expansion.empty() && !args.try_get_child<g::argument, 0>() &&
!args.try_get_child<g::redirection, 0>()) {
// Ok, no arguments or redirections; check to see if the command is a directory.
use_implicit_cd =
path_as_implicit_cd(cmd, parser->vars().get_pwd_slash(), parser->vars())
.has_value();
}
}
if (!has_command && !use_implicit_cd) {
// No command.
return this->handle_command_not_found(cmd, statement, no_cmd_err_code);
}
}
// Produce the full argument list and the set of IO redirections.
wcstring_list_t cmd_args;
redirection_spec_list_t redirections;
if (use_implicit_cd) {
// Implicit cd is simple.
cmd_args = {L"cd", cmd};
path_to_external_command.clear();
// If we have defined a wrapper around cd, use it, otherwise use the cd builtin.
process_type =
function_exists(L"cd", *parser) ? process_type_t::function : process_type_t::builtin;
} else {
// Not implicit cd.
const globspec_t glob_behavior = (cmd == L"set" || cmd == L"count") ? nullglob : failglob;
// Form the list of arguments. The command is the first argument, followed by any arguments
// from expanding the command, followed by the argument nodes themselves. E.g. if the
// command is '$gco foo' and $gco is git checkout.
cmd_args.push_back(cmd);
cmd_args.insert(cmd_args.end(), args_from_cmd_expansion.begin(),
args_from_cmd_expansion.end());
argument_node_list_t arg_nodes = statement.descendants<g::argument>();
eval_result_t arg_result =
this->expand_arguments_from_nodes(arg_nodes, &cmd_args, glob_behavior);
if (arg_result != eval_result_t::ok) {
return arg_result;
}
// The set of IO redirections that we construct for the process.
if (!this->determine_redirections(statement.child<1>(), &redirections)) {
return eval_result_t::error;
}
// Determine the process type.
process_type = process_type_for_command(statement, cmd);
}
// Populate the process.
proc->type = process_type;
proc->set_argv(cmd_args);
proc->set_redirection_specs(std::move(redirections));
proc->actual_cmd = std::move(path_to_external_command);
return eval_result_t::ok;
}
// Determine the list of arguments, expanding stuff. Reports any errors caused by expansion. If we
// have a wildcard that could not be expanded, report the error and continue.
eval_result_t parse_execution_context_t::expand_arguments_from_nodes(
const argument_node_list_t &argument_nodes, wcstring_list_t *out_arguments,
globspec_t glob_behavior) {
// Get all argument nodes underneath the statement. We guess we'll have that many arguments (but
// may have more or fewer, if there are wildcards involved).
out_arguments->reserve(out_arguments->size() + argument_nodes.size());
std::vector<completion_t> arg_expanded;
for (const auto &arg_node : argument_nodes) {
// Expect all arguments to have source.
assert(arg_node.has_source());
const wcstring arg_str = arg_node.get_source(pstree->src);
// Expand this string.
parse_error_list_t errors;
arg_expanded.clear();
auto expand_ret = expand_string(arg_str, &arg_expanded, expand_flag::no_descriptions,
parser->vars(), parser->shared(), &errors);
parse_error_offset_source_start(&errors, arg_node.source_range()->start);
switch (expand_ret) {
case expand_result_t::error: {
return this->report_errors(errors);
}
case expand_result_t::wildcard_no_match: {
if (glob_behavior == failglob) {
// Report the unmatched wildcard error and stop processing.
report_unmatched_wildcard_error(arg_node);
return eval_result_t::error;
}
break;
}
case expand_result_t::wildcard_match:
case expand_result_t::ok: {
break;
}
default: {
DIE("unexpected expand_string() return value");
break;
}
}
// Now copy over any expanded arguments. Use std::move() to avoid extra allocations; this
// is called very frequently.
out_arguments->reserve(out_arguments->size() + arg_expanded.size());
for (completion_t &new_arg : arg_expanded) {
out_arguments->push_back(std::move(new_arg.completion));
}
}
// We may have received a cancellation during this expansion.
if (auto ret = check_end_execution()) {
return *ret;
}
return eval_result_t::ok;
}
bool parse_execution_context_t::determine_redirections(
tnode_t<g::arguments_or_redirections_list> node, redirection_spec_list_t *out_redirections) {
// Get all redirection nodes underneath the statement.
while (auto redirect_node = node.next_in_list<g::redirection>()) {
wcstring target; // file path or target fd
auto redirect = redirection_for_node(redirect_node, pstree->src, &target);
if (!redirect || !redirect->is_valid()) {
// TODO: figure out if this can ever happen. If so, improve this error message.
report_error(redirect_node, _(L"Invalid redirection: %ls"),
redirect_node.get_source(pstree->src).c_str());
return false;
}
// PCA: I can't justify this skip_variables flag. It was like this when I got here.
bool target_expanded =
2019-05-12 22:04:18 +00:00
expand_one(target, no_exec() ? expand_flag::skip_variables : expand_flags_t{},
parser->vars(), parser->shared());
if (!target_expanded || target.empty()) {
// TODO: Improve this error message.
report_error(redirect_node, _(L"Invalid redirection target: %ls"), target.c_str());
return false;
}
// Make a redirection spec from the redirect token.
assert(redirect && redirect->is_valid() && "expected to have a valid redirection");
redirection_spec_t spec{redirect->fd, redirect->mode, std::move(target)};
// Validate this spec.
if (spec.mode == redirection_mode_t::fd && !spec.is_close() && !spec.get_target_as_fd()) {
const wchar_t *fmt =
_(L"Requested redirection to '%ls', which is not a valid file descriptor");
report_error(redirect_node, fmt, spec.target.c_str());
return false;
}
out_redirections->push_back(std::move(spec));
if (redirect->stderr_merge) {
// This was a redirect like &> which also modifies stderr.
// Also redirect stderr to stdout.
out_redirections->push_back(get_stderr_merge());
}
}
return true;
}
eval_result_t parse_execution_context_t::populate_not_process(
job_t *job, process_t *proc, tnode_t<g::not_statement> not_statement) {
auto &flags = job->mut_flags();
flags.negate = !flags.negate;
auto optional_time = not_statement.require_get_child<g::optional_time, 2>();
if (optional_time.tag() == parse_optional_time_time) {
flags.has_time_prefix = true;
if (!job->mut_flags().foreground) {
this->report_error(not_statement, ERROR_TIME_BACKGROUND);
return eval_result_t::error;
}
}
return this->populate_job_process(
job, proc, not_statement.require_get_child<g::statement, 3>(),
not_statement.require_get_child<g::variable_assignments, 1>());
}
template <typename Type>
eval_result_t parse_execution_context_t::populate_block_process(job_t *job, process_t *proc,
tnode_t<g::statement> statement,
tnode_t<Type> specific_statement) {
// We handle block statements by creating process_type_t::block_node, that will bounce back to
// us when it's time to execute them.
UNUSED(job);
static_assert(Type::token == symbol_block_statement || Type::token == symbol_if_statement ||
Type::token == symbol_switch_statement,
"Invalid block process");
assert(statement && "statement missing");
assert(specific_statement && "specific_statement missing");
// The set of IO redirections that we construct for the process.
// TODO: fix this ugly find_child.
auto arguments = specific_statement.template find_child<g::arguments_or_redirections_list>();
redirection_spec_list_t redirections;
if (!this->determine_redirections(arguments, &redirections)) {
return eval_result_t::error;
}
proc->type = process_type_t::block_node;
proc->block_node_source = pstree;
proc->internal_block_node = statement;
proc->set_redirection_specs(std::move(redirections));
return eval_result_t::ok;
}
eval_result_t parse_execution_context_t::apply_variable_assignments(
process_t *proc, tnode_t<grammar::variable_assignments> variable_assignments,
const block_t **block) {
variable_assignment_node_list_t assignment_list =
get_variable_assignment_nodes(variable_assignments);
if (assignment_list.empty()) return eval_result_t::ok;
*block = parser->push_block(block_t::variable_assignment_block());
for (const auto &variable_assignment : assignment_list) {
const wcstring &source = variable_assignment.get_source(pstree->src);
auto equals_pos = variable_assignment_equals_pos(source);
assert(equals_pos);
const wcstring &variable_name = source.substr(0, *equals_pos);
const wcstring expression = source.substr(*equals_pos + 1);
std::vector<completion_t> expression_expanded;
parse_error_list_t errors;
// TODO this is mostly copied from expand_arguments_from_nodes, maybe extract to function
auto expand_ret =
expand_string(expression, &expression_expanded, expand_flag::no_descriptions,
parser->vars(), parser->shared(), &errors);
parse_error_offset_source_start(
&errors, variable_assignment.source_range()->start + *equals_pos + 1);
switch (expand_ret) {
case expand_result_t::error: {
this->report_errors(errors);
return eval_result_t::error;
}
case expand_result_t::wildcard_no_match: // nullglob (equivalent to set)
case expand_result_t::wildcard_match:
case expand_result_t::ok: {
break;
}
default: {
DIE("unexpected expand_string() return value");
break;
}
}
wcstring_list_t vals;
for (auto &completion : expression_expanded) {
vals.emplace_back(std::move(completion.completion));
}
if (proc) proc->variable_assignments.push_back({variable_name, vals});
parser->vars().set(variable_name, ENV_LOCAL | ENV_EXPORT, std::move(vals));
}
return eval_result_t::ok;
}
eval_result_t parse_execution_context_t::populate_job_process(
job_t *job, process_t *proc, tnode_t<grammar::statement> statement,
tnode_t<grammar::variable_assignments> variable_assignments) {
// Get the "specific statement" which is boolean / block / if / switch / decorated.
const parse_node_t &specific_statement = statement.get_child_node<0>();
const block_t *block = nullptr;
eval_result_t result = this->apply_variable_assignments(proc, variable_assignments, &block);
cleanup_t scope([&]() {
if (block) parser->pop_block(block);
});
if (result != eval_result_t::ok) return eval_result_t::error;
switch (specific_statement.type) {
case symbol_not_statement: {
result = this->populate_not_process(job, proc, {&tree(), &specific_statement});
break;
}
case symbol_block_statement:
result = this->populate_block_process(
job, proc, statement, tnode_t<g::block_statement>(&tree(), &specific_statement));
break;
case symbol_if_statement:
result = this->populate_block_process(
job, proc, statement, tnode_t<g::if_statement>(&tree(), &specific_statement));
break;
case symbol_switch_statement:
result = this->populate_block_process(
job, proc, statement, tnode_t<g::switch_statement>(&tree(), &specific_statement));
break;
case symbol_decorated_statement: {
// Get the plain statement. It will pull out the decoration itself.
tnode_t<g::decorated_statement> dec_stat{&tree(), &specific_statement};
auto plain_statement = dec_stat.find_child<g::plain_statement>();
result = this->populate_plain_process(job, proc, plain_statement);
break;
}
default: {
FLOGF(error, L"'%ls' not handled by new parser yet.",
2019-06-04 03:30:48 +00:00
specific_statement.describe().c_str());
PARSER_DIE();
break;
}
}
return result;
}
eval_result_t parse_execution_context_t::populate_job_from_job_node(
job_t *j, tnode_t<grammar::job> job_node, const block_t *associated_block) {
UNUSED(associated_block);
// Tell the job what its command is.
j->set_command(get_source(job_node));
// We are going to construct process_t structures for every statement in the job. Get the first
// statement.
tnode_t<g::optional_time> optional_time = job_node.child<0>();
tnode_t<g::variable_assignments> variable_assignments = job_node.child<1>();
tnode_t<g::statement> statement = job_node.child<2>();
// Create processes. Each one may fail.
process_list_t processes;
processes.emplace_back(new process_t());
if (optional_time.tag() == parse_optional_time_time) {
j->mut_flags().has_time_prefix = true;
if (job_node_is_background(job_node)) {
this->report_error(job_node, ERROR_TIME_BACKGROUND);
return eval_result_t::error;
}
}
eval_result_t result =
this->populate_job_process(j, processes.back().get(), statement, variable_assignments);
// Construct process_ts for job continuations (pipelines), by walking the list until we hit the
// terminal (empty) job continuation.
tnode_t<g::job_continuation> job_cont = job_node.child<3>();
assert(job_cont);
while (auto pipe = job_cont.try_get_child<g::tok_pipe, 0>()) {
if (result != eval_result_t::ok) {
break;
}
auto variable_assignments = job_cont.require_get_child<g::variable_assignments, 2>();
auto statement = job_cont.require_get_child<g::statement, 3>();
// Handle the pipe, whose fd may not be the obvious stdout.
auto parsed_pipe = pipe_or_redir_t::from_string(get_source(pipe));
assert(parsed_pipe.has_value() && parsed_pipe->is_pipe && "Failed to parse valid pipe");
if (!parsed_pipe->is_valid()) {
result = report_error(pipe, ILLEGAL_FD_ERR_MSG, get_source(pipe).c_str());
break;
}
processes.back()->pipe_write_fd = parsed_pipe->fd;
if (parsed_pipe->stderr_merge) {
// This was a pipe like &| which redirects both stdout and stderr.
// Also redirect stderr to stdout.
auto specs = processes.back()->redirection_specs();
specs.push_back(get_stderr_merge());
processes.back()->set_redirection_specs(std::move(specs));
}
// Store the new process (and maybe with an error).
processes.emplace_back(new process_t());
result =
this->populate_job_process(j, processes.back().get(), statement, variable_assignments);
// Get the next continuation.
job_cont = job_cont.require_get_child<g::job_continuation, 4>();
assert(job_cont);
}
// Inform our processes of who is first and last
processes.front()->is_first_in_job = true;
processes.back()->is_last_in_job = true;
// Return what happened.
if (result == eval_result_t::ok) {
// Link up the processes.
2016-10-23 20:58:12 +00:00
assert(!processes.empty()); //!OCLINT(multiple unary operator)
j->processes = std::move(processes);
}
return result;
}
static bool remove_job(parser_t &parser, job_t *job) {
for (auto j = parser.jobs().begin(); j != parser.jobs().end(); ++j) {
if (j->get() == job) {
parser.jobs().erase(j);
return true;
}
}
return false;
}
eval_result_t parse_execution_context_t::run_1_job(tnode_t<g::job> job_node,
const block_t *associated_block) {
if (auto ret = check_end_execution()) {
return *ret;
}
// Get terminal modes.
struct termios tmodes = {};
if (parser->is_interactive() && tcgetattr(STDIN_FILENO, &tmodes)) {
// Need real error handling here.
wperror(L"tcgetattr");
return eval_result_t::error;
}
// Increment the eval_level for the duration of this command.
scoped_push<int> saved_eval_level(&parser->eval_level, parser->eval_level + 1);
2014-03-31 17:01:39 +00:00
// Save the node index.
scoped_push<tnode_t<grammar::job>> saved_node(&executing_job_node, job_node);
// Profiling support.
long long start_time = 0, parse_time = 0, exec_time = 0;
profile_item_t *profile_item = this->parser->create_profile_item();
if (profile_item != nullptr) {
start_time = get_time();
}
// When we encounter a block construct (e.g. while loop) in the general case, we create a "block
// process" containing its node. This allows us to handle block-level redirections.
// However, if there are no redirections, then we can just jump into the block directly, which
// is significantly faster.
if (job_is_simple_block(job_node)) {
tnode_t<g::optional_time> optional_time = job_node.child<0>();
cleanup_t timer = push_timer(optional_time.tag() == parse_optional_time_time);
tnode_t<g::variable_assignments> variable_assignments = job_node.child<1>();
const block_t *block = nullptr;
eval_result_t result =
this->apply_variable_assignments(nullptr, variable_assignments, &block);
cleanup_t scope([&]() {
if (block) parser->pop_block(block);
});
tnode_t<g::statement> statement = job_node.child<2>();
const parse_node_t &specific_statement = statement.get_child_node<0>();
assert(specific_statement_type_is_redirectable_block(specific_statement));
if (result == eval_result_t::ok) {
switch (specific_statement.type) {
case symbol_block_statement: {
result =
this->run_block_statement({&tree(), &specific_statement}, associated_block);
break;
}
case symbol_if_statement: {
result =
this->run_if_statement({&tree(), &specific_statement}, associated_block);
break;
}
case symbol_switch_statement: {
result = this->run_switch_statement({&tree(), &specific_statement});
break;
}
default: {
// Other types should be impossible due to the
// specific_statement_type_is_redirectable_block check.
PARSER_DIE();
break;
}
}
}
2014-03-31 17:01:39 +00:00
if (profile_item != nullptr) {
// Block-types profile a little weird. They have no 'parse' time, and their command is
// just the block type.
exec_time = get_time();
profile_item->level = parser->eval_level;
profile_item->parse = 0;
profile_item->exec = static_cast<int>(exec_time - start_time);
profile_item->cmd = profiling_cmd_name_for_redirectable_block(
specific_statement, this->tree(), this->pstree->src);
profile_item->skipped = false;
}
2014-03-31 17:01:39 +00:00
return result;
}
const auto &ld = parser->libdata();
auto job_control_mode = get_job_control_mode();
bool wants_job_control =
(job_control_mode == job_control_t::all) ||
((job_control_mode == job_control_t::interactive) && parser->is_interactive());
job_t::properties_t props{};
props.wants_terminal = wants_job_control && !ld.is_event;
props.skip_notification =
ld.is_subshell || ld.is_block || ld.is_event || !parser->is_interactive();
props.from_event_handler = ld.is_event;
shared_ptr<job_t> job = std::make_shared<job_t>(acquire_job_id(), props, this->lineage);
job->tmodes = tmodes;
job->mut_flags().foreground = !job_node_is_background(job_node);
job->mut_flags().job_control = wants_job_control;
// 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.
auto &libdata = parser->libdata();
const auto saved_caller_jid = libdata.caller_job_id;
Introduce the internal jobs for functions This PR is aimed at improving how job ids are assigned. In particular, previous to this commit, a job id would be consumed by functions (and thus aliases). Since it's usual to use functions as command wrappers this results in awkward job id assignments. For example if the user is like me and just made the jump from vim -> neovim then the user might create the following alias: ``` alias vim=nvim ``` Previous to this commit if the user ran `vim` after setting up this alias, backgrounded (^Z) and ran `jobs` then the output might be: ``` Job Group State Command 2 60267 stopped nvim $argv ``` If the user subsequently opened another vim (nvim) session, backgrounded and ran jobs then they might see what follows: ``` Job Group State Command 4 70542 stopped nvim $argv 2 60267 stopped nvim $argv ``` These job ids feel unnatural, especially when transitioning away from e.g. bash where job ids are sequentially incremented (and aliases/functions don't consume a job id). See #6053 for more details. As @ridiculousfish pointed out in https://github.com/fish-shell/fish-shell/issues/6053#issuecomment-559899400, we want to elide a job's job id if it corresponds to a single function in the foreground. This translates to the following prerequisites: - A job must correspond to a single process (i.e. the job continuation must be empty) - A job must be in the foreground (i.e. `&` wasn't appended) - The job's single process must resolve to a function invocation If all of these conditions are true then we should mark a job as "internal" and somehow remove it from consideration when any infrastructure tries to interact with jobs / job ids. I saw two paths to implement these requirements: - At the time of job creation calculate whether or not a job is "internal" and use a separate list of job ids to track their ids. Additionally introduce a new flag denoting that a job is internal so that e.g. `jobs` doesn't list internal jobs - I started implementing this route but quickly realized I was computing the same information that would be computed later on (e.g. "is this job a single process" and "is this jobs statement a function"). Specifically I was computing data that populate_job_process would end up computing later anyway. Additionally this added some weird complexities to the job system (after the change there were two job id lists AND an additional flag that had to be taken into consideration) - Once a function is about to be executed we release the current jobs job id if the prerequisites are satisfied (which at this point have been fully computed). - I opted for this solution since it seems cleaner. In this implementation "releasing a job id" is done by both calling `release_job_id` and by marking the internal job_id member variable to -1. The former operation allows subsequent child jobs to reuse that same job id (so e.g. the situation described in Motivation doesn't occur), and the latter ensures that no other job / job id infrastructure will interact with these jobs because valid jobs have positive job ids. The second operation causes job_id to become non-const which leads to the list of code changes outside of `exec.c` (i.e. a codemod from `job_t::job_id` -> `job_t::job_id()` and moving the old member variable to a non-const private `job_t::job_id_`) Note: Its very possible I missed something and setting the job id to -1 will break some other infrastructure, please let me know if so! I tried to run `make/ninja lint`, but a bunch of non-relevant issues appeared (e.g. `fatal error: 'config.h' file not found`). I did successfully clang-format (`git clang-format -f`) and run tests, though. This PR closes #6053.
2019-12-29 15:46:07 +00:00
libdata.caller_job_id = job->job_id();
// Populate the job. This may fail for reasons like command_not_found. If this fails, an error
// will have been printed.
eval_result_t pop_result =
2018-01-16 00:00:19 +00:00
this->populate_job_from_job_node(job.get(), job_node, associated_block);
Introduce the internal jobs for functions This PR is aimed at improving how job ids are assigned. In particular, previous to this commit, a job id would be consumed by functions (and thus aliases). Since it's usual to use functions as command wrappers this results in awkward job id assignments. For example if the user is like me and just made the jump from vim -> neovim then the user might create the following alias: ``` alias vim=nvim ``` Previous to this commit if the user ran `vim` after setting up this alias, backgrounded (^Z) and ran `jobs` then the output might be: ``` Job Group State Command 2 60267 stopped nvim $argv ``` If the user subsequently opened another vim (nvim) session, backgrounded and ran jobs then they might see what follows: ``` Job Group State Command 4 70542 stopped nvim $argv 2 60267 stopped nvim $argv ``` These job ids feel unnatural, especially when transitioning away from e.g. bash where job ids are sequentially incremented (and aliases/functions don't consume a job id). See #6053 for more details. As @ridiculousfish pointed out in https://github.com/fish-shell/fish-shell/issues/6053#issuecomment-559899400, we want to elide a job's job id if it corresponds to a single function in the foreground. This translates to the following prerequisites: - A job must correspond to a single process (i.e. the job continuation must be empty) - A job must be in the foreground (i.e. `&` wasn't appended) - The job's single process must resolve to a function invocation If all of these conditions are true then we should mark a job as "internal" and somehow remove it from consideration when any infrastructure tries to interact with jobs / job ids. I saw two paths to implement these requirements: - At the time of job creation calculate whether or not a job is "internal" and use a separate list of job ids to track their ids. Additionally introduce a new flag denoting that a job is internal so that e.g. `jobs` doesn't list internal jobs - I started implementing this route but quickly realized I was computing the same information that would be computed later on (e.g. "is this job a single process" and "is this jobs statement a function"). Specifically I was computing data that populate_job_process would end up computing later anyway. Additionally this added some weird complexities to the job system (after the change there were two job id lists AND an additional flag that had to be taken into consideration) - Once a function is about to be executed we release the current jobs job id if the prerequisites are satisfied (which at this point have been fully computed). - I opted for this solution since it seems cleaner. In this implementation "releasing a job id" is done by both calling `release_job_id` and by marking the internal job_id member variable to -1. The former operation allows subsequent child jobs to reuse that same job id (so e.g. the situation described in Motivation doesn't occur), and the latter ensures that no other job / job id infrastructure will interact with these jobs because valid jobs have positive job ids. The second operation causes job_id to become non-const which leads to the list of code changes outside of `exec.c` (i.e. a codemod from `job_t::job_id` -> `job_t::job_id()` and moving the old member variable to a non-const private `job_t::job_id_`) Note: Its very possible I missed something and setting the job id to -1 will break some other infrastructure, please let me know if so! I tried to run `make/ninja lint`, but a bunch of non-relevant issues appeared (e.g. `fatal error: 'config.h' file not found`). I did successfully clang-format (`git clang-format -f`) and run tests, though. This PR closes #6053.
2019-12-29 15:46:07 +00:00
assert(libdata.caller_job_id == job->job_id() && "Caller job ID unexpectedly changed");
parser->libdata().caller_job_id = saved_caller_jid;
// Store time it took to 'parse' the command.
if (profile_item != nullptr) {
parse_time = get_time();
}
// Clean up the job on failure or cancellation.
if (pop_result == eval_result_t::ok) {
// Success. Give the job to the parser - it will clean it up.
parser->job_add(job);
// Check to see if this contained any external commands.
bool job_contained_external_command = false;
for (const auto &proc : job->processes) {
if (proc->type == process_type_t::external) {
job_contained_external_command = true;
break;
}
}
// Actually execute the job.
if (!exec_job(*this->parser, job, lineage)) {
remove_job(*this->parser, job.get());
}
2019-10-22 00:21:40 +00:00
// Update universal variables on external conmmands.
// TODO: justify this, why not on every command?
if (job_contained_external_command) {
parser->vars().universal_barrier();
}
}
if (profile_item != nullptr) {
exec_time = get_time();
profile_item->level = parser->eval_level;
profile_item->parse = static_cast<int>(parse_time - start_time);
profile_item->exec = static_cast<int>(exec_time - parse_time);
profile_item->cmd = job ? job->command() : wcstring();
profile_item->skipped = (pop_result != eval_result_t::ok);
}
job_reap(*parser, false); // clean up jobs
return pop_result;
}
eval_result_t parse_execution_context_t::run_job_conjunction(
tnode_t<grammar::job_conjunction> job_expr, const block_t *associated_block) {
eval_result_t result = eval_result_t::ok;
tnode_t<g::job_conjunction> cursor = job_expr;
// continuation is the parent of the cursor
tnode_t<g::job_conjunction_continuation> continuation;
while (cursor) {
if (auto reason = check_end_execution()) {
result = *reason;
break;
}
bool skip = false;
if (continuation) {
// Check the conjunction type.
2019-12-20 04:41:53 +00:00
parse_job_decoration_t conj = bool_statement_type(continuation);
2019-12-29 22:25:42 +00:00
assert((conj == parse_job_decoration_and || conj == parse_job_decoration_or) &&
"Unexpected conjunction");
skip = should_skip(conj);
}
if (!skip) {
result = run_1_job(cursor.child<0>(), associated_block);
}
continuation = cursor.child<1>();
cursor = continuation.try_get_child<g::job_conjunction, 2>();
}
return result;
}
2019-12-20 04:41:53 +00:00
bool parse_execution_context_t::should_skip(parse_job_decoration_t type) const {
switch (type) {
2019-12-20 04:41:53 +00:00
case parse_job_decoration_and:
// AND. Skip if the last job failed.
return parser->get_last_status() != 0;
2019-12-20 04:41:53 +00:00
case parse_job_decoration_or:
// OR. Skip if the last job succeeded.
return parser->get_last_status() == 0;
default:
return false;
}
}
2018-01-16 00:08:06 +00:00
template <typename Type>
eval_result_t parse_execution_context_t::run_job_list(tnode_t<Type> job_list,
const block_t *associated_block) {
// We handle both job_list and andor_job_list uniformly.
2018-01-16 00:08:06 +00:00
static_assert(Type::token == symbol_job_list || Type::token == symbol_andor_job_list,
"Not a job list");
eval_result_t result = eval_result_t::ok;
while (auto job_conj = job_list.template next_in_list<g::job_conjunction>()) {
if (auto reason = check_end_execution()) {
result = *reason;
break;
}
// Maybe skip the job if it has a leading and/or.
// Skipping is treated as success.
if (should_skip(get_decorator(job_conj))) {
result = eval_result_t::ok;
} else {
result = this->run_job_conjunction(job_conj, associated_block);
}
}
// Returns the result of the last job executed or skipped.
return result;
}
eval_result_t parse_execution_context_t::eval_node(tnode_t<g::statement> statement,
const block_t *associated_block) {
assert(statement && "Empty node in eval_node");
assert(statement.matches_node_tree(tree()) && "statement has unexpected tree");
enum eval_result_t status = eval_result_t::ok;
if (auto block = statement.try_get_child<g::block_statement, 0>()) {
status = this->run_block_statement(block, associated_block);
} else if (auto ifstat = statement.try_get_child<g::if_statement, 0>()) {
status = this->run_if_statement(ifstat, associated_block);
} else if (auto switchstat = statement.try_get_child<g::switch_statement, 0>()) {
status = this->run_switch_statement(switchstat);
} else {
FLOGF(error, L"Unexpected node %ls found in %s", statement.node()->describe().c_str(),
2019-06-04 03:30:48 +00:00
__FUNCTION__);
abort();
}
return status;
}
eval_result_t parse_execution_context_t::eval_node(tnode_t<g::job_list> job_list,
const block_t *associated_block) {
// Apply this block IO for the duration of this function.
assert(job_list && "Empty node in eval_node");
assert(job_list.matches_node_tree(tree()) && "job_list has unexpected tree");
assert(associated_block && "Null block");
// Check for infinite recursion: a function which immediately calls itself..
wcstring func_name;
auto infinite_recursive_node =
this->infinite_recursive_statement_in_job_list(job_list, &func_name);
if (infinite_recursive_node) {
// We have an infinite recursion.
return this->report_error(infinite_recursive_node, INFINITE_FUNC_RECURSION_ERR_MSG,
func_name.c_str());
}
// Check for stack overflow. The TOP check ensures we only do this for function calls.
2019-12-22 23:37:14 +00:00
if (associated_block->type() == block_type_t::top && parser->function_stack_is_overflowing()) {
return this->report_error(job_list, CALL_STACK_LIMIT_EXCEEDED_ERR_MSG);
}
return this->run_job_list(job_list, associated_block);
}
int parse_execution_context_t::line_offset_of_node(tnode_t<g::job> node) {
// If we're not executing anything, return -1.
if (!node) {
return -1;
}
// If for some reason we're executing a node without source, return -1.
auto range = node.source_range();
if (!range) {
return -1;
}
return this->line_offset_of_character_at_offset(range->start);
}
2014-03-31 17:01:39 +00:00
int parse_execution_context_t::line_offset_of_character_at_offset(size_t offset) {
// Count the number of newlines, leveraging our cache.
assert(offset <= pstree->src.size());
// Easy hack to handle 0.
if (offset == 0) {
return 0;
}
2014-03-31 17:01:39 +00:00
// We want to return (one plus) the number of newlines at offsets less than the given offset.
// cached_lineno_count is the number of newlines at indexes less than cached_lineno_offset.
const wchar_t *str = pstree->src.c_str();
if (offset > cached_lineno_offset) {
size_t i;
for (i = cached_lineno_offset; i < offset && str[i] != L'\0'; i++) {
// Add one for every newline we find in the range [cached_lineno_offset, offset).
if (str[i] == L'\n') {
cached_lineno_count++;
}
}
cached_lineno_offset =
i; // note: i, not offset, in case offset is beyond the length of the string
} else if (offset < cached_lineno_offset) {
// Subtract one for every newline we find in the range [offset, cached_lineno_offset).
for (size_t i = offset; i < cached_lineno_offset; i++) {
if (str[i] == L'\n') {
cached_lineno_count--;
}
}
cached_lineno_offset = offset;
}
return cached_lineno_count;
}
int parse_execution_context_t::get_current_line_number() {
int line_number = -1;
int line_offset = this->line_offset_of_node(this->executing_job_node);
if (line_offset >= 0) {
// The offset is 0 based; the number is 1 based.
line_number = line_offset + 1;
}
return line_number;
}
int parse_execution_context_t::get_current_source_offset() const {
int result = -1;
if (executing_job_node) {
if (auto range = executing_job_node.source_range()) {
result = static_cast<int>(range->start);
}
}
return result;
}