clad

This module encodes a list of command line arguments as a dynamic.Dynamic and provides primitives to build a dynamic.Decoder to decode records from command line arguments.

Arguments are parsed from long names (--name) or short names (-n). Values are decoded in the form --name value or --name=value. Boolean flags do not need an explicit value. If the flag exists it is True, and False if it is missing. (i.e. --verbose)

Examples

Encoding

Clad encodes the arguments without any knowledge of your target record. It cannot know if a field is intended to be a single, basic type or a list with a single item. Therefore it encodes everything as a list.

All of the following get encoded the same:

--name Lucy --count 3 --verbose
--name Lucy --count 3 --verbose true
--name=Lucy --count=3 --verbose=true
// {"--name": ["Lucy"], "--count": [3], "--verbose": [true]}

Since the target record is unknown, missing Bool arguments are not encoded at all:

--name Lucy --count 3
// {"--name": ["Lucy"], "--count": [3]}

There is no way to know that a long name and a short name are the same argument when encoding. So they are encoded as separate fields:

--name Lucy -n Joe
// {"--name": ["Lucy"], "-n": ["Joe"]}

Decoding Fields

Clad provides the arg function to handle these quirks of the Dynamic representation.

--name Lucy
use name <- clad.arg(long_name: "name", short_name: "n", of: dynamic.string)
// -> "Lucy"
-n Lucy
use name <- clad.arg(long_name: "name", short_name: "n", of: dynamic.string)
// -> "Lucy"
-n Lucy -n Joe
use names <- clad.arg("name", "n", of: dynamic.list(dynamic.string))
// -> ["Lucy", "Joe"]

Clad’s toggle decoder only requires the name. Missing arguments are False:

--verbose
use verbose <- clad.toggle(long_name: "verbose", short_name: "v")
// -> True
--name Lucy
use verbose <- clad.toggle(long_name: "verbose", short_name: "v")
// -> False

It’s common for CLI’s to have default values for arguments. This can be accomplished with a dynamic.optional, but the arg_with_default function is provided for convenience:

--name Lucy
use count <- clad.arg_with_default(
  long_name: "count",
  short_name: "c",
  of: dynamic.int,
  default: 1,
)
// -> 1

Decoding Records

Clad’s API is heavily inspired by (read: copied from) toy.

fn arg_decoder() {
  use name <- clad.arg("name", "n", dynamic.string)
  use count <- clad.arg_with_default("count", "c", dynamic.int, 1)
  use verbose <- clad.toggle("verbose", "v")
  clad.decoded(Args(name:, count:, verbose:))
}

And then use it to decode the arguments:

// arguments: ["--name", "Lucy", "--count", "3", "--verbose"]

let args =
  arg_decoder()
  |> clad.decode(arguments)
let assert Ok(Args("Lucy", 3, True)) = args

Here are a few examples of arguments that would decode the same:

--name Lucy --count 3 --verbose
--name=Lucy -c 3 -v=true
-n=Lucy -c=3 -v

Errors

Clad returns the first error it encounters. If multiple fields have errors, only the first one will be returned.

// arguments: ["--count", "three"]

let args =
  arg_decoder()
  |> clad.decode(arguments)
let assert Error([DecodeError("field", "nothing", ["--name"])]) = args

If a field has a default value, but the argument is supplied with the incorrect type, an error will be returned rather than falling back on the default value.

// arguments: ["-n", "Lucy" "-c", "three"]

let args =
  arg_decoder()
  |> clad.decode(arguments)
let assert Error([DecodeError("Int", "String", ["-c"])]) = args

Functions

pub fn arg(
  long_name long_name: String,
  short_name short_name: String,
  of decoder: fn(Dynamic) -> Result(a, List(DecodeError)),
  then next: fn(a) -> fn(Dynamic) -> Result(b, List(DecodeError)),
) -> fn(Dynamic) -> Result(b, List(DecodeError))

Decode an argument by either its long name (--name) or short name (-n).

List arguments are represented by repeated values.

Examples

// data: ["--name", "Lucy"]
use name <- clad.arg(long_name: "name", short_name: "n", of: dynamic.string)
// -> "Lucy"
// data: ["-n", "Lucy"]
use name <- clad.arg(long_name: "name", short_name: "n", of: dynamic.string)
// -> "Lucy"
// data: ["-n", "Lucy", "-n", "Joe"]
use name <- clad.arg(
  long_name: "name",
  short_name: "n",
  of: dynamic.list(dynamic.string)
)
// -> ["Lucy", "Joe"]
pub fn arg_with_default(
  long_name long_name: String,
  short_name short_name: String,
  of decoder: fn(Dynamic) -> Result(a, List(DecodeError)),
  default default: a,
  then next: fn(a) -> fn(Dynamic) -> Result(b, List(DecodeError)),
) -> fn(Dynamic) -> Result(b, List(DecodeError))

Decode an argument, returning a default value if the argument does not exist

Examples

// data: ["--name", "Lucy"]
use name <- clad.arg(
  long_name: "name",
  short_name: "n",
  of: dynamic.string,
  default: "Joe"
)
// -> "Lucy"
// data: []
use name <- clad.arg(
  long_name: "name",
  short_name: "n",
  of: dynamic.string,
  default: "Joe"
)
// -> "Joe"
pub fn decode(
  decoder: fn(Dynamic) -> Result(a, List(DecodeError)),
  arguments: List(String),
) -> Result(a, List(DecodeError))

Run a decoder on a list of command line arguments, decoding the value if it is of the desired type, or returning errors.

This function pairs well with the argv package.

Examples

{
  use name <- clad.arg("name", "n", dynamic.string)
  use email <- clad.arg("email", "e", dynamic.string),
  clad.decoded(SignUp(name:, email:))
}
|> clad.decode(["-n", "Lucy", "--email=lucy@example.com"])
// -> Ok(SignUp(name: "Lucy", email: "lucy@example.com"))

with argv:

{
  use name <- clad.arg("name", "n", dynamic.string)
  use email <- clad.arg("email", "e", dynamic.string),
  clad.decoded(SignUp(name:, email:))
}
|> clad.decode(argv.load().arguments)
pub fn decoded(
  value: a,
) -> fn(Dynamic) -> Result(a, List(DecodeError))

Creates a decoder which directly returns the provided value. Used to collect decoded values into a record.

Examples

pub fn user_decoder() {
  use name <- clad.string("name", "n")
  clad.decoded(User(name:))
}
pub fn long_name(
  long_name: String,
  decoder: fn(Dynamic) -> Result(a, List(DecodeError)),
  next: fn(a) -> fn(Dynamic) -> Result(b, List(DecodeError)),
) -> fn(Dynamic) -> Result(b, List(DecodeError))

Decode an argument only by a long name

Examples

// data: ["--name", "Lucy"]
use name <- clad.long_name("name", dynamic.string)
// -> "Lucy"
pub fn short_name(
  short_name: String,
  decoder: fn(Dynamic) -> Result(a, List(DecodeError)),
  next: fn(a) -> fn(Dynamic) -> Result(b, List(DecodeError)),
) -> fn(Dynamic) -> Result(b, List(DecodeError))

Decode an argument only by a short name

Examples

// data: ["-n", "Lucy"]
use name <- clad.short_name("n", dynamic.string)
// -> "Lucy"
pub fn toggle(
  long_name long_name: String,
  short_name short_name: String,
  then next: fn(Bool) ->
    fn(Dynamic) -> Result(a, List(DecodeError)),
) -> fn(Dynamic) -> Result(a, List(DecodeError))

A decoder that decodes Bool arguments.

Toggles do not need an explicit value. If the flag exists it is True, and False if it is missing. (i.e. --verbose)

Examples

// data: ["-v"]
use verbose <- clad.toggle(long_name: "verbose", short_name: "v")
// -> True
// data: []
use verbose <- clad.toggle(long_name: "verbose", short_name: "v")
// -> False
Search Document