dev.forkhandles.bunting.BuntingFlag.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of bunting4k Show documentation
Show all versions of bunting4k Show documentation
ForkHandles CLI Flag library
package dev.forkhandles.bunting
import dev.forkhandles.bunting.Visibility.Public
import dev.forkhandles.bunting.Visibility.Secret
import java.util.UUID
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
/**
* A value passed on the command line which are normally passed with a `-` (short) or `--` (long) prefix.
*/
sealed class BuntingFlag(val description: String? = null,
internal val visibility: Visibility = Public) : ReadOnlyProperty
/**
* Command flags always appear at the start of a command and are not prefixed with a '-' or '--'.
*/
class Command internal constructor(private val fn: BuntingConstructor) : BuntingFlag() {
override fun getValue(thisRef: Bunting, property: KProperty<*>) =
if (thisRef.args.firstOrNull() == property.name) fn(thisRef.args.drop(1).toTypedArray()) else null
}
/**
* Switch flags are optional but not passed with a value attached. They resolve to a boolean.
*/
class Switch internal constructor(description: String?) : BuntingFlag(description) {
override fun getValue(thisRef: Bunting, property: KProperty<*>): Boolean =
thisRef.args.contains("--${property.name}") || thisRef.args.contains("-${property.name.first()}")
}
/**
* Required flags cause a failure when missing..
*/
class Required internal constructor(internal val fn: (String) -> T,
description: String?,
visibility: Visibility) : BuntingFlag(description, visibility) {
override fun getValue(thisRef: Bunting, property: KProperty<*>): T = thisRef.retrieve(property)?.let {
try {
fn(it)
} catch (e: Exception) {
throw IllegalFlag(property, visibility(it), e)
}
} ?: throw MissingFlag(property)
}
enum class Visibility : (String) -> String {
Public {
override operator fun invoke(value: String): String = value
},
Secret {
override operator fun invoke(value: String): String = "*".repeat(value.length)
};
}
/**
* Optional flags just return null when missing.
*/
class Optional internal constructor(internal val fn: (String) -> T,
description: String?,
visibility: Visibility,
private val config: Config,
private val io: IO) : BuntingFlag(description, visibility) {
fun secret() = Optional(fn, description, Secret, config, io)
fun required() = Required(fn, description, visibility)
fun prompted() = Prompted(fn, description, visibility, io)
fun defaultsTo(default: T) = Defaulted(fn,
(description?.takeIf { it.isNotBlank() }?.let { "$it. " }
?: "") + "Defaults to \"${visibility(default.toString())}\"",
visibility,
{ default })
fun configuredAs(configProperty: String) = Configured(fn,
(description?.takeIf { it.isNotBlank() }?.let { "$it. " }
?: "") + "Configured as \"$configProperty\"",
visibility,
configProperty,
config)
fun described(new: String): Optional = Optional(fn, new, visibility, config, io)
fun map(nextFn: (T) -> NEXT) = Optional({ nextFn(fn(it)) }, description, visibility, config, io)
override fun getValue(thisRef: Bunting, property: KProperty<*>) = thisRef.retrieve(property)?.let {
try {
fn(it)
} catch (e: Exception) {
throw IllegalFlag(property, it, e)
}
}
}
/**
* Defaulted flags fall back to a passed value when missing.
*/
class Defaulted internal constructor(internal val fn: (String) -> T,
description: String?,
visibility: Visibility,
private val default: () -> T) : BuntingFlag(description, visibility) {
override fun getValue(thisRef: Bunting, property: KProperty<*>): T = thisRef.retrieve(property)?.let {
try {
fn(it)
} catch (e: Exception) {
throw IllegalFlag(property, visibility(it), e)
}
} ?: default()
}
/**
* Configured flags are stored in the config.
*/
class Configured internal constructor(internal val fn: (String) -> T,
description: String?,
visibility: Visibility,
private val configProperty: String,
private val config: Config) : BuntingFlag(description, visibility) {
override fun getValue(thisRef: Bunting, property: KProperty<*>): T? =
(thisRef.retrieve(property) ?: config[configProperty])?.let {
try {
fn(it)
} catch (e: Exception) {
throw IllegalFlag(property, visibility(it), e)
}
}
fun defaultsTo(default: String) = ConfiguredDefault(fn,
(description?.takeIf { it.isNotBlank() }?.let { "$it. " }
?: "") + "Defaults to \"${visibility(default)}\"",
visibility,
configProperty,
config,
default)
}
class ConfiguredDefault internal constructor(internal val fn: (String) -> T,
description: String?,
visibility: Visibility,
private val configProperty: String,
private val config: Config,
private val default: String) : BuntingFlag(description, visibility) {
override fun getValue(thisRef: Bunting, property: KProperty<*>): T = (thisRef.retrieve(property) ?: config[configProperty] ?: default.also { config[configProperty] = it }).let {
try {
fn(it)
} catch (e: Exception) {
throw IllegalFlag(property, visibility(it), e)
}
}
}
/**
* Prompted flags cause a prompt to be displayed to the user when missing.
*/
class Prompted internal constructor(internal val fn: (String) -> T,
description: String?,
visibility: Visibility,
private val io: IO
) : BuntingFlag(description, visibility) {
override fun getValue(thisRef: Bunting, property: KProperty<*>): T =
with(thisRef.retrieve(property) ?: promptForValue()) {
try {
fn(this)
} catch (e: Exception) {
throw IllegalFlag(property, visibility(this), e)
}
}
private fun promptForValue() = io.run {
write("Enter value for \"$description\": ")
read(visibility)
}
}
private fun Bunting.retrieve(property: KProperty<*>): String? {
val windowed = args.toList().windowed(2).map { it[0] to it[1] }.toMap()
return windowed["--${property.name}"] ?: windowed["-${property.name.first()}"]
}
fun Optional.int() = map(String::toInt)
fun Optional.float() = map(String::toFloat)
fun Optional.long() = map(String::toLong)
fun Optional.uuid() = map(UUID::fromString)
fun Optional.char() = map(String::first)
fun Optional.boolean() = map {
when {
it.toBoolean() -> it.toBoolean()
it.lowercase() != false.toString() -> throw IllegalArgumentException()
else -> false
}
}
inline fun > Optional.enum() =
described((description?.let { "$it. " } ?: "")
+ "Option choice: " + enumValues().toList()).map { enumValueOf(it) }
© 2015 - 2025 Weber Informatics LLC | Privacy Policy