%~sIのバグについて

先日予告した通り、news:alt.msdos.batch.nt で発見した ~s 修飾子のバグについて。

for変数や、バッチ引数には、%~sI%~s1 という修飾子でショートネーム(または8.3形式。以下ではSFNと書く)形式のパス名・ファイル名を得ることが出来る。しかしこれにはバグがある。NT4時代からあって、直っていないらしい。このバグは簡単に再現できる。

md "long long long dir"
cd "long long log dir"
echo.>"A B.txt"
for %%A in (*.txt) do echo %%~sA

本来は、\LONGLO~1\ABBE64~1.TXT のように表示されるべき部分が、\LONGLO~1\ABBE64~1.TXTB.txt のように後ろにごみ(LFNの残骸)が付いてしまう。
こういうバグがある以上、特定の環境で一時的に使うバッチならともかく、汎用ツールを作る時は ~s 修飾子を安易に使えないと言うことだ。

解決法としては、パスの上位部分から順にSFN形式にしていくしか無いようだ。汎用サブルーチン :sfnsub とその(上記スクリプトで作ったファイルでの)テストスクリプトを示す。:sfnsub の仕様としては、" で囲んだフルパスを引数にすると、環境変数 SFN にそのSFNを返す。" で引数を囲むのは必須。なお、これと次のスクリプトは私とfoxidrive氏の合作である。

@echo off
setlocal
for %%A in ("A B.txt") do @echo %%~sA&call :sfnsub "%%~fA"
echo %SFN%
goto :eof
:sfnsub
set ARG=%1
set ARG=%ARG:\=\" "%
set SFN=
call :loop %ARG%
goto :eof
:loop
for %%A in ("%SFN%%~1") do set SFN=%%~sA
shift
if not "%~1"=="" goto :loop
set "SFN=%SFN:&=^&%"
set "SFN=%SFN:^^=^%"
goto :eof

汎用と書いたが、%スクリプト中でサブルーチンの引数として渡せない(注1)ので % を含んだファイル名は駄目だがそれ以外のファイル名に使える特殊文字には対応しているはずだ(まだ漏れがあるかもしれないが)。なお、この方法と別に COMMAND.COM を使ってカレントディレクトリを SFN に変換する別法もある。

@echo off
setlocal
for %%A in ("A B.txt") do @echo %%~sA&call :sfnsub "%%~fA"
echo %SFN%
goto :eof

:sfnsub
pushd .
cd /d "%~dp1"
command /c rem
for %%A in ("%~nx1") do set SFN=%%~sA
popd
goto :eof

こっちのほうがわかりやすい。ただし、日本語版Windowsの場合は初回のCOMMAND.COM実行時に画面がクリアされていろいろメッセージが出る(注2)のでいまいちである。

いずれにせよたかがSFNを得るために大げさな話であり、スクリプトではSFNを使うのを避けたほうがいいだろう。
\Program files\ の下だけ調べてみたが、%~sI が正しい結果を返さないファイルは結構ある。

(注1:もちろん、% を %%%% に置換してから引数に使えばよいが、for変数には置換が使えないし、置換するために一般の環境変数に一旦セットすればさらに多くの問題を引き起こすので現実的でない)
(注2:英語版では出ないらしい。これを出さない方法は無いだろうか?)