Processando arquivos com nomes que contenham um “^” (Carat), inWindows

1

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

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

Como 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, depois 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

Ao executar este script, recebo 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"


Running again, with "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=""

Portanto, de qualquer forma, com ou sem o uso de "enabledelayedexpansion" , não posso processar arquivos com nomes que contenham "^" (Carat).

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

    
por Kevin Fegan 19.07.2014 / 17:53

3 respostas

2

Depois de brincar com isso por um tempo, eu criei esta solução de trabalho:

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

acidentalmente "tropecei" nessa solução de trabalho que usa um método que pode ser um comportamento não documentado de loops forçados aninhados.

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

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

Por engano eu digitei isso:

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

Eu não fiquei realmente surpreso (no início) quando isso funcionou, porque eu pensei que "sed" estava funcionando como esperado. Então, percebi que usei a variável errada: set "f1=%%~f" em vez de: set "f1=%%~g" , o que foi surpreendente.

Alterei 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"

nenhum dos quais funcionou.

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

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

    
por 19.07.2014 / 20:53
1

Parabéns para você por descobrir dois obscuros comportamentos de lote do Windows em um Q e A!

É impossível passar um número ímpar de carets entre aspas como uma string literal por meio de CALL no lote ou na linha de comando. Uma explicação pode ser encontrada na Fase 6) em Como o Windows Command Interpreter (CMD.EXE) analisa os 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 sem aspas consome um cursor como parte do comportamento de escape. A parte citada é deixada sozinha. O comando agora parece:

call echo Unquoted ^ "Quoted ^"

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

echo Unquoted ^^ "Quoted ^^"

O CALL passa por uma segunda fase 2), resultando em:

echo Unquoted ^ "Quoted ^^"

Produzindo a seguinte saída 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 com circunflexo através de CALL. Use uma estratégia alternativa. Existem várias opções. Eu listei alguns abaixo.

1a) Não use CALL. Você pode usar parênteses após o DO para criar um código arbitrariamente complexo. Esta é de longe a minha estratégia favorita, porque a chamada é inerentemente lenta. A única coisa que você não pode fazer é usar o GOTO dentro do loop, pois ele terminará imediatamente o processamento do loop. Se você precisar manipular as variáveis dentro do loop, será necessário ativar 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!, então você deve 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 quiser usar o CALL, então não passe o valor como um literal de string. Em vez disso, armazene o valor em uma variável de ambiente. Observe que o valor de var é citado para proteger contra caracteres especiais.

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

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


2b) Eu prefiro usar a expansão atrasada para que eu não precise se preocupar se os caracteres especiais da string são citados. Note que o valor de var não é citado porque a aspa de abertura aparece antes do nome da variável dentro da 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 saiba apenas trabalhar com uma variável, você pode passar o nome da variável como um argumento. Isso requer uma 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 para a variável "tunelamento" como você fez em sua resposta . Eu usei essa técnica no passado, mas não gosto porque é ofuscada. Alguém que tenta manter o código depois de ter sido escrito provavelmente não entenderá o que está acontecendo.

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

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 somente acessíveis dentro de um loop DO. Isso é explicado de forma críptica no sistema HELP embutido. Digite help for ou for /? para obter ajuda. A seção relevante está aproximadamente na metade do caminho. Observe a palavra em negrito no final da citação.

Some examples might help:

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

would parse each line in myfile.txt, ignoring lines that begin with a semicolon, passing the 2nd and 3rd token from each line to the for body, with tokens delimited by commas and/or spaces. Notice the for body statements reference %i to get the 2nd token, %j to get the 3rd token, and %k to get all remaining tokens after the 3rd. For file names that contain spaces, you need to quote the filenames with double quotes. In order to use double quotes in this manner, you also need to use the usebackq option, otherwise the double quotes will be interpreted as defining a literal string to parse.

%i is explicitly declared in the for statement and the %j and %k are implicitly declared via the tokens= option. You can specify up to 26 tokens via the tokens= line, provided it does not cause an attempt to declare a variable higher than the letter 'z' or 'Z'. Remember, FOR variables are single-letter, case sensitive, global, and you can't have more than 52 total active at any one time.

Isso é tanto documentação oficial quanto eu já vi para o comportamento. Muito enigmático, e não muito útil. De fato, muitas das informações contidas no último parágrafo estão simplesmente erradas ! Consulte o link para 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.

    
por 21.07.2014 / 17:31
0

Eu absolutamente não sou um especialista e, portanto, não posso apontar o que está errado no seu programa detalhado. Mas eu corri esse lote e encontrei algum 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)
    
por 19.07.2014 / 20:19