# \[ANN\] OptParse.jl – a composable, type-stable CLI parser

**URL:** <https://discourse.julialang.org/t/ann-optparse-jl-a-composable-type-stable-cli-parser/136580>\
**Category:** Package Announcements\
**Tags:** package, parsing, cli, trim\
**Created:** [April 5, 2026, 10:56pm UTC](https://discourse.julialang.org/t/ann-optparse-jl-a-composable-type-stable-cli-parser/136580 "2026-04-05T22:56:52Z")\
**Posts on this page:** 16\
**Page:** 1

<div class="post-metadata">

**Author:** ![cshen](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/cshen/32/217287_2.png) [@cshen](https://discourse.julialang.org/u/cshen)\
**Post date:** [April 5, 2026, 10:56pm UTC](https://discourse.julialang.org/t/ann-optparse-jl-a-composable-type-stable-cli-parser/136580/1 "2026-04-05T22:56:52Z")

</div>

# [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](https://github.com/ghyatzo/OptParse.jl)  
docs: [OptParse docs](https://ghyatzo.github.io/OptParse.jl/dev/)

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

```julia
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 parsers
- `or(...)` 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

```julia
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`

```julia-auto
$ 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

---

<div class="post-metadata">

**Author:** ![ericphanson](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/ericphanson/32/215186_2.png) [@ericphanson](https://discourse.julialang.org/u/ericphanson)\
**Post date:** [April 6, 2026, 10:43am UTC](https://discourse.julialang.org/t/ann-optparse-jl-a-composable-type-stable-cli-parser/136580/2 "2026-04-06T10:43:28Z")

</div>

This looks quite nice!

Two little nitpicks (while its still experimental):

- I think `object` is too general of a name; in the context of a bigger program it won’t be immediately obvious that it has to do with the parsing portion. In your example, naming the variable `parser` helps; instead could that be the name of the function? `p = parser(...)`?
- `OptParse` vs `argparse` seems like a mismatch, why not `OptParse` and `optparse` ?

---

<div class="post-metadata">

**Author:** ![klafyvel](https://avatars.discourse-cdn.com/v4/letter/k/a587f6/32.png) [@klafyvel](https://discourse.julialang.org/u/klafyvel)\
**Post date:** [April 6, 2026, 7:08pm UTC](https://discourse.julialang.org/t/ann-optparse-jl-a-composable-type-stable-cli-parser/136580/3 "2026-04-06T19:08:46Z")

</div>

Nice library, I’ve been trying hard to prevent myself from writing one with a similar scope for the past few weeks, I’m happy that someone did it!

---

<div class="post-metadata">

**Author:** ![cshen](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/cshen/32/217287_2.png) [@cshen](https://discourse.julialang.org/u/cshen)\
**Post date:** [April 6, 2026, 7:24pm UTC](https://discourse.julialang.org/t/ann-optparse-jl-a-composable-type-stable-cli-parser/136580/4 "2026-04-06T19:24:00Z")

</div>

Why not `optparse`? Because I just got so used to `argparse` that i never even thought about using `optparse` instead which makes much more sense. Thanks for the fresh set of eyes.

Regarding `object`, I agree that it is a bit too generic, I think that in the context of the library in a vacuum it does make sense, but you raise a good point that in more complex codebases it could be a bit confusing. I kind of like `parser` from a user perspective but from an internal point of view I’m not sure I’d like to have such ambiguity, but probably I’m just biased having spent too much time with this. Thanks for the feedback, much appreciated!

---

<div class="post-metadata">

**Author:** ![TheLateKronos](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/thelatekronos/32/12824_2.png) [@TheLateKronos](https://discourse.julialang.org/u/TheLateKronos)\
**Post date:** [April 7, 2026, 4:26am UTC](https://discourse.julialang.org/t/ann-optparse-jl-a-composable-type-stable-cli-parser/136580/5 "2026-04-07T04:26:27Z")

</div>

Congratz on your package! But I do have to ask: Why a new package, and not an improvement PR to ArgParse.jl? [Docs](https://carlobaldassi.github.io/ArgParse.jl/stable/)

---

<div class="post-metadata">

**Author:** ![cshen](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/cshen/32/217287_2.png) [@cshen](https://discourse.julialang.org/u/cshen)\
**Post date:** [April 7, 2026, 8:15am UTC](https://discourse.julialang.org/t/ann-optparse-jl-a-composable-type-stable-cli-parser/136580/6 "2026-04-07T08:15:36Z")

</div>

Because this package has a foundamentally different design. ArgParse is macro based, while this has a compositional/functional approach. There are just too many differences for this to be a simple improvement PR to another package.

---

<div class="post-metadata">

**Author:** ![penelopeysm](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/penelopeysm/32/213172_2.png) [@penelopeysm](https://discourse.julialang.org/u/penelopeysm)\
**Post date:** [April 8, 2026, 2:41pm UTC](https://discourse.julialang.org/t/ann-optparse-jl-a-composable-type-stable-cli-parser/136580/7 "2026-04-08T14:41:27Z")

</div>

I like Haskell and optparse-applicative a _lot_, so am very pleased to hear about this!

Are NamedTuples meant to be the ‘blessed’ interface? One nice thing about optparse-applicative is that you can define e.g. some config struct

```haskell
data Config = Config {
    port::Int -- ...
}

```

and then parse directly into that. Is there an easy way to do that if I define something like this in Julia?

```julia
struct Config
   port::Int # ...
end

```

I know that ultimately NamedTuples and structs are really the same thing, but structs are easier to dispatch on and to reason about since they can be documented & their fields are easy to see, etc.

I’m wondering, in particular, if there’s an easy way to derive parsers for structs based on some introspection. I wouldn’t be surprised if Accessors (or ConstructionBase?) has some code for that already.

---

<div class="post-metadata">

**Author:** ![cshen](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/cshen/32/217287_2.png) [@cshen](https://discourse.julialang.org/u/cshen)\
**Post date:** [April 8, 2026, 8:45pm UTC](https://discourse.julialang.org/t/ann-optparse-jl-a-composable-type-stable-cli-parser/136580/8 "2026-04-08T20:45:51Z")

</div>

Glad you like this!

Very good question. One of the benefits that having such a composable design buys you is that this is basically a DSL, that you can sort of easily lower other syntaxes into. There is definitely a lot of unexplored design space on higher level wrappers for this that I haven’t had the time to properly explore yet.

Your struct idea is something that I’ve been playing a bit as well, especially for `objects`. But one problem I’ve been facing is that it’s easy to get the return type of a parser, but very hard to get the parser from the return type, since the return type is only a small aspect of a parser, and you’d have to find a way to cram all that behaviour information into and around the return type itself, which probably Haskel has a way of doing that Julia does not (that i know of), other than some custom syntax via macros.

You can definitely define a macro that can be used to go from something like this:

```julia
@magicmacro struct MyOptions
    "NAME: this is the help text"
    name::String -- option("-n", "--name") 
end

```

back to the actual parser, but I am not sure that buys you much more than just going:

```julia
const _MyOption = (
    name = option("-n", "--name", str("NAME");
        help = "this it the help text")
)
const MyOption = resulttype(_MyOption)

```

with which you can then dispatch on `MyOption` just as easily (almost, having anonymous named tuples types means that you can’t distinguish two structures if they have the same fields. This is the sole reason by the `@constant` parser is a thing, it’s basically a way to tag the anonymous struct).

There’s definitely a lot left to explore though!

---

<div class="post-metadata">

**Author:** ![singularitti](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/singularitti/32/17678_2.png) [@singularitti](https://discourse.julialang.org/u/singularitti)\
**Post date:** [April 8, 2026, 9:56pm UTC](https://discourse.julialang.org/t/ann-optparse-jl-a-composable-type-stable-cli-parser/136580/9 "2026-04-08T21:56:41Z")

</div>

Has anyone tried my package [ComposableCommands.jl](https://github.com/singularitti/ComposableCommands.jl) yet? It’s not a parser per se. It’s more like a helper to construct complex commands in a structured way and intepret them into Julia `Cmd` type.

---

<div class="post-metadata">

**Author:** ![penelopeysm](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/penelopeysm/32/213172_2.png) [@penelopeysm](https://discourse.julialang.org/u/penelopeysm)\
**Post date:** [April 8, 2026, 11:51pm UTC](https://discourse.julialang.org/t/ann-optparse-jl-a-composable-type-stable-cli-parser/136580/10 "2026-04-08T23:51:02Z")

</div>

Thanks, @cshen! I guess I’m mostly thinking about how to replicate the applicative functor part of it, which is imo what makes those libraries so elegant. Fundamentally we’d need a function `A -> B -> Object` which can then be lifted to get `Parser A -> Parser B -> Parser Object`, and that’s very natural in Haskell because constructors of record types (i.e. structs) have exactly that signature, but it might be a bit more awkward in Julia because of the lack of currying (I think it should still be possible though). Right now in the examples you specify what’s essentially a NamedTuple that holds `Parser A` and `Parser B` and then OptParse joins those to get a `Parser (NamedTuple((:a, :b))`, but I wonder what’s the missing bit of info that we need to instead join them into a `Parser Object`, and that has to be somehow related to the constructor of `Object`, but I haven’t thought deeper than that. (My type notation here is a bad mishmash of Haskell and Julia, sorry!)

As it happens, I was thinking about this the other day as I just started an OCaml project (unsurprisingly the main CLI parser [cmdliner](https://github.com/dbuenzli/cmdliner) also uses applicative-style parsing) and wondering whether there was something similar in Julia so this is very timely, excited to see where you go with it 🙂

---

<div class="post-metadata">

**Author:** ![klafyvel](https://avatars.discourse-cdn.com/v4/letter/k/a587f6/32.png) [@klafyvel](https://discourse.julialang.org/u/klafyvel)\
**Post date:** [April 9, 2026, 8:08am UTC](https://discourse.julialang.org/t/ann-optparse-jl-a-composable-type-stable-cli-parser/136580/11 "2026-04-09T08:08:35Z")

</div>

You might want to have a look into [GitHub - JuliaServices/StructUtils.jl · GitHub](https://github.com/JuliaServices/StructUtils.jl) to get a macro that takes care of the magic without having to put your own hands into macro stuff!

---

<div class="post-metadata">

**Author:** ![cshen](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/cshen/32/217287_2.png) [@cshen](https://discourse.julialang.org/u/cshen)\
**Post date:** [April 9, 2026, 12:28pm UTC](https://discourse.julialang.org/t/ann-optparse-jl-a-composable-type-stable-cli-parser/136580/12 "2026-04-09T12:28:44Z")

</div>

That’s certainly interesting and promising, I’ll keep this in mind, Thanks!

---

<div class="post-metadata">

**Author:** ![RomeoV](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/romeov/32/37687_2.png) [@RomeoV](https://discourse.julialang.org/u/RomeoV)\
**Post date:** [April 12, 2026, 10:16am UTC](https://discourse.julialang.org/t/ann-optparse-jl-a-composable-type-stable-cli-parser/136580/13 "2026-04-12T10:16:38Z")

</div>

Looks great. I made something similar a while ago: [GitHub - RomeoV/TrimmableCLIParser.jl · GitHub](https://github.com/RomeoV/TrimmableCLIParser.jl)

---

<div class="post-metadata">

**Author:** ![cshen](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/cshen/32/217287_2.png) [@cshen](https://discourse.julialang.org/u/cshen)\
**Post date:** [May 13, 2026, 11:19pm UTC](https://discourse.julialang.org/t/ann-optparse-jl-a-composable-type-stable-cli-parser/136580/14 "2026-05-13T23:19:01Z")

</div>

Hi, it’s been a while. Let’s dive in:

# OptParse v0.3.0

This is the first breaking change! Here’s a quick rundown of what changed.

## 1. Public API rename and cleanup 🚲 🛖

This is where most of the breaking comes from. Hopefully this new set of names makes the code read better and more intentional.

- `argparse`/`tryargparse` → `optparse`/`tryoptparse`
- `object` → `record`
- `multiple` → `many` (zero or more) / `many1` (one or more) / `repeated` (custom)
- `gate` → `switch`
- `resulttype` → `valuetype`
- various public keyword args were all normalized to `snake_case` 🐍

Ok, this was the boring part. The big first new thing is

## 2. Help Metadata and Automatic Help Generation 🆘

### Help metadata

After… _check notes_… 7 attempts I finally managed to get the help system in a decent enough shape to be put to the test.

The help information is not embedded inside the single parsers but instead lives on its own and can be attached to whatever parser you want. This allows composability and reusability of smaller parser without having to juggle multiple descriptions

```julia-auto
const reuse = option("--name", str("NAME"))

const p1 = record((
    name = reuse |> help("description relative to p1")
))

const p2 = record((
    name = reuse |> help("description relative to p2")
))

```

Another benefit of keeping the help metadata separate is that for parsers that require big descriptions, you can move the prose away from the parsing logic, which helps to keep things terse.

you can stack `help` messages, and they will just be merged together with the latest application overwriting previous ones.

The help metadata is gathered and then used to construct a help message when requested, like so:

```julia-auto
Gitlike

A non-trivial git-inspired CLI used to stress `OptParse` parsing, help
generation, and trimming.

The example intentionally mixes global options, nested subcommands, repeated
arguments, and mutually exclusive groups.

Usage: <COMMAND> [ARGS...] [OPTIONS]

Commands:
   status Show working tree status
   add Add file contents to the index
   commit Record changes to the repository
   clone Clone a repository into a new directory
   push Update remote refs
   remote Manage configured remotes

Options:
   -C <PATH> Working directory
   -c <KEY=VALUE> Config override
   [--paginate] Paginate
   [--no-pager] No pager
   [--version] Version

Examples:
  gitlike status --short
  gitlike commit -m "initial import"
  gitlike remote add origin https://example/repo.git

```

Which brings me to

### Automatic Help Generation

Automatic help generation comes mainly in the form of an application level entry point `runparse(parser, argv; ...)`. This new entry point wraps the basic `optparse` entry point that takes care of the actual parsing semantics and adds a small layer of CLI policy on top of it:

- lexical help flags such as `--help`: `prog cmd --help`
- an optional top level positional help command: `prog help cmd subcmd `
- customizable behaviour for bare invocation: “what does `prog` do”?

If more control is required, there is the lower level API available that allows you to

- generate the raw string: `generate_help(parser, argv; progname=...)`
- print the help as needed: `print_help(io, parser, argv; progname=...)`

If you want positional help explicitly somewhere inside the parser tree, there is a `helpcommand()` helper that parses invokations such as `help cmd subcmd` into a `HelpRequest`, which can be used to render a desired focused help as needed.

How do you parse into a `HelpRequest`? Great question thank you for asking:

## 3. Typed Parsers and Construction 🛠

Anonymous named tuples are still central to OptParse composable model, but there is now a much better story on how to construct and dispatch on named application types.

### dynamic construction: `construct`.

```julia
struct ServerConfig
    host::String
    port::Int
end

parser = construct(ServerConfig, record((
    host = option("--host", str("HOST")),
    port = option("--port", integer("PORT"))
))

```

Under the hood simply delegates to `StructUtils.make`. In a normal Julia runtime, that means that it can use the full lifting machinery and behaviours from `StructUtils` to construct custom types.

As a small bonus that came for free from the composability is the ability to construct parametric types directly with the correct type inferred from the result type of the parser.

```julia-auto
struct Point{T}
    x::T
    y::T
end

parser = construct(Point, sequence(arg(integer()), arg(integer())))
#::Point{Int64}

```

### Exact construction: `construct_exact`

Unfortunately all the introspection going on in `StructUtils` make it not very amenable to trimming.  
But you can opt in (pun intended) into stricter semantics with `construct_exact(T, parser)`. This path requires exact shape agreement between the parser output and the target type (checked during construction (pun not intended) of the parser).

- for `record` children, field names and order must match the struct exactly
- for `sequence` children, positional arity and types must match exactly
- the target type must be concrete

This provides a narrower surface for the verifier to check and therefore be happy about.

This helps a lot the dispatching story after the parsing is done. Before you had to do

```julia-auto
const parser = ...
const MyType = resulttype(parser)

doaction(r::MyType) = ...

```

where the actual shape of `MyType` was mixed within the parser behaviour and hard to reason about. Now you’d do

```julia-auto
struct MyType
    ...
end
const parser = construct(MyType, ...)

doaction(r::MyType) = ....

```

which is much more declarative and nicer to read and understand what’s going on.

Ok, but that looks like a lot of typing, And what if I assume the wrong return type for one of my parser fields?

## 4. @parser macro 🪛

This is a very lightweight macro that helps with setting up a parser that construct a user defined type automatically, taking care of the actual type of the fields.

```julia-auto
some_complicated_subparser = or(...)

mytype_p = @parser MyType begin
    "Some description"
    anoption = flag("-f", "--flag")
 
    "Another option"
    branch = some_complicated_subparser

    "Some args for good measure"
    positional = many(arg(str()))
end

```

this lower to roughly something like

```julia-auto
struct MyType
    anoption::valuetype(...)
    branch::valuetype(some_complicated_subparser)
    positional::valuetype(...)
end

mytype_p = construct_exact(MyType, record((
    anoption = flag("-f", "--flag")
    branch = some_complicated_subparser
    positional = many(arg(str()))
)))  

```

This throws away a bit of flexibility for ease of construction and terseness. It’s intended for simpler parsers that wouldn’t really make use of all the composability features, so that the happy path is nice and smooth, while still keeping the ability to construct more complicated and custom parsers as needed.

That’s it, apologies for the long read, and as always: Feedbacks and complaints are welcome!

---

<div class="post-metadata">

**Author:** ![klafyvel](https://avatars.discourse-cdn.com/v4/letter/k/a587f6/32.png) [@klafyvel](https://discourse.julialang.org/u/klafyvel)\
**Post date:** [May 14, 2026, 1:51pm UTC](https://discourse.julialang.org/t/ann-optparse-jl-a-composable-type-stable-cli-parser/136580/15 "2026-05-14T13:51:14Z")

</div>

That looks so great! I’ll have a good occasion to try it soon. 🙂

---

<div class="post-metadata">

**Author:** ![cshen](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/cshen/32/217287_2.png) [@cshen](https://discourse.julialang.org/u/cshen)\
**Post date:** [June 28, 2026, 10:48pm UTC](https://discourse.julialang.org/t/ann-optparse-jl-a-composable-type-stable-cli-parser/136580/16 "2026-06-28T22:48:10Z")

</div>

Hello there folks,

Some more time has passed, and things have changed. This is a small update on what happened. This is a new breaking change, but it’s very limited in scope as it only touches the `@parser` macro and a very niche usage of the `str` value parser, if you weren’t using those before, you’re good to go.

# v0.4.0 Breaking changes:

#### new `@parser`

Let’s start with the main breaking change, the `@parser` macro got a “facelift”, as in, both a new face and a `lift`.

**Before:**

```julia
parser = @parser "Server configuration" Config begin
    "Host"
    host = option("--host", str("HOST"))
    "Port"
    port = option("--port", integer("PORT"))
end "Server configuration parser"

```

**After:**

```julia
@parser struct Config
    @description "Server configuration"
    "Host"
    host = option("--host", str("HOST"))
    "Port"
    port = option("--port", integer("PORT"))
    @footer "Server configuration parser"
end

# The parser is retrieved via `lift`:
parser = lift(Config)
# This parser can be composed normally or embedded in other parsers
cmd = command("mycmd", parser)
# Or used directly with the entrypoints:
optparse(Config, args)

```

the change hopefully makes it clearer that you’re creating a custom type, adds a dedicated small DSL to specify help prose and organize it more cleanly.

Additionally this introduces the `lift` function to “retrieve” the parser that will generate the custom type, instead of having to think of a random binding name to carry it around as before.

#### `str` no longer accepts empty strings

Before the `str` parser would accept empty strings by default. Now it doesn’t. but you can pass in `allow_empty=true` to opt into the old behaviour.

# v0.4.0 Features and improvements

Let’s start with the biggest one:

#### User-extensible parser and value-parser interfaces

Both `AbstractParser` and `AbstractValueParser` are now first-class extension points with a documented, minimal interface and some runtime validation utilities.

This means that the end user can define new ad hoc value parsers or new parser with a custom behaviour.

What this also means is that the package can now work with package extentions!

The library remains fully trimmable, but of course the extensions or custom parser/value prarser needs to be typestable.

Currently defining a new parser is rather involved (especially keeping type stability), but it can be done with a bit of effort. Value parsers are instead much easier to implement.

#### FastIdentifiers.jl extension

OptParse.jl now interfaces with the amazing FastIdentifiers.jl from @tecosaur and provides a new `identifier` value parser:

```julia-auto
using OptParse
using FastIdentifiers

@defid MyId ("i",
	            skip("-"),
	            :id(digits(6, pad=6)),
	            optional(".v", :version(digits(max=255)),
	                optional(".p", :participants(digits(max=2^16-1))))
	        )

const id = option("--ident", identifier(MyId))

```

#### New help layout and annotations

The automatic help generated has a new layout, and includes annotations to present choices, optionality and default values.

```julia-auto
A small server CLI demonstrating the new help annotations.

Usage: [OPTIONS] --format <FORMAT> <FILE>...

Arguments:
   <FILE>... required
       Input files

Options:
   -v, --verbose
       Run in verbose mode

   --host <HOST> default: localhost
       Listening host

   -p, --port <PORT> default: 8080
       Listening port

   -l, --level <LEVEL> default: info choices: debug, info, warn, error
       Log verbosity level

   --format <FORMAT> required choices: json, text, csv
       Output format

   -c, --config <FILE>
       Configuration file

Some footer, maybe for examples.

```

#### `partial` — pass-through parsing

Wrap any parser with `partial(p)` to consume only the arguments it recognizes  
and return the rest as a vector. Ideal for wrapper CLIs that forward unknown  
flags to child programs. Only `tryoptparse` is supported (no throwing variant) —  
use `ErrorTypes` to inspect the result.

```julia
using ErrorTypes: is_error, unwrap

wrapper = partial(record(; verbose = flag("-v"), config = optional(option("-c", str()))))
result = tryoptparse(wrapper, ["-v", "--child-flag", "file"])
# Ok(((verbose = true, config = nothing), ["--child-flag", "file"]))
value, remaining = unwrap(result)

```

#### `record` keyword syntax

`record` now also accepts keyword arguments directly, avoiding the inner tuple:

```julia
record(; name = option("-n", str()), port = option("-p", integer()))

```

The named-tuple form `record((name = ..., port = ...))` still works.

That’s it! Hope you’ll like it.

Feel free to open issues and ask questions!
