実例で学ぶ脱初心者のためのシェルスクリプト(パラメータ置換編)

一度習得すると重宝するシェルスクリプトだが,慣れないうちはこれほど不便なものはないと思ってしまう.PerlRubyなどのスクリプト言語の方が使う機会が多いので,そっちで書けばいいじゃん,と思っていた.自分の場合はコマンドラインスクリプトも全てPerlで書いていた.


まぁ,シェルスクリプトも勉強したろ,と思って実験のバッチ処理に使っていた.自分の場合はファイル名の処理に困った.例えばhoge.arffというファイル名を探してきて,hoge.outという出力ファイルで保存したい.

その場合には${変数名%パターン名}という構文を用いる.こんな感じ.

for f in `find ./ -type f -name "*.arff"`
do
  # hoge.arffの.arffを削除して新たにoutという
  OUTPUT_FILE=${f%.arff}".out"

  COMMAND="java weka.classifiers.bayes.NaiveBayesMultinomial -t "$f

  echo $COMMAND" > "$OUTPUT_FILENAME
  $COMMAND > $OUTPUT_FILENAME
done

するとこんな感じで処理してくれる.

java weka.classifiers.bayes.NaiveBayesMultinomial -t ./experiment1/file1.arff > ./experiment1/file1.out
(以下略)

これはかなり便利!一括でファイル名を変更したり,コピーしたりするのにとても重宝する.それまで,hoge.arff.outというようにお尻に文字列をくっつけることしか出来なかったけれど,こんなことができるんだと思いました.


ちなみに,リダイレクト部分を$COMMANDに含まなかったのは,そのままコマンド呼び出ししようとすると,>以下の部分までをひとつのコマンドと解釈してしまうため,リダイレクトとして解釈してくれないから.これも一度かなりはまりました.


このfor文と``による組み合わせが自分の中ではかなり最強で.それまで,いちいちPerlスクリプトを書いていたことが,シェルスクリプトでもっと簡単に書けるようになった.
別にスクリプト書かなくても,シェルで直接for文打つようになったらシェル無しでは生きていけなくなりました.


  パイプだけで十分.そんなふうに考えていた時期が俺にもありました


次のケースは30分前に習得したことなのでかなりリアル.

Wekaの実験結果のファイルの中で,元のファイルのあるクラスの事例数が閾値未満の結果を除いて,全ての結果を新しいディレクトリ以下に保存する,という話.


かなりわかりづらいので,箇条書き

  • 結果が既に階層構造に保存されている=>この階層構造を保持したまま,新しいディレクトリ以下にコピーしたい
  • ただし,ある条件を満たすファイルだけ
    • 結果である*.outファイルだけでいい
    • 正例の事例数が閾値以上のもの.正例の数は*.arffファイルを見ればよい
    • hoge.arffの結果はhoge.outに保存されている.


こんな感じの課題.説明するといくらでも難しそうに言えるが,書いてみるととても簡単.スクリプトの流れはこんな感じ.

  1. 閾値コマンドラインから取得する
  2. findコマンドで.outファイルを全て探し出し,forループを回す
  3. 各ファイルについて以下の処理をする
    1. hoge.arffファイルの正例の数を数える(grep | wcで数える)
    2. 閾値以上の場合
      1. コピー先ディレクトリ名を生成
      2. コピー先ディレクトリが存在しなければ作成(mkdir -p)
      3. コピーする


言葉で説明するほうがややこしいな.というわけで書いたスクリプトをどうぞ.

#!/bin/sh

# 保存先のディレクトリ
SAVE_DIR='../HOGE/'

# コマンドライン引数がない場合エラー
if [ ! $1 ]; then
    echo "Input correct threshold."
    exit
fi

# 第1引数を閾値にする
THRESHOLD=$1
echo "Input Threshold is "$THRESHOLD

for f in `find ./ -type f -name "*.out"`
do
  ARFF_FILE=${f%.out}'.arff'

  # 該当ファイルの正例の数を数える.条件を変える場合ここを変更
  VALUE=`grep '"pos"}' $ARFF_FILE | wc -l`

  if [ $VALUE -ge $THRESHOLD ]; then

      # 先頭の../HOGE/./piyo/bar.out というようになってしまうので,./を除去
      TMP_PATH=$SAVE_DIR${f##./}
      TMP_DIR=`dirname $TMP_PATH`

      # 保存先のディレクトリが存在しなかったらmkdir -pで作成
      if [ ! -d $TMP_DIR ]; then
          echo $TMP_DIR" does not exist."
          echo 'mkdir -p '$TMP_DIR
          mkdir -p $TMP_DIR
      fi

      # ファイルのコピー
      CMD='cp '$f' '$SAVE_DIR${f##./}
      echo $CMD
      $CMD
  fi
done

いくつかポイントがあるが,まず

VALUE=`grep '"pos"}' $ARFF_FILE | wc -l`

正例のクラス名をposにしており,今回は全てSparse ARFFにしていたので,これでファイル毎の正例の数を数えることができる.できる人には当たり前なのかもしれないけれど,これ出来た瞬間に目の前が開けた感じがしました.


次に

TMP_PATH=$SAVE_DIR${f##./}

このけったいな${f##./}は,$fの中身を見て,前方に./がマッチした場合取り除くというもの.マッチしない場合は別になにもしない.


スクリプト内のコメントで書いてあるが,保存先が../HOGE/で対象ファイルが./hoge/bar/piyo.outで与えられている場合,そのまま結合すると../HOGE/./hoge/bar/piyo.outというように./が邪魔になる.


この方法で./を取り除くことができる.


実はこの${f%.arff}なのだが,%以下のパターンを除去するという意味があるらしい.パラメータ置換と呼ばれる手法で,かなり使い勝手がよい.


パラメータ置換というキーワードで検索すると色々出てきた.
以下はhttp://lwr.homelinux.com/shell_referrence.html#parmから引用.

    パラメータ置換
    --------------------------------------------------------------
    パラメータ展開        説明
    --------------------------------------------------------------
    ${param:-default}     param が空ならば、default の値を返す。
                          空でないならば、param の値を返す。
    ${param:=default}     param が空ならば、param に default の
                          値を設定し、その値を返す。
    ${param:?default}     param が存在しないか、空の場合は
                          param:   default と表示してアボートする。
    ${param:+default}     paramが空でない場合には    default を返す。
    ${#param}             param の長さを返す。
    ${param%word}         param の末尾を起点として、word に一致
                          する param の最短部分を削除し、残りの
                          部分を返す。
    ${param%%word}        param の末尾を起点として、word に一致
                          する param の最長部分を削除し、残りの
                          部分を返す。
    ${param#word}         param の先頭を起点として、word に一致
                          する param の最短部分を削除し、残りの
                          部分を返す。
    ${param##word}        param の先頭を起点として、word に一致
                          する param の最長部分を削除し、残りの
                          部分を返す。

おっと引用元があるみたい.


しかし,パラメータ置換まじ便利だな.