├── .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 | --------------------------------------------------------------------------------