May 19, 2020
By: Kevin

Clojure脚本开发bb和nbb

使用Clojure开发脚本

脚本有别于web和桌面应用开发, 第一需求是快, 快速启动, 快速结束. 执行效率不是要求特别高. 主要看中是启动速度.

能够方便的调用其他脚本, 文本操作能力强.

在最早写这片文章的时候, 是2020年, 当时Lumo看起来还是个不错的方案, 发展到2023年, clojure脚本领域的王者无疑是bb(babashka)和nbb(nodejs babashka), 这俩都是毫秒级启动.

而且生态强大(借助nodejs和java生态), 文法规范, 表达力强, 特别适合做项目脚本.

  • nbb基于nodejs, 完善的nrepl支持, 对js生态熟悉的可以尝试下.
  • bb基于GraalVM, 完善的nrepl支持. 启动迅速, 依赖简单, 只依赖与一个不到10m的解释器.

问题

写一个脚本, 读取当前的git commit, 获得当前时间, 把这两个内容插入到源代码合适的位置(便于显示).

代码

nbb版本

使用nbb来执行以下代码

;; 使用shelljs, moment, os, moment
(require '["shelljs$default" :as sh])
(require '["fs" :as fs])
(require '["os" :as os])
(require '["moment$default" :as moment])
(require '[clojure.string :as str])


(require '[clojure.tools.cli :refer [parse-opts]])

;; 我们要修改的文件
(def portal-cljs "public/index.html")

(defn osx? []
  (str/starts-with? (.platform os) "darwin"))

(defn shell []
  (if (osx?)
    "bash"
    "powershell"))

(defn latest-commit []
  (->> (sh/exec "git log -1")
       str
       (re-seq #"commit (\w+)")
       first
       second))

(defn package-time []
  (-> (new js/Date)
      moment
      (.format "yyyy-MM-DD HH:mm")))

(defn package-day []
  (-> (package-time)
      (str/split #" ")
      first))

(defn version-time []
  (str "version:" (latest-commit) " build at: " (package-time)))


(defn process-file [file]
  (-> file
      (fs/readFileSync)
      str
      (str/replace #"version.*"  (version-time))
      (#(fs/writeFileSync file %))))


(def cli-options
  [["-p" "--package"]])

(let [options (parse-opts *command-line-args* cli-options)
      package? (get-in options [:options :package])]
  (process-file portal-cljs)
  )

bb版本

使用 bb script.clj 来执行, 你发这两个脚本非常类似, 除了os, shell, 时间处理依赖的库不太一样.

#!/usr/bin/env bb

(require '[clojure.java.shell :as shell])
(require '[clojure.string :as str])
(require '[clojure.tools.cli :refer [parse-opts]])

(def portal-cljs "src/cljs/pages/portal.cljs")
(def ws-clj "src/clj/ws.clj")

(defn win? []
  (str/starts-with? (System/getProperty "os.name") "Windows"))

(defn shell []
  (if (win?)
    "powershell"
    "bash"))

(defn latest-commit []
  (->> (shell/sh "git" "log" "-1")
       :out
       (re-seq #"commit (\w+)")
       first
       second))

(defn package-time []
  (let [zone (java.time.ZoneId/of "Asia/Shanghai")
        pattern (java.time.format.DateTimeFormatter/ofPattern "yyyy-MM-dd HH:mm")]
    (-> (java.time.ZonedDateTime/now)
        (.withZoneSameInstant zone)
        (.format  pattern))))

(defn package-day []
  (-> (package-time)
      (str/split #" ")
      first))

(defn version-time []
  (format "version:%s build at:%s, trail version will expire in 7 days" (latest-commit) (package-time)))


(defn process-file [file]
  (-> file
      slurp
      (str/replace #"(def \^\:private PACKAGE-DAY )\".+\""  (format "$1\"%s\"" (package-day)))
      (str/replace #"(def \^\:private VERSION )\".+\""  (format "$1\"%s\"" (version-time)))
      (#(spit file %))))

(defn build-package []
  (apply shell/sh (shell) (-> "npx shadow-cljs release :app :electron;"
                              (str/split #" ")))
  (apply shell/sh (shell) (-> "npx electron-builder build"
                              (str/split #" "))))


(def cli-options
  [["-p" "--package"]])

(let [options (parse-opts *command-line-args* cli-options)
      package? (get-in options [:options :package])]
  (process-file portal-cljs)
  (process-file ws-clj)
  (when package?
    (build-package)))

repl的支持

两者的参数完全一致, 推荐使用 nrepl-server 启动后再用cider/calva connect上来进行开发.

  • repl Start REPL. Use rlwrap for history.
  • socket-repl [addr] Start a socket REPL. Address defaults to localhost:1666.
  • nrepl-server [addr] Start nREPL server. Address defaults to localhost:1667.

结论

比起shell来, clojure有更丰富的数据结构, 更好的调试体验(repl). 鼓励大家尝试

参考材料

Tags: clojure tool