Linux標準教科書(Ver.3.0.0)のまとめ ~第9章~

# 第９章 シェルスクリプト

## はじめに

この章では、シェルスクリプトを使用して、コマンド入力による作業を自動化する方法が説明されています。

例えば、 /etc ディレクトリと /home ディレクトリを圧縮して外部サーバーにコピーする作業について考えてみましょう。実行するコマンドはたとえば以下のようになります。

```console:例
# tar cvzf 120626-etc.tar.gz /etc
# tar cvzf 120626-home.tar.gz /home
# scp 120626-etc.tar.gz root@backup.local.example.com:~/backup
# scp 120626-home.tar.gz root@backup.local.example.com:~/backup
```

これをいちいち手打ちで実行するのは非効率です。そこで、先ほどの一連の流れを以下のようなファイルを作成して、実行することで、1コマンドで全てを実行することができます。

```shell:system-backup.sh
#!/bin/bash

tar cvzf 120626-etc.tar.gz /etc
tar cvzf 120626-home.tar.gz /home
scp 120626-etc.tar.gz root@backup.local.example.com:~/backup
scp 120626-home.tar.gz root@backup.local.example.com:~/backup
```

このように人が行う作業を減らすことができますし、実行ファイルを正しく書ければ、ヒューマンエラーをなくすことができます。作業を効率化するため、この章でシェルスクリプトを学びます。


## シェルスクリプト

ここからは、実際にシェルスクリプトを作成して、実行してみます。

### シェルスクリプトの作成

まず vi を使って `lsdate.sh` ファイルを作りましょう。

```console:実行コマンド
$ vi lsdate.sh
(lsdate.sh というファイルを作成)
```

上を実行すると vi が起動するので、 vi 上で以下のように記述します。

```shell:lsdate.sh
#!/bin/bash
ls
date
```

1行目に `#!/bin/bash` と記述しましたが、これは **使用するシェルの種類** を指定しています。今回は `bash` というシェルを使用することがわかります。
2行目以降に **実行したいコマンド** を1行ずつ入力します。

### シェルスクリプトの実行

シェルスクリプトを実行するには、ファイルに実行権限を付与する必要があります。 `chmod` コマンドを使用して、実行権限を付与しましょう。

```console:実行コマンド
$ chmod u+x lsdate.sh
(所有者に実行権限を付与)

$ ls -l lsdate.sh
-rwxrw-r-- 1 test test 20 Apr 11 09:52 lsdate.sh
(所有者に実行権限が付与されたことを確認)
```

これでシェルスクリプトを実行できる準備ができました。では実際に実行してみましょう。

```console:実行コマンド
$ ./lsdate.sh
lsdate.sh
Wed Apr 11 11:09:47 UTC 2018
(ls, date の順にコマンドが実行されることを確認)
```

### コメント

コメントとは、プログラム中に書く注釈のことです。シェルの場合は、 `#` で始まる行がコメントと認識され、プログラム実行時には無視されます。

先ほど作った `lsdate.sh` の `date` コマンドをコメントアウトしてみましょう。

```shell:lsdate.sh
#!/bin/bash
ls
#date
```

それでは実行してみましょう。

```console:実行コマンド
$ ./lsdate.sh
lsdate.sh
(date コマンドが実行されないことを確認)
```

### echoコマンド

echo コマンドは引数で与えた文字列を出力するコマンドです。

```:書式
echo [オプション] 文字列
```

実際に、echoコマンドを実行して、文字列が出力されることを確認しましょう。

```console:実行コマンド
$ echo Message test
Message test
(文字列が出力されることを確認)
```

### 変数

シェルスクリプトプログラミングでは、変数への代入を `=` 、参照を `$` を使って行います。
練習として、 `echo` コマンドを併用して、変数に代入した値を出力してみましょう。

```console:実行コマンド
$ abc=123
$ echo $abc
123
(変数 abc に代入した 123 が出力されるか確認)
```

また `bash` では、一次元の配列変数を使用できます。要素は `[]` で囲み、配列変数の内容を表示する場合は　 `$` の後ろに `{}` で配列変数を囲みます。

```console:実行コマンド
$ array[0]=123
$ array[1]=456
$ echo ${array[0]}
123
(配列の要素が出力されることを確認)
```

変数には種類があります。上記で使用していた変数は **シェル変数** といいます。これは実行しているシェルの内部でのみ有効です。それに対し、 **環境変数** というのは、そこから実行されたコマンド内でも有効になります。

実際に2つのスクリプト `BBB.sh` と `CCC.sh` を使って、シェル変数と環境変数の動作の違いについて確認してみましょう。

```shell:BBB.sh
#!/bin/bash
xxx=123                 # シェル変数xxxに123を代入する
export yyy=234          # 環境変数yyyに234を代入する
echo xxx=$xxx in BBB.sh # 変数xxxの値を表示する
echo yyy=$yyy in BBB.sh # 変数yyyの値を表示する
./CCC.sh                # CCC.shを実行する
```

```shell:CCC.sh
#!/bin/bash
echo xxx=$xxx in CCC.sh # 変数xxxの値を表示する
echo yyy=$yyy in CCC.sh # 変数yyyの値を表示する
```

```console:実行コマンド
$ ./BBB.sh
xxx=123 in BBB.sh
yyy=234 in BBB.sh
xxx= in CCC.sh
yyy=234 in CCC.sh
(シェル変数 xxx が表示されないことを確認)
(環境変数 yyy が表示されることを確認)
```

以上の結果から、シェル変数は実行されたシェルスクリプト内でしか使用できず、環境変数は実行されたシェルスクリプト内で実行されるコマンド内でも使用できることがわかります。

### readコマンド

readコマンドは、標準入力からデータを読み込み、変数に代入します。すでに変数にデータが入っていた場合、新しいデータに上書きされます。

```:書式
read 変数名
```

実際に `read` コマンドを使用して、変数に値を代入してみましょう。

```console:実行コマンド
$ echo $abc
123
(シェル変数 abc の値を確認)

$ read abc
aaabbbccc
(標準入力を行う)

$ echo $abc
aaabbbccc
(標準入力した値が出力されることを確認)
```

### シェル変数操作コマンド

シェル変数の一覧を表示する場合は、 `set` コマンドを使用します。また削除する場合には、 `unset` コマンドを使用します。

```console:実行コマンド
$ set
BASH=/bin/bash
( --- 略 ---- )
(シェル変数の一覧を確認)

$ set | grep ^abc
abc=aaabbbccc
(abc で始まるシェル変数のみを確認)

$ unset abc
(シェル変数 abc を削除する)

$ set | grep ^abc
(シェル変数 abc が表示されないことを確認)
```

### 環境変数操作コマンド

現在の環境変数の一覧を表示する場合は、 `env` コマンドを使用します。また削除する場合には、 `unset` コマンドを使用します。

```console:実行コマンド
$ export ABC=999999
(環境変数 ABC を設定する)

$ env
ABC=999999
( --- 略 --- )
(環境変数の一覧を確認)

$ env | grep ^ABC
ABC=999999
(ABC で始まる環境変数のみを確認)

$ unset ABC
(環境変数 ABC を削除する)

$ env | grep ^ABC
(シェル変数 abc が表示されないことを確認)
```

### 引用符

シェルスクリプトでは、文字列を引用符で囲むことができます。利用できる引用符には `シングルクォート` 、`ダブルクォート` 、 `バッククォート` があり、使用される引用符により囲まれた文字列の処理が異なります。

- **シングルクォート**: 文字列中の `$` も文字列として認識されるため、変数は展開されません。
- **ダブルクォート**: 引用符内の `$` 付き変数は展開された文字列になります。
- **バッククォート**: コマンドとして解釈され、変数を展開した上でコマンドが実行されます。

※ 引用符は入れ子が可能です。

```console:実行コマンド
$ ABC=123
(シェル変数 ABC に 123 を代入する)

$ echo 'Value of ABC is $ABC.'
Value of ABC is $ABC.
(シェル変数 ABC が展開されずに表示されることを確認)

$ echo "Value of ABC is $ABC."
Value of ABC is 123.
(シェル変数 ABC が展開されて表示されることを確認)

$ XYZ=`date`;
(シェル変数 XYZ に date コマンドを代入する)

$ echo "It is $XYZ now."
It is Thu Apr 12 00:16:41 UTC 2018 now.
(date コマンドの実行結果が展開されて表示されることを確認)

$ echo "It is `date` now."
It is Thu Apr 12 00:16:41 UTC 2018 now.
(date コマンドの実行結果が展開されて表示されることを確認)
```

### 引数

シェルスクリプトは、実行時にオプションを引数として参照することができます。引数は `$1` ,` $2` …など `$` の後に引数の番号を指定することで参照できます。

```shell:args.sh
#!/bin/bash

echo '$1:' $1;
echo '$2:' $2;
echo '$3:' $3;
echo '$0:' $0;
echo '$#:' $#;
```

```console:実行コマンド
$ ./args.sh aaa bbb ccc
$1: aaa
$2: bbb
$3: ccc
$0: ./args.sh
$#: 3
($1~$3 は指定した引数が表示されることを確認)
($0 は実行コマンド名が表示されることを確認)
($# は引数の数が表示されることを確認)
```

### shiftコマンド

`shift` コマンドは、引数の順序をずらします。 実行すると、 `$2`が `$1` に、 `$3` が `$2` になります。

```shell:argsshift.sh
#!/bin/bash

echo '$1:' $1;
echo '$2:' $2;
echo '$3:' $3;
shift
echo '$1:' $1;
echo '$2:' $2;
```

```console:実行コマンド
$ ./argsshift.sh aaa bbb ccc
$1: aaa
$2: bbb
$3: ccc
$1: bbb
$2: ccc
($1 に 第2引数が、$2 に第３引数が表示されることを確認)
```

### エスケープシーケンス

プログラミング言語には、特別な扱いを受ける文字があります。例えば、echoコマンドで、　`Value of ABC is "123".` のようにダブルクォートを出力する方法を考えてみましょう。

```console:実行コマンド
$ ABC=123
(シェル変数 ABC に 123 を代入する)

$ echo "Value of ABC is "$ABC"."
Value of ABC is 123.
(""が表示されないことを確認)

$ echo "Value of ABC is \"$ABC\"."
Value of ABC is "123".
(""も表示されることを確認)
```

シェルスクリプトでは `\` をエスケープ文字と呼び、 直後の1文字の扱いを変更します。また、 `\` を行末に入力することで改行コードとしても使用することができます。(シェルスクリプト内でも同様です。)

```console:実行コマンド
$ echo "I am a cat. As yet I have no name."
I am a cat. As yet I have no name.

$ echo "I am a cat. \
> As yet I have no name."
I am a cat. As yet I have no name.
(途中でバックスラッシュを入力しても改行は無視され同じ結果になることを確認)
```

また、 `\` の次に続く1文字をセットすることで、特別な意味を持つ文字列にすることができます。
例としては、 \t(タブ)、 \n(改行) などがあります。

```console:実行コマンド
$ echo -e "I am a cat. \nAs yet I have no name\041"
I am a cat.
As yet I have no name!
(\n は改行、 \041は ! として表示されることを確認)
```

### sourceコマンド

bash などのシェルの内部コマンドで指定されたファイルを読み込んでシェル環境を設定します。ファイル内容はシェルコマンドと解釈し実行します。

一般的な用途としては、シェルの環境設定ファイルである `.bashrc` や `.bash_profile` などを修正した後に、ログインしなおさずに設定を有効にする場合に使用されます。

① シェルスクリプト `set.sh` を用意

```shell:set.sh
#!/bin/bash
abc=xyz
echo $abc
```

② $abc を echo コマンドで出力

```console:実行コマンド
$ echo $abc
($abc には何も格納されていないため、何も表示されない)
```

③ `set.sh` スクリプトを実行

```console:実行コマンド
$ ./set.sh
xyz
(スクリプトの内で設定された変数 abc の値が echo で出力された)
```

④ `$abc` を echo コマンドで出力

```console:実行コマンド
$ echo $abc
(変数 abc への値の設定はスクリプト内でしか有効でないので、何も表示されない)
```

⑤ source コマンドで `set.sh` を読み込む


```console:実行コマンド
$ source set.sh
xyz
(スクリプト内で設定された変数 abc の値が　echo で出力された)
```

⑥ `$abc` を echo コマンドで出力

```console:実行コマンド
$ echo $abc
xyz
(source コマンドで読み込んだことにより、 set.sh の終了後も変数 abc に値が格納されたまま)
```

### 条件分岐

シェルスクリプトも他のプログラミング言語同様、 **条件分岐** を使用できます。

#### if 文

文法は以下の通りです。

```:書式
if 条件式1 then ... elif 条件式2 ... else ... fi
```

if 文は fi で終了します。if 文で利用される条件式には、次のようなものがあります。

- 文字列比較を行う演算子

|演算子|比較内容|
|:--|:--|
|a == b|a と b が等しければ真|
|a != b|a と b が等しくなければ真|

- 数値比較を行う演算子

|演算子|比較内容|
|:--|:--|
|a -eq b|a と b が等しければ真|
|a -ne b|a と b が等しくなければ真|
|a -ge b|a が b 以上であれば真|
|a -le b|a が b 以下であれば真|
|a -gt b|a が b より大きい値であれば真|
|a -lt b|a が b 未満であれば真|

#### ファイル属性の確認

シェルスクリプトではファイル属性の確認もできます。

```:書式
if test -d パス; then ...
```

`-d` の部分がファイル属性確認の演算子となります。上記の if 文は、 「 パスがディレクトリであれば真の値を返す」 というものです。ファイルの属性確認演算子は以下のようなものが利用できます。

|演算子|内容|
|:--|:--|
|-f ファイル名|通常ファイルなら真|
|-d ファイル名|ディレクトリファイルなら真|
|-e ファイル名|ファイルが存在すれば真|
|-L ファイル名|シンボリックなら真|
|-r ファイル名|読み取り可能ファイルなら真|
|-w ファイル名|書き込み可能ファイルなら真|
|-x ファイル名|ファイルが存在して、実行権限があれば真|
|-s ファイル名|サイズが0よりも大きければ真|

また test コマンドは `[]` を使って記述することもできます。

```:書式
if [ 条件節 ]; then ...
if test 条件節; then ...
```

#### 複数の条件を重ねる

シェルスクリプトにおける、論理積や論理和の書き方は2通り存在します。

- 論理積

```:書式
[ 条件A -a 条件B -a 条件C ] ...
[ 条件A ] && [ 条件B ] && [ 条件C ] ...
```

- 論理和

```:書式
[ 条件A -o 条件B -o 条件C ] ...
[ 条件A ] || [ 条件B ] || [ 条件C ] ...
```

#### case 文

一対多の分岐を行うとき、 case 文を使用することができます。

```:書式
case 変数 in
  値A)
    処理1;;
  値B)
    処理2;;
esac
```

練習として、実際に case 文を使用したシェルスクリプトを書いて、実行してみましょう。

```shell:case.sh
#!/bin/bash

case $1 in
  a|A)
    echo "引数にaまたはAが入力されました";;
  b|B)
    echo "引数にbまたはBが入力されました";;
  *)
    echo "引数に予測していない文字が入力されました";;
esac
```

```console:実行コマンド
$ ./case.sh a
引数にaまたはAが入力されました

$ ./case.sh b
引数にbまたはBが入力されました

$ ./case.sh c
引数に予測していない文字が入力されました
```

### 繰り返し

シェルスクリプトでも繰り返し処理を行えます。シェルスクリプトで用いられている繰り返しは、以下の3通りです。

#### for 文

for 文は値を列挙し、それを対象に処理を繰り返します。

```:書式
for 値 in 値のリスト
do
  処理
done
```

練習として、for 文を使ってコマンドを実行してみましょう。

```console:実行コマンド
$ for i in a b c d
> do
>   echo $i
> done
a
b
c
d
```

#### while/until 文

while 文は条件が成立している間ループを繰り返し、条件が成立しなくなったら終了という処理で利用できます。 until 文がその反対です。

```:書式
while 条件式
do
  処理
done

until 条件式
do
  処理
done
```

expr コマンドを用いてループカウンタ用の変数をインクリメントしながら処理を行うこともできます。
実際に while 文を使ったシェルスクリプトを作成し、実行してみましょう。

```shell:loop.sh
#!/bin/bash

count=1
while [ $count -le 10 ]
do
  echo "この処理は$count回実行されました"
  count=`expr $count + 1`
done
```

#### select 文

ユーザに対し数値による入力を促します。

```:書式
select 変数 in リスト
do
  処理
done
```

実際に以下のコマンドを実行してみましょう。

```console:実行コマンド
$ select name in "apple" "banana" "orange"
> do
>   echo "You selected $name";
> done
1) apple
2) banana
3) orange
$? 1
You selected apple
( Ctrl + C で中止)
```

#### 繰り返しの制御

break や continue を用いることで、繰り返しを制御することができます。

- break: 繰り返しを終了
- continue: 繰り返しの先頭に戻る

実際に break と continue を使用したシェルスクリプトを作成し、実行してみましょう。

```shell:sample.sh
#!/bin/bash

while true
do
  echo "Continue? (y/n)"
  read input
  case $input in
    n) break
      ;;
    y) continue
      ;;
    *) echo "Please input y or n."
      ;;
  esac
done
```

```console:実行コマンド
$ ./sample.sh
Continue? (y/n)
y
(y を入力)

Continue? (y/n)
(繰り返しの先頭に戻った)

a
(y, n以外を入力)

Please input y or n.
Continue? (y/n)
n
(nを入力)

$
(繰り返しが終了した)
```

### サブルーチン

一連の処理をまとめて再利用できるようにしたものを、サブルーチンと言います。言語によって呼び方が違いますが、シェルスクリプトでは関数と呼ばれています。

関数は、引数とよばれるデータを与え、処理をして結果を返すという機能のあつまりです。書き方は以下の2通りあります。

```:書式1
function 関数名
{
  処理
}
```

```:書式2
関数名()
{
  処理
}
```

またシェルスクリプトの関数で、結果を返すときは return 文を実行します。

```:書式
return 変数名
```

### デバッグ

作成したプログラムが思ったように動作しない場合、どこに問題があるのか調べる必要がありますが、そのときに使用するコマンドが `sh` コマンドです。

`sh` コマンド自体はシェルを起動するコマンドですが、 `-x` オプションをつけて引数にシェルスクリプトを指定することで、コマンドや変数の中身を表示しながらスクリプトを実行することができます。

```console:実行コマンド
$ sh -x ./sample.sh
+ true
+ echo 'Continue? (y/n)'
Continue? (y/n)
+ read input
y
(yを入力)

+ case $input in
+ continue
+ true
+ echo 'Continue? (y/n)'
Continue? (y/n)
+ read input
n
(nを入力)

+ case $input in
+ break
(処理が完了)
```