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

Alternative to lein run that can pass CLI options as-is #374

Closed
alexander-yakushev opened this issue Dec 31, 2015 · 20 comments
Closed

Alternative to lein run that can pass CLI options as-is #374

alexander-yakushev opened this issue Dec 31, 2015 · 20 comments

Comments

@alexander-yakushev
Copy link
Contributor

I've seen this page which suggests that lein run translates to simply requiring and running the necessary function from the main namespace. I did that, but I also need to pass all the CLI arguments to -main method. Parsing the args on Boot side is not an option because I want to distribute the application later as an uberjar. At the same time, there is no easy way to deliver CLI args from Boot to -main without Boot processing them first. In the end, I had to add this hack to my build.boot:

(alter-var-root
 #'boot.core/construct-tasks
 (constantly
  (fn [& argv]
    (loop [ret [] [op-str & args] argv]
      (if-not op-str
        (apply comp (filter fn? ret))
        (let [op (-> op-str symbol resolve)]
          (when-not (and op (:boot.core/task (meta op)))
            (throw (IllegalArgumentException. (format "No such task (%s)" op-str))))
          (if (:raw-args (meta op))
            (recur (conj ret (apply (var-get op) (rest argv))) [])

            (let [spec   (:argspec (meta op))
                  parsed (boot.from.clojure.tools.cli/parse-opts args spec :in-order true)]
              (when (seq (:errors parsed))
                (throw (IllegalArgumentException. (clojure.string/join "\n" (:errors parsed)))))
              (let [[opts argv] (#'boot.core/parse-task-opts args spec)]
                (recur (conj ret (apply (var-get op) opts)) argv))))))))))

And subsequently make a task like this:

(defn ^:boot.core/task ^:raw-args run
  "Run the project."
  [& args]
  (with-pre-wrap fileset
    (require 'my.main.namespace)
    (eval `(my.main.namespace/-main ~@args))
    fileset)) 

Could we have something a little less hacky for this?

@micha
Copy link
Contributor

micha commented Dec 31, 2015

What about something like this:

(deftask run
  "Resolves a function and applies it to arguments."
  [a argv ARG [str] "The argument vector."
   m main SYM  sym  "The symbol to resolve to obtain the -main fn."]
  (with-pass-thru _
    (require (symbol (namespace main)))
    (eval `(~main ~@argv))))

Example usage:

boot run -m my.main.namespace/-main -a foo -a bar -a baz

@alexander-yakushev
Copy link
Contributor Author

That's a little too verbose for me. I pass a lot of arguments to main, putting -a in front of each would be tiresome and error-prone.

@seancorfield
Copy link
Contributor

What does Boot currently do with --? That's used in some programs as a way to separate "control" arguments from "free form" arguments. Could that or a similar flag be used to tell Boot about a list of free form arguments to pass as-is?

@alexander-yakushev
Copy link
Contributor Author

@seancorfield AFAIK boot uses -- to separate arguments to one task from other subsequent tasks.

@seancorfield
Copy link
Contributor

Does it? Seems redundant since boot task1 -a arg --flag task2 -b --more stuff task3 is already unambiguous.

@alexander-yakushev
Copy link
Contributor Author

At least it's like that in the docs. https://github.com/boot-clj/boot#build-from-the-command-line see third code example.

@seancorfield
Copy link
Contributor

"The -- args below are optional" -- my emphasis there. Right now -- is just whitespace, ignored. I wonder how many people actually use them like that tho'? If zero then we could reuse them to indicate raw arguments. Else we need to pick something similar but different.

@micha
Copy link
Contributor

micha commented Dec 31, 2015

Boot doesn't actually do anything with --, it's sort of like a comment. It's ignored during argument parsing if it appears in place of a task:

boot -- show -e -- repl -h # okay: -- ignored

but not when used as an argument:

boot -- show -- -e repl -h # error: -e is not a task

@micha
Copy link
Contributor

micha commented Dec 31, 2015

Currently we're using :argspecs metadata on the var to validate cli arguments. Perhaps if a task doesn't have this metadata key but does have :arglists we can parse that instead, and collect the correct number of arguments.

@seancorfield
Copy link
Contributor

Interesting. And then use -- as a way to signal "end of arguments" in the case of & args?

boot task1 -a regular -a args task2 any number of args -- task3 --back to --regular args

@micha
Copy link
Contributor

micha commented Dec 31, 2015

Another option to consider in the meantime:

(deftask run
  [e expr EXPR edn "The expression to evaluate."]
  (with-pass-thru _ (eval expr)))

example:

boot run -e '(-main foo bar baz)'

and:

(task-options!
  run {:expr '(-main foo bar baz)})

@micha
Copy link
Contributor

micha commented Dec 31, 2015

You could get more fancy from there, requiring namespaces, adding options for cleanup, etc.

@lachholden
Copy link

What I'm currently going with (similar to above):

(require 'namespace.core)
(require '[clojure.string :as s])

(deftask run
  "Runs namespace with the given arguments"
  [a args ARGS str "The arguments to pass the program"]
  (apply namespace.core/-main (s/split args #" ")))

Which can then be executed as boot run -a "the cli args to pass"

@alexander-yakushev
Copy link
Contributor Author

Thanks @lachy-xe and @micha. I'm still using my hack, but I'll probably rewrite it to one of your suggestions.

@micha
Copy link
Contributor

micha commented Aug 2, 2016

@alexander-yakushev I think I found a solution! How does this look to you?

# note the spaces around the brackets!
$ boot -v watch [ run -x arg1 arg2 ] deploy

In the command line above -x arg1 and arg2 will be passed to the run task. Within the
task body the positional arguments are accessible (as strings, of course) via *args*.

# brackets can be nested!
# - they're passed to the task as arguments
# - they must be balanced
$ boot -v watch [ run -x arg1 [ arg2 ] ] deploy

The task-options! macro would remain only for options, not for positional parameters.

@micha
Copy link
Contributor

micha commented Aug 2, 2016

The -- would continue to keep its current meaning, for example suppose you wanted to pass -w
as a positional parameter, not an option:

# pass -w as an argument, not an option
$ boot -v watch [ run -x -- -w arg1 arg2 ] deploy

And you can still use -- to separate tasks from each other in the pipeline:

$ boot -v -- watch -- [ run -x -- -w arg1 arg2 ] -- deploy

@micha micha closed this as completed in afd0a1a Aug 2, 2016
@micha micha removed the help wanted label Aug 2, 2016
@micha
Copy link
Contributor

micha commented Aug 2, 2016

Feel free to reopen if this isn't a good solution :)

@alexander-yakushev
Copy link
Contributor Author

It looks nice! How can I test it?

@daveyarwood
Copy link
Member

+1, I like this.

So just for clarification, could I define a run task that takes only positional args by doing something like this?

(deftask run
  []
  (let [[x y z] *args*]
    (prn :x x :y y :z z)))

Then, on the command line:

boot [ run oh hello there ]

would result in this?

:x "oh" :y "hello" :z "there"

@micha
Copy link
Contributor

micha commented Aug 2, 2016

@daveyarwood yep!

@alexander-yakushev this feature is in master branch now, if you want to build it. i'll try to release something on clojars though.

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

5 participants