A minimal C argument parser in 130 lines.
Drop tinyparser.c and tinyparser.h into your project and you're done.
| 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 |
gcc yourfile.c tinyparser.c -o yourprogramArgTable 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 afterparse()runs (pArg)
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;A pArg holds whichever union member matches the registered type:
union { bool bValue; char cValue; char* sValue; int iVal; int* aVal; };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.
Zero-initialises a table. Call this before anything else.
Registers an argument definition. Returns false if the slot is already occupied (hash collision or duplicate name).
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.
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.
Frees all heap-allocated nodes and nulls the slots. Call when done.
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 |
#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
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
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.
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 passedpArg* color = getArg(&table, "no-color", true);
// color->bValue == false when --no-color is passed- No collision resolution. If two argument names hash to the same slot,
insertArgreturnsfalsefor 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 --verbosewould consume--verboseas 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.