Skip to content

Commit 975d5ea

Browse files
committed
Add some tests
1 parent 1f0bd3b commit 975d5ea

9 files changed

Lines changed: 312 additions & 137 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22

33
## Added
44

5-
## Fixed
6-
7-
## Changed
5+
- subcommand handling
6+
- rudimentary flag handling
7+
- help text generation

README.md

Lines changed: 121 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,129 @@
1-
# {project}
1+
# cli
22

33
<!-- badges -->
4-
[![CircleCI](https://circleci.com/gh/lambdaisland/{project}.svg?style=svg)](https://circleci.com/gh/lambdaisland/{project}) [![cljdoc badge](https://cljdoc.org/badge/lambdaisland/{project})](https://cljdoc.org/d/lambdaisland/{project}) [![Clojars Project](https://img.shields.io/clojars/v/lambdaisland/{project}.svg)](https://clojars.org/lambdaisland/{project})
4+
[![cljdoc badge](https://cljdoc.org/badge/com.lambdaisland/cli)](https://cljdoc.org/d/com.lambdaisland/cli) [![Clojars Project](https://img.shields.io/clojars/v/com.lambdaisland/cli.svg)](https://clojars.org/com.lambdaisland/cli)
55
<!-- /badges -->
66

7+
Command line parser with good subcommand and help handling
8+
9+
## Features
10+
11+
<!-- installation -->
12+
## Installation
13+
14+
To use the latest release, add the following to your `deps.edn` ([Clojure CLI](https://clojure.org/guides/deps_and_cli))
15+
16+
```
17+
com.lambdaisland/cli {:mvn/version "0.0.0"}
18+
```
19+
20+
or add the following to your `project.clj` ([Leiningen](https://leiningen.org/))
21+
22+
```
23+
[com.lambdaisland/cli "0.0.0"]
24+
```
25+
<!-- /installation -->
26+
27+
## Rationale
28+
29+
This is an opinionated CLI argument handling library. It is meant for command
30+
line tools with subcommands (for example git, which has `git commit`, `git log`
31+
and so forth). It works exactly how we like it, which mostly means it sticks to
32+
common conventions (in particular the prominent GNU conventions), needs little
33+
ceremony, and provides your tool with built-in help facilities automagically.
34+
35+
It is Babashka compatible, and in fact pairs really nicely with `bb` for making
36+
home-grown or general purpose tools.
37+
38+
## Usage
39+
40+
```clj
41+
(require '[lambdaisland.cli :as cli])
42+
43+
(cli/dispatch
44+
{:commands
45+
["run"
46+
{:description "Run the thing"
47+
:command (fn [args flags]
48+
,,,)}
49+
50+
"widgets"
51+
{:description "Work with widgets"
52+
:subcommands
53+
["ls"
54+
{:description "List widgets"
55+
:command (fn [args flags] ,,,)}
56+
"create NAME"
57+
{:description "Create a new widget"
58+
:command (fn [args flags] ,,,)}
59+
"delete ID"
60+
{:description "Delete the widget with the given ID"
61+
:command (fn [args flags] ,,,)}]}]
62+
63+
:flags
64+
["-v,--verbose" {:description "Increase verbosity"}]})
65+
```
66+
67+
<!-- opencollective -->
68+
## Lambda Island Open Source
69+
70+
Thank you! cli is made possible thanks to our generous backers. [Become a
71+
backer on OpenCollective](https://opencollective.com/lambda-island) so that we
72+
can continue to make cli better.
73+
74+
<a href="https://opencollective.com/lambda-island">
75+
<img src="https://opencollective.com/lambda-island/organizations.svg?avatarHeight=46&width=800&button=false">
76+
<img src="https://opencollective.com/lambda-island/individuals.svg?avatarHeight=46&width=800&button=false">
77+
</a>
78+
<img align="left" src="https://github.com/lambdaisland/open-source/raw/master/artwork/lighthouse_readme.png">
79+
80+
&nbsp;
81+
82+
cli is part of a growing collection of quality Clojure libraries created and maintained
83+
by the fine folks at [Gaiwan](https://gaiwan.co).
84+
85+
Pay it forward by [becoming a backer on our OpenCollective](http://opencollective.com/lambda-island),
86+
so that we continue to enjoy a thriving Clojure ecosystem.
87+
88+
You can find an overview of all our different projects at [lambdaisland/open-source](https://github.com/lambdaisland/open-source).
89+
90+
&nbsp;
91+
92+
&nbsp;
93+
<!-- /opencollective -->
94+
95+
<!-- contributing -->
96+
## Contributing
97+
98+
We warmly welcome patches to cli. Please keep in mind the following:
99+
100+
- adhere to the [LambdaIsland Clojure Style Guide](https://nextjournal.com/lambdaisland/clojure-style-guide)
101+
- write patches that solve a problem
102+
- start by stating the problem, then supply a minimal solution `*`
103+
- by contributing you agree to license your contributions as MPL 2.0
104+
- don't break the contract with downstream consumers `**`
105+
- don't break the tests
106+
107+
We would very much appreciate it if you also
108+
109+
- update the CHANGELOG and README
110+
- add tests for new functionality
111+
112+
We recommend opening an issue first, before opening a pull request. That way we
113+
can make sure we agree what the problem is, and discuss how best to solve it.
114+
This is especially true if you add new dependencies, or significantly increase
115+
the API surface. In cases like these we need to decide if these changes are in
116+
line with the project's goals.
117+
118+
`*` This goes for features too, a feature needs to solve a problem. State the problem it solves first, only then move on to solving it.
119+
120+
`**` Projects that have a version that starts with `0.` may still see breaking changes, although we also consider the level of community adoption. The more widespread a project is, the less likely we're willing to introduce breakage. See [LambdaIsland-flavored Versioning](https://github.com/lambdaisland/open-source#lambdaisland-flavored-versioning) for more info.
121+
<!-- /contributing -->
122+
123+
<!-- license -->
7124
## License
8125

9-
Copyright &copy; 2020 Arne Brasseur and Contributors
126+
Copyright &copy; 2024 Arne Brasseur and Contributors
10127

11128
Licensed under the term of the Mozilla Public License 2.0, see LICENSE.
12-
13-
Available under the terms of the Eclipse Public License 1.0, see LICENSE.txt
129+
<!-- /license -->

bb.edn

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1-
{:deps {com.lambdaisland/launchpad {:mvn/version "0.26.123-alpha"}}}
1+
{:deps {com.lambdaisland/launchpad {:mvn/version "0.26.123-alpha"}
2+
lambdaisland/open-source {:git/url "https://github.com/lambdaisland/open-source"
3+
:git/sha "7ce125cbd14888590742da7ab3b6be9bba46fc7a"
4+
#_#_:local/root "../open-source"}}}

bin/proj

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/usr/bin/env bb
2+
3+
(ns proj
4+
(:require [lioss.main :as lioss]))
5+
6+
(lioss/main
7+
{:license :mpl
8+
:inception-year 2024
9+
:description "Command line parser with good subcommand and help handling"
10+
:group-id "com.lambdaisland"})
11+
12+
;; Local Variables:
13+
;; mode:clojure
14+
;; End:

deps.edn

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@
1010

1111
:test
1212
{:extra-paths ["test"]
13-
:extra-deps {lambdaisland/kaocha {:mvn/version "1.77.1236"}}}}}
13+
:extra-deps {lambdaisland/kaocha {:mvn/version "1.87.1366"}}}}}

deps.local.edn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{:launchpad/aliases [:test]}

src/lambdaisland/cli.clj

Lines changed: 23 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,25 @@
11
(ns lambdaisland.cli
22
(:require [clojure.string :as str]))
33

4-
(defn print-help [prefix commands]
5-
(println "Usage:" prefix "[COMMAND] [COMMAND_ARGS...]")
6-
(println)
7-
(doseq [[cmd {:keys [description]}] (partition 2 commands)]
8-
(println (format " %-15s%s" cmd (or description "")))))
4+
;; I've tried to be somewhat consistent with variable naming
5+
6+
;; - cmdspec: the map passed into dispatch, with :commands and :flags, possibly augmented with :flagspecs
7+
;; - cli-args: the vector of cli arguments as the come in, or the tail of it if part has been processed
8+
;; - flagspecs: map of the possible flags with metadata, expanded to serve direct lookup, e.g. {"-i" {,,,} "--input" {,,,} "--no-input" {,,,}}
9+
;; - flagspec: map of how to deal with a given flag {:flag "--foo", :key :foo, :short? false, :argcnt 1}
10+
;; - argcnt: number of arguments a given flag consumes (usually zero or one, but could be more)
11+
;; - args/pos-args: vector of positional arguments that will go to the command
12+
;; - opts: options that will go the command, based on any parsed flags
13+
;; - commands: specification of (sub)-commands, can be vector (for order) or map
14+
;; - raw-flagspecs: flags as specified in the cmdspec, without normalization
15+
;; - cmd: a single (sub) command like `"add"` or `"widgets"`
16+
17+
(defn print-help [{:keys [commands flags] :as cmdspec} _]
18+
(let [commands (if (vector? commands) (partition 2 commands) commands)]
19+
(println "Usage:" (or (:name cmdspec) "cli") "[command...] [flags-or-args...]")
20+
(println)
21+
(doseq [[cmd {:keys [description]}] commands]
22+
(println (format (str " %-" (+ 3 (apply max (map (comp count first) commands))) "s%s") cmd (or description ""))))))
923

1024
(defn parse-error! [& msg]
1125
(throw (ex-info (str/join " " msg) {:type ::parse-error})))
@@ -52,13 +66,13 @@
5266
{:value (not negative?)})
5367
flagopts))))
5468

55-
(defn parse-flagspecs [flags]
69+
(defn parse-flagspecs [raw-flagspecs]
5670
(into {"--help" {:key :help :value true}}
5771
(map (juxt :flag identity))
5872
(mapcat
5973
(fn [[flagspec flagopts]]
6074
(parse-flagspec flagspec flagopts))
61-
(if (vector? flags) (partition 2 flags) flags))))
75+
(if (vector? raw-flagspecs) (partition 2 raw-flagspecs) raw-flagspecs))))
6276

6377
(defn dispatch
6478
([cmdspec]
@@ -68,6 +82,7 @@
6882
(dispatch cmdspec pos-args flags)))
6983
([{:keys [commands flags name] :as cmdspec} pos-args opts]
7084
(let [[cmd & pos-args] pos-args
85+
opts (update opts ::command (fnil conj []) cmd)
7186
program-name (or (:name cmdspec) "cli")
7287
command-map (if (vector? commands) (apply hash-map commands) commands)
7388
command-vec (if (vector? commands) commands (into [] cat commands))
@@ -79,127 +94,9 @@
7994
(nil? commands))
8095
(= "help" cmd)
8196
(:help opts))
82-
(print-help program-name command-vec)
97+
(print-help cmdspec opts)
8398

8499
:else
85100
(if subcommands
86101
(dispatch {:commands subcommands :flags flags :name (str program-name " " cmd)} pos-args opts)
87102
(command pos-args opts))))))
88-
89-
(with-out-str
90-
(dispatch
91-
{:commands ["run" {:command (fn [args flags]
92-
(print "RUN" args flags))}]}
93-
["run"]))
94-
;; => "RUN nil nil"
95-
96-
(with-out-str
97-
(dispatch
98-
{:commands ["run" {:command (fn [args flags]
99-
(print "RUN" args flags))}]}
100-
["run" "hello"]))
101-
;; => "RUN (hello) nil"
102-
103-
(with-out-str
104-
(dispatch
105-
{:commands ["run" {:command (fn [args flags]
106-
(print "RUN" args flags))
107-
}]}
108-
["help"]))
109-
;; => "Usage: cli [COMMAND] [COMMAND_ARGS...]\n\n run \n"
110-
111-
(with-out-str
112-
(dispatch
113-
{:commands ["run" {:command (fn [args flags]
114-
(print "RUN" args flags))
115-
:description "Do something"}]}
116-
["help"]))
117-
;; => "Usage: cli [COMMAND] [COMMAND_ARGS...]\n\n run Do something\n"
118-
119-
(with-out-str
120-
(dispatch
121-
{:commands {"run" {:command (fn [args flags]
122-
(print "RUN" args flags))
123-
:description "Do something"}}}
124-
["help"]))
125-
126-
(defn show-args [cmd]
127-
(fn [args flags]
128-
(print (str/upper-case cmd) args flags)))
129-
130-
(println
131-
(dispatch
132-
{:commands {"run" {:command (show-args "run")
133-
:description "Do something"}
134-
"widget" {:description "Work with widgets"
135-
:commands
136-
["ls" {:description "List widgets"
137-
:command (show-args "widget ls")}
138-
"add" {:description "Add widget"
139-
:command (show-args "widget add")}]}}}
140-
["widget" "ls" "x" "--recursive" "--help"]))
141-
142-
(println
143-
(dispatch
144-
{:commands {"run" {:command (show-args "run")
145-
:description "Do something"}
146-
"widget" {:description "Work with widgets"
147-
:commands
148-
["ls" {:description "List widgets"
149-
:command (show-args "widget ls")}
150-
"add" {:description "Add widget"
151-
:command (show-args "widget add")}]}}
152-
}
153-
["widget" "ls" "x" "--recursive" "--help"]))
154-
155-
(dispatch
156-
{:commands {"run" {:command (show-args "run")
157-
:description "Do something"}
158-
"widget" {:description "Work with widgets"
159-
:commands
160-
["ls" {:description "List widgets"
161-
:command (show-args "widget ls")}
162-
"add" {:description "Add widget"
163-
:command (show-args "widget add")}]}}
164-
:flags ["-i,--input FILE" {:desc "Specify input file"}
165-
"--output FILE" "Specify output file"]}
166-
["widget" "ls" "--input" "INPUT" "--output" "OUTPUT"])
167-
168-
169-
(parse-flagspec
170-
"-i,--input FILE" {:desc "Specify input file"})
171-
172-
(parse-flagspecs
173-
["-i,--input FILE" {:desc "Specify input file"}
174-
"--output FILE" "Specify output file"
175-
"--[no-]foo" ""])
176-
177-
(let [cmdspec
178-
{:commands {"run" {:command (show-args "run")
179-
:description "Do something"}
180-
"widget" {:description "Work with widgets"
181-
:commands
182-
["ls" {:description "List widgets"
183-
:command (show-args "widget ls")}
184-
"add" {:description "Add widget"
185-
:command (show-args "widget add")}]}}
186-
:flags ["-i,--input FILE" {:desc "Specify input file"
187-
:key "XXX"}
188-
"--output FILE" "Specify output file"]}]
189-
(split-flags
190-
(assoc cmdspec :flagspecs (parse-flagspecs (:flags cmdspec)))
191-
["widget" "ls" "--help" "--input" "INPUT" "--output" "OUTPUT"]))
192-
193-
(dispatch
194-
{:commands {"run" {:command (show-args "run")
195-
:description "Do something"}
196-
"widget" {:description "Work with widgets"
197-
:commands
198-
["ls" {:description "List widgets"
199-
:command (show-args "widget ls")
200-
:help "List widgets in the order they exist."}
201-
"add" {:description "Add widget"
202-
:command (show-args "widget add")}]}}
203-
:flags ["-i,--input FILE" {:desc "Specify input file"}
204-
"--output FILE" "Specify output file"]}
205-
["widget" "ls" "--help"])

0 commit comments

Comments
 (0)