robocopyが成功したのに失敗扱い? 終了コード1の意味と「8以上だけ失敗」にする書き方(PowerShell・バッチ)

robocopyが成功したのに失敗扱い?

こんにちは、おうどんです🍜

夜間バックアップのジョブを作る。 中身は 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.txt3
コピー先に 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以上」になっているか見てみてください。 なっていたら、今夜はゆっくりきつねうどんです🍜

参考資料

確認日:2026-09-25。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

おうどん|癒しと創作をたのしむ雑食クリエイター
写経アプリやクレイセラピー、優しい和風デザインがすき。
ZARDと刀剣と文字に癒されて、今はアプリ作ってます。

コメント

コメントする

目次