Permalink
Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.
Sign up
Fetching contributors…
Cannot retrieve contributors at this time.
Cannot retrieve contributors at this time
| (ns boot.cli | |
| (:refer-clojure :exclude [assert]) | |
| (:require | |
| [boot.util :as util] | |
| [clojure.java.io :as io] | |
| [clojure.string :as string] | |
| [clojure.pprint :as pprint] | |
| [boot.from.clojure.tools.cli :as cli])) | |
| (def ^:private A-Z? | |
| (->> (range (int \A) (inc (int \Z))) (map char) set (partial contains?))) | |
| (defn- depunc [s] (string/replace s #"\.$" "")) | |
| (defn- decap [s] (apply str (string/lower-case (first s)) (rest s))) | |
| (defn- indent [n] #(string/replace % #"\n" (apply str "\n" (repeat n " ")))) | |
| (defn- rm-lines [n] #(->> (string/split % #"\n") (drop n) (string/join "\n"))) | |
| (defn- parse-atom [type] | |
| (case type | |
| int read-string | |
| float read-string | |
| str (fnil identity "") | |
| kw keyword | |
| sym symbol | |
| char first | |
| bool identity | |
| edn (fnil read-string "nil") | |
| regex (fnil re-pattern "") | |
| code (fnil (comp eval read-string) "nil") | |
| file io/file)) | |
| (defn- assert-atom [type] | |
| (case type | |
| int integer? | |
| float float? | |
| str string? | |
| kw keyword? | |
| sym symbol? | |
| char char? | |
| bool #(contains? #{true false} %) | |
| edn (constantly true) | |
| regex #(instance? java.util.regex.Pattern %) | |
| code (constantly true) | |
| file #(instance? java.io.File %))) | |
| (defn- parse-fn | |
| "Return a function accepting arguments that are split according to optarg. | |
| The function works also for optargs splitting: | |
| ((parse-fn SYM:VER=PATH) SYM:VER=PATH) => [SYM VER PATH] | |
| But the typical usage is: | |
| ((parse-fn SYM:VER=PATH) \"prj:1.3=parent\") => [prj 1.3 parent]" | |
| [optarg] | |
| (fn [arg] | |
| (let [chars (->> optarg str (remove A-Z?))] | |
| (if-not (seq chars) | |
| arg | |
| (loop [ret [], arg arg, [c & chars] chars] | |
| (if-not c | |
| (conj ret arg) | |
| (let [splitter (re-pattern (str "(?<!\\\\)" c)) | |
| cleaner (re-pattern (str "\\\\" c)) | |
| [nxt arg] (string/split arg (re-pattern (str "(?<!\\\\)" c)) 2) | |
| nxt (string/replace nxt cleaner (str c))] | |
| (recur (conj ret nxt) arg chars)))))))) | |
| (defn- parse-type [type args] | |
| (if (symbol? type) | |
| ((parse-atom type) args) | |
| (mapv parse-type type args))) | |
| (defn- assert-type [type args] | |
| (if (symbol? type) | |
| ((assert-atom type) args) | |
| (every? identity (mapv assert-type type args)))) | |
| (defn- assoc-fn [optarg type] | |
| (fn [m k v] | |
| (let [flag? (not optarg) | |
| int-flag? (and flag? (= 'int type)) | |
| bool-flag? (and flag? (= 'bool type))] | |
| (cond | |
| bool-flag? (assoc m k v) | |
| int-flag? (update-in m [k] (fnil inc 0)) | |
| (symbol? type) (assoc m k (parse-type type v)) | |
| ;; [fix #707] Andrea Richiardi: not sure this is the right place for | |
| ;; this, because in theory we should handle the case of single vector | |
| ;; optargs up the chain. One of the checks is on the values though. | |
| (and (sequential? type) | |
| (sequential? v) | |
| (= type (flatten type))) | |
| (assoc m k (->> (interleave type v) | |
| (partition 2) | |
| (mapv #(parse-type (first %) (second %))))) | |
| (coll? type) (update-in m [k] (fnil conj (empty type)) (parse-type (first type) v)))))) | |
| (defn- deprecated [short] | |
| (:deprecated (meta short))) | |
| (defn- deprecated-doc [doc] | |
| (str "DEPRECATED: " doc)) | |
| (defn- format-doc [short optarg type doc] | |
| (let [atom? (symbol? type) | |
| flag? (not optarg) | |
| incr? (and flag? (= 'int type)) | |
| docstring (cond | |
| incr? (format "Increase %s" (decap doc)) | |
| flag? doc | |
| atom? (format "%s sets %s." optarg (depunc (decap doc))) | |
| sequential? (format "%s sets %s" (str optarg) (decap doc)) | |
| :else (let [f "Conj %s onto %s" | |
| v ((parse-fn optarg) (str optarg))] | |
| (format f (if (string? v) v (pr-str (mapv symbol v))) (decap doc))))] | |
| (cond-> docstring | |
| (deprecated short) deprecated-doc))) | |
| (defn- argspec->cli-argspec | |
| ([short long type doc] | |
| (argspec->cli-argspec short long nil type doc)) | |
| ([short long optarg type doc] | |
| (let [doc (if-not (empty? doc) doc (format "The %s option." long))] | |
| ((fnil into []) | |
| (when short [:short-opt (str "-" short)]) | |
| [:id (keyword long) | |
| :long-opt (str "--" long) | |
| :required (when optarg (str optarg)) | |
| :desc (format-doc short optarg type doc) | |
| :parse-fn `(#'parse-fn ~(when optarg (list 'quote optarg))) | |
| :assoc-fn `(#'assoc-fn ~(when optarg (list 'quote optarg)) '~type)])))) | |
| (defn- argspec->assert | |
| ([short long type doc] | |
| (argspec->assert short long nil type doc)) | |
| ([short long optarg type doc] | |
| (if (:! (meta type)) | |
| nil | |
| `(when-not (or (nil? ~long) (#'assert-type '~type ~long)) | |
| (throw (IllegalArgumentException. | |
| ~(format "option :%s must be of type %s" long type))))))) | |
| (defn- argspec->deprecation-warning | |
| ([short long type doc] | |
| (argspec->deprecation-warning short long nil type doc)) | |
| ([short long optarg type doc] | |
| (if-let [deprecated (deprecated short)] | |
| `(when-not (nil? ~long) | |
| (util/warn-deprecated ~(format "option %s is deprecated. %s\n" long (if (string? deprecated) deprecated ""))))))) | |
| (defn- argspec->summary | |
| ([short long type doc] | |
| (argspec->summary short long nil type doc)) | |
| ([short long optarg type doc] | |
| [(str ":" long) (str (when (:! (meta type)) "^:! ")) (str type) (cond-> doc (deprecated short) deprecated-doc)])) | |
| (defn- argspec-seq [args] | |
| (when (seq args) | |
| (let [[[short long & optarg-type] [doc & more]] | |
| (split-with (complement string?) args) | |
| ret (into [(when (not= short '_) short) long] optarg-type)] | |
| (cons (conj (vec ret) doc) (when (seq more) (argspec-seq more)))))) | |
| (defn- cli-argspec->bindings [argspec] | |
| (->> (cli/compile-option-specs argspec) | |
| (mapv (comp symbol name :id)) | |
| (assoc {:as '*opts*} :keys))) | |
| (defn- format-lines [lens parts] | |
| (->> parts (mapv #(->> (interleave lens %) | |
| (pprint/cl-format nil "~{ ~vA ~vA~vA ~vA~}") | |
| string/trimr)))) | |
| (defn- clj-summary [argspecs] | |
| (let [parts (mapv (partial apply argspec->summary) argspecs) | |
| lens (apply map #(apply max (map count %&)) parts)] | |
| (string/join "\n" (format-lines lens parts)))) | |
| (defn- cli-summary [argspecs] | |
| (let [cli-args (mapv (partial apply argspec->cli-argspec) argspecs)] | |
| (:summary (cli/parse-opts [] cli-args)))) | |
| (defn- split-args [args] | |
| (loop [split {} [arg & more] args] | |
| (cond | |
| (nil? arg) split | |
| (not (keyword? arg)) (recur (update-in split [:cli] (fnil conj []) arg) more) | |
| (empty? more) (update-in split [:errors] (fnil conj []) | |
| (str "no value supplied for option " arg)) | |
| :else (recur (assoc-in split [:kw arg] (first more)) (rest more))))) | |
| (defmacro ^:private assert | |
| [test fmt & args] | |
| `(when-not ~test (util/warn (apply format ~fmt (map pr-str [~@args]))))) | |
| (defn- assert-argspecs [argspecs] | |
| (let [split (->> argspecs (partition-by string?)) | |
| specs (->> split (partition 2)) | |
| dupes (->> (mapcat (partial take 2) split) | |
| (filter symbol?) | |
| (group-by identity) | |
| (reduce-kv #(if (< (count %3) 2) %1 (conj %1 %2)) []))] | |
| (doseq [[[short long optarg type & extra] [desc]] specs] | |
| (assert (and (not= 'h short) (not= 'help long)) | |
| "cli: the -h/--help option is reserved\n") | |
| (assert ((some-fn nil? (every-pred symbol? #(= 1 (count (name %))))) short) | |
| "cli: expected short option, got %s\n" short) | |
| (assert (symbol? long) | |
| "cli: expected long option, got %s\n" long) | |
| (assert (nil? (seq extra)) | |
| "cli: option %s: expected description, got %s\n" long (first extra)) | |
| (assert (or (and type (symbol? optarg)) | |
| (#{'int 'bool} optarg)) | |
| "cli: option %s: expected optarg, got %s\n" long (or optarg desc)) | |
| (assert (or (not type) (not= 'bool type)) | |
| "cli: option %s: flags should not have optargs, got %s\n" long optarg)) | |
| (assert (not (seq dupes)) | |
| (format "cli: options must be unique: %s\n" (string/join ", " dupes))))) | |
| (defn- separate-cli-opts | |
| "Given and argv and a tools.cli type argspec spec, returns a vector of the | |
| parsed option map and a list of remaining non-option arguments. This is how | |
| tasks in a pipeline created on the cli are separated into individual tasks | |
| and task options." | |
| [argv spec] | |
| (loop [opts [] [car & cdr :as argv] argv] | |
| (if-not car | |
| [opts argv] | |
| (let [opts* (conj opts car) | |
| parsd (cli/parse-opts opts* spec :in-order true)] | |
| (if (seq (:arguments parsd)) [opts argv] (recur opts* cdr)))))) | |
| (defmacro clifn [& forms] | |
| (let [[doc argspecs & body] | |
| (if (string? (first forms)) | |
| forms | |
| (list* "No description provided." forms)) | |
| _ (when-not (vector? argspecs) | |
| (throw (IllegalArgumentException. | |
| (format "Parameter declaration should be a vector: %s" argspecs)))) | |
| argspecs (argspec-seq argspecs)] | |
| (assert-argspecs (mapcat identity argspecs)) | |
| (let [doc (string/replace doc #"\n " "\n") | |
| helpspec '[h help bool "Print this help info."] | |
| argspecs (cons helpspec argspecs) | |
| cli-args (mapv (partial apply argspec->cli-argspec) argspecs) | |
| bindings (cli-argspec->bindings cli-args) | |
| arglists (list 'list (list 'quote ['& bindings])) | |
| cli-doc (format "%s\n\nOptions:\n%s\n" doc (cli-summary argspecs)) | |
| clj-doc (format "%s\n\nKeyword Args:\n%s\n" doc (clj-summary argspecs)) | |
| varmeta {:doc clj-doc :arglists arglists :argspec cli-args}] | |
| `(-> (fn [& args#] | |
| (let [split# (#'split-args args#) | |
| [opts# args#] (#'separate-cli-opts (:cli split#) ~cli-args) | |
| parsed# (cli/parse-opts opts# ~cli-args) | |
| ~bindings (merge (:kw split#) (:options parsed#)) | |
| ~'*args* args# | |
| ~'*usage* #(print ~cli-doc)] | |
| (when-let [e# (seq (mapcat :errors [split# parsed#]))] | |
| (throw (IllegalArgumentException. (string/join "\n" e#)))) | |
| ~@(mapv (partial apply argspec->assert) argspecs) | |
| ~@(mapv (partial apply argspec->deprecation-warning) argspecs) | |
| (if-not ~'help (do ~@body) (~'*usage*)))) | |
| (with-meta ~varmeta))))) | |
| (defmacro defclifn [sym & forms] | |
| (let [no-doc-string-forms (if (string? (first forms)) | |
| (rest forms) | |
| forms)] | |
| (if (and (list? (first no-doc-string-forms)) | |
| (vector? (first (first no-doc-string-forms)))) | |
| (throw (IllegalArgumentException. "Multiple arity format not supported for tasks. Use single arity format. Ex (deftask build [x y ...] ;commands)")))) | |
| `(let [var# (def ~sym (clifn ~@forms)) | |
| fmtdoc# (comp string/trim (#'indent 2)) | |
| meta# (update-in (meta ~sym) [:doc] fmtdoc#)] | |
| (doto var# (alter-meta! (fnil merge {}) meta#)))) |