Serenity Operating System
1/*
2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include <AK/Assertions.h>
8#include <AK/DeprecatedString.h>
9#include <AK/HashMap.h>
10#include <AK/NumberFormat.h>
11#include <AK/QuickSort.h>
12#include <AK/StringBuilder.h>
13#include <AK/URL.h>
14#include <AK/Utf8View.h>
15#include <AK/Vector.h>
16#include <LibCore/ArgsParser.h>
17#include <LibCore/DateTime.h>
18#include <LibCore/DeprecatedFile.h>
19#include <LibCore/DirIterator.h>
20#include <LibCore/System.h>
21#include <LibMain/Main.h>
22#include <ctype.h>
23#include <dirent.h>
24#include <errno.h>
25#include <fcntl.h>
26#include <grp.h>
27#include <inttypes.h>
28#include <pwd.h>
29#include <stdio.h>
30#include <string.h>
31#include <sys/ioctl.h>
32#include <sys/stat.h>
33#include <sys/sysmacros.h>
34#include <sys/types.h>
35#include <time.h>
36#include <unistd.h>
37
38struct FileMetadata {
39 DeprecatedString name;
40 DeprecatedString path;
41 struct stat stat {
42 };
43};
44
45static int do_file_system_object_long(char const* path);
46static int do_file_system_object_short(char const* path);
47
48static bool print_names(char const* path, size_t longest_name, Vector<FileMetadata> const& files);
49
50static bool filemetadata_comparator(FileMetadata& a, FileMetadata& b);
51
52static bool flag_classify = false;
53static bool flag_colorize = false;
54static bool flag_long = false;
55static bool flag_show_dotfiles = false;
56static bool flag_show_almost_all_dotfiles = false;
57static bool flag_ignore_backups = false;
58static bool flag_list_directories_only = false;
59static bool flag_show_inode = false;
60static bool flag_print_numeric = false;
61static bool flag_hide_group = false;
62static bool flag_human_readable = false;
63static bool flag_human_readable_si = false;
64static bool flag_sort_by_timestamp = false;
65static bool flag_reverse_sort = false;
66static bool flag_disable_hyperlinks = false;
67static bool flag_recursive = false;
68static bool flag_force_newline = false;
69
70static size_t terminal_rows = 0;
71static size_t terminal_columns = 0;
72static bool output_is_terminal = false;
73
74static HashMap<uid_t, DeprecatedString> users;
75static HashMap<gid_t, DeprecatedString> groups;
76
77static bool is_a_tty = false;
78
79ErrorOr<int> serenity_main(Main::Arguments arguments)
80{
81 TRY(Core::System::pledge("stdio rpath tty"));
82
83 struct winsize ws;
84 int rc = ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws);
85 if (rc == 0) {
86 terminal_rows = ws.ws_row;
87 terminal_columns = ws.ws_col;
88 output_is_terminal = true;
89 }
90
91 is_a_tty = isatty(STDOUT_FILENO);
92 if (!is_a_tty) {
93 flag_disable_hyperlinks = true;
94 } else {
95 flag_colorize = true;
96 }
97
98 TRY(Core::System::pledge("stdio rpath"));
99
100 Vector<StringView> paths;
101
102 Core::ArgsParser args_parser;
103 args_parser.set_general_help("List files in a directory.");
104 args_parser.add_option(flag_show_dotfiles, "Show dotfiles", "all", 'a');
105 args_parser.add_option(flag_show_almost_all_dotfiles, "Do not list implied . and .. directories", nullptr, 'A');
106 args_parser.add_option(flag_ignore_backups, "Do not list implied entries ending with ~", "ignore-backups", 'B');
107 args_parser.add_option(flag_list_directories_only, "List directories themselves, not their contents", "directory", 'd');
108 args_parser.add_option(flag_long, "Display long info", "long", 'l');
109 args_parser.add_option(flag_sort_by_timestamp, "Sort files by timestamp", nullptr, 't');
110 args_parser.add_option(flag_reverse_sort, "Reverse sort order", "reverse", 'r');
111 args_parser.add_option(flag_classify, "Append a file type indicator to entries", "classify", 'F');
112 args_parser.add_option(flag_colorize, "Use pretty colors", nullptr, 'G');
113 args_parser.add_option(flag_show_inode, "Show inode ids", "inode", 'i');
114 args_parser.add_option(flag_print_numeric, "In long format, display numeric UID/GID", "numeric-uid-gid", 'n');
115 args_parser.add_option(flag_hide_group, "In long format, do not show group information", nullptr, 'o');
116 args_parser.add_option(flag_human_readable, "Print human-readable sizes", "human-readable", 'h');
117 args_parser.add_option(flag_human_readable_si, "Print human-readable sizes in SI units", "si", 0);
118 args_parser.add_option(flag_disable_hyperlinks, "Disable hyperlinks", "no-hyperlinks", 'K');
119 args_parser.add_option(flag_recursive, "List subdirectories recursively", "recursive", 'R');
120 args_parser.add_option(flag_force_newline, "List one file per line", nullptr, '1');
121 args_parser.add_positional_argument(paths, "Directory to list", "path", Core::ArgsParser::Required::No);
122 args_parser.parse(arguments);
123
124 if (flag_show_almost_all_dotfiles)
125 flag_show_dotfiles = true;
126
127 if (flag_long) {
128 setpwent();
129 for (auto* pwd = getpwent(); pwd; pwd = getpwent())
130 users.set(pwd->pw_uid, pwd->pw_name);
131 endpwent();
132 setgrent();
133 for (auto* grp = getgrent(); grp; grp = getgrent())
134 groups.set(grp->gr_gid, grp->gr_name);
135 endgrent();
136 }
137
138 auto do_file_system_object = [&](char const* path) {
139 if (flag_long)
140 return do_file_system_object_long(path);
141 return do_file_system_object_short(path);
142 };
143
144 if (paths.is_empty())
145 paths.append("."sv);
146
147 Vector<FileMetadata> files;
148 for (auto& path : paths) {
149 FileMetadata metadata;
150 metadata.name = path;
151
152 int rc = lstat(DeprecatedString(path).characters(), &metadata.stat);
153 if (rc < 0) {
154 perror("lstat");
155 continue;
156 }
157
158 files.append(metadata);
159 }
160 quick_sort(files, filemetadata_comparator);
161
162 int status = 0;
163
164 for (size_t i = 0; i < files.size(); i++) {
165 auto path = files[i].name;
166
167 if (flag_recursive && Core::DeprecatedFile::is_directory(path)) {
168 size_t subdirs = 0;
169 Core::DirIterator di(path, Core::DirIterator::SkipParentAndBaseDir);
170
171 if (di.has_error()) {
172 status = 1;
173 fprintf(stderr, "%s: %s\n", path.characters(), strerror(di.error().code()));
174 }
175
176 while (di.has_next()) {
177 DeprecatedString directory = di.next_full_path();
178 if (Core::DeprecatedFile::is_directory(directory) && !Core::DeprecatedFile::is_link(directory)) {
179 ++subdirs;
180 FileMetadata new_file;
181 new_file.name = move(directory);
182 files.insert(i + subdirs, move(new_file));
183 }
184 }
185 }
186
187 bool show_dir_separator = files.size() > 1 && Core::DeprecatedFile::is_directory(path) && !flag_list_directories_only;
188 if (show_dir_separator) {
189 printf("%s:\n", path.characters());
190 }
191 auto rc = do_file_system_object(path.characters());
192 if (rc != 0)
193 status = rc;
194 if (show_dir_separator && i != files.size() - 1) {
195 puts("");
196 }
197 }
198
199 return status;
200}
201
202static int print_escaped(StringView name)
203{
204 int printed = 0;
205
206 Utf8View utf8_name(name);
207 if (utf8_name.validate()) {
208 out("{}", name);
209 return utf8_name.length();
210 }
211
212 for (auto c : name) {
213 if (isprint(c)) {
214 putchar(c);
215 printed++;
216 } else {
217 printed += printf("\\%03d", c);
218 }
219 }
220
221 return printed;
222}
223
224static DeprecatedString& hostname()
225{
226 static DeprecatedString s_hostname;
227 if (s_hostname.is_null()) {
228 char buffer[HOST_NAME_MAX];
229 if (gethostname(buffer, sizeof(buffer)) == 0)
230 s_hostname = buffer;
231 else
232 s_hostname = "localhost";
233 }
234 return s_hostname;
235}
236
237static size_t print_name(const struct stat& st, DeprecatedString const& name, char const* path_for_link_resolution, char const* path_for_hyperlink)
238{
239 if (!flag_disable_hyperlinks) {
240 auto full_path = Core::DeprecatedFile::real_path_for(path_for_hyperlink);
241 if (!full_path.is_null()) {
242 auto url = URL::create_with_file_scheme(full_path, {}, hostname());
243 out("\033]8;;{}\033\\", url.serialize());
244 }
245 }
246
247 size_t nprinted = 0;
248
249 if (!flag_colorize || !output_is_terminal) {
250 nprinted = printf("%s", name.characters());
251 } else {
252 char const* begin_color = "";
253 char const* end_color = "\033[0m";
254
255 if (st.st_mode & S_ISVTX)
256 begin_color = "\033[42;30;1m";
257 else if (st.st_mode & S_ISUID)
258 begin_color = "\033[41;1m";
259 else if (st.st_mode & S_ISGID)
260 begin_color = "\033[43;1m";
261 else if (S_ISLNK(st.st_mode))
262 begin_color = "\033[36;1m";
263 else if (S_ISDIR(st.st_mode))
264 begin_color = "\033[34;1m";
265 else if (st.st_mode & 0111)
266 begin_color = "\033[32;1m";
267 else if (S_ISSOCK(st.st_mode))
268 begin_color = "\033[35;1m";
269 else if (S_ISCHR(st.st_mode) || S_ISBLK(st.st_mode))
270 begin_color = "\033[33;1m";
271 printf("%s", begin_color);
272 nprinted = print_escaped(name);
273 printf("%s", end_color);
274 }
275 if (S_ISLNK(st.st_mode)) {
276 if (path_for_link_resolution) {
277 auto link_destination_or_error = Core::DeprecatedFile::read_link(path_for_link_resolution);
278 if (link_destination_or_error.is_error()) {
279 perror("readlink");
280 } else {
281 nprinted += printf(" -> ") + print_escaped(link_destination_or_error.value());
282 }
283 } else {
284 if (flag_classify)
285 nprinted += printf("@");
286 }
287 } else if (S_ISDIR(st.st_mode)) {
288 if (flag_classify)
289 nprinted += printf("/");
290 } else if (st.st_mode & 0111) {
291 if (flag_classify)
292 nprinted += printf("*");
293 }
294
295 if (!flag_disable_hyperlinks) {
296 printf("\033]8;;\033\\");
297 }
298
299 return nprinted;
300}
301
302static bool print_filesystem_object(DeprecatedString const& path, DeprecatedString const& name, const struct stat& st)
303{
304 if (flag_show_inode)
305 printf("%s ", DeprecatedString::formatted("{}", st.st_ino).characters());
306
307 if (S_ISDIR(st.st_mode))
308 printf("d");
309 else if (S_ISLNK(st.st_mode))
310 printf("l");
311 else if (S_ISBLK(st.st_mode))
312 printf("b");
313 else if (S_ISCHR(st.st_mode))
314 printf("c");
315 else if (S_ISFIFO(st.st_mode))
316 printf("f");
317 else if (S_ISSOCK(st.st_mode))
318 printf("s");
319 else if (S_ISREG(st.st_mode))
320 printf("-");
321 else
322 printf("?");
323
324 printf("%c%c%c%c%c%c%c%c",
325 st.st_mode & S_IRUSR ? 'r' : '-',
326 st.st_mode & S_IWUSR ? 'w' : '-',
327 st.st_mode & S_ISUID ? 's' : (st.st_mode & S_IXUSR ? 'x' : '-'),
328 st.st_mode & S_IRGRP ? 'r' : '-',
329 st.st_mode & S_IWGRP ? 'w' : '-',
330 st.st_mode & S_ISGID ? 's' : (st.st_mode & S_IXGRP ? 'x' : '-'),
331 st.st_mode & S_IROTH ? 'r' : '-',
332 st.st_mode & S_IWOTH ? 'w' : '-');
333
334 if (st.st_mode & S_ISVTX)
335 printf("t");
336 else
337 printf("%c", st.st_mode & S_IXOTH ? 'x' : '-');
338
339 printf(" %3u", st.st_nlink);
340
341 auto username = users.get(st.st_uid);
342 if (!flag_print_numeric && username.has_value()) {
343 printf(" %7s", username.value().characters());
344 } else {
345 printf(" %7u", st.st_uid);
346 }
347
348 if (!flag_hide_group) {
349 auto groupname = groups.get(st.st_gid);
350 if (!flag_print_numeric && groupname.has_value()) {
351 printf(" %7s", groupname.value().characters());
352 } else {
353 printf(" %7u", st.st_gid);
354 }
355 }
356
357 if (S_ISCHR(st.st_mode) || S_ISBLK(st.st_mode)) {
358 printf(" %4u,%4u ", major(st.st_rdev), minor(st.st_rdev));
359 } else {
360 if (flag_human_readable) {
361 printf(" %10s ", human_readable_size(st.st_size).characters());
362 } else if (flag_human_readable_si) {
363 printf(" %10s ", human_readable_size(st.st_size, AK::HumanReadableBasedOn::Base10).characters());
364 } else {
365 printf(" %10" PRIu64 " ", (uint64_t)st.st_size);
366 }
367 }
368
369 printf(" %s ", Core::DateTime::from_timestamp(st.st_mtime).to_deprecated_string().characters());
370
371 print_name(st, name, path.characters(), path.characters());
372
373 printf("\n");
374 return true;
375}
376
377static int do_file_system_object_long(char const* path)
378{
379 if (flag_list_directories_only) {
380 struct stat stat {
381 };
382 int rc = lstat(path, &stat);
383 if (rc < 0)
384 perror("lstat");
385 if (print_filesystem_object(path, path, stat))
386 return 0;
387 return 2;
388 }
389
390 auto flags = Core::DirIterator::SkipDots;
391 if (flag_show_dotfiles)
392 flags = Core::DirIterator::Flags::NoFlags;
393 if (flag_show_almost_all_dotfiles)
394 flags = Core::DirIterator::SkipParentAndBaseDir;
395
396 Core::DirIterator di(path, flags);
397
398 if (di.has_error()) {
399 auto error = di.error();
400 if (error.code() == ENOTDIR) {
401 struct stat stat {
402 };
403 int rc = lstat(path, &stat);
404 if (rc < 0)
405 perror("lstat");
406 if (print_filesystem_object(path, path, stat))
407 return 0;
408 return 2;
409 }
410 fprintf(stderr, "%s: %s\n", path, strerror(di.error().code()));
411 return 1;
412 }
413
414 Vector<FileMetadata> files;
415 while (di.has_next()) {
416 FileMetadata metadata;
417 metadata.name = di.next_path();
418 VERIFY(!metadata.name.is_empty());
419
420 if (metadata.name.ends_with('~') && flag_ignore_backups && metadata.name != path)
421 continue;
422
423 StringBuilder builder;
424 builder.append({ path, strlen(path) });
425 builder.append('/');
426 builder.append(metadata.name);
427 metadata.path = builder.to_deprecated_string();
428 VERIFY(!metadata.path.is_null());
429 int rc = lstat(metadata.path.characters(), &metadata.stat);
430 if (rc < 0)
431 perror("lstat");
432
433 files.append(move(metadata));
434 }
435
436 quick_sort(files, filemetadata_comparator);
437
438 for (auto& file : files) {
439 if (!print_filesystem_object(file.path, file.name, file.stat))
440 return 2;
441 }
442 return 0;
443}
444
445static bool print_filesystem_object_short(char const* path, char const* name, size_t* nprinted)
446{
447 struct stat st;
448 int rc = lstat(path, &st);
449 if (rc == -1) {
450 printf("lstat(%s) failed: %s\n", path, strerror(errno));
451 return false;
452 }
453
454 if (flag_show_inode)
455 printf("%s ", DeprecatedString::formatted("{}", st.st_ino).characters());
456
457 *nprinted = print_name(st, name, nullptr, path);
458 return true;
459}
460
461static bool print_names(char const* path, size_t longest_name, Vector<FileMetadata> const& files)
462{
463 size_t printed_on_row = 0;
464 size_t nprinted = 0;
465 for (size_t i = 0; i < files.size(); ++i) {
466 auto& name = files[i].name;
467 StringBuilder builder;
468 builder.append({ path, strlen(path) });
469 builder.append('/');
470 builder.append(name);
471 if (!print_filesystem_object_short(builder.to_deprecated_string().characters(), name.characters(), &nprinted))
472 return 2;
473 int offset = 0;
474 if (terminal_columns > longest_name)
475 offset = terminal_columns % longest_name / (terminal_columns / longest_name);
476
477 // The offset must be at least 2 because:
478 // - With each file an additional char is printed e.g. '@','*'.
479 // - Each filename must be separated by a space.
480 size_t column_width = longest_name + max(offset, 2);
481 printed_on_row += column_width;
482
483 if (is_a_tty) {
484 for (size_t j = nprinted; i != (files.size() - 1) && j < column_width; ++j)
485 printf(" ");
486 }
487 if ((printed_on_row + column_width) >= terminal_columns || flag_force_newline) {
488 printf("\n");
489 printed_on_row = 0;
490 }
491 }
492 return printed_on_row;
493}
494
495int do_file_system_object_short(char const* path)
496{
497 if (flag_list_directories_only) {
498 size_t nprinted = 0;
499 bool status = print_filesystem_object_short(path, path, &nprinted);
500 printf("\n");
501 if (status)
502 return 0;
503 return 2;
504 }
505
506 auto flags = Core::DirIterator::SkipDots;
507 if (flag_show_dotfiles)
508 flags = Core::DirIterator::Flags::NoFlags;
509 if (flag_show_almost_all_dotfiles)
510 flags = Core::DirIterator::SkipParentAndBaseDir;
511
512 Core::DirIterator di(path, flags);
513 if (di.has_error()) {
514 auto error = di.error();
515 if (error.code() == ENOTDIR) {
516 size_t nprinted = 0;
517 bool status = print_filesystem_object_short(path, path, &nprinted);
518 printf("\n");
519 if (status)
520 return 0;
521 return 2;
522 }
523 fprintf(stderr, "%s: %s\n", path, strerror(di.error().code()));
524 return 1;
525 }
526
527 Vector<FileMetadata> files;
528 size_t longest_name = 0;
529 while (di.has_next()) {
530 FileMetadata metadata;
531 metadata.name = di.next_path();
532
533 if (metadata.name.ends_with('~') && flag_ignore_backups && metadata.name != path)
534 continue;
535
536 StringBuilder builder;
537 builder.append({ path, strlen(path) });
538 builder.append('/');
539 builder.append(metadata.name);
540 metadata.path = builder.to_deprecated_string();
541 VERIFY(!metadata.path.is_null());
542 int rc = lstat(metadata.path.characters(), &metadata.stat);
543 if (rc < 0)
544 perror("lstat");
545
546 files.append(metadata);
547 if (metadata.name.length() > longest_name)
548 longest_name = metadata.name.length();
549 }
550 quick_sort(files, filemetadata_comparator);
551
552 if (print_names(path, longest_name, files))
553 printf("\n");
554 return 0;
555}
556
557bool filemetadata_comparator(FileMetadata& a, FileMetadata& b)
558{
559 if (flag_sort_by_timestamp && (a.stat.st_mtime != b.stat.st_mtime))
560 return (a.stat.st_mtime > b.stat.st_mtime) ^ flag_reverse_sort;
561 return (a.name < b.name) ^ flag_reverse_sort;
562}