Skip to content

Commit c5b10bf

Browse files
committed
Implement auto-naming of options and positionals
- Each of the ArgParser methods that takes names and returns a Delegate<T> has an overload that takes no name, but returns a DelegateProvider<T>. - A DelegateProvider<T> has an `operator fun provideDelegate` that returns a Delegate<T>, using a name derived from the name of the property the DelegateProvider is being bound to. Also removed option from README.md, as it is internal, and fixed spelling of addValidator.
1 parent 5d0b6c7 commit c5b10bf

File tree

3 files changed

+216
-24
lines changed

3 files changed

+216
-24
lines changed

README.md

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,21 @@ Typical usage is to create a class to represent the set of parsed arguments,
1919
which are in turn each represented by properties that delegate to an
2020
`ArgParser`:
2121

22+
```kotlin
23+
class MyArgs(parser: ArgParser) {
24+
val verbose by parser.flagging(help = "enable verbose mode")
25+
26+
val name by parser.storing(help = "name of the widget")
27+
28+
val size by parser.storing(help = "size of the plumbus") { toInt() }
29+
}
30+
```
31+
32+
The names of an option is inferred from the name of the property it's bound to.
33+
Direct control over the option name is also possible, and for most types of
34+
options it's also possible to have multiple names (typically used for a short
35+
and long name):
36+
2237
```kotlin
2338
class MyArgs(parser: ArgParser) {
2439
val verbose by parser.flagging("-v", "--verbose",
@@ -94,25 +109,10 @@ Various types of options can be parsed from the command line arguments:
94109
Here the `mode` property will be set to the corresponding `Mode` value depending
95110
on which of `--fast`, `--small`, and `--quiet` appears (last) in the arguments.
96111

97-
- For the times when none of the above do what you need, the much more powerful
98-
`option` method can be used. (The methods described above are convenience
99-
methods built on top of `option`.)
112+
`mapping` is one of the few cases where it is not possible to infer the option
113+
name from the property name.
100114

101-
```kotlin
102-
val zaphod by parser.option(
103-
"--fibonacci",
104-
help = "collects fibonnaci sequence, remembers length") {
105-
var prev = 0
106-
var current = 1
107-
var result = 0
108-
while (peek() == current) {
109-
result++
110-
prev, current = current, current+prev
111-
next()
112-
}
113-
return result
114-
}
115-
```
115+
## Modifying Delegates
116116

117117
The delegates returned by any of these methods also have a few methods for setting
118118
optional attributes:
@@ -184,7 +184,7 @@ recommended that transform functions (given to `storing`, `positionalList`, etc.
184184
throw a `SystemExitException` when parsing fails.
185185

186186
Additional post-parsing validation can be performed on a delegate using
187-
`addValidtator`.
187+
`addValidator`.
188188

189189
As a convenience, these exceptions can be handled by using the `runMain`
190190
extension function:

src/main/kotlin/ArgParser.kt

Lines changed: 119 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ class ArgParser(args: Array<out String>,
5757
isRepeating = false,
5858
help = help) { true }.default(false)
5959

60+
/**
61+
* Creates a DelegateProvider for a zero-argument option that returns true if and only the option is present in args.
62+
*/
63+
fun flagging(help: String) =
64+
DelegateProvider { name -> flagging(identifierToOptionName(name), help = help) }
65+
6066
/**
6167
* Creates a Delegate for a zero-argument option that returns the count of how many times the option appears in args.
6268
*/
@@ -68,6 +74,11 @@ class ArgParser(args: Array<out String>,
6874
isRepeating = true,
6975
help = help) { value.orElse { 0 } + 1 }.default(0)
7076

77+
/**
78+
* Creates a DelegateProvider for a zero-argument option that returns the count of how many times the option appears in args.
79+
*/
80+
fun counting(help: String) = DelegateProvider { name -> counting(identifierToOptionName(name), help = help) }
81+
7182
/**
7283
* Creates a Delegate for a single-argument option that stores and returns the option's (transformed) argument.
7384
*/
@@ -85,12 +96,25 @@ class ArgParser(args: Array<out String>,
8596
help = help) { transform(this.next()) }
8697
}
8798

99+
/**
100+
* Creates a DelegateProvider for a single-argument option that stores and returns the option's (transformed) argument.
101+
*/
102+
fun <T> storing(
103+
help: String,
104+
transform: String.() -> T
105+
) = DelegateProvider { name -> storing(identifierToOptionName(name), help = help, transform = transform) }
106+
88107
/**
89108
* Creates a Delegate for a single-argument option that stores and returns the option's argument.
90109
*/
91110
fun storing(vararg names: String, help: String): Delegate<String> =
92111
storing(*names, help = help) { this }
93112

113+
/**
114+
* Creates a DelegateProvider for a single-argument option that stores and returns the option's argument.
115+
*/
116+
fun storing(help: String) = DelegateProvider { name -> storing(identifierToOptionName(name), help = help) }
117+
94118
/**
95119
* Creates a Delegate for a single-argument option that adds the option's (transformed) argument to a
96120
* MutableCollection each time the option appears in args, and returns said MutableCollection.
@@ -114,6 +138,17 @@ class ArgParser(args: Array<out String>,
114138
}.default(initialValue)
115139
}
116140

141+
/**
142+
* Creates a DelegateProvider for a single-argument option that adds the option's (transformed) argument to a
143+
* MutableCollection each time the option appears in args, and returns said MutableCollection.
144+
*/
145+
fun <E, T : MutableCollection<E>> adding(
146+
initialValue: T,
147+
help: String,
148+
transform: String.() -> E
149+
) = DelegateProvider { name ->
150+
adding(identifierToOptionName(name), initialValue = initialValue, help = help, transform = transform) }
151+
117152
/**
118153
* Creates a Delegate for a single-argument option that adds the option's (transformed) argument to a
119154
* MutableList each time the option appears in args, and returns said MutableCollection.
@@ -124,13 +159,30 @@ class ArgParser(args: Array<out String>,
124159
transform: String.() -> T
125160
) = adding(*names, initialValue = mutableListOf(), help = help, transform = transform)
126161

162+
/**
163+
* Creates a DelegateProvider for a single-argument option that adds the option's (transformed) argument to a
164+
* MutableList each time the option appears in args, and returns said MutableCollection.
165+
*/
166+
fun <T> adding(
167+
help: String,
168+
transform: String.() -> T
169+
) = DelegateProvider { name ->
170+
adding(identifierToOptionName(name), help = help, transform = transform) }
171+
127172
/**
128173
* Creates a Delegate for a single-argument option that adds the option's argument to a MutableList each time the
129174
* option appears in args, and returns said MutableCollection.
130175
*/
131176
fun adding(vararg names: String, help: String): Delegate<MutableList<String>> =
132177
adding(*names, help = help) { this }
133178

179+
/**
180+
* Creates a DelegateProvider for a single-argument option that adds the option's argument to a MutableList each time the
181+
* option appears in args, and returns said MutableCollection.
182+
*/
183+
fun adding(help: String) = DelegateProvider { name ->
184+
adding(identifierToOptionName(name), help = help) }
185+
134186
/**
135187
* Creates a Delegate for a zero-argument option that maps from the option's name as it appears in args to one of a
136188
* fixed set of values.
@@ -189,6 +241,12 @@ class ArgParser(args: Array<out String>,
189241
*/
190242
fun positional(name: String, help: String) = positional(name, help = help) { this }
191243

244+
/**
245+
* Creates a DelegateProvider for a single positional argument which returns the argument's value.
246+
*/
247+
fun positional(help: String) =
248+
DelegateProvider { ident -> positional(identifierToArgName(ident), help = help) }
249+
192250
/**
193251
* Creates a Delegate for a single positional argument which returns the argument's transformed value.
194252
*/
@@ -204,6 +262,14 @@ class ArgParser(args: Array<out String>,
204262
}
205263
}
206264

265+
/**
266+
* Creates a DelegateProvider for a single positional argument which returns the argument's transformed value.
267+
*/
268+
fun <T> positional(
269+
help: String,
270+
transform: String.() -> T
271+
) = DelegateProvider { ident -> positional(identifierToArgName(ident), help = help, transform = transform) }
272+
207273
/**
208274
* Creates a Delegate for a sequence of positional arguments which returns a List containing the arguments.
209275
*/
@@ -213,6 +279,14 @@ class ArgParser(args: Array<out String>,
213279
help: String
214280
) = positionalList(name, sizeRange, help = help) { this }
215281

282+
/**
283+
* Creates a DelegateProvider for a sequence of positional arguments which returns a List containing the arguments.
284+
*/
285+
fun positionalList(
286+
sizeRange: IntRange = 1..Int.MAX_VALUE,
287+
help: String
288+
) = DelegateProvider { ident -> positionalList(identifierToArgName(ident), sizeRange, help = help) }
289+
216290
/**
217291
* Creates a Delegate for a sequence of positional arguments which returns a List containing the transformed
218292
* arguments.
@@ -241,6 +315,16 @@ class ArgParser(args: Array<out String>,
241315
}
242316
}
243317

318+
/**
319+
* Creates a DelegateProvider for a sequence of positional arguments which returns a List containing the transformed
320+
* arguments.
321+
*/
322+
fun <T> positionalList(
323+
sizeRange: IntRange = 1..Int.MAX_VALUE,
324+
help: String,
325+
transform: String.() -> T
326+
) = DelegateProvider { ident -> positionalList(identifierToArgName(ident), sizeRange, help, transform) }
327+
244328
internal abstract class WrappingDelegate<U, W>(private val inner: Delegate<U>) : Delegate<W> {
245329

246330
abstract fun wrap(u: U): W
@@ -258,7 +342,7 @@ class ArgParser(args: Array<out String>,
258342
override fun default(value: W): Delegate<W> =
259343
apply { inner.default(unwrap(value)) }
260344

261-
override fun addValidtator(validator: Delegate<W>.() -> Unit): Delegate<W> =
345+
override fun addValidator(validator: Delegate<W>.() -> Unit): Delegate<W> =
262346
apply { validator(this) }
263347
}
264348

@@ -281,7 +365,31 @@ class ArgParser(args: Array<out String>,
281365
fun default(value: T): Delegate<T>
282366

283367
/** Add validation logic. Validator should throw a [SystemExitException] on failure. */
284-
fun addValidtator(validator: Delegate<T>.() -> Unit): Delegate<T>
368+
fun addValidator(validator: Delegate<T>.() -> Unit): Delegate<T>
369+
370+
@Deprecated("Use addValidator instead")
371+
fun addValidtator(validator: Delegate<T>.() -> Unit) = addValidator(validator)
372+
}
373+
374+
/**
375+
* Provides a [Delegate] when given a name. This makes it possible to infer
376+
* a name for the `Delegate` based on the name it is bound to, rather than
377+
* specifying a name explicitly.
378+
*/
379+
class DelegateProvider<T>(private val ctor: (ident: String) -> Delegate<T>) {
380+
operator fun provideDelegate(thisRef: Any?, prop: KProperty<*>): Delegate<T> {
381+
return ctor(prop.name).apply {
382+
defaultValue?.let {
383+
default(it.value)
384+
}
385+
}
386+
}
387+
388+
fun default(t: T): DelegateProvider<T> = apply {
389+
defaultValue = Holder(t)
390+
}
391+
392+
private var defaultValue : Holder<T>? = null
285393
}
286394

287395
internal abstract class ParsingDelegate<T>(
@@ -305,7 +413,7 @@ class ArgParser(args: Array<out String>,
305413
return this
306414
}
307415

308-
override fun addValidtator(validator: Delegate<T>.() -> Unit): Delegate<T> = apply {
416+
override fun addValidator(validator: Delegate<T>.() -> Unit): Delegate<T> = apply {
309417
validators.add(validator)
310418
}
311419

@@ -621,6 +729,14 @@ class ArgParser(args: Array<out String>,
621729

622730
private fun optionNameToArgName(name: String) =
623731
LEADING_HYPHENS.replace(name, "").toUpperCase().replace('-', '_')
732+
733+
internal fun identifierToOptionName(ident: String) : String {
734+
return if (ident.length == 1) ("-" + ident) else ("--" + ident.replace('_', '-'))
735+
}
736+
737+
internal fun identifierToArgName(ident: String) : String {
738+
return ident.toUpperCase()
739+
}
624740
}
625741

626742
init {

src/test/kotlin/ArgParserTest.kt

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ package com.xenomachina.argparser
2020

2121
import com.xenomachina.common.orElse
2222
import org.junit.Assert.assertEquals
23-
import org.junit.Assert.assertTrue
2423
import org.junit.Assert.assertFalse
24+
import org.junit.Assert.assertTrue
2525
import org.junit.Rule
2626
import org.junit.Test
2727
import org.junit.rules.TestName
@@ -267,6 +267,32 @@ class ArgParserTest {
267267
8,
268268
Args(parserOf("-x9", "-x8")).x)
269269
}
270+
@Test
271+
fun testDefaultWithProvider() {
272+
class Args(parser: ArgParser) {
273+
val x by parser.storing(help = TEST_HELP) { toInt() }.default(5)
274+
}
275+
276+
// Test with no value
277+
assertEquals(
278+
5,
279+
Args(parserOf()).x)
280+
281+
// Test with value
282+
assertEquals(
283+
6,
284+
Args(parserOf("-x6")).x)
285+
286+
// Test with value as separate arg
287+
assertEquals(
288+
7,
289+
Args(parserOf("-x", "7")).x)
290+
291+
// Test with multiple values
292+
assertEquals(
293+
8,
294+
Args(parserOf("-x9", "-x8")).x)
295+
}
270296

271297
@Test
272298
fun testFlag() {
@@ -587,7 +613,7 @@ class ArgParserTest {
587613

588614
val xDelegate = parser.storing("-x",
589615
help = TEST_HELP) { toInt() }
590-
.addValidtator {
616+
.addValidator {
591617
if (value.rem(2) != 0)
592618
throw InvalidArgumentException("$errorName must be even, $value is odd")
593619
}
@@ -904,6 +930,56 @@ ultrices tempus lectus fermentum vestibulum. Phasellus.
904930
}
905931
}
906932

933+
@Test
934+
fun testImplicitLongFlagName() {
935+
class Args(parser: ArgParser) {
936+
val flag1 by parser.flagging(help = TEST_HELP)
937+
val flag2 by parser.flagging(help = TEST_HELP)
938+
val count by parser.counting(help = TEST_HELP)
939+
val store by parser.storing(help = TEST_HELP)
940+
val store_int by parser.storing(help = TEST_HELP) { toInt() }
941+
val adder by parser.adding(help = TEST_HELP)
942+
val int_adder by parser.adding(help = TEST_HELP) { toInt() }
943+
val int_set_adder by parser.adding(initialValue = mutableSetOf<Int>(), help = TEST_HELP) { toInt() }
944+
val positional by parser.positional(help = TEST_HELP)
945+
val positional_int by parser.positional(help = TEST_HELP) { toInt() }
946+
val positionalList by parser.positionalList(sizeRange = 2..2, help = TEST_HELP)
947+
val positionalList_int by parser.positionalList(sizeRange = 2..2, help = TEST_HELP) { toInt() }
948+
}
949+
950+
Args(parserOf(
951+
"--flag1", "--count", "--count", "--store=hello", "--store-int=42",
952+
"--adder=foo", "--adder=bar",
953+
"--int-adder=2", "--int-adder=4", "--int-adder=6",
954+
"--int-set-adder=64", "--int-set-adder=128", "--int-set-adder=20",
955+
"1", "1", "2", "3", "5", "8"
956+
)).run {
957+
assertTrue(flag1)
958+
assertFalse(flag2)
959+
assertEquals(2, count)
960+
assertEquals("hello", store)
961+
assertEquals(42, store_int)
962+
assertEquals(listOf("foo", "bar"), adder)
963+
assertEquals(listOf(2, 4, 6), int_adder)
964+
assertEquals(setOf(20, 64, 128), int_set_adder)
965+
assertEquals("1", positional)
966+
assertEquals(1, positional_int)
967+
assertEquals(listOf("2", "3"), positionalList)
968+
assertEquals(listOf(5, 8), positionalList_int)
969+
}
970+
971+
shouldThrow<MissingRequiredPositionalArgumentException> {
972+
Args(parserOf(
973+
"13", "21", "34", "55", "89"
974+
)).run {
975+
assertFalse(flag1)
976+
}
977+
}.run {
978+
assertEquals("missing POSITIONALLIST_INT operand", message)
979+
}
980+
}
981+
982+
// TODO: test auto-naming on positional args
907983
// TODO: test default on argument
908984
// TODO: test default on argumentList
909985
// TODO: test addValidator on argument

0 commit comments

Comments
 (0)