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 object returned.
Installation
using Pkg
Pkg.add(url="https://github.com/ghyatzo/OptParse")Quick Start
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 == 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:
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.
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 tofalsegate- 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.
Modifiers
Enhance parsers with additional behavior:
optional- Convenience wrapper fordefault(p, nothing)default- Provides a fallback valuemultiple- Allows repeated matches, returns a vector
Constructors
Compose 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.
Complete Application Example
Here's a more realistic example showing a package manager-style CLI:
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 --manifestType Stability
OptParse 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 = integer())),
object((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 the result of a specific parser, use resulttype:
greet = command("greet", object((
cmd = @constant(:greet),
name = option("-n", str("NAME")),
)))
const Greet = resulttype(greet)
handle(x::Greet) = println("hello $(x.name)")Error Handling
OptParse exposes two entrypoints:
parser = option("-p", integer("PORT"; 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("PORT"; min=1000))
try
argparse(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.