OptParse.jl
A Type Stable Composable CLI Parser for Julia
OptParse is a command-line argument parser that emphasizes composability, type stability, and clarity. Heavily inspired by Optique and optparse-applicative, OptParse allows you to build complex argument parsers from simple, reusable components.
OptParse is in active development. The API is experimental and subject to change. Type stability is tested and promising, but needs more real-world validation.
Philosophy
The aim is to provide an argument parsing package for CLI apps that supports trimming.
In OptParse, everything is a parser. Complex parsers are built from simpler ones through composition. Following the principle of "parse, don't validate," OptParse returns exactly what you ask for—or fails with a clear explanation.
Each parser is a tree of subparsers. Leaf nodes do the actual parsing, intermediate nodes compose and orchestrate parsers to create new behaviours. Parsing is done in two passes:
- in the first, the input is checked against each branch of the tree until a match is found. Each node updates its state
to reflect if it succeded or not. This is the parse step.
- if the input match any of the branches we consider the step successful, otherwise we return the error of why it failed to match.
- the second pass is the
completestep. The tree is collapsed, eventual validation error handled and a final value returned.
Installation
using Pkg
Pkg.add(url="https://github.com/ghyatzo/OptParse")Quick Start
using OptParse
# Define a parser
parser = record((
name = option("-n", "--name", str("NAME")),
port = option("-p", "--port", integer("PORT"; min=1000)),
verbose = flag("-v", "--verbose")
))
# Parse arguments
result = optparse(parser, ["--name", "myserver", "-p", "8080", "-v"])
@assert result.name == "myserver"
@assert result.port == 8080
@assert result.verbose == trueCurrent parsing conventions:
- short option names are single-letter only
- short options must separate the flag from the value:
-n value - long options use the
--longform --means: from that point on, stop recognizing flags and options. Everything after it can be consumed by positional-style parsers
For the public entrypoints:
optparse(parser, argv)is the high-level convenience entrypointtryoptparse(parser, argv)is the lower-level entrypoint and returns a result container instead of throwingvaluetype(parser)returns the final value type produced by a parser
optparse has two modes controlled through the juliac key via Preferences.jl mechanisms:
- in normal Julia runtime usage, it returns the parsed value or throws
OptParse.ParseException - when
juliacmode is enabled, it renders the error tostderrand returnsnothingon failure instead of throwing
If you need stable non-throwing behavior across environments, use tryoptparse.
Core Concepts
OptParse provides four types of building blocks that compose together to create powerful CLI parsers:
Primitives
The fundamental parsers that match command-line tokens:
option- Matches key-value pairs:--port 8080or-p 8080flag- Optional boolean flags that default tofalseswitch- Required presence flags:--experimentalor-xarg- Positional arguments:source destinationcommand- Subcommands:git add file.txt@constant- Always returns a constant value
Value Parsers
Type-safe parsers that convert strings to values:
str- String values with optional pattern validationinteger/i8,u32, etc. - Integer types with min/max boundsflt/flt32,flt64- Floating point numberschoice- Enumerated values from a string list or@enumtypeuuid- UUID validationpath- Existing filesystem paths
When you want a named placeholder in help or usage, prefer the positional metavar form: str("FILE"), integer("PORT"), choice("MODE", Mode). The metavar= keyword still works, but the positional form is the main API.
The full constructor reference for value parsers is listed in the API reference. For older code, see the Migration Guide for public API renames.
Modifiers
Enhance parsers with additional behavior:
optional- Convenience wrapper fordefault(p, nothing)default- Provides a fallback valuemany/many1/repeated- Allows repeated matches, returns a vectorhelp- Attaches help text to a parser without changing parsing semanticshidden- Hides a parser from usage/help output without changing parsing semantics
Help annotations compose directly with normal parsers:
parser = command("serve", record((
host = option("--host", str("HOST")) |> help("Host", "Hostname to bind"),
port = default(option("--port", integer("PORT")), 8080) |> help("Port", "TCP port to listen on"),
verbose = flag("-v", "--verbose") |> help("Verbose", "Enable verbose logging"),
)))Those annotations do not change how parsing works. They are used when OptParse renders usage and high-level parse failures.
Constructors
Compose parsers into complex structures:
record- Named tuple of parsers (most common)or- Mutually exclusive alternatives (for subcommands)sequence- Ordered sequence of parsers (returns a tuple)combine/concat- Merge several parser groups
or(...) is order-dependent: branches are tried in the order they are listed, and the first semantic match wins. Put broader positional parsers like arg(...) or many(arg(...)) last.
Complete Application Example
Here's a more realistic example showing a package manager-style CLI:
using OptParse
# Shared options
commonOpts = record((
verbose = flag("-v", "--verbose"),
quiet = flag("-q", "--quiet")
))
# Add command
addCmd = command("add", combine(
commonOpts,
record((packages = many(arg(str("PACKAGE"))),))
))
# Remove command
removeCmd = command("remove", "rm", combine(
commonOpts,
record((
all = flag("--all"),
packages = many(arg(str("PACKAGE")))
))
))
# Instantiate command
instantiateCmd = command("instantiate", combine(
commonOpts,
record((
manifest = flag("-m", "--manifest"),
project = flag("-p", "--project")
))
))
# Complete parser
parser = or(addCmd, removeCmd, instantiateCmd)
# Usage examples:
# julia pkg.jl add DataFrames Plots -v
# julia pkg.jl remove --all -q
# julia pkg.jl instantiate --manifestType Stability
OptParse is designed for type stability. The return type of your parser is fully determined at compile time:
parser = record((
name = option("-n", str()),
port = option("-p", integer())
))
# Return type: @NamedTuple{name::String, port::Int64}
parser = or(
record((mode = @constant(:a), value = integer())),
record((mode = @constant(:b), value = str()))
)
# Return type: Union{@NamedTuple{mode::Val{:a}, ...}, NamedTuple{mode::Val{:b}, ...}}This means that Julia's compiler can optimize your parsing code effectively, and you get better performance and compile-time guarantees about the structure of your parsed results.
When you want to dispatch on parsed values, prefer constructing a named type with construct. For anonymous parser outputs, valuetype still gives you the resulting value type:
greet = command("greet", record((
cmd = @constant(:greet),
name = option("-n", str("NAME")),
)))
const Greet = valuetype(greet)
handle(x::Greet) = println("hello $(x.name)")Application Entry Points And Automatic Help
OptParse now has a more explicit split between parser semantics and application-facing CLI behavior.
At the lower level:
tryoptparsereturns a result containeroptparsereturns the parsed value or throws
Those entrypoints stay close to the parser model itself.
For command-line applications, OptParse also provides runparse, which adds a small amount of CLI policy on top:
- lexical help flags such as
--help - an optional top-level positional help command
- customizable behavior for bare invocation through
on_empty
parser = command("serve", record((
host = option("--host", str("HOST")),
port = default(option("--port", integer("PORT")), 8080),
)))
runparse(parser, ["--help"]; progname = "prog")
runparse(parser, []; progname = "prog", on_empty = ["help"])The lower-level help API is still available when you want full control:
If you want positional help explicitly in an ordinary parser tree, use helpcommand. It parses invocations such as help remote add into a HelpRequest, which higher-level entrypoints such as runparse can interpret by rendering focused help from the original parser.
This gives OptParse a clearer split:
- parser definitions remain the single source of truth
- help rendering remains explicit and compositional
- automatic help behavior lives in a dedicated top-level runner
Typed Parsers And Construction
Anonymous parser outputs are still central to OptParse, but there is now a more complete story for constructing named application types.
The flexible path is construct:
struct ServerConfig
host::String
port::Int
end
parser = construct(ServerConfig, record((
host = option("--host", str("HOST")),
port = option("--port", integer("PORT")),
)))construct delegates to StructUtils, which makes it the dynamic and user-extensible path. In normal Julia runtime, that means it can take advantage of richer lifting behavior, including custom StructUtils integration and parametric construction when that can be recovered dynamically.
For trimming and exact matching, OptParse also provides construct_exact. This path is deliberately stricter:
- for
record(...), field names and order must match exactly - for
sequence(...), positional arity and types must match exactly - the target type must be concrete
On top of that, @parser offers a concise typed workflow that defines a struct and its matching parser together:
parser = @parser "Server configuration" Config begin
"Hostname to bind"
host = option("--host", str("HOST"))
"TCP port"
port = default(option("--port", integer("PORT")), 8080)
end "Used by the development server."The macro derives struct field types from the parser expressions themselves via valuetype, so the struct shape stays aligned with the parser output.
So the typed story now has three layers:
- anonymous
record(...)/sequence(...)composition - dynamic lifting with
construct - exact, trim-friendly typed construction with
construct_exactand@parser
Error Handling
OptParse exposes two entrypoints:
parser = option("-p", integer("PORT"; min=1000))
# Throwing API
value = optparse(parser, ["-p", "3000"])
# Lower-level API
result = tryoptparse(parser, ["-p", "3000"])optparse returns the parsed value on success and throws OptParse.ParseException on failure. tryoptparse returns a result container instead of throwing, which is useful if you want to inspect failures programmatically.
Rendered error messages are produced centrally from structured internal diagnostics. The exact wording may evolve, but failures are surfaced with parser-specific context, for example invalid values, missing required inputs, or unexpected arguments. In the high-level optparse path, the exception renderer also appends a focused usage line derived from the parser tree.
parser = option("-p", integer("PORT"; min=1000))
try
optparse(parser, ["-p", "abc"])
catch err
@assert err isa OptParse.ParseException
endInternals
If you want to understand the parser runtime or contribute a new parser family or value parser, see the development guide.
Contributing
Contributions are welcome! Please feel free to submit issues or pull requests on GitHub.
Acknowledgments
OptParse's design is inspired by:
- Optique - Typescript CLI parsing library with similar composable design
- optparse-applicative - Haskell command-line parser that pioneered this approach
License
OptParse is released under the MIT License.