概要
bash 4.0 以降に追加された機能を使ってスクリプトを書くときに個人的に気になった個所をピックアップしてみました。
に新しくリリースされたバージョンが前のバージョンから何が変わったのかの過去からの履歴がありますが、相当量あるので今回はそのごく一部の紹介に留めます。
- bultinのファイル読み取り関数
- 連想配列
- 配列の負のインデックス
- 閉じ忘れのエラーメッセージが親切に(5.3)
- Linter/Formatter
の内容を書きます。
はじめに
なぜ 4.0 以降かというと業務端末の macOS のディフォルトシェルが2019年末のバージョンで zsh に変わってから bash をあまり触っていなかったので、3.2 系で知識が止まっているためです。
今年(2025年)7月には bash 5.3 がリリースされたので、これを機会に知識を更新しようと思いました。
最新のバージョンがOS標準インストールのシェルになるのは5年くらいかかりそうな気はしますが
を見る限り、最近のOSであれば 5.1+ あたりは対応していそうですね。
前提
私の bash の用途としては CI/CD のパイプライン中のスクリプトです。 動作環境としては Ubuntu が多いと思います。単体テストを細かく書いたほうがいい処理は Python などを使います。
そのため以下は今回取り上げません。
- 対話型シェル向けの機能 ( 環境変数、キーバインド、 readline、補完機能 他 )
- 追加インストールが必要なもの ( bash-builtins 他 )
- 並行実行機能 (使ってバグがあっても適切にハンドリングできる技量が不足していると思うので)
- 単体テストフレームワーク
追加・変更機能
mapfile
ディフォルトでは改行区切りのレコードを配列に格納してくれる組み込み関数です。
例えば以下のような example.csv があったとして
1,1Password
2,Local env
mapfile を使って以下のように読み込めます。
#!/usr/bin/env bash
# 配列を宣言
declare -a arr
# ディフォルトデリミタの文末改行を取り除いて(-t)読み込み
mapfile -t arr < example.csv
for i in "${!arr[@]}"; do
echo "index: $i value: ${arr[$i]}"
done
実行結果は以下になります。
index: 0 value: 1,1Password
index: 1 value: 2,Local env
また、 -d でデリミタを指定して配列に格納することもできます。
#!/usr/bin/env bash
declare -a arr
csv="a,b,c,d"
mapfile -t -d "," arr <<< "$csv"
for i in "${!arr[@]}"; do
echo "index: $i value: ${arr[$i]}"
done
実行結果は以下になります。
index: 0 value: a
index: 1 value: b
index: 2 value: c
index: 3 value: d
-t が -d で指定した末尾要素を取り除いているのでカンマは付いていませんが、今度は末尾改行が残っているようです。ヒアストリングを展開して考えると
<<<EOS
a,b,c,d
EOS
なので最後の要素が d\n になっているんでしょうね。ヒアストリング部分を
< <(echo -n "$csv")
にすれば改行はないので、 mapfile でやりたければ入力側で工夫する必要がありそうです。
これまでの内容を踏まえて例の example.csv のようなカンマと改行区切りの単純なフォーマットは以下でループ処理できることになりそうです。
#!/usr/bin/env bash
declare -a lines
declare -a elems
mapfile -t lines < example.csv
for li in "${!lines[@]}"; do
mapfile -t -d "," elems < <(echo -n "${lines[$li]}")
for ei in "${!elems[@]}"; do
echo "index ${li}-${ei}: value: ${elems[$ei]}"
done
done
実行結果は以下になります。
index 0-0: value: 1
index 0-1: value: 1Password
index 1-0: value: 2
index 1-1: value: Local env
名前の通りファイルから行を読みだして使うのに便利な関数でした。 -s で指定デリミタ数分をスキップして読み出しもできるので、ヘッダ付きのファイルの読み出しにも使えそうです。
連想配列
連想配列というとワードカウントかマスタ突合なのでワードカウントをやります。ワードカウント定番お題の "London Bridge is falling down" のワードカウントをするので歌詞をBBCのサイトからコピペして lyrics.txt に保存します。
せっかくなので mapfile を使って以下を作りました。
#!/usr/bin/env bash
declare -a lines
declare -a words
declare -A word_counts
INPUT_FILE_NAME="lyrics.txt"
# ファイル読み込み
mapfile -t lines < "$INPUT_FILE_NAME"
for line in "${lines[@]}"; do
# カンマやピリオドなどは消しておく
mapfile -t -d " " words < <(echo -n "$line" | tr -d ",.")
for word in "${words[@]}"; do
# カウント
# キーが存在しない場合はエラーになるかと思ったら
# ${word_counts[$word]:-0} 的な挙動で 0 でした
((word_counts[$word]++))
done
done
# 結果表示
echo "----- Count results ----"
for word in "${!word_counts[@]}"; do
printf "%d %s\n" "${word_counts[$word]}" "$word"
done | sort -nr -k1
結果の上位は以下です。
----- Count results ----
12 down
12 and
9 falling
8 lady
8 fair
8 My
未定義値の扱いが 4.x 系の間で色々調整されたようで、少々雑な使い方でも期待した結果が得られたと思います。
負のインデックスへの対応
負のインデックスの場合は先頭からではなく、末尾からカウントした結果になるものです。
#!/usr/bin/env bash
declare -a arr=(1 2 3)
echo "${arr[0]}" #=> 1
echo "${arr[-1]}" #=> 3
終了していない複合コマンドのエラーメッセージで開始行を表示
これは例えば fi の閉じ忘れをした if があった場合に、 line (終了行数): syntax error: unxpected end of file だけで、 開始位置の if の位置を教えてくれるという内容です。
Linter/Formatterを適応しておけば分かりますが、これは初心者がエラー場所を探したり、昔書かれたシェルを見るときに便利だと思います。
2025年12月時点の最新版である 5.3 から導入というのには少し驚きました(Linterで指摘されはしますが)。
Linter/Formatter
Linter
ShellCheck を間接的に使っているのでスクリプトでも同様に使おうと思います。
GitHub Actions の Workflow ファイルの Linter の actionlint
が ShellCheck を内部的に使っていてお世話になっています。
Linter でよく調べる内容は以下でした。
| トピック | 内容 |
|---|---|
| ドキュメント | GitHubのwiki |
| 指摘IDと指摘詳細の表示 | 公式サイトのURLがエラーメッセージと共に表示 |
| 特定の指摘を無視 | 無視したい行の直上に # shellcheck disable=(指摘ID) のコメントを書く(複数指定は指摘IDをカンマ区切りで指定) |
| 全体で指摘を無視 |
.shellcheckrc に diasble=(指摘ID) を記載(複数指定は指摘IDカンマ区切りで指定 or 改行区切りで diasble=(指摘ID) を複数記載 |
| pre-commit | 同作者のhookあり(koalaman/shellcheck-precommit) |
| GitHub Actionsでの利用方法 | GitHub Actions - 公式Wiki ※SARIFレポート作る Actions もあるらしい |
Formatter
shfmt を使います。
Formatter でよく調べる内容は以下でした。
| トピック | 内容 |
|---|---|
| ドキュメント | GitHubのman |
| ファイル直接書き換え |
-w オプション指定 |
| インデント指定 | ディフォルトはタブ。スペースの場合は -i num オプションでスペースの個数を num で指定 |
| minify |
-mn オプション指定 |
| pre-commit | 3rd Party の hook あり |
| GitHub Actionsでの利用方法 | 公式での言及なし。reviewdogと 3rd Party の Actions のみ |
マニュアルに以下オプションが Google shell style gude に近いフォーマットになるとのことで、適応検討してみようかと思います。
shfmt -i 2 -ci -bn
おわりに
バージョンの初回リリースで言うと10-15年の間が空いている状態からのキャッチアップだったのでピックアップするのも大変でした。
それでいて前提にも記載しましたが、他の言語にもあるような機能を bash ではこうやるといった内容になっていますね。「bash で〇〇ができない」から他の言語を使っていたりするので、注目する点が他の言語にもある機能になりがちにはなるのですが。
これを機会にリリースの度に自分が使いそうな機能には目を通しておこうと思います。