mirror of
https://github.com/RGBCube/serenity
synced 2025-05-14 07:44:59 +00:00

Similar to POSIX read, the basic read and write functions of AK::Stream do not have a lower limit of how much data they read or write (apart from "none at all"). Rename the functions to "read some [data]" and "write some [data]" (with "data" being omitted, since everything here is reading and writing data) to make them sufficiently distinct from the functions that ensure to use the entire buffer (which should be the go-to function for most usages). No functional changes, just a lot of new FIXMEs.
152 lines
5.1 KiB
C++
152 lines
5.1 KiB
C++
/*
|
|
* Copyright (c) 2022, Eli Youngs <eli.m.youngs@gmail.com>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <AK/CharacterTypes.h>
|
|
#include <AK/GenericLexer.h>
|
|
#include <AK/Vector.h>
|
|
#include <LibCore/ArgsParser.h>
|
|
#include <LibCore/File.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;
|
|
Optional<StringView> output_filepath;
|
|
};
|
|
|
|
static Vector<StringView> split_flags(StringView const& input)
|
|
{
|
|
Vector<StringView> flags;
|
|
|
|
auto lexer = GenericLexer(input);
|
|
while (!lexer.is_eof()) {
|
|
StringView flag;
|
|
|
|
if (lexer.next_is(is_ascii_digit)) {
|
|
flag = lexer.consume_while(is_ascii_digit);
|
|
} else if (lexer.peek() == 'w') {
|
|
flag = lexer.consume_all();
|
|
} else {
|
|
flag = lexer.consume(1);
|
|
}
|
|
|
|
flags.append(flag);
|
|
}
|
|
|
|
return flags;
|
|
}
|
|
|
|
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 options = PosixOptions(PosixFlags::Global | PosixFlags::SingleMatch);
|
|
Optional<StringView> output_filepath;
|
|
|
|
auto flags = split_flags(lexer.consume_all());
|
|
for (auto const& flag : flags) {
|
|
if (flag.starts_with('w')) {
|
|
auto flag_filepath = flag.substring_view(1).trim_whitespace();
|
|
if (flag_filepath.is_empty())
|
|
return Error::from_string_literal("No filepath was provided for the 'w' flag.");
|
|
output_filepath = flag_filepath;
|
|
} else if (flag == "g"sv) {
|
|
// Allow multiple matches per line by un-setting the SingleMatch flag
|
|
options &= ~PosixFlags::SingleMatch;
|
|
} else if (flag == "i"sv || flag == "I"sv) {
|
|
options |= PosixFlags::Insensitive;
|
|
} else {
|
|
warnln("sed: Unsupported flag: {}", flag);
|
|
}
|
|
}
|
|
|
|
return SubstitutionCommand { Regex<PosixExtended> { pattern }, replacement, options, output_filepath };
|
|
}
|
|
|
|
ErrorOr<int> serenity_main(Main::Arguments args)
|
|
{
|
|
TRY(Core::System::pledge("stdio cpath rpath wpath"));
|
|
|
|
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));
|
|
|
|
Optional<NonnullOwnPtr<Core::File>> maybe_output_file;
|
|
if (command.output_filepath.has_value())
|
|
maybe_output_file = TRY(Core::File::open_file_or_standard_stream(command.output_filepath.release_value(), Core::File::OpenMode::Write));
|
|
|
|
if (filepaths.is_empty())
|
|
filepaths = { "-"sv };
|
|
|
|
Array<u8, PAGE_SIZE> buffer {};
|
|
for (auto const& filepath : filepaths) {
|
|
auto file_unbuffered = TRY(Core::File::open_file_or_standard_stream(filepath, Core::File::OpenMode::Read));
|
|
auto file = TRY(Core::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);
|
|
|
|
if (maybe_output_file.has_value()) {
|
|
auto const& output_file = maybe_output_file.value();
|
|
// FIXME: This should write the entire span.
|
|
TRY(output_file->write_some(result.bytes()));
|
|
TRY(output_file->write_some("\n"sv.bytes()));
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|