/** * file: ls.c * author: Tyler Murphy */ #include "command.h" #include "lslib.h" #include #include #include #include #include #include #include #include #include #include #include /* quirky colors */ #define FILE_COLOR ANSCII BLACK COLOR #define DIR_COLOR ANSCII BOLD NEXT NORMAL BLUE COLOR #define DIR_COLOR_EXEC ANSCII BACKGROUND GREEN NEXT NORMAL BLACK COLOR #define LINK_COLOR ANSCII BOLD NEXT NORMAL TURQUOISE COLOR #define SET_UID_COLOR ANSCII BACKGROUND RED NEXT NORMAL WHITE COLOR #define SET_GID_COLOR ANSCII BACKGROUND YELLOW NEXT NORMAL BLACK COLOR #define EXEC_COLOR ANSCII BOLD NEXT NORMAL GREEN COLOR #define BLK_COLOR ANSCII BOLD NEXT NORMAL YELLOW COLOR #define SOCK_COLOR ANSCII BOLD NEXT NORMAL MAGENTA COLOR /** * Describes when to do something */ enum When { YES, /* always */ NO, /* never */ AUTO /* when not tty */ }; /** * Flags that are to be used with ls */ static struct { bool hidden; /* -a if to show hidden files */ bool hide_dot; /* -A if to hide dot dirs */ bool more_info; /* -l if to print info as a long list */ bool one_column; /* -1 if to print files as a single column */ bool recurse; /* -R if to recurse directorys */ bool octets; /* -o if to print mode as its octet instead of romanized */ enum When colored; /* --color=never/no/yes/always/auto */ } flags; /** * Keeps track of the files data */ struct FileInfo { struct passwd* usr; /* the files owner user */ struct group* grp; /* the files owner group */ char* name; /* the file name */ char date[13]; /* the files romanized date */ char mode[11]; /* the files mode romanized or octets */ char size[5]; /* the human readable file size */ int links; /* the amount of inode links */ int bytes; /* the amount of bytes in the file*/ bool set_uid; /* if the file is set uid */ bool set_gid; /* if the file is set gid */ bool exec; /* if the binary is executable by the user running ls */ unsigned char type; /* the type of the file */ }; /** * Keeps track of the biggest data to make the output data look pretty */ struct FileListInfo { int max_link; int max_usr; int max_grp; int max_size; int max_name; int total_len; int total_size; }; /** * Gets a directory and fails if it cannot be opened * @param path the path to the directory * @returns the directory if found or NULL if not */ static DIR* get_directory(char* path) { DIR* d = opendir(path); if (d == NULL) { if (errno == ENOTDIR) { error_s("`%s` is a a file", path); } else { error_s("failed to open directory '%s'", path); } } return d; } /** * Takes in a file and parses its mode to be humand readable * @param info the file infomation to store the mode into * @param mode the octet form of the mode * @param type the type of the file */ static void parse_file_mode(struct FileInfo* info, mode_t mode, int type) { switch (type) { case DT_BLK: info->mode[0] = 'b'; /* block device */ break; case DT_CHR: info->mode[0] = 'c'; /* something i dunno */ break; case DT_DIR: info->mode[0] = 'd'; /* directory */ break; case DT_FIFO: info->mode[0] = 'f'; /* fifo */ break; case DT_LNK: info->mode[0] = 'l'; /* symlink */ break; case DT_SOCK: info->mode[0] = 's'; /* socket */ break; case DT_UNKNOWN: info->mode[0] = 'u'; /* unkown (eye emoji) */ break; case DT_WHT: info->mode[0] = 'w'; /* tf, wheat? idk */ break; default: info->mode[0] = '-'; /* regular file */ break; } /* parse info for the user bits */ info->mode[1] = (mode & S_IRUSR) ? 'r' : '-'; info->mode[2] = (mode & S_IWUSR) ? 'w' : '-'; if (mode & S_IXUSR && mode & S_ISUID) { info->mode[3] = 's'; } else if (mode & S_ISUID) { info->mode[3] = 'S'; } else if (mode & S_IXUSR) { info->mode[3] = 'x'; } else { info->mode[3] = '-'; } /* parse info for the group bits */ info->mode[4] = (mode & S_IRGRP) ? 'r' : '-'; info->mode[5] = (mode & S_IWGRP) ? 'w' : '-'; if (mode & S_IXGRP && mode & S_ISGID) { info->mode[6] = 's'; } else if (mode & S_ISGID) { info->mode[6] = 'S'; } else if (mode & S_IXGRP) { info->mode[6] = 'x'; } else { info->mode[6] = '-'; } /* parse info for the other bits */ info->mode[7] = (mode & S_IROTH) ? 'r' : '-'; info->mode[8] = (mode & S_IWOTH) ? 'w' : '-'; if (mode & S_IXOTH && mode & S_ISVTX) { info->mode[9] = 't'; } else if (mode & S_ISVTX) { info->mode[9] = 'T'; } else if (mode & S_IXOTH) { info->mode[9] = 'x'; } else { info->mode[9] = '-'; } /* make a null terminated string */ info->mode[10] = '\0'; } /** * Gets information about a given file_name in a directory and * stores it into a fileinfo struct * @param file_name the name of the file in the directory currently * stored in the path buffer * @param info the file info struct to store the data into * @returns if successfull in reading the file */ static bool get_file_info(const char* file_name, struct FileInfo* info) { uid_t uid; gid_t gid; int save, ty; struct stat s; size_t file_len; uid = getuid(); gid = getgid(); /* default to all false */ memset(&s, 0, sizeof(struct stat)); /* push the file to the path buffer to stat */ save = push_path_buffer(file_name); /* get file stats */ if (lstat(get_path_buffer(), &s) < 0) { error_s("failed to read file '%s'", get_path_buffer()); pop_path_buffer(save); return false; } /* get file type */ ty = (s.st_mode & S_IFMT) >> 12; /* parse long list mode as octets or human readable */ if (flags.octets) { info->mode[0] = '0' + (s.st_mode / (8 * 8 * 8) % 8); info->mode[1] = '0' + (s.st_mode / (8 * 8 * 1) % 8); info->mode[2] = '0' + (s.st_mode / (8 * 1 * 1) % 8); info->mode[3] = '0' + (s.st_mode / (1 * 1 * 1) % 8); info->mode[4] = '\0'; } else { parse_file_mode(info, s.st_mode, ty); } /* check if the file is executable by the user */ if ( (s.st_mode & S_IXUSR && s.st_uid == uid) || (s.st_mode & S_IXGRP && s.st_gid == gid) || s.st_mode & S_IXOTH ) { info->exec = true; } else { info->exec = false; } /* get setuid and setgid bits */ info->set_uid = s.st_mode & S_ISUID; info->set_gid = s.st_mode & S_ISGID; /* get user information */ info->usr = getpwuid(s.st_uid); if (info->usr == NULL) { error_s("failed to get user id: `%s`", get_path_buffer()); pop_path_buffer(save); return false; } /* get group information */ info->grp = getgrgid(s.st_gid); if (info->grp == NULL) { error_s("failed to get user id: `%s`", get_path_buffer()); pop_path_buffer(save); return false; } /* update inode links and file type */ info->links = s.st_nlink; info->type = ty; /* copy the file name into the info */ file_len = strlen(file_name) + 1; info->name = xalloc(file_len); memcpy(info->name, file_name, file_len); /* get the date and time for the file */ print_file_size(s.st_size, info->size); print_date_time(s.st_mtim.tv_sec + s.st_mtim.tv_nsec / 1000000000, info->date); /* convert the bytes into human readable */ info->bytes = (s.st_size + s.st_blksize - 1) / s.st_blksize; /* cleanup */ pop_path_buffer(save); return true; } /** * Gets the file color from its mode * @pram info the file info to get the color from * @return the anscii escape code for the file color */ static char* get_file_color(struct FileInfo* info) { char* color; if (info->type == DT_DIR) { if (info->mode[8] == 'w') { color = DIR_COLOR_EXEC; /* directory is green if anyone can write it */ } else { color = DIR_COLOR; /* other whise as the french say it, bleu */ } } else if (info->type == DT_LNK) { color = LINK_COLOR; /* symlink color */ } else if (info->type == DT_SOCK) { color = SOCK_COLOR; /* sockt color */ } else if ( info->type == DT_CHR || info->type == DT_BLK ) { color = BLK_COLOR; /* the weird ones are yellow, cool */ } else { if (info->set_uid) { color = SET_UID_COLOR; /* hightlighted red if setuid */ } else if (info->set_gid) { color = SET_GID_COLOR; /* highlighted yellow if setgid and not setuid */ } else if (info->exec) { color = EXEC_COLOR; /* green if executable */ } else { color = FILE_COLOR; /* white if basic and boring */ } } return color; } /** * Lists a file in long info mode * @param info the global info for a directory * @param finfo the file specific info */ static void list_file_long(struct FileListInfo* info, struct FileInfo* finfo) { char* color; color = get_file_color(finfo); printf("%s %*d %*s %*s %*s %s %s%s%s", /* print data */ finfo->mode, info->max_link, finfo->links, info->max_usr, finfo->usr->pw_name, info->max_grp, finfo->grp->gr_name, info->max_size, finfo->size, finfo->date, flags.colored != NO ? color : "", /* if not colored dont color */ finfo->name, flags.colored != NO ? "\x1b[0m" : "" ); /* if it is a symlink, add the path where it points to */ if (finfo->type == DT_LNK) { int save = push_path_buffer(finfo->name); /* get the link */ char lnk[PATH_MAX]; ssize_t n; if ((n = readlink(get_path_buffer(), lnk, PATH_MAX)) != -1) { printf(" -> %.*s\n", (int)n, lnk); /* add if successfull */ } else { putchar('\n'); /* dont if not */ } pop_path_buffer(save); /* cleanup */ } else { putchar('\n'); /* if not link put newline now */ } } /** * Print one file in column mode * @param finfo the file info to be printed */ static void list_file_column(struct FileInfo* finfo) { char* color; color = get_file_color(finfo); printf("%s%s%s\n", /* print the data */ flags.colored != NO ? color : "", finfo->name, /* if not colored dont color */ flags.colored != NO ? "\x1b[0m" : "" ); } /** * List a file normally (without -l or -1) * @param info the global folder information * @param finfo the file specific info * @param tty_width the width of the tty * @param index, the index in the direcory that has been printed * @param column_width the width of the column * @param row_count the count of rows */ static void list_file_normal( struct FileListInfo* info, struct FileInfo* finfo, int tty_width, int index, int column_width, int row_count ) { char* color; color = get_file_color(finfo); /* get file color */ if (info->total_len > tty_width) { /* snap to column if the files span multiple lines */ if (index != 0 && index % row_count == 0) putchar('\n'); /* if at the end of the column put a new line */ printf("%s%*s%s", flags.colored != NO ? color : "", -column_width, finfo->name, flags.colored != NO ? "\x1b[0m" : ""); /* if not colored dont color */ } else { /* otherwise just print next to eachother seperated by two spaces */ printf("%s%s%s ", flags.colored != NO ? color : "", finfo->name, flags.colored != NO ? "\x1b[0m" : ""); } } /** * Get the width of the tty, set to 0 if not a tty * @param width the int pointer to store the width in */ static void get_window_size(int* width) { struct winsize w; ioctl(STDOUT_FILENO, TIOCGWINSZ, &w); if (!isatty(1)) { *width = 0; } else { *width = w.ws_col; } } /** * List a set of files * @param files a array of file info structs containing file data * @param file_len the amount of files * @param info the global folder infomation */ static void list_files(struct FileInfo* files, int file_len, struct FileListInfo info) { /* pre define variables to make gcc happy */ int column_width = 0, row_count = 0, i = 0, width = 0; get_window_size(&width); /* get screen width */ /* if your not a tty, set to column mode and disable color if not forced */ if (width > 0) { column_width = info.max_name + 1; row_count = width / column_width; } else { flags.one_column = true; if (flags.colored == AUTO) flags.colored = NO; } /* if set to long list, display folder size */ if (flags.more_info) { char total[13]; print_file_size(info.total_size, total); printf("total %s\n", total); } /* list files base on list mode */ for (i = 0; i < file_len; i++) { struct FileInfo finfo = files[i]; if (flags.more_info) { /* -l long list */ list_file_long(&info, &finfo); } else if (flags.one_column) { /* -1 column list */ list_file_column(&finfo); } else { /* normal default listing */ list_file_normal(&info, &finfo, width, i, column_width, row_count); } free(finfo.name); /* cleanup */ } if (!flags.more_info) printf("\n"); /* only long long list prints a new line at the end */ } /** * Get the number of places a number fills * @param n the number to check * @returns the num of places the number filles */ static int num_places (int n) { int r = 1; if (n < 0) n = (n == INT_MIN) ? INT_MAX: -n; while (n > 9) { /* keep going for each place */ n /= 10; r++; } return r; } /** * Read and push a file info the given array * @param file a pointer to the list of files * @param info the global folder info to update as read * @param size the amount of files currently in the array * @param capacity the capacity of the array * @param file_path the path of the file */ static void push_file( struct FileInfo** files, struct FileListInfo* info, int* size, int* capacity, const char* file_path ) { /* allocate variables on the stack */ struct FileInfo finfo; int user_len, group_len, name_len, size_len, link_len; /* if you fail to get the file info, abort */ if (!get_file_info(file_path, &finfo)) return; /* if the array is full realloc to make it bigger */ if (*size == *capacity) { *capacity *= 2; *files = xrealloc(*files, sizeof(struct FileInfo) * *capacity); } /* update user name length if its larger than global */ user_len = strlen(finfo.usr->pw_name); if (user_len > info->max_usr) info->max_usr = user_len; /* update group name length if its larger than global */ group_len = strlen(finfo.grp->gr_name); if (group_len > info->max_grp) info->max_grp = group_len; /* update file name length if its larger than global */ name_len = strlen(file_path); if (name_len > info->max_name) info->max_name = name_len; /* update file size human readable length if its larger than global */ size_len = strlen(finfo.size); if (size_len > info->max_size) info->max_size = size_len; /* update inode link place count length if its larger than global */ link_len = num_places(finfo.links); if (link_len > info->max_link) info->max_link = link_len; /* update final info */ info->total_len += name_len + 2; info->total_size += finfo.bytes; /* insert file data into array */ (*files)[*size] = finfo; (*size)++; /* increment size counter */ } /** * When recursing, read all files and folders, recurses each folder, and prints each file * @param dir_name the dir_name relative to the current path buffer */ static void recurse_directory(char* dir_name) { /* allocate variables on the stack */ DIR* d; int capacity, size, save; struct dirent* file; struct FileInfo* files; struct FileListInfo info; /* push the dir name to the path buffer */ save = push_path_buffer(dir_name); /* if failed to open dir abort */ d = get_directory(get_path_buffer()); if (d == NULL) { return; } /* allocate initial array*/ capacity = 8; size = 0; files = xalloc(sizeof(struct FileInfo) * capacity); memset(&info, 0, sizeof(struct FileListInfo)); /* zero out default directory globals */ /* for each file read, recurse if directory, read file if file */ while((file = readdir(d)) != NULL) { if (!flags.hidden && prefix(".", file->d_name)) continue; /* if not set to show hidden, skip */ if (flags.hide_dot && is_dot_dir(file->d_name)) continue; /* if its set to hide dot skip */ if (file->d_type == DT_DIR && !is_dot_dir(file->d_name)) { /* dont recurse dot files or scary infinite loop */ recurse_directory(file->d_name); /* recurse if directory */ } else { push_file(&files, &info, &size, &capacity, file->d_name); /* read file if a file */ } } /* print dir name */ if (flags.colored == NO) { printf("\n%s:\n", get_path_buffer()); } else { printf("\n%s%s:%s\n", DIR_COLOR, get_path_buffer(), FILE_COLOR); } /* list files */ list_files(files, size, info); free(files); /* clean up */ closedir(d); pop_path_buffer(save); } /** * List directory non recursivly or recursivly * @param path the name of the directory to search */ static void list_directory(char* path) { /* allocate variables on the stack */ DIR* d; int capacity, size, save; struct FileInfo* files; struct FileListInfo info; struct dirent* file; /* if set to recurse, run the recurse function */ if (flags.recurse) { recurse_directory(path); return; } /* if failed to open directory, abort */ d = get_directory(path); if (d == NULL) return; /* push folder to path buffer */ save = push_path_buffer(path); /* set array defaults */ capacity = 8; size = 0; /* allocate file array */ files = xalloc(sizeof(struct FileInfo) * capacity); memset(&info, 0, sizeof(struct FileListInfo)); /* zero out directory globals */ /* add each file regardless if its a directory or file since not recursing */ while ((file = readdir(d)) != NULL) { if (!flags.hidden && prefix(".", file->d_name)) continue; /* if not set to show hidden ship */ if (flags.hide_dot && is_dot_dir(file->d_name)) continue; /* if set to hide dot skip */ push_file(&files, &info, &size, &capacity, file->d_name); /* add file to array */ } /* only list files if there are any to list */ if (size > 0) list_files(files, size, info); /* cleanup */ free(files); closedir(d); pop_path_buffer(save); } /* return if a path is a directory */ static bool is_dir(const char* path) { struct stat s; if (stat(path, &s) < 0) return false; return S_ISDIR(s.st_mode); } /** * Goes though each argument and prints the argument if it is a file * @param start the argument to start at * @param argc the argument count * @param argv, the argument data */ static void list_file_args(int start, int argc, char** argv) { /* allocate variables on the stack */ int capacity, size, i; struct FileInfo* files; struct FileListInfo info; /* array defaults */ capacity = 8; size = 0; files = xalloc(sizeof(struct FileInfo) * capacity); /* allocate array */ memset(&info, 0, sizeof(struct FileListInfo)); /* zero out globals */ for (i = start; i < argc; i++) { if (is_dir(argv[i])) continue; /* if the argument is a dir skip */ push_file(&files, &info, &size, &capacity, argv[i]); } /* print file args */ if (size > 0) list_files(files, size, info); /* clean up */ free(files); } /** * Prints help message for ls */ static void help(void) { printf("Usage: ls [FILE]...\n\n"); printf("List directory contents\n\n"); printf("\t-1\tOne column output\n"); printf("\t-l\tLong format\n"); printf("\t-o\tList raw octets instead of romanized file mode\n"); printf("\t-a\tInclude names starting with .\n"); printf("\t-A\tLike -a but without . and ..\n"); printf("\t-R\tRecurse\n"); } /** * Takes in each argument that has a single - and parses it * @param c the character after the - * @param next the next argument in argv that hasnt been parsed * @reutrn if the next arg was used or if the arg was invalid */ static int short_arg(char c, char* next) { UNUSED(next); /* next arg unused */ switch (c) { case 'R': flags.recurse = true; break; case '1': flags.one_column = true; break; case 'A': flags.hide_dot = true; flags.hidden = true; break; case 'a': flags.hidden = true; break; case 'l': flags.more_info = true; break; case 'o': flags.octets = true; break; default: return ARG_INVALID; } return ARG_UNUSED; } /** * Takes in each long argument (--) * @param cur the current argument * @param next the next argument in argv that hasnt been parsed * @param if the next arg was used or if the arg was invalid */ static int long_arg(char* cur, char* next) { UNUSED(next); /* next arg unused */ if (prefix("--color=", cur)) { /* parse --color argument */ char* arg = cur + 8; if (streql("yes", arg) || streql("always", arg)) { flags.colored = YES; } else if (streql("auto", arg)) { flags.colored = AUTO; } else if (streql("no", arg) || streql("never", arg)) { flags.colored = NO; } else { error("invalid color options: %s", arg); } } else { return ARG_IGNORE; /* stop reading arguments */ } return ARG_UNUSED; /* return */ } /** * Lists a directorys contents */ COMMAND(ls_main) { int start, i; bool titled; /* initalize flag defaults */ flags.hidden = false; flags.more_info = false; flags.hide_dot = false; flags.one_column = false; flags.recurse = false; flags.colored = NO; flags.octets = false; /* parse given arguments */ start = parse_args(argc, argv, help, short_arg, long_arg); /* if not arguments are given list current directory */ if (argc - start == 0) { list_directory("."); return EXIT_SUCCESS; } /* list all arguments that are files */ list_file_args(start, argc, argv); /* list each directory in the arguments */ titled = argc - start > 1; for (i = start; i < argc; i++) { if (!is_dir(argv[i])) continue; /* if not directory skip */ if (titled && !flags.recurse) { /* list title of title is forced and not set to recurse*/ if (flags.colored != NO) { printf("\n%s%s:%s\n", DIR_COLOR, argv[i], FILE_COLOR); } else { printf("\n%s:\n", argv[i]); } } list_directory(argv[i]); /* parse and list directory */ } return EXIT_SUCCESS; /* return */ }