在 Clojure 中构建 CLI 脚本



在Clojure中构建CLI脚本的常见/标准方法是什么?

在我看来,这种方法应该包括以下特征:

  • 一种轻松处理争论的方法,stdin/out/err。

  • 无需花费太多时间来启动(理想情况下具有某种 JIT),否则就会失去在 shell 中将东西一起破解的目的。

  • 此外,期望一种简单的方法可以在不设置项目(可能全局安装它们)的情况下包含一次性依赖项,这是合理的。

理想情况下,提供解决方案用法的简单示例将不胜感激。有点等同于:

#!/bin/bash
echo "$@"
cat /dev/stdin

注意:我知道这个问题之前在这里受到过一些质疑。但这个问题是不完整的,答案也没有达成共识,似乎不存在很大一部分解决方案。

现在有了新的CLI工具,可以在不使用第三方工具的情况下创建一个独立的Clojure脚本。安装clj命令行工具后,像下面这样的脚本应该就可以工作了。

就原始问题而言,这可以与任何Clojure/JVM CLI程序一样好,具体取决于您:require的库。我还没有对它进行基准测试,所以我不会评论性能,但如果它让你担心,那么请自己试验一下,看看启动时间是否可以接受。我想说这在依赖管理方面得分很高,因为脚本是完全独立的(除了现在无论如何都推荐运行 Clojure 的clj工具)。

文件:~/bin/script.sh

#!/bin/sh
"exec" "clj" "-Sdeps" "{:deps,{hiccup,{:mvn/version,"1.0.5"}}}" "$0" "$@"
(ns my-script
(:require
[hiccup.core :as hiccup]))
(println
(hiccup/html
[:div
[:span "Command line args: " (clojure.string/join ", " *command-line-args*)]
[:span "Stdin: " (read-line)]]))

然后确保它是可执行的:

$ chmod +x ~/bin/script.sh

并运行它:

$ echo "stdin" | script.sh command line args
<div><span>Command line args: command, line, args</span><span>Stdin: stdin</span></div>

铌。这主要是一个 shell 脚本,它将第三行的字符串视为要执行的命令。随后的执行将使用给定的参数运行clj命令行工具,该工具将这些字符串评估为字符串(没有副作用),然后继续计算下面的 Clojure 代码。

另请注意,依赖项被指定为传递给第三行clj的映射。你可以在Clojure网站上阅读更多关于它是如何工作的。依赖关系图中的标记用逗号分隔,Clojure 将其视为空格,但大多数 shell 不会将其视为空格。

感谢"clojurians"Slack小组 #tools-deps频道上的好人,这个解决方案从何而来。

一个选项是运行在MacOS和Linux上的Planck。它使用自托管的ClojureScript,具有快速启动并针对JavaScriptCore。

它有一个很好的SDK,并模仿了Clojure的一些东西,而这些东西在ClojureScript中没有,例如planck.io类似于clojure.java.io.它支持通过tools.deps.alpha/deps.edn加载依赖项。

回显stdin非常简单:

(require '[planck.core :refer [*in* slurp]])
(print (slurp *in*))

并打印命令行参数:

(println *command-line-args*)

$ echo "foo" | planck stdin.cljs 1 2 3
foo
(1 2 3)

一个具有依赖项的独立脚本示例,即不是项目:Planck 中的tree命令行工具。

需要注意的是,Planck 不支持使用 npm 依赖项。因此,如果您需要这些,请选择针对NodeJS的Lumo。

第三种选择是小丑,它是用 Go 编写的 Clojure 解释器。

我知道您要求非项目创建方法来实现这一目标,但由于这个特定问题已经在我脑海中存在了很长一段时间,我想我会提出另一种选择。

TLDR:跳转到下面的"创建可执行 CLI 命令"部分

背景

我有与您前段时间几乎相同的要求列表,并登陆创建可执行 jar 文件。我不是在谈论通过java -jar myfile.jar执行,而是自包含的uber-jars,您可以像使用任何其他二进制文件一样直接执行。

如果您阅读 zip 文件规范(jar 文件所遵循的 jar 文件是 zip 文件),事实证明这实际上是可能的。简短的版本是您需要:

  • 用你需要的东西做一个胖罐
  • 在文件开头的二进制 jar 内容中插入 bash/bat/shell 脚本
  • chmod +xuber jar 文件(或者如果在 Windows 上,请选中可执行文件框)
  • 重写 JAR 文件元数据记录,以便插入的脚本文本不会使 zip 文件内部偏移失效

应该注意的是,这实际上是由 zip 文件规范支持的。这就是自解压 zip 文件等的工作方式,生成的胖 jar(在上述过程之后)仍然是有效的 jar 文件和有效的 zip 存档。所有相关命令(如java -jar)仍然有效,文件现在也可以直接从命令行执行。

此外,按照上述模式,还可以添加对滴灌jvm启动器之类的支持,这大大加快了cli脚本的启动时间。

事实证明,当我大约一年前开始研究这个问题时,重写 jar 文件元数据的最后一点的库并不存在。不仅在 clojure 中,而且在整个 JVM 上。这仍然让我大吃一惊:jvm 上所有语言的中央部署单元是 jar 文件,并且没有实际读取 jar 文件内部的库。内部就像实际的zip文件结构一样,而不仅仅是java的ZipFile和朋友所做的。

此外,我找不到一个用于clojure的库,它以干净的方式处理zip文件规范所需的二进制结构。

溶液:

  • octet 具有我认为可用于 clojure 的二进制库中最干净的接口,因此我为 octet 编写了一个拉取请求,添加了对 zip 文件规范所需功能的支持。
  • 然后,我创建了一个新的库 clj-zip-meta,它读取和解释 zip 文件元数据,并且能够进行上面最后一点中描述的偏移量重写。
  • 然后,我创建了一个对现有 clojure lib lein-binplus 的拉取请求,以添加对 clj-zip-meta 实现的 zip 元重写的支持,并添加对自定义前导码脚本的支持,以便能够在不需要java -jar的情况下创建真正的可执行 jar
  • 在所有这些之后,我创建了一个leiningen模板cli-cmd来支持创建cli命令项目,该项目支持上述所有花里胡哨的功能,并具有结构良好的命令行解析设置...或者我认为结构良好的:)。欢迎评论。

创建可执行 CLI 命令

因此,您可以使用leiningen创建一个新的命令行clojure应用程序,并使用以下命令运行它:

~> lein new cli-cmd mycmd
~> cd mycmd
~> lein bin 
Compiling mycmd.core
Compiling mycmd.core
Created /home/mbjarland/tmp/clj-cmd/mycmd/target/mycmd-0.1.0-SNAPSHOT.jar
Created /home/mbjarland/tmp/clj-cmd/mycmd/target/mycmd-0.1.0-SNAPSHOT-standalone.jar
Creating standalone executable: /home/mbjarland/tmp/clj-cmd/mycmd/target/mycmd
Re-aligning zip offsets
~> target/mycmd 
---- debug output, remove for production code ----
options    {:port 80, :hostname "localhost", :verbosity 0}
arguments  []
errors     nil
summary    
-p, --port PORT      80         Port number
-H, --hostname HOST  localhost  Remote host
--detach                    Detach from controlling process
-v                              Verbosity level; may be specified multiple times to increase value
-h, --help
--------------------------------------------------
This is my program. There are many like it, but this one is mine.
Usage: mycmd [options] action
Options:
-p, --port PORT      80         Port number
-H, --hostname HOST  localhost  Remote host
--detach                    Detach from controlling process
-v                              Verbosity level; may be specified multiple times to increase value
-h, --help
Actions:
start    Start a new server
stop     Stop an existing server
status   Print a server's status
Please refer to the manual page for more information.
Error: invalid action '' specified!   

命令的输出只是我添加到 leiningen 模板的样板示例命令行分析。

自定义前导码脚本位于boot/jar-preamble.sh,它支持滴灌。换句话说,如果你的路径上有滴水,生成的可执行文件会使用它,否则它会回退到内部启动 uber jar 的标准java -jar方式。

命令行解析的源代码和 cli 应用程序的代码正常存在于 src 目录下。

如果您想进行黑客攻击,可以更改前导码脚本并重新运行lein bin新的前导码将通过构建过程插入到可执行文件中。

另外应该注意的是,这种方法仍然java -jar幕后,所以你确实需要java在你的路径上。

Ayway,冗长的解释,但希望它对有这个问题的人有一些用处。

考虑Lumo,一个专门为脚本设计的ClojureScript环境。

请注意,虽然它同时支持 ClojureScript (JAR) 和 NPM 依赖项,但依赖项支持仍在开发中。

我编写了许多 Clojure (JVM) 脚本,并使用 CLI-matic 库 https://github.com/l3nz/cli-matic/来抽象大部分与命令行解析、创建和维护帮助、错误等相关的样板文件。

最新更新