Windows RENAME命令如何解释通配符?


77

Windows RENAME(REN)命令如何解释通配符?

内置的帮助功能没有任何帮助-它根本无法解决通配符。

微软的TechNet XP联机帮助也好不了多少。这是关于通配符的所有说明:

“可以在任何一个文件名参数中使用通配符(*?)。如果在filename2中使用通配符,则通配符表示的字符将与filename1中的相应字符相同。”

没有太多帮助-可以用多种方式解释该语句。

在某些情况下,我已经成功地在filename2参数中成功使用了通配符,但是它一直是反复试验的。我一直无法预期什么有效,什么无效。通常,我不得不诉诸于编写带有FOR循环的小型批处理脚本,该循环解析每个名称,以便我可以根据需要构建每个新名称。不太方便。

如果我知道处理通配符的规则,那么我认为我可以更有效地使用RENAME命令,而不必频繁地使用批处理。当然,知道规则也将有利于批处理开发。

(是的-在这种情况下,我会发布一对配对的问题和答案。我已经厌倦了不了解规则并决定自己进行实验。我认为很多其他人可能会对我发现的东西感兴趣)


这里有很多关于如何使用通配符重命名的好例子:lagmonster.org/docs/DOS7/z-ren1.html
马修·洛克

5
@MatthewLock-有趣的链接,但是这些规则和示例适用于MSDOS 7,而不适用于Windows。有显着差异。例如,MSDOS不允许在*Windows 之后添加附加字符。这会产生巨大的后果。我希望我早已知道该站点。这可能使我的调查更加容易。MSDOS7规则与长文件名之前的旧DOS规则显着不同,它们是Windows处理方式的一个步骤。我已经找到了文件名较长的DOS规则,这些规则对我的调查毫无用处。
dbenham

我不知道;)
马修·洛克

Answers:


116

这些规则是在Vista机器上进行大量测试后发现的。没有对文件名中的Unicode执行任何测试。

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禁用NTFS卷上短8.3名称的生成,这时文件掩码结果的解释更为直接。禁用短名称之前生成的任何短名称都将保留。

targetMask

注意-我尚未进行任何严格的测试,但看来这些相同的规则也适用于COPY命令的目标名称

targetMask指定新名称。它总是应用于全名;即使sourceMask与8.3的短名称匹配,也不会将targetMask应用于8.3的短名称。

sourceMask中是否存在通配符对targetMask中的通配符处理方式没有影响。

在下面的讨论- c代表任意字符,是不是*?.

严格按照从左到右的顺序对源名称进行targetMask处理,没有回溯。

  • c-只要不是下一个字符,就在源名称中提升位置.,并将其追加c到目标名称之后。(用替换源中的字符c,但从不替换.

  • ?-匹配从源长名称下一个字符,只要下一个字符不是其附加到目标名称. 如果下一个字符是.,或者如果在源名称的末尾则没有字符被添加到结果和当前源名称中的位置保持不变。

  • *在targetMask的末尾-将所有剩余的字符从源追加到目标。如果已经在源末尾,则不执行任何操作。

  • *c-匹配从当前位置到最后一次出现的所有源字符c(区分大小写的贪婪匹配),并将匹配的字符集附加到目标名称。如果c未找到,则将附加源中所有剩余的字符,然后单击“添加”。c 这是我知道的唯一情况,Windows文件模式匹配区分大小写。

  • *.-通过从匹配的当前位置的所有源字符最后的次数.(贪婪匹配)和附加字符的匹配组的目标名称。如果.未找到,则将附加源中的所有剩余字符,后跟.

  • *?-将所有剩余的字符从源追加到目标。如果已经在源的末尾,则不执行任何操作。

  • .*位于最前面-在第一次出现时使源代码中的位置前进,.而不复制任何字符,并追加.到目标名称。如果.在源中找不到,则前进到源末尾并追加.到目标名称。

该targetMask已经用尽之后,任何尾随.{space}被修剪掉所产生的目标名称的末尾,因为Windows文件名不能结束.{space}

一些实际的例子

在任何扩展名之前将字符替换为第一和第三位置(如果尚不存在,则添加第二或第三字符)

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 的sourceMask 将与所有文件匹配,因为它始终与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。然后,重命名继续寻找要处理的更多文件,并通过新的短名称来找到新命名的文件223456~1.X。然后再次重命名该文件,得到的最终结果223456789.123.xx

如果禁用8.3名称生成,则RENAME会提供预期的结果。

我还没有完全弄清楚引发这种奇怪行为的所有触发条件。我担心可能会创建一个永无止境的递归RENAME,但是我从来没有能够诱导一个。

我相信以下所有内容都必须正确才能引发该错误。我看到的每个错误案例都有以下条件,但并非所有满足以下条件的案例都存在错误。

  • 短的8.3名称必须启用
  • sourceMask必须与原始长名称匹配。
  • 初始重命名必须生成一个短名称,该短名称也必须与sourceMask相匹配
  • 初始重命名的短名称的排序必须晚于原始短名称(如果存在)?

6
多么彻底的答案。+ 1。
meder omuraliev 2012年

精心制作!
Andriy M

13
基于此,Microsoft应该在中添加“有关用法,请参阅superuser.com/a/475875REN /?
efotinis 2013年

4
@CAD-此答案是Simon应我的要求在其网站上包含的100%原始内容。查看该SS64页面的底部,您会看到Simon为我的工作功劳。
dbenham

2
@ JacksOnF1re-新信息/技术已添加到我的答案中。您实际上可以Copy of 使用晦涩的正斜杠技术删除前缀:ren "Copy of *.txt" "////////*"
dbenham

4

与exebook相似,这是一个C#实现,用于从源文件获取目标文件名。

我在dbenham的示例中发现了1个小错误:

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

    }

感谢您对我的示例中的错误有所了解。我已经编辑了答案以解决此问题。
dbenham 2014年

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

我设法用BASIC编写了以下代码以屏蔽通配符文件名:

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

4
您能否阐明这是如何回答问题的?
fixer1234'2013-10-13

它复制REN用于通配符匹配的函数,例如根据重命名文件名之前调用函数的方式处理REN * .TMP * .DOC。
eoredson '16
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.