Serenity Operating System
at master 266 lines 11 kB view raw
1/* 2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> 3 * 4 * SPDX-License-Identifier: BSD-2-Clause 5 */ 6 7#include "Shell.h" 8#include <AK/LexicalPath.h> 9#include <LibCore/ArgsParser.h> 10#include <LibCore/DeprecatedFile.h> 11#include <LibCore/Event.h> 12#include <LibCore/EventLoop.h> 13#include <LibCore/System.h> 14#include <LibMain/Main.h> 15#include <signal.h> 16#include <stdio.h> 17#include <string.h> 18#include <unistd.h> 19 20RefPtr<Line::Editor> editor; 21Shell::Shell* s_shell; 22 23ErrorOr<int> serenity_main(Main::Arguments arguments) 24{ 25 Core::EventLoop loop; 26 27 Core::EventLoop::register_signal(SIGINT, [](int) { 28 s_shell->kill_job(s_shell->current_job(), SIGINT); 29 }); 30 31 Core::EventLoop::register_signal(SIGWINCH, [](int) { 32 s_shell->kill_job(s_shell->current_job(), SIGWINCH); 33 }); 34 35 Core::EventLoop::register_signal(SIGTTIN, [](int) {}); 36 Core::EventLoop::register_signal(SIGTTOU, [](int) {}); 37 38 Core::EventLoop::register_signal(SIGHUP, [](int) { 39 for (auto& it : s_shell->jobs) 40 s_shell->kill_job(it.value.ptr(), SIGHUP); 41 42 s_shell->editor()->save_history(s_shell->get_history_path()); 43 }); 44 45 TRY(Core::System::pledge("stdio rpath wpath cpath proc exec tty sigaction unix fattr")); 46 47 RefPtr<::Shell::Shell> shell; 48 bool attempt_interactive = false; 49 50 auto initialize = [&](bool posix_mode) { 51 auto configuration = Line::Configuration::from_config(); 52 if (!attempt_interactive) { 53 configuration.set(Line::Configuration::Flags::None); 54 configuration.set(Line::Configuration::SignalHandler::NoSignalHandlers); 55 configuration.set(Line::Configuration::OperationMode::NonInteractive); 56 configuration.set(Line::Configuration::RefreshBehavior::Eager); 57 } 58 59 editor = Line::Editor::construct(move(configuration)); 60 editor->initialize(); 61 62 shell = Shell::Shell::construct(*editor, attempt_interactive, posix_mode || LexicalPath::basename(arguments.strings[0]) == "sh"sv); 63 s_shell = shell.ptr(); 64 65 s_shell->setup_signals(); 66 67 sigset_t blocked; 68 sigemptyset(&blocked); 69 sigaddset(&blocked, SIGTTOU); 70 sigaddset(&blocked, SIGTTIN); 71 pthread_sigmask(SIG_BLOCK, &blocked, nullptr); 72 73 shell->termios = editor->termios(); 74 shell->default_termios = editor->default_termios(); 75 76 editor->on_display_refresh = [&](auto& editor) { 77 editor.strip_styles(); 78 if (shell->should_format_live()) { 79 auto line = editor.line(); 80 ssize_t cursor = editor.cursor(); 81 editor.clear_line(); 82 editor.insert(shell->format(line, cursor)); 83 if (cursor >= 0) 84 editor.set_cursor(cursor); 85 } 86 (void)shell->highlight(editor); 87 }; 88 editor->on_tab_complete = [&](const Line::Editor&) { 89 return shell->complete(); 90 }; 91 editor->on_paste = [&](Utf32View data, Line::Editor& editor) { 92 auto line = editor.line(editor.cursor()); 93 Shell::Parser parser(line, false); 94 auto ast = parser.parse(); 95 if (!ast) { 96 editor.insert(data); 97 return; 98 } 99 100 auto hit_test_result = ast->hit_test_position(editor.cursor()); 101 // If the argument isn't meant to be an entire command, escape it. 102 // This allows copy-pasting entire commands where commands are expected, and otherwise escapes everything. 103 auto should_escape = false; 104 if (!hit_test_result.matching_node && hit_test_result.closest_command_node) { 105 // There's *some* command, but our cursor is immediate after it 106 should_escape = editor.cursor() >= hit_test_result.closest_command_node->position().end_offset; 107 hit_test_result.matching_node = hit_test_result.closest_command_node; 108 } else if (hit_test_result.matching_node && hit_test_result.closest_command_node) { 109 // There's a command, and we're at the end of or in the middle of some node. 110 auto leftmost_literal = hit_test_result.closest_command_node->leftmost_trivial_literal(); 111 if (leftmost_literal) 112 should_escape = !hit_test_result.matching_node->position().contains(leftmost_literal->position().start_offset); 113 } 114 115 if (should_escape) { 116 DeprecatedString escaped_string; 117 Optional<char> trivia {}; 118 bool starting_trivia_already_provided = false; 119 auto escape_mode = Shell::Shell::EscapeMode::Bareword; 120 if (hit_test_result.matching_node->kind() == Shell::AST::Node::Kind::StringLiteral) { 121 // If we're pasting in a string literal, make sure to only consider that specific escape mode 122 auto* node = static_cast<Shell::AST::StringLiteral const*>(hit_test_result.matching_node.ptr()); 123 switch (node->enclosure_type()) { 124 case Shell::AST::StringLiteral::EnclosureType::None: 125 break; 126 case Shell::AST::StringLiteral::EnclosureType::SingleQuotes: 127 escape_mode = Shell::Shell::EscapeMode::SingleQuotedString; 128 trivia = '\''; 129 starting_trivia_already_provided = true; 130 break; 131 case Shell::AST::StringLiteral::EnclosureType::DoubleQuotes: 132 escape_mode = Shell::Shell::EscapeMode::DoubleQuotedString; 133 trivia = '"'; 134 starting_trivia_already_provided = true; 135 break; 136 } 137 } 138 139 if (starting_trivia_already_provided) { 140 escaped_string = shell->escape_token(data, escape_mode); 141 } else { 142 escaped_string = shell->escape_token(data, Shell::Shell::EscapeMode::Bareword); 143 if (auto string = shell->escape_token(data, Shell::Shell::EscapeMode::SingleQuotedString); string.length() + 2 < escaped_string.length()) { 144 escaped_string = move(string); 145 trivia = '\''; 146 } 147 if (auto string = shell->escape_token(data, Shell::Shell::EscapeMode::DoubleQuotedString); string.length() + 2 < escaped_string.length()) { 148 escaped_string = move(string); 149 trivia = '"'; 150 } 151 } 152 153 if (trivia.has_value() && !starting_trivia_already_provided) 154 editor.insert(*trivia); 155 156 editor.insert(escaped_string); 157 158 if (trivia.has_value()) 159 editor.insert(*trivia); 160 } else { 161 editor.insert(data); 162 } 163 }; 164 }; 165 166 StringView command_to_run = {}; 167 StringView file_to_read_from = {}; 168 Vector<StringView> script_args; 169 bool skip_rc_files = false; 170 StringView format; 171 bool should_format_live = false; 172 bool keep_open = false; 173 bool posix_mode = false; 174 175 Core::ArgsParser parser; 176 parser.add_option(command_to_run, "String to read commands from", "command-string", 'c', "command-string"); 177 parser.add_option(skip_rc_files, "Skip running shellrc files", "skip-shellrc", 0); 178 parser.add_option(format, "Format the given file into stdout and exit", "format", 0, "file"); 179 parser.add_option(should_format_live, "Enable live formatting", "live-formatting", 'f'); 180 parser.add_option(keep_open, "Keep the shell open after running the specified command or file", "keep-open", 0); 181 parser.add_option(posix_mode, "Behave like a POSIX-compatible shell", "posix", 0); 182 parser.add_positional_argument(file_to_read_from, "File to read commands from", "file", Core::ArgsParser::Required::No); 183 parser.add_positional_argument(script_args, "Extra arguments to pass to the script (via $* and co)", "argument", Core::ArgsParser::Required::No); 184 185 parser.set_stop_on_first_non_option(true); 186 parser.parse(arguments); 187 188 if (!format.is_empty()) { 189 auto file = TRY(Core::DeprecatedFile::open(format, Core::OpenMode::ReadOnly)); 190 191 initialize(posix_mode); 192 193 ssize_t cursor = -1; 194 puts(shell->format(file->read_all(), cursor).characters()); 195 return 0; 196 } 197 198 auto pid = getpid(); 199 if (auto sid = getsid(pid); sid == 0) { 200 if (auto res = Core::System::setsid(); res.is_error()) 201 dbgln("{}", res.release_error()); 202 } else if (sid != pid) { 203 if (getpgid(pid) != pid) { 204 if (auto res = Core::System::setpgid(pid, sid); res.is_error()) 205 dbgln("{}", res.release_error()); 206 207 if (auto res = Core::System::setsid(); res.is_error()) 208 dbgln("{}", res.release_error()); 209 } 210 } 211 212 auto execute_file = !file_to_read_from.is_empty() && "-"sv != file_to_read_from; 213 attempt_interactive = !execute_file && (command_to_run.is_empty() || keep_open); 214 215 if (keep_open && command_to_run.is_empty() && !execute_file) { 216 warnln("Option --keep-open can only be used in combination with -c or when specifying a file to execute."); 217 return 1; 218 } 219 220 initialize(posix_mode); 221 222 shell->set_live_formatting(should_format_live); 223 shell->current_script = arguments.strings[0]; 224 225 if (!skip_rc_files) { 226 auto run_rc_file = [&](auto& name) { 227 DeprecatedString file_path = name; 228 if (file_path.starts_with('~')) 229 file_path = shell->expand_tilde(file_path); 230 if (Core::DeprecatedFile::exists(file_path)) { 231 shell->run_file(file_path, false); 232 } 233 }; 234 run_rc_file(Shell::Shell::global_init_file_path); 235 run_rc_file(Shell::Shell::local_init_file_path); 236 shell->cache_path(); 237 } 238 239 Vector<String> args_to_pass; 240 TRY(args_to_pass.try_ensure_capacity(script_args.size())); 241 for (auto& arg : script_args) 242 TRY(args_to_pass.try_append(TRY(String::from_utf8(arg)))); 243 244 shell->set_local_variable("ARGV", adopt_ref(*new Shell::AST::ListValue(move(args_to_pass)))); 245 246 if (!command_to_run.is_empty()) { 247 auto result = shell->run_command(command_to_run); 248 if (!keep_open) 249 return result; 250 } 251 252 if (execute_file) { 253 auto result = shell->run_file(file_to_read_from); 254 if (!keep_open) { 255 if (result) 256 return 0; 257 return 1; 258 } 259 } 260 261 shell->add_child(*editor); 262 263 Core::EventLoop::current().post_event(*shell, make<Core::CustomEvent>(Shell::Shell::ShellEventType::ReadLine)); 264 265 return loop.exec(); 266}