summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnotherTest <ali.mpfard@gmail.com>2020-06-17 18:05:06 +0430
committerAndreas Kling <kling@serenityos.org>2020-07-05 15:43:14 +0200
commita4627f24390035a8a8402e019be42b03f525f034 (patch)
tree5f4d8832790d8a899d43f08ba89a81fe7b3647c5
parent6f7ac5d2e29d3dc764a565c2fb6425af8ee673d3 (diff)
downloadserenity-a4627f24390035a8a8402e019be42b03f525f034.zip
Shell: Switch to a new parser and AST
This commit also completely reworks the execution, highlighting and completion model to work with the new AST. New additions: - $(...) stdout captures - fd>&fd redirections - fd>&- redirections (close fd) - read-write redirections (<> path) - completely event-based execution - the weird idea of allowing the user to redirect the shell's own fds - variables in strings - local variables - minimal list support - adding hyperlinks to all paths that exist
-rw-r--r--Shell/AST.cpp1708
-rw-r--r--Shell/AST.h815
-rw-r--r--Shell/Builtin.cpp701
-rw-r--r--Shell/CMakeLists.txt2
-rw-r--r--Shell/Forward.h35
-rw-r--r--Shell/Job.h16
-rw-r--r--Shell/Parser.cpp966
-rw-r--r--Shell/Parser.h203
-rw-r--r--Shell/Shell.cpp1646
-rw-r--r--Shell/Shell.h74
-rw-r--r--Shell/main.cpp18
11 files changed, 4415 insertions, 1769 deletions
diff --git a/Shell/AST.cpp b/Shell/AST.cpp
new file mode 100644
index 0000000000..9f6340e913
--- /dev/null
+++ b/Shell/AST.cpp
@@ -0,0 +1,1708 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "AST.h"
+#include "Shell.h"
+#include <AK/String.h>
+#include <AK/StringBuilder.h>
+#include <AK/URL.h>
+#include <LibCore/File.h>
+
+//#define EXECUTE_DEBUG
+
+namespace AST {
+
+static inline void print_indented(const String& str, int indent)
+{
+ dbgprintf("%.*c%s\n", indent * 2, ' ', str.characters());
+}
+
+void Node::dump(int level) const
+{
+ print_indented(String::format("%s at %d:%d", class_name().characters(), m_position.start_offset, m_position.end_offset), level);
+}
+
+Node::Node(Position position)
+ : m_position(position)
+{
+}
+
+Vector<Line::CompletionSuggestion> Node::complete_for_editor(Shell& shell, size_t offset, RefPtr<Node> matching_node)
+{
+ if (matching_node) {
+ if (matching_node->is_bareword()) {
+ auto corrected_offset = offset - matching_node->position().start_offset;
+ auto* node = static_cast<BarewordLiteral*>(matching_node.ptr());
+
+ if (corrected_offset > node->text().length())
+ return {};
+ return shell.complete_path(node->text(), corrected_offset);
+ }
+ return {};
+ }
+ auto result = hit_test_position(offset);
+ if (!result.matching_node)
+ return {};
+ auto node = result.matching_node;
+ if (node->is_bareword() || node != result.closest_node_with_semantic_meaning)
+ node = result.closest_node_with_semantic_meaning;
+
+ if (!node)
+ return {};
+
+ return node->complete_for_editor(shell, offset, result.matching_node);
+}
+
+Vector<Line::CompletionSuggestion> Node::complete_for_editor(Shell& shell, size_t offset)
+{
+ return Node::complete_for_editor(shell, offset, nullptr);
+}
+
+Node::~Node()
+{
+}
+
+void And::dump(int level) const
+{
+ Node::dump(level);
+ m_left->dump(level + 1);
+ m_right->dump(level + 1);
+}
+
+RefPtr<Value> And::run(TheExecutionInputType input_value)
+{
+ auto shell = input_value;
+
+ auto left = m_left->run(input_value);
+ ASSERT(left->is_job());
+
+ auto* job_value = static_cast<JobValue*>(left.ptr());
+ const auto job = job_value->job();
+ if (!job) {
+ // Something has gone wrong, let's just pretend that the job failed.
+ return job_value;
+ }
+
+ shell->block_on_job(job);
+
+ if (job->exit_code() == 0)
+ return m_right->run(input_value);
+
+ return job_value;
+}
+
+void And::highlight_in_editor(Line::Editor& editor, Shell& shell, HighlightMetadata metadata)
+{
+ metadata.is_first_in_list = true;
+ m_left->highlight_in_editor(editor, shell, metadata);
+ m_right->highlight_in_editor(editor, shell, metadata);
+}
+
+HitTestResult And::hit_test_position(size_t offset)
+{
+ if (!position().contains(offset))
+ return {};
+
+ auto result = m_left->hit_test_position(offset);
+ if (result.matching_node)
+ return result;
+ return m_right->hit_test_position(offset);
+}
+
+And::And(Position position, RefPtr<Node> left, RefPtr<Node> right)
+ : Node(move(position))
+ , m_left(move(left))
+ , m_right(move(right))
+{
+}
+
+And::~And()
+{
+}
+
+void ListConcatenate::dump(int level) const
+{
+ Node::dump(level);
+ m_element->dump(level + 1);
+ m_list->dump(level + 1);
+}
+
+RefPtr<Value> ListConcatenate::run(TheExecutionInputType input_value)
+{
+ auto list = m_list->run(input_value);
+ auto element = m_element->run(input_value);
+
+ return adopt(*new ListValue({ move(element), move(list) }));
+}
+
+void ListConcatenate::highlight_in_editor(Line::Editor& editor, Shell& shell, HighlightMetadata metadata)
+{
+ auto first = metadata.is_first_in_list;
+ metadata.is_first_in_list = false;
+ m_list->highlight_in_editor(editor, shell, metadata);
+ metadata.is_first_in_list = first;
+ m_element->highlight_in_editor(editor, shell, metadata);
+}
+
+HitTestResult ListConcatenate::hit_test_position(size_t offset)
+{
+ if (!position().contains(offset))
+ return {};
+
+ auto result = m_element->hit_test_position(offset);
+ if (result.matching_node)
+ return result;
+ result = m_list->hit_test_position(offset);
+ if (!result.closest_node_with_semantic_meaning)
+ result.closest_node_with_semantic_meaning = this;
+ return result;
+}
+
+ListConcatenate::ListConcatenate(Position position, RefPtr<Node> element, RefPtr<Node> list)
+ : Node(move(position))
+ , m_element(move(element))
+ , m_list(move(list))
+{
+}
+
+ListConcatenate::~ListConcatenate()
+{
+}
+
+void Background::dump(int level) const
+{
+ Node::dump(level);
+ m_command->dump(level + 1);
+}
+
+RefPtr<Value> Background::run(TheExecutionInputType input_value)
+{
+ auto commands = m_command->run(input_value)->resolve_as_commands(input_value);
+ auto& last = commands.last();
+ last.should_wait = false;
+
+ return adopt(*new CommandSequenceValue(move(commands)));
+}
+
+void Background::highlight_in_editor(Line::Editor& editor, Shell& shell, HighlightMetadata metadata)
+{
+ m_command->highlight_in_editor(editor, shell, metadata);
+}
+
+HitTestResult Background::hit_test_position(size_t offset)
+{
+ if (!position().contains(offset))
+ return {};
+
+ return m_command->hit_test_position(offset);
+}
+
+Background::Background(Position position, RefPtr<Node> command)
+ : Node(move(position))
+ , m_command(move(command))
+{
+}
+
+Background::~Background()
+{
+}
+
+void BarewordLiteral::dump(int level) const
+{
+ Node::dump(level);
+ print_indented(m_text, level + 1);
+}
+
+RefPtr<Value> BarewordLiteral::run(TheExecutionInputType)
+{
+ return adopt(*new StringValue(m_text));
+}
+
+void BarewordLiteral::highlight_in_editor(Line::Editor& editor, Shell& shell, HighlightMetadata metadata)
+{
+ if (metadata.is_first_in_list) {
+ editor.stylize({ m_position.start_offset, m_position.end_offset }, { Line::Style::Bold });
+ return;
+ }
+ if (m_text.starts_with('-')) {
+ if (m_text == "--") {
+ editor.stylize({ m_position.start_offset, m_position.end_offset }, { Line::Style::Foreground(Line::Style::XtermColor::Green) });
+ return;
+ }
+ if (m_text == "-")
+ return;
+
+ if (m_text.starts_with("--")) {
+ auto index = m_text.index_of("=").value_or(m_text.length() - 1) + 1;
+ editor.stylize({ m_position.start_offset, m_position.start_offset + index }, { Line::Style::Foreground(Line::Style::XtermColor::Cyan) });
+ } else {
+ editor.stylize({ m_position.start_offset, m_position.end_offset }, { Line::Style::Foreground(Line::Style::XtermColor::Cyan) });
+ }
+ }
+ if (Core::File::exists(m_text)) {
+ auto realpath = shell.resolve_path(m_text);
+ auto url = URL::create_with_file_protocol(realpath);
+ url.set_host(shell.hostname);
+ editor.stylize({ m_position.start_offset, m_position.end_offset }, { Line::Style::Hyperlink(url.to_string()) });
+ }
+}
+
+BarewordLiteral::BarewordLiteral(Position position, String text)
+ : Node(move(position))
+ , m_text(move(text))
+{
+}
+
+BarewordLiteral::~BarewordLiteral()
+{
+}
+
+void CastToCommand::dump(int level) const
+{
+ Node::dump(level);
+ m_inner->dump(level + 1);
+}
+
+RefPtr<Value> CastToCommand::run(TheExecutionInputType input_value)
+{
+ if (m_inner->is_command())
+ return m_inner->run(input_value);
+
+ auto shell = input_value;
+ auto argv = m_inner->run(input_value)->resolve_as_list(input_value);
+
+ return adopt(*new CommandValue(move(argv)));
+}
+
+void CastToCommand::highlight_in_editor(Line::Editor& editor, Shell& shell, HighlightMetadata metadata)
+{
+ m_inner->highlight_in_editor(editor, shell, metadata);
+}
+
+HitTestResult CastToCommand::hit_test_position(size_t offset)
+{
+ if (!position().contains(offset))
+ return {};
+
+ auto result = m_inner->hit_test_position(offset);
+ if (!result.closest_node_with_semantic_meaning)
+ result.closest_node_with_semantic_meaning = this;
+ return result;
+}
+
+Vector<Line::CompletionSuggestion> CastToCommand::complete_for_editor(Shell& shell, size_t offset, RefPtr<Node> matching_node)
+{
+ if (!matching_node)
+ return {};
+
+ ASSERT(matching_node->is_bareword());
+ auto corrected_offset = offset - matching_node->position().start_offset;
+ auto* node = static_cast<BarewordLiteral*>(matching_node.ptr());
+
+ if (corrected_offset > node->text().length())
+ return {};
+
+ return shell.complete_program_name(node->text(), corrected_offset);
+}
+
+CastToCommand::CastToCommand(Position position, RefPtr<Node> inner)
+ : Node(move(position))
+ , m_inner(move(inner))
+{
+}
+
+CastToCommand::~CastToCommand()
+{
+}
+
+void CastToList::dump(int level) const
+{
+ Node::dump(level);
+ m_inner->dump(level + 1);
+}
+
+RefPtr<Value> CastToList::run(TheExecutionInputType input_value)
+{
+ auto shell = input_value;
+ auto values = m_inner->run(input_value)->resolve_as_list(input_value);
+ Vector<RefPtr<Value>> cast_values;
+ for (auto& value : values)
+ cast_values.append(adopt(*new StringValue(value)));
+
+ return adopt(*new ListValue(cast_values));
+}
+
+void CastToList::highlight_in_editor(Line::Editor& editor, Shell& shell, HighlightMetadata metadata)
+{
+ m_inner->highlight_in_editor(editor, shell, metadata);
+}
+
+HitTestResult CastToList::hit_test_position(size_t offset)
+{
+ if (!position().contains(offset))
+ return {};
+
+ return m_inner->hit_test_position(offset);
+}
+
+CastToList::CastToList(Position position, RefPtr<Node> inner)
+ : Node(move(position))
+ , m_inner(move(inner))
+{
+}
+
+CastToList::~CastToList()
+{
+}
+
+void CloseFdRedirection::dump(int level) const
+{
+ Node::dump(level);
+ print_indented(String::format("%d -> Close", m_fd), level);
+}
+
+RefPtr<Value> CloseFdRedirection::run(TheExecutionInputType)
+{
+ Command command;
+ command.redirections.append(*new CloseRedirection(m_fd));
+ return adopt(*new CommandValue(move(command)));
+}
+
+void CloseFdRedirection::highlight_in_editor(Line::Editor& editor, Shell&, HighlightMetadata)
+{
+ editor.stylize({ m_position.start_offset, m_position.end_offset - 1 }, { Line::Style::Foreground(0x87, 0x9b, 0xcd) }); // 25% Darkened Periwinkle
+ editor.stylize({ m_position.end_offset - 1, m_position.end_offset }, { Line::Style::Foreground(0xff, 0x7e, 0x00) }); // Amber
+}
+
+CloseFdRedirection::CloseFdRedirection(Position position, int fd)
+ : Node(move(position))
+ , m_fd(fd)
+{
+}
+
+CloseFdRedirection::~CloseFdRedirection()
+{
+}
+
+void CommandLiteral::dump(int level) const
+{
+ Node::dump(level);
+ print_indented("(Generated command literal)", level + 1);
+}
+
+RefPtr<Value> CommandLiteral::run(TheExecutionInputType)
+{
+ return adopt(*new CommandValue(m_command));
+}
+
+CommandLiteral::CommandLiteral(Position position, Command command)
+ : Node(move(position))
+ , m_command(move(command))
+{
+}
+
+CommandLiteral::~CommandLiteral()
+{
+}
+
+void Comment::dump(int level) const
+{
+ Node::dump(level);
+ print_indented(m_text, level + 1);
+}
+
+RefPtr<Value> Comment::run(TheExecutionInputType)
+{
+ return adopt(*new StringValue(""));
+}
+
+void Comment::highlight_in_editor(Line::Editor& editor, Shell&, HighlightMetadata)
+{
+ editor.stylize({ m_position.start_offset, m_position.end_offset }, { Line::Style::Foreground(150, 150, 150) }); // Light gray
+}
+
+Comment::Comment(Position position, String text)
+ : Node(move(position))
+ , m_text(move(text))
+{
+}
+
+Comment::~Comment()
+{
+}
+
+void DoubleQuotedString::dump(int level) const
+{
+ Node::dump(level);
+ m_inner->dump(level + 1);
+}
+
+RefPtr<Value> DoubleQuotedString::run(TheExecutionInputType input_value)
+{
+ StringBuilder builder;
+ auto shell = input_value;
+ auto values = m_inner->run(input_value)->resolve_as_list(input_value);
+
+ builder.join("", values);
+
+ return adopt(*new StringValue(builder.to_string()));
+}
+
+void DoubleQuotedString::highlight_in_editor(Line::Editor& editor, Shell& shell, HighlightMetadata metadata)
+{
+ Line::Style style { Line::Style::Foreground(Line::Style::XtermColor::Yellow) };
+ if (metadata.is_first_in_list)
+ style.unify_with({ Line::Style::Bold });
+
+ editor.stylize({ m_position.start_offset, m_position.end_offset }, style);
+ metadata.is_first_in_list = false;
+ m_inner->highlight_in_editor(editor, shell, metadata);
+}
+
+HitTestResult DoubleQuotedString::hit_test_position(size_t offset)
+{
+ if (!position().contains(offset))
+ return {};
+
+ return m_inner->hit_test_position(offset);
+}
+
+DoubleQuotedString::DoubleQuotedString(Position position, RefPtr<Node> inner)
+ : Node(move(position))
+ , m_inner(move(inner))
+{
+}
+
+DoubleQuotedString::~DoubleQuotedString()
+{
+}
+
+void DynamicEvaluate::dump(int level) const
+{
+ Node::dump(level);
+ m_inner->dump(level + 1);
+}
+
+RefPtr<Value> DynamicEvaluate::run(TheExecutionInputType input_value)
+{
+ auto result = m_inner->run(input_value);
+ // Dynamic Evaluation behaves differently between strings and lists.
+ // Strings are treated as variables, and Lists are treated as commands.
+ if (result->is_string()) {
+ auto name_part = result->resolve_as_list(input_value);
+ ASSERT(name_part.size() == 1);
+ return adopt(*new SimpleVariableValue(name_part[0]));
+ }
+
+ // If it's anything else, we're just gonna cast it to a list.
+ auto list = result->resolve_as_list(input_value);
+ return adopt(*new CommandValue(move(list)));
+}
+
+void DynamicEvaluate::highlight_in_editor(Line::Editor& editor, Shell& shell, HighlightMetadata metadata)
+{
+ editor.stylize({ m_position.start_offset, m_position.end_offset }, { Line::Style::Foreground(Line::Style::XtermColor::Yellow) });
+ m_inner->highlight_in_editor(editor, shell, metadata);
+}
+
+HitTestResult DynamicEvaluate::hit_test_position(size_t offset)
+{
+ if (!position().contains(offset))
+ return {};
+
+ return m_inner->hit_test_position(offset);
+}
+
+DynamicEvaluate::DynamicEvaluate(Position position, RefPtr<Node> inner)
+ : Node(move(position))
+ , m_inner(move(inner))
+{
+}
+
+DynamicEvaluate::~DynamicEvaluate()
+{
+}
+
+void Fd2FdRedirection::dump(int level) const
+{
+ Node::dump(level);
+ print_indented(String::format("%d -> %d", source_fd, dest_fd), level);
+}
+
+RefPtr<Value> Fd2FdRedirection::run(TheExecutionInputType)
+{
+ Command command;
+ command.redirections.append(*new FdRedirection(source_fd, dest_fd, Rewiring::Close::None));
+ return adopt(*new CommandValue(move(command)));
+}
+
+void Fd2FdRedirection::highlight_in_editor(Line::Editor& editor, Shell&, HighlightMetadata)
+{
+ editor.stylize({ m_position.start_offset, m_position.end_offset }, { Line::Style::Foreground(0x87, 0x9b, 0xcd) }); // 25% Darkened Periwinkle
+}
+
+Fd2FdRedirection::Fd2FdRedirection(Position position, int src, int dst)
+ : Node(move(position))
+ , source_fd(src)
+ , dest_fd(dst)
+{
+}
+
+Fd2FdRedirection::~Fd2FdRedirection()
+{
+}
+
+void Glob::dump(int level) const
+{
+ Node::dump(level);
+ print_indented(m_text, level + 1);
+}
+
+RefPtr<Value> Glob::run(TheExecutionInputType)
+{
+ return adopt(*new GlobValue(m_text));
+}
+
+void Glob::highlight_in_editor(Line::Editor& editor, Shell&, HighlightMetadata metadata)
+{
+ Line::Style style { Line::Style::Foreground(Line::Style::XtermColor::Cyan) };
+ if (metadata.is_first_in_list)
+ style.unify_with({ Line::Style::Bold });
+ editor.stylize({ m_position.start_offset, m_position.end_offset }, move(style));
+}
+
+Glob::Glob(Position position, String text)
+ : Node(move(position))
+ , m_text(move(text))
+{
+}
+
+Glob::~Glob()
+{
+}
+
+void Execute::dump(int level) const
+{
+ Node::dump(level);
+ if (m_capture_stdout)
+ print_indented("(Capturing stdout)", level + 1);
+ m_command->dump(level + 1);
+}
+
+RefPtr<Value> Execute::run(TheExecutionInputType input_value)
+{
+ RefPtr<Job> job;
+
+ auto shell = input_value;
+ auto commands = m_command->run(input_value)->resolve_as_commands(input_value);
+ Vector<RefPtr<Job>> jobs_to_wait_for;
+
+ auto run_commands = [&](auto& commands) {
+ for (auto& command : commands) {
+#ifdef EXECUTE_DEBUG
+ dbg() << "Command " << (m_capture_stdout ? "Capturing stdout " : "") << (command.should_wait ? "" : "In background ");
+ for (auto& arg : command.argv)
+ dbg() << "argv: " << arg;
+ for (auto& redir : command.redirections) {
+ if (redir->is_path_redirection()) {
+ auto path_redir = (const PathRedirection*)redir.ptr();
+ dbg() << "redir path " << (int)path_redir->direction << " " << path_redir->path << " <-> " << path_redir->fd;
+ } else if (redir->is_fd_redirection()) {
+ auto fd_redir = (const FdRedirection*)redir.ptr();
+ dbg() << "redir fd " << fd_redir->source_fd << " -> " << fd_redir->dest_fd;
+ } else if (redir->is_close_redirection()) {
+ auto close_redir = (const CloseRedirection*)redir.ptr();
+ dbg() << "close fd " << close_redir->fd;
+ } else {
+ ASSERT_NOT_REACHED();
+ }
+ }
+#endif
+ job = shell->run_command(command);
+
+ if (command.should_wait) {
+ shell->block_on_job(job);
+ } else {
+ if (command.is_pipe_source) {
+ jobs_to_wait_for.append(job);
+ } else {
+ if (job)
+ job->set_running_in_background(true);
+ shell->take_back_stdin();
+ }
+ }
+ }
+ };
+
+ if (m_capture_stdout) {
+ int pipefd[2];
+ int rc = pipe(pipefd);
+ if (rc < 0) {
+ dbg() << "Error: cannot pipe(): " << strerror(errno);
+ return adopt(*new StringValue(""));
+ }
+ auto last_in_commands = commands.take_last();
+
+ last_in_commands.redirections.append(*new FdRedirection(STDOUT_FILENO, pipefd[1], Rewiring::Close::Destination));
+ last_in_commands.should_wait = false;
+ last_in_commands.is_pipe_source = true;
+
+ Vector<Command> commands;
+ commands.append(commands);
+ commands.append(last_in_commands);
+
+ run_commands(commands);
+
+ auto notifier = Core::Notifier::construct(pipefd[0], Core::Notifier::Read);
+ StringBuilder builder;
+
+ notifier->on_ready_to_read = [&] {
+ u8 buffer[4096];
+ size_t remaining_size = 4096;
+ for (;;) {
+ if (remaining_size == 0)
+ return;
+ auto read_size = read(pipefd[0], buffer, remaining_size);
+ if (read_size < 0) {
+ dbg() << "read() failed: " << strerror(errno);
+ return;
+ }
+ if (read_size == 0)
+ break;
+ remaining_size -= read_size;
+ }
+
+ builder.append(StringView { buffer, 4096 - remaining_size });
+ };
+
+ for (auto job : jobs_to_wait_for) {
+ shell->block_on_job(job);
+ }
+
+ notifier->on_ready_to_read = nullptr;
+
+ if (close(pipefd[0]) < 0) {
+ dbg() << "close() failed: " << strerror(errno);
+ }
+
+ return adopt(*new StringValue(builder.build(), shell->local_variable_or("IFS", "\n")));
+ }
+
+ run_commands(commands);
+ for (auto job : jobs_to_wait_for) {
+ shell->block_on_job(job);
+ }
+
+ return adopt(*new JobValue(move(job)));
+}
+
+void Execute::highlight_in_editor(Line::Editor& editor, Shell& shell, HighlightMetadata metadata)
+{
+ if (m_capture_stdout)
+ editor.stylize({ m_position.start_offset, m_position.end_offset }, { Line::Style::Foreground(Line::Style::XtermColor::Green) });
+ metadata.is_first_in_list = true;
+ m_command->highlight_in_editor(editor, shell, metadata);
+}
+
+HitTestResult Execute::hit_test_position(size_t offset)
+{
+ if (!position().contains(offset))
+ return {};
+
+ auto result = m_command->hit_test_position(offset);
+ if (!result.closest_node_with_semantic_meaning)
+ result.closest_node_with_semantic_meaning = this;
+ return result;
+}
+
+Vector<Line::CompletionSuggestion> Execute::complete_for_editor(Shell& shell, size_t offset, RefPtr<Node> matching_node)
+{
+ if (!matching_node)
+ return {};
+
+ ASSERT(matching_node->is_bareword());
+ auto corrected_offset = offset - matching_node->position().start_offset;
+ auto* node = static_cast<BarewordLiteral*>(matching_node.ptr());
+
+ if (corrected_offset > node->text().length())
+ return {};
+
+ return shell.complete_program_name(node->text(), corrected_offset);
+}
+
+Execute::Execute(Position position, RefPtr<Node> command, bool capture_stdout)
+ : Node(move(position))
+ , m_command(move(command))
+ , m_capture_stdout(capture_stdout)
+{
+}
+
+Execute::~Execute()
+{
+}
+
+void Join::dump(int level) const
+{
+ Node::dump(level);
+ m_left->dump(level + 1);
+ m_right->dump(level + 1);
+}
+
+RefPtr<Value> Join::run(TheExecutionInputType input_value)
+{
+ Command command;
+
+ auto left = m_left->run(input_value)->resolve_as_commands(input_value);
+ auto right = m_right->run(input_value)->resolve_as_commands(input_value);
+
+ auto last_in_left = left.take_last();
+ auto first_in_right = right.take_first();
+
+ command.argv.append(last_in_left.argv);
+ command.argv.append(first_in_right.argv);
+
+ command.redirections.append(last_in_left.redirections);
+ command.redirections.append(first_in_right.redirections);
+
+ command.should_wait = first_in_right.should_wait && last_in_left.should_wait;
+
+ Vector<Command> commands;
+ commands.append(left);
+ commands.append(command);
+ commands.append(right);
+
+ return adopt(*new CommandSequenceValue(move(commands)));
+}
+
+void Join::highlight_in_editor(Line::Editor& editor, Shell& shell, HighlightMetadata metadata)
+{
+ m_left->highlight_in_editor(editor, shell, metadata);
+ if (m_left->is_list() || m_left->is_command())
+ metadata.is_first_in_list = false;
+ m_right->highlight_in_editor(editor, shell, metadata);
+}
+
+HitTestResult Join::hit_test_position(size_t offset)
+{
+ if (!position().contains(offset))
+ return {};
+
+ auto result = m_left->hit_test_position(offset);
+ if (result.matching_node)
+ return result;
+ return m_right->hit_test_position(offset);
+}
+
+Join::Join(Position position, RefPtr<Node> left, RefPtr<Node> right)
+ : Node(move(position))
+ , m_left(move(left))
+ , m_right(move(right))
+{
+}
+
+Join::~Join()
+{
+}
+
+void Or::dump(int level) const
+{
+ Node::dump(level);
+ m_left->dump(level + 1);
+ m_right->dump(level + 1);
+}
+
+RefPtr<Value> Or::run(TheExecutionInputType input_value)
+{
+ auto shell = input_value;
+
+ auto left = m_left->run(input_value);
+ ASSERT(left->is_job());
+
+ auto* job_value = static_cast<JobValue*>(left.ptr());
+ const auto job = job_value->job();
+ if (!job) {
+ // Something has gone wrong, let's just pretend that the job failed.
+ return m_right->run(input_value);
+ }
+
+ shell->block_on_job(job);
+
+ if (job->exit_code() == 0)
+ return job_value;
+
+ return m_right->run(input_value);
+}
+
+void Or::highlight_in_editor(Line::Editor& editor, Shell& shell, HighlightMetadata metadata)
+{
+ m_left->highlight_in_editor(editor, shell, metadata);
+ m_right->highlight_in_editor(editor, shell, metadata);
+}
+
+HitTestResult Or::hit_test_position(size_t offset)
+{
+ if (!position().contains(offset))
+ return {};
+
+ auto result = m_left->hit_test_position(offset);
+ if (result.matching_node)
+ return result;
+ return m_right->hit_test_position(offset);
+}
+
+Or::Or(Position position, RefPtr<Node> left, RefPtr<Node> right)
+ : Node(move(position))
+ , m_left(move(left))
+ , m_right(move(right))
+{
+}
+
+Or::~Or()
+{
+}
+
+void Pipe::dump(int level) const
+{
+ Node::dump(level);
+ m_left->dump(level + 1);
+ m_right->dump(level + 1);
+}
+
+RefPtr<Value> Pipe::run(TheExecutionInputType input_value)
+{
+ int pipefd[2];
+ int rc = pipe(pipefd);
+ if (rc < 0) {
+ dbg() << "Error: cannot pipe(): " << strerror(errno);
+ return adopt(*new StringValue(""));
+ }
+ auto left = m_left->run(input_value)->resolve_as_commands(input_value);
+ auto right = m_right->run(input_value)->resolve_as_commands(input_value);
+
+ auto last_in_left = left.take_last();
+ auto first_in_right = right.take_first();
+
+ last_in_left.redirections.append(*new FdRedirection(STDOUT_FILENO, pipefd[1], Rewiring::Close::Destination));
+ last_in_left.should_wait = false;
+ last_in_left.is_pipe_source = true;
+ first_in_right.redirections.append(*new FdRedirection(STDIN_FILENO, pipefd[0], Rewiring::Close::Destination));
+
+ Vector<Command> commands;
+ commands.append(left);
+ commands.append(last_in_left);
+ commands.append(first_in_right);
+ commands.append(right);
+
+ return adopt(*new CommandSequenceValue(move(commands)));
+}
+
+void Pipe::highlight_in_editor(Line::Editor& editor, Shell& shell, HighlightMetadata metadata)
+{
+ m_left->highlight_in_editor(editor, shell, metadata);
+ m_right->highlight_in_editor(editor, shell, metadata);
+}
+
+HitTestResult Pipe::hit_test_position(size_t offset)
+{
+ if (!position().contains(offset))
+ return {};
+
+ auto result = m_left->hit_test_position(offset);
+ if (result.matching_node)
+ return result;
+ return m_right->hit_test_position(offset);
+}
+
+Pipe::Pipe(Position position, RefPtr<Node> left, RefPtr<Node> right)
+ : Node(move(position))
+ , m_left(move(left))
+ , m_right(move(right))
+{
+}
+
+Pipe::~Pipe()
+{
+}
+
+PathRedirectionNode::PathRedirectionNode(Position position, int fd, RefPtr<Node> path)
+ : Node(move(position))
+ , m_fd(fd)
+ , m_path(move(path))
+{
+}
+
+void PathRedirectionNode::highlight_in_editor(Line::Editor& editor, Shell& shell, HighlightMetadata metadata)
+{
+ editor.stylize({ m_position.start_offset, m_position.end_offset }, { Line::Style::Foreground(0x87, 0x9b, 0xcd) }); // 25% Darkened Periwinkle
+ metadata.is_first_in_list = false;
+ m_path->highlight_in_editor(editor, shell, metadata);
+ if (m_path->is_bareword()) {
+ auto path_text = m_path->run(nullptr)->resolve_as_list(nullptr);
+ ASSERT(path_text.size() == 1);
+ // Apply a URL to the path.
+ auto& position = m_path->position();
+ auto& path = path_text[0];
+ if (!path.starts_with('/'))
+ path = String::format("%s/%s", shell.cwd.characters(), path.characters());
+ auto url = URL::create_with_file_protocol(path);
+ url.set_host(shell.hostname);
+ editor.stylize({ position.start_offset, position.end_offset }, { Line::Style::Hyperlink(url.to_string()) });
+ }
+}
+
+HitTestResult PathRedirectionNode::hit_test_position(size_t offset)
+{
+ if (!position().contains(offset))
+ return {};
+
+ auto result = m_path->hit_test_position(offset);
+ if (!result.closest_node_with_semantic_meaning)
+ result.closest_node_with_semantic_meaning = this;
+ return result;
+}
+
+Vector<Line::CompletionSuggestion> PathRedirectionNode::complete_for_editor(Shell& shell, size_t offset, RefPtr<Node> matching_node)
+{
+ if (!matching_node)
+ return {};
+
+ ASSERT(matching_node->is_bareword());
+ auto corrected_offset = offset - matching_node->position().start_offset;
+ auto* node = static_cast<BarewordLiteral*>(matching_node.ptr());
+
+ if (corrected_offset > node->text().length())
+ return {};
+
+ return shell.complete_path(node->text(), corrected_offset);
+}
+
+PathRedirectionNode::~PathRedirectionNode()
+{
+}
+
+void ReadRedirection::dump(int level) const
+{
+ Node::dump(level);
+ m_path->dump(level + 1);
+ print_indented(String::format("To %d", m_fd), level + 1);
+}
+
+RefPtr<Value> ReadRedirection::run(TheExecutionInputType input_value)
+{
+ Command command;
+ auto path_segments = m_path->run(input_value)->resolve_as_list(input_value);
+ StringBuilder builder;
+ builder.join(" ", path_segments);
+
+ command.redirections.append(*new PathRedirection(builder.to_string(), m_fd, PathRedirection::Read));
+ return adopt(*new CommandValue(move(command)));
+}
+
+ReadRedirection::ReadRedirection(Position position, int fd, RefPtr<Node> path)
+ : PathRedirectionNode(move(position), fd, move(path))
+{
+}
+
+ReadRedirection::~ReadRedirection()
+{
+}
+
+void ReadWriteRedirection::dump(int level) const
+{
+ Node::dump(level);
+ m_path->dump(level + 1);
+ print_indented(String::format("To/From %d", m_fd), level + 1);
+}
+
+RefPtr<Value> ReadWriteRedirection::run(TheExecutionInputType input_value)
+{
+ Command command;
+ auto path_segments = m_path->run(input_value)->resolve_as_list(input_value);
+ StringBuilder builder;
+ builder.join(" ", path_segments);
+
+ command.redirections.append(*new PathRedirection(builder.to_string(), m_fd, PathRedirection::ReadWrite));
+ return adopt(*new CommandValue(move(command)));
+}
+
+ReadWriteRedirection::ReadWriteRedirection(Position position, int fd, RefPtr<Node> path)
+ : PathRedirectionNode(move(position), fd, move(path))
+{
+}
+
+ReadWriteRedirection::~ReadWriteRedirection()
+{
+}
+
+void Sequence::dump(int level) const
+{
+ Node::dump(level);
+ m_left->dump(level + 1);
+ m_right->dump(level + 1);
+}
+
+RefPtr<Value> Sequence::run(TheExecutionInputType input_value)
+{
+ auto left = m_left->run(input_value)->resolve_as_commands(input_value);
+ auto right = m_right->run(input_value)->resolve_as_commands(input_value);
+
+ Vector<Command> commands;
+ commands.append(left);
+ commands.append(right);
+
+ return adopt(*new CommandSequenceValue(move(commands)));
+}
+
+void Sequence::highlight_in_editor(Line::Editor& editor, Shell& shell, HighlightMetadata metadata)
+{
+ m_left->highlight_in_editor(editor, shell, metadata);
+ m_right->highlight_in_editor(editor, shell, metadata);
+}
+
+HitTestResult Sequence::hit_test_position(size_t offset)
+{
+ if (!position().contains(offset))
+ return {};
+
+ auto result = m_left->hit_test_position(offset);
+ if (result.matching_node)
+ return result;
+ return m_right->hit_test_position(offset);
+}
+
+Sequence::Sequence(Position position, RefPtr<Node> left, RefPtr<Node> right)
+ : Node(move(position))
+ , m_left(move(left))
+ , m_right(move(right))
+{
+}
+
+Sequence::~Sequence()
+{
+}
+
+void SimpleVariable::dump(int level) const
+{
+ Node::dump(level);
+ print_indented(m_name, level + 1);
+}
+
+RefPtr<Value> SimpleVariable::run(TheExecutionInputType)
+{
+ return adopt(*new SimpleVariableValue(m_name));
+}
+
+void SimpleVariable::highlight_in_editor(Line::Editor& editor, 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));
+}
+
+HitTestResult SimpleVariable::hit_test_position(size_t offset)
+{
+ if (!position().contains(offset))
+ return {};
+
+ return { this, this };
+}
+
+Vector<Line::CompletionSuggestion> SimpleVariable::complete_for_editor(Shell& shell, size_t offset, RefPtr<Node> matching_node)
+{
+ if (!matching_node)
+ return {};
+
+ if (matching_node != this)
+ return {};
+
+ auto corrected_offset = offset - matching_node->position().start_offset - 1;
+
+ if (corrected_offset > m_name.length() + 1)
+ return {};
+
+ return shell.complete_variable(m_name, corrected_offset);
+}
+
+SimpleVariable::SimpleVariable(Position position, String name)
+ : Node(move(position))
+ , m_name(move(name))
+{
+}
+
+SimpleVariable::~SimpleVariable()
+{
+}
+
+void SpecialVariable::dump(int level) const
+{
+ Node::dump(level);
+ print_indented(String { &m_name, 1 }, level + 1);
+}
+
+RefPtr<Value> SpecialVariable::run(TheExecutionInputType)
+{
+ return adopt(*new SpecialVariableValue(m_name));
+}
+
+void SpecialVariable::highlight_in_editor(Line::Editor& editor, Shell&, HighlightMetadata)
+{
+ editor.stylize({ m_position.start_offset, m_position.end_offset }, { Line::Style::Foreground(214, 112, 214) });
+}
+
+Vector<Line::CompletionSuggestion> SpecialVariable::complete_for_editor(Shell&, size_t, RefPtr<Node>)
+{
+ return {};
+}
+
+HitTestResult SpecialVariable::hit_test_position(size_t offset)
+{
+ if (!position().contains(offset))
+ return {};
+
+ return { this, this };
+}
+
+SpecialVariable::SpecialVariable(Position position, char name)
+ : Node(move(position))
+ , m_name(name)
+{
+}
+
+SpecialVariable::~SpecialVariable()
+{
+}
+
+void StringConcatenate::dump(int level) const
+{
+ Node::dump(level);
+ m_left->dump(level + 1);
+ m_right->dump(level + 1);
+}
+
+RefPtr<Value> StringConcatenate::run(TheExecutionInputType input_value)
+{
+ auto left = m_left->run(input_value)->resolve_as_list(input_value);
+ auto right = m_right->run(input_value)->resolve_as_list(input_value);
+
+ StringBuilder builder;
+ builder.join(" ", left);
+ builder.join(" ", right);
+
+ return adopt(*new StringValue(builder.to_string()));
+}
+
+void StringConcatenate::highlight_in_editor(Line::Editor& editor, Shell& shell, HighlightMetadata metadata)
+{
+ m_left->highlight_in_editor(editor, shell, metadata);
+
+ // Do not highlight '/foo/bar' in '~/foo/bar'
+ if (!(m_right->is_bareword() && m_left->is_tilde()))
+ m_right->highlight_in_editor(editor, shell, metadata);
+}
+
+HitTestResult StringConcatenate::hit_test_position(size_t offset)
+{
+ if (!position().contains(offset))
+ return {};
+
+ auto result = m_left->hit_test_position(offset);
+ if (result.matching_node)
+ return result;
+ return m_right->hit_test_position(offset);
+}
+
+StringConcatenate::StringConcatenate(Position position, RefPtr<Node> left, RefPtr<Node> right)
+ : Node(move(position))
+ , m_left(move(left))
+ , m_right(move(right))
+{
+}
+
+StringConcatenate::~StringConcatenate()
+{
+}
+
+void StringLiteral::dump(int level) const
+{
+ Node::dump(level);
+ print_indented(m_text, level + 1);
+}
+
+RefPtr<Value> StringLiteral::run(TheExecutionInputType)
+{
+ return adopt(*new StringValue(m_text));
+}
+
+void StringLiteral::highlight_in_editor(Line::Editor& editor, Shell&, HighlightMetadata metadata)
+{
+ Line::Style style { Line::Style::Foreground(Line::Style::XtermColor::Yellow) };
+ if (metadata.is_first_in_list)
+ style.unify_with({ Line::Style::Bold });
+ editor.stylize({ m_position.start_offset, m_position.end_offset }, move(style));
+}
+
+StringLiteral::StringLiteral(Position position, String text)
+ : Node(move(position))
+ , m_text(move(text))
+{
+}
+
+StringLiteral::~StringLiteral()
+{
+}
+
+void StringPartCompose::dump(int level) const
+{
+ Node::dump(level);
+ m_left->dump(level + 1);
+ m_right->dump(level + 1);
+}
+
+RefPtr<Value> StringPartCompose::run(TheExecutionInputType input_value)
+{
+ auto left = m_left->run(input_value)->resolve_as_list(input_value);
+ auto right = m_right->run(input_value)->resolve_as_list(input_value);
+
+ StringBuilder builder;
+ builder.join(" ", left);
+ builder.join(" ", right);
+
+ return adopt(*new StringValue(builder.to_string()));
+}
+
+void StringPartCompose::highlight_in_editor(Line::Editor& editor, Shell& shell, HighlightMetadata metadata)
+{
+ m_left->highlight_in_editor(editor, shell, metadata);
+ m_right->highlight_in_editor(editor, shell, metadata);
+}
+
+HitTestResult StringPartCompose::hit_test_position(size_t offset)
+{
+ if (!position().contains(offset))
+ return {};
+
+ auto result = m_left->hit_test_position(offset);
+ if (result.matching_node)
+ return result;
+ return m_right->hit_test_position(offset);
+}
+
+StringPartCompose::StringPartCompose(Position position, RefPtr<Node> left, RefPtr<Node> right)
+ : Node(move(position))
+ , m_left(move(left))
+ , m_right(move(right))
+{
+}
+
+StringPartCompose::~StringPartCompose()
+{
+}
+
+void SyntaxError::dump(int level) const
+{
+ Node::dump(level);
+}
+
+RefPtr<Value> SyntaxError::run(TheExecutionInputType)
+{
+ dbg() << "SYNTAX ERROR AAAA";
+ return adopt(*new StringValue(""));
+}
+
+void SyntaxError::highlight_in_editor(Line::Editor& editor, Shell&, HighlightMetadata)
+{
+ editor.stylize({ m_position.start_offset, m_position.end_offset }, { Line::Style::Foreground(Line::Style::XtermColor::Red), Line::Style::Bold });
+}
+
+SyntaxError::SyntaxError(Position position)
+ : Node(move(position))
+{
+}
+
+SyntaxError::~SyntaxError()
+{
+}
+
+void Tilde::dump(int level) const
+{
+ Node::dump(level);
+ print_indented(m_username, level + 1);
+}
+
+RefPtr<Value> Tilde::run(TheExecutionInputType)
+{
+ return adopt(*new TildeValue(m_username));
+}
+
+void Tilde::highlight_in_editor(Line::Editor& editor, Shell&, HighlightMetadata)
+{
+ editor.stylize({ m_position.start_offset, m_position.end_offset }, { Line::Style::Foreground(Line::Style::XtermColor::Cyan) });
+}
+
+HitTestResult Tilde::hit_test_position(size_t offset)
+{
+ if (!position().contains(offset))
+ return {};
+
+ return { this, this };
+}
+
+Vector<Line::CompletionSuggestion> Tilde::complete_for_editor(Shell&, size_t, RefPtr<Node>)
+{
+ return {};
+}
+
+String Tilde::text() const
+{
+ StringBuilder builder;
+ builder.append('~');
+ builder.append(m_username);
+ return builder.to_string();
+}
+
+Tilde::Tilde(Position position, String username)
+ : Node(move(position))
+ , m_username(move(username))
+{
+}
+
+Tilde::~Tilde()
+{
+}
+
+void WriteAppendRedirection::dump(int level) const
+{
+ Node::dump(level);
+ m_path->dump(level + 1);
+ print_indented(String::format("From %d", m_fd), level + 1);
+}
+
+RefPtr<Value> WriteAppendRedirection::run(TheExecutionInputType input_value)
+{
+ Command command;
+ auto path_segments = m_path->run(input_value)->resolve_as_list(input_value);
+ StringBuilder builder;
+ builder.join(" ", path_segments);
+
+ command.redirections.append(*new PathRedirection(builder.to_string(), m_fd, PathRedirection::WriteAppend));
+ return adopt(*new CommandValue(move(command)));
+}
+
+WriteAppendRedirection::WriteAppendRedirection(Position position, int fd, RefPtr<Node> path)
+ : PathRedirectionNode(move(position), fd, move(path))
+{
+}
+
+WriteAppendRedirection::~WriteAppendRedirection()
+{
+}
+
+void WriteRedirection::dump(int level) const
+{
+ Node::dump(level);
+ m_path->dump(level + 1);
+ print_indented(String::format("From %d", m_fd), level + 1);
+}
+
+RefPtr<Value> WriteRedirection::run(TheExecutionInputType input_value)
+{
+ Command command;
+ auto path_segments = m_path->run(input_value)->resolve_as_list(input_value);
+ StringBuilder builder;
+ builder.join(" ", path_segments);
+
+ command.redirections.append(*new PathRedirection(builder.to_string(), m_fd, PathRedirection::Write));
+ return adopt(*new CommandValue(move(command)));
+}
+
+WriteRedirection::WriteRedirection(Position position, int fd, RefPtr<Node> path)
+ : PathRedirectionNode(move(position), fd, move(path))
+{
+}
+
+WriteRedirection::~WriteRedirection()
+{
+}
+
+void VariableDeclarations::dump(int level) const
+{
+ Node::dump(level);
+ for (auto& var : m_variables) {
+ print_indented("Set", level + 1);
+ var.name->dump(level + 2);
+ var.value->dump(level + 2);
+ }
+}
+
+RefPtr<Value> VariableDeclarations::run(TheExecutionInputType input_value)
+{
+ auto shell = input_value;
+ for (auto& var : m_variables) {
+ auto name_value = var.name->run(input_value)->resolve_as_list(input_value);
+ ASSERT(name_value.size() == 1);
+ auto name = name_value[0];
+ auto value = var.value->run(input_value);
+ if (value->is_list()) {
+ auto parts = value->resolve_as_list(input_value);
+ shell->set_local_variable(name, adopt(*new ListValue(move(parts))));
+ } else {
+ auto part = value->resolve_as_list(input_value);
+ shell->set_local_variable(name, adopt(*new StringValue(part[0])));
+ }
+ }
+
+ return adopt(*new ListValue(Vector<String> {}));
+}
+
+void VariableDeclarations::highlight_in_editor(Line::Editor& editor, Shell& shell, HighlightMetadata metadata)
+{
+ metadata.is_first_in_list = false;
+ for (auto& var : m_variables) {
+ var.name->highlight_in_editor(editor, shell, metadata);
+ // Highlight the '='.
+ editor.stylize({ var.name->position().end_offset - 1, var.name->position().end_offset }, { Line::Style::Foreground(Line::Style::XtermColor::Blue) });
+ var.value->highlight_in_editor(editor, shell, metadata);
+ }
+}
+
+HitTestResult VariableDeclarations::hit_test_position(size_t offset)
+{
+ if (!position().contains(offset))
+ return {};
+
+ for (auto decl : m_variables) {
+ auto result = decl.value->hit_test_position(offset);
+ if (result.matching_node)
+ return result;
+ }
+
+ return { nullptr, nullptr };
+}
+
+VariableDeclarations::VariableDeclarations(Position position, Vector<Variable> variables)
+ : Node(move(position))
+ , m_variables(move(variables))
+{
+}
+
+VariableDeclarations::~VariableDeclarations()
+{
+}
+
+Value::~Value()
+{
+}
+Vector<AST::Command> Value::resolve_as_commands(TheExecutionInputType input_value)
+{
+ Command command;
+ command.argv = resolve_as_list(input_value);
+ return { command };
+}
+
+ListValue::~ListValue()
+{
+}
+
+ListValue::ListValue(Vector<String> values)
+{
+ m_contained_values.ensure_capacity(values.size());
+ for (auto& str : values)
+ m_contained_values.append(adopt(*new StringValue(move(str))));
+}
+
+Vector<String> ListValue::resolve_as_list(TheExecutionInputType input_value)
+{
+ Vector<String> values;
+ for (auto& value : m_contained_values)
+ values.append(value->resolve_as_list(input_value));
+
+ return values;
+}
+
+CommandValue::~CommandValue()
+{
+}
+
+CommandSequenceValue::~CommandSequenceValue()
+{
+}
+
+Vector<String> CommandSequenceValue::resolve_as_list(TheExecutionInputType)
+{
+ // TODO: Somehow raise an "error".
+ return {};
+}
+
+Vector<Command> CommandSequenceValue::resolve_as_commands(TheExecutionInputType)
+{
+ return m_contained_values;
+}
+
+Vector<String> CommandValue::resolve_as_list(TheExecutionInputType)
+{
+ // TODO: Somehow raise an "error".
+ return {};
+}
+
+Vector<Command> CommandValue::resolve_as_commands(TheExecutionInputType)
+{
+ return { m_command };
+}
+
+JobValue::~JobValue()
+{
+}
+
+StringValue::~StringValue()
+{
+}
+Vector<String> StringValue::resolve_as_list(TheExecutionInputType)
+{
+ if (is_list()) {
+ auto parts = StringView(m_string).split_view(m_split);
+ Vector<String> result;
+ result.ensure_capacity(parts.size());
+ for (auto& part : parts)
+ result.append(part);
+ return result;
+ }
+
+ return { m_string };
+}
+
+GlobValue::~GlobValue()
+{
+}
+Vector<String> GlobValue::resolve_as_list(TheExecutionInputType input_value)
+{
+ auto shell = input_value;
+ return shell->expand_globs(m_glob, shell->cwd);
+}
+
+SimpleVariableValue::~SimpleVariableValue()
+{
+}
+Vector<String> SimpleVariableValue::resolve_as_list(TheExecutionInputType input_value)
+{
+ auto shell = input_value;
+
+ if (auto value = shell->lookup_local_variable(m_name))
+ return value->resolve_as_list(input_value);
+
+ char* env_value = getenv(m_name.characters());
+ if (env_value == nullptr)
+ return { "" };
+
+ 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;
+}
+
+SpecialVariableValue::~SpecialVariableValue()
+{
+}
+Vector<String> SpecialVariableValue::resolve_as_list(TheExecutionInputType input_value)
+{
+ auto shell = input_value;
+ switch (m_name) {
+ case '?':
+ return { String::number(shell->last_return_code) };
+ case '$':
+ return { String::number(getpid()) };
+ default:
+ return { "" };
+ }
+}
+
+TildeValue::~TildeValue()
+{
+}
+Vector<String> TildeValue::resolve_as_list(TheExecutionInputType input_value)
+{
+ auto shell = input_value;
+ StringBuilder builder;
+ builder.append("~");
+ builder.append(m_username);
+ return { shell->expand_tilde(builder.to_string()) };
+}
+
+Result<Rewiring, String> CloseRedirection::apply() const
+{
+ auto rc = close(fd);
+ if (rc < 0)
+ return String { strerror(errno) };
+
+ return String {};
+}
+
+CloseRedirection::~CloseRedirection()
+{
+}
+
+Result<Rewiring, String> PathRedirection::apply() const
+{
+ auto check_fd_and_return = [my_fd = this->fd](int fd, const String& path) -> Result<Rewiring, String> {
+ if (fd < 0) {
+ String error = strerror(errno);
+ dbg() << "open() failed for '" << path << "' with " << error;
+ return error;
+ }
+ return Rewiring { my_fd, fd };
+ };
+ switch (direction) {
+ case AST::PathRedirection::WriteAppend:
+ return check_fd_and_return(open(path.characters(), O_WRONLY | O_CREAT | O_APPEND, 0666), path);
+
+ case AST::PathRedirection::Write:
+ return check_fd_and_return(open(path.characters(), O_WRONLY | O_CREAT | O_TRUNC, 0666), path);
+
+ case AST::PathRedirection::Read:
+ return check_fd_and_return(open(path.characters(), O_RDONLY), path);
+
+ case AST::PathRedirection::ReadWrite:
+ return check_fd_and_return(open(path.characters(), O_RDWR | O_CREAT, 0666), path);
+ }
+
+ ASSERT_NOT_REACHED();
+}
+
+PathRedirection::~PathRedirection()
+{
+}
+
+FdRedirection::~FdRedirection()
+{
+}
+
+Redirection::~Redirection()
+{
+}
+
+}
diff --git a/Shell/AST.h b/Shell/AST.h
new file mode 100644
index 0000000000..1cd7f8e4fa
--- /dev/null
+++ b/Shell/AST.h
@@ -0,0 +1,815 @@
+/*
+ * Copyright (c) 2020, the SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "Forward.h"
+#include "Job.h"
+#include <AK/NonnullRefPtr.h>
+#include <AK/RefCounted.h>
+#include <AK/RefPtr.h>
+#include <AK/String.h>
+#include <AK/Types.h>
+#include <AK/Vector.h>
+#include <LibLine/Editor.h>
+
+using TheExecutionInputType = RefPtr<Shell>;
+
+namespace AST {
+
+struct HighlightMetadata {
+ bool is_first_in_list { true };
+};
+
+struct Position {
+ size_t start_offset { 0 };
+ size_t end_offset { 0 };
+ bool contains(size_t offset) const { return start_offset <= offset && offset <= end_offset; }
+};
+
+struct Rewiring {
+ int source_fd { -1 };
+ int dest_fd { -1 };
+ enum class Close {
+ None,
+ Source,
+ Destination,
+ } must_be_closed { Close::None };
+};
+
+struct Redirection : public RefCounted<Redirection> {
+ virtual Result<Rewiring, String> apply() const = 0;
+ virtual ~Redirection();
+ virtual bool is_path_redirection() const { return false; }
+ virtual bool is_fd_redirection() const { return false; }
+ virtual bool is_close_redirection() const { return false; }
+};
+
+struct CloseRedirection : public Redirection {
+ int fd { -1 };
+
+ virtual Result<Rewiring, String> apply() const override;
+ virtual ~CloseRedirection();
+ CloseRedirection(int fd)
+ : fd(fd)
+ {
+ }
+
+private:
+ virtual bool is_close_redirection() const override { return true; }
+};
+
+struct PathRedirection : public Redirection {
+ String path;
+ int fd { -1 };
+ enum {
+ Read,
+ Write,
+ WriteAppend,
+ ReadWrite,
+ } direction { Read };
+
+ virtual Result<Rewiring, String> apply() const override;
+ virtual ~PathRedirection();
+ PathRedirection(String path, int fd, decltype(direction) direction)
+ : path(move(path))
+ , fd(fd)
+ , direction(direction)
+ {
+ }
+
+private:
+ virtual bool is_path_redirection() const override { return true; }
+};
+
+struct FdRedirection : public Redirection
+ , public Rewiring {
+
+ virtual Result<Rewiring, String> apply() const override { return *this; }
+ virtual ~FdRedirection();
+ FdRedirection(int source, int dest, Rewiring::Close close)
+ : Rewiring({ source, dest, close })
+ {
+ }
+
+private:
+ virtual bool is_fd_redirection() const override { return true; }
+};
+
+struct Command {
+ Vector<String> argv;
+ Vector<NonnullRefPtr<Redirection>> redirections;
+ bool should_wait { true };
+ bool is_pipe_source { false };
+};
+
+struct HitTestResult {
+ RefPtr<Node> matching_node;
+ RefPtr<Node> closest_node_with_semantic_meaning; // This is used if matching_node is a bareword
+};
+
+class Value : public RefCounted<Value> {
+public:
+ virtual Vector<String> resolve_as_list(TheExecutionInputType) = 0;
+ virtual Vector<Command> resolve_as_commands(TheExecutionInputType);
+ virtual ~Value();
+ virtual bool is_command() const { return false; }
+ virtual bool is_glob() const { return false; }
+ virtual bool is_job() const { return false; }
+ virtual bool is_list() const { return false; }
+ virtual bool is_string() const { return false; }
+};
+
+class CommandValue final : public Value {
+public:
+ virtual Vector<String> resolve_as_list(TheExecutionInputType) override;
+ virtual Vector<Command> resolve_as_commands(TheExecutionInputType) override;
+ virtual ~CommandValue();
+ virtual bool is_command() const override { return true; }
+ CommandValue(Command command)
+ : m_command(move(command))
+ {
+ }
+
+ CommandValue(Vector<String> argv)
+ : m_command({ move(argv), {}, true, false })
+ {
+ }
+
+private:
+ Command m_command;
+};
+
+class CommandSequenceValue final : public Value {
+public:
+ virtual Vector<String> resolve_as_list(TheExecutionInputType) override;
+ virtual Vector<Command> resolve_as_commands(TheExecutionInputType) override;
+ virtual ~CommandSequenceValue();
+ virtual bool is_command() const override { return true; }
+ CommandSequenceValue(Vector<Command> commands)
+ : m_contained_values(move(commands))
+ {
+ }
+
+private:
+ Vector<Command> m_contained_values;
+};
+
+class JobValue final : public Value {
+public:
+ virtual Vector<String> resolve_as_list(TheExecutionInputType) override { ASSERT_NOT_REACHED(); }
+ virtual Vector<Command> resolve_as_commands(TheExecutionInputType) override { ASSERT_NOT_REACHED(); }
+ virtual ~JobValue();
+ virtual bool is_job() const override { return true; }
+ JobValue(RefPtr<Job> job)
+ : m_job(move(job))
+ {
+ }
+
+ const RefPtr<Job> job() const { return m_job; }
+
+private:
+ RefPtr<Job> m_job;
+};
+
+class ListValue final : public Value {
+public:
+ virtual Vector<String> resolve_as_list(TheExecutionInputType) override;
+ virtual ~ListValue();
+ virtual bool is_list() const override { return true; }
+ ListValue(Vector<String> values);
+ ListValue(Vector<RefPtr<Value>> values)
+ : m_contained_values(move(values))
+ {
+ }
+
+private:
+ Vector<RefPtr<Value>> m_contained_values;
+};
+
+class StringValue final : public Value {
+public:
+ virtual Vector<String> resolve_as_list(TheExecutionInputType) override;
+ virtual ~StringValue();
+ virtual bool is_string() const override { return m_split.is_null(); }
+ virtual bool is_list() const override { return !m_split.is_null(); }
+ StringValue(String string, String split_by = {})
+ : m_string(string)
+ , m_split(move(split_by))
+ {
+ }
+
+private:
+ String m_string;
+ String m_split;
+};
+
+class GlobValue final : public Value {
+public:
+ virtual Vector<String> resolve_as_list(TheExecutionInputType) override;
+ virtual ~GlobValue();
+ virtual bool is_glob() const override { return true; }
+ GlobValue(String glob)
+ : m_glob(glob)
+ {
+ }
+
+private:
+ String m_glob;
+};
+
+class SimpleVariableValue final : public Value {
+public:
+ virtual Vector<String> resolve_as_list(TheExecutionInputType) override;
+ virtual ~SimpleVariableValue();
+ // FIXME: Should override is_list and is_string,
+ // as it might have different types of values.
+ SimpleVariableValue(String name)
+ : m_name(name)
+ {
+ }
+
+private:
+ String m_name;
+};
+
+class SpecialVariableValue final : public Value {
+public:
+ virtual Vector<String> resolve_as_list(TheExecutionInputType) override;
+ virtual ~SpecialVariableValue();
+ // FIXME: Should override is_list and is_string,
+ // as it might have different types of values.
+ SpecialVariableValue(char name)
+ : m_name(name)
+ {
+ }
+
+private:
+ char m_name { -1 };
+};
+
+class TildeValue final : public Value {
+public:
+ virtual Vector<String> resolve_as_list(TheExecutionInputType) override;
+ virtual ~TildeValue();
+ virtual bool is_string() const override { return true; }
+ TildeValue(String name)
+ : m_username(name)
+ {
+ }
+
+private:
+ String m_username;
+};
+
+class Node : public RefCounted<Node> {
+public:
+ virtual void dump(int level) = const 0;
+ virtual RefPtr<Value> run(TheExecutionInputType) = 0;
+ virtual void highlight_in_editor(Line::Editor&, Shell&, HighlightMetadata = {}) = 0;
+ virtual Vector<Line::CompletionSuggestion> complete_for_editor(Shell&, size_t, RefPtr<Node> matching_node);
+ Vector<Line::CompletionSuggestion> complete_for_editor(Shell& shell, size_t offset);
+ virtual HitTestResult hit_test_position(size_t offset)
+ {
+ if (m_position.contains(offset))
+ return { this, nullptr };
+ return { nullptr, nullptr };
+ }
+ virtual String class_name() const { return "Node"; }
+ Node(Position);
+ virtual ~Node();
+
+ virtual bool is_bareword() const { return false; }
+ virtual bool is_command() const { return false; }
+ virtual bool is_execute() const { return false; }
+ virtual bool is_glob() const { return false; }
+ virtual bool is_tilde() const { return false; }
+ virtual bool is_variable_decls() const { return false; }
+
+ virtual bool is_list() const { return false; }
+
+ const Position& position() const { return m_position; }
+
+protected:
+ Position m_position;
+};
+
+class PathRedirectionNode : public Node {
+public:
+ PathRedirectionNode(Position, int, RefPtr<Node>);
+ virtual ~PathRedirectionNode();
+ virtual void highlight_in_editor(Line::Editor&, Shell&, HighlightMetadata = {}) override;
+ virtual Vector<Line::CompletionSuggestion> complete_for_editor(Shell&, size_t, RefPtr<Node> matching_node) override;
+ virtual HitTestResult hit_test_position(size_t offset) override;
+ virtual bool is_command() const override { return true; }
+ virtual bool is_list() const override { return true; }
+
+protected:
+ int m_fd { -1 };
+ RefPtr<Node> m_path;
+};
+
+class And final : public Node {
+public:
+ And(Position, RefPtr<Node>, RefPtr<Node>);
+ virtual ~And();
+
+private:
+ virtual void dump(int level) const override;
+ virtual RefPtr<Value> run(TheExecutionInputType) override;
+ virtual void highlight_in_editor(Line::Editor&, Shell&, HighlightMetadata = {}) override;
+ virtual HitTestResult hit_test_position(size_t) override;
+ virtual String class_name() const override { return "And"; }
+
+ RefPtr<Node> m_left;
+ RefPtr<Node> m_right;
+};
+
+class ListConcatenate final : public Node {
+public:
+ ListConcatenate(Position, RefPtr<Node>, RefPtr<Node>);
+ virtual ~ListConcatenate();
+
+private:
+ virtual void dump(int level) const override;
+ virtual void highlight_in_editor(Line::Editor&, Shell&, HighlightMetadata = {}) override;
+ virtual HitTestResult hit_test_position(size_t) override;
+ virtual String class_name() const override { return "ListConcatenate"; }
+ virtual bool is_list() const override { return true; }
+
+ RefPtr<Node> m_element;
+ RefPtr<Node> m_list;
+};
+
+class Background final : public Node {
+public:
+ Background(Position, RefPtr<Node>);
+ virtual ~Background();
+
+private:
+ virtual void dump(int level) const override;
+ virtual RefPtr<Value> run(TheExecutionInputType) override;
+ virtual void highlight_in_editor(Line::Editor&, Shell&, HighlightMetadata = {}) override;
+ virtual HitTestResult hit_test_position(size_t) override;
+ virtual String class_name() const override { return "Background"; }
+
+ RefPtr<Node> m_command;
+};
+
+class BarewordLiteral final : public Node {
+public:
+ BarewordLiteral(Position, String);
+ virtual ~BarewordLiteral();
+ const String& text() const { return m_text; }
+
+private:
+ virtual void dump(int level) const override;
+ virtual RefPtr<Value> run(TheExecutionInputType) override;
+ virtual void highlight_in_editor(Line::Editor&, Shell&, HighlightMetadata = {}) override;
+ virtual String class_name() const override { return "BarewordLiteral"; }
+ virtual bool is_bareword() const override { return true; }
+
+ String m_text;
+};
+
+class CastToCommand final : public Node {
+public:
+ CastToCommand(Position, RefPtr<Node>);
+ virtual ~CastToCommand();
+
+private:
+ virtual void dump(int level) const override;
+ virtual RefPtr<Value> run(TheExecutionInputType) override;
+ virtual void highlight_in_editor(Line::Editor&, Shell&, HighlightMetadata = {}) override;
+ virtual HitTestResult hit_test_position(size_t) override;
+ virtual Vector<Line::CompletionSuggestion> complete_for_editor(Shell&, size_t, RefPtr<Node> matching_node) override;
+ virtual String class_name() const override { return "CastToCommand"; }
+ virtual bool is_command() const override { return true; }
+ virtual bool is_list() const override { return true; }
+
+ RefPtr<Node> m_inner;
+};
+
+class CastToList final : public Node {
+public:
+ CastToList(Position, RefPtr<Node>);
+ virtual ~CastToList();
+
+private:
+ virtual void dump(int level) const override;
+ virtual RefPtr<Value> run(TheExecutionInputType) override;
+ virtual void highlight_in_editor(Line::Editor&, Shell&, HighlightMetadata = {}) override;
+ virtual HitTestResult hit_test_position(size_t) override;
+ virtual String class_name() const override { return "CastToList"; }
+ virtual bool is_list() const override { return true; }
+
+ RefPtr<Node> m_inner;
+};
+
+class CloseFdRedirection final : public Node {
+public:
+ CloseFdRedirection(Position, int);
+ virtual ~CloseFdRedirection();
+
+private:
+ virtual void dump(int level) const override;
+ virtual RefPtr<Value> run(TheExecutionInputType) override;
+ virtual void highlight_in_editor(Line::Editor&, Shell&, HighlightMetadata = {}) override;
+ virtual String class_name() const override { return "CloseFdRedirection"; }
+ virtual bool is_command() const override { return true; }
+
+ int m_fd { -1 };
+};
+
+class CommandLiteral final : public Node {
+public:
+ CommandLiteral(Position, Command);
+ virtual ~CommandLiteral();
+
+private:
+ virtual void dump(int level) const override;
+ virtual RefPtr<Value> run(TheExecutionInputType) override;
+ virtual void highlight_in_editor(Line::Editor&, Shell&, HighlightMetadata = {}) override { ASSERT_NOT_REACHED(); }
+ virtual String class_name() const override { return "CommandLiteral"; }
+ virtual bool is_command() const override { return true; }
+ virtual bool is_list() const override { return true; }
+
+ Command m_command;
+};
+
+class Comment : public Node {
+public:
+ Comment(Position, String);
+ virtual ~Comment();
+ const String& text() const { return m_text; }
+
+private:
+ virtual void dump(int level) const override;
+ virtual RefPtr<Value> run(TheExecutionInputType) override;
+ virtual void highlight_in_editor(Line::Editor&, Shell&, HighlightMetadata = {}) override;
+ virtual String class_name() const override { return "Comment"; }
+
+ String m_text;
+};
+
+class DynamicEvaluate final : public Node {
+public:
+ DynamicEvaluate(Position, RefPtr<Node>);
+ virtual ~DynamicEvaluate();
+
+private:
+ virtual void dump(int level) const override;
+ virtual RefPtr<Value> run(TheExecutionInputType) override;
+ virtual void highlight_in_editor(Line::Editor&, Shell&, HighlightMetadata = {}) override;
+ virtual HitTestResult hit_test_position(size_t) override;
+ virtual String class_name() const override { return "DynamicEvaluate"; }
+
+ virtual bool is_bareword() const override { return m_inner->is_bareword(); }
+ virtual bool is_command() const override { return is_list(); }
+ virtual bool is_execute() const override { return true; }
+ virtual bool is_glob() const override { return m_inner->is_glob(); }
+ virtual bool is_list() const override
+ {
+ return m_inner->is_list() || m_inner->is_command() || m_inner->is_glob(); // Anything that generates a list.
+ }
+
+ RefPtr<Node> m_inner;
+};
+
+class DoubleQuotedString final : public Node {
+public:
+ DoubleQuotedString(Position, RefPtr<Node>);
+ virtual ~DoubleQuotedString();
+
+private:
+ virtual void dump(int level) const override;
+ virtual RefPtr<Value> run(TheExecutionInputType) override;
+ virtual void highlight_in_editor(Line::Editor&, Shell&, HighlightMetadata = {}) override;
+ virtual HitTestResult hit_test_position(size_t) override;
+ virtual String class_name() const override { return "DoubleQuotedString"; }
+
+ RefPtr<Node> m_inner;
+};
+
+class Fd2FdRedirection final : public Node {
+public:
+ Fd2FdRedirection(Position, int, int);
+ virtual ~Fd2FdRedirection();
+
+private:
+ virtual void dump(int level) const override;
+ virtual RefPtr<Value> run(TheExecutionInputType) override;
+ virtual void highlight_in_editor(Line::Editor&, Shell&, HighlightMetadata = {}) override;
+ virtual String class_name() const override { return "Fd2FdRedirection"; }
+ virtual bool is_command() const override { return true; }
+
+ int source_fd { -1 };
+ int dest_fd { -1 };
+};
+
+class Glob final : public Node {
+public:
+ Glob(Position, String);
+ virtual ~Glob();
+ const String& text() const { return m_text; }
+
+private:
+ virtual void dump(int level) const override;
+ virtual RefPtr<Value> run(TheExecutionInputType) override;
+ virtual void highlight_in_editor(Line::Editor&, Shell&, HighlightMetadata = {}) override;
+ virtual String class_name() const override { return "Glob"; }
+ virtual bool is_glob() const override { return true; }
+ virtual bool is_list() const override { return true; }
+
+ String m_text;
+};
+
+class Execute final : public Node {
+public:
+ Execute(Position, RefPtr<Node>, bool capture_stdout = false);
+ virtual ~Execute();
+ void capture_stdout() { m_capture_stdout = true; }
+ RefPtr<Node> command() { return m_command; }
+
+private:
+ virtual void dump(int level) const override;
+ virtual RefPtr<Value> run(TheExecutionInputType) override;
+ virtual void highlight_in_editor(Line::Editor&, Shell&, HighlightMetadata = {}) override;
+ virtual HitTestResult hit_test_position(size_t) override;
+ virtual Vector<Line::CompletionSuggestion> complete_for_editor(Shell&, size_t, RefPtr<Node> matching_node) override;
+ virtual String class_name() const override { return "Execute"; }
+ virtual bool is_execute() const override { return true; }
+
+ RefPtr<Node> m_command;
+ bool m_capture_stdout { false };
+};
+
+class Join final : public Node {
+public:
+ Join(Position, RefPtr<Node>, RefPtr<Node>);
+ virtual ~Join();
+
+private:
+ virtual void dump(int level) const override;
+ virtual RefPtr<Value> run(TheExecutionInputType) override;
+ virtual void highlight_in_editor(Line::Editor&, Shell&, HighlightMetadata = {}) override;
+ virtual HitTestResult hit_test_position(size_t) override;
+ virtual String class_name() const override { return "Join"; }
+ virtual bool is_command() const override { return true; }
+ virtual bool is_list() const override { return true; }
+
+ RefPtr<Node> m_left;
+ RefPtr<Node> m_right;
+};
+
+class Or final : public Node {
+public:
+ Or(Position, RefPtr<Node>, RefPtr<Node>);
+ virtual ~Or();
+
+private:
+ virtual void dump(int level) const override;
+ virtual RefPtr<Value> run(TheExecutionInputType) override;
+ virtual void highlight_in_editor(Line::Editor&, Shell&, HighlightMetadata = {}) override;
+ virtual HitTestResult hit_test_position(size_t) override;
+ virtual String class_name() const override { return "Or"; }
+ virtual bool is_list() const override { return true; }
+
+ RefPtr<Node> m_left;
+ RefPtr<Node> m_right;
+};
+
+class Pipe final : public Node {
+public:
+ Pipe(Position, RefPtr<Node>, RefPtr<Node>);
+ virtual ~Pipe();
+
+private:
+ virtual void dump(int level) const override;
+ virtual RefPtr<Value> run(TheExecutionInputType) override;
+ virtual void highlight_in_editor(Line::Editor&, Shell&, HighlightMetadata = {}) override;
+ virtual HitTestResult hit_test_position(size_t) override;
+ virtual String class_name() const override { return "Pipe"; }
+ virtual bool is_list() const override { return true; }
+
+ RefPtr<Node> m_left;
+ RefPtr<Node> m_right;
+};
+
+class ReadRedirection final : public PathRedirectionNode {
+public:
+ ReadRedirection(Position, int, RefPtr<Node>);
+ virtual ~ReadRedirection();
+
+private:
+ virtual void dump(int level) const override;
+ virtual RefPtr<Value> run(TheExecutionInputType) override;
+ virtual String class_name() const override { return "ReadRedirection"; }
+};
+
+class ReadWriteRedirection final : public PathRedirectionNode {
+public:
+ ReadWriteRedirection(Position, int, RefPtr<Node>);
+ virtual ~ReadWriteRedirection();
+
+private:
+ virtual void dump(int level) const override;
+ virtual RefPtr<Value> run(TheExecutionInputType) override;
+ virtual String class_name() const override { return "ReadWriteRedirection"; }
+};
+
+class Sequence final : public Node {
+public:
+ Sequence(Position, RefPtr<Node>, RefPtr<Node>);
+ virtual ~Sequence();
+
+private:
+ virtual void dump(int level) const override;
+ virtual RefPtr<Value> run(TheExecutionInputType) override;
+ virtual void highlight_in_editor(Line::Editor&, Shell&, HighlightMetadata = {}) override;
+ virtual HitTestResult hit_test_position(size_t) override;
+ virtual String class_name() const override { return "Sequence"; }
+ virtual bool is_list() const override { return true; }
+
+ RefPtr<Node> m_left;
+ RefPtr<Node> m_right;
+};
+
+class SimpleVariable final : public Node {
+public:
+ SimpleVariable(Position, String);
+ virtual ~SimpleVariable();
+
+private:
+ virtual void dump(int level) const override;
+ virtual RefPtr<Value> run(TheExecutionInputType) override;
+ virtual void highlight_in_editor(Line::Editor&, Shell&, HighlightMetadata = {}) override;
+ virtual Vector<Line::CompletionSuggestion> complete_for_editor(Shell&, size_t, RefPtr<Node> matching_node) override;
+ virtual HitTestResult hit_test_position(size_t) override;
+ virtual String class_name() const override { return "SimpleVariable"; }
+
+ String m_name;
+};
+
+class SpecialVariable final : public Node {
+public:
+ SpecialVariable(Position, char);
+ virtual ~SpecialVariable();
+
+private:
+ virtual void dump(int level) const override;
+ virtual RefPtr<Value> run(TheExecutionInputType) override;
+ virtual void highlight_in_editor(Line::Editor&, Shell&, HighlightMetadata = {}) override;
+ virtual Vector<Line::CompletionSuggestion> complete_for_editor(Shell&, size_t, RefPtr<Node> matching_node) override;
+ virtual HitTestResult hit_test_position(size_t) override;
+ virtual String class_name() const override { return "SpecialVariable"; }
+
+ char m_name { -1 };
+};
+
+class StringConcatenate final : public Node {
+public:
+ StringConcatenate(Position, RefPtr<Node>, RefPtr<Node>);
+ virtual ~StringConcatenate();
+
+private:
+ virtual void dump(int level) const override;
+ virtual RefPtr<Value> run(TheExecutionInputType) override;
+ virtual void highlight_in_editor(Line::Editor&, Shell&, HighlightMetadata = {}) override;
+ virtual HitTestResult hit_test_position(size_t) override;
+ virtual String class_name() const override { return "StringConcatenate"; }
+
+ RefPtr<Node> m_left;
+ RefPtr<Node> m_right;
+};
+
+class StringLiteral final : public Node {
+public:
+ StringLiteral(Position, String);
+ virtual ~StringLiteral();
+ const String& text() const { return m_text; }
+
+private:
+ virtual void dump(int level) const override;
+ virtual RefPtr<Value> run(TheExecutionInputType) override;
+ virtual void highlight_in_editor(Line::Editor&, Shell&, HighlightMetadata = {}) override;
+ virtual String class_name() const override { return "StringLiteral"; }
+
+ String m_text;
+};
+
+class StringPartCompose final : public Node {
+public:
+ StringPartCompose(Position, RefPtr<Node>, RefPtr<Node>);
+ virtual ~StringPartCompose();
+
+private:
+ virtual void dump(int level) const override;
+ virtual RefPtr<Value> run(TheExecutionInputType) override;
+ virtual void highlight_in_editor(Line::Editor&, Shell&, HighlightMetadata = {}) override;
+ virtual HitTestResult hit_test_position(size_t) override;
+ virtual String class_name() const override { return "StringPartCompose"; }
+
+ RefPtr<Node> m_left;
+ RefPtr<Node> m_right;
+};
+
+class SyntaxError final : public Node {
+public:
+ SyntaxError(Position);
+ virtual ~SyntaxError();
+
+private:
+ virtual void dump(int level) const override;
+ virtual RefPtr<Value> run(TheExecutionInputType) override;
+ virtual void highlight_in_editor(Line::Editor&, Shell&, HighlightMetadata = {}) override;
+ virtual HitTestResult hit_test_position(size_t) override { return { nullptr, nullptr }; }
+ virtual String class_name() const override { return "SyntaxError"; }
+};
+
+class Tilde final : public Node {
+public:
+ Tilde(Position, String);
+ virtual ~Tilde();
+ String text() const;
+
+private:
+ virtual void dump(int level) const override;
+ virtual RefPtr<Value> run(TheExecutionInputType) override;
+ virtual void highlight_in_editor(Line::Editor&, Shell&, HighlightMetadata = {}) override;
+ virtual Vector<Line::CompletionSuggestion> complete_for_editor(Shell&, size_t, RefPtr<Node> matching_node) override;
+ virtual HitTestResult hit_test_position(size_t) override;
+ virtual String class_name() const override { return "Tilde"; }
+ virtual bool is_tilde() const override { return true; }
+
+ String m_username;
+};
+
+class VariableDeclarations final : public Node {
+public:
+ struct Variable {
+ RefPtr<Node> name;
+ RefPtr<Node> value;
+ };
+ VariableDeclarations(Position, Vector<Variable> variables);
+ virtual ~VariableDeclarations();
+
+ const Vector<Variable>& variables() const { return m_variables; }
+
+private:
+ virtual void dump(int level) const override;
+ virtual RefPtr<Value> run(TheExecutionInputType) override;
+ virtual void highlight_in_editor(Line::Editor&, Shell&, HighlightMetadata = {}) override;
+ virtual HitTestResult hit_test_position(size_t) override;
+ virtual String class_name() const override { return "VariableDeclarations"; }
+ virtual bool is_variable_decls() const override { return true; }
+
+ Vector<Variable> m_variables;
+};
+
+class WriteAppendRedirection final : public PathRedirectionNode {
+public:
+ WriteAppendRedirection(Position, int, RefPtr<Node>);
+ virtual ~WriteAppendRedirection();
+
+private:
+ virtual void dump(int level) const override;
+ virtual RefPtr<Value> run(TheExecutionInputType) override;
+ virtual String class_name() const override { return "WriteAppendRedirection"; }
+};
+
+class WriteRedirection final : public PathRedirectionNode {
+public:
+ WriteRedirection(Position, int, RefPtr<Node>);
+ virtual ~WriteRedirection();
+
+private:
+ virtual void dump(int level) const override;
+ virtual RefPtr<Value> run(TheExecutionInputType) override;
+ virtual String class_name() const override { return "WriteRedirection"; }
+};
+
+}
diff --git a/Shell/Builtin.cpp b/Shell/Builtin.cpp
new file mode 100644
index 0000000000..14d7aece5c
--- /dev/null
+++ b/Shell/Builtin.cpp
@@ -0,0 +1,701 @@
+/*
+ * Copyright (c) 2020, The SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Shell.h"
+#include <AK/LexicalPath.h>
+#include <LibCore/ArgsParser.h>
+#include <LibCore/File.h>
+#include <signal.h>
+#include <sys/wait.h>
+
+extern RefPtr<Line::Editor> editor;
+
+int Shell::builtin_bg(int argc, const char** argv)
+{
+ int job_id = -1;
+
+ Core::ArgsParser parser;
+ parser.add_positional_argument(job_id, "Job ID to run in background", "job-id", Core::ArgsParser::Required::No);
+
+ if (!parser.parse(argc, const_cast<char**>(argv), false))
+ return 1;
+
+ if (job_id == -1 && !jobs.is_empty())
+ job_id = find_last_job_id();
+
+ auto* job = const_cast<Job*>(find_job(job_id));
+
+ if (!job) {
+ if (job_id == -1) {
+ fprintf(stderr, "bg: no current job\n");
+ } else {
+ fprintf(stderr, "bg: job with id %d not found\n", job_id);
+ }
+ return 1;
+ }
+
+ job->set_running_in_background(true);
+
+ dbg() << "Resuming " << job->pid() << " (" << job->cmd() << ")";
+ fprintf(stderr, "Resuming job %llu - %s\n", job->job_id(), job->cmd().characters());
+
+ if (killpg(job->pgid(), SIGCONT) < 0) {
+ perror("killpg");
+ return 1;
+ }
+
+ return 0;
+}
+
+int Shell::builtin_cd(int argc, const char** argv)
+{
+ const char* arg_path = nullptr;
+
+ Core::ArgsParser parser;
+ parser.add_positional_argument(arg_path, "Path to change to", "path", Core::ArgsParser::Required::No);
+
+ if (!parser.parse(argc, const_cast<char**>(argv), false))
+ return 1;
+
+ String new_path;
+
+ if (!arg_path) {
+ new_path = home;
+ if (cd_history.is_empty() || cd_history.last() != home)
+ cd_history.enqueue(home);
+ } else {
+ if (cd_history.is_empty() || cd_history.last() != arg_path)
+ cd_history.enqueue(arg_path);
+ if (strcmp(arg_path, "-") == 0) {
+ char* oldpwd = getenv("OLDPWD");
+ if (oldpwd == nullptr)
+ return 1;
+ new_path = oldpwd;
+ } else if (arg_path[0] == '/') {
+ new_path = argv[1];
+ } else {
+ StringBuilder builder;
+ builder.append(cwd);
+ builder.append('/');
+ builder.append(arg_path);
+ new_path = builder.to_string();
+ }
+ }
+
+ auto real_path = Core::File::real_path_for(new_path);
+ if (real_path.is_empty()) {
+ fprintf(stderr, "Invalid path '%s'\n", new_path.characters());
+ return 1;
+ }
+ const char* path = real_path.characters();
+
+ int rc = chdir(path);
+ if (rc < 0) {
+ if (errno == ENOTDIR) {
+ fprintf(stderr, "Not a directory: %s\n", path);
+ } else {
+ fprintf(stderr, "chdir(%s) failed: %s\n", path, strerror(errno));
+ }
+ return 1;
+ }
+ setenv("OLDPWD", cwd.characters(), 1);
+ cwd = real_path;
+ setenv("PWD", cwd.characters(), 1);
+ return 0;
+}
+
+int Shell::builtin_cdh(int argc, const char** argv)
+{
+ int index = -1;
+
+ Core::ArgsParser parser;
+ parser.add_positional_argument(index, "Index of the cd history entry (leave out for a list)", "index", Core::ArgsParser::Required::No);
+
+ if (!parser.parse(argc, const_cast<char**>(argv), false))
+ return 1;
+
+ if (index == -1) {
+ if (cd_history.is_empty()) {
+ fprintf(stderr, "cdh: no history available\n");
+ return 0;
+ }
+
+ for (ssize_t i = cd_history.size() - 1; i >= 0; --i)
+ printf("%lu: %s\n", cd_history.size() - i, cd_history.at(i).characters());
+ return 0;
+ }
+
+ if (index < 1 || (size_t)index > cd_history.size()) {
+ fprintf(stderr, "cdh: history index out of bounds: %d not in (0, %zu)\n", index, cd_history.size());
+ return 1;
+ }
+
+ const char* path = cd_history.at(cd_history.size() - index).characters();
+ const char* cd_args[] = { "cd", path, nullptr };
+ return Shell::builtin_cd(2, cd_args);
+}
+
+int Shell::builtin_dirs(int argc, const char** argv)
+{
+ // The first directory in the stack is ALWAYS the current directory
+ directory_stack.at(0) = cwd.characters();
+
+ bool clear = false;
+ bool print = false;
+ bool number_when_printing = false;
+ char separator = ' ';
+
+ Vector<const char*> paths;
+
+ Core::ArgsParser parser;
+ parser.add_option(clear, "Clear the directory stack", "clear", 'c');
+ parser.add_option(print, "Print directory entries one per line", "print", 'p');
+ parser.add_option(number_when_printing, "Number the directories in the stack when printing", "number", 'v');
+ parser.add_positional_argument(paths, "Extra paths to put on the stack", "path", Core::ArgsParser::Required::No);
+
+ if (!parser.parse(argc, const_cast<char**>(argv), false))
+ return 1;
+
+ // -v implies -p
+ print = print || number_when_printing;
+
+ if (print) {
+ if (!paths.is_empty()) {
+ fprintf(stderr, "dirs: 'print' and 'number' are not allowed when any path is specified");
+ return 1;
+ }
+ separator = '\n';
+ }
+
+ if (clear) {
+ for (size_t i = 1; i < directory_stack.size(); i++)
+ directory_stack.remove(i);
+ }
+
+ for (auto& path : paths)
+ directory_stack.append(path);
+
+ if (print || (!clear && paths.is_empty())) {
+ int index = 0;
+ for (auto& directory : directory_stack) {
+ if (number_when_printing)
+ printf("%d ", index++);
+ print_path(directory);
+ fputc(separator, stdout);
+ }
+ }
+
+ return 0;
+}
+
+int Shell::builtin_exit(int argc, const char** argv)
+{
+ int exit_code = 0;
+ Core::ArgsParser parser;
+ parser.add_positional_argument(exit_code, "Exit code", "code", Core::ArgsParser::Required::No);
+ if (!parser.parse(argc, const_cast<char**>(argv)))
+ return 1;
+
+ if (!jobs.is_empty()) {
+ if (!m_should_ignore_jobs_on_next_exit) {
+ fprintf(stderr, "Shell: You have %zu active job%s, run 'exit' again to really exit.\n", jobs.size(), jobs.size() > 1 ? "s" : "");
+ m_should_ignore_jobs_on_next_exit = true;
+ return 1;
+ }
+ }
+ stop_all_jobs();
+ save_history();
+ printf("Good-bye!\n");
+ exit(exit_code);
+ return 0;
+}
+
+int Shell::builtin_export(int argc, const char** argv)
+{
+ Vector<const char*> vars;
+
+ Core::ArgsParser parser;
+ parser.add_positional_argument(vars, "List of variable[=value]'s", "values", Core::ArgsParser::Required::No);
+
+ if (!parser.parse(argc, const_cast<char**>(argv), false))
+ return 1;
+
+ if (vars.is_empty()) {
+ for (size_t i = 0; environ[i]; ++i)
+ puts(environ[i]);
+ return 0;
+ }
+
+ for (auto& value : vars) {
+ auto parts = String { value }.split_limit('=', 2);
+
+ if (parts.size() == 1) {
+ auto value = lookup_local_variable(parts[0]);
+ if (value) {
+ auto values = value->resolve_as_list(*this);
+ StringBuilder builder;
+ builder.join(" ", values);
+ parts.append(builder.to_string());
+ } else {
+ // Ignore the export.
+ continue;
+ }
+ }
+
+ int setenv_return = setenv(parts[0].characters(), parts[1].characters(), 1);
+
+ if (setenv_return != 0) {
+ perror("setenv");
+ return 1;
+ }
+
+ if (parts[0] == "PATH")
+ cache_path();
+ }
+
+ return 0;
+}
+
+int Shell::builtin_fg(int argc, const char** argv)
+{
+ int job_id = -1;
+
+ Core::ArgsParser parser;
+ parser.add_positional_argument(job_id, "Job ID to bring to foreground", "job-id", Core::ArgsParser::Required::No);
+
+ if (!parser.parse(argc, const_cast<char**>(argv), false))
+ return 1;
+
+ if (job_id == -1 && !jobs.is_empty())
+ job_id = find_last_job_id();
+
+ auto* job = const_cast<Job*>(find_job(job_id));
+
+ if (!job) {
+ if (job_id == -1) {
+ fprintf(stderr, "fg: no current job\n");
+ } else {
+ fprintf(stderr, "fg: job with id %d not found\n", job_id);
+ }
+ return 1;
+ }
+
+ job->set_running_in_background(false);
+
+ dbg() << "Resuming " << job->pid() << " (" << job->cmd() << ")";
+ fprintf(stderr, "Resuming job %llu - %s\n", job->job_id(), job->cmd().characters());
+
+ if (killpg(job->pgid(), SIGCONT) < 0) {
+ perror("killpg");
+ return 1;
+ }
+
+ block_on_job(job);
+
+ return job->exit_code();
+}
+
+int Shell::builtin_disown(int argc, const char** argv)
+{
+ Vector<const char*> str_job_ids;
+
+ Core::ArgsParser parser;
+ parser.add_positional_argument(str_job_ids, "Id of the jobs to disown (omit for current job)", "job_ids", Core::ArgsParser::Required::No);
+
+ if (!parser.parse(argc, const_cast<char**>(argv), false))
+ return 1;
+
+ Vector<size_t> job_ids;
+ for (auto& job_id : str_job_ids) {
+ auto id = StringView(job_id).to_uint();
+ if (id.has_value())
+ job_ids.append(id.value());
+ else
+ fprintf(stderr, "disown: Invalid job id %s\n", job_id);
+ }
+
+ if (job_ids.is_empty()) {
+ u64 id = 0;
+ for (auto& job : jobs)
+ id = max(id, job.value->job_id());
+ job_ids.append(id);
+ }
+
+ Vector<size_t> keys_of_jobs_to_disown;
+
+ for (auto id : job_ids) {
+ bool found = false;
+ for (auto& entry : jobs) {
+ if (entry.value->job_id() == id) {
+ keys_of_jobs_to_disown.append(entry.key);
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ fprintf(stderr, "disown: job with id %zu not found\n", id);
+ }
+ }
+ if (keys_of_jobs_to_disown.is_empty()) {
+ if (str_job_ids.is_empty()) {
+ fprintf(stderr, "disown: no current job\n");
+ }
+ // An error message has already been printed about the nonexistence of each listed job.
+ return 1;
+ }
+ for (auto job_index : keys_of_jobs_to_disown) {
+ auto job = jobs.get(job_index).value();
+
+ job->deactivate();
+
+ if (!job->is_running_in_background())
+ fprintf(stderr, "disown warning: job %llu is currently not running, 'kill -%d %d' to make it continue\n", job->job_id(), SIGCONT, job->pid());
+
+ jobs.remove(job_index);
+ }
+
+ return 0;
+}
+
+int Shell::builtin_history(int, const char**)
+{
+ for (size_t i = 0; i < editor->history().size(); ++i) {
+ printf("%6zu %s\n", i, editor->history()[i].characters());
+ }
+ return 0;
+}
+
+int Shell::builtin_jobs(int argc, const char** argv)
+{
+ bool list = false, show_pid = false;
+
+ Core::ArgsParser parser;
+ parser.add_option(list, "List all information about jobs", "list", 'l');
+ parser.add_option(show_pid, "Display the PID of the jobs", "pid", 'p');
+
+ if (!parser.parse(argc, const_cast<char**>(argv), false))
+ return 1;
+
+ enum {
+ Basic,
+ OnlyPID,
+ ListAll,
+ } mode { Basic };
+
+ if (show_pid)
+ mode = OnlyPID;
+
+ if (list)
+ mode = ListAll;
+
+ for (auto& job : jobs) {
+ auto pid = job.value->pid();
+ int wstatus;
+ auto rc = waitpid(pid, &wstatus, WNOHANG);
+ if (rc == -1) {
+ perror("waitpid");
+ return 1;
+ }
+ auto status = "running";
+
+ if (rc != 0) {
+ if (WIFEXITED(wstatus))
+ status = "exited";
+
+ if (WIFSTOPPED(wstatus))
+ status = "stopped";
+
+ if (WIFSIGNALED(wstatus))
+ status = "signaled";
+ }
+
+ char background_indicator = '-';
+
+ if (job.value->is_running_in_background())
+ background_indicator = '+';
+
+ switch (mode) {
+ case Basic:
+ printf("[%llu] %c %s %s\n", job.value->job_id(), background_indicator, status, job.value->cmd().characters());
+ break;
+ case OnlyPID:
+ printf("[%llu] %c %d %s %s\n", job.value->job_id(), background_indicator, pid, status, job.value->cmd().characters());
+ break;
+ case ListAll:
+ printf("[%llu] %c %d %d %s %s\n", job.value->job_id(), background_indicator, pid, job.value->pgid(), status, job.value->cmd().characters());
+ break;
+ }
+ }
+
+ return 0;
+}
+
+int Shell::builtin_popd(int argc, const char** argv)
+{
+ if (directory_stack.size() <= 1) {
+ fprintf(stderr, "Shell: popd: directory stack empty\n");
+ return 1;
+ }
+
+ bool should_not_switch = false;
+ String path = directory_stack.take_last();
+
+ Core::ArgsParser parser;
+ parser.add_option(should_not_switch, "Do not switch dirs", "no-switch", 'n');
+
+ if (!parser.parse(argc, const_cast<char**>(argv), false))
+ return 1;
+
+ bool should_switch = !should_not_switch;
+
+ // When no arguments are given, popd removes the top directory from the stack and performs a cd to the new top directory.
+ if (argc == 1) {
+ int rc = chdir(path.characters());
+ if (rc < 0) {
+ fprintf(stderr, "chdir(%s) failed: %s\n", path.characters(), strerror(errno));
+ return 1;
+ }
+
+ cwd = path;
+ return 0;
+ }
+
+ LexicalPath lexical_path(path.characters());
+ if (!lexical_path.is_valid()) {
+ fprintf(stderr, "LexicalPath failed to canonicalize '%s'\n", path.characters());
+ return 1;
+ }
+
+ const char* real_path = lexical_path.string().characters();
+
+ struct stat st;
+ int rc = stat(real_path, &st);
+ if (rc < 0) {
+ fprintf(stderr, "stat(%s) failed: %s\n", real_path, strerror(errno));
+ return 1;
+ }
+
+ if (!S_ISDIR(st.st_mode)) {
+ fprintf(stderr, "Not a directory: %s\n", real_path);
+ return 1;
+ }
+
+ if (should_switch) {
+ int rc = chdir(real_path);
+ if (rc < 0) {
+ fprintf(stderr, "chdir(%s) failed: %s\n", real_path, strerror(errno));
+ return 1;
+ }
+
+ cwd = lexical_path.string();
+ }
+
+ return 0;
+}
+
+int Shell::builtin_pushd(int argc, const char** argv)
+{
+ StringBuilder path_builder;
+ bool should_switch = true;
+
+ // From the BASH reference manual: https://www.gnu.org/software/bash/manual/html_node/Directory-Stack-Builtins.html
+ // With no arguments, pushd exchanges the top two directories and makes the new top the current directory.
+ if (argc == 1) {
+ if (directory_stack.size() < 2) {
+ fprintf(stderr, "pushd: no other directory\n");
+ return 1;
+ }
+
+ String dir1 = directory_stack.take_first();
+ String dir2 = directory_stack.take_first();
+ directory_stack.insert(0, dir2);
+ directory_stack.insert(1, dir1);
+
+ int rc = chdir(dir2.characters());
+ if (rc < 0) {
+ fprintf(stderr, "chdir(%s) failed: %s\n", dir2.characters(), strerror(errno));
+ return 1;
+ }
+
+ cwd = dir2;
+
+ return 0;
+ }
+
+ // Let's assume the user's typed in 'pushd <dir>'
+ if (argc == 2) {
+ directory_stack.append(cwd.characters());
+ if (argv[1][0] == '/') {
+ path_builder.append(argv[1]);
+ } else {
+ path_builder.appendf("%s/%s", cwd.characters(), argv[1]);
+ }
+ } else if (argc == 3) {
+ directory_stack.append(cwd.characters());
+ for (int i = 1; i < argc; i++) {
+ const char* arg = argv[i];
+
+ if (arg[0] != '-') {
+ if (arg[0] == '/') {
+ path_builder.append(arg);
+ } else
+ path_builder.appendf("%s/%s", cwd.characters(), arg);
+ }
+
+ if (!strcmp(arg, "-n"))
+ should_switch = false;
+ }
+ }
+
+ LexicalPath lexical_path(path_builder.to_string());
+ if (!lexical_path.is_valid()) {
+ fprintf(stderr, "LexicalPath failed to canonicalize '%s'\n", path_builder.to_string().characters());
+ return 1;
+ }
+
+ const char* real_path = lexical_path.string().characters();
+
+ struct stat st;
+ int rc = stat(real_path, &st);
+ if (rc < 0) {
+ fprintf(stderr, "stat(%s) failed: %s\n", real_path, strerror(errno));
+ return 1;
+ }
+
+ if (!S_ISDIR(st.st_mode)) {
+ fprintf(stderr, "Not a directory: %s\n", real_path);
+ return 1;
+ }
+
+ if (should_switch) {
+ int rc = chdir(real_path);
+ if (rc < 0) {
+ fprintf(stderr, "chdir(%s) failed: %s\n", real_path, strerror(errno));
+ return 1;
+ }
+
+ cwd = lexical_path.string();
+ }
+
+ return 0;
+}
+
+int Shell::builtin_pwd(int, const char**)
+{
+ print_path(cwd);
+ fputc('\n', stdout);
+ return 0;
+}
+
+int Shell::builtin_time(int argc, const char** argv)
+{
+ Vector<const char*> args;
+
+ Core::ArgsParser parser;
+ parser.add_positional_argument(args, "Command to execute with arguments", "command", Core::ArgsParser::Required::Yes);
+
+ if (!parser.parse(argc, const_cast<char**>(argv), false))
+ return 1;
+
+ StringBuilder builder;
+ builder.join(' ', args);
+
+ Core::ElapsedTimer timer;
+ timer.start();
+ // TODO: Exit code
+ run_command(builder.string_view());
+ fprintf(stderr, "Time: %d ms\n", timer.elapsed());
+ return 0;
+}
+
+int Shell::builtin_umask(int argc, const char** argv)
+{
+ const char* mask_text = nullptr;
+
+ Core::ArgsParser parser;
+ parser.add_positional_argument(mask_text, "New mask (omit to get current mask)", "octal-mask", Core::ArgsParser::Required::No);
+
+ if (!parser.parse(argc, const_cast<char**>(argv), false))
+ return 1;
+
+ if (!mask_text) {
+ mode_t old_mask = umask(0);
+ printf("%#o\n", old_mask);
+ umask(old_mask);
+ return 0;
+ }
+
+ unsigned mask;
+ int matches = sscanf(mask_text, "%o", &mask);
+ if (matches == 1) {
+ umask(mask);
+ return 0;
+ }
+
+ fprintf(stderr, "umask: Invalid mask '%s'\n", mask_text);
+ return 1;
+}
+
+int Shell::builtin_unset(int argc, const char** argv)
+{
+ Vector<const char*> vars;
+
+ Core::ArgsParser parser;
+ parser.add_positional_argument(vars, "List of variables", "variables", Core::ArgsParser::Required::Yes);
+
+ if (!parser.parse(argc, const_cast<char**>(argv), false))
+ return 1;
+
+ for (auto& value : vars) {
+ if (lookup_local_variable(value)) {
+ unset_local_variable(value);
+ } else {
+ unsetenv(value);
+ }
+ }
+
+ return 0;
+}
+
+bool Shell::run_builtin(int argc, const char** argv, int& retval)
+{
+ if (argc == 0)
+ return false;
+
+ StringView name { argv[0] };
+
+#define __ENUMERATE_SHELL_BUILTIN(builtin) \
+ if (name == #builtin) { \
+ retval = builtin_##builtin(argc, argv); \
+ return true; \
+ }
+
+ ENUMERATE_SHELL_BUILTINS();
+
+#undef __ENUMERATE_SHELL_BUILTIN
+
+ return false;
+}
diff --git a/Shell/CMakeLists.txt b/Shell/CMakeLists.txt
index bf35dab3a9..339f9a48de 100644
--- a/Shell/CMakeLists.txt
+++ b/Shell/CMakeLists.txt
@@ -1,4 +1,6 @@
set(SOURCES
+ AST.cpp
+ Builtin.cpp
main.cpp
Parser.cpp
Shell.cpp
diff --git a/Shell/Forward.h b/Shell/Forward.h
new file mode 100644
index 0000000000..86a0ff0858
--- /dev/null
+++ b/Shell/Forward.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2020, The SerenityOS developers.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+class Shell;
+namespace AST {
+
+class Node;
+class Value;
+
+}
diff --git a/Shell/Job.h b/Shell/Job.h
index d374a9c8d5..c674136942 100644
--- a/Shell/Job.h
+++ b/Shell/Job.h
@@ -27,6 +27,7 @@
#pragma once
#include "Execution.h"
+#include <AK/Function.h>
#include <AK/JsonObject.h>
#include <AK/JsonValue.h>
#include <AK/OwnPtr.h>
@@ -34,7 +35,12 @@
#include <LibCore/ElapsedTimer.h>
#include <LibCore/Object.h>
-class Job {
+#define JOB_TIME_INFO
+#ifndef __serenity__
+# undef JOB_TIME_INFO
+#endif
+
+class Job : public RefCounted<Job> {
public:
explicit Job()
{
@@ -42,10 +48,12 @@ public:
~Job()
{
+#ifdef JOB_TIME_INFO
if (m_active) {
auto elapsed = m_command_timer.elapsed();
dbg() << "Command \"" << m_cmd << "\" finished in " << elapsed << " ms";
}
+#endif
}
Job(pid_t pid, unsigned pgid, String cmd, u64 job_id)
@@ -64,7 +72,10 @@ public:
u64 job_id() const { return m_job_id; }
bool exited() const { return m_exited; }
int exit_code() const { return m_exit_code; }
+ bool should_be_disowned() const { return m_should_be_disowned; }
+ void disown() { m_should_be_disowned = true; }
bool is_running_in_background() const { return m_running_in_background; }
+ Function<void(RefPtr<Job>)> on_exit;
Core::ElapsedTimer& timer() { return m_command_timer; }
@@ -74,6 +85,8 @@ public:
return;
m_exit_code = exit_code;
m_exited = true;
+ if (on_exit)
+ on_exit(*this);
}
void set_running_in_background(bool running_in_background)
@@ -93,4 +106,5 @@ private:
int m_exit_code { -1 };
Core::ElapsedTimer m_command_timer;
mutable bool m_active { true };
+ bool m_should_be_disowned { false };
};
diff --git a/Shell/Parser.cpp b/Shell/Parser.cpp
index 41bccbfe91..2f8cd59afb 100644
--- a/Shell/Parser.cpp
+++ b/Shell/Parser.cpp
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2020, the SerenityOS developers.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -29,355 +29,747 @@
#include <stdio.h>
#include <unistd.h>
-void Parser::commit_token(Token::Type type, AllowEmptyToken allow_empty)
+char Parser::peek()
{
- if (allow_empty == AllowEmptyToken::No && m_token.is_empty())
- return;
- Token token { String::copy(m_token), m_position, m_token.size(), type };
- if (state() == InRedirectionPath) {
- m_redirections.last().path = move(token);
- m_token.clear_with_capacity();
- return;
+ if (m_offset == m_input.length())
+ return 0;
+
+ ASSERT(m_offset < m_input.length());
+ return m_input[m_offset];
+}
+
+char Parser::consume()
+{
+ auto ch = peek();
+ ++m_offset;
+ return ch;
+}
+
+void Parser::putback()
+{
+ ASSERT(m_offset > 0);
+ --m_offset;
+}
+
+bool Parser::expect(char ch)
+{
+ return expect(StringView { &ch, 1 });
+}
+
+bool Parser::expect(const StringView& expected)
+{
+ if (expected.length() + m_offset > m_input.length())
+ return false;
+
+ for (size_t i = 0; i < expected.length(); ++i) {
+ if (peek() != expected[i])
+ return false;
+
+ consume();
}
- m_tokens.append(token);
- m_token.clear_with_capacity();
-};
-void Parser::commit_subcommand()
+ return true;
+}
+
+template<typename A, typename... Args>
+RefPtr<A> Parser::create(Args... args)
{
- if (m_tokens.is_empty())
- return;
- m_subcommands.append({ move(m_tokens), move(m_redirections), {} });
+ return adopt(*new A(AST::Position { m_rule_start_offsets.last(), m_offset }, args...));
}
-void Parser::commit_command(Attributes attributes)
+[[nodiscard]] OwnPtr<Parser::ScopedOffset> Parser::push_start()
{
- if (m_subcommands.is_empty())
- return;
- m_commands.append({ move(m_subcommands), attributes });
+ return make<ScopedOffset>(m_rule_start_offsets, m_offset);
}
-void Parser::do_pipe()
+static constexpr bool is_whitespace(char c)
{
- m_redirections.append({ Redirection::Pipe, STDOUT_FILENO });
- commit_subcommand();
+ return c == ' ' || c == '\t';
}
-void Parser::begin_redirect_read(int fd)
+static constexpr bool is_word_character(char c)
{
- m_redirections.append({ Redirection::FileRead, fd });
+ return (c <= '9' && c >= '0') || (c <= 'Z' && c >= 'A') || (c <= 'z' && c >= 'a') || c == '_';
}
-void Parser::begin_redirect_write(int fd)
+static constexpr bool is_digit(char c)
{
- m_redirections.append({ Redirection::FileWrite, fd });
+ return c <= '9' && c >= '0';
}
-bool Parser::in_state(State state) const
+static constexpr auto is_not(char c)
{
- for (auto s : m_state_stack)
- if (s == state)
- return true;
- return false;
+ return [c](char ch) { return ch != c; };
}
-Vector<Command> Parser::parse()
+static inline char to_byte(char a, char b)
{
- for (size_t i = 0; i < m_input.length(); ++i, m_position = i) {
- char ch = m_input.characters()[i];
- switch (state()) {
- case State::Free:
- if (ch == '#') {
- commit_token(Token::Bare);
-
- while (i < m_input.length()) {
- ch = m_input.characters()[++i];
- ++m_position;
- if (ch == '\n')
- break;
- m_token.append(ch);
- }
- commit_token(Token::Comment);
- break;
- }
- if (ch == ' ') {
- commit_token(Token::Bare);
- break;
- }
- if (ch == ';') {
- commit_token(Token::Special);
- commit_subcommand();
- commit_command();
- break;
- }
- if (ch == '&') {
- commit_token(Token::Special);
-
- if (i + 1 >= m_input.length()) {
- in_background:;
- // Nothing interesting past this token, commit with InBackground
- commit_subcommand();
- commit_command(Attributes::InBackground);
- break;
- }
+ char buf[3] { a, b, 0 };
+ return strtol(buf, nullptr, 16);
+}
- ch = m_input.characters()[++i];
- ++m_position;
+RefPtr<AST::Node> Parser::parse()
+{
+ m_offset = 0;
- if (ch == '&') {
- // This is '&&', commit with ShortCircuit
- commit_subcommand();
- commit_command(Attributes::ShortCircuitOnFailure);
- break;
- }
+ return parse_toplevel();
+}
+
+RefPtr<AST::Node> Parser::parse_toplevel()
+{
+ auto rule_start = push_start();
+
+ if (auto sequence = parse_sequence())
+ return create<AST::Execute>(sequence);
+
+ return nullptr;
+}
+
+RefPtr<AST::Node> Parser::parse_sequence()
+{
+ auto rule_start = push_start();
+ auto var_decls = parse_variable_decls();
- --i;
- --m_position;
- goto in_background;
+ auto pipe_seq = parse_pipe_sequence();
+ if (!pipe_seq)
+ return var_decls;
+
+ if (var_decls)
+ pipe_seq = create<AST::Sequence>(move(var_decls), move(pipe_seq));
+
+ consume_while(is_whitespace);
+
+ switch (peek()) {
+ case ';':
+ consume();
+ if (auto expr = parse_sequence()) {
+ return create<AST::Sequence>(move(pipe_seq), move(expr)); // Sequence
+ }
+ return pipe_seq;
+ case '&': {
+ auto execute_pipe_seq = create<AST::Execute>(pipe_seq);
+ consume();
+ if (peek() == '&') {
+ consume();
+ if (auto expr = parse_sequence()) {
+ return create<AST::And>(move(execute_pipe_seq), move(expr)); // And
}
- if (ch == '|') {
- commit_token(Token::Special);
- if (m_tokens.is_empty()) {
- fprintf(stderr, "Syntax error: Nothing before pipe (|)\n");
- return {};
+ return execute_pipe_seq;
+ }
+
+ auto bg = create<AST::Background>(move(pipe_seq)); // Execute Background
+ if (auto rest = parse_sequence())
+ return create<AST::Sequence>(move(bg), move(rest)); // Sequence Background Sequence
+
+ return bg;
+ }
+ case '|': {
+ auto execute_pipe_seq = create<AST::Execute>(pipe_seq);
+ consume();
+ if (peek() != '|') {
+ putback();
+ return execute_pipe_seq;
+ }
+ consume();
+ if (auto expr = parse_sequence()) {
+ return create<AST::Or>(move(execute_pipe_seq), move(expr)); // Or
+ }
+ putback();
+ return execute_pipe_seq;
+ }
+ default:
+ return pipe_seq;
+ }
+}
+
+RefPtr<AST::Node> Parser::parse_variable_decls()
+{
+ auto rule_start = push_start();
+
+ consume_while(is_whitespace);
+
+ auto offset_before_name = m_offset;
+ auto var_name = consume_while(is_word_character);
+ if (var_name.is_empty())
+ return nullptr;
+
+ if (!expect('=')) {
+ m_offset = offset_before_name;
+ return nullptr;
+ }
+
+ auto name_expr = create<AST::BarewordLiteral>(move(var_name));
+
+ auto expression = parse_expression();
+ if (!expression) {
+ if (is_whitespace(peek())) {
+ auto string_start = push_start();
+ expression = create<AST::StringLiteral>("");
+ } else {
+ m_offset = offset_before_name;
+ return nullptr;
+ }
+ }
+
+ Vector<AST::VariableDeclarations::Variable> variables;
+ variables.append({ move(name_expr), move(expression) });
+
+ if (consume_while(is_whitespace).is_empty())
+ return create<AST::VariableDeclarations>(move(variables));
+
+ auto rest = parse_variable_decls();
+ if (!rest)
+ return create<AST::VariableDeclarations>(move(variables));
+
+ ASSERT(rest->is_variable_decls());
+ auto* rest_decl = static_cast<AST::VariableDeclarations*>(rest.ptr());
+
+ variables.append(rest_decl->variables());
+
+ return create<AST::VariableDeclarations>(move(variables));
+}
+
+RefPtr<AST::Node> Parser::parse_pipe_sequence()
+{
+ auto rule_start = push_start();
+ auto command = parse_command();
+ if (!command)
+ return nullptr;
+
+ consume_while(is_whitespace);
+
+ if (peek() != '|')
+ return command;
+
+ consume();
+
+ if (auto pipe_seq = parse_pipe_sequence()) {
+ return create<AST::Pipe>(move(command), move(pipe_seq)); // Pipe
+ }
+
+ putback();
+ return command;
+}
+
+RefPtr<AST::Node> Parser::parse_command()
+{
+ auto rule_start = push_start();
+ consume_while(is_whitespace);
+
+ auto redir = parse_redirection();
+ if (!redir) {
+ auto list_expr = parse_list_expression();
+ if (!list_expr)
+ return nullptr;
+
+ auto next_command = parse_command();
+ auto cast = create<AST::CastToCommand>(move(list_expr)); // Cast List Command
+ if (!next_command)
+ return cast;
+
+ return create<AST::Join>(move(cast), move(next_command)); // Join List Command
+ }
+
+ auto command = parse_command();
+ if (!command)
+ return redir;
+
+ return create<AST::Join>(move(redir), command); // Join Command Command
+}
+
+RefPtr<AST::Node> Parser::parse_redirection()
+{
+ auto rule_start = push_start();
+ auto pipe_fd = 0;
+ auto number = consume_while(is_digit);
+ if (number.is_empty()) {
+ pipe_fd = -1;
+ } else {
+ auto fd = number.to_int();
+ ASSERT(fd.has_value());
+ pipe_fd = fd.value();
+ }
+
+ switch (peek()) {
+ case '>': {
+ consume();
+ if (peek() == '>') {
+ consume();
+ consume_while(is_whitespace);
+ pipe_fd = pipe_fd >= 0 ? pipe_fd : STDOUT_FILENO;
+ auto path = parse_expression();
+ if (!path) {
+ if (!at_end()) {
+ // Eat a character and hope the problem goes away
+ consume();
}
- do_pipe();
- break;
- }
- if (ch == '>') {
- commit_token(Token::Special);
- begin_redirect_write(STDOUT_FILENO);
- ASSERT(!m_redirections.is_empty());
- m_redirections.last().redirection_op_start = m_position;
-
- // Search for another > for append.
- push_state(State::InWriteAppendOrRedirectionPath);
- break;
+ return create<AST::SyntaxError>();
}
- if (ch == '<') {
- commit_token(Token::Special);
- begin_redirect_read(STDIN_FILENO);
- ASSERT(!m_redirections.is_empty());
- m_redirections.last().redirection_op_start = m_position;
- push_state(State::InRedirectionPath);
- break;
+ return create<AST::WriteAppendRedirection>(pipe_fd, move(path)); // Redirection WriteAppend
+ }
+ if (peek() == '&') {
+ consume();
+ // FIXME: 'fd>&-' Syntax not the best. needs discussion.
+ if (peek() == '-') {
+ consume();
+ pipe_fd = pipe_fd >= 0 ? pipe_fd : STDOUT_FILENO;
+ return create<AST::CloseFdRedirection>(pipe_fd); // Redirection CloseFd
}
- if (ch == '\\') {
- if (i == m_input.length() - 1) {
- fprintf(stderr, "Syntax error: Nothing to escape (\\)\n");
- return {};
- }
- char next_ch = m_input.characters()[i + 1];
- m_token.append(next_ch);
- ++i;
- break;
+ int dest_pipe_fd = 0;
+ auto number = consume_while(is_digit);
+ pipe_fd = pipe_fd >= 0 ? pipe_fd : STDOUT_FILENO;
+ if (number.is_empty()) {
+ dest_pipe_fd = -1;
+ } else {
+ auto fd = number.to_int();
+ ASSERT(fd.has_value());
+ dest_pipe_fd = fd.value();
}
- if (ch == '\'') {
- push_state(State::InSingleQuotes);
- break;
+ return create<AST::Fd2FdRedirection>(pipe_fd, dest_pipe_fd); // Redirection Fd2Fd
+ }
+ consume_while(is_whitespace);
+ pipe_fd = pipe_fd >= 0 ? pipe_fd : STDOUT_FILENO;
+ auto path = parse_expression();
+ if (!path) {
+ if (!at_end()) {
+ // Eat a character and hope the problem goes away
+ consume();
}
- if (ch == '\"') {
- push_state(State::InDoubleQuotes);
- break;
+ return create<AST::SyntaxError>();
+ }
+ return create<AST::WriteRedirection>(pipe_fd, move(path)); // Redirection Write
+ }
+ case '<': {
+ consume();
+ enum {
+ Read,
+ ReadWrite,
+ } mode { Read };
+
+ if (peek() == '>') {
+ mode = ReadWrite;
+ consume();
+ }
+
+ consume_while(is_whitespace);
+ pipe_fd = pipe_fd >= 0 ? pipe_fd : STDIN_FILENO;
+ auto path = parse_expression();
+ if (!path) {
+ if (!at_end()) {
+ // Eat a character and hope the problem goes away
+ consume();
}
+ return create<AST::SyntaxError>();
+ }
+ if (mode == Read)
+ return create<AST::ReadRedirection>(pipe_fd, move(path)); // Redirection Read
- // redirection from zsh-style multi-digit fd, such as {10}>file
- if (ch == '{') {
- bool is_multi_fd_redirection = false;
- size_t redir_end = i + 1;
-
- while (redir_end < m_input.length()) {
- char lookahead_ch = m_input.characters()[redir_end];
- if (isdigit(lookahead_ch)) {
- ++redir_end;
- continue;
- }
- if (lookahead_ch == '}' && redir_end + 1 != m_input.length()) {
- // Disallow {}> and {}<
- if (redir_end == i + 1)
- break;
-
- ++redir_end;
- if (m_input.characters()[redir_end] == '>' || m_input.characters()[redir_end] == '<')
- is_multi_fd_redirection = true;
- break;
- }
- break;
- }
+ return create<AST::ReadWriteRedirection>(pipe_fd, move(path)); // Redirection ReadWrite
+ }
+ default:
+ return nullptr;
+ }
+}
- if (is_multi_fd_redirection) {
- commit_token(Token::Special);
+RefPtr<AST::Node> Parser::parse_list_expression()
+{
+ consume_while(is_whitespace);
- int fd = atoi(&m_input.characters()[i + 1]);
+ auto rule_start = push_start();
- if (m_input.characters()[redir_end] == '>') {
- begin_redirect_write(fd);
- ASSERT(!m_redirections.is_empty());
- m_redirections.last().redirection_op_start = m_position;
- // Search for another > for append.
- push_state(State::InWriteAppendOrRedirectionPath);
- }
- if (m_input.characters()[redir_end] == '<') {
- begin_redirect_read(fd);
- ASSERT(!m_redirections.is_empty());
- m_redirections.last().redirection_op_start = m_position;
- push_state(State::InRedirectionPath);
- }
+ auto expr = parse_expression();
+ if (!expr)
+ return nullptr;
- i = redir_end;
+ if (consume_while(is_whitespace).is_empty())
+ return expr;
- break;
- }
- }
- if (isdigit(ch)) {
- if (i != m_input.length() - 1) {
- char next_ch = m_input.characters()[i + 1];
- if (next_ch == '>') {
- commit_token(Token::Special);
- begin_redirect_write(ch - '0');
- ASSERT(!m_redirections.is_empty());
- m_redirections.last().redirection_op_start = m_position;
- ++i;
-
- // Search for another > for append.
- push_state(State::InWriteAppendOrRedirectionPath);
- break;
- }
- if (next_ch == '<') {
- commit_token(Token::Special);
- begin_redirect_read(ch - '0');
- ASSERT(!m_redirections.is_empty());
- m_redirections.last().redirection_op_start = m_position;
- ++i;
-
- push_state(State::InRedirectionPath);
- break;
- }
- }
- }
- m_token.append(ch);
- break;
- case State::InWriteAppendOrRedirectionPath:
- if (ch == '>') {
- commit_token(Token::Special);
- pop_state();
- push_state(State::InRedirectionPath);
- ASSERT(m_redirections.size());
- m_redirections.last().type = Redirection::FileWriteAppend;
- break;
- }
+ auto list = parse_list_expression();
+ if (!list)
+ return create<AST::CastToList>(move(expr));
- // Not another > means that it's probably a path.
- pop_state();
- push_state(InRedirectionPath);
- [[fallthrough]];
- case State::InRedirectionPath:
- if (ch == '<') {
- commit_token(Token::Special);
- begin_redirect_read(STDIN_FILENO);
+ return create<AST::ListConcatenate>(move(expr), move(list)); // Join Element List
+}
+
+RefPtr<AST::Node> Parser::parse_expression()
+{
+ auto rule_start = push_start();
+ auto starting_char = peek();
+
+ if (strchr("&|[]){} ;<>", starting_char) != nullptr)
+ return nullptr;
+
+ if (isdigit(starting_char)) {
+ ScopedValueRollback offset_rollback { m_offset };
+
+ auto redir = parse_redirection();
+ if (redir)
+ return nullptr;
+ }
+
+ if (starting_char == '$') {
+ if (auto variable = parse_variable())
+ return variable;
+
+ if (auto inline_exec = parse_evaluate())
+ return inline_exec;
+ }
+
+ if (starting_char == '#')
+ return parse_comment();
+
+ if (starting_char == '(') {
+ consume();
+ auto list = parse_list_expression();
+ if (!list)
+ list = create<AST::SyntaxError>();
+ if (!expect(')'))
+ return create<AST::SyntaxError>();
+ return create<AST::CastToList>(move(list)); // Cast To List
+ }
+
+ return parse_string_composite();
+}
+
+RefPtr<AST::Node> Parser::parse_string_composite()
+{
+ auto rule_start = push_start();
+ if (auto string = parse_string()) {
+ if (auto next_part = parse_string_composite())
+ return create<AST::StringConcatenate>(move(string), move(next_part)); // Concatenate String StringComposite
+
+ return string;
+ }
+
+ if (auto variable = parse_variable()) {
+ if (auto next_part = parse_string_composite())
+ return create<AST::StringConcatenate>(move(variable), move(next_part)); // Concatenate Variable StringComposite
+
+ return variable;
+ }
+
+ if (auto glob = parse_glob()) {
+ if (auto next_part = parse_string_composite())
+ return create<AST::StringConcatenate>(move(glob), move(next_part)); // Concatenate Glob StringComposite
+
+ return glob;
+ }
+
+ if (auto bareword = parse_bareword()) {
+ if (auto next_part = parse_string_composite())
+ return create<AST::StringConcatenate>(move(bareword), move(next_part)); // Concatenate Bareword StringComposite
+
+ return bareword;
+ }
+
+ if (auto inline_command = parse_evaluate()) {
+ if (auto next_part = parse_string_composite())
+ return create<AST::StringConcatenate>(move(inline_command), move(next_part)); // Concatenate Execute StringComposite
+
+ return inline_command;
+ }
+
+ return nullptr;
+}
+
+RefPtr<AST::Node> Parser::parse_string()
+{
+ auto rule_start = push_start();
+ if (at_end())
+ return nullptr;
+
+ if (peek() == '"') {
+ consume();
+ auto inner = parse_doublequoted_string_inner();
+ if (!expect('"') || !inner)
+ return create<AST::SyntaxError>();
+ return create<AST::DoubleQuotedString>(move(inner)); // Double Quoted String
+ }
+
+ if (peek() == '\'') {
+ consume();
+ auto text = consume_while(is_not('\''));
+ if (!expect('\''))
+ return create<AST::SyntaxError>();
+
+ return create<AST::StringLiteral>(move(text)); // String Literal
+ }
+
+ return nullptr;
+}
+
+RefPtr<AST::Node> Parser::parse_doublequoted_string_inner()
+{
+ auto rule_start = push_start();
+ if (at_end())
+ return nullptr;
+
+ StringBuilder builder;
+ while (!at_end() && peek() != '"') {
+ if (peek() == '\\') {
+ consume();
+ if (at_end()) {
break;
}
- if (ch == '>') {
- commit_token(Token::Special);
- begin_redirect_read(STDOUT_FILENO);
+ auto ch = consume();
+ switch (ch) {
+ case '\\':
+ default:
+ builder.append(ch);
break;
- }
- if (ch == '|') {
- commit_token(Token::Special);
- if (m_tokens.is_empty()) {
- fprintf(stderr, "Syntax error: Nothing before pipe (|)\n");
- return {};
+ case 'x': {
+ if (m_input.length() <= m_offset + 2)
+ break;
+ auto first_nibble = tolower(consume());
+ auto second_nibble = tolower(consume());
+ if (!isxdigit(first_nibble) || !isxdigit(second_nibble)) {
+ builder.append(first_nibble);
+ builder.append(second_nibble);
+ break;
}
- do_pipe();
- pop_state();
+ builder.append(to_byte(first_nibble, second_nibble));
break;
}
- if (ch == '"') {
- push_state(State::InDoubleQuotes);
+ case 'a':
+ builder.append('\a');
break;
- }
- if (ch == '\'') {
- push_state(State::InSingleQuotes);
+ case 'b':
+ builder.append('\b');
break;
- }
- if (ch == ' ') {
- if (m_token.is_empty()) {
- // foo > bar
- // ^ We are at this space, we want to ignore it but not leave the state.
- break;
- }
- commit_token(Token::Special);
- if (m_tokens.is_empty()) {
- fprintf(stderr, "Syntax error: Redirection without a path\n");
- return {};
- }
- pop_state();
+ case 'e':
+ builder.append('\x1b');
break;
- }
- m_token.append(ch);
- break;
- case State::InSingleQuotes:
- if (ch == '\'') {
- if (!in_state(State::InRedirectionPath))
- commit_token(Token::SingleQuoted, AllowEmptyToken::Yes);
- pop_state();
+ case 'f':
+ builder.append('\f');
break;
- }
- m_token.append(ch);
- break;
- case State::InDoubleQuotes:
- if (ch == '\"') {
- if (!in_state(State::InRedirectionPath))
- commit_token(Token::DoubleQuoted, AllowEmptyToken::Yes);
- pop_state();
+ case 'r':
+ builder.append('\r');
+ break;
+ case 'n':
+ builder.append('\n');
break;
}
- if (ch == '\\') {
- if (i == m_input.length() - 1) {
- fprintf(stderr, "Syntax error: Nothing to escape (\\)\n");
- return {};
+ continue;
+ }
+ if (peek() == '$') {
+ auto string_literal = create<AST::StringLiteral>(builder.to_string()); // String Literal
+ if (auto variable = parse_variable()) {
+ auto inner = create<AST::StringPartCompose>(
+ move(string_literal),
+ move(variable)); // Compose String Variable
+
+ if (auto string = parse_doublequoted_string_inner()) {
+ return create<AST::StringPartCompose>(move(inner), move(string)); // Compose Composition Composition
}
- char next_ch = m_input.characters()[i + 1];
- if (next_ch == '$' || next_ch == '`'
- || next_ch == '"' || next_ch == '\\') {
- m_token.append(next_ch);
- ++i;
- continue;
+
+ return inner;
+ }
+
+ if (auto evaluate = parse_evaluate()) {
+ auto composition = create<AST::StringPartCompose>(
+ move(string_literal),
+ move(evaluate)); // Compose String Sequence
+
+ if (auto string = parse_doublequoted_string_inner()) {
+ return create<AST::StringPartCompose>(move(composition), move(string)); // Compose Composition Composition
}
- m_token.append('\\');
- break;
+
+ return composition;
}
- m_token.append(ch);
- break;
- };
+ }
+
+ builder.append(consume());
+ }
+
+ return create<AST::StringLiteral>(builder.to_string()); // String Literal
+}
+
+RefPtr<AST::Node> Parser::parse_variable()
+{
+ auto rule_start = push_start();
+ if (at_end())
+ return nullptr;
+
+ if (peek() != '$')
+ return nullptr;
+
+ consume();
+ switch (peek()) {
+ case '$':
+ case '?':
+ return create<AST::SpecialVariable>(consume()); // Variable Special
+ default:
+ break;
+ }
+
+ auto name = consume_while(is_word_character);
+
+ if (name.length() == 0) {
+ putback();
+ return nullptr;
}
- while (m_state_stack.size() > 1) {
- if (state() == State::InDoubleQuotes) {
- pop_state();
- commit_token(Token::UnterminatedDoubleQuoted, AllowEmptyToken::Yes);
- } else if (state() == State::InSingleQuotes) {
- pop_state();
- commit_token(Token::UnterminatedSingleQuoted, AllowEmptyToken::Yes);
+ return create<AST::SimpleVariable>(move(name)); // Variable Simple
+}
+
+RefPtr<AST::Node> Parser::parse_evaluate()
+{
+ auto rule_start = push_start();
+ if (at_end())
+ return nullptr;
+
+ if (peek() != '$')
+ return nullptr;
+
+ consume();
+ auto inner = parse_expression();
+
+ if (!inner) {
+ inner = create<AST::SyntaxError>();
+ } else {
+ if (inner->is_list()) {
+ auto execute_inner = create<AST::Execute>(move(inner));
+ execute_inner->capture_stdout();
+ inner = execute_inner;
} else {
- commit_token(Token::Bare, AllowEmptyToken::No);
- pop_state();
+ // Trying to evaluate something other than a list
+ // FIXME: This bit be dynamic, what do?
+ auto dyn_inner = create<AST::DynamicEvaluate>(move(inner));
+ inner = dyn_inner;
}
}
- ASSERT(state() == State::Free);
- commit_token(Token::Bare);
- commit_subcommand();
- commit_command();
+ return inner;
+}
- if (!m_subcommands.is_empty()) {
- for (auto& redirection : m_subcommands.last().redirections) {
- if (redirection.type == Redirection::Pipe) {
- fprintf(stderr, "Syntax error: Nothing after last pipe (|)\n");
- return {};
+RefPtr<AST::Node> Parser::parse_comment()
+{
+ if (at_end())
+ return nullptr;
+
+ if (peek() != '#')
+ return nullptr;
+
+ consume();
+ auto text = consume_while(is_not('\n'));
+ if (peek() == '\n')
+ consume();
+
+ return create<AST::Comment>(move(text)); // Comment
+}
+
+RefPtr<AST::Node> Parser::parse_bareword()
+{
+ auto rule_start = push_start();
+ StringBuilder builder;
+ auto is_acceptable_bareword_character = [](char c) {
+ return strchr("\\\"'*$&#|()[]{} ?;<>", c) == nullptr;
+ };
+ while (!at_end()) {
+ char ch = peek();
+ if (ch == '\\') {
+ consume();
+ if (!at_end()) {
+ ch = consume();
+ if (is_acceptable_bareword_character(ch))
+ builder.append('\\');
}
+ builder.append(ch);
+ continue;
}
+
+ if (is_acceptable_bareword_character(ch)) {
+ builder.append(consume());
+ continue;
+ }
+
+ break;
+ }
+
+ if (builder.is_empty())
+ return nullptr;
+
+ auto string = builder.to_string();
+ if (string.starts_with('~')) {
+ String username;
+ auto first_slash_index = string.index_of("/");
+ if (first_slash_index.has_value()) {
+ username = string.substring_view(1, first_slash_index.value() - 1);
+ string = string.substring_view(first_slash_index.value(), string.length() - first_slash_index.value());
+ } else {
+ username = string.substring_view(1, string.length() - 1);
+ string = "";
+ }
+ auto current_end = m_offset;
+ m_offset -= string.length();
+ auto tilde = create<AST::Tilde>(move(username));
+ auto text_start = push_start();
+ m_offset = current_end;
+ if (string.is_empty())
+ return tilde;
+ return create<AST::StringPartCompose>(move(tilde), create<AST::BarewordLiteral>(move(string))); // Compose Varible Bareword
}
- return move(m_commands);
+ if (string.starts_with("\\~")) {
+ // Un-escape the tilde, but only at the start (where it would be an expansion)
+ }
+
+ return create<AST::BarewordLiteral>(builder.to_string()); // Bareword Literal
+}
+
+RefPtr<AST::Node> Parser::parse_glob()
+{
+ auto rule_start = push_start();
+ auto bareword_part = parse_bareword();
+
+ if (at_end())
+ return bareword_part;
+
+ char ch = peek();
+ if (ch == '*' || ch == '?') {
+ consume();
+ // FIXME: Join all parts before making AST nodes
+ StringBuilder textbuilder;
+ if (bareword_part) {
+ ASSERT(bareword_part->is_bareword() || bareword_part->is_tilde());
+ StringView text;
+ if (bareword_part->is_tilde()) {
+ auto bareword = static_cast<AST::BarewordLiteral*>(bareword_part.ptr());
+ text = bareword->text();
+ } else {
+ auto tilde = static_cast<AST::Tilde*>(bareword_part.ptr());
+ text = tilde->text();
+ }
+ textbuilder.append(text);
+ }
+
+ textbuilder.append(ch);
+
+ auto glob_after = parse_glob();
+ if (glob_after) {
+ if (glob_after->is_glob()) {
+ auto glob = static_cast<AST::BarewordLiteral*>(glob_after.ptr());
+ textbuilder.append(glob->text());
+ } else if (glob_after->is_bareword()) {
+ auto bareword = static_cast<AST::BarewordLiteral*>(glob_after.ptr());
+ textbuilder.append(bareword->text());
+ } else {
+ ASSERT_NOT_REACHED();
+ }
+ }
+
+ return create<AST::Glob>(textbuilder.to_string()); // Glob
+ }
+
+ return bareword_part;
+}
+
+StringView Parser::consume_while(Function<bool(char)> condition)
+{
+ auto start_offset = m_offset;
+
+ while (!at_end() && condition(peek()))
+ consume();
+
+ return m_input.substring_view(start_offset, m_offset - start_offset);
}
diff --git a/Shell/Parser.h b/Shell/Parser.h
index 112747ce3e..3956f518fc 100644
--- a/Shell/Parser.h
+++ b/Shell/Parser.h
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * Copyright (c) 2020, the SerenityOS developers.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -26,111 +26,134 @@
#pragma once
+#include "AST.h"
+#include <AK/Function.h>
+#include <AK/RefPtr.h>
#include <AK/String.h>
+#include <AK/StringBuilder.h>
#include <AK/Vector.h>
-struct Token {
- enum Type {
- Bare,
- SingleQuoted,
- DoubleQuoted,
- UnterminatedSingleQuoted,
- UnterminatedDoubleQuoted,
- Comment,
- Special,
- };
- String text;
- size_t end;
- size_t length;
- Type type;
-};
+class Parser {
+public:
+ Parser(StringView input)
+ : m_input(move(input))
+ {
+ }
-enum Attributes {
- None = 0x0,
- ShortCircuitOnFailure = 0x1,
- InBackground = 0x2,
-};
+ RefPtr<AST::Node> parse();
-struct Redirection {
- enum Type {
- Pipe,
- FileWrite,
- FileWriteAppend,
- FileRead,
+private:
+ RefPtr<AST::Node> parse_toplevel();
+ RefPtr<AST::Node> parse_sequence();
+ RefPtr<AST::Node> parse_variable_decls();
+ RefPtr<AST::Node> parse_pipe_sequence();
+ RefPtr<AST::Node> parse_command();
+ RefPtr<AST::Node> parse_redirection();
+ RefPtr<AST::Node> parse_list_expression();
+ RefPtr<AST::Node> parse_expression();
+ RefPtr<AST::Node> parse_string_composite();
+ RefPtr<AST::Node> parse_string();
+ RefPtr<AST::Node> parse_doublequoted_string_inner();
+ RefPtr<AST::Node> parse_variable();
+ RefPtr<AST::Node> parse_evaluate();
+ RefPtr<AST::Node> parse_comment();
+ RefPtr<AST::Node> parse_bareword();
+ RefPtr<AST::Node> parse_glob();
+
+ template<typename A, typename... Args>
+ RefPtr<A> create(Args... args);
+
+ bool at_end() const { return m_input.length() <= m_offset; }
+ char peek();
+ char consume();
+ void putback();
+ bool expect(char);
+ bool expect(const StringView&);
+
+ StringView consume_while(Function<bool(char)>);
+
+ struct ScopedOffset {
+ ScopedOffset(Vector<size_t>& offsets, size_t offset)
+ : offsets(offsets)
+ , offset(offset)
+ {
+ offsets.append(offset);
+ }
+ ~ScopedOffset()
+ {
+ auto last = offsets.take_last();
+ ASSERT(last == offset);
+ }
+
+ Vector<size_t>& offsets;
+ size_t offset;
};
- Type type;
- int fd { -1 };
- int rewire_fd { -1 };
- size_t redirection_op_start { 0 };
- Token path {};
-};
-struct Rewiring {
- int fd { -1 };
- int rewire_fd { -1 };
-};
+ OwnPtr<ScopedOffset> push_start();
-struct Subcommand {
- Vector<Token> args;
- Vector<Redirection> redirections;
- Vector<Rewiring> rewirings;
+ StringView m_input;
+ size_t m_offset { 0 };
+ Vector<size_t> m_rule_start_offsets;
};
-struct Command {
- Vector<Subcommand> subcommands;
- Attributes attributes;
-};
+#if 0
+constexpr auto the_grammar = R"(
+toplevel :: sequence?
-class Parser {
-public:
- explicit Parser(const String& input)
- : m_input(input)
- {
- }
+sequence :: variable_decls? pipe_sequence ';' sequence
+ | variable_decls? pipe_sequence '&'
+ | variable_decls? pipe_sequence '&' '&' sequence
+ | variable_decls? pipe_sequence '|' '|' sequence
+ | variable_decls? pipe_sequence
- Vector<Command> parse();
+variable_decls :: identifier '=' expression (' '+ variable_decls)? ' '*
-private:
- enum class AllowEmptyToken {
- No,
- Yes,
- };
- void commit_token(Token::Type, AllowEmptyToken = AllowEmptyToken::No);
- void commit_subcommand();
- void commit_command(Attributes = None);
- void do_pipe();
- void begin_redirect_read(int fd);
- void begin_redirect_write(int fd);
-
- enum State {
- Free,
- InSingleQuotes,
- InDoubleQuotes,
- InWriteAppendOrRedirectionPath,
- InRedirectionPath,
- };
+pipe_sequence :: command '|' pipe_sequence
+ | command
- State state() const { return m_state_stack.last(); }
+command :: redirection command
+ | list_expression command?
- void pop_state()
- {
- m_state_stack.take_last();
- }
+redirection :: number? '>'{1,2} ' '* string_composite
+ | number? '<' ' '* string_composite
+ | number? '>' '&' number
- void push_state(State state)
- {
- m_state_stack.append(state);
- }
+list_expression :: ' '* expression (' '+ list_expression)?
- bool in_state(State) const;
+expression :: evaluate
+ | string_composite
+ | comment
+ | '(' list_expression ')'
- Vector<State> m_state_stack { Free };
- String m_input;
+evaluate :: '$' expression {eval / dynamic resolve}
- Vector<Command> m_commands;
- Vector<Subcommand> m_subcommands;
- Vector<Token> m_tokens;
- Vector<Redirection> m_redirections;
- Vector<char> m_token;
- size_t m_position { 0 };
-};
+string_composite :: string string_composite?
+ | variable string_composite?
+ | bareword string_composite?
+ | glob string_composite?
+
+string :: '"' dquoted_string_inner '"'
+ | "'" [^']* "'"
+
+dquoted_string_inner :: '\' . dquoted_string_inner? {concat}
+ | variable dquoted_string_inner? {compose}
+ | . dquoted_string_inner?
+ | '\' 'x' digit digit dquoted_string_inner?
+ | '\' [abefrn] dquoted_string_inner?
+
+variable :: '$' identifier
+ | '$' '$'
+ | '$' '?'
+ | ...
+
+comment :: '#' [^\n]*
+
+bareword :: [^"'*$&#|()[\]{} ?;<>] bareword?
+ | '\' [^"'*$&#|()[\]{} ?;<>] bareword?
+
+bareword_with_tilde_expansion :: '~' bareword?
+
+glob :: [*?] bareword?
+ | bareword [*?]
+)";
+#endif
diff --git a/Shell/Shell.cpp b/Shell/Shell.cpp
index db41a684d4..9ae3b6bc80 100644
--- a/Shell/Shell.cpp
+++ b/Shell/Shell.cpp
@@ -47,13 +47,9 @@
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/utsname.h>
-#include <sys/wait.h>
#include <termios.h>
#include <unistd.h>
-// FIXME: We do not expand variables inside strings
-// if we want to be more sh-like, we should do that some day
-static constexpr bool HighlightVariablesInsideStrings = false;
static bool s_disable_hyperlinks = false;
extern RefPtr<Line::Editor> editor;
@@ -125,706 +121,7 @@ String Shell::prompt() const
return builder.to_string();
};
- auto the_prompt = build_prompt();
- auto prompt_metrics = editor->actual_rendered_string_metrics(the_prompt);
- auto prompt_length = prompt_metrics.line_lengths.last();
-
- if (m_should_continue != ExitCodeOrContinuationRequest::Nothing) {
- const auto format_string = "\033[34m%.*-s\033[m";
- switch (m_should_continue) {
- case ExitCodeOrContinuationRequest::Pipe:
- return String::format(format_string, prompt_length, "pipe> ");
- case ExitCodeOrContinuationRequest::DoubleQuotedString:
- return String::format(format_string, prompt_length, "dquote> ");
- case ExitCodeOrContinuationRequest::SingleQuotedString:
- return String::format(format_string, prompt_length, "squote> ");
- default:
- break;
- }
- }
- return the_prompt;
-}
-
-int Shell::builtin_bg(int argc, const char** argv)
-{
- int job_id = -1;
-
- Core::ArgsParser parser;
- parser.add_positional_argument(job_id, "Job id to run in background", "job_id", Core::ArgsParser::Required::No);
-
- if (!parser.parse(argc, const_cast<char**>(argv), false))
- return 1;
-
- if (job_id == -1 && !jobs.is_empty())
- job_id = find_last_job_id();
-
- Job* job = nullptr;
-
- for (auto& entry : jobs) {
- if (entry.value->job_id() == (u64)job_id) {
- job = entry.value;
- break;
- }
- }
- if (!job) {
- if (job_id == -1) {
- printf("bg: no current job\n");
- } else {
- printf("bg: job with id %d not found\n", job_id);
- }
- return 1;
- }
-
- job->set_running_in_background(true);
-
- dbg() << "Resuming " << job->pid() << " (" << job->cmd() << ")";
- printf("Resuming job %llu - %s\n", job->job_id(), job->cmd().characters());
-
- if (killpg(job->pgid(), SIGCONT) < 0) {
- perror("killpg");
- return 1;
- }
-
- return 0;
-}
-
-int Shell::builtin_cd(int argc, const char** argv)
-{
- const char* arg_path = nullptr;
-
- Core::ArgsParser parser;
- parser.add_positional_argument(arg_path, "Path to change to", "path", Core::ArgsParser::Required::No);
-
- if (!parser.parse(argc, const_cast<char**>(argv), false))
- return 1;
-
- String new_path;
-
- if (!arg_path) {
- new_path = home;
- if (cd_history.is_empty() || cd_history.last() != home)
- cd_history.enqueue(home);
- } else {
- if (cd_history.is_empty() || cd_history.last() != arg_path)
- cd_history.enqueue(arg_path);
- if (strcmp(argv[1], "-") == 0) {
- char* oldpwd = getenv("OLDPWD");
- if (oldpwd == nullptr)
- return 1;
- new_path = oldpwd;
- } else if (arg_path[0] == '/') {
- new_path = argv[1];
- } else {
- StringBuilder builder;
- builder.append(cwd);
- builder.append('/');
- builder.append(arg_path);
- new_path = builder.to_string();
- }
- }
-
- LexicalPath lexical_path(new_path);
- if (!lexical_path.is_valid()) {
- printf("LexicalPath failed to canonicalize '%s'\n", new_path.characters());
- return 1;
- }
- const char* path = lexical_path.string().characters();
-
- struct stat st;
- int rc = stat(path, &st);
- if (rc < 0) {
- printf("stat(%s) failed: %s\n", path, strerror(errno));
- return 1;
- }
- if (!S_ISDIR(st.st_mode)) {
- printf("Not a directory: %s\n", path);
- return 1;
- }
- rc = chdir(path);
- if (rc < 0) {
- printf("chdir(%s) failed: %s\n", path, strerror(errno));
- return 1;
- }
- setenv("OLDPWD", cwd.characters(), 1);
- cwd = lexical_path.string();
- setenv("PWD", cwd.characters(), 1);
- return 0;
-}
-
-int Shell::builtin_cdh(int argc, const char** argv)
-{
- int index = -1;
-
- Core::ArgsParser parser;
- parser.add_positional_argument(index, "Index of the cd history entry (leave out for a list)", "index", Core::ArgsParser::Required::No);
-
- if (!parser.parse(argc, const_cast<char**>(argv), false))
- return 1;
-
- if (index == -1) {
- if (cd_history.size() == 0) {
- printf("cdh: no history available\n");
- return 0;
- }
-
- for (int i = cd_history.size() - 1; i >= 0; --i)
- printf("%lu: %s\n", cd_history.size() - i, cd_history.at(i).characters());
- return 0;
- }
-
- if (index < 1 || (size_t)index > cd_history.size()) {
- fprintf(stderr, "cdh: history index out of bounds: %d not in (0, %zu)\n", index, cd_history.size());
- return 1;
- }
-
- const char* path = cd_history.at(cd_history.size() - index).characters();
- const char* cd_args[] = { "cd", path };
- return Shell::builtin_cd(2, cd_args);
-}
-
-int Shell::builtin_dirs(int argc, const char** argv)
-{
- // The first directory in the stack is ALWAYS the current directory
- directory_stack.at(0) = cwd.characters();
-
- if (argc == 1) {
- for (auto& directory : directory_stack) {
- print_path(directory);
- fputc(' ', stdout);
- }
-
- printf("\n");
- return 0;
- }
-
- bool clear = false;
- bool print = false;
- bool number_when_printing = false;
-
- Vector<const char*> paths;
-
- Core::ArgsParser parser;
- parser.add_option(clear, "Clear the directory stack", "clear", 'c');
- parser.add_option(print, "Print directory entries", "print", 'p');
- parser.add_option(number_when_printing, "Number the directories in the stack when printing", "number", 'v');
- parser.add_positional_argument(paths, "Extra paths to put on the stack", "paths", Core::ArgsParser::Required::No);
-
- if (!parser.parse(argc, const_cast<char**>(argv), false))
- return 1;
-
- // -v implies -p
- print = print || number_when_printing;
-
- if (clear) {
- for (size_t i = 1; i < directory_stack.size(); i++)
- directory_stack.remove(i);
- }
-
- for (auto& path : paths)
- directory_stack.append(path);
-
- if (print) {
- auto idx = 0;
- for (auto& directory : directory_stack) {
- if (number_when_printing)
- printf("%d ", idx++);
- print_path(directory);
- fputc('\n', stdout);
- }
- }
-
- return 0;
-}
-
-int Shell::builtin_exit(int, const char**)
-{
- if (!jobs.is_empty()) {
- if (!m_should_ignore_jobs_on_next_exit) {
- printf("Shell: You have %zu active job%s, run 'exit' again to really exit.\n", jobs.size(), jobs.size() > 1 ? "s" : "");
- m_should_ignore_jobs_on_next_exit = true;
- return 1;
- }
- }
- stop_all_jobs();
- save_history();
- printf("Good-bye!\n");
- exit(0);
- return 0;
-}
-
-int Shell::builtin_export(int argc, const char** argv)
-{
- Vector<const char*> vars;
-
- Core::ArgsParser parser;
- parser.add_positional_argument(vars, "List of variable[=value]'s", "values", Core::ArgsParser::Required::No);
-
- if (!parser.parse(argc, const_cast<char**>(argv), false))
- return 1;
-
- if (vars.size() == 0) {
- for (int i = 0; environ[i]; ++i)
- puts(environ[i]);
- return 0;
- }
-
- int return_value = 0;
-
- for (auto& value : vars) {
- auto parts = String { value }.split_limit('=', 2);
-
- if (parts.size() == 1) {
- parts.append("");
- }
-
- int setenv_return = setenv(parts[0].characters(), parts[1].characters(), 1);
-
- if (setenv_return != 0) {
- perror("setenv");
- return_value = 1;
- break;
- }
-
- if (parts[0] == "PATH")
- cache_path();
- }
-
- return return_value;
-}
-
-int Shell::builtin_fg(int argc, const char** argv)
-{
- int job_id = -1;
-
- Core::ArgsParser parser;
- parser.add_positional_argument(job_id, "Job id to bring to foreground", "job_id", Core::ArgsParser::Required::No);
-
- if (!parser.parse(argc, const_cast<char**>(argv), false))
- return 1;
-
- if (job_id == -1 && !jobs.is_empty())
- job_id = find_last_job_id();
-
- Job* job = nullptr;
-
- for (auto& entry : jobs) {
- if (entry.value->job_id() == (u64)job_id) {
- job = entry.value;
- break;
- }
- }
- if (!job) {
- if (job_id == -1) {
- printf("fg: no current job\n");
- } else {
- printf("fg: job with id %d not found\n", job_id);
- }
- return 1;
- }
-
- job->set_running_in_background(false);
-
- dbg() << "Resuming " << job->pid() << " (" << job->cmd() << ")";
- printf("Resuming job %llu - %s\n", job->job_id(), job->cmd().characters());
-
- if (killpg(job->pgid(), SIGCONT) < 0) {
- perror("killpg");
- return 1;
- }
-
- int return_value = 0;
-
- auto current_pid = getpid();
- auto current_pgid = getpgid(current_pid);
-
- setpgid(job->pid(), job->pgid());
- tcsetpgrp(0, job->pgid());
- SpawnedProcess process { job->cmd(), job->pid() };
-
- do {
- if (wait_for_pid(process, true, return_value) == IterationDecision::Break)
- break;
- } while (errno == EINTR);
-
- setpgid(current_pid, current_pgid);
- tcsetpgrp(0, current_pgid);
-
- return return_value;
-}
-
-int Shell::builtin_disown(int argc, const char** argv)
-{
- Vector<const char*> str_job_ids;
-
- Core::ArgsParser parser;
- parser.add_positional_argument(str_job_ids, "Id of the jobs to disown (omit for current job)", "job_ids", Core::ArgsParser::Required::No);
-
- if (!parser.parse(argc, const_cast<char**>(argv), false))
- return 1;
-
- Vector<size_t> job_ids;
- for (auto& job_id : str_job_ids) {
- auto id = StringView(job_id).to_uint();
- if (id.has_value())
- job_ids.append(id.value());
- else
- printf("Invalid job id: %s\n", job_id);
- }
-
- if (job_ids.is_empty())
- job_ids.append(jobs.size() - 1);
-
- Vector<size_t> keys_of_jobs_to_disown;
-
- for (auto id : job_ids) {
- bool found = false;
- for (auto& entry : jobs) {
- if (entry.value->job_id() == id) {
- keys_of_jobs_to_disown.append(entry.key);
- found = true;
- break;
- }
- }
- if (!found) {
- printf("job with id %zu not found\n", id);
- }
- }
- if (keys_of_jobs_to_disown.is_empty()) {
- if (str_job_ids.is_empty()) {
- printf("disown: no current job\n");
- }
- // An error message has already been printed about the nonexistence of each listed job.
- return 1;
- }
- for (auto job_index : keys_of_jobs_to_disown) {
- auto job = jobs.get(job_index).value();
-
- job->deactivate();
-
- if (!job->is_running_in_background())
- printf("disown warning: job %llu is currently not running, 'kill -%d %d' to make it continue\n", job->job_id(), SIGCONT, job->pid());
-
- jobs.remove(job_index);
- }
-
- return 0;
-}
-
-int Shell::builtin_history(int, const char**)
-{
- for (size_t i = 0; i < editor->history().size(); ++i) {
- printf("%6zu %s\n", i, editor->history()[i].characters());
- }
- return 0;
-}
-
-int Shell::builtin_jobs(int argc, const char** argv)
-{
- bool list = false, show_pid = false;
-
- Core::ArgsParser parser;
- parser.add_option(list, "List all information about jobs", "list", 'l');
- parser.add_option(show_pid, "Display the PID of the jobs", "pid", 'p');
-
- if (!parser.parse(argc, const_cast<char**>(argv), false))
- return 1;
-
- enum {
- Basic,
- OnlyPID,
- ListAll,
- } mode { Basic };
-
- if (show_pid)
- mode = OnlyPID;
-
- if (list)
- mode = ListAll;
-
- for (auto& job : jobs) {
- auto pid = job.value->pid();
- int wstatus;
- auto rc = waitpid(pid, &wstatus, WNOHANG);
- if (rc == -1) {
- perror("waitpid");
- return 1;
- }
- auto status = "running";
-
- if (rc != 0) {
- if (WIFEXITED(wstatus))
- status = "exited";
-
- if (WIFSTOPPED(wstatus))
- status = "stopped";
-
- if (WIFSIGNALED(wstatus))
- status = "signaled";
- }
-
- char background_indicator = '-';
-
- if (job.value->is_running_in_background())
- background_indicator = '+';
-
- switch (mode) {
- case Basic:
- printf("[%llu] %c %s %s\n", job.value->job_id(), background_indicator, status, job.value->cmd().characters());
- break;
- case OnlyPID:
- printf("[%llu] %c %d %s %s\n", job.value->job_id(), background_indicator, pid, status, job.value->cmd().characters());
- break;
- case ListAll:
- printf("[%llu] %c %d %d %s %s\n", job.value->job_id(), background_indicator, pid, job.value->pgid(), status, job.value->cmd().characters());
- break;
- }
- }
-
- return 0;
-}
-
-int Shell::builtin_popd(int argc, const char** argv)
-{
- if (directory_stack.size() <= 1) {
- fprintf(stderr, "Shell: popd: directory stack empty\n");
- return 1;
- }
-
- bool should_not_switch = false;
- String path = directory_stack.take_last();
-
- Core::ArgsParser parser;
- parser.add_option(should_not_switch, "Do not switch dirs", "no-switch", 'n');
-
- if (!parser.parse(argc, const_cast<char**>(argv), false))
- return 1;
-
- bool should_switch = !should_not_switch;
-
- // When no arguments are given, popd removes the top directory from the stack and performs a cd to the new top directory.
- if (argc == 1) {
- int rc = chdir(path.characters());
- if (rc < 0) {
- fprintf(stderr, "chdir(%s) failed: %s\n", path.characters(), strerror(errno));
- return 1;
- }
-
- cwd = path;
- return 0;
- }
-
- LexicalPath lexical_path(path.characters());
- if (!lexical_path.is_valid()) {
- fprintf(stderr, "LexicalPath failed to canonicalize '%s'\n", path.characters());
- return 1;
- }
-
- const char* real_path = lexical_path.string().characters();
-
- struct stat st;
- int rc = stat(real_path, &st);
- if (rc < 0) {
- fprintf(stderr, "stat(%s) failed: %s\n", real_path, strerror(errno));
- return 1;
- }
-
- if (!S_ISDIR(st.st_mode)) {
- fprintf(stderr, "Not a directory: %s\n", real_path);
- return 1;
- }
-
- if (should_switch) {
- int rc = chdir(real_path);
- if (rc < 0) {
- fprintf(stderr, "chdir(%s) failed: %s\n", real_path, strerror(errno));
- return 1;
- }
-
- cwd = lexical_path.string();
- }
-
- return 0;
-}
-
-int Shell::builtin_pushd(int argc, const char** argv)
-{
- StringBuilder path_builder;
- bool should_switch = true;
-
- // From the BASH reference manual: https://www.gnu.org/software/bash/manual/html_node/Directory-Stack-Builtins.html
- // With no arguments, pushd exchanges the top two directories and makes the new top the current directory.
- if (argc == 1) {
- if (directory_stack.size() < 2) {
- fprintf(stderr, "pushd: no other directory\n");
- return 1;
- }
-
- String dir1 = directory_stack.take_first();
- String dir2 = directory_stack.take_first();
- directory_stack.insert(0, dir2);
- directory_stack.insert(1, dir1);
-
- int rc = chdir(dir2.characters());
- if (rc < 0) {
- fprintf(stderr, "chdir(%s) failed: %s\n", dir2.characters(), strerror(errno));
- return 1;
- }
-
- cwd = dir2;
-
- return 0;
- }
-
- // Let's assume the user's typed in 'pushd <dir>'
- if (argc == 2) {
- directory_stack.append(cwd.characters());
- if (argv[1][0] == '/') {
- path_builder.append(argv[1]);
- } else {
- path_builder.appendf("%s/%s", cwd.characters(), argv[1]);
- }
- } else if (argc == 3) {
- directory_stack.append(cwd.characters());
- for (int i = 1; i < argc; i++) {
- const char* arg = argv[i];
-
- if (arg[0] != '-') {
- if (arg[0] == '/') {
- path_builder.append(arg);
- } else
- path_builder.appendf("%s/%s", cwd.characters(), arg);
- }
-
- if (!strcmp(arg, "-n"))
- should_switch = false;
- }
- }
-
- LexicalPath lexical_path(path_builder.to_string());
- if (!lexical_path.is_valid()) {
- fprintf(stderr, "LexicalPath failed to canonicalize '%s'\n", path_builder.to_string().characters());
- return 1;
- }
-
- const char* real_path = lexical_path.string().characters();
-
- struct stat st;
- int rc = stat(real_path, &st);
- if (rc < 0) {
- fprintf(stderr, "stat(%s) failed: %s\n", real_path, strerror(errno));
- return 1;
- }
-
- if (!S_ISDIR(st.st_mode)) {
- fprintf(stderr, "Not a directory: %s\n", real_path);
- return 1;
- }
-
- if (should_switch) {
- int rc = chdir(real_path);
- if (rc < 0) {
- fprintf(stderr, "chdir(%s) failed: %s\n", real_path, strerror(errno));
- return 1;
- }
-
- cwd = lexical_path.string();
- }
-
- return 0;
-}
-
-int Shell::builtin_pwd(int, const char**)
-{
- print_path(cwd);
- fputc('\n', stdout);
- return 0;
-}
-
-int Shell::builtin_time(int argc, const char** argv)
-{
- Vector<const char*> args;
-
- Core::ArgsParser parser;
- parser.add_positional_argument(args, "Command to execute with arguments", "command", Core::ArgsParser::Required::Yes);
-
- if (!parser.parse(argc, const_cast<char**>(argv), false))
- return 1;
-
- StringBuilder builder;
- builder.join(' ', args);
-
- Core::ElapsedTimer timer;
- timer.start();
- auto exit_code = run_command(builder.string_view());
- if (!exit_code.has_value()) {
- printf("Shell: Incomplete command: %s\n", builder.to_string().characters());
- exit_code = 1;
- }
- printf("Time: %d ms\n", timer.elapsed());
- return exit_code.value();
-}
-
-int Shell::builtin_umask(int argc, const char** argv)
-{
- const char* mask_text = nullptr;
-
- Core::ArgsParser parser;
- parser.add_positional_argument(mask_text, "New mask (omit to get current mask)", "octal-mask", Core::ArgsParser::Required::No);
-
- if (!parser.parse(argc, const_cast<char**>(argv), false))
- return 1;
-
- if (!mask_text) {
- mode_t old_mask = umask(0);
- printf("%#o\n", old_mask);
- umask(old_mask);
- return 0;
- }
-
- unsigned mask;
- int matches = sscanf(mask_text, "%o", &mask);
- if (matches == 1) {
- umask(mask);
- return 0;
- }
-
- return 0;
-}
-
-int Shell::builtin_unset(int argc, const char** argv)
-{
- Vector<const char*> vars;
-
- Core::ArgsParser parser;
- parser.add_positional_argument(vars, "List of variables", "variables", Core::ArgsParser::Required::Yes);
-
- if (!parser.parse(argc, const_cast<char**>(argv), false))
- return 1;
-
- for (auto& value : vars)
- unsetenv(value);
-
- return 0;
-}
-
-bool Shell::run_builtin(int argc, const char** argv, int& retval)
-{
- if (argc == 0)
- return false;
-
- StringView name { argv[0] };
-
-#define __ENUMERATE_SHELL_BUILTIN(builtin) \
- if (name == #builtin) { \
- retval = builtin_##builtin(argc, argv); \
- return true; \
- }
-
- ENUMERATE_SHELL_BUILTINS();
-
-#undef __ENUMERATE_SHELL_BUILTIN
-
- return false;
+ return build_prompt();
}
String Shell::expand_tilde(const String& expression)
@@ -879,13 +176,12 @@ Vector<StringView> Shell::split_path(const StringView& path)
size_t substart = 0;
for (size_t i = 0; i < path.length(); i++) {
- char ch = path.characters_without_null_termination()[i];
+ char ch = path[i];
if (ch != '/')
continue;
size_t sublen = i - substart;
if (sublen != 0)
parts.append(path.substring_view(substart, sublen));
- parts.append(path.substring_view(i, 1));
substart = i + 1;
}
@@ -896,193 +192,121 @@ Vector<StringView> Shell::split_path(const StringView& path)
return parts;
}
-Vector<String> Shell::expand_globs(const StringView& path, const StringView& base)
+Vector<String> Shell::expand_globs(const StringView& path, StringView base)
{
+ if (path.starts_with('/'))
+ base = "/";
auto parts = split_path(path);
+ String base_string = base;
+ struct stat statbuf;
+ if (lstat(base_string.characters(), &statbuf) < 0) {
+ perror("lstat");
+ return {};
+ }
- StringBuilder builder;
- builder.append(base);
- Vector<String> res;
-
- for (size_t i = 0; i < parts.size(); ++i) {
- auto& part = parts[i];
- if (!is_glob(part)) {
- builder.append(part);
- continue;
- }
-
- // Found a glob.
- String new_base = builder.to_string();
- StringView new_base_v = new_base;
- if (new_base_v.is_empty())
- new_base_v = ".";
- Core::DirIterator di(new_base_v, Core::DirIterator::SkipParentAndBaseDir);
-
- if (di.has_error()) {
- return res;
- }
-
- while (di.has_next()) {
- String name = di.next_path();
-
- // Dotfiles have to be explicitly requested
- if (name[0] == '.' && part[0] != '.')
- continue;
+ StringBuilder resolved_base_path_builder;
+ resolved_base_path_builder.append(Core::File::real_path_for(base));
+ if (S_ISDIR(statbuf.st_mode))
+ resolved_base_path_builder.append('/');
- if (name.matches(part, CaseSensitivity::CaseSensitive)) {
+ auto resolved_base = resolved_base_path_builder.string_view();
- StringBuilder nested_base;
- nested_base.append(new_base);
- nested_base.append(name);
+ auto results = expand_globs(move(parts), resolved_base);
- StringView remaining_path = path.substring_view_starting_after_substring(part);
- Vector<String> nested_res = expand_globs(remaining_path, nested_base.to_string());
- for (auto& s : nested_res)
- res.append(s);
- }
- }
- return res;
+ for (auto& entry : results) {
+ entry = entry.substring(resolved_base.length(), entry.length() - resolved_base.length());
+ if (entry.is_empty())
+ entry = ".";
}
- // Found no globs.
- String new_path = builder.to_string();
- if (access(new_path.characters(), F_OK) == 0)
- res.append(new_path);
- return res;
-}
+ // Make the output predictable and nice.
+ quick_sort(results);
-Vector<String> Shell::expand_parameters(const StringView& param) const
-{
- if (!param.starts_with('$'))
- return { param };
-
- String variable_name = String(param.substring_view(1, param.length() - 1));
- if (variable_name == "?")
- return { String::number(last_return_code) };
- else if (variable_name == "$")
- return { String::number(getpid()) };
-
- char* env_value = getenv(variable_name.characters());
- if (env_value == nullptr)
- return { "" };
-
- 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 results;
}
-Vector<String> Shell::process_arguments(const Vector<Token>& args)
+Vector<String> Shell::expand_globs(Vector<StringView> path_segments, const StringView& base)
{
- Vector<String> argv_string;
- for (auto& arg : args) {
- if (arg.type == Token::Comment)
- continue;
-
- // This will return the text passed in if it wasn't a variable
- // This lets us just loop over its values
- auto expanded_parameters = expand_parameters(arg.text);
-
- for (auto& exp_arg : expanded_parameters) {
- if (exp_arg.starts_with('~'))
- exp_arg = expand_tilde(exp_arg);
-
- auto expanded_globs = expand_globs(exp_arg, "");
- for (auto& path : expanded_globs)
- argv_string.append(path);
-
- if (expanded_globs.is_empty())
- argv_string.append(exp_arg);
- }
+ if (path_segments.is_empty()) {
+ String base_str = base;
+ if (access(base_str.characters(), F_OK) == 0)
+ return { move(base_str) };
+ return {};
}
- return argv_string;
-}
+ auto first_segment = path_segments.take_first();
+ if (is_glob(first_segment)) {
+ Vector<String> result;
-ContinuationRequest Shell::is_complete(const Vector<Command>& commands)
-{
- // check if the last command ends with a pipe, or an unterminated string
- auto& last_command = commands.last();
- auto& subcommands = last_command.subcommands;
- if (subcommands.size() == 0)
- return ContinuationRequest::Nothing;
+ Core::DirIterator di(base, Core::DirIterator::SkipParentAndBaseDir);
+ if (di.has_error())
+ return {};
- auto& last_subcommand = subcommands.last();
+ while (di.has_next()) {
+ String path = di.next_path();
- if (!last_subcommand.redirections.find([](auto& redirection) { return redirection.type == Redirection::Pipe; }).is_end())
- return ContinuationRequest::Pipe;
+ // Dotfiles have to be explicitly requested
+ if (path[0] == '.' && first_segment[0] != '.')
+ continue;
- if (!last_subcommand.args.find([](auto& token) { return token.type == Token::UnterminatedSingleQuoted; }).is_end())
- return ContinuationRequest::SingleQuotedString;
+ if (path.matches(first_segment, CaseSensitivity::CaseSensitive)) {
+ StringBuilder builder;
+ builder.append(base);
+ if (!base.ends_with('/'))
+ builder.append('/');
+ builder.append(path);
+ result.append(expand_globs(path_segments, builder.string_view()));
+ }
+ }
- if (!last_subcommand.args.find([](auto& token) { return token.type == Token::UnterminatedDoubleQuoted; }).is_end())
- return ContinuationRequest::DoubleQuotedString;
+ return result;
+ } else {
+ StringBuilder builder;
+ builder.append(base);
+ if (!base.ends_with('/'))
+ builder.append('/');
+ builder.append(first_segment);
- return ContinuationRequest::Nothing;
+ return expand_globs(move(path_segments), builder.string_view());
+ }
}
-IterationDecision Shell::wait_for_pid(const Shell::SpawnedProcess& process, bool is_first_command_in_chain, int& return_value)
+String Shell::resolve_path(String path) const
{
- if (is_first_command_in_chain)
- m_waiting_for_pid = process.pid;
-
- int wstatus = 0;
- int rc = waitpid(process.pid, &wstatus, WSTOPPED);
- auto errno_save = errno;
-
- if (is_first_command_in_chain)
- m_waiting_for_pid = -1;
-
- errno = errno_save;
- if (rc < 0 && errno != EINTR) {
- if (errno != ECHILD)
- perror("waitpid");
- return IterationDecision::Break;
- }
-
- const Job* job = nullptr;
- u64 job_id = 0;
- auto maybe_job = jobs.get(process.pid);
- if (maybe_job.has_value()) {
- job = maybe_job.value();
- job_id = job->job_id();
- }
+ if (!path.starts_with('/'))
+ path = String::format("%s/%s", cwd.characters(), path.characters());
- if (WIFEXITED(wstatus)) {
- if (WEXITSTATUS(wstatus) != 0)
- dbg() << "Shell: " << process.name << ":" << process.pid << " exited with status " << WEXITSTATUS(wstatus);
+ return Core::File::real_path_for(path);
+}
- return_value = WEXITSTATUS(wstatus);
+RefPtr<AST::Value> Shell::lookup_local_variable(const String& name)
+{
+ auto value = m_local_variables.get(name).value_or(nullptr);
+ return value;
+}
- if (job) {
- auto* mutable_job = const_cast<Job*>(job);
- mutable_job->set_has_exit(return_value);
- Core::EventLoop::current().post_event(*this, make<Core::CustomEvent>(ChildExited, mutable_job));
- }
- return IterationDecision::Break;
+String Shell::local_variable_or(const String& name, const String& replacement)
+{
+ auto value = lookup_local_variable(name);
+ if (value) {
+ StringBuilder builder;
+ builder.join(" ", value->resolve_as_list(*this));
+ return builder.to_string();
}
+ return replacement;
+}
- if (WIFSTOPPED(wstatus)) {
- fprintf(stderr, "Shell: [%llu] %s(%d) %s\n", job_id, process.name.characters(), process.pid, strsignal(WSTOPSIG(wstatus)));
- return IterationDecision::Continue;
- }
+void Shell::set_local_variable(const String& name, RefPtr<AST::Value> value)
+{
+ m_local_variables.set(name, move(value));
+}
- if (WIFSIGNALED(wstatus)) {
- printf("Shell: [%llu] %s(%d) exited due to signal '%s'\n", job_id, process.name.characters(), process.pid, strsignal(WTERMSIG(wstatus)));
- } else {
- printf("Shell: [%llu] %s(%d) exited abnormally\n", job_id, process.name.characters(), process.pid);
- }
- if (job) {
- auto* mutable_job = const_cast<Job*>(job);
- mutable_job->set_has_exit(-1);
- Core::EventLoop::current().post_event(*this, make<Core::CustomEvent>(ChildExited, mutable_job));
- }
- return IterationDecision::Break;
+void Shell::unset_local_variable(const String& name)
+{
+ m_local_variables.remove(name);
}
-ExitCodeOrContinuationRequest Shell::run_command(const StringView& cmd)
+int Shell::run_command(const StringView& cmd)
{
if (cmd.is_empty())
return 0;
@@ -1090,278 +314,172 @@ ExitCodeOrContinuationRequest Shell::run_command(const StringView& cmd)
if (cmd.starts_with("#"))
return 0;
- auto commands = Parser(cmd).parse();
+ auto command = Parser(cmd).parse();
- if (!commands.size())
- return 1;
-
- auto needs_more = is_complete(commands);
- if (needs_more != ExitCodeOrContinuationRequest::Nothing)
- return needs_more;
+ if (!command)
+ return 0;
#ifdef SH_DEBUG
- for (auto& command : commands) {
- for (size_t i = 0; i < command.subcommands.size(); ++i) {
- for (size_t j = 0; j < i; ++j)
- dbgprintf(" ");
- for (auto& arg : command.subcommands[i].args) {
- switch (arg.type) {
- case Token::Bare:
- dbgprintf("<%s> ", arg.text.characters());
- break;
- case Token::SingleQuoted:
- dbgprintf("'<%s>' ", arg.text.characters());
- break;
- case Token::DoubleQuoted:
- dbgprintf("\"<%s>\" ", arg.text.characters());
- break;
- case Token::UnterminatedSingleQuoted:
- dbgprintf("\'<%s> ", arg.text.characters());
- break;
- case Token::UnterminatedDoubleQuoted:
- dbgprintf("\"<%s> ", arg.text.characters());
- break;
- case Token::Special:
- dbgprintf("<%s> ", arg.text.characters());
- break;
- case Token::Comment:
- dbgprintf("<%s> ", arg.text.characters());
- break;
- }
- }
-
- dbgprintf("\n");
- for (auto& redirecton : command.subcommands[i].redirections) {
- for (size_t j = 0; j < i; ++j)
- dbgprintf(" ");
- dbgprintf(" ");
- switch (redirecton.type) {
- case Redirection::Pipe:
- dbgprintf("Pipe\n");
- break;
- case Redirection::FileRead:
- dbgprintf("fd:%d = FileRead: %s\n", redirecton.fd, redirecton.path.text.characters());
- break;
- case Redirection::FileWrite:
- dbgprintf("fd:%d = FileWrite: %s\n", redirecton.fd, redirecton.path.text.characters());
- break;
- case Redirection::FileWriteAppend:
- dbgprintf("fd:%d = FileWriteAppend: %s\n", redirecton.fd, redirecton.path.text.characters());
- break;
- default:
- break;
- }
- }
- }
- if (auto attributes = command.attributes) {
- dbgprintf("\n ");
- if (attributes & Attributes::InBackground)
- dbgprintf("InBackground ");
- if (attributes & Attributes::ShortCircuitOnFailure)
- dbgprintf("ShortCircuitOnFailure ");
- }
- dbgprintf("\n");
- }
+ dbg() << "Command follows";
+ command->dump(0);
#endif
- struct termios trm;
- tcgetattr(0, &trm);
+ tcgetattr(0, &termios);
- int return_value = 0;
- bool fail_short_circuits = false;
+ auto result = command->run(*this);
+ if (result->is_job()) {
+ auto job_result = static_cast<AST::JobValue*>(result.ptr());
+ auto job = job_result->job();
+ if (!job)
+ last_return_code = 0;
+ else if (job->exited())
+ last_return_code = job->exit_code();
+ }
- for (auto& command : commands) {
- if (fail_short_circuits) {
- if (command.attributes & Attributes::ShortCircuitOnFailure)
- continue;
+ return last_return_code;
+}
+
+RefPtr<Job> Shell::run_command(AST::Command& command)
+{
+ FileDescriptionCollector fds;
- // Do not fail any command after this one, as we've reached the end of a short-circuit chain,
- // e.g. foo && bar && baz ; foobar
- // ^ we reached this command.
- fail_short_circuits = false;
+ // Resolve redirections.
+ Vector<AST::Rewiring> rewirings;
+ for (auto& redirection : command.redirections) {
+ auto rewiring_result = redirection->apply();
+ if (rewiring_result.is_error()) {
+ if (!rewiring_result.error().is_empty())
+ fprintf(stderr, "error: %s\n", rewiring_result.error().characters());
continue;
}
+ auto& rewiring = rewiring_result.value();
+ rewirings.append(rewiring);
- if (command.subcommands.is_empty())
- continue;
+ if (rewiring.must_be_closed == AST::Rewiring::Close::Source) {
+ fds.add(rewiring.source_fd);
+ } else if (rewiring.must_be_closed == AST::Rewiring::Close::Destination) {
+ fds.add(rewiring.dest_fd);
+ }
+ }
- FileDescriptionCollector fds;
-
- for (size_t i = 0; i < command.subcommands.size(); ++i) {
- auto& subcommand = command.subcommands[i];
- for (auto& redirection : subcommand.redirections) {
- switch (redirection.type) {
- case Redirection::Pipe: {
- int pipefd[2];
- int rc = pipe(pipefd);
- if (rc < 0) {
- perror("pipe");
- return 1;
- }
- subcommand.rewirings.append({ STDOUT_FILENO, pipefd[1] });
- auto& next_command = command.subcommands[i + 1];
- next_command.rewirings.append({ STDIN_FILENO, pipefd[0] });
- fds.add(pipefd[0]);
- fds.add(pipefd[1]);
- break;
- }
- case Redirection::FileWriteAppend: {
- int fd = open(redirection.path.text.characters(), O_WRONLY | O_CREAT | O_APPEND, 0666);
- if (fd < 0) {
- perror("open");
- return 1;
- }
- subcommand.rewirings.append({ redirection.fd, fd });
- fds.add(fd);
- break;
- }
- case Redirection::FileWrite: {
- int fd = open(redirection.path.text.characters(), O_WRONLY | O_CREAT | O_TRUNC, 0666);
- if (fd < 0) {
- perror("open");
- return 1;
- }
- subcommand.rewirings.append({ redirection.fd, fd });
- fds.add(fd);
- break;
- }
- case Redirection::FileRead: {
- int fd = open(redirection.path.text.characters(), O_RDONLY);
- if (fd < 0) {
- perror("open");
- return 1;
- }
- subcommand.rewirings.append({ redirection.fd, fd });
- fds.add(fd);
- break;
- }
- }
+ // If the command is empty, do all the rewirings in the current process and return.
+ // This allows the user to mess with the shell internals, but is apparently useful?
+ // We'll just allow the users to shoot themselves until they get tired of doing so.
+ if (command.argv.is_empty()) {
+ for (auto& rewiring : rewirings) {
+#ifdef SH_DEBUG
+ dbgprintf("in %d, dup2(%d, %d)\n", getpid(), rewiring.dest_fd, rewiring.source_fd);
+#endif
+ int rc = dup2(rewiring.dest_fd, rewiring.source_fd);
+ if (rc < 0) {
+ perror("dup2");
+ return nullptr;
}
}
- Vector<SpawnedProcess> children;
+ fds.collect();
+ return nullptr;
+ }
- for (size_t i = 0; i < command.subcommands.size(); ++i) {
- auto& subcommand = command.subcommands[i];
- Vector<String> argv_string = process_arguments(subcommand.args);
- Vector<const char*> argv;
- argv.ensure_capacity(argv_string.size());
- for (const auto& s : argv_string) {
- argv.append(s.characters());
- }
- argv.append(nullptr);
+ Vector<const char*> argv;
+ Vector<String> copy_argv = command.argv;
+ argv.ensure_capacity(command.argv.size() + 1);
-#ifdef SH_DEBUG
- for (auto& arg : argv) {
- dbgprintf("<%s> ", arg);
- }
- dbgprintf("\n");
-#endif
+ for (auto& arg : copy_argv)
+ argv.append(arg.characters());
+
+ argv.append(nullptr);
- int retval = 0;
- if (run_builtin(argv.size() - 1, argv.data(), retval))
- return retval;
+ int retval = 0;
+ if (run_builtin(argv.size() - 1, argv.data(), retval))
+ return nullptr;
- pid_t child = fork();
- if (!child) {
- setpgid(0, 0);
- tcsetpgrp(0, getpid());
- tcsetattr(0, TCSANOW, &default_termios);
- for (auto& rewiring : subcommand.rewirings) {
+ pid_t child = fork();
+ if (!child) {
+ setpgid(0, 0);
+ tcsetpgrp(0, getpid());
+ tcsetattr(0, TCSANOW, &default_termios);
+ for (auto& rewiring : rewirings) {
#ifdef SH_DEBUG
- dbgprintf("in %s<%d>, dup2(%d, %d)\n", argv[0], getpid(), rewiring.rewire_fd, rewiring.fd);
+ dbgprintf("in %s<%d>, dup2(%d, %d)\n", argv[0], getpid(), rewiring.dest_fd, rewiring.source_fd);
#endif
- int rc = dup2(rewiring.rewire_fd, rewiring.fd);
- if (rc < 0) {
- perror("dup2");
- return 1;
- }
- }
+ int rc = dup2(rewiring.dest_fd, rewiring.source_fd);
+ if (rc < 0) {
+ perror("dup2");
+ return nullptr;
+ }
+ }
- fds.collect();
-
- int rc = execvp(argv[0], const_cast<char* const*>(argv.data()));
- if (rc < 0) {
- if (errno == ENOENT) {
- int shebang_fd = open(argv[0], O_RDONLY);
- auto close_argv = ScopeGuard([shebang_fd]() { if (shebang_fd >= 0) close(shebang_fd); });
- char shebang[256] {};
- ssize_t num_read = -1;
- if ((shebang_fd >= 0) && ((num_read = read(shebang_fd, shebang, sizeof(shebang))) >= 2) && (StringView(shebang).starts_with("#!"))) {
- StringView shebang_path_view(&shebang[2], num_read - 2);
- Optional<size_t> newline_pos = shebang_path_view.find_first_of("\n\r");
- shebang[newline_pos.has_value() ? (newline_pos.value() + 2) : num_read] = '\0';
- fprintf(stderr, "%s: Invalid interpreter \"%s\": %s\n", argv[0], &shebang[2], strerror(ENOENT));
- } else
- fprintf(stderr, "%s: Command not found.\n", argv[0]);
- } else {
- int saved_errno = errno;
- struct stat st;
- if (stat(argv[0], &st) == 0 && S_ISDIR(st.st_mode)) {
- fprintf(stderr, "Shell: %s: Is a directory\n", argv[0]);
- _exit(126);
- }
- fprintf(stderr, "execvp(%s): %s\n", argv[0], strerror(saved_errno));
- }
+ fds.collect();
+
+ int rc = execvp(argv[0], const_cast<char* const*>(argv.data()));
+ if (rc < 0) {
+ if (errno == ENOENT) {
+ int shebang_fd = open(argv[0], O_RDONLY);
+ auto close_argv = ScopeGuard([shebang_fd]() { if (shebang_fd >= 0) close(shebang_fd); });
+ char shebang[256] {};
+ ssize_t num_read = -1;
+ if ((shebang_fd >= 0) && ((num_read = read(shebang_fd, shebang, sizeof(shebang))) >= 2) && (StringView(shebang).starts_with("#!"))) {
+ StringView shebang_path_view(&shebang[2], num_read - 2);
+ Optional<size_t> newline_pos = shebang_path_view.find_first_of("\n\r");
+ shebang[newline_pos.has_value() ? (newline_pos.value() + 2) : num_read] = '\0';
+ fprintf(stderr, "%s: Invalid interpreter \"%s\": %s\n", argv[0], &shebang[2], strerror(ENOENT));
+ } else
+ fprintf(stderr, "%s: Command not found.\n", argv[0]);
+ } else {
+ int saved_errno = errno;
+ struct stat st;
+ if (stat(argv[0], &st) == 0 && S_ISDIR(st.st_mode)) {
+ fprintf(stderr, "Shell: %s: Is a directory\n", argv[0]);
_exit(126);
}
- ASSERT_NOT_REACHED();
+ fprintf(stderr, "execvp(%s): %s\n", argv[0], strerror(saved_errno));
}
- children.append({ argv[0], child });
-
- StringBuilder cmd;
- cmd.join(" ", argv_string);
-
- auto job = make<Job>(child, (unsigned)child, cmd.build(), find_last_job_id() + 1);
- jobs.set((u64)child, move(job));
+ _exit(126);
}
+ ASSERT_NOT_REACHED();
+ }
-#ifdef SH_DEBUG
- dbgprintf("Closing fds in shell process:\n");
-#endif
- fds.collect();
-
-#ifdef SH_DEBUG
- dbgprintf("Now we gotta wait on children:\n");
- for (auto& child : children)
- dbgprintf(" %d (%s)\n", child.pid, child.name.characters());
-#endif
+ StringBuilder cmd;
+ cmd.join(" ", command.argv);
- if (command.attributes & Attributes::InBackground) {
- // Set the jobs as running in background and continue without waiting.
- for (auto& child : children)
- const_cast<Job*>(jobs.get(child.pid).value())->set_running_in_background(true);
+ auto job = adopt(*new Job(child, (unsigned)child, cmd.build(), find_last_job_id() + 1));
+ jobs.set((u64)child, job);
- continue;
- }
+ job->on_exit = [](auto job) {
+ if (job->is_running_in_background())
+ fprintf(stderr, "Shell: Job %d(%s) exited\n", job->pid(), job->cmd().characters());
+ job->disown();
+ };
- for (size_t i = 0; i < children.size(); ++i) {
- auto& child = children[i];
- dbg() << "Now waiting for " << child.name << " (" << child.pid << ")";
- do {
- if (wait_for_pid(child, i != children.size() - 1, return_value) == IterationDecision::Break)
- break;
- } while (errno == EINTR);
- }
+ return *job;
+}
- if (command.attributes & Attributes::ShortCircuitOnFailure) {
- if (return_value != 0) {
- fail_short_circuits = true;
- }
- }
- }
+void Shell::take_back_stdin()
+{
+ tcsetpgrp(0, m_pid);
+ tcsetattr(0, TCSANOW, &termios);
+}
- last_return_code = return_value;
+void Shell::block_on_job(RefPtr<Job> job)
+{
+ if (!job)
+ return;
- // FIXME: Should I really have to tcsetpgrp() after my child has exited?
- // Is the terminal controlling pgrp really still the PGID of the dead process?
- tcsetpgrp(0, getpid());
- tcsetattr(0, TCSANOW, &trm);
+ Core::EventLoop loop;
+ job->on_exit = [&, old_exit = move(job->on_exit)](auto job) {
+ if (old_exit)
+ old_exit(job);
+ loop.quit(0);
+ };
+ if (job->exited()) {
+ take_back_stdin();
+ return;
+ }
- // Clear the exit flag after any non-exit command has been executed.
- m_should_ignore_jobs_on_next_exit = false;
+ loop.exec();
- return return_value;
+ take_back_stdin();
}
String Shell::get_history_path()
@@ -1467,8 +585,11 @@ void Shell::cache_path()
while (programs.has_next()) {
auto program = programs.next_path();
String program_path = String::format("%s/%s", directory.characters(), program.characters());
+ auto escaped_name = escape_token(program);
+ if (cached_path.contains_slow(escaped_name))
+ continue;
if (access(program_path.characters(), X_OK) == 0)
- cached_path.append(escape_token(program.characters()));
+ cached_path.append(escaped_name);
}
}
@@ -1481,111 +602,12 @@ void Shell::cache_path()
void Shell::highlight(Line::Editor& editor) const
{
- StringBuilder builder;
- bool is_offset_by_string_start = false;
- if (m_should_continue == ExitCodeOrContinuationRequest::DoubleQuotedString) {
- builder.append('"');
- is_offset_by_string_start = true;
- }
- if (m_should_continue == ExitCodeOrContinuationRequest::SingleQuotedString) {
- builder.append('\'');
- is_offset_by_string_start = true;
- }
- builder.append(editor.line());
- auto commands = Parser { builder.string_view() }.parse();
- auto first_command { true };
- for (auto& command : commands) {
- for (auto& subcommand : command.subcommands) {
- auto& redirections = subcommand.redirections;
- for (auto& redirection : redirections) {
- if (redirection.type == Redirection::Pipe)
- continue;
- if (redirection.path.length == 0)
- continue;
- Line::Style redirection_style { Line::Style::Foreground(0x87, 0x9b, 0xcd) }; // 25% darkened periwinkle :)
- auto end = redirection.path.end;
- auto redirection_op_start = redirection.redirection_op_start;
- if (is_offset_by_string_start) {
- end--;
- redirection_op_start--;
- }
-
- editor.stylize({ redirection_op_start, end }, redirection_style);
- }
- auto first { true };
- for (auto& arg : subcommand.args) {
- auto start = arg.end - arg.length;
-
- if (arg.type == Token::Comment) {
- editor.stylize({ start, arg.end }, { Line::Style::Foreground(150, 150, 150) }); // light gray
- continue;
- }
-
- if (m_should_continue == ExitCodeOrContinuationRequest::DoubleQuotedString || m_should_continue == ExitCodeOrContinuationRequest::SingleQuotedString) {
- if (!first_command)
- --start;
- --arg.end;
- }
- if (first) {
- first = false;
- // only treat this as a command name if we're not continuing strings
- if (!first_command || (m_should_continue == ExitCodeOrContinuationRequest::Nothing || m_should_continue == ExitCodeOrContinuationRequest::Pipe)) {
- editor.stylize({ start, arg.end }, { Line::Style::Bold });
- first_command = false;
- continue;
- }
- first_command = false;
- }
-
- if (arg.type == Token::SingleQuoted || arg.type == Token::UnterminatedSingleQuoted) {
- editor.stylize({ start - 1, arg.end + (arg.type != Token::UnterminatedSingleQuoted) }, { Line::Style::Foreground(Line::Style::XtermColor::Yellow) });
- continue;
- }
-
- if (arg.type == Token::DoubleQuoted || arg.type == Token::UnterminatedDoubleQuoted) {
- editor.stylize({ start - 1, arg.end + (arg.type != Token::UnterminatedDoubleQuoted) }, { Line::Style::Foreground(Line::Style::XtermColor::Yellow) });
- if constexpr (HighlightVariablesInsideStrings)
- goto highlight_variables;
- else
- continue;
- }
-
- if (is_glob(arg.text)) {
- editor.stylize({ start, arg.end }, { Line::Style::Foreground(59, 142, 234) }); // bright-ish blue
- continue;
- }
-
- if (arg.text.starts_with("--")) {
- if (arg.length == 2)
- editor.stylize({ start, arg.end }, { Line::Style::Foreground(Line::Style::XtermColor::Green) });
- else
- editor.stylize({ start, arg.end }, { Line::Style::Foreground(Line::Style::XtermColor::Cyan) });
- } else if (arg.text.starts_with("-") && arg.length > 1) {
- editor.stylize({ start, arg.end }, { Line::Style::Foreground(Line::Style::XtermColor::Cyan) });
- }
-
- highlight_variables:;
-
- size_t slice_index = 0;
- Optional<size_t> maybe_index;
- while (slice_index < arg.length) {
- maybe_index = arg.text.substring_view(slice_index, arg.length - slice_index).find_first_of('$');
- if (!maybe_index.has_value())
- break;
- auto index = maybe_index.value() + 1;
- auto end_index = index;
- if (index >= arg.length)
- break;
- for (; end_index < arg.length; ++end_index) {
- if (!is_word_character(arg.text[end_index]))
- break;
- }
- editor.stylize({ index + start - 1, end_index + start }, { Line::Style::Foreground(214, 112, 214) });
- slice_index = end_index + 1;
- }
- }
- }
- }
+ auto line = editor.line();
+ Parser parser(line);
+ auto ast = parser.parse();
+ if (!ast)
+ return;
+ ast->highlight_in_editor(editor, const_cast<Shell&>(*this));
}
Vector<Line::CompletionSuggestion> Shell::complete(const Line::Editor& editor)
@@ -1594,95 +616,19 @@ Vector<Line::CompletionSuggestion> Shell::complete(const Line::Editor& editor)
Parser parser(line);
- auto commands = parser.parse();
+ auto ast = parser.parse();
- if (commands.size() == 0)
+ if (!ast)
return {};
- // Get the last token and whether it's the first in its subcommand.
- String token;
- bool is_first_in_subcommand = false;
- auto& subcommand = commands.last().subcommands;
- String file_token_trail = " ";
- String directory_token_trail = "/";
-
- if (subcommand.size() == 0) {
- // foo bar; <tab>
- token = "";
- is_first_in_subcommand = true;
- } else {
- auto& last_command = subcommand.last();
- if (!last_command.redirections.is_empty() && last_command.redirections.last().type != Redirection::Pipe) {
- // foo > bar<tab>
- const auto& redirection = last_command.redirections.last();
- const auto& path = redirection.path;
-
- if (path.end != line.length())
- return {};
-
- token = path.text;
- is_first_in_subcommand = false;
- if (path.type == Token::UnterminatedDoubleQuoted)
- file_token_trail = "\"";
- else if (path.type == Token::UnterminatedSingleQuoted)
- file_token_trail = "'";
- } else {
- if (last_command.args.size() == 0) {
- // foo bar | <tab>
- token = "";
- is_first_in_subcommand = true;
- } else {
- auto& args = last_command.args;
- if (args.last().type == Token::Comment) // we cannot complete comments
- return {};
-
- if (args.last().end != line.length()) {
- // There was a token separator at the end
- is_first_in_subcommand = false;
- token = "";
- } else {
- is_first_in_subcommand = args.size() == 1;
- token = last_command.args.last().text;
- }
- }
- }
- }
-
- Vector<Line::CompletionSuggestion> suggestions;
-
- bool should_suggest_only_executables = false;
-
- if (is_first_in_subcommand) {
- auto match = binary_search(cached_path.data(), cached_path.size(), token, [](const String& token, const String& program) -> int {
- return strncmp(token.characters(), program.characters(), token.length());
- });
-
- if (match) {
- String completion = *match;
- editor.suggest(escape_token(token).length(), 0);
-
- // Now that we have a program name starting with our token, we look at
- // other program names starting with our token and cut off any mismatching
- // characters.
-
- int index = match - cached_path.data();
- for (int i = index - 1; i >= 0 && cached_path[i].starts_with(token); --i) {
- suggestions.append({ cached_path[i], " " });
- }
- for (size_t i = index + 1; i < cached_path.size() && cached_path[i].starts_with(token); ++i) {
- suggestions.append({ cached_path[i], " " });
- }
- suggestions.append({ cached_path[index], " " });
-
- return suggestions;
- }
-
- // fallthrough to suggesting local files, but make sure to only suggest executables
- should_suggest_only_executables = true;
- }
+ return ast->complete_for_editor(*this, line.length());
+}
+Vector<Line::CompletionSuggestion> Shell::complete_path(const String& part, size_t offset)
+{
+ auto token = part.substring_view(0, offset);
+ StringView original_token = token;
String path;
- String original_token = token;
ssize_t last_slash = token.length() - 1;
while (last_slash >= 0 && token[last_slash] != '/')
@@ -1691,11 +637,11 @@ Vector<Line::CompletionSuggestion> Shell::complete(const Line::Editor& editor)
if (last_slash >= 0) {
// Split on the last slash. We'll use the first part as the directory
// to search and the second part as the token to complete.
- path = token.substring(0, last_slash + 1);
+ path = token.substring_view(0, last_slash + 1);
if (path[0] != '/')
path = String::format("%s/%s", cwd.characters(), path.characters());
path = LexicalPath::canonicalized_path(path);
- token = token.substring(last_slash + 1, token.length() - last_slash - 1);
+ token = token.substring_view(last_slash + 1, token.length() - last_slash - 1);
} else {
// We have no slashes, so the directory to search is the current
// directory and the token to complete is just the original token.
@@ -1707,12 +653,14 @@ Vector<Line::CompletionSuggestion> Shell::complete(const Line::Editor& editor)
// since we are not suggesting anything starting with
// `/foo/', but rather just `bar...'
auto token_length = escape_token(token).length();
- editor.suggest(token_length, original_token.length() - token_length);
+ editor->suggest(token_length, original_token.length() - token_length);
// only suggest dot-files if path starts with a dot
Core::DirIterator files(path,
token.starts_with('.') ? Core::DirIterator::SkipParentAndBaseDir : Core::DirIterator::SkipDots);
+ Vector<Line::CompletionSuggestion> suggestions;
+
while (files.has_next()) {
auto file = files.next_path();
if (file.starts_with(token)) {
@@ -1721,10 +669,9 @@ Vector<Line::CompletionSuggestion> Shell::complete(const Line::Editor& editor)
int stat_error = stat(file_path.characters(), &program_status);
if (!stat_error) {
if (S_ISDIR(program_status.st_mode)) {
- if (!should_suggest_only_executables)
- suggestions.append({ escape_token(file), directory_token_trail, { Line::Style::Hyperlink(String::format("file://%s", file_path.characters())), Line::Style::Anchored } });
+ suggestions.append({ escape_token(file), "/" });
} else {
- suggestions.append({ escape_token(file), file_token_trail, { Line::Style::Hyperlink(String::format("file://%s", file_path.characters())), Line::Style::Anchored } });
+ suggestions.append({ escape_token(file), " " });
}
}
}
@@ -1733,26 +680,79 @@ Vector<Line::CompletionSuggestion> Shell::complete(const Line::Editor& editor)
return suggestions;
}
+Vector<Line::CompletionSuggestion> Shell::complete_program_name(const String& name, size_t offset)
+{
+ auto match = binary_search(cached_path.data(), cached_path.size(), name, [](const String& name, const String& program) -> int {
+ return strncmp(name.characters(), program.characters(), name.length());
+ });
+
+ if (!match)
+ return complete_path(name, offset);
+
+ String completion = *match;
+ editor->suggest(escape_token(name).length(), 0);
+
+ // Now that we have a program name starting with our token, we look at
+ // other program names starting with our token and cut off any mismatching
+ // characters.
+
+ Vector<Line::CompletionSuggestion> suggestions;
+
+ int index = match - cached_path.data();
+ for (int i = index - 1; i >= 0 && cached_path[i].starts_with(name); --i) {
+ suggestions.append({ cached_path[i], " " });
+ }
+ for (size_t i = index + 1; i < cached_path.size() && cached_path[i].starts_with(name); ++i) {
+ suggestions.append({ cached_path[i], " " });
+ }
+ suggestions.append({ cached_path[index], " " });
+
+ return suggestions;
+}
+
+Vector<Line::CompletionSuggestion> Shell::complete_variable(const String& name, size_t offset)
+{
+ Vector<Line::CompletionSuggestion> suggestions;
+ auto pattern = name.substring_view(0, offset);
+
+ editor->suggest(offset);
+
+ // Look at local variables.
+ for (auto& variable : m_local_variables) {
+ if (variable.key.starts_with(pattern))
+ suggestions.append(variable.key);
+ }
+
+ // Look at the environment.
+ for (auto i = 0; environ[i]; ++i) {
+ auto entry = StringView { environ[i] };
+ if (entry.starts_with(pattern)) {
+ auto parts = entry.split_view('=');
+ if (parts.is_empty() || parts.first().is_empty())
+ continue;
+ String name = parts.first();
+ if (suggestions.contains_slow(name))
+ continue;
+ suggestions.append(move(name));
+ }
+ }
+
+ return suggestions;
+}
+
bool Shell::read_single_line()
{
+ take_back_stdin();
auto line_result = editor->get_line(prompt());
if (line_result.is_error()) {
if (line_result.error() == Line::Editor::Error::Eof || line_result.error() == Line::Editor::Error::Empty) {
// Pretend the user tried to execute builtin_exit()
- // but only if there's no continuation.
- if (m_should_continue == ContinuationRequest::Nothing) {
- m_complete_line_builder.clear();
- run_command("exit");
- return read_single_line();
- } else {
- // Ignore the Eof.
- return true;
- }
+ m_complete_line_builder.clear();
+ run_command("exit");
+ return read_single_line();
} else {
m_complete_line_builder.clear();
- m_should_continue = ContinuationRequest::Nothing;
- m_should_break_current_command = false;
Core::EventLoop::current().quit(1);
return false;
}
@@ -1760,13 +760,6 @@ bool Shell::read_single_line()
auto& line = line_result.value();
- if (m_should_break_current_command) {
- m_complete_line_builder.clear();
- m_should_continue = ContinuationRequest::Nothing;
- m_should_break_current_command = false;
- return true;
- }
-
if (line.is_empty())
return true;
@@ -1774,11 +767,7 @@ bool Shell::read_single_line()
m_complete_line_builder.append("\n");
m_complete_line_builder.append(line);
- auto complete_or_exit_code = run_command(m_complete_line_builder.string_view());
- m_should_continue = complete_or_exit_code.continuation;
-
- if (!complete_or_exit_code.has_value())
- return true;
+ run_command(m_complete_line_builder.string_view());
editor->add_to_history(m_complete_line_builder.build());
m_complete_line_builder.clear();
@@ -1793,17 +782,6 @@ void Shell::custom_event(Core::CustomEvent& event)
return;
}
- if (event.custom_type() == ChildExited) {
- auto* job_ptr = event.data();
- if (job_ptr) {
- auto& job = *(Job*)job_ptr;
- if (job.is_running_in_background())
- fprintf(stderr, "Shell: Job %d(%s) exited\n", job.pid(), job.cmd().characters());
- jobs.remove(job.pid());
- }
- return;
- }
-
event.ignore();
}
@@ -1811,6 +789,7 @@ Shell::Shell()
{
uid = getuid();
tcsetpgrp(0, getpgrp());
+ m_pid = getpid();
int rc = gethostname(hostname, Shell::HostNameSize);
if (rc < 0)
@@ -1895,6 +874,15 @@ u64 Shell::find_last_job_id() const
return job_id;
}
+const Job* Shell::find_job(u64 id)
+{
+ for (auto& entry : jobs) {
+ if (entry.value->job_id() == id)
+ return entry.value;
+ }
+ return nullptr;
+}
+
void Shell::save_to(JsonObject& object)
{
Core::Object::save_to(object);
diff --git a/Shell/Shell.h b/Shell/Shell.h
index dfaf2871ff..cfdf06a860 100644
--- a/Shell/Shell.h
+++ b/Shell/Shell.h
@@ -39,37 +39,6 @@
#include <LibLine/Editor.h>
#include <termios.h>
-struct ExitCodeOrContinuationRequest {
- enum ContinuationRequest {
- Nothing,
- Pipe,
- DoubleQuotedString,
- SingleQuotedString,
- };
-
- ExitCodeOrContinuationRequest(ContinuationRequest continuation)
- : continuation(continuation)
- {
- }
-
- ExitCodeOrContinuationRequest(int exit)
- : exit_code(exit)
- {
- }
-
- bool has_value() const { return exit_code.has_value(); }
- int value() const
- {
- ASSERT(has_value());
- return exit_code.value();
- }
-
- Optional<int> exit_code;
- ContinuationRequest continuation { Nothing };
-};
-
-using ContinuationRequest = ExitCodeOrContinuationRequest::ContinuationRequest;
-
#define ENUMERATE_SHELL_BUILTINS() \
__ENUMERATE_SHELL_BUILTIN(cd) \
__ENUMERATE_SHELL_BUILTIN(cdh) \
@@ -94,13 +63,21 @@ class Shell : public Core::Object {
C_OBJECT(Shell);
public:
- ExitCodeOrContinuationRequest run_command(const StringView&);
+ int run_command(const StringView&);
+ RefPtr<Job> run_command(AST::Command&);
bool run_builtin(int argc, const char** argv, int& retval);
+ void block_on_job(RefPtr<Job>);
String prompt() const;
static String expand_tilde(const String&);
- static Vector<String> expand_globs(const StringView& path, const StringView& base);
- Vector<String> expand_parameters(const StringView&) const;
+ static Vector<String> expand_globs(const StringView& path, StringView base);
+ static Vector<String> expand_globs(Vector<StringView> path_segments, const StringView& base);
+ String resolve_path(String) const;
+
+ RefPtr<AST::Value> lookup_local_variable(const String&);
+ String local_variable_or(const String&, const String&);
+ void set_local_variable(const String&, RefPtr<AST::Value>);
+ void unset_local_variable(const String&);
static String escape_token(const String& token);
static String unescape_token(const String& token);
@@ -108,25 +85,22 @@ public:
static bool is_glob(const StringView&);
static Vector<StringView> split_path(const StringView&);
- Vector<String> process_arguments(const Vector<Token>&);
-
- static ContinuationRequest is_complete(const Vector<Command>&);
-
void highlight(Line::Editor&) const;
Vector<Line::CompletionSuggestion> complete(const Line::Editor&);
+ Vector<Line::CompletionSuggestion> complete_path(const String&, size_t offset);
+ Vector<Line::CompletionSuggestion> complete_program_name(const String&, size_t offset);
+ Vector<Line::CompletionSuggestion> complete_variable(const String&, size_t offset);
- bool is_waiting_for(pid_t pid) const { return m_waiting_for_pid == pid; }
+ void take_back_stdin();
u64 find_last_job_id() const;
+ const Job* find_job(u64 id);
String get_history_path();
void load_history();
void save_history();
void print_path(const String& path);
- bool should_read_more() const { return m_should_continue != ContinuationRequest::Nothing; }
- void finish_command() { m_should_break_current_command = true; }
-
bool read_single_line();
struct termios termios;
@@ -148,12 +122,11 @@ public:
int last_return_code { 0 };
Vector<String> directory_stack;
CircularQueue<String, 8> cd_history; // FIXME: have a configurable cd history length
- HashMap<u64, OwnPtr<Job>> jobs;
+ HashMap<u64, RefPtr<Job>> jobs;
Vector<String, 256> cached_path;
enum ShellEventType {
ReadLine,
- ChildExited,
};
private:
@@ -163,16 +136,9 @@ private:
// ^Core::Object
virtual void save_to(JsonObject&) override;
- struct SpawnedProcess {
- String name;
- pid_t pid;
- };
-
void cache_path();
void stop_all_jobs();
- IterationDecision wait_for_pid(const SpawnedProcess&, bool is_first_command_in_chain, int& return_value);
-
virtual void custom_event(Core::CustomEvent&) override;
#define __ENUMERATE_SHELL_BUILTIN(builtin) \
@@ -190,11 +156,11 @@ private:
#undef __ENUMERATE_SHELL_BUILTIN
};
- ExitCodeOrContinuationRequest::ContinuationRequest m_should_continue { ExitCodeOrContinuationRequest::Nothing };
StringBuilder m_complete_line_builder;
- bool m_should_break_current_command { false };
bool m_should_ignore_jobs_on_next_exit { false };
- pid_t m_waiting_for_pid { -1 };
+ pid_t m_pid { 0 };
+
+ HashMap<String, RefPtr<AST::Value>> m_local_variables;
};
static constexpr bool is_word_character(char c)
diff --git a/Shell/main.cpp b/Shell/main.cpp
index d6f6f7c436..e3dcb0b5ad 100644
--- a/Shell/main.cpp
+++ b/Shell/main.cpp
@@ -73,17 +73,22 @@ int main(int argc, char** argv)
signal(SIGCHLD, [](int) {
auto& jobs = s_shell->jobs;
+ Vector<u64> disowned_jobs;
for (auto& job : jobs) {
- if (s_shell->is_waiting_for(job.value->pid()))
- continue;
int wstatus = 0;
auto child_pid = waitpid(job.value->pid(), &wstatus, WNOHANG);
if (child_pid == job.value->pid()) {
- if (WIFEXITED(wstatus) || WIFSIGNALED(wstatus)) {
- Core::EventLoop::current().post_event(*s_shell, make<Core::CustomEvent>(Shell::ShellEventType::ChildExited, job.value));
+ if (WIFEXITED(wstatus)) {
+ job.value->set_has_exit(WEXITSTATUS(wstatus));
+ } else if (WIFSIGNALED(wstatus) && !WIFSTOPPED(wstatus)) {
+ job.value->set_has_exit(126);
}
}
+ if (job.value->should_be_disowned())
+ disowned_jobs.append(job.key);
}
+ for (auto key : disowned_jobs)
+ jobs.remove(key);
});
// Ignore SIGTSTP as the shell should not be suspended with ^Z.
@@ -133,10 +138,7 @@ int main(int argc, char** argv)
}
editor->on_interrupt_handled = [&] {
- if (shell->should_read_more()) {
- shell->finish_command();
- editor->finish();
- }
+ editor->finish();
};
shell->add_child(*editor);