最近、会社で使うツール類のコードを書いていて「シェルでやれよ」的な処理に出くわすことが多いんですよ。ファイル操作やら外部コマンドの実行やら。で、シェル書いて、というのも考えるんですけど、永続化とか処理そのものの構造化とか考えるとプログラミング言語でできる限り書きたい。自分がある程度まともに使える言語の中で一番その辺の処理が楽そうなのがScalaなので、最近はScalaばっかり使っています。
とはいえ、Scala(というかJava)のファイル操作や外部プロセス実行のAPIがイケてるかというと、まあそういう訳でもないですね。ちょっと冗長というか。
そういう面もあって、中々「シェルでやれよ」的な処理を書くのがおっくうだった訳ですが、今日見つけた「Ammonite-Ops」というライブラリがかなり理想的な使い勝手でした。軽く使ってみた感触をまとめてみます。
Ammonite-Opsとは
Scalaで書かれたファイル操作、外部コマンド実行用のライブラリです。姉妹プロダクトとしてREPL環境を提供する「Ammonite-REPL」とシェルを提供する「Ammonite-Shell」があります。Ammonite-OpsはScalaプロダクトの中からファイルシステムのDSLを使うためのライブラリです。
http://lihaoyi.github.io/Ammonite/#Ammonite-Ops
セットアップ
build.sbt
に依存関係を追加するだけです。
libraryDependencies += “com.lihaoyi” %% “ammonite-ops” % “0.4.8"
特にリポジトリのリゾルバを追加する必要はありません。
使い方
Ammonite-OpsのDSLを使うためのお約束として、 import ammonite.ops._
しておきましょう。以降のサンプルは全て
この前提ですのでご注意ください。
パス
実際に使ってみたところをいくつかピックアップします。今回サンプル作ってないDSLや使い方もありますので、もっと突っ込んだ使い方は公式をご覧ください。結果をチェーンしたり、かなりシェルのコマンドに近い使い方もできるようなので、詳しい人はすんなり入れるのではないかと思います。
Path
と RelPath
Ammonite-Opsは、ファイルシステムを扱うのに Path
と RelPath
という二つのクラスを使います。どちらもパスを表すクラスなのですが、 Path
は絶対パス、 RelPath
は相対パスを扱うことができます。
どちらのクラスも指定する方法は幾つかありますが、一番簡単な方法は用意されたDSLを使う方法です。
val wd = cwd/‘target/‘folder // Path(絶対パス) val relPath = ‘target/‘folder/“test.txt” // RelPath(相対パス)
ルートを含まない構成を指定すると RelPath
となります。パスの構成に使えるのは、シンボルか文字列、 Array[T]
もしくは Seq[T]
です。他には Path
や RelPath
のコンストラクタはパスを表す文字列や java.io.File
などを受けることができます。この辺りは実行時引数から取得したパスとか、Javaのライブラリから File
のインスタンスが返る場合とかに使いそうですね。
また、 RelPath
は Path
からの相対として作成することもできます。
val wd = cwd/‘target/‘classes/‘main // 作業パス val baseDir = ced/‘target // 基準となるパス val relPath = wd relativeTo wd // 基準パスからの相対パス(classes/main)が返る
この相対パスの存在が非常にありがたい訳です。例えば、ファイル操作をする時に入力となるファイルを別ディレクトリに基準からの相対パスを維持して出力したい、とかって当たり前の様にある要件です。その時に、入力ファイルのパスと基準さえわかっていれば、ルールをあまり意識しなくていいのは非常に助かります。
その他にも各種コマンドの結果として RelPath
が返却されることがあります。
特殊なパス
Path
を構築する際のサンプルにも出ましたが、幾つかベースを指定するための特殊なパスがあります。
cwd
・・・Current Working Directoryの略。Javaのプロセスが動いているディレクトリで、実行環境によって異なります。home
・・・実行ユーザのホームディレクトリ。root
・・・rootディレクトリ。makeTmp
・・・一時ディレクトリ。
makeTmp
のみ使い方に注意が必要。特に公式には使い方のサンプルがないので、記載しておきます。
val temp = Path(Path.makeTmp)/‘resources
Path.makeTmp
はどうも java.nio.file.Path
が返るようで、これを Path
にキャストしてやる必要があるようです。
ファイル操作
これらのパスを操作するDSLが用意されています。
val wd = cwd/‘target/‘classes mkdir! wd // ディレクトリの作成。コメントを読む限りは mkdir -p と同等のようで、親ディレクトリもまとめて作ってくれるようです。 ls! wd // リストアップ。Seq[Path] で返してくれます。 rm! wd // 削除
この他にも mv
や exists
などがあります。 exists
と mkdir
以外のDSLは指定したパスが存在しない時に例外をスローします。パスの状態を知りたい時は stat
を使います。
val info = stat! cwd/‘target/“file1.txt” info.name // ファイル名 info.isDir // ディレクトリかどうか(このケースはfalse) info.isFile // ファイルかどうか(このケースはtrue)
テキストファイルだったら読み込みと書き込みのDSLも用意されています。まだパスを渡すだけの単純な read
しか使ってないので、サンプルは特に作りません。Readerとか用意したりする必要もなく、パスを指定するだけで文字列として返してくれるので非常に楽です。
外部コマンドの実行
これが非常にすっきり書けて良かったです。今回Pandocを実行するのに書いた箇所を抜粋します。
implicit val wd: Path = inputPath/up // (1) if (exists! outputPath == false) mkdir! outputPath %%pandoc (List(“-o”, “outputFile.docx”, “input.md”)) // (2)
(1)の箇所でPandoc実行のワーキングディレクトリを指定しています。Pandocがマークダウンから画像を差し込む時に、相対パスを指定している場合はワーキングディレクトリをマークダウンと同じディレクトリにしないと参照できないための措置です。これを指定しない場合は外部コマンドの実行をする前に import ammonite.ops.ImplicitWd._
を指定して下さい。 cwd
が暗黙的に指定されます。
(2)の箇所でpandocを実行しています。 %
もしくは %%
の後にコマンドで実行可能です。両者の違いは戻り値で、 ‘%’ はリターンコードが戻り値で実行結果のメッセージなどは標準出力に出ます。 ‘%%’ は実行結果の標準出力などが取りまとめられた CommandResult
クラスが返り、リターンコードが0以外で終了した時は例外がスローされます。用途に応じて使うのが良いと思います。コマンドの引数は可変長引数で文字列を渡すか、サンプルのようにリストを渡すことができます。
まとめ
今、PlantUMLとPandocを連携させてマークダウンで書かれたドキュメントをビルドするツールを作っています。社内で使うつもりですが、特に機密情報はないですし、ゆくゆくはDockerイメージとして構築して使うことを目指して作っているツールです。
ドキュメントのためのツールなのでファイル操作は必須ですし、PandocはJavaやScalaのラッパーライブラリが使えなさそうだったので、外部コマンドの実行もあります *1 。非常に便利なので、完全に社内向けに作っているDocker管理ツールでも活躍してくれそうです。そちらも含めて使い込んでみようと思います。使いやすいDSLなのですが、私の頭ではコード読んでも内容が全くわからないのが難点。どうやったらこんなDSL作れるんだろう。。。