parseCommand function
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);
}