このままでいいか

アニメとか,プログラミングとか,研究とか,思ったことをだらだらと書く感じで

設定とかをファイルで管理するためのツールを作った話!

f:id:dbym4820:20191204194741p:plain

この記事は,Lisp Advent Calendar 2019の5日目の記事です.

内容は,こちらのユーティリティ(Utsushiyo)の紹介です.

github.com

目次

本記事の狙いと注意点

  • 自分のファイル管理方法(自作ツール:Utsushiyo)の周知
  • ツールの存在・使い方のドキュメント化
  • 自分の中での機能の整理

注意点

  • 本記事,及びUtsushiyoはUnix系の一部OSのみに対応しています.現状,Windowsでは動作しません.ご容赦ください.
  • 特に本記事では,MacOSXのファイルパスを例として提示していますので,異なる環境の読者の方は適宜,読み替えてください.

Utsushiyoとは?

何を解決したい?

Common Lispでソフトウェアを作成するとき,一部の設定をLispプログラムに埋め込むのではなく,ファイルとして外部に出して管理するための,簡単/抽象化された/共通の仕組みがない(と僕は思ってる)問題を解決したい.

それらをプロジェクトごとの環境(システムやプロジェクト)ごとの個別の管理をできるようにしたい.

例えば...

Webサーバを起動するポート番号をファイルで指定する場合
(ql:quickload '(hunchentoot alexandria))

;; ポートの設定ファイルを/Users/tomabu/.software-name/config/ディレクトリにある,
;; portというテキストファイルに,5000などのポート番号を格納する場合
(defvar *port-config-file-pathname* 
        "/Users/tomabu/.software-name/config/port")

(defun get-port-from-file ()
  ;;; ファイルの中身を文字列として読み出す
  (alexandria:read-file-into-string *port-config-file-pathname*))

;; サーバを立ち上げる
(hunchentoot:start
 (make-instance 'hunchentoot:easy-acceptor
                :port (get-port-from-file))) ;;;; ここでファイルにあるポートを指定
  • これを以下のように書けたら嬉しいと思った
;; $HOME/.utsushiyo/software-name/config/port => 5000 がある場合
(ql:quickload '(hunchentoot utsushiyo))

(hunchentoot:start
 (make-instance 'hunchentoot:easy-acceptor
                :port (utsushiyo:get-attribute "software-name" "config/port") ;;;; ファイルからテキストを取得する関数を定義するマクロ))

この例だけ見ると,行数も未字数もさほど変わらないのになんいが嬉しいの?となるかもしれませんが,コードの意味的に,以下のように違うと考えています.

  • 1つ目の例で書いている処理は,「ファイルから文字列を読み出す」処理
  • 2つ目の例で書いている(つもりの)ことは,「ポート番号を外部に永続化して,それを呼び出す」処理

また,同様の永続化を別の情報(例.使うサーバをHunchentootにするか,Wooにするか)についても実施したくなったとき,

  • 1つ目の例だと,「ファイルパスの定義をそれぞれ別々に書く」や「ファイルパスを制御する関数を書く」など,多様な解決策が生まれたり,コードの量が無駄に増えたりする可能性がある
  • 2つ目の例だと,get-attributeを1行加えるだけでいい

という違いが生まれます.

かつて,自分がCLでWebアプリケーションを書いているときには,ほぼ必ず1つ目のような処理が,ポート以外の情報についてもいくつか現れていました.

こうした設定管理ファイルの処理手続きを,(2つ目の例のように)もう少し抽象化して定義したかったわけです.

Common LispのファイルI/OプラクティスとUtsushiyoの位置づけ

Lisp Advent Calendar3日目の記事でmyaoさんが触れているように,CL処理系による違いやOSによる違いなど,CLのパス周りは若干複雑で面倒です.

この複雑さを吸収するために,UIOPやalexandria,cl-fadなどの有用なライブラリが公開されています.

Utsushiyoは,これらのライブラリより抽象度高く,実用ソフトウェアを作成するときの設定などのファイル管理の枠組みを与えるフレームワークとして開発しました.

以下,インストール法と,具体的な使い方,僕の個人的な使用例を紹介します.

どうやって使うか?

インストール

UtsushiyoはASDF3を活用してライブラリ化していますが,Quicklispには登録されていない(登録する気もない)ので,Roswellをインストールしていると話が早いです.

してない場合は,utsushiyoをGithubから,CLにパスが通っているディレクトリに置いてください.

(Roswellが入っていれば)以下のコマンドでインストール完了します

~$ ros install dbym4820/utsushiyo

インストールできたら,REPLを立ち上げて,Quicklispでロードできるか,試してみてください.

CL-USER> (ql:quickload :utsushiyo)
To load "utsushiyo":
  Load 1 ASDF system:
    utsushiyo
; Loading "utsushiyo"
[package utsushiyo].
(:UTSUSHIYO)

ロードが完了したら,Utsushiyoの設定を初期化してください(ホームディレクトリに.utsushiyoディレクトリが作成されます).

CL-USER> (utsushiyo:bootstrap)
; No value

ディレクトリが生成されているか確認します.

~ $ ls -la ~/ | grep .utsushiyo
drwxr-xr-x    3 tomabu  staff    102 11  1  2018 .utsushiyo

Utsushiyoで管理する各設定ファイルは,このディレクトリ以下に配置されることになります.

デフォルトでは,utsushiyo自身の設定用ディレクトリのみが配置されています.

以上で,インストールが完了です.

シチュエーション別の使用方法

最も基本的な使い方

手動で設定ファイルを作成し,それを読み込むようにする

  • 事前準備
# utsushiyoが正しく初期化できているか確認
~ $ ls -la ~/.utsushiyo/
total 0
drwxr-xr-x    4 tomabu  staff   136 12  4 16:37 .
drwxr-xr-x+ 114 tomabu  staff  3876 11 26 14:33 ..
drwxr-xr-x    6 tomabu  staff   204 12  4  2018 utsushiyo

# original-web-appという名前の環境設定ディレクトリ/ファイルを定義する
# ポート番号を5000番に設定するファイルを書く
~ $ mkdir ~/.utsushiyo/original-web-app
~ $ mkdir ~/.utsushiyo/original-web-app/config
~ $ touch ~/.utsushiyo/original-web-app/config/port
~ $ echo "5000" >> ~/.utsushiyo/original-web-app/config/port
~ $ cat ~/.utsushiyo/original-web-app/config/port
5000
  • CL内での設定読み込み
CL-USER> (utsushiyo:get-attribute "original-web-app" "config/port")
"5000"

ASDFプロジェクトへの組み込み方

ASDF3を使って開発されたシステムにUtsushiyoを活用する方法を以下に説明します.

ここでは,clackningleを使ってHelloworldをするだけのhello-webというシステムを考えます.ただし,起動するポート番号は可変で,システムのエンドユーザがファイルの中身を書き換えることで変更可能にしたいとします.

システムを開発するディレクトリは,~/code/hello-web/だとします.システムの雛形は,cl-projectros-projectを使って作成してください.*1

例えば,こんな感じに.

CL-USER> (ql:quickload :cl-project)

To load "cl-project":
  Load 1 ASDF system:
    cl-project
; Loading "cl-project"

(:CL-PROJECT)
CL-USER> (cl-project:make-project #P"/Users/tomabu/code/hello-web/" 
                                  :name "hello-web" 
                                  :depends-on '(ningle clack utsushiyo))

writing /Users/tomabu/code/hello-web/hello-web.asd
writing /Users/tomabu/code/hello-web/README.org
writing /Users/tomabu/code/hello-web/README.markdown
writing /Users/tomabu/code/hello-web/.gitignore
writing /Users/tomabu/code/hello-web/src/main.lisp
writing /Users/tomabu/code/hello-web/tests/main.lisp
T

基本的に,Utsushiyoを組み込むためには,「.asd」ファイルと同じディレクトリにある,srcディレクトリ以下に,「utsushiyo」という名前のディレクトリが必要です.

このutsushiyoディレクトリ以下のファイルは,hello-webシステムをQuicklispでロードしたユーザのローカル環境下にコピーされることになります.

例えば,~/.utsushiyo/hello-web/config/portというファイルをユーザのローカルにコピーさせるなら,以下のようなディレクトリとファイルを作成することになります(デフォルトポートを5000番とする場合).

~/code $ tree ~/code/
/Users/tomabu/code/
└── hello-web
    ├── README.markdown
    ├── README.org
    ├── hello-web.asd
    ├── src
    │   ├── main.lisp
    │   └── utsushiyo ;; このmディレクトリ以下が重要(中身が空でも構いません)
    │       └── config
    │           └── port ;; 中に,"5000"と書かれたテキストファイル
    └── tests
        └── main.lisp
5 directories, 6 files

src/main.lisp内に,Webアプリの本体を書いていきます.

コードは以下のようになものになりますね.

(defpackage hello-web
  (:use :cl)
  (:import-from :ningle
                :route
                :<app>)
  (:import-from :clack
                :clackup)
  (:import-from :utsushiyo
                :find-system-dir
                :get-attribute
                :init
                :exist-env-p)
  (:export
   :start-system
   :stop-system))
(in-package :hello-web)


;; hello-web用のutsushiyoディレクトリがユーザローカルなければ作成(コピー)
(unless (utsushiyo:exist-env-p "hello-web")
  (utsushiyo:init "hello-web"
          :project-root-path (utsushiyo:find-system-dir :hello-web))) ;;;; この部分について後述

(defvar *server* (make-instance '<app>))
(defparameter *active-server* nil)

(defun start-system ()
  (setf (ningle:route *server* "/")
    #'(lambda (params)
        (declare (ignorable params))
        (format nil "Hello World"))
    *active-server*
    (clack:clackup *server*
                   :port (parse-integer
                            (utsushiyo:get-attribute "hello-web" "config/port")) ;; 環境設定(ポート番号)を読み込む
                   :silent t)))

(defun stop-system ()
  (clack.handler:stop *active-server*)
  (setf *active-server* nil))

これで目的の機能は実装できました実装できました.

ASDFのパスを通して*2,Quicklispとかで読み込みましょう.

CL-USER> (ql:quickload :hello-web)
To load "hello-web":
  Load 1 ASDF system:
    hello-web
; Loading "hello-web"
..................................................
[package hello-web]
(:HELLO-WEB)
CL-USER> (hello-web:start-system)
#S(CLACK.HANDLER::HANDLER
   :SERVER :HUNCHENTOOT
   :ACCEPTOR #<SB-THREAD:THREAD "clack-handler-hunchentoot" RUNNING
                {10068FC4B3}>)

立ち上がりましたね.

では,実際にブラウザで挙動を確認します.

http://localhost:5000にアクセスすると,以下のような表示になりました.

f:id:dbym4820:20191204191706p:plain

無事,立ち上がっています.

現在は設定通り,5000番で立ち上がっているシステムですが,これを25252番ポートで実行するように変更したいとしましょう.

先程のサーバを一度落として,$HOME/.utsushiyo/hello-web/config/portの中身を25252に書き換えます.

~ $ echo "25252" > $HOME/.utsushiyo/hello-web/config/port

先ほどと同じ手順で立ち上げ,今度は,http://localhost:25252にアクセスすると

f:id:dbym4820:20191204191554p:plain

無事動きました.基本的な作業はこれだけです.

補足

unless句部分について

現状,utsushiyo:initは,既存環境があっても,それを上書きする形でイニシャライズしてしまいます.

毎回イニシャライズしてしまうと,config/portのテキストを書き換えても,hello-webのプロジェクトディレクトリ側の設定が反映されてしまいますので,すでにローカル環境に$HOME/.utsushiyo/hello-web/がある場合には,Initしないようにしなければなりません.

すでに環境があるかどうかを確認するメソッドがexist-env-pが用意されていますので,これを使います.

find-system-dir部分について

Quicklispなどでシステムを配布する場合には,ユーザ環境内のどのディレクトリに,「src/utsushiyo/」が配置されているかわかりません.

それをシステム名でASDFに探索してもらい,ユーザ環境に合わせた動的なパスを取得するために,find-system-dir関数を用意しています.

引数に,キーワードシンボルとして,システム名を与えると,ASDFのシステムのパス名を返します.

Utsushiyoはこちらのディレクトリ内の,「src/utsushiyo/」をユーザローカルにコピーして使用しますので,配布の際にはこちらを使用するのが無難です.

おわりに

CLプロジェクトの設定とかを一部ファイルに切り出して管理するための自作ツール(フレームワーク)をご紹介しました.

工夫点

今回説明していないこと

  • 基本は自分しか使わないと思ってやってたので,今の所運用でなんとかしている部分が結構ある
  • その他,提供しているAPIについて: GithubのREADMEに突貫のAPI一覧を載せました*3

    • 例えば,* set-attributeというメソッドについて(get-attributeと対をなす,ファイルの中身を書き換えるメソッド)

問題点とLimitation

  • WIndowsには対応していない
  • 抽象度が上がるけど,Utsushiyoを知らない人にとって,コードの可読性は下がってしまう可能性が高い
  • 依存ライブラリが増えること自体が害なことが少なくない

といったところです.そういうわけで,自分専用のユーティリティ感ありますが,もし気が向いたら試してみてください!

github.com

長い記事になってしまいましたし,最後まで読んでくださる方,記事内容を理解してくださる方はどれほどいるのやら...

ともかく,Lisp Advent Calendar 2019,引き続き楽しみですね!

明日(6日目)は,eshamsterさんによる,「cl-base + rove + Travis CI でテストする」です!

*1:ASDFのシステムの使い方については,Common Lisp.net!とか,Lispアドカレ20191日目の記事とか,こちらの記事とかを見て,ご自身で調べてください.とはいえ,わからない人は,この記事自体,わからないかもですが...

*2:パスの通し方は,こちらの記事に書いてます.

*3:(そこだけ日本語のまま...いつか英語と日本語両方準備するかもしれない)