diff options
author | Eli Youngs <eli.m.youngs@gmail.com> | 2022-12-17 01:06:26 -0800 |
---|---|---|
committer | Andrew Kaster <andrewdkaster@gmail.com> | 2023-01-06 13:52:21 -0700 |
commit | 0ecbc5c02a5a91c5c92b479d4f9fce45149a3426 (patch) | |
tree | 7fc345dd723716a3fc6f4454cb18af6fd39d2859 /Userland | |
parent | 87a961534f5424c340c59515d453b81eab7b81d3 (diff) | |
download | serenity-0ecbc5c02a5a91c5c92b479d4f9fce45149a3426.zip |
Userland: Add a sed utility
Diffstat (limited to 'Userland')
-rw-r--r-- | Userland/Utilities/CMakeLists.txt | 1 | ||||
-rw-r--r-- | Userland/Utilities/sed.cpp | 103 |
2 files changed, 104 insertions, 0 deletions
diff --git a/Userland/Utilities/CMakeLists.txt b/Userland/Utilities/CMakeLists.txt index 52e67a629d..3cf14b4a39 100644 --- a/Userland/Utilities/CMakeLists.txt +++ b/Userland/Utilities/CMakeLists.txt @@ -112,6 +112,7 @@ target_link_libraries(pkill PRIVATE LibRegex) target_link_libraries(pls PRIVATE LibCrypt) target_link_libraries(pro PRIVATE LibProtocol LibHTTP) target_link_libraries(run-tests PRIVATE LibRegex LibCoredump LibDebug) +target_link_libraries(sed PRIVATE LibRegex) target_link_libraries(shot PRIVATE LibGfx LibGUI LibIPC) target_link_libraries(sql PRIVATE LibLine LibSQL LibIPC) target_link_libraries(su PRIVATE LibCrypt) diff --git a/Userland/Utilities/sed.cpp b/Userland/Utilities/sed.cpp new file mode 100644 index 0000000000..ebb814bd52 --- /dev/null +++ b/Userland/Utilities/sed.cpp @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2022, Eli Youngs <eli.m.youngs@gmail.com> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include <AK/GenericLexer.h> +#include <AK/Vector.h> +#include <LibCore/ArgsParser.h> +#include <LibCore/Stream.h> +#include <LibCore/System.h> +#include <LibMain/Main.h> +#include <LibRegex/RegexMatcher.h> +#include <LibRegex/RegexOptions.h> + +struct SubstitutionCommand { + Regex<PosixExtended> regex; + StringView replacement; + PosixOptions options; +}; + +static ErrorOr<SubstitutionCommand> parse_command(StringView command) +{ + auto generic_error_message = "Incomplete substitution command"sv; + + auto lexer = GenericLexer(command); + + auto address = lexer.consume_until('s'); + if (!address.is_empty()) + warnln("sed: Addresses are currently ignored"); + + if (!lexer.consume_specific('s')) + return Error::from_string_view(generic_error_message); + + if (lexer.is_eof()) + return Error::from_string_view(generic_error_message); + + auto delimiter = lexer.consume(); + if (delimiter == '\n' || delimiter == '\\') + return Error::from_string_literal("\\n and \\ cannot be used as delimiters."); + + auto pattern = lexer.consume_until(delimiter); + if (pattern.is_empty()) + return Error::from_string_literal("Substitution patterns cannot be empty."); + + if (!lexer.consume_specific(delimiter)) + return Error::from_string_view(generic_error_message); + + auto replacement = lexer.consume_until(delimiter); + + // According to Posix, "s/x/y" is an invalid substitution command. + // It must have a closing delimiter: "s/x/y/" + if (!lexer.consume_specific(delimiter)) + return Error::from_string_literal("The substitution command was not properly terminated."); + + PosixOptions const options = PosixOptions(PosixFlags::Global | PosixFlags::SingleMatch); + + auto flags = lexer.consume_all(); + if (!flags.is_empty()) + warnln("sed: Flags are currently ignored"); + + return SubstitutionCommand { Regex<PosixExtended> { pattern }, replacement, options }; +} + +ErrorOr<int> serenity_main(Main::Arguments args) +{ + TRY(Core::System::pledge("stdio rpath")); + + Core::ArgsParser args_parser; + + StringView command_input; + Vector<StringView> filepaths; + + args_parser.add_positional_argument(command_input, "Command", "command_input", Core::ArgsParser::Required::Yes); + args_parser.add_positional_argument(filepaths, "File", "file", Core::ArgsParser::Required::No); + + args_parser.parse(args); + + auto command = TRY(parse_command(command_input)); + + if (filepaths.is_empty()) + filepaths = { "-"sv }; + + Array<u8, PAGE_SIZE> buffer {}; + for (auto const& filepath : filepaths) { + auto file_unbuffered = TRY(Core::Stream::File::open_file_or_standard_stream(filepath, Core::Stream::OpenMode::Read)); + auto file = TRY(Core::Stream::BufferedFile::create(move(file_unbuffered))); + + while (!file->is_eof()) { + auto line = TRY(file->read_line(buffer)); + + // Substitutions can apply to blank lines in the middle of a file, + // but not to the trailing newline that marks the end of a file. + if (line.is_empty() && file->is_eof()) + break; + + auto result = command.regex.replace(line, command.replacement, command.options); + outln(result); + } + } + + return 0; +} |