シェルスクリプトには、パッケージや名前空間やクラスのような、分類的な区切りがありません。関数を定義できるだけで、それらをまとめる単位がないのです。
AI の進化で、自分でスクリプトを書く場面は減りました。代わりに、AI が書いたものを読んで確認する場面が増えています。数百行のスクリプトが一度に出てきて、それが正しいかどうかを判断しなければなりません。
しかし AI は、関数を並べる順序までは考えてくれません。数十個の関数が平坦に並び、どこから読めばいいのか分からない。一部だけ直してもらうと、関数の位置が変わって差分が読めなくなる。「あの処理はどこだったか」と探すたびに、ファイル全体をスクロールすることになります。
ファイルを分割すれば解決しますが、1ファイルで配れる手軽さや確実性は失いたくありません。ctags や LSP を入れれば定義には飛べますが、SSH した先のサーバーや、コードレビューの画面では使えません。
今回紹介する分類コメントを数行足すだけで、平坦に並んだ数十個の関数に分類のブロックができ、読みたいところだけを開いて読めるようになります。
使用前(分類コメントなし)
function Main() {
...
}
function FilterLogByDate() {
...
}
function SetArrayFromLines() {
...
}
関数の並びの途中です。どこまでがこのスクリプト固有の処理で、どこからが汎用の処理に変わったのかは、関数名を1つずつ読んで初めて分かります。
使用後(分類コメントあり)
#region: Main
function Main() {
...
}
function FilterLogByDate() {
...
}
#endregion:
#region: Array
function SetArrayFromLines() {
...
}
#endregion:
区切りがどこにあるかが見た目で分かり、#region: Array で検索すればこの位置を表示できます。
すべて折りたたんだ状態
VSCode で分類コメントをすべて折りたたむと、スクリプトはこうなります。
#region: Main ⋯
#region: Array ⋯
#region: Error ⋯
**数百行あっても、画面に出るのはこれだけです。このスクリプトが何を扱うかやレイヤー構成が、一目で分かります。**読みたい分類だけを開けば、その中の関数が現れます。
準備
Claude に規則を伝える
Claude に規則を渡しておけば、生成するスクリプトに最初から分類コメント付きで出てきます。
ブラウザーで開いた claude.ai や Claude のアプリを使う場合は、画面左下のアイコンから「設定」を開き、「Claude への指示」の欄に規則を貼り付けます。ここに書いた内容は、すべての新しい会話に適用されます。特定のプロジェクトだけに適用したい場合は、プロジェクトを作成し、そのプロジェクトの「プロジェクトの指示」に書きます。
Claude Code を使う場合は、プロジェクトの直下に CLAUDE.md という名前のファイルを置き、その中に規則を書きます。Claude Code は起動時にこのファイルを読み、以降の生成に反映します。
規則の全文は https://takakiriy.github.io/structured-documents/ref/AI/coding_rule_for_AI.html に置いています。
bash IDE 拡張機能を入れる
分類を開いた後、パンくずリストから関数を選べるようになります。折りたたむだけなら、入れなくても構いません。
- VSCode の左側の拡張機能アイコン(四角が4つ並んだもの)を押します
- 検索欄に
Bash IDEと入力します - 発行元が
Mads Hartmannのものを選び、「インストール」を押します
インストール後、.sh ファイルを開くと、画面上部にパンくずリストが表示されます。
以下では、分類コメントの書式と、そこから目的の関数を表示するまでの操作を説明します。
書式
分類コメントは、開始と終了の2行です。1行だけの区切りにせず対にしたのは、エディターで折りたためるようにするためです。
開始と終了
#region: Array
function SetArrayFromLines() {
...
}
#endregion:
#region: の右に分類名を書き、#endregion: で閉じます。終了側には分類名を書いても書かなくてもどちらでも良いです。書いた方が開始と終了の対応関係が明確になりますが、その対応関係を維持するコストがかかります。分類名は Pascal ケース(単語の大文字から始めて、単語の間は詰めます)で、開始と終了それぞれの前後の行に空行を入れます。 #region: の右を空白文字を含む通常の英文にするのではなく Pascal ケースにするのは、関数名・グローバル変数名と同様に、分類名に表記ゆれが起きないようにするためです。 あわせて、1語に詰まっていることで、分類名が1つのかたまりとして記憶に残ります。英文のままだと、どこまでが名前なのかを読むたびに判断することになり、覚えて使い回す対象になりません。
関数は必ずどれかの分類の中に置きます。分類に属さない関数を残すと、#endregion: から次の #region: までの間に関数が現れ、#region: で折り畳めなくなります。 例外は、ファイルの先頭に置く Help 関数の定義だけです。最初の #region: より上にあるので、分類と分類の間には挟まりません。
折りたたみ
VSCode は、bash のファイルでも #region と #endregion を折りたたみのマーカーとして認識します。開始行の左に現れるマークをクリックすると、#endregion: までが1行に畳まれます。
bash 自体は # から行末までをコメントとして読み飛ばすので、書いても動作は変わりません。閉じ忘れてもエラーになりません。折りたたみが効くのは、VSCode が言語ごとに登録している折りたたみのマーカーに一致するからです。分類コメントは、この挙動を利用しています。
分類をすべて畳めば、ファイルは分類名の並びになります。読みたい分類だけを開けば、その中の関数が現れます。
分類コメントの名前について
#region は、C# ではプリプロセッサ ディレクティブで、#endregion と対応していなければコンパイル エラーになります。bash では単なるコメントなので、エラーになりません。この記事では分類コメントと呼んでいます。
C# の #region と違うのは、#region の後にコロンを付けている点です。 これは typrm のタグの書式に合わせていることと、名前であることが明確になるためです。 VSCode 的には、コロンの有無は動作に影響しません。
💡 なお、分類コメントの名前は当初
#section:にするつもりでした。 section は全体を切り分けた一部分を指し、分類を表す語としてはそちらのほうが自然です。 一方 region は境界で囲まれた範囲を指すので、開始と終了で囲む今回の書式には合っています。 ただ、決め手になったのは意味ではありません。#section:では VSCode の折りたたみが効かないからです。 認識されるのは#regionと#endregionの組だけです。名前の座りより、畳める実用性を取りました。
目的の関数を表示する
分類コメントの本来の用途は、読む範囲を絞ることです。絞ったうえで、目的の関数を画面に出すまでの操作が要ります。VSCode であれば、折りたたんだ状態から分類を選んで開きます。 その他の環境では文字列検索をします。具体的な操作を、環境ごとに見ていきます。
すべて折りたたんで、一覧から選ぶ
まず、VSCode で Ctrl + ( K, 0(ゼロ)) を押して、すべて折りたたみます。 すると #region: が一覧されるので、読みたい分類の名前を選び、開いていきます。 目的の関数が表示されたら、Ctrl + ( K, J ) を押して、すべての折り畳みを解除して通常の表示に戻ります。 もし、bash IDE 拡張機能をインストールしてあれば、分類コメントを開いた後は、パンくずリストから関数一覧を表示して、関数を選ぶこともできます。
grep で一覧を出してから開く
折りたたむ機能がないエディターを使う場合や、折りたたみたくない場合は、まず #region: で分類の一覧を出します。
$ grep -n '#region:' collect-access-log.sh
21:#region: Main
80:#region: Array
93:#region: Error
このようにして、数十個の関数を上から読まなくても、分類の中にある関数だけを読めば済みます。
less
/#region:
と入力すると、最初の分類コメントが表示されます。n を押すたびに次の分類へ進みます。分類の切れ目だけを順に見ていけるので、ファイル全体をスクロールする必要がありません。
特定の分類を直接表示するなら、こうします。
/#region: Array
GitHub、コードレビュー画面
ブラウザーでコードを見るとき、定義へ飛ぶ機能は使えません。使えるのはブラウザーの検索( Ctrl + F )だけです。
分類コメントはファイルの中身なので、この場面でも同じように機能します。#region: Array を検索すれば、その位置がハイライトされます。レビューで「Array の分類を見てください」と伝えれば、相手は検索するだけで該当箇所に着きます。行番号を伝える必要がありません。
SSH した先のサーバーで、less や vi しか無い状態でも同じです。エディターの設定も、ctags ツールのインストールも要りません。
どのエディターでも検索の操作が同じなのは、単なる文字列だからです。プラグインの有無や設定の違いに影響されません。折りたたみだけは、#region を認識するエディターに限られます。
ライブラリ化
分類コメントは、読むための目印にとどまりません。他のスクリプトへそのままコピーできるかどうかを判断する材料にもなります。そのために1つだけ規則を足しています。
ライブラリになりうる関数のルール
ライブラリになりうる関数は、グローバル変数を参照しない決まりにしています。必要な値は、すべて引数で受け取ります。Array のような、扱う対象の名前を付けた分類には、そういう関数が集まります。
#region: Array
function SetArrayFromLines() {
local -n arrayRef="$1"
shift
local IFS=$'\n'
arrayRef=( ) && read -r -d "" -a arrayRef <<< "$*" || true
}
#endregion:
この関数は、どのスクリプトに置いても同じように動きます。逆に Main のような、このスクリプトの事情を含む分類の関数は、グローバル変数を参照してよい側です。
分類名からコピーできるかが分かる
規則を守っていれば、分類名がライブラリにありそうな名前のとき、そこをそのままコピーできることが分かります。
$ grep -n '#region:' collect-access-log.sh
21:#region: Main
80:#region: Array
93:#region: Error
Array は、他のスクリプトへコピーすれば動きます。一方、分類名がアプリケーションにありそうな名前のときは、グローバル変数を参照していることが多く、コピーしただけでは動きません。中身を読んで、引数に置き換える編集が要ります。
判断のためにコードを読み直す必要がないのは、分類名を決めた時点でその情報が書き込まれているからです。分類コメントは、区切りを示すと同時に、その区切りの性格も示しています。
コピーするときの操作
いきなり貼り付けるのは危険です。コピー先に同じ関数が既にあり、どちらが新しいか分からないまま上書きすると事故になるので、差分を見ながら適用します。
- コピーする関数を選択し、コピーします
- コピー先のスクリプトを開きます
- F1 キーを押し、
clipboardと入力します(日本語表示ならクリップボード) -
File: Compare Active File with Clipboard(ファイル: アクティブ ファイルをクリップボードと比較)を選びます
左にクリップボードの内容、右にコピー先のファイルが並び、差分が色分けされます。違いがあった部分について、どちらが新しいか確認して適用して同期してください。なお、この同期のために、マスターとなるコードが書かれたファイルを用意しておくと良いでしょう。
分類ごとコピーする場合は、折りたたんだ状態で #region: の行頭から、見えている次の行の行頭までを選択すると、分類全体を選んでコピーできます。
コピーするときの注意点
他の関数を呼んでいないか。 呼んでいれば、その関数も一緒にコピーする必要があります。呼び先がさらに別の関数を呼んでいれば、連鎖します。
AI と組み合わせる
コーディング ルール に書いてあるような Claude への指示に分類コメントの規則を書いておくと、生成の時点で分類が付きます。
AI が自動的にレイヤーに分割する
「アクセスログを集計するスクリプトを書いて」と頼むだけで、関数が分類に分かれた状態で出てきます。どの関数がこのスクリプト固有で、どれが他でも使えるかを、AI が判断して振り分けます。
「関数をもう1つ足して」と頼んだときも、AI はその関数が属する分類を判断し、その分類の中に挿入します。
分類名で対象を示せる
分類名は、AI と会話するときの共通の呼び名になります。
Array の分類の関数を直して
修正の依頼だけでなく、「Main の分類だけ見せて」「Array の分類は触らないで」のように、範囲を指すときにも使えます。関数が何個あっても、1語で指せます。
分類を書き換えてフィードバックする
AI が付けた分類が、自分の考えと合わないことがあります。粒度が細かすぎる、固有の処理が汎用側の分類に入っている、といった場合です。
そのときは、分類コメントを自分で書き換えて、関数を移します。
書き換えたスクリプトを AI に渡せば、以降の生成はその分類に従います。「こう分類してほしい」を文章で説明する代わりに、コードの形で示せます。
実例
冒頭の断片の元になったスクリプトです。アクセスログを集計して、指定した期間の件数を出力します。
#!/bin/bash
#// AI-generated code
function Help() {
echo "Usage: collect-access-log.sh [--log-path __LogPath__] __StartDate__ __EndDate__"
echo " __StartDate__, __EndDate__: YYYY-MM-DD"
}
LogPath="/var/log/httpd/access_log"
PositionalArgs=()
while [[ $# -gt 0 ]]; do
case $1 in
-l|--log-path) Options_LogPath="$2"; shift; shift;;
-*) echo "Unknown option $1"; exit 1;;
*) PositionalArgs+=("$1"); shift;;
esac
done
set -- "${PositionalArgs[@]}" #// set $1, $2, ...
unset PositionalArgs
#region: Main
function Main() {
local startDate="$1"
local endDate="$2"
local logPath="${Options_LogPath:-${LogPath}}"
local logText="$( cat "${logPath}" )" || Error "ERROR: not found ${logPath}"
local filteredText="$( FilterLogByDate "${logText}" "${startDate}" "${endDate}" )" || Error
local logLines=( ) && SetArrayFromLines logLines "${filteredText}"
echo "${#logLines[@]}"
}
function FilterLogByDate() {
#// Filters access_log lines by the date part of the timestamp field
local logText="$1"
local startDate="$2" #// YYYY-MM-DD
local endDate="$3" #// YYYY-MM-DD
local logLines=( ) && SetArrayFromLines logLines "${logText}"
local line=""
local outputLines=( )
for line in "${logLines[@]}"; do
local timestamp="$( GetLogTimestamp "${line}" )" || Error
local date="${timestamp%%:*}" #// left of ":"
if [[ "${date}" > "${startDate}" || "${date}" == "${startDate}" ]] &&
[[ "${date}" < "${endDate}" || "${date}" == "${endDate}" ]]; then
outputLines+=( "${line}" )
fi
done
printf "%s\n" "${outputLines[@]}"
}
function GetLogTimestamp() {
#// Gets the timestamp field of an access_log line, e.g. "2026-08-22:10:15:30"
local logLine="$1"
local bracketPart="${logLine#*[}" #// right of "["
echo "${bracketPart%%]*}" #// left of "]"
}
#endregion:
#region: Array
function SetArrayFromLines() {
#// Sets lines to array except for empty line
local -n arrayRef="$1"
shift
local IFS=$'\n'
arrayRef=( ) && read -r -d "" -a arrayRef <<< "$*" || true #// function stdin
}
#endregion:
#region: Error
function Error() {
local errorMessage="${1-""}"
if [ "${errorMessage}" == "" ]; then
errorMessage="ERROR"
fi
echo "${errorMessage}" >&2
exit 1
}
#endregion:
Main "$@"
分類は3つです。それだけで、次のことが分かります。
- ログの形式に依存する処理を含め、このスクリプト固有の処理は
Mainの分類にある -
ArrayとErrorの分類は、他のスクリプトへそのままコピーできる -
LogPathというグローバル変数を参照しているのはMainだけで、ArrayもErrorも参照していない
最後の点は、コピーの判断に直結します。SetArrayFromLines は、このスクリプトの事情を何も含んでいません。Array の分類ごと、別のスクリプトへコピーできます。
分類コメントを外しても、スクリプトは同じように動きます。変わるのは、読む側が最初に手にする情報の量だけです。
まとめ
ここまで bash を対象に書いてきましたが、この仕組みが成り立つ条件は2つしかありません。
-
#から始まる行が、無視されるか、意味を持たないこと - VSCode がその形式に対して、
#regionを折りたたみのマーカーとして登録していること
どちらも bash に固有のものではありません。 他の言語でも分類コメントが使えるかもしれません。
