view src/main/kotlin/name/blackcap/passman/Arguments.kt @ 28:287eadf5ab30 default tip

Check for timeouts inside subcommands while in interactive mode as well.
author David Barts <n5jrn@me.com>
date Wed, 31 Jul 2024 11:21:18 -0700
parents 3a3067ba673b
children
line wrap: on
line source

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

@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) {
            throw SubcommandException(message = e.message ?: "syntax error", status = 2)
        }
        if (commandLine.hasOption("help")) {
            HelpFormatter().printHelp("$SHORTNAME $name [options] csv_file", options)
            throw SubcommandException(status = 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 -> throw SubcommandException(message = "--$name value must not be empty")
            1 -> return optionValue[0]
            else -> throw SubcommandException(message = "--$name value must be a single character")
        }
    }
}

fun parseInto(name: String, args: Array<String>, into: Any): Array<String> =
    AnnotationArgumentParser(name, args, into).parse()