Skip to content

Commit a43d66f

Browse files
authored
Bind positional args directly instead of re-injecting into the token stream (#202)
1 parent bbe48bc commit a43d66f

2 files changed

Lines changed: 50 additions & 71 deletions

File tree

src/babashka/cli.cljc

Lines changed: 45 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,6 @@
1111

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

14-
;; squint can't tell an injected keyword from a string arg, so wrap injected opts
15-
#?(:squint (deftype Injected [opt]))
16-
1714
(defn merge-opts
1815
"Merges babashka CLI options."
1916
[m & ms]
@@ -239,26 +236,6 @@
239236
{:cmds cmds
240237
:args args})))
241238

242-
(defn- args->opts
243-
([args args->opt-keys] (args->opts args args->opt-keys #{}))
244-
([args args->opt-keys ignored-args]
245-
(let [[new-args args->opts]
246-
(if args->opt-keys
247-
(if (and (seq args)
248-
(not (contains? ignored-args (first args))))
249-
(let [arg-count (count args)
250-
cnt (min arg-count
251-
(bounded-count arg-count args->opt-keys))]
252-
[(concat (interleave #?(:squint (map ->Injected args->opt-keys)
253-
:default args->opt-keys)
254-
args)
255-
(drop cnt args))
256-
(drop cnt args->opt-keys)])
257-
[args args->opt-keys])
258-
[args args->opt-keys])]
259-
{:args new-args
260-
:args->opts args->opts})))
261-
262239
(defn- analyze-arg [arg mode open-opt boolean-opt? valued-opt known-keys alias-keys]
263240
(let [fst-char (first-char arg)
264241
snd-char (second-char arg)
@@ -624,54 +601,53 @@
624601
;; metadata (e.g., parsed option :foo might have been typed as :foo, --foo or -f),
625602
;; so error messages can echo what the user actually typed
626603
stamp (fn [m k lit] (if lit (vary-meta m assoc-in [::opt->flag k] lit) m))
627-
;; inject leading positional args (in CLIs these are typically commands)
628-
;; as options as per :args->opts
604+
ignored-args (::dispatch-tree-ignored-args parse-opts)
605+
a->o (or (:args->opts parse-opts)
606+
;; :cmd-opts is the old name for :args->opts, left in for backward compat
607+
(:cmds-opts parse-opts))
608+
;; bind leading positional args (in CLIs these are typically commands)
609+
;; to :args->opts keys, pairwise. An ignored first arg (a subcommand
610+
;; name) leaves the whole leading group alone for dispatch to route.
629611
{leading-pos-args :cmds args :args} (parse-cmds args)
630-
{new-leading-pos-args :args a->o :args->opts}
631-
(if-let [a->o (or (:args->opts parse-opts)
632-
;; :cmd-opts is the old name for :args->opts, left in for backward compat
633-
(:cmds-opts parse-opts))]
634-
(args->opts leading-pos-args a->o (::dispatch-tree-ignored-args parse-opts))
635-
{:args->opts nil :args args})
636-
[leading-pos-args args] (if (not= new-leading-pos-args args)
637-
[nil (concat new-leading-pos-args args)]
612+
bind-leading? (and (seq a->o) (seq leading-pos-args)
613+
(not (contains? ignored-args (first leading-pos-args))))
614+
[acc0 kpo0 leftover-leading a->o last-bound]
615+
(if bind-leading?
616+
(loop [acc {} kpo [] toks (seq leading-pos-args) ks a->o bound nil]
617+
(if (and toks (seq ks))
618+
(let [k (first ks)]
619+
(recur (add-val-to-opt acc k (collect-fn collect coerce k) (first toks))
620+
(track-kpo kpo k) (next toks) (rest ks) k))
621+
[acc kpo toks ks bound]))
622+
[{} [] nil a->o nil])
623+
;; leftover leading args (keys exhausted) re-enter the loop and stop
624+
;; option parsing at the trailing-args rule, like any unbound arg
625+
[leading-pos-args args] (if bind-leading?
626+
[nil (concat leftover-leading args)]
638627
[leading-pos-args args])
639628
[parsed last-open-opt last-valued-opt implicit-values key-parse-order]
640629
(if (and (::dispatch-tree parse-opts) (seq leading-pos-args))
641630
[(vary-meta {} assoc-in [:org.babashka/cli :args] (into (vec leading-pos-args) args)) nil nil #{} []]
642-
(loop [acc {}
631+
(loop [acc acc0
643632
#_{:clj-kondo/ignore [:unused-binding]}
644633
recur-action nil ;; for debugging only
645-
open-opt nil ;; the cli option keyword we are working on
646-
valued-opt nil ;; the cli option keyword that has been given value(s) (but not necessarily all values)
634+
open-opt last-bound ;; the cli option keyword we are working on
635+
valued-opt last-bound ;; the cli option keyword that has been given value(s) (but not necessarily all values)
647636
mode (when no-keyword-opts :hyphens) ;; :hyphens --foo/-f else :keywords :foo
648637
args (seq args) ;; remaining cli args
649-
a->o a->o ;; requested args->opts
638+
a->o a->o ;; remaining args->opts keys
650639
implicit-values {}
651-
opt-parse-order []]
640+
opt-parse-order kpo0]
652641
#_(println (format "loop %-31s o: %-10s v: %-10s a: %s" recur-action open-opt valued-opt args))
653642
(if-not args
654643
;; exit loop: no command line args left
655644
[acc open-opt valued-opt implicit-values opt-parse-order]
656645
(let [raw-arg (first args)
657-
opt-injected? #?(:squint (instance? Injected raw-arg)
658-
:default (keyword? raw-arg))
659-
#?@(:squint [raw-arg (if opt-injected? (.-opt raw-arg) raw-arg)])]
660-
(if opt-injected?
661-
;; continue loop: this opt and its value was injected by args->opts
662-
;; opt-val-collector does not apply for injected opts, so is `nil`.
663-
;; valued-opt resets to nil: a freshly injected opt has no value yet
664-
(recur (maybe-close-open-opt acc open-opt valued-opt nil)
665-
:found-injected-opt
666-
raw-arg nil
667-
mode (next args) a->o
668-
(track-ivs implicit-values open-opt valued-opt)
669-
(track-kpo opt-parse-order raw-arg))
670-
(let [arg (str raw-arg)
671-
opt-val-collector (collect-fn collect coerce open-opt)
672-
boolean-opt? (expects-bool-val? open-opt)
673-
{:keys [hyphen-opt composite-opt kwd-opt mode fst-colon]}
674-
(analyze-arg arg mode open-opt boolean-opt? valued-opt known-keys alias-keys)]
646+
arg (str raw-arg)
647+
opt-val-collector (collect-fn collect coerce open-opt)
648+
boolean-opt? (expects-bool-val? open-opt)
649+
{:keys [hyphen-opt composite-opt kwd-opt mode fst-colon]}
650+
(analyze-arg arg mode open-opt boolean-opt? valued-opt known-keys alias-keys)]
675651
(if (or hyphen-opt kwd-opt)
676652
;; arg is -f/-foo or :foo
677653
(let [long-opt? (str/starts-with? arg "--")
@@ -749,19 +725,18 @@
749725
repeated-opts ;; must specify --foo a --foo and not --foo a b
750726
(contains? (::dispatch-tree-ignored-args parse-opts) (first args)))))]
751727
(if done-parsing-options?
752-
(let [{new-trailing-pos-args :args a->o :args->opts}
753-
(if (and args a->o)
754-
(args->opts args a->o (::dispatch-tree-ignored-args parse-opts))
755-
{:args args})
756-
new-args? (not= args new-trailing-pos-args)]
757-
(if new-args?
758-
;; continue loop: with trailing args -> options
759-
(recur acc
760-
:injected-trailing-args-to-opts
761-
open-opt valued-opt
762-
mode new-trailing-pos-args a->o
763-
implicit-values opt-parse-order)
764-
;; exit loop: args -> options resulted in no new args
728+
(let [k (when-not (contains? ignored-args arg)
729+
(first a->o))]
730+
(if k
731+
;; continue loop: bind this arg to the next :args->opts key
732+
(recur (add-val-to-opt (maybe-close-open-opt acc open-opt valued-opt opt-val-collector)
733+
k (collect-fn collect coerce k) arg)
734+
:bound-positional
735+
k k
736+
mode (next args) (rest a->o)
737+
(track-ivs implicit-values open-opt valued-opt)
738+
(track-kpo opt-parse-order k))
739+
;; exit loop: no keys left (or a subcommand name): the rest are args
765740
[(vary-meta acc assoc-in [:org.babashka/cli :args] (vec args)) open-opt valued-opt implicit-values opt-parse-order]))
766741
(let [opt (when-not (and (= :keywords mode) fst-colon) open-opt)]
767742
;; continue loop: add/update opt with value (arg), and setup to parse next arg
@@ -772,7 +747,7 @@
772747
opt opt ;; keep opt open
773748
mode (next args) a->o
774749
(cond-> implicit-values (boolean? raw-arg) (assoc open-opt raw-arg))
775-
opt-parse-order)))))))))))
750+
opt-parse-order)))))))))
776751
;; Finalize: process last opt, prepend leading positional args to args metadata
777752
implicit-values (track-ivs implicit-values last-open-opt last-valued-opt)
778753
opt-val-collector (collect-fn collect coerce last-open-opt)

test/babashka/cli_test.cljc

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,11 @@
334334
(is (= {:y ["a" "b" "c"]}
335335
(cli/parse-opts ["a" "b" "c"] {:args->opts (repeat :y) :spec {:y {:coerce []}}})))
336336
(is (= {:first "a" :rest ["b" "c"]}
337-
(cli/parse-opts ["a" "b" "c"] {:args->opts (cons :first (repeat :rest)) :spec {:rest {:coerce []}}})))))
337+
(cli/parse-opts ["a" "b" "c"] {:args->opts (cons :first (repeat :rest)) :spec {:rest {:coerce []}}}))))
338+
(testing "an option token never occupies a positional slot"
339+
(is (= {:x "a" :force true :y "b" :other "c"}
340+
(cli/parse-opts ["a" "--force" "b" "--other" "c"]
341+
{:args->opts [:x :y :z] :coerce {:force :boolean}})))))
338342

339343
(defn- err-data [f]
340344
(try (f) nil

0 commit comments

Comments
 (0)