Bashメモ

Bashはググりづらいので使い方をメモしておく。

# 変数参照

```bash
$(コマンド)             `` と同じ。
$((式))                 中を式として計算

${name:-value}          未定義または空欄なら "value" を返す
${name:=value}          未定義または空欄なら "value" を返し、name を "value" で書き換える
${name:?必須エラー}     未定義または空欄なら「必須エラー」をエラーメッセージとして出力

${name-value}           未定義なら "value" を返す
${name=value}           未定義なら "value" を返し、name を "value" で書き換える
${name?必須エラー}      未定義なら「必須エラー」をエラーメッセージとして出力

${name:2:3}             2バイト目(0スタート)から3バイト

a=(a b c)               配列
${a[0]}                 配列の要素
${a[@]}                 配列のすべての要素

declare -A my_dict      連想配列
my_dict["key1"]="value1"
my_dict["key2"]="value2"
${my_dict["key2"]}      連想配列の要素
${my_dict[@]}          連想配列のすべての要素
${!my_dict[@]}          連想配列のすべてのキー

${変数名#パターン}      前方一致でのマッチ部分削除(最短マッチ)
${変数名##パターン}     前方一致でのマッチ部分削除(最長マッチ)
${変数名%パターン}      後方一致でのマッチ部分削除(最短マッチ)
${変数名%%パターン}     後方一致でのマッチ部分削除(最長マッチ)
${変数名/置換前文字列/置換後文字列}     文字列置換(最初にマッチしたもののみ)
${変数名//置換前文字列/置換後文字列}    文字列置換(マッチしたものすべて)

var="/my/path/dir/test.dat"
echo ${var##*/}         # → test.dat       ※ファイル名だけ取り出し
echo ${var%/*}          # → /my/path/dir   ※ディレクトリだけ取り出し
echo ${var##*.}         # → dat            ※拡張子だけ取り出し
```


# 特殊な変数

```bash
$#      引数の数
$?      終了ステータス
$$      プロセスID
$0      実行されたシェルスクリプトの実行ファイル名
$*      引数すべてをスペース(IFS)区切りで
$@      引数すべてをスペース(IFS関係なく)区切りで
```


# 関数

```bash
# ./func_hello
hello () {
    local name=value  # 関数内のローカル変数
    echo $1
    return 0          # 0が正常
}

. func_hello            外部シェルの定義読み込み
export -f hello         外部シェルの関数定義をexport

date () {
    command date $1     command で関数を無限ループで呼び出すのを防ぐ
}
# これでdateを乗っ取れる
export -f date
# これで乗っ取り解除
unset date
```


# よく使うお決まりスクリプト

## 実行ファイルの置かれているディレクトリを返す。最後の/は無い。

```bash
script_dir_path=$(dirname $(readlink -f $0))
```

## スクリプトの置かれているディレクトリを返す。最後の/は無い。

source で読んでるファイルの中も読み先のファイルが置かれているディレクトリのパスが取れる

```bash
# 相対パス
script_dir_path="$(dirname "${BASH_SOURCE[0]}")"
# 絶対パス
script_dir_path="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
```

## 相対パスから絶対パスを得る

```bash
ABS_PATH=$(cd ../../some_dir && pwd)
```
- いったん cd で移動してパスを得る。サブシェルなので cd はおおもとに影響しない

## Ctrl+C でもファイルを消す

```bash
trap "
  mv /tmp/swap-file original-file
  rm /tmp/target-file
" EXIT
```

## 一時ファイルを作る

```bash
temp_file=$(mktemp) # /tmp 配下にランダムな名前のファイルが作成される。 例: /tmp/tmp.C3N9Ng6IaU
temp_dir=$(mktemp -d) # -d を付けることでディレクトリ作成となる。 例: /tmp/tmp.wDOVMXVcio
trap "
rm $temp_file
rm $temp_dir
" EXIT
```


# bashオプション

```bash
set -u        未定義の変数を参照したらエラーにする。
set -e        エラーがあったらその時点で中断する。（なお、diff は差分があったらエラー扱い。）

set -eu       -e と -u を同時に指定する場合。
set +e        -e の指定を解除する。

set -o pipefail    パイプの場合でも中間でエラーが発生したら中断する（デフォルトは中断しない）。
```

```bash:-e中にエラーでも強制終了させない
# -e 中だと、grep でヒットしなかったり、
# diff で差分があった場合などエラー扱いされてしまうが
# 強制終了したくない場合がある。

# & true をつけることで強制終了しなくなる。
# これは -e は条件式の場合、最後の句がエラーだった場合に強制終了する仕様だが、
# 差分アリの場合、ショートサーキットで最後(true)が実行されないためである。
# 差分ナシの場合は、true (結局何もしない) が実行される。
grep aaa && true
RES1=&?

grep bbb && true
RES2=&?

# まとめてエラー判定
# 実は exit 1 は無くても同様に動くが、可読性のため。
[ "${RES1}" -eq 0 -a "${RES2}" -eq 0 ] || exit 1
```

## bashオプションによるデバッグ出力

```bash
set -x        デバッグ用。すべての実行コマンドを標準エラーに出力するようになる。
set -v        デバッグ用。実行文が標準エラーに出力されるようになる。

# これをしておくと、-x/v で出力される際にファイル名、行番号、関数名が出力されるようになる。
export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME:+$FUNCNAME(): }'

: ここにコメントが書ける、コンソールに出力される。

: "特殊な記号等を利用するときはクォーテーションで括る(| や & 等)"

: "
こうすれば
複数コメントも書ける。
ちゃんと改行もされて出力される。
"
```

# 分岐/ループ

## if

```bash
word=2
if [ $word -eq 1 ]; then
    echo "if"
elif [ $word -eq 2 ]; then
    echo "elif"
else
    echo "else"
fi
```


## case

```bash
word="b"
case $word in
"a")
    echo "a"
    ;;
"b")
    echo "b"
    ;;
"c")
    echo "c"
    ;;
*)
    echo "d"
esac
```


## while

```bash
i=0
while [ $i -lt 10 ]; do
    echo $i
    i=`expr $i + 1`
done 
```


## for

```bash
for name in a b c d ; do
    echo $name
done 
```

# 条件

## 数値比較(`[ ]`の中で使う)

```bash
-eq　等しい (equal)
-ne　等しくない (not equal)
-lt　小さい (less than)
-le　小さいまたは等しい (less than or equal)
-gt　大きい (greater than)
-ge　大きいまたは等しい (greater than or equal)
```


## 文字列比較(`[ ]`の中で使う)

```bash
文字列              文字列の長さが0より大きければ真
-n 文字列           文字列の長さが0より大きければ真
! 文字列            文字列の長さが0であれば真
-z 文字列           文字列の長さが0であれば真
文字列1 = 文字列2   2つの文字列が等しければ真
文字列1 != 文字列2  2つの文字列が等しくなければ真
```


## 論理(`[ ]`の中で使う)

```bash
-o      OR
-a      AND
! 条件  逆にする

# こちらのほうが見やすいかも
! [ 条件 ] && [ 条件 ]

# もしくは [[ ]] を使う
[[ ! 条件 && ( 条件 || 条件 ) ]]
```

## ファイルチェック(`[ ]`の中で使う)

```bash
-e　ファイルが存在する（ディレクトリを含むどのようなタイプのファイルであっても）
-f　レギュラーファイル（ディレクトリ等を除く）が存在する。
-d　ディレクトリが存在する。
-r　ファイルが存在し、リード権がある。
-s　ファイルが存在し、フィルサイズが０でない。
ファイル1 -nt ファイル2     ファイル1がファイル2より新しければ真
ファイル1 -ot ファイル2     ファイル1がファイル2より古ければ真
```


## 高度な条件(`[[ ]]`の中で使う)

```bash
[[ ]]  単語分割と、パス名展開がこの中は行われない。チルダ展開、パラメータと変数の展開、算術式展開、コマンド置換、 プロセス置換、クォート除去は実行される。

[[ 文字1 < 文字2 ]]               辞書順で比較
[[ 文字1 > 文字2 ]]               （同上）
[[ 文字1 == パターン ]]           パターンマッチング。パターンの一部(全部)を "" で囲んだら、その部分はただの文字列になる。
[[ 文字1 != パターン ]]           （同上）
[[ 文字1 =~ 正規表現 ]]           正規表現(クォートで囲むと動かないので注意)
                                 ${BASH_REMATCH[0]}  正規表現でマッチした全体取得　　　
                                 ${BASH_REMATCH[1]}  正規表現で1つ目の()でマッチした部分取得
[[ ! 条件 && ( 条件 || 条件 ) ]]  のように記述可。&& と || はショートサーキット演算子。
```


# リダイレクト

```bash
> 出力ファイル 2>&1               エラー出力も標準出力に
```

- 注意: `ls` など、出力先が標準出力かどうかで動作が変わるものがあるので注意。

## 複数の出力をまとめる

```bash:同じシェルで実行(変数スコープも共有)
a=1
{
  a=2
  echo "AA";
  cat ファイル名;
} > zzz.txt
echo $a  # 2を出力
```

```bash:サブシェルで実行(変数スコープが中で閉じる)
a=1
(
  a=2
  echo "aaa"
  cat ファイル名;
) > zzz.txt
echo $a  # 1を出力
```

# コマンド

## awk

```bash
# １列目と２列目だけ切り出し
cat ファイル名 | awk '{print $1,$2}'  # 空白区切り
cat ファイル名 | awk '{print $1 $2}'  # 区切りなし

# 行の抽出
cat ファイル名 | awk '/^A/'  # Aで始まる行のみ出力（ {print} はデフォルトなので省略可 ） 
cat ファイル名 | awk '$2~/^A/'  # ２列目がAで始まる行のみ出力

# 区切り記号の変更
cat ファイル名 | awk 'BEGIN{FS=",";OFS=","} {print $1,$3}'
cat ファイル名 | awk 'BEGIN{FS=",";OFS="\t"} {print $1,$3}'

# 変数AAAの行ごとの先頭に # を付ける
echo "$AAA" | awk '{ print "# " $0 }'

# 変数を使って重複IDはスキップ
cat ファイル名 | awk 'BEGIN{FS=",";OFS=","} (!ids[$1]){print $0; ids[$1]=1}'

# 外部から変数渡す
cat ファイル名 | awk --assign a=11 'BEGIN{OFS=","}{print a,$0}'  # 11, が先頭につく

# 出力を文字列連結（空白で）
cat ファイル名 | awk --assign a=11 'BEGIN{OFS=","}{print a "22",$0}'  # 1122, が先頭につく

# ' を出力したい
cat ファイル名 | awk --assign a=11 --assign q="'" 'BEGIN{OFS=","}{print q a q,$1}'  # '11', が先頭につく
```

## cut

```bash
cat ファイル名 | cut -f2  # タブ区切りで２列目だけを取り出し
cat ファイル名 | cut -d, -f2  # カンマ区切りで２列目だけを取り出し

```

## date

```bash
date +'%Y-%m-%d %H:%M:%S'  # 年-月-日 時:分:秒 のフォーマットで現在時刻を出力
```

## echo

```bash
echo -e         エスケープ・コードを使用可能にする
echo -n         最後の改行を出力しない
```

## find

```bash
# 特定の条件でファイル検索して、その中を検索
find ファイルパス -maxdepth 1 -type f -name "aaa-*.txt" -print0 | xargs -0 grep aaa

# 特定の条件でファイル検索して、そのファイルをリネーム
find ファイルパス -maxdepth 1 -type f -name "aaa-*.txt" -print0 | xargs -0 -I {} mv {} {}-moved

# ファイル名のパターンを指定してのループ
for F in $(find . -mindepth 1 -maxdepth 1 -type f -name "パターン" -print0 | xargs -0 -I {} basename {}); do
  echo "[$F]"
done
```

### find と ls の違い

|                    | find                 | ls            |
|--------------------|----------------------|---------------|
| 区切り                | 改行                   | 空白            |
| * などパターン検索でヒットなし | 出力なし(forでループ回らない)    | パターンそのものを１件出力 |
| * などパターン検索で階層の扱い | 階層は含まない。パターンはファイル名のみ | 階層を辿る         |
| ヒットしない場合の終了コード     | 成功扱い                 | エラー扱い         |

## grep

```bash
grep 検索文字 ファイルパス
grep -w 検索文字 ファイルパス  # 単語として検索
grep -i 検索文字 ファイルパス  # 大文字小文字無視
grep -F 検索文字 ファイルパス  # 固定文字として
grep -l 検索文字 ファイルパス  # ヒットしたファイル名のみ出力
```

## paste

```bash
paste a.txt b.txt  # タブ区切りで横並びに結合。足りない行は空白に。
paste -d, a.txt b.txt  # カンマ区切り
paste -d "\n" a.txt b.txt  # 交互
```

## sed

```bash
# 単純に置き換え
cat ファイル名 | sed 's/a/b/g' 
cat ファイル名 | sed 's|/||g'

# ファイルから 5～7 行(1～)を切りぬく
cat ファイル名 | sed -n '5,7 p' > 出力ファイル名
```

## seq

```bash
seq 1 6  # １～５の連番 (1 2 3 4 5 6) を改行区切りで出力
seq 1 2 6  # ２飛び (1 3 5) 
seq 1 0.2 2  # 小数点 (1.0 1.2 1.4 1.6 1.8 2.0)
seq -s, 1 6  # カンマ区切り (1,2,3,4,5,6)
seq -w 8 10  # ゼロ埋めして等幅に (08 09 10)
```

### `ブレース展開` を使った連番生成

```bash
echo {8..10}  # 8 9 10 (改行区切り)
echo {10..8}  # 10 9 8 (改行区切り)
echo {08..10}  # 08 09 10 (改行区切り)
echo {6..10..2}  # 6 8 10 (改行区切り)
echo {A..C}  # A B C (改行区切り)
```

## sort

```bash
cat ファイル | sort -k2 -n  # ２列目を数字とみなして昇順ソート
cat ファイル | sort -k2 -nr  # ２列目を数字とみなして降順ソート
```

## tac 

```bash
tac a.txt b.txt  # 結合して逆順ソート
```

## wc

```bash
cat ファイル名 | wc -l  # 行番号
```

## その他コマンド

### サイズの大きなファイルを特定
```bash
du -m ファイルパス | sort -n -k 1
```

# ヒアドキュメント

```bash:test.datにそのまま出力
cat <<EOS > test.dat
hoge
fuga
piyo
${AAA} ← 展開される
EOS
```

```bash:test.datにそのまま出力
cat <<'EOS' > test.dat
piyo
${AAA} ← 展開されない
EOS
```

```bash:代入も
AAA="$(cat <<EOS
piyo
EOS
)
```

```bash:これでも良いが
AAA="\
piyo
")
```

# 環境チェック

```bash
if ! [ "${OS:-}" = "Windows_NT" ]; then
  echo "Windows OS の gitbash から実行してください。"
  exit 1
fi
```

# 単体テストで使うテクニック

```bash:ディレクトリ同士を比較して差分チェック
check_diff() {
  echo "diff -r $*"
  # shellcheck disable=SC2086
  diff -r "$@"
  RES="$?"
  if [ $RES -eq 0 ]; then
    echo "（OK. 差分なし）"
  fi
  return $RES
}
```

```bash:ファイル/ディレクトリの更新日付変更
UPD="$(date -d "30 days ago" '+%Y%m%d%H%M.%S')"
mkdir -p          "hoge"
# ファイルを更新すると上位のディレクトリの更新日付が現在時刻に変わるので必ず末端から更新する
touch -t "${UPD}" "hoge/keep"
touch -t "${UPD}" "hoge"

# 階層辿ってすべて更新したいなら
find "hoge" -exec touch -t "${UPD}" {} +
# {} はヒットしたファイル名。+ がついているのでヒットした全ファイルが列挙される
```

# ちょっと変わった使い方

## コマンド終了時に音(beep)で知らせる

```bash
any-command; echo -en "\a"
```


# 知っておくべき仕様

## !!

`!～` には特別な意味がある。たとえば `!!` は直前のコマンドに置き換えられる。どんな引用符の中でも。echo "OK!!" は「OK*直前のコマンド*」と出力される。

```
$ echo 111
$ echo "OK!!"
OKecho 111
```


# 参考

- [Efficient Linuxコマンドライン ―開発と自分に磨きをかけるLinuxのテクノロジー](https://amzn.to/48JXll5) ... 良書！