diff --git a/kyaa.h b/kyaa.h new file mode 100644 index 0000000..2f825a3 --- /dev/null +++ b/kyaa.h @@ -0,0 +1,113 @@ +/* kyaa.h - macro hacks for handling main() arguments + This is free and unencumbered software released into the public domain. + Refer to kyaa.md for documentation. +*/ + +#pragma once + +#ifndef KYAA_OKAY +#define KYAA_OKAY 0 +#endif +#ifndef KYAA_ERROR +#define KYAA_ERROR 1 +#endif +#ifndef KYAA_ITER +#define KYAA_ITER i +#endif + +#define KYAA_SETUP \ + /* sanity checks */ \ + if (argc <= 0 || argv == NULL || argv[0] == NULL) { \ + fprintf(stderr, "You've met with a terrible fate.\n"); \ + return KYAA_ERROR; \ + } \ + char *kyaa_name = argv[0]; \ + bool kyaa_read_stdin = false; \ + char kyaa_flag = '\0'; \ + bool kyaa_keep_parsing = true; \ + bool kyaa_parse_next = false; \ + +#define KYAA_LOOP \ + KYAA_SETUP \ + for (int KYAA_ITER = 1; KYAA_ITER < argc; KYAA_ITER++) \ + +#define KYAA_BEGIN \ + char *kyaa_arg = argv[KYAA_ITER]; \ + if (kyaa_keep_parsing && (kyaa_parse_next || kyaa_arg[0] == '-')) { \ + if (!kyaa_parse_next && kyaa_arg[1] == '-' && kyaa_arg[2] == '\0') { \ + kyaa_keep_parsing = false; \ + continue; \ + } \ + if (!kyaa_parse_next && kyaa_arg[1] == '\0') { \ + kyaa_read_stdin = true; \ + } else { \ + /* case: kyaa_parse_next: arg is at least 1 char initialized. */ \ + /* case: !kyaa_parse_next: arg is at least 3 chars initialized. */ \ + char *kyaa_etc = kyaa_parse_next ? kyaa_arg : kyaa_arg + 2; \ + bool kyaa_no_more = false; \ + bool kyaa_helping = false; \ + bool kyaa_any = false; \ + if (!kyaa_parse_next && kyaa_arg[1] != '-') { \ + kyaa_flag = kyaa_arg[1]; \ + kyaa_no_more = kyaa_arg[2] == '\0'; \ + } \ + if (kyaa_flag == 'h' || !strcmp(kyaa_arg, "--help")) { \ + printf("usage:\n"); \ + kyaa_helping = true; \ + } \ + if (0) { \ + +#define KYAA_END \ + } \ + if (!kyaa_any && !kyaa_helping) { \ + if (kyaa_flag) { \ + fprintf(stderr, "unknown flag: -%c\n", kyaa_flag); \ + } else { \ + fprintf(stderr, "unknown flag: %s\n", kyaa_arg); \ + } \ + return KYAA_ERROR; \ + } \ + if (kyaa_helping) { \ + return KYAA_OKAY; \ + } \ + kyaa_parse_next = false; \ + kyaa_flag = '\0'; \ + continue; \ + } \ + } \ + +#define KYAA_DESCRIBE(c, name, description) \ + printf(" -%c --%s\n%s\n", c, name, description) \ + +#define KYAA_FLAG(c, name, description) \ + } \ + if (kyaa_helping) { \ + KYAA_DESCRIBE(c, name, description); \ + } else if (kyaa_flag == c || !strcmp(kyaa_arg, "--"name)) { \ + kyaa_flag = c; \ + kyaa_any = true; \ + +#define KYAA_FLAG_ARG(c, name, description) \ + } \ + if (kyaa_helping) { \ + KYAA_DESCRIBE(c, name, description); \ + } else if (kyaa_flag == c || !strcmp(kyaa_arg, "--"name)) { \ + if (kyaa_no_more || kyaa_flag != c) { \ + kyaa_parse_next = true; \ + if (KYAA_ITER + 1 == argc || argv[KYAA_ITER + 1][0] == '\0') { \ + fprintf(stderr, "expected an argument for --%s (-%c)\n", name, c); \ + return KYAA_ERROR; \ + } \ + kyaa_flag = c; \ + continue; \ + } \ + kyaa_flag = c; \ + kyaa_any = true; \ + +#define KYAA_HELP(description) \ + } \ + if (kyaa_helping) { \ + if (description != NULL) { \ + printf("%s\n", description); \ + } \ + diff --git a/kyaa.md b/kyaa.md new file mode 100644 index 0000000..dc9dba7 --- /dev/null +++ b/kyaa.md @@ -0,0 +1,160 @@ +# kyaa + +super hacky macro hacks for parsing arguments in C. + +## prerequisites + +C99 or greater. + +standard library headers: +* `stdbool.h` +* `stdio.h` +* `string.h` + +in addition, `kyaa_extra.h` requires `limits.h`, +or at least `LONG_MIN` to be defined. + +## tutorial + +ensure `argc` and `argv` are defined. +kyaa doesn't actually care if it's in `main` or not. + +iterate over the arguments with `KYAA_LOOP`: + +```c +int main(int argc, char *argv[]) { + KYAA_LOOP { + // KYAA_ITER, kyaa_name, kyaa_read_stdin, and kyaa_flag are exposed here. + // [other code goes here] + } + return 0; +} +``` + +use `KYAA_BEGIN` and `KYAA_END` to begin parsing arguments. +put unrelated code above `KYAA_BEGIN` or below `KYAA_END`, but not within: + +```c + KYAA_LOOP { + // [other code goes here] + KYAA_BEGIN + // kyaa_arg and kyaa_etc are exposed here. + // [kyaa code goes here] + KYAA_END + // [other code goes here] + } +``` + +use `KYAA_FLAG` for defining flags that don't take an argument, +and `KYAA_FLAG_ARG` or `KYAA_FLAG_LONG` for flags that do: + +```c + bool use_feature = false; + char *log_fn = "logs.txt"; + long my_var = 0; + KYAA_LOOP { + // [other code goes here] + KYAA_BEGIN + + // arguments: short flag, long flag, help description + KYAA_FLAG("-x", "--enable-feature", +" enable some feature") + use_feature = true; + + // same arguments, but kyaa_etc contains the relevant string. + KYAA_FLAG_ARG("-l", "--log-file", +" use a given filename for the log file") + log_fn = kyaa_etc; + + // same arguments, kyaa_etc is set, but kyaa_flag_arg is also set. + KYAA_FLAG_LONG("-v", "--var", +" set an integer variable\n" +" default: 0") + my_var = kyaa_flag_arg; + + KYAA_END + // [other code goes here] + } +``` + +kyaa secretly wraps flag handling in if/else statements with {} blocks. +do not confuse it for a switch/case method. + +kyaa handles `-h` and `--help` for printing help text. +additional help text may be defined using `KYAA_HELP`: + +```c + KYAA_LOOP { + KYAA_BEGIN + KYAA_HELP( +" {files...}\n" +" do things with files.") + KYAA_END + + do_stuff(kyaa_arg); + } +``` + +kyaa interprets an argument of `-` as a request to enable reading from stdin: +`kyaa_read_stdin` is set to true. + +arguments may be passed in three ways, consider: +* `-v42` +* `-v 42` +* `--var 42` + +kyaa returns `KYAA_OKAY` when `-h` or `--help` is given, +and `KYAA_ERROR` in the event of invalid flags, missing arguments, +or invalid numbers (`KYAA_FLAG_LONG`). +kyaa uses `continue` for handling arguments. + +kyaa prints error messages to `stderr`, and help text to `stdout`. + +`KYAA_ITER` may be redefined to avoid name collisions. + +## TODO + +* support `--var=42` argument style +* fix overlapping names, e.g. `KYAA_FLAG` vs `kyaa_flag`, `KYAA_FLAG_ARG` vs `kyaa_flag_arg`, etc. +* maybe pass `argc`/`argv` manually? + +## API + +### kyaa.h + +return type or type | name | arguments or default value +--- | --- | --- +define | KYAA\_OKAY | 0 +define | KYAA\_ERROR | 1 +define | KYAA\_ITER | i +|| +macro | KYAA\_SETUP | *none* +macro | KYAA\_LOOP | *none* +macro | KYAA\_BEGIN | *none* +macro | KYAA\_END | *none* +macro | KYAA\_DESCRIBE | c, name, description +macro | KYAA\_FLAG | c, name, description +macro | KYAA\_FLAG\_ARG | c, name, description +macro | KYAA\_HELP | description +|| +`char *` | kyaa\_name | *n/a* +`bool` | kyaa\_read\_stdin | *n/a* +`char *` | kyaa\_arg | *n/a* +|| **internal use** || +`char` | kyaa\_flag | *n/a* +`bool` | kyaa\_keep\_parsing | *n/a* +`bool` | kyaa\_parse\_next | *n/a* +`bool` | kyaa\_no\_more | *n/a* +`bool` | kyaa\_helping | *n/a* +`bool` | kyaa\_any | *n/a* + +### kyaa\_extra.h + +return value or type | name | arguments or default value +--- | --- | --- +macro | KYAA\_FLAG\_LONG | c, name, description +|| +`char *` | kyaa\_flag\_arg | *n/a* +|| +`char *` | kyaa\_skip\_spaces | `char *` str +`const char *` | kyaa\_str\_to\_long | `char *` str, `long *` p\_out diff --git a/kyaa_extra.h b/kyaa_extra.h new file mode 100644 index 0000000..77869f4 --- /dev/null +++ b/kyaa_extra.h @@ -0,0 +1,211 @@ +/* kyaa_extra.h - extensions to kyaa for parsing arguments. + This is free and unencumbered software released into the public domain. + Refer to kyaa.md for documentation. +*/ + +static char *kyaa_skip_spaces(char *str) { + /* iterates str to first non-space character according to the C locale */ + while (*str != '\0') { + switch (*str) { + case ' ': + case '\f': + case '\n': + case '\r': + case '\t': + case '\v': { str++; } break; + default: return str; + } + } + return str; +} + +static const char *kyaa__base_2(char **p_str, long *p_out) { + char *str = *p_str; + long out = *p_out; + for (char c; (c = *str) != '\0'; str++) { + switch (c) { + case '0': case '1': { + long digit = (long)(c - '0'); + if (out < (LONG_MIN + digit) / 2) { + return "out of range for long integer"; + } + out = out * 2 - digit; + } break; + + case '2': case '3': case '4': case '5': + case '6': case '7': case '8': case '9': + case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': + case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': + case '.': return "invalid character for base 2 integer"; + + default: goto exit; + } + } +exit: + *p_str = str; + *p_out = out; + return NULL; +} + +static const char *kyaa__base_8(char **p_str, long *p_out) { + char *str = *p_str; + long out = *p_out; + for (char c; (c = *str) != '\0'; str++) { + switch (c) { + case '0': case '1': case '2': case '3': + case '4': case '5': case '6': case '7': { + long digit = (long)(c - '0'); + if (out < (LONG_MIN + digit) / 8) { + return "out of range for long integer"; + } + out = out * 8 - digit; + } break; + + case '8': case '9': + case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': + case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': + case '.': return "invalid character for base 8 integer"; + + default: goto exit; + } + } +exit: + *p_str = str; + *p_out = out; + return NULL; +} + +static const char *kyaa__base_10(char **p_str, long *p_out) { + char *str = *p_str; + long out = *p_out; + for (char c; (c = *str) != '\0'; str++) { + switch (c) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': { + long digit = (long)(c - '0'); + if (out < (LONG_MIN + digit) / 10) { + return "out of range for long integer"; + } + out = out * 10 - digit; + } break; + + case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': + case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': + case '.': return "invalid character for base 10 integer"; + + default: goto exit; + } + } +exit: + *p_str = str; + *p_out = out; + return NULL; +} + +static const char *kyaa__base_16(char **p_str, long *p_out) { + char *str = *p_str; + long out = *p_out; + for (char c; (c = *str) != '\0'; str++) { + switch (c) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': { + long digit = (long)(c - '0'); + if (out < (LONG_MIN + digit) / 16) { + return "out of range for long integer"; + } + out = out * 16 - digit; + } break; + + case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': { + long digit = (long)(c - 'A') + 10; + if (out < (LONG_MIN + digit) / 16) { + return "out of range for long integer"; + } + out = out * 16 - digit; + } break; + + case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': { + long digit = (long)(c - 'a') + 10; + if (out < (LONG_MIN + digit) / 16) { + return "out of range for long integer"; + } + out = out * 16 - digit; + } break; + + case '.': return "invalid character for base 16 integer"; + + default: goto exit; + } + } +exit: + *p_str = str; + *p_out = out; + return NULL; +} + +static const char *kyaa_str_to_long(char *str, long *p_out) { + /* returns error message or NULL */ + long out = 0; + int base = 10; + bool negated = false; + + str = kyaa_skip_spaces(str); + + switch (*str) { + case '-': { negated = true; str++; } break; + case '+': { str++; } break; + default: break; + } + + switch (*str) { + case '#': { base = 10; str++; } break; + case '$': { base = 8; str++; } break; + case '%': { base = 2; str++; } break; + case '0': { base = -1; str++; } break; + default: break; + } + + if (base == -1) { + switch (*str) { + case '\0': { *p_out = 0; } return NULL; + case 'b': { base = 2; str++; } break; + case 'h': { base = 16; str++; } break; + case 'o': { base = 8; str++; } break; + case 'x': { base = 16; str++; } break; + default: { base = 8; } break; + } + } + + if (*str == '\0') return "no number given"; + + // NOTE: we actually subtract each digit from the result instead of summing. + // this lets us represent LONG_MIN without overflowing. + const char *err; + switch (base) { + case 2: { err = kyaa__base_2( &str, &out); } break; + case 8: { err = kyaa__base_8( &str, &out); } break; + case 10: { err = kyaa__base_10(&str, &out); } break; + case 16: { err = kyaa__base_16(&str, &out); } break; + default: return "internal error"; + } + if (err != NULL) return err; + + str = kyaa_skip_spaces(str); + if (*str != '\0') return "unexpected character"; + + // NOTE: out is negative here; see above comment. + // assuming two's complement + if (!negated && out == LONG_MIN) return "out of range for long integer"; + *p_out = negated ? out : -out; + return NULL; +} + +#define KYAA_FLAG_LONG(c, name, description) \ + KYAA_FLAG_ARG(c, name, description) \ + long kyaa_flag_arg; \ + const char *err = kyaa_str_to_long(kyaa_etc, &kyaa_flag_arg); \ + if (err) { \ + fprintf(stderr, "%s\n", err); \ + return KYAA_ERROR; \ + } \ +