diff src/main/kotlin/name/blackcap/passman/Arguments.kt @ 16:7a74ae668665

Add export subcommand.
author David Barts <n5jrn@me.com>
date Sun, 05 Feb 2023 10:50:39 -0800
parents
children ea65ab890f66
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/kotlin/name/blackcap/passman/Arguments.kt	Sun Feb 05 10:50:39 2023 -0800
@@ -0,0 +1,104 @@
+package name.blackcap.passman
+
+import org.apache.commons.cli.*
+import kotlin.reflect.KCallable
+import kotlin.reflect.KMutableProperty
+import kotlin.reflect.full.findAnnotation
+import kotlin.reflect.full.hasAnnotation
+import kotlin.reflect.full.isSubtypeOf
+import kotlin.reflect.typeOf
+import kotlin.system.exitProcess
+
+@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY)
+annotation class Argument(
+    val shortName: Char = AnnotationArgumentInfo.UNSPEC_SHORT,
+    val longName: String = AnnotationArgumentInfo.UNSPEC_LONG,
+    val description: String);
+
+private class AnnotationArgumentInfo(val property: KMutableProperty<*>) {
+    companion object {
+        const val UNSPEC_SHORT: Char = '\u0000'
+        const val UNSPEC_LONG: String = ""
+    }
+    val type = property.returnType
+    val annotation = property.findAnnotation<Argument>()!!
+    val longName = if (annotation.longName == UNSPEC_LONG) { property.name } else { annotation.longName }
+    val shortName = if (annotation.shortName == UNSPEC_SHORT) { longName.first() } else { annotation.shortName }
+}
+
+private class AnnotationArgumentParser(val name: String, val args: Array<String>, val into: Any) {
+    private companion object {
+        val BOOLEAN_TYPE = typeOf<Boolean>()
+        val STRING_TYPE = typeOf<String>()
+        val CHAR_TYPE = typeOf<Char>()
+    }
+
+    val annotated = into::class.members.filter { it is KMutableProperty<*> && it.hasAnnotation<Argument>() }
+    val options = Options()
+    lateinit var commandLine: CommandLine
+
+    fun parse(): Array<String> {
+        build()
+        doParse()
+        return extract()
+    }
+
+    private fun build() {
+        annotated.iterate { _, info ->
+            when {
+                info.type.isSubtypeOf(BOOLEAN_TYPE) ->
+                    options.addOption(info.shortName.toString(), info.longName, false, info.annotation.description)
+
+                info.type.isSubtypeOf(STRING_TYPE) || info.type.isSubtypeOf(CHAR_TYPE) ->
+                    options.addOption(info.shortName.toString(), info.longName, true, info.annotation.description)
+            }
+        }
+    }
+
+    private fun doParse() {
+        commandLine = try {
+            DefaultParser().parse(options, args)
+        } catch (e: ParseException) {
+            die(e.message ?: "syntax error", 2)
+            throw RuntimeException("this will never happen")
+        }
+        if (commandLine.hasOption("help")) {
+            HelpFormatter().printHelp("$SHORTNAME $name [options] csv_file", options)
+            exitProcess(0)
+        }
+    }
+
+    private fun extract(): Array<String> {
+        annotated.iterate { annotated, info ->
+            if (commandLine.hasOption(info.longName)) {
+                when {
+                    info.type.isSubtypeOf(BOOLEAN_TYPE) ->
+                        annotated.setter.call(true)
+                    info.type.isSubtypeOf(STRING_TYPE) ->
+                        annotated.setter.call(commandLine.getOptionValue(info.longName))
+                    info.type.isSubtypeOf(CHAR_TYPE) ->
+                        annotated.setter.call(commandLine.getCharOptionValue(info.longName))
+                }
+            }
+        }
+        return commandLine.args
+    }
+
+    private fun Collection<KCallable<*>>.iterate(block: (KMutableProperty<*>, AnnotationArgumentInfo) -> Unit) =
+        forEach {
+            block(it as KMutableProperty<*>, AnnotationArgumentInfo(it))
+        }
+
+    private fun CommandLine.getCharOptionValue(name: String): Char {
+        val optionValue = getOptionValue(name)
+        when (optionValue.length) {
+            0 -> die("--$name value must not be empty")
+            1 -> return optionValue[0]
+            else -> die("--$name value must be a single character")
+        }
+        throw RuntimeException("this will never happen")
+    }
+}
+
+fun parseInto(name: String, args: Array<String>, into: Any): Array<String> =
+    AnnotationArgumentParser(name, args, into).parse()