Serenity Operating System
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}