1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-05-31 15:48:12 +00:00

Shell: Add support for indexing into variables

Now a variable may have an optional slice (only _one_ slice), which can
also use negative indices to index from the end.
This works on both lists and strings.
The contents of the slice have the same semantics as brace expansions.
For example:
```sh
$ x=(1 2 3 4 5 6)
$ echo $x[1..3] # select indices 1, 2, 3
2 3 4
$ echo $x[3,4,1,0] # select indices 3, 4, 1, 0 (in that order)
4 5 2 1
$ x="Well Hello Friends!"
$ echo $x[5..9]
Hello
```
This commit is contained in:
AnotherTest 2021-03-13 03:10:18 +03:30 committed by Andreas Kling
parent ddcef0452a
commit 3b8fa5a753
11 changed files with 391 additions and 56 deletions

View file

@ -173,6 +173,101 @@ static inline Vector<Command> join_commands(Vector<Command> left, Vector<Command
return commands;
}
static String resolve_slices(RefPtr<Shell> shell, String&& input_value, NonnullRefPtrVector<Slice> slices)
{
if (slices.is_empty())
return move(input_value);
for (auto& slice : slices) {
auto value = slice.run(shell);
if (!value) {
shell->raise_error(Shell::ShellError::InvalidSliceContentsError, "Invalid slice contents", slice.position());
return move(input_value);
}
auto index_values = value->resolve_as_list(shell);
Vector<size_t> indices;
indices.ensure_capacity(index_values.size());
size_t i = 0;
for (auto& value : index_values) {
auto maybe_index = value.to_int();
if (!maybe_index.has_value()) {
shell->raise_error(Shell::ShellError::InvalidSliceContentsError, String::formatted("Invalid value in slice index {}: {} (expected a number)", i, value), slice.position());
return move(input_value);
}
++i;
auto index = maybe_index.value();
auto original_index = index;
if (index < 0)
index += input_value.length();
if (index < 0 || (size_t)index >= input_value.length()) {
shell->raise_error(Shell::ShellError::InvalidSliceContentsError, String::formatted("Slice index {} (evaluated as {}) out of value bounds [0-{})", index, original_index, input_value.length()), slice.position());
return move(input_value);
}
indices.unchecked_append(index);
}
StringBuilder builder { indices.size() };
for (auto& index : indices)
builder.append(input_value[index]);
input_value = builder.build();
}
return move(input_value);
}
static Vector<String> resolve_slices(RefPtr<Shell> shell, Vector<String>&& values, NonnullRefPtrVector<Slice> slices)
{
if (slices.is_empty())
return move(values);
for (auto& slice : slices) {
auto value = slice.run(shell);
if (!value) {
shell->raise_error(Shell::ShellError::InvalidSliceContentsError, "Invalid slice contents", slice.position());
return move(values);
}
auto index_values = value->resolve_as_list(shell);
Vector<size_t> indices;
indices.ensure_capacity(index_values.size());
size_t i = 0;
for (auto& value : index_values) {
auto maybe_index = value.to_int();
if (!maybe_index.has_value()) {
shell->raise_error(Shell::ShellError::InvalidSliceContentsError, String::formatted("Invalid value in slice index {}: {} (expected a number)", i, value), slice.position());
return move(values);
}
++i;
auto index = maybe_index.value();
auto original_index = index;
if (index < 0)
index += values.size();
if (index < 0 || (size_t)index >= values.size()) {
shell->raise_error(Shell::ShellError::InvalidSliceContentsError, String::formatted("Slice index {} (evaluated as {}) out of value bounds [0-{})", index, original_index, values.size()), slice.position());
return move(values);
}
indices.unchecked_append(index);
}
Vector<String> result;
result.ensure_capacity(indices.size());
for (auto& index : indices)
result.unchecked_append(values[index]);
values = move(result);
}
return move(values);
}
void Node::for_each_entry(RefPtr<Shell> shell, Function<IterationDecision(NonnullRefPtr<Value>)> callback)
{
auto value = run(shell)->resolve_without_cast(shell);
@ -2529,23 +2624,73 @@ Subshell::~Subshell()
{
}
void Slice::dump(int level) const
{
Node::dump(level);
m_selector->dump(level + 1);
}
RefPtr<Value> Slice::run(RefPtr<Shell> shell)
{
return m_selector->run(shell);
}
void Slice::highlight_in_editor(Line::Editor& editor, Shell& shell, HighlightMetadata metadata)
{
m_selector->highlight_in_editor(editor, shell, metadata);
}
HitTestResult Slice::hit_test_position(size_t offset) const
{
return m_selector->hit_test_position(offset);
}
Vector<Line::CompletionSuggestion> Slice::complete_for_editor(Shell& shell, size_t offset, const HitTestResult& hit_test_result)
{
// TODO: Maybe intercept this, and suggest values in range?
return m_selector->complete_for_editor(shell, offset, hit_test_result);
}
Slice::Slice(Position position, NonnullRefPtr<AST::Node> selector)
: Node(move(position))
, m_selector(move(selector))
{
if (m_selector->is_syntax_error())
set_is_syntax_error(m_selector->syntax_error_node());
}
Slice::~Slice()
{
}
void SimpleVariable::dump(int level) const
{
Node::dump(level);
print_indented(m_name, level + 1);
print_indented("(Name)", level + 1);
print_indented(m_name, level + 2);
print_indented("(Slice)", level + 1);
if (m_slice)
m_slice->dump(level + 2);
else
print_indented("(None)", level + 2);
}
RefPtr<Value> SimpleVariable::run(RefPtr<Shell>)
{
return create<SimpleVariableValue>(m_name);
NonnullRefPtr<Value> value = create<SimpleVariableValue>(m_name);
if (m_slice)
value = value->with_slices(*m_slice);
return value;
}
void SimpleVariable::highlight_in_editor(Line::Editor& editor, Shell&, HighlightMetadata metadata)
void SimpleVariable::highlight_in_editor(Line::Editor& editor, Shell& shell, HighlightMetadata metadata)
{
Line::Style style { Line::Style::Foreground(214, 112, 214) };
if (metadata.is_first_in_list)
style.unify_with({ Line::Style::Bold });
editor.stylize({ m_position.start_offset, m_position.end_offset }, move(style));
if (m_slice)
m_slice->highlight_in_editor(editor, shell, metadata);
}
HitTestResult SimpleVariable::hit_test_position(size_t offset) const
@ -2553,6 +2698,9 @@ HitTestResult SimpleVariable::hit_test_position(size_t offset) const
if (!position().contains(offset))
return {};
if (m_slice && m_slice->position().contains(offset))
return m_slice->hit_test_position(offset);
return { this, this, nullptr };
}
@ -2574,7 +2722,7 @@ Vector<Line::CompletionSuggestion> SimpleVariable::complete_for_editor(Shell& sh
}
SimpleVariable::SimpleVariable(Position position, String name)
: Node(move(position))
: VariableNode(move(position))
, m_name(move(name))
{
}
@ -2586,17 +2734,28 @@ SimpleVariable::~SimpleVariable()
void SpecialVariable::dump(int level) const
{
Node::dump(level);
print_indented("(Name)", level + 1);
print_indented(String { &m_name, 1 }, level + 1);
print_indented("(Slice)", level + 1);
if (m_slice)
m_slice->dump(level + 2);
else
print_indented("(None)", level + 2);
}
RefPtr<Value> SpecialVariable::run(RefPtr<Shell>)
{
return create<SpecialVariableValue>(m_name);
NonnullRefPtr<Value> value = create<SpecialVariableValue>(m_name);
if (m_slice)
value = value->with_slices(*m_slice);
return value;
}
void SpecialVariable::highlight_in_editor(Line::Editor& editor, Shell&, HighlightMetadata)
void SpecialVariable::highlight_in_editor(Line::Editor& editor, Shell& shell, HighlightMetadata metadata)
{
editor.stylize({ m_position.start_offset, m_position.end_offset }, { Line::Style::Foreground(214, 112, 214) });
if (m_slice)
m_slice->highlight_in_editor(editor, shell, metadata);
}
Vector<Line::CompletionSuggestion> SpecialVariable::complete_for_editor(Shell&, size_t, const HitTestResult&)
@ -2609,11 +2768,14 @@ HitTestResult SpecialVariable::hit_test_position(size_t offset) const
if (!position().contains(offset))
return {};
if (m_slice && m_slice->position().contains(offset))
return m_slice->hit_test_position(offset);
return { this, this, nullptr };
}
SpecialVariable::SpecialVariable(Position position, char name)
: Node(move(position))
: VariableNode(move(position))
, m_name(name)
{
}
@ -3091,6 +3253,20 @@ ListValue::ListValue(Vector<String> values)
m_contained_values.append(adopt(*new StringValue(move(str))));
}
NonnullRefPtr<Value> Value::with_slices(NonnullRefPtr<Slice> slice) const&
{
auto value = clone();
value->m_slices.append(move(slice));
return value;
}
NonnullRefPtr<Value> Value::with_slices(NonnullRefPtrVector<Slice> slices) const&
{
auto value = clone();
value->m_slices.append(move(slices));
return value;
}
ListValue::~ListValue()
{
}
@ -3101,7 +3277,7 @@ Vector<String> ListValue::resolve_as_list(RefPtr<Shell> shell)
for (auto& value : m_contained_values)
values.append(value.resolve_as_list(shell));
return values;
return resolve_slices(shell, move(values), m_slices);
}
NonnullRefPtr<Value> ListValue::resolve_without_cast(RefPtr<Shell> shell)
@ -3110,7 +3286,10 @@ NonnullRefPtr<Value> ListValue::resolve_without_cast(RefPtr<Shell> shell)
for (auto& value : m_contained_values)
values.append(value.resolve_without_cast(shell));
return create<ListValue>(move(values));
NonnullRefPtr<Value> value = create<ListValue>(move(values));
if (!m_slices.is_empty())
value = value->with_slices(m_slices);
return value;
}
CommandValue::~CommandValue()
@ -3121,9 +3300,9 @@ CommandSequenceValue::~CommandSequenceValue()
{
}
Vector<String> CommandSequenceValue::resolve_as_list(RefPtr<Shell>)
Vector<String> CommandSequenceValue::resolve_as_list(RefPtr<Shell> shell)
{
// TODO: Somehow raise an "error".
shell->raise_error(Shell::ShellError::EvaluatedSyntaxError, "Unexpected cast of a command sequence to a list");
return {};
}
@ -3132,9 +3311,9 @@ Vector<Command> CommandSequenceValue::resolve_as_commands(RefPtr<Shell>)
return m_contained_values;
}
Vector<String> CommandValue::resolve_as_list(RefPtr<Shell>)
Vector<String> CommandValue::resolve_as_list(RefPtr<Shell> shell)
{
// TODO: Somehow raise an "error".
shell->raise_error(Shell::ShellError::EvaluatedSyntaxError, "Unexpected cast of a command to a list");
return {};
}
@ -3150,7 +3329,7 @@ JobValue::~JobValue()
StringValue::~StringValue()
{
}
Vector<String> StringValue::resolve_as_list(RefPtr<Shell>)
Vector<String> StringValue::resolve_as_list(RefPtr<Shell> shell)
{
if (is_list()) {
auto parts = StringView(m_string).split_view(m_split, m_keep_empty);
@ -3158,16 +3337,16 @@ Vector<String> StringValue::resolve_as_list(RefPtr<Shell>)
result.ensure_capacity(parts.size());
for (auto& part : parts)
result.append(part);
return result;
return resolve_slices(shell, move(result), m_slices);
}
return { m_string };
return { resolve_slices(shell, String { m_string }, m_slices) };
}
NonnullRefPtr<Value> StringValue::resolve_without_cast(RefPtr<Shell> shell)
{
if (is_list())
return create<AST::ListValue>(resolve_as_list(shell));
return create<AST::ListValue>(resolve_as_list(shell)); // No need to reapply the slices.
return *this;
}
@ -3178,12 +3357,12 @@ GlobValue::~GlobValue()
Vector<String> GlobValue::resolve_as_list(RefPtr<Shell> shell)
{
if (!shell)
return { m_glob };
return { resolve_slices(shell, String { m_glob }, m_slices) };
auto results = shell->expand_globs(m_glob, shell->cwd);
if (results.is_empty())
shell->raise_error(Shell::ShellError::InvalidGlobError, "Glob did not match anything!", m_generation_position);
return results;
return resolve_slices(shell, move(results), m_slices);
}
SimpleVariableValue::~SimpleVariableValue()
@ -3192,29 +3371,31 @@ SimpleVariableValue::~SimpleVariableValue()
Vector<String> SimpleVariableValue::resolve_as_list(RefPtr<Shell> shell)
{
if (!shell)
return {};
return resolve_slices(shell, Vector<String> {}, m_slices);
if (auto value = resolve_without_cast(shell); value != this)
return value->resolve_as_list(shell);
char* env_value = getenv(m_name.characters());
if (env_value == nullptr)
return { "" };
return { resolve_slices(shell, "", m_slices) };
Vector<String> res;
String str_env_value = String(env_value);
const auto& split_text = str_env_value.split_view(' ');
for (auto& part : split_text)
res.append(part);
return res;
return { resolve_slices(shell, String { env_value }, m_slices) };
}
NonnullRefPtr<Value> SimpleVariableValue::resolve_without_cast(RefPtr<Shell> shell)
{
VERIFY(shell);
if (auto value = shell->lookup_local_variable(m_name))
return value.release_nonnull();
if (auto value = shell->lookup_local_variable(m_name)) {
auto result = value.release_nonnull();
// If a slice is applied, add it.
if (!m_slices.is_empty())
result = result->with_slices(m_slices);
return result;
}
return *this;
}
@ -3229,24 +3410,24 @@ Vector<String> SpecialVariableValue::resolve_as_list(RefPtr<Shell> shell)
switch (m_name) {
case '?':
return { String::number(shell->last_return_code) };
return { resolve_slices(shell, String::number(shell->last_return_code), m_slices) };
case '$':
return { String::number(getpid()) };
return { resolve_slices(shell, String::number(getpid()), m_slices) };
case '*':
if (auto argv = shell->lookup_local_variable("ARGV"))
return argv->resolve_as_list(shell);
return {};
return resolve_slices(shell, argv->resolve_as_list(shell), m_slices);
return resolve_slices(shell, Vector<String> {}, m_slices);
case '#':
if (auto argv = shell->lookup_local_variable("ARGV")) {
if (argv->is_list()) {
auto list_argv = static_cast<AST::ListValue*>(argv.ptr());
return { String::number(list_argv->values().size()) };
return { resolve_slices(shell, String::number(list_argv->values().size()), m_slices) };
}
return { "1" };
return { resolve_slices(shell, "1", m_slices) };
}
return { "0" };
return { resolve_slices(shell, "0", m_slices) };
default:
return { "" };
return { resolve_slices(shell, "", m_slices) };
}
}
@ -3260,9 +3441,9 @@ Vector<String> TildeValue::resolve_as_list(RefPtr<Shell> shell)
builder.append(m_username);
if (!shell)
return { builder.to_string() };
return { resolve_slices(shell, builder.to_string(), m_slices) };
return { shell->expand_tilde(builder.to_string()) };
return { resolve_slices(shell, shell->expand_tilde(builder.to_string()), m_slices) };
}
Result<NonnullRefPtr<Rewiring>, String> CloseRedirection::apply() const