parseCommand function

CommandList parseCommand(
  1. String input
)

Parse a command string into a CommandList.

Implementation

CommandList parseCommand(String input) {
  final tokens = tokenizeBash(input);
  final entries = <CommandListEntry>[];

  // Filter out comments and newlines for simpler parsing.
  final filtered = tokens
      .where(
        (t) =>
            t.type != BashTokenType.comment && t.type != BashTokenType.newline,
      )
      .toList();

  if (filtered.isEmpty) {
    return const CommandList(entries: []);
  }

  var i = 0;

  /// Consume one simple command from the token stream.
  SimpleCommand? parseSimpleCmd() {
    final assignments = <String, String>{};
    final redirects = <Redirect>[];
    final words = <String>[];

    // Leading assignments.
    while (i < filtered.length &&
        filtered[i].type == BashTokenType.assignment) {
      final parsed = parseAssignment(filtered[i].value);
      if (parsed != null) {
        assignments[parsed.name] = parsed.value;
      }
      i++;
    }

    // Words, redirects, variables, quoted strings, etc.
    while (i < filtered.length) {
      final t = filtered[i];

      // Stop at list/pipe operators.
      if (t.type == BashTokenType.pipe ||
          t.type == BashTokenType.and_ ||
          t.type == BashTokenType.or_ ||
          t.type == BashTokenType.semicolon ||
          t.type == BashTokenType.background ||
          t.type == BashTokenType.rparen ||
          t.type == BashTokenType.rbrace) {
        break;
      }

      // Redirections.
      if (t.type == BashTokenType.redirect ||
          t.type == BashTokenType.heredocMarker) {
        final rType = _redirectTypeFromToken(t.value);
        i++;
        String target = '';
        if (i < filtered.length) {
          target = _tokenTextValue(filtered[i]);
          i++;
        }
        redirects.add(
          Redirect(type: rType, fd: _fdFromRedirect(t.value), target: target),
        );
        continue;
      }

      // Everything else is a word.
      words.add(_tokenTextValue(t));
      i++;
    }

    if (words.isEmpty && assignments.isEmpty) return null;

    final executable = words.isNotEmpty ? words.first : '';
    final args = words.length > 1 ? words.sublist(1) : <String>[];

    return SimpleCommand(
      executable: executable,
      arguments: args,
      assignments: assignments,
      redirects: redirects,
    );
  }

  /// Parse one pipeline.
  Pipeline parsePipe() {
    var negated = false;
    if (i < filtered.length &&
        filtered[i].type == BashTokenType.word &&
        filtered[i].value == '!') {
      negated = true;
      i++;
    }

    final commands = <SimpleCommand>[];
    final first = parseSimpleCmd();
    if (first != null) commands.add(first);

    while (i < filtered.length && filtered[i].type == BashTokenType.pipe) {
      i++; // skip |
      final next = parseSimpleCmd();
      if (next != null) commands.add(next);
    }

    return Pipeline(commands: commands, negated: negated);
  }

  // Parse the full command list.
  while (i < filtered.length) {
    final pipeline = parsePipe();

    var op = ListOperator.sequential;
    if (i < filtered.length) {
      final t = filtered[i];
      if (t.type == BashTokenType.and_) {
        op = ListOperator.and_;
        i++;
      } else if (t.type == BashTokenType.or_) {
        op = ListOperator.or_;
        i++;
      } else if (t.type == BashTokenType.background) {
        op = ListOperator.background;
        i++;
      } else if (t.type == BashTokenType.semicolon) {
        op = ListOperator.sequential;
        i++;
      } else {
        // Unknown — skip to avoid infinite loop.
        i++;
      }
    }

    entries.add(CommandListEntry(pipeline: pipeline, operator_: op));
  }

  return CommandList(entries: entries);
}