diff --git a/src/net/lewisship/cli_tools/builtins.clj b/src/net/lewisship/cli_tools/builtins.clj index 86f8d26..f9cee38 100644 --- a/src/net/lewisship/cli_tools/builtins.clj +++ b/src/net/lewisship/cli_tools/builtins.clj @@ -1,7 +1,5 @@ (ns net.lewisship.cli-tools.builtins "Built-in commands, available to any tool." - {:command-category "Built-in" - :command-category-order 100} (:require [net.lewisship.cli-tools.impl :as impl] [net.lewisship.cli-tools :as cli :refer [defcommand]])) @@ -9,7 +7,8 @@ "List available commands. If a search term is provided, the --commands option is ignored." - [output-level (cli/select-option "-c" "--commands FILTER" "Print commands: " #{:none :root :all} + [output-level (cli/select-option "-c" "--commands FILTER" "Print commands:" + #{:none :root :all} :default :default) :args search-term ["SEARCH" "Filter shown commands to those that match this term" @@ -17,4 +16,3 @@ (if search-term (impl/print-search-results search-term) (impl/print-tool-help output-level))) - diff --git a/src/net/lewisship/cli_tools/completions.clj b/src/net/lewisship/cli_tools/completions.clj index 45fcd97..d44139d 100644 --- a/src/net/lewisship/cli_tools/completions.clj +++ b/src/net/lewisship/cli_tools/completions.clj @@ -58,8 +58,9 @@ (defn- extract-command [fn-prefix [command-name command-map]] (let [{:keys [fn]} command-map - title (binding [ansi/*color-enabled* false] - (impl/extract-command-title command-map)) + title (-> (impl/extract-command-title command-map) + ansi/compose + string/trim) fn-name (simplify fn-prefix command-name)] ;; TODO: Support messy group/command combos (if fn @@ -110,7 +111,8 @@ io/output-stream io/writer)] (try - (binding [*out* w] + (binding [*out* w + ansi/*color-enabled* false] (print-tool tool-name command-root groups)) (catch Throwable t (abort 1 [:red @@ -119,5 +121,5 @@ (class t))])))) (perr [:cyan "Wrote " output-path])) ;; Just write to standard output - (print-tool tool-name command-root groups))))) - + (binding [ansi/*color-enabled* false] + (print-tool tool-name command-root groups)))))) diff --git a/src/net/lewisship/cli_tools/impl.clj b/src/net/lewisship/cli_tools/impl.clj index d55d3ed..d1ab653 100644 --- a/src/net/lewisship/cli_tools/impl.clj +++ b/src/net/lewisship/cli_tools/impl.clj @@ -724,7 +724,7 @@ frequencies)] [:faint (when command-count - (list + (str (h/numberword command-count) " " (inflect/pluralize-noun command-count "sub-command"))) (when (and command-count group-count) diff --git a/test-resources/expected/messy-completions.txt b/test-resources/expected/messy-completions.txt new file mode 100644 index 0000000..a1a195a --- /dev/null +++ b/test-resources/expected/messy-completions.txt @@ -0,0 +1,53 @@ +#compdef _messy messy + +_messy() { + local line state + + _arguments -C \ + "1: :->cmds" \ + "*::arg:->args" + + case "$state" in + cmds) + _values "messy command" \ + "completions[Generate zsh command completions]" \ + "simple[Simple command]" \ + "messy[Messy command]" \ + "help[List available commands]" + ;; + args) + case $line[1] in + completions) _messy_completions ;; + + simple) _messy_simple ;; + + messy) _messy_messy ;; + + help) _messy_help ;; + + esac + ;; + esac +} + +_messy_completions() { + _arguments -s \ + '(-h --help)'{-h,--help}$'[This command summary]' +} + +_messy_simple() { + _arguments -s \ + '(-h --help)'{-h,--help}$'[This command summary]' +} + +_messy_messy() { + _arguments -s \ + '(-h --help)'{-h,--help}$'[This command summary]' +} + +_messy_help() { + _arguments -s \ + '(-c --commands)'{-c,--commands}$'[Print commands: all, none, root]':FILTER \ + '(-h --help)'{-h,--help}$'[This command summary]' +} + diff --git a/test-resources/expected/simple-completions.txt b/test-resources/expected/simple-completions.txt new file mode 100644 index 0000000..80c1783 --- /dev/null +++ b/test-resources/expected/simple-completions.txt @@ -0,0 +1,45 @@ +#compdef _simple simple + +_simple() { + local line state + + _arguments -C \ + "1: :->cmds" \ + "*::arg:->args" + + case "$state" in + cmds) + _values "simple command" \ + "colors[Shows available foreground and background colors]" \ + "completions[Generate zsh command completions]" \ + "help[List available commands]" + ;; + args) + case $line[1] in + colors) _simple_colors ;; + + completions) _simple_completions ;; + + help) _simple_help ;; + + esac + ;; + esac +} + +_simple_colors() { + _arguments -s \ + '(-h --help)'{-h,--help}$'[This command summary]' +} + +_simple_completions() { + _arguments -s \ + '(-h --help)'{-h,--help}$'[This command summary]' +} + +_simple_help() { + _arguments -s \ + '(-c --commands)'{-c,--commands}$'[Print commands: all, none, root]':FILTER \ + '(-h --help)'{-h,--help}$'[This command summary]' +} + diff --git a/test-resources/expected/subgroup-completions.txt b/test-resources/expected/subgroup-completions.txt new file mode 100644 index 0000000..1924358 --- /dev/null +++ b/test-resources/expected/subgroup-completions.txt @@ -0,0 +1,68 @@ +#compdef _subgroup subgroup + +_subgroup() { + local line state + + _arguments -C \ + "1: :->cmds" \ + "*::arg:->args" + + case "$state" in + cmds) + _values "subgroup command" \ + "completions[Generate zsh command completions]" \ + "help[List available commands]" \ + "subgroup[one sub-command]" + ;; + args) + case $line[1] in + completions) _subgroup_completions ;; + + help) _subgroup_help ;; + + subgroup) _subgroup_subgroup ;; + + esac + ;; + esac +} + +_subgroup_completions() { + _arguments -s \ + '(-h --help)'{-h,--help}$'[This command summary]' +} + +_subgroup_help() { + _arguments -s \ + '(-c --commands)'{-c,--commands}$'[Print commands: all, none, root]':FILTER \ + '(-h --help)'{-h,--help}$'[This command summary]' +} + +_subgroup_subgroup() { + local state line + + _arguments -C \ + "1: :->cmds" \ + "*::arg:->args" + + case "$state" in + cmds) + _values "subgroup subgroup subcommands" \ + "example[Does something interesting]" + ;; + args) + case $line[1] in + example) _subgroup_subgroup_example ;; + esac + ;; + esac +} + + +_subgroup_subgroup_example() { + _arguments -s \ + '(-v --verbose)'{-v,--verbose}$'[Extra output]' \ + '(-l --limit)'{-l,--limit}$'[Maximum globnars to frobnicate]':NUMBER \ + '(-h --help)'{-h,--help}$'[This command summary]' +} + diff --git a/test/net/lewisship/cli_tools/aux.clj b/test/net/lewisship/cli_tools/aux.clj index cb3a6e0..5d137d5 100644 --- a/test/net/lewisship/cli_tools/aux.clj +++ b/test/net/lewisship/cli_tools/aux.clj @@ -1,6 +1,7 @@ (ns net.lewisship.cli-tools.aux (:require [clojure.test :refer [is]] - [net.lewisship.cli-tools.test :refer [with-exit]])) + [net.lewisship.cli-tools :as cli-tools] + [net.lewisship.cli-tools.test :refer [with-exit capture-result]])) (defmacro with-exit-errors [expected-errors & body] @@ -9,3 +10,8 @@ (reset! *errors# errors#))] (with-exit 1 ~@body)) (is (= @*errors# ~expected-errors)))) + +(defn dispatch-with-result + [options] + (capture-result + (cli-tools/dispatch (assoc options :cache-dir nil)))) diff --git a/test/net/lewisship/cli_tools/completion_group.clj b/test/net/lewisship/cli_tools/completion_group.clj new file mode 100644 index 0000000..15d396c --- /dev/null +++ b/test/net/lewisship/cli_tools/completion_group.clj @@ -0,0 +1,10 @@ +(ns net.lewisship.cli-tools.completion-group + (:require [net.lewisship.cli-tools :refer [defcommand]])) + +(defcommand example + "Does something interesting." + [verbose? ["-v" "--verbose" "Extra output"] + limit ["-l" "--limit NUMBER" "Maximum globnars to frobnicate"]] + ;; Make linter happy + {:verbose? verbose? + :limit limit}) diff --git a/test/net/lewisship/cli_tools/completions_test.clj b/test/net/lewisship/cli_tools/completions_test.clj new file mode 100644 index 0000000..ab7349d --- /dev/null +++ b/test/net/lewisship/cli_tools/completions_test.clj @@ -0,0 +1,53 @@ +(ns net.lewisship.cli-tools.completions-test + "Tests related to command completions." + (:require [clojure.java.io :as io] + [clojure.string :as string] + [clojure.test :refer [deftest is]] + [net.lewisship.cli-tools.aux :refer [dispatch-with-result]])) + +(defn- expected [file] + (-> (str "expected/" file) + io/resource + slurp + string/split-lines)) + +(defn- dispatch + ([options] + (-> options + (assoc :arguments ["completions"]) + dispatch-with-result + :out-lines)) + ([to options] + (let [result (-> options + (assoc :arguments ["completions"]) + dispatch-with-result + :out)] + (-> (str "test-resources/expected/" to) + io/file + (spit result)) + (println result)))) + +(deftest simple-completions + (is (match? (expected "simple-completions.txt") + (dispatch '{:tool-name "simple" + :namespaces [net.lewisship.cli-tools.colors + net.lewisship.cli-tools.completions]})))) + +(deftest subgroup-completion + (is (match? (expected "subgroup-completions.txt") + (dispatch + '{:tool-name "subgroup" + :namespaces [net.lewisship.cli-tools.completions] + :groups + {"subgroup" {:namespaces [net.lewisship.cli-tools.completion-group]}}})))) + +(deftest messy-completions + ;; where command name and group name collide + ;; Not sure the current behavior is correct + (is (match? (expected "messy-completions.txt") + (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"}}})))) diff --git a/test/net/lewisship/cli_tools_test.clj b/test/net/lewisship/cli_tools_test.clj index 66c1f58..1dd2a9d 100644 --- a/test/net/lewisship/cli_tools_test.clj +++ b/test/net/lewisship/cli_tools_test.clj @@ -8,14 +8,10 @@ net.lewisship.conflict [net.lewisship.cli-tools.impl :as impl] [net.lewisship.cli-tools.test :refer [with-err-str capture-result]] - [net.lewisship.cli-tools.aux :refer [with-exit-errors]] + [net.lewisship.cli-tools.aux :refer [with-exit-errors dispatch-with-result]] [clojure.repl :as repl]) (:import (java.io BufferedReader StringReader))) -(defn- dispatch - [options] - (capture-result - (cli-tools/dispatch (assoc options :cache-dir nil)))) (use-fixtures :once @@ -107,9 +103,9 @@ (defn invoke-command [& args] - (dispatch {:tool-name "harness" - :namespaces ['net.lewisship.cli-tools-test] - :arguments args})) + (dispatch-with-result {:tool-name "harness" + :namespaces ['net.lewisship.cli-tools-test] + :arguments args})) (deftest tool-options-access @@ -200,62 +196,62 @@ (deftest help-with-default-and-explicit-summary (is (match? {:status 0 :out (slurp "test-resources/tool-help.txt")} - (dispatch {:tool-name "test-harness" - :doc "Example commands as part of unit test suite. + (dispatch-with-result {:tool-name "test-harness" + :doc "Example commands as part of unit test suite. Even this docstring is part of the test." - :namespaces '[net.lewisship.example-ns] - :arguments ["help"]})))) + :namespaces '[net.lewisship.example-ns] + :arguments ["help"]})))) (deftest group-help-defaults-from-first-ns-meta (is (match? {:status 0 :out (slurp "test-resources/tool-help-group-default.txt")} - (dispatch {:tool-name "test-harness" + (dispatch-with-result {:tool-name "test-harness" :groups {"group" {:namespaces '[net.lewisship.group-default-ns net.lewisship.example-ns]}} - :arguments ["help"]})))) + :arguments ["help"]})))) (deftest group-help-full (is (match? {:status 0 :out (slurp "test-resources/tool-help-group-full.txt")} - (dispatch {:tool-name "test-harness" + (dispatch-with-result {:tool-name "test-harness" :groups {"group" {:namespaces '[net.lewisship.group-default-ns net.lewisship.example-ns]}} - :arguments ["help" "-c" "all"]})))) + :arguments ["help" "-c" "all"]})))) (deftest help-with-search-term (is (match? {:status 0 :out (slurp "test-resources/tool-help-search.txt")} - (dispatch {:tool-name "test-harness" - :namespaces '[net.lewisship.example-ns] - :arguments ["help" "EXP"]})))) + (dispatch-with-result {:tool-name "test-harness" + :namespaces '[net.lewisship.example-ns] + :arguments ["help" "EXP"]})))) (deftest help-with-search-term-no-match (is (match? {:status 0 :out (slurp "test-resources/tool-help-search-no-match.txt")} - (dispatch {:tool-name "test-harness" - :namespaces '[net.lewisship.example-ns] - :arguments ["help" "Xyzzyx"]})))) + (dispatch-with-result {:tool-name "test-harness" + :namespaces '[net.lewisship.example-ns] + :arguments ["help" "Xyzzyx"]})))) (deftest use-of-command-ns-meta (is (match? {:status 0 :out (slurp "test-resources/combo-help.txt")} - (dispatch {:tool-name "combo" - :namespaces '[net.lewisship.cli-tools.colors] - :arguments ["-h"]})))) + (dispatch-with-result {:tool-name "combo" + :namespaces '[net.lewisship.cli-tools.colors] + :arguments ["-h"]})))) (deftest pre-invoke-callback (let [*args (atom nil) callback (fn [command-map remaining-args] (reset! *args [command-map remaining-args]))] (is (match? {:status 0} - (dispatch {:tool-name "callback" - :pre-invoke callback - :namespaces '[net.lewisship.cli-tools.colors] - :arguments ["colors" "-h"]}))) + (dispatch-with-result {:tool-name "callback" + :pre-invoke callback + :namespaces '[net.lewisship.cli-tools.colors] + :arguments ["colors" "-h"]}))) (is (match? [{:command-path ["colors"]} ["-h"]] @@ -338,13 +334,13 @@ (defn exec-group [& args] - (dispatch {:tool-name "group-test" - :namespaces '[net.lewisship.example-ns] - :groups {"group" {:namespaces '[net.lewisship.group-ns] + (dispatch-with-result {:tool-name "group-test" + :namespaces '[net.lewisship.example-ns] + :groups {"group" {:namespaces '[net.lewisship.group-ns] :doc "Grouped commands" :groups {"nested" {:namespaces '[net.lewisship.cli-tools.group-nested] :doc "Nested commands inside group"}}}} - :arguments args})) + :arguments args})) (deftest help-with-default-and-explicit-summary-grouped (is (match? {:status 0 @@ -362,11 +358,11 @@ (exec-group "gr" "--help"))))) (comment - (->> (dispatch {:tool-name "test-harness" + (->> (dispatch-with-result {:tool-name "test-harness" :groups {"group" {:namespaces '[net.lewisship.group-default-ns net.lewisship.example-ns]}} - :arguments ["help" "-c" "all"]}) + :arguments ["help" "-c" "all"]}) :out (spit "test-resources/tool-help-grouped.txt"))