aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar ridiculousfish <corydoras@ridiculousfish.com>2013-12-29 16:23:26 -0800
committerGravatar ridiculousfish <corydoras@ridiculousfish.com>2013-12-29 16:23:26 -0800
commita9787b769fce4327be5db4f361fb47208d4f79d1 (patch)
treeb71139c1669cb964350ae7b863bfdb14b89c9e86
parenta42711e31cdb41e3c504ed161c07e56698d29e7a (diff)
Support for implicit cd, no-exec, and the exit builtin. All tests now
pass (!). Error reporting still unsteady.
-rw-r--r--exec.cpp6
-rw-r--r--parse_execution.cpp275
-rw-r--r--parse_execution.h12
-rw-r--r--parser.cpp4
-rw-r--r--reader.cpp2
-rw-r--r--reader.h2
-rw-r--r--tests/test9.in2
7 files changed, 250 insertions, 53 deletions
diff --git a/exec.cpp b/exec.cpp
index e150723d..594a5385 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 137b7e00..0733fb8b 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)
- {
- /* Reset the loop state */
- fb->loop_status = LOOP_NORMAL;
- fb->skip = false;
- continue;
- }
- else if (fb->loop_status == LOOP_BREAK)
+ if (this->cancellation_reason(fb) == execution_cancellation_loop_control)
{
- 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 8d89158b..f68cad5f 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 bd0471df..6b7d1909 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 0905b379..f5ae62f4 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 b954c1be..e028e2f0 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 a16281f1..e449a21d 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