シェルスクリプトの書き方

最新版は以下に記載しました。
[bashスクリプトの書き方](https://hana-shin.hatenablog.com/entry/2022/02/11/231225)
[bash変数展開の使い方](https://hana-shin.hatenablog.com/entry/2023/03/15/174546)
[bashスクリプトのデバッグ方法](https://hana-shin.hatenablog.com/entry/2023/04/15/110122)
[bashdbの使い方](https://hana-shin.hatenablog.com/entry/2023/04/09/215204)
[ShellCheckコマンドの使い方](https://hana-shin.hatenablog.com/entry/2022/02/24/221414)



#1 はじめに
bsahのサンプルスクリプトです。
自分が仕事で使いそうなものをまとめました。

#2 環境
VMware Workstation 15 Player上の仮想マシンを使いました。
仮想マシンの版数は以下のとりです。

```console:CentOSの版数
[root@server ~]# cat /etc/redhat-release
CentOS Linux release 7.6.1810 (Core)
```

```console:カーネル版数
[root@server ~]# uname -r
3.10.0-957.el7.x86_64
```

```console:bash版数
[root@server ~]# bash --version
GNU bash, バージョン 4.2.46(2)-release (x86_64-redhat-linux-gnu)
-snip-
```

#3 Hello World

```console:スクリプト
[root@server ~]# cat test.sh
#!/usr/bin/bash
echo "Hello World"
```

```console:実行権付与
[root@server ~]# chmod 744 test.sh
```

```console:スクリプト実行
[root@server ~]# ./test.sh
Hello World
```


#4 引数の使い方
引数の意味は、次のとおり。
`$0`:スクリプトの名前そのものを表す。
`$1,$2,$3...`:引数を表す。

```console:サンプルスクリプト
[root@server ~]# cat test.sh
#!/usr/bin/bash
echo "$0" "$1" "$2"
```

スクリプトに`aa`を引数として与えてみます。

```console:実行結果
[root@server ~]# ./test.sh aa
./test.sh aa
```

スクリプトに`aa`と`bb`を引数として与えてみます。

```console:実行結果
[root@server ~]# ./test.sh aa bb
./test.sh aa bb
```

#5 if文の使い方

```console:サンプルスクリプト
[root@server ~]# cat test.sh
#!/usr/bin/bash

if [ "$1" -eq 1 ]; then
  echo 'one'
elif [ "$1" -eq 2 ]; then
  echo 'two'
else
  echo 'other'
fi
```

```console:実行結果
[root@server ~]# ./test.sh 1
one
[root@server ~]# ./test.sh 2
two
[root@server ~]# ./test.sh 3
other
```


#6 for文の使い方

##6.1 リストを使った繰り返し

```console:構文
for 変数 in リスト
do
  処理
done
```

```console:サンプルスクリプト
[root@server ~]# cat test.sh
#!/usr/bin/bash
for i in 1 2 3
do
   echo $i
done
```

```console:実行結果
[root@server ~]# ./test.sh
1
2
3
```

##6.2 スクリプトに渡される引数を全て使う方法
シェルの特殊変数`$@`はスクリプトに渡される引数すべてを意味します。

```console:サンプルスクリプト
[root@server ~]# cat test.sh
#!/usr/bin/bash
for x in "$@"
do
  echo "$x"
done
```

```console:実行結果
[root@server ~]# ./test.sh 11
11
[root@server ~]# ./test.sh 11 22
11
22
```

#7 ファイル名の扱い方
##7.1 スクリプトの名前を表示する方法
`$0`は、スクリプ自身の名前を表します。

```console:サンプルスクリプト
[root@server ~]# cat test.sh
#!/usr/bin/bash
echo "$0"
```

```console:実行結果
[root@server ~]# ./test.sh
./test.sh
```

##7.2 パス名からファイル名を取り出す方法
ここでは、パス名からファイル名を取り出したあと
basenameコマンドを使って、ファイルの拡張子を削除してみます。

```console:サンプルスクリプト
[root@server ~]# cat test.sh
#!/usr/bin/bash
echo "$0"
tmp=$(basename "$0")
echo "$tmp"

tmp=$(basename "$0" .sh)
echo "$tmp"
```

```console:実行結果
[root@server ~]# ./test.sh
./test.sh
test.sh
test
```


##7.3 スクリプトの拡張子を変更する方法
スクリプトの拡張子を変更してみます。
ここでは、`test.sh`を`test.log`に変更してみます。

```console:サンプルスクリプト
[root@server ~]# vi test.sh
[root@server ~]# cat test.sh
#!/usr/bin/bash

echo "$0"
TMP=$(basename "$0" .sh)
echo "$TMP"

LOG_FILE=$TMP.log
echo "$LOG_FILE"
```

```console:実行結果（その1）
[root@server ~]# ./test.sh
./test.sh
test
test.log
```


```console:実行結果（その2）
[root@server ~]# cp test.sh aa.sh
[root@server ~]# ./aa.sh
./aa.sh
aa
aa.log
```



#8 ファイルのインクルード方法(source)
sourceコマンドを使って、`functions`ファイルを読み込んでみます。

```console:サンプルスクリプト（本体）
[root@server ~]# cat test.sh
#!/usr/bin/bash
source ./functions

add 1 2
echo "Ans:"$?
```

```console:サンプルスクリプト（共通定義）
[root@server ~]# cat functions
#!/usr/bin/bash
function add() {
 a=`expr "$1" + "$2"`
 return "$a"
}
```

```console:実行結果
[root@server ~]# ./test.sh
Ans:3
```

#9 ファイル、ディレクトリの存在有無を調べる方法
##9.1 ファイルの存在有無を調べる方法

```console:サンプルスクリプト
[root@server ~]# cat test.sh
#!/usr/bin/bash
echo "$1"

if [ -f "$1" ]; then
  echo 'exist'
else
  echo 'not exist'
fi
```

```console:実行結果（ファイルが存在する場合）
[root@server ~]# touch /tmp/hoge
[root@server ~]# ./test.sh /tmp/hoge
/tmp/hoge
exist
```

```console:実行結果（ファイルが存在しない場合）
[root@server ~]# rm -f /tmp/hoge
[root@server ~]# ./test.sh /tmp/hoge
/tmp/hoge
not exist
```

##9.2 ディレクトリの存在有無を調べる方法

```console:サンプルスクリプト
[root@server ~]# cat test.sh
#!/usr/bin/bash
echo "$1"

if [ -d "$1" ]; then
  echo 'exist'
else
  echo 'not exist'
fi
```

```console:実行結果（ディレクトリが存在する場合）
[root@server ~]# mkdir /tmp/hoge
[root@server ~]# ./test.sh /tmp/hoge
/tmp/hoge
exist
```

```console:実行結果（ディレクトリが存在しない場合）
[root@server ~]# rm -fr /tmp/hoge
[root@server ~]# ./test.sh /tmp/hoge
/tmp/hoge
not exist
```

##9.3 AND条件を複数指定する方法(-a)
事前準備として、テスト用のファイルを作成します。

```console:テスト用ファイル作成
[root@server ~]# touch /tmp/test1.txt
[root@server ~]# touch /tmp/test2.txt
[root@server ~]# touch /tmp/test3.txt
```

作成した3つのファイル全てが存在する場合は`true`、
1つでも存在しないと`false`を表示するスクリプトを作成します。

```console:サンプルスクリプト
[root@server ~]# cat tp.sh
#!/usr/bin/bash

if [ -f /tmp/test1.txt -a \
     -f /tmp/test2.txt -a \
     -f /tmp/test3.txt ]; then
  echo "true"
else
  echo "false"
fi
```

```console:実行結果
[root@server ~]# ./tp.sh
true
```

次に、テスト用に作成したファイルを1つ削除してみます。

```console:ファイルの削除
[root@server ~]# rm /tmp/test3.txt
rm: 通常の空ファイル `/tmp/test3.txt' を削除しますか? y
```

`test1.txt`,`test2.txt`,`test3.txt`の3つが存在しないので、
実行結果が、`false`になったことがわかります。

```console:実行結果
[root@server ~]# ./tp.sh
false
```

##9.4 OR条件を複数指定する方法(-o)
事前準備として、テスト用のファイルを作成します。

```console:テスト用ファイル作成
[root@server ~]# touch /tmp/test1.txt
[root@server ~]# touch /tmp/test2.txt
[root@server ~]# touch /tmp/test3.txt
```

作成した3つのファイルのいずれかが存在する場合は`true`、
全て存在しないと`false`を表示するスクリプトを作成します。

```console:サンプルスクリプト
[root@server ~]# cat tp.sh
#!/usr/bin/bash

if [ -f /tmp/test1.txt -o \
     -f /tmp/test2.txt -o \
     -f /tmp/test3.txt ]; then
  echo "true"
else
  echo "false"
fi
```

テスト用に作成したファイルを1つ削除してみます。

```console:ファイルの削除
[root@server ~]# rm /tmp/test3.txt
rm: 通常の空ファイル `/tmp/test3.txt' を削除しますか? y
```

`test1.txt`,`test2.txt`が存在するので、実行結果が`true`であることがわかります。

```console:実行結果
[root@server ~]# ./tp.sh
true
```

残りのファイルも全て削除してみます。

```console:ファイルの削除
[root@server ~]# rm /tmp/test1.txt
rm: 通常の空ファイル `/tmp/test1.txt' を削除しますか? y
[root@server ~]# rm /tmp/test2.txt
rm: 通常の空ファイル `/tmp/test2.txt' を削除しますか? y```
```

作成した3つのファイル全てが存在しないので、スクリプトの実行結果は`false`になります。

```console:実行結果
[root@server ~]# ./tp.sh
false
```




#10 関数の使い方
##10.1 関数の呼び出し方法
`source`コマンドは、ファイル(functions)をインクルードするコマンドです。
メイン関数から、関数sub1を呼び出してみます。

```console:サンプルスクリプト（関数の呼び出し側）
[root@server ~]# cat test.sh
#!/usr/bin/bash

source ./functions
sub1
```

```console:サンプルスクリプト（関数本体）
[root@server ~]# cat functions
#!/usr/bin/bash

function sub1() {
  echo "sub1"
}
```

```console:実行結果
[root@server ~]# ./test.sh
sub1
```

##10.2 引数の使い方
メイン関数から関数sub1に`11`を渡します。
そして、関数sub1から関数sbu2に`aa`と`bb`を渡してみます。

```console:サンプルスクリプト（関数の呼び出し側）
[root@server ~]# cat test.sh
#!/usr/bin/bash

source ./functions
sub1 11
```

```console:サンプルスクリプト（関数本体）
[root@server ~]# cat functions
#!/usr/bin/bash

function sub1() {
  echo "$1"
  sub2 "aa" "bb"
}

function sub2() {
  echo "$1" "$2"
}
```

```console:実行結果
[root@server ~]# ./test.sh
11
aa bb
```

##10.3 戻り値の使い方（数値編）

```console:サンプルスクリプト（関数の呼び出し側）
[root@server ~]# cat test.sh
#!/usr/bin/bash

source ./functions
sub1 11
echo $?
```

関数への引数から`1`を引いて、その結果を戻り値として返します。

```console:サンプルスクリプト（関数本体）
[root@server ~]# cat functions
#!/usr/bin/bash

function sub1() {
  tmp=$(expr "$1" - 10)
  return "$tmp"
}
```

```console:実行結果
[root@server ~]# ./test.sh
1
```


##10.4 戻り値の使い方（文字列編）


```console:サンプルスクリプト（関数の呼び出し側）
[root@server ~]# cat test.sh
#!/usr/bin/bash

source ./functions
sub1 aa
```

```console:サンプルスクリプト（関数本体）
[root@server ~]# cat functions
#!/usr/bin/bash

function sub1() {
  tmp=Sample-$1
  echo "$tmp"
}
```

```console:実行結果
[root@server ~]# ./test.sh
Sample-aa
```


#11 標準出力、標準エラー出力をファイルにリダイレクトする方法
`aa`というファイルを作成します。`bb`は作成しません。

```console:ファイルの確認
[root@server ~]# touch aa
[root@server ~]# ls aa bb
ls: bb にアクセスできません: そのようなファイルやディレクトリはありません
aa
```


lsコマンドを実行して、標準出力と標準エラー出力をlogファイルに保存します。

```console:
[root@server ~]# ls aa bb > log 2>&1
[root@server ~]# cat log
ls: bb にアクセスできません: そのようなファイルやディレクトリはありません
aa
```


#12 配列の使い方

```console:サンプルスクリプト
[root@server ~]# cat tp.sh
#!/usr/bin/bash

package_list1=("glibc" "iproute")
package_list2=("httpd")

function searche_package() {
    name=$1[@]
    a=("${!name}")
    for package_name in "${a[@]}" ; do
        rpm -qa | grep ^"${package_name}"
    done
}

if [ "$1" -eq 1 ]; then
  searche_package package_list1
elif [ "$1" -eq 2 ]; then
  searche_package package_list2
fi
```

```console:実行結果(その1)
[root@server ~]# ./tp.sh 1
glibc-headers-2.17-260.el7.x86_64
glibc-debuginfo-common-2.17-260.el7.x86_64
glibc-devel-2.17-260.el7.x86_64
glibc-debuginfo-2.17-260.el7.x86_64
glibc-common-2.17-260.el7.x86_64
glibc-2.17-260.el7.x86_64
iproute-4.11.0-14.el7.x86_64
```

```console:実行結果(その2)
[root@server ~]# ./tp.sh 2
httpd-tools-2.4.6-93.el7.centos.x86_64
httpd-2.4.6-93.el7.centos.x86_64
```

#13 デバッグ方法

##13.1 トレースオプションを指定する方法(-x)
1行目に`-x`オプションを指定して、スクリプトを実行しみます。

```console:サンプルスクリプト
[root@server ~]# cat test.sh
#!/usr/bin/bash -x
echo "$1"

if [ -d "$1" ]; then
  echo 'exist'
else
  echo 'not exist'
fi
```

実行したコマンドと実行結果が表示されていることがわかります。

```console:実行結果
[root@server ~]# ./test.sh /tmp
+ echo /tmp
/tmp
+ '[' -d /tmp ']'
+ echo exist
exist
```

##13.2 ソースコードも一緒に出力する方法(-v)
1行目に`-v`オプションも指定して、スクリプトを実行しみます。

```console:サンプルスクリプト
[root@server ~]# cat test.sh
#!/usr/bin/bash -vx
echo "$1"

if [ -d "$1" ]; then
  echo 'exist'
else
  echo 'not exist'
fi
```

```console:実行結果
[root@server ~]# ./test.sh /tmp
#!/usr/bin/bash -vx
echo "$1"
+ echo /tmp
/tmp

if [ -d "$1" ]; then
  echo 'exist'
else
  echo 'not exist'
fi
+ '[' -d /tmp ']'
+ echo exist
exist
```


##13.3 スクリプトを変更せずにデバッグする方法(-x)
`13.1`の方法ではデバッグ対象のスクリプトを修正する必要があります。
ここでは、スクリプトを修正せずにデバッグしてみます。

```console:実行結果
[root@server ~]# bash -x test.sh /tmp
+ echo /tmp
/tmp
+ '[' -d /tmp ']'
+ echo exist
exist
```


#Z 参考情報
[逆引きシェルスクリプト](https://linux.just4fun.biz/?%E9%80%86%E5%BC%95%E3%81%8D%E3%82%B7%E3%82%A7%E3%83%AB%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88/%E3%82%B7%E3%82%A7%E3%83%AB%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%AF%E3%83%AB%E3%83%BC%E3%83%89%E6%96%B9%E6%B3%95)

[shellcheckコマンドの使い方](https://qiita.com/hana_shin/items/ecd675ea0550c4a44d59)
[シェルのif文でAND条件を複数指定したい](https://open-groove.net/shell/if-and-or/)
[bashで条件を複数ORで連結する](https://hacknote.jp/archives/11619/)
[シェルスクリプトのデバッグ](https://shellscript.sunone.me/debug.html)