3.3 レッスン 2
Certificate: |
Linux Essentials |
---|---|
Version: |
1.6 |
Topic: |
3 コマンドラインの力 |
Objective: |
3.3 コマンドをスクリプトにする |
Lesson: |
2 of 2 |
はじめに
このセクションでは、以下の例を使って、Bashスクリプトを見ていきます:
#!/bin/bash # A simple script to greet a single user. if [ $# -eq 1 ] then username=$1 echo "Hello $username!" else echo "Please enter only one argument." fi echo "Number of arguments: $#."
-
スクリプトは、インタープリタのパスを示す シェバン で始めます。
-
スクリプトには、その使い方を説明するコメントを入れておきます。
-
例示するスクリプトは、起動時に渡される1つの 引数 を使います。
-
このスクリプトには、組み込み変数
$#
の状態を判定する if文 を含んでいます。この変数には、引数の数がセットされます。 -
スクリプトに渡された引数の数が1であれば、最初の引数の値を新しい変数
username
にセットし、スクリプトはあいさつを表示します。その他の場合には、エラーメッセージを表示します。 -
最後に、スクリプトは引数の数を表示します。これはデバッグに役立ちます。
このスクリプトは、Bashスクリプトの他の機能を説明するためにも良いスタート点になります。
終了コード
このスクリプトには、2つの状態があることに気づくでしょう: "Hello <user>!"
と表示するか、エラーメッセージを表示するか、です。ほとんどのユーティリティも、同様の構成です。お馴染みの cat
を例に取り上げます。
cat
の実行に成功した場合と、失敗した場合を比べてみましょう。ここで取り上げるスクリプトは、new_script.sh
です。
$ cat -n new_script.sh 1 #!/bin/bash 2 3 # A simple script to greet a single user. 4 5 if [ $# -eq 1 ] 6 then 7 username=$1 8 9 echo "Hello $username!" 10 else 11 echo "Please enter only one argument." 12 fi 13 echo "Number of arguments: $#."
cat
コマンドの実行は成功して、-n
フラグによって行番号も出力されました。行番号はスクリプトのデバッグ時に役立ちますが、これはスクリプトの一部では ない ことに注意して下さい。
ここで、新登場の組み込み変数 $?
の値を確認します。出力に着目しましょう:
$ echo $? 0
次に、cat
が失敗する場合を見ていきます。まずエラーメッセージを確認して、$?
変数をチェックします。
$ cat -n dummyfile.sh cat: dummyfile.sh: No such file or directory $ echo $? 1
この動作について説明します: cat
を実行する度に 終了コード が返されます。終了コードは、コマンドが成功したのか、何かのエラーが発生したのかを示します。終了コード ゼロ はコマンドが成功裏に完了したことを意味します。利用する機会の多いLinuxコマンドのほとんどが、このように動作します。ゼロ以外の終了コードは、何らかのエラーを示します。$?
には、直前に実行されたコマンド の終了コードがストアされます。
ユーザーが終了コードを目にすることは通常ありませんが、スクリプトを書く場合には非常に役立ちます。例えば、ファイルをリモートのネットワークドライブにコピーするスクリプトを考えてみましょう。コピーに失敗する原因はたくさんあります: 例えば、ローカルマシンがネットワークに接続されていなかったり、リモートドライブが満杯であったり、などなど。コピーユーティリティの終了コードをチェックすることで、スクリプト実行中に問題の発生をユーザーに警告することができます。
終了コードを実装しておくことはとても良い習慣ですから、ここで実行しておきましょう。スクリプトには、成功と失敗の2つのパスがあります。成功を示すのにゼロを、失敗を示すのに1を使います。
1 #!/bin/bash 2 3 # A simple script to greet a single user. 4 5 if [ $# -eq 1 ] 6 then 7 username=$1 8 9 echo "Hello $username!" 10 exit 0 11 else 12 echo "Please enter only one argument." 13 exit 1 14 fi 15 echo "Number of arguments: $#."
$ ./new_script.sh Carol Hello Carol! $ echo $? 0
15行目の echo
コマンドが、完全に無視されることに注意して下さい。exit
を使用するとスクリプトはすぐに終了してしまうので、決してこの行に至ることがありません。
たくさんの引数を扱う
これまでのところ、スクリプトは1つの引数しか扱うことができませんでした。1つ以外の引数を指定すると、エラーになります。このスクリプトを、より汎用性の高いものにする方法を見ていきます。
直感的には、$2
や $3
など、もっとたくさんの位置変数を使えばよいと考えるかもしれません。残念ながら、ユーザーが幾つの引数を使うかを予測することはできません。この問題を解決するためには、別の組み込み変数を導入することが有用です。
スクリプトのロジックを変更しましょう。引数がゼロ個の時はエラーとしますが、それ以外の個数の引数では成功するようにします。この新しいスクリプトを friendly2.sh
と呼びます。
1 #!/bin/bash 2 3 # a friendly script to greet users 4 5 if [ $# -eq 0 ] 6 then 7 echo "Please enter at least one user to greet." 8 exit 1 9 else 10 echo "Hello $@!" 11 exit 0 12 fi
$ ./friendly2.sh Carol Dave Henry Hello Carol Dave Henry!
スクリプトに渡されたすべての引数を格納する組み込み変数が2つあります: $@
と $*
です。ほとんどの場合、どちらも同じように動作します。Bash は引数を 解析 して、空白を見つける度にそれぞれを区切ります。実際には、$@
の内容は以下のようになります。
|
|
|
|
|
|
他のプログラミング言語に慣れているならば、この種の変数が 配列 であると分かるでしょう。Bashでは、以下に示す arraytest
の中の FILES
変数のように、単純に要素を空白で区切って配列を作ることができます。
FILES="/usr/sbin/accept /usr/sbin/pwck/ usr/sbin/chroot"
配列は、複数の項目のリストです。まだ項目それぞれを扱う方法を紹介していないので、今のところはあまり役に立ちません。
Forループ
先に述べた arraytest
の例を見ていきましょう。この例では、FILES
という配列を指定しています。この変数をほどいて、個々の値に順にアクセスする方法が必要です。これを行うには、すべてのプログラミング言語にある Forループ と呼ばれる構造を用います。ここでは2つの変数を使います: ひとつは範囲で、もう一つは作業中の個別の値です。スクリプト全体を示します:
#!/bin/bash FILES="/usr/sbin/accept /usr/sbin/pwck/ usr/sbin/chroot" for file in $FILES do ls -lh $file done
$ ./arraytest lrwxrwxrwx 1 root root 10 Apr 24 11:02 /usr/sbin/accept -> cupsaccept -rwxr-xr-x 1 root root 54K Mar 22 14:32 /usr/sbin/pwck -rwxr-xr-x 1 root root 43K Jan 14 07:17 /usr/sbin/chroot
前記の friendly2.sh
の例に戻り、変数 $@
の中に含まれる値のリストを処理するように変更しましょう。わかりやすくするために、ここでは後者の(個々の値を保持する)変数を username
としましょう。 スクリプトは次のようになります。
1 #!/bin/bash 2 3 # a friendly script to greet users 4 5 if [ $# -eq 0 ] 6 then 7 echo "Please enter at least one user to greet." 8 exit 1 9 else 10 for username in $@ 11 do 12 echo "Hello $username!" 13 done 14 exit 0 15 fi
ここで定義した変数の名前は任意であり、配列のそれぞれの要素毎に do…done
の中のすべての行が実行されます。スクリプトからの出力を見てみましょう:
$ ./friendly2.sh Carol Dave Henry Hello Carol! Hello Dave! Hello Henry!
続いて、もう少し人間味のある出力にしてみましょう。あいさつを、1行にしたいと思います。
1 #!/bin/bash 2 3 # a friendly script to greet users 4 5 if [ $# -eq 0 ] 6 then 7 echo "Please enter at least one user to greet." 8 exit 1 9 else 10 echo -n "Hello $1" 11 shift 12 for username in $@ 13 do 14 echo -n ", and $username" 15 done 16 echo "!" 17 exit 0 18 fi
いくつかの注意点があります:
-
echo
に-n
を指定すると、表示後の 改行が抑止 されます。つまり、すべての echoが同じ行に出力されて、16行目の!
の後でのみ改行が表示されます。 -
shift
コマンドは、配列から先頭の要素を取り除きます。つまり、これが:
|
|
|
|
|
|
こうなります:
|
|
|
|
出力を見てみましょう:
$ ./friendly2.sh Carol Hello Carol! $ ./friendly2.sh Carol Dave Henry Hello Carol, and Dave, and Henry!
正規表現によるエラーチェック
ユーザーが入力したすべての引数を確認したいことがあります。例えば、friendly2.sh
に渡されるすべての名前が アルファベットのみ であることを確認し、特殊文字や数字はエラーとしたい場合です。この種の確認を行うには、grep
を使用します。
grep
では正規表現が使えることを思い出しましょう。
$ echo Animal | grep "^[A-Za-z]*$" Animal $ echo $? 0
$ echo 4n1ml | grep "^[A-Za-z]*$" $ echo $? 1
文字 ^
と $
は、それぞれ行の先頭と末尾を示します。[A-Za-z]
はアルファベットの大文字と小文字の範囲を示します。文字 *
は 繰り返し を示し、文字の範囲がゼロ個以上続く文字列と一致します。つまり grep
は、入力がアルファベット のみ である場合に成功し、そのほかの場合には失敗します。
次に注意すべきことは、grep
は一致したかどうかに応じた終了コードを返すことです。一致した場合は 0
を返し、一致しなかった場合は 1
を返します。これを利用してスクリプト内で引数をテストすることができます。
1 #!/bin/bash 2 3 # a friendly script to greet users 4 5 if [ $# -eq 0 ] 6 then 7 echo "Please enter at least one user to greet." 8 exit 1 9 else 10 for username in $@ 11 do 12 echo $username | grep "^[A-Za-z]*$" > /dev/null 13 if [ $? -eq 1 ] 14 then 15 echo "ERROR: Names must only contains letters." 16 exit 2 17 else 18 echo "Hello $username!" 19 fi 20 done 21 exit 0 22 fi
12行目で標準出力を /dev/null
にリダイレクトしていますが、これは出力を抑止する簡単な方法です。 ここでは grep
コマンドの出力は不要で、13行目で終了コードをテストしたいだけです。無効な引数を示すために、終了コード 2
を使っていることにも注意しましょう。異なるエラーを示すために異なる終了コードを使うのは良い習慣です。熟練ユーザーは、これらの終了コードをトラブルシューティングに利用します。
$ ./friendly2.sh Carol Dave Henry Hello Carol! Hello Dave! Hello Henry! $ ./friendly2.sh 42 Carol Dave Henry ERROR: Names must only contains letters. $ echo $? 2
演習問題
-
以下の
script1.sh
を見て下さい:#!/bin/bash if [ $# -lt 1 ] then echo "This script requires at least 1 argument." exit 1 fi echo $1 | grep "^[A-Z]*$" > /dev/null if [ $? -ne 0 ] then echo "no cake for you!" exit 2 fi echo "here's your cake!" exit 0
以下のコマンドの出力はどうなりますか?
-
./script1.sh
-
echo $?
-
./script1.sh cake
-
echo $?
-
./script1.sh CAKE
-
echo $?
-
-
以下の
script2.sh
を見て下さい:for filename in $1/*.txt do cp $filename $filename.bak done
このスクリプトの目的が何かを記述して下さい。
発展問題
-
任意個の引数を受け取り、10より大きい数値の引数のみを表示するスクリプトを作成してください。
まとめ
このレッスンでは、次のことを学びました:
-
終了コードとは何か、また、その意味と実装方法
-
コマンドの終了コードを確認する方法
-
for
ループとは何か、また、それを配列と共に使う方法 -
スクリプト内と入力をチェックするために、
grep
と正規表現、終了コードを使う方法
演習で使用したコマンド:
shift
-
配列の最初の項目を取り除きます。
特殊変数
$?
-
直前に実行したコマンドの終了コードを保持する。
$@
,$*
-
スクリプトに渡されたすべての引数を、配列として保持する。
演習の解答
-
以下の
script1.sh
を見て下さい:#!/bin/bash if [ $# -lt 1 ] then echo "This script requires at least 1 argument." exit 1 fi echo $1 | grep "^[A-Z]*$" > /dev/null if [ $? -ne 0 ] then echo "no cake for you!" exit 2 fi echo "here's your cake!" exit 0
以下のコマンドに対する出力は何ですか?
-
コマンド:
./script1.sh
出力:
This script requires at least 1 argument.
-
コマンド:
echo $?
出力:
1
-
コマンド:
./script1.sh cake
出力:
no cake for you!
-
コマンド:
echo $?
出力:
2
-
コマンド:
./script1.sh CAKE
出力:
here’s your cake!
-
コマンド:
echo $?
出力:
0
-
-
以下の
script2.sh
を見て下さい:for filename in $1/*.txt do cp $filename $filename.bak done
このスクリプトの目的が何かを記述して下さい。
最初の引数で指定したサブディレクトリから、
.txt
で終わるすべてのファイルのバックアップコピーを作成します。
発展演習の解答
-
任意個の引数を受け取り、10より大きい数値の引数のみを表示するスクリプトを作成してください。
#!/bin/bash for i in $@ do echo $i | grep "^[0-9]*$" > /dev/null if [ $? -eq 0 ] then if [ $i -gt 10 ] then echo -n "$i " fi fi done echo ""