Skip to content

Instantly share code, notes, and snippets.

@arturaz
Last active July 10, 2023 20:19
Show Gist options
  • Save arturaz/619761ea94d69e048c8df06dc388ba99 to your computer and use it in GitHub Desktop.
Save arturaz/619761ea94d69e048c8df06dc388ba99 to your computer and use it in GitHub Desktop.
A parser written with cats-parse that turns a string into a list of command line arguments, similar to how a shell would do it.
package app.utils
import cats.parse.{Parser as P, Parser0 as P0}
import cats.syntax.show.*
import scala.util.control.NonFatal
/**
* A parser that turns a string into a list of command line arguments, similar to how a shell would do it.
*
* Only supports " and \ as escape characters.
*/
object CommandLineArgsParser {
private val escapeChar =
(
P.string(""" \" """.trim).as('"')
| P.string(""" \\ """.trim).as('\\')
| P.string("\\ ").as(' ')
).withContext("escapeChar")
private val normalChar =
P.charWhere(c => c != '"' && c != '\\').withContext("normalChar")
private val quotedContent: P0[String] =
(escapeChar | normalChar).rep0.map(_.mkString("")).withContext("quotedContent")
private val quotedStr: P[String] =
(P.char('"') *> quotedContent <* P.char('"')).withContext("quotedStr")
private val unquotedStr: P[String] =
P.charsWhile(c => !c.isWhitespace && c != '"').string.withContext("unquotedStr")
private val argument: P[String] =
quotedStr.orElse(unquotedStr).withContext("argument")
private val whitespace: P[Unit] =
P.charsWhile(_.isWhitespace).void.withContext("whitespace")
private val arguments: P0[List[String]] =
argument.repSep0(whitespace).withContext("arguments")
def parse(s: String): Either[P.Error, List[String]] = arguments.parseAll(s.trim)
}
package app.utils
import app.AppCommonDataTest
import cats.syntax.show.*
class CommandLineArgsParserTest extends AppCommonDataTest {
extension (s: String) {
def shouldParseTo(expected: List[String]): Unit = {
CommandLineArgsParser.parse(s) match {
case Left(error) => fail(s"Failed to parse '$s':\n${error.show}")
case Right(parsed) => parsed should_=== expected
}
}
}
test("it should parse empty string") {
"" shouldParseTo Nil
}
test("it should parse a single unquoted argument") {
"foo" shouldParseTo List("foo")
}
test("it should parse a single quoted argument") {
"\"foo\"" shouldParseTo List("foo")
}
test("it should parse a single quoted argument with spaces") {
"\"foo bar\"" shouldParseTo List("foo bar")
}
test("it should parse a single quoted argument with escaped quotes") {
""" "foo \"bar\"" """ shouldParseTo List("foo \"bar\"")
}
test("it should parse a single quoted argument with escaped backslashes") {
"\"foo \\\\bar\\\\\"" shouldParseTo List("foo \\bar\\")
}
test("it should parse a single quoted argument with escaped spaces") {
"\"foo\\ bar\"" shouldParseTo List("foo bar")
}
test("it should parse a single quoted argument with escaped spaces and escaped quotes") {
"\"foo\\ \\\"bar\\\"\"" shouldParseTo List("foo \"bar\"")
}
test("it should parse a single quoted argument with escaped spaces and escaped backslashes") {
"\"foo\\ \\\\bar\\\\\"" shouldParseTo List("foo \\bar\\")
}
test("it should parse a single quoted argument with escaped spaces, escaped quotes and escaped backslashes") {
"\"foo\\ \\\"\\\\bar\\\\\\\"\"" shouldParseTo List("foo \"\\bar\\\"")
}
test("it should parse multiple unquoted arguments") {
"foo bar" shouldParseTo List("foo", "bar")
}
test("it should parse multiple quoted arguments") {
"\"foo\" \"bar\"" shouldParseTo List("foo", "bar")
}
test("it should parse multiple quoted arguments with spaces") {
"\"foo bar\" \"baz qux\"" shouldParseTo List("foo bar", "baz qux")
}
test("it should parse multiple quoted arguments with escaped quotes") {
"\"foo \\\"bar\\\"\" \"baz \\\"qux\\\"\"" shouldParseTo List("foo \"bar\"", "baz \"qux\"")
}
test("it should parse multiple quoted arguments with escaped backslashes") {
"\"foo \\\\bar\\\\\" \"baz \\\\qux\\\\\"" shouldParseTo List("foo \\bar\\", "baz \\qux\\")
}
test("it should parse multiple quoted arguments with escaped spaces") {
"\"foo\\ bar\" \"baz\\ qux\"" shouldParseTo List("foo bar", "baz qux")
}
test("it should parse multiple quoted arguments with escaped spaces and escaped quotes") {
"\"foo\\ \\\"bar\\\"\" \"baz\\ \\\"qux\\\"\"" shouldParseTo List("foo \"bar\"", "baz \"qux\"")
}
test("it should parse multiple quoted arguments with escaped spaces and escaped backslashes") {
"\"foo\\ \\\\bar\\\\\" \"baz\\ \\\\qux\\\\\"" shouldParseTo List("foo \\bar\\", "baz \\qux\\")
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment