diff options
author | AnotherTest <ali.mpfard@gmail.com> | 2020-06-17 18:05:06 +0430 |
---|---|---|
committer | Andreas Kling <kling@serenityos.org> | 2020-07-05 15:43:14 +0200 |
commit | a4627f24390035a8a8402e019be42b03f525f034 (patch) | |
tree | 5f4d8832790d8a899d43f08ba89a81fe7b3647c5 | |
parent | 6f7ac5d2e29d3dc764a565c2fb6425af8ee673d3 (diff) | |
download | serenity-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.cpp | 1708 | ||||
-rw-r--r-- | Shell/AST.h | 815 | ||||
-rw-r--r-- | Shell/Builtin.cpp | 701 | ||||
-rw-r--r-- | Shell/CMakeLists.txt | 2 | ||||
-rw-r--r-- | Shell/Forward.h | 35 | ||||
-rw-r--r-- | Shell/Job.h | 16 | ||||
-rw-r--r-- | Shell/Parser.cpp | 966 | ||||
-rw-r--r-- | Shell/Parser.h | 203 | ||||
-rw-r--r-- | Shell/Shell.cpp | 1646 | ||||
-rw-r--r-- | Shell/Shell.h | 74 | ||||
-rw-r--r-- | Shell/main.cpp | 18 |
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); |