├── .github
├── workflows
│ ├── test.yml
│ ├── snapshot.yml
│ ├── doc-build.yml
│ └── release.yml
└── PULL_REQUEST_TEMPLATE
├── doc
├── cljdoc.edn
├── new-in-0-4.md
└── parse-opts.md
├── deps-clr.edn
├── .gitignore
├── run-tests.sh
├── CONTRIBUTING.md
├── src
├── main
│ ├── dotnet
│ │ └── packager
│ │ │ └── clojure.tools.cli.csproj
│ └── clojure
│ │ └── clojure
│ │ └── tools
│ │ └── cli.cljc
└── test
│ └── clojure
│ └── clojure
│ └── tools
│ ├── cli_legacy_test.cljc
│ └── cli_test.cljc
├── deps.edn
├── pom.xml
├── CHANGELOG.md
├── README.md
├── LICENSE
└── epl.html
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on: [push]
4 |
5 | jobs:
6 | call-test:
7 | uses: clojure/build.ci/.github/workflows/test.yml@master
8 |
--------------------------------------------------------------------------------
/.github/workflows/snapshot.yml:
--------------------------------------------------------------------------------
1 | name: Snapshot on demand
2 |
3 | on: [workflow_dispatch]
4 |
5 | jobs:
6 | call-snapshot:
7 | uses: clojure/build.ci/.github/workflows/snapshot.yml@master
8 | secrets: inherit
9 |
--------------------------------------------------------------------------------
/.github/workflows/doc-build.yml:
--------------------------------------------------------------------------------
1 | name: Build API Docs
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | jobs:
7 | call-doc-build-workflow:
8 | uses: clojure/build.ci/.github/workflows/doc-build.yml@master
9 | with:
10 | project: clojure/tools.cli
11 |
--------------------------------------------------------------------------------
/doc/cljdoc.edn:
--------------------------------------------------------------------------------
1 | {:cljdoc.doc/tree [["Readme" {:file "README.md"}]
2 | ["Changes" {:file "CHANGELOG.md"}]
3 | ["Command-Line Options" {:file "doc/parse-opts.md"}]
4 | ["Changes since 0.3.x" {:file "doc/new-in-0-4.md"}]]}
5 |
--------------------------------------------------------------------------------
/deps-clr.edn:
--------------------------------------------------------------------------------
1 | {:paths ["src/main/clojure"]
2 |
3 | :aliases
4 | {:test
5 | {:extra-paths ["src/test/clojure"]
6 | :extra-deps {io.github.dmiller/test-runner {:git/sha "c055ea13d19c6a9b9632aa2370fcc2215c8043c3"}}
7 | ;; :main-opts ["-m" "cognitect.test-runner" "-d" "src/test/clojure"]
8 | :exec-fn cognitect.test-runner.api/test
9 | :exec-args {:dirs ["src/test/clojure"]}}}}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .calva/mcp-server/port
2 | .calva/repl.calva-repl
3 | .rebel_readline_history
4 | .vs/
5 | *.nupkg
6 | *.suo
7 | *.user
8 | /.calva/output-window
9 | /.clj-kondo/.cache
10 | /.cpcache
11 | /.lein-failures
12 | /.lein-repl-history
13 | /.lsp/.cache
14 | /.nrepl-port
15 | /.portal/vs-code.edn
16 | /.socket-repl-port
17 | /cljs-test-runner-out
18 | bin/
19 | obj/
20 | target
21 |
--------------------------------------------------------------------------------
/run-tests.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | for v in 9 10 11 12
4 | do
5 | echo ""
6 | echo "Running tests for Clojure 1.$v..."
7 | clojure -M:test:runner:1.$v
8 | if [ $? -ne 0 ]; then
9 | echo "Tests failed for Clojure 1.$v"
10 | exit 1
11 | fi
12 | done
13 |
14 | echo ""
15 | echo "Running tests for ClojureScript..."
16 | clojure -M:test:cljs-runner
17 | if [ $? -ne 0 ]; then
18 | echo "Tests failed for ClojureScript"
19 | exit 1
20 | fi
21 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | This is a [Clojure contrib] project.
2 |
3 | Under the Clojure contrib [guidelines], this project cannot accept
4 | pull requests. All patches must be submitted via [JIRA].
5 |
6 | See [Contributing] on the Clojure website for
7 | more information on how to contribute.
8 |
9 | [Clojure contrib]: https://clojure.org/community/contrib_libs
10 | [Contributing]: https://clojure.org/community/contributing
11 | [JIRA]: http://clojure.atlassian.net/browse/TCLI
12 | [guidelines]: https://clojure.org/community/contrib_howto
13 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release on demand
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | releaseVersion:
7 | description: "Version to release"
8 | required: true
9 | snapshotVersion:
10 | description: "Snapshot version after release"
11 | required: true
12 |
13 | jobs:
14 | call-release:
15 | uses: clojure/build.ci/.github/workflows/release.yml@master
16 | with:
17 | releaseVersion: ${{ github.event.inputs.releaseVersion }}
18 | snapshotVersion: ${{ github.event.inputs.snapshotVersion }}
19 | secrets: inherit
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE:
--------------------------------------------------------------------------------
1 | Hi! Thanks for your interest in contributing to this project.
2 |
3 | Clojure contrib projects do not use GitHub issues or pull requests, and
4 | require a signed Contributor Agreement. If you would like to contribute,
5 | please read more about the CA and sign that first (this can be done online).
6 |
7 | Then go to this project's issue tracker in JIRA to create tickets, update
8 | tickets, or submit patches. For help in creating tickets and patches,
9 | please see:
10 |
11 | - Signing the CA: https://clojure.org/community/contributing
12 | - Creating Tickets: https://clojure.org/community/creating_tickets
13 | - Developing Patches: https://clojure.org/community/developing_patches
14 | - Contributing FAQ: https://clojure.org/community/contributing
15 |
--------------------------------------------------------------------------------
/src/main/dotnet/packager/clojure.tools.cli.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0;netstandard2.1
5 |
6 |
7 |
8 |
9 | clojure.tools.cli
10 | clojure.tools
11 | clojure.tools.cli
12 | clojure.tools.cli
13 | clojure.tools.cli
14 | Gareth Jones, Sung Pae, Sean Corfield
15 | Something appropriate.
16 | Copyright © Rich Hickey and contributors 2023
17 | EPL-1.0
18 | https://github.com/clojure/tools.cku
19 | Clojure contributors
20 | Clojure;ClojureCLR
21 | 1.0.219
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/deps.edn:
--------------------------------------------------------------------------------
1 | {:paths ["src/main/clojure"]
2 | :aliases {:test {:extra-paths ["src/test/clojure"]}
3 | :1.9 {:override-deps {org.clojure/clojure {:mvn/version "1.9.0"}}}
4 | :1.10 {:override-deps {org.clojure/clojure {:mvn/version "1.10.3"}}}
5 | :1.11 {:override-deps {org.clojure/clojure {:mvn/version "1.11.4"}}}
6 | :1.12 {:override-deps {org.clojure/clojure {:mvn/version "1.12.0"}}}
7 | :runner
8 | {:extra-deps {io.github.cognitect-labs/test-runner
9 | {:git/tag "v0.5.1" :git/sha "dfb30dd"}}
10 | ;; required to override test-runner's transitive dependency
11 | ;; on an older version of this project:
12 | :override-deps {org.clojure/tools.cli {:local/root "."}}
13 | :main-opts ["-m" "cognitect.test-runner"
14 | "-d" "src/test/clojure"]}
15 | :cljs-runner
16 | {:extra-deps {olical/cljs-test-runner {:mvn/version "3.8.1"}}
17 | ;; required to override cljs-test-runner's transitive dependency
18 | ;; on an older version of this project:
19 | :override-deps {org.clojure/tools.cli {:local/root "."}}
20 | :main-opts ["-m" "cljs-test-runner.main"
21 | "-d" "src/test/clojure"]}}}
22 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 | 4.0.0
3 | tools.cli
4 | 1.2.999-SNAPSHOT
5 | tools.cli
6 | Command-line processing tools for Clojure.
7 |
8 |
9 | org.clojure
10 | pom.contrib
11 | 1.3.0
12 |
13 |
14 |
15 | Gareth Jones
16 | Sung Pae
17 | Sean Corfield
18 |
19 |
20 |
21 |
23 | 1.9.0
24 |
25 |
26 |
27 | scm:git:git@github.com:clojure/tools.cli.git
28 | scm:git:git@github.com:clojure/tools.cli.git
29 | git@github.com:clojure/tools.cli.git
30 | HEAD
31 |
32 |
33 |
--------------------------------------------------------------------------------
/doc/new-in-0-4.md:
--------------------------------------------------------------------------------
1 | ## Improvements in 0.4.x
2 |
3 | This section highlights the changes/improvents in the 0.4.x series of
4 | releases, compared to the earlier 0.3.x series.
5 |
6 | As a general note, `clojure.tools.cli/cli` is deprecated and you should
7 | use `clojure.tools.cli/parse-opts` instead. The legacy function will remain
8 | for the foreseeable future, but will not get bug fixes or new features.
9 |
10 | ### Better Option Tokenization
11 |
12 | In accordance with the [GNU Program Argument Syntax Conventions][GNU], two
13 | features have been added to the options tokenizer:
14 |
15 | * Short options may be grouped together.
16 |
17 | For instance, `-abc` is equivalent to `-a -b -c`. If the `-b` option
18 | requires an argument, the same `-abc` is interpreted as `-a -b "c"`.
19 |
20 | * Long option arguments may be specified with an equals sign.
21 |
22 | `--long-opt=ARG` is equivalent to `--long-opt "ARG"`.
23 |
24 | If the argument is omitted, it is interpreted as the empty string.
25 | e.g. `--long-opt=` is equivalent to `--long-opt ""`
26 |
27 | [GNU]: https://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html
28 |
29 | ### In-order Processing for Subcommands
30 |
31 | Large programs are often divided into subcommands with their own sets of
32 | options. To aid in designing such programs, `clojure.tools.cli/parse-opts`
33 | accepts an `:in-order` option that directs it to stop processing arguments at
34 | the first unrecognized token.
35 |
36 | For instance, the `git` program has a set of top-level options that are
37 | unrecognized by subcommands and vice-versa:
38 |
39 | git --git-dir=/other/proj/.git log --oneline --graph
40 |
41 | By default, `clojure.tools.cli/parse-opts` interprets this command line as:
42 |
43 | options: [[--git-dir /other/proj/.git]
44 | [--oneline]
45 | [--graph]]
46 | arguments: [log]
47 |
48 | When :in-order is true however, the arguments are interpreted as:
49 |
50 | options: [[--git-dir /other/proj/.git]]
51 | arguments: [log --oneline --graph]
52 |
53 | Note that the options to `log` are not parsed, but remain in the unprocessed
54 | arguments vector. These options could be handled by another call to
55 | `parse-opts` from within the function that handles the `log` subcommand.
56 |
57 | ### Options Summary
58 |
59 | `parse-opts` returns a minimal options summary string:
60 |
61 | -p, --port NUMBER 8080 Required option with default
62 | --host HOST localhost Short and long options may be omitted
63 | -d, --detach Boolean option
64 | -h, --help
65 |
66 | This may be inserted into a larger usage summary, but it is up to the caller.
67 |
68 | If the default formatting of the summary is unsatisfactory, a `:summary-fn`
69 | may be supplied to `parse-opts`. This function will be passed the sequence
70 | of compiled option specification maps and is expected to return an options
71 | summary.
72 |
73 | The default summary function `clojure.tools.cli/summarize` is public and may
74 | be useful within your own `:summary-fn` for generating the default summary.
75 |
76 | ### Option Argument Validation
77 |
78 | By default, option validation is performed immediately after parsing, which
79 | means that "flag" arguments will have a Boolean value, even if a `:default`
80 | is specified with a different type of value.
81 |
82 | You can choose to perform validation after option processing instead, with
83 | the `:post-validation true` flag. During option processing, `:default` values
84 | are applied and `:assoc-fn` and `:update-fn` are invoked. If an option is
85 | specified more than once, `:post-validation true` will cause validation to
86 | be performed after each new option value is processed.
87 |
88 | There is a new option entry `:validate`, which takes a tuple of
89 | `[validation-fn validation-msg]`. The validation-fn receives an option's
90 | argument *after* being parsed by `:parse-fn` if it exists. The validation-msg
91 | can either be a string or a function of one argument that can be called on
92 | the invalid option argument to produce a string:
93 |
94 | ["-p" "--port PORT" "A port number"
95 | :parse-fn #(Integer/parseInt %)
96 | :validate [#(< 0 % 0x10000) #(str % " is not a number between 0 and 65536")]]
97 |
98 | If the validation-fn returns a falsey value, the validation-msg is added to the
99 | errors vector.
100 |
101 | ### Error Handling and Return Values
102 |
103 | Instead of throwing errors, `parse-opts` collects error messages into a vector
104 | and returns them to the caller. Unknown options, missing required arguments,
105 | validation errors, and exceptions thrown during `:parse-fn` are all added to
106 | the errors vector.
107 |
108 | Any option can be flagged as required by providing a `:missing` key in the
109 | option spec with a string that should be used for the error message if the
110 | option is omitted.
111 |
112 | The error message when a required argument is omitted (either a short opt with
113 | `:require` or a long opt describing an argument) is:
114 |
115 | `Missing required argument for ...`
116 |
117 | Correspondingly, `parse-opts` returns the following map of values:
118 |
119 | {:options A map of default options merged with parsed values from the command line
120 | :arguments A vector of unprocessed arguments
121 | :summary An options summary string
122 | :errors A vector of error messages, or nil if no errors}
123 |
124 | During development, parse-opts asserts the uniqueness of option `:id`,
125 | `:short-opt`, and `:long-opt` values and throws an error on failure.
126 |
127 | ### ClojureScript Support
128 |
129 | As of 0.4.x, the namespace is `clojure.tools.cli` for both Clojure and
130 | ClojureScript programs. The entire API, including the legacy (pre-0.3.x)
131 | functions, is now available in both Clojure and ClojureScript.
132 |
133 | For the 0.3.x releases, the ClojureScript namespace was `cljs.tools.cli` and
134 | only `parse-opts` and `summarize` were available.
135 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | * Release 1.2.245 2025-09-28
4 | * Clarify `:id` and `:required` properties for when long option is omitted. Document `:missing` option. This addresses [TCLI-106](https://clojure.atlassian.net/browse/TCLI-106).
5 | * Add `deps-clr.edn` to be compatible with `cljr` (ClojureCLR's `deps` tool).
6 | * Update dependencies to latest versions for testing; add multi-version testing script; drop Clojure 1.8 support.
7 |
8 | * Release 1.1.230 2024-02-19
9 | * Documentation and dev/test/CI infrastructure updates.
10 |
11 | * Release 1.0.219 2023-05-08
12 | * Add ClojureCLR support [TCLI-102](https://clojure.atlassian.net/browse/TCLI-102) [@dmiller](https://github.com/dmiller).
13 |
14 | * Release 1.0.214 2022-10-08
15 | * Document `:missing`, `:multi`, and `:post-validation` in the docstrings and in the README.
16 | * In the help summary, display default values for all options that provide them [TCLI-100](https://clojure.atlassian.net/browse/TCLI-100). Previously, only options that had required arguments would have their defaults shown.
17 |
18 | * Release 1.0.206 2021-02-27
19 | * Allow validation to be performed either after parsing (before option processing) -- current default -- or after option processing, via the `:post-validation true` flag [TCLI-98](https://clojure.atlassian.net/browse/TCLI-98).
20 | * Allow validation message to be a function (of the invalid argument) in addition to being a plain string [TCLI-97](https://clojure.atlassian.net/browse/TCLI-97).
21 | * Add `:multi true` to modify behavior of `:update-fn` [TCLI-96](https://clojure.atlassian.net/browse/TCLI-96).
22 |
23 | * Release 1.0.194 2020-02-20
24 | * Switch to 1.0.x versioning.
25 | * Document the `:missing` option [TCLI-95](https://clojure.atlassian.net/browse/TCLI-95).
26 | * Release 0.4.2 2019-03-26
27 | * Restore ClojureScript compatibility (Martin Klepsch)
28 | [TCLI-94](https://clojure.atlassian.net/browse/TCLI-94).
29 | * Replace `clojure.pprint/cl-format` for better compatibility with GraalVM
30 | [TCLI-93](https://clojure.atlassian.net/browse/TCLI-93).
31 | * Release 0.4.1 2018-09-22
32 | * Add `:update-fn` as the preferred way to handle non-idempotent options. It
33 | is a simpler alternative to using `:assoc-fn` for some such options.
34 | * Add `:default-fn` as a way to compute default option values after parsing.
35 | This is particularly useful with `:update-fn` since you can use it to
36 | override the `:default` value if necessary
37 | [TCLI-90](https://clojure.atlassian.net/browse/TCLI-90).
38 | * Release 0.4.0 on 2018-09-12
39 | * Convert everything to use `.cljc` files and add `clj`/`deps.edn` support
40 | [TCLI-91](https://clojure.atlassian.net/browse/TCLI-91). This **drops
41 | support for Clojure 1.7 and earlier** but brings full feature parity to
42 | ClojureScript. Tests for Clojure can be run with `clj -A:test:runner` and
43 | for ClojureScript with `clj -A:test:cljs-runner`. Multi-version testing is
44 | possible with aliases `:1.8`, `:1.9`, and `:master`.
45 | * Release 0.3.7 on 2018-04-25
46 | * Fix NPE from `nil` long option
47 | [TCLI-89](https://clojure.atlassian.net/browse/TCLI-89) (Peter Schwarz).
48 | * Release 0.3.6 on 2018-04-11
49 | * Restore support for `--no` prefix in long options
50 | [TCLI-88](https://clojure.atlassian.net/browse/TCLI-88) (Arne Brasseur).
51 | * Release 0.3.5 on 2016-05-04
52 | * Fix `summarize` in cljs after renaming during TCLI-36 below
53 | [TCLI-85](https://clojure.atlassian.net/browse/TCLI-85).
54 | * Release 0.3.4 on 2016-05-01
55 | * Clarify use of `summarize` via expanded docstring and make both of the
56 | functions it calls public so it is easier to build your own `:summary-fn`.
57 | [TCLI-36](https://clojure.atlassian.net/browse/TCLI-36).
58 | * Release 0.3.3 on 2015-08-21
59 | * Add `:missing` to option specification to produce the given error message
60 | if the option is not provided (and has no default value).
61 | [TCLI-12](https://clojure.atlassian.net/browse/TCLI-12)
62 | * Add `:strict` to `parse-opts`:
63 | If true, treats required option arguments that match other options as a
64 | parse error (missing required argument).
65 | [TCLI-10](https://clojure.atlassian.net/browse/TCLI-10)
66 | * Release 0.3.2 on 2015-07-28
67 | * Add `:no-defaults` to `parse-opts`:
68 | Returns sequence of options that excludes defaulted ones. This helps
69 | support constructing options from multiple sources (command line, config file).
70 | * Add `get-default-options`:
71 | Returns sequence of options that have defaults specified.
72 | * Support multiple validations [TCLI-9](https://clojure.atlassian.net/browse/TCLI-9)
73 | * Support in-order arguments [TCLI-5](https://clojure.atlassian.net/browse/TCLI-5):
74 | `:in-order` processes arguments up to the first unknown option;
75 | A warning is displayed when unknown options are encountered.
76 | * Release 0.3.1 on 2014-01-02
77 | * Apply patch for [TCLI-8](https://clojure.atlassian.net/browse/TCLI-8):
78 | Correct test that trivially always passes
79 | * Apply patch for [TCLI-7](https://clojure.atlassian.net/browse/TCLI-7):
80 | summarize throws when called with an empty sequence of options
81 | * Release 0.3.0 on 2013-12-15
82 | * Add public functions `parse-opts` and `summarize` to supersede `cli`,
83 | addressing [TCLI-3](https://clojure.atlassian.net/browse/TCLI-3),
84 | [TCLI-4](https://clojure.atlassian.net/browse/TCLI-4), and
85 | [TCLI-6](https://clojure.atlassian.net/browse/TCLI-6)
86 | * Add ClojureScript port of `parse-opts` and `summarize`, available in
87 | `cljs.tools.cli`.
88 | * Move extra documentation of `cli` function to
89 | https://github.com/clojure/tools.cli/wiki/Documentation-for-0.2.4
90 | * Release 0.2.4 on 2013-08-06
91 | * Applying patch for [TCLI-2](https://clojure.atlassian.net/browse/TCLI-2)
92 | (support an assoc-fn option)
93 | * Release 0.2.3 on 2013-08-06
94 | * Add optional description string to prefix the returned banner
95 | * Release 0.2.2 on 2012-08-09
96 | * Applying patch for [TCLI-1](https://clojure.atlassian.net/browse/TCLI-1)
97 | (do not include keys when no value provided by :default)
98 | * Release 0.2.1 on 2011-11-03
99 | * Removing the :required option. Hangover from when -h and --help were
100 | implemented by default, causes problems if you want help and dont
101 | provide a :required argument.
102 | * Release 0.2.0 on 2011-10-31
103 | * Remove calls to System/exit
104 | * Remove built-in help options
105 | * Release 0.1.0
106 | * Initial import of Clargon codebase
107 |
--------------------------------------------------------------------------------
/src/test/clojure/clojure/tools/cli_legacy_test.cljc:
--------------------------------------------------------------------------------
1 | (ns clojure.tools.cli-legacy-test
2 | (:require [clojure.string :refer [split]]
3 | [clojure.test :refer [deftest is testing]]
4 | [clojure.tools.cli :as cli :refer [cli]]))
5 |
6 | (defn parse-int [x]
7 | #?(:clj (Integer/parseInt x)
8 | :cljs (do (assert (re-seq #"^\d" x))
9 | (js/parseInt x))))
10 |
11 | (testing "syntax"
12 | (deftest should-handle-simple-strings
13 | (is (= {:host "localhost"}
14 | (first (cli ["--host" "localhost"]
15 | ["--host"])))))
16 |
17 | (testing "booleans"
18 | (deftest should-handle-trues
19 | (is (= {:verbose true}
20 | (first (cli ["--verbose"]
21 | ["--[no-]verbose"])))))
22 | (deftest should-handle-falses
23 | (is (= {:verbose false}
24 | (first (cli ["--no-verbose"]
25 | ["--[no-]verbose"])))))
26 |
27 | (testing "explicit syntax"
28 | (is (= {:verbose true}
29 | (first (cli ["--verbose"]
30 | ["--verbose" :flag true]))))
31 | (is (= {:verbose false}
32 | (first (cli ["--no-verbose"]
33 | ["--verbose" :flag true]))))))
34 |
35 | (testing "default values"
36 | (deftest should-default-when-no-value
37 | (is (= {:server "10.0.1.10"}
38 | (first (cli []
39 | ["--server" :default "10.0.1.10"])))))
40 | (deftest should-override-when-supplied
41 | (is (= {:server "127.0.0.1"}
42 | (first (cli ["--server" "127.0.0.1"]
43 | ["--server" :default "10.0.1.10"])))))
44 | (deftest should-omit-key-when-no-default
45 | (is (= false
46 | (contains? (cli ["--server" "127.0.0.1"]
47 | ["--server" :default "10.0.1.10"]
48 | ["--names"])
49 | :server)))))
50 |
51 | (deftest should-apply-parse-fn
52 | (is (= {:names ["john" "jeff" "steve"]}
53 | (first (cli ["--names" "john,jeff,steve"]
54 | ["--names" :parse-fn #(vec (split % #","))])))))
55 |
56 | (testing "aliases"
57 | (deftest should-support-multiple-aliases
58 | (is (= {:server "localhost"}
59 | (first (cli ["-s" "localhost"]
60 | ["-s" "--server"])))))
61 |
62 | (deftest should-use-last-alias-provided-as-name-in-map
63 | (is (= {:server "localhost"}
64 | (first (cli ["-s" "localhost"]
65 | ["-s" "--server"]))))))
66 |
67 | (testing "merging args"
68 | (deftest should-merge-identical-arguments
69 | (let [assoc-fn (fn [previous key val]
70 | (assoc previous key
71 | (if-let [oldval (get previous key)]
72 | (merge oldval val)
73 | (hash-set val))))
74 | [options args _] (cli ["-p" "1" "--port" "2"]
75 | ["-p" "--port" "description"
76 | :assoc-fn assoc-fn
77 | :parse-fn #(parse-int %)])]
78 | (is (= {:port #{1 2}} options)))))
79 |
80 | (testing "extra arguments"
81 | (deftest should-provide-access-to-trailing-args
82 | (let [[options args _] (cli ["--foo" "bar" "a" "b" "c"]
83 | ["-f" "--foo"])]
84 | (is (= {:foo "bar"} options))
85 | (is (= ["a" "b" "c"] args))))
86 |
87 | (deftest should-work-with-trailing-boolean-args
88 | (let [[options args _] (cli ["--no-verbose" "some-file"]
89 | ["--[no-]verbose"])]
90 | (is (= {:verbose false} options))
91 | (is (= ["some-file"] args))))
92 |
93 | (deftest should-accept-double-hyphen-as-end-of-args
94 | (let [[options args _] (cli ["--foo" "bar" "--verbose" "--" "file" "-x" "other"]
95 | ["--foo"]
96 | ["--[no-]verbose"])]
97 | (is (= {:foo "bar" :verbose true} options))
98 | (is (= ["file" "-x" "other"] args)))))
99 |
100 | (testing "description"
101 | (deftest should-be-able-to-supply-description
102 | (let [[options args banner]
103 | (cli ["-s" "localhost"]
104 | "This program does something awesome."
105 | ["-s" "--server" :description "Server name"])]
106 | (is (= {:server "localhost"} options))
107 | (is (empty? args))
108 | (is (re-find #"This program does something awesome" banner)))))
109 |
110 | (testing "handles GNU option parsing conventions"
111 | (deftest should-handle-gnu-option-parsing-conventions
112 | (is (= (take 2 (cli ["foo" "-abcp80" "bar" "--host=example.com"]
113 | ["-a" "--alpha" :flag true]
114 | ["-b" "--bravo" :flag true]
115 | ["-c" "--charlie" :flag true]
116 | ["-h" "--host" :flag false]
117 | ["-p" "--port" "Port number"
118 | :flag false :parse-fn #(parse-int %)]))
119 | [{:alpha true :bravo true :charlie true :port 80 :host "example.com"}
120 | ["foo" "bar"]])))))
121 |
122 | (def normalize-args
123 | #'cli/normalize-args)
124 |
125 | (deftest test-normalize-args
126 | (testing "expands clumped short options"
127 | (is (= (normalize-args [] ["-abc" "foo"])
128 | ["-a" "-b" "-c" "foo"]))
129 | (is (= (normalize-args [{:switches ["-p"] :flag false}] ["-abcp80" "foo"])
130 | ["-a" "-b" "-c" "-p" "80" "foo"])))
131 | (testing "expands long options with assignment"
132 | (is (= (normalize-args [{:switches ["--port"] :flag false}] ["--port=80" "--noopt=" "foo"])
133 | ["--port" "80" "--noopt" "" "foo"])))
134 | (testing "preserves double dash"
135 | (is (= (normalize-args [] ["-ab" "--" "foo" "-c"])
136 | ["-a" "-b" "--" "foo" "-c"])))
137 | (testing "hoists all options and optargs to the front"
138 | (is (= (normalize-args
139 | [{:switches ["-x"] :flag false}
140 | {:switches ["-y"] :flag false}
141 | {:switches ["--zulu"] :flag false}]
142 | ["foo" "-axray" "bar" "-by" "yankee" "-c" "baz" "--zulu" "zebra"
143 | "--" "--full" "stop"])
144 | ["-a" "-x" "ray" "-b" "-y" "yankee" "-c" "--zulu" "zebra"
145 | "foo" "bar" "baz" "--" "--full" "stop"]))))
146 |
147 | (deftest all-together-now
148 | (let [[options args _] (cli ["-p" "8080"
149 | "--no-verbose"
150 | "--log-directory" "/tmp"
151 | "--server" "localhost"
152 | "filename"]
153 | ["-p" "--port" :parse-fn #(parse-int %)]
154 | ["--host" :default "localhost"]
155 | ["--[no-]verbose" :default true]
156 | ["--log-directory" :default "/some/path"]
157 | ["--server"])]
158 | (is (= {:port 8080
159 | :host "localhost"
160 | :verbose false
161 | :log-directory "/tmp"
162 | :server "localhost"} options))
163 | (is (= ["filename"] args))))
164 |
--------------------------------------------------------------------------------
/doc/parse-opts.md:
--------------------------------------------------------------------------------
1 | # `clojure.tools.cli/parse-opts`
2 |
3 | [`parse-opts`][parse-opts] is the primary function in this library.
4 |
5 | ## docstring
6 |
7 | This is the current docstring for `parse-opts` (I plan to expand
8 | this into complete documentation for the library, with examples, over time):
9 |
10 | ```
11 | Parse arguments sequence according to given option specifications and the
12 | GNU Program Argument Syntax Conventions:
13 |
14 | https://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html
15 |
16 | Option specifications are a sequence of vectors with the following format:
17 |
18 | [short-opt long-opt-with-required-description description
19 | :property value]
20 |
21 | The first three string parameters in an option spec are positional and
22 | optional, and may be nil in order to specify a later parameter.
23 |
24 | By default, options are toggles that default to nil, but the second string
25 | parameter may be used to specify that an option requires an argument.
26 |
27 | e.g. ["-p" "--port PORT"] specifies that --port requires an argument,
28 | of which PORT is a short description.
29 |
30 | The :property value pairs are optional and take precedence over the
31 | positional string arguments. The valid properties are:
32 |
33 | :id The key for this option in the resulting option map. This
34 | is normally set to the keywordized name of the long option
35 | without the leading dashes.
36 |
37 | Multiple option entries can share the same :id in order to
38 | transform a value in different ways, but only one of these
39 | option entries may contain a :default(-fn) entry.
40 |
41 | This option is mandatory if no long option is provided.
42 |
43 | :short-opt The short format for this option, normally set by the first
44 | positional string parameter: e.g. "-p". Must be unique.
45 |
46 | :long-opt The long format for this option, normally set by the second
47 | positional string parameter; e.g. "--port". Must be unique.
48 |
49 | :required A description of the required argument for this option if
50 | one is required; normally set in the second positional
51 | string parameter after the long option: "--port PORT",
52 | which would be equivalent to :required "PORT".
53 |
54 | The absence of this entry indicates that the option is a
55 | boolean toggle that is set to true when specified on the
56 | command line.
57 |
58 | :missing Indicates that this option is required (not just an argument),
59 | and provides the string to use as an error message if omitted.
60 |
61 | :desc A optional short description of this option.
62 |
63 | :default The default value of this option. If none is specified, the
64 | resulting option map will not contain an entry for this
65 | option unless set on the command line. Also see :default-fn
66 | (below).
67 |
68 | This default is applied before any arguments are parsed so
69 | this is a good way to seed values for :assoc-fn or :update-fn
70 | as well as the simplest way to provide defaults.
71 |
72 | If you need to compute a default based on other command line
73 | arguments, or you need to provide a default separate from the
74 | seed for :assoc-fn or :update-fn, see :default-fn below.
75 |
76 | :default-desc An optional description of the default value. This should be
77 | used when the string representation of the default value is
78 | too ugly to be printed on the command line, or :default-fn
79 | is used to compute the default.
80 |
81 | :default-fn A function to compute the default value of this option, given
82 | the whole, parsed option map as its one argument. If no
83 | function is specified, the resulting option map will not
84 | contain an entry for this option unless set on the command
85 | line. Also see :default (above).
86 |
87 | If both :default and :default-fn are provided, if the
88 | argument is not provided on the command-line, :default-fn will
89 | still be called (and can override :default).
90 |
91 | :parse-fn A function that receives the required option argument and
92 | returns the option value.
93 |
94 | If this is a boolean option, parse-fn will receive the value
95 | true. This may be used to invert the logic of this option:
96 |
97 | ["-q" "--quiet"
98 | :id :verbose
99 | :default true
100 | :parse-fn not]
101 |
102 | :assoc-fn A function that receives the current option map, the current
103 | option :id, and the current parsed option value, and returns
104 | a new option map. The default is 'assoc'.
105 |
106 | For non-idempotent options, where you need to compute a option
107 | value based on the current value and a new value from the
108 | command line. If you only need the the current value, consider
109 | :update-fn (below).
110 |
111 | You cannot specify both :assoc-fn and :update-fn for an
112 | option.
113 |
114 | :update-fn Without :multi true:
115 |
116 | A function that receives just the existing parsed option value,
117 | and returns a new option value, for each option :id present.
118 | The default is 'identity'.
119 |
120 | This may be used to create non-idempotent options where you
121 | only need the current value, like setting a verbosity level by
122 | specifying an option multiple times. ("-vvv" -> 3)
123 |
124 | ["-v" "--verbose"
125 | :default 0
126 | :update-fn inc]
127 |
128 | :default is applied first. If you wish to omit the :default
129 | option value, use fnil in your :update-fn as follows:
130 |
131 | ["-v" "--verbose"
132 | :update-fn (fnil inc 0)]
133 |
134 | With :multi true:
135 |
136 | A function that receives both the existing parsed option value,
137 | and the parsed option value from each instance of the option,
138 | and returns a new option value, for each option :id present.
139 | The :multi option is ignored if you do not specify :update-fn.
140 |
141 | For non-idempotent options, where you need to compute a option
142 | value based on the current value and a new value from the
143 | command line. This can sometimes be easier than use :assoc-fn.
144 |
145 | ["-f" "--file NAME"
146 | :default []
147 | :update-fn conj
148 | :multi true]
149 |
150 | :default is applied first. If you wish to omit the :default
151 | option value, use fnil in your :update-fn as follows:
152 |
153 | ["-f" "--file NAME"
154 | :update-fn (fnil conj [])
155 | :multi true]
156 |
157 | Regardless of :multi, you cannot specify both :assoc-fn
158 | and :update-fn for an option.
159 |
160 | :multi true/false, applies only to options that use :update-fn.
161 |
162 | :validate A vector of [validate-fn validate-msg ...]. Multiple pairs
163 | of validation functions and error messages may be provided.
164 |
165 | :validate-fn A vector of functions that receives the parsed option value
166 | and returns a falsy value or throws an exception when the
167 | value is invalid. The validations are tried in the given
168 | order.
169 |
170 | :validate-msg A vector of error messages corresponding to :validate-fn
171 | that will be added to the :errors vector on validation
172 | failure. Can be plain strings, or functions to be applied
173 | to the (invalid) option argument to produce a string.
174 |
175 | :post-validation true/false. By default, validation is performed after
176 | parsing an option, prior to assoc/default/update processing.
177 | Specifying true here will cause the validation to be
178 | performed after assoc/default/update processing, instead.
179 |
180 | parse-opts returns a map with four entries:
181 |
182 | {:options The options map, keyed by :id, mapped to the parsed value
183 | :arguments A vector of unprocessed arguments
184 | :summary A string containing a minimal options summary
185 | :errors A possible vector of error message strings generated during
186 | parsing; nil when no errors exist}
187 |
188 | A few function options may be specified to influence the behavior of
189 | parse-opts:
190 |
191 | :in-order Stop option processing at the first unknown argument. Useful
192 | for building programs with subcommands that have their own
193 | option specs.
194 |
195 | :no-defaults Only include option values specified in arguments and do not
196 | include any default values in the resulting options map.
197 | Useful for parsing options from multiple sources; i.e. from a
198 | config file and from the command line.
199 |
200 | :strict Parse required arguments strictly: if a required argument value
201 | matches any other option, it is considered to be missing (and
202 | you have a parse error).
203 |
204 | :summary-fn A function that receives the sequence of compiled option specs
205 | (documented at #'clojure.tools.cli/compile-option-specs), and
206 | returns a custom option summary string.
207 | ```
208 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # tools.cli
2 |
3 | Tools for working with command line arguments.
4 |
5 | ## Stable Releases and Dependency Information
6 |
7 | This project follows the version scheme MAJOR.MINOR.COMMITS where MAJOR and MINOR provide some relative indication of the size of the change, but do not follow semantic versioning. In general, all changes endeavor to be non-breaking (by moving to new names rather than by breaking existing names). COMMITS is an ever-increasing counter of commits since the beginning of this repository.
8 |
9 | Latest stable release: 1.2.245
10 |
11 | * [All Released Versions](https://central.sonatype.com/artifact/org.clojure/tools.cli/versions)
12 | * [Development Snapshot Versions](https://oss.sonatype.org/index.html#nexus-search;gav~org.clojure~tools.cli~~~)
13 |
14 | [clj/deps.edn](https://clojure.org/guides/deps_edn) dependency information:
15 | ```clojure
16 | org.clojure/tools.cli {:mvn/version "1.2.245"}
17 | ```
18 |
19 | [Leiningen](https://leiningen.org/) dependency information:
20 | ```clojure
21 | [org.clojure/tools.cli "1.2.245"]
22 | ```
23 | [Maven](https://maven.apache.org/) dependency information:
24 | ```xml
25 |
26 | org.clojure
27 | tools.cli
28 | 1.2.245
29 |
30 | ```
31 |
32 | ### Historical Release Notes
33 |
34 | Starting with 0.4.x, `tools.cli` supports use with `clj`/`deps.edn` and brings
35 | the legacy API to ClojureScript by switching to `.cljc` files. This means it
36 | requires Clojure(Script) 1.9 or later.
37 |
38 | The 0.3.x series of tools.cli introduced a new flexible API, better adherence
39 | to GNU option parsing conventions, and ClojureScript support.
40 |
41 | The old function `clojure.tools.cli/cli` was superseded by
42 | `clojure.tools.cli/parse-opts`, and should not be used in new programs.
43 |
44 | The older function will remain for the foreseeable future. It has also been
45 | adapted to use the new tokenizer, so upgrading is still worthwhile even if you
46 | are not ready to migrate to `parse-opts`.
47 |
48 | ## Quick Start
49 |
50 | ```clojure
51 | (ns my.program
52 | (:require [clojure.tools.cli :refer [parse-opts]])
53 | (:gen-class))
54 |
55 | (def cli-options
56 | ;; An option with an argument
57 | [["-p" "--port PORT" "Port number"
58 | :default 80
59 | :parse-fn #(Integer/parseInt %)
60 | :validate [#(< 0 % 0x10000) "Must be a number between 0 and 65536"]]
61 | ;; A non-idempotent option (:default is applied first)
62 | ["-v" nil "Verbosity level"
63 | :id :verbosity
64 | :default 0
65 | :update-fn inc] ; Prior to 0.4.1, you would have to use:
66 | ;; :assoc-fn (fn [m k _] (update-in m [k] inc))
67 | ;; A boolean option defaulting to nil
68 | ["-h" "--help"]])
69 |
70 | (defn -main [& args]
71 | (parse-opts args cli-options))
72 | ```
73 |
74 | Execute the command line:
75 |
76 | clojure -M -m my.program -vvvp8080 foo --help --invalid-opt
77 |
78 | (or use `lein run` or however you run your program instead of `clojure -M -m my.program`)
79 |
80 | to produce the map:
81 |
82 | ```clojure
83 | {:options {:port 8080
84 | :verbosity 3
85 | :help true}
86 |
87 | :arguments ["foo"]
88 |
89 | :summary " -p, --port PORT 80 Port number
90 | -v Verbosity level
91 | -h, --help"
92 |
93 | :errors ["Unknown option: \"--invalid-opt\""]}
94 | ```
95 |
96 | **Note** that exceptions are _not_ thrown on parse errors, so errors must be
97 | handled explicitly after checking the `:errors` entry for a truthy value.
98 |
99 | Please see the [example program](#example-usage) for a more detailed example
100 | and refer to the docstring of `parse-opts` for comprehensive documentation
101 | (as part of the [API Documentation](https://clojure.github.io/tools.cli/)):
102 |
103 | https://clojure.github.io/tools.cli/index.html#clojure.tools.cli/parse-opts
104 |
105 | ## See Also
106 |
107 | An interesting library built on top of `tool.cli` that provides a more compact,
108 | higher-level API is [cli-matic](https://github.com/l3nz/cli-matic).
109 |
110 | ## Example Usage
111 |
112 | This is an example of a program that uses most of the `tools.cli` features.
113 | For detailed documentation, please see the docstring of `parse-opts`.
114 |
115 | ```clojure
116 | (ns cli-example.core
117 | (:require [cli-example.server :as server]
118 | [clojure.string :as string]
119 | [clojure.tools.cli :refer [parse-opts]])
120 | (:import (java.net InetAddress))
121 | (:gen-class))
122 |
123 | (def cli-options
124 | [;; First three strings describe a short-option, long-option with optional
125 | ;; example argument description, and a description. All three are optional
126 | ;; and positional.
127 | ["-p" "--port PORT" "Port number"
128 | :default 80
129 | :parse-fn #(Integer/parseInt %)
130 | :validate [#(< 0 % 0x10000) "Must be a number between 0 and 65536"]]
131 | ["-H" "--hostname HOST" "Remote host"
132 | :default (InetAddress/getByName "localhost")
133 | ;; Specify a string to output in the default column in the options summary
134 | ;; if the default value's string representation is very ugly
135 | :default-desc "localhost"
136 | :parse-fn #(InetAddress/getByName %)]
137 | ;; If no argument description is given, the option is assumed to
138 | ;; be a boolean option defaulting to nil
139 | [nil "--detach" "Detach from controlling process"]
140 | ["-v" nil "Verbosity level; may be specified multiple times to increase value"
141 | ;; If no long-option is specified, an option :id must be given
142 | :id :verbosity
143 | :default 0
144 | ;; Use :update-fn to create non-idempotent options (:default is applied first)
145 | :update-fn inc]
146 | ["-f" "--file NAME" "File names to read"
147 | :multi true ; use :update-fn to combine multiple instance of -f/--file
148 | ;; if no -f/--file options are given, return this error:
149 | :missing "At least one file name is required"
150 | ;; with :multi true, the :update-fn is passed both the existing parsed
151 | ;; value(s) and the new parsed value from each option; using fnil lets
152 | ;; us avoid specifying a :default value
153 | :update-fn (fnil conj [])]
154 | ["-t" nil "Timeout in seconds"
155 | ;; Since there is no long option, we need to specify the name used for
156 | ;; the argument to the option...
157 | :id :timeout
158 | ;; ...and we need to specify the description of argument that is required
159 | ;; for the option:
160 | :required "TIMEOUT"
161 | ;; parse-long was added in Clojure 1.11:
162 | :parse-fn parse-long]
163 | ;; A boolean option that can explicitly be set to false
164 | ["-d" "--[no-]daemon" "Daemonize the process" :default true]
165 | ["-h" "--help"]])
166 |
167 | ;; The :required specification provides the name shown in the usage summary
168 | ;; for the argument that an option expects. It is only needed when the long
169 | ;; form specification of the option is not given, only the short form. In
170 | ;; addition, :id must be specified to provide the internal keyword name for
171 | ;; the option. If you want to indicate that an option itself is required,
172 | ;; you can use the :missing key to provide a message that will be shown
173 | ;; if the option is not present.
174 |
175 | ;; The :default values are applied first to options. Sometimes you might want
176 | ;; to apply default values after parsing is complete, or specifically to
177 | ;; compute a default value based on other option values in the map. For those
178 | ;; situations, you can use :default-fn to specify a function that is called
179 | ;; for any options that do not have a value after parsing is complete, and
180 | ;; which is passed the complete, parsed option map as it's single argument.
181 | ;; :default-fn (constantly 42) is effectively the same as :default 42 unless
182 | ;; you have a non-idempotent option (with :update-fn or :assoc-fn) -- in which
183 | ;; case any :default value is used as the initial option value rather than nil,
184 | ;; and :default-fn will be called to compute the final option value if none was
185 | ;; given on the command-line (thus, :default-fn can override :default)
186 | ;; Note: validation is *not* performed on the result of :default-fn (this is
187 | ;; an open issue for discussion and is not currently considered a bug).
188 |
189 | (defn usage [options-summary]
190 | (->> ["This is my program. There are many like it, but this one is mine."
191 | ""
192 | "Usage: program-name [options] action"
193 | ""
194 | "Options:"
195 | options-summary
196 | ""
197 | "Actions:"
198 | " start Start a new server"
199 | " stop Stop an existing server"
200 | " status Print a server's status"
201 | ""
202 | "Please refer to the manual page for more information."]
203 | (string/join \newline)))
204 |
205 | (defn error-msg [errors]
206 | (str "The following errors occurred while parsing your command:\n\n"
207 | (string/join \newline errors)))
208 |
209 | (defn validate-args
210 | "Validate command line arguments. Either return a map indicating the program
211 | should exit (with an error message, and optional ok status), or a map
212 | indicating the action the program should take and the options provided."
213 | [args]
214 | (let [{:keys [options arguments errors summary]} (parse-opts args cli-options)]
215 | (cond
216 | (:help options) ; help => exit OK with usage summary
217 | {:exit-message (usage summary) :ok? true}
218 | errors ; errors => exit with description of errors
219 | {:exit-message (error-msg errors)}
220 | ;; custom validation on arguments
221 | (and (= 1 (count arguments))
222 | (#{"start" "stop" "status"} (first arguments)))
223 | {:action (first arguments) :options options}
224 | :else ; failed custom validation => exit with usage summary
225 | {:exit-message (usage summary)})))
226 |
227 | (defn exit [status msg]
228 | (println msg)
229 | (System/exit status))
230 |
231 | (defn -main [& args]
232 | (let [{:keys [action options exit-message ok?]} (validate-args args)]
233 | (if exit-message
234 | (exit (if ok? 0 1) exit-message)
235 | (case action
236 | "start" (server/start! options)
237 | "stop" (server/stop! options)
238 | "status" (server/status! options)))))
239 | ```
240 |
241 | ## Developer Information
242 |
243 | * [GitHub project](https://github.com/clojure/tools.cli)
244 | * [Bug Tracker](https://clojure.atlassian.net/browse/TCLI)
245 | * [Continuous Integration](https://github.com/clojure/tools.cli/actions/workflows/test.yml)
246 |
247 | ## License
248 |
249 | Copyright (c) Rich Hickey and contributors. All rights reserved.
250 |
251 | The use and distribution terms for this software are covered by the
252 | Eclipse Public License 1.0 (https://opensource.org/license/epl-1-0/)
253 | which can be found in the file epl.html at the root of this distribution.
254 | By using this software in any fashion, you are agreeing to be bound by
255 | the terms of this license.
256 |
257 | You must not remove this notice, or any other, from this software.
258 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Eclipse Public License - v 1.0
2 |
3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC
4 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM
5 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
6 |
7 | 1. DEFINITIONS
8 |
9 | "Contribution" means:
10 |
11 | a) in the case of the initial Contributor, the initial code and documentation
12 | distributed under this Agreement, and
13 | b) in the case of each subsequent Contributor:
14 | i) changes to the Program, and
15 | ii) additions to the Program;
16 |
17 | where such changes and/or additions to the Program originate from and are
18 | distributed by that particular Contributor. A Contribution 'originates'
19 | from a Contributor if it was added to the Program by such Contributor
20 | itself or anyone acting on such Contributor's behalf. Contributions do not
21 | include additions to the Program which: (i) are separate modules of
22 | software distributed in conjunction with the Program under their own
23 | license agreement, and (ii) are not derivative works of the Program.
24 |
25 | "Contributor" means any person or entity that distributes the Program.
26 |
27 | "Licensed Patents" mean patent claims licensable by a Contributor which are
28 | necessarily infringed by the use or sale of its Contribution alone or when
29 | combined with the Program.
30 |
31 | "Program" means the Contributions distributed in accordance with this
32 | Agreement.
33 |
34 | "Recipient" means anyone who receives the Program under this Agreement,
35 | including all Contributors.
36 |
37 | 2. GRANT OF RIGHTS
38 | a) Subject to the terms of this Agreement, each Contributor hereby grants
39 | Recipient a non-exclusive, worldwide, royalty-free copyright license to
40 | reproduce, prepare derivative works of, publicly display, publicly
41 | perform, distribute and sublicense the Contribution of such Contributor,
42 | if any, and such derivative works, in source code and object code form.
43 | b) Subject to the terms of this Agreement, each Contributor hereby grants
44 | Recipient a non-exclusive, worldwide, royalty-free patent license under
45 | Licensed Patents to make, use, sell, offer to sell, import and otherwise
46 | transfer the Contribution of such Contributor, if any, in source code and
47 | object code form. This patent license shall apply to the combination of
48 | the Contribution and the Program if, at the time the Contribution is
49 | added by the Contributor, such addition of the Contribution causes such
50 | combination to be covered by the Licensed Patents. The patent license
51 | shall not apply to any other combinations which include the Contribution.
52 | No hardware per se is licensed hereunder.
53 | c) Recipient understands that although each Contributor grants the licenses
54 | to its Contributions set forth herein, no assurances are provided by any
55 | Contributor that the Program does not infringe the patent or other
56 | intellectual property rights of any other entity. Each Contributor
57 | disclaims any liability to Recipient for claims brought by any other
58 | entity based on infringement of intellectual property rights or
59 | otherwise. As a condition to exercising the rights and licenses granted
60 | hereunder, each Recipient hereby assumes sole responsibility to secure
61 | any other intellectual property rights needed, if any. For example, if a
62 | third party patent license is required to allow Recipient to distribute
63 | the Program, it is Recipient's responsibility to acquire that license
64 | before distributing the Program.
65 | d) Each Contributor represents that to its knowledge it has sufficient
66 | copyright rights in its Contribution, if any, to grant the copyright
67 | license set forth in this Agreement.
68 |
69 | 3. REQUIREMENTS
70 |
71 | A Contributor may choose to distribute the Program in object code form under
72 | its own license agreement, provided that:
73 |
74 | a) it complies with the terms and conditions of this Agreement; and
75 | b) its license agreement:
76 | i) effectively disclaims on behalf of all Contributors all warranties
77 | and conditions, express and implied, including warranties or
78 | conditions of title and non-infringement, and implied warranties or
79 | conditions of merchantability and fitness for a particular purpose;
80 | ii) effectively excludes on behalf of all Contributors all liability for
81 | damages, including direct, indirect, special, incidental and
82 | consequential damages, such as lost profits;
83 | iii) states that any provisions which differ from this Agreement are
84 | offered by that Contributor alone and not by any other party; and
85 | iv) states that source code for the Program is available from such
86 | Contributor, and informs licensees how to obtain it in a reasonable
87 | manner on or through a medium customarily used for software exchange.
88 |
89 | When the Program is made available in source code form:
90 |
91 | a) it must be made available under this Agreement; and
92 | b) a copy of this Agreement must be included with each copy of the Program.
93 | Contributors may not remove or alter any copyright notices contained
94 | within the Program.
95 |
96 | Each Contributor must identify itself as the originator of its Contribution,
97 | if
98 | any, in a manner that reasonably allows subsequent Recipients to identify the
99 | originator of the Contribution.
100 |
101 | 4. COMMERCIAL DISTRIBUTION
102 |
103 | Commercial distributors of software may accept certain responsibilities with
104 | respect to end users, business partners and the like. While this license is
105 | intended to facilitate the commercial use of the Program, the Contributor who
106 | includes the Program in a commercial product offering should do so in a manner
107 | which does not create potential liability for other Contributors. Therefore,
108 | if a Contributor includes the Program in a commercial product offering, such
109 | Contributor ("Commercial Contributor") hereby agrees to defend and indemnify
110 | every other Contributor ("Indemnified Contributor") against any losses,
111 | damages and costs (collectively "Losses") arising from claims, lawsuits and
112 | other legal actions brought by a third party against the Indemnified
113 | Contributor to the extent caused by the acts or omissions of such Commercial
114 | Contributor in connection with its distribution of the Program in a commercial
115 | product offering. The obligations in this section do not apply to any claims
116 | or Losses relating to any actual or alleged intellectual property
117 | infringement. In order to qualify, an Indemnified Contributor must:
118 | a) promptly notify the Commercial Contributor in writing of such claim, and
119 | b) allow the Commercial Contributor to control, and cooperate with the
120 | Commercial Contributor in, the defense and any related settlement
121 | negotiations. The Indemnified Contributor may participate in any such claim at
122 | its own expense.
123 |
124 | For example, a Contributor might include the Program in a commercial product
125 | offering, Product X. That Contributor is then a Commercial Contributor. If
126 | that Commercial Contributor then makes performance claims, or offers
127 | warranties related to Product X, those performance claims and warranties are
128 | such Commercial Contributor's responsibility alone. Under this section, the
129 | Commercial Contributor would have to defend claims against the other
130 | Contributors related to those performance claims and warranties, and if a
131 | court requires any other Contributor to pay any damages as a result, the
132 | Commercial Contributor must pay those damages.
133 |
134 | 5. NO WARRANTY
135 |
136 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN
137 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR
138 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE,
139 | NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each
140 | Recipient is solely responsible for determining the appropriateness of using
141 | and distributing the Program and assumes all risks associated with its
142 | exercise of rights under this Agreement , including but not limited to the
143 | risks and costs of program errors, compliance with applicable laws, damage to
144 | or loss of data, programs or equipment, and unavailability or interruption of
145 | operations.
146 |
147 | 6. DISCLAIMER OF LIABILITY
148 |
149 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY
150 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL,
151 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION
152 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
153 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
154 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE
155 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY
156 | OF SUCH DAMAGES.
157 |
158 | 7. GENERAL
159 |
160 | If any provision of this Agreement is invalid or unenforceable under
161 | applicable law, it shall not affect the validity or enforceability of the
162 | remainder of the terms of this Agreement, and without further action by the
163 | parties hereto, such provision shall be reformed to the minimum extent
164 | necessary to make such provision valid and enforceable.
165 |
166 | If Recipient institutes patent litigation against any entity (including a
167 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself
168 | (excluding combinations of the Program with other software or hardware)
169 | infringes such Recipient's patent(s), then such Recipient's rights granted
170 | under Section 2(b) shall terminate as of the date such litigation is filed.
171 |
172 | All Recipient's rights under this Agreement shall terminate if it fails to
173 | comply with any of the material terms or conditions of this Agreement and does
174 | not cure such failure in a reasonable period of time after becoming aware of
175 | such noncompliance. If all Recipient's rights under this Agreement terminate,
176 | Recipient agrees to cease use and distribution of the Program as soon as
177 | reasonably practicable. However, Recipient's obligations under this Agreement
178 | and any licenses granted by Recipient relating to the Program shall continue
179 | and survive.
180 |
181 | Everyone is permitted to copy and distribute copies of this Agreement, but in
182 | order to avoid inconsistency the Agreement is copyrighted and may only be
183 | modified in the following manner. The Agreement Steward reserves the right to
184 | publish new versions (including revisions) of this Agreement from time to
185 | time. No one other than the Agreement Steward has the right to modify this
186 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The
187 | Eclipse Foundation may assign the responsibility to serve as the Agreement
188 | Steward to a suitable separate entity. Each new version of the Agreement will
189 | be given a distinguishing version number. The Program (including
190 | Contributions) may always be distributed subject to the version of the
191 | Agreement under which it was received. In addition, after a new version of the
192 | Agreement is published, Contributor may elect to distribute the Program
193 | (including its Contributions) under the new version. Except as expressly
194 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or
195 | licenses to the intellectual property of any Contributor under this Agreement,
196 | whether expressly, by implication, estoppel or otherwise. All rights in the
197 | Program not expressly granted under this Agreement are reserved.
198 |
199 | This Agreement is governed by the laws of the State of New York and the
200 | intellectual property laws of the United States of America. No party to this
201 | Agreement will bring a legal action under this Agreement more than one year
202 | after the cause of action arose. Each party waives its rights to a jury trial in
203 | any resulting litigation.
204 |
205 |
206 |
--------------------------------------------------------------------------------
/epl.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Eclipse Public License - Version 1.0
8 |
25 |
26 |
27 |
28 |
29 |
30 | Eclipse Public License - v 1.0
31 |
32 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
33 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR
34 | DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS
35 | AGREEMENT.
36 |
37 | 1. DEFINITIONS
38 |
39 | "Contribution" means:
40 |
41 | a) in the case of the initial Contributor, the initial
42 | code and documentation distributed under this Agreement, and
43 | b) in the case of each subsequent Contributor:
44 | i) changes to the Program, and
45 | ii) additions to the Program;
46 | where such changes and/or additions to the Program
47 | originate from and are distributed by that particular Contributor. A
48 | Contribution 'originates' from a Contributor if it was added to the
49 | Program by such Contributor itself or anyone acting on such
50 | Contributor's behalf. Contributions do not include additions to the
51 | Program which: (i) are separate modules of software distributed in
52 | conjunction with the Program under their own license agreement, and (ii)
53 | are not derivative works of the Program.
54 |
55 | "Contributor" means any person or entity that distributes
56 | the Program.
57 |
58 | "Licensed Patents" mean patent claims licensable by a
59 | Contributor which are necessarily infringed by the use or sale of its
60 | Contribution alone or when combined with the Program.
61 |
62 | "Program" means the Contributions distributed in accordance
63 | with this Agreement.
64 |
65 | "Recipient" means anyone who receives the Program under
66 | this Agreement, including all Contributors.
67 |
68 | 2. GRANT OF RIGHTS
69 |
70 | a) Subject to the terms of this Agreement, each
71 | Contributor hereby grants Recipient a non-exclusive, worldwide,
72 | royalty-free copyright license to reproduce, prepare derivative works
73 | of, publicly display, publicly perform, distribute and sublicense the
74 | Contribution of such Contributor, if any, and such derivative works, in
75 | source code and object code form.
76 |
77 | b) Subject to the terms of this Agreement, each
78 | Contributor hereby grants Recipient a non-exclusive, worldwide,
79 | royalty-free patent license under Licensed Patents to make, use, sell,
80 | offer to sell, import and otherwise transfer the Contribution of such
81 | Contributor, if any, in source code and object code form. This patent
82 | license shall apply to the combination of the Contribution and the
83 | Program if, at the time the Contribution is added by the Contributor,
84 | such addition of the Contribution causes such combination to be covered
85 | by the Licensed Patents. The patent license shall not apply to any other
86 | combinations which include the Contribution. No hardware per se is
87 | licensed hereunder.
88 |
89 | c) Recipient understands that although each Contributor
90 | grants the licenses to its Contributions set forth herein, no assurances
91 | are provided by any Contributor that the Program does not infringe the
92 | patent or other intellectual property rights of any other entity. Each
93 | Contributor disclaims any liability to Recipient for claims brought by
94 | any other entity based on infringement of intellectual property rights
95 | or otherwise. As a condition to exercising the rights and licenses
96 | granted hereunder, each Recipient hereby assumes sole responsibility to
97 | secure any other intellectual property rights needed, if any. For
98 | example, if a third party patent license is required to allow Recipient
99 | to distribute the Program, it is Recipient's responsibility to acquire
100 | that license before distributing the Program.
101 |
102 | d) Each Contributor represents that to its knowledge it
103 | has sufficient copyright rights in its Contribution, if any, to grant
104 | the copyright license set forth in this Agreement.
105 |
106 | 3. REQUIREMENTS
107 |
108 | A Contributor may choose to distribute the Program in object code
109 | form under its own license agreement, provided that:
110 |
111 | a) it complies with the terms and conditions of this
112 | Agreement; and
113 |
114 | b) its license agreement:
115 |
116 | i) effectively disclaims on behalf of all Contributors
117 | all warranties and conditions, express and implied, including warranties
118 | or conditions of title and non-infringement, and implied warranties or
119 | conditions of merchantability and fitness for a particular purpose;
120 |
121 | ii) effectively excludes on behalf of all Contributors
122 | all liability for damages, including direct, indirect, special,
123 | incidental and consequential damages, such as lost profits;
124 |
125 | iii) states that any provisions which differ from this
126 | Agreement are offered by that Contributor alone and not by any other
127 | party; and
128 |
129 | iv) states that source code for the Program is available
130 | from such Contributor, and informs licensees how to obtain it in a
131 | reasonable manner on or through a medium customarily used for software
132 | exchange.
133 |
134 | When the Program is made available in source code form:
135 |
136 | a) it must be made available under this Agreement; and
137 |
138 | b) a copy of this Agreement must be included with each
139 | copy of the Program.
140 |
141 | Contributors may not remove or alter any copyright notices contained
142 | within the Program.
143 |
144 | Each Contributor must identify itself as the originator of its
145 | Contribution, if any, in a manner that reasonably allows subsequent
146 | Recipients to identify the originator of the Contribution.
147 |
148 | 4. COMMERCIAL DISTRIBUTION
149 |
150 | Commercial distributors of software may accept certain
151 | responsibilities with respect to end users, business partners and the
152 | like. While this license is intended to facilitate the commercial use of
153 | the Program, the Contributor who includes the Program in a commercial
154 | product offering should do so in a manner which does not create
155 | potential liability for other Contributors. Therefore, if a Contributor
156 | includes the Program in a commercial product offering, such Contributor
157 | ("Commercial Contributor") hereby agrees to defend and
158 | indemnify every other Contributor ("Indemnified Contributor")
159 | against any losses, damages and costs (collectively "Losses")
160 | arising from claims, lawsuits and other legal actions brought by a third
161 | party against the Indemnified Contributor to the extent caused by the
162 | acts or omissions of such Commercial Contributor in connection with its
163 | distribution of the Program in a commercial product offering. The
164 | obligations in this section do not apply to any claims or Losses
165 | relating to any actual or alleged intellectual property infringement. In
166 | order to qualify, an Indemnified Contributor must: a) promptly notify
167 | the Commercial Contributor in writing of such claim, and b) allow the
168 | Commercial Contributor to control, and cooperate with the Commercial
169 | Contributor in, the defense and any related settlement negotiations. The
170 | Indemnified Contributor may participate in any such claim at its own
171 | expense.
172 |
173 | For example, a Contributor might include the Program in a commercial
174 | product offering, Product X. That Contributor is then a Commercial
175 | Contributor. If that Commercial Contributor then makes performance
176 | claims, or offers warranties related to Product X, those performance
177 | claims and warranties are such Commercial Contributor's responsibility
178 | alone. Under this section, the Commercial Contributor would have to
179 | defend claims against the other Contributors related to those
180 | performance claims and warranties, and if a court requires any other
181 | Contributor to pay any damages as a result, the Commercial Contributor
182 | must pay those damages.
183 |
184 | 5. NO WARRANTY
185 |
186 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS
187 | PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
188 | OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION,
189 | ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY
190 | OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely
191 | responsible for determining the appropriateness of using and
192 | distributing the Program and assumes all risks associated with its
193 | exercise of rights under this Agreement , including but not limited to
194 | the risks and costs of program errors, compliance with applicable laws,
195 | damage to or loss of data, programs or equipment, and unavailability or
196 | interruption of operations.
197 |
198 | 6. DISCLAIMER OF LIABILITY
199 |
200 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT
201 | NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT,
202 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING
203 | WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF
204 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
205 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR
206 | DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED
207 | HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
208 |
209 | 7. GENERAL
210 |
211 | If any provision of this Agreement is invalid or unenforceable under
212 | applicable law, it shall not affect the validity or enforceability of
213 | the remainder of the terms of this Agreement, and without further action
214 | by the parties hereto, such provision shall be reformed to the minimum
215 | extent necessary to make such provision valid and enforceable.
216 |
217 | If Recipient institutes patent litigation against any entity
218 | (including a cross-claim or counterclaim in a lawsuit) alleging that the
219 | Program itself (excluding combinations of the Program with other
220 | software or hardware) infringes such Recipient's patent(s), then such
221 | Recipient's rights granted under Section 2(b) shall terminate as of the
222 | date such litigation is filed.
223 |
224 | All Recipient's rights under this Agreement shall terminate if it
225 | fails to comply with any of the material terms or conditions of this
226 | Agreement and does not cure such failure in a reasonable period of time
227 | after becoming aware of such noncompliance. If all Recipient's rights
228 | under this Agreement terminate, Recipient agrees to cease use and
229 | distribution of the Program as soon as reasonably practicable. However,
230 | Recipient's obligations under this Agreement and any licenses granted by
231 | Recipient relating to the Program shall continue and survive.
232 |
233 | Everyone is permitted to copy and distribute copies of this
234 | Agreement, but in order to avoid inconsistency the Agreement is
235 | copyrighted and may only be modified in the following manner. The
236 | Agreement Steward reserves the right to publish new versions (including
237 | revisions) of this Agreement from time to time. No one other than the
238 | Agreement Steward has the right to modify this Agreement. The Eclipse
239 | Foundation is the initial Agreement Steward. The Eclipse Foundation may
240 | assign the responsibility to serve as the Agreement Steward to a
241 | suitable separate entity. Each new version of the Agreement will be
242 | given a distinguishing version number. The Program (including
243 | Contributions) may always be distributed subject to the version of the
244 | Agreement under which it was received. In addition, after a new version
245 | of the Agreement is published, Contributor may elect to distribute the
246 | Program (including its Contributions) under the new version. Except as
247 | expressly stated in Sections 2(a) and 2(b) above, Recipient receives no
248 | rights or licenses to the intellectual property of any Contributor under
249 | this Agreement, whether expressly, by implication, estoppel or
250 | otherwise. All rights in the Program not expressly granted under this
251 | Agreement are reserved.
252 |
253 | This Agreement is governed by the laws of the State of New York and
254 | the intellectual property laws of the United States of America. No party
255 | to this Agreement will bring a legal action under this Agreement more
256 | than one year after the cause of action arose. Each party waives its
257 | rights to a jury trial in any resulting litigation.
258 |
259 |
260 |
261 |
262 |
--------------------------------------------------------------------------------
/src/test/clojure/clojure/tools/cli_test.cljc:
--------------------------------------------------------------------------------
1 | (ns clojure.tools.cli-test
2 | (:require [clojure.tools.cli :as cli :refer [get-default-options parse-opts summarize]]
3 | [clojure.string :refer [join]]
4 | [clojure.test :refer [deftest is testing]]))
5 |
6 | ;; Refer private vars
7 | (def tokenize-args #'cli/tokenize-args)
8 | (def compile-option-specs #'cli/compile-option-specs)
9 | (def parse-option-tokens #'cli/parse-option-tokens)
10 |
11 | (deftest test-tokenize-args
12 | (testing "expands clumped short options"
13 | (is (= (tokenize-args #{"-p"} ["-abcp80"])
14 | [[[:short-opt "-a"] [:short-opt "-b"] [:short-opt "-c"] [:short-opt "-p" "80"]] []])))
15 | (testing "detects arguments to long options"
16 | (is (= (tokenize-args #{"--port" "--host"} ["--port=80" "--host" "example.com"])
17 | [[[:long-opt "--port" "80"] [:long-opt "--host" "example.com"]] []]))
18 | (is (= (tokenize-args #{} ["--foo=bar" "--noarg=" "--bad =opt"])
19 | [[[:long-opt "--foo" "bar"] [:long-opt "--noarg" ""] [:long-opt "--bad =opt"]] []])))
20 | (testing "stops option processing on double dash"
21 | (is (= (tokenize-args #{} ["-a" "--" "-b"])
22 | [[[:short-opt "-a"]] ["-b"]])))
23 | (testing "finds trailing options unless :in-order is true"
24 | (is (= (tokenize-args #{} ["-a" "foo" "-b"])
25 | [[[:short-opt "-a"] [:short-opt "-b"]] ["foo"]]))
26 | (is (= (tokenize-args #{} ["-a" "foo" "-b"] :in-order true)
27 | [[[:short-opt "-a"]] ["foo" "-b"]])))
28 | (testing "does not interpret single dash as an option"
29 | (is (= (tokenize-args #{} ["-"]) [[] ["-"]]))))
30 |
31 | (deftest test-compile-option-specs
32 | (testing "does not set values for :default unless specified"
33 | (is (= (map #(contains? % :default) (compile-option-specs
34 | [["-f" "--foo"]
35 | ["-b" "--bar=ARG" :default 0]]))
36 | [false true])))
37 | (testing "does not set values for :default-fn unless specified"
38 | (is (= (map #(contains? % :default-fn) (compile-option-specs
39 | [["-f" "--foo"]
40 | ["-b" "--bar=ARG"
41 | :default-fn (constantly 0)]]))
42 | [false true])))
43 | (testing "interprets first three string arguments as short-opt, long-opt=required, and desc"
44 | (is (= (map (juxt :short-opt :long-opt :required :desc)
45 | (compile-option-specs [["-a" :id :alpha]
46 | ["-b" "--beta"]
47 | [nil nil "DESC" :id :gamma]
48 | ["-f" "--foo=FOO" "desc"]]))
49 | [["-a" nil nil nil]
50 | ["-b" "--beta" nil nil]
51 | [nil nil nil "DESC"]
52 | ["-f" "--foo" "FOO" "desc"]])))
53 | (testing "parses --[no-]opt style flags to a proper id"
54 | (is (= (-> (compile-option-specs [["-f" "--[no-]foo"]])
55 | first
56 | (select-keys [:id :short-opt :long-opt]))
57 | {:id :foo,
58 | :short-opt "-f",
59 | :long-opt "--[no-]foo"})))
60 | (testing "throws AssertionError on unset :id, duplicate :short-opt or :long-opt,
61 | multiple :default(-fn) entries per :id, or both :assoc-fn/:update-fn present"
62 | (is (thrown? #?(:clj AssertionError :cljr Exception :cljs :default)
63 | (compile-option-specs [["-a" :id nil]])))
64 | (is (thrown? #?(:clj AssertionError :cljr Exception :cljs :default)
65 | (compile-option-specs [{:id :a :short-opt "-a"} {:id :b :short-opt "-a"}])))
66 | (is (thrown? #?(:clj AssertionError :cljr Exception :cljs :default)
67 | (compile-option-specs [{:id :alpha :long-opt "--alpha"} {:id :beta :long-opt "--alpha"}])))
68 | (is (thrown? #?(:clj AssertionError :cljr Exception :cljs :default)
69 | (compile-option-specs [{:id :alpha :default 0} {:id :alpha :default 1}])))
70 | (is (thrown? #?(:clj AssertionError :cljr Exception :cljs :default)
71 | (compile-option-specs [{:id :alpha :default-fn (constantly 0)}
72 | {:id :alpha :default-fn (constantly 1)}])))
73 | (is (thrown? #?(:clj AssertionError :cljr Exception :cljs :default)
74 | (compile-option-specs [{:id :alpha :assoc-fn assoc :update-fn identity}]))))
75 | (testing "desugars `--long-opt=value`"
76 | (is (= (map (juxt :id :long-opt :required)
77 | (compile-option-specs [[nil "--foo FOO"] [nil "--bar=BAR"]]))
78 | [[:foo "--foo" "FOO"]
79 | [:bar "--bar" "BAR"]])))
80 | (testing "desugars :validate [fn msg]"
81 | (let [port? #(< 0 % 0x10000)]
82 | (is (= (map (juxt :validate-fn :validate-msg)
83 | (compile-option-specs
84 | [[nil "--name NAME" :validate [seq "Must be present"]]
85 | [nil "--port PORT" :validate [integer? "Must be an integer"
86 | port? "Must be between 0 and 65536"]]
87 | [:id :back-compat
88 | :validate-fn identity
89 | :validate-msg "Should be backwards compatible"]]))
90 | [[[seq] ["Must be present"]]
91 | [[integer? port?] ["Must be an integer" "Must be between 0 and 65536"]]
92 | [[identity] ["Should be backwards compatible"]]]))))
93 | (testing "accepts maps as option specs without munging values"
94 | (is (= (compile-option-specs [{:id ::foo :short-opt "-f" :long-opt "--foo"}])
95 | [{:id ::foo :short-opt "-f" :long-opt "--foo"}])))
96 | (testing "warns about unknown keys"
97 | (when *assert*
98 | (is (re-find #"Warning:.* :flag"
99 | (with-out-str
100 | #?(:clj (binding [*err* *out*]
101 | (compile-option-specs [[nil "--alpha" :validate nil :flag true]]))
102 | :cljr (binding [*err* *out*]
103 | (compile-option-specs [[nil "--alpha" :validate nil :flag true]]))
104 | :cljs (binding [*print-err-fn* *print-fn*]
105 | (compile-option-specs [[nil "--alpha" :validate nil :flag true]]))))))
106 | (is (re-find #"Warning:.* :validate"
107 | (with-out-str
108 | #?(:clj (binding [*err* *out*]
109 | (compile-option-specs [{:id :alpha :validate nil}]))
110 | :cljr (binding [*err* *out*]
111 | (compile-option-specs [{:id :alpha :validate nil}]))
112 | :cljs (binding [*print-err-fn* *print-fn*]
113 | (compile-option-specs [{:id :alpha :validate nil}])))))))))
114 |
115 | (defn has-error? [re coll]
116 | (seq (filter (partial re-seq re) coll)))
117 |
118 | (defn parse-int [x]
119 | #?(:clj (Integer/parseInt x)
120 | :cljr (Int32/Parse x)
121 | :cljs (do (assert (re-seq #"^\d" x))
122 | (js/parseInt x))))
123 |
124 | (deftest test-parse-option-tokens
125 | (testing "parses and validates option arguments"
126 | (let [specs (compile-option-specs
127 | [["-p" "--port NUMBER"
128 | :parse-fn parse-int
129 | :validate [#(< 0 % 0x10000) #(str % " is not between 0 and 65536")]]
130 | ["-f" "--file PATH"
131 | :missing "--file is required"
132 | :validate [#(not= \/ (first %)) "Must be a relative path"
133 | ;; N.B. This is a poor way to prevent path traversal
134 | #(not (re-find #"\.\." %)) "No path traversal allowed"]]
135 | ["-l" "--level"
136 | :default 0 :update-fn inc
137 | :post-validation true
138 | :validate [#(<= % 2) #(str "Level " % " is more than 2")]]
139 | ["-q" "--quiet"
140 | :id :verbose
141 | :default true
142 | :parse-fn not]])]
143 | (is (= (parse-option-tokens specs [[:long-opt "--port" "80"] [:short-opt "-q"] [:short-opt "-f" "FILE"]])
144 | [{:port (int 80) :verbose false :file "FILE" :level 0} []]))
145 | (is (= (parse-option-tokens specs [[:short-opt "-f" "-p"]])
146 | [{:file "-p" :verbose true :level 0} []]))
147 | (is (has-error? #"Unknown option"
148 | (peek (parse-option-tokens specs [[:long-opt "--unrecognized"]]))))
149 | (is (has-error? #"Missing required"
150 | (peek (parse-option-tokens specs [[:long-opt "--port"]]))))
151 | (is (has-error? #"Missing required"
152 | (peek (parse-option-tokens specs [[:short-opt "-f" "-p"]] :strict true))))
153 | (is (has-error? #"--file is required"
154 | (peek (parse-option-tokens specs []))))
155 | (is (has-error? #"0 is not between"
156 | (peek (parse-option-tokens specs [[:long-opt "--port" "0"]]))))
157 | (is (has-error? #"Level 3 is more than 2"
158 | (peek (parse-option-tokens specs [[:short-opt "-f" "FILE"]
159 | [:short-opt "-l"] [:short-opt "-l"] [:long-opt "--level"]]))))
160 | (is (has-error? #"Error while parsing"
161 | (peek (parse-option-tokens specs [[:long-opt "--port" "FOO"]]))))
162 | (is (has-error? #"Must be a relative path"
163 | (peek (parse-option-tokens specs [[:long-opt "--file" "/foo"]]))))
164 | (is (has-error? #"No path traversal allowed"
165 | (peek (parse-option-tokens specs [[:long-opt "--file" "../../../etc/passwd"]]))))))
166 | (testing "merges values over default option map"
167 | (let [specs (compile-option-specs
168 | [["-a" "--alpha"]
169 | ["-b" "--beta" :default false]
170 | ["-g" "--gamma=ARG"]
171 | ["-d" "--delta=ARG" :default "DELTA"]])]
172 | (is (= (parse-option-tokens specs [])
173 | [{:beta false :delta "DELTA"} []]))
174 | (is (= (parse-option-tokens specs [[:short-opt "-a"]
175 | [:short-opt "-b"]
176 | [:short-opt "-g" "GAMMA"]
177 | [:short-opt "-d" "delta"]])
178 | [{:alpha true :beta true :gamma "GAMMA" :delta "delta"} []]))))
179 | (testing "associates :id and value with :assoc-fn"
180 | (let [specs (compile-option-specs
181 | [["-a" nil
182 | :id :alpha
183 | :default true
184 | ;; same as (update-in m [k] not)
185 | :assoc-fn (fn [m k v] (assoc m k (not v)))]
186 | ["-v" "--verbose"
187 | :default 0
188 | ;; same as (update-in m [k] inc)
189 | :assoc-fn (fn [m k _] (assoc m k (inc (m k))))]])]
190 | (is (= (parse-option-tokens specs [])
191 | [{:alpha true :verbose 0} []]))
192 | (is (= (parse-option-tokens specs [[:short-opt "-a"]])
193 | [{:alpha false :verbose 0} []]))
194 | (is (= (parse-option-tokens specs [[:short-opt "-v"]
195 | [:short-opt "-v"]
196 | [:long-opt "--verbose"]])
197 | [{:alpha true :verbose 3} []]))
198 | (is (= (parse-option-tokens specs [[:short-opt "-v"]] :no-defaults true)
199 | [{:verbose 1} []]))))
200 | (testing "updates :id and value with :update-fn"
201 | (let [specs (compile-option-specs
202 | [["-a" nil
203 | :id :alpha
204 | :default true
205 | :update-fn not]
206 | ["-v" "--verbose"
207 | :default 0
208 | :update-fn inc]
209 | ["-f" "--file NAME"
210 | :multi true
211 | :default []
212 | :update-fn conj]])]
213 | (is (= (parse-option-tokens specs [])
214 | [{:alpha true :verbose 0 :file []} []]))
215 | (is (= (parse-option-tokens specs [[:short-opt "-a"]])
216 | [{:alpha false :verbose 0 :file []} []]))
217 | (is (= (parse-option-tokens specs [[:short-opt "-f" "ONE"]
218 | [:short-opt "-f" "TWO"]
219 | [:long-opt "--file" "THREE"]])
220 | [{:alpha true :verbose 0 :file ["ONE" "TWO" "THREE"]} []]))
221 | (is (= (parse-option-tokens specs [[:short-opt "-v"]
222 | [:short-opt "-v"]
223 | [:long-opt "--verbose"]])
224 | [{:alpha true :verbose 3 :file []} []]))
225 | (is (= (parse-option-tokens specs [[:short-opt "-v"]] :no-defaults true)
226 | [{:verbose 1} []]))))
227 | (testing "associates :id and value with :assoc-fn, without :default"
228 | (let [specs (compile-option-specs
229 | [["-a" nil
230 | :id :alpha
231 | ;; use fnil to have an implied :default true
232 | :assoc-fn (fn [m k _] (update-in m [k] (fnil not true)))]
233 | ["-v" "--verbose"
234 | ;; use fnil to have an implied :default 0
235 | :assoc-fn (fn [m k _] (update-in m [k] (fnil inc 0)))]])]
236 | (is (= (parse-option-tokens specs [])
237 | [{} []]))
238 | (is (= (parse-option-tokens specs [[:short-opt "-a"]])
239 | [{:alpha false} []]))
240 | (is (= (parse-option-tokens specs [[:short-opt "-v"]
241 | [:short-opt "-v"]
242 | [:long-opt "--verbose"]])
243 | [{:verbose 3} []]))
244 | (is (= (parse-option-tokens specs [[:short-opt "-v"]] :no-defaults true)
245 | [{:verbose 1} []]))))
246 | (testing "updates :id and value with :update-fn, without :default"
247 | (let [specs (compile-option-specs
248 | [["-a" nil
249 | :id :alpha
250 | ;; use fnil to have an implied :default true
251 | :update-fn (fnil not true)]
252 | ["-v" "--verbose"
253 | ;; use fnil to have an implied :default 0
254 | :update-fn (fnil inc 0)]
255 | ["-f" "--file NAME"
256 | :multi true
257 | ;; use fnil to have an implied :default []
258 | :update-fn (fnil conj [])]])]
259 | (is (= (parse-option-tokens specs [])
260 | [{} []]))
261 | (is (= (parse-option-tokens specs [[:short-opt "-a"]])
262 | [{:alpha false} []]))
263 | (is (= (parse-option-tokens specs [[:short-opt "-f" "A"]
264 | [:short-opt "-f" "B"]
265 | [:long-opt "--file" "C"]])
266 | [{:file ["A" "B" "C"]} []]))
267 | (is (= (parse-option-tokens specs [[:short-opt "-v"]
268 | [:short-opt "-v"]
269 | [:long-opt "--verbose"]])
270 | [{:verbose 3} []]))
271 | (is (= (parse-option-tokens specs [[:short-opt "-v"]] :no-defaults true)
272 | [{:verbose 1} []]))))
273 | (testing "updates :id and value with :update-fn, with :default-fn"
274 | (let [specs (compile-option-specs
275 | [["-a" nil
276 | :id :alpha
277 | ;; use fnil to have an implied :default true
278 | :update-fn (fnil not true)]
279 | ["-v" "--verbose"
280 | :default-fn #(if (contains? % :alpha) 1 0)
281 | ;; use fnil to have an implied :default 0
282 | :update-fn (fnil inc 0)]])]
283 | (is (= (parse-option-tokens specs [])
284 | [{:verbose 0} []]))
285 | (is (= (parse-option-tokens specs [[:short-opt "-a"]])
286 | [{:alpha false :verbose 1} []]))
287 | (is (= (parse-option-tokens specs [[:short-opt "-v"]
288 | [:short-opt "-v"]
289 | [:long-opt "--verbose"]])
290 | [{:verbose 3} []]))
291 | (is (= (parse-option-tokens specs [[:short-opt "-v"]] :no-defaults true)
292 | [{:verbose 1} []]))))
293 | (testing ":default-fn can override :default value"
294 | (let [specs (compile-option-specs
295 | [["-x" "--X"
296 | :default 0
297 | :update-fn inc
298 | ;; account for :Y always having a default here
299 | :default-fn #(if (pos? (:Y %)) 1 2)]
300 | ["-y" "--Y"
301 | :default 0
302 | :update-fn inc]])]
303 | (is (= (parse-option-tokens specs [])
304 | [{:X 2 :Y 0} []]))
305 | (is (= (parse-option-tokens specs [[:short-opt "-x"]])
306 | [{:X 1 :Y 0} []]))
307 | (is (= (parse-option-tokens specs [[:short-opt "-x"]
308 | [:short-opt "-x"]])
309 | [{:X 2 :Y 0} []]))
310 | (is (= (parse-option-tokens specs [[:short-opt "-y"]])
311 | [{:X 1 :Y 1} []]))
312 | (is (= (parse-option-tokens specs [[:short-opt "-x"]
313 | [:short-opt "-y"]])
314 | [{:X 1 :Y 1} []]))
315 | (is (= (parse-option-tokens specs [[:short-opt "-x"]
316 | [:short-opt "-x"]
317 | [:short-opt "-y"]])
318 | [{:X 2 :Y 1} []])))
319 | (let [specs (compile-option-specs
320 | [["-x" "--X"
321 | :default 0
322 | :update-fn inc
323 | ;; account for :Y not having a default here
324 | :default-fn #(if (contains? % :Y) 1 2)]
325 | ["-y" "--Y"
326 | :update-fn (fnil inc 0)]])]
327 | (is (= (parse-option-tokens specs [])
328 | [{:X 2} []]))
329 | (is (= (parse-option-tokens specs [[:short-opt "-x"]])
330 | [{:X 1} []]))
331 | (is (= (parse-option-tokens specs [[:short-opt "-x"]
332 | [:short-opt "-x"]])
333 | [{:X 2} []]))
334 | (is (= (parse-option-tokens specs [[:short-opt "-y"]])
335 | [{:X 1 :Y 1} []]))
336 | (is (= (parse-option-tokens specs [[:short-opt "-x"]
337 | [:short-opt "-y"]])
338 | [{:X 1 :Y 1} []]))
339 | (is (= (parse-option-tokens specs [[:short-opt "-x"]
340 | [:short-opt "-x"]
341 | [:short-opt "-y"]])
342 | [{:X 2 :Y 1} []]))))
343 | (testing "can deal with negative flags"
344 | (let [specs (compile-option-specs [["-p" "--[no-]profile" "Enable/disable profiling"]])]
345 | (is (= (parse-option-tokens specs []) [{} []]))
346 | (is (= (parse-option-tokens specs [[:short-opt "-p"]]) [{:profile true} []]))
347 | (is (= (parse-option-tokens specs [[:long-opt "--profile"]]) [{:profile true} []]))
348 | (is (= (parse-option-tokens specs [[:long-opt "--no-profile"]]) [{:profile false} []])))
349 | (let [specs (compile-option-specs [["-p" "--[no-]profile" "Enable/disable profiling"
350 | :default false]])]
351 | (is (= (parse-option-tokens specs []) [{:profile false} []]))
352 | (is (= (parse-option-tokens specs [[:short-opt "-p"]]) [{:profile true} []]))
353 | (is (= (parse-option-tokens specs [[:long-opt "--profile"]]) [{:profile true} []]))
354 | (is (= (parse-option-tokens specs [[:long-opt "--no-profile"]]) [{:profile false} []])))))
355 |
356 | (deftest test-summarize
357 | (testing "summarizes options"
358 | (is (= (summarize (compile-option-specs
359 | [["-s" "--server HOST" "Upstream server"
360 | :default :some-object-whose-string-representation-is-awful
361 | :default-desc "example.com"]
362 | ["-p" "--port=PORT" "Upstream port number"
363 | :default 80]
364 | ["-o" nil "Output file"
365 | :id :output
366 | :required "PATH"]
367 | ["-v" nil "Verbosity level; may be specified more than once"
368 | :id :verbose
369 | :default 0]
370 | [nil "--ternary t|f|?" "A ternary option defaulting to false"
371 | :default false
372 | :parse-fn #(case %
373 | "t" true
374 | "f" false
375 | "?" :maybe)]
376 | ["-d" "--[no-]daemon" "Daemonize the process"]
377 | [nil "--help"]]))
378 | (join \newline
379 | [" -s, --server HOST example.com Upstream server"
380 | " -p, --port PORT 80 Upstream port number"
381 | " -o PATH Output file"
382 | " -v 0 Verbosity level; may be specified more than once"
383 | " --ternary t|f|? false A ternary option defaulting to false"
384 | " -d, --[no-]daemon Daemonize the process"
385 | " --help"]))))
386 | (testing "prints :default column even when no default for required flag"
387 | (is (= (summarize (compile-option-specs [["-b" "--boolean" "A boolean option with a hidden default"
388 | :default true]
389 | ["-o" "--option ARG" "An option without a default"]]))
390 | (join \newline [" -b, --boolean true A boolean option with a hidden default"
391 | " -o, --option ARG An option without a default"]))))
392 | (testing "works with no options"
393 | (is (= (summarize (compile-option-specs []))
394 | ""))))
395 |
396 | (deftest test-get-default-options
397 | (testing "Extracts map of default options from a sequence of option vectors."
398 | (is (= (get-default-options [[:id :a :default "a"]
399 | [:id :b :default 98]
400 | [:id :c]])
401 | {:a "a" :b 98}))))
402 |
403 | (deftest test-parse-opts
404 | (testing "parses options to :options"
405 | (is (= (:options (parse-opts ["-abp80"] [["-a" "--alpha"]
406 | ["-b" "--beta"]
407 | ["-p" "--port PORT"
408 | :parse-fn parse-int]]))
409 | {:alpha true :beta true :port (int 80)})))
410 | (testing "collects error messages into :errors"
411 | (let [specs [["-f" "--file PATH"
412 | :validate [#(not= \/ (first %)) "Must be a relative path"]]
413 | ["-p" "--port PORT"
414 | :parse-fn parse-int
415 | :validate [#(< 0 % 0x10000) "Must be between 0 and 65536"]]]
416 | errors (:errors (parse-opts ["-f" "/foo/bar" "-p0"] specs))]
417 | (is (has-error? #"Must be a relative path" errors))
418 | (is (has-error? #"Must be between 0 and 65536" errors))))
419 | (testing "collects unprocessed arguments into :arguments"
420 | (is (= (:arguments (parse-opts ["foo" "-a" "bar" "--" "-b" "baz"]
421 | [["-a" "--alpha"] ["-b" "--beta"]]))
422 | ["foo" "bar" "-b" "baz"])))
423 | (testing "provides an option summary at :summary"
424 | (is (re-seq #"-a\W+--alpha" (:summary (parse-opts [] [["-a" "--alpha"]])))))
425 | (testing "processes arguments in order when :in-order is true"
426 | (is (= (:arguments (parse-opts ["-a" "foo" "-b"]
427 | [["-a" "--alpha"] ["-b" "--beta"]]
428 | :in-order true))
429 | ["foo" "-b"])))
430 | (testing "does not merge over default values when :no-defaults is true"
431 | (let [option-specs [["-p" "--port PORT" :default 80]
432 | ["-H" "--host HOST" :default "example.com"]
433 | ["-q" "--quiet" :default true]
434 | ["-n" "--noop"]]]
435 | (is (= (:options (parse-opts ["-n"] option-specs))
436 | {:port 80 :host "example.com" :quiet true :noop true}))
437 | (is (= (:options (parse-opts ["-n"] option-specs :no-defaults true))
438 | {:noop true}))))
439 | (testing "accepts optional summary-fn for generating options summary"
440 | (is (= (:summary (parse-opts [] [["-a" "--alpha"] ["-b" "--beta"]]
441 | :summary-fn (fn [specs]
442 | (str "Usage: myprog ["
443 | (join \| (map :long-opt specs))
444 | "] arg1 arg2"))))
445 | "Usage: myprog [--alpha|--beta] arg1 arg2"))))
446 |
447 | (comment
448 | ;; CLJS test runner; same as `lein cljsbuild test`
449 | (defn run-cljs-tests []
450 | (println
451 | (clojure.java.shell/sh
452 | "phantomjs"
453 | "target/runner.js"
454 | "target/cli_test.js"))))
455 |
--------------------------------------------------------------------------------
/src/main/clojure/clojure/tools/cli.cljc:
--------------------------------------------------------------------------------
1 | ; Copyright (c) Rich Hickey. All rights reserved.
2 | ; The use and distribution terms for this software are covered by the
3 | ; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
4 | ; which can be found in the file epl-v10.html at the root of this distribution.
5 | ; By using this software in any fashion, you are agreeing to be bound by
6 | ; the terms of this license.
7 | ; You must not remove this notice, or any other, from this software.
8 |
9 | (ns ^{:author "Gareth Jones, Sung Pae, Sean Corfield"
10 | :doc "Tools for working with command line arguments."}
11 | clojure.tools.cli
12 | (:require [clojure.string :as s]
13 | #?(:cljs goog.string.format)))
14 |
15 | ;;
16 | ;; Utility Functions:
17 | ;;
18 |
19 | (defn- make-format
20 | "Given a sequence of column widths, return a string suitable for use in
21 | format to print a sequences of strings in those columns."
22 | [lens]
23 | (s/join (map #(str " %" (when-not (zero? %) (str "-" %)) "s") lens)))
24 |
25 | (defn- tokenize-args
26 | "Reduce arguments sequence into [opt-type opt ?optarg?] vectors and a vector
27 | of remaining arguments. Returns as [option-tokens remaining-args].
28 |
29 | Expands clumped short options like \"-abc\" into:
30 | [[:short-opt \"-a\"] [:short-opt \"-b\"] [:short-opt \"-c\"]]
31 |
32 | If \"-b\" were in the set of options that require arguments, \"-abc\" would
33 | then be interpreted as: [[:short-opt \"-a\"] [:short-opt \"-b\" \"c\"]]
34 |
35 | Long options with `=` are always parsed as option + optarg, even if nothing
36 | follows the `=` sign.
37 |
38 | If the :in-order flag is true, the first non-option, non-optarg argument
39 | stops options processing. This is useful for handling subcommand options."
40 | [required-set args & options]
41 | (let [{:keys [in-order]} (apply hash-map options)]
42 | (loop [opts [] argv [] [car & cdr] args]
43 | (if car
44 | (condp re-seq car
45 | ;; Double dash always ends options processing
46 | #"^--$" (recur opts (into argv cdr) [])
47 | ;; Long options with assignment always passes optarg, required or not
48 | #"^--\S+=" (recur (conj opts (into [:long-opt] (s/split car #"=" 2)))
49 | argv cdr)
50 | ;; Long options, consumes cdr head if needed
51 | #"^--" (let [[optarg cdr] (if (contains? required-set car)
52 | [(first cdr) (rest cdr)]
53 | [nil cdr])]
54 | (recur (conj opts (into [:long-opt car] (if optarg [optarg] [])))
55 | argv cdr))
56 | ;; Short options, expands clumped opts until an optarg is required
57 | #"^-." (let [[os cdr] (loop [os [] [c & cs] (rest car)]
58 | (let [o (str \- c)]
59 | (if (contains? required-set o)
60 | (if (seq cs)
61 | ;; Get optarg from rest of car
62 | [(conj os [:short-opt o (s/join cs)]) cdr]
63 | ;; Get optarg from head of cdr
64 | [(conj os [:short-opt o (first cdr)]) (rest cdr)])
65 | (if (seq cs)
66 | (recur (conj os [:short-opt o]) cs)
67 | [(conj os [:short-opt o]) cdr]))))]
68 | (recur (into opts os) argv cdr))
69 | (if in-order
70 | (recur opts (into argv (cons car cdr)) [])
71 | (recur opts (conj argv car) cdr)))
72 | [opts argv]))))
73 |
74 | #?(:cljs
75 | ;; alias to Google Closure string format
76 | (defn- format
77 | [fmt & args]
78 | (apply goog.string.format fmt args)))
79 |
80 | (def ^{:private true} spec-keys
81 | [:id :short-opt :long-opt :required :desc
82 | :default :default-desc :default-fn
83 | :parse-fn :assoc-fn :update-fn :multi :post-validation
84 | :validate-fn :validate-msg :missing])
85 |
86 | (defn- select-spec-keys
87 | "Select only known spec entries from map and warn the user about unknown
88 | entries at development time."
89 | [map]
90 | (when *assert*
91 | (let [unknown-keys (keys (apply dissoc map spec-keys))]
92 | (when (seq unknown-keys)
93 | (let [msg (str "Warning: The following options to parse-opts are unrecognized: "
94 | (s/join ", " unknown-keys))]
95 | #?(:clj (binding [*out* *err*] (println msg))
96 | :cljr (binding [*out* *err*] (println msg))
97 | :cljs (binding [*print-fn* *print-err-fn*] (println msg)))))))
98 |
99 | (select-keys map spec-keys))
100 |
101 | (defn- compile-spec [spec]
102 | (let [sopt-lopt-desc (take-while #(or (string? %) (nil? %)) spec)
103 | spec-map (apply hash-map (drop (count sopt-lopt-desc) spec))
104 | [short-opt long-opt desc] sopt-lopt-desc
105 | long-opt (or long-opt (:long-opt spec-map))
106 | [long-opt req] (when long-opt
107 | (rest (re-find #"^(--[^ =]+)(?:[ =](.*))?" long-opt)))
108 | #?@(:cljr (req (if (= req "") nil req))) ;;; Regular expression variation
109 | id (when long-opt
110 | (keyword (nth (re-find #"^--(\[no-\])?(.*)" long-opt) 2)))
111 | validate (:validate spec-map)
112 | [validate-fn validate-msg] (when (seq validate)
113 | (->> (partition 2 2 (repeat nil) validate)
114 | (apply map vector)))]
115 | (merge {:id id
116 | :short-opt short-opt
117 | :long-opt long-opt
118 | :required req
119 | :desc desc
120 | :validate-fn validate-fn
121 | :validate-msg validate-msg}
122 | (select-spec-keys (dissoc spec-map :validate)))))
123 |
124 | (defn- distinct?* [coll]
125 | (if (seq coll)
126 | (apply distinct? coll)
127 | true))
128 |
129 | (defn- wrap-val [map key]
130 | (if (contains? map key)
131 | (update-in map [key] #(cond (nil? %) nil
132 | (coll? %) %
133 | :else [%]))
134 | map))
135 |
136 | (defn- compile-option-specs
137 | "Map a sequence of option specification vectors to a sequence of:
138 |
139 | {:id Keyword ; :server
140 | :short-opt String ; \"-s\"
141 | :long-opt String ; \"--server\"
142 | :required String ; \"HOSTNAME\"
143 | :desc String ; \"Remote server\"
144 | :default Object ; #
145 | :default-desc String ; \"example.com\"
146 | :default-fn IFn ; (constantly 0)
147 | :parse-fn IFn ; #(InetAddress/getByName %)
148 | :assoc-fn IFn ; assoc
149 | :update-fn IFn ; identity
150 | :validate-fn [IFn] ; [#(instance? Inet4Address %)
151 | ; #(not (.isMulticastAddress %)]
152 | :validate-msg [String] ; [\"Must be an IPv4 host\"
153 | ; \"Must not be a multicast address\"]
154 | ; can also be a function (of the invalid argument)
155 | :post-validation Boolean ; default false
156 | :missing String ; \"server must be specified\"
157 | }
158 |
159 | :id defaults to the keywordized name of long-opt without leading dashes, but
160 | may be overridden in the option spec.
161 |
162 | The option spec entry `:validate [fn msg ...]` desugars into the two vector
163 | entries :validate-fn and :validate-msg. Multiple pairs of validation
164 | functions and error messages may be provided.
165 |
166 | A :default(-fn) entry will not be included in the compiled spec unless
167 | specified. The :default is applied before options are parsed, the :default-fn
168 | is applied after options are parsed (only where an option was not specified,
169 | and is passed the whole options map as its single argument, so defaults can
170 | be computed from other options if needed).
171 |
172 | An option spec may also be passed as a map containing the entries above,
173 | in which case that subset of the map is transferred directly to the result
174 | vector.
175 |
176 | An assertion error is thrown if any :id values are unset, or if there exist
177 | any duplicate :id, :short-opt, or :long-opt values, or if both :assoc-fn and
178 | :update-fn are provided for any single option."
179 | [option-specs]
180 | {:post [(every? :id %)
181 | (distinct?* (map :id (filter :default %)))
182 | (distinct?* (map :id (filter :default-fn %)))
183 | (distinct?* (remove nil? (map :short-opt %)))
184 | (distinct?* (remove nil? (map :long-opt %)))
185 | (every? (comp not (partial every? identity))
186 | (map (juxt :assoc-fn :update-fn) %))]}
187 | (map (fn [spec]
188 | (-> (if (map? spec)
189 | (select-spec-keys spec)
190 | (compile-spec spec))
191 | (wrap-val :validate-fn)
192 | (wrap-val :validate-msg)))
193 | option-specs))
194 |
195 | (defn- default-option-map [specs default-key]
196 | (reduce (fn [m s]
197 | (if (contains? s default-key)
198 | (assoc m (:id s) (default-key s))
199 | m))
200 | {} specs))
201 |
202 | (defn- missing-errors
203 | "Given specs, returns a map of spec id to error message if missing."
204 | [specs]
205 | (reduce (fn [m s]
206 | (if (:missing s)
207 | (assoc m (:id s) (:missing s))
208 | m))
209 | {} specs))
210 |
211 | (defn- find-spec [specs opt-type opt]
212 | (first
213 | (filter
214 | (fn [spec]
215 | (when-let [spec-opt (get spec opt-type)]
216 | (let [flag-tail (second (re-find #"^--\[no-\](.*)" spec-opt))
217 | candidates (if flag-tail
218 | #{(str "--" flag-tail) (str "--no-" flag-tail)}
219 | #{spec-opt})]
220 | (contains? candidates opt))))
221 | specs)))
222 |
223 | (defn- pr-join [& xs]
224 | (pr-str (s/join \space xs)))
225 |
226 | (defn- missing-required-error [opt example-required]
227 | (str "Missing required argument for " (pr-join opt example-required)))
228 |
229 | (defn- parse-error [opt optarg msg]
230 | (str "Error while parsing option " (pr-join opt optarg) ": " msg))
231 |
232 | (defn- validation-error [value opt optarg msg]
233 | (str "Failed to validate " (pr-join opt optarg)
234 | (if msg (str ": " (if (string? msg) msg (msg value))) "")))
235 |
236 | (defn- validate [value spec opt optarg]
237 | (let [{:keys [validate-fn validate-msg]} spec]
238 | (or (loop [[vfn & vfns] validate-fn [msg & msgs] validate-msg]
239 | (when vfn
240 | (if (try (vfn value) (catch #?(:clj Throwable :cljr Exception :cljs :default) _))
241 | (recur vfns msgs)
242 | [::error (validation-error value opt optarg msg)])))
243 | [value nil])))
244 |
245 | (defn- parse-value [value spec opt optarg]
246 | (let [{:keys [parse-fn]} spec
247 | [value error] (if parse-fn
248 | (try
249 | [(parse-fn value) nil]
250 | (catch #?(:clj Throwable :cljr Exception :cljs :default) e
251 | [nil (parse-error opt optarg (str e))]))
252 | [value nil])]
253 | (cond error
254 | [::error error]
255 | (:post-validation spec)
256 | [value nil]
257 | :else
258 | (validate value spec opt optarg))))
259 |
260 | (defn- allow-no? [spec]
261 | (and (:long-opt spec)
262 | (re-find #"^--\[no-\]" (:long-opt spec))))
263 |
264 | (defn- neg-flag? [spec opt]
265 | (and (allow-no? spec)
266 | (re-find #"^--no-" opt)))
267 |
268 | (defn- parse-optarg [spec opt optarg]
269 | (let [{:keys [required]} spec]
270 | (if (and required (nil? optarg))
271 | [::error (missing-required-error opt required)]
272 | (let [value (if required
273 | optarg
274 | (not (neg-flag? spec opt)))]
275 | (parse-value value spec opt optarg)))))
276 |
277 | (defn- parse-option-tokens
278 | "Reduce sequence of [opt-type opt ?optarg?] tokens into a map of
279 | {option-id value} merged over the default values in the option
280 | specifications.
281 |
282 | If the :no-defaults flag is true, only options specified in the tokens are
283 | included in the option-map.
284 |
285 | Unknown options, missing options, missing required arguments, option
286 | argument parsing exceptions, and validation failures are collected into
287 | a vector of error message strings.
288 |
289 | If the :strict flag is true, required arguments that match other options
290 | are treated as missing, instead of a literal value beginning with - or --.
291 |
292 | Returns [option-map error-messages-vector]."
293 | [specs tokens & options]
294 | (let [{:keys [no-defaults strict]} (apply hash-map options)
295 | defaults (default-option-map specs :default)
296 | default-fns (default-option-map specs :default-fn)
297 | requireds (missing-errors specs)]
298 | (-> (reduce
299 | (fn [[m ids errors] [opt-type opt optarg]]
300 | (if-let [spec (find-spec specs opt-type opt)]
301 | (let [[value error] (parse-optarg spec opt optarg)
302 | id (:id spec)]
303 | (if-not (= value ::error)
304 | (if (and strict
305 | (or (find-spec specs :short-opt optarg)
306 | (find-spec specs :long-opt optarg)))
307 | [m ids (conj errors (missing-required-error opt (:required spec)))]
308 | (let [m' (if-let [update-fn (:update-fn spec)]
309 | (if (:multi spec)
310 | (update m id update-fn value)
311 | (update m id update-fn))
312 | ((:assoc-fn spec assoc) m id value))]
313 | (if (:post-validation spec)
314 | (let [[value error] (validate (get m' id) spec opt optarg)]
315 | (if (= value ::error)
316 | [m ids (conj errors error)]
317 | [m' (conj ids id) errors]))
318 | [m' (conj ids id) errors])))
319 | [m ids (conj errors error)]))
320 | [m ids (conj errors (str "Unknown option: " (pr-str opt)))]))
321 | [defaults [] []] tokens)
322 | (#(reduce
323 | (fn [[m ids errors] [id error]]
324 | (if (contains? m id)
325 | [m ids errors]
326 | [m ids (conj errors error)]))
327 | % requireds))
328 | (#(reduce
329 | (fn [[m ids errors] [id f]]
330 | (if (contains? (set ids) id)
331 | [m ids errors]
332 | [(assoc m id (f (first %))) ids errors]))
333 | % default-fns))
334 | (#(let [[m ids errors] %]
335 | (if no-defaults
336 | [(select-keys m ids) errors]
337 | [m errors]))))))
338 |
339 | (defn make-summary-part
340 | "Given a single compiled option spec, turn it into a formatted string,
341 | optionally with its default values if requested."
342 | [show-defaults? spec]
343 | (let [{:keys [short-opt long-opt required desc
344 | default default-desc default-fn]} spec
345 | opt (cond (and short-opt long-opt) (str short-opt ", " long-opt)
346 | long-opt (str " " long-opt)
347 | short-opt short-opt)
348 | [opt dd] [(if required
349 | (str opt \space required)
350 | opt)
351 | (or default-desc
352 | (when (contains? spec :default)
353 | (if (some? default)
354 | #?(:cljr (pr-str default) :default (str default)) ;; in order to get the proper case for booleans
355 | "nil"))
356 | (when default-fn
357 | "")
358 | "")]]
359 | (if show-defaults?
360 | [opt dd (or desc "")]
361 | [opt (or desc "")])))
362 |
363 | (defn format-lines
364 | "Format a sequence of summary parts into columns. lens is a sequence of
365 | lengths to use for parts. There are two sequences of lengths if we are
366 | not displaying defaults. There are three sequences of lengths if we
367 | are showing defaults."
368 | [lens parts]
369 | (let [fmt (make-format lens)]
370 | (map #(s/trimr (apply format fmt %)) parts)))
371 |
372 | (defn- required-arguments [specs]
373 | (reduce
374 | (fn [s {:keys [required short-opt long-opt]}]
375 | (if required
376 | (into s (remove nil? [short-opt long-opt]))
377 | s))
378 | #{} specs))
379 |
380 | (defn summarize
381 | "Reduce options specs into a options summary for printing at a terminal.
382 | Note that the specs argument should be the compiled version. That effectively
383 | means that you shouldn't call summarize directly. When you call parse-opts
384 | you get back a :summary key which is the result of calling summarize (or
385 | your user-supplied :summary-fn option) on the compiled option specs."
386 | [specs]
387 | (if (seq specs)
388 | (let [show-defaults? (some #(or (contains? % :default)
389 | (contains? % :default-fn)) specs)
390 | parts (map (partial make-summary-part show-defaults?) specs)
391 | lens (apply map (fn [& cols] (apply max (map count cols))) parts)
392 | lines (format-lines lens parts)]
393 | (s/join \newline lines))
394 | ""))
395 |
396 | (defn get-default-options
397 | "Extract the map of default options from a sequence of option vectors.
398 |
399 | As of 0.4.1, this also applies any :default-fn present."
400 | [option-specs]
401 | (let [specs (compile-option-specs option-specs)
402 | vals (default-option-map specs :default)]
403 | (reduce (fn [m [id f]]
404 | (if (contains? m id)
405 | m
406 | (update-in m [id] (f vals))))
407 | vals
408 | (default-option-map specs :default-fn))))
409 |
410 | (defn parse-opts
411 | "Parse arguments sequence according to given option specifications and the
412 | GNU Program Argument Syntax Conventions:
413 |
414 | https://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html
415 |
416 | Option specifications are a sequence of vectors with the following format:
417 |
418 | [short-opt long-opt-with-required-description description
419 | :property value]
420 |
421 | The first three string parameters in an option spec are positional and
422 | optional, and may be nil in order to specify a later parameter.
423 |
424 | By default, options are toggles that default to nil, but the second string
425 | parameter may be used to specify that an option requires an argument.
426 |
427 | e.g. [\"-p\" \"--port PORT\"] specifies that --port requires an argument,
428 | of which PORT is a short description.
429 |
430 | The :property value pairs are optional and take precedence over the
431 | positional string arguments. The valid properties are:
432 |
433 | :id The key for this option in the resulting option map. This
434 | is normally set to the keywordized name of the long option
435 | without the leading dashes.
436 |
437 | Multiple option entries can share the same :id in order to
438 | transform a value in different ways, but only one of these
439 | option entries may contain a :default(-fn) entry.
440 |
441 | This option is mandatory if no long option is provided.
442 |
443 | :short-opt The short format for this option, normally set by the first
444 | positional string parameter: e.g. \"-p\". Must be unique.
445 |
446 | :long-opt The long format for this option, normally set by the second
447 | positional string parameter; e.g. \"--port\". Must be unique.
448 |
449 | :required A description of the required argument for this option if
450 | one is required; normally set in the second positional
451 | string parameter after the long option: \"--port PORT\",
452 | which would be equivalent to :required \"PORT\".
453 |
454 | The absence of this entry indicates that the option is a
455 | boolean toggle that is set to true when specified on the
456 | command line.
457 |
458 | :missing Indicates that this option is required (not just an argument),
459 | and provides the string to use as an error message if omitted.
460 |
461 | :desc A optional short description of this option.
462 |
463 | :default The default value of this option. If none is specified, the
464 | resulting option map will not contain an entry for this
465 | option unless set on the command line. Also see :default-fn
466 | (below).
467 |
468 | This default is applied before any arguments are parsed so
469 | this is a good way to seed values for :assoc-fn or :update-fn
470 | as well as the simplest way to provide defaults.
471 |
472 | If you need to compute a default based on other command line
473 | arguments, or you need to provide a default separate from the
474 | seed for :assoc-fn or :update-fn, see :default-fn below.
475 |
476 | :default-desc An optional description of the default value. This should be
477 | used when the string representation of the default value is
478 | too ugly to be printed on the command line, or :default-fn
479 | is used to compute the default.
480 |
481 | :default-fn A function to compute the default value of this option, given
482 | the whole, parsed option map as its one argument. If no
483 | function is specified, the resulting option map will not
484 | contain an entry for this option unless set on the command
485 | line. Also see :default (above).
486 |
487 | If both :default and :default-fn are provided, if the
488 | argument is not provided on the command-line, :default-fn will
489 | still be called (and can override :default).
490 |
491 | :parse-fn A function that receives the required option argument and
492 | returns the option value.
493 |
494 | If this is a boolean option, parse-fn will receive the value
495 | true. This may be used to invert the logic of this option:
496 |
497 | [\"-q\" \"--quiet\"
498 | :id :verbose
499 | :default true
500 | :parse-fn not]
501 |
502 | :assoc-fn A function that receives the current option map, the current
503 | option :id, and the current parsed option value, and returns
504 | a new option map. The default is 'assoc'.
505 |
506 | For non-idempotent options, where you need to compute a option
507 | value based on the current value and a new value from the
508 | command line. If you only need the the current value, consider
509 | :update-fn (below).
510 |
511 | You cannot specify both :assoc-fn and :update-fn for an
512 | option.
513 |
514 | :update-fn Without :multi true:
515 |
516 | A function that receives just the existing parsed option value,
517 | and returns a new option value, for each option :id present.
518 | The default is 'identity'.
519 |
520 | This may be used to create non-idempotent options where you
521 | only need the current value, like setting a verbosity level by
522 | specifying an option multiple times. (\"-vvv\" -> 3)
523 |
524 | [\"-v\" \"--verbose\"
525 | :default 0
526 | :update-fn inc]
527 |
528 | :default is applied first. If you wish to omit the :default
529 | option value, use fnil in your :update-fn as follows:
530 |
531 | [\"-v\" \"--verbose\"
532 | :update-fn (fnil inc 0)]
533 |
534 | With :multi true:
535 |
536 | A function that receives both the existing parsed option value,
537 | and the parsed option value from each instance of the option,
538 | and returns a new option value, for each option :id present.
539 | The :multi option is ignored if you do not specify :update-fn.
540 |
541 | For non-idempotent options, where you need to compute a option
542 | value based on the current value and a new value from the
543 | command line. This can sometimes be easier than use :assoc-fn.
544 |
545 | [\"-f\" \"--file NAME\"
546 | :default []
547 | :update-fn conj
548 | :multi true]
549 |
550 | :default is applied first. If you wish to omit the :default
551 | option value, use fnil in your :update-fn as follows:
552 |
553 | [\"-f\" \"--file NAME\"
554 | :update-fn (fnil conj [])
555 | :multi true]
556 |
557 | Regardless of :multi, you cannot specify both :assoc-fn
558 | and :update-fn for an option.
559 |
560 | :multi true/false, applies only to options that use :update-fn.
561 |
562 | :validate A vector of [validate-fn validate-msg ...]. Multiple pairs
563 | of validation functions and error messages may be provided.
564 |
565 | :validate-fn A vector of functions that receives the parsed option value
566 | and returns a falsy value or throws an exception when the
567 | value is invalid. The validations are tried in the given
568 | order.
569 |
570 | :validate-msg A vector of error messages corresponding to :validate-fn
571 | that will be added to the :errors vector on validation
572 | failure. Can be plain strings, or functions to be applied
573 | to the (invalid) option argument to produce a string.
574 |
575 | :post-validation true/false. By default, validation is performed after
576 | parsing an option, prior to assoc/default/update processing.
577 | Specifying true here will cause the validation to be
578 | performed after assoc/default/update processing, instead.
579 |
580 | parse-opts returns a map with four entries:
581 |
582 | {:options The options map, keyed by :id, mapped to the parsed value
583 | :arguments A vector of unprocessed arguments
584 | :summary A string containing a minimal options summary
585 | :errors A possible vector of error message strings generated during
586 | parsing; nil when no errors exist}
587 |
588 | A few function options may be specified to influence the behavior of
589 | parse-opts:
590 |
591 | :in-order Stop option processing at the first unknown argument. Useful
592 | for building programs with subcommands that have their own
593 | option specs.
594 |
595 | :no-defaults Only include option values specified in arguments and do not
596 | include any default values in the resulting options map.
597 | Useful for parsing options from multiple sources; i.e. from a
598 | config file and from the command line.
599 |
600 | :strict Parse required arguments strictly: if a required argument value
601 | matches any other option, it is considered to be missing (and
602 | you have a parse error).
603 |
604 | :summary-fn A function that receives the sequence of compiled option specs
605 | (documented at #'clojure.tools.cli/compile-option-specs), and
606 | returns a custom option summary string.
607 | "
608 | [args option-specs & options]
609 | (let [{:keys [in-order no-defaults strict summary-fn]} (apply hash-map options)
610 | specs (compile-option-specs option-specs)
611 | req (required-arguments specs)
612 | [tokens rest-args] (tokenize-args req args :in-order in-order)
613 | [opts errors] (parse-option-tokens specs tokens
614 | :no-defaults no-defaults :strict strict)]
615 | {:options opts
616 | :arguments rest-args
617 | :summary ((or summary-fn summarize) specs)
618 | :errors (when (seq errors) errors)}))
619 |
620 | ;;
621 | ;; Legacy API
622 | ;;
623 |
624 | (defn- build-doc [{:keys [switches docs default]}]
625 | [(apply str (interpose ", " switches))
626 | (or (str default) "")
627 | (or docs "")])
628 |
629 | (defn- banner-for [desc specs]
630 | (when desc
631 | (println desc)
632 | (println))
633 | (let [docs (into (map build-doc specs)
634 | [["--------" "-------" "----"]
635 | ["Switches" "Default" "Desc"]])
636 | max-cols (->> (for [d docs] (map count d))
637 | (apply map (fn [& c] (apply vector c)))
638 | (map #(apply max %)))
639 | vs (for [d docs]
640 | (mapcat (fn [& x] (apply vector x)) max-cols d))]
641 | (doseq [v vs]
642 | (let [fmt (make-format (take-nth 2 v))]
643 | (print (apply format fmt (take-nth 2 (rest v)))))
644 | (prn))))
645 |
646 | (defn- name-for [k]
647 | (s/replace k #"^--no-|^--\[no-\]|^--|^-" ""))
648 |
649 | (defn- flag-for [^String v]
650 | (not (s/starts-with? v "--no-")))
651 |
652 | (defn- opt? [^String x]
653 | (s/starts-with? x "-"))
654 |
655 | (defn- flag? [^String x]
656 | (s/starts-with? x "--[no-]"))
657 |
658 | (defn- end-of-args? [x]
659 | (= "--" x))
660 |
661 | (defn- spec-for
662 | [arg specs]
663 | (->> specs
664 | (filter (fn [s]
665 | (let [switches (set (s :switches))]
666 | (contains? switches arg))))
667 | first))
668 |
669 | (defn- default-values-for
670 | [specs]
671 | (reduce (fn [m s]
672 | (if (contains? s :default)
673 | ((:assoc-fn s) m (:name s) (:default s))
674 | m))
675 | {} specs))
676 |
677 | (defn- apply-specs
678 | [specs args]
679 | (loop [options (default-values-for specs)
680 | extra-args []
681 | args args]
682 | (if-not (seq args)
683 | [options extra-args]
684 | (let [opt (first args)
685 | spec (spec-for opt specs)]
686 | (cond
687 | (end-of-args? opt)
688 | (recur options (into extra-args (vec (rest args))) nil)
689 |
690 | (and (opt? opt) (nil? spec))
691 | (throw #?(:clj (Exception. (str "'" opt "' is not a valid argument"))
692 | :cljr (Exception. (str "'" opt "' is not a valid argument"))
693 | :cljs (js/Error. (str "'" opt "' is not a valid argument"))))
694 |
695 | (and (opt? opt) (spec :flag))
696 | (recur ((spec :assoc-fn) options (spec :name) (flag-for opt))
697 | extra-args
698 | (rest args))
699 |
700 | (opt? opt)
701 | (recur ((spec :assoc-fn) options (spec :name) ((spec :parse-fn) (second args)))
702 | extra-args
703 | (drop 2 args))
704 |
705 | :else
706 | (recur options (conj extra-args (first args)) (rest args)))))))
707 |
708 | (defn- switches-for
709 | [switches flag]
710 | (-> (for [^String s switches]
711 | (cond (and flag (flag? s))
712 | [(s/replace s #"\[no-\]" "no-") (s/replace s #"\[no-\]" "")]
713 |
714 | (and flag (s/starts-with? s "--"))
715 | [(s/replace s #"--" "--no-") s]
716 |
717 | :else
718 | [s]))
719 | flatten))
720 |
721 | (defn- generate-spec
722 | [raw-spec]
723 | (let [[switches raw-spec] (split-with #(and (string? %) (opt? %)) raw-spec)
724 | [docs raw-spec] (split-with string? raw-spec)
725 | options (apply hash-map raw-spec)
726 | aliases (map name-for switches)
727 | flag (or (flag? (last switches)) (options :flag))]
728 | (merge {:switches (switches-for switches flag)
729 | :docs (first docs)
730 | :aliases (set aliases)
731 | :name (keyword (last aliases))
732 | :parse-fn identity
733 | :assoc-fn assoc
734 | :flag flag}
735 | (when flag {:default false})
736 | options)))
737 |
738 | (defn- normalize-args
739 | "Rewrite arguments sequence into a normalized form that is parsable by cli."
740 | [specs args]
741 | (let [required-opts (->> specs
742 | (filter (complement :flag))
743 | (mapcat :switches)
744 | (into #{}))
745 | ;; Preserve double-dash since this is a pre-processing step
746 | largs (take-while (partial not= "--") args)
747 | rargs (drop (count largs) args)
748 | [opts largs] (tokenize-args required-opts largs)]
749 | (concat (mapcat rest opts) largs rargs)))
750 |
751 | (defn ^{:deprecated "since 0.4.x"} cli
752 | "THIS IS A LEGACY FUNCTION and is deprecated. Please use
753 | clojure.tools.cli/parse-opts in new applications.
754 |
755 | Parse the provided args using the given specs. Specs are vectors
756 | describing a command line argument. For example:
757 |
758 | [\"-p\" \"--port\" \"Port to listen on\" :default 3000 :parse-fn #(Integer/parseInt %)]
759 |
760 | First provide the switches (from least to most specific), then a doc
761 | string, and pairs of options.
762 |
763 | Valid options are :default, :parse-fn, and :flag. See
764 | https://github.com/clojure/tools.cli/wiki/Documentation-for-0.2.4 for more
765 | detailed examples.
766 |
767 | Returns a vector containing a map of the parsed arguments, a vector
768 | of extra arguments that did not match known switches, and a
769 | documentation banner to provide usage instructions."
770 | [args & specs]
771 | (let [[desc specs] (if (string? (first specs))
772 | [(first specs) (rest specs)]
773 | [nil specs])
774 | specs (map generate-spec specs)
775 | args (normalize-args specs args)
776 | [options extra-args] (apply-specs specs args)
777 | banner (with-out-str (banner-for desc specs))]
778 | [options extra-args banner]))
779 |
--------------------------------------------------------------------------------