view src/main/kotlin/name/blackcap/passman/Shplitter.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 ea65ab890f66
children
line wrap: on
line source

package name.blackcap.passman

// A state is represented by a state-executing function (see below).
typealias State = (Char) -> Boolean

// Support for simplified *nix shell style splitting into tokens. We are
// focused on splitting things into tokens only. No variable expansion,
// no ~home expansion, etc. Backslash quotes the next character. Single
// quotes quote everything literally up to the next closing single quote.
// Double quotes quote to the closing double quote but honor backslashes
// (i.e. "\"\nice\"" -> "nice"). No \t, \n, \r etc. backslash escapes
// supported (KISS).
class Shplitter() {
    private val QUOTING = setOf<State>(::inSingle, ::inDouble)
    private val WHITESPACE = setOf<Char>(' ', '\t', '\n')
    private var oldStates = mutableListOf<State>()
    private var state: State = ::space
    private var accum = mutableListOf<String>()
    private var current = StringBuilder()

    val complete: Boolean
        get() = state == ::space || state == ::nonspace

    // Feeds more input into this tokenizer
    fun feed(input: String) : Unit {
        for (ch in input) {
            while (state(ch))
                ;
        }
    }

    fun split(): Iterable<String> {
        if (complete) {
            if (state == ::nonspace) {
                popState()
                accum.add(current.toString())
                current.clear()
            }
            return accum
        } else {
            throw IllegalStateException("incomplete quoted expression")
        }
    }

    // State transitions

    private fun pushState(newState: State): Unit {
        oldStates.add(state)
        state = newState
    }

    private fun popState(): Unit {
        state = oldStates.removeLast()
    }

    private fun lastState(): State = oldStates.last()

    // States. A state is represented by a function that accepts the
    // character currently being processed, and returns whether it should
    // immediately transition to the next state without reading a new
    // character.

    private fun space(ch: Char): Boolean =
        when (ch) {
            in WHITESPACE -> { false }
            '\'' -> { pushState(::nonspace); pushState(::inSingle); false }
            '"' -> { pushState(::nonspace); pushState(::inDouble); false }
            '\\' -> { pushState(::nonspace); pushState(::backslash); false }
            else -> { pushState(::nonspace); true }
        }

    private fun nonspace(ch: Char): Boolean  =
        when (ch) {
            in WHITESPACE -> {
                accum.add(current.toString())
                current.clear()
                popState()
                false
            }
            '\'' -> {
                pushState(::inSingle)
                false
            }
            '"' -> {
                pushState(::inDouble)
                false
            }
            '\\' -> {
                pushState(::backslash)
                false
            }
            else -> {
                current.append(ch)
                false
            }
        }

    private fun inSingle(ch: Char): Boolean =
        when (ch) {
            '\'' -> { popState(); false }
            else -> { current.append(ch); false }
        }

    private fun inDouble(ch: Char): Boolean =
        when (ch) {
            '\\' -> { pushState(::backslash); false }
            '"' -> { popState(); false }
            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 {
        // continue existing string no matter what
        current.append(ch)
        popState()
        return false
    }
}