FreeRDP
Loading...
Searching...
No Matches
winpr/libwinpr/utils/cmdline.c
1
20#include <winpr/config.h>
21
22#include <winpr/crt.h>
23#include <winpr/assert.h>
24#include <winpr/cmdline.h>
25
26#include "../log.h"
27
28#define TAG WINPR_TAG("commandline")
29
50#if !defined(WITH_DEBUG_UTILS_CMDLINE_DUMP)
51static const char censoredmessage[] =
52 "<censored: build with -DWITH_DEBUG_UTILS_CMDLINE_DUMP=ON for details>";
53#endif
54
55#define log_error(flags, msg, index, arg) \
56 log_error_((flags), (msg), (index), (arg), __FILE__, __func__, __LINE__)
57static void log_error_(DWORD flags, LPCSTR message, int index, WINPR_ATTR_UNUSED LPCSTR argv,
58 const char* file, const char* fkt, size_t line)
59{
60 if ((flags & COMMAND_LINE_SILENCE_PARSER) == 0)
61 {
62 const DWORD level = WLOG_ERROR;
63 static wLog* log = NULL;
64 if (!log)
65 log = WLog_Get(TAG);
66
67 if (!WLog_IsLevelActive(log, level))
68 return;
69
70 WLog_PrintTextMessage(log, level, line, file, fkt, "Failed at index %d [%s]: %s", index,
71#if defined(WITH_DEBUG_UTILS_CMDLINE_DUMP)
72 argv
73#else
74 censoredmessage
75#endif
76 ,
77 message);
78 }
79}
80
81#define log_comma_error(msg, arg) log_comma_error_((msg), (arg), __FILE__, __func__, __LINE__)
82static void log_comma_error_(const char* message, WINPR_ATTR_UNUSED const char* argument,
83 const char* file, const char* fkt, size_t line)
84{
85 const DWORD level = WLOG_ERROR;
86 static wLog* log = NULL;
87 if (!log)
88 log = WLog_Get(TAG);
89
90 if (!WLog_IsLevelActive(log, level))
91 return;
92
93 WLog_PrintTextMessage(log, level, line, file, fkt, "%s [%s]", message,
94#if defined(WITH_DEBUG_UTILS_CMDLINE_DUMP)
95 argument
96#else
97 censoredmessage
98#endif
99 );
100}
101
102int CommandLineParseArgumentsA(int argc, LPSTR* argv, COMMAND_LINE_ARGUMENT_A* options, DWORD flags,
103 void* context, COMMAND_LINE_PRE_FILTER_FN_A preFilter,
104 COMMAND_LINE_POST_FILTER_FN_A postFilter)
105{
106 int status = 0;
107 int count = 0;
108 BOOL notescaped = FALSE;
109 const char* sigil = NULL;
110 size_t sigil_length = 0;
111 char* keyword = NULL;
112 size_t keyword_index = 0;
113 char* separator = NULL;
114 char* value = NULL;
115 int toggle = 0;
116
117 if (!argv)
118 return status;
119
120 if (argc == 1)
121 {
122 if (flags & COMMAND_LINE_IGN_UNKNOWN_KEYWORD)
123 status = 0;
124 else
125 status = COMMAND_LINE_STATUS_PRINT_HELP;
126
127 return status;
128 }
129
130 for (int i = 1; i < argc; i++)
131 {
132 size_t keyword_length = 0;
133 BOOL found = FALSE;
134 BOOL escaped = TRUE;
135
136 if (preFilter)
137 {
138 count = preFilter(context, i, argc, argv);
139
140 if (count < 0)
141 {
142 log_error(flags, "PreFilter rule could not be applied", i, argv[i]);
143 status = COMMAND_LINE_ERROR;
144 return status;
145 }
146
147 if (count > 0)
148 {
149 i += (count - 1);
150 continue;
151 }
152 }
153
154 sigil = argv[i];
155 size_t length = strlen(argv[i]);
156
157 if ((sigil[0] == '/') && (flags & COMMAND_LINE_SIGIL_SLASH))
158 {
159 sigil_length = 1;
160 }
161 else if ((sigil[0] == '-') && (flags & COMMAND_LINE_SIGIL_DASH))
162 {
163 sigil_length = 1;
164
165 if (length > 2)
166 {
167 if ((sigil[1] == '-') && (flags & COMMAND_LINE_SIGIL_DOUBLE_DASH))
168 sigil_length = 2;
169 }
170 }
171 else if ((sigil[0] == '+') && (flags & COMMAND_LINE_SIGIL_PLUS_MINUS))
172 {
173 sigil_length = 1;
174 }
175 else if ((sigil[0] == '-') && (flags & COMMAND_LINE_SIGIL_PLUS_MINUS))
176 {
177 sigil_length = 1;
178 }
179 else if (flags & COMMAND_LINE_SIGIL_NONE)
180 {
181 sigil_length = 0;
182 }
183 else if (flags & COMMAND_LINE_SIGIL_NOT_ESCAPED)
184 {
185 if (notescaped)
186 {
187 log_error(flags, "Unescaped sigil", i, argv[i]);
188 return COMMAND_LINE_ERROR;
189 }
190
191 sigil_length = 0;
192 escaped = FALSE;
193 notescaped = TRUE;
194 }
195 else
196 {
197 log_error(flags, "Invalid sigil", i, argv[i]);
198 return COMMAND_LINE_ERROR;
199 }
200
201 if ((sigil_length > 0) || (flags & COMMAND_LINE_SIGIL_NONE) ||
202 (flags & COMMAND_LINE_SIGIL_NOT_ESCAPED))
203 {
204 if (length < (sigil_length + 1))
205 {
206 if ((flags & COMMAND_LINE_IGN_UNKNOWN_KEYWORD))
207 continue;
208
209 log_error(flags, "Unexpected keyword", i, argv[i]);
210 return COMMAND_LINE_ERROR_NO_KEYWORD;
211 }
212
213 keyword_index = sigil_length;
214 keyword = &argv[i][keyword_index];
215 toggle = -1;
216
217 if (flags & COMMAND_LINE_SIGIL_ENABLE_DISABLE)
218 {
219 if (strncmp(keyword, "enable-", 7) == 0)
220 {
221 toggle = TRUE;
222 keyword_index += 7;
223 keyword = &argv[i][keyword_index];
224 }
225 else if (strncmp(keyword, "disable-", 8) == 0)
226 {
227 toggle = FALSE;
228 keyword_index += 8;
229 keyword = &argv[i][keyword_index];
230 }
231 }
232
233 separator = NULL;
234
235 if ((flags & COMMAND_LINE_SEPARATOR_COLON) && (!separator))
236 separator = strchr(keyword, ':');
237
238 if ((flags & COMMAND_LINE_SEPARATOR_EQUAL) && (!separator))
239 separator = strchr(keyword, '=');
240
241 if (separator)
242 {
243 SSIZE_T separator_index = (separator - argv[i]);
244 SSIZE_T value_index = separator_index + 1;
245 keyword_length = WINPR_ASSERTING_INT_CAST(size_t, (separator - keyword));
246 value = &argv[i][value_index];
247 }
248 else
249 {
250 if (length < keyword_index)
251 {
252 log_error(flags, "Argument required", i, argv[i]);
253 return COMMAND_LINE_ERROR;
254 }
255
256 keyword_length = length - keyword_index;
257 value = NULL;
258 }
259
260 if (!escaped)
261 continue;
262
263 for (size_t j = 0; options[j].Name != NULL; j++)
264 {
265 COMMAND_LINE_ARGUMENT_A* cur = &options[j];
266 BOOL match = FALSE;
267
268 if (strncmp(cur->Name, keyword, keyword_length) == 0)
269 {
270 if (strlen(cur->Name) == keyword_length)
271 match = TRUE;
272 }
273
274 if ((!match) && (cur->Alias != NULL))
275 {
276 if (strncmp(cur->Alias, keyword, keyword_length) == 0)
277 {
278 if (strlen(cur->Alias) == keyword_length)
279 match = TRUE;
280 }
281 }
282
283 if (!match)
284 continue;
285
286 found = match;
287 cur->Index = i;
288
289 if ((flags & COMMAND_LINE_SEPARATOR_SPACE) && ((i + 1) < argc))
290 {
291 BOOL argument = 0;
292 int value_present = 1;
293
294 if (flags & COMMAND_LINE_SIGIL_DASH)
295 {
296 if (strncmp(argv[i + 1], "-", 1) == 0)
297 value_present = 0;
298 }
299
300 if (flags & COMMAND_LINE_SIGIL_DOUBLE_DASH)
301 {
302 if (strncmp(argv[i + 1], "--", 2) == 0)
303 value_present = 0;
304 }
305
306 if (flags & COMMAND_LINE_SIGIL_SLASH)
307 {
308 if (strncmp(argv[i + 1], "/", 1) == 0)
309 value_present = 0;
310 }
311
312 if ((cur->Flags & COMMAND_LINE_VALUE_REQUIRED) ||
313 (cur->Flags & COMMAND_LINE_VALUE_OPTIONAL))
314 argument = TRUE;
315 else
316 argument = FALSE;
317
318 if (value_present && argument)
319 {
320 i++;
321 value = argv[i];
322 }
323 else if (!value_present && (cur->Flags & COMMAND_LINE_VALUE_OPTIONAL))
324 {
325 value = NULL;
326 }
327 else if (!value_present && argument)
328 {
329 log_error(flags, "Argument required", i, argv[i]);
330 return COMMAND_LINE_ERROR;
331 }
332 }
333
334 if (!(flags & COMMAND_LINE_SEPARATOR_SPACE))
335 {
336 if (value && (cur->Flags & COMMAND_LINE_VALUE_FLAG))
337 {
338 log_error(flags, "Unexpected value", i, argv[i]);
339 return COMMAND_LINE_ERROR_UNEXPECTED_VALUE;
340 }
341 }
342 else
343 {
344 if (value && (cur->Flags & COMMAND_LINE_VALUE_FLAG))
345 {
346 i--;
347 value = NULL;
348 }
349 }
350
351 if (!value && (cur->Flags & COMMAND_LINE_VALUE_REQUIRED))
352 {
353 log_error(flags, "Missing value", i, argv[i]);
354 status = COMMAND_LINE_ERROR_MISSING_VALUE;
355 return status;
356 }
357
358 cur->Flags |= COMMAND_LINE_ARGUMENT_PRESENT;
359
360 if (value)
361 {
362 if (!(cur->Flags & (COMMAND_LINE_VALUE_OPTIONAL | COMMAND_LINE_VALUE_REQUIRED)))
363 {
364 log_error(flags, "Unexpected value", i, argv[i]);
365 return COMMAND_LINE_ERROR_UNEXPECTED_VALUE;
366 }
367
368 cur->Value = value;
369 cur->Flags |= COMMAND_LINE_VALUE_PRESENT;
370 }
371 else
372 {
373 if (cur->Flags & COMMAND_LINE_VALUE_FLAG)
374 {
375 cur->Value = (LPSTR)1;
376 cur->Flags |= COMMAND_LINE_VALUE_PRESENT;
377 }
378 else if (cur->Flags & COMMAND_LINE_VALUE_BOOL)
379 {
380 if (flags & COMMAND_LINE_SIGIL_ENABLE_DISABLE)
381 {
382 if (toggle == -1)
383 cur->Value = BoolValueTrue;
384 else if (!toggle)
385 cur->Value = BoolValueFalse;
386 else
387 cur->Value = BoolValueTrue;
388 }
389 else
390 {
391 if (sigil[0] == '+')
392 cur->Value = BoolValueTrue;
393 else if (sigil[0] == '-')
394 cur->Value = BoolValueFalse;
395 else
396 cur->Value = BoolValueTrue;
397 }
398
399 cur->Flags |= COMMAND_LINE_VALUE_PRESENT;
400 }
401 }
402
403 if (postFilter)
404 {
405 count = postFilter(context, &options[j]);
406
407 if (count < 0)
408 {
409 log_error(flags, "PostFilter rule could not be applied", i, argv[i]);
410 status = COMMAND_LINE_ERROR;
411 return status;
412 }
413 }
414
415 if (cur->Flags & COMMAND_LINE_PRINT)
416 return COMMAND_LINE_STATUS_PRINT;
417 else if (cur->Flags & COMMAND_LINE_PRINT_HELP)
418 return COMMAND_LINE_STATUS_PRINT_HELP;
419 else if (cur->Flags & COMMAND_LINE_PRINT_VERSION)
420 return COMMAND_LINE_STATUS_PRINT_VERSION;
421 else if (cur->Flags & COMMAND_LINE_PRINT_BUILDCONFIG)
422 return COMMAND_LINE_STATUS_PRINT_BUILDCONFIG;
423 }
424
425 if (!found && (flags & COMMAND_LINE_IGN_UNKNOWN_KEYWORD) == 0)
426 {
427 log_error(flags, "Unexpected keyword", i, argv[i]);
428 return COMMAND_LINE_ERROR_NO_KEYWORD;
429 }
430 }
431 }
432
433 return status;
434}
435
436int CommandLineParseArgumentsW(WINPR_ATTR_UNUSED int argc, WINPR_ATTR_UNUSED LPWSTR* argv,
437 WINPR_ATTR_UNUSED COMMAND_LINE_ARGUMENT_W* options,
438 WINPR_ATTR_UNUSED DWORD flags, WINPR_ATTR_UNUSED void* context,
439 WINPR_ATTR_UNUSED COMMAND_LINE_PRE_FILTER_FN_W preFilter,
440 WINPR_ATTR_UNUSED COMMAND_LINE_POST_FILTER_FN_W postFilter)
441{
442 WLog_ERR("TODO", "TODO: implement");
443 return 0;
444}
445
446int CommandLineClearArgumentsA(COMMAND_LINE_ARGUMENT_A* options)
447{
448 for (size_t i = 0; options[i].Name != NULL; i++)
449 {
450 options[i].Flags &= COMMAND_LINE_INPUT_FLAG_MASK;
451 options[i].Value = NULL;
452 }
453
454 return 0;
455}
456
457int CommandLineClearArgumentsW(COMMAND_LINE_ARGUMENT_W* options)
458{
459 for (int i = 0; options[i].Name != NULL; i++)
460 {
461 options[i].Flags &= COMMAND_LINE_INPUT_FLAG_MASK;
462 options[i].Value = NULL;
463 }
464
465 return 0;
466}
467
468const COMMAND_LINE_ARGUMENT_A* CommandLineFindArgumentA(const COMMAND_LINE_ARGUMENT_A* options,
469 LPCSTR Name)
470{
471 WINPR_ASSERT(options);
472 WINPR_ASSERT(Name);
473
474 for (size_t i = 0; options[i].Name != NULL; i++)
475 {
476 if (strcmp(options[i].Name, Name) == 0)
477 return &options[i];
478
479 if (options[i].Alias != NULL)
480 {
481 if (strcmp(options[i].Alias, Name) == 0)
482 return &options[i];
483 }
484 }
485
486 return NULL;
487}
488
489const COMMAND_LINE_ARGUMENT_W* CommandLineFindArgumentW(const COMMAND_LINE_ARGUMENT_W* options,
490 LPCWSTR Name)
491{
492 WINPR_ASSERT(options);
493 WINPR_ASSERT(Name);
494
495 for (size_t i = 0; options[i].Name != NULL; i++)
496 {
497 if (_wcscmp(options[i].Name, Name) == 0)
498 return &options[i];
499
500 if (options[i].Alias != NULL)
501 {
502 if (_wcscmp(options[i].Alias, Name) == 0)
503 return &options[i];
504 }
505 }
506
507 return NULL;
508}
509
510const COMMAND_LINE_ARGUMENT_A* CommandLineFindNextArgumentA(const COMMAND_LINE_ARGUMENT_A* argument)
511{
512 const COMMAND_LINE_ARGUMENT_A* nextArgument = NULL;
513
514 if (!argument || !argument->Name)
515 return NULL;
516
517 nextArgument = &argument[1];
518
519 if (nextArgument->Name == NULL)
520 return NULL;
521
522 return nextArgument;
523}
524
525static int is_quoted(char c)
526{
527 switch (c)
528 {
529 case '"':
530 return 1;
531 case '\'':
532 return -1;
533 default:
534 return 0;
535 }
536}
537
538static size_t get_element_count(const char* list, BOOL* failed, BOOL fullquoted)
539{
540 size_t count = 0;
541 int quoted = 0;
542 bool escaped = false;
543 BOOL finished = FALSE;
544 BOOL first = TRUE;
545 const char* it = list;
546
547 if (!list)
548 return 0;
549 if (strlen(list) == 0)
550 return 0;
551
552 while (!finished)
553 {
554 BOOL nextFirst = FALSE;
555
556 const char cur = *it++;
557
558 /* Ignore the symbol that was escaped. */
559 if (escaped)
560 {
561 escaped = false;
562 continue;
563 }
564
565 switch (cur)
566 {
567 case '\0':
568 if (quoted != 0)
569 {
570 log_comma_error("Invalid argument (missing closing quote)", list);
571 *failed = TRUE;
572 return 0;
573 }
574 finished = TRUE;
575 break;
576 case '\\':
577 if (!escaped)
578 {
579 escaped = true;
580 continue;
581 }
582 break;
583 case '\'':
584 case '"':
585 if (!fullquoted)
586 {
587 int now = is_quoted(cur) && !escaped;
588 if (now == quoted)
589 quoted = 0;
590 else if (quoted == 0)
591 quoted = now;
592 }
593 break;
594 case ',':
595 if (first)
596 {
597 log_comma_error("Invalid argument (empty list elements)", list);
598 *failed = TRUE;
599 return 0;
600 }
601 if (quoted == 0)
602 {
603 nextFirst = TRUE;
604 count++;
605 }
606 break;
607 default:
608 break;
609 }
610
611 first = nextFirst;
612 }
613 return count + 1;
614}
615
616static char* get_next_comma(char* string, BOOL fullquoted)
617{
618 const char* log = string;
619 int quoted = 0;
620 bool first = true;
621 bool escaped = false;
622
623 WINPR_ASSERT(string);
624
625 while (TRUE)
626 {
627 char* last = string;
628 const char cur = *string++;
629 if (escaped)
630 {
631 escaped = false;
632 continue;
633 }
634
635 switch (cur)
636 {
637 case '\0':
638 if (quoted != 0)
639 log_comma_error("Invalid quoted argument", log);
640 return NULL;
641
642 case '\\':
643 if (!escaped)
644 {
645 escaped = true;
646 continue;
647 }
648 break;
649 case '\'':
650 case '"':
651 if (!fullquoted)
652 {
653 int now = is_quoted(cur);
654 if ((quoted == 0) && !first)
655 {
656 log_comma_error("Invalid quoted argument", log);
657 return NULL;
658 }
659 if (now == quoted)
660 quoted = 0;
661 else if (quoted == 0)
662 quoted = now;
663 }
664 break;
665
666 case ',':
667 if (first)
668 {
669 log_comma_error("Invalid argument (empty list elements)", log);
670 return NULL;
671 }
672 if (quoted == 0)
673 return last;
674 break;
675
676 default:
677 break;
678 }
679 first = FALSE;
680 }
681
682 return NULL;
683}
684
685static BOOL is_valid_fullquoted(const char* string)
686{
687 char cur = '\0';
688 char last = '\0';
689 const char quote = *string++;
690
691 /* We did not start with a quote. */
692 if (is_quoted(quote) == 0)
693 return FALSE;
694
695 while ((cur = *string++) != '\0')
696 {
697 /* A quote is found. */
698 if (cur == quote)
699 {
700 /* If the quote was escaped, it is valid. */
701 if (last != '\\')
702 {
703 /* Only allow unescaped quote as last character in string. */
704 if (*string != '\0')
705 return FALSE;
706 }
707 /* If the last quote in the string is escaped, it is wrong. */
708 else if (*string != '\0')
709 return FALSE;
710 }
711 last = cur;
712 }
713
714 /* The string did not terminate with the same quote as it started. */
715 if (last != quote)
716 return FALSE;
717 return TRUE;
718}
719
720char** CommandLineParseCommaSeparatedValuesEx(const char* name, const char* list, size_t* count)
721{
722 char** p = NULL;
723 char* str = NULL;
724 size_t nArgs = 0;
725 size_t prefix = 0;
726 size_t len = 0;
727 size_t namelen = 0;
728 BOOL failed = FALSE;
729 char* copy = NULL;
730 char* unquoted = NULL;
731 BOOL fullquoted = FALSE;
732
733 BOOL success = FALSE;
734 if (count == NULL)
735 goto fail;
736
737 *count = 0;
738 if (list)
739 {
740 int start = 0;
741 int end = 0;
742 unquoted = copy = _strdup(list);
743 if (!copy)
744 goto fail;
745
746 len = strlen(unquoted);
747 if (len > 0)
748 {
749 start = is_quoted(unquoted[0]);
750 end = is_quoted(unquoted[len - 1]);
751
752 if ((start != 0) && (end != 0))
753 {
754 if (start != end)
755 {
756 log_comma_error("Invalid argument (quote mismatch)", list);
757 goto fail;
758 }
759 if (!is_valid_fullquoted(unquoted))
760 goto fail;
761 unquoted[len - 1] = '\0';
762 unquoted++;
763 len -= 2;
764 fullquoted = TRUE;
765 }
766 }
767 }
768
769 *count = get_element_count(unquoted, &failed, fullquoted);
770 if (failed)
771 goto fail;
772
773 if (*count == 0)
774 {
775 if (!name)
776 goto fail;
777 else
778 {
779 size_t clen = strlen(name);
780 p = (char**)calloc(2UL + clen, sizeof(char*));
781
782 if (p)
783 {
784 char* dst = (char*)&p[1];
785 p[0] = dst;
786 (void)sprintf_s(dst, clen + 1, "%s", name);
787 *count = 1;
788 success = TRUE;
789 goto fail;
790 }
791 }
792 }
793
794 nArgs = *count;
795
796 if (name)
797 nArgs++;
798
799 prefix = (nArgs + 1UL) * sizeof(char*);
800 if (name)
801 namelen = strlen(name);
802 p = (char**)calloc(len + prefix + 1 + namelen + 1, sizeof(char*));
803
804 if (!p)
805 goto fail;
806
807 str = &((char*)p)[prefix];
808 memcpy(str, unquoted, len);
809
810 if (name)
811 {
812 char* namestr = &((char*)p)[prefix + len + 1];
813 memcpy(namestr, name, namelen);
814
815 p[0] = namestr;
816 }
817
818 for (size_t index = name ? 1 : 0; index < nArgs; index++)
819 {
820 char* ptr = str;
821 const int quote = is_quoted(*ptr);
822 char* comma = get_next_comma(str, fullquoted);
823
824 if ((quote != 0) && !fullquoted)
825 ptr++;
826
827 p[index] = ptr;
828
829 if (comma)
830 {
831 char* last = comma - 1;
832 const int lastQuote = is_quoted(*last);
833
834 if (!fullquoted)
835 {
836 if (lastQuote != quote)
837 {
838 log_comma_error("Invalid argument (quote mismatch)", list);
839 goto fail;
840 }
841 else if (lastQuote != 0)
842 *last = '\0';
843 }
844 *comma = '\0';
845
846 str = comma + 1;
847 }
848 else if (quote)
849 {
850 char* end = strrchr(ptr, '"');
851 if (!end)
852 goto fail;
853 *end = '\0';
854 }
855 }
856
857 *count = nArgs;
858 success = TRUE;
859fail:
860 free(copy);
861 if (!success)
862 {
863 if (count)
864 *count = 0;
865 free((void*)p);
866 return NULL;
867 }
868 return p;
869}
870
871char** CommandLineParseCommaSeparatedValues(const char* list, size_t* count)
872{
873 return CommandLineParseCommaSeparatedValuesEx(NULL, list, count);
874}
875
876char* CommandLineToCommaSeparatedValues(int argc, char* argv[])
877{
878 return CommandLineToCommaSeparatedValuesEx(argc, argv, NULL, 0);
879}
880
881static const char* filtered(const char* arg, const char* filters[], size_t number)
882{
883 if (number == 0)
884 return arg;
885 for (size_t x = 0; x < number; x++)
886 {
887 const char* filter = filters[x];
888 size_t len = strlen(filter);
889 if (_strnicmp(arg, filter, len) == 0)
890 return &arg[len];
891 }
892 return NULL;
893}
894
895char* CommandLineToCommaSeparatedValuesEx(int argc, char* argv[], const char* filters[],
896 size_t number)
897{
898 char* str = NULL;
899 size_t offset = 0;
900 size_t size = WINPR_ASSERTING_INT_CAST(size_t, argc) + 1;
901 if ((argc <= 0) || !argv)
902 return NULL;
903
904 for (int x = 0; x < argc; x++)
905 size += strlen(argv[x]);
906
907 str = calloc(size, sizeof(char));
908 if (!str)
909 return NULL;
910 for (int x = 0; x < argc; x++)
911 {
912 int rc = 0;
913 const char* arg = filtered(argv[x], filters, number);
914 if (!arg)
915 continue;
916 rc = _snprintf(&str[offset], size - offset, "%s,", arg);
917 if (rc <= 0)
918 {
919 free(str);
920 return NULL;
921 }
922 offset += (size_t)rc;
923 }
924 if (offset > 0)
925 str[offset - 1] = '\0';
926 return str;
927}
928
929void CommandLineParserFree(char** ptr)
930{
931 union
932 {
933 char* p;
934 char** pp;
935 } uptr;
936 uptr.pp = ptr;
937 free(uptr.p);
938}