在 Clojure 中创建 Linux 命令行工具

了解 leiningen 工具如何帮助您管理 Clojure 项目。

本文是对 Clojure 函数式编程语言的温和介绍,该语言基于 LISP,使用 Java JVM 并具有方便的 REPL。并且,由于 Clojure 基于 LISP,请准备好看到大量的括号!

安装 Clojure

您可以通过以 root 用户或使用 sudo 执行以下命令在 Debian Linux 机器上安装 Clojure


# apt-get install clojure

要查找您正在使用的 Clojure 版本非常简单,只需在 Clojure REPL 中执行以下命令之一,您可以通过运行 clojure 进入 REPL


# clojure
Clojure 1.8.0
user=> *clojure-version*
{:major 1, :minor 8, :incremental 0, :qualifier nil}
user=> (clojure-version)
"1.8.0"
user=> (println *clojure-version*)
{:major 1, :minor 8, :incremental 0, :qualifier nil}
nil

第一个命令使您进入 Clojure REPL,它显示 user=> 提示符并等待用户输入。其余三个应在 Clojure REPL 中执行的命令将生成相同的输出,在本例中,该输出显示正在使用 Clojure 版本 1.8.0。因此,如果您正在跟随学习,恭喜您!您刚刚运行了您的第一个 Clojure 代码!

leiningen 工具

在获得 Clojure 后,您应该做的第一件事是安装一个非常方便的名为 leiningen 的工具,这是在 Linux 机器上使用和管理 Clojure 项目的最简单方法。请按照 leiningen.org 上的说明进行操作,或使用您喜欢的软件包管理器在 Linux 机器上安装 leiningen。此外,如果您一直使用 Clojure 并处理大型 Clojure 项目,则像 JenkinsSemaphore 这样的工具将自动化您的构建和测试阶段,并为您节省大量时间。

安装 leiningen 后,使用 lein 命令(它是 leiningen 软件包的可执行文件的名称)创建一个名为 hw 的新项目


$ lein new hw
Generating a project called hw based on the 'default' template.
The default template is intended for library projects,
not applications. To see other templates (app, plugin, etc),
try `lein help new`.

前面的命令将创建一个名为 hw 的新目录,其中将包含文件和其他目录。您需要对某些项目文件进行一些更改才能执行项目。首先,您需要编辑可以在 hw 目录中找到的 project.clj,并使其如下所示


$ cat project.clj
(defproject hw "0.1.0-SNAPSHOT"
  :main hw.core
  :dependencies [[org.clojure/clojure "1.8.0"]])

然后,编辑 ./src/hw/core.clj 文件,使其如下所示


$ cat src/hw/core.clj
(ns hw.core)

(defn -main [& args]
  (println "Hello World!"))

./src/hw/core.clj 文件是您可以找到 Clojure 代码的地方。执行前面的项目就像在项目目录中运行 lein run 命令一样简单


$ lein run
Hello World!

第一次执行 lein run 时,lein 可能会自动下载构建项目所需的一些文件。此外,请记住,lein 可以做的事情远不止我在这里描述的这些。接下来最重要的 lein 命令是 lein clean,它清理 Clojure 项目,以及 lein repl,它启动 Clojure 控制台。

Clojure 数据类型

Clojure 的理念基于 Lisp,这意味着 Clojure 代码包含大量括号。此外,Clojure 是一种函数式和动态类型编程语言,它支持并发编程,这意味着 Clojure 的函数(尝试)没有副作用。由于 Clojure 的实现基于 Java 虚拟机,因此 Clojure 数据类型是 Java 数据类型,这意味着所有 Clojure 值实际上是对 Java 类的引用。此外,大多数 Clojure 数据类型是不可变的,这意味着它们在创建后无法更改。最后,Clojure 有一种不寻常的相等性检查方式。为了找出两个列表是否相同,Clojure 会检查这两个列表的实际值。大多数编程语言不会这样做,因为这可能是一个缓慢的过程,尤其是在处理大型列表时。然而,Clojure 通过为每个对象保留一个哈希值,并通过比较两个对象的哈希值而不是实际访问它们的所有值来避免这种风险。只要对象是不可变的,这就可以工作,因为如果对象是可变的并且其中一个对象发生更改,则其哈希表将不会更新以反映该更改。

总而言之,Clojure 支持数字、布尔值、字符、字符串、nil 值、函数变量、命名空间、符号、集合、关键字和 var。var 是可变的 Clojure 类型之一。集合可以是列表、哈希映射、向量或序列,但列表和哈希映射是最流行的 Clojure 数据类型。

但是,关于 Clojure 的介绍已经足够了;让我们开始编写真正的 Clojure 代码。

处理数据

首先,让我们看看如何在 Clojure 中定义和填充变量,以及如何使用 Clojure shell 访问列表的所有元素。Clojure 语法要求您以前缀方式放置运算符和函数,这意味着运算符放置在其参数之前,而不是参数之间(中缀)。简单来说,要计算 9 和 4 的和,您应该写 + 9 4 而不是 9 + 4。

与 Clojure shell 的交互从这里开始


user=> (- 10)
-10
user=> (- 10 10)
0
user=> (- 10 (+ 5 5) )
0
user=> (/ 10 (+ 5 5) )
1
user=> (println "Hello\nLinux Journal!")
Hello
Linux Journal!
nil
user=> (str "w12")
"w12"

这段 Clojure 代码首先对数字进行一些基本操作,然后对字符串进行操作。在第一个语句中,您可以看到 Clojure 中的所有内容都必须放在括号中。第三个数字运算等效于 10 - (5 + 5),等于零;而第四个数字运算等效于 10 / (5 + 5),等于 1。正如您在 Hello World 程序中已经看到的,println 函数用于在屏幕上打印数据;而 str 函数可以帮助您将任何内容(包括数字)转换为字符串。str 的一个很好的优点是,当使用多个参数调用它时,您可以使用它来连接字符串。

下一个交互验证了 Clojure 中的字符(写为 \a\b 等)不等同于长度为 1 的使用双引号的 Clojure 字符串。但是,当您使用 str 处理单个字符时,您会得到一个字符串


user=> (= \a "a")
false
user=> (= (str \a) "a")
true

现在,准备好迎接更高级的内容


user=> (map (fn [x] (.toUpperCase x)) (.split
 ↪"Hello Linux Journal!" " "))
("HELLO" "LINUX" "JOURNAL!")

前面的 Clojure 代码做了很多事情。它将其输入字符串拆分为单词,并将每个单词转换为大写——好处是,在 Clojure 中编写此语句的方式自然且易于阅读——只要您从右向左开始阅读它。

以下与 Clojure shell 的交互显示了如何使用 Clojure 映射,它(正如您可能期望的那样)将键与值关联起来


user=> (def myMap {:name "Mihalis"
:surname "Tsoukalos"
:livesAt {:country "Greece"
:city "Athens" } } )
#'user/myMap

首先,您创建一个新映射并将其分配给名为 myMap 的变量。请注意,myMap 包含一个嵌套值——即,映射中的映射。

在下一个交互中,您将看到从上一个映射获取数据的各种方法


user=> (get myMap :country)
nil
user=> (get myMap :name)
"Mihalis"
user=> (myMap :name)
"Mihalis"
user=> (:name myMap)
"Mihalis"
user=> (get myMap :surname)
"Tsoukalos"
user=> (get-in myMap [:livesAt :country])
"Greece"
user=> (get-in myMap [:livesAt :city])
"Athens"
user=> (get-in myMap [:livesAt :wrong])
nil

因此,您可以使用 get 关键字获取键的值,并且可以使用 get-in 关键字在嵌套值内部移动。此外,还有两种无需使用 get 关键字即可获取键值的方法,这在第二个和第三个命令中进行了说明。

此外,如果键不存在,您将获得 nil 值。最后,这是如何迭代列表的所有元素


user=> (def myList (list 0 1 2 3 4 5))
#'user/myList
user=> (doseq [[value index] (map vector myList (range))]
(println index ": " value))
0 :  0
1 :  1
2 :  2
3 :  3
4 :  4
5 :  5
nil

因此,首先您将包含数字的列表存储到 myList 变量中,然后使用 doseq 迭代列表的元素。

计算斐波那契数

本节介绍如何在 Clojure 中定义一个函数,该函数计算属于斐波那契数列的自然数。像这样创建用于计算斐波那契数列的 Clojure 项目


$ lein new fibo
$ cd fibo
$ vi src/fibo/core.clj
$ vi project.clj

src/fibo/core.clj 的内容应为以下内容


$ cat src/fibo/core.clj
(ns fibo.core)

(def fib
  (->> [0 1]
    (iterate (fn [[a b]] [b (+ a b)]))
    (map first)))

(defn -main [& args]
  (println "Printing Fibonacci numbers!"))
  (println (nth fib 10))
  (println (take 15 fib))

在上述代码中,fib 函数的定义负责计算斐波那契数列的数字。之后,main 函数使用 fib 两次。第一次是获取特定的斐波那契数,第二次是获取包含前 15 个斐波那契数的列表。

执行 fibo 项目会生成如下所示的输出


$ lein run
55
(0 1 1 2 3 5 8 13 21 34 55 89 144 233 377)
Printing Fibonacci numbers!

当您开始对 Clojure 感到舒适时,尝试以不同的方式实现 fib 函数,因为在 Clojure 中有很多方法可以计算斐波那契数。

处理命令行参数

现在,让我们看看如何使用 lein 项目在 Clojure 中使用程序的命令行参数。创建“cla”项目的步骤如下


$ lein new cla
$ cd cla

首先,您应该编辑 src/cla/core.clj 以包含处理程序命令行参数的实际 Clojure 代码。之后,您编辑 project.clj,就完成了。您可以在 src/cla/core.clj 中定义的 main 函数中找到实际处理程序命令行参数的 Clojure 代码


(defn -main [& args] ; Get command line arguments
  (if-not (empty? args)
    (doseq [arg args]
       (println arg))

; In case there are no command line arguments
    (throw (Exception. "Need at least one
 ↪command line argument!"))))

前面的 Clojure 代码使用 doseq 迭代 args 变量的项,并打印其每一项。此外,最后一行代码说明了如何在 Clojure 中处理异常。您需要该行是因为如果 args 列表为空,doseq 将不会运行,当程序在没有任何命令行参数的情况下执行时,就会发生这种情况。最后,您可以看到 Clojure 中的注释是以分号开头的行或分号字符后的行部分。

执行 Clojure 项目会生成如下所示的输出


$ lein run one 2 three -5
one
2
three
-5

如您所见,向 Clojure 项目提供命令行参数的方式与大多数编程语言相同。请注意,如果您在不提供任何命令行参数的情况下执行 lein run,程序将崩溃并产生大量调试输出,包括以下消息


$ lein run
Exception in thread "main" java.lang.Exception: Need at least
one command line argument!,

获取用户输入

除了使用程序的命令行参数外,还有另一种获取用户输入的方法,即在程序执行期间。以下是如何在 Clojure shell 中使用 read-line 函数从用户获取输入


user=> (def userInput (read-line))
Hello there!
#'user/userInput
user=> (println userInput)
Hello there!
nil

第一个命令使用 read-line 函数从用户读取一行,并将该行分配给名为 userInput 的新变量;而第二个命令打印 userInput 变量的值。

Clojure 宏

宏定义看起来像函数定义,因为它们具有名称、参数列表和带有 Clojure 代码的主体,并且它们允许使用用户代码扩展 Clojure 编译器。一般来说,在 Clojure 中有三种情况下您需要使用宏:当您想在编译时执行代码时,当您想使用内联代码时,以及当您需要访问未评估的参数时。但是,由于宏仅在编译时可用,因此在可能的情况下最好使用函数而不是宏。

在 Clojure 中复制文件

接下来,介绍如何在 Clojure 中复制文件,以防您想评估 Clojure 是否可以用作像 C 和 Go 这样的系统编程语言。正如您可能期望的那样,您将使用 lein 工具生成项目


$ lein new copy
$ cd copy/
$ vi project.clj
$ vi src/copy/core.clj

最后两个命令表示您需要更改 project.clj 和 src/copy/core.clj 文件。

您可以在 main() 函数的实现中找到此项目的逻辑


(defn -main [& args]
   (let [input (clojure.java.io/file "/tmp/aFile.txt")
        output (clojure.java.io/file "/tmp/aCopy.txt")]

    (try
       (= nil (clojure.java.io/copy input output))
       (catch Exception e (str "exception: "
 ↪(.getMessage e))))) )

与大多数编程语言一样,您可以使用多种技术来复制文件。此示例使用最简单的方法,通过单个函数调用复制文件。其他技术包括一次性读取输入文件并以相同方式将其写入输出文件,以及逐行读取输入文件并逐行写入输出文件。出于简单性考虑,输入和输出文件名在项目文件中硬编码,并分别分配给名为 inputoutput 的两个变量。之后,调用 clojure.java.io/copy 会创建输入文件的副本。虽然此方法不需要太多代码行,但当您想要复制大型文件或想要能够更改流程的某些参数时,它可能不是很有效。

执行项目不会生成任何输出,但会创建输入文件的所需副本


$ ls -l /tmp/aFile.txt /tmp/aCopy.txt
ls: /tmp/aCopy.txt: No such file or directory
-rw-r--r--  1 mtsouk  wheel  14 Jun 28 10:32 /tmp/aFile.txt
$ lein run
$ ls -l /tmp/aFile.txt /tmp/aCopy.txt
-rw-r--r--  1 mtsouk  wheel  14 Jun 28 10:49 /tmp/aCopy.txt
-rw-r--r--  1 mtsouk  wheel  14 Jun 28 10:32 /tmp/aFile.txt

如果您想使您的代码更健壮,您可能需要使用 (.exists (io/file "aFile.txt")) 语句来检查您的输入文件在尝试复制之前是否存在,并使用 (.isDirectory (io/file "/a/path/to/somewhere")) 语句来确保您的输入文件和输出文件都不是目录。

列出目录的目录和文件

最后,让我们看看如何访问给定目录中驻留的文件和目录。您可以按如下方式创建 lein 项目


$ lein new list
$ cd list

正如预期的那样,您需要编辑新项目中的两个文件:project.clj 和 src/list/core.clj。您可以在 src/list/core.clj 中定义的 listFileDir 函数的 Clojure 代码中找到程序的逻辑


(defn listFileDir [d]
  (println "Files in " (.getName d))
  (doseq [f (.listFiles d)]
    (if (.isDirectory f)
      (print "* ")
      (print "- "))
    (println (.getName f))))

运行您的 lein 项目会生成如下所示的输出


$ lein run
Files in  .
- project.clj
- LICENSE
* test
- CHANGELOG.md
* target
- .hgignore
* resources
- README.md
- .gitignore
* doc
* src

结论

本文介绍了 Clojure,这是一种非常有趣的函数式编程语言,拥有众多粉丝。棘手的是,您需要通过编写小的 Clojure 程序来习惯 Clojure,以便认识到它的优势。

资源

Mihalis Tsoukalos 是一位 UNIX 管理员和开发人员,一位 DBA 和数学家,喜欢技术写作。他是 Go Systems ProgrammingMastering Go 的作者。您可以通过 http://www.mtsoukalos.eu 和 @mactsouk 联系他。

加载 Disqus 评论