度々簡易な 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 "{}"'
make
でmake 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 に全て載ってゐる。