Skip to content

Commit 13066ab

Browse files
author
Ryan Miville
committed
add list support
1 parent 92d6ef5 commit 13066ab

File tree

6 files changed

+185
-51
lines changed

6 files changed

+185
-51
lines changed

README.md

+8-8
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77

88
Command line argument decoders for Gleam.
99

10-
Clad aims to make it as easy as possible to parse command line arguments in
11-
Gleam. The goal is to support simple to medium complexity command line
10+
Clad makes it easy and familiar to parse command line arguments in
11+
Gleam. The goal is to support simple-to-medium complexity command line
1212
interfaces while staying as minimal as possible. It is inspired by
1313
[minimist](https://github.com/minimistjs/minimist) and
1414
[gleam/json](https://hexdocs.pm/gleam_json/)
@@ -36,13 +36,13 @@ pub fn main() {
3636
use name <- zero.field("name", zero.string)
3737
use age <- zero.field("age", zero.int)
3838
use enrolled <- zero.field("enrolled", zero.bool)
39-
use classes <- clad.positional_arguments()
39+
use classes <- zero.field("class", zero.list(zero.string))
4040
zero.success(Student(name:, age:, enrolled:, classes:))
4141
}
4242
43-
// args: --name Lucy --age 8 --enrolled true math science art
43+
// args: --name Lucy --age 8 --enrolled true --class math --class art
4444
let result = clad.decode(argv.load().arguments, decoder)
45-
let assert Ok(Student("Lucy", 8, True, ["math", "science", "art"])) = result
45+
let assert Ok(Student("Lucy", 8, True, ["math", "art"])) = result
4646
}
4747
```
4848

@@ -62,13 +62,13 @@ pub fn main() {
6262
use name <- clad.opt("name", "n", zero.string)
6363
use age <- clad.opt("age", "a", zero.int)
6464
use enrolled <- clad.opt("enrolled", "e", clad.flag())
65-
use classes <- clad.positional_arguments()
65+
use classes <- clad.opt("class", "c", clad.list(zero.string))
6666
zero.success(Student(name:, age:, enrolled:, classes:))
6767
}
6868
69-
// args: --name=Lucy -ea8 math science art
69+
// args: --name=Lucy -ea8 -c math -c art
7070
let result = clad.decode(argv.load().arguments, decoder)
71-
let assert Ok(Student("Lucy", 8, True, ["math", "science", "art"])) = result
71+
let assert Ok(Student("Lucy", 8, True, ["math", "art"])) = result
7272
}
7373
```
7474

gleam.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name = "clad"
2-
version = "0.2.0"
2+
version = "0.2.1"
33

44
# Fill out these fields if you intend to generate HTML documentation or publish
55
# your project to the Hex package manager.

src/clad.gleam

+116-29
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
////
66
//// The following arguments:
77
//// ```sh
8-
//// -x=3 -y 4 -n5 -abc --hello world --beep=boop foo bar baz
8+
//// -x=3 -y 4 -n5 -abc --hello world --list one --list two --beep=boop foo bar baz
99
//// ```
1010
//// will be encoded as a `dynamic.Dynamic` in this shape:
1111
//// ```json
@@ -16,6 +16,7 @@
1616
//// "b": True,
1717
//// "c": True,
1818
//// "hello": "world",
19+
//// "list": ["one", "two"],
1920
//// "beep": "boop",
2021
//// "_": ["foo", "bar", "baz"]
2122
//// }
@@ -26,20 +27,44 @@
2627
//// Arguments can be decoded with a normal `zero.Decoder`
2728
////
2829
//// ```gleam
29-
//// // args: --name Lucy --age 8 --enrolled true
30+
//// // args: --name Lucy --age 8 --enrolled true --class math --class art
3031
////
3132
//// let decoder = {
3233
//// use name <- zero.field("name", zero.string)
3334
//// use age <- zero.field("age", zero.int)
3435
//// use enrolled <- zero.field("enrolled", zero.bool)
35-
//// zero.success(Student(name:, age:, enrolled:))
36+
//// use classes <- zero.field("class", zero.list(zero.string))
37+
//// zero.success(Student(name:, age:, enrolled:, classes:))
3638
//// }
3739
////
3840
//// let result = clad.decode(args, decoder)
39-
//// assert result == Ok(Student("Lucy", 8, True))
41+
//// assert result == Ok(Student("Lucy", 8, True, ["math", "art"]))
4042
//// ```
4143
//// Clad provides additional functions to support some common CLI behaviors.
4244
////
45+
//// ## Lists
46+
////
47+
//// Clad encodes the arguments without any information about the target record.
48+
//// Unlike other formats like JSON, CLI argument types can be ambiguous. For
49+
//// instance, if there's only one string provided for a `List(String)` argument,
50+
//// Clad will encode it as a String.
51+
////
52+
//// To handle this case, use the `list()` function.
53+
////
54+
//// ```gleam
55+
//// // args: --name Lucy --age 8 --enrolled true --class math
56+
////
57+
//// let decoder = {
58+
//// use name <- zero.field("name", zero.string)
59+
//// use age <- zero.field("age", zero.int)
60+
//// use enrolled <- zero.field("enrolled", zero.bool)
61+
//// use classes <- zero.field("class", clad.list(zero.string))
62+
//// zero.success(Student(name:, age:, enrolled:, classes:))
63+
//// }
64+
////
65+
//// let result = clad.decode(args, decoder)
66+
//// assert result == Ok(Student("Lucy", 8, True, ["math"]))
67+
//// ```
4368
//// ## Boolean Flags
4469
////
4570
//// CLI's commonly represent boolean flags just by the precense or absence of the
@@ -49,21 +74,22 @@
4974
//// Clad provides the `flag()` decoder to handle this case.
5075
////
5176
//// ```gleam
52-
//// // args1: --name Lucy --age 8 --enrolled
53-
//// // args2: --name Bob --age 3
77+
//// // args1: --name Lucy --age 8 --class math --class art --enrolled
78+
//// // args2: --name Bob --age 3 --class math
5479
////
5580
//// let decoder = {
5681
//// use name <- zero.field("name", zero.string)
5782
//// use age <- zero.field("age", zero.int)
5883
//// use enrolled <- zero.field("enrolled", clad.flag())
59-
//// zero.success(Student(name:, age:, enrolled:))
84+
//// use classes <- zero.field("class", clad.list(zero.string))
85+
//// zero.success(Student(name:, age:, enrolled:, classes:))
6086
//// }
6187
////
6288
//// let result = clad.decode(args1, decoder)
63-
//// assert result == Ok(Student("Lucy", 8, True))
89+
//// assert result == Ok(Student("Lucy", 8, True, ["math", "art"]))
6490
////
6591
//// let result = clad.decode(args2, decoder)
66-
//// assert result == Ok(Student("Bob", 3, False))
92+
//// assert result == Ok(Student("Bob", 3, False, ["math"]))
6793
//// ```
6894
////
6995
//// ## Alternate Names
@@ -74,46 +100,55 @@
74100
//// Clad provides the `opt()` function for this.
75101
////
76102
//// ```gleam
77-
//// // args1: -n Lucy -a 8 -e
78-
//// // args2: --name Bob --age 3
103+
//// // args1: -n Lucy -a 8 -e -c math -c art
104+
//// // args2: --name Bob --age 3 --class math
79105
////
80106
//// let decoder = {
81107
//// use name <- clad.opt(long_name: "name", short_name: "n", zero.string)
82108
//// use age <- clad.opt(long_name: "age", short_name: "a", zero.int)
83109
//// use enrolled <- clad.opt(long_name: "enrolled", short_name: "e" clad.flag())
84-
//// zero.success(Student(name:, age:, enrolled:))
110+
//// use classes <- clad.opt(long_name: "class", short_name: "c", clad.list(zero.string))
111+
//// zero.success(Student(name:, age:, enrolled:, classes:))
85112
//// }
86113
////
87114
//// let result = clad.decode(args1, decoder)
88-
//// assert result == Ok(Student("Lucy", 8, True))
115+
//// assert result == Ok(Student("Lucy", 8, True, ["math", "art"]))
89116
////
90117
//// let result = clad.decode(args2, decoder)
91-
//// assert result == Ok(Student("Bob", 3, False))
118+
//// assert result == Ok(Student("Bob", 3, False, ["math"]))
92119
//// ```
93120
////
94121
//// ## Positional Arguments
95122
////
96123
//// A CLI may also support positional arguments. These are any arguments that are
97124
//// not attributed to a named option. Clad provides the `positional_arguments()` decoder to
98-
//// retrieve these values.
125+
//// retrieve these values. All arguments followed by a `--` will be added to the positional arguemnts.
99126
////
100127
//// ```gleam
101-
//// // args1: -n Lucy -ea8 math science art
102-
//// // args2: --name Bob --age 3
128+
//// // args1: -n Lucy -ea8 -c math -c art -- Lucy is a star student!
129+
//// // args2: --name Bob who is --age 3 --class math Bob -- -idk
103130
////
104131
//// let decoder = {
105132
//// use name <- clad.opt("name", "n", zero.string)
106133
//// use age <- clad.opt("age", "a", zero.int)
107134
//// use enrolled <- clad.opt("enrolled", "e" clad.flag())
108-
//// use classes <- clad.positional_arguments()
109-
//// zero.success(Student(name:, age:, enrolled:, classes:))
135+
//// use classes <- clad.opt(long_name: "class", short_name: "c", clad.list(zero.string))
136+
//// use notes <- clad.positional_arguments()
137+
//// let notes = string.join(notes, " ")
138+
//// zero.success(Student(name:, age:, enrolled:, classes:, notes:))
110139
//// }
111140
////
112141
//// let result = clad.decode(args1, decoder)
113-
//// assert result == Ok(Student("Lucy", 8, True, ["math", "science", "art"]))
142+
//// let assert Ok(Student(
143+
//// "Lucy",
144+
//// 8,
145+
//// True,
146+
//// ["math", "art"],
147+
//// "Lucy is a star student!",
148+
//// )) = result
114149
////
115150
//// let result = clad.decode(args2, decoder)
116-
//// assert result == Ok(Student("Bob", 3, False, []))
151+
//// assert result == Ok(Student("Bob", 3, False, ["math"], "who is Bob -idk"))
117152
//// ```
118153

119154
import decode/zero.{type Decoder}
@@ -131,7 +166,11 @@ import gleam/string
131166
const positional_arg_name = "_"
132167

133168
type State {
134-
State(opts: Dict(String, Dynamic), positional: List(String))
169+
State(
170+
opts: Dict(String, Dynamic),
171+
list_opts: Dict(String, List(Dynamic)),
172+
positional: List(String),
173+
)
135174
}
136175

137176
/// Run a decoder on a list of command line arguments, decoding the value if it
@@ -162,6 +201,8 @@ pub fn decode(
162201
}
163202

164203
/// Get all of the unnamed, positional arguments
204+
///
205+
/// Clad encodes all arguments following a `--` as positional arguments.
165206
/// ```gleam
166207
/// let decoder = {
167208
/// use positional <- clad.positional_arguments
@@ -172,6 +213,9 @@ pub fn decode(
172213
///
173214
/// let result = clad.decode(["-a1", "-b", "2"], decoder)
174215
/// assert result == Ok([])
216+
///
217+
/// let result = clad.decode(["-a1", "--", "-b", "2"], decoder)
218+
/// assert result == Ok(["-b", "2"])
175219
/// ```
176220
pub fn positional_arguments(
177221
next: fn(List(String)) -> Decoder(final),
@@ -189,6 +233,9 @@ pub fn positional_arguments(
189233
/// let result = clad.decode(["-v"], decoder)
190234
/// assert result == Ok(True)
191235
///
236+
/// let result = clad.decode(["-v", "false"], decoder)
237+
/// assert result == Ok(False)
238+
///
192239
/// let result = clad.decode([], decoder)
193240
/// assert result == Ok(False)
194241
/// ```
@@ -228,10 +275,12 @@ fn optional_field(
228275
/// use name <- clad.opt("name", "n", zero.string)
229276
/// zero.success(name)
230277
/// }
231-
/// clad.decode(["--name", "Lucy"], decoder)
232-
/// // -> Ok("Lucy")
233-
/// clad.decode(["-n", "Lucy"], decoder)
234-
/// // -> Ok("Lucy")
278+
///
279+
/// let result = clad.decode(["--name", "Lucy"], decoder)
280+
/// assert result == Ok("Lucy")
281+
///
282+
/// let result = clad.decode(["-n", "Lucy"], decoder)
283+
/// assert result == Ok("Lucy")
235284
/// ```
236285
pub fn opt(
237286
long_name: String,
@@ -246,15 +295,37 @@ pub fn opt(
246295
}
247296
}
248297

298+
/// A `List` decoder that will wrap a single item in a list.
299+
/// Clad has no knowledge of the target record, so single item lists will be
300+
/// encoded as the inner type rather than a list.
301+
/// ```gleam
302+
/// let decoder = {
303+
/// use classes <- zero.field("class", clad.list(zero.string))
304+
/// zero.success(classes)
305+
/// }
306+
/// let result = clad.decode(["--class", "art"], decoder)
307+
/// assert result == Ok(["art"])
308+
/// ```
309+
pub fn list(of inner: Decoder(a)) -> Decoder(List(a)) {
310+
let single = inner |> zero.map(list.wrap)
311+
zero.one_of(zero.list(inner), [single])
312+
}
313+
249314
fn parse(args: List(String)) -> State {
250-
let state = State(dict.new(), list.new())
315+
let state = State(dict.new(), dict.new(), list.new())
251316

252317
let state = parse_args(args, state)
253318
State(..state, positional: list.reverse(state.positional))
254319
}
255320

256321
fn to_dynamic(state: State) -> Dynamic {
322+
let list_opts =
323+
dict.map_values(state.list_opts, fn(_, values) {
324+
list.reverse(values) |> dynamic.from
325+
})
326+
257327
state.opts
328+
|> dict.merge(list_opts)
258329
|> dict.insert(positional_arg_name, dynamic.from(state.positional))
259330
|> dynamic.from
260331
}
@@ -354,8 +425,24 @@ fn parse_arg(
354425
}
355426

356427
fn set_arg(state: State, key: String, value: String) -> State {
357-
let opts = dict.insert(state.opts, key, parse_value(value))
358-
State(..state, opts:)
428+
let in_opt = dict.get(state.opts, key)
429+
let in_list = dict.get(state.list_opts, key)
430+
case in_opt, in_list {
431+
Error(_), Error(_) -> {
432+
let opts = dict.insert(state.opts, key, parse_value(value))
433+
State(..state, opts:)
434+
}
435+
Ok(v), _ -> {
436+
let opts = dict.delete(state.opts, key)
437+
let list_opts = dict.insert(state.list_opts, key, [parse_value(value), v])
438+
State(..state, opts:, list_opts:)
439+
}
440+
_, Ok(values) -> {
441+
let list_opts =
442+
dict.insert(state.list_opts, key, [parse_value(value), ..values])
443+
State(..state, list_opts:)
444+
}
445+
}
359446
}
360447

361448
fn parse_short(arg: String, state: State) -> #(State, Option(String)) {

test/clad_test.gleam

+20-1
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,25 @@ pub fn positional_arguments_test() {
187187
zero.success(#(a, b, c))
188188
}
189189

190-
clad.decode(["-ab5", "foo", "bar", "baz"], decoder)
190+
clad.decode(["-ab5", "foo", "--hello", "world", "bar", "baz"], decoder)
191191
|> should.equal(Ok(#(True, 5, ["foo", "bar", "baz"])))
192+
193+
clad.decode(["-ab5", "foo", "--", "--hello", "world", "bar", "baz"], decoder)
194+
|> should.equal(Ok(#(True, 5, ["foo", "--hello", "world", "bar", "baz"])))
195+
}
196+
197+
pub fn list_test() {
198+
let decoder = {
199+
use list <- zero.field("a", clad.list(zero.int))
200+
zero.success(list)
201+
}
202+
203+
clad.decode(["-a", "1", "-a", "2", "-a", "3"], decoder)
204+
|> should.equal(Ok([1, 2, 3]))
205+
206+
clad.decode(["-a", "1"], decoder)
207+
|> should.equal(Ok([1]))
208+
209+
clad.decode([], decoder)
210+
|> should.be_error
192211
}

0 commit comments

Comments
 (0)