Skip to content

Commit 668b1c2

Browse files
authored
Merge pull request #38 from hlship/hls/20280515-messy
Handle messy groups (groups that are also commands)
2 parents b42b9d0 + e67d984 commit 668b1c2

File tree

10 files changed

+202
-59
lines changed

10 files changed

+202
-59
lines changed

CHANGES.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@
2424
* Tool help output has been reordered, with top-level tool commands first (previously, those were in a "Builtin" group and listed last)
2525
* Tool help now displays just top-level commands by default (add --full to list nested commands)
2626
* When extracting the first sentence as the single-line index, embedded period are no longer considered the end of the sentence
27-
* net.lewisship.cli-tools
27+
* `net.lewisship.cli-tools`:
2828
* New `command-path` function returns a composed string of the tool name and command path
2929
* `dispatch` function has new options:
3030
* :handler is a function to handle top-level tool options (then delegate to `dispatch*`)
3131
* :transformer provides a function to add additional commands and groups after namespaces are loaded
3232
* :source-dirs specifies extra directories to consider when caching
33+
* Can handle "messy" case where a command has the same name as a group
34+
* Cache files are now stored in `~/.cache/net.lewisship.cli-tools` by default
3335

3436
# 0.15.1 -- 27 Jan 2025
3537

doc/caching.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ Our mockup of 1500 commands across 250 namespaces executes twice as fast using t
1818

1919
Babashka is amazingly fast for these purposes; the same test executes in 0.23 seconds.
2020

21-
By default, `dispatch` will store its cache in the `~/.cli-tools-cache` directory; the environment variable
22-
`CLI_TOOLS_CACHE_DIR` can override this default.
21+
By default, `dispatch` will store its cache in the `~/.cache/net.lewisship.cli-tools` directory; the environment variable
22+
`CLI_TOOLS_CACHE_DIR` can override this default. (The `~/.cache` part is via
23+
`babaska.fs/xdg-cache-home`).
2324

24-
Alternately, you can also specify a path as the :cache-dir option.
25+
Alternately, you can also specify a java.nio.file.Path instance as the :cache-dir option.
2526

src/net/lewisship/cli_tools.clj

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,8 +256,9 @@
256256
color-flag help)))
257257

258258
(def ^:private default-dispatch-options
259-
{:cache-dir (or (System/getenv "CLI_TOOLS_CACHE_DIR")
260-
"~/.cli-tools-cache")
259+
{:cache-dir (or (some-> (System/getenv "CLI_TOOLS_CACHE_DIR")
260+
fs/expand-home)
261+
(fs/xdg-cache-home "net.lewisship.cli-tools"))
261262
:handler default-tool-handler})
262263

263264
(defn dispatch

src/net/lewisship/cli_tools/cache.cljc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@
9090
(defn write-to-cache
9191
[cache-root digest cache-data]
9292
(let [_ (when-not (fs/exists? cache-root)
93-
(fs/create-dir cache-root))
93+
(fs/create-dirs cache-root))
9494
f (fs/file cache-root (str digest ".edn"))]
9595
(spit f (pr-str cache-data))))
9696

src/net/lewisship/cli_tools/impl.clj

Lines changed: 87 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -686,8 +686,9 @@
686686

687687
(defn- command-match?
688688
[command search-term]
689-
(let [{:keys [doc title command-path]} command]
689+
(let [{:keys [doc group-doc title command-path]} command]
690690
(or (string-matches? doc search-term)
691+
(string-matches? group-doc search-term)
691692
(string-matches? title search-term)
692693
(some #(string-matches? % search-term) command-path))))
693694

@@ -709,11 +710,13 @@
709710
[command-map]
710711
(or (:title command-map)
711712
(-> command-map :doc first-sentence)
712-
(when-not (:fn command-map)
713+
(-> command-map :group-doc first-sentence)
714+
(when (-> command-map :subs seq)
713715
(let [{command-count true
714716
group-count false} (->> command-map
715717
:subs
716718
vals
719+
;; Not quite accurate if messy (command and group at same time)
717720
(map #(-> % :fn some?))
718721
frequencies)]
719722
[:faint
@@ -743,7 +746,7 @@
743746
(pout (when recurse? "\n")
744747
[:bold (string/join " " (:command-path container-map))]
745748
" - "
746-
(or (some-> container-map :doc cleanup-docstring)
749+
(or (some-> container-map :group-doc cleanup-docstring)
747750
missing-doc)))
748751

749752
(when (seq sorted-commands)
@@ -761,7 +764,7 @@
761764
;; Recurse and print sub-groups
762765
(when recurse?
763766
(->> sorted-commands
764-
(remove :fn) ; Remove commands, leave groups
767+
(filter #(-> % :subs seq)) ; Remove commands, leave groups and messy command/groups
765768
(run! #(print-commands command-name-width' % (:subs %) true))))))
766769

767770
(defn- command-path-width
@@ -840,18 +843,60 @@
840843
[tool-name]
841844
(list ", use " [:bold.green tool-name " help"] " to list commands"))
842845

846+
(defn- no-command
847+
[tool-name]
848+
(abort [:bold.green tool-name] ": no command provided" (use-help-message tool-name)))
849+
850+
(defn- incomplete
851+
[tool-name command-path matchable-terms]
852+
(abort
853+
[:bold.green tool-name ": "
854+
(string/join " " (butlast command-path))
855+
[:red (last command-path)]]
856+
" is incomplete; "
857+
(compose-list matchable-terms)
858+
" could follow; use "
859+
[:bold [:green tool-name " " (string/join " " command-path) " --help (or -h)"]]
860+
" to list commands"))
861+
862+
(defn- no-match
863+
[tool-name command-path term matched-terms matchable-terms]
864+
(let [body (if (-> matched-terms count pos?)
865+
(list "could match "
866+
(compose-list matched-terms {:conjuction "or"}))
867+
(list "is not a command, expected "
868+
(compose-list matchable-terms {:conjuction "or"})))
869+
help-suffix (list
870+
"; use "
871+
[:bold [:green tool-name " "
872+
(if (seq command-path)
873+
(string/join " " command-path)
874+
"help")]]
875+
(when (seq command-path)
876+
" --help (or -h)")
877+
" to list commands")]
878+
(abort
879+
[:bold [:green tool-name] ": "
880+
[:green (string/join " " command-path)]
881+
(when (seq command-path) " ")
882+
[:red term]]
883+
" "
884+
body
885+
help-suffix)))
886+
843887
(defn dispatch
844888
[{:keys [command-root arguments tool-name] :as options}]
845889
(binding [*tool-options* options]
846890
(let [command-name (first arguments)]
847891
(if (or (nil? command-name)
848892
(string/starts-with? command-name "-"))
849-
(abort [:bold.green tool-name] ": no command provided" (use-help-message tool-name))
850-
(loop [prefix [] ; Needed?
851-
term command-name
852-
remaining-args (next arguments)
853-
container-map nil
854-
commands-map command-root]
893+
(no-command tool-name)
894+
(loop [command-path []
895+
term command-name
896+
remaining-args (next arguments)
897+
container-map nil
898+
commands-map command-root
899+
invoke-last-command-fn nil]
855900
(cond-let
856901
(#{"-h" "--help"} term)
857902
(do
@@ -864,58 +909,48 @@
864909
;; Options start with a '-', but we're still looking for commands
865910
(or (nil? term)
866911
(string/starts-with? term "-"))
867-
(abort
868-
[:bold.green tool-name ": "
869-
(string/join " " (butlast prefix))
870-
[:red (last prefix)]]
871-
" is incomplete; "
872-
(compose-list matchable-terms)
873-
" could follow; use "
874-
[:bold [:green tool-name " " (string/join " " prefix) " --help (or -h)"]]
875-
" to list commands")
912+
;; In messy mode, this lets us backtrack to the containing command (which is also a group, that's
913+
;; why we call it messy) and invoke it as a command now that know the next term doesn't indentify
914+
;; another command or group.
915+
(if invoke-last-command-fn
916+
(invoke-last-command-fn)
917+
(incomplete tool-name command-path matchable-terms))
876918

877919
:let [matched-terms (find-matches term matchable-terms)
878920
match-count (count matched-terms)]
879921

880922
(not= 1 match-count)
881-
(let [body (if (pos? match-count)
882-
(list "could match "
883-
(compose-list matched-terms {:conjuction "or"}))
884-
(list "is not a command, expected "
885-
(compose-list matchable-terms {:conjuction "or"})))
886-
help-suffix (list
887-
"; use "
888-
[:bold [:green tool-name " "
889-
(if (seq prefix)
890-
(string/join " " prefix)
891-
"help")]]
892-
(when (seq prefix)
893-
" --help (or -h)")
894-
" to list commands")]
895-
(abort
896-
[:bold [:green tool-name] ": "
897-
[:green (string/join " " prefix)]
898-
(when (seq prefix) " ")
899-
[:red term]]
900-
" "
901-
body
902-
help-suffix))
923+
;; Likewise, if the next term doesn't look like an option, but doesn't match a nested command or group
924+
;; then it must be a positional argument to the container command (that's also a messy group).
925+
(if invoke-last-command-fn
926+
(invoke-last-command-fn)
927+
(no-match tool-name command-path term matched-terms matchable-terms))
903928

904929
;; Exactly one match
905930
:let [matched-term (first matched-terms)
906931
matched-command (get possible-commands matched-term)]
907932

908933
(:fn matched-command)
909-
(binding [*command-map* matched-command]
910-
(apply (-> matched-command :fn requiring-resolve) remaining-args))
934+
(let [invoke-command #(binding [*command-map* matched-command]
935+
(apply (-> matched-command :fn requiring-resolve) remaining-args))]
936+
;; It's a command, but has :subs, so it's also a group (this is the "messy" scenario)
937+
(if (-> matched-command :subs seq)
938+
(recur (:command-path matched-command)
939+
(first remaining-args)
940+
(rest remaining-args)
941+
matched-command
942+
(:subs matched-command)
943+
invoke-command)
944+
(invoke-command)))
911945

912946
;; Otherwise, it was a command group.
913947
:else
914948
(recur (:command-path matched-command)
915949
(first remaining-args)
916950
(rest remaining-args)
917951
matched-command
918-
(:subs matched-command)))))))
952+
(:subs matched-command)
953+
invoke-last-command-fn))))))
919954
nil)
920955

921956
(defn command-map?
@@ -951,7 +986,7 @@
951986
{:fn (symbol command-var)
952987
;; Commands have a full :doc and an optional short :title
953988
;; (the title defaults to the first sentence of the :doc
954-
;; if not provided
989+
;; if not provided)
955990
:doc doc
956991
:command command-name
957992
:command-path (conj path command-name)}
@@ -971,16 +1006,18 @@
9711006
;; Mix in nested groups to form the subs for this group
9721007
subs (reduce-kv
9731008
(fn [commands group-command group-descriptor]
974-
(assoc commands group-command
1009+
(update commands group-command merge
9751010
(build-command-group group-descriptor path' group-command)))
9761011
direct-commands
9771012
groups)
9781013
doc' (or doc
9791014
(some #(-> % find-ns meta :doc) namespaces))]
980-
{:doc doc' ; groups have just :doc, no :title
981-
:command command
982-
:command-path path'
983-
:subs subs}))
1015+
(cond-> {
1016+
:command command
1017+
:command-path path'
1018+
:subs subs}
1019+
; groups have just :group-doc, no :title
1020+
doc' (assoc :group-doc doc'))))
9841021

9851022
(defn expand-tool-options
9861023
[dispatch-options]

src/net/lewisship/cli_tools/specs.clj

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
(ns net.lewisship.cli-tools.specs
22
(:require [clojure.spec.alpha :as s]
33
[clojure.string :as str]
4-
[net.lewisship.cli-tools :as cli-tools]))
4+
[net.lewisship.cli-tools :as cli-tools])
5+
(:import (java.nio.file Path)))
56

67
(s/def ::dispatch-options (s/keys :req-un [::namespaces]
78
:opt-un [::tool-name
@@ -28,7 +29,7 @@
2829
(s/def ::group (s/keys :req-un [::namespaces]
2930
:opt-un [::doc ::groups]))
3031

31-
(s/def ::cache-dir (s/nilable string?))
32+
(s/def ::cache-dir (s/nilable #(instance? Path %)))
3233

3334
(s/def ::transformer fn?)
3435

test-resources/messy-full-help.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
Usage: bigmess [OPTIONS] COMMAND ...
2+
3+
Options:
4+
-C, --color Enable ANSI color output
5+
-N, --no-color Disable ANSI color output
6+
-h, --help This tool summary
7+
8+
Commands:
9+
help: List available commands
10+
messy: Messy command
11+
simple: Simple command
12+
13+
messy - Messy command and group at same time
14+
15+
Commands:
16+
nested: Command nested under messy group/command
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
(ns net.lewisship.cli-tools.messy-test
2+
(:require [clj-commons.ansi :as ansi]
3+
[clojure.string :as string]
4+
[net.lewisship.cli-tools :as cli]
5+
[net.lewisship.cli-tools.aux :refer [capture-result]]
6+
[clojure.test :refer [deftest is]]))
7+
8+
(cli/set-prevent-exit! true)
9+
10+
(defn- dispatch [& args]
11+
(binding [ansi/*color-enabled* false]
12+
(capture-result
13+
(cli/dispatch {:tool-name "bigmess"
14+
:namespaces '[net.lewisship.messy-commands]
15+
:groups {"messy" {:namespaces '[net.lewisship.messy]
16+
:doc "Messy command and group at same time"}}
17+
:arguments args
18+
:cache-dir nil
19+
:messy? true}))))
20+
21+
(deftest full-help
22+
(is (match? {:status 0
23+
:out (slurp "test-resources/messy-full-help.txt")}
24+
(dispatch "help" "--full"))))
25+
26+
(deftest simple-commands-work
27+
(is (match? {:status 0
28+
:out "simple: ok\n"}
29+
(dispatch "simp"))))
30+
31+
(deftest missing-positional-in-nested
32+
(is (match? {:status 1
33+
:err "Error in bigmess messy: No value for required argument NAME\n"}
34+
(dispatch "messy"))))
35+
36+
(deftest commands-nested-inside-commands
37+
(is (match? {:out "nested: ok\n"}
38+
(dispatch "mess" "nest"))))
39+
40+
(deftest matches-command-when-cant-find-sub-command
41+
(is (match? {:out "messy: kiwi ok\n"}
42+
(dispatch "mess" "kiwi"))))
43+
44+
(comment
45+
46+
(defn capture [f & args]
47+
(let [{:keys [out err]} (apply dispatch args)
48+
captured (if (string/blank? out)
49+
err
50+
out)
51+
out-path (str "test-resources/" f ".txt")]
52+
(spit out-path captured)
53+
(println (str out-path ":"))
54+
(print captured)))
55+
56+
(capture "messy-full-help" "help" "--full")
57+
58+
(capture "messy-simple" "simple")
59+
60+
(capture "messy-nested-fail" "mess" "nomatch")
61+
)
62+
63+

test/net/lewisship/messy.clj

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
(ns net.lewisship.messy
2+
(:require [net.lewisship.cli-tools :refer [defcommand]]))
3+
4+
(defcommand nested
5+
"Command nested under messy group/command."
6+
[]
7+
(println "nested: ok"))

0 commit comments

Comments
 (0)