Template
feat(cli): add entrypoint with --help/--version and exit codes
This commit is contained in:
+2
-1
@@ -3,4 +3,5 @@
|
|||||||
AM_CFLAGS = -std=c23 -Wall -Wextra -Wpedantic
|
AM_CFLAGS = -std=c23 -Wall -Wextra -Wpedantic
|
||||||
|
|
||||||
bin_PROGRAMS = stupidtools
|
bin_PROGRAMS = stupidtools
|
||||||
stupidtools_SOURCES = main.c
|
stupidtools_SOURCES = main.c cli.c
|
||||||
|
noinst_HEADERS = cli.h
|
||||||
|
|||||||
@@ -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 <string.h>
|
||||||
|
|
||||||
|
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] <buildfile.kdl>\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;
|
||||||
|
}
|
||||||
@@ -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 <stdio.h>
|
||||||
|
|
||||||
|
/* 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 */
|
||||||
+31
-9
@@ -4,21 +4,43 @@
|
|||||||
* Copyright (c) 2026 huntedbytheirs
|
* Copyright (c) 2026 huntedbytheirs
|
||||||
* SPDX-License-Identifier: BSD-3-Clause
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
*
|
*
|
||||||
* Minimal skeleton: reports the version. Real CLI parsing (todo 4)
|
* Entrypoint: parse argv via cli.c, dispatch --help/--version/usage
|
||||||
* replaces this stub.
|
* errors, and reserve the `stupidtools <buildfile>` shape. Buildfile
|
||||||
|
* processing arrives in later todos (16+).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <stdio.h>
|
#include "cli.h"
|
||||||
|
|
||||||
/* Must match AC_INIT version in configure.ac (todo 1: 1.0.0). */
|
#include <stdio.h>
|
||||||
#define STUPIDTOOLS_VERSION "1.0.0"
|
|
||||||
|
|
||||||
int
|
int
|
||||||
main(int argc, char **argv)
|
main(int argc, char **argv)
|
||||||
{
|
{
|
||||||
(void)argc;
|
struct cli_opts opts;
|
||||||
(void)argv;
|
enum cli_action action;
|
||||||
|
|
||||||
printf("stupidtools %s\n", STUPIDTOOLS_VERSION);
|
action = cli_parse(argc, argv, &opts);
|
||||||
return 0;
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,88 @@ else
|
|||||||
fail "--version exited non-zero (rc=$rc): $version_out"
|
fail "--version exited non-zero (rc=$rc): $version_out"
|
||||||
fi
|
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 -------------------------------------------------------------
|
# --- summary -------------------------------------------------------------
|
||||||
if [ "$TESTS_FAILED" -ne 0 ]; then
|
if [ "$TESTS_FAILED" -ne 0 ]; then
|
||||||
printf 'FAILED: %d/%d checks failed\n' "$TESTS_FAILED" "$TESTS_RUN" >&2
|
printf 'FAILED: %d/%d checks failed\n' "$TESTS_FAILED" "$TESTS_RUN" >&2
|
||||||
|
|||||||
Reference in New Issue
Block a user