dbe*_*ham 89 windows command-line rename batch-file cmd.exe
Windows RENAME (REN) 命令如何解释通配符?
内置的帮助工具没有帮助 - 它根本不解决通配符。
在微软的TechNet XP联机帮助也好不了多少。以下是关于通配符的全部内容:
“您可以在任一文件名参数中使用通配符(
*和?)。如果您在 filename2 中使用通配符,则通配符表示的字符将与 filename1 中的相应字符相同。”
没有太大帮助 - 可以通过多种方式解释该语句。
在某些情况下,我已经成功地在filename2参数中使用通配符,但它一直是反复试验。我无法预测什么有效,什么无效。我经常不得不使用 FOR 循环编写一个小批量脚本来解析每个名称,以便我可以根据需要构建每个新名称。不是很方便。
如果我知道如何处理通配符的规则,那么我认为我可以更有效地使用 RENAME 命令,而不必经常求助于批处理。当然,了解规则也有利于批量开发。
(是的 - 这是我发布成对问题和答案的情况。我厌倦了不知道规则并决定自己进行实验。我想很多其他人可能对我的发现感兴趣)
dbe*_*ham 141
这些规则是在 Vista 机器上进行大量测试后发现的。未对文件名中的 unicode 进行测试。
RENAME 需要 2 个参数 - 一个 sourceMask,后跟一个 targetMask。sourceMask 和 targetMask 都可以包含*和/或?通配符。通配符的行为在源掩码和目标掩码之间略有不同。
注- REN可用来重命名文件夹,但通配符不能重命名的文件夹时,无论是在sourceMask或targetMask允许的。如果 sourceMask 至少匹配一个文件,则文件将被重命名,文件夹将被忽略。如果 sourceMask 仅匹配文件夹而不匹配文件,则如果源或目标中出现通配符,则会生成语法错误。如果 sourceMask 不匹配任何内容,则会导致“找不到文件”错误。
此外,在重命名文件时,只允许在 sourceMask 的文件名部分使用通配符。文件名前的路径中不允许使用通配符。
sourceMask 用作过滤器来确定哪些文件被重命名。通配符在这里的作用与任何其他过滤文件名的命令相同。
?- 匹配任何 0 或 1 个字符,除了 . 这个通配符是贪婪的 - 如果它不是 a,它总是消耗下一个字符. 但是,如果在名称结尾或下一个字符是 a,它将不匹配任何内容而不会失败.
*- 匹配任何 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,但从不替换.)
?-匹配从源长名称的下一个字符,并将其追加到目标名称,只要该源字是不. 如果下一个字符是.,或者如果在源名称的末尾则没有字符被添加到结果和当前源名称中的位置不变。
*at end of targetMask - 将所有剩余字符从源附加到目标。如果已经在源的末尾,则什么都不做。
*c- 匹配从当前位置到最后一次出现c(区分大小写的贪婪匹配)的所有源字符,并将匹配的字符集附加到目标名称。如果c未找到,则附加源中的所有剩余字符,然后是c 这是我所知道的 Windows 文件模式匹配区分大小写的唯一情况。
*.- 匹配从当前位置到最后一次出现.(贪婪匹配)的所有源字符,并将匹配的字符集附加到目标名称。如果.未找到,则追加来自源的所有剩余字符,然后是.
*?- 将所有剩余字符从源附加到目标。如果已经在源的末尾,则什么都不做。
.without *in front - 在不复制任何字符的情况下,通过第一次出现在源中的位置前进.,并附.加到目标名称。如果.未在源中找到,则前进到源的末尾并附.加到目标名称。
用完 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
Run Code Online (Sandbox Code Playgroud)
更改每个文件的(最终)扩展名
ren * *.txt
a -> a.txt
b.dat -> b.txt
c.x.y -> c.x.txt
Run Code Online (Sandbox Code Playgroud)
为每个文件附加一个扩展名
ren * *?.bak
a -> a.bak
b.dat -> b.dat.bak
c.x.y -> c.x.y.bak
Run Code Online (Sandbox Code Playgroud)
在初始扩展后删除任何额外的扩展。请注意,?必须使用 enough 来保留完整的现有名称和初始扩展名。
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)
Run Code Online (Sandbox Code Playgroud)
与上面相同,但过滤掉初始名称和/或扩展名超过 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)
Run Code Online (Sandbox Code Playgroud)
更改_名称最后的字符并尝试保留扩展名。(如果_出现在扩展中,则无法正常工作)
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)
Run Code Online (Sandbox Code Playgroud)
任何名称都可以分解为由. 字符分隔的组件,只能在每个组件的末尾附加或删除。不能在组件的开头或中间删除或添加字符,同时使用通配符保留其余部分。任何地方都允许换人。
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
Run Code Online (Sandbox Code Playgroud)
如果启用短名称,则名称至少为 8?且?扩展名至少为 3的 sourceMask将匹配所有文件,因为它将始终匹配 8.3 短名称。
ren ????????.??? ?x.????999.*rForTheCourse
part1.part2.part3.part4 -> px.part999.part3.parForTheCourse
Run Code Online (Sandbox Code Playgroud)
这篇 SuperUser 帖子描述了如何使用一组正斜杠 ( ) 从文件名中/删除前导字符(除了.)。每个要删除的字符都需要一个斜杠。我已经确认了 Windows 10 机器上的行为。
ren "abc-*.txt" "////*.txt"
abc-123.txt --> 123.txt
abc-HelloWorld.txt --> HelloWorld.txt
Run Code Online (Sandbox Code Playgroud)
不幸的是,领导/不能.在名称中删除。因此,该技术不能用于删除包含.. 例如:
ren "abc.xyz.*.txt" "////////*.txt"
abc.xyz.123.txt --> .xyz.123.txt
abc.xyz.HelloWorld.txt --> .xyz.HelloWorld.txt
Run Code Online (Sandbox Code Playgroud)
此技术仅适用于源掩码和目标掩码都用双引号括起来的情况。没有必要引号的以下所有形式都失败并出现此错误: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
Run Code Online (Sandbox Code Playgroud)
该/不能被用来去除任何的字符在中间或文件名的结束。它只能删除前导(前缀)字符。另请注意,此技术不适用于文件夹名称。
从技术上讲,/它不能用作通配符。而是按照c目标掩码规则进行简单的字符替换。但是在替换之后,REN 命令识别/出文件名中的无效,并/从名称中去除前导斜杠。如果 REN 检测到/目标名称的中间,则会出现语法错误。
从一个空的测试文件夹开始:
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
Run Code Online (Sandbox Code Playgroud)
我相信 sourceMask*1*首先匹配长文件名,然后将文件重命名为223456789.123.x. RENAME 然后继续寻找更多要处理的文件,并通过新的短名称223456~1.X. 然后再次重命名该文件,最终结果为223456789.123.xx.
如果我禁用 8.3 名称生成,则 RENAME 会给出预期的结果。
我还没有完全计算出所有必须存在的触发条件才能引发这种奇怪的行为。我担心创建一个永无止境的递归 RENAME 是可能的,但我永远无法诱导一个。
我相信以下所有内容都必须正确才能引发错误。我看到的每个被窃听的案例都有以下条件,但并非所有满足以下条件的案例都被窃听。
小智 5
与 exebook 类似,这里有一个从源文件获取目标文件名的 C# 实现。
我在 dbenham 的示例中发现了 1 个小错误:
ren *_* *_NEW.*
abc_newt_1.dat -> abc_newt_NEW.txt (should be: abd_newt_NEW.dat)
Run Code Online (Sandbox Code Playgroud)
这是代码:
/// <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('.', ' ');
}
Run Code Online (Sandbox Code Playgroud)
这是一个用于测试示例的 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));
}
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
172984 次 |
| 最近记录: |