mirror of
https://gitlab.com/sortix/sortix.git
synced 2023-02-13 20:55:38 -05:00
1002 lines
26 KiB
C
1002 lines
26 KiB
C
/*
|
|
* Copyright (c) 2011, 2012, 2013, 2014, 2015 Jonas 'Sortie' Termansen.
|
|
*
|
|
* Permission to use, copy, modify, and distribute this software for any
|
|
* purpose with or without fee is hereby granted, provided that the above
|
|
* copyright notice and this permission notice appear in all copies.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*
|
|
* ls.c
|
|
* Lists directory contents.
|
|
*/
|
|
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/wait.h>
|
|
|
|
#include <assert.h>
|
|
#include <dirent.h>
|
|
#include <err.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <grp.h>
|
|
#include <libgen.h>
|
|
#include <locale.h>
|
|
#include <pwd.h>
|
|
#include <stdarg.h>
|
|
#include <stdbool.h>
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <termios.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
#include <wchar.h>
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
bool no_recurse;
|
|
};
|
|
|
|
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)
|
|
{
|
|
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, size);
|
|
return copy;
|
|
}
|
|
|
|
static struct dirent* dirent_make(const char* name)
|
|
{
|
|
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;
|
|
}
|
|
|
|
static struct timespec record_timestamp(struct record* record)
|
|
{
|
|
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();
|
|
}
|
|
|
|
static int sort_records(const void* a_ptr, const void* b_ptr)
|
|
{
|
|
struct record* a = (struct record*) a_ptr;
|
|
struct record* b = (struct record*) b_ptr;
|
|
if ( option_sort == SORT_SIZE )
|
|
{
|
|
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);
|
|
}
|
|
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));
|
|
}
|
|
|
|
static int sort_files_then_dirs(const void* a_ptr, const void* b_ptr)
|
|
{
|
|
struct record* a = (struct record*) a_ptr;
|
|
struct record* b = (struct record*) b_ptr;
|
|
if ( !option_directory )
|
|
{
|
|
bool a_as_dir = S_ISDIR(a->st.st_mode) && !a->no_recurse;
|
|
bool b_as_dir = S_ISDIR(b->st.st_mode) && !b->no_recurse;
|
|
if ( a_as_dir && !b_as_dir )
|
|
return 1;
|
|
if ( !a_as_dir && b_as_dir )
|
|
return -1;
|
|
}
|
|
return sort_records(a_ptr, b_ptr);
|
|
}
|
|
|
|
static unsigned char mode_to_dt(mode_t mode)
|
|
{
|
|
if ( S_ISBLK(mode) )
|
|
return DT_BLK;
|
|
if ( S_ISCHR(mode) )
|
|
return DT_CHR;
|
|
if ( S_ISDIR(mode) )
|
|
return DT_DIR;
|
|
if ( S_ISFIFO(mode) )
|
|
return DT_FIFO;
|
|
if ( S_ISLNK(mode) )
|
|
return DT_LNK;
|
|
if ( S_ISREG(mode) )
|
|
return DT_REG;
|
|
if ( S_ISSOCK(mode) )
|
|
return DT_SOCK;
|
|
return DT_UNKNOWN;
|
|
}
|
|
|
|
static void color(const char** pre,
|
|
const char** post,
|
|
struct stat* st,
|
|
bool failure)
|
|
{
|
|
*pre = "";
|
|
*post = "";
|
|
|
|
if ( !option_colors )
|
|
return;
|
|
|
|
if ( failure )
|
|
{
|
|
*pre = "\e[1;31m";
|
|
*post = "\e[m";
|
|
return;
|
|
}
|
|
|
|
*post = "\e[m";
|
|
switch ( mode_to_dt(st->st_mode) )
|
|
{
|
|
case DT_UNKNOWN: *pre = "\e[1;31m"; break;
|
|
case DT_BLK: *pre = "\e[1;33m"; break;
|
|
case DT_CHR: *pre = "\e[1;33m"; break;
|
|
case DT_DIR: *pre = "\e[1;34m"; break;
|
|
case DT_FIFO: *pre = "\e[33m"; break;
|
|
case DT_LNK: *pre = "\e[1;36m"; break;
|
|
case DT_SOCK: *pre = "\e[1;35m"; break;
|
|
case DT_REG:
|
|
if ( st->st_mode & 0111 )
|
|
*pre = "\e[1;32m";
|
|
else
|
|
*post = "";
|
|
break;
|
|
default:
|
|
*post = "";
|
|
}
|
|
}
|
|
|
|
static void color_record(const char** pre,
|
|
const char** post,
|
|
struct record* record)
|
|
{
|
|
bool failure = record->stat_attempt < 0 ||
|
|
(record->dirent->d_type == DT_LNK && !record->symlink_path);
|
|
color(pre, post, &record->st, failure);
|
|
}
|
|
|
|
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 )
|
|
{
|
|
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;
|
|
}
|
|
|
|
if ( !count )
|
|
return;
|
|
|
|
// TODO: -x support.
|
|
struct column_size* column_sizes;
|
|
size_t columns = 0;
|
|
size_t rows = 0;
|
|
while ( true )
|
|
{
|
|
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++ )
|
|
{
|
|
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++ )
|
|
{
|
|
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 ( 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
|
|
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);
|
|
}
|
|
}
|
|
|
|
free(column_sizes);
|
|
}
|
|
|
|
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;
|
|
|
|
for ( size_t i = 0; i < count; i++ )
|
|
{
|
|
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;
|
|
}
|
|
|
|
// 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;
|
|
case DT_DIR: perms[0] = 'd'; break;
|
|
case DT_FIFO: perms[0] = 'p'; break;
|
|
case DT_LNK: perms[0] = 'l'; break;
|
|
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');
|
|
}
|
|
}
|
|
|
|
static void show(struct record* records, size_t count)
|
|
{
|
|
qsort(records, count, sizeof(*records), sort_records);
|
|
if ( option_long )
|
|
show_long(records, count);
|
|
else if ( option_column )
|
|
show_column(records, count);
|
|
else
|
|
show_simple(records, count);
|
|
}
|
|
|
|
static int ls_directory(int parentfd, const char* relpath, const char* path);
|
|
|
|
static int show_recursive(int fd, const char* path,
|
|
struct record* records, size_t count)
|
|
{
|
|
// 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 )
|
|
{
|
|
for ( nondir_count = 0; nondir_count < count; nondir_count++ )
|
|
{
|
|
if ( records[nondir_count].no_recurse )
|
|
continue;
|
|
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;
|
|
}
|
|
|
|
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 )
|
|
{
|
|
warn("fdopendir: %s", path);
|
|
close(fd);
|
|
return 1;
|
|
}
|
|
|
|
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)) )
|
|
{
|
|
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 )
|
|
{
|
|
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 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) )
|
|
ret = 1;
|
|
record->no_recurse = isdotdot;
|
|
}
|
|
|
|
// TODO: Stupid /dev/net/fs fake directory, so ignore ENOTDIR for now.
|
|
if ( errno != 0 && errno != ENOTDIR )
|
|
err(1, "%s", path);
|
|
|
|
if ( option_recursive )
|
|
show_recursive(dirfd(dir), path, records, records_used);
|
|
else
|
|
show(records, records_used);
|
|
|
|
closedir(dir);
|
|
|
|
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;
|
|
}
|
|
|
|
static void compact_arguments(int* argc, char*** argv)
|
|
{
|
|
for ( int i = 0; i < *argc; i++ )
|
|
{
|
|
while ( i < *argc && !(*argv)[i] )
|
|
{
|
|
for ( int n = i; n < *argc; n++ )
|
|
(*argv)[n] = (*argv)[n+1];
|
|
(*argc)--;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void version(FILE* fp, const char* argv0)
|
|
{
|
|
fprintf(fp, "%s (Sortix) %s\n", argv0, VERSIONSTR);
|
|
}
|
|
|
|
static void help(FILE* fp, const char* argv0)
|
|
{
|
|
fprintf(fp, "Usage: %s [OPTION]... [FILE]...\n", argv0);
|
|
}
|
|
|
|
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++ )
|
|
{
|
|
const char* arg = argv[i];
|
|
if ( arg[0] != '-' || !arg[1] )
|
|
continue;
|
|
argv[i] = NULL;
|
|
if ( !strcmp(arg, "--") )
|
|
break;
|
|
if ( arg[1] != '-' )
|
|
{
|
|
char c;
|
|
while ( (c = *++arg) ) switch ( c )
|
|
{
|
|
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;
|
|
// 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(1);
|
|
}
|
|
}
|
|
else if ( !strcmp(arg, "--version") )
|
|
version(stdout, argv0), exit(0);
|
|
else if ( !strcmp(arg, "--help") )
|
|
help(stdout, argv0), exit(0);
|
|
else if ( !strcmp(arg, "--all") )
|
|
option_all = DOTFILE_ALL;
|
|
else if ( !strcmp(arg, "--almost-all") )
|
|
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(1);
|
|
}
|
|
}
|
|
|
|
compact_arguments(&argc, &argv);
|
|
|
|
time_t current_time;
|
|
struct tm current_year_tm;
|
|
time(¤t_time);
|
|
localtime_r(¤t_time, ¤t_year_tm);
|
|
current_year = current_year_tm.tm_year;
|
|
|
|
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;
|
|
}
|