🌸

コードを読むために必要なプロンプトはこれだ! 〜 シェルスクリプト チュートリアル (番外編)

に公開1

AI が生成したコードを読む必要性

AI はベースとなるコードを生成することが得意です。 しかし AI はデバッグをしたり仕様変更に応じたりすることがまだ不得意です。 つまり、生成されたコードを人間が引き継いで読まなければならないのです。

AI にシェルスクリプトを生成させると、コマンドをダラダラと並べた感じになります。 また、全体的に詳細から概要へという流れになっているため、概要より先に実装仕様書をいきなり読まされる感じになります。

そこで、読みやすいシェルスクリプトを AI が生成するようになる定型のプロンプトを紹介します。 このプロンプトをコピペで指示するだけで生成されたシェルスクリプトを読む気が失せることがほぼ無くなります。

単純に読みやすく書いてくださいと指示しても大して読みやすくなりません。 どうすれば読みやすくなるのかについて具体的に指示する必要があります。 「プログラミング言語としての シェルスクリプト チュートリアル」というシリーズ記事を私が書いてきたように(まだシリーズは終わっていませんが)、以前から、なるべく既存のプログラミング言語と同じように読めるように書く方法を模索してきました。 その成果を AI に指示しました。

シェルスクリプトのコードは、初心者プログラミングによくある単体のデータを直接扱う処理よりも、コマンドやプログラムを使って「データの集合に対するまとまった演算」を書くことになることが多いので、処理をイメージしにくいかもしれません。 そんな方にこそ、AI に生成してもらいましょう。 こんな書き方があったのかと学ぶことができ、シェルスクリプトだけでできることが広がります。

生成しっぱなしで指示ばかり出していると、AI は pit of death になって急激に賢さが無くなり、何も進まなくなり、詰みます。

普通に生成した場合のシェルスクリプト

まずは、AI に特に指示せずに、シェルスクリプトを出力させるとどうなるか見てみましょう。 サンプルとして、今日の運勢をランダムに書き換えるスクリプトを生成してみます。

Claude Sonnet 4 へ入力したプロンプト:

公開中の GitHub Pages の一部に書かれた今日の運勢をランダムに書き換えて公開するシェルスクリプトを作ってください。

生成されたスクリプトの完全版は、GitHub にあります。

https://github.com/Takakiriy/Trials/blob/master/try_to_generate_shellscript/update-fortune-default.sh

ここでは生成されたスクリプトの一部を示します。 なお、生成するたびに内容が変わるため、ここに挙げた特徴に合っていないこともあります。

  • main 関数 の中にコマンドがダラダラと並べられる
  • function が書いてないので、関数であることが分かりにくい
  • メイン処理が最後にある (ログやエラーに関する処理を最初に読まなければならない)
  • 全体的な処理概要の情報がコメントに依存している (改修していくと情報が誤りになっていく)
#!/bin/bash

# GitHub Pages 今日の運勢更新スクリプト
# 使用前に設定を変更してください

set -e  # エラーが発生したら終了

# ====== 設定項目 ======
REPO_URL="https://github.com/username/repository.git"  # GitHubリポジトリのURL
BRANCH="main"  # または "gh-pages"
HTML_FILE="index.html"  # 運勢が書かれているHTMLファイル
TEMP_DIR="/tmp/fortune_update_$$"

# ====== 運勢のパターン ======
FORTUNES=(
    "今日は素晴らしい一日になるでしょう!新しいチャンスが訪れるかもしれません。"
    "穏やかな一日です。リラックスして過ごすのが吉。"
    "少し注意が必要な日です。慎重に行動しましょう。"
    "運気上昇中!積極的に行動すると良い結果が得られそう。"
    "今日は学びの日。新しい知識を得るのに最適です。"
    "人とのつながりを大切にする日。コミュニケーションを心がけて。"
)

# ====== ログ関数 ======
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}

error_exit() {
    log "エラー: $1" >&2
    cleanup
    exit 1
}

cleanup() {
    if [ -d "$TEMP_DIR" ]; then
        log "一時ディレクトリを削除中..."
        rm -rf "$TEMP_DIR"
    fi
}

# ====== メイン処理 ======
main() {
    log "今日の運勢更新スクリプトを開始します"
    
    # 必要なコマンドの確認
    command -v git >/dev/null 2>&1 || error_exit "gitコマンドが見つかりません"
    command -v sed >/dev/null 2>&1 || error_exit "sedコマンドが見つかりません"
    
    # 一時ディレクトリ作成
    mkdir -p "$TEMP_DIR" || error_exit "一時ディレクトリの作成に失敗"
    cd "$TEMP_DIR"
    
    # リポジトリをクローン
    log "リポジトリをクローン中..."
    git clone "$REPO_URL" repo || error_exit "リポジトリのクローンに失敗"
    cd repo
    
    # 指定ブランチに切り替え
    git checkout "$BRANCH" || error_exit "ブランチ $BRANCH への切り替えに失敗"
    
    # HTMLファイルの存在確認
    if [ ! -f "$HTML_FILE" ]; then
        error_exit "HTMLファイル $HTML_FILE が見つかりません"
    fi
    
(中略)
}

# ====== スクリプト実行 ======
main "$@"

読みやすくなるプロンプトを使って生成した場合のシェルスクリプト

次に、読みやすいシェルスクリプトを AI が生成するようになる定型のプロンプトを使うと、どう出力されるか見てみましょう。

生成されたスクリプトの完全版は、GitHub にあります。

https://github.com/Takakiriy/Trials/blob/master/try_to_generate_shellscript/update-fortune-readable.sh

ここでは生成されたスクリプトの一部を示します。 その後に、入力したプロンプトを示します。

大きな違いは、全体的に概要から詳細へという流れになっていることです。 このプロンプトは、これまでに私が書いた記事「シェルスクリプト チュートリアル」や「オプション解析テンプレート」で解説したルールを使ってコードを生成するように指示しています。 具体的にどうやって読みやすくなったのかは、リンク先の記事を参照してください。

#!/bin/bash

function  ShowHelp() {
    echo  "GitHub Pages の今日の運勢をランダムに更新するスクリプト"
    echo  ""
    echo  "使用方法:"
    echo  "  ${0} [オプション] __HtmlFile__"
    echo  ""
    echo  "引数:"
    echo  "  __HtmlFile__      更新するHTMLファイルのパス"
    echo  ""
    echo  "オプション:"
    echo  "  -h, --help        このヘルプを表示"
    echo  "  -r, --repo-path   GitリポジトリのパスPATH] (デフォルト: カレントディレクトリ)"
}

#// グローバル変数
RepoPath="."
CommitMessage="Update today's fortune"
FortuneElementId="today-fortune"

#// CLIオプション解析
PositionalArgs=()
while [[ $# -gt 0 ]]; do
    case $1 in
        -h|--help) ShowHelp; exit 0;;
        -r|--repo-path) Options_RepoPath="$2"; shift; shift;;
        -*) Error  "Unknown option $1";;
        *) PositionalArgs+=("$1"); shift;;
    esac
done
set -- "${PositionalArgs[@]}"  #// set $1, $2, ...
unset PositionalArgs

function  Main() {
    local  htmlFile="$1"
    
    #// 引数チェック
    if [[ "${htmlFile}" == "" ]]; then
        Error  "HTMLファイルのパスが指定されていません"
    fi
    
    ValidateEnvironment  "${htmlFile}"
    
    local  newFortune
    newFortune=$( GenerateRandomFortune )  ||  Error  "運勢の生成に失敗しました"
    
    UpdateFortuneInHtml  "${htmlFile}"  "${newFortune}"
    
    CommitAndPushChanges  "${htmlFile}"
    
    echo  "運勢の更新が完了しました: ${newFortune}"
}

function  ValidateEnvironment() {
    local  htmlFile="$1"
    
    #// HTMLファイルの存在チェック
    if [[ ! -f "${htmlFile}" ]]; then
        Error  "HTMLファイルが見つかりません: ${htmlFile}"
    fi
    
    #// Gitリポジトリかどうかチェック
    cd  "${RepoPath}"  ||  Error  "指定されたリポジトリパスにアクセスできません: ${RepoPath}"
    
    if [[ ! -d ".git" ]]; then
        Error  "指定されたパスはGitリポジトリではありません: ${RepoPath}"
    fi
    
    #// git コマンドの存在チェック
    which  git  > /dev/null  2>&1  ||  Error  "gitコマンドが見つかりません"
}

function  GenerateRandomFortune() {
    local  fortunes=(
        "今日は素晴らしい一日になるでしょう!"
        "新しい出会いがあなたを待っています"
        "チャンスを逃さないよう注意深く行動しましょう"
        "今日は創造性が高まる日です"
        "困難な状況も乗り越えられる力があります"
        "周りの人との協力が成功の鍵になります"
    )
    
    local  randomIndex=$(( RANDOM % ${#fortunes[@]} ))
    echo  "${fortunes[${randomIndex}]}"
}

function  UpdateFortuneInHtml() {
    local  htmlFile="$1"
    local  newFortune="$2"
    
    #// HTMLファイル内の運勢要素を更新
    local  tempFile
    tempFile=$( mktemp )  ||  Error  "一時ファイルの作成に失敗しました"
    
    #// sed を使って指定されたIDの要素内のテキストを置換
    sed  -E  "s|(<[^>]*id=[\"']${FortuneElementId}[\"'][^>]*>)[^<]*(</[^>]*>)|\1${newFortune}\2|g"  "${htmlFile}"  >  "${tempFile}"  ||  Error  "HTMLファイルの更新に失敗しました"
    
    mv  "${tempFile}"  "${htmlFile}"  ||  Error  "HTMLファイルの保存に失敗しました"
}

(略)

Main  "$@"

Claude Sonnet 4 へ入力したプロンプト:

長いですが、2行目以降はコピペするだけです。

公開中の GitHub Pages の一部に書かれた今日の運勢をランダムに書き換えて公開するシェルスクリプトを作ってください。

ただし、スクリプトは以下のコーディングルールに従ってください。

- 最も上には、ヘルプを表示する関数、グローバル変数、CLI のオプションの解析をするスクリプト、Main 関数の順に書く
- 処理内容が関数名から読みやすくなる場合、関数を定義する
- 関数の引数は関数の最初で必ずローカル変数に代入する
- if 文で数値の比較をするときは (( )) で囲み、-eq などハイフンから始まる演算子ではなく == などの演算子を使う
- if 文の中で -n や -z を使わず ${var} == "" のように判定すること
- CLI のオプションの解析をするスクリプトは、if 文を使わず、添付の option_tepmlate.sh ファイルと完全に同じ形式でオプション解析部分を書いてください
- オプションの値を格納する変数名は Options_ から始めてください
- 1つのオプションに関する処理は、case に指定した変数と比較する値から最後の shift まで必ず 1行で書いてください
- Main 関数を定義する。関数コールツリーの深い関数を下のほうに書く
- インデントは空白4つ
- コメントの記号#のすぐ右に // を書くこと(#// から始める)
- ヘルプやコメントに、引数や置き換える部分を書くときは、<argument> 形式ではなく、Pascal スタイルでかき、先頭と末尾にアンダーバー2つ付ける
- exit を含む Error 関数を最も下(ただし Main 関数呼び出しより上)に定義すること
- エラーメッセージは、Error 関数の引数に渡して、それを Error 関数の中で表示すること
- set -e を使わないこと
- エラー判定には必ず __Command__  ||  Error  のように呼び出してください。
- すべての関数の中で、エラーを表す 0 以外の値を返す return を書かないで Error を呼び出してください
- 関数定義は function から始め、それに続く空白文字は2つにする
- 関数名は Pascal ケース
- グローバル変数名は Pascal ケース
- 変数を参照するときは "${ }" で囲む。ただし、関数の引数など変数名が数字のときは { } を書かず "$1" のように書く
- ローカル変数名は Camel ケースにして、local の右の空白文字は2つにする
- 呼び出す関数名やコマンド名や echo や return などの命令の右の空白文字は2つにする
- git などサブコマンドを呼び出すときは、コマンド名の右は空白文字1つ、サブコマンド名の右は空白文字2つにする
- パイプやリダイレクトの記号の前後は空白文字2つにする
- case 文は使用禁止
- $( ) で囲むコマンドは $( の右に空白文字を入れ、) の左に空白文字を入れる

添付する option_tepmlate.sh ファイル:

PositionalArgs=()
while [[ $# -gt 0 ]]; do
    case $1 in
        -s|--search-path)  Options_SearchPath="$2"; shift; shift;;  #search: bash default values
        -*) echo "Unknown option $1"; exit 1;;
        *) PositionalArgs+=("$1"); shift;;
    esac
done
set -- "${PositionalArgs[@]}"  #// set $1, $2, ...
unset PositionalArgs

echo "\$SearchPath = \"${Options_SearchPath}\""
echo "\$1          = \"$1\""
echo "\$2          = \"$2\""

ただし、指示した内容の一部は無視されてしまいます。 ですので、生成されたら次の指示も入力してください。 といっても、無視されたコーディングルールをもう一度入力するだけです。

1つのオプションに関する処理は、case に指定した変数と比較する値から最後の shift まで必ず 1行で書いてください

さいごに

処理内容をダラダラ並べられることがなく、全体を数個の処理に分けて読んでいけるので、理解がとてもしやすくなりました。 また、シェルスクリプトにありがちな謎の 1文字オプション(-n や -z など)も使われなくなります。 大枠を AI が生成した後は、具体的な指示で微調整をすることになりますが、AI と人間でそれぞれ得意な作業を分担するためには、コミュニケーションをスムーズにする形式にこだわることがとても大事です。

Discussion

余白 | yohaku余白 | yohaku

エンジニアリングの知識に乏しい私には、投稿された記事をどこまで理解できているか心もとないですが、とても興味深く読ませていただきました。