Serenity Operating System
at master 190 lines 7.0 kB view raw
1/* 2 * Copyright (c) 2021, the SerenityOS developers. 3 * Copyright (c) 2022, Sam Atkins <atkinssj@serenityos.org> 4 * 5 * SPDX-License-Identifier: BSD-2-Clause 6 */ 7 8#include <LibCore/ArgsParser.h> 9#include <LibCore/DeprecatedFile.h> 10#include <LibCore/File.h> 11#include <LibCore/System.h> 12#include <LibMain/Main.h> 13#include <string.h> 14#include <strings.h> 15#include <unistd.h> 16 17#define COL1_COLOR "\x1B[32m{}\x1B[0m" 18#define COL2_COLOR "\x1B[34m{}\x1B[0m" 19#define COL3_COLOR "\x1B[31m{}\x1B[0m" 20 21ErrorOr<int> serenity_main(Main::Arguments arguments) 22{ 23 TRY(Core::System::pledge("stdio rpath")); 24 25 DeprecatedString file1_path; 26 DeprecatedString file2_path; 27 bool suppress_col1 { false }; 28 bool suppress_col2 { false }; 29 bool suppress_col3 { false }; 30 bool case_insensitive { false }; 31 bool color { false }; 32 bool no_color { false }; 33 bool print_total { false }; 34 35 Core::ArgsParser args_parser; 36 args_parser.set_general_help("Compare two sorted files line by line"); 37 args_parser.add_option(suppress_col1, "Suppress column 1 (lines unique to file1)", nullptr, '1'); 38 args_parser.add_option(suppress_col2, "Suppress column 2 (lines unique to file2)", nullptr, '2'); 39 args_parser.add_option(suppress_col3, "Suppress column 3 (lines common to both files)", nullptr, '3'); 40 args_parser.add_option(case_insensitive, "Use case-insensitive comparison of lines", nullptr, 'i'); 41 args_parser.add_option(color, "Always print colored output", "color", 'c'); 42 args_parser.add_option(no_color, "Do not print colored output", "no-color", 0); 43 args_parser.add_option(print_total, "Print a summary", "total", 't'); 44 args_parser.add_positional_argument(file1_path, "First file to compare", "file1"); 45 args_parser.add_positional_argument(file2_path, "Second file to compare", "file2"); 46 args_parser.parse(arguments); 47 48 if (color && no_color) { 49 warnln("Cannot specify 'color' and 'no-color' together"); 50 return 1; 51 } 52 53 bool print_color = TRY(Core::System::isatty(STDOUT_FILENO)); 54 if (color) 55 print_color = true; 56 else if (no_color) 57 print_color = false; 58 59 if (file1_path == "-" && file2_path == "-") { 60 warnln("File1 and file2 cannot both be the standard input"); 61 return 1; 62 } 63 64 auto open_file = [](DeprecatedString const& path, auto& file, int file_number) { 65 auto file_or_error = Core::File::open_file_or_standard_stream(path, Core::File::OpenMode::Read); 66 if (file_or_error.is_error()) { 67 warnln("Failed to open file{} '{}': {}", file_number, path, file_or_error.error()); 68 return false; 69 } 70 71 if (path != "-" && Core::DeprecatedFile::is_directory(path)) { 72 warnln("Failed to open file{} '{}': is a directory", file_number, path); 73 return false; 74 } 75 76 auto buffered_file_or_error = Core::BufferedFile::create(file_or_error.release_value()); 77 if (buffered_file_or_error.is_error()) { 78 warnln("Failed to create buffer for file{} '{}': {}", file_number, path, buffered_file_or_error.error()); 79 return false; 80 } 81 82 file = buffered_file_or_error.release_value(); 83 return true; 84 }; 85 86 OwnPtr<Core::BufferedFile> file1; 87 OwnPtr<Core::BufferedFile> file2; 88 if (!(open_file(file1_path, file1, 1) && open_file(file2_path, file2, 2))) 89 return 1; 90 91 char tab { '\t' }; 92 size_t tab_count { 0 }; 93 DeprecatedString col1_fmt; 94 DeprecatedString col2_fmt; 95 DeprecatedString col3_fmt; 96 if (!suppress_col1) 97 col1_fmt = DeprecatedString::formatted("{}{}", DeprecatedString::repeated(tab, tab_count++), print_color ? COL1_COLOR : "{}"); 98 if (!suppress_col2) 99 col2_fmt = DeprecatedString::formatted("{}{}", DeprecatedString::repeated(tab, tab_count++), print_color ? COL2_COLOR : "{}"); 100 if (!suppress_col3) 101 col3_fmt = DeprecatedString::formatted("{}{}", DeprecatedString::repeated(tab, tab_count++), print_color ? COL3_COLOR : "{}"); 102 103 auto cmp = [&](DeprecatedString const& str1, DeprecatedString const& str2) { 104 if (case_insensitive) 105 return strcasecmp(str1.characters(), str2.characters()); 106 return strcmp(str1.characters(), str2.characters()); 107 }; 108 109 bool read_file1 { true }; 110 bool read_file2 { true }; 111 int col1_count { 0 }; 112 int col2_count { 0 }; 113 int col3_count { 0 }; 114 DeprecatedString file1_line; 115 DeprecatedString file2_line; 116 Array<u8, PAGE_SIZE> buffer; 117 118 auto should_continue_comparing_files = [&]() { 119 if (read_file1) { 120 auto can_read_file1_line = file1->can_read_line(); 121 if (can_read_file1_line.is_error() || !can_read_file1_line.value()) 122 return false; 123 } 124 if (read_file2) { 125 auto can_read_file2_line = file2->can_read_line(); 126 if (can_read_file2_line.is_error() || !can_read_file2_line.value()) 127 return false; 128 } 129 return true; 130 }; 131 132 while (should_continue_comparing_files()) { 133 if (read_file1) 134 file1_line = TRY(file1->read_line(buffer)); 135 if (read_file2) 136 file2_line = TRY(file2->read_line(buffer)); 137 138 int cmp_result = cmp(file1_line, file2_line); 139 140 if (cmp_result == 0) { 141 ++col3_count; 142 read_file1 = read_file2 = true; 143 if (!suppress_col3) 144 outln(col3_fmt, file1_line); 145 } else if (cmp_result < 0) { 146 ++col1_count; 147 read_file1 = true; 148 read_file2 = false; 149 if (!suppress_col1) 150 outln(col1_fmt, file1_line); 151 } else { 152 ++col2_count; 153 read_file1 = false; 154 read_file2 = true; 155 if (!suppress_col2) 156 outln(col2_fmt, file2_line); 157 } 158 } 159 160 // If the most recent line read was not a match, then the last line read from one of the files has not yet been output. 161 // So let's output it! 162 if (!read_file1 && !suppress_col1) { 163 ++col1_count; 164 outln(col1_fmt, file1_line); 165 } else if (!read_file2 && !suppress_col2) { 166 ++col2_count; 167 outln(col2_fmt, file2_line); 168 } 169 170 auto process_remaining = [&](DeprecatedString const& fmt, auto& file, int& count, bool print) { 171 while (true) { 172 auto can_read_result = file->can_read_line(); 173 if (can_read_result.is_error() || !can_read_result.value()) 174 break; 175 ++count; 176 auto line = file->read_line(buffer); 177 if (line.is_error()) 178 break; 179 if (print) 180 outln(fmt, line.value()); 181 } 182 }; 183 process_remaining(col1_fmt, file1, col1_count, !suppress_col1); 184 process_remaining(col2_fmt, file2, col2_count, !suppress_col2); 185 186 if (print_total) 187 outln(print_color ? COL1_COLOR "\t" COL2_COLOR "\t" COL3_COLOR "\ttotal"sv : "{}\t{}\t{}\ttotal"sv, col1_count, col2_count, col3_count); 188 189 return 0; 190}