Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

moved over clargon and README

  • Loading branch information...
commit 33995975a5e3c4fb8408da5252b45f7dbc09465d 1 parent 9a27eeb
@gar3thjon3s gar3thjon3s authored
View
64 README.md
@@ -1,6 +1,68 @@
# tools.cli
-Future home of clojure.contrib.command-line.
+Clargon is a Command Line ARG parser...ON. An attempt at creating a
+OptParser-like thing for clojure, but with the added bonus of nested
+groups of arguments.
+
+## Usage
+
+Example:
+
+ (clargon args
+ (required ["-p" "--port" "Listen on this port"] #(Integer. %))
+ (optional ["--host" "The hostname" :default "localhost"])
+ (optional ["--verbose" :default true])
+ (optional ["--log-directory" :default "/some/path"])
+ (group "--server"
+ (optional ["-n" "--name"])
+ (optional ["-p" "--port"] #(Integer. %))
+ (group "--paths"
+ (optional ["--inbound" :default "/tmp/inbound"])
+ (optional ["--outbound" :default "/tmp/outbound"]))))
+
+with args of:
+
+ '("-p" "8080"
+ "--no-verbose"
+ "--log-directory" "/tmp"
+ "--server--name" "localhost"
+ "--server--port" "9090"
+ "--server--paths--inbound" "/dev/null")
+
+will produce a clojure map with the names picked out for you as keywords:
+
+ {:port 8080
+ :host "localhost"
+ :verbose false
+ :log-directory "/tmp"
+ :server {:name "localhost"
+ :port 9090
+ :paths {:inbound "/dev/null"
+ :outbound "/tmp/outbound"}}}
+
+A flag of -h or --help is provided which will currently give a
+documentation string:
+
+ Usage:
+
+ Switches Default Required Desc
+ -------- ------- -------- ----
+ -p, --port Yes Listen on this port
+ --host localhost No The hostname
+ --verbose true No
+ --log-directory /some/path No /some/path
+ --server-n, --server--name No
+ --server-p, --server--port No
+ --server--paths--inbound /tmp/inbound No /tmp/inbound
+ --server--paths--outbound /tmp/outbound No /tmp/outbound
+
+Required parameters will halt program execution if not provided,
+optionals will not. Defaults can be provided as shown above. Errors
+caused by parsing functions (such as #(Integer. %) above) will halt
+program execution. Doc strings are provided as shown above on port and
+host.
+
+See the tests for more example usage.
## License
View
144 src/main/clojure/clojure/tools/cli.clj
@@ -0,0 +1,144 @@
+(ns clojure.tools.cli
+ #^{:author "Gareth Jones"
+ :doc ""}
+ (:use [clojure.string :only (replace)]
+ [clojure.pprint :only (pprint cl-format)])
+ (:refer-clojure :exclude [replace]))
+
+;; help message stuff
+
+(defn build-doc [{:keys [switches docs options]}]
+ [(apply str (interpose ", " switches))
+ (or (str (options :default)) "")
+ (if (options :required) "Yes" "No")
+ (or docs "")])
+
+(defn show-help [specs]
+ (println "Usage:")
+ (println)
+ (let [docs (into (map build-doc specs)
+ [["--------" "-------" "--------" "----"]
+ ["Switches" "Default" "Required" "Desc"]])
+ max-cols (->> (for [d docs] (map count d))
+ (apply map (fn [& c] (apply vector c)))
+ (map #(apply max %)))
+ vs (for [d docs]
+ (mapcat (fn [& x] (apply vector x)) max-cols d))]
+ (doseq [v vs]
+ (cl-format true "~{ ~vA ~vA ~vA ~vA ~}" v)
+ (prn))))
+
+;; option parsing
+
+(defn print-and-fail [msg]
+ (println msg)
+ (System/exit 1))
+
+(defn help-and-quit [specs]
+ (show-help specs)
+ (System/exit 0))
+
+(defn name-for [k]
+ (replace k #"^--no-|^--|^-" ""))
+
+(defn flag-for [v]
+ (not (.startsWith v "--no-")))
+
+(defn opt? [x]
+ (.startsWith x "-"))
+
+(defn strip-parents [alias]
+ (last (.split alias "--")))
+
+(defn path-for [alias]
+ (map keyword (.split alias "--")))
+
+(defn parse-args [args]
+ (into {}
+ (map (fn [[k v]]
+ (if (and (opt? k) (or (nil? v) (opt? v)))
+ [(name-for k) (flag-for k)]
+ [(name-for k) v]))
+ (filter (fn [[k v]] (opt? k))
+ (partition-all 2 1 args)))))
+
+(defn parse-spec [spec args]
+ (let [{:keys [parse-fn aliases options path]} spec
+ raw (->> (map #(args %) aliases)
+ (remove nil?)
+ (first))
+ raw (if (nil? raw)
+ (:default options)
+ raw)]
+ (if (and (nil? raw)
+ (:required options))
+ (print-and-fail (str (last aliases) " is a required parameter"))
+ (try
+ [path (parse-fn raw)]
+ (catch Exception _
+ (print-and-fail (str "could not parse " (last aliases) " with value of " raw)))))))
+
+(defn optional
+ "Generates a function for parsing optional arguments. Params is a
+ vector containing string aliases for the argument (from less to more
+ specific), a doc string, and optionally a default value prefixed
+ by :default.
+
+ Parse-fn is an optional parsing function for converting
+ a string value into something more useful.
+
+ Example:
+
+ (optional [\"-p\" \"--port\"
+ \"Listen for connections on this port\"
+ :default 8080]
+ #(Integer. %))"
+ [params & [parse-fn]]
+ (fn [parent args]
+ (let [parse-fn (or parse-fn identity)
+ options (apply hash-map (drop-while string? params))
+ switches (->> (take-while #(and (string? %) (opt? %)) params)
+ (map #(str parent %)))
+ aliases (map name-for switches)
+ docs (first (filter #(and (string? %) (not (opt? %))) params))
+ name (or (options :name)
+ (strip-parents (last aliases)))
+ path (path-for (last aliases))]
+ {:parse-fn parse-fn
+ :options options
+ :aliases aliases
+ :switches switches
+ :docs docs
+ :name name
+ :path path})))
+
+(defn required
+ "Generates a function for parsing required arguments. Takes same
+ parameters as 'optional'. Not providing this argument to clargon
+ will cause an error to be printed and program execution to be
+ halted."
+ [params & [parse-fn]]
+ (optional (into params [:required true]) parse-fn))
+
+(defn group
+ "Generates a function for parsing a named group of 'optional' and
+ 'required' arguments."
+ [name & spec-fns]
+ (fn [parent args]
+ (let [full-name (if (empty? parent)
+ name
+ (str parent name))]
+ (map #(% full-name args) spec-fns))))
+
+(defn clargon
+ "Takes a list of args from the command line and applies the spec-fns
+ to generate a map of options.
+
+ Spec-fns are calls to 'optional', 'required', and 'group'."
+ [args & spec-fns]
+ (let [args (parse-args args)
+ specs (flatten (map #(% "" args) spec-fns))]
+ (if (some #(contains? #{"h" "help"} %) (keys args))
+ (help-and-quit specs)
+ (reduce (fn [h [path value]] (assoc-in h path value))
+ {} (map #(parse-spec % args) specs)))))
View
112 src/test/clojure/clojure/tools/cli_test.clj
@@ -0,0 +1,112 @@
+(ns clojure.tools.cli-test
+ (:use [clojure.tools.cli] :reload)
+ (:use [clojure.test]))
+
+(testing "help"
+
+ (deftest should-print-help-shortform
+ (let [help-called (atom nil)]
+ (binding [help-and-quit (partial reset! help-called)]
+ (clargon '("-h") (optional ["--port"]))
+ (is (not (nil? @help-called))))))
+
+ (deftest should-print-help-longform
+ (let [help-called (atom nil)]
+ (binding [help-and-quit (partial reset! help-called)]
+ (clargon '("--help") (optional ["--port"]))
+ (is (not (nil? @help-called)))))))
+
+(testing "syntax"
+
+ (deftest should-handle-simple-strings
+ (is (= {:host "localhost"}
+ (clargon '("--host" "localhost") (optional ["--host"])))))
+
+ (testing "booleans"
+ (deftest should-handle-trues
+ (is (= {:verbose true}
+ (clargon '("--verbose") (optional ["--verbose"])))))
+
+ (deftest should-handle-falses
+ (is (= {:verbose false}
+ (clargon '("--no-verbose") (optional ["--verbose"]))))))
+
+ (testing "default values"
+ (deftest should-default-when-no-value
+ (is (= {:server "10.0.1.10"}
+ (clargon '() (optional ["--server" :default "10.0.1.10"])))))
+ (deftest should-override-when-supplied
+ (is (= {:server "127.0.0.1"}
+ (clargon '("--server" "127.0.0.1") (optional ["--server" :default "10.0.1.10"]))))))
+
+ (deftest should-apply-parse-fn
+ (is (= {:names ["john" "jeff" "steve"]}
+ (clargon '("--names" "john,jeff,steve")
+ (optional ["--names"] #(vec (.split % ",")))))))
+
+ (testing "aliases"
+ (deftest should-support-multiple-aliases
+ (is (= {:server "localhost"}
+ (clargon '("-s" "localhost")
+ (optional ["-s" "--server"])))))
+ (deftest should-use-last-alias-provided-as-name-in-map
+ (is (= {:sizzle "localhost"}
+ (clargon '("-s" "localhost")
+ (optional ["-s" "--server" "--sizzle"]))))))
+
+ (testing "required"
+ (deftest should-succeed-when-provided
+ (clargon '("--server" "localhost")
+ (required ["--server"])))
+ (deftest should-fail-when-not-provided
+ (try
+ (binding [print-and-fail (fn [x] (throw (Exception. "success!")))]
+ (is (thrown-with-msg? Exception #"success!"
+ (clargon '() (required ["--server"]))))))))
+
+ (testing "grouped parameters"
+ (deftest should-support-groups
+ (is (= {:server {:name "localhost"
+ :port 9090}}
+ (clargon '("--server--name" "localhost" "--server--port" "9090")
+ (group "--server"
+ (optional ["--name"])
+ (optional ["--port"] #(Integer. %)))))))
+ (deftest should-support-nested-groups
+ (is (= {:servers {:client {:host {:name "localhost" :port 1234}}}}
+ (clargon '("--servers--client--host--name" "localhost")
+ (group "--servers"
+ (group "--client"
+ (group "--host"
+ (optional ["--name"])
+ (optional ["--port" :default 1234]))))))))))
+
+(deftest all-together-now
+ (is (= {:port 8080
+ :host "localhost"
+ :verbose false
+ :log-directory "/tmp"
+ :server {:name "localhost"
+ :port 9090
+ :paths {:inbound "/dev/null"
+ :outbound "/tmp/outbound"}}}
+ (clargon '("-p" "8080"
+ "--no-verbose"
+ "--log-directory" "/tmp"
+ "--server--name" "localhost"
+ "--server--port" "9090"
+ "--server--paths--inbound" "/dev/null")
+ (required ["-p" "--port"] #(Integer. %))
+ (optional ["--host" :default "localhost"])
+ (optional ["--verbose" :default true])
+ (optional ["--log-directory" :default "/some/path"])
+ (group "--server"
+ (optional ["-n" "--name"])
+ (optional ["-p" "--port"] #(Integer. %))
+ (group "--paths"
+ (optional ["--inbound" :default "/tmp/inbound"])
+ (optional ["--outbound" :default "/tmp/outbound"])))))))
+
+
+
+
Please sign in to comment.
Something went wrong with that request. Please try again.