From 2d694c7e68214eabbe4cb56c87de612ec1e633b2 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Wed, 24 Jun 2026 21:45:23 +0200 Subject: [PATCH 1/8] wip: squint support --- .github/workflows/ci.yml | 16 ++++++ .gitignore | 2 + CHANGELOG.md | 4 ++ bb.edn | 5 ++ squint.edn | 3 ++ src/babashka/cli.cljc | 52 ++++++++++++------ test-squint/babashka/cli_squint_test.cljs | 65 +++++++++++++++++++++++ 7 files changed, 132 insertions(+), 15 deletions(-) create mode 100644 squint.edn create mode 100644 test-squint/babashka/cli_squint_test.cljs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f9ba4b..ad35baa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,6 +107,22 @@ jobs: - name: nushell completions run: nu script/completion-smoke.nu + test-squint: + runs-on: ubuntu-latest + + steps: + - name: "Checkout code" + uses: "actions/checkout@v7" + + - name: Setup Clojure + uses: DeLaGuardo/setup-clojure@13.6.1 + with: + bb: 1.12.218 + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Run tests + run: bb squint-test + test-cljd: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 18fa183..dc3e25a 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ README.html pubspec.lock lib/cljd-out test/cljd-out +node_modules +.squint-out diff --git a/CHANGELOG.md b/CHANGELOG.md index 06a2185..5e538b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ For breaking changes, check [here](#breaking-changes). [Babashka CLI](https://github.com/babashka/cli): turn Clojure functions into CLIs! +## Unreleased + + + ## v0.11.74 (2026-06-23) - [#180](https://github.com/babashka/cli/issues/180): ClojureDart support diff --git a/bb.edn b/bb.edn index 12f094b..89dd08d 100644 --- a/bb.edn +++ b/bb.edn @@ -20,6 +20,11 @@ cljs-test {:doc "Run CLJS tests" :task (apply clojure "-M:test:cljs-test" *command-line-args*)} + squint-test {:doc "Run squint tests" + :task (do (shell "npm" "install" "--no-save" "--prefix" "." "squint-cljs") + (shell "node_modules/.bin/squint" "compile") + (shell "node" ".squint-out/babashka/cli_squint_test.mjs"))} + cljd-test {:doc "Run ClojureDart tests" :task (do (shell "dart pub get") (apply clojure "-M:cljd" "test" diff --git a/squint.edn b/squint.edn new file mode 100644 index 0000000..c6a4a91 --- /dev/null +++ b/squint.edn @@ -0,0 +1,3 @@ +{:paths ["src" "test-squint"] + :output-dir ".squint-out" + :extension "mjs"} diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index b17ac24..ce60601 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -1,9 +1,10 @@ (ns babashka.cli (:refer-clojure :exclude [parse-boolean parse-long parse-double]) (:require - #?(:clj [clojure.edn :as edn] - :cljd [cljd.edn :as edn] - :cljs [cljs.reader :as edn]) + #?@(:squint [] + :clj [[clojure.edn :as edn]] + :cljd [[cljd.edn :as edn]] + :cljs [[cljs.reader :as edn]]) #?@(:cljd [["dart:io" :as io]]) [babashka.cli.internal :as internal] [clojure.string :as str]) @@ -11,6 +12,15 @@ #?(:clj (set! *warn-on-reflection* true)) +;; squint has no keyword type (keywords are plain strings) so it cannot tell an +;; injected option keyword apart from a positional string argument. Wrap injected +;; opts in this marker instead. +#?(:squint (deftype Injected [opt])) + +;; squint core has no vary-meta. +#?(:squint (defn- vary-meta [obj f & args] + (with-meta obj (apply f (meta obj) args)))) + (defn merge-opts "Merges babashka CLI options." [m & ms] @@ -38,6 +48,10 @@ (defn- parse-double [x] #?(:clj (Double/parseDouble x) :cljd (or (dart:core/double.tryParse x) (throw-unexpected x)) + :squint (let [v (js/JSON.parse x)] + (if (and (number? v) (not (int? v))) + v + (throw-unexpected x))) :cljs (let [v (js/JSON.parse x)] (if (double? v) v @@ -144,10 +158,10 @@ (:int :long) parse-long :double parse-double :number parse-number - :symbol symbol + :symbol #?(:squint identity :default symbol) :keyword parse-keyword :string identity - :edn edn/read-string + :edn #?(:squint auto-coerce :default edn/read-string) :auto auto-coerce ;; default f) @@ -237,7 +251,9 @@ (let [arg-count (count args) cnt (min arg-count (bounded-count arg-count args->opt-keys))] - [(concat (interleave args->opt-keys args) + [(concat (interleave #?(:squint (map ->Injected args->opt-keys) + :default args->opt-keys) + args) (drop cnt args)) (drop cnt args->opt-keys)]) [args args->opt-keys]) @@ -290,7 +306,8 @@ ::resolved true)))) (defn- kw->str [kw] - (subs (str kw) 1)) + #?(:squint (str kw) + :default (subs (str kw) 1))) (defn- option-label "User-facing name for option `opt` in an error message: the literal flag the @@ -557,7 +574,9 @@ ;; exit loop: no command line args left [acc open-opt valued-opt implicit-true-keys opt-parse-order] (let [raw-arg (first args) - opt-injected? (keyword? raw-arg)] + opt-injected? #?(:squint (instance? Injected raw-arg) + :default (keyword? raw-arg)) + #?@(:squint [raw-arg (if opt-injected? (.-opt raw-arg) raw-arg)])] (if opt-injected? ;; continue loop: this opt and its value was injected by args->opts ;; opt-val-collector does not apply for injected opts, so is `nil` @@ -607,7 +626,7 @@ next-arg (first next-args) next-arg-info (analyze-arg next-arg mode open-opt boolean-opt? valued-opt known-keys alias-keys) negated-opt? (when-not (contains? known-keys parsed-opt) - (str/starts-with? (str parsed-opt) ":no-"))] + (str/starts-with? (str parsed-opt) #?(:squint "no-" :default ":no-")))] (if (or (:hyphen-opt next-arg-info) ;; --open-opt --next (empty? next-args) ;; --open-opt negated-opt?) ;; --no-foo @@ -621,7 +640,7 @@ mode (concat expanded next-args) a->o implicit-true-keys opt-parse-order)) (let [parsed-opt (if negated-opt? - (keyword (str/replace (str parsed-opt) ":no-" "")) + (keyword (str/replace (str parsed-opt) #?(:squint "no-" :default ":no-") "")) parsed-opt)] ;; continue loop: adding true for --foo or false for --no-foo to args (recur (stamp (maybe-close-open-opt acc open-opt valued-opt opt-val-collector) parsed-opt literal-opt) @@ -936,7 +955,7 @@ (if desc desc ""))])) (if (map? spec) (let [order (or order (keys spec))] - (map (fn [k] [k (spec k)]) order)) + (map (fn [k] [k (get spec k)]) order)) spec)))) (defn- opts->help-rows @@ -948,7 +967,7 @@ `format-opts`." [{:keys [spec order required]}] (let [entries (if (map? spec) - (map (fn [k] [k (spec k)]) (or order (keys spec))) + (map (fn [k] [k (get spec k)]) (or order (keys spec))) spec) ;; `:no-doc` options still parse but are hidden from help, like `:no-doc` ;; commands are hidden from the command list @@ -1176,8 +1195,8 @@ (defn- deep-merge [a b] (reduce (fn [acc k] (update acc k (fn [v] (if (map? v) - (deep-merge v (b k)) - (b k))))) + (deep-merge v (get b k)) + (get b k))))) a (keys b))) (defn- inherited-entries @@ -1579,7 +1598,10 @@ $env.config.completions.external.completer = {|spans| (println (if desc (str value \tab desc) value))))) (defn- eprintln [s] - #?(:cljs (binding [*print-fn* *print-err-fn*] (println s)) + #?(:squint (if (and (exists? js/process) js/process.stderr) + (.write js/process.stderr (str s "\n")) + (println s)) + :cljs (binding [*print-fn* *print-err-fn*] (println s)) :default (binding [*out* *err*] (println s)))) (defn- has-parse-opts? [m] diff --git a/test-squint/babashka/cli_squint_test.cljs b/test-squint/babashka/cli_squint_test.cljs new file mode 100644 index 0000000..a24b0b2 --- /dev/null +++ b/test-squint/babashka/cli_squint_test.cljs @@ -0,0 +1,65 @@ +(ns babashka.cli-squint-test + "Squint smoke test for babashka.cli. Squint has no keyword type, so literal + :foo keys compile to plain strings and match the string keys parse-opts + returns. Runs as a script: compile with squint, then node the output, which + exits non-zero on failure." + (:require + [babashka.cli :as cli] + [clojure.string :as str] + [clojure.test :as t :refer [deftest is testing]])) + +(deftest parse-opts-test + (testing "default number coercion" + (is (= {:foo 1 :bar true} (cli/parse-opts ["--foo" "1" "--bar"])))) + (testing "string stays string" + (is (= {:foo "abc"} (cli/parse-opts ["--foo" "abc"])))) + (testing "explicit coerce" + (is (= {:x 1 :y 1.5} (cli/parse-opts ["--x" "1" "--y" "1.5"] {:coerce {:x :int :y :double}}))) + (is (= {:k "foo"} (cli/parse-opts ["--k" ":foo"] {:coerce {:k :keyword}})))) + (testing "negation" + (is (= {:foo false} (cli/parse-opts ["--no-foo"] {:coerce {:foo :boolean}})))) + (testing "alias" + (is (= {:foo "bar"} (cli/parse-opts ["-f" "bar"] {:alias {:f :foo}})))) + (testing "collect" + (is (= {:e ["a" "b"]} (cli/parse-opts ["--e" "a" "--e" "b"] {:collect {:e []}}))))) + +(deftest keyword-args-test + (is (= {:foo "bar" :baz "quux"} (cli/parse-opts [":foo" "bar" ":baz" "quux"])))) + +(deftest auto-coerce-test + (is (= {:foo :bar} (cli/parse-opts ["--foo" ":bar"] {:coerce {:foo :auto}}))) + (is (= {:n 42} (cli/parse-opts ["--n" "42"] {:coerce {:n :auto}})))) + +(deftest parse-args-test + (is (= {:opts {:foo 1} :args ["x" "y"]} + (select-keys (cli/parse-args ["--foo" "1" "x" "y"]) [:opts :args]))) + (testing "args->opts injection" + (is (= {:a 1 :b 2} + (:opts (cli/parse-args ["1" "2"] {:args->opts [:a :b] :coerce {:a :int :b :int}})))))) + +(deftest dispatch-test + (let [table [{:cmds ["add"] :fn (fn [m] [:add (:opts m)])} + {:cmds ["sub"] :fn (fn [m] [:sub (:opts m)])} + {:cmds [] :fn (fn [_] :root)}]] + (is (= [:add {:x 1}] (cli/dispatch table ["add" "--x" "1"] {:coerce {:x :int}}))) + (is (= [:sub {}] (cli/dispatch table ["sub"]))) + (is (= :root (cli/dispatch table []))))) + +(deftest format-opts-test + (let [s (cli/format-opts {:spec {:foo {:desc "the foo" :alias :f :ref ""} + :verbose {:desc "be loud" :coerce :boolean}}})] + (is (string? s)) + (is (str/includes? s "--foo")) + (is (str/includes? s "the foo")) + (is (str/includes? s "-f,")))) + +(deftest coerce-error-test + (is (thrown-with-msg? js/Error #"cannot transform input \"abc\" to int" + (cli/parse-opts ["--port" "abc"] {:coerce {:port :int}})))) + +(defn -main [& _] + (let [{:keys [fail error]} (t/run-tests)] + (when (pos? (+ (or fail 0) (or error 0))) + (js/process.exit 1)))) + +(-main) From 0c357194ac782bbf0ddd53ca7f17228db6f51596 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Wed, 24 Jun 2026 23:04:48 +0200 Subject: [PATCH 2/8] Use core vary-meta under squint --- package-lock.json | 60 +++++++++++++++++++++++++++++++++++++++++++ package.json | 5 ++++ src/babashka/cli.cljc | 4 --- 3 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 package-lock.json create mode 100644 package.json diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b7ada42 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,60 @@ +{ + "name": "cli", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "squint-cljs": "^0.13.195" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/squint-cljs": { + "version": "0.13.195", + "resolved": "https://registry.npmjs.org/squint-cljs/-/squint-cljs-0.13.195.tgz", + "integrity": "sha512-W8TeNy58jMta0v5SyDwPI58y1IQ9Q3OG3gEZbGa+pqRrmkZEVJ5J4K2OovRJO33tQ5sJVqwjg/lyQcjedL7hvQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/borkdude" + } + ], + "dependencies": { + "chokidar": "^4.0.3" + }, + "bin": { + "squint": "node_cli.js" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..22d08ff --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "squint-cljs": "^0.13.195" + } +} diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index ce60601..2cdd191 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -17,10 +17,6 @@ ;; opts in this marker instead. #?(:squint (deftype Injected [opt])) -;; squint core has no vary-meta. -#?(:squint (defn- vary-meta [obj f & args] - (with-meta obj (apply f (meta obj) args)))) - (defn merge-opts "Merges babashka CLI options." [m & ms] From 054327b336fb9f304209bb0025a21808f8f749b5 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Wed, 24 Jun 2026 23:12:12 +0200 Subject: [PATCH 3/8] Use core double? under squint --- src/babashka/cli.cljc | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index 2cdd191..42544b7 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -44,10 +44,6 @@ (defn- parse-double [x] #?(:clj (Double/parseDouble x) :cljd (or (dart:core/double.tryParse x) (throw-unexpected x)) - :squint (let [v (js/JSON.parse x)] - (if (and (number? v) (not (int? v))) - v - (throw-unexpected x))) :cljs (let [v (js/JSON.parse x)] (if (double? v) v From 6848c5ab68591562fabb93187e975acd6801fdd8 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Thu, 25 Jun 2026 08:28:21 +0200 Subject: [PATCH 4/8] remove difference --- src/babashka/cli.cljc | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index 42544b7..249cc6a 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -1590,10 +1590,7 @@ $env.config.completions.external.completer = {|spans| (println (if desc (str value \tab desc) value))))) (defn- eprintln [s] - #?(:squint (if (and (exists? js/process) js/process.stderr) - (.write js/process.stderr (str s "\n")) - (println s)) - :cljs (binding [*print-fn* *print-err-fn*] (println s)) + #?(:cljs (binding [*print-fn* *print-err-fn*] (println s)) :default (binding [*out* *err*] (println s)))) (defn- has-parse-opts? [m] From eb37b994e06fa34b12af6de5f88a52c7c9e56a75 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Thu, 25 Jun 2026 10:48:47 +0200 Subject: [PATCH 5/8] wip --- src/babashka/cli.cljc | 15 ++++++--------- test-squint/babashka/cli_squint_test.cljs | 3 +++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index 249cc6a..d54e183 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -1,10 +1,9 @@ (ns babashka.cli (:refer-clojure :exclude [parse-boolean parse-long parse-double]) (:require - #?@(:squint [] - :clj [[clojure.edn :as edn]] - :cljd [[cljd.edn :as edn]] - :cljs [[cljs.reader :as edn]]) + #?(:clj [clojure.edn :as edn] + :cljd [cljd.edn :as edn] + :cljs [cljs.reader :as edn]) #?@(:cljd [["dart:io" :as io]]) [babashka.cli.internal :as internal] [clojure.string :as str]) @@ -12,9 +11,7 @@ #?(:clj (set! *warn-on-reflection* true)) -;; squint has no keyword type (keywords are plain strings) so it cannot tell an -;; injected option keyword apart from a positional string argument. Wrap injected -;; opts in this marker instead. +;; squint can't tell an injected keyword from a string arg, so wrap injected opts #?(:squint (deftype Injected [opt])) (defn merge-opts @@ -150,10 +147,10 @@ (:int :long) parse-long :double parse-double :number parse-number - :symbol #?(:squint identity :default symbol) + :symbol symbol :keyword parse-keyword :string identity - :edn #?(:squint auto-coerce :default edn/read-string) + :edn edn/read-string :auto auto-coerce ;; default f) diff --git a/test-squint/babashka/cli_squint_test.cljs b/test-squint/babashka/cli_squint_test.cljs index a24b0b2..46d5aae 100644 --- a/test-squint/babashka/cli_squint_test.cljs +++ b/test-squint/babashka/cli_squint_test.cljs @@ -30,6 +30,9 @@ (is (= {:foo :bar} (cli/parse-opts ["--foo" ":bar"] {:coerce {:foo :auto}}))) (is (= {:n 42} (cli/parse-opts ["--n" "42"] {:coerce {:n :auto}})))) +(deftest edn-coerce-test + (is (= {:m {:a 1 :b [1 2]}} (cli/parse-opts ["--m" "{:a 1 :b [1 2]}"] {:coerce {:m :edn}})))) + (deftest parse-args-test (is (= {:opts {:foo 1} :args ["x" "y"]} (select-keys (cli/parse-args ["--foo" "1" "x" "y"]) [:opts :args]))) From bd8a506b8168e1caee8cc1c307d4fe060c8f5f06 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Thu, 25 Jun 2026 12:59:42 +0200 Subject: [PATCH 6/8] test runner --- bb.edn | 2 +- squint.edn | 4 +- src/babashka/cli.cljc | 6 +- test-squint/babashka/cli_squint_test.cljs | 68 ----------------------- test/babashka/cli/completion_test.cljc | 20 +++---- test/babashka/cli_test.cljc | 11 ++-- test/babashka/run_tests.cljs | 8 +++ 7 files changed, 31 insertions(+), 88 deletions(-) delete mode 100644 test-squint/babashka/cli_squint_test.cljs create mode 100644 test/babashka/run_tests.cljs diff --git a/bb.edn b/bb.edn index 89dd08d..2cd5efe 100644 --- a/bb.edn +++ b/bb.edn @@ -23,7 +23,7 @@ squint-test {:doc "Run squint tests" :task (do (shell "npm" "install" "--no-save" "--prefix" "." "squint-cljs") (shell "node_modules/.bin/squint" "compile") - (shell "node" ".squint-out/babashka/cli_squint_test.mjs"))} + (shell "node" ".squint-out/babashka/run_tests.mjs"))} cljd-test {:doc "Run ClojureDart tests" :task (do (shell "dart pub get") diff --git a/squint.edn b/squint.edn index c6a4a91..d45975a 100644 --- a/squint.edn +++ b/squint.edn @@ -1,3 +1,5 @@ -{:paths ["src" "test-squint"] +{:paths ["src" "test"] + :deps {io.github.borkdude/deflet {:git/sha "a3c86cdbd23531b2d59e6d7db1c49418cb325c7f" + :git/tag "v0.1.0"}} :output-dir ".squint-out" :extension "mjs"} diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index d54e183..81e95f6 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -462,7 +462,7 @@ (:pred vf)) vf)] (when-let [[_ v] (find m k)] - (when-not (f v) + (when-not (if (set? f) (contains? f v) (f v)) (let [ex-msg-fn (or (:ex-msg vf) (fn [{:keys [flag value]}] (str "Invalid value for option " flag ": " value))) @@ -1021,7 +1021,7 @@ (= k prev) (conj (pop acc) (str "<" (name prev) ">...")) :else (recur (next s) (conj acc (str "<" (name k) ">")) k (inc n))))))) -(defn #?(:cljd ^:no-doc cmd-children :default ^:private cmd-children) +(defn #?(:cljd ^:no-doc cmd-children :squint ^:no-doc cmd-children :default ^:private cmd-children) "Visible `[name child]` pairs of `node`'s commands, for display (help, completions, error suggestions): `:no-doc` children are dropped. An explicit node `:cmd-order` (vector of names) selects which children are shown and in @@ -1378,7 +1378,7 @@ (str/split token #"=" 2) [token])) -(defn #?(:cljd ^:no-doc complete-tree* :default ^:private complete-tree*) +(defn #?(:cljd ^:no-doc complete-tree* :squint ^:no-doc complete-tree* :default ^:private complete-tree*) "Returns completion candidate maps (`{:value :description}`) for dispatch tree `cmd-tree` and `args` (a vector of tokens, last = the token being completed). `global-opts` are the dispatch-level opts, accepted at every level like diff --git a/test-squint/babashka/cli_squint_test.cljs b/test-squint/babashka/cli_squint_test.cljs deleted file mode 100644 index 46d5aae..0000000 --- a/test-squint/babashka/cli_squint_test.cljs +++ /dev/null @@ -1,68 +0,0 @@ -(ns babashka.cli-squint-test - "Squint smoke test for babashka.cli. Squint has no keyword type, so literal - :foo keys compile to plain strings and match the string keys parse-opts - returns. Runs as a script: compile with squint, then node the output, which - exits non-zero on failure." - (:require - [babashka.cli :as cli] - [clojure.string :as str] - [clojure.test :as t :refer [deftest is testing]])) - -(deftest parse-opts-test - (testing "default number coercion" - (is (= {:foo 1 :bar true} (cli/parse-opts ["--foo" "1" "--bar"])))) - (testing "string stays string" - (is (= {:foo "abc"} (cli/parse-opts ["--foo" "abc"])))) - (testing "explicit coerce" - (is (= {:x 1 :y 1.5} (cli/parse-opts ["--x" "1" "--y" "1.5"] {:coerce {:x :int :y :double}}))) - (is (= {:k "foo"} (cli/parse-opts ["--k" ":foo"] {:coerce {:k :keyword}})))) - (testing "negation" - (is (= {:foo false} (cli/parse-opts ["--no-foo"] {:coerce {:foo :boolean}})))) - (testing "alias" - (is (= {:foo "bar"} (cli/parse-opts ["-f" "bar"] {:alias {:f :foo}})))) - (testing "collect" - (is (= {:e ["a" "b"]} (cli/parse-opts ["--e" "a" "--e" "b"] {:collect {:e []}}))))) - -(deftest keyword-args-test - (is (= {:foo "bar" :baz "quux"} (cli/parse-opts [":foo" "bar" ":baz" "quux"])))) - -(deftest auto-coerce-test - (is (= {:foo :bar} (cli/parse-opts ["--foo" ":bar"] {:coerce {:foo :auto}}))) - (is (= {:n 42} (cli/parse-opts ["--n" "42"] {:coerce {:n :auto}})))) - -(deftest edn-coerce-test - (is (= {:m {:a 1 :b [1 2]}} (cli/parse-opts ["--m" "{:a 1 :b [1 2]}"] {:coerce {:m :edn}})))) - -(deftest parse-args-test - (is (= {:opts {:foo 1} :args ["x" "y"]} - (select-keys (cli/parse-args ["--foo" "1" "x" "y"]) [:opts :args]))) - (testing "args->opts injection" - (is (= {:a 1 :b 2} - (:opts (cli/parse-args ["1" "2"] {:args->opts [:a :b] :coerce {:a :int :b :int}})))))) - -(deftest dispatch-test - (let [table [{:cmds ["add"] :fn (fn [m] [:add (:opts m)])} - {:cmds ["sub"] :fn (fn [m] [:sub (:opts m)])} - {:cmds [] :fn (fn [_] :root)}]] - (is (= [:add {:x 1}] (cli/dispatch table ["add" "--x" "1"] {:coerce {:x :int}}))) - (is (= [:sub {}] (cli/dispatch table ["sub"]))) - (is (= :root (cli/dispatch table []))))) - -(deftest format-opts-test - (let [s (cli/format-opts {:spec {:foo {:desc "the foo" :alias :f :ref ""} - :verbose {:desc "be loud" :coerce :boolean}}})] - (is (string? s)) - (is (str/includes? s "--foo")) - (is (str/includes? s "the foo")) - (is (str/includes? s "-f,")))) - -(deftest coerce-error-test - (is (thrown-with-msg? js/Error #"cannot transform input \"abc\" to int" - (cli/parse-opts ["--port" "abc"] {:coerce {:port :int}})))) - -(defn -main [& _] - (let [{:keys [fail error]} (t/run-tests)] - (when (pos? (+ (or fail 0) (or error 0))) - (js/process.exit 1)))) - -(-main) diff --git a/test/babashka/cli/completion_test.cljc b/test/babashka/cli/completion_test.cljc index b717c9c..d02e30e 100644 --- a/test/babashka/cli/completion_test.cljc +++ b/test/babashka/cli/completion_test.cljc @@ -5,6 +5,7 @@ #?(:cljd [cljd.test :refer [deftest is testing]] :default [clojure.test :refer [deftest is testing]]) #?@(:cljd [["dart:io" :as io]] + :cljs [["fs" :as fs]] :clj [[babashka.fs :as fs] [clojure.java.io :as io]]))) @@ -16,8 +17,7 @@ (defn- read-snippet [shell] #?(:cljd (.readAsStringSync (io/File. (str "test/resources/completion/completion." shell))) :clj (slurp (io/resource (str "resources/completion/completion." shell))) - :cljs (.readFileSync (js/require "fs") - (str "test/resources/completion/completion." shell) "utf8"))) + :cljs (fs/readFileSync (str "test/resources/completion/completion." shell) "utf8"))) ;; clojure.string/split-lines drops trailing empty lines. cljd keeps them (defn- lines* [s] @@ -27,12 +27,12 @@ ;; test helpers over the private completion fns: return the candidate value strings (defn complete [table args] - (mapv :value (#?(:cljd cli/complete-tree* :default #'cli/complete-tree*) (cli/table->tree table) args))) + (mapv :value (#?(:cljd cli/complete-tree* :squint cli/complete-tree* :default #'cli/complete-tree*) (cli/table->tree table) args))) (defn complete-options [opts args] - (mapv :value (#?(:cljd cli/complete-tree* :default #'cli/complete-tree*) (cli/table->tree [(assoc opts :cmds [])]) args))) + (mapv :value (#?(:cljd cli/complete-tree* :squint cli/complete-tree* :default #'cli/complete-tree*) (cli/table->tree [(assoc opts :cmds [])]) args))) ;; an unconfigured value position defaults to the shell's file completion (defn- files? [table args] - (= [{:file-completion true}] (#?(:cljd cli/complete-tree* :default #'cli/complete-tree*) (cli/table->tree table) args))) + (= [{:file-completion true}] (#?(:cljd cli/complete-tree* :squint cli/complete-tree* :default #'cli/complete-tree*) (cli/table->tree table) args))) (defn- files-options? [opts args] (files? [(assoc opts :cmds [])] args)) @@ -240,7 +240,7 @@ (testing "no :complete/:complete-fn/:validate -> the shell's file completion" (is (files-options? {:spec {:env {:coerce :string}}} ["--env" ""]))) (testing ":complete false opts out of the file default" - (is (empty? (#?(:cljd cli/complete-tree* :default #'cli/complete-tree*) + (is (empty? (#?(:cljd cli/complete-tree* :squint cli/complete-tree* :default #'cli/complete-tree*) (cli/table->tree [{:cmds [] :spec {:msg {:complete false}}}]) ["--msg" ""]))))) @@ -352,7 +352,7 @@ marker))))) (testing ":complete false opts a positional out of the file default" (let [v [{:cmds ["run"] :args->opts [:name] :spec {:name {:complete false}}}]] - (is (not-any? :file-completion (#?(:cljd cli/complete-tree* :default #'cli/complete-tree*) (cli/table->tree v) ["run" ""]))))) + (is (not-any? :file-completion (#?(:cljd cli/complete-tree* :squint cli/complete-tree* :default #'cli/complete-tree*) (cli/table->tree v) ["run" ""]))))) (testing "variadic :args->opts does not hang and emits the marker" (let [v [{:cmds ["x"] :args->opts (cons :a (repeat :b))}]] (is (contains? (set (lines* (complete-via-cmd v {:prog "p"} "p x one two "))) @@ -400,7 +400,7 @@ (is (= #{"--force"} (set (complete t ["deploy" "--env=dev" ""]))))) (testing "no file fallback inside --opt= (shells match files against the whole token)" (let [u [{:cmds ["deploy"] :spec {:out {:coerce :string}}}]] - (is (empty? (#?(:cljd cli/complete-tree* :default #'cli/complete-tree*) (cli/table->tree u) ["deploy" "--out="]))))) + (is (empty? (#?(:cljd cli/complete-tree* :squint cli/complete-tree* :default #'cli/complete-tree*) (cli/table->tree u) ["deploy" "--out="]))))) (testing "bash wordbreak splitting (--opt = val arrives as three tokens)" (let [out (fn [& toks] (with-out-str @@ -446,7 +446,7 @@ (is (= #{"--opt"} (set (complete t ["sub" "--verbose" "--"])))))) (testing "dispatch-level :inherit true" (is (= ["--env"] - (mapv :value (#?(:cljd cli/complete-tree* :default #'cli/complete-tree*) + (mapv :value (#?(:cljd cli/complete-tree* :squint cli/complete-tree* :default #'cli/complete-tree*) (cli/table->tree [{:cmds [] :spec {:env {}}} {:cmds ["sub"] :fn identity}]) ["sub" "--"] @@ -466,7 +466,7 @@ (deftest dispatch-level-spec-test ;; dispatch-level :spec options parse at every level, so they complete there too (is (= #{"--local" "--glob"} - (set (mapv :value (#?(:cljd cli/complete-tree* :default #'cli/complete-tree*) + (set (mapv :value (#?(:cljd cli/complete-tree* :squint cli/complete-tree* :default #'cli/complete-tree*) (cli/table->tree [{:cmds ["sub"] :fn identity :spec {:local {}}}]) ["sub" "--"] {:spec {:glob {}}})))))) diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index 05280a2..6827295 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -1,7 +1,7 @@ (ns babashka.cli-test (:require [babashka.cli :as cli] - #?@(:cljd [] :default [[babashka.cli.test-report]]) + #?@(:cljd [] :squint [] :default [[babashka.cli.test-report]]) [borkdude.deflet :as d] [clojure.string :as str] #?(:cljd [cljd.test :refer [deftest is testing]] @@ -502,13 +502,13 @@ siblings, in table order" (let [tree (cli/table->tree [{:cmds ["a"] :fn identity :doc "A"} {:cmds [] :cmd {"b" {:fn identity :doc "B"}}}])] - (is (= ["a" "b"] (mapv first (#?(:cljd cli/cmd-children :default #'cli/cmd-children) tree)))) + (is (= ["a" "b"] (mapv first (#?(:cljd cli/cmd-children :squint cli/cmd-children :default #'cli/cmd-children) tree)))) (is (submap? {:dispatch ["a"]} (cli/dispatch tree ["a"]))) (is (submap? {:dispatch ["b"]} (cli/dispatch tree ["b"])))) (testing "catch-all first: its children list first" (let [tree (cli/table->tree [{:cmds [] :cmd {"b" {:fn identity :doc "B"}}} {:cmds ["a"] :fn identity :doc "A"}])] - (is (= ["b" "a"] (mapv first (#?(:cljd cli/cmd-children :default #'cli/cmd-children) tree)))))))) + (is (= ["b" "a"] (mapv first (#?(:cljd cli/cmd-children :squint cli/cmd-children :default #'cli/cmd-children) tree)))))))) (defn- run-dispatch "Run [[cli/dispatch]] capturing stdout+stderr and *exit-fn* calls. Returns @@ -625,7 +625,7 @@ (cli/format-command-help {:table {:cmd-order [] :cmd {"a" {:fn identity}}} :prog "p"})))) (testing ":help true does not change command order (sorted tree, >8 children)" - (let [tree {:cmd (into (sorted-map) + (let [tree {:cmd (into #?(:squint {} :default (sorted-map)) (map (fn [i] [(str "c" i) {:fn identity}])) (range 10))} expected (mapv #(str "c" %) (range 10))] @@ -1058,7 +1058,8 @@ :cljs :default) e (= {:type :org.babashka/cli :cause :validate - :msg "Expected positive number for option :foo but got: 0" + :msg #?(:squint "Expected positive number for option foo but got: 0" + :default "Expected positive number for option :foo but got: 0") :option :foo :flag "--foo" :spec nil diff --git a/test/babashka/run_tests.cljs b/test/babashka/run_tests.cljs new file mode 100644 index 0000000..125a9cc --- /dev/null +++ b/test/babashka/run_tests.cljs @@ -0,0 +1,8 @@ +(ns babashka.run-tests + (:require [clojure.test :as t] + [babashka.cli-test] + [babashka.cli.completion-test])) +(defn -main [& _] + (let [{:keys [fail error]} (t/run-tests 'babashka.cli-test 'babashka.cli.completion-test)] + (when (pos? (+ (or fail 0) (or error 0))) (js/process.exit 1)))) +(-main) From a8a2bd63fc39bf191287e6e8034ae2a307aee4fc Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Thu, 25 Jun 2026 13:27:38 +0200 Subject: [PATCH 7/8] Test against squint latest --- bb.edn | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bb.edn b/bb.edn index 2cd5efe..f1bc1ff 100644 --- a/bb.edn +++ b/bb.edn @@ -21,7 +21,7 @@ :task (apply clojure "-M:test:cljs-test" *command-line-args*)} squint-test {:doc "Run squint tests" - :task (do (shell "npm" "install" "--no-save" "--prefix" "." "squint-cljs") + :task (do (shell "npm" "install" "--no-save" "--prefix" "." "squint-cljs@latest") (shell "node_modules/.bin/squint" "compile") (shell "node" ".squint-out/babashka/run_tests.mjs"))} From 7d211a4ca6db11a342333fc4ea569adf68cafeab Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Thu, 25 Jun 2026 14:19:57 +0200 Subject: [PATCH 8/8] Add java and clojure CLI to squint CI job for :deps resolution --- .github/workflows/ci.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ad35baa..fa601d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,10 +114,17 @@ jobs: - name: "Checkout code" uses: "actions/checkout@v7" + - name: Prepare java + uses: actions/setup-java@v5 + with: + distribution: "zulu" + java-version: 21 + - name: Setup Clojure uses: DeLaGuardo/setup-clojure@13.6.1 with: bb: 1.12.218 + cli: latest github-token: ${{ secrets.GITHUB_TOKEN }} - name: Run tests