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; }