Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: babashka/cli
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v0.8.56
Choose a base ref
...
head repository: babashka/cli
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: main
Choose a head ref

Commits on Feb 22, 2024

  1. Copy the full SHA
    7c5b207 View commit details
  2. v0.8.57

    borkdude committed Feb 22, 2024
    Copy the full SHA
    1e4ead0 View commit details
  3. Add Aliases section to README.md (#84)

    Here is a bit of docs around the use cases described in #82.
    arichiardi authored Feb 22, 2024
    Copy the full SHA
    de2a348 View commit details

Commits on Feb 26, 2024

  1. Add basic example to readme (#86)

    * add simple spec example
    
    * updated example comments identation
    
    * removed "above" dup
    tastyminerals authored Feb 26, 2024
    Copy the full SHA
    01f7d1d View commit details

Commits on Mar 4, 2024

  1. Correct README example regarding :error-fn (#88)

    The indentation of :error-fn seemes to be misaligned.
    This commit corrects it.
    rynkowsg authored Mar 4, 2024
    Copy the full SHA
    2d21513 View commit details

Commits on Mar 12, 2024

  1. Copy the full SHA
    a0608d7 View commit details
  2. v0.8.58

    borkdude committed Mar 12, 2024
    Copy the full SHA
    05868bf View commit details

Commits on Mar 15, 2024

  1. Expose matched commands in :no-match and :input-exhausted error cases…

    … for dispatch (#93)
    
    * Refactor
    
    * Expose matched cmds as :dispatch in :no-match and :input-exhausted errors
    
    * Add test
    Sohalt authored Mar 15, 2024
    Copy the full SHA
    acdde51 View commit details

Commits on Mar 18, 2024

  1. Copy the full SHA
    d319f1f View commit details

Commits on Mar 29, 2024

  1. Copy the full SHA
    8cef8ad View commit details

Commits on Apr 30, 2024

  1. v0.8.59

    borkdude committed Apr 30, 2024
    Copy the full SHA
    dcdece0 View commit details

Commits on May 8, 2024

  1. Copy the full SHA
    4c76124 View commit details

Commits on May 13, 2024

  1. Delete mention of deprecated option from cli/dispatch docstring (#100)

    * Delete mention of deprecated option from docstring
    
    * Use the non-deprecated form
    teodorlu authored May 13, 2024
    Copy the full SHA
    729ba8b View commit details

Commits on Jul 23, 2024

  1. v0.8.60

    borkdude committed Jul 23, 2024
    Copy the full SHA
    33fd50a View commit details

Commits on Jul 24, 2024

  1. Fix link to Clojure CLI doc at clojure.org in README.md (#101)

    Doc reorg upstream split CLI and deps.edn docs, so this fixes the link to point to the new location.
    tripplilley authored Jul 24, 2024
    Copy the full SHA
    836b15f View commit details

Commits on Jul 29, 2024

  1. Copy the full SHA
    de2569f View commit details

Commits on Oct 17, 2024

  1. Copy the full SHA
    2b2404a View commit details

Commits on Nov 15, 2024

  1. Copy the full SHA
    6ee198e View commit details
  2. Copy the full SHA
    19c0353 View commit details
  3. v0.8.61

    borkdude committed Nov 15, 2024
    Copy the full SHA
    811a2b8 View commit details

Commits on Dec 22, 2024

  1. Copy the full SHA
    cec7b2b View commit details
Showing with 328 additions and 85 deletions.
  1. +33 −25 API.md
  2. +27 −0 CHANGELOG.md
  3. +100 −1 README.md
  4. +54 −34 src/babashka/cli.cljc
  5. +12 −11 src/babashka/cli/exec.clj
  6. +17 −10 test/babashka/cli/exec_test.clj
  7. +84 −3 test/babashka/cli_test.cljc
  8. +1 −1 version.edn
58 changes: 33 additions & 25 deletions API.md
Original file line number Diff line number Diff line change
@@ -19,10 +19,11 @@
- [`babashka.cli.exec`](#babashkacliexec)
- [`-main`](#-main) - Main entrypoint for command line usage.
- [`main`](#main)
- [`user`](#user)
- [`scratch`](#scratch)
- [`-main`](#-main-1)
- [`dns-get-spec`](#dns-get-spec)
- [`dns-spec`](#dns-spec)
- [`global-spec`](#global-spec)
- [`sub1-spec`](#sub1-spec)
- [`sub2-spec`](#sub2-spec)
- [`table`](#table)
# babashka.cli

@@ -71,7 +72,7 @@ Subcommand dispatcher.
Table is in the form:

```clojure
[{:cmds ["sub_1" .. "sub_n"] :fn f :cmds-opts [:lib]}
[{:cmds ["sub_1" .. "sub_n"] :fn f :args->opts [:lib]}
...
{:cmds [] :fn f}]
```
@@ -90,21 +91,21 @@ Subcommand dispatcher.
Each entry in the table may have additional [`parse-args`](#parse-args) options.

For more information and examples, see [README.md](README.md#subcommands).
<br><sub>[source](https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L666-L698)</sub>
<br><sub>[source](https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L684-L716)</sub>
## `format-opts`
``` clojure

(format-opts {:as cfg, :keys [indent], :or {indent 2}})
```

<sub>[source](https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L566-L570)</sub>
<sub>[source](https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L583-L587)</sub>
## `format-table`
``` clojure

(format-table {:keys [rows indent]})
(format-table {:keys [rows indent], :or {indent 2}})
```

<sub>[source](https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L526-L537)</sub>
<sub>[source](https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L543-L554)</sub>
## `merge-opts`
``` clojure

@@ -127,21 +128,21 @@ Merges babashka CLI options.
(opts->table {:keys [spec order]})
```

<sub>[source](https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L547-L564)</sub>
<sub>[source](https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L564-L581)</sub>
## `pad`
``` clojure

(pad len s)
```

<sub>[source](https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L516-L516)</sub>
<sub>[source](https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L533-L533)</sub>
## `pad-cells`
``` clojure

(pad-cells rows)
```

<sub>[source](https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L518-L524)</sub>
<sub>[source](https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L535-L541)</sub>
## `parse-args`
``` clojure

@@ -152,7 +153,7 @@ Merges babashka CLI options.

Same as [`parse-opts`](#parse-opts) but separates parsed opts into `:opts` and adds
`:cmds` and `:rest-args` on the top level instead of metadata.
<br><sub>[source](https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L494-L501)</sub>
<br><sub>[source](https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L504-L511)</sub>
## `parse-cmds`
``` clojure

@@ -164,7 +165,7 @@ Same as [`parse-opts`](#parse-opts) but separates parsed opts into `:opts` and a
Parses sub-commands (arguments not starting with an option prefix) and returns a map with:
* `:cmds` - The parsed subcommands
* `:args` - The remaining (unparsed) arguments
<br><sub>[source](https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L205-L215)</sub>
<br><sub>[source](https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L206-L216)</sub>
## `parse-keyword`
``` clojure

@@ -212,9 +213,9 @@ Parse the command line arguments `args`, a seq of strings.
;; => throws 'Unknown option --qux' exception b/c there is no :qux key in the spec
```

<br><sub>[source](https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L259-L492)</sub>
<br><sub>[source](https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L261-L502)</sub>
## `rows`
<sub>[source](https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L540-L542)</sub>
<sub>[source](https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L557-L559)</sub>
## `spec->opts`
``` clojure

@@ -224,7 +225,7 @@ Parse the command line arguments `args`, a seq of strings.


Converts spec into opts format. Pass existing opts as optional second argument.
<br><sub>[source](https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L184-L203)</sub>
<br><sub>[source](https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L184-L204)</sub>
# babashka.cli.exec


@@ -249,25 +250,32 @@ Main entrypoint for command line usage.
clojure -M:exec clojure.core prn :a 1 :b 2
;;=> {:a "1" :b "2"}
```
<br><sub>[source](https://github.com/babashka/cli/blob/main/src/babashka/cli/exec.clj#L88-L101)</sub>
<br><sub>[source](https://github.com/babashka/cli/blob/main/src/babashka/cli/exec.clj#L89-L102)</sub>
## `main`
``` clojure

(main & args)
```

<sub>[source](https://github.com/babashka/cli/blob/main/src/babashka/cli/exec.clj#L83-L86)</sub>
# user
<sub>[source](https://github.com/babashka/cli/blob/main/src/babashka/cli/exec.clj#L84-L87)</sub>
# scratch





## `-main`
``` clojure

(-main & args)
```

<sub>[source](https://github.com/babashka/cli/blob/main/src/scratch.clj#L15-L17)</sub>
## `dns-get-spec`
<sub>[source](https://github.com/babashka/cli/blob/main/src/scratch.clj#L8-L8)</sub>
## `dns-spec`
<sub>[source](https://github.com/babashka/cli/blob/main/src/scratch.clj#L7-L7)</sub>
## `global-spec`
<sub>[source](https://github.com/babashka/cli/blob/main/src/scratch.clj#L3-L3)</sub>
## `sub1-spec`
<sub>[source](https://github.com/babashka/cli/blob/main/src/scratch.clj#L4-L4)</sub>
## `sub2-spec`
<sub>[source](https://github.com/babashka/cli/blob/main/src/scratch.clj#L5-L5)</sub>
<sub>[source](https://github.com/babashka/cli/blob/main/src/scratch.clj#L4-L6)</sub>
## `table`
<sub>[source](https://github.com/babashka/cli/blob/main/src/scratch.clj#L17-L19)</sub>
<sub>[source](https://github.com/babashka/cli/blob/main/src/scratch.clj#L10-L13)</sub>
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -4,6 +4,33 @@ For breaking changes, check [here](#breaking-changes).

[Babashka CLI](https://github.com/babashka/cli): turn Clojure functions into CLIs!

## v0.8.62 (2024-12-22)

- Fix [#109](https://github.com/babashka/cli/issues/109): allow options to start with a number

## v0.8.61 (2024-11-15)

- Fix [#102](https://github.com/babashka/cli/issues/102): `format-table` correctly pads cells containing ANSI escape codes
- Fix [#106](https://github.com/babashka/cli/issues/106): Multiple options before subcommand conflict with subcommand
- Fix [#104](https://github.com/babashka/cli/issues/104): Allow extra arguments to be passed before options in exec function

## v0.8.60 (2024-07-23)

- Fix [#98](https://github.com/babashka/cli/issues/98): internal options should not interfere with :restrict

## v0.8.59 (2024-04-30)

- Fix [#96](https://github.com/babashka/cli/issues/96): prevent false defaults from being removed/ignored
- Fix [#91](https://github.com/babashka/cli/issues/91): keyword options and hyphen options should not mix

## v0.8.58 (2024-03-12)

Fix [#89](https://github.com/babashka/cli/issues/89): long option never represents alias

## v0.8.57 (2024-02-22)

Fix [#82](https://github.com/babashka/cli/issues/82): prefer alias over composite option

## v0.8.56 (2024-02-13)

- Add `:opts` to `:error-fn` input
101 changes: 100 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -39,7 +39,7 @@ The main ideas:
- Put as little effort as possible into turning a clojure function into a CLI,
similar to `-X` style invocations. For lazy people like me! If you are not
familiar with `clj -X`, read the docs
[here](https://clojure.org/reference/deps_and_cli#_execute_a_function).
[here](https://clojure.org/reference/clojure_cli#use_fn).
- But with a better UX by not having to use quotes on the command line as a
result of having to pass EDN directly: `:dir foo` instead of `:dir '"foo"'` or
who knows how to write the latter in `cmd.exe` or Powershell.
@@ -64,13 +64,79 @@ your exec functions into CLIs.

## TOC

- [Simple example](#simple-example)
- [Options](#options)
- [Arguments](#arguments)
- [Subcommands](#subcommands)
- [Babashka tasks](#babashka-tasks)
- [Clojure CLI](#clojure-cli)
- [Leiningen](#leiningen)

## Simple example
Here is an example script to get you started!

```clojure
#!/usr/bin/env bb
(require '[babashka.cli :as cli]
'[babashka.fs :as fs])

(defn dir-exists?
[path]
(fs/directory? path))

(defn show-help
[spec]
(cli/format-opts (merge spec {:order (vec (keys (:spec spec)))})))

(def cli-spec
{:spec
{:num {:coerce :long
:desc "Number of some items"
:alias :n ; adds -n alias for --num
:validate pos? ; tests if supplied --num >0
:require true} ; --num,-n is required
:dir {:desc "Directory name to do stuff"
:alias :d
:validate dir-exists?} ; tests if --dir exists
:flag {:coerce :boolean ; defines a boolean flag
:desc "I am just a flag"}}
:error-fn ; a function to handle errors
(fn [{:keys [spec type cause msg option] :as data}]
(when (= :org.babashka/cli type)
(case cause
:require
(println
(format "Missing required argument: %s\n" option))
:validate
(println
(format "%s does not exist!\n" msg)))))})

(defn -main
[args]
(let [opts (cli/parse-opts args cli-spec)]
(if (or (:help opts) (:h opts))
(println (show-help cli-spec))
(println "Here are your cli args!:" opts))))

(-main *command-line-args*)
```

And this is how you run it:
```
$ bb try-me.clj --num 1 --dir my_dir --flag
Here are your cli args!: {:num 1, :dir my_dir, :flag true}
$ bb try-me.clj --help
Missing required argument: :num
-n, --num Number of some items
-d, --dir Directory name to do stuff
--flag I am just a flag
```

Using the [`spec`](https://github.com/babashka/cli#spec) format is optional and you can implement you own parsing logic just with [`parse-opts`/`parse-args`](https://github.com/babashka/cli#options).
However, many would find the above example familiar.

## Options

For parsing options, use either [`parse-opts`](https://github.com/babashka/cli/blob/main/API.md#parse-opts) or [`parse-args`](https://github.com/babashka/cli/blob/main/API.md#parse-args).
@@ -411,6 +477,39 @@ For example to add a header row with labels for each column, you could do someth
:indent 2})
```

### Aliases

An `:alias` specifies a mapping from short to long name.

The library can distinguish aliases with characters in common, so a way to implement the common `-v`/`-vv` unix pattern is:
``` clojure
(def spec {:verbose {:alias :v
:desc "Enable verbose output."}
:very-verbose {:alias :vv
:desc "Enable very verbose output."}})
```

You get:

```clojure
(cli/parse-opts ["-v"] {:spec spec})
;;=> {:verbose true}

(cli/parse-opts ["-vv"] {:spec spec})
;;=> {:very-verbose true}
```

Another way would be to collect the flags in a vector with `:coerce` (and base verbosity on the size of that vector):

``` clojure
(def spec {:verbose {:alias :v
:desc "Enable verbose output."
:coerce []}})

user=> (cli/parse-opts ["-vvv"] {:spec spec})
{:verbose [true true true]}
```

## Subcommands

To handle subcommands, use
88 changes: 54 additions & 34 deletions src/babashka/cli.cljc
Original file line number Diff line number Diff line change
@@ -197,8 +197,9 @@
(assoc aliases alias k)))
require (update :require (fnil #(conj % k) #{}))
validate (update :validate assoc k validate)
default (update :exec-args (fn [new-exec-args]
(assoc new-exec-args k (get exec-args k default))))))
(some? default) (update :exec-args
(fn [new-exec-args]
(assoc new-exec-args k (get exec-args k default))))))
{}
spec)))

@@ -232,18 +233,23 @@
{:args new-args
:args->opts args->opts})))

(defn- parse-key [arg mode current-opt coerce-opt added]
(defn- parse-key [arg mode current-opt coerce-opt added known-keys alias-keys]
(let [fst-char (first-char arg)
snd-char (second-char arg)
hyphen-opt? (and (= fst-char \-)
(not (number-char? snd-char)))
hyphen-opt? (and (not= :keywords mode)
(= \- fst-char)
(let [k (keyword (subs arg 1))]
(or
(contains? known-keys k)
(contains? alias-keys k)
(not (number-char? snd-char)))))
mode (or mode (when hyphen-opt? :hyphens))
fst-colon? (= \: fst-char)
kwd-opt? (and (not= :hyphens mode)
fst-colon?
(or (= :boolean coerce-opt)
(or (not current-opt)
(= added current-opt))))
(not current-opt)
(= added current-opt)))
mode (or mode
(when kwd-opt?
:keywords))
@@ -305,8 +311,10 @@
no-keyword-opts (:no-keyword-opts opts)
restrict (or (:restrict opts)
(:closed opts))
known-keys (set (concat (keys (if (map? spec)
spec (into {} spec)))
spec-map (if (map? spec)
spec (into {} spec))
alias-keys (set (concat (keys aliases) (map :alias (vals spec-map))))
known-keys (set (concat (keys spec-map)
(vals aliases)
(keys coerce-opts)))
restrict (if (true? restrict)
@@ -360,7 +368,7 @@
{:keys [hyphen-opt
composite-opt
kwd-opt
mode fst-colon]} (parse-key arg mode current-opt coerce-opt added)]
mode fst-colon]} (parse-key arg mode current-opt coerce-opt added known-keys alias-keys)]
(if (or hyphen-opt
kwd-opt)
(let [long-opt? (str/starts-with? arg "--")
@@ -377,20 +385,22 @@
(str/split kname #"=")
[kname])
raw-k (keyword kname)
k (get aliases raw-k raw-k)]
alias (when-not long-opt?
(get aliases raw-k))
k (or alias raw-k)]
(if arg-val
(recur (process-previous acc current-opt added collect-fn)
k nil mode (cons arg-val (rest args)) a->o)
(let [next-args (next args)
next-arg (first next-args)
m (parse-key next-arg mode current-opt coerce-opt added)
m (parse-key next-arg mode current-opt coerce-opt added known-keys alias-keys)
negative? (when-not (contains? known-keys k)
(str/starts-with? (str k) ":no-"))]
(if (or (:hyphen-opt m)
(empty? next-args)
negative?)
;; implicit true
(if composite-opt
(if (and (not alias) composite-opt)
(let [chars (name k)
args (mapcat (fn [char]
[(str "-" char) true])
@@ -414,7 +424,9 @@
(not= arg "true")
(not= arg "false"))
(and (= added current-opt)
(not collect-fn)))]
(or
(not collect-fn)
(contains? (::dispatch-tree-ignored-args opts) (first args)))))]
(if the-end?
(let [{new-args :args
a->o :args->opts}
@@ -459,7 +471,8 @@
opts)]
(when restrict
(doseq [k (keys opts)]
(when-not (contains? restrict k)
(when (and (not (contains? restrict k))
(not= (namespace k) "babashka.cli"))
(error-fn {:cause :restrict
:msg (str "Unknown option: " k)
:restrict restrict
@@ -516,17 +529,24 @@
(defn- kw->str [kw]
(subs (str kw) 1))

(defn pad [len s] (str s (apply str (repeat (- len (count s)) " "))))
(defn- str-width
"Width of `s` when printed, i.e. without ANSI escape codes."
[s]
(let [strip-escape-codes #(str/replace %
(re-pattern "(\\x9B|\\x1B\\[)[0-?]*[ -\\/]*[@-~]") "")]
(count (strip-escape-codes s))))

(defn pad [len s] (str s (apply str (repeat (- len (str-width s)) " "))))

(defn pad-cells [rows]
(let [widths (reduce
(fn [widths row]
(map max (map count row) widths)) (repeat 0) rows)
(let [widths (reduce
(fn [widths row]
(map max (map str-width row) widths)) (repeat 0) rows)
pad-row (fn [row]
(map (fn [width col] (pad width col)) widths row))]
(map pad widths row))]
(map pad-row rows)))

(defn format-table [{:keys [rows indent]}]
(defn format-table [{:keys [rows indent] :or {indent 2}}]
(let [rows (pad-cells rows)
fmt-row (fn [leader divider trailer row]
(str leader
@@ -557,8 +577,8 @@
(when (:ref columns)
(if ref ref ""))
(when (or (:default-desc columns)
(:default columns))
(str (or default-desc default "")))
(some? (:default columns)))
(str (or default-desc (str default) "")))
(when (:desc columns)
(if desc desc ""))]))
(if (map? spec)
@@ -642,29 +662,29 @@
{:error :no-match
:wrong-input arg
:available-commands (keys (:cmd cmd-info))
:dispatch cmds
:opts (dissoc all-opts ::opts-by-cmds)}
{:error :input-exhausted
:available-commands (keys (:cmd cmd-info))
:dispatch cmds
:opts (dissoc all-opts ::opts-by-cmds)})))))))

(defn- dispatch-tree
([tree args]
(dispatch-tree tree args nil))
([tree args opts]
(let [{:as res :keys [cmd-info error wrong-input available-commands]}
(let [{:as res :keys [cmd-info error available-commands]}
(dispatch-tree' tree args opts)
error-fn* (or (:error-fn opts)
error-fn (or (:error-fn opts)
(fn [{:keys [msg] :as data}]
(throw (ex-info msg data))))
error-fn (fn [data]
(-> {;; :tree tree
:type :org.babashka/cli
:wrong-input wrong-input :all-commands available-commands}
(merge data)
error-fn*))]
(throw (ex-info msg data))))]
(case error
(:no-match :input-exhausted)
(error-fn {:cause error :opts (:opts res)})
(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
@@ -676,7 +696,7 @@
Table is in the form:
```clojure
[{:cmds [\"sub_1\" .. \"sub_n\"] :fn f :cmds-opts [:lib]}
[{:cmds [\"sub_1\" .. \"sub_n\"] :fn f :args->opts [:lib]}
...
{:cmds [] :fn f}]
```
23 changes: 12 additions & 11 deletions src/babashka/cli/exec.clj
Original file line number Diff line number Diff line change
@@ -7,7 +7,6 @@

(set! *warn-on-reflection* true)

#_:clj-kondo/ignore
(def ^:private ^:dynamic *basis* "For testing" nil)

(defmacro ^:private req-resolve [f]
@@ -54,16 +53,18 @@
(str/starts-with? f "{")
[(edn/read-string f) cmds]
:else [nil (cons f cmds)])
f (case (count cmds)
0 (resolve-exec-fn ns-default exec-fn)
1 (let [f (first cmds)]
(if (str/includes? f "/")
(symbol f)
(resolve-exec-fn ns-default (symbol f))))
2 (let [[ns-default f] cmds]
(if (str/includes? f "/")
(symbol f)
(resolve-exec-fn (symbol ns-default) (symbol f)))))
[f unconsumed-args] (case (count cmds)
0 [(resolve-exec-fn ns-default exec-fn)]
1 [(let [f (first cmds)]
(if (str/includes? f "/")
(symbol f)
(resolve-exec-fn ns-default (symbol f))))]
(let [[ns-default f & unconsumed-args] cmds]
[(if (str/includes? f "/")
(symbol f)
(resolve-exec-fn (symbol ns-default) (symbol f)))
unconsumed-args]))
args (concat unconsumed-args args)
f* f
f (req-resolve f)
_ (assert (ifn? f) (str "Could not resolve function: " f*))
27 changes: 17 additions & 10 deletions test/babashka/cli/exec_test.clj
Original file line number Diff line number Diff line change
@@ -20,6 +20,11 @@
(is (submap? {:a 1 :b 2} (main "babashka.cli.exec-test/foo" ":a" "1" ":b" "2")))
(is (submap? {:b 1} (main "babashka.cli.exec-test" "foo" ":b" "1")))
(is (submap? {:a 1 :b 2} (main "babashka.cli.exec-test" "foo" ":a" "1" ":b" "2")))
(is (submap? {:args ["arg"]
:exec true}
(-> (main "babashka.cli.exec-test" "foo" "arg" ":a" "1" ":b" "2")
meta
:org.babashka/cli)))
(is (submap? {:a 1 :b 2} (main
"{:coerce {:a :long}}"
"babashka.cli.exec-test" "foo" ":a" "1" ":b" "2")))
@@ -35,20 +40,20 @@
":a" "1" ":b" "2"))))
(is (submap? {:a 1 :b 2}
(binding [babashka.cli.exec/*basis* '{:argmap {:org.babashka/cli {:coerce {:a :long}}
:exec-fn babashka.cli.exec-test/foo}}]
:exec-fn babashka.cli.exec-test/foo}}]
(main
":a" "1" ":b" "2"))))
(is (submap? {:a 1 :b 2}
(binding [babashka.cli.exec/*basis*
'{:argmap {:org.babashka/cli {:coerce {:a :long}}
:ns-default babashka.cli.exec-test}}]
:ns-default babashka.cli.exec-test}}]
(main
"foo" ":a" "1" ":b" "2"))))
(is (submap? {:a 1 :b 2}
(binding [babashka.cli.exec/*basis*
'{:argmap {:org.babashka/cli {:coerce {:a :long}}
:ns-default babashka.cli.exec-test
:exec-fn foo}}]
:ns-default babashka.cli.exec-test
:exec-fn foo}}]
(main ":a" "1" ":b" "2"))))
(let [basis "{:argmap {:org.babashka/cli {:coerce {:a :long}}
:ns-default babashka.cli.exec-test
@@ -63,18 +68,20 @@
(edn/read-string
(with-out-str (binding [babashka.cli.exec/*basis*
'{:argmap {:org.babashka/cli {:coerce {:a :long}}
:ns-default babashka.cli.exec-test
:exec-fn foo}}]
:ns-default babashka.cli.exec-test
:exec-fn foo}}]
(main "clojure.core/prn" ":a" "1" ":b" "2"))))))
(is (submap? {:a 1 :b 2}
(edn/read-string
(with-out-str (binding [babashka.cli.exec/*basis*
'{:argmap {:ns-default clojure.pprint
:exec-fn foo}}]
:exec-fn foo}}]
(main "pprint" ":a" "1" ":b" "2"))))))
(is (:exec (:org.babashka/cli
(meta (binding [babashka.cli.exec/*basis*
'{:argmap {:org.babashka/cli {:coerce {:a :long}}
:ns-default babashka.cli.exec-test
:exec-fn foo}}]
(main ":a" "1" ":b" "2")))))))
:ns-default babashka.cli.exec-test
:exec-fn foo}}]
(main ":a" "1" ":b" "2"))))))

)
87 changes: 84 additions & 3 deletions test/babashka/cli_test.cljc
Original file line number Diff line number Diff line change
@@ -203,10 +203,11 @@
:exec-args {:from :edn, :to :json, :paths ["src" "test"]}}
(cli/spec->opts spec nil)))
(is (= (str/trim "
-p, --pretty Pretty-print output.
-p, --pretty false Pretty-print output.
--paths src test Paths of files to transform.
") (str/trim
(cli/format-opts {:spec [[:pretty {:desc "Pretty-print output."
:default false
:alias :p}]
[:paths {:desc "Paths of files to transform."
:coerce []
@@ -408,7 +409,12 @@
(is (= {:dispatch ["foo" "bar"], :opts {:version "2000"}, :args ["some-arg"]}
(-> (cli/dispatch
table
["foo" "bar" "--version" "2000" "some-arg"]))))))))
["foo" "bar" "--version" "2000" "some-arg"])))))
(testing "dispatch errors return :dispatch key"
(is (= {:type :org.babashka/cli, :dispatch ["foo" "bar"], :all-commands '("baz"), :cause :input-exhausted, :opts {}}
(cli/dispatch [{:cmds ["foo" "bar" "baz"] :fn identity}] ["foo" "bar"] {:error-fn identity})))
(is (= {:type :org.babashka/cli, :dispatch ["foo" "bar"], :wrong-input "wrong", :all-commands '("baz"), :cause :no-match, :opts {}}
(cli/dispatch [{:cmds ["foo" "bar" "baz"] :fn identity}] ["foo" "bar" "wrong"] {:error-fn identity})))))))

(deftest table->tree-test
(testing "internal represenation"
@@ -460,7 +466,10 @@
(is (= -10 (cli/auto-coerce "-10")))
(is (submap? {:foo -10} (cli/parse-opts ["--foo" "-10"])))
(is (submap? {:foo -10} (cli/parse-opts ["--foo" "-10"] {:coerce {:foo :number}})))
(is (submap? {:foo "-10"} (cli/parse-opts ["--foo" "-10"] {:coerce {:foo :string}}))))
(is (submap? {:foo "-10"} (cli/parse-opts ["--foo" "-10"] {:coerce {:foo :string}})))
(is (submap? {:6 true} (cli/parse-opts ["-6"] {:spec {:6 {}}})))
(is (submap? {:6 true} (cli/parse-opts ["-6"] {:coerce {:6 :boolean}})))
(is (submap? {:ipv6 true} (cli/parse-opts ["-6"] {:aliases {:6 :ipv6}}))))

(deftest format-opts-test
(testing "default width with default and default-desc"
@@ -481,6 +490,21 @@
:desc "Barbarbar" :default-desc "Mos def"}}}))
:indent 2})))))

(deftest format-table-test
(let [contains-row-matching (fn [re table]
(let [rows (str/split-lines table)]
(is (some #(re-find re %) rows)
(str "expected " (pr-str rows)
" to contain a row matching " (pr-str re)))))]
(testing "ANSI escape codes don't count towards a cell's width"
(let [table (cli/format-table {:rows [["widest" "<- sets column width to 6"]
["\033[31mfoo\033[0m" "<- needs 3+1 padding"]
["bar" "<- needs 3+1 padding"]]})]
(contains-row-matching #"\033\[31mfoo\033\[0m <-"
table)
(contains-row-matching #"bar <-"
table)))))

(deftest require-test
(is (thrown-with-msg?
Exception #"Required option: :bar"
@@ -540,3 +564,60 @@
:exec-args {:dude [:baz]}})))
(is (= {:foo [:bar]} (cli/parse-opts ["--foo" ":bar"] {:coerce {:foo []}
:exec-args {:foo [:baz]}}))))

(deftest issue-82-alias-preference
(is (= {:opts {:verbose2 true}}
(cli/parse-args ["-vv"] {:spec {:verbose1 {:alias :v}
:verbose2 {:alias :vv}}}))))

(deftest issue-89-alias-only-for-short-opt
(is (= {:f "dude"} (cli/parse-opts ["--f" "dude"] {:spec {:foo {:alias :f}}}))))

(deftest issue-91-keyword-mode-overrides-hypens-mode
(is (= {:args ["--baz"], :opts {:foo 1}}
(cli/parse-args [":foo" 1 "--baz"] {}))))

(deftest issue-98-dispatch+restrict-test
(is (thrown? Exception
(cli/dispatch [{:cmds ["foo"]
:fn identity
:spec {:x {:coerce :boolean}}}]
["foo" "--y"]
{:restrict true})))
(is (= {:dispatch ["foo"], :opts {}, :args nil}
(cli/dispatch [{:cmds ["foo"]
:fn identity
:spec {:x {:coerce :boolean}}}]
["foo"]
{:restrict true}))))

(deftest issue-106-test
(d/deflet
(def global-spec {:config {:desc "Config edn file to use"
:coerce []}
:verbose {:coerce :boolean}})
(def dns-spec {})
(def dns-get-spec {})
(def table
[{:cmds [] :fn identity :spec global-spec}
{:cmds ["dns"] :fn identity :spec dns-spec}
{:cmds ["dns" "get"] :fn identity :spec dns-get-spec}])
(is (submap?
{:dispatch ["dns"], :opts {:config ["config-dev.edn" "other.edn"]}, :args nil}
(cli/dispatch table ["--config" "config-dev.edn" "--config" "other.edn" "dns"])))
(is (submap?
{:dispatch ["dns" "get"],
:opts {:config ["config-dev.edn" "other.edn"]},
:args nil}
(cli/dispatch table ["--config" "config-dev.edn" "--config" "other.edn" "dns" "get"])))
(is (submap?
{:dispatch ["dns" "get"],
:opts {:config ["config-dev.edn" "other.edn"], :verbose true},
:args nil}
(cli/dispatch table ["--config" "config-dev.edn" "--verbose" "--config" "other.edn" "dns" "get"])))
(is (submap?
{:dispatch ["dns" "get"],
:opts {:config ["config-dev.edn" "other.edn"]},
:args nil}
(cli/dispatch table ["--config" "config-dev.edn" "--config=other.edn" "dns" "get"]))))
)
2 changes: 1 addition & 1 deletion version.edn
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{:major 0, :minor 8, :release 56}
{:major 0, :minor 8, :release 61}