71

Как команда Windows RENAME (REN) интерпретирует подстановочные знаки?

Встроенное средство HELP не помогает - оно вообще не обращается к групповым символам.

Онлайн-справка Microsoft technet XP не намного лучше. Вот все, что нужно сказать о подстановочных знаках:

«Вы можете использовать подстановочные знаки (* и ?) в любом из параметров имени файла. Если вы используете подстановочные знаки в filename2, символы, представленные подстановочными знаками, будут идентичны соответствующим символам в filename1. "

Немного помощи - есть много способов, которыми это утверждение можно интерпретировать.

Мне иногда удавалось успешно использовать подстановочные знаки в параметре filename2 , но это всегда было методом проб и ошибок. Я не смог предвидеть, что работает, а что нет. Часто мне приходилось прибегать к написанию небольшого пакетного скрипта с циклом FOR, который анализирует каждое имя, чтобы я мог создавать каждое новое имя по мере необходимости. Не очень удобно.

Если бы я знал правила обработки подстановочных знаков, то решил, что мог бы использовать команду RENAME более эффективно, не прибегая к пакетной обработке так часто. Конечно, знание правил также будет полезно для пакетной разработки.

(Да - это тот случай, когда я выкладываю парный вопрос и ответ. Я устал от незнания правил и решил поэкспериментировать самостоятельно. Я думаю, что многие другие могут быть заинтересованы в том, что я обнаружил)

4 ответа4

108

Эти правила были обнаружены после тщательного тестирования на компьютере с Vista. Никаких тестов с юникодом в именах файлов не проводилось.

RENAME требует 2 параметра - sourceMask, за которым следует targetMask. И sourceMask, и targetMask могут содержать * и / или ? подстановочные знаки. Поведение подстановочных знаков немного меняется между исходной и целевой масками.

Примечание - REN может быть использован для переименования папки, но подстановочные символы не допускается ни в sourceMask или targetMask при переименовании папки.Если sourceMask соответствует хотя бы одному файлу, файлы будут переименованы, а папки будут игнорироваться. Если sourceMask соответствует только папкам, а не файлам, генерируется синтаксическая ошибка, если в источнике или цели появляются символы подстановки. Если sourceMask не совпадает ни с чем, возникает ошибка "файл не найден".

Кроме того, при переименовании файлов подстановочные знаки допускаются только в части имени файла в SourceMask. Подстановочные знаки не допускаются в пути, ведущем к имени файла.

sourceMask

SourceMask работает как фильтр, чтобы определить, какие файлы переименованы. Подстановочные знаки работают здесь так же, как и с любой другой командой, которая фильтрует имена файлов.

  • ? - Соответствует любому 0 или 1 символу, кроме . Этот подстановочный знак является жадным - он всегда потребляет следующий символ, если он не является . Однако он не будет ничего совпадать без сбоев, если в конце имени или если следующий символ - .

  • * - Соответствует любым 0 или более символам, включая . (с одним исключением ниже). Этот подстановочный знак не жадный. Это будет соответствовать так мало или столько, сколько необходимо для соответствия последующих символов.

Все не подстановочные символы должны совпадать, за исключением нескольких особых случаев.

  • . - Соответствует самому себе или может соответствовать концу имени (ничего), если больше не осталось символов. (Примечание: действительное имя Windows не может заканчиваться .)

  • {space} - соответствует самому себе или может совпадать с концом имени (ничего), если больше не осталось символов. (Примечание: допустимое имя Windows не может заканчиваться на {space})

  • *. в конце - соответствует любому 0 или более символов, кроме . . на самом деле может быть любая комбинация . и {space} пока последний символ в маске . Это единственное исключение, где * не просто соответствует ни одному набору символов.

Вышеуказанные правила не так сложны. Но есть еще одно очень важное правило, которое запутывает ситуацию: SourceMask сравнивается как с длинным, так и с коротким именем 8.3 (если оно существует). Последнее правило может усложнить интерпретацию результатов, потому что не всегда очевидно, когда маска соответствует короткому имени.

Можно использовать RegEdit, чтобы отключить генерацию коротких имен 8.3 на томах NTFS, после чего интерпретация результатов маски файла будет гораздо более простой. Любые короткие имена, которые были созданы до отключения коротких имен, останутся.

targetMask

Примечание. Я не проводил тщательного тестирования, но, похоже, эти же правила работают и для целевого имени команды COPY.

TargetMask указывает новое имя. Это всегда применяется к полному длинному имени; TargetMask никогда не применяется к короткому имени 8.3, даже если sourceMask соответствует короткому имени 8.3.

Наличие или отсутствие подстановочных знаков в sourceMask не влияет на то, как подстановочные знаки обрабатываются в targetMask.

В следующем обсуждении - c представляет любой символ, который не * ? или .

TargetMask обрабатывается по отношению к имени источника строго слева направо без обратного отслеживания.

  • c - продвигает позицию внутри имени источника до тех пор, пока следующий символ не будет . и добавляет c к целевому имени. (Заменяет символ, который был в источнике на c , но никогда не заменяет .)

  • ? - Соответствует следующему символу из длинного имени источника и добавляет его к целевому имени, пока следующий символ не является . Если следующий . или если в конце имени источника не добавляется символ к результату, а текущая позиция в имени источника не изменяется.

  • * в конце targetMask - добавляет все оставшиеся символы от источника к цели. Если уже в конце источника, то ничего не делает.

  • *c - Сопоставляет все исходные символы от текущей позиции до последнего вхождения c (жадное совпадение с учетом регистра) и добавляет соответствующий набор символов к целевому имени. Если c не найден, то все остальные символы из источника добавляются, а затем c Это единственная известная мне ситуация, когда сопоставление шаблонов файлов Windows чувствительно к регистру.

  • *. - Соответствует всем исходным символам от текущей позиции до последнего вхождения . (жадное совпадение) и добавляет соответствующий набор символов к целевому имени. Если . не найден, тогда все остальные символы из источника добавляются, после чего .

  • *? - Добавляет все оставшиеся символы от источника к цели. Если уже в конце источника, то ничего не делает.

  • . без * впереди - продвигает позицию в источнике через первое вхождение . без копирования каких-либо символов, и добавляет . на имя цели. Если . не найден в источнике, затем переходит к концу источника и добавляет . на имя цели.

После того, как targetMask был исчерпан, любой трейлинг . и {space} обрезаются в конце конечного имени цели, поскольку имена файлов Windows не могут заканчиваться на . или {space}

Некоторые практические примеры

Замените символ в 1-й и 3-й позициях перед любым расширением (добавляет 2-й или 3-й символ, если он еще не существует)

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

Изменить (окончательное) расширение каждого файла

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

Добавить расширение для каждого файла

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

Удалите все дополнительные расширения после исходного расширения. Обратите внимание, что адекватно ? должен использоваться для сохранения полного существующего имени и исходного расширения.

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)

То же, что и выше, но отфильтровывать файлы с начальным именем и / или расширением длиннее 5 символов, чтобы они не усекались. (Очевидно, можно добавить дополнительный ? на любом конце targetMask для сохранения имен и расширений длиной до 6 символов)

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)

Измените символы после последней _ в имени и попытайтесь сохранить расширение. (Не работает должным образом, если _ появляется в расширении)

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)

Любое имя может быть разбито на компоненты, которые разделены . Символы могут быть добавлены или удалены только в конце каждого компонента. Символы не могут быть удалены или добавлены в начало или в середину компонента, сохраняя остаток с подстановочными знаками. Замены разрешены где угодно.

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

Если короткие имена включены, то источникМаска с по крайней мере 8 ? за имя и хотя бы 3 ? поскольку расширение будет соответствовать всем файлам, потому что оно всегда будет соответствовать короткому имени 8.3.

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


Полезная причуда / ошибка? для удаления префиксов имен

Этот пост SuperUser описывает, как набор косых черт (/) может использоваться для удаления начальных символов из имени файла. Для удаления каждого символа требуется один слеш. Я подтвердил поведение на компьютере с Windows 10.

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

Этот метод работает, только если исходная и целевая маски заключены в двойные кавычки. Все следующие формы без необходимых кавычек завершаются с этой ошибкой: 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

/ Нельзя использовать для удаления каких-либо символов в середине или конце имени файла. Он может удалять только начальные (префиксные) символы.

Технически / не работает как подстановочный знак. Вместо этого он выполняет простую подстановку символов, но затем после подстановки команда REN распознает, что / не является допустимой в имени файла, и удаляет начальную / косую черту из имени. REN выдает синтаксическую ошибку, если обнаруживает / в середине целевого имени.


Возможная ошибка RENAME - одна команда может дважды переименовать один и тот же файл!

Начиная с пустой тестовой папки:

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

Я считаю, что sourceMask *1* сначала совпадает с длинным именем файла, и файл переименовывается с ожидаемым результатом 223456789.123.x . Затем RENAME продолжает поиск файлов для обработки и находит новый файл с новым коротким именем 223456~1.X Затем файл снова переименовывается, давая окончательный результат 223456789.123.xx .

Если я отключу генерацию имени 8.3, то RENAME даст ожидаемый результат.

Я не полностью проработал все условия триггера, которые должны существовать, чтобы вызвать это странное поведение. Я был обеспокоен тем, что возможно создать бесконечное рекурсивное RENAME, но я так и не смог вызвать его.

Я считаю, что все следующее должно быть правдой, чтобы вызвать ошибку. В каждом найденном мной случае были следующие условия, но не во всех случаях, которые удовлетворяли следующим условиям, были ошибки.

  • Короткие 8.3 имена должны быть включены
  • SourceMask должен соответствовать оригинальному длинному имени.
  • Первоначальное переименование должно генерировать короткое имя, которое также соответствует sourceMask
  • Исходное переименованное короткое имя должно быть отсортировано позже исходного короткого имени (если оно существовало?)
3

Как и в exebook, вот реализация C # для получения целевого имени файла из исходного файла.

Я нашел 1 маленькую ошибку в примерах dbenham:

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

Вот код:

    /// <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('.', ' ');
    }

А вот метод тестирования NUnit для тестирования примеров:

    [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));

    }
1

Может быть, кто-то может найти это полезным. Этот код JavaScript основан на ответе dbenham выше.

Я не очень тестировал sourceMask , но targetMask соответствует всем примерам, приведенным в dbenham.

function maskMatch(path, mask) {
    mask = mask.replace(/\./g, '\\.')
    mask = mask.replace(/\?/g, '.')
    mask = mask.replace(/\*/g, '.+?')
    var r = new RegExp('^'+mask+'$', '')
    return path.match(r)
}

function maskNewName(path, mask) {
    if (path == '') return
    var x = 0, R = ''
    for (var m = 0; m < mask.length; m++) {
        var ch = mask[m], q = path[x], z = mask[m + 1]
        if (ch != '.' && ch != '*' && ch != '?') {
            if (q && q != '.') x++
            R += ch
        } else if (ch == '?') {
            if (q && q != '.') R += q, x++
        } else if (ch == '*' && m == mask.length - 1) {
            while (x < path.length) R += path[x++]
        } else if (ch == '*') {
            if (z == '.') {
                for (var i = path.length - 1; i >= 0; i--) if (path[i] == '.') break
                if (i < 0) {
                    R += path.substr(x, path.length) + '.'
                    i = path.length
                } else R += path.substr(x, i - x + 1)
                x = i + 1, m++
            } else if (z == '?') {
                R += path.substr(x, path.length), m++, x = path.length
            } else {
                for (var i = path.length - 1; i >= 0; i--) if (path[i] == z) break
                if (i < 0) R += path.substr(x, path.length) + z, x = path.length, m++
                else R += path.substr(x, i - x), x = i + 1
            }
        } else if (ch == '.') {
            while (x < path.length) if (path[x++] == '.') break
            R += '.'
        }
    }
    while (R[R.length - 1] == '.') R = R.substr(0, R.length - 1)
}
0

Мне удалось написать этот код на бейсике, чтобы замаскировать подстановочные имена файлов:

REM inputs a filename and matches wildcards returning masked output filename.
FUNCTION maskNewName$ (path$, mask$)
IF path$ = "" THEN EXIT FUNCTION
IF INSTR(path$, "?") OR INSTR(path$, "*") THEN EXIT FUNCTION
x = 0
R$ = ""
FOR m = 0 TO LEN(mask$) - 1
    ch$ = MID$(mask$, m + 1, 1)
    q$ = MID$(path$, x + 1, 1)
    z$ = MID$(mask$, m + 2, 1)
    IF ch$ <> "." AND ch$ <> "*" AND ch$ <> "?" THEN
        IF LEN(q$) AND q$ <> "." THEN x = x + 1
        R$ = R$ + ch$
    ELSE
        IF ch$ = "?" THEN
            IF LEN(q$) AND q$ <> "." THEN R$ = R$ + q$: x = x + 1
        ELSE
            IF ch$ = "*" AND m = LEN(mask$) - 1 THEN
                WHILE x < LEN(path$)
                    R$ = R$ + MID$(path$, x + 1, 1)
                    x = x + 1
                WEND
            ELSE
                IF ch$ = "*" THEN
                    IF z$ = "." THEN
                        FOR i = LEN(path$) - 1 TO 0 STEP -1
                            IF MID$(path$, i + 1, 1) = "." THEN EXIT FOR
                        NEXT
                        IF i < 0 THEN
                            R$ = R$ + MID$(path$, x + 1) + "."
                            i = LEN(path$)
                        ELSE
                            R$ = R$ + MID$(path$, x + 1, i - x + 1)
                        END IF
                        x = i + 1
                        m = m + 1
                    ELSE
                        IF z$ = "?" THEN
                            R$ = R$ + MID$(path$, x + 1, LEN(path$))
                            m = m + 1
                            x = LEN(path$)
                        ELSE
                            FOR i = LEN(path$) - 1 TO 0 STEP -1
                                'IF MID$(path$, i + 1, 1) = z$ THEN EXIT FOR
                                IF UCASE$(MID$(path$, i + 1, 1)) = UCASE$(z$) THEN EXIT FOR
                            NEXT
                            IF i < 0 THEN
                                R$ = R$ + MID$(path$, x + 1, LEN(path$)) + z$
                                x = LEN(path$)
                                m = m + 1
                            ELSE
                                R$ = R$ + MID$(path$, x + 1, i - x)
                                x = i + 1
                            END IF
                        END IF
                    END IF
                ELSE
                    IF ch$ = "." THEN
                        DO WHILE x < LEN(path$)
                            IF MID$(path$, x + 1, 1) = "." THEN
                                x = x + 1
                                EXIT DO
                            END IF
                            x = x + 1
                        LOOP
                        R$ = R$ + "."
                    END IF
                END IF
            END IF
        END IF
    END IF
NEXT
DO WHILE RIGHT$(R$, 1) = "."
    R$ = LEFT$(R$, LEN(R$) - 1)
LOOP
R$ = RTRIM$(R$)
maskNewName$ = R$
END FUNCTION

Всё ещё ищете ответ? Посмотрите другие вопросы с метками .