トップ回答者
バッチでの一括ファイル名変更について

質問
-
あるディレクトリ内のファイル名を一括で変更する、というよくある処理を行おうとしています。
単純な例として元ファイル名の頭に指定文字を付加する、という事を試したのですが、
何故かforループ中に最初に処理したはずのファイルの変更後のファイル名が再度処理対象になってしまう場合があるようです。(頻度は高い印象です)
(ちなみにこれはあくまで簡単化した例であり、実際にやりたい事は名前の途中に任意の文字列を挿入する事です)
文章で書くとよく分からないかもしれませんので、以下に例を記載します。
#元のファイルリスト:
a.bat
foo_01.txt
foo_02.txt
foo_03.txt
foo_04.txt
#a.bat:
@echo off
for %%f in (*.txt) do (
echo %%f
ren %%f %1%%f
)
echo ---
dir /b
#実行結果:
C:\temp>a.bat 0
foo_01.txt
foo_02.txt
foo_03.txt
foo_04.txt
---
0foo_01.txt
0foo_02.txt
0foo_03.txt
0foo_04.txt
a.bat
C:\temp>a.bat 1
0foo_01.txt
0foo_02.txt
0foo_03.txt
0foo_04.txt
10foo_01.txt
---
10foo_02.txt
10foo_03.txt
10foo_04.txt
110foo_01.txt
a.bat
C:\temp>a.bat 2
10foo_02.txt
10foo_03.txt
10foo_04.txt
110foo_01.txt
210foo_02.txt
---
210foo_03.txt
210foo_04.txt
2110foo_01.txt
2210foo_02.txt
a.bat
C:\temp>
こういう感じです。
このような単純な例でも再現するのですが、ネットで検索しても該当する現象を挙げている記事が見つけられませんでした。
当方の何か特殊な条件(?)でも関係しているのでしょうか?
それとも普通に説明の付く現象でしょうか?(だと助かる)
*補足
上記forのin(*.txt)部分をin(*)とすると、この現象は起こらないようです。
どのような仕様の影響なのでしょうか?
- 編集済み satoh_ma 2018年1月29日 10:37
2018年1月29日 6:40
回答
-
© ウィンドウズスクリプトプログラマ - Windows Script Programmer 2018
再帰になるのは
FindFirstFileでパターンを指定
FindNextFileで現エントリの次から探すという仕組みから明らか。
0foo_01.txt
0foo_02.txt
0foo_03.txt
0foo_04.txt ←
10foo_01.txt ← 離れてる ←
---
10foo_02.txt ← 隣 近いキャッシュ単位とかの影響では。
*の場合は
0foo_04.txt
a.bat
がキャッシュにあって、
10foo_01.txt
は見えないとか。真偽は不明。
探したら、ありました。
forコマンドの憂鬱。ファイルセットのリカージョン: Windows Script Programming
これは、win32apiの仕様なので、for renに限ったことではありません。どの開発言語でも同じです。
For Each File In Folder.Files
こんな風にしてました。
k=k+1
Set arr(k)=File
Next
For k=1 To Folder.Files.Count
Set File=arr(k)
File.Name="_" & File.Name
Next
- 回答としてマーク satoh_ma 2018年2月1日 4:54
- 編集済み ウィンドウズスクリプトプログラマ 2018年2月1日 5:22
2018年1月31日 11:37 -
これは、FindFirstFile() / FindNextFile() API の再帰の問題なんかではなく、ファイルシステム ドライバの検索ロジックに起因する現象であると考えられます。
そもそも再帰に起因する現象であるなら、a.bat のパラメータ (0, 1, 2) に依存することなく同じように現象が起きるはずですが、実際には違いますよね?
質問での a.bat パラメータには 0, 1, 2 を指定していますが、これを 0, 0, 0 や 1, 1, 1 などの同一文字の繰り返しにすると起きないはずです。(厳密には起きる「可能性」もあるけど。)
再帰が原因なら、0, 0, 0 や 1, 1, 1 などのパラメータ指定でも起きないと辻褄が合わない。。。つまり再帰が原因ではない左証になると思います。ぢゃぁ、どーいうことかというと。。。。
for コマンドでのファイル検索処理では確かに FindFirstFile() / FindNextFile() API が使用されますが、これらの API は単なるインターフェイスにすぎません。
実際の検索処理はファイル システム ドライバ自身が行っています。
で、ファイル システム ドライバ側でのファイル検索ロジックは、個々のファイル システム (NTFS, FAT32, exFAT) に依存して異なります。
USB メモリを FAT32 や exFAT でフォーマットしなおして試せば確認できると思いますが、今回の現象はファイル システム (フォーマット) に依存して変化するはずです。
もしこの現象が FindFirstFile() / FindNextFile() API に起因しているならファイル システムには依存しないはずですが、実際にはそうならない(はずで)、即ち API よりももっと低レベルな部分の処理 (つまりファイル システム ドライバ) での挙動に影響を受けている。。。ということです。
FindFirstFile() / FindNextFile() API の結果がファイル システム ドライバ側の処理に依存して変化することに関しては、かるーくではありますが、下記 FindNextFile() API の説明でも触れられています。
(蛇足ですけど、FindFirstFile() / FindNextFile() API にキャッシュを制御する機能はありません。)
---------------------------------------------
FindNextFile function
https://msdn.microsoft.com/en-us/library/windows/desktop/aa364428(v=vs.85).aspxRemarks
....
The order in which the search returns the files, such as alphabetical order, is not guaranteed, and is dependent on the file system.
....
The order in which this function returns the file names is dependent on the file system type.
With the NTFS file system and CDFS file systems, the names are usually returned in alphabetical order.
With FAT file systems, the names are usually returned in the order the files were written to the disk, which may or may not be in alphabetical order.
However, as stated previously, these behaviors are not guaranteed.
....
---------------------------------------------今回質問されたバッチ ファイルの挙動を調べたいのであれば、Microsoft 。。。 というか Sysinternals が公開している "Process Monitor" というツールを使えば、その詳細を確認することができます。
ちなみに。。。。
FindFirstFile() / FindNextFile() API に限った話ではありませんが、ほとんどの Win32 API は単なるインターフェイスです。
実際の処理は、それよりも低レベルの各種ドライバ等で行われるので、コマンドやそれに関連する API などの表面的な仕様だけでその挙動のすべてを把握するのは、かなり至難の業かと。2018年2月1日 6:40 -
> 上記forのin(*.txt)部分をin(*)とすると、
> この現象は起こらないようです。
> どのような仕様の影響なのでしょうか?先の返信で書き忘れたので、一応追加で返信しておきます。
(もっとも単なる推測ですけど。)
先の返信でも説明したように、今回の現象はファイル システム ドライバのファイル検索ロジックに起因するものと考えられますが、"in (*)" では発生しないというのも同様の理由と考えられます。
先に示した FindNextFile() API の説明で、"With the NTFS file system and CDFS file systems, the names are usually returned in alphabetical order." という一文があります。
つまり NTFS ドライバはファイル検索で検出されたファイルのリストをアルファベット順で返すように頑張ってくれる。。。ということですが、その検索ロジックはファイル検索時に与えられるフィルタ条件によって変化させる必要があると考えられます。
"in (*)" の場合はフィルタ条件が「無い」と同義になると思われ、その結果検索ロジックがシンプルになり、今回のような現象が起きにくくなる。。。。ということかと。で、FindFistFile() API の説明を読み返したら。。。今回のような現象が起きる可能性について言及されてますね。
----------------------------------------------
FindFirstFile function
https://msdn.microsoft.com/ja-jp/library/windows/desktop/aa364418%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396Remarks
.....
Be aware that some other thread or process could create or delete a file with this name
between the time you query for the result and the time you act on the information.
If this is a potential concern for your application,
one possible solution is to use the CreateFile function with CREATE_NEW
(which fails if the file exists) or OPEN_EXISTING (which fails if the file does not exist).
.....
----------------------------------------------上記説明を考慮すると for コマンド構文の性質上、「(セット)」で与えられる条件によっては、copy, move, ren 等のファイル名に影響を与えるコマンドと組み合わせて使用しすると、今回のような現象が起きても当然なのかもしれません。
つまり、FindFistFile() / FindNextFile() API の挙動は再帰やキャッシュなどはまったく無関係で、この API で検索処理を行っている最中に、検索対象のファイル名が変化する場合は、ちゃんと自分で考慮しなさい。。。ということなんだろうと思います。
これを「forコマンドのバグだ」と言ってしまうのはちょっと酷かと思いますが、for コマンド側の実装に「...」と思うのも確かに納得できます。
(for コマンド構文上の限界。。。ということなのかも。)
そもそも FindFistFile() / FindNextFile() API の説明では、再帰やキャッシュに関してまったく言及されていないので基本的にはそれら影響は関係なく、この API での検索中に対象ファイル名に変化が発生するか否かだけが問題になるのだと思います。
キャッシュ状態に依存して結果が変化する API なんて、意味ないですし。最後に蛇足ではありますが。。。。
FindFistFile() / FindNextFile() API コールでのファイル検索リクエストは、カーネル モード側で IRP_MJ_DIRECTORY_CONTROL - IRP_MN_QUERY_DIRECTORY / FileBothDirectoryInformation の IRP (I/O Request Packet) に変換され、最終的に対応するファイル システム ドライバに渡され処理されます。
このリクエストに関するドキュメントはそれなりに公開されているので、もし興味があるなら、そこら辺を調べてみると面白いかも。
----------------------------------------------
NtQueryDirectoryFile function
https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/content/ntifs/nf-ntifs-ntquerydirectoryfileIRP_MJ_DIRECTORY_CONTROL
https://docs.microsoft.com/ja-jp/windows-hardware/drivers/ifs/irp-mj-directory-control
----------------------------------------------- 回答としてマーク satoh_ma 2018年2月2日 4:41
2018年2月2日 3:14
すべての返信
-
© ウィンドウズスクリプトプログラマ - Windows Script Programmer 2018
再帰です。変更後の名前が昇順で後ろに追加されるせいです。
再帰されないように、いったん全部取り出してから、リネームする。パイプを通す。
for /f "delims=" %%f in ('dir /b *.txt') do (
ただし、パイプは非jisのunicodeを通さないので©などがあるとダメです。
その場合は、renameでパターンにマッチしないように変えて、再度戻す、2段階で。
*.txt → xxx*.zzz パターンにマッチしないように
*.zzz → *.txt これは ren *.zzz *.txt でよい。- 編集済み ウィンドウズスクリプトプログラマ 2018年1月29日 16:22
2018年1月29日 13:21 -
© ウィンドウズスクリプトプログラマ - Windows Script Programmer 2018
再帰です。変更後の名前が昇順で後ろに追加されるせいです。
返答ありがとうございます。
再帰があるというのはどこかに記載があるでしょうか。
また追加されるのが最初の1ファイルだけというのがよく分かりません。
それと"*.txt"を"*"に変えると起こらないのは何故なのでしょうか?
(他にこのような現象に遭遇しているような方の記事に出会わないのも不思議です)よろしければお教えください。
- 編集済み satoh_ma 2018年1月30日 4:33
2018年1月30日 2:29 -
© ウィンドウズスクリプトプログラマ - Windows Script Programmer 2018
再帰になるのは
FindFirstFileでパターンを指定
FindNextFileで現エントリの次から探すという仕組みから明らか。
0foo_01.txt
0foo_02.txt
0foo_03.txt
0foo_04.txt ←
10foo_01.txt ← 離れてる ←
---
10foo_02.txt ← 隣 近いキャッシュ単位とかの影響では。
*の場合は
0foo_04.txt
a.bat
がキャッシュにあって、
10foo_01.txt
は見えないとか。真偽は不明。
探したら、ありました。
forコマンドの憂鬱。ファイルセットのリカージョン: Windows Script Programming
これは、win32apiの仕様なので、for renに限ったことではありません。どの開発言語でも同じです。
For Each File In Folder.Files
こんな風にしてました。
k=k+1
Set arr(k)=File
Next
For k=1 To Folder.Files.Count
Set File=arr(k)
File.Name="_" & File.Name
Next
- 回答としてマーク satoh_ma 2018年2月1日 4:54
- 編集済み ウィンドウズスクリプトプログラマ 2018年2月1日 5:22
2018年1月31日 11:37 -
お付き合いありがとうございます。
ですが、うーん、まだ釈然としません。
© ウィンドウズスクリプトプログラマ - Windows Script Programmer 2018
再帰になるのは
FindFirstFileでパターンを指定
FindNextFileで現エントリの次から探すという仕組みから明らか。
(*.txt)とした場合、先にそれが展開されるのだと漠然と思ったのですが、そうではなく順次列挙されるということですね。
(まぁ結果からそう思えましたが、最近はLinuxでshを組む機会が多いもので余計に展開されるという先入観が…)
また実際に昔Win32APIでプログラムを組んでいた経験もありますが、このような現象に出会ったことはありませんでした。
(単純このケースに当たるようなコードを使った事があったかどうかは忘却の彼方ですが)
あらためて試してみたいところですが、あいにく現在それができる環境が無いのが残念。
© ウィンドウズスクリプトプログラマ - Windows Script Programmer 2018
キャッシュ単位とかの影響では。 *の場合は
0foo_04.txt
a.bat
がキャッシュにあって、
10foo_01.txt
は見えないとか。
これはあらためてディレクトリから"a.bat"を外してtxtファイルのみとして試してみましたが、再帰は起こりませんでした。
"*.txt"の場合再帰が起こっているという事自体は、実際の現象からも納得するしかありませんが、
一番の疑問は、何故再度処理されるのが1ファイルなのか、何故"*"にすると起こらないのか、という点ですね…
いっそ「forコマンドのバグだ」と言われればすっきりするんですが。(笑)2018年2月1日 2:55 -
© ウィンドウズスクリプトプログラマ - Windows Script Programmer 2018
たぶん
パターン指定のfindfirstfileは存否確認が多く、findnextfileは呼ばないことが多いのでキャッシュを残さない。
パターン指定なし(*)のfindfirstfileとfindnextfileはfindnextfileを呼ぶことが多いのでキャッシュを残す。
つまり、パターン指定のfindfirstfileとfindnextfileの間でのみ、キャッシュのリフレッシュが行われる。
1つだけ再帰というのは、エントリが少なくて、すべてキャッシュに載る場合でしょう。大量エントリなら、総エントリ数/キャッシュ単位 となるのではないか。
また、findfirstfileで大量エントリをすべてキャッシュするわけに行かないので再帰が発生するのは明らかでしょう。- 編集済み ウィンドウズスクリプトプログラマ 2018年2月1日 5:32
2018年2月1日 4:43 -
これは、FindFirstFile() / FindNextFile() API の再帰の問題なんかではなく、ファイルシステム ドライバの検索ロジックに起因する現象であると考えられます。
そもそも再帰に起因する現象であるなら、a.bat のパラメータ (0, 1, 2) に依存することなく同じように現象が起きるはずですが、実際には違いますよね?
質問での a.bat パラメータには 0, 1, 2 を指定していますが、これを 0, 0, 0 や 1, 1, 1 などの同一文字の繰り返しにすると起きないはずです。(厳密には起きる「可能性」もあるけど。)
再帰が原因なら、0, 0, 0 や 1, 1, 1 などのパラメータ指定でも起きないと辻褄が合わない。。。つまり再帰が原因ではない左証になると思います。ぢゃぁ、どーいうことかというと。。。。
for コマンドでのファイル検索処理では確かに FindFirstFile() / FindNextFile() API が使用されますが、これらの API は単なるインターフェイスにすぎません。
実際の検索処理はファイル システム ドライバ自身が行っています。
で、ファイル システム ドライバ側でのファイル検索ロジックは、個々のファイル システム (NTFS, FAT32, exFAT) に依存して異なります。
USB メモリを FAT32 や exFAT でフォーマットしなおして試せば確認できると思いますが、今回の現象はファイル システム (フォーマット) に依存して変化するはずです。
もしこの現象が FindFirstFile() / FindNextFile() API に起因しているならファイル システムには依存しないはずですが、実際にはそうならない(はずで)、即ち API よりももっと低レベルな部分の処理 (つまりファイル システム ドライバ) での挙動に影響を受けている。。。ということです。
FindFirstFile() / FindNextFile() API の結果がファイル システム ドライバ側の処理に依存して変化することに関しては、かるーくではありますが、下記 FindNextFile() API の説明でも触れられています。
(蛇足ですけど、FindFirstFile() / FindNextFile() API にキャッシュを制御する機能はありません。)
---------------------------------------------
FindNextFile function
https://msdn.microsoft.com/en-us/library/windows/desktop/aa364428(v=vs.85).aspxRemarks
....
The order in which the search returns the files, such as alphabetical order, is not guaranteed, and is dependent on the file system.
....
The order in which this function returns the file names is dependent on the file system type.
With the NTFS file system and CDFS file systems, the names are usually returned in alphabetical order.
With FAT file systems, the names are usually returned in the order the files were written to the disk, which may or may not be in alphabetical order.
However, as stated previously, these behaviors are not guaranteed.
....
---------------------------------------------今回質問されたバッチ ファイルの挙動を調べたいのであれば、Microsoft 。。。 というか Sysinternals が公開している "Process Monitor" というツールを使えば、その詳細を確認することができます。
ちなみに。。。。
FindFirstFile() / FindNextFile() API に限った話ではありませんが、ほとんどの Win32 API は単なるインターフェイスです。
実際の処理は、それよりも低レベルの各種ドライバ等で行われるので、コマンドやそれに関連する API などの表面的な仕様だけでその挙動のすべてを把握するのは、かなり至難の業かと。2018年2月1日 6:40 -
新たな情報をありがとうございます。
確かに「0 > 0 > 0 > 1 > 1 > 1」のようなパターンでは起こりませんでした。
FindFirstFile/FindNextFileの説明は私も見ていたのですが、単にファイル名がソートされて返ってくるかどうかに関して言ってるだけだと思ってました。
実際の検索処理はファイル システム ドライバ自身が行っています。
なるほど、で試してみました。
で、ファイル システム ドライバ側でのファイル検索ロジックは、個々のファイル システム (NTFS, FAT32, exFAT) に依存して異なります。
USB メモリを FAT32 や exFAT でフォーマットしなおして試せば確認できると思いますが、今回の現象はファイル システム (フォーマット) に依存して変化するはずです。
exFATではこの現象は見られませんでしたが、FAT32ではまた異なる結果になりました。
F:\>a.bat 0
とりあえず、件の現象は仕様的に明に規定されているものでは無ということで覚えておこうと思います。
foo_01.txt
foo_02.txt
foo_03.txt
foo_04.txt
---
0foo_01.txt
0foo_02.txt
0foo_03.txt
0foo_04.txt
a.bat
F:\>a.bat 1
0foo_01.txt
0foo_02.txt
0foo_03.txt
0foo_04.txt
---
10foo_01.txt
10foo_02.txt
10foo_03.txt
10foo_04.txt
a.bat
F:\>a.bat 2
10foo_01.txt
10foo_02.txt
10foo_03.txt
10foo_04.txt
210foo_01.txt
210foo_02.txt
210foo_03.txt
210foo_04.txt
2210foo_01.txt
---
2210foo_02.txt
2210foo_03.txt
2210foo_04.txt
a.bat
22210foo_01.txt
F:\>a.bat 3
2210foo_02.txt
2210foo_03.txt
2210foo_04.txt
22210foo_01.txt
---
32210foo_02.txt
32210foo_03.txt
32210foo_04.txt
a.bat
322210foo_01.txt
2018年2月1日 10:08 -
© ウィンドウズスクリプトプログラマ - Windows Script Programmer 2018
F:\>a.bat 2
のところが特異点ですね。短いファイル名ができるところなので、そのせいかも。 エントリ数が変化するとキャッシュをリフレッシュするとか。
find*fileはファイルシステムに丸投げしてるだけだったような。キャッシュの制御はファイルシステムごと。少数エントリではそれに影響されるでしょう。多数エントリでは所詮キャッシュに載り切れないので再帰が発生するでしょう。
find*file → rename → find*file → rename → find*file → rename → find*file → rename →
find*file ごとに読むと遅いのでまとめ読みするのでしょう。
再帰を避けるのには、トランザクションを完全に分ける。
(find*file → find*file → find*file → → ) → (rename → rename → rename → →)- 編集済み ウィンドウズスクリプトプログラマ 2018年2月2日 4:43
2018年2月1日 11:30 -
> 上記forのin(*.txt)部分をin(*)とすると、
> この現象は起こらないようです。
> どのような仕様の影響なのでしょうか?先の返信で書き忘れたので、一応追加で返信しておきます。
(もっとも単なる推測ですけど。)
先の返信でも説明したように、今回の現象はファイル システム ドライバのファイル検索ロジックに起因するものと考えられますが、"in (*)" では発生しないというのも同様の理由と考えられます。
先に示した FindNextFile() API の説明で、"With the NTFS file system and CDFS file systems, the names are usually returned in alphabetical order." という一文があります。
つまり NTFS ドライバはファイル検索で検出されたファイルのリストをアルファベット順で返すように頑張ってくれる。。。ということですが、その検索ロジックはファイル検索時に与えられるフィルタ条件によって変化させる必要があると考えられます。
"in (*)" の場合はフィルタ条件が「無い」と同義になると思われ、その結果検索ロジックがシンプルになり、今回のような現象が起きにくくなる。。。。ということかと。で、FindFistFile() API の説明を読み返したら。。。今回のような現象が起きる可能性について言及されてますね。
----------------------------------------------
FindFirstFile function
https://msdn.microsoft.com/ja-jp/library/windows/desktop/aa364418%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396Remarks
.....
Be aware that some other thread or process could create or delete a file with this name
between the time you query for the result and the time you act on the information.
If this is a potential concern for your application,
one possible solution is to use the CreateFile function with CREATE_NEW
(which fails if the file exists) or OPEN_EXISTING (which fails if the file does not exist).
.....
----------------------------------------------上記説明を考慮すると for コマンド構文の性質上、「(セット)」で与えられる条件によっては、copy, move, ren 等のファイル名に影響を与えるコマンドと組み合わせて使用しすると、今回のような現象が起きても当然なのかもしれません。
つまり、FindFistFile() / FindNextFile() API の挙動は再帰やキャッシュなどはまったく無関係で、この API で検索処理を行っている最中に、検索対象のファイル名が変化する場合は、ちゃんと自分で考慮しなさい。。。ということなんだろうと思います。
これを「forコマンドのバグだ」と言ってしまうのはちょっと酷かと思いますが、for コマンド側の実装に「...」と思うのも確かに納得できます。
(for コマンド構文上の限界。。。ということなのかも。)
そもそも FindFistFile() / FindNextFile() API の説明では、再帰やキャッシュに関してまったく言及されていないので基本的にはそれら影響は関係なく、この API での検索中に対象ファイル名に変化が発生するか否かだけが問題になるのだと思います。
キャッシュ状態に依存して結果が変化する API なんて、意味ないですし。最後に蛇足ではありますが。。。。
FindFistFile() / FindNextFile() API コールでのファイル検索リクエストは、カーネル モード側で IRP_MJ_DIRECTORY_CONTROL - IRP_MN_QUERY_DIRECTORY / FileBothDirectoryInformation の IRP (I/O Request Packet) に変換され、最終的に対応するファイル システム ドライバに渡され処理されます。
このリクエストに関するドキュメントはそれなりに公開されているので、もし興味があるなら、そこら辺を調べてみると面白いかも。
----------------------------------------------
NtQueryDirectoryFile function
https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/content/ntifs/nf-ntifs-ntquerydirectoryfileIRP_MJ_DIRECTORY_CONTROL
https://docs.microsoft.com/ja-jp/windows-hardware/drivers/ifs/irp-mj-directory-control
----------------------------------------------- 回答としてマーク satoh_ma 2018年2月2日 4:41
2018年2月2日 3:14