Saltar al contenido

¿Cómo interpreta el comando RENAME de Windows los comodines?

Solución:

Estas reglas se descubrieron después de realizar pruebas exhaustivas en una máquina Vista. No se realizaron pruebas con Unicode en los nombres de archivo.

RENAME requiere 2 parámetros: una sourceMask, seguida de una targetMask. Tanto sourceMask como targetMask pueden contener * y / o ? comodines. El comportamiento de los comodines cambia ligeramente entre las máscaras de origen y destino.

NotaREN se puede utilizar para cambiar el nombre de una carpeta, pero los comodines son no permitido en sourceMask o targetMask al cambiar el nombre de una carpeta. Si sourceMask coincide con al menos un archivo, se cambiará el nombre de los archivos y se ignorarán las carpetas. Si sourceMask solo coincide con carpetas y no con archivos, se genera un error de sintaxis si aparecen comodines en el origen o el destino. Si sourceMask no coincide con nada, se produce un error de “archivo no encontrado”.

Además, al cambiar el nombre de los archivos, los comodines solo se permiten en la parte del nombre del archivo de sourceMask. No se permiten comodines en la ruta que conduce al nombre del archivo.

sourceMask

SourceMask funciona como un filtro para determinar qué archivos se renombran. Los comodines funcionan aquí igual que con cualquier otro comando que filtre nombres de archivos.

  • ? – Coincide con cualquier 0 o 1 carácter excepto . Este comodín es codicioso: siempre consume el siguiente carácter si no es un . Sin embargo, no coincidirá con nada sin fallas si al final del nombre o si el siguiente carácter es un .

  • * – Coincide con 0 o más caracteres incluso . (con una excepción a continuación). Este comodín no es codicioso. Coincidirá tanto como sea necesario para permitir que los personajes posteriores coincidan.

Todos los caracteres que no sean comodines deben coincidir con ellos mismos, con algunas excepciones de casos especiales.

  • . – Coincide con sí mismo o puede coincidir con el final del nombre (nada) si no quedan más caracteres. (Nota: un nombre de Windows válido no puede terminar con .)

  • {space} – Coincide con sí mismo o puede coincidir con el final del nombre (nada) si no quedan más caracteres. (Nota: un nombre de Windows válido no puede terminar con {space})

  • *. al final: coincide con 0 o más caracteres excepto . La terminación . en realidad puede ser cualquier combinación de . y {space} siempre que el último personaje de la máscara sea . Esta es la única excepción en la que * no coincide simplemente con ningún conjunto de caracteres.

Las reglas anteriores no son tan complejas. Pero hay una regla más muy importante que hace que la situación sea confusa: sourceMask se compara tanto con el nombre largo como con el nombre corto 8.3 (si existe). Esta última regla puede hacer que la interpretación de los resultados sea muy complicada, porque no siempre es obvio cuando la máscara coincide con el nombre corto.

Es posible utilizar RegEdit para deshabilitar la generación de nombres 8.3 cortos en volúmenes NTFS, momento en el que la interpretación de los resultados de la máscara de archivo es mucho más sencilla. Los nombres cortos que se generaron antes de deshabilitar los nombres cortos permanecerán.

targetMask

Nota: no he realizado ninguna prueba rigurosa, pero parece que estas mismas reglas también funcionan para el nombre de destino del comando COPY

TargetMask especifica el nuevo nombre. Siempre se aplica al nombre largo completo; TargetMask nunca se aplica al nombre 8.3 corto, incluso si sourceMask coincide con el nombre 8.3 corto.

La presencia o ausencia de comodines en sourceMask no tiene ningún impacto en cómo se procesan los comodines en targetMask.

En la siguiente discusión: c representa cualquier personaje que no sea *, ?, o .

TargetMask se procesa contra el nombre de la fuente estrictamente de izquierda a derecha sin retroceso.

  • c – Avanza la posición dentro del nombre de la fuente solo si el carácter de la fuente no es ., y siempre agrega c al nombre del objetivo. (Reemplaza el personaje que estaba en la fuente con c, pero nunca reemplaza .)

  • ? – Coincide con el siguiente carácter del nombre largo de origen y lo agrega al nombre de destino siempre que el carácter de origen no sea . Si el siguiente personaje es . o si al final del nombre de la fuente, no se agrega ningún carácter al resultado y la posición actual dentro del nombre de la fuente no se modifica.

  • * al final de targetMask: agrega todos los caracteres restantes del origen al destino. Si ya está al final de la fuente, entonces no hace nada.

  • *c – Coincide con todos los caracteres de origen desde la posición actual hasta la última aparición de c (coincidencia codiciosa que distingue entre mayúsculas y minúsculas) y agrega el conjunto de caracteres coincidentes al nombre de destino. Si c no se encuentra, se añaden todos los caracteres restantes de la fuente, seguidos de c Esta es la única situación que conozco en la que la coincidencia de patrones de archivos de Windows distingue entre mayúsculas y minúsculas.

  • *. – Coincide con todos los caracteres de origen desde la posición actual a través del último ocurrencia de . (coincidencia codiciosa) y agrega el conjunto de caracteres coincidentes al nombre del objetivo. Si . no se encuentra, se añaden todos los caracteres restantes de la fuente, seguidos de .

  • *? – Agrega todos los caracteres restantes del origen al destino. Si ya está al final de la fuente, no hace nada.

  • . sin * delante: avanza la posición en la fuente a través del primero ocurrencia de . sin copiar ningún carácter, y agrega . al nombre del objetivo. Si . no se encuentra en la fuente, luego avanza hasta el final de la fuente y agrega . al nombre del objetivo.

Una vez agotada la máscara de destino, cualquier . y {space} se recortan del final del nombre de destino resultante porque los nombres de archivo de Windows no pueden terminar con . o {space}

Algunos ejemplos practicos

Sustituir un carácter en la 1ª y 3ª posición antes de cualquier extensión (agrega un 2º o 3º carácter si aún no existe)

ren  *  A?Z*
  1        -> AZ
  12       -> A2Z
  1.txt    -> AZ.txt
  12.txt   -> A2Z.txt
  123      -> A2Z
  123.txt  -> A2Z.txt
  1234     -> A2Z4
  1234.txt -> A2Z4.txt

Cambiar la extensión (final) de cada archivo

ren  *  *.txt
  a     -> a.txt
  b.dat -> b.txt
  c.x.y -> c.x.txt

Agregue una extensión a cada archivo

ren  *  *?.bak
  a     -> a.bak
  b.dat -> b.dat.bak
  c.x.y -> c.x.y.bak

Elimine cualquier extensión adicional después de la extensión inicial. Tenga en cuenta que adecuado ? debe usarse para preservar el nombre completo existente y la extensión inicial.

ren  *  ?????.?????
  a     -> a
  a.b   -> a.b
  a.b.c -> a.b
  part1.part2.part3    -> part1.part2
  123456.123456.123456 -> 12345.12345   (note truncated name and extension because not enough `?` were used)

Igual que el anterior, pero filtra los archivos con nombre inicial y / o extensión de más de 5 caracteres para que no se trunquen. (Obviamente podría agregar un adicional ? en cualquier extremo de targetMask para conservar nombres y extensiones de hasta 6 caracteres)

ren  ?????.?????.*  ?????.?????
  a      ->  a
  a.b    ->  a.b
  a.b.c  ->  a.b
  part1.part2.part3  ->  part1.part2
  123456.123456.123456  (Not renamed because doesn't match sourceMask)

Cambiar personajes después del último _ en nombre e intento de preservar la extensión. (No funciona correctamente si _ aparece en extensión)

ren  *_*  *_NEW.*
  abcd_12345.txt  ->  abcd_NEW.txt
  abc_newt_1.dat  ->  abc_newt_NEW.dat
  abcdef.jpg          (Not renamed because doesn't match sourceMask)
  abcd_123.a_b    ->  abcd_123.a_NEW  (not desired, but no simple RENAME form will work in this case)

Cualquier nombre se puede dividir en componentes que están delimitados por . Los caracteres solo se pueden agregar o eliminar al final de cada componente. Los caracteres no se pueden eliminar ni agregar al principio o en medio de un componente mientras se conserva el resto con comodines. Se permiten sustituciones en cualquier lugar.

ren  ??????.??????.??????  ?x.????999.*rForTheCourse
  part1.part2            ->  px.part999.rForTheCourse
  part1.part2.part3      ->  px.part999.parForTheCourse
  part1.part2.part3.part4   (Not renamed because doesn't match sourceMask)
  a.b.c                  ->  ax.b999.crForTheCourse
  a.b.CarPart3BEER       ->  ax.b999.CarParForTheCourse

Si los nombres cortos están habilitados, entonces un sourceMask con al menos 8 ? para el nombre y al menos 3 ? para la extensión coincidirá con todos los archivos porque siempre coincidirá con el nombre corto 8.3.

ren ????????.???  ?x.????999.*rForTheCourse
  part1.part2.part3.part4  ->  px.part999.part3.parForTheCourse

¿Qué peculiaridad / error útil? para borrar prefijos de nombres

Esta publicación de SuperUser describe cómo un conjunto de barras diagonales (/) se puede utilizar para eliminar caracteres iniciales (excepto .) de un nombre de archivo. Se requiere una barra para eliminar cada carácter. Confirmé el comportamiento en una máquina con Windows 10.

ren "abc-*.txt" "////*.txt"
  abc-123.txt        --> 123.txt
  abc-HelloWorld.txt --> HelloWorld.txt

Desafortunadamente liderando / no se puede quitar . en un nombre. Por tanto, la técnica no se puede utilizar para eliminar un prefijo que contiene .. Por ejemplo:

ren "abc.xyz.*.txt" "////////*.txt"
  abc.xyz.123.txt        --> .xyz.123.txt
  abc.xyz.HelloWorld.txt --> .xyz.HelloWorld.txt

Esta técnica solo funciona si tanto la máscara de origen como la de destino están entre comillas dobles. Todos los formularios siguientes sin las cotizaciones requeridas fallan con este error: The syntax of the command is incorrect

REM - All of these forms fail with a syntax error.
ren abc-*.txt "////*.txt"
ren "abc-*.txt" ////*.txt
ren abc-*.txt ////*.txt

los / no se puede utilizar para eliminar ningún carácter en el medio o al final de un nombre de archivo. Solo puede eliminar los caracteres iniciales (prefijos). También tenga en cuenta que esta técnica no funciona con nombres de carpetas.

Técnicamente el / no funciona como comodín. Más bien está haciendo una simple sustitución de caracteres siguiendo el c regla de la máscara de destino. Pero luego, después de la sustitución, el comando REN reconoce que / no es válido en un nombre de archivo y elimina el / barras del nombre. REN da un error de sintaxis si detecta / en medio de un nombre de destino.

Posible error de RENAME: ¡un solo comando puede cambiar el nombre del mismo archivo dos veces!

Comenzando en una carpeta de prueba vacía:

C:test>copy nul 123456789.123
        1 file(s) copied.

C:test>dir /x
 Volume in drive C is OS
 Volume Serial Number is EE2C-5A11

 Directory of C:test

09/15/2012  07:42 PM    <DIR>                       .
09/15/2012  07:42 PM    <DIR>                       ..
09/15/2012  07:42 PM                 0 123456~1.123 123456789.123
               1 File(s)              0 bytes
               2 Dir(s)  327,237,562,368 bytes free

C:test>ren *1* 2*3.?x

C:test>dir /x
 Volume in drive C is OS
 Volume Serial Number is EE2C-5A11

 Directory of C:test

09/15/2012  07:42 PM    <DIR>                       .
09/15/2012  07:42 PM    <DIR>                       ..
09/15/2012  07:42 PM                 0 223456~1.XX  223456789.123.xx
               1 File(s)              0 bytes
               2 Dir(s)  327,237,562,368 bytes free

REM Expected result = 223456789.123.x

Creo que la fuenteMáscara *1* primero coincide con el nombre de archivo largo, y el archivo se renombra al resultado esperado de 223456789.123.x. RENAME luego continúa buscando más archivos para procesar y encuentra el archivo recién nombrado a través del nuevo nombre corto de 223456~1.X. Luego, el archivo se renombra nuevamente dando el resultado final de 223456789.123.xx.

Si desactivo la generación de nombres 8.3, RENAME da el resultado esperado.

No he resuelto completamente todas las condiciones desencadenantes que deben existir para inducir este comportamiento extraño. Me preocupaba que pudiera ser posible crear un RENAME recursivo interminable, pero nunca pude inducir uno.

Creo que todo lo siguiente debe ser cierto para inducir el error. Todos los casos con errores que vi tenían las siguientes condiciones, pero no todos los casos que cumplían con las siguientes condiciones tenían errores.

  • Los nombres cortos 8.3 deben estar habilitados
  • SourceMask debe coincidir con el nombre largo original.
  • El cambio de nombre inicial debe generar un nombre corto que también coincida con sourceMask
  • El nombre corto inicial renombrado debe ordenarse más tarde que el nombre corto original (¿si existiera?)

Similar al exebook, aquí hay una implementación de C # para obtener el nombre de archivo de destino de un archivo de origen.

Encontré 1 pequeño error en los ejemplos de dbenham:

 ren  *_*  *_NEW.*
   abc_newt_1.dat  ->  abc_newt_NEW.txt (should be: abd_newt_NEW.dat)

Aquí está el código:

    /// <summary>
    /// Returns a filename based on the sourcefile and the targetMask, as used in the second argument in rename/copy operations.
    /// targetMask may contain wildcards (* and ?).
    /// 
    /// This follows the rules of: http://superuser.com/questions/475874/how-does-the-windows-rename-command-interpret-wildcards
    /// </summary>
    /// <param name="sourcefile">filename to change to target without wildcards</param>
    /// <param name="targetMask">mask with wildcards</param>
    /// <returns>a valid target filename given sourcefile and targetMask</returns>
    public static string GetTargetFileName(string sourcefile, string targetMask)
    {
        if (string.IsNullOrEmpty(sourcefile))
            throw new ArgumentNullException("sourcefile");

        if (string.IsNullOrEmpty(targetMask))
            throw new ArgumentNullException("targetMask");

        if (sourcefile.Contains('*') || sourcefile.Contains('?'))
            throw new ArgumentException("sourcefile cannot contain wildcards");

        // no wildcards: return complete mask as file
        if (!targetMask.Contains('*') && !targetMask.Contains('?'))
            return targetMask;

        var maskReader = new StringReader(targetMask);
        var sourceReader = new StringReader(sourcefile);
        var targetBuilder = new StringBuilder();


        while (maskReader.Peek() != -1)
        {

            int current = maskReader.Read();
            int sourcePeek = sourceReader.Peek();
            switch (current)
            {
                case '*':
                    int next = maskReader.Read();
                    switch (next)
                    {
                        case -1:
                        case '?':
                            // Append all remaining characters from sourcefile
                            targetBuilder.Append(sourceReader.ReadToEnd());
                            break;
                        default:
                            // Read source until the last occurrance of 'next'.
                            // We cannot seek in the StringReader, so we will create a new StringReader if needed
                            string sourceTail = sourceReader.ReadToEnd();
                            int lastIndexOf = sourceTail.LastIndexOf((char) next);
                            // If not found, append everything and the 'next' char
                            if (lastIndexOf == -1)
                            {
                                targetBuilder.Append(sourceTail);
                                targetBuilder.Append((char) next);

                            }
                            else
                            {
                                string toAppend = sourceTail.Substring(0, lastIndexOf + 1);
                                string rest = sourceTail.Substring(lastIndexOf + 1);
                                sourceReader.Dispose();
                                // go on with the rest...
                                sourceReader = new StringReader(rest);
                                targetBuilder.Append(toAppend);
                            }
                            break;
                    }

                    break;
                case '?':
                    if (sourcePeek != -1 && sourcePeek != '.')
                    {
                        targetBuilder.Append((char)sourceReader.Read());
                    }
                    break;
                case '.':
                    // eat all characters until the dot is found
                    while (sourcePeek != -1 && sourcePeek != '.')
                    {
                        sourceReader.Read();
                        sourcePeek = sourceReader.Peek();
                    }

                    targetBuilder.Append('.');
                    // need to eat the . when we peeked it
                    if (sourcePeek == '.')
                        sourceReader.Read();

                    break;
                default:
                    if (sourcePeek != '.') sourceReader.Read(); // also consume the source's char if not .
                    targetBuilder.Append((char)current);
                    break;
            }

        }

        sourceReader.Dispose();
        maskReader.Dispose();
        return targetBuilder.ToString().TrimEnd('.', ' ');
    }

Y aquí hay un método de prueba NUnit para probar los ejemplos:

    [Test]
    public void TestGetTargetFileName()
    {
        string targetMask = "?????.?????";
        Assert.AreEqual("a", FileUtil.GetTargetFileName("a", targetMask));
        Assert.AreEqual("a.b", FileUtil.GetTargetFileName("a.b", targetMask));
        Assert.AreEqual("a.b", FileUtil.GetTargetFileName("a.b.c", targetMask));
        Assert.AreEqual("part1.part2", FileUtil.GetTargetFileName("part1.part2.part3", targetMask));
        Assert.AreEqual("12345.12345", FileUtil.GetTargetFileName("123456.123456.123456", targetMask));

        targetMask = "A?Z*";
        Assert.AreEqual("AZ", FileUtil.GetTargetFileName("1", targetMask));
        Assert.AreEqual("A2Z", FileUtil.GetTargetFileName("12", targetMask));
        Assert.AreEqual("AZ.txt", FileUtil.GetTargetFileName("1.txt", targetMask));
        Assert.AreEqual("A2Z.txt", FileUtil.GetTargetFileName("12.txt", targetMask));
        Assert.AreEqual("A2Z", FileUtil.GetTargetFileName("123", targetMask));
        Assert.AreEqual("A2Z.txt", FileUtil.GetTargetFileName("123.txt", targetMask));
        Assert.AreEqual("A2Z4", FileUtil.GetTargetFileName("1234", targetMask));
        Assert.AreEqual("A2Z4.txt", FileUtil.GetTargetFileName("1234.txt", targetMask));

        targetMask = "*.txt";
        Assert.AreEqual("a.txt", FileUtil.GetTargetFileName("a", targetMask));
        Assert.AreEqual("b.txt", FileUtil.GetTargetFileName("b.dat", targetMask));
        Assert.AreEqual("c.x.txt", FileUtil.GetTargetFileName("c.x.y", targetMask));

        targetMask = "*?.bak";
        Assert.AreEqual("a.bak", FileUtil.GetTargetFileName("a", targetMask));
        Assert.AreEqual("b.dat.bak", FileUtil.GetTargetFileName("b.dat", targetMask));
        Assert.AreEqual("c.x.y.bak", FileUtil.GetTargetFileName("c.x.y", targetMask));

        targetMask = "*_NEW.*";
        Assert.AreEqual("abcd_NEW.txt", FileUtil.GetTargetFileName("abcd_12345.txt", targetMask));
        Assert.AreEqual("abc_newt_NEW.dat", FileUtil.GetTargetFileName("abc_newt_1.dat", targetMask));
        Assert.AreEqual("abcd_123.a_NEW", FileUtil.GetTargetFileName("abcd_123.a_b", targetMask));

        targetMask = "?x.????999.*rForTheCourse";

        Assert.AreEqual("px.part999.rForTheCourse", FileUtil.GetTargetFileName("part1.part2", targetMask));
        Assert.AreEqual("px.part999.parForTheCourse", FileUtil.GetTargetFileName("part1.part2.part3", targetMask));
        Assert.AreEqual("ax.b999.crForTheCourse", FileUtil.GetTargetFileName("a.b.c", targetMask));
        Assert.AreEqual("ax.b999.CarParForTheCourse", FileUtil.GetTargetFileName("a.b.CarPart3BEER", targetMask));

    }
¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *