diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/flink/FlinkProcessBuilder.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/flink/FlinkProcessBuilder.scala index 9597c974f9e..36c047d82d6 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/flink/FlinkProcessBuilder.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/flink/FlinkProcessBuilder.scala @@ -154,9 +154,7 @@ class FlinkProcessBuilder( val memory = conf.get(ENGINE_FLINK_MEMORY) buffer += s"-Xmx$memory" val javaOptions = conf.get(ENGINE_FLINK_JAVA_OPTIONS).filter(StringUtils.isNotBlank(_)) - if (javaOptions.isDefined) { - buffer += javaOptions.get - } + javaOptions.map(parseOptionString).foreach(buffer += _) val classpathEntries = new mutable.LinkedHashSet[String] // flink engine runtime jar diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/hive/HiveProcessBuilder.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/hive/HiveProcessBuilder.scala index 8fc14d4ca5a..cd7b61bd63e 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/hive/HiveProcessBuilder.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/hive/HiveProcessBuilder.scala @@ -64,9 +64,7 @@ class HiveProcessBuilder( val memory = conf.get(ENGINE_HIVE_MEMORY) buffer += s"-Xmx$memory" val javaOptions = conf.get(ENGINE_HIVE_JAVA_OPTIONS).filter(StringUtils.isNotBlank(_)) - if (javaOptions.isDefined) { - buffer += javaOptions.get - } + javaOptions.map(parseOptionString).foreach(buffer += _) // -Xmx5g // java options val classpathEntries = new mutable.LinkedHashSet[String] diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/jdbc/JdbcProcessBuilder.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/jdbc/JdbcProcessBuilder.scala index 6d7b66aac0a..6c4080d4bab 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/jdbc/JdbcProcessBuilder.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/jdbc/JdbcProcessBuilder.scala @@ -73,7 +73,7 @@ class JdbcProcessBuilder( buffer += s"-Xmx$memory" val javaOptions = conf.get(ENGINE_JDBC_JAVA_OPTIONS).filter(StringUtils.isNotBlank(_)) - javaOptions.foreach(buffer += _) + javaOptions.map(parseOptionString).foreach(buffer += _) val classpathEntries = new mutable.LinkedHashSet[String] mainResource.foreach(classpathEntries.add) diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/trino/TrinoProcessBuilder.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/trino/TrinoProcessBuilder.scala index 6c21215b561..87de0899d28 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/trino/TrinoProcessBuilder.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/trino/TrinoProcessBuilder.scala @@ -64,9 +64,7 @@ class TrinoProcessBuilder( val memory = conf.get(ENGINE_TRINO_MEMORY) buffer += s"-Xmx$memory" val javaOptions = conf.get(ENGINE_TRINO_JAVA_OPTIONS).filter(StringUtils.isNotBlank(_)) - if (javaOptions.isDefined) { - buffer += javaOptions.get - } + javaOptions.map(parseOptionString).foreach(buffer += _) val classpathEntries = new mutable.LinkedHashSet[String] // trino engine runtime jar diff --git a/kyuubi-util-scala/src/main/scala/org/apache/kyuubi/util/command/CommandLineUtils.scala b/kyuubi-util-scala/src/main/scala/org/apache/kyuubi/util/command/CommandLineUtils.scala index 91327223a60..61e92321a29 100644 --- a/kyuubi-util-scala/src/main/scala/org/apache/kyuubi/util/command/CommandLineUtils.scala +++ b/kyuubi-util-scala/src/main/scala/org/apache/kyuubi/util/command/CommandLineUtils.scala @@ -19,6 +19,7 @@ package org.apache.kyuubi.util.command import java.io.File +import scala.collection.mutable.ListBuffer import scala.util.matching.Regex object CommandLineUtils { @@ -72,4 +73,88 @@ object CommandLineUtils { } } } + + /** + * Parse a string as if it were a list of arguments, following bash semantics. + * For example: + * + * Input: "\"ab cd\" efgh 'i \" j'" + * Output: [ "ab cd", "efgh", "i \" j" ] + */ + def parseOptionString(s: String): List[String] = { + val opts = ListBuffer[String]() + val opt = new StringBuilder() + var inOpt = false + var inSingleQuote = false + var inDoubleQuote = false + var escapeNext = false + var hasData = false + + var i = 0 + while (i < s.length) { + val c = s.codePointAt(i) + if (escapeNext) { + opt.appendAll(Character.toChars(c)) + escapeNext = false + } else if (inOpt) { + c match { + case '\\' => + if (inSingleQuote) { + opt.appendAll(Character.toChars(c)) + } else { + escapeNext = true + } + case '\'' => + if (inDoubleQuote) { + opt.appendAll(Character.toChars(c)) + } else { + inSingleQuote = !inSingleQuote + } + case '"' => + if (inSingleQuote) { + opt.appendAll(Character.toChars(c)) + } else { + inDoubleQuote = !inDoubleQuote + } + case _ => + if (!Character.isWhitespace(c) || inSingleQuote || inDoubleQuote) { + opt.appendAll(Character.toChars(c)) + } else { + opts += opt.toString() + opt.setLength(0) + inOpt = false + hasData = false + } + } + } else { + c match { + case '\'' => + inSingleQuote = true + inOpt = true + hasData = true + case '"' => + inDoubleQuote = true + inOpt = true + hasData = true + case '\\' => + escapeNext = true + inOpt = true + hasData = true + case _ => + if (!Character.isWhitespace(c)) { + inOpt = true + hasData = true + opt.appendAll(Character.toChars(c)) + } + } + } + i += Character.charCount(c) + } + + require(!inSingleQuote && !inDoubleQuote && !escapeNext, s"Invalid option string: $s") + if (hasData) { + opts += opt.toString() + } + opts.toList + } } diff --git a/kyuubi-util-scala/src/test/scala/org/apache/kyuubi/util/command/CommandUtilsSuite.scala b/kyuubi-util-scala/src/test/scala/org/apache/kyuubi/util/command/CommandUtilsSuite.scala index e000e7478b6..104d02b07b9 100644 --- a/kyuubi-util-scala/src/test/scala/org/apache/kyuubi/util/command/CommandUtilsSuite.scala +++ b/kyuubi-util-scala/src/test/scala/org/apache/kyuubi/util/command/CommandUtilsSuite.scala @@ -47,4 +47,11 @@ class CommandUtilsSuite extends AnyFunSuite { assertResult(Seq("-cp", "/path/a.jar:/path2/b*.jar"))( genClasspathOption(Seq("/path/a.jar", "/path2/b*.jar"))) } + + test("parseOptionString should parse a string as a list of arguments") { + val input = "\"ab cd\" efgh 'i \" j'" + val expectedOutput = List("ab cd", "efgh", "i \" j") + val actualOutput = CommandLineUtils.parseOptionString(input) + assert(actualOutput == expectedOutput) + } }