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