Skip to content

Commit

Permalink
Return ArgumentCollection from parse
Browse files Browse the repository at this point in the history
This separates the definition of options (what's available to a user)
from the associated arguments (what's provided by the user).

Included documentation on public methods.

See #2
  • Loading branch information
jimschubert committed Dec 12, 2016
1 parent f5e0320 commit d542dca
Show file tree
Hide file tree
Showing 13 changed files with 240 additions and 82 deletions.
4 changes: 2 additions & 2 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#Mon Dec 05 21:26:08 EST 2016
#Sun Dec 11 20:37:03 EST 2016
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-3.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-3.2-bin.zip
18 changes: 9 additions & 9 deletions kopper-cli/src/main/kotlin/us/jimschubert/kopper/cli/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@ fun main(args: Array<String>) {
parser.flag("a", listOf("allowEmpty"))
parser.flag("h", listOf("help"), description = "Displays this message")

parser.parse(arrayOf("-f", "asdf.txt", "--quiet=true", "trailing", "arguments" ))
val options = parser.parse(arrayOf("-f", "asdf.txt", "--quiet=true", "trailing", "arguments" ))

if(parser.isSet("h")) {
if(options.flag("h")) {
println(parser.printHelp())
return
}

println("q=${parser.isSet("q")}")
println("quiet=${parser.isSet("quiet")}")
println("silent=${parser.isSet("silent")}")
println("f=${parser.get("f")}")
println("file=${parser.get("file")}")
println("allowEmpty=${parser.isSet("allowEmpty")}")
println("remainingArgs=${parser.remainingArgs.joinToString()}")
println("q=${options.flag("q")}")
println("quiet=${options.flag("quiet")}")
println("silent=${options.flag("silent")}")
println("f=${options.option("f")}")
println("file=${options.option("file")}")
println("allowEmpty=${options.flag("allowEmpty")}")
println("unparsedArgs=${options.unparsedArgs.joinToString()}")
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package us.jimschubert.kopper.typed
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty

/**
* Delegates argument parsing of a boolean option
*/
class BooleanArgument(
caller: TypedArgumentParser,
val shortOption: String,
Expand All @@ -21,7 +24,6 @@ class BooleanArgument(
* @return the property value.
*/
override fun getValue(thisRef: TypedArgumentParser, property: KProperty<*>): Boolean {
thisRef.ensureParsed()
return thisRef.parser.isSet(shortOption)
return thisRef.ensureParsed().flag(shortOption)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package us.jimschubert.kopper.typed
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty

/**
* Delegates argument parsing of a numeric option defined by @param[T]
*/
class NumericArgument<out T>(
caller: TypedArgumentParser,
val shortOption: String,
Expand All @@ -23,14 +26,13 @@ class NumericArgument<out T>(
*/
@Suppress("UNCHECKED_CAST")
override fun getValue(thisRef: TypedArgumentParser, property: KProperty<*>): T? {
thisRef.ensureParsed()
when (default) {
is Float -> return thisRef.parser.get(shortOption)?.toFloat() as T? ?: default
is Long -> return thisRef.parser.get(shortOption)?.toLong() as T? ?: default
is Int -> return thisRef.parser.get(shortOption)?.toInt() as T? ?: default
is Double -> return thisRef.parser.get(shortOption)?.toDouble() as T? ?: default
is Byte -> return thisRef.parser.get(shortOption)?.toByte() as T? ?: default
is Short -> return thisRef.parser.get(shortOption)?.toShort() as T? ?: default
is Float -> return thisRef.ensureParsed().option(shortOption)?.toFloat() as T? ?: default
is Long -> return thisRef.ensureParsed().option(shortOption)?.toLong() as T? ?: default
is Int -> return thisRef.ensureParsed().option(shortOption)?.toInt() as T? ?: default
is Double -> return thisRef.ensureParsed().option(shortOption)?.toDouble() as T? ?: default
is Byte -> return thisRef.ensureParsed().option(shortOption)?.toByte() as T? ?: default
is Short -> return thisRef.ensureParsed().option(shortOption)?.toShort() as T? ?: default
}

return null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import us.jimschubert.kopper.StringOption
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty

/**
* Delegates argument parsing of a string option
*/
class StringArgument(
caller: TypedArgumentParser,
val shortOption: String,
Expand All @@ -23,7 +26,6 @@ class StringArgument(
* @return the property value.
*/
override fun getValue(thisRef: TypedArgumentParser, property: KProperty<*>): String {
thisRef.ensureParsed()
return thisRef.parser.get(shortOption) ?: ""
return thisRef.ensureParsed().option(shortOption) ?: ""
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
package us.jimschubert.kopper.typed

import us.jimschubert.kopper.ArgumentCollection
import us.jimschubert.kopper.Parser

/**
* A base type for object-oriented argument parsing.
*
* Use delegated properties defined by, e.g. [BooleanArgument], [NumericArgument], [StringArgument]
*/
abstract class TypedArgumentParser(val args: Array<String>) {
private var hasParsed = false
private var arguments: ArgumentCollection? = null
val self: TypedArgumentParser by lazy { this }
internal val parser = Parser()
internal fun ensureParsed() {
if(!hasParsed) {
parser.parse(args)
hasParsed = true
internal fun ensureParsed() : ArgumentCollection {
if(arguments == null) {
arguments = parser.parse(args)
}
return arguments!!
}

fun printHelp() : String {
return parser.printHelp()
}

val _etc_: List<String> get() = parser.remainingArgs
val _etc_: List<String> get() {
return ensureParsed().unparsedArgs
}
}
14 changes: 14 additions & 0 deletions kopper/src/main/kotlin/us/jimschubert/kopper/Argument.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package us.jimschubert.kopper

/**
* Represents an actual value passed via command line arguments, including the option
* responsible for parsing the argument, and the args list that provided options and values.
*/
data class Argument<out T>(val value: T?, val option: Option<*>, val args: List<String> = listOf())

/**
* Merges a newer argument with an older argument
*/
operator fun <T> Argument<T>.plus(newer: Argument<T>): Argument<T> {
return copy(value = newer.value, args = args + newer.args)
}
25 changes: 25 additions & 0 deletions kopper/src/main/kotlin/us/jimschubert/kopper/ArgumentCollection.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package us.jimschubert.kopper

/**
* Represents a collection of arguments provided by the user.
*
* Includes helper methods for querying by option name (short or long).
*
* @note Maintains a bucket of unparsed arguments
*/
class ArgumentCollection(val unparsedArgs: List<String>,
parsedArgumentList: List<Argument<*>>
) : List<Argument<*>> by parsedArgumentList {
fun option(name: String): String? {
val found: Argument<*>? = find { (it.option.shortOption == name || it.option.longOption.contains(name)) }
return (found?.value as? String?)
}

fun flag(name: String): Boolean {
val found = find {
it.option.isFlag && (it.option.shortOption == name || it.option.longOption.contains(name))
} ?: return false

return found.value as? Boolean ?: false
}
}
15 changes: 13 additions & 2 deletions kopper/src/main/kotlin/us/jimschubert/kopper/Option.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package us.jimschubert.kopper

/**
* A generic representation of a command line option
*/
abstract class Option<T>(
open val shortOption: String,
open val longOption: List<String> = listOf(),
Expand Down Expand Up @@ -44,15 +47,20 @@ abstract class Option<T>(
return (value as? T?) ?: default
}

internal fun applyParsedOption(value: String?): Unit {
internal fun applyParsedOption(value: String?): T? {
actual = parseOption(value)
return actual
}

internal fun setAsDefault() {
internal fun setAsDefault(): T? {
this.actual = this.default
return this.actual
}
}

/**
* A command line option represented as a string
*/
class StringOption(
override val shortOption: String,
override val longOption: List<String> = listOf(),
Expand All @@ -67,6 +75,9 @@ class StringOption(
false
)

/**
* A command line option represented as a true/false value
*/
class BooleanOption(
override val shortOption: String,
override val longOption: List<String> = listOf(),
Expand Down
86 changes: 66 additions & 20 deletions kopper/src/main/kotlin/us/jimschubert/kopper/Parser.kt
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
package us.jimschubert.kopper

/**
* Defines how a set of command line options will be parsed into the arguments that were passed
*/
class Parser {
private var options: MutableList<Option<*>> = mutableListOf()
private var _args: MutableList<String> = mutableListOf()

private var _name: String? = null

/**
* The name of the application
*/
val name: String?
get() = _name

private var _applicationDescription: String? = null

/**
* A description of the application
*/
val applicationDescription: String?
get() = _applicationDescription

val remainingArgs: List<String> get() = _args.toList()

fun option(shortOption: String,
longOption: List<String> = listOf(),
description: String? = null,
Expand All @@ -23,16 +32,25 @@ class Parser {
return this
}

/**
* Sets the application name
*/
fun setName(name: String) : Parser {
_name = name
return this
}

/**
* Sets the application description
*/
fun setApplicationDescription(description: String) : Parser {
_applicationDescription = description
return this
}

/**
* Defines a flag/switch represented as a true or false value
*/
fun flag(shortOption: String,
longOption: List<String> = listOf(),
description: String? = null,
Expand All @@ -42,52 +60,80 @@ class Parser {
return this
}

/**
* Allows for defining custom options derived from [Option]
*/
fun <T> custom(option: Option<T>): Parser {
options.add(option)

return this
}

fun parse(args: Array<String>) {
/**
* Parses input args into a collection of arguments with metadata about how those arguments were parsed.
*/
fun parse(args: Array<String>): ArgumentCollection {
_args.clear()
val passedArguments : MutableList<Argument<*>> = mutableListOf()
var argIndex = 0

while(argIndex < args.size) {
val s = args[argIndex]

if(s.startsWith("--")) {
val (left,right) = s.removePrefix("--").kvp()
val option = options.find { it.longOption.contains(left) }
if(false == option?.isFlag)
val result = if(false == option?.isFlag) {
option?.applyParsedOption(right)
else
}
else {
option?.applyParsedOption(right)
}

if(option != null) {
val usedArgs = if (right != null) listOf(left, right) else listOf(left)
val prev = passedArguments.find { it.option == option }
val current = Argument(result, option, usedArgs)
if(prev != null) {
passedArguments.remove(prev)
passedArguments.add(prev + current)
} else {
passedArguments.add(current)
}
}
} else if (s.startsWith("-")) {
val next = if(argIndex < (args.size-1)) args[argIndex+1] else null
val option = options.find { it.shortOption == s.removePrefix("-")}
if(false == next?.startsWith("-")) {
option?.applyParsedOption(next)
val hasFollowingOption = next?.startsWith("-")
val result = if(false == hasFollowingOption) {
argIndex++
} else if (option != null) {
option?.applyParsedOption(next)
} else {
option?.setAsDefault()
}

if(option != null) {
val usedArgs = if(false == hasFollowingOption) listOf<String>(next!!) else listOf<String>()
val prev = passedArguments.find { it.option == option }
val current = Argument(result, option, usedArgs)
if(prev != null) {
passedArguments.remove(prev)
passedArguments.add(prev + current)
} else {
passedArguments.add(current)
}
}
} else _args.add(s)

argIndex++
}
}

fun get(option: String): String? {
val found = options.find { (it.shortOption == option || it.longOption.contains(option)) }
return found?.actual as? String?
}

fun isSet(option: String): Boolean {
val found = options.find {
it.isFlag && (it.shortOption == option || it.longOption.contains(option))
} as? BooleanOption ?: return false

return found.actual ?: false
return ArgumentCollection(_args.toList(), passedArguments)
}

/**
* Provides a formatted string for printing a help message to a user.
*/
fun printHelp(): String {
val buffer = StringBuffer()
if(name != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package us.jimschubert.kopper

fun String.kvp(): Pair<String,String?> {
val idx = indexOfFirst { it == '=' }
/**
* Splits a string on the first occurrence of a separator and returns a pair of key -> value?
*/
fun String.kvp(separator: Char = '='): Pair<String,String?> {
val idx = indexOfFirst { it == separator }
if(idx > 1) {
val last = idx+1
return Pair(substring(0, idx), if(last==length) null else substring(last, length))
Expand Down
Loading

0 comments on commit d542dca

Please sign in to comment.