シェルスクリプトTIPS

###シェルスクリプトのTIPS

最近はPyrhonやRubyなど他のスクリプト言語も流行っていますが、Unixエンジニアと言えばシェルスクリプト。この記事は基本的なシェルスクリプトがかける人向けのTIPSです。他人が書いたシェルスクリプト読んでいて、「なんだこれ？」と思った時の参考にして下さい。


##### <font color="LimeGreen">◆安全なSHEBANGについて(/bin/shの実体)</font>

昨今、多くのUnixOSの/bin/shはBashとなっているが、実はすべてのUnix/Linuxがそうなっているとは限らない。AshだったりZshだったりDashだったりの可能性もある。なので、BASH固有の機能などを利用しているシェルスクリプトの場合、一行目に書くシェバンは、シェルを明示しておく。

```
#!/bin/bash
```

もしくはすべてのUnixシェルで実行できるよう古典的で安全な(PORTABLEな)シェルスクリプトを記述しておくと良い。ただし、実行速度が低下したり、配列が使えない、EXPRコマンドを用いたインクリメントがトリッキーなどの制限がある。


##### <font color="LimeGreen">◆DEBUGモード</font>

どこでエラーが起きているのかわからないが正常終了しない場合、Xオプションを付けてDEBUGモードで実行する。シェルプログラミングの場合、これとPRINTFデバッグで大方は解決する。

```
# /bin/bash -x myscript.sh
```

##### <font color="LimeGreen">◆インクルード</font>

ドットの後にパラメータや自作の関数などを定義したファイルを指定すれば、実行時に読み込まれる。パラメータ専用ファイルなどを作成し、プロジェクトごとに編集すれば直接スクリプトを編集するより安全にカスタマイズ・デプロイが可能となる。

```
#!/bin/bash

. /usr/dev/param
. /user/dev/functions

#(以下省略)
```

##### <font color="LimeGreen">◆ワンライナー1 セミコロンで複数行を1行に収める</font>

```
for name in foo bar foo2
do
  echo ${name}
done
```

セミコロンを用いて一行で書くと次のように表せる

```
for name in foo bar foo2; do echo ${name}; done
```

ワンライナーの良い点として可読性は低下するがコマンドラインで気軽に投入が可能で、高度なコマンドを運用マニュアルに組み込むことができる。

##### <font color="LimeGreen">◆ワンライナー2 異なる記述で同じ処理をさせる</font>

```
if [ -e /var/tmp/test ]
then
   rm /var/tmp/test
fi
```

前のコマンドが正常終了(終了コードはTrue)した場合、&& 以降の処理を実行する。
頻出の記述法なので知っておくと〇

```
[ -e /var/tmp/test ] && rm /var/tmp/file 
```

よく知られた話だが if [  ] then ～ の [ は、実体は /bin/test のエイリアス(別名)である /bin/[ であり、次のように記述することができる

```
/bin/test -e /var/tmp/test && rm /var/tmp/file
```

&&と逆で、直前のコマンドが異常終了(終了コードがTrueでない)した場合、|| 以降の処理を実行する。
[ の後の ! はNOTを意味し、ファイルが存在しない場合を表す。

```
# testファイルが存在しない場合、作成しなさい
if [ ! -e /var/tmp/test ]
then
   touch /var/tmp/test
fi
```

下のように書くことが可能。

```
[ -e /var/tmp/test ] || touch /var/tmp/test
```


##### <font color="LimeGreen">◆&&と||を利用したワンライナーのハマりポイント</font>

よくやる間違いで下記のコードは意味が異なるので注意する。
test_Aが成立している場合、コマンドBを実行し、そうでない場合はコマンドCを実行する

```
If [ test_A ]
then
  command_B
else
  command_C
fi
```

```

[ test_A ] && command_B || command_C
```

上記ではtest_Aが成立している場合、コマンドBを実行する、そうでない場合は、コマンドCを実行する。ところが、test_Aが成立しておりコマンドBの実行した後、コマンドBの実行結果が失敗だった場合はコマンドCが実行されてしまう (ココがハマリポイント)


##### <font color="LimeGreen">◆逆ワンライナー 1行で書ける処理を複数行に分割する</font>

スクリプトの可読性を高める場合に利用される。

```
cat /var/tmp/test.log | grep -v ^# | sed -^#/d | egrep 'foo1|foo4|foo5' | awk '{print $1,$3,$7,$11}' | sort -e | sed -e 's/foo/bar/g' > /var/tmp/tmp.log.txt
```

```
cat /var/tmp/test.log              | \
    grep -v ^#                     | \
    sed -e '^$/d'                  | \
    egrep 'foo1|foo4|foo5'         | \
    awk '{print $1,$3,$7,$11}'     | \ 
    sort -u                        | \
    sed -e 's/foo/bar/g'             \
    > /var/tmp/tmp.log.txt
```

##### <font color="LimeGreen">◆[ と [[ の違い</font>

シェルスクリプトでは正規表現と組み合わせて[[がパターンマッチングで利用されます。
クォートしていない場合、パターンマッチングの結果、fooはfo[ou\](fooもしくはfouのどちらか)に該当するためTrueが返されます。
ただし、[[でもクォートしている場合、文字列解釈の結果は変わらないためFalseが返されます。

```
# 文字列はアンマッチ
(bash)# [ qiita == 'qiit[aiueo]' ]; echo $?
1

# 文字列はマッチ
(bash)# [[ qiita == qiit[aiueo] ]]; echo $?
0

# 文字列はアンマッチ
(bash)# [[ qiita == 'qiit[aiueo]' ]]; echo $?
1
```

その他に大きな違いとしては、文字列のパターンマッチングに利用される他、[[の場合、文字列に数式が書かれている場合は文字列と解釈せず、計算を行ってからテストが実行される。こちらも覚えておくと◎。

```
(bash)# char='10 + 10'
(bash)# [ ${char} -eq 20 ]; echo $?
-bash: [: 10+10 integer expression expected

(bash)# [[ ${char} -eq 20 ]]; echo $?
0
```

##### <font color="LimeGreen">◆インクリメント・デクリメント</font>

外部コマンド expr を用いて計算。exprのプロセスをforkするコストが高いためスクリプトの実行速度はやや遅くなるが、どのUnixシェルでも動作するため安全でポータブル性が高い書き方となる。

```
cnt=`expr ${cnt} + 1`
cnt=`expr ${cnt} - 1`
```

BASHのビルドイン機能。以下の３つの書き方だと実行結果は高速だが、他のシェルで動かない場合もあるため、BASHの脆弱性が見つかった場合などに他のシェルに切り替えて実行するなどの対策は打てなくなるというデメリットがある。

```
((cnt++))
let cnt++
let ++cnt
※デクリメントは--にしてください 
```

##### <font color="LimeGreen">◆ユーザによる入力を受け付ける</font>

ITやCASEを利用してユーザによる値の入力や選択を記述できる。入力文字列を表示させたくない場合はSオプションを利用する。

```
(bash)# read str
aiueo <ENTER>
(bash)# echo ${str}
aiueo
```

##### <font color="LimeGreen">◆分岐先で何もしたくない時はコロン</font>

if elseやcaseにいろいろ分岐を書いていたけれど、いくつかの処理をとりやめたい場合でも、後に再度利用しそうだったり、条件式やパラメータなどが非常に重要で消したくなかったりする場合、その分岐構造そのものは残しておきたかったりする場合があります。そういう場合は処理部をコメントアウトし替わりに ： を入れておきます。

```
if [ -e /usr/dev/functions ] 
then
      . /usr/dev/functions
else
      # (停止したい処理はコメントアウト)
      :
fi
```


##### <font color="LimeGreen">◆yesコマンド</font>

文字列yと終了コード0を超高速で延々と返す謎のコマンドyes。

```
(bash)# yes
y
y
y
y
```

使う場面が思い浮かばないと思いきや、無限ループさせたい場合に使ったりします。

```
#!/bin/bash

. /usr/dev/functions
. /usr/dev/param

while [ yes ]
do
   sleep ${interval}
   chk=$(ps -ef | grep -c ${srvs})
   if [ ${chk} -lt 2 ] 
   then
      # function alert_to
      aleat_to all_engineer@foobar.co.jp
      # function reboot_services
      reboot_services ${srvs}
   fi
done

```

##### <font color="LimeGreen">◆スクリプトの簡単なDaemon化</font>

常駐化させてデーモン化(サービス化)させたい場合なんかにはnohupを使います。以下のスクリプトですと、process_chk.shはログアウト後も常駐プロセスとなり働き続けてくれます。

```
(bash)# nohup /bin/bash process_chk.sh &
```


##### <font color="LimeGreen">◆スクリプトの実行結果を画面でも見たいし、同時にファイルにも保存したい</font>

```
(bash)# ./chk.sh | tee -a /var/tmp/result.log
OK
OK
NG
```

##### <font color="LimeGreen">◆精密な計算を行いたい</font>

exprコマンドは整数しか扱えないため、小数点何桁といった計算を行うにはbcコマンドを利用する。デフォルトでは小数点以下は表示しないので、scaleで桁数を指定する。

```
#!/bin/bash
result=$(echo "scale=1; 101354 * 1.08" | bc)
echo ${result}
```

##### <font color="LimeGreen">◆ランダムな数値を得る</font>

BASHのシェル変数を利用するパターン

```
(bash)# echo ${RANDOM}
8732
```

BASHの機能を利用せず安全でポータブルな方法を検討した結果、時間の文字列でハッシュをとれば良いんじゃないかというアイデアが上手くいった

```
(bash)# date | md5sum | tr -dc "0123456789"
87329348
```

##### <font color="LimeGreen">◆ランダムな文字列を得る</font>

先ほどの手法を再利用して、ランダムな文字列を得る。シンプルな出し方のため、パスワードとして利用するには向かない。パスワードとして利用するにはdateコマンドのオプションを工夫するか、次の方法を利用する。

```
(bash)# date | md5sum | tr -dc "abcdefghijklmnopqrstuvwxyz"
ndrtnrvlkjr
```

##### <font color="LimeGreen">◆暗号強度の高い文字列を得る</font>

先ほどの手法で任意のパスワードなどを生成することができるが、パスワードとして利用するには類推可能であるため、ここで得た文字列にもう少し高度な処理を施しておくと、事前計算による類推や一覧表からの抽出は使えなくなりより強度が増します。具体的にはパスワードソルトを指定しておきます。

```
(bash)# SALT=hogehoge
(bash)# echo "$(date)${SALT}" | md5sum | tr -dc "abcdefghijklmnopqrstuvwxyz"
fdsjlnelr
```





##### <font color="LimeGreen">◆typesetとdeclare</font>

どちらも同じ変数を宣言する命令。typedefはKSHなど他のシェルでも利用が可能で古典的かつポータブル。declaireの方が新しい。

|オプション|内容|
|----|----|
|a|配列を宣言|
|i|数値を宣言|
|r|読み出し専用を宣言|
|p|宣言した変数の値を表示|


他にもあれば適当に更新の予定。