├── .gitignore
├── CHANGELOG
├── LICENSE
├── README.markdown
├── build.sbt
├── core
└── src
│ ├── main
│ └── scala
│ │ └── com
│ │ └── concurrentthought
│ │ └── cla
│ │ ├── Args.scala
│ │ ├── Help.scala
│ │ ├── Opt.scala
│ │ ├── OptParser.scala
│ │ └── package.scala
│ └── test
│ └── scala
│ └── com
│ └── concurrentthought
│ └── cla
│ ├── ArgsSpec.scala
│ ├── CLAPackageSpec.scala
│ ├── HelpSpec.scala
│ ├── OptParserSpec.scala
│ ├── OptSpec.scala
│ ├── SpecHelper.scala
│ └── StringOut.scala
├── examples
└── src
│ └── main
│ └── scala
│ └── com
│ └── concurrentthought
│ └── cla
│ └── examples
│ └── CLASampleMain.scala
├── project
├── build.properties
└── plugins.sbt
├── scalastyle-config.xml
├── sonatype.sbt
└── version.sbt
/.gitignore:
--------------------------------------------------------------------------------
1 | logs
2 | project/project
3 | project/target
4 | target
5 | tmp
6 | .history
7 | /.idea
8 | /*.iml
9 | /.idea_modules
10 | /.classpath
11 | /.project
12 | /.settings
13 | /project/*-shim.sbt
14 | *.class
15 | .DS_Store
16 | notes.md
17 |
18 |
--------------------------------------------------------------------------------
/CHANGELOG:
--------------------------------------------------------------------------------
1 | Version 0.5.0 (2018-05-03)
2 | --------------------------
3 | - Upgraded to Scala 2.12, SBT 1.1.2, and upgraded several dependency versions.
4 | Thanks to @intracer for help!
5 | - No API changes.
6 |
7 | Version 0.4.0 (2016-01-02)
8 | --------------------------
9 | - Reworked the API for invoking the parsing. There is now a `process` method
10 | which automatically handles printing the help message, when the user requests
11 | help or an invalid option is specified, then exiting.
12 | - Reorganized the project into a "core", which is the released library, and an
13 | "examples" project, which currently consists only of `CLASampleMain.scala`.
14 |
15 | Version 0.3.3 (2015-11-27)
16 | --------------------------
17 | - Fixed regression in 0.3.2.
18 |
19 | Version 0.3.2 (2015-11-27)
20 | --------------------------
21 | - Updated to Scala 2.10.5 and 2.11.7, SBT 0.13.9.
22 |
23 | Version 0.3.0 (2015-02-14)
24 | --------------------------
25 | - String "DSL" now parsed with a Parboiled parser.
26 | - Internal refactoring and simplification.
27 |
28 | Version 0.2.1 (2015-01-15)
29 | --------------------------
30 | - Scala 2.11.4 and 2.10.4 support.
31 | - Added the concept of required vs. "optional" options.
32 |
33 | Version 0.2.0 (2015-01-15)
34 | --------------------------
35 | - First public release
36 | - Added a "path" type option.
37 |
38 | Version 0.1.0 (2015-01-15)
39 | --------------------------
40 | - Initial private release.
41 |
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2015-2017, Concurrent Thought
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
15 |
16 | =======================================================================
17 |
18 | Apache License
19 | Version 2.0, January 2004
20 | http://www.apache.org/licenses/
21 |
22 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
23 |
24 | 1. Definitions.
25 |
26 | "License" shall mean the terms and conditions for use, reproduction,
27 | and distribution as defined by Sections 1 through 9 of this document.
28 |
29 | "Licensor" shall mean the copyright owner or entity authorized by
30 | the copyright owner that is granting the License.
31 |
32 | "Legal Entity" shall mean the union of the acting entity and all
33 | other entities that control, are controlled by, or are under common
34 | control with that entity. For the purposes of this definition,
35 | "control" means (i) the power, direct or indirect, to cause the
36 | direction or management of such entity, whether by contract or
37 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
38 | outstanding shares, or (iii) beneficial ownership of such entity.
39 |
40 | "You" (or "Your") shall mean an individual or Legal Entity
41 | exercising permissions granted by this License.
42 |
43 | "Source" form shall mean the preferred form for making modifications,
44 | including but not limited to software source code, documentation
45 | source, and configuration files.
46 |
47 | "Object" form shall mean any form resulting from mechanical
48 | transformation or translation of a Source form, including but
49 | not limited to compiled object code, generated documentation,
50 | and conversions to other media types.
51 |
52 | "Work" shall mean the work of authorship, whether in Source or
53 | Object form, made available under the License, as indicated by a
54 | copyright notice that is included in or attached to the work
55 | (an example is provided in the Appendix below).
56 |
57 | "Derivative Works" shall mean any work, whether in Source or Object
58 | form, that is based on (or derived from) the Work and for which the
59 | editorial revisions, annotations, elaborations, or other modifications
60 | represent, as a whole, an original work of authorship. For the purposes
61 | of this License, Derivative Works shall not include works that remain
62 | separable from, or merely link (or bind by name) to the interfaces of,
63 | the Work and Derivative Works thereof.
64 |
65 | "Contribution" shall mean any work of authorship, including
66 | the original version of the Work and any modifications or additions
67 | to that Work or Derivative Works thereof, that is intentionally
68 | submitted to Licensor for inclusion in the Work by the copyright owner
69 | or by an individual or Legal Entity authorized to submit on behalf of
70 | the copyright owner. For the purposes of this definition, "submitted"
71 | means any form of electronic, verbal, or written communication sent
72 | to the Licensor or its representatives, including but not limited to
73 | communication on electronic mailing lists, source code control systems,
74 | and issue tracking systems that are managed by, or on behalf of, the
75 | Licensor for the purpose of discussing and improving the Work, but
76 | excluding communication that is conspicuously marked or otherwise
77 | designated in writing by the copyright owner as "Not a Contribution."
78 |
79 | "Contributor" shall mean Licensor and any individual or Legal Entity
80 | on behalf of whom a Contribution has been received by Licensor and
81 | subsequently incorporated within the Work.
82 |
83 | 2. Grant of Copyright License. Subject to the terms and conditions of
84 | this License, each Contributor hereby grants to You a perpetual,
85 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
86 | copyright license to reproduce, prepare Derivative Works of,
87 | publicly display, publicly perform, sublicense, and distribute the
88 | Work and such Derivative Works in Source or Object form.
89 |
90 | 3. Grant of Patent License. Subject to the terms and conditions of
91 | this License, each Contributor hereby grants to You a perpetual,
92 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
93 | (except as stated in this section) patent license to make, have made,
94 | use, offer to sell, sell, import, and otherwise transfer the Work,
95 | where such license applies only to those patent claims licensable
96 | by such Contributor that are necessarily infringed by their
97 | Contribution(s) alone or by combination of their Contribution(s)
98 | with the Work to which such Contribution(s) was submitted. If You
99 | institute patent litigation against any entity (including a
100 | cross-claim or counterclaim in a lawsuit) alleging that the Work
101 | or a Contribution incorporated within the Work constitutes direct
102 | or contributory patent infringement, then any patent licenses
103 | granted to You under this License for that Work shall terminate
104 | as of the date such litigation is filed.
105 |
106 | 4. Redistribution. You may reproduce and distribute copies of the
107 | Work or Derivative Works thereof in any medium, with or without
108 | modifications, and in Source or Object form, provided that You
109 | meet the following conditions:
110 |
111 | (a) You must give any other recipients of the Work or
112 | Derivative Works a copy of this License; and
113 |
114 | (b) You must cause any modified files to carry prominent notices
115 | stating that You changed the files; and
116 |
117 | (c) You must retain, in the Source form of any Derivative Works
118 | that You distribute, all copyright, patent, trademark, and
119 | attribution notices from the Source form of the Work,
120 | excluding those notices that do not pertain to any part of
121 | the Derivative Works; and
122 |
123 | (d) If the Work includes a "NOTICE" text file as part of its
124 | distribution, then any Derivative Works that You distribute must
125 | include a readable copy of the attribution notices contained
126 | within such NOTICE file, excluding those notices that do not
127 | pertain to any part of the Derivative Works, in at least one
128 | of the following places: within a NOTICE text file distributed
129 | as part of the Derivative Works; within the Source form or
130 | documentation, if provided along with the Derivative Works; or,
131 | within a display generated by the Derivative Works, if and
132 | wherever such third-party notices normally appear. The contents
133 | of the NOTICE file are for informational purposes only and
134 | do not modify the License. You may add Your own attribution
135 | notices within Derivative Works that You distribute, alongside
136 | or as an addendum to the NOTICE text from the Work, provided
137 | that such additional attribution notices cannot be construed
138 | as modifying the License.
139 |
140 | You may add Your own copyright statement to Your modifications and
141 | may provide additional or different license terms and conditions
142 | for use, reproduction, or distribution of Your modifications, or
143 | for any such Derivative Works as a whole, provided Your use,
144 | reproduction, and distribution of the Work otherwise complies with
145 | the conditions stated in this License.
146 |
147 | 5. Submission of Contributions. Unless You explicitly state otherwise,
148 | any Contribution intentionally submitted for inclusion in the Work
149 | by You to the Licensor shall be under the terms and conditions of
150 | this License, without any additional terms or conditions.
151 | Notwithstanding the above, nothing herein shall supersede or modify
152 | the terms of any separate license agreement you may have executed
153 | with Licensor regarding such Contributions.
154 |
155 | 6. Trademarks. This License does not grant permission to use the trade
156 | names, trademarks, service marks, or product names of the Licensor,
157 | except as required for reasonable and customary use in describing the
158 | origin of the Work and reproducing the content of the NOTICE file.
159 |
160 | 7. Disclaimer of Warranty. Unless required by applicable law or
161 | agreed to in writing, Licensor provides the Work (and each
162 | Contributor provides its Contributions) on an "AS IS" BASIS,
163 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
164 | implied, including, without limitation, any warranties or conditions
165 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
166 | PARTICULAR PURPOSE. You are solely responsible for determining the
167 | appropriateness of using or redistributing the Work and assume any
168 | risks associated with Your exercise of permissions under this License.
169 |
170 | 8. Limitation of Liability. In no event and under no legal theory,
171 | whether in tort (including negligence), contract, or otherwise,
172 | unless required by applicable law (such as deliberate and grossly
173 | negligent acts) or agreed to in writing, shall any Contributor be
174 | liable to You for damages, including any direct, indirect, special,
175 | incidental, or consequential damages of any character arising as a
176 | result of this License or out of the use or inability to use the
177 | Work (including but not limited to damages for loss of goodwill,
178 | work stoppage, computer failure or malfunction, or any and all
179 | other commercial damages or losses), even if such Contributor
180 | has been advised of the possibility of such damages.
181 |
182 | 9. Accepting Warranty or Additional Liability. While redistributing
183 | the Work or Derivative Works thereof, You may choose to offer,
184 | and charge a fee for, acceptance of support, warranty, indemnity,
185 | or other liability obligations and/or rights consistent with this
186 | License. However, in accepting such obligations, You may act only
187 | on Your own behalf and on Your sole responsibility, not on behalf
188 | of any other Contributor, and only if You agree to indemnify,
189 | defend, and hold each Contributor harmless for any liability
190 | incurred by, or claims asserted against, such Contributor by reason
191 | of your accepting any such warranty or additional liability.
192 |
193 | END OF TERMS AND CONDITIONS
194 |
195 | APPENDIX: How to apply the Apache License to your work.
196 |
197 | To apply the Apache License to your work, attach the following
198 | boilerplate notice, with the fields enclosed by brackets "[]"
199 | replaced with your own identifying information. (Don't include
200 | the brackets!) The text should be enclosed in the appropriate
201 | comment syntax for the file format. We also recommend that a
202 | file or class name and description of purpose be included on the
203 | same "printed page" as the copyright notice for easier
204 | identification within third-party archives.
205 |
206 | Copyright [yyyy] [name of copyright owner]
207 |
208 | Licensed under the Apache License, Version 2.0 (the "License");
209 | you may not use this file except in compliance with the License.
210 | You may obtain a copy of the License at
211 |
212 | http://www.apache.org/licenses/LICENSE-2.0
213 |
214 | Unless required by applicable law or agreed to in writing, software
215 | distributed under the License is distributed on an "AS IS" BASIS,
216 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
217 | See the License for the specific language governing permissions and
218 | limitations under the License.
219 |
220 |
221 | =======================================================================
222 |
--------------------------------------------------------------------------------
/README.markdown:
--------------------------------------------------------------------------------
1 | ## Command Line Arguments
2 |
3 | Dean Wampler, Ph.D.
4 | [@deanwampler](https://twitter.com/deanwampler)
5 |
6 | This is a [Scala](http://scala-lang.org) library for handling command-line arguments. It has few dependencies on other libraries, [Parboiled](https://github.com/sirthias/parboiled/wiki/parboiled-for-Scala), for parsing, and [ScalaTest](http://scalatest.org) and [ScalaCheck](http://scalacheck.org), for testing. So its footprint is small.
7 |
8 | ## Usage
9 |
10 | This library is built for Scala 2.11.12 and 2.12.6, the default (2.10 support was dropped in the 0.5.0 release). Artifacts are published to [Sonatype's OSS service](https://oss.sonatype.org/index.html#nexus-search;quick%7Eshapeless). You'll need the following settings.
11 |
12 | ```
13 | resolvers ++= Seq(
14 | Resolver.sonatypeRepo("releases"),
15 | Resolver.sonatypeRepo("snapshots")
16 | )
17 | ...
18 |
19 | scalaVersion := "2.12.6" // or 2.11.8
20 |
21 | libraryDependencies ++= Seq(
22 | "com.concurrentthought.cla" %% "command-line-arguments" % "0.5.0"
23 | "com.concurrentthought.cla" %% "command-line-arguments-examples" % "0.5.0"
24 | )
25 | ```
26 |
27 | The `examples` can be omitted.
28 |
29 | ## API
30 |
31 | The included [com.concurrentthought.cla.CLASampleMain](src/main/scala/com/concurrentthought/cla/CLASampleMain.scala) shows two different idiomatic ways to set up and use the API.
32 |
33 | The simplest approach parses a multi-line string to specify the command-line arguments [com.concurrentthought.cla.Args](src/main/scala/com/concurrentthought/cla/Args.scala):
34 |
35 | ```scala
36 | import com.concurrentthought.cla._
37 |
38 | object CLASampleMain {
39 |
40 | def main(argstrings: Array[String]) = {
41 | val initialArgs: Args = """
42 | |run-main CLASampleMain [options]
43 | |Demonstrates the CLA API.
44 | | -i | --in | --input string Path to input file.
45 | | [-o | --out | --output string=/dev/null] Path to output file.
46 | | [-l | --log | --log-level int=3] Log level to use.
47 | | [-p | --path path] Path elements separated by ':' (*nix) or ';' (Windows).
48 | | [--things seq([-|])] String elements separated by '-' or '|'.
49 | | [-q | --quiet flag] Suppress some verbose output.
50 | | others Other arguments.
51 | |Note that --input and "others" are required.
52 | |""".stripMargin.toArgs
53 |
54 | // Process the input arguments. If help requested or an error occurs,
55 | // a message is written to stdout and the program exits with an error code.
56 | // Default arguments for `process` aren't shown. See also Args#parse() for
57 | // more flexible handling.
58 | val finalArgs: Args = initialArgs.process(argstrings)
59 |
60 | // If here, successfully parsed the args and none where "--help" or "-h".
61 | showResults(finalArgs)
62 | }
63 | ...
64 | ```
65 |
66 | The [Scaladocs comments](src/main/scala/com/concurrentthought/cla/package.scala) for the [cla package](src/main/scala/com/concurrentthought/cla/package.scala) explain the format and its limitations, but hopefully most of the format is reasonable intuitive from the example.
67 |
68 | The first and last lines in the string that *don't* have leading whitespace are interpreted as lines to show as part of the corresponding help message. It's a good idea to use the first line to show an example of how to invoke the program.
69 |
70 | Next come the command-line options, one per line. Each must start with whitespace, followed by zero or more flags separated by `|`. There can be at most one option that has no flags. It is used to provide a help message for how command-line tokens that aren't associated with flags will be interpreted. (Note that the library will still handle these tokens whether or not you specify a line like this.)
71 |
72 | To indicate that an option can be omitted by the user (i.e., it's truly _optional_), the flags and name must be wrapped in `[...]`. Otherwise, the user must specify the option explicitly on the command line. However, if a default value is specified (discussed next), it makes an option _optional_ anyway. The purpose of the optional feature is to indicate to the user which arguments are required and to automatically report missing arguments as errors.
73 |
74 | In this example, all are optional except for the `--input` and `others` arguments.
75 |
76 | The center "column" specifies the type of the option. All but the `flag` and `~flag` types accept an optional default value, which is indicated with an equals `=` sign. The following "types" are supported:
77 |
78 | | String | Interpretation | Corresponding Helper Method |
79 | | -------: | :-------------- | :----------------------------- |
80 | | `flag` | `Boolean` value | [Opt.flag](src/main/scala/com/concurrentthought/cla/Opt.scala) |
81 | | `~flag` | `Boolean` value | [Opt.flag](src/main/scala/com/concurrentthought/cla/Opt.scala) |
82 | | `string` | `String` value | [Opt.string](src/main/scala/com/concurrentthought/cla/Opt.scala) |
83 | | `byte` | `Byte` value | [Opt.byte](src/main/scala/com/concurrentthought/cla/Opt.scala) |
84 | | `char` | `Char` value | [Opt.char](src/main/scala/com/concurrentthought/cla/Opt.scala) |
85 | | `int` | `Int` value | [Opt.int](src/main/scala/com/concurrentthought/cla/Opt.scala) |
86 | | `long` | `Long` value | [Opt.long](src/main/scala/com/concurrentthought/cla/Opt.scala) |
87 | | `float` | `Float` value | [Opt.float](src/main/scala/com/concurrentthought/cla/Opt.scala) |
88 | | `double` | `Double` value | [Opt.double](src/main/scala/com/concurrentthought/cla/Opt.scala) |
89 | | `seq` | `Seq[String]` [1] | [Opt.seqString](src/main/scala/com/concurrentthought/cla/Opt.scala) |
90 | | `path` | "path-like" `Seq[String]` [1] | [Opt.path](src/main/scala/com/concurrentthought/cla/Opt.scala) |
91 | | *other* | Only allowed for the single, no-flags case | [Args.remainingOpt](src/main/scala/com/concurrentthought/cla/Args.scala) |
92 |
93 |
94 | 1: Both `path` and `seq` split an argument using the delimiter regular expression. For `path`, this is the platform-specific path separator, given by `sys.props.getOrElse("path.separator", ":")`. It is designed for class paths, etc. For `seq`, you must provide the delimiter regular expression using a suffix of the form `(delimRE)`. In the example above, the regex is `[-|]` (split on either `-` or `|`).
95 |
96 | Both `flag` and `~flag` represent `Boolean` flags where no default value can be supplied (e.g., `--help`). The value corresponding to a `flag` defaults to `false` if the user doesn't invoke the flag on the command line, `~flag` ("tilde" or "not" flag) defaults to `true`.
97 |
98 | So, when an option expects something other than a `String`, the token given on the command line (or as a default value) will be parsed into the correct type, with error handling captured in the [Args.failures](src/main/scala/com/concurrentthought/cla/Args.scala) field.
99 |
100 | Finally, the rest of the text on a line is the help message for the option.
101 |
102 | Before discussing the `process` method shown, let's see two alternative, programmatic ways to declare [Args](src/main/scala/com/concurrentthought/cla/Args.scala) using the API:
103 |
104 | ```scala
105 | ...
106 | def main2(argstrings: Array[String]) = {
107 | val input = Opt.string(
108 | name = "input",
109 | flags = Seq("-i", "--in", "--input"),
110 | help = "Path to input file.",
111 | requiredFlag = true)
112 | val output = Opt.string(
113 | name = "output",
114 | flags = Seq("-o", "--out", "--output"),
115 | default = Some("/dev/null"),
116 | help = "Path to output file.")
117 | val logLevel = Opt.int(
118 | name = "log-level",
119 | flags = Seq("-l", "--log", "--log-level"),
120 | default = Some(3),
121 | help = "Log level to use.")
122 | val path = Opt.path(
123 | name = "path",
124 | flags = Seq("-p", "--path"))
125 | val things = Opt.seqString(delimsRE = "[-|]")(
126 | name = "things",
127 | flags = Seq("--things"),
128 | help = "String elements separated by '-' or '|'.")
129 | val others = Args.makeRemainingOpt(
130 | name = "others",
131 | help = "Other arguments",
132 | requiredFlag = true)
133 |
134 | val initialArgs = Args(
135 | "run-main CLASampleMain [options]",
136 | "Demonstrates the CLA API.",
137 | """Note that --input and "others" are required.""",
138 | Seq(input, output, logLevel, path, things, Args.quietFlag, others))
139 |
140 | val finalArgs: Args = initialArgs.process(argstrings)
141 | showResults(finalArgs)
142 | }
143 | ...
144 | }
145 | ```
146 |
147 | Each option is defined using a [com.concurrentthought.cla.Opt](src/main/scala/com/concurrentthought/cla/Opt.scala) value. In this case, there are helper methods in the `Opt` companion object for constructing options where the values are strings or numbers. The `string` and `int` helpers are used here for `String` and `Int` arguments, respectively).
148 |
149 | The arguments to each of these helpers (and also for `Opt[V].apply()` that they invoke) is the option name, used to retrieve the value later, a `Seq` of flags for command line invocation, an optional default value if the command-line argument isn't used (defaults to `None`), a help string (defaults to ""), and a boolean flag indicating whether or not the "option" is required (defaults to `false`, which is sort of the opposite behavior of the string DSL discussed previously).
150 |
151 | There are also two helpers for command-line arguments that are strings that contain sequences of elements. We use one of them here, `seqString`, for a classpath-style argument, where the elements will be split into a `Seq[String]`, using `:` and `;` as delimiters; the first argument is a regular expression for the delimiter. If you want to support a path-like option, e.g., a `CLASSPATH`, there is another, even more specific helper, `Opt.path`, that handles the platform-specific value for the path-element separator.
152 |
153 | There is also a more general `seq[V]` helper, where the string is first split, then parsed into `V` instances. See [Opt.seq[V]](src/main/scala/com/concurrentthought/cla/Opt.scala) for more details.
154 |
155 | The first two arguments to the `Args.apply()` method provide help strings. The first shows how to run the application, e.g., `run-main CLASampleMain` as shown, or perhaps `java -cp ... foo.bar.Main`, etc. The string is arbitrary. The second string is an optional description of the program. Finally, a `Seq[Opt[V]]` specifies the actual options supported. Note that we didn't define a `Flag` for quiet, as in the first example, instead we used a built-in flag `Args.quietFlag`.
156 |
157 | Here is a slightly more concise way to write the content in `main2`:
158 |
159 | ```scala
160 | ...
161 | def main3(argstrings: Array[String]) = {
162 | import Opt._
163 | import Args._
164 | val initialArgs = Args(
165 | "run-main CLASampleMain [options]",
166 | "Demonstrates the CLA API.",
167 | """Note that --input and "others" are required.""",
168 | Seq(
169 | string("input", Seq("-i", "--in", "--input"), None, "Path to input file."),
170 | string("output", Seq("-o", "--out", "--output"), Some("/dev/null"), "Path to output file."),
171 | int( "log-level", Seq("-l", "--log", "--log-level"), Some(3), "Log level to use."),
172 | path( "path", Seq("-p", "--path"), None),
173 | seqString("[:;]")(
174 | "things", Seq("--things"), None, "String elements separated by '-' or '|'."),
175 | Args.quietFlag,
176 | makeRemainingOpt(
177 | "others", "Other arguments", true)))
178 |
179 | val finalArgs: Args = initialArgs.process(argstrings)
180 | showResults(finalArgs)
181 | }
182 | ...
183 | ```
184 |
185 | This is more concise, but perhaps harder to follow.
186 |
187 | The [Args#process](src/main/scala/com/concurrentthought/cla/Args.scala) first calls `Args#parse` on the user-specified arguments, which returns a new `Args` instance with updated values for each argument. However, if an error occurs or help is requested, `process` automatically prints a message and exits. This behavior is configurable by overriding default arguments. See also `Args#parse()` for more flexible handling.
188 |
189 | You'll almost always want to include logic like this in your code that uses this library.
190 |
191 | If `--quiet` wasn't specified, then you might print information about the argument values. We demonstrate this in the `CLASampleMain` program. where the `showResults` method prints all the options and the current values for them, either the defaults or the user-specified values.
192 |
193 | ```
194 | protected def showResults(parsedArgs: Args): Unit = {
195 | if (parsedArgs.getOrElse("quiet", false)) {
196 | println("(... I'm being very quiet...)")
197 | } else {
198 | // Print all the default values or those specified by the user.
199 | parsedArgs.printValues()
200 |
201 | // Print all the values including repeats.
202 | parsedArgs.printAllValues()
203 |
204 | // Repeat the "other" arguments (not associated with flags).
205 | println("\nYou gave the following \"other\" arguments: " +
206 | parsedArgs.remaining.mkString(", "))
207 | ...
208 | ```
209 |
210 | What's the difference between `printValues` and `printAllValues`. They address the case where the user should be able to repeat some options, for example, multiple sources of input, while other examples should only be used once. To simplify handling, the API remembers all occurrences of an option on the command line. The method `printAllValues` and the corresponding `getAll` and `getAllOrElse` methods print or return all occurrences seen, respectively. So, if you want an option to be repeatable, retrieve the results with `getAll` or `getAllOrElse`. Otherwise, use `get` and `getOrElse`, which return the *last* occurrence of an option (or the default, if any). This supports the common practice in POSIX systems of allowing subsequent option occurrences to override previous occurrences on a command line.
211 |
212 | Finally, we extract some other values and "use" them.
213 |
214 | ```
215 | ...
216 | showPathElements(parsedArgs.get[Seq[String]]("path"))
217 | showLogLevel(parsedArgs.getOrElse("log-level", 0))
218 | println
219 | }
220 | }
221 |
222 | protected def showPathElements(path: Option[Seq[String]]) = path match {
223 | case None => println("No path elements to show!")
224 | case Some(seq) => println(s"Setting path elements to $seq")
225 | }
226 |
227 | protected def showLogLevel(level: Int) =
228 | println(s"New log level: $level")
229 | }
230 | ```
231 |
232 | The `get[V]` method returns values of the expected type. It uses `asInstanceOf[]` internally, but it should never fail because the parsing process already converted the value to the correct type (and then put it in a `Map[String,Any]` used by `get[V]`).
233 |
234 | Note that an advantage of `getOrElse[V]` is that its type parameter can be inferred due to the second argument.
235 |
236 | Try running the following examples within SBT (`run` and `run-main com.concurrentthought.cla.CLASampleMain` do the same thing):
237 |
238 | ```
239 | run-main com.concurrentthought.cla.CLASampleMain -h
240 | run -h
241 | run --help
242 | run -i /in -o /out -l 4 -p a:b --things x-y|z foo bar baz
243 | run -i /in -o /out -l 4 -p a:b --things x-y|z foo bar baz --quiet
244 | run --in /in --out=/out -l=4 --path "a:b" --things=x-y|z foo bar baz
245 | ```
246 |
247 | The last example mixes `argflag value` and `argflag=value` syntax, which of are both supported.
248 |
249 | Try a few runs with unknown flags and other errors. Note the error handling that's done, such as when you omit a value expected by a flag, or you provide an invalid value, such as `--log-level foo`.
250 |
--------------------------------------------------------------------------------
/build.sbt:
--------------------------------------------------------------------------------
1 | // Many details adapted from the Cats build: https://github.com/non/cats
2 | import com.typesafe.sbt.pgp.PgpKeys.publishSigned
3 | import com.typesafe.sbt.SbtGit.GitKeys._
4 | import com.typesafe.sbt.SbtSite.SiteKeys._
5 | import ReleaseTransformations._
6 | import ScoverageSbtPlugin._
7 |
8 | val scala211 = "2.11.12"
9 | val scala212 = "2.12.6"
10 | val scalaDefaultVersion = scala212
11 |
12 | lazy val buildSettings = Seq(
13 | organization := "com.concurrentthought.cla",
14 | description := "A library for handling command-line arguments.",
15 |
16 | scalaVersion := scalaDefaultVersion,
17 | crossScalaVersions := Seq(scala212, scala211),
18 |
19 | maxErrors := 5,
20 | triggeredMessage := Watched.clearWhenTriggered,
21 |
22 | scalacOptions in Compile := commonScalacOptions ++ {
23 | CrossVersion.partialVersion(scalaVersion.value) match {
24 | case Some((2, 10)) =>
25 | Seq()
26 | case Some((2, 11)) =>
27 | Seq("-Ywarn-infer-any", "-Ywarn-unused-import", "-language:existentials")
28 | case Some((2, 12)) =>
29 | Seq("-Ywarn-infer-any", "-Ywarn-unused-import")
30 | case Some(_) | None =>
31 | Seq() // should never happen!
32 | }
33 | },
34 | scalacOptions in (Compile, console) := minScalacOptions,
35 | // scalacOptions in (Compile, console) ~= {_.filterNot("-Ywarn-unused-import" == _)},
36 | // scalacOptions in (Test, console) ~= {_.filterNot("-Ywarn-unused-import" == _)},
37 | scalacOptions in (ScalaUnidoc, unidoc) += "-Ymacro-expand:none",
38 |
39 | libraryDependencies ++= Seq(
40 | "org.parboiled" %% "parboiled-scala" % "1.1.8",
41 | "org.scalatest" %% "scalatest" % "3.0.0" % "test",
42 | "org.scalacheck" %% "scalacheck" % "1.13.4" % "test"),
43 |
44 | fork in console := true
45 | )
46 |
47 | lazy val scoverageSettings = Seq(
48 | coverageMinimum := 60,
49 | coverageFailOnMinimum := false,
50 | coverageHighlighting := scalaBinaryVersion.value != "2.10",
51 | coverageExcludedPackages := "com\\.concurrentthought\\.cla\\.examples\\..*"
52 | )
53 |
54 | lazy val minScalacOptions = Seq(
55 | "-deprecation",
56 | "-unchecked",
57 | "-feature",
58 | "-encoding", "utf8")
59 |
60 | lazy val commonScalacOptions = minScalacOptions ++ Seq(
61 | "-Xfatal-warnings",
62 | "-Xlint",
63 | "-Xfuture",
64 | // "-Yinline-warnings",
65 | "-Yno-adapted-args",
66 | "-Ywarn-dead-code",
67 | "-Ywarn-numeric-widen",
68 | "-Ywarn-value-discard")
69 |
70 | lazy val sharedPublishSettings = Seq(
71 | releaseCrossBuild := true,
72 | releaseTagName := version.value,
73 | releasePublishArtifactsAction := PgpKeys.publishSigned.value,
74 | publishMavenStyle := true,
75 | publishArtifact in Test := false,
76 | pomIncludeRepository := Function.const(false),
77 | publishTo := {
78 | val nexus = "https://oss.sonatype.org/"
79 | if (isSnapshot.value)
80 | Some("Snapshots" at nexus + "content/repositories/snapshots")
81 | else
82 | Some("Releases" at nexus + "service/local/staging/deploy/maven2")
83 | }
84 | )
85 |
86 | lazy val sharedReleaseProcess = Seq(
87 | releaseProcess := Seq[ReleaseStep](
88 | checkSnapshotDependencies,
89 | inquireVersions,
90 | runClean, // disabled to reduce memory usage during release
91 | runTest,
92 | setReleaseVersion,
93 | commitReleaseVersion,
94 | tagRelease,
95 | publishArtifacts,
96 | setNextVersion,
97 | commitNextVersion,
98 | releaseStepCommand("sonatypeReleaseAll"),
99 | pushChanges)
100 | )
101 |
102 | lazy val publishSettings = Seq(
103 | homepage := Some(url("https://github.com/deanwampler/command-line-arguments")),
104 | licenses := Seq("Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0")),
105 | scmInfo := Some(ScmInfo(
106 | url("https://github.com/deanwampler/command-line-arguments"),
107 | "scm:git:git@github.com:deanwampler/command-line-arguments.git")),
108 | // apiURL := Some(url("...")) // TODO
109 | autoAPIMappings := true,
110 | pomExtra := (
111 |
112 |
113 | deanwampler
114 | Dean Wampler
115 | http://concurrentthought.com
116 |
117 |
118 | ),
119 | credentials += Credentials(Path.userHome / ".sonatype" / ".credentials")
120 | ) ++ sharedPublishSettings ++ sharedReleaseProcess
121 |
122 | lazy val root = project.in(file("."))
123 | .enablePlugins(ScalaUnidocPlugin, GhpagesPlugin)
124 | .settings(
125 | name := "root",
126 | siteSubdirName in ScalaUnidoc := "latest/api",
127 | addMappingsToSiteDir(mappings in (ScalaUnidoc, packageDoc), siteSubdirName in ScalaUnidoc),
128 | gitRemoteRepo := "git@github.com:deanwampler/command-line-arguments.git",
129 | skip in publish := true
130 | )
131 | .settings(buildSettings)
132 | .aggregate(core, examples)
133 |
134 | lazy val core = project.in(file("core"))
135 | .settings(name := "command-line-arguments")
136 | .settings(buildSettings ++ scoverageSettings)
137 | .settings(publishSettings)
138 |
139 | lazy val examples = project.in(file("examples"))
140 | .settings(name := "command-line-arguments-examples")
141 | .settings(buildSettings)
142 | .settings(publishSettings)
143 | .dependsOn(core)
144 |
145 | addCommandAlias("validate", ";scalastyle;test")
146 |
147 | initialCommands += """
148 | import com.concurrentthought.cla._
149 | import com.concurrentthought.cla.examples._
150 | """
151 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/concurrentthought/cla/Args.scala:
--------------------------------------------------------------------------------
1 | package com.concurrentthought.cla
2 | import scala.util.control.NonFatal
3 | import scala.util.{Try, Success, Failure}
4 | import java.io.PrintStream
5 |
6 | /**
7 | * Contains the options defined, the current values, which are the defaults
8 | * before parsing and afterwards are the defaults overridden by the actual
9 | * invocation options, and contains any parsing errors that were found, which
10 | * is empty before parse is called. In order to properly construct the default
11 | * values, this constructor is protected. Instead, use the companion Args.apply
12 | * to construct initial instances correctly. Subsequent calls to `Args.parse` return
13 | * new, updated instances.
14 | */
15 | case class Args protected (
16 | programInvocation: String,
17 | leadingComments: String,
18 | trailingComments: String,
19 | opts: Seq[Opt[_]],
20 | defaults: Map[String,Any],
21 | values: Map[String,Any],
22 | allValues: Map[String,Seq[Any]],
23 | remaining: Seq[String],
24 | failures: Seq[(String,Any)]) {
25 |
26 | def help: String = Help(this)
27 |
28 | val requiredOptions = opts.filter(o => o.required)
29 |
30 | // We want the unknownOptionMatch to be just before the "remaining" option.
31 | protected val remainingOpt = opts.find(_.flags.size == 0).get
32 | protected val remainingOptName = remainingOpt.name
33 | protected val opts2: Seq[Opt[_]] = opts.filter(_.name != remainingOptName)
34 |
35 | lazy val parserChain: Opt.Parser[_] =
36 | opts2.map(_.parser) reduceLeft (_ orElse _) orElse unknownOptionMatch orElse remainingOpt.parser
37 |
38 | /**
39 | * Convenience method to parse the argument list and handle errors or
40 | * help requests.
41 | * If a parsing error occurs or help is requested, an appropriate message
42 | * is printed to the `out` argument and the program exits with a call to
43 | * `sys.exit(n)` with the integer exit returned by the sister `Args#process`
44 | * method. Normally, the default values for the `out` and `exit` arguments
45 | * are only overridden for testing.
46 | * For more customized handling, {@see #parse}.
47 | * @return Args
48 | */
49 | def process(
50 | args: Seq[String],
51 | out: PrintStream = Console.out,
52 | exit: Int => Unit = n => sys.exit(n)): Args = {
53 | val newArgs = parse(args)
54 | if (newArgs.handleHelp(out)) exit(0)
55 | else if (newArgs.handleErrors(out)) exit(1)
56 | newArgs
57 | }
58 |
59 | /**
60 | * Parse the user-specified arguments, using `parserChain`. Note that if
61 | * an unrecognized flag is found, i.e., a string that starts with one or two
62 | * '-', it is an error. Otherwise, all unrecognized options are added to the
63 | * resulting `values` in a `Seq[String]` with the key, "remaining".
64 | * @return Args
65 | */
66 | def parse(args: Seq[String]): Args = {
67 | def p(args2: Seq[String]): Seq[(String, Any)] = args2 match {
68 | case Nil => Nil
69 | case seq: Any => try {
70 | parserChain(seq) match {
71 | case ((flag, Success(value)), tail) => (flag, value) +: p(tail)
72 | case ((flag, Failure(failure)), tail) => (flag, failure) +: p(tail)
73 | }
74 | } catch {
75 | case e @ Args.UnrecognizedArgument(head, tail) => (head, e) +: p(tail)
76 | // Otherwise, assume that attempting to parse the value failed, but
77 | // perhaps it's the next option?
78 | case NonFatal(nf) => (seq.head, nf) +: p(seq.tail)
79 | }
80 | }
81 |
82 | val (failures, successes) = p(args) partition {
83 | case (_, NonFatal(nf)) => true
84 | case _ => false
85 | }
86 |
87 | // The "remaining" values aren't included in the values and allValues maps,
88 | // but handled separately.
89 | val newAllValues = allValues ++ successes.foldLeft(Map.empty[String,Vector[Any]]){
90 | case (map, (key, value)) =>
91 | val newVect = map.get(key) match {
92 | case None => Vector(value)
93 | case Some(v) => v :+ value
94 | }
95 | map + (key -> newVect)
96 | } - remainingOptName
97 | val newValues = values ++ successes.toMap - remainingOptName
98 | // The remaining defaults are replaced by the new tokens:
99 | val newRemaining = successes.filter(_._1 == remainingOptName).map(_._2.toString).toVector
100 |
101 | val failures2 = resolveFailures(successes.map(_._1), failures)
102 | copy(values = newValues, allValues = newAllValues, remaining = newRemaining, failures = failures2)
103 | }
104 |
105 | /** Ignore all parse errors if help was requested */
106 | protected def resolveFailures(keys: Seq[String], failures: Seq[(String,Any)]): Seq[(String,Any)] = {
107 | if (keys.contains(Args.HELP_KEY)) Nil
108 | else {
109 | val missing = requiredOptions.filter(o => !keys.contains(o.name))
110 | if (missing.size == 0) failures
111 | else {
112 | val missingOpts = requiredOptions.filter(o => missing.contains(o))
113 | failures ++ missingOpts.map(o => (o.name, Args.MissingRequiredArgument(o)))
114 | }
115 | }
116 | }
117 |
118 | import scala.reflect.ClassTag
119 |
120 |
121 | /**
122 | * Return the value for the option. This will be either the default specified,
123 | * if the user did not invoke the option, or the _last_ invocation of the
124 | * command line option. In other words, if the argument list contains
125 | * `--foo bar1 --foo bar2`, then `Some("bar2")` is returned.
126 | * Note: Use `remaining` to get the tokens not associated with a flag.
127 | * @see getAll
128 | */
129 | def get[V : ClassTag](flag: String): Option[V] =
130 | values.get(flag).map(_.asInstanceOf[V])
131 |
132 | /**
133 | * Like `get`, but an alternative is specified, if no value for the option
134 | * exists, so the return value is of type `V`, rather than `Option[V]`.
135 | * Note: Use `remaining` to get the tokens not associated with a flag.
136 | */
137 | def getOrElse[V : ClassTag](flag: String, orElse: V): V =
138 | values.getOrElse(flag, orElse).asInstanceOf[V]
139 |
140 | /**
141 | * Return a `Seq` with all values specified for the option. This supports
142 | * the case where an option can be repeated on the command line.
143 | * If the user did not specify the option, then default is mapped to a
144 | * return value as follows:
145 | *
146 | * - `None` => `Nil`
147 | *
- `Some(x)` => `Seq(x)`
148 | *
149 | * If the user specified one or more invocations, then all of the values
150 | * are returned in `Seq`. For example, for `--foo bar1 --foo bar2`, then
151 | * this method returns `Seq("bar1", "bar2")`.
152 | * Note: Use `remaining` to get the tokens not associated with a flag.
153 | * @see get
154 | */
155 | def getAll[V : ClassTag](flag: String): Seq[V] =
156 | allValues.getOrElse(flag, Nil).map(_.asInstanceOf[V])
157 |
158 | /**
159 | * Like `getAll`, but an alternative is specified, if no value for exists.
160 | * Note: Use `remaining` to get the tokens not associated with a flag.
161 | */
162 | def getAllOrElse[V : ClassTag](flag: String, orElse: Seq[V]): Seq[V] =
163 | allValues.getOrElse(flag, orElse).map(_.asInstanceOf[V])
164 |
165 | /**
166 | * Print the current values. Before any parsing is done, the values are
167 | * the defaults. After parsing, they are the defaults overridden by any
168 | * user-supplied options. If an option is specified multiple times, then
169 | * the _last_ invocation is shown.
170 | * Note that the "remaining" arguments are the same in this output and in
171 | * `printAllValues`.
172 | * @see printAllValues
173 | */
174 | def printValues(out: PrintStream = Console.out): Unit =
175 | doPrintValues(out, "")(
176 | key => values.getOrElse(key, ""))
177 |
178 | /**
179 | * Print all the current values. Before any parsing is done, the values are
180 | * the defaults. After parsing, they are the defaults overridden by all the
181 | * user-supplied options. If an option is specified multiple times, then
182 | * all values are shown.
183 | * @see printValues
184 | */
185 | def printAllValues(out: PrintStream = Console.out): Unit =
186 | doPrintValues(out, " (all values given)")(
187 | key => allValues.getOrElse(key, Vector.empty[String]))
188 |
189 | private def doPrintValues[V](out: PrintStream, suffix: String)(get: String => V): Unit = {
190 | out.println(s"\nCommand line arguments$suffix:")
191 | val keys = opts.map(_.name)
192 | val max = keys.maxBy(_.size).size
193 | val fmt = s" %${max}s: %s"
194 | keys.filter(_ != remainingOptName).foreach(key => out.println(fmt.format(key, get(key))))
195 | out.println(fmt.format(remainingOptName, remaining))
196 | out.println()
197 | }
198 |
199 |
200 | /**
201 | * Was the help option invoked?
202 | * If so, print the help message to the output `PrintStream` and return true.
203 | * Otherwise, return false. Callers may wish to exit if true is returned.
204 | */
205 | def handleHelp(out: PrintStream = Console.out): Boolean =
206 | get[Boolean](Args.HELP_KEY) match {
207 | case Some(true) => out.println(help); true
208 | case _ => false
209 | }
210 |
211 | /**
212 | * Were errors found in the argument list?
213 | * If so, print the error messages, followed by the help message and return true.
214 | * Otherwise, return false. Callers may wish to exit if true is returned.
215 | */
216 | def handleErrors(out: PrintStream = Console.err): Boolean =
217 | if (failures.size > 0) {
218 | out.println(help)
219 | true
220 | }
221 | else false
222 |
223 | protected val unknownOptionRE = "(--?.+)".r
224 |
225 | /** Unknown option that starts with one or two '-' matches! */
226 | protected val unknownOptionMatch: Opt.Parser[Any] = {
227 | case unknownOptionRE(flag) +: tail => throw Args.UnrecognizedArgument(flag, tail)
228 | }
229 |
230 | override def toString: String = s"""Args:
231 | | program invocation: $programInvocation
232 | | leading comments: $leadingComments
233 | | trailing comments: $trailingComments
234 | | opts: $opts
235 | | defaults: $defaults
236 | | values: $values
237 | | allValues: $allValues
238 | | remaining: $remaining
239 | | failures: $failures
240 | |""".stripMargin
241 |
242 | }
243 |
244 | object Args {
245 |
246 | val HELP_KEY = "help"
247 | val REMAINING_KEY = "remaining"
248 |
249 | val defaultProgramInvocation: String = "java -cp ..."
250 | val defaultComments: String = ""
251 |
252 | def empty: Args = {
253 | apply(Args.defaultProgramInvocation, Args.defaultComments, Args.defaultComments, Nil)
254 | }
255 |
256 | def apply(opts: Seq[Opt[_]]): Args = {
257 | apply(Args.defaultProgramInvocation, Args.defaultComments, Args.defaultComments, opts)
258 | }
259 |
260 | def apply(programInvocation: String, opts: Seq[Opt[_]]): Args = {
261 | apply(programInvocation, Args.defaultComments, Args.defaultComments, opts)
262 | }
263 |
264 | def apply(
265 | programInvocation: String,
266 | leadingComments: String,
267 | trailingComments: String,
268 | opts: Seq[Opt[_]]): Args = {
269 | def defs = defaults(opts)
270 | apply(programInvocation, leadingComments, trailingComments, opts, defs, defs)
271 | }
272 |
273 | def apply(
274 | programInvocation: String,
275 | leadingComments: String,
276 | trailingComments: String,
277 | opts: Seq[Opt[_]],
278 | defaults: Map[String,Any]): Args =
279 | apply(programInvocation, leadingComments, trailingComments, opts, defaults, defaults)
280 |
281 | def apply(
282 | programInvocation: String,
283 | leadingComments: String,
284 | trailingComments: String,
285 | opts: Seq[Opt[_]],
286 | defaults: Map[String,Any],
287 | values: Map[String,Any]): Args = {
288 |
289 | val noFlagOpts = opts.filter(_.flags.size == 0)
290 | require(noFlagOpts.size <= 1, "At most one option can have no flags, used for all command-line tokens not associated with flags.")
291 |
292 | // Add opts or help at the beginning and "remaining" (no flag) tokens at
293 | // the end, if necessary. Also, add defaults and values for the extra
294 | // options, if needed.
295 | var opts1 = opts.toVector
296 | var defaults1 = defaults
297 | var values1 = values
298 | var remaining1 = Vector.empty[String]
299 | if (opts1.exists(_.name == HELP_KEY) == false) { // scalastyle:ignore
300 | opts1 = helpFlag +: opts1
301 | val hf = (HELP_KEY -> false)
302 | defaults1 = defaults1 + hf
303 | values1 = values1 + hf
304 | }
305 | if (noFlagOpts.size == 0) {
306 | opts1 = opts1 :+ remainingOpt
307 | } else {
308 | // Make sure the remaining values aren't in "defaults1" or "values1", but update "remaining1"
309 | val noFlagName = noFlagOpts.head.name
310 | defaults1 -= noFlagName
311 | values1 -= noFlagName
312 | remaining1 = noFlagOpts.head.default match {
313 | case Some(s) => s match {
314 | case s: Seq[_] => s.map(_.toString).toVector // _ should already be String, but erasure...
315 | case x: Any => Vector(x.toString)
316 | }
317 | case None => Vector.empty[String]
318 | }
319 | }
320 | val allValues1 = values1.map{ case (k,v) => (k,Vector(v)) }
321 | val failures1 = Seq.empty[(String,Any)]
322 | new Args(programInvocation, leadingComments, trailingComments,
323 | opts1, defaults1, values1, allValues1, remaining1, failures1)
324 | }
325 |
326 | // Common options.
327 |
328 | /** Show Help. Normally the program will exit afterwards. */
329 | val helpFlag = Opt.flag(
330 | name = HELP_KEY,
331 | flags = Seq("-h", "--h", "--help"),
332 | help = "Show this help message.")
333 |
334 | /** Minimize logging and other output. */
335 | val quietFlag = Opt.flag(
336 | name = "quiet",
337 | flags = Seq("-q", "--quiet"),
338 | help = "Suppress some verbose output.")
339 |
340 | /**
341 | * A special option for "remaining" or "bare" tokens that aren't associated with a flag.
342 | * Note that it has no flags; only one such option is allowed in an `Args`.
343 | */
344 | def makeRemainingOpt(
345 | name: String = REMAINING_KEY,
346 | help: String = "All remaining arguments that aren't associated with flags.",
347 | requiredFlag: Boolean = false): Opt[String] =
348 | new Opt[String](name = name, flags = Nil, help = help, requiredFlag = requiredFlag)(s => Try(s)) {
349 |
350 | /** Now there are no flags expected as the first token. */
351 | override val parser: Opt.Parser[String] = {
352 | case value +: tail => ((name, Success(value)), tail)
353 | }
354 | }
355 |
356 | val remainingOpt = makeRemainingOpt()
357 |
358 | /** Socket host and port. */
359 | def socketOpt(default: Option[(String, Int)] = None,
360 | required: Boolean = false): Opt[(String,Int)] = Opt[(String,Int)](
361 | name = "socket",
362 | flags = Seq("-s", "--socket"),
363 | default = default,
364 | help = "Socket host:port.",
365 | requiredFlag = required) { s =>
366 | val array = s.split(":")
367 | if (array.length != 2) Failure(Opt.InvalidValueString("--socket", s))
368 | else {
369 | val host = array(0)
370 | Try(array(1).toInt) match {
371 | case Success(port) => Success(host -> port)
372 | case Failure(th) => Failure(Opt.InvalidValueString("--socket", s"$s (not an int?)", Some(th)))
373 | }
374 | }
375 | }
376 |
377 | case class MissingRequiredArgument[T](o: Opt[T])
378 | extends RuntimeException("") {
379 |
380 | override def toString: String =
381 | s"""Missing required argument: "${o.name}"${flagsString} ${o.help}"""
382 |
383 | protected def flagsString =
384 | if (o.flags.size == 0) ""
385 | else s""" with flags ${o.flags.mkString(" | ")},"""
386 | }
387 |
388 | case class UnrecognizedArgument(arg: String, rest: Seq[String])
389 | extends RuntimeException("") {
390 | override def toString: String =
391 | s"Unrecognized argument (or missing value): $arg ${restOfArgs(rest)}"
392 |
393 | private def restOfArgs(rest: Seq[String]) =
394 | if (rest.size == 0) "(end of arguments)" else s"""(rest of arguments: ${rest.mkString(" ")})"""
395 | }
396 |
397 | def defaults(opts: Seq[Opt[_]]): Map[String,Any] =
398 | opts.filter(_.default != None).map(o => (o.name, o.default.get)).toMap
399 | }
400 |
401 |
402 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/concurrentthought/cla/Help.scala:
--------------------------------------------------------------------------------
1 | package com.concurrentthought.cla
2 |
3 | /**
4 | * Format a help message for the command-line invocation of a program, based
5 | * on the `Args` object passed to `apply`.
6 | */
7 | object Help {
8 | /** Arbitrary maximum width for the descriptive string after the options. */
9 | val maxHelpWidth = 60
10 |
11 | /**
12 | * Return the help string for the given `Args`.
13 | * Note that the formatted help message will include the default values for the
14 | * options, when they are defined.
15 | */
16 | def apply(args: Args): String = {
17 | val lines = Vector(s"Usage: ${args.programInvocation} [options]", args.leadingComments) ++
18 | errorsHelp(args) ++
19 | Vector("Where the supported options are the following:", "") ++
20 | argsHelp(args) ++ Vector("", trailing(args), args.trailingComments)
21 | (for { s <- lines } yield s).mkString("", "\n", "\n")
22 | }
23 |
24 | protected def errorsHelp(args: Args): Vector[String] =
25 | if (args.failures.size == 0) Vector.empty
26 | else "The following parsing errors occurred:" +:
27 | args.failures.map{ case (flag, err) => s" $err" }.toVector
28 |
29 | protected def argsHelp(args: Args): Vector[String] = {
30 | val strings = args.opts.map(o => (toFlagsHelp(o), toHelp(o)))
31 | val maxFlagLen = strings.map(_._1).maxBy(_.size).size
32 | val fmt = s"%-${maxFlagLen}s %s"
33 | strings.foldLeft(Vector.empty[String]) {
34 | case (vect, (flags, hlp)) =>
35 | vect ++ hlp.zipWithIndex.map {
36 | case (h, i) => fmt.format(if (i==0) flags else "", h)
37 | }
38 | }
39 | }
40 |
41 | protected def toFlagsHelp(opt: Opt[_]): String = {
42 | val prefix = " "
43 | val valueName = opt match {
44 | case f: Opt.Flag => ""
45 | case _ => opt.name
46 | }
47 | val s = opt.flags.mkString(" | ")
48 | val (pre, suf) = if (!opt.required) ("[", "]") else (" ", " ")
49 | if (s.trim.length == 0) prefix+pre+valueName+suf
50 | else if (valueName.length > 0) prefix+pre+s+prefix+valueName+suf
51 | else prefix+pre+s+suf
52 | }
53 |
54 | protected def toHelp(opt: Opt[_]): Vector[String] = {
55 | val h = opt.help
56 | val hs = if (h.length > Help.maxHelpWidth) wrap(h) else Vector(h)
57 | opt.default match {
58 | case None => hs
59 | case Some(d) => d match {
60 | case b: Boolean => hs // suppress!
61 | case _ => hs ++ Vector(s"(default: ${d})")
62 | }
63 | }
64 | }
65 |
66 | /**
67 | * Add a trailing message about the alternative syntax, but only if there are
68 | * actually options that have flags and values, i.e., that aren't `Flags` and
69 | * aren't the special case option for tokens without a flag.
70 | * A bit of a hack...
71 | */
72 | protected def trailing(args: Args): String =
73 | if (args.opts.exists(o => o.isInstanceOf[Opt.Flag] == false && o.flags != Nil)) {
74 | "You can also use --foo=bar syntax. Arguments shown in [...] are optional. All others are required."
75 | } else ""
76 |
77 | protected def wrap(s: String): Vector[String] = {
78 | s.foldLeft((Vector.empty[String], 0, "")) {
79 | // Hit whitespace? If so, are we within 4 pos. of the max?
80 | case ((vect, pos, string), c) if pos > (Help.maxHelpWidth - 4) && c.isWhitespace =>
81 | (vect :+ string, 0, "") // start new string!
82 | // Are we starting a new string, but parsing whitespace? Skip it.
83 | case ((vect, pos, ""), c) if c.isWhitespace => (vect, pos, "")
84 | // Normal character or well within the max width.
85 | case ((vect, pos, string), c) => (vect, pos + 1, string :+ c)
86 | } match {
87 | case (vect, _, "") => vect
88 | case (vect, _, s) => vect :+ s
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/concurrentthought/cla/Opt.scala:
--------------------------------------------------------------------------------
1 | package com.concurrentthought.cla
2 | import scala.util.{Try, Success, Failure}
3 |
4 | /**
5 | * A command-line option, which might actually be required, with a corresponding
6 | * value. It has the following fields:
7 | *
8 | * - `name` - serve as a lookup key for retrieving the value.
9 | * - `flags` - the arguments that invoke the option, e.g., `-h` and `--help`.
10 | * - `help` - a message displayed for command-line help.
11 | * - `default` - an optional default value, for when the user doesn't specify the option.
12 | * - `required` - the user must specify this option on the command line.
13 | * This flag is effectively ignored if a `default` is provided.
14 | * - `parser` - An implementation feature for parsing arguments.
15 | * - `fromString: String => V` - convert the found value from a String to the correct type.
16 | *
17 | */
18 | case class Opt[V] (
19 | name: String,
20 | flags: Seq[String],
21 | default: Option[V] = None,
22 | help: String = "",
23 | requiredFlag: Boolean = false)(fromString: String => Try[V]) {
24 |
25 | require (name.length != 0, "The Opt name can't be empty.")
26 |
27 | protected val optEQRE = "^([^=]+)=(.*)$".r
28 |
29 | /** Is the required flag true _and_ the default is None? */
30 | def required: Boolean = requiredFlag && default == None
31 |
32 | val parser: Opt.Parser[V] = {
33 | case optEQRE(flag, value) +: tail if flags.contains(flag) => parserHelper(flag, value, tail)
34 | case flag +: value +: tail if flags.contains(flag) => parserHelper(flag, value, tail)
35 | }
36 |
37 | protected def parserHelper(flag: String, value: String, tail: Seq[String]) = fromString(value) match {
38 | case sv @ Success(v) => ((name, sv), tail)
39 | case Failure(ex) => ex match {
40 | case ivs: Opt.InvalidValueString => ((name, Failure(ivs)), tail)
41 | case ex: Any => ((name, Failure(Opt.InvalidValueString(flag, value, Some(ex)))), tail)
42 | }
43 | }
44 | }
45 |
46 | object Opt {
47 | /**
48 | * Each option attempts to parse one or more tokens in the argument list.
49 | * If successful, it returns the option's name and extracted value as a tuple,
50 | * along with the rest of the arguments.
51 | */
52 | type Parser[V] = PartialFunction[Seq[String], ((String, Try[V]), Seq[String])]
53 |
54 | /**
55 | * Lift `String => V` to `String => Try[V]`.
56 | */
57 | def toTry[V](to: String => V): String => Try[V] = s => Try(to(s))
58 |
59 | /**
60 | * Lift `String => V` to `String => Seq[V]`.
61 | */
62 | def toSeq[V](to: String => V): String => Seq[V] = s => Vector(to(s))
63 |
64 | /**
65 | * Exception raised when an invalid value string is given. Not all errors
66 | * are detected and reported this way. For example, calls to `s.toInt` for
67 | * an invalid string will result in `NumberFormatException`.
68 | */
69 | case class InvalidValueString(
70 | flag: String, valueMessage: String, cause: Option[Throwable] = None)
71 | extends RuntimeException(s"$valueMessage for option $flag",
72 | if (cause == None) null else cause.get) { // scalastyle:ignore
73 |
74 | /** Override toString to show a nicer name for the exception than the FQN. */
75 | override def toString: String = {
76 | val causeStr = if (cause == None) "" else s" (cause: ${cause.get})"
77 | "Invalid value string: " + getMessage + causeStr
78 | }
79 | }
80 |
81 | /**
82 | * An implementation class for flags (boolean) options.
83 | */
84 | protected[cla] class Flag(
85 | name: String,
86 | flags: Seq[String],
87 | defaultValue: Boolean = false,
88 | help: String = "",
89 | requiredFlag: Boolean = false) extends Opt[Boolean](
90 | name, flags, Some(defaultValue), help, requiredFlag)(toTry(_.toBoolean)) {
91 |
92 | // Override the parser so it doesn't consume the next token (if any).
93 | override val parser: Opt.Parser[Boolean] = {
94 | case flag +: tail if flags.contains(flag) => parserHelper(flag, (!defaultValue).toString, tail)
95 | }
96 | }
97 |
98 | // Helper methods to create options.
99 |
100 | /**
101 | * Create a "flag" (Boolean) option. Unlike all the other kinds of
102 | * options, it does not consume an argument that follows it.
103 | * Instead the inferred default value corresponding to the flag is false.
104 | * If a user specifies the flag on the command line, the corresponding value
105 | * is true.
106 | * @see Opt.notflag
107 | */
108 | def flag(
109 | name: String,
110 | flags: Seq[String],
111 | help: String = "",
112 | requiredFlag: Boolean = false): Flag =
113 | new Flag(name, flags, false, help, requiredFlag)
114 |
115 | /**
116 | * Like `flag`, but the default value is true, not false.
117 | * @see Opt.flag
118 | */
119 | def notflag(
120 | name: String,
121 | flags: Seq[String],
122 | help: String = "",
123 | requiredFlag: Boolean = false): Flag =
124 | new Flag(name, flags, true, help, requiredFlag)
125 |
126 | /** Create a String option */
127 | def string(
128 | name: String,
129 | flags: Seq[String],
130 | default: Option[String] = None,
131 | help: String = "",
132 | requiredFlag: Boolean = false): Opt[String] =
133 | apply(name, flags, default, help, requiredFlag)(toTry(identity))
134 |
135 | /** Create a Byte option. */
136 | def byte(
137 | name: String,
138 | flags: Seq[String],
139 | default: Option[Byte] = None,
140 | help: String = "",
141 | requiredFlag: Boolean = false): Opt[Byte] =
142 | apply(name, flags, default, help, requiredFlag)(toTry(_.toByte))
143 |
144 | /** Create a Char option. Just takes the first character in the value string. */
145 | def char(
146 | name: String,
147 | flags: Seq[String],
148 | default: Option[Char] = None,
149 | help: String = "",
150 | requiredFlag: Boolean = false): Opt[Char] =
151 | apply(name, flags, default, help, requiredFlag)(toTry(_(0)))
152 |
153 | /** Create an Int option. */
154 | def int(
155 | name: String,
156 | flags: Seq[String],
157 | default: Option[Int] = None,
158 | help: String = "",
159 | requiredFlag: Boolean = false): Opt[Int] =
160 | apply(name, flags, default, help, requiredFlag)(toTry(_.toInt))
161 |
162 | /** Create a Long option. */
163 | def long(
164 | name: String,
165 | flags: Seq[String],
166 | default: Option[Long] = None,
167 | help: String = "",
168 | requiredFlag: Boolean = false): Opt[Long] =
169 | apply(name, flags, default, help, requiredFlag)(toTry(_.toLong))
170 |
171 | /** Create a Float option. */
172 | def float(
173 | name: String,
174 | flags: Seq[String],
175 | default: Option[Float] = None,
176 | help: String = "",
177 | requiredFlag: Boolean = false): Opt[Float] =
178 | apply(name, flags, default, help, requiredFlag)(toTry(_.toFloat))
179 |
180 | /** Create a Double option. */
181 | def double(
182 | name: String,
183 | flags: Seq[String],
184 | default: Option[Double] = None,
185 | help: String = "",
186 | requiredFlag: Boolean = false): Opt[Double] =
187 | apply(name, flags, default, help, requiredFlag)(toTry(_.toDouble))
188 |
189 | /**
190 | * Create an option where the value string represents a sequence with a delimiter.
191 | * The delimiter string is treated as a regex. For matching on several possible
192 | * delimiter characters, use "[;-_]", for example. The resulting substrings won't
193 | * be trimmed of whitespace, in case you want it, but you can also remove any
194 | * internal whitespace (i.e., not at the beginning or end of the input string),
195 | * e.g., "\\s*[;-_]\\s*". The delimiter is given as a separate argument list so
196 | * that the list of common Opt arguments is consistent with the other helper
197 | * methods.
198 | */
199 | def seq[V](delimsRE: String)(
200 | name: String,
201 | flags: Seq[String],
202 | default: Option[Seq[V]] = None,
203 | help: String = "",
204 | requiredFlag: Boolean = false)(fromString: String => Try[V]): Opt[Seq[V]] = {
205 | require (delimsRE.trim.length > 0, "The delimiters RE string can't be empty.")
206 | apply(name, flags, default, help, requiredFlag) {
207 | s => seqSupport(name, s, delimsRE, fromString)
208 | }
209 | }
210 |
211 | /**
212 | * A helper method when the substrings are returned without further processing required.
213 | */
214 | def seqString(delimsRE: String)(
215 | name: String,
216 | flags: Seq[String],
217 | default: Option[Seq[String]] = None,
218 | help: String = "",
219 | requiredFlag: Boolean = false): Opt[Seq[String]] =
220 | seq[String](delimsRE)(name, flags, default, help, requiredFlag)(toTry(_.toString))
221 |
222 | /**
223 | * A helper method for path-like structures, where the default delimiter
224 | * for the platform is used, e.g., ':' for *nix systems and ';' for Windows.
225 | */
226 | def path(
227 | name: String,
228 | flags: Seq[String],
229 | default: Option[Seq[String]] = None,
230 | help: String = "List of file system paths",
231 | requiredFlag: Boolean = false): Opt[Seq[String]] =
232 | seqString(pathSeparator)(
233 | name, flags, default, help, requiredFlag)
234 |
235 | def pathSeparator: String = sys.props.getOrElse("path.separator",":")
236 |
237 | private def seqSupport[V](name: String, str: String, delimsRE: String,
238 | fromString: String => Try[V]): Try[Seq[V]] = {
239 | def f(strs: Seq[String], vect: Vector[V]): Try[Vector[V]] = strs match {
240 | case head +: tail => fromString(head) match {
241 | case Success(value) => f(tail, vect :+ value)
242 | case Failure(ex) => Failure(ex)
243 | }
244 | case Nil => Success(vect)
245 | }
246 | f(str.split(delimsRE), Vector.empty[V])
247 | }
248 | }
249 |
250 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/concurrentthought/cla/OptParser.scala:
--------------------------------------------------------------------------------
1 | package com.concurrentthought.cla
2 | import org.parboiled.scala._ // scalastyle:ignore
3 | import org.parboiled.errors.{ErrorUtils, ParsingException}
4 |
5 | /** A set of "Elements", used for convenient access from clients. */
6 | object Elems {
7 | sealed trait Elem
8 |
9 | case class OptElem(optional: Boolean, flags_remaining: FlagsAndType_Or_RemainingElem, help: String) extends Elem
10 |
11 | abstract class FlagsAndType_Or_RemainingElem extends Elem
12 | case class FlagsAndTypeElem(flags: FlagsElem, typ: TypeElem[_]) extends FlagsAndType_Or_RemainingElem
13 | case class RemainingElem(name: String) extends FlagsAndType_Or_RemainingElem
14 |
15 |
16 | case class FlagsElem(flags: Seq[FlagElem]) extends Elem
17 | case class FlagElem(flag: String) extends Elem
18 | case class StringElem(text: String) extends Elem
19 |
20 | import scala.reflect.ClassTag
21 |
22 | abstract class TypeElem[T : ClassTag](val initialValueStr: String)(toT: String => T) extends Elem {
23 | val initialValue: Option[T] = try {
24 | if (initialValueStr.trim == "") None else Some(toT(initialValueStr))
25 | } catch {
26 | case scala.util.control.NonFatal(ex) =>
27 | throw new ParsingException(
28 | s"Invalid initial value string, '$initialValueStr' for type ${implicitly[ClassTag[T]]}. Cause: $ex",
29 | ex)
30 | }
31 | }
32 |
33 | case class FlagTypeElem( ivs: String) extends TypeElem[Boolean](removeEQ(ivs))(_.toBoolean)
34 | case class StringTypeElem( ivs: String) extends TypeElem[String](removeEQ(ivs))(_.toString)
35 | case class ByteTypeElem( ivs: String) extends TypeElem[Byte](removeEQ(ivs))(_.toByte)
36 | case class CharTypeElem( ivs: String) extends TypeElem[Char](removeEQ(ivs))(_(0))
37 | case class IntTypeElem( ivs: String) extends TypeElem[Int](removeEQ(ivs))(_.toInt)
38 | case class LongTypeElem( ivs: String) extends TypeElem[Long](removeEQ(ivs))(_.toLong)
39 | case class FloatTypeElem( ivs: String) extends TypeElem[Float](removeEQ(ivs))(_.toFloat)
40 | case class DoubleTypeElem( ivs: String) extends TypeElem[Double](removeEQ(ivs))(_.toDouble)
41 |
42 | case class SeqTypeElem(delimiter: String, ivs: String) extends TypeElem[String](toVS(ivs))(identity)
43 | case class PathTypeElem(ivs: String) extends TypeElem[String](removeEQ(ivs))(identity)
44 |
45 | private def removeEQ(s:String) =
46 | if (s.startsWith("=")) s.substring(1,s.length) else s
47 |
48 | // Because of the way the parse strings are passed to SeqTypeElem, the ivs
49 | // string includes the delimiter string, so we remove it here, then remove
50 | // the equals sign.
51 | private def toVS(s:String) = {
52 | val ary = s.split("=",2)
53 | if (ary.length == 2) removeEQ(ary(1)) else ""
54 | }
55 | }
56 |
57 | // scalastyle:off
58 |
59 | /** Parse a line defining an option. */
60 | object OptParser extends Parser {
61 |
62 | val knownTypes = Vector ("flag", "~flag", "string", "byte", "char", "int", "long", "float", "double", "seq", "path")
63 |
64 | import Elems._
65 |
66 | protected def whiteSpace = " \n\r\t\f"
67 |
68 | def Opt: Rule1[OptElem] = rule { OptionalOpt | RequiredOpt }
69 |
70 | def OptionalOpt: Rule1[OptElem] = rule { group(OptionalFlagsAndType_Or_Remaining ~
71 | optional(WhiteSpacePlus) ~ Help) ~~> ((ft: FlagsAndType_Or_RemainingElem, help: String) => OptElem(true, ft, help)) }
72 | def RequiredOpt: Rule1[OptElem] = rule { group(FlagsAndType_Or_Remaining ~
73 | optional(WhiteSpacePlus) ~ Help) ~~> ((ft: FlagsAndType_Or_RemainingElem, help: String) => OptElem(false, ft, help)) }
74 |
75 | def LeftBracket = rule { "[" ~ WhiteSpaceStar }
76 | def RightBracket = rule { WhiteSpaceStar ~ "]" }
77 |
78 | def OptionalFlagsAndType_Or_Remaining: Rule1[FlagsAndType_Or_RemainingElem] = rule {
79 | group(LeftBracket ~ FlagsAndType_Or_Remaining ~ RightBracket) }
80 |
81 | def FlagsAndType_Or_Remaining: Rule1[FlagsAndType_Or_RemainingElem] = rule {
82 | (FlagsAndType | Remaining) }
83 |
84 | def FlagsAndType: Rule1[FlagsAndTypeElem] = rule {
85 | group(Flags ~ WhiteSpacePlus ~ TypeAndInit) ~~> ((f: FlagsElem, t: TypeElem[_]) => FlagsAndTypeElem(f,t)) }
86 |
87 | // def Remaining: Rule1[RemainingElem] = rule { RT ~ (WhiteSpacePlus | EOI) }
88 | // protected def RT = rule { Name ~~> (se => RemainingElem(se.text)) }
89 | def Remaining: Rule1[RemainingElem] = rule { Name ~~> (se => RemainingElem(se.text)) }
90 |
91 | def Flags: Rule1[FlagsElem] = rule { oneOrMore(Flag, separator = WhiteSpaceStar ~ "|" ~ WhiteSpaceStar) ~~> FlagsElem }
92 | def Flag: Rule1[FlagElem] = rule { Flag2 ~> FlagElem }
93 | def Flag2 = rule { ("--" | "-") ~ N2 }
94 |
95 | def TypeAndInit: Rule1[TypeElem[_]] = rule {
96 | FlagType | NotFlagType |
97 | StringType | ByteType | CharType |
98 | IntType | LongType | FloatType | DoubleType |
99 | SeqType | PathType }
100 |
101 | def FlagType: Rule1[FlagTypeElem] = rule { "flag" ~ push(FlagTypeElem("false")) }
102 | def NotFlagType: Rule1[FlagTypeElem] = rule { "~flag" ~ push(FlagTypeElem("true")) }
103 | def StringType: Rule1[StringTypeElem] = rule { "string" ~ InitialValue ~> StringTypeElem }
104 | def ByteType: Rule1[ByteTypeElem] = rule { "byte" ~ InitialValue ~> ByteTypeElem }
105 | def CharType: Rule1[CharTypeElem] = rule { "char" ~ InitialValue ~> CharTypeElem }
106 | def IntType: Rule1[IntTypeElem] = rule { "int" ~ InitialValue ~> IntTypeElem }
107 | def LongType: Rule1[LongTypeElem] = rule { "long" ~ InitialValue ~> LongTypeElem }
108 | def FloatType: Rule1[FloatTypeElem] = rule { "float" ~ InitialValue ~> FloatTypeElem }
109 | def DoubleType: Rule1[DoubleTypeElem] = rule { "double" ~ InitialValue ~> DoubleTypeElem }
110 |
111 | def SeqType: Rule1[SeqTypeElem] = rule { "seq" ~ SeqDIV ~~> SeqTypeElem }
112 | def PathType: Rule1[PathTypeElem] = rule { "path" ~ InitialValue ~> PathTypeElem }
113 | protected def SeqDIV = rule { group(Delim ~ InitialValue) ~> identity }
114 |
115 | def Help: Rule1[String] = rule { zeroOrMore(ANY) ~> identity }
116 |
117 | def InitialValue = rule { optional("=" ~ oneOrMore(noneOf(whiteSpace+"[]"))) }
118 |
119 | // Keep the trailing ")", for use in splitting "upstream".
120 | def Delim = rule { "(" ~ D2 ~ ")" }
121 | protected def D2: Rule1[String] = rule { oneOrMore(noneOf(")")) ~> identity }
122 |
123 | def Name = rule { N2 ~> StringElem }
124 | protected def N2 = rule { LDU ~ zeroOrMore(LDU | "-") }
125 |
126 | def LDU = rule { Letter | Digit | "_" }
127 | def Letter = rule { "a" - "z" | "A" - "Z" }
128 | def Digit = rule { "0" - "9" }
129 |
130 | def WhiteSpace = rule { anyOf(whiteSpace) }
131 | def WhiteSpaceStar = rule { zeroOrMore(WhiteSpace) }
132 | def WhiteSpacePlus = rule { oneOrMore(WhiteSpace) }
133 |
134 | /** Parse a full option string */
135 | def parse(s: String): Either[ParsingException, OptElem] =
136 | parseWithRule(s, Opt)
137 |
138 | def parseWithRule[E](s: String, rule: Rule1[E]): Either[ParsingException, E] =
139 | try {
140 | val result: ParsingResult[E] = ReportingParseRunner(rule).run(s.trim)
141 | result.result match {
142 | case Some(x) => Right(x)
143 | case None => Left(new ParsingException(
144 | s"Invalid input: `${s}'\n Error:" + ErrorUtils.printParseErrors(result)))
145 | }
146 | } catch {
147 | case pe: ParsingException => Left(pe)
148 | }
149 | }
150 |
151 | // scalastyle:on
152 |
--------------------------------------------------------------------------------
/core/src/main/scala/com/concurrentthought/cla/package.scala:
--------------------------------------------------------------------------------
1 | package com.concurrentthought
2 | import scala.util.Try
3 |
4 | /**
5 | * Package object that adds a mini-DSL allowing the user to construct an `Args`
6 | * using using a multi-line string.
7 | * @note Experimental!
8 | */
9 | package object cla {
10 |
11 | /**
12 | * Specify the command-line arguments using a single, multi-line string.
13 | * Note the following example:
14 | * {{{
15 | * import com.concurrentthought.cla._
16 | *
17 | * val args: Args = """
18 | * |java -cp ... foo
19 | * |Some description
20 | * |and a second line.
21 | * | [-i | --in | --input string] Path to input file.
22 | * | [-o | --out | --output string=/dev/null] Path to output file.
23 | * | [-l | --log | --log-level int=3] Log level to use.
24 | * | -p | --path path Path elements separated by ':' (*nix) or ';' (Windows).
25 | * | [-q | --quiet flag] Suppress some verbose output.
26 | * | [--things seq([-|])] String elements separated by '-' or '|'.
27 | * | others Other stuff.
28 | * |Any additional help description lines,
29 | * |which also have no leading whitespace.
30 | * |""".stripMargin.toArgs
31 | * }}}
32 | * The format, as illustrated in the example, has the following requirements:
33 | *
34 | * - Zero or more leading and trailing lines without opening whitespace are
35 | * interpreted as the "program invocation" string in the help message,
36 | * followed by zero or more description lines, which will be concatenated
37 | * together (separated by whitespace) in the help message.
38 | * - Each option appears on a line with leading whitespace.
39 | * - Each option has one or more flags, separated by "|". As a special case,
40 | * one option can have no flags. It is used to provide a help message for all other
41 | * command-line tokens that aren't associated with flags (which will be stored
42 | * in `Args.remaining`).
43 | * Note that these tokens are handled whether or not you specify a line like
44 | * this or not.
45 | * - If the option is not required for the user to specify a value, the flags
46 | * and name must be wrapped in "[...]". Specifying a default value is effectively
47 | * the name as not required, but the main purpose is display the expected
48 | * behavior to the user.
49 | * - After the flags, the "middle column" describes the type of option and
50 | * optionally a default value. The types correspond to several helper functions
51 | * in `Opt`: `string`, `byte`, `char`, `int`, `long`, `float`, `double`, `path`,
52 | * `seqString`, where the string "seq" is used, followed by a required
53 | * `(delim)` suffix to specify the delimiter regex as shown in the example, and
54 | * `flag` which indicates a boolean flag where no value is expected, but the
55 | * flag's presence means `true` and absence means `false`.
56 | * However, for a no-flag option, the value in this column is interpreted as a
57 | * name for the option for the help message. This is the one case where the string
58 | * isn't interpreted as a type specifier.
59 | * The `=` indicates the default value to use, if present. Current limitations
60 | * of the type specification include the following: (i) `Opt.seq[V]` isn't
61 | * supported in this mechanism, (ii) default values can't be specified for
62 | * the `path` or `seq` types, nor for the no-flag case (an implementation limitation).
63 | *
- The remaining text is interpreted as the help string.
64 | *
65 | *
66 | * @note This is an experimental feature. There are several known limitations:
67 | *
68 | * - It provides no way to use the predefined flags like `Args.quietFlag`,
69 | * although `Args.helpFlag` and `Args.remainingOpt` are automatically added
70 | * if the list of options doesn't explicitly define help and no-flag constructs.
71 | * - The general case of `Opt.seq[V]` isn't supported, only `Opt.seqString`.
72 | * - It needs to be tested on a lot more examples.
73 | *
74 | */
75 | implicit class ToArgs(str: String) {
76 |
77 | import Elems._ // scalastyle:ignore
78 |
79 | def toArgs: Args = {
80 | val lines = str.split("\n").filter(_.length != 0).toVector
81 |
82 | // Partition the lines into the leading comments, the options (which have
83 | // leading whitespace), and the trailing comments.
84 | type VS = Vector[String]
85 | val ve = Vector.empty[String]
86 | def split(lines: Vector[String], result: (VS,VS,VS)): (VS,VS,VS) = {
87 | val leadingWhitespace = """^\s+""".r
88 | val (l,o,t) = result
89 | lines match {
90 | case head +: tail => leadingWhitespace.findFirstMatchIn(head) match {
91 | case None if o.size == 0 => split(tail, (l :+ head, o, t))
92 | case None => split(tail, (l, o, t :+ head))
93 | case _ => split(tail, (l, o :+ head, t))
94 | }
95 | case _ => result
96 | }
97 | }
98 | val (leading, options, trailing) = split(lines, (ve,ve,ve))
99 | val opts = options map { line =>
100 | OptParser.parse(line) match {
101 | case Left(ex) => throw ParseError(ex.getMessage, ex)
102 | case Right(OptElem(optional: Boolean, re: RemainingElem, help: String)) =>
103 | Args.makeRemainingOpt(re.name, help, !optional)
104 | case Right(OptElem(optional: Boolean, fte: FlagsAndTypeElem, help: String)) =>
105 | val flagStrs = fte.flags.flags.map (_.flag)
106 | val name = flagStrs.last.replaceAll("^--?", "")
107 | toOpt(fte.typ, name, optional, flagStrs, help)
108 | case Right(r) => throw ParseError("Unexpected element: "+r)
109 | }
110 | }
111 | val (programInvocation, description) = leading.size match {
112 | case 0 => ("", "")
113 | case 1 => (leading(0), "")
114 | case n:Int => (leading(0), leading.slice(1, n).mkString(" "))
115 | }
116 | Args(programInvocation, description, trailing.mkString(" "), opts.toVector)
117 | }
118 |
119 | // scalastyle:off
120 | protected def toOpt(typeElem: TypeElem[_],
121 | name: String, optional: Boolean,
122 | flags: Seq[String], help: String): Opt[_] = typeElem match {
123 |
124 | case e: FlagTypeElem =>
125 | if (toBool(e.initialValue) == false) Opt.flag(name, flags, help, !optional)
126 | else Opt.notflag(name, flags, help, !optional)
127 | case e: StringTypeElem => Opt.string(name, flags, e.initialValue, help, !optional)
128 | case e: ByteTypeElem => Opt.byte( name, flags, e.initialValue, help, !optional)
129 | case e: CharTypeElem => Opt.char( name, flags, e.initialValue, help, !optional)
130 | case e: IntTypeElem => Opt.int( name, flags, e.initialValue, help, !optional)
131 | case e: LongTypeElem => Opt.long( name, flags, e.initialValue, help, !optional)
132 | case e: FloatTypeElem => Opt.float( name, flags, e.initialValue, help, !optional)
133 | case e: DoubleTypeElem => Opt.double(name, flags, e.initialValue, help, !optional)
134 | case e: SeqTypeElem => Opt.seq(e.delimiter)(
135 | name, flags, toInitSeq(e.initialValue, e.delimiter), help, !optional)(s => Try(s.toString))
136 | case e: PathTypeElem => Opt.path( name, flags, toInitSeq(e.initialValue, Opt.pathSeparator), help, !optional)
137 | }
138 | // scalastyle:on
139 |
140 | protected def toBool(o: Option[Boolean]): Boolean = o.getOrElse(false)
141 |
142 | protected def toInitSeq(init: Option[String], delim: String): Option[Seq[String]] =
143 | init.map(_.split(delim).toSeq)
144 | }
145 | }
146 |
147 | package cla {
148 | case class ParseError(msg: String, cause: Throwable = null) // scalastyle:ignore
149 | extends RuntimeException(msg, cause)
150 | }
151 |
152 |
--------------------------------------------------------------------------------
/core/src/test/scala/com/concurrentthought/cla/ArgsSpec.scala:
--------------------------------------------------------------------------------
1 | package com.concurrentthought.cla
2 | import scala.util.{Try, Success, Failure}
3 | import org.scalatest.FunSpec
4 | import java.io._
5 |
6 | class ArgsSpec extends FunSpec {
7 | import SpecHelper._
8 |
9 | describe ("case class Args") {
10 | describe ("empty program invocation and comments") {
11 | it ("includes default values") {
12 | val args = Args.empty
13 | assert(args.programInvocation === Args.defaultProgramInvocation)
14 | assert(args.leadingComments === Args.defaultComments)
15 | assert(args.trailingComments === Args.defaultComments)
16 | }
17 | }
18 | describe ("nonempty program invocation and comments") {
19 | it ("includes the specified values") {
20 | val args = Args("programInvocation", "leadingComments", "trailingComments", Nil)
21 | assert(args.programInvocation === "programInvocation")
22 | assert(args.leadingComments === "leadingComments")
23 | assert(args.trailingComments === "trailingComments")
24 | }
25 | }
26 | describe ("empty list of options") {
27 | it ("still includes help") {
28 | val args = Args.empty.parse(Array("--help"))
29 | assert(args.programInvocation === Args.defaultProgramInvocation)
30 | assert(args.leadingComments === Args.defaultComments)
31 | assert(args.trailingComments === Args.defaultComments)
32 | val helpName = Args.helpFlag.name
33 | assert(args.opts.contains(Args.helpFlag))
34 | assert(args.defaults.contains(helpName))
35 | assert(args.values.contains(helpName))
36 | assert(args.allValues.contains(helpName))
37 | }
38 |
39 | it ("but the default help can be overridden, as long as the option has the name field \"help\".") {
40 | val altHelp = Opt.flag(
41 | name = Args.HELP_KEY,
42 | flags = Seq("-H", "--H", "--HELP"),
43 | help = "Show this HELP message.")
44 | val args = Args(opts = Seq(altHelp))
45 | assert(args.opts.contains(altHelp))
46 | // before parsing:
47 | assert(args.defaults === Map(Args.HELP_KEY -> false))
48 | assert(args.values === Map(Args.HELP_KEY -> false))
49 | assert(args.allValues === Map(Args.HELP_KEY -> Vector(false)))
50 | // after parsing:
51 | val args2 = args.parse(Array("--HELP"))
52 | assert(args2.defaults === Map(Args.HELP_KEY -> false))
53 | assert(args2.values === Map(Args.HELP_KEY -> true))
54 | assert(args2.allValues === Map(Args.HELP_KEY -> Vector(true)))
55 | }
56 |
57 | it ("still includes an option for 'remaining' tokens not associated with flags") {
58 | val args = Args.empty
59 | val args2 = args.parse(Array("foo", "bar"))
60 | val remainingName = Args.remainingOpt.name
61 | Seq(args, args2) foreach { a =>
62 | assert(a.opts.contains(Args.remainingOpt))
63 | assert(a.defaults.contains(remainingName) === false)
64 | assert(a.values.contains(remainingName) === false)
65 | assert(a.allValues.contains(remainingName) === false)
66 | }
67 | assert(args.remaining === Vector.empty[String])
68 | assert(args2.remaining === Vector("foo", "bar"))
69 | }
70 | }
71 |
72 | describe ("Zero or one option can have no flags.") {
73 | it ("an error is thrown if more than two `Opts` are given") {
74 | intercept[IllegalArgumentException] {
75 | Args(Seq(Opt.string("one", Nil), Opt.string("two", Nil)))
76 | }
77 | () // Suppress -Ywarn-value-discard warning
78 | }
79 | }
80 |
81 | describe ("before parsing") {
82 | it ("contains a map of the defined options with their default values") {
83 | val (args, values, allValues, remaining) = all
84 | assert(args.defaults === allDefaults)
85 | }
86 |
87 | it ("contains maps of the values an 'all values' seen, which equal the default values") {
88 | val (args, values, allValues, remaining) = all
89 | assert(args.values === values)
90 | assert(args.allValues === allValues)
91 | }
92 |
93 | it ("contains the default values for the 'remaining' tokens, if any, which aren't associated with flags") {
94 | val (args, values, allValues, remaining) = all
95 | assert(args.remaining === remaining)
96 | }
97 | }
98 |
99 | describe ("after parsing") {
100 |
101 | it ("contains a map of the defined options, where each value is the LAST specified option value, or the default value") {
102 | val (args, values, allValues, remaining) = all
103 | assert(args.values === values)
104 | }
105 |
106 | it ("contains a map of the defined options, where each value has ALL the specified option values, or the default value") {
107 | val (args, values, allValues, remaining) = all
108 | assert(args.allValues === allValues)
109 | assert(args.failures === Nil)
110 | }
111 |
112 | it ("contains all the valid options matched and the failures for invalid options") {
113 | val args = Args(opts = allOpts).parse(Array("--foo", "bar", "--string", "hello", "-b"))
114 | val values = allDefaults + ("string" -> "hello")
115 | assert(args.values === values)
116 | assert(args.remaining === Vector("bar"))
117 | val failures = List(
118 | ("--foo", Args.UnrecognizedArgument("--foo", Seq("bar", "--string", "hello", "-b"))),
119 | ("-b", Args.UnrecognizedArgument("-b", Nil)))
120 | assert(args.failures === failures)
121 | }
122 |
123 | it ("contains failures when argument values are not parseable to the correct type") {
124 | val args = Args(opts = allOpts).parse(Array(
125 | "--byte", "z",
126 | "--char", "",
127 | "--int", "z",
128 | "--long", "z",
129 | "--float", "z",
130 | "--float", "1_2",
131 | "--double", "z",
132 | "--double", "2_2",
133 | "--seq", "a:b_c-d"))
134 | val failures = List(
135 | ("byte", ivs("--byte", "z", Some(nfe("z")))),
136 | ("char", ivs("--char", "", Some(new StringIndexOutOfBoundsException(0)))),
137 | ("int", ivs("--int", "z", Some(nfe("z")))),
138 | ("long", ivs("--long", "z", Some(nfe("z")))),
139 | ("float", ivs("--float", "z", Some(nfe("z")))),
140 | ("float", ivs("--float", "1_2", Some(nfe("1_2")))),
141 | ("double", ivs("--double", "z", Some(nfe("z")))),
142 | ("double", ivs("--double", "2_2", Some(nfe("2_2")))),
143 | ("seq", ivs("--seq", "a:b_c-d", Some(nfe("a")))))
144 |
145 | assert(args.values === args.defaults)
146 | assert(args.remaining === Vector.empty[String])
147 | args.failures zip failures foreach { case ((flag1,ex1), (flag2,ex2)) =>
148 | assert(flag1 === flag2)
149 | // Exception.equals doesn't work.
150 | assert(ex1.toString === ex2.toString)
151 | }
152 | }
153 |
154 | it ("contains values for successfully-parsed input and failures when the arguments couldn't be parsed, for repeated options") {
155 | val args = Args(opts = allOpts).parse(Array(
156 | "--byte", "1",
157 | "--byte", "z",
158 | "--byte", "2"))
159 | val failures = List(
160 | ("byte", ivs("--byte", "z", Some(nfe("z")))))
161 | assert(args.values === (args.defaults + ("byte" -> 2)))
162 | assert(args.remaining === Vector.empty[String])
163 | assert(args.getAll[Byte]("byte") === Vector(1,2))
164 | args.failures zip failures foreach { case ((flag1,ex1), (flag2,ex2)) =>
165 | assert(flag1 === flag2)
166 | // Exception.equals doesn't work.
167 | assert(ex1.toString === ex2.toString)
168 | }
169 | }
170 |
171 | it ("contains a 'remaining' value for the non-flag, unrecognized arguments") {
172 | val args = Args.empty.parse(Array("a", "b"))
173 | assert(args.remaining === Vector("a", "b"))
174 | assert(args.values === Map(Args.HELP_KEY -> false))
175 | assert(args.allValues === Map(Args.HELP_KEY -> Vector(false)))
176 | assert(args.failures.isEmpty)
177 | }
178 |
179 | val req1 = Opt.string("req1", Seq("--r1"), requiredFlag = true)
180 | val req2 = Opt.string("req2", Seq("--r2"), requiredFlag = true, default = Some("foo"))
181 | val req3 = Opt.string("req3", Seq("--r3"), requiredFlag = true)
182 | val requiredArgs = Args(opts = Seq(req1, req2, req3))
183 |
184 | it ("contains a list of the required options, which are marked required AND don't have default values") {
185 | assert(requiredArgs.requiredOptions === Seq(req1, req3))
186 | }
187 |
188 | it ("returns failures for required options that weren't specified") {
189 | val args2 = requiredArgs.parse(Array.empty[String])
190 | assert(args2.failures === Seq(
191 | ("req1", Args.MissingRequiredArgument(req1)),
192 | ("req3", Args.MissingRequiredArgument(req3))))
193 | }
194 |
195 | def unknowns() = {
196 | val args = Args.empty.parse(Array("--foo", "-b", "bbb"))
197 | assert(args.values === Map(Args.HELP_KEY -> false))
198 | assert(args.remaining === Vector("bbb"))
199 | val expected = List(
200 | ("--foo", Args.UnrecognizedArgument("--foo", Seq("-b", "bbb"))),
201 | ("-b", Args.UnrecognizedArgument("-b", Seq("bbb"))))
202 | assert(args.failures === expected)
203 | assert(args.remaining === Vector("bbb"))
204 | }
205 | it ("contains a list of the unknown flags in the user input") { unknowns() }
206 | it ("the 'values' doesn't contain the values specified with the bad options") { unknowns() }
207 | it ("the 'remains' does contain the values specified with the bad options") { unknowns() }
208 |
209 | it ("contains failures when an option is at the end of the list without a required value") {
210 | val args = Args(opts = allOpts)
211 | Seq("--byte", "--char", "--int", "--long", "--float", "--float", "--double", "--double", "--seq") foreach { flag =>
212 | val args2 = args.parse(Array(flag))
213 | assert(args2.defaults === allDefaults)
214 | assert(args2.values === allDefaults)
215 | assert(args2.remaining === Vector.empty[String])
216 | assert(args2.failures === Seq((flag, Args.UnrecognizedArgument(flag, Nil))))
217 | }
218 | }
219 | }
220 |
221 | describe ("process())") {
222 |
223 | val unexpectedExit: Int => Unit = (n) => fail(s"Unexpected exit($n)")
224 | def expectedExit(expected: Int): Int => Unit = {
225 | (n) => assert(expected === n, s"Unexpected exit($n)")
226 | ()
227 | }
228 |
229 | it ("when successful, returns an Args with the updated Args") {
230 | val bytes = new ByteArrayOutputStream(2048)
231 | val out2 = new PrintStream(bytes, true)
232 | val args = Args(opts = allOpts).process(
233 | Array("--string", "hello"), out2, unexpectedExit)
234 | val values = allDefaults + ("string" -> "hello")
235 | assert(args.values === values)
236 | assert(args.remaining.size === 0)
237 | assert(args.failures.size === 0)
238 | assert(bytes.size === 0)
239 | }
240 |
241 | it ("when successful, but help requested returns a None and outputs the help") {
242 | val bytes = new ByteArrayOutputStream(2048)
243 | val out2 = new PrintStream(bytes, true)
244 | Args(opts = allOpts).process(
245 | Array("--help", "--string", "hello"), out2, expectedExit(0))
246 | assert(bytes.size !== 0)
247 | }
248 |
249 | it ("when unsuccessful, returns a None and outputs an error message") {
250 | val bytes = new ByteArrayOutputStream(2048)
251 | val out2 = new PrintStream(bytes, true)
252 | Args(opts = allOpts).process(
253 | Array("--bogus", "--string", "hello"), out2, expectedExit(1))
254 | assert(bytes.size !== 0)
255 | }
256 | }
257 |
258 | describe ("get[V]") {
259 | it ("returns an Option[V] for the flag, either the default or the last user-specified value.") {
260 | val (args, values, allValues, remaining) = all
261 | assert(args.get[String]("foo") === None)
262 | assert(args.get[String]("string") === Some("world!"))
263 | assert(args.get[Byte]("byte") === Some(3))
264 | assert(args.get[Char]("char") === Some('a'))
265 | assert(args.get[Int]("int") === Some(4))
266 | assert(args.get[Long]("long") === Some(5))
267 | assert(args.get[Float]("float") === Some(1.1F))
268 | assert(args.get[Double]("double") === Some(2.2))
269 | assert(args.get[Seq[Double]]("seq") === Some(Seq(111.3, 126.2, 123.4, 354.6)))
270 | }
271 | }
272 |
273 | describe ("getOrElse[V]") {
274 | it ("returns the found value or the default of type V for the flag or the second argument") {
275 | val (args, values, allValues, remaining) = all
276 | assert(args.getOrElse("string", "goodbye!") === "world!")
277 | assert(args.getOrElse("string2", "goodbye!") === "goodbye!")
278 | assert(args.getOrElse("byte", 1) === 3)
279 | assert(args.getOrElse("byte2", 1) === 1)
280 | assert(args.getOrElse("seq", Seq(1.1, 2.2)) === Seq(111.3, 126.2, 123.4, 354.6))
281 | assert(args.getOrElse("seq2", Seq(1.1, 2.2)) === Seq(1.1, 2.2))
282 | }
283 | }
284 |
285 | describe ("getAll[V]") {
286 | it ("returns a Seq[V] of the values for all invocations of the option, or the default value") {
287 | val (args, values, allValues, remaining) = all
288 | assert(args.getAll[String]("foo") === Nil)
289 | assert(args.getAll[String]("string") === Vector("hello", "world!"))
290 | assert(args.getAll[Byte]("byte") === Vector(2,3))
291 | assert(args.getAll[Char]("char") === Vector('a'))
292 | assert(args.getAll[Int]("int") === Vector(4))
293 | assert(args.getAll[Long]("long") === Vector(5))
294 | assert(args.getAll[Float]("float") === Vector(1.1F))
295 | assert(args.getAll[Double]("double") === Vector(2.2))
296 | assert(args.getAll[Seq[Double]]("seq") === Vector(Seq(111.3, 126.2, 123.4, 354.6)))
297 | }
298 | it ("returns Nil if there were no invocations of the option and the default value is None") {
299 | val (args, values, allValues, remaining) = all
300 | assert(args.getAll[String]("foo") === Nil)
301 | }
302 | }
303 |
304 | describe ("getOrElse[V]") {
305 | it ("returns a Seq[V] the values for all invocations of the option or the default value or the second argument") {
306 | val (args, values, allValues, remaining) = all
307 | assert(args.getAllOrElse("string", Seq("goodbye!")) === Vector("hello", "world!"))
308 | assert(args.getAllOrElse("string2", Seq("goodbye!")) === Seq("goodbye!"))
309 | assert(args.getAllOrElse("byte", Seq(1)) === Vector(2,3))
310 | assert(args.getAllOrElse("byte2", Seq(1)) === Seq(1))
311 | assert(args.getAllOrElse("seq", Seq(Seq(1.1, 2.2))) === Vector(Seq(111.3, 126.2, 123.4, 354.6)))
312 | assert(args.getAllOrElse("seq2", Seq(Seq(1.1, 2.2))) === Seq(Seq(1.1, 2.2)))
313 | }
314 | }
315 |
316 | describe ("toString") {
317 | val args = Args(
318 | programInvocation = "java foo",
319 | leadingComments = "leading",
320 | trailingComments = "trailing",
321 | opts = allOpts)
322 |
323 | val tos = args.toString
324 |
325 | it ("contains the program invocation") {
326 | assert(tos.contains("program invocation: java foo"))
327 | }
328 | it ("contains the leading comments") {
329 | assert(tos.contains("leading comments: leading"))
330 | }
331 | it ("contains the trailing comments") {
332 | assert(tos.contains("trailing comments: trailing"))
333 | }
334 | it ("contains the options") {
335 | assert(tos.contains(s"opts: $allOpts"))
336 | }
337 | it ("contains the defaults") {
338 | assert(tos.contains(s"defaults: ${args.defaults}"))
339 | }
340 | it ("contains the values") {
341 | assert(tos.contains(s"values: ${args.values}"))
342 | }
343 | it ("contains all values") {
344 | assert(tos.contains(s"allValues: ${args.allValues}"))
345 | }
346 | it ("contains the remaining tokens") {
347 | assert(tos.contains(s"remaining: ${args.remaining}"))
348 | }
349 | it ("contains the failures") {
350 | assert(tos.contains(s"failures: ${args.failures}"))
351 | }
352 | }
353 |
354 | describe ("printValues") {
355 | it ("prints the default values before any parsing is done") {
356 | val args = Args(opts = allOpts)
357 | val out = new StringOut
358 | args.printValues(out.out)
359 | val expected = """
360 | |Command line arguments:
361 | | help: false
362 | | anti: true
363 | | string: foobar
364 | | byte: 0
365 | | char: x
366 | | int: 0
367 | | long: 0
368 | | float: 0.0
369 | | double: 0.0
370 | | seq: List()
371 | | seq-string: List()
372 | | path: List()
373 | | others: Vector()
374 | |
375 | |""".stripMargin
376 | assert(out.toString === expected)
377 | }
378 |
379 | it ("prints the default values overridden by user-specified options (the last invocation of any one option...) after parsing is done") {
380 | val args = Args(opts = allOpts).parse(Array(
381 | "--help",
382 | "--anti",
383 | "--string", "hello",
384 | "--byte", "3",
385 | "--char", "abc",
386 | "--int", "4",
387 | "--long", "5",
388 | "--float", "1.1",
389 | "foo",
390 | "--double", "2.2",
391 | "--seq", "111.3:126.2_123.4-354.6",
392 | "--seq-string", "a:b_c-d",
393 | "bar",
394 | "--path", s"/foo/bar${pathDelim}/home/me",
395 | "baz"))
396 | val out = new StringOut
397 | args.printValues(out.out)
398 | val expected = """
399 | |Command line arguments:
400 | | help: true
401 | | anti: false
402 | | string: hello
403 | | byte: 3
404 | | char: a
405 | | int: 4
406 | | long: 5
407 | | float: 1.1
408 | | double: 2.2
409 | | seq: Vector(111.3, 126.2, 123.4, 354.6)
410 | | seq-string: Vector(a, b, c, d)
411 | | path: Vector(/foo/bar, /home/me)
412 | | others: Vector(foo, bar, baz)
413 | |
414 | |""".stripMargin
415 | assert(out.toString === expected)
416 | }
417 | }
418 |
419 | describe ("printAllValues") {
420 | it ("prints the default values before any parsing is done") {
421 | val args = Args(opts = allOpts)
422 | val out = new StringOut
423 | args.printAllValues(out.out)
424 | val expected = """
425 | |Command line arguments (all values given):
426 | | help: Vector(false)
427 | | anti: Vector(true)
428 | | string: Vector(foobar)
429 | | byte: Vector(0)
430 | | char: Vector(x)
431 | | int: Vector(0)
432 | | long: Vector(0)
433 | | float: Vector(0.0)
434 | | double: Vector(0.0)
435 | | seq: Vector(List())
436 | | seq-string: Vector(List())
437 | | path: Vector(List())
438 | | others: Vector()
439 | |
440 | |""".stripMargin
441 | assert(out.toString === expected)
442 | }
443 |
444 | it ("prints the default values overridden by user-specified options after parsing is done") {
445 | val args = Args(opts = allOpts).parse(Array(
446 | "--help",
447 | "--anti",
448 | "--string", "hello",
449 | "--byte", "3",
450 | "--char", "abc",
451 | "--int", "4",
452 | "--long", "5",
453 | "--float", "1.1",
454 | "foo",
455 | "--double", "2.2",
456 | "--seq", "111.3:126.2_123.4-354.6",
457 | "--seq-string", "a:b_c-d",
458 | "bar",
459 | "--path", s"/foo/bar${pathDelim}/home/me",
460 | "baz"))
461 | val out = new StringOut
462 | args.printAllValues(out.out)
463 | val expected = """
464 | |Command line arguments (all values given):
465 | | help: Vector(true)
466 | | anti: Vector(false)
467 | | string: Vector(hello)
468 | | byte: Vector(3)
469 | | char: Vector(a)
470 | | int: Vector(4)
471 | | long: Vector(5)
472 | | float: Vector(1.1)
473 | | double: Vector(2.2)
474 | | seq: Vector(Vector(111.3, 126.2, 123.4, 354.6))
475 | | seq-string: Vector(Vector(a, b, c, d))
476 | | path: Vector(Vector(/foo/bar, /home/me))
477 | | others: Vector(foo, bar, baz)
478 | |
479 | |""".stripMargin
480 | assert(out.toString === expected)
481 | }
482 | }
483 |
484 | describe ("handleHelp") {
485 | it ("does nothing before any arguments have been parsed") {
486 | val args = Args(opts = allOpts)
487 | val out = new StringOut
488 | args.handleHelp(out.out)
489 | assert(out.toString.length === 0, out.toString)
490 | }
491 | it ("does nothing if help wasn't requested") {
492 | val args = Args(opts = allOpts).parse(Array[String]())
493 | val out = new StringOut
494 | args.handleHelp(out.out)
495 | assert(out.toString.length === 0, out.toString)
496 | }
497 | it ("prints the help message to the out PrintStream, if help is requested") {
498 | val args = Args(opts = allOpts).parse(Array("--help"))
499 | val out = new StringOut
500 | args.handleHelp(out.out)
501 | assert(out.toString.length > 0)
502 | }
503 | }
504 |
505 | describe ("handleErrors") {
506 | it ("does nothing before any arguments have been parsed") {
507 | val args = Args(opts = allOpts)
508 | val out = new StringOut
509 | args.handleErrors(out.out)
510 | assert(out.toString.length === 0, out.toString)
511 | }
512 | it ("does nothing if no parsing errors occurred") {
513 | val args = Args(opts = allOpts).parse(Array("--help"))
514 | val out = new StringOut
515 | args.handleErrors(out.out)
516 | assert(out.toString.length === 0, out.toString)
517 | }
518 | it ("prints the error and help messages to the out PrintStream if errors occurred") {
519 | val args = Args(opts = allOpts).parse(Array("--xxx"))
520 | val out = new StringOut
521 | args.handleErrors(out.out)
522 | assert(out.toString.length > 0)
523 | }
524 | }
525 | }
526 |
527 | private def nfe(s: String) = new NumberFormatException("For input string: \"%s\"".format(s))
528 | private def ivs(flag: String, value: String, ex: Option[RuntimeException]) =
529 | Opt.InvalidValueString(flag, value, ex)
530 |
531 | private def all: (Args, Map[String,Any], Map[String,Seq[Any]], Vector[String]) = {
532 | val args = Args(opts = allOpts).parse(Array(
533 | "--string", "hello",
534 | "--string", "world!",
535 | "--byte", "2",
536 | "--byte", "3",
537 | "--char", "abc",
538 | "--int", "4",
539 | "--long", "5",
540 | "--float", "1.1",
541 | "--double", "2.2",
542 | "foo",
543 | "--seq", "111.3:126.2_123.4-354.6",
544 | "--seq-string", "a:b_c-d",
545 | "--path", s"/foo/bar${pathDelim}/home/me",
546 | "bar"))
547 | val values = Map[String,Any](
548 | Args.HELP_KEY -> false,
549 | "anti" -> true,
550 | "string" -> "world!",
551 | "byte" -> 3,
552 | "char" -> 'a',
553 | "int" -> 4,
554 | "long" -> 5,
555 | "float" -> 1.1F,
556 | "double" -> 2.2,
557 | "seq" -> Seq(111.3, 126.2, 123.4, 354.6),
558 | "seq-string" -> Vector("a", "b", "c", "d"),
559 | "path" -> Vector("/foo/bar", "/home/me"))
560 | val allValues = Map[String,Seq[Any]](
561 | Args.HELP_KEY -> Vector(false),
562 | "anti" -> Vector(true),
563 | "string" -> Vector("hello", "world!"),
564 | "byte" -> Vector(2,3),
565 | "char" -> Vector('a'),
566 | "int" -> Vector(4),
567 | "long" -> Vector(5),
568 | "float" -> Vector(1.1F),
569 | "double" -> Vector(2.2),
570 | "seq" -> Vector(Seq(111.3, 126.2, 123.4, 354.6)),
571 | "seq-string" -> Vector(Vector("a", "b", "c", "d")),
572 | "path" -> Vector(Vector("/foo/bar", "/home/me")))
573 |
574 | val remaining = Vector("foo", "bar")
575 |
576 | (args, values, allValues, remaining)
577 | }
578 |
579 |
580 | describe ("helpFlag") {
581 | it ("defines a help option") {
582 | assert(Args.helpFlag.name === Args.HELP_KEY)
583 | assert(Args.helpFlag.default === Some(false))
584 | }
585 | it ("doesn't consume a value") {
586 | val result = Args.helpFlag.parser(Seq("--help", "one", "two"))
587 | assert((Args.HELP_KEY, Success(true)) === result._1)
588 | assert(Seq("one", "two") === result._2)
589 | }
590 | }
591 |
592 | describe ("quietFlag") {
593 | it ("defines a quiet option") {
594 | assert(Args.quietFlag.default === Some(false))
595 | }
596 | it ("doesn't consume a value") {
597 | val result = Args.quietFlag.parser(Seq("--quiet", "one", "two"))
598 | assert(("quiet", Success(true)) === result._1)
599 | assert(Seq("one", "two") === result._2)
600 | }
601 | }
602 |
603 | describe ("remainingOpt") {
604 | it ("defines a 'remaining' option for the command-line tokens not associated with flags") {
605 | assert(Args.remainingOpt.name === Args.REMAINING_KEY)
606 | assert(Args.remainingOpt.flags === Nil)
607 | assert(Args.remainingOpt.default === None)
608 | }
609 | }
610 |
611 | describe ("socketOpt") {
612 | it ("defines a socket (host:port) option") {
613 | assert(Args.socketOpt().default === None)
614 | }
615 | it ("can be made required") {
616 | assert(Args.socketOpt(required = true).required === true)
617 | }
618 |
619 | describe ("""requires a string of the form "host:port" option""") {
620 | it ("can be provided a default value") {
621 | assert(Args.socketOpt(default = Some(("host",123))).default === Some(("host",123)))
622 | }
623 | it ("succeeds if the host is a name or IP address and the port is an integer") {
624 | val expected = (("socket", Try(("host", 123))), Nil)
625 | assert(Args.socketOpt().parser(Seq("--socket", "host:123")) === expected)
626 | }
627 | it ("returns a Failure(Opt.InvalidValueString) if the :port is missing") {
628 | Args.socketOpt().parser(Seq("--socket", "host"))
629 | val result = Args.socketOpt().parser(Seq("--socket", "host", "--bar"))
630 | val r1 = ("socket", Failure(Opt.InvalidValueString("--socket", "host", None)))
631 | val r2 = Seq("--bar")
632 | assert((r1, r2) === result)
633 | }
634 | it ("returns a Failure(Opt.InvalidValueString) if the host: is missing") {
635 | val result = Args.socketOpt().parser(Seq("--socket", "123", "--bar"))
636 | val r1 = ("socket", Failure(Opt.InvalidValueString("--socket", "123", None)))
637 | val r2 = Seq("--bar")
638 | assert((r1, r2) === result)
639 | }
640 | it ("returns a Failure(Opt.InvalidValueString) if the port is not an integer") {
641 | Args.socketOpt().parser(Seq("--socket", "host:foo", "--bar")) match {
642 | case (("socket", Failure(failure)), Seq("--bar")) => failure match {
643 | case Opt.InvalidValueString("--socket", "host:foo (not an int?)", Some(th)) => /* pass */
644 | case _ => fail("Unexpected exception: "+failure)
645 | }
646 | case badResult => fail(badResult.toString)
647 | }
648 | }
649 | }
650 | }
651 |
652 | describe ("MissingRequiredArgument") {
653 | it ("handles required arguments that weren't provided") {
654 | val mra = Args.MissingRequiredArgument(SpecHelper.longOpt)
655 | assert(mra.toString.contains(
656 | """Missing required argument: "long" with flags -l | --l | --long, long help message"""))
657 | }
658 | }
659 | }
660 |
--------------------------------------------------------------------------------
/core/src/test/scala/com/concurrentthought/cla/CLAPackageSpec.scala:
--------------------------------------------------------------------------------
1 | package com.concurrentthought.cla
2 | import org.scalatest.FunSpec
3 |
4 | class CLAPackageSpec extends FunSpec {
5 | import SpecHelper._
6 |
7 | describe ("package cla") {
8 | describe ("implicit class ToArgs") {
9 |
10 | it ("converts a multi line string into an Args") {
11 | val args = argsStr.toArgs
12 | assert(args.programInvocation === "java -cp ... foo")
13 | assert(args.leadingComments === "Some description and a second line.")
14 | assert(args.trailingComments === "Comments after the options, which can be multiple lines.")
15 | checkBefore(args)
16 | }
17 |
18 | it ("constructs an Args that parses arguments like any other Args") {
19 | val args = argsStr.toArgs.parse(Array(
20 | "--quiet",
21 | "--anti",
22 | "--input", "/foo/bar",
23 | "--output", "/out/baz",
24 | "--log-level", "4",
25 | "one", "two",
26 | "--path", s"a${pathDelim}b${pathDelim}c",
27 | "--things", "a-b|c",
28 | "three", "four"))
29 | assert(args.programInvocation === "java -cp ... foo")
30 | assert(args.leadingComments === "Some description and a second line.")
31 | assert(args.trailingComments === "Comments after the options, which can be multiple lines.")
32 | checkAfter(args)
33 | }
34 | }
35 |
36 | describe("The String Format") {
37 | describe ("starts with zero or more lines, with no leading spaces") {
38 | it ("uses a blank 'program invocation' and 'comments' if no such lines appear") {
39 | val str = """
40 | | -i | --in | --input string Path to input file.
41 | |""".stripMargin
42 | val args = str.toArgs
43 | assert(args.programInvocation === "")
44 | assert(args.leadingComments === "")
45 | assert(args.trailingComments === "")
46 | }
47 | it ("uses the first such line as the 'program invocation'.") {
48 | val str = """
49 | |java -cp ... foo
50 | | -i | --in | --input string Path to input file.
51 | |""".stripMargin
52 | val args = str.toArgs
53 | assert(args.programInvocation === "java -cp ... foo")
54 | assert(args.leadingComments === "")
55 | }
56 | it ("uses all subsequent lines as the 'leading comments', joined together into one, space-separated line") {
57 | val str = """
58 | |java -cp ... foo
59 | |Some description
60 | |and a second line.
61 | | -i | --in | --input string Path to input file.
62 | |""".stripMargin
63 | val args = str.toArgs
64 | assert(args.programInvocation === "java -cp ... foo")
65 | assert(args.leadingComments === "Some description and a second line.")
66 | assert(args.trailingComments === "")
67 | }
68 | it ("uses all trailing lines with no leading whitespace after the options as the 'trailing comments', joined together into one, space-separated line") {
69 | val str = """
70 | |java -cp ... foo
71 | |Some description
72 | |and a second line.
73 | | -i | --in | --input string Path to input file.
74 | |Comments after the options,
75 | |which can be multiple lines.
76 | |""".stripMargin
77 | val args = str.toArgs
78 | assert(args.programInvocation === "java -cp ... foo")
79 | assert(args.leadingComments === "Some description and a second line.")
80 | assert(args.trailingComments === "Comments after the options, which can be multiple lines.")
81 | }
82 | }
83 |
84 | describe ("it expects each option on a separate line, with leading whitespace") {
85 | it ("extracts the leading zero or more single and/or double '-' flags, separated by |") {
86 | val args = argsStr.toArgs
87 | val expectedFlags = Vector(
88 | Args.helpFlag.flags,
89 | Vector("-i", "--in" , "--input"),
90 | Vector("-o", "--out", "--output"),
91 | Vector("-l", "--log", "--log-level"),
92 | Vector("-p", "--path"),
93 | Vector("--things"))
94 | (args.opts.map(_.flags) zip expectedFlags) foreach { case (f, ef) => assert(f === ef) }
95 | }
96 | }
97 | }
98 | }
99 |
100 | protected def checkBefore(args: Args) = {
101 | // assert(args.opts === expectedOpts)
102 | (args.opts zip expectedOpts) foreach { case (o, eo) => assert(o === eo) }
103 | assert(args.defaults === expectedDefaults)
104 | assert(args.values === expectedValuesBefore)
105 | assert(args.allValues === expectedAllValuesBefore)
106 | assert(args.remaining === expectedRemainingBefore)
107 | }
108 |
109 | protected def checkAfter(args: Args) = {
110 | (args.opts zip expectedOpts) foreach { case (o, eo) => assert(o === eo) }
111 | assert(args.defaults === expectedDefaults)
112 | assert(args.values === expectedValuesAfter)
113 | assert(args.allValues === expectedAllValuesAfter)
114 | assert(args.remaining === expectedRemainingAfter)
115 | }
116 | }
--------------------------------------------------------------------------------
/core/src/test/scala/com/concurrentthought/cla/HelpSpec.scala:
--------------------------------------------------------------------------------
1 | package com.concurrentthought.cla
2 | import org.scalatest.FunSpec
3 |
4 | class HelpSpec extends FunSpec {
5 | import SpecHelper._
6 |
7 | val inOpt = Opt.string(
8 | name = "in",
9 | flags = Seq("-i", "--in", "--input"),
10 | help = "Input files with an extremely long help message that should be wrapped by Help so it doesn't run off the screen like it does in this test source file!",
11 | requiredFlag = true)
12 | val outOpt = Opt.string(
13 | name = "out",
14 | flags = Seq("-o", "--o", "--out", "--output"),
15 | default = Some("/dev/null"),
16 | help = "Output files.")
17 | val reqOpt1 = Opt.string(
18 | name = "req1",
19 | flags = Seq("-r1", "--r1", "--req1"),
20 | help = "Required opt. 1",
21 | requiredFlag = true)
22 | val reqOpt2 = Opt.string(
23 | name = "req2",
24 | flags = Seq("-r2", "--r2", "--req2"),
25 | default = Some("foo"),
26 | help = "Required opt. 2 (not really required)",
27 | requiredFlag = true)
28 |
29 | describe ("Help") {
30 | it ("returns a help string based on the command-line arguments") {
31 | val args = Args(
32 | "java HelpSpec",
33 | "A ScalaTest for the Help class.",
34 | "Trailing comments.",
35 | Seq(inOpt, outOpt, reqOpt1, reqOpt2, Args.quietFlag))
36 | val help = Help(args)
37 | assert(help ===
38 | s"""Usage: java HelpSpec [options]
39 | |A ScalaTest for the Help class.
40 | |Where the supported options are the following:
41 | |
42 | | [-h | --h | --help] Show this help message.
43 | | -i | --in | --input in Input files with an extremely long help message that should
44 | | be wrapped by Help so it doesn't run off the screen like it
45 | | does in this test source file!
46 | | [-o | --o | --out | --output out] Output files.
47 | | (default: /dev/null)
48 | | -r1 | --r1 | --req1 req1 Required opt. 1
49 | | [-r2 | --r2 | --req2 req2] Required opt. 2 (not really required)
50 | | (default: foo)
51 | | [-q | --quiet] Suppress some verbose output.
52 | | [remaining] All remaining arguments that aren't associated with flags.
53 | |
54 | |You can also use --foo=bar syntax. Arguments shown in [...] are optional. All others are required.
55 | |Trailing comments.
56 | |""".stripMargin)
57 | }
58 |
59 | def doOptionalArgs() = {
60 | val help = Help(Args("java HelpSpec", Nil))
61 | assert(help ===
62 | s"""Usage: java HelpSpec [options]
63 | |
64 | |Where the supported options are the following:
65 | |
66 | | [-h | --h | --help] Show this help message.
67 | | [remaining] All remaining arguments that aren't associated with flags.
68 | |
69 | |
70 | |
71 | |""".stripMargin)
72 | }
73 |
74 | it ("returns a help string even when help is the only command-line argument supported") {
75 | doOptionalArgs
76 | }
77 | it ("returns a help string with [...] around optional arguments") {
78 | doOptionalArgs
79 | }
80 | it ("defaults the 'remaining' arguments to optional") {
81 | doOptionalArgs
82 | }
83 | it ("the 'remaining' arguments can be specified explicitly to make them required") {
84 | val help = Help(Args("java HelpSpec", Seq(Args.makeRemainingOpt(requiredFlag=true))))
85 | assert(help ===
86 | s"""Usage: java HelpSpec [options]
87 | |
88 | |Where the supported options are the following:
89 | |
90 | | [-h | --h | --help] Show this help message.
91 | | remaining All remaining arguments that aren't associated with flags.
92 | |
93 | |
94 | |
95 | |""".stripMargin)
96 | }
97 |
98 | it ("returns a help string that includes the error messages after parsing") {
99 | val args = Args(
100 | "java HelpSpec",
101 | "A ScalaTest for no user-defined options.",
102 | "Trailing comments.",
103 | Seq(intOpt))
104 | .parse(Seq("--foo", "--int", "x"))
105 | val help = Help(args)
106 | assert(help ===
107 | s"""Usage: java HelpSpec [options]
108 | |A ScalaTest for no user-defined options.
109 | |The following parsing errors occurred:
110 | | Unrecognized argument (or missing value): --foo (rest of arguments: --int x)
111 | | Invalid value string: x for option --int (cause: java.lang.NumberFormatException: For input string: "x")
112 | |Where the supported options are the following:
113 | |
114 | | [-h | --h | --help] Show this help message.
115 | | [-i | --i | --int int] int help message
116 | | (default: 0)
117 | | [remaining] All remaining arguments that aren't associated with flags.
118 | |
119 | |You can also use --foo=bar syntax. Arguments shown in [...] are optional. All others are required.
120 | |Trailing comments.
121 | |""".stripMargin)
122 | }
123 | }
124 | }
--------------------------------------------------------------------------------
/core/src/test/scala/com/concurrentthought/cla/OptParserSpec.scala:
--------------------------------------------------------------------------------
1 | package com.concurrentthought.cla
2 | import org.scalatest.FunSpec
3 | import org.scalatest.prop.PropertyChecks
4 | import org.scalacheck.Gen
5 | import org.parboiled.scala.testing.ParboiledTest
6 |
7 | class OptParserSpec extends FunSpec with PropertyChecks with ParboiledTest {
8 | import OptParser._
9 | import Elems._
10 |
11 | val leadingChar = ('a' to 'z') ++ ('A' to 'Z') ++ ('0' to '9') :+ '_'
12 | val leadingChars = Gen.oneOf(leadingChar)
13 | val trailingChar = Gen.oneOf('-' +: leadingChar)
14 | val trailingStrings = Gen.listOf(trailingChar)
15 |
16 | val typesWithInit = Vector (
17 | ("string=foo", StringTypeElem("=foo"), Some("foo")),
18 | ("byte=127", ByteTypeElem("=127"), Some(127)),
19 | ("char=127", CharTypeElem("=127"), Some('1')),
20 | ("int=256", IntTypeElem("=256"), Some(256)),
21 | ("long=512", LongTypeElem("=512"), Some(512)),
22 | ("float=1.2", FloatTypeElem("=1.2"), Some(1.2F)),
23 | ("double=2.3", DoubleTypeElem("=2.3"), Some(2.3)),
24 | ("seq(:)=a:b:c", SeqTypeElem(":","(:)=a:b:c"), Some("a:b:c")), // yes, odd SeqTypeElem args
25 | ("path=a:b:c", PathTypeElem("=a:b:c"), Some("a:b:c")))
26 |
27 | val flagsTypes = Vector (
28 | ("flag", FlagTypeElem("false"), Some(false)),
29 | ("~flag", FlagTypeElem("true"), Some(true)))
30 |
31 | val typesWithoutInit = Vector (
32 | ("string", StringTypeElem(""), None),
33 | ("byte", ByteTypeElem(""), None),
34 | ("char", CharTypeElem(""), None),
35 | ("int", IntTypeElem(""), None),
36 | ("long", LongTypeElem(""), None),
37 | ("float", FloatTypeElem(""), None),
38 | ("double", DoubleTypeElem(""), None),
39 | ("seq(:)", SeqTypeElem(":","(:)"), None), // yes, odd SeqTypeElem args
40 | ("path", PathTypeElem(""), None)) ++ flagsTypes
41 |
42 | // From parsing SpecHelper.argsStr:
43 | val expectedElems = Vector(
44 | Right(OptElem(false,FlagsAndTypeElem(FlagsElem(List(FlagElem("-i"), FlagElem("--in"), FlagElem("--input"))), StringTypeElem("")), "Path to input file.")),
45 | Right(OptElem(true, FlagsAndTypeElem(FlagsElem(List(FlagElem("-o"), FlagElem("--out"), FlagElem("--output"))), StringTypeElem("=/dev/null")), "Path to output file.")),
46 | Right(OptElem(true, FlagsAndTypeElem(FlagsElem(List(FlagElem("-l"), FlagElem("--log"), FlagElem("--log-level"))), IntTypeElem("=3")), "Log level to use.")),
47 | Right(OptElem(true, FlagsAndTypeElem(FlagsElem(List(FlagElem("-p"), FlagElem("--path"))), PathTypeElem("")), "Path elements separated by ':' (*nix) or ';' (Windows).")),
48 | Right(OptElem(false,FlagsAndTypeElem(FlagsElem(List(FlagElem("--things"))), SeqTypeElem("[-|]","([-|])")), "Path elements separated by '-' or '|'.")),
49 | Right(OptElem(true, FlagsAndTypeElem(FlagsElem(List(FlagElem("-q"), FlagElem("--quiet"))), FlagTypeElem("false")), "Suppress some verbose output.")),
50 | Right(OptElem(true, FlagsAndTypeElem(FlagsElem(List(FlagElem("-a"), FlagElem("--anti"))), FlagTypeElem("true")), """An "antiflag" (defaults to true).""")),
51 | Right(OptElem(true, RemainingElem("others"), "Other stuff.")))
52 |
53 | def fail(message: String): Nothing = super.fail(message)
54 |
55 | protected def forNames[L,R](
56 | mkStrs: String => Seq[String])(
57 | actual: String => Either[L,R])(
58 | expected: String => Either[L,R]) = {
59 | forAll(leadingChars, trailingStrings) {
60 | (leading: Char, trailing: Seq[Char]) =>
61 | val s = (leading +: trailing).mkString("")
62 | for { s2 <- Seq(s, "_"+s); s3 <- mkStrs(s2) } {
63 | assert(actual(s3) === expected(s2))
64 | val three = s"--${s3.trim}"
65 | actual(three) match {
66 | case Right(_) => fail(s"Didn't fail with --$three")
67 | case Left(_) => // pass
68 | }
69 | }
70 | }
71 | }
72 |
73 | describe ("OptParser") {
74 | describe ("Name parser returns a name as an StringElem") {
75 | it ("expects the name to begin with a number, letter, or '_' and zero or more trailing characters from the same set plus '-'") {
76 | forNames(s => Seq(s))(s => parseWithRule(s, Name))(s => Right(StringElem(s)))
77 | }
78 | }
79 | describe ("Flag parser returns a '-name' or '--name' as an FlagElem") {
80 | it ("expects the flag to begin with a '-' or '--' followed by a valid name") {
81 | forNames(s => Seq("-"+s) )(s => parseWithRule(s, Flag))(s => Right(FlagElem("-"+s)) )
82 | forNames(s => Seq("--"+s))(s => parseWithRule(s, Flag))(s => Right(FlagElem("--"+s)) )
83 | }
84 | }
85 | describe ("Flags parser returns an FlagsElem with flags") {
86 | it ("expects the sequence of flags separated by '|' and optional white space") {
87 | forNames{ s =>
88 | val c = s.charAt(0)
89 | Seq(s" -$c|--$c |-$s| --$s ")
90 | }{
91 | s => parseWithRule(s, Flags)
92 | }{ s =>
93 | val c = s.charAt(0)
94 | val l = List(FlagElem("-"+c), FlagElem("--"+c),
95 | FlagElem("-"+s), FlagElem("--"+s))
96 | Right(FlagsElem(l))
97 | }
98 | }
99 | }
100 |
101 | describe ("Type parser returns an TypeElem") {
102 | it ("returns the corresponding type or name element when no initializer is specified") {
103 | typesWithoutInit foreach { case (flag, expectedElem, initVal) =>
104 | parseWithRule(flag, TypeAndInit) match {
105 | case Left(ex) => fail(ex)
106 | case Right(term) =>
107 | assert(term === expectedElem)
108 | assert(term.initialValue == initVal)
109 | }
110 | }
111 | }
112 |
113 | it ("returns the corresponding type or name element with an initializer, when specified") {
114 | typesWithInit foreach { case (flag, expectedElem, initVal) =>
115 | parseWithRule(flag, TypeAndInit) match {
116 | case Left(ex) => fail(ex)
117 | case Right(term) =>
118 | assert(term === expectedElem)
119 | assert(term.initialValue == initVal, flag)
120 | }
121 | }
122 | }
123 | }
124 |
125 | def toFlags(flag: String) = {
126 | val c = flag(0)
127 | val s = flag.split("[=(]")(0)
128 | s"-$c| --$c| -$s|--$s"
129 | }
130 |
131 | def makeFlagsAndTypesElem(flag: String, expectedTypeElem: TypeElem[_]) = {
132 | val flag2 = if (flag == "~flag") "not-flag" else flag
133 | val flagsStr = toFlags(flag2)
134 | val flags = flagsStr.split("""\s*\|\s*""").map(_.trim).toList
135 | val s = s"$flagsStr $flag"
136 | val flagsElem = FlagsElem(flags map (s => FlagElem(s)))
137 | (s, FlagsAndTypeElem(flagsElem, expectedTypeElem))
138 | }
139 |
140 | describe ("FlagsAndType parser") {
141 | it ("returns a list of flags and the type indicator") {
142 | typesWithoutInit ++ typesWithInit foreach {
143 | case (flag, expectedTypeElem, initVal) =>
144 | val (s, expected) = makeFlagsAndTypesElem(flag, expectedTypeElem)
145 |
146 | parseWithRule(s, FlagsAndType) match {
147 | case Left(ex) => fail(ex)
148 | case Right(term) => assert(term === expected)
149 | }
150 | }
151 | }
152 | it ("rejects flag and ~flag with initializers") {
153 | Seq("", "~") foreach { prefix =>
154 | val f = s"[-f | --flag ${prefix}flag=foo]"
155 | parseWithRule(f, FlagsAndType) match {
156 | case Right(term) => fail(s"$f succeeded ($term), but should have failed!")
157 | case Left(ex) =>
158 | assert(ex.toString.contains(s"ParsingException: Invalid input: `$f'"))
159 | }
160 | }
161 | }
162 | }
163 |
164 | describe ("Full Opt parser") {
165 | it ("returns an option with a list of flags, the type indicator, and optional help") {
166 | typesWithoutInit ++ typesWithInit foreach {
167 | case (flag, expectedTypeElem, initVal) =>
168 | val (s, expectedFTE) = makeFlagsAndTypesElem(flag, expectedTypeElem)
169 |
170 | Seq("", "with help") foreach { help =>
171 | val expected = OptElem(false, expectedFTE, help)
172 | OptParser.parse(s"$s $help") match {
173 | case Left(ex) => fail(ex)
174 | case Right(term) => assert(term === expected)
175 | }
176 | }
177 | }
178 | }
179 | it ("accepts optional arguments enclosed in [...]") {
180 | typesWithoutInit ++ typesWithInit foreach {
181 | case (flag, expectedTypeElem, initVal) =>
182 | val (s, expectedFTE) = makeFlagsAndTypesElem(flag, expectedTypeElem)
183 |
184 | Seq("", "with help") foreach { help =>
185 | val expected = OptElem(true, expectedFTE, help)
186 | OptParser.parse(s"[$s] $help") match {
187 | case Left(ex) => fail(ex)
188 | case Right(term) => assert(term === expected)
189 | }
190 | }
191 | }
192 | }
193 | it ("parses a 'remaining arguments' line") {
194 | Seq(
195 | (s"%s", false, ""),
196 | (s"[%s]", true, ""),
197 | (s"%s with help", false, "with help"),
198 | (s"[%s] with help", true, "with help")) foreach { case (fmt, bool, help) =>
199 |
200 | forNames(s => Seq(fmt.format(s)))(s => OptParser.parse(s))(
201 | s => Right(OptElem(bool, RemainingElem(s), help)))
202 |
203 | }
204 | }
205 | it ("parses each valid line it is fed from a real specification") {
206 | SpecHelper.argsStr.split("\n").filter(_.startsWith(" ")).toSeq.zip(expectedElems) foreach {
207 | case (line, expected) =>
208 | assert(OptParser.parse(line) === expected)
209 | }
210 | }
211 | }
212 | }
213 | }
214 |
--------------------------------------------------------------------------------
/core/src/test/scala/com/concurrentthought/cla/OptSpec.scala:
--------------------------------------------------------------------------------
1 | package com.concurrentthought.cla
2 | import org.scalatest.FunSpec
3 | import scala.util.{Try, Success, Failure}
4 |
5 | class OptSpec extends FunSpec {
6 | import SpecHelper._
7 |
8 | describe ("case class Opt") {
9 | it ("requires a non-empty name") {
10 | intercept [IllegalArgumentException] {
11 | Opt.string(
12 | name = "",
13 | flags = Seq("-i", "--in", "--input"),
14 | default = Some("/data/input"),
15 | help = "help message")
16 | }
17 | () // Suppress -Ywarn-value-discard warning
18 | }
19 |
20 | it ("allows an empty help message") {
21 | // No exception thrown:
22 | Opt.string(
23 | name = "in",
24 | flags = Seq("-i", "--in", "--input"))
25 | () // Suppress -Ywarn-value-discard warning
26 | }
27 |
28 | it ("can be required") {
29 | val opt = Opt.string(
30 | name = "in",
31 | flags = Seq("-i", "--in", "--input"),
32 | requiredFlag = true)
33 | assert(opt.required === true)
34 | }
35 |
36 | it ("defaults to not required") {
37 | val opt = Opt.string(
38 | name = "in",
39 | flags = Seq("-i", "--in", "--input"))
40 | assert(opt.required === false)
41 | }
42 |
43 | it ("ignores the required flag if the default is not None") {
44 | val opt = Opt.string(
45 | name = "in",
46 | flags = Seq("-i", "--in", "--input"),
47 | default = Some("foo"),
48 | requiredFlag = true)
49 | assert(opt.required === false)
50 | }
51 |
52 | it ("allows the list of flags to be empty (but see Args requirements)") {
53 | // No exception thrown:
54 | Opt.string(
55 | name = "in",
56 | flags = Nil)
57 | () // Suppress -Ywarn-value-discard warning
58 | }
59 |
60 | it ("allows the value to default to None") {
61 | val opt = Opt.string(
62 | name = "in",
63 | flags = Seq("-i", "--in", "--input"),
64 | help = "help message")
65 | assert(opt.default === None)
66 | }
67 |
68 | it ("accepts a non-None value") {
69 | assert(stringOpt.default === Some("foobar"))
70 | }
71 |
72 | describe("parsing tokens") {
73 | it ("extracts the flag and (optional) value from the sequence") {
74 | val result = stringOpt.parser(Seq("--string", "foo", "one", "two"))
75 | assert(("string", Success("foo")) === result._1)
76 | assert(Seq("one", "two") === result._2)
77 | }
78 | it ("handles 'flag=value' or 'flag value' forms") {
79 | val result = stringOpt.parser(Seq("--string", "foo", "one", "two"))
80 | assert(("string", Success("foo")) === result._1)
81 | assert(Seq("one", "two") === result._2)
82 | val result2 = stringOpt.parser(Seq("--string=foo", "one", "two"))
83 | assert(("string", Success("foo")) === result2._1)
84 | assert(Seq("one", "two") === result2._2)
85 | }
86 | it ("converts the value to the expected type") {
87 | val result = intOpt.parser(Seq("--int", "1234", "one", "two"))
88 | assert(("int", Success(1234)) === result._1)
89 | assert(Seq("one", "two") === result._2)
90 | }
91 | it ("returns a Failure if the value can't be converted to the expected type") {
92 | val result = intOpt.parser(Seq("--int", "xyz", "one", "two"))
93 | val (name, Failure(ex)) = result._1
94 | assert("int" === name)
95 | ex match {
96 | case Opt.InvalidValueString("--int", "xyz", _) => /* okay */
97 | case _ => fail(ex.toString)
98 | }
99 | assert(Seq("one", "two") === result._2)
100 | }
101 | }
102 | }
103 |
104 | describe ("object Opt") {
105 |
106 | describe ("flag() constructs a Boolean option") {
107 | it ("defaults the value to false") {
108 | assert(helpFlag.default === Some(false))
109 | }
110 |
111 | it ("can be set to use true as the default value") {
112 | assert(antiFlag.default === Some(true))
113 | }
114 |
115 | it ("does not consume any values in the argument list.") {
116 | val result = helpFlag.parser(Seq("--help", "one", "two"))
117 | assert(Seq("one", "two") === result._2)
118 | }
119 |
120 | it ("returns true if the option is used and the default was false.") {
121 | val result = helpFlag.parser(Seq("--help", "one", "two"))
122 | assert((Args.HELP_KEY, Success(true)) === result._1)
123 | }
124 |
125 | it ("returns false if the option is used and the default was true.") {
126 | val result = antiFlag.parser(Seq("--anti", "one", "two"))
127 | assert(("anti", Success(false)) === result._1)
128 | }
129 | }
130 |
131 | describe ("string() constructs a String option") {
132 | it ("returns the value string unmodified") {
133 | val result = stringOpt.parser(Seq("--string", "foo", "one", "two"))
134 | assert(("string", Success("foo")) === result._1)
135 | assert(Seq("one", "two") === result._2)
136 | }
137 | it ("accepts an empty value string") {
138 | val result = stringOpt.parser(Seq("--string", "", "one", "two"))
139 | assert(("string", Success("")) === result._1)
140 | assert(Seq("one", "two") === result._2)
141 | }
142 | }
143 |
144 | describe ("char() constructs a Char option") {
145 | it ("returns the first character of the value string") {
146 | val result = charOpt.parser(Seq("--char", "foo", "one", "two"))
147 | assert(("char", Success('f')) === result._1)
148 | assert(Seq("one", "two") === result._2)
149 | }
150 | it ("requires a non-empty value string") {
151 | val result = charOpt.parser(Seq("--char", "foo", "one", "two"))
152 | assert(("char", Success('f')) === result._1)
153 | assert(Seq("one", "two") === result._2)
154 | }
155 | it ("returns a Failure(Opt.InvalidValueString) if the value string is empty") {
156 | val result = charOpt.parser(Seq("--char", "", "one", "two"))
157 | result._1 match {
158 | case ("char", Failure(Opt.InvalidValueString("--char", "", Some(ex)))) => /* pass */
159 | case bad => fail(bad.toString)
160 | }
161 | assert(Seq("one", "two") === result._2)
162 | }
163 | }
164 |
165 | describe ("byte() constructs a Byte option") {
166 | it ("parses the string into a Byte") {
167 | val result = byteOpt.parser(Seq("--byte", "126", "one", "two"))
168 | assert(("byte", Success(126)) === result._1)
169 | assert(Seq("one", "two") === result._2)
170 | }
171 | it ("returns a Failure(Opt.InvalidValueString) if the value is not an integer") {
172 | byteOpt.parser(Seq("--byte", "x", "--bar")) match {
173 | case (("byte", Failure(failure)), Seq("--bar")) => failure match {
174 | case Opt.InvalidValueString("--byte", "x", Some(th)) => /* pass */
175 | case _ => fail("Unexpected exception: "+failure)
176 | }
177 | case badResult => fail(badResult.toString)
178 | }
179 | }
180 | }
181 |
182 | describe ("int() constructs an Int option") {
183 | it ("parses the string into an Int") {
184 | val result = intOpt.parser(Seq("--int", "1000", "one", "two"))
185 | assert(("int", Success(1000)) === result._1)
186 | assert(Seq("one", "two") === result._2)
187 | }
188 | it ("returns a Failure(Opt.InvalidValueString) if the value is not an integer") {
189 | intOpt.parser(Seq("--int", "x", "--bar")) match {
190 | case (("int", Failure(failure)), Seq("--bar")) => failure match {
191 | case Opt.InvalidValueString("--int", "x", Some(th)) => /* pass */
192 | case _ => fail("Unexpected exception: "+failure)
193 | }
194 | case badResult => fail(badResult.toString)
195 | }
196 | }
197 | }
198 |
199 | describe ("long() constructs a Long option") {
200 | it ("parses the string into a Long") {
201 | val result = longOpt.parser(Seq("--long", "100000000", "one", "two"))
202 | assert(("long", Success(100000000)) === result._1)
203 | assert(Seq("one", "two") === result._2)
204 | }
205 | it ("returns a Failure(Opt.InvalidValueString) if the value is not an integer") {
206 | longOpt.parser(Seq("--long", "x", "--bar")) match {
207 | case (("long", Failure(failure)), Seq("--bar")) => failure match {
208 | case Opt.InvalidValueString("--long", "x", Some(th)) => /* pass */
209 | case _ => fail("Unexpected exception: "+failure)
210 | }
211 | case badResult => fail(badResult.toString)
212 | }
213 | }
214 | }
215 |
216 | describe ("float() constructs a Float option") {
217 | it ("parses the string into a Float") {
218 | val result = floatOpt.parser(Seq("--float", "126.1", "one", "two"))
219 | assert(("float", Success(126.1F)) === result._1)
220 | assert(Seq("one", "two") === result._2)
221 | val result2 = floatOpt.parser(Seq("--float", "126", "one", "two"))
222 | assert(("float", Success(126F)) === result2._1)
223 | assert(Seq("one", "two") === result2._2)
224 | }
225 | it ("returns a Failure(Opt.InvalidValueString) if the value is not a number") {
226 | floatOpt.parser(Seq("--float", "x", "--bar")) match {
227 | case (("float", Failure(failure)), Seq("--bar")) => failure match {
228 | case Opt.InvalidValueString("--float", "x", Some(th)) => /* pass */
229 | case _ => fail("Unexpected exception: "+failure)
230 | }
231 | case badResult => fail(badResult.toString)
232 | }
233 | }
234 | }
235 |
236 | describe ("double() constructs a Double option") {
237 | it ("parses the string into a Double") {
238 | val result = doubleOpt.parser(Seq("--double", "126.2", "one", "two"))
239 | assert(("double", Success(126.2)) === result._1)
240 | assert(Seq("one", "two") === result._2)
241 | val result2 = doubleOpt.parser(Seq("--double", "126", "one", "two"))
242 | assert(("double", Success(126F)) === result2._1)
243 | assert(Seq("one", "two") === result2._2)
244 | }
245 | it ("returns a Failure(Opt.InvalidValueString) if the value is not a number") {
246 | doubleOpt.parser(Seq("--double", "x", "--bar")) match {
247 | case (("double", Failure(failure)), Seq("--bar")) => failure match {
248 | case Opt.InvalidValueString("--double", "x", Some(th)) => /* pass */
249 | case _ => fail("Unexpected exception: "+failure)
250 | }
251 | case badResult => fail(badResult.toString)
252 | }
253 | }
254 | }
255 |
256 | describe ("seq[V]() constructs a Seq[V] option") {
257 | it ("parses the string into a Seq[V]") {
258 | val result = seqOpt.parser(Seq("--seq", "111.3:126.2_123.4-354.6", "one", "two"))
259 | assert((("seq", Try(Seq(111.3, 126.2, 123.4, 354.6))), Seq("one", "two")) === result)
260 | }
261 | it ("returns a Failure(Opt.InvalidValueString) if the value fails to parse") {
262 | seqOpt.parser(Seq("--seq", "a:b_c-d", "--bar")) match {
263 | case (("seq", Failure(failure)), Seq("--bar")) => failure match {
264 | case Opt.InvalidValueString("--seq", "a:b_c-d", Some(th)) => /* pass */
265 | case _ => fail("Unexpected exception: "+failure)
266 | }
267 | case badResult => fail(badResult.toString)
268 | }
269 | }
270 | }
271 |
272 | describe ("seqString() constructs a Seq[String] option") {
273 | it ("splits the string into a Seq[String]") {
274 | val result = seqStringOpt.parser(Seq("--seq-string", "111.3:126.2_123.4-354.6", "one", "two"))
275 | assert((("seq-string", Try(Seq("111.3", "126.2", "123.4", "354.6"))), Seq("one", "two")) === result)
276 | }
277 | }
278 |
279 | describe ("path() constructs a Seq[String] option or platform-specific path, like CLASSPATH") {
280 | it ("""splits the string into a Seq[String] using the delimiter given by sys.props.getOrElse("path.separator",":")""") {
281 | val path1 = Seq("/foo/bar", "/home/me")
282 | val path =
283 | if (pathDelim != ":") path1.map(s => "C:"+s).mkString(pathDelim)
284 | else path1.mkString(pathDelim)
285 | val expected = Try(path.split(pathDelim).toVector)
286 | val result = pathOpt.parser(Seq("--path", path, "one", "two"))
287 | assert((("path", expected), Seq("one", "two")) === result)
288 | }
289 | }
290 | }
291 | }
--------------------------------------------------------------------------------
/core/src/test/scala/com/concurrentthought/cla/SpecHelper.scala:
--------------------------------------------------------------------------------
1 | package com.concurrentthought.cla
2 |
3 | object SpecHelper {
4 |
5 | val noremains = Args.REMAINING_KEY -> Vector.empty[String]
6 |
7 | val helpFlag = Opt.flag(
8 | name = Args.HELP_KEY,
9 | flags = Seq("-h", "--h", "--help"),
10 | help = "help message")
11 |
12 | val antiFlag = Opt.notflag(
13 | name = "anti",
14 | flags = Seq("-a", "--anti"),
15 | help = "anti help message")
16 |
17 | val stringOpt = Opt.string(
18 | name = "string",
19 | flags = Seq("-s", "--s", "--string"),
20 | default = Some("foobar"),
21 | help = "string help message")
22 |
23 | val charOpt = Opt.char(
24 | name = "char",
25 | flags = Seq("-c", "--c", "--char"),
26 | default = Some('x'),
27 | help = "string help message")
28 |
29 | val byteOpt = Opt.byte(
30 | name = "byte",
31 | flags = Seq("-b", "--b", "--byte"),
32 | default = Some(0),
33 | help = "byte help message")
34 |
35 | val intOpt = Opt.int(
36 | name = "int",
37 | flags = Seq("-i", "--i", "--int"),
38 | default = Some(0),
39 | help = "int help message")
40 |
41 | val longOpt = Opt.long(
42 | name = "long",
43 | flags = Seq("-l", "--l", "--long"),
44 | default = Some(0),
45 | help = "long help message")
46 |
47 | val floatOpt = Opt.float(
48 | name = "float",
49 | flags = Seq("-f", "--f", "--float"),
50 | default = Some(0.0F),
51 | help = "float help message")
52 |
53 | val doubleOpt = Opt.double(
54 | name = "double",
55 | flags = Seq("-d", "--d", "--double"),
56 | default = Some(0.0),
57 | help = "double help message")
58 |
59 | val seqOpt = Opt.seq[Double](delimsRE = "[-_:]")(
60 | name = "seq",
61 | flags = Seq("-s", "--s", "--seq"),
62 | default = Some(Nil),
63 | help = "seq help message")(Opt.toTry(_.toDouble))
64 |
65 |
66 | val seqStringOpt = Opt.seqString(delimsRE = "[-_:]")(
67 | name = "seq-string",
68 | flags = Seq("-ss", "--ss", "--seq-string"),
69 | default = Some(Nil),
70 | help = "seq-string help message")
71 |
72 | val pathOpt = Opt.path(
73 | name = "path",
74 | flags = Seq("-p", "--path"),
75 | default = Some(Nil))
76 |
77 | val othersOpt = Args.makeRemainingOpt(
78 | name = "others",
79 | help = "Other tokens")
80 |
81 | val pathDelim = sys.props.getOrElse("path.separator",":")
82 |
83 | protected val allOpts1 = Vector(helpFlag, antiFlag, stringOpt, byteOpt, charOpt,
84 | intOpt, longOpt, floatOpt, doubleOpt, seqOpt, seqStringOpt, pathOpt)
85 | val allOpts = allOpts1 :+ othersOpt
86 | val allDefaults = allOpts1.map(o => (o.name, o.default.get)).toMap
87 | val allRemaining = Vector.empty[String]
88 |
89 |
90 | val argsStr = """
91 | |java -cp ... foo
92 | |Some description
93 | |and a second line.
94 | | -i | --in | --input string Path to input file.
95 | | [-o | --out | --output string=/dev/null] Path to output file.
96 | | [-l | --log | --log-level int=3] Log level to use.
97 | | [-p | --path path] Path elements separated by ':' (*nix) or ';' (Windows).
98 | | --things seq([-|]) Path elements separated by '-' or '|'.
99 | | [-q | --quiet flag] Suppress some verbose output.
100 | | [-a | --anti ~flag] An "antiflag" (defaults to true).
101 | | [others] Other stuff.
102 | |Comments after the options,
103 | |which can be multiple lines.
104 | |""".stripMargin
105 |
106 | val expectedOpts = Vector(Args.helpFlag,
107 | Opt.string( "input", Vector("-i", "--in" , "--input"), None, "Path to input file.", true),
108 | Opt.string( "output", Vector("-o", "--out", "--output"), Some("/dev/null"), "Path to output file."),
109 | Opt.int ( "log-level", Vector("-l", "--log", "--log-level"), Some(3), "Log level to use."),
110 | Opt.path ( "path", Vector("-p", "--path"), None, "Path elements separated by ':' (*nix) or ';' (Windows)."),
111 | Opt.seqString("""[-|]""")
112 | ( "things", Vector("--things"), None, "Path elements separated by '-' or '|'.", true),
113 | Opt.flag( "quiet", Vector("-q", "--quiet"), "Suppress some verbose output."),
114 | Opt.notflag("anti", Vector("-a", "--anti"), "An \"antiflag\" (defaults to true)."),
115 | Args.makeRemainingOpt("others", "Other stuff."))
116 |
117 | val expectedDefaults = Map[String,Any](
118 | Args.HELP_KEY -> false,
119 | "quiet" -> false,
120 | "anti" -> true,
121 | "output" -> "/dev/null",
122 | "log-level" -> 3)
123 |
124 | val expectedValuesBefore = Map[String,Any](
125 | Args.HELP_KEY -> false,
126 | "quiet" -> false,
127 | "anti" -> true,
128 | "output" -> "/dev/null",
129 | "log-level" -> 3)
130 |
131 |
132 | val expectedAllValuesBefore =
133 | expectedValuesBefore map { case (k,v) => k -> Vector(v) }
134 |
135 | val expectedValuesAfter = Map[String,Any](
136 | Args.HELP_KEY -> false,
137 | "quiet" -> true,
138 | "anti" -> false,
139 | "input" -> "/foo/bar",
140 | "output" -> "/out/baz",
141 | "log-level" -> 4,
142 | "path" -> Vector("a", "b", "c"),
143 | "things" -> Vector("a", "b", "c"))
144 |
145 | val expectedRemainingBefore = Vector.empty[String]
146 | val expectedRemainingAfter = Vector("one", "two", "three", "four")
147 |
148 | val expectedAllValuesAfter =
149 | expectedValuesAfter.map { case (k,v) => k -> Vector(v) }
150 |
151 | }
--------------------------------------------------------------------------------
/core/src/test/scala/com/concurrentthought/cla/StringOut.scala:
--------------------------------------------------------------------------------
1 | package com.concurrentthought.cla
2 | import java.io._
3 |
4 | class StringOut {
5 |
6 | private val bytes = new ByteArrayOutputStream()
7 | val out: PrintStream = new PrintStream(bytes)
8 |
9 | override def toString = bytes.toString("ISO-8859-1")
10 | }
11 |
12 |
--------------------------------------------------------------------------------
/examples/src/main/scala/com/concurrentthought/cla/examples/CLASampleMain.scala:
--------------------------------------------------------------------------------
1 | package com.concurrentthought.cla.examples
2 | import com.concurrentthought.cla._
3 |
4 | /**
5 | * Demonstrates how to use the API. Try running with different arguments,
6 | * including `--help`. Try running the following examples within SBT:
7 | * {{{
8 | * run-main com.concurrentthought.cla.examples.CLASampleMain -h
9 | * run -h
10 | * run --help
11 | * run -i /in -o /out -l 4 -p a:b --things x-y|z foo bar baz
12 | * run --in /in --out=/out -l=4 --path "a:b" --things=x-y|z -q foo bar baz
13 | * }}}
14 | * The last example demonstrates that both `argflag value` and `argflag=value`
15 | * syntax is supported. The "[...]" indicate optional arguments, so in this example,
16 | * you must specify the `input` argument and at least one token for "others".
17 | */
18 | object CLASampleMain {
19 |
20 | def main(argstrings: Array[String]): Unit = {
21 | val initialArgs: Args = """
22 | |run-main CLASampleMain [options]
23 | |Demonstrates the CLA API.
24 | | -i | --in | --input string Path to input file.
25 | | [-o | --out | --output string=/dev/null] Path to output file.
26 | | [-l | --log | --log-level int=3] Log level to use.
27 | | [-p | --path path] Path elements separated by ':' (*nix) or ';' (Windows).
28 | | [--things seq([-|])] String elements separated by '-' or '|'.
29 | | [-q | --quiet flag] Suppress some verbose output.
30 | | others Other arguments.
31 | |Note that --input and "others" are required.
32 | |""".stripMargin.toArgs
33 |
34 | val finalArgs: Args = initialArgs.process(argstrings)
35 |
36 | // If here, successfully parsed the args and none where "--help" or "-h".
37 | showResults(finalArgs)
38 | }
39 |
40 | /** Functionally identical to `main`, but more verbose. */
41 | def main2(argstrings: Array[String]): Unit = {
42 | val input = Opt.string(
43 | name = "input",
44 | flags = Seq("-i", "--in", "--input"),
45 | help = "Path to input file.",
46 | requiredFlag = true)
47 | val output = Opt.string(
48 | name = "output",
49 | flags = Seq("-o", "--out", "--output"),
50 | default = Some("/dev/null"),
51 | help = "Path to output file.")
52 | val logLevel = Opt.int(
53 | name = "log-level",
54 | flags = Seq("-l", "--log", "--log-level"),
55 | default = Some(3),
56 | help = "Log level to use.")
57 | val path = Opt.path(
58 | name = "path",
59 | flags = Seq("-p", "--path"))
60 | val things = Opt.seqString(delimsRE = "[-|]")(
61 | name = "things",
62 | flags = Seq("--things"),
63 | help = "String elements separated by '-' or '|'.")
64 | val others = Args.makeRemainingOpt(
65 | name = "others",
66 | help = "Other arguments",
67 | requiredFlag = true)
68 |
69 | val initialArgs = Args(
70 | "run-main CLASampleMain [options]",
71 | "Demonstrates the CLA API.",
72 | """Note that --input and "others" are required.""",
73 | Seq(input, output, logLevel, path, things, Args.quietFlag, others)).parse(argstrings)
74 |
75 | val finalArgs: Args = initialArgs.process(argstrings)
76 | showResults(finalArgs)
77 | }
78 |
79 | /**
80 | * Functionally identical to `main` and `main2`, but more verbose than `main`,
81 | * yet a little less verbose than `main2`.
82 | */
83 | def main3(argstrings: Array[String]): Unit = {
84 | import Opt._
85 | import Args._
86 | val initialArgs = Args(
87 | "run-main CLASampleMain [options]",
88 | "Demonstrates the CLA API.",
89 | """Note that --input and "others" are required.""",
90 | Seq(
91 | string("input", Seq("-i", "--in", "--input"), None, "Path to input file.", true),
92 | string("output", Seq("-o", "--out", "--output"), Some("/dev/null"), "Path to output file."),
93 | int( "log-level", Seq("-l", "--log", "--log-level"), Some(3), "Log level to use."),
94 | path( "path", Seq("-p", "--path"), None),
95 | seqString("[:;]")(
96 | "things", Seq("--things"), None, "String elements separated by '-' or '|'."),
97 | Args.quietFlag,
98 | makeRemainingOpt(
99 | "others", "Other arguments", true)))
100 |
101 | val finalArgs: Args = initialArgs.process(argstrings)
102 | showResults(finalArgs)
103 | }
104 |
105 | protected def showResults(parsedArgs: Args): Unit = {
106 |
107 | // Was quiet specified? If not, then write some stuff...
108 | if (parsedArgs.getOrElse("quiet", false)) {
109 | println("(... I'm being very quiet...)")
110 | } else {
111 | // Print all the default values or those specified by the user.
112 | parsedArgs.printValues()
113 |
114 | // Print all the values including repeats.
115 | parsedArgs.printAllValues()
116 |
117 | // Repeat the "other" arguments (not associated with flags).
118 | println("\nYou gave the following \"other\" arguments: " +
119 | parsedArgs.remaining.mkString(", "))
120 |
121 | // Extract values and use them. Note that an advantage of getOrElse is that
122 | // the type parameter for the function can be inferred. E.g., `[Int]` is
123 | // inferred here.
124 | showPathElements(parsedArgs.get[Seq[String]]("path"))
125 | showLogLevel(parsedArgs.getOrElse("log-level", 0))
126 | println
127 | }
128 | }
129 |
130 | protected def showPathElements(path: Option[Seq[String]]) = path match {
131 | case None => println("No path elements to show!")
132 | case Some(seq) => println(s"Setting path elements to $seq")
133 | }
134 |
135 | protected def showLogLevel(level: Int) =
136 | println(s"New log level: $level")
137 | }
138 |
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=1.1.2
2 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | scalacOptions ++= Seq("-deprecation", "-feature")
2 |
3 | addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.8")
4 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.3")
5 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.0")
6 | // addSbtPlugin("com.eed3si9n" % "sbt-export-repo" % "0.1.0")
7 | addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.7.0")
8 | addSbtPlugin("com.eed3si9n" % "sbt-unidoc" % "0.4.1")
9 | addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "1.0.0")
10 | addSbtPlugin("com.typesafe.sbt" % "sbt-ghpages" % "0.6.2")
11 | addSbtPlugin("com.typesafe.sbt" % "sbt-site" % "1.3.2")
12 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.1")
13 | addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "1.0.0")
14 |
--------------------------------------------------------------------------------
/scalastyle-config.xml:
--------------------------------------------------------------------------------
1 |
2 | Scalastyle standard configuration
3 |
4 |
5 |
6 |
7 |
8 |
9 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
--------------------------------------------------------------------------------
/sonatype.sbt:
--------------------------------------------------------------------------------
1 | sonatypeProfileName := "com.concurrentthought"
2 |
--------------------------------------------------------------------------------
/version.sbt:
--------------------------------------------------------------------------------
1 | version in ThisBuild := "0.5.0"
2 |
--------------------------------------------------------------------------------