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

78

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

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

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

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

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

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

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

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

dbenham
источник
Здесь есть множество хороших примеров того, как переименовать с использованием подстановочных знаков: lagmonster.org/docs/DOS7/z-ren1.html
Мэтью Лок
5
@MatthewLock - интересная ссылка, но эти правила и примеры предназначены для MSDOS 7, а не для Windows. Есть существенные различия. Например, MSDOS не позволяет добавлять дополнительные символы после *Windows. Это имеет огромные последствия. Хотел бы я знать об этом сайте; это могло бы облегчить мое расследование. Правила MSDOS7 значительно отличаются от старых правил DOS перед длинными именами файлов, и они являются шагом в направлении того, как Windows справляется с этим. Я обнаружил правила DOS с длинными именами файлов, и они были бесполезны для моего расследования.
ДБЕНХАМ
Я не знал этого;)
Мэтью Лок

Ответы:

117

Эти правила были обнаружены после тщательного тестирования на компьютере с 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

Если короткие имена включены, то sourceMask с по крайней мере 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
  • Исходное переименованное короткое имя должно быть отсортировано позже исходного короткого имени (если оно существовало?)
dbenham
источник
6
Какой исчерпывающий ответ .. +1.
meder omuraliev
Потрясающе сложный!
Андрей М
13
Исходя из этого, Microsoft должна просто добавить «Для использования, см. Superuser.com/a/475875 » в REN /?.
efotinis
4
@CAD - Этот ответ является 100% оригинальным контентом, который Саймон включил на свой сайт по моей просьбе. Посмотрите на нижнюю часть этой страницы SS64, и вы увидите, что Саймон отдает мне должное за эту работу.
Дбенхем
2
@ JacksOnF1re - Новая информация / техника добавлена ​​в мой ответ. Вы можете фактически удалить свой Copy of префикс, используя непонятную технику прямого слеша:ren "Copy of *.txt" "////////*"
dbenham
4

Как и в 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));

    }
amrunning
источник
Спасибо за голову об ошибке в моем примере. Я отредактировал свой ответ, чтобы исправить это.
ДБЕНХАМ
1

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

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
eoredson
источник
4
Можете ли вы уточнить, как это отвечает на то, что было задано в вопросе?
fixer1234
Он дублирует функцию, которую REN использует для сопоставления с подстановочными знаками, например для обработки REN * .TMP * .DOC, в зависимости от того, как вызывается функция перед переименованием имен файлов.
Eoredson
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)
}
exebook
источник