Alternative to tools.cli in 10 lines of code

栏目: IT技术 · 发布时间: 4年前

内容简介:Let me start with what I think command line interface to a program should do:For its purpose, writing command line entry point is too damn verbose: parsing args, performing validation, providing defaults — all this is a boring busywork. There are libraries

Let me start with what I think command line interface to a program should do:

  • it should teach its user how to use it using by invoking it with some well-known argument such as -h or --help ;
  • it should run a program, optionally with some arguments.

Status quo

For its purpose, writing command line entry point is too damn verbose: parsing args, performing validation, providing defaults — all this is a boring busywork. There are libraries such as tools.cli that help with that, but if you look at it’s API, you will see yet another ad-hoc implementation of half of clojure spec: it’s a data-driven parser of a sequence of strings with schema, validations and defaults. Its benefit is conformance to GNU Program Argument Syntax Guidelines, but I would prefer to have an API that is simple and straightforward than having to remember what every letter means in the context of a particular command.

If only defining a command was as easy as defining a function… If only parsing arguments was as predictable as using clojure reader… Perhaps it’s possible? Without further ado, let’s jump straight into 10 lines of Clojure this post is about.

10 lines of Clojure this post is about

(defn -main [& opts]
  (let [f #(try
             (let [form (read-string %)]
               (cond
                 (qualified-symbol? form) @(requiring-resolve form)
                 (symbol? form) @((ns-publics (symbol (namespace `-main))) form)
                 :else form))
             (catch Exception _ %))
        [f & args] (map f opts)]
    (some-> (apply f args) prn)))

This entry point establishes a convention for invoking a program and parsing arguments that is a bit vague, but simple, straightforward and powerful. The convention: it is a function call in main ns with parens omitted. The important implication of this convention is that you learn both clojure and command line APIs at the same time. Here is how invocation of this CLI might look like:

$ clj -m cli foo :x bar :y true

And here is how using this ns from clojure might look like:

(cli/foo :x "bar" :y true)

Looks pretty similar, isn’t it? The actual implementation allows a bit more in one place and a bit less in another, but I think that’s fine. Let’s explore the possibilities and limitations!

Arguments, defaults, validation

With this entry point creating tasks is done by defining new functions. Let’s start with a simple task that we will use to see how arguments are parsed:

(ns cli)

(defn echo [& args]
  (apply prn args))

(defn -main [] ...)

Now lets invoke it:

$ clj -m cli echo :host 127.0.0.1 :port 8080 :async true
:host "127.0.0.1" :port 8080 :async true

As you can see, :host , :port and :async are keywords, true is a boolean, and 127.0.0.1 is a string. We can easily and consistently parse arguments to values that make sense and invoke a function! What about defaults and validation? Just use normal Clojure code to do both! Here is an example:

(defn ensure-connection
  "Ensure a connection to a network resource is possible

  Available options:
  - :host (optional, defaults to localhost) - target host
  - :port (required) - target port
  - :timeout (ms, optional, defaults to 10000) - connection timeout"
  [& {:keys [host port timeout]
      :or {host "localhost"
           timeout 10000}}]
  {:pre [(string? host) (int? port) (int? timeout)]}
  (doto (java.net.Socket.)
    (.connect (java.net.InetSocketAddress. ^String host ^int port) timeout)
    (.close))
  (println "Connection can be established"))

It certainly looks like a regular clojure function you might find in your code or someone’s library. Let’s try invoking it from the command line with required parameter missing:

$ clj -m cli ensure-connection
Execution error (AssertionError) at cli/ensure-connection (cli.clj:16).
Assert failed: (int? port)

Full report at:
/tmp/clojure-10136260048334273705.edn

Wonderful, we have validation! You can use spec or hand-written error messages to improve error reporting. Now let’s add missing parameter:

clj -m cli ensure-connection :port 443
Execution error (ConnectException) at sun.nio.ch.Net/pollConnect (Net.java:-2).
Connection refused

Full report at:
/tmp/clojure-1463050396010033872.edn

Exit code is 1, since I don’t have anything running on port 443. Overriding defaults is dead simple:

$ clj -m cli ensure-connection :port 443 :host google.com
Connection can be established

More power to the user

You may have noticed that this entry point resolves symbols. One unintended consequence is that this CLI allows invoking any function by specifying its fully qualified symbol:

$ clj -m cli clojure.core/prn :woot
:woot

While this particular behavior is certainly not intended and can be restricted with a bit more code, the ability to supply symbols is extremely useful for improving expressivity available to this CLI: it supports any def-ed value as an argument! Suppose we write a custom repl that in its first iteration behaves exactly like clojure.main/repl :

(defn repl [& options]
  (apply clojure.main/repl options))

Invoking it from the command line supports all options expecting functions, so we can e.g. configure its printing behavior from the command line:

$ clj -m cli repl :print clojure.pprint/pprint
user=> (meta #'tap>)
{:arglists ([x]),
 :doc "sends x to any taps. Will not block. Returns true if there was room in the queue,\n  false if not (dropped).",
 :added "1.10",
 :line 7886,
 :column 1,
 :file "clojure/core.clj",
 :name tap>,
 :ns #object[clojure.lang.Namespace 0x30404dba "clojure.core"]}

Getting help

What about learning the API? First of all, it’s easy to create help command that prints function’s docstrings:

(defn help [f]
  (println (:doc (meta (resolve (symbol (Compiler/demunge (.getName (class f)))))))))

Your documentation in code is now available in the command line:

$ clj -m cli help ensure-connection
Tests a connection to a network resource.
  Available options:
  - :host (optional, defaults to localhost) - target host
  - :port (required) - target port
  - :timeout (ms, optional, defaults to 10000) - connection timeout

Now, what about well-known higher-level help using --help or -h ? Wait, aren’t those valid clojure symbols? Lets try defining those!

(defn --help []
  (println "Available commands:")
  (doseq [sym (sort (keys (dissoc (ns-publics (symbol (namespace `--help))) '-main)))]
    (println (str "  " sym)))
  (println "Use help <command> to see description of that command"))

(def -h --help)

Would that work? Yes it would!

$ clj -m cli --help
Available commands:
  --help
  -h
  echo
  ensure-connection
  help
  repl
Use help <command> to see description of that command

Limitations

This entry point does not evaluate S-expressions, instead forms are read as is:

$ clj -m cli echo "(if 1 :foo :bar)"
(if 1 :foo :bar)

I think this is a good thing: if you want to start writing code for such command line invocation, it’s a sign you should write a proper program. But now that you learned the command line API, you also learned the library API!

Summary

If you are willing to give up GNU Program Argument Syntax Guidelines for your command line entry point to a Clojure library, you might find yourself having a simple, concise, powerful and straightforward API that will make your code transparent to your users.

What do you think? Discuss on reddit .


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

CSS3实用指南

CSS3实用指南

吉伦瓦特 / 屈超、周志超 / 人民邮电出版社 / 2012-3 / 49.00元

CSS3为Web的视觉样式语言注入了强大的新功能,让设计人员更加轻松自如地设计优美而引人入胜的内容。借助CSS3,不使用图片就可以创建半透明背 景、渐变、阴影等夺人眼球的视觉效果;还可以使用漂亮、独特、非Web安全的字体显示文本;不用Flash就可以创建动画;不用JavaScript就可 以定制适应用户的设备和屏幕尺寸的设计。 本书通过一系列实用且新颖的范例,向读者展示如何实现以上功能和更多......一起来看看 《CSS3实用指南》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

MD5 加密
MD5 加密

MD5 加密工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具