A Type Stable Composable CLI Parser for Julia, inspired by optparse-applicative and Optique.
Warning
Work In Progress: OptParse is in active development. The API is experimental and subject to frequent change. Type stability is tested and promising, but needs more real-world validation.
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
parsestep. - 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 object returned.
- automatic usage and help printing
- more value parsers (like dates, URIs, date-times...)
-
mapmodifier, unfortunately until julia has something likeTypedCallabes it's impossible to ensure type stability with arbitrary functions. -
longest-matchcombinator -
groupcombinator: light simple parser useful only for enclosing multiple parsers together in the same category. mainly useful for help messages. - automatic suggestions / shell completions
using OptParse
# Define a parser
parser = object((
name = option("-n", "--name", str("NAME")),
port = option("-p", "--port", integer("PORT"; min=1000)),
verbose = flag("-v", "--verbose")
))
# Parse arguments
result = argparse(parser, ["--name", "myserver", "-p", "8080", "-v"])
@assert result.name == "myserver"
@assert result.port == 8080
@assert result.verbose == trueThe style implemented in this library is the following:
- short form names only accept single letters:
-nis fine,-runwill be treated as bundled-r -u -n. - short form options must separate the flag from the value:
-n name. No gcc style-L/usr/include. - long form is represented with two dashes
--long --means: from that point on, stop recognizing flags and options. Everything after it can be consumed by positional-style parsers.
For the public entrypoints:
argparse(parser, argv)is the high-level convenience entrypointtryargparse(parser, argv)is the lower-level entrypoint and returns a result object instead of throwingresulttype(parser)returns the final value type produced by a parser
argparse 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 tryargparse.
OptParse provides four types of building blocks:
The fundamental parsers that match command-line tokens:
option- Matches key-value pairs:--port 8080or-p 8080flag- Optional boolean flags like:--verboseor-vgate- Required presence flags used to guard a branch or featurearg- Positional arguments:cp source destinationcommand- Subcommands:git add file.txt
# Options with different styles
port = option("-p", "--port", integer("PORT"))
result = argparse(port, ["--port=8080"]) # Long form with =
result = argparse(port, ["-p", "8080"]) # Short form
# Flags can be bundled
parser = object((
all = gate("-a"),
long = gate("-l"),
human = gate("-h")
))
result = argparse(parser, ["-alh"]) # Equivalent to ["-a", "-l", "-h"]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
# Type-safe parsing with constraints
port = option("-p", integer("PORT"; min=1000, max=65535))
level = option("-l", choice("LEVEL", ["debug", "info", "warn", "error"]))
config = option("-c", str("FILE"; pattern=r".*\.toml$"))
@enum Mode begin
Debug
Release
end
mode = option("--mode", choice("MODE", Mode))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.
Enhance parsers with additional behavior:
optional- Convenience wrapper fordefault(p, nothing)default- Provides a fallback valuemultiple- Allows repeated matches, returns a vector
# Optional values
email = optional(option("-e", "--email", str("EMAIL")))
# With defaults
port = default(option("-p", integer("PORT")), 8080)
# Multiple values
packages = multiple(arg(str("PACKAGE"))) # pkg add Package1 Package2 Package3
# Verbosity levels
verbosity = multiple(gate("-v")) # -v -v -v or -vvvCompose parsers into complex structures:
object- Named tuple of parsers (most common)or- Mutually exclusive alternatives (for subcommands)sequence- Ordered sequence of parsers (returns a tuple)combine/concat- Merge multiple 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 multiple(arg(...)) last.
# Object composition
parser = object((
input = arg(str("INPUT")),
output = option("-o", "--output", str("OUTPUT")),
force = flag("-f", "--force")
))
# Alternative commands with or
addCmd = command("add", object((
action = @constant(:add),
packages = multiple(arg(str("PACKAGE")))
)))
removeCmd = command("remove", object((
action = @constant(:remove),
packages = multiple(arg(str("PACKAGE")))
)))
pkgParser = or(addCmd, removeCmd)Here's a more realistic example showing subcommands:
using OptParse
# Shared options
commonOpts = object((
verbose = flag("-v", "--verbose"),
quiet = flag("-q", "--quiet")
))
# Add command
addCmd = command("add", combine(
commonOpts,
object((packages = multiple(arg(str("PACKAGE"))),))
))
# Remove command
removeCmd = command("remove", "rm", combine(
commonOpts,
object((
all = flag("--all"),
packages = multiple(arg(str("PACKAGE")))
))
))
# Instantiate command
instantiateCmd = command("instantiate", combine(
commonOpts,
object((
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 --manifestOptParse is designed for type stability. The return type of your parser is fully determined at compile time:
parser = object((
name = option("-n", str()),
port = option("-p", integer())
))
# Return type: @NamedTuple{name::String, port::Int64}
parser = or(
object((mode = @constant(:a), value = arg(integer()))),
object((mode = @constant(:b), value = arg(str())))
)
# Return type: Union{@NamedTuple{mode::Val{:a}, ...}, @NamedTuple{mode::Val{:b}, ...}}If you want to dispatch on the output of a specific parser, expose the type
through resulttype:
greet = command("greet", object((
cmd = @constant(:greet),
name = option("-n", str("NAME")),
)))
const Greet = resulttype(greet)
handle(x::Greet) = println("hello $(x.name)")OptParse exposes two entrypoints:
parser = option("-p", integer(min=1000))
# Throwing API
value = argparse(parser, ["-p", "3000"])
# Lower-level API
result = tryargparse(parser, ["-p", "3000"])argparse returns the parsed value on success and throws OptParse.ParseException on failure.
tryargparse returns a result object 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.
parser = option("-p", integer(min=1000))
try
argparse(parser, ["-p", "abc"])
catch err
@assert err isa OptParse.ParseException
endusing Pkg
Pkg.add("OptParse")Comprehensive documentation is available through Julia's help system:
julia> using OptParse
julia> ?option
julia> ?object
julia> ?or
For more detailed documentation, see the Documentation.
Additionally, the Optique's excellent documentation website or docs from the great optparse-applicative, might be of interest for more in depth analysis or phylosophy. There are some differences but the core concepts are the same since both have been an inspiration on the design of this library.
Contributions are welcome! Please feel free to submit issues or pull requests.
MIT License. See LICENSE for details.
- optparse-applicative - Haskell command-line parser
- Optique - Typescript CLI parsing library