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"])