Technology

Windowsで階層フォルダに保存されたファイルを指定した場所に集める方法

カメラで写真や動画を撮影した場合、日付ごとにフォルダが作成され、その中に写真ファイルや動画ファイルが作成されることがあります。

この作成された写真や動画などを整理して1つのフォルダの中にまとめたい場合に、マウスでフォルダを開いて、中のファイルをコピーして次のフォルダへ進むという作業をするのは、ファイルが多いと大変な作業になります。

本記事では、WindowsのPowerShellまたはコマンドプロンプトを用いて、階層状のフォルダの中にある指定した拡張子をもつファイルを、ユーザーが指定したフォルダの中に集めるバッチファイルを作成しました。その内容をご紹介します。

目次

バッチファイルについて

手軽に使えるように、環境構築を行う必要のないWindowsのバッチファイルを使用します。バッチファイルは、複数のコマンドをまとめて実行するためのスクリプトファイルで、任意のテキストエディタで記述した後に拡張子を.batに変えることでバッチファイルになります。

バッチファイルには、コマンドプロンプトやPowerShellで使われるコマンドを記述することができます。実行するには、エクスプローラーでダブルクリックするか、コマンドプロンプトやPowerShellからコマンドとして実行します。

コード

作成したバッチファイルを以下に示します。このバッチファイルをメモ帳などにコピーし、保存する際にファイルの種類を「すべてのファイル」に変更してから、拡張子を.batとして保存すればバッチファイルが完成します。

@echo off

set "run_cp_or_mv=0"

rem コマンドライン引数の第1引数は移動かコピーかを選択する
if "%~1"=="-m" (
    set "moveflag=1"
) else if "%~1"=="-c" (
    set "moveflag=0"
) else (
    echo Enter -c to copy a file or -m to move a file.
    goto :eof
)
rem コマンドライン引数の第2引数にコピー元のフォルダ名、第3引数にコピー先のフォルダ名を記入する
if "%~2"=="" (
    echo Enter the name of the folder from which you are copying.
    goto :eof
) else (
    set "source_folder=%~2"
)
if "%~3"=="" (
    echo Enter the name of the destination folder.
    goto :eof
) else (
    set "destination_folder=%~3"
)

rem コピー元フォルダとコピー先フォルダの包含チェック
echo %~f3 | findstr /i /b "%~f2" > nul
if not errorlevel 1 (
    echo Error: The destination folder must not be inside the source folder.
    goto :eof
)
echo %~f2 | findstr /i /b "%~f3" > nul
if not errorlevel 1 (
    echo Error: The source folder must not be inside the destination folder.
    goto :eof
)

rem コマンドライン引数の第4引数はコピーするファイルの拡張子をオプションで指定する
if "%~4"=="" (
    set "file_extension=*.*"
) else (
    set "file_extension=%~4"
)

setlocal enabledelayedexpansion

rem 注意: ファイルの同一性はファイルサイズと更新日時で判定しています。
rem ファイルの内容(ハッシュ値)までは比較していません。

for /R "%source_folder%" %%F in (%file_extension%) do (
rem コピー元のフォルダを含めた階層の浅いフォルダ内のファイルを順に処理する
    set "base_name=%%~nF"
    set "extension=%%~xF"
    set "file_name=%%~nxF"
    set "source_file_size=%%~zF"
    set "source_update_date=%%~tF"
    set "counter=1"
    set "run_cp_or_mv=0"

    if exist "%destination_folder%\%%~nxF" (
    rem コピー先のフォルダに既に同名のファイルがある場合
        call :check_file_name
        if !run_cp_or_mv! equ 1 (
            if  !moveflag! equ 0 (
                copy "%%F" "%%~dpF!new_file_name!" > nul
                echo Copied and renamed %%F to !new_file_name!
            ) else (
                move "%%F" "%%~dpF!new_file_name!" > nul
                echo Moved %%F to !new_file_name!
            )
            move "%%~dpF!new_file_name!" "%destination_folder%\!new_file_name!" > nul
        ) else (
            echo Skipped %%F
            echo [destination: !destination_update_date!  source: !source_update_date!]
        )
    ) else (
        if  !moveflag! equ 0 (
            robocopy "%%~dpF" "%destination_folder%" "%%~nxF" > nul
            echo Copied %%F to %destination_folder%
        ) else (
            move "%%F" "%destination_folder%\%%~nxF" > nul
            echo Moved %%F to %destination_folder%
        )
    )
)
echo File collection completed.

goto :eof


rem コピー先にすでに同じファイルがある場合は連番を後ろにつけたファイル名を作成
:check_file_name
    set "chk_name=%destination_folder%\!file_name!"

    :loop

    set "next_name=%destination_folder%\!base_name!_!counter!!extension!"
    set "new_file_name=!base_name!_!counter!!extension!"

    rem chk_nameには最初のループはコピーしたいファイルの名前、その後は連番をつけたファイルの名前が設定される
    if exist "!chk_name!" (
        for %%A in ("!chk_name!") do (
            set "destination_file_size=%%~zA"
            set "destination_update_date=%%~tA"
        )
    ) else (
        set "destination_file_size=-1"
    )

    if !source_file_size! equ !destination_file_size! (
    rem ファイルサイズが同じ場合
        if "!source_update_date!" gtr "!destination_update_date!" (
        rem 更新日が新しい場合
            if exist "!next_name!" (
                set /A counter+=1
                set "chk_name=!next_name!"
                goto loop
            ) else (
                set run_cp_or_mv=1
            )
        )
    ) else (
    rem  ファイルサイズが異なる場合
        if exist "!next_name!" (
            set /A counter+=1
            set "chk_name=!next_name!"
            goto loop
        ) else (
            set run_cp_or_mv=1
        )
    )
endlocal
goto :eof

仕様

コマンドライン引数で指定したフォルダの階層構造以下にある全てのファイルを、指定した別のフォルダにコピーまたは移動させるバッチファイルです。

オプションとして、対象とするファイルの拡張子を指定できます。

ファイル名が同じファイルがある場合には、ファイルサイズを比較します。ファイルサイズが異なる場合には連番をつけて別ファイルとしてコピーまたは移動させます。

ファイル名とファイルサイズが同じ場合には更新日時を確認します。更新日時が同じか古いファイルならばスキップし、新しいファイルの場合には連番をつけて別ファイルとしてコピーまたは移動させます。

なお、ファイルの同一性はファイルサイズと更新日時で判定しており、ファイルの内容(ハッシュ値)までは比較していません。同名かつ同サイズで中身が異なるファイルが存在する場合にはご注意ください。

使い方

コマンドラインまたはPowerShellで次のコマンドを実行します。バッチファイル名を「fcollect.bat」とすると、以下のフォーマットで実行します。

fcollect.bat [オプション(必須)] コピー元フォルダ コピー先フォルダ 拡張子

オプションには次のいずれかを必ず指定します。

オプション(必須)意味
-cファイルを指定フォルダにコピーする
-mファイルを指定フォルダに移動する

大切なファイルを処理する場合はコピーを選択する方が安心です。

「コピー元フォルダ」にはコピー(または移動)元のフォルダ名を指定し、「コピー先フォルダ」にはコピー(または移動)先のフォルダ名を指定します。

「コピー元フォルダ」と「コピー先フォルダ」は完全に独立なフォルダにしてください。一方が他方の中に含まれている場合にはエラーメッセージを表示して終了します。

「拡張子」ではファイルの種類を指定できます。拡張子はデフォルトではすべてのファイルを対象とする「*.*」が設定されています。

例えば、テキストファイルのみを移動させたい場合は「*.txt」などとします。デフォルトから変更しない場合には拡張子の指定は省略できます。

以下は、カレントディレクトリにあるフォルダ「Folder1以下の階層構造に存在する拡張子が.movであるファイルをすべて「Folder2」にコピーする場合の例です。

fcollect.bat -c .\Folder1 .\Folder2 *.mov 

コードの説明

簡単なコードの説明を行います。

rem コピー元フォルダとコピー先フォルダの包含チェック
echo %~f3 | findstr /i /b "%~f2" > nul
if not errorlevel 1 (
    echo Error: The destination folder must not be inside the source folder.
    goto :eof
)
echo %~f2 | findstr /i /b "%~f3" > nul
if not errorlevel 1 (
    echo Error: The source folder must not be inside the destination folder.
    goto :eof
)

このブロックは、引数を受け取った後、コピー元フォルダとコピー先フォルダが互いに包含関係にないかをチェックしています。%~f3%~f2 はそれぞれ第3引数(コピー先)と第2引数(コピー元)のフルパスを表します。findstr /i /b で一方のフルパスが他方のフルパスで始まっているかを調べ、包含関係にある場合にはエラーメッセージを表示して終了します。逆方向のチェックも同様に行っています。

for /R "%source_folder%" %%F in (%file_extension%) do (...)

この行は、指定されたフォルダ内のファイルに対してループを実行します。/Rは、forコマンドのオプションの1つで、再帰的にフォルダ内のファイルを探索することを指定します。ここでは%source_folder%以下の階層構造を探索しています。%%Fはループ変数でそれぞれのファイルを表しており、%file_extension%で指定されたファイルの拡張子に合致するファイルを取り出しています(例えば、*.*はすべてのファイルを対象にします)。

    set "base_name=%%~nF"
    set "extension=%%~xF"
    set "file_name=%%~nxF"
    set "source_file_size=%%~zF"
    set "source_update_date=%%~tF"

%%は、forループ変数を参照するためのプレフィックスです。%%Fはループ変数Fを参照することを意味します。

~は変数の修飾子で、変数の値を操作するために使用します。例えば、

C:\Path\to\Source\Folder\file_name.txt

%%Fに読み込まれたとします。nはファイル名を表す修飾子で、%%~nFはループ変数からファイル名(file_name)を取り出します。xはファイルの拡張子を表す修飾子です。%%~nxFはループ変数からファイル名と拡張子(file_name.txt)を取り出します。同様に、zはファイルサイズを、tは更新日時を取り出しています。

if exist "%destination_folder%\%%~nxF" (
   ...
) else (
   ...
)

この条件文は、移動先またはコピー先のフォルダ%destination_folder%%%~nxFというファイルがすでに存在しているかどうかを調べています。存在しない場合には、

if  !moveflag! equ 0 (
     robocopy "%%~dpF" "%destination_folder%" "%%~nxF" > nul
     echo Copied %%F to %destination_folder%
) else (
     move "%%F" "%destination_folder%\%%~nxF" > nul
     echo Moved %%F to %destination_folder%
)

を実行し、移動フラグmoveflagが0の場合はコピーを、それ以外の場合は移動を行います。コピーにはrobocopyコマンド、移動にはmoveコマンドを使用しています。 > nulは結果を標準出力に表示しないことを意味します。なお、%%~dpFはループ変数Fに設定されているファイルのドライブ名とパスを意味します。先ほどの例でいえば、

C:\Path\to\Source\Folder\

の部分を指しています。移動先またはコピー先のフォルダ%destination_folder%%%~nxFというファイルがすでに存在している場合には、

call :check_file_name
if !run_cp_or_mv! equ 1 (
     if  !moveflag! equ 0 (
          copy "%%F" "%%~dpF!new_file_name!" > nul
          echo Copied and renamed %%F to !new_file_name!
     ) else (
          move "%%F" "%%~dpF!new_file_name!" > nul
          echo Moved %%F to !new_file_name!
     )
     move "%%~dpF!new_file_name!" "%destination_folder%\!new_file_name!" > nul
) else (
     echo Skipped %%F
     echo [destination: !destination_update_date!  source: !source_update_date!]
)

を実行します。関数check_file_nameの中でファイル名につける連番号を調べ、新しいファイルの名前を「元のファイル名_連番号.拡張子」という形とします。この関数では、

if !source_file_size! equ !destination_file_size! (
rem ファイルサイズが同じ場合
    if "!source_update_date!" gtr "!destination_update_date!" (
    rem 更新日が新しい場合
        if exist "!next_name!" (
            set /A counter+=1
            set "chk_name=!next_name!"
            goto loop
        ) else (
            set run_cp_or_mv=1
        )
    )
) else (
rem  ファイルサイズが異なる場合
    if exist "!next_name!" (
        set /A counter+=1
        set "chk_name=!next_name!"
        goto loop
    ) else (
        set run_cp_or_mv=1
    )
)

の部分で、コピー元のファイルとコピー先にある同名のファイルのファイルサイズと更新日を比較しています。ファイルサイズが異なる場合やファイルサイズは同じだけど更新日が新しい場合は、連番付きのファイル名がすでに存在するかを確認し、存在すればカウンタを増やして再度ループします。存在しなければ、その連番をファイル名に採用し、コピーまたは移動を行うことを示すフラグrun_cp_or_mvを1に設定します。

関数check_file_nameの処理が終わった後、設定されたそのフラグに基づき、ファイルを移動またはコピーするか、スキップをするかを条件文で判別します。

if !run_cp_or_mv! equ 1 (
  ...
) else (
  ...
)

まとめ

私が旅行などでカメラで撮影した写真や動画ファイルを整理するために作成したバッチファイルをご紹介しました。階層状のフォルダ内にある全てのファイルを、指定したフォルダにコピーまたは移動させることができます。また、拡張子の指定やファイル名の重複にも対応しました。これはバッチファイルを用いた自動化の一例ですが、その他にもいろいろなことを自動化することができますので、よければご活用ください。

-Technology
-