diff --git a/CHANGELOG.md b/CHANGELOG.md
index 56830a217..3bab1c405 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -43,6 +43,7 @@ This section is for changes merged to the `major` branch that are not also merge
- The names `argparse`, `read`, `set`, `status`, `test` and `[` are now reserved and not allowed as function names. This prevents users unintentionally breaking stuff (#3000).
- Wrapping completions (from `complete -w` or `function -w`) can now inject arguments. For example, `complete gco -w 'git checkout'` now works properly (#1976). The `alias` function has been updated to respect this behavior.
- The `jobs` builtin now has a `-q` and `--quiet` option to silence the output.
+- fish now supports `&&`, `||`, and `!` (#4620).
## Other significant changes
- Command substitution output is now limited to 10 MB by default (#3822).
diff --git a/doc_src/faq.hdr b/doc_src/faq.hdr
index 45756f77e..133ac438c 100644
--- a/doc_src/faq.hdr
+++ b/doc_src/faq.hdr
@@ -281,7 +281,7 @@ The fish user community extends fish in unique and useful ways via scripts that
- Oh My Fish
- Tacklebox
-This is not an exhaustive list and the fish project has no opinion regarding the merits of the repositories listed above or the scripts found therein. We mention these only because you may find within them a solution to a need you have such as supporting the `&&` and `||` operators or improved integration with other tools that you use.
+This is not an exhaustive list and the fish project has no opinion regarding the merits of the repositories listed above or the scripts found therein.
\htmlonly[block]
diff --git a/doc_src/tutorial.hdr b/doc_src/tutorial.hdr
index 061792af7..832d4d95e 100644
--- a/doc_src/tutorial.hdr
+++ b/doc_src/tutorial.hdr
@@ -420,17 +420,23 @@ echo chips
\section tut_combiners Combiners (And, Or, Not)
-Unlike other shells, `fish` does not have special syntax like && or || to combine commands. Instead it has commands `and`, `or`, and `not`.
+fish supports the familiar `&&` and `||` to combine commands, and `!` to negate them:
\fish{cli-dark}
->_ cp file1.txt file1_bak.txt; and echo "Backup successful"; or echo "Backup failed"
+>_ ./configure && make && sudo make install
+\endfish
+
+fish also supports `and`, `or`, and `not`. The first two are job modifiers and have lower precedence. Example usage:
+
+\fish{cli-dark}
+>_ cp file1.txt file1_bak.txt && cp file2.txt file2_bak.txt ; and echo "Backup successful"; or echo "Backup failed"
Backup failed
\endfish
As mentioned in the section on the semicolon, this can also be written in multiple lines, like so:
\fish
-cp file1.txt file1_bak.txt
+cp file1.txt file1_bak.txt && cp file2.txt file2_bak.txt
and echo "Backup successful"
or echo "Backup failed"
\endfish
diff --git a/lexicon_filter.in b/lexicon_filter.in
index 19c33d3ff..211a5dc43 100644
--- a/lexicon_filter.in
+++ b/lexicon_filter.in
@@ -584,10 +584,11 @@ x
# A special case. Tidy up after performing command substitution.
# Redirectors
s/\([^{|] *\)|/\1@redr{|}/g
-s/\&@EOL$/@redr{@amp}@EOL/g
-s/@amp@EOL$/@redr{@amp}@EOL/g
-s/\([<>]\)@amp\([0-9]\)/@redr{\1@amp\2}/g
-s/\([^{&] *\)&[^@a-z]/\1@redr{\&}/g
+#s/\&@EOL$/@redr{@amp}@EOL/g
+#s/@amp@EOL$/@redr{@amp}@EOL/g
+#s/\([<>]\)@amp\([0-9]\)/@redr{\1@amp\2}/g
+s/@amp&/@optr{@amp@amp}/g
+#s/\([^{&] *\)&[^@a-z]/\1@redr{\&}/g
s/\([^{<>^] *\)\([0-9]* *[<>^][<>^]*[^@][a-zA-Z0-9./_-]*\)/\1@redr{\2}/g
s/\\}/}\\/g
#.
diff --git a/src/fish_indent.cpp b/src/fish_indent.cpp
index 801b835ca..12545786d 100644
--- a/src/fish_indent.cpp
+++ b/src/fish_indent.cpp
@@ -121,7 +121,7 @@ static void prettify_node_recursive(const wcstring &source, const parse_node_tre
const bool is_root_case_list =
node_type == symbol_case_item_list && parent_type != symbol_case_item_list;
const bool is_if_while_header =
- (node_type == symbol_job || node_type == symbol_andor_job_list) &&
+ (node_type == symbol_job_conjunction || node_type == symbol_andor_job_list) &&
(parent_type == symbol_if_clause || parent_type == symbol_while_header);
if (is_root_job_list || is_root_case_list || is_if_while_header) {
diff --git a/src/fish_tests.cpp b/src/fish_tests.cpp
index 29a01ba5f..ff01bdbe3 100644
--- a/src/fish_tests.cpp
+++ b/src/fish_tests.cpp
@@ -535,10 +535,14 @@ static void test_tokenizer() {
const wchar_t *str =
L"string &1 'nested \"quoted\" '(string containing subshells "
L"){and,brackets}$as[$well (as variable arrays)] not_a_redirect^ ^ ^^is_a_redirect "
+ L"&&& ||| "
+ L"&& || & |"
L"Compress_Newlines\n \n\t\n \nInto_Just_One";
- const int types[] = {TOK_STRING, TOK_REDIRECT, TOK_STRING, TOK_REDIRECT, TOK_STRING,
- TOK_STRING, TOK_STRING, TOK_REDIRECT, TOK_REDIRECT, TOK_STRING,
- TOK_STRING, TOK_END, TOK_STRING};
+ const int types[] = {TOK_STRING, TOK_REDIRECT, TOK_STRING, TOK_REDIRECT, TOK_STRING,
+ TOK_STRING, TOK_STRING, TOK_REDIRECT, TOK_REDIRECT, TOK_STRING,
+ TOK_ANDAND, TOK_BACKGROUND, TOK_OROR, TOK_PIPE, TOK_ANDAND,
+ TOK_OROR, TOK_BACKGROUND, TOK_PIPE, TOK_STRING, TOK_END,
+ TOK_STRING};
say(L"Test correct tokenization");
@@ -834,6 +838,38 @@ static void test_parser() {
err(L"backgrounded 'while' conditional not reported as error");
}
+ if (!parse_util_detect_errors(L"true | || false")) {
+ err(L"bogus boolean statement error not detected on line %d", __LINE__);
+ }
+
+ if (!parse_util_detect_errors(L"|| false")) {
+ err(L"bogus boolean statement error not detected on line %d", __LINE__);
+ }
+
+ if (!parse_util_detect_errors(L"&& false")) {
+ err(L"bogus boolean statement error not detected on line %d", __LINE__);
+ }
+
+ if (!parse_util_detect_errors(L"true ; && false")) {
+ err(L"bogus boolean statement error not detected on line %d", __LINE__);
+ }
+
+ if (!parse_util_detect_errors(L"true ; || false")) {
+ err(L"bogus boolean statement error not detected on line %d", __LINE__);
+ }
+
+ if (!parse_util_detect_errors(L"true || && false")) {
+ err(L"bogus boolean statement error not detected on line %d", __LINE__);
+ }
+
+ if (!parse_util_detect_errors(L"true && || false")) {
+ err(L"bogus boolean statement error not detected on line %d", __LINE__);
+ }
+
+ if (!parse_util_detect_errors(L"true && && false")) {
+ err(L"bogus boolean statement error not detected on line %d", __LINE__);
+ }
+
say(L"Testing basic evaluation");
// Ensure that we don't crash on infinite self recursion and mutual recursion. These must use
@@ -3410,6 +3446,11 @@ static void test_new_parser_correctness() {
{L"begin; end", true},
{L"begin if true; end; end;", true},
{L"begin if true ; echo hi ; end; end", true},
+ {L"true && false || false", true},
+ {L"true || false; and true", true},
+ {L"true || ||", false},
+ {L"|| true", false},
+ {L"true || \n\n false", true},
};
for (size_t i = 0; i < sizeof parser_tests / sizeof *parser_tests; i++) {
@@ -3624,9 +3665,6 @@ static void test_new_parser_errors() {
{L"case", parse_error_unbalancing_case},
{L"if true ; case ; end", parse_error_unbalancing_case},
-
- {L"foo || bar", parse_error_double_pipe},
- {L"foo && bar", parse_error_double_background},
};
for (size_t i = 0; i < sizeof tests / sizeof *tests; i++) {
@@ -3742,9 +3780,7 @@ static void test_error_messages() {
{L"echo \"foo\"$\"bar\"", ERROR_NO_VAR_NAME},
{L"echo foo $ bar", ERROR_NO_VAR_NAME},
{L"echo foo$(foo)bar", ERROR_BAD_VAR_SUBCOMMAND1},
- {L"echo \"foo$(foo)bar\"", ERROR_BAD_VAR_SUBCOMMAND1},
- {L"echo foo || echo bar", ERROR_BAD_OR},
- {L"echo foo && echo bar", ERROR_BAD_AND}};
+ {L"echo \"foo$(foo)bar\"", ERROR_BAD_VAR_SUBCOMMAND1}};
parse_error_list_t errors;
for (size_t i = 0; i < sizeof error_tests / sizeof *error_tests; i++) {
@@ -3915,10 +3951,28 @@ static void test_highlighting() {
{L"2>", highlight_spec_redirection},
{NULL, -1}};
+ const highlight_component_t components15[] = {{L"if", highlight_spec_command},
+ {L"true", highlight_spec_command},
+ {L"&&", highlight_spec_operator},
+ {L"false", highlight_spec_command},
+ {L";", highlight_spec_statement_terminator},
+ {L"or", highlight_spec_operator},
+ {L"false", highlight_spec_command},
+ {L"||", highlight_spec_operator},
+ {L"true", highlight_spec_command},
+ {L";", highlight_spec_statement_terminator},
+ {L"and", highlight_spec_operator},
+ {L"not", highlight_spec_operator},
+ {L"!", highlight_spec_operator},
+ {L"true", highlight_spec_command},
+ {L";", highlight_spec_statement_terminator},
+ {L"end", highlight_spec_command},
+ {NULL, -1}};
+
const highlight_component_t *tests[] = {components1, components2, components3, components4,
components5, components6, components7, components8,
components9, components10, components11, components12,
- components13, components14};
+ components13, components14, components15};
for (size_t which = 0; which < sizeof tests / sizeof *tests; which++) {
const highlight_component_t *components = tests[which];
// Count how many we have.
@@ -3954,8 +4008,10 @@ static void test_highlighting() {
if (expected_colors.at(i) != colors.at(i)) {
const wcstring spaces(i, L' ');
- err(L"Wrong color at index %lu in text (expected %#x, actual %#x):\n%ls\n%ls^", i,
- expected_colors.at(i), colors.at(i), text.c_str(), spaces.c_str());
+ err(L"Wrong color in test %lu at index %lu in text (expected %#x, actual "
+ L"%#x):\n%ls\n%ls^",
+ which + 1, i, expected_colors.at(i), colors.at(i), text.c_str(),
+ spaces.c_str());
}
}
}
diff --git a/src/highlight.cpp b/src/highlight.cpp
index cc69010ec..1c91172d7 100644
--- a/src/highlight.cpp
+++ b/src/highlight.cpp
@@ -1033,7 +1033,6 @@ const highlighter_t::color_array_t &highlighter_t::highlight() {
case symbol_if_clause:
case symbol_else_clause:
case symbol_case_item:
- case symbol_boolean_statement:
case symbol_decorated_statement:
case symbol_if_statement: {
this->color_children(node, parse_token_type_string, highlight_spec_command);
@@ -1059,6 +1058,20 @@ const highlighter_t::color_array_t &highlighter_t::highlight() {
this->color_argument(fhead.child<1>());
break;
}
+
+ case parse_token_type_andand:
+ case parse_token_type_oror:
+ this->color_node(node, highlight_spec_operator);
+ break;
+
+ case symbol_not_statement:
+ this->color_children(node, parse_token_type_string, highlight_spec_operator);
+ break;
+
+ case symbol_job_decorator:
+ this->color_node(node, highlight_spec_operator);
+ break;
+
case parse_token_type_pipe:
case parse_token_type_background:
case parse_token_type_end:
diff --git a/src/history.cpp b/src/history.cpp
index be0066a42..d2a58f676 100644
--- a/src/history.cpp
+++ b/src/history.cpp
@@ -1773,6 +1773,10 @@ static bool should_import_bash_history_line(const std::string &line) {
if (line.find("((") != std::string::npos) return false;
if (line.find("))") != std::string::npos) return false;
+ // Temporarily skip lines with && and ||
+ if (line.find("&&") != std::string::npos) return false;
+ if (line.find("||") != std::string::npos) return false;
+
// Skip lines that end with a backslash. We do not handle multiline commands from bash history.
if (line.back() == '\\') return false;
diff --git a/src/parse_constants.h b/src/parse_constants.h
index b8e43fe0c..9deb0efc3 100644
--- a/src/parse_constants.h
+++ b/src/parse_constants.h
@@ -17,6 +17,9 @@ enum parse_token_type_t {
token_type_invalid = 1,
// Non-terminal tokens
symbol_job_list,
+ symbol_job_conjunction,
+ symbol_job_conjunction_continuation,
+ symbol_job_decorator,
symbol_job,
symbol_job_continuation,
symbol_statement,
@@ -33,7 +36,7 @@ enum parse_token_type_t {
symbol_switch_statement,
symbol_case_item_list,
symbol_case_item,
- symbol_boolean_statement,
+ symbol_not_statement,
symbol_decorated_statement,
symbol_plain_statement,
symbol_arguments_or_redirections_list,
@@ -52,6 +55,8 @@ enum parse_token_type_t {
parse_token_type_pipe,
parse_token_type_redirection,
parse_token_type_background,
+ parse_token_type_andand,
+ parse_token_type_oror,
parse_token_type_end,
// Special terminal type that means no more tokens forthcoming.
parse_token_type_terminate,
@@ -77,36 +82,12 @@ const enum_map token_enum_map[] = {
{parse_token_type_pipe, L"parse_token_type_pipe"},
{parse_token_type_redirection, L"parse_token_type_redirection"},
{parse_token_type_string, L"parse_token_type_string"},
+ {parse_token_type_andand, L"parse_token_type_andand"},
+ {parse_token_type_oror, L"parse_token_type_oror"},
{parse_token_type_terminate, L"parse_token_type_terminate"},
- {symbol_andor_job_list, L"symbol_andor_job_list"},
- {symbol_argument, L"symbol_argument"},
- {symbol_argument_list, L"symbol_argument_list"},
- {symbol_arguments_or_redirections_list, L"symbol_arguments_or_redirections_list"},
- {symbol_begin_header, L"symbol_begin_header"},
- {symbol_block_header, L"symbol_block_header"},
- {symbol_block_statement, L"symbol_block_statement"},
- {symbol_boolean_statement, L"symbol_boolean_statement"},
- {symbol_case_item, L"symbol_case_item"},
- {symbol_case_item_list, L"symbol_case_item_list"},
- {symbol_decorated_statement, L"symbol_decorated_statement"},
- {symbol_else_clause, L"symbol_else_clause"},
- {symbol_else_continuation, L"symbol_else_continuation"},
- {symbol_end_command, L"symbol_end_command"},
- {symbol_for_header, L"symbol_for_header"},
- {symbol_freestanding_argument_list, L"symbol_freestanding_argument_list"},
- {symbol_function_header, L"symbol_function_header"},
- {symbol_if_clause, L"symbol_if_clause"},
- {symbol_if_statement, L"symbol_if_statement"},
- {symbol_job, L"symbol_job"},
- {symbol_job_continuation, L"symbol_job_continuation"},
- {symbol_job_list, L"symbol_job_list"},
- {symbol_optional_newlines, L"symbol_optional_newlines"},
- {symbol_optional_background, L"symbol_optional_background"},
- {symbol_plain_statement, L"symbol_plain_statement"},
- {symbol_redirection, L"symbol_redirection"},
- {symbol_statement, L"symbol_statement"},
- {symbol_switch_statement, L"symbol_switch_statement"},
- {symbol_while_header, L"symbol_while_header"},
+// Define all symbols
+#define ELEM(sym) {symbol_##sym, L"symbol_" #sym},
+#include "parse_grammar_elements.inc"
{token_type_invalid, L"token_type_invalid"},
{token_type_invalid, NULL}};
#define token_enum_map_len (sizeof token_enum_map / sizeof *token_enum_map)
@@ -130,21 +111,30 @@ enum parse_keyword_t {
parse_keyword_if,
parse_keyword_in,
parse_keyword_not,
+ parse_keyword_exclam,
parse_keyword_or,
parse_keyword_switch,
parse_keyword_while,
} __packed;
-const enum_map keyword_enum_map[] = {
- {parse_keyword_and, L"and"}, {parse_keyword_begin, L"begin"},
- {parse_keyword_builtin, L"builtin"}, {parse_keyword_case, L"case"},
- {parse_keyword_command, L"command"}, {parse_keyword_else, L"else"},
- {parse_keyword_end, L"end"}, {parse_keyword_exec, L"exec"},
- {parse_keyword_for, L"for"}, {parse_keyword_function, L"function"},
- {parse_keyword_if, L"if"}, {parse_keyword_in, L"in"},
- {parse_keyword_not, L"not"}, {parse_keyword_or, L"or"},
- {parse_keyword_switch, L"switch"}, {parse_keyword_while, L"while"},
- {parse_keyword_none, NULL}};
+const enum_map keyword_enum_map[] = {{parse_keyword_exclam, L"!"},
+ {parse_keyword_and, L"and"},
+ {parse_keyword_begin, L"begin"},
+ {parse_keyword_builtin, L"builtin"},
+ {parse_keyword_case, L"case"},
+ {parse_keyword_command, L"command"},
+ {parse_keyword_else, L"else"},
+ {parse_keyword_end, L"end"},
+ {parse_keyword_exec, L"exec"},
+ {parse_keyword_for, L"for"},
+ {parse_keyword_function, L"function"},
+ {parse_keyword_if, L"if"},
+ {parse_keyword_in, L"in"},
+ {parse_keyword_not, L"not"},
+ {parse_keyword_or, L"or"},
+ {parse_keyword_switch, L"switch"},
+ {parse_keyword_while, L"while"},
+ {parse_keyword_none, NULL}};
#define keyword_enum_map_len (sizeof keyword_enum_map / sizeof *keyword_enum_map)
// Node tag values.
@@ -158,7 +148,7 @@ enum parse_statement_decoration_t {
};
// Boolean statement types, stored in node tag.
-enum parse_bool_statement_type_t { parse_bool_and, parse_bool_or, parse_bool_not };
+enum parse_bool_statement_type_t { parse_bool_none, parse_bool_and, parse_bool_or };
// Whether a statement is backgrounded.
enum parse_optional_background_t { parse_no_background, parse_background };
@@ -183,10 +173,7 @@ enum parse_error_code_t {
parse_error_unbalancing_end, // end outside of block
parse_error_unbalancing_else, // else outside of if
- parse_error_unbalancing_case, // case outside of switch
-
- parse_error_double_pipe, // foo || bar, has special error message
- parse_error_double_background // foo && bar, has special error message
+ parse_error_unbalancing_case // case outside of switch
};
enum { PARSER_TEST_ERROR = 1, PARSER_TEST_INCOMPLETE = 2 };
@@ -289,12 +276,6 @@ void parse_error_offset_source_start(parse_error_list_t *errors, size_t amt);
/// Error issued on $.
#define ERROR_NO_VAR_NAME _(L"Expected a variable name after this $.")
-/// Error on ||.
-#define ERROR_BAD_OR _(L"Unsupported use of '||'. In fish, please use 'COMMAND; or COMMAND'.")
-
-/// Error on &&.
-#define ERROR_BAD_AND _(L"Unsupported use of '&&'. In fish, please use 'COMMAND; and COMMAND'.")
-
/// Error on foo=bar.
#define ERROR_BAD_EQUALS_IN_COMMAND5 \
_(L"Unsupported use of '='. To run '%ls' with a modified environment, please use 'env " \
diff --git a/src/parse_execution.cpp b/src/parse_execution.cpp
index 6116424ab..c9bcd6f58 100644
--- a/src/parse_execution.cpp
+++ b/src/parse_execution.cpp
@@ -107,7 +107,7 @@ tnode_t parse_execution_context_t::infinite_recursive_statem
const wcstring &forbidden_function_name = parser->forbidden_function.back();
// Get the first job in the job list.
- auto first_job = job_list.next_in_list();
+ tnode_t first_job = job_list.try_get_child().child<0>();
if (!first_job) {
return {};
}
@@ -215,7 +215,7 @@ bool parse_execution_context_t::job_is_simple_block(tnode_t job_node) co
return is_empty(statement.require_get_child().child<5>());
case symbol_if_statement:
return is_empty(statement.require_get_child().child<3>());
- case symbol_boolean_statement:
+ case symbol_not_statement:
case symbol_decorated_statement:
// not block statements
return false;
@@ -244,12 +244,12 @@ parse_execution_result_t parse_execution_context_t::run_if_statement(
}
// An if condition has a job and a "tail" of andor jobs, e.g. "foo ; and bar; or baz".
- tnode_t condition_head = if_clause.child<1>();
+ tnode_t condition_head = if_clause.child<1>();
tnode_t condition_boolean_tail = if_clause.child<3>();
// Check the condition and the tail. We treat parse_execution_errored here as failure, in
// accordance with historic behavior.
- parse_execution_result_t cond_ret = run_1_job(condition_head, ib);
+ parse_execution_result_t cond_ret = run_job_conjunction(condition_head, ib);
if (cond_ret == parse_execution_success) {
cond_ret = run_job_list(condition_boolean_tail, ib);
}
@@ -527,13 +527,13 @@ parse_execution_result_t parse_execution_context_t::run_while_statement(
parse_execution_result_t ret = parse_execution_success;
// The conditions of the while loop.
- tnode_t condition_head = header.child<1>();
+ tnode_t condition_head = header.child<1>();
tnode_t condition_boolean_tail = header.child<3>();
// Run while the condition is true.
for (;;) {
// Check the condition.
- parse_execution_result_t cond_ret = this->run_1_job(condition_head, wb);
+ parse_execution_result_t cond_ret = this->run_job_conjunction(condition_head, wb);
if (cond_ret == parse_execution_success) {
cond_ret = run_job_list(condition_boolean_tail, wb);
}
@@ -921,33 +921,11 @@ bool parse_execution_context_t::determine_io_chain(tnode_t bool_statement) {
- // Handle a boolean statement.
- bool skip_job = false;
- switch (bool_statement_type(bool_statement)) {
- case parse_bool_and: {
- // AND. Skip if the last job failed.
- skip_job = (proc_get_last_status() != 0);
- break;
- }
- case parse_bool_or: {
- // OR. Skip if the last job succeeded.
- skip_job = (proc_get_last_status() == 0);
- break;
- }
- case parse_bool_not: {
- // NOT. Negate it.
- job->set_flag(JOB_NEGATE, !job->get_flag(JOB_NEGATE));
- break;
- }
- }
-
- if (skip_job) {
- return parse_execution_skipped;
- }
+parse_execution_result_t parse_execution_context_t::populate_not_process(
+ job_t *job, process_t *proc, tnode_t not_statement) {
+ job->set_flag(JOB_NEGATE, !job->get_flag(JOB_NEGATE));
return this->populate_job_process(job, proc,
- bool_statement.require_get_child());
+ not_statement.require_get_child());
}
template
@@ -985,8 +963,8 @@ parse_execution_result_t parse_execution_context_t::populate_job_process(
parse_execution_result_t result = parse_execution_success;
switch (specific_statement.type) {
- case symbol_boolean_statement: {
- result = this->populate_boolean_process(job, proc, {&tree(), &specific_statement});
+ case symbol_not_statement: {
+ result = this->populate_not_process(job, proc, {&tree(), &specific_statement});
break;
}
case symbol_block_statement:
@@ -1220,19 +1198,64 @@ parse_execution_result_t parse_execution_context_t::run_1_job(tnode_t jo
return parse_execution_success;
}
+parse_execution_result_t parse_execution_context_t::run_job_conjunction(
+ tnode_t job_expr, const block_t *associated_block) {
+ parse_execution_result_t result = parse_execution_success;
+ tnode_t cursor = job_expr;
+ // continuation is the parent of the cursor
+ tnode_t continuation;
+ while (cursor) {
+ if (should_cancel_execution(associated_block)) break;
+ bool skip = false;
+ if (continuation) {
+ // Check the conjunction type.
+ parse_bool_statement_type_t conj = bool_statement_type(continuation);
+ assert((conj == parse_bool_and || conj == parse_bool_or) && "Unexpected conjunction");
+ skip = should_skip(conj);
+ }
+ if (! skip) {
+ result = run_1_job(cursor.child<0>(), associated_block);
+ }
+ continuation = cursor.child<1>();
+ cursor = continuation.try_get_child();
+ }
+ return result;
+}
+
+bool parse_execution_context_t::should_skip(parse_bool_statement_type_t type) const {
+ switch (type) {
+ case parse_bool_and:
+ // AND. Skip if the last job failed.
+ return proc_get_last_status() != 0;
+ case parse_bool_or:
+ // OR. Skip if the last job succeeded.
+ return proc_get_last_status() == 0;
+ default:
+ return false;
+ }
+}
+
template
parse_execution_result_t parse_execution_context_t::run_job_list(tnode_t job_list,
const block_t *associated_block) {
+ // We handle both job_list and andor_job_list uniformly.
static_assert(Type::token == symbol_job_list || Type::token == symbol_andor_job_list,
"Not a job list");
parse_execution_result_t result = parse_execution_success;
- while (tnode_t job = job_list.template next_in_list()) {
+ while (auto job_conj = job_list.template next_in_list()) {
if (should_cancel_execution(associated_block)) break;
- result = this->run_1_job(job, associated_block);
+
+ // Maybe skip the job if it has a leading and/or.
+ // Skipping is treated as success.
+ if (should_skip(get_decorator(job_conj))) {
+ result = parse_execution_success;
+ } else {
+ result = this->run_job_conjunction(job_conj, associated_block);
+ }
}
- // Returns the last job executed.
+ // Returns the result of the last job executed or skipped.
return result;
}
diff --git a/src/parse_execution.h b/src/parse_execution.h
index 403d4392f..dceb87235 100644
--- a/src/parse_execution.h
+++ b/src/parse_execution.h
@@ -72,6 +72,9 @@ class parse_execution_context_t {
tnode_t job_list, wcstring *out_func_name) const;
bool is_function_context() const;
+ /// Return whether we should skip a job with the given bool statement type.
+ bool should_skip(parse_bool_statement_type_t type) const;
+
/// Indicates whether a job is a simple block (one block, no redirections).
bool job_is_simple_block(tnode_t job) const;
@@ -81,8 +84,8 @@ class parse_execution_context_t {
// These create process_t structures from statements.
parse_execution_result_t populate_job_process(job_t *job, process_t *proc,
tnode_t statement);
- parse_execution_result_t populate_boolean_process(
- job_t *job, process_t *proc, tnode_t bool_statement);
+ parse_execution_result_t populate_not_process(job_t *job, process_t *proc,
+ tnode_t not_statement);
parse_execution_result_t populate_plain_process(job_t *job, process_t *proc,
tnode_t statement);
@@ -114,6 +117,7 @@ class parse_execution_context_t {
io_chain_t *out_chain);
parse_execution_result_t run_1_job(tnode_t job, const block_t *associated_block);
+ parse_execution_result_t run_job_conjunction(tnode_t job_conj, const block_t *associated_block);
template
parse_execution_result_t run_job_list(tnode_t job_list_node,
const block_t *associated_block);
diff --git a/src/parse_grammar.h b/src/parse_grammar.h
index 0526ef055..488bb9314 100644
--- a/src/parse_grammar.h
+++ b/src/parse_grammar.h
@@ -35,6 +35,8 @@ using tok_string = primitive;
using tok_pipe = primitive;
using tok_background = primitive;
using tok_redirection = primitive;
+using tok_andand = primitive;
+using tok_oror = primitive;
// Define keyword types.
template
@@ -195,14 +197,34 @@ struct alternative {};
static const production_element_t *resolve(const parse_token_t &, const parse_token_t &, \
parse_node_tag_t *);
-// A job_list is a list of jobs, separated by semicolons or newlines
+// A job_list is a list of job_conjunctions, separated by semicolons or newlines
DEF_ALT(job_list) {
- using normal = seq;
+ using normal = seq;
using empty_line = seq;
using empty = grammar::empty;
ALT_BODY(job_list, normal, empty_line, empty);
};
+// Job decorators are 'and' and 'or'. These apply to the whole job.
+DEF_ALT(job_decorator) {
+ using ands = single>;
+ using ors = single>;
+ using empty = grammar::empty;
+ ALT_BODY(job_decorator, ands, ors, empty);
+};
+
+// A job_conjunction is a job followed by a continuation.
+DEF(job_conjunction) produces_sequence {
+ BODY(job_conjunction);
+};
+
+DEF_ALT(job_conjunction_continuation) {
+ using andands = seq;
+ using orors = seq;
+ using empty = grammar::empty;
+ ALT_BODY(job_conjunction_continuation, andands, orors, empty);
+};
+
// A job is a non-empty list of statements, separated by pipes. (Non-empty is useful for cases
// like if statements, where we require a command). To represent "non-empty", we require a
// statement, followed by a possibly empty job_continuation, and then optionally a background
@@ -217,12 +239,12 @@ DEF_ALT(job_continuation) {
// A statement is a normal command, or an if / while / and etc
DEF_ALT(statement) {
- using boolean = single;
+ using nots = single;
using block = single;
using ifs = single;
using switchs = single;
using decorated = single;
- ALT_BODY(statement, boolean, block, ifs, switchs, decorated);
+ ALT_BODY(statement, nots, block, ifs, switchs, decorated);
};
// A block is a conditional, loop, or begin/end
@@ -231,7 +253,7 @@ produces_sequence, job, tok_end, andor_job_list, job_list>{
+produces_sequence, job_conjunction, tok_end, andor_job_list, job_list>{
BODY(if_clause)};
DEF_ALT(else_clause) {
@@ -280,7 +302,8 @@ produces_sequence, tok_string, keyword, job, tok_end, andor_job_list>{BODY(while_header)};
+produces_sequence, job_conjunction, tok_end, andor_job_list>{
+ BODY(while_header)};
DEF(begin_header) produces_single>{BODY(begin_header)};
@@ -289,19 +312,17 @@ DEF(function_header)
produces_sequence, argument, argument_list, tok_end>{
BODY(function_header)};
-// A boolean statement is AND or OR or NOT
-DEF_ALT(boolean_statement) {
- using ands = seq, statement>;
- using ors = seq, statement>;
+DEF_ALT(not_statement) {
using nots = seq, statement>;
- ALT_BODY(boolean_statement, ands, ors, nots);
+ using exclams = seq, statement>;
+ ALT_BODY(not_statement, nots, exclams);
};
// An andor_job_list is zero or more job lists, where each starts with an `and` or `or` boolean
// statement.
DEF_ALT(andor_job_list) {
using empty = grammar::empty;
- using andor_job = seq;
+ using andor_job = seq;
using empty_line = seq;
ALT_BODY(andor_job_list, empty, andor_job, empty_line);
};
diff --git a/src/parse_grammar_elements.inc b/src/parse_grammar_elements.inc
index ff087eeb7..79339b6cb 100644
--- a/src/parse_grammar_elements.inc
+++ b/src/parse_grammar_elements.inc
@@ -1,6 +1,9 @@
// Define ELEM before including this file.
ELEM(job_list)
ELEM(job)
+ELEM(job_decorator)
+ELEM(job_conjunction)
+ELEM(job_conjunction_continuation)
ELEM(job_continuation)
ELEM(statement)
ELEM(if_statement)
@@ -16,7 +19,7 @@ ELEM(for_header)
ELEM(while_header)
ELEM(begin_header)
ELEM(function_header)
-ELEM(boolean_statement)
+ELEM(not_statement)
ELEM(andor_job_list)
ELEM(decorated_statement)
ELEM(plain_statement)
diff --git a/src/parse_productions.cpp b/src/parse_productions.cpp
index e97b27208..fefb0377b 100644
--- a/src/parse_productions.cpp
+++ b/src/parse_productions.cpp
@@ -61,6 +61,41 @@ RESOLVE(job_list) {
}
}
+// A job decorator is AND or OR
+RESOLVE(job_decorator) {
+ UNUSED(token2);
+
+ switch (token1.keyword) {
+ case parse_keyword_and: {
+ *out_tag = parse_bool_and;
+ return production_for();
+ }
+ case parse_keyword_or: {
+ *out_tag = parse_bool_or;
+ return production_for();
+ }
+ default: {
+ *out_tag = parse_bool_none;
+ return production_for();
+ }
+ }
+}
+
+RESOLVE(job_conjunction_continuation) {
+ UNUSED(token2);
+ UNUSED(out_tag);
+ switch (token1.type) {
+ case parse_token_type_andand:
+ *out_tag = parse_bool_and;
+ return production_for();
+ case parse_token_type_oror:
+ *out_tag = parse_bool_or;
+ return production_for();
+ default:
+ return production_for();
+ }
+}
+
RESOLVE(job_continuation) {
UNUSED(token2);
UNUSED(out_tag);
@@ -108,10 +143,9 @@ RESOLVE(statement) {
switch (token1.type) {
case parse_token_type_string: {
switch (token1.keyword) {
- case parse_keyword_and:
- case parse_keyword_or:
- case parse_keyword_not: {
- return production_for();
+ case parse_keyword_not:
+ case parse_keyword_exclam: {
+ return production_for();
}
case parse_keyword_for:
case parse_keyword_while:
@@ -182,6 +216,19 @@ RESOLVE(case_item_list) {
return production_for();
}
+RESOLVE(not_statement) {
+ UNUSED(token2);
+ UNUSED(out_tag);
+ switch (token1.keyword) {
+ case parse_keyword_not:
+ return production_for();
+ case parse_keyword_exclam:
+ return production_for();
+ default:
+ return NO_PRODUCTION;
+ }
+}
+
RESOLVE(andor_job_list) {
UNUSED(out_tag);
@@ -245,27 +292,6 @@ RESOLVE(block_header) {
}
}
-// A boolean statement is AND or OR or NOT.
-RESOLVE(boolean_statement) {
- UNUSED(token2);
-
- switch (token1.keyword) {
- case parse_keyword_and: {
- *out_tag = parse_bool_and;
- return production_for();
- }
- case parse_keyword_or: {
- *out_tag = parse_bool_or;
- return production_for();
- }
- case parse_keyword_not: {
- *out_tag = parse_bool_not;
- return production_for();
- }
- default: { return NO_PRODUCTION; }
- }
-}
-
RESOLVE(decorated_statement) {
// If this is e.g. 'command --help' then the command is 'command' and not a decoration. If the
@@ -356,6 +382,8 @@ const production_element_t *parse_productions::production_for_token(parse_token_
case parse_token_type_pipe:
case parse_token_type_redirection:
case parse_token_type_background:
+ case parse_token_type_andand:
+ case parse_token_type_oror:
case parse_token_type_end:
case parse_token_type_terminate: {
debug(0, "Terminal token type %ls passed to %s", token_type_description(node_type),
diff --git a/src/parse_tree.cpp b/src/parse_tree.cpp
index a284b346d..9c51025f4 100644
--- a/src/parse_tree.cpp
+++ b/src/parse_tree.cpp
@@ -138,30 +138,29 @@ static wcstring token_type_user_presentable_description(
switch (type) {
// Hackish. We only support the following types.
- case symbol_statement: {
+ case symbol_statement:
return L"a command";
- }
- case symbol_argument: {
+ case symbol_argument:
return L"an argument";
- }
- case parse_token_type_string: {
+ case symbol_job:
+ case symbol_job_list:
+ return L"a job";
+ case parse_token_type_string:
return L"a string";
- }
- case parse_token_type_pipe: {
+ case parse_token_type_pipe:
return L"a pipe";
- }
- case parse_token_type_redirection: {
+ case parse_token_type_redirection:
return L"a redirection";
- }
- case parse_token_type_background: {
+ case parse_token_type_background:
return L"a '&'";
- }
- case parse_token_type_end: {
+ case parse_token_type_andand:
+ return L"'&&'";
+ case parse_token_type_oror:
+ return L"'||'";
+ case parse_token_type_end:
return L"end of the statement";
- }
- case parse_token_type_terminate: {
+ case parse_token_type_terminate:
return L"end of the input";
- }
default: { return format_string(L"a %ls", token_type_description(type)); }
}
}
@@ -213,43 +212,32 @@ wcstring parse_token_t::user_presentable_description() const {
/// Convert from tokenizer_t's token type to a parse_token_t type.
static inline parse_token_type_t parse_token_type_from_tokenizer_token(
enum token_type tokenizer_token_type) {
- parse_token_type_t result = token_type_invalid;
switch (tokenizer_token_type) {
- case TOK_STRING: {
- result = parse_token_type_string;
- break;
- }
- case TOK_PIPE: {
- result = parse_token_type_pipe;
- break;
- }
- case TOK_END: {
- result = parse_token_type_end;
- break;
- }
- case TOK_BACKGROUND: {
- result = parse_token_type_background;
- break;
- }
- case TOK_REDIRECT: {
- result = parse_token_type_redirection;
- break;
- }
- case TOK_ERROR: {
- result = parse_special_type_tokenizer_error;
- break;
- }
- case TOK_COMMENT: {
- result = parse_special_type_comment;
- break;
- }
- default: {
- debug(0, "Bad token type %d passed to %s", (int)tokenizer_token_type, __FUNCTION__);
- DIE("bad token type");
- break;
- }
+ case TOK_NONE:
+ DIE("TOK_NONE passed to parse_token_type_from_tokenizer_token");
+ return token_type_invalid;
+ case TOK_STRING:
+ return parse_token_type_string;
+ case TOK_PIPE:
+ return parse_token_type_pipe;
+ case TOK_ANDAND:
+ return parse_token_type_andand;
+ case TOK_OROR:
+ return parse_token_type_oror;
+ case TOK_END:
+ return parse_token_type_end;
+ case TOK_BACKGROUND:
+ return parse_token_type_background;
+ case TOK_REDIRECT:
+ return parse_token_type_redirection;
+ case TOK_ERROR:
+ return parse_special_type_tokenizer_error;
+ case TOK_COMMENT:
+ return parse_special_type_comment;
}
- return result;
+ debug(0, "Bad token type %d passed to %s", (int)tokenizer_token_type, __FUNCTION__);
+ DIE("bad token type");
+ return token_type_invalid;
}
/// Helper function for parse_dump_tree().
@@ -674,37 +662,8 @@ void parse_ll_t::parse_error_failed_production(struct parse_stack_element_t &sta
parse_token_t token) {
fatal_errored = true;
if (this->should_generate_error_messages) {
- bool done = false;
-
- // Check for ||.
- if (token.type == parse_token_type_pipe && token.source_start > 0) {
- // Here we wanted a statement and instead got a pipe. See if this is a double pipe: foo
- // || bar. If so, we have a special error message.
- const parse_node_t *prev_pipe = nodes.find_node_matching_source_location(
- parse_token_type_pipe, token.source_start - 1, NULL);
- if (prev_pipe != NULL) {
- // The pipe of the previous job abuts our current token. So we have ||.
- this->parse_error(token, parse_error_double_pipe, ERROR_BAD_OR);
- done = true;
- }
- }
-
- // Check for &&.
- if (!done && token.type == parse_token_type_background && token.source_start > 0) {
- // Check to see if there was a previous token_background.
- const parse_node_t *prev_background = nodes.find_node_matching_source_location(
- parse_token_type_background, token.source_start - 1, NULL);
- if (prev_background != NULL) {
- // We have &&.
- this->parse_error(token, parse_error_double_background, ERROR_BAD_AND);
- done = true;
- }
- }
-
- if (!done) {
- const wcstring expected = stack_elem.user_presentable_description();
- this->parse_error_unexpected_token(expected.c_str(), token);
- }
+ const wcstring expected = stack_elem.user_presentable_description();
+ this->parse_error_unexpected_token(expected.c_str(), token);
}
}
@@ -771,6 +730,8 @@ static bool type_is_terminal_type(parse_token_type_t type) {
case parse_token_type_redirection:
case parse_token_type_background:
case parse_token_type_end:
+ case parse_token_type_andand:
+ case parse_token_type_oror:
case parse_token_type_terminate: {
return true;
}
@@ -999,7 +960,7 @@ static inline parse_keyword_t keyword_with_name(const wchar_t *name) {
static bool is_keyword_char(wchar_t c) {
return (c >= L'a' && c <= L'z') || (c >= L'A' && c <= L'Z') || (c >= L'0' && c <= L'9') ||
- c == L'\'' || c == L'"' || c == L'\\' || c == '\n';
+ c == L'\'' || c == L'"' || c == L'\\' || c == '\n' || c == L'!';
}
/// Given a token, returns the keyword it matches, or parse_keyword_none.
diff --git a/src/parse_tree.h b/src/parse_tree.h
index ac46d6500..184037f1c 100644
--- a/src/parse_tree.h
+++ b/src/parse_tree.h
@@ -95,7 +95,7 @@ class parse_node_t {
// This is used to store e.g. the statement decoration.
parse_node_tag_t tag : 4;
// Description
- wcstring describe(void) const;
+ wcstring describe() const;
// Constructor
explicit parse_node_t(parse_token_type_t ty)
diff --git a/src/parse_util.cpp b/src/parse_util.cpp
index 24007a988..2927763ee 100644
--- a/src/parse_util.cpp
+++ b/src/parse_util.cpp
@@ -1047,6 +1047,7 @@ parser_test_error_bits_t parse_util_detect_errors_in_argument(tnode_t job,
parse_error_list_t *parse_errors) {
+ namespace g = grammar;
auto source_range = job.source_range();
if (!source_range) return false;
@@ -1056,29 +1057,30 @@ static bool detect_errors_in_backgrounded_job(tnode_t job,
// foo & ; or bar
// if foo & ; end
// while foo & ; end
- if (job.try_get_parent()) {
+ auto job_conj = job.try_get_parent();
+ if (job_conj.try_get_parent()) {
errored = append_syntax_error(parse_errors, source_range->start,
BACKGROUND_IN_CONDITIONAL_ERROR_MSG);
- } else if (job.try_get_parent()) {
+ } else if (job_conj.try_get_parent()) {
errored = append_syntax_error(parse_errors, source_range->start,
BACKGROUND_IN_CONDITIONAL_ERROR_MSG);
- } else if (auto job_list = job.try_get_parent()) {
+ } else if (auto jlist = job_conj.try_get_parent()) {
// This isn't very complete, e.g. we don't catch 'foo & ; not and bar'.
- // Build the job list and then advance it by one.
- auto first_job = job_list.next_in_list();
- assert(first_job == job && "Expected first job to be the node we found");
- (void)first_job;
- // Try getting the next job as a boolean statement.
- auto next_job = job_list.next_in_list();
- tnode_t next_stmt = next_job.child<0>();
- if (auto bool_stmt = next_stmt.try_get_child()) {
+ // Fetch the job list and then advance it by one.
+ auto first_jconj = jlist.next_in_list();
+ assert(first_jconj == job.try_get_parent() &&
+ "Expected first job to be the node we found");
+ (void)first_jconj;
+
+ // Try getting the next job's decorator.
+ if (auto next_job_dec = jlist.next_in_list()) {
// The next job is indeed a boolean statement.
- parse_bool_statement_type_t bool_type = bool_statement_type(bool_stmt);
- if (bool_type == parse_bool_and) { // this is not allowed
- errored = append_syntax_error(parse_errors, bool_stmt.source_range()->start,
+ parse_bool_statement_type_t bool_type = bool_statement_type(next_job_dec);
+ if (bool_type == parse_bool_and) {
+ errored = append_syntax_error(parse_errors, next_job_dec.source_range()->start,
BOOL_AFTER_BACKGROUND_ERROR_MSG, L"and");
- } else if (bool_type == parse_bool_or) { // this is not allowed
- errored = append_syntax_error(parse_errors, bool_stmt.source_range()->start,
+ } else if (bool_type == parse_bool_or) {
+ errored = append_syntax_error(parse_errors, next_job_dec.source_range()->start,
BOOL_AFTER_BACKGROUND_ERROR_MSG, L"or");
}
}
@@ -1096,7 +1098,8 @@ static bool detect_errors_in_plain_statement(const wcstring &buff_src,
// In a few places below, we want to know if we are in a pipeline.
tnode_t st = pst.try_get_parent().try_get_parent();
- const bool is_in_pipeline = statement_is_in_pipeline(st, true /* count first */);
+ pipeline_position_t pipe_pos = get_pipeline_position(st);
+ bool is_in_pipeline = (pipe_pos != pipeline_position_t::none);
// We need to know the decoration.
const enum parse_statement_decoration_t decoration = get_decoration(pst);
@@ -1106,6 +1109,19 @@ static bool detect_errors_in_plain_statement(const wcstring &buff_src,
errored = append_syntax_error(parse_errors, source_start, EXEC_ERR_MSG, L"exec");
}
+ // This is a somewhat stale check that 'and' and 'or' are not in pipelines, except at the
+ // beginning. We can't disallow them as commands entirely because we need to support 'and
+ // --help', etc.
+ if (pipe_pos == pipeline_position_t::subsequent) {
+ // check if our command is 'and' or 'or'. This is very clumsy; we don't catch e.g. quoted
+ // commands.
+ wcstring command = pst.child<0>().get_source(buff_src);
+ if (command == L"and" || command == L"or") {
+ errored =
+ append_syntax_error(parse_errors, source_start, EXEC_ERR_MSG, command.c_str());
+ }
+ }
+
if (maybe_t mcommand = command_for_plain_statement(pst, buff_src)) {
wcstring command = std::move(*mcommand);
// Check that we can expand the command.
@@ -1250,16 +1266,9 @@ parser_test_error_bits_t parse_util_detect_errors(const wcstring &buff_src,
has_unclosed_block = true;
} else if (node.type == symbol_statement && !node.has_source()) {
// Check for a statement without source in a pipeline, i.e. unterminated pipeline.
- has_unclosed_pipe |= statement_is_in_pipeline({&node_tree, &node}, false);
- } else if (node.type == symbol_boolean_statement) {
- // 'or' and 'and' can be in a pipeline, as long as they're first.
- tnode_t gbs{&node_tree, &node};
- parse_bool_statement_type_t type = bool_statement_type(gbs);
- if ((type == parse_bool_and || type == parse_bool_or) &&
- statement_is_in_pipeline(gbs.try_get_parent(),
- false /* don't count first */)) {
- errored = append_syntax_error(&parse_errors, node.source_start, EXEC_ERR_MSG,
- (type == parse_bool_and) ? L"and" : L"or");
+ auto pipe_pos = get_pipeline_position({&node_tree, &node});
+ if (pipe_pos != pipeline_position_t::none) {
+ has_unclosed_pipe = true;
}
} else if (node.type == symbol_argument) {
tnode_t arg{&node_tree, &node};
diff --git a/src/tnode.cpp b/src/tnode.cpp
index e0ee20ea5..8e4a447fb 100644
--- a/src/tnode.cpp
+++ b/src/tnode.cpp
@@ -46,10 +46,14 @@ enum parse_statement_decoration_t get_decoration(tnode_t stmt) {
+enum parse_bool_statement_type_t bool_statement_type(tnode_t stmt) {
return static_cast(stmt.tag());
}
+enum parse_bool_statement_type_t bool_statement_type(tnode_t cont) {
+ return static_cast(cont.tag());
+}
+
maybe_t redirection_type(tnode_t redirection,
const wcstring &src, int *out_fd,
wcstring *out_target) {
@@ -107,24 +111,35 @@ bool job_node_is_background(tnode_t job) {
return bg.tag() == parse_background;
}
-bool statement_is_in_pipeline(tnode_t st, bool include_first) {
+parse_bool_statement_type_t get_decorator(tnode_t conj) {
+ using namespace grammar;
+ tnode_t dec;
+ // We have two possible parents: job_list and andor_job_list.
+ if (auto p = conj.try_get_parent()) {
+ dec = p.require_get_child();
+ } else if (auto p = conj.try_get_parent()) {
+ dec = p.require_get_child();
+ }
+ // note this returns 0 (none) if dec is empty.
+ return bool_statement_type(dec);
+}
+
+pipeline_position_t get_pipeline_position(tnode_t st) {
using namespace grammar;
if (!st) {
- return false;
+ return pipeline_position_t::none;
}
// If we're part of a job continuation, we're definitely in a pipeline.
if (st.try_get_parent()) {
- return true;
+ return pipeline_position_t::subsequent;
}
- // If include_first is set, check if we're the beginning of a job, and if so, whether that job
+ // Check if we're the beginning of a job, and if so, whether that job
// has a non-empty continuation.
- if (include_first) {
- tnode_t jc = st.try_get_parent().child<1>();
- if (jc.try_get_child()) {
- return true;
- }
+ tnode_t jc = st.try_get_parent().child<1>();
+ if (jc.try_get_child()) {
+ return pipeline_position_t::first;
}
- return false;
+ return pipeline_position_t::none;
}
diff --git a/src/tnode.h b/src/tnode.h
index c7edc5da6..60fac38b6 100644
--- a/src/tnode.h
+++ b/src/tnode.h
@@ -91,6 +91,8 @@ class tnode_t {
// Helper to return whether the given tree is the same as ours.
bool matches_node_tree(const parse_node_tree_t &t) const { return &t == tree; }
+ const parse_node_tree_t *get_tree() const { return tree; }
+
bool has_source() const { return nodeptr && nodeptr->has_source(); }
// return the tag, or 0 if missing.
@@ -147,7 +149,7 @@ class tnode_t {
const parse_node_t *child = nullptr;
if (nodeptr) child = tree->get_child(*nodeptr, Index);
if (child && child->type == ChildType::token) return {tree, child};
- return {};
+ return {tree, nullptr};
}
/// assert that this is not empty and that the child at index Index has the given type, then
@@ -206,7 +208,7 @@ class tnode_t {
// We require that we can contain ourselves, and ItemType as well.
static_assert(child_type_possible(), "Is not a list");
static_assert(child_type_possible(), "Is not a list of that type");
- if (!nodeptr) return {};
+ if (!nodeptr) return {tree, nullptr};
const parse_node_t *next =
tree->next_node_in_node_list(*nodeptr, ItemType::token, &nodeptr);
return {tree, next};
@@ -232,7 +234,9 @@ maybe_t command_for_plain_statement(tnode_t
parse_statement_decoration_t get_decoration(tnode_t stmt);
/// Return the type for a boolean statement.
-enum parse_bool_statement_type_t bool_statement_type(tnode_t stmt);
+enum parse_bool_statement_type_t bool_statement_type(tnode_t stmt);
+
+enum parse_bool_statement_type_t bool_statement_type(tnode_t stmt);
/// Given a redirection, get the redirection type (or none) and target (file path, or fd).
maybe_t redirection_type(tnode_t redirection,
@@ -249,9 +253,19 @@ arguments_node_list_t get_argument_nodes(tnode_t);
-/// Return whether the statement is part of a pipeline. If include_first is set, the first command
-/// in a pipeline is considered part of it; otherwise only the second or additional commands are.
-bool statement_is_in_pipeline(tnode_t st, bool include_first);
+/// If the conjunction is has a decorator (and/or), return it; otherwise return none. This only
+/// considers the leading conjunction, e.g. in `and true || false` only the 'true' conjunction will
+/// return 'and'.
+parse_bool_statement_type_t get_decorator(tnode_t);
+
+/// Return whether the statement is part of a pipeline.
+/// This doesn't detect e.g. pipelines involving our parent's block statements.
+enum class pipeline_position_t {
+ none, // not part of a pipeline
+ first, // first command in a pipeline
+ subsequent // second or further command in a pipeline
+};
+pipeline_position_t get_pipeline_position(tnode_t st);
/// Check whether an argument_list is a root list.
inline bool argument_list_is_root(tnode_t list) {
diff --git a/src/tokenizer.cpp b/src/tokenizer.cpp
index 1bf300f01..eb49cf263 100644
--- a/src/tokenizer.cpp
+++ b/src/tokenizer.cpp
@@ -528,16 +528,28 @@ maybe_t tokenizer_t::tok_next() {
break;
}
case L'&': {
- result.type = TOK_BACKGROUND;
- result.length = 1;
- this->buff++;
+ if (this->buff[1] == L'&') {
+ result.type = TOK_ANDAND;
+ result.length = 2;
+ this->buff += 2;
+ } else {
+ result.type = TOK_BACKGROUND;
+ result.length = 1;
+ this->buff++;
+ }
break;
}
case L'|': {
- result.type = TOK_PIPE;
- result.redirected_fd = 1;
- result.length = 1;
- this->buff++;
+ if (this->buff[1] == L'|') {
+ result.type = TOK_OROR;
+ result.length = 2;
+ this->buff += 2;
+ } else {
+ result.type = TOK_PIPE;
+ result.redirected_fd = 1;
+ result.length = 1;
+ this->buff++;
+ }
break;
}
case L'>':
diff --git a/src/tokenizer.h b/src/tokenizer.h
index 0c5226ead..e0aa58b50 100644
--- a/src/tokenizer.h
+++ b/src/tokenizer.h
@@ -14,6 +14,8 @@ enum token_type {
TOK_ERROR, /// Error reading token
TOK_STRING, /// String token
TOK_PIPE, /// Pipe token
+ TOK_ANDAND, /// && token
+ TOK_OROR, /// || token
TOK_END, /// End token (semicolon or newline, not literal end)
TOK_REDIRECT, /// redirection token
TOK_BACKGROUND, /// send job to bg token
diff --git a/tests/andandoror.err b/tests/andandoror.err
new file mode 100644
index 000000000..60f61565c
--- /dev/null
+++ b/tests/andandoror.err
@@ -0,0 +1,15 @@
+
+####################
+# Basic && and || support
+
+####################
+# && and || in if statements
+
+####################
+# && and || in while statements
+
+####################
+# Nots
+
+####################
+# Complex scenarios
diff --git a/tests/andandoror.in b/tests/andandoror.in
new file mode 100644
index 000000000..fff23d3b9
--- /dev/null
+++ b/tests/andandoror.in
@@ -0,0 +1,46 @@
+logmsg "Basic && and || support"
+
+echo first && echo second
+echo third || echo fourth
+true && false ; echo "true && false: $status"
+true || false ; echo "true || false: $status"
+true && false || true ; echo "true && false || true: $status"
+
+logmsg "&& and || in if statements"
+
+if true || false ; echo "if test 1 ok" ; end
+if true && false ; else; echo "if test 2 ok" ; end
+if true && false ; or true ; echo "if test 3 ok" ; end
+if [ 0 = 1 ] || [ 5 -ge 3 ] ; echo "if test 4 ok"; end
+
+logmsg "&& and || in while statements"
+
+set alpha 0
+set beta 0
+set gamma 0
+set delta 0
+while [ $alpha -lt 2 ] && [ $beta -lt 3 ]
+ or [ $gamma -lt 4 ] || [ $delta -lt 5 ]
+ echo $alpha $beta $gamma
+ set alpha ( math $alpha + 1 )
+ set beta ( math $beta + 1 )
+ set gamma ( math $gamma + 1 )
+ set delta ( math $delta + 1 )
+end
+
+logmsg "Nots"
+
+true && ! false ; echo $status
+not true && ! false ; echo $status
+not not not true ; echo $status
+not ! ! not true ; echo $status
+not ! echo not ! ; echo $status
+
+logmsg "Complex scenarios"
+
+begin; echo 1 ; false ; end || begin ; echo 2 && echo 3 ; end
+
+if false && true
+ or not false
+ echo 4
+end
diff --git a/tests/andandoror.out b/tests/andandoror.out
new file mode 100644
index 000000000..649267ebd
--- /dev/null
+++ b/tests/andandoror.out
@@ -0,0 +1,40 @@
+
+####################
+# Basic && and || support
+first
+second
+third
+true && false: 1
+true || false: 0
+true && false || true: 0
+
+####################
+# && and || in if statements
+if test 1 ok
+if test 2 ok
+if test 3 ok
+if test 4 ok
+
+####################
+# && and || in while statements
+0 0 0
+1 1 1
+2 2 2
+3 3 3
+4 4 4
+
+####################
+# Nots
+0
+1
+1
+0
+not !
+0
+
+####################
+# Complex scenarios
+1
+2
+3
+4