TypedArgs
Structured CLI arguments for mruby and CRuby. Lets you pass arrays, hashes, and arrays of records on the command line without quoting JSON or writing a config file.
--mode=fast
--item+=apple --item+=banana
--range:min,max:=5,10
--servers+:name,port:=alpha,80
--servers+:name,port:=beta,443
becomes
{
"mode" => "fast",
"item" => ["apple", "banana"],
"range" => {"min" => 5, "max" => 10},
"servers" => [
{"name" => "alpha", "port" => 80},
{"name" => "beta", "port" => 443}
]
}
It's a small, dependency-free parser that runs anywhere mruby runs (embedded, BusyBox, Alpine, sandboxed VMs) and on CRuby ≥ 2.5.
When you'd want this
You have a CLI that takes input that's genuinely structured — a list of servers, a set of feature toggles, a hash of coordinates — and the alternatives don't fit:
- JSON on argv means escaping
'{"servers":[{"name":"alpha"}]}'past the shell. - Repeating a flag with an ad-hoc convention (
--server alpha:80 --server beta:443) means inventing a mini-syntax per tool. - A config file is heavy when you only have three values to pass.
If your CLI takes scalars and the occasional list, a normal option parser is probably a better fit. TypedArgs earns its place when structure is the point.
The four operators
| Form | Meaning |
|---|---|
--key=value |
scalar |
--key+=value |
append to array |
--key:f1,f2:=v1,v2 |
hash with named fields |
--key+:f1,f2:=v1,v2 |
append a hash to an array |
Scalar values are auto-typed: integers, floats, true, false, nil, and otherwise strings. Tuple arity is enforced — declaring two fields means you must supply two values.
Usage
require "typedargs"
args = TypedArgs.opts("--mode=fast", "--debug=true")
args["mode"] # => "fast"
args["debug"] # => true
Calling TypedArgs.opts with no arguments reads from ARGV. In mruby you provide ARGV yourself; see tools/typedargs_test/test.c.
Keys
Keys are letters, digits, underscore, dash, and dot. They can't start with a digit or dash. Dotted keys (--db.host=localhost) are stored as flat strings, not nested hashes — args["db.host"], not args["db"]["host"]. Auto-nesting opens ambiguities the grammar avoids.
Short-flag aliases
TypedArgs.alias("-v", "--verbose")
TypedArgs.opts("-v") # => {"verbose" => true}
TypedArgs.opts("-vfoo") # => {"verbose" => "foo"}
The alias target can be any long-flag form, including operators:
TypedArgs.alias("-r", "--range:min,max:=")
TypedArgs.opts("-r5,10") # => {"range" => {"min" => 5, "max" => 10}}
TypedArgs.alias("-S", "--servers+:name,port:=")
TypedArgs.opts("-Salpha,80", "-Sbeta,443")
# => {"servers" => [{"name"=>"alpha","port"=>80}, {"name"=>"beta","port"=>443}]}
Aliases are textual rewrites performed before parsing. TypedArgs.reset_aliases! clears the alias map (useful in tests).
Override rules
Flags apply in argv order. Later flags overwrite earlier ones unless they're accumulating.
| Sequence | Result |
|---|---|
--foo=1 then --foo+=2 |
[2] |
--foo+=1 then --foo+=2 |
[1, 2] |
--foo:a,b:=1,2 then --foo:a,b:=3,4 |
{"a"=>3, "b"=>4} |
--foo+:a,b:=1,2 then --foo+:a,b:=3,4 |
[{"a"=>1,"b"=>2}, {"a"=>3,"b"=>4}] |
--foo=1, --foo+=2, --foo:n:=x, --foo=bar |
"bar" |
The operator on each flag determines the shape of the value at that key.
Errors
Invalid input raises a TypedArgs::SyntaxError subclass with a caret pointing at the offending byte:
--range:min,max:=5
^
Syntax error: Arity mismatch: expected 2, got 1
The exception classes are InvalidCharacterError, InvalidKeyStartError, UnterminatedStringError, ArityMismatchError, UnexpectedTokenError, InvalidSuffixPositionError, InvalidFieldListError, and InvalidNumberError.
Caveats
The shell still owns word-splitting and metacharacter handling — --secret=p$ssw0rd will need quoting in bash regardless of what TypedArgs does after argv arrives. Strictness around scalars (e.g. 12.34.56 raises rather than parsing as a string) is intentional but stricter than most option parsers.
Installation
For CRuby:
gem install typedargs
For mruby, add to your build_config.rb:
conf.gem mgem: 'typedargs'
License
Apache-2.0.