Processando arquivos com nomes que contêm um "^" (Caret) no Windows

Processando arquivos com nomes que contêm um "^" (Caret) no Windows

Estou tendo problemas para processar arquivos com nomes que contenham um"^"(Careta).

O que estou percebendo é que se eu usar aspas duplas ao avaliar os nomes dos arquivos, os "Caret's" serão duplicados. Se eu não usar aspas duplas, os "Caret's" nos nomes dos arquivos NÃO serão duplicados (preservados), mas como alguns nomes de arquivos contêm espaços incorporados, tenho que avaliar os nomes dos arquivos COM as aspas duplas.

Por exemplo, tenho uma pasta que contém alguns arquivos:

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

Eu tenho um script em lote que coleta os nomes dos arquivos, lê os nomes dos arquivos e processa cada arquivo.

@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

Executando este script, obtenho a seguinte saída:

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"

Correndo novamente, com 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=""

Então, de qualquer forma, com ou sem o uso de"enabledelayedexpansion", não consigo processar arquivos com nomes que contenham um"^"(Careta).

Alguma idéia de como fazer isso ou o que estou fazendo de errado?

Responder1

Depois de brincar com isso por um tempo, descobri esta solução funcional:

@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

A saída da execução deste script é:

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"

Eu acidentalmente "tropecei" nesta solução funcional que usa um método que pode ser um comportamento não documentado de loops for aninhados.

Eu estava tentando usar "sed" para alterar o"^^"na string citada para um único"^", assim:

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

Em vez disso, digitei isso por engano:

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

Não fiquei realmente surpreso (a princípio) quando isso funcionou, porque pensei que "sed" estava funcionando conforme o esperado. Então, percebi que havia usado a variável for errada:set "f1=%%~f"em vez de:set "f1=%%~g", o que foi surpreendente.

Eu mudei para usar a variável correta:set "f1=%%~g", apenas para descobrir que não funcionou.

Eu tentei várias versões disso, incluindo:

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

nada disso funcionou.

Portanto, isso só parece funcionar se for "mal utilizado" usando a variável for errada. Embora isso pareça útil neste caso, tenho dificuldade em confiar que funcionará a longo prazo.

Eu ficaria muito interessado em ouvir de outras pessoas se este é realmente um comportamento "documentado" (esperado) ou não.

Responder2

Parabéns a você por descobrir dois comportamentos obscuros de lote do Windows em uma pergunta e uma resposta!

É impossível passar um número ímpar de caracteres entre aspas como uma string literal por meio de CALL em lote ou linha de comando. Uma explicação pode ser encontrada na Fase 6) emComo o Windows Command Interpreter (CMD.EXE) analisa scripts?.

Aqui está um exemplo do problema. Suponha que um script contenha o seguinte comando:

call echo Unquoted ^^ "Quoted ^"

Após a fase 2 do analisador, a parte não citada consome um sinal de intercalação como parte do comportamento de escape. A parte citada é deixada sozinha. O comando agora se parece com:

call echo Unquoted ^ "Quoted ^"

Após a detecção de CALL na fase 6, todos os sinais de intercalação são duplicados e o seguinte é passado pelo mecanismo CALL:

echo Unquoted ^^ "Quoted ^^"

A CALL passa por uma 2ª fase 2), resultando em:

echo Unquoted ^ "Quoted ^^"

Produzindo o seguinte resultado final:

Unquoted ^ "Quoted ^^"

Seu exemplo com o loop FOR ignora a fase inicial 2 porque a expansão da variável FOR ocorre após a fase 2.


A solução - Não passe literais de string entre aspas contendo acento circunflexo por meio de CALL.Use uma estratégia alternativa. Existem várias opções. Listei alguns abaixo.

1a)Não use CALL. Você pode usar parênteses após DO para criar código arbitrariamente complexo. Esta é de longe a minha estratégia favorita, porque CALL é inerentemente lento. A única coisa que você não pode fazer é usar GOTO dentro do loop, pois isso encerrará imediatamente o processamento do loop. Se precisar manipular variáveis ​​dentro do loop, você precisará habilitar e usar a expansão atrasada.

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


1b)Se a variável FOR puder conter !, você deverá ativar e desativar a expansão atrasada dentro do loop para evitar corrupção.

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


2a)Se você realmente deseja usar CALL, não passe o valor como uma string literal. Em vez disso, armazene o valor em uma variável de ambiente. Observe que o valor de var é citado para proteção contra caracteres especiais.

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

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


2b)Prefiro usar a expansão atrasada para não precisar me preocupar se os caracteres especiais dentro da string estão entre aspas. Observe que o valor de var não está entre aspas porque a aspa de abertura aparece antes do nome da variável na instrução SET.

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

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


2c)Em vez de escrever uma sub-rotina que só sabe trabalhar com uma variável, você pode passar o nome da variável como argumento. Isso requer expansão atrasada.

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

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


3)Use a variável FOR "tunneling" como você fez emsua Resposta. Já usei essa técnica no passado, mas não gosto dela porque é ofuscada. Alguém que tenta manter o código depois de escrito provavelmente não entenderá o que está acontecendo.

Variáveis ​​FOR só têm escopo dentro do loop DO de uma instrução FOR. Quando você CALL sai do loop, o escopo termina. Mas, como você descobriu, se a rotina CALL tiver sua própria instrução FOR, as variáveis ​​FOR mais antigas reaparecerão "magicamente".

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

A explicação é que as variáveis ​​FOR são globais, mas acessíveis apenas dentro de um loop DO. Isto é explicado enigmaticamente no sistema de AJUDA integrado. Digite help forou for /?para obter ajuda. A seção relevante está na metade do caminho. Observe a palavra em negrito no final da citação.

Alguns exemplos podem ajudar:

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

analisaria cada linha em myfile.txt, ignorando as linhas que começam com ponto e vírgula, passando o segundo e o terceiro token de cada linha para o corpo for, com tokens delimitados por vírgulas e/ou espaços. Observe que as instruções for body fazem referência a %i para obter o segundo token, %j para obter o terceiro token e %k para obter todos os tokens restantes após o terceiro. Para nomes de arquivos que contêm espaços, você precisa colocar os nomes dos arquivos entre aspas duplas. Para usar aspas duplas dessa maneira, você também precisa usar a opção usebackq, caso contrário, as aspas duplas serão interpretadas como definindo uma string literal a ser analisada.

%i é declarado explicitamente na instrução for e %j e %k são declarados implicitamente por meio da opção tokens=. Você pode especificar até 26 tokens através da linha tokens=, desde que isso não cause uma tentativa de declarar uma variável maior que a letra 'z' ou 'Z'. Lembre-se, as variáveis ​​FOR são de uma única letra, diferenciam maiúsculas de minúsculas,global, e você não pode ter mais de 52 ativos no total ao mesmo tempo.

Essa é a documentação oficial que já vi sobre o comportamento. Muito enigmático e não muito útil. Na verdade, muitas das informações contidas no último parágrafo são simplesmenteerrado! Verhttps://stackoverflow.com/a/8520993/1012053para saber a verdade sobre o número máximo de variáveis ​​FOR disponíveis e quais são os caracteres válidos para variáveis ​​FOR.

Responder3

Não sou absolutamente um especialista e, portanto, não posso apontar o que há de errado no seu programa detalhado. Mas executei este lote e encontrei o resultado pretendido:

@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)

informação relacionada