Skip to content

Commit 7473e2c

Browse files
authored
Merge pull request #36 from hlship/hls/20250813-tool-handler
Support custom tool handlers (for tool-level options)
2 parents a8cfe34 + 3dcd67a commit 7473e2c

21 files changed

Lines changed: 407 additions & 266 deletions

CHANGES.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
55
* Groups are now defined in the options passed to `net.lewisship.cli-tools/dispatch`, not in
66
namespace metadata
77
* Command names are matched as prefixes (not substrings)
8-
* The tool's documentation must now be specified in top-level :doc option key.
8+
* The tool's documentation must now be specified in top-level :doc option key (not from namespace meta-data)
99
* `net.lewisship.cli-tools`:
10-
* In a `defcommand`, the :summary key has been renamed to :title
10+
* In a `defcommand`:
11+
* The :summary key has been renamed to :title
12+
* The :as keyword is no longer supported
1113
* `abort` has been stripped down, it no longer writes the tool name, command path, etc.
12-
* `defcommand`: the :as keyword is not longer supported.
13-
* The two-arg variant of `print-errors` has been removed.
14-
*
14+
* The two-arg variant of `print-errors` has been removed
15+
* `dispatch*` function arguments have changed
16+
* `expand-dispatch-options` has been removed
17+
1518
*Changes*
1619

1720
* Groups may now be nested, to arbitrary depth
@@ -20,6 +23,8 @@
2023
* Tool help now displays just top-level commands by default (add --full to list nested commands)
2124
* net.lewisship.cli-tools
2225
* New `command-path` function returns a composed string of the tool name and command path
26+
* `dispatch` function has new options:
27+
* :handler is a function to handle top-level tool options (then delegate to `dispatch*`)
2328

2429
# 0.15.1 -- 27 Jan 2025
2530

src/net/lewisship/cli_tools.clj

Lines changed: 106 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
[clj-commons.ansi :as ansi]
55
[clojure.string :as string]
66
[net.lewisship.cli-tools.impl :as impl :refer [cond-let]]
7+
[clojure.tools.cli :as cli]
78
[net.lewisship.cli-tools.cache :as cache]))
89

910
(defn exit
@@ -162,60 +163,124 @@
162163
~validations)
163164
~@body))))))
164165

165-
;; TODO: Expandable expansion and caching
166166

167-
(defn dispatch*
168-
"Invoked by [[dispatch]] after namespace and command resolution."
169-
[expanded-options]
170-
(impl/dispatch expanded-options))
171-
172-
(defn expand-dispatch-options
173-
"Called by [[dispatch]] to expand the options before calling [[dispatch*]].
174-
Some applications may call this instead of `dispatch`, modify the results, and then
175-
invoke `dispatch*`."
167+
(def ^{:added "0.16.0"}
168+
default-tool-options
169+
"Default tool command line options."
170+
[["-C" "--color" "Enable ANSI color output"]
171+
["-N" "--no-color" "Disable ANSI color output"]
172+
["-h" "--help" "This tool summary"]])
173+
174+
(defn- expand-tool-options
175+
"Expanded dispatch options into tool options, leveraging a cache."
176176
[options]
177-
(let [{:keys [cache-dir arguments]} options
178-
options' (select-keys options [:tool-name
179-
:doc
180-
:namespaces
181-
:groups])
182-
result (if-not cache-dir
183-
(impl/expand-dispatch-options options')
184-
(let [cache-dir' (fs/expand-home cache-dir)
185-
digest (cache/classpath-digest options')
186-
cached (cache/read-from-cache cache-dir' digest)]
187-
(if cached
188-
cached
189-
(let [full (impl/expand-dispatch-options options')]
190-
(cache/write-to-cache cache-dir' digest full)
191-
full))))]
192-
(assoc result :arguments (or arguments *command-line-args*))))
193-
194-
(def ^:private default-options
177+
(let [{:keys [cache-dir]} options
178+
options' (select-keys options [:tool-name
179+
:doc
180+
:namespaces
181+
:groups])
182+
cache-dir' (when cache-dir
183+
(fs/expand-home cache-dir))
184+
digest (when cache-dir'
185+
(cache/classpath-digest options'))
186+
cached (when digest
187+
(cache/read-from-cache cache-dir' digest))
188+
result (if cached
189+
cached
190+
(let [expanded (impl/expand-tool-options options')]
191+
(when cache-dir'
192+
(cache/write-to-cache cache-dir' digest expanded))
193+
expanded))]
194+
(merge result
195+
(select-keys options [:tool-name :doc :arguments :tool-summary]))))
196+
197+
(defn dispatch*
198+
"Called from a tool handler to process remaining command line arguments.
199+
200+
- dispatch-options - modified dispatch options
201+
- color-flag - if non-nil, enables or disables ANSI colors before dispatching
202+
- help - if true, then change the arguments to \"help\", to print tool help
203+
204+
In the dispatch options map, the tool handler should have set the following:
205+
206+
- :arguments -- seq of remaining arguments after processing tool-level options
207+
- :tool-summary -- summary of tool options (used when printing tool help)."
208+
[dispatch-options color-flag help?]
209+
(cond
210+
(some? color-flag)
211+
(binding [ansi/*color-enabled* color-flag]
212+
(dispatch* dispatch-options nil help?))
213+
214+
help?
215+
(recur (assoc dispatch-options :arguments ["help"]) nil false)
216+
217+
:else
218+
(-> dispatch-options expand-tool-options impl/dispatch)))
219+
220+
(defn summarize-specs
221+
"Converts a tools.cli command specification to a description of the options; this is an enhanced version of
222+
clojure.tools.cli/summarize that makes use of indentation and ANSI colors.
223+
224+
Returns a delay (to ensure that ANSI color enabled/disabled options are enforced)."
225+
{:added "0.16.0"}
226+
[specs]
227+
;; summarize-specs is called before we parse the command line options (-C, -N) that may enable/disable
228+
;; ANSI colors, so a delay is used to prevent premature evaluation.
229+
(delay (impl/summarize-specs specs)))
230+
231+
(defn default-tool-handler
232+
"Default tool handler, passed the tool options. The [[default-tool-options]] support enabling or disabling
233+
ANSI fonts, and requesting top-level help.
234+
235+
This is the default for the :handler key of dispatch options.
236+
237+
This function is passed the dispatch options, parses the default tool options, and delegates the rest to [[dispatch*]]."
238+
{:added "0.16.0"}
239+
[dispatch-options]
240+
(let [{:keys [options arguments summary errors]
241+
:as result} (cli/parse-opts (:arguments dispatch-options)
242+
default-tool-options
243+
:in-order true
244+
:summary-fn summarize-specs)
245+
{:keys [color no-color help]} options
246+
color-flag (cond color true
247+
no-color false)]
248+
(when errors
249+
(throw (ex-info "Tool parse sanity check" result)))
250+
251+
(dispatch* (-> dispatch-options
252+
(assoc :arguments arguments
253+
:tool-summary summary))
254+
color-flag help)))
255+
256+
(def ^:private default-dispatch-options
195257
{:cache-dir (or (System/getenv "CLI_TOOLS_CACHE_DIR")
196-
"~/.cli-tools-cache")})
258+
"~/.cli-tools-cache")
259+
:handler default-tool-handler})
197260

198261
(defn dispatch
199262
"Locates commands in namespaces, finds the current command
200263
(as identified by the first command line argument) and processes CLI options and arguments.
201264
202-
options:
265+
dispatch-options:
203266
204267
- :tool-name (optional, string) - used in command summary and errors
205268
- :doc (optional, string) - used in help summary
206269
- :arguments - command line arguments to parse (defaults to `*command-line-args*`)
207270
- :namespaces - seq of symbols identifying namespaces to search for root-level commands
208271
- :groups - map of group command (a string) to a group map
272+
- :handler - function to handle tool-level options, defaults to [[default-tool-handler]]
273+
- :cache-dir (optional, string) - directory to cache data in, or nil to disable cache
209274
210275
The :tool-name option is only semi-optional; in a Babashka script, it will default
211276
from the `babashka.file` system property, if any. An exception is thrown if :tool-name
212277
is not provided and can't be defaulted.
213278
214279
A group map defines a set of commands grouped under a common name. Its structure:
215280
216-
- :doc - string, a short string identifying the purpose of the group
217-
- :namespaces - seq of symbols identifying namespaces of commands in the group
218-
- :groups - recusive map of groups nested within the group
281+
- :doc (optional, string) - a short string identifying the purpose of the group
282+
- :namespaces (seq of symbols, required) - identifies namespaces providing commands in the group
283+
- :groups (optional, map) - recusive map of groups nested within the group
219284
220285
dispatch will always add the `net.lewiship.cli-tools.builtins` namespace to the root
221286
namespace list; this ensures the built-in `help` command is available.
@@ -226,13 +291,16 @@
226291
227292
Caching is enabled by default; this means that a scan of all namespaces is only required on the first
228293
execution; subsequently, only the single namespace implementing the selected command will need to
229-
be loaded.
294+
be loaded. :cache-dir defaults to the value of the CLI_TOOLS_CACHE_DIR environment variable, or
295+
to the default value `~/.cli-tools-cache`. If set to nil, then caching is disabled.
230296
231297
Returns nil."
232-
[options]
233-
(-> (merge default-options options)
234-
expand-dispatch-options
235-
dispatch*))
298+
[dispatch-options]
299+
(let [options' (merge {:arguments *command-line-args*}
300+
default-dispatch-options
301+
dispatch-options)
302+
{:keys [handler]} options']
303+
(handler (dissoc options' :handler))))
236304

237305
(defn select-option
238306
"Builds a standard option spec for selecting from a list of possible values.

src/net/lewisship/cli_tools/builtins.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@
1212
search-term ["SEARCH" "Filter shown commands to those that match this term"
1313
:optional true]]
1414
;; dispatch binds *options* for us
15-
(impl/print-tool-help impl/*options* search-term full?))
15+
(impl/print-tool-help impl/*tool-options* search-term full?))
1616

src/net/lewisship/cli_tools/completions.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@
101101
output-path ["PATH" "File to write completions to."
102102
:optional true]]
103103
(binding [impl/*introspection-mode* true]
104-
(let [{:keys [command-root tool-name groups]} impl/*options*]
104+
(let [{:keys [command-root tool-name groups]} impl/*tool-options*]
105105
(if output-path
106106
(do
107107
(with-open [w (-> output-path

0 commit comments

Comments
 (0)