【備忘録】シェルスクリプトで条件文を絡めた処理を記述する

## はじめに

シェルスクリプトについて勉強したことを qiita にまとめています。
これまで、以下について学んできました。

- [Linux の基本操作を覚える](https://qiita.com/yuk1_sys/items/c03f77e516edbe9f6b32)
- [【備忘録】シェルのカスタマイズを学ぶ](https://qiita.com/yuk1_sys/items/e16555ee86d6e06ef0b9)
- [【備忘録】シェルスクリプトについて学ぶ](https://qiita.com/yuk1_sys/items/82fe6386398eafad2e98)

さて、今回は条件文などを主に扱っていきます。
シェルスクリプトについて学んでいる方、興味を持っている方はぜひお付き合いください。

#### 対象読者

- シェルスクリプトで条件文の記述を学びたい方
- bashの標準入力、標準出力を学びたい方
- パスの指定や終了ステータスについて学びたい方
- ファイルや標準出力の操作コマンドを学びたい方


## 変数と定数


### 変数の基本

変数とは、bash の処理を実行する際に特定の値を格納する領域のことを指す。

##### 変数作成の注意点

- = の前後に空白を空けない（ ❌ name = Taro ）
- 変数名に \\$ を使わない（ ❌ name\\$2=Taro ）
- " や ' で囲わない場合は空白を入れない（ ❌ name=Taro Yamada ）
- 変数名に空白を入れない（ ❌ my name=Taro ）
- if、for などシェルスクリプト上で利用される文字を変数名にしない（ ❌ if=12 ）
- 変数名に漢字、ひらがな、カタカナを使わない（ ❌ 名前=Taro ）
- 環境変数で使用されている変数を上書かない（ ❌ HOME=my_home ）

##### 変数のルール

- 使用できる文字は、**英数字**または **_（アンダースコア）**
- 最初の一文字目は、**数字を使えない**
- 大文字と小文字は**区別される**

##### 変数名の設定に使用するルール

[Google styleguide - Shell Style Guide](https://google.github.io/styleguide/shellguide.html) 参考

- 一般的な変数は、**小文字**を用いて、**_** でつなぐ
例）name, capital_name, zip_code
- **環境変数**と**定数**は、**大文字**を用いて、**_** でつなぐ
例）ORACLE_SID, SAVE_FILE_PATH, HOME


### 変数の設定

変数の設定は、以下のように 変数名=〇〇 とすればよい

```bash:例
name=Taro
```

ただし上の例では、Taroに空白が入るとエラーになるため、**変数の宣言では原則として、" または '** を用いる（ただし、**数値を代入する場合** は " や ' をつけない）

```bash:例
name="Taro"
name='Taro'
```

変数を取り出す場合には、"${変数名}" とする

```bash:例
echo "${name}"

echo $name # この書き方でもよいが "${}" として実行するのが基本形
```


### 変数利用時のダブルクォート、シングルクォートの違い

##### ダブルクォート（ " ）の場合

中に存在する変数が展開される

```text:例
[ec2-user@localhost ~]$ name="Taro"
[ec2-user@localhost ~]$ echo "Hi, My name is ${name}"
Hi, My name is Taro
```

##### シングルクォート（ ' ）の場合

中に存在する変数が展開されない

```text:例
[ec2-user@localhost ~]$ name="Taro"
[ec2-user@localhost ~]$ echo 'Hi, My name is ${name}'
Hi, My name is ${name}
```

##### 変数を展開した値を、別の変数に入れる

- ダブルクォートで囲うと中の変数が展開されて、展開された値が変数に格納される
- ダブルクォートの中でダブルクォートを使うには \\" とする
- ダブルクォートの中では、シングルクォートはそのまま使える

```text:例
[ec2-user@localhost ~]$ name="Taro"
[ec2-user@localhost ~]$ age=28
[ec2-user@localhost ~]$ sentence="He said \"Hi, My name is ${name}. I'm ${age} year's old\""
[ec2-user@localhost ~]$ echo $sentence
He said "Hi, My name is Taro. I'm 28 year's old"
```

### 定数

- 定数は、プログラムを記述する際に **値が書き変わらないことを前提としている** 変数である
- **readonly 〇〇** として、変数を定数として設定する
- 定数は、**大文字**で記述する（規約）

```bash:例
readonly APP_PATH="/opt/app/bin" # 変数名の前に readonly をつける

echo "${APP_PATH}" # 利用する場合は普通の変数と同じ

❌ APP_PATH="/usr/app/bin" # 値を書き換えるとエラーになる
```



### コマンドの実行結果を変数に格納する

コマンドの実行結果を変数に格納する方法は、以下の 2 通りがある。

##### 1. \`\` バッククォートを用いる

```text:例（ ls の実行結果を変数 files に格納する ）
[ec2-user@localhost ~]$ files="`ls`"
[ec2-user@localhost ~]$ echo "${files}"
file1.txt
file2.txt
```

##### 2. $() を用いる（ 可読性が良いため推奨 ）

```text:例（ ls の実行結果を変数 files に格納する ）
[ec2-user@localhost ~]$ files="$(ls)"
[ec2-user@localhost ~]$ echo "${files}"
file1.txt
file2.txt
```



### 特殊な変数

$ を使用した特殊な変数も存在する

| 変数名 | 格納されている値 |
|:-|:-|
| $0 | 実行しているファイル名 |
| $USER | 実行しているユーザ名 |
| $SECONDS | スクリプトを実行してからの秒数 |
| $RANDOM | ランダムの数値 |
| $LINENO | 行番号 |



## 配列の利用

配列は、**複数の値を一つの変数に格納する** 用途で用いられる。
() で囲って、スペースで区切ることにより配列を作成する。

```text:例（ [数字]で特定の要素にアクセスする ）
[ec2-user@localhost ~]$ months=("January" "February" "March")
[ec2-user@localhost ~]$ echo "${months[0]}"
January
```

```text:例（ [@]で全要素にアクセスする ）
[ec2-user@localhost ~]$ months=("January" "February" "March")
[ec2-user@localhost ~]$ echo "${months[@]}"
January February March
```

##### 値を変更・追加する

```text:例（ 0 番目の要素の値を変更 ）
[ec2-user@localhost ~]$ months=("January" "February" "March")
[ec2-user@localhost ~]$ months[0]="january"
[ec2-user@localhost ~]$ echo "${months[@]}"
january February March
```

```text:例（ monthsに要素を追加 ）
[ec2-user@localhost ~]$ months=("January" "February" "March")
[ec2-user@localhost ~]$ months+=("April" "May")
[ec2-user@localhost ~]$ echo "${months[@]}"
January February March April May

[ec2-user@localhost ~]$ months=("${months[@]}" "June" "July")
[ec2-user@localhost ~]$ echo "${months[@]}"
January February March April May June July
```

##### 配列を明示的に宣言するには declare -a と記述する

```text:例
[ec2-user@localhost ~]$ declare -a months=("January" "February" "March")
```

##### 長さを取得する

```text:例（ 要素の数を取得 ）
[ec2-user@localhost ~]$ declare -a months=("January" "February" "March")
[ec2-user@localhost ~]$ echo "${#months[@]}"
3
```

```text:例（ 要素 1 の文字数を取得 ）
[ec2-user@localhost ~]$ declare -a months=("January" "February" "March")
[ec2-user@localhost ~]$ echo "${#months[1]}"
8
```

##### 配列をつなげる

```text:例
[ec2-user@localhost ~]$ array3=("${array1[@]}" "${array2[@]}")
```

##### 要素の削除

```text:例（ 要素 1 を削除する ）
[ec2-user@localhost ~]$ unset array[1]
```

##### 要素のうち一部にアクセスする

```text:例（ 要素 2 から 2 要素取り出す ）
[ec2-user@localhost ~]$ months=("January" "February" "March" "April" "May")
[ec2-user@localhost ~]$ echo "${months[@]:2:2}"
March April
```

##### 要素から一部の文字を取り出す

```text:例（ 要素 1 の 2 文字目から 5 文字取り出す ）
[ec2-user@localhost ~]$ months=("January" "February" "March" "April" "May")
[ec2-user@localhost ~]$ echo "${months[1]:2:5}"
bruar
```



## 数値計算

以下の通り実行しても数値計算はできない。

```text:例
[ec2-user@localhost ~]$ echo "1 + 3"
1 + 3
```

##### 1. $(expr 計算式) を用いる

```text:例
[ec2-user@localhost ~]$ echo "$(expr 2 + 3)"
5
[ec2-user@localhost ~]$ echo "$(expr 2 - 3)"
-1
[ec2-user@localhost ~]$ echo "$(expr 2 \* 3)"
6
[ec2-user@localhost ~]$ echo "$(expr 2 / 3)"
0
[ec2-user@localhost ~]$ echo "$(expr 2 % 3)"
2
```

##### 2. $(()) を用いる（ 可読性が良いため推奨 ）

```text:例
[ec2-user@localhost ~]$ echo "$((2 + 3))"
5
[ec2-user@localhost ~]$ echo "$((2 - 3))"
-1
[ec2-user@localhost ~]$ echo "$((2 \* 3))"
6
[ec2-user@localhost ~]$ echo "$((2 / 3))"
0
[ec2-user@localhost ~]$ echo "$((2 % 3))"
2
```



## 標準入力

標準入力とは、ユーザのターミナルからの入力のことを指す。
入力された値を変数に格納する。

**read** コマンドを実行することで、ユーザからの入力を受け付ける。


### read コマンド

`read [変数名]` で、直後にユーザが入力した値を `[変数名]` に格納する。

```text:例（ read コマンドの後にユーザの入力を受け付ける ）
[ec2-user@localhost ~]$ read name
Taro
[ec2-user@localhost ~]$ echo "${name}"
Taro
```

##### read コマンドのオプション

| オプション | 意味 |
|:-|:-|
| -s | 画面に入力内容が表示されない（パスワードに利用する） |
| -p | 標準入力の前に文字列を表示する |
| -a | 画面に入力した内容を配列にする |

```text:例（ -s ）
[ec2-user@localhost ~]$ read -s password

[ec2-user@localhost ~]$ echo "${password}"
12345678
```

```text:例（ -p ）
[ec2-user@localhost ~]$ read -p "お名前を入力してください: " name
お名前を入力してください: Yuta
[ec2-user@localhost ~]$ echo "${name}"
Yuta
```

```text:例（ -a ）
[ec2-user@localhost ~]$ read -a favourite_foods
寿司 スパゲッティ 焼肉
[ec2-user@localhost ~]$ echo "${favourite_foods[1]}"
スパゲッティ
```



## 引数

引数とは、シェルスクリプトを実行する際に渡す変数を指す。
空白で区切って、第一引数、第二引数、... となる。

\\${ 1 } ... \\${ N } : 第一引数 ... 第 N 引数
\\${ # } : 引数の数
\\${ @ } : 全引数

```bash:例（ test.sh ）
#!/bin/bash
echo "第一引数は: ${1}"
echo "第二引数は: ${2}"
echo "第三引数は: ${3}"
echo "引数の数は: ${#}"
echo "全引数は: ${@}"
```

```text:例（ test.sh の実行 ）
[ec2-user@localhost ~]$ ./test.sh a b c d e
第一引数は: a
第二引数は: b
第三引数は: c
引数の数は: 5
全引数は: a b c d e
```



## if 文

### if の３通りの書き方

##### 1. 「 if test 条件文 」で記述する

```bash:例
if test 条件式1; then
    # 条件式1を満たす場合に、実行したい処理

elif test 条件式2; then
    # 条件式1を満たさず条件式2を満たす場合に、実行したい処理

else
    # 条件式1も条件式2も満たさない場合に、実行したい処理

fi
```

##### 2. 「 if [ 条件文 ] 」で記述する

```bash:例
if [ 条件式1 ]; then
    # 条件式1を満たす場合に、実行したい処理

elif [ 条件式2 ]; then
    # 条件式1を満たさず条件式2を満たす場合に、実行したい処理

else
    # 条件式1も条件式2も満たさない場合に、実行したい処理

fi
```

##### 3. 「 if [[ 条件文 ]] 」で記述する （ 正規表現処理などに対応しているため推奨 ）

```bash:例
if [[ 条件式1 ]]; then
    # 条件式1を満たす場合に、実行したい処理

elif [[ 条件式2 ]]; then
    # 条件式1を満たさず条件式2を満たす場合に、実行したい処理

else
    # 条件式1も条件式2も満たさない場合に、実行したい処理

fi
```

##### 文字列の比較

| 比較文字 | 意味 |
|:-|:-|
| = | 等しいか |
| == | 等しいか |
| != | 等しくないか |

##### 数値の比較

| 比較文字 | 意味 |
|:-|:-|
| -eq | 等しいか |
| -ne | 等しくないか |
| -lt | より小さいか（ less than ） |
| -le | 以下か（ less equal ） |
| -gt | より大きいか（ greater than ） |
| -ge | 以上か（ greater equal ） |


### 数値比較の if 文の特殊な書き方

(( 条件式 )) と記述することで、> 、< などの比較式を使うことができる。

```bash:例
if (( "${number}" < 12 )); then
    echo "numberは12より小さいです"
fi

# 計算式を記述
if (( "${number}" + 10 == 20 )); then
    echo "number+10は20です"
fi

# [[]]で計算式を記述した場合
if [[ $(( "${number}" + 10 )) -eq 20 ]]; then
    echo "number+10は20です"
fi
```

##### (())を利用した数値の加算・減算

数値を1つ増やしたい（減らしたい）場合に利用される方法。

```bash:例
(( number++ )) # number が +1 される（ number が 1 だった場合、2 になる ）
(( number+=2 )) # number が +2 される
(( number-- )) # number が -1 される
(( number-=2 )) # number が -2 される
(( number*=3 )) # number が *3 される
(( number/=2 )) # number が /2 される
```


### 否定形の書き方

条件式を満たさない場合の処理を記述する。

```bash:例（ if ! test 条件式; then ）
if ! test "${name}" == "Taro"; then
    # ${name}がTaroでない場合に実行する処理
```

```bash:例（ if [ ! 条件式 ]; then ）
if [ ! "${name}" == "Taro" ]; then
    # ${name}がTaroでない場合に実行する処理
```

```bash:例（ if [[ ! 条件式 ]]; then ）
if [[ ! "${name}" == "Taro" ]]; then
    # ${name}がTaroでない場合に実行する処理
```

```bash:例（ if ! (( 条件式 )); then ）
if ! (( "${name}" == "Taro" )); then
    # ${name}がTaroでない場合に実行する処理
```


### OR（ 論理和 ）の書き方

A or B のように、複数の条件のどれかを満たせば処理が実行される if 文。

```bash:例（ if test 条件式1 || test 条件式2; then ）
if test "${name}" == "Taro" || test "${age}" -gt 21; then
    # ${name}がTaro、または${age}が12より大きい場合に実行する処理
fi
```

```bash:例（ if [ 条件式1 ] || [ 条件式2 ]; then ）
if [ "${name}" == "Taro" ] || [ "${age}" -gt 21 ]; then
    # ${name}がTaro、または${age}が12より大きい場合に実行する処理
fi
```

```bash:例（ if [ 条件式1 -o 条件式2 ]; then ）
if [ "${name}" == "Taro" -o "${age}" -gt 21 ]; then
    # ${name}がTaro、または${age}が12より大きい場合に実行する処理
fi
```

```bash:【推奨】例（ if [[ 条件式1 || 条件式2 ]]; then ）
if [[ "${name}" == "Taro" || "${age}" -gt 21 ]]; then
    # ${name}がTaro、または${age}が12より大きい場合に実行する処理
fi
```

```bash:【推奨】例（ if (( 条件式1 || 条件式2 )); then ）
if (( "${number}" >= 12 || "${age}" > 21 )); then
    # ${number}が12以上、または${age}が12より大きい場合に実行する処理
fi
```


### AND（ 論理積 ）の書き方

A and B のように、複数の条件の全てを満たせば処理が実行される if 文。

```bash:例（ if test 条件式1 && test 条件式2; then ）
if test "${name}" == "Taro" && test "${age}" -gt 21; then
    # ${name}がTaro、かつ${age}が12より大きい場合に実行する処理
fi
```

```bash:例（ if [ 条件式1 ] && [ 条件式2 ]; then ）
if [ "${name}" == "Taro" ] && [ "${age}" -gt 21 ]; then
    # ${name}がTaro、かつ${age}が12より大きい場合に実行する処理
fi
```

```bash:例（ if [ 条件式1 -a 条件式2 ]; then ）
if [ "${name}" == "Taro" -a "${age}" -gt 21 ]; then
    # ${name}がTaro、かつ${age}が12より大きい場合に実行する処理
fi
```

```bash:【推奨】例（ if [[ 条件式1 && 条件式2 ]]; then ）
if [[ "${name}" == "Taro" && "${age}" -gt 21 ]]; then
    # ${name}がTaro、かつ${age}が12より大きい場合に実行する処理
fi
```

```bash:【推奨】例（ if (( 条件式1 && 条件式2 )); then ）
if (( "${number}" >= 12 && "${age}" > 21 )); then
    # ${number}が12以上、かつ${age}が12より大きい場合に実行する処理
fi
```


### 変数が設定されていない場合の処理

変数に値が設定されていない場合の処理を記述するには、以下のようにする。

```bash:【推奨】例 ①（ -z ）
if [[ -z "${my_var}" ]]; then
    # 変数が設定されていないか空白の場合の処理
fi
```

```bash:例 ②（ == "" ）
if [[ "${my_var}" == "" ]]; then
    # 変数が設定されていないか空白の場合の処理
fi
```


### 変数が設定されている場合の処理

変数に値が設定されている場合の処理を記述するには、以下のようにする。

```bash:【推奨】例 ①（ -n ）
if [[ -n "${my_var}" ]]; then
    # 変数が設定されていないか空白の場合の処理
fi
```

```bash:例 ②（ != "" ）
if [[ "${my_var}" != "" ]]; then
    # 変数が設定されていないか空白の場合の処理
fi
```


### 正規表現によるパターンマッチング

変数に対して、〇〇 で始まる文字列なのか、〇〇 を含む文字列なのかといった特定の条件で処理を記述する場合に用いる。

##### 前方一致

```bash:例
if [[ "${var}" == 1* ]]; then
    # ${var}が1で始まる場合の処理を記述する
fi
```

##### 後方一致

```bash:例
if [[ "${var}" == *1 ]]; then
    # ${var}が1で終わる場合の処理を記述する
fi
```

##### 部分一致

```bash:例
if [[ "${var}" == *1* ]]; then
    # ${var}が1を含む場合の処理を記述する
fi
```


### ファイルのチェック

Bash を実行する際に、特定のファイルが現在のディレクトリ上に存在するのか、書き込み権限があるのかといったチェックをすることがある。

| 例 | 意味 |
|:-|:-|
| [[ -e 〇〇 ]] | 〇〇ファイル、もしくは〇〇ディレクトリが存在するか |
| [[ -f 〇〇 ]] | ディレクトリでなく〇〇ファイルが存在するか |
| [[ -d 〇〇 ]] | ファイルでなく〇〇ディレクトリが存在するか |
| [[ -w 〇〇 ]] | 〇〇ファイル、もしくはディレクトリに書き込み権限があるか |
| [[ -x 〇〇 ]] | 〇〇ファイル、もしくはディレクトリに実行権限があるか |
| [[ fileA -nt fileB ]] | fileAがfileBよりも新しいか |
| [[ fileA -ot fileB ]] | fileAがfileBよりも古いか |



## case 文

変数の値に応じて処理を変更する文

```bash:例
case "${1}" in
    "A")
        echo "a";;
    "B")
        echo "b";;
    "C")
        echo "c";;
    "*")
        echo "etc";;
esac
```



## select 文

複数ん選択肢の中から、どれかを選択する構文。

「select 変数名 in スペース区切りの文字列」のように記述する。

**変数名**：ユーザが選んだ変数名
**$REPLY**：ユーザが選んだ文字のインデックス（ 1 始まり ）

```bash:例
select character in Goku Bezita Pikkoro Gohan Kuririn
do
    echo "Selected character: $character"
    echo "Selected number: $REPLY"
done
```

```text:bash
[ec2-user@localhost ~]$ ./test.sh
1) Goku
2) Bezita
3) Pikkoro
4) Gohan
5) Kuririn
```


## for 文

### 文字列に対する for ループ

指定したリストを一つずつ変数に格納して、ループ処理を行う。
リストは、空白区切りの文字列で表す。

下の例の場合、3 回処理が実行される。

```bash:例
names="Taro Jiro Saburo"
for name in ${names} # 変数の前後に "" をつけないことに注意
do
    echo "${name}"
done
```


### リストに対する for ループ

リストに対してループを実行するには、変数名を "${array[@]}" とする。

```bash:例
names=("Shinfi" "Rei" "Asuka")
for name in ${names[@]} # 変数の前後に "" をつけないことに注意
do
    echo "${name}"
done
```


### 数値の範囲を指定した for ループ

数値の 0 ~ 10 までのように、範囲を指定してループすることもできる。

```bash:例（ 0~5 まで (0 1 2 3 4 5) がループされる ）
for num in {0..5}
do
    echo "${num}"
done
```

```bash:例（ 0~5 まで 2 つ飛ばしで、(0 2 4) がループされる ）
for num in {0..5..2}
do
    echo "${num}"
done
```

```bash:例（ 10~0 まで -2 つ飛ばしで、(10 8 6 4 2 0) がループされる ）
for num in {10..0..2}
do
    echo "${num}"
done
```

##### シーケンスを用いた for ループ

シーケンスを利用して数値範囲を指定することも可能。

```bash:例（ 0~5 まで (0 1 2 3 4 5) がループされる ）
for num in $( seq 0 5 )
do
    echo "${num}"
done
```

```bash:例（ 0~5 まで 2 つ飛ばしで、(0 2 4) がループされる ）
for num in $( seq 0 2 5 )
do
    echo "${num}"
done
```

```bash:例（ 10~0 まで -2 つ飛ばしで、(10 8 6 4 2 0) がループされる ）
for num in $( seq 10 -2 0 )
do
    echo "${num}"
done
```


### 辞書の利用

配列に特定のキーに対応する値を入れることができる（一般的に辞書という）

```bash:例
declare -A salaries # 配列の宣言

# キーに対応する値を入れる
salaries['Taro']=1000000
salaries['Hanako']=1500000
salaries['Yoshiko']=1250000

# キーをループして取り出すには、${!配列名[@]} とする
for name in ${!salaries[@]}
do
    echo "${name}: ${salaries[${name}]}"
done
```


### continue で処理を飛ばす

continue が実行されると、**その後の処理が飛ばされて次のループに移る**ことができる。

特定の条件下で処理を飛ばしたい場合に用いられる。

```bash:例
for num in {0..10}
do
    if [[ "${num}" -eq 5 ]]; then
        continue
    fi
    # numが5の時だけこの後の処理は実行されない
    echo "${num}"
done
```


### break でループを終了する

break が実行されると、**ループ処理を終了**させることができる。

```bash:例
for num in {0..10}
do
    if [[ "${num}" -eq 5 ]]; then
        break
    fi
    # numが5でループは終了するため、numが5以降ではこの後の処理は実行されない
    echo "${num}"
done
```


### 引数をループする

以下のように ${@}（全引数）を用いて取り出す

```bash:例
i=1
for user in ${@}
do
    echo "Username - ${i}: ${user}"
    (( i++ ))
done
```



## while 文

指定した条件を満たしている間は、ループ処理を行う。

**while [[ 条件式 ]]** : 条件式を満たしている限り処理を続ける
**(( counter++ ))** : 変数 counter に 1 加算されて、1..10まで値が増えていく

```bash:例
counter=1
while [[ "${counter}" -le 10 ]] # (( "${counter}" <= 10 ))でも同じ
do
    echo "${counter}"
    (( counter++ ))
done
```



## until 文

指定した条件を満たすまで、ループ処理を行う。

**until [[ 条件式 ]]** : 条件式を満たすまで、中の処理を続ける
**(( counter++ ))** : 変数counterに 1 加算されて、1..10 まで値が増えていく

```bash:例
counter=1
until [[ "${counter}" -gt 10 ]] # (( "${counter}" > 10 ))でも同じ
do
    echo "${counter}"
    (( counter++ ))
done
```



## 標準出力

標準出力とは、コマンドを実行した際に出力されるテキストのことを指す。

デフォルトではディスプレイ上に出力されるが、出力先をファイルに変更することも可能。

##### >

標準出力をディスプレイでなく、ファイルに書き出す（ファイルの中身がすでに存在する場合は**上書かれる**）

```text:例
[ec2-user@localhost ~]$ echo "Hello World" > file.txt
[ec2-user@localhost ~]$ cat file.txt
Hello World
```

##### >>

標準出力をディスプレイでなく、ファイルに書き出す（ファイルの中身がすでに存在する場合は**追記される**）

```text:例（ > の続き ）
[ec2-user@localhost ~]$ echo "Hello Japan" > file.txt
[ec2-user@localhost ~]$ cat file.txt
Hello World
Hello Japan
```


### 標準エラー出力

標準エラー出力とは、コマンドが失敗した際に出力されるエラーメッセージのことを指す。

デフォルトではディスプレイ上に出力されるが、出力先をファイルに変更することも可能。

##### 2>

標準エラー出力をディスプレイでなく、ファイルに書き出す（ファイルの中身がすでに存在する場合は**上書かれる**）

```text:例
[ec2-user@localhost ~]$ echo "Hello" 2> file.txt
[ec2-user@localhost ~]$ cat error.txt
-bash: ech: コマンドが見つかりません
```

##### 2>>

標準エラー出力をディスプレイでなく、ファイルに書き出す（ファイルの中身がすでに存在する場合は**追記される**）

```text:例（ > の続き ）
[ec2-user@localhost ~]$ echo "Hello" 2>> file.txt
[ec2-user@localhost ~]$ cat error.txt
-bash: ech: コマンドが見つかりません
-bash: ech: コマンドが見つかりません
```


### 標準出力と標準エラー出力を別のファイルに出力する

##### 1> 〇〇 2> ××

標準出力をファイル〇〇に、標準エラー出力をファイル××に書き出す（ファイルの中身がすでに存在する場合は**上書かれる**）

```text:例
[ec2-user@localhost ~]$ ./file.sh 1> output.txt 2> error.txt
```

##### 1>> 〇〇 2>> ××

標準出力をファイル〇〇に、標準エラー出力をファイル××に書き出す（ファイルの中身がすでに存在する場合は**追記する**）

```text:例
[ec2-user@localhost ~]$ ./file.sh 1>> output.txt 2>> error.txt
```


### 標準出力と標準エラー出力を同じファイルに出力する

##### &> 〇〇

標準出力と標準エラー出力をファイル〇〇に書き出す（ファイルの中身がすでに存在する場合は**上書かれる**）

```text:例
[ec2-user@localhost ~]$ ./file.sh &> output.txt
```

##### &>> 〇〇

標準出力と標準エラー出力をファイル〇〇に書き出す（ファイルの中身がすでに存在する場合は**追記する**）

```text:例
[ec2-user@localhost ~]$ ./file.sh &>> output.txt
```

##### 2>&1

標準エラー出力を標準出力と同じ設定にする

```text:例（標準エラー出力も標準出力もファイル output.txt に出力される）
[ec2-user@localhost ~]$ ./file.sh > output.txt 2>&1
```


### 標準出力と標準エラー出力を一切出力しない

##### > /dev/null

標準出力をディスプレイ、ファイルに出力しない

```text:例
[ec2-user@localhost ~]$ ./file.sh > /dev/null
```

##### 2> /dev/null

標準エラー出力をディスプレイ、ファイルに出力しない

```text:例
[ec2-user@localhost ~]$ ./file.sh 2> /dev/null
```

##### &> /dev/null

標準出力と標準エラー出力をディスプレイ、ファイルに出力しない

```text:例
[ec2-user@localhost ~]$ ./file.sh &> /dev/null
```



## ファイルからの入力


### cat の実行結果を変数に格納する

以下のように cat コマンドの実行結果をそのまま = でつなぐことで、変数にファイルの中身を格納することができる

```bash:例（ $() で cat の実行結果を変数に格納する ）
#!/bin/bash

file=$(cat file.txt)
```


### while ループで 1 行ずつ、ファイルを読み込む

ファイルを 1 行ずつ読み込んで処理をしたい場合、while を利用する方法がある

```bash:例（ 変数 p にはファイルが 1 行ずつ格納されてループする ）
while read p
do
    echo "${p}"
done < file.txt
```


### cat で実行した結果を while に渡してループする

もう 1 つの代表的なファイル入力の方法として、cat ファイル名 | while を実行して、ループする方法がある。

このとき、while の中の処理は **サブシェル** と呼ばれ、**while 外の変数を変更しても変更が反映されない** ため注意。

```bash:例
var="before"

cat file.txt | while read p
do
    var="after"
    echo "${p}"
done

echo "${var}" # "before"のままで修正内容が反映されない
```



## パスについて

### 【非推奨】 絶対パス

ルートディレクトリからの各ファイルやディレクトリへのパス。

ユーザのカレントディレクトリに左右されないが、ファイルの配置場所やディレクトリ名が変わるとパスも当然変わるので利用は控えたい。

例）/home/test/hoge.txt , /tmp/bigfile.txt


### 【非推奨】 相対パス

カレントディレクトリからの各ファイルやディレクトリへのパス。

ユーザのカレントディレクトリによって相対パスは変化するので利用は控えたい。

例）./hoge.txt , ../data.txt


### 【推奨】 realpath と dirname を用いたファイルの読み込み

##### realpath

指定したファイル、ディレクトリの絶対パスを表示するコマンド

```text:例
[ec2-user@localhost ~]$ realpath sample.txt
/home/ec2-user/sample.txt
```

##### dirname

ファイル、ディレクトリの絶対パス名から親ディレクトリ名を取得するコマンド

```text:例
[ec2-user@localhost ~]$ dirname /home/ec2-user/sample.txt
/home/ec2-user
```

##### realpath と dirname を組み合わせる

```text:ディレクトリ階層
project
    ├─ bin
    │   └─ program.sh
    └─ data
        └─ name.csv
```

```bash:例（ 上記のディレクトリ階層の program.sh から name.csv を参照する ）
# program.sh

full_path=$(realpath $0) # 実行シェルの絶対パスを取得
echo "${full_path}" # /home/test/project/bin/program.sh
bin_path=$(dirname "${full_path}") # 1つ上のディレクトリ（/home/test/project/bin）
project_path=$(dirname ${bin_path}) # 1つ上のディレクトリ（/home/test/project）
data_path="${project_path}"/data # /home/test/project/data
cat "${data_path}/name.csv" # /home/test/project/data/name.csv
```



## 終了ステータス

終了ステータスとは、Bash でコマンドを実行した際にそのコマンドが **成功、もしくは失敗したか** を表すコードを指す。

- コマンドが **成功した場合** は 0
- コマンドが **失敗した場合** は 0 以外（ 多くは 1 ）を返す

### $?

直前に実行したコマンドの終了ステータスを数値で取得する

```text:例
[ec2-user@localhost ~]$ date -c
date: 無効なオプション -- 'c'
Try 'date --help' for more information.
[ec2-user@localhost ~]$ echo $?
1
```


###### 終了ステータスでコマンドの実行結果を制御する

$? を用いて終了ステータスを取得し、コマンドが成功したか確認しながら処理を記述する

```bash:例
if (( $? != 0 )); then
    # 前回のコマンドの実行が失敗した場合の処理
fi
```


### exit

**指定した終了ステータスを返してプロセスの処理を終了させる** コマンド。

引数を指定しない場合には、終了ステータス 0 が返される。

```bash:例
if (); then
    exit 1; # この場合はエラーコード（ ステータス 1 ）として終了する
fi
# 処理
exit 0 # 正常のステータス（ 0 ）で終了する
```



## && と ||

### &&

コマンドを複数つなげて実行する場合に用いる。

1 つ目のコマンドが **成功した場合**（終了ステータス 0）に、2 つ目のコマンドが実行される。

```text:例
[ec2-user@localhost ~]$ echo "A" && echo "B"
A
B
```


### ||

コマンドを複数つなげて実行する場合に用いる。

1 つ目のコマンドが **失敗した場合**（終了ステータス 0 以外）に、2 つ目のコマンドが実行される。

```text:例
[ec2-user@localhost ~]$ cho "A" || echo "B"
-bash: cho: コマンドが見つかりません
B
```

##### A && B || C

**コマンド A が成功した場合** は、次にコマンド B が実行され、
**コマンド A かコマンド B が失敗した場合** は、次にコマンド C が実行される。

```text:例
[ec2-user@localhost ~]$ echo "A" && ech "C" || echo "B"
A
-bash: cho: コマンドが見つかりません
B
```



## ファイル、標準出力操作

### | （パイプ）

プログラムの標準出力の結果を別のプログラムで扱う

```text:例（ 標準出力に出力された "ABCDEF" から 1-4 文字目を切り取る ）
[ec2-user@localhost ~]$ echo "ABCDEF" | cut -c 1-4
ABCD
```


### コマンド集

##### cut コマンド

ファイルや標準出力から、行ごとに一部の文字を切り取る

| オプション | 意味 |
|:-|:-|
| -c | 取り出す文字の位置を指定して取り出す |
| -f | 取り出すフィールド番号を指定する |
| -d | デリミタ（区切り文字）を指定する |

```text:例
[ec2-user@localhost ~]$ echo "ABCDEF" | cut -c "1,3,4"
ACD
[ec2-user@localhost ~]$ cat file.txt
1,Taro,12
2,Jiro,11
3,Hanako,14
[ec2-user@localhost ~]$ cut -d "," -f "3,2" file.txt
Taro,12
Jiro,11
Hanako,14
```

##### tr コマンド

文字列の変換・消去をする

| オプション | 意味 |
|:-|:-|
| -d | 指定した文字を削除 |
| -s | 指定した文字が連続した場合、1つにする |
| -s [:space:] | スペースが連続した場合、1つにする |

```text:例
[ec2-user@localhost ~]$ echo "ABCDEAABAABB" | tr -d "A"
BCDEBBB
[ec2-user@localhost ~]$ echo "ABCDEAABAABB" | tr -d "AB"
CDE
[ec2-user@localhost ~]$ echo "ABCDEAABAABB" | tr -s "A"
ABCDEABAB
[ec2-user@localhost ~]$ echo "ABCDEAABAABB" | tr -s "AB"
ABCDEABAB
```

##### sed コマンド

テキストを定められた手順で一括処理するコマンド（ ストリームエディタの略 ）

| コマンド例 | 出力結果 |
|:-|:-|
| sed "nd" ファイル名 | ファイルの n 行目を削除して標準出力 |
| sed "$d" ファイル名 | ファイルの最終行を削除して標準出力 |
| sed "2,4$d" ファイル名 | ファイルの 2-4 行目を削除して標準出力 |
| sed "/abc/d" ファイル名 | 文字列 abc を含んだ行を削除して標準出力 |
| sed "/^$/d" ファイル名 | 空白行を削除して標準出力 |
| sed "s/〇〇/××" ファイル名 | ファイルの各行の一番最初の 〇〇 を ×× に変換して標準出力 |
| sed "s/〇〇/××/2" ファイル名 | ファイルの各行の 2 番目の 〇〇 を ×× に変換して標準出力 |
| sed "s/〇〇/××/g" ファイル名 | ファイルの各行の全 〇〇 を ×× に変換し標準出力 |
| sed "s/〇〇/××/2g" ファイル名 | ファイルの各行の 2 番目以降の 〇〇 を ×× に変換し標準出力 |
| sed -i "s/〇〇/××/g" ファイル名 | ファイルの各行の 2 番目以降の 〇〇 を ×× に変換して保存 |

```text:例
[ec2-user@localhost ~]$ echo "apple banana orange" | sed "s/apple/ringo/g"
ringo banana orange
```

##### sort コマンド

ファイルの行を降順/昇順に並び替えて出力するコマンド

| コマンド例 | 出力結果 |
|:-|:-|
| sort -r | 降順に並び替える |
| sort -R | ランダムに並び替える |
| sort -b | 並び替える際に行頭に空白があっても無視する |
| sort -n | 数値を文字でなく数字として並び替える |
| sort -i | 大文字/小文字を区別せずに並び替える |
| sort -t <デリミタ> | 指定した文字を区切り文字とする |
| sort -k n | n 番目をソート対象にしてにして並び替える |
| sort -k n1 n2 | n1 番目と n2 番目をソート対象にして並び替える |

```text:例
[ec2-user@localhost ~]$ echo -e "orange\napple\ngrape" | sort
apple
grape
orange
```

##### uniq コマンド

ソートされた文字列から、重複行を 1 つにして出力するコマンド

| コマンド例 | 出力結果 |
|:-|:-|
| uniq -i | 大文字、小文字を区別しない |
| uniq -d | 重複している行のみを取り出す |
| uniq -u | 重複していない行のみを取り出す |

```text:例
[ec2-user@localhost ~]$ cat file.txt
A
A
B
C
C
[ec2-user@localhost ~]$ uniq file.txt
A
B
C
```

##### join コマンド

2 つのファイルから列の値が共通する要素に対して結合する

| コマンド例 | 出力結果 |
|:-|:-|
| join -1 〇〇 -2 ×× ファイル1 ファイル2 | ファイル 1 の 〇〇 列とファイル 2 の ×× 列でつなげる |
| join -t | カンマ区切りの文字に対して結合する |
| join -a 1 | 共通する要素とともにファイル 1 に存在しファイル 2 に存在しない列を表示する |
| join -v 1 | ファイル 1 に存在しファイル 2 に存在しない列のみを表示する |

```text:例
[ec2-user@localhost ~]$ cat file1.txt
1 APPLE
2 GRAPE
[ec2-user@localhost ~]$ cat file2.txt
1 100円
2 200円
[ec2-user@localhost ~]$ join file1.txt file2.txt
1 APPLE 100円
2 GRAPE 200円
```

##### grep コマンド

ファイルや標準出力から、特定の文字を含んだ行だけ取り出す

| オプション | 意味 |
|:-|:-|
| -v 〇〇 | 〇〇 を含まない行だけ取り出す |
| -n | 行数も一緒に表示する |
| -i | 文字列の大文字と小文字を区別しない |

```text:例（ T を含んだ行だけ取り出す ）
[ec2-user@localhost ~]$ cat name.txt
Sato
Tabuchi
Tanaka
Ueda
Utsumi
[ec2-user@localhost ~]$ grep T name.txt
Tabuchi
Tanaka
```

##### awk コマンド

ファイルや標準出力から、行ごとに何らかの処理を施したい場合に用いる

※ awk コマンドの内部は c 言語で動いているため、bash 等で処理させるよりも高速に結果を得ることができる

詳細は [とほほのAWK入門](https://www.tohoho-web.com/ex/awk.html) 参照。

| コマンド例 | 出力結果 |
|:-|:-|
| awk '{print}' file_name | ファイルの中身をそのまま表示する |
| awk '/〇〇/ {print}' file_name | ファイルから〇〇を含んでいる行を取り出す |
| awk '{print \\$1, \\$4}' file_name | ファイルから空白区切りで 1 行目と 4 行目を取り出す |
| awk -F | 区切り文字を指定する |

| 特殊文字 | 意味 |
|:-|:-|
| $0 | 行に存在する文字全て |
| NR | 行番号 |
| NF | 現在いる行のフィールド数 |
| FS | 各フィールドを分離する区切り文字（デフォルトは空白） |
| BEGIN{} | awk 実行前に実行される前処理 |
| END{} | awk 実行後に実行される後処理 |

| 変数 | 意味 |
|:-|:-|
| var=〇〇 | 変数を宣言して値を代入する |
| foo[1] | 変数名[添え字]で配列として扱う |

| 構文 | 意味|
|:-|:-|
| print文 | 文を出力する |
| if文 | 特定の条件のものを出力する |
| for文 | ループして処理を実行する |

```text:例1（ employee.txt の中身 ）
[ec2-user@localhost ~]$ cat employee.txt
ayaka manager account 45000
shinji clerk account 25000
mika manager sales 50000
atsushi manager account 47000
takuro clerk sales 15000
john clerk sales 23000
```

```text:例2（ 行番号、1列目、3列目で取り出し ）
[ec2-user@localhost ~]$ awk '{print NR, $1, $3}' employee.txt
1 ayaka account
2 shinji account
3 mika sales
4 atsushi account
5 takuro sales
6 john sales
```

```text:例3（ 2行目がclerkだけ ）
[ec2-user@localhost ~]$ awk 'if($2=="clerk") print NR,$1,$3' employee.txt
2 shinji account
5 takuro sales
6 john sales
```

```text:例4（ 行の長さが25より大きい ）
[ec2-user@localhost ~]$ awk 'if(length($0)>25) print NR, $0' employee.txt
1 ayaka manager account 45000
2 shinji clerk account 25000
4 atsushi manager account 47000
```

```text:例5（ 2行目がmanagerの行の数を取得 ）
[ec2-user@localhost ~]$ awk 'BEGIN{count=0} {if($2=="manager") count++} END{print count}' employee.txt
3
```

```text:例6（ 行の長さの最大値を取得 ）
[ec2-user@localhost ~]$ awk 'BEGIN{max=0} {if(length($0)>max) max=length($0)} END{print max}' employee.txt
29
```

```text:例7（ 給料の合計を計算 ）
[ec2-user@localhost ~]$ awk '
> {salaries[$2]+=$4}
> END{for(job in salaries){print "給料の合計: " job ": " salaries[job]}}
> ' employee.txt
給料の合計: manager: 142000
給料の合計: clerk: 63000
```

```text:例7（ BEGINでprintだけ実行 ）
[ec2-user@localhost ~]$ awk 'BEGIN{for(i=-60;i<=0;i++) print i}'
-60
-59
-58
・・・
1
0
```

##### xargs コマンド

標準出力から |（パイプ）で受け取った値をコマンドの引数として用いる

| オプション | 意味 |
|:-|:-|
| -t | 実際にどういうコマンドが事項されるか画面上に表示する |
| -p | そのコマンドを実行するかどうか尋ねられる |
| -I 〇〇 | 〇〇 を変数として、標準出力を渡す |

```text:例
[ec2-user@localhost ~]$ ls
[ec2-user@localhost ~]$ echo 'One Two Three' | xargs touch
[ec2-user@localhost ~]$ ls
One Three Two
```

```bash:例（ date の実行結果が touch のオプション -d にセットされる ）
date '+%Y/%m/%d' -d '5 day ago' | xargs -I % touch -d '%' file.txt
```

##### head コマンド

ファイル、標準出力の行頭から何行か指定した行数分取得する

| コマンド例 | 出力結果 |
|:-|:-|
| head -n 〇〇 ファイル | ファイルの行頭から 〇〇 行表示する |
| head -c 〇〇 ファイル | ファイルの先頭から 〇〇 バイト数取得 |

```text:例
[ec2-user@localhost ~]$ head -n 2 file1.txt
1 APPLE
2 GRAPE
```

##### tail コマンド

ファイル、標準出力の行末から何行か指定した行数分取得する

| コマンド例 | 出力結果 |
|:-|:-|
| tail -n 〇〇 ファイル | ファイルの行末から 〇〇 行表示する |
| tail -c 〇〇 ファイル | ファイルの末尾から 〇〇 バイト数取得 |
| tail -F ファイル | ファイルの行末に追加されるテキストをリアルタイムに表示する |

```text:例
[ec2-user@localhost ~]$ tail -n 3 file1.txt
8
9
10
[ec2-user@localhost ~]$ tail -F file.txt
```

##### history コマンド

ログインユーザの実行したコマンドの履歴を表示する

**/home/user/.bash_history** : ユーザの実行した履歴が書き込まれているファイル（ログアウトするとそのセッションで実行したコマンド履歴が書き込まれる）

**HISTSIZE** : 保存できる履歴のサイズを設定する環境変数

| コマンド例 | 出力結果 |
|:-|:-|
| history [件数] | 指定した件数分最新の履歴を取得する |
| history -c | 現在接続中のセッションの履歴を全て削除する |
| history -d <行番号> | 行番号に対応した履歴を削除する |
| history -s <文字列> | 文字列を履歴に追加する |

```text:例
[ec2-user@localhost ~]$ history
：
986 vim file1.txt
987 tail -n 3 file1.txt
988 history
```

##### wc コマンド

標準出力やファイルのサイズ、単語数、行数などの情報を取得する

| コマンド例 | 出力結果 |
|:-|:-|
| wc -l | ファイルの行数を表示 |
| wc -c | ファイルの文字数を表示 |
| wc -w | ファイルの単語数を表示 |

```text:例（ 6行、単語数5、33バイト ）
[ec2-user@localhost ~]$ wc name.txt
6 5 33 name.txt
```

##### split コマンド

ファイルを特定のサイズで分割する

| コマンド例 | 出力結果 |
|:-|:-|
| split -b | バイト数を指定してファイルを分割 |
| split -l | 行数を指定してファイルを分割 |

```text:例（ file.txtからファイル分割 ）
[ec2-user@localhost ~]$ split -b 100 file.txt
[ec2-user@localhost ~]$ ls
file.txt xaa xab xac xad xae
[ec2-user@localhost ~]$ cat x* > recovered_file.txt
```

```text:例（ ファイルを復元する ）
[ec2-user@localhost ~]$ cat x* > recovered_file.txt
```

##### find コマンド

指定した条件に該当するファイル、ディレクトリを見つける

`find 検索するパス -オプション ""` として実行

| コマンド例 | 出力結果 |
|:-|:-|
| find -name | 指定した文字列でファイル、ディレクトリを検索 |
| find -atime 〇〇 | 最終アクセス日時を利用して検索（-atime -3: 3日前までアクセスしたファイル） |
| find -mtime 〇〇 | 最終更新日時を利用して検索 |
| find -size | ファイルサイズで検索（-size -100k: 100kBよりも小さいサイズ） |
| find -user | ファイルの所有者で検索 |
| find -type | ファイルのタイプ（f: ファイル、d: ディレクトリ、l: シンボリックリンク）で検索 |

```text:例（ fileで始まる名前のファイルを検索 ）
[ec2-user@localhost ~]$ find ./ -name "file*"
./a/file
./a/file1
./file.txt
```

##### getopts コマンド

指定した形式の引数を取得するコマンド

while 文と getopts でオプションの引数を取得し、case 文でシェル変数に値をセットする

```bash:例
# 以下のような形式でシェルスクリプトを実行する
# ./test.sh -u Taro -a 21 -f TaroYamada

# 実装例
while getopts "u:a:f:" opt
do
    case "${opt}" in
        u) username=${OPTARG};;
        a) age=${OPTARG};;
        f) fullname=${OPTARG};;
    esac
done
echo "Username: $username"
echo "Age: $age"
echo "Full Name: $fullname"
```



## おわりに

思ったより内容量が多くなりました。
split コマンドで記事も分割できないかなあ。