.bat を読んで、何をしているか説明できる.bat を、テンプレートを土台に自分で書ける| 項目 | 決定 | 資料への影響 |
|---|---|---|
| 分量 | 全3回 × 各60分 | 1回あたり本編45分+デモ10分+質疑5分。1回で1つの完結したテーマにする |
| 進め方 | デモ・読み物中心 | 参加者は環境構築なしで参加可。講師が実演し、参加者は資料を読む。演習は「宿題」として任意提示 |
| 対象範囲 | cmd.exe / バッチのみ | PowerShell との比較章は作らない。第3回の末尾で「バッチの限界」に1項だけ触れる |
| 他言語対比 | 入れる | 各回に JS / Java / C# との対比表を1つずつ配置する |
| テンプレート | 入れる | 第3回で「まずこれをコピー」の定型を配布。成果物として独立ファイルにする |
ハンズオンをしない代わりに、資料側で以下を担保します。
| 持っている | 持っていない(=説明が必要) |
|---|---|
| 変数・条件分岐・ループ・関数の概念 文字列型と数値型の区別 スコープの概念 例外処理の概念 コードを読む習慣 |
cmd.exe というプロセスのイメージ 「値が文字列として展開されてから実行される」モデル 終了コードで制御するという発想 Windows の文字コード事情(CP932 / CRLF) コマンドの標準入出力とリダイレクト |
プログラミング経験者にバッチを教えるときの最大の障害は、「言語だと思って読む」ことです。バッチは構文解析してから実行する言語ではなく、1行ずつテキストを置換してからコマンドを起動する仕組みです。ここを最初に壊さないと、遅延展開もエスケープも「よく分からない例外規則」として暗記対象になってしまいます。
したがって説明順は次の通りにします。
既知言語との対比は「似ている」で終わらせない。対比表は毎回、右端に「対比が破綻する点」の列を置きます。set を「代入」と紹介するだけだと、参加者は set がスコープを持たずプロセス全体の環境変数を書き換えるという事実に、自分のコードが壊れて初めて気づくことになります。
| 回 | タイトル | この回の中心的な問い | 持ち帰るもの |
|---|---|---|---|
| 第1回 | バッチの実行モデル | なぜバッチは他の言語と同じように読むと壊れるのか | 実行モデルの図 / 変数と終了コードの対比表 |
| 第2回 | 制御構文と展開の罠 | ループの中で変数が更新されないのはなぜか | 落とし穴カタログ(動かないコードと修正の対) |
| 第3回 | 実務で使えるバッチを書く | 他人に渡して壊れないバッチの条件は何か | テンプレート集(そのままコピーして使える) |
回をまたいで同じ題材を育てる形にします。毎回まったく別の例を出すより、同じスクリプトが3回で実務品質に到達する過程を見せたほうが、読み物として最後まで読まれます。
| 回 | 題材スクリプトの状態 |
|---|---|
| 第1回 | 指定フォルダのファイルを1つ、日付付きの名前でバックアップフォルダにコピーする(10行程度・ベタ書き) |
| 第2回 | フォルダ内の複数ファイルをループ処理し、拡張子で振り分ける(for / if / サブルーチン化) |
| 第3回 | 引数・ログ・エラー処理・文字コード対応を備え、ダブルクリックでも動く配布可能な形にする |
| 時間 | 内容 | 形式 |
|---|---|---|
| 5分 | 導入: バッチは今でも必要か / .bat と .cmd の違い | 説明 |
| 15分 | 実行モデル: cmd.exe が1行をどう処理するか | 図 + 説明 |
| 12分 | 変数: set / %VAR% / setlocal / 全部文字列 | 説明 + 対比表 |
| 8分 | 終了コード: ERRORLEVEL / && / || | 説明 |
| 5分 | 引数: %1 と %~dp0 | 説明 |
| 10分 | デモ: 題材スクリプト(バックアップ処理)を1行ずつ作る | 実演 |
| 5分 | 質疑 | — |
| やりたいこと | JS | Java / C# | バッチ | 対比が破綻する点 |
|---|---|---|---|---|
| 変数に代入 | let n = 5; |
int n = 5; |
set n=5 |
5 は数値ではなく文字列。set /a を使ったときだけ数値として扱われる |
| 変数を読む | n |
n |
%n% |
読むのは実行時ではなく「行の解析時」。未定義なら空文字ではなく %n% という文字列がそのまま残る |
| スコープ | ブロック / 関数 | ブロック / クラス | setlocal |
ブロックスコープが存在しない。setlocal を書かないとプロセスの環境変数を汚染する |
| エラー通知 | throw |
例外 | 終了コード | 例外がないので伝播もしない。呼び出しごとに自分で確認しないと処理が続行してしまう |
| コメント | // |
// |
rem / :: |
:: は本来ラベルであり、for や if の括弧内では構文エラーになる |
.bat と .cmd の違いは第1回で決着させる。実務上の差は、set や path といった一部の内部コマンドを実行したとき ERRORLEVEL がどう変化するかだけです。ここを曖昧にしたまま進むと、参加者は「どちらを使えばいいのか」を毎回迷い続けます。資料では「新規に作るなら .cmd、既存に合わせるならそれに従う」と結論を明示します。
| 時間 | 内容 | 形式 |
|---|---|---|
| 3分 | 前回の復習: 実行モデルの図を再掲 | 説明 |
| 10分 | if: 比較演算子 / defined / exist / else の書き方の制約 | 説明 |
| 12分 | for: 4つの形(無印 / /l / /f / /r)と tokens delims | 説明 + 対比表 |
| 12分 | 遅延展開: なぜ !VAR! が必要か(この回の山場) | 誤りコード → 修正 |
| 8分 | 関数化: call :label / goto :eof / 戻り値の受け渡し | 説明 |
| 10分 | デモ: 題材スクリプトをループ + 振り分け対応にする | 実演 |
| 5分 | 質疑 | — |
この回の中心はここです。「動かないコードを見せる → 出力を見せる → 実行モデルの図に戻って原因を示す → 修正する」の順に書きます。
@echo off
setlocal
set count=0
for %%f in (*.txt) do (
set /a count=count+1
echo 現在: %count%
)
echo 合計: %count%
動かない3ファイルあっても 現在: 0 が3回出て、合計: 3 になります。for の括弧内は、ループが始まる前に丸ごと1回だけ文字列置換されるため、%count% はすべて 0 に確定済みです。
@echo off
setlocal enabledelayedexpansion
set count=0
for %%f in (*.txt) do (
set /a count=count+1
echo 現在: !count!
)
echo 合計: !count!
動く!count! は行の解析時ではなく実行の直前に展開されます。
| やりたいこと | JS | Java / C# | バッチ | 対比が破綻する点 |
|---|---|---|---|---|
| 配列を回す | for (const x of arr) |
for (var x : arr) |
for %%f in (...) do |
配列型が存在しない。回せるのはファイル名・空白区切り文字列・行のいずれか |
| 回数ループ | for (let i=0;i<n;i++) |
同じ | for /l %%i in (1,1,n) do |
上限に変数を直接書けない場面がある(展開タイミングの問題) |
| 行を読む | split("\n") |
BufferedReader |
for /f |
空行が黙って飛ばされる。行頭が ; の行も既定で無視される |
| 関数を呼ぶ | f(a) |
f(a) |
call :f a |
戻り値の仕組みがない。ERRORLEVEL か呼び出し元の変数を書き換えて返す |
| 早期 return | return; |
return; |
goto :eof |
サブルーチン内では戻り、トップレベルではスクリプト終了。同じ記述で意味が変わる |
| 文字列の部分取得 | s.slice(0,3) |
substring |
%s:~0,3% |
この記法は %VAR% にしか使えず、%1 などの引数には直接使えない |
^ & | > < % " の扱いは、都度説明すると散らばって覚えられません。「どの文字が、どの文脈でエスケープを必要とするか」の一覧表を1つ作り、そこを参照する形にします。
| 時間 | 内容 | 形式 |
|---|---|---|
| 3分 | この回の位置づけ: 「動く」から「渡せる」へ | 説明 |
| 10分 | 文字コードと改行: CP932 / CRLF / chcp 65001 の是非 | 説明 + 実演 |
| 10分 | 実行環境の固定: %~dp0 / pushd / ダブルクリック実行 | 説明 |
| 10分 | エラー処理の型: || exit /b / errorlevel の判定 / ログ出力 | 説明 |
| 8分 | 引数処理とヘルプ表示の定型 | 説明 |
| 10分 | デモ: 題材スクリプトを配布可能な形に仕上げる(テンプレート適用) | 実演 |
| 4分 | バッチの限界: これ以上は別の道具を使う判断基準 | 説明 |
| 5分 | 質疑 | — |
第3回の実質的な成果物です。参加者が翌日からコピーして使えることを最優先にし、行ごとに「なぜ必要か」のコメントを日本語で入れます。
@echo off
rem ==== 実行環境を固定する ====
rem ダブルクリック実行だとカレントがバッチの場所とは限らないため、必ず移動する
pushd "%~dp0" || exit /b 1
rem ==== 変数の影響をこのスクリプト内に閉じる ====
setlocal enabledelayedexpansion
rem ==== 引数チェック ====
if "%~1"=="" (
echo 使い方: %~nx0 ^<対象フォルダ^>
exit /b 1
)
set "TARGET=%~1"
if not exist "!TARGET!\" (
echo エラー: フォルダが見つかりません: !TARGET!
exit /b 1
)
rem ==== 本処理 ====
call :main
set "RC=!errorlevel!"
rem ==== 後始末(成功・失敗にかかわらず通る) ====
endlocal & set "RC=%RC%"
popd
exit /b %RC%
:main
rem ここに処理を書く。失敗したら exit /b で 0 以外を返す
goto :eof
テンプレートは1種類にしない。上の「引数を取るツール型」に加えて、(1) 他のスクリプトや exe を起動するだけのランチャー型、(2) 定時実行を前提にログを残すバッチ処理型 の3種を用意します。用途が違うと必要な定型も違い、1つに全部詰めると読めない長さになります。
| やりたいこと | JS / Java / C# | バッチ | 注意点 |
|---|---|---|---|
| 失敗したら中断 | 例外が自動で伝播 | cmd || exit /b 1 |
1行ごとに書く必要がある。書き忘れた行は黙って通過する |
| スクリプトの場所を得る | __dirname 相当 |
%~dp0 |
末尾に \ が付くので "%~dp0file" と連結する。"%~dp0\file" は誤り |
| 標準出力をファイルへ | ロガー | >> log.txt 2>&1 |
順序が重要。2>&1 >> log.txt では標準エラーが画面に残る |
| 終了コードを返す | process.exit(n) |
exit /b n |
/b を忘れると cmd.exe ごと閉じる。呼び出し元から使われる想定なら致命的 |
PowerShell との比較章は作らない方針ですが、判断基準だけは示します。配列や連想配列が必要、JSON を扱う、エラー処理が数階層になる、文字列処理が主目的のいずれかに該当したらバッチで書くべきではない、という4条件に絞って提示します。
20260824-dos-command-learn\
├── README.html … 資料一覧(各資料への入口)
├── docs\
│ ├── plan\
│ │ └── plan.html … 本計画書
│ ├── 01-execution-model.html … 第1回 バッチの実行モデル
│ ├── 02-control-flow.html … 第2回 制御構文と展開の罠
│ ├── 03-practical.html … 第3回 実務で使えるバッチを書く
│ ├── A1-appendix-index.html … 付録: 逆引き参照(3種の索引 / 詳細は各資料へリンク)
│ ├── A2-cheatsheet.html … 付録: 記法早見表(変数展開・エスケープ・for のオプション)
│ └── A3-pitfalls.html … 付録: 落とし穴カタログ(症状 → 原因 → 修正)
├── samples\
│ ├── 01\ … 03\ … 各回のデモで使うバッチ(回ごとに完成版を置く)
│ └── README.html … サンプルの実行方法
└── templates\
├── tool.cmd … 引数を取るツール型
├── launcher.cmd … 他のスクリプト・exe を起動するランチャー型
└── batchjob.cmd … 定時実行・ログ出力前提のバッチ処理型
本編と付録が名前順で混ざらないよう、接頭辞で系列を分けます。
| 系列 | 接頭辞 | 並び順の意味 |
|---|---|---|
| 本編 | 01- 〜 03- | 勉強会で読む順(=学ぶ順) |
| 付録 | A1- 〜 A3- | 引くときに使う入口の順。逆引き索引 → 早見表 → 落とし穴カタログ |
エクスプローラやエディタの名前順で 01 → 02 → 03 → A1 → A2 → A3 と並び、本編を読み終えた先に付録が続く形になります。付録を後から追加する場合も A4- 以降を使い、本編の連番には割り込ませません。
付録を各回から独立させる理由。勉強会中は第1〜3回を順に読みますが、勉強会後に実際にバッチを書く場面で必要になるのは「症状から引く」「記法だけ確認する」という使い方です。回の資料に埋め込むと後から引けません。各回の本文からは付録の該当項目へリンクします。
samples\ 配下に閉じた動作にする。外部フォルダを書き換えるサンプルは作らない(参加者が試したときの事故を避ける)実際にバッチを書く人が踏むものを先に列挙し、これを A3-pitfalls.html の目次とします。各項目は「症状 → 原因 → 修正」の形で書きます。
| # | 症状 | 原因 | 扱う回 |
|---|---|---|---|
| 1 | ループ内で変数が更新されない | 括弧ブロックが実行前に一括展開される(遅延展開が必要) | 第2回 |
| 2 | 変数の値の末尾に空白が入る | set VAR = value と書いた。空白も値の一部になる | 第1回 |
| 3 | 日本語が化ける・文字化けで実行が止まる | ファイルが UTF-8。バッチは CP932 で読まれる | 第3回 |
| 4 | ダブルクリックすると動くのに、別の場所から呼ぶと失敗する | 相対パス依存。カレントディレクトリを固定していない | 第3回 |
| 5 | パスに空白が含まれると失敗する | 引数を " で囲んでいない、または二重に囲んでいる | 第1回 |
| 6 | エラーが出ているのに処理が続行する | 終了コードを確認していない。例外は存在しない | 第3回 |
| 7 | if の中の ERRORLEVEL が期待と違う | ブロック内の %errorlevel% はブロック進入時の値に固定されている | 第2回 |
| 8 | exit したらコンソールごと閉じた | /b を付けていない | 第3回 |
| 9 | :: コメントを入れたらループが構文エラーになった | :: はラベルであり、括弧内では使えない | 第1回 |
| 10 | % を含む文字列が消える | バッチ内では %% と書く必要がある | 第2回 |
| 11 | for /f でファイル名ではなく文字列そのものが処理される | 引用符の有無で対象の解釈が変わる(usebackq の話) | 第2回 |
| 12 | 他のバッチを呼んだらそこで処理が終わってしまった | call を付けずに呼んだため、制御が戻らない | 第2回 |
| 13 | スクリプト実行後、環境変数が汚れている | setlocal がない | 第1回 |
| 14 | UNC パス(\\server\share)でカレント移動が失敗する | cd は UNC を扱えない。pushd を使う | 第3回 |
この14項目は、資料の品質検証にも使う。3回分の資料を書き終えたあと、各項目が本当にどこかで説明されているかを1つずつ突き合わせます。落とし穴の説明漏れは、資料としての価値を最も大きく損なう欠陥です。
資料は「第1回から順に」ではなく、依存関係の少ないものから作ります。早見表と落とし穴カタログを先に作ると、各回の本文からリンクを張りながら書けます。
| 順 | 作るもの | 状態 | 備考 |
|---|---|---|---|
| 1 | 本計画書(docs/plan/plan.html) | 完了 | — |
| 2 | README.html(資料一覧) | 完了 | 資料が増えるたびにリンクを追加する |
| 3 | A2-cheatsheet.html(記法早見表) | 完了 | 各回から参照されるため最初に固める |
| 4 | A3-pitfalls.html(落とし穴カタログ 14項目) | 完了 | 症状 → 原因 → 修正の3点セット |
| 5 | templates\ 3種のテンプレート | 完了 | 第3回の骨格。動作確認も行う |
| 6 | 第1回資料 + サンプル | 完了 | 実行モデルの図が中心 |
| 7 | 第2回資料 + サンプル(失敗版・修正版) | 完了 | 最も分量が多くなる想定 |
| 8 | 第3回資料 + サンプル | 完了 | テンプレートを題材に適用する形にする |
| 9 | 付録 A1-appendix-index.html(逆引き参照) | 完了 | 番号は A1 だが制作は最後。索引A(やりたいこと)/B(他言語)/C(症状)の3構成 |
| 10 | 全体の突き合わせ | 完了 | 落とし穴14項目は全項目が本編から参照済み / 全サンプルを実行して動作確認済み / 内部リンクとアンカーの切れなし |
以下は資料の内容に影響しますが、なくても着手はできます。判明した時点で反映します。
where や curl のような比較的新しく標準搭載されたコマンドをデモで使うのは避けますすべての資料・テンプレート・サンプルの作成が完了しました。制作中に実機で確認した結果、当初の計画から次の点を補強しています。
| 実機で判明したこと | 反映先 |
|---|---|
set "名前=値" は代入を守るが、%VAR% での読み出し時に再解析されて壊れる | 第1回 2章 / 第2回 4章 |
遅延展開が有効なとき ! のリテラル表示には ^^! が必要(^! では展開される) | 第2回 4章・7章 / 早見表 3章 |
rem のコメント行でも % の解析が行われ、無効な修飾子だとバッチが停止する | 第1回 2章 |
| 化けるかどうかはファイルの文字コードとコンソールのコードページの組み合わせで決まる | 第3回 2章 / 落とし穴 #3 |
| 空の変数に置換記法を使うと予期しない文字列が残る | 第2回 8章 |
%TIME% は時が1桁だと先頭が空白になる(%TIME: =0% で対処) | 第1回 3章 / 第3回 3章 |
wmic は動作するが非推奨。for /f で読むと末尾に余分な文字が混入する | 第3回 3章 |
開催日が決まり次第、当日の進行(デモの順序と実演環境)を確認します。参加者の Windows 環境と配布方法が判明した場合も、該当箇所を見直します。
本編(第1〜3回)は学ぶ順に並んでいますが、勉強会後に実際にバッチを書く人が必要とするのは探す順です。この2つは並び方が根本的に違うため、同じ文書では両立できません。そこで付録として逆引き専用のページ(A1-appendix-index.html)を用意します。
「探す」と言っても、探し始める手がかりは人によって違います。次の3つを別々の索引として持ちます。
| 索引 | 手がかり | 想定する場面 |
|---|---|---|
| 索引A やりたいこと逆引き | 日本語の目的 | 「フォルダ内のファイルを順に処理したい」— これから書く。何を使えばいいか分からない |
| 索引B 他言語からの逆引き | 既知言語の構文 | 「JS の for...of にあたるものは?」— 書きたい処理は頭にあり、記法だけが分からない |
| 索引C 症状からの逆引き | エラー文・おかしな挙動 | 「この時点では予期されていません。 と出た」— 既に書いたものが動かない |
| やりたいこと | 使うもの | 参照先 |
|---|---|---|
| フォルダ内のファイルを1つずつ処理したい | for %%f in (...) / for /r | 第2回 |
| テキストファイルを1行ずつ読みたい | for /f | 第2回 |
| ファイル・フォルダの存在を確認したい | if exist | 第2回 |
| 引数を受け取りたい / 省略時にヘルプを出したい | %~1 / if "%~1"=="" | 第1回・第3回 |
| スクリプトと同じ場所のファイルを参照したい | "%~dp0file" | 第3回 |
| 処理が失敗したらそこで止めたい | || exit /b 1 | 第3回 |
| 数値を数えたい・計算したい | set /a | 第1回 |
| 文字列の一部を取り出したい / 置換したい | %V:~0,3% / %V:a=b% | 第2回 |
| 処理を関数のようにまとめたい | call :label / goto :eof | 第2回 |
| 画面の出力をログファイルに残したい | >> log.txt 2>&1 | 第3回 |
| 日付入りのファイル名を作りたい | 日付取得の定型 | 第3回 |
| 共有フォルダ(UNC パス)を扱いたい | pushd / popd | 第3回 |
本編の各回に置く対比表は「学ぶための対比」で、対比が破綻する点の説明を含みます。索引Bは「引くための対比」なので、記法と参照先だけに絞って一覧性を優先します。
| JS | Java / C# | バッチ | 参照先 |
|---|---|---|---|
console.log(x) | System.out.println / Console.WriteLine | echo %x% | 第1回 |
let n = 5 | int n = 5; | set n=5 | 第1回 |
if (a === b) | if (a.equals(b)) | if "%a%"=="%b%" | 第2回 |
for (const x of arr) | for (var x : arr) | for %%x in (...) do | 第2回 |
for (let i=0;i<n;i++) | 同じ | for /l %%i in (1,1,n) | 第2回 |
function f() {} | メソッド | :f ラベル + call :f | 第2回 |
return | return; | goto :eof | 第2回 |
try / catch | 例外処理 | if errorlevel 1 / || | 第3回 |
process.argv | args[] | %1 … %* | 第1回 |
__dirname | 実行パス取得 | %~dp0 | 第3回 |
process.exit(1) | System.exit(1) / Environment.Exit | exit /b 1 | 第3回 |
s.substring(0,3) | substring | %s:~0,3% | 第2回 |
s.replace(...) | replace | %s:a=b% | 第2回 |
process.env.X | getenv | %X% | 第1回 |
| (該当なし) | 配列 / List / Map | 存在しない | 第3回(バッチの限界) |
参照先は落とし穴カタログの項番(本計画の第8章のリストと同じ番号)です。
| 症状・目にするメッセージ | まず疑うところ | 落とし穴 # |
|---|---|---|
| ループ内の出力が初期値のまま変わらない | 遅延展開(%VAR% を !VAR! に) | 1 |
| 比較が常に成立しない / 値の末尾に空白が付く | set VAR = value の空白 | 2 |
日本語が ? や別の文字になる | ファイルの文字コード(UTF-8 で保存していないか) | 3 |
指定されたパスが見つかりません。 | カレントディレクトリ / %~dp0 の連結 | 4・5 |
この時点では予期されていません。 | 括弧・引用符・エスケープ(& | %) | 10 |
コマンドの構文が誤っています。 | 引数の引用符 / 空白を含むパス | 5 |
| ラベルが見つからない旨のメッセージ | goto 先のスペル / :: を括弧内に書いていないか | 9 |
| エラーが出ているのに次の処理に進む | 終了コードを確認していない | 6・7 |
| 実行後、ウィンドウが一瞬で閉じる | exit に /b がない / pause がない | 8 |
| 呼び出したバッチから戻ってこない | call を付けずに呼んでいる | 12 |
| 実行後、環境変数が書き換わっている | setlocal がない | 13 |
索引には解説を書かない。付録の各行は「手がかり → 記法 → 参照先」の3列だけに保ちます。ここに説明を書き始めると本編と内容が二重管理になり、片方だけ直して食い違うことになります。エラーメッセージの文言は環境によって差が出るため、資料作成時に実機で発生させて確認したものだけを載せます。