Last active
July 10, 2023 20:19
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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