ワンライナーの分解術|関数化・再利用

設計パターン&テンプレ
スポンサーリンク

この記事の狙い

手元のワンライナーを**“使い捨て”で終わらせず**、安全・再利用・テスト可能な小さな関数へ分解する手順を示します。
入出力の境界を決め、標準出力=結果/標準エラー=ログ終了コードの意味副作用の隔離を徹底します。

前提と対象

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 pipefailIFS と read -rNUL セーフ(-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 セーフか(ファイル名処理)
  • クォート-- を徹底しているか
  • pipefailIFS/read -r を組み込んだか
  • 関数名は動詞+目的語で用途が見えるか
  • 小さな関数をパイプで組み替えられるか(差し替え容易性)

アンチパターン → 改善

悪い例問題改善
巨大ワンライナー1本可読性ゼロ、再利用不可collect/transform/emit に3分割
変換で echoexit 混在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)は区間限定

参考リンク

スポンサーリンク
Bash玄

はじめまして!Bash玄です。

エンジニアとしてシステム運用に携わる中で、手作業の多さに限界を感じ、Bashスクリプトを活用して業務を効率化したのがきっかけで、この道に入りました。「手作業は負け」「スクリプトはシンプルに」をモットーに、誰でも実践できるBashスクリプトの書き方を発信しています。

このサイトでは、Bashの基礎から実践的なスクリプト作成まで、初心者でもわかりやすく解説しています。少しでも「Bashって便利だな」と思ってもらえたら嬉しいです!

# 好きなこと
- シンプルなコードを書くこと
- コマンドラインを快適にカスタマイズすること
- 自動化で時間を生み出すこと

# このサイトを読んでほしい人
- Bashに興味があるけど、何から始めればいいかわからない人
- 定型業務を自動化したい人
- 効率よくターミナルを使いこなしたい人

Bashの世界に一歩踏み出して、一緒に「Bash道」を極めていきましょう!

Bash玄をフォローする