Windows で名前に「^」(キャレット)が含まれるファイルを処理する

Windows で名前に「^」(キャレット)が含まれるファイルを処理する

名前に「」を含むファイルを処理できません"^"(キャレット)。

私が気づいたのは、ファイル名を評価するときに二重引用符を使用すると、「キャレット」が二重になるということです。二重引用符を使用しない場合、ファイル名の「キャレット」は二重になりません (保持されます) が、一部のファイル名には埋め込まれたスペースが含まれているため、二重引用符を付けてファイル名を評価する必要があります。

たとえば、いくつかのファイルを含むフォルダーがあるとします。

G:\Test-folder\Abcxyz 1.txt
G:\Test-folder\Abcxyz2.txt
G:\Test-folder\Abcxyz3.txt
G:\Test-folder\Abc^xyz 1.txt
G:\Test-folder\Abc^xyz2.txt
G:\Test-folder\Abc^xyz3^.txt

ファイル名を収集し、ファイル名を読み取って各ファイルを処理するバッチ スクリプトがあります。

@echo off

rem collect the filenames
dir /s /b "G:\Test-folder\ab*" > "G:\Test-folder\list.txt"

rem Note: here I have an opportunity to inspect and modify the filenames as necessary, but I have not found any modifications that solve this problem. 

rem process each file
for /f "usebackq delims=" %%f in ("G:\Test-folder\list.txt") do call :work "%%~f"

@echo.
@echo Back: f1="%f1%"
@echo.
@echo.

@echo Running again, with "setlocal enabledelayedexpansion".
@echo.

for /f "usebackq delims=" %%f in ("G:\Test-folder\list.txt") do call :work2 "%%~f"

@echo.
@echo Back: f2="%f2%"
@echo.
goto :EOF



:work
rem :work

set "f1=%~1"

if exist "%f1%" goto :dostuff

@echo.
@echo File "%f1%" not found.
@echo       %f1%
@echo      "%~1"
@echo       %~1
@echo.
goto :EOF

:dostuff
rem do some stuff :dostuff
@echo File "%f1%" found.
goto :EOF



:work2
rem :work2

setlocal enabledelayedexpansion
set "f2=%~1"

if exist "!f2!" goto :dostuff2

@echo.
@echo File "!f2!" not found.
@echo       !f2!
@echo      "%~1"
@echo       %~1
@echo.
endlocal
goto :EOF

:dostuff2
rem do some stuff :dostuff2
@echo File "!f2!" found.
endlocal
goto :EOF

このスクリプトを実行すると、次の出力が得られます。

File "G:\Test-folder\Abcxyz 1.txt" found.
File "G:\Test-folder\Abcxyz2.txt" found.
File "G:\Test-folder\Abcxyz3.txt" found.

File "G:\Test-folder\Abc^^xyz 1.txt" not found.
      G:\Test-folder\Abc^xyz 1.txt
     "G:\Test-folder\Abc^^xyz 1.txt"
      G:\Test-folder\Abc^xyz 1.txt


File "G:\Test-folder\Abc^^xyz2.txt" not found.
      G:\Test-folder\Abc^xyz2.txt
     "G:\Test-folder\Abc^^xyz2.txt"
      G:\Test-folder\Abc^xyz2.txt


File "G:\Test-folder\Abc^^xyz3^^.txt" not found.
      G:\Test-folder\Abc^xyz3^.txt
     "G:\Test-folder\Abc^^xyz3^^.txt"
      G:\Test-folder\Abc^xyz3^.txt


Back: f1="G:\Test-folder\Abc^^xyz3^^.txt"

で再度実行しますsetlocal enabledelayedexpansion

File "G:\Test-folder\Abcxyz 1.txt" found.
File "G:\Test-folder\Abcxyz2.txt" found.
File "G:\Test-folder\Abcxyz3.txt" found.

File "G:\Test-folder\Abc^^xyz 1.txt" not found.
      G:\Test-folder\Abc^^xyz 1.txt
     "G:\Test-folder\Abc^^xyz 1.txt"
      G:\Test-folder\Abc^xyz 1.txt


File "G:\Test-folder\Abc^^xyz2.txt" not found.
      G:\Test-folder\Abc^^xyz2.txt
     "G:\Test-folder\Abc^^xyz2.txt"
      G:\Test-folder\Abc^xyz2.txt


File "G:\Test-folder\Abc^^xyz3^^.txt" not found.
      G:\Test-folder\Abc^^xyz3^^.txt
     "G:\Test-folder\Abc^^xyz3^^.txt"
      G:\Test-folder\Abc^xyz3^.txt


Back: f2=""

いずれにしても、使用の有無にかかわらず、"enabledelayedexpansion"、名前に「」を含むファイルを処理できません"^"(キャレット)。

これをどうやって行うべきか、あるいは私が何を間違っているかについて何かアイデアはありますか?

答え1

しばらくこれを試してみた後、次のような実用的な解決策を思いつきました。

@echo off

rem collect the filenames
dir /s /b "G:\Test-folder\ab*" >"G:\Test-folder\list.txt"

rem process each file
for /f "usebackq delims=" %%f in ("G:\Test-folder\list.txt") do call :work "%%~f"
@echo.

rem Note: I still could not make this work with "setlocal enabledelayedexpansion".

goto :EOF



:work
rem :work

set "f1=%~1"

if exist "%f1%" goto :dostuff

@echo.
@echo File "%f1%" not found.
@echo       %f1%
@echo      "%~1"
@echo       %~1
@echo.

rem Notice that the "action" of this (next) for-loop is: [set "f1=%%~f"]
rem which uses the "for-variable" from the "outer" for-loop: "%%f"
rem instead of the "for-variable" from the "this" for-loop: "%%g"

@for /f "usebackq delims=" %%g in (`echo "dummy"`) do set "f1=%%~f"

if exist "%f1%" goto :dostuff

@echo File "%f1%" not found.
@echo       %f1%
@echo.
goto :EOF

:dostuff
rem do some stuff :dostuff

@echo File "%f1%" found.
for %%g in ("%f1%") do echo name:"%%~ng" extn:"%%~xg" file-size:"%%~zg"
@echo.
goto :EOF

このスクリプトを実行した場合の出力は次のようになります。

File "G:\Test-folder\Abcxyz 1.txt" found.
name:"Abcxyz 1" extn:".txt" file-size:"14"

File "G:\Test-folder\Abcxyz2.txt" found.
name:"Abcxyz2" extn:".txt" file-size:"13"

File "G:\Test-folder\Abcxyz3.txt" found.
name:"Abcxyz3" extn:".txt" file-size:"13"


File "G:\Test-folder\Abc^^xyz 1.txt" not found.
      G:\Test-folder\Abc^xyz 1.txt
     "G:\Test-folder\Abc^^xyz 1.txt"
      G:\Test-folder\Abc^xyz 1.txt

File "G:\Test-folder\Abc^xyz 1.txt" found.
name:"Abc^xyz 1" extn:".txt" file-size:"15"


File "G:\Test-folder\Abc^^xyz2.txt" not found.
      G:\Test-folder\Abc^xyz2.txt
     "G:\Test-folder\Abc^^xyz2.txt"
      G:\Test-folder\Abc^xyz2.txt

File "G:\Test-folder\Abc^xyz2.txt" found.
name:"Abc^xyz2" extn:".txt" file-size:"14"


File "G:\Test-folder\Abc^^xyz3^^.txt" not found.
      G:\Test-folder\Abc^xyz3^.txt
     "G:\Test-folder\Abc^^xyz3^^.txt"
      G:\Test-folder\Abc^xyz3^.txt

File "G:\Test-folder\Abc^xyz3^.txt" found.
name:"Abc^xyz3^" extn:".txt" file-size:"15"

私は偶然、ネストされた for ループの文書化されていない動作である可能性のあるメソッドを使用するこの実用的なソリューションに「偶然」出会いました。

私は「sed」を使って変更しようとしていました"^^"引用符で囲まれた文字列を単一の"^"、 このような:

@for /f "usebackq delims=" %%g in (`echo "%f1%"^|sed -r "s/(\x5e)\1/\1/g"`) do set "f1=%%~g"

代わりに、誤って次のように入力しました:

@for /f "usebackq delims=" %%g in (`echo "%f1%"^|sed -r "s/(\x5e)\1/\1/g"`) do set "f1=%%~f"

これが機能したとき、私は(最初は)あまり驚きませんでした。なぜなら、「sed」が期待通りに機能していると思ったからです。その後、間違った for 変数を使用していたことに気付きました。set "f1=%%~f"の代わりに:set "f1=%%~g"、それは驚きでした。

正しい変数を使用するように変更しました:set "f1=%%~g"しかし、それは機能しないことがわかりました。

私はこれを含め、さまざまなバージョンを試しました:

@for /f "usebackq delims=" %%g in (`echo "%f1%"`) do set "f1=%%~g"

どれも機能しませんでした。

したがって、これは間違った for 変数を使用して「誤用」された場合にのみ機能するようです。この場合は便利に思えますが、長期的に機能するとは信じがたいです。

これが実際に「文書化された」(予想される)動作であるかどうか、他の人の意見を聞くのは非常に興味深いです。

答え2

1 つの Q&A で 2 つのわかりにくい Windows バッチ動作を発見していただき、ありがとうございます。

バッチまたはコマンドラインのいずれにおいても、CALLを介して引用符で囲まれた奇数のキャレットを文字列リテラルとして渡すことはできません。説明はフェーズ6)にあります。Windows コマンド インタープリター (CMD.EXE) はスクリプトをどのように解析しますか?

問題の例を以下に示します。スクリプトに次のコマンドが含まれているとします。

call echo Unquoted ^^ "Quoted ^"

パーサーのフェーズ 2 の後、引用符で囲まれていない部分はエスケープ動作の一部としてキャレットを消費します。引用符で囲まれた部分はそのまま残されます。コマンドは次のようになります。

call echo Unquoted ^ "Quoted ^"

フェーズ 6 で CALL が検出されると、すべてのキャレットが 2 倍になり、次の内容が CALL メカニズムに渡されます。

echo Unquoted ^^ "Quoted ^^"

CALL は第 2 フェーズ 2) を経て、次のようになります。

echo Unquoted ^ "Quoted ^^"

次の最終出力が生成されます。

Unquoted ^ "Quoted ^^"

FOR ループの例では、FOR 変数の拡張がフェーズ 2 の後に発生するため、最初のフェーズ 2 はバイパスされます。


解決策 - キャレットを含む引用符で囲まれた文字列リテラルを CALL に渡さないでください。別の戦略を使用します。いくつかのオプションがあります。以下にいくつか挙げました。

1a)CALL はまったく使用しないでください。DO の後に括弧を使用して、任意の複雑なコードを作成できます。CALL は本質的に遅いため、これは私のお気に入りの戦略です。唯一できないのは、ループ内で GOTO を使用することです。ループ処理が直ちに終了するためです。ループ内で変数を操作する必要がある場合は、遅延展開を有効にして使用する必要があります。

setlocal enableDelayedExpansion
for ....%%A  in (...) do (
  set "var=%%A"
  echo the value of var=!var!
  ... whatever
)


1b)FOR 変数に ! が含まれている可能性がある場合は、破損を防ぐために、ループ内で遅延展開のオンとオフを切り替える必要があります。

for ... %%A in (...) do (
  setlocal enableDelayedExpansion
  set "var=%%A"
  echo the value of var=!var!
  ... whatever
  endlocal
)


2a)どうしても CALL を使用したい場合は、値を文字列リテラルとして渡さないでください。代わりに、値を環境変数に格納します。var の値は特殊文字から保護するために引用符で囲まれていることに注意してください。

for ... %%A in (...) do (
  set var="%%~A"
  call :work
)
exit /b

:work
echo var=%var%
... etc.
exit /b


2b)文字列内の特殊文字が引用符で囲まれているかどうかを気にしなくて済むように、遅延展開を使用することを好みます。開始引用符が SET ステートメント内の変数名の前に表示されるため、var の値は引用符で囲まれていないことに注意してください。

for ... %%A in (...) do (
  set "var=%%~A"
  call :work
)
exit /b

:work
setlocal enableDelayedExpansion
echo var=!var!
... etc.
exit /b


2c)1 つの変数のみを扱うサブルーチンを書く代わりに、変数の名前を引数として渡すことができます。これには遅延展開が必要です。

for ... %%A in (...) do (
  set "var=%%~A"
  call :work var
)
exit /b

:work
setlocal enableDelayedExpansion
echo %1=!%1!
... etc.
exit /b


3)FOR変数「トンネリング」を次のように使用します。あなたの答え過去にこの手法を使用したことがありますが、難読化されているため好きではありません。コードを書いた後にそれを保守しようとする人は、何が起こっているのか理解できないでしょう。

FOR 変数のスコープは、FOR ステートメントの DO ループ内でのみ有効です。ループから CALL すると、スコープは終了します。ただし、ご存じのとおり、CALL されたルーチンに独自の FOR ステートメントがある場合は、古い FOR 変数が「魔法のように」再び表示されます。

for ... %%A in (...) do call :work
exit /b

:work
echo The A variable is no longer in scope: %%A
for %%x in (x) do echo The A variable is back: %%A

説明によると、FOR 変数はグローバルですが、DO ループ内でのみアクセスできます。これは、組み込みのヘルプ システムで分かりやすく説明されています。ヘルプを表示するにはhelp for、またはとfor /?入力してください。関連するセクションは、約半分のところにあります。引用の最後の方にある太字の単語に注意してください。

いくつかの例が役立つかもしれません:

FOR /F "eol=; tokens=2,3* delims=, " %i in (myfile.txt) do @echo %i %j %k

は、myfile.txt の各行を解析し、セミコロンで始まる行を無視し、各行の 2 番目と 3 番目のトークンを for 本体に渡します。トークンはコンマやスペースで区切られます。for 本体のステートメントでは、2 番目のトークンを取得するために %i を参照し、3 番目のトークンを取得するために %j を参照し、3 番目のトークンの後の残りのすべてのトークンを取得するために %k を参照していることに注意してください。スペースを含むファイル名の場合は、ファイル名を二重引用符で囲む必要があります。この方法で二重引用符を使用するには、usebackq オプションも使用する必要があります。そうしないと、二重引用符は解析するリテラル文字列を定義するものとして解釈されます。

%i は for 文で明示的に宣言され、%j と %k は tokens= オプションで暗黙的に宣言されます。tokens= 行で最大 26 個のトークンを指定できますが、文字 'z' または 'Z' より大きい変数を宣言しようとしないようにしてください。FOR 変数は 1 文字で、大文字と小文字が区別されます。グローバル一度にアクティブにできるアカウントは合計 52 個までです。

これは、この動作に関する公式ドキュメントとしては私がこれまで見た中で最も長いものです。非常に難解で、あまり役に立ちません。実際、最後の段落の情報の多くは単純に間違っている! 見るhttps://stackoverflow.com/a/8520993/1012053使用可能な FOR 変数の最大数と、FOR 変数に有効な文字が何であるかについての真実。

答え3

私はまったく専門家ではないので、あなたの冗長なプログラムのどこが間違っているのかを指摘することはできません。しかし、このバッチを実行して、意図した結果を見つけました。

@ECHO OFF

DIR /b /s >list.txt

SETLOCAL enabledelayedexpansion
FOR /f "delims=" %%x IN (list.txt) DO IF EXIST "%%x" (@ECHO %%x found) else (@ECHO %%x not found)

関連情報