この記事の狙い
手元のワンライナーを**“使い捨て”で終わらせず**、安全・再利用・テスト可能な小さな関数へ分解する手順を示します。
入出力の境界を決め、標準出力=結果/標準エラー=ログ、終了コードの意味、副作用の隔離を徹底します。
前提と対象
Bash 4+/set -Eeuo pipefail 推奨。日常の grep | awk | sed | sort などを関数へ落とし込むイメージです。
TL;DR(最小実装・コピペ可)
#!/usr/bin/env bash
set -Eeuo pipefail
set -o pipefail
# 役割を分ける:collect → transform → emit
collect_lines() { # 入力源の抽象化(ファイル/コマンド/標準入力)
if [[ $# -gt 0 ]]; then cat -- "$@"
else cat -
fi
}
transform_sum_digits() { # 純粋関数:stdin→stdout、ログはstderr
awk '/^[0-9]+$/{s+=$1} END{print s+0}'
}
emit_result() { # 出力の最終形(ファイル/標準出力)
local out="${1:-/dev/stdout}"
cat >"$out"
}
# パイプで繋ぐと “元ワンライナー” と等価
main(){ collect_lines "$@" | transform_sum_digits | emit_result; }
main "$@"
ワンライナーを分解する手順(レシピ)
1) 役割ごとに関数化
- collect:入力ソースの取得(ファイル/コマンド/HTTPなど)。ここでだけ外部と接する
- transform:変換・フィルタ(純粋に
stdin→stdout。ログは stderr) - emit:出力の終端(ファイル/端末/キュー)
collect_find_logs(){ find . -type f -name '*.log' -print0; } # 例: NUL列挙
transform_pick_errors(){ xargs -0 -n1 grep -H -- 'ERROR'; } # 例: 抽出
emit_to_file(){ local f="$1"; cat >"$f"; }
2) I/O 契約を固定(Pure 化)
- 変換関数は標準入力から読み、標準出力にだけ書く
- ログは
>&2、制御は終了コードで返す - これでテストが容易になり、パイプで自由に差し替え可能
filter_csv_col(){ awk -F, '{print $3}' || return 3; }
3) パラメータを“引数”で受ける
グローバル変数に依存せず、引数で制御すると再利用が効きます。
normalize_case(){ local mode="${1:-lower}"
case "$mode" in
lower) tr '[:upper:]' '[:lower:]' ;;
upper) tr '[:lower:]' '[:upper:]' ;;
*) echo "unknown mode: $mode" >&2; return 2;;
esac
}
4) サブシェルと副作用の隔離
()内は親に影響しない:一時的なcdや環境変更に{}は親に残す:結果を変数に束ねたいときだけ
( cd repo && git pull --ff-only )
5) 安全化の定石を先に入れておく
set -o pipefail、IFS と read -r、NUL セーフ(-print0/-0, -d '')--のオプション終端、"$@"のクォート
read_nul_list(){ while IFS= read -r -d '' p; do printf '%s\0' "$p"; done; }
分解の例(Before → After)
例1:ログからエラーコード集計
Before(ワンライナー)
grep -h 'ERROR' *.log | awk '{print $3}' | sort | uniq -c | sort -nr | head -n5
After(関数化)
collect_errors(){ grep -h -- 'ERROR' -- "$@"; }
pluck_code(){ awk '{print $3}'; }
top_counts(){ sort | uniq -c | sort -nr | head -n "${1:-5}"; }
main(){
collect_errors "$@" | pluck_code | top_counts 5
}
# 使い方: ./script.sh /var/log/app/*.log
利点:pluck_code を別の入力にも再利用でき、top_counts 20 など柔軟に差し替え可能。
例2:NUL セーフ・並列 gzip
Before
find logs -name '*.log' -type f -print0 | xargs -0 -P4 -n1 gzip -9
After
collect_logs(){ find "$1" -name '*.log' -type f -print0; }
compress_one(){ xargs -0 -n1 -P"${JOBS:-4}" -I{} gzip -9 -- "{}"; }
main(){ collect_logs "${1:-logs}" | compress_one; }
利点:JOBS で並列度を環境変数制御、処理単位を差し替えやすい。
例3:HTTP → JSON → CSV(副作用分離)
fetch_json(){ curl -fsSL -- "$1"; } # collect
to_csv(){ jq -r '[.id,.name,.email]|@csv'; } # transform
save(){ local out="$1"; cat >"$out"; } # emit
main(){ fetch_json "$1" | to_csv | save "$2"; }
テストしやすい構成にする
1) 変換関数のユニットテスト
out="$(printf 'A\nb\n' | normalize_case lower)"
[[ "$out" == $'a\nb\n' ]] || exit 1
2) コピペ可・最小テストブロック
#!/usr/bin/env bash
set -Eeuo pipefail
set -o pipefail
# SUT
filter_csv_col(){ awk -F, '{print $2}'; }
normalize_case(){ tr '[:upper:]' '[:lower:]'; }
# テスト1: 列抽出
out="$(printf 'a,BB,c\nx,YY,z\n' | filter_csv_col)"
[[ "$out" == $'BB\nYY' ]] || { echo NG1; exit 1; }
# テスト2: 連結(パイプ再利用性)
out="$(printf 'a,BB\nx,YY\n' | filter_csv_col | normalize_case)"
[[ "$out" == $'bb\nyy' ]] || { echo NG2; exit 1; }
echo PASS
リファクタ指針(チェックリスト)
- 入出力の境界を決めたか(stdin/stdout/stderr/exit)
- 変換は純粋関数になっているか(副作用・グローバル参照がない)
- NUL セーフか(ファイル名処理)
- クォートと
--を徹底しているか pipefail、IFS/read -rを組み込んだか- 関数名は動詞+目的語で用途が見えるか
- 小さな関数をパイプで組み替えられるか(差し替え容易性)
アンチパターン → 改善
| 悪い例 | 問題 | 改善 |
|---|---|---|
| 巨大ワンライナー1本 | 可読性ゼロ、再利用不可 | collect/transform/emit に3分割 |
変換で echo と exit 混在 | stdoutがログで汚れる | 変換は stdout に結果、stderrにログ、returnで制御 |
| 改行区切りでファイル名処理 | 改行・空白で壊れる | -print0 / -0 / -d '' に切替 |
| グローバル変数を前提 | 再利用困難・テスト不可 | 引数で制御。必要なら printf -v / nameref |
| パイプ右側で状態更新 | サブシェルで消える | プロセス置換や lastpipe を使用 |
実務Tips
- まずワンライナーの核心(最小の
transform)を抽出→テスト→周辺の collect/emit を足す teeと> >(…)で途中結果の分岐ログを作ると、運用トラブル時に強い- “高コスト外部コマンド”は関数の外でまとめて呼ぶ(起動回数を減らす)
- 可搬性重視なら Bash 組み込み・パラメータ展開を活用して外部依存を減らす
互換性と移植性
- プロセス置換
< <(...)は Bash 拡張。POSIX sh 互換が必要な場合は一時ファイルで代替 mapfile/readarrayは Bash 4+。Bash 3.x ではwhile IFS= read -rで代替- GNU オプション(
-print0,-zなど)の有無は環境差に注意
セキュリティと安全設計
- ユーザー入力を正規化/検証してからパスやコマンド引数へ
evalを使わず、可変名はprintf -v/declare -nで代替- ログに機密を残さない。必要ならマスクし、トレース(
set -x)は区間限定
参考リンク
- GNU Bash Reference Manual — Functions / Pipelines / Process Substitution
https://www.gnu.org/software/bash/manual/bash.html - Wooledge BashGuide — Pipes, Redirections, and Functions
https://mywiki.wooledge.org/ - GNU findutils / xargs —
-print0/-0
https://www.gnu.org/software/findutils/
