From d030850c39050bea8c5238f70f18ca87249f374c Mon Sep 17 00:00:00 2001 From: sohalt Date: Wed, 13 Mar 2024 14:16:14 +0100 Subject: [PATCH 1/6] auto-completions for bb.cli/dispatch --- src/babashka/cli.cljc | 148 +++++++++++++++++++--- test/babashka/cli/completion_test.clj | 95 ++++++++++++++ test/resources/completion/completion.bash | 5 + test/resources/completion/completion.fish | 5 + test/resources/completion/completion.zsh | 2 + 5 files changed, 239 insertions(+), 16 deletions(-) create mode 100644 test/babashka/cli/completion_test.clj create mode 100644 test/resources/completion/completion.bash create mode 100644 test/resources/completion/completion.fish create mode 100644 test/resources/completion/completion.zsh diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index 6bb0bef..c146a86 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -4,7 +4,8 @@ #?(:clj [clojure.edn :as edn] :cljs [cljs.reader :as edn]) [babashka.cli.internal :as internal] - [clojure.string :as str]) + [clojure.string :as str] + [clojure.set :as set]) #?(:clj (:import (clojure.lang ExceptionInfo)))) #?(:clj (set! *warn-on-reflection* true)) @@ -590,8 +591,116 @@ {} table)) (comment - (table->tree [{:cmds [] :fn identity}]) - ) + (table->tree [{:cmds [] :fn identity}])) + +;; completion +(defn format-long-opt [k] + (str "--" (kw->str k))) +(defn format-short-opt [k] + (str "-" (kw->str k))) + +(defn possibilities [cmd-tree] + (concat (keys (:cmd cmd-tree)) + (map format-long-opt (keys (:spec cmd-tree))) + (map format-short-opt (keep :alias (vals (:spec cmd-tree)))))) + +(defn true-prefix? [prefix s] + (and (< (count prefix) (count s)) + (str/starts-with? s prefix))) + +(defn second-to-last [xs] + (when (>= (count xs) 2) (nth xs (- (count xs) 2)))) + +(def possible-values (constantly [])) + +(defn strip-prefix [prefix s] + (if (str/starts-with? s prefix) + (subs s (count prefix)) + s)) + +(defn bool-opt? [o spec] + (let [long-opt? (str/starts-with? o "--") + opt-kw (if long-opt? + (keyword (strip-prefix "--" o)) + (some (fn [[k v]] (when (= (keyword (strip-prefix "-" o)) (:alias v)) k)) spec))] + (= :boolean (get-in spec [opt-kw :coerce])))) + +(defn is-gnu-option? [s] + (and s (str/starts-with? s "-"))) + +(defn complete-tree + "given a CLI spec in tree form and input as a list of tokens, + returns possible tokens to complete the input" + [cmd-tree input] + (let [[head & tail] input + head (or head "") + subtree (get-in cmd-tree [:cmd head])] + (if (and subtree (first tail)) + ;; matching command -> descend tree + (complete-tree subtree tail) + (if (is-gnu-option? head) + (let [{:keys [args opts err]} (try (parse-args input cmd-tree) + (catch clojure.lang.ExceptionInfo _ {:err :error}))] + (if (and args (not (str/blank? (first args)))) + ;; parsed/consumed options and still have args left -> descend tree + (complete-tree cmd-tree args) + ;; no more args -> last input is (part of) an opt or opt value or empty string + (let [to-complete (last input) + previous-token (second-to-last input)] + (if (and (is-gnu-option? previous-token) (not (bool-opt? previous-token (:spec cmd-tree)))) + ;; complete value + (possible-values previous-token) + (let [possible-commands (keys (:cmd cmd-tree)) + ;; don't suggest options which we already have parsed + possible-options (set/difference (set (keys (:spec cmd-tree))) (set (keys opts))) + ;; generate string representation of possible options + possible-completions (concat possible-commands + (map format-long-opt possible-options) + (keep (fn [option-name] + (when-let [alias (get-in cmd-tree [:spec option-name :alias])] + (format-short-opt alias))) + possible-options))] + (filter (partial true-prefix? to-complete) possible-completions)))))) + (filter (partial true-prefix? head) (possibilities cmd-tree)))))) + +(defn complete [cmd-table input] + (complete-tree (table->tree cmd-table) input)) + + +(defn generate-completion-shell-snippet [type program-name] + (case type + :bash (format "_babashka_cli_dynamic_completion() +{ + source <( \"$1\" --babashka.cli/complete \"bash\" \"${COMP_WORDS[*]// / }\" ) +} +complete -o nosort -F _babashka_cli_dynamic_completion %s +" program-name) + :zsh (format "#compdef %s +source <( \"${words[1]}\" --babashka.cli/complete \"zsh\" \"${words[*]// / }\" ) +" program-name) + :fish (format "function _babashka_cli_dynamic_completion + set --local COMP_LINE (commandline --cut-at-cursor) + %s --babashka.cli/complete fish $COMP_LINE +end +complete --command %s --no-files --arguments \"(_babashka_cli_dynamic_completion)\" +" program-name program-name))) + +(defn print-completion-shell-snippet [type program-name] + (print (generate-completion-shell-snippet type program-name))) + +(defn format-completion [shell {:keys [completion description]}] + (case shell + :bash (format "COMPREPLY+=( \"%s\" )" completion) + :zsh (str "compadd" (when description (str " -x \"" description "\"")) " -- " completion) + :fish completion)) + +(defn print-completions [shell tree cmdline] + (let [[_program-name & to-complete] (str/split (str/triml cmdline) #" +" -1) + completions (complete-tree tree to-complete)] + (doseq [completion completions] + (println (format-completion shell {:completion completion}))))) + +;; dispatch (defn- deep-merge [a b] (reduce (fn [acc k] (update acc k (fn [v] @@ -656,19 +765,26 @@ ([tree args] (dispatch-tree tree args nil)) ([tree args opts] - (let [{:as res :keys [cmd-info error available-commands]} - (dispatch-tree' tree args opts) - error-fn (or (:error-fn opts) - (fn [{:keys [msg] :as data}] - (throw (ex-info msg data))))] - (case error - (:no-match :input-exhausted) - (error-fn (merge - {:type :org.babashka/cli - :cause error - :all-commands available-commands} - (select-keys res [:wrong-input :opts :dispatch]))) - nil ((:fn cmd-info) (dissoc res :cmd-info)))))) + (let [command-name (get-in opts [:completion :command]) + [opt shell cmdline] args] + (case opt + "--babashka.cli/completion-snippet" + (print-completion-shell-snippet (keyword shell) command-name) + "--babashka.cli/complete" + (print-completions (keyword shell) tree cmdline) + (let [{:as res :keys [cmd-info error available-commands]} + (dispatch-tree' tree args opts) + error-fn (or (:error-fn opts) + (fn [{:keys [msg] :as data}] + (throw (ex-info msg data))))] + (case error + (:no-match :input-exhausted) + (error-fn (merge + {:type :org.babashka/cli + :cause error + :all-commands available-commands} + (select-keys res [:wrong-input :opts :dispatch]))) + nil ((:fn cmd-info) (dissoc res :cmd-info)))))))) (defn dispatch "Subcommand dispatcher. diff --git a/test/babashka/cli/completion_test.clj b/test/babashka/cli/completion_test.clj new file mode 100644 index 0000000..beafa86 --- /dev/null +++ b/test/babashka/cli/completion_test.clj @@ -0,0 +1,95 @@ +(ns babashka.cli.completion-test + (:require [babashka.cli :as cli :refer [complete]] + [clojure.java.io :as io] + [clojure.test :refer :all])) + +(def cmd-table + [{:cmds ["foo"] :spec {:foo-opt {:coerece :string + :alias :f} + :foo-opt2 {:coerece :string} + :foo-flag {:coerce :boolean + :alias :l}}} + {:cmds ["foo" "bar"] :spec {:bar-opt {:coerce :keyword} + :bar-flag {:coerce :boolean}}} + {:cmds ["bar"]} + {:cmds ["bar-baz"]}]) + +(deftest completion-test + (testing "complete commands" + (is (= #{"foo" "bar" "bar-baz"} (set (complete cmd-table [""])))) + (is (= #{"bar" "bar-baz"} (set (complete cmd-table ["ba"])))) + (is (= #{"bar-baz"} (set (complete cmd-table ["bar"])))) + (is (= #{"foo"} (set (complete cmd-table ["f"]))))) + + (testing "no completions for full command" + (is (= #{} (set (complete cmd-table ["foo"]))))) + + (testing "complete subcommands and options" + (is (= #{"bar" "-f" "--foo-opt" "--foo-opt2" "-l" "--foo-flag"} (set (complete cmd-table ["foo" ""]))))) + + (testing "complete suboption" + (is (= #{"-f" "--foo-opt" "--foo-opt2" "-l" "--foo-flag"} (set (complete cmd-table ["foo" "-"]))))) + + (testing "complete short-opt" + (is (= #{} (set (complete cmd-table ["foo" "-f"])))) + (is (= #{} (set (complete cmd-table ["foo" "-f" ""])))) + (is (= #{} (set (complete cmd-table ["foo" "-f" "foo-val"])))) + (is (= #{} (set (complete cmd-table ["foo" "-f" "bar"])))) + (is (= #{} (set (complete cmd-table ["foo" "-f" "foo-flag"])))) + (is (= #{} (set (complete cmd-table ["foo" "-f" "foo-opt2"])))) + (is (= #{} (set (complete cmd-table ["foo" "-f" "123"])))) + (is (= #{} (set (complete cmd-table ["foo" "-f" ":foo"])))) + (is (= #{} (set (complete cmd-table ["foo" "-f" "true"])))) + (is (= #{"bar" "--foo-opt2" "-l" "--foo-flag"} (set (complete cmd-table ["foo" "-f" "foo-val" ""]))))) + + (testing "complete option with same prefix" + (is (= #{"--foo-opt" "--foo-opt2" "--foo-flag"} (set (complete cmd-table ["foo" "--foo"])))) + (is (= #{"--foo-opt2"} (set (complete cmd-table ["foo" "--foo-opt"]))))) + + (testing "complete long-opt" + (is (= #{} (set (complete cmd-table ["foo" "--foo-opt2"])))) + (is (= #{} (set (complete cmd-table ["foo" "--foo-opt" ""])))) + (is (= #{} (set (complete cmd-table ["foo" "--foo-opt" "foo-val"])))) + (is (= #{} (set (complete cmd-table ["foo" "--foo-opt" "bar"])))) + (is (= #{} (set (complete cmd-table ["foo" "--foo-opt" "foo-flag"])))) + (is (= #{} (set (complete cmd-table ["foo" "--foo-opt" "foo-opt2"])))) + (is (= #{} (set (complete cmd-table ["foo" "--foo-opt" "123"])))) + (is (= #{} (set (complete cmd-table ["foo" "--foo-opt" ":foo"])))) + (is (= #{} (set (complete cmd-table ["foo" "--foo-opt" "true"])))) + (is (= #{"bar" "--foo-opt2" "-l" "--foo-flag"} (set (complete cmd-table ["foo" "--foo-opt" "foo-val" ""]))))) + + (is (= #{"--foo-flag"} (set (complete cmd-table ["foo" "--foo-f"])))) + + (testing "complete short flag" + (is (= #{} (set (complete cmd-table ["foo" "-l"])))) + (is (= #{"bar" "-f" "--foo-opt" "--foo-opt2"} (set (complete cmd-table ["foo" "-l" ""]))))) + + (testing "complete long flag" + (is (= #{} (set (complete cmd-table ["foo" "--foo-flag"])))) + (is (= #{"bar" "-f" "--foo-opt" "--foo-opt2"} (set (complete cmd-table ["foo" "--foo-flag" ""]))))) + + (is (= #{"-f" "--foo-opt" "--foo-opt2"} (set (complete cmd-table ["foo" "--foo-flag" "-"])))) + (is (= #{"bar"} (set (complete cmd-table ["foo" "--foo-flag" "b"])))) + + (testing "complete subcommand" + (is (= #{"--bar-opt" "--bar-flag"} (set (complete cmd-table ["foo" "--foo-flag" "bar" ""])))) + (is (= #{"--bar-opt" "--bar-flag"} (set (complete cmd-table ["foo" "--foo-flag" "bar" "-"])))) + (is (= #{"--bar-opt" "--bar-flag"} (set (complete cmd-table ["foo" "--foo-flag" "bar" "--"])))) + (is (= #{"--bar-opt" "--bar-flag"} (set (complete cmd-table ["foo" "--foo-flag" "bar" "--bar-"])))) + (is (= #{"--bar-opt"} (set (complete cmd-table ["foo" "--foo-flag" "bar" "--bar-o"])))) + (is (= #{} (set (complete cmd-table ["foo" "--foo-flag" "bar" "--bar-opt" "a"])))) + (is (= #{"--bar-flag"} (set (complete cmd-table ["foo" "--foo-flag" "bar" "--bar-opt" "bar-val" ""])))))) + + +(deftest parse-opts-completion-test + (cli/parse-opts ["--babashka.cli/completion-snippet" "zsh"] {:complete true}) + (cli/parse-opts ["--babashka.cli/complete" "zsh" "foo"] {:complete true})) + +(deftest dispatch-completion-test + (is (= (slurp (io/resource "resources/completion/completion.zsh")) (with-out-str (cli/dispatch cmd-table ["--babashka.cli/completion-snippet" "zsh"] {:completion {:command "myprogram"}})))) + (is (= (slurp (io/resource "resources/completion/completion.bash")) (with-out-str (cli/dispatch cmd-table ["--babashka.cli/completion-snippet" "bash"] {:completion {:command "myprogram"}})))) + (is (= (slurp (io/resource "resources/completion/completion.fish")) (with-out-str (cli/dispatch cmd-table ["--babashka.cli/completion-snippet" "fish"] {:completion {:command "myprogram"}})))) + + (is (= "compadd -- foo\n" (with-out-str (cli/dispatch cmd-table ["--babashka.cli/complete" "zsh" "myprogram f"] {:completion {:command "myprogram"}})))) + (is (= "COMPREPLY+=( \"foo\" )\n" (with-out-str (cli/dispatch cmd-table ["--babashka.cli/complete" "bash" "myprogram f "] {:completion {:command "myprogram"}})))) + (is (= "foo\n" (with-out-str (cli/dispatch cmd-table ["--babashka.cli/complete" "fish" "myprogram f "] {:completion {:command "myprogram"}}))))) diff --git a/test/resources/completion/completion.bash b/test/resources/completion/completion.bash new file mode 100644 index 0000000..8f47215 --- /dev/null +++ b/test/resources/completion/completion.bash @@ -0,0 +1,5 @@ +_babashka_cli_dynamic_completion() +{ + source <( "$1" --babashka.cli/complete "bash" "${COMP_WORDS[*]// / }" ) +} +complete -o nosort -F _babashka_cli_dynamic_completion myprogram diff --git a/test/resources/completion/completion.fish b/test/resources/completion/completion.fish new file mode 100644 index 0000000..17ab9f6 --- /dev/null +++ b/test/resources/completion/completion.fish @@ -0,0 +1,5 @@ +function _babashka_cli_dynamic_completion + set --local COMP_LINE (commandline --cut-at-cursor) + myprogram --babashka.cli/complete fish $COMP_LINE +end +complete --command myprogram --no-files --arguments "(_babashka_cli_dynamic_completion)" diff --git a/test/resources/completion/completion.zsh b/test/resources/completion/completion.zsh new file mode 100644 index 0000000..4e51479 --- /dev/null +++ b/test/resources/completion/completion.zsh @@ -0,0 +1,2 @@ +#compdef myprogram +source <( "${words[1]}" --babashka.cli/complete "zsh" "${words[*]// / }" ) From 2a65b992119059df6fcca99ac21994ab626f38cb Mon Sep 17 00:00:00 2001 From: sohalt Date: Thu, 21 Mar 2024 12:44:51 +0100 Subject: [PATCH 2/6] complete-options --- src/babashka/cli.cljc | 98 ++++++++++++++++++++------- test/babashka/cli/completion_test.clj | 33 ++++++++- 2 files changed, 106 insertions(+), 25 deletions(-) diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index c146a86..0c79498 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -258,6 +258,9 @@ :kwd-opt kwd-opt? :fst-colon fst-colon?})) +(declare print-completion-shell-snippet) +(declare print-opts-completions) + (defn parse-opts "Parse the command line arguments `args`, a seq of strings. Instead of a leading `:` either `--` or `-` may be used as well. @@ -322,6 +325,19 @@ (-> {:spec spec :type :org.babashka/cli} (merge data) error-fn*)) + [opt shell cmdline] args + _ (case opt + "--babashka.cli/completion-snippet" + (if-let [command-name (get-in opts [:completion :command])] + (do (print-completion-shell-snippet (keyword shell) command-name) + (System/exit 0)) + (binding [*out* *err*] + (println "Need `:completion {:command \"\"}` in opts to support shell completions") + (System/exit 1))) + "--babashka.cli/complete" + (do (print-opts-completions (keyword shell) opts cmdline) + (System/exit 0)) + :noop) {:keys [cmds args]} (parse-cmds args) {new-args :args a->o :args->opts} @@ -611,6 +627,7 @@ (defn second-to-last [xs] (when (>= (count xs) 2) (nth xs (- (count xs) 2)))) +;; TODO complete option values (def possible-values (constantly [])) (defn strip-prefix [prefix s] @@ -618,16 +635,53 @@ (subs s (count prefix)) s)) -(defn bool-opt? [o spec] +(defn bool-opt? [o opts] (let [long-opt? (str/starts-with? o "--") opt-kw (if long-opt? (keyword (strip-prefix "--" o)) - (some (fn [[k v]] (when (= (keyword (strip-prefix "-" o)) (:alias v)) k)) spec))] - (= :boolean (get-in spec [opt-kw :coerce])))) + (get-in opts [:alias (keyword (strip-prefix "-" o))]))] + (= :boolean (get-in opts [:coerce opt-kw])))) (defn is-gnu-option? [s] (and s (str/starts-with? s "-"))) +(defn complete-options + "given an opts map as expected by parse-opts and input as a list of tokens, + returns possible tokens to complete the input" + [opts input] + (let [spec (:spec opts) + opts (if spec + (merge-opts + opts + (spec->opts spec opts)) + opts) + coerce-opts (:coerce opts) + aliases (or + (:alias opts) + (:aliases opts)) + known-keys (set (concat (keys (if (map? spec) + spec (into {} spec))) + (vals aliases) + (keys coerce-opts))) + {parsed-opts :opts :keys [args err]} (try (parse-args input opts) + (catch clojure.lang.ExceptionInfo _ {:err :error})) + to-complete (last input)] + (cond + (and args (not (str/blank? (first args)))) [] + :else + (let [previous-token (second-to-last input) + ;; don't suggest options which we already have parsed + possible-options (set/difference known-keys (set (keys parsed-opts))) + ;; generate string representation of possible options + possible-completions (concat (map format-long-opt possible-options) + (keep (fn [option-name] + (when-let [alias (some (fn [[alias long]] (when (= long option-name) alias)) aliases)] + (format-short-opt alias))) + possible-options))] + (if (and (is-gnu-option? previous-token) (not (bool-opt? previous-token opts))) + (possible-values previous-token to-complete opts) + (filter (partial true-prefix? to-complete) possible-completions)))))) + (defn complete-tree "given a CLI spec in tree form and input as a list of tokens, returns possible tokens to complete the input" @@ -639,34 +693,24 @@ ;; matching command -> descend tree (complete-tree subtree tail) (if (is-gnu-option? head) - (let [{:keys [args opts err]} (try (parse-args input cmd-tree) - (catch clojure.lang.ExceptionInfo _ {:err :error}))] + (let [{:keys [args]} (try (parse-args input cmd-tree) + (catch clojure.lang.ExceptionInfo _))] (if (and args (not (str/blank? (first args)))) ;; parsed/consumed options and still have args left -> descend tree (complete-tree cmd-tree args) ;; no more args -> last input is (part of) an opt or opt value or empty string - (let [to-complete (last input) - previous-token (second-to-last input)] - (if (and (is-gnu-option? previous-token) (not (bool-opt? previous-token (:spec cmd-tree)))) - ;; complete value - (possible-values previous-token) - (let [possible-commands (keys (:cmd cmd-tree)) - ;; don't suggest options which we already have parsed - possible-options (set/difference (set (keys (:spec cmd-tree))) (set (keys opts))) - ;; generate string representation of possible options - possible-completions (concat possible-commands - (map format-long-opt possible-options) - (keep (fn [option-name] - (when-let [alias (get-in cmd-tree [:spec option-name :alias])] - (format-short-opt alias))) - possible-options))] - (filter (partial true-prefix? to-complete) possible-completions)))))) + (let [opts (spec->opts (:spec cmd-tree)) + to-complete (last input) + previous-token (second-to-last input) + incomplete-option? (and (is-gnu-option? previous-token) (not (bool-opt? previous-token opts))) + possible-commands (if incomplete-option? [] (filter (partial true-prefix? to-complete) (keys (:cmd cmd-tree)))) + possible-options (complete-options opts input)] + (concat possible-commands possible-options)))) (filter (partial true-prefix? head) (possibilities cmd-tree)))))) (defn complete [cmd-table input] (complete-tree (table->tree cmd-table) input)) - (defn generate-completion-shell-snippet [type program-name] (case type :bash (format "_babashka_cli_dynamic_completion() @@ -694,7 +738,13 @@ complete --command %s --no-files --arguments \"(_babashka_cli_dynamic_completion :zsh (str "compadd" (when description (str " -x \"" description "\"")) " -- " completion) :fish completion)) -(defn print-completions [shell tree cmdline] +(defn print-opts-completions [shell opts cmdline] + (let [[_program-name & to-complete] (str/split (str/triml cmdline) #" +" -1) + completions (complete-options opts to-complete)] + (doseq [completion completions] + (println (format-completion shell {:completion completion}))))) + +(defn print-dispatch-completions [shell tree cmdline] (let [[_program-name & to-complete] (str/split (str/triml cmdline) #" +" -1) completions (complete-tree tree to-complete)] (doseq [completion completions] @@ -771,7 +821,7 @@ complete --command %s --no-files --arguments \"(_babashka_cli_dynamic_completion "--babashka.cli/completion-snippet" (print-completion-shell-snippet (keyword shell) command-name) "--babashka.cli/complete" - (print-completions (keyword shell) tree cmdline) + (print-dispatch-completions (keyword shell) tree cmdline) (let [{:as res :keys [cmd-info error available-commands]} (dispatch-tree' tree args opts) error-fn (or (:error-fn opts) diff --git a/test/babashka/cli/completion_test.clj b/test/babashka/cli/completion_test.clj index beafa86..9f091cc 100644 --- a/test/babashka/cli/completion_test.clj +++ b/test/babashka/cli/completion_test.clj @@ -1,5 +1,5 @@ (ns babashka.cli.completion-test - (:require [babashka.cli :as cli :refer [complete]] + (:require [babashka.cli :as cli :refer [complete-options complete]] [clojure.java.io :as io] [clojure.test :refer :all])) @@ -14,6 +14,37 @@ {:cmds ["bar"]} {:cmds ["bar-baz"]}]) +(def opts {:spec {:aopt {:alias :a + :coerce :string} + :aopt2 {:coerce :string + :validate #{"aval2"}} + :bflag {:alias :b + :coerce :boolean}}}) + +(deftest complete-options-test + (is (= #{"--aopt" "--aopt2" "--bflag" "-b" "-a"} (set (complete-options opts [""])))) + (is (= #{"--aopt" "--aopt2" "--bflag" "-b" "-a"} (set (complete-options opts ["-"])))) + (is (= #{"--aopt" "--aopt2" "--bflag"} (set (complete-options opts ["--"])))) + (is (= #{"--aopt" "--aopt2"} (set (complete-options opts ["--a"])))) + (is (= #{"--bflag"} (set (complete-options opts ["--b"])))) + (is (= #{} (set (complete-options opts ["--bflag"])))) + (is (= #{"--aopt" "--aopt2" "-a"} (set (complete-options opts ["--bflag" ""])))) + (is (= #{} (set (complete-options opts ["--aopt" ""])))) + (is (= #{} (set (complete-options opts ["--aopt" "aval"])))) + (is (= #{"--aopt2" "--bflag" "-b"} (set (complete-options opts ["--aopt" "aval" ""])))) + (is (= #{"--aopt" "--bflag" "-b" "-a"} (set (complete-options opts ["--aopt2" "aval2" ""])))) + (testing "failing options" + (is (= #{} (set (complete-options opts ["--aopt" "-"])))) + (is (= #{} (set (complete-options opts ["--aopt" "--bflag"])))) + ;;FIXME + #_(is (= #{} (set (complete-options opts ["--aopt" "--bflag" ""]))))) + (testing "invalid option value" + ;;FIXME + #_(is (= #{} (set (complete-options opts ["--aopt2" "invalid" ""]))))) + (testing "complete option with same prefix" + (is (= #{"--aopt" "--aopt2"} (set (complete-options opts ["--a"])))) + (is (= #{"--aopt2"} (set (complete-options opts ["--aopt"])))))) + (deftest completion-test (testing "complete commands" (is (= #{"foo" "bar" "bar-baz"} (set (complete cmd-table [""])))) From c12668aa5c31cfa297a59b5a76faea0270c7fd8a Mon Sep 17 00:00:00 2001 From: sohalt Date: Thu, 21 Mar 2024 13:44:53 +0100 Subject: [PATCH 3/6] remove test --- test/babashka/cli/completion_test.clj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/babashka/cli/completion_test.clj b/test/babashka/cli/completion_test.clj index 9f091cc..e7a51b7 100644 --- a/test/babashka/cli/completion_test.clj +++ b/test/babashka/cli/completion_test.clj @@ -112,10 +112,6 @@ (is (= #{"--bar-flag"} (set (complete cmd-table ["foo" "--foo-flag" "bar" "--bar-opt" "bar-val" ""])))))) -(deftest parse-opts-completion-test - (cli/parse-opts ["--babashka.cli/completion-snippet" "zsh"] {:complete true}) - (cli/parse-opts ["--babashka.cli/complete" "zsh" "foo"] {:complete true})) - (deftest dispatch-completion-test (is (= (slurp (io/resource "resources/completion/completion.zsh")) (with-out-str (cli/dispatch cmd-table ["--babashka.cli/completion-snippet" "zsh"] {:completion {:command "myprogram"}})))) (is (= (slurp (io/resource "resources/completion/completion.bash")) (with-out-str (cli/dispatch cmd-table ["--babashka.cli/completion-snippet" "bash"] {:completion {:command "myprogram"}})))) From 6c7d7f052d6cc4c04515966356284b5c56ecc3f6 Mon Sep 17 00:00:00 2001 From: sohalt Date: Thu, 21 Mar 2024 14:16:32 +0100 Subject: [PATCH 4/6] disable completion test on windows --- test/babashka/cli/completion_test.clj | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/test/babashka/cli/completion_test.clj b/test/babashka/cli/completion_test.clj index e7a51b7..7ef25cb 100644 --- a/test/babashka/cli/completion_test.clj +++ b/test/babashka/cli/completion_test.clj @@ -1,5 +1,6 @@ (ns babashka.cli.completion-test (:require [babashka.cli :as cli :refer [complete-options complete]] + [babashka.fs :as fs] [clojure.java.io :as io] [clojure.test :refer :all])) @@ -113,10 +114,11 @@ (deftest dispatch-completion-test - (is (= (slurp (io/resource "resources/completion/completion.zsh")) (with-out-str (cli/dispatch cmd-table ["--babashka.cli/completion-snippet" "zsh"] {:completion {:command "myprogram"}})))) - (is (= (slurp (io/resource "resources/completion/completion.bash")) (with-out-str (cli/dispatch cmd-table ["--babashka.cli/completion-snippet" "bash"] {:completion {:command "myprogram"}})))) - (is (= (slurp (io/resource "resources/completion/completion.fish")) (with-out-str (cli/dispatch cmd-table ["--babashka.cli/completion-snippet" "fish"] {:completion {:command "myprogram"}})))) - - (is (= "compadd -- foo\n" (with-out-str (cli/dispatch cmd-table ["--babashka.cli/complete" "zsh" "myprogram f"] {:completion {:command "myprogram"}})))) - (is (= "COMPREPLY+=( \"foo\" )\n" (with-out-str (cli/dispatch cmd-table ["--babashka.cli/complete" "bash" "myprogram f "] {:completion {:command "myprogram"}})))) - (is (= "foo\n" (with-out-str (cli/dispatch cmd-table ["--babashka.cli/complete" "fish" "myprogram f "] {:completion {:command "myprogram"}}))))) + (when-not (fs/windows?) + (is (= (slurp (io/resource "resources/completion/completion.zsh")) (with-out-str (cli/dispatch cmd-table ["--babashka.cli/completion-snippet" "zsh"] {:completion {:command "myprogram"}})))) ; + (is (= (slurp (io/resource "resources/completion/completion.bash")) (with-out-str (cli/dispatch cmd-table ["--babashka.cli/completion-snippet" "bash"] {:completion {:command "myprogram"}})))) + (is (= (slurp (io/resource "resources/completion/completion.fish")) (with-out-str (cli/dispatch cmd-table ["--babashka.cli/completion-snippet" "fish"] {:completion {:command "myprogram"}})))) + + (is (= "compadd -- foo\n" (with-out-str (cli/dispatch cmd-table ["--babashka.cli/complete" "zsh" "myprogram f"] {:completion {:command "myprogram"}})))) + (is (= "COMPREPLY+=( \"foo\" )\n" (with-out-str (cli/dispatch cmd-table ["--babashka.cli/complete" "bash" "myprogram f "] {:completion {:command "myprogram"}})))) + (is (= "foo\n" (with-out-str (cli/dispatch cmd-table ["--babashka.cli/complete" "fish" "myprogram f "] {:completion {:command "myprogram"}})))))) From 38f5c16b082913abfbdf4483adda314adb5375ab Mon Sep 17 00:00:00 2001 From: sohalt Date: Thu, 21 Mar 2024 15:04:41 +0100 Subject: [PATCH 5/6] fix spec --- test/babashka/cli/completion_test.clj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/babashka/cli/completion_test.clj b/test/babashka/cli/completion_test.clj index 7ef25cb..ccfce92 100644 --- a/test/babashka/cli/completion_test.clj +++ b/test/babashka/cli/completion_test.clj @@ -5,9 +5,9 @@ [clojure.test :refer :all])) (def cmd-table - [{:cmds ["foo"] :spec {:foo-opt {:coerece :string + [{:cmds ["foo"] :spec {:foo-opt {:coerce :string :alias :f} - :foo-opt2 {:coerece :string} + :foo-opt2 {:coerce :string} :foo-flag {:coerce :boolean :alias :l}}} {:cmds ["foo" "bar"] :spec {:bar-opt {:coerce :keyword} From 00e4a2a0d4007b3cf45fdcbc656a5ad7a311afe7 Mon Sep 17 00:00:00 2001 From: sohalt Date: Thu, 21 Mar 2024 17:31:16 +0100 Subject: [PATCH 6/6] Use opts under `org.babashka.cli` ns --- src/babashka/cli.cljc | 16 ++++++++-------- test/babashka/cli/completion_test.clj | 12 ++++++------ test/resources/completion/completion.bash | 2 +- test/resources/completion/completion.fish | 2 +- test/resources/completion/completion.zsh | 2 +- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index 0c79498..7c6bcf7 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -327,14 +327,14 @@ error-fn*)) [opt shell cmdline] args _ (case opt - "--babashka.cli/completion-snippet" + "--org.babashka.cli/completion-snippet" (if-let [command-name (get-in opts [:completion :command])] (do (print-completion-shell-snippet (keyword shell) command-name) (System/exit 0)) (binding [*out* *err*] - (println "Need `:completion {:command \"\"}` in opts to support shell completions") + (println "Need `:completion {:command \"\"}` in `opts` to support shell completions") (System/exit 1))) - "--babashka.cli/complete" + "--org.babashka.cli/complete" (do (print-opts-completions (keyword shell) opts cmdline) (System/exit 0)) :noop) @@ -715,16 +715,16 @@ (case type :bash (format "_babashka_cli_dynamic_completion() { - source <( \"$1\" --babashka.cli/complete \"bash\" \"${COMP_WORDS[*]// / }\" ) + source <( \"$1\" --org.babashka.cli/complete \"bash\" \"${COMP_WORDS[*]// / }\" ) } complete -o nosort -F _babashka_cli_dynamic_completion %s " program-name) :zsh (format "#compdef %s -source <( \"${words[1]}\" --babashka.cli/complete \"zsh\" \"${words[*]// / }\" ) +source <( \"${words[1]}\" --org.babashka.cli/complete \"zsh\" \"${words[*]// / }\" ) " program-name) :fish (format "function _babashka_cli_dynamic_completion set --local COMP_LINE (commandline --cut-at-cursor) - %s --babashka.cli/complete fish $COMP_LINE + %s --org.babashka.cli/complete fish $COMP_LINE end complete --command %s --no-files --arguments \"(_babashka_cli_dynamic_completion)\" " program-name program-name))) @@ -818,9 +818,9 @@ complete --command %s --no-files --arguments \"(_babashka_cli_dynamic_completion (let [command-name (get-in opts [:completion :command]) [opt shell cmdline] args] (case opt - "--babashka.cli/completion-snippet" + "--org.babashka.cli/completion-snippet" (print-completion-shell-snippet (keyword shell) command-name) - "--babashka.cli/complete" + "--org.babashka.cli/complete" (print-dispatch-completions (keyword shell) tree cmdline) (let [{:as res :keys [cmd-info error available-commands]} (dispatch-tree' tree args opts) diff --git a/test/babashka/cli/completion_test.clj b/test/babashka/cli/completion_test.clj index ccfce92..d87afbc 100644 --- a/test/babashka/cli/completion_test.clj +++ b/test/babashka/cli/completion_test.clj @@ -115,10 +115,10 @@ (deftest dispatch-completion-test (when-not (fs/windows?) - (is (= (slurp (io/resource "resources/completion/completion.zsh")) (with-out-str (cli/dispatch cmd-table ["--babashka.cli/completion-snippet" "zsh"] {:completion {:command "myprogram"}})))) ; - (is (= (slurp (io/resource "resources/completion/completion.bash")) (with-out-str (cli/dispatch cmd-table ["--babashka.cli/completion-snippet" "bash"] {:completion {:command "myprogram"}})))) - (is (= (slurp (io/resource "resources/completion/completion.fish")) (with-out-str (cli/dispatch cmd-table ["--babashka.cli/completion-snippet" "fish"] {:completion {:command "myprogram"}})))) + (is (= (slurp (io/resource "resources/completion/completion.zsh")) (with-out-str (cli/dispatch cmd-table ["--org.babashka.cli/completion-snippet" "zsh"] {:completion {:command "myprogram"}})))) ; + (is (= (slurp (io/resource "resources/completion/completion.bash")) (with-out-str (cli/dispatch cmd-table ["--org.babashka.cli/completion-snippet" "bash"] {:completion {:command "myprogram"}})))) + (is (= (slurp (io/resource "resources/completion/completion.fish")) (with-out-str (cli/dispatch cmd-table ["--org.babashka.cli/completion-snippet" "fish"] {:completion {:command "myprogram"}})))) - (is (= "compadd -- foo\n" (with-out-str (cli/dispatch cmd-table ["--babashka.cli/complete" "zsh" "myprogram f"] {:completion {:command "myprogram"}})))) - (is (= "COMPREPLY+=( \"foo\" )\n" (with-out-str (cli/dispatch cmd-table ["--babashka.cli/complete" "bash" "myprogram f "] {:completion {:command "myprogram"}})))) - (is (= "foo\n" (with-out-str (cli/dispatch cmd-table ["--babashka.cli/complete" "fish" "myprogram f "] {:completion {:command "myprogram"}})))))) + (is (= "compadd -- foo\n" (with-out-str (cli/dispatch cmd-table ["--org.babashka.cli/complete" "zsh" "myprogram f"] {:completion {:command "myprogram"}})))) + (is (= "COMPREPLY+=( \"foo\" )\n" (with-out-str (cli/dispatch cmd-table ["--org.babashka.cli/complete" "bash" "myprogram f "] {:completion {:command "myprogram"}})))) + (is (= "foo\n" (with-out-str (cli/dispatch cmd-table ["--org.babashka.cli/complete" "fish" "myprogram f "] {:completion {:command "myprogram"}})))))) diff --git a/test/resources/completion/completion.bash b/test/resources/completion/completion.bash index 8f47215..eb7bf46 100644 --- a/test/resources/completion/completion.bash +++ b/test/resources/completion/completion.bash @@ -1,5 +1,5 @@ _babashka_cli_dynamic_completion() { - source <( "$1" --babashka.cli/complete "bash" "${COMP_WORDS[*]// / }" ) + source <( "$1" --org.babashka.cli/complete "bash" "${COMP_WORDS[*]// / }" ) } complete -o nosort -F _babashka_cli_dynamic_completion myprogram diff --git a/test/resources/completion/completion.fish b/test/resources/completion/completion.fish index 17ab9f6..2562885 100644 --- a/test/resources/completion/completion.fish +++ b/test/resources/completion/completion.fish @@ -1,5 +1,5 @@ function _babashka_cli_dynamic_completion set --local COMP_LINE (commandline --cut-at-cursor) - myprogram --babashka.cli/complete fish $COMP_LINE + myprogram --org.babashka.cli/complete fish $COMP_LINE end complete --command myprogram --no-files --arguments "(_babashka_cli_dynamic_completion)" diff --git a/test/resources/completion/completion.zsh b/test/resources/completion/completion.zsh index 4e51479..0e1e013 100644 --- a/test/resources/completion/completion.zsh +++ b/test/resources/completion/completion.zsh @@ -1,2 +1,2 @@ #compdef myprogram -source <( "${words[1]}" --babashka.cli/complete "zsh" "${words[*]// / }" ) +source <( "${words[1]}" --org.babashka.cli/complete "zsh" "${words[*]// / }" )