Support for implicit cd, no-exec, and the exit builtin. All tests now

pass (!). Error reporting still unsteady.
This commit is contained in:
ridiculousfish 2013-12-29 16:23:26 -08:00
parent a42711e31c
commit a9787b769f
7 changed files with 250 additions and 53 deletions

View file

@ -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. */ /* 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) 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 */ /* 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; const process_t *p = job->first_process;
if (p && p->type == INTERNAL_BUILTIN) if (p && p->type == INTERNAL_BUILTIN)

View file

@ -11,6 +11,7 @@
#include "builtin.h" #include "builtin.h"
#include "parser.h" #include "parser.h"
#include "expand.h" #include "expand.h"
#include "reader.h"
#include "wutil.h" #include "wutil.h"
#include "exec.h" #include "exec.h"
#include "path.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 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) 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); this->run_job_list(block_contents, fb);
/* Handle break or continue */ if (this->cancellation_reason(fb) == execution_cancellation_loop_control)
if (fb->loop_status == LOOP_CONTINUE)
{ {
/* Reset the loop state */ /* Handle break or continue */
fb->loop_status = LOOP_NORMAL; if (fb->loop_status == LOOP_CONTINUE)
fb->skip = false; {
continue; /* Reset the loop state */
} fb->loop_status = LOOP_NORMAL;
else if (fb->loop_status == LOOP_BREAK) fb->skip = false;
{ continue;
break; }
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) */ /* The block ought to go inside the loop (see #1212) */
this->run_job_list(block_contents, wb); this->run_job_list(block_contents, wb);
/* Handle break or continue */ if (this->cancellation_reason(wb) == execution_cancellation_loop_control)
if (wb->loop_status == LOOP_CONTINUE)
{ {
/* Reset the loop state */ /* Handle break or continue */
wb->loop_status = LOOP_NORMAL; if (wb->loop_status == LOOP_CONTINUE)
wb->skip = false; {
continue; /* Reset the loop state */
} wb->loop_status = LOOP_NORMAL;
else if (wb->loop_status == LOOP_BREAK) wb->skip = false;
{ continue;
break; }
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()); 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 */ /* Creates a 'normal' (non-block) process */
process_t *parse_execution_context_t::create_plain_process(job_t *job, const parse_node_t &statement) 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; bool errored = false;
/* Get the decoration */ /* We may decide that a command should be an implicit cd */
assert(statement.type == symbol_plain_statement); bool use_implicit_cd = false;
/* Get the command. We expect to always get it here. */ /* Get the command. We expect to always get it here. */
wcstring cmd; wcstring cmd;
@ -442,28 +584,7 @@ process_t *parse_execution_context_t::create_plain_process(job_t *job, const par
if (errored) if (errored)
return NULL; 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) */ /* 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 parse_statement_decoration_t decoration = tree.decoration_for_plain_statement(statement);
enum process_type_t process_type = EXTERNAL; 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; wcstring actual_cmd;
if (process_type == EXTERNAL) 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); 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; 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 */ /* Return the process, or NULL on error */
process_t *result = NULL; 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; 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) if (process_errored)
proc_set_last_status(1); proc_set_last_status(1);
const int ret = proc_get_last_status(); const int ret = proc_get_last_status();

View file

@ -34,11 +34,23 @@ class parse_execution_context_t
/* Should I cancel? */ /* Should I cancel? */
bool should_cancel_execution(const block_t *block) const; 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. */ /* Report an error. Always returns true. */
bool append_error(const parse_node_t &node, const wchar_t *fmt, ...); bool append_error(const parse_node_t &node, const wchar_t *fmt, ...);
/* Wildcard error helper */ /* Wildcard error helper */
bool append_unmatched_wildcard_error(const parse_node_t &unmatched_wildcard); 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 */ /* Utilities */
wcstring get_source(const parse_node_t &node) const; 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; 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;

View file

@ -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) && while (tok_has_next(current_tokenizer) &&
!error_code && !error_code &&
!sanity_check() && !sanity_check() &&
!exit_status()) !shell_is_exiting())
{ {
this->eval_job(current_tokenizer); this->eval_job(current_tokenizer);
event_fire(NULL); event_fire(NULL);
@ -2759,7 +2759,7 @@ int parser_t::eval(const wcstring &cmd_str, const io_chain_t &io, enum block_typ
break; 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() ); //debug( 2, L"Status %d\n", proc_get_last_status() );

View file

@ -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()) if (get_is_interactive())
return job_list_is_empty() && data->end_loop; return job_list_is_empty() && data->end_loop;

View file

@ -217,7 +217,7 @@ void reader_set_exit_on_interrupt(bool flag);
/** /**
Returns true if the shell is exiting, 0 otherwise. 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. The readers interrupt signal handler. Cancels all currently running blocks.

View file

@ -67,5 +67,7 @@ while contains $i a
echo Darp echo Darp
end end
# Test implicit cd. This should do nothing.
./
false false