File: filesizes.c 1 /* 2 The MIT License (MIT) 3 4 Copyright © 2020-2025 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 -O3 -flto -o ./filesizes ./filesizes.c 29 */ 30 31 #include <dirent.h> 32 #include <stdbool.h> 33 #include <stdint.h> 34 #include <stdio.h> 35 #include <stdlib.h> 36 #include <string.h> 37 #include <sys/stat.h> 38 39 #ifdef _WIN32 40 #include <fcntl.h> 41 #include <windows.h> 42 #endif 43 44 #ifdef RED_ERRORS 45 #define ERROR_STYLE "\x1b[38;2;204;0;0m" 46 #ifdef __APPLE__ 47 #define ERROR_STYLE "\x1b[31m" 48 #endif 49 #define RESET_STYLE "\x1b[0m" 50 #else 51 #define ERROR_STYLE 52 #define RESET_STYLE 53 #endif 54 55 #define ERROR_LINE(MSG) (ERROR_STYLE MSG RESET_STYLE "\n") 56 57 const char* info = "" 58 "filesizes [options...] [filenames...]\n" 59 "\n" 60 "Show the byte-counts for all the files given: output is lines, each with 2\n" 61 "tab-separated items, the name and the byte-count. First line has the column\n" 62 "names.\n" 63 "\n" 64 "\n" 65 "Options\n" 66 "\n" 67 " -h, --h show this help message\n" 68 " -help, --help aliases for option -h\n" 69 ""; 70 71 // handle_stdin counts the standard-input's bytes 72 void handle_stdin() { 73 unsigned char buf[32 * 1024]; 74 uint64_t bytes = 0; 75 76 fputc('-', stdout); 77 78 while (!feof(stdin)) { 79 size_t n = fread(&buf, sizeof(buf[0]), sizeof(buf), stdin); 80 if (n < 1) { 81 // assume input is over when no bytes were read 82 break; 83 } 84 bytes += n; 85 } 86 87 printf("\t%lu\n", (long unsigned int)bytes); 88 } 89 90 bool handle_file(const char* path); 91 92 // fail_fullpath_alloc gives up by quitting, and is used in func handle_folder 93 void fail_fullpath_alloc(DIR* entries) { 94 closedir(entries); 95 fprintf(stderr, ERROR_LINE("out of memory")); 96 exit(1); 97 } 98 99 // handle_folder handles folder entries for func handle_files: these 2 funcs 100 // may involve mutual recursion, when nested files/folders are involved 101 bool handle_folder(const char* path) { 102 DIR* entries = opendir(path); 103 104 if (entries == NULL) { 105 return false; 106 } 107 108 size_t path_len = strlen(path); 109 // find the slash's position, whether included in the folder path or not 110 bool trailing_slash = path[path_len - 1] == '/'; 111 size_t slash = trailing_slash ? path_len - 1 : path_len; 112 // ensure starting capacity can fit a slash either way 113 size_t cap = slash + 2; 114 115 // fullpath is a reusable string-area to keep appending the final parts 116 // of full pathnames for all the folder's entries 117 char* fullpath = malloc(cap); 118 119 // if allocation fails, simply give and quit the app with a message 120 if (fullpath == NULL) { 121 fail_fullpath_alloc(entries); 122 return false; 123 } 124 125 // start full-path string with the folder's path 126 strcpy(fullpath, path); 127 128 // ensure a slash is between the folder's path and its entries' names 129 if (fullpath[slash] != '/') { 130 fullpath[slash + 0] = '/'; 131 fullpath[slash + 1] = 0; 132 } 133 134 // remember where to start appending entry names in the full-path string 135 size_t start = slash + 1; 136 137 while (!feof(stdout)) { 138 const struct dirent* item = readdir(entries); 139 if (item == NULL) { 140 break; 141 } 142 143 const char* name = item->d_name; 144 145 // ignore entries `.` and `..` 146 if (name[0] == '.') { 147 if ((name[1] == 0) || (name[1] == '.' && name[2] == 0)) { 148 continue; 149 } 150 } 151 152 // ensure capacity of the full-path is enough for this entry's name 153 size_t extra = strlen(name); 154 if (start + extra >= cap) { 155 char* old = fullpath; 156 cap = start + extra + 2; 157 fullpath = realloc(fullpath, cap); 158 159 // if allocation fails, simply give and quit the app with a message 160 if (fullpath == NULL) { 161 free(old); 162 fail_fullpath_alloc(entries); 163 return false; 164 } 165 } 166 167 // complete full-path using the name of the current entry 168 strcpy(fullpath + slash + 1, name); 169 170 // handle entry, possibly recursively in case of another folder 171 if (!handle_file(fullpath)) { 172 free(fullpath); 173 return false; 174 } 175 } 176 177 closedir(entries); 178 free(fullpath); 179 return true; 180 } 181 182 // handle_file handles data from the filename given; returns false only when 183 // the file can't be queried for its size, likely because it doesn't exist 184 bool handle_file(const char* path) { 185 struct stat st; 186 if (stat(path, &st) != 0) { 187 fprintf(stderr, ERROR_LINE("can't open file named '%s'"), path); 188 return false; 189 } 190 191 if (!S_ISDIR(st.st_mode)) { 192 printf("%s\t%ld\n", path, st.st_size); 193 return true; 194 } 195 196 return handle_folder(path); 197 } 198 199 // is_help_option simplifies control-flow for func run 200 bool is_help_option(const char* s) { 201 return (s[0] == '-') && (s[1] != 0) && ( 202 strcmp(s, "-h") == 0 || 203 strcmp(s, "-help") == 0 || 204 strcmp(s, "--h") == 0 || 205 strcmp(s, "--help") == 0 206 ); 207 } 208 209 // run returns the number of errors 210 int run(int argc, char** argv) { 211 puts("file\tbytes"); 212 213 size_t errors = 0; 214 for (size_t i = 1; i < argc && !feof(stdout); i++) { 215 // a `-` filename stands for the standard input 216 if (argv[i][0] == '-' && argv[i][1] == 0) { 217 handle_stdin(); 218 continue; 219 } 220 221 if (!handle_file(argv[i])) { 222 errors++; 223 } 224 } 225 226 // no filenames means use stdin as the only input 227 if (argc < 2) { 228 handle_stdin(); 229 } 230 231 return errors; 232 } 233 234 int main(int argc, char** argv) { 235 #ifdef _WIN32 236 setmode(fileno(stdin), O_BINARY); 237 // ensure output lines end in LF instead of CRLF on windows 238 setmode(fileno(stdout), O_BINARY); 239 setmode(fileno(stderr), O_BINARY); 240 #endif 241 242 if (argc > 1 && is_help_option(argv[1])) { 243 fprintf(stderr, "%s", info); 244 return 0; 245 } 246 247 return run(argc, argv) == 0 ? 0 : 1; 248 }