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 的“专业”级别上这样做是非常不愉快的经历。我也是软件行业的,但不是微软的,但我可以预测这会得到最低优先级和最低严重性,所以我不会这样做。