#include #include #include #include #include #include #include #include #include #include #include #include struct Config { enum class Visibility { Normal, AlmostAll, All }; Visibility visibility { Visibility::Normal }; bool show_as_list { false }; bool human_readable { false }; bool directory { false }; }; struct simple_entry_t { BAN::String name; struct stat st; BAN::String link; }; struct full_entry_t { BAN::String access; BAN::String hard_links; BAN::String owner_name; BAN::String owner_group; BAN::String size; BAN::String month; BAN::String day; BAN::String time; BAN::String full_name; }; bool g_stdout_terminal { false }; winsize g_terminal_size {}; const char* entry_color(mode_t mode) { // TODO: handle suid, sgid, sticky if (S_ISFIFO(mode) || S_ISCHR(mode) || S_ISBLK(mode)) return "\e[33m"; if (S_ISDIR(mode)) return "\e[34m"; if (S_ISSOCK(mode)) return "\e[35m"; if (S_ISLNK(mode)) return "\e[36m"; if (mode & (S_IXUSR | S_IXGRP | S_IXOTH)) return "\e[32m"; return "\e[0m"; } BAN::String build_access_string(mode_t mode, const Config&) { BAN::String access; MUST(access.resize(10)); access[0] = S_ISBLK(mode) ? 'b' : S_ISCHR(mode) ? 'c' : S_ISDIR(mode) ? 'd' : S_ISFIFO(mode) ? 'f' : S_ISLNK(mode) ? 'l' : S_ISSOCK(mode) ? 's' : '-'; access[1] = (mode & S_IRUSR) ? 'r' : '-'; access[2] = (mode & S_IWUSR) ? 'w' : '-'; access[3] = (mode & S_ISUID) ? ((mode & S_IXUSR) ? 's' : 'S') : (mode & S_IXUSR) ? 'x' : '-'; access[4] = (mode & S_IRGRP) ? 'r' : '-'; access[5] = (mode & S_IWGRP) ? 'w' : '-'; access[6] = (mode & S_ISGID) ? ((mode & S_IXGRP) ? 's' : 'S') : (mode & S_IXGRP) ? 'x' : '-'; access[7] = (mode & S_IROTH) ? 'r' : '-'; access[8] = (mode & S_IWOTH) ? 'w' : '-'; access[9] = (mode & S_ISVTX) ? ((mode & S_IXOTH) ? 't' : 'T') : (mode & S_IXOTH) ? 'x' : '-'; return access; } BAN::String build_hard_links_string(nlink_t links, const Config&) { return MUST(BAN::String::formatted("{}", links)); } BAN::String build_owner_name_string(uid_t uid, const Config&) { struct passwd* passwd = getpwuid(uid); if (passwd == nullptr) return MUST(BAN::String::formatted("{}", uid)); return BAN::String(BAN::StringView(passwd->pw_name)); } BAN::String build_owner_group_string(gid_t gid, const Config&) { struct group* grp = getgrgid(gid); if (grp == nullptr) return MUST(BAN::String::formatted("{}", gid)); return BAN::String(BAN::StringView(grp->gr_name)); } BAN::String build_size_string(off_t size, const Config& config) { if (!config.human_readable || size < 1024) return MUST(BAN::String::formatted("{}", size)); size = size / 1024 * 10; size_t suffix_idx = 0; for (; size >= 10240; size /= 1024) suffix_idx++; constexpr char suffix[] { 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'R', 'Q' }; if (size >= 100) return MUST(BAN::String::formatted("{}{}", size / 10, suffix[suffix_idx])); return MUST(BAN::String::formatted("{}.{}{}", size / 10, size % 10, suffix[suffix_idx])); } BAN::String build_month_string(BAN::Time time, const Config&) { static const char* months[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; return BAN::String(BAN::StringView(months[(time.month - 1) % 12])); } BAN::String build_day_string(BAN::Time time, const Config&) { return MUST(BAN::String::formatted("{}", time.day)); } BAN::String build_time_string(BAN::Time time, const Config&) { static uint32_t current_year = ({ timespec real_time; clock_gettime(CLOCK_REALTIME, &real_time); BAN::from_unix_time(real_time.tv_sec).year; }); if (time.year != current_year) return MUST(BAN::String::formatted("{}", time.year)); return MUST(BAN::String::formatted("{2}:{2}", time.hour, time.minute)); } BAN::Vector resolve_column_widths(const BAN::Vector& entries, size_t columns) { BAN::Vector widths; MUST(widths.resize(columns)); const size_t rows = BAN::Math::div_round_up(entries.size(), columns); for (size_t i = 0; i < entries.size(); i++) widths[i / rows] = BAN::Math::max(widths[i / rows], entries[i].name.size()); size_t full_width = (columns - 1); for (auto width : widths) { if (width == 0) return {}; full_width += width; } if (full_width <= g_terminal_size.ws_col) return widths; return {}; } BAN::Vector resolve_layout(const BAN::Vector& entries) { for (size_t columns = entries.size(); columns > 1; columns--) if (auto widths = resolve_column_widths(entries, columns); !widths.empty()) return widths; return {}; } int list_directory(const BAN::String& path, Config config) { static char link_buffer[PATH_MAX]; BAN::Vector entries; struct stat st; auto stat_func = config.directory ? lstat : stat; if (stat_func(path.data(), &st) == -1) { perror("stat"); return 2; } const bool is_directory = S_ISDIR(st.st_mode); const size_t block_size = st.st_blksize; size_t blocks_used = 0; int ret = 0; if (!is_directory) { MUST(entries.emplace_back(path, st, BAN::String())); if (S_ISLNK(st.st_mode)) { if (readlink(path.data(), link_buffer, sizeof(link_buffer)) == -1) perror("readlink"); else MUST(entries.back().link.append(link_buffer)); } } else { DIR* dirp = opendir(path.data()); if (dirp == NULL) { perror("opendir"); return 2; } struct dirent* dirent; while ((dirent = readdir(dirp))) { switch (config.visibility) { case Config::Visibility::Normal: if (dirent->d_name[0] == '.') continue; break; case Config::Visibility::AlmostAll: if (strcmp(dirent->d_name, ".") == 0 || strcmp(dirent->d_name, "..") == 0) continue; break; case Config::Visibility::All: break; } if (fstatat(dirfd(dirp), dirent->d_name, &st, AT_SYMLINK_NOFOLLOW) == -1) { perror("fstatat"); ret = 1; continue; } blocks_used += st.st_blocks; MUST(entries.emplace_back(BAN::StringView(dirent->d_name), st, BAN::String())); if (S_ISLNK(st.st_mode)) { if (readlinkat(dirfd(dirp), dirent->d_name, link_buffer, sizeof(link_buffer)) == -1) perror("readlink"); else MUST(entries.back().link.append(link_buffer)); } } closedir(dirp); } BAN::sort::sort(entries.begin(), entries.end(), [](const simple_entry_t& lhs, const simple_entry_t& rhs) { // sort directories first const bool lhs_isdir = S_ISDIR(lhs.st.st_mode); const bool rhs_isdir = S_ISDIR(rhs.st.st_mode); if (lhs_isdir != rhs_isdir) return lhs_isdir; // sort by name for (size_t i = 0; i < BAN::Math::min(lhs.name.size(), rhs.name.size()); i++) if (lhs.name[i] != rhs.name[i]) return lhs.name[i] < rhs.name[i]; return lhs.name.size() < rhs.name.size(); } ); if (!config.show_as_list) { if (!g_stdout_terminal) { for (const auto& entry : entries) printf("%s\n", entry.name.data()); return ret; } bool should_quote = false; for (size_t i = 0; i < entries.size() && !should_quote; i++) should_quote = entries[i].name.sv().contains(' '); if (!should_quote) for (auto& entry : entries) MUST(entry.name.push_back(' ')); else { for (auto& entry : entries) { const char ch = entry.name.sv().contains(' ') ? '\'' : ' '; MUST(entry.name.insert(ch, 0)); MUST(entry.name.push_back(ch)); } } auto layout = resolve_layout(entries); if (layout.empty()) for (const auto& entry : entries) printf("%s%s\e[m\n", entry_color(entry.st.st_mode), entry.name.data()); else { const size_t cols = layout.size(); const size_t rows = BAN::Math::div_round_up(entries.size(), cols); for (size_t row = 0; row < rows; row++) { for (size_t col = 0; col < cols; col++) { const size_t i = col * rows + row; if (i >= entries.size()) break; char format[32]; sprintf(format, "%%s%%-%zus\e[m", layout[col]); if (col != 0) printf(" "); printf(format, entry_color(entries[i].st.st_mode), entries[i].name.data()); } printf("\n"); } } return ret; } BAN::Vector full_entries; MUST(full_entries.reserve(entries.size())); full_entry_t max_entry; for (const simple_entry_t& entry : entries) { full_entry_t full_entry; #define GET_ENTRY_STRING(property, input) \ full_entry.property = build_ ## property ## _string(input, config); \ if (full_entry.property.size() > max_entry.property.size()) \ max_entry.property = full_entry.property; GET_ENTRY_STRING(access, entry.st.st_mode); GET_ENTRY_STRING(hard_links, entry.st.st_nlink); GET_ENTRY_STRING(owner_name, entry.st.st_uid); GET_ENTRY_STRING(owner_group, entry.st.st_gid); GET_ENTRY_STRING(size, entry.st.st_size); BAN::Time time = BAN::from_unix_time(entry.st.st_mtim.tv_sec); GET_ENTRY_STRING(month, time); GET_ENTRY_STRING(day, time); GET_ENTRY_STRING(time, time); full_entry.full_name = MUST(BAN::String::formatted("{}{}\e[m", entry_color(entry.st.st_mode), entry.name)); if (S_ISLNK(entry.st.st_mode)) { MUST(full_entry.full_name.append(" -> "_sv)); MUST(full_entry.full_name.append(entry.link)); } MUST(full_entries.push_back(BAN::move(full_entry))); } if (is_directory) { if (config.human_readable) printf("total: %s\n", build_size_string(blocks_used * block_size, config).data()); else printf("total: %zu\n", blocks_used); } for (const auto& full_entry : full_entries) printf("%*s %*s %*s %*s %*s %*s %*s %*s %s\n", (int)max_entry.access.size(), full_entry.access.data(), (int)max_entry.hard_links.size(), full_entry.hard_links.data(), (int)max_entry.owner_name.size(), full_entry.owner_name.data(), (int)max_entry.owner_group.size(), full_entry.owner_group.data(), (int)max_entry.size.size(), full_entry.size.data(), (int)max_entry.month.size(), full_entry.month.data(), (int)max_entry.day.size(), full_entry.day.data(), (int)max_entry.time.size(), full_entry.time.data(), full_entry.full_name.data() ); return ret; } int usage(const char* argv0, int ret) { FILE* fout = ret ? stderr : stdout; fprintf(fout, "usage: %s [OPTION]... [FILE]...\n", argv0); fprintf(fout, " -a, --all show hidden files\n"); fprintf(fout, " -l, --list use list format\n"); fprintf(fout, " -d, --directory show directories as directories, don't list their contents\n"); fprintf(fout, " -h, --help show this message and exit\n"); return ret; } int main(int argc, char* argv[]) { Config config; for (;;) { static option long_options[] { { "all", no_argument, nullptr, 'a' }, { "almost-all", no_argument, nullptr, 'A' }, { "directory" , no_argument, nullptr, 'd' }, { "human-readable", no_argument, nullptr, 'h' }, { "list", no_argument, nullptr, 'l' }, { "help", no_argument, nullptr, 'x' }, }; int ch = getopt_long(argc, argv, "aAlh", long_options, nullptr); if (ch == -1) break; switch (ch) { case 'a': config.visibility = Config::Visibility::All; break; case 'A': config.visibility = Config::Visibility::AlmostAll; break; case 'd': config.directory = true; break; case 'h': config.human_readable = true; break; case 'l': config.show_as_list = true; break; case 'x': fprintf(stderr, "usage: %s [OPTION]... [FILE]...\n", argv[0]); fprintf(stderr, " list information about FILEs\n"); fprintf(stderr, "OPTIONS:\n"); fprintf(stderr, " -a, --all do not ignore entries starting with .\n"); fprintf(stderr, " -A, --almost-all do not list . and ..\n"); fprintf(stderr, " -d, --directory list directories and not their contents\n"); fprintf(stderr, " -h, --human-readable print sizes in human readable form\n"); fprintf(stderr, " -l, --list use long listing format\n"); fprintf(stderr, " --help show this message and exit\n"); return 0; case '?': fprintf(stderr, "invalid option %c\n", optopt); fprintf(stderr, "see '%s --help' for usage\n", argv[0]); return 1; } } BAN::Vector files; if (optind == argc) MUST(files.emplace_back("."_sv)); else for (int i = optind; i < argc; i++) MUST(files.emplace_back(BAN::StringView(argv[i]))); g_stdout_terminal = isatty(STDOUT_FILENO) && tcgetwinsize(STDOUT_FILENO, &g_terminal_size) == 0; int ret = 0; for (size_t i = 0; i < files.size(); i++) { if (i > 0) printf("\n"); if (files.size() > 1) printf("%s:\n", files[i].data()); ret = BAN::Math::max(ret, list_directory(files[i], config)); } return ret; }