clad

This module encodes a list of command line arguments as a dynamic.Dynamic and provides functions to decode those arguments using a decode/zero.Decoder.

Encoding

The following arguments:

-x=3 -y 4 -n5 -abc --hello world --list one --list two --beep=boop foo bar baz

will be encoded as a dynamic.Dynamic in this shape:

{
  "name": 3,
  "y": 4,
  "a": True,
  "b": True,
  "c": True,
  "hello": "world",
  "list": ["one", "two"],
  "beep": "boop",
  "_": ["foo", "bar", "baz"]
}

Decoding

Arguments can be decoded with a normal zero.Decoder

// args: --name Lucy --age 8 --enrolled true --class math --class art

let decoder = {
  use name <- zero.field("name", zero.string)
  use age <- zero.field("age", zero.int)
  use enrolled <- zero.field("enrolled", zero.bool)
  use classes <- zero.field("class", zero.list(zero.string))
  zero.success(Student(name:, age:, enrolled:, classes:))
}

let result = clad.decode(args, decoder)
assert result == Ok(Student("Lucy", 8, True, ["math", "art"]))

Clad provides additional functions to support some common CLI behaviors.

Lists

Clad encodes the arguments without any information about the target record. Unlike other formats like JSON, CLI argument types can be ambiguous. For instance, if there’s only one string provided for a List(String) argument, Clad will encode it as a String.

To handle this case, use the list() function.

// args: --name Lucy --age 8 --enrolled true --class math

let decoder = {
  use name <- zero.field("name", zero.string)
  use age <- zero.field("age", zero.int)
  use enrolled <- zero.field("enrolled", zero.bool)
  use classes <- zero.field("class", clad.list(zero.string))
  zero.success(Student(name:, age:, enrolled:, classes:))
}

let result = clad.decode(args, decoder)
assert result == Ok(Student("Lucy", 8, True, ["math"]))

Boolean Flags

CLI’s commonly represent boolean flags just by the precense or absence of the option. Since Clad has no knowledge of your target record, it cannot encode missing flags as False.

Clad provides the flag() decoder to handle this case.

// args1: --name Lucy --age 8 --class math --class art --enrolled
// args2: --name Bob --age 3 --class math

let decoder = {
  use name <- zero.field("name", zero.string)
  use age <- zero.field("age", zero.int)
  use enrolled <- zero.field("enrolled", clad.flag())
  use classes <- zero.field("class", clad.list(zero.string))
  zero.success(Student(name:, age:, enrolled:, classes:))
}

let result = clad.decode(args1, decoder)
assert result == Ok(Student("Lucy", 8, True, ["math", "art"]))

let result = clad.decode(args2, decoder)
assert result == Ok(Student("Bob", 3, False, ["math"]))

Alternate Names

It is also common for CLI’s to support long names and short names for options (e.g. --name and -n).

Clad provides the opt() function for this.

// args1: -n Lucy -a 8 -e -c math -c art
// args2: --name Bob --age 3 --class math

let decoder = {
  use name <- clad.opt(long_name: "name", short_name: "n", zero.string)
  use age <- clad.opt(long_name: "age", short_name: "a", zero.int)
  use enrolled <- clad.opt(long_name: "enrolled", short_name: "e" clad.flag())
  use classes <- clad.opt(long_name: "class", short_name: "c", clad.list(zero.string))
  zero.success(Student(name:, age:, enrolled:, classes:))
}

let result = clad.decode(args1, decoder)
assert result == Ok(Student("Lucy", 8, True, ["math", "art"]))

let result = clad.decode(args2, decoder)
assert result == Ok(Student("Bob", 3, False, ["math"]))

Positional Arguments

A CLI may also support positional arguments. These are any arguments that are not attributed to a named option. Clad provides the positional_arguments() decoder to retrieve these values. All arguments followed by a -- will be added to the positional arguemnts.

// args1: -n Lucy -ea8 -c math -c art -- Lucy is a star student!
// args2: --name Bob who is --age 3 --class math Bob -- -idk

let decoder = {
  use name <- clad.opt("name", "n", zero.string)
  use age <- clad.opt("age", "a", zero.int)
  use enrolled <- clad.opt("enrolled", "e" clad.flag())
  use classes <- clad.opt(long_name: "class", short_name: "c", clad.list(zero.string))
  use notes <- clad.positional_arguments()
  let notes = string.join(notes, " ")
  zero.success(Student(name:, age:, enrolled:, classes:, notes:))
}

let result = clad.decode(args1, decoder)
let assert Ok(Student(
  "Lucy",
  8,
  True,
  ["math", "art"],
  "Lucy is a star student!",
)) = result

let result = clad.decode(args2, decoder)
assert result == Ok(Student("Bob", 3, False, ["math"], "who is Bob -idk"))

Functions

pub fn decode(
  args: List(String),
  decoder: Decoder(a),
) -> 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

// args: --name Lucy --email=lucy@example.com

let decoder = {
  use name <- zero.field("name", dynamic.string)
  use email <- zero.field("email", dynamic.string),
  clad.decoded(SignUp(name:, email:))
}

let result = clad.decode(argv.load().arguments, decoder)
assert result == Ok(SignUp(name: "Lucy", email: "lucy@example.com"))
pub fn flag(
  long_name: String,
  short_name: String,
  next: fn(Bool) -> Decoder(a),
) -> Decoder(a)

Decode a command line flag as a Bool. Returns False if value is not present

let decoder = {
  use verbose <- clad.flag("verbose", "v", clad.flag())
  zero.success(verbose)
}
let result = clad.decode(["-v"], decoder)
assert result == Ok(True)

let result = clad.decode(["-v", "false"], decoder)
assert result == Ok(False)

let result = clad.decode([], decoder)
assert result == Ok(False)
pub fn list(of inner: Decoder(a)) -> Decoder(List(a))

A List decoder that will wrap a single item in a list. Clad has no knowledge of the target record, so single item lists will be encoded as the inner type rather than a list.

let decoder = {
  use classes <- zero.field("class", clad.list(zero.string))
  zero.success(classes)
}
let result = clad.decode(["--class", "art"], decoder)
assert result == Ok(["art"])
pub fn opt(
  long_name: String,
  short_name: String,
  field_decoder: Decoder(a),
  next: fn(a) -> Decoder(b),
) -> Decoder(b)

Decode a command line option by either a long name or short name

let decoder = {
  use name <- clad.opt("name", "n", zero.string)
  zero.success(name)
}

let result = clad.decode(["--name", "Lucy"], decoder)
assert result == Ok("Lucy")

let result = clad.decode(["-n", "Lucy"], decoder)
assert result == Ok("Lucy")
pub fn positional_arguments(
  next: fn(List(String)) -> Decoder(a),
) -> Decoder(a)

Get all of the unnamed, positional arguments

Clad encodes all arguments following a -- as positional arguments.

let decoder = {
  use positional <- clad.positional_arguments
  zero.success(positional)
}
let result = clad.decode(["-a1", "hello", "-b", "2", "world"], decoder)
assert result == Ok(["hello", "world"])

let result = clad.decode(["-a1", "-b", "2"], decoder)
assert result == Ok([])

let result = clad.decode(["-a1", "--", "-b", "2"], decoder)
assert result == Ok(["-b", "2"])
Search Document