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:
parent
ddcef0452a
commit
3b8fa5a753
11 changed files with 391 additions and 56 deletions
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue