comparison src/main/kotlin/name/blackcap/passman/ImportSubcommand.kt @ 16:7a74ae668665

Add export subcommand.
author David Barts <n5jrn@me.com>
date Sun, 05 Feb 2023 10:50:39 -0800
parents 4dae7a15ee48
children ea65ab890f66
comparison
equal deleted inserted replaced
15:0fc90892a3ae 16:7a74ae668665
1 package name.blackcap.passman 1 package name.blackcap.passman
2 2
3 import com.opencsv.CSVParserBuilder 3 import com.opencsv.CSVParserBuilder
4 import com.opencsv.CSVReaderBuilder 4 import com.opencsv.CSVReaderBuilder
5 import com.opencsv.exceptions.CsvException 5 import com.opencsv.exceptions.CsvException
6 import org.apache.commons.cli.* 6 import org.apache.commons.cli.ParseException
7 import java.io.FileReader 7 import java.io.FileInputStream
8 import java.io.IOException 8 import java.io.IOException
9 import java.io.InputStreamReader
9 import java.text.SimpleDateFormat 10 import java.text.SimpleDateFormat
10 import java.util.* 11 import java.util.*
11 import kotlin.system.exitProcess
12 12
13 class ImportSubcommand(): Subcommand() { 13 class ImportSubcommand(): Subcommand() {
14 private companion object { 14 private companion object {
15 val CSV_DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").apply {
16 timeZone = TimeZone.getTimeZone("UTC")
17 isLenient = false
18 }
19 const val ESCAPE = "escape"
20 const val FORCE = "force"
21 const val HELP = "help"
22 const val IGNORE = "ignore"
23 const val QUOTE = "quote"
24 const val SEPARATOR = "separator"
25 const val SKIP = "skip"
26 const val NFIELDS = 7 15 const val NFIELDS = 7
27
28 } 16 }
29 private lateinit var commandLine: CommandLine 17 private lateinit var csvDateFormat: SimpleDateFormat
30 private lateinit var db: Database 18 private lateinit var db: Database
31 19 private val options = ImportExportArguments()
32 /* default option values */ 20 private lateinit var csvFile: String
33 private var escape = '\\'
34 private var quote = '"'
35 private var separator = ','
36
37 private var line = 0 21 private var line = 0
38 22
39 override fun run(args: Array<String>) { 23 override fun run(args: Array<String>) {
40 parseArguments(args) 24 parseArguments(args)
41 db = Database.open() 25 db = Database.open()
48 die("line $line, $message") 32 die("line $line, $message")
49 } 33 }
50 } 34 }
51 35
52 private fun parseArguments(args: Array<String>) { 36 private fun parseArguments(args: Array<String>) {
53 val options = Options().apply { 37 val params = parseInto("import", args, options)
54 addOption("e", ImportSubcommand.ESCAPE, true, "CSV escape character (default $escape).") 38 when (params.size) {
55 addOption("f", ImportSubcommand.FORCE, false, "Do not ask before overwriting.") 39 0 -> die("expecting CSV file name", 2)
56 addOption("h", ImportSubcommand.HELP, false, "Print this help message.") 40 1 -> csvFile = params[0]
57 addOption("i", ImportSubcommand.IGNORE, false, "Ignore white space before quoted strings.") 41 else -> die("unexpected trailing arguments", 2)
58 addOption("q", ImportSubcommand.QUOTE, true, "CSV string-quoting character (default $quote).")
59 addOption("s", ImportSubcommand.SEPARATOR, true, "CSV separator character (default $separator).")
60 addOption("k", ImportSubcommand.SKIP, false, "Skip first line of input.")
61 } 42 }
62 try { 43 csvDateFormat = SimpleDateFormat(options.format).apply {
63 commandLine = DefaultParser().parse(options, args) 44 timeZone = TimeZone.getTimeZone(options.zone)
64 } catch (e: ParseException) { 45 isLenient = false
65 die(e.message ?: "syntax error", 2)
66 } 46 }
67 if (commandLine.hasOption(ImportSubcommand.HELP)) {
68 HelpFormatter().printHelp("$SHORTNAME import [options] csv_file", options)
69 exitProcess(0)
70 }
71 if (commandLine.args.isEmpty()) {
72 die("expecting other CSV file name", 2)
73 }
74 if (commandLine.args.size > 1) {
75 die("unexpected trailing arguments", 2)
76 }
77 escape = getOptionChar(ImportSubcommand.ESCAPE, escape)
78 quote = getOptionChar(ImportSubcommand.QUOTE, quote)
79 separator = getOptionChar(ImportSubcommand.SEPARATOR, separator)
80 }
81
82 private fun getOptionChar(optionName: String, defaultValue: Char): Char {
83 val optionValue = commandLine.getOptionValue(optionName) ?: return defaultValue
84 val ret = optionValue.firstOrNull()
85 if (ret == null) {
86 die("--$optionName value must not be empty")
87 }
88 return ret!!
89 } 47 }
90 48
91 private fun doImport() { 49 private fun doImport() {
92 val csvParser = CSVParserBuilder() 50 val csvParser = CSVParserBuilder()
93 .withEscapeChar(escape) 51 .withEscapeChar(options.escape)
94 .withQuoteChar(quote) 52 .withQuoteChar(options.quote)
95 .withSeparator(separator) 53 .withSeparator(options.separator)
96 .withIgnoreLeadingWhiteSpace(commandLine.hasOption(ImportSubcommand.IGNORE)) 54 .withIgnoreLeadingWhiteSpace(options.ignore)
97 .build() 55 .build()
98 56
99 val csvReader = CSVReaderBuilder(FileReader(commandLine.args[0])) 57 val csvReader = CSVReaderBuilder(InputStreamReader(FileInputStream(csvFile), options.charset))
100 .withCSVParser(csvParser) 58 .withCSVParser(csvParser)
101 .build() 59 .build()
102 60
103 csvReader.use { 61 csvReader.use {
104 if (commandLine.hasOption(ImportSubcommand.SKIP)) { 62 if (options.skip) {
105 line++ 63 line++
106 it.skip(1) 64 it.skip(1)
107 } 65 }
108 66
109 it.iterator().forEach { fields -> 67 it.iterator().forEach { fields ->
143 private fun parseCsvTime(unparsed: String): Date? { 101 private fun parseCsvTime(unparsed: String): Date? {
144 if (saysNull(unparsed)) { 102 if (saysNull(unparsed)) {
145 return null 103 return null
146 } 104 }
147 try { 105 try {
148 return CSV_DATE_FORMAT.parse(unparsed) 106 return csvDateFormat.parse(unparsed)
149 } catch (e: ParseException) { 107 } catch (e: ParseException) {
150 die("${see(unparsed)} - invalid date/time string") 108 die("${see(unparsed)} - invalid date/time string")
151 throw e /* kotlin is too stupid to realize this never happens */ 109 throw e /* kotlin is too stupid to realize this never happens */
152 } 110 }
153 } 111 }
154 112
155 private fun okToChange(thisEntry: Entry?, otherEntry: Entry): Boolean = 113 private fun okToChange(thisEntry: Entry?, otherEntry: Entry): Boolean =
156 thisEntry == null || commandLine.hasOption(FORCE) || askUserIfOkToOverwrite(thisEntry, otherEntry) 114 thisEntry == null || options.force || askUserIfOkToOverwrite(thisEntry, otherEntry)
157 115
158 private fun saysNull(string: String) = string.lowercase() == "null" 116 private fun saysNull(string: String) = string.lowercase() == "null"
159 117
160 } 118 }