From 89350182d717d4bb43d9e796d21db8fe1329a738 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Wro=C5=84ski?= Date: Fri, 19 Nov 2021 15:18:40 +0100 Subject: [PATCH 1/2] Escape backticks in bash/zsh completion --- .../src/main/scala/caseapp/Annotations.scala | 4 ++- .../scala/caseapp/core/complete/Bash.scala | 2 +- .../scala/caseapp/core/complete/Zsh.scala | 7 +++-- project/Mima.scala | 2 +- .../scala/caseapp/CompletionDefinitions.scala | 13 +++++++- .../test/scala/caseapp/CompletionTests.scala | 30 ++++++++++++++++++- 6 files changed, 50 insertions(+), 8 deletions(-) diff --git a/annotations/shared/src/main/scala/caseapp/Annotations.scala b/annotations/shared/src/main/scala/caseapp/Annotations.scala index f979823f..3235e029 100644 --- a/annotations/shared/src/main/scala/caseapp/Annotations.scala +++ b/annotations/shared/src/main/scala/caseapp/Annotations.scala @@ -17,8 +17,10 @@ object ValueDescription { } /** Help message for the annotated argument + * @messageMd + * not used by case-app itself, only there as a convenience for case-app users */ -final case class HelpMessage(message: String) extends StaticAnnotation +final case class HelpMessage(message: String, messageMd: String = "") extends StaticAnnotation /** Name for the annotated case class of arguments E.g. MyApp */ diff --git a/core/shared/src/main/scala/caseapp/core/complete/Bash.scala b/core/shared/src/main/scala/caseapp/core/complete/Bash.scala index e7a0f397..73b71db3 100644 --- a/core/shared/src/main/scala/caseapp/core/complete/Bash.scala +++ b/core/shared/src/main/scala/caseapp/core/complete/Bash.scala @@ -19,7 +19,7 @@ object Bash { } private def escape(s: String): String = - s.replace("\"", "\\\"") + s.replace("\"", "\\\"").replace("`", "\\`").linesIterator.toStream.headOption.getOrElse("") def print(items: Seq[CompletionItem]): String = { val newLine = System.lineSeparator() val b = new StringBuilder diff --git a/core/shared/src/main/scala/caseapp/core/complete/Zsh.scala b/core/shared/src/main/scala/caseapp/core/complete/Zsh.scala index 3a8db7e1..a7fd4c60 100644 --- a/core/shared/src/main/scala/caseapp/core/complete/Zsh.scala +++ b/core/shared/src/main/scala/caseapp/core/complete/Zsh.scala @@ -34,14 +34,15 @@ object Zsh { else res } - + private def escape(s: String): String = + s.replace("'", "\\'").replace("`", "\\`").linesIterator.toStream.headOption.getOrElse("") private def defs(item: CompletionItem): Seq[String] = { val (options, arguments) = item.values.partition(_.startsWith("-")) val optionsOutput = if (options.isEmpty) Nil else { val escapedOptions = options - val desc = item.description.map(":" + _.replace("'", "\\'")).getOrElse("") + val desc = item.description.map(desc => ":" + escape(desc)).getOrElse("") options.map { opt => "\"" + opt + desc + "\"" } @@ -49,7 +50,7 @@ object Zsh { val argumentsOutput = if (arguments.isEmpty) Nil else { - val desc = item.description.map(":" + _.replace("'", "\\'")).getOrElse("") + val desc = item.description.map(desc => ":" + escape(desc)).getOrElse("") arguments.map("'" + _.replace(":", "\\:") + desc + "'") } optionsOutput ++ argumentsOutput diff --git a/project/Mima.scala b/project/Mima.scala index 0711db14..4dd4f346 100644 --- a/project/Mima.scala +++ b/project/Mima.scala @@ -8,7 +8,7 @@ import scala.sys.process._ object Mima { def binaryCompatibilityVersions: Set[String] = - Seq("git", "tag", "--merged", "HEAD^", "--contains", "706a1d90cca205b69e4cff583abb9411526e7d58") + Seq("git", "tag", "--merged", "HEAD^", "--contains", "c8abc969f219357022ea8cf816b4e7653833c620") .!! .linesIterator .map(_.trim) diff --git a/tests/shared/src/test/scala/caseapp/CompletionDefinitions.scala b/tests/shared/src/test/scala/caseapp/CompletionDefinitions.scala index d599ecd2..1427eaf7 100644 --- a/tests/shared/src/test/scala/caseapp/CompletionDefinitions.scala +++ b/tests/shared/src/test/scala/caseapp/CompletionDefinitions.scala @@ -59,18 +59,29 @@ object CompletionDefinitions { @Name("g") @HelpMessage("A pattern") glob: String = "", @Name("d") count: Int = 0 ) + case class BackTickOptions( + @HelpMessage( + """A pattern with backtick `--` + |with multiline""".stripMargin + ) backtick: String = "", + @Name("d") count: Int = 0 + ) object First extends Command[FirstOptions] { def run(options: FirstOptions, args: RemainingArgs): Unit = ??? } object Second extends Command[SecondOptions] { def run(options: SecondOptions, args: RemainingArgs): Unit = ??? } + object BackTick extends Command[BackTickOptions] { + def run(options: BackTickOptions, args: RemainingArgs): Unit = ??? + } object Prog extends CommandsEntryPoint { def progName = "prog" def commands = Seq( First, - Second + Second, + BackTick ) } } diff --git a/tests/shared/src/test/scala/caseapp/CompletionTests.scala b/tests/shared/src/test/scala/caseapp/CompletionTests.scala index 077a9651..9ae0e58d 100644 --- a/tests/shared/src/test/scala/caseapp/CompletionTests.scala +++ b/tests/shared/src/test/scala/caseapp/CompletionTests.scala @@ -1,6 +1,6 @@ package caseapp -import caseapp.core.complete.CompletionItem +import caseapp.core.complete.{Bash, CompletionItem, Zsh} import utest._ object CompletionTests extends TestSuite { @@ -110,6 +110,7 @@ object CompletionTests extends TestSuite { test { val res = Prog.complete(Seq(""), 0) val expected = List( + CompletionItem("back-tick", None, Nil), CompletionItem("first", None, Nil), CompletionItem("second", None, Nil) ) @@ -140,6 +141,33 @@ object CompletionTests extends TestSuite { ) assert(res == expected) } + + test("bash") { + val res = Prog.complete(Seq("back-tick", "-"), 1) + val expected = List( + CompletionItem("--backtick", Some("A pattern with backtick `--`\nwith multiline"), Nil), + CompletionItem("--count", None, List("-d")) + ) + assert(res == expected) + + val compRely = Bash.print(res) + val expectedCompRely = """"--backtick -- A pattern with backtick \`--\`"""".stripMargin + + assert(compRely.contains(expectedCompRely)) + } + test("zsh") { + val res = Prog.complete(Seq("back-tick", "-"), 1) + val expected = List( + CompletionItem("--backtick", Some("A pattern with backtick `--`\nwith multiline"), Nil), + CompletionItem("--count", None, List("-d")) + ) + assert(res == expected) + + val compRely = Zsh.print(res) + val expectedCompRely = """"--backtick:A pattern with backtick \`--\`"""".stripMargin + + assert(compRely.contains(expectedCompRely)) + } } test("commands with default") { From 953183d725d96eed945e0c21caaabbbdd12d6cf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Wro=C5=84ski?= Date: Fri, 26 Nov 2021 10:15:54 +0100 Subject: [PATCH 2/2] Use MurmurHash3 to count id in Zsh - avoid using java methods from MessageDigest --- .../scala/caseapp/core/complete/Zsh.scala | 23 +++++-------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/core/shared/src/main/scala/caseapp/core/complete/Zsh.scala b/core/shared/src/main/scala/caseapp/core/complete/Zsh.scala index a7fd4c60..89ec0c3a 100644 --- a/core/shared/src/main/scala/caseapp/core/complete/Zsh.scala +++ b/core/shared/src/main/scala/caseapp/core/complete/Zsh.scala @@ -1,12 +1,6 @@ package caseapp.core.complete -import java.math.BigInteger -import java.nio.charset.StandardCharsets -import java.security.MessageDigest - -import dataclass.data - -import scala.collection.mutable +import scala.util.hashing.MurmurHash3 object Zsh { @@ -24,15 +18,10 @@ object Zsh { |} |""".stripMargin - private def md5(content: Iterator[String]): String = { - val md = MessageDigest.getInstance("MD5") - for (s <- content) md.update(s.getBytes(StandardCharsets.UTF_8)) - val digest = md.digest() - val res = new BigInteger(1, digest).toString(16) - if (res.length < 32) - ("0" * (32 - res.length)) + res - else - res + private def hash(content: Iterator[String]): String = { + val hash = MurmurHash3.arrayHash(content.toArray) + if (hash < 0) (hash * -1).toString + else hash.toString } private def escape(s: String): String = s.replace("'", "\\'").replace("`", "\\`").linesIterator.toStream.headOption.getOrElse("") @@ -59,7 +48,7 @@ object Zsh { private def render(commands: Seq[String]): String = if (commands.isEmpty) "_files" + System.lineSeparator() else { - val id = md5(commands.iterator) + val id = hash(commands.iterator) s"""local -a args$id |args$id=( |${commands.mkString(System.lineSeparator())}