// The fish parser. Contains functions for parsing and evaluating code. #include "config.h" // IWYU pragma: keep #include #include #include #include #include #include "common.h" #include "env.h" #include "event.h" #include "expand.h" #include "fallback.h" // IWYU pragma: keep #include "function.h" #include "intern.h" #include "parse_constants.h" #include "parse_execution.h" #include "parse_util.h" #include "parser.h" #include "proc.h" #include "reader.h" #include "sanity.h" #include "tnode.h" #include "wutil.h" // IWYU pragma: keep class io_chain_t; /// Error for evaluating in illegal scope. #define INVALID_SCOPE_ERR_MSG _(L"Tried to evaluate commands using invalid block type '%ls'") /// While block description. #define WHILE_BLOCK N_(L"'while' block") /// For block description. #define FOR_BLOCK N_(L"'for' block") /// Breakpoint block. #define BREAKPOINT_BLOCK N_(L"block created by breakpoint") /// If block description. #define IF_BLOCK N_(L"'if' conditional block") /// Function invocation block description. #define FUNCTION_CALL_BLOCK N_(L"function invocation block") /// Function invocation block description. #define FUNCTION_CALL_NO_SHADOW_BLOCK N_(L"function invocation block with no variable shadowing") /// Switch block description. #define SWITCH_BLOCK N_(L"'switch' block") /// Top block description. #define TOP_BLOCK N_(L"global root block") /// Command substitution block description. #define SUBST_BLOCK N_(L"command substitution block") /// Begin block description. #define BEGIN_BLOCK N_(L"'begin' unconditional block") /// Source block description. #define SOURCE_BLOCK N_(L"block created by the . builtin") /// Source block description. #define EVENT_BLOCK N_(L"event handler block") /// Unknown block description. #define UNKNOWN_BLOCK N_(L"unknown/invalid block") /// Data structure to describe a block type, like while blocks, command substitution blocks, etc. struct block_lookup_entry { // The block type id. The legal values are defined in parser.h. block_type_t type; // The name of the builtin that creates this type of block, if any. const wchar_t *name; // A description of this block type. const wchar_t *desc; }; /// List of all legal block types. static const struct block_lookup_entry block_lookup[] = { {WHILE, L"while", WHILE_BLOCK}, {FOR, L"for", FOR_BLOCK}, {IF, L"if", IF_BLOCK}, {FUNCTION_CALL, 0, FUNCTION_CALL_BLOCK}, {FUNCTION_CALL_NO_SHADOW, 0, FUNCTION_CALL_NO_SHADOW_BLOCK}, {SWITCH, L"switch", SWITCH_BLOCK}, {TOP, 0, TOP_BLOCK}, {SUBST, 0, SUBST_BLOCK}, {BEGIN, L"begin", BEGIN_BLOCK}, {SOURCE, L"source", SOURCE_BLOCK}, {EVENT, 0, EVENT_BLOCK}, {BREAKPOINT, L"breakpoint", BREAKPOINT_BLOCK}, {(block_type_t)0, 0, 0}}; // Given a file path, return something nicer. Currently we just "unexpand" tildes. static wcstring user_presentable_path(const wcstring &path) { return replace_home_directory_with_tilde(path); } parser_t::parser_t() : cancellation_requested(false), is_within_fish_initialization(false) {} // Out of line destructor to enable forward declaration of parse_execution_context_t parser_t::~parser_t() = default; static parser_t s_principal_parser; parser_t &parser_t::principal_parser() { ASSERT_IS_NOT_FORKED_CHILD(); ASSERT_IS_MAIN_THREAD(); return s_principal_parser; } void parser_t::set_is_within_fish_initialization(bool flag) { is_within_fish_initialization = flag; } void parser_t::skip_all_blocks() { // Tell all blocks to skip. // This may be called from a signal handler! s_principal_parser.cancellation_requested = true; } // Given a new-allocated block, push it onto our block stack, acquiring ownership void parser_t::push_block_int(block_t *new_current) { const enum block_type_t type = new_current->type(); new_current->src_lineno = parser_t::get_lineno(); const wchar_t *filename = parser_t::current_filename(); if (filename != NULL) { new_current->src_filename = intern(filename); } // New blocks should be skipped if the outer block is skipped, except TOP and SUBST block, which // open up new environments. const block_t *old_current = this->current_block(); new_current->skip = old_current && old_current->skip; // Type TOP and SUBST are never skipped. if (type == TOP || type == SUBST) { new_current->skip = false; } new_current->job = nullptr; new_current->loop_status = LOOP_NORMAL; // Push it onto our stack. This acquires ownership because of unique_ptr. this->block_stack.emplace_back(new_current); // Types TOP and SUBST are not considered blocks for the purposes of `status is-block`. if (type != TOP && type != SUBST) { is_block = true; } if (type == BREAKPOINT) { is_breakpoint = true; } if (new_current->type() != TOP) { env_push(type == FUNCTION_CALL); new_current->wants_pop_env = true; } } void parser_t::pop_block(const block_t *expected) { assert(expected == this->current_block()); if (block_stack.empty()) { debug(1, L"function %s called on empty block stack.", __func__); bugreport(); return; } // acquire ownership out of the block stack // this will trigger deletion when it goes out of scope std::unique_ptr old = std::move(block_stack.back()); block_stack.pop_back(); if (old->wants_pop_env) env_pop(); // Figure out if `status is-block` should consider us to be in a block now. bool new_is_block = false; for (const auto &b : block_stack) { const enum block_type_t type = b->type(); if (type != TOP && type != SUBST) { new_is_block = true; break; } } is_block = new_is_block; // Are we still in a breakpoint? bool new_is_breakpoint = false; for (const auto &b : block_stack) { const enum block_type_t type = b->type(); if (type == BREAKPOINT) { new_is_breakpoint = true; break; } } is_breakpoint = new_is_breakpoint; } const wchar_t *parser_t::get_block_desc(int block) const { for (size_t i = 0; block_lookup[i].desc; i++) { if (block_lookup[i].type == block) { return _(block_lookup[i].desc); } } return _(UNKNOWN_BLOCK); } #if 0 // TODO: Lint says this isn't used (which is true). Should this be removed? wcstring parser_t::block_stack_description() const { wcstring result; size_t idx = this->block_count(); size_t spaces = 0; while (idx--) { if (spaces > 0) { result.push_back(L'\n'); } for (size_t j = 0; j < spaces; j++) { result.push_back(L' '); } result.append(this->block_at_index(idx)->description()); spaces++; } return result; } #endif const block_t *parser_t::block_at_index(size_t idx) const { // Zero corresponds to the last element in our vector. size_t count = block_stack.size(); return idx < count ? block_stack.at(count - idx - 1).get() : NULL; } block_t *parser_t::block_at_index(size_t idx) { size_t count = block_stack.size(); return idx < count ? block_stack.at(count - idx - 1).get() : NULL; } block_t *parser_t::current_block() { return block_stack.empty() ? NULL : block_stack.back().get(); } void parser_t::forbid_function(const wcstring &function) { forbidden_function.push_back(function); } void parser_t::allow_function() { forbidden_function.pop_back(); } /// Print profiling information to the specified stream. static void print_profile(const std::vector> &items, FILE *out) { for (size_t pos = 0; pos < items.size(); pos++) { const profile_item_t *me, *prev; size_t i; int my_time; me = items.at(pos).get(); if (me->skipped) { continue; } my_time = me->parse + me->exec; for (i = pos + 1; i < items.size(); i++) { prev = items.at(i).get(); if (prev->skipped) { continue; } if (prev->level <= me->level) { break; } if (prev->level > me->level + 1) { continue; } my_time -= prev->parse + prev->exec; } if (me->cmd.size() == 0) { continue; } if (fwprintf(out, L"%d\t%d\t", my_time, me->parse + me->exec) < 0) { wperror(L"fwprintf"); return; } for (i = 0; i < me->level; i++) { if (fwprintf(out, L"-") < 0) { wperror(L"fwprintf"); return; } } if (fwprintf(out, L"> %ls\n", me->cmd.c_str()) < 0) { wperror(L"fwprintf"); return; } } } void parser_t::emit_profiling(const char *path) const { // Save profiling information. OK to not use CLO_EXEC here because this is called while fish is // dying (and hence will not fork). FILE *f = fopen(path, "w"); if (!f) { debug(1, _(L"Could not write profiling information to file '%s'"), path); } else { if (fwprintf(f, _(L"Time\tSum\tCommand\n"), profile_items.size()) < 0) { wperror(L"fwprintf"); } else { print_profile(profile_items, f); } if (fclose(f)) { wperror(L"fclose"); } } } void parser_t::expand_argument_list(const wcstring &arg_list_src, expand_flags_t eflags, std::vector *output_arg_list) { assert(output_arg_list != NULL); // Parse the string as an argument list. parse_node_tree_t tree; if (!parse_tree_from_string(arg_list_src, parse_flag_none, &tree, NULL /* errors */, symbol_freestanding_argument_list)) { // Failed to parse. Here we expect to have reported any errors in test_args. return; } // Get the root argument list and extract arguments from it. assert(!tree.empty()); //!OCLINT(multiple unary operator) tnode_t arg_list(&tree, &tree.at(0)); while (auto arg = arg_list.next_in_list()) { const wcstring arg_src = arg.get_source(arg_list_src); if (expand_string(arg_src, output_arg_list, eflags, NULL) == EXPAND_ERROR) { break; // failed to expand a string } } } wcstring parser_t::stack_trace() const { wcstring trace; this->stack_trace_internal(0, &trace); return trace; } void parser_t::stack_trace_internal(size_t block_idx, wcstring *buff) const { // Check if we should end the recursion. if (block_idx >= this->block_count()) return; const block_t *b = this->block_at_index(block_idx); if (b->type() == EVENT) { // This is an event handler. const event_block_t *eb = static_cast(b); wcstring description = event_get_desc(eb->event); append_format(*buff, _(L"in event handler: %ls\n"), description.c_str()); buff->append(L"\n"); // Stop recursing at event handler. No reason to believe that any other code is relevant. // // It might make sense in the future to continue printing the stack trace of the code that // invoked the event, if this is a programmatic event, but we can't currently detect that. return; } if (b->type() == FUNCTION_CALL || b->type() == FUNCTION_CALL_NO_SHADOW || b->type() == SOURCE || b->type() == SUBST) { // These types of blocks should be printed. int i; switch (b->type()) { case SOURCE: { const source_block_t *sb = static_cast(b); const wchar_t *source_dest = sb->source_file; append_format(*buff, _(L"from sourcing file %ls\n"), user_presentable_path(source_dest).c_str()); break; } case FUNCTION_CALL: case FUNCTION_CALL_NO_SHADOW: { const function_block_t *fb = static_cast(b); append_format(*buff, _(L"in function '%ls'\n"), fb->name.c_str()); break; } case SUBST: { append_format(*buff, _(L"in command substitution\n")); break; } default: { break; // can't get here } } const wchar_t *file = b->src_filename; if (file) { append_format(*buff, _(L"\tcalled on line %d of file %ls\n"), b->src_lineno, user_presentable_path(file).c_str()); } else if (is_within_fish_initialization) { append_format(*buff, _(L"\tcalled during startup\n")); } else { append_format(*buff, _(L"\tcalled on standard input\n")); } if (b->type() == FUNCTION_CALL) { const function_block_t *fb = static_cast(b); const process_t *const process = fb->process; if (process->argv(1)) { wcstring tmp; for (i = 1; process->argv(i); i++) { if (i > 1) tmp.push_back(L' '); tmp.append(process->argv(i)); } append_format(*buff, _(L"\twith parameter list '%ls'\n"), tmp.c_str()); } } append_format(*buff, L"\n"); } // Recursively print the next block. parser_t::stack_trace_internal(block_idx + 1, buff); } /// Returns the name of the currently evaluated function if we are currently evaluating a function, /// NULL otherwise. This is tested by moving down the block-scope-stack, checking every block if it /// is of type FUNCTION_CALL. If the caller doesn't specify a starting position in the stack we /// begin with the current block. const wchar_t *parser_t::is_function(size_t idx) const { // PCA: Have to make this a string somehow. ASSERT_IS_MAIN_THREAD(); const wchar_t *result = NULL; for (size_t block_idx = idx; block_idx < this->block_count(); block_idx++) { const block_t *b = this->block_at_index(block_idx); if (b->type() == FUNCTION_CALL || b->type() == FUNCTION_CALL_NO_SHADOW) { const function_block_t *fb = static_cast(b); result = fb->name.c_str(); break; } else if (b->type() == SOURCE) { // If a function sources a file, obviously that function's offset doesn't contribute. break; } } return result; } /// Return the function name for the specified stack frame. Default is zero (current frame). /// The special value zero means the function frame immediately above the closest breakpoint frame. const wchar_t *parser_t::get_function_name(int level) { if (level == 0) { // Return the function name for the level preceding the most recent breakpoint. If there // isn't one return the function name for the current level. int idx = 0; for (const auto &b : block_stack) { const enum block_type_t type = b->type(); if (type == BREAKPOINT) { return this->is_function(idx); } idx++; } return NULL; // couldn't find a breakpoint frame } else if (level == 1) { // Return the function name for the current level. return this->is_function(); } // Return the function name for the specific function stack frame. int idx = 0; for (const auto &b : block_stack) { const enum block_type_t type = b->type(); if (type == FUNCTION_CALL || type == FUNCTION_CALL_NO_SHADOW) { if (--level == 0) return this->is_function(idx); } idx++; } return NULL; // couldn't find that function level } int parser_t::get_lineno() const { int lineno = -1; if (execution_context) { lineno = execution_context->get_current_line_number(); // If we are executing a function, we have to add in its offset. const wchar_t *function_name = is_function(); if (function_name != NULL) { lineno += function_get_definition_lineno(function_name); } } return lineno; } const wchar_t *parser_t::current_filename() const { ASSERT_IS_MAIN_THREAD(); for (size_t i = 0; i < this->block_count(); i++) { const block_t *b = this->block_at_index(i); if (b->type() == FUNCTION_CALL || b->type() == FUNCTION_CALL_NO_SHADOW) { const function_block_t *fb = static_cast(b); return function_get_definition_file(fb->name); } else if (b->type() == SOURCE) { const source_block_t *sb = static_cast(b); return sb->source_file; } } // We query a global array for the current file name, but only do that if we are the principal // parser. if (this == &principal_parser()) { return reader_current_filename(); } return NULL; } wcstring parser_t::current_line() { if (!execution_context) { return wcstring(); } int source_offset = execution_context->get_current_source_offset(); if (source_offset < 0) { return wcstring(); } const int lineno = this->get_lineno(); const wchar_t *file = this->current_filename(); wcstring prefix; // If we are not going to print a stack trace, at least print the line number and filename. if (!shell_is_interactive() || is_function()) { if (file) { append_format(prefix, _(L"%ls (line %d): "), user_presentable_path(file).c_str(), lineno); } else if (is_within_fish_initialization) { append_format(prefix, L"%ls (line %d): ", _(L"Startup"), lineno); } else { append_format(prefix, L"%ls (line %d): ", _(L"Standard input"), lineno); } } bool is_interactive = shell_is_interactive(); bool skip_caret = is_interactive && !is_function(); // Use an error with empty text. assert(source_offset >= 0); parse_error_t empty_error = {}; empty_error.source_start = source_offset; wcstring line_info = empty_error.describe_with_prefix(execution_context->get_source(), prefix, is_interactive, skip_caret); if (!line_info.empty()) { line_info.push_back(L'\n'); } line_info.append(this->stack_trace()); return line_info; } void parser_t::job_add(shared_ptr job) { assert(job != NULL); assert(!job->processes.empty()); this->my_job_list.push_front(std::move(job)); } bool parser_t::job_remove(job_t *job) { for (auto iter = my_job_list.begin(); iter != my_job_list.end(); ++iter) { if (iter->get() == job) { my_job_list.erase(iter); return true; } } debug(1, _(L"Job inconsistency")); sanity_lose(); return false; } void parser_t::job_promote(job_t *job) { job_list_t::iterator loc; for (loc = my_job_list.begin(); loc != my_job_list.end(); ++loc) { if (loc->get() == job) { break; } } assert(loc != my_job_list.end()); // Move the job to the beginning. std::rotate(my_job_list.begin(), loc, my_job_list.end()); } job_t *parser_t::job_get(job_id_t id) { for (const auto &job : my_job_list) { if (id <= 0 || job->job_id == id) return job.get(); } return NULL; } job_t *parser_t::job_get_from_pid(pid_t pid) const { job_iterator_t jobs; job_t *job; pid_t pgid = getpgid(pid); if (pgid == -1) { return 0; } while ((job = jobs.next())) { if (job->pgid == pgid) { for (const process_ptr_t &p : job->processes) { if (p->pid == pid) { return job; } } } } return 0; } profile_item_t *parser_t::create_profile_item() { profile_item_t *result = nullptr; if (g_profiling_active) { profile_items.push_back(make_unique()); result = profile_items.back().get(); } return result; } int parser_t::eval(wcstring cmd, const io_chain_t &io, enum block_type_t block_type) { // Parse the source into a tree, if we can. parse_error_list_t error_list; parsed_source_ref_t ps = parse_source(cmd, parse_flag_none, &error_list); if (!ps) { // Get a backtrace. This includes the message. wcstring backtrace_and_desc; this->get_backtrace(cmd, error_list, backtrace_and_desc); // Print it. fwprintf(stderr, L"%ls\n", backtrace_and_desc.c_str()); return 1; } this->eval(ps, io, block_type); return 0; } void parser_t::eval(parsed_source_ref_t ps, const io_chain_t &io, enum block_type_t block_type) { CHECK_BLOCK(1); assert(block_type == TOP || block_type == SUBST); if (!ps->tree.empty()) { // Execute the first node. tnode_t start{&ps->tree, &ps->tree.front()}; this->eval_node(ps, start, io, block_type); } } template int parser_t::eval_node(parsed_source_ref_t ps, tnode_t node, const io_chain_t &io, enum block_type_t block_type) { static_assert( std::is_same::value || std::is_same::value, "Unexpected node type"); CHECK_BLOCK(1); // Handle cancellation requests. If our block stack is currently empty, then we already did // successfully cancel (or there was nothing to cancel); clear the flag. If our block stack is // not empty, we are still in the process of cancelling; refuse to evaluate anything. if (this->cancellation_requested) { if (!block_stack.empty()) { return 1; } this->cancellation_requested = false; } // Only certain blocks are allowed. if ((block_type != TOP) && (block_type != SUBST)) { debug(1, INVALID_SCOPE_ERR_MSG, parser_t::get_block_desc(block_type)); bugreport(); return 1; } job_reap(0); // not sure why we reap jobs here // Start it up scope_block_t *scope_block = this->push_block(block_type); // Create and set a new execution context. using exc_ctx_ref_t = std::unique_ptr; scoped_push exc(&execution_context, make_unique(ps, this)); int result = execution_context->eval_node(node, scope_block, io); exc.restore(); this->pop_block(scope_block); job_reap(0); // reap again return result; } // Explicit instantiations. TODO: use overloads instead? template int parser_t::eval_node(parsed_source_ref_t, tnode_t, const io_chain_t &, enum block_type_t); template int parser_t::eval_node(parsed_source_ref_t, tnode_t, const io_chain_t &, enum block_type_t); bool parser_t::detect_errors_in_argument_list(const wcstring &arg_list_src, wcstring *out, const wchar_t *prefix) const { bool errored = false; parse_error_list_t errors; // Use empty string for the prefix if it's NULL. if (!prefix) prefix = L""; //!OCLINT(parameter reassignment) // Parse the string as an argument list. parse_node_tree_t tree; if (!parse_tree_from_string(arg_list_src, parse_flag_none, &tree, &errors, symbol_freestanding_argument_list)) { // Failed to parse. errored = true; } if (!errored) { // Get the root argument list and extract arguments from it. assert(!tree.empty()); //!OCLINT(multiple unary operator) tnode_t arg_list(&tree, &tree.at(0)); while (auto arg = arg_list.next_in_list()) { const wcstring arg_src = arg.get_source(arg_list_src); if (parse_util_detect_errors_in_argument(arg, arg_src, &errors)) { errored = true; } } } if (!errors.empty() && out != NULL) { out->assign(errors.at(0).describe_with_prefix( arg_list_src, prefix, false /* not interactive */, false /* don't skip caret */)); } return errored; } void parser_t::get_backtrace(const wcstring &src, const parse_error_list_t &errors, wcstring &output) const { if (!errors.empty()) { const parse_error_t &err = errors.at(0); const bool is_interactive = shell_is_interactive(); // Determine if we want to try to print a caret to point at the source error. The // err.source_start <= src.size() check is due to the nasty way that slices work, which is // by rewriting the source. size_t which_line = 0; bool skip_caret = true; if (err.source_start != SOURCE_LOCATION_UNKNOWN && err.source_start <= src.size()) { // Determine which line we're on. which_line = 1 + std::count(src.begin(), src.begin() + err.source_start, L'\n'); // Don't include the caret if we're interactive, this is the first line of text, and our // source is at its beginning, because then it's obvious. skip_caret = (is_interactive && which_line == 1 && err.source_start == 0); } wcstring prefix; const wchar_t *filename = this->current_filename(); if (filename) { if (which_line > 0) { prefix = format_string(_(L"%ls (line %lu): "), user_presentable_path(filename).c_str(), which_line); } else { prefix = format_string(_(L"%ls: "), user_presentable_path(filename).c_str()); } } else { prefix = L"fish: "; } const wcstring description = err.describe_with_prefix(src, prefix, is_interactive, skip_caret); if (!description.empty()) { output.append(description); output.push_back(L'\n'); } output.append(this->stack_trace()); } } block_t::block_t(block_type_t t) : block_type(t) {} block_t::~block_t() = default; wcstring block_t::description() const { wcstring result; switch (this->type()) { case WHILE: { result.append(L"while"); break; } case FOR: { result.append(L"for"); break; } case IF: { result.append(L"if"); break; } case FUNCTION_CALL: { result.append(L"function_call"); break; } case FUNCTION_CALL_NO_SHADOW: { result.append(L"function_call_no_shadow"); break; } case SWITCH: { result.append(L"switch"); break; } case SUBST: { result.append(L"substitution"); break; } case TOP: { result.append(L"top"); break; } case BEGIN: { result.append(L"begin"); break; } case SOURCE: { result.append(L"source"); break; } case EVENT: { result.append(L"event"); break; } case BREAKPOINT: { result.append(L"breakpoint"); break; } } if (this->src_lineno >= 0) { append_format(result, L" (line %d)", this->src_lineno); } if (this->src_filename != NULL) { append_format(result, L" (file %ls)", this->src_filename); } return result; } // Various block constructors. if_block_t::if_block_t() : block_t(IF) {} event_block_t::event_block_t(const event_t &evt) : block_t(EVENT), event(evt) {} function_block_t::function_block_t(const process_t *p, wcstring n, bool shadows) : block_t(shadows ? FUNCTION_CALL : FUNCTION_CALL_NO_SHADOW), process(p), name(std::move(n)) {} source_block_t::source_block_t(const wchar_t *src) : block_t(SOURCE), source_file(src) {} for_block_t::for_block_t() : block_t(FOR) {} while_block_t::while_block_t() : block_t(WHILE) {} switch_block_t::switch_block_t() : block_t(SWITCH) {} scope_block_t::scope_block_t(block_type_t type) : block_t(type) { assert(type == BEGIN || type == TOP || type == SUBST); } breakpoint_block_t::breakpoint_block_t() : block_t(BREAKPOINT) {}