diff --git a/exec.cpp b/exec.cpp index e150723db..594a53857 100644 --- a/exec.cpp +++ b/exec.cpp @@ -577,6 +577,12 @@ static bool can_use_posix_spawn_for_job(const job_t *job, const process_t *proce /* What exec does if no_exec is set. This only has to handle block pushing and popping. See #624. */ static void exec_no_exec(parser_t &parser, const job_t *job) { + if (parser_use_ast()) + { + /* With the new parser, commands aren't responsible for pushing / popping blocks, so there's nothing to do */ + return; + } + /* Hack hack hack. If this is an 'end' job, then trigger a pop. If this is a job that would create a block, trigger a push. See #624 */ const process_t *p = job->first_process; if (p && p->type == INTERNAL_BUILTIN) diff --git a/parse_execution.cpp b/parse_execution.cpp index 137b7e001..0733fb8bb 100644 --- a/parse_execution.cpp +++ b/parse_execution.cpp @@ -11,6 +11,7 @@ #include "builtin.h" #include "parser.h" #include "expand.h" +#include "reader.h" #include "wutil.h" #include "exec.h" #include "path.h" @@ -47,7 +48,28 @@ node_offset_t parse_execution_context_t::get_offset(const parse_node_t &node) co bool parse_execution_context_t::should_cancel_execution(const block_t *block) const { - return block && (block->skip || block->loop_status != LOOP_NORMAL); + return cancellation_reason(block) != execution_cancellation_none; +} + +parse_execution_context_t::execution_cancellation_reason_t parse_execution_context_t::cancellation_reason(const block_t *block) const +{ + if (shell_is_exiting()) + { + return execution_cancellation_exit; + } + else if (block && block->loop_status != LOOP_NORMAL) + { + /* Nasty hack - break and continue set the 'skip' flag as well as the loop status flag. */ + return execution_cancellation_loop_control; + } + else if (block && block->skip) + { + return execution_cancellation_skip; + } + else + { + return execution_cancellation_none; + } } int parse_execution_context_t::run_if_statement(const parse_node_t &statement) @@ -229,17 +251,20 @@ int parse_execution_context_t::run_for_statement(const parse_node_t &header, con this->run_job_list(block_contents, fb); - /* Handle break or continue */ - if (fb->loop_status == LOOP_CONTINUE) + if (this->cancellation_reason(fb) == execution_cancellation_loop_control) { - /* Reset the loop state */ - fb->loop_status = LOOP_NORMAL; - fb->skip = false; - continue; - } - else if (fb->loop_status == LOOP_BREAK) - { - break; + /* Handle break or continue */ + if (fb->loop_status == LOOP_CONTINUE) + { + /* Reset the loop state */ + fb->loop_status = LOOP_NORMAL; + fb->skip = false; + continue; + } + else if (fb->loop_status == LOOP_BREAK) + { + break; + } } } @@ -374,17 +399,20 @@ int parse_execution_context_t::run_while_statement(const parse_node_t &header, c /* The block ought to go inside the loop (see #1212) */ this->run_job_list(block_contents, wb); - /* Handle break or continue */ - if (wb->loop_status == LOOP_CONTINUE) + if (this->cancellation_reason(wb) == execution_cancellation_loop_control) { - /* Reset the loop state */ - wb->loop_status = LOOP_NORMAL; - wb->skip = false; - continue; - } - else if (wb->loop_status == LOOP_BREAK) - { - break; + /* Handle break or continue */ + if (wb->loop_status == LOOP_CONTINUE) + { + /* Reset the loop state */ + wb->loop_status = LOOP_NORMAL; + wb->skip = false; + continue; + } + else if (wb->loop_status == LOOP_BREAK) + { + break; + } } } @@ -418,15 +446,129 @@ bool parse_execution_context_t::append_unmatched_wildcard_error(const parse_node return append_error(unmatched_wildcard, WILDCARD_ERR_MSG, get_source(unmatched_wildcard).c_str()); } +/* Handle the case of command not found */ +void parse_execution_context_t::handle_command_not_found(const wcstring &cmd_str, const parse_node_t &statement_node, int err_code) +{ + assert(statement_node.type == symbol_plain_statement); + + /* + We couldn't find the specified command. + What we want to happen now is that the + specified job won't get executed, and an + error message is printed on-screen, but + otherwise, the parsing/execution of the + file continues. Because of this, we don't + want to call error(), since that would stop + execution of the file. Instead we let + p->actual_command be 0 (null), which will + cause the job to silently not execute. We + also print an error message and set the + status to 127 (This is the standard number + for this, used by other shells like bash + and zsh). + */ + + const wchar_t * const cmd = cmd_str.c_str(); + const wchar_t * const equals_ptr = wcschr(cmd, L'='); + if (equals_ptr != NULL) + { + /* Try to figure out if this is a pure variable assignment (foo=bar), or if this appears to be running a command (foo=bar ruby...) */ + + const wcstring name_str = wcstring(cmd, equals_ptr - cmd); //variable name, up to the = + const wcstring val_str = wcstring(equals_ptr + 1); //variable value, past the = + + + const parse_node_tree_t::parse_node_list_t args = tree.find_nodes(statement_node, symbol_argument, 1); + + if (! args.empty()) + { + const wcstring argument = get_source(*args.at(0)); + + wcstring ellipsis_str = wcstring(1, ellipsis_char); + if (ellipsis_str == L"$") + ellipsis_str = L"..."; + + /* Looks like a command */ + debug(0, + _(L"Unknown command '%ls'. Did you mean to run %ls with a modified environment? Try 'env %ls=%ls %ls%ls'. See the help section on the set command by typing 'help set'."), + cmd, + argument.c_str(), + name_str.c_str(), + val_str.c_str(), + argument.c_str(), + ellipsis_str.c_str()); + } + else + { + debug(0, + COMMAND_ASSIGN_ERR_MSG, + cmd, + name_str.c_str(), + val_str.c_str()); + } + } + else if (cmd[0]==L'$' || cmd[0] == VARIABLE_EXPAND || cmd[0] == VARIABLE_EXPAND_SINGLE) + { + + const env_var_t val_wstr = env_get_string(cmd+1); + const wchar_t *val = val_wstr.missing() ? NULL : val_wstr.c_str(); + if (val) + { + debug(0, + _(L"Variables may not be used as commands. Instead, define a function like 'function %ls; %ls $argv; end' or use the eval builtin instead, like 'eval %ls'. See the help section for the function command by typing 'help function'."), + cmd+1, + val, + cmd, + cmd); + } + else + { + debug(0, + _(L"Variables may not be used as commands. Instead, define a function or use the eval builtin instead, like 'eval %ls'. See the help section for the function command by typing 'help function'."), + cmd, + cmd); + } + } + else if (wcschr(cmd, L'$')) + { + debug(0, + _(L"Commands may not contain variables. Use the eval builtin instead, like 'eval %ls'. See the help section for the eval command by typing 'help eval'."), + cmd, + cmd); + } + else if (err_code!=ENOENT) + { + debug(0, + _(L"The file '%ls' is not executable by this user"), + cmd?cmd:L"UNKNOWN"); + } + else + { + /* + Handle unrecognized commands with standard + command not found handler that can make better + error messages + */ + + wcstring_list_t event_args; + event_args.push_back(cmd_str); + event_fire_generic(L"fish_command_not_found", &event_args); + } + + /* Set the last proc status appropriately */ + proc_set_last_status(err_code==ENOENT?STATUS_UNKNOWN_COMMAND:STATUS_NOT_EXECUTABLE); +} /* Creates a 'normal' (non-block) process */ process_t *parse_execution_context_t::create_plain_process(job_t *job, const parse_node_t &statement) { + assert(statement.type == symbol_plain_statement); + bool errored = false; - /* Get the decoration */ - assert(statement.type == symbol_plain_statement); + /* We may decide that a command should be an implicit cd */ + bool use_implicit_cd = false; /* Get the command. We expect to always get it here. */ wcstring cmd; @@ -442,28 +584,7 @@ process_t *parse_execution_context_t::create_plain_process(job_t *job, const par if (errored) return NULL; - - /* The list of arguments. The command is the first argument. TODO: count hack */ - const parse_node_t *unmatched_wildcard = NULL; - wcstring_list_t argument_list = this->determine_arguments(statement, &unmatched_wildcard); - argument_list.insert(argument_list.begin(), cmd); - - /* If we were not able to expand any wildcards, here is the first one that failed */ - if (unmatched_wildcard != NULL) - { - job_set_flag(job, JOB_WILDCARD_ERROR, 1); - errored = append_unmatched_wildcard_error(*unmatched_wildcard); - } - - if (errored) - return NULL; - - /* The set of IO redirections that we construct for the process */ - io_chain_t process_io_chain; - errored = ! this->determine_io_chain(statement, &process_io_chain); - if (errored) - return NULL; - + /* Determine the process type, which depends on the statement decoration (command, builtin, etc) */ enum parse_statement_decoration_t decoration = tree.decoration_for_plain_statement(statement); enum process_type_t process_type = EXTERNAL; @@ -500,15 +621,71 @@ process_t *parse_execution_context_t::create_plain_process(job_t *job, const par wcstring actual_cmd; if (process_type == EXTERNAL) { - /* Determine the actual command. Need to support implicit cd here */ + /* Determine the actual command. This may be an implicit cd. */ bool has_command = path_get_path(cmd, &actual_cmd); - if (! has_command) + /* 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 && decoration == parse_statement_decoration_none) { - /* TODO: support fish_command_not_found, implicit cd, etc. here */ + /* Implicit cd requires an empty argument and redirection list */ + const parse_node_t *args = get_child(statement, 1, symbol_arguments_or_redirections_list); + if (args->child_count == 0) + { + /* Ok, no arguments or redirections; check to see if the first argument is a directory */ + wcstring implicit_cd_path; + use_implicit_cd = path_can_be_implicit_cd(cmd, &implicit_cd_path); + } + } + + if (! has_command && ! use_implicit_cd) + { + /* No command */ + this->handle_command_not_found(cmd, statement, no_cmd_err_code); errored = true; } } + if (errored) + return NULL; + + /* The argument list and set of IO redirections that we will construct for the process */ + wcstring_list_t argument_list; + io_chain_t process_io_chain; + if (use_implicit_cd) + { + /* Implicit cd is simple */ + argument_list.push_back(L"cd"); + argument_list.push_back(cmd); + actual_cmd.clear(); + + /* If we have defined a wrapper around cd, use it, otherwise use the cd builtin */ + process_type = function_exists(L"cd") ? INTERNAL_FUNCTION : INTERNAL_BUILTIN; + } + else + { + /* Form the list of arguments. The command is the first argument. TODO: count hack */ + const parse_node_t *unmatched_wildcard = NULL; + argument_list = this->determine_arguments(statement, &unmatched_wildcard); + argument_list.insert(argument_list.begin(), cmd); + + /* If we were not able to expand any wildcards, here is the first one that failed */ + if (unmatched_wildcard != NULL) + { + job_set_flag(job, JOB_WILDCARD_ERROR, 1); + errored = append_unmatched_wildcard_error(*unmatched_wildcard); + } + + if (errored) + return NULL; + + /* The set of IO redirections that we construct for the process */ + errored = ! this->determine_io_chain(statement, &process_io_chain); + if (errored) + return NULL; + } + /* Return the process, or NULL on error */ process_t *result = NULL; @@ -953,7 +1130,7 @@ int parse_execution_context_t::run_1_job(const parse_node_t &job_node, const blo profile_item->skipped = process_errored; } - /* Set the last status to 1 if the job could not be executed */ + /* Set the last status to 1 if the job could not be executed. TODO: Don't stomp STATUS_UNKNOWN_COMMAND / STATUS_NOT_EXECUTABLE */ if (process_errored) proc_set_last_status(1); const int ret = proc_get_last_status(); diff --git a/parse_execution.h b/parse_execution.h index 8d89158bd..f68cad5fd 100644 --- a/parse_execution.h +++ b/parse_execution.h @@ -34,11 +34,23 @@ class parse_execution_context_t /* Should I cancel? */ bool should_cancel_execution(const block_t *block) const; + /* Ways that we can stop executing a block. These are in a sort of ascending order of importance, e.g. `exit` should trump `break` */ + enum execution_cancellation_reason_t + { + execution_cancellation_none, + execution_cancellation_loop_control, + execution_cancellation_skip, + execution_cancellation_exit + }; + execution_cancellation_reason_t cancellation_reason(const block_t *block) const; + /* Report an error. Always returns true. */ bool append_error(const parse_node_t &node, const wchar_t *fmt, ...); /* Wildcard error helper */ bool append_unmatched_wildcard_error(const parse_node_t &unmatched_wildcard); + void handle_command_not_found(const wcstring &cmd, const parse_node_t &statement_node, int err_code); + /* Utilities */ wcstring get_source(const parse_node_t &node) const; const parse_node_t *get_child(const parse_node_t &parent, node_offset_t which, parse_token_type_t expected_type = token_type_invalid) const; diff --git a/parser.cpp b/parser.cpp index bd0471df0..6b7d19091 100644 --- a/parser.cpp +++ b/parser.cpp @@ -2740,7 +2740,7 @@ int parser_t::eval(const wcstring &cmd_str, const io_chain_t &io, enum block_typ while (tok_has_next(current_tokenizer) && !error_code && !sanity_check() && - !exit_status()) + !shell_is_exiting()) { this->eval_job(current_tokenizer); event_fire(NULL); @@ -2759,7 +2759,7 @@ int parser_t::eval(const wcstring &cmd_str, const io_chain_t &io, enum block_typ break; } - if ((!error_code) && (!exit_status()) && (!proc_get_last_status())) + if ((!error_code) && (!shell_is_exiting()) && (!proc_get_last_status())) { //debug( 2, L"Status %d\n", proc_get_last_status() ); diff --git a/reader.cpp b/reader.cpp index 0905b3792..f5ae62f48 100644 --- a/reader.cpp +++ b/reader.cpp @@ -2736,7 +2736,7 @@ static void reader_super_highlight_me_plenty(size_t match_highlight_pos) } -int exit_status() +bool shell_is_exiting() { if (get_is_interactive()) return job_list_is_empty() && data->end_loop; diff --git a/reader.h b/reader.h index b954c1bea..e028e2f03 100644 --- a/reader.h +++ b/reader.h @@ -217,7 +217,7 @@ void reader_set_exit_on_interrupt(bool flag); /** Returns true if the shell is exiting, 0 otherwise. */ -int exit_status(); +bool shell_is_exiting(); /** The readers interrupt signal handler. Cancels all currently running blocks. diff --git a/tests/test9.in b/tests/test9.in index a16281f10..e449a21dd 100644 --- a/tests/test9.in +++ b/tests/test9.in @@ -67,5 +67,7 @@ while contains $i a echo Darp end +# Test implicit cd. This should do nothing. +./ false