Skip to content

Commit

Permalink
Simplified example and added to new cli package doc. More doc, more t…
Browse files Browse the repository at this point in the history
…ests. Fixed bug where unknown short options were missed.
  • Loading branch information
cquinn committed Jul 15, 2017
1 parent 481f808 commit 67c1403
Show file tree
Hide file tree
Showing 9 changed files with 349 additions and 84 deletions.
69 changes: 29 additions & 40 deletions examples/commandline/main.pony
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,35 @@ use "cli"

actor Main
new create(env: Env) =>
try
let cmd =
match CommandParser(cli_spec()).parse(env.args, env.vars())
| let c: Command => c
| let ch: CommandHelp =>
ch.print_help(env.out)
env.exitcode(0)
return
| let se: SyntaxError =>
env.out.print(se.string())
env.exitcode(1)
return
end
let cs =
try
CommandSpec.leaf("echo", "A sample echo program", [
OptionSpec.bool("upper", "Uppercase words"
where short' = 'U', default' = false)
], [
ArgSpec.string_seq("words", "The words to echo")
]).>add_help()
else
env.exitcode(-1) // some kind of coding error
return
end

// cmd is a valid Command, now use it.
let cmd =
match CommandParser(cs).parse(env.args, env.vars())
| let c: Command => c
| let ch: CommandHelp =>
ch.print_help(env.out)
env.exitcode(0)
return
| let se: SyntaxError =>
env.out.print(se.string())
env.exitcode(1)
return
end

let upper = cmd.option("upper").bool()
let words = cmd.arg("words").string_seq()
for word in words.values() do
env.out.write(if upper then word.upper() else word end + " ")
end

fun tag cli_spec(): CommandSpec box ? =>
"""
Builds and returns the spec for a sample chat client's CLI.
"""
let cs = CommandSpec.parent("chat", "A sample chat program", [
OptionSpec.bool("admin", "Chat as admin" where default' = false)
OptionSpec.string("name", "Your name" where short' = 'n')
OptionSpec.i64("volume", "Chat volume" where short' = 'v')
], [
CommandSpec.leaf("say", "Say something", Array[OptionSpec](), [
ArgSpec.string("words", "The words to say")
])
CommandSpec.leaf("emote", "Send an emotion", [
OptionSpec.f64("speed", "Emote play speed" where default' = F64(1.0))
], [
ArgSpec.string("emotion", "Emote to send")
])
CommandSpec.parent("config", "Configuration commands", Array[OptionSpec](), [
CommandSpec.leaf("server", "Server configuration", Array[OptionSpec](), [
ArgSpec.string("address", "Address of the server")
])
])
])
cs.add_help()
cs
env.out.print("")
103 changes: 100 additions & 3 deletions packages/cli/_test.pony
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ actor Main is TestList
fun tag tests(test: PonyTest) =>
test(_TestMinimal)
test(_TestBadName)
test(_TestUnknownCommand)
test(_TestUnexpectedArg)
test(_TestUnknownShort)
test(_TestUnknownLong)
test(_TestHyphenArg)
test(_TestBools)
test(_TestDefaults)
Expand Down Expand Up @@ -51,6 +55,98 @@ class iso _TestBadName is UnitTest
h.fail("expected error on bad command name: " + cs.name())
end

class iso _TestUnknownCommand is UnitTest
fun name(): String => "ponycli/unknown_command"

// Negative test: unknown command should report
fun apply(h: TestHelper) ? =>
let cs = _Fixtures.chat_cli_spec()

let args: Array[String] = [
"ignored"
"unknown"
]

let cmdErr = CommandParser(cs).parse(args)
h.log("Parsed: " + cmdErr.string())

match cmdErr
| let se: SyntaxError => None
h.assert_eq[String]("Error: unknown command at: 'unknown'", se.string())
else
h.fail("expected syntax error for unknown command: " + cmdErr.string())
end

class iso _TestUnexpectedArg is UnitTest
fun name(): String => "ponycli/unknown_command"

// Negative test: unexpected arg/command token should report
fun apply(h: TestHelper) ? =>
let cs = _Fixtures.bools_cli_spec()

let args: Array[String] = [
"ignored"
"unknown"
]

let cmdErr = CommandParser(cs).parse(args)
h.log("Parsed: " + cmdErr.string())

match cmdErr
| let se: SyntaxError => None
h.assert_eq[String](
"Error: too many positional arguments at: 'unknown'", se.string())
else
h.fail("expected syntax error for unknown command: " + cmdErr.string())
end

class iso _TestUnknownShort is UnitTest
fun name(): String => "ponycli/unknown_short"

// Negative test: unknown short option should report
fun apply(h: TestHelper) ? =>
let cs = _Fixtures.bools_cli_spec()

let args: Array[String] = [
"ignored"
"-Z"
]

let cmdErr = CommandParser(cs).parse(args)
h.log("Parsed: " + cmdErr.string())

match cmdErr
| let se: SyntaxError => None
h.assert_eq[String]("Error: unknown short option at: 'Z'", se.string())
else
h.fail(
"expected syntax error for unknown short option: " + cmdErr.string())
end

class iso _TestUnknownLong is UnitTest
fun name(): String => "ponycli/unknown_long"

// Negative test: unknown long option should report
fun apply(h: TestHelper) ? =>
let cs = _Fixtures.bools_cli_spec()

let args: Array[String] = [
"ignored"
"--unknown"
]

let cmdErr = CommandParser(cs).parse(args)
h.log("Parsed: " + cmdErr.string())

match cmdErr
| let se: SyntaxError => None
h.assert_eq[String](
"Error: unknown long option at: 'unknown'", se.string())
else
h.fail(
"expected syntax error for unknown long option: " + cmdErr.string())
end

class iso _TestHyphenArg is UnitTest
fun name(): String => "ponycli/hyphen"

Expand Down Expand Up @@ -355,7 +451,7 @@ class iso _TestMustBeLeaf is UnitTest
match cmdErr
| let se: SyntaxError => None
else
h.fail("expected syntax error for non-leaf cmd: " + cmdErr.string())
h.fail("expected syntax error for non-leaf command: " + cmdErr.string())
end

class iso _TestHelp is UnitTest
Expand Down Expand Up @@ -410,7 +506,7 @@ primitive _Fixtures
CommandSpec.parent("chat", "A sample chat program", [
OptionSpec.bool("admin", "Chat as admin" where default' = false)
OptionSpec.string("name", "Your name" where short' = 'n')
OptionSpec.f64("volume", "Chat volume" where short' = 'v')
OptionSpec.f64("volume", "Chat volume" where short' = 'v', default' = 1.0)
], [
CommandSpec.leaf("say", "Say something", Array[OptionSpec](), [
ArgSpec.string_seq("words", "The words to say")
Expand All @@ -420,7 +516,8 @@ primitive _Fixtures
], [
ArgSpec.string("emotion", "Emote to send")
])
CommandSpec.parent("config", "Configuration commands", Array[OptionSpec](), [
CommandSpec.parent("config", "Configuration commands",
Array[OptionSpec](), [
CommandSpec.leaf("server", "Server configuration", Array[OptionSpec](), [
ArgSpec.string("address", "Address of the server")
])
Expand Down
Binary file added packages/cli/cli
Binary file not shown.
98 changes: 98 additions & 0 deletions packages/cli/cli.pony
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""
# CLI Package
The CLI package provides enhanced Posix+GNU command line parsing with the
feature of commands that can be specified in a hierarchy.
See [RFC-0038](https://github.com/ponylang/rfcs/blob/master/text/0038-cli-format.md) for more background.
The general EBNF of a command line is:
```ebnf
command_line ::= root_command (option | command)* (option | arg)*
command ::= alphanum_word
alphanum_word ::= alphachar(alphachar | numchar | '_' | '-')*
option ::= longoption | shortoptionset
longoption ::= '--'alphanum_word['='arg | ' 'arg]
shortoptionset := '-'alphachar[alphachar]...['='arg | ' 'arg]
arg := boolarg | intarg | floatarg | stringarg
boolarg := 'true' | 'false'
intarg> := ['-'] numchar...
floatarg ::= ['-'] numchar... ['.' numchar...]
stringarg ::= anychar
```
Some examples:
```
usage: ls [<options>] [<args> ...]
usage: make [<options>] <command> [<options>] [<args> ...]
usage: chat [<options>] <command> <subcommand> [<options>] [<args> ...]
```
## Usage
The types in the cli package are broken down into three groups:
### Specs
Pony programs use constructors to create the spec objects to specify their
command line syntax. Many aspects of the spec are checked for correctness at
compile time, and the result represents everything the parser needs to know
when parsing a command line or forming syntax help messages.
### Parser
Programs then use the spec to create a parser at runtime to parse any given
command line. This is often env.args(), but could also be commands from files
or other input sources. The result of a parse is either a parsed command, a
help command object, or a syntax error.
### Commands
Programs then query the returned parsed command to determine the command
specified, and the effective values for all options and arguments.
# Example program
This program opens the files that are given as command line arguments
and prints their contents.
```pony
use "cli"
actor Main
new create(env: Env) =>
let cs =
try
CommandSpec.leaf("echo", "A sample echo program", [
OptionSpec.bool("upper", "Uppercase words"
where short' = 'U', default' = false)
], [
ArgSpec.string_seq("words", "The words to echo")
]).>add_help()
else
env.exitcode(-1) // some kind of coding error
return
end
let cmd =
match CommandParser(cs).parse(env.args, env.vars())
| let c: Command => c
| let ch: CommandHelp =>
ch.print_help(env.out)
env.exitcode(0)
return
| let se: SyntaxError =>
env.out.print(se.string())
env.exitcode(1)
return
end
let upper = cmd.option("upper").bool()
let words = cmd.arg("words").string_seq()
for word in words.values() do
env.out.write(if upper then word.upper() else word end + " ")
end
env.out.print("")
```
"""
Loading

0 comments on commit 67c1403

Please sign in to comment.