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

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

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

内置的帮助功能毫无帮助 —— 它根本没有解决通配符问题。

Microsoft Technet XP 联机帮助并没有好多少。以下是有关通配符的全部说明:

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

没有太大的帮助——该语句可以有很多种解释。

我成功地在文件名2在某些情况下,我可以使用参数,但总是反复试验。我无法预测什么有效,什么无效。我经常不得不写一个小的批处理脚本,用 FOR 循环来解析每个名称,这样我就可以根据需要构建每个新名称。不是很方便。

如果我知道通配符的处理规则,那么我想我可以更有效地使用 RENAME 命令,而不必经常求助于批处理。当然,了解规则也会有利于批处理开发。

(是的 - 这是我发布配对问答的情况。我厌倦了不了解规则,并决定自己进行实验。我想很多人可能会对我的发现感兴趣)

答案1

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

RENAME 需要 2 个参数 - 一个 sourceMask,后面跟着一个 targetMask。sourceMask 和 targetMask 都可以包含*and/or?通配符。通配符的行为在源掩码和目标掩码之间略有不同。

笔记-REN 可用于重命名文件夹,但通配符不是重命名文件夹时,sourceMask 或 targetMask 中允许使用通配符。如果 sourceMask 至少匹配一个文件,则将重命名文件并忽略文件夹。如果 sourceMask 仅匹配文件夹而不匹配文件,则如果源或目标中出现通配符,则会产生语法错误。如果 sourceMask 不匹配任何内容,则会产生“文件未找到”错误。

此外,重命名文件时,仅允许在 sourceMask 的文件名部分使用通配符。不允许在文件名之前的路径中使用通配符。

源掩码

sourceMask 用作过滤器来确定哪些文件被重命名。通配符在这里的作用与任何其他过滤文件名的命令相同。

  • ?- 匹配任意 0 个或 1 个字符除了 . 此通配符是贪婪的 - 它总是消耗下一个字符,如果它不是,. 但是,如果在名称末尾,或者下一个字符是.

  • *- 匹配任意 0 个或多个字符包括 .(下面有一个例外)。此通配符不贪婪。它将匹配尽可能少或尽可能多的字符,以使后续字符能够匹配。

所有非通配符都必须与自身匹配,但有少数特殊情况例外。

  • .- 匹配自身,或者如果没有剩余字符,则可以匹配名称的结尾(无)。(注意 - 有效的 Windows 名称不能以 结尾.

  • {space}- 匹配自身,或者如果没有剩余字符,则可以匹配名称的结尾(无)。(注意 - 有效的 Windows 名称不能以 结尾{space}

  • *.在末尾 - 匹配任意 0 个或多个字符除了 . 终止.实际上可以是和的任意组合.{space}只要掩码中的最后一个字符是,. 这是唯一的例外,它*不简单地匹配任何字符集。

上述规则并不复杂。但还有一条非常重要的规则使情况变得混乱:sourceMask 会与长名称和短 8.3 名称(如果存在)进行比较。最后这条规则可能会使结果的解释变得非常棘手,因为当掩码通过短名称匹配时并不总是很明显。

可以使用 RegEdit 禁用 NTFS 卷上的 8.3 短名称生成,此时文件掩码结果的解释会更加直接。禁用短名称之前生成的任何短名称都将保留。

目标掩码

注意 - 我没有做过任何严格的测试,但看起来这些相同的规则也适用于 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

删除初始扩展名后的任何额外扩展名。请注意,?必须使用 adequate 来保留完整的现有名称和初始扩展名。

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

删除名称前缀的有用怪癖/错误?

这篇超级用户帖子/描述了如何使用一组正斜杠 ( ).从文件名中删除前导字符( 除外)。每个要删除的字符都需要一个斜杠。我已经在 Windows 10 计算机上确认了此行为。

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

不幸的是,名称中的前导/无法删除.。因此该技术不能用于删除包含的前缀.。例如:

ren "abc.xyz.*.txt" "////////*.txt"
  abc.xyz.123.txt        --> .xyz.123.txt
  abc.xyz.HelloWorld.txt --> .xyz.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

不能/用于删除文件名中间或结尾的任何字符。它只能删除前导(前缀)字符。另请注意,此技术不适用于文件夹名称。

从技术上讲,/并不是通配符。相反,它只是按照c目标掩码规则进行简单的字符替换。但在替换之后,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 匹配的短名称
  • 初始重命名的短名称必须排序晚于原始短名称(如果存在?)

答案2

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

    }

答案3

我已设法用 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

我必须做一个注释:

“ren” 不等于“rename”,至少在 2021 年 Windows 10 / Server 2016 上不等于。

重命名命令有效...但是 ren 有一个错误

如果目标名称与 %random% 或 %date% 等环境变量结合使用,则删除星号

使用 ren 命令时,%random% 仅被取一次... ren.txt %随机%.txt 实际上就是“贪婪的”,星号会被吃掉。Ren 将使用第一个文件生成类似 12521.txt 的内容,然后不再说存在重复文件……

例如:我有

johndoe.txt

janedoe.txt

与 ren

ren "*.txt" "%random%*.txt"

存在重复的文件名,或者找不到该文件。

目录显示

12533.txt

janedoe.txt

with rename (%random% is now different):

rename "*.txt" "%random%*.txt"

回声

johndoe31008.txt

janedoe31008.txt

具有完全相同参数的重命名命令确实有效。

当我妻子告诉我我需要受到惩罚时,我会向微软提出支持请求。我有资格这样做,但上次我这样做时,在 MSDN 的“专业”级别上这样做是非常不愉快的经历。我也是软件行业的,但不是微软的,但我可以预测这会得到最低优先级和最低严重性,所以我不会这样做。

相关内容