1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-25 15:57:45 +00:00

Shell: Improve the parsing of history event designators

This commit is contained in:
TheFightingCatfish 2021-07-30 18:55:57 +08:00 committed by Ali Mohammad Pur
parent 05c3755e62
commit f67c2c97b1
3 changed files with 98 additions and 72 deletions

View file

@ -362,20 +362,25 @@ match "$(make_some_value)" {
History expansion may be utilized to reuse previously typed words or commands. History expansion may be utilized to reuse previously typed words or commands.
Such expressions are of the general form `!<event_designator>(:<word_designator>)`, where `event_designator` would select an entry in the shell history, and `word_designator` would select a word (or a range of words) from that entry. Such expressions are of the general form `!<event_designator>(:<word_designator>)`, where `event_designator` would select an entry in the shell history, and `word_designator` would select a word (or a range of words) from that entry.
| Event designator | effect | | Event designator | Effect |
| :- | :----- | | :- | :----- |
| `!` | Select the immediately preceding command | | `!` | The immediately preceding command |
| _n_ | Select the _n_'th entry in the history | | _n_ | The _n_'th entry in the history, starting with 1 as the first entry |
| -_n_ | Select the last _n_'th entry in the history | | -_n_ | The last _n_'th entry in the history, starting with -1 as the previous entry |
| _str_ | Select the most recent entry starting with _str_ | | _str_ | The most recent entry starting with _str_ |
| ?_str_ | Select the most recent entry containing _str_ | | `?`_str_ | The most recent entry containing _str_ |
| Word designator | effect | | Word designator | Effect |
| :-- | :----- | | :-- | :----- |
| _n_ | The _n_'th word, starting with 0 as the command | | _n_ | The word at index _n_, starting with 0 as the first word (usually the command) |
| `^` | The first word (index 0) | | `^` | The first argument (index 1) |
| `$` | The last word | | `$` | The last argument |
| _x_-_y_ | The range of words starting at _x_ and ending at _y_ (inclusive) | | _x_-_y_ | The range of words starting at _x_ and ending at _y_ (inclusive). _x_ defaults to 0 if omitted |
| `*` | All the arguments. Equivalent to `^`-`$` |
| _x_`*` | The range of words starting at _x_ and ending at the last word (`$`) (inclusive) |
| _x_- | The range of words starting at _x_ and ending at the second to last word (inclusive). _x_ defaults to 0 if omitted |
Note: The event designator and the word designator should usually be separated by a colon (`:`). This colon can be omitted only if the word designator starts with `^`, `$` or `*` (such as `!1^` for the first argument of the first entry in the history).
## Formal Grammar ## Formal Grammar

View file

@ -917,7 +917,7 @@ struct HistorySelector {
if (kind == Index) if (kind == Index)
return selector; return selector;
if (kind == Last) if (kind == Last)
return size - 1; return size - selector - 1;
VERIFY_NOT_REACHED(); VERIFY_NOT_REACHED();
} }
}; };

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2020, the SerenityOS developers. * Copyright (c) 2020-2021, the SerenityOS developers.
* *
* SPDX-License-Identifier: BSD-2-Clause * SPDX-License-Identifier: BSD-2-Clause
*/ */
@ -1589,7 +1589,17 @@ RefPtr<AST::Node> Parser::parse_history_designator()
nullptr } nullptr }
}; };
bool is_word_selector = false;
switch (peek()) { switch (peek()) {
case ':':
consume();
[[fallthrough]];
case '^':
case '$':
case '*':
is_word_selector = true;
break;
case '!': case '!':
consume(); consume();
selector.event.kind = AST::HistorySelector::EventKind::IndexFromEnd; selector.event.kind = AST::HistorySelector::EventKind::IndexFromEnd;
@ -1601,7 +1611,7 @@ RefPtr<AST::Node> Parser::parse_history_designator()
selector.event.kind = AST::HistorySelector::EventKind::ContainingStringLookup; selector.event.kind = AST::HistorySelector::EventKind::ContainingStringLookup;
[[fallthrough]]; [[fallthrough]];
default: { default: {
TemporaryChange chars_change { m_extra_chars_not_allowed_in_barewords, { ':' } }; TemporaryChange chars_change { m_extra_chars_not_allowed_in_barewords, { ':', '^', '$', '*' } };
auto bareword = parse_bareword(); auto bareword = parse_bareword();
if (!bareword || !bareword->is_bareword()) { if (!bareword || !bareword->is_bareword()) {
@ -1622,91 +1632,102 @@ RefPtr<AST::Node> Parser::parse_history_designator()
selector.event.kind = AST::HistorySelector::EventKind::IndexFromEnd; selector.event.kind = AST::HistorySelector::EventKind::IndexFromEnd;
else else
selector.event.kind = AST::HistorySelector::EventKind::IndexFromStart; selector.event.kind = AST::HistorySelector::EventKind::IndexFromStart;
auto number = selector.event.text.to_int(); auto number = abs(selector.event.text.to_int().value_or(0));
if (number.has_value()) if (number != 0)
selector.event.index = abs(number.value()); selector.event.index = number - 1;
else else
syntax_error = create<AST::SyntaxError>("History entry index value invalid or out of range"); syntax_error = create<AST::SyntaxError>("History entry index value invalid or out of range");
} }
break; if (":^$*"sv.contains(peek())) {
is_word_selector = true;
if (peek() == ':')
consume();
}
} }
} }
if (peek() != ':') { if (!is_word_selector) {
auto node = create<AST::HistoryEvent>(move(selector)); auto node = create<AST::HistoryEvent>(move(selector));
if (syntax_error) if (syntax_error)
node->set_is_syntax_error(*syntax_error); node->set_is_syntax_error(*syntax_error);
return node; return node;
} }
consume();
// Word selectors // Word selectors
auto parse_word_selector = [&]() -> Optional<AST::HistorySelector::WordSelector> { auto parse_word_selector = [&]() -> Optional<AST::HistorySelector::WordSelector> {
auto rule_start = push_start();
auto c = peek(); auto c = peek();
AST::HistorySelector::WordSelectorKind word_selector_kind;
ssize_t offset = -1;
if (isdigit(c)) { if (isdigit(c)) {
auto num = consume_while(is_digit); auto num = consume_while(is_digit);
auto value = num.to_uint(); auto value = num.to_uint();
if (!value.has_value()) { if (!value.has_value())
return AST::HistorySelector::WordSelector { return {};
AST::HistorySelector::WordSelectorKind::Index, word_selector_kind = AST::HistorySelector::WordSelectorKind::Index;
0, offset = value.value();
{ m_rule_start_offsets.last(), m_offset, m_rule_start_lines.last(), line() }, } else if (c == '^') {
syntax_error ? NonnullRefPtr(*syntax_error) : create<AST::SyntaxError>("Word selector value invalid or out of range")
};
}
return AST::HistorySelector::WordSelector {
AST::HistorySelector::WordSelectorKind::Index,
value.value(),
{ m_rule_start_offsets.last(), m_offset, m_rule_start_lines.last(), line() },
syntax_error
};
}
if (c == '^') {
consume(); consume();
return AST::HistorySelector::WordSelector { word_selector_kind = AST::HistorySelector::WordSelectorKind::Index;
AST::HistorySelector::WordSelectorKind::Index, offset = 1;
0, } else if (c == '$') {
{ m_rule_start_offsets.last(), m_offset, m_rule_start_lines.last(), line() },
syntax_error
};
}
if (c == '$') {
consume(); consume();
return AST::HistorySelector::WordSelector { word_selector_kind = AST::HistorySelector::WordSelectorKind::Last;
AST::HistorySelector::WordSelectorKind::Last, offset = 0;
0,
{ m_rule_start_offsets.last(), m_offset, m_rule_start_lines.last(), line() },
syntax_error
};
} }
return {}; if (offset == -1)
return {};
return AST::HistorySelector::WordSelector {
word_selector_kind,
static_cast<size_t>(offset),
{ m_rule_start_offsets.last(), m_offset, m_rule_start_lines.last(), line() },
syntax_error
};
}; };
auto start = parse_word_selector(); auto make_word_selector = [&](AST::HistorySelector::WordSelectorKind word_selector_kind, size_t offset) {
if (!start.has_value()) { return AST::HistorySelector::WordSelector {
word_selector_kind,
offset,
{ m_rule_start_offsets.last(), m_offset, m_rule_start_lines.last(), line() },
syntax_error
};
};
auto first_char = peek();
if (!(is_digit(first_char) || "^$-*"sv.contains(first_char))) {
if (!syntax_error) if (!syntax_error)
syntax_error = create<AST::SyntaxError>("Expected a word selector after ':' in a history event designator", true); syntax_error = create<AST::SyntaxError>("Expected a word selector after ':' in a history event designator", true);
auto node = create<AST::HistoryEvent>(move(selector)); } else if (first_char == '*') {
node->set_is_syntax_error(*syntax_error);
return node;
}
selector.word_selector_range.start = start.release_value();
if (peek() == '-') {
consume(); consume();
auto end = parse_word_selector(); selector.word_selector_range.start = make_word_selector(AST::HistorySelector::WordSelectorKind::Index, 1);
if (!end.has_value()) { selector.word_selector_range.end = make_word_selector(AST::HistorySelector::WordSelectorKind::Last, 0);
if (!syntax_error) } else if (first_char == '-') {
syntax_error = create<AST::SyntaxError>("Expected a word selector after '-' in a history event designator word selector", true); consume();
auto node = create<AST::HistoryEvent>(move(selector)); selector.word_selector_range.start = make_word_selector(AST::HistorySelector::WordSelectorKind::Index, 0);
node->set_is_syntax_error(*syntax_error); auto last_selector = parse_word_selector();
return node; if (!last_selector.has_value())
} selector.word_selector_range.end = make_word_selector(AST::HistorySelector::WordSelectorKind::Last, 1);
selector.word_selector_range.end = move(end); else
selector.word_selector_range.end = last_selector.release_value();
} else { } else {
selector.word_selector_range.end.clear(); auto first_selector = parse_word_selector();
// peek() should be a digit, ^, or $ here, so this should always have value.
VERIFY(first_selector.has_value());
selector.word_selector_range.start = first_selector.release_value();
if (peek() == '-') {
consume();
auto last_selector = parse_word_selector();
if (last_selector.has_value()) {
selector.word_selector_range.end = last_selector.release_value();
} else {
selector.word_selector_range.end = make_word_selector(AST::HistorySelector::WordSelectorKind::Last, 1);
}
} else if (peek() == '*') {
consume();
selector.word_selector_range.end = make_word_selector(AST::HistorySelector::WordSelectorKind::Last, 0);
} else {
selector.word_selector_range.end.clear();
}
} }
auto node = create<AST::HistoryEvent>(move(selector)); auto node = create<AST::HistoryEvent>(move(selector));