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