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<grammar::freestanding_argument_list> arg_list(&tree, &tree.at(0)); while (auto arg = arg_list.next_in_list<grammar::argument>()) { 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; }
bool parser_t::detect_errors_in_argument_list(const wcstring &arg_list_src, wcstring *out, const wchar_t *prefix) { bool errored = false; parse_error_list_t errors; /* Use empty string for the prefix if it's NULL */ if (prefix == NULL) { prefix = L""; } /* 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 */ assert(! tree.empty()); const parse_node_t *arg_list = &tree.at(0); assert(arg_list->type == symbol_freestanding_argument_list); /* Extract arguments from it */ while (arg_list != NULL && ! errored) { const parse_node_t *arg_node = tree.next_node_in_node_list(*arg_list, symbol_argument, &arg_list); if (arg_node != NULL) { const wcstring arg_src = arg_node->get_source(arg_list_src); if (parse_util_detect_errors_in_argument(*arg_node, 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; }
parser_test_error_bits_t parse_util_detect_errors(const wcstring &buff_src, parse_error_list_t *out_errors, bool allow_incomplete) { parse_node_tree_t node_tree; parse_error_list_t parse_errors; parser_test_error_bits_t res = 0; // Whether we encountered a parse error bool errored = false; // Whether we encountered an unclosed block // We detect this via an 'end_command' block without source bool has_unclosed_block = false; // Whether there's an unclosed quote, and therefore unfinished // This is only set if allow_incomplete is set bool has_unclosed_quote = false; // Parse the input string into a parse tree // Some errors are detected here bool parsed = parse_tree_from_string(buff_src, allow_incomplete ? parse_flag_leave_unterminated : parse_flag_none, &node_tree, &parse_errors); if (allow_incomplete) { for (size_t i=0; i < parse_errors.size(); i++) { if (parse_errors.at(i).code == parse_error_tokenizer_unterminated_quote) { // Remove this error, since we don't consider it a real error has_unclosed_quote = true; parse_errors.erase(parse_errors.begin() + i); i--; } } } // #1238: If the only error was unterminated quote, then consider this to have parsed successfully. A better fix would be to have parse_tree_from_string return this information directly (but it would be a shame to munge up its nice bool return). if (parse_errors.empty() && has_unclosed_quote) { parsed = true; } if (! parsed) { errored = true; } // has_unclosed_quote may only be set if allow_incomplete is true assert(! has_unclosed_quote || allow_incomplete); // Expand all commands // Verify 'or' and 'and' not used inside pipelines // Verify pipes via parser_is_pipe_forbidden // Verify return only within a function // Verify no variable expansions if (! errored) { const size_t node_tree_size = node_tree.size(); for (size_t i=0; i < node_tree_size; i++) { const parse_node_t &node = node_tree.at(i); if (node.type == symbol_end_command && ! node.has_source()) { // an 'end' without source is an unclosed block has_unclosed_block = true; } else if (node.type == symbol_boolean_statement) { // 'or' and 'and' can be in a pipeline, as long as they're first parse_bool_statement_type_t type = parse_node_tree_t::statement_boolean_type(node); if ((type == parse_bool_and || type == parse_bool_or) && node_tree.statement_is_in_pipeline(node, false /* don't count first */)) { errored = append_syntax_error(&parse_errors, node, EXEC_ERR_MSG, (type == parse_bool_and) ? L"and" : L"or"); } } else if (node.type == symbol_argument) { const wcstring arg_src = node.get_source(buff_src); res |= parse_util_detect_errors_in_argument(node, arg_src, &parse_errors); } else if (node.type == symbol_job) { if (node_tree.job_should_be_backgrounded(node)) { /* Disallow background in the following cases: foo & ; and bar foo & ; or bar if foo & ; end while foo & ; end */ const parse_node_t *job_parent = node_tree.get_parent(node); assert(job_parent != NULL); switch (job_parent->type) { case symbol_if_clause: case symbol_while_header: { assert(node_tree.get_child(*job_parent, 1) == &node); errored = append_syntax_error(&parse_errors, node, BACKGROUND_IN_CONDITIONAL_ERROR_MSG); break; } case symbol_job_list: { // This isn't very complete, e.g. we don't catch 'foo & ; not and bar' assert(node_tree.get_child(*job_parent, 0) == &node); const parse_node_t *next_job_list = node_tree.get_child(*job_parent, 1, symbol_job_list); assert(next_job_list != NULL); const parse_node_t *next_job = node_tree.next_node_in_node_list(*next_job_list, symbol_job, NULL); if (next_job != NULL) { const parse_node_t *next_statement = node_tree.get_child(*next_job, 0, symbol_statement); if (next_statement != NULL) { const parse_node_t *spec_statement = node_tree.get_child(*next_statement, 0); if (spec_statement && spec_statement->type == symbol_boolean_statement) { switch (parse_node_tree_t::statement_boolean_type(*spec_statement)) { // These are not allowed case parse_bool_and: errored = append_syntax_error(&parse_errors, *spec_statement, BOOL_AFTER_BACKGROUND_ERROR_MSG, L"and"); break; case parse_bool_or: errored = append_syntax_error(&parse_errors, *spec_statement, BOOL_AFTER_BACKGROUND_ERROR_MSG, L"or"); break; case parse_bool_not: // This one is OK break; } } } } break; } default: break; } } } else if (node.type == symbol_plain_statement) { // In a few places below, we want to know if we are in a pipeline const bool is_in_pipeline = node_tree.statement_is_in_pipeline(node, true /* count first */); // We need to know the decoration const enum parse_statement_decoration_t decoration = node_tree.decoration_for_plain_statement(node); // Check that we don't try to pipe through exec if (is_in_pipeline && decoration == parse_statement_decoration_exec) { errored = append_syntax_error(&parse_errors, node, EXEC_ERR_MSG, L"exec"); } wcstring command; if (node_tree.command_for_plain_statement(node, buff_src, &command)) { // Check that we can expand the command if (! expand_one(command, EXPAND_SKIP_CMDSUBST | EXPAND_SKIP_VARIABLES | EXPAND_SKIP_JOBS, NULL)) { // TODO: leverage the resulting errors errored = append_syntax_error(&parse_errors, node, ILLEGAL_CMD_ERR_MSG, command.c_str()); } // Check that pipes are sound if (! errored && parser_is_pipe_forbidden(command) && is_in_pipeline) { errored = append_syntax_error(&parse_errors, node, EXEC_ERR_MSG, command.c_str()); } // Check that we don't return from outside a function // But we allow it if it's 'return --help' if (! errored && command == L"return") { const parse_node_t *ancestor = &node; bool found_function = false; while (ancestor != NULL) { const parse_node_t *possible_function_header = node_tree.header_node_for_block_statement(*ancestor); if (possible_function_header != NULL && possible_function_header->type == symbol_function_header) { found_function = true; break; } ancestor = node_tree.get_parent(*ancestor); } if (! found_function && ! first_argument_is_help(node_tree, node, buff_src)) { errored = append_syntax_error(&parse_errors, node, INVALID_RETURN_ERR_MSG); } } // Check that we don't break or continue from outside a loop if (! errored && (command == L"break" || command == L"continue")) { // Walk up until we hit a 'for' or 'while' loop. If we hit a function first, stop the search; we can't break an outer loop from inside a function. // This is a little funny because we can't tell if it's a 'for' or 'while' loop from the ancestor alone; we need the header. That is, we hit a block_statement, and have to check its header. bool found_loop = false, end_search = false; const parse_node_t *ancestor = &node; while (ancestor != NULL && ! end_search) { const parse_node_t *loop_or_function_header = node_tree.header_node_for_block_statement(*ancestor); if (loop_or_function_header != NULL) { switch (loop_or_function_header->type) { case symbol_while_header: case symbol_for_header: // this is a loop header, so we can break or continue found_loop = true; end_search = true; break; case symbol_function_header: // this is a function header, so we cannot break or continue. We stop our search here. found_loop = false; end_search = true; break; default: // most likely begin / end style block, which makes no difference break; } } ancestor = node_tree.get_parent(*ancestor); } if (! found_loop && ! first_argument_is_help(node_tree, node, buff_src)) { errored = append_syntax_error(&parse_errors, node, (command == L"break" ? INVALID_BREAK_ERR_MSG : INVALID_CONTINUE_ERR_MSG)); } } // Check that we don't do an invalid builtin (#1252) if (! errored && decoration == parse_statement_decoration_builtin && ! builtin_exists(command)) { errored = append_syntax_error(&parse_errors, node, UNKNOWN_BUILTIN_ERR_MSG, command.c_str()); } } } } } if (errored) res |= PARSER_TEST_ERROR; if (has_unclosed_block || has_unclosed_quote) res |= PARSER_TEST_INCOMPLETE; if (out_errors) { out_errors->swap(parse_errors); } return res; }