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