Functional style main function argument parsing for Kotlin

Viewed 3177

Please let me know if this way I write a Q&A is inappropriate. Also, I am expecting some better answer, too. The both solutions I provided are not perfect.

There are some Kotlin argument parser on the Internet now, for example GitHub: xenomachina/kotlin-argparser, GitHub: Kotlin/kotlinx.cli or GitHub: ajalt/clikt. However I don't want to add such a huge folder into my (maybe) small project. What I want is a simple and clean solution, for example just a function, with a "fluent" stream-style implementation. Instead, those projects are all containing several files.

What I am thinking is, just need to resolve the command line parameter into a Map<String, List<String>>, use map.containsKey() to get no_argument parameter, and use map[key] to get required_argument parameter.

For example, a command line parameter list

-a -b c -d e f g -h --ignore --join k --link m n o -p "q r s"

will be parsed as:

{-a=[], -b=[c], -d=[e, f, g], -h=[], --ignore=[], --join=[k], --link=[m, n, o], -p=[q r s]}

or we say

mapOf(
    "-a" to listOf(), // POSIX style, no argument
    "-b" to listOf("c"), // POSIX style, with single argument
    "-d" to listOf("e", "f", "g"), // POSIX style, with multiple argument
    "-h" to listOf(), // POSIX style, no argument
    "--ignore" to listOf(), // GNU style, no argument
    "--join" to listOf("k"), // GNU style, with single argument
    "--link" to listOf("m", "n", "o"), // GNU style, with multiple argument
    "-p" to listOf("q r s") // POSIX style, with single argument containing whitespaces
)
3 Answers

Well, my solution involves immutability and folding with last parameter as well.

fun main(args: Array<String>) {
    val map = args.fold(Pair(emptyMap<String, List<String>>(), "")) { (map, lastKey), elem ->
        if (elem.startsWith("-"))  Pair(map + (elem to emptyList()), elem)
        else Pair(map + (lastKey to map.getOrDefault(lastKey, emptyList()) + elem), lastKey)
    }.first

    println(map)

    val expected = mapOf(
        "-a" to emptyList(),
        "-b" to listOf("c"),
        "-d" to listOf("e", "f", "g"),
        "-h" to emptyList(),
        "--ignore" to emptyList(),
        "--join" to listOf("k"),
        "--link" to listOf("m", "n", "o"),
        "-p" to listOf("q r s"))

    check(map == expected)
}

Output

{-a=[], -b=[c], -d=[e, f, g], -h=[], --ignore=[], --join=[k], --link=[m, n, o], -p=[q r s]}

It also handles the case where the first arguments are parameters, and you can access them in map[""]

Here is my implementation.

fun getopt(args: Array<String>): Map<String, List<String>> = args.fold(mutableListOf()) {
    acc: MutableList<MutableList<String>>, s: String ->
    acc.apply {
        if (s.startsWith('-')) add(mutableListOf(s))
        else last().add(s)
    }
}.associate { it[0] to it.drop(1) }

Use fold to group parameters with their corresponding arguments (that is, convert [-p0 arg0 arg1 -p1 arg2] into [[-p0, arg0, arg1], [-p1, arg2]]), then associate into a Map. This function is streaming, but needs 2 pass of data. Also, if there is some leading arguments without previous parameter, it will cause an exception.

Here is another implementation:

fun getopt(args: Array<String>): Map<String, List<String>>
{
    var last = ""
    return args.fold(mutableMapOf()) {
        acc: MutableMap<String, MutableList<String>>, s: String ->
        acc.apply {
            if (s.startsWith('-'))
            {
                this[s] = mutableListOf()
                last = s
            }
            else this[last]?.add(s)
        }
    }
}

Directly construct the map structure, but a reference for the last parameter should be kept for adding next arguments. This function is not so streaming, but just need 1 pass of data. And it just simply discard the leading arguments without a previous parameter.

Related