Compare two folders and output missing files in both folders
robocopy
(included in recent Windows versions) can do this in one pass:
given \source\
and \source2\
with some files which are common and files which exist only in either folder, running
robocopy source source2 /L /NJH /NJS /NP /NS
yields
D:\Users\me\test\source\
*EXTRA Datei only_in_source2.txt
Neue Datei only_in_source.txt
where lines starting with a *
denote files only in source2
(independent of the OS language), and other lines denote files only in source
.
The options suppress various output items, and /L
takes care that differences are listed only, not copied.
You can save the next code as ccomp.cmd
, and then call it with the /?
flag to find out how to use it (tested on Windows 7, Windows 10):
Syntax:
ccomp <dir_tree1> <dir_tree2>
Code:
@echo off
if "!ERRORLEVEL!" == "%ERRORLEVEL%" (
REM if delayed expansion is enabled:
(
echo.
echo ERROR: This script must be called in a Disabled Delayed Expansion block ^(default^)^^^!
echo.
call :PressAKey "Press a key to exit..."
echo.
)>con
exit /b 1
)
setlocal disabledelayedexpansion
if defined _first_time goto :label1MAIN
REM Test CHCP:
chcp /?>nul 2>nul||(
(
echo. & echo ERROR: Could not start chcp ^(necessary^)!
)>con
exit /b 1
)
REM Get the initial code page:
call :GetCurrentCodePage _initial_CP
REM Change the code page (character encoding) for "CON" (console) to 65001 (UTF-8):
set _con_error=false
mode con cp select=65001>nul 2>nul||(
(
echo. & echo WARNING: Could not change the code page for CON ^(Console^)!
)>con
set _con_error=true
)
set _help_flag=0
set /a _count=1
set "_script_path=%~Dpnx0"
:repeat1MAIN
if _%1_ == _""_ (
REM "%1" is """"
set _param%_count%=""
shift
set /a _count+=1
goto :repeat1MAIN
) else (
if /i "%~1" == "/?" (
set _help_flag=1
shift
goto :repeat1MAIN
) else (
if /i "%~1" == "/help" (
set _help_flag=1
shift
goto :repeat1MAIN
) else (
if /i "%~1" == "/h" (
set _help_flag=1
shift
goto :repeat1MAIN
) else (
if not "%~1" == "" (
REM if "%1" is null, it means that no more parameters are provided
set "_param%_count%=%~1"
shift
set /a _count+=1
goto :repeat1MAIN
)
)
)
)
)
set /a _count-=1
set /a _param_count=_count
REM start Checking parameters \/
if "%_help_flag%" == "1" (
call :DisplayHelp
exit /b 0
) else (
if %_param_count% gtr 2 (
(
echo. & echo ERROR: Too many parameters!
)>con
exit /b 1
) else (
if %_param_count% lss 2 (
(
echo. & echo ERROR: Too few parameters!
)>con
exit /b 1
)
)
)
set "_error=false"
if "%_param1%" == """" (
(
echo. & echo ERROR: First provided directory parameter must not be empty!
)>con
set "_error=true"
) else (
call :TestIfDirAcccessible _param1 _is_param1_dir
setlocal enabledelayedexpansion
if not "!_is_param1_dir!" == "0" (
endlocal
(
echo. & echo ERROR: First provided directory parameter: "%_param1%" is not a directory or is not accessible!
)>con
set "_error=true"
) else (
endlocal
)
)
if "%_param2%" == """" (
(
echo. & echo ERROR: Second provided directory parameter must not be empty!
)>con
set "_error=true"
) else (
call :TestIfDirAcccessible _param2 _is_param2_dir
setlocal enabledelayedexpansion
if not "!_is_param2_dir!" == "0" (
endlocal
(
echo. & echo ERROR: Second provided directory parameter "%_param2%" is not a directory or is not accessible!
)>con
set "_error=true"
) else (
endlocal
)
)
call :TestIfPathIsUNC _param1 _result1
call :TestIfPathIsUNC _param2 _result2
if "%_result1%" == "true" (
set "_error=true"
(
echo. & echo ERROR: Path1: "%_param1%" seems to be a UNC path ^(contains \\^), and UNC paths are not supported by this program ^(but a UNC path can be mounted ^(for example by using pushd^), in order to make it accessible^)!
)>con
)
if "%_result2%" == "true" (
set "_error=true"
(
echo. & echo ERROR: Path2: "%_param2%" seems to be a UNC path ^(contains \\^), and UNC paths are not supported by this program ^(but a UNC path can be mounted ^(for example by using pushd^), in order to make it accessible^)!
)>con
)
if "%_error%" == "true" (
(
echo. & call :PressAKey "Press a key to exit!"
echo.
)>con
exit /b 1
)
cmd /u /c ^(echo.^&echo Path1: "%_param1%"^)
cmd /u /c ^(echo.^&echo Path2: "%_param2%"^)
REM end Checking parameters /\
REM If everything seems ok, proceed to PROCESSING:
pushd "%_param1%\">nul
set "_param1=%CD%"
if "%_param1:~-1%" == "\" set "_param1=%_param1:~0,-1%"
popd>nul
pushd "%_param2%\">nul
set "_param2=%CD%"
if "%_param2:~-1%" == "\" set "_param2=%_param2:~0,-1%"
popd>nul
call :ConvertDriveLetterToUpperCase _param1 _param1
call :ConvertDriveLetterToUpperCase _param2 _param2
call :GetStrLen _param1 _param1_len
call :GetStrLen _param2 _param2_len
pushd "%_script_path%\..">nul
if not defined _first_time (
call :EscapePathString _script_path _script_path_escaped
call :EscapePathString _param1 _param1_escaped
call :EscapePathString _param2 _param2_escaped
)
popd>nul
REM Change the code page (character encoding) for "CON" (console) to 437 (ANSI):
if "%_con_error%" == "false" (
mode con cp select=437>nul 2>nul
)
:label1MAIN
REM CHCP 437 = ANSI CODE PAGE
REM CHCP 65001 = UTF-8 CODE PAGE
if not defined _first_time (
set _first_time=defined
chcp 65001>nul
cmd /u /c ^(echo.^&echo Start time: %date% %time%^&echo.^)
cmd /u /c ^(for /f ^"tokens=^*^" %%l in ^(^'^^^( setlocal^^^&^^^"%%_script_path_escaped%%^^^" ^^^"%%_param1_escaped%%^^^" ^^^"%%_param2_escaped%%^^^"^^^&title Sorting Results ^^^^^^^^^^^^^^^(finally^^^^^^^^^^^^^^^)^^^&endlocal ^^^)^^^|sort^'^) do @^(if not defined _once ^( set "_once=defined"^&echo %%l^) else echo %%l ^)^)^&if defined _once echo.
set "_first_time="
set "_second_time="
set "_third_time="
cmd /u /c ^(call echo End time: %%date%% %%time%%^)
chcp 437>nul
REM Restore the initial code page:
chcp %_initial_CP%>nul 2>nul
) else (
if not defined _second_time (
set _second_time=defined
(
call :ProcedureAnalyzeFiles
)
) else (
if not defined _third_time (
set "_third_time=defined"
(
call :ProcedureProcessFilenamesLengthAndSize1
chcp 65001>nul
call :ProcedureProcessFilenamesLengthAndSize2
chcp 437>nul
@REM After sorting: "-" is displayed before all other characters:
@call echo ----------------------------------------""%%_count1%%""
@title Sorting results. Please wait...
)
)
)
)
endlocal & (
if defined _first_time set "_first_time=%_first_time%"
if defined _second_time set "_second_time=%_second_time%"
if defined _third_time set "_third_time=%_third_time%"
)
goto :eof
REM \\\/// Next subroutines use jeb's syntax for working with delayed expansion: \\\///
:GetStrLen
REM - by jeb - adaptation
(
setlocal EnableDelayedExpansion
set "s=!%~1!#"
set "len=0"
for %%P in (4096 2048 1024 512 256 128 64 32 16 8 4 2 1) do (
if "!s:~%%P,1!" NEQ "" (
set /a "len+=%%P"
set "s=!s:~%%P!"
)
)
)
(
endlocal
set "%~2=%len%"
exit /b
)
:EscapePathString
(
setlocal EnableDelayedExpansion
set "string=!%~1!"
call :GetStrLen string string_len
set /a string_len-=1
for /l %%i in (0,1,!string_len!) do (
rem escape "^", "(", ")", "!", "&"
if "!string:~%%i,1!" == "^" (
set "result=!result!^^^^"
) else (
if "!string:~%%i,1!" == "(" (
set "result=!result!^^^("
) else (
if "!string:~%%i,1!" == ")" (
set "result=!result!^^^)"
) else (
if "!string:~%%i,1!" == "^!" (
set "result=!result!^^^!"
) else (
if "!string:~%%i,1!" == "&" (
set "result=!result!^^^&"
) else (
if "!string:~%%i,1!" == "%%" (
set "result=!result!%%"
) else (
set "result=!result!!string:~%%i,1!"
)
)
)
)
)
)
)
)
(
endlocal
set "%~2=%result%"
exit /b
)
:TestIfDirAcccessible
(
setlocal EnableDelayedExpansion
set _returned_code=0
(pushd "!%~1!">nul 2>nul||set "_returned_code=1")&&popd>nul
)
(
endlocal
set "%~2=%_returned_code%"
exit /b
)
:TestIfPathIsUNC
(
setlocal EnableDelayedExpansion
set "_current_path=!%~1!"
set "_is_unc_path=true"
if "!_current_path:\\=!" == "!_current_path!" (
set "_is_unc_path=false"
)
)
(
endlocal
set "%~2=%_is_unc_path%"
exit /b
)
:GetCurrentCodePage
(
setlocal EnableDelayedExpansion
set "_current_cp=-1"
for /f "tokens=1,2* delims=:" %%a in ('chcp 2^>nul') do (
set "_current_cp=%%~b"
)
)
(
endlocal
set "%~1=%_current_cp%"
exit /b
)
:ConvertDriveLetterToUpperCase
(
setlocal EnableDelayedExpansion
set "_path=!%~1!"
for /f "tokens=1 delims=\" %%f in ('echo "!_path!"') do set "_path_drive=%%~df"
set "_upper_drive=!_path_drive!"
for %%D in (A: B: C: D: E: F: G: H: I: J: K: L: M: N: O: P: Q: R: S: T: U: V: W: X: Y: Z:) do (
if /i "!_path_drive!" == "%%D" (
set "_upper_drive=%%D"
goto :endConvertDriveLetterToUpperCase
)
)
)
:endConvertDriveLetterToUpperCase
(
endlocal
set "%~1=%_upper_drive%%_path:~2%"
exit /b
)
REM ///\\\ The subroutines above use jeb's syntax for working with delayed expansion: ///\\\
:GenerateShortName
@set "%~1=%~s2"
@goto :eof
:GenerateShortNameSize
@set "%~1=%~z2"
@goto :eof
:ProcedureAnalyzeFiles
setlocal
for /f "tokens=1,2,3,4,5* delims=/" %%p in ('chcp 65001^>nul^&^(setlocal^&^"%_script_path_escaped%^" ^"%_param1_escaped%^" ^"%_param2_escaped%^"^|sort^&endlocal^)^&chcp 437^>nul') do (
if not defined _second_time_for1 (
set /a _total_count=%%~p>nul 2>nul
set /a _currrent_count=1
set _second_time_for1=defined
) else (
set /a _current_count+=1
REM START PROCESSING:
setlocal enabledelayedexpansion
title Analyzing file !_current_count! of !_total_count!...
set "_previous_file=!_current_file!"
set "_previous_file_type=!_current_file_type!"
set "_previous_file_visibility=!_current_file_visibility!"
set "_previous_file_dir=!_current_file_dir!"
REM Due to a bug in sort, an extra " is added at the end of the line, so we need to remove it:
set "_previous_file_size=!_current_file_size:"=!"
set "_previous_file_base_dir=!_current_file_base_dir!"
setlocal disabledelayedexpansion
set "_current_file=%%~p"
set "_current_file_type=%%~q"
set "_current_file_visibility=%%~r"
set "_current_file_dir=%%~s"
set "_current_file_size=%%~t"
setlocal enabledelayedexpansion
if "!_current_file_dir!" == "1" (
endlocal
set "_current_file_base_dir=%_param1%"
) else (
if "!_current_file_dir!" == "2" (
endlocal
set "_current_file_base_dir=%_param2%"
) else (
endlocal
)
)
setlocal enabledelayedexpansion
REM Due to a bug in sort, an extra " is added at the end of the line, so we need to remove it:
set "_current_file_size=!_current_file_size:"=!"
REM if not first time:
if NOT "!_previous_file_type!" == "" (
if "!_next!" == "1" (
if "!_current_file_type!" == "!_previous_file_type!" (
if "!_current_file_type!" == "file" (
if NOT "!_current_file!" == "!_previous_file!" (
echo Only in "!_previous_file_dir!" - !_previous_file_type!: "!_previous_file_base_dir!!_previous_file!"
) else (
set "_for_one_cannot_get_size=0"
if "!_previous_file_size!" == "-1" set "_for_one_cannot_get_size=1"
if "!_current_file_size!" == "-1" set "_for_one_cannot_get_size=1"
if "!_for_one_cannot_get_size!" == "0" (
if "!_previous_file_size!" GTR "!_current_file_size!" (
echo "!_previous_file_base_dir!!_previous_file!" ^(!_previous_file_dir!^) size ^(!_previous_file_size!B^) is bigger than "!_current_file_base_dir!!_current_file!" ^(!_current_file_dir!^) size ^(!_current_file_size!B^)
) else (
if "!_previous_file_size!" LSS "!_current_file_size!" (
echo "!_previous_file_base_dir!!_previous_file!" ^(!_previous_file_dir!^) size ^(!_previous_file_size!B^) is smaller than "!_current_file_base_dir!!_current_file!" ^(!_current_file_dir!^) size ^(!_current_file_size!B^)
)
)
)
)
) else (
if NOT "!_current_file!" == "!_previous_file!" (
echo Only in "!_previous_file_dir!" - !_previous_file_type!: "!_previous_file_base_dir!!_previous_file!"
)
)
) else (
echo Only in "!_previous_file_dir!" - !_previous_file_type!: "!_previous_file_base_dir!!_previous_file!"
)
)
)
endlocal
endlocal
endlocal
setlocal enabledelayedexpansion
if "%%~s" == "1" (
endlocal
set "_current_file_base_dir=%_param1%"
) else (
if "%%~s" == "2" (
endlocal
set "_current_file_base_dir=%_param2%"
) else (
endlocal
)
)
set "_temp=%%~p"
setlocal enabledelayedexpansion
REM if not first time:
if NOT "!_current_file_type!" == "" (
if "!_next!" == "1" (
if "%%~q" == "!_current_file_type!" (
if "!_temp!" == "!_current_file!" (
endlocal
set _next=2
) else (
endlocal
set _next=1
)
) else (
endlocal
set _next=1
)
) else (
endlocal
set _next=1
)
) else (
endlocal
set _next=1
)
set "_current_file=%%~p"
set "_current_file_type=%%~q"
set "_current_file_visibility=%%~r"
set "_current_file_dir=%%~s"
set "_current_file_size=%%~t"
)
)
REM Treat the last file separately:
chcp 65001>nul
setlocal enabledelayedexpansion
if "!_next!" == "1" (
echo Only in "!_current_file_dir!" - !_current_file_type!: "!_current_file_base_dir!!_current_file!"
)
endlocal
chcp 437>nul
endlocal
goto :eof
:ProcedureProcessFilenamesLengthAndSize1
@setlocal
@set /a _count1=0
@REM Process directories:
@chcp 65001>nul
@title Loading directory paths for directory 1. Please wait...
@pushd "%_param1%">nul
@for /r /d %%f in (*) do @(
@set "_current_path=%%~f"
@if "%%~zf" == "" (
@call :GenerateShortName _short_name "%%~f"
@call :GenerateShortNameSize _size "%%_short_name%%"
)
@set /a _count1+=1 >nul
@setlocal enabledelayedexpansion
@title Processing file !_count1!...
@if not "%%~zf" == "" (
@echo "!_current_path:~%_param1_len%!///dir///1///1///%%~zf"
) else (
@if not "!_size!" == "" (
@echo "!_current_path:~%_param1_len%!///dir///1///1///!_size!"
) else (
@echo "!_current_path:~%_param1_len%!///dir///1///1///-1"
)
)
@endlocal
)
@popd>nul
@title Please wait...
@REM Process directories:
@title Loading directory paths for directory 2. Please wait...
@pushd "%_param2%">nul
@for /r /d %%f in (*) do @(
@set "_current_path=%%~f"
@if "%%~zf" == "" (
@call :GenerateShortName _short_name "%%~f"
@call :GenerateShortNameSize _size "%%_short_name%%"
)
@set /a _count1+=1 >nul
@setlocal enabledelayedexpansion
@title Processing file !_count1!...
@if not "%%~zf" == "" (
@echo "!_current_path:~%_param2_len%!///dir///1///2///%%~zf"
) else (
@if not "!_size!" == "" (
@echo "!_current_path:~%_param2_len%!///dir///1///2///!_size!"
) else (
@echo "!_current_path:~%_param2_len%!///dir///1///2///-1"
)
)
@endlocal
)
@popd>nul
@title Please wait...
@REM Process files:
@title Loading file paths for directory 1. Please wait...
@pushd "%_param1%">nul
@for /r %%f in (*) do @(
@set "_current_path=%%~f"
@if "%%~zf" == "" (
@call :GenerateShortName _short_name "%%~f"
@call :GenerateShortNameSize _size "%%_short_name%%"
)
@set /a _count1+=1 >nul
@setlocal enabledelayedexpansion
@title Processing file !_count1!...
@if not "%%~zf" == "" (
@echo "!_current_path:~%_param1_len%!///file///1///1///%%~zf"
) else (
@if not "!_size!" == "" (
@echo "!_current_path:~%_param1_len%!///file///1///1///!_size!"
) else (
@echo "!_current_path:~%_param1_len%!///file///1///1///-1"
)
)
@endlocal
)
@popd>nul
@title Please wait...
@REM Process files:
@title Loading file paths for directory 2. Please wait...
@pushd "%_param2%">nul
@for /r %%f in (*) do @(
@set "_current_path=%%~f"
@if "%%~zf" == "" (
@call :GenerateShortName _short_name "%%~f"
@call :GenerateShortNameSize _size "%%_short_name%%"
)
@set /a _count1+=1 >nul
@setlocal enabledelayedexpansion
@title Processing file !_count1!...
@if not "%%~zf" == "" (
@echo "!_current_path:~%_param2_len%!///file///1///2///%%~zf"
) else (
@if not "!_size!" == "" (
@echo "!_current_path:~%_param2_len%!///file///1///2///!_size!"
) else (
@echo "!_current_path:~%_param2_len%!///file///1///2///-1"
)
)
@endlocal
)
@popd>nul
@title Please wait...
@chcp 437>nul
@endlocal & (
@set _count1=%_count1%
)
@goto :eof
:ProcedureProcessFilenamesLengthAndSize2
@setlocal
@REM Process hidden directories:
@for /f "tokens=*" %%f in ('title Loading hidden directory paths for directory 1. Please wait...^&@pushd "%_param1%"^>nul^&^&dir /a:dh /s /b 2^>nul^&^&popd^>nul^&title Please wait...') do @(
@set "_current_path=%%~f"
@if "%%~zf" == "" (
@call :GenerateShortName _short_name "%%~f"
@call :GenerateShortNameSize _size "%%_short_name%%"
)
@set /a _count1+=1 >nul
@setlocal enabledelayedexpansion
@title Processing file !_count1!...
@if not "%%~zf" == "" (
@echo "!_current_path:~%_param1_len%!///dir///0///1///%%~zf"
) else (
@if not "!_size!" == "" (
@echo "!_current_path:~%_param1_len%!///dir///0///1///!_size!"
) else (
@REM This file is hidden and probably has a Unicode path:
@echo "!_current_path:~%_param1_len%!///dir///0///1///-1"
)
)
@endlocal
)
@title Please wait...
@REM Process hidden directories:
@for /f "tokens=*" %%f in ('title Loading hidden directory paths for directory 2. Please wait...^&@pushd "%_param2%"^>nul^&^&dir /a:dh /s /b 2^>nul^&^&popd^>nul^&title Please wait...') do @(
@set "_current_path=%%~f"
@if "%%~zf" == "" (
@call :GenerateShortName _short_name "%%~f"
@call :GenerateShortNameSize _size "%%_short_name%%"
)
@set /a _count1+=1 >nul
@setlocal enabledelayedexpansion
@set /a _count1+=1 >nul
@title Processing file !_count1!...
@if not "%%~zf" == "" (
@echo "!_current_path:~%_param2_len%!///dir///0///2///%%~zf"
) else (
@if not "!_size!" == "" (
@echo "!_current_path:~%_param2_len%!///dir///0///2///!_size!"
) else (
@REM This file is hidden and probably has a Unicode path:
@echo "!_current_path:~%_param2_len%!///dir///0///2///-1"
)
)
@endlocal
)
@title Please wait...
@REM Process hidden files:
@for /f "tokens=*" %%f in ('title Loading hidden file paths for directory 1. Please wait...^&@pushd "%_param1%"^>nul^&^&dir /a:-dh /s /b 2^>nul^&^&popd^>nul^&title Please wait...') do @(
@set "_current_path=%%~f"
@if "%%~zf" == "" (
@call :GenerateShortName _short_name "%%~f"
@call :GenerateShortNameSize _size "%%_short_name%%"
)
@set /a _count1+=1 >nul
@setlocal enabledelayedexpansion
@title Processing file !_count1!...
@if not "%%~zf" == "" (
@echo "!_current_path:~%_param1_len%!///file///0///1///%%~zf"
) else (
@if not "!_size!" == "" (
@echo "!_current_path:~%_param1_len%!///file///0///1///!_size!"
) else (
@REM This file is hidden and probably has a Unicode path:
@echo "!_current_path:~%_param1_len%!///file///0///1///-1"
)
)
@endlocal
)
@title Please wait...
@REM Process hidden files:
@for /f "tokens=*" %%f in ('title Loading hidden file paths for directory 2. Please wait...^&@pushd "%_param2%"^>nul^&^&dir /a:-dh /s /b 2^>nul^&^&popd^>nul^&title Please wait...') do @(
@set "_current_path=%%~f"
@set /a _count1+=1 >nul
@if "%%~zf" == "" (
@call :GenerateShortName _short_name "%%~f"
@call :GenerateShortNameSize _size "%%_short_name%%"
)
@setlocal enabledelayedexpansion
@title Processing file !_count1!...
@if not "%%~zf" == "" (
@echo "!_current_path:~%_param2_len%!///file///0///2///%%~zf"
) else (
@if not "!_size!" == "" (
@echo "!_current_path:~%_param2_len%!///file///0///2///!_size!"
) else (
@REM This file is hidden and probably has a Unicode path:
@echo "!_current_path:~%_param2_len%!///file///0///2///-1"
)
)
@endlocal
)
@title Please wait...
@endlocal & (
@set _count1=%_count1%
)
@goto :eof
:DisplayHelp
echo.
echo %~n0 ^(Cmd COMPare^) - Compare two directory trees by file and folder paths and by size
echo.
echo Syntax: %~n0 ^<dir_tree1^> ^<dir_tree2^>
echo.
echo - where ^<dir_tree1^> and ^<dir_tree2^> are two directory trees to be compared, provided by the user
echo.
echo * Note 1:
echo - Files that are hidden and also have a Unicode path - are compared only by path, not also by size
echo - In some rare cases ^(for some Unicode file paths^) some files may be misreported
echo.
echo * Note 2:
echo - This program uses the ^"sort^" utility for sorting results, so the waiting time for the comparison of the two directories depends on (is a multiple of) n * log n ^(the time complexity for the quick sort algorithm ^(used in ^"sort^"^)^) - where - n is the total number of files in the two directories that are to be compared.
echo - The output of the program is Unicode
goto :eof
:PressAKey
set /p=%~1<nul
pause>nul
goto :eof
This Powershell script does what you want.
$fso = Get-ChildItem -Recurse -path C:\Temp\Source
$fsoBU = Get-ChildItem -Recurse -path C:\Temp\Source2
Compare-Object -ReferenceObject $fso -DifferenceObject $fsoBU
That, and several other options, are discussed here: https://stackoverflow.com/questions/3804109/what-is-the-best-way-to-compare-2-folder-trees-on-windows