diff --git a/src/Makefile.am b/src/Makefile.am index 79c6d78..469638d 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -3,4 +3,5 @@ AM_CFLAGS = -std=c23 -Wall -Wextra -Wpedantic bin_PROGRAMS = stupidtools -stupidtools_SOURCES = main.c +stupidtools_SOURCES = main.c cli.c +noinst_HEADERS = cli.h diff --git a/src/cli.c b/src/cli.c new file mode 100644 index 0000000..a18c63e --- /dev/null +++ b/src/cli.c @@ -0,0 +1,111 @@ +/* + * cli.c - argument parsing for the stupidtools entrypoint. + * + * Manual parsing on purpose: the flag surface is tiny and fully under + * our control, so there is no need for getopt's permutation rules or + * its platform quirks. Unknown options are usage errors (exit 2) with + * the usage text on stderr; a missing buildfile is a runtime error + * (exit 1). -- terminates option parsing so a buildfile whose name + * starts with '-' stays addressable. + * + * Copyright (c) 2026 huntedbytheirs + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include "cli.h" + +#include + +static const char * +program_name(const char *argv0) +{ + if (argv0 == NULL || argv0[0] == '\0') { + return "stupidtools"; + } + return argv0; +} + +static int +is_option(const char *arg) +{ + return arg[0] == '-' && arg[1] != '\0' && strcmp(arg, "-") != 0; +} + +void +cli_print_usage(FILE *stream, const char *program) +{ + fprintf(stream, + "Usage: %s [OPTIONS] \n" + "\n" + "Reads a KDL build file and emits a POSIX-sh ./configure script.\n" + "\n" + "Options:\n" + " -h, --help show this help and exit\n" + " -V, --version print version and exit\n" + "\n" + "Note: buildfile processing is not yet implemented.\n", + program); +} + +/* Take the next positional argument, reporting usage errors to stderr. + * Returns CLI_ACTION_ERROR (with usage printed) or CLI_ACTION_RUN. */ +static enum cli_action +take_positional(const char *arg, struct cli_opts *opts) +{ + if (opts->buildfile != NULL) { + fprintf(stderr, "%s: unexpected extra argument '%s'\n", + opts->program, arg); + cli_print_usage(stderr, opts->program); + return CLI_ACTION_ERROR; + } + opts->buildfile = arg; + return CLI_ACTION_RUN; +} + +enum cli_action +cli_parse(int argc, char **argv, struct cli_opts *opts) +{ + enum cli_action action; + int i; + + opts->program = program_name(argc > 0 ? argv[0] : NULL); + opts->buildfile = NULL; + + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + + if (!is_option(arg)) { + action = take_positional(arg, opts); + if (action != CLI_ACTION_RUN) { + return action; + } + continue; + } + + if (strcmp(arg, "--") == 0) { + for (i++; i < argc; i++) { + action = take_positional(argv[i], opts); + if (action != CLI_ACTION_RUN) { + return action; + } + } + break; + } + + if (strcmp(arg, "-h") == 0 || strcmp(arg, "--help") == 0) { + cli_print_usage(stdout, opts->program); + return CLI_ACTION_HELP; + } + + if (strcmp(arg, "-V") == 0 || strcmp(arg, "--version") == 0) { + return CLI_ACTION_VERSION; + } + + fprintf(stderr, "%s: unrecognized option '%s'\n", + opts->program, arg); + cli_print_usage(stderr, opts->program); + return CLI_ACTION_ERROR; + } + + return CLI_ACTION_RUN; +} diff --git a/src/cli.h b/src/cli.h new file mode 100644 index 0000000..42b86af --- /dev/null +++ b/src/cli.h @@ -0,0 +1,36 @@ +/* + * cli.h - argument parsing for the stupidtools entrypoint. + * + * Copyright (c) 2026 huntedbytheirs + * SPDX-License-Identifier: BSD-3-Clause + */ + +#ifndef STUPIDTOOLS_CLI_H +#define STUPIDTOOLS_CLI_H + +#include + +/* Must match AC_INIT version in configure.ac (todo 1: 1.0.0). */ +#define STUPIDTOOLS_VERSION "1.0.0" + +#define CLI_EXIT_OK 0 +#define CLI_EXIT_RUNTIME 1 +#define CLI_EXIT_USAGE 2 + +/* What the caller should do after parsing argv. */ +enum cli_action { + CLI_ACTION_RUN, /* proceed with the parsed buildfile (todo 16+) */ + CLI_ACTION_HELP, /* usage already printed to stdout, exit 0 */ + CLI_ACTION_VERSION, /* caller prints the version line, exit 0 */ + CLI_ACTION_ERROR /* error + usage already printed to stderr, exit 2 */ +}; + +struct cli_opts { + const char *program; /* argv[0] as given (or a default) */ + const char *buildfile; /* single positional buildfile, NULL if none */ +}; + +enum cli_action cli_parse(int argc, char **argv, struct cli_opts *opts); +void cli_print_usage(FILE *stream, const char *program); + +#endif /* STUPIDTOOLS_CLI_H */ diff --git a/src/main.c b/src/main.c index 68906bd..52174ee 100644 --- a/src/main.c +++ b/src/main.c @@ -4,21 +4,43 @@ * Copyright (c) 2026 huntedbytheirs * SPDX-License-Identifier: BSD-3-Clause * - * Minimal skeleton: reports the version. Real CLI parsing (todo 4) - * replaces this stub. + * Entrypoint: parse argv via cli.c, dispatch --help/--version/usage + * errors, and reserve the `stupidtools ` shape. Buildfile + * processing arrives in later todos (16+). */ -#include +#include "cli.h" -/* Must match AC_INIT version in configure.ac (todo 1: 1.0.0). */ -#define STUPIDTOOLS_VERSION "1.0.0" +#include int main(int argc, char **argv) { - (void)argc; - (void)argv; + struct cli_opts opts; + enum cli_action action; - printf("stupidtools %s\n", STUPIDTOOLS_VERSION); - return 0; + action = cli_parse(argc, argv, &opts); + + switch (action) { + case CLI_ACTION_HELP: + return CLI_EXIT_OK; + case CLI_ACTION_VERSION: + printf("stupidtools %s\n", STUPIDTOOLS_VERSION); + return CLI_EXIT_OK; + case CLI_ACTION_ERROR: + return CLI_EXIT_USAGE; + case CLI_ACTION_RUN: + break; + } + + if (opts.buildfile == NULL) { + fprintf(stderr, "%s: error: no build file given\n", + opts.program); + cli_print_usage(stderr, opts.program); + return CLI_EXIT_RUNTIME; + } + + /* Accepted: the buildfile is parsed in todo 16+, not read here. */ + (void)opts.buildfile; + return CLI_EXIT_OK; } diff --git a/tests/run.sh b/tests/run.sh index 90a3ea8..549e8cf 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -48,6 +48,88 @@ else fail "--version exited non-zero (rc=$rc): $version_out" fi +# --- 3. --help must exit 0 and print usage on stdout ---------------------- +help_out= +help_rc=0 +help_out=$("$BIN" --help 2>/dev/null) || help_rc=$? +if [ "$help_rc" -eq 0 ]; then + case "$help_out" in + *"Usage: "*) + pass "--help exits 0 and prints usage on stdout" ;; + *) + fail "--help printed no usage on stdout: '$help_out'" ;; + esac +else + fail "--help exited non-zero (rc=$help_rc): $help_out" +fi + +# --- 4. unknown option must exit 2, usage on stderr, silent stdout ------- +bogus_out= +bogus_rc=0 +bogus_out=$("$BIN" --bogus 2>&1 >/dev/null) || bogus_rc=$? +if [ "$bogus_rc" -eq 2 ]; then + case "$bogus_out" in + *"Usage: "*) + pass "--bogus exits 2 and prints usage on stderr" ;; + *) + fail "--bogus printed no usage on stderr: '$bogus_out'" ;; + esac +else + fail "--bogus exited rc=$bogus_rc (want 2): $bogus_out" +fi +if [ -z "$("$BIN" --bogus 2>/dev/null)" ]; then + pass "--bogus prints nothing on stdout" +else + fail "--bogus leaked output on stdout" +fi + +# --- 5. no arguments must exit 1 with usage on stderr --------------------- +noargs_out= +noargs_rc=0 +noargs_out=$("$BIN" 2>&1 >/dev/null) || noargs_rc=$? +if [ "$noargs_rc" -eq 1 ]; then + case "$noargs_out" in + *"Usage: "*) + pass "no arguments exits 1 and prints usage on stderr" ;; + *) + fail "no arguments printed no usage on stderr: '$noargs_out'" ;; + esac +else + fail "no arguments exited rc=$noargs_rc (want 1): $noargs_out" +fi + +# --- 6. one positional buildfile must be accepted (exit 0, no read) ------ +if "$BIN" buildfile.kdl >/dev/null 2>&1; then + pass "one positional buildfile is accepted (exit 0)" +else + fail "one positional buildfile was rejected (rc=$?)" +fi + +# --- 7. --version=1 is an unknown option form -> exit 2 ------------------- +"$BIN" --version=1 >/dev/null 2>&1 +rc=$? +if [ "$rc" -eq 2 ]; then + pass "--version=1 exits 2 (unknown option)" +else + fail "--version=1 exited rc=$rc (want 2)" +fi + +# --- 8. -- ends option parsing; following text is positional -------------- +if "$BIN" -- --bogus >/dev/null 2>&1; then + pass "-- terminates option parsing (--bogus taken as buildfile)" +else + fail "-- --bogus exited non-zero (rc=$?)" +fi + +# --- 9. two positionals are a usage error -> exit 2 ----------------------- +"$BIN" a.kdl b.kdl >/dev/null 2>&1 +rc=$? +if [ "$rc" -eq 2 ]; then + pass "two positionals exit 2 (usage error)" +else + fail "two positionals exited rc=$rc (want 2)" +fi + # --- summary ------------------------------------------------------------- if [ "$TESTS_FAILED" -ne 0 ]; then printf 'FAILED: %d/%d checks failed\n' "$TESTS_FAILED" "$TESTS_RUN" >&2