5
5
////
6
6
//// The following arguments:
7
7
//// ```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
9
9
//// ```
10
10
//// will be encoded as a `dynamic.Dynamic` in this shape:
11
11
//// ```json
16
16
//// "b": True,
17
17
//// "c": True,
18
18
//// "hello": "world",
19
+ //// "list": ["one", "two"],
19
20
//// "beep": "boop",
20
21
//// "_": ["foo", "bar", "baz"]
21
22
//// }
26
27
//// Arguments can be decoded with a normal `zero.Decoder`
27
28
////
28
29
//// ```gleam
29
- //// // args: --name Lucy --age 8 --enrolled true
30
+ //// // args: --name Lucy --age 8 --enrolled true --class math --class art
30
31
////
31
32
//// let decoder = {
32
33
//// use name <- zero.field("name", zero.string)
33
34
//// use age <- zero.field("age", zero.int)
34
35
//// 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:))
36
38
//// }
37
39
////
38
40
//// let result = clad.decode(args, decoder)
39
- //// assert result == Ok(Student("Lucy", 8, True))
41
+ //// assert result == Ok(Student("Lucy", 8, True, ["math", "art"] ))
40
42
//// ```
41
43
//// Clad provides additional functions to support some common CLI behaviors.
42
44
////
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
+ //// ```
43
68
//// ## Boolean Flags
44
69
////
45
70
//// CLI's commonly represent boolean flags just by the precense or absence of the
49
74
//// Clad provides the `flag()` decoder to handle this case.
50
75
////
51
76
//// ```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
54
79
////
55
80
//// let decoder = {
56
81
//// use name <- zero.field("name", zero.string)
57
82
//// use age <- zero.field("age", zero.int)
58
83
//// 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:))
60
86
//// }
61
87
////
62
88
//// let result = clad.decode(args1, decoder)
63
- //// assert result == Ok(Student("Lucy", 8, True))
89
+ //// assert result == Ok(Student("Lucy", 8, True, ["math", "art"] ))
64
90
////
65
91
//// let result = clad.decode(args2, decoder)
66
- //// assert result == Ok(Student("Bob", 3, False))
92
+ //// assert result == Ok(Student("Bob", 3, False, ["math"] ))
67
93
//// ```
68
94
////
69
95
//// ## Alternate Names
74
100
//// Clad provides the `opt()` function for this.
75
101
////
76
102
//// ```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
79
105
////
80
106
//// let decoder = {
81
107
//// use name <- clad.opt(long_name: "name", short_name: "n", zero.string)
82
108
//// use age <- clad.opt(long_name: "age", short_name: "a", zero.int)
83
109
//// 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:))
85
112
//// }
86
113
////
87
114
//// let result = clad.decode(args1, decoder)
88
- //// assert result == Ok(Student("Lucy", 8, True))
115
+ //// assert result == Ok(Student("Lucy", 8, True, ["math", "art"] ))
89
116
////
90
117
//// let result = clad.decode(args2, decoder)
91
- //// assert result == Ok(Student("Bob", 3, False))
118
+ //// assert result == Ok(Student("Bob", 3, False, ["math"] ))
92
119
//// ```
93
120
////
94
121
//// ## Positional Arguments
95
122
////
96
123
//// A CLI may also support positional arguments. These are any arguments that are
97
124
//// 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.
99
126
////
100
127
//// ```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
103
130
////
104
131
//// let decoder = {
105
132
//// use name <- clad.opt("name", "n", zero.string)
106
133
//// use age <- clad.opt("age", "a", zero.int)
107
134
//// 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:))
110
139
//// }
111
140
////
112
141
//// 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
114
149
////
115
150
//// 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" ))
117
152
//// ```
118
153
119
154
import decode/zero . { type Decoder }
@@ -131,7 +166,11 @@ import gleam/string
131
166
const positional_arg_name = "_"
132
167
133
168
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
+ )
135
174
}
136
175
137
176
/// Run a decoder on a list of command line arguments, decoding the value if it
@@ -162,6 +201,8 @@ pub fn decode(
162
201
}
163
202
164
203
/// Get all of the unnamed, positional arguments
204
+ ///
205
+ /// Clad encodes all arguments following a `--` as positional arguments.
165
206
/// ```gleam
166
207
/// let decoder = {
167
208
/// use positional <- clad.positional_arguments
@@ -172,6 +213,9 @@ pub fn decode(
172
213
///
173
214
/// let result = clad.decode(["-a1", "-b", "2"], decoder)
174
215
/// assert result == Ok([])
216
+ ///
217
+ /// let result = clad.decode(["-a1", "--", "-b", "2"], decoder)
218
+ /// assert result == Ok(["-b", "2"])
175
219
/// ```
176
220
pub fn positional_arguments (
177
221
next : fn ( List ( String ) ) -> Decoder ( final) ,
@@ -189,6 +233,9 @@ pub fn positional_arguments(
189
233
/// let result = clad.decode(["-v"], decoder)
190
234
/// assert result == Ok(True)
191
235
///
236
+ /// let result = clad.decode(["-v", "false"], decoder)
237
+ /// assert result == Ok(False)
238
+ ///
192
239
/// let result = clad.decode([], decoder)
193
240
/// assert result == Ok(False)
194
241
/// ```
@@ -228,10 +275,12 @@ fn optional_field(
228
275
/// use name <- clad.opt("name", "n", zero.string)
229
276
/// zero.success(name)
230
277
/// }
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")
235
284
/// ```
236
285
pub fn opt (
237
286
long_name : String ,
@@ -246,15 +295,37 @@ pub fn opt(
246
295
}
247
296
}
248
297
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
+
249
314
fn parse ( args : List ( String ) ) -> State {
250
- let state = State ( dict . new ( ) , list . new ( ) )
315
+ let state = State ( dict . new ( ) , dict . new ( ) , list . new ( ) )
251
316
252
317
let state = parse_args ( args , state )
253
318
State ( .. state , positional : list . reverse ( state . positional ) )
254
319
}
255
320
256
321
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
+
257
327
state . opts
328
+ |> dict . merge ( list_opts )
258
329
|> dict . insert ( positional_arg_name , dynamic . from ( state . positional ) )
259
330
|> dynamic . from
260
331
}
@@ -354,8 +425,24 @@ fn parse_arg(
354
425
}
355
426
356
427
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
+ }
359
446
}
360
447
361
448
fn parse_short ( arg : String , state : State ) -> # ( State , Option ( String ) ) {
0 commit comments