From ef551a045d2856cdca4c7164b1383126e2855baf Mon Sep 17 00:00:00 2001 From: Ali Mohammad Pur Date: Tue, 18 Apr 2023 18:53:17 +0330 Subject: [PATCH] Shell: Add support for the POSIX 'read' builtin --- Userland/Shell/Builtin.cpp | 92 ++++++++++++++++++++++++++++++++++++++ Userland/Shell/Shell.h | 1 + 2 files changed, 93 insertions(+) diff --git a/Userland/Shell/Builtin.cpp b/Userland/Shell/Builtin.cpp index 143b63ceba..c6ca3fb3bd 100644 --- a/Userland/Shell/Builtin.cpp +++ b/Userland/Shell/Builtin.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -1781,6 +1782,97 @@ ErrorOr Shell::builtin_argsparser_parse(Main::Arguments arguments) return 0; } +ErrorOr Shell::builtin_read(Main::Arguments arguments) +{ + if (!m_in_posix_mode) { + raise_error(ShellError::EvaluatedSyntaxError, "read: POSIX builtin used in non-POSIX mode"); + return 1; + } + + bool no_escape = false; + Vector variables; + + Core::ArgsParser parser; + parser.add_option(no_escape, "Do not interpret backslash escapes", "no-escape", 'r'); + parser.add_positional_argument(variables, "Variables to read into", "variable", Core::ArgsParser::Required::Yes); + + if (!parser.parse(arguments, Core::ArgsParser::FailureBehavior::Ignore)) + return 1; + + auto split_by_any_of = " \t\n"_short_string; + + if (auto const* value_from_env = getenv("IFS"); value_from_env) + split_by_any_of = TRY(String::from_utf8({ value_from_env, strlen(value_from_env) })); + else if (auto split_by_variable = TRY(lookup_local_variable("IFS"sv)); split_by_variable) + split_by_any_of = TRY(const_cast(*split_by_variable).resolve_as_string(*this)); + + auto file = TRY(Core::File::standard_input()); + auto buffered_stream = TRY(Core::BufferedFile::create(move(file))); + + StringBuilder builder; + ByteBuffer buffer; + + enum class LineState { + Done, + EscapedNewline, + }; + auto read_line = [&]() -> ErrorOr { + if (m_is_interactive && isatty(STDIN_FILENO)) { + // Show prompt + warn("read: "); + } + size_t attempted_line_size = 32; + + for (;;) { + auto result = buffered_stream->read_line(TRY(buffer.get_bytes_for_writing(attempted_line_size))); + if (result.is_error() && result.error().is_errno() && result.error().code() == EMSGSIZE) { + attempted_line_size *= 2; + continue; + } + + auto used_bytes = TRY(move(result)); + if (!no_escape && used_bytes.ends_with("\\\n"sv)) { + builder.append(used_bytes.substring_view(0, used_bytes.length() - 2)); + return LineState::EscapedNewline; + } + + if (used_bytes.ends_with("\n"sv)) + used_bytes = used_bytes.substring_view(0, used_bytes.length() - 1); + + builder.append(used_bytes); + return LineState::Done; + } + }; + + LineState state; + do { + state = TRY(read_line()); + } while (state == LineState::EscapedNewline); + + auto line = builder.string_view(); + if (variables.size() == 1) { + set_local_variable(variables[0], make_ref_counted(TRY(String::from_utf8(line)))); + return 0; + } + + auto fields = line.split_view_if(is_any_of(split_by_any_of), SplitBehavior::KeepEmpty); + + for (size_t i = 0; i < variables.size(); ++i) { + auto& variable = variables[i]; + StringView variable_value; + if (i >= fields.size()) + variable_value = ""sv; + else if (i == variables.size() - 1) + variable_value = line.substring_view_starting_from_substring(fields[i]); + else + variable_value = fields[i]; + + set_local_variable(variable, make_ref_counted(TRY(String::from_utf8(variable_value)))); + } + + return 0; +} + bool Shell::has_builtin(StringView name) const { if (name == ":"sv) diff --git a/Userland/Shell/Shell.h b/Userland/Shell/Shell.h index c875446de2..2babf1a4ce 100644 --- a/Userland/Shell/Shell.h +++ b/Userland/Shell/Shell.h @@ -56,6 +56,7 @@ __ENUMERATE_SHELL_BUILTIN(noop) \ __ENUMERATE_SHELL_BUILTIN(break) \ __ENUMERATE_SHELL_BUILTIN(continue) \ + __ENUMERATE_SHELL_BUILTIN(read) \ __ENUMERATE_SHELL_BUILTIN(argsparser_parse) #define ENUMERATE_SHELL_OPTIONS() \