File: filesizes.c 1 /* 2 The MIT License (MIT) 3 4 Copyright © 2024 pacman64 5 6 Permission is hereby granted, free of charge, to any person obtaining a copy of 7 this software and associated documentation files (the “Software”), to deal 8 in the Software without restriction, including without limitation the rights to 9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 of the Software, and to permit persons to whom the Software is furnished to do 11 so, subject to the following conditions: 12 13 The above copyright notice and this permission notice shall be included in all 14 copies or substantial portions of the Software. 15 16 THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 SOFTWARE. 23 */ 24 25 /* 26 You can build this command-line app by running 27 28 cc -Wall -s -O2 -o ./filesizes ./filesizes.c 29 */ 30 31 #include <dirent.h> 32 #include <fcntl.h> 33 #include <math.h> 34 #include <stdbool.h> 35 #include <stdint.h> 36 #include <stdio.h> 37 #include <stdlib.h> 38 #include <string.h> 39 #include <sys/stat.h> 40 41 #ifdef _WIN32 42 #include <windows.h> 43 #endif 44 45 // info is the multi-line help message 46 const char* info = "" 47 "filesizes [options...] [filenames...]\n" 48 "\n" 49 "Show the byte-counts for all the files given: output is lines, each with 2\n" 50 "tab-separated items, the name and the byte-count. First line has the column\n" 51 "names.\n" 52 "\n" 53 "\n" 54 "Options\n" 55 "\n" 56 " -h, --h show this help message\n" 57 " -help, --help aliases for option -h\n" 58 ""; 59 60 // handle_stdin counts the standard-input's bytes 61 void handle_stdin() { 62 unsigned char buf[32 * 1024]; 63 uint64_t bytes = 0; 64 65 putc('-', stdout); 66 67 while (!feof(stdin)) { 68 size_t n = fread(&buf, sizeof(buf[0]), sizeof(buf), stdin); 69 if (n < 1) { 70 // assume input is over when no bytes were read 71 break; 72 } 73 bytes += n; 74 } 75 76 printf("\t%lu\n", (long unsigned int)bytes); 77 } 78 79 bool handle_file(const char* path); 80 81 // fail_fullpath_alloc gives up by quitting, and is used in func handle_folder 82 void fail_fullpath_alloc(DIR* entries) { 83 char* msg = "\x1b[31mcan't get memory for the full file names\x1b[0m\n"; 84 closedir(entries); 85 fflush(stdout); 86 fprintf(stderr, msg); 87 exit(1); 88 } 89 90 // handle_folder handles folder entries for func handle_files: these 2 funcs 91 // may involve mutual recursion, when nested files/folders are involved 92 bool handle_folder(const char* path) { 93 DIR* entries = opendir(path); 94 95 if (entries == NULL) { 96 return false; 97 } 98 99 size_t path_len = strlen(path); 100 // find the slash's position, whether included in the folder path or not 101 bool trailing_slash = path[path_len - 1] == '/'; 102 size_t slash = trailing_slash ? path_len - 1 : path_len; 103 // ensure starting capacity can fit a slash either way 104 size_t cap = slash + 2; 105 106 // fullpath is a reusable string-area to keep appending the final parts 107 // of full pathnames for all the folder's entries 108 char* fullpath = malloc(cap); 109 110 // if allocation fails, simply give and quit the app with a message 111 if (fullpath == NULL) { 112 fail_fullpath_alloc(entries); 113 return false; 114 } 115 116 // start full-path string with the folder's path 117 strcpy(fullpath, path); 118 119 // ensure a slash is between the folder's path and its entries' names 120 if (fullpath[slash] != '/') { 121 fullpath[slash + 0] = '/'; 122 fullpath[slash + 1] = 0; 123 } 124 125 // remember where to start appending entry names in the full-path string 126 size_t start = slash + 1; 127 128 while (!feof(stdout)) { 129 struct dirent* item = readdir(entries); 130 if (item == NULL) { 131 break; 132 } 133 134 const char* name = item->d_name; 135 136 // ignore entries `.` and `..` 137 if (name[0] == '.') { 138 if ((name[1] == 0) || (name[1] == '.' && name[2] == 0)) { 139 continue; 140 } 141 } 142 143 // ensure capacity of the full-path is enough for this entry's name 144 size_t extra = strlen(name); 145 if (start + extra >= cap) { 146 char* old = fullpath; 147 cap = start + extra + 2; 148 fullpath = realloc(fullpath, cap); 149 150 // if allocation fails, simply give and quit the app with a message 151 if (fullpath == NULL) { 152 free(old); 153 fail_fullpath_alloc(entries); 154 return false; 155 } 156 } 157 158 // complete full-path using the name of the current entry 159 strcpy(fullpath + slash + 1, name); 160 161 // handle entry, possibly recursively in case of another folder 162 if (!handle_file(fullpath)) { 163 free(fullpath); 164 return false; 165 } 166 } 167 168 closedir(entries); 169 free(fullpath); 170 return true; 171 } 172 173 // handle_file handles data from the filename given; returns false only when 174 // the file can't be queried for its size, likely because it doesn't exist 175 bool handle_file(const char* path) { 176 struct stat st; 177 if (stat(path, &st) != 0) { 178 fprintf(stderr, "\x1b[31mcan't open file named %s\x1b[0m\n", path); 179 return false; 180 } 181 182 if (!S_ISDIR(st.st_mode)) { 183 printf("%s\t%ld\n", path, st.st_size); 184 return true; 185 } 186 187 return handle_folder(path); 188 } 189 190 // is_help_option simplifies control-flow for func run 191 bool is_help_option(char* s) { 192 return (s[0] == '-') && (s[1] != 0) && ( 193 strcmp(s, "-h") == 0 || strcmp(s, "-help") == 0 || 194 strcmp(s, "--h") == 0 || strcmp(s, "--help") == 0 195 ); 196 } 197 198 // run returns the number of errors 199 int run(int argc, char** argv) { 200 size_t errors = 0; 201 202 // handle special cmd-line options 203 for (size_t i = 1; i < argc; i++) { 204 if (is_help_option(argv[i])) { 205 // help option is handled right away, also quitting the app 206 fprintf(stderr, "%s", info); 207 return 0; 208 } 209 } 210 211 puts("file\tbytes"); 212 213 for (size_t i = 1; i < argc && !feof(stdout); i++) { 214 // a `-` filename stands for the standard input 215 if (argv[i][0] == '-' && argv[i][1] == 0) { 216 handle_stdin(); 217 continue; 218 } 219 220 if (!handle_file(argv[i])) { 221 errors++; 222 } 223 } 224 225 // no filenames means use stdin as the only input 226 if (argc < 2) { 227 handle_stdin(); 228 } 229 230 return errors; 231 } 232 233 int main(int argc, char** argv) { 234 #ifdef _WIN32 235 setmode(fileno(stdin), O_BINARY); 236 // ensure output lines end in LF instead of CRLF on windows 237 setmode(fileno(stdout), O_BINARY); 238 setmode(fileno(stderr), O_BINARY); 239 #endif 240 241 return run(argc, argv) == 0 ? 0 : 1; 242 }