DOSコマンド勉強会 資料作成計画

対象: JavaScript の基礎文法・開発経験がある人 / Java または C# を知っている人  |  ゴール: .bat .cmd を自分で書けるようになる

📅 作成: 2026-08-24 / 更新: 2026-08-25

目次

  1. この勉強会のゴールと基本方針
  2. 対象者の前提と、そこから決まる説明順
  3. 全3回の全体構成
  4. 第1回: バッチの実行モデル
  5. 第2回: 制御構文と展開の罠
  6. 第3回: 実務で使えるバッチを書く
  7. 成果物とファイル構成
  8. 資料に必ず含める落とし穴リスト
  9. 制作手順と、確認したいこと
  10. 付録: 逆引き参照

この勉強会のゴールと基本方針

ゴール(勉強会が終わったときの状態)

  1. 渡された .bat を読んで、何をしているか説明できる
  2. 数十行規模の .bat を、テンプレートを土台に自分で書ける
  3. バッチ特有の落とし穴(遅延展開・エスケープ・文字コード)に自力で気づける
  4. 「これはバッチで書くべきではない」という判断ができる

決定した進行形式

項目決定資料への影響
分量全3回 × 各60分1回あたり本編45分+デモ10分+質疑5分。1回で1つの完結したテーマにする
進め方デモ・読み物中心参加者は環境構築なしで参加可。講師が実演し、参加者は資料を読む。演習は「宿題」として任意提示
対象範囲cmd.exe / バッチのみPowerShell との比較章は作らない。第3回の末尾で「バッチの限界」に1項だけ触れる
他言語対比入れる各回に JS / Java / C# との対比表を1つずつ配置する
テンプレート入れる第3回で「まずこれをコピー」の定型を配布。成果物として独立ファイルにする

読み物中心にするうえでの設計判断

ハンズオンをしない代わりに、資料側で以下を担保します。

対象者の前提と、そこから決まる説明順

参加者が持っているもの / 持っていないもの

持っている持っていない(=説明が必要)
変数・条件分岐・ループ・関数の概念
文字列型と数値型の区別
スコープの概念
例外処理の概念
コードを読む習慣
cmd.exe というプロセスのイメージ
「値が文字列として展開されてから実行される」モデル
終了コードで制御するという発想
Windows の文字コード事情(CP932 / CRLF)
コマンドの標準入出力とリダイレクト

説明順を決める原則

プログラミング経験者にバッチを教えるときの最大の障害は、「言語だと思って読む」ことです。バッチは構文解析してから実行する言語ではなく、1行ずつテキストを置換してからコマンドを起動する仕組みです。ここを最初に壊さないと、遅延展開もエスケープも「よく分からない例外規則」として暗記対象になってしまいます。

したがって説明順は次の通りにします。

  1. 実行モデル(第1回前半)— なぜ罠が多いのかの根拠を先に置く
  2. 変数と終了コード(第1回後半)— バッチの2大データ経路
  3. 制御構文と展開の罠(第2回)— 実行モデルの帰結として説明する
  4. 実務の型(第3回)— 罠を回避済みのテンプレートに落とす

既知言語との対比は「似ている」で終わらせない。対比表は毎回、右端に「対比が破綻する点」の列を置きます。set を「代入」と紹介するだけだと、参加者は set がスコープを持たずプロセス全体の環境変数を書き換えるという事実に、自分のコードが壊れて初めて気づくことになります。

全3回の全体構成

第1回 バッチの実行モデル 1行ずつ展開して実行する 変数 / 終了コード / 引数 → 読めるようになる 第2回 制御構文と展開の罠 if / for / call / goto 遅延展開 / エスケープ → 書けるようになる 第3回 実務で使う型 文字コード / エラー処理 ランチャー / テンプレート → 配れるようになる 各回60分(本編45分 + デモ10分 + 質疑5分) / 各回は単独でも読める構成にする

回ごとの狙いと成果

タイトルこの回の中心的な問い持ち帰るもの
第1回 バッチの実行モデル なぜバッチは他の言語と同じように読むと壊れるのか 実行モデルの図 / 変数と終了コードの対比表
第2回 制御構文と展開の罠 ループの中で変数が更新されないのはなぜか 落とし穴カタログ(動かないコードと修正の対)
第3回 実務で使えるバッチを書く 他人に渡して壊れないバッチの条件は何か テンプレート集(そのままコピーして使える)

全体を貫く「1本のサンプル」

回をまたいで同じ題材を育てる形にします。毎回まったく別の例を出すより、同じスクリプトが3回で実務品質に到達する過程を見せたほうが、読み物として最後まで読まれます。

題材スクリプトの状態
第1回指定フォルダのファイルを1つ、日付付きの名前でバックアップフォルダにコピーする(10行程度・ベタ書き)
第2回フォルダ内の複数ファイルをループ処理し、拡張子で振り分ける(for / if / サブルーチン化)
第3回引数・ログ・エラー処理・文字コード対応を備え、ダブルクリックでも動く配布可能な形にする

第1回: バッチの実行モデル

時間配分

時間内容形式
5分導入: バッチは今でも必要か / .bat.cmd の違い説明
15分実行モデル: cmd.exe が1行をどう処理するか図 + 説明
12分変数: set / %VAR% / setlocal / 全部文字列説明 + 対比表
8分終了コード: ERRORLEVEL / && / ||説明
5分引数: %1%~dp0説明
10分デモ: 題材スクリプト(バックアップ処理)を1行ずつ作る実演
5分質疑

中心に置く図: 1行が実行されるまで

1行を読む ファイルを閉じずに逐次読む 変数を文字列に置換 %VAR% をここで展開 (実行前に確定してしまう) コマンドを実行 内部コマンド or 外部 exe 終了コードを残す ERRORLEVEL に上書き 重要: 2番目の「置換」は、括弧で囲まれたブロック全体に対して一括で先に行われる → 第2回の遅延展開の話につながる

載せる対比表(第1回分)

やりたいことJSJava / C#バッチ対比が破綻する点
変数に代入 let n = 5; int n = 5; set n=5 5 は数値ではなく文字列。set /a を使ったときだけ数値として扱われる
変数を読む n n %n% 読むのは実行時ではなく「行の解析時」。未定義なら空文字ではなく %n% という文字列がそのまま残る
スコープ ブロック / 関数 ブロック / クラス setlocal ブロックスコープが存在しない。setlocal を書かないとプロセスの環境変数を汚染する
エラー通知 throw 例外 終了コード 例外がないので伝播もしない。呼び出しごとに自分で確認しないと処理が続行してしまう
コメント // // rem / :: :: は本来ラベルであり、forif の括弧内では構文エラーになる

.bat.cmd の違いは第1回で決着させる。実務上の差は、setpath といった一部の内部コマンドを実行したとき ERRORLEVEL がどう変化するかだけです。ここを曖昧にしたまま進むと、参加者は「どちらを使えばいいのか」を毎回迷い続けます。資料では「新規に作るなら .cmd、既存に合わせるならそれに従う」と結論を明示します。

第2回: 制御構文と展開の罠

時間配分

時間内容形式
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! は行の解析時ではなく実行の直前に展開されます。

載せる対比表(第2回分)

やりたいことJSJava / 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ページにする

^ & | > < % " の扱いは、都度説明すると散らばって覚えられません。「どの文字が、どの文脈でエスケープを必要とするか」の一覧表を1つ作り、そこを参照する形にします。

第3回: 実務で使えるバッチを書く

時間配分

時間内容形式
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つに全部詰めると読めない長さになります。

載せる対比表(第3回分)

やりたいこと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-引くときに使う入口の順。逆引き索引 → 早見表 → 落とし穴カタログ

エクスプローラやエディタの名前順で 010203A1A2A3 と並び、本編を読み終えた先に付録が続く形になります。付録を後から追加する場合も A4- 以降を使い、本編の連番には割り込ませません。

付録を各回から独立させる理由。勉強会中は第1〜3回を順に読みますが、勉強会後に実際にバッチを書く場面で必要になるのは「症状から引く」「記法だけ確認する」という使い方です。回の資料に埋め込むと後から引けません。各回の本文からは付録の該当項目へリンクします。

サンプルバッチの扱い

資料に必ず含める落とし穴リスト

実際にバッチを書く人が踏むものを先に列挙し、これを A3-pitfalls.html の目次とします。各項目は「症状 → 原因 → 修正」の形で書きます。

#症状原因扱う回
1ループ内で変数が更新されない括弧ブロックが実行前に一括展開される(遅延展開が必要)第2回
2変数の値の末尾に空白が入るset VAR = value と書いた。空白も値の一部になる第1回
3日本語が化ける・文字化けで実行が止まるファイルが UTF-8。バッチは CP932 で読まれる第3回
4ダブルクリックすると動くのに、別の場所から呼ぶと失敗する相対パス依存。カレントディレクトリを固定していない第3回
5パスに空白が含まれると失敗する引数を " で囲んでいない、または二重に囲んでいる第1回
6エラーが出ているのに処理が続行する終了コードを確認していない。例外は存在しない第3回
7if の中の ERRORLEVEL が期待と違うブロック内の %errorlevel% はブロック進入時の値に固定されている第2回
8exit したらコンソールごと閉じた/b を付けていない第3回
9:: コメントを入れたらループが構文エラーになった:: はラベルであり、括弧内では使えない第1回
10% を含む文字列が消えるバッチ内では %% と書く必要がある第2回
11for /f でファイル名ではなく文字列そのものが処理される引用符の有無で対象の解釈が変わる(usebackq の話)第2回
12他のバッチを呼んだらそこで処理が終わってしまったcall を付けずに呼んだため、制御が戻らない第2回
13スクリプト実行後、環境変数が汚れているsetlocal がない第1回
14UNC パス(\\server\share)でカレント移動が失敗するcd は UNC を扱えない。pushd を使う第3回

この14項目は、資料の品質検証にも使う。3回分の資料を書き終えたあと、各項目が本当にどこかで説明されているかを1つずつ突き合わせます。落とし穴の説明漏れは、資料としての価値を最も大きく損なう欠陥です。

制作手順と、確認したいこと

制作の順序

資料は「第1回から順に」ではなく、依存関係の少ないものから作ります。早見表と落とし穴カタログを先に作ると、各回の本文からリンクを張りながら書けます。

作るもの状態備考
1本計画書(docs/plan/plan.html完了
2README.html(資料一覧)完了資料が増えるたびにリンクを追加する
3A2-cheatsheet.html(記法早見表)完了各回から参照されるため最初に固める
4A3-pitfalls.html(落とし穴カタログ 14項目)完了症状 → 原因 → 修正の3点セット
5templates\ 3種のテンプレート完了第3回の骨格。動作確認も行う
6第1回資料 + サンプル完了実行モデルの図が中心
7第2回資料 + サンプル(失敗版・修正版)完了最も分量が多くなる想定
8第3回資料 + サンプル完了テンプレートを題材に適用する形にする
9付録 A1-appendix-index.html(逆引き参照)完了番号は A1 だが制作は最後。索引A(やりたいこと)/B(他言語)/C(症状)の3構成
10全体の突き合わせ完了落とし穴14項目は全項目が本編から参照済み / 全サンプルを実行して動作確認済み / 内部リンクとアンカーの切れなし

先に決めたいこと

以下は資料の内容に影響しますが、なくても着手はできます。判明した時点で反映します。

次のアクション

すべての資料・テンプレート・サンプルの作成が完了しました。制作中に実機で確認した結果、当初の計画から次の点を補強しています。

実機で判明したこと反映先
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種類の入口を用意する

「探す」と言っても、探し始める手がかりは人によって違います。次の3つを別々の索引として持ちます。

索引手がかり想定する場面
索引A
やりたいこと逆引き
日本語の目的「フォルダ内のファイルを順に処理したい」— これから書く。何を使えばいいか分からない
索引B
他言語からの逆引き
既知言語の構文「JS の for...of にあたるものは?」— 書きたい処理は頭にあり、記法だけが分からない
索引C
症状からの逆引き
エラー文・おかしな挙動この時点では予期されていません。 と出た」— 既に書いたものが動かない

索引A: やりたいこと逆引き(抜粋)

やりたいこと使うもの参照先
フォルダ内のファイルを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: 他言語からの逆引き(抜粋)

本編の各回に置く対比表は「学ぶための対比」で、対比が破綻する点の説明を含みます。索引Bは「引くための対比」なので、記法と参照先だけに絞って一覧性を優先します。

JSJava / C#バッチ参照先
console.log(x)System.out.println / Console.WriteLineecho %x%第1回
let n = 5int 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回
returnreturn;goto :eof第2回
try / catch例外処理if errorlevel 1 / ||第3回
process.argvargs[]%1%*第1回
__dirname実行パス取得%~dp0第3回
process.exit(1)System.exit(1) / Environment.Exitexit /b 1第3回
s.substring(0,3)substring%s:~0,3%第2回
s.replace(...)replace%s:a=b%第2回
process.env.Xgetenv%X%第1回
(該当なし)配列 / List / Map存在しない第3回(バッチの限界)

索引C: 症状からの逆引き(抜粋)

参照先は落とし穴カタログの項番(本計画の第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列だけに保ちます。ここに説明を書き始めると本編と内容が二重管理になり、片方だけ直して食い違うことになります。エラーメッセージの文言は環境によって差が出るため、資料作成時に実機で発生させて確認したものだけを載せます。