You are browsing as a guest. Sign up (or log in) to start making projects!

4h 46m 46s logged

Devlog #9: CLI Argument Parser (Commit: 870e5f9279)

7 changed files with 782 additions and 32 deletions

The CLI argument parser is Zinn’s final core module. It’s built entirely from scratch, without any external dependencies. Other modules (string views, error handling, logger) were designed to be used seamlessly, and this is the first proof.


The architecture

The parser is built around declarative option schemas:

const ZinnCliOption ZINN_CLI_OPTIONS[] = {
    {.long_name = ZINN_STRING_VIEW_INIT("width"),
     .short_name = 'w',
     .type = ZINN_CLI_TYPE_INT64,
     .offset_in_config = offsetof(ZinnConfig, width),
     ...},
};

Each option knows its type, whether it’s required, and exactly which byte offset inside ZinnConfig to write to. The parser walks argv, matches tokens against schema entries, and writes parsed values directly into the config struct via offsetof + pointer arithmetic.

This prevents huge bug-prone switch statements you’d have to write (or copy-paste) if using getopt, for example.

The schema validates itself at parse time: duplicate short/long names, offsets exceeding the config struct, zero options, and too many options all get caught before any argv is touched.

For the caller, using this module is rather simple. Declare the schema, call zinn_cli_parse( /*...*/ ), handle failure, and the config is provided nicely in a structure.


Following the code style

The original parsing function I wrote was a huge, monolithic function with over 200 lines. I extracted what I could:

  • _zinn_validate_schema: validates the schema itself
  • _zinn_pre_scan_help_version: early return for --help / --version before parsing errors can block them
  • _zinn_write_option_value: type-dispatching write via offsetof/switch

The main loop is still ~150 lines. The long-option and short-option matching share too many moving parts: the argv index i (which gets bumped for next-arg values), matched_index (required-flag tracking), the positional_only flag, and the token itself. Extracting them would mean threading half a dozen mutable out-parameters through a helper that’s only called once.

Technically, the parsing function breaks the code style, but it would’ve been too much trouble than its worth to try and extract anything more. (Also, I wrote the rules, so I can do whatever I want)


Safety

I own the entire error path. Every parse failure captures file, function, line, and a description in the diagnostic stack. Run --width=abc and you get a trace pointing to the exact switch case that failed, plus a Use ‘–help’ hint (see 2nd attachment). You don’t get that from getopt returning '?'.


What’s next?

After adding a new module, unit tests follow, as usual.

0
5

Comments 0

No comments yet. Be the first!