1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-27 13:57:35 +00:00

Shell: Move to Userland/Shell/

This commit is contained in:
Andreas Kling 2021-01-12 11:53:14 +01:00
parent 07c7e35372
commit c4e2fd8123
35 changed files with 11 additions and 9 deletions

2996
Userland/Shell/AST.cpp Normal file

File diff suppressed because it is too large Load diff

1324
Userland/Shell/AST.h Normal file

File diff suppressed because it is too large Load diff

929
Userland/Shell/Builtin.cpp Normal file
View 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;
}
}

View 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)

View 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;
};
}

View 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
View 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
View 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
View 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
View 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;
};
}

View 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));
}
}

View 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

File diff suppressed because it is too large Load diff

262
Userland/Shell/Parser.h Normal file
View 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

File diff suppressed because it is too large Load diff

351
Userland/Shell/Shell.h Normal file
View 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;
}
}

View 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

View 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

View 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'

View 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

View 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

View 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
}

View 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

View 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

View 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

View 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

View 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

View 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
View 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();
}