シェルスクリプトのパスとパイプ

考えてみるとここ1年はJavaのコードはほとんど書いていなくてそれよりはシェルスクリプトを書いている機会の方が多かった。

なのでここら辺でシェルスクリプトを書いていてハマったところというかちょっとしたTipsをメモっておこうと思う。前提としてBashである。


まずはパスについての話。

シェルスクリプトでパスをべた書きしているとポータビリティが失われてしまい別のマシンに移行する場合に困るケースが多い。

なのでこれをスマートに解決したいわけである。

かといって単純に相対パスを使っていると困るケースがある。

例えばある基準となるディレクトリ${BASE_DIR}があってその下にシェルスクリプトをおくディレクトリscriptsとログをおくディレクトリlogという構成にしたいとしよう。

 scripts/
 log/

この場合にシェルスクリプトの中で

echo ... > ../log/log.txt

とかやるとシェルスクリプトを実行する場所がscriptsに固定されてしまう。

それで良いケースもあるだろうが、もうちょっとスマートにやる場合は基準となるディレクトリ${BASE_DIR}の絶対パスを求めてそれを使うことである。

どう求めるかというとこんな感じ。

BASE_DIR=$(cd $(dirname $0);pwd)

説明はまるっと借用させていただくw

$0は実行中のシェルスクリプトのファイル名。
dirnameを使うことで、シェルスクリプトディレクトリーが取得できる。
ただしこれは相対パスかもしれないので、cdでそのディレクトリーに移動し、pwdでその場所(絶対パス)を取得している。

UNIXシェルスクリプトメモ(Hishidama's UNIX shell script Memo)

このように設定した${BASE_DIR}を使ってパスを作ればよい。

echo ... > ${BASE_DIR}/log/log.txt

この手の共通的な設定は別ファイル、例えばconfig.shにくくりだして読み込むのがよろしい。

こんな感じにね。

cwd=`dirname $0`

. ${cwd}/config.sh


つづいてはパイプ周りの話。

例えばあるディレクトリ${WORK_DIR}にあるデータファイル全てを1つ1つ処理したい。
ただし仕様としてある1つのファイルでエラーが起きた場合も処理を続行したい。
しかし1ファイルでも異常があったら最終的には異常終了にしたい。
全てのファイルを正常に処理できたときのみ正常終了とする。

言葉だと伝わりづらいと思うが、シェルスクリプトで書くとこんな感じになるだろう。

#!/bin/sh

cwd=`dirname $0`

. ${cwd}/config.sh

exit_code=0

ls ${WORK_DIR} | while read datafile
do
    ${cwd}/process.sh ${WORK_DIR}/${datafile}
    if [ $? -ne 0 ]; then
      exit_code=1
    fi
done

exit ${exit_code}

しかしこの方法では仕様を満たさない。
ループの外側のexit_codeが1になることは無いから。
つまり1つのファイルでエラーが起きた場合でも全体としては正常終了する。

何故かというと | (パイプ)処理から先は別プロセスで起動していて、ループの外側と内側のexit_code変数は別物だから。

解決策はリダイレクトを使う。

#!/bin/sh

cwd=`dirname $0`

. ${cwd}/config.sh

exit_code=0

ls ${WORK_DIR} > datafile.list

while read datafile
do
    ${cwd}/process.sh ${WORK_DIR}/${datafile}
    if [ $? -ne 0 ]; then
      exit_code=1
    fi
done < datafile.list

exit ${exit_code}

これならOK。

リダイレクトするとそのファイルの処理を考えないといけないのが南天のど飴なのであるがこれは仕方ない。

似たような話だが、下記のようにすると多重ループをexitで抜けられない。

#!/bin/sh

cwd=`dirname $0`

. ${cwd}/config.sh

ls ${WORK_DIR} | while read dir
do
  ls ${WORK_DIR}/${dir} | while read datafile
  do
    ${cwd}/process.sh ${WORK_DIR}/${dir}/${datafile}
    if [ $? -ne 0 ]; then
      exit 1
    fi
  done
done

これはさっきと違って1ファイルでも異常があったら処理を続行せずに終了したいパターン。

これもリダイレクトすればOK。

#!/bin/sh

cwd=`dirname $0`

. ${cwd}/config.sh

ls ${WORK_DIR} > dirs.txt
while read dir
do
  ls ${WORK_DIR}/${dir} > dir.txt
  while read datafile
  do
    ${cwd}/process.sh ${WORK_DIR}/${dir}/${datafile}
    if [ $? -ne 0 ]; then
      exit 1
    fi
  done < dir.txt
done < dirs.txt

もしくはこんな感じでdoneの直後でexitする。

#!/bin/sh

cwd=`dirname $0`

. ${cwd}/config.sh

ls ${WORK_DIR} | while read dir
do
  ls ${WORK_DIR}/${dir} | while read datafile
  do
    ${cwd}/process.sh ${WORK_DIR}/${dir}/${datafile}
    if [ $? -ne 0 ]; then
      exit 1
    fi
  done
  if [ $? -ne 0 ]; then
    exit 1
  fi
done

いじょ。