c4se記:さっちゃんですよ☆

.。oO(さっちゃんですよヾ(〃l _ l)ノ゙☆)

.。oO(此のblogは、主に音樂考察Programming に分類されますよ。ヾ(〃l _ l)ノ゙♬♪♡)

音樂は SoundCloud に公開中です。

考察は現在は主に Scrapbox で公表中です。

Programming は GitHub で開發中です。

Clojure で task runner を書く

度々簡易な task runner を書いてゐる。いつもは Makefile を使ってゐて、makeと打つだけで何をするべきか思ひ出せるやうにしてある。例へば、

.PHONY: help
help:
    @awk -F':.*##' '/^[-_a-zA-Z0-9]+:.*##/{printf"%-16s\t%s\n",$$1,$$2}' $(MAKEFILE_LIST) | sort

.PHONY: format
format: ## Format all stuffs.
    clojure -m cljfmt.main fix .
    npx prettier --write package.json
    npx prettier --parser yaml --write .yamllint .github/workflows/*.yml
    ag -g '\.md$' | xargs -t -P "$(($(nproc) * 2))" -I {} sh -eux -c 'npx prettier --write "{}" && npx textlint --fix "{}"'

makemake helpが實行され、help が見られる。

便利 shell script 集として Makefile を使ってゐるのである。shell 藝が難しく成ってきたら Makefile を捨てる。普通はtools/format.sh等の樣に個別の shell script file を用意するのであらう。趣味でやってゐるのでなければそうするだらう。自分は Rakefile を書いてゐた。しかしその project で Ruby を使ふのが躊躇はれる場合、且つそれが趣味であれば shell script 集としての task runner 位いは書いてしまふ。Python だとこうだ。先ず task 一覧を記録する變數を作る。

tasks = {}

そこに次の樣に decorator を書くと記録される樣にしたい。

@task
def format():
    """Format code."""
    run(r"ag -l '\r' | xargs -t -I{} sed -i -e 's/\r//' {}")
    with docker() as _run:
        _run("poetry run black *.py imperial_calendar tests ui")
        _run("npx prettier --write README.md templates/*.md")
        _run("npx prettier --write *.js")
        _run(
            "sh -eux -c {:s}".format(
                quote(
                    r"ag --hidden -g \.ya?ml$ | xargs -t npx prettier --parser yaml --write"
                )
            )
        )

これは高階の手續きで書ける。

def task(function):
    """Define a task."""
    if function.__doc__:
        tasks[function.__name__] = function.__doc__

    def wrapper():
        function()

    return wrapper

task を實行する code も短い。help こそ欲しい機能であるからそれも實裝する。

if __name__ == "__main__":
    if len(sys.argv) == 1 or sys.argv[1] == "help":
        for task_name, describe in tasks.items():
            print(f"{task_name.ljust(16)}\t{describe}")
        exit(0)
    for task_name in sys.argv[1:]:
        locals()[task_name]()

これで./tasks.pyを叩けば全てを思ひ出せる。task の依存關係を解決する仕組みは無い。build 成果物を指定したい時には pull 型或いは bottom up の依存解決が要る (例へば ne-sachirou/docker-elixir/lib/mix/tasks/make.ex)。しかし shell script 集の樣なもの詰まり push 型や top down とでも言へるものである場合は、高機能な programming 言語が手元に有るのだから、依存する task を明示して呼べば濟む。若しくは手續き (記法としては函數) を書いて呼べば好い。

shell command を實行する機能は当然要る。これは project 毎に特殊な處理と成るが、例へば、

def run(command: str, capture_output=False, text=None) -> subprocess.CompletedProcess:
    """Run command."""
    command = command.strip()
    print(command)
    env = os.environ.copy()
    env["DOCKER_BUILDKIT"] = "1"
    return subprocess.run(
        command,
        env=env,
        capture_output=capture_output,
        check=True,
        shell=True,
        text=text,
    )

Clojure で同じ事をやらう。先ず task 一覧を記録する變數を作る。Clojure は transactional に再代入可能な變數を作れる。

(def tasks (ref {}))

次の樣に macro で task を書ける樣にしたい。

; deps.edn
{:deps {
  cljfmt {:mvn/version "0.6.7"}}}
(require '[cljfmt.main :as cljfmt])

(deftask format
  "Format all stuffs."
  (sh "sh" "-eux" "-c" "ag -l '\\r' | xargs -t -I {} sed -i -e 's/\\r//' {}")
  (println "+ clojure -m cljfmt.main fix .")
  (cljfmt/fix ["."] (cljfmt/merge-default-options {}))
  (sh "npx" "prettier" "--write" "package.json")
  (sh "npx" "prettier" "--parser" "yaml" "--write" ".yamllint" ".github/workflows/*.yml"))

簡單な macro。

(defmacro deftask [task-name doc & body]
  (dosync (alter tasks assoc task-name {:doc doc :body (conj body 'do)}))
  nil)

實行するには少し手間が要る。先ず task を呼ぶ部分はこう。

(defn act [task-name]
  (let [tasks @tasks
        task-name (symbol task-name)]
    (if (contains? tasks task-name)
      (->> tasks task-name :body eval)
      (do (println (format "Task %s not found." task-name))
          (System/exit 1)))))

(defn main [argv]
  (cond
    (or (empty? argv) (some #(= % "help") argv)) (help)
    (some #(= % "repl") argv) (repl)
    :else
    (do (doseq [task-name argv] (act task-name))
        (System/exit 0))))

(main (vec *command-line-args*))

help は、

(defn help []
  (println (format "%-16s\t%s" 'help "Show this help."))
  (println (format "%-16s\t%s" 'repl "Start a REPL."))
  (doseq [[task-name task] @tasks]
    (println (format "%-16s\t%s" task-name (:doc task)))))

面倒なのは次だ。Clojure は、實行は速いが JVM の起動が遲い。そこでclojure tasks.cljとしても實行出來、更に REPL でも動かせる樣にしておく。

(defn repl []
  (println "`(help)` to print a help.")
  (println "`(act :TASK-NAME)` to run the task.")
  (help)
  (clojure.main/repl))

これを起動するには、

rlwrap -c -b "(){}[],^%$#@\"\";:''|\\" clojure tasks.clj repl

これは覺えられないので Makefile を書いておく。

.PHONY: repl
repl:
    rlwrap -c -b "(){}[],^%$#@\"\";:''|\\" clojure tasks.clj repl

makeと叩けば REPL が起動し、help が表示される。

shell script を實行するのには例へばこう。

(require '[clojure.java.shell :as shell])

(defn sh [exe & args]
  (println (clojure.string/join " " (concat ["+" exe] args)))
  (let [{:keys [exit out err]} (apply shell/sh (cons exe args))]
    (if (= 0 exit)
      (print err out)
      (do (print (format "Exit with %d\n" exit) out err)
          (System/exit exit)))))

Community-Powered Clojure Documentation and Examples | ClojureDocs に全て載ってゐる。