最近Go言語で遊んでます。文法がシンプルなのも気に入ってますが、適当な場所にhello.goを作成して、go build hello.goとするだけで実行ファイルが出来上がる、という手軽さも中々心地よいです。「簡単に取り掛かることができる」というのも楽しくプログラミングするうえで重要な要素なんだなと実感しました。
一方のClojureは、まずプロジェクトを作って次にproject.cljを書いて…と作業にかかるまでに一手間です。もっと手軽に書けたら良いのに…と調べていたら、Bootが使えそうでした。
Bootを使ったスクリプト
ClojureのビルドツールにBootというものがあります1。Leiningenのようにプロジェクトを作って開発することもできますが、ホームページの機能紹介には
Write quick Clojure scripts
without a project context using shebang support.
とあり、プロジェクトがなくても使えるようです。
早速試してみましょう。まず、適当な場所で次のファイルを作成します。
> touch hello.clj
ファイルの中身は以下のとおりとします。
# !/usr/bin/env boot
(ns boot.user
(:require [clojure.string :as str]))
(defn -main []
(println (str/upper-case "hello world!")))
一行目にShebangを書いていること以外は普通のClojureのコードですね。実行方法は以下のとおりです。
> chmod +x hello.clj
> ./hello.clj
HELLO WORLD!
なお、Bootスクリプトはboot.userネームスペースで実行されますがnsで明示する必要はありません。-mainも省略可能です。この方がスクリプト言語っぽく見える気もします。
# !/usr/bin/env boot
(require '[clojure.string :as str])
(println (str/upper-case "hello world!"))
ライブラリの使用
ライブラリを使用する場合はBootのset-env!関数で依存関係を記述します。以下は、enliveのチュートリアルを参考にした、Hacker Newsの記事タイトルを取得するスクリプトです。
# !/usr/bin/env boot
(set-env! :dependencies '[[enlive "1.1.6"]])
(ns boot.user
(:require [net.cgrand.enlive-html :as html]))
(def ^:dynamic *base-url* "https://news.ycombinator.com/")
(defn fetch-url [url]
(html/html-resource (java.net.URL. url)))
(defn hn-headlines-and-points []
(map html/text
(html/select (fetch-url *base-url*)
#{[:a.storylink] [:span.score]})))
(defn print-headlines-and-points [p]
(doseq [line (map (fn [[h s]] (str h " (" s ")"))
(partition 2 (hn-headlines-and-points)))]
(println line)))
(defn -main []
(print-headlines-and-points))
Shebangに続く(set-env! :dependencies '[[enlive "1.1.6"]])で依存関係を記述しました。これで:requireのフォームで通常通りライブラリを読み込めるようになります。また、依存するライブラリはスクリプトの起動時に自動で取得してくれるので、先ほどと同様に実行できます。
> chmod +x boot_scrape.clj
> ./scrape_boot.clj
The 3D Printing Revolution That Wasn’t (79 points)
Announcing Bookdown: Authoring Books and Technical Documents with R Markdown (23 points)
Amazon States Language – A JSON-based language to describe state machines (223 points)
・・・(以下略)・・・
簡単ですね。
ファイル分割
プログラムがある程度大きくなってきたらファイルを分割したくなると思います。-main関数以外を別ファイルに移動してみましょう。
> mkdir lib
> touch lib/hn_scrape.clj
(ns hn-scrape
(:require [net.cgrand.enlive-html :as html]))
(def ^:dynamic *base-url* "https://news.ycombinator.com/")
(defn fetch-url [url]
(html/html-resource (java.net.URL. url)))
(defn hn-headlines-and-points []
(map html/text
(html/select (fetch-url *base-url*)
#{[:a.storylink] [:span.score]})))
(defn print-headlines-and-points []
(doseq [line (map (fn [[h s]] (str h " (" s ")"))
(partition 2 (hn-headlines-and-points)))]
(println line)))
# !/usr/bin/env boot
(set-env! :dependencies '[[enlive "1.1.6"]]
:source-paths #{"lib"})
(ns boot.user
(:require [hn-scrape :refer [print-headlines-and-points]]))
(defn -main []
(print-headlines-and-points))
ここでは新たにlibというディレクトリを作成し、外に出した関数はその中のhn_scrape.cljというファイルに記述しました。名前空間はhn-scrapeです。boot_scrape.cljの方では、set-env!に:source-pathsを追記して、読み込みたいファイルのあるディレクトリを指定します。そうすることで、:requireは:source-pathsからhn-scrape名前空間を見つけてくれます。実行方法は先ほどまでと同様で./boot_scrape.cljとするだけです。
コマンドライン引数
コマンドラインから引数を与えることもできます。コマンドラインからの引数は-main関数のargsに入っています。例えばこんな感じです。
# !/usr/bin/env boot
(defn -main [& args]
(println args))
> chmod +x arg_sample.clj
> ./arg_sample.clj hoge fuga piyo
(hoge fuga piyo)
これを使っても良いのですが、Bootには引数をいい感じに扱ってくれるマクロdefclifnがあります。利用例を見てみます。
# !/usr/bin/env boot
(require '[boot.cli :refer [defclifn]])
(defclifn -main
"This is a sample script."
[n number VALUE int "the number"]
(println number))
> ./arg_sample.clj -n 10
10
> ./arg_sample.clj --number 10
10
> ./arg_sample.clj -h
This is a sample script.
Options:
-h, --help Print this help info.
-n, --number VALUE VALUE sets the number.
>
> ./arg_sample.clj -n hoge
clojure.lang.ExceptionInfo: option :minimum-score must be of type int
・・・(以下略)・・・
-main関数の引数にn number VALUE int "the number"とありますが、この5つで1セットです。最初のnはオプションの指定方法です。2つめのnumberは引数で与えた値が束縛されます(--numberというオプションにもなります)。3つめのVALUEは「この-nオプションは値をもつよ。フラグじゃないよ。」ということを示します(逆に、フラグの動作をするオプションを定義する場合は省略します)。
特に自分で定義しなくても-h(と--help)オプションが追加され、型チェックもしてくれます。この型チェックに使われるのが4つめのintで、5つめは-hオプションのヘルプドキュメントになります2。
それでは、コマンドライン引数を使って、先ほどのboot_scrape.cljを与えたスコア以上の記事のみ抽出するように修正してみます。
(ns hn-scrape
(:require [net.cgrand.enlive-html :as html]))
(def ^:dynamic *base-url* "https://news.ycombinator.com/")
(defn fetch-url [url]
(html/html-resource (java.net.URL. url)))
(defn hn-headlines-and-points []
(map html/text
(html/select (fetch-url *base-url*)
#{[:a.storylink] [:span.score]})))
(defn score-gt? [[_ s] minimum-point]
(let [this-point (Integer/parseInt (first (clojure.string/split s #" ")))]
(> this-point minimum-point)))
(defn print-headlines-and-points [minimum-point]
(doseq [line (->> (partition 2 (hn-headlines-and-points))
(filter #(score-gt? % minimum-point))
(map (fn [[h s]] (str h " (" s ")"))))]
(println line)))
# !/usr/bin/env boot
(set-env! :dependencies '[[enlive "1.1.6"]]
:source-paths #{"lib"})
(ns boot.user
(:require [hn-scrape :refer [print-headlines-and-points]]
[boot.cli :refer [defclifn]]))
(defclifn -main
"This is a sample script."
[m minimum-score VALUE int "Minimum score to list"]
(if-not minimum-score
(do (boot.util/fail "The -m/--minimum-score option is required!") (*usage*))
(print-headlines-and-points minimum-score)))
> ./boot_scrape.clj -m 100
Amazon States Language – A JSON-based language to describe state machines (223 points)
Don’t prematurely obsess on a single “big problem” or “big theory” (102 points)
Reflections on Rusting Trust (231 points)
・・・(以下略)・・・
lib/hn_scrape.cljの修正は本質じゃないので省きますが、boot_scrape.cljの方にはif-notから始まる引数チェックを加えました。(*usage*)関数は先ほどのヘルプと同じ動きをします。
終わりに
とりあえず以上を押さえておけば僕の用途では問題なさそうです。Bootを使ってこれからも楽しくClojureを書きたいと思います!
-
Bootのホームページでは、Bootを指して「It's not a build tool - it's build tooling. 」と言っています。ニュアンスの違いが分かるような、分からないような… ↩
-
これらの引数に関する詳しい情報は、Boot WikiのTask Options DSLを参照してください。 ↩