annotate src/main/kotlin/name/blackcap/passman/Shplitter.kt @ 19:7d80cbcb67bb

add shlex-style splitter and tests
author David Barts <n5jrn@me.com>
date Sun, 30 Jun 2024 20:37:36 -0700
parents
children 4391afcf6bd0
Ignore whitespace changes - Everywhere: Within whitespace: At end of lines:
rev   line source
19
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
1 package name.blackcap.passman
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
2
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
3 // A state is represented by a state-executing function (see below).
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
4 typealias State = (Char) -> Boolean
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
5
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
6 // Support for simplified *nix shell style splitting into tokens. We are
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
7 // focused on splitting things into tokens only. No variable expansion,
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
8 // no ~home expansion, etc. Backslash quotes the next character. Single
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
9 // quotes quote everything literally up to the next closing single quote.
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
10 // Double quotes quote to the closing double quote but honor backslashes
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
11 // (i.e. "\"\nice\"" -> "nice"). No \t, \n, \r etc. backslash escapes
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
12 // supported (KISS).
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
13 class Shplitter() {
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
14 private val QUOTING = setOf<State>(::inSingle, ::inDouble)
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
15 private val WHITESPACE = setOf<Char>(' ', '\t', '\n')
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
16 private var oldStates = mutableListOf<State>()
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
17 private var state: State = ::space
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
18 private var accum = mutableListOf<String>()
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
19 private var current = StringBuilder()
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
20
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
21 val complete: Boolean
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
22 get() = state == ::space || state == ::nonspace
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
23
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
24 // Feeds more input into this tokenizer
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
25 fun feed(input: String) : Unit {
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
26 for (ch in input) {
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
27 while (state(ch))
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
28 ;
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
29 }
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
30 }
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
31
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
32 fun split(): Iterable<String> {
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
33 if (complete) {
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
34 if (current.isNotEmpty()) {
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
35 accum.add(current.toString())
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
36 current.clear()
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
37 }
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
38 if (state == ::nonspace) {
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
39 popState()
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
40 }
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
41 return accum
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
42 } else {
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
43 throw IllegalStateException("incomplete quoted expression")
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
44 }
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
45 }
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
46
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
47 // State transitions
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
48
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
49 private fun pushState(newState: State): Unit {
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
50 oldStates.add(state)
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
51 state = newState
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
52 }
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
53
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
54 private fun popState(): Unit {
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
55 state = oldStates.removeLast()
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
56 }
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
57
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
58 private fun lastState(): State = oldStates.last()
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
59
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
60 private fun endQuote(): Unit {
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
61 if (lastState() == ::space) {
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
62 accum.add(current.toString())
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
63 current.clear()
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
64 }
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
65 popState()
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
66 }
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
67
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
68 // States. A state is represented by a function that accepts the
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
69 // character currently being processed, and returns whether it should
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
70 // immediately transition to the next state without reading a new
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
71 // character.
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
72
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
73 private fun space(ch: Char): Boolean =
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
74 when (ch) {
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
75 in WHITESPACE -> { false }
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
76 '\'' -> { pushState(::inSingle); false }
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
77 '"' -> { pushState(::inDouble); false }
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
78 '\\' -> { pushState(::backslash); false }
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
79 else -> { pushState(::nonspace); true }
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
80 }
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
81
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
82 private fun nonspace(ch: Char): Boolean =
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
83 when (ch) {
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
84 in WHITESPACE -> {
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
85 accum.add(current.toString())
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
86 current.clear()
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
87 popState()
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
88 false
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
89 }
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
90 '\'' -> {
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
91 pushState(::inSingle)
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
92 false
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
93 }
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
94 '"' -> {
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
95 pushState(::inDouble)
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
96 false
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
97 }
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
98 '\\' -> {
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
99 pushState(::backslash)
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
100 false
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
101 }
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
102 else -> {
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
103 current.append(ch)
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
104 false
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
105 }
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
106 }
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
107
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
108 private fun inSingle(ch: Char): Boolean =
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
109 when (ch) {
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
110 '\'' -> { endQuote(); false }
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
111 else -> { current.append(ch); false }
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
112 }
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
113
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
114 private fun inDouble(ch: Char): Boolean =
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
115 when (ch) {
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
116 '\\' -> { pushState(::backslash); false }
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
117 '"' -> { endQuote(); false }
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
118 else -> { current.append(ch); false }
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
119 }
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
120
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
121 private fun backslash(ch: Char): Boolean {
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
122 val last = lastState()
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
123 if (ch == '\n' && last !in QUOTING) {
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
124 // if not quoting, \\n makes a normal whitespace out of command terminator
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
125 popState()
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
126 return true
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
127 } else if (last == ::space) {
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
128 // start a new unquoted string no matter what
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
129 current.append(ch)
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
130 state = ::nonspace
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
131 return false
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
132 } else {
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
133 // continue existing string no matter what
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
134 current.append(ch)
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
135 popState()
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
136 return false
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
137 }
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
138 }
7d80cbcb67bb add shlex-style splitter and tests
David Barts <n5jrn@me.com>
parents:
diff changeset
139 }