Build a command-line options parser to validate and map a CLI command into a structured Elm type.
This is an alternative to Cli.Option that is designed to
generate a JSON schema describing the valid ways to invoke the CLI command, but with more precise type information.
Cli.Option still generates a JSON schema, but Cli.Option.Typed.customDecoder lets you pass in an
elm-ts-json Decoder
with arbitrary and fully typed JSON values, and the primitive Options
like int carry more precise type information instead of just String
in the JSON Schema output.
The vast majority of users will use elm-cli-options-parser through elm-pages
when they build elm-pages scripts.
When you define an elm-pages script using Script.withSchema,
you pass in an elm-ts-json Encoder
and return a matching Elm type that the script will output as JSON,
and elm-pages introspect will automatically show all of the type information for your
CLI options and the output JSON as part of the introspection output.
Cli.Option.TypedAll CLIs built with elm-cli-options-parser can be invoked either with traditional CLI arguments, or
with a single JSON string CLI argument, allowing for easier consumption by
LLM agents and programmatic invocation of your CLI. That, along with
the precise type information in the JSON Schema describing your CLI options, makes Cli.Option.Typed
a good choice for CLIs when they may be invoked programmatically or by LLM agents.
You can use Cli.Option for a slightly simpler API that
treats all values as strings if automated tool access isn't a priority for your CLI.
Both modules produce the same Option type and work with the same
OptionsParser.with pipeline, so they can be interwoven freely.
Here is the terminology used for building up Command-Line parsers with this library.

See the examples
folder for end-to-end examples (including TypedGreet.elm which uses this module).
import Cli.Option.Typed as Option
import Cli.OptionsParser as OptionsParser exposing (with)
import Cli.Program as Program
type alias Options =
{ name : String
, count : Int
, verbose : Bool
}
programConfig : Program.Config Options
programConfig =
Program.config
|> Program.add
(OptionsParser.build Options
|> with
(Option.requiredKeywordArg
"name"
Option.string
)
|> with
(Option.requiredKeywordArg
"count"
Option.int
)
|> with
(Option.flag
"verbose"
)
)This parser handles both CLI and JSON input:
mytool --name hello --count 3 --verbose{ "name": "hello", "count": 3, "verbose": true, "$cli": {} }And Program.toJsonSchema "mytool" programConfig generates a JSON Schema with
proper types ("type": "string", "type": "integer", etc.).
A decoder that knows how to parse values from both CLI args and JSON input.
Use string, int, float for primitives, or customDecoder for
custom types (objects, arrays, etc.) where the CLI input is a JSON string.
A string value. In CLI mode, the raw string is passed through as-is. In JSON mode, a JSON string field is decoded.
Option.requiredKeywordArg "name" Option.string
-- CLI: --name hello → "hello"
-- JSON: {"name": "hello"} → "hello"An integer value. In CLI mode, the string is parsed as a JSON integer. In JSON mode, a JSON integer field is decoded.
Option.requiredKeywordArg "count" Option.int
-- CLI: --count 42 → 42
-- JSON: {"count": 42} → 42A float value. In CLI mode, the string is parsed as a JSON number. In JSON mode, a JSON number field is decoded.
Option.requiredKeywordArg "rate" Option.float
-- CLI: --rate 3.14 → 3.14
-- JSON: {"rate": 3.14} → 3.14Create a CliDecoder from a TsDecode.Decoder.
In CLI mode, the string value is parsed as a JSON value. This means the CLI user must pass valid JSON.
For strings, this means the CLI value must be quoted: --name '"hello"'.
If you want bare string values, use string instead.
customDecoder is especially useful for decoding complex structured values like JSON objects or arrays.
import TsJson.Decode as TsDecode
pointDecoder : TsDecode.Decoder { x : Int, y : Int }
pointDecoder =
TsDecode.succeed (\x y -> { x = x, y = y })
|> TsDecode.andMap (TsDecode.field "x" TsDecode.int)
|> TsDecode.andMap (TsDecode.field "y" TsDecode.int)
pointOption : Option String { x : Int, y : Int } { position : BeginningOption, canAddMissingMessage : () }
pointOption =
Option.requiredKeywordArg "point" (Option.customDecoder pointDecoder)
-- CLI: --point '{"x":1,"y":2}'
-- JSON: {"point": {"x": 1, "y": 2}}A positional argument that must be provided.
Example: src/Main.elm in elm make src/Main.elm
Option.requiredPositionalArg "port" Option.int
-- CLI: mytool 8080 → 8080
-- JSON: {"port": 8080} → 8080A keyword argument that must be provided.
Example: --name my-app or --name=my-app
Option.requiredKeywordArg "count" Option.int
-- CLI: --count 42 → 42
-- JSON: {"count": 42} → 42
-- Schema: {"type": "integer"}A keyword argument that may be omitted.
Example: --output main.js or --output=main.js
Option.optionalKeywordArg "greeting" Option.string
-- CLI: --greeting hi → Just "hi", omitted → Nothing
-- JSON: {"greeting": "hi"} → Just "hi", absent → NothingA keyword argument that can be provided multiple times.
Example: --header "Auth: token" --header "Accept: json"
Option.keywordArgList "header" Option.string
-- CLI: --header "X-A: 1" --header "X-B: 2" → ["X-A: 1", "X-B: 2"]A flag with no argument. Always Bool — no decoder needed.
Example: --debug in elm make --debug
Option.flag "verbose"
-- CLI: --verbose → True, omitted → False
-- JSON: {"verbose": true} → True, absent → FalseThese must be added with their corresponding OptionsParser.with... function,
not the regular OptionsParser.with. See the Cli.OptionsParser.BuilderState
docs for why.
An optional positional argument.
Must be used with OptionsParser.withOptionalPositionalArg
(not OptionsParser.with).
Example: <revision> in git log [<revision>]
Parses to: Just "abc123" (or Nothing if omitted)
Option.optionalPositionalArg "revision" Option.stringCollect all remaining positional arguments as a list.
Must be used with OptionsParser.withRestArgs
(not OptionsParser.with), and must be the last option in the pipeline.
Example: <files>... in elm-test [<files>...]
Parses to: ["tests/First.elm", "tests/Second.elm"] (or [] if none provided)
Option.restArgs "files"
-- CLI: mytool a.txt b.txt → ["a.txt", "b.txt"]Mutually exclusive option values. Restricts the option to a fixed set of string values, each mapped to an Elm value.
type ReportFormat
= Json
| Junit
| Console
reportOption : Option String ReportFormat { position : BeginningOption, canAddMissingMessage : () }
reportOption =
Option.requiredKeywordArg "report" Option.string
|> Option.oneOf
[ ( "json", Json )
, ( "junit", Junit )
, ( "console", Console )
]The help text will show the allowed values:
$ ./elm-test --help
elm-test [--report <json|junit|console>] <TESTFILES>...And if you run it with an unrecognized value, you get a validation error:
$ ./elm-test --report xml
Validation errors:
`report` failed a validation. Must be one of [json, junit, console]
Value was:
"xml"The JSON schema will include an enum constraint with the allowed values.
Validations allow you to guarantee that if you receive the data in Elm, it meets a set of preconditions. If it doesn't, the user will see an error message describing the validation error, which option it came from, and the value the option had.
Note that failing a validation will not cause the next OptionsParser in
your Cli.Program.Config to be run. Instead,
if the OptionsParser is a match except for validation errors, you will get an
error message regardless.
Example:
capitalizedNameRegex : String
capitalizedNameRegex =
"[A-Z][A-Za-z]*"
validateParser : OptionsParser.OptionsParser ( String, Maybe Int ) BuilderState.NoMoreOptions
validateParser =
OptionsParser.build (\a b -> ( a, b ))
|> OptionsParser.with
(Option.requiredKeywordArg "name" Option.string
|> Option.validate (Cli.Validate.regex capitalizedNameRegex)
)
|> OptionsParser.with
(Option.optionalKeywordArg "age" Option.int)
{-
$ ./validation --name Mozart --age 262
Mozart is 262 years old
$ ./validation --name mozart
Validation errors:
`name` failed a validation. Must be of form /[A-Z][A-Za-z]*/
Value was:
"mozart"
-}See Cli.Validate for some validation helpers that can be used
in conjunction with the following functions.
Run a validation on the parsed value. If validation fails, the user sees the error message.
Option.requiredKeywordArg "name" Option.string
|> Option.validate
(Cli.Validate.regex "^[A-Z][A-Za-z]*")Like validate, but only runs when the value is Just.
Does nothing for Nothing.
Transform the option value, or fail with a validation error.
If the function returns Err, the error message is shown to the user.
Option.requiredKeywordArg "count" Option.string
|> Option.validateMap String.toIntLike validateMap, but only runs when the value is Just.
Does nothing for Nothing.
Option.optionalKeywordArg "count" Option.string
|> Option.validateMapIfPresent String.toIntTransform an option's value. Use this for infallible transformations.
For transformations that can fail, use validateMap instead
so the user gets a helpful error message.
Option.requiredKeywordArg "name" Option.string
|> Option.map String.toUpper
Option.requiredKeywordArg "output" Option.string
|> Option.map (\path -> path ++ "/index.html")Transform a flag's Bool into a custom type.
type Verbosity
= Quiet
| Verbose
verbosityOption : Option Bool Verbosity { position : BeginningOption }
verbosityOption =
Option.flag "verbose"
|> Option.mapFlag { present = Verbose, absent = Quiet }Provide a default value for an optional option. Turns a Maybe value
into a plain value.
Option.optionalKeywordArg "greeting" Option.string
|> Option.withDefault "Hello"Phantom type marker for options that can be used with
OptionsParser.with. Most option constructors
produce this type.
Phantom type marker for optional positional args. Must be used with
OptionsParser.withOptionalPositionalArg.
Phantom type marker for rest args. Must be used with
OptionsParser.withRestArgs.
Add a description shown in help text and JSON schema.
Option.requiredKeywordArg "name" Option.string
|> Option.withDescription "Your name for the greeting"Set a custom display name (metavar) for a keyword argument's value placeholder in help text and usage synopsis.
By default, the keyword arg name is uppercased (e.g., --output-dir <OUTPUT_DIR>).
Use this to provide a more descriptive placeholder.
Option.requiredKeywordArg "output-dir" Option.string
|> Option.withDisplayName "PATH"
-- Shows as: --output-dir <PATH>Add a custom error message for when a required option is missing.
This only works on required options (requiredPositionalArg, requiredKeywordArg).
Option.requiredKeywordArg "repository" Option.string
|> Option.withMissingMessage "You must specify a repository to clone."
The type for an option in the pipeline. Use with
OptionsParser.with.