Skip to content

Commit 8b8d90a

Browse files
authored
Concat adjacent quoted tokens in atfiles (#632)
Fixes #628
1 parent 4321e74 commit 8b8d90a

2 files changed

Lines changed: 26 additions & 4 deletions

File tree

  • clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parsers
  • test/src/commonTest/kotlin/com/github/ajalt/clikt/parsers

clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parsers/atfile.kt

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ internal fun shlex(
1111
): List<String> {
1212
val toks = mutableListOf<String>()
1313
var inQuote: Char? = null
14+
var inToken = false // Track if we're building a token (for empty quoted strings)
1415
val sb = StringBuilder()
1516
var i = 0
1617
fun err(msg: String): Nothing {
@@ -31,14 +32,15 @@ internal fun shlex(
3132
i += 1
3233
} while (i <= text.lastIndex && text[i].isWhitespace())
3334
} else {
35+
inToken = true
3436
sb.append(text[i + 1])
3537
i += 2
3638
}
3739
}
3840

3941
c == inQuote -> {
40-
toks += sb.toString()
41-
sb.clear()
42+
// Don't emit here - just close the quote. Adjacent quoted/unquoted
43+
// strings should concatenate into a single token (POSIX behavior).
4244
inQuote = null
4345
i += 1
4446
}
@@ -49,19 +51,22 @@ internal fun shlex(
4951
}
5052

5153
c in "\"'" && inQuote == null -> {
54+
inToken = true
5255
inQuote = c
5356
i += 1
5457
}
5558

5659
c.isWhitespace() && inQuote == null -> {
57-
if (sb.isNotEmpty()) {
60+
if (inToken) {
5861
toks += sb.toString()
5962
sb.clear()
63+
inToken = false
6064
}
6165
i += 1
6266
}
6367

6468
else -> {
69+
inToken = true
6570
sb.append(c)
6671
i += 1
6772
}
@@ -72,7 +77,7 @@ internal fun shlex(
7277
err(localization.unclosedQuote())
7378
}
7479

75-
if (sb.isNotEmpty()) {
80+
if (inToken) {
7681
toks += sb.toString()
7782
}
7883

test/src/commonTest/kotlin/com/github/ajalt/clikt/parsers/AtFileTest.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,23 @@ class AtFileTest {
180180
C().parse("@file")
181181
}
182182

183+
@[Test JsName("adjacent_quoted_strings_concatenate")]
184+
fun `adjacent quoted strings concatenate`() {
185+
// Verify that adjacent quoted/unquoted strings produce a single token (POSIX behavior)
186+
// e.g., "'"c"'" should produce 'c' (single-quote, c, single-quote)
187+
class C : TestCommand() {
188+
val arg by argument().multiple()
189+
190+
override fun run_() {
191+
arg shouldBe listOf("a", "b", "'c'")
192+
}
193+
}
194+
195+
C().withAtFiles(
196+
"foo" to """a b "'"c"'""""
197+
).parse("@foo")
198+
}
199+
183200
@[Test JsName("parsing_atfile_with_alias")]
184201
fun `parsing atfile with alias`() {
185202
class C : TestCommand() {

0 commit comments

Comments
 (0)