Bashシェルスクリプトの書き方（基本）のおさらいメモ

# 概要

タイトルの通りBashシェルスクリプトの書き方のおさらいメモです。
動作確認にDocker Desktop for Windowsと[ubuntu official image] (https://hub.docker.com/_/ubuntu)を利用しました。

**環境**

* Windows 10 Professional
* Docker Desktop for Windows 2.0.0.3 (31259)
    * Ubuntu 18.04.2

**参考**

* [Bash Reference Manual] (https://www.gnu.org/software/bash/manual/bash.html)
* [Bash Scripting Best Practices] (https://sap1ens.com/blog/2017/07/01/bash-scripting-best-practices/)
* [Best Practices for Writing Bash Scripts] (https://kvz.io/blog/2013/11/21/bash-best-practices/)

**バージョン**

ubuntuのバージョン

```terminal
# cat /etc/os-release
NAME="Ubuntu"
VERSION="18.04.2 LTS (Bionic Beaver)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 18.04.2 LTS"
VERSION_ID="18.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=bionic
UBUNTU_CODENAME=bionic
```

bashのバージョン

```terminal
# echo $BASH_VERSION
4.4.19(1)-release
```

# シェル構文 (Shell Syntax)

## クォート (Quoting)

### シングルクォート (Single Quotes)

シングルクォート `'...'` で囲まれた文字は、パラメータ展開やコマンド置換は行われません。

```bash
$ message="World"
$ echo '$Hello ${message}!'
Hello ${message}!
$ echo '$(date "+%Y/%m/%d")'
$(date "+%Y/%m/%d")
```

シングルクォートで囲まれた文字にシングルクォートを含めることはできません。後述のエスケープキャラクタでエスケープすることもできません。

```bash:NG
$ echo 'in 80's'
```

### ダブルクォート (Double Quotes)

ダブルクォート `"..."` で囲まれた文字は、パラメータ展開やコマンド置換が行われます。
パラメータ展開やコマンド置換をしたくない場合は後述のエスケープキャラクタを使います。

```bash
$ message="World"
$ echo "Hello ${message}!"
Hello World
$ echo "$(date "+%Y/%m/%d")"
2019/04/28
```

### Escape Character

バックスラッシュ `\` に続く1文字を文字の通り解釈します。

```bash
$ echo "Hello \${message}!"
Hello ${message}!
$ echo "\$(date "+%Y/%m/%d")"
$(date "+%Y/%m/%d")
```

# シェルコマンド (Shell Commands)

## ループ (Looping Constructs)

### whileループ

`while`はtest-commandの終了ステータスが1(為)である限りconsequent-commandsを繰り返し実行します。

```text:syntax
while test-commands; do consequent-commands; done
```

例

```bash
i=0; while [ $i -lt 5 ]; do echo $i; ((i++)); done
```

```bash
i=0
while [ $i -lt 5 ]
do
  echo $i
  ((i++))
done
```

条件式の評価の部分 `[ $i -lt 5 ]`は、`test [ $i -lt 5 ]` や `test $i -lt 5` という書き方もあります。

#### デモ) ファイルを読み込む

その1) 半角スペース区切り

```text:inpt.txt
1 2
2 3
3 4
4 5
5 6
```

```bash
while read c1 c2
do
  echo "$c1 + $c2 = $((c1 + c2))"
done < input.txt
```

その2) カンマ区切り

```text:inpt.txt
1, 2
2, 3
3, 4
4, 5
5, 6
```

IFSで区切り文字を指定します。IFSはシェル変数で文字列を分割する区切り文字が設定されています。デフォルトは半角スペース、タブ、改行になります。

```bash
while IFS=, read c1 c2
do
  echo "$c1 + $c2 = $((c1 + c2))"
done < input.txt
```

### forループ

wordsを展開しそれぞれの値に対してcommandsを実行します。値はnameにバインドされます。

```text:syntax
for name [ [in [words …] ] ; ] do commands; done
```

例

```bash
for i in 0 1 2 3 4; do echo $i; done
```

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

forループには次のような算術式を使った書き方もあります。

```text:syntax
for (( expr1 ; expr2 ; expr3 )) ; do commands ; done
```

例

```bash
for (( i=0; i<5; i++ )); do echo $i; done
```

```bash
for (( i=0; i<5; i++ ))
do
  echo $i
done
```

#### デモ) ディレクトリの一覧

```bash
$ for dir in `find $(pwd) -mindepth 1 -type d`; do echo $dir; done
```

## 条件 (Conditional Constructs)

### if

test-commandsの終了ステータスが0(真)の場合、consequent-commandsが実行されます。1(為)でelifが後続する場合はmore-test-commandsが実行され終了ステータスを判定します。

```text:syntax
if test-commands; then
  consequent-commands;
[elif more-test-commands; then
  more-consequents;]
[else alternate-consequents;]
fi
```

例)

`test`コマンドを使う書き方。

```bash
$ if test $var = "A"; then echo "var is A"; else echo "var isn't A"; fi
```

`test`はコマンドなので単体で使用できます。

```bash
$ a="ABC"
$ b="ABC"
$ c="abc"
$ test $a = $b; echo $?
0
$ test $a = $c; echo $?
1
```

`test`コマンドの別名である`[ ... ]`を使う書き方もあります。

```bash
$ if [ $var = "A" ]; then echo "var is A"; else echo "var isn't A"; fi
```

#### デモ）ファイル・ディレクトリの判定

ファイル・ディレクトリの存在チェック

```bash
if [ -e input.txt ]; then echo "file is exists"; else echo "file is not exists"; fi
```

- ファイルの存在チェックは`-f`
- ディレクトリの存在チェックは`-d`
- シンボリックリンクの存在チェックは`-L`

ディレクトリのシンボリックリンク

```bash
if [ -d dir1 -a -L dir1 ]; then echo "dir is symbolic link"; else echo "dir is not symbolic link"; fi
```

### case

wordに一致する最初のパターンのcommand-listを実行します。同じcommand-listを実行するパターンが複数ある場合は`|`で区切って指定します。

```text:syntax
case word in
    [ [(] pattern [| pattern]…) command-list ;;]…
esac
```

例)

```bash
var=9
case $var in
    [0-9])
      echo "var is numeric"
      ;;
    [a-z])
      echo "var is lower case"
      ;;
    [A-Z])
      echo "var is upper case"
     　;;
    +|-|/|%)
      echo "var is symbol"
      ;;
    *)
      echo "default"
      ;;
esac
```

一般的な慣習として、デフォルトケースは一番最後に`*`として定義します。

> It’s a common idiom to use ‘*’ as the final pattern to define the default case, since that pattern will always match.

### `(( 算術式 ))`

`(( 算術式 ))`は、算術式を評価し終了ステータスを返します。終了ステータスは、式の結果が0以外のときは0(真)、それ以外のときは1(為)になります。


```bash
$ i=5; while ((i--)); do echo $i; done
```

### `[[ 条件式 ]]`

`[[ 条件式 ]]`は、条件式を評価し終了ステータスを返します。終了ステータスは、評価が真(true)のときは0(真)、為(false)のときは1(為)になります。

```bash
$ f="apple"; if [[ $f == [aA]pple ]]; then echo "match"; else echo "unmatch"; fi
match
```

# シェルパラメータ (Shell Parameters)

## 変数の割り当て

変数の割り当ては`変数=値`で行います。値を定義しなかった場合はnull文字が設定されます。
`${#変数}`と書くと文字列の長さを取得できます。

```bash
$ message="Hello World"
$ echo "$message"
Hello World
$ echo "${#message}"
11
```

```bash
$ message=
$ echo "$message"

$ echo "${#message}"
0
```

### 変数の属性 (Attributes )

`declare`はビルトインコマンドで、変数の定義と同時に属性を設定することができます。
`-r`オプションは変数を読み取り専用にします。

```bash
$ declare -r message="Hello Wolrd"
$ message=""
bash: message: readonly variable
```

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

```bash
$ readonly message="Hello World"
```

変数の定義と読み取り専用化は別々に行うこともできます。

```bash
$ message="Hello World"
$ readonly message
```

#### 属性の確認

`-p`オプションを指定すると属性と値を表示します。

```bash
$ declare -p message
declare -r message="Hello World"
```

#### 数値

`-i`オプションを指定すると数値(intger)として定義されます。
数値として定義すると後述する算術式を使わなくても数値計算が行えます。

```bash
$ declare -i num=0
$ num=num+10
$ echo $num
10
```

数値の場合、`${#変数}`は桁数を返します。

```bash
$ declare -i num=1000
$ echo ${#num}
4
```

#### 配列

`-a`オプションを指定すると配列として定義されます。
添え字に`@`を指定すると配列全体を参照します。

```bash
$ declare -a array=(a b c)
$ echo "${array[@]}"
a b c
$ echo "${#array[@]}"
3
$ array[0]=A
$ echo "${array[@]}"
A b c
```

配列をfor文で走査する場合は次のように記述します。

```bash
for i in ${array[@]}
do
  echo "$i"
done
```

添え字を使いたい場合は次のようにfor文を記述します。
添え字の変数は先頭に`$`を付けなくても構いません（付けても動きます）。

```bash
for ((i = 0; i < ${#array[@]}; i++)) {
  array[i]=${array[i]^^}
  echo "$i=${array[i]}"
}
```

# シェル展開 (Shell Expansions)

## ブレース展開 (Brace Expansion)

```bash
$ echo a{1,2,3}
a1 a2 a3
```

ダブルクォーテーション内のブレース展開はされません。

```bash
$ echo "a{1,2,3}"
a{1,2,3}
```

### シーケンス式 (sequence expression)

シーケンス式 `{x..y}` を使って連番を生成できます。

```bash
for i in {1..10}; do echo $i; done
```

`{x..y..incr}`という書き方で増分を指定できます。

```bash
for i in {1..10..3}; do echo $i; done
```

数値以外に文字も指定できます。

```bash
for i in {a..f}; do echo $i; done
```

## シェルパラメータ展開 (Shell Parameter Expansion)

### 基本形式 (The basic form)

parameterの値が展開されます。

```text
${parameter}
```

### `${paramter:-word}`

変数 `parameter` が未定義かnull文字の場合、代わりに `word` が展開されます。 

```bash
$ now=
$ echo ${now:-$(date)}
Sun Apr 28 16:27:40 UTC 2019
```

### `${parameter:=word}`

変数 `parameter` が未定義かnull文字の場合、代わりに `word` が展開され`parameter`に代入されます。

```bash
$ now=
$ echo ${now:=$(date)}
Sun Apr 28 16:27:40 UTC 2019
$ echo $now
Sun Apr 28 16:27:40 UTC 2019
```

### `${parameter:?word}`

変数 `parameter` が未定義かnull文字の場合、`word`の展開された結果が標準エラーに出力されます。`word`が未定義の場合は、代わりのメッセージが出力されます。

```bash
$ now=
$ echo ${now:?}
bash: now: parameter null or not set
```

```bash
$ now=
$ echo ${now:?"is null"}
bash: now: is null
```

### `${parameter:+word}`

変数 `parameter` が定義されている（未定義かnull文字以外）場合、`word` が展開されます。それ以外の場合は空文字が展開されます。

```bash
$ now=$(date "+%Y/%m/%d")
$ echo ${now:+"today is $now"}
today is 2019/04/30
```

### 部分文字列展開 (Substring Expansion)

#### `${parameter:offset}`

```bash
$ var=0123456789abc
$ echo ${var:5}
56789abc
```

#### `${parameter:offset:length}`

```bash
$ var=0123456789abc
$ echo ${var:5:3}
567
```

### 文字数 `${#parameter}`

`parameter`の文字数を展開します。

```bash
$ var=20190429
$ echo ${#var}
8
```

### パターンに一致する部分を削除

parameterからwordに一致する部分を削除します。

#### `${parameter#word}`

`parameter`の左側から`word`で指定するパターンに最短一致する部分を削除して展開します。

#### `${parameter##word}`

`parameter`の左側から`word`で指定するパターンに最長一致する部分を削除して展開します。

```bash
$ var=123123123abc456456456
$ echo ${var#12*3}
123123abc456456456
$ echo ${var##12*3}
abc456456456
```

#### `${parameter%word}`

`parameter`の右側から`word`で指定するパターンに最短一致する部分を削除して展開します。

#### `${parameter%%word}`

`parameter`の右側から`word`で指定するパターンに最長一致する部分を削除して展開します。

```bash
$ var=123123123abc456456456
$ echo ${var%45*6}
123123123abc456456
$ echo ${var%%45*6}
123123123abc
```

### パターンに一致する部分を置換

parameterからpatternに一致する部分をstringで置換します。

#### `${parameter/pattern/string}`

最短一致

```bash
$ var=123123123abc456456456
$ echo ${var/[[:digit::]]/*}
*23123123abc456456456
```

#### `${parameter//pattern/string}`

最長一致

```bash
$ var=123123123abc456456456
$ echo ${var//[[:digit::]]/*}
*********abc*********
```

## コマンド置換 (Command Substitution)

### バッククォート

従来からある記法です。

```bash
$ NOW=`date "+%Y/%m/%d %H:%M:%S"`
2019/04/28 05:29:56
```

### `$( )`

新しい記法です。

```bash
$ NOW=$(date "+%Y/%m/%d %H:%M:%S")
2019/04/28 05:31:27
```

## 算術展開 (Arithmetic Expansion)

`$(( 算術式 ))`

```bash
$ x=10
$ y=5
$ echo $x $y
10 5
$ echo "x + y = $(( x + y ))"
x + y = 15
```

# Bash組み込みコマンド (Bash Builtin Commands)

**echo**

- オプション `-n` : 改行(newline)が抑制されます。
- オプション `-e` : エスケープキャラクタが有効になります。

```text:syntax
echo [-neE] [arg …]
```

```bash
$ echo -n "abc"; echo "def"
abcdef
```

```bash
$ echo "Hello\tWolrd"
Hello\tWolrd
$ echo -e "Hello\tWolrd"
Hello   Wolrd
```

**printf**

- オプション `-v var` : 出力結果を変数varに格納します。

```text:syntax
printf [-v var] format [arguments]
```

```bash
echo "+-----+-+-+-+----------------------------------------------------+"
echo "| no  |f|d|s|file                                                |"
echo "+-----+-+-+-+----------------------------------------------------+"

declare -i i=1
for name in $(find /etc)
do
  f=""
  if [ -f "$name" ]; then
    f="o"
  fi
  d=""
  if [ -d "$name" ]; then
    d="o"
  fi
  l=""
  if [ -L "$name" ]; then
    l="o"
  fi
  printf "| %3d |%s|%s|%s| %-50s |\n" $i ${f:-" "} ${d:-" "} ${l:-" "} $name
  ((i++))
done

echo "+-----+-+-+-+----------------------------------------------------+"
```

**source**

filenameで指定したシェルスクリプトを読み込みます。`.`はsourceの別名です。

```text:syntax
source filename
```

# 補足

## vim

**Ubuntuにvimをインストールする**

```bash
# apt-get update
# apt-get install -y vim
```

**vimのカラースキーマをセットする**
使えるカラースキーマを確認

```bash
# ls -1 /usr/share/vim/vim*/colors/
README.txt
blue.vim
darkblue.vim
default.vim
delek.vim
desert.vim
elflord.vim
evening.vim
industry.vim
koehler.vim
morning.vim
murphy.vim
pablo.vim
peachpuff.vim
ron.vim
shine.vim
slate.vim
torte.vim
zellner.vim
```

.vimrcに下記の行を追加
colorschemeに使用するカラースキーマ名を設定

```text:.vimrc
syntax on
colorscheme blue
```

![blue.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22772/9144b620-55e0-a04a-5166-bdbd504eac83.png)

ちなみに"default"
![default.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22772/2db14e87-7cae-9fa8-be4d-a5da56fe995c.png)

"evening"
![evening.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22772/cbb85e05-0530-6740-6eeb-2afe649f3dbf.png)

カラースキーマは[Vim Colors] (https://vimcolors.com/)で探せます。