Serenity Operating System
at master 327 lines 13 kB view raw
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}