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 .


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

查看所有标签

猜你喜欢:

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

How to Design Programs

How to Design Programs

Matthias Felleisen、Robert Bruce Findler、Matthew Flatt、Shriram Krishnamurthi / The MIT Press / 2001-2-12 / 71.00美元

This introduction to programming places computer science in the core of a liberal arts education. Unlike other introductory books, it focuses on the program design process. This approach fosters a var......一起来看看 《How to Design Programs》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

随机密码生成器
随机密码生成器

多种字符组合密码

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具