view src/main/kotlin/name/blackcap/passman/Main.kt @ 26:69526ae8c8de

Allow list --name to be abbreviated -n, since it is so commonly used.
author David Barts <n5jrn@me.com>
date Fri, 05 Jul 2024 10:11:31 -0700
parents 2188b2f13326
children 3a3067ba673b
line wrap: on
line source

package name.blackcap.passman

import java.io.BufferedReader
import java.io.InputStreamReader
import java.util.*
import java.util.stream.Collectors
import kotlin.reflect.jvm.javaMethod
import kotlin.reflect.jvm.kotlinFunction
import kotlin.system.exitProcess

fun main(args: Array<String>) {
    val NOPASSWORD = setOf<String>("help", "quit")
    if (args.isEmpty()) {
        openDatabase()
        runInteractive()
    }
    val subcommand = args[0]
    val scArgs = args.sliceArray(1 until args.size)
    if (subcommand !in NOPASSWORD) {
        openDatabase()
    }
    runNonInteractive(subcommand, scArgs)
}

fun openDatabase() {
    try {
        Database.default = Database.open()
    } catch (e: DatabaseException) {
        handleMessagedException(e)
        exitProcess(2)
    }
}

fun runNonInteractive(subcommand: String, scArgs: Array<String>): Unit {
    try {
        runSubcommand(subcommand, scArgs)
    } catch (e: SubcommandException) {
        handleSubcommandException(e)
        exitProcess(e.status)
    } catch (e: MessagedException) {
        handleMessagedException(e)
        exitProcess(1)
    }
    exitProcess(0)
}

fun runInteractive() {
    val DISALLOWED = setOf<String>("password")
    val QUIT = setOf<String>("exit", "quit")
    var lastStatus = 0
    println("This is PassMan interactive mode. Type help for help.")
    while (true) {
        val rawLine = System.console()?.readLine("passman> ")
        if (rawLine == null) {
            println()  // ensure shell prompt comes out on a line of its own
            break
        }
        val s = Shplitter()
        s.feed(rawLine)
        if (!s.complete) {
            error("unterminated quoting")
            lastStatus = 1
            continue
        }
        val line = s.split().toList()
        if (line.isEmpty()) {
            continue
        }
        val subcommand = line.first()
        val scArgs = line.drop(1).toTypedArray()
        if (subcommand in QUIT) {
            break
        }
        if (subcommand in DISALLOWED) {
            error("$subcommand subcommand not allowed in interactive mode")
            lastStatus = 2
            continue
        }
        try {
            runSubcommand(subcommand, scArgs)
            lastStatus = 0
        } catch(e: SubcommandException) {
            handleSubcommandException(e)
            lastStatus = e.status
        } catch(e: MessagedException) {
            handleMessagedException(e)
            lastStatus = 1
        }
    }
    exitProcess(lastStatus)
}

fun runSubcommand(name: String, args: Array<String>): Unit {
    val instance = getInstanceForClass(getClassForSubcommand(name))
    if (instance == null) {
        throw SubcommandException(message = "${see(name)} - unknown subcommand", status = 2)
    } else {
        instance.run(args)
    }
}

fun getClassForSubcommand(name: String): Class<Subcommand>? = try {
    val shortName = name.replace('-', '_')
        .lowercase()
        .replaceFirstChar { it.titlecase(Locale.getDefault()) }
    Class.forName("$MAIN_PACKAGE.${shortName}Subcommand") as? Class<Subcommand>
} catch (e: ClassNotFoundException) {
    null
}

fun getInstanceForClass(klass: Class<Subcommand>?) = try {
    klass?.getDeclaredConstructor()?.newInstance()
} catch (e: ReflectiveOperationException) {
    null
}

fun handleMessagedException(e: MessagedException)  = error(e.message)

fun handleSubcommandException(e: SubcommandException): Unit {
    if (e.message != null) {
        error(e.message!!)
    }
}