bash シェルスクリプト入門 -シェルスクリプトのいろは-

意外に知らない人が多いシェルスクリプトについて、基本的な部分の解説。主に初心者をターゲットとした内容。

シェルスクリプトとは?

複数のコマンドと条件分岐やループ処理等を使用し、一連の処理を実現するプログラムのことである。bash、zsh などのシェルを利用し、シェルによって解釈・実行されるスクリプトなので、シェルスクリプトと呼ばれる。

まれに 「シェルスクリプト」を単に「シェル」と呼ぶ人がいるが、それは厳密には間違いである。「シェル」と「シェルスクリプト」は、無関係ではないが厳密には別物である。

当ページ 管理人のネット上・実社会での観測結果によると、「シェルスクリプト」を「シェル」と呼ぶ人のスキルは著しく低い傾向がある。

同感である。

シェルスクリプトを作成する

シェルスクリプトを作成するにあたって、まず使用するシェルを決める必要がある。古い UNIX 環境では sh (Bourne Shell) や ksh (Korn Shell) が使用されていたが、最近の Linux 環境であれば bash や zsh でシェルスクリプトを作成するのが一般的である。

当サイトでは bash を用いてシェルスクリプトを作成している。

シェルには他にも有名な csh / tcsh があるが、決して csh / tcsh でシェルスクリプト作成してはいけない。詳細は『有害な csh プログラミング』を参照のこと。

まずは簡単なシェルスクリプトを作成してみる。vi で helloworld.sh という名前で新規ファイルを作成し、以下の内容を入力する。

#!/bin/bash

echo "Hello World !"

exit 0

先頭に指定した #! で始まる「シバン」と呼ばれる文字列は、スクリプトを実行するためのインタプリタを指定している。つまり、「このシェルスクリプトは bash によって解釈・実行されます」と、宣言するためのものである。

これは決まり文句のようなものなので、必ず 1 行目に指定すること

シェルスクリプトを実行する

さっそく helloworld.sh を実行してみる。シェルスクリプトを実行する方法は大きく分けて 2 つある。

  1. bash コマンドに実行するシェルスクリプトのファイルを指定する。e.g. $ bash helloworld.sh
  2. シェルスクリプトのファイルをコマンドとして直接実行する。e.g. $ ./helloworld.sh

bash コマンドで実行する

シェルスクリプトを実行するにはこれが一番簡単な方法だと思う。bash コマンドに引数として実行するシェルスクリプトのファイル名を指定するだけ。

$ bash helloworld.sh
Hello World !

この場合、ファイル名だけを指定して実行できるのは、そのファイルがカレントディレクトリにある場合のみだ。ファイルが別のディレクトリにある場合は、絶対パスもしくは相対パスで指定すること

$ cd /tmp
$ bash helloworld.sh
bash: helloworld.sh: そのようなファイルやディレクトリはありません
$ bash /home/sunone/helloworld.sh
Hello World !

コマンドとして直接実行する

シェルスクリプトを (ls や cat のように) コマンドとして直接実行するためには、主に以下の 2 つの条件が必要。

  1. ファイルのパーミッションに実行権が付与されていること
  2. シェルがファイルの場所を特定できること

まずはじめにファイルの実行権を確認してみる。

$ ls -l helloworld.sh
-rw-rw-r-- 1 sunone sunone 43  5月  7 15:23 helloworld.sh

ls -l の結果の一番左から 2 ~ 4 文字目が自分の持っている権限 (正確にはファイル所有者の権限) を表している。rw- なので、「読み込み権限(r)」と「書き込み権限(w)」があることになる。

実行権にあたる x がないので、chmod コマンドでこれを付与する。

$ chmod +x helloworld.sh
$ ls -l helloworld.sh
-rwxrwxr-x 1 vagrant vagrant 43  5月  7 15:23 helloworld.sh

実行権を意味する x が表示されていれば、正常に実行権が付与されたことになる。

これで前述の 1 (ファイルのパーミッションに実行権が付与されていること) の条件を満たしたので、 次は 2 (シェルがファイルの場所を特定できること) を満たす条件を考える。

「シェルがファイルの場所を特定できる」とは要するに、

  1. 絶対パス指定でコマンドを実行 e.g. $ /home/sunone/helloworld.sh
  2. 相対パス指定でコマンドを実行 e.g. $ ./helloworld.sh
  3. PATH の通ったディレクトにファイルを置いてコマンドを実行 e.g. $ helloworld.sh

の 3 つのいずれかの場合に相当する。いずれも満たしていないとシェルはコマンドの場所を特定できないのでエラーとなる。

$ helloworld.sh
bash: helloworld.sh: コマンドが見つかりません

シェルは実行したコマンドが、カレントディレクトリ内にあったとしても、それを実行してはくれない

シェルがコマンドを実行してくれるのは、

  • コマンドをフルパスもしくは相対パス指定で実行したとき、つまり明示的に場所を指定して実行したとき
  • 環境変数 PATH に設定されているディレクトリ内にコマンドが存在するとき

のみである。

ls や chmod がコマンド名だけで実行できるのは、環境変数 PATH にこれらコマンドが存在する /bin が設定されているためである。

実際に echo コマンドで環境変数 PATH を確認してみる。

$ echo $PATH
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/usr/local/bin:/usr/bin

helloworld.sh を実行するには、環境変数 PATH にカレントディレクトリ(.)を設定してもよいが、一般にカレントディレクトリを環境変数 PATH に設定するのは セキュリティ上好ましくないとされている (開発で使用している仮想環境、等であればそこまで気にする必要はないであろうが)。

参考までに、設定方法は以下のとおり。

$ export PATH="${PATH}:."

PATH はコロン区切りで設定するため、現在の PATH の設定値 ${PATH} にコロンを付加し、カレントディレクトリ(.)を追加したものを環境変数 PATH に再設定する (前方に設定したディレクトリほど優先度が高い、つまり同じコマンドがあれば前方のディレクトリが優先)。

さらに export コマンドで、現在ログイン中のシェルと、そこから派生したシェルで、指定した変数 (PATH) の値を有効にすることができる。

これで環境変数 PATH にシェルスクリプトファイルのディレクトリが指定されたので、コマンドとして実行できる。

$ helloworld.sh
Hello World !

シェルスクリプトの文法

まずシェルスクリプトの文法を解説する前に、シェルスクリプトのくせのある書き方が苦手だという初心者が多い理由について考えてみたい。個人的にはシェルスクリプトに慣れているので特にくせがあって苦手だという意識はないが、そう思う人と思わない人の違いはどこにあるのか。

あくまで個人的見解だが、シェルスクリプトに苦手意識を持っている人はシェルスクリプトを文法で捉えていることに原因があると思う。筆者はシェルスクリプトを文法ではなく、長いコマンドラインとして捉えている。

シェルスクリプトは他のプログラミング言語と違い、if や for といった制御文もコマンドであり、それらを含めてすべてコマンドの羅列にすぎない。シェルスクリプトの場合は、シンタックスエラーをコマンドエラーとして考えるとわかりやすい

例えばシェル変数を設定する場合、シェルスクリプトでは = の前後にスペースがあるとエラーになる。

$ var = "hoge"
bash: var: コマンドが見つかりません

これはシェルが変数 var を「スペースがあることによって var コマンドと判断」し、それを実行しようとしたために起きたエラーである。そのため、変数に値を設定するには、= の前後にスペースがあってはいけないのだ。

文法として考えるとなぜスペースがあるとエラーになるのかわかりづらいが、コマンドとして考えるとエラーの原因が分かりやすくなる。シェルスクリプトは「どんなコマンドが実行されるか?」を考えるのが基本だ。

筆者がシェルスクリプトを作成するときに意識しているポイントをまとめてみる。

  1. シェルスクリプトはすべてコマンドの羅列である
  2. シンタックスエラーではなくコマンドエラーとして考える
  3. シェルスクリプトはコマンドライン・テンプレートである
  4. 最終的にどのようなコマンドが実行されるかイメージする

1、2 に関しては前述のとおり。3、4 を理解できるようになると、初心者レベルを脱却できるはずだ。

シェルスクリプト実行時、シェルはまず変数を展開し、最終的に現れたコマンドを実行する。

#!/bin/bash

opt='-l'
ls $opt

例えば上記のシェルスクリプトの場合、シェルはls $opt の変数 opt を展開し、現れたコマンド ls -l を実行する。このようにシェルスクリプトは、コマンドラインを作成するテンプレートとしてみなすことができる。

シェルスクリプトはコマンドラインのテンプレートして捉え、最終的にどのようなコマンドが実行されるかをイメージして作成するとレベルアップの近道になるはずだ

if 文の使用例

# シャープから行末はコメントになります。

# if文の使用例
if [ 0 -eq 0 ]; then
  echo "equal."
fi

条件式には [ コマンドを使用します。これは if と同様に文法の一部ではなく /bin/[ というコマンドである。したがって [ の前後は必ずスペースが必要になる。それに続いている 0-eq もパラメータであるため、区切りとしてスペースが必要になる。

セミコロン ; は、同一行に複数コマンドを記述する場合に区切りとして使用する。C 言語などとは異なり行末に必ずセミコロンを付ける必要はないが、同一行内に複数コマンドを記述する場合はセミコロンが必要になる。

また、then は本来、改行後に記述する必要があるが行の無駄なので ; を使用して if と同一行内に記述するようにする。文法的にはセミコロンの前後にスペースは不要であるが、筆者は後ろにのみスペースを入れるスタイルを推奨する。

if 文での複数分岐も可能。

# read コマンドでキーボードから入力した文字列を、変数 str に設定する
read str

# 変数は必ず "" で囲んで使用する (変数に値が入っていない場合、"" がないと文法エラーになるため)
if [ "$str" = "hoge" ]; then
  echo "hoge"
elif [ "$str" = "fuga" ]; then
  echo "fuga"
else
  echo "unknown"
fi

elif を増やして更に分岐を加えることも可能であるが、そういった場合は次の case 文を使用した方がよいだろう。

case 文の使用例

# 各分岐の最後の ;; を忘れずに
case "$str" in
  "hoge" ) echo "hoge"
           echo "hoge" ;;
  "fuga" ) echo "" ;;
  * ) echo "unknown" ;;
esac

パターンの部分に正規表現は使用できないが、代わりにワイルドカード (すべての文字列と一致の「*」と、任意の一文字と一致の「?」) が使用可能。

for 文の使用例

次はループ処理を行う for 文。動作的には他の言語にある foreach 文と同様である。

# in に続く文字が次々に変数 i に代入される
for i in 0 1 2 3
do
  echo $i
done

while 文の使用例

while 文のループ継続条件式には、if 文と同様に [ コマンドを使用可能である。

while [ "$str" = "" ]
do
  read str
done

無限ループにするには : コマンド(ヌルコマンド)を指定する。: コマンドは一切の処理を行わずに、ただ正常終了するだけのコマンドである

while :
do
  read str

  if [ "$str" = "end" ]; then
    # break コマンドでループを抜ける
    break
  fi
done

このように while 文や if 文の条件式部分には、[ コマンド以外の任意のコマンドが指定可能である。 それは while 文や if 文が見ているのは条件式自体ではなく、「終了ステータス」というコマンドの実行結果を表す数値だからである。

# hogefile の中に文字列 hoge があるか?
if grep 'hoge' hogefile >/dev/null 2>&1
  echo "hoge found."
fi

>/dev/null 2>&1 を指定すると、コマンドの実行結果やエラーなどの出力をディスプレイに一切表示しないようになる (ちなみに grep コマンドの -sq オプションでも同様の効果を得られる)。

>/dev/null2>&1 は逆にしてはいけない。必ずこの順番で指定すること。

ちなみに「1」は標準出力、「2」はエラー出力を意味している。要するに >/dev/null 2>&1 は、「2 (エラー出力)」を「1 (標準出力)」にまとめて、さらに出力先を「/dev/null (ごみ箱)」に変更する、という意味になる。

終了ステータス

「終了ステータス」は、コマンド実行終了後に変数 $? へ自動的に設定されている。通常、コマンド実行成功の場合は「0」、コマンド実行失敗は「0 以外」となる。

touch コマンドで 0 バイトのファイル hoge を作成し、それを ls コマンドで参照してみる。

$ touch hoge
$ ls hoge
hoge
$ echo $?
0

直前に hoge ファイルを作成しているので、当然 ls コマンドは成功し、終了ステータスは 0 になる。

続けて rm コマンドで hoge ファイルを削除してから、同様にして ls コマンドを実行してみる。

$ rm hoge
$ ls hoge
ls: hoge: No such file or directory
$ echo $?
2

今度は hoge ファイルが存在しないため、ls コマンドの終了ステータスは 2 (0 以外)となっている。