diff --git a/Userland/Shell/AST.h b/Userland/Shell/AST.h index 953d917e8a..9aab2bd10c 100644 --- a/Userland/Shell/AST.h +++ b/Userland/Shell/AST.h @@ -451,6 +451,7 @@ public: virtual bool should_override_execution_in_current_process() const { return false; } Position const& position() const { return m_position; } + Position& position() { return m_position; } virtual void clear_syntax_error(); virtual void set_is_syntax_error(SyntaxError& error_node); virtual SyntaxError& syntax_error_node() diff --git a/Userland/Shell/PosixLexer.cpp b/Userland/Shell/PosixLexer.cpp index 716563f82e..bc1953f93c 100644 --- a/Userland/Shell/PosixLexer.cpp +++ b/Userland/Shell/PosixLexer.cpp @@ -961,6 +961,8 @@ StringView Token::type_name() const return "HeredocContents"sv; case Type::AssignmentWord: return "AssignmentWord"sv; + case Type::ListAssignmentWord: + return "ListAssignmentWord"sv; case Type::Bang: return "Bang"sv; case Type::Case: diff --git a/Userland/Shell/PosixLexer.h b/Userland/Shell/PosixLexer.h index e82fc5ff28..24d6b0d102 100644 --- a/Userland/Shell/PosixLexer.h +++ b/Userland/Shell/PosixLexer.h @@ -242,6 +242,7 @@ struct Token { // Not produced by this lexer, but generated in later stages. AssignmentWord, + ListAssignmentWord, Bang, Case, CloseBrace, diff --git a/Userland/Shell/PosixParser.cpp b/Userland/Shell/PosixParser.cpp index 574dd129c4..615cf3afca 100644 --- a/Userland/Shell/PosixParser.cpp +++ b/Userland/Shell/PosixParser.cpp @@ -141,6 +141,27 @@ ErrorOr Parser::fill_token_buffer(Optional starting_reduction) } m_token_index = 0; + // Detect Assignment words, bash-like lists extension + for (size_t i = 1; i < m_token_buffer.size(); ++i) { + // Treat 'ASSIGNMENT_WORD OPEN_PAREN' where ASSIGNMENT_WORD is `word=' and OPEN_PAREN has no preceding trivia as a bash-like list assignment. + auto& token = m_token_buffer[i - 1]; + auto& next_token = m_token_buffer[i]; + + if (token.type != Token::Type::AssignmentWord) + continue; + + if (!token.value.ends_with('=')) + continue; + + if (next_token.type != Token::Type::OpenParen) + continue; + + if (token.position.map([](auto& x) { return x.end_offset + 1; }) != next_token.position.map([](auto& x) { return x.start_offset; })) + continue; + + token.type = Token::Type::ListAssignmentWord; + } + return {}; } @@ -1437,15 +1458,24 @@ ErrorOr> Parser::parse_for_clause() Optional {}); } -RefPtr Parser::parse_word_list() +RefPtr Parser::parse_word_list(AllowNewlines allow_newlines) { Vector> nodes; auto start_position = peek().position.value_or(empty_position()); + if (allow_newlines == AllowNewlines::Yes) { + while (peek().type == Token::Type::Newline) + skip(); + } + for (; peek().type == Token::Type::Word;) { auto word = TRY_OR_THROW_PARSE_ERROR_AT(parse_word(), start_position); nodes.append(word.release_nonnull()); + if (allow_newlines == AllowNewlines::Yes) { + while (peek().type == Token::Type::Newline) + skip(); + } } return make_ref_counted( @@ -1857,6 +1887,32 @@ ErrorOr> Parser::parse_word() return word; } +ErrorOr> Parser::parse_bash_like_list() +{ + if (peek().type != Token::Type::OpenParen) + return nullptr; + + auto start_position = peek().position.value_or(empty_position()); + consume(); + + auto list = parse_word_list(AllowNewlines::Yes); + + if (peek().type != Token::Type::CloseParen) { + return make_ref_counted( + peek().position.value_or(empty_position()), + TRY(String::formatted("Expected ')', not {}", peek().type_name()))); + } + + consume(); + + if (list) + list->position() = start_position.with_end(peek().position.value_or(empty_position())); + else + list = make_ref_counted(start_position.with_end(peek().position.value_or(empty_position())), Vector> {}); + + return list; +} + ErrorOr> Parser::parse_do_group() { if (peek().type != Token::Type::Do) { @@ -1893,6 +1949,7 @@ ErrorOr> Parser::parse_simple_command() auto start_position = peek().position.value_or(empty_position()); Vector definitions; + HashMap> list_assignments; Vector> nodes; for (;;) { @@ -1902,7 +1959,19 @@ ErrorOr> Parser::parse_simple_command() break; } - while (peek().type == Token::Type::AssignmentWord) { + while (is_one_of(peek().type, Token::Type::ListAssignmentWord, Token::Type::AssignmentWord)) { + if (peek().type == Token::Type::ListAssignmentWord) { + auto token = consume(); + auto value = TRY(parse_bash_like_list()); + if (!value) + return make_ref_counted( + token.position.value_or(empty_position()), + TRY(String::formatted("Expected a list literal after '{}', not {}", token.value, peek().type_name()))); + + list_assignments.set(token.value, value.release_nonnull()); + continue; + } + definitions.append(peek().value); if (nodes.is_empty()) { @@ -1939,7 +2008,7 @@ ErrorOr> Parser::parse_simple_command() Token::Type::Word, Token::Type::IoNumber, Token::Type::Less, Token::Type::LessAnd, Token::Type::Great, Token::Type::GreatAnd, Token::Type::DoubleGreat, Token::Type::LessGreat, Token::Type::Clobber)) { - if (!nodes.is_empty()) { + if (!definitions.is_empty() || !list_assignments.is_empty()) { Vector variables; for (auto& definition : definitions) { auto equal_offset = definition.find_byte_offset('='); @@ -1965,12 +2034,27 @@ ErrorOr> Parser::parse_simple_command() variables.append({ move(name), move(expanded_value) }); } + for (auto& [key, value] : list_assignments) { + auto equal_offset = key.find_byte_offset('='); + auto split_offset = equal_offset.value_or(key.bytes().size()); + auto name = make_ref_counted( + empty_position(), + TRY(key.substring_from_byte_offset_with_shared_superstring(0, split_offset))); + + variables.append({ move(name), move(value) }); + } return make_ref_counted(empty_position(), move(variables)); } return nullptr; } + if (!list_assignments.is_empty()) { + return make_ref_counted( + peek().position.value_or(empty_position()), + "List assignments are not allowed as a command prefix"_string); + } + // auto first = true; for (;;) { if (peek().type == Token::Type::Word) { diff --git a/Userland/Shell/PosixParser.h b/Userland/Shell/PosixParser.h index 3a817e842f..b6d4f6b41b 100644 --- a/Userland/Shell/PosixParser.h +++ b/Userland/Shell/PosixParser.h @@ -21,8 +21,13 @@ public: (void)fill_token_buffer(starting_reduction); } + enum class AllowNewlines { + No, + Yes, + }; + RefPtr parse(); - RefPtr parse_word_list(); + RefPtr parse_word_list(AllowNewlines = AllowNewlines::No); struct Error { DeprecatedString message; @@ -94,6 +99,7 @@ private: ErrorOr> parse_io_file(AST::Position, Optional fd); ErrorOr> parse_io_here(AST::Position, Optional fd); ErrorOr> parse_word(); + ErrorOr> parse_bash_like_list(); ErrorOr parse_case_list(); template