# HG changeset patch # User David Barts # Date 1719944859 25200 # Node ID ea65ab890f66bdebe37abc79a99ad9b76b1a3e4a # Parent 4391afcf6bd0c933d86900fb28a30e44bd8dacd7 More work to support interactive feature. diff -r 4391afcf6bd0 -r ea65ab890f66 src/main/kotlin/name/blackcap/passman/Arguments.kt --- a/src/main/kotlin/name/blackcap/passman/Arguments.kt Sun Jun 30 22:28:52 2024 -0700 +++ b/src/main/kotlin/name/blackcap/passman/Arguments.kt Tue Jul 02 11:27:39 2024 -0700 @@ -59,12 +59,11 @@ commandLine = try { DefaultParser().parse(options, args) } catch (e: ParseException) { - die(e.message ?: "syntax error", 2) - throw RuntimeException("this will never happen") + throw SubcommandException(message = e.message ?: "syntax error", status = 2) } if (commandLine.hasOption("help")) { HelpFormatter().printHelp("$SHORTNAME $name [options] csv_file", options) - exitProcess(0) + throw SubcommandException(status = 0) } } @@ -92,11 +91,10 @@ private fun CommandLine.getCharOptionValue(name: String): Char { val optionValue = getOptionValue(name) when (optionValue.length) { - 0 -> die("--$name value must not be empty") + 0 -> throw SubcommandException(message = "--$name value must not be empty") 1 -> return optionValue[0] - else -> die("--$name value must be a single character") + else -> throw SubcommandException(message = "--$name value must be a single character") } - throw RuntimeException("this will never happen") } } diff -r 4391afcf6bd0 -r ea65ab890f66 src/main/kotlin/name/blackcap/passman/Console.kt --- a/src/main/kotlin/name/blackcap/passman/Console.kt Sun Jun 30 22:28:52 2024 -0700 +++ b/src/main/kotlin/name/blackcap/passman/Console.kt Tue Jul 02 11:27:39 2024 -0700 @@ -33,7 +33,7 @@ private fun must(getter: () -> T, checker: (T) -> Boolean): T { while (true) { - var got = getter() + val got = getter() if (checker(got)) { return got } @@ -42,9 +42,8 @@ } private fun doConsoleIo(getter: () -> T?, message: String): T { - val ret = getter() - if (ret == null) { - die(message) - } - return ret!! + val ret = getter() ?: throw ConsoleException(message) + return ret } + +class ConsoleException(message: String, cause: Throwable? = null) : MessagedException(message, cause) diff -r 4391afcf6bd0 -r ea65ab890f66 src/main/kotlin/name/blackcap/passman/CreateSubcommand.kt --- a/src/main/kotlin/name/blackcap/passman/CreateSubcommand.kt Sun Jun 30 22:28:52 2024 -0700 +++ b/src/main/kotlin/name/blackcap/passman/CreateSubcommand.kt Tue Jul 02 11:27:39 2024 -0700 @@ -1,7 +1,6 @@ package name.blackcap.passman import org.apache.commons.cli.* -import kotlin.system.exitProcess class CreateSubcommand(): Subcommand() { private companion object { @@ -13,7 +12,7 @@ } private lateinit var commandLine: CommandLine - override fun run(args: Array) { + override fun run(args: Array): { val options = Options().apply { addOption("g", GENERATE, false, "Use password generator.") addOption("h", HELP, false, "Print this help message.") @@ -24,14 +23,14 @@ try { commandLine = DefaultParser().parse(options, args) } catch (e: ParseException) { - die(e.message ?: "syntax error", 2) + throw SubcommandException(message = e.message ?: "syntax error", status = 2, cause = e) } if (commandLine.hasOption(HELP)) { HelpFormatter().printHelp("$SHORTNAME create [options]", options) - exitProcess(0) + return } checkArguments() - val db = Database.open() + val db = Database.default val entry = if (commandLine.hasOption(GENERATE)) { val rawLength = commandLine.getOptionValue(LENGTH) @@ -41,7 +40,7 @@ -1 } if (length < MIN_GENERATED_LENGTH) { - die("${see(rawLength)} - invalid length") + throw SubcommandException(message = "${see(rawLength)} - invalid length") } Entry.withGeneratedPassword(length, commandLine.hasOption(SYMBOLS), @@ -57,7 +56,7 @@ result.next() val count = result.getInt(1) if (count > 0) { - die("record matching ${see(entry.name)} already exists") + throw SubcommandException(message = "record matching ${see(entry.name)} already exists") } } @@ -100,10 +99,10 @@ } } if (bad) { - exitProcess(2); + throw SubcommandException(status = 2) } if (commandLine.args.isNotEmpty()) { - die("unexpected trailing arguments", 2) + throw SubcommandException(message = "unexpected trailing arguments", status = 2) } } } diff -r 4391afcf6bd0 -r ea65ab890f66 src/main/kotlin/name/blackcap/passman/Database.kt --- a/src/main/kotlin/name/blackcap/passman/Database.kt Sun Jun 30 22:28:52 2024 -0700 +++ b/src/main/kotlin/name/blackcap/passman/Database.kt Tue Jul 02 11:27:39 2024 -0700 @@ -12,6 +12,7 @@ private const val PLAINTEXT = "This is a test." private const val SALT_LENGTH = 16 private const val DEFAULT_PROMPT = "Decryption key: " + lateinit var default: Database fun open(passwordPrompt: String = DEFAULT_PROMPT, fileName: String = DB_FILE, create: Boolean = true): Database { @@ -20,10 +21,14 @@ if (create) { error("initializing database ${see(fileName)}") } else { - die("${see(fileName)} not found") + throw DatabaseException("${see(fileName)} not found") } } - val masterPassword = getPassword(passwordPrompt, !exists) + val masterPassword = try { + getPassword(passwordPrompt, !exists) + } catch (e: ConsoleException) { + throw DatabaseException(e.message, cause = e) + } val conn = DriverManager.getConnection("jdbc:sqlite:$fileName") val enc = if (exists) { reuse(conn, masterPassword) } else { init(conn, masterPassword) } val ret = Database(conn, enc) @@ -37,15 +42,14 @@ it.setString(1, "salt") val result = it.executeQuery() if (!result.next()) { - die("corrupt database, missing salt parameter") + throw DatabaseException("corrupt database, missing salt parameter") } val salt = result.getBytes(1) return Encryption(masterPassword, salt) } } catch (e: SQLException) { e.printStackTrace() - die("unable to reopen database") - throw RuntimeException("this will never happen") + throw DatabaseException("unable to reopen database", e) } } @@ -83,8 +87,7 @@ return encryption } catch (e: SQLException) { e.printStackTrace() - die("unable to initialize database") - throw RuntimeException("this will never happen") + throw DatabaseException("unable to initialize database", e) } } @@ -94,7 +97,7 @@ stmt.setString(1, "test") val result = stmt.executeQuery() if (!result.next()) { - die("corrupt database, missing test parameter") + throw DatabaseException("corrupt database, missing test parameter") } val readFromDb = result.getDecryptedString(1, database.encryption) if (readFromDb != PLAINTEXT) { @@ -104,9 +107,9 @@ } } catch (e: SQLException) { e.printStackTrace() - die("unable to verify decryption key") + throw DatabaseException("unable to verify decryption key", e) } catch (e: GeneralSecurityException) { - die("invalid decryption key") + throw DatabaseException("invalid decryption key", e) } } } @@ -114,6 +117,8 @@ fun makeKey(name: String): Long = Hashing.hash(encryption.encryptFromString0(name.lowercase())) } +class DatabaseException(message: String, cause: Throwable? = null) : MessagedException(message, cause) + fun ResultSet.getDecryptedString(columnIndex: Int, encryption: Encryption): String? { return encryption.decryptToString(getBytes(columnIndex) ?: return null) } diff -r 4391afcf6bd0 -r ea65ab890f66 src/main/kotlin/name/blackcap/passman/DeleteSubcommand.kt --- a/src/main/kotlin/name/blackcap/passman/DeleteSubcommand.kt Sun Jun 30 22:28:52 2024 -0700 +++ b/src/main/kotlin/name/blackcap/passman/DeleteSubcommand.kt Tue Jul 02 11:27:39 2024 -0700 @@ -1,17 +1,15 @@ package name.blackcap.passman -import kotlin.system.exitProcess - class DeleteSubcommand(): Subcommand() { - override fun run(args: Array) { + override fun run(args: Array): { if (args.isEmpty()) { - die("expecting a site name", 2) + throw SubcommandException(message = "expecting a site name", status = 2) } if (args[0] == "-h" || args[0].startsWith("--h")) { println("usage: passman delete name [...]") - exitProcess(0) + return 0 } - val db = Database.open() + val db = Database.default var errors = 0 for (nameIn in args) { db.connection.prepareStatement("delete from passwords where id = ?").use { @@ -22,6 +20,8 @@ } } } - exitProcess(if (errors > 0) 1 else 0) + if (errors > 0) { + throw SubcommandException() + } } } diff -r 4391afcf6bd0 -r ea65ab890f66 src/main/kotlin/name/blackcap/passman/ExportSubcommand.kt --- a/src/main/kotlin/name/blackcap/passman/ExportSubcommand.kt Sun Jun 30 22:28:52 2024 -0700 +++ b/src/main/kotlin/name/blackcap/passman/ExportSubcommand.kt Tue Jul 02 11:27:39 2024 -0700 @@ -18,22 +18,22 @@ private val options = ImportExportArguments() private lateinit var csvFile: String - override fun run(args: Array) { + override fun run(args: Array): { parseArguments(args) - db = Database.open() + db = Database.default try { doExport() } catch (e: IOException) { - die(e.message ?: "I/O error") + throw SubcommandException(message = e.message ?: "I/O error", cause = e) } } private fun parseArguments(args: Array) { val params = parseInto("export", args, options) when (params.size) { - 0 -> die("expecting CSV file name", 2) + 0 -> throw SubcommandException(message = "expecting CSV file name", status = 2) 1 -> csvFile = params[0] - else -> die("unexpected trailing arguments", 2) + else -> throw SubcommandException(message = "unexpected trailing arguments", status = 2) } csvDateFormat = SimpleDateFormat(options.format).apply { timeZone = TimeZone.getTimeZone(options.zone) @@ -74,4 +74,4 @@ val value = getDate(columnIndex) return if (value == null) NULL else csvDateFormat.format(value) } -} \ No newline at end of file +} diff -r 4391afcf6bd0 -r ea65ab890f66 src/main/kotlin/name/blackcap/passman/ImportSubcommand.kt --- a/src/main/kotlin/name/blackcap/passman/ImportSubcommand.kt Sun Jun 30 22:28:52 2024 -0700 +++ b/src/main/kotlin/name/blackcap/passman/ImportSubcommand.kt Tue Jul 02 11:27:39 2024 -0700 @@ -22,23 +22,23 @@ override fun run(args: Array) { parseArguments(args) - db = Database.open() + db = Database.default try { doImport() } catch (e: IOException) { - die(e.message ?: "I/O error") + throw SubcommandException(message = e.message ?: "I/O error", cause = e) } catch (e: CsvException) { - val message = e.message ?: "CSV error" - die("line $line, $message") + val baseMessage = e.message ?: "CSV error" + throw SubcommandException(message = "line $line, $baseMessage", cause = e) } } private fun parseArguments(args: Array) { val params = parseInto("import", args, options) when (params.size) { - 0 -> die("expecting CSV file name", 2) + 0 -> throw SubcommandException(message = "expecting CSV file name", status = 2) 1 -> csvFile = params[0] - else -> die("unexpected trailing arguments", 2) + else -> throw SubcommandException(message = "unexpected trailing arguments", status = 2) } csvDateFormat = SimpleDateFormat(options.format).apply { timeZone = TimeZone.getTimeZone(options.zone) @@ -85,7 +85,7 @@ private fun fromCsv(fields: Array): Entry { if (fields.size != NFIELDS) { - die("line $line, expected $NFIELDS fields but got ${fields.size}") + throw SubcommandException(message = "line $line, expected $NFIELDS fields but got ${fields.size}") } return Entry( name = fields[0], @@ -105,8 +105,7 @@ try { return csvDateFormat.parse(unparsed) } catch (e: ParseException) { - die("${see(unparsed)} - invalid date/time string") - throw e /* kotlin is too stupid to realize this never happens */ + throw SubcommandException(message = "${see(unparsed)} - invalid date/time string", cause = e) } } diff -r 4391afcf6bd0 -r ea65ab890f66 src/main/kotlin/name/blackcap/passman/ListSubcommand.kt --- a/src/main/kotlin/name/blackcap/passman/ListSubcommand.kt Sun Jun 30 22:28:52 2024 -0700 +++ b/src/main/kotlin/name/blackcap/passman/ListSubcommand.kt Tue Jul 02 11:27:39 2024 -0700 @@ -2,7 +2,6 @@ import org.apache.commons.cli.* import java.util.regex.PatternSyntaxException -import kotlin.system.exitProcess class ListSubcommand(): Subcommand() { private companion object { @@ -57,14 +56,14 @@ try { commandLine = DefaultParser().parse(options, args) } catch (e: ParseException) { - die(e.message ?: "syntax error", 2) + throw SubcommandException(message = e.message ?: "syntax error", cause = e, status = 2) } if (commandLine.hasOption(HELP)) { HelpFormatter().printHelp("$SHORTNAME list [options]", options) - exitProcess(0) + return } if (commandLine.args.isNotEmpty()) { - die("unexpected trailing arguments", 2) + throw SubcommandException(message = "unexpected trailing arguments", status = 2) } STRING_OPTIONS.forEach { @@ -82,7 +81,7 @@ } matchers[it.name]!! += { x -> x is String && x.contains(Regex(value, regexOptions)) } } catch (e: PatternSyntaxException) { - die("${see(value)} - invalid regular expression") + throw SubcommandException(message = "${see(value)} - invalid regular expression", cause = e, status = 2) } } } @@ -90,7 +89,7 @@ TIME_OPTIONS.forEach { commandLine.getOptionValues(it.name)?.forEach { rawValue -> if (rawValue.isEmpty()) { - die("empty string is not a valid time expression") + throw SubcommandException(message = "empty string is not a valid time expression") } val (op, exp) = when(rawValue.first()) { '+', '>' -> Pair('>', rawValue.substring(1)) @@ -100,8 +99,7 @@ } val value = parseDateTime(exp) if (value == null) { - die("${see(rawValue)} - invalid time expression") - throw RuntimeException("will never happen") + throw SubcommandException(message = "${see(rawValue)} - invalid time expression") } if (it.name !in matchers) { matchers[it.name] = mutableListOf<(Any?) -> Boolean>() @@ -117,7 +115,7 @@ } private fun runQuery(): Unit { - val db = Database.open() + val db = Database.default db.connection.prepareStatement("select $NAME, $USERNAME, $NOTES, $CREATED, $MODIFIED, $ACCESSED from passwords").use { val results = it.executeQuery() diff -r 4391afcf6bd0 -r ea65ab890f66 src/main/kotlin/name/blackcap/passman/Main.kt --- a/src/main/kotlin/name/blackcap/passman/Main.kt Sun Jun 30 22:28:52 2024 -0700 +++ b/src/main/kotlin/name/blackcap/passman/Main.kt Tue Jul 02 11:27:39 2024 -0700 @@ -15,6 +15,7 @@ } val subcommand = args[0]; val scArgs = args.sliceArray(1 until args.size) + Database.default = Database.open() runSubcommand(subcommand, scArgs) } diff -r 4391afcf6bd0 -r ea65ab890f66 src/main/kotlin/name/blackcap/passman/MergeSubcommand.kt --- a/src/main/kotlin/name/blackcap/passman/MergeSubcommand.kt Sun Jun 30 22:28:52 2024 -0700 +++ b/src/main/kotlin/name/blackcap/passman/MergeSubcommand.kt Tue Jul 02 11:27:39 2024 -0700 @@ -2,7 +2,6 @@ import org.apache.commons.cli.* import java.sql.ResultSet -import kotlin.system.exitProcess class MergeSubcommand(): Subcommand() { private companion object { @@ -15,7 +14,10 @@ override fun run(args: Array) { parseArguments(args) - db = Database.open() + if (commandLine.hasOption(MergeSubcommand.HELP)) { + return + } + db = Database.default doMerge() } @@ -28,17 +30,17 @@ try { commandLine = DefaultParser().parse(options, args) } catch (e: ParseException) { - die(e.message ?: "syntax error", 2) + throw SubcommandException(message = e.message ?: "syntax error", status = 2, cause = e) } if (commandLine.hasOption(MergeSubcommand.HELP)) { HelpFormatter().printHelp("$SHORTNAME merge [options] other_database", options) - exitProcess(0) + return } if (commandLine.args.isEmpty()) { - die("expecting other database name", 2) + throw SubcommandException(message = "expecting other database name", status = 2) } if (commandLine.args.size > 1) { - die("unexpected trailing arguments", 2) + throw SubcommandException(message = "unexpected trailing arguments", status = 2) } } diff -r 4391afcf6bd0 -r ea65ab890f66 src/main/kotlin/name/blackcap/passman/MessagedException.kt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/kotlin/name/blackcap/passman/MessagedException.kt Tue Jul 02 11:27:39 2024 -0700 @@ -0,0 +1,6 @@ +package name.blackcap.passman + +// Exception that always has a non-null message. +open class MessagedException(_message: String, _cause: Throwable? = null) : Exception(_message, _cause) { + override val message = _message +} diff -r 4391afcf6bd0 -r ea65ab890f66 src/main/kotlin/name/blackcap/passman/PasswordSubcommand.kt --- a/src/main/kotlin/name/blackcap/passman/PasswordSubcommand.kt Sun Jun 30 22:28:52 2024 -0700 +++ b/src/main/kotlin/name/blackcap/passman/PasswordSubcommand.kt Tue Jul 02 11:27:39 2024 -0700 @@ -3,23 +3,22 @@ import java.nio.file.Files import java.nio.file.Path import java.nio.file.StandardCopyOption -import kotlin.system.exitProcess class PasswordSubcommand : Subcommand() { override fun run(args: Array) { // Parse arguments if (args.size > 0 && (args[0] == "-h" || args[0].startsWith("--h"))) { println("usage: passman password") - exitProcess(0) + return } if (!args.isEmpty()) { - die("unexpected arguments", 2) + throw SubcommandException(message = "unexpected arguments", status = 2) } // Open databases println("Changing database encryption key...") val oldPath = Path.of(DB_FILE) - val oldDb = Database.open(fileName = DB_FILE, passwordPrompt = "Old database key: ") + val oldDb = Database.default val newPath = Path.of(NEW_DB_FILE) if (Files.exists(newPath)) { println("WARNING: deleting ${see(NEW_DB_FILE)}") @@ -31,7 +30,9 @@ println("WARNING: do not interrupt this process or data may be lost!") copyRecords(oldDb, newDb) - // Wrap up + // Wrap up. XXX - this closes Database.default, so this subcommand may + // only be invoked directly from the shell prompt, never in interactive + // mode. oldDb.connection.close() newDb.connection.close() Files.move(newPath, oldPath, StandardCopyOption.REPLACE_EXISTING) diff -r 4391afcf6bd0 -r ea65ab890f66 src/main/kotlin/name/blackcap/passman/ReadSubcommand.kt --- a/src/main/kotlin/name/blackcap/passman/ReadSubcommand.kt Sun Jun 30 22:28:52 2024 -0700 +++ b/src/main/kotlin/name/blackcap/passman/ReadSubcommand.kt Tue Jul 02 11:27:39 2024 -0700 @@ -1,7 +1,6 @@ package name.blackcap.passman import org.apache.commons.cli.* -import kotlin.system.exitProcess class ReadSubcommand(): Subcommand() { private companion object { @@ -23,24 +22,23 @@ try { commandLine = DefaultParser().parse(options, args) } catch (e: ParseException) { - die(e.message ?: "syntax error", 2) + throw SubcommandException(message = e.message ?: "syntax error", status = 2, cause = e) } if (commandLine.hasOption(HELP)) { HelpFormatter().printHelp("$SHORTNAME read [options] name", options) - exitProcess(0) + return } if (commandLine.args.isEmpty()) { - die("expecting site name", 2) + throw SubcommandException(message = "expecting site name", status = 2) } if (commandLine.args.size > 1) { - die("unexpected trailing arguments", 2) + throw SubcommandException(message = "unexpected trailing arguments", status = 2) } val nameIn = commandLine.args[0]; - val db = Database.open() + val db = Database.default val entry = Entry.fromDatabase(db, nameIn) if (entry == null) { - die("no record matches ${see(nameIn)}") - return // Kotlin is too stupid to realize we never get here + throw SubcommandException(message = "no record matches ${see(nameIn)}") } try { print(ALT_SB + CLEAR) diff -r 4391afcf6bd0 -r ea65ab890f66 src/main/kotlin/name/blackcap/passman/RenameSubcommand.kt --- a/src/main/kotlin/name/blackcap/passman/RenameSubcommand.kt Sun Jun 30 22:28:52 2024 -0700 +++ b/src/main/kotlin/name/blackcap/passman/RenameSubcommand.kt Tue Jul 02 11:27:39 2024 -0700 @@ -1,9 +1,6 @@ package name.blackcap.passman import org.apache.commons.cli.* -import java.sql.PreparedStatement -import java.sql.Types -import kotlin.system.exitProcess class RenameSubcommand(): Subcommand() { private companion object { @@ -17,7 +14,10 @@ override fun run(args: Array) { parseArguments(args) - db = Database.open() + if (commandLine.hasOption(HELP)) { + return + } + db = Database.default renameIt() } @@ -29,17 +29,17 @@ try { commandLine = DefaultParser().parse(options, args) } catch (e: ParseException) { - die(e.message ?: "syntax error", 2) + throw SubcommandException(message = e.message ?: "syntax error", status = 2, cause = e) } if (commandLine.hasOption(HELP)) { HelpFormatter().printHelp("$SHORTNAME rename [options] source destination", options) - exitProcess(0) + return } if (commandLine.args.size < 2) { - die("expecting source and destination", 2) + throw SubcommandException(message = "expecting source and destination", status = 2) } if (commandLine.args.size > 2) { - die("unexpected trailing arguments", 2) + throw SubcommandException(message = "unexpected trailing arguments", status = 2) } source = commandLine.args[0] destination = commandLine.args[1] @@ -50,13 +50,13 @@ val did = db.makeKey(destination) if(!recordExists(sid)) { - die("no record matches ${see(source)}") + throw SubcommandException(message = "no record matches ${see(source)}") } if (recordExists(did)) { if (commandLine.hasOption(FORCE)) { deleteRecord(did) } else { - die("record matching ${see(destination)} already exists") + throw SubcommandException(message = "record matching ${see(destination)} already exists") } } diff -r 4391afcf6bd0 -r ea65ab890f66 src/main/kotlin/name/blackcap/passman/Shplitter.kt --- a/src/main/kotlin/name/blackcap/passman/Shplitter.kt Sun Jun 30 22:28:52 2024 -0700 +++ b/src/main/kotlin/name/blackcap/passman/Shplitter.kt Tue Jul 02 11:27:39 2024 -0700 @@ -108,22 +108,13 @@ else -> { current.append(ch); false } } + // XXX - multiline commands achieved with backslash-newline + // sequences not supported, so this only works with single- + // line commands. private fun backslash(ch: Char): Boolean { - val last = lastState() - if (ch == '\n' && last !in QUOTING) { - // if not quoting, \\n makes a normal whitespace out of command terminator - popState() - return true - } else if (last == ::space) { - // start a new unquoted string no matter what - current.append(ch) - state = ::nonspace - return false - } else { - // continue existing string no matter what - current.append(ch) - popState() - return false - } + // continue existing string no matter what + current.append(ch) + popState() + return false } } diff -r 4391afcf6bd0 -r ea65ab890f66 src/main/kotlin/name/blackcap/passman/Subcommand.kt --- a/src/main/kotlin/name/blackcap/passman/Subcommand.kt Sun Jun 30 22:28:52 2024 -0700 +++ b/src/main/kotlin/name/blackcap/passman/Subcommand.kt Tue Jul 02 11:27:39 2024 -0700 @@ -3,3 +3,10 @@ abstract class Subcommand() { abstract fun run(args: Array): Unit } + +// Replaces fatal errors that used to exit the process. +class SubcommandException(message: String? = null, cause: Throwable? = null, status: Int = 1) : Exception(message, cause) { + private val _status = status + val status: Int + get() = _status +} diff -r 4391afcf6bd0 -r ea65ab890f66 src/main/kotlin/name/blackcap/passman/UpdateSubcommand.kt --- a/src/main/kotlin/name/blackcap/passman/UpdateSubcommand.kt Sun Jun 30 22:28:52 2024 -0700 +++ b/src/main/kotlin/name/blackcap/passman/UpdateSubcommand.kt Tue Jul 02 11:27:39 2024 -0700 @@ -4,7 +4,6 @@ import java.sql.Types import java.util.* import kotlin.properties.Delegates -import kotlin.system.exitProcess class UpdateSubcommand(): Subcommand() { private lateinit var commandLine: CommandLine @@ -28,6 +27,9 @@ override fun run(args: Array) { parseArguments(args) + if (commandLine.hasOption(HELP)) { + return + } checkDatabase() try { update() @@ -47,14 +49,14 @@ try { commandLine = DefaultParser().parse(options, args) } catch (e: ParseException) { - die(e.message ?: "syntax error", 2) + throw SubcommandException(message = e.message ?: "syntax error", status = 2, cause = e) } if (commandLine.hasOption(HELP)) { HelpFormatter().printHelp("$SHORTNAME update [options] name", options) - exitProcess(0) + return } checkArguments() - db = Database.open() + db = Database.default nameIn = commandLine.args[0] id = db.makeKey(nameIn) val rawLength = commandLine.getOptionValue(LENGTH) @@ -64,7 +66,7 @@ -1 } if (length < MIN_GENERATED_LENGTH) { - die("${see(rawLength)} - invalid length") + throw SubcommandException(message = "${see(rawLength)} - invalid length") } } @@ -79,13 +81,13 @@ } } if (bad) { - exitProcess(2); + throw SubcommandException(status = 2) } if (commandLine.args.isEmpty()) { - die("expecting site name", 2) + throw SubcommandException(message = "expecting site name", status = 2) } if (commandLine.args.size > 1) { - die("unexpected trailing arguments", 2) + throw SubcommandException(message = "unexpected trailing arguments", status = 2) } } @@ -96,7 +98,7 @@ result.next() val count = result.getInt(1) if (count < 1) { - die("no record matches " + see(nameIn)) + throw SubcommandException(message = "no record matches " + see(nameIn)) } } } @@ -140,9 +142,13 @@ } private fun updateOne(name: String): Unit { - val prompt = name.replaceFirstChar { it.titlecase(Locale.getDefault()) } + ": " + val prompt = name.replaceFirstChar { it.uppercase(Locale.getDefault()) } + ": " val value: Any? = if (name in SENSITIVE_FIELDS) { - updatePassword() + try { + getPassword(prompt) + } catch (e: ConsoleException) { + throw SubcommandException(message = e.message, cause = e) + } } else { val rawValue = readLine(prompt) if (name in NULLABLE_FIELDS && rawValue == NULL_SPECIFIED) { @@ -181,17 +187,4 @@ addOne("password", newPassword) } - private fun updatePassword(): CharArray { - while (true) { - val pw1 = getPassword("Password: ") - if (pw1.isEmpty()) { - return pw1 - } - val pw2 = getPassword("Verification: ") - if (pw1 contentEquals pw2) { - return pw1 - } - error("mismatch, try again") - } - } }