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

Hoe interpreteert de Windows opdracht RENAME jokertekens?

Hoe interpreteert het Windows commando RENAME (REN) jokertekens?

De ingebouwde HELP-faciliteit biedt geen hulp - die gaat helemaal niet in op jokertekens.

De Microsoft technet XP online help is niet veel beter. Hier is alles wat het te zeggen heeft over jokertekens:

“U kunt jokertekens (* en ?) gebruiken in beide bestandsnaamparameters. Als u jokertekens gebruikt in bestandsnaam2, zullen de tekens die door de jokertekens worden vertegenwoordigd, identiek zijn aan de overeenkomstige tekens in bestandsnaam1.”

Niet veel hulp - er zijn vele manieren waarop die verklaring kan worden geïnterpreteerd.

Ik ben er een paar keer in geslaagd om met succes jokertekens in de parameter filename2 te gebruiken, maar het is altijd vallen en opstaan geweest. Ik heb niet kunnen voorspellen wat werkt en wat niet. Ik heb vaak mijn toevlucht moeten nemen tot het schrijven van een klein batch script met een FOR lus die elke naam parseert zodat ik elke nieuwe naam kan bouwen als dat nodig is. Niet erg handig.

Als ik de regels kende voor hoe wildcards worden verwerkt, dan zou ik het RENAME commando effectiever kunnen gebruiken zonder zo vaak mijn toevlucht te hoeven nemen tot batch. Natuurlijk zou het ook de ontwikkeling van batch ten goede komen als ik de regels zou kennen.

_(Ja - dit is een geval waarin ik een gekoppelde vraag en antwoord plaats. Ik was het zat de regels niet te kennen en besloot zelf te experimenteren. Ik denk dat veel anderen geïnteresseerd zullen zijn in wat ik heb ontdekt.

Antwoorden (4)

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

Deze regels zijn ontdekt na uitvoerige tests op een Vista-machine. Er zijn geen tests gedaan met unicode in bestandsnamen._

RENAME vereist 2 parameters - een sourceMask, gevolgd door een targetMask. Zowel de sourceMask als de targetMask kunnen * en/of ? jokertekens bevatten. Het gedrag van de jokertekens verschilt enigszins tussen bron- en doelmaskers.

Note - REN kan worden gebruikt om een map te hernoemen, maar jokertekens zijn niet toegestaan in zowel het bronMask als het doelMask bij het hernoemen van een map. Als de sourceMask overeenkomt met ten minste één bestand, dan wordt het bestand hernoemd en worden mappen genegeerd. Als de sourceMask alleen overeenkomt met mappen en niet met bestanden, dan wordt een syntaxisfout gegenereerd als jokertekens voorkomen in source of target. Als de sourceMask nergens mee overeenkomt, resulteert dat in een “bestand niet gevonden”-fout.

Ook bij het hernoemen van bestanden zijn jokertekens alleen toegestaan in het bestandsnaamgedeelte van de sourceMask. Jokertekens zijn niet toegestaan in het pad dat naar de bestandsnaam leidt.

sourceMask

De sourceMask werkt als een filter om te bepalen welke bestanden hernoemd worden. De jokertekens werken hier hetzelfde als bij elk ander commando dat bestandsnamen filtert.

  • ? - Komt overeen met elk 0 of 1 teken exclusief . Dit jokerteken is hebzuchtig - het gebruikt altijd het volgende teken als dat geen . is. Het zal echter zonder falen met niets overeenkomen als het aan het einde van de naam staat of als het volgende teken een .
  • * - Komt overeen met elk 0 of meer tekens inclusief . (met één uitzondering hieronder). Deze wildcard is niet gretig. Het komt overeen met zo weinig of zo veel als nodig is om volgende tekens overeen te laten komen.

Alle niet-wildcard tekens moeten met zichzelf overeenkomen, met een paar speciale uitzonderingen.

  • . - Komt overeen met zichzelf of kan overeenkomen met het einde van de naam (niets) als er geen tekens meer over zijn. (Opmerking - een geldige Windows-naam kan niet eindigen op .)

  • {space} - Komt overeen met zichzelf of kan overeenkomen met het einde van de naam (niets) als er geen tekens meer over zijn. (Opmerking - een geldige Windows-naam kan niet eindigen op {space})

  • *. aan het eind - Komt overeen met 0 of meer tekens uitgezonderd . De afsluitende . kan in feite elke combinatie van . en {space} zijn, zolang het allerlaatste teken in het masker maar . is. Dit is de enige uitzondering waarbij * niet simpelweg overeenkomt met een willekeurige reeks tekens.

De bovenstaande regels zijn niet zo ingewikkeld. Maar er is nog een zeer belangrijke regel die de situatie verwarrend maakt: De sourceMask wordt vergeleken met zowel de lange naam als de korte 8.3 naam (als die bestaat). Deze laatste regel kan de interpretatie van de resultaten erg lastig maken, omdat het niet altijd duidelijk is wanneer het masker wordt vergeleken via de korte naam.

Het is mogelijk om RegEdit te gebruiken om het genereren van korte 8.3 namen op NTFS volumes uit te schakelen, waardoor de interpretatie van de resultaten van het bestandsmasker veel eenvoudiger wordt. Alle korte namen die werden gegenereerd voordat de korte namen werden uitgeschakeld, blijven bestaan.

targetMask

Note - Ik heb geen strenge tests gedaan, maar het lijkt erop dat dezelfde regels ook werken voor de doelnaam van het COPY-commando

Het targetMask geeft de nieuwe naam aan. Het wordt altijd toegepast op de volledige lange naam; Het targetMask wordt nooit toegepast op de korte 8.3 naam, zelfs niet als het sourceMask overeenkomt met de korte 8.3 naam.

De aan- of afwezigheid van jokertekens in de sourceMask heeft geen invloed op de manier waarop jokertekens in de targetMask worden verwerkt.

In de volgende discussie - staat c voor elk teken dat niet *, ? of . is

De targetMask wordt strikt van links naar rechts tegen de bronnaam afgezet, zonder back-tracking.

  • c - Gaat alleen vooruit in de bronnaam als het bronteken niet . is, en voegt altijd c toe aan de doelnaam. (Vervangt het teken dat in bron was door c, maar vervangt nooit .)

  • ? - Komt overeen met het volgende teken uit de lange bronnaam en voegt dit toe aan de doelnaam zolang het bronteken niet . is Als het volgende teken . is of als het aan het eind van de bronnaam is, wordt geen teken aan het resultaat toegevoegd en blijft de huidige positie binnen de bronnaam ongewijzigd.

  • * aan einde van targetMask - Voegt alle resterende tekens van bron naar doel toe. Indien al aan het eind van bron, dan doet het niets.

  • *c - Zoekt alle bron-tekens vanaf de huidige positie tot en met het laatste voorkomen van c (hoofdlettergevoelige “greedy match”) en voegt de overeenkomende set tekens toe aan de doelnaam. Als c niet wordt gevonden, worden alle resterende tekens van de bron toegevoegd, gevolgd door c Dit is de enige situatie waarvan ik weet dat Windows bestandspatroonherkenning hoofdlettergevoelig is.

  • *. - Zoekt alle bron-tekens vanaf de huidige positie tot en met het laatste voorkomen van . (greedy match) en voegt de gematchte set van tekens aan de doelnaam. Als . niet wordt gevonden, dan worden alle resterende karakters van bron toegevoegd, gevolgd door .

  • *? - Voegt alle resterende karakters van bron toe aan het doel. Indien reeds aan het einde van bron, doet niets.

  • . zonder * ervoor - Gaat de positie in bron verder door het eerste voorkomen van . zonder enige karakters te kopiëren, en voegt . toe aan de doelnaam. Als . niet in de bron wordt gevonden, gaat het verder naar het einde van de bron en voegt . toe aan de doelnaam.

Nadat het doelMask is uitgeput, worden alle . en {space} aan het eind van de resulterende doelnaam weggeknipt, omdat Windows-bestandsnamen niet kunnen eindigen met . of {space}

Enkele praktische voorbeelden

Vervang een teken op de 1e en 3e positie vóór een extensie (voegt een 2e of 3e karakter toe als het nog niet bestaat)

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

Wijzig de (uiteindelijke) extensie van elk bestand

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

Voeg een extensie toe aan elk bestand

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

Verwijder elke extra extensie na de initiële extensie. Merk op dat adequate ? moet worden gebruikt om de volledige bestaande naam en initiële extensie te behouden.

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)

Hetzelfde als hierboven, maar filter bestanden met een initiële naam en/of extensie langer dan 5 karakters uit zodat ze niet worden afgekapt. (Uiteraard kan een extra ? aan beide uiteinden van targetMask worden toegevoegd om namen en extensies tot 6 karakters lang te behouden)

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)

Verander karakters na laatste _ in naam en probeer extensie te behouden. (Werkt niet goed als er _ in de extensie staat)

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)

Elke naam kan worden opgedeeld in componenten die worden afgebakend door . Tekens mogen alleen worden toegevoegd aan of verwijderd uit het eind van elk component. Tekens kunnen niet worden verwijderd uit of toegevoegd aan het begin of midden van een component met behoud van de rest met jokertekens. Substituties zijn overal toegestaan.

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

Als korte namen zijn ingeschakeld, dan zal een sourceMask met ten minste 8 ? voor de naam en ten minste 3 ? voor de extensie overeenkomen met alle bestanden, omdat het altijd overeenkomt met de korte 8.3 naam.

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

Handige quirk/bug? voor het verwijderen van naamvoorvoegsels

Deze SuperUser post beschrijft hoe een set voorwaartse schuine strepen (/) gebruikt kan worden om voorloopkarakters uit een bestandsnaam te verwijderen. Eén schuine streep is nodig voor elk karakter dat verwijderd moet worden. Ik heb het gedrag bevestigd op een Windows 10-machine.

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

Deze techniek werkt alleen als zowel de bron- als doelmaskers tussen dubbele aanhalingstekens staan. Alle volgende formulieren zonder de vereiste aanhalingstekens falen met deze foutmelding: 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

De / kan niet worden gebruikt om tekens in het midden of aan het eind van een bestandsnaam te verwijderen. Het kan alleen de voorloop (prefix) tekens verwijderen. Merk ook op dat deze techniek niet werkt met mapnamen.

Technisch gezien werkt de / niet als een wildcard. Het doet eerder een eenvoudige tekenvervanging, maar na de vervanging herkent het REN commando dat / niet geldig is in een bestandsnaam, en verwijdert de leidende / schuine strepen uit de naam. REN geeft een syntaxisfout als het / in het midden van een doelnaam detecteert.

Mogelijke RENAME fout - een enkel commando kan hetzelfde bestand twee keer hernoemen!

Beginnen in een lege testmap:

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

Ik denk dat het sourceMask *1* eerst overeenkomt met de lange bestandsnaam, en het bestand wordt hernoemd naar het verwachte resultaat van 223456789.123.x. RENAME gaat dan verder met zoeken naar meer bestanden om te verwerken en vindt het nieuw genoemde bestand via de nieuwe korte naam van 223456~1.X. Het bestand wordt dan opnieuw hernoemd, met als eindresultaat 223456789.123.xx.

Als ik 8.3 naamgeneratie uitschakel, geeft de RENAME het verwachte resultaat.

Ik heb nog niet alle triggervoorwaarden uitgewerkt die moeten bestaan om dit vreemde gedrag te veroorzaken. Ik was bang dat het mogelijk zou zijn om een nooit eindigende recursieve RENAME te maken, maar ik heb er nooit een kunnen induceren.

Ik geloof dat al het volgende waar moet zijn om de bug te veroorzaken. Elk geval met een bug dat ik zag, had de volgende voorwaarden, maar niet alle gevallen die aan de volgende voorwaarden voldeden, hadden een bug.

  • Korte 8.3 namen moeten ingeschakeld zijn
  • Het bronMasker moet overeenkomen met de oorspronkelijke lange naam.
  • De initiële hernoeming moet een korte naam genereren die ook overeenkomt met de sourceMask
  • De initiële hernoemde korte naam moet later sorteren dan de oorspronkelijke korte naam (als die al bestond?)
4
4
4
2014-12-16 10:13:11 +0000

Vergelijkbaar met exebook, hier is een C# implementatie om de doel bestandsnaam te krijgen van een bronbestand.

Ik vond 1 kleine fout in dbenham’s voorbeelden:

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

Hier is de code:

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

En hier is een NUnit test methode om de voorbeelden te testen:

[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

Misschien kan iemand dit nuttig vinden. Deze JavaScript-code is gebaseerd op het antwoord van dbenham hierboven.

Ik heb sourceMask niet veel getest, maar targetMask komt overeen met alle voorbeelden die dbenham heeft gegeven.

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

Ik ben er in geslaagd om deze code in BASIC te schrijven om wildcard bestandsnamen te maskeren:

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