473 lines
12 KiB
C++
473 lines
12 KiB
C++
#include <BAN/Sort.h>
|
|
#include <BAN/String.h>
|
|
#include <BAN/Time.h>
|
|
#include <BAN/Vector.h>
|
|
|
|
#include <dirent.h>
|
|
#include <fcntl.h>
|
|
#include <getopt.h>
|
|
#include <grp.h>
|
|
#include <pwd.h>
|
|
#include <stdio.h>
|
|
#include <sys/stat.h>
|
|
#include <termios.h>
|
|
|
|
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<size_t> resolve_column_widths(const BAN::Vector<simple_entry_t>& entries, size_t columns)
|
|
{
|
|
BAN::Vector<size_t> 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<size_t> resolve_layout(const BAN::Vector<simple_entry_t>& 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<simple_entry_t> 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_entry_t> 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<BAN::String> 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;
|
|
}
|