1. はじめに
本稿では、実際に活用されているシェルスクリプトを取り上げ、シェルスクリプトを作成する際のいくつかのテクニックを紹介します。どれも基本的で応用範囲の広いテクニックなので、多くの読者に参考にしていただければと思います。
2. シェルスクリプト
サーバ運用にしろ業務システム運用にしろ、LinuxやFreeBSDといったUNIX系OSを活用する場合、ツールとしてのシェルスクリプトは欠かせない存在です。いかにして建設的に手を抜きつつ、処理を自動化して効率化を達成できるかどうかは、シェルスクリプトの活用いかんにかかっていると言ってもいいでしょう。
もちろんそれはシェルスクリプトに限らず、ほかのプログラミング言語でも問題ありません。シェルスクリプトと他のプログラミング言語を比べた場合、シェルで使っているコマンドをそのまま使えることが、シェルスクリプトの最大の特徴であり利点です。
他人が作成したシェルスクリプトを読むことはなかなか困難です。であればPythonのようにもっと読みやすいプログラミング言語を使えばよいということになりますが、それを踏まえてもシェルで使っているコマンドをそのまま使えるというのは、何事にも代え難い利点です。しかも、シェルスクリプトで得たテクニックはシェルでも使えます。シェルやシェルスクリプトの上達は、直接的に仕事の早さと便利さにつながります。シェルスクリプトが管理者を魅了して止まないのは、そんなところにも理由があるのです。
読みにくいシェルスクリプトではありますが、上達のための最も有力な方法は、他人のシェルスクリプトを読んで技を覚えることです。本稿では実際に使っているシェルスクリプトを取り上げ、応用範囲の広いテクニックを紹介します。
3. アーカイブ展開コマンド extract(1)
一括化して圧縮されたファイル、いわゆるアーカイブや書庫と呼ばれるファイルを展開するには、それぞれの形式に応じたコマンドを使います。例えばTarballであればtar(1)コマンドを使うし、Zipballならunzip(1)コマンドを使います。指定するオプションもさまざまです。
コマンドやオプションを覚えるのが面倒だという理由で、複数のアーカイブを選別してそれぞれ適切なコマンドを実行する、いわゆる別インターフェイスとなるコマンドを作成することは、シェルスクリプトを覚えた方であれば誰しも一度は考えるものでしょう。その実装例がリスト3.1です。
#!/bin/sh
# Copyright (c) 2005, 2006 Daichi GOTO <daichi@ongs.co.jp>
# All rights reserved.
EXTRACTHOMEDIR="$HOME"/.extract
usage_msg='usage:
extract files...
-h print help message
-v print version'
# check the option arguments
#
while getopts hv option
do
case "$option" in
h)
echo "${usage_msg}"
exit 0
;;
v)
echo "version 1.9"
exit 0
;;
esac
done
shift $(($OPTIND - 1))
# create extract home directory
#
if [ ! -d "$EXTRACTHOMEDIR" ]
then
mkdir "$EXTRACTHOMEDIR"
fi
# utility functoins
#
target_has_topdir()
{
exec ls "$1" | {
IFS= read directory
IFS= read eol
if [ -d "$1/$directory" -a -z "$eol" ]
then
return 0
fi
return 1
}
}
move_to_currentpath()
{
workdir="$1"
target="$2"
if target_has_topdir "$workdir"
then
if [ -e $(basename "$workdir"/*) ]
then
EXTRACTDIR=$(mktemp -d "$(basename $workdir/*)".XXXXXX)
|| return 1
echo "extracting $target to $EXTRACTDIR"
else
EXTRACTDIR=./
echo "extracting $target to ./$(basename $workdir/*)"
fi
else
dirname="${target%%.[a-z]*}"
if [ -e "$dirname" ]
then
EXTRACTDIR=$(mktemp -d "$dirname".XXXXXX) || return 1
else
EXTRACTDIR=$(mktemp -d "$dirname") || return 1
fi
echo "extracting $target to $EXTRACTDIR"
fi
mv "$workdir"/* "$EXTRACTDIR"
return 0
}
for target
do
WORKDIR=$(mktemp -d "$EXTRACTHOMEDIR"/XXXXXX)
case "$target" in
*.[Tt][Gg][Zz]|*.[Tt][Aa][Rr].[Gg][Zz]|*.[Tt][Aa][Rr].[Zz])
tar vzxf "$target" -C "$WORKDIR"
move_to_currentpath "$WORKDIR" "$target"
;;
*.[Tt][Bb][Zz]|*.[Tt][Aa][Rr].[Bb][Zz]2)
tar vjxf "$target" -C "$WORKDIR"
move_to_currentpath "$WORKDIR" "$target"
;;
*.[Gg][Zz])
gzip -d "$target"
move_to_currentpath "$WORKDIR" "$target"
;;
*.[Zz][Ii][Pp]|*.[JjWw][Aa][Rr])
check_required_program unzip /usr/ports/archivers/unzip/
unzip "$target" -d "$WORKDIR"
move_to_currentpath "$WORKDIR" "$target"
;;
*.[Ll][Zz][Hh])
check_required_program lha /usr/ports/archivers/lha/
lha ew="$WORKDIR" "$target"
move_to_currentpath "$WORKDIR" "$target"
;;
*)
echo "unkown compress type: $target"
;;
esac
rm -r -f "$WORKDIR"
done
リスト3.1には、それ以外の処理も加えてあります。ファイルやディレクトリが一つのディレクトリに収まっている上品なアーカイブは便利ですが、なかにはそうではないものもあります。ホームディレクトリでアーカイブを展開したがために、いくつのもファイルやディレクトリがホームディレクトリにバラまかれてしまった経験がある方もいることでしょう。
しかし、かといって、その都度展開するディレクトリを作成して作業するのは繁雑だし、ディレクトリが一つしかないアーカイブにも同じことをしていては二度手間です。リスト3.1には、必要がなければそのまま展開し、バラまいてしまうタイプのアーカイブは適切な名前のディレクトリ作成してそこに展開する処理が加えてあります。
リスト3.1で採用したアルゴリズムは次のようなものです。
- アーカイブを展開するディレクトリを作成
- そのディレクトリへアーカイブを展開
- 展開されたものがひとつのディレクトリに収まっているかそうでないか調べる
- そうであれば、展開したディレクトリをカレントディレクトリへ移動
- そうでなければ、カレントディレクトリに新しくディレクトリ作成し、そこへ展開したものを移動
- 作業ディレクトリを削除
これはなかなか便利な機能で、アーカイブを展開する機会が多い方には便利に使っていただけると思います。その過程でいくつものテクニックが使われているので、シェルスクリプトの実用教材としても面白いでしょう。

|| return 1
echo "extracting $target to $EXTRACTDIR"
else
EXTRACTDIR=./
echo "extracting $target to ./$(basename $workdir/*)"
fi
else
dirname="${target%%.[a-z]*}"
if [ -e "$dirname" ]
then
EXTRACTDIR=$(mktemp -d "$dirname".XXXXXX) || return 1
else
EXTRACTDIR=$(mktemp -d "$dirname") || return 1
fi
echo "extracting $target to $EXTRACTDIR"
fi
mv "$workdir"/* "$EXTRACTDIR"
return 0
}
for target
do
WORKDIR=$(mktemp -d "$EXTRACTHOMEDIR"/XXXXXX)
case "$target" in
*.[Tt][Gg][Zz]|*.[Tt][Aa][Rr].[Gg][Zz]|*.[Tt][Aa][Rr].[Zz])
tar vzxf "$target" -C "$WORKDIR"
move_to_currentpath "$WORKDIR" "$target"
;;
*.[Tt][Bb][Zz]|*.[Tt][Aa][Rr].[Bb][Zz]2)
tar vjxf "$target" -C "$WORKDIR"
move_to_currentpath "$WORKDIR" "$target"
;;
*.[Gg][Zz])
gzip -d "$target"
move_to_currentpath "$WORKDIR" "$target"
;;
*.[Zz][Ii][Pp]|*.[JjWw][Aa][Rr])
check_required_program unzip /usr/ports/archivers/unzip/
unzip "$target" -d "$WORKDIR"
move_to_currentpath "$WORKDIR" "$target"
;;
*.[Ll][Zz][Hh])
check_required_program lha /usr/ports/archivers/lha/
lha ew="$WORKDIR" "$target"
move_to_currentpath "$WORKDIR" "$target"
;;
*)
echo "unkown compress type: $target"
;;
esac
rm -r -f "$WORKDIR"
done