[ANN] OptParse.jl – a composable, type-stable CLI parser
Hi all,
I’ve been working on OptParse.jl, a command-line argument parser for Julia built around three main ideas:
- type stability: the main goal was to make a CLI library that works well with trimming.
- composability: an ‘everything is a parser’ approach, where larger CLIs are built from small reusable pieces.
- parse, don’t validate: validation is embedded in the parser itself, so a successful parse already gives you a valid result.
repo: OptParse.jl
docs: OptParse docs
The design is inspired by libraries like optparse-applicative (Haskell) and Optique (TypeScript), but adapted to Julia’s type system and compilation model.
Quick example
using OptParse
parser = object((
name = option("-n", "--name", str("NAME")),
port = option("-p", "--port", integer("PORT"; min = 1000)),
verbose = flag("-v", "--verbose"),
))
result = argparse(parser, ["--name", "myserver", "-p", "8080", "-v"])
@assert result.name == "myserver"
@assert result.port == 8080
@assert result.verbose == true
The API is organized around a few kinds of “parser building blocks”:
Primitive parsers
match basic CLI structure such as options, flags, positional arguments, and commands
input = arg(str("INPUT"))output = option("-o", "--output", str("OUTPUT"))verbose = flag("-v", "--verbose")
Value parsers
Convert raw strings into typed validated values.
These are responsible for turning a matched string into a typed valid value.
They are the validation layer of the system.
integer("PORT"; min = 1024, max = 65535)choice("MODE", ["debug", "release"])
Constructors
Combine smaller parsers into larger ones.
object(...)for named collections of parsersor(...)for alternatives
This is what makes subcommands and larger application parsers ergonomic to express.
Modifiers
Adjust parser behavior, for example by making something optional or repeatable
default(p, value)optional(p)multiple(p)
Bigger Demo
module HelloWorld
using OptParse
const hello = command("hello", object((;
cmd = @constant(:hello),
name = option("-n", "--name", str("NAME")),
)))
const goodbye = command("goodbye", object((;
cmd = @constant(:goodbye),
name = option("-n", "--name", str("NAME")),
)))
const parser = or(hello, goodbye)
const Hello = resulttype(hello)
const Goodbye = resulttype(goodbye)
runaction(x::Hello) = println(Core.stdout, "Hello, $(x.name)!")
runaction(x::Goodbye) = println(Core.stdout, "Goodbye, $(x.name)!")
function @main(args::Vector{String})::Cint
obj = argparse(parser, args)
isnothing(obj) && return 1
runaction(obj)
return 0
end
end # module HelloWorld
and then after compiling with juliac
$ helloworld hello --name OptParse
Hello, OptParse!
$ helloworld goodbye --name OptParse
Goodbye, OptParse!
Extensibility
The package is extensible in design, but today new parser families and value parsers still need package-level integration to preserve type stability and trimming behavior.
Current status
This is still experimental and under active development. This means a lot of churn.
Next steps:
- automatic usage/help generation (ongoing)
- some API polish and changes
- broader real-world validation
- extra parser types that are still missing
- extra value parsers
Feedback welcome
The user-facing layer is still intentionally a bit minimal; I’d rather add convenience APIs based on actual usage than guess wrong too early.
I’d especially like feedback on:
- API ergonomics
- dispatching mechanism of the parse result
- readability of parser definitions for medium/large CLIs
- expected help/usage behavior and style
- missing parser/value-parser combinators
Moreover, the combinator surface is large enough that real-world stress testing would be especially valuable.
Acknowledgements
This library has very few dependencies but those few have been essential:
- ErrorTypes.jl
- WrappedUnions.jl
- Accessors.jl
Thanks for the amazing work on these!
That’s it, hope you’ll like it!
Cheers