- Kotlin 100%
| gradle/wrapper | ||
| src | ||
| .gitignore | ||
| build.gradle.kts | ||
| gradlew | ||
| gradlew.bat | ||
| LICENSE | ||
| README.MD | ||
| settings.gradle.kts | ||
Args
Args is a simple library for extracting well-defined key-value style arguments from your runtime program arguments.
Motivation
JVM-based applications do not have a standardized method to extract program arguments passed-in at application runtime
boot; they are simply shipped to your main function as an array of strings.
The Args library allows application developers to create well-defined named arguments that have well-defined value types. These arguments and their specified values are parsed by Args and stored in a type-safe map.
Usage
Here is a sample application that uses Args to create well-defined arguments:
class TestSample {
val nameArg = Argument.Text("name", 'n', Regex("(?:[A-Z][A-Za-z\\-'_]* ?)+"))
val ageArg = Argument.Integral("age", 'a', 0..120)
val favoriteSeasonArg = Argument.Text("favoriteSeason", 's', Regex("spring|summer|autumn|winter"))
val percentGremlinArg = Argument.Decimal("percentGremlin", 'G', 0.0..100.0)
val isFurryArg = Argument.Flag("isFurry", 'F')
val boopedArg = Argument.Presence("booped", 'b')
class MyArgsDelegate(backing: ArgsMap) {
val name: String? by backing
val age: Long? by backing
val favoriteSeason: String? by backing
val percentGremlin: Double? by backing
val isFurry: Boolean? by backing
val booped: Unit? by backing
}
val MyArgs = Arguments(nameArg, ageArg, favoriteSeasonArg, percentGremlinArg, isFurryArg, boopedArg)
fun parseMyArgs(vararg args: CharSequence): MyArgsDelegate =
MyArgsDelegate(MyArgs.parse(args))
@Test fun `parse sample with good args`() {
val delegate = parseMyArgs(
"--name", "Vivi",
"--age", "5", // in fox years
"+s", "autumn",
"+G", "98.17",
"+Fb"
)
assertEquals("Vivi", delegate.name)
assertEquals(5L, delegate.age)
assertEquals("autumn", delegate.favoriteSeason)
assertEquals(99.0, delegate.percentGremlin!!, 1.0)
assertNotNull(delegate.booped)
assertTrue(delegate.isFurry)
}
}
Declaring Named Arguments
All arguments are defined using one of the Argument subclasses. Common to all argument types, they have an associated
"long name", a case-insensitive alphanumeric string that is passed to your application with a -- or ++ prefix; and
an optional "short name", a case-sensitive alphanumeric character that is passed to your application with a - or +
prefix.
Parsing Input Arguments
Once all desired Argument types are defined, you may package them into an Arguments set for parsing and processing.
On instantiation, Arguments will throw an exception if there are any argument name conflicts. All argument long names
must be unique, case-insensitively. Short names, when not null (meaning that argument has no short name), must be
unique, case-sensitively. (Flag-type arguments have an additional restriction in that, because the capitalization of
its short name is pertinent to its state, their short names must be unique case-insensitively.)
When you receive input application arguments (e.g. those passed to your fun main(vararg args: String) function by
your CLI), you may pass them to Arguments.parse (as an Array<CharSequence>) and receive an ArgsMap in return.
Note that the parse function does not manage argument term delimiters like spaces; terms in the array must be
delimited and assigned to sequential indices in the input Array externally (e.g. as handled by the CLI).
Arguments have a value associated with them. Generally, arguments receive an explicit value in the input term immediately following the argument declaration term in the input Array.
If the input arguments do not conform to the restrictions of the Args specification - e.g., arguments with long/short
names not registered with your Arguments set; values not attached to a respective named argument; missing values for
argument types that require explicit values; etc. - an exception will be thrown by the parse invocation.
ArgsMap
ArgsMap is a simple type-safe map, using Argument objects as keys, and their resolved associated input values as
values. For a given arg: Argument<T>, a call to argsMap.get(arg) (or argsMap[arg]) will return a non-null element
of type T if that argument was present in the input, or null if that argument is absent in the input.
Argument Types
Presence (<Unit>)
val boopedArg = Argument.Presence("booped", 'b') //usage: parse("--booped"), parse("-b")
class Presence(longName: String, shortName: Char? = null): Argument<Unit>
Presence-type arguments signal whether or not they were present in the input arguments set. If present, the argument
will yield Unit as value.
When specifying input Presence argument by short names, you may group them in one argument term. For example, to
signal presence of three Presence arguments with short names 'a', 'b', 'c', you may pass one term "-abc" over input.
Flag (<Boolean>)
val isFurryArg = Argument.Flag("isFurry", 'F')
class Flag(longName: String, shortName: Char? = null): Argument<Boolean>
Flag-type arguments yield a true or false boolean value.
Flag arguments may receive an optional explicit value which always takes precedence over implicit rules:
-F true, --isFurry false
When an explicit value is not provided, the following rules govern the flag's implicit value:
- When specified by long name:
- A
++prefix impliestrue(++isFurry). - A
--prefix impliesfalse(--isFurry).
- A
- When specified by short name:
- Flags have a "Direct" specification variant (when specified in uppercase or numeric), and an "Inverse" variant (when specified in lowercase).
- In Direct Specification:
- A
+prefix impliestrue(+F). - A
-prefix impliesfalse(-F).
- A
- In Inverse Specification:
- A
+prefix impliesfalse(+f). - A
-prefix impliestrue(-f).
- A
Like Presence arguments, Flag arguments can be specified in short name groups, following the implicit rules above;
for instance, +abC would set the values of arguments with short names 'a' and 'b' to false, and 'c' to true.
Integral (<Long>)
val ageArg = Argument.Integral("age", 'a', 0..120)
class Integral(
longName: String,
shortName: Char? = null,
val range: LongRange = Long.MIN_VALUE..Long.MAX_VALUE
): Argument<Long>
Integral-type arguments yield a Long integer value. The argument may restrict the range of input values (inclusive)
that are allowed to be assigned to its value. If the input value is out of range, Arguments.parse will throw an
exception. Integral arguments require an explicit value to be provided by input.
The value may use any format compatible with String.toLongLenient. If it returns
null (malformed input), Arguments.parse will throw an exception.
Decimal (<Double>)
val percentGremlinArg = Argument.Decimal("percentGremlin", 'G', 0.0..100.0)
class Decimal(
longName: String,
shortName: Char? = null,
val range: ClosedFloatingPointRange<Double> = Double.NEGATIVE_INFINITY..Double.POSITIVE_INFINITY
): Argument<Double>
Decimal-type arguments yield a Double floating point value. The argument may restrict the range of input values
(inclusive) that are allowed to be assigned to its value. If the input value is out of range, Arguments.parse will
throw an exception. Decimal arguments require an explicit value to be provided by input.
The value may use any format compatible with String.toDouble. Exceptions thrown by toDouble caused by malformed
input will be rethrown by Arguments.parse.
Text (<String>)
val nameArg = Argument.Text("name", 'n', Regex("(?:[A-Z][A-Za-z\\-'_]* ?)+"))
class Text(
longName: String,
shortName: Char? = null,
val regexRestriction: Regex? = null
): Argument<String>
Text-type arguments yield a String value (i.e., it will return the raw contents of the next index of terms).
Text arguments may optionally specify a regexRestriction, which will cause Arguments.parse to throw an exception
if the value does not match the Regex pattern.
License
Args by Archandle Studio is marked CC0 1.0.
To view a copy of this mark, please read the LICENSE file or visit https://creativecommons.org/publicdomain/zero/1.0/