From 785c26b84431eb8556a79a76f3fb50cdc7907db7 Mon Sep 17 00:00:00 2001 From: Nikita Chulkov Date: Mon, 31 Aug 2026 16:33:02 +1100 Subject: [PATCH 01/18] fix: fixed crash when flag name is logner than the buffer --- optly.h | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/optly.h b/optly.h index 33e81dc..abc8446 100644 --- a/optly.h +++ b/optly.h @@ -466,6 +466,7 @@ OPTLYDEF uint32_t optly_flag_value_uint32(const OptlyCommand *command, c OPTLYDEF uint64_t optly_flag_value_uint64(const OptlyCommand *command, const char *name); OPTLYDEF float optly_flag_value_float(const OptlyCommand *command, const char *name); OPTLYDEF double optly_flag_value_double(const OptlyCommand *command, const char *name); +OPTLYDEF char *optly_flag_value_enum(const OptlyCommand *command, const char *name); OPTLYDEF OptlyPositional *optly_get_positional(OptlyCommand *command, const char *name); #endif // OPTLY_H @@ -960,7 +961,16 @@ static void optly_parse_long_flags(char ***argv_ptr, int *argc_ptr, OptlyFlag *f tmp[len] = '\0'; char *eq2 = strchr(tmp, '='); - *eq2 = '\0'; + + // NOTE: a flag name longer than the buffer is truncated before its '=', + // so the copy may not contain one even though the argument did. + if (!eq2) { + OPTLY_LOG(WARN, "Unknown flag: %s", *argv); + optly_push_error(errs, OPTLY_ERR_UNKNOWN_FLAG, *argv); + return; + } + + *eq2 = '\0'; arg = tmp; value = eq + 1; From 50d3973565ed09be1b7f2e7e22103072c9b5708d Mon Sep 17 00:00:00 2001 From: Nikita Chulkov Date: Mon, 31 Aug 2026 16:33:21 +1100 Subject: [PATCH 02/18] test: introduced tspec tests --- .gitignore | 1 + tests/command_errors/command_errors.c | 29 ++++++ tests/command_errors/test.tspec | 53 ++++++++++ tests/commands/commands.c | 55 ++++++++++ tests/commands/test.tspec | 110 ++++++++++++++++++++ tests/enums/enums.c | 26 +++++ tests/enums/test.tspec | 107 +++++++++++++++++++ tests/errors/errors.c | 33 ++++++ tests/errors/test.tspec | 142 ++++++++++++++++++++++++++ tests/exit_behaviour/exit_behaviour.c | 18 ++++ tests/exit_behaviour/test.tspec | 33 ++++++ tests/flags/flags.c | 26 +++++ tests/flags/test.tspec | 138 +++++++++++++++++++++++++ tests/long_flags/long_flags.c | 22 ++++ tests/long_flags/test.tspec | 34 ++++++ tests/positionals/positionals.c | 33 ++++++ tests/positionals/test.tspec | 78 ++++++++++++++ tests/typed_values/test.tspec | 41 ++++++++ tests/typed_values/typed_values.c | 39 +++++++ tests/usage/test.tspec | 28 +++++ tests/usage/usage.c | 30 ++++++ 21 files changed, 1076 insertions(+) create mode 100644 tests/command_errors/command_errors.c create mode 100644 tests/command_errors/test.tspec create mode 100644 tests/commands/commands.c create mode 100644 tests/commands/test.tspec create mode 100644 tests/enums/enums.c create mode 100644 tests/enums/test.tspec create mode 100644 tests/errors/errors.c create mode 100644 tests/errors/test.tspec create mode 100644 tests/exit_behaviour/exit_behaviour.c create mode 100644 tests/exit_behaviour/test.tspec create mode 100644 tests/flags/flags.c create mode 100644 tests/flags/test.tspec create mode 100644 tests/long_flags/long_flags.c create mode 100644 tests/long_flags/test.tspec create mode 100644 tests/positionals/positionals.c create mode 100644 tests/positionals/test.tspec create mode 100644 tests/typed_values/test.tspec create mode 100644 tests/typed_values/typed_values.c create mode 100644 tests/usage/test.tspec create mode 100644 tests/usage/usage.c diff --git a/.gitignore b/.gitignore index 885d70f..378c2d6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .cache compile_commands.json out +**/.side_effects/ diff --git a/tests/command_errors/command_errors.c b/tests/command_errors/command_errors.c new file mode 100644 index 0000000..0ac1349 --- /dev/null +++ b/tests/command_errors/command_errors.c @@ -0,0 +1,29 @@ +// A command tree with no positionals, so an unrecognised token is reported as +// an unknown command instead of being collected as a positional value. +#define OPTLY_NO_EXIT +#define OPTLY_IMPLEMENTATION +#include "optly.h" +#include + +int main(int argc, char **argv) { + OptlyCommand cmd = { + .name = "app", + .flags = optly_flags( + optly_flag_bool("verbose", 'v', "Verbose", .value.as_bool = false) + ), + .commands = optly_commands( + optly_command("run", "Run"), + optly_command("build", "Build") + ) + }; + + OptlyErrors errs = optly_parse_args(argc, argv, &cmd); + + printf("errors=%zu\n", optly_errors_count(&errs)); + for (size_t i = 0; i < optly_errors_count(&errs); i++) { + OptlyError e = optly_errors_at(&errs, i); + printf("%zu: %s (%s)\n", i, optly_error_message(e.kind), e.arg ? e.arg : ""); + } + printf("command=%s\n", cmd.next_command ? cmd.next_command->name : ""); + return 0; +} diff --git a/tests/command_errors/test.tspec b/tests/command_errors/test.tspec new file mode 100644 index 0000000..094b8ba --- /dev/null +++ b/tests/command_errors/test.tspec @@ -0,0 +1,53 @@ +:test an_unknown_token_is_reported_as_an_unknown_command +:command compile +:blob executable 2 +cc +:blob args 97 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. command_errors.c -o .side_effects/command_errors +:int return 0 +:command run +:blob executable 28 +.side_effects/command_errors +:blob args 4 +nope +:int return 0 +:blob stdout 50 +errors=1 +0: Unknown command (nope) +command= + +:test a_known_command_is_selected +:command compile +:blob executable 2 +cc +:blob args 97 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. command_errors.c -o .side_effects/command_errors +:int return 0 +:command run +:blob executable 28 +.side_effects/command_errors +:blob args 3 +run +:int return 0 +:blob stdout 21 +errors=0 +command=run + +:test an_unknown_subcommand_is_reported +:command compile +:blob executable 2 +cc +:blob args 97 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. command_errors.c -o .side_effects/command_errors +:int return 0 +:command run +:blob executable 28 +.side_effects/command_errors +:blob args 8 +run nope +:int return 0 +:blob stdout 47 +errors=1 +0: Unknown command (nope) +command=run + diff --git a/tests/commands/commands.c b/tests/commands/commands.c new file mode 100644 index 0000000..7c06eea --- /dev/null +++ b/tests/commands/commands.c @@ -0,0 +1,55 @@ +// A three-level command tree. Prints which command was selected and the flag +// values at each level, so command-local flags are visibly distinct from the +// global ones that share their name. +#define OPTLY_IMPLEMENTATION +#include "optly.h" +#include + +int main(int argc, char **argv) { + OptlyCommand cmd = { + .name = "app", + .flags = optly_flags( + optly_flag_bool("verbose", 'v', "Verbose", .value.as_bool = false) + ), + .commands = optly_commands( + optly_command("run", "Run", + .flags = optly_flags( + optly_flag_uint16("port", 'p', "Port", .value.as_uint16 = 8080), + optly_flag_bool("verbose", 'v', "Verbose", .value.as_bool = false) + ), + .commands = optly_commands( + optly_command("check", "Check", + .flags = optly_flags( + optly_flag_bool("strict", 's', "Strict", .value.as_bool = false) + ) + ) + ) + ), + optly_command("build", "Build") + ) + }; + + optly_parse_args(argc, argv, &cmd); + + printf("global.verbose=%d\n", optly_flag_value_bool(&cmd, "verbose")); + + OptlyCommand *c = cmd.next_command; + if (!c) { + printf("command=\n"); + return 0; + } + + printf("command=%s\n", c->name); + + if (optly_is_command(c, "run")) { + printf("run.port=%u\n", optly_flag_value_uint16(c, "port")); + printf("run.verbose=%d\n", optly_flag_value_bool(c, "verbose")); + } + + if (c->next_command) { + printf("subcommand=%s\n", c->next_command->name); + printf("check.strict=%d\n", optly_flag_value_bool(c->next_command, "strict")); + } + + return 0; +} diff --git a/tests/commands/test.tspec b/tests/commands/test.tspec new file mode 100644 index 0000000..df0eec1 --- /dev/null +++ b/tests/commands/test.tspec @@ -0,0 +1,110 @@ +:test no_command_leaves_next_command_null +:command compile +:blob executable 2 +cc +:blob args 85 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. commands.c -o .side_effects/commands +:int return 0 +:command run +:blob executable 22 +.side_effects/commands +:int return 0 +:blob stdout 32 +global.verbose=0 +command= + +:test a_command_is_selected_by_name +:command compile +:blob executable 2 +cc +:blob args 85 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. commands.c -o .side_effects/commands +:int return 0 +:command run +:blob executable 22 +.side_effects/commands +:blob args 5 +build +:int return 0 +:blob stdout 31 +global.verbose=0 +command=build + +:test command_flags_follow_the_command +:command compile +:blob executable 2 +cc +:blob args 85 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. commands.c -o .side_effects/commands +:int return 0 +:command run +:blob executable 22 +.side_effects/commands +:blob args 11 +run -p 9000 +:int return 0 +:blob stdout 57 +global.verbose=0 +command=run +run.port=9000 +run.verbose=0 + +:test a_global_flag_before_the_command_is_global +:command compile +:blob executable 2 +cc +:blob args 85 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. commands.c -o .side_effects/commands +:int return 0 +:command run +:blob executable 22 +.side_effects/commands +:blob args 13 +--verbose run +:int return 0 +:blob stdout 57 +global.verbose=1 +command=run +run.port=8080 +run.verbose=0 + +:test the_same_short_flag_binds_to_the_level_it_appears_at +:command compile +:blob executable 2 +cc +:blob args 85 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. commands.c -o .side_effects/commands +:int return 0 +:command run +:blob executable 22 +.side_effects/commands +:blob args 9 +-v run -v +:int return 0 +:blob stdout 57 +global.verbose=1 +command=run +run.port=8080 +run.verbose=1 + +:test subcommands_nest +:command compile +:blob executable 2 +cc +:blob args 85 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. commands.c -o .side_effects/commands +:int return 0 +:command run +:blob executable 22 +.side_effects/commands +:blob args 18 +run check --strict +:int return 0 +:blob stdout 89 +global.verbose=0 +command=run +run.port=8080 +run.verbose=0 +subcommand=check +check.strict=1 + diff --git a/tests/enums/enums.c b/tests/enums/enums.c new file mode 100644 index 0000000..f2d7ca6 --- /dev/null +++ b/tests/enums/enums.c @@ -0,0 +1,26 @@ +// Enum flags carry their allowed values inline; slot 0 holds the current one. +#define OPTLY_NO_EXIT +#define OPTLY_IMPLEMENTATION +#include "optly.h" +#include + +int main(int argc, char **argv) { + OptlyCommand cmd = { + .name = "app", + .flags = optly_flags( + optly_flag_enum("log", 'l', "Log level", optly_enum_values("verbose", "debug", "verbose", "warn")), + optly_flag_bool("force", 'f', "Force", .value.as_bool = false) + ) + }; + + OptlyErrors errs = optly_parse_args(argc, argv, &cmd); + + printf("errors=%zu\n", optly_errors_count(&errs)); + for (size_t i = 0; i < optly_errors_count(&errs); i++) { + OptlyError e = optly_errors_at(&errs, i); + printf("%zu: %s (%s)\n", i, optly_error_message(e.kind), e.arg ? e.arg : ""); + } + printf("log=%s\n", optly_flag_value_enum(&cmd, "log")); + printf("force=%d\n", optly_flag_value_bool(&cmd, "force")); + return 0; +} diff --git a/tests/enums/test.tspec b/tests/enums/test.tspec new file mode 100644 index 0000000..a81a642 --- /dev/null +++ b/tests/enums/test.tspec @@ -0,0 +1,107 @@ +:test the_default_is_used_when_the_flag_is_absent +:command compile +:blob executable 2 +cc +:blob args 79 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. enums.c -o .side_effects/enums +:int return 0 +:command run +:blob executable 19 +.side_effects/enums +:int return 0 +:blob stdout 29 +errors=0 +log=verbose +force=0 + +:test an_allowed_value_is_accepted +:command compile +:blob executable 2 +cc +:blob args 79 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. enums.c -o .side_effects/enums +:int return 0 +:command run +:blob executable 19 +.side_effects/enums +:blob args 10 +--log=warn +:int return 0 +:blob stdout 26 +errors=0 +log=warn +force=0 + +:test the_short_form_accepts_a_value +:command compile +:blob executable 2 +cc +:blob args 79 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. enums.c -o .side_effects/enums +:int return 0 +:command run +:blob executable 19 +.side_effects/enums +:blob args 8 +-l debug +:int return 0 +:blob stdout 27 +errors=0 +log=debug +force=0 + +:test a_value_outside_the_set_is_rejected +:command compile +:blob executable 2 +cc +:blob args 79 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. enums.c -o .side_effects/enums +:int return 0 +:command run +:blob executable 19 +.side_effects/enums +:blob args 10 +--log=nope +:int return 0 +:blob stdout 62 +errors=1 +0: Invalid value for flag (nope) +log=verbose +force=0 + +:test the_last_value_wins +:command compile +:blob executable 2 +cc +:blob args 79 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. enums.c -o .side_effects/enums +:int return 0 +:command run +:blob executable 19 +.side_effects/enums +:blob args 22 +--log=debug --log=warn +:int return 0 +:blob stdout 26 +errors=0 +log=warn +force=0 + +:test an_enum_flag_mixes_with_other_flags +:command compile +:blob executable 2 +cc +:blob args 79 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. enums.c -o .side_effects/enums +:int return 0 +:command run +:blob executable 19 +.side_effects/enums +:blob args 13 +-f --log=warn +:int return 0 +:blob stdout 26 +errors=0 +log=warn +force=1 + diff --git a/tests/errors/errors.c b/tests/errors/errors.c new file mode 100644 index 0000000..aa77cd0 --- /dev/null +++ b/tests/errors/errors.c @@ -0,0 +1,33 @@ +// Prints every accumulated error in order. OPTLY_NO_EXIT keeps the process +// alive so the whole list is observable; tests/exit_behaviour covers the +// default, which is to exit. +#define OPTLY_NO_EXIT +#define OPTLY_IMPLEMENTATION +#include "optly.h" +#include + +int main(int argc, char **argv) { + OptlyCommand cmd = { + .name = "app", + .flags = optly_flags( + optly_flag_string("token", 'T', "Token", .required = true), + optly_flag_uint32("threads", 't', "Threads", .value.as_uint32 = 4), + optly_flag_bool("verbose", 'v', "Verbose", .value.as_bool = false) + ), + .commands = optly_commands( + optly_command("run", "Run") + ), + .positionals = optly_positionals( + optly_positional("files", "Files", .min = 1, .max = 2) + ) + }; + + OptlyErrors errs = optly_parse_args(argc, argv, &cmd); + + printf("errors=%zu\n", optly_errors_count(&errs)); + for (size_t i = 0; i < optly_errors_count(&errs); i++) { + OptlyError e = optly_errors_at(&errs, i); + printf("%zu: %s (%s)\n", i, optly_error_message(e.kind), e.arg ? e.arg : ""); + } + return 0; +} diff --git a/tests/errors/test.tspec b/tests/errors/test.tspec new file mode 100644 index 0000000..c556867 --- /dev/null +++ b/tests/errors/test.tspec @@ -0,0 +1,142 @@ +:test unknown_flag_is_reported +:command compile +:blob executable 2 +cc +:blob args 81 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. errors.c -o .side_effects/errors +:int return 0 +:command run +:blob executable 20 +.side_effects/errors +:blob args 17 +-T x a.txt --nope +:int return 0 +:blob stdout 34 +errors=1 +0: Unknown flag (--nope) + +:test a_flag_without_its_value_is_reported +:command compile +:blob executable 2 +cc +:blob args 81 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. errors.c -o .side_effects/errors +:int return 0 +:command run +:blob executable 20 +.side_effects/errors +:blob args 20 +-T x a.txt --threads +:int return 0 +:blob stdout 44 +errors=1 +0: Flag requires a value (threads) + +:test a_value_that_is_not_a_number_is_reported +:command compile +:blob executable 2 +cc +:blob args 81 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. errors.c -o .side_effects/errors +:int return 0 +:command run +:blob executable 20 +.side_effects/errors +:blob args 24 +-T x a.txt --threads=abc +:int return 0 +:blob stdout 41 +errors=1 +0: Invalid value for flag (abc) + +:test a_missing_required_flag_is_reported +:command compile +:blob executable 2 +cc +:blob args 81 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. errors.c -o .side_effects/errors +:int return 0 +:command run +:blob executable 20 +.side_effects/errors +:blob args 5 +a.txt +:int return 0 +:blob stdout 49 +errors=1 +0: Required flag is not present (token) + +:test batching_a_non_bool_flag_is_reported +:command compile +:blob executable 2 +cc +:blob args 81 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. errors.c -o .side_effects/errors +:int return 0 +:command run +:blob executable 20 +.side_effects/errors +:blob args 3 +-tT +:int return 0 +:blob stdout 168 +errors=4 +0: Cannot batch non-boolean flags (t) +1: Cannot batch non-boolean flags (T) +2: Required flag is not present (token) +3: Not enough positional arguments (files) + +:test too_few_positionals_are_reported +:command compile +:blob executable 2 +cc +:blob args 81 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. errors.c -o .side_effects/errors +:int return 0 +:command run +:blob executable 20 +.side_effects/errors +:blob args 4 +-T x +:int return 0 +:blob stdout 52 +errors=1 +0: Not enough positional arguments (files) + +:test too_many_positionals_are_reported +:command compile +:blob executable 2 +cc +:blob args 81 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. errors.c -o .side_effects/errors +:int return 0 +:command run +:blob executable 20 +.side_effects/errors +:blob args 22 +-T x a.txt b.txt c.txt +:int return 0 +:blob stdout 50 +errors=1 +0: Too many positional arguments (files) + +:test every_error_is_collected_not_just_the_first +:command compile +:blob executable 2 +cc +:blob args 81 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. errors.c -o .side_effects/errors +:int return 0 +:command run +:blob executable 20 +.side_effects/errors +:blob args 20 +--nope --threads=abc +:int return 0 +:blob stdout 149 +errors=4 +0: Unknown flag (--nope) +1: Invalid value for flag (abc) +2: Required flag is not present (token) +3: Not enough positional arguments (files) + diff --git a/tests/exit_behaviour/exit_behaviour.c b/tests/exit_behaviour/exit_behaviour.c new file mode 100644 index 0000000..1ff34b2 --- /dev/null +++ b/tests/exit_behaviour/exit_behaviour.c @@ -0,0 +1,18 @@ +// optly exits on a bad command line by default. The C suite cannot observe +// that, because it defines OPTLY_NO_EXIT to stay alive. +#define OPTLY_IMPLEMENTATION +#include "optly.h" +#include + +int main(int argc, char **argv) { + OptlyCommand cmd = { + .name = "app", + .flags = optly_flags( + optly_flag_uint32("threads", 't', "Worker threads", .value.as_uint32 = 4) + ) + }; + + optly_parse_args(argc, argv, &cmd); + printf("survived\n"); + return 0; +} diff --git a/tests/exit_behaviour/test.tspec b/tests/exit_behaviour/test.tspec new file mode 100644 index 0000000..7d4a67f --- /dev/null +++ b/tests/exit_behaviour/test.tspec @@ -0,0 +1,33 @@ +:test bad_flag_exits_nonzero_by_default +:command compile +:blob executable 2 +cc +:blob args 97 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. exit_behaviour.c -o .side_effects/exit_behaviour +:int return 0 +:command run +:blob executable 28 +.side_effects/exit_behaviour +:blob args 6 +--nope +:int return 1 +:blob stdout 0 + +:blob stderr_contains 12 +Unknown flag +:test valid_arguments_return_to_the_caller +:command compile +:blob executable 2 +cc +:blob args 97 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. exit_behaviour.c -o .side_effects/exit_behaviour +:int return 0 +:command run +:blob executable 28 +.side_effects/exit_behaviour +:blob args 4 +-t 8 +:int return 0 +:blob stdout 9 +survived + diff --git a/tests/flags/flags.c b/tests/flags/flags.c new file mode 100644 index 0000000..7edf7d7 --- /dev/null +++ b/tests/flags/flags.c @@ -0,0 +1,26 @@ +// Dumps every flag optly parsed, so the .tspec can assert on exact bytes. +#define OPTLY_IMPLEMENTATION +#include "optly.h" +#include + +int main(int argc, char **argv) { + OptlyCommand cmd = { + .name = "app", + .flags = optly_flags( + optly_flag_bool("verbose", 'v', "Verbose", .value.as_bool = false), + optly_flag_bool("quiet", 'q', "Quiet", .value.as_bool = false), + optly_flag_bool("force", 'f', "Force", .value.as_bool = false), + optly_flag_uint32("threads", 't', "Threads", .value.as_uint32 = 4), + optly_flag_string("out", 'o', "Output", .value.as_string = "a.out") + ) + }; + + optly_parse_args(argc, argv, &cmd); + + printf("verbose=%d\n", optly_flag_value_bool(&cmd, "verbose")); + printf("quiet=%d\n", optly_flag_value_bool(&cmd, "quiet")); + printf("force=%d\n", optly_flag_value_bool(&cmd, "force")); + printf("threads=%u\n", optly_flag_value_uint32(&cmd, "threads")); + printf("out=%s\n", optly_flag_value_string(&cmd, "out")); + return 0; +} diff --git a/tests/flags/test.tspec b/tests/flags/test.tspec new file mode 100644 index 0000000..e2a198c --- /dev/null +++ b/tests/flags/test.tspec @@ -0,0 +1,138 @@ +:test defaults_are_used_when_no_flag_is_given +:command compile +:blob executable 2 +cc +:blob args 79 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. flags.c -o .side_effects/flags +:int return 0 +:command run +:blob executable 19 +.side_effects/flags +:int return 0 +:blob stdout 46 +verbose=0 +quiet=0 +force=0 +threads=4 +out=a.out + +:test long_flags_are_recognised +:command compile +:blob executable 2 +cc +:blob args 79 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. flags.c -o .side_effects/flags +:int return 0 +:command run +:blob executable 19 +.side_effects/flags +:blob args 21 +--verbose --threads 8 +:int return 0 +:blob stdout 46 +verbose=1 +quiet=0 +force=0 +threads=8 +out=a.out + +:test short_flags_are_recognised +:command compile +:blob executable 2 +cc +:blob args 79 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. flags.c -o .side_effects/flags +:int return 0 +:command run +:blob executable 19 +.side_effects/flags +:blob args 7 +-v -t 8 +:int return 0 +:blob stdout 46 +verbose=1 +quiet=0 +force=0 +threads=8 +out=a.out + +:test short_bool_flags_can_be_batched +:command compile +:blob executable 2 +cc +:blob args 79 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. flags.c -o .side_effects/flags +:int return 0 +:command run +:blob executable 19 +.side_effects/flags +:blob args 4 +-vqf +:int return 0 +:blob stdout 46 +verbose=1 +quiet=1 +force=1 +threads=4 +out=a.out + +:test inline_long_value_with_equals +:command compile +:blob executable 2 +cc +:blob args 79 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. flags.c -o .side_effects/flags +:int return 0 +:command run +:blob executable 19 +.side_effects/flags +:blob args 26 +--threads=16 --out=bin/app +:int return 0 +:blob stdout 49 +verbose=0 +quiet=0 +force=0 +threads=16 +out=bin/app + +:test inline_short_value_with_equals +:command compile +:blob executable 2 +cc +:blob args 79 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. flags.c -o .side_effects/flags +:int return 0 +:command run +:blob executable 19 +.side_effects/flags +:blob args 5 +-t=16 +:int return 0 +:blob stdout 47 +verbose=0 +quiet=0 +force=0 +threads=16 +out=a.out + +:test separate_short_value_with_space +:command compile +:blob executable 2 +cc +:blob args 79 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. flags.c -o .side_effects/flags +:int return 0 +:command run +:blob executable 19 +.side_effects/flags +:blob args 10 +-o bin/app +:int return 0 +:blob stdout 48 +verbose=0 +quiet=0 +force=0 +threads=4 +out=bin/app + diff --git a/tests/long_flags/long_flags.c b/tests/long_flags/long_flags.c new file mode 100644 index 0000000..f494ad2 --- /dev/null +++ b/tests/long_flags/long_flags.c @@ -0,0 +1,22 @@ +// A flag name longer than OPTLY_FLAG_BUFFER_LENGTH is truncated into the +// stack copy used to split '='. The copy then has no '=' in it even though +// the argument did. +#define OPTLY_NO_EXIT +#define OPTLY_IMPLEMENTATION +#include "optly.h" +#include + +int main(int argc, char **argv) { + OptlyCommand cmd = { + .name = "app", + .flags = optly_flags( + optly_flag_string("out", 'o', "Out", .value.as_string = "a.out") + ) + }; + + OptlyErrors errs = optly_parse_args(argc, argv, &cmd); + + printf("errors=%zu\n", optly_errors_count(&errs)); + printf("out=%s\n", optly_flag_value_string(&cmd, "out")); + return 0; +} diff --git a/tests/long_flags/test.tspec b/tests/long_flags/test.tspec new file mode 100644 index 0000000..6a21859 --- /dev/null +++ b/tests/long_flags/test.tspec @@ -0,0 +1,34 @@ +:test a_flag_name_longer_than_the_buffer_does_not_crash +:command compile +:blob executable 2 +cc +:blob args 89 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. long_flags.c -o .side_effects/long_flags +:int return 0 +:command run +:blob executable 24 +.side_effects/long_flags +:blob args 304 +--aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=x +:int return 0 +:blob stdout 19 +errors=1 +out=a.out + +:test a_normal_inline_value_still_works +:command compile +:blob executable 2 +cc +:blob args 89 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. long_flags.c -o .side_effects/long_flags +:int return 0 +:command run +:blob executable 24 +.side_effects/long_flags +:blob args 13 +--out=bin/app +:int return 0 +:blob stdout 21 +errors=0 +out=bin/app + diff --git a/tests/positionals/positionals.c b/tests/positionals/positionals.c new file mode 100644 index 0000000..8ecec03 --- /dev/null +++ b/tests/positionals/positionals.c @@ -0,0 +1,33 @@ +// Prints every positional value in order, so ordering and the -- delimiter +// are both pinned by the expected bytes. +#define OPTLY_IMPLEMENTATION +#include "optly.h" +#include + +int main(int argc, char **argv) { + OptlyCommand cmd = { + .name = "app", + .flags = optly_flags( + optly_flag_bool("verbose", 'v', "Verbose", .value.as_bool = false) + ), + .positionals = optly_positionals( + optly_positional("files", "Files", .min = 1, .max = 0) + ) + }; + + optly_parse_args(argc, argv, &cmd); + + printf("verbose=%d\n", optly_flag_value_bool(&cmd, "verbose")); + + OptlyPositional *p = optly_get_positional(&cmd, "files"); + if (!p) { + printf("files=\n"); + return 0; + } + + printf("count=%zu\n", p->count); + for (size_t i = 0; i < p->count; i++) { + printf("files[%zu]=%s\n", i, p->values[i]); + } + return 0; +} diff --git a/tests/positionals/test.tspec b/tests/positionals/test.tspec new file mode 100644 index 0000000..241d6af --- /dev/null +++ b/tests/positionals/test.tspec @@ -0,0 +1,78 @@ +:test values_are_collected_in_order +:command compile +:blob executable 2 +cc +:blob args 91 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. positionals.c -o .side_effects/positionals +:int return 0 +:command run +:blob executable 25 +.side_effects/positionals +:blob args 17 +a.txt b.txt c.txt +:int return 0 +:blob stdout 63 +verbose=0 +count=3 +files[0]=a.txt +files[1]=b.txt +files[2]=c.txt + +:test flags_may_be_interleaved_with_values +:command compile +:blob executable 2 +cc +:blob args 91 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. positionals.c -o .side_effects/positionals +:int return 0 +:command run +:blob executable 25 +.side_effects/positionals +:blob args 21 +a.txt --verbose b.txt +:int return 0 +:blob stdout 48 +verbose=1 +count=2 +files[0]=a.txt +files[1]=b.txt + +:test everything_after_a_double_dash_is_positional +:command compile +:blob executable 2 +cc +:blob args 91 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. positionals.c -o .side_effects/positionals +:int return 0 +:command run +:blob executable 25 +.side_effects/positionals +:blob args 27 +-- --not-a-flag a.txt b.txt +:int return 0 +:blob stdout 70 +verbose=0 +count=3 +files[0]=--not-a-flag +files[1]=a.txt +files[2]=b.txt + +:test a_double_dash_after_a_value_still_ends_flag_parsing +:command compile +:blob executable 2 +cc +:blob args 91 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. positionals.c -o .side_effects/positionals +:int return 0 +:command run +:blob executable 25 +.side_effects/positionals +:blob args 11 +a.txt -- -v +:int return 0 +:blob stdout 45 +verbose=0 +count=2 +files[0]=a.txt +files[1]=-v + diff --git a/tests/typed_values/test.tspec b/tests/typed_values/test.tspec new file mode 100644 index 0000000..dee5977 --- /dev/null +++ b/tests/typed_values/test.tspec @@ -0,0 +1,41 @@ +:test every_type_round_trips_from_an_inline_value +:command compile +:blob executable 2 +cc +:blob args 93 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. typed_values.c -o .side_effects/typed_values +:int return 0 +:command run +:blob executable 26 +.side_effects/typed_values +:blob args 98 +--ch=Z --i8=-8 --i16=-16 --i32=-32 --i64=-64 --u8=8 --u16=16 --u32=32 --u64=64 --f32=1.5 --f64=2.5 +:int return 0 +:blob stdout 79 +ch=Z +i8=-8 +i16=-16 +i32=-32 +i64=-64 +u8=8 +u16=16 +u32=32 +u64=64 +f32=1.50 +f64=2.50 + +:test every_type_round_trips_from_a_short_flag +:command compile +:blob executable 2 +cc +:blob args 93 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. typed_values.c -o .side_effects/typed_values +:int return 0 +:command run +:blob executable 26 +.side_effects/typed_values +:blob args 68 +-c Z -a -8 -b -16 -d -32 -e -64 -f 8 -g 16 -h 32 -i 64 -j 1.5 -k 2.5 +:int return 1 +:blob stdout 0 + diff --git a/tests/typed_values/typed_values.c b/tests/typed_values/typed_values.c new file mode 100644 index 0000000..14421fe --- /dev/null +++ b/tests/typed_values/typed_values.c @@ -0,0 +1,39 @@ +// One flag per supported type, printed back so the .tspec pins the conversion. +#define OPTLY_IMPLEMENTATION +#include "optly.h" +#include +#include + +int main(int argc, char **argv) { + OptlyCommand cmd = { + .name = "app", + .flags = optly_flags( + optly_flag_char("ch", 'c', "Char", .value.as_char = '?'), + optly_flag_int8("i8", 'a', "int8", .value.as_int8 = 0), + optly_flag_int16("i16", 'b', "int16", .value.as_int16 = 0), + optly_flag_int32("i32", 'd', "int32", .value.as_int32 = 0), + optly_flag_int64("i64", 'e', "int64", .value.as_int64 = 0), + optly_flag_uint8("u8", 'f', "uint8", .value.as_uint8 = 0), + optly_flag_uint16("u16",'g', "uint16", .value.as_uint16 = 0), + optly_flag_uint32("u32",'h', "uint32", .value.as_uint32 = 0), + optly_flag_uint64("u64",'i', "uint64", .value.as_uint64 = 0), + optly_flag_float("f32", 'j', "float", .value.as_float = 0), + optly_flag_double("f64",'k', "double", .value.as_double = 0) + ) + }; + + optly_parse_args(argc, argv, &cmd); + + printf("ch=%c\n", optly_flag_value_char(&cmd, "ch")); + printf("i8=%d\n", optly_flag_value_int8(&cmd, "i8")); + printf("i16=%d\n", optly_flag_value_int16(&cmd, "i16")); + printf("i32=%" PRId32 "\n", optly_flag_value_int32(&cmd, "i32")); + printf("i64=%" PRId64 "\n", optly_flag_value_int64(&cmd, "i64")); + printf("u8=%u\n", optly_flag_value_uint8(&cmd, "u8")); + printf("u16=%u\n", optly_flag_value_uint16(&cmd, "u16")); + printf("u32=%" PRIu32 "\n", optly_flag_value_uint32(&cmd, "u32")); + printf("u64=%" PRIu64 "\n", optly_flag_value_uint64(&cmd, "u64")); + printf("f32=%.2f\n", (double)optly_flag_value_float(&cmd, "f32")); + printf("f64=%.2f\n", optly_flag_value_double(&cmd, "f64")); + return 0; +} diff --git a/tests/usage/test.tspec b/tests/usage/test.tspec new file mode 100644 index 0000000..50a9942 --- /dev/null +++ b/tests/usage/test.tspec @@ -0,0 +1,28 @@ +:test usage_lists_flags_commands_and_positionals +:command compile +:blob executable 2 +cc +:blob args 79 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. usage.c -o .side_effects/usage +:int return 0 +:command run +:blob executable 19 +.side_effects/usage +:int return 0 +:blob stderr 281 +Example application + +Usage: app [FLAGS] [files...] + +COMMANDS + run Run server + +POSITIONAL ARGUMENTS + files (0.. values) + +FLAGS + -v --verbose Enable verbose output + -t --threads Worker threads (default: 4) + + -h --help Show this message + diff --git a/tests/usage/usage.c b/tests/usage/usage.c new file mode 100644 index 0000000..92580f0 --- /dev/null +++ b/tests/usage/usage.c @@ -0,0 +1,30 @@ +// Prints the generated usage for a command tree that exercises flags, +// subcommands and positionals. +#define OPTLY_IMPLEMENTATION +#define OPTLY_GEN_HELP_FLAG +#include "optly.h" + +int main(int argc, char **argv) { + OptlyCommand cmd = { + .name = "app", + .description = "Example application", + .flags = optly_flags( + optly_flag_bool("verbose", 'v', "Enable verbose output", .value.as_bool = false), + optly_flag_uint32("threads", 't', "Worker threads", .value.as_uint32 = 4) + ), + .commands = optly_commands( + optly_command("run", "Run server", + .flags = optly_flags( + optly_flag_uint16("port", 'p', "Server port", .value.as_uint16 = 8080) + ) + ) + ), + .positionals = optly_positionals( + optly_positional("files", "Files to process", .min = 0, .max = 0) + ) + }; + + optly_parse_args(argc, argv, &cmd); + optly_usage(&cmd); + return 0; +} From c3e88bf91c1aafa9e24f7ca3c2b4d237d953bd32 Mon Sep 17 00:00:00 2001 From: Nikita Chulkov Date: Mon, 31 Aug 2026 16:33:38 +1100 Subject: [PATCH 03/18] test: removed old test.c --- tests/test.c | 465 --------------------------------------------------- 1 file changed, 465 deletions(-) delete mode 100644 tests/test.c diff --git a/tests/test.c b/tests/test.c deleted file mode 100644 index 1782581..0000000 --- a/tests/test.c +++ /dev/null @@ -1,465 +0,0 @@ -#include -#include -#include -#include -#include - -#define OPTLY_NO_EXIT -#define OPTLY_IMPLEMENTATION -#define OPTLY_LOG(...) -#include "optly.h" - -/* -------------------------- tiny test framework -------------------------- */ -#define ANSI_RED(x) "\033[31m" x "\033[0m" -#define ANSI_GREEN(x) "\033[32m" x "\033[0m" - -static int g_asserts = 0; -static int g_failed = 0; - -#define ASSERT_TRUE(expr) \ - do { \ - g_asserts++; \ - if (!(expr)) { \ - fprintf(stderr, " " ANSI_RED("ASSERT_TRUE failed") ": %s (%s:%d)\n", #expr, __FILE__, __LINE__); \ - g_failed++; \ - } \ - } while (0) - -#define ASSERT_FALSE(expr) \ - do { \ - g_asserts++; \ - if ((expr)) { \ - fprintf(stderr, " " ANSI_RED("ASSERT_FALSE failed") ": %s (%s:%d)\n", #expr, __FILE__, __LINE__); \ - g_failed++; \ - } \ - } while (0) - -#define ASSERT_EQ_INT(actual, expected) \ - do { \ - g_asserts++; \ - long long a__ = (long long)(actual); \ - long long e__ = (long long)(expected); \ - if (a__ != e__) { \ - fprintf(stderr, " " ANSI_RED("ASSERT_EQ_INT failed") ": got=%lld expected=%lld (%s:%d)\n", a__, e__, __FILE__, __LINE__); \ - g_failed++; \ - } \ - } while (0) - -#define ASSERT_EQ_STR(actual, expected) \ - do { \ - g_asserts++; \ - const char *a__ = (actual); \ - const char *e__ = (expected); \ - if ((a__ == NULL && e__ != NULL) || (a__ != NULL && e__ == NULL) || (a__ && e__ && strcmp(a__, e__) != 0)) { \ - fprintf(stderr, " " ANSI_RED("ASSERT_EQ_STR failed") ": got=%s expected=%s (%s:%d)\n", a__ ? a__ : "NULL", e__ ? e__ : "NULL", __FILE__, __LINE__); \ - g_failed++; \ - } \ - } while (0) - -#define ASSERT_FLOAT_NEAR(actual, expected, eps) \ - do { \ - g_asserts++; \ - double a__ = (double)(actual); \ - double e__ = (double)(expected); \ - if (fabs(a__ - e__) > (eps)) { \ - fprintf(stderr, " " ANSI_RED("ASSERT_FLOAT_NEAR failed") ": got=%f expected=%f eps=%f (%s:%d)\n", a__, e__, (double)(eps), __FILE__, __LINE__); \ - g_failed++; \ - } \ - } while (0) - -#define RUN_TEST(fn) \ - do { \ - int before = g_failed; \ - fn(); \ - if (g_failed == before) \ - fprintf(stderr, ANSI_GREEN("[PASS]") " %s\n", #fn); \ - else \ - fprintf(stderr, ANSI_RED("[FAIL]") " %s\n", #fn); \ - } while (0) - -/* ------------------------------ helpers ---------------------------------- */ - -#define assert_err_count(errs, expected) \ - do { \ - int failed = g_failed; \ - ASSERT_EQ_INT((long long)optly_errors_count((errs)), (long long)(expected)); \ - if (g_failed > failed) optly_error_print((errs)); \ - } while (0) - -#define assert_err_at(errs, idx, k, aarg) \ - do { \ - OptlyError e = optly_errors_at((errs), (idx)); \ - ASSERT_EQ_INT((int)e.kind, (int)(k)); \ - if (aarg) ASSERT_EQ_STR(e.arg, (aarg)); \ - } while (0) - -#define ARGV(...) {__VA_ARGS__, NULL} - -size_t count_argc(char *argv[]) { - size_t i = 1; - for (char **v = argv; *v; v++) ++i; - return i; -} - -/* ------------------------------ test cases -------------------------------- */ - -static void test_optional_flags_defaults(void) { - OptlyCommand cmd = optly_command( - "app", - .flags = optly_flags( - optly_flag_bool("verbose", .shortname = 'v', .value.as_bool = false), - optly_flag_uint32("threads", .shortname = 't', .value.as_uint32 = 4) - ) - ); - - char *argv[] = ARGV("app"); - OptlyErrors errs = optly_parse_args(count_argc(argv), argv, &cmd); - assert_err_count(&errs, 0); - ASSERT_FALSE(optly_flag_value_bool(&cmd, "verbose")); - ASSERT_EQ_INT(optly_flag_value_uint32(&cmd, "threads"), 4); -} - -static void test_long_short_and_batch_bools(void) { - OptlyCommand cmd = optly_command( - "app", - .flags = optly_flags( - optly_flag_bool("a", .shortname = 'a'), - optly_flag_bool("b", .shortname = 'b'), - optly_flag_bool("c", .shortname = 'c') - ) - ); - - char *argv[] = ARGV("app", "-abc"); - OptlyErrors errs = optly_parse_args(count_argc(argv), argv, &cmd); - assert_err_count(&errs, 0); - ASSERT_TRUE(optly_flag_value_bool(&cmd, "a")); - ASSERT_TRUE(optly_flag_value_bool(&cmd, "b")); - ASSERT_TRUE(optly_flag_value_bool(&cmd, "c")); -} - -static void test_inline_and_separate_values(void) { - OptlyCommand cmd = optly_command( - "app", - .flags = optly_flags( - optly_flag_uint32("threads", .shortname = 't'), - optly_flag_string("name", .shortname = 'n') - ) - ); - - char *argv[] = ARGV("app", "--threads=8", "--name", "Alice"); - OptlyErrors errs = optly_parse_args(count_argc(argv), argv, &cmd); - assert_err_count(&errs, 0); - ASSERT_EQ_INT(optly_flag_value_uint32(&cmd, "threads"), 8); - ASSERT_EQ_STR(optly_flag_value_string(&cmd, "name"), "Alice"); -} - -static void test_short_value_equals_and_space(void) { - OptlyCommand cmd = optly_command( - "app", - .flags = optly_flags( - optly_flag_int64("value", .shortname = 'x') - ) - ); - - char *argv[] = ARGV("app", "-x=77", "-x", "99"); - OptlyErrors errs = optly_parse_args(count_argc(argv), argv, &cmd); - - // New value will override last one - assert_err_count(&errs, 0); - ASSERT_EQ_INT(optly_flag_value_int64(&cmd, "value"), 99); -} - -static void test_typed_values(void) { - OptlyCommand cmd = optly_command( - "app", - .flags = optly_flags( - optly_flag_char("ch", .shortname = 'c'), - optly_flag_int8("i8", .shortname = 'a'), - optly_flag_int16("i16", .shortname = 'b'), - optly_flag_int32("i32", .shortname = 'd'), - optly_flag_int64("i64", .shortname = 'e'), - optly_flag_uint8("u8", .shortname = 'f'), - optly_flag_uint16("u16", .shortname = 'g'), - optly_flag_uint32("u32", .shortname = 'h'), - optly_flag_uint64("u64", .shortname = 'i'), - optly_flag_float("f32", .shortname = 'j'), - optly_flag_double("f64", .shortname = 'k') - ) - ); - - char *argv[] = ARGV("app", "--ch=Z", "--i8=-8", "--i16=-16", "--i32=-32", "--i64=-64", "--u8=8", "--u16=16", "--u32=32", "--u64=64", "--f32=1.5", "--f64=2.5"); - - OptlyErrors errs = optly_parse_args(count_argc(argv), argv, &cmd); - assert_err_count(&errs, 0); - ASSERT_EQ_INT(optly_flag_value_char(&cmd, "ch"), 'Z'); - ASSERT_EQ_INT(optly_flag_value_int8(&cmd, "i8"), -8); - ASSERT_EQ_INT(optly_flag_value_int16(&cmd, "i16"), -16); - ASSERT_EQ_INT(optly_flag_value_int32(&cmd, "i32"), -32); - ASSERT_EQ_INT(optly_flag_value_int64(&cmd, "i64"), -64); - ASSERT_EQ_INT(optly_flag_value_uint8(&cmd, "u8"), 8); - ASSERT_EQ_INT(optly_flag_value_uint16(&cmd, "u16"), 16); - ASSERT_EQ_INT(optly_flag_value_uint32(&cmd, "u32"), 32); - ASSERT_EQ_INT(optly_flag_value_uint64(&cmd, "u64"), 64); - ASSERT_FLOAT_NEAR(optly_flag_value_float(&cmd, "f32"), 1.5f, 1e-6); - ASSERT_FLOAT_NEAR(optly_flag_value_double(&cmd, "f64"), 2.5, 1e-9); -} - -static void test_commands_and_command_flags(void) { - OptlyCommand cmd = optly_command( - "app", - .flags = optly_flags( - optly_flag_bool("verbose", .shortname = 'v') - ), - .commands = optly_commands( - optly_command( - "run", - .flags = optly_flags( - optly_flag_uint16("port", .shortname = 'p', .value.as_uint16 = 8080), - optly_flag_bool("verbose", .shortname = 'v') - ) - ) - ) - ); - - char *argv[] = ARGV("app", "--verbose", "run", "-p", "9000", "-v"); - OptlyErrors errs = optly_parse_args(count_argc(argv), argv, &cmd); - assert_err_count(&errs, 0); - ASSERT_TRUE(optly_flag_value_bool(&cmd, "verbose")); - ASSERT_TRUE(cmd.next_command != NULL); - ASSERT_EQ_STR(cmd.next_command->name, "run"); - ASSERT_EQ_INT(optly_flag_value_uint16(cmd.next_command, "port"), 9000); - ASSERT_TRUE(optly_flag_value_bool(cmd.next_command, "verbose")); -} - -static void test_subcommand_selection(void) { - OptlyCommand cmd = optly_command( - "app", - .commands = optly_commands( - optly_command( - "run", - .commands = optly_commands(optly_command("check", NULL)) - ) - ) - ); - - char *argv[] = ARGV("app", "run", "check"); - OptlyErrors errs = optly_parse_args(count_argc(argv), argv, &cmd); - assert_err_count(&errs, 0); - ASSERT_TRUE(cmd.next_command != NULL); - ASSERT_EQ_STR(cmd.next_command->name, "run"); - ASSERT_TRUE(cmd.next_command->next_command != NULL); - ASSERT_EQ_STR(cmd.next_command->next_command->name, "check"); -} - -static void test_positionals_and_delimiter(void) { - OptlyCommand cmd = optly_command( - "app", - .flags = optly_flags( - optly_flag_bool("verbose", .shortname = 'v') - ), - .positionals = optly_positionals( - optly_positional("files", .min = 1, .max = 0) // variadic - ) - ); - - char *argv[] = ARGV("app", "--", "--not-a-flag", "a.txt", "b.txt"); - OptlyErrors errs = optly_parse_args(count_argc(argv), argv, &cmd); - assert_err_count(&errs, 0); - OptlyPositional *p = optly_get_positional(&cmd, "files"); - ASSERT_TRUE(p != NULL); - ASSERT_EQ_INT(p->count, 3); - ASSERT_EQ_STR(p->values[0], "--not-a-flag"); - ASSERT_EQ_STR(p->values[1], "a.txt"); - ASSERT_EQ_STR(p->values[2], "b.txt"); -} - -static void test_error_unknown_flag_missing_invalid(void) { - OptlyCommand cmd = optly_command( - "app", - .flags = optly_flags( - optly_flag_uint32("threads", .shortname = 't'), - optly_flag_int32("num", .shortname = 'n') - ) - ); - - char *argv[] = ARGV("app", "--unknown", "--threads", "--num=abc"); - OptlyErrors errs = optly_parse_args(count_argc(argv), argv, &cmd); - assert_err_count(&errs, 3); - assert_err_at(&errs, 0, OPTLY_ERR_UNKNOWN_FLAG, "--unknown"); - assert_err_at(&errs, 1, OPTLY_ERR_MISSING_VALUE, "threads"); - assert_err_at(&errs, 2, OPTLY_ERR_INVALID_VALUE, "abc"); -} - -static void test_error_unknown_command(void) { - OptlyCommand cmd = optly_command( - "app", - .commands = optly_commands(optly_command("run", NULL)) - ); - - char *argv[] = ARGV("app", "build"); - OptlyErrors errs = optly_parse_args(count_argc(argv), argv, &cmd); - assert_err_count(&errs, 1); - assert_err_at(&errs, 0, OPTLY_ERR_UNKNOWN_COMMAND, "build"); -} - -static void test_error_required_and_batch_non_bool(void) { - OptlyCommand cmd = optly_command( - "app", - .flags = optly_flags( - optly_flag_string("token", .shortname = 'T', .required = true), - optly_flag_uint32("threads", .shortname = 't') - ) - ); - - char *argv[] = ARGV("app", "-tT"); - OptlyErrors errs = optly_parse_args(count_argc(argv), argv, &cmd); - - // -tT => batch, but t is not bool => OPTLY_ERR_BATCH_NON_BOOL - // T required token not found => OPTLY_ERR_MISSING_REQUIRED - assert_err_count(&errs, 3); - assert_err_at(&errs, 0, OPTLY_ERR_BATCH_NON_BOOL, "t"); - assert_err_at(&errs, 1, OPTLY_ERR_BATCH_NON_BOOL, "T"); - assert_err_at(&errs, 2, OPTLY_ERR_MISSING_REQUIRED, "token"); -} - -static void test_error_positionals_too_few_and_too_many(void) { - // too few - { - OptlyCommand cmd = optly_command( - "app", - .positionals = optly_positionals( - optly_positional("src", .min = 1, .max = 1) - ) - ); - - char *argv[] = ARGV("app"); - OptlyErrors errs = optly_parse_args(count_argc(argv), argv, &cmd); - assert_err_count(&errs, 1); - assert_err_at(&errs, 0, OPTLY_ERR_POSITIONAL_TOO_FEW, "src"); - } - // too many - { - OptlyCommand cmd = optly_command( - "app", - .positionals = optly_positionals( - optly_positional("src", .min = 1, .max = 1), - optly_positional("dst", .min = 0, .max = 1) - ) - ); - - char *argv[] = ARGV("app", "a", "b", "c"); - OptlyErrors errs = optly_parse_args(count_argc(argv), argv, &cmd); - assert_err_count(&errs, 1); - assert_err_at(&errs, 0, OPTLY_ERR_POSITIONAL_TOO_MANY, "dst"); - } -} - -static void test_enum_default_and_parse(void) { - OptlyCommand cmd = { - "app", - .flags = optly_flags( - optly_flag_enum("log", 'l', optly_enum_values("verbose", "debug", "verbose", "warn")) - ) - }; - - // No args -> default value - { - char *argv[] = ARGV("app"); - OptlyErrors errs = optly_parse_args(count_argc(argv), argv, &cmd); - assert_err_count(&errs, 0); - ASSERT_EQ_STR(optly_flag_value_enum(&cmd, "log"), "verbose"); - } - - // Explicit value - { - char *argv[] = ARGV("app", "--log=warn"); - OptlyErrors errs = optly_parse_args(count_argc(argv), argv, &cmd); - assert_err_count(&errs, 0); - ASSERT_EQ_STR(optly_flag_value_enum(&cmd, "log"), "warn"); - } -} - -static void test_enum_errors(void) { - OptlyCommand cmd = { - "app", - .flags = optly_flags( - optly_flag_enum("mode", 'm', optly_enum_values("fast", "fast", "slow")) - ) - }; - - { - char *argv[] = ARGV("app", "--mode=invalid"); - OptlyErrors errs = optly_parse_args(count_argc(argv), argv, &cmd); - assert_err_count(&errs, 1); - assert_err_at(&errs, 0, OPTLY_ERR_INVALID_VALUE, "invalid"); - } - - { - char *argv[] = ARGV("app", "--mode"); - OptlyErrors errs = optly_parse_args(count_argc(argv), argv, &cmd); - assert_err_count(&errs, 1); - assert_err_at(&errs, 0, OPTLY_ERR_MISSING_VALUE, "mode"); - } -} - -static void test_enum_short_and_overwrite(void) { - OptlyCommand cmd = { - "app", - .flags = optly_flags( - optly_flag_enum("level", 'l', optly_enum_values("low", "low", "mid", "high")) - ) - }; - - char *argv[] = ARGV("app", "-l=mid", "-l", "high"); - OptlyErrors errs = optly_parse_args(count_argc(argv), argv, &cmd); - assert_err_count(&errs, 0); - ASSERT_EQ_STR(optly_flag_value_enum(&cmd, "level"), "high"); -} - -static void test_enum_mixed_with_other_flags(void) { - OptlyCommand cmd = { - "app", - .flags = optly_flags( - optly_flag_enum("log", 'l', optly_enum_values("warn", "debug", "info", "warn")), - optly_flag_uint32("threads", 't', .value.as_uint32 = 2) - ) - }; - - char *argv[] = ARGV("app", "--log=info", "--threads", "8"); - OptlyErrors errs = optly_parse_args(count_argc(argv), argv, &cmd); - - assert_err_count(&errs, 0); - ASSERT_EQ_STR(optly_flag_value_enum(&cmd, "log"), "info"); - ASSERT_EQ_INT(optly_flag_value_uint32(&cmd, "threads"), 8); -} - -int main(void) { - fprintf(stderr, "\nRunning optly tests...\n\n"); - - RUN_TEST(test_optional_flags_defaults); - RUN_TEST(test_long_short_and_batch_bools); - RUN_TEST(test_inline_and_separate_values); - RUN_TEST(test_short_value_equals_and_space); - RUN_TEST(test_typed_values); - RUN_TEST(test_commands_and_command_flags); - RUN_TEST(test_subcommand_selection); - RUN_TEST(test_positionals_and_delimiter); - RUN_TEST(test_error_unknown_flag_missing_invalid); - RUN_TEST(test_error_unknown_command); - RUN_TEST(test_error_required_and_batch_non_bool); - RUN_TEST(test_error_positionals_too_few_and_too_many); - RUN_TEST(test_enum_default_and_parse); - RUN_TEST(test_enum_errors); - RUN_TEST(test_enum_short_and_overwrite); - RUN_TEST(test_enum_mixed_with_other_flags); - - fprintf(stderr, "\nAsserts: %d\n", g_asserts); - - if (g_failed == 0) { - fprintf(stderr, ANSI_GREEN("✅ All tests passed\n")); - return 0; - } - - fprintf(stderr, ANSI_RED("❌ Failed: %d\n"), g_failed); - return 1; -} From 1cc8a552af6953ac230c60f472ce34290c52445e Mon Sep 17 00:00:00 2001 From: Nikita Chulkov Date: Mon, 31 Aug 2026 16:36:13 +1100 Subject: [PATCH 04/18] chore: updated readme --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 72dc50b..20e03a4 100644 --- a/README.md +++ b/README.md @@ -241,6 +241,12 @@ Define these before including optly: Optly is a C library. It is not tested as C++ and does not try to compile as C++ -- use argparse, CLI11 or cxxopts there. +## Tests + +Test are located in `./tests/` folder (duh!). We are using .tspec tests format, using strum as tspec runner. + +Read more about tspec and strum at [https://github.com/strongleong/strum](https://github.com/strongleong/strum) + ## Design Goals Optly focuses on: From 2c8a48d174d94beb4b6855625e59673994912d4a Mon Sep 17 00:00:00 2001 From: Nikita Chulkov Date: Mon, 31 Aug 2026 16:38:05 +1100 Subject: [PATCH 05/18] chore: updated changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb61056..ed59692 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## Upcoming +## Added + +- Added tests, using tspec tests format and strum runner. + +## Fixed + +- Fixed crash when argument length is not truncated correctly. + ## v2.3.6 ### Fixed From fc1b38feac5beb2b452ae85fc5552d6136e76024 Mon Sep 17 00:00:00 2001 From: Nikita Chulkov Date: Mon, 31 Aug 2026 16:42:20 +1100 Subject: [PATCH 06/18] fix: usage now respects `OPTLY_HELP_SHORT_FLAG` and `OPTLY_VERSION_SHORT_FLAG` --- optly.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/optly.h b/optly.h index abc8446..770c8fc 100644 --- a/optly.h +++ b/optly.h @@ -766,11 +766,11 @@ static void optly_usage_flags(OptlyFlag *flags) { } #ifdef OPTLY_GEN_HELP_FLAG - fprintf(stderr, "\n %-*s Show this message\n", (int)pad + type_name_pad, "-h --help"); + fprintf(stderr, "\n %-*s Show this message\n", (int)pad + type_name_pad, OPTLY_HELP_SHORT_FLAG " --help"); #endif #ifdef OPTLY_GEN_VERSION_FLAG - fprintf(stderr, " %-*s Show version\n", (int)pad + type_name_pad, "-v --version"); + fprintf(stderr, " %-*s Show version\n", (int)pad + type_name_pad, OPTLY_VERSION_SHORT_FLAG " --version"); #endif } From e88640d589c95f3f22b38a8f6e28c12f02d2d780 Mon Sep 17 00:00:00 2001 From: Nikita Chulkov Date: Mon, 31 Aug 2026 16:42:59 +1100 Subject: [PATCH 07/18] test: added regression tests for changing short help and version flags in usage --- .../short_flag_override/short_flag_override.c | 21 ++++++++++ tests/short_flag_override/test.tspec | 42 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 tests/short_flag_override/short_flag_override.c create mode 100644 tests/short_flag_override/test.tspec diff --git a/tests/short_flag_override/short_flag_override.c b/tests/short_flag_override/short_flag_override.c new file mode 100644 index 0000000..231a868 --- /dev/null +++ b/tests/short_flag_override/short_flag_override.c @@ -0,0 +1,21 @@ +// The generated help and version flags can have their short forms moved. +// Usage has to advertise the same forms the parser accepts. +#define OPTLY_GEN_HELP_FLAG +#define OPTLY_GEN_VERSION_FLAG +#define OPTLY_HELP_SHORT_FLAG "-?" +#define OPTLY_VERSION_SHORT_FLAG "-V" +#define OPTLY_IMPLEMENTATION +#include "optly.h" + +int main(int argc, char **argv) { + OptlyCommand cmd = { + .name = "app", + .flags = optly_flags( + optly_flag_bool("verbose", 'v', "Verbose", .value.as_bool = false) + ) + }; + + optly_parse_args(argc, argv, &cmd, "1.0.0"); + optly_usage(&cmd); + return 0; +} diff --git a/tests/short_flag_override/test.tspec b/tests/short_flag_override/test.tspec new file mode 100644 index 0000000..f3ba28f --- /dev/null +++ b/tests/short_flag_override/test.tspec @@ -0,0 +1,42 @@ +:test usage_advertises_the_overridden_short_flags +:command compile +:blob executable 2 +cc +:blob args 107 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. short_flag_override.c -o .side_effects/short_flag_override +:int return 0 +:command run +:blob executable 33 +.side_effects/short_flag_override +:int return 0 +:blob stderr 138 +Usage: app [FLAGS] + +FLAGS + -v --verbose Verbose + + -? --help Show this message + -V --version Show version + +:test the_overridden_help_short_flag_is_accepted +:command compile +:blob executable 2 +cc +:blob args 107 +-std=c99 -Wall -Wextra -Werror -pedantic -I../.. short_flag_override.c -o .side_effects/short_flag_override +:int return 0 +:command run +:blob executable 33 +.side_effects/short_flag_override +:blob args 2 +-? +:int return 0 +:blob stderr 138 +Usage: app [FLAGS] + +FLAGS + -v --verbose Verbose + + -? --help Show this message + -V --version Show version + From 5e43c5a6ba1875695f02330602bcfaeff6a21d0c Mon Sep 17 00:00:00 2001 From: Nikita Chulkov Date: Mon, 31 Aug 2026 16:44:12 +1100 Subject: [PATCH 08/18] chore: updated changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed59692..178aa58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,14 @@ ## Added - Added tests, using tspec tests format and strum runner. +- `optly_flag_value_enum` is now actually declared in the header part of + the library. ## Fixed - Fixed crash when argument length is not truncated correctly. +- `OPTLY_HELP_SHORT_FLAG` and `OPTLY_VERSION_SHORT_FLAG` are now respected + in `optly_usage_flags()` ## v2.3.6 From 5a97ec785ae1567d50f5fac852f3749f2404f603 Mon Sep 17 00:00:00 2001 From: Nikita Chulkov Date: Mon, 31 Aug 2026 16:45:08 +1100 Subject: [PATCH 09/18] fix: removed duplicated `optly_get_positional()` declaration --- optly.h | 1 - 1 file changed, 1 deletion(-) diff --git a/optly.h b/optly.h index 770c8fc..c271a83 100644 --- a/optly.h +++ b/optly.h @@ -467,7 +467,6 @@ OPTLYDEF uint64_t optly_flag_value_uint64(const OptlyCommand *command, c OPTLYDEF float optly_flag_value_float(const OptlyCommand *command, const char *name); OPTLYDEF double optly_flag_value_double(const OptlyCommand *command, const char *name); OPTLYDEF char *optly_flag_value_enum(const OptlyCommand *command, const char *name); -OPTLYDEF OptlyPositional *optly_get_positional(OptlyCommand *command, const char *name); #endif // OPTLY_H From d7696d02f2d1452a8c0be35e113bcfb0cca7615d Mon Sep 17 00:00:00 2001 From: Nikita Chulkov Date: Mon, 31 Aug 2026 16:45:39 +1100 Subject: [PATCH 10/18] chore: `Positional` -> `OptlyPositional` in docblock --- optly.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optly.h b/optly.h index c271a83..f82e7f8 100644 --- a/optly.h +++ b/optly.h @@ -156,7 +156,7 @@ Named positional: (can have many valies inside) - Positional *pos = optly_get_positional(&cmd, "name"); + OptlyPositional *pos = optly_get_positional(&cmd, "name"); Or directly through command: From 21178211c60be947771876aff236c54d1acff43b Mon Sep 17 00:00:00 2001 From: Nikita Chulkov Date: Mon, 31 Aug 2026 16:52:59 +1100 Subject: [PATCH 11/18] feat: introcuded OPTLY_NULL_*, NULL_* is deprecated --- optly.h | 69 ++++++++++++++++++++++++++++++++------------------------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/optly.h b/optly.h index f82e7f8..69740e5 100644 --- a/optly.h +++ b/optly.h @@ -44,8 +44,8 @@ // Values are unions, so you need to specify member of value with correct type some way { "threads", 't', "Worker threads", false, {.as_uint32 = 4}, OPTLY_TYPE_UINT32 }, - // Flag arrays should always ends with NULL_FLAG. Try to not forget about it :) - NULL_FLAG, + // Flag arrays should always ends with OPTLY_NULL_FLAG. Try to not forget about it :) + OPTLY_NULL_FLAG, }, // This is your commands. (git `commit`, docker `compose` `up`) @@ -54,7 +54,7 @@ // Instead of defining whole struct manually you can use helper functions optly_command("run", "Runs server", - // optly_flags macro deals with type castings and closing array with NULL_FLAG + // optly_flags macro deals with type castings and closing array with OPTLY_NULL_FLAG optly_flags( // This is *command* flag. `./app -p 8080 run` will not work, but `./app run -p 8080` will @@ -379,9 +379,18 @@ OPTLYDEF OptlyError optly_errors_at(const OptlyErrors *errs, size_t i); OPTLYDEF const char *optly_error_message(OptlyErrorKind err); OPTLYDEF void optly_error_print(const OptlyErrors *errs); -#define NULL_FLAG {.fullname = NULL, .shortname = 0, .value = {.as_int64 = 0}, .type = 0} -#define NULL_COMMAND {.name = NULL, .flags = NULL} -#define NULL_POSITIONAL {.name = NULL} +#define OPTLY_NULL_FLAG {.fullname = NULL, .shortname = 0, .value = {.as_int64 = 0}, .type = 0} +#define OPTLY_NULL_COMMAND {.name = NULL, .flags = NULL} +#define OPTLY_NULL_POSITIONAL {.name = NULL} + +/** + * WARN: the unprefixed spellings are the original names and are kept so + * existing code builds. They will be removed in v3 + * @deprecated + */ +#define NULL_FLAG OPTLY_NULL_FLAG +#define NULL_COMMAND OPTLY_NULL_COMMAND +#define NULL_POSITIONAL OPTLY_NULL_POSITIONAL // NOTE: Forcing designated initializer for automatically zero-initializing missing fields #define optly_flag(name, ...) \ @@ -395,22 +404,22 @@ OPTLYDEF void optly_error_print(const OptlyErrors *errs); .name = (namme), __VA_ARGS__ \ } -#define optly_flags(...) \ - (OptlyFlag[]) { \ - __VA_ARGS__, NULL_FLAG \ +#define optly_flags(...) \ + (OptlyFlag[]) { \ + __VA_ARGS__, OPTLY_NULL_FLAG \ } -#define optly_commands(...) \ - (OptlyCommand[]) { \ - __VA_ARGS__, NULL_COMMAND \ +#define optly_commands(...) \ + (OptlyCommand[]) { \ + __VA_ARGS__, OPTLY_NULL_COMMAND \ } #define optly_positional(namme, ...) \ (OptlyPositional) { \ .name = (namme), __VA_ARGS__ \ } -#define optly_positionals(...) \ - (OptlyPositional[]) { \ - __VA_ARGS__, NULL_POSITIONAL \ +#define optly_positionals(...) \ + (OptlyPositional[]) { \ + __VA_ARGS__, OPTLY_NULL_POSITIONAL \ } #define optly_flag_bool(name, ...) optly_flag(name, __VA_ARGS__, .type = OPTLY_TYPE_BOOL) @@ -453,20 +462,20 @@ static inline bool optly_is_command_null(const OptlyCommand *cmd) { return cmd == NULL || cmd->name == NULL; } -OPTLYDEF bool optly_flag_value_bool(const OptlyCommand *command, const char *name); -OPTLYDEF char optly_flag_value_char(const OptlyCommand *command, const char *name); -OPTLYDEF char *optly_flag_value_string(const OptlyCommand *command, const char *name); -OPTLYDEF int8_t optly_flag_value_int8(const OptlyCommand *command, const char *name); -OPTLYDEF int16_t optly_flag_value_int16(const OptlyCommand *command, const char *name); -OPTLYDEF int32_t optly_flag_value_int32(const OptlyCommand *command, const char *name); -OPTLYDEF int64_t optly_flag_value_int64(const OptlyCommand *command, const char *name); -OPTLYDEF uint8_t optly_flag_value_uint8(const OptlyCommand *command, const char *name); -OPTLYDEF uint16_t optly_flag_value_uint16(const OptlyCommand *command, const char *name); -OPTLYDEF uint32_t optly_flag_value_uint32(const OptlyCommand *command, const char *name); -OPTLYDEF uint64_t optly_flag_value_uint64(const OptlyCommand *command, const char *name); -OPTLYDEF float optly_flag_value_float(const OptlyCommand *command, const char *name); -OPTLYDEF double optly_flag_value_double(const OptlyCommand *command, const char *name); -OPTLYDEF char *optly_flag_value_enum(const OptlyCommand *command, const char *name); +OPTLYDEF bool optly_flag_value_bool(const OptlyCommand *command, const char *name); +OPTLYDEF char optly_flag_value_char(const OptlyCommand *command, const char *name); +OPTLYDEF char *optly_flag_value_string(const OptlyCommand *command, const char *name); +OPTLYDEF int8_t optly_flag_value_int8(const OptlyCommand *command, const char *name); +OPTLYDEF int16_t optly_flag_value_int16(const OptlyCommand *command, const char *name); +OPTLYDEF int32_t optly_flag_value_int32(const OptlyCommand *command, const char *name); +OPTLYDEF int64_t optly_flag_value_int64(const OptlyCommand *command, const char *name); +OPTLYDEF uint8_t optly_flag_value_uint8(const OptlyCommand *command, const char *name); +OPTLYDEF uint16_t optly_flag_value_uint16(const OptlyCommand *command, const char *name); +OPTLYDEF uint32_t optly_flag_value_uint32(const OptlyCommand *command, const char *name); +OPTLYDEF uint64_t optly_flag_value_uint64(const OptlyCommand *command, const char *name); +OPTLYDEF float optly_flag_value_float(const OptlyCommand *command, const char *name); +OPTLYDEF double optly_flag_value_double(const OptlyCommand *command, const char *name); +OPTLYDEF char *optly_flag_value_enum(const OptlyCommand *command, const char *name); #endif // OPTLY_H @@ -494,7 +503,7 @@ OPTLYDEF char *optly_flag_value_enum(const OptlyCommand *command, con #else -#if defined(LOGCIE) && LOGCIE_VERSION_NUMBER < 1200 && (defined (__GNUC__) || defined(__clang__)) +#if defined(LOGCIE) && LOGCIE_VERSION_NUMBER < 1200 && (defined(__GNUC__) || defined(__clang__)) #warning "Your Logcie version is too old. Falling back to fprintf logging." #endif From a7bbdf47a90e8e26a34efab08243e02ff742834a Mon Sep 17 00:00:00 2001 From: Nikita Chulkov Date: Mon, 31 Aug 2026 17:11:39 +1100 Subject: [PATCH 12/18] fix: return empty string isntead of NULL in optly_flag_value_double --- optly.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optly.h b/optly.h index 69740e5..9bf77ab 100644 --- a/optly.h +++ b/optly.h @@ -1196,7 +1196,7 @@ OPTLYDEF double optly_flag_value_double(const OptlyCommand *command, const char OPTLYDEF char *optly_flag_value_enum(const OptlyCommand *command, const char *name) { const OptlyFlag *flag = optly_get_flag(command->flags, name); - return flag ? flag->value.as_enum[0] : NULL; + return flag ? flag->value.as_enum[0] : ""; } OPTLYDEF OptlyPositional *optly_get_positional(OptlyCommand *command, const char *name) { From 6893140379a417ad65a00fc55601b1abdee72a9b Mon Sep 17 00:00:00 2001 From: Nikita Chulkov Date: Mon, 31 Aug 2026 17:12:07 +1100 Subject: [PATCH 13/18] buid: added build.c --- build.c | 438 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 438 insertions(+) create mode 100644 build.c diff --git a/build.c b/build.c new file mode 100644 index 0000000..054fff9 --- /dev/null +++ b/build.c @@ -0,0 +1,438 @@ +// Build tool for optly. Compile it once and it rebuilds itself from then on: +// +// cc build.c -o build +// ./build # compile every example into ./out +// ./build tests # run the .tspec suite +// +// It parses its own command line with optly, so the library is exercised by +// the thing that builds it. + +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#include +#include + +// NOTE: windows.h defines ERROR, which collides with the log level names. +#ifdef ERROR +#undef ERROR +#endif + +#define mkdir(path, mode) _mkdir(path) +#define PATH_SEP "\\" +#define PATH_LIST_SEP ';' +#else +#include +#include +#include + +#define PATH_SEP "/" +#define PATH_LIST_SEP ':' +#endif // _WIN32 + +#define PATH_MAX_LEN 4096 +#define CMD_MAX_ARGV 64 +#define MAX_ENTRIES 128 + +// NOTE: strum is the reference .tspec runner, but the format is not tied to +// it. --tspec-runner exists so a second implementation can be dropped in +// without touching this file. +#define DEFAULT_TSPEC_RUNNER "strum" +#define STRUM_HOMEPAGE "https://github.com/Strongleong/Strum" + +#define OPTLY_GEN_HELP_FLAG +#define OPTLY_GEN_HELP_COMMAND +#define OPTLY_IMPLEMENTATION +#include "optly.h" + +static bool verbose = true; + +#define LOG(level, ...) \ + do { \ + fprintf(stderr, "[build] " level ": "); \ + fprintf(stderr, __VA_ARGS__); \ + fprintf(stderr, "\n"); \ + } while (0) + +#define INFO(...) \ + do { \ + if (verbose) LOG("INFO", __VA_ARGS__); \ + } while (0) +#define WARN(...) LOG("WARN", __VA_ARGS__) +#define FATAL(...) LOG("FATAL", __VA_ARGS__) + +// NOTE: the tree is built inside main rather than at file scope. The DSL +// expands to compound literals, which are not constant initializers, so a +// static one does not compile under -pedantic. The header says as much. +static OptlyCommand command; + +// ---------------------------------------------------------------- filesystem + +// snprintf silently truncates; a build tool that half-writes a path would +// compile to the wrong file. Every path is joined through here so overflow is +// reported instead of guessed at. +static bool join_path(char *out, size_t cap, const char *dir, const char *name) { + int n = snprintf(out, cap, "%s" PATH_SEP "%s", dir, name); + return n > 0 && (size_t)n < cap; +} + +static bool dir_exists(const char *path) { + struct stat sb; + return stat(path, &sb) == 0 && S_ISDIR(sb.st_mode); +} + +static bool file_exists(const char *path) { + struct stat sb; + return stat(path, &sb) == 0 && S_ISREG(sb.st_mode); +} + +// Collects the names of entries in dir that pass filter, sorted, so a build +// does not depend on the order the filesystem hands them back. +static int compare_names(const void *a, const void *b) { + return strcmp(*(const char *const *)a, *(const char *const *)b); +} + +static int list_dir(const char *dir, bool (*filter)(const char *name), char names[][256], int max) { + DIR *d = opendir(dir); + + if (!d) { + FATAL("Can not open %s: %s", dir, strerror(errno)); + return -1; + } + + int count = 0; + + for (struct dirent *e = readdir(d); e && count < max; e = readdir(d)) { + if (e->d_name[0] == '.') { + continue; + } + + if (filter && !filter(e->d_name)) { + continue; + } + + snprintf(names[count], 256, "%s", e->d_name); + count++; + } + + closedir(d); + + char *ptrs[MAX_ENTRIES]; + + for (int i = 0; i < count; i++) { + ptrs[i] = names[i]; + } + + qsort(ptrs, (size_t)count, sizeof(ptrs[0]), compare_names); + + char sorted[MAX_ENTRIES][256]; + + for (int i = 0; i < count; i++) { + snprintf(sorted[i], 256, "%s", ptrs[i]); + } + + for (int i = 0; i < count; i++) { + snprintf(names[i], 256, "%s", sorted[i]); + } + + return count; +} + +// ------------------------------------------------------------------ processes + +static bool is_executable(const char *path) { +#ifdef _WIN32 + return _access(path, 0) == 0; +#else + return access(path, X_OK) == 0; +#endif +} + +// Resolves a bare name against PATH so a missing runner is reported before +// anything is compiled, rather than as a failed exec halfway through. +static bool find_executable(const char *name, char *out, size_t cap) { + if (strchr(name, PATH_SEP[0]) != NULL) { + snprintf(out, cap, "%s", name); + return is_executable(out); + } + + const char *path = getenv("PATH"); + + if (!path) { + return false; + } + + while (*path) { + const char *sep = strchr(path, PATH_LIST_SEP); + size_t len = sep ? (size_t)(sep - path) : strlen(path); + + if (len > 0 && len < cap) { + snprintf(out, cap, "%.*s" PATH_SEP "%s", (int)len, path, name); + + if (is_executable(out)) { + return true; + } + } + + if (!sep) { + break; + } + + path = sep + 1; + } + + return false; +} + +static void print_command(char *const argv[]) { + if (!verbose) { + return; + } + + fprintf(stderr, "+"); + + for (int i = 0; argv[i]; i++) { + fprintf(stderr, " %s", argv[i]); + } + + fprintf(stderr, "\n"); +} + +// Runs argv to completion and returns its exit status, or -1 if it could not +// be started. No shell is involved, so arguments never need quoting. +static int run(char *const argv[]) { + print_command(argv); + +#ifdef _WIN32 + intptr_t rc = _spawnvp(_P_WAIT, argv[0], (const char *const *)argv); + return rc < 0 ? -1 : (int)rc; +#else + pid_t pid = fork(); + + if (pid < 0) { + FATAL("Can not fork: %s", strerror(errno)); + return -1; + } + + if (pid == 0) { + execvp(argv[0], argv); + fprintf(stderr, "[build] FATAL: can not run %s: %s\n", argv[0], strerror(errno)); + _exit(127); + } + + int status = 0; + + if (waitpid(pid, &status, 0) < 0) { + FATAL("Can not wait for %s: %s", argv[0], strerror(errno)); + return -1; + } + + if (WIFSIGNALED(status)) { + return 128 + WTERMSIG(status); + } + + return WIFEXITED(status) ? WEXITSTATUS(status) : -1; +#endif +} + +// ---------------------------------------------------------------- the targets + +static char outdir[PATH_MAX_LEN] = {0}; + +static bool has_c_extension(const char *name) { + size_t len = strlen(name); + return len > 2 && strcmp(name + len - 2, ".c") == 0; +} + +static bool build_example(const char *cc, const char *std, bool debug, const char *name) { + char src[PATH_MAX_LEN]; + char out[PATH_MAX_LEN]; + char stdflag[64]; + + char stem[256]; + + snprintf(stdflag, sizeof(stdflag), "-std=%s", std); + snprintf(stem, sizeof(stem), "%s", name); + stem[strlen(stem) - 2] = '\0'; // drop ".c" + + if (!join_path(src, sizeof(src), "examples", name) || !join_path(out, sizeof(out), outdir, stem)) { + FATAL("Path too long for %s", name); + return false; + } + + char *argv[CMD_MAX_ARGV]; + int i = 0; + + argv[i++] = (char *)cc; + argv[i++] = stdflag; + argv[i++] = (char *)"-Wall"; + argv[i++] = (char *)"-Wextra"; + argv[i++] = (char *)"-Werror"; + argv[i++] = (char *)"-pedantic"; + argv[i++] = (char *)"-I."; + + if (debug) { + argv[i++] = (char *)"-O0"; + argv[i++] = (char *)"-g"; + } else { + argv[i++] = (char *)"-O2"; + } + + argv[i++] = src; + argv[i++] = (char *)"-o"; + argv[i++] = out; + argv[i] = NULL; + + return run(argv) == 0; +} + +static bool build_examples(const char *cc, const char *std, bool debug) { + char names[MAX_ENTRIES][256]; + int count = list_dir("examples", has_c_extension, names, MAX_ENTRIES); + + if (count < 0) { + return false; + } + + if (count == 0) { + WARN("No examples found"); + return true; + } + + bool ok = true; + + for (int i = 0; i < count; i++) { + if (!build_example(cc, std, debug, names[i])) { + FATAL("Failed to build examples" PATH_SEP "%s", names[i]); + ok = false; + } + } + + if (ok) { + INFO("Built %d example%s into %s", count, count == 1 ? "" : "s", outdir); + } + + return ok; +} + +static bool run_tests(const char *runner_name) { + char runner[PATH_MAX_LEN]; + + if (!find_executable(runner_name, runner, sizeof(runner))) { + FATAL("Test runner '%s' not found in PATH", runner_name); + FATAL("strum is the reference .tspec runner: " STRUM_HOMEPAGE); + FATAL("Use --tspec-runner to point at a different one"); + return false; + } + + if (!dir_exists("tests")) { + FATAL("No tests directory here"); + return false; + } + + char *argv[] = {runner, (char *)".", NULL}; + + if (chdir("tests") != 0) { + FATAL("Can not enter tests: %s", strerror(errno)); + return false; + } + + int rc = run(argv); + + if (chdir("..") != 0) { + FATAL("Can not leave tests: %s", strerror(errno)); + return false; + } + + return rc == 0; +} + +static bool clean(void) { + char names[MAX_ENTRIES][256]; + int count = list_dir(outdir, NULL, names, MAX_ENTRIES); + + if (count < 0) { + return false; + } + + for (int i = 0; i < count; i++) { + char path[PATH_MAX_LEN]; + + if (!join_path(path, sizeof(path), outdir, names[i])) { + WARN("Path too long for %s", names[i]); + continue; + } + + if (remove(path) != 0) { + WARN("Can not remove %s: %s", path, strerror(errno)); + } + } + + INFO("Removed %d file%s from %s", count, count == 1 ? "" : "s", outdir); + return true; +} + +int main(int argc, char *argv[]) { + command = (OptlyCommand){ + .name = "build", + .description = "Build optly's examples and run its tests", + .flags = optly_flags( + optly_flag_bool("debug", 'd', "Compile with debug flags", .value.as_bool = false), + optly_flag_bool("silent", 's', "Compile without unnecessary output", .value.as_bool = false), + optly_flag_string("outdir", 'o', "Set output dir", .value.as_string = "." PATH_SEP "out"), + optly_flag_string("compiler", 'c', "Set which C compiler to use", .value.as_string = "cc"), + optly_flag_string("std", 0, "Set which C standard to compile against", .value.as_string = "c99") + ), + .commands = optly_commands( + optly_command("tests", "Run the .tspec suite", .flags = optly_flags(optly_flag_string("tspec-runner", 'r', "Runner to execute .tspec files with", .value.as_string = DEFAULT_TSPEC_RUNNER))), + optly_command("clean", "Remove the output directory's contents") + ) + }; + + optly_parse_args(argc, argv, &command); + + verbose = !optly_flag_value_bool(&command, "silent"); + + snprintf(outdir, sizeof(outdir), "%s", optly_flag_value_string(&command, "outdir")); + + size_t len = strlen(outdir); + + while (len > 1 && outdir[len - 1] == PATH_SEP[0]) { + outdir[--len] = '\0'; + } + + if (!dir_exists(outdir) && mkdir(outdir, 0755) != 0) { + FATAL("Can not create %s: %s", outdir, strerror(errno)); + return 1; + } + + if (optly_is_command(command.next_command, "tests")) { + const char *runner = optly_flag_value_string(command.next_command, "tspec-runner"); + return run_tests(runner) ? 0 : 1; + } + + if (optly_is_command(command.next_command, "clean")) { + return clean() ? 0 : 1; + } + + if (!file_exists("optly.h")) { + FATAL("optly.h is not here; run build from the repository root"); + return 1; + } + + return build_examples( + optly_flag_value_string(&command, "compiler"), + optly_flag_value_string(&command, "std"), + optly_flag_value_bool(&command, "debug") + ) + ? 0 + : 1; +} From 1490b549b2a13b1fded9ca73bb54d10247413bb9 Mon Sep 17 00:00:00 2001 From: Nikita Chulkov Date: Mon, 31 Aug 2026 17:12:14 +1100 Subject: [PATCH 14/18] build: removed build.sh --- build.sh | 103 ------------------------------------------------------- 1 file changed, 103 deletions(-) delete mode 100755 build.sh diff --git a/build.sh b/build.sh deleted file mode 100755 index 01d07cc..0000000 --- a/build.sh +++ /dev/null @@ -1,103 +0,0 @@ -#!/bin/bash - - -CFLAGS="-Wall -Wextra -std=c99 -pedantic" -CLIBS="-I./" -CDEBUG="-O0 -ggdb -fsanitize=address -fno-omit-frame-pointer" -CPROFILE="-pg" -CRELEASE="-O3 -march=native" -CC="clang" - -OUTDIR="./out" -VERBOSE=true -DEBUG=false - -usage() { - echo "-d --debug Compile with debug flags" - echo "-p --profile Compile with profile flags" - echo "-s --silent Compile without unnececary output" - echo "-o --outdir Set output dir (default: ./out)" - echo "-c --compiler Set which compier to use (default: clang)" - echo "-h --help Print help" -} - -handle_arg() { - case "$1" in - "${2}d"|"--debug") CFLAGS="$CFLAGS $CDEBUG" - shift - ;; - "${2}p"|"--profile") CFLAGS="$CPROFILE $CFLAGS" - shift - ;; - "${2}s"|"--silent") VERBOSE=false - shift - shift - ;; - "${2}o"|"--outdir") OUTDIR="$2" - shift - shift - ;; - "${2}c"|"--compiler") CC="$2" - shift - shift - ;; - "${2}h"|"--help") usage; - shift - exit 1; - ;; - ""|"-") # pass through - shift - ;; - *) echo "Unknown command '$1'"; - usage; - exit 1; - esac -} - -while [[ $# -gt 0 ]]; do - case "$1" in - -*) - for (( i=0; i<${#1}; i++ )); do - handle_arg "${1:$i:1}" '' - done - shift - ;; - *) echo "Unknown command '$1'"; - shift - usage; - exit 1; - esac -done - -set -e - -if [ ! -d "$OUTDIR" ]; then - mkdir -p "$OUTDIR"; -fi - -if $DEBUG; then - CFLAGS="$CFLAGS $CDEBUG" -else - CFLAGS="$CFLAGS $CRELEASE" -fi - -tests=$(find ./tests -name '*.c') -examples=$(find ./examples -name '*.c') - -for example in $examples; do - CMD="$CC $CFLAGS $CLIBS -o $OUTDIR/$(basename ${example%.*}) $example" - - if $VERBOSE; then - echo + $CMD - fi - - if $COMPILE; then - $CMD - fi -done - -if $VERBOSE; then - set -x -fi - -$CC $CFLAGS $CSTD $CLIBS -o "$OUTDIR/tests" $tests From 39b9b694257787bb9390aefa605542e2bef27639 Mon Sep 17 00:00:00 2001 From: Nikita Chulkov Date: Mon, 31 Aug 2026 17:12:21 +1100 Subject: [PATCH 15/18] chore: updated .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 378c2d6..4fb8f16 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ compile_commands.json out **/.side_effects/ +out/ +build From 4a7c15071ea92b804cbca3afa548d0769a868f9b Mon Sep 17 00:00:00 2001 From: Nikita Chulkov Date: Mon, 31 Aug 2026 17:12:37 +1100 Subject: [PATCH 16/18] ci: updated ci --- .github/workflows/main.yml | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2934223..3fc0553 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,7 +8,8 @@ on: paths: - '**.c' - '**.h' - - 'build.sh' + - '**.tspec' + - '.github/workflows/main.yml' workflow_dispatch: jobs: @@ -16,5 +17,20 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - run: ./build.sh && ./out/tests + - uses: actions/checkout@v4 + + - name: Build strum + run: | + git clone --depth 1 https://github.com/Strongleong/Strum strum + cd strum + cc build.c -o build + ./build -c gcc + echo "$PWD/out" >> "$GITHUB_PATH" + + - name: Build + run: | + cc build.c -o build + ./build + + - name: Test + run: ./build tests From d487232244152deeed884df4aa73b51e7e931b2d Mon Sep 17 00:00:00 2001 From: Nikita Chulkov Date: Mon, 31 Aug 2026 17:16:27 +1100 Subject: [PATCH 17/18] chore: updated readme --- README.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 20e03a4..57c298c 100644 --- a/README.md +++ b/README.md @@ -241,9 +241,36 @@ Define these before including optly: Optly is a C library. It is not tested as C++ and does not try to compile as C++ -- use argparse, CLI11 or cxxopts there. +## Building + +Optly itself is a single header -- there is nothing to build to use it. The +repository ships `build.c`, which compiles the examples and runs the tests. +Compile it once, then use the binary: + +``` bash +cc build.c -o build + +./build # compile every example into ./out +./build tests # run the .tspec suite +./build clean # empty the output directory +``` + +Run `./build help` for more info. + ## Tests -Test are located in `./tests/` folder (duh!). We are using .tspec tests format, using strum as tspec runner. +Tests live in `./tests/`, one directory per concept. Each holds a small C +fixture and a `test.tspec` that compiles it, runs it, and compares the exact +bytes it writes. + +They are written in the .tspec format and run with strum: + +``` bash +./build tests +``` + +strum has to be on your `PATH`. Use `--tspec-runner` to point at a different +implementation of the format. Read more about tspec and strum at [https://github.com/strongleong/strum](https://github.com/strongleong/strum) From 45fce375edadcd25820faf510efa1f6fd033079a Mon Sep 17 00:00:00 2001 From: Nikita Chulkov Date: Mon, 31 Aug 2026 17:18:09 +1100 Subject: [PATCH 18/18] chore: changelog --- CHANGELOG.md | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 178aa58..6487735 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,17 +2,34 @@ ## Upcoming -## Added +### Added + +- `OPTLY_NULL_FLAG`, `OPTLY_NULL_COMMAND` and `OPTLY_NULL_POSITIONAL`. + The old spellings still work, but they are deprecated and will be + removed in v3. +- `optly_flag_value_enum()` is declared in the header. It has always been + there, but only code that defined `OPTLY_IMPLEMENTATION` could reach it. +- A test suite in the .tspec format, run with strum. 43 tests across 11 + directories, covering flag forms, every value type, commands and + subcommands, positionals, error accumulation, enums, usage output and the + exit behaviour. -- Added tests, using tspec tests format and strum runner. -- `optly_flag_value_enum` is now actually declared in the header part of - the library. +### Fixed + +- **A crash reachable from any optly program's command line.** A flag written + as `--name=value` where the name is longer than `OPTLY_FLAG_BUFFER_LENGTH` + (256) was truncated before its `=`, and the split then wrote through a NULL + pointer. Such a flag is now reported as unknown. +- `OPTLY_HELP_SHORT_FLAG` and `OPTLY_VERSION_SHORT_FLAG` were honoured when + parsing but ignored when printing usage, so overriding them left a program + advertising flags it did not accept. +- `optly_get_positional()` was declared twice in the header. -## Fixed +### Changed -- Fixed crash when argument length is not truncated correctly. -- `OPTLY_HELP_SHORT_FLAG` and `OPTLY_VERSION_SHORT_FLAG` are now respected - in `optly_usage_flags()` +- `build.sh` is replaced by `build.c`. The script never compiled the examples: + it guarded the compile with a variable it never assigned. Its flag handling + was broken too, and bash ruled out Windows. See the README for usage. ## v2.3.6