Procesar archivos con nombres que contienen un "^" (Caret) en Windows

Procesar archivos con nombres que contienen un "^" (Caret) en Windows

Tengo problemas para procesar archivos con nombres que contienen un"^"(Signo de intercalación).

Lo que estoy notando es que si uso comillas dobles al evaluar los nombres de archivos, los "Caret" se duplican. Si no uso las comillas dobles, los "Caret" en los nombres de archivos NO se duplican (conservan), pero como algunos de los nombres de archivos contienen espacios incrustados, tengo que evaluar los nombres de archivos CON las comillas dobles.

Como ejemplo, tengo una carpeta que contiene algunos archivos:

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

Tengo un script por lotes que recopila los nombres de los archivos, luego los lee y procesa cada archivo.

@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

Al ejecutar este script, obtengo el siguiente resultado:

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"

Corriendo de nuevo, con 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=""

Entonces, de cualquier manera, con o sin el uso de"enabledelayedexpansion", no puedo procesar archivos con nombres que contengan un"^"(Signo de intercalación).

¿Alguna idea sobre cómo hacer esto o qué estoy haciendo mal?

Respuesta1

Después de jugar con esto por un tiempo, se me ocurrió esta solución 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

El resultado de ejecutar este script es:

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"

Accidentalmente "me topé" con esta solución funcional que utiliza un método que puede ser un comportamiento no documentado de bucles for anidados.

Estaba intentando usar "sed" para cambiar el"^^"en la cadena citada a un solo"^", como esto:

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

En su lugar, escribí esto por error:

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

Realmente no me sorprendió (al principio) que esto funcionara, porque pensé que "sed" estaba funcionando como se esperaba. Entonces, me di cuenta de que había usado la variable for incorrecta:set "f1=%%~f"en lugar de:set "f1=%%~g", lo cual fue sorprendente.

Lo cambié para usar la variable correcta:set "f1=%%~g", sólo para descubrir que no funcionó.

Probé varias versiones de esto, incluyendo:

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

Ninguno de los cuales funcionó.

Por lo tanto, esto sólo parece funcionar si se "usa mal" mediante el uso de la variable for incorrecta. Si bien esto parece útil en este caso, me cuesta confiar en que funcionará a largo plazo.

Me interesaría mucho saber de otros si este es realmente un comportamiento "documentado" (esperado) o no.

Respuesta2

¡Felicitaciones por descubrir dos comportamientos oscuros por lotes de Windows en una pregunta y respuesta!

Es imposible pasar un número impar de signos de intercalación entre comillas como una cadena literal a través de CALL, ya sea en lotes o en la línea de comando. Puede encontrar una explicación en la Fase 6) en¿Cómo analiza los scripts el intérprete de comandos de Windows (CMD.EXE)?.

Aquí hay un ejemplo del problema. Supongamos que un script contiene el siguiente comando:

call echo Unquoted ^^ "Quoted ^"

Después de la fase 2 del analizador, la parte sin comillas consume un signo de intercalación como parte del comportamiento de escape. La parte citada se deja sola. El comando ahora se ve así:

call echo Unquoted ^ "Quoted ^"

Tras la detección de CALL en la fase 6, todos los signos de intercalación se duplican y lo siguiente pasa a través del mecanismo CALL:

echo Unquoted ^^ "Quoted ^^"

La CONVOCATORIA pasa por una 2da fase 2), resultando:

echo Unquoted ^ "Quoted ^^"

Produciendo el siguiente resultado final:

Unquoted ^ "Quoted ^^"

Su ejemplo con el bucle FOR omite la fase inicial 2 porque la expansión de la variable FOR ocurre después de la fase 2.


La solución: no pase cadenas literales entre comillas que contengan signos de intercalación a través de CALL.Utilice una estrategia alternativa. Hay varias opciones. He enumerado algunos a continuación.

1a)No utilice CALL en absoluto. Puede utilizar paréntesis después del DO para crear código arbitrariamente complejo. Esta es, con diferencia, mi estrategia favorita, porque CALL es inherentemente lenta. Lo único que no puede hacer es usar GOTO dentro del bucle, ya que finalizará inmediatamente el procesamiento del bucle. Si necesita manipular variables dentro del bucle, deberá habilitar y utilizar la expansión retardada.

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


1b)Si la variable FOR puede contener !, entonces debe activar y desactivar la expansión retrasada dentro del bucle para evitar daños.

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


2a)Si realmente desea utilizar CALL, no pase el valor como una cadena literal. En su lugar, almacene el valor en una variable de entorno. Tenga en cuenta que el valor de var se cita para proteger contra caracteres especiales.

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

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


2b)Prefiero usar la expansión retrasada para no tener que preocuparme si se citan caracteres especiales dentro de la cadena. Tenga en cuenta que el valor de var no se cita porque la comilla inicial aparece antes del nombre de la variable dentro de la declaración SET.

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

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


2c)En lugar de escribir una subrutina que sólo sepa cómo trabajar con una variable, puede pasar el nombre de la variable como argumento. Esto requiere una expansión retrasada.

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

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


3)Utilice FOR la ​​variable "tunelización" como lo hizo enTu respuesta. He usado esta técnica en el pasado, pero no me gusta porque está confusa. Alguien que intente mantener el código después de haberlo escrito probablemente no entenderá lo que está pasando.

Las variables FOR solo tienen alcance dentro del bucle DO de una declaración FOR. Cuando LLAMA fuera del bucle, el alcance finaliza. Pero, como habrá descubierto, si la rutina CALLed tiene su propia declaración FOR, las variables FOR más antiguas reaparecen "mágicamente".

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

La explicación es que las variables FOR son globales, pero solo se puede acceder a ellas dentro de un bucle DO. Esto se explica crípticamente en el sistema de AYUDA integrado. Escriba help foro for /?para obtener ayuda. La sección correspondiente está aproximadamente a la mitad del camino. Tenga en cuenta la palabra en negrita hacia el final de la cita.

Algunos ejemplos podrían ayudar:

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

analizaría cada línea en myfile.txt, ignorando las líneas que comienzan con punto y coma, pasando el segundo y tercer token de cada línea al cuerpo, con tokens delimitados por comas y/o espacios. Observe que las declaraciones del cuerpo hacen referencia a %i para obtener el segundo token, %j para obtener el tercer token y %k para obtener todos los tokens restantes después del tercero. Para nombres de archivos que contienen espacios, debe citar los nombres de archivos con comillas dobles. Para utilizar comillas dobles de esta manera, también debe utilizar la opción usebackq; de lo contrario, las comillas dobles se interpretarán como la definición de una cadena literal para analizar.

%i se declara explícitamente en la declaración for y %j y %k se declaran implícitamente a través de la opción tokens=. Puede especificar hasta 26 tokens a través de la línea tokens=, siempre que no provoque un intento de declarar una variable mayor que la letra 'z' o 'Z'. Recuerde, las variables FOR son de una sola letra, distinguen entre mayúsculas y minúsculas,global, y no puedes tener más de 52 en total activos al mismo tiempo.

Esta es la mayor documentación oficial que he visto sobre el comportamiento. Muy críptico y no muy útil. De hecho, gran parte de la información contenida en ese último párrafo es simplementeequivocado! Verhttps://stackoverflow.com/a/8520993/1012053para conocer la verdad sobre el número máximo de variables FOR disponibles y cuáles son los caracteres válidos para las variables FOR.

Respuesta3

No soy en absoluto un experto y, por lo tanto, no puedo señalar qué está mal en su detallado programa. Pero ejecuté este lote y encontré el resultado deseado:

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

información relacionada