概要

scala.sys.processパッケージには外部コマンドを気楽に実行できるありがたい機能が入っている。Scala2.9以降で搭載された。

今回はとりあえずlsやcatなどのコマンドの実行や、tail -fの結果取得などをしてみた。

@CretedDate 2011/09/18
@Versions Scala2.9.1

lsしてみる

scala.sys.process.Processを使って結果を標準出力に出してみる。

scala> import scala.sys.process.Process
scala> val process = Process("ls").run
bin
doc
lib
src

こんな感じでlsの結果が標準出力される。exitValueで終了コードも取れる。

scala> process.exitValue
Int = 0

実行する際は、run、!、!!などが使える。lsが出力したファイルの一覧を取りたい場合は、!!を使う。

scala> val ret = Process("ls") !!
scala> ret.split("\n")
Array[java.lang.String] = Array(bin, doc, lib, src)

同期処理で実行したい場合は、!を使う。!は終了コード(Int)を返す。

scala> Process("ls") !
Int = 0

runは非同期でProcessを返す。!や!!は同期処理。例えば下記の処理は一瞬で次の行に進む。

scala> Process("sleep 5") run

けど、下記の処理は5秒待ってから次の処理に進む。

scala> Process("sleep 5") !
scala> Process("sleep 5") !!

runした場合はexitValueを実行すると、終了コードが取れるまで(コマンドが終了するまで)待つことになる。ので、下記の記述だと2行目のところでsleepが終わるまで待機する。

scala> val process = Process("sleep 5") run
scala> process.exitValue

暗黙の型変換を使ったもっと手軽な書き方

scala> import scala.sys.process._

上記のようにimportすると、ProcessImplicitsもimportされる。ProcessImplicitsは、stringToProcessという凶悪なクラスを含んでいて、StringをProcessに暗黙変換してくれる。

というわけで、lsの結果は下記のようにして取得できることになる。

scala> "ls" !!
String = 
"bin
doc
lib
src"

linesかlines_!で行ごとに結果を取ることもできる。linesは実行失敗時に例外が起こる。lines_!は失敗しても例外が起きないでスルー。結果はStreamで返る。

scala> ("ls" lines_!) foreach println
bin
doc
lib
src

linesはStringOpsのlinesと名前が被るので、"ls" linesと書くとエラーになる。Process("ls") linesのように書かないといけない、ちょっと残念な子。

小さいファイルの中身をさらっと読み込んで文字列にしたい場合は、こんな書き方も可能ということになる。

scala> val str = "cat foo.txt" !!

もちろんこんな書き方をするとOS変更であっという間に動かなくなるので推奨されるものでもないけど、ちょっとしたスクリプトを書く時には便利。

リダイレクトやパイプ

結果をファイルにリダイレクトしてみる。普通に書くとなんか怒らる。

scala> "ls > hoge.txt" !
ls: cannot access >: そのようなファイルやディレクトリはありません
ls: cannot access hoge.txt: そのようなファイルやディレクトリはありません

引数がパスとして見られているようだ。というわけで、ProcessBuilderに定義されているリダイレクト機能を使う。

scala> import java.io.File
scala> "ls" #> new File("foo.txt") !

#> でリダイレクトできる。パイプは #| を使う。たとえばps ax | grep scalaとかをしようと思ったら、こうなる。

scala> "ps ax" #| "grep scala" !

#&&(1個目が成功したら次のコマンドも実行)とか、#||(1個目のコマンドが失敗したら次のコマンドを実行)とか、普通にコマンド打つ時に近い感じのものが用意されてる。もちろん追記(#>>)もある。

至れり尽くせり。

tail -fの情報を取得してみる

ProcessIOというProcessの出力に関する処理を定義できるクラスを、runする時に引数に渡してやると、入出力に関するInputStreamやOutputStreamを操作できる。

例えば、tail -fの結果をリアルタイムで取ろうと思った場合は、下記のように書ける。

import sys.process._
import java.io.{ BufferedReader, InputStreamReader }

// ProcessIOの用意
val pio = new ProcessIO(
  in => {},
  out => {
    val reader = new BufferedReader(new InputStreamReader(out))
    def readLine(): Unit = {
      val line = reader.readLine()
      println(line) // ここに1行ずつで結果が来るから適当に処理する
      if(line != null) readLine()
    }
    readLine()
  },
  err => {})

// runする時に引数に渡す
val process = "tail -f test.txt".run(pio)
process.exitValue

この状態でtest.txtというファイルに対して適当に追記を行うと、1行追加されるたびに標準出力に追加された追記部分が出力される。

尚、このプログラムはCtrl+Cしないといつまでも終わらない。

楽しい例文集

以下、少しだけ楽しい気分になる適当な例文。環境によっては動かないことも多々あるだろうと思われる。もちろん利用は非推奨。

// ファイルを文字列に格納する
val str = "cat filename" !!
// ファイルを1行ごとに文字列に格納する
val lines = "cat foo.txt" lines_!
// ファイルを指定行数だけ先頭から読み込む
val lines = "head -%d %s" format (10, "test.txt") !!
// ファイルを指定行数だけ末尾から読み込む
val lines = "tail -%d %s" format (10, "test.txt") !!
// 文字列をファイルに出力する
val str = "出力したい文字列"
("echo " + str) #> new File("test.txt") !
// ファイルの行数を調べる
val lineCount = ("wc -l filename" !!).split(" ")(0).toInt
// 指定URLのコンテンツを取得して文字列に格納する
def wget(url: String) = "wget -q %s -O -" format url !!
val str = wget("http://www.yahoo.co.jp/")
// 配下のディレクトリから指定拡張子のついたファイルを再帰的取得
def find(ext: String) = "find . -name *.%s" format ext lines_!
val files = find("txt") foreach println
// 今日の日付を取得する
val today = ("date +%Y/%m/%d" !!) trim
// 平方根を出す
echo "scale=10; sqrt(5)" | bc

遊び方いろいろ。

まとめ

こんな感じでScalaでは外部コマンドがもの凄く簡単に打てる。あまりに便利過ぎてつい使い過ぎてしまいそうで怖い。

Scalaは大規模開発の用途から、日常のちょっとした処理まで様々な用途で使えるけど、用途によってはこうした機能を活用しても良さげな気がする。