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

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

RENAME需要2个参数 – – 一个sourceMask,然后是targetMask。sourceMask和targetMask都可以包含*和/或?通配符。通配符的行为在源掩码和目标掩码之间略有变化。

Note - REN 可用于重命名文件夹,但当重命名文件夹时,sourceMask 或 targetMask 中不允许**通配符。如果sourceMask至少匹配一个文件,那么文件将被重命名,文件夹将被忽略。如果sourceMask只匹配文件夹而不匹配文件,那么如果source或target中出现通配符,将产生语法错误。如果sourceMask不匹配任何内容,则会产生 “未找到文件 "的错误。

另外,重命名文件时,只允许在sourceMask的文件名部分使用通配符。通配符不允许出现在通往文件名的路径中。

sourceMask

sourceMask的作用是作为一个过滤器来决定哪些文件被重命名。通配符的作用和其他过滤文件名的命令一样。

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

  • * - 匹配任何 0 或更多字符_包括 . (下面有一个例外)。这个通配符并不贪心。它将根据需要匹配少量或尽可能多的字符,以使后续字符能够匹配。

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

  • . - 匹配自身,或者如果没有更多的字符,它可以匹配名称的末尾(无)。

  • . - 如果没有更多的字符,则匹配本身或它可以匹配名称的结尾(无)。

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

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

在NTFS卷上可以使用RegEdit来禁止生成8.3短名,这时对文件掩码结果的解释就更直接了。任何在禁用短名之前生成的短名都将保留。

targetMask

注意–我还没有做任何严格的测试,但是这些规则似乎也适用于COPY commmand的目标名

targetMask指定了新的名字。它总是应用于完整的长名;targetMask永远不会应用于短的8.3名,即使sourceMask匹配短的8.3名。

sourceMask中是否存在通配符对targetMask中如何处理通配符没有影响。

在下面的讨论中,.代表任何不是*c*的字符

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

  • ? - 只有当源字符不是.时,才会在源名称中推进位置,并始终将c追加到目标名称中。

  • . - 只要源字符不是c,就从源长名中匹配下一个字符,并将其追加到目标名中,如果下一个字符是c,或者在源名的末尾,那么结果中不会添加任何字符,源名中的当前位置不变。

  • . at end of targetMask - 将所有剩余字符从源名添加到目标名。如果已经在源名称的末尾,则不做任何操作。

  • ? - 匹配从当前位置到最后出现的.的所有源字符(区分大小写的贪婪匹配),并将匹配的字符集追加到目标名称中。如果没有找到 .,那么所有剩余的字符都会被追加到源文件中,然后再追加 *,这是我知道的唯一一种 Windows 文件模式匹配区分大小写的情况。

  • *c - 匹配从当前位置到最后出现的 c 的所有源字符 (贪婪匹配),并将匹配的字符集附加到目标名称上。字符到目标名称。如果没有找到 c,则会将源名中所有剩余的字符附加到目标名中,然后是 c

  • *. - 将源名中所有剩余的字符附加到目标名中。如果已经在源码的末尾,则不做任何操作。

  • . 前面没有. - 将源文件中的位置向前推进,从 .的第一个出现开始,而不复制任何字符,并将 *?追加到目标名称中。如果在源文件中没有找到 .,则前进到源文件的末尾,并将 *追加到目标名称。

targetMask用完后。任何尾部的..都会被修剪掉,因为Windows文件名不能以..结尾

一些实际的例子

在任何扩展名之前的第1和第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 的两端添加额外的 {space},以保留长达 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)

任何名称都可以分解成以0x6为界的组件,字符只能附加到每个组件的末尾或从每个组件中删除。不能从组件的开头或中间删除或添加字符,同时用通配符保留其余部分。允许在任何地方进行替换。

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 {space}、扩展名至少为 3 ?的 sourceMask 将匹配所有文件,因为它将始终匹配 8.3 的短名称。

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

删除名字前缀的有用怪癖/bug?

这个超级用户帖子描述了如何使用一组正向斜线(?)从文件名中删除前导字符。每个要删除的字符都需要一个斜线。我已经在Windows 10机器上确认了该行为。

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

只有当源和目标掩码都用双引号括起来时,该技术才会有效。以下所有没有必要引号的表格都会因该错误而失败。_

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 The syntax of the command is incorrect首先匹配了长文件名,文件被重命名为预期的结果/。然后rename继续寻找更多的文件进行处理,通过新的短名/找到新命名的文件。然后再次对文件进行重命名,得到最终结果/

如果我禁用8.3名称生成,那么RENAME就会得到预期的结果。

我还没有完全弄清楚所有的触发条件,这些条件必须存在,才能引起这种奇怪的行为。我担心可能会创建一个永无止境的递归RENAME,但我一直没能诱导出这样的行为。

我相信以下所有条件必须为真才能诱发这个bug。我看到的每一个被窃听的案例都有以下条件,但并不是所有满足以下条件的案例都被窃听。

-必须启用短8.3名 -源码掩码必须与原始长名匹配。 -初始重命名必须生成一个短名,这个短名也必须与sourceMask相匹配 -初始重命名的短名必须比原来的短名排序晚(如果它存在?

4
4
4
2014-12-16 10:13:11 +0000

类似于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));

    }
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中写了这段代码来屏蔽通配符文件名:

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
```。