Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dispatch tree #75

Closed
wants to merge 3 commits into from
Closed

Dispatch tree #75

wants to merge 3 commits into from

Conversation

Sohalt
Copy link
Contributor

@Sohalt Sohalt commented Nov 14, 2023

Expand dispatch to also work on a tree instead of a table.

The tree consists of nested maps with string keys for command parts and keyword keys for opts to be passed to parse-args.
e.g.

(def tree {"foo" {"bar" {"baz" {:fn identity}}}})

encodes a command foo bar baz that invokes identity:

(dispatch-tree' tree ["foo" "bar" "baz"])
;=> {:cmd-info {:fn #object[clojure.core$identity 0x1f39fc08 "clojure.core$identity@1f39fc08"]}, :dispatch ["foo" "bar" "baz"], :opts {}, :args nil}

There can be options for parse-args, such as :specs at every level, e.g.

(def tree {"foo" {"bar" {"baz" {:fn identity
                                :spec {:baz-flag {:coerce :boolean}}}
                         :spec {:bar-flag {:coerce :boolean}
                                :bar-option {:coerce :keyword}}}}})

(dispatch-tree' tree ["foo" "bar" "--bar-option" "asdf" "--bar-flag" "baz" "--baz-flag"] {})
;=> {:cmd-info {:fn #object[clojure.core$identity 0x1f39fc08 "clojure.core$identity@1f39fc08"], :spec {:baz-flag {:coerce :boolean}}}, :dispatch ["foo" "bar" "baz"], :opts {:bar-option :asdf, :bar-flag true, :baz-flag true}, :args nil}

If there is no :fn key at the node that's reached by parsing the args, the dispatch will return an error:

(dispatch-tree' tree [] {})
;=>{:error :input-exhausted, :available-commands ("foo")}
(dispatch-tree' tree ["quux"] {})
;=>{:error :no-match, :wrong-input "quux", :available-commands ("foo")}

There's a table->tree function to convert tables to trees.

@borkdude
Copy link
Contributor

user=> (cli/dispatch-tree' tree ["foo" "bar" "baz"])
Execution error (ArityException) at user/eval408 (REPL:1).
Wrong number of args (2) passed to: babashka.cli/dispatch-tree'

@borkdude
Copy link
Contributor

Comparison on string fails on Windows due to newlines being different on Windows

@borkdude
Copy link
Contributor

Note: it seems that all options are gathered in one bag:

user=> (cli/dispatch-tree' tree ["foo" "--foo" "dude" "bar" "baz" "dude"] nil)
{:cmd-info {:fn #object[clojure.core$identity 0x363f0ba0 "clojure.core$identity@363f0ba0"]}, :dispatch ["foo" "bar" "baz"], :opts {:foo :dude}, :args ("dude")}

Are there situations where one might want to see where options came from? Like {"foo" {:opts {:foo :dude}}}
I remember this coming up in #66
Might want to see what "clap" is returning.

@borkdude
Copy link
Contributor

TODO for @borkdude : check how argparse works in Python regarding this

@Sohalt
Copy link
Contributor Author

Sohalt commented Nov 15, 2023

Comparison on string fails on Windows due to newlines being different on Windows

Yep, noticed that.

I'm not entirely sure dispatch should print an error at all. I think it's generally bad behavior for libraries to print something. That's why I added the dispatch-tree' function, which will only return the error and leave it to disptach-tree to print a message.

Maybe dispatch-tree could call :error-fn when it cannot dispatch to a function?

@Sohalt
Copy link
Contributor Author

Sohalt commented Nov 15, 2023

Also with dispatch internally converting tables to the tree representation and then dispatching that, there's breaking behavior for cases using the table where ordering matters. e.g.

(def table [{:cmds ["foo"] :fn (fn [_] (print "foo")} {:cmds ["foo" "bar"} :fn (fn [_] (print "bar")])

old behavior:

(dispatch table ["foo" "bar"])
"foo"

new behavior

(dispatch table ["foo" "bar"])
"bar"

@Sohalt
Copy link
Contributor Author

Sohalt commented Nov 15, 2023

Note: it seems that all options are gathered in one bag

yes. I could do something like:

(cli/dispatch-tree' tree ["foo" "--foo" "dude" "bar" "baz" "--quux" "xyzzy" "dude"] nil)
{:opts {["foo"] {:foo "dude} ["foo" "bar" "baz"] {:quux "xyzzy"}}}

Though it feels a bit heavy

@borkdude
Copy link
Contributor

Did some research with @lispyclouds to how argparse (Python) does stuff:

import argparse


parser = argparse.ArgumentParser()

subparsers1 = parser.add_subparsers(dest="which_parser_l1")

sub1 = subparsers1.add_parser("sub1")
sub1.add_argument("--bar")

subparsers2 = sub1.add_subparsers(dest="which_parser_l2")
sub2 = subparsers2.add_parser("sub2")
sub2.add_argument("--dude")

res = parser.parse_args(["sub1", "--bar", "1", "sub2", "--dude", "2"])

This returns:

{'which_parser_l1': 'sub1', 'bar': '1', 'which_parser_l2': 'sub2', 'dude': '2'}

Since python dicts are ordered (by insertion), you can retrieve back the information which subcommand got which options.
I think this would be nice information to have.
Why else would you not pass all options last if you're not going to be interested in this?

@borkdude
Copy link
Contributor

borkdude commented Jan 8, 2024

Superseded

@borkdude borkdude closed this Jan 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants