From e8dff51bb6addf771fdf00e4a18b5009eb4a24c5 Mon Sep 17 00:00:00 2001 From: "Howard M. Lewis Ship" Date: Tue, 14 Oct 2025 12:08:22 -0700 Subject: [PATCH] Add a :pre-invoke callback --- CHANGES.md | 2 ++ doc/dispatch.md | 16 ++++++++++++++++ src/net/lewisship/cli_tools.clj | 4 ++-- src/net/lewisship/cli_tools/impl.clj | 4 +++- src/net/lewisship/cli_tools/specs.clj | 8 +++++++- test/net/lewisship/cli_tools_test.clj | 14 ++++++++++++++ 6 files changed, 44 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index f4a9f84..1e310c6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -36,6 +36,8 @@ * :tool-options-handler - callback function for handling extra tool options * :transformer provides a function to add additional commands and groups after namespaces are loaded * :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 * Cache files are now stored in `~/.cache/net.lewisship.cli-tools` by default * Added initial support for commands defined as Babashka CLI functions diff --git a/doc/dispatch.md b/doc/dispatch.md index a7a1637..b1992ff 100644 --- a/doc/dispatch.md +++ b/doc/dispatch.md @@ -61,3 +61,19 @@ If :doc is omitted, then `dispatch` will search the group's namespaces, and use it finds. If :title is omitted, then the first line of the docstring, up to the first `.`, will be used as the title. + +## Callbacks + +Certain options define callbacks that will be invoked during dispatch. These are optional. + +Any return value from the callbacks is ignored. + +The :pre-dispatch callback is invoked once all command data has been assembled (possibly, by loading from cache). +It is passed the full dispatch options. + +After :pre-dispatch, `dispatch` will start to consume command line arguments to identify the specific command function +to invoke. + +The :pre-invoke callback is invoked once a specific command function is identified. +The callback is invoked immediately before the command function is invoked; it is passed the +command map, and a seq of remaining arguments (that will be passed to the command function). diff --git a/src/net/lewisship/cli_tools.clj b/src/net/lewisship/cli_tools.clj index 103e0d5..83559d4 100644 --- a/src/net/lewisship/cli_tools.clj +++ b/src/net/lewisship/cli_tools.clj @@ -220,7 +220,7 @@ (merge {:tool-name tool-name' :cache-digest digest :command-root command-root} - (select-keys options [:doc :arguments :tool-summary :pre-dispatch])))) + (select-keys options [:doc :arguments :tool-summary :pre-dispatch :pre-invoke])))) (defn- dispatch* "Called (indirectly/anonymously) from a tool handler to process remaining command line arguments." @@ -258,7 +258,7 @@ (as identified by the first command line argument) and processes CLI options and arguments. dispatch-options: - + - :tool-name (optional, string) - used in command summary and errors - :doc (optional, string) - used in help summary - :arguments - command line arguments to parse (defaults to `*command-line-args*`) diff --git a/src/net/lewisship/cli_tools/impl.clj b/src/net/lewisship/cli_tools/impl.clj index eb7fc47..d55d3ed 100644 --- a/src/net/lewisship/cli_tools/impl.clj +++ b/src/net/lewisship/cli_tools/impl.clj @@ -886,7 +886,7 @@ help-suffix))) (defn dispatch - [{:keys [command-root arguments tool-name] :as options}] + [{:keys [command-root arguments tool-name pre-invoke] :as options}] (binding [*tool-options* options] (let [command-name (first arguments)] (if (or (nil? command-name) @@ -933,6 +933,8 @@ (:fn matched-command) (let [invoke-command #(binding [*command-map* matched-command] + (when pre-invoke + (pre-invoke matched-command remaining-args)) (apply (-> matched-command :fn requiring-resolve) remaining-args))] ;; It's a command, but has :subs, so it's also a group (this is the "messy" scenario) (if (-> matched-command :subs seq) diff --git a/src/net/lewisship/cli_tools/specs.clj b/src/net/lewisship/cli_tools/specs.clj index 949f8db..37c3331 100644 --- a/src/net/lewisship/cli_tools/specs.clj +++ b/src/net/lewisship/cli_tools/specs.clj @@ -13,7 +13,9 @@ ::tool-options-handler ::extra-tool-options ::source-dirs - ::cache-dir])) + ::cache-dir + ::pre-dispatch + ::pre-invoke])) (s/def ::non-blank-string (s/and string? #(not (str/blank? %)))) @@ -39,6 +41,10 @@ (s/def ::tool-options-handler fn?) +(s/def ::pre-dispatch fn?) + +(s/def ::pre-invoke fn?) + (s/def ::source-dirs (s/coll-of string?)) ;; dispatch doesn't return diff --git a/test/net/lewisship/cli_tools_test.clj b/test/net/lewisship/cli_tools_test.clj index 39d6132..66c1f58 100644 --- a/test/net/lewisship/cli_tools_test.clj +++ b/test/net/lewisship/cli_tools_test.clj @@ -247,6 +247,20 @@ :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"]}))) + + (is (match? [{:command-path ["colors"]} + ["-h"]] + @*args)))) + (defcommand set-mode "Sets the execution mode" [mode ["-m" "--mode MODE" (str "Execution mode, one of " mode-names)