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

defclifn subcommands #346

Closed
daveyarwood opened this issue Nov 21, 2015 · 7 comments
Closed

defclifn subcommands #346

daveyarwood opened this issue Nov 21, 2015 · 7 comments

Comments

@daveyarwood
Copy link
Member

I've been thinking a lot about how to build robust Clojure command-line apps that have a hierarchy of commands and subcommands.

clojure.tools.cli has a way to do this, but it's not ideal, IMHO, because it stops parsing once it gets to an argument it doesn't recognize, so it's up to you to call parse-opts again to handle parsing the subcommands.

I found another thing called cliopatra that supports both option-binding and standalone argument-binding, which I think could be used to do what I'm describing, but I think this could still be made more intuitive, and it still relies on clojure.tools.cli, which is inferior to Boot's defclifn IMHO.

defclifn is the most elegant and concise way that I've seen to represent a CLI command as a function. I think it would be extra elegant if we added support for delegating unrecognized arguments to subcommands, which would also just be functions (probably defined using defclifn, in most cases). I'd be happy to try my hand at a PR for this, but I'd like to get some feedback on the idea first.

I'm imagining being able to do something like this:

(defclifn log
  "Show commit history."
  [p pretty FSTR str  "The format string for pretty-printing."
   g graph       bool "Display an ASCII graph of branch/merge history."]
  (do-some-stuff-using-opts))

(defclifn -main
  "Top-level git command."
  [d git-dir PATH str "The path to the .git directory (default: `cwd`/.git)"]
  {:subcommands [log add commit push]} ; etc.
  (when git-dir
    (set-git-dir git-dir))
  (when log
    (do-subcommand log)))

The idea would be that I could then call my main function from the command line (let's say this is a boot script called git) and give the main function both task options and subcommands to perform:

./git --git-dir /path/to/.git log --graph --pretty=oneline

I'm not sure yet how this might translate into REPL usage... maybe something like this?

(-main :git-dir "/path/to/.git"
       :subcommands [log :graph true :pretty "oneline"])

Re: do-subcommand above: I'm thinking this could be a built-in symbol that is available only within a defclifn form, so it has the subcommand's parsed options as a context for executing the subcommand. There could also be a do-subcommands, which can take 0 or more subcommands as arguments and execute each of them (using their parsed options) in the order provided at the command-line. The 0 arity could just execute all of the subcommands the user included. I feel iffy about making defclifn an anaphoric macro though, and I'm open to other approaches.

Anyway, thoughts?

@micha
Copy link
Contributor

micha commented Nov 21, 2015

Boot implements subcommands via a pipeline, which is I think the way you want to do it. The reason why there are commands and subcommands is so the preceeding things can prepare context for the things that follow. With boot the command line and REPL correspond naturally---subcommand just means function composition.

@daveyarwood
Copy link
Member Author

Oh, interesting. So maybe what we want is to make functions defined using defclifn composable? (Are they already?)

@micha
Copy link
Contributor

micha commented Nov 21, 2015

They're functions, so you can compose them, no problem there :)

Check out the deftask macro, it seems like this is what you want: https://github.com/boot-clj/boot/blob/master/boot/core/src/boot/core.clj#L646-L664

@micha
Copy link
Contributor

micha commented Nov 21, 2015

Why not just use tasks? Those are "subcommands", no?

@daveyarwood
Copy link
Member Author

Yeah, tasks could definitely work for what I'm trying to do. I've never thought about tasks as things that could be exposed to the user, though -- can you define tasks using deftask and then expose them via the -main function?

What got me thinking about this is Alda's CLI interface. I have a bunch of tasks defined using defclifn, and a -main function which manually looks at the first CLI argument and matches it to one of those tasks. So I'm wondering if there is a way to define -main as a composable task that will delegate its arguments to one of the subtasks, e.g. alda play --file my-file.alda will dispatch to the play task, passing along the --file option.

@micha micha closed this as completed in 2e1753d Dec 19, 2015
@micha
Copy link
Contributor

micha commented Dec 19, 2015

I think 2.5.2-SNAPSHOT fixes some issues.

Consider this boot script (call it foop):

#!/usr/bin/env boot
;; vim: set ft=clojure:

(use 'boot.cli 'boot.util)

(defn dosub
  [[sub & args]]
  (if-let [subfn (guard (resolve (symbol sub)))]
    (apply subfn args)
    (throw (Exception. (format "no such subcommand: %s" sub)))))

(defclifn -main
  [a aarp bool "The aarp option."]
  (prn :main :opts *opts* :args *args*)
  (some-> *args* seq dosub))

(defclifn sub1
  [b barp bool "The barp option."]
  (prn :sub1 :opts *opts* :args *args*)
  (some-> *args* seq dosub))

(defclifn sub2
  [c carp bool "The carp option."]
  (prn :sub2 :opts *opts* :args *args*)
  (some-> *args* seq dosub))

I can now do:

$ ./foop -a
:main :opts {:aarp true} :args nil
$ ./foop -a sub1 -b
:main :opts {:aarp true} :args ("sub1" "-b")
:sub1 :opts {:barp true} :args nil
$ ./foop -a sub1 -b sub2 -c
:main :opts {:aarp true} :args ("sub1" "-b" "sub2" "-c")
:sub1 :opts {:barp true} :args ("sub2" "-c")
:sub2 :opts {:carp true} :args nil

Can you please set BOOT_VERSION=2.5.2-SNAPSHOT in your boot.properties file and see if this does what you were wanting to do?

@micha micha reopened this Dec 19, 2015
@daveyarwood
Copy link
Member Author

This is awesome! Exactly what I had in mind. Thanks! 👍

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

No branches or pull requests

2 participants