mirror of
https://github.com/RGBCube/serenity
synced 2025-07-27 04:57:45 +00:00
Shell: Move to Userland/Shell/
This commit is contained in:
parent
07c7e35372
commit
c4e2fd8123
35 changed files with 11 additions and 9 deletions
2996
Userland/Shell/AST.cpp
Normal file
2996
Userland/Shell/AST.cpp
Normal file
File diff suppressed because it is too large
Load diff
1324
Userland/Shell/AST.h
Normal file
1324
Userland/Shell/AST.h
Normal file
File diff suppressed because it is too large
Load diff
929
Userland/Shell/Builtin.cpp
Normal file
929
Userland/Shell/Builtin.cpp
Normal file
|
@ -0,0 +1,929 @@
|
|||
/*
|
||||
* 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/LexicalPath.h>
|
||||
#include <AK/ScopeGuard.h>
|
||||
#include <LibCore/ArgsParser.h>
|
||||
#include <LibCore/EventLoop.h>
|
||||
#include <LibCore/File.h>
|
||||
#include <inttypes.h>
|
||||
#include <signal.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
extern char** environ;
|
||||
|
||||
namespace Shell {
|
||||
|
||||
int Shell::builtin_alias(int argc, const char** argv)
|
||||
{
|
||||
Vector<const char*> arguments;
|
||||
|
||||
Core::ArgsParser parser;
|
||||
parser.add_positional_argument(arguments, "List of name[=values]'s", "name[=value]", Core::ArgsParser::Required::No);
|
||||
|
||||
if (!parser.parse(argc, const_cast<char**>(argv), false))
|
||||
return 1;
|
||||
|
||||
if (arguments.is_empty()) {
|
||||
for (auto& alias : m_aliases)
|
||||
printf("%s=%s\n", escape_token(alias.key).characters(), escape_token(alias.value).characters());
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool fail = false;
|
||||
for (auto& argument : arguments) {
|
||||
auto parts = String { argument }.split_limit('=', 2, true);
|
||||
if (parts.size() == 1) {
|
||||
auto alias = m_aliases.get(parts[0]);
|
||||
if (alias.has_value()) {
|
||||
printf("%s=%s\n", escape_token(parts[0]).characters(), escape_token(alias.value()).characters());
|
||||
} else {
|
||||
fail = true;
|
||||
}
|
||||
} else {
|
||||
m_aliases.set(parts[0], parts[1]);
|
||||
add_entry_to_cache(parts[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return fail ? 1 : 0;
|
||||
}
|
||||
|
||||
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);
|
||||
job->set_should_announce_exit(true);
|
||||
job->set_is_suspended(false);
|
||||
|
||||
dbgln("Resuming {} ({})", job->pid(), job->cmd());
|
||||
warnln("Resuming job {} - {}", job->job_id(), job->cmd().characters());
|
||||
|
||||
// Try using the PGID, but if that fails, just use the PID.
|
||||
if (killpg(job->pgid(), SIGCONT) < 0) {
|
||||
if (kill(job->pid(), SIGCONT) < 0) {
|
||||
perror("kill");
|
||||
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;
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
|
||||
if (cd_history.is_empty() || cd_history.last() != real_path)
|
||||
cd_history.enqueue(real_path);
|
||||
|
||||
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_exec(int argc, const char** argv)
|
||||
{
|
||||
if (argc < 2) {
|
||||
fprintf(stderr, "Shell: No command given to exec\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
Vector<const char*> argv_vector;
|
||||
argv_vector.append(argv + 1, argc - 1);
|
||||
argv_vector.append(nullptr);
|
||||
|
||||
execute_process(move(argv_vector));
|
||||
}
|
||||
|
||||
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 (m_is_interactive) {
|
||||
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();
|
||||
m_editor->save_history(get_history_path());
|
||||
if (m_is_interactive)
|
||||
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_glob(int argc, const char** argv)
|
||||
{
|
||||
Vector<const char*> globs;
|
||||
Core::ArgsParser parser;
|
||||
parser.add_positional_argument(globs, "Globs to resolve", "glob");
|
||||
|
||||
if (!parser.parse(argc, const_cast<char**>(argv), false))
|
||||
return 1;
|
||||
|
||||
for (auto& glob : globs) {
|
||||
for (auto& expanded : expand_globs(glob, cwd))
|
||||
outln("{}", expanded);
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
RefPtr<Job> 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);
|
||||
job->set_is_suspended(false);
|
||||
|
||||
dbgln("Resuming {} ({})", job->pid(), job->cmd());
|
||||
warnln("Resuming job {} - {}", job->job_id(), job->cmd().characters());
|
||||
|
||||
tcsetpgrp(STDOUT_FILENO, job->pgid());
|
||||
tcsetpgrp(STDIN_FILENO, job->pgid());
|
||||
|
||||
// Try using the PGID, but if that fails, just use the PID.
|
||||
if (killpg(job->pgid(), SIGCONT) < 0) {
|
||||
if (kill(job->pid(), SIGCONT) < 0) {
|
||||
perror("kill");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
block_on_job(job);
|
||||
|
||||
if (job->exited())
|
||||
return job->exit_code();
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
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())
|
||||
job_ids.append(find_last_job_id());
|
||||
|
||||
Vector<const Job*> jobs_to_disown;
|
||||
|
||||
for (auto id : job_ids) {
|
||||
auto job = find_job(id);
|
||||
if (!job)
|
||||
fprintf(stderr, "disown: job with id %zu not found\n", id);
|
||||
else
|
||||
jobs_to_disown.append(job);
|
||||
}
|
||||
|
||||
if (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 : jobs_to_disown) {
|
||||
job->deactivate();
|
||||
|
||||
if (!job->is_running_in_background())
|
||||
fprintf(stderr, "disown warning: job %" PRIu64 " is currently not running, 'kill -%d %d' to make it continue\n", job->job_id(), SIGCONT, job->pid());
|
||||
|
||||
jobs.remove(job->pid());
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Shell::builtin_history(int, const char**)
|
||||
{
|
||||
for (size_t i = 0; i < m_editor->history().size(); ++i) {
|
||||
printf("%6zu %s\n", i, m_editor->history()[i].entry.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;
|
||||
|
||||
Job::PrintStatusMode mode = Job::PrintStatusMode::Basic;
|
||||
|
||||
if (show_pid)
|
||||
mode = Job::PrintStatusMode::OnlyPID;
|
||||
|
||||
if (list)
|
||||
mode = Job::PrintStatusMode::ListAll;
|
||||
|
||||
for (auto& it : jobs) {
|
||||
if (!it.value->print_status(mode))
|
||||
return 1;
|
||||
}
|
||||
|
||||
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_setopt(int argc, const char** argv)
|
||||
{
|
||||
if (argc == 1) {
|
||||
#define __ENUMERATE_SHELL_OPTION(name, default_, description) \
|
||||
if (options.name) \
|
||||
fprintf(stderr, #name "\n");
|
||||
|
||||
ENUMERATE_SHELL_OPTIONS();
|
||||
|
||||
#undef __ENUMERATE_SHELL_OPTION
|
||||
}
|
||||
|
||||
Core::ArgsParser parser;
|
||||
#define __ENUMERATE_SHELL_OPTION(name, default_, description) \
|
||||
bool name = false; \
|
||||
bool not_##name = false; \
|
||||
parser.add_option(name, "Enable: " description, #name, '\0'); \
|
||||
parser.add_option(not_##name, "Disable: " description, "no_" #name, '\0');
|
||||
|
||||
ENUMERATE_SHELL_OPTIONS();
|
||||
|
||||
#undef __ENUMERATE_SHELL_OPTION
|
||||
|
||||
if (!parser.parse(argc, const_cast<char**>(argv), false))
|
||||
return 1;
|
||||
|
||||
#define __ENUMERATE_SHELL_OPTION(name, default_, description) \
|
||||
if (name) \
|
||||
options.name = true; \
|
||||
if (not_##name) \
|
||||
options.name = false;
|
||||
|
||||
ENUMERATE_SHELL_OPTIONS();
|
||||
|
||||
#undef __ENUMERATE_SHELL_OPTION
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Shell::builtin_shift(int argc, const char** argv)
|
||||
{
|
||||
int count = 1;
|
||||
|
||||
Core::ArgsParser parser;
|
||||
parser.add_positional_argument(count, "Shift count", "count", Core::ArgsParser::Required::No);
|
||||
|
||||
if (!parser.parse(argc, const_cast<char**>(argv), false))
|
||||
return 1;
|
||||
|
||||
if (count < 1)
|
||||
return 0;
|
||||
|
||||
auto argv_ = lookup_local_variable("ARGV");
|
||||
if (!argv_) {
|
||||
fprintf(stderr, "shift: ARGV is unset\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!argv_->is_list())
|
||||
argv_ = adopt(*new AST::ListValue({ argv_.release_nonnull() }));
|
||||
|
||||
auto& values = static_cast<AST::ListValue*>(argv_.ptr())->values();
|
||||
if ((size_t)count > values.size()) {
|
||||
fprintf(stderr, "shift: shift count must not be greater than %zu\n", values.size());
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (auto i = 0; i < count; ++i)
|
||||
values.take_first();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Shell::builtin_source(int argc, const char** argv)
|
||||
{
|
||||
const char* file_to_source = nullptr;
|
||||
Vector<const char*> args;
|
||||
|
||||
Core::ArgsParser parser;
|
||||
parser.add_positional_argument(file_to_source, "File to read commands from", "path");
|
||||
parser.add_positional_argument(args, "ARGV for the sourced file", "args", Core::ArgsParser::Required::No);
|
||||
|
||||
if (!parser.parse(argc, const_cast<char**>(argv)))
|
||||
return 1;
|
||||
|
||||
Vector<String> string_argv;
|
||||
for (auto& arg : args)
|
||||
string_argv.append(arg);
|
||||
|
||||
auto previous_argv = lookup_local_variable("ARGV");
|
||||
ScopeGuard guard { [&] {
|
||||
if (!args.is_empty())
|
||||
set_local_variable("ARGV", move(previous_argv));
|
||||
} };
|
||||
|
||||
if (!args.is_empty())
|
||||
set_local_variable("ARGV", AST::create<AST::ListValue>(move(string_argv)));
|
||||
|
||||
if (!run_file(file_to_source, true))
|
||||
return 126;
|
||||
|
||||
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;
|
||||
|
||||
AST::Command command;
|
||||
for (auto& arg : args)
|
||||
command.argv.append(arg);
|
||||
|
||||
auto commands = expand_aliases({ move(command) });
|
||||
|
||||
Core::ElapsedTimer timer;
|
||||
int exit_code = 1;
|
||||
timer.start();
|
||||
for (auto& job : run_commands(commands)) {
|
||||
block_on_job(job);
|
||||
exit_code = job.exit_code();
|
||||
}
|
||||
fprintf(stderr, "Time: %d ms\n", timer.elapsed());
|
||||
return exit_code;
|
||||
}
|
||||
|
||||
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_wait(int argc, const char** argv)
|
||||
{
|
||||
Vector<const char*> job_ids;
|
||||
|
||||
Core::ArgsParser parser;
|
||||
parser.add_positional_argument(job_ids, "Job IDs to wait for, defaults to all jobs if missing", "jobs", Core::ArgsParser::Required::No);
|
||||
|
||||
if (!parser.parse(argc, const_cast<char**>(argv), false))
|
||||
return 1;
|
||||
|
||||
Vector<NonnullRefPtr<Job>> jobs_to_wait_for;
|
||||
|
||||
if (job_ids.is_empty()) {
|
||||
for (auto it : jobs)
|
||||
jobs_to_wait_for.append(it.value);
|
||||
} else {
|
||||
for (String id_s : job_ids) {
|
||||
auto id_opt = id_s.to_uint();
|
||||
if (id_opt.has_value()) {
|
||||
if (auto job = find_job(id_opt.value())) {
|
||||
jobs_to_wait_for.append(*job);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
warnln("wait: invalid or nonexistent job id {}", id_s);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& job : jobs_to_wait_for) {
|
||||
job->set_running_in_background(false);
|
||||
block_on_job(job);
|
||||
}
|
||||
|
||||
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) {
|
||||
if (lookup_local_variable(value)) {
|
||||
unset_local_variable(value);
|
||||
} else {
|
||||
unsetenv(value);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool Shell::run_builtin(const AST::Command& command, const NonnullRefPtrVector<AST::Rewiring>& rewirings, int& retval)
|
||||
{
|
||||
if (command.argv.is_empty())
|
||||
return false;
|
||||
|
||||
if (!has_builtin(command.argv.first()))
|
||||
return false;
|
||||
|
||||
Vector<const char*> argv;
|
||||
for (auto& arg : command.argv)
|
||||
argv.append(arg.characters());
|
||||
|
||||
argv.append(nullptr);
|
||||
|
||||
StringView name = command.argv.first();
|
||||
|
||||
SavedFileDescriptors fds { rewirings };
|
||||
|
||||
for (auto& rewiring : rewirings) {
|
||||
int rc = dup2(rewiring.old_fd, rewiring.new_fd);
|
||||
if (rc < 0) {
|
||||
perror("dup2(run)");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Core::EventLoop loop;
|
||||
setup_signals();
|
||||
|
||||
#define __ENUMERATE_SHELL_BUILTIN(builtin) \
|
||||
if (name == #builtin) { \
|
||||
retval = builtin_##builtin(argv.size() - 1, argv.data()); \
|
||||
if (!has_error(ShellError::None)) \
|
||||
raise_error(m_error, m_error_description, command.position); \
|
||||
return true; \
|
||||
}
|
||||
|
||||
ENUMERATE_SHELL_BUILTINS();
|
||||
|
||||
#undef __ENUMERATE_SHELL_BUILTIN
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Shell::has_builtin(const StringView& name) const
|
||||
{
|
||||
#define __ENUMERATE_SHELL_BUILTIN(builtin) \
|
||||
if (name == #builtin) { \
|
||||
return true; \
|
||||
}
|
||||
|
||||
ENUMERATE_SHELL_BUILTINS();
|
||||
|
||||
#undef __ENUMERATE_SHELL_BUILTIN
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
19
Userland/Shell/CMakeLists.txt
Normal file
19
Userland/Shell/CMakeLists.txt
Normal file
|
@ -0,0 +1,19 @@
|
|||
set(SOURCES
|
||||
AST.cpp
|
||||
Builtin.cpp
|
||||
Formatter.cpp
|
||||
Job.cpp
|
||||
NodeVisitor.cpp
|
||||
Parser.cpp
|
||||
Shell.cpp
|
||||
)
|
||||
|
||||
serenity_lib(LibShell shell)
|
||||
target_link_libraries(LibShell LibCore LibLine)
|
||||
|
||||
set(SOURCES
|
||||
main.cpp
|
||||
)
|
||||
|
||||
serenity_bin(Shell)
|
||||
target_link_libraries(Shell LibShell)
|
65
Userland/Shell/Execution.h
Normal file
65
Userland/Shell/Execution.h
Normal file
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||
* 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 <AK/Forward.h>
|
||||
#include <AK/NonnullRefPtrVector.h>
|
||||
#include <AK/String.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibCore/ElapsedTimer.h>
|
||||
|
||||
namespace Shell {
|
||||
|
||||
class FileDescriptionCollector {
|
||||
public:
|
||||
FileDescriptionCollector() { }
|
||||
~FileDescriptionCollector();
|
||||
|
||||
void collect();
|
||||
void add(int fd);
|
||||
|
||||
private:
|
||||
Vector<int, 32> m_fds;
|
||||
};
|
||||
|
||||
class SavedFileDescriptors {
|
||||
public:
|
||||
SavedFileDescriptors(const NonnullRefPtrVector<AST::Rewiring>&);
|
||||
~SavedFileDescriptors();
|
||||
|
||||
private:
|
||||
struct SavedFileDescriptor {
|
||||
int original { -1 };
|
||||
int saved { -1 };
|
||||
};
|
||||
|
||||
Vector<SavedFileDescriptor> m_saves;
|
||||
FileDescriptionCollector m_collector;
|
||||
};
|
||||
|
||||
}
|
755
Userland/Shell/Formatter.cpp
Normal file
755
Userland/Shell/Formatter.cpp
Normal file
|
@ -0,0 +1,755 @@
|
|||
/*
|
||||
* 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 "Formatter.h"
|
||||
#include "AST.h"
|
||||
#include "Parser.h"
|
||||
#include <AK/TemporaryChange.h>
|
||||
|
||||
namespace Shell {
|
||||
|
||||
String Formatter::format()
|
||||
{
|
||||
auto node = Parser(m_source).parse();
|
||||
if (m_cursor >= 0)
|
||||
m_output_cursor = m_cursor;
|
||||
|
||||
if (!node)
|
||||
return String();
|
||||
|
||||
if (node->is_syntax_error())
|
||||
return m_source;
|
||||
|
||||
if (m_cursor >= 0) {
|
||||
auto hit_test = node->hit_test_position(m_cursor);
|
||||
if (hit_test.matching_node)
|
||||
m_hit_node = hit_test.matching_node.ptr();
|
||||
else
|
||||
m_hit_node = nullptr;
|
||||
}
|
||||
|
||||
m_parent_node = nullptr;
|
||||
|
||||
node->visit(*this);
|
||||
|
||||
auto string = m_builder.string_view();
|
||||
|
||||
if (!string.ends_with(" "))
|
||||
m_builder.append(m_trivia);
|
||||
|
||||
return m_builder.to_string();
|
||||
}
|
||||
|
||||
void Formatter::with_added_indent(int indent, Function<void()> callback)
|
||||
{
|
||||
TemporaryChange indent_change { m_current_indent, m_current_indent + indent };
|
||||
callback();
|
||||
}
|
||||
|
||||
void Formatter::in_new_block(Function<void()> callback)
|
||||
{
|
||||
current_builder().append('{');
|
||||
|
||||
with_added_indent(1, [&] {
|
||||
insert_separator();
|
||||
callback();
|
||||
});
|
||||
|
||||
insert_separator();
|
||||
current_builder().append('}');
|
||||
}
|
||||
|
||||
void Formatter::test_and_update_output_cursor(const AST::Node* node)
|
||||
{
|
||||
if (!node)
|
||||
return;
|
||||
|
||||
if (node != m_hit_node)
|
||||
return;
|
||||
|
||||
m_output_cursor = current_builder().length() + m_cursor - node->position().start_offset;
|
||||
}
|
||||
|
||||
void Formatter::visited(const AST::Node* node)
|
||||
{
|
||||
m_last_visited_node = node;
|
||||
}
|
||||
|
||||
void Formatter::will_visit(const AST::Node* node)
|
||||
{
|
||||
if (!m_last_visited_node)
|
||||
return;
|
||||
|
||||
if (!node)
|
||||
return;
|
||||
|
||||
auto direct_sequence_child = !m_parent_node || m_parent_node->kind() == AST::Node::Kind::Sequence;
|
||||
|
||||
if (direct_sequence_child && node->kind() != AST::Node::Kind::Sequence) {
|
||||
// Collapse more than one empty line to a single one.
|
||||
if (node->position().start_line.line_number - m_last_visited_node->position().end_line.line_number > 1)
|
||||
current_builder().append('\n');
|
||||
}
|
||||
}
|
||||
|
||||
void Formatter::insert_separator()
|
||||
{
|
||||
current_builder().append('\n');
|
||||
insert_indent();
|
||||
}
|
||||
|
||||
void Formatter::insert_indent()
|
||||
{
|
||||
for (size_t i = 0; i < m_current_indent; ++i)
|
||||
current_builder().append(" ");
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::PathRedirectionNode* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
|
||||
NodeVisitor::visit(node);
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::And* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
auto should_indent = m_parent_node && m_parent_node->kind() != AST::Node::Kind::And;
|
||||
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
|
||||
|
||||
with_added_indent(should_indent ? 1 : 0, [&] {
|
||||
node->left()->visit(*this);
|
||||
|
||||
current_builder().append(" \\");
|
||||
insert_separator();
|
||||
current_builder().append("&& ");
|
||||
|
||||
node->right()->visit(*this);
|
||||
});
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::ListConcatenate* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
|
||||
|
||||
auto first = true;
|
||||
for (auto& subnode : node->list()) {
|
||||
if (!first)
|
||||
current_builder().append(' ');
|
||||
first = false;
|
||||
subnode->visit(*this);
|
||||
}
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::Background* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
|
||||
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
|
||||
NodeVisitor::visit(node);
|
||||
current_builder().append(" &");
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::BarewordLiteral* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
current_builder().append(node->text());
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::BraceExpansion* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
current_builder().append('{');
|
||||
|
||||
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
|
||||
bool first = true;
|
||||
for (auto& entry : node->entries()) {
|
||||
if (!first)
|
||||
current_builder().append(',');
|
||||
first = false;
|
||||
entry.visit(*this);
|
||||
}
|
||||
|
||||
current_builder().append('}');
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::CastToCommand* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
if (m_options.explicit_parentheses)
|
||||
current_builder().append('(');
|
||||
|
||||
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
|
||||
NodeVisitor::visit(node);
|
||||
|
||||
if (m_options.explicit_parentheses)
|
||||
current_builder().append(')');
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::CastToList* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
current_builder().append('(');
|
||||
|
||||
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
|
||||
NodeVisitor::visit(node);
|
||||
|
||||
current_builder().append(')');
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::CloseFdRedirection* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
|
||||
|
||||
current_builder().appendf(" %d>&-", node->fd());
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::CommandLiteral*)
|
||||
{
|
||||
ASSERT_NOT_REACHED();
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::Comment* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
current_builder().append("#");
|
||||
current_builder().append(node->text());
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::ContinuationControl* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
if (node->continuation_kind() == AST::ContinuationControl::Break)
|
||||
current_builder().append("break");
|
||||
else if (node->continuation_kind() == AST::ContinuationControl::Continue)
|
||||
current_builder().append("continue");
|
||||
else
|
||||
ASSERT_NOT_REACHED();
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::DynamicEvaluate* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
current_builder().append('$');
|
||||
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
|
||||
NodeVisitor::visit(node);
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::DoubleQuotedString* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
current_builder().append("\"");
|
||||
|
||||
TemporaryChange quotes { m_options.in_double_quotes, true };
|
||||
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
|
||||
|
||||
NodeVisitor::visit(node);
|
||||
|
||||
current_builder().append("\"");
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::Fd2FdRedirection* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
|
||||
|
||||
current_builder().appendf(" %d>&%d", node->source_fd(), node->dest_fd());
|
||||
if (m_hit_node == node)
|
||||
++m_output_cursor;
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::FunctionDeclaration* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
current_builder().append(node->name().name);
|
||||
current_builder().append('(');
|
||||
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
|
||||
|
||||
auto first = true;
|
||||
for (auto& arg : node->arguments()) {
|
||||
if (!first)
|
||||
current_builder().append(' ');
|
||||
first = false;
|
||||
current_builder().append(arg.name);
|
||||
}
|
||||
|
||||
current_builder().append(") ");
|
||||
|
||||
in_new_block([&] {
|
||||
if (node->block())
|
||||
node->block()->visit(*this);
|
||||
});
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::ForLoop* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
auto is_loop = node->iterated_expression().is_null();
|
||||
current_builder().append(is_loop ? "loop" : "for ");
|
||||
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
|
||||
|
||||
if (!is_loop) {
|
||||
if (node->variable_name() != "it") {
|
||||
current_builder().append(node->variable_name());
|
||||
current_builder().append(" in ");
|
||||
}
|
||||
|
||||
node->iterated_expression()->visit(*this);
|
||||
}
|
||||
|
||||
current_builder().append(' ');
|
||||
in_new_block([&] {
|
||||
if (node->block())
|
||||
node->block()->visit(*this);
|
||||
});
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::Glob* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
current_builder().append(node->text());
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::Execute* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
auto& builder = current_builder();
|
||||
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
|
||||
ScopedValueRollback options_rollback { m_options };
|
||||
|
||||
if (node->does_capture_stdout()) {
|
||||
builder.append("$");
|
||||
m_options.explicit_parentheses = true;
|
||||
}
|
||||
|
||||
NodeVisitor::visit(node);
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::IfCond* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
|
||||
current_builder().append("if ");
|
||||
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
|
||||
|
||||
node->condition()->visit(*this);
|
||||
|
||||
current_builder().append(' ');
|
||||
|
||||
in_new_block([&] {
|
||||
if (node->true_branch())
|
||||
node->true_branch()->visit(*this);
|
||||
});
|
||||
|
||||
if (node->false_branch()) {
|
||||
current_builder().append(" else ");
|
||||
if (node->false_branch()->kind() != AST::Node::Kind::IfCond) {
|
||||
in_new_block([&] {
|
||||
node->false_branch()->visit(*this);
|
||||
});
|
||||
} else {
|
||||
node->false_branch()->visit(*this);
|
||||
}
|
||||
} else if (node->else_position().has_value()) {
|
||||
current_builder().append(" else ");
|
||||
}
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::Join* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
auto should_parenthesise = m_options.explicit_parentheses;
|
||||
|
||||
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
|
||||
TemporaryChange parens { m_options.explicit_parentheses, false };
|
||||
|
||||
if (should_parenthesise)
|
||||
current_builder().append('(');
|
||||
|
||||
NodeVisitor::visit(node);
|
||||
|
||||
if (should_parenthesise)
|
||||
current_builder().append(')');
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::MatchExpr* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
current_builder().append("match ");
|
||||
|
||||
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
|
||||
|
||||
node->matched_expr()->visit(*this);
|
||||
|
||||
if (!node->expr_name().is_empty()) {
|
||||
current_builder().append(" as ");
|
||||
current_builder().append(node->expr_name());
|
||||
}
|
||||
|
||||
current_builder().append(' ');
|
||||
in_new_block([&] {
|
||||
auto first_entry = true;
|
||||
for (auto& entry : node->entries()) {
|
||||
if (!first_entry)
|
||||
insert_separator();
|
||||
first_entry = false;
|
||||
auto first = true;
|
||||
for (auto& option : entry.options) {
|
||||
if (!first)
|
||||
current_builder().append(" | ");
|
||||
first = false;
|
||||
option.visit(*this);
|
||||
}
|
||||
|
||||
current_builder().append(' ');
|
||||
if (entry.match_names.has_value() && !entry.match_names.value().is_empty()) {
|
||||
current_builder().append("as (");
|
||||
auto first = true;
|
||||
for (auto& name : entry.match_names.value()) {
|
||||
if (!first)
|
||||
current_builder().append(' ');
|
||||
first = false;
|
||||
current_builder().append(name);
|
||||
}
|
||||
current_builder().append(") ");
|
||||
}
|
||||
in_new_block([&] {
|
||||
if (entry.body)
|
||||
entry.body->visit(*this);
|
||||
});
|
||||
}
|
||||
});
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::Or* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
auto should_indent = m_parent_node && m_parent_node->kind() != AST::Node::Kind::Or;
|
||||
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
|
||||
|
||||
with_added_indent(should_indent ? 1 : 0, [&] {
|
||||
node->left()->visit(*this);
|
||||
|
||||
current_builder().append(" \\");
|
||||
insert_separator();
|
||||
current_builder().append("|| ");
|
||||
|
||||
node->right()->visit(*this);
|
||||
});
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::Pipe* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
auto should_indent = m_parent_node && m_parent_node->kind() != AST::Node::Kind::Pipe;
|
||||
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
|
||||
|
||||
node->left()->visit(*this);
|
||||
current_builder().append(" \\");
|
||||
|
||||
with_added_indent(should_indent ? 1 : 0, [&] {
|
||||
insert_separator();
|
||||
current_builder().append("| ");
|
||||
|
||||
node->right()->visit(*this);
|
||||
});
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::Range* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
current_builder().append('{');
|
||||
|
||||
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
|
||||
node->start()->visit(*this);
|
||||
current_builder().append("..");
|
||||
node->end()->visit(*this);
|
||||
|
||||
current_builder().append('}');
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::ReadRedirection* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
|
||||
|
||||
if (node->fd() != 0)
|
||||
current_builder().appendf(" %d<", node->fd());
|
||||
else
|
||||
current_builder().append(" <");
|
||||
NodeVisitor::visit(node);
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::ReadWriteRedirection* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
|
||||
|
||||
if (node->fd() != 0)
|
||||
current_builder().appendf(" %d<>", node->fd());
|
||||
else
|
||||
current_builder().append(" <>");
|
||||
NodeVisitor::visit(node);
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::Sequence* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
|
||||
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
|
||||
node->left()->visit(*this);
|
||||
insert_separator();
|
||||
|
||||
node->right()->visit(*this);
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::Subshell* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
|
||||
|
||||
in_new_block([&] {
|
||||
insert_separator();
|
||||
NodeVisitor::visit(node);
|
||||
insert_separator();
|
||||
});
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::SimpleVariable* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
current_builder().append('$');
|
||||
current_builder().append(node->name());
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::SpecialVariable* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
current_builder().append('$');
|
||||
current_builder().append(node->name());
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::Juxtaposition* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
|
||||
NodeVisitor::visit(node);
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::StringLiteral* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
if (!m_options.in_double_quotes)
|
||||
current_builder().append("'");
|
||||
|
||||
if (m_options.in_double_quotes) {
|
||||
for (auto ch : node->text()) {
|
||||
switch (ch) {
|
||||
case '"':
|
||||
case '\\':
|
||||
case '$':
|
||||
current_builder().append('\\');
|
||||
break;
|
||||
case '\n':
|
||||
current_builder().append("\\n");
|
||||
continue;
|
||||
case '\r':
|
||||
current_builder().append("\\r");
|
||||
continue;
|
||||
case '\t':
|
||||
current_builder().append("\\t");
|
||||
continue;
|
||||
case '\v':
|
||||
current_builder().append("\\v");
|
||||
continue;
|
||||
case '\f':
|
||||
current_builder().append("\\f");
|
||||
continue;
|
||||
case '\a':
|
||||
current_builder().append("\\a");
|
||||
continue;
|
||||
case '\e':
|
||||
current_builder().append("\\e");
|
||||
continue;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
current_builder().append(ch);
|
||||
}
|
||||
} else {
|
||||
current_builder().append(node->text());
|
||||
}
|
||||
|
||||
if (!m_options.in_double_quotes)
|
||||
current_builder().append("'");
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::StringPartCompose* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
|
||||
NodeVisitor::visit(node);
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::SyntaxError* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
|
||||
NodeVisitor::visit(node);
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::Tilde* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
current_builder().append(node->text());
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::VariableDeclarations* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
|
||||
|
||||
auto first = true;
|
||||
for (auto& entry : node->variables()) {
|
||||
if (!first)
|
||||
current_builder().append(' ');
|
||||
first = false;
|
||||
entry.name->visit(*this);
|
||||
current_builder().append('=');
|
||||
|
||||
if (entry.value->is_command())
|
||||
current_builder().append('(');
|
||||
|
||||
entry.value->visit(*this);
|
||||
|
||||
if (entry.value->is_command())
|
||||
current_builder().append(')');
|
||||
}
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::WriteAppendRedirection* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
|
||||
|
||||
if (node->fd() != 1)
|
||||
current_builder().appendf(" %d>>", node->fd());
|
||||
else
|
||||
current_builder().append(" >>");
|
||||
NodeVisitor::visit(node);
|
||||
visited(node);
|
||||
}
|
||||
|
||||
void Formatter::visit(const AST::WriteRedirection* node)
|
||||
{
|
||||
will_visit(node);
|
||||
test_and_update_output_cursor(node);
|
||||
TemporaryChange<const AST::Node*> parent { m_parent_node, node };
|
||||
|
||||
if (node->fd() != 1)
|
||||
current_builder().appendf(" %d>", node->fd());
|
||||
else
|
||||
current_builder().append(" >");
|
||||
NodeVisitor::visit(node);
|
||||
visited(node);
|
||||
}
|
||||
|
||||
}
|
129
Userland/Shell/Formatter.h
Normal file
129
Userland/Shell/Formatter.h
Normal file
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* 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 "NodeVisitor.h"
|
||||
#include <AK/Forward.h>
|
||||
#include <AK/StringBuilder.h>
|
||||
#include <AK/StringView.h>
|
||||
#include <AK/Types.h>
|
||||
#include <ctype.h>
|
||||
|
||||
namespace Shell {
|
||||
|
||||
class Formatter final : public AST::NodeVisitor {
|
||||
public:
|
||||
Formatter(const StringView& source, ssize_t cursor = -1)
|
||||
: m_builder(round_up_to_power_of_two(source.length(), 16))
|
||||
, m_source(source)
|
||||
, m_cursor(cursor)
|
||||
{
|
||||
size_t offset = 0;
|
||||
for (auto ptr = m_source.end() - 1; ptr >= m_source.begin() && isspace(*ptr); --ptr)
|
||||
++offset;
|
||||
|
||||
m_trivia = m_source.substring_view(m_source.length() - offset, offset);
|
||||
}
|
||||
|
||||
String format();
|
||||
size_t cursor() const { return m_output_cursor; }
|
||||
|
||||
private:
|
||||
virtual void visit(const AST::PathRedirectionNode*) override;
|
||||
virtual void visit(const AST::And*) override;
|
||||
virtual void visit(const AST::ListConcatenate*) override;
|
||||
virtual void visit(const AST::Background*) override;
|
||||
virtual void visit(const AST::BarewordLiteral*) override;
|
||||
virtual void visit(const AST::BraceExpansion*) override;
|
||||
virtual void visit(const AST::CastToCommand*) override;
|
||||
virtual void visit(const AST::CastToList*) override;
|
||||
virtual void visit(const AST::CloseFdRedirection*) override;
|
||||
virtual void visit(const AST::CommandLiteral*) override;
|
||||
virtual void visit(const AST::Comment*) override;
|
||||
virtual void visit(const AST::ContinuationControl*) override;
|
||||
virtual void visit(const AST::DynamicEvaluate*) override;
|
||||
virtual void visit(const AST::DoubleQuotedString*) override;
|
||||
virtual void visit(const AST::Fd2FdRedirection*) override;
|
||||
virtual void visit(const AST::FunctionDeclaration*) override;
|
||||
virtual void visit(const AST::ForLoop*) override;
|
||||
virtual void visit(const AST::Glob*) override;
|
||||
virtual void visit(const AST::Execute*) override;
|
||||
virtual void visit(const AST::IfCond*) override;
|
||||
virtual void visit(const AST::Join*) override;
|
||||
virtual void visit(const AST::MatchExpr*) override;
|
||||
virtual void visit(const AST::Or*) override;
|
||||
virtual void visit(const AST::Pipe*) override;
|
||||
virtual void visit(const AST::Range*) override;
|
||||
virtual void visit(const AST::ReadRedirection*) override;
|
||||
virtual void visit(const AST::ReadWriteRedirection*) override;
|
||||
virtual void visit(const AST::Sequence*) override;
|
||||
virtual void visit(const AST::Subshell*) override;
|
||||
virtual void visit(const AST::SimpleVariable*) override;
|
||||
virtual void visit(const AST::SpecialVariable*) override;
|
||||
virtual void visit(const AST::Juxtaposition*) override;
|
||||
virtual void visit(const AST::StringLiteral*) override;
|
||||
virtual void visit(const AST::StringPartCompose*) override;
|
||||
virtual void visit(const AST::SyntaxError*) override;
|
||||
virtual void visit(const AST::Tilde*) override;
|
||||
virtual void visit(const AST::VariableDeclarations*) override;
|
||||
virtual void visit(const AST::WriteAppendRedirection*) override;
|
||||
virtual void visit(const AST::WriteRedirection*) override;
|
||||
|
||||
void test_and_update_output_cursor(const AST::Node*);
|
||||
void visited(const AST::Node*);
|
||||
void will_visit(const AST::Node*);
|
||||
void insert_separator();
|
||||
void insert_indent();
|
||||
|
||||
ALWAYS_INLINE void with_added_indent(int indent, Function<void()>);
|
||||
ALWAYS_INLINE void in_new_block(Function<void()>);
|
||||
|
||||
StringBuilder& current_builder() { return m_builder; }
|
||||
|
||||
struct Options {
|
||||
size_t max_line_length_hint { 80 };
|
||||
bool explicit_parentheses { false };
|
||||
bool explicit_braces { false };
|
||||
bool in_double_quotes { false };
|
||||
} m_options;
|
||||
|
||||
size_t m_current_indent { 0 };
|
||||
|
||||
StringBuilder m_builder;
|
||||
|
||||
StringView m_source;
|
||||
size_t m_output_cursor { 0 };
|
||||
ssize_t m_cursor { -1 };
|
||||
AST::Node* m_hit_node { nullptr };
|
||||
|
||||
const AST::Node* m_parent_node { nullptr };
|
||||
const AST::Node* m_last_visited_node { nullptr };
|
||||
|
||||
StringView m_trivia;
|
||||
};
|
||||
|
||||
}
|
85
Userland/Shell/Forward.h
Normal file
85
Userland/Shell/Forward.h
Normal file
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
namespace Shell::AST {
|
||||
|
||||
struct Command;
|
||||
class Node;
|
||||
class Value;
|
||||
class SyntaxError;
|
||||
class Pipeline;
|
||||
struct Rewiring;
|
||||
class NodeVisitor;
|
||||
|
||||
class PathRedirectionNode;
|
||||
class And;
|
||||
class ListConcatenate;
|
||||
class Background;
|
||||
class BarewordLiteral;
|
||||
class BraceExpansion;
|
||||
class CastToCommand;
|
||||
class CastToList;
|
||||
class CloseFdRedirection;
|
||||
class CommandLiteral;
|
||||
class Comment;
|
||||
class ContinuationControl;
|
||||
class DynamicEvaluate;
|
||||
class DoubleQuotedString;
|
||||
class Fd2FdRedirection;
|
||||
class FunctionDeclaration;
|
||||
class ForLoop;
|
||||
class Glob;
|
||||
class Execute;
|
||||
class IfCond;
|
||||
class Join;
|
||||
class MatchExpr;
|
||||
class Or;
|
||||
class Pipe;
|
||||
class Range;
|
||||
class ReadRedirection;
|
||||
class ReadWriteRedirection;
|
||||
class Sequence;
|
||||
class Subshell;
|
||||
class SimpleVariable;
|
||||
class SpecialVariable;
|
||||
class Juxtaposition;
|
||||
class StringLiteral;
|
||||
class StringPartCompose;
|
||||
class SyntaxError;
|
||||
class Tilde;
|
||||
class VariableDeclarations;
|
||||
class WriteAppendRedirection;
|
||||
class WriteRedirection;
|
||||
|
||||
}
|
||||
|
||||
namespace Shell {
|
||||
|
||||
class Shell;
|
||||
|
||||
}
|
124
Userland/Shell/Job.cpp
Normal file
124
Userland/Shell/Job.cpp
Normal file
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* 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 "Job.h"
|
||||
#include "AST.h"
|
||||
#include "Shell.h"
|
||||
#include <inttypes.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
namespace Shell {
|
||||
|
||||
bool Job::print_status(PrintStatusMode mode)
|
||||
{
|
||||
int wstatus;
|
||||
auto rc = waitpid(m_pid, &wstatus, WNOHANG);
|
||||
auto status = "running";
|
||||
|
||||
if (rc > 0) {
|
||||
if (WIFEXITED(wstatus))
|
||||
status = "exited";
|
||||
|
||||
if (WIFSTOPPED(wstatus))
|
||||
status = "stopped";
|
||||
|
||||
if (WIFSIGNALED(wstatus))
|
||||
status = "signaled";
|
||||
} else if (rc < 0) {
|
||||
// We couldn't waitpid() it, probably because we're not the parent shell.
|
||||
// just use the old information.
|
||||
if (exited())
|
||||
status = "exited";
|
||||
else if (m_is_suspended)
|
||||
status = "stopped";
|
||||
else if (signaled())
|
||||
status = "signaled";
|
||||
}
|
||||
|
||||
char background_indicator = '-';
|
||||
|
||||
if (is_running_in_background())
|
||||
background_indicator = '+';
|
||||
|
||||
const AST::Command& command = *m_command;
|
||||
|
||||
switch (mode) {
|
||||
case PrintStatusMode::Basic:
|
||||
outln("[{}] {} {} {}", m_job_id, background_indicator, status, command);
|
||||
break;
|
||||
case PrintStatusMode::OnlyPID:
|
||||
outln("[{}] {} {} {} {}", m_job_id, background_indicator, m_pid, status, command);
|
||||
break;
|
||||
case PrintStatusMode::ListAll:
|
||||
outln("[{}] {} {} {} {} {}", m_job_id, background_indicator, m_pid, m_pgid, status, command);
|
||||
break;
|
||||
}
|
||||
fflush(stdout);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Job::Job(pid_t pid, unsigned pgid, String cmd, u64 job_id, AST::Command&& command)
|
||||
: m_pgid(pgid)
|
||||
, m_pid(pid)
|
||||
, m_job_id(job_id)
|
||||
, m_cmd(move(cmd))
|
||||
{
|
||||
m_command = make<AST::Command>(move(command));
|
||||
|
||||
set_running_in_background(false);
|
||||
m_command_timer.start();
|
||||
}
|
||||
|
||||
void Job::set_has_exit(int exit_code)
|
||||
{
|
||||
if (m_exited)
|
||||
return;
|
||||
m_exit_code = exit_code;
|
||||
m_exited = true;
|
||||
if (on_exit)
|
||||
on_exit(*this);
|
||||
}
|
||||
|
||||
void Job::set_signalled(int sig)
|
||||
{
|
||||
if (m_exited)
|
||||
return;
|
||||
m_exited = true;
|
||||
m_exit_code = 126;
|
||||
m_term_sig = sig;
|
||||
if (on_exit)
|
||||
on_exit(*this);
|
||||
}
|
||||
|
||||
void Job::unblock() const
|
||||
{
|
||||
if (!m_exited && on_exit)
|
||||
on_exit(*this);
|
||||
}
|
||||
|
||||
}
|
136
Userland/Shell/Job.h
Normal file
136
Userland/Shell/Job.h
Normal file
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* 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 "Execution.h"
|
||||
#include "Forward.h"
|
||||
#include <AK/Function.h>
|
||||
#include <AK/JsonObject.h>
|
||||
#include <AK/JsonValue.h>
|
||||
#include <AK/OwnPtr.h>
|
||||
#include <AK/String.h>
|
||||
#include <LibCore/ElapsedTimer.h>
|
||||
#include <LibCore/Object.h>
|
||||
|
||||
#define JOB_TIME_INFO
|
||||
#ifndef __serenity__
|
||||
# undef JOB_TIME_INFO
|
||||
#endif
|
||||
|
||||
namespace Shell {
|
||||
|
||||
struct LocalFrame;
|
||||
|
||||
class Job : public RefCounted<Job> {
|
||||
public:
|
||||
static NonnullRefPtr<Job> create(pid_t pid, pid_t pgid, String cmd, u64 job_id, AST::Command&& command) { return adopt(*new Job(pid, pgid, move(cmd), job_id, move(command))); }
|
||||
|
||||
~Job()
|
||||
{
|
||||
#ifdef JOB_TIME_INFO
|
||||
if (m_active) {
|
||||
auto elapsed = m_command_timer.elapsed();
|
||||
// Don't mistake this for the command!
|
||||
dbg() << "Job entry \"" << m_cmd << "\" deleted in " << elapsed << " ms";
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
Function<void(RefPtr<Job>)> on_exit;
|
||||
|
||||
pid_t pgid() const { return m_pgid; }
|
||||
pid_t pid() const { return m_pid; }
|
||||
const String& cmd() const { return m_cmd; }
|
||||
const AST::Command& command() const { return *m_command; }
|
||||
AST::Command* command_ptr() { return m_command; }
|
||||
u64 job_id() const { return m_job_id; }
|
||||
bool exited() const { return m_exited; }
|
||||
bool signaled() const { return m_term_sig != -1; }
|
||||
int exit_code() const
|
||||
{
|
||||
ASSERT(exited());
|
||||
return m_exit_code;
|
||||
}
|
||||
int termination_signal() const
|
||||
{
|
||||
ASSERT(signaled());
|
||||
return m_term_sig;
|
||||
}
|
||||
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; }
|
||||
bool should_announce_exit() const { return m_should_announce_exit; }
|
||||
bool should_announce_signal() const { return m_should_announce_signal; }
|
||||
bool is_suspended() const { return m_is_suspended; }
|
||||
void unblock() const;
|
||||
|
||||
Core::ElapsedTimer& timer() { return m_command_timer; }
|
||||
|
||||
void set_has_exit(int exit_code);
|
||||
void set_signalled(int sig);
|
||||
|
||||
void set_is_suspended(bool value) const { m_is_suspended = value; }
|
||||
|
||||
void set_running_in_background(bool running_in_background)
|
||||
{
|
||||
m_running_in_background = running_in_background;
|
||||
}
|
||||
|
||||
void set_should_announce_exit(bool value) { m_should_announce_exit = value; }
|
||||
void set_should_announce_signal(bool value) { m_should_announce_signal = value; }
|
||||
|
||||
void deactivate() const { m_active = false; }
|
||||
|
||||
enum class PrintStatusMode {
|
||||
Basic,
|
||||
OnlyPID,
|
||||
ListAll,
|
||||
};
|
||||
|
||||
bool print_status(PrintStatusMode);
|
||||
|
||||
private:
|
||||
Job(pid_t pid, unsigned pgid, String cmd, u64 job_id, AST::Command&& command);
|
||||
|
||||
pid_t m_pgid { 0 };
|
||||
pid_t m_pid { 0 };
|
||||
u64 m_job_id { 0 };
|
||||
String m_cmd;
|
||||
bool m_exited { false };
|
||||
bool m_running_in_background { false };
|
||||
bool m_should_announce_exit { false };
|
||||
bool m_should_announce_signal { true };
|
||||
int m_exit_code { -1 };
|
||||
int m_term_sig { -1 };
|
||||
Core::ElapsedTimer m_command_timer;
|
||||
mutable bool m_active { true };
|
||||
mutable bool m_is_suspended { false };
|
||||
bool m_should_be_disowned { false };
|
||||
OwnPtr<AST::Command> m_command;
|
||||
};
|
||||
|
||||
}
|
245
Userland/Shell/NodeVisitor.cpp
Normal file
245
Userland/Shell/NodeVisitor.cpp
Normal file
|
@ -0,0 +1,245 @@
|
|||
/*
|
||||
* 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 "NodeVisitor.h"
|
||||
#include "AST.h"
|
||||
|
||||
namespace Shell::AST {
|
||||
|
||||
void NodeVisitor::visit(const AST::PathRedirectionNode* node)
|
||||
{
|
||||
node->path()->visit(*this);
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::And* node)
|
||||
{
|
||||
node->left()->visit(*this);
|
||||
node->right()->visit(*this);
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::ListConcatenate* node)
|
||||
{
|
||||
for (auto& subnode : node->list())
|
||||
subnode->visit(*this);
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::Background* node)
|
||||
{
|
||||
node->command()->visit(*this);
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::BarewordLiteral*)
|
||||
{
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::BraceExpansion* node)
|
||||
{
|
||||
for (auto& entry : node->entries())
|
||||
entry.visit(*this);
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::CastToCommand* node)
|
||||
{
|
||||
node->inner()->visit(*this);
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::CastToList* node)
|
||||
{
|
||||
if (node->inner())
|
||||
node->inner()->visit(*this);
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::CloseFdRedirection*)
|
||||
{
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::CommandLiteral*)
|
||||
{
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::Comment*)
|
||||
{
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::ContinuationControl*)
|
||||
{
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::DynamicEvaluate* node)
|
||||
{
|
||||
node->inner()->visit(*this);
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::DoubleQuotedString* node)
|
||||
{
|
||||
if (node->inner())
|
||||
node->inner()->visit(*this);
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::Fd2FdRedirection*)
|
||||
{
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::FunctionDeclaration* node)
|
||||
{
|
||||
if (node->block())
|
||||
node->block()->visit(*this);
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::ForLoop* node)
|
||||
{
|
||||
if (node->iterated_expression())
|
||||
node->iterated_expression()->visit(*this);
|
||||
if (node->block())
|
||||
node->block()->visit(*this);
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::Glob*)
|
||||
{
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::Execute* node)
|
||||
{
|
||||
node->command()->visit(*this);
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::IfCond* node)
|
||||
{
|
||||
node->condition()->visit(*this);
|
||||
if (node->true_branch())
|
||||
node->true_branch()->visit(*this);
|
||||
if (node->false_branch())
|
||||
node->false_branch()->visit(*this);
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::Join* node)
|
||||
{
|
||||
node->left()->visit(*this);
|
||||
node->right()->visit(*this);
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::MatchExpr* node)
|
||||
{
|
||||
node->matched_expr()->visit(*this);
|
||||
for (auto& entry : node->entries()) {
|
||||
for (auto& option : entry.options)
|
||||
option.visit(*this);
|
||||
if (entry.body)
|
||||
entry.body->visit(*this);
|
||||
}
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::Or* node)
|
||||
{
|
||||
node->left()->visit(*this);
|
||||
node->right()->visit(*this);
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::Pipe* node)
|
||||
{
|
||||
node->left()->visit(*this);
|
||||
node->right()->visit(*this);
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::Range* node)
|
||||
{
|
||||
node->start()->visit(*this);
|
||||
node->end()->visit(*this);
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::ReadRedirection* node)
|
||||
{
|
||||
visit(static_cast<const AST::PathRedirectionNode*>(node));
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::ReadWriteRedirection* node)
|
||||
{
|
||||
visit(static_cast<const AST::PathRedirectionNode*>(node));
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::Sequence* node)
|
||||
{
|
||||
node->left()->visit(*this);
|
||||
node->right()->visit(*this);
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::Subshell* node)
|
||||
{
|
||||
if (node->block())
|
||||
node->block()->visit(*this);
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::SimpleVariable*)
|
||||
{
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::SpecialVariable*)
|
||||
{
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::Juxtaposition* node)
|
||||
{
|
||||
node->left()->visit(*this);
|
||||
node->right()->visit(*this);
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::StringLiteral*)
|
||||
{
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::StringPartCompose* node)
|
||||
{
|
||||
node->left()->visit(*this);
|
||||
node->right()->visit(*this);
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::SyntaxError*)
|
||||
{
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::Tilde*)
|
||||
{
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::VariableDeclarations* node)
|
||||
{
|
||||
for (auto& entry : node->variables()) {
|
||||
entry.name->visit(*this);
|
||||
entry.value->visit(*this);
|
||||
}
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::WriteAppendRedirection* node)
|
||||
{
|
||||
visit(static_cast<const AST::PathRedirectionNode*>(node));
|
||||
}
|
||||
|
||||
void NodeVisitor::visit(const AST::WriteRedirection* node)
|
||||
{
|
||||
visit(static_cast<const AST::PathRedirectionNode*>(node));
|
||||
}
|
||||
|
||||
}
|
76
Userland/Shell/NodeVisitor.h
Normal file
76
Userland/Shell/NodeVisitor.h
Normal file
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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"
|
||||
|
||||
namespace Shell::AST {
|
||||
|
||||
class NodeVisitor {
|
||||
public:
|
||||
virtual void visit(const AST::PathRedirectionNode*);
|
||||
virtual void visit(const AST::And*);
|
||||
virtual void visit(const AST::ListConcatenate*);
|
||||
virtual void visit(const AST::Background*);
|
||||
virtual void visit(const AST::BarewordLiteral*);
|
||||
virtual void visit(const AST::BraceExpansion*);
|
||||
virtual void visit(const AST::CastToCommand*);
|
||||
virtual void visit(const AST::CastToList*);
|
||||
virtual void visit(const AST::CloseFdRedirection*);
|
||||
virtual void visit(const AST::CommandLiteral*);
|
||||
virtual void visit(const AST::Comment*);
|
||||
virtual void visit(const AST::ContinuationControl*);
|
||||
virtual void visit(const AST::DynamicEvaluate*);
|
||||
virtual void visit(const AST::DoubleQuotedString*);
|
||||
virtual void visit(const AST::Fd2FdRedirection*);
|
||||
virtual void visit(const AST::FunctionDeclaration*);
|
||||
virtual void visit(const AST::ForLoop*);
|
||||
virtual void visit(const AST::Glob*);
|
||||
virtual void visit(const AST::Execute*);
|
||||
virtual void visit(const AST::IfCond*);
|
||||
virtual void visit(const AST::Join*);
|
||||
virtual void visit(const AST::MatchExpr*);
|
||||
virtual void visit(const AST::Or*);
|
||||
virtual void visit(const AST::Pipe*);
|
||||
virtual void visit(const AST::Range*);
|
||||
virtual void visit(const AST::ReadRedirection*);
|
||||
virtual void visit(const AST::ReadWriteRedirection*);
|
||||
virtual void visit(const AST::Sequence*);
|
||||
virtual void visit(const AST::Subshell*);
|
||||
virtual void visit(const AST::SimpleVariable*);
|
||||
virtual void visit(const AST::SpecialVariable*);
|
||||
virtual void visit(const AST::Juxtaposition*);
|
||||
virtual void visit(const AST::StringLiteral*);
|
||||
virtual void visit(const AST::StringPartCompose*);
|
||||
virtual void visit(const AST::SyntaxError*);
|
||||
virtual void visit(const AST::Tilde*);
|
||||
virtual void visit(const AST::VariableDeclarations*);
|
||||
virtual void visit(const AST::WriteAppendRedirection*);
|
||||
virtual void visit(const AST::WriteRedirection*);
|
||||
};
|
||||
|
||||
}
|
1555
Userland/Shell/Parser.cpp
Normal file
1555
Userland/Shell/Parser.cpp
Normal file
File diff suppressed because it is too large
Load diff
262
Userland/Shell/Parser.h
Normal file
262
Userland/Shell/Parser.h
Normal file
|
@ -0,0 +1,262 @@
|
|||
/*
|
||||
* 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 "AST.h"
|
||||
#include <AK/Function.h>
|
||||
#include <AK/RefPtr.h>
|
||||
#include <AK/String.h>
|
||||
#include <AK/StringBuilder.h>
|
||||
#include <AK/Vector.h>
|
||||
|
||||
namespace Shell {
|
||||
|
||||
class Parser {
|
||||
public:
|
||||
Parser(StringView input)
|
||||
: m_input(move(input))
|
||||
{
|
||||
}
|
||||
|
||||
RefPtr<AST::Node> parse();
|
||||
|
||||
struct SavedOffset {
|
||||
size_t offset;
|
||||
AST::Position::Line line;
|
||||
};
|
||||
SavedOffset save_offset() const;
|
||||
|
||||
private:
|
||||
constexpr static size_t max_allowed_nested_rule_depth = 2048;
|
||||
RefPtr<AST::Node> parse_toplevel();
|
||||
RefPtr<AST::Node> parse_sequence();
|
||||
RefPtr<AST::Node> parse_function_decl();
|
||||
RefPtr<AST::Node> parse_and_logical_sequence();
|
||||
RefPtr<AST::Node> parse_or_logical_sequence();
|
||||
RefPtr<AST::Node> parse_variable_decls();
|
||||
RefPtr<AST::Node> parse_pipe_sequence();
|
||||
RefPtr<AST::Node> parse_command();
|
||||
RefPtr<AST::Node> parse_control_structure();
|
||||
RefPtr<AST::Node> parse_continuation_control();
|
||||
RefPtr<AST::Node> parse_for_loop();
|
||||
RefPtr<AST::Node> parse_loop_loop();
|
||||
RefPtr<AST::Node> parse_if_expr();
|
||||
RefPtr<AST::Node> parse_subshell();
|
||||
RefPtr<AST::Node> parse_match_expr();
|
||||
AST::MatchEntry parse_match_entry();
|
||||
RefPtr<AST::Node> parse_match_pattern();
|
||||
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();
|
||||
RefPtr<AST::Node> parse_brace_expansion();
|
||||
RefPtr<AST::Node> parse_brace_expansion_spec();
|
||||
|
||||
template<typename A, typename... Args>
|
||||
NonnullRefPtr<A> create(Args... args);
|
||||
|
||||
bool at_end() const { return m_input.length() <= m_offset; }
|
||||
char peek();
|
||||
char consume();
|
||||
bool expect(char);
|
||||
bool expect(const StringView&);
|
||||
bool next_is(const StringView&);
|
||||
|
||||
void restore_to(size_t offset, AST::Position::Line line)
|
||||
{
|
||||
m_offset = offset;
|
||||
m_line = move(line);
|
||||
}
|
||||
|
||||
AST::Position::Line line() const { return m_line; }
|
||||
|
||||
StringView consume_while(Function<bool(char)>);
|
||||
|
||||
struct ScopedOffset {
|
||||
ScopedOffset(Vector<size_t>& offsets, Vector<AST::Position::Line>& lines, size_t offset, size_t lineno, size_t linecol)
|
||||
: offsets(offsets)
|
||||
, lines(lines)
|
||||
, offset(offset)
|
||||
, line({ lineno, linecol })
|
||||
{
|
||||
offsets.append(offset);
|
||||
lines.append(line);
|
||||
}
|
||||
~ScopedOffset()
|
||||
{
|
||||
auto last = offsets.take_last();
|
||||
ASSERT(last == offset);
|
||||
|
||||
auto last_line = lines.take_last();
|
||||
ASSERT(last_line == line);
|
||||
}
|
||||
|
||||
Vector<size_t>& offsets;
|
||||
Vector<AST::Position::Line>& lines;
|
||||
size_t offset;
|
||||
AST::Position::Line line;
|
||||
};
|
||||
|
||||
void restore_to(const ScopedOffset& offset) { restore_to(offset.offset, offset.line); }
|
||||
|
||||
OwnPtr<ScopedOffset> push_start();
|
||||
|
||||
StringView m_input;
|
||||
size_t m_offset { 0 };
|
||||
|
||||
AST::Position::Line m_line { 0, 0 };
|
||||
|
||||
Vector<size_t> m_rule_start_offsets;
|
||||
Vector<AST::Position::Line> m_rule_start_lines;
|
||||
|
||||
bool m_is_in_brace_expansion_spec { false };
|
||||
bool m_continuation_controls_allowed { false };
|
||||
};
|
||||
|
||||
#if 0
|
||||
constexpr auto the_grammar = R"(
|
||||
toplevel :: sequence?
|
||||
|
||||
sequence :: variable_decls? or_logical_sequence terminator sequence
|
||||
| variable_decls? or_logical_sequence '&' sequence
|
||||
| variable_decls? or_logical_sequence
|
||||
| variable_decls? function_decl (terminator sequence)?
|
||||
| variable_decls? terminator sequence
|
||||
|
||||
function_decl :: identifier '(' (ws* identifier)* ')' ws* '{' [!c] toplevel '}'
|
||||
|
||||
or_logical_sequence :: and_logical_sequence '|' '|' and_logical_sequence
|
||||
| and_logical_sequence
|
||||
|
||||
and_logical_sequence :: pipe_sequence '&' '&' and_logical_sequence
|
||||
| pipe_sequence
|
||||
|
||||
terminator :: ';'
|
||||
| '\n'
|
||||
|
||||
variable_decls :: identifier '=' expression (' '+ variable_decls)? ' '*
|
||||
| identifier '=' '(' pipe_sequence ')' (' '+ variable_decls)? ' '*
|
||||
|
||||
pipe_sequence :: command '|' pipe_sequence
|
||||
| command
|
||||
| control_structure '|' pipe_sequence
|
||||
| control_structure
|
||||
|
||||
control_structure[c] :: for_expr
|
||||
| loop_expr
|
||||
| if_expr
|
||||
| subshell
|
||||
| match_expr
|
||||
| ?c: continuation_control
|
||||
|
||||
continuation_control :: 'break'
|
||||
| 'continue'
|
||||
|
||||
for_expr :: 'for' ws+ (identifier ' '+ 'in' ws*)? expression ws+ '{' [c] toplevel '}'
|
||||
|
||||
loop_expr :: 'loop' ws* '{' [c] toplevel '}'
|
||||
|
||||
if_expr :: 'if' ws+ or_logical_sequence ws+ '{' toplevel '}' else_clause?
|
||||
|
||||
else_clause :: else '{' toplevel '}'
|
||||
| else if_expr
|
||||
|
||||
subshell :: '{' toplevel '}'
|
||||
|
||||
match_expr :: 'match' ws+ expression ws* ('as' ws+ identifier)? '{' match_entry* '}'
|
||||
|
||||
match_entry :: match_pattern ws* (as identifier_list)? '{' toplevel '}'
|
||||
|
||||
identifier_list :: '(' (identifier ws*)* ')'
|
||||
|
||||
match_pattern :: expression (ws* '|' ws* expression)*
|
||||
|
||||
command :: redirection command
|
||||
| list_expression command?
|
||||
|
||||
redirection :: number? '>'{1,2} ' '* string_composite
|
||||
| number? '<' ' '* string_composite
|
||||
| number? '>' '&' number
|
||||
| number? '>' '&' '-'
|
||||
|
||||
list_expression :: ' '* expression (' '+ list_expression)?
|
||||
|
||||
expression :: evaluate expression?
|
||||
| string_composite expression?
|
||||
| comment expression?
|
||||
| '(' list_expression ')' expression?
|
||||
|
||||
evaluate :: '$' '(' pipe_sequence ')'
|
||||
| '$' expression {eval / dynamic resolve}
|
||||
|
||||
string_composite :: string string_composite?
|
||||
| variable string_composite?
|
||||
| bareword string_composite?
|
||||
| glob string_composite?
|
||||
| brace_expansion 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 [*?]
|
||||
|
||||
brace_expansion :: '{' brace_expansion_spec '}'
|
||||
|
||||
brace_expansion_spec :: expression? (',' expression?)*
|
||||
| expression '..' expression
|
||||
)";
|
||||
#endif
|
||||
|
||||
}
|
1939
Userland/Shell/Shell.cpp
Normal file
1939
Userland/Shell/Shell.cpp
Normal file
File diff suppressed because it is too large
Load diff
351
Userland/Shell/Shell.h
Normal file
351
Userland/Shell/Shell.h
Normal file
|
@ -0,0 +1,351 @@
|
|||
/*
|
||||
* 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 "Job.h"
|
||||
#include "Parser.h"
|
||||
#include <AK/CircularQueue.h>
|
||||
#include <AK/HashMap.h>
|
||||
#include <AK/NonnullOwnPtrVector.h>
|
||||
#include <AK/String.h>
|
||||
#include <AK/StringBuilder.h>
|
||||
#include <AK/Types.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibCore/Notifier.h>
|
||||
#include <LibCore/Object.h>
|
||||
#include <LibLine/Editor.h>
|
||||
#include <termios.h>
|
||||
|
||||
#define ENUMERATE_SHELL_BUILTINS() \
|
||||
__ENUMERATE_SHELL_BUILTIN(alias) \
|
||||
__ENUMERATE_SHELL_BUILTIN(cd) \
|
||||
__ENUMERATE_SHELL_BUILTIN(cdh) \
|
||||
__ENUMERATE_SHELL_BUILTIN(pwd) \
|
||||
__ENUMERATE_SHELL_BUILTIN(exec) \
|
||||
__ENUMERATE_SHELL_BUILTIN(exit) \
|
||||
__ENUMERATE_SHELL_BUILTIN(export) \
|
||||
__ENUMERATE_SHELL_BUILTIN(glob) \
|
||||
__ENUMERATE_SHELL_BUILTIN(unset) \
|
||||
__ENUMERATE_SHELL_BUILTIN(history) \
|
||||
__ENUMERATE_SHELL_BUILTIN(umask) \
|
||||
__ENUMERATE_SHELL_BUILTIN(dirs) \
|
||||
__ENUMERATE_SHELL_BUILTIN(pushd) \
|
||||
__ENUMERATE_SHELL_BUILTIN(popd) \
|
||||
__ENUMERATE_SHELL_BUILTIN(setopt) \
|
||||
__ENUMERATE_SHELL_BUILTIN(shift) \
|
||||
__ENUMERATE_SHELL_BUILTIN(source) \
|
||||
__ENUMERATE_SHELL_BUILTIN(time) \
|
||||
__ENUMERATE_SHELL_BUILTIN(jobs) \
|
||||
__ENUMERATE_SHELL_BUILTIN(disown) \
|
||||
__ENUMERATE_SHELL_BUILTIN(fg) \
|
||||
__ENUMERATE_SHELL_BUILTIN(bg) \
|
||||
__ENUMERATE_SHELL_BUILTIN(wait)
|
||||
|
||||
#define ENUMERATE_SHELL_OPTIONS() \
|
||||
__ENUMERATE_SHELL_OPTION(inline_exec_keep_empty_segments, false, "Keep empty segments in inline execute $(...)") \
|
||||
__ENUMERATE_SHELL_OPTION(verbose, false, "Announce every command that is about to be executed")
|
||||
|
||||
namespace Shell {
|
||||
|
||||
class Shell;
|
||||
|
||||
class Shell : public Core::Object {
|
||||
C_OBJECT(Shell);
|
||||
|
||||
public:
|
||||
constexpr static auto local_init_file_path = "~/.shellrc";
|
||||
constexpr static auto global_init_file_path = "/etc/shellrc";
|
||||
|
||||
bool should_format_live() const { return m_should_format_live; }
|
||||
void set_live_formatting(bool value) { m_should_format_live = value; }
|
||||
|
||||
void setup_signals();
|
||||
|
||||
struct SourcePosition {
|
||||
String source_file;
|
||||
String literal_source_text;
|
||||
Optional<AST::Position> position;
|
||||
};
|
||||
|
||||
int run_command(const StringView&, Optional<SourcePosition> = {});
|
||||
bool is_runnable(const StringView&);
|
||||
RefPtr<Job> run_command(const AST::Command&);
|
||||
NonnullRefPtrVector<Job> run_commands(Vector<AST::Command>&);
|
||||
bool run_file(const String&, bool explicitly_invoked = true);
|
||||
bool run_builtin(const AST::Command&, const NonnullRefPtrVector<AST::Rewiring>&, int& retval);
|
||||
bool has_builtin(const StringView&) const;
|
||||
void block_on_job(RefPtr<Job>);
|
||||
void block_on_pipeline(RefPtr<AST::Pipeline>);
|
||||
String prompt() const;
|
||||
|
||||
static String expand_tilde(const String&);
|
||||
static Vector<String> expand_globs(const StringView& path, StringView base);
|
||||
static Vector<String> expand_globs(Vector<StringView> path_segments, const StringView& base);
|
||||
Vector<AST::Command> expand_aliases(Vector<AST::Command>);
|
||||
String resolve_path(String) const;
|
||||
String resolve_alias(const String&) const;
|
||||
|
||||
RefPtr<AST::Value> get_argument(size_t);
|
||||
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>, bool only_in_current_frame = false);
|
||||
void unset_local_variable(const String&, bool only_in_current_frame = false);
|
||||
|
||||
void define_function(String name, Vector<String> argnames, RefPtr<AST::Node> body);
|
||||
bool has_function(const String&);
|
||||
bool invoke_function(const AST::Command&, int& retval);
|
||||
|
||||
String format(const StringView&, ssize_t& cursor) const;
|
||||
|
||||
RefPtr<Line::Editor> editor() const { return m_editor; }
|
||||
|
||||
struct LocalFrame {
|
||||
LocalFrame(const String& name, HashMap<String, RefPtr<AST::Value>> variables)
|
||||
: name(name)
|
||||
, local_variables(move(variables))
|
||||
{
|
||||
}
|
||||
|
||||
String name;
|
||||
HashMap<String, RefPtr<AST::Value>> local_variables;
|
||||
};
|
||||
|
||||
struct Frame {
|
||||
Frame(NonnullOwnPtrVector<LocalFrame>& frames, const LocalFrame& frame)
|
||||
: frames(frames)
|
||||
, frame(frame)
|
||||
{
|
||||
}
|
||||
~Frame();
|
||||
|
||||
void leak_frame() { should_destroy_frame = false; }
|
||||
|
||||
private:
|
||||
NonnullOwnPtrVector<LocalFrame>& frames;
|
||||
const LocalFrame& frame;
|
||||
bool should_destroy_frame { true };
|
||||
};
|
||||
|
||||
[[nodiscard]] Frame push_frame(String name);
|
||||
void pop_frame();
|
||||
|
||||
static String escape_token_for_single_quotes(const String& token);
|
||||
static String escape_token(const String& token);
|
||||
static String unescape_token(const String& token);
|
||||
static bool is_special(char c);
|
||||
|
||||
static bool is_glob(const StringView&);
|
||||
static Vector<StringView> split_path(const StringView&);
|
||||
|
||||
void highlight(Line::Editor&) const;
|
||||
Vector<Line::CompletionSuggestion> complete();
|
||||
Vector<Line::CompletionSuggestion> complete_path(const String& base, 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);
|
||||
Vector<Line::CompletionSuggestion> complete_user(const String&, size_t offset);
|
||||
Vector<Line::CompletionSuggestion> complete_option(const String&, const String&, size_t offset);
|
||||
|
||||
void restore_ios();
|
||||
|
||||
u64 find_last_job_id() const;
|
||||
const Job* find_job(u64 id);
|
||||
const Job* current_job() const { return m_current_job; }
|
||||
void kill_job(const Job*, int sig);
|
||||
|
||||
String get_history_path();
|
||||
void print_path(const String& path);
|
||||
|
||||
bool read_single_line();
|
||||
|
||||
void notify_child_event();
|
||||
|
||||
struct termios termios;
|
||||
struct termios default_termios;
|
||||
bool was_interrupted { false };
|
||||
bool was_resized { false };
|
||||
|
||||
String cwd;
|
||||
String username;
|
||||
String home;
|
||||
|
||||
constexpr static auto TTYNameSize = 32;
|
||||
constexpr static auto HostNameSize = 64;
|
||||
|
||||
char ttyname[TTYNameSize];
|
||||
char hostname[HostNameSize];
|
||||
|
||||
uid_t uid;
|
||||
int last_return_code { 0 };
|
||||
Vector<String> directory_stack;
|
||||
CircularQueue<String, 8> cd_history; // FIXME: have a configurable cd history length
|
||||
HashMap<u64, NonnullRefPtr<Job>> jobs;
|
||||
Vector<String, 256> cached_path;
|
||||
|
||||
String current_script;
|
||||
|
||||
enum ShellEventType {
|
||||
ReadLine,
|
||||
};
|
||||
|
||||
enum class ShellError {
|
||||
None,
|
||||
InternalControlFlowBreak,
|
||||
InternalControlFlowContinue,
|
||||
EvaluatedSyntaxError,
|
||||
NonExhaustiveMatchRules,
|
||||
InvalidGlobError,
|
||||
OpenFailure,
|
||||
};
|
||||
|
||||
void raise_error(ShellError kind, String description, Optional<AST::Position> position = {})
|
||||
{
|
||||
m_error = kind;
|
||||
m_error_description = move(description);
|
||||
if (m_source_position.has_value() && position.has_value())
|
||||
m_source_position.value().position = position.release_value();
|
||||
}
|
||||
bool has_error(ShellError err) const { return m_error == err; }
|
||||
const String& error_description() const { return m_error_description; }
|
||||
ShellError take_error()
|
||||
{
|
||||
auto err = m_error;
|
||||
m_error = ShellError::None;
|
||||
m_error_description = {};
|
||||
return err;
|
||||
}
|
||||
void possibly_print_error() const;
|
||||
bool is_control_flow(ShellError error)
|
||||
{
|
||||
switch (error) {
|
||||
case ShellError::InternalControlFlowBreak:
|
||||
case ShellError::InternalControlFlowContinue:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#define __ENUMERATE_SHELL_OPTION(name, default_, description) \
|
||||
bool name { default_ };
|
||||
|
||||
struct Options {
|
||||
ENUMERATE_SHELL_OPTIONS();
|
||||
} options;
|
||||
|
||||
#undef __ENUMERATE_SHELL_OPTION
|
||||
|
||||
private:
|
||||
Shell(Line::Editor&);
|
||||
Shell();
|
||||
virtual ~Shell() override;
|
||||
|
||||
// FIXME: Port to Core::Property
|
||||
void save_to(JsonObject&);
|
||||
void bring_cursor_to_beginning_of_a_line() const;
|
||||
|
||||
void cache_path();
|
||||
void add_entry_to_cache(const String&);
|
||||
void stop_all_jobs();
|
||||
const Job* m_current_job { nullptr };
|
||||
LocalFrame* find_frame_containing_local_variable(const String& name);
|
||||
|
||||
void run_tail(RefPtr<Job>);
|
||||
void run_tail(const AST::Command&, const AST::NodeWithAction&, int head_exit_code);
|
||||
|
||||
[[noreturn]] void execute_process(Vector<const char*>&& argv);
|
||||
|
||||
virtual void custom_event(Core::CustomEvent&) override;
|
||||
|
||||
#define __ENUMERATE_SHELL_BUILTIN(builtin) \
|
||||
int builtin_##builtin(int argc, const char** argv);
|
||||
|
||||
ENUMERATE_SHELL_BUILTINS();
|
||||
|
||||
#undef __ENUMERATE_SHELL_BUILTIN
|
||||
|
||||
constexpr static const char* builtin_names[] = {
|
||||
#define __ENUMERATE_SHELL_BUILTIN(builtin) #builtin,
|
||||
|
||||
ENUMERATE_SHELL_BUILTINS()
|
||||
|
||||
#undef __ENUMERATE_SHELL_BUILTIN
|
||||
};
|
||||
|
||||
bool m_should_ignore_jobs_on_next_exit { false };
|
||||
pid_t m_pid { 0 };
|
||||
|
||||
struct ShellFunction {
|
||||
String name;
|
||||
Vector<String> arguments;
|
||||
RefPtr<AST::Node> body;
|
||||
};
|
||||
|
||||
HashMap<String, ShellFunction> m_functions;
|
||||
NonnullOwnPtrVector<LocalFrame> m_local_frames;
|
||||
NonnullRefPtrVector<AST::Redirection> m_global_redirections;
|
||||
|
||||
HashMap<String, String> m_aliases;
|
||||
bool m_is_interactive { true };
|
||||
bool m_is_subshell { false };
|
||||
bool m_should_reinstall_signal_handlers { true };
|
||||
|
||||
ShellError m_error { ShellError::None };
|
||||
String m_error_description;
|
||||
Optional<SourcePosition> m_source_position;
|
||||
|
||||
bool m_should_format_live { false };
|
||||
|
||||
RefPtr<Line::Editor> m_editor;
|
||||
|
||||
bool m_default_constructed { false };
|
||||
|
||||
mutable bool m_last_continuation_state { false }; // false == not needed.
|
||||
};
|
||||
|
||||
static constexpr bool is_word_character(char c)
|
||||
{
|
||||
return c == '_' || (c <= 'Z' && c >= 'A') || (c <= 'z' && c >= 'a');
|
||||
}
|
||||
|
||||
inline size_t find_offset_into_node(const String& unescaped_text, size_t escaped_offset)
|
||||
{
|
||||
size_t unescaped_offset = 0;
|
||||
size_t offset = 0;
|
||||
for (auto& c : unescaped_text) {
|
||||
if (offset == escaped_offset)
|
||||
return unescaped_offset;
|
||||
|
||||
if (Shell::is_special(c))
|
||||
++offset;
|
||||
++offset;
|
||||
++unescaped_offset;
|
||||
}
|
||||
|
||||
return unescaped_offset;
|
||||
}
|
||||
|
||||
}
|
55
Userland/Shell/Tests/backgrounding.sh
Normal file
55
Userland/Shell/Tests/backgrounding.sh
Normal file
|
@ -0,0 +1,55 @@
|
|||
#!/bin/Shell
|
||||
|
||||
echo "Not running Shell-backgrounding as it has a high failure rate"
|
||||
exit 0
|
||||
|
||||
setopt --verbose
|
||||
|
||||
fail(msg) {
|
||||
echo FAIL: $msg
|
||||
exit 1
|
||||
}
|
||||
|
||||
last_idx=''
|
||||
block_idx=0
|
||||
block() {
|
||||
block_idx=$(expr 1 + $block_idx)
|
||||
last_idx=$block_idx
|
||||
mkfifo fifo$block_idx
|
||||
cat fifo$block_idx&
|
||||
}
|
||||
|
||||
unblock(idx) {
|
||||
echo unblock $idx > fifo$idx
|
||||
rm -f fifo$idx
|
||||
}
|
||||
|
||||
assert_job_count(count) {
|
||||
ecount=$(jobs | wc -l)
|
||||
shift
|
||||
if test $ecount -ne $count {
|
||||
for $* {
|
||||
unblock $it
|
||||
}
|
||||
fail "expected $ecount == $count"
|
||||
}
|
||||
}
|
||||
|
||||
block
|
||||
i=$last_idx
|
||||
|
||||
assert_job_count 1 $i
|
||||
|
||||
unblock $i
|
||||
wait
|
||||
|
||||
block
|
||||
i=$last_idx
|
||||
block
|
||||
j=$last_idx
|
||||
|
||||
assert_job_count 2 $i $j
|
||||
|
||||
unblock $i
|
||||
unblock $j
|
||||
wait
|
21
Userland/Shell/Tests/brace-exp.sh
Normal file
21
Userland/Shell/Tests/brace-exp.sh
Normal file
|
@ -0,0 +1,21 @@
|
|||
#!/bin/sh
|
||||
|
||||
setopt --verbose
|
||||
|
||||
fail() {
|
||||
echo $*
|
||||
exit 1
|
||||
}
|
||||
|
||||
test "$(echo {a,b,})" = "a b " || fail normal brace expansion with one empty slot
|
||||
test "$(echo {a,,b})" = "a b" || fail normal brace expansion with one empty slot
|
||||
test "$(echo {a,,,b})" = "a b" || fail normal brace expansion with two empty slots
|
||||
test "$(echo {a,b,,})" = "a b " || fail normal brace expansion with two empty slots
|
||||
|
||||
test "$(echo {a..c})" = "a b c" || fail range brace expansion, alpha
|
||||
test "$(echo {0..3})" = "0 1 2 3" || fail range brace expansion, number
|
||||
test "$(echo {😂..😄})" = "😂 😃 😄" || fail range brace expansion, unicode codepoint
|
||||
|
||||
# Make sure that didn't mess with dots and commas in normal barewords
|
||||
test .. = ".." || fail range brace expansion delimiter affects normal barewords
|
||||
test , = "," || fail normal brace expansion delimiter affects normal barewords
|
17
Userland/Shell/Tests/builtin-redir.sh
Normal file
17
Userland/Shell/Tests/builtin-redir.sh
Normal file
|
@ -0,0 +1,17 @@
|
|||
#!/bin/sh
|
||||
|
||||
rm -rf shell-test
|
||||
mkdir -p shell-test
|
||||
cd shell-test
|
||||
|
||||
time sleep 1 2>timeerr >timeout
|
||||
cat timeout
|
||||
# We cannot be sure about the values, so just assert that they're not empty.
|
||||
test -n "$(cat timeerr)" || echo "Failure: 'time' stderr output not redirected correctly" && exit 1
|
||||
test -e timeout || echo "Failure: 'time' stdout output not redirected correctly" && exit 1
|
||||
|
||||
time ls 2> /dev/null | head > timeout
|
||||
test -n "$(cat timeout)" || echo "Failure: 'time' stdout not piped correctly" && exit 1
|
||||
|
||||
cd ..
|
||||
rm -rf shell-test # TODO: Remove this file at the end once we have `trap'
|
39
Userland/Shell/Tests/control-structure-as-command.sh
Normal file
39
Userland/Shell/Tests/control-structure-as-command.sh
Normal file
|
@ -0,0 +1,39 @@
|
|||
#!/bin/sh
|
||||
|
||||
setopt --verbose
|
||||
|
||||
rm -rf shell-test 2> /dev/null
|
||||
mkdir shell-test
|
||||
cd shell-test
|
||||
|
||||
touch a b c
|
||||
|
||||
# Can we do logical stuff with control structures?
|
||||
ls && for $(seq 1) { echo yes > listing }
|
||||
test "$(cat listing)" = "yes" || echo for cannot appear as second part of '&&' && exit 1
|
||||
rm listing
|
||||
|
||||
# FIXME: This should work!
|
||||
# for $(seq 1) { echo yes > listing } && echo HELLO!
|
||||
# test "$(cat listing)" = "yes" || echo for cannot appear as first part of '&&' && exit 1
|
||||
# rm listing
|
||||
|
||||
# Can we pipe things into and from control structures?
|
||||
ls | if true { cat > listing }
|
||||
test "$(cat listing)" = "a b c" || echo if cannot be correctly redirected to && exit 1
|
||||
rm listing
|
||||
|
||||
ls | for $(seq 1) { cat > listing }
|
||||
test "$(cat listing)" = "a b c" || echo for cannot be correctly redirected to && exit 1
|
||||
rm listing
|
||||
|
||||
for $(seq 4) { echo $it } | cat > listing
|
||||
test "$(cat listing)" = "1 2 3 4" || echo for cannot be correctly redirected from && exit 1
|
||||
rm listing
|
||||
|
||||
if true { echo TRUE! } | cat > listing
|
||||
test "$(cat listing)" = "TRUE!" || echo if cannot be correctly redirected from && exit 1
|
||||
rm listing
|
||||
|
||||
cd ..
|
||||
rm -rf shell-test
|
41
Userland/Shell/Tests/function.sh
Normal file
41
Userland/Shell/Tests/function.sh
Normal file
|
@ -0,0 +1,41 @@
|
|||
#!/bin/sh
|
||||
|
||||
# Syntax ok?
|
||||
fn() { echo $* }
|
||||
|
||||
# Can we invoke that?
|
||||
test "$(fn 1)" = 1 || echo cannot invoke "'fn 1'" && exit 1
|
||||
test "$(fn 1 2)" = "1 2" || echo cannot invoke "'fn 1 2'" && exit 1
|
||||
|
||||
# With explicit argument names?
|
||||
fn(a) { echo $a }
|
||||
|
||||
# Can we invoke that?
|
||||
test "$(fn 1)" = 1 || echo cannot invoke "'fn 1'" with explicit names && exit 1
|
||||
test "$(fn 1 2)" = 1 || echo cannot invoke "'fn 1 2'" with explicit names and extra arguments && exit 1
|
||||
|
||||
# Can it fail?
|
||||
if fn 2>/dev/null {
|
||||
echo "'fn'" with an explicit argument is not failing with not enough args
|
||||
exit 1
|
||||
}
|
||||
|
||||
# $0 in function should be its name
|
||||
fn() { echo $0 }
|
||||
|
||||
test "$(fn)" = fn || echo '$0' in function not equal to its name && exit 1
|
||||
|
||||
# Ensure ARGV does not leak from inner frames.
|
||||
fn() {
|
||||
fn2 1 2 3
|
||||
echo $*
|
||||
}
|
||||
|
||||
fn2() { }
|
||||
|
||||
test "$(fn foobar)" = "foobar" || echo 'Frames are somehow messed up in nested functions' && exit 1
|
||||
|
||||
fn(xfoo) { }
|
||||
xfoo=1
|
||||
fn 2
|
||||
test $xfoo -eq 1 || echo 'Functions overwrite parent scopes' && exit 1
|
50
Userland/Shell/Tests/if.sh
Normal file
50
Userland/Shell/Tests/if.sh
Normal file
|
@ -0,0 +1,50 @@
|
|||
#!/bin/sh
|
||||
|
||||
setopt --verbose
|
||||
|
||||
if test 1 -eq 1 {
|
||||
# Are comments ok?
|
||||
# Basic 'if' structure, empty block.
|
||||
if true {
|
||||
} else {
|
||||
echo "if true runs false branch"
|
||||
exit 2
|
||||
}
|
||||
if false {
|
||||
echo "if false runs true branch"
|
||||
exit 2
|
||||
} else {
|
||||
}
|
||||
|
||||
# Basic 'if' structure, without 'else'
|
||||
if false {
|
||||
echo "Fail: 'if false' runs the branch"
|
||||
exit 2
|
||||
}
|
||||
|
||||
# Extended 'cond' form.
|
||||
if false {
|
||||
echo "Fail: 'if false' with 'else if' runs first branch"
|
||||
exit 2
|
||||
} else if true {
|
||||
} else {
|
||||
echo "Fail: 'if false' with 'else if' runs last branch"
|
||||
exit 2
|
||||
}
|
||||
|
||||
# FIXME: Some form of 'not' would be nice
|
||||
# &&/|| in condition
|
||||
if true || false {
|
||||
} else {
|
||||
echo "Fail: 'if true || false' runs false branch"
|
||||
exit 2
|
||||
}
|
||||
|
||||
if true && false {
|
||||
echo "Fail: 'if true && false' runs true branch"
|
||||
exit 2
|
||||
}
|
||||
} else {
|
||||
echo "Fail: 'if test 1 -eq 1' runs false branch"
|
||||
exit 1
|
||||
}
|
77
Userland/Shell/Tests/loop.sh
Normal file
77
Userland/Shell/Tests/loop.sh
Normal file
|
@ -0,0 +1,77 @@
|
|||
#!/bin/sh
|
||||
|
||||
singlecommand_ok=yes
|
||||
multicommand_ok=yes
|
||||
inlineexec_ok=yes
|
||||
implicit_ok=yes
|
||||
infinite_ok=''
|
||||
break_ok=yes
|
||||
continue_ok=yes
|
||||
break_in_infinite_ok=''
|
||||
|
||||
# Full form
|
||||
# Empty
|
||||
for x in () { }
|
||||
|
||||
# Empty block but nonempty list
|
||||
for x in (1 2 3) { }
|
||||
|
||||
# Single command in block
|
||||
for cmd in ((test 1 = 1) (test 2 = 2)) {
|
||||
$cmd || unset singlecommand_ok
|
||||
}
|
||||
|
||||
# Multiple commands in block
|
||||
for cmd in ((test 1 = 1) (test 2 = 2)) {
|
||||
test -z "$cmd"
|
||||
test -z "$cmd" && unset multicommand_ok
|
||||
|
||||
}
|
||||
|
||||
# $(...) as iterable expression
|
||||
test_file=sh-test-1
|
||||
echo 1 > $test_file
|
||||
echo 2 >> $test_file
|
||||
echo 3 >> $test_file
|
||||
echo 4 >> $test_file
|
||||
lst=()
|
||||
for line in $(cat $test_file) {
|
||||
lst=($lst $line)
|
||||
}
|
||||
test "$lst" = "1 2 3 4" || unset inlineexec_ok
|
||||
rm $test_file
|
||||
|
||||
# Implicit var
|
||||
for ((test 1 = 1) (test 2 = 2)) {
|
||||
$it || unset implicit_ok
|
||||
}
|
||||
|
||||
# Infinite loop
|
||||
loop {
|
||||
infinite_ok=yes
|
||||
break
|
||||
unset break_ok
|
||||
}
|
||||
|
||||
# 'Continue'
|
||||
for (1 2 3) {
|
||||
continue
|
||||
unset continue_ok
|
||||
}
|
||||
|
||||
# 'break' in infinite external loop
|
||||
for $(yes) {
|
||||
break_in_infinite_ok=yes
|
||||
break
|
||||
}
|
||||
|
||||
test $singlecommand_ok || echo Fail: Single command inside for body
|
||||
test $multicommand_ok || echo Fail: Multiple commands inside for body
|
||||
test $inlineexec_ok || echo Fail: Inline Exec
|
||||
test $implicit_ok || echo Fail: implicit iter variable
|
||||
test $infinite_ok || echo Fail: infinite loop
|
||||
test $break_ok || echo Fail: break
|
||||
test $continue_ok || echo Fail: continue
|
||||
test $break_in_infinite_ok || echo Fail: break from external infinite loop
|
||||
|
||||
test "$singlecommand_ok $multicommand_ok $inlineexec_ok $implicit_ok $infinite_ok $break_ok $continue_ok $break_in_infinite_ok" = "yes yes yes yes yes yes yes yes" || exit 1
|
83
Userland/Shell/Tests/match.sh
Normal file
83
Userland/Shell/Tests/match.sh
Normal file
|
@ -0,0 +1,83 @@
|
|||
#!/bin/Shell
|
||||
|
||||
result=no
|
||||
match hello {
|
||||
he* { result=yes }
|
||||
* { result=fail }
|
||||
};
|
||||
|
||||
test "$result" = yes || echo invalid result $result for normal string match, single option && exit 1
|
||||
|
||||
result=no
|
||||
match hello {
|
||||
he* | f* { result=yes }
|
||||
* { result=fail }
|
||||
};
|
||||
|
||||
test "$result" = yes || echo invalid result $result for normal string match, multiple options && exit 1
|
||||
|
||||
result=no
|
||||
match (well hello friends) {
|
||||
(* *) { result=fail }
|
||||
(* * *) { result=yes }
|
||||
* { result=fail }
|
||||
};
|
||||
|
||||
test "$result" = yes || echo invalid result $result for list match && exit 1
|
||||
|
||||
result=no
|
||||
match yes as v {
|
||||
() { result=fail }
|
||||
(*) { result=yes }
|
||||
* { result=$v }
|
||||
};
|
||||
|
||||
test "$result" = yes || echo invalid result $result for match with name && exit 1
|
||||
|
||||
result=no
|
||||
# $(...) is a list, $(echo) should be an empty list, not an empty string
|
||||
match $(echo) {
|
||||
* { result=fail }
|
||||
() { result=yes }
|
||||
};
|
||||
|
||||
test "$result" = yes || echo invalid result $result for list subst match && exit 1
|
||||
|
||||
result=no
|
||||
# "$(...)" is a string, "$(echo)" should be an empty string, not an empty list
|
||||
match "$(echo)" {
|
||||
* { result=yes }
|
||||
() { result=fail }
|
||||
};
|
||||
|
||||
test "$result" = yes || echo invalid result $result for string subst match && exit 1
|
||||
|
||||
match (foo bar) {
|
||||
(f? *) as (x y) {
|
||||
result=fail
|
||||
}
|
||||
(f* b*) as (x y) {
|
||||
if [ "$x" = oo -a "$y" = ar ] {
|
||||
result=yes
|
||||
} else {
|
||||
result=fail
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test "$result" = yes || echo invalid result $result for subst match with name && exit 1
|
||||
|
||||
match (foo bar baz) {
|
||||
(f? * *z) as (x y z) {
|
||||
result=fail
|
||||
}
|
||||
(f* b* *z) as (x y z) {
|
||||
if [ "$x" = oo -a "$y" = ar -a "$z" = ba ] {
|
||||
result=yes
|
||||
} else {
|
||||
result=fail
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test "$result" = yes || echo invalid result $result for subst match with name 2 && exit 1
|
13
Userland/Shell/Tests/sigpipe.sh
Normal file
13
Userland/Shell/Tests/sigpipe.sh
Normal file
|
@ -0,0 +1,13 @@
|
|||
#!/bin/sh
|
||||
|
||||
# `head -n 1` should close stdout of the `Shell -c` command, which means the
|
||||
# second echo should exit unsuccessfully and sigpipe.sh.out should not be
|
||||
# created.
|
||||
rm -f sigpipe.sh.out
|
||||
|
||||
{ echo foo && echo bar && echo baz > sigpipe.sh.out } | head -n 1 > /dev/null
|
||||
|
||||
# Failing commands don't make the test fail, just an explicit `exit 1` does.
|
||||
# So the test only fails if sigpipe.sh.out exists (since then `exit 1` runs),
|
||||
# not if the `test` statement returns false.
|
||||
test -e sigpipe.sh.out && exit 1
|
15
Userland/Shell/Tests/special-vars.sh
Normal file
15
Userland/Shell/Tests/special-vars.sh
Normal file
|
@ -0,0 +1,15 @@
|
|||
#!/bin/sh
|
||||
|
||||
test "$*" = "" || echo "Fail: Argv list not empty" && exit 1
|
||||
test "$#" -eq 0 || echo "Fail: Argv list empty but count non-zero" && exit 1
|
||||
test "$ARGV" = "$*" || echo "Fail: \$ARGV not equal to \$*" && exit 1
|
||||
|
||||
ARGV=(1 2 3)
|
||||
test "$#" -eq 3 || echo "Fail: Assignment to ARGV does not affect \$#" && exit 1
|
||||
test "$*" = "1 2 3" || echo "Fail: Assignment to ARGV does not affect \$*" && exit 1
|
||||
|
||||
shift
|
||||
test "$*" = "2 3" || echo "Fail: 'shift' does not work correctly" && exit 1
|
||||
|
||||
shift 2
|
||||
test "$*" = "" || echo "Fail: 'shift 2' does not work correctly" && exit 1
|
28
Userland/Shell/Tests/subshell.sh
Normal file
28
Userland/Shell/Tests/subshell.sh
Normal file
|
@ -0,0 +1,28 @@
|
|||
#/bin/sh
|
||||
|
||||
setopt --verbose
|
||||
|
||||
rm -rf shell-test
|
||||
mkdir shell-test
|
||||
cd shell-test
|
||||
|
||||
# Simple sequence (grouping)
|
||||
{ echo test > testfile }
|
||||
test "$(cat testfile)" = "test" || echo cannot write to file in subshell && exit 1
|
||||
|
||||
# Simple sequence - many commands
|
||||
{ echo test1 > testfile; echo test2 > testfile }
|
||||
test "$(cat testfile)" = "test2" || echo cannot write to file in subshell 2 && exit 1
|
||||
|
||||
|
||||
# Does it exit with the last exit code?
|
||||
{ test -z "a" }
|
||||
exitcode=$?
|
||||
test "$exitcode" -eq 1 || echo exits with $exitcode when it should exit with 1 && exit 1
|
||||
|
||||
{ test -z "a" || echo test }
|
||||
exitcode=$?
|
||||
test "$exitcode" -eq 0 || echo exits with $exitcode when it should exit with 0 && exit 1
|
||||
|
||||
cd ..
|
||||
rm -rf shell-test
|
92
Userland/Shell/Tests/valid.sh
Normal file
92
Userland/Shell/Tests/valid.sh
Normal file
|
@ -0,0 +1,92 @@
|
|||
#!/bin/sh
|
||||
|
||||
# Are comments ignored?
|
||||
# Sanity check: can we do && and || ?
|
||||
true || exit 2
|
||||
false
|
||||
|
||||
# Apply some useful aliases
|
||||
fail() {
|
||||
echo $*
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Can we chain &&'s?
|
||||
false && exit 2 && fail "can't chain &&'s"
|
||||
|
||||
# Proper precedence between &&'s and ||'s
|
||||
false && exit 2 || true && false && fail Invalid precedence between '&&' and '||'
|
||||
|
||||
|
||||
# Sanity check: can we pass arguments to 'test'?
|
||||
test yes = yes || exit 2
|
||||
|
||||
# Sanity check: can we use $(command)?
|
||||
test "$(echo yes)" = yes || exit 2
|
||||
# Redirections.
|
||||
test -z "$(echo foo > /dev/null)" || fail direct path redirection
|
||||
test -z "$(echo foo 2> /dev/null 1>&2)" || fail indirect redirection
|
||||
test -n "$(echo foo 2> /dev/null)" || fail fds interfere with each other
|
||||
|
||||
# Argument unpack
|
||||
test "$(echo (yes))" = yes || fail arguments inside bare lists
|
||||
test "$(echo (no)() yes)" = yes || fail arguments inside juxtaposition: empty
|
||||
test "$(echo (y)(es))" = yes || fail arguments inside juxtaposition: list
|
||||
test "$(echo "y"es)" = yes || fail arguments inside juxtaposition: string
|
||||
|
||||
# String substitution
|
||||
foo=yes
|
||||
test "$(echo $foo)" = yes || fail simple string var lookup
|
||||
test "$(echo "$foo")" = yes || fail stringified string var lookup
|
||||
|
||||
# List substitution
|
||||
foo=(yes)
|
||||
# Static lookup, as list
|
||||
test "$(echo $foo)" = yes || fail simple list var lookup
|
||||
# Static lookup, stringified
|
||||
test "$(echo "$foo")" = yes || fail stringified list var lookup
|
||||
# Dynamic lookup through static expression
|
||||
test "$(echo $'foo')" = yes || fail dynamic lookup through static exp
|
||||
# Dynamic lookup through dynamic expression
|
||||
ref_to_foo=foo
|
||||
test "$(echo $"$ref_to_foo")" = yes || fail dynamic lookup through dynamic exp
|
||||
|
||||
# More redirections
|
||||
echo test > /tmp/sh-test
|
||||
test "$(cat /tmp/sh-test)" = test || fail simple path redirect
|
||||
rm /tmp/sh-test
|
||||
|
||||
# 'brace' expansions
|
||||
test "$(echo x(yes no))" = "xyes xno" || fail simple juxtaposition expansion
|
||||
test "$(echo (y n)(es o))" = "yes yo nes no" || fail list-list juxtaposition expansion
|
||||
test "$(echo ()(foo bar baz))" = "" || fail empty expansion
|
||||
|
||||
# Variables inside commands
|
||||
to_devnull=(>/dev/null)
|
||||
test "$(echo hewwo $to_devnull)" = "" || fail variable containing simple command
|
||||
|
||||
word_count=(() | wc -w)
|
||||
test "$(echo well hello friends $word_count)" -eq 3 || fail variable containing pipeline
|
||||
|
||||
# Globs
|
||||
mkdir sh-test
|
||||
pushd sh-test
|
||||
touch (a b c)(d e f)
|
||||
test "$(echo a*)" = "ad ae af" || fail '*' glob expansion
|
||||
test "$(echo a?)" = "ad ae af" || fail '?' glob expansion
|
||||
|
||||
glob_in_var='*'
|
||||
test "$(echo $glob_in_var)" = '*' || fail substituted string acts as glob
|
||||
|
||||
test "$(echo (a*))" = "ad ae af" || fail globs in lists resolve wrong
|
||||
test "$(echo x(a*))" = "xad xae xaf" || fail globs in lists do not resolve to lists
|
||||
test "$(echo "foo"a*)" = "fooad fooae fooaf" || fail globs join to dquoted strings
|
||||
popd
|
||||
rm -fr sh-test
|
||||
|
||||
# Setopt
|
||||
setopt --inline_exec_keep_empty_segments
|
||||
test "$(echo -n "a\n\nb")" = "a b" || fail inline_exec_keep_empty_segments has no effect
|
||||
|
||||
setopt --no_inline_exec_keep_empty_segments
|
||||
test "$(echo -n "a\n\nb")" = "a b" || fail cannot unset inline_exec_keep_empty_segments
|
194
Userland/Shell/main.cpp
Normal file
194
Userland/Shell/main.cpp
Normal file
|
@ -0,0 +1,194 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||
* 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 "Execution.h"
|
||||
#include "Shell.h"
|
||||
#include <LibCore/ArgsParser.h>
|
||||
#include <LibCore/Event.h>
|
||||
#include <LibCore/EventLoop.h>
|
||||
#include <LibCore/File.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
RefPtr<Line::Editor> editor;
|
||||
Shell::Shell* s_shell;
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
Core::EventLoop loop;
|
||||
|
||||
Core::EventLoop::register_signal(SIGINT, [](int) {
|
||||
s_shell->kill_job(s_shell->current_job(), SIGINT);
|
||||
});
|
||||
|
||||
Core::EventLoop::register_signal(SIGWINCH, [](int) {
|
||||
s_shell->kill_job(s_shell->current_job(), SIGWINCH);
|
||||
});
|
||||
|
||||
Core::EventLoop::register_signal(SIGTTIN, [](int) {});
|
||||
Core::EventLoop::register_signal(SIGTTOU, [](int) {});
|
||||
|
||||
Core::EventLoop::register_signal(SIGHUP, [](int) {
|
||||
for (auto& it : s_shell->jobs)
|
||||
s_shell->kill_job(it.value.ptr(), SIGHUP);
|
||||
|
||||
s_shell->editor()->save_history(s_shell->get_history_path());
|
||||
});
|
||||
|
||||
editor = Line::Editor::construct();
|
||||
editor->initialize();
|
||||
|
||||
auto shell = Shell::Shell::construct(*editor);
|
||||
s_shell = shell.ptr();
|
||||
|
||||
s_shell->setup_signals();
|
||||
|
||||
#ifndef __serenity__
|
||||
sigset_t blocked;
|
||||
sigemptyset(&blocked);
|
||||
sigaddset(&blocked, SIGTTOU);
|
||||
sigaddset(&blocked, SIGTTIN);
|
||||
pthread_sigmask(SIG_BLOCK, &blocked, nullptr);
|
||||
#endif
|
||||
#ifdef __serenity__
|
||||
if (pledge("stdio rpath wpath cpath proc exec tty accept sigaction unix fattr", nullptr) < 0) {
|
||||
perror("pledge");
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
shell->termios = editor->termios();
|
||||
shell->default_termios = editor->default_termios();
|
||||
|
||||
editor->on_display_refresh = [&](auto& editor) {
|
||||
editor.strip_styles();
|
||||
if (shell->should_format_live()) {
|
||||
auto line = editor.line();
|
||||
ssize_t cursor = editor.cursor();
|
||||
editor.clear_line();
|
||||
editor.insert(shell->format(line, cursor));
|
||||
if (cursor >= 0)
|
||||
editor.set_cursor(cursor);
|
||||
}
|
||||
shell->highlight(editor);
|
||||
};
|
||||
editor->on_tab_complete = [&](const Line::Editor&) {
|
||||
return shell->complete();
|
||||
};
|
||||
|
||||
const char* command_to_run = nullptr;
|
||||
const char* file_to_read_from = nullptr;
|
||||
Vector<const char*> script_args;
|
||||
bool skip_rc_files = false;
|
||||
const char* format = nullptr;
|
||||
bool should_format_live = false;
|
||||
|
||||
Core::ArgsParser parser;
|
||||
parser.add_option(command_to_run, "String to read commands from", "command-string", 'c', "command-string");
|
||||
parser.add_option(skip_rc_files, "Skip running shellrc files", "skip-shellrc", 0);
|
||||
parser.add_option(format, "Format the given file into stdout and exit", "format", 0, "file");
|
||||
parser.add_option(should_format_live, "Enable live formatting", "live-formatting", 'f');
|
||||
parser.add_positional_argument(file_to_read_from, "File to read commands from", "file", Core::ArgsParser::Required::No);
|
||||
parser.add_positional_argument(script_args, "Extra argumets to pass to the script (via $* and co)", "argument", Core::ArgsParser::Required::No);
|
||||
|
||||
parser.parse(argc, argv);
|
||||
|
||||
shell->set_live_formatting(should_format_live);
|
||||
|
||||
if (format) {
|
||||
auto file = Core::File::open(format, Core::IODevice::ReadOnly);
|
||||
if (file.is_error()) {
|
||||
fprintf(stderr, "Error: %s", file.error().characters());
|
||||
return 1;
|
||||
}
|
||||
|
||||
ssize_t cursor = -1;
|
||||
puts(shell->format(file.value()->read_all(), cursor).characters());
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto pid = getpid();
|
||||
if (auto sid = getsid(pid); sid == 0) {
|
||||
if (setsid() < 0) {
|
||||
perror("setsid");
|
||||
// Let's just hope that it's ok.
|
||||
}
|
||||
} else if (sid != pid) {
|
||||
if (getpgid(pid) != pid) {
|
||||
dbgln("We were already in a session with sid={} (we are {}), let's do some gymnastics", sid, pid);
|
||||
if (setpgid(pid, sid) < 0) {
|
||||
auto strerr = strerror(errno);
|
||||
dbgln("couldn't setpgid: {}", strerr);
|
||||
}
|
||||
if (setsid() < 0) {
|
||||
auto strerr = strerror(errno);
|
||||
dbgln("couldn't setsid: {}", strerr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shell->current_script = argv[0];
|
||||
|
||||
if (!skip_rc_files) {
|
||||
auto run_rc_file = [&](auto& name) {
|
||||
String file_path = name;
|
||||
if (file_path.starts_with('~'))
|
||||
file_path = shell->expand_tilde(file_path);
|
||||
if (Core::File::exists(file_path)) {
|
||||
shell->run_file(file_path, false);
|
||||
}
|
||||
};
|
||||
run_rc_file(Shell::Shell::global_init_file_path);
|
||||
run_rc_file(Shell::Shell::local_init_file_path);
|
||||
}
|
||||
|
||||
{
|
||||
Vector<String> args;
|
||||
for (auto* arg : script_args)
|
||||
args.empend(arg);
|
||||
shell->set_local_variable("ARGV", adopt(*new Shell::AST::ListValue(move(args))));
|
||||
}
|
||||
|
||||
if (command_to_run) {
|
||||
dbgln("sh -c '{}'\n", command_to_run);
|
||||
shell->run_command(command_to_run);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (file_to_read_from && StringView { "-" } != file_to_read_from) {
|
||||
if (shell->run_file(file_to_read_from))
|
||||
return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
shell->add_child(*editor);
|
||||
|
||||
Core::EventLoop::current().post_event(*shell, make<Core::CustomEvent>(Shell::Shell::ShellEventType::ReadLine));
|
||||
|
||||
return loop.exec();
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue