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