Просмотр исходного кода

Shell: Start implementing a POSIX-compliant parser

The parser is still very much a work-in-progress, but it can currently
parse most of the basic bits, the only *completely* unimplemented things
in the parser are:
- heredocs (io_here)
- alias expansion
- arithmetic expansion

There are a whole suite of bugs, and syntax highlighting is unreliable
at best.
For now, this is not attached anywhere, a future commit will enable it
for /bin/sh or a `Shell --posix` invocation.
Ali Mohammad Pur 2 лет назад
Родитель
Сommit
2a276c86d4

+ 4 - 0
AK/Debug.h.in

@@ -410,6 +410,10 @@
 #cmakedefine01 SHELL_JOB_DEBUG
 #endif
 
+#ifndef SHELL_POSIX_PARSER_DEBUG
+#cmakedefine01 SHELL_POSIX_PARSER_DEBUG
+#endif
+
 #ifndef SOLITAIRE_DEBUG
 #cmakedefine01 SOLITAIRE_DEBUG
 #endif

+ 1 - 0
Meta/CMake/all_the_debug_macros.cmake

@@ -167,6 +167,7 @@ set(SH_DEBUG ON)
 set(SHELL_JOB_DEBUG ON)
 set(SH_LANGUAGE_SERVER_DEBUG ON)
 set(SHARED_QUEUE_DEBUG ON)
+set(SHELL_POSIX_PARSER_DEBUG ON)
 set(SIGNAL_DEBUG ON)
 set(SLAVEPTY_DEBUG ON)
 set(SMP_DEBUG ON)

+ 21 - 2
Userland/Shell/AST.cpp

@@ -3004,6 +3004,24 @@ RefPtr<Value> Juxtaposition::run(RefPtr<Shell> shell)
     auto left = left_value->resolve_as_list(shell);
     auto right = right_value->resolve_as_list(shell);
 
+    if (m_mode == Mode::StringExpand) {
+        Vector<DeprecatedString> result;
+        result.ensure_capacity(left.size() + right.size());
+
+        for (auto& left_item : left)
+            result.append(left_item);
+
+        if (!result.is_empty() && !right.is_empty()) {
+            auto& last = result.last();
+            last = DeprecatedString::formatted("{}{}", last, right.first());
+            right.take_first();
+        }
+        for (auto& right_item : right)
+            result.append(right_item);
+
+        return make_ref_counted<ListValue>(move(result));
+    }
+
     if (left_value->is_string() && right_value->is_string()) {
 
         VERIFY(left.size() == 1);
@@ -3016,7 +3034,7 @@ RefPtr<Value> Juxtaposition::run(RefPtr<Shell> shell)
         return make_ref_counted<StringValue>(builder.to_deprecated_string());
     }
 
-    // Otherwise, treat them as lists and create a list product.
+    // Otherwise, treat them as lists and create a list product (or just append).
     if (left.is_empty() || right.is_empty())
         return make_ref_counted<ListValue>({});
 
@@ -3114,10 +3132,11 @@ HitTestResult Juxtaposition::hit_test_position(size_t offset) const
     return result;
 }
 
-Juxtaposition::Juxtaposition(Position position, NonnullRefPtr<Node> left, NonnullRefPtr<Node> right)
+Juxtaposition::Juxtaposition(Position position, NonnullRefPtr<Node> left, NonnullRefPtr<Node> right, Juxtaposition::Mode mode)
     : Node(move(position))
     , m_left(move(left))
     , m_right(move(right))
+    , m_mode(mode)
 {
     if (m_left->is_syntax_error())
         set_is_syntax_error(m_left->syntax_error_node());

+ 17 - 1
Userland/Shell/AST.h

@@ -47,6 +47,16 @@ struct Position {
     } start_line, end_line;
 
     bool contains(size_t offset) const { return start_offset <= offset && offset <= end_offset; }
+
+    Position with_end(Position const& end) const
+    {
+        return {
+            .start_offset = start_offset,
+            .end_offset = end.end_offset,
+            .start_line = start_line,
+            .end_line = end.end_line,
+        };
+    }
 };
 
 struct NameWithPosition {
@@ -989,6 +999,7 @@ public:
     NonnullRefPtr<Node> const& condition() const { return m_condition; }
     RefPtr<Node> const& true_branch() const { return m_true_branch; }
     RefPtr<Node> const& false_branch() const { return m_false_branch; }
+    RefPtr<Node>& false_branch() { return m_false_branch; }
     Optional<Position> const else_position() const { return m_else_position; }
 
 private:
@@ -1301,7 +1312,11 @@ private:
 
 class Juxtaposition final : public Node {
 public:
-    Juxtaposition(Position, NonnullRefPtr<Node>, NonnullRefPtr<Node>);
+    enum class Mode {
+        ListExpand,
+        StringExpand,
+    };
+    Juxtaposition(Position, NonnullRefPtr<Node>, NonnullRefPtr<Node>, Mode = Mode::ListExpand);
     virtual ~Juxtaposition();
     virtual void visit(NodeVisitor& visitor) override { visitor.visit(this); }
 
@@ -1318,6 +1333,7 @@ private:
 
     NonnullRefPtr<Node> m_left;
     NonnullRefPtr<Node> m_right;
+    Mode m_mode { Mode::ListExpand };
 };
 
 class Heredoc final : public Node {

+ 2 - 0
Userland/Shell/CMakeLists.txt

@@ -12,6 +12,8 @@ set(SOURCES
     Job.cpp
     NodeVisitor.cpp
     Parser.cpp
+    PosixLexer.cpp
+    PosixParser.cpp
     Shell.cpp
 )
 

+ 2 - 1
Userland/Shell/Formatter.cpp

@@ -7,6 +7,7 @@
 #include "Formatter.h"
 #include "AST.h"
 #include "Parser.h"
+#include "PosixParser.h"
 #include <AK/Hex.h>
 #include <AK/ScopedValueRollback.h>
 #include <AK/TemporaryChange.h>
@@ -15,7 +16,7 @@ namespace Shell {
 
 DeprecatedString Formatter::format()
 {
-    auto node = m_root_node ? m_root_node : Parser(m_source).parse();
+    auto node = m_root_node ?: (m_parse_as_posix ? Posix::Parser(m_source).parse() : Parser(m_source).parse());
     if (m_cursor >= 0)
         m_output_cursor = m_cursor;
 

+ 4 - 1
Userland/Shell/Formatter.h

@@ -19,10 +19,11 @@ namespace Shell {
 
 class Formatter final : public AST::NodeVisitor {
 public:
-    Formatter(StringView source, ssize_t cursor = -1)
+    Formatter(StringView source, ssize_t cursor = -1, bool parse_as_posix = false)
         : m_builders({ StringBuilder { round_up_to_power_of_two(source.length(), 16) } })
         , m_source(source)
         , m_cursor(cursor)
+        , m_parse_as_posix(parse_as_posix)
     {
         if (m_source.is_empty())
             return;
@@ -124,6 +125,8 @@ private:
 
     StringView m_trivia;
     Vector<DeprecatedString> m_heredocs_to_append_after_sequence;
+
+    bool m_parse_as_posix { false };
 };
 
 }

+ 155 - 0
Userland/Shell/ImmediateFunctions.cpp

@@ -453,6 +453,161 @@ RefPtr<AST::Node> Shell::immediate_join(AST::ImmediateExpression& invoking_node,
     return AST::make_ref_counted<AST::StringLiteral>(invoking_node.position(), builder.to_deprecated_string(), AST::StringLiteral::EnclosureType::None);
 }
 
+RefPtr<AST::Node> Shell::immediate_value_or_default(AST::ImmediateExpression& invoking_node, NonnullRefPtrVector<AST::Node> const& arguments)
+{
+    if (arguments.size() != 2) {
+        raise_error(ShellError::EvaluatedSyntaxError, "Expected exactly 2 arguments to value_or_default", invoking_node.position());
+        return nullptr;
+    }
+
+    auto name = const_cast<AST::Node&>(arguments.first()).run(*this)->resolve_as_string(*this);
+    if (!local_variable_or(name, ""sv).is_empty())
+        return make_ref_counted<AST::SimpleVariable>(invoking_node.position(), name);
+
+    return arguments.last();
+}
+
+RefPtr<AST::Node> Shell::immediate_assign_default(AST::ImmediateExpression& invoking_node, NonnullRefPtrVector<AST::Node> const& arguments)
+{
+    if (arguments.size() != 2) {
+        raise_error(ShellError::EvaluatedSyntaxError, "Expected exactly 2 arguments to assign_default", invoking_node.position());
+        return nullptr;
+    }
+
+    auto name = const_cast<AST::Node&>(arguments.first()).run(*this)->resolve_as_string(*this);
+    if (!local_variable_or(name, ""sv).is_empty())
+        return make_ref_counted<AST::SimpleVariable>(invoking_node.position(), name);
+
+    auto value = const_cast<AST::Node&>(arguments.last()).run(*this)->resolve_without_cast(*this);
+    set_local_variable(name, value);
+
+    return make_ref_counted<AST::SyntheticNode>(invoking_node.position(), value);
+}
+
+RefPtr<AST::Node> Shell::immediate_error_if_empty(AST::ImmediateExpression& invoking_node, NonnullRefPtrVector<AST::Node> const& arguments)
+{
+    if (arguments.size() != 2) {
+        raise_error(ShellError::EvaluatedSyntaxError, "Expected exactly 2 arguments to error_if_empty", invoking_node.position());
+        return nullptr;
+    }
+
+    auto name = const_cast<AST::Node&>(arguments.first()).run(*this)->resolve_as_string(*this);
+    if (!local_variable_or(name, ""sv).is_empty())
+        return make_ref_counted<AST::SimpleVariable>(invoking_node.position(), name);
+
+    auto error_value = const_cast<AST::Node&>(arguments.last()).run(*this)->resolve_as_string(*this);
+    if (error_value.is_empty())
+        error_value = DeprecatedString::formatted("Expected {} to be non-empty", name);
+
+    raise_error(ShellError::EvaluatedSyntaxError, error_value, invoking_node.position());
+    return nullptr;
+}
+
+RefPtr<AST::Node> Shell::immediate_null_or_alternative(AST::ImmediateExpression& invoking_node, NonnullRefPtrVector<AST::Node> const& arguments)
+{
+    if (arguments.size() != 2) {
+        raise_error(ShellError::EvaluatedSyntaxError, "Expected exactly 2 arguments to null_or_alternative", invoking_node.position());
+        return nullptr;
+    }
+
+    auto value = const_cast<AST::Node&>(arguments.first()).run(*this)->resolve_without_cast(*this);
+    if ((value->is_string() && value->resolve_as_string(*this).is_empty()) || (value->is_list() && value->resolve_as_list(*this).is_empty()))
+        return make_ref_counted<AST::SyntheticNode>(invoking_node.position(), value);
+
+    return arguments.last();
+}
+
+RefPtr<AST::Node> Shell::immediate_defined_value_or_default(AST::ImmediateExpression& invoking_node, NonnullRefPtrVector<AST::Node> const& arguments)
+{
+    if (arguments.size() != 2) {
+        raise_error(ShellError::EvaluatedSyntaxError, "Expected exactly 2 arguments to defined_value_or_default", invoking_node.position());
+        return nullptr;
+    }
+
+    auto name = const_cast<AST::Node&>(arguments.first()).run(*this)->resolve_as_string(*this);
+    if (!find_frame_containing_local_variable(name))
+        return arguments.last();
+
+    return make_ref_counted<AST::SimpleVariable>(invoking_node.position(), name);
+}
+
+RefPtr<AST::Node> Shell::immediate_assign_defined_default(AST::ImmediateExpression& invoking_node, NonnullRefPtrVector<AST::Node> const& arguments)
+{
+    if (arguments.size() != 2) {
+        raise_error(ShellError::EvaluatedSyntaxError, "Expected exactly 2 arguments to assign_defined_default", invoking_node.position());
+        return nullptr;
+    }
+
+    auto name = const_cast<AST::Node&>(arguments.first()).run(*this)->resolve_as_string(*this);
+    if (find_frame_containing_local_variable(name))
+        return make_ref_counted<AST::SimpleVariable>(invoking_node.position(), name);
+
+    auto value = const_cast<AST::Node&>(arguments.last()).run(*this)->resolve_without_cast(*this);
+    set_local_variable(name, value);
+
+    return make_ref_counted<AST::SyntheticNode>(invoking_node.position(), value);
+}
+
+RefPtr<AST::Node> Shell::immediate_error_if_unset(AST::ImmediateExpression& invoking_node, NonnullRefPtrVector<AST::Node> const& arguments)
+{
+    if (arguments.size() != 2) {
+        raise_error(ShellError::EvaluatedSyntaxError, "Expected exactly 2 arguments to error_if_unset", invoking_node.position());
+        return nullptr;
+    }
+
+    auto name = const_cast<AST::Node&>(arguments.first()).run(*this)->resolve_as_string(*this);
+    if (find_frame_containing_local_variable(name))
+        return make_ref_counted<AST::SimpleVariable>(invoking_node.position(), name);
+
+    auto error_value = const_cast<AST::Node&>(arguments.last()).run(*this)->resolve_as_string(*this);
+    if (error_value.is_empty())
+        error_value = DeprecatedString::formatted("Expected {} to be set", name);
+
+    raise_error(ShellError::EvaluatedSyntaxError, error_value, invoking_node.position());
+    return nullptr;
+}
+
+RefPtr<AST::Node> Shell::immediate_null_if_unset_or_alternative(AST::ImmediateExpression& invoking_node, NonnullRefPtrVector<AST::Node> const& arguments)
+{
+    if (arguments.size() != 2) {
+        raise_error(ShellError::EvaluatedSyntaxError, "Expected exactly 2 arguments to null_if_unset_or_alternative", invoking_node.position());
+        return nullptr;
+    }
+
+    auto name = const_cast<AST::Node&>(arguments.first()).run(*this)->resolve_as_string(*this);
+    if (!find_frame_containing_local_variable(name))
+        return arguments.last();
+
+    return make_ref_counted<AST::SimpleVariable>(invoking_node.position(), name);
+}
+
+RefPtr<AST::Node> Shell::immediate_reexpand(AST::ImmediateExpression& invoking_node, NonnullRefPtrVector<AST::Node> const& arguments)
+{
+    if (arguments.size() != 1) {
+        raise_error(ShellError::EvaluatedSyntaxError, "Expected exactly 1 argument to reexpand", invoking_node.position());
+        return nullptr;
+    }
+
+    auto value = const_cast<AST::Node&>(arguments.first()).run(*this)->resolve_as_string(*this);
+    return parse(value, m_is_interactive, false);
+}
+
+RefPtr<AST::Node> Shell::immediate_length_of_variable(AST::ImmediateExpression& invoking_node, NonnullRefPtrVector<AST::Node> const& arguments)
+{
+    if (arguments.size() != 1) {
+        raise_error(ShellError::EvaluatedSyntaxError, "Expected exactly 1 argument to length_of_variable", invoking_node.position());
+        return nullptr;
+    }
+
+    auto name = const_cast<AST::Node&>(arguments.first()).run(*this)->resolve_as_string(*this);
+    auto variable = make_ref_counted<AST::SimpleVariable>(invoking_node.position(), name);
+
+    return immediate_length_impl(
+        invoking_node,
+        { move(variable) },
+        false);
+}
+
 RefPtr<AST::Node> Shell::run_immediate_function(StringView str, AST::ImmediateExpression& invoking_node, NonnullRefPtrVector<AST::Node> const& arguments)
 {
 #define __ENUMERATE_SHELL_IMMEDIATE_FUNCTION(name) \

+ 725 - 0
Userland/Shell/PosixLexer.cpp

@@ -0,0 +1,725 @@
+/*
+ * Copyright (c) 2022, Ali Mohammad Pur <mpfard@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <AK/CharacterTypes.h>
+#include <Shell/PosixLexer.h>
+
+static bool is_operator(StringView text)
+{
+    return Shell::Posix::Token::operator_from_name(text).has_value();
+}
+
+static bool is_part_of_operator(StringView text, char ch)
+{
+    StringBuilder builder;
+    builder.append(text);
+    builder.append(ch);
+
+    return Shell::Posix::Token::operator_from_name(builder.string_view()).has_value();
+}
+
+namespace Shell::Posix {
+
+Vector<Token> Lexer::batch_next()
+{
+    for (; m_next_reduction != Reduction::None;) {
+        auto result = reduce(m_next_reduction);
+        m_next_reduction = result.next_reduction;
+        if (!result.tokens.is_empty())
+            return result.tokens;
+    }
+
+    return {};
+}
+
+ExpansionRange Lexer::range(ssize_t offset) const
+{
+    return {
+        m_state.position.end_offset - m_state.position.start_offset + offset - 1,
+        0,
+    };
+}
+
+char Lexer::consume()
+{
+    auto ch = m_lexer.consume();
+    if (ch == '\n') {
+        m_state.position.end_line.line_number++;
+        m_state.position.end_line.line_column = 0;
+    }
+
+    m_state.position.end_offset++;
+    return ch;
+}
+
+bool Lexer::consume_specific(char ch)
+{
+    if (m_lexer.peek() == ch) {
+        consume();
+        return true;
+    }
+    return false;
+}
+
+Lexer::ReductionResult Lexer::reduce(Reduction reduction)
+{
+    switch (reduction) {
+    case Reduction::None:
+        return { {}, Reduction::None };
+    case Reduction::End:
+        return reduce_end();
+    case Reduction::Operator:
+        return reduce_operator();
+    case Reduction::Comment:
+        return reduce_comment();
+    case Reduction::SingleQuotedString:
+        return reduce_single_quoted_string();
+    case Reduction::DoubleQuotedString:
+        return reduce_double_quoted_string();
+    case Reduction::Expansion:
+        return reduce_expansion();
+    case Reduction::CommandExpansion:
+        return reduce_command_expansion();
+    case Reduction::Start:
+        return reduce_start();
+    case Reduction::ArithmeticExpansion:
+        return reduce_arithmetic_expansion();
+    case Reduction::SpecialParameterExpansion:
+        return reduce_special_parameter_expansion();
+    case Reduction::ParameterExpansion:
+        return reduce_parameter_expansion();
+    case Reduction::CommandOrArithmeticSubstitutionExpansion:
+        return reduce_command_or_arithmetic_substitution_expansion();
+    case Reduction::ExtendedParameterExpansion:
+        return reduce_extended_parameter_expansion();
+    }
+
+    VERIFY_NOT_REACHED();
+}
+
+Lexer::ReductionResult Lexer::reduce_end()
+{
+    return {
+        .tokens = { Token::eof() },
+        .next_reduction = Reduction::None,
+    };
+}
+
+Lexer::ReductionResult Lexer::reduce_operator()
+{
+    if (m_lexer.is_eof()) {
+        if (is_operator(m_state.buffer.string_view())) {
+            auto tokens = Token::operators_from(m_state);
+            m_state.buffer.clear();
+            m_state.position.start_offset = m_state.position.end_offset;
+            m_state.position.start_line = m_state.position.end_line;
+
+            return {
+                .tokens = move(tokens),
+                .next_reduction = Reduction::End,
+            };
+        }
+
+        return reduce(Reduction::Start);
+    }
+
+    if (is_part_of_operator(m_state.buffer.string_view(), m_lexer.peek())) {
+        m_state.buffer.append(consume());
+        return {
+            .tokens = {},
+            .next_reduction = Reduction::Operator,
+        };
+    }
+
+    auto tokens = Vector<Token> {};
+    if (is_operator(m_state.buffer.string_view())) {
+        tokens.extend(Token::operators_from(m_state));
+        m_state.buffer.clear();
+        m_state.position.start_offset = m_state.position.end_offset;
+        m_state.position.start_line = m_state.position.end_line;
+    }
+
+    auto result = reduce(Reduction::Start);
+    tokens.extend(move(result.tokens));
+    return {
+        .tokens = move(tokens),
+        .next_reduction = result.next_reduction,
+    };
+}
+
+Lexer::ReductionResult Lexer::reduce_comment()
+{
+    if (m_lexer.is_eof()) {
+        return {
+            .tokens = {},
+            .next_reduction = Reduction::End,
+        };
+    }
+
+    if (consume() == '\n') {
+        return {
+            .tokens = { Token::newline() },
+            .next_reduction = Reduction::Start,
+        };
+    }
+
+    return {
+        .tokens = {},
+        .next_reduction = Reduction::Comment,
+    };
+}
+
+Lexer::ReductionResult Lexer::reduce_single_quoted_string()
+{
+    if (m_lexer.is_eof()) {
+        auto tokens = Token::maybe_from_state(m_state);
+        tokens.append(Token::continuation('\''));
+        return {
+            .tokens = move(tokens),
+            .next_reduction = Reduction::End,
+        };
+    }
+
+    auto ch = consume();
+    m_state.buffer.append(ch);
+
+    if (ch == '\'') {
+        return {
+            .tokens = {},
+            .next_reduction = Reduction::Start,
+        };
+    }
+
+    return {
+        .tokens = {},
+        .next_reduction = Reduction::SingleQuotedString,
+    };
+}
+
+Lexer::ReductionResult Lexer::reduce_double_quoted_string()
+{
+    m_state.previous_reduction = Reduction::DoubleQuotedString;
+    if (m_lexer.is_eof()) {
+        auto tokens = Token::maybe_from_state(m_state);
+        tokens.append(Token::continuation('"'));
+        return {
+            .tokens = move(tokens),
+            .next_reduction = Reduction::End,
+        };
+    }
+
+    auto ch = consume();
+    m_state.buffer.append(ch);
+
+    if (m_state.escaping) {
+        m_state.escaping = false;
+
+        return {
+            .tokens = {},
+            .next_reduction = Reduction::DoubleQuotedString,
+        };
+    }
+
+    switch (ch) {
+    case '\\':
+        m_state.escaping = true;
+        return {
+            .tokens = {},
+            .next_reduction = Reduction::DoubleQuotedString,
+        };
+    case '"':
+        m_state.previous_reduction = Reduction::Start;
+        return {
+            .tokens = {},
+            .next_reduction = Reduction::Start,
+        };
+    case '$':
+        if (m_lexer.next_is("("))
+            m_state.expansions.empend(CommandExpansion { .command = StringBuilder {}, .range = range() });
+        else
+            m_state.expansions.empend(ParameterExpansion { .parameter = StringBuilder {}, .range = range() });
+        return {
+            .tokens = {},
+            .next_reduction = Reduction::Expansion,
+        };
+    case '`':
+        m_state.expansions.empend(CommandExpansion { .command = StringBuilder {}, .range = range() });
+        return {
+            .tokens = {},
+            .next_reduction = Reduction::CommandExpansion,
+        };
+    default:
+        return {
+            .tokens = {},
+            .next_reduction = Reduction::DoubleQuotedString,
+        };
+    }
+}
+
+Lexer::ReductionResult Lexer::reduce_expansion()
+{
+    if (m_lexer.is_eof())
+        return reduce(m_state.previous_reduction);
+
+    auto ch = m_lexer.peek();
+
+    switch (ch) {
+    case '{':
+        consume();
+        m_state.buffer.append(ch);
+        return {
+            .tokens = {},
+            .next_reduction = Reduction::ExtendedParameterExpansion,
+        };
+    case '(':
+        consume();
+        m_state.buffer.append(ch);
+        return {
+            .tokens = {},
+            .next_reduction = Reduction::CommandOrArithmeticSubstitutionExpansion,
+        };
+    case 'a' ... 'z':
+    case 'A' ... 'Z':
+    case '_': {
+        consume();
+        m_state.buffer.append(ch);
+        auto& expansion = m_state.expansions.last().get<ParameterExpansion>();
+        expansion.parameter.append(ch);
+        expansion.range.length = m_state.position.end_offset - expansion.range.start - m_state.position.start_offset;
+
+        return {
+            .tokens = {},
+            .next_reduction = Reduction::ParameterExpansion,
+        };
+    }
+    case '0' ... '9':
+    case '-':
+    case '!':
+    case '@':
+    case '#':
+    case '?':
+    case '*':
+    case '$':
+        return reduce(Reduction::SpecialParameterExpansion);
+    default:
+        m_state.buffer.append(ch);
+        return reduce(m_state.previous_reduction);
+    }
+}
+
+Lexer::ReductionResult Lexer::reduce_command_expansion()
+{
+    if (m_lexer.is_eof()) {
+        auto& expansion = m_state.expansions.last().get<CommandExpansion>();
+        expansion.range.length = m_state.position.end_offset - expansion.range.start - m_state.position.start_offset;
+
+        return {
+            .tokens = { Token::continuation('`') },
+            .next_reduction = m_state.previous_reduction,
+        };
+    }
+
+    auto ch = consume();
+
+    if (!m_state.escaping && ch == '`') {
+        m_state.buffer.append(ch);
+        auto& expansion = m_state.expansions.last().get<CommandExpansion>();
+        expansion.range.length = m_state.position.end_offset - expansion.range.start - m_state.position.start_offset;
+
+        return {
+            .tokens = {},
+            .next_reduction = m_state.previous_reduction,
+        };
+    }
+
+    if (!m_state.escaping && ch == '\\') {
+        m_state.escaping = true;
+        return {
+            .tokens = {},
+            .next_reduction = Reduction::CommandExpansion,
+        };
+    }
+
+    m_state.escaping = false;
+    m_state.buffer.append(ch);
+    m_state.expansions.last().get<CommandExpansion>().command.append(ch);
+    return {
+        .tokens = {},
+        .next_reduction = Reduction::CommandExpansion,
+    };
+}
+
+Lexer::ReductionResult Lexer::reduce_start()
+{
+    if (m_lexer.is_eof()) {
+        auto tokens = Token::maybe_from_state(m_state);
+        m_state.buffer.clear();
+        m_state.position.start_offset = m_state.position.end_offset;
+        m_state.position.start_line = m_state.position.end_line;
+
+        return {
+            .tokens = move(tokens),
+            .next_reduction = Reduction::End,
+        };
+    }
+
+    if (m_state.escaping && consume_specific('\n')) {
+        m_state.escaping = false;
+
+        auto buffer = m_state.buffer.to_deprecated_string().substring(0, m_state.buffer.length() - 1);
+        m_state.buffer.clear();
+        m_state.buffer.append(buffer);
+
+        return {
+            .tokens = {},
+            .next_reduction = Reduction::Start,
+        };
+    }
+
+    if (!m_state.escaping && m_lexer.peek() == '#' && m_state.buffer.is_empty()) {
+        consume();
+        return {
+            .tokens = {},
+            .next_reduction = Reduction::Comment,
+        };
+    }
+
+    if (!m_state.escaping && consume_specific('\n')) {
+        auto tokens = Token::maybe_from_state(m_state);
+        tokens.append(Token::newline());
+
+        m_state.buffer.clear();
+        m_state.position.start_offset = m_state.position.end_offset;
+        m_state.position.start_line = m_state.position.end_line;
+
+        return {
+            .tokens = move(tokens),
+            .next_reduction = Reduction::Start,
+        };
+    }
+
+    if (!m_state.escaping && consume_specific('\\')) {
+        m_state.escaping = true;
+        m_state.buffer.append('\\');
+        return {
+            .tokens = {},
+            .next_reduction = Reduction::Start,
+        };
+    }
+
+    if (!m_state.escaping && is_part_of_operator(""sv, m_lexer.peek())) {
+        auto tokens = Token::maybe_from_state(m_state);
+        m_state.buffer.clear();
+        m_state.buffer.append(consume());
+        m_state.position.start_offset = m_state.position.end_offset;
+        m_state.position.start_line = m_state.position.end_line;
+
+        return {
+            .tokens = move(tokens),
+            .next_reduction = Reduction::Operator,
+        };
+    }
+
+    if (!m_state.escaping && consume_specific('\'')) {
+        m_state.buffer.append('\'');
+        return {
+            .tokens = {},
+            .next_reduction = Reduction::SingleQuotedString,
+        };
+    }
+
+    if (!m_state.escaping && consume_specific('"')) {
+        m_state.buffer.append('"');
+        return {
+            .tokens = {},
+            .next_reduction = Reduction::DoubleQuotedString,
+        };
+    }
+
+    if (!m_state.escaping && is_ascii_space(m_lexer.peek())) {
+        consume();
+        auto tokens = Token::maybe_from_state(m_state);
+        m_state.buffer.clear();
+        m_state.expansions.clear();
+        m_state.position.start_offset = m_state.position.end_offset;
+        m_state.position.start_line = m_state.position.end_line;
+
+        return {
+            .tokens = move(tokens),
+            .next_reduction = Reduction::Start,
+        };
+    }
+
+    if (!m_state.escaping && consume_specific('$')) {
+        m_state.buffer.append('$');
+        if (m_lexer.next_is("("))
+            m_state.expansions.empend(CommandExpansion { .command = StringBuilder {}, .range = range() });
+        else
+            m_state.expansions.empend(ParameterExpansion { .parameter = StringBuilder {}, .range = range() });
+
+        return {
+            .tokens = {},
+            .next_reduction = Reduction::Expansion,
+        };
+    }
+
+    if (!m_state.escaping && consume_specific('`')) {
+        m_state.buffer.append('`');
+        m_state.expansions.empend(CommandExpansion { .command = StringBuilder {}, .range = range() });
+        return {
+            .tokens = {},
+            .next_reduction = Reduction::CommandExpansion,
+        };
+    }
+
+    m_state.escaping = false;
+    m_state.buffer.append(consume());
+    return {
+        .tokens = {},
+        .next_reduction = Reduction::Start,
+    };
+}
+
+Lexer::ReductionResult Lexer::reduce_arithmetic_expansion()
+{
+    if (m_lexer.is_eof()) {
+        auto& expansion = m_state.expansions.last().get<ArithmeticExpansion>();
+        expansion.range.length = m_state.position.end_offset - expansion.range.start - m_state.position.start_offset;
+
+        return {
+            .tokens = { Token::continuation("$((") },
+            .next_reduction = m_state.previous_reduction,
+        };
+    }
+
+    if (m_lexer.peek() == ')' && m_state.buffer.string_view().ends_with(')')) {
+        m_state.buffer.append(consume());
+        auto& expansion = m_state.expansions.last().get<ArithmeticExpansion>();
+        expansion.expression = expansion.value.to_deprecated_string().substring(0, expansion.value.length() - 1);
+        expansion.value.clear();
+        expansion.range.length = m_state.position.end_offset - expansion.range.start - m_state.position.start_offset;
+
+        return {
+            .tokens = {},
+            .next_reduction = m_state.previous_reduction,
+        };
+    }
+
+    auto ch = consume();
+    m_state.buffer.append(ch);
+    m_state.expansions.last().get<ArithmeticExpansion>().value.append(ch);
+    return {
+        .tokens = {},
+        .next_reduction = Reduction::ArithmeticExpansion,
+    };
+}
+
+Lexer::ReductionResult Lexer::reduce_special_parameter_expansion()
+{
+    auto ch = consume();
+    m_state.buffer.append(ch);
+    m_state.expansions.last() = ParameterExpansion {
+        .parameter = StringBuilder {},
+        .range = range(-1),
+    };
+    m_state.expansions.last().get<ParameterExpansion>().parameter.append(ch);
+
+    return {
+        .tokens = {},
+        .next_reduction = m_state.previous_reduction,
+    };
+}
+
+Lexer::ReductionResult Lexer::reduce_parameter_expansion()
+{
+    auto& expansion = m_state.expansions.last().get<ParameterExpansion>();
+
+    if (m_lexer.is_eof()) {
+        return {
+            .tokens = {},
+            .next_reduction = Reduction::Start,
+        };
+    }
+
+    auto next = m_lexer.peek();
+    if (is_ascii_alphanumeric(next)) {
+        m_state.buffer.append(consume());
+        expansion.parameter.append(next);
+        expansion.range.length = m_state.position.end_offset - expansion.range.start - m_state.position.start_offset;
+
+        return {
+            .tokens = {},
+            .next_reduction = Reduction::ParameterExpansion,
+        };
+    }
+
+    return reduce(m_state.previous_reduction);
+}
+
+Lexer::ReductionResult Lexer::reduce_command_or_arithmetic_substitution_expansion()
+{
+    if (m_lexer.is_eof()) {
+        return {
+            .tokens = { Token::continuation("$(") },
+            .next_reduction = m_state.previous_reduction,
+        };
+    }
+
+    auto ch = m_lexer.peek();
+    if (ch == '(' && m_state.buffer.string_view().ends_with("$("sv)) {
+        m_state.buffer.append(consume());
+        m_state.expansions.last() = ArithmeticExpansion {
+            .expression = "",
+            .value = StringBuilder {},
+            .range = range(-2)
+        };
+        return {
+            .tokens = {},
+            .next_reduction = Reduction::ArithmeticExpansion,
+        };
+    }
+
+    if (ch == ')') {
+        m_state.buffer.append(consume());
+        m_state.expansions.last().visit([&](auto& expansion) {
+            expansion.range.length = m_state.position.end_offset - expansion.range.start - m_state.position.start_offset;
+        });
+        return {
+            .tokens = {},
+            .next_reduction = m_state.previous_reduction,
+        };
+    }
+
+    m_state.buffer.append(consume());
+    m_state.expansions.last().get<CommandExpansion>().command.append(ch);
+    return {
+        .tokens = {},
+        .next_reduction = Reduction::CommandOrArithmeticSubstitutionExpansion,
+    };
+}
+
+Lexer::ReductionResult Lexer::reduce_extended_parameter_expansion()
+{
+    auto& expansion = m_state.expansions.last().get<ParameterExpansion>();
+
+    if (m_lexer.is_eof()) {
+        return {
+            .tokens = { Token::continuation("${") },
+            .next_reduction = m_state.previous_reduction,
+        };
+    }
+
+    auto ch = m_lexer.peek();
+    if (ch == '}') {
+        m_state.buffer.append(consume());
+        expansion.range.length = m_state.position.end_offset - expansion.range.start - m_state.position.start_offset;
+
+        return {
+            .tokens = {},
+            .next_reduction = m_state.previous_reduction,
+        };
+    }
+
+    m_state.buffer.append(consume());
+    expansion.parameter.append(ch);
+    expansion.range.length = m_state.position.end_offset - expansion.range.start - m_state.position.start_offset;
+
+    return {
+        .tokens = {},
+        .next_reduction = Reduction::ExtendedParameterExpansion,
+    };
+}
+
+StringView Token::type_name() const
+{
+    switch (type) {
+    case Type::Eof:
+        return "Eof"sv;
+    case Type::Newline:
+        return "Newline"sv;
+    case Type::Continuation:
+        return "Continuation"sv;
+    case Type::Token:
+        return "Token"sv;
+    case Type::And:
+        return "And"sv;
+    case Type::Pipe:
+        return "Pipe"sv;
+    case Type::OpenParen:
+        return "OpenParen"sv;
+    case Type::CloseParen:
+        return "CloseParen"sv;
+    case Type::Great:
+        return "Great"sv;
+    case Type::Less:
+        return "Less"sv;
+    case Type::AndIf:
+        return "AndIf"sv;
+    case Type::OrIf:
+        return "OrIf"sv;
+    case Type::DoubleSemicolon:
+        return "DoubleSemicolon"sv;
+    case Type::DoubleLess:
+        return "DoubleLess"sv;
+    case Type::DoubleGreat:
+        return "DoubleGreat"sv;
+    case Type::LessAnd:
+        return "LessAnd"sv;
+    case Type::GreatAnd:
+        return "GreatAnd"sv;
+    case Type::LessGreat:
+        return "LessGreat"sv;
+    case Type::DoubleLessDash:
+        return "DoubleLessDash"sv;
+    case Type::Clobber:
+        return "Clobber"sv;
+    case Type::Semicolon:
+        return "Semicolon"sv;
+    case Type::AssignmentWord:
+        return "AssignmentWord"sv;
+    case Type::Bang:
+        return "Bang"sv;
+    case Type::Case:
+        return "Case"sv;
+    case Type::CloseBrace:
+        return "CloseBrace"sv;
+    case Type::Do:
+        return "Do"sv;
+    case Type::Done:
+        return "Done"sv;
+    case Type::Elif:
+        return "Elif"sv;
+    case Type::Else:
+        return "Else"sv;
+    case Type::Esac:
+        return "Esac"sv;
+    case Type::Fi:
+        return "Fi"sv;
+    case Type::For:
+        return "For"sv;
+    case Type::If:
+        return "If"sv;
+    case Type::In:
+        return "In"sv;
+    case Type::IoNumber:
+        return "IoNumber"sv;
+    case Type::OpenBrace:
+        return "OpenBrace"sv;
+    case Type::Then:
+        return "Then"sv;
+    case Type::Until:
+        return "Until"sv;
+    case Type::VariableName:
+        return "VariableName"sv;
+    case Type::While:
+        return "While"sv;
+    case Type::Word:
+        return "Word"sv;
+    }
+    return "Idk"sv;
+}
+
+}

+ 413 - 0
Userland/Shell/PosixLexer.h

@@ -0,0 +1,413 @@
+/*
+ * Copyright (c) 2022, Ali Mohammad Pur <mpfard@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <AK/DeprecatedString.h>
+#include <AK/GenericLexer.h>
+#include <AK/Variant.h>
+#include <AK/Vector.h>
+#include <Shell/AST.h>
+
+namespace Shell::Posix {
+
+enum class Reduction {
+    None,
+    End,
+    Operator,
+    Comment,
+    SingleQuotedString,
+    DoubleQuotedString,
+    Expansion,
+    CommandExpansion,
+    Start,
+    ArithmeticExpansion,
+    SpecialParameterExpansion,
+    ParameterExpansion,
+    CommandOrArithmeticSubstitutionExpansion,
+    ExtendedParameterExpansion,
+};
+
+struct ExpansionRange {
+    size_t start;
+    size_t length;
+};
+
+struct ParameterExpansion {
+    StringBuilder parameter;
+    ExpansionRange range;
+};
+
+struct CommandExpansion {
+    StringBuilder command;
+    ExpansionRange range;
+};
+
+struct ArithmeticExpansion {
+    DeprecatedString expression;
+    StringBuilder value;
+    ExpansionRange range;
+};
+
+using Expansion = Variant<ParameterExpansion, CommandExpansion, ArithmeticExpansion>;
+
+struct ResolvedParameterExpansion {
+    DeprecatedString parameter;
+    DeprecatedString argument;
+    ExpansionRange range;
+    enum class Op {
+        UseDefaultValue,                    // ${parameter:-word}
+        AssignDefaultValue,                 // ${parameter:=word}
+        IndicateErrorIfEmpty,               // ${parameter:?word}
+        UseAlternativeValue,                // ${parameter:+word}
+        UseDefaultValueIfUnset,             // ${parameter-default}
+        AssignDefaultValueIfUnset,          // ${parameter=default}
+        IndicateErrorIfUnset,               // ${parameter?default}
+        UseAlternativeValueIfUnset,         // ${parameter+default}
+        RemoveLargestSuffixByPattern,       // ${parameter%%pattern}
+        RemoveLargestPrefixByPattern,       // ${parameter##pattern}
+        RemoveSmallestSuffixByPattern,      // ${parameter%pattern}
+        RemoveSmallestPrefixByPattern,      // ${parameter#pattern}
+        StringLength,                       // ${#parameter}
+        GetPositionalParameter,             // ${parameter}
+        GetVariable,                        // ${parameter}
+        GetLastBackgroundPid,               // $!
+        GetPositionalParameterList,         // $*
+        GetCurrentOptionFlags,              // $-
+        GetPositionalParameterCount,        // $#
+        GetLastExitStatus,                  // $?
+        GetPositionalParameterListAsString, // $@
+        GetShellProcessId,                  // $$
+    } op;
+
+    enum class Expand {
+        Nothing,
+        Word,
+    } expand { Expand::Nothing };
+
+    DeprecatedString to_deprecated_string() const
+    {
+        StringBuilder builder;
+        builder.append("{"sv);
+        switch (op) {
+        case Op::UseDefaultValue:
+            builder.append("UseDefaultValue"sv);
+            break;
+        case Op::AssignDefaultValue:
+            builder.append("AssignDefaultValue"sv);
+            break;
+        case Op::IndicateErrorIfEmpty:
+            builder.append("IndicateErrorIfEmpty"sv);
+            break;
+        case Op::UseAlternativeValue:
+            builder.append("UseAlternativeValue"sv);
+            break;
+        case Op::UseDefaultValueIfUnset:
+            builder.append("UseDefaultValueIfUnset"sv);
+            break;
+        case Op::AssignDefaultValueIfUnset:
+            builder.append("AssignDefaultValueIfUnset"sv);
+            break;
+        case Op::IndicateErrorIfUnset:
+            builder.append("IndicateErrorIfUnset"sv);
+            break;
+        case Op::UseAlternativeValueIfUnset:
+            builder.append("UseAlternativeValueIfUnset"sv);
+            break;
+        case Op::RemoveLargestSuffixByPattern:
+            builder.append("RemoveLargestSuffixByPattern"sv);
+            break;
+        case Op::RemoveLargestPrefixByPattern:
+            builder.append("RemoveLargestPrefixByPattern"sv);
+            break;
+        case Op::RemoveSmallestSuffixByPattern:
+            builder.append("RemoveSmallestSuffixByPattern"sv);
+            break;
+        case Op::RemoveSmallestPrefixByPattern:
+            builder.append("RemoveSmallestPrefixByPattern"sv);
+            break;
+        case Op::StringLength:
+            builder.append("StringLength"sv);
+            break;
+        case Op::GetPositionalParameter:
+            builder.append("GetPositionalParameter"sv);
+            break;
+        case Op::GetLastBackgroundPid:
+            builder.append("GetLastBackgroundPid"sv);
+            break;
+        case Op::GetPositionalParameterList:
+            builder.append("GetPositionalParameterList"sv);
+            break;
+        case Op::GetCurrentOptionFlags:
+            builder.append("GetCurrentOptionFlags"sv);
+            break;
+        case Op::GetPositionalParameterCount:
+            builder.append("GetPositionalParameterCount"sv);
+            break;
+        case Op::GetLastExitStatus:
+            builder.append("GetLastExitStatus"sv);
+            break;
+        case Op::GetPositionalParameterListAsString:
+            builder.append("GetPositionalParameterListAsString"sv);
+            break;
+        case Op::GetShellProcessId:
+            builder.append("GetShellProcessId"sv);
+            break;
+        case Op::GetVariable:
+            builder.append("GetVariable"sv);
+            break;
+        }
+        builder.append(" "sv);
+        builder.append(parameter);
+        builder.append(" ("sv);
+        builder.append(argument);
+        builder.append(")"sv);
+        builder.append("}"sv);
+        return builder.to_deprecated_string();
+    }
+};
+
+struct ResolvedCommandExpansion {
+    RefPtr<AST::Node> command;
+    ExpansionRange range;
+};
+
+using ResolvedExpansion = Variant<ResolvedParameterExpansion, ResolvedCommandExpansion>;
+
+struct State {
+    StringBuilder buffer {};
+    Reduction previous_reduction { Reduction::Start };
+    bool escaping { false };
+    AST::Position position {
+        .start_offset = 0,
+        .end_offset = 0,
+        .start_line = {
+            .line_number = 0,
+            .line_column = 0,
+        },
+        .end_line = {
+            .line_number = 0,
+            .line_column = 0,
+        },
+    };
+    Vector<Expansion> expansions {};
+};
+
+struct Token {
+    enum class Type {
+        Eof,
+        Newline,
+        Continuation,
+        Token,
+        And,
+        Pipe,
+        OpenParen,
+        CloseParen,
+        Great,
+        Less,
+        AndIf,
+        OrIf,
+        DoubleSemicolon,
+        DoubleLess,
+        DoubleGreat,
+        LessAnd,
+        GreatAnd,
+        LessGreat,
+        DoubleLessDash,
+        Clobber,
+        Semicolon,
+
+        // Not produced by this lexer, but generated in later stages.
+        AssignmentWord,
+        Bang,
+        Case,
+        CloseBrace,
+        Do,
+        Done,
+        Elif,
+        Else,
+        Esac,
+        Fi,
+        For,
+        If,
+        In,
+        IoNumber,
+        OpenBrace,
+        Then,
+        Until,
+        VariableName,
+        While,
+        Word,
+    };
+
+    Type type;
+    DeprecatedString value;
+    Optional<AST::Position> position;
+    Vector<Expansion> expansions;
+    Vector<ResolvedExpansion> resolved_expansions {};
+    StringView original_text;
+    bool could_be_start_of_a_simple_command { false };
+
+    static Vector<Token> maybe_from_state(State const& state)
+    {
+        if (state.buffer.is_empty() || state.buffer.string_view().trim_whitespace().is_empty())
+            return {};
+
+        auto token = Token {
+            .type = Type::Token,
+            .value = state.buffer.to_deprecated_string(),
+            .position = state.position,
+            .expansions = state.expansions,
+            .original_text = {},
+        };
+        return { move(token) };
+    }
+
+    static Optional<Token::Type> operator_from_name(StringView name)
+    {
+        if (name == "&&"sv)
+            return Token::Type::AndIf;
+        if (name == "||"sv)
+            return Token::Type::OrIf;
+        if (name == ";;"sv)
+            return Token::Type::DoubleSemicolon;
+        if (name == "<<"sv)
+            return Token::Type::DoubleLess;
+        if (name == ">>"sv)
+            return Token::Type::DoubleGreat;
+        if (name == "<&"sv)
+            return Token::Type::LessAnd;
+        if (name == ">&"sv)
+            return Token::Type::GreatAnd;
+        if (name == "<>"sv)
+            return Token::Type::LessGreat;
+        if (name == "<<-"sv)
+            return Token::Type::DoubleLessDash;
+        if (name == ">|"sv)
+            return Token::Type::Clobber;
+        if (name == ";"sv)
+            return Token::Type::Semicolon;
+        if (name == "&"sv)
+            return Token::Type::And;
+        if (name == "|"sv)
+            return Token::Type::Pipe;
+        if (name == "("sv)
+            return Token::Type::OpenParen;
+        if (name == ")"sv)
+            return Token::Type::CloseParen;
+        if (name == ">"sv)
+            return Token::Type::Great;
+        if (name == "<"sv)
+            return Token::Type::Less;
+
+        return {};
+    }
+
+    static Vector<Token> operators_from(State const& state)
+    {
+        auto name = state.buffer.string_view();
+        auto type = operator_from_name(name);
+        if (!type.has_value())
+            return {};
+
+        return {
+            Token {
+                .type = *type,
+                .value = name,
+                .position = state.position,
+                .expansions = {},
+                .original_text = {},
+            }
+        };
+    }
+
+    static Token eof()
+    {
+        return {
+            .type = Type::Eof,
+            .value = {},
+            .position = {},
+            .expansions = {},
+            .original_text = {},
+        };
+    }
+
+    static Token newline()
+    {
+        return {
+            .type = Type::Newline,
+            .value = "\n",
+            .position = {},
+            .expansions = {},
+            .original_text = {},
+        };
+    }
+
+    static Token continuation(char expected)
+    {
+        return {
+            .type = Type::Continuation,
+            .value = DeprecatedString::formatted("{:c}", expected),
+            .position = {},
+            .expansions = {},
+            .original_text = {},
+        };
+    }
+
+    static Token continuation(DeprecatedString expected)
+    {
+        return {
+            .type = Type::Continuation,
+            .value = move(expected),
+            .position = {},
+            .expansions = {},
+            .original_text = {},
+        };
+    }
+
+    StringView type_name() const;
+};
+
+class Lexer {
+public:
+    explicit Lexer(StringView input)
+        : m_lexer(input)
+    {
+    }
+
+    Vector<Token> batch_next();
+
+private:
+    struct ReductionResult {
+        Vector<Token> tokens;
+        Reduction next_reduction { Reduction::None };
+    };
+
+    ReductionResult reduce(Reduction);
+    ReductionResult reduce_end();
+    ReductionResult reduce_operator();
+    ReductionResult reduce_comment();
+    ReductionResult reduce_single_quoted_string();
+    ReductionResult reduce_double_quoted_string();
+    ReductionResult reduce_expansion();
+    ReductionResult reduce_command_expansion();
+    ReductionResult reduce_start();
+    ReductionResult reduce_arithmetic_expansion();
+    ReductionResult reduce_special_parameter_expansion();
+    ReductionResult reduce_parameter_expansion();
+    ReductionResult reduce_command_or_arithmetic_substitution_expansion();
+    ReductionResult reduce_extended_parameter_expansion();
+
+    char consume();
+    bool consume_specific(char);
+    ExpansionRange range(ssize_t offset = 0) const;
+
+    GenericLexer m_lexer;
+    State m_state;
+    Reduction m_next_reduction { Reduction::Start };
+};
+
+}

+ 1922 - 0
Userland/Shell/PosixParser.cpp

@@ -0,0 +1,1922 @@
+/*
+ * Copyright (c) 2022, Ali Mohammad Pur <mpfard@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <AK/CharacterTypes.h>
+#include <AK/Debug.h>
+#include <AK/StringUtils.h>
+#include <Shell/PosixParser.h>
+
+template<typename T, typename... Ts>
+static inline bool is_one_of(T const& value, Ts const&... values)
+{
+    return ((value == values) || ...);
+}
+
+static inline bool is_io_operator(Shell::Posix::Token const& token)
+{
+    using namespace Shell::Posix;
+    return is_one_of(token.type,
+        Token::Type::Less, Token::Type::Great,
+        Token::Type::LessAnd, Token::Type::GreatAnd,
+        Token::Type::DoubleLess, Token::Type::DoubleGreat,
+        Token::Type::LessGreat, Token::Type::Clobber);
+}
+
+static inline bool is_separator(Shell::Posix::Token const& token)
+{
+    using namespace Shell::Posix;
+    return is_one_of(token.type,
+        Token::Type::Semicolon, Token::Type::Newline,
+        Token::Type::AndIf, Token::Type::OrIf,
+        Token::Type::Pipe,
+        Token::Type::And);
+}
+
+static inline bool is_a_reserved_word_position(Shell::Posix::Token const& token, Optional<Shell::Posix::Token> const& previous_token, Optional<Shell::Posix::Token> const& previous_previous_token)
+{
+    using namespace Shell::Posix;
+
+    auto is_start_of_command = !previous_token.has_value()
+        || previous_token->value.is_empty()
+        || is_separator(*previous_token)
+        || is_one_of(previous_token->type,
+            Token::Type::OpenParen, Token::Type::CloseParen, Token::Type::Newline, Token::Type::DoubleSemicolon,
+            Token::Type::Semicolon, Token::Type::Pipe, Token::Type::OrIf, Token::Type::AndIf);
+    if (is_start_of_command)
+        return true;
+
+    if (!previous_token.has_value())
+        return false;
+
+    auto previous_is_reserved_word = is_one_of(previous_token->value,
+        "for"sv, "in"sv, "case"sv, "if"sv, "then"sv, "else"sv,
+        "elif"sv, "while"sv, "until"sv, "do"sv, "done"sv, "esac"sv,
+        "fi"sv, "!"sv, "{"sv, "}"sv);
+
+    if (previous_is_reserved_word)
+        return true;
+
+    if (!previous_previous_token.has_value())
+        return false;
+
+    auto is_third_in_case = previous_previous_token->value == "case"sv
+        && token.type == Token::Type::Token && token.value == "in"sv;
+
+    if (is_third_in_case)
+        return true;
+
+    auto is_third_in_for = previous_previous_token->value == "for"sv
+        && token.type == Token::Type::Token && is_one_of(token.value, "in"sv, "do"sv);
+
+    return is_third_in_for;
+}
+
+static inline bool is_reserved(Shell::Posix::Token const& token)
+{
+    using namespace Shell::Posix;
+    return is_one_of(token.type,
+        Token::Type::If, Token::Type::Then, Token::Type::Else,
+        Token::Type::Elif, Token::Type::Fi, Token::Type::Do,
+        Token::Type::Done, Token::Type::Case, Token::Type::Esac,
+        Token::Type::While, Token::Type::Until, Token::Type::For,
+        Token::Type::In, Token::Type::OpenBrace, Token::Type::CloseBrace,
+        Token::Type::Bang);
+}
+
+static inline bool is_valid_name(StringView word)
+{
+    // Dr.POSIX: a word consisting solely of underscores, digits, and alphabetics from the portable character set. The first character of a name is not a digit.
+    return !word.is_empty()
+        && !is_ascii_digit(word[0])
+        && all_of(word, [](auto ch) { return is_ascii_alphanumeric(ch) || ch == '_'; });
+}
+
+namespace Shell::Posix {
+void Parser::fill_token_buffer()
+{
+    for (;;) {
+        auto token = next_expanded_token();
+        if (!token.has_value())
+            break;
+#if SHELL_POSIX_PARSER_DEBUG
+        DeprecatedString position = "(~)";
+        if (token->position.has_value())
+            position = DeprecatedString::formatted("{}:{}", token->position->start_offset, token->position->end_offset);
+        DeprecatedString expansions = "";
+        for (auto& exp : token->resolved_expansions)
+            exp.visit(
+                [&](ResolvedParameterExpansion& x) { expansions = DeprecatedString::formatted("{}param({}),", expansions, x.to_deprecated_string()); },
+                [&](ResolvedCommandExpansion& x) { expansions = DeprecatedString::formatted("{}command({:p})", expansions, x.command.ptr()); });
+        DeprecatedString rexpansions = "";
+        for (auto& exp : token->expansions)
+            exp.visit(
+                [&](ParameterExpansion& x) { rexpansions = DeprecatedString::formatted("{}param({}) from {} to {},", rexpansions, x.parameter.string_view(), x.range.start, x.range.length); },
+                [&](auto&) { rexpansions = DeprecatedString::formatted("{}...,", rexpansions); });
+        dbgln("Token @ {}: '{}' (type {}) - parsed expansions: {} - raw expansions: {}", position, token->value.replace("\n"sv, "\\n"sv, ReplaceMode::All), token->type_name(), expansions, rexpansions);
+#endif
+    }
+    m_token_index = 0;
+}
+
+RefPtr<AST::Node> Parser::parse()
+{
+    return parse_complete_command();
+}
+
+Optional<Token> Parser::next_expanded_token()
+{
+    while (m_token_buffer.find_if([](auto& token) { return token.type == Token::Type::Eof; }).is_end()) {
+        auto tokens = m_lexer.batch_next();
+        auto expanded = perform_expansions(move(tokens));
+        m_token_buffer.extend(expanded);
+    }
+
+    if (m_token_buffer.size() == m_token_index)
+        return {};
+
+    return m_token_buffer[m_token_index++];
+}
+
+Vector<Token> Parser::perform_expansions(Vector<Token> tokens)
+{
+    if (tokens.is_empty())
+        return {};
+
+    Vector<Token> expanded_tokens;
+    auto previous_token = Optional<Token>();
+    auto previous_previous_token = Optional<Token>();
+    auto tokens_taken_from_buffer = 0;
+
+    expanded_tokens.ensure_capacity(tokens.size());
+
+    auto swap_expansions = [&] {
+        if (previous_previous_token.has_value())
+            expanded_tokens.append(previous_previous_token.release_value());
+
+        if (previous_token.has_value())
+            expanded_tokens.append(previous_token.release_value());
+
+        for (; tokens_taken_from_buffer > 0; tokens_taken_from_buffer--)
+            m_token_buffer.append(expanded_tokens.take_first());
+
+        swap(tokens, expanded_tokens);
+        expanded_tokens.clear_with_capacity();
+    };
+
+    // (1) join all consecutive newlines (this works around a grammar ambiguity)
+    auto previous_was_newline = !m_token_buffer.is_empty() && m_token_buffer.last().type == Token::Type::Newline;
+    for (auto& token : tokens) {
+        if (token.type == Token::Type::Newline) {
+            if (previous_was_newline)
+                continue;
+            previous_was_newline = true;
+        } else {
+            previous_was_newline = false;
+        }
+        expanded_tokens.append(move(token));
+    }
+
+    swap_expansions();
+
+    // (2) Detect reserved words
+    if (m_token_buffer.size() >= 1) {
+        previous_token = m_token_buffer.take_last();
+        tokens_taken_from_buffer++;
+    }
+    if (m_token_buffer.size() >= 1) {
+        previous_previous_token = m_token_buffer.take_last();
+        tokens_taken_from_buffer++;
+    }
+
+    auto check_reserved_word = [&](auto& token) {
+        if (is_a_reserved_word_position(token, previous_token, previous_previous_token)) {
+            if (token.value == "if"sv)
+                token.type = Token::Type::If;
+            else if (token.value == "then"sv)
+                token.type = Token::Type::Then;
+            else if (token.value == "else"sv)
+                token.type = Token::Type::Else;
+            else if (token.value == "elif"sv)
+                token.type = Token::Type::Elif;
+            else if (token.value == "fi"sv)
+                token.type = Token::Type::Fi;
+            else if (token.value == "while"sv)
+                token.type = Token::Type::While;
+            else if (token.value == "until"sv)
+                token.type = Token::Type::Until;
+            else if (token.value == "do"sv)
+                token.type = Token::Type::Do;
+            else if (token.value == "done"sv)
+                token.type = Token::Type::Done;
+            else if (token.value == "case"sv)
+                token.type = Token::Type::Case;
+            else if (token.value == "esac"sv)
+                token.type = Token::Type::Esac;
+            else if (token.value == "for"sv)
+                token.type = Token::Type::For;
+            else if (token.value == "in"sv)
+                token.type = Token::Type::In;
+            else if (token.value == "!"sv)
+                token.type = Token::Type::Bang;
+            else if (token.value == "{"sv)
+                token.type = Token::Type::OpenBrace;
+            else if (token.value == "}"sv)
+                token.type = Token::Type::CloseBrace;
+            else if (token.type == Token::Type::Token)
+                token.type = Token::Type::Word;
+        } else if (token.type == Token::Type::Token) {
+            token.type = Token::Type::Word;
+        }
+    };
+
+    for (auto& token : tokens) {
+        if (!previous_token.has_value()) {
+            check_reserved_word(token);
+            previous_token = token;
+            continue;
+        }
+        if (!previous_previous_token.has_value()) {
+            check_reserved_word(token);
+            previous_previous_token = move(previous_token);
+            previous_token = token;
+            continue;
+        }
+
+        check_reserved_word(token);
+        expanded_tokens.append(exchange(*previous_previous_token, exchange(*previous_token, move(token))));
+    }
+
+    swap_expansions();
+
+    // (3) Detect io_number tokens
+    previous_token = Optional<Token>();
+    tokens_taken_from_buffer = 0;
+    if (m_token_buffer.size() >= 1) {
+        previous_token = m_token_buffer.take_last();
+        tokens_taken_from_buffer++;
+    }
+
+    for (auto& token : tokens) {
+        if (!previous_token.has_value()) {
+            previous_token = token;
+            continue;
+        }
+
+        if (is_io_operator(token) && previous_token->type == Token::Type::Word && all_of(previous_token->value, is_ascii_digit)) {
+            previous_token->type = Token::Type::IoNumber;
+        }
+
+        expanded_tokens.append(exchange(*previous_token, move(token)));
+    }
+
+    swap_expansions();
+
+    // (4) Try to identify simple commands
+    previous_token = Optional<Token>();
+    tokens_taken_from_buffer = 0;
+
+    if (m_token_buffer.size() >= 1) {
+        previous_token = m_token_buffer.take_last();
+        tokens_taken_from_buffer++;
+    }
+
+    for (auto& token : tokens) {
+        if (!previous_token.has_value()) {
+            token.could_be_start_of_a_simple_command = true;
+            previous_token = token;
+            continue;
+        }
+
+        token.could_be_start_of_a_simple_command = is_one_of(previous_token->type, Token::Type::OpenParen, Token::Type::CloseParen, Token::Type::Newline)
+            || is_separator(*previous_token)
+            || (!is_reserved(*previous_token) && is_reserved(token));
+
+        expanded_tokens.append(exchange(*previous_token, move(token)));
+    }
+
+    swap_expansions();
+
+    // (5) Detect assignment words
+    for (auto& token : tokens) {
+        if (token.could_be_start_of_a_simple_command)
+            m_disallow_command_prefix = false;
+
+        // Check if we're in a command prefix (could be an assignment)
+        if (!m_disallow_command_prefix && token.type == Token::Type::Word && token.value.contains('=')) {
+            // If the word before '=' is a valid name, this is an assignment
+            auto parts = token.value.split_limit('=', 2);
+            if (is_valid_name(parts[0]))
+                token.type = Token::Type::AssignmentWord;
+            else
+                m_disallow_command_prefix = true;
+        } else {
+            m_disallow_command_prefix = true;
+        }
+
+        expanded_tokens.append(move(token));
+    }
+
+    swap_expansions();
+
+    // (6) Parse expansions
+    for (auto& token : tokens) {
+        if (!is_one_of(token.type, Token::Type::Word, Token::Type::AssignmentWord)) {
+            expanded_tokens.append(move(token));
+            continue;
+        }
+
+        Vector<ResolvedExpansion> resolved_expansions;
+        for (auto& expansion : token.expansions) {
+            auto resolved = expansion.visit(
+                [&](ParameterExpansion const& expansion) -> ResolvedExpansion {
+                    auto text = expansion.parameter.string_view();
+                    // ${NUMBER}
+                    if (all_of(text, is_ascii_digit)) {
+                        return ResolvedParameterExpansion {
+                            .parameter = expansion.parameter.to_deprecated_string(),
+                            .argument = {},
+                            .range = expansion.range,
+                            .op = ResolvedParameterExpansion::Op::GetPositionalParameter,
+                        };
+                    }
+
+                    if (text.length() == 1) {
+                        ResolvedParameterExpansion::Op op;
+                        switch (text[0]) {
+                        case '!':
+                            op = ResolvedParameterExpansion::Op::GetLastBackgroundPid;
+                            break;
+                        case '@':
+                            op = ResolvedParameterExpansion::Op::GetPositionalParameterList;
+                            break;
+                        case '-':
+                            op = ResolvedParameterExpansion::Op::GetCurrentOptionFlags;
+                            break;
+                        case '#':
+                            op = ResolvedParameterExpansion::Op::GetPositionalParameterCount;
+                            break;
+                        case '?':
+                            op = ResolvedParameterExpansion::Op::GetLastExitStatus;
+                            break;
+                        case '*':
+                            op = ResolvedParameterExpansion::Op::GetPositionalParameterListAsString;
+                            break;
+                        case '$':
+                            op = ResolvedParameterExpansion::Op::GetShellProcessId;
+                            break;
+                        default:
+                            if (is_valid_name(text)) {
+                                op = ResolvedParameterExpansion::Op::GetVariable;
+                            } else {
+                                error(token, "Unknown parameter expansion: {}", text);
+                                return ResolvedParameterExpansion {
+                                    .parameter = expansion.parameter.to_deprecated_string(),
+                                    .argument = {},
+                                    .range = expansion.range,
+                                    .op = ResolvedParameterExpansion::Op::StringLength,
+                                };
+                            }
+                        }
+
+                        return ResolvedParameterExpansion {
+                            .parameter = {},
+                            .argument = {},
+                            .range = expansion.range,
+                            .op = op,
+                        };
+                    }
+
+                    if (text.starts_with('#')) {
+                        return ResolvedParameterExpansion {
+                            .parameter = text.substring_view(1).to_deprecated_string(),
+                            .argument = {},
+                            .range = expansion.range,
+                            .op = ResolvedParameterExpansion::Op::StringLength,
+                        };
+                    }
+
+                    GenericLexer lexer { text };
+                    auto parameter = lexer.consume_while([first = true](char c) mutable {
+                        if (first) {
+                            first = false;
+                            return is_ascii_alpha(c) || c == '_';
+                        }
+                        return is_ascii_alphanumeric(c) || c == '_';
+                    });
+
+                    StringView argument;
+                    ResolvedParameterExpansion::Op op;
+                    switch (lexer.peek()) {
+                    case ':':
+                        lexer.ignore();
+                        switch (lexer.is_eof() ? 0 : lexer.consume()) {
+                        case '-':
+                            argument = lexer.consume_all();
+                            op = ResolvedParameterExpansion::Op::UseDefaultValue;
+                            break;
+                        case '=':
+                            argument = lexer.consume_all();
+                            op = ResolvedParameterExpansion::Op::AssignDefaultValue;
+                            break;
+                        case '?':
+                            argument = lexer.consume_all();
+                            op = ResolvedParameterExpansion::Op::IndicateErrorIfEmpty;
+                            break;
+                        case '+':
+                            argument = lexer.consume_all();
+                            op = ResolvedParameterExpansion::Op::UseAlternativeValue;
+                            break;
+                        default:
+                            error(token, "Unknown parameter expansion: {}", text);
+                            return ResolvedParameterExpansion {
+                                .parameter = parameter.to_deprecated_string(),
+                                .argument = {},
+                                .range = expansion.range,
+                                .op = ResolvedParameterExpansion::Op::StringLength,
+                            };
+                        }
+                        break;
+                    case '-':
+                        lexer.ignore();
+                        argument = lexer.consume_all();
+                        op = ResolvedParameterExpansion::Op::UseDefaultValueIfUnset;
+                        break;
+                    case '=':
+                        lexer.ignore();
+                        argument = lexer.consume_all();
+                        op = ResolvedParameterExpansion::Op::AssignDefaultValueIfUnset;
+                        break;
+                    case '?':
+                        lexer.ignore();
+                        argument = lexer.consume_all();
+                        op = ResolvedParameterExpansion::Op::IndicateErrorIfUnset;
+                        break;
+                    case '+':
+                        lexer.ignore();
+                        argument = lexer.consume_all();
+                        op = ResolvedParameterExpansion::Op::UseAlternativeValueIfUnset;
+                        break;
+                    case '%':
+                        if (lexer.consume_specific('%'))
+                            op = ResolvedParameterExpansion::Op::RemoveLargestSuffixByPattern;
+                        else
+                            op = ResolvedParameterExpansion::Op::RemoveSmallestSuffixByPattern;
+                        argument = lexer.consume_all();
+                        break;
+                    case '#':
+                        if (lexer.consume_specific('#'))
+                            op = ResolvedParameterExpansion::Op::RemoveLargestPrefixByPattern;
+                        else
+                            op = ResolvedParameterExpansion::Op::RemoveSmallestPrefixByPattern;
+                        argument = lexer.consume_all();
+                        break;
+                    default:
+                        if (is_valid_name(text)) {
+                            op = ResolvedParameterExpansion::Op::GetVariable;
+                        } else {
+                            error(token, "Unknown parameter expansion: {}", text);
+                            return ResolvedParameterExpansion {
+                                .parameter = parameter.to_deprecated_string(),
+                                .argument = {},
+                                .range = expansion.range,
+                                .op = ResolvedParameterExpansion::Op::StringLength,
+                            };
+                        }
+                    }
+                    VERIFY(lexer.is_eof());
+
+                    return ResolvedParameterExpansion {
+                        .parameter = parameter.to_deprecated_string(),
+                        .argument = argument.to_deprecated_string(),
+                        .range = expansion.range,
+                        .op = op,
+                        .expand = ResolvedParameterExpansion::Expand::Word,
+                    };
+                },
+                [&](ArithmeticExpansion const& expansion) -> ResolvedExpansion {
+                    error(token, "Arithmetic expansion is not supported");
+                    return ResolvedParameterExpansion {
+                        .parameter = ""sv,
+                        .argument = ""sv,
+                        .range = expansion.range,
+                        .op = ResolvedParameterExpansion::Op::StringLength,
+                        .expand = ResolvedParameterExpansion::Expand::Nothing,
+                    };
+                },
+                [&](CommandExpansion const& expansion) -> ResolvedExpansion {
+                    Parser parser { expansion.command.string_view() };
+                    auto node = parser.parse();
+                    m_errors.extend(move(parser.m_errors));
+                    return ResolvedCommandExpansion {
+                        move(node),
+                        expansion.range,
+                    };
+                });
+
+            resolved_expansions.append(move(resolved));
+        }
+
+        token.resolved_expansions = move(resolved_expansions);
+        expanded_tokens.append(move(token));
+    }
+
+    swap_expansions();
+
+    // (7) Loop variables
+    previous_token = {};
+    tokens_taken_from_buffer = 0;
+    if (m_token_buffer.size() >= 1) {
+        previous_token = m_token_buffer.take_last();
+        tokens_taken_from_buffer++;
+    }
+
+    for (auto& token : tokens) {
+        if (!previous_token.has_value()) {
+            previous_token = token;
+            continue;
+        }
+
+        if (previous_token->type == Token::Type::For && token.type == Token::Type::Word && is_valid_name(token.value)) {
+            token.type = Token::Type::VariableName;
+        }
+
+        expanded_tokens.append(exchange(*previous_token, token));
+    }
+
+    swap_expansions();
+
+    // (8) Function names
+    previous_token = {};
+    previous_previous_token = {};
+    tokens_taken_from_buffer = 0;
+    if (m_token_buffer.size() >= 1) {
+        previous_token = m_token_buffer.take_last();
+        tokens_taken_from_buffer++;
+    }
+    if (m_token_buffer.size() >= 1) {
+        previous_previous_token = m_token_buffer.take_last();
+        tokens_taken_from_buffer++;
+    }
+
+    for (auto& token : tokens) {
+        if (!previous_token.has_value()) {
+            previous_token = token;
+            continue;
+        }
+        if (!previous_previous_token.has_value()) {
+            previous_previous_token = move(previous_token);
+            previous_token = token;
+            continue;
+        }
+
+        // NAME ( )
+        if (previous_previous_token->could_be_start_of_a_simple_command
+            && previous_previous_token->type == Token::Type::Word
+            && previous_token->type == Token::Type::OpenParen
+            && token.type == Token::Type::CloseParen) {
+
+            previous_previous_token->type = Token::Type::VariableName;
+        }
+
+        expanded_tokens.append(exchange(*previous_previous_token, exchange(*previous_token, token)));
+    }
+
+    swap_expansions();
+
+    return tokens;
+}
+
+static AST::Position empty_position()
+{
+    return { 0, 0, { 0, 0 }, { 0, 0 } };
+}
+
+RefPtr<AST::Node> Parser::parse_complete_command()
+{
+    auto list = [&] {
+        // separator...
+        while (is_separator(peek()))
+            skip();
+
+        // list EOF
+        auto list = parse_list();
+        if (eof())
+            return list;
+
+        // list separator EOF
+        while (is_separator(peek()))
+            skip();
+
+        if (eof())
+            return list;
+
+        auto position = peek().position;
+        auto syntax_error = make_ref_counted<AST::SyntaxError>(
+            position.value_or(empty_position()),
+            "Extra tokens after complete command"sv);
+
+        if (list)
+            list->set_is_syntax_error(*syntax_error);
+        else
+            list = syntax_error;
+
+        return list;
+    }();
+
+    if (!list)
+        return nullptr;
+
+    return make_ref_counted<AST::Execute>(list->position(), *list);
+}
+
+RefPtr<AST::Node> Parser::parse_list()
+{
+    NonnullRefPtrVector<AST::Node> nodes;
+    Vector<AST::Position> positions;
+
+    auto start_position = peek().position.value_or(empty_position());
+
+    for (;;) {
+        auto new_node = parse_and_or();
+        if (!new_node)
+            break;
+
+        if (peek().type == Token::Type::And) {
+            new_node = make_ref_counted<AST::Background>(
+                new_node->position(),
+                *new_node);
+        }
+
+        nodes.append(new_node.release_nonnull());
+
+        if (!is_separator(peek()) || eof())
+            break;
+
+        auto position = consume().position;
+        if (position.has_value())
+            positions.append(position.release_value());
+    }
+
+    auto end_position = peek().position.value_or(empty_position());
+
+    return make_ref_counted<AST::Sequence>(
+        AST::Position {
+            start_position.start_offset,
+            end_position.end_offset,
+            start_position.start_line,
+            start_position.end_line,
+        },
+        move(nodes),
+        move(positions));
+}
+
+RefPtr<AST::Node> Parser::parse_and_or()
+{
+    auto node = parse_pipeline();
+    if (!node)
+        return {};
+
+    for (;;) {
+        if (peek().type == Token::Type::AndIf) {
+            auto and_token = consume();
+            while (peek().type == Token::Type::Newline)
+                skip();
+
+            auto rhs = parse_pipeline();
+            if (!rhs)
+                return {};
+            node = make_ref_counted<AST::And>(
+                node->position(),
+                *node,
+                rhs.release_nonnull(),
+                and_token.position.value_or(empty_position()));
+            continue;
+        }
+        if (peek().type == Token::Type::OrIf) {
+            auto or_token = consume();
+            while (peek().type == Token::Type::Newline)
+                skip();
+
+            auto rhs = parse_pipeline();
+            if (!rhs)
+                return {};
+            node = make_ref_counted<AST::And>(
+                node->position(),
+                *node,
+                rhs.release_nonnull(),
+                or_token.position.value_or(empty_position()));
+            continue;
+        }
+        break;
+    }
+
+    return node;
+}
+
+RefPtr<AST::Node> Parser::parse_pipeline()
+{
+    return parse_pipe_sequence();
+}
+
+RefPtr<AST::Node> Parser::parse_pipe_sequence()
+{
+    auto node = parse_command();
+    if (!node)
+        return {};
+
+    for (;;) {
+        if (peek().type != Token::Type::Pipe)
+            break;
+
+        consume();
+        while (peek().type == Token::Type::Newline)
+            skip();
+
+        auto rhs = parse_command();
+        if (!rhs)
+            return {};
+        node = make_ref_counted<AST::Pipe>(
+            node->position(),
+            *node,
+            rhs.release_nonnull());
+    }
+
+    return node;
+}
+
+RefPtr<AST::Node> Parser::parse_command()
+{
+    auto node = [this] {
+        if (auto node = parse_function_definition())
+            return node;
+
+        if (auto node = parse_simple_command())
+            return node;
+
+        auto node = parse_compound_command();
+        if (!node)
+            return node;
+
+        if (auto list = parse_redirect_list()) {
+            auto position = list->position();
+            node = make_ref_counted<AST::Join>(
+                node->position().with_end(position),
+                *node,
+                list.release_nonnull());
+        }
+
+        return node;
+    }();
+
+    if (!node)
+        return nullptr;
+
+    return make_ref_counted<AST::CastToCommand>(node->position(), *node);
+}
+
+RefPtr<AST::Node> Parser::parse_function_definition()
+{
+    // NAME OPEN_PAREN CLOSE_PAREN newline* function_body
+
+    auto start_index = m_token_index;
+    ArmedScopeGuard reset = [&] {
+        m_token_index = start_index;
+    };
+
+    if (peek().type != Token::Type::VariableName) {
+        return nullptr;
+    }
+
+    auto name = consume();
+
+    if (consume().type != Token::Type::OpenParen)
+        return nullptr;
+
+    if (consume().type != Token::Type::CloseParen)
+        return nullptr;
+
+    while (peek().type == Token::Type::Newline)
+        skip();
+
+    auto body = parse_function_body();
+
+    if (!body)
+        return nullptr;
+
+    reset.disarm();
+
+    return make_ref_counted<AST::FunctionDeclaration>(
+        name.position.value_or(empty_position()).with_end(peek().position.value_or(empty_position())),
+        AST::NameWithPosition { name.value, name.position.value_or(empty_position()) },
+        Vector<AST::NameWithPosition> {},
+        body.release_nonnull());
+}
+
+RefPtr<AST::Node> Parser::parse_function_body()
+{
+    // compound_command redirect_list?
+
+    auto node = parse_compound_command();
+    if (!node)
+        return nullptr;
+
+    if (auto list = parse_redirect_list()) {
+        auto position = list->position();
+        node = make_ref_counted<AST::Join>(
+            node->position().with_end(position),
+            *node,
+            list.release_nonnull());
+    }
+
+    return node;
+}
+
+RefPtr<AST::Node> Parser::parse_redirect_list()
+{
+    // io_redirect*
+
+    RefPtr<AST::Node> node;
+
+    for (;;) {
+        auto new_node = parse_io_redirect();
+        if (!new_node)
+            break;
+
+        if (node) {
+            node = make_ref_counted<AST::Join>(
+                node->position().with_end(new_node->position()),
+                *node,
+                new_node.release_nonnull());
+        } else {
+            node = new_node;
+        }
+    }
+
+    return node;
+}
+
+RefPtr<AST::Node> Parser::parse_compound_command()
+{
+    if (auto node = parse_brace_group())
+        return node;
+
+    if (auto node = parse_subshell())
+        return node;
+
+    if (auto node = parse_if_clause())
+        return node;
+
+    if (auto node = parse_for_clause())
+        return node;
+
+    if (auto node = parse_case_clause())
+        return node;
+
+    if (auto node = parse_while_clause())
+        return node;
+
+    if (auto node = parse_until_clause())
+        return node;
+
+    return {};
+}
+
+RefPtr<AST::Node> Parser::parse_while_clause()
+{
+    if (peek().type != Token::Type::While)
+        return nullptr;
+
+    auto start_position = consume().position.value_or(empty_position());
+    auto condition = parse_compound_list();
+    if (!condition)
+        condition = make_ref_counted<AST::SyntaxError>(
+            peek().position.value_or(empty_position()),
+            "Expected condition after 'while'"sv);
+
+    auto do_group = parse_do_group();
+    if (!do_group)
+        do_group = make_ref_counted<AST::SyntaxError>(
+            peek().position.value_or(empty_position()),
+            "Expected 'do' after 'while'"sv);
+
+    // while foo; bar -> loop { if foo { bar } else { break } }
+    return make_ref_counted<AST::ForLoop>(
+        start_position.with_end(peek().position.value_or(empty_position())),
+        Optional<AST::NameWithPosition> {},
+        Optional<AST::NameWithPosition> {},
+        nullptr,
+        make_ref_counted<AST::IfCond>(
+            start_position.with_end(peek().position.value_or(empty_position())),
+            Optional<AST::Position> {},
+            condition.release_nonnull(),
+            do_group.release_nonnull(),
+            make_ref_counted<AST::ContinuationControl>(
+                start_position,
+                AST::ContinuationControl::ContinuationKind::Break)));
+}
+
+RefPtr<AST::Node> Parser::parse_until_clause()
+{
+    if (peek().type != Token::Type::Until)
+        return nullptr;
+
+    auto start_position = consume().position.value_or(empty_position());
+    auto condition = parse_compound_list();
+    if (!condition)
+        condition = make_ref_counted<AST::SyntaxError>(
+            peek().position.value_or(empty_position()),
+            "Expected condition after 'until'"sv);
+
+    auto do_group = parse_do_group();
+    if (!do_group)
+        do_group = make_ref_counted<AST::SyntaxError>(
+            peek().position.value_or(empty_position()),
+            "Expected 'do' after 'until'"sv);
+
+    // until foo; bar -> loop { if foo { break } else { bar } }
+    return make_ref_counted<AST::ForLoop>(
+        start_position.with_end(peek().position.value_or(empty_position())),
+        Optional<AST::NameWithPosition> {},
+        Optional<AST::NameWithPosition> {},
+        nullptr,
+        make_ref_counted<AST::IfCond>(
+            start_position.with_end(peek().position.value_or(empty_position())),
+            Optional<AST::Position> {},
+            condition.release_nonnull(),
+            make_ref_counted<AST::ContinuationControl>(
+                start_position,
+                AST::ContinuationControl::ContinuationKind::Break),
+            do_group.release_nonnull()));
+}
+
+RefPtr<AST::Node> Parser::parse_brace_group()
+{
+    if (peek().type != Token::Type::OpenBrace)
+        return nullptr;
+
+    consume();
+
+    auto list = parse_compound_list();
+
+    RefPtr<AST::SyntaxError> error;
+    if (peek().type != Token::Type::CloseBrace) {
+        error = make_ref_counted<AST::SyntaxError>(
+            peek().position.value_or(empty_position()),
+            DeprecatedString::formatted("Expected '}}', not {}", peek().type_name()));
+    } else {
+        consume();
+    }
+
+    if (error) {
+        if (list)
+            list->set_is_syntax_error(*error);
+        else
+            list = error;
+    }
+
+    return make_ref_counted<AST::Execute>(list->position(), *list);
+}
+
+RefPtr<AST::Node> Parser::parse_case_clause()
+{
+    auto start_position = peek().position.value_or(empty_position());
+    if (peek().type != Token::Type::Case)
+        return nullptr;
+
+    skip();
+
+    RefPtr<AST::SyntaxError> syntax_error;
+    auto expr = parse_word();
+    if (!expr)
+        expr = make_ref_counted<AST::SyntaxError>(
+            peek().position.value_or(empty_position()),
+            DeprecatedString::formatted("Expected a word, not {}", peek().type_name()));
+
+    if (peek().type != Token::Type::In) {
+        syntax_error = make_ref_counted<AST::SyntaxError>(
+            peek().position.value_or(empty_position()),
+            DeprecatedString::formatted("Expected 'in', not {}", peek().type_name()));
+    } else {
+        skip();
+    }
+
+    while (peek().type == Token::Type::Newline)
+        skip();
+
+    Vector<AST::MatchEntry> entries;
+
+    for (;;) {
+        if (eof() || peek().type == Token::Type::Esac)
+            break;
+
+        if (peek().type == Token::Type::Newline) {
+            skip();
+            continue;
+        }
+
+        // Parse a pattern list
+        auto needs_dsemi = true;
+        if (peek().type == Token::Type::OpenParen) {
+            skip();
+            needs_dsemi = false;
+        }
+
+        auto result = parse_case_list();
+
+        if (peek().type == Token::Type::CloseParen) {
+            skip();
+        } else {
+            if (!syntax_error)
+                syntax_error = make_ref_counted<AST::SyntaxError>(
+                    peek().position.value_or(empty_position()),
+                    DeprecatedString::formatted("Expected ')', not {}", peek().type_name()));
+            break;
+        }
+
+        while (peek().type == Token::Type::Newline)
+            skip();
+
+        auto compound_list = parse_compound_list();
+
+        if (peek().type == Token::Type::DoubleSemicolon) {
+            skip();
+        } else if (needs_dsemi) {
+            if (!syntax_error)
+                syntax_error = make_ref_counted<AST::SyntaxError>(
+                    peek().position.value_or(empty_position()),
+                    DeprecatedString::formatted("Expected ';;', not {}", peek().type_name()));
+        }
+
+        if (syntax_error) {
+            if (compound_list)
+                compound_list->set_is_syntax_error(*syntax_error);
+            else
+                compound_list = syntax_error;
+            syntax_error = nullptr;
+        }
+
+        entries.append(AST::MatchEntry {
+            .options = move(result.nodes),
+            .match_names = {},
+            .match_as_position = {},
+            .pipe_positions = move(result.pipe_positions),
+            .body = move(compound_list),
+        });
+    }
+
+    if (peek().type != Token::Type::Esac) {
+        syntax_error = make_ref_counted<AST::SyntaxError>(
+            peek().position.value_or(empty_position()),
+            DeprecatedString::formatted("Expected 'esac', not {}", peek().type_name()));
+    } else {
+        skip();
+    }
+
+    auto node = make_ref_counted<AST::MatchExpr>(
+        start_position.with_end(peek().position.value_or(empty_position())),
+        expr.release_nonnull(),
+        DeprecatedString {},
+        Optional<AST::Position> {},
+        move(entries));
+
+    if (syntax_error)
+        node->set_is_syntax_error(*syntax_error);
+
+    return node;
+}
+
+Parser::CaseItemsResult Parser::parse_case_list()
+{
+    // Just a list of words split by '|', delimited by ')'
+    NonnullRefPtrVector<AST::Node> nodes;
+    Vector<AST::Position> pipes;
+
+    for (;;) {
+        if (eof() || peek().type == Token::Type::CloseParen)
+            break;
+
+        if (peek().type != Token::Type::Word)
+            break;
+
+        auto node = parse_word();
+        if (!node)
+            node = make_ref_counted<AST::SyntaxError>(
+                peek().position.value_or(empty_position()),
+                DeprecatedString::formatted("Expected a word, not {}", peek().type_name()));
+
+        nodes.append(node.release_nonnull());
+
+        if (peek().type == Token::Type::Pipe) {
+            pipes.append(peek().position.value_or(empty_position()));
+            skip();
+        } else {
+            break;
+        }
+    }
+
+    if (nodes.is_empty())
+        nodes.append(make_ref_counted<AST::SyntaxError>(
+            peek().position.value_or(empty_position()),
+            DeprecatedString::formatted("Expected a word, not {}", peek().type_name())));
+
+    return { move(pipes), move(nodes) };
+}
+
+RefPtr<AST::Node> Parser::parse_if_clause()
+{
+    // If compound_list Then compound_list {Elif compound_list Then compound_list (Fi|Else)?} [(?=Else) compound_list] (?!=Fi) Fi
+    auto start_position = peek().position.value_or(empty_position());
+    if (peek().type != Token::Type::If)
+        return nullptr;
+
+    skip();
+    auto main_condition = parse_compound_list();
+    if (!main_condition)
+        main_condition = make_ref_counted<AST::SyntaxError>(empty_position(), "Expected compound list after 'if'");
+
+    RefPtr<AST::SyntaxError> syntax_error;
+    if (peek().type != Token::Type::Then) {
+        syntax_error = make_ref_counted<AST::SyntaxError>(
+            peek().position.value_or(empty_position()),
+            DeprecatedString::formatted("Expected 'then', not {}", peek().type_name()));
+    } else {
+        skip();
+    }
+
+    auto main_consequence = parse_compound_list();
+    if (!main_consequence)
+        main_consequence = make_ref_counted<AST::SyntaxError>(empty_position(), "Expected compound list after 'then'");
+
+    auto node = make_ref_counted<AST::IfCond>(start_position, Optional<AST::Position>(), main_condition.release_nonnull(), main_consequence.release_nonnull(), nullptr);
+    auto active_node = node;
+
+    while (peek().type == Token::Type::Elif) {
+        skip();
+        auto condition = parse_compound_list();
+        if (!condition)
+            condition = make_ref_counted<AST::SyntaxError>(empty_position(), "Expected compound list after 'elif'");
+
+        if (peek().type != Token::Type::Then) {
+            if (!syntax_error)
+                syntax_error = make_ref_counted<AST::SyntaxError>(
+                    peek().position.value_or(empty_position()),
+                    DeprecatedString::formatted("Expected 'then', not {}", peek().type_name()));
+        } else {
+            skip();
+        }
+
+        auto consequence = parse_compound_list();
+        if (!consequence)
+            consequence = make_ref_counted<AST::SyntaxError>(empty_position(), "Expected compound list after 'then'");
+
+        auto new_node = make_ref_counted<AST::IfCond>(start_position, Optional<AST::Position>(), condition.release_nonnull(), consequence.release_nonnull(), nullptr);
+
+        active_node->false_branch() = new_node;
+        active_node = move(new_node);
+    }
+
+    auto needs_fi = true;
+    switch (peek().type) {
+    case Token::Type::Else:
+        skip();
+        active_node->false_branch() = parse_compound_list();
+        if (!active_node->false_branch())
+            active_node->false_branch() = make_ref_counted<AST::SyntaxError>(empty_position(), "Expected compound list after 'else'");
+        break;
+    case Token::Type::Fi:
+        needs_fi = false;
+        break;
+    default:
+        if (!syntax_error)
+            syntax_error = make_ref_counted<AST::SyntaxError>(
+                peek().position.value_or(empty_position()),
+                DeprecatedString::formatted("Expected 'else' or 'fi', not {}", peek().type_name()));
+        break;
+    }
+
+    if (needs_fi) {
+        if (peek().type != Token::Type::Fi) {
+            if (!syntax_error)
+                syntax_error = make_ref_counted<AST::SyntaxError>(
+                    peek().position.value_or(empty_position()),
+                    DeprecatedString::formatted("Expected 'fi', not {}", peek().type_name()));
+        } else {
+            skip();
+        }
+    }
+
+    if (syntax_error)
+        node->set_is_syntax_error(*syntax_error);
+
+    return node;
+}
+
+RefPtr<AST::Node> Parser::parse_subshell()
+{
+    auto start_position = peek().position.value_or(empty_position());
+    if (peek().type != Token::Type::OpenParen)
+        return nullptr;
+
+    skip();
+    RefPtr<AST::SyntaxError> error;
+
+    auto list = parse_compound_list();
+    if (!list)
+        error = make_ref_counted<AST::SyntaxError>(peek().position.value_or(empty_position()), "Expected compound list after ("sv);
+
+    if (peek().type != Token::Type::CloseParen)
+        error = make_ref_counted<AST::SyntaxError>(peek().position.value_or(empty_position()), "Expected ) after compound list"sv);
+    else
+        skip();
+
+    if (!list)
+        return error;
+
+    return make_ref_counted<AST::Subshell>(
+        start_position.with_end(peek().position.value_or(empty_position())),
+        list.release_nonnull());
+}
+
+RefPtr<AST::Node> Parser::parse_compound_list()
+{
+    while (peek().type == Token::Type::Newline)
+        skip();
+
+    auto term = parse_term();
+    if (!term)
+        return term;
+
+    if (is_separator(peek())) {
+        if (consume().type == Token::Type::And) {
+            term = make_ref_counted<AST::Background>(
+                term->position().with_end(peek().position.value_or(empty_position())),
+                *term);
+        }
+    }
+
+    return term;
+}
+
+RefPtr<AST::Node> Parser::parse_term()
+{
+    NonnullRefPtrVector<AST::Node> nodes;
+    Vector<AST::Position> positions;
+
+    auto start_position = peek().position.value_or(empty_position());
+
+    for (;;) {
+        auto new_node = parse_and_or();
+        if (!new_node)
+            break;
+
+        nodes.append(new_node.release_nonnull());
+
+        if (!is_separator(peek()))
+            break;
+
+        auto position = consume().position;
+        if (position.has_value())
+            positions.append(position.release_value());
+    }
+
+    auto end_position = peek().position.value_or(empty_position());
+
+    return make_ref_counted<AST::Sequence>(
+        start_position.with_end(end_position),
+        move(nodes),
+        move(positions));
+}
+
+RefPtr<AST::Node> Parser::parse_for_clause()
+{
+    // FOR NAME newline+ do_group
+    // FOR NAME newline+ IN separator do_group
+    // FOR NAME IN separator do_group
+    // FOR NAME IN wordlist separator do_group
+
+    if (peek().type != Token::Type::For)
+        return nullptr;
+
+    auto start_position = consume().position.value_or(empty_position());
+
+    DeprecatedString name;
+    Optional<AST::Position> name_position;
+    if (peek().type == Token::Type::VariableName) {
+        name_position = peek().position;
+        name = consume().value;
+    } else {
+        name = "it";
+        error(peek(), "Expected a variable name, not {}", peek().type_name());
+    }
+
+    auto saw_newline = false;
+    while (peek().type == Token::Type::Newline) {
+        saw_newline = true;
+        skip();
+    }
+
+    auto saw_in = false;
+    Optional<AST::Position> in_kw_position;
+    if (peek().type == Token::Type::In) {
+        saw_in = true;
+        in_kw_position = peek().position;
+        skip();
+    } else if (!saw_newline) {
+        error(peek(), "Expected 'in' or a newline, not {}", peek().type_name());
+    }
+
+    RefPtr<AST::Node> iterated_expression;
+    if (!saw_newline)
+        iterated_expression = parse_word_list();
+
+    if (saw_in) {
+        if (peek().type == Token::Type::Semicolon)
+            skip();
+        else
+            error(peek(), "Expected a semicolon, not {}", peek().type_name());
+    }
+
+    auto body = parse_do_group();
+    return AST::make_ref_counted<AST::ForLoop>(
+        start_position.with_end(peek().position.value_or(empty_position())),
+        AST::NameWithPosition { move(name), name_position.value_or(empty_position()) },
+        Optional<AST::NameWithPosition> {},
+        move(iterated_expression),
+        move(body),
+        move(in_kw_position),
+        Optional<AST::Position> {});
+}
+
+RefPtr<AST::Node> Parser::parse_word_list()
+{
+    NonnullRefPtrVector<AST::Node> nodes;
+
+    auto start_position = peek().position.value_or(empty_position());
+
+    for (; peek().type == Token::Type::Word;) {
+        auto word = parse_word();
+        nodes.append(word.release_nonnull());
+    }
+
+    return make_ref_counted<AST::ListConcatenate>(
+        start_position.with_end(peek().position.value_or(empty_position())),
+        move(nodes));
+}
+
+RefPtr<AST::Node> Parser::parse_word()
+{
+    if (peek().type != Token::Type::Word)
+        return nullptr;
+
+    auto token = consume();
+    RefPtr<AST::Node> word;
+
+    enum class Quote {
+        None,
+        Single,
+        Double,
+    } in_quote { Quote::None };
+
+    auto append_bareword = [&](StringView string) {
+        if (!word && string.starts_with('~')) {
+            GenericLexer lexer { string };
+            lexer.ignore();
+            auto user = lexer.consume_while(is_ascii_alphanumeric);
+            string = lexer.remaining();
+
+            word = make_ref_counted<AST::Tilde>(token.position.value_or(empty_position()), user);
+        }
+
+        if (string.is_empty())
+            return;
+
+        auto node = make_ref_counted<AST::BarewordLiteral>(token.position.value_or(empty_position()), string);
+
+        if (word) {
+            word = make_ref_counted<AST::Juxtaposition>(
+                word->position().with_end(token.position.value_or(empty_position())),
+                *word,
+                move(node),
+                AST::Juxtaposition::Mode::StringExpand);
+        } else {
+            word = move(node);
+        }
+    };
+
+    auto append_string_literal = [&](StringView string) {
+        if (string.is_empty())
+            return;
+
+        auto node = make_ref_counted<AST::StringLiteral>(token.position.value_or(empty_position()), string, AST::StringLiteral::EnclosureType::SingleQuotes);
+
+        if (word) {
+            word = make_ref_counted<AST::Juxtaposition>(
+                word->position().with_end(token.position.value_or(empty_position())),
+                *word,
+                move(node),
+                AST::Juxtaposition::Mode::StringExpand);
+        } else {
+            word = move(node);
+        }
+    };
+
+    auto append_string_part = [&](StringView string) {
+        if (string.is_empty())
+            return;
+
+        auto node = make_ref_counted<AST::StringLiteral>(token.position.value_or(empty_position()), string, AST::StringLiteral::EnclosureType::DoubleQuotes);
+
+        if (word) {
+            word = make_ref_counted<AST::Juxtaposition>(
+                word->position().with_end(token.position.value_or(empty_position())),
+                *word,
+                move(node),
+                AST::Juxtaposition::Mode::StringExpand);
+        } else {
+            word = move(node);
+        }
+    };
+
+    auto append_parameter_expansion = [&](ResolvedParameterExpansion const& x) {
+        DeprecatedString immediate_function_name;
+        RefPtr<AST::Node> node;
+        switch (x.op) {
+        case ResolvedParameterExpansion::Op::UseDefaultValue:
+            immediate_function_name = "value_or_default";
+            break;
+        case ResolvedParameterExpansion::Op::AssignDefaultValue:
+            immediate_function_name = "assign_default";
+            break;
+        case ResolvedParameterExpansion::Op::IndicateErrorIfEmpty:
+            immediate_function_name = "error_if_empty";
+            break;
+        case ResolvedParameterExpansion::Op::UseAlternativeValue:
+            immediate_function_name = "null_or_alternative";
+            break;
+        case ResolvedParameterExpansion::Op::UseDefaultValueIfUnset:
+            immediate_function_name = "defined_value_or_default";
+            break;
+        case ResolvedParameterExpansion::Op::AssignDefaultValueIfUnset:
+            immediate_function_name = "assign_defined_default";
+            break;
+        case ResolvedParameterExpansion::Op::IndicateErrorIfUnset:
+            immediate_function_name = "error_if_unset";
+            break;
+        case ResolvedParameterExpansion::Op::UseAlternativeValueIfUnset:
+            immediate_function_name = "null_if_unset_or_alternative";
+            break;
+        case ResolvedParameterExpansion::Op::RemoveLargestSuffixByPattern:
+            // FIXME: Implement this
+        case ResolvedParameterExpansion::Op::RemoveSmallestSuffixByPattern:
+            immediate_function_name = "remove_suffix";
+            break;
+        case ResolvedParameterExpansion::Op::RemoveLargestPrefixByPattern:
+            // FIXME: Implement this
+        case ResolvedParameterExpansion::Op::RemoveSmallestPrefixByPattern:
+            immediate_function_name = "remove_prefix";
+            break;
+        case ResolvedParameterExpansion::Op::StringLength:
+            immediate_function_name = "length_of_variable";
+            break;
+        case ResolvedParameterExpansion::Op::GetPositionalParameter:
+        case ResolvedParameterExpansion::Op::GetVariable:
+            node = make_ref_counted<AST::SimpleVariable>(
+                token.position.value_or(empty_position()),
+                x.parameter);
+            break;
+        case ResolvedParameterExpansion::Op::GetLastBackgroundPid:
+            node = make_ref_counted<AST::SyntaxError>(
+                token.position.value_or(empty_position()),
+                "$! not implemented");
+            break;
+        case ResolvedParameterExpansion::Op::GetPositionalParameterList:
+            node = make_ref_counted<AST::SpecialVariable>(
+                token.position.value_or(empty_position()),
+                '*');
+            break;
+        case ResolvedParameterExpansion::Op::GetCurrentOptionFlags:
+            node = make_ref_counted<AST::SyntaxError>(
+                token.position.value_or(empty_position()),
+                "The current option flags are not available in parameter expansions");
+            break;
+        case ResolvedParameterExpansion::Op::GetPositionalParameterCount:
+            node = make_ref_counted<AST::SpecialVariable>(
+                token.position.value_or(empty_position()),
+                '#');
+            break;
+        case ResolvedParameterExpansion::Op::GetLastExitStatus:
+            node = make_ref_counted<AST::SpecialVariable>(
+                token.position.value_or(empty_position()),
+                '?');
+            break;
+        case ResolvedParameterExpansion::Op::GetPositionalParameterListAsString:
+            node = make_ref_counted<AST::SyntaxError>(
+                token.position.value_or(empty_position()),
+                "$* not implemented");
+            break;
+        case ResolvedParameterExpansion::Op::GetShellProcessId:
+            node = make_ref_counted<AST::SpecialVariable>(
+                token.position.value_or(empty_position()),
+                '$');
+            break;
+        }
+
+        if (!node) {
+            NonnullRefPtrVector<AST::Node> arguments;
+            arguments.append(make_ref_counted<AST::BarewordLiteral>(
+                token.position.value_or(empty_position()),
+                x.parameter));
+
+            if (!x.argument.is_empty()) {
+                // dbgln("Will parse {}", x.argument);
+                arguments.append(*Parser { x.argument }.parse_word());
+            }
+
+            node = make_ref_counted<AST::ImmediateExpression>(
+                token.position.value_or(empty_position()),
+                AST::NameWithPosition {
+                    immediate_function_name,
+                    token.position.value_or(empty_position()),
+                },
+                move(arguments),
+                Optional<AST::Position> {});
+        }
+
+        if (x.expand == ResolvedParameterExpansion::Expand::Word) {
+            node = make_ref_counted<AST::ImmediateExpression>(
+                token.position.value_or(empty_position()),
+                AST::NameWithPosition {
+                    "reexpand",
+                    token.position.value_or(empty_position()),
+                },
+                Vector { node.release_nonnull() },
+                Optional<AST::Position> {});
+        }
+
+        if (word) {
+            word = make_ref_counted<AST::Juxtaposition>(
+                word->position().with_end(token.position.value_or(empty_position())),
+                *word,
+                node.release_nonnull(),
+                AST::Juxtaposition::Mode::StringExpand);
+        } else {
+            word = move(node);
+        }
+    };
+
+    auto append_command_expansion = [&](ResolvedCommandExpansion const& x) {
+        if (!x.command)
+            return;
+
+        RefPtr<AST::Execute> execute_node;
+
+        if (x.command->is_execute()) {
+            execute_node = const_cast<AST::Execute&>(static_cast<AST::Execute const&>(*x.command));
+            execute_node->capture_stdout();
+        } else {
+            execute_node = make_ref_counted<AST::Execute>(
+                word ? word->position() : empty_position(),
+                *x.command,
+                true);
+        }
+
+        if (word) {
+            word = make_ref_counted<AST::Juxtaposition>(
+                word->position(),
+                *word,
+                execute_node.release_nonnull(),
+                AST::Juxtaposition::Mode::StringExpand);
+        } else {
+            word = move(execute_node);
+        }
+    };
+
+    auto append_string = [&](StringView string) {
+        if (string.is_empty())
+            return;
+
+        Optional<size_t> run_start;
+        auto escape = false;
+        for (size_t i = 0; i < string.length(); ++i) {
+            auto ch = string[i];
+            switch (ch) {
+            case '\\':
+                if (!escape && i + 1 < string.length()) {
+                    if (is_one_of(string[i + 1], '"', '\'', '$', '`', '\\')) {
+                        escape = in_quote != Quote::Single;
+                        continue;
+                    }
+                }
+                break;
+            case '\'':
+                if (in_quote == Quote::Single) {
+                    in_quote = Quote::None;
+                    append_string_literal(string.substring_view(*run_start, i - *run_start));
+                    run_start = i + 1;
+                    continue;
+                }
+                if (in_quote == Quote::Double) {
+                    escape = false;
+                    continue;
+                }
+                [[fallthrough]];
+            case '"':
+                if (ch == '\'' && in_quote == Quote::Single) {
+                    escape = false;
+                    continue;
+                }
+                if (!escape) {
+                    if (ch == '"' && in_quote == Quote::Double) {
+                        in_quote = Quote::None;
+                        if (run_start.has_value())
+                            append_string_part(string.substring_view(*run_start, i - *run_start));
+                        run_start = i + 1;
+                        continue;
+                    }
+                    if (run_start.has_value())
+                        append_bareword(string.substring_view(*run_start, i - *run_start));
+                    in_quote = ch == '\'' ? Quote::Single : Quote::Double;
+                    run_start = i + 1;
+                }
+                escape = false;
+                [[fallthrough]];
+            default:
+                if (!run_start.has_value())
+                    run_start = i;
+                escape = false;
+                continue;
+            }
+        }
+
+        if (run_start.has_value())
+            append_bareword(string.substring_view(*run_start, string.length() - *run_start));
+    };
+
+    size_t current_offset = 0;
+    for (auto& expansion : token.resolved_expansions) {
+        expansion.visit(
+            [&](ResolvedParameterExpansion const& x) {
+                if (x.range.start >= token.value.length()) {
+                    dbgln("Parameter expansion range {}-{} is out of bounds for '{}'", x.range.start, x.range.length, token.value);
+                    return;
+                }
+
+                if (x.range.start != current_offset) {
+                    append_string(token.value.substring_view(current_offset, x.range.start - current_offset));
+                    current_offset = x.range.start;
+                }
+                current_offset += x.range.length;
+                append_parameter_expansion(x);
+            },
+            [&](ResolvedCommandExpansion const& x) {
+                if (x.range.start >= token.value.length()) {
+                    dbgln("Parameter expansion range {}-{} is out of bounds for '{}'", x.range.start, x.range.length, token.value);
+                    return;
+                }
+
+                if (x.range.start != current_offset) {
+                    append_string(token.value.substring_view(current_offset, x.range.start - current_offset));
+                    current_offset = x.range.start;
+                }
+                current_offset += x.range.length;
+                append_command_expansion(x);
+            });
+    }
+
+    if (current_offset >= token.value.length()) {
+        dbgln("Parameter expansion range {}- is out of bounds for '{}'", current_offset, token.value);
+        return word;
+    }
+
+    if (current_offset != token.value.length())
+        append_string(token.value.substring_view(current_offset));
+
+    return word;
+}
+
+RefPtr<AST::Node> Parser::parse_do_group()
+{
+    if (peek().type != Token::Type::Do) {
+        return make_ref_counted<AST::SyntaxError>(
+            peek().position.value_or(empty_position()),
+            DeprecatedString::formatted("Expected 'do', not {}", peek().type_name()));
+    }
+
+    consume();
+
+    auto list = parse_compound_list();
+
+    RefPtr<AST::SyntaxError> error;
+    if (peek().type != Token::Type::Done) {
+        error = make_ref_counted<AST::SyntaxError>(
+            peek().position.value_or(empty_position()),
+            DeprecatedString::formatted("Expected 'done', not {}", peek().type_name()));
+    } else {
+        consume();
+    }
+
+    if (error) {
+        if (list)
+            list->set_is_syntax_error(*error);
+        else
+            list = error;
+    }
+
+    return make_ref_counted<AST::Execute>(list->position(), *list);
+}
+
+RefPtr<AST::Node> Parser::parse_simple_command()
+{
+    auto start_position = peek().position.value_or(empty_position());
+
+    Vector<DeprecatedString> definitions;
+    NonnullRefPtrVector<AST::Node> nodes;
+
+    for (;;) {
+        if (auto io_redirect = parse_io_redirect())
+            nodes.append(*io_redirect);
+        else
+            break;
+    }
+
+    while (peek().type == Token::Type::AssignmentWord) {
+        definitions.append(peek().value);
+
+        if (!nodes.is_empty()) {
+            nodes.append(
+                make_ref_counted<AST::BarewordLiteral>(
+                    peek().position.value_or(empty_position()),
+                    consume().value));
+        } else {
+            // env (assignments) (command)
+            nodes.append(make_ref_counted<AST::BarewordLiteral>(
+                empty_position(),
+                "env"));
+
+            nodes.append(
+                make_ref_counted<AST::BarewordLiteral>(
+                    peek().position.value_or(empty_position()),
+                    consume().value));
+        }
+    }
+
+    // WORD or io_redirect: IO_NUMBER or io_file
+    if (!is_one_of(peek().type,
+            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()) {
+            Vector<AST::VariableDeclarations::Variable> variables;
+            for (auto& definition : definitions) {
+                auto parts = definition.split_limit('=', 2, SplitBehavior::KeepEmpty);
+                auto name = make_ref_counted<AST::BarewordLiteral>(
+                    empty_position(),
+                    parts[0]);
+                auto value = make_ref_counted<AST::BarewordLiteral>(
+                    empty_position(),
+                    parts.size() > 1 ? parts[1] : "");
+
+                variables.append({ move(name), move(value) });
+            }
+
+            return make_ref_counted<AST::VariableDeclarations>(empty_position(), move(variables));
+        }
+        return nullptr;
+    }
+
+    // auto first = true;
+    for (;;) {
+        if (peek().type == Token::Type::Word) {
+            auto new_word = parse_word();
+            if (!new_word)
+                break;
+
+            // if (first) {
+            //     first = false;
+            //     new_word = make_ref_counted<AST::ImmediateExpression>(
+            //         new_word->position(),
+            //         AST::NameWithPosition {
+            //             "substitute_aliases"sv,
+            //             empty_position(),
+            //         },
+            //         NonnullRefPtrVector<AST::Node> { *new_word },
+            //         Optional<AST::Position> {});
+            // }
+
+            nodes.append(new_word.release_nonnull());
+        } else if (auto io_redirect = parse_io_redirect()) {
+            nodes.append(io_redirect.release_nonnull());
+        } else {
+            break;
+        }
+    }
+
+    auto node = make_ref_counted<AST::ListConcatenate>(
+        start_position.with_end(peek().position.value_or(empty_position())),
+        move(nodes));
+
+    return node;
+}
+
+RefPtr<AST::Node> Parser::parse_io_redirect()
+{
+    auto start_position = peek().position.value_or(empty_position());
+    auto start_index = m_token_index;
+
+    // io_redirect: IO_NUMBER? io_file | IO_NUMBER? io_here
+    Optional<int> io_number;
+
+    if (peek().type == Token::Type::IoNumber)
+        io_number = consume().value.to_int(TrimWhitespace::No);
+
+    if (auto io_file = parse_io_file(start_position, io_number))
+        return io_file;
+
+    // if (auto io_here = parse_io_here(start_position, io_number))
+    //     return io_here;
+
+    m_token_index = start_index;
+    return nullptr;
+}
+
+RefPtr<AST::Node> Parser::parse_io_file(AST::Position start_position, Optional<int> fd)
+{
+    auto start_index = m_token_index;
+
+    // io_file = (LESS | LESSAND | GREAT | GREATAND | DGREAT | LESSGREAT | CLOBBER) WORD
+    auto io_operator = peek().type;
+    if (!is_one_of(io_operator,
+            Token::Type::Less, Token::Type::LessAnd, Token::Type::Great, Token::Type::GreatAnd,
+            Token::Type::DoubleGreat, Token::Type::LessGreat, Token::Type::Clobber))
+        return nullptr;
+
+    auto io_operator_token = consume();
+
+    auto word = parse_word();
+    if (!word) {
+        m_token_index = start_index;
+        return nullptr;
+    }
+
+    auto position = start_position.with_end(peek().position.value_or(empty_position()));
+    switch (io_operator) {
+    case Token::Type::Less:
+        return make_ref_counted<AST::ReadRedirection>(
+            position,
+            fd.value_or(0),
+            word.release_nonnull());
+    case Token::Type::Clobber:
+        // FIXME: Add support for clobber (and 'noclobber')
+    case Token::Type::Great:
+        return make_ref_counted<AST::WriteRedirection>(
+            position,
+            fd.value_or(1),
+            word.release_nonnull());
+    case Token::Type::DoubleGreat:
+        return make_ref_counted<AST::WriteAppendRedirection>(
+            position,
+            fd.value_or(1),
+            word.release_nonnull());
+    case Token::Type::LessGreat:
+        return make_ref_counted<AST::ReadWriteRedirection>(
+            position,
+            fd.value_or(0),
+            word.release_nonnull());
+    case Token::Type::LessAnd:
+    case Token::Type::GreatAnd: {
+        auto is_less = io_operator == Token::Type::LessAnd;
+        auto source_fd = fd.value_or(is_less ? 0 : 1);
+        if (word->is_bareword()) {
+            auto maybe_target_fd = static_ptr_cast<AST::BarewordLiteral>(word)->text().to_int(AK::TrimWhitespace::No);
+            if (maybe_target_fd.has_value()) {
+                auto target_fd = maybe_target_fd.release_value();
+                if (is_less)
+                    swap(source_fd, target_fd);
+
+                return make_ref_counted<AST::Fd2FdRedirection>(
+                    position,
+                    source_fd,
+                    target_fd);
+            }
+        }
+        if (is_less) {
+            return make_ref_counted<AST::ReadRedirection>(
+                position,
+                source_fd,
+                word.release_nonnull());
+        }
+
+        return make_ref_counted<AST::WriteRedirection>(
+            position,
+            source_fd,
+            word.release_nonnull());
+    }
+    default:
+        VERIFY_NOT_REACHED();
+    }
+}
+
+}

+ 117 - 0
Userland/Shell/PosixParser.h

@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2022, Ali Mohammad Pur <mpfard@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <Shell/AST.h>
+#include <Shell/PosixLexer.h>
+
+namespace Shell::Posix {
+
+class Parser {
+public:
+    Parser(StringView input, bool interactive = false)
+        : m_lexer(input)
+        , m_in_interactive_mode(interactive)
+        , m_eof_token(Token::eof())
+    {
+        fill_token_buffer();
+    }
+
+    RefPtr<AST::Node> parse();
+    RefPtr<AST::Node> parse_word_list();
+
+    struct Error {
+        DeprecatedString message;
+        Optional<AST::Position> position;
+    };
+    auto& errors() const { return m_errors; }
+
+private:
+    Optional<Token> next_expanded_token();
+    Vector<Token> perform_expansions(Vector<Token> tokens);
+    void fill_token_buffer();
+
+    Token const& peek() const
+    {
+        if (eof())
+            return m_eof_token;
+        return m_token_buffer[m_token_index];
+    }
+    Token const& consume()
+    {
+        if (eof())
+            return m_eof_token;
+        return m_token_buffer[m_token_index++];
+    }
+    void skip()
+    {
+        if (eof())
+            return;
+        m_token_index++;
+    }
+    bool eof() const
+    {
+        return m_token_index == m_token_buffer.size() || m_token_buffer[m_token_index].type == Token::Type::Eof;
+    }
+
+    struct CaseItemsResult {
+        Vector<AST::Position> pipe_positions;
+        NonnullRefPtrVector<AST::Node> nodes;
+    };
+
+    RefPtr<AST::Node> parse_complete_command();
+    RefPtr<AST::Node> parse_list();
+    RefPtr<AST::Node> parse_and_or();
+    RefPtr<AST::Node> parse_pipeline();
+    RefPtr<AST::Node> parse_pipe_sequence();
+    RefPtr<AST::Node> parse_command();
+    RefPtr<AST::Node> parse_compound_command();
+    RefPtr<AST::Node> parse_subshell();
+    RefPtr<AST::Node> parse_compound_list();
+    RefPtr<AST::Node> parse_term();
+    RefPtr<AST::Node> parse_for_clause();
+    RefPtr<AST::Node> parse_case_clause();
+    CaseItemsResult parse_case_list();
+    RefPtr<AST::Node> parse_if_clause();
+    RefPtr<AST::Node> parse_while_clause();
+    RefPtr<AST::Node> parse_until_clause();
+    RefPtr<AST::Node> parse_function_definition();
+    RefPtr<AST::Node> parse_function_body();
+    RefPtr<AST::Node> parse_brace_group();
+    RefPtr<AST::Node> parse_do_group();
+    RefPtr<AST::Node> parse_simple_command();
+    RefPtr<AST::Node> parse_prefix();
+    RefPtr<AST::Node> parse_suffix();
+    RefPtr<AST::Node> parse_io_redirect();
+    RefPtr<AST::Node> parse_redirect_list();
+    RefPtr<AST::Node> parse_io_file(AST::Position, Optional<int> fd);
+    RefPtr<AST::Node> parse_io_here(AST::Position, Optional<int> fd);
+    RefPtr<AST::Node> parse_word();
+
+    template<typename... Ts>
+    void error(Token const& token, CheckedFormatString<Ts...> fmt, Ts&&... args)
+    {
+        m_errors.append(Error {
+            DeprecatedString::formatted(fmt.view(), forward<Ts>(args)...),
+            token.position,
+        });
+    }
+
+    Lexer m_lexer;
+    bool m_in_interactive_mode { false };
+    Vector<Token, 2> m_token_buffer;
+    size_t m_token_index { 0 };
+    Vector<Token> m_previous_token_buffer;
+
+    Vector<Error> m_errors;
+
+    Token m_eof_token;
+
+    bool m_disallow_command_prefix { true };
+};
+
+}

+ 37 - 12
Userland/Shell/Shell.cpp

@@ -26,6 +26,7 @@
 #include <LibCore/System.h>
 #include <LibCore/Timer.h>
 #include <LibLine/Editor.h>
+#include <Shell/PosixParser.h>
 #include <errno.h>
 #include <fcntl.h>
 #include <inttypes.h>
@@ -297,7 +298,7 @@ Vector<AST::Command> Shell::expand_aliases(Vector<AST::Command> initial_commands
             auto alias = resolve_alias(command.argv[0]);
             if (!alias.is_null()) {
                 auto argv0 = command.argv.take_first();
-                auto subcommand_ast = Parser { alias }.parse();
+                auto subcommand_ast = parse(alias, false);
                 if (subcommand_ast) {
                     while (subcommand_ast->is_execute()) {
                         auto* ast = static_cast<AST::Execute*>(subcommand_ast.ptr());
@@ -477,7 +478,7 @@ bool Shell::invoke_function(const AST::Command& command, int& retval)
 
 DeprecatedString Shell::format(StringView source, ssize_t& cursor) const
 {
-    Formatter formatter(source, cursor);
+    Formatter formatter(source, cursor, m_in_posix_mode);
     auto result = formatter.format();
     cursor = formatter.cursor();
 
@@ -580,7 +581,7 @@ int Shell::run_command(StringView cmd, Optional<SourcePosition> source_position_
     if (cmd.is_empty())
         return 0;
 
-    auto command = Parser(cmd, m_is_interactive).parse();
+    auto command = parse(cmd, m_is_interactive);
 
     if (!command)
         return 0;
@@ -1410,8 +1411,7 @@ void Shell::remove_entry_from_cache(StringView entry)
 void Shell::highlight(Line::Editor& editor) const
 {
     auto line = editor.line();
-    Parser parser(line, m_is_interactive);
-    auto ast = parser.parse();
+    auto ast = parse(line, m_is_interactive);
     if (!ast)
         return;
     ast->highlight_in_editor(editor, const_cast<Shell&>(*this));
@@ -1425,9 +1425,7 @@ Vector<Line::CompletionSuggestion> Shell::complete()
 
 Vector<Line::CompletionSuggestion> Shell::complete(StringView line)
 {
-    Parser parser(line, m_is_interactive);
-
-    auto ast = parser.parse();
+    auto ast = parse(line, m_is_interactive);
 
     if (!ast)
         return {};
@@ -2177,8 +2175,9 @@ Shell::Shell()
     cache_path();
 }
 
-Shell::Shell(Line::Editor& editor, bool attempt_interactive)
-    : m_editor(editor)
+Shell::Shell(Line::Editor& editor, bool attempt_interactive, bool posix_mode)
+    : m_in_posix_mode(posix_mode)
+    , m_editor(editor)
 {
     uid = getuid();
     tcsetpgrp(0, getpgrp());
@@ -2224,8 +2223,8 @@ Shell::Shell(Line::Editor& editor, bool attempt_interactive)
         cache_path();
     }
 
-    m_editor->register_key_input_callback('\n', [](Line::Editor& editor) {
-        auto ast = Parser(editor.line()).parse();
+    m_editor->register_key_input_callback('\n', [this](Line::Editor& editor) {
+        auto ast = parse(editor.line(), false);
         if (ast && ast->is_syntax_error() && ast->syntax_error_node().is_continuable())
             return true;
 
@@ -2484,6 +2483,32 @@ void Shell::timer_event(Core::TimerEvent& event)
         m_editor->save_history(get_history_path());
 }
 
+RefPtr<AST::Node> Shell::parse(StringView input, bool interactive, bool as_command) const
+{
+    if (m_in_posix_mode) {
+        Posix::Parser parser(input);
+        if (as_command) {
+            auto node = parser.parse();
+            if constexpr (SHELL_POSIX_PARSER_DEBUG) {
+                dbgln("Parsed with the POSIX Parser:");
+                node->dump(0);
+            }
+            return node;
+        }
+
+        return parser.parse_word_list();
+    }
+
+    Parser parser { input, interactive };
+    if (as_command)
+        return parser.parse();
+
+    auto nodes = parser.parse_as_multiple_expressions();
+    return make_ref_counted<AST::ListConcatenate>(
+        nodes.is_empty() ? AST::Position { 0, 0, { 0, 0 }, { 0, 0 } } : nodes.first().position(),
+        move(nodes));
+}
+
 void FileDescriptionCollector::collect()
 {
     for (auto fd : m_fds)

+ 24 - 11
Userland/Shell/Shell.h

@@ -61,16 +61,26 @@
     __ENUMERATE_SHELL_OPTION(verbose, false, "Announce every command that is about to be executed")                  \
     __ENUMERATE_SHELL_OPTION(invoke_program_for_autocomplete, false, "Attempt to use the program being completed itself for autocompletion via --complete")
 
-#define ENUMERATE_SHELL_IMMEDIATE_FUNCTIONS()           \
-    __ENUMERATE_SHELL_IMMEDIATE_FUNCTION(concat_lists)  \
-    __ENUMERATE_SHELL_IMMEDIATE_FUNCTION(length)        \
-    __ENUMERATE_SHELL_IMMEDIATE_FUNCTION(length_across) \
-    __ENUMERATE_SHELL_IMMEDIATE_FUNCTION(remove_suffix) \
-    __ENUMERATE_SHELL_IMMEDIATE_FUNCTION(remove_prefix) \
-    __ENUMERATE_SHELL_IMMEDIATE_FUNCTION(regex_replace) \
-    __ENUMERATE_SHELL_IMMEDIATE_FUNCTION(filter_glob)   \
-    __ENUMERATE_SHELL_IMMEDIATE_FUNCTION(split)         \
-    __ENUMERATE_SHELL_IMMEDIATE_FUNCTION(join)
+#define ENUMERATE_SHELL_IMMEDIATE_FUNCTIONS()                          \
+    __ENUMERATE_SHELL_IMMEDIATE_FUNCTION(concat_lists)                 \
+    __ENUMERATE_SHELL_IMMEDIATE_FUNCTION(length)                       \
+    __ENUMERATE_SHELL_IMMEDIATE_FUNCTION(length_across)                \
+    __ENUMERATE_SHELL_IMMEDIATE_FUNCTION(remove_suffix)                \
+    __ENUMERATE_SHELL_IMMEDIATE_FUNCTION(remove_prefix)                \
+    __ENUMERATE_SHELL_IMMEDIATE_FUNCTION(regex_replace)                \
+    __ENUMERATE_SHELL_IMMEDIATE_FUNCTION(filter_glob)                  \
+    __ENUMERATE_SHELL_IMMEDIATE_FUNCTION(split)                        \
+    __ENUMERATE_SHELL_IMMEDIATE_FUNCTION(join)                         \
+    __ENUMERATE_SHELL_IMMEDIATE_FUNCTION(value_or_default)             \
+    __ENUMERATE_SHELL_IMMEDIATE_FUNCTION(assign_default)               \
+    __ENUMERATE_SHELL_IMMEDIATE_FUNCTION(error_if_empty)               \
+    __ENUMERATE_SHELL_IMMEDIATE_FUNCTION(null_or_alternative)          \
+    __ENUMERATE_SHELL_IMMEDIATE_FUNCTION(defined_value_or_default)     \
+    __ENUMERATE_SHELL_IMMEDIATE_FUNCTION(assign_defined_default)       \
+    __ENUMERATE_SHELL_IMMEDIATE_FUNCTION(error_if_unset)               \
+    __ENUMERATE_SHELL_IMMEDIATE_FUNCTION(null_if_unset_or_alternative) \
+    __ENUMERATE_SHELL_IMMEDIATE_FUNCTION(length_of_variable)           \
+    __ENUMERATE_SHELL_IMMEDIATE_FUNCTION(reexpand)
 
 namespace Shell {
 
@@ -376,10 +386,12 @@ public:
 #undef __ENUMERATE_SHELL_OPTION
 
 private:
-    Shell(Line::Editor&, bool attempt_interactive);
+    Shell(Line::Editor&, bool attempt_interactive, bool posix_mode = false);
     Shell();
     virtual ~Shell() override;
 
+    RefPtr<AST::Node> parse(StringView, bool interactive = false, bool as_command = true) const;
+
     void timer_event(Core::TimerEvent&) override;
 
     bool is_allowed_to_modify_termios(const AST::Command&) const;
@@ -450,6 +462,7 @@ private:
     bool m_is_interactive { true };
     bool m_is_subshell { false };
     bool m_should_reinstall_signal_handlers { true };
+    bool m_in_posix_mode { false };
 
     ShellError m_error { ShellError::None };
     DeprecatedString m_error_description;