105.2 レッスン 2
Certificate: |
LPIC-1 |
---|---|
Version: |
5.0 |
Topic: |
105 シェルとシェルスクリプト |
Objective: |
105.2 簡単なスクリプトをカスタマイズまたは作成する |
Lesson: |
2 of 2 |
はじめに
シェルスクリプトは通常、ファイルやディレクトリに関連する操作を自動化することを目的としています。これは、コマンドラインで手動で実行できる操作と同じです。とはいえ、シェルスクリプトでは、Linuxオペレーティングシステムの多くの機能や構成情報を利用することが多く、ユーザーの文書ファイルだけを操作するとは限りません。
シェルスクリプトを作成するために、多くBash組み込みコマンドだけではなく、Linuxシステムに備わっている多くのコマンドラインユーティリティを組み合わせて使用する事もできます。
test コマンド
スクリプト言語としてのBashはファイルを処理することが多いので、Bashの組み込みコマンド test
には、ファイルシステムオブジェクト(基本的にはファイルとディレクトリ)のプロパティを評価するための多くのオプションがあります。ファイルとディレクトリに焦点を当てたテストでは、たとえば、あるタスクを実行するために必要なファイルとディレクトリが存在していて、読み取り可能であることを確認します。結果を if 文で評価して、テストが成功した場合には、適切な一群のアクションが実行されます。
test
コマンドでは、2つの異なる構文で式を評価できます。つまり、test
コマンドの引数として条件式を指定する方法と、角括弧内に条件式を置く方法があります。この場合 test
というコマンド名は使いません。例えば、/etc
が有効なディレクトリであるかどうかを判定する条件式は、test -d /etc
ないしは [ -d /etc ]
と書くことができます。(訳注: [
の後と ]
の前に空白が必要です。)
$ test -d /etc $ echo $? 0 $ [ -d /etc ] $ echo $? 0
特殊変数 $?
には、直前に実行したコマンドの終了ステータスコードが格納されて、値0はテストが成功したことを意味しますから、どちらの構文でも /etc
が有効なディレクトリであると評価しました。ファイルまたはディレクトリへのパスが変数 $VAR
に格納されていると仮定すると、次の式を test
への引数または角括弧内で使用できます。
-a "$VAR"
-
VAR
のパスがファイルシステムに存在する場合に成功します(-e
と同じ) -b "$VAR"
-
VAR
のパスがブロックデバイスである場合に成功します。 -c "$VAR"
-
VAR
のパスがキャラクターデバイスである場合に成功します。 -d "$VAR"
-
VAR
のパスがディレクトリである場合に成功します。 -e "$VAR"
-
VAR
のパスがファイルシステムに存在する場合に成功します(-a
と同じ)。 -f "$VAR"
-
VAR
のパスが存在し、それが通常ファイルである場合に成功します。 -g "$VAR"
-
VAR
のパスにSGIDパーミッションがある場合に成功します。 -h "$VAR"
-
VAR
のパスがシンボリックリンクである場合に成功します(-L
と同じ)。 -L "$VAR"
-
VAR
のパスがシンボリックリンクである場合に成功します(-h
と同じ)。 -k "$VAR"
-
VAR
のパスに スティッキー ビットパーミッションがある場合に成功します。 -p "$VAR"
-
VAR
のパスが pipe である場合に成功します。 -r "$VAR"
-
VAR
のパスが現在のユーザーに読み取れる場合に成功します。 -s "$VAR"
-
VAR
のパスが存在し、空でない場合に成功します。 -S "$VAR"
-
VAR
のパスがソケットである場合に成功します。 -t "$VAR"
-
VAR
のパスが端末で開かれている場合に成功します。 -u "$VAR"
-
VAR
のパスにSUIDパーミッションがある場合に成功します。 -w "$VAR"
-
VAR
のパスが現在のユーザーによって書き込み可能である場合に成功します。 -x "$VAR"
-
VAR
のパスが現在のユーザーによって実行可能である場合に成功します。 -O "$VAR"
-
VAR
のパスが現在のユーザーによって所有されている場合に成功します。 -G "$VAR"
-
VAR
のパスが現在のユーザーが所属するグループに属している場合に成功します。 -N "$VAR"
-
VAR
のパスが最後にアクセスされた後に変更されている場合に成功します。 "$VAR1" -nt "$VAR2"
-
VAR1
のパスがVAR2
のパスよりも後に変更された場合に成功します。 "$VAR1" -ot "$VAR2"
-
VAR1
のパスがVAR2
のパスよりも前に変更された場合に成功します。 "$VAR1" -ef "$VAR2"
-
VAR1
のパスとVAR2
のパスがハードリンクされている場合に成功します。
次に示すように、文字列の比較を行う事もできます。この場合、テストする変数を二重引用符で囲むことがお勧めです。条件式にはパラメーターが必要ですから、変数が空の場合には必要なパラメーターが無いために構文エラーが発生が発生することがあります。二重引用符で囲んでおくと、変数が空の場合でもそこに「空文字列」があることが分かります。
-z "$TXT"
-
変数
TXT
が空(サイズがゼロ)である場合に成功します。 -n "$TXT"
ortest "$TXT"
-
変数
TXT
が空でない場合に成功します。 "$TXT1" = "$TXT2"
ないし"$TXT1" == "$TXT2"
-
TXT1
とTXT2
が等しい場合に成功します。 "$TXT1" != "$TXT2"
-
TXT1
とTXT2
が等しくない場合に成功します。 "$TXT1" < "$TXT2"
-
TXT1
がTXT2
よりもアルファベット順で前にある場合に成功します。 "$TXT1" > "$TXT2"
-
TXT1
がTXT2
よりもアルファベット順で後にある場合に成功します。
言語が異なると、アルファベットの順序が異なることがあります。常に同じ結果を得たい場合は、環境変数 LANG
に C
をセットして(LANG=C
)、言語設定を上書きします。ログインシェルの設定を上書きしないように、シェルスクリプトの中で設定するようにしましょう。
数値比較の為の条件式もあります。
$NUM1 -lt $NUM2
-
NUM1
がNUM2
よりも小さい場合に成功します。(lt
は less than の略) $NUM1 -gt $NUM2
-
NUM1
がNUM2
よりも大きい場合に成功します。(gt
は grater than の略) $NUM1 -le $NUM2
-
NUM1
がNUM2
以下である場合に成功します。(le
は less than or equal の略) $NUM1 -ge $NUM2
-
NUM1
がNUM2
以上である場合に成功します。(ge
は grater than or equal の略) $NUM1 -eq $NUM2
-
NUM1
がNUM2
と等しい場合に成功します。 $NUM1 -ne $NUM2
-
NUM1
がNUM2
と等しくない場合に成功します。
論理演算を使って、条件式を反転したり、複数の条件式を結合することができます。
! EXPR
-
条件式
EXPR
の結果を反転します。 EXPR1 -a EXPR2
-
EXPR1
とEXPR2
の両方が真である場合に真になります。 EXPR1 -o EXPR2
-
2つの式の少なくとも1つが真である場合に真になります。
if
文のバリエーションに、case
文があります。case
文は、そこで指定した文字列(WORD)の値に応じて、処理内容を選択する場合に便利です。case
ブロックの中では、case
文で指定した文字列が、パターン — )
で終わる縦棒 |
で区切られた文字列のリスト — に続くブロックが実行されます。サンプルとして、引数に指定したディストリビューションが使用しているパッケージ形式を表示するスクリプトを見てみましょう。
#!/bin/bash DISTRO=$1 echo -n "Distribution $DISTRO uses " case "$DISTRO" in debian | ubuntu | mint) echo -n "the DEB" ;; centos | fedora | opensuse ) echo -n "the RPM" ;; *) echo -n "an unknown" ;; esac echo " package format."
パターンに一致した場合に実行するコマンドのリストは、次のいずれかで終了します:
;;
-
他のパターンとの一致を試みず、caseブロックの次に進みます。
:&
-
次のバターンと一致するかどうかにかかわらず、そのコマンドリストの実行に進みます。
::&
-
次のパターンとの一致を調べて、一致すればそのコマンドリストの実行に進みます。
サンプルの最後のパターンである *
は、いずれのパターンとも一致しなかった場合に実行されるコマンドリストを定義するためのものです。case
ブロックは、esac
(case
の逆順)で終了します。
このサンプルスクリプトの名前が script.sh
であり、最初の引数に opensuse
を指定して実行すると、次のように出力されます。
$ ./script.sh opensuse Distribution opensuse uses the RPM package format.
Tip
|
Bashには動作を調整するための特別な変数(フラグ)がいくつも用意されていて、その中の一つに |
検索文字列とパターンを評価する際には、チルダ展開、パラメータ展開、コマンド置換、算術展開が行われます。アイテムが引用符で囲まれている場合には、マッチングの前に引用符は削除されます。
ループ
スクリプトは同じタスクを何度も繰り返すためのツールとして利用されることがよくありますので、終了条件を満たすまで同じコマンドセットを繰り返し実行する ループ を作成するコマンドが備わっています。Bashでは、異なるループ構造を実現する for
、until
、while
の3種類のループコマンドがあります。
for
ループでは、指定したリストから1つずつアイテムを取り出して、それぞれの単語に対してコマンドリストを実行します。ループの前にリストから取り出したアイテムが変数に割り当てられますので、その変数をコマンドリストの中で使用して処理を進めます。すべてのアイテムを処理すると、ループが終了します。for
ループの構文は次の通りです。
for VARNAME in LIST do COMMANDS done
VARNAME
は取り出したアイテムを格納する変数で、LIST
は通常、空白で区切った単語の並びです。LIST
の区切り文字は、環境変数 IFS
で指定することができ、デフォルトでは スペース、タブ、改行 です。コマンドのリストは、do
と done
で囲みます。
次の例では、リストから数値を順に取り出して1つずつ変数 NUM
に格納して、コマンドリストを実行します。
#!/bin/bash for NUM in 1 1 2 3 5 8 13 do echo -n "$NUM is " if [ $(( $NUM % 2 )) -ne 0 ] then echo "odd." else echo "even." fi done
この例では、if
文の算術式で変数 NUM
が偶数か奇数かを判定しています。スクリプトファイルが現在のディレクトリにある script.sh
だとすると、次のように実行します。
$ ./script.sh 1 is odd. 1 is odd. 2 is even. 3 is odd. 5 is odd. 8 is even. 13 is odd.
Bashでは、二重括弧を用いて、C言語の for
文と同等のインデックスを用いるループも使えます。
#!/bin/bash SEQ=( 1 1 2 3 5 8 13 ) for (( IDX = 0; IDX < ${#SEQ[*]}; IDX++ )) do echo -n "${SEQ[$IDX]} is " if [ $(( ${SEQ[$IDX]} % 2 )) -ne 0 ] then echo "odd." else echo "even." fi done
このサンプルも、先の例と同じ結果を出力します。ここでは、リストから取り出したアイテムを格納する NUM
変数ではなく、配列の要素番号(インデックス)を格納する IDX
変数を使用しています。IDX
の値は、IDX = 0
によって初期化され、ループを1回実行する度に IDX++
によって1ずつ増加します。配列の要素数と比較する IDX < ${#SEQ[*]}
が成立する(真となる)間は、ループが繰り返されます。
until
ループは、条件が満たされるまで(たとえば test
コマンドが0(成功)を返すまで)コマンドリストを実行し続けるものです。前の例と同じループ条件を until
ループで書くと、次のようになります。
#!/bin/bash SEQ=( 1 1 2 3 5 8 13 ) IDX=0 until [ $IDX -eq ${#SEQ[*]} ] do echo -n "${SEQ[$IDX]} is " if [ $(( ${SEQ[$IDX]} % 2 )) -ne 0 ] then echo "odd." else echo "even." fi IDX=$(( $IDX + 1 )) done
until
ループでは、インデックスの初期化や更新のコードが別に必要となるため、for
ループよりも複雑に感じるかもしれませんが、ループの停止条件が単純な数値だけで決められない場合などには、より分かりやすく表現することができることがあります。ループが無限に実行されることが無いように、ループを回る度にインデックスを更新し、条件がいつかは満たされることが重要です。
while
ループは until
ループとは逆に、条件が満たされている間(たとえば test
コマンドが0(成功)を返している間)はコマンドリストを実行し続けるものです。前の例の until [ $IDX -eq ${#SEQ[*]} ]
を while
で書き直すと while [ $IDX -lt ${#SEQ[*]} ]
になります。IDX
の値が、SEQ
の要素数よりも小さい間は、ループを繰り返すことになります。
より複雑な例
最後に少し実用的なサンプルを見てみましょう。ユーザーが指定するファイルやディレクトリを、別のストレージデバイスに定期的にバックアップするスクリプトを考えます。自動化して定期的に実行するアプリケーションのひとつです。
スクリプトは、第1引数にバックアップ対象のディレクトリやファイルを探す起点ディレクトリを指定し、第2引数にバックアップ先の(別デバイス上の)ディレクトリを指定するものとします。バックアップするファイルやディレクトリは、あらかじめ ~/.sync.list
ファイルにリストしておきます。ここでは次のように、1行に1つのディレクトリないしファイルを記入したファイルを作成しました。
$ cat ~/.sync.list Documents To do Work Family Album .config .ssh .bash_profile .vimrc
このファイルにはファイルとディレクトリが混在しており、名前に空白が含まれているものもあります。このようなファイルをシェルスクリプで読み込む場合には、Bashの組み込みコマンド mapfile
を使うのが適当です。このコマンドは、任意のテキストを解析して、要素を配列変数に格納します。スクリプトファイルの名前は sync.sh
として、以下の内容を記入します。
#!/bin/bash set -ef # List of items to sync FILE=~/.sync.list # Origin directory FROM=$1 # Destination directory TO=$2 # Check if both directories are valid if [ ! -d "$FROM" -o ! -d "$TO" ] then echo Usage: echo "$0 <SOURCEDIR> <DESTDIR>" exit 1 fi # Create array from file mapfile -t LIST < $FILE # Sync items for (( IDX = 0; IDX < ${#LIST[*]}; IDX++ )) do echo -e "$FROM/${LIST[$IDX]} \u2192 $TO/${LIST[$IDX]}"; rsync -qa --delete "$FROM/${LIST[$IDX]}" "$TO"; done
順に説明していきましょう。まず、set
コマンドでシェルの動作オプションを調整します。-e
オプションは、コマンドが失敗した場合(終了ステータスが0ではない場合)に、スクリプトの実行を直ぐに終了します。-f
オプションは、ファイル名のグロブを無効にします。コマンドのオプションと同様に、2つのオプションをまとめて -ef
と指定することができます。このステップは必須ではありませんが、想定外の状況でスクリプトが予期しない動作をすることの予防になります。
スクリプトの本質的な処理内容は、大きく3つの部分に分けることができます。
-
パラメーターを確認する
FILE
変数には、コピーするファイルやディレクトリのリストを含むファイルのパス名 —~/.sync.list
— が格納されます。FROM
変数には起点となるディレクトリの、TO
変数にはコピー先ディレクトリの、パス名がそれぞれ格納されます。これらはユーザーが引数で指定するので、ディレクトリの存在を[ ! -d "$FROM" -o ! -d "$TO" ]
で確認し、いずれかが無い場合には簡単なヘルプメッセージを表示して、ステータス1で終了します。 -
バックアップ対象のファイルとディレクトリのリストをロードする
バックアップするファイルやディレクトリのリストを、
mapfile -t LIST < $FILE
で配列に読み取ります。-t
オプションは、それぞれのアイテムの末尾にある改行を削除することを指定しています(つまり1行を1要素として読み取ります)。$FILE
からリダイレクトで読み込んでいることに着目して下さい。 -
コピーしてユーザーに通知する
二重括弧形式の
for
ループでは、IDX
変数をインデックスとして使用して、LIST
からアイテムを一つずつ取り出して処理します。echo
コマンドは、処理内容をユーザーに通知するもので、Unicode文字\u2192
(右向き矢印)を使用するので-e
オプションを指定しています。rsync
コマンドは、コピー元とコピー先を比較して、変更されたファイルのみをコピーする、バックアップ目的にはとても便利なコマンドです。-q
オプションはエラーメッセージの出力を抑止し、-a
オプションはファイルやディレクトリの属性をコピー先でも維持する アーカイブモード を指示しています。--delete
オプションは、コピー元に無いファイルがコピー先に存在する場合にそれを削除するものです。
ユーザー carol
のホームディレクトリ(ここに .rsync.list
にリストされたアイテムがすべて存在するものとします)をコピー元とし、マウントされた外部ストレージデバイス /media/carol/backup
をコピー先ディレクトリとして指定すると、実行結果は次のようになります。
$ sync.sh /home/carol /media/carol/backup /home/carol/Documents → /media/carol/backup/Documents /home/carol/"To do" → /media/carol/backup/"To do" /home/carol/Work → /media/carol/backup/Work /home/carol/"Family Album" → /media/carol/backup/"Family Album" /home/carol/.config → /media/carol/backup/.config /home/carol/.ssh → /media/carol/backup/.ssh /home/carol/.bash_profile → /media/carol/backup/.bash_profile /home/carol/.vimrc → /media/carol/backup/.vimrc
この例では、バックアップ対象ファイルのリストを収めたファイル(.sync.list
)をチルダを使って指定しているので、carol
以外のユーザーでも利用することができます。この script.sh
を、PATH
が通ったディレクトリ(たとえば /usr/local/bin
)に置くとよいでしょう。
演習
-
test
コマンドで、変数FROM
に格納されているファイルパスが、変数TO
に格納されているファイルよりも新しいかどうかを確認するにはどうしますか? -
0から9までの数列を出力するスクリプトを作りたいのですが、次のスクリプトは0を無限に出力してしまいます。どう修正すればよいですか?
#!/bin/bash COUNTER=0 while [ $COUNTER -lt 10 ] do echo $COUNTER done
-
ソートされたユーザー名のリストを出力するスクリプトを作成しました。あるシステムでは次のように表示されます。
carol Dave emma Frank Grace henry
同じスクリプトで、別のシステムでは次のようにソートされます。
Dave Frank Grace carol emma henry
出力が異なる理由を説明してください。
発展演習
-
スクリプトに与えられたすべての引数で、Bash配列を初期化するにはどうしますか?
-
直感に反して、コマンド
test 1 > 2
がtrueと評価されるのはなぜですか? -
フィールド区切り文字を一時的に改行文字のみに変更し、それを元に戻すにはどうしますか?
まとめ
このレッスンでは、条件判定を行う test
コマンドと、条件によって実行するコマンドを制御する if
文ならびに case
文、さらにループ構造について説明しました。実用的なシェルスクリプトの例として、簡単なファイル同期スクリプトも示しました。このレッスンで説明した事柄は次の通りです。
-
if
とcase
による条件判断 -
ループの作り方:
for
、until
、while
-
配列やパラメータを列挙する方法
以下のコマンドと手順を紹介しました:
test
-
指定された条件を満たしているかどうかを判断します。
if
-
条件判断の結果に基づいてコマンドの実行を分岐します。
case
-
変数の値に応じてコマンドの実行を分岐します。
for
-
条件に基づいてコマンドの実行を繰り返します。
until
-
条件が満たされるまで、コマンドの実行を繰り返します。
while
-
条件が満たされている間、コマンドの実行を繰り返します。
演習の解答
-
test
コマンドで、変数FROM
に格納されているファイルが、変数TO
に格納されているファイルよりも新しいかどうかを確認するにはどうしますか?コマンド
test "$FROM" -nt "$TO"
は、FROM
変数のファイルがTO
変数のファイルよりも新しい場合に、終了コード0(成功)を返します。 -
0から9までの数列を出力するスクリプトを作りたいのですが、次のスクリプトは0を無限に出力してしまいます。どう修正すればよいですか?
#!/bin/bash COUNTER=0 while [ $COUNTER -lt 10 ] do echo $COUNTER done
ループの最後で、変数
COUNTER
の値を1増加します。つまり、COUNTER=$(( $COUNTER + 1 ))
をdone
の前に追加します。 -
ソートされたユーザー名のリストを出力するスクリプトを作成しました。あるシステムでは次のように表示されます。
carol Dave emma Frank Grace henry
同じスクリプトで、別のシステムでは次のようにソートされます。
Dave Frank Grace carol emma henry
出力が異なる理由を説明してください。
言語設定(ロケール)によって文字列の順序が異なります。言語による相違を回避するには、
LANG
環境変数にC
をセットしてから、ソートを行います。
発展演習の解答
-
スクリプトに与えられたすべての引数で、Bash配列を初期化するにはどうしますか?
PARAMS=( $* )
ないしPARAMS=( "$@" )
で、すべての引数を含むPARAMS
という配列を作成します。 -
直感に反して、コマンド
test 1 > 2
がtrueと評価されるのはなぜですか?エスケーブされていないので、
>
がリダイレクト指示と解釈されます。何も判断していないのでtest
コマンドは成功します。また、>
は文字列の比較を行うもので、数値の比較には使用できません。 -
フィールド区切り文字を一時的に改行文字のみに変更し、それを元に戻すにはどうしますか?
OLDIFS=$IFS
で、別の変数OLDIFS
にIFS
の値を格納しておきます。次にIFS=$'\n'
でフィールド区切り文字を変更し、コマンドを実行します。最後に、IFS=$OLDIFS
でIFS
の値を元に戻します。