From 328f4e1fd69615335daaf8510fbc3e2c9f075f5a Mon Sep 17 00:00:00 2001 From: Jonas 'Sortie' Termansen Date: Sun, 25 Oct 2015 18:27:13 +0100 Subject: [PATCH] Add features to ls(1) and fix implementation issues. New options: -1 Disable columnizing. -c Sort by file status change time. -C Column directory entries. -h Show file sizes with magnitude suffixes such as 42.3K. -r Sort entries in the opposite order. -R Recursively list directory contents. -S Sort by file size in decreasing order. -u Sort by access time. --color=always|auto|never Control whether colorizing is enabled. The output is now natively columnized instead of running a column(1) subprocess. Multiple operands are now implemented correctly (directory contents are columned separately rather than their concatenation, the name of each directory is printed prior to its content, ...). The file owner and group names are now shown rather than a hard-coded root. Long listings are now properly aligned. Mixing file and directory operands works correctly now. Columnizing is now vertical rather than across. Clean up the source code and remove cruft. Stat information on directory entries are read once only now, which which fixes an incosistent sort race condition when sorting according to lstat. Move to the openat paradigm. --- utils/ls.cpp | 1145 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 776 insertions(+), 369 deletions(-) diff --git a/utils/ls.cpp b/utils/ls.cpp index 92d8bcc5..cdf5d66f 100644 --- a/utils/ls.cpp +++ b/utils/ls.cpp @@ -26,134 +26,243 @@ #include #include +#include #include -#include #include +#include #include #include +#include #include #include #include #include #include #include +#include #include #include +#include -int current_year; +struct passwd* getpwuid_cache(uid_t uid) +{ + static struct passwd* cache_pwd; + static uid_t cache_uid; + if ( cache_pwd && cache_uid == uid ) + return cache_pwd; + if ( (cache_pwd = getpwuid(uid)) ) + cache_uid = uid; + return cache_pwd; +} -bool option_colors = false; -bool option_directory = false; -bool option_inode = false; -bool option_long_format = false; -bool option_show_dotdot = false; -bool option_show_dotfiles = false; -bool option_time_modified = false; +struct group* getgrgid_cache(gid_t gid) +{ + static struct group* cache_grp; + static gid_t cache_gid; + if ( cache_grp && cache_gid == gid ) + return cache_grp; + if ( (cache_grp = getgrgid(gid)) ) + cache_gid = gid; + return cache_grp; +} -int stdout_copy_fd; -pid_t child_pid; +enum dotfile +{ + DOTFILE_NO, + DOTFILE_ALMOST, + DOTFILE_ALL, +}; + +enum sort +{ + SORT_COLLATE, + SORT_SIZE, + SORT_TIME, +}; + +enum timestamp +{ + TIMESTAMP_ATIME, + TIMESTAMP_CTIME, + TIMESTAMP_MTIME, +}; + +struct record +{ + struct dirent* dirent; + struct stat st; + int stat_attempt; + char* symlink_path; + struct stat symlink_st; + int symlink_stat_attempt; +}; + +static int order_normal(int comparison) +{ + return comparison; +} + +static int order_reverse(int comparison) +{ + return comparison == 0 ? 0 : comparison < 0 ? 1 : -1; +} + +static int current_year; + +static enum dotfile option_all = DOTFILE_NO; +static bool option_colors = false; +static bool option_column = false; +static bool option_directory = false; +static bool option_human_readable = false; +static bool option_inode = false; +static bool option_long = false; +static bool option_multiple_operands = false; +static bool option_recursive = false; +static int (*option_reverse)(int) = order_normal; +static enum sort option_sort = SORT_COLLATE; +static enum timestamp option_time_type = TIMESTAMP_MTIME; + +static size_t string_display_length(const char* str) +{ + size_t display_length = 0; + mbstate_t ps; + memset(&ps, 0, sizeof(ps)); + while ( true ) + { + wchar_t wc; + size_t amount = mbrtowc(&wc, str, SIZE_MAX, &ps); + if ( amount == 0 ) + break; + if ( amount == (size_t) -1 || amount == (size_t) -2 ) + { + display_length++; + str++; + memset(&ps, 0, sizeof(ps)); + continue; + } + int width = wcwidth(wc); + if ( width < 0 ) + width = 0; + if ( SIZE_MAX - display_length < (size_t) width ) + display_length = SIZE_MAX; + else + display_length += (size_t) width; + str += amount; + } + return display_length; +} + +static void print_left_aligned(const char* string, size_t field_width) +{ + size_t string_width = string_display_length(string); + fputs(string, stdout); + for ( size_t i = string_width; i < field_width; i++ ) + putchar(' '); +} + +static void print_right_aligned(const char* string, size_t field_width) +{ + size_t string_width = string_display_length(string); + for ( size_t i = string_width; i < field_width; i++ ) + putchar(' '); + fputs(string, stdout); +} + +#define FORMAT_BYTES_LENGTH (sizeof(off_t) * 3 + 1 + 1 + 1) +static void format_bytes_amount(char* dest, size_t destsize, uintmax_t num_bytes) +{ + uintmax_t value = num_bytes; + uintmax_t value_fraction = 0; + uintmax_t exponent = 1024; + char suffixes[] = { 'B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y' }; + size_t num_suffixes = sizeof(suffixes) / sizeof(suffixes[0]); + size_t suffix_index = 0; + while ( exponent <= value && suffix_index + 1 < num_suffixes) + { + value_fraction = value % exponent; + value /= exponent; + suffix_index++; + } + char suffix = suffixes[suffix_index]; + if ( suffix_index == 0 ) + { + snprintf(dest, destsize, "%ju%c", value, suffix); + return; + } + char value_fraction_char = '0' + (value_fraction / (1024 / 10 + 1)) % 10; + snprintf(dest, destsize, "%ju.%c%c", value, value_fraction_char, suffix); +} static struct dirent* dirent_dup(struct dirent* entry) { - struct dirent* copy = (struct dirent*) malloc(entry->d_reclen); + size_t size = sizeof(struct dirent) + strlen(entry->d_name) + 1; + struct dirent* copy = (struct dirent*) malloc(size); if ( !copy ) return NULL; - memcpy(copy, entry, entry->d_reclen); + memcpy(copy, entry, size); return copy; } -bool finish_output() +static struct dirent* dirent_make(const char* name) { - bool result = true; - int errnum = errno; - if ( fflush(stdout) == EOF ) - result = false; - if ( child_pid ) - { - int status; - close(1); - dup2(stdout_copy_fd, 1); - waitpid(child_pid, &status, 0); - child_pid = 0; - if ( !WIFEXITED(status) || WEXITSTATUS(status) != 0 ) - result = false; - } - errno = errnum; - return result; + size_t size = sizeof(struct dirent) + strlen(name) + 1; + struct dirent* ret = (struct dirent*) malloc(size); + if ( !ret ) + return NULL; + strcpy(ret->d_name, name); + return ret; } -void ls_error(int status, int errnum, const char* format, ...) +static struct timespec record_timestamp(struct record* record) { - finish_output(); - - // TODO: The rest is just plain generic gnu_error(3), how about a function - // called gnu_verror(3) that accepts a va_list. - fprintf(stderr, "%s: ", program_invocation_name); - - va_list list; - va_start(list, format); - vfprintf(stderr, format, list); - va_end(list); - - if ( errnum ) - fprintf(stderr, ": %s", strerror(errnum)); - fprintf(stderr, "\n"); - if ( status ) - exit(status); + switch ( option_time_type ) + { + case TIMESTAMP_ATIME: return record->st.st_atim; + case TIMESTAMP_CTIME: return record->st.st_ctim; + case TIMESTAMP_MTIME: return record->st.st_mtim; + } + __builtin_unreachable(); } -int argv_mtim_compare(const void* a_void, const void* b_void) +static int sort_records(const void* a_ptr, const void* b_ptr) { - const char* a = *(const char**) a_void; - const char* b = *(const char**) b_void; - - if ( !a && b ) - return 1; - if ( a && !b ) - return -1; - if ( !a && !b ) - return 0; - - struct stat a_st; - struct stat b_st; - if ( lstat(a, &a_st) == 0 && lstat(b, &b_st) == 0 ) + struct record* a = (struct record*) a_ptr; + struct record* b = (struct record*) b_ptr; + if ( option_sort == SORT_SIZE ) { - if ( a_st.st_mtim.tv_sec < b_st.st_mtim.tv_sec ) - return 1; - if ( a_st.st_mtim.tv_sec > b_st.st_mtim.tv_sec ) - return -1; - if ( a_st.st_mtim.tv_nsec < b_st.st_mtim.tv_nsec ) - return 1; - if ( a_st.st_mtim.tv_nsec > b_st.st_mtim.tv_nsec ) - return -1; - return 0; + if ( a->st.st_size < b->st.st_size ) + return option_reverse(1); + if ( b->st.st_size < a->st.st_size ) + return option_reverse(1); } - return strcmp(a, b); + else if ( option_sort == SORT_TIME ) + { + struct timespec a_ts = record_timestamp(a); + struct timespec b_ts = record_timestamp(b); + if ( a_ts.tv_sec < b_ts.tv_sec ) + return option_reverse(1); + if ( a_ts.tv_sec > b_ts.tv_sec ) + return option_reverse(-1); + if ( a_ts.tv_nsec < b_ts.tv_nsec ) + return option_reverse(1); + if ( a_ts.tv_nsec > b_ts.tv_nsec ) + return option_reverse(-1); + } + return option_reverse(strcoll(a->dirent->d_name, b->dirent->d_name)); } -int sort_dirents_dirfd; - -int sort_dirents(const void* a_void, const void* b_void) +static int sort_files_then_dirs(const void* a_ptr, const void* b_ptr) { - struct dirent* a = *(struct dirent**) a_void; - struct dirent* b = *(struct dirent**) b_void; - struct stat a_st; - struct stat b_st; - if ( option_time_modified && - fstatat(sort_dirents_dirfd, a->d_name, &a_st, 0) == 0 && - fstatat(sort_dirents_dirfd, b->d_name, &b_st, 0) == 0 ) + struct record* a = (struct record*) a_ptr; + struct record* b = (struct record*) b_ptr; + if ( !option_directory ) { - if ( a_st.st_mtim.tv_sec < b_st.st_mtim.tv_sec ) + if ( S_ISDIR(a->st.st_mode) && !S_ISDIR(b->st.st_mode) ) return 1; - if ( a_st.st_mtim.tv_sec > b_st.st_mtim.tv_sec ) + if ( !S_ISDIR(a->st.st_mode) && S_ISDIR(b->st.st_mode) ) return -1; - if ( a_st.st_mtim.tv_nsec < b_st.st_mtim.tv_nsec ) - return 1; - if ( a_st.st_mtim.tv_nsec > b_st.st_mtim.tv_nsec ) - return -1; - return 0; } - return strcmp(a->d_name, b->d_name); + return sort_records(a_ptr, b_ptr); } static unsigned char mode_to_dt(mode_t mode) @@ -175,135 +284,328 @@ static unsigned char mode_to_dt(mode_t mode) return DT_UNKNOWN; } -static void color_entry(bool enable_colors, - const char** pre_ptr, - const char** post_ptr, - unsigned char type, - struct stat* st, - bool error_situation) +static void color(const char** pre, + const char** post, + struct stat* st, + bool failure) { - const char* pre = ""; - const char* post = ""; + *pre = ""; + *post = ""; - if ( error_situation ) + if ( !option_colors ) + return; + + if ( failure ) { - pre = "\e[31m"; - post = "\e[m"; - } - else if ( enable_colors ) - { - post = "\e[m"; - switch ( type ) - { - case DT_UNKNOWN: pre = "\e[91m"; break; - case DT_BLK: pre = "\e[93m"; break; - case DT_CHR: pre = "\e[93m"; break; - case DT_DIR: pre = "\e[36m"; break; - case DT_FIFO: pre = "\e[33m"; break; - case DT_LNK: pre = "\e[96m"; break; - case DT_SOCK: pre = "\e[35m"; break; - case DT_REG: - if ( st && (st->st_mode & 0111) ) - pre = "\e[32m"; - else - post = ""; - break; - default: - post = ""; - } + *pre = "\e[31m"; + *post = "\e[m"; + return; } - *pre_ptr = pre; - *post_ptr = post; + *post = "\e[m"; + switch ( mode_to_dt(st->st_mode) ) + { + case DT_UNKNOWN: *pre = "\e[91m"; break; + case DT_BLK: *pre = "\e[93m"; break; + case DT_CHR: *pre = "\e[93m"; break; + case DT_DIR: *pre = "\e[36m"; break; + case DT_FIFO: *pre = "\e[33m"; break; + case DT_LNK: *pre = "\e[96m"; break; + case DT_SOCK: *pre = "\e[35m"; break; + case DT_REG: + if ( st->st_mode & 0111 ) + *pre = "\e[32m"; + else + *post = ""; + break; + default: + *post = ""; + } } -int handle_entry_internal(const char* fullpath, const char* name, unsigned char type) +static void color_record(const char** pre, + const char** post, + struct record* record) { - // TODO: Use openat and fstat. + bool failure = record->stat_attempt < 0 || + (record->dirent->d_type == DT_LNK && !record->symlink_path); + color(pre, post, &record->st, failure); +} - struct stat st; - memset(&st, 0, sizeof(st)); - bool stat_error = false; - if ( option_long_format || - type == DT_UNKNOWN || - type == DT_REG || - type == DT_LNK ) +static void color_symlink(const char** pre, + const char** post, + struct record* record) +{ + color(pre, post, &record->symlink_st, record->symlink_stat_attempt < 0); +} + +static bool should_stat(struct record* record) +{ + (void) record; + return option_colors || + option_long || + option_recursive || + option_sort != SORT_COLLATE; +} + +static bool stat_symlink(DIR* dir, const char* dirpath, struct record* record) +{ + if ( !(0 < record->st.st_size && record->st.st_size <= 65536) ) + return true; + size_t symlink_path_size = record->st.st_size + 1; + if ( !(record->symlink_path = (char*) malloc(symlink_path_size)) ) + err(1, "malloc"); + ssize_t ret = readlinkat(dirfd(dir), record->dirent->d_name, + record->symlink_path, symlink_path_size); + if ( ret < 0 ) { - if ( lstat(fullpath, &st) == 0 ) - type = mode_to_dt(st.st_mode); - else - stat_error = true; + warn("readlink: %s/%s", dirpath, record->dirent->d_name); + return false; + } + record->symlink_path[ret] = '\0'; + if ( fstatat(dirfd(dir), record->symlink_path, &record->symlink_st, 0) < 0 ) + return record->symlink_stat_attempt = -1, true; + return record->symlink_stat_attempt = 1, true; +} + +static bool stat_record(DIR* dir, const char* dirpath, struct record* record) +{ + record->st.st_ino = record->dirent->d_ino; + record->st.st_dev = record->dirent->d_dev; + if ( !should_stat(record) ) + return record->stat_attempt = 0, true; + if ( fstatat(dirfd(dir), record->dirent->d_name, &record->st, + AT_SYMLINK_NOFOLLOW) < 0 ) + { + warn("stat: %s/%s", dirpath, record->dirent->d_name); + return record->stat_attempt = -1, false; + } + if ( record->dirent->d_type == DT_UNKNOWN ) + record->dirent->d_type = mode_to_dt(record->st.st_mode); + record->stat_attempt = 1; + if ( S_ISLNK(record->st.st_mode) && !stat_symlink(dir, dirpath, record) ) + return false; + return true; +} + +static void show_simple(struct record* records, size_t count) +{ + for ( size_t i = 0; i < count; i++ ) + { + struct record* record = &records[i]; + if ( option_inode ) + printf("%ju ", (uintmax_t) record->st.st_ino); + const char* pre; + const char* post; + color_record(&pre, &post, record); + printf("%s%s%s\n", pre, record->dirent->d_name, post); + } +} + +struct column_size +{ + size_t width_inode; + size_t width_name; +}; + +static void show_column(struct record* records, size_t count) +{ + static size_t display_width = 80; + static bool display_width_set = false; + if ( !display_width_set ) + { + const char* display_width_env = getenv("COLUMNS"); + struct winsize ws; + if ( display_width_env ) + display_width = strtoul(display_width_env, NULL, 10); + else if ( tcgetwinsize(1, &ws) == 0 ) + display_width = ws.ws_col; } - char* link_dest = NULL; - bool link_stat_error = false; - struct stat link_st; - memset(&link_st, 0, sizeof(link_st)); - unsigned char link_type = DT_UNKNOWN; - if ( type == DT_LNK ) + if ( !count ) + return; + + // TODO: -x support. + struct column_size* column_sizes; + size_t columns = 0; + size_t rows = 0; + while ( true ) { - size_t link_dest_length = 0 <= st.st_size ? st.st_size : 256; - link_dest = (char*) malloc(link_dest_length + 1); - assert(link_dest); - ssize_t readlink_ret = readlink(fullpath, link_dest, link_dest_length); - if ( readlink_ret <= 0 ) + size_t attempt_rows = rows + 1; + size_t attempt_columns = count / attempt_rows + + (count % attempt_rows ? 1 : 0); + struct column_size* attempt_column_sizes = (struct column_size*) + reallocarray(NULL, attempt_columns, sizeof(struct column_size)); + if ( !attempt_column_sizes ) + err(1, "malloc"); + size_t attempt_width = 0; + for ( size_t c = 0; c < attempt_columns; c++ ) { - free(link_dest); - link_dest = NULL; - link_stat_error = true; - } - else - { - link_dest[readlink_ret] = '\0'; - const char* next_link_dest = link_dest; - char* new_link_dest = NULL; - if ( link_dest[0] != '/' ) + struct column_size* c_sizes = &attempt_column_sizes[c]; + memset(c_sizes, 0, sizeof(*c_sizes)); + size_t c_off = attempt_rows * c; + for ( size_t r = 0; r < attempt_rows; r++ ) { - char* fullpath_dir_raw = strdup(fullpath); - const char* fullpath_dir = dirname(fullpath_dir_raw); - asprintf(&new_link_dest, "%s/%s", fullpath_dir, link_dest); - assert(new_link_dest); - free(fullpath_dir_raw); - next_link_dest = new_link_dest; + size_t i = c_off + r; + if ( count <= i ) + break; + struct record* record = &records[i]; + if ( option_inode ) + { + char inode_str[sizeof(ino_t) * 3]; + snprintf(inode_str, sizeof(inode_str), + "%ju", (uintmax_t) record->dirent->d_ino); + size_t inode_width = string_display_length(inode_str); + if ( c_sizes->width_inode < inode_width ) + c_sizes->width_inode = inode_width; + } + const char* name = record->dirent->d_name; + size_t name_width = string_display_length(name); + if ( c_sizes->width_name < name_width ) + c_sizes->width_name = name_width; } - if ( lstat(next_link_dest, &link_st) == 0 ) - link_type = mode_to_dt(link_st.st_mode); + if ( c != 0 ) + attempt_width += 2; + if ( option_inode ) + attempt_width += c_sizes->width_inode + 1; + attempt_width += c_sizes->width_name; + } + rows = attempt_rows; + columns = attempt_columns; + if ( attempt_width <= display_width || attempt_rows == count ) + { + column_sizes = attempt_column_sizes; + break; + } + free(attempt_column_sizes); + } + + for ( size_t r = 0; r < rows; r++ ) + { + for ( size_t c = 0; c < columns; c++ ) + { + size_t i = c * rows + r; + if ( count <= i ) + break; + struct column_size* c_sizes = &column_sizes[c]; + struct record* record = &records[i]; + if ( option_inode ) + { + char inode_str[sizeof(ino_t) * 3]; + snprintf(inode_str, sizeof(inode_str), + "%ju", (uintmax_t) record->dirent->d_ino); + print_right_aligned(inode_str, c_sizes->width_inode); + putchar(' '); + } + const char* pre; + const char* post; + color_record(&pre, &post, record); + fputs(pre, stdout); + if ( c + 1 == columns || count - i <= rows ) + fputs(record->dirent->d_name, stdout); else - link_stat_error = true; - free(new_link_dest); + print_left_aligned(record->dirent->d_name, c_sizes->width_name); + fputs(post, stdout); + if ( c + 1 == columns || count - i <= rows ) + putchar('\n'); + else + fputs(" ", stdout); } } - const char* color_pre = ""; - const char* color_post = ""; - color_entry(option_colors, - &color_pre, - &color_post, - type, - !stat_error ? &st : NULL, - link_stat_error); + free(column_sizes); +} - const char* link_color_pre = ""; - const char* link_color_post = ""; - if ( type == DT_LNK ) - color_entry(option_colors, - &link_color_pre, - &link_color_post, - link_type, - !link_stat_error ? &link_st : NULL, - link_stat_error); +static void show_long(struct record* records, size_t count) +{ + size_t nlink_field_width = 0; + size_t owner_field_width = 0; + size_t group_field_width = 0; + size_t size_field_width = 0; + size_t time_field_width = 0; - if ( option_inode ) - printf("%ju ", (uintmax_t) st.st_ino); - if ( !option_long_format ) + for ( size_t i = 0; i < count; i++ ) { - printf("%s%s%s\n", color_pre, name, color_post); - free(link_dest); - return 0; + struct record* record = &records[i]; + + nlink_t nlink = record->st.st_nlink; + char nlink_str[sizeof(nlink_t) * 3]; + snprintf(nlink_str, sizeof(nlink_str), "%ju", (uintmax_t) nlink); + size_t nlink_width = string_display_length(nlink_str); + if ( nlink_field_width < nlink_width ) + nlink_field_width = nlink_width; + + char owner_fallback[sizeof(uid_t) * 3]; + struct passwd* pwd; + const char* owner_str; + if ( record->stat_attempt != 1 ) + owner_str = "?"; + else if ( (pwd = getpwuid_cache(record->st.st_uid)) ) + owner_str = pwd->pw_name; + else + { + snprintf(owner_fallback, sizeof(owner_fallback), + "%ju", (uintmax_t) record->st.st_uid); + owner_str = owner_fallback; + } + size_t owner_width = string_display_length(owner_str); + if ( owner_field_width < owner_width ) + owner_field_width = owner_width; + + char group_fallback[sizeof(gid_t) * 3]; + struct group* grp; + const char* group_str; + if ( record->stat_attempt != 1 ) + group_str = "?"; + else if ( (grp = getgrgid_cache(record->st.st_gid)) ) + group_str = grp->gr_name; + else + { + snprintf(group_fallback, sizeof(group_fallback), + "%ju", (uintmax_t) record->st.st_gid); + group_str = group_fallback; + } + size_t group_width = string_display_length(group_str); + if ( group_field_width < group_width ) + group_field_width = group_width; + + off_t size = record->st.st_size; + char size_str[FORMAT_BYTES_LENGTH + 1]; + if ( option_human_readable ) + format_bytes_amount(size_str, sizeof(size_str), size); + else + snprintf(size_str, sizeof(size_str), "%ji", (intmax_t) size); + size_t size_width = string_display_length(size_str); + if ( size_field_width < size_width ) + size_field_width = size_width; + + struct timespec ts = record_timestamp(record); + struct tm mod_tm; + localtime_r(&ts.tv_sec, &mod_tm); + char time_str[64]; + if ( current_year == mod_tm.tm_year ) + strftime(time_str, sizeof(time_str), "%b %e %H:%M", &mod_tm); + else + strftime(time_str, sizeof(time_str), "%b %e %Y", &mod_tm); + size_t time_width = string_display_length(time_str); + if ( time_field_width < time_width ) + time_field_width = time_width; } - char perms[11]; - switch ( type ) + + // TODO: Show a total number of filesystem blocks. + // (But only when listing a directory?) + + for ( size_t i = 0; i < count; i++ ) { + struct record* record = &records[i]; + + if ( option_inode ) + printf("%ju ", (uintmax_t) record->st.st_ino); + + mode_t mode = record->st.st_mode; + char perms[11]; + switch ( record->dirent->d_type ) + { case DT_UNKNOWN: perms[0] = '?'; break; case DT_BLK: perms[0] = 'b'; break; case DT_CHR: perms[0] = 'c'; break; @@ -313,141 +615,256 @@ int handle_entry_internal(const char* fullpath, const char* name, unsigned char case DT_REG: perms[0] = '-'; break; case DT_SOCK: perms[0] = 's'; break; default: perms[0] = '?'; break; + } + const char flagnames[] = { 'x', 'w', 'r' }; + for ( size_t n = 0; n < 9; n++ ) + perms[9-n] = mode & (1 << n) ? flagnames[n % 3] : '-'; + if ( mode & S_ISUID ) + perms[3] = mode & 0100 ? 's' : 'S'; + if ( mode & S_ISGID ) + perms[6] = mode & 0010 ? 's' : 'S'; + if ( mode & S_ISVTX ) + perms[9] = mode & 0001 ? 't' : 'T'; + if ( record->stat_attempt != 1 ) + memset(perms, '?', sizeof(perms) - 1); + perms[10] = '\0'; + fputs(perms, stdout); + putchar(' '); + + nlink_t nlink = record->st.st_nlink; + char nlink_str[sizeof(nlink_t) * 3]; + snprintf(nlink_str, sizeof(nlink_str), "%ju", (uintmax_t) nlink); + print_right_aligned(nlink_str, nlink_field_width); + putchar(' '); + + char owner_fallback[sizeof(uid_t) * 3]; + struct passwd* pwd; + const char* owner_str; + if ( record->stat_attempt != 1 ) + owner_str = "?"; + else if ( (pwd = getpwuid_cache(record->st.st_uid)) ) + owner_str = pwd->pw_name; + else + { + snprintf(owner_fallback, sizeof(owner_fallback), + "%ju", (uintmax_t) record->st.st_uid); + owner_str = owner_fallback; + } + print_left_aligned(owner_str, owner_field_width); + putchar(' '); + + char group_fallback[sizeof(gid_t) * 3]; + struct group* grp; + const char* group_str; + if ( record->stat_attempt != 1 ) + group_str = "?"; + else if ( (grp = getgrgid_cache(record->st.st_gid)) ) + group_str = grp->gr_name; + else + { + snprintf(group_fallback, sizeof(group_fallback), + "%ju", (uintmax_t) record->st.st_gid); + group_str = group_fallback; + } + print_left_aligned(group_str, group_field_width); + putchar(' '); + + off_t size = record->st.st_size; + char size_str[FORMAT_BYTES_LENGTH + 1]; + if ( option_human_readable ) + format_bytes_amount(size_str, sizeof(size_str), size); + else + snprintf(size_str, sizeof(size_str), "%ji", (intmax_t) size); + print_right_aligned(size_str, size_field_width); + putchar(' '); + + struct timespec ts = record_timestamp(record); + struct tm mod_tm; + localtime_r(&ts.tv_sec, &mod_tm); + char time_str[64]; + if ( current_year == mod_tm.tm_year ) + strftime(time_str, sizeof(time_str), "%b %e %H:%M", &mod_tm); + else + strftime(time_str, sizeof(time_str), "%b %e %Y", &mod_tm); + print_left_aligned(time_str, time_field_width); + putchar(' '); + + const char* pre; + const char* post; + color_record(&pre, &post, record); + fputs(pre, stdout); + fputs(record->dirent->d_name, stdout); + fputs(post, stdout); + if ( record->symlink_path ) + { + color_symlink(&pre, &post, record); + fputs(" -> ", stdout); + fputs(pre, stdout); + fputs(record->symlink_path, stdout); + fputs(post, stdout); + } + putchar('\n'); } - const char flagnames[] = { 'x', 'w', 'r' }; - for ( size_t i = 0; i < 9; i++ ) - { - bool set = st.st_mode & (1UL< %s%s%s", link_color_pre, link_dest, link_color_post); - printf("\n"); - free(link_dest); - return 0; + show_simple(records, count); } -int handle_entry(const char* path, const char* name, unsigned char type) -{ - bool isdotdot = strcmp(name, ".") == 0 || strcmp(name, "..") == 0; - bool isdotfile = !isdotdot && name[0] == '.'; - if ( isdotdot && !option_show_dotdot && !option_directory ) - return 0; - if ( isdotfile && !option_show_dotfiles && !option_directory ) - return 0; - char* fullpath; - asprintf(&fullpath, "%s/%s", path, name); - int result = handle_entry_internal(fullpath, name, type); - free(fullpath); - return result; -} +static int ls_directory(int parentfd, const char* relpath, const char* path); -int ls(const char* path) +static int show_recursive(int fd, const char* path, + struct record* records, size_t count) { - if ( option_directory ) - return handle_entry_internal(path, path, DT_UNKNOWN); - - int ret = 1; - DIR* dir; - const size_t DEFAULT_ENTRIES_LEN = 4UL; - size_t entrieslen = DEFAULT_ENTRIES_LEN; - size_t entriesused = 0; - size_t entriessize = sizeof(struct dirent*) * entrieslen; - struct dirent** entries = (struct dirent**) malloc(entriessize); - if ( !entries ) + // TODO: Use a proper join path function below. + if ( path && !strcmp(path, "/") ) + path = ""; + if ( option_recursive && path ) + show(records, count); + int ret = 0; + qsort(records, count, sizeof(*records), sort_files_then_dirs); + size_t nondir_count = count; + if ( !option_directory ) { - ls_error(0, errno, "malloc"); - goto cleanup_done; + for ( nondir_count = 0; nondir_count < count; nondir_count++ ) + if ( S_ISDIR(records[nondir_count].st.st_mode) ) + break; } + if ( !(option_recursive && path) ) + show(records, nondir_count); + for ( size_t i = nondir_count; i < count; i++ ) + { + struct record* record = &records[i]; + static bool not_first_directory = false; + if ( (not_first_directory && option_recursive) || i != 0 ) + putchar('\n'); + not_first_directory = true; + const char* name = record->dirent->d_name; + char* subpath_storage = NULL; + if ( path && asprintf(&subpath_storage, "%s/%s", path, name) < 0 ) + err(1, "malloc"); + const char* subpath = path ? subpath_storage : name; + if ( option_recursive || 2 <= count ) + printf("%s:\n", subpath); + ret |= ls_directory(fd, name, subpath); + free(subpath_storage); + } + return ret; +} - dir = opendir(path); +static int ls_directory(int parentfd, const char* relpath, const char* path) +{ + int fd = openat(parentfd, relpath, O_RDONLY | O_DIRECTORY); + if ( fd < 0 ) + { + warn("%s", path); + return 1; + } + DIR* dir = fdopendir(fd); if ( !dir ) { - if ( errno == ENOTDIR ) - return handle_entry_internal(path, path, DT_UNKNOWN); - error(0, errno, "%s", path); - ret = 2; - goto cleanup_entries; + warn("fdopendir: %s", path); + close(fd); + return 1; } - while ( struct dirent* entry = readdir(dir) ) + size_t records_used = 0; + size_t records_length = 64; + struct record* records = (struct record*) + reallocarray(NULL, records_length, sizeof(struct record)); + if ( !records ) + err(1, "malloc"); + + int ret = 0; + + struct dirent* entry; + while ( (errno = 0, entry = readdir(dir)) ) { - if ( entriesused == entrieslen ) + const char* name = entry->d_name; + bool isdotdot = strcmp(name, ".") == 0 || strcmp(name, "..") == 0; + if ( isdotdot && option_all != DOTFILE_ALL ) + continue; + bool isdotfile = !isdotdot && name[0] == '.'; + if ( isdotfile && option_all == DOTFILE_NO ) + continue; + + if ( records_used == records_length ) { - size_t newentrieslen = entrieslen * 2UL; - struct dirent** newentries; - entriessize = sizeof(struct dirent*) * newentrieslen; - newentries = (struct dirent**) realloc(entries, entriessize); - if ( !newentries ) - { - ls_error(0, errno, "realloc"); - goto cleanup_dir; - } - entries = newentries; - entrieslen = newentrieslen; + struct record* new_records = (struct record*) + reallocarray(records, records_length, 2 * sizeof(struct record)); + if ( !new_records ) + err(1, "malloc"); + records = new_records; + records_length *= 2; } - struct dirent* copy = dirent_dup(entry); - if ( !copy ) - { - ls_error(0, errno, "malloc"); - goto cleanup_dir; - } - entries[entriesused++] = copy; + + struct record* record = &records[records_used++]; + memset(record, 0, sizeof(*record)); + if ( !(record->dirent = dirent_dup(entry)) ) + err(1, "malloc"); + if ( stat_record(dir, path, record) < 0 ) + ret = 1; } -#if defined(__sortix__) - if ( derror(dir) ) - { - ls_error(0, errno, path); - goto cleanup_dir; - } -#endif + // TODO: Stupid /dev/net/fs fake directory, so ignore ENOTDIR for now. + if ( errno != 0 && errno != ENOTDIR ) + err(1, "%s", path); - sort_dirents_dirfd = dirfd(dir); - qsort(entries, entriesused, sizeof(*entries), sort_dirents); + if ( option_recursive ) + show_recursive(dirfd(dir), path, records, records_used); + else + show(records, records_used); - for ( size_t i = 0; i < entriesused; i++ ) - { - if ( handle_entry(path, entries[i]->d_name, entries[i]->d_type) != 0 ) - goto cleanup_dir; - } - - ret = 0; - -cleanup_dir: closedir(dir); -cleanup_entries: - for ( size_t i = 0; i < entriesused; i++ ) - free(entries[i]); - free(entries); -cleanup_done: + + for ( size_t i = 0; i < records_used; i++ ) + { + free(records[i].dirent); + free(records[i].symlink_path); + } + free(records); + + return ret; +} + +static int ls_operands(char** paths, size_t paths_count) +{ + int ret = 0; + struct record* records = (struct record*) + reallocarray(NULL, paths_count, sizeof(struct record)); + if ( !records ) + err(1, "malloc"); + size_t records_used = 0; + for ( size_t i = 0; i < paths_count; i++ ) + { + struct record* record = &records[records_used]; + memset(record, 0, sizeof(*record)); + const char* path = paths[i]; + if ( fstatat(AT_FDCWD, path, &record->st, AT_SYMLINK_NOFOLLOW) < 0 ) + { + warn("%s", path); + ret = 1; + continue; + } + record->stat_attempt = 1; + if ( !(record->dirent = dirent_make(paths[i])) ) + err(1, "malloc"); + record->dirent->d_ino = record->st.st_ino; + record->dirent->d_dev = record->st.st_dev; + record->dirent->d_type = mode_to_dt(record->st.st_mode); + records_used++; + } + show_recursive(AT_FDCWD, NULL, records, records_used); + for ( size_t i = 0; i < records_used; i++ ) + free(records[i].dirent); + free(records); return ret; } @@ -481,6 +898,12 @@ int main(int argc, char** argv) { setlocale(LC_ALL, ""); + if ( isatty(1) ) + { + option_colors = true; + option_column = true; + } + const char* argv0 = argv[0]; for ( int i = 1; i < argc; i++ ) { @@ -494,16 +917,35 @@ int main(int argc, char** argv) { while ( char c = *++arg ) switch ( c ) { - case 'a': option_show_dotdot = true, option_show_dotfiles = true; break; - case 'A': option_show_dotdot = false, option_show_dotfiles = true; break; + case '1': option_column = false; break; // TODO: Semantics? + case 'a': option_all = DOTFILE_ALL; break; + case 'A': option_all = DOTFILE_ALMOST; break; + case 'c': option_time_type = TIMESTAMP_CTIME; break; + case 'C': option_column = true; break; // TODO: Disable -l and such. case 'd': option_directory = true; break; + // TODO: -f. + // TODO: -F. + case 'h': option_human_readable = true; break; + // TODO: -H. case 'i': option_inode = true; break; - case 'l': option_long_format = true; break; - case 't': option_time_modified = true; break; + // TODO: -k. + case 'l': option_long = true; break; + // TODO: -L. + // TODO: -m. + // TODO: -n. + // TODO: -p. + // TODO: -q. + case 'r': option_reverse = order_reverse; break; + case 'R': option_recursive = true; break; + // TODO: -s. + case 'S': option_sort = SORT_SIZE; break; + case 't': option_sort = SORT_TIME; break; + case 'u': option_time_type = TIMESTAMP_ATIME; break; + // TODO: -x. default: fprintf(stderr, "%s: unknown option -- '%c'\n", argv0, c); help(stderr, argv0); - exit(2); + exit(1); } } else if ( !strcmp(arg, "--version") ) @@ -511,18 +953,30 @@ int main(int argc, char** argv) else if ( !strcmp(arg, "--help") ) help(stdout, argv0), exit(0); else if ( !strcmp(arg, "--all") ) - option_show_dotdot = false, option_show_dotfiles = true; + option_all = DOTFILE_ALL; else if ( !strcmp(arg, "--almost-all") ) - option_show_dotdot = false, option_show_dotfiles = true; + option_all = DOTFILE_ALMOST; + else if ( !strcmp(arg, "--color=always") ) // TODO: Proper parsing. + option_colors = true; + else if ( !strcmp(arg, "--color=auto") ) // TODO: Proper parsing. + ; + else if ( !strcmp(arg, "--color=never") ) // TODO: Proper parsing. + option_colors=false; else if ( !strcmp(arg, "--directory") ) option_directory = true; + else if ( !strcmp(arg, "--human-readable") ) + option_human_readable = true; else if ( !strcmp(arg, "--inode") ) option_inode = true; + else if ( !strcmp(arg, "--recursive") ) + option_recursive = true; + else if ( !strcmp(arg, "--reverse") ) + option_reverse = order_reverse; else { fprintf(stderr, "%s: unrecognized option: %s\n", argv0, arg); help(stderr, argv0); - exit(2); + exit(1); } } @@ -534,58 +988,11 @@ int main(int argc, char** argv) localtime_r(¤t_time, ¤t_year_tm); current_year = current_year_tm.tm_year; - if ( isatty(1) ) - option_colors = true; - - child_pid = 0; - bool columnable = !option_long_format; - if ( columnable && isatty(1) ) - { - int pipes[2]; - if ( pipe(pipes) ) - error(1, errno, "pipe"); - child_pid = fork(); - if ( child_pid < 0 ) - error(1, errno, "fork"); - if ( child_pid ) - { - stdout_copy_fd = dup(1); - close(1); - dup(pipes[1]); - close(pipes[0]); - close(pipes[1]); - } - else - { - close(0); - dup(pipes[0]); - close(pipes[0]); - close(pipes[1]); - const char* columner = "column"; - const char* argv[] = { columner, NULL }; - execvp(columner, (char* const*) argv); - error(127, errno, "%s", columner); - } - } - - int result = 0; - - if ( 2 <= argc ) - { - // TODO: This isn't the strictly correct semantics: - if ( option_time_modified ) - qsort(argv + 1, argc - 1, sizeof(char*), argv_mtim_compare); - - for ( int i = 1; i < argc; i++ ) - if ( (result = ls(argv[i])) != 0 ) - break; - } - else - { - result = ls("."); - } - - finish_output(); - - return result; + option_multiple_operands = 3 <= argc; + char* curdir = (char*) "."; + int ret = 2 <= argc ? ls_operands(argv + 1, argc - 1) + : ls_operands(&curdir, 1); + if ( ferror(stdout) || fflush(stdout) == EOF ) + return 1; + return ret; }