Serenity Operating System
1/*
2 * Copyright (c) 2020, Emanuel Sprung <emanuel.sprung@gmail.com>
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include <AK/Assertions.h>
8#include <AK/DeprecatedString.h>
9#include <AK/LexicalPath.h>
10#include <AK/ScopeGuard.h>
11#include <AK/StringBuilder.h>
12#include <AK/Vector.h>
13#include <LibCore/ArgsParser.h>
14#include <LibCore/DeprecatedFile.h>
15#include <LibCore/DirIterator.h>
16#include <LibCore/File.h>
17#include <LibCore/System.h>
18#include <LibMain/Main.h>
19#include <LibRegex/Regex.h>
20#include <stdio.h>
21#include <unistd.h>
22
23enum class BinaryFileMode {
24 Binary,
25 Text,
26 Skip,
27};
28
29template<typename... Ts>
30void fail(StringView format, Ts... args)
31{
32 warn("\x1b[31m");
33 warnln(format, forward<Ts>(args)...);
34 warn("\x1b[0m");
35 abort();
36}
37
38constexpr StringView ere_special_characters = ".^$*+?()[{\\|"sv;
39constexpr StringView basic_special_characters = ".^$*[\\"sv;
40
41static DeprecatedString escape_characters(StringView string, StringView characters)
42{
43 StringBuilder builder;
44 for (auto ch : string) {
45 if (characters.contains(ch))
46 builder.append('\\');
47
48 builder.append(ch);
49 }
50 return builder.to_deprecated_string();
51}
52
53ErrorOr<int> serenity_main(Main::Arguments args)
54{
55 TRY(Core::System::pledge("stdio rpath"));
56
57 DeprecatedString program_name = AK::LexicalPath::basename(args.strings[0]);
58
59 Vector<DeprecatedString> files;
60
61 bool recursive = (program_name == "rgrep"sv);
62 bool use_ere = (program_name == "egrep"sv);
63 bool fixed_strings = (program_name == "fgrep"sv);
64 Vector<DeprecatedString> patterns;
65 BinaryFileMode binary_mode { BinaryFileMode::Binary };
66 bool case_insensitive = false;
67 bool line_numbers = false;
68 bool invert_match = false;
69 bool quiet_mode = false;
70 bool suppress_errors = false;
71 bool colored_output = isatty(STDOUT_FILENO);
72 bool count_lines = false;
73
74 size_t matched_line_count = 0;
75
76 Core::ArgsParser args_parser;
77 args_parser.add_option(recursive, "Recursively scan files", "recursive", 'r');
78 args_parser.add_option(use_ere, "Extended regular expressions", "extended-regexp", 'E');
79 args_parser.add_option(fixed_strings, "Treat pattern as a string, not a regexp", "fixed-strings", 'F');
80 args_parser.add_option(Core::ArgsParser::Option {
81 .argument_mode = Core::ArgsParser::OptionArgumentMode::Required,
82 .help_string = "Pattern",
83 .long_name = "regexp",
84 .short_name = 'e',
85 .value_name = "Pattern",
86 .accept_value = [&](StringView str) {
87 patterns.append(str);
88 return true;
89 },
90 });
91 args_parser.add_option(case_insensitive, "Make matches case-insensitive", nullptr, 'i');
92 args_parser.add_option(line_numbers, "Output line-numbers", "line-numbers", 'n');
93 args_parser.add_option(invert_match, "Select non-matching lines", "invert-match", 'v');
94 args_parser.add_option(quiet_mode, "Do not write anything to standard output", "quiet", 'q');
95 args_parser.add_option(suppress_errors, "Suppress error messages for nonexistent or unreadable files", "no-messages", 's');
96 args_parser.add_option(Core::ArgsParser::Option {
97 .argument_mode = Core::ArgsParser::OptionArgumentMode::Required,
98 .help_string = "Action to take for binary files ([binary], text, skip)",
99 .long_name = "binary-mode",
100 .accept_value = [&](StringView str) {
101 if ("text"sv == str)
102 binary_mode = BinaryFileMode::Text;
103 else if ("binary"sv == str)
104 binary_mode = BinaryFileMode::Binary;
105 else if ("skip"sv == str)
106 binary_mode = BinaryFileMode::Skip;
107 else
108 return false;
109 return true;
110 },
111 });
112 args_parser.add_option(Core::ArgsParser::Option {
113 .argument_mode = Core::ArgsParser::OptionArgumentMode::None,
114 .help_string = "Treat binary files as text (same as --binary-mode text)",
115 .long_name = "text",
116 .short_name = 'a',
117 .accept_value = [&](auto) {
118 binary_mode = BinaryFileMode::Text;
119 return true;
120 },
121 });
122 args_parser.add_option(Core::ArgsParser::Option {
123 .argument_mode = Core::ArgsParser::OptionArgumentMode::None,
124 .help_string = "Ignore binary files (same as --binary-mode skip)",
125 .long_name = nullptr,
126 .short_name = 'I',
127 .accept_value = [&](auto) {
128 binary_mode = BinaryFileMode::Skip;
129 return true;
130 },
131 });
132 args_parser.add_option(Core::ArgsParser::Option {
133 .argument_mode = Core::ArgsParser::OptionArgumentMode::Required,
134 .help_string = "When to use colored output for the matching text ([auto], never, always)",
135 .long_name = "color",
136 .short_name = 0,
137 .value_name = "WHEN",
138 .accept_value = [&](StringView str) {
139 if ("never"sv == str)
140 colored_output = false;
141 else if ("always"sv == str)
142 colored_output = true;
143 else if ("auto"sv != str)
144 return false;
145 return true;
146 },
147 });
148 args_parser.add_option(count_lines, "Output line count instead of line contents", "count", 'c');
149 args_parser.add_positional_argument(files, "File(s) to process", "file", Core::ArgsParser::Required::No);
150 args_parser.parse(args);
151
152 // mock grep behavior: if -e is omitted, use first positional argument as pattern
153 if (patterns.size() == 0 && files.size())
154 patterns.append(files.take_first());
155
156 auto user_has_specified_files = !files.is_empty();
157 auto user_specified_multiple_files = files.size() >= 2;
158
159 PosixOptions options {};
160 if (case_insensitive)
161 options |= PosixFlags::Insensitive;
162
163 auto grep_logic = [&](auto&& regular_expressions) {
164 for (auto& re : regular_expressions) {
165 if (re.parser_result.error != regex::Error::NoError) {
166 warnln("regex parse error: {}", regex::get_error_string(re.parser_result.error));
167 return 1;
168 }
169 }
170
171 auto matches = [&](StringView str, StringView filename, size_t line_number, bool print_filename, bool is_binary) {
172 size_t last_printed_char_pos { 0 };
173 if (is_binary && binary_mode == BinaryFileMode::Skip)
174 return false;
175
176 for (auto& re : regular_expressions) {
177 auto result = re.match(str, PosixFlags::Global);
178 if (!(result.success ^ invert_match))
179 continue;
180
181 if (quiet_mode)
182 return true;
183
184 if (count_lines) {
185 matched_line_count++;
186 return true;
187 }
188
189 if (is_binary && binary_mode == BinaryFileMode::Binary) {
190 outln(colored_output ? "binary file \x1B[34m{}\x1B[0m matches"sv : "binary file {} matches"sv, filename);
191 } else {
192 if ((result.matches.size() || invert_match) && print_filename)
193 out(colored_output ? "\x1B[34m{}:\x1B[0m"sv : "{}:"sv, filename);
194 if ((result.matches.size() || invert_match) && line_numbers)
195 out(colored_output ? "\x1B[35m{}:\x1B[0m"sv : "{}:"sv, line_number);
196
197 for (auto& match : result.matches) {
198 auto pre_match_length = match.global_offset - last_printed_char_pos;
199 out(colored_output ? "{}\x1B[32m{}\x1B[0m"sv : "{}{}"sv,
200 pre_match_length > 0 ? StringView(&str[last_printed_char_pos], pre_match_length) : ""sv,
201 match.view.to_deprecated_string());
202 last_printed_char_pos = match.global_offset + match.view.length();
203 }
204 auto remaining_length = str.length() - last_printed_char_pos;
205 outln("{}", remaining_length > 0 ? StringView(&str[last_printed_char_pos], remaining_length) : ""sv);
206 }
207
208 return true;
209 }
210
211 return false;
212 };
213
214 bool did_match_something = false;
215
216 auto handle_file = [&matches, binary_mode, count_lines, quiet_mode,
217 user_specified_multiple_files, &matched_line_count, &did_match_something](StringView filename, bool print_filename) -> ErrorOr<void> {
218 auto file = TRY(Core::File::open(filename, Core::File::OpenMode::Read));
219 auto buffered_file = TRY(Core::BufferedFile::create(move(file)));
220
221 for (size_t line_number = 1; TRY(buffered_file->can_read_line()); ++line_number) {
222 Array<u8, PAGE_SIZE> buffer;
223 auto line = TRY(buffered_file->read_line(buffer));
224
225 auto is_binary = line.contains('\0');
226
227 auto matched = matches(line, filename, line_number, print_filename, is_binary);
228 did_match_something = did_match_something || matched;
229 if (matched && is_binary && binary_mode == BinaryFileMode::Binary)
230 break;
231 }
232
233 if (count_lines && !quiet_mode) {
234 if (user_specified_multiple_files)
235 outln("{}:{}", filename, matched_line_count);
236 else
237 outln("{}", matched_line_count);
238 matched_line_count = 0;
239 }
240
241 return {};
242 };
243
244 auto add_directory = [&handle_file, user_has_specified_files, suppress_errors](DeprecatedString base, Optional<DeprecatedString> recursive, auto handle_directory) -> void {
245 Core::DirIterator it(recursive.value_or(base), Core::DirIterator::Flags::SkipDots);
246 while (it.has_next()) {
247 auto path = it.next_full_path();
248 if (!Core::DeprecatedFile::is_directory(path)) {
249 auto key = user_has_specified_files ? path.view() : path.substring_view(base.length() + 1, path.length() - base.length() - 1);
250 if (auto result = handle_file(key, true); result.is_error() && !suppress_errors)
251 warnln("Failed with file {}: {}", key, result.release_error());
252
253 } else {
254 handle_directory(base, path, handle_directory);
255 }
256 }
257 };
258
259 if (!files.size() && !recursive) {
260 char* line = nullptr;
261 size_t line_len = 0;
262 ssize_t nread = 0;
263 ScopeGuard free_line = [line] { free(line); };
264 size_t line_number = 0;
265 while ((nread = getline(&line, &line_len, stdin)) != -1) {
266 VERIFY(nread > 0);
267 if (line[nread - 1] == '\n')
268 --nread;
269 // Human-readable indexes start at 1, so it's fine to increment already.
270 line_number += 1;
271 StringView line_view(line, nread);
272 bool is_binary = line_view.contains('\0');
273
274 if (is_binary && binary_mode == BinaryFileMode::Skip)
275 return 1;
276
277 auto matched = matches(line_view, "stdin"sv, line_number, false, is_binary);
278 did_match_something = did_match_something || matched;
279 if (matched && is_binary && binary_mode == BinaryFileMode::Binary)
280 break;
281 }
282
283 if (count_lines && !quiet_mode)
284 outln("{}", matched_line_count);
285 } else {
286 if (recursive) {
287 if (user_has_specified_files) {
288 for (auto& filename : files) {
289 add_directory(filename, {}, add_directory);
290 }
291 } else {
292 add_directory(".", {}, add_directory);
293 }
294
295 } else {
296 bool print_filename { files.size() > 1 };
297 for (auto& filename : files) {
298 auto result = handle_file(filename, print_filename);
299 if (result.is_error()) {
300 if (!suppress_errors)
301 warnln("Failed with file {}: {}", filename, result.release_error());
302 return 1;
303 }
304 }
305 }
306 }
307
308 return did_match_something ? 0 : 1;
309 };
310
311 if (use_ere) {
312 Vector<Regex<PosixExtended>> regular_expressions;
313 for (auto pattern : patterns) {
314 auto escaped_pattern = (fixed_strings) ? escape_characters(pattern, ere_special_characters) : pattern;
315 regular_expressions.append(Regex<PosixExtended>(escaped_pattern, options));
316 }
317 return grep_logic(regular_expressions);
318 }
319
320 Vector<Regex<PosixBasic>> regular_expressions;
321 for (auto pattern : patterns) {
322 auto escaped_pattern = (fixed_strings) ? escape_characters(pattern, basic_special_characters) : pattern;
323 dbgln("'{}'", escaped_pattern);
324 regular_expressions.append(Regex<PosixBasic>(escaped_pattern, options));
325 }
326 return grep_logic(regular_expressions);
327}