2012-09-16 13:59:11 +0000 2012-09-16 13:59:11 +0000
81
81

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

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

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

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

“Вы можете использовать подстановочные знаки (* и ?) в любом параметре имени файла. Если вы используете подстановочные знаки в имени файла2, то символы, представляемые подстановочными знаками, будут идентичны соответствующим символам в имени файла1.”

Не очень помогает - есть много способов интерпретации этого выражения.

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

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

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

Ответы (4)

120
120
120
2012-09-16 14:00:21 +0000

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

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

Note - REN может использоваться для переименования папки, но при переименовании папки подстановочные знаки not разрешены ни в исходнойМаске, ни в целевойМаске. Если Маска-источник совпадает, по крайней мере, с одним файлом, то файл(и) будет переименован, а папки проигнорированы. Если маска-источник совпадает только с папками, а не с файлами, то при появлении подстановочных знаков в исходном или целевом коде будет выдана синтаксическая ошибка. Если маска-источник ничему не соответствует, то выдается ошибка “файл не найден”.

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

маска-источник

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

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

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

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

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

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

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

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

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

targetMask

Note - я не проводил строгого тестирования, но, похоже, эти же правила работают и для целевого имени COPY-запятой

The targetMask задает новое имя. Она всегда применяется к полному длинному имени; targetMask никогда не применяется к короткому имени 8.3, даже если исходнаяМаска совпала с коротким именем 8.3.

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

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

ЦелеваяМаска обрабатывается против имени источника строго слева направо без обратной связи.

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

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

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

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

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

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

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

После того, как целевая маска исчерпана, любой трейлинг . и {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

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

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)

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

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)

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

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

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

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

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

Эта техника работает только в том случае, если и исходная и целевая маски заключены в двойные кавычки. Все следующие формы без необходимых кавычек не справляются с этой ошибкой: The syntax of the command is incorrect

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

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

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

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

Запуск в пустой папке теста:

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

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

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

Я не полностью проработал все условия триггера, которые должны существовать, чтобы вызвать такое странное поведение. Меня беспокоило, что можно создать бесконечное рекурсивное РЕНАМЕ, но я никогда не мог его вызвать. 0x2 и 0x2 и я считаю, что все нижеследующее должно быть правдой, чтобы вызвать ошибку. Каждый прослушиваемый случай, который я видел, имел следующие условия, но не все случаи, которые удовлетворяли следующим условиям, были прослушиваны.

  • Должны быть включены короткие имена 8.3
  • Маска источника должна совпадать с оригинальным длинным именем.
  • Первоначальное переименование должно генерировать короткое имя, которое также совпадает с маской-источником
  • Первоначальное переименованное короткое имя должно сортироваться позже, чем оригинальное короткое имя (если оно существовало?).
4
4
4
2014-12-16 10:13:11 +0000

Как и в экс-книге, здесь есть реализация на 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
1
1
2014-04-09 17:07:37 +0000

Может, кто-нибудь найдет это полезным. Этот 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)
}
1
1
1
2016-10-13 01:27:15 +0000

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

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

Похожие вопросы

3
19
10
28
7