こんにちは、おうどんです🍜
夜間バックアップのジョブを作る。 中身は robocopy 1行だけの、シンプルな処理です。 手で動かしたら、ファイルはちゃんとコピーされた。
翌朝。 ジョブ管理の画面を開く。
真っ赤。
……待って。 ファイル、ちゃんとあるよ? ログにも「コピー済み」って書いてあるよ?🤔
説明用の架空の場面ですが、robocopy をジョブやスクリプトに組み込むと、かなりの確率で出会う現象です。
犯人は、robocopy の終了コード(プログラムが終わるときに、呼び出し元へ返す数字)。 robocopy は、全部うまくコピーできたときに 1 を返すんです。
そして世の中の多くの仕組みは、「0 なら成功、それ以外は失敗」という約束で動いています。 robocopy「全部コピーできました!(1)」 ジョブ管理「1? はい失敗」
どっちも自分のルールでは正しい。 いちばんこまるやつです。。。
今回は、robocopy の終了コードの読み方と、PowerShell・バッチで本当の失敗だけを失敗にする書き方をまとめます。
結論:まずはこれだけ
ひとことで言うと、robocopy の終了コードは0〜7が成功系、8以上が失敗です。
- robocopy の終了コードは「成功か失敗か」ではなく「何が起きたかの報告」。1 は「コピーしました」
- 失敗の判定は「0 以外」ではなく「8以上」で行う(PowerShell なら
$LASTEXITCODE -ge 8) - ジョブから呼ぶときは、ラッパー(robocopy を包んで結果を翻訳するスクリプト)で 8 未満を 0 に直して返す
「え、1 で成功なの? 0 じゃなくて?」
はい。 うどん屋さんで「1」と言われたら、1杯目ができたって意味なんです。 「1番の失敗です」じゃない。
robocopy の終了コードは「結果の報告」
ひとことで言うと、robocopy の終了コードは通知表ではなく、作業報告書なんです。
Microsoft Learn の robocopy のリファレンスには、終了コードの表が載っています。 日本語にするとこうです(意訳)。
| 値 | 意味 |
|---|---|
| 0 | コピーしたファイルはない。失敗もない。不一致もない(コピー先にもう同じファイルがあったので、スキップした) |
| 1 | すべてのファイルを正常にコピーした |
| 2 | コピー先に、コピー元にない余分なファイルがある。コピーしたファイルはない |
| 3 | いくつかコピーした。余分なファイルもあった。失敗はない |
| 5 | いくつかコピーした。不一致のファイルもあった。失敗はない |
| 6 | 余分なファイルと不一致のファイルがある。コピーしたファイルはなく、失敗もない |
| 7 | コピーした。不一致もあった。余分なファイルもあった |
| 8 | いくつかのファイルがコピーできなかった |
そして表の下に、こう書かれています。
8 以上の値は、コピー処理中に少なくとも1つ失敗があったことを示す(意訳)
同じ表はMicrosoft のサポート記事(Return codes used by the Robocopy utility)にも載っています。
用語を少しだけ補足します。
- 余分なファイル(Extra):コピー先にだけあって、コピー元にはないファイル
- 不一致(Mismatch):公式の表には詳しい定義がありません。今回の検証では、コピー元の
m.txtというファイルと同じ名前のフォルダがコピー先にあったときに、この扱いになりました
つまり 0〜7 は、「コピーした/しなかった」「余分があった」「不一致があった」という状況の報告で、失敗ではありません。 失敗は 8 から。
「2 って、コピー先に余分なファイルがあるってだけで、別に悪いことじゃないのでは?」
そうなんです。 「冷蔵庫に、頼んでないプリンが入ってました」という報告なんですよね。 それでジョブが止まるのは困る。
実際に試してみる
今回の検証環境(日本語 Windows 10 Pro 22H2 / Windows PowerShell 5.1)で確かめます。 まずは、同じコピーを2回続けて実行します。 この例はスクリプトと同じ場所にある w2 フォルダを中身ごと削除して作り直すので、専用のテストフォルダで試してくださいね。
# demo.ps1
Set-Location $PSScriptRoot
if (Test-Path w2) { Remove-Item -Recurse -Force w2 }
New-Item -ItemType Directory w2\src | Out-Null
Set-Content w2\src\menu.txt 'kitsune udon'
robocopy w2\src w2\dst /E /NJH /NJS /NFL /NDL /NP | Out-Null
"1st: LASTEXITCODE=$LASTEXITCODE"
robocopy w2\src w2\dst /E /NJH /NJS /NFL /NDL /NP | Out-Null
"2nd: LASTEXITCODE=$LASTEXITCODE"
1st: LASTEXITCODE=1
2nd: LASTEXITCODE=0
$LASTEXITCODE は、PowerShell が「最後に動かした外部プログラムの終了コード」を入れておく変数です(about_Automatic_Variables)。
1回目はコピーしたので 1。 2回目はもう同じファイルがあるのでスキップして 0。 コピーした日のほうが数字が大きいんです。 働いたほうが怒られる職場、ちょっと泣きます。。。
ほかの状況も、検証用のスクリプトで作って確かめました(結果だけをまとめたものです)。
| 作った状況 | 終了コード |
|---|---|
| 空のコピー先に初めてコピー | 1 |
| もう一度そのまま実行(差分なし) | 0 |
コピー先にだけ old.txt を置いた | 2 |
コピー元に新しいファイル+コピー先に old.txt | 3 |
コピー先に m.txt という名前のフォルダを作り、コピー元には m.txt ファイル(old.txt も残したまま) | 6 |
コピー元のファイルを別プロセスで開いたまま(共有なしでロック)、/R:0 /W:0 で実行 | 8 |
| 存在しないフォルダをコピー元に指定 | 16 |
最後の 16 は Microsoft の表にない値で、今回の検証環境での観測です。 ドキュメントで言えるのは「8 以上は失敗」までですが、その判定で拾えます。
PowerShell の $? も「失敗」と言う
PowerShell には、直前のコマンドが成功したかを入れておく $? という変数もあります。 about_Automatic_Variables によると、外部プログラムの場合は $LASTEXITCODE が 0 のときだけ True、それ以外は False です。
demo.ps1 と同じ準備(フォルダ名は w6)をして、1回目の robocopy の直後にこう書いて確かめました。
robocopy w6\src w6\dst /E /NJH /NJS /NFL /NDL /NP | Out-Null $ok = $? "LASTEXITCODE = $LASTEXITCODE" "`$? = $ok"
LASTEXITCODE = 1
$? = False
コピーは成功しているのに、$? は False。 if ($?) { ... } で成功判定を書いていると、コピーした日は「失敗」側に進みます。
ちなみに $ok = $? とすぐ保存しているのは、$? が直前のコマンドの結果だからです。 間に "LASTEXITCODE = ..." の行をはさむと、そっちの結果(成功)で上書きされてしまいます。 $? は、金魚くらいの記憶力だと思っておきましょう。
PowerShell で「8以上だけ失敗」にする
ひとことで言うと、robocopy の報告を、ジョブ向けの言葉に翻訳してから返すんです。
robocopy をラッパースクリプトで包みます。
# backup.ps1
param(
[string]$Source = 'C:\data',
[string]$Dest = 'D:\backup\data',
[string]$Log = 'C:\logs\backup.log'
)
robocopy $Source $Dest /E /R:2 /W:5 /NP /LOG+:$Log
$rc = $LASTEXITCODE
if ($rc -ge 8) {
Write-Output "robocopy failed: exit code $rc"
exit $rc
}
Write-Output "robocopy OK: exit code $rc"
exit 0
robocopy の直後に $rc へ保存し(別の外部プログラムを動かすと上書きされるため)、8 以上は元の値のまま、8 未満は 0 で返します。
powershell.exe -File でスクリプトを呼ぶと、exit に書いた値がそのまま終了コードになります(about_Automatic_Variables の $LASTEXITCODE の項)。
検証では、別の PowerShell からこう呼び出して、呼び出し側の $LASTEXITCODE を表示しました。
& powershell -NoProfile -ExecutionPolicy Bypass -File .\backup.ps1 -Source $src -Dest $dst -Log $log "caller sees LASTEXITCODE=$LASTEXITCODE"
普通にコピーできたとき:
ログ ファイル: C:\...\w3\logs\backup.log
robocopy OK: exit code 1
caller sees LASTEXITCODE=0
コピー元のファイルをロックしておいたとき:
ログ ファイル: C:\...\w3\logs\backup.log
robocopy failed: exit code 8
caller sees LASTEXITCODE=8
(ログファイルのパスの途中は ... に省略。1行目は /LOG+: を付けたときに robocopy が画面に出すメッセージです)
1 は 0 に翻訳され、失敗の 8 は 8 のまま届きました。
通訳さん、いい仕事🍵
バッチファイル(cmd)で「8以上だけ失敗」にする
ひとことで言うと、if errorlevel 1 は「1 以上なら」という意味なので、robocopy とは相性が悪いんです。
cmd では、終了コードが ERRORLEVEL に入ります。 Microsoft Learn の if コマンドのリファレンスによると、if errorlevel <数字> は「直前のプログラムの終了コードが、その数字以上なら真」です。
つまり if errorlevel 1 は、robocopy の成功(1〜7)を全部エラー扱いにします。 実際に確かめました。
@echo off robocopy "%~1" "%~2" /E /NJH /NJS /NFL /NDL /NP >nul echo ERRORLEVEL=%ERRORLEVEL% if errorlevel 1 (echo if errorlevel 1 : NG) else (echo if errorlevel 1 : OK) if errorlevel 8 (echo if errorlevel 8 : NG) else (echo if errorlevel 8 : OK)
1回目(コピーした):
ERRORLEVEL=1
if errorlevel 1 : NG
if errorlevel 8 : OK
2回目(差分なし)は ERRORLEVEL=0 で、どちらも OK でした。 if errorlevel 1 だと、コピーした日だけ NG になります。 バックアップが仕事をした日だけ怒られる。 理不尽の極み。
ジョブから呼ぶバッチなら、PowerShell 版と同じく翻訳して返します。
@echo off
robocopy "%~1" "%~2" /E /R:2 /W:5 /NP /LOG+:"%~3"
set RC=%ERRORLEVEL%
if %RC% GEQ 8 (
echo robocopy failed: exit code %RC%
exit /b %RC%
)
echo robocopy OK: exit code %RC%
exit /b 0
GEQ は「以上」を表す比較演算子です(同じく if のリファレンスより)。 set RC=%ERRORLEVEL% で先に値を保存しているのは、PowerShell 版の $rc と同じ理由です。
検証では、このバッチを PowerShell から cmd /c で呼び、呼び出し側の $LASTEXITCODE を見ました。 普通にコピーできたときは robocopy OK: exit code 1 と表示されて呼び出し側は 0、ロックしたファイルがあるときは robocopy failed: exit code 8 と表示されて呼び出し側は 8 でした。
🔰 ここまで読めば今日から困らない
- robocopy は 0〜7 が成功系、8以上が失敗
- 判定は
-ge 8(PowerShell)/GEQ 8(バッチ) - ジョブから呼ぶなら、ラッパーで 8 未満を 0 に直して返す
これで、朝の真っ赤な画面とはお別れです。
ここから先は中級者向け。読み飛ばしてもOKです。
もう一歩:終了コードを分解して読む
ひとことで言うと、robocopy の終了コードは、いくつかのスイッチのオン・オフを1つの数字にまとめたものとして読めます。
さっきの表をよく見ると、足し算になっているんです。
- 3 = 1(コピーした)+ 2(余分あり)
- 5 = 1(コピーした)+ 4(不一致あり)
- 6 = 2(余分あり)+ 4(不一致あり)
- 7 = 1 + 2 + 4
表には 4 単体の行はありませんが、5 と 6 の説明から、4 が「不一致」を表していると読み取れます。
これはビット(2進数の各けた。0 か 1 のスイッチ)の考え方です。 2進数にすると、1 は 001、2 は 010、4 は 100。 けたがかぶらないので、足しても情報が混ざりません。
PowerShell の -band(ビットごとの AND。そのけたが立っているかを調べる演算子)で分解してみます。
# decode.ps1
function Get-RobocopyResult([int]$Code) {
$flags = @()
if ($Code -band 1) { $flags += 'Copied' }
if ($Code -band 2) { $flags += 'Extra' }
if ($Code -band 4) { $flags += 'Mismatch' }
if ($Code -band 8) { $flags += 'Failed' }
if ($Code -band 16) { $flags += 'Fatal' }
[pscustomobject]@{
Code = $Code
Binary = [Convert]::ToString($Code, 2).PadLeft(5, '0')
Flags = ($flags -join ',')
Result = if ($Code -ge 8) { 'NG' } else { 'OK' }
}
}
0, 1, 2, 3, 6, 8, 9, 16 | ForEach-Object { Get-RobocopyResult $_ } | Format-Table -AutoSize
Code Binary Flags Result
---- ------ ----- ------
0 00000 OK
1 00001 Copied OK
2 00010 Extra OK
3 00011 Copied,Extra OK
6 00110 Extra,Mismatch OK
8 01000 Failed NG
9 01001 Copied,Failed NG
16 10000 Fatal NG
(前後の空行は省いています)
Copied などのラベルは、表を読んでこの記事で付けた名前です(16 の Fatal は、今回の観測からの仮の名前)。 判定(8 以上は NG)はドキュメントどおりです。
ログに exit code 9 と出ていたら、「コピーできたものもあるけど、失敗もあった」と読めます。 解読できると、ちょっと探偵っぽくて楽しいです🔎
もう一歩:/R と /W を必ず指定する
ひとことで言うと、robocopy は既定のままだと、失敗したファイルをものすごく粘り強く待ちます。
robocopy のリファレンスによると、再試行の既定値は次のとおりです。
/R:<n>(失敗したコピーの再試行回数):既定は 1,000,000回/W:<n>(再試行の間の待ち時間・秒):既定は 30秒
同じファイルで再試行を100万回使い切るとすると、再試行の間の待ち時間だけで 1,000,000 × 30秒 = 3,000万秒。 だいたい 347日です。コピーを試みる時間などは別なので、処理全体の上限が347日という意味ではありません。
1年近く同じファイルの前で待つ。忠犬ハチ公もびっくり。
こうなると、終了コード以前に終了しません。 ジョブは失敗にもならず、ずっと実行中のままです。
ジョブでは /R と /W を環境に合わせて明示しましょう。 backup.ps1 の /R:2 /W:5(2回まで、5秒おき)では、ロックしたファイルがあっても約10秒で終わり、8 が返りました。
もうひとつ、/MIR(コピー元にないものをコピー先から削除する)を使うなら、先に /L(一覧を出すだけでコピー・削除をしない)で下見を。 今回の検証では /L でも終了コード 1 が返り、ファイルは作られませんでした。 ミラーの向きを逆にしてバックアップ元を消す……想像しただけで胃が痛い。
チェックリスト
- 判定は
$?やif errorlevel 1ではなく、$LASTEXITCODE -ge 8/%ERRORLEVEL% GEQ 8にした - 終了コードは robocopy の直後に変数へ保存した
- ジョブから呼ぶラッパーは、8 未満を 0 に、8 以上は元の値で返す
/Rと/Wを明示した(既定のままにしていない)/LOG+:でログを残し、失敗時に原因を追えるようにした/MIRや/PURGEは、先に/Lで下見した
まとめ
冒頭では、robocopy が「全部コピーできました」と報告したのに、ジョブは真っ赤になっていました。
でも robocopy は、嘘をついていなかったんです。 ただ、報告の言葉がジョブ管理と違っただけ。
robocopy の1は、失敗ではなく仕事をした証拠です。 報告を読むルールを「8以上」に合わせてあげれば、ちゃんと働いた日に怒られることはなくなります。
逆に「失敗なのに成功扱い」になる bash の話は、Bashで処理が失敗したのに成功扱い? pipefail・PIPESTATUSで終了コードを確かめる で書いています。
まずは、今動いているバッチや PowerShell の中から robocopy を探して、直後の判定が「8以上」になっているか見てみてください。 なっていたら、今夜はゆっくりきつねうどんです🍜
参考資料
- Microsoft Learn:robocopy — 終了コードの表と「8 以上は失敗」、
/R・/Wの既定値、/L・/MIR・/PURGE・/LOG+:などのオプション - Microsoft Learn:Return codes used by the Robocopy utility — 終了コードの表(サポート記事版)
- Microsoft Learn:about_Automatic_Variables(PowerShell 5.1) —
$LASTEXITCODE、外部プログラムの$?の決まり方、-Fileで呼んだときのexitの扱い - Microsoft Learn:if —
if errorlevel <数字>が「以上」で判定されること、GEQなどの比較演算子
確認日:2026-09-25。

コメント