シェルスクリプトの基本的な書式と考え方 

Linuxのシェルスクリプト（bashスクリプト）の書き方とコマンドの特徴をまとめました。

## シェルスクリプトを実行する

Linuxでシェルスクリプトを実行するには以下の3ステップの順番に進めます。
（1と2は逆でも問題ありません。）

1. ファイルを作成し先頭にシェバン（shebang）を定義する
1. ファイルに実行権限を付与する
1. ファイルを実行する

通常、Linuxにはbashがプリインストールされているため上記以外の事前準備は不要です。
ただし、シェルスクリプト内で利用するコマンドは必要に応じて追加でインストールします。

### シェバン（shebang）を定義する
シェルスクリプトの先頭に`#!/bin/bash`と記載することで、このファイルがシェルスクリプトであることを定義できます。

```shell:シェバン（shebang）の定義方法
#!/bin/bash

echo "Hello,World"
```

### シェルスクリプトに実行権限を付与する
`vi`コマンドや`touch`コマンドで作成したファイルは、パーミッションに実行権限がありません。パーミッションを変更するには`chmod`コマンドを利用します。例えば以下のような指定方法があります。
（パーミッション変更の書式は他にも複数あります。）

```shell:chmodで実行権限を追加する基本書式
$ chmod u+x sample01.sh
$ chmod 755 sample01.sh
```

以下のコマンドではユーザー(所有者)に実行権限を付与しています。
ファイル作成時のパーミッションは`644`ですが、上記のコマンドによりパーミッションは`744`または`755`になります。

:::note info
シェルスクリプトの拡張子は[.sh]と付けるのが一般的です。
:::


### シェルスクリプトを実行する

シェルスクリプトを実行するには主に以下の方法があります。

- ファイル名を指定して実行する（シェバンの定義内容をもとにシェルスクリプトを実行）
- `bash`コマンドにより実行する（bashの引数としてシェルスクリプトを実行）
- `sources`コマンドにより実行する（カレントシェルでシェルスクリプトを実行）

ファイル名を指定してシェルスクリプトを実行する場合、絶対パスか相対パスで指定しますが、絶対パスが長い場合は`PATH変数`にシェルスクリプトの保存先を追加することで、シェルスクリプト名（ファイル名）のみで実行可能になります。

:::note warn
sourcesコマンドでシェルスクリプトを実行する場合は、現在のシェル環境（実行されるシェルの種類やエイリアス）などの影響を受けるため注意が必要です。
:::

## Linuxの変数

Linux（シェル）の変数の取り扱いはやや複雑です。内容を整理しました。

- 変数は自由に定義することが可能
- 変数名は[A-Z][a-z][0-9][_]が使用可能、ただし先頭数字はNG
- 変数は`$変数名`で参照可能（例：$var）
- 変数は`${変数名}`で他の文字列と連結可能（例：${var}_file）
- 変数の代入時には`$`は付けない、また、`=`の間にはスペースを含めない
- シェル変数と環境変数の2種類がある

:::note info
変数の参照は視覚的に分かりやすい${変数名}の方が推奨されることが多いです。
:::

### シェル変数と環境変数

Linuxの変数にはシェル変数と環境変数があります。主な違いは以下のとおりです。

- シェル変数はプロセス内でのみ有効（ローカル変数）
- 環境変数は子プロセス内でも有効（グローバル変数）
- シェル変数は`set`コマンドで確認する
- 環境変数は`printenv`コマンドで確認する
- シェル変数を`export`コマンドで定義すると環境変数になる

#### 主なシェル変数

Almalinux8.4でデフォルトで定義されている主なシェル変数および環境変数には以下のようなものがありました。
（ディストリビューションにより異なります。）

|シェル変数|意味|
|---|---|
|BASH|bashのパス|
|IFS|シェルの区切り文字|
|HISTSIZE|保存可能なコマンド履歴数|
|PS1|プロンプト設定|

シェル変数`PS1`にはユーザ名やカレントディレクトリなどが設定されています。追加で現在時刻`¥t`を設定すると、後からログの内容をさかのぼる場合、コマンドの実行時刻が把握できるため便利です。

#### 主な環境変数

主な環境変数は以下のとおりです。環境変数はシェル変数に含められます。

|環境変数|意味|
|---|---|
|HOME|ログインユーザのホームディレクトリ|
|PWD|カレントディレクトリ|
|HOSTNAME|ホストネーム|
|TERM|ターミナル|
|PATH|コマンド検索パス、前から順番にコマンドを探す|
|LANG|ロケール情報（地域、言語）|

PATH環境変数にパスを追加するには、既存のパスを上書きしないように`$PATH`の末尾（または任意の場所）に追加します。

```shell:既存パスの末尾に新しいパスを追加する
$ export PATH="$PATH:~/bin"
```

:::note info
コマンド検索パスから対象のコマンドのパスを検索するにはwhichコマンドを利用します。
:::


### 特殊な変数（位置パラメータ）

主な引数関連のシェル変数（位置パラメータ）には以下のようなものがあります。

|特殊な変数|意味|
|---|---|
|$0|ファイル名|
|$1|一つ目の引数|
|$N|Nつ目の引数|
|$@|すべての引数（引数ごとに一つの文字列）|
|$*|すべての引数（すべての引数で一つの文字列）|
|$#|引数の数|
|$?|直前の実行結果の終了ステータス|

### 変数の属性を変更する

変数の属性を変更するには`declare`コマンドを使います。

|declareコマンドのオプション|変数の属性|
|---|---|
|オプションなし|変数を定義（文字列として定義）|
|-r|読み取り専用（変更不可）|
|-i|整数として定義|
|-a|配列として定義|
|-A|連想配列として定義|

#### 属性の指定によるエラー発生の条件

変数を`declareコマンド（iオプション）`で定義した場合、整数以外の値を代入すると以下のような結果になります。

- 文字を入力した場合、変数には0が代入される
- 少数値を入力した場合、[syntax error: invalid arithmetic operator]が発生する

#### declareコマンドとreadonlyコマンド

`declare -r`コマンド同様、`readonly`コマンドでも読み取り専用の変数を定義できます。

- readonly…グルーバルな定数を定義
- declare -r…ローカルな定数を定義

### 変数を数字として取り扱う

通常、変数は文字として扱われます。変数を数字として取り扱うには以下のような方法があります。

- 算術式展開（二重括弧記号）
- exprコマンド
- letコマンド
- declareコマンド（またはtypesetコマンド）

bash（Bourne Again shell）の前身であるsh（Bourneシェル）では、`letコマンド`や`算術式展開（二重括弧記号）`は使えなかったため、`exprコマンド`が使われていたようです。

```bash:sample_calc.sh（算術演算の基本書式）
#!/bin/bash

# 変数の定義
int_a=10
int_b=20

int_c=$int_a+$int_b
echo 算術式展開を利用せずにそのまま計算した出力結果：${int_c}
unset int_c

int_c=$((int_a+int_b))
echo 算術式展開を利用して計算した出力結果：${int_c}
unset int_c

int_c=$(expr $int_a + $int_b)
echo exprコマンドを利用して計算した出力結果：${int_c}
unset int_c

let int_c=$int_a+$int_b
echo letコマンドを利用して計算した出力結果：${int_c}
unset int_c

declare -i int_c=$int_a+$int_b
echo declareコマンドを利用して計算した出力結果：${int_c}
unset int_c
```

```shell:実行結果
算術式展開を利用せずにそのまま計算した出力結果：10+20
算術式展開を利用して計算した出力結果：30
exprコマンドを利用して計算した出力結果：30
letコマンドを利用して計算した出力結果：30
declareコマンドを利用して計算した出力結果：30
```

### 変数の展開

展開の機能を利用することで、特定の記号（メタ文字）により記載されたコマンドを別の文字列として扱うことができます。本来、複数のコマンドの記載が必要なケースでも展開を利用することで効率的でシンプルな表現が可能です。

主な展開には以下のようなものがあります。

- チルダ展開
- ブルース展開
- パス名展開
- パラメータ展開
- コマンド置換
- プロセス展開

:::note info
上記で解説した算術式展開も展開の一つです。
:::

#### チルダ展開

ユーザのホームディレクトリを指定するにはチルダ展開を利用します。

```shell:チルダ展開の基本書式
$ cd ~
$ pwd
/home/username
```

#### ブルース展開

ブルース展開を利用することで、1コマンドで複数のファイルやディレクトリの作成が可能です。

```shell:ブルース展開の基本書式（パターン１）
$ touch file{A,B,C}.txt
$ ls
fileA.txt  fileB.txt  fileC.txt
```

```shell:ブルース展開の基本書式（パターン2）
$ touch file{1..5}.txt
$ ls
file1.txt  file2.txt  file3.txt  file4.txt  file5.txt
```

#### パス名展開

主なパス名展開は以下のとおりです。

|記号|意味|
|---|---|
|[文字]|カッコ([])内に含まれるいずれかの文字|
|[!文字]または[^文字]|カッコ([])内に含まれないいずれかの文字|

```shell:パス展開の基本書式（パターン1）
$ ls
file1.txt  file2.txt  file3.txt  file4.txt  file5.txt
$ ls file[1-3].txt
file1.txt  file2.txt  file3.txt
```

```shell:パス展開の基本書式（パターン2）
$ ls
file1.txt  file2.txt  file3.txt  file4.txt  file5.txt
$ ls file[!2,4].txt
file1.txt  file3.txt  file5.txt
```

#### パラメータ展開

パラメータ展開を利用する事で以下のようなことができます。

- 変数の定義の有無によって展開する文字列を置き換える
- 変数の一部を加工（削除）する

変数の定義の有無により展開する文字列を置き換えるには以下のような記載方法があります。

|記載方法|意味|変数の上書き|
|---|---|---|
|${var}|通常の変数の展開|-|
|${var:-word}|変数（var）が未定義なら文字列（word）、定義されていれば変数（var）を展開|上書きしない|
|${var:=word}|変数（var）が未定義なら文字列（word）、定義されていれば変数（var）を展開|上書きする|
|${var:+word}|変数（var）が未定義なら空文字、定義されていれば文字列（word）を展開|上書きしない|
|${var:?err_msg}|変数（var）が未定義なら標準エラー出力として文字列（err_msg）を展開|-|

:::note info
変数が空文字列の場合でも未定義として扱います。また、文字列部分は別の変数を置き換えることも可能です。
:::

変数の一部を加工（削除）するには以下のような記載方法があります。

|記載方法|意味|
|---|---|
|${#var}|変数（var）の文字数を展開|
|${var:n}|変数（var）のn+1文字目から末尾までを展開|
|${var: -n}|変数（var）の末尾からn文字目までを展開|
|${var:n:m}|変数（var）のn+1文字目からm文字目までを展開|
|${var#pattern}|変数（var）より文字列（pattern）に一致した部分を先頭から取り除き残りを展開（前方、最短一致）|
|${var##pattern}|変数（var）より文字列（pattern）に一致した部分を先頭から取り除き残りを展開（前方、最長一致）|
|${var%pattern}|変数（var）より文字列（pattern）に一致した部分を末尾から取り除き残りを展開（後方、最短一致）|
|${var%%pattern}|変数（var）より文字列（pattern）に一致した部分を末尾から取り除き残りを展開（後方、最長一致）|


```bash:sample_parameter_deploy.sh（パラメータ展開の基本書式）
#!/bin/bash

# 変数の定義
var=/dir1/dir2/dir3/filename

# 通常の変数の展開
echo "変数の展開（通常の展開）：${var}"

# パラメータ展開を利用した変数の展開：
echo "変数の文字数を展開：${#var}"

echo "先頭から5文字を削除し、6文字目以降を展開する：${var:5}"
echo "後方から8文字目までを展開する：${var: -8}"
echo "先頭から6文字を削除し、7文字目から4文字目までを展開する：${var:6:4}"

echo "前方一致（最短一致）で先頭部分を削除して展開する：${var#*/}"
echo "前方一致（最長一致）で先頭部分を削除して展開する：${var##*/}"
echo "後方一致（最短一致）で後方部分を削除して展開する：${var%/*}"
echo "後方一致（最長一致）で後方部分を削除して展開する：${var%%/*}"
```

```shell:実行結果
変数の展開（通常の展開）：/dir1/dir2/dir3/filename
変数の文字数を展開：24
先頭から5文字を削除し、6文字目以降を展開する：/dir2/dir3/filename
後方から8文字目までを展開する：filename
先頭から6文字を削除し、7文字目から4文字目までを展開する：dir2
前方一致（最短一致）で先頭部分を削除して展開する：dir1/dir2/dir3/filename
前方一致（最長一致）で先頭部分を削除して展開する：filename
後方一致（最短一致）で後方部分を削除して展開する：/dir1/dir2/dir3
後方一致（最長一致）で後方部分を削除して展開する：
```


::: note warm
${var: -n}の記載方法については[:]の後ろに[半角スペース]が必要です。
:::


#### コマンド置換

コマンドの実行結果を文字列として展開します。以下のような記載方法が可能です。

- $(command)
- \`command\`

記載方法は`$(command)`の方が見やすく、コマンド置換の中で入れ子することも可能であるため、バッククォートよりも`$(command)`が使われるのが一般的です。

```shell:コマンド置換の基本書式
$ touch $(date +%F)
$ ls
2022-03-31.txt
```


#### プロセス展開

2つのファイルを引数として扱うコマンドを実行する場合にプロセス展開が便利です。

例えば、2つのコマンドの実行結果を`diffコマンド`で比較したい場合、比較するために一時的にファイルを作成する必要がありますが、プロセス展開を利用すれば、そのような処理は不要です。

```shell:プロセス展開の基本書式
$ diff <(sort file1.txt) <(sort file2.txt)
```

### クォーテーションの使い分け

Linuxでは2種類のクォーテーションを正しく使い分ける必要があります。

- シングルクォート`('var')`
- ダブルクォート`("var")`

それぞれの特徴は以下のとおりです。

- シングルクォートとダブルクォートはスペースを含んだ文字や変数を扱う
- シングルクォートは[$]を文字と扱うため変数として展開しない
- ダブルクォートは[$]を変数展開として扱うため変数として展開される
- ダブルクォートでも[$]の前に[\]でエスケープすれば[$]を記号として扱う

コマンド置換で利用するバッククォート（\`command\`）も含めると3種類のクォーテーションが存在します。


## 標準入出力とエラー出力

スクリプトを実際に実行してみると、想定外のエラーが発生することがあります。
（正常に動作しているように見えても標準入力の内容によってはエラーになるケースが意外と多いです。）

エラーが予想される場合は先回りして、エラーの出力先やエラー発生時の制御などを工夫する必要があります。

||標準の出力先|FD番号|
|---|---|---|
|標準入力（stdin）|キーボード|0|
|標準出力（stdout）|ディスプレイ|1|
|標準エラー出力（stderr）|ディスプレイ|2|

### 標準出力のリダイレクト
コマンドの実行結果をファイルに標準出力する

```shell:標準出力の基本書式
# command > result.txt
```

### 標準エラー出力のリダイレクト
コマンドの実行結果（エラー内容）をファイルに標準エラー出力する

```shell:標準エラー出力の基本書式
# command 2> result.txt
```

### 標準入力のリダイレクト
ファイルの内容を標準入力としてコマンドを実行

```shell:標準入力の基本書式
# command < input.txt
```

### 標準出力と標準エラー出力のリダイレクト
コマンドの実行結果＆エラー内容をファイルに標準出力＆標準エラー出力

```shell:標準出力と標準エラー出力を同じ出力先にする
# command &> result.txt
# command >& result.txt
# command > result.txt 2>&1
```

コマンドの実行結果（標準出力）をディスプレイ（標準エラー出力先）に出力

```shell:標準出力を標準エラー出力と同じ出力先にする
# command 1>&2
```

### その他の注意事項

出力先のリダイレクトについてポイントをまとめました。

- `set -o noclobber`を設定すると既存ファイルの上書きを防ぐ
- `>|`でリダイレクトすると`set -o noclobber`を無視する
- 追記の場合は`>`を`>>`に変更する
- 出力先を`/dev/null`にすると出力内容が削除される（どこにも出力されない）
- 標準出力の`>`と`1>`は同じ意味
- 標準入力の`<`と`2<`は同じ意味
- 標準エラー出力の出力先を標準出力と同じにするには`2>&1`を指定する

## 条件分岐と繰り返し

条件分岐と繰り返しに関するコマンドをまとめました。

|コマンド|内容|
|---|---|
|if（test）|定義した条件をもとに処理を分岐する|
|case|定義した条件をもとに処理を分岐する|
|for(seq)|定義した条件をもとに処理を繰り返す|
|while|定義した条件の間処理を繰り返す|
|until|定義した条件になるまでの間処理を繰り返す|
|continue|繰り返し処理の先頭に戻る|
|break|繰り返し処理を抜ける|

### 条件分岐（if文）

ifコマンドは条件によって処理を分岐するためのコマンドです。記載方法は以下のとおりです。

- if…最初の条件が真の場合の処理
- elif…最初の条件が偽で次の条件が真の場合の処理
- else…上記の全ての条件が偽の場合の処理
- fi…if文の終了

```bash:sample_if.sh（基本的な共通コマンド）
#!/bin/bash

echo -n '文字列[OK]を入力して下さい。:'

read input

if [ "$input" = OK ]; then
	echo '入力された文字列は正しいです。'
else
	echo '入力された文字列に誤りがあります。'
fi
```
`[`は特殊文字のように見えますが、コマンドとして扱います。
`[test]コマンド`でも同様の表現ができます。以下の2パターンの記載方法は同じ意味です。

- [ "$input" = OK ]
- test "$input" = OK

どちらも[testコマンド]と呼ばれますが、一般的には`[`コマンドが利用されます。

::: note info
readコマンドは標準入力の値を指定した変数に代入するコマンドです。
:::

### ifコマンドと終了ステータス

終了ステータス`[$?]`は以下の結果を返します。

- コマンド結果が正しい場合は0を返す
- コマンド結果が正しくない場合、条件にヒットしない場合は0以外を返す
- exitコマンド（関数の場合はreturnコマンド）により終了ステータスを指定することも可能

::: note info
grep -q コマンド(quietオプション)は検索文字自体は表示させず、検索した文字が一致するかしないかによって終了ステータスを判断する際によく利用されます。
:::

### 主な比較演算子

比較演算子はifコマンドやtestコマンドの他にもさまざまな制御構造で利用できます。

#### 主な比較演算子（文字列）

文字列の主な比較演算子は以下のとおりです。
|演算子|内容|
|:-:|:-:|
|str1 = str2|str1とstr2が等しい|
|str1 != str2|str1とstr2が等しくない|
|-n str1|str1が空文字ではない|
|-z str1|str1が空文字である|

#### 主な比較演算子（整数）

整数の主な比較演算子は以下のとおりです。
|演算子|内容|由来|
|:-:|:-:|:-:|
|int1 -eq int2|int1とint2が等しい|equal|
|int2 -ne int2|int1とint2が等しくない|not equal|
|int1 -gt int2|int1がint2より大きい|greater then|
|int1 -lt int2|int1がint2より小さい|less then|
|int1 -ge int2|int1がint2以上|greater equal
|int1 -le int2|int1がint2以下|less equal|

文字に対して整数用の比較演算子をもとに評価をすると[integer expression expected]のエラーが発生します。
入力内容を整数値に限定したい場合は、変数を整数として定義`（declareコマンド（iオプション））`することでエラーを回避できます。

#### 主な比較演算子（ファイル属性）

ファイル属性の主な演算子は以下のとおりです。
|演算子|内容|
|:-:|:-:|
|-a file(または -e file)|ファイルまたはディレクトリが存在する|
|-f file|ファイルが存在する|
|-d file|ディレクトリが存在する|


主な結合演算子は以下のとおりです。
|　演算子|内容|
|:-:|:-:|
|条件式1 -a 条件式2|両方が真の場合に真（AND）|
|条件式1 -o 条件式2|どちらかが真の場合に真（OR）|
|! 条件式|否定（not）|
|[ 条件式1 ] && [ 条件式2 ]|条件式1が真の場合に条件式2を実行|
|[ 条件式1 ] &#124;&#124; [ 条件式2 ]|条件式1が偽の場合に条件式2を実行|

### 条件分岐(case文)

caseコマンドはifコマンド同様、条件によって処理を分岐するためのコマンドです。ifコマンドよりもシンプルな構成で記載できますが、行数は多くなる傾向です。記載方法は以下のとおりです。

- case…条件式（パターンを列挙）
- esac…case文の終了

```bash:sample_case.sh（基本的な共通コマンド）
#!/bin/bash

echo -n '[A]〜[C]の文字を入力して下さい。:'

read input

case "$input" in
    "A")
        echo '入力された文字は[A]です。'
        ;;
    "B")
        echo '入力された文字は[B]です。'
        ;;
    "C")
        echo '入力された文字は[C]です。'
        ;;
    *)
        echo '入力された文字は[A]〜[C]以外です。'
        ;;
esac
```

`caseコマンド`は最初に一致した条件の処理を実行し、それ以降の条件は評価しません。（スキップします。）

### 繰り返し（for文、seqコマンド）

`forコマンド`は決められた回数だけ処理を繰り返すためのコマンドです。またリスト部分（inの後ろ）の値を変数名として参照することが可能です。記載方法は以下のとおりです。

```bash:sample_for.sh（forコマンドの基本書式）
#!/bin/bash

for i in 1 2 3 4 5
do
	echo $i
done
```

`seqコマンド`でも同様の実行結果を表すことが可能です。

```shell:seqコマンドの基本書式
$ seq 1 5
```

`forコマンド`と`seqコマンド`を組み合わせることで、構文を見やすくすることも可能です。
以下のサンプルではリスト部分を`seqコマンド`でコマンド置換すると記載方法は以下のようになります。

```bash:sample_for_seq.sh（forコマンドとseqコマンドの組み合わせ）
#!/bin/bash

for i in $(seq 1 5)
do
	echo $i
done
```

実行結果はすべて同じになります。

```shell:実行結果
1
2
3
4
5
```

::: note info
forコマンドのリスト部分を省略すると、引数（$@変数）がリストとして扱われます。
:::

### 繰り返し（whileコマンド、untilコマンド）

whileコマンドは条件が正しい間、繰り返し処理を続けます。untilコマンドはその逆です。

 - whileコマンド…変数[i]が10以下の間、2ずつ加算し続ける
 - untilコマンド…変数[i]が10を超えるまで、2ずつ加算し続ける

それぞれの記載方法は以下のとおりです。

```bash:sample_while.sh（whileコマンドの基本書式）
#!/bin/bash

i=0
while [[ $i -le 10 ]]
do
	echo "$i"
	i=$((i + 2))
done
```

```bash:sample_until.sh（untilコマンドの基本書式）
#!/bin/bash

i=0
until [[ $i -gt 10 ]]
do
	echo "$i"
	i=$((i + 2))
done
```

```shell:実行結果
0
2
4
6
8
10
```

## シェル関数

まとまった処理をシェル関数にまとめることで、同じ処理を何度も書かなくて済むため、長文のシェルスクリプトでもシンプルな構成が可能になります。

|関数関連の用語|意味|
|---|---|
|関数|まとまった処理をひとまとめに定義する|
|関数名|定義した関数を実行する（関数を呼び出す）|
|引数|関数を呼び出す際に関数内で利用する変数を渡す|
|戻り値|実行した関数により結果を取得する|

### シェル関数の基本書式

記載方法は以下のとおりです。

```bash:sample_func1.sh（シェル関数パターン1）
#!/bin/bash

# 関数の定義
function func1 ()
{
    # シェル関数内の処理
}
# 関数の呼び出し
func1
```

```bash:sample_func2.sh（シェル関数パターン2）
#!/bin/bash

# 関数の定義
function func1
{
    # シェル関数内の処理
}
# 関数の呼び出し
func1
```

```bash:sample_func3.sh（シェル関数パターン3）
#!/bin/bash

# 関数の定義
func1 ()
{
    # シェル関数内の処理
}
# 関数の呼び出し
func1
```

### シェル関数の終了ステータス

シェル関数の終了ステータスは[シェル関数内で最後に実行されたコマンドの終了ステータス]になりますが、`returnコマンド`で明示的に指定することで、シェル関数全体の終了ステータスとして扱うことができます。
通常の終了ステータス同様、終了ステータスに0以外を指定すればエラーとして扱うことが可能です。

### シェル関数とローカル変数

また、通常、変数を定義すると呼び出し元でも関数内（呼び出し先）でも同一のグローバル変数として扱われますが、関数内でローカル変数を定義すると、その変数は関数内でのみ有効な変数になります。

```bash:sample_func4.sh（シェル関数とローカル変数）
#!/bin/bash

func1 ()
{
	local var="local"
	echo "関数内の変数の値：$var"
}

var="global"
echo "メインプログラムの変数の値：$var"
func1
echo "メインプログラムの変数の値：$var"
```

関数内で上書きされているローカル変数がメインプログラム上では上書きされていないことが分かります。

```shell:実行結果
メインプログラムの変数の値：global
関数内の変数の値：local
メインプログラムの変数の値：global
```

## シェルスクリプトでよく利用するコマンド
シェルスクリプトでよく利用されるコマンドには以下のようなものがあります。
主に出力結果を加工するようなコマンドが中心です。

- grepコマンド
- sedコマンド
- awkコマンド
- teeコマンド
- xargsコマンド
- readコマンド

### grepコマンド

grepコマンドはファイルなどの標準入力から検索した文字列を含む行を出力することができます。

```shell:sedコマンドの基本書式
$ head -5 /etc/passwd
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
$ head -5 /etc/passwd | grep root
root:x:0:0:root:/root:/bin/bash
```

grepコマンドの主なオプションは以下のとおりです。

|オプション|内容|
|:-:|---|
|-e|検索文字をor条件で指定する|
|-i|大文字と小文字を区別せずに検索する|
|-v|一致しない行を出力する|
|-n|検索結果に行番号を含めて出力する|
|-o|検索結果に一致した文字のみを表示する|
|-q|検索結果に一致した行の有無を返す（一致した行は出力しない）|

:::note info
grepコマンドの[-q]オプションはif文などと組み合わせて使われるのが一般的です。
:::

### sedコマンド

sed(Stream EDitor)コマンドの特徴は以下のとおりです。

- ファイルなどの指定した文字列を置き換えて表示する
- 対象ファイルそのものは上書きしない（-iオプションで上書きも可能）
- リダイレクト先を同じファイル名にするとリダイレクトの特性上、ファイルの中身が空になる

以下の例では`/etc/passwd`の`:（コロン）`を`,（カンマ）`に置き換えて表示しています。

```shell:sedコマンドの基本書式
$ head -5 /etc/passwd
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
$ sed s/:/,/g /etc/passwd | head -5
root,x,0,0,root,/root,/bin/bash
bin,x,1,1,bin,/bin,/sbin/nologin
daemon,x,2,2,daemon,/sbin,/sbin/nologin
adm,x,3,4,adm,/var/adm,/sbin/nologin
lp,x,4,7,lp,/var/spool/lpd,/sbin/nologin
```

コマンド内の`s/.../.../g`の部分をスクリプトと呼びます。[s]と[g]には以下のような意味があるようです。

- s…substitute
- g…global

### awkコマンド

`awkコマンド`は指定のテキストより特定の箇所を抽出したり加工したりすることが可能です。

一般的な`awkコマンド`の記載方法は以下のとおりです。

```shell:awkコマンドの基本書式
$ awk -F: '{print $1,$(NF-1),$(NF)}' /etc/passwd | head -5
root /root /bin/bash
bin /bin /sbin/nologin
daemon /sbin /sbin/nologin
adm /var/adm /sbin/nologin
lp /var/spool/lpd /sbin/nologin
```

この例では`/etc/passwd`の`:（コロン）`を区切り文字として以下の3列を抽出しています。

- $1…1列目
- ${NF-1}…最後から2列目
- ${NF}…最後の列

### teeコマンド

`tee`コマンドは標準入力から受け取った結果を標準出力（モニターへの出力）とファイルの両方に出力します。

以下の例では`findコマンド`の結果を標準出力とファイル(cmdlist.txt)の両方に出力しています。

```shell:teeコマンドの基本書式
$ find /etc -type f | head -5 | tee cmdlist.txt
/etc/issue.net
/etc/nsswitch.conf
/etc/dnf/vars/infra
/etc/dnf/vars/contentdir
/etc/dnf/protected.d/yum.conf
$ cat cmdlist.txt 
/etc/issue.net
/etc/nsswitch.conf
/etc/dnf/vars/infra
/etc/dnf/vars/contentdir
/etc/dnf/protected.d/yum.conf
```

### xargsコマンド

xargsコマンド（エックスアーグズ）は、標準入力の結果を指定したコマンドに引数として渡します。パイプと組み合わせて利用するのが一般的です。
例えば、`findコマンド`の結果を`ls -lコマンド`に渡すような使い方をよく見かけます。

以下のサンプルでは、`/etc`配下のタイムスタンプが4年（1460日）以上経過したファイルの一覧に対して`ls -lコマンド`を実行しています。

```shell:xargsコマンドの基本書式
$ find /etc -type f -mtime +1460 
/etc/profile.d/gawk.sh
/etc/profile.d/gawk.csh
/etc/xattr.conf
$ find /etc -type f -mtime +1460 | xargs ls -l
-rw-r--r-- 1 root root 1107 Dec 14  2017 /etc/profile.d/gawk.csh
-rw-r--r-- 1 root root  757 Dec 14  2017 /etc/profile.d/gawk.sh
-rw-r--r-- 1 root root  642 Dec  9  2016 /etc/xattr.conf
```

### readコマンド

readコマンドは標準入力の値を指定した変数に代入するコマンドです。
主な使い方は以下の2パターンです。

- キーボードから対話形式に入力された内容を変数に代入
- ファイル内の内容をwhile文などを使用して1行ずつ変数に代入

readコマンドの基本書式（対話式）の基本書式は以下のとおりです。

```bash:sample_read01.sh（readコマンドの基本書式（対話式））
#!/bin/bash

echo '上書きしますか？（Yes/No）'
read input

if [ $input == Yes ]; then
	echo '上書きしました。'
elif [ $input == No ]; then
	echo 'キャンセルしました。'
else
	echo '[Yes]か[No]を入力してください。'
fi
```

```shell:実行結果
$ ./sample_read01.sh 
上書きしますか？（Yes/No）
Yes
上書きしました。
```
readコマンドの基本書式（ファイルからの読み込み式）の基本書式は以下のとおりです。

```bash:sample_read02.sh（readコマンドの基本書式（ファイルからの読み込み式））
#!/bin/bash

while IFS= read -r line
do
	printf '%s\n' "$line"
done
```

```shell:実行結果
$ cal | ./sample_read02.sh 
     April 2022     
Su Mo Tu We Th Fr Sa
                1  2
 3  4  5  6  7  8  9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
```
シェルスクリプトの内容について補足します。

- calコマンド（カレンダーの表示）の実行結果を1行ずつ出力する
- readコマンドでエスケープ文字を無効化するために[-r]オプションを使用する
- [IFS= ]によってIFS変数を空文字に設定することでスペース部分の左詰めを防ぐ

通常、IFS変数にはスペース、タブ、改行コードが含まれておりますが、空文字で上書きすることで、単語分割が行われず、スペース部分が左詰めされることを防ぎます。

なお、上のサンプルでは変数（$line）をダブルコーテーション（""）で囲まなければ、スペースごとに行が改行されてしまいます。
（いろいろ調べましたが理由が分かりませんでした。。。）

## 正規表現

よく利用される正規表現には以下のようなものがあります。

|正規表現|意味|
|---|---|
|^|行の先頭|
|$|行の末尾|
|.|任意の一文字|
|*|直前の文字の0回以上の繰り返し|
|[文字]|カッコ([])内に含まれるいずれかの1文字|
|[文字-文字]|カッコ([])内に含まれるいずれかの1文字|
|[^文字]|カッコ([])内に含まれないいずれかの1文字|
|\（バックスラッシュ）|直前のメタ文字（正規表現）の打ち消し|

また、以下のコマンドでは拡張正規表現も利用できます。

- grepコマンド（-Eオプション） 
- egrepコマンド
- sedコマンド（-rオプション）

|拡張正規表現|意味|
|---|---|
|+|直前の文字の1回以上の繰り返し|
|?|直前の文字の0回または1回の繰り返し|

### 拡張正規表現の具体例

以下のサイトに検索サンプル集がまとめられていました。

- [正規表現サンプル集](https://www.megasoft.co.jp/mifes/seiki/)

|正規表現|意味|
|---|---|
|(Word1\|Word2)|word1またはword2のどちらか|
|New(Word1\|Word2)|NewWord1またはNewWord2のどちらか|
|^-?[0-9]+$|先頭に0回か1回の[-]、末尾に1回以上の数字の繰り返し|


## その他の関連情報

シェルスクリプトを作成する上で必須の知識ではありませんが、関連する情報をまとめました。

### シェルスクリプト名の定義

上記の`[位置パラメータ]`でも解説しましたが、変数値に`[$0]`を定義することで実行しているシェルスクリプト名を取得することが可能です。
ただし、`[$0]`には実行したファイル名がそのまま設定されるため、ファイル名以外のパス（相対パスまたは絶対パス）が含まれてしまいます。

以下のように`[パラメータ展開]`（前方一致（最長一致）で先頭部分を削除して展開）を指定することで、ファイル名のみを出力することができます。
また、ファイル名はスクリプト内で変更されることがないため、`readonly`で読み取り専用（変更不可）にするのが一般的です。

```bash:script_name.sh（シェルスクリプト名の定義）
#!/bin/bash

readonly script_name=${0##*/}
```

### echoコマンドとprintfコマンドの違い

`echoコマンド`と`printf`コマンドはどちらも文字を標準出力に出力するコマンドです。
ただし、`echoコマンド`の場合、環境により改行のオプションが異なり、また指定した文字列にオプションで利用可能な文字が含まれているとそのままオプションとして扱われるため、あらかじめ決まっている固定文字以外の場合は、`printfコマンド`を利用することが推奨されています。
なお、`printf`コマンドは自動で改行がおこなわれないため、改行箇所に明示的に改行オプション`[¥n]`を記載する必要があります。

`printfコマンド`は書式を指定する必要があります。主な書式は以下のとおりです。
|書式|内容|
|---|---|
|%s|文字列|
|%d（または%i）|整数値|

### コマンドを複数行に分ける、1行にまとめる

1行のコマンドを複数行に分けるにはバックスラッシュ(`\`)をつかいます。

```bash:sample.sh（1行のコマンドを複数行に分ける）
#!/bin/bash

echo \
"Hello,World"
```

一方、複数行のコマンドを1行fにまとめるにはセミコロン(`;`)をつかいます。

```bash:sample.sh（複数行のコマンドを1行にまとめる）
#!/bin/bash

echo "Hello,World";pwd;ls
```

### ヒヤドキュメントの活用

複数行の標準入力を1つのコマンドに渡す

```shell:ヒヤドキュメントの基本書式
#!/bin/bash

cat << END
test01
test02
test03
END
```

### コマンドのグループ化の活用
複数行のコマンドの実行結果を1つの出力先に渡す

```shell:コマンドグループ化の基本書式

#!/bin/bash

{
    date +%Y-%m-%d
    pwd
    ls
} > result.txt
```

### シェルスクリプトのデバッグ

エラーや不具合を切り分けるためにデバッグ機能を利用することができます。
デバッグ自体は必須ではありません。

```shell:デバッグの実行
# sh -x ./sample01.sh
```

デバッグ機能はシェルスクリプトの先頭（シェバンの箇所）に定義することも可能です。

### 作業ログを記録するscriptコマンド

`scriptコマンド`は作業ログを記録する際に便利です。特徴は以下のとおりです。

- [script ファイル名]で作業ログの記録を開始する
- [script -a ファイル名]で既存のファイルに作業ログを追記する
- ファイル名を省略すると[typescript]のファイルが新規作成され記録される
- [exit]または[Ctrl+d]で記録を停止し、指定したログファイルにログ情報を出力する

### bash関連の設定ファイル

bashをログインシェルとして起動した場合、以下の順番で設定ファイルが読み込まれます。

|起動|ファイル名|内容|
|:-:|---|---|
|1|/etc/profile|全ユーザ共通、基本的に修正不要|
|2|~./bash_profile|ログイン時にのみ読み込まれる|
|3|~./bashrc|bashを起動するたびに[~./bash_profile]から読み込まれる|

### コマンドが実行される流れ

Linuxでコマンドやスクリプトを実行すると、以下のシステムコールを呼び出し、子プロセスが生成される流れです。

- fork…子プロセスを生成する
- exec…子プロセスを実行する
- exit…子プロセスを終了する
- wait…子プロセスの終了（exit）を待ち、子プロセスを削除する

`forkシステムコール`と`waitシステムコール`は親プロセス側で実行され、`execシステムコール`と`exitシステムコール`は子プロセス側で実行されます。`waitシステムコール`で正常に子プロセスを削除できなかった場合、子プロセスは`ゾンビプロセス`として残り続けてしまいます。

## まとめ

主に以下の内容が理解できれば基本的なシェルスクリプトが作成できると感じました。

- 変数の種類と特徴、展開、クォーテーションの使い分けなど
- シェルスクリプト（シェル）の基本的な記載方法
- 分岐と繰り返し、関数などの一般的なプログラムの知識

Windowsのバッチファイルと比べると、利用するコマンド数が多く、各コマンドのオプションも多岐にわたります。複雑なシェルスクリプトを作成するにはLinux全般の知識も欠かせないため、学習範囲はある程度広くなると思います。

## 参考文献

この記事は以下の書籍を参考にして執筆しました。

- [新しいシェルプログラミングの教科書](https://www.amazon.co.jp/dp/B077NC4N14/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1)

シェルスクリプトを作成する上で必要になる情報が網羅的にまとめられています。かなりボリュームはありますが、初心者にも分かりやすく解説されていました。