Skip to content

Commit 7f4c2af

Browse files
committed
Add option and argument name validation
1 parent 096f3e3 commit 7f4c2af

File tree

2 files changed

+133
-2
lines changed

2 files changed

+133
-2
lines changed

src/main/kotlin/ArgParser.kt

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ class ArgParser(args: Array<out String>,
214214
* @param handler a function that computes the value of this option from an [OptionInvocation]
215215
*/
216216
fun <T> option(
217+
// TODO: fix ordering: help goes first
217218
// TODO: add optionalArg: Boolean
218219
vararg names: String,
219220
errorName: String,
@@ -243,6 +244,7 @@ class ArgParser(args: Array<out String>,
243244
* @param handler a function that computes the value of this option from an [OptionInvocation]
244245
*/
245246
fun <T> option(
247+
// TODO: fix ordering: help goes first
246248
// TODO: add optionalArg: Boolean
247249
errorName: String,
248250
help: String,
@@ -480,6 +482,18 @@ class ArgParser(args: Array<out String>,
480482
val argNames: List<String>,
481483
val isRepeating: Boolean,
482484
val handler: OptionInvocation<T>.() -> T) : ParsingDelegate<T>(parser, errorName, help) {
485+
init {
486+
for (optionName in optionNames) {
487+
if (!OPTION_NAME_RE.matches(optionName)) {
488+
throw IllegalArgumentException("$optionName is not a valid option name")
489+
}
490+
}
491+
for (argName in argNames) {
492+
if (!ARG_NAME_RE.matches(argName)) {
493+
throw IllegalArgumentException("$argName is not a valid argument name")
494+
}
495+
}
496+
}
483497

484498
fun parseOption(name: String, firstArg: String?, index: Int, args: Array<out String>): Int {
485499
val arguments = mutableListOf<String>()
@@ -521,10 +535,16 @@ class ArgParser(args: Array<out String>,
521535

522536
private class PositionalDelegate<T>(
523537
parser: ArgParser,
524-
errorName: String,
538+
argName: String,
525539
val sizeRange: IntRange,
526540
help: String,
527-
val transform: String.() -> T) : ParsingDelegate<List<T>>(parser, errorName, help) {
541+
val transform: String.() -> T) : ParsingDelegate<List<T>>(parser, argName, help) {
542+
543+
init {
544+
if (!ARG_NAME_RE.matches(argName)) {
545+
throw IllegalArgumentException("$argName is not a valid argument name")
546+
}
547+
}
528548

529549
override fun registerLeaf(root: Delegate<*>) {
530550
assert(holder == null)
@@ -780,6 +800,13 @@ class ArgParser(args: Array<out String>,
780800
}
781801
}
782802

803+
private const val OPTION_CHAR_CLASS = "[a-zA-Z0-9]"
804+
private val OPTION_NAME_RE = Regex("^(-$OPTION_CHAR_CLASS)|(--$OPTION_CHAR_CLASS+([-_]$OPTION_CHAR_CLASS+)*)$")
805+
private const val ARG_INITIAL_CHAR_CLASS = "[A-Z]"
806+
private const val ARG_CHAR_CLASS = "[A-Z0-9]"
807+
private val ARG_NAME_RE = Regex("^$ARG_INITIAL_CHAR_CLASS+([-_]$ARG_CHAR_CLASS+)*$")
808+
809+
783810
fun <T> ArgParser.DelegateProvider<T>.default(newDefault: T): ArgParser.DelegateProvider<T> {
784811
return ArgParser.DelegateProvider(ctor = ctor, defaultHolder = Holder(newDefault))
785812
}

src/test/kotlin/ArgParserTest.kt

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,110 @@ private val oneArgName = listOf("ARG_NAME")
6262

6363
abstract class Test(body: () -> Unit) : FunSpec({ test(this::class.qualifiedName!!, body) })
6464

65+
class OptionNameValidationTest : Test({
66+
val parser = parserOf()
67+
68+
// These are all acceptable.
69+
parser.option<Int>("-x", errorName = "", help = TEST_HELP) { 0 }
70+
parser.option<Int>("--x", errorName = "", help = TEST_HELP) { 0 }
71+
parser.option<Int>("--xy", errorName = "", help = TEST_HELP) { 0 }
72+
parser.option<Int>("-X", errorName = "", help = TEST_HELP) { 0 }
73+
parser.option<Int>("--X", errorName = "", help = TEST_HELP) { 0 }
74+
parser.option<Int>("--XY", errorName = "", help = TEST_HELP) { 0 }
75+
parser.option<Int>("--X-Y", errorName = "", help = TEST_HELP) { 0 }
76+
parser.option<Int>("--X_Y", errorName = "", help = TEST_HELP) { 0 }
77+
parser.option<Int>("-5", errorName = "", help = TEST_HELP) { 0 }
78+
parser.option<Int>("--5", errorName = "", help = TEST_HELP) { 0 }
79+
parser.option<Int>("--5Y", errorName = "", help = TEST_HELP) { 0 }
80+
parser.option<Int>("--X5", errorName = "", help = TEST_HELP) { 0 }
81+
82+
shouldThrow<IllegalArgumentException> {
83+
parser.option<Int>("-_", errorName = "", help = TEST_HELP) { 0 }
84+
}
85+
86+
shouldThrow<IllegalArgumentException> {
87+
parser.option<Int>("---x", errorName = "", help = TEST_HELP) { 0 }
88+
}
89+
90+
shouldThrow<IllegalArgumentException> {
91+
parser.option<Int>("x", errorName = "", help = TEST_HELP) { 0 }
92+
}
93+
94+
shouldThrow<IllegalArgumentException> {
95+
parser.option<Int>("", errorName = "", help = TEST_HELP) { 0 }
96+
}
97+
98+
shouldThrow<IllegalArgumentException> {
99+
parser.option<Int>("-xx", errorName = "", help = TEST_HELP) { 0 }
100+
}
101+
102+
shouldThrow<IllegalArgumentException> {
103+
parser.option<Int>("--foo bar", errorName = "", help = TEST_HELP) { 0 }
104+
}
105+
106+
shouldThrow<IllegalArgumentException> {
107+
parser.option<Int>("--foo--bar", errorName = "", help = TEST_HELP) { 0 }
108+
}
109+
110+
shouldThrow<IllegalArgumentException> {
111+
parser.option<Int>("--f!oobar", errorName = "", help = TEST_HELP) { 0 }
112+
}
113+
})
114+
115+
class PositionalNameValidationTest : Test({
116+
val parser = parserOf()
117+
118+
// These are all acceptable.
119+
parser.positional<Int>("X", help = TEST_HELP) { 0 }
120+
parser.positional<Int>("XYZ", help = TEST_HELP) { 0 }
121+
parser.positional<Int>("XY-Z", help = TEST_HELP) { 0 }
122+
parser.positional<Int>("XY_Z", help = TEST_HELP) { 0 }
123+
124+
shouldThrow<IllegalArgumentException> {
125+
parser.positional<Int>("-", help = TEST_HELP) { 0 }
126+
}
127+
128+
shouldThrow<IllegalArgumentException> {
129+
parser.positional<Int>("_", help = TEST_HELP) { 0 }
130+
}
131+
132+
shouldThrow<IllegalArgumentException> {
133+
parser.positional<Int>("x", help = TEST_HELP) { 0 }
134+
}
135+
136+
shouldThrow<IllegalArgumentException> {
137+
parser.positional<Int>("", help = TEST_HELP) { 0 }
138+
}
139+
140+
shouldThrow<IllegalArgumentException> {
141+
parser.positional<Int>("-X", help = TEST_HELP) { 0 }
142+
}
143+
144+
shouldThrow<IllegalArgumentException> {
145+
parser.positional<Int>("X-", help = TEST_HELP) { 0 }
146+
}
147+
148+
shouldThrow<IllegalArgumentException> {
149+
parser.positional<Int>("X--Y", help = TEST_HELP) { 0 }
150+
}
151+
152+
shouldThrow<IllegalArgumentException> {
153+
parser.positional<Int>("X!", help = TEST_HELP) { 0 }
154+
}
155+
156+
shouldThrow<IllegalArgumentException> {
157+
parser.positional<Int>("5", help = TEST_HELP) { 0 }
158+
}
159+
160+
// This should be acceptable
161+
parser.option<Int>("--foobar", argNames = listOf("X-Y"), errorName = "", help = TEST_HELP) { 0 }
162+
163+
// This should not
164+
shouldThrow<IllegalArgumentException> {
165+
parser.option<Int>("--foobar", argNames = listOf("X--Y"), errorName = "", help = TEST_HELP) { 0 }
166+
}
167+
})
168+
65169
class ArglessShortOptionsTest : Test({
66170
class Args(parser: ArgParser) {
67171
val xyz by parser.option<MutableList<String>>("-x", "-y", "-z",

0 commit comments

Comments
 (0)