BATのドラッグ&ドロップ機能まとめ

普段自分が使っているバッチファイルのドラッグ&ドロップ機能（以下D&D）まとめです。
その他にもD&Dしたファイルの名前変更例をいくつか載せています。

環境 windows11 [Version 10.0.22000.493]

bat歴が浅いため、何か改善点などあれば気軽にコメントしていただけると幸いです。

##1. bat作成、実行時の注意点

バッチファイルの実行は__自己責任__でお願いします。特に改名後は元に戻すことが難しいため、バックアップを取ったうえで実行してください。
ドロップした__ファイル、フォルダ、またはパス__に__`^`__もしくは__`&`__が入っていると__機能しない__ので、これが無い前提で進めます。（ドロップした__フォルダ内のファイル__に含まれている場合は機能します）。

##2. D&Dのテンプレート
### 2-1. D&Dの方法について
D&Dには以下の2通りがありますが、今回は前者での処理を前提にしています。
<ul><li>バッチファイル、もしくはショートカットのアイコンにドロップ</li><li>バッチを起動後、画面にドロップ</li></ul>

### 2-2. 従来の方法
調べて出るものはおおよそこんな感じの処理がされていると思います。（どちらも同じ内容が表示されます）

```bat:test_v0.1.bat
@echo off
:Loop
echo "[%1]"
shift
if not "%1"=="" goto Loop
Pause >nul
```

```bat:test_v0.2.bat
@echo off
for %%A in (%*) do (
	echo "[%%A]"
)
pause >nul
```

しかし、これら方法は万能ではなく、 ドロップしたファイル、フォルダ、またはパスに__`=`__、__`;`__、__`,`__があると勝手に改行されてしまいます。
例えば、`C:\test`に以下のファイルを入れ、先ほどのバッチファイルにドロップしてみましょう。
<ul><li>1.txt</li><li>2=3,4;5.docx</li><li>6 7=8,9;10.xlsx</li></ul>

```bat:実行結果
# 選択の仕方によっては順番通りになりません
"[C:\test\1.txt]"
"[C:\test\2]"
"[3]"
"[4]"
"[5.docx]"
"["C:\test\6 7=8,9;10.xlsx"]"
```

どちらのバッチでも`2=3,4;5.docx`で誤動作が起こっていることがわかります。
これはパスの両端にダブルクォーテーション`"`が無く、文字のエスケープができなかったため発生しています。
「これらの文字を使わなけらばいいだけ」と思われるかもしれませんが、バッチは親切な言語ではないので、バッチで何かする際に不確定要素を`"`で囲うことはほぼ必須です。そのためトラブル対策のためにも`"`で囲っておく必要があります。

※ `6 7=8,9;10.xlsx`のように誤作動が起こらない時もあります。（`半角スペース`があるとき自動的に両端が`"`で囲われるため）

### 2-3. 誤作動対策
誤動作を防ぐ方法として、test_v0.2.batの方法を以下のように改良します。
※ バッチ内にあるように、ドライブ`C`、`D`、`E`、`F`以外で使用する場合にはそのドライブを記入してください。

```bat:test_v1.bat
@echo off
rem ドロップされた全ファイルのパスを%Drop%に格納する
set "Drop=%*"
set "Drop=%Drop:"=%"

rem setlocal enable~で遅延展開
setlocal enabledelayedexpansion

rem %Drop%の文字列内の[ C:],[ D:],...を[" "C:],[" "D:],...に置換
rem ()内のC D E Fは使用するドライブ。他のドライブも使う場合はさらに付足す
for %%A in (C D E F) do (set "Drop=!Drop: %%A:=" "%%A:!")

rem 最後に%Drop%の両端を"で囲う
endlocal & set "Drop="%Drop%""

for %%A in (%Drop%) do (
	echo %%A
)
pause >nul
```

やっていることは力技で、`%*=%Drop%`に格納されている全ファイルのパス
<strong>C:\test\1.txt C:\test\2=3,4;5.docx "C:\test\6 7=8,9;10.xlsx"</strong>を以下のように変形しているだけです。
<ul><li>置換して<strong>"</strong>を消す（<strong>C:\test\1.txt C:\test\2=3,4;5.docx C:\test\6 7=8,9;10.xlsx</strong>）</li><li><strong>[ C:]</strong>を<strong>[" "C:]</strong>に置換（<strong>C:\test\1.txt" "C:\test\2=3,4;5.docx" "C:\test\6 7=8,9;10.xlsx</strong>）</li><li>両端に<strong>"</strong>を追加（<strong>"C:\test\1.txt" "C:\test\2=3,4;5.docx" "C:\test\6 7=8,9;10.xlsx"</strong>）</li></ul>このバッチに先ほどのファイルをドロップすると正しく機能します。

```bat:test_v1.bat&nbsp;実行結果
# 選択の仕方によっては順番通りになりません
"[C:\test\1.txt]"
"[C:\test\2=3,4;5.docx]"
"["C:\test\6 7=8,9;10.xlsx"]"
```
※遅延展開を用いていますが、ファイル名に`!`が含まれていても誤作動は起こらないです。
※変数の文字列は8192文字ほどしか格納できないため、__ファイル数が多いと強制終了します__。（パスの長さにもよりますが、100ファイル未満が目安です）
これを超えるファイルを処理したい場合は、大量のファイルが入ったフォルダを2-4のバッチにドロップしてください。





###2-4. ファイルとフォルダの区別
ファイルとフォルダを見分ける方法はいくつかありますが、今回はパスが存在するか否かで判定を行います。

```bat:test_v2.bat
@echo off
rem 文字コードをANSI(Shift_JIS)にしないと文字化けします。
set "Drop=%*"
set "Drop=%Drop:"=%"

setlocal enabledelayedexpansion
rem C D E Fドライブ以外も使う場合は書き足す
for %%A in (C D E F) do (set "Drop=!Drop: %%A:=" "%%A:!")
endlocal & set "Drop="%Drop%""


for %%A in (%Drop%) do (
	rem "%%~A"で"を消去、最後に\を付けてパスが存在するか確認
	if exist "%%~A\" (
		rem パスが存在する=フォルダ時の処理。
		rem　フォルダに"."が付いている場合に備え、%%~xA(拡張子)をつけ、%%~nxAとする。
		echo パス"%%~dpA" "%%~nxA"はフォルダです。
	) else (
		rem パスが存在しない=ファイル時の処理。
		echo パス"%%~dpA" "%%~nA%%~xA"はファイルです。
	)
)
pause >nul
```

`C:\test2` 内に以下のファイル、フォルダを入れて実行してみると

|  ファイル | フォルダ |
|:----------|:--------|
|   1.jpg   |    3    |
|  2 -.png  |  4.bat  |
| test2.bat |

```bat:test_v2.bat&nbsp;実行結果
パス"C:\test2\" "1.jpg"はファイルです
パス"C:\test2\" "2 -.png"はファイルです
パス"C:\test2\" "3"はフォルダです
パス"C:\test2\" "4.bat"はフォルダです
```

問題なく区別出来ていることがわかります。


###2-5. D&D処理のテンプレート
2-4だとまだ実用的ではないので、以下の処理を追加します。
<ul><li>D&D以外での起動時に戻る</li><li>ファイル、フォルダ毎にカレントディレクトリを設定</li><li>フォルダの場合フォルダ内の全ファイルに対して処理</li></ul>


```bat:test_v3.bat
@echo off
rem 文字コードをANSI(Shift_JIS)にしないと文字化けします。

if "%~1"=="" (
	echo;
	echo ドラッグ^&ドロップのみ対応しています
	pause
	exit
)

set "Drop=%*"
set "Drop=%Drop:"=%"

setlocal enabledelayedexpansion
rem C D E Fドライブ以外も使う場合は書き足す
for %%A in (C D E F) do (set "Drop=!Drop: %%A:=" "%%A:!")
endlocal & set "Drop="%Drop%""

rem 今後のバッチで付足す部分
:Select

::-------------------- ループ部分

for %%A in (%Drop%) do (

	rem ローカル変数の開始、ディレクトリが存在するかの確認
	setlocal
	if exist "%%~A\" (

		rem カレントディレクトリの設定、フォルダ名を変数に格納。
		pushd "%%~A\"
		set "Fold=%%~nxA"

        rem for内でそれぞれのファイル名、拡張子を格納、処理部を呼び出す
		for %%B in (*.*) do (
			set "FileName=%%~nB"
			set "FileExt=%%~xB"
			call :ProcessingUnit
		)

	) else (
		rem ファイル時の処理。ほとんどフォルダ時と同じ
		pushd "%%~dpA"
		set "FileName=%%~nA"
		set "FileExt=%%~xA"
		call :ProcessingUnit
	)
	rem ローカル変数終了で、変数、カレントディレクトリを戻す
	endlocal
)

echo 終わり
pause >nul
exit /b

::-------------------- 処理部分

:ProcessingUnit
rem %Fold%はフォルダをドロップした時のみ設定される
echo "[カレントディレクトリ:%cd%\][ファイル名:%FileName%][拡張子:%FileExt%][フォルダ:%Fold%]"

rem 処理内容

exit /b 0
```

`:ProcessingUnit`の`処理内容`の部分に処理したい内容を書くことで実現可能です。

※ 2-3にあるように大量のファイルをドロップすることはできません。この場合、大量のファイルが入ったフォルダをドロップすることで制限なしに処理させることができます。

※ `call :ProcessingUnit "%%~nB"...`のように、引数を設定していないのは以下のトラブルが起こるからです。
<ul><li>ファイル名に<strong>"%"</strong>が含まれている場合、call先で勝手に展開され消える</li><li>ファイル名に<strong>"^"</strong>が含まれている場合、call先で勝手に増殖する（<strong>"^^"</strong>になる）</li></ul>
後者に関しては詳しく解説されている方がいるのでよろしければどうぞ。~~なんで引数が使えないんだ、、、~~


https://qiita.com/yz2cm/items/8058d503a1b84688af09#%E3%82%B5%E3%83%BC%E3%82%AB%E3%83%A0%E3%83%95%E3%83%AC%E3%83%83%E3%82%AF%E3%82%B9%E3%81%AE%E4%B8%8D%E6%80%9D%E8%AD%B0


##3. ファイル名の改変例
ここからはD&D処理の例なので、先ほどの`test_v3.bat`に付足す形で書きます。
###3-1. ファイル名の置換
`test_v3.bat`の`:Select`、`:ProcessingUnit`を以下のように変えることで実現可能です。（ただ置換機能でごり押ししただけ）

```bat:test_v3.bat&nbsp;改良部分
:Select
rem 置換の設定
cls
echo;
echo # ファイル名に使用できない\/:*?^<^>^|は書かないでください
echo 意図しないトラブルが発生する場合があります
echo;
echo 以下の内容を入力してください
set/p "before=置換前文字列=>"
if "%before%"=="" (
	echo 何も入力されていません
	pause
	goto Select
)
set/p "after=置換後文字列=>"

cls
echo;
echo 以下の内容で大丈夫ですか?(Y/N)
echo "置換前:%before%→置換後:%after%"
choice /c yn >nul
if not %errorlevel%==1 (goto :Select)


echo 置換内容によっては元に戻すことが極めて困難になります。
echo 本当に置換を実行しますか?(O/N)
choice /c on >nul
if not %errorlevel%==1 (exit)

rem %after%が空の時用に一時的に変数を設定(これをしないと置換後に=が付いてしまう)
if "%after%"=="" (set after=:)

rem :Selectは以上

...

:ProcessingUnit
title "対象:%FileName%%FileExt%"
rem いったん!を\に置換
set "FileName2=%FileName:!=\%"
set "before=%before:!=\%"
set "after=%after:!=\%"

setlocal enabledelayedexpansion
rem 遅延展開を利用し、設定したように置換して%temp%に格納
set "temp=!FileName2:%before%=%after%!"

rem 遅延展開を終了させると同時に\を!に置換
endlocal & set "FileName3=%temp:\=!%"

rem %after%が空だった時の一時変数:を消して改名
ren "%FileName%%FileExt%" "%FileName3::=%%FileExt%"

exit /b 0
```

###3-2. 文字列の追加
3-1とは別のファイルに書き足してください。

```bat:test_v3.bat&nbsp;改良部分
:Select
rem 文字列追加の設定
cls
echo;
echo # ファイル名に使用できない\/:*?^<^>^|は書かないでください
echo 意図しないトラブルが発生する場合があります
echo;
echo 以下の内容を入力してください
set/p "before=ファイル前の文字列=>"
set/p "after=ファイル後の文字列=>"

cls
echo;
echo 以下の内容で大丈夫ですか?(Y/N)
echo "%before%[ファイル名]%after%.拡張子"
choice /c yn >nul
if not %errorlevel%==1 (goto :Select)

rem :Selectは以上

...

:ProcessingUnit
title "対象:%FileName%%FileExt%"

rem ファイル前後に設定した文字列を追加する
ren "%FileName%%FileExt%" "%before%%FileName%%after%%FileExt%"

exit /b 0
```

###3-3. ファイル名をファイル作成日時に変更
※ バッチにもある通り、デフォルトでは`"ファイル作成日時(元ファイル名).拡張子"`に変換されます。`"ファイル作成日時.拡張子"`に変更もできますが、元のファイル名に戻すことが極めて困難になるためバックアップを取るようにしてください。

```bat:test_v3.bat&nbsp;改良部分
:Select
rem 確認

cls
echo;
echo 以下の内容で大丈夫ですか?(Y/N)
echo "西暦_月日_分(元ファイル名).拡張子"
choice /c yn >nul
if not %errorlevel%==1 (exit)

rem :Selectは以上

...

:ProcessingUnit
title "対象:%FileName%%FileExt%"
for %%a in ("%FileName%%FileExt%") do (

	rem %%~taでファイルの作成日時に変換。それをfor /fで任意の形に
	for /f "tokens=1-5 delims=:/ " %%b in ("%%~ta") do (

		rem 同名のファイルが存在した時の処理
		if exist "%%b_%%c%%d_%%e%%f%FileExt%" (
			set /a numkaburi+=1
		) else (
			set /a numkaburi=1
		)
		rem :Renameの呼び出し
		call :Rename "%%b_%%c%%d_%%e%%f"
	)

)
exit /b 0

:Rename
if not "%numkaburi%"=="1" (set "kaburi=_%numkaburi%") else (set "kaburi=")

ren "%FileName%%FileExt%" "%~1%kaburi%(%FileName%)%FileExt%"

rem 元のファイル名を引き継がない書き方
rem #この方法だと元のファイル名に戻すことが極めて困難になります。バックアップを取ったうえで実行してください。
rem ren "%FileName%%FileExt%" "%~1%kaburi%%FileExt%"

exit /b 0
```


## 4. おまけ
3章のバッチファイルでも大丈夫ですが、3ファイル作っても置き場所に困るので1つにまとめます。

```bat:test_v4.bat
@echo off
mode con:cols=55 lines=8
rem 文字コードをANSI(Shift_JIS)にしないと文字化けします。

if "%~1"=="" (
	echo;
	echo ドラッグ^&ドロップのみ対応しています
	pause
	exit /b
)

set "Drop=%*"
set "Drop=%Drop:"=%"

setlocal enabledelayedexpansion
rem C D E Fドライブ以外も使う場合は書き足す
for %%A in (C D E F) do (set "Drop=!Drop: %%A:=" "%%A:!")
endlocal & set "Drop="%Drop%""


rem 初期設定
title ファイル名改名バッチ
set num=1
rem 列数。 項目を3つ以上に増やす時に増やす
set column=3

rem UI的な何か。押された処理を%num%に格納する
:Select
for /l %%a in (1,1,%column%) do (
	if %num%==%%a (set "u%%a=＞") else (set "u%%a=　")
)

(
	cls
	echo;
	echo %u1%[ 置換 ]%u2%[ 文字列追加 ]%u3%[ 作成日時表記 ]
	echo; (操作 左右:AD 決定:W^)
)
choice /c awd >nul
set /a num+=%errorlevel%-2
if %num%==0 set num=%column%
if %num% gtr %column% set num=1
if not %errorlevel%==2 goto Select

rem Wが押された時の動作
if %num% geq 1 if %num% leq 2 call :Config1
if %num%==3 call :Config2

rem 確認
cls
echo;
echo 以下の内容で大丈夫ですか?(Y/N)
echo "%text2%"
choice /c yn >nul
if not %errorlevel%==1 (
	if %num%==3 (exit) else (goto Select)
)

if %num%==1 (
	call :Confirm
	if "%after%"=="" set "after=:"
)


::-------------------- ループ部分

for %%A in (%Drop%) do (

	rem ローカル変数の開始、ディレクトリが存在するかの確認
	setlocal
	if exist "%%~A\" (

		rem カレントディレクトリの設定、フォルダ名を変数に格納。
		pushd "%%~A\"
		set "Fold=%%~nxA"

        rem for内でそれぞれのファイル名、拡張子を格納、処理部を呼び出す
		for %%B in (*.*) do (
			set "FileName=%%~nB"
			set "FileExt=%%~xB"
			call :ProcessingUnit%num%
		)

	) else (
		rem ファイル時の処理。ほとんどフォルダ時と同じ
		pushd "%%~dpA"
		set "FileName=%%~nA"
		set "FileExt=%%~xA"
		call :ProcessingUnit%num%
	)
	rem ローカル変数終了で、変数、カレントディレクトリを戻す
	endlocal
)

cls
echo 終わり(30秒後に自動終了)
timeout /t 30 /nobreak
exit /b

::-------------------- 設定部分

:Config1
if %num%==1 (set text1=置換) else (set text1=ファイル)
cls
echo;
echo # ファイル名に使用できない\/:*?^<^>^|は書かないでください
echo 意図しないトラブルが発生する場合があります
echo;
echo 以下の内容を入力してください
set/p "before=%text1%前文字列=>"

if "%before%"=="" (
	if %num%==1 (
		echo 何も入力されていません
		pause
		goto Select
	)
)
set/p "after=%text1%後文字列=>"

if %num%==1 (
	set "text2=置換前:%before%→置換後:%after%"
) else (
	set "text2=%before%[ファイル名]%after%.拡張子"
)
exit /b 0

:Config2
set "text2=西暦_月日_分(元ファイル名).拡張子"
exit /b 0

:Confirm
echo 内容によっては元に戻すことが極めて困難になります。
echo 本当に置換を実行しますか?(O/N)
choice /c on >nul
if not %errorlevel%==1 (exit)

::-------------------- 処理部分


::---------- num=1の時の処理
:ProcessingUnit1
title "対象:%FileName%%FileExt%"
rem いったん!を\に置換
set "FileName2=%FileName:!=\%"
set "before=%before:!=\%"
set "after=%after:!=\%"

setlocal enabledelayedexpansion
rem 遅延展開を利用し、設定したように置換して%temp%に格納
set "temp=!FileName2:%before%=%after%!"

rem 遅延展開を終了させると同時に\を!に置換
endlocal & set "FileName3=%temp:\=!%"

rem %after%が空だった時の一時変数:を消して改名
ren "%FileName%%FileExt%" "%FileName3::=%%FileExt%"

exit /b 0


::---------- num=2の時の処理
:ProcessingUnit2
title "対象:%FileName%%FileExt%"

rem ファイル前後に設定した文字列を追加する
ren "%FileName%%FileExt%" "%before%%FileName%%after%%FileExt%"

exit /b 0


::---------- num=3の時の処理
:ProcessingUnit3
title "対象:%FileName%%FileExt%"

for %%a in ("%FileName%%FileExt%") do (

	rem %%~taでファイルの作成日時に変換。それをfor /fで任意の形に
	for /f "tokens=1-5 delims=:/ " %%b in ("%%~ta") do (

		rem 同名のファイルが存在した時の処理
		if exist "%%b_%%c%%d_%%e%%f%FileExt%" (
			set /a numkaburi+=1
		) else (
			set /a numkaburi=1
		)
		rem :Renameの呼び出し
		call :Rename "%%b_%%c%%d_%%e%%f"
	)

)
exit /b 0

:Rename
if not "%numkaburi%"=="1" (set "kaburi=_%numkaburi%") else (set "kaburi=")

ren "%FileName%%FileExt%" "%~1%kaburi%(%FileName%)%FileExt%"

rem 元のファイル名を引き継がない書き方
rem #この方法だと元のファイル名に戻すことが極めて困難になります。バックアップを取ったうえで実行してください。
rem ren "%FileName%%FileExt%" "%~1%kaburi%%FileExt%"

exit /b 0
```