cargs.c 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694
  1. #include <ctype.h>
  2. #include <string.h>
  3. #include <stdlib.h>
  4. #include <stdio.h>
  5. #include "cargs.h"
  6. #define ENABLE_VALIDATION
  7. #define NULLCHAR 0
  8. #define ALLOCATE(type, amount) \
  9. (type*)malloc(sizeof(type) * amount)
  10. #define REALLOCATE(pointer, type, amount) \
  11. (type*)realloc(pointer, sizeof(type) * amount)
  12. #define ADD_ELEM(type, pointer, elem, currLen)\
  13. if (pointer == NULL) {\
  14. pointer = ALLOCATE(type, 1);\
  15. pointer[0] = elem;\
  16. } else {\
  17. pointer = REALLOCATE(pointer, type, currLen + 1);\
  18. pointer[currLen] = elem;\
  19. }\
  20. #define PTR_NULLSAFE_SET(pointer, value) if (pointer != NULL) *pointer = value
  21. #define OUT
  22. const __cargs_bool __cargs_true = 1;
  23. const __cargs_bool __cargs_false = 0;
  24. void parseCurrentArg(
  25. ParseContext* context,
  26. ParseResult* result,
  27. const int argc,
  28. char* argv[],
  29. const size_t pos,
  30. OUT size_t* nextPos);
  31. const __cargs_bool isInteger(char* str)
  32. {
  33. const size_t len = strlen(str);
  34. for (size_t i = 0; i < len; i++) {
  35. if (!isdigit(str[i])) {
  36. return __cargs_false;
  37. }
  38. }
  39. return __cargs_true;
  40. }
  41. const __cargs_bool isDecimal(char* str)
  42. {
  43. const size_t len = strlen(str);
  44. for (size_t i = 0; i < len; i++) {
  45. if (!(isdigit(str[i]) || str[i] == '.')) {
  46. return __cargs_false;
  47. }
  48. }
  49. return __cargs_true;
  50. }
  51. const OptionData booleanArg(
  52. const char shortOption,
  53. char* longOption,
  54. OUT __cargs_bool* source,
  55. char* description)
  56. {
  57. OptionData ret = {
  58. .shortOption = shortOption,
  59. .type = Boolean,
  60. .longOption = longOption,
  61. .description = description,
  62. .sourcePtr = (void*)source,
  63. };
  64. PTR_NULLSAFE_SET(source, 0);
  65. return ret;
  66. }
  67. const OptionData integerArg(
  68. const char shortOption,
  69. char* longOption,
  70. OUT __cargs_int* source,
  71. char* description)
  72. {
  73. OptionData ret = {
  74. .shortOption = shortOption,
  75. .type = Integer,
  76. .longOption = longOption,
  77. .description = description,
  78. .sourcePtr = (void*)source,
  79. };
  80. PTR_NULLSAFE_SET(source, 0);
  81. return ret;
  82. }
  83. const OptionData decimalArg(
  84. const char shortOption,
  85. char* longOption,
  86. OUT __cargs_decimal* source,
  87. char* description)
  88. {
  89. OptionData ret = {
  90. .shortOption = shortOption,
  91. .type = Decimal,
  92. .longOption = longOption,
  93. .description = description,
  94. .sourcePtr = (void*)source,
  95. };
  96. PTR_NULLSAFE_SET(source, 0);
  97. return ret;
  98. }
  99. const OptionData stringArg(
  100. const char shortOption,
  101. char* longOption,
  102. char** source,
  103. char* description)
  104. {
  105. OptionData ret = {
  106. .shortOption = shortOption,
  107. .type = String,
  108. .longOption = longOption,
  109. .description = description,
  110. .sourcePtr = (void*)source,
  111. };
  112. PTR_NULLSAFE_SET(source, "");
  113. return ret;
  114. }
  115. void freeResult(ParseResult* result)
  116. {
  117. if (result == NULL) {
  118. return;
  119. }
  120. free(result->decimalValues);
  121. free(result->decimalOptions);
  122. free(result->errors);
  123. free(result->freeValues);
  124. free(result->integerOptions);
  125. free(result->integerValues);
  126. free(result->booleanOptions);
  127. free(result->stringOptions);
  128. free(result->stringValues);
  129. free(result);
  130. }
  131. ParseResult* parseArgs(ParseContext* context, const int argc, char* argv[])
  132. {
  133. ParseResult* result = ALLOCATE(ParseResult, 1);
  134. result->booleanOptionsCount = 0;
  135. result->decimalOptionsCount = 0;
  136. result->integerOptionsCount = 0;
  137. result->stringOptionsCount = 0;
  138. result->errorsCount = 0;
  139. size_t pos = 1;
  140. while (pos < argc) {
  141. size_t nextPos = 0;
  142. parseCurrentArg(context, result, argc, argv, pos, &nextPos);
  143. pos = nextPos;
  144. }
  145. return result;
  146. }
  147. typedef enum
  148. {
  149. Long,
  150. Short,
  151. None,
  152. }
  153. OptionLengthCategory;
  154. const size_t searchOptionShort(
  155. ParseContext* context,
  156. char shortName,
  157. OUT __cargs_bool* success)
  158. {
  159. const OptionData* items = context->items;
  160. for (size_t i = 0; i < context->itemsCount; i++) {
  161. if (items[i].shortOption == shortName) {
  162. *success = __cargs_true;
  163. return i;
  164. }
  165. }
  166. *success = __cargs_false;
  167. return 0;
  168. }
  169. const size_t searchOptionLong(
  170. ParseContext* context,
  171. const char* longName,
  172. OUT __cargs_bool* success)
  173. {
  174. const OptionData* items = context->items;
  175. for (size_t i = 0; i < context->itemsCount; i++) {
  176. if (items[i].longOption == NULL) {
  177. continue;
  178. }
  179. if (strcmp(items[i].longOption, longName) == 0) {
  180. *success = __cargs_true;
  181. return i;
  182. }
  183. }
  184. *success = __cargs_false;
  185. return 0;
  186. }
  187. const OptionLengthCategory getOptionCategory(char* option)
  188. {
  189. if (option == NULL) {
  190. return None;
  191. }
  192. const size_t len = strlen(option);
  193. if (strncmp(option, "--", 2) == 0) { // if arg starts with '--'
  194. return len > 2 ? Long : None;
  195. }
  196. if (strncmp(option, "-", 1) == 0) { // if arg starts with '-'
  197. return len > 1 ? Short : None;
  198. }
  199. return None;
  200. }
  201. void processNonBooleanArgs(
  202. ParseContext* context,
  203. ParseResult* result,
  204. const size_t optionIndex,
  205. const OptionData* data,
  206. int argc,
  207. char* argv[],
  208. const int pos,
  209. OUT size_t* nextPos)
  210. {
  211. if (pos == argc - 1) {
  212. ParseError error = {
  213. .category = NonBooleanOptionDoesntHaveArgument,
  214. .argIndex = pos,
  215. .optionShortName = NULLCHAR,
  216. };
  217. ADD_ELEM(
  218. ParseError,
  219. result->errors,
  220. error,
  221. result->errorsCount);
  222. result->errorsCount++;
  223. *nextPos = pos + 1;
  224. return;
  225. }
  226. switch (data->type) {
  227. case Integer:
  228. if (!isInteger(argv[pos + 1])) {
  229. ParseError error = {
  230. .category = IntegerParsingError,
  231. .argIndex = pos,
  232. .optionShortName = NULLCHAR,
  233. };
  234. ADD_ELEM(
  235. ParseError,
  236. result->errors,
  237. error,
  238. result->errorsCount);
  239. result->errorsCount++;
  240. *nextPos = pos + 2;
  241. return;
  242. }
  243. __cargs_int valInt = (__cargs_int)atoi(argv[pos + 1]);
  244. ADD_ELEM(
  245. __cargs_int,
  246. result->integerValues,
  247. valInt,
  248. result->integerOptionsCount);
  249. ADD_ELEM(
  250. size_t,
  251. result->integerOptions,
  252. optionIndex,
  253. result->integerOptionsCount);
  254. result->integerOptionsCount++;
  255. *nextPos = pos + 2;
  256. __cargs_int* sourceInt = (__cargs_int*)data->sourcePtr;
  257. PTR_NULLSAFE_SET(sourceInt, valInt);
  258. break;
  259. case String:
  260. { }
  261. char* valStr = argv[pos + 1];
  262. ADD_ELEM(
  263. char*,
  264. result->stringValues,
  265. valStr,
  266. result->stringOptionsCount);
  267. ADD_ELEM(
  268. size_t,
  269. result->stringOptions,
  270. optionIndex,
  271. result->stringOptionsCount);
  272. result->stringOptionsCount++;
  273. *nextPos = pos + 2;
  274. char** sourceStr = (char**)data->sourcePtr;
  275. PTR_NULLSAFE_SET(sourceStr, valStr);
  276. break;
  277. case Decimal:
  278. if (!isDecimal(argv[pos + 1])) {
  279. ParseError error = {
  280. .category = DecimalParsingError,
  281. .argIndex = pos,
  282. .optionShortName = NULLCHAR,
  283. };
  284. ADD_ELEM(
  285. ParseError,
  286. result->errors,
  287. error,
  288. result->errorsCount);
  289. result->errorsCount++;
  290. *nextPos = pos + 2;
  291. return;
  292. }
  293. __cargs_decimal valDecimal = (__cargs_decimal)atof(argv[pos + 1]);
  294. ADD_ELEM(
  295. __cargs_decimal,
  296. result->decimalValues,
  297. valDecimal,
  298. result->decimalOptionsCount);
  299. ADD_ELEM(
  300. size_t,
  301. result->decimalOptions,
  302. valDecimal,
  303. result->decimalOptionsCount);
  304. result->decimalOptionsCount++;
  305. *nextPos = pos + 2;
  306. __cargs_decimal* sourceDbl = (__cargs_decimal*)data->sourcePtr;
  307. PTR_NULLSAFE_SET(sourceDbl, valDecimal);
  308. break;
  309. default:
  310. *nextPos = pos + 1;
  311. break;
  312. }
  313. }
  314. void parseCurrentArg(
  315. ParseContext* context,
  316. ParseResult* result,
  317. const int argc,
  318. char* argv[],
  319. const size_t pos,
  320. OUT size_t* nextPos)
  321. {
  322. char* arg = argv[pos];
  323. const OptionLengthCategory cat = getOptionCategory(arg);
  324. *nextPos = pos;
  325. OptionData* items = context->items;
  326. switch (cat) {
  327. case Long:
  328. goto longProcessing;
  329. return;
  330. case Short:
  331. goto shortProcessing;
  332. break;
  333. case None:
  334. ADD_ELEM(char*, result->freeValues, arg, result->freeValuesCount);
  335. result->freeValuesCount++;
  336. *nextPos = *nextPos + 1;
  337. break;
  338. }
  339. return;
  340. // TODO: REFACTOR THIS!!!
  341. longProcessing:
  342. // arg + 2 -> pointer to start of a arg name (after --)
  343. { }
  344. __cargs_bool searchSuccess = __cargs_false;
  345. size_t optionIndex = searchOptionLong(context, arg + 2, &searchSuccess);
  346. if (!searchSuccess) {
  347. ParseError error = {
  348. .category = OptionDoesntExist,
  349. .argIndex = pos,
  350. .optionShortName = NULLCHAR,
  351. };
  352. ADD_ELEM(ParseError, result->errors, error, result->errorsCount);
  353. result->errorsCount++;
  354. *nextPos = *nextPos + 1;
  355. return;
  356. }
  357. OptionData option = items[optionIndex];
  358. if (option.type != Boolean) {
  359. processNonBooleanArgs(
  360. context, result, optionIndex, &option, argc, argv, pos, nextPos);
  361. return;
  362. }
  363. ADD_ELEM(
  364. size_t,
  365. result->booleanOptions,
  366. optionIndex,
  367. result->booleanOptionsCount);
  368. __cargs_bool* srcBool = (__cargs_bool*)option.sourcePtr;
  369. PTR_NULLSAFE_SET(srcBool, __cargs_true);
  370. *nextPos = *nextPos + 1;
  371. result->booleanOptionsCount++;
  372. return;
  373. // TODO: REFACTOR THIS!!!
  374. shortProcessing:
  375. { }
  376. const size_t len = strlen(arg);
  377. if (len == 2) {
  378. __cargs_bool searchSuccess = __cargs_false;
  379. size_t optionIndex = searchOptionShort(context, arg[1], &searchSuccess);
  380. if (!searchSuccess) {
  381. ParseError error = {
  382. .category = OptionDoesntExist,
  383. .argIndex = pos,
  384. .optionShortName = NULLCHAR,
  385. };
  386. ADD_ELEM(ParseError, result->errors, error, result->errorsCount);
  387. result->errorsCount++;
  388. *nextPos = *nextPos + 1;
  389. return;
  390. }
  391. OptionData option = items[optionIndex];
  392. if (option.type != Boolean) {
  393. processNonBooleanArgs(
  394. context, result, optionIndex,
  395. &option, argc, argv, pos, nextPos);
  396. return;
  397. }
  398. ADD_ELEM(
  399. size_t,
  400. result->booleanOptions,
  401. optionIndex,
  402. result->booleanOptionsCount);
  403. __cargs_bool* srcBool = (__cargs_bool*)option.sourcePtr;
  404. PTR_NULLSAFE_SET(srcBool, __cargs_true);
  405. *nextPos = *nextPos + 1;
  406. }
  407. else {
  408. // args should be boolean
  409. for (size_t i = 1; i < len; i++) {
  410. __cargs_bool charSuccess = __cargs_false;
  411. const size_t index = searchOptionShort(context, arg[i], &charSuccess);
  412. if (!charSuccess) {
  413. ParseError error = {
  414. .category = OptionDoesntExist,
  415. .argIndex = pos,
  416. .optionShortName = arg[i],
  417. };
  418. ADD_ELEM(
  419. ParseError,
  420. result->errors,
  421. error,
  422. result->errorsCount);
  423. result->errorsCount++;
  424. continue;
  425. }
  426. OptionData option = items[index];
  427. if (option.type != Boolean) {
  428. ParseError error = {
  429. .category = CombinedOptionsShouldBeBoolean,
  430. .argIndex = pos,
  431. .optionShortName = option.shortOption,
  432. };
  433. ADD_ELEM(
  434. ParseError,
  435. result->errors,
  436. error,
  437. result->errorsCount);
  438. result->errorsCount++;
  439. continue;
  440. }
  441. ADD_ELEM(
  442. size_t,
  443. result->booleanOptions,
  444. optionIndex,
  445. result->booleanOptionsCount);
  446. __cargs_bool* srcBool = (__cargs_bool*)option.sourcePtr;
  447. PTR_NULLSAFE_SET(srcBool, __cargs_true);
  448. }
  449. *nextPos = *nextPos + 1;
  450. }
  451. }
  452. const __cargs_bool fprintErrors(
  453. FILE* file,
  454. ParseContext* context,
  455. ParseResult* result,
  456. const int argc,
  457. char* argv[])
  458. {
  459. ParseError* errors = result->errors;
  460. __cargs_bool status = __cargs_false;
  461. for (size_t i = 0; i < result->errorsCount; i++) {
  462. const ParseError error = errors[i];
  463. const ParseErrorCategory cat = error.category;
  464. char* stringFormat;
  465. switch (cat) {
  466. case OptionDoesntExist:
  467. stringFormat = "%s: Unrecognized option '%s'\n";
  468. break;
  469. case NonBooleanOptionDoesntHaveArgument:
  470. stringFormat = "%s: Option '%s' requires an argument\n";
  471. break;
  472. case CombinedOptionsShouldBeBoolean:
  473. stringFormat = "%s: Option '%s' is not boolean so cannot be combined\n";
  474. break;
  475. case IntegerParsingError:
  476. stringFormat = "%s: Unable to cast '%s' argument value to integer\n";
  477. break;
  478. case DecimalParsingError:
  479. stringFormat = "%s: Unable to cast '%s' argument value to decimal\n";
  480. break;
  481. }
  482. if (error.optionShortName == NULLCHAR) {
  483. fprintf(file, stringFormat, argv[0], argv[error.argIndex]);
  484. }
  485. else {
  486. char argName[4];
  487. sprintf(OUT argName, "-%c", error.optionShortName);
  488. fprintf(file, stringFormat, argv[0], argName);
  489. }
  490. status = __cargs_true;
  491. }
  492. return status;
  493. }
  494. const __cargs_bool printErrors(
  495. ParseContext* context,
  496. ParseResult* result,
  497. const int argc,
  498. char* argv[])
  499. {
  500. return fprintErrors(stderr, context, result, argc, argv);
  501. }
  502. void fprintHelp(FILE* file, ParseContext* context)
  503. {
  504. OptionData* options = context->items;
  505. size_t maxLongNameLen = 0;
  506. for (size_t i = 0; i < context->itemsCount; i++) {
  507. const OptionData option = options[i];
  508. if (option.longOption == NULL) {
  509. continue;
  510. }
  511. const size_t longNameLen = strlen(option.longOption);
  512. if (longNameLen > maxLongNameLen) {
  513. maxLongNameLen = longNameLen;
  514. }
  515. }
  516. maxLongNameLen += 2; // --optionName
  517. char format[64];
  518. sprintf(format, " %%s%%c %%%zus %%s\n", maxLongNameLen); // %zu - print size_t
  519. size_t nextLinesWhitespaceLength =
  520. + 2 // 2 spaces
  521. + 2 // smallName (-s)
  522. + 1 // delimiter
  523. + 1 // 1 space
  524. + maxLongNameLen // longName (--long)
  525. + 4; // 4 spaces
  526. char nextLineFormat[64];
  527. sprintf(nextLineFormat, "%%%zus%%s\n", nextLinesWhitespaceLength + 2);
  528. char* longName = ALLOCATE(char, maxLongNameLen + 2);
  529. for (size_t i = 0; i < context->itemsCount; i++) {
  530. const OptionData option = options[i];
  531. char delimiter = ' ';
  532. char shortName[4];
  533. if (option.shortOption != NULLCHAR && option.longOption != NULL) {
  534. delimiter = ',';
  535. }
  536. if (option.shortOption == NULLCHAR) {
  537. sprintf(shortName, " ");
  538. }
  539. else {
  540. sprintf(shortName, "-%c", option.shortOption);
  541. }
  542. if (option.longOption == NULL) {
  543. sprintf(longName, " ");
  544. }
  545. else {
  546. sprintf(longName, "--%s", option.longOption);
  547. }
  548. char* description;
  549. if (option.description == NULL) {
  550. description = " ";
  551. }
  552. else {
  553. description = option.description;
  554. }
  555. size_t descriptionLength = strlen(description);
  556. // required for strtok_r
  557. char* dynDescription = ALLOCATE(char, descriptionLength + 2);
  558. strcpy(dynDescription, description);
  559. char* tokSavePtr;
  560. char* delim = "\n";
  561. char* token = strtok_r(dynDescription, delim, &tokSavePtr);
  562. if (token == NULL) {
  563. token = "";
  564. }
  565. fprintf(file, format, shortName, delimiter, longName, token);
  566. while (__cargs_true) {
  567. token = strtok_r(NULL, delim, &tokSavePtr);
  568. if (token == NULL) {
  569. break;
  570. }
  571. fprintf(file, nextLineFormat, " ", token);
  572. }
  573. free(dynDescription);
  574. }
  575. free(longName);
  576. }
  577. void printHelp(ParseContext* context)
  578. {
  579. fprintHelp(stdout, context);
  580. }