Serenity Operating System
at master 151 lines 5.1 kB view raw
1/* 2 * Copyright (c) 2022, Eli Youngs <eli.m.youngs@gmail.com> 3 * 4 * SPDX-License-Identifier: BSD-2-Clause 5 */ 6 7#include <AK/CharacterTypes.h> 8#include <AK/GenericLexer.h> 9#include <AK/Vector.h> 10#include <LibCore/ArgsParser.h> 11#include <LibCore/File.h> 12#include <LibCore/System.h> 13#include <LibMain/Main.h> 14#include <LibRegex/RegexMatcher.h> 15#include <LibRegex/RegexOptions.h> 16 17struct SubstitutionCommand { 18 Regex<PosixExtended> regex; 19 StringView replacement; 20 PosixOptions options; 21 Optional<StringView> output_filepath; 22}; 23 24static Vector<StringView> split_flags(StringView const& input) 25{ 26 Vector<StringView> flags; 27 28 auto lexer = GenericLexer(input); 29 while (!lexer.is_eof()) { 30 StringView flag; 31 32 if (lexer.next_is(is_ascii_digit)) { 33 flag = lexer.consume_while(is_ascii_digit); 34 } else if (lexer.peek() == 'w') { 35 flag = lexer.consume_all(); 36 } else { 37 flag = lexer.consume(1); 38 } 39 40 flags.append(flag); 41 } 42 43 return flags; 44} 45 46static ErrorOr<SubstitutionCommand> parse_command(StringView command) 47{ 48 auto generic_error_message = "Incomplete substitution command"sv; 49 50 auto lexer = GenericLexer(command); 51 52 auto address = lexer.consume_until('s'); 53 if (!address.is_empty()) 54 warnln("sed: Addresses are currently ignored"); 55 56 if (!lexer.consume_specific('s')) 57 return Error::from_string_view(generic_error_message); 58 59 if (lexer.is_eof()) 60 return Error::from_string_view(generic_error_message); 61 62 auto delimiter = lexer.consume(); 63 if (delimiter == '\n' || delimiter == '\\') 64 return Error::from_string_literal("\\n and \\ cannot be used as delimiters."); 65 66 auto pattern = lexer.consume_until(delimiter); 67 if (pattern.is_empty()) 68 return Error::from_string_literal("Substitution patterns cannot be empty."); 69 70 if (!lexer.consume_specific(delimiter)) 71 return Error::from_string_view(generic_error_message); 72 73 auto replacement = lexer.consume_until(delimiter); 74 75 // According to Posix, "s/x/y" is an invalid substitution command. 76 // It must have a closing delimiter: "s/x/y/" 77 if (!lexer.consume_specific(delimiter)) 78 return Error::from_string_literal("The substitution command was not properly terminated."); 79 80 PosixOptions options = PosixOptions(PosixFlags::Global | PosixFlags::SingleMatch); 81 Optional<StringView> output_filepath; 82 83 auto flags = split_flags(lexer.consume_all()); 84 for (auto const& flag : flags) { 85 if (flag.starts_with('w')) { 86 auto flag_filepath = flag.substring_view(1).trim_whitespace(); 87 if (flag_filepath.is_empty()) 88 return Error::from_string_literal("No filepath was provided for the 'w' flag."); 89 output_filepath = flag_filepath; 90 } else if (flag == "g"sv) { 91 // Allow multiple matches per line by un-setting the SingleMatch flag 92 options &= ~PosixFlags::SingleMatch; 93 } else if (flag == "i"sv || flag == "I"sv) { 94 options |= PosixFlags::Insensitive; 95 } else { 96 warnln("sed: Unsupported flag: {}", flag); 97 } 98 } 99 100 return SubstitutionCommand { Regex<PosixExtended> { pattern }, replacement, options, output_filepath }; 101} 102 103ErrorOr<int> serenity_main(Main::Arguments args) 104{ 105 TRY(Core::System::pledge("stdio cpath rpath wpath")); 106 107 Core::ArgsParser args_parser; 108 109 StringView command_input; 110 Vector<StringView> filepaths; 111 112 args_parser.add_positional_argument(command_input, "Command", "command_input", Core::ArgsParser::Required::Yes); 113 args_parser.add_positional_argument(filepaths, "File", "file", Core::ArgsParser::Required::No); 114 115 args_parser.parse(args); 116 117 auto command = TRY(parse_command(command_input)); 118 119 Optional<NonnullOwnPtr<Core::File>> maybe_output_file; 120 if (command.output_filepath.has_value()) 121 maybe_output_file = TRY(Core::File::open_file_or_standard_stream(command.output_filepath.release_value(), Core::File::OpenMode::Write)); 122 123 if (filepaths.is_empty()) 124 filepaths = { "-"sv }; 125 126 Array<u8, PAGE_SIZE> buffer {}; 127 for (auto const& filepath : filepaths) { 128 auto file_unbuffered = TRY(Core::File::open_file_or_standard_stream(filepath, Core::File::OpenMode::Read)); 129 auto file = TRY(Core::BufferedFile::create(move(file_unbuffered))); 130 131 while (!file->is_eof()) { 132 auto line = TRY(file->read_line(buffer)); 133 134 // Substitutions can apply to blank lines in the middle of a file, 135 // but not to the trailing newline that marks the end of a file. 136 if (line.is_empty() && file->is_eof()) 137 break; 138 139 auto result = command.regex.replace(line, command.replacement, command.options); 140 outln(result); 141 142 if (maybe_output_file.has_value()) { 143 auto const& output_file = maybe_output_file.value(); 144 TRY(output_file->write_until_depleted(result.bytes())); 145 TRY(output_file->write_until_depleted("\n"sv.bytes())); 146 } 147 } 148 } 149 150 return 0; 151}