Skip to content

Commit a9787b7

Browse files
Support for implicit cd, no-exec, and the exit builtin. All tests now
pass (!). Error reporting still unsteady.
1 parent a42711e commit a9787b7

File tree

7 files changed

+250
-53
lines changed

7 files changed

+250
-53
lines changed

exec.cpp

+6
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,12 @@ static bool can_use_posix_spawn_for_job(const job_t *job, const process_t *proce
577577
/* What exec does if no_exec is set. This only has to handle block pushing and popping. See #624. */
578578
static void exec_no_exec(parser_t &parser, const job_t *job)
579579
{
580+
if (parser_use_ast())
581+
{
582+
/* With the new parser, commands aren't responsible for pushing / popping blocks, so there's nothing to do */
583+
return;
584+
}
585+
580586
/* 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 */
581587
const process_t *p = job->first_process;
582588
if (p && p->type == INTERNAL_BUILTIN)

parse_execution.cpp

+226-49
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include "builtin.h"
1212
#include "parser.h"
1313
#include "expand.h"
14+
#include "reader.h"
1415
#include "wutil.h"
1516
#include "exec.h"
1617
#include "path.h"
@@ -47,7 +48,28 @@ node_offset_t parse_execution_context_t::get_offset(const parse_node_t &node) co
4748

4849
bool parse_execution_context_t::should_cancel_execution(const block_t *block) const
4950
{
50-
return block && (block->skip || block->loop_status != LOOP_NORMAL);
51+
return cancellation_reason(block) != execution_cancellation_none;
52+
}
53+
54+
parse_execution_context_t::execution_cancellation_reason_t parse_execution_context_t::cancellation_reason(const block_t *block) const
55+
{
56+
if (shell_is_exiting())
57+
{
58+
return execution_cancellation_exit;
59+
}
60+
else if (block && block->loop_status != LOOP_NORMAL)
61+
{
62+
/* Nasty hack - break and continue set the 'skip' flag as well as the loop status flag. */
63+
return execution_cancellation_loop_control;
64+
}
65+
else if (block && block->skip)
66+
{
67+
return execution_cancellation_skip;
68+
}
69+
else
70+
{
71+
return execution_cancellation_none;
72+
}
5173
}
5274

5375
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
229251

230252
this->run_job_list(block_contents, fb);
231253

232-
/* Handle break or continue */
233-
if (fb->loop_status == LOOP_CONTINUE)
234-
{
235-
/* Reset the loop state */
236-
fb->loop_status = LOOP_NORMAL;
237-
fb->skip = false;
238-
continue;
239-
}
240-
else if (fb->loop_status == LOOP_BREAK)
254+
if (this->cancellation_reason(fb) == execution_cancellation_loop_control)
241255
{
242-
break;
256+
/* Handle break or continue */
257+
if (fb->loop_status == LOOP_CONTINUE)
258+
{
259+
/* Reset the loop state */
260+
fb->loop_status = LOOP_NORMAL;
261+
fb->skip = false;
262+
continue;
263+
}
264+
else if (fb->loop_status == LOOP_BREAK)
265+
{
266+
break;
267+
}
243268
}
244269
}
245270

@@ -374,17 +399,20 @@ int parse_execution_context_t::run_while_statement(const parse_node_t &header, c
374399
/* The block ought to go inside the loop (see #1212) */
375400
this->run_job_list(block_contents, wb);
376401

377-
/* Handle break or continue */
378-
if (wb->loop_status == LOOP_CONTINUE)
402+
if (this->cancellation_reason(wb) == execution_cancellation_loop_control)
379403
{
380-
/* Reset the loop state */
381-
wb->loop_status = LOOP_NORMAL;
382-
wb->skip = false;
383-
continue;
384-
}
385-
else if (wb->loop_status == LOOP_BREAK)
386-
{
387-
break;
404+
/* Handle break or continue */
405+
if (wb->loop_status == LOOP_CONTINUE)
406+
{
407+
/* Reset the loop state */
408+
wb->loop_status = LOOP_NORMAL;
409+
wb->skip = false;
410+
continue;
411+
}
412+
else if (wb->loop_status == LOOP_BREAK)
413+
{
414+
break;
415+
}
388416
}
389417
}
390418

@@ -418,15 +446,129 @@ bool parse_execution_context_t::append_unmatched_wildcard_error(const parse_node
418446
return append_error(unmatched_wildcard, WILDCARD_ERR_MSG, get_source(unmatched_wildcard).c_str());
419447
}
420448

449+
/* Handle the case of command not found */
450+
void parse_execution_context_t::handle_command_not_found(const wcstring &cmd_str, const parse_node_t &statement_node, int err_code)
451+
{
452+
assert(statement_node.type == symbol_plain_statement);
453+
454+
/*
455+
We couldn't find the specified command.
456+
457+
What we want to happen now is that the
458+
specified job won't get executed, and an
459+
error message is printed on-screen, but
460+
otherwise, the parsing/execution of the
461+
file continues. Because of this, we don't
462+
want to call error(), since that would stop
463+
execution of the file. Instead we let
464+
p->actual_command be 0 (null), which will
465+
cause the job to silently not execute. We
466+
also print an error message and set the
467+
status to 127 (This is the standard number
468+
for this, used by other shells like bash
469+
and zsh).
470+
*/
471+
472+
const wchar_t * const cmd = cmd_str.c_str();
473+
const wchar_t * const equals_ptr = wcschr(cmd, L'=');
474+
if (equals_ptr != NULL)
475+
{
476+
/* 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...) */
421477

478+
const wcstring name_str = wcstring(cmd, equals_ptr - cmd); //variable name, up to the =
479+
const wcstring val_str = wcstring(equals_ptr + 1); //variable value, past the =
480+
481+
482+
const parse_node_tree_t::parse_node_list_t args = tree.find_nodes(statement_node, symbol_argument, 1);
483+
484+
if (! args.empty())
485+
{
486+
const wcstring argument = get_source(*args.at(0));
487+
488+
wcstring ellipsis_str = wcstring(1, ellipsis_char);
489+
if (ellipsis_str == L"$")
490+
ellipsis_str = L"...";
491+
492+
/* Looks like a command */
493+
debug(0,
494+
_(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'."),
495+
cmd,
496+
argument.c_str(),
497+
name_str.c_str(),
498+
val_str.c_str(),
499+
argument.c_str(),
500+
ellipsis_str.c_str());
501+
}
502+
else
503+
{
504+
debug(0,
505+
COMMAND_ASSIGN_ERR_MSG,
506+
cmd,
507+
name_str.c_str(),
508+
val_str.c_str());
509+
}
510+
}
511+
else if (cmd[0]==L'$' || cmd[0] == VARIABLE_EXPAND || cmd[0] == VARIABLE_EXPAND_SINGLE)
512+
{
513+
514+
const env_var_t val_wstr = env_get_string(cmd+1);
515+
const wchar_t *val = val_wstr.missing() ? NULL : val_wstr.c_str();
516+
if (val)
517+
{
518+
debug(0,
519+
_(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'."),
520+
cmd+1,
521+
val,
522+
cmd,
523+
cmd);
524+
}
525+
else
526+
{
527+
debug(0,
528+
_(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'."),
529+
cmd,
530+
cmd);
531+
}
532+
}
533+
else if (wcschr(cmd, L'$'))
534+
{
535+
debug(0,
536+
_(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'."),
537+
cmd,
538+
cmd);
539+
}
540+
else if (err_code!=ENOENT)
541+
{
542+
debug(0,
543+
_(L"The file '%ls' is not executable by this user"),
544+
cmd?cmd:L"UNKNOWN");
545+
}
546+
else
547+
{
548+
/*
549+
Handle unrecognized commands with standard
550+
command not found handler that can make better
551+
error messages
552+
*/
553+
554+
wcstring_list_t event_args;
555+
event_args.push_back(cmd_str);
556+
event_fire_generic(L"fish_command_not_found", &event_args);
557+
}
558+
559+
/* Set the last proc status appropriately */
560+
proc_set_last_status(err_code==ENOENT?STATUS_UNKNOWN_COMMAND:STATUS_NOT_EXECUTABLE);
561+
}
422562

423563
/* Creates a 'normal' (non-block) process */
424564
process_t *parse_execution_context_t::create_plain_process(job_t *job, const parse_node_t &statement)
425565
{
566+
assert(statement.type == symbol_plain_statement);
567+
426568
bool errored = false;
427569

428-
/* Get the decoration */
429-
assert(statement.type == symbol_plain_statement);
570+
/* We may decide that a command should be an implicit cd */
571+
bool use_implicit_cd = false;
430572

431573
/* Get the command. We expect to always get it here. */
432574
wcstring cmd;
@@ -442,28 +584,7 @@ process_t *parse_execution_context_t::create_plain_process(job_t *job, const par
442584

443585
if (errored)
444586
return NULL;
445-
446-
/* The list of arguments. The command is the first argument. TODO: count hack */
447-
const parse_node_t *unmatched_wildcard = NULL;
448-
wcstring_list_t argument_list = this->determine_arguments(statement, &unmatched_wildcard);
449-
argument_list.insert(argument_list.begin(), cmd);
450-
451-
/* If we were not able to expand any wildcards, here is the first one that failed */
452-
if (unmatched_wildcard != NULL)
453-
{
454-
job_set_flag(job, JOB_WILDCARD_ERROR, 1);
455-
errored = append_unmatched_wildcard_error(*unmatched_wildcard);
456-
}
457-
458-
if (errored)
459-
return NULL;
460-
461-
/* The set of IO redirections that we construct for the process */
462-
io_chain_t process_io_chain;
463-
errored = ! this->determine_io_chain(statement, &process_io_chain);
464-
if (errored)
465-
return NULL;
466-
587+
467588
/* Determine the process type, which depends on the statement decoration (command, builtin, etc) */
468589
enum parse_statement_decoration_t decoration = tree.decoration_for_plain_statement(statement);
469590
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
500621
wcstring actual_cmd;
501622
if (process_type == EXTERNAL)
502623
{
503-
/* Determine the actual command. Need to support implicit cd here */
624+
/* Determine the actual command. This may be an implicit cd. */
504625
bool has_command = path_get_path(cmd, &actual_cmd);
505626

506-
if (! has_command)
627+
/* 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 */
628+
const int no_cmd_err_code = errno;
629+
630+
/* If the specified command does not exist, and is undecorated, try using an implicit cd. */
631+
if (! has_command && decoration == parse_statement_decoration_none)
507632
{
508-
/* TODO: support fish_command_not_found, implicit cd, etc. here */
633+
/* Implicit cd requires an empty argument and redirection list */
634+
const parse_node_t *args = get_child(statement, 1, symbol_arguments_or_redirections_list);
635+
if (args->child_count == 0)
636+
{
637+
/* Ok, no arguments or redirections; check to see if the first argument is a directory */
638+
wcstring implicit_cd_path;
639+
use_implicit_cd = path_can_be_implicit_cd(cmd, &implicit_cd_path);
640+
}
641+
}
642+
643+
if (! has_command && ! use_implicit_cd)
644+
{
645+
/* No command */
646+
this->handle_command_not_found(cmd, statement, no_cmd_err_code);
509647
errored = true;
510648
}
511649
}
650+
if (errored)
651+
return NULL;
652+
653+
/* The argument list and set of IO redirections that we will construct for the process */
654+
wcstring_list_t argument_list;
655+
io_chain_t process_io_chain;
656+
if (use_implicit_cd)
657+
{
658+
/* Implicit cd is simple */
659+
argument_list.push_back(L"cd");
660+
argument_list.push_back(cmd);
661+
actual_cmd.clear();
662+
663+
/* If we have defined a wrapper around cd, use it, otherwise use the cd builtin */
664+
process_type = function_exists(L"cd") ? INTERNAL_FUNCTION : INTERNAL_BUILTIN;
665+
}
666+
else
667+
{
668+
/* Form the list of arguments. The command is the first argument. TODO: count hack */
669+
const parse_node_t *unmatched_wildcard = NULL;
670+
argument_list = this->determine_arguments(statement, &unmatched_wildcard);
671+
argument_list.insert(argument_list.begin(), cmd);
672+
673+
/* If we were not able to expand any wildcards, here is the first one that failed */
674+
if (unmatched_wildcard != NULL)
675+
{
676+
job_set_flag(job, JOB_WILDCARD_ERROR, 1);
677+
errored = append_unmatched_wildcard_error(*unmatched_wildcard);
678+
}
679+
680+
if (errored)
681+
return NULL;
682+
683+
/* The set of IO redirections that we construct for the process */
684+
errored = ! this->determine_io_chain(statement, &process_io_chain);
685+
if (errored)
686+
return NULL;
687+
}
688+
512689

513690
/* Return the process, or NULL on error */
514691
process_t *result = NULL;
@@ -953,7 +1130,7 @@ int parse_execution_context_t::run_1_job(const parse_node_t &job_node, const blo
9531130
profile_item->skipped = process_errored;
9541131
}
9551132

956-
/* Set the last status to 1 if the job could not be executed */
1133+
/* Set the last status to 1 if the job could not be executed. TODO: Don't stomp STATUS_UNKNOWN_COMMAND / STATUS_NOT_EXECUTABLE */
9571134
if (process_errored)
9581135
proc_set_last_status(1);
9591136
const int ret = proc_get_last_status();

parse_execution.h

+12
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,23 @@ class parse_execution_context_t
3434
/* Should I cancel? */
3535
bool should_cancel_execution(const block_t *block) const;
3636

37+
/* Ways that we can stop executing a block. These are in a sort of ascending order of importance, e.g. `exit` should trump `break` */
38+
enum execution_cancellation_reason_t
39+
{
40+
execution_cancellation_none,
41+
execution_cancellation_loop_control,
42+
execution_cancellation_skip,
43+
execution_cancellation_exit
44+
};
45+
execution_cancellation_reason_t cancellation_reason(const block_t *block) const;
46+
3747
/* Report an error. Always returns true. */
3848
bool append_error(const parse_node_t &node, const wchar_t *fmt, ...);
3949
/* Wildcard error helper */
4050
bool append_unmatched_wildcard_error(const parse_node_t &unmatched_wildcard);
4151

52+
void handle_command_not_found(const wcstring &cmd, const parse_node_t &statement_node, int err_code);
53+
4254
/* Utilities */
4355
wcstring get_source(const parse_node_t &node) const;
4456
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;

0 commit comments

Comments
 (0)