ヘルプと Usage 整備|自己説明する CLI

関数・モジュール化

この記事の狙い

Bash 製 CLI を自己説明型にします。
--help--version、わかりやすい Usage/Options/Examples/Exit codes/Env常に同じ型で出し、引数エラー時は用法を短く見せる実装を示します。

前提と対象

  • Bash 4+(macOS の古い Bash は 3.x のため、できれば新しい Bash を使用)
  • set -Eeuo pipefail 前提
  • getopts を基本に、長いオプション(--long)も受けたい場合のパターンも提示

TL;DR(“コピペ可”・最小実装)

  • -h|--help詳細ヘルプ、引数エラー時は短い用法+誘導
  • -V|--version機械可読NAME VERSION
  • 解析は getopts--long は前処理で短い別名に変換
#!/usr/bin/env bash
set -Eeuo pipefail

APP_NAME="${APP_NAME:-mycli}"
APP_VERSION="${APP_VERSION:-1.0.0}"

usage() {
  cat <<'USAGE'
Usage:
  mycli [options] <subcommand> [args...]

Subcommands:
  greet            print greeting
  sum              add integers

Options:
  -o, --output PATH    write result to PATH (default: stdout)
  -v, --verbose        verbose logging
  -n, --dry-run        do not change anything
  -h, --help           show help and exit
  -V, --version        print version and exit

Environment:
  MYCLI_LOG_LEVEL   override log level (info|debug)

Examples:
  mycli greet --verbose Alice
  mycli sum 10 20 -o result.txt

Exit codes:
  0 success / 2 usage error / 3 runtime error
USAGE
}

short_usage() {
  printf 'Usage: %s [options] <subcommand> [args...]\n' "$APP_NAME" >&2
  printf "Try '%s --help' for more information.\n" "$APP_NAME" >&2
}

version() { printf '%s %s\n' "$APP_NAME" "$APP_VERSION"; }

# --- logging (stderr) ---
log() { printf '%s\n' "$*" >&2; }
die() { log "ERROR: $*"; exit "${2:-3}"; }

# --- long options → short 別名化(getopts 前処理) ---
normalize_args() {
  local out=() a
  for a in "$@"; do
    case "$a" in
      --help)    out+=(-h) ;;
      --version) out+=(-V) ;;
      --verbose) out+=(-v) ;;
      --dry-run) out+=(-n) ;;
      --output)  out+=(-o) ;;              # 引数付きの --output は次の値をそのまま消費
      --output=*) out+=(-o "${a#*=}") ;;   # --output=FILE も許可
      --) out+=(--) ;;                      # 以降はそのまま
      *)  out+=("$a") ;;
    esac
  done
  printf '%s\0' "${out[@]}"
}

main() {
  # 正式引数へ正規化を流し込む
  mapfile -d '' -t argv < <(normalize_args "$@")
  set -- "${argv[@]}"

  local verbose=0 dry_run=0 out=/dev/stdout

  # オプション解析(短い別名になっている想定)
  while getopts ":o:vnVh" opt; do
    case "$opt" in
      o) out="$OPTARG" ;;
      v) verbose=1 ;;
      n) dry_run=1 ;;
      V) version; exit 0 ;;
      h) usage; exit 0 ;;
      \?) short_usage; exit 2 ;;
      :)  log "missing value for -$OPTARG"; short_usage; exit 2 ;;
    esac
  done
  shift $((OPTIND - 1))

  # サブコマンド必須
  local sub="${1-}"; [[ -n "$sub" ]] || { short_usage; exit 2; }
  shift

  case "$sub" in
    greet)
      local name="${1-}"; [[ -n "$name" ]] || { log "greet requires NAME"; short_usage; exit 2; }
      (( verbose )) && log "greeting to $name"
      (( dry_run )) && log "(dry-run) would greet $name" && exit 0
      printf 'hello, %s\n' "$name" >"$out"
      ;;
    sum)
      local t=0 x
      for x in "$@"; do [[ "$x" =~ ^-?[0-9]+$ ]] || die "not an integer: $x" 2; t=$(( t + x )); done
      (( verbose )) && log "sum=$t"
      printf '%s\n' "$t" >"$out"
      ;;
    *)
      log "unknown subcommand: $sub"; short_usage; exit 2 ;;
  esac
}

main "$@"

実行例:

./mycli --help
./mycli -V
./mycli greet Alice -v
./mycli sum 10 20 --output=out.txt

設計の要点(原則)

  1. 短い用法(短文)と詳細ヘルプ(章立て)を分ける
    • エラー時は「短い用法+--help へ誘導」。--help は章立ての長文。
  2. 出力を役割で分離
    • stdout: 正常系の結果(他コマンドに渡る)
    • stderr: ヘルプ・ログ・警告・用法エラー
  3. 終了コードの規約化
    • 0: 成功 / 2: 用法エラー / 3: 実行時エラー(ネットワークや I/O など)
  4. バージョン出力は機械可読
    • NAME VERSION 一行。スクリプトからの自動取得が容易。

ステップ実装(分解解説)

段階1:Usage の章立てを固定する

固定の順番で書くと迷いません:

  • Usage(1行)
  • Subcommands(あれば)
  • Options
  • Environment
  • Examples
  • Exit codes

段階2:getopts × 長いオプション

  • Bash の getopts短いオプション専用
  • そこで、本編のように**--long を前処理で短い別名に変換**してから getopts へ渡すと、実装が単純になります。
  • --opt=VALUE-o VALUE に畳み込むと UX が上がります。

段階3:ヘルプは定数文字列でよい

  • CLI の Help はプログラムで組み立てないのが原則。
  • 動的変化は環境変数・デフォルト値だけに留め、本文は整形済みのヒアドキュメントに。

段階4:出力先の統一

  • 結果は > "$out" のように最初に開いたファイルへ。
  • ログは log() を通して stderr へ。

段階5:エラーは理由→用法の順

  • まず何が悪かったかを1行で伝え、そのあと短い用法で救済します。

失敗しやすい点(アンチパターン)

  • echo でヘルプを出す-n 解釈やエスケープで崩れる → cat <<'EOF' を使う
  • ヘルプを stdout に出す → パイプ先に混入 → ヘルプは stderr(ただし --help は慣習上 stdout も可、現場で統一)
  • 終了コードが常に 0 → CI/スクリプトから判別不能 → 用法エラーは 2 を徹底
  • 長いオプションを getopts で直接扱おうとする → できない → 前処理で畳み込む

“コピペ可”テストブロック(記事末テンプレ)

#!/usr/bin/env bash
set -Eeuo pipefail
SUT="${SUT:-./mycli}"
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT

pass(){ printf 'PASS %s\n' "$1"; }
fail(){ printf 'FAIL %s\n' "$1"; exit 1; }

# 1) --version は機械可読
mapfile -t v < <("$SUT" -V)
[[ "${v[0]}" =~ ^mycli[[:space:]][0-9]+\.[0-9]+\.[0-9]+$ ]] || fail "version"

# 2) --help は Usage を含む
"$SUT" --help | grep -q '^Usage:' || fail "help"

# 3) 引数不足は用法エラー(2)で短い用法を出す
set +e; "$SUT" 2>"$TMP/err"; st=$?; set -e
[[ $st -eq 2 ]] || fail "usage exit"
grep -q '^Usage:' "$TMP/err" || fail "short usage"

# 4) 動作確認: greet
out="$("$SUT" greet Alice)"
[[ "$out" == "hello, Alice" ]] || fail "greet"

# 5) 動作確認: sum + 出力ファイル
"$SUT" sum 7 8 --output="$TMP/out.txt"
[[ "$(cat "$TMP/out.txt")" == "15" ]] || fail "sum file"

echo "ALL GREEN"

運用設計(実務)

  • 例は実行可能な最小の 2〜3 本に絞る(CI のドキュメントテストにも使える)
  • 環境変数の一覧をヘルプに常設し、デフォルトも併記
  • ヘルプの更新を CI で検知grep で必須文言・バージョンの存在を確認する簡単なチェックで十分)
  • サブコマンドが増えるなら**bin/mycli-<sub> を自動委譲**する設計も有効(git 方式)
# サブコマンド委譲(上級・任意)
delegate() {
  local sub="$1"; shift
  local cmd="${BASH_SOURCE[0]%/*}/mycli-$sub"
  if command -v "$cmd" >/dev/null 2>&1; then exec "$cmd" "$@"; fi
  return 127
}

互換性と移植性

  • getopts は POSIX。短いオプションはどこでも動く
  • 長いオプションは本稿の前処理方式が安全(getopt(1) は環境差で壊れやすい)
  • macOS/BSD と GNU で readlink 挙動が違うため、ヘルプ本文にパス依存の例を書かないのが無難

セキュリティと安全設計

  • すべての変数展開はダブルクォート
  • 出力ファイルは先に変数へ束縛してから一箇所で開く(> "$out")。リダイレクトの散在を避ける
  • エラーメッセージは人間に読める 1 文短い用法で復旧を促す

パフォーマンスの勘所(短く)

  • ヘルプ生成は定数文字列なのでゼロコストに近い
  • 解析の前処理(長→短変換)は引数個数に線形で十分軽量

参考リンク

Bash玄

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

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

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

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

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

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

Bash玄をフォローする