Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 

Repository files navigation

Tinyparser

A minimal C argument parser in 130 lines.

Drop tinyparser.c and tinyparser.h into your project and you're done.

Files

File Purpose
tinyparser.h Types, defines, and the initTable inline — include this everywhere
tinyparser.c Function definitions — compile this alongside your code
example.c A simple example

Setup

gcc yourfile.c tinyparser.c -o yourprogram

Concepts

The Argument Table

ArgTable is a fixed-size hash table (ARGPOOL_SIZE = 256 slots) of HashNode pointers. Each node holds two things:

  • setArgs — the argument definition you registered (Arg)
  • passedArgs — the value filled in after parse() runs (pArg)

Argument Definition (Arg)

typedef struct {
    char*   longName; // long form name, no leading dashes  e.g. "numbers-i-like", "numbers" and "nothing--to--see--here" are all valid here, while "--this-is-invalid", "-thisisinvalid", and "----invalid" are all invalid here.
    char    alias;    // single-char short form      e.g. 'n'  (0 for none)
    uint8_t flags;    // type/behavior flags
} Arg;

Parsed Value (pArg)

A pArg holds whichever union member matches the registered type:

union { bool bValue; char cValue; char* sValue; int iVal; int* aVal; };

Flags

The flags field is an 8-bit mask that controls how a value is stored. Tinyparser supports, as the name implies, a small amount of behavioral flags.

Bit Mask Meaning
0 0x01 Store false when the flag is present (default stores true)
1 0x02 Value is an int
2 0x04 Value is a string (char*)
3 0x08 Value is an array of the type in bit 1 or bit 2
4 0x10 Argument is required

Note

Tinyparser does NOT enforce the required flag (0x10).

Common flag combinations:

flags Type pArg member to read
0x00 Bool flag (stores true when present) bValue
0x01 Bool flag (stores false when present) bValue
0x02 Integer iVal
0x04 String sValue
0x0A Integer array (0x02 | 0x08) aVal

Integer arrays are length-prefixed, so aVal[0] holds the element count and the values start at aVal[1]. Both 0 and negative values are valid array elements.


API

void initTable(ArgTable* table)

Zero-initialises a table. Call this before anything else.

bool insertArg(ArgTable* table, Arg argument)

Registers an argument definition. Returns false if the slot is already occupied (hash collision or duplicate name).

bool parse(int argc, char** argv, ArgTable* table)

Walks argv and fills passedArgs for each recognised argument. Returns false if an unrecognised argument is encountered.

Accepts both long form (--name value) and short form (-n value). Whether a value token is consumed is determined by the argument's flags: typed args (int, string, array) always consume the next token unconditionally, so negative values like -6 work fine.

void* getArg(ArgTable* table, char* longName, bool getPargs)

Looks up a node by long name. Pass true to get a pArg* (parsed value), false to get an Arg* (definition). Cast the return value accordingly.

void freeTable(ArgTable* table)

Frees all heap-allocated nodes and nulls the slots. Call when done.


Hash Function

Tinyparser uses a small multiplicative hash seeded with two 64-bit constants:

#define K1 0x59757a7541696861ULL // YuzuAiha
#define K2 0x41696861724D6569ULL // AiharMei

uint64_t hashArg(char* argstr) {
    uint64_t value = K1;
    while (*argstr) { value *= K2 ^ (uint8_t)*argstr; argstr++; } // mix
    value ^= value >> 33; // avalanche
    return value % ARGPOOL_SIZE; // bucket index
}

For each character, the accumulator is multiplied by K2 XOR'd with the byte. After the loop, a final value ^= value >> 33 shift mixes the high bits down into the low bits (an avalanche step) so that strings differing only near the end still spread across the table. The result is reduced modulo ARGPOOL_SIZE (256) to get the table index.

Perfmarks (98 various CLI flag names, fixed seed, MinGW gcc -O2, AMD64):

Metric Value Notes
Throughput ~1,285 MB/s ~7 ns/hash
Avalanche 49.9% bits flipped ideal is 50%
Bucket change on 1-bit flip 99.4% ideal ~99.9%
Collisions (256 buckets, 98 keys) 19 random baseline ~16
Collisions (1024 buckets, 98 keys) 2 random baseline ~4.5
Chi² / dof (1024 buckets, 100k keys) 1.20 1.0 = perfectly uniform

Worked Examples

Basic setup

#include "tinyparser.h"
#include <stdio.h>

int main(int argc, char** argv) {
    ArgTable table;
    initTable(&table);

    insertArg(&table, (Arg){"verbose", 'v', 0x00}); // bool flag
    insertArg(&table, (Arg){"count",   'c', 0x02}); // int
    insertArg(&table, (Arg){"output",  'o', 0x04}); // string

    if (!parse(argc, argv, &table)) 
    {
        puts("unknown argument");
        return 1;
    }

    pArg* verbose = getArg(&table, "verbose", true);
    pArg* count   = getArg(&table, "count",   true);
    pArg* output  = getArg(&table, "output",  true);

    printf("verbose: %s\n",  verbose->bValue ? "true" : "false");
    printf("count:   %d\n",  count->iVal);
    printf("output:  %s\n",  output->sValue);

    freeTable(&table);
    return 0;
}
$ ./prog --verbose --count 10 --output result.txt
verbose: true
count:   10
output:  result.txt

Integer array

Pass comma-separated integers with no spaces. The array is length-prefixed: aVal[0] holds the element count, and the values live in aVal[1] through aVal[count]. Negative values and 0 are both valid.

insertArg(&table, (Arg){"ports", 'p', 0x0A}); // int array (0x02 | 0x08)
$ ./prog --ports 80,443,8080
pArg* ports = getArg(&table, "ports", true);
int* p = ports->aVal;
int iter = 1; // be nice to the stack :)
for (; iter <= p[0]; iter++) // p[0] is the count
    printf("port: %d\n", p[iter]);
port: 80
port: 443
port: 8080

Short-form alias

Any argument registered with a non-zero alias can be passed with a single dash.

insertArg(&table, (Arg){"output", 'o', 0x04});
$ ./prog -o result.txt

This is identical to --output result.txt.


Store-false flag

Set bit 0 to store false instead of true when the flag is present. Useful for opt-out flags.

insertArg(&table, (Arg){"no-color", 0, 0x01}); // stores false when passed
pArg* color = getArg(&table, "no-color", true);
// color->bValue == false when --no-color is passed

Limitations

  • No collision resolution. If two argument names hash to the same slot, insertArg returns false for the second one. Rename one of the arguments.
  • Typed args greedily consume the next token. Because an int/string/array arg always takes the following token as its value, you cannot pass a flag immediately after one without an intervening value (e.g. --count --verbose would consume --verbose as the count's value). Always supply the value.
  • Required flag (bit 4) is not enforced by parse. Check it yourself by walking the table after parsing if you need validation.

About

Minimal C argument parser in 130 lines

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages