シェル&シェルスクリプト チートシート

- ここに書いてあることは基本的にPOSIX compliant（のはず）
- POSIX compliantじゃないことはその旨記載

# 変数の参照

https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_06_02


# 忘れがちなcase文

```sh
$var="YES"
case $var in
  "Y" | "y" | "yes" | "Yes" | "YES" )
    echo "YES!"
    ;;  
  * ) 
    echo "NO!"
    ;;  
esac
```

# 引数の処理

例:

```sh
while getopts f:h OPT; do
  case $OPT in
    "f" ) FLG_f="TRUE"; VALUE_f=${OPTARG} ;;
    "h" ) echo "使い方: ${CMDNAME} [-f] LOGDIR"
          echo "        ${CMDNAME} [-h]"
          echo "    LOGDIR     ログが保存されているディレクトリのパス"
          echo "    -h          このヘルプを表示する"
          exit 0 ;;
  esac
done
shift `expr $OPTIND - 1`

log_dir="${1}"
if [ "${log_dir}" = "" ]; then
  error '第1引数にログディレクトリのパスを指定してください'
fi
```

# 文字に色をつけたい

色をつける: `\e[文字色2桁;背景色2桁m`
戻す: `\e[m`

```sh
printf '\e[31mRED\e[m\n'    # 赤
printf '\e[32mGREEN\e[m\n'  # 緑
printf '\e[33mYELLOW\e[m\n' # 黄色
printf '\e[34mBLUE\e[m\n'   # 青
printf 'NORMAL\n'           # 元の色
```

# ファイルから環境変数を読み込んで設定したい

`source`を使うのではなく、単に`NAME=val`の形式で環境変数の一覧が書かれているファイルから読み込んで環境変数を設定する方法。
パイプを使って `cat ENV.txt | ...` とかやろうとすると[パイプはサブプロセスで実行されてしまう](https://fumiyas.github.io/2013/12/10/lastpipe.sh-advent-calendar.html)ので、`for`でファイルを読み込む。

```shell-session
$ cat ENV.txt
URL=http://example.com/
$ for env in `cat ENV.txt`; do export "${env}"; done; sh -c 'echo ${URL}'
http://example.com/
```

# エスケープシーケンスを楽に扱いたい

`tput`を使う。（POSIX準拠コマンドではない）

from: https://qiita.com/onokatio/items/5d282b72ac5565ae4569


# ランダムな文字列を得たい

from: https://qiita.com/yasuo-ozu/items/b57af1ffcb867347bab9

- `/dev/urandom`自体はPOSIXでは規定されてないっぽいです

## 数字だけ

```sh
cat /dev/urandom | LC_CTYPE=C tr -dc '0-9' | fold -w 32 | head -n 1
```

## 0~7の範囲の数字を1文字

```sh
cat /dev/urandom | LC_CTYPE=C tr -dc '0-7' | fold -w 1 | head -n 1
```

## 数字とアルファベット

```sh
cat /dev/urandom | LC_CTYPE=C tr -dc 'a-z0-9' | fold -w 32 | head -n 1
```


# 文字列を1文字ずつに分解したい

```sh
echo "aaaa" | fold -w 1 | tr '\n' ' ' | sed 's/ $//g'
```

# 簡単なエラーハンドリング

```sh
error() {
  printf '\e[31mエラー: %s\n終了します\e[m\n' "${1}" 1>&2
  exit 1
}
```

```sh:使い方
error '不明なエラー'
```

こういうのもいいかも

```sh
CMDNAME=`basename $0`

error() {
  printf '\e[31m[%s] エラー: %s\e[m\n' "${CMDNAME}" "${1}" 1>&2
  printf '\e[31m[%s] 終了します\e[m\n' "${CMDNAME}" 1>&2
  exit 1
}
```

```sh
CMDNAME=$(basename $0)
error() {
  printf '\e[31m%s: エラー: %s\e[m\n' "${CMDNAME}" "${1}" 1>&2
  printf '\e[31m%s: 終了します\e[m\n' "${CMDNAME}" 1>&2
  exit 1
}
```

# set -eu するときのエラーハンドリング

```sh
#!/bin/sh

set -eu

error_handler() {
  # エラー時の処理
}

cmdname=$(basename "${0}")
error() {
  printf '\e[31m%s: エラー: %s\e[m\n' "${cmdname}" "${1}" 1>&2
  printf '\e[31m%s: 終了します\e[m\n' "${cmdname}" 1>&2
  exit 1
}

trap error_handler EXIT

# ここで通常の処理

# ここで通常の終了処理

# 異常終了時ハンドラの解除
trap '' EXIT
```

# root権限で実行しているかどうかチェックする

```sh
if [ "$(id -u)" != "0" ]; then
  echo "エラー: root権限が必要です" 1>&2
  exit 1
fi
```


# スクリプト自身のファイル名（コマンド名）を取得する

```sh
CMDNAME=$(basename $0)
```

# 出力1行ごとにタイムスタンプをつける

```sh:timestamp.sh
#!/bin/sh

while read -r l; do
  printf '%s %s\n' "`date \"+%Y-%m-%dT%H:%M:%S%z\"`" "${l}"
done
```

```shell-session
some_command | ./timestamp.sh
```


# ファイルに書き込み権限があるかどうかチェックする

`test`コマンドで普通にできる

```sh
test -w path/to/file
```

または

```sh
[ -w path/to/file ]
```


# スクリプトのパス（スクリプトのあるディレクトリ）の取得

```sh
dirname $0
```


# 標準出力/標準入力を流れているデータの速度を見たい

`pv`コマンドを使う

参考: https://qiita.com/kitsuyui/items/549c8e786e7d456e0923


# `while read`で標準入力から読み込んでいる最中に端末からの入力を受け付けたい

```sh
read input < /dev/tty
```

https://stackoverflow.com/questions/6883363/read-input-in-bash-inside-a-while-loop


# 1行を更新しつづけたい

```sh
while :; do
  date +%s
  sleep 1
  printf '\e[1F\e[2K'
done
```

https://qiita.com/PruneMazui/items/8a023347772620025ad6


# psの結果を毎秒見たい

- dateとpsの結果を受け取る
- 画面をクリアする
- dateとpsの結果を表示する

```sh
while :; do date="$(date)"; ps="$(ps -A -o pid,command | grep ffmpeg)"; clear; printf '%s\n----------------\n%s\n' "${date}" "${ps}"; sleep 1; done
```


# カウントアップタイマー

```sh
st="$(date +%s)"; while :; do ct="$(date +%s)"; printf '%d' "$((ct-st))"; sleep 1; clear; done
```


# [非POSIX] 出力結果をテーブル表示したい

```sh
column -t -s ' '
```

例:
<img width="1175" alt="スクリーンショット 2019-12-24 3.45.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26038/c1b0e724-a0bb-d057-a918-8552684d4968.png">


# xargsを入れ子にしたい

xargsのパラメータには1コマンドしか渡せないっぽいので、`sh -c 'command'`で入れ子にしたいコマンドを実行する。

```sh
ls | xargs -IXXX sh -c 'ls | xargs -IYYY mv XXX/YYY XXX/dst'
```


# 現在時刻を`yyyymmdd HHMMSS`で取得したい

西暦4桁、時分秒を2桁で取得したい。

```sh
date "+%Y-%m-%d% %H:%M:%S"
```


# ファイルのN行目以降を出力したい

例えば、`file`の2行目以降を出力したい場合は、

```sh
tail -n +2 < file
```


# 月の最終日を求めたい

2020年の5月の最終日を求めるには、

```sh
cal 05 2020 | grep -v '^ *$' | tail -n 1 | sed 's/  *$//g' | sed 's/^.*\(..\)$/\1/'
```

# 文字列テンプレート

- evalは普通に怖い
- `fuga=" date; "`みたいな単純なインジェクションは大丈夫そうだけど、テンプレートにはコマンド書き放題だし、大丈夫なのかよくわからん
- どうしても個別のファイルにしたいわけでなければ普通にヒアドキュメントを使ったほうがよさそう

```shell-session
$ printf 'あの${hoge}の${fuga}風\n' > template.txt 
$ hoge="イーハトーヴォ"; fuga="すきとおった"
$ eval "echo $(cat template.txt)"
あのイーハトーヴォのすきとおった風
$ hoge="環七沿い"; fuga="排気ガス混じりの"
$ eval "echo $(cat template.txt)"
あの環七沿いの排気ガス混じりの風
```

- テンプレートエンジンを使いたいなら、mustacheのC実装である[mustach](https://gitlab.com/jobol/mustach)がよさそう


# [非POSIX] 現在時刻と24時間後の時刻を表示する

`utconv`自体はPOSIX shellがあれば動くぞ

```sh
now_t=$(date +%s); tomo_t=$((now_t + 86400)); printf 'now: %s %s\ntomorrow: %s %s\n' "${now_t}" "$(utconv -r ${now_t})" "${tomo_t}" "$(utconv -r ${tomo_t})"
```


# [非POSIX] シェルスクリプトをステップ実行したい

そんな欲求が出てきた時点で行数を減らすことを考えたほうがいいのですが、`bashdb`を使うとステップ実行ができる。
`brew install`する場合は、手動でsymlinkをつくってやらないといけなかった。
あと、bashの`--posix`がどの程度bourne shellと互換性あるのかはよくわからない。

```sh
brew install bashdb
mkdir /usr/local/Cellar/bash/5.0.18/share/bashdb   # ここはバージョンによって変わるので注意
ln -s /usr/local/Cellar/bashdb/5.0-1.1.2/share/bashdb/bashdb-main.inc /usr/local/Cellar/bash/5.0.18/share/bashdb/bashdb-main.inc   # ここはバージョンによって変わるので注意
bash --posix --debugger hello.sh 
```