Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,29 @@ 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: 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
run: bb squint-test

test-cljd:
runs-on: ubuntu-latest

Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ README.html
pubspec.lock
lib/cljd-out
test/cljd-out
node_modules
.squint-out
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ For breaking changes, check [here](#breaking-changes).

## Unreleased

- Squint support (v0.14.196+)
- [#166](https://github.com/babashka/cli/issues/166): trigger negation invalid error for `--no-foo` when `:foo`'s specified `:coerce` is not `:boolean`
([@lread](https://github.com/lread))

Expand Down
5 changes: 5 additions & 0 deletions bb.edn
Original file line number Diff line number Diff line change
Expand Up @@ -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@latest")
(shell "node_modules/.bin/squint" "compile")
(shell "node" ".squint-out/babashka/run_tests.mjs"))}

cljd-test {:doc "Run ClojureDart tests (requires Dart SDK: https://dart.dev/get-dart)"
:task (do (shell "dart pub get")
(apply clojure "-M:cljd" "test"
Expand Down
60 changes: 60 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"devDependencies": {
"squint-cljs": "^0.13.195"
}
}
5 changes: 5 additions & 0 deletions squint.edn
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{:paths ["src" "test"]
:deps {io.github.borkdude/deflet {:git/sha "a3c86cdbd23531b2d59e6d7db1c49418cb325c7f"
:git/tag "v0.1.0"}}
:output-dir ".squint-out"
:extension "mjs"}
38 changes: 24 additions & 14 deletions src/babashka/cli.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@

#?(:clj (set! *warn-on-reflection* true))

;; squint can't tell an injected keyword from a string arg, so wrap injected opts
#?(:squint (deftype Injected [opt]))

(defn merge-opts
"Merges babashka CLI options."
[m & ms]
Expand Down Expand Up @@ -242,7 +245,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])
Expand Down Expand Up @@ -295,7 +300,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
Expand Down Expand Up @@ -346,7 +352,7 @@
(let [coll-coerce? (coll? coerce-k)
empty-coll (when coll-coerce? (or (empty coerce-k) []))
cf (coerce-coerce-fn coerce-k)
iv (and implicit-values (k implicit-values))]
iv (and implicit-values (get implicit-values k))]
(try
(cond
(and coll-coerce? (coll? v))
Expand All @@ -368,7 +374,9 @@
;; instead of "cannot transform (implicit) true to ..."
:msg (case iv
true (str "Missing value for option " (option-label km k))
false (str "Cannot negate option " (str/replace-first (option-label km k) "no-" ""))
;; NOTE: squint lacks clojure.string/replace-first until > 0.14.196; use JS interop for now
false (str "Cannot negate option " #?(:squint (.replace (option-label km k) "no-" "")
:default (str/replace-first (option-label km k) "no-" "")))
(str "Invalid value for option " (option-label km k) ": "
(coerce-failure-reason (:input data) iv (:coerce-fn data))))
:option k
Expand Down Expand Up @@ -463,7 +471,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)))
Expand Down Expand Up @@ -567,7 +575,9 @@
;; exit loop: no command line args left
[acc open-opt valued-opt implicit-values 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`
Expand Down Expand Up @@ -616,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
Expand All @@ -630,7 +640,7 @@
mode (concat expanded next-args) a->o
implicit-values 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)
Expand Down Expand Up @@ -945,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
Expand All @@ -957,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
Expand Down Expand Up @@ -1022,7 +1032,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
Expand Down Expand Up @@ -1185,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
Expand Down Expand Up @@ -1379,7 +1389,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
Expand Down
20 changes: 10 additions & 10 deletions test/babashka/cli/completion_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -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]])))

Expand All @@ -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]
Expand All @@ -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))

Expand Down Expand Up @@ -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" ""])))))

Expand Down Expand Up @@ -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 ")))
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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" "--"]
Expand All @@ -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 {}}}))))))
Expand Down
11 changes: 6 additions & 5 deletions test/babashka/cli_test.cljc
Original file line number Diff line number Diff line change
@@ -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]]
Expand Down Expand Up @@ -524,13 +524,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
Expand Down Expand Up @@ -647,7 +647,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))]
Expand Down Expand Up @@ -1080,7 +1080,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
Expand Down
8 changes: 8 additions & 0 deletions test/babashka/run_tests.cljs
Original file line number Diff line number Diff line change
@@ -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)
Loading