Reflect newlines after pipes in fish grammar

The previous attempt to support newlines after pipes changed the lexer to
swallow newlines after encountering a pipe. This has two problems that are
difficult to fix:

1. comments cannot be placed after the pipe
2. fish_indent won't know about the newlines, so it will erase them

Address these problems by removing the lexer behavior, and replacing it
with a new parser symbol "optional_newlines" allowing the newlines to be
reflected directly in the fish grammar.
This commit is contained in:
ridiculousfish 2018-02-18 14:37:44 -08:00
parent ea4e997dc9
commit fd2a0dffa9
12 changed files with 50 additions and 80 deletions

View file

@ -38,6 +38,7 @@ This section is for changes merged to the `major` branch that are not also merge
- Slicing $history (in particular, `$history[1]` for the last executed command) is much faster. - Slicing $history (in particular, `$history[1]` for the last executed command) is much faster.
- The pager will now show the full command instead of just its last line if the number of completions is large (#4702). - The pager will now show the full command instead of just its last line if the number of completions is large (#4702).
- Tildes in file names are now properly escaped in completions (#2274) - Tildes in file names are now properly escaped in completions (#2274)
- A pipe at the end of a line now allows the job to continue on the next line (#1285)
## Other significant changes ## Other significant changes
- Command substitution output is now limited to 10 MB by default (#3822). - Command substitution output is now limited to 10 MB by default (#3822).

View file

@ -739,6 +739,18 @@ static void test_parser() {
err(L"leading pipe not reported properly"); err(L"leading pipe not reported properly");
} }
if (parse_util_detect_errors(L"true | # comment") != PARSER_TEST_INCOMPLETE) {
err(L"comment after pipe not reported as incomplete");
}
if (parse_util_detect_errors(L"true | # comment \n false ")) {
err(L"comment and newline after pipe wrongly reported as error");
}
if (parse_util_detect_errors(L"true | ; false ") != PARSER_TEST_ERROR) {
err(L"semicolon after pipe not detected as error");
}
if (detect_argument_errors(L"foo")) { if (detect_argument_errors(L"foo")) {
err(L"simple argument reported as error"); err(L"simple argument reported as error");
} }

View file

@ -45,6 +45,7 @@ enum parse_token_type_t {
symbol_argument, symbol_argument,
symbol_redirection, symbol_redirection,
symbol_optional_background, symbol_optional_background,
symbol_optional_newlines,
symbol_end_command, symbol_end_command,
// Terminal types. // Terminal types.
parse_token_type_string, parse_token_type_string,
@ -99,6 +100,7 @@ const enum_map<parse_token_type_t> token_enum_map[] = {
{symbol_job, L"symbol_job"}, {symbol_job, L"symbol_job"},
{symbol_job_continuation, L"symbol_job_continuation"}, {symbol_job_continuation, L"symbol_job_continuation"},
{symbol_job_list, L"symbol_job_list"}, {symbol_job_list, L"symbol_job_list"},
{symbol_optional_newlines, L"symbol_optional_newlines"},
{symbol_optional_background, L"symbol_optional_background"}, {symbol_optional_background, L"symbol_optional_background"},
{symbol_plain_statement, L"symbol_plain_statement"}, {symbol_plain_statement, L"symbol_plain_statement"},
{symbol_redirection, L"symbol_redirection"}, {symbol_redirection, L"symbol_redirection"},

View file

@ -1056,7 +1056,7 @@ parse_execution_result_t parse_execution_context_t::populate_job_from_job_node(
if (result != parse_execution_success) { if (result != parse_execution_success) {
break; break;
} }
tnode_t<g::statement> statement = job_cont.require_get_child<g::statement, 1>(); tnode_t<g::statement> statement = job_cont.require_get_child<g::statement, 2>();
// Handle the pipe, whose fd may not be the obvious stdout. // Handle the pipe, whose fd may not be the obvious stdout.
int pipe_write_fd = fd_redirected_by_pipe(get_source(pipe)); int pipe_write_fd = fd_redirected_by_pipe(get_source(pipe));
@ -1071,7 +1071,7 @@ parse_execution_result_t parse_execution_context_t::populate_job_from_job_node(
result = this->populate_job_process(j, processes.back().get(), statement); result = this->populate_job_process(j, processes.back().get(), statement);
// Get the next continuation. // Get the next continuation.
job_cont = job_cont.require_get_child<g::job_continuation, 2>(); job_cont = job_cont.require_get_child<g::job_continuation, 3>();
assert(job_cont); assert(job_cont);
} }

View file

@ -210,7 +210,7 @@ DEF_ALT(job_list) {
DEF(job) produces_sequence<statement, job_continuation, optional_background>{BODY(job)}; DEF(job) produces_sequence<statement, job_continuation, optional_background>{BODY(job)};
DEF_ALT(job_continuation) { DEF_ALT(job_continuation) {
using piped = seq<tok_pipe, statement, job_continuation>; using piped = seq<tok_pipe, optional_newlines, statement, job_continuation>;
using empty = grammar::empty; using empty = grammar::empty;
ALT_BODY(job_continuation, piped, empty); ALT_BODY(job_continuation, piped, empty);
}; };
@ -343,6 +343,13 @@ DEF_ALT(optional_background) {
DEF(end_command) produces_single<keyword<parse_keyword_end>>{BODY(end_command)}; DEF(end_command) produces_single<keyword<parse_keyword_end>>{BODY(end_command)};
// Note optional_newlines only allows newline-style tok_end, not semicolons.
DEF_ALT(optional_newlines) {
using empty = grammar::empty;
using newlines = seq<tok_end, optional_newlines>;
ALT_BODY(optional_newlines, empty, newlines);
};
// A freestanding_argument_list is equivalent to a normal argument list, except it may contain // A freestanding_argument_list is equivalent to a normal argument list, except it may contain
// TOK_END (newlines, and even semicolons, for historical reasons) // TOK_END (newlines, and even semicolons, for historical reasons)
DEF_ALT(freestanding_argument_list) { DEF_ALT(freestanding_argument_list) {

View file

@ -25,6 +25,7 @@ ELEM(arguments_or_redirections_list)
ELEM(argument) ELEM(argument)
ELEM(redirection) ELEM(redirection)
ELEM(optional_background) ELEM(optional_background)
ELEM(optional_newlines)
ELEM(end_command) ELEM(end_command)
ELEM(freestanding_argument_list) ELEM(freestanding_argument_list)
#undef ELEM #undef ELEM

View file

@ -309,6 +309,12 @@ RESOLVE(arguments_or_redirections_list) {
} }
} }
RESOLVE(optional_newlines) {
UNUSED(token2);
if (token1.is_newline) return production_for<newlines>();
return production_for<empty>();
}
RESOLVE(optional_background) { RESOLVE(optional_background) {
UNUSED(token2); UNUSED(token2);
@ -324,10 +330,6 @@ RESOLVE(optional_background) {
} }
} }
#define TEST(SYM) \
case (symbol_##SYM): \
resolver = SYM::resolve; \
break;
const production_element_t *parse_productions::production_for_token(parse_token_type_t node_type, const production_element_t *parse_productions::production_for_token(parse_token_type_t node_type,
const parse_token_t &input1, const parse_token_t &input1,
@ -342,35 +344,14 @@ const production_element_t *parse_productions::production_for_token(parse_token_
parse_node_tag_t *out_tag) = //!OCLINT(unused param) parse_node_tag_t *out_tag) = //!OCLINT(unused param)
NULL; NULL;
switch (node_type) { switch (node_type) {
TEST(job_list) // Handle all of our grammar elements
TEST(job) #define ELEM(SYM) \
TEST(statement) case (symbol_##SYM): \
TEST(job_continuation) resolver = SYM::resolve; \
TEST(boolean_statement) break;
TEST(block_statement) #include "parse_grammar_elements.inc"
TEST(if_statement)
TEST(if_clause)
TEST(else_clause)
TEST(else_continuation)
TEST(switch_statement)
TEST(decorated_statement)
TEST(case_item_list)
TEST(case_item)
TEST(argument_list)
TEST(freestanding_argument_list)
TEST(block_header)
TEST(for_header)
TEST(while_header)
TEST(begin_header)
TEST(function_header)
TEST(plain_statement)
TEST(andor_job_list)
TEST(arguments_or_redirections_list)
TEST(argument)
TEST(redirection)
TEST(optional_background)
TEST(end_command)
// Everything else is an error.
case parse_token_type_string: case parse_token_type_string:
case parse_token_type_pipe: case parse_token_type_pipe:
case parse_token_type_redirection: case parse_token_type_redirection:

View file

@ -256,9 +256,6 @@ static inline parse_token_type_t parse_token_type_from_tokenizer_token(
return result; return result;
} }
#if 1
// Disabled for the 2.2.0 release: https://github.com/fish-shell/fish-shell/issues/1809.
/// Helper function for parse_dump_tree(). /// Helper function for parse_dump_tree().
static void dump_tree_recursive(const parse_node_tree_t &nodes, const wcstring &src, static void dump_tree_recursive(const parse_node_tree_t &nodes, const wcstring &src,
node_offset_t node_idx, size_t indent, wcstring *result, node_offset_t node_idx, size_t indent, wcstring *result,
@ -284,7 +281,6 @@ static void dump_tree_recursive(const parse_node_tree_t &nodes, const wcstring &
append_format(*result, L"%2lu - %l2u ", *line, node_idx); append_format(*result, L"%2lu - %l2u ", *line, node_idx);
result->append(indent * spacesPerIndent, L' '); result->append(indent * spacesPerIndent, L' ');
;
result->append(node.describe()); result->append(node.describe());
if (node.child_count > 0) { if (node.child_count > 0) {
append_format(*result, L" <%lu children>", node.child_count); append_format(*result, L" <%lu children>", node.child_count);
@ -335,7 +331,6 @@ wcstring parse_dump_tree(const parse_node_tree_t &nodes, const wcstring &src) {
} }
return result; return result;
} }
#endif
/// Struct representing elements of the symbol stack, used in the internal state of the LL parser. /// Struct representing elements of the symbol stack, used in the internal state of the LL parser.
struct parse_stack_element_t { struct parse_stack_element_t {
@ -1046,12 +1041,12 @@ static parse_keyword_t keyword_for_token(token_type tok, const wcstring &token)
} }
/// Placeholder invalid token. /// Placeholder invalid token.
static const parse_token_t kInvalidToken = { static constexpr parse_token_t kInvalidToken = {
token_type_invalid, parse_keyword_none, false, false, SOURCE_OFFSET_INVALID, 0}; token_type_invalid, parse_keyword_none, false, false, false, SOURCE_OFFSET_INVALID, 0};
/// Terminal token. /// Terminal token.
static const parse_token_t kTerminalToken = { static constexpr parse_token_t kTerminalToken = {
parse_token_type_terminate, parse_keyword_none, false, false, SOURCE_OFFSET_INVALID, 0}; parse_token_type_terminate, parse_keyword_none, false, false, false, SOURCE_OFFSET_INVALID, 0};
static inline bool is_help_argument(const wcstring &txt) { static inline bool is_help_argument(const wcstring &txt) {
return txt == L"-h" || txt == L"--help"; return txt == L"-h" || txt == L"--help";
@ -1074,6 +1069,7 @@ static inline parse_token_t next_parse_token(tokenizer_t *tok, tok_t *token) {
result.keyword = keyword_for_token(token->type, token->text); result.keyword = keyword_for_token(token->type, token->text);
result.has_dash_prefix = !token->text.empty() && token->text.at(0) == L'-'; result.has_dash_prefix = !token->text.empty() && token->text.at(0) == L'-';
result.is_help_argument = result.has_dash_prefix && is_help_argument(token->text); result.is_help_argument = result.has_dash_prefix && is_help_argument(token->text);
result.is_newline = (result.type == parse_token_type_end && token->text == L"\n");
// These assertions are totally bogus. Basically our tokenizer works in size_t but we work in // These assertions are totally bogus. Basically our tokenizer works in size_t but we work in
// uint32_t to save some space. If we have a source file larger than 4 GB, we'll probably just // uint32_t to save some space. If we have a source file larger than 4 GB, we'll probably just
@ -1157,6 +1153,7 @@ bool parse_tree_from_string(const wcstring &str, parse_tree_flags_t parse_flags,
parse_keyword_none, parse_keyword_none,
false, false,
false, false,
false,
queue[error_token_idx].source_start, queue[error_token_idx].source_start,
queue[error_token_idx].source_length}; queue[error_token_idx].source_length};
parser.accept_tokens(token, kInvalidToken); parser.accept_tokens(token, kInvalidToken);

View file

@ -31,6 +31,7 @@ struct parse_token_t {
enum parse_keyword_t keyword; // Any keyword represented by this token enum parse_keyword_t keyword; // Any keyword represented by this token
bool has_dash_prefix; // Hackish: whether the source contains a dash prefix bool has_dash_prefix; // Hackish: whether the source contains a dash prefix
bool is_help_argument; // Hackish: whether the source looks like '-h' or '--help' bool is_help_argument; // Hackish: whether the source looks like '-h' or '--help'
bool is_newline; // Hackish: if TOK_END, whether the source is a newline.
source_offset_t source_start; source_offset_t source_start;
source_offset_t source_length; source_offset_t source_length;

View file

@ -121,7 +121,7 @@ bool statement_is_in_pipeline(tnode_t<grammar::statement> st, bool include_first
// has a non-empty continuation. // has a non-empty continuation.
if (include_first) { if (include_first) {
tnode_t<job_continuation> jc = st.try_get_parent<job>().child<1>(); tnode_t<job_continuation> jc = st.try_get_parent<job>().child<1>();
if (jc.try_get_child<statement, 1>()) { if (jc.try_get_child<statement, 2>()) {
return true; return true;
} }
} }

View file

@ -102,16 +102,7 @@ bool tokenizer_t::next(struct tok_t *result) {
} }
assert(this->buff >= this->orig_buff); assert(this->buff >= this->orig_buff);
if (this->last_type == TOK_PIPE) { result->length = current_pos >= this->last_pos ? current_pos - this->last_pos : 0;
// Ignore subsequent whitespaces or a newline after a pipe (#1285).
int pipe_pos = current_pos - 1;
while (this->orig_buff[pipe_pos] != L'|') {
pipe_pos--;
}
result->length = pipe_pos - this->last_pos + 1;
} else {
result->length = current_pos >= this->last_pos ? current_pos - this->last_pos : 0;
}
this->tok_next(); this->tok_next();
return true; return true;
@ -538,12 +529,14 @@ void tokenizer_t::tok_next() {
this->last_type = TOK_END; this->last_type = TOK_END;
// fwprintf( stderr, L"End of string\n" ); // fwprintf( stderr, L"End of string\n" );
this->has_next = false; this->has_next = false;
this->last_token.clear();
break; break;
} }
case L'\r': // carriage-return case L'\r': // carriage-return
case L'\n': // newline case L'\n': // newline
case L';': { case L';': {
this->last_type = TOK_END; this->last_type = TOK_END;
this->last_token.assign(1, *this->buff);
this->buff++; this->buff++;
// Hack: when we get a newline, swallow as many as we can. This compresses multiple // Hack: when we get a newline, swallow as many as we can. This compresses multiple
// subsequent newlines into a single one. // subsequent newlines into a single one.
@ -553,7 +546,6 @@ void tokenizer_t::tok_next() {
this->buff++; this->buff++;
} }
} }
this->last_token.clear();
break; break;
} }
case L'&': { case L'&': {
@ -565,7 +557,6 @@ void tokenizer_t::tok_next() {
this->last_token = L"1"; this->last_token = L"1";
this->last_type = TOK_PIPE; this->last_type = TOK_PIPE;
this->buff++; this->buff++;
skip_newline_after_pipe();
break; break;
} }
case L'>': case L'>':
@ -580,9 +571,6 @@ void tokenizer_t::tok_next() {
TOK_CALL_ERROR(this, TOK_OTHER, REDIRECT_ERROR, this->buff); TOK_CALL_ERROR(this, TOK_OTHER, REDIRECT_ERROR, this->buff);
} else { } else {
this->buff += consumed; this->buff += consumed;
if (mode == TOK_PIPE) {
skip_newline_after_pipe();
}
this->last_type = mode; this->last_type = mode;
this->last_token = to_string(fd); this->last_token = to_string(fd);
} }
@ -606,9 +594,6 @@ void tokenizer_t::tok_next() {
TOK_CALL_ERROR(this, TOK_OTHER, PIPE_ERROR, error_location); TOK_CALL_ERROR(this, TOK_OTHER, PIPE_ERROR, error_location);
} else { } else {
this->buff += consumed; this->buff += consumed;
if (mode == TOK_PIPE) {
skip_newline_after_pipe();
}
this->last_type = mode; this->last_type = mode;
this->last_token = to_string(fd); this->last_token = to_string(fd);
} }
@ -621,20 +606,6 @@ void tokenizer_t::tok_next() {
} }
} }
/// If the line ends with pipe, continue to the next line (#1285).
void tokenizer_t::skip_newline_after_pipe() {
while (1) {
if (this->buff[0] == L'\n') {
this->buff++;
break;
} else if (my_iswspace(this->buff[0])) {
this->buff++;
} else {
break;
}
}
}
wcstring tok_first(const wcstring &str) { wcstring tok_first(const wcstring &str) {
wcstring result; wcstring result;
tokenizer_t t(str.c_str(), TOK_SQUASH_ERRORS); tokenizer_t t(str.c_str(), TOK_SQUASH_ERRORS);

View file

@ -107,9 +107,6 @@ class tokenizer_t {
void read_comment(); void read_comment();
void tok_next(); void tok_next();
/// Skip whitespaces and a newline after a pipe at the end of the line.
void skip_newline_after_pipe();
public: public:
/// Constructor for a tokenizer. b is the string that is to be tokenized. It is not copied, and /// Constructor for a tokenizer. b is the string that is to be tokenized. It is not copied, and
/// should not be freed by the caller until after the tokenizer is destroyed. /// should not be freed by the caller until after the tokenizer is destroyed.