├── .gitignore ├── CHANGELOG.md ├── COPYING ├── Makefile ├── README.md ├── cflag.c ├── cflag.h ├── package.json └── test.c /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *~ 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | ### Added 10 | - Support building in C23 mode (e.g. `-std=c23`). 11 | 12 | ## [1.1.1] 13 | ### Added 14 | - Line-wrap and indent option help texts, by default to 80 columns, or to 15 | the actual terminal size in systems where `TIOCGWINSZ` is available. 16 | 17 | ## [1.1.0] - 2020-08-19 18 | ### Added 19 | - Support `float` and `double` flags. 20 | 21 | ## [1.0.0] - 2020-08-18 22 | ### Changed 23 | - Remove `typedef` for types `CFlag` and `CFlagStatus`, instead user code 24 | must use `struct cflag` and `enum cflag_status` directly. 25 | 26 | ## [0.1.1] - 2020-06-05 27 | ### Added 28 | - Support `unsigned int` flags. 29 | - Support file size (`size_t`) flags with SI suffixes. 30 | - Support time interval (`unsigned long long`) flags with conventional time 31 | suffixes (e.g. `2d` for “two days”). 32 | 33 | ## [0.1.0] - 2020-06-01 34 | 35 | Initial release. 36 | 37 | [Unreleased]: https://github.com/aperezdc/cflag/compare/1.1.2...HEAD 38 | [1.1.2]: https://github.com/aperezdc/cflag/compare/1.1.1...1.1.2 39 | [1.1.1]: https://github.com/aperezdc/cflag/compare/1.1.0...1.1.1 40 | [1.1.0]: https://github.com/aperezdc/cflag/compare/1.0.0...1.1.0 41 | [1.0.0]: https://github.com/aperezdc/cflag/compare/0.1.1...1.0.0 42 | [0.1.1]: https://github.com/aperezdc/cflag/compare/0.1.0...0.1.1 43 | [0.1.0]: https://github.com/aperezdc/cflag/releases/tag/0.1.0 44 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Igalia S.L., Adrian Perez 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: test.o cflag.o 2 | 3 | clean: 4 | $(RM) test test.o cflag.o 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | cflag 2 | ===== 3 | 4 | Installation 5 | ------------ 6 | 7 | With [clib](https://github.com/clibs/clib): 8 | 9 | ```sh 10 | clib install aperezdc/cflag 11 | ``` 12 | 13 | Example 14 | ------- 15 | 16 | ```c 17 | #include "cflag.h" 18 | 19 | int 20 | main(int argc, char **argv) 21 | { 22 | int requests = 5000; 23 | int concurrency = 10; 24 | bool verbose = false; 25 | const char *url = "https://perezdecastro.org"; 26 | 27 | const struct cflag options[] = { 28 | CFLAG(int, "requests", 'r', &requests, 29 | "Number of total requests"), 30 | CFLAG(int, "concurrency", 0 /* no short option */, &concurrency, 31 | "Number of concurrent requests"), 32 | CFLAG(bool, NULL /* no long option */, 'v', &verbose, 33 | "Verbosely show progress"), 34 | CFLAG(string, "url", 'U', &url, 35 | "Target URL"), 36 | CFLAG_HELP, 37 | CFLAG_END 38 | }; 39 | 40 | cflag_apply(options, "[options] --url URL", &argc, &argv); 41 | 42 | return EXIT_SUCCESS; 43 | } 44 | ``` 45 | -------------------------------------------------------------------------------- /cflag.c: -------------------------------------------------------------------------------- 1 | /* cflag.c 2 | * Copyright (C) 2020-2024 Adrian Perez de Castro 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | 7 | #if defined(_POSIX_C_SOURCE) && (_POSIX_C_SOURCE < 199309L) 8 | #undef _POSIX_C_SOURCE 9 | #endif 10 | 11 | #ifndef _POSIX_C_SOURCE 12 | #define _POSIX_C_SOURCE 199309L 13 | #endif 14 | 15 | #include "cflag.h" 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | #if defined(linux) || defined(__linux) || defined(__linux__) || \ 25 | defined(__OpenBSD__) || defined(__FreeBSD__) || defined(__NetBSD__) 26 | # define HAVE_TIOCGWINZS 1 27 | # include 28 | #else 29 | # define HAVE_TIOCGWINZS 0 30 | #endif 31 | 32 | static unsigned 33 | term_columns(FILE *f) 34 | { 35 | #if HAVE_TIOCGWINZS 36 | struct winsize ws; 37 | int fd = fileno(f); 38 | if (fd >= 0 && ioctl(fd, TIOCGWINSZ, &ws) == 0) 39 | return ws.ws_col; 40 | #endif /* HAVE_TIOCGWINZS */ 41 | 42 | return 80; 43 | } 44 | 45 | static bool 46 | findent(FILE *out, unsigned rcol, const char *text) 47 | { 48 | if (rcol < 20) 49 | return false; 50 | 51 | if (!(text && *text)) 52 | return true; 53 | 54 | fputs(" ", out); 55 | for (unsigned col = 3; text && *text;) { 56 | const char *spc = strchr(text, ' '); 57 | const size_t len = spc ? (++spc - text) : strlen(text); 58 | 59 | if (col + len > rcol) { 60 | /* Move over to the next line. */ 61 | fputc('\n', out); 62 | return findent(out, rcol, text); 63 | } 64 | 65 | fwrite(text, sizeof(char), len, out); 66 | col += len; 67 | text = spc; 68 | } 69 | 70 | return true; 71 | } 72 | 73 | static inline bool 74 | is_long_flag(const char *s) 75 | { 76 | return (s[0] == '-' && s[1] == '-' && s[2] != '\0'); 77 | } 78 | 79 | static inline bool 80 | is_short_flag(const char *s) 81 | { 82 | return (s[0] == '-' && s[1] != '\0' && s[2] == '\0'); 83 | } 84 | 85 | static inline bool 86 | is_negated(const char *s) 87 | { 88 | return strncmp("--no-", s, 5) == 0; 89 | } 90 | 91 | static const struct cflag* 92 | find_short(const struct cflag specs[], 93 | int letter) 94 | { 95 | for (unsigned i = 0; specs[i].name != NULL || specs[i].letter != '\0'; ++i) { 96 | if (specs[i].letter == '\0') 97 | continue; 98 | if (specs[i].letter == letter) 99 | return &specs[i]; 100 | } 101 | return NULL; 102 | } 103 | 104 | static const struct cflag* 105 | find_long(const struct cflag specs[], 106 | const char *name) 107 | { 108 | for (unsigned i = 0; specs[i].name != NULL || specs[i].letter != '\0'; ++i) { 109 | if (!specs[i].name) 110 | continue; 111 | if (strcmp(specs[i].name, name) == 0) 112 | return &specs[i]; 113 | } 114 | return NULL; 115 | } 116 | 117 | static inline bool 118 | needs_arg(const struct cflag *spec) 119 | { 120 | return (*spec->func)(NULL, NULL) == CFLAG_NEEDS_ARG; 121 | } 122 | 123 | void 124 | cflag_usage(const struct cflag specs[], 125 | const char *progname, 126 | const char *usage, 127 | FILE *out) 128 | { 129 | assert(specs); 130 | assert(progname); 131 | assert(usage); 132 | 133 | if (!out) 134 | out = stderr; 135 | { 136 | const char *slash = strrchr(progname, '/'); 137 | if (slash) 138 | progname = slash + 1; 139 | } 140 | 141 | const unsigned rcol = term_columns(out) - 3; 142 | 143 | fprintf(out, "Usage: %s %s\n", progname, usage); 144 | fprintf(out, "Command line options:\n\n"); 145 | 146 | for (unsigned i = 0;; ++i) { 147 | const struct cflag *spec = &specs[i]; 148 | 149 | const bool has_letter = spec->letter != '\0'; 150 | const bool has_name = spec->name != NULL; 151 | 152 | if (!(has_name || has_letter)) 153 | break; 154 | 155 | if (has_letter && has_name) 156 | fprintf(out, "-%c, --%s", spec->letter, spec->name); 157 | else if (has_name) 158 | fprintf(out, "--%s", spec->name); 159 | else 160 | fprintf(out, "-%c", spec->letter); 161 | 162 | if (needs_arg(spec)) 163 | fprintf(out, " "); 164 | 165 | fputc('\n', out); 166 | if (!findent(out, rcol, spec->help)) { 167 | fputs(" ", out); 168 | fputs(spec->help, out); 169 | } 170 | fputc('\n', out); 171 | fputc('\n', out); 172 | } 173 | } 174 | 175 | int 176 | cflag_parse(const struct cflag specs[], 177 | int *pargc, 178 | char ***pargv) 179 | { 180 | assert(specs); 181 | assert(pargc); 182 | assert(pargv); 183 | 184 | int argc = *pargc; 185 | char **argv = *pargv; 186 | 187 | for (; argc > 0; --argc, ++argv) { 188 | const char *arg = *argv; 189 | 190 | bool negated = false; 191 | const struct cflag *spec; 192 | if (is_short_flag(arg)) { 193 | if (arg[1] == '-') /* -- stop processing command line flags */ 194 | break; 195 | spec = find_short(specs, arg[1]); 196 | } else if (is_long_flag(arg)) { 197 | spec = find_long(specs, &arg[2]); 198 | if (!spec && is_negated(arg)) { 199 | const struct cflag *negspec = find_long(specs, &arg[5]); 200 | if (negspec->func == cflag_bool) { 201 | spec = negspec; 202 | negated = true; 203 | } 204 | } 205 | } else { 206 | *pargc = argc; *pargv = argv; 207 | return CFLAG_OK; 208 | } 209 | 210 | if (!spec) { 211 | *pargc = argc; *pargv = argv; 212 | return CFLAG_UNDEFINED; 213 | } 214 | 215 | arg = NULL; 216 | if (needs_arg(spec)) { 217 | if (argc == 1) { 218 | *pargc = argc; *pargv = argv; 219 | return CFLAG_NEEDS_ARG; 220 | } 221 | arg = *(++argv); 222 | --argc; 223 | } 224 | 225 | const enum cflag_status status = (*spec->func)(spec, arg); 226 | if (status != CFLAG_OK) { 227 | *pargc = argc; *pargv = argv; 228 | return status; 229 | } 230 | 231 | /* 232 | * XXX: This fixup here is ugly, but avoids needing to pass 233 | * additional parameters to cflag_ functions. 234 | */ 235 | if (spec->func == cflag_bool && negated) 236 | *((bool*) spec->data) = false; 237 | } 238 | 239 | *pargc = argc; *pargv = argv; 240 | return CFLAG_OK; 241 | } 242 | 243 | const char* 244 | cflag_status_name(enum cflag_status value) 245 | { 246 | switch (value) { 247 | case CFLAG_OK: return "success"; 248 | case CFLAG_SHOW_HELP: return "help requested"; 249 | case CFLAG_UNDEFINED: return "no such option"; 250 | case CFLAG_BAD_FORMAT: return "argument has invalid format"; 251 | case CFLAG_NEEDS_ARG: return "missing argument"; 252 | } 253 | assert(!"Unreachable"); 254 | abort(); 255 | } 256 | 257 | const char* 258 | cflag_apply(const struct cflag specs[], 259 | const char *syntax, 260 | int *pargc, 261 | char ***pargv) 262 | { 263 | assert(specs); 264 | assert(syntax); 265 | assert(pargc); 266 | assert(pargv); 267 | 268 | int argc = *pargc; 269 | char **argv = *pargv; 270 | 271 | const char *argv0 = *argv++; argc--; 272 | { 273 | const char *slash = strrchr(argv0, '/'); 274 | if (slash) argv0 = slash + 1; 275 | } 276 | 277 | const enum cflag_status status = cflag_parse(specs, &argc, &argv); 278 | switch (status) { 279 | case CFLAG_SHOW_HELP: 280 | cflag_usage(specs, argv0, syntax, stdout); 281 | exit(EXIT_SUCCESS); 282 | case CFLAG_OK: 283 | *pargc = argc; 284 | *pargv = argv; 285 | return argv0; 286 | default: 287 | break; 288 | } 289 | 290 | fprintf(stderr, "%s: %s: '%s'\n", argv0, cflag_status_name(status), *argv); 291 | exit(EXIT_FAILURE); 292 | } 293 | 294 | enum cflag_status 295 | cflag_bool(const struct cflag *spec, 296 | const char *arg) 297 | { 298 | (void) arg; 299 | 300 | if (!spec) 301 | return CFLAG_OK; 302 | 303 | *((bool*) spec->data) = true; 304 | return CFLAG_OK; 305 | } 306 | 307 | enum cflag_status 308 | cflag_int(const struct cflag *spec, 309 | const char *arg) 310 | { 311 | if (!spec) 312 | return CFLAG_NEEDS_ARG; 313 | 314 | return (sscanf(arg, "%d", (int*) spec->data) == 1) ? CFLAG_OK : CFLAG_BAD_FORMAT; 315 | } 316 | 317 | enum cflag_status 318 | cflag_uint(const struct cflag *spec, 319 | const char *arg) 320 | { 321 | if (!spec) 322 | return CFLAG_NEEDS_ARG; 323 | 324 | return (sscanf(arg, "%u", (unsigned*) spec->data) == 1) ? CFLAG_OK : CFLAG_BAD_FORMAT; 325 | } 326 | 327 | enum cflag_status 328 | cflag_float(const struct cflag *spec, 329 | const char *arg) 330 | { 331 | if (!spec) 332 | return CFLAG_NEEDS_ARG; 333 | 334 | char *endptr; 335 | float v = strtof(arg, &endptr); 336 | if (errno == ERANGE || *endptr != '\0') 337 | return CFLAG_BAD_FORMAT; 338 | 339 | *((float*) spec->data) = v; 340 | return CFLAG_OK; 341 | } 342 | 343 | enum cflag_status 344 | cflag_double(const struct cflag *spec, 345 | const char *arg) 346 | { 347 | if (!spec) 348 | return CFLAG_NEEDS_ARG; 349 | 350 | char *endptr; 351 | double v = strtod(arg, &endptr); 352 | if (errno == ERANGE || *endptr != '\0') 353 | return CFLAG_BAD_FORMAT; 354 | 355 | *((double*) spec->data) = v; 356 | return CFLAG_OK; 357 | } 358 | 359 | enum cflag_status 360 | cflag_string(const struct cflag *spec, 361 | const char *arg) 362 | { 363 | if (!spec) 364 | return CFLAG_NEEDS_ARG; 365 | 366 | *((const char**) spec->data) = arg; 367 | return CFLAG_OK; 368 | } 369 | 370 | enum cflag_status 371 | cflag_bytes(const struct cflag *spec, 372 | const char *arg) 373 | { 374 | if (!spec) 375 | return CFLAG_NEEDS_ARG; 376 | 377 | char *endpos; 378 | unsigned long long v = strtoull(arg, &endpos, 0); 379 | if (v == ULLONG_MAX && errno == ERANGE) 380 | return CFLAG_BAD_FORMAT; 381 | 382 | if (endpos) { 383 | switch (*endpos) { 384 | case 'g': case 'G': v *= 1024 * 1024 * 1024; break; /* gigabytes */ 385 | case 'm': case 'M': v *= 1024 * 1024; break; /* megabytes */ 386 | case 'k': case 'K': v *= 1024; break; /* kilobytes */ 387 | case 'b': case 'B': case '\0': break; /* bytes */ 388 | } 389 | } 390 | 391 | *((size_t*) spec->data) = v; 392 | return CFLAG_OK; 393 | } 394 | 395 | enum cflag_status 396 | cflag_timei(const struct cflag *spec, 397 | const char *arg) 398 | { 399 | if (!spec) 400 | return CFLAG_NEEDS_ARG; 401 | 402 | char *endpos; 403 | unsigned long long v = strtoull(arg, &endpos, 0); 404 | 405 | if (v == ULLONG_MAX && errno == ERANGE) 406 | return CFLAG_BAD_FORMAT; 407 | 408 | if (endpos) { 409 | switch (*endpos) { 410 | case 'y': v *= 60 * 60 * 24 * 365; break; /* years */ 411 | case 'M': v *= 60 * 60 * 24 * 30; break; /* months */ 412 | case 'w': v *= 60 * 60 * 24 * 7; break; /* weeks */ 413 | case 'd': v *= 60 * 60 * 24; break; /* days */ 414 | case 'h': v *= 60 * 60; break; /* hours */ 415 | case 'm': v *= 60; break; /* minutes */ 416 | case 's': case '\0': break; /* seconds */ 417 | default : return CFLAG_BAD_FORMAT; 418 | } 419 | } 420 | 421 | *((unsigned long long*) spec->data) = v; 422 | return CFLAG_OK; 423 | } 424 | 425 | enum cflag_status 426 | cflag_help(const struct cflag *spec, 427 | const char *arg) 428 | { 429 | (void) spec; 430 | (void) arg; 431 | 432 | return CFLAG_SHOW_HELP; 433 | } 434 | -------------------------------------------------------------------------------- /cflag.h: -------------------------------------------------------------------------------- 1 | /* 2 | * cflag.h 3 | * Copyright (C) 2020 Adrian Perez de Castro 4 | * 5 | * SPDX-License-Identifier: MIT 6 | */ 7 | 8 | #ifndef CFLAG_H 9 | #define CFLAG_H 10 | 11 | #include 12 | 13 | enum cflag_type { 14 | CFLAG_TYPE_BOOL = 0, 15 | CFLAG_TYPE_INT, 16 | CFLAG_TYPE_STRING, 17 | CFLAG_TYPE_CUSTOM, 18 | CFLAG_TYPE_HELP, 19 | }; 20 | 21 | enum cflag_status { 22 | CFLAG_OK = 0, 23 | CFLAG_SHOW_HELP, 24 | CFLAG_UNDEFINED, 25 | CFLAG_BAD_FORMAT, 26 | CFLAG_NEEDS_ARG, 27 | }; 28 | 29 | struct cflag; 30 | 31 | typedef enum cflag_status (*cflag_func) (const struct cflag*, const char *arg); 32 | 33 | struct cflag { 34 | cflag_func func; 35 | const char *name; 36 | int letter; 37 | void *data; 38 | const char *help; 39 | }; 40 | 41 | 42 | #define CFLAG(_t, _name, _letter, _data, _help) \ 43 | ((struct cflag) { \ 44 | .func = cflag_ ## _t, \ 45 | .name = (_name), \ 46 | .letter = (_letter), \ 47 | .data = (_data), \ 48 | .help = (_help), \ 49 | }) 50 | #define CFLAG_HELP \ 51 | CFLAG(help, "help", 'h', NULL, "Prints command line usage help.") 52 | #define CFLAG_END \ 53 | { .name = NULL, .letter = '\0' } 54 | 55 | enum cflag_status cflag_bool (const struct cflag*, const char*); 56 | enum cflag_status cflag_int (const struct cflag*, const char*); 57 | enum cflag_status cflag_uint (const struct cflag*, const char*); 58 | enum cflag_status cflag_float (const struct cflag*, const char*); 59 | enum cflag_status cflag_double (const struct cflag*, const char*); 60 | enum cflag_status cflag_string (const struct cflag*, const char*); 61 | enum cflag_status cflag_bytes (const struct cflag*, const char*); 62 | enum cflag_status cflag_timei (const struct cflag*, const char*); 63 | enum cflag_status cflag_help (const struct cflag*, const char*); 64 | 65 | void cflag_usage(const struct cflag specs[], 66 | const char *progname, 67 | const char *syntax, 68 | FILE *out); 69 | 70 | int cflag_parse(const struct cflag specs[], 71 | int *pargc, 72 | char ***pargv); 73 | 74 | const char* cflag_apply(const struct cflag specs[], 75 | const char *syntax, 76 | int *pargc, 77 | char ***pargv); 78 | 79 | const char* cflag_status_name(enum cflag_status value); 80 | 81 | #endif /* !CFLAG_H */ 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cflag", 3 | "version": "1.1.2", 4 | "repo": "aperezdc/cflag", 5 | "description": "Non-allocating command line flag parser", 6 | "license": "MIT", 7 | "keywords": ["cli", "flag", "options", "parser"], 8 | "src": ["cflag.h", "cflag.c"] 9 | } 10 | -------------------------------------------------------------------------------- /test.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Adrian Perez de Castro 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | 7 | #include "cflag.h" 8 | #include 9 | #include 10 | #include 11 | 12 | int 13 | main(int argc, char **argv) 14 | { 15 | int requests = 5000; 16 | int concurrency = 10; 17 | bool verbose = false; 18 | const char *url = "https://perezdecastro.org"; 19 | 20 | const struct cflag options[] = { 21 | CFLAG(int, "requests", 'r', &requests, 22 | "Number of total requests"), 23 | CFLAG(int, "concurrency", 0 /* no short option */, &concurrency, 24 | "Number of concurrent requests"), 25 | CFLAG(bool, NULL /* no long option */, 'v', &verbose, 26 | "Verbosely show progress"), 27 | CFLAG(string, "url", 'U', &url, 28 | "Target URL"), 29 | CFLAG_HELP, 30 | CFLAG_END 31 | }; 32 | 33 | cflag_apply(options, "[options] --url URL", &argc, &argv); 34 | 35 | printf("requests = %d\n", requests); 36 | printf("concurrency = %d\n", concurrency); 37 | printf("verbose = %s\n", verbose ? "true" : "false"); 38 | printf("url = %s\n", url); 39 | 40 | return EXIT_SUCCESS; 41 | } 42 | --------------------------------------------------------------------------------