Shell: Add support for the bashy list literals in POSIX mode

This is currently only allowed in assignments, where x=(...) is parsed
as the assignment of the list (...) to the variable x.
This commit is contained in:
Ali Mohammad Pur 2023-10-05 13:09:43 +03:30 committed by Ali Mohammad Pur
parent ebe254a6d3
commit dc495e299e
Notes: sideshowbarker 2024-07-17 09:39:38 +09:00
5 changed files with 98 additions and 4 deletions

View file

@ -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()

View file

@ -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:

View file

@ -242,6 +242,7 @@ struct Token {
// Not produced by this lexer, but generated in later stages.
AssignmentWord,
ListAssignmentWord,
Bang,
Case,
CloseBrace,

View file

@ -141,6 +141,27 @@ ErrorOr<void> Parser::fill_token_buffer(Optional<Reduction> 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<RefPtr<AST::Node>> Parser::parse_for_clause()
Optional<AST::Position> {});
}
RefPtr<AST::Node> Parser::parse_word_list()
RefPtr<AST::Node> Parser::parse_word_list(AllowNewlines allow_newlines)
{
Vector<NonnullRefPtr<AST::Node>> 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<AST::ListConcatenate>(
@ -1857,6 +1887,32 @@ ErrorOr<RefPtr<AST::Node>> Parser::parse_word()
return word;
}
ErrorOr<RefPtr<AST::Node>> 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<AST::SyntaxError>(
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<AST::ListConcatenate>(start_position.with_end(peek().position.value_or(empty_position())), Vector<NonnullRefPtr<AST::Node>> {});
return list;
}
ErrorOr<RefPtr<AST::Node>> Parser::parse_do_group()
{
if (peek().type != Token::Type::Do) {
@ -1893,6 +1949,7 @@ ErrorOr<RefPtr<AST::Node>> Parser::parse_simple_command()
auto start_position = peek().position.value_or(empty_position());
Vector<String> definitions;
HashMap<String, NonnullRefPtr<AST::Node>> list_assignments;
Vector<NonnullRefPtr<AST::Node>> nodes;
for (;;) {
@ -1902,7 +1959,19 @@ ErrorOr<RefPtr<AST::Node>> 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<AST::SyntaxError>(
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<RefPtr<AST::Node>> 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<AST::VariableDeclarations::Variable> variables;
for (auto& definition : definitions) {
auto equal_offset = definition.find_byte_offset('=');
@ -1965,12 +2034,27 @@ ErrorOr<RefPtr<AST::Node>> 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<AST::BarewordLiteral>(
empty_position(),
TRY(key.substring_from_byte_offset_with_shared_superstring(0, split_offset)));
variables.append({ move(name), move(value) });
}
return make_ref_counted<AST::VariableDeclarations>(empty_position(), move(variables));
}
return nullptr;
}
if (!list_assignments.is_empty()) {
return make_ref_counted<AST::SyntaxError>(
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) {

View file

@ -21,8 +21,13 @@ public:
(void)fill_token_buffer(starting_reduction);
}
enum class AllowNewlines {
No,
Yes,
};
RefPtr<AST::Node> parse();
RefPtr<AST::Node> parse_word_list();
RefPtr<AST::Node> parse_word_list(AllowNewlines = AllowNewlines::No);
struct Error {
DeprecatedString message;
@ -94,6 +99,7 @@ private:
ErrorOr<RefPtr<AST::Node>> parse_io_file(AST::Position, Optional<int> fd);
ErrorOr<RefPtr<AST::Node>> parse_io_here(AST::Position, Optional<int> fd);
ErrorOr<RefPtr<AST::Node>> parse_word();
ErrorOr<RefPtr<AST::Node>> parse_bash_like_list();
ErrorOr<CaseItemsResult> parse_case_list();
template<typename... Ts>