0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AI に地図が付いたシェルスクリプトを生成させる ― bash に C# の `#region` を持ち込む

0
Posted at

map

シェルスクリプトには、パッケージや名前空間やクラスのような、分類的な区切りがありません。関数を定義できるだけで、それらをまとめる単位がないのです。

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 拡張機能を入れる

分類を開いた後、パンくずリストから関数を選べるようになります。折りたたむだけなら、入れなくても構いません。

  1. VSCode の左側の拡張機能アイコン(四角が4つ並んだもの)を押します
  2. 検索欄に Bash IDE と入力します
  3. 発行元が 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 した先のサーバーで、lessvi しか無い状態でも同じです。エディターの設定も、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 は、他のスクリプトへコピーすれば動きます。一方、分類名がアプリケーションにありそうな名前のときは、グローバル変数を参照していることが多く、コピーしただけでは動きません。中身を読んで、引数に置き換える編集が要ります。

判断のためにコードを読み直す必要がないのは、分類名を決めた時点でその情報が書き込まれているからです。分類コメントは、区切りを示すと同時に、その区切りの性格も示しています。

コピーするときの操作

いきなり貼り付けるのは危険です。コピー先に同じ関数が既にあり、どちらが新しいか分からないまま上書きすると事故になるので、差分を見ながら適用します。

  1. コピーする関数を選択し、コピーします
  2. コピー先のスクリプトを開きます
  3. F1 キーを押し、clipboard と入力します(日本語表示なら クリップボード
  4. 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 の分類にある
  • ArrayError の分類は、他のスクリプトへそのままコピーできる
  • LogPath というグローバル変数を参照しているのは Main だけで、ArrayError も参照していない

最後の点は、コピーの判断に直結します。SetArrayFromLines は、このスクリプトの事情を何も含んでいません。Array の分類ごと、別のスクリプトへコピーできます。

分類コメントを外しても、スクリプトは同じように動きます。変わるのは、読む側が最初に手にする情報の量だけです。

まとめ

ここまで bash を対象に書いてきましたが、この仕組みが成り立つ条件は2つしかありません。

  • # から始まる行が、無視されるか、意味を持たないこと
  • VSCode がその形式に対して、#region を折りたたみのマーカーとして登録していること

どちらも bash に固有のものではありません。 他の言語でも分類コメントが使えるかもしれません。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?