From 257c6d4fe715f0435331ae30eb65f84a74f4ffca Mon Sep 17 00:00:00 2001 From: "Howard M. Lewis Ship" Date: Fri, 21 Nov 2025 14:29:15 -0800 Subject: [PATCH 1/3] Ensure that extra tool options are written to completions --- CHANGES.md | 4 +- resources/net/lewisship/cli_tools/group.tpl | 4 +- .../net/lewisship/cli_tools/top-level.tpl | 8 ++-- src/net/lewisship/cli_tools.clj | 14 ++----- src/net/lewisship/cli_tools/completions.clj | 19 +++++---- src/net/lewisship/cli_tools/impl.clj | 6 +++ test-resources/expected/messy-completions.txt | 8 ++-- .../expected/simple-completions.txt | 8 ++-- .../expected/subgroup-completions.txt | 10 ++--- test-resources/expected/tool-options.txt | 41 +++++++++++++++++++ .../lewisship/cli_tools/completions_test.clj | 10 ++++- 11 files changed, 91 insertions(+), 41 deletions(-) create mode 100644 test-resources/expected/tool-options.txt diff --git a/CHANGES.md b/CHANGES.md index fe7dd9e..8f54b69 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -25,7 +25,7 @@ * You may now enter `-h` or `--help` after a group to get help for just that group * Tool help output has been reordered, with top-level tool commands first (previously, those were in a "Builtin" group and listed last) * Tool help now displays just root-level commands by default (add `--commands all` to list nested commands) -* When extracting the first sentence as the single-line index, embedded periods are no longer considered the end of the sentence +* When extracting the first sentence as the single-line title, embedded periods are no longer considered the end of the sentence * `net.lewisship.cli-tools`: * Added function `tool-name` * Added function `command-root` @@ -38,7 +38,7 @@ * :source-dirs specifies extra directories to consider when caching * :pre-dispatch - callback function invoked before dispatch * :pre-invoke - callback function invoked before the dispatched command function is invoked - * Can now handle "messy" case where a command has the same name as a group + * Can now handle the case where a command has the same name as a group * Cache files are now stored in `~/.cache/net.lewisship.cli-tools` by default * Added initial support for commands defined as Babashka CLI functions * Added `net.lewiship.cli-tools.test` namespace diff --git a/resources/net/lewisship/cli_tools/group.tpl b/resources/net/lewisship/cli_tools/group.tpl index 86dfad0..89fd7ec 100644 --- a/resources/net/lewisship/cli_tools/group.tpl +++ b/resources/net/lewisship/cli_tools/group.tpl @@ -8,7 +8,7 @@ case "$state" in cmds) _values "{{tool}} {{group.name}} subcommands" {% for sub in group.subs %} \ - "{{sub.name}}[{{sub.summary}}]" {% endfor %} + "{{sub.name}}[{{sub.title}}]" {% endfor %} ;; args) case $line[1] in {% for sub in group.subs %} @@ -17,5 +17,3 @@ ;; esac } - - diff --git a/resources/net/lewisship/cli_tools/top-level.tpl b/resources/net/lewisship/cli_tools/top-level.tpl index 0db7cdb..21ed316 100644 --- a/resources/net/lewisship/cli_tools/top-level.tpl +++ b/resources/net/lewisship/cli_tools/top-level.tpl @@ -3,14 +3,15 @@ _{{tool}}() { local line state - _arguments -C \ + _arguments -C {% for opt in options %} \ + {{ opt }} {% endfor %} \ "1: :->cmds" \ - "*::arg:->args" + "*::args:->args" case "$state" in cmds) _values "{{tool}} command" {% for cmd in commands %} \ - "{{cmd.name}}[{{cmd.summary}}]" {% endfor %} + "{{cmd.name}}[{{cmd.title}}]" {% endfor %} ;; args) case $line[1] in {% for cmd in commands %} @@ -20,4 +21,3 @@ _{{tool}}() { ;; esac } - diff --git a/src/net/lewisship/cli_tools.clj b/src/net/lewisship/cli_tools.clj index 639679b..cc4025e 100644 --- a/src/net/lewisship/cli_tools.clj +++ b/src/net/lewisship/cli_tools.clj @@ -190,14 +190,6 @@ ~validations) ~@body)))))) - -(def ^:private - default-tool-options - "Default tool command line options." - [["-C" "--color" "Enable ANSI color output"] - ["-N" "--no-color" "Disable ANSI color output"] - ["-h" "--help" "This command summary"]]) - (defn- expand-tool-options "Expand dispatch options into tool options, leveraging a cache." [options] @@ -221,7 +213,7 @@ (merge {:tool-name tool-name' :cache-digest digest :command-root command-root} - (select-keys options [:doc :arguments :tool-summary :pre-dispatch :pre-invoke])))) + (select-keys options [:doc :arguments :tool-summary :pre-dispatch :pre-invoke :extra-tool-options])))) (defn- dispatch* "Called (indirectly/anonymously) from a tool handler to process remaining command line arguments." @@ -308,7 +300,7 @@ default-dispatch-options dispatch-options) {:keys [extra-tool-options tool-options-handler]} merged-options - full-options (concat extra-tool-options default-tool-options) + full-options (concat extra-tool-options impl/default-tool-options) {:keys [options arguments summary errors]} (cli/parse-opts (:arguments merged-options) full-options @@ -334,7 +326,7 @@ (if tool-options-handler (tool-options-handler options dispatch-options' callback) - (dispatch* dispatch-options')))))) + (callback)))))) (defn select-option "Builds a standard option spec for selecting from a list of possible values. diff --git a/src/net/lewisship/cli_tools/completions.clj b/src/net/lewisship/cli_tools/completions.clj index cbe14db..23cce0d 100644 --- a/src/net/lewisship/cli_tools/completions.clj +++ b/src/net/lewisship/cli_tools/completions.clj @@ -65,12 +65,12 @@ (if fn {:name command-name :fn-name fn-name - :summary title + :title title :options (options command-map)} {:name (->> command-map :command-path (string/join " ")) - :summary title + :title title :fn-name fn-name :subs (map #(extract-command fn-name %) (:subs command-map))}))) @@ -86,12 +86,14 @@ :command command})))) (defn- print-tool - [tool-name command-root _groups] + [tool-name command-root extra-options] (let [prefix (str "_" tool-name) + options (map #(apply to-opt %) (concat extra-options impl/default-tool-options)) commands (->> command-root (keep #(extract-command prefix %)))] (selmer.util/without-escaping (render "top-level" {:tool tool-name + :options options :commands commands}) (render-commands tool-name commands)))) @@ -102,7 +104,9 @@ output-path ["PATH" "File to write completions to." :optional true]] (binding [impl/*introspection-mode* true] - (let [{:keys [command-root tool-name groups]} impl/*tool-options*] + (let [{:keys [command-root tool-name extra-tool-options]} impl/*tool-options* + generator #(binding [ansi/*color-enabled* false] + (print-tool tool-name command-root extra-tool-options))] (if output-path (do (with-open [w (-> output-path @@ -110,9 +114,7 @@ io/output-stream io/writer)] (try - (binding [*out* w - ansi/*color-enabled* false] - (print-tool tool-name command-root groups)) + (generator) (catch Throwable t (abort 1 [:red (command-path) ": " @@ -120,5 +122,4 @@ (class t))])))) (perr [:cyan "Wrote " output-path])) ;; Just write to standard output - (binding [ansi/*color-enabled* false] - (print-tool tool-name command-root groups)))))) + (generator))))) diff --git a/src/net/lewisship/cli_tools/impl.clj b/src/net/lewisship/cli_tools/impl.clj index b6122df..1921338 100644 --- a/src/net/lewisship/cli_tools/impl.clj +++ b/src/net/lewisship/cli_tools/impl.clj @@ -1061,3 +1061,9 @@ :subs)] (cond->> root transformer (transformer dispatch-options)))) + +(def default-tool-options + "Default tool command line options." + [["-C" "--color" "Enable ANSI color output"] + ["-N" "--no-color" "Disable ANSI color output"] + ["-h" "--help" "This command summary"]]) diff --git a/test-resources/expected/messy-completions.txt b/test-resources/expected/messy-completions.txt index a1a195a..9d661fc 100644 --- a/test-resources/expected/messy-completions.txt +++ b/test-resources/expected/messy-completions.txt @@ -3,9 +3,12 @@ _messy() { local line state - _arguments -C \ + _arguments -C \ + '(-C --color)'{-C,--color}$'[Enable ANSI color output]' \ + '(-N --no-color)'{-N,--no-color}$'[Disable ANSI color output]' \ + '(-h --help)'{-h,--help}$'[This command summary]' \ "1: :->cmds" \ - "*::arg:->args" + "*::args:->args" case "$state" in cmds) @@ -29,7 +32,6 @@ _messy() { ;; esac } - _messy_completions() { _arguments -s \ '(-h --help)'{-h,--help}$'[This command summary]' diff --git a/test-resources/expected/simple-completions.txt b/test-resources/expected/simple-completions.txt index 80c1783..e5f3a3d 100644 --- a/test-resources/expected/simple-completions.txt +++ b/test-resources/expected/simple-completions.txt @@ -3,9 +3,12 @@ _simple() { local line state - _arguments -C \ + _arguments -C \ + '(-C --color)'{-C,--color}$'[Enable ANSI color output]' \ + '(-N --no-color)'{-N,--no-color}$'[Disable ANSI color output]' \ + '(-h --help)'{-h,--help}$'[This command summary]' \ "1: :->cmds" \ - "*::arg:->args" + "*::args:->args" case "$state" in cmds) @@ -26,7 +29,6 @@ _simple() { ;; esac } - _simple_colors() { _arguments -s \ '(-h --help)'{-h,--help}$'[This command summary]' diff --git a/test-resources/expected/subgroup-completions.txt b/test-resources/expected/subgroup-completions.txt index 1924358..66fa378 100644 --- a/test-resources/expected/subgroup-completions.txt +++ b/test-resources/expected/subgroup-completions.txt @@ -3,9 +3,12 @@ _subgroup() { local line state - _arguments -C \ + _arguments -C \ + '(-C --color)'{-C,--color}$'[Enable ANSI color output]' \ + '(-N --no-color)'{-N,--no-color}$'[Disable ANSI color output]' \ + '(-h --help)'{-h,--help}$'[This command summary]' \ "1: :->cmds" \ - "*::arg:->args" + "*::args:->args" case "$state" in cmds) @@ -26,7 +29,6 @@ _subgroup() { ;; esac } - _subgroup_completions() { _arguments -s \ '(-h --help)'{-h,--help}$'[This command summary]' @@ -57,8 +59,6 @@ _subgroup_subgroup() { ;; esac } - - _subgroup_subgroup_example() { _arguments -s \ '(-v --verbose)'{-v,--verbose}$'[Extra output]' \ diff --git a/test-resources/expected/tool-options.txt b/test-resources/expected/tool-options.txt new file mode 100644 index 0000000..e5b334f --- /dev/null +++ b/test-resources/expected/tool-options.txt @@ -0,0 +1,41 @@ +#compdef _options options + +_options() { + local line state + + _arguments -C \ + '(-d --debug)'{-d,--debug}$'[Enable debug mode]' \ + '(-o --output-path)'{-o,--output-path}$'[Write output to file, not stdout]':FILE \ + '(-C --color)'{-C,--color}$'[Enable ANSI color output]' \ + '(-N --no-color)'{-N,--no-color}$'[Disable ANSI color output]' \ + '(-h --help)'{-h,--help}$'[This command summary]' \ + "1: :->cmds" \ + "*::args:->args" + + case "$state" in + cmds) + _values "options command" \ + "completions[Generate zsh command completions]" \ + "help[List available commands]" + ;; + args) + case $line[1] in + completions) _options_completions ;; + + help) _options_help ;; + + esac + ;; + esac +} +_options_completions() { + _arguments -s \ + '(-h --help)'{-h,--help}$'[This command summary]' +} + +_options_help() { + _arguments -s \ + '(-c --commands)'{-c,--commands}$'[Print commands: all, none, root]':FILTER \ + '(-h --help)'{-h,--help}$'[This command summary]' +} + diff --git a/test/net/lewisship/cli_tools/completions_test.clj b/test/net/lewisship/cli_tools/completions_test.clj index ab7349d..c5aaa2c 100644 --- a/test/net/lewisship/cli_tools/completions_test.clj +++ b/test/net/lewisship/cli_tools/completions_test.clj @@ -45,9 +45,17 @@ ;; where command name and group name collide ;; Not sure the current behavior is correct (is (match? (expected "messy-completions.txt") - (dispatch + (dispatch {:tool-name "messy" :namespaces '[net.lewisship.cli-tools.completions net.lewisship.messy-commands] :groups {"messy" {:namespaces '[net.lewisship.messy] :doc "Messy command and group at same time"}}})))) + +(deftest tool-options + (is (match? (expected "tool-options.txt") + (dispatch + {:tool-name "options" + :namespaces '[net.lewisship.cli-tools.completions] + :extra-tool-options [["-d" "--debug" "Enable debug mode"] + ["-o" "--output-path FILE" "Write output to file, not stdout"]]})))) From ed52ea8457e82fbe9c9cedf6754a8ff0fa19faf3 Mon Sep 17 00:00:00 2001 From: "Howard M. Lewis Ship" Date: Fri, 21 Nov 2025 14:32:29 -0800 Subject: [PATCH 2/3] Fix writing completions to a file --- src/net/lewisship/cli_tools/completions.clj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/net/lewisship/cli_tools/completions.clj b/src/net/lewisship/cli_tools/completions.clj index 23cce0d..26ef139 100644 --- a/src/net/lewisship/cli_tools/completions.clj +++ b/src/net/lewisship/cli_tools/completions.clj @@ -114,7 +114,8 @@ io/output-stream io/writer)] (try - (generator) + (binding [*out* w] + (generator)) (catch Throwable t (abort 1 [:red (command-path) ": " From e3c8f82d15304acb2841be2b0e60de09974f911e Mon Sep 17 00:00:00 2001 From: "Howard M. Lewis Ship" Date: Fri, 21 Nov 2025 14:38:12 -0800 Subject: [PATCH 3/3] Bump dependencies, fix linter warning --- .github/workflows/clojure.yml | 2 +- deps.edn | 4 ++-- test/net/lewisship/cli_tools_test.clj | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/clojure.yml b/.github/workflows/clojure.yml index 0fcd29b..42c16df 100644 --- a/.github/workflows/clojure.yml +++ b/.github/workflows/clojure.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup Java uses: actions/setup-java@v5 diff --git a/deps.edn b/deps.edn index d5dc3f8..ae46322 100644 --- a/deps.edn +++ b/deps.edn @@ -27,8 +27,8 @@ {:extra-deps {org.clojure/tools.cli {:mvn/version "1.2.245" :optional true} babashka/fs {:mvn/version "0.5.27" :optional true} babashka/process {:mvn/version "0.6.23" :optional true} - selmer/selmer {:mvn/version "1.12.65" :optional true} - org.babashka/cli {:mvn/version "0.8.66" :optional true}}} + selmer/selmer {:mvn/version "1.12.69" :optional true} + org.babashka/cli {:mvn/version "0.8.67" :optional true}}} :1.11 {:override-deps {org.clojure/clojure ^:antq/exclude {:mvn/version "1.11.4"}}} diff --git a/test/net/lewisship/cli_tools_test.clj b/test/net/lewisship/cli_tools_test.clj index c19fa68..8ed0a78 100644 --- a/test/net/lewisship/cli_tools_test.clj +++ b/test/net/lewisship/cli_tools_test.clj @@ -20,6 +20,7 @@ :cache-dir nil}] (f)))) +#_{:clj-kondo/ignore [:unused-private-var]} (defn- capture "Used when output changes to capture new output (the embedded ANSI sequences are hard to work with." ([file result]