105.2 レッスン 1
Certificate: |
LPIC-1 |
---|---|
Version: |
5.0 |
Topic: |
105 シェルとシェルスクリプト |
Objective: |
105.2 簡単なスクリプトの作成とカスタマイズ |
Lesson: |
1 of 2 |
はじめに
Linuxシェル環境では、シェル組み込みコマンドと、さまざまなプログラムが提供する スクリプト と呼ばれるコマンドを含むファイルを組み合わせて、ユーザーやシステム固有のタスクを自動化できます。実際に、オペレーティングシステムの保守作業の多くは、一連のコマンド、条件判断、条件ループなどから成るスクリプトで実行されます。オペレーティングシステム自体にかかわるタスクを行うスクリプトが多いですが、スクリプトを用いてファイルの名前変更を一括して行ったり、データの収集や解析を行ったり、あるいはコマンドを連続して実行するといった、ユーザー自身のタスクを行うことにも役立ちます。
スクリプトはテキストファイルにすぎませんが、プログラムのように動作します。本体のプログラム(インタプリタ)は、スクリプトファイルに書かれている命令を読み取って実行します。あるいは、Linuxシェルセッションと同様に(スクリプトを含む)コマンドを入力するたびに実行する対話型セッションで、インタプリタを利用することもできます。 エイリアスやシェル関数として実装できないような複雑な処理を行いたい場合に、命令とコマンドをグループ化するためにスクリプトファイルを使用します。さらに、スクリプトファイルは単なるテキストファイルであるため、テキストエディタのみで作成・変更できますから、容易にメンテナンスすることができます。
スクリプトの構造と実行方法
スクリプトファイルとは、対応するインタープリタによって実行されるコマンドの並びを内容とするファイルです。インタープリタがスクリプトを読み取る方法はさまざまですが、シェルセッションとはまったく異なります。スクリプトファイルの先頭には文字 #!
(シェバン と読みます)が置かれていて、その後に続く文字列がインタープリタです。Bashスクリプトの場合は、最初の行が #!/bin/bash
になり、続くすべてのコマンドは /bin/bash
によって解釈されます。ハッシュ文字 #
で始まる行は無視されるので、コメントや覚え書きを置くために使用します。空白行も無視されます。最も簡単なシェルスクリプトは次のようになります:
#!/bin/bash # A very simple script echo "Cheers from the script file! Current time is: " date +%H:%M
このスクリプトには、/bin/bash
によって解釈される2つの命令のみがあります。組み込みコマンド echo
と、通常コマンド date
です。スクリプトファイルを実行する最も基本的な方法は、スクリプトファイルのパスを引数としてインタプリタを実行することです。つまり、この例が現在のディレクトリの script.sh
という名前のファイルに保管されている場合は、次のコマンドで実行します:
$ bash script.sh Cheers from the script file! Current time is: 10:57
echo
コマンドは文字列を表示した後に改行を出力しますが、オプション -n
でこの動作を抑止できます。つまり、スクリプトで echo -n
を使用すると、両方のコマンドの出力が同じ行に表示されます。
$ bash script.sh Cheers from the script file! Current time is: 10:57
必須ではありませんが、ファイル名にサフィックス .sh
を付けておくと、一覧や検索の際にシェルスクリプトを識別しやすくなります。
Tip
|
シェルは、 |
他のユーザーにスクリプトを実行させることがある場合は、適切なパーミッションが設定されていることが重要です。コマンド chmod o+r script.sh
は、システム内のすべてのユーザーに読み取り権限を与えて、コマンド bash
の引数にスクリプトファイルのパス名を指定することで script.sh
を実行できるようにします。あるいは、スクリプトファイルに実行パーミッションをセットして、通常のコマンドのようにファイルを実行できるようにします。スクリプトに実行パーミッションをセットするには、コマンド chmod
を使用します。
$ chmod +x script.sh
実行パーミッションを有効にすると、現在のディレクトリにある script.sh
という名前のスクリプトファイルを、コマンド ./script.sh
で直接実行できます。PATH
環境変数にリストされているディレクトリにスクリプトファイルを置けば、パスがなくても実行できます。
Warning
|
スクリプトファイルに SetUID や SetGID のビットをセットしても、期待するように実行権限を変更する事はできません。起動されるプログラムはインタプリタであり、スクリプトファイルではないからです。 |
シェルスクリプトのファイルでは、コマンドのインデントや書き方はそれほど厳密ではありません。スクリプトのすべての行は、先頭行から順に、普通のシェルコマンドとして実行されます。また、コマンドラインと同じルールが行毎に適用されます。例えば、2つ以上のコマンドをセミコロンで区切って同じ行に続けることができます。
echo "Cheers from the script file! Current time is:" ; date +%H:%M
この形式が便利な場合もありますが、1行に1つずつコマンドを置けばセミコロンで区切ったのと同じように続けて実行されるので、ほとんど使われません。つまり、Bashスクリプトファイルではセミコロンを改行文字に置き換えることができるのです。
スクリプトが実行されると、そこに含まれるコマンドは現在のセッションで直接実行されるのではなく、サブシェル と呼ばれる新しいBashプロセスによって実行されます。それにより、スクリプトが現在のセッションの環境変数を上書きしたり、現在のセッションに意図しない変更を残したりするのを防ぎます。現在のシェルセッションでスクリプトの内容を実行したい場合は、source script.sh
ないし . script.sh
(ドットとスクリプト名の間にスペースがあることに注意してください)を使って実行します。
通常のコマンド実行と同様に、スクリプトが実行を終えるまで、シェルプロンプトには戻りません。そして、その終了ステータスコードを $?
変数で参照することができるようになります。この動作を変更して、現在のシェルでスクリプトを実行したい場合は、スクリプト(ないしその他のコマンド)の前に exec
コマンドを置きます。こうすると、スクリプトの実行が終了すると、現在のシェルが終了しますから、ログインシェルから実行した場合はログアウトしてしまうことになります。
変数
シェルスクリプトの変数は、対話セッションと同じように機能します。たとえば、SOLUTION=42
(等号の前後にスペースを入れない)と書くと、SOLUTION
という名前の変数に値 42
を割り当てます。変数名には大文字を使用することが慣例ですが、必須ではありません。なお、変数名をアルファベット以外の文字で始めることはできません。(訳注: 日本語は使用できません。)
Bashスクリプトでは、ユーザーが定義する通常の変数だけではなく、特殊変数 と呼ばれる特別な変数も使えます。通常の変数とは異なり、特殊変数の名前は記号1文字ないし数字です。スクリプトに渡される引数やその他の有用な情報が、0
、*
、?
などの特殊変数に格納されます。ドルマークに続けてこれら変数の値を参照すると、以下の情報が返されます:
$*
-
スクリプトに渡されたすべての引数(すべての引数を連結した1つの文字列)
$@
-
スクリプトに渡されたすべての引数。
"$@"
のように二重引用符内で使用すると、それぞれの引数が二重引用符で囲まれます $#
-
引数の数
$0
-
スクリプトファイルの名前
$!
-
最後に実行されたプログラムのPID。
$$
-
現在のシェルのPID
$?
-
最後に終了したコマンドの終了ステータスコードを示す数値。POSIX標準プロセスの場合、数値
0
は、最後のコマンドが正常に実行されたことを意味します。これは、シェルスクリプトでも同様です。
位置変数 は、0
以外の1桁以上で示されるパラメータです。たとえば、変数 $1
はスクリプトに指定された最初の引数に対応し、$2
は2番目の引数に対応します。パラメータの位置が9より大きい場合は、${10}
、${11}
などのように、波括弧(中括弧)で囲みます。
一方、通常の変数には、スクリプト内でセットした値や、他のコマンドの出力を格納します。たとえば、コマンド read
をスクリプト内で使用すると、スクリプトの実行中にユーザーに入力を求めることができます。
echo "Do you want to continue (y/n)?" read ANSWER
入力された値が変数 ANSWER
に格納されます。変数名が指定されていない場合は、デフォルトで変数 REPLY
が使用されます。コマンド read
を使用して、複数の変数を同時に読み取ることもできます。
echo "Type your first name and last name:" read NAME SURNAME
この例では、変数 NAME
と SURNAME
に、空白で区切られた単語のそれぞれが割り当てられます。入力された単語の数が変数の数よりも多い場合、残りの単語は最後の変数に(空白で区切られて)格納されます。read
のオプション -p
を使用してユーザーに表示するメッセージを指定できるので、この例の echo
コマンドは冗長です。
read -p "Type your first name and last name:" NAME SURNAME
システムタスクを実行するスクリプトは、多くの場合、他のプログラムによって提供される情報を利用します。バッククオート を使うと、コマンドの出力を変数に格納することができます。
$ OS=`uname -o`
この例では、コマンド uname -o
の出力が、変数 OS
に格納されます。$()
でも同じ結果が得られます。
$ OS=$(uname -o)
変数名の前にハッシュ #
を付加すると、変数の長さ、つまり変数に含まれる文字数が返されます。ただし、この機能を使うときは、変数を明示するために波括弧を使用します。
$ OS=$(uname -o) $ echo $OS GNU/Linux $ echo ${#OS} 9
Bashでは1次元配列を格納する変数も使えるので、関連する複数の要素を1つの変数に格納できます。配列の各要素は数値のインデックスを使って読み書きします。通常の変数とは異なり、配列はBashの組み込みコマンド declare
で宣言する必要があります。たとえば、SIZES
という名前の変数を配列として宣言するにはこうします:
$ declare -a SIZES
括弧を使った表記法で項目のリストを定義すると、配列を暗黙的に宣言することもできます。
$ SIZES=( 1048576 1073741824 )
この例では、2つの大きな整数値が SIZES
配列に格納されます。配列要素を参照するには、波括弧と角括弧を使用します。配列のインデックスは0から始まるため、最初の要素の内容は ${SIZES[0]}
に、2番目の要素は ${SIZES[1]}
にあります。
$ echo ${SIZES[0]} 1048576 $ echo ${SIZES[1]} 1073741824
配列要素への代入は、読み取りとは異なり、波括弧なしで指定します(たとえば、SIZES[0]=1048576
)。通常の変数と同様に、配列内の要素の長さをハッシュ文字で参照できます(たとえば、SIZES
配列の最初の要素の長さは ${#SIZES[0]}
となり、この例では7です)。インデックスに @
または *
を指定すると、配列内の要素数が返されます。
$ echo ${#SIZES[@]} 2 $ echo ${#SIZES[*]} 2
また、コマンド置換を利用することで、コマンドの出力を初期要素とする配列を宣言することもできます。次の例は、現在のシステムでサポートされているファイルシステムを要素とする配列を作成する方法を示しています:
$ FS=( $(cut -f 2 < /proc/filesystems) )
コマンド cut -f 2 < /proc/filesystems
は、(/proc/filesystems
ファイルの2列目の列にリストされている)実行中のカーネルで現在サポートされているすべてのファイルシステムを出力します。そのため、FS
配列には、それぞれの要素に1つのファイルシステムが含まれます。デフォルトでは、空白文字(空白、タブ、改行)で区切られた単語が配列要素になりますので、さまざまな形式のテキストを使用して配列を初期化できます。
Tip
|
Bashでは、変数 |
算術式
Bashでは、組み込みコマンド expr
を使用して、実用的な整数算術演算を行えます。たとえば、$VAL1
と $VAL2
の2つの数値変数を足すには、次のコマンドを使います。
$ SUM=`expr $VAL1 + $VAL2`
例では、結果の値が $SUM
変数に格納されます。コマンド expr
は $(())
に置き換えることができるので、前の例は SUM=$(( $VAL1 + $VAL2 ))
と書き直すことができます。二重アスタリスク演算子は累乗を表すので、前の配列宣言 SIZES=( 1048576 1073741824)
は SIZES=( $((1024**2)) $((1024**3)) )
と書くことができます。
算術式でもコマンド置換が使えます。ファイル /proc/meminfo
には、RAMの空きバイト数などシステムメモリに関する詳細情報が含まれていますので例に取り上げましょう:
$ FREE=$(( 1000 * `sed -nre '2s/[^[:digit:]]//gp' < /proc/meminfo` ))
この例では、算術式の内部で sed
コマンドを使い、/proc/meminfo
の内容を解析しています。/proc/meminfo
ファイルの2列目には、キロバイト単位の空きメモリ量がありますから、算術式でその数値に1000を掛けて、RAMの空きバイト数を取得します。(訳注: sedで2行目(MemFree)だけを取り出し、数値以外の文字を取り除いていることに注意してください。)
条件付き実行
スクリプトの中には、スクリプトファイル内のすべてのコマンドを実行するのではなく、あらかじめ定義した条件に一致するコマンドのみを実行するものがあります。たとえば、保守用のスクリプトでは、コマンドの実行に失敗した場合にのみ、管理者にメールで警告メッセージを送信します。Bashにはコマンド実行の成否を評価することに限定した方法と、一般のプログラミング言語に見られるような汎用的な条件実行の方法が用意されています。
コマンドを &&
で区切ると、左側のコマンドでエラーが発生しなかった場合、つまり終了ステータスが 0
の場合にのみ、右側のコマンドが実行されます。(訳注: &&
は AND を示す条件演算子です。1つでも失敗するコマンドが現れた時点で結果が False に決まりますから、後続のコマンドを実行する必要が無くなる、というワケです。)
COMMAND A && COMMAND B && COMMAND C
コマンドを ||
で区切ると、動作が逆転します。すなわち、左側のコマンドでエラーが発生した場合、つまり終了ステータスが0ではない場合にのみ、右側のコマンドが実行されます。(訳注: ||
は OR を示す条件演算子です。1つでも成功するコマンドが現れた時点で結果が TRUE に決まりますから、後続のコマンドを実行する必要が無くなる、と言うワケです。)
すべてのプログラミング言語において、あらかじめ定義した条件に応じてコマンドを実行することは、最も重要な機能の一つです。条件に応じてコマンドを実行する最も簡単な方法は、組み込みコマンド if
を使うことです。これは、引数に与えられたコマンドがステータスコードに0(成功)を返す場合にのみ、ブロック内の1つないしは複数のコマンドを実行します。さまざまな条件を評価することができる test
コマンドが用意されていて、ほとんどの場合は if
と test
を組み合わせて使用します。次の例では、ファイル /bin/bash
が存在して実行できる場合に Confirmed: /bin/bash is executable.
というメッセージを表示します。
if test -x /bin/bash ; then echo "Confirmed: /bin/bash is executable." fi
test
コマンドに -x
オプションを指定すると、与えられたパスが実行可能なファイルである場合にのみ、ステータスコード 0 を返します。次の例では、test
の代わりに角括弧を使い、まったく同じ結果を得る別の方法を示しています。(訳注: セミコロンの位置に注意してください。]
までが、[
コマンドの引数になります。[
コマンドは、if ブロックを見やすく書くために用意された、test
コマンドの特別な呼び出し方法です。)
if [ -x /bin/bash ] ; then echo "Confirmed: /bin/bash is executable." fi
else
コマンドは if
ブロックのオプションであり、存在する場合は、条件式が真でない場合に実行する一連のコマンドを定義できます。
if [ -x /bin/bash ] ; then echo "Confirmed: /bin/bash is executable." else echo "No, /bin/bash is not executable." fi
Bashインタープリタが条件ブロックの終了を判断できるように、if
ブロックは常に fi
で終わる必要があります。
スクリプトからの出力
ファイル操作のみを行う含むスクリプトであっても、進捗に関するメッセージを標準出力に表示することは重要です。ユーザーに問題を知らせられますし、そのメッセージをログとして残すこともできます。
Bashの組み込みコマンド echo
は、単純な文字列を表示するためによく使われますが、いくつかの拡張機能も備えています。-e
オプションを指定すると、エスケープシーケンス (バックスラッシュと1文字) を使って特殊文字を表示できます。例えば、以下のようになります:
#!/bin/bash # Get the operating system's generic name OS=$(uname -o) # Get the amount of free memory in bytes FREE=$(( 1000 * `sed -nre '2s/[^[:digit:]]//gp' < /proc/meminfo` )) echo -e "Operating system:\t$OS" echo -e "Unallocated RAM:\t$(( $FREE / 1024**2 )) MB"
オプションなしの echo
では引用符を使わなくても構いませんが、オプション -e
を使用する場合は引用符で囲む必要があります。前のスクリプトでは、両方の echo
コマンドでテキストの位置を合わせるためにタブ文字 \t
を使用して、次のような出力にしています:
Operating system: GNU/Linux Unallocated RAM: 1491 MB
改行文字 \n
を使用すると出力行を区切ることができるので、2つの echo
コマンドを1つに結合してもまったく同じ出力が得られます。
echo -e "Operating system:\t$OS\nUnallocated RAM:\t$(( $FREE / 1024**2 )) MB"
ほとんどのテキストメッセージの表示は echo
コマンドで充分ですが、厳密な書式が必要となる場合があります。Bashの組み込みコマンド printf
を使用すると、変数の表示方法をより細かく制御できます。printf
コマンドでは、最初の引数に出力書式を指定します。コマンドラインに指定された順序で、プレースホルダが引数に置き換えられます。たとえば、前の例のメッセージを、次の printf
コマンドで生成できます。
printf "Operating system:\t%s\nUnallocated RAM:\t%d MB\n" $OS $(( $FREE / 1024**2 ))
テキスト用のプレースホルダ %s
が $OS
変数の内容で置き換えられ、整数用の %d
プレースホルダが RAM内の空き容量(メガバイト単位)に置き換えられます。printf
はテキストの最後に改行文字を追加しないので、必要に応じて改行文字 \n
を出力書式の最後に置く必要があります。パターン全体を単一の引数として解釈する必要があるので、引用符で囲みます。
Tip
|
|
printf
では、書式を示す文字列と、出力に埋め込む変数が独立しています。すなわち、書式を示す文字列を別の変数に格納することもできます。
MSG='Operating system:\t%s\nUnallocated RAM:\t%d MB\n' printf "$MSG" $OS $(( $FREE / 1024**2 ))
この方法は、ユーザーの要件に応じて出力形式を変更する場合に特に便利です。たとえば、何種類かのCSV(カンマ区切り)リストを出力する場合に、種類ごとに書式文字列を切り替えて表示するスクリプトを簡単に作成できます。
演習
-
read
コマンドの-s
オプションは、入力された内容を画面に表示しないので、パスワードを入力するときに便利です。read
コマンドで、入力された内容を見せずに、その値をPASSWORD
変数に格納するにはどうしますか? -
whoami
は、コマンドを呼び出したユーザーのユーザー名を表示するコマンドで、スクリプト内でコマンドを実行しているユーザーを識別するためよく使われます。Bashスクリプト内で、whoami
コマンドの出力をWHO
という名前の変数に格納するにはどうしますか? -
apt-get dist-upgrade
が成功したときだけsystemctl reboot
を実行したい場合に、apt-get dist-upgrade
とsystemctl reboot
コマンドの間に置くオペレータは何ですか?
発展演習
-
新しく作成したBashスクリプトを実行しようとしたら、次のエラーメッセージが表示されました:
bash: ./script.sh: Permission denied
ファイル
./script.sh
が自作のものである場合、このエラーの原因として考えられることは何ですか? -
do.sh
という名前のスクリプトファイルが実行可能であり、undo.sh
という名前のシンボリックリンクがそれを指しているとします。スクリプト内から、呼び出したファイル名がdo.sh
であるかundo.sh
であるかを識別するにはどうしますか? -
メールサービスが適切に構成されているシステムでは、次のコマンドで通知メッセージをroot宛に送信できます:
mail -s "Maintenance Error" root <<<"Scheduled task error"
cronジョブ などの自動化タスクでは、このようなコマンドでエラーをシステム管理者に通知することがよくあります。(それが何であれ)コマンドの終了ステータスが失敗であった場合に、この
mail
コマンドを実行する if ブロックを記述してください。
まとめ
このレッスンでは、Bashのシェルスクリプトを理解し、記述するための基本的な概念について説明しました。シェルスクリプトは、あらゆるLinuxディストリビューションの中核であり、システム管理やユーザーのタスクを自動化するための非常に柔軟な方法を提供しています。レッスンでは、以下のテーマを取り上げました:
-
シェルスクリプトの構造と、スクリプトファイルのパーミッション
-
スクリプトにおける引数
-
変数を使用したユーザー入力の読み取りと、コマンド出力の利用
-
Bashの配列
-
簡単なテストと条件実行
-
出力の書式指定
以下のコマンドと手順を紹介しました:
-
コマンド置換、配列の展開、算術式
-
||
および&&
演算子を使用した条件付きコマンド実行 -
echo
-
chmod
-
exec
-
read
-
declare
-
test
-
if
-
printf
演習の解答
-
read
コマンドの-s
オプションは、入力された内容を画面に表示しないので、パスワードを入力するときに便利です。read
コマンドで、入力された内容を見せずに、その値をPASSWORD
変数に格納するにはどうしますか?read -s PASSWORD
-
whoami
は、コマンドを呼び出したユーザーのユーザー名を表示するコマンドで、スクリプト内でコマンドを実行しているユーザーを識別するためよく使われます。Bashスクリプト内で、whoami
コマンドの出力をWHO
という名前の変数に格納するにはどうしますか?WHO=`whoami`
またはWHO=$(whoami)
-
apt-get dist-upgrade
が成功したときだけsystemctl reboot
を実行したい場合に、apt-get dist-upgrade
とsystemctl reboot
コマンドの間に置くオペレータは何ですか?&&
オペレータ。apt-get dist-upgrade && systemctl reboot
発展演習の解答
-
新しく作成したBashスクリプトを実行しようとしたら、次のエラーメッセージが表示されました:
bash: ./script.sh: Permission denied
ファイル
./script.sh
が自作のものである場合、このエラーの原因として考えられることは何ですか?./script.sh
ファイルに実行権限が付いていません。 -
do.sh
という名前のスクリプトファイルが実行可能であり、undo.sh
という名前のシンボリックリンクがそれを指しているとします。スクリプト内から、呼び出したファイル名がdo.sh
であるかundo.sh
であるかを識別するにはどうしますか?位置変数
$0
に、スクリプトの呼び出しに使用されたファイル名が格納されています。 -
メールサービスが適切に構成されているシステムでは、次のコマンドで通知メッセージをroot宛に送信できます:
mail -s "Maintenance Error" root <<<"Scheduled task error"
cronジョブ などの自動化タスクでは、このようなコマンドでエラーをシステム管理者に通知することがよくあります。(それが何であれ)コマンドの終了ステータスが失敗であった場合に、この
mail
コマンドを実行する if ブロックを記述してください。if [ "$?" -ne 0 ]; then mail -s "Maintenance Error" root <<<"Scheduled task error"; fi