批处理脚本编程之书-全-
批处理脚本编程之书(全)
原文:
zh.annas-archive.org/md5/9b100141775160857024c1bb4aa7463c译者:飞龙
前言

一本关于 bat 文件的书?为什么?难道阿兹特克人也用过 Batch 吗?它不是脚本语言中的 Betamax 吗?你应该写一本关于更新、更有吸引力的编程语言的书,而不是一本 Model T 的维修手册。
我希望我能直接忽略这种抗议,将其视为啰嗦小人的消极言论,但这是一个我感到必须回应的观点。Batch 语言并不新颖,按照今天的标准来看,它缺乏一些功能让人费解,但它依然是一个极为有用的语言,短期内不会消失,尤其是它已经与每台 Windows 电脑上的操作系统捆绑在一起。虽然 Batch 只是众多脚本语言中的一种,但仍然有许多大型和小型公司在使用 Batch 代码,有些任务确实比任何其他语言更适合用 bat 文件来完成。至于那些缺失的功能——布尔值、数组、哈希表、栈,甚至面向对象设计等等——在本书的结尾,我会教你如何自己构建这些功能。
但我个人写一本关于 Batch 脚本的书的最直接原因是,经过二十年的个人和职业用途的 bat 文件编写,我相信自己已经在这个领域学到了足够的知识,能够与更广泛的社区分享我的经验和见解。在很多年里,我在一家公司为 Windows 服务器编写大规模处理程序,所有的程序都由 bat 文件驱动。其他人可能会选择更现代的脚本语言,但在我之前的那位程序员已经精通了 bat 文件的艺术,以至于没有人认真考虑过使用 Batch 之外的替代方案。直到他退休,我才在非正式的情况下接替了他的工作,扮演起了罗宾的角色,直到后来我被正式晋升为蝙蝠侠。
编写 Batch 代码仍然是任何程序员甚至非程序员的重要技能,但现有的文档往往稀缺、分散,甚至有时不准确。与其他语言相比,掌握 Batch 语言需要大量的经验和实验,而我有一个独特的视角可以分享。这就是我写这本书的原因。
本书的读者群体
这本书既不是为初学者写的,也不是为专家写的;它是为两者而写的。实际上,我希望能够接触到三个群体。第一个是那些几乎每天都在编写、维护或与 bat 文件打交道的程序员。第二个是所有在 Windows 机器上工作的其他程序员,第三个是那些也在 Windows 电脑上工作的非程序员。
第一类人群,那些与 Batch 密切合作的人,显然也在本书的读者范围内。这本书是我在 Batch 脚本领域二十年深入工作的结晶。到本书的最后,你将会探索到一些复杂的概念,比如创建命令、数据结构、运算符,甚至是语言的创造者未曾预见的编码范式。我将逐步讲解这些复杂的内容,但我希望在这些页面中,你能找到掌握这门语言以及进一步探索其未涉及部分所需的所有工具。
如果你属于第二类人群,你可能不会维护成千上万行的 Batch 代码,但在 Windows 电脑上,你会用其他语言编写代码,并且你至少应该对 Batch 有一定的了解。这项技能让你可以通过运行一个简单的(或者可能不太简单的)bat 文件,完成一些常见且重复的任务。用其他语言编写的代码在动画化时会面临一些挑战,其中之一就是你的机器环境与程序最终运行的生产环境不同。为此,我将向你展示如何用几行 Batch 代码模拟或模拟另一个计算机的环境。到本书的最后,我相信你会发现,bat 文件是许多问题的解决方案。
即使是非程序员,最后这一类人,也能通过一些 Batch 代码来简化重复性任务,比如移动文件、合并报告或连接到网络驱动器,从而使 Windows 资源管理器更易使用。由于编程不在你的工作职责范围内,你的雇主不太可能为你的计算机安装其他编程语言的基础设施,来让你完成相对简单的编码任务,但你所需要的编写和执行 bat 文件的工具已经在你的工作站上了。编写 bat 文件所需的技能就是能够创建文本文件、重命名文件,并向文件中输入几行内容。如果你能双击文件,你就能运行 bat 文件。这就是你所需要的一切(除了这本书)。
如何阅读本书
每个作者,无论哪种类型的作品,都会设想读者坐在火旁,品着雪利酒(或者对我来说是一杯好的麦芽酒,不要太甜),全神贯注地读着每一个字,阅读、理解,再继续阅读,直到书本结束。嗯……这是一本技术书籍,所以我的读者中有相当一部分是程序员,他们坐在电脑前,试图弄明白为什么他们的 bat 文件没有按预期运行。我曾经也经历过这种困境,深刻理解其中的难处,为了帮助你,我将这本书组织成了有标题、子标题、详细的目录和索引。你可以找到能解答问题的章节和页码,并直接跳到那一部分,但这并不是阅读这本书或任何一本书的理想方式。
我将本书结构设计为简短而简洁的章节。即使你只是想解决某个特定问题,我也建议你通读相关章节,因为每一章都像是一份课程计划。(我的日常工作是编码,但我受过数学训练,已经在康涅狄格州曼彻斯特社区学院教授了二十多年的数学课程。)
一堂典型的课程从基本概念开始,接着是一些简单的示例。然后我会深入探讨该主题的复杂性,展示该概念的应用,甚至解释常见的陷阱和避免的错误。并不是每一课(或每一章)都会遵循这个结构,但很多都会。如果你有关于如何复制文件的疑问,我建议你从头到尾阅读第七章。跳到章节中间就像是上课迟到 20 分钟。
我还建议你亲自执行一些我提供的编码示例。大部分代码片段都很短且容易输入,你可以从本书的在线版本中获取较长的代码。更好的做法是修改代码,探索其结果,并将其变成你自己的代码。
本书结构
Batch 的独特之处在于一个单一的命令——for 命令,远远超过其他命令,支配了整个 Batch 脚本语言。因此,我将本书分为三部分,围绕这个至关重要的命令进行组织。第一部分标题为“基础知识”,涵盖了你在学习 for 命令之前需要掌握的相关内容。第一部分包括以下章节:
第一章:Batch 本章将向你介绍 Batch 脚本语言,同时帮助你编写可能是你第一次编写的 bat 文件。我还会提供一些编辑技巧,由于 Batch 是一种解释型语言,我还会讨论解释器的角色和重要性。
第二章:变量与值 本章讲解如何定义变量,并查询其值,无论是显示到控制台还是用于其他目的。
第三章:作用域与延迟扩展 在学习如何定义变量在 bat 文件中的访问范围后,我将介绍 Batch 中最有趣的特性之一——延迟扩展,它影响如何解析变量。
第四章:条件执行 if...else 语句是大多数编程语言中的基本结构,Batch 也不例外。在本章中,你将学习如何根据不同的条件语句执行或跳过代码片段。
第五章:字符串与布尔数据类型 本章讲解构建和连接字符串,提取更大字符串中的子字符串,以及在字符串中替换特定文本的任务。我还将介绍我们将要构建的许多非 Batch 内建工具中的第一个——布尔值或评估为真或假的变量。
第六章: 整数与浮点数据类型 你将学习加法、减法、乘法和除法等整数操作的所有细节。本章还详细介绍了取余运算,以及八进制和十六进制的算术运算。然后我将深入探讨另一种在批处理语言中并非固有的数据类型:浮点数。
第七章: 与文件的操作 本章处理与文件相关的许多任务,如复制、移动、删除、重命名文件,甚至创建一个空文件。
第八章: 执行编译后的程序 本章探讨了如何在有路径和没有路径的情况下调用程序,特别是当你没有提供路径时,解释器是如何找到你的程序的。
第九章: 标签与非顺序执行 本章介绍了标签以及它们在允许你将代码执行转向批处理文件中的前后命令中所起的作用,有时甚至会启动一个循环。
第十章: 调用例程和批处理文件 在上一章的基础上,你将学习在批处理文件中创建可调用例程的所有内容,以及如何从另一个批处理文件中调用一个批处理文件。
第十一章: 参数与参数值 如果你不能将参数传递给被调用的代码,或者被调用的代码不能将参数值返回给你,那么调用其他代码往往毫无意义。本章深入探讨了这一过程的所有细节,甚至揭示了隐藏的参数。
第十二章: 输出、重定向与管道 在区分了程序员和解释器所产生的输出后,我讨论了如何将这两者重定向到控制台或文件,这自然引出了将一个命令的输出传递给另一个命令的管道技术及其应用。
第十三章: 与目录的操作 本章详细介绍了如何创建和删除目录,以及如何检索有关目录及其内容的大量信息。我还演示了将本地和网络目录映射到驱动器字母的技术。
第十四章: 转义 如果你想在字符串中使用某个字符,而这个字符在批处理(Batch)中是一个具有特定功能的特殊字符,你会遇到问题。本章详细介绍了针对这一问题的解决方案,有时这些解决方案可能会出乎意料地复杂。
第十五章: 交互式批处理 在本章中,你将构建一个功能完整的批处理用户界面,允许从控制台接受自由格式文本,并让用户从列表中选择一个项目,除此之外还有其他功能。
第十六章: 代码块 代码块不仅仅是代码的块。本章探讨了代码块中的变量为何以及如何能够拥有两个不同的值。我还将介绍裸代码块,并解释它的意义。
第二部分的标题为“for命令”,正如其名称所暗示的,这部分内容深入探讨了前面提到的for命令,它为你提供了一大批(双关含义)功能。你将在这里找到以下内容:
第十七章:for命令的基础 本章详细介绍了for命令的功能,未引入任何选项,但仍然非常强大。它可以创建处理任意数量输入文件或文本字符串的循环,并通过使用修饰符,你将能够确定关于文件的几乎所有信息,除了文件内容。
第十八章:目录、递归和迭代循环 本章探讨了for命令的一些选项,这些选项提供了更多的功能。使用其中一个选项,命令可以遍历一个目录列表,而不是文件名列表。使用另一个选项,你可以递归地处理目录和子目录,例如,在一个文件夹及其所有子文件夹中搜索符合特定模式的文件。还有一个选项将命令转换为一个迭代循环,每次执行时递增或递减索引。
第十九章:读取文件和其他输入 最后一个选项为for命令提供了强大功能,允许你读取文件。本章详细介绍了如何在读取文件时解析或重新格式化每一条记录。除了传统的文件,命令还可以读取和处理普通文本,无论是硬编码的还是来自变量的,甚至可以将另一个命令的输出作为文件读取。
第二十章:for命令的高级技巧 本章深入探讨了for命令的一些令人印象深刻的应用,例如将其他语言(例如 PowerShell 和 Python)的命令嵌入到你的批处理脚本中。我还讨论了绕过命令限制的一些技巧。
“高级主题”是第三部分的标题,讨论了各种各样的主题,特别是那些在我拥有for命令工具之前无法涉及的内容。以下是详细内容:
第二十一章:伪环境变量 本章详细介绍了伪环境变量,或称特殊变量,这些变量并不总是由你控制。例如,批处理有一些特定的变量,用来保存日期、时间以及批处理命令和被调用程序的返回代码。我还解释了如何安全地设置这些变量,并分享了批处理文件(.bat)和命令文件(.cmd)之间的区别。
第二十二章:编写报告 本章解释了如何使用批处理格式化基础的文本文件报告,包括标题、详细记录和结尾记录。
第二十三章:递归 一些问题非常适合使用递归技术,即代码调用自身的方法。本章通过详细且有趣的例子演示了如何在批处理语言中实现递归。
第二十四章:文本字符串搜索 本章探讨了多种文本字符串搜索的排列方式。搜索文件、变量或硬编码文本中的一个或多个单词或字面字符串。你还会找到一些使用正则表达式的例子。
第二十五章:批处理文件构建批处理文件 本章详细介绍了一个批处理文件如何构建第二个完全功能的批处理文件,包括动态和静态代码,同时也思考了如果是阿基米德,他会如何使用批处理。
第二十六章:自动重启与多线程 在讨论如何自动重启失败的进程后,本章通过构建一个批处理文件来自动终止并重启一个挂起的进程。我还讨论了如何在单一批处理文件的控制下同时执行多个线程或并发任务。
第二十七章:与/或运算符 这可能听起来像是一个基础话题,但批处理语言本身并没有与运算符或或运算符。本章构建了模拟这些运算符的技术,用于各种情况。
第二十八章:紧凑的条件执行 本章详细介绍了一种紧凑且有趣的结构,它看起来和行为非常类似于 if...else 结构。我会在检查两者之间微妙但重要的差异后,讨论何时最好使用每种方式。
第二十九章:数组和哈希表 这些数据结构并非批处理的内建功能,但你将学习如何填充和从数组及哈希表中检索数据。
第三十章:杂项 本章涵盖了一些不同的主题:文件属性、位操作、查询 Windows 注册表以及排序文件内容。
第三十一章:故障排除技巧与测试技术 我分享了多年来在开发和测试批处理文件中学到的许多技巧和方法。
第三十二章:面向对象设计 尽管听起来有点疯狂,本章将呈现用户自定义批处理文件功能的顶峰。我将在讲解面向对象设计的四个支柱后,带领你通过一个尽可能全面实现这些原则的模型。我希望经验丰富的编码者会觉得本章既有信息性又有娱乐性。
第三十三章:堆栈、队列与现实世界中的对象 本章应用刚学到的面向对象设计原则,构建实现堆栈和队列数据结构的对象。
在第一部分和第三部分的每一章中,我都设定了一个狭窄的主题或任务讨论;我不打算讨论某个特定命令,但通常会在章节中介绍一条或多条命令。对于每一条命令,我都会解释它的功能,展示它的语法,并详细介绍我认为最有用的特性。
我的目标是,如果你不是程序员,你至少能读懂前两部分并从中获得信息。读得更深入一点,你或许就能成为一名程序员。
其他资源
如果你在寻找单个批处理命令的全面且简明的解释,不妨去看看 <wbr>ss64<wbr>.com<wbr>/nt<wbr>/。这是一个很棒且组织良好的资源,在写这本书时我参考了它很多次。本书不是命令的列表,而是关于如何用这些命令解决问题的讨论。我通常会展示我认为最有用的命令选项,但你可以在这个网站上找到完整的命令列表。
如果你(希望)遇到的情况很少能在这些页面上找到解决方案,那么下一个最好的选择是向在线的技术社区寻求帮助。用“bat file”加上你的问题在网络上搜索应该能得到一些结果。在众多的在线论坛中,我发现最好的创意和建议常常来自于 <wbr>stackoverflow<wbr>.com。
风格说明
大多数技术书籍和手册读起来都很枯燥,而我已尽最大努力打破这种趋势。首先,我没有忘记我的首要任务是解释我试图传授的技术内容。但举个例子,当讨论 sort 命令时,我不想对像苹果和香蕉这样的东西进行排序;与其如此,不如把《星际迷航》中的 Enterprise 星舰的舰长们排序,或者至少我觉得这样更有趣。讨论参数时,我使用了一个 Mad Libs 游戏,通过不同的词性作为参数传递。关于交互式批处理的章节还与用户分享了些笑话。
并非每一章都能举出有趣的例子或幽默的轶事,但我已经尽力避免让文件的第一条记录是“Record 1”或一串用管道符号分隔的字段(如 field 1| field 2| field 3)。
理想情况下,我希望能引发一阵可听的笑声;如果能看到一抹微笑和点头,我也会很高兴;即便只是翻个白眼和叹气,我也能接受。无聊去死。我秉持着“与其平凡,不如独特地糟糕”这一座右铭。(我希望能为这句话归功于自己,但很多年前,在加利福尼亚州索诺玛县的本齐格酒庄旅游时,我们的导游用这句话来形容酒庄的哲学。)
注意事项
根据我的经验,Batch 相较于其他语言有许多显著的注意事项。在接下来的章节中,我常常会在一些看似明确的语法或用法陈述后加上除了这个词。(例如,“&符号用于终止命令,除非后面跟着第二个&符号或……”)英语独特之处在于,它的语法中有很多注意事项,这些在其他语言中根本不存在——想想看“i 在 e 前面,除了在 c 后面。”也许这使得 Batch 成为典型的爱国美国编程语言(或者也可能是英国式的)。
这些Batch 注意事项如此普遍,以至于我已开始称它们为 batveat(发音为 bat-vē-ät,商标待定)。这些注意事项对于没有指导的新用户来说可能非常令人沮丧,但随着章节的展开,我会指出那些曾经让我受困的 batveat,希望你能避免这些痛苦。
伍迪·格思里
我为这本书选择的引言来自传奇艺术家伍迪·格思里(Woody Guthrie)的一句相对著名的名言,但我曾犹豫是否使用它,因为担心它可能会被误解。引用的目的并非自负,而是富有理想的。伍迪曾穿越美国,提倡经济正义,同时也宣扬反对种族主义和性别歧视。他并非通过枯燥无味的演讲,而是通过吉他和敏锐的歌词来传播这些思想,这些歌词即使在他早逝之后,依然引起人们的共鸣。
伍迪·格思里试图将历史的轨迹引向社会正义,而我则在尝试通过富有信息性、可读性和娱乐性的语言,使一个深奥的编程语言变得更易接近。我希望能够为理解一个复杂话题做出贡献,并且我只能以伍迪的崇高榜样为目标,努力追随。
Batch 的爱
永远,永远不要邀请超过一个 Batch 程序员参加派对。一个人就足够了。如果我们没有同行,我们会像其他人一样谈论体育、政治、书籍、电影和旅行。但一旦你把至少两个程序员聚在一起,你就会听到类似“我最近找到了一种在 if 命令的条件语句中编码 or 操作符的新方法。你想让我分享给你吗?”我们会毁掉你的派对。
乐观主义者会说 Batch 是深奥的,而悲观主义者则会说它是晦涩的。事实可能介于两者之间,你会在本书中经常看到这两个词。它的语法与大多数编程语言不同,某些功能的缺失促使人们以富有创意的方式解决问题,这些问题在其他语言中可能显得无趣。结果就是,一些人在你的超级碗派对上讨论构造哈希表的不同方式,几乎让空气都变得稀薄。
我觉得这些难题令人精神焕发,这也是我喜欢在 Batch 脚本中编程的主要原因,而其他人可能会觉得这是一项苦差事。有时候,我真的很享受用一种编程语言来编写栈实现,这本身就是一项了不起的成就。为了简单展示一个挑战,"@"符号本身可以作为变量名,而从其值中提取倒数第二个字符需要使用语法 %@:~-2,1%。这看起来更像是漫画中的脏话,而不是代码,诚然,它确实显得有些深奥,甚至可能显得有些神秘,但请不要因为害怕就把这本书放下;我保证,只需几章,你就会完全理解。
在一群对这门技术略懂一二的程序员中成为 Batch 专家,可能会让你感觉像是一个苏美尔祭司——你是那种能解读脚本并将其意义与智慧传授给他人的特权人群中的一员。但我之所以能占据这个位置,并不是因为某种偶然的天赋,也不是为了个人私利而守护解读这门楔形文字的能力。通过这本书,我希望能让所有想学习这门并不那么古老的脚本的人,都成为高阶祭司和祭司女。在接下来的章节中,我会诚实地谈论我在这门语言中的问题和挫折,但我真的很喜欢编写批处理文件,等你读完这本书后,我希望能把你变成一个信徒。
第一部分:基础知识
就像批处理世界围绕着至关重要的 for 命令运转一样,本书也是如此。第一部分将探讨脚本语言的核心内容,而非基础知识,因为 Batch 语法没有什么是“基础”的。完成本部分后,你将能够编写许多 Batch 解决方案,但随着章节的推进,你会逐渐感受到第二部分中 for 命令的引力。
在这一部分,你将学习变量、作用域以及字符串、整数、浮点数和布尔值等数据类型。我还将讨论文件、目录,以及如何调用程序、内部例程和其他批处理文件。你将了解管道和转义等技巧,如何构建交互式批处理用户界面,等等。
第一章:1 批处理

如果你正在阅读这本书(我敢肯定你正在阅读),你是想编写一些批处理代码。在本章结束时,你将实现这一目标,编写并执行你可能的第一个 bat 文件。
在此过程中,我将介绍批处理脚本语言及其起源于 MS-DOS 命令提示符,以及包含其源代码的两种文件类型:bat 和 cmd。我还将讨论编辑器,它们是编写 bat 文件的主要工具,以及你可以使用的选项。最后,没有对批处理世界的介绍,就不能算完整,必须提及解释器的概况。
MS-DOS 命令提示符
MS-DOS(微软磁盘操作系统)命令提示符存在于每台安装了 Microsoft Windows 的计算机上。如果你年纪稍大一点——也就是,如果你见证过个人计算机的初期发展——你可能还记得那个黑色的矩形框(或者如果你回到 Windows 之前的时代,可能是整个屏幕),你在其中输入命令来执行各种任务,比如安装或启动程序,复制或删除文件。如今,普通用户几乎不再接触它了,因为图形用户界面将这些任务简化到几次点击就能完成,但它依然存在。
要访问 Windows 计算机上的命令提示符,进入开始菜单,在搜索框中输入CMD,即命令的缩写。按回车,MS-DOS 命令提示符将打开。在这个提示符下,你可以输入多种命令。你可以执行程序、复制文件,甚至删除整个*C:*驱动器。别担心,不过;你得知道相关命令才能做到这一点。我不会在这里列出所有可能的命令,因为这本书就是讲这个的,甚至它也并非详尽无遗,但我们可以看一个例子。假设你在一个项目中,文件夹里有好几个文档。每天下来,工作几个小时后,备份文件到另一个驱动器是个好主意。为此,你可以将列表 1-1 中的单个命令输入命令提示符并按回车。
**xcopy C:\****`YourPath`****\*.* D:\****`YourBackUpPath`****\ /F /S /Y**
列表 1-1:备份一些文件的命令
目前不用担心语法(等到第七章再说)。重点是,你可以每天在命令提示符中输入这个命令来复制所有文件。你可能会反驳说,这行命令很容易出错,比如路径中的某个部分或者那些斜杠后面的字母,这些字母到底是干什么的呢。难道不应该使用 Windows,即进入某个文件夹,选择所有文件,右击选择“复制”,然后进入另一个文件夹,右击选择“粘贴”,最后点击确认复制吗?我同意,这种方式比输入命令更简单,但请注意,Windows 的操作涉及八个独立步骤,并且可能需要十几个鼠标点击。
第三种方法比命令提示符方法和点击密集的 Windows 方法都要简单。这种方法就是使用批处理脚本语言进行编程。
批处理脚本语言
批处理对任何编码语言来说都是一个不太合适的名字,无论是脚本语言还是其他语言。一些流行的语言通常以咖啡、宝石、音乐符号或英国喜剧团体的名字命名。即便是被广泛诟病的 COBOL 也有自己独特的缩写,当提到 Pascal 时,大多数人首先想到的通常是 20 世纪的编程语言,而非 17 世纪的数学家。在这个谱系的另一端,“批处理”一词显得平淡无奇;即便在计算机科学的语境中,它也很模糊。批处理是指一次性运行多个作业或大量数据的通用术语,与本书的主题——批处理脚本语言截然不同。
微软在 1980 年代初为 MS-DOS 操作系统开发了批处理脚本或编码语言,并且自 1985 年以来,这一语言已被安装在每一台运行 Windows 的机器上。我猜想,微软的一位员工在厌倦了不断输入一系列重复命令后,可能会想,“如果我们能把这些命令批量处理成一个单一的文件,然后快速而轻松地执行,那该多好?”
这些批处理命令代表了批处理语言的起源。单独的命令并不构成一种语言;语言是执行这些命令的框架。将这些命令批量处理成一个文件使得分支逻辑、复杂循环和可重用代码成为可能——这就是一种真正语言的雏形。
批处理有许多用途。它的命令行起源使它成为计算机或系统管理的理想语言:创建、复制、移动和删除文件。批处理还可以检查和修改目录、注册表,并设置计算机的环境。
一个简单的批处理文件可以设置一些变量,并执行用许多其他语言编写的程序。批处理可以与用户互动,显示信息并收集数据。文件可以被读取、写入和修改。可以生成基本的报告,久而久之,你会发现批处理能够支持复杂而精密的脚本。
在个人电脑普及之前,其他操作系统也有类似批处理的脚本语言。Unix Shell 脚本在基于 Unix 的操作系统上执行,而 JCL(作业控制语言)对于 IBM 大型机至关重要。在向熟悉大型机的新同事介绍批处理时,我通常不会太严谨,而是将其描述为“PC 版的 JCL”。
根据我的经验,程序员对批处理的知识既广泛又浅薄。(我脑海里浮现出一群程序员,手持笔记本电脑,站在儿童游泳池中,裤脚卷起,害怕进入成人池,更不用说它的深水区了。)大多数程序员能够创建一个简单的批处理应用程序,但在面对更复杂的问题时,他们本能地会转而使用其他语言,而这些问题其实可以通过批处理更容易地解决。
Bat 文件
虽然“批处理”这个术语含糊不清,但大多数程序员都知道什么是 bat 文件。命令批处理被输入到批处理文件中(简称为 bat 文件)。因此,bat 文件是包含批处理源代码的文件。(然而,batphile是指那些对夜行性飞行哺乳动物有极大热情甚至是爱好的人。)当执行 bat 文件时,它包含的命令将依次执行,直到遇到终止执行的命令或文件末尾。
大多数 Windows 文件在文件名后面都有扩展名。现代的 Word 文档以 .docx 结尾。Excel 电子表格以 .xlsx 结尾。简单的文本文件通常以 .txt 结尾,PDF 文件则以...嗯,你应该明白了。为了区分 bat 文件与其他文件,它必须具有 .bat 扩展名。
默认情况下,Windows 中文件扩展名不会显示在文件名后面,但我们需要显示它们。要显示扩展名,请在 Windows 资源管理器的 查看 菜单下寻找显示文件扩展名的选项。如果不容易找到,可以在网上搜索“显示文件扩展名”以及你的操作系统名称。
现在你的文件会显示扩展名;例如,你的 Word 文档可能会以 .docx 为后缀。更重要的是,你的 bat 文件会以 .bat 为后缀。许多人称之为 批处理文件,但在接下来的内容中,我将简单地称它们为 bat 文件。
你的第一个 bat 文件
让我们创建一个 bat 文件。首先,右键点击桌面以弹出上下文菜单,选择 新建 ▶ 文本文档。将文本文件重命名为类似 SaveProject.bat 的名称。文件名可以是你认为合适的任何名称,但文件名后的扩展名必须从 .txt 改为 .bat。右键点击文件,选择 重命名,输入新的名称和扩展名,然后按下 ENTER 键。弹出的提示可能会警告你更改扩展名可能会造成严重后果。其实不会;只需选择 是 确认即可。右键点击桌面上的新文件,选择 编辑(不要选择打开,我稍后会解释)。它应该会在记事本中打开供你编辑。
将清单 1-2 中的两行文本输入到你的 bat 文件中。
xcopy `C:\YourPath\`*.* `D:\YourBackUpPath\`/F /S /Y
pause
清单 1-2:你第一个 bat 文件的完整内容
将 C:\YourPath\ 更改为你想要备份的文件夹,并将 D:\YourBackUpPath\ 更改为你想要保存所有内容的文件夹。(我假设你的备份设备,例如闪存驱动器,被分配为 *D:* 盘符,但它可能是其他盘符。如果没有其他方法,至少为了测试这个功能,你可以在同一个驱动器上定义一个备份路径。)请注意,清单 1-2 中的第一行代码与我们在 清单 1-1 中输入到命令提示符的代码完全相同,但现在它后面跟着第二行,包含一个单词:pause。
在付出努力整理这些内容之后,你可以获得收益。无论何时你想在未来进行备份,只需执行这个 bat 文件。为此,你有多种选择;一种是双击桌面上的图标,另一种是右键点击 bat 文件并选择 打开。你可能会期望 打开 选项打开文件进行编辑,但它实际上是执行 bat 文件。
就这样。一个窗口将会打开,显示所有被复制的文件以及它们从哪里复制到哪里。bat 文件会保持窗口打开,直到你按下任意键关闭它。如果没有这一点,复制过程仍然会发生,但窗口可能会迅速关闭,以至于你无法确定是否成功。
警告
有时,Windows 文件关联可能没有正确分配。也就是说,Word 文档应该与 Word 关联并通过 Word 打开。同样,bat 文件应该与执行 bat 文件的 Windows 程序关联。例如,如果你的 bat 文件在记事本中打开,那么具有 .bat 扩展名的文件关联就已损坏。解决此问题的方法因操作系统而异。请在网上搜索“bat 文件关联修复”和你的操作系统,以了解如何修复该问题。
你可以在 MS-DOS 命令提示符中输入的几乎任何内容,都可以编写成 bat 文件,这样就可以轻松并重复执行。你不需要从互联网上下载任何东西。你需要的一切都已经存在于你的 Windows 计算机上。
我在上一段前加上了 几乎,因为某些命令,尤其是即将讨论的重要 for 命令,在 bat 文件和命令提示符中有稍微不同的语法。更奇怪的是,bat 文件中的命令有时可能会与在命令提示符中输入的完全相同的命令产生略微不同的输出。
本书的重点是编写 bat 文件,而不是使用命令提示符,因此本书中的所有代码示例都将适用于 bat 文件,所有输出示例也将是 bat 文件代码的输出。帮助命令,虽然尚未讨论,将解释任何语法上的差异。基于同样的原因,本书不会涉及主要在命令提示符中使用的命令。
cmd 文件
在示例 1-2 中,我们创建了一个带有——我知道我说的很明显——.bat 扩展名的批处理文件。在 Windows NT 发布时,微软引入了一个非常相似的文件,扩展名为.cmd,它同样包含批处理源代码。任何来自 bat 文件的批处理命令都可以输入到 cmd 文件中。事实上,如果你将第一个批处理文件从示例 1-2 改名为.cmd 扩展名,并以与 bat 文件相同的方式执行它,你将得到相同的结果。
在如何执行这两种类型的文件方面,存在一些技术上的差异,但从用户的角度来看,它们几乎是相同的。唯一显著的区别(我将在第二十一章中讨论)是关于返回码的设置时机和方式,即便如此,这个差异也只会在非常狭窄的情况下显现出来。
本书中几乎所有提到的 bat 文件同样适用于 cmd 文件,但出于多种原因,我会仅仅称其为 bat 文件。在程序员的日常用语中,bat 文件是常态。带有.cmd扩展名的文件通常也会被称为 bat 文件,但反过来却很少发生。单音节的bat相比其没有元音的替代词(通常被称为“see-em-dee”文件)更容易发音,而后者一旦频繁使用,就显得相当累赘。最后一个不容忽视的原因是:你手中这本书的封面艺术。如果这本书是关于 cmd 文件的,它的视觉吸引力肯定会差很多。
由于 cmd 文件较新,有人可能会认为它们在未来会得到更好的支持,且更适合用于新的开发。我无法反驳这个观点,但我仍然发现自己创建带有.bat扩展名的文件,而微软继续支持这两种格式。如果在过去三十年中,cmd 文件并没有取代 bat 文件,我不认为 bat 文件会很快消失。
编辑器推荐
我之前提到过,当你编辑第一个批处理文件时,它可能会在记事本中打开。记事本是最基础的文本文件编辑器。虽然许多人认为 Word 已经发展成了一个臃肿的庞然大物,但记事本则正好相反,缺乏任何有用的功能。它可以用来编辑一个简单的批处理文件,但在我们继续之前,我必须强调使用更好的编辑器的重要性。这里所说的编辑器,是指一个可以在窗口中打开(而非执行)批处理文件,用于阅读和修改的工具。
我个人最喜欢的是 Notepad++。初次听到这个名字时,我错误地认为它不过是一个强化版的记事本,但实际使用起来真的很令人愉悦。命令、变量、运算符、标签、注释,以及我们尚未覆盖的更多项目,都以不同的颜色显示,帮助提高可读性。大多数编辑器都有这个功能,但如果你双击一个变量,它不仅会高亮该变量,还会高亮文件中所有该变量的其他实例,这使得查找拼写错误变得更加容易,尽管由于 Notepad++ 的易用自动完成功能,拼写错误在其中并不常见。如果你输入一个变量名,比如 myVeryVerboseVariableName——首先,真丢脸,缩写是有原因的——下次你开始输入 myVe 时,Notepad++ 会巧妙地给你一个选项,通过按一个键就能插入剩下的 21 个字符。
Notepad++ 非常可配置。如果你觉得变量的橙色有点刺眼,你可以将其改为烧橙色,或者改为紫红色。它也适用于其他多种编程语言,最棒的是,它是免费的。只需访问 <wbr>notepad<wbr>-plus<wbr>-plus<wbr>.org 或在网上搜索 “Notepad++ 下载”,你就能轻松完成下载。
UltraEdit 也是一个强大的编辑器,Visual Studio Code 正快速成为多种编程语言(包括 Batch)中非常流行的编辑器。在互联网上,你还会找到其他几个编辑器,其中许多是免费的。下载两个或三个并进行试验,但无论如何,千万不要满足于记事本。
Batch 解释器
Batch 是一种脚本语言,而不是编译语言。编译语言,如 Java、Visual Basic 和 C#,是以文本形式编写的,但这些文本不能直接执行。相反,程序员通常在集成开发环境(IDE)中编写程序,通过点击一个或两个按钮运行 编译器,将代码转换为可执行文件。生成的文件完全不可读,但对计算机而言是可读的,并且经过优化以便执行。无法阅读的文本还有一个附加好处,就是能将任何专有信息隐藏起来,不被用户看到。
这段编译代码的下一个组成部分是 运行时;运行时有不同的形式,但运行时会加载到计算机上,并用于运行任何用特定语言编写的可执行文件。
作为一种脚本语言,Batch 也是以文本形式编写的,但其余的过程与编译语言大相径庭。没有编译器,也没有运行时;相反,Batch 解释器,或者说 cmd.exe 可执行文件,实际上承担了两者的角色。(它的 16 位前身是 command.com。)Batch 解释器有时被称为 命令解释器 或 命令行解释器,但通常它仅仅被简单地称为 解释器。
解释器执行包含文本的 bat 文件。缺少中间编译步骤确实有一些优势。很快,我会分享一些有趣的批处理技巧,正是因为代码没有编译,这些技巧才得以实现,但它也给编码者带来了一些挑战。当一个程序被编译时,编译器会捕捉到语法错误,甚至会给出如何改进代码的建议。例如,编译器很容易捕捉到缺少的括号,这样编码者可以在几秒钟内修复它。在 bat 文件中,缺少的字符不会被捕捉,直到解释器执行它,甚至可能在第一次执行时也不会发现。通过这种方式,解释器扮演了编译器的角色——一个非常低效的编译器。
解释器更像是一个运行时。当本章前面描述的 bat 文件被打开或执行时,就会发出一个调用给解释器,读取 bat 文件并逐行执行——或者说解释它。这意味着 bat 文件并未针对执行进行优化,且无论好坏,任何拥有记事本技术的用户都可以看到代码。此外,bat 文件可以在任何 Windows 机器上执行,因为所有这些机器都装有解释器。一个有趣的副作用是,特别长时间运行的 bat 文件的后期阶段实际上可以在先前逻辑执行时进行编码,这是一个令人印象深刻的,但并不特别实用的功能。
所有现代集成开发环境(IDE)都配有动画器或调试器,允许你逐行执行代码,分析变量,甚至可能修改它们。你可以设置执行在特定行或多行时停止。这是任何编码者都非常有用的工具,但批处理不支持动画器。bat 文件的生命周期如下:它被编写,然后被执行。就是这样。
一些脚本语言,如 JavaScript,通常可以两种方式运行——源代码可以通过解释器运行,或者使用来自多个公司的产品将其编译成可执行文件。批处理文件中并没有类似的常用方式。bat 文件只能通过解释器运行,理解解释器是什么非常重要,因为在接下来的章节中我会经常提到它。
总结
在本章中,我介绍了批处理脚本语言、bat 文件、cmd 文件和解释器。你编写并执行了一个 bat 文件(也许是你的第一个),并学习了有关编辑器的知识。
现在你已经准备好真正开始编码了。在第二章中,你将学习如何使用常用的 set 命令给变量赋值,以及如何解析或提取这些值。你还将探索一些在批处理世界中广泛使用的其他命令,为你未来编写任何 bat 文件奠定必要的基础。
第二章:2 变量和值

现在我们准备开始编码,我们将探讨变量、值和 Batch 的 set 命令,后者用于将值分配给变量。虽然这些话题如果你在其他语言中有过编码经验可能显得微不足道,但 Batch 有一些独特的怪癖,值得注意。
你将学习如何在控制台显示变量的值,以确认它已经正确设置。此外,我将介绍 命令分隔符,它允许你在一行中输入多个命令。我还会向你展示如何创建注释以及如何设置保存到你计算机上的变量,这些变量即使在关闭 bat 文件后也能继续使用。最后,你将学习如何在命令提示符下访问任何 Batch 命令的文档,这对任何使用 bat 文件的人来说都是一项有用的技能。
设置和解析变量
变量 是一个命名字段,定义了一个内存位置,用于存储稍后使用的值。许多甚至大多数语言允许并通常要求在赋值之前定义变量的特定数据类型,通常是某种文本或数字类型。但 Batch 不这么做;变量在第一次被“设置”值时就会存在,而且该值可以包含字母、数字和其他字符。之后,程序员可以选择将其视为某种数据类型,或者不做任何处理。在 第五章 和 第六章 中,我会回到如何处理数据类型的方法,但本章重点讨论将值赋给变量这一看似简单的任务。
以变量为例,程序员可能有一个字段来表示他们的心情状态;无论是出于自恋还是强迫症原因都无关紧要。该变量定义为或命名为 myMood,可能的两种值是 happy 和 sad。要将变量设置为 happy,显然可以使用 set 命令:
set myMood=happy
执行此操作后,myMood 变量将包含值 happy。
如果这个话题特别令人困惑,以下命令会清除之前的值,并用不同的值替换它:
set myMood=nonplussed
但你怎么能确定这个或任何其他变量的值呢?在 Batch 中,揭示变量的值被称为 解析变量,通常是通过将变量用百分号包围来完成的。也就是说,%myMood% 会在执行先前的 set 命令后解析为 nonplussed。现在,为了实际看到解析变量的结果,我们需要稍微偏离一下——一个非常重要的偏离。
显示变量的值
在本节中,你将学习如何快速显示计算机屏幕上已解析的变量值,但这种技巧不仅仅用于此。我们将在未来的章节中回到这一点,展示 Batch 的其他许多特性,例如将其作为测试技术,这对于任何 Batch 程序员来说都是至关重要的。
写入控制台
要在屏幕上显示一个变量的内容,我们需要两个额外的命令:echo 和 pause。为了演示,让我们创建一个小的 bat 文件。打开计算机上的一个新文件夹,可能是 *C:\Batch*,然后在其中创建一个名为 Mood.bat 的 bat 文件,文件内容为 Listing 2-1 中显示的三行。
set myMood=happy
echo My mood is %myMood%.
pause
Listing 2-1: bat 文件 Mood.bat 显示已解析的变量
如果你双击或打开 Mood.bat,bat 文件应该会执行,并且一个黑色窗口将显示白色文字。这个窗口是 DOS 窗口,或称为 控制台,在本书中我会称它为控制台。
我们已经在 Listing 2-1 的第一行讨论过 set 命令。在此上下文中,echo 命令将剩余的语句(不包括实际文本 echo 后面的空格)输出到控制台。显示的文本是 "My mood is" 和一个尾随的空格,然后是 myMood 的内容或值——也就是文本 happy——以及尾随的句点。pause 命令保持控制台打开。如果没有它,窗口会在你来得及阅读之前打开并关闭。
Listing 2-2 显示了当这个 bat 文件执行时写入控制台的所有内容,这比你预期的可能还要多。
C:\Batch>set myMood=happy
C:\Batch>echo My mood is happy.
My mood is happy.
C:\Batch>pause
Press any key to continue . . .
Listing 2-2: Mood.bat bat 文件的控制台显示
每个命令前面都有当前目录 *C:\Batch*,然后是一个大于号(>)作为分隔符。(你将在 第八章 中了解当前目录的更多内容。现在,只需将其视为正在执行的 bat 文件的路径。)
第一行显示了 set 命令的执行,第二行显示了 echo 命令的执行。第三行是执行 echo 命令后的结果——即 Listing 2-2 中描述的输出到控制台。你可以从缺少前缀文本 C:\Batch> 来判断它不是一个命令。更重要的是,%myMood% 解析为文本 happy。pause 命令也会生成输出,显示文本 Press any key to continue ... ,正如消息所示,执行会暂停,直到按下任意键,此时 bat 文件将结束,控制台也会关闭。另外,注意 set 命令不会生成任何输出,因为它只是设置一个变量的值——没有东西需要输出。
清理控制台
输出到控制台的问题之一是,命令与来自 echo 和 pause 命令的期望输出交织在一起,造成了一团乱。有关不同输出和如何管理它们的更多内容,你将在 第十二章 中学习;在这里,我们只快速看一下清理这些内容的方式。在 Mood.bat 文件的顶部执行一个 echo 命令,后跟参数 off,正如 Listing 2-1 中所示:
@echo off
set myMood=happy
echo My mood is %myMood%.
pause
echo off 命令并不是抑制后续命令的实际输出,而是抑制显示每个命令正在执行的行——即,显示当前目录的那一行。此外,在 echo 命令前加上 @ 符号,可以抑制它自身执行的内容不显示在控制台上。
与 Listing 2-2 相比,当执行修改后的 bat 文件时,控制台的显示变得更加简洁:
My mood is happy.
Press any key to continue . . .
现在我们可以轻松演示如何将 myMood 变量初始化为忧郁值,然后将其重置为快乐值:
@echo off
set myMood=gloomy
echo My mood is %myMood%.
set myMood=cheerful
echo Now my mood is %myMood%.
pause
结果显示相同的变量在不同时间解析为两个不同的值:
My mood is gloomy.
Now my mood is cheerful.
Press any key to continue . . .
随着我们的 bat 文件变得越来越复杂,echo 命令可以将输出发送到控制台以外的其他地方(有关更多细节,请参见 第十二章)。通过在命令前加上 > con,可以显式地将数据重定向到控制台:
> con echo This will always get to the "con" or console
这种技术将在后续章节中非常有用,用于演示代码片段中的操作。为了简洁起见,后面的示例中我不会包含初始的 echo off 和尾随的 pause 命令,但我建议你添加它们来清理并保持控制台开启。
set 命令的特性
设置变量通常是大多数编程语言中的简单话题,但批处理并不像大多数编程语言。所有批处理程序员都需要理解以下 set 命令的特性,以避免将来可能遇到的一些问题。
大小写敏感性
仔细检查以下两个命令。它们看起来有些不同,但功能上是等效的:
SET myMood=whimsical
set MYMOOD=whimsical
批处理命令和变量不区分大小写。这里 set 命令在一个命令中是大写的,在另一个命令中是小写的,但解释器将它们视为相同。你也可以使用 Set,而功能不受影响。为了保险起见,sET 和 SeT 也能以相同方式工作,但你得真是个反叛者才会这样编码。同样,你可以交替使用 myMood、MYMOOD 和 mymood 变量。然而,变量的值是按输入的方式存储的,因此它是区分大小写的。如果变量被设置为 WHIMSICAL,它将被解析为 WHIMSICAL;同样,如果设置为 Whimsical,它将被解析为 Whimsical。
这完全是风格和个人偏好的问题。我发现许多 bat 文件中过多的内容都被大写了。大写本应让某些东西突出,但如果一切都在闪烁霓虹灯,什么也就没法突出。大多数批处理程序员将命令名称中的所有字母都大写,但在本书中,我使用小写字符来表示所有批处理命令。此外,我更喜欢使用驼峰命名法的变量。
注意
驼峰式命名文本易于阅读,即使它包含多个没有空格或其他字符分隔的单词。驼峰式命名的第一个字母可以是大写(首字母大写)或小写(首字母小写)。但要符合驼峰式命名规则,所有后续单词的第一个字母必须大写,其余部分小写。一个首字母大写变体(也叫做Pascal或大驼峰命名法)是 MyMood。对应的首字母小写变体(也叫做dromedary或小驼峰命名法)将是 myMood。想象一下,一只骆驼低着头喝水。
有效的变量字符
大多数编程语言都有严格的规则,规定了变量名中可使用的字符列表。通常,数字和 26 个字母(大小写均可)是允许的,另外还有一些特殊字符可以使用。但批处理语言不同,几乎键盘上的每个字符都是有效的变量名字符,尽管你应该避免将数字作为变量名的第一个字符。(虽然你可以设置以数字开头的变量名,但解决这些变量时会有问题,为什么会这样,我会在第三章中解释。)
一些字符是非法的,因为它们在批处理语言中有特定用途;例如,波浪号 (~)、和号 (&)、百分号 (%),以及小于号 (<) 和大于号 (>) 是保留字符,但其他一些字符会让任何不熟悉批处理语言的程序员感到惊讶。 列表 2-3 中的三个 set 命令成功地将这三个具有奇怪单字符名称的变量设置为它们各自的描述。
set ;=semicolon
set @=at
set #=hashtag
> con echo %;% %@% %#%
列表 2-3:设置具有奇怪单字符名称的变量
列表 2-3 中的 echo 命令将文本分号和井号写入控制台。
即使是以下这种怪异的做法,也能将文本“这确实有效”存储到包含美元符号、点和不匹配括号的变量中:
set var$with.oddchars}=This actually works
这个变量名展示了可能的内容,但它难以阅读,并不推荐使用。
然而,明智地使用这些字符在变量名中,可以成为一个方便的工具。例如,一组相关的变量可能都带有前导或尾部的下划线,作为它们之间关系的视觉提示;数字可以用 # 来表示,这比 nbr 更简洁,也比 no 更清晰。在本书的后面,我将利用这个有趣的特性构建包含括号的有意义的数组和哈希表。
等号两侧的空格
以下是大多数新手批处理程序员在学习其他语言时常犯的经典错误。仔细检查[列表 2-4 中显示的 set 命令。
set X = Hello
> con echo The value of X is "%X%".
列表 2-4:设置带有等号两侧空格的变量
如果你期望 echo 命令的结果是
The value of X is "Hello".
那将是一个可以理解的错误,但仍然是错误。实际结果是这样的:
The value of X is "".
空的引号意味着 X 没有被设置,或者被设置为 null,意味着根本没有任何内容,甚至没有空格。
注意
在 第一章 中,我提到过在命令提示符下,语法和输出可能与 bat 文件有所不同,这是一个典型的例子。在命令提示符下输入相同的代码,显示的是对未设置变量的尝试解析,表现得非常不同:
The value of X is "%X%".
我不会在接下来的页面中指出每个差异,因此如果你在命令提示符下看到任何异常,尝试将代码放入 bat 文件中。
现在,Listing 2-4 中的 set 命令并不复杂,显然是在将 X 设置为文本 Hello,对吧?而且它看起来与其他更现代的语言中的赋值命令很像,应该会按预期进行赋值。
这是我们的第一个 batveat(批处理警告;有关 batveat 的详细信息,请参见介绍部分)。这个问题的关键是等号前的空格。批处理解释器就像难搞的青少年一样,聪明而且无情,对自己都不好。变量名从 set 命令后的第一个非空格字符开始,到赋值运算符或等号之前的字符结束——不管这个字符是什么。因此,这里设置的变量由两个字符组成,一个 X 后跟一个空格:
set X = Hello
%X% 没有解析出任何内容,但 %X % 确实有一个值,这个值是 Hello,对吧?其实不完全是;这是我们的第二个警告。变量的值是等号后的字符串,一直到语句的末尾。因此,赋值的内容是等号后面的空格,然后是单词 Hello 中的五个字符。
让我们像这样修改 Listing 2-4 中的 echo 命令:
set X = Hello
> con echo The value of X-space is "%X %".
现在,解决带空格的变量会显示其值,而该值包含一个前导空格:
The value of X-space is " Hello".
这个方法可行,但通常变量名后带空格是一个潜在的意外。我们甚至可以在变量名中间嵌入空格,但能做某件事和做这件事是个好主意之间有很大区别。这绝不应被解读为鼓励创建晦涩的代码;这更像是一个关于在 set 命令中等号两侧空格的警告。
再次查看 Listing 2-4,我们去掉等号前后空格,回到原始的 echo 命令:
set X=Hello
> con echo The value of X is "%X%".
最后,我们得到了写入控制台的预期结果:
The value of X is "Hello".
一个更容易犯的错误是无意中在行末加上一个或两个空格。因为在编辑器中看不到空格,这很容易被忽略。(在 Notepad++ 中,进入 查看 ▶ 显示符号 ▶ 显示空格和制表符,可以将空格显示为淡点。其他优秀的编辑器也会有类似的功能。)
在本书后面你会学到,有些有效的原因会将变量的值前置或后置空格,但一定要小心不要不小心这样做。然而,我很难想到一个合法的例子,说明变量的名称后面会被附加一个或多个空格。在使用基本的 set 命令时,确保变量名和等号之间没有空格。
命令分隔符
& 符号是一个特殊字符,作为命令分隔符使用,它不像普通文本那样处理。例如,你可以将 Listing 2-3 中的三行代码串联成一行,每个命令之间用 & 字符分隔:
set ;=semicolon& set @=at& set #=hashtag
这在功能上等同于将三个命令写在三行上。
有时,这种技巧对于合并简单且相似的命令很有用,但过度使用它会使代码难以阅读。然而,我发现了命令分隔符的两个非常有用的应用。
向命令添加备注
命令分隔符的一个用途是将文本添加到一行的末尾,使其被视为注释。rem 命令将其后面的文本变成注释。通常,我们会在某些有趣的代码前面(或前几行)放置 rem 命令作为注释,但我们也可以将它与特定命令一起使用,通过命令分隔符来附加它。例如,下面的两行执行相同的逻辑:
set myMood=reflective
set myMood=reflective& rem This is a thoughtful and contemplative mood.
然而,第二行为任何阅读代码的人提供了更多的信息。
终止命令
命令分隔符的第二个用途是明确地终止命令,以便编码人员能够清楚地区分是否有多余的空格。以下命令是在将变量设置为空值、一个空格还是多个空格?
set myMood=
按照现在的写法,不可能告诉(除非你让编辑器显示空格作为可见字符)。
如果你没有阅读前面的讨论,下面的命令可能会让你觉得它正在将变量设置为一个 & 符号:
set myMood=&
但这个命令明确地告诉读者,变量被设置为空值,因为该语句在等号操作符后立即被 & 符号终止。
同样,编码人员可能希望变量具有特定长度(你将在 第二十二章 中看到这个应用,特别是在格式化报告时)。下面的命令将变量设置为一个 10 字节左对齐的值,包含文本 pensive 后跟三个空格:
set myMood=pensive &
如果没有 & 符号,确定文本后面跟了多少个空格(如果有的话)将非常困难。从技术上讲,& 符号并没有分隔两个命令,但它无疑终止了一个命令。
显示变量信息
set 命令还有一个有趣的用法。当没有等号时,它会输出变量的值,因此,如果 myMood 变量已经定义,你可以在 bat 文件中输入此命令:
set myMood
输出到控制台的结果可能如下所示:
myMood=hopeful
如果在 set 命令后仅输入变量名的第一部分,则所有以该文本开头的变量将会被显示。因此,以下命令可能会输出比 myMood 变量的值更多的内容:
set myM
也许,真的也许,它会输出如下内容:
Mymar=A Genus of Fairyflies
myMood=hopeful
虽然不太可能设置这样的变量,但如果它存在,并且如果这些是以 mym 开头的唯一两个变量,那么输出的结果将是这些变量的值。请注意,该命令会查找所有变量,忽略大小写。同时还要注意,变量值中包含了嵌入空格的示例。
在看到此技巧在完整变量名和部分变量名中都能正常工作之后,我们可以将其扩展到完全没有变量名的情况:
set
这个没有参数的命令会生成所有活动变量的列表,这些变量是在 bat 文件启动时加载的,以及来自 bat 文件本身的任何添加和修改。
持久设置变量
set 命令本质上是临时的。它在特定的批处理流中定义变量,直到变量被重新赋值或脚本终止,此时通过 set 命令设置的所有变量都会消失在记忆中。但有时我们希望一个变量能够供其他进程或计算机上的其他 bat 文件访问——甚至在原始 bat 文件终止后很久,甚至在计算机关机和重启后也能访问。我们需要的是一种极限版的 set 命令,或者叫做 set Xtreme 命令。名字起得很恰当,我们有了专为此目的设计的 setx 命令。(说实话,我不知道这个命令名字的来源,但这是我在聚会上讲的一个故事。在引言中你已经被提醒过不要邀请我这种人去参加派对。)
合乎逻辑的假设是 set 和 setx 命令的语法应该是相同的。你还能期待什么呢,除了变量、等号和一个值?这有点令人困惑,但事实并非如此。setx 命令不需要等号。相反,变量名和值之间由空格或多个空格分隔,如下所示:
setx myMood puzzled
在讨论 set 命令时,我们明确看到变量名和值确实可以包含空格。这显然会在设置变量的命令中造成困扰,因为空格是命令的分隔符。但将变量名和/或值用双引号括起来,可以轻松解决这个问题。这个命令创建了一个有两个词的变量名,并将一个包含两个词的值赋给它:
setx "my mood" "cautiously optimistic"
要测试这个,首先在一个 bat 文件中执行上面的命令,然后在另一个 bat 文件中或直接在命令提示符下执行以下命令:
set my m
要查看效果,你必须在 setx 命令完成后启动第二个 bat 文件或打开命令提示符,因为当会话开始时,解释器会加载计算机现有的变量。
所有以我的 m 开头的变量(不区分大小写,包括嵌入的空格)都会被显示,包括变量 my mood。除非该变量被其他进程重新赋值,例如未来的 setx 命令,否则它会在计算机可操作时一直存在,直到被改变。
setx 命令是一个很棒的工具,可以帮助开发已编译的代码。当开发中的某个程序最终在生产环境中运行时,显然它会在不同的机器上运行,并且有自己的环境变量。在启用该程序时,一些 IDE 有很好的机制来模拟这些环境变量和文件连接设置;而其他 IDE 遗憾地没有。我见过一些不太优雅的解决方案来绕过这个缺陷,但一个好的解决方案是实际上在开发机器上设置所有需要的值,然后再启用程序。
对于用某些语言开发的每个程序,我会创建一个包含一系列 setx 命令的 bat 文件,每个变量都需要被持久化设置。执行 bat 文件后,我可以启用程序,它会在后续的生产环境中找到我预期的所有环境变量。如果我想启用另一个程序,我可以首先快速且轻松地运行与其关联的 bat 文件。如果在操作完成后,我希望恢复某些变量的先前状态,我也可以为此创建一个 bat 文件。(某些集成开发环境(IDE)在打开时只会存储一次环境中的所有变量。如果你的 IDE 是这种行为,确保在打开 IDE 之前运行 bat 文件。)
命令行帮助
本章将介绍的最后一个批处理命令是记录我已经讨论过的命令以及将来会介绍的所有其他命令的命令。help 命令接受另一个命令作为参数,并返回关于该命令的大量信息,从简要的功能描述和一般语法开始。
我将使用 set 命令进行演示,因为它在本章中起着核心作用。要调用 help 命令,请在命令提示符下输入它(在 Windows 开始菜单中输入 CMD 然后按回车)。接着,要获取更多关于 set 命令的详细信息,请输入以下内容:
**help set**
解释器生成了关于 set 命令的过多信息,无法在此完全显示,但以下是前几行内容:
Displays, sets, or removes cmd.exe environment variables.
SET [variable=[string]]
variable Specifies the environment-variable name.
string Specifies a series of characters to assign to the variable.
命令的简要描述后紧接着是其一般语法,显然语法的开头是命令名本身。所有方括号中的文本(也称为硬括号)是可选的。方括号包围的文本,[variable=[string]],表示命令可以使用或不使用括号内的文本。请记住,未带参数的 set 命令会返回所有活动变量的列表。嵌套的方括号进一步表明,string 也是可选的——也就是说,variable 可以设置为空值。
一些命令的帮助信息中提供了该命令的使用示例、附加说明和可用选项列表。选项是指为命令分配的设置或调整,用于开启或关闭某些附加功能。它们也叫做开关;事实上,帮助命令令人沮丧地将这两个术语交替使用。为了保持一致性,我将仅使用更常见的术语选项,但如果有人提到批处理命令开关,他们指的其实是选项。
选项通常由一个斜杠和一个字母组成,但你最终会遇到一些更复杂的选项。在命令提示符下滚动显示 set 命令的帮助信息,可以看到两个有趣且有用的选项。/A 选项允许命令执行算术运算(第六章)。/P 或 prompt 选项用于通过用户输入的数据设置变量(第十五章)。
在第一章中,你可能遇到了第一个批处理文件,其中包含了一个用于复制文件的命令。我不会在第七章中详细讲解 xcopy 命令,但在前一章中,它使用了三个选项(/F、/S 和 /Y)。这些选项具体的作用暂时不重要——但它们能够开启或关闭某些功能,并且这些选项在帮助命令中有文档说明。
一些命令有许多选项,其他命令则只有少数选项,甚至有些命令没有选项。在介绍命令时,我会详细讲解我认为重要且实用的选项,但你还需要使用帮助命令来查找更完整的列表。然而,一些未记录的选项无法通过帮助命令找到,要发现这些隐藏的宝藏,请访问 <wbr>ss64<wbr>.com<wbr>/nt<wbr>/ 或其他相关资源。
我建议在第一次使用某个特定命令时,或者作为可用选项的提示时使用帮助命令。尝试将其用于本章中提到的任何其他命令,甚至是帮助命令本身。是的,在命令提示符下输入此命令
**help help**
显示有关帮助命令的文档。
注意
在任何批处理命令后跟上 /? 将检索相同的信息。也就是说,输入 set /? 的效果与 help set相同。
总结
批处理编程可能很复杂,即使是看似简单的设置变量也可能有一些细微差别。在本章中,我详细介绍了 set 命令及其特殊之处,并将其与 setx 命令进行了比较,后者设置的变量是持久化的。你还学习了如何解析变量并将其显示在控制台上。现在你可以向批处理文件中添加注释,使用命令分隔符来实现多种功能,最重要的是,能够快速访问任何命令的文档。
在下一章,我们将进一步探讨变量,特别是变量的作用域。我们将研究如何定义变量在何时何地拥有特定的值,以及如何启用强大的延迟扩展功能。作为预告,我将告诉你一个秘密:一个变量的值可以是另一个变量的名称,而这个变量本身有自己的值。
第三章:3 范围和延迟扩展

在上一章中,你学习了变量的概念、如何设置它们以及如何解析它们的值。在本章中,我将重点介绍 setlocal 命令,这是批处理的一些重要且不同特性的核心,它改变了你处理变量的时机、地点和方式。首先,它定义了范围:这些变量在哪里、何时可以访问和操作。其次,它启用了一个叫做延迟扩展的功能,它改变了变量的解析方式,其中一个结果是允许你将一个变量存储在另一个变量中。
所有语言都以某种方式处理范围,但延迟扩展,或者类似的功能,要少得多,你会看到一些出乎意料的用途。最后,setlocal 命令启用了命令扩展,这是一个尴尬的术语,指的是为许多其他 Batch 命令启用的一堆附加功能。
范围
范围定义了一个变量的生命周期。全局变量可以在任何地方设置、解析、删除和修改,这对于大多数简单的批处理文件来说是有效的。局部变量在一个有限的生命周期内创建,旨在仅在某个代码段中可访问,在该段中它是在范围内的。如果在其他地方修改变量而无法识别,那么该变量就超出范围了。
在 Batch 中,setlocal 命令开始了一个代码段,在该段中变量是可访问的,而 endlocal 命令结束该代码段,使这些变量超出范围。在两个命令之间定义或操作的所有内容在该空间内有效,但在执行 endlocal 命令后,这些变量会恢复为先前的状态。
为了演示,以下代码将变量的状态输出到控制台,既在 setlocal 命令的范围内,也在范围外。一个变量仅在 setlocal 的范围内定义,一个仅在范围外定义,另一个则在范围内外都定义。在 echo 命令的右侧,我包含了备注,显示了结果,特别是解析后的变量,输出到控制台:
❶ set inAndOut=OUT
set outer=OUT
❷ setlocal
❸ set inAndOut=IN
set inner=IN
❹ > con echo Inside Scope: &rem Inside Scope:
> con echo Outer Variable = %outer% &rem Outer Variable = OUT
> con echo Inner Variable = %inner% &rem Inner Variable = IN
> con echo In/Out Variable = %inAndOut% &rem In/Out Variable = IN
❺ endlocal
❻ > con echo Outside Scope: &rem Outside Scope:
> con echo Outer Variable = %outer% &rem Outer Variable = OUT
> con echo Inner Variable = %inner% &rem Inner Variable =
> con echo In/Out Variable = %inAndOut% &rem In/Out Variable = OUT
这里有很多内容需要解释。我们先来看定义的第一个变量:在 setlocal ❷ 执行之前,inAndOut 被设置为 OUT ❶,这意味着它是在命令的范围外设置的。在执行 setlocal 后,同一个变量被设置为 IN ❸,此时它在范围内。当首次查询 inAndOut 时,它解析为 IN ❹,因为它在范围内。但在执行 endlocal ❺ 后,它就超出了范围并恢复为先前的状态,即 OUT ❻。(顺便提一下,IN-N-OUT 始终是美味的。)
现在考虑一下内部变量,它只在范围内定义一次。也就是说,在执行 setlocal ❷ 后,它被设置为 IN ❸。然后在执行 endlocal ❺ 之前,该变量解析为 IN ❹ 的值,但有趣的地方在这里;在执行 endlocal ❺ 后,它恢复为先前未定义的状态——也就是 null 或空 ❻。
最终的变量是 outer,它也只定义了一次,但当它超出作用域时,它在 setlocal ❷ 执行前被设置为 OUT ❶。正如你所料,在 endlocal ❺ 执行时,当它超出作用域时,变量的值仍然是 OUT ❻。但如你所不料,它的值在 setlocal ❷ 的作用域内也可以使用,因为它的值在 endlocal ❺ 执行前也是 OUT ❹。
这个例子表明,setlocal 命令不会阻止我们使用已经在作用域内的变量。到目前为止存在的一切仍然可用。它的作用是:setlocal 执行时会对环境进行快照,并在 endlocal 执行时返回该环境快照。
使用 setlocal 和 endlocal 命令定义作用域只有一个用途,但它非常重要:隐藏或分隔代码中的变量,以防止冲突。默认情况下,Batch 变量是全局的;在一个 bat 文件中设置的变量可以在被调用的 bat 文件中解析或重置,甚至在被调用的内部例程中也是如此。默认情况下,许多其他编程语言使用相反的方法,限制在被调用的程序和例程内部使用的变量的作用域。有时,全局变量完全没问题,但在其他情况下,限制作用域是更好的选择。定义作用域的能力使你能够为你的应用选择最合适的方式。
如果你正在编写一个将被许多其他进程调用的工具 bat 文件,你可能不知道调用进程正在使用哪些变量。在 bat 文件的顶部放置一个 setlocal,并在末尾或接近末尾放置一个 endlocal,可以定义并限制作用域。这样做的结果是,如果你恰好使用了与调用 bat 文件相同的变量名,你不会覆盖它的变量,这样调用者可以放心地调用你的 bat 文件,确保没有不良副作用。对于被调用的内部例程,通常也会采取这种做法。(在第十章中,我们将研究如何调用内部例程和其他 bat 文件。)
定义作用域提出了一个有趣的问题。如果调用一个工具 bat 文件来执行特定任务,那么很有可能这个任务的部分内容是设置并返回某个变量。有一种方法可以让变量在执行 endlocal 命令后仍然生效,我将在第十六章中分享这个方法。
延迟扩展
setlocal 命令是一个多功能工具。除了定义作用域外,当与非常具体的参数一起使用时,它还启用了延迟扩展:
setlocal EnableDelayedExpansion
恰如其分,参数是完全展开的,没有任何缩写的迹象。
延迟扩展实现了两轮变量解析:初始解析和延迟解析或扩展。当解释器执行 bat 文件时,它会逐行处理代码,首先读取或解析一行,然后执行该行。初始解析发生在解释器解析这一行时,而延迟扩展发生在它执行这一行时。
这个特性允许一些在大多数编程语言中无法实现的有趣行为。例如,你可以将一个变量的值视为变量本身——或者它的值可以作为另一个变量名的一部分。在 列表 3-1 中,Toyota 既是变量名,也是值;这不是巧合。
setlocal EnableDelayedExpansion
set Car=Toyota
set Toyota=Prius
列表 3-1:设置启用延迟扩展的 Car 和 Toyota
首先,我们需要使用 setlocal 命令并提供启用延迟扩展的参数。接下来,我们将 Car 设置为汽车的品牌,这里是 Toyota。但 Toyota 生产多个车型,如果我们想捕获特定的车型,可以将定义为 Toyota 的变量设置为 Prius 的值。
值与变量
如前所述,Toyota 既是一个值,也是一个变量。它是 Car 变量的值,同时也是一个包含 Prius 值的变量。现在,我们可以执行三条语句,将三个变量输出到控制台,如 列表 3-2 所示。
> con echo Car = %Car%
> con echo Car Again = !Car!
> con echo Delayed Expansion = !%Car%!
列表 3-2:通过三种不同方式解析 Car
下面是 列表 3-2 生成的输出:
Car = Toyota
Car Again = Toyota
Delayed Expansion = Prius
Car 的第一次解析现在显得相当平常。将变量用百分号包围(%)会解析为它的值 Toyota。第二条命令引入了新东西:使用感叹号(!)作为分隔符来解析变量 !Car!,而不是百分号。被感叹号包围的变量同样会解析为 Toyota,但为什么要使用两个不同的符号来完成相同的功能呢?答案将在我们查看最后一条命令后显现。
第三种解析方法真正展示了延迟扩展的强大功能。变量被百分号包围,接着又被感叹号包围。解释器首先将 %Car% 解析为 Toyota。请确保你坐好,接下来的部分可能让你吃惊:这个值现在被感叹号包围,这导致它再次被解析,因此 !Toyota! 变成了 Prius。将所有内容组合在一起,变量被解析如下:
!%Car%! → !Toyota! → Prius
为了回答关于两个不同符号执行相同功能的问题,解释器需要这两个符号来完成解析,因为现在我们有了两轮解析:百分号用于内层解析,而感叹号用于外层解析。(我们能不能用两组双百分号将变量包裹起来?不行,for 命令的语法对双百分号有特定用途,你将在 第十七章 中学习到这个内容。)
演示延迟扩展如何影响代码的最佳方式是运行相同的代码,但不启用延迟扩展。如果我们从列表 3-1 中移除 setlocal,则列表 3-2 的结果是:
Car = Toyota
Car Again = !Car!
Delayed Expansion = !Toyota!
如果没有延迟扩展,感叹号将被视为普通文本,对批处理没有任何意义。!Car! 变量根本没有被解析;解释器甚至不会把这三个字母当作一个变量。!%Car%! 变量经历了一轮变量解析,但感叹号只是陪衬。
在第二章中,我巧妙地避开了一个问题,提到过变量名不应该以数字开头。从技术上讲,你可以设置这样的变量,但你无法通过百分号解析它;只能通过感叹号,并且必须启用延迟扩展。处理这个小怪癖的最佳方法是避免让变量名以数字开头。
现在我们有一个变量,它可以解析为一个值,这个值再次被解析为另一个值。这通常在那些现代编译语言中难以做到,或者根本做不到。说实话,尽管整个词既是变量又是值听起来很酷,但在现实世界中并不常用,但部分变量名有很多应用场景。
部分变量名
当解析值仅作为变量名的一部分使用时,这种技巧变得更加有趣和实用。为了演示,考虑以下设置命令,它们定义了五个城市的标志性美食:
set foodNash=Hot Chicken
set foodNYC=Thin Crust Pizza
set foodChic=Deep Dish Pizza
set foodNO=Muffuletta Sandwich
set foodSTL=Frozen Custard
每个变量名是由食物和城市的常用缩写连接而成,并被设置为该城市著名的菜肴。这里只展示了五个变量,但你可以定义任意数量的变量。
以下一组变量使用了五个城市的相同缩写,每个缩写后附加了 "Full",并被赋予该城市的全名:
set NashFull=Nashville
set NYCFull=New York City
set ChicFull=Chicago
set NOFull=New Orleans
set STLFull=St Louis
现在,考虑这个带有两种延迟扩展示例的 echo 命令:
> con echo The best !food%city%! can be found only in !%city%Full!.
如果 city 被设置为 NO 并启用了延迟扩展,那么这个命令会将以下内容写入控制台:
The best Muffuletta Sandwich can be found only in New Orleans.
为了理解这一过程,让我们先看看 !food%city%! 变量。内部变量 city 和它的百分号符号被解析为 NO,从而揭示了 foodNO 变量。接着,感叹号定界符将其解析为最美味的三明治;不,这不是一份华丽的火腿芝士三明治。总结一下:
!food%city%! → !foodNO! → 穆夫尔塔三明治
同样,城市的全名也会通过两步解析。这里唯一的区别是,变量名中的硬编码部分位于要解析的部分之后:
!%city%Full! → !NOFull! → 新奥尔良
echo 命令对不同的城市值表现不同,这一点很重要。它会根据变量设置为 NYC、Nash、Chic 和 STL 时,依次向控制台输出以下四句:
The best Thin Crust Pizza can be found only in New York City.
The best Hot Chicken can be found only in Nashville.
The best Deep Dish Pizza can be found only in Chicago.
The best Frozen Custard can be found only in St Louis.
我在这一节开始时提到过,将解析后的值作为变量名的一部分更为有用。这个例子是为了教学目的,但你可以很容易地将这种技术扩展到更实际的应用中。在专业领域中,与城市中心的美食领域不同,你可以创建一组变量,根据不同地点定义文件传输路径,例如 pathNYC、pathNash 和 pathSTL。然后,一条命令就可以利用相同的延迟扩展技术,将文件传输到多个目的地中的一个。(我将在第五章中再次使用这种技术,讨论子字符串操作。)
创意程序员可以几乎无限制地使用延迟扩展,我们将在第二十九章中探讨数组和哈希表时,深入了解一些这些应用。第二部分中的 for 命令(见第 II 部分)将大大依赖延迟扩展,或许它最有趣的应用将在第十六章中展示,在那里一个变量将能够同时保存两个值。
命令扩展
setlocal 命令也接受一个参数,用于开启命令扩展。与延迟扩展不同,命令扩展默认应处于激活状态,但你可以通过以下命令显式启用它们:
setlocal EnableExtensions
启用命令扩展解锁了大量额外的功能和可用选项,适用于多个批处理命令。例如,for 命令对于任何批处理程序员来说都是不可或缺的。我们尚未讨论它,但批处理有一种变体,当命令扩展禁用时使用。启用命令扩展后,它变成了一匹动力十足的工作马,至少有 10 种形式。即使是第二章中讨论的 set 命令(通常不被认为是动态或有趣的命令)也有了这个设置后,附加的功能和可用选项。具体功能因命令而异,你可以通过命令提示符上的 help 命令(在第二章中也有介绍)获取它们的详细信息。
为了演示通过启用命令扩展解锁的额外功能,返回命令提示符并输入上一章中的相同命令,以获取关于 set 命令的文档:
**help set**
在简短的几行文字后,解释当命令扩展未启用时命令的作用,解释器将显示以下行:
If Command Extensions are enabled SET changes as follows:
以下是所有已解锁的扩展功能。信息量太大,无法全部展示,但在这个小样本中,展示了两个先前无法使用的选项:
Two new switches have been added to the SET command:
SET /A expression
SET /P variable=[promptString]
我在第二章中提到了这些选项,但没有提到命令扩展会启用它们。帮助命令在启用命令扩展时,比在禁用时提供更多的 set 命令功能信息,许多其他命令也同样如此。随着我介绍更多的命令,我鼓励你通过帮助命令进一步调查它们,以查看更多的用途和选项,并了解命令扩展开启时会启用哪些功能。
关于 setlocal 和 endlocal 的最终想法
在编写 bat 文件的二十年中,我对使用 setlocal 和 endlocal 命令有一些强烈的看法,我并不忌讳分享它们。每个我编写的高层 bat 文件,在代码的最前面或靠近最前面的位置都有这个命令:
setlocal EnableExtensions EnableDelayedExpansion
我将高层 bat 文件定义为没有从其他 bat 文件调用的 bat 文件。我很少遇到不希望启用命令扩展和延迟扩展的情况。这些额外的功能几乎没有成本。就像你可以把一辆丰田变成一辆兰博基尼,而没有像成本和油耗这样的问题。但在那种罕见的情况下,你可以通过 DisableExtensions 和 DisableDelayedExpansion 参数禁用这些功能。
此外,每当我编写可能对其他代码产生不良影响的逻辑时,我会在该逻辑前加上一个没有参数的简单 setlocal 命令,并用相应的 endlocal 命令结束它。别担心;延迟扩展仍然是从原始的 setlocal 命令中启用的。你甚至可以嵌套多个 setlocal 和 endlocal 命令,在子部分内创建具有定义范围的代码子区块,但不能超过 32 层深。我从未遇到过接近这个限制的情况,但如果你遇到,可以在被调用的例程或其他 bat 文件中进一步嵌套。(关于这些调用是如何执行的,我会在第十章中讲到。)
为了完整性起见,最好让原始的 setlocal 命令在 bat 文件的末尾有一个相应的 endlocal 命令,但如果省略,解释器会在退出高层 bat 文件前执行一个隐式的 endlocal。
重要的是,这本书是在假设启用了命令扩展和延迟扩展的前提下编写的。通常情况下,我不会让你感到无聊去了解在这些设置下解锁了哪些功能,哪些没有。如果你在测试中发现书中的示例无法正常工作,请确保你在执行该命令时使用了两个启用参数。
注意
我只有一个例外,就是关于所有高级 bat 文件都以之前提到的特定 setlocal 命令开始的规则,这个例外出现在本书中。在后面的章节中,我将提供一些非常简短的 bat 文件示例,可能只有两三行。这些简单的示例可能不需要这个命令,它的使用可能会把注意力从当前话题中分散开。在这些情况下,我不会包含该命令,但请理解,它本来可以并且应该存在。
总结
本章的主要内容是 setlocal 命令,它定义作用域并启用命令扩展。最重要的是,它启用了延迟扩展,为定义和使用变量开辟了广阔的可能性。
启用延迟扩展后,你看到如何仅通过一个命令,根据定义城市的变量值,输出五个句子中的一个。但如果禁用了延迟扩展,你可能不得不通过五个 if 命令来查询该变量。在本章中的示例中,这将是一个不优雅的解决方案,但一般来说,if 命令是任何语言中重要的工作马,Batch 也不例外。在下一章,我将详细讨论它——而且由于这是 Batch——还会谈到它的一些特性。
第四章:4 条件执行

if 命令,几乎所有编程语言中都有,是在条件为真时执行一行或多行代码,而在该条件为假时执行不同的代码块。
基本概念很简单,但在 Batch 中,条件子句,或评估为真或假的实体,与其他语言中的类似子句有很大不同。大多数比较运算符在 Batch 中是独有的,在本章中你将学习如何使用语法来确定路径或文件是否存在,以及变量是否已赋值。了解评估返回码的不同技术也很重要。
此外,你还将学习如何有效管理需要评估多个条件的情况,以及如何避免一些常见的障碍。编写一个大部分时间都能正常工作的 if 命令并不难,但在某些数据条件下,它可能会中断或无法按预期执行。
基本的 if 命令
在最基本的形式中,if 命令会在条件为真时执行一行或多行代码。我将展示如何扩展此命令,在相同条件为假时执行不同的代码,但我们先从其基本结构开始。
几乎每个 Batch 命令实现都以命令名称本身开始,通常后跟参数和/或选项。例如,set 命令总是以这三个字母开始。通常,它后面跟着一个由变量名、等号和一个值组成的参数,但在第二章中,你已经学到它也可以在没有任何参数或选项的情况下工作。(这样的命令会输出活动变量的列表。)
if 命令是独特的。它也以命令名称开始,但相似之处止于此;它可以跨越多行,并且有两个主要组件。以下是一般形式,你可以用代码替换斜体部分的文本:
if `conditional clause` (
`true code block`
)
条件子句是一个会计算为真或假的表达式。如果它为真,解释器会执行真代码块中的命令;如果它为假,则不会执行这些代码。在第十六章中,我会更深入地讨论代码块,但目前为止,代码块只是放在括号之间的一行或多行代码。
使用这种语法时,左括号不仅必须紧跟在条件子句后面,而且还必须位于同一行。其他语言可能允许(甚至鼓励)你将左括号放在下一行,并与右括号对齐,但在 Batch 中这是不允许的。然而,良好的格式要求右括号应与 if 命令的开头对齐,而中间的命令需要缩进。我的惯例是使用三个空格缩进,但任何数量的缩进都可以。以下是一个有效的示例:
if "%today%" equ "07/04/2026" (
set event=sestercentennial
)
条件语句 "%today%" equ "07/04/2026" 正在寻找一个已解析的变量和一些硬编码文本之间的相等性。这个条件语句相对简单,但我很快会展示使用不同的比较运算符、关键字甚至选项的更为复杂的语句。许多编程语言将条件语句放在括号内;无论好坏,在 Batch 中,条件语句是独立存在的,括号用来包围即将出现的代码块。
如果条件语句为真,将执行代码块中的命令(在这个例子中是 set 命令),导致 event 被设置为一个表示四分之一千年庆典的术语。
以下更加紧凑的单行格式与之前的例子功能等效:
if "%today%" equ "07/04/2026" (set event=sestercentennial)
使用单行时不再需要括号。以下代码在功能上与前面的两个例子等效:
if "%today%" equ "07/04/2026" set event=sestercentennial
从技术上讲,由于缺少括号,set 命令不再位于代码块中。没有一个简洁的术语,它现在仅仅是当条件语句为真时执行的命令。
在前面的两个例子中,我在条件语句后留下了两个空格,而且我通常会留下超过两个空格。从语法上讲,这并不是必须的,但由于没有明确区分条件语句和后续内容,稍作分隔可以提高可读性。
你甚至可以在单行中执行多个命令,通过 & 命令分隔符,如第二章中所讨论:
if "%today%" equ "07/04/2026" set event=sestercentennial& set code=ugly
如果条件语句为真,将执行一个额外的 set 命令,这个命令恰好对代码进行自我审查。我偶尔见过这种技巧,通常是在设置错误代码和错误信息时使用,但在这种情况下,使用单行代码会让代码难以理解。
如果执行的逻辑中有任何稍微有趣的地方,请使用多行代码:
if "%today%" equ "07/04/2026" (
set event=sestercentennial
set code=elegant
)
“优雅”在这里无疑是个过誉的说法,但我希望你同意,这种技巧使得代码更加易读。只需一眼,读者就能知道,如果条件为真,两个变量将被设置。
条件语句
前一节中的例子都使用了简单的条件语句,但该语句可以更加动态,采取许多不同的形式,并使用不同的运算符和关键字。
比较运算符
一个比较运算符,顾名思义,用于比较两个操作数是否相等,或者一个是否大于另一个。你可能已经猜到,前面例子中的 equ 运算符表示相等,没错。
这是 Batch 比较运算符的完整列表:
equ 或 == 相等
neq 不相等
lss 小于
leq 小于或等于
gtr 大于
geq 大于或等于
你可以选择两个功能等效的替代选项作为等号运算符。为了与用于赋值的单个等号区分开来,例如在 set 命令中,Batch 使用双等号作为比较运算符。我偏好的语法是 equ 运算符,因为它看起来与其他的类似,但有些程序员因为相反的原因更喜欢使用 == 运算符。
neq 运算符会在比较的两个操作数不相等时将条件子句评估为 true。最后四个运算符用于判断哪个操作数大于或小于另一个。例如,假设 age 被设置为一个数字值,以下单行代码块仅在变量设置为大于 12 的值时执行:
if %age% gtr 12 (
> con echo Adult Movie Theater Ticket Required
)
你可能会尝试将 %age% > 12 作为条件子句,但大于符号在 Batch 中已经有一个定义的用途;事实上,它在此代码块中用于向控制台写一条短消息。因此,你必须使用三个字符的字母代码 gtr 作为运算符。同样,本节列出的运算符用于大于或等于、小于以及小于或等于比较。
不太直观的是,这些运算符也适用于字母数字值。所有数字小于所有字母;a 小于 A;A 小于 b;b 小于 B;以此类推。这不会结束更大的争论,但至少在 Batch 的世界里,Picard 大于 Kirk。
条件子句关键字
你会在 if 命令的帮助文档中找到以下不可或缺的关键字,但不要搞错了;这些关键字是特定于条件子句的:
exist exist 关键字检查路径或文件是否存在,如果找到则返回 true。你可以将路径或文件硬编码,或者为了灵活性,你可以使用包含潜在路径或文件的变量:
if exist C:\Batch\myFile.txt set do=something
if exist %pathAndFileName% set do=something
你还可以将多个变量串联在一起以构建路径或文件名。
defined 使用 defined 关键字的以下条件子句检查变量是否已定义——也就是说,它是否解析为任何值,即使是一个空格?一个常见的错误是将百分号放在变量两边,但以下是使用该关键字的正确语法:
if defined varThatMayBeEmpty set do=something
这与以下使用百分号解析变量的代码功能等效:
if "%varThatMayBeEmpty%" neq "" set do=something
这个关键字常用于验证期望的输入变量。如果一个或多个变量未定义,你可以采取适当的措施,可能会启动中止操作。
not not 关键字在条件子句的最前面时会否定整个条件子句。这在为变量设置默认值时非常有用,特别是在变量尚未被其他人或其他程序设置时。例如,下面的代码确保 skyColor 被设置为它通常的颜色:
if not defined skyColor set skyColor=Blue
你可以将 not 关键字与 exist 关键字结合使用,以判断特定文件是否不存在。掌握了这一点,你就可以创建文件、发起中止操作,或者根据你的应用需求做其他事情。一些程序员会将 not 关键字与 equ 运算符一起使用,但我认为这样做最多只是勉强可行,我更倾向于单独使用 neq 运算符。从逻辑上讲没有区别,但不论你偏好哪种方式,都要保持一致。
警告
经过二十年的批处理编码,我仍然会不自觉地在 exist 关键字末尾加上 s,尽管我不太愿意承认这一点。Notepad++ 每次都会忠实地提醒我,因为它会加粗关键字;那个多余的字符会让整个单词失去加粗效果,从而使它显得突出。没有这样的编辑器,自己写代码要小心。
不区分大小写选项
if 命令有一个选项,就像我们之前看到的关键字一样,它适用于条件语句。/i 选项使得条件语句中的相等(和不等)运算符不区分大小写。
举个例子,下面的 if 命令中,如果没有选项,条件语句只有在 myMood 完全等于 happy 时才会返回 true——这是不等式右侧的硬编码值:
if "%myMood%" equ "happy" set do=something
下面是添加了 /i 选项的相同代码:
if /i "%myMood%" equ "happy" set do=something
现在,如果变量的值为 HAPPY、Happy、happy 或该词大小写变化的其他 29 种可能组合时,条件语句就会返回 true。
注意
/i 选项可能看起来与我之前提到的其他选项以及未来提到的选项有点不同。如前所述,尽管解释器不区分大小写,我在写批处理命令时总是使用小写字母。选项也不受大小写影响。尽管如此,由于选项通常只是一个斜杠后跟一个字符,我通常会将其大写以突出显示。但根据字体的不同,大写字母 I 经常看起来像小写字母 L,因此我在使用 /i 选项时会偏离我的个人习惯,这个选项最常与 if 命令一起使用。是的,我知道这个选项与不区分大小写有关,讽刺的是这一点我并不忽视。
errorlevel 变量
在调用可执行文件或执行多个批处理命令后,返回代码会存储在 errorlevel 伪环境变量中。(第一章中复制文件的命令就是一个会设置此变量的例子。)你将在第二十一章中了解更多关于伪环境变量的内容,但现在,先把 errorlevel 看作是一个包含返回代码的变量,你不应该使用 set 命令设置它。(如果你这样做,就会破坏 errorlevel 变量。)errorlevel 变量可以像其他变量一样在 Batch 中通过 if 命令进行评估。例如,下面的命令将返回代码为 1 或更大的情况视为失败:
if %errorlevel% geq 1 set msg=FAILURE
Batch 还支持一种古老的语法,仅适用于这个特殊变量,其中百分号符号和等号运算符被省略。以下代码与前面的示例功能等效:
if errorlevel 1 set msg=FAILURE
起初,这看起来可能简化且吸引人,因为省略了内容而没有新增任何内容,但语法掩盖了一个令人惊讶的陷阱。许多 Batch 编程人员误将这个条件语句理解为检查返回码是否等于 1。毕竟,用返回码 0 测试时,确实返回 false,用返回码 1 测试时,确实返回 true。但条件语句 errorlevel 1 等同于 %errorlevel% geq 1 和 %errorlevel% gtr 0。它对所有正整数返回 true。
将这种语法与 not 关键字结合使用,效果将更加晦涩:
if not errorlevel 0 set msg=The Return Code is NEGATIVE
这看起来像是一个在返回码不等于 0 时评估为 true 的条件语句吗?实际上它是返回码大于或等于 0 的否定。%errorlevel% lss 0 条件语句功能等效,且可读性更强。
缺乏比较运算符的语法的另一个问题是,通常情况下,0 代表良好的返回码,而其他所有值,包括负值,都表示某种问题。
neq 运算符使得这个条件语句对所有非零值返回 true:
if %errorlevel% neq 0 set msg=FAILURE
你可能会遇到这种晦涩的语法,因此理解其工作原理很重要,但更重要的是不要传播它。始终使用百分号符号(或感叹号)和比较运算符来评估 errorlevel 伪环境变量。
if...else 结构
编程语言中的一条不成文规则是,if 命令必须配有 else 关键字。前面提到的关键字与条件语句相关,但这个关键字则与 if 命令本身紧密相连。下面是 if...else 结构的一般形式,再次提醒,斜体文本必须替换为代码:
if `conditional clause` (
`true code block`
) else (
`false code block`
)
前两行和第三行开头的右括号与我在本章开头展示的通用形式相同。接下来是 else 关键字,紧随其后的是 false 代码块,用第二对括号括起来。这表示当条件语句评估为 false 时执行的代码。
下面是一个简单的 if...else 结构示例:
if %fahrenheit% gtr 70 (
set pants=shorts
) else (
set pants=jeans
)
如果 fahrenheit 变量大于 70,则 pants 变量设置为 shorts。否则,pants 变量设置为 jeans。总有一个代码块会被执行。
你可以将此结构压缩成一行代码:
if %fahrenheit% gtr 70 (set pants=shorts) else (set pants=jeans)
包围 false 代码块的括号在技术上是可选的,但为了可读性,建议包含括号。
除非你对阅读你代码的人不屑一顾,否则单行的 if...else 结构通常不是一个好习惯,虽然对于最简单的任务你可能会例外。
与其他语言不同,在 Batch 中,else 关键字不能单独写在一行上;它甚至不能是行的开头或结尾。为了清晰地标记这两个代码块,最好将关键字夹在一对闭合和打开的括号之间,写在同一行。
else if 构造
if...else 构造非常适合逻辑流程中只有两个分支的情况,一个用于 true,另一个用于 false。当分支超过两个时,else if 构造允许多个条件语句。清单 4-1 有三个语句和四个分支,每个条件语句对应一个分支,默认分支则在没有任何条件语句评估为 true 时执行。
if %fahrenheit% gtr 80 (
set pants=shorts
) else if %fahrenheit% gtr 60 (
set pants=light khakis
) else if %fahrenheit% gtr 32 (
set pants=jeans
) else (
set pants=lined jeans
)
清单 4-1:具有四个逻辑分支的 else if 构造
该逻辑假设 fahrenheit 被设置为描述温度的整数。如果大于 80 度,第一个 set 命令将执行。如果大于 60 度,也就是说在 61 到 80 度之间(包括 80),第二个 set 命令将执行。如果前两个条件语句为 false 且温度高于冰点,第三个 set 命令将执行。如果所有三个条件语句为 false,则温度为 32 度或更低,第四个也是最后一个 set 命令会分配一条非常暖和的裤子。
在第一个 else 关键字后面没有紧跟着左括号。相反,它后面跟着另一个 if 命令,并带有自己的条件语句,然后才是左括号。
清单 4-1 包含两个 else if 语句,但根据需要你可以编写任意数量的条件语句。解释器会执行第一个评估为 true 的条件语句对应的代码块;之后,控制跳转到整个结构的末尾,不会继续评估其他语句。
很多时候,如果没有任何条件语句为 true,你可能需要执行一个最终的代码块,也就是默认的代码块。例如,如果清单 4-1 未能设置某个变量,那么有人可能会在没有穿适当衣物的情况下离开家。最后的 else 关键字后面没有 if 命令,因此它的代码块(默认代码块)会在前面三个条件语句都未评估为 true 时执行。
在清单 4-1 中,fahrenheit 被用来判断它落在哪四个范围中的哪个范围。这是使用 else if 条件语句的常见方式,但它们并不一定要紧密关联。每个条件语句可以检查完全不同的变量,或者使用之前提到的三个关键字。例如,下面是清单 4-1 的重新构想,只有三个条件语句发生了变化:
if /i "%season%" equ "Summer" (
set pants=shorts
) else if exist C:\Batch\Spring.txt (
set pants=light khakis
) else if %celsius% gtr 0 (
set pants=jeans
) else (
set pants=lined jeans
)
第一个条件子句执行不区分大小写的相等比较,比较已解析的变量与硬编码文本。第二个子句是检查文件是否存在,第三个子句是检查 celsius 变量的值是否高于冰点。再次由于默认的代码块,这段代码保证将变量设置为四个值之一。
增强的相等性判定技巧
如果我不提到与这些条件子句相关的一个重要警告,我将感到遗憾。在本章的一些示例中,我将相等式两侧都用双引号括起来,但即使没有它们,命令通常仍然会工作。以下两个 if 命令非常相似,但功能上不等价:
if /i "%myMood%" equ "happy" set do=something
if /i %myMood% equ happy set do=something
如果 myMood 设置为 happy,则子句评估为 true;如果设置为 sad,结果为 false。无论哪种情况,它对这两个命令都有效。
这很棒,但现在假设变量没有设置,或者它被设置为 null 或某些空格。没有双引号的命令会崩溃,但它通过以下含糊的消息提示了问题(假设你没有使用 第二章 中提到的 echo off 命令):
happy was unexpected at this time.
C:\Batch> if /i equ happy set do=something
这里的第一行是错误消息,接下来是困惑解释器的内容。要理解这个错误,我们必须像解释器一样思考。一旦它看到 if 命令开始这一行,它期望接下来是有限列表中的某个项(可能是 not、exist、defined 或 /i),任何无法识别的内容都会被假定为条件语句的左侧部分。显然,它找到了 /i。假设接下来没有出现三个关键字之一,解释器现在期望的是三个特定顺序的项目:一些文本;一个运算符,如 equ、neq 或 ==;以及更多文本。如果 myMood 的值几乎是任何东西,它将被解析为第一个文本字段。解释器接着会很高兴地找到 equ 运算符,并且知道它正在处理一个相等式,它会将硬编码的 happy 解释为这个相等式的右侧。成功。
当变量解析为空或任何数量的空格时,所有的逻辑就会崩溃。解释器看到 if /i 开始语句,因此它不期望接下来看到 equ。not 关键字本来是合理的,但不是 equ。因此,它错误地认为 equ 是可能的相等式的左侧,而接下来的内容应该是运算符。但接下来是 happy 文本,而运算符列表显然不包含这个单词。正如消息所述,解释器此时不期望看到 happy。失败。
幸运的是,有两种评估可能解析为空的变量的方法。
前导点技巧
解决这个问题的一种常见技巧是,在等式的每一边加上一个点符号,或者几乎任何字符,只要它一致应用,这样解释器就一定能在等式两边找到内容:
if /i .%myMood% equ .happy set do=something
前导点技巧在变量设置为空时效果很好,因为命令会解析为 if /i . equ .happy。点符号不等于点符号加上单词,所以它被评估为 false,然后我们继续。如果变量被设置为 happy,命令将解析为 if /i .happy equ .happy,并且会找到相等性。成功。
但我不太喜欢这种技巧,因为它容易受到另一个陷阱的影响。现在假设变量被设置为一个由两个单词组成的情绪,比如“恼怒的沮丧”——从多个方面来看,这与 Batch 代码无关,实在不好处理。再次出现问题:
depressed was unexpected at this time.
C:\Batch> if /i .irritably depressed equ .happy set do=something
别灰心。解释器被嵌入的空格骗了。别告诉别人,但它真的没有那么聪明。它认为.irritably 是子句的左边,并将 depressed 视为完全意外的运算符。失败。但是,还有另一种技巧。
双引号技巧
回到最初的例子,这将我们带回到在等式两边加上双引号的情况:
if /i "%myMood%" equ "happy" set do=something
这里的双引号与前导点提供的内容类似,但并不仅仅是如此。
解释器将双引号内的所有内容视为一个整体。当带有嵌入空格的变量被解析时,解释器会看到如下内容:
if /i "irritably depressed" equ "happy"
尽管有嵌入空格,解释器将“irritably depressed”视为一个整体,或者在这种情况下视为等式的左侧,右侧为“happy”。结果是 Batch 正确地识别出这两个实体是不相等的。成功。
如果被查询的字母数字变量可能未设置,或者可能包含嵌入的空格,我几乎总是会在等式的每一边加上双引号。然而,你可能注意到我在条件子句中没有加上双引号来包围%errorlevel%。该变量总是被设置为一个数字,因此不需要引号。更重要的是,当解释器看到没有引号的数字进行比较时,它会做数值比较,意味着 000 等于 0。加上引号则会进行文本比较,“000”就不等于“0”了。
前导点与双引号
在比较字母数字值时,我通常会在等式的每一边加上双引号。当一个值为空时有效;当一个值是一个或多个空格时有效;当一个值中有嵌入的空格时有效;当一个值是更典型的非空格值时也有效。然而,我之所以使用非定语修饰词通常,是因为有一个非常细微的点。
假设有一个变量包含一个带有尾随空格的值。也许值 sad 是通过在尾部添加空格来填充的,变成了四个字符的值。这与三个字符的值 sad 相等吗?从最纯粹和最准确的角度来看,不,它们是不相等的——使用双引号方法正确地可以发现它们是不同的。但在一个不那么严格的场景下,你可能会认为这些值是相等的。
使用点号方法会找到两个值相等,因为尾随空格只是成为了等号左侧与操作符之间的一个普通空格。在这种尾随空格的狭窄情况中,点号方法更好,但它只在变量没有嵌入空格的情况下才有效。
最终分析,双引号技巧远胜于点号方法,虽然在某些特定情况下它不是。养成几乎在所有非数字比较中使用双引号的习惯。
注意
Batch 提供了一个有趣且紧凑的替代方案来代替if命令,尽管它的行为有所不同。它不是一个命令,没有任何关键字,甚至不支持条件语句。在第二十八章中,我将回到条件执行的话题,并提供更多关于它是什么的信息,而不是它不是什么。
总结
if命令在几乎所有,甚至是所有,编程语言中都是必不可少的,Batch 也不例外。在本章中,你了解了条件语句,包括其比较两个操作数的有效运算符,以及用于验证变量、路径或文件是否存在的关键字,以及当条件为真或假时会发生什么。你还学习了如何评估多个条件语句,以便有条件地执行多个逻辑分支。
如常见情况,Batch 给了你更多需要考虑的内容,因此我详细介绍了增强条件语句的有用技巧,比较了字母数字值和数字值。但是,什么样的值算作字母数字值或数字值呢?我将在接下来的两章中讨论数据类型,来回答这个问题。
第五章:5 字符串和布尔数据类型

在学习 Batch 数据类型赋值的第一件事就是,Batch 不允许赋予数据类型。这里并没有什么平等主义的底层思想,但所有 Batch 变量都是平等的。从本质上讲,保存数字、文本甚至布尔值的变量没有任何区别。然而,设置为数字的变量可以被视为数值类型,接下来我将集中讲解这些数据类型。
在本章中,在概述所有 Batch 数据类型之后,你将学习字符串和字符变量。你还将进一步探索字符串,了解如何进行子字符串提取和文本替换。布尔值并非 Batch 的创建者设计的,但我将教你如何构建和使用这个有用的数据类型。
常见数据类型
许多(如果不是大多数的话)编程语言不仅允许,而且要求在赋值或以任何方式使用变量之前,必须先声明其数据类型。不同语言之间有所不同,但这里有一个一般的数据类型列表:
字符 单个字母数字字符
字符串 零个或多个字母数字字符
整数 正数和负整数
浮点数 具有小数点的数字
布尔值 真或假
无论好坏,Batch 变量并没有显式声明。变量名第一次被解释器发现时,它就会凭空产生。这种做法确实提供了很大的灵活性,但也可能很棘手且危险。解释器会认为变量名拼写错误的实例是一个完全不同的变量,且编译器并不会捕捉到这个错误。相反,它会被当作一个新的变量,这个变量可能最终什么也没有。
变量可以被赋予一个整数,并且可以对其进行算术运算。这个变量之后可以被赋值为文本,并像字符串一样处理。这也意味着,包含字符串的变量可能不小心进行算术运算,但从好的一面看,数字可以轻松地当作字符串处理,在控制台或报告中无需任何转换。这是纯粹的数字混乱,是为虚无主义者设计的编程语言,不知为何它居然能工作。
即使你不能直接赋予数据类型,你仍然可以创建变量,并将其当作某种类型来使用,但——我必须强调这一点——每个 Batch 变量的底层结构实际上只是几字节的内存,没有明确的类型区分。
字符
字符仅仅是单个字节的文本;在 Batch 世界中,可以把它看作是一个非常短的字符串,因为它和任何其他单字符字符串的处理方式完全相同。我将简短讲解这一部分,并继续介绍字符串。
字符串
字符串是任意长度的文本,包含字母字符、数字和/或特殊字符。以下命令将 aString 变量设置为一个包含五个单词的字符串:
set aString=Awesome Batch Code Dares Excellence
包括嵌入的空格,其长度总计为 35 个字符,或者用程序员的话来说,是 35 个字节。
许多特殊字符,如美元符号和英镑符号,可以明确地包含在字符串中,但其他字符,如百分号符号,则不行,因为它们在 Batch 中有特定的用途。在第十四章中,我将讲解如何通过转义实现将所有字符包含在字符串中的方法,但现在要理解的是,当解释器在字符串中遇到感叹号时,它不会中止执行,但你可能不会看到预期的结果。例如,赋值给这个变量的值中的最后一个字符是一个感叹号:
set aString=Awesome Batch Code Dares Excellence!
> con echo A String is "%aString%"
这是 echo 命令的结果:
A String is "Awesome Batch Code Dares Excellence"
标点符号没有写入控制台,因为它没有包含在字符串变量中。
注意
如第三章中所述,我假设本书中启用了延迟扩展。这个例子很好地说明了这一点,因为如果延迟扩展被禁用,感叹号将仅仅是另一个字符,而不是用来解析变量的分隔符。这个字符会作为值的一部分被包含在内,并与其余文本一起写入控制台。能够将感叹号视为普通文本,可能是禁用延迟扩展的唯一优势。这个微不足道的优势与延迟扩展所提供的功能相比显得微不足道,这也是我推荐全书都使用它的原因。
在后续章节中,我会讨论如何将字符串和其他数据类型写入文件,但在这里我将解释如何构建、连接、提取子串和操作字符串。
构建和连接
上一个例子使用单个 set 命令将值 Awesome Batch Code Dares Excellence 赋给一个变量。以下六行执行相同的任务:
set a=Awesome
set b=Batch &
set c=Code
set d=Dares
set e= Excellence
set aString=%a% %b%%c% %d%%e%
实际上,这种方法在构建字符串时效率极低,但它很好地展示了连接的原理。
由字母表前五个字母定义的变量每个都被设置为一个单词。然后在最后一行,所有五个变量都被解析并连接起来创建一个 aString。注意结果中嵌入的四个空格:一个是来自 Batch 后面的空格,另一个是来自 Excellence 前面的空格,其他两个则是嵌入在最后的 set 命令中。
上一个例子展示了如何通过连接其他字符串来创建一个字符串,但你也可以通过附加或预置其他文本来扩展现有字符串:
set longText=This field contains a brutal run-on sentence and if its prose
set longText=%longText% were to be typed into a single line the reader would
set longText=%longText% be forced to scroll way over to the right to read what
set longText=%longText% you are reading now and then scroll way back to the
set longText=%longText% left after mercifully getting to this period.
这里一个字符串被四次附加额外文本,从而创建了一个非常长的字符串。
这种方法是我创建长字符串变量的偏好方式,但你也可以使用“续行符”或插入符号(^)完成相同的任务。当解释器遇到行尾的插入符号时,它会将下一行附加到该行:
set longText=This field contains a brutal run-on sentence and if its prose ^
were to be typed into a single line the reader would be forced to scroll way^
over to the right to read what you are reading now and then scroll way back ^
to the left after mercifully getting to this period.
在这个例子中,使用了三个插入符号来制作一个四行的 set 命令。第一行和第三行前面有一个空格,而它们的后续行从第一字节开始,这导致了单词之间的空格。为了展示一种不同的实现方式,第二个插入符号紧跟在单词 way 后面,接下来的行在下一个单词 over 前有一个空格。最终结果是一个由空格分隔的长字符串。
我不太喜欢这种技术,原因很简单,它会破坏我的缩进规则。我通常将大部分命令缩进两个或更多空格,正如 set 命令的第一行所示,但任何后续行开头的空格都被视为附加文本的一部分。这实际上意味着这些行必须左对齐。我会在第九章中深入讨论缩进规则。现在只需理解它是有效的——但不美观。
注意
我把“续行符”加上引号是因为这是一个过度简化。插入符号实际上是一个转义字符。在第十四章中,我会解释为什么这很重要,但许多批处理程序员通常称其为续行符。
子字符串
任何值得一提的语言都会支持一种子字符串函数,能够提取字符串的一部分,而 Batch 也不例外。在接下来的几个例子中,我们假设 aString 变量已经像之前那样设置好了:
set aString=Awesome Batch Code Dares Excellence
一个子字符串函数需要两个数字,即偏移量或起始位置和所需文本的长度。令人惊讶的是,Batch 使用的是更现代语言中常见的零偏移量,而不是 20 世纪语言中更常见的 1 偏移量。这意味着第一个字节的位置是 0(而不是 1),第二个字节的位置是 1,第 100 个字节的位置是 99,以此类推。
子字符串的语法有点笨重。变量使用百分号进行解析,这是常见的做法,但闭合的百分号前面会加上冒号、波浪号、偏移量、逗号,最终是长度。因此,下面的语法会返回 aString 变量的前三个字符:
set subString=%aString:~0,3%
偏移量为 0 告诉解释器从第一个字节开始,长度定义为 3,最终将文本 Awe 赋值给 subString。
以下代码从同一字符串的第一个单词中提取文本 some:
set subString=%aString:~3,4%
我们需要从第 4 个字节开始,这就是零偏移量 3。如果你觉得零偏移量有些混乱,可以将偏移量理解为子字符串之前的字节数。更明显的是,长度是 4。
这里有两个子字符串与硬编码的 "to" 和几个空格一起拼接:
set phrase=%aString:~15,3% to %aString:~8,5%
第 15 个字节是 Code 中的字母 C,因此第一个子字符串是该单词的剩余三个字节。第 8 个字节是 Batch 前的空格,因此接下来的五个字节包含整个单词。结果是一个恰当的,虽然不那么高深的,重新诠释原始字符串:ode to Batch。
如果没有定义长度,解释器将返回字符串的其余部分。为了演示,以下子字符串没有长度,也没有前导逗号。偏移量对应于 35 字节变量中倒数第一个单词前面的 25 个字节:
set subString=%aString:~25%
结果是,subString 被赋值为字符串“Excellence”,即原始字符串的最后 10 个字节。
负偏移量
注意以下示例中的负偏移量。有趣的是,这也将“Excellence”赋值给变量:
set subString=%aString:~-10%
一个负偏移量表示起始位置相对于字符串的结尾,而不是开头,这意味着-10 告诉解释器子字符串应从字符串末尾起 10 个字节的位置开始。由于没有给定长度,它将返回文本的其余部分。只要变量已填充,%aString:~-1%是检查其最后一个字节的简便方法。
这两个命令都会返回相同的子字符串:
set subString=%aString:~15,3%
set subString=%aString:~-20,3%
第一个命令的偏移量是原始字符串起始位置的 15 个字节,而第二个命令通过从 35 字节的变量末尾起始的 20 个字节来找到相同的位置。
负长度
负长度的工作方式类似。不要把它看作是一个长度;把它看作是字符串末尾不包含在子字符串中的字节数。例如,以下命令返回一个去掉首尾字节的字符串:
set subString=%aString:~1,-1%
你甚至可以将负偏移量与负长度一起使用。以下命令提取字符串的倒数第二个字节:
set subString=%aString:~-2,-1%
偏移量-2 告诉解释器从倒数第二个字节开始,长度-1 表示删除最后一个字节。
实际中的子字符串
批处理中的一个不错的特性是,如果请求的子字符串超出了字符串的长度,解释器会返回 null,而不会崩溃。因此,当遇到 35 字节字符串的命令%aString:~99,1%时,解释器不会崩溃,也不会返回空格。它只会返回一个空字符串。这是确定字符串长度的一种方便方法,避免了编译代码中常见的 null 指针异常。如果第 36 个字节为空(即"%aString:~35,1%"为""),但第 35 个字节已填充,则字符串的长度正好为 35 个字节。
然而,这种语法仅在截取已填充的字符串时有效。正如我刚才提到的,当字符串长度在 1 到 35 字节之间时,%aString:~35,1%的解析结果为 null;当然,如果字符串长度为 36 字节或更长,它会解析为第 36 个字节。但如果字符串为空或设置为 null,%aString:35,1%会解析为35,1,或冒号和后续定界符之间的所有内容。同样,由于这个警告,当尝试检查空字符串的最后一个字节时,%aString:-1%会解析为-1,而不是你可能期望的 null。
现在你已经知道如何从另一个字符串中提取字符串的任何部分,但之前的例子中,所有的偏移量和长度都是硬编码的。通常情况下,甚至大多数情况下,这两个数字会是变量。在以下示例中,偏移量和长度被定义为显而易见的命名变量,并在第三个命令中使用:
set offset=15
set length=3
set subString=!aString:~%offset%,%length%!
包围偏移量和长度的百分号首先会解析这些变量的数值。然后,感叹号发挥作用,使得 !aString:~15,3! 解析为我们熟悉的 ode,这是启用延迟扩展的又一次胜利。
完成下一章后,我将在其中讨论算术运算,你将能够计算持有整数值的变量,用作偏移量和长度来查找子字符串。
文本替换
Batch 还有一个便捷的机制,可以将字符串的全部或部分替换为其他文本。例如,假设以下变量包含这个不太合适的文件名:
set filNm=File_Name_With_Underscores.docx
如果你不喜欢这个文件名,你可以将下划线替换为短横线。在第七章中,我将介绍用于重命名文件的理想命令,但在这里我将讨论如何构建包含新文件名的变量。
文本替换语法类似于用于子字符串提取时的语法。变量和冒号仍然被百分号包围,但现在没有波浪号。冒号后面是要查找并更改的文本,接着是等号分隔符,最后是替换文本:
set newFilNm=%filNm:_=-%
每一个下划线字符(_),而不仅仅是遇到的第一个下划线,都会被替换为短横线(-),从而得到 File-Name-With-Underscores.docx。小心不要更改超过预期的文本。
看着这个文件名,也可以考虑将 Underscores 替换为 Dashes。幸运的是,Batch 不要求目标文本和替换文本的长度相同,因此这个附加命令进一步将变量的值更新为 File-Name-With-Dashes.docx:
set newFilNm=%newFilNm:underscor=Dash%
由于这两个单词都以 es 结尾,我使用单数形式的 Dash 作为替换文本,而目标文本是 underscor,后者甚至不是一个真实的单词。此外,注意到在变量的值中 Underscores 是大写的,而在替换语法中,underscor 是小写的。非常重要的一点是,Batch 执行的是不区分大小写的替换。目标文本的大小写可以是任意的,甚至是混合大小写,这对结果没有影响,但替换文本会按原样使用。因此,%newFilNm:UNDERscor=Dash% 与之前命令中的变量解析功能完全相同,但 %newFilNm:underscor=DASH% 将会导致文件名变为 File-Name-With-DASHes.docx。
这很微妙,但前两个命令展示了两种不同的赋值方法。第一个命令将修改后的 filNm 值赋给 newFilNm,而不改变 filNm。第二个命令将 newFilNm 重新赋值给它自己,以便其最终值反映两个文本替换。这两种方法为你提供了灵活性,可以选择直接在变量内修改值,或者保持两个变量,一个保存旧文本,一个保存新文本。
你还可以使用延迟扩展将目标文本 targ 和替换文本 repl 转换为变量。这里有一个例子:
set targ=Love
set repl=Hate
set aString=I Love Broccoli
set aString=!aString:%targ%=%repl%!
结果是更诚实的字符串 "I Hate Broccoli"。
文本搜索是文本替换语法的一个绝妙应用。在第二十四章中,我将对比两种判断一个字符串是否为另一个字符串一部分的方法。findstr 命令效果不错,但基于前述语法的方法执行速度要快得多。剧透:文本搜索逻辑会将搜索到的文本替换为空,然后将结果与原始文本进行比较。如果它们不同,说明文本被找到了。
布尔值
布尔值在编译语言中无处不在,它们只有两个状态:true 或 false。一旦设置,你可以单独使用它们作为 if 命令中的条件语句,评估为真或假,从而决定是否执行一段代码。Batch 并不明确支持布尔值,但通过一点巧妙的设计,你可以创建布尔值。
许多篇幅已被用来探讨“上帝是否存在?”这个问题。这不是那类书籍,但我们可以回答一个更简单的问题:“God.txt 是否存在?”在第四章中,我展示了如何使用 if 命令来判断一个文本文件是否存在:
if exist C:\Batch\God.txt (
set god=Found
) else (
set god=NotFound
)
一个变量根据某一时刻文件的状态被设置为 Found 或 NotFound。然后,可以在未来询问 god 变量,以确定 God.txt 是否在那个较早的时刻存在。它能工作,但有点笨重;布尔值将提供更优雅的解决方案。然后,你可以根据需要在代码中多次引用这个布尔值,甚至可能重置它。
设置和评估布尔值
在 Batch 中,布尔值像所有变量一样,本质上只是一些文本,但这些文本可以被评估为真或假。按照惯例,我总是将布尔变量名以小写字母 b 开头,后跟大写字母,以便使其作为布尔值脱颖而出。(一个更冗长且描述性的选项是以 bool 文字开头。)让我们复制之前示例中的逻辑,唯一的区别是将笨重的变量 god 替换为布尔值 bGod,如果找到 God.txt 则将其设置为 true,否则为 false:
if exist C:\Batch\God.txt (
set bGod=true==true
) else (
set bGod=false==x
)
在其他编程语言中,布尔值通常显式设置为 true 或 false。例如,一个有效的 Java 命令是 bGod = true;。但是,Batch 布尔值的前述设置命令看起来有点不同;特别是每个命令都有三个等号。第一个等号仅用于赋值;另外两个则是赋值的一部分。当 if 命令的条件语句为真时,我们将 bGod 设置为 truetrue;如果不是,则值为 falsex。这看起来确实有些奇怪,但现在该变量虽然技术上仍然只是文本,但可以像这样被评估为另一个 if 命令的条件语句:
if %bGod% > con echo Let us pray.
那么怎么做呢?如果 bGod 被设置为我们认为的 true,解释器将 %bGod% 解析为 if true == true。该变量包含一个等号操作符,两个相等的等号,并且两边的值是相同的。(别问操作符周围的空格,这就是解释器看到的。)将所有这些放在 if 命令后面,它将被评估为真。
然而,如果该变量被设置为我们认为的 false,那么命令将被解析为 if false == x,它比较两个明显不同的值,导致 if 命令后面的代码不被执行。
带有布尔值的 if 命令还可以与 not 子句一起使用:
if not %bGod% > con echo Live every day to the fullest.
如果文本 if not %bGod% 被解析为 if not true == true,那么评估结果是 not true 或 false。但当文本解析为双重否定 if not false == x 时,它将评估为 not false 或 true,并且文本将被写入控制台。
布尔值转换为字符串
我选择 truetrue 作为 true 的值,但 xx 或 0 == 0 也能工作,并且需要更少的击键。即使是 falsefalse 也会评估为 true,但我们不必这样做。同样,falsex 本来可以包含任何两个不同的字符串,但我选择了这些值,使得布尔值的文本 true 或 false 始终处于前沿。布尔变量的结构使你能够模仿编译代码中的另一个布尔特性——将布尔值转换为字符串。
如前所述,你可以通过简单地去掉等号后面的所有内容,将 Batch 布尔值转换为字符串 true 或 false。当我们在 第十九章中讲到 for 命令时,我将展示具体是如何实现的,但现在,下面的代码行可以截断多余的文本:
for /F "delims==" %%b in ("%bGod%") do set bStrGod=%%b
在此执行之后,针对有效的布尔值,名为 bStrGod 的 布尔字符串 变量将包含 true 或 false。
(如果布尔变量以 b 开头,那么将布尔字符串变量以 bs 开头可能是有意义的,但我选择的惯例避免了人们对我的代码充满 BS 的指责。)
总结
字符串在 Batch 中无处不在,在本章中,我详细讲解了如何构建和连接字符串。子字符串提取和文本替换是两个强大而有用的工具,尽管它们的语法较为深奥,但所有 Batch 编程人员都应该掌握。布尔值虽然不那么常见,但我希望我已经展示了这一不常用数据类型的实用性。
在下一章,我将继续讨论数据类型,深入探讨数字数据类型。我将详细介绍三种不同进制的整数和浮点数,为探索 Batch 中如何处理算术运算提供一个很好的机会。
第六章:6 整数与浮点数据类型

在第五章中,我详细介绍了字符串和布尔数据类型。在这一章中,我将转向数值数据类型,特别是整数和浮点数据类型,并对它们进行深入研究。批处理能够轻松处理整数,无论是十进制、十六进制还是八进制变体。
然而,浮点数和布尔值类似,因为批处理实际上并不显式支持它们作为数据类型。但再一次,这个限制为富有创意的批处理程序员提供了发挥想象力的机会,这正是我们在本章结束之前将要做的。
八进制案例研究
8 月 1 日,某个“零零年代”的年份:我记不清确切的年份,但对于月份和日期我是非常确定的,原因到本章结束时就会明了。
我当时还相对较新于批处理,但我知道的比许多人都多,所以一个同事找到了我,帮忙处理他一直在挣扎的任务。在这段批处理代码中,他需要根据当前日期来确定前一天的日期。对于大部分日期来说,这个任务相当简单,但当今天是月初时,情况就变得复杂了。因为每个月的天数不同;新年的第一天是一个独特的挑战;闰年每四年发生一次,除了不发生的情况。
这个初步事件发生在 2 月,可能是 3 月,这是一个有趣的小练习,我写了代码并进行了测试。像所有优秀的程序员一样,我测试了每年第一天和最后一天。我还测试了几个极端月份的第一天,特别是像一月和十二月这样的月份。我测试了不同年份的 3 月 1 日,不是因为我是在 2 月编写这段代码,而是因为闰年的特殊性。不久后,我将代码交了出去,转向了其他项目。
代码在大约六个月内运行得很好。然后在 8 月 1 日,它突然就不工作了。我不记得后续的结果是什么,但我的同事花了大量时间追踪根本原因。他最终锁定了我的批处理文件,但无法弄清楚为什么它在那天停止工作。他的老板根本不听这些解释——代码能正常运行半年,然后突然崩溃。我的同事一定做了某种更改,导致了这个问题,他被挑战去找到这个更改。
这次搜索最终浪费了他半天的工作时间,但经过大量细致的工作,他终于将问题带给了我。我打开执行日志,找到了试图查找 08/01 之前日期的逻辑结果,然后...
我抬头望向天空,举起双手,带着沙特纳式的戏剧化大声喊道:“八进制!”我有些夸张——那一刻并没有像《星际迷航 II:可汗的愤怒》中可汗将柯克舰长(由威廉·沙特纳以莎士比亚式的风格演绎)困在一颗死去的星球中心那样戏剧化,但至少对我来说那一刻是相当难忘的。
执行日志中到底有什么让我不高兴的地方?让我们来看看,但在深入研究八进制之前,我将从整数开始。
整数
我们已经使用 set 命令处理字母数字值,但它也可以通过/A 选项用于算术运算。回想一下,像这样的语句会发生什么:
set x=4+5
x 所表示的变量被设置为文本 4+5。
使用/A 选项会将其转换为算术设置命令,因此以下命令将 x 变量设置为数字 9:
set /A x=4+5
/A 选项将 set 命令转化为执行加法和其他算术运算的工具。那些先前的值显然是硬编码为数字的。
一个稍微有趣一点的例子是将变量设置为数字值,然后通过 set /A 命令将它们相加,如列表 6-1 所示。
set nbr1=4
set nbr2=5
set /A sum = nbr1 + nbr2
> con echo The sum is %sum%.
列表 6-1:通过 set /A 命令添加两个数字变量
控制台输出为“和是 9”,并且列表 6-1 展示了/A 选项显著改变了 set 命令——三次。首先也是最明显的,解锁了算术。第二,等号两边有空格,而在第二章中,我曾经大篇幅讲过空格带来的危险。为了说明这一点,这个没有使用/A 选项的命令:
set myVar = X
并没有将 myVar 设置为 X,而是将一个六字符的变量 myVar(后面跟着空格)设置为一个由空格和 X 组成的两字符值。相比之下,/A 选项使得 set 命令更像现代编程语言中的赋值操作符,因为命令中的空格不会被视为变量名或值的一部分;令人耳目一新的是,它们只是空格。
这三个命令在功能上是等价的;每个命令都将 myVar 设置为 7:
set myVar=7
set /A myVar=7
set /A myVar = 7
若不使用/A 选项,等号两边不能有空格。然而,使用/A 选项时,可以有空格,但空格不是必需的,这就是/A 选项解锁的第二个重要区别。
列表 6-1 中的第三个区别在于变量 nbr1 和 nbr2 没有被百分号包围。因此,/A 选项允许你在没有常见分隔符的情况下解析变量。为了灵活性考虑,你仍然可以使用百分号和嵌入空格,或者不使用,所以这四个语句在逻辑上是等价的:
set /A result = nbr1 + nbr2
set /A result = %nbr1% + %nbr2%
set /A result=nbr1+nbr2
set /A result=%nbr1%+%nbr2%
空格使代码更加易读,因此我不建议使用之前代码中的最后两个选项。第一个选项最简洁,但有些人已经习惯了变量周围有百分号,第二个选项或许能提供一种让人感到安稳的一致性。
让我们再来看一遍列表 6-1 中的 set /A 命令,不过这次它会在批处理文件的最开始执行:
set /A sum = nbr1 + nbr2
> con echo The sum is %sum%.
控制台输出的求和结果将是 0。由于 nbr1 和 nbr2 尚未定义,未设置的变量在数值上下文中默认视为 0,而与此不同的是,在字母数字上下文中未设置的变量默认视为 null。由于两者都未设置,算术 0 + 0 的结果为 0。
警告
允许的整数范围包括从-2,147,483,648 到 2,147,483,647(包含这两个值)。批量算术将数字存储为 32 位带符号字段,因此任何整数将属于这些 2**³² 个值。这通常不会导致问题,但由于代码不是编译型的,需要注意确保正在处理的数据符合这一限制。代码不会中止,也不会挂起;它只是无法计算出正确的值。批量算术不是宏观经济学的首选语言。
批量算术
批量算术不仅仅是简单的加法。以下列表展示了五种主要的算术运算(加法、减法、乘法、除法和取模运算)及其语法:
set /A sum = nbr1 + nbr2
set /A difference = nbr1 - nbr2
set /A product = nbr1 * nbr2
set /A quotient = nbr1 / nbr2
set /A modulo = nbr1 %% nbr2
这些运算符与其他编程语言类似,但请注意取模除法的双百分号。帮助命令显示的是单一的百分号,但正确的批量语法需要两个百分号。(实际上,取模字符只是单一的百分号,但第一个百分号实际上是转义第二个。如果现在这并不太容易理解,可以等到第十四章再深入了解,但现在就使用两个符号。)
现在我们来执行这些算术命令,但首先我们要定义两个操作数 nbr1 和 nbr2。每条语句右侧的结果作为注释显示(如前所述,&符号用于分隔两个命令,第二个命令可以是 rem 命令):
set nbr1=7
set nbr2=2
set /A sum = nbr1 + nbr2 &rem sum=9
set /A difference = nbr1 - nbr2 &rem difference=5
set /A product = nbr1 * nbr2 &rem product=14
set /A quotient = nbr1 / nbr2 &rem quotient=3
set /A modulo = nbr1 %% nbr2 &rem modulo=1
加法、减法和乘法操作没有什么意外的结果,但将 7 除以 2 的结果是 3,而不是 3.5,因为批量算术只处理整数,并且会截断结果的小数部分。将 19 除以 10 不会得到 1.9,甚至不会返回四舍五入后的值 2\。1.9 的中间结果会被截断为 1。
取模是一个有用的运算符,用于返回余数。取模n返回的值是从 0 到n - 1,因此取模 2 操作对于偶数返回 0,因为 2/2、4/2、6/2 等的结果都是整数,不会产生余数。奇数则返回 1,因为 3/2、5/2、7/2 等都有余数 1。
奇怪的是,批量算术不支持指数或幂函数,这对一些人来说是一个令人沮丧的缺陷,但对另一些人来说则是激发创意的动力。你可以创建一个例程,输入基数和指数并返回指数结果(我将在第十八章中这样做)。
增强赋值运算符
增量赋值运算符 可以简化代码,当你希望将一个数字添加到变量并将结果存储在同一变量中时。最明显的例子是一个简单的计数器,可能在每次执行 set 命令时都想将一个变量递增 1,例如:
set /A veryVerboseTallyVariable = veryVerboseTallyVariable + 1
我故意选择了一个冗长且笨重的变量名,因为无论我们这些程序员怎么努力,它们有时几乎是不可避免的。
以下语法在逻辑上等价、简洁,并且更易理解:
set /A veryVerboseTallyVariable += 1
下一个命令将 17 添加到一个更简洁命名的变量中:
set /A nbr += 17
同样,以下的 set 命令分别减去 2、乘以 2、除以 2,并执行模 2 除法:
set /A nbr -= 2
set /A nbr *= 2
set /A nbr /= 2
set /A nbr %%= 2
再次注意模除运算符的双百分号。许多经验丰富的 Batch 程序员并不知道增量赋值运算符在 Batch 中是可用的,错误地认为它们只存在于现代编程语言中,但它们确实存在,且在适当时应该使用。
运算顺序
你可以使用数学中的运算顺序规则进行更复杂的算术运算。你可能在代数预备课程中学过 PEMDAS 首字母缩略词(或者用“请原谅我亲爱的莎莉阿姨”作为记忆法),表示“括号、指数、乘法和除法、加法和减法”。在 Batch 中,我们使用 PMDAS,它发音困难,但如前所述,指数运算并不被支持(也许“请做甜点莎莉阿姨”这一记忆法能流行起来)。让我们看这个例子:
set /A nbr = 3 * (1 + 2) / 4 - 5
首先,1 和 2 被加在一起得 3,因为它们在括号内,尽管加法和减法在运算顺序中排在最后。乘法和除法具有相同的优先级,因此解释器会从左到右执行它们。表达式开头的 3 与加法中的 3 相乘,得到 9,然后 9 被 4 除,结果是 2.25。实际上,这是截断的,所以结果是 2。最后,减去 5,结果是 -3。
这个例子仅用于教学,因为直接将 nbr 设置为 -3 会更简单。实际上,会使用硬编码数字和变量的混合。例如:
set /A nbr = ((nbr1 + nbr2) * -10) / 4
根据 PMDAS 规则,这里外部的括号是多余的,但它们使得语句更易读。
增量赋值运算符也可以与更复杂的表达式一起使用。以下两个语句在逻辑上是等价的:
set /A nbr = nbr + (2 * (4 + nbr) - -5)
set /A nbr += 2 * (4 + nbr) - -5
在这两个命令中,变量 nbr 都在通过包含 nbr 的数学表达式递增,唯一的区别是第二个命令使用了增量赋值运算符。根据运算顺序,两者都会给变量加 4,然后将其翻倍,最后减去 -5(减去 -5 等同于加 5)。最终,这个表达式的结果是 nbr 增加的量。
八进制与十六进制运算
Batch 支持八进制和十六进制算术运算。这两种数制比十进制更接近计算机的思维方式,因此对程序员来说,理解它们并能够使用它们是很有帮助的。
十进制数系统是基数为 10,使用数字 0 到 9。没有代表 10 的数字;相反,有两个数字:新的位值从 1 开始,而个位数从 0 重新开始,因此是 10。与此相反,八进制数系统是基数为 8,使用数字 0 到 7。将 1 加到八进制的 7 不会得到 8,因为 8(和 9)在八进制数系统中是无意义的字符。相反,八进制数 10(发音为“一零”,因为它不是“十”)等于十进制数 8。同样,八进制数 11 等于十进制数 9,依此类推。
十六进制数系统是基数为 16,因此它和八进制面临相反的问题:它需要 16 个独特的数字,超过了大多数人类数制中使用的 10 个数字,因为我们是进化出了每只手有五个手指的结构。从 0 数到 9 后,我们有了“数字”A、B、C、D、E 和 F。十六进制的 B 等于十进制的 11,十六进制的 F 等于十进制的 15,十六进制的 10 等于十进制的 16。
Batch 可以进行八进制、十六进制和/或十进制输入的算术运算,并始终以十进制返回结果。十六进制数前面会加上 0x,而八进制数前面则单独加 0。因此,这两个变量分别被赋予八进制和十六进制的值:
set octalNbr=012
set hexadecimalNbr=0xB
无论操作数的基数是十进制、八进制还是十六进制,Batch 总是将结果存储为十进制。为了演示,首先看这个例子:
set decimal7=7
set decimal1=1
set octal7=07
set octal1=01
set /A decimal = decimal7 + decimal1
set /A octal = octal7 + octal1
数字 7 和 1 作为十进制和八进制相加。十进制的结果显然是 8。两个八进制数的和是八进制 10(“一零”,而不是十进制 10),但解释器会立即将其作为十进制 8 存储。在这个例子中,十进制和八进制表现相同,但这并不总是这样。
现在看这个例子:
set decimal11=11
set decimal2=2
set octal11=011
set octal2=02
set /A decimal = decimal11 + decimal2
set /A octal = octal11 + octal2
> con echo The decimal sum is %decimal%.
> con echo The octal sum is %octal%.
十进制加法得到十进制 13,而八进制加法得到八进制 13(“一三”,而不是十进制 13)。记住,八进制数系统没有 8 或 9。八进制 10 是十进制 8,在这个例子中,八进制 13 是十进制 11。因此,在 Batch 中,11 + 2 = 13,但 011 + 02 = 013 = 11,所以下面显示的结果是:
The decimal sum is 13.
The octal sum is 11.
解释器甚至可以处理十进制和八进制值混合的算术运算。十进制的 10 + 10 是 20,而八进制的 010 + 010 是 16。当将十进制和八进制相加时,比如 10 + 010,Batch 会给出正确的结果 18。通常,这种类型的算术是偶然发生的,但有时精明的程序员会利用这一点,了解它是可能的也是很有用的。
以类似的方式,这些值被视为十六进制:
set /A hexadecimalNbr = 0xA * 0x14
通过这次乘法,0xA 等于十进制的 10,而 0x14 转换为十进制时比 16 大 4。执行此语句后,变量的值为 200,10 和 20 的积。
八进制和十六进制可以是强大的工具;但是,如果你打算做十进制算术运算,务必小心确保没有前导零。由于十六进制以 0x 开头,因此不小心进行十六进制运算要困难得多,但由于一个看似无害的前导零不知不觉地进行八进制运算则极为容易。
注意
因为数学无处不在,你会在第十六章、第十八章和第二十一章中找到包含各种批处理文件算术运算示例的框。Batch 还具有用于位操作的算术运算符:按位与、按位或、按位异或、逻辑左移和逻辑右移。我将等到第三十章再探索它们,因为这些运算符使用一些具有其他用途的特殊字符,而且许多有经验的程序员从未在编译代码中操作过位,更不用说在批处理文件中了。
浮动点数
批处理不明确处理浮动点数——也就是非整数有理数。事实上,如果需要对这样的数字进行大量处理,使用比批处理更好的工具。它就像是用铁锹挖掘房屋地基。虽然可以做到,但只有最严谨的苦行者才能完成。如果任务足够大,可以编写一些编译代码并从批处理文件中调用,但当只需要做一些轻量级的浮动点算术时,Batch 可以处理,就像你可以用铁锹在前院种几个郁金香球茎一样。
请记住,所有批处理变量实际上只是华丽的字符串。我们可以很容易地为几个变量赋予浮动点值——也就是带有小数点的数字。以下是两笔金额,单位为美元和美分:
set amt1=1.99
set amt2=2.50
如果这些是整数,我们可以简单地使用 set /A 命令将它们相加。让我们试试,看会发生什么:
set /A sum = amt1 + amt2
结果是 3 存储在总和中,而不是期待的 4.49。每个数字的小数部分完全被忽略,导致 1 和 2 的整数和。
我们需要去掉小数点,进行算术运算,再恢复小数点。将每个金额乘以 100 就能解决这个问题,但批处理不会允许这样做。不过,由于浮动点值实际上只是一个伪装的字符串,我们可以使用上一章中描述的语法去掉小数点:
set amt1=%amt1:.=%
set amt2=%amt2:.=%
现在金额是 199 和 250。此 set /A 命令的结果是 449:
set /A sum = amt1 + amt2
为了恢复小数,我们不能仅仅除以 100——这仅适用于整数——但是我们可以使用上一章中的字符串解析逻辑。通过子字符串提取,以下 set 命令将变量重置为三个项目的连接:数字的前面部分(去掉最后两个字节),一个硬编码的小数点(或点),以及数字的最后两个字节:
set sum=%sum:~0,-2%.%sum:~-2%
> con echo The sum is %sum%.
最终,写入控制台的变量被设置为 4.49。
乘法的工作方式相同。如果你以 499 美元购买那台新电脑,第一年无需支付款项,利率为 19%,那么一年后你将欠多少钱?利率转化为 1.19 的倍数,但我们仍然必须去掉小数点。在找到两个整数的乘积后,我们通过在最后两个字节前插入小数点来恢复小数,如列表 6-2 所示。
set amt=499
set factor=1.19
set factor=%factor:.=%
set /A product = amt * factor
set product=%product:~0,-2%.%product:~-2%
> con echo The product is %product%.
列表 6-2:整数与浮动小数的乘法
593.81 的乘积可能会让你重新考虑融资计划。
每个程序员的目标应该是编写“防弹”的代码。不幸的是,之前的代码更像是棉网而不是凯夫拉尔,并且有许多警告需要讨论。我们做了几个假设,如果其中任何一个被违反,代码就会出错。加法假设两个数字都包含两位小数;1.9 而不是 1.90 会使结果偏差 10 倍。除了小数点外,任何非数字字符都会引发问题,值前面的零会触发八进制运算。乘法则更为复杂。列表 6-2 包含一个整数,但如果 amt 表示的是美元和分,乘积将会有四个小数位,而不是两个。为了表示美元和分,应该去掉最后两个字节——或者更好地说,进行四舍五入。
我在这里不会深入探讨这些细节,原因很简单,如果输入数据不一致并且需要数据验证,批处理中的浮动小数算术可能不是最佳解决方案。为所有可能的情况编写代码无疑是繁琐的。重要的是,程序员要了解现有的选项。如果所有的值都具有一致的小数位数,则可以通过几行代码进行运算。当我在批处理程序中不得不使用浮动小数类型时,通常是为了处理涉及一致数据的特定任务。拿出那把铁锹,但只在合适的时候。
八进制案例研究,继续
那么,我在千年初的 8 月 1 日的执行日志中到底发现了什么呢?在批处理文件中,今天的日期被格式化为 CCYYMMDD,例如 20050801,并且被分解为三个独立的字段:
todaysYear = 2005
todaysMonth = 08
todaysDay = 01
如果 todaysDay 不是 01,我们只需从八位数中减去 1,然后继续。但当它是 01 时,我们需要进行一些额外的算术运算。仅考虑月份的逻辑(并且理解 1 月会有一些特殊逻辑),我们必须减去 1 来确定前一个月份:
set /A month = todaysMonth - 1
当 todaysMonth 是 03 时,月份为 2;当 todaysMonth 是 07 时,月份为 6。但当 todaysMonth 是 08(即 8 月 1 日)时,前面的算术结果会变成-1。
解释器看到前导 0,并将算术视为八进制运算。八进制只理解数字 0 到 7,所以当解释器看到 8 时,它会认为这个字符像“ohkuh”一样陌生(“ohkuh”是瓦肯语言中数字 8 的发音),并简单地忽略它。最终,set /A命令将表达式中剩余部分的数学结果(即-1)赋值给月份变量。这个值最终打破了日期逻辑,导致我们无法获得所需的 7 月 31 日日期。
“八进制!”
使用子字符串提取和if命令,我插入了这一行代码来去除 todaysMonth 变量中可能存在的前导零:
if %todaysMonth:~0,1% equ 0 set todaysMonth=%todaysMonth:~1%
这段代码在未来几年都能正常工作,甚至在 8 月和 9 月的 1 号。如果原始代码没有在 8 月 1 日运行,那么在 9 月 1 日运行时会失败,因为 9 月被表示为 09。但如果代码没有在这两天运行呢?下一次会在哪一天失败呢?在 10 月 1 日,月份会被表示为 10。解释器会把它当作十进制数处理,因此代码会按预期执行。所以,8 月和 9 月的 1 号是唯一能打破这段代码的日期。
要非常注意八进制。
总结
在本章中,我讨论了数值数据类型以及它们在批处理中的处理方式。与大多数其他编程语言不同,批处理变量并没有定义为某种特定的数据类型。从本质上讲,所有变量都是简单的字符串,但当该字符串包含一个数字时,它可以被视为数值。
加法、减法、乘法、除法,甚至取模运算都能很容易地在十进制整数上进行,按照你可能在学校里学过的运算顺序规则。八进制和十六进制整数也得到了支持,尽管八进制运算容易被误用。根据我的个人经验,确保你的十进制整数没有前导零。增强赋值运算符提供了一种方便且未充分利用的工具,用于递增整数。
批处理中不支持浮动点数数据类型,但你已经学会了,通过一些小技巧,可以对带小数点的数字进行一些简单的算术运算。
转变话题,我将在下一章讨论文件操作。批处理的一个非常有用的功能是创建、复制、移动、重命名和删除文件和目录。
第七章:7 操作文件

如果你问一个对 Batch 只有间接了解的程序员,它的主要用途是什么,他们的回答很可能会提到移动文件。Batch 能做的远不止这些,但毫无疑问,它的主要用途之一就是文件操作。在本章中,我们将探索可用的不同命令和技巧。你还将学习如何创建空文件,以及合并、移动、重命名和删除文件的方法。我将介绍文件掩码和通配符,允许你对许多同名文件而不仅仅是一个文件执行你即将学习的命令。
复制文件的命令
Batch 有三个复制文件的命令:copy、xcopy 和 robocopy。在这一节中,我将对它们进行比较,并提供何时使用每个命令的建议,因为它们各自有不同的应用场景。你是在复制许多小文件还是几个大文件?网络稳定吗?速度是一个考虑因素吗?你希望返回码是简单的还是更细致的?日志记录对你有多重要?你需要回答许多问题,才能决定在特定的复制任务中使用最合适的命令和选项。
copy
copy 命令提供了一种快速简便的方法来创建空文件:
copy nul C:\Target\EmptyFile.dat
在 Batch 中,nul 这个词表示一个始终为空的文件(显然,有人觉得需要缩写 null)。在这里,我们通过复制 nul 文件来创建一个空文件,文件路径和名称由我们选择(在第十二章中,我将演示如何通过将不需要的输出发送到这个文件来处理它,让它消失)。有趣的事实是:Windows 不允许你在任何文件夹中创建名为 nul 的文件,无论扩展名是什么,或者没有扩展名,甚至手动创建也不行。你可以试试。
我使用复制命令的唯一其他场景是合并两个或更多相对较小的文件。/B 选项执行 二进制 文件复制,这意味着每一个字节,包括回车符和换行符等特殊字符,都会被原样复制,从而实现文件的真实连接。源文件通过加号分隔,后面跟着合并后的文件:
copy /B C:\Source\Header.txt + C:\Source\Details.txt C:\Target\MergedFile.txt
我们在这里将两个文件连接起来,但我们可以通过更多的加号分隔符合并更多文件。
你可能已经注意到,我只提到了复制命令的两种用途,而且都不涉及复制实际的文件。你可以使用该命令复制文件,但我从不使用它,因为它过于原始,选项也很少。这些不足很快在 Batch 首次发布后就显现出来,随后它很大程度上被更有用且可配置的 xcopy 命令取代。
xcopy
xcopy 命令的基本语法有两个参数——源文件和目标路径:
xcopy C:\Source\File2Copy.txt C:\Target\
xcopy 命令的一个巨大优势是,如果目标目录尚不存在,它会自动创建该目录。相比之下,copy 命令找不到路径就会失败。
在前面的示例中只给出了目标路径,这意味着复制的文件将与源文件同名。但我们可以通过简单地提供文件名来在 xcopy 命令中重命名目标文件:
xcopy C:\Source\File2Copy.txt C:\Target\RenamedFile.dat
这个命令有时会失败,因为解释器无法确定目标是目录还是文件(在第十二章中,我会讨论如何让这始终正常工作)。
xcopy 命令有两个选项,我几乎每次调用命令时都会使用。/Y 选项抑制确认覆盖目标文件的提示。虽然在通过命令提示符或交互式运行时,询问是否确认覆盖可能有意义,但在大多数其他情况下,它只是停止处理并造成卡顿,因此最好关闭它。
另一个我离不开的选项是 /F。它显示每个被复制文件的完整源路径和目标路径及文件名,这在使用通配符复制多个文件时,能留下有用的审计记录。如果没有 /F 选项,只会显示源文件或文件的路径,并给出文件总数。处于另一极端,如果你对这些信息不感兴趣,可以使用 /Q 选项(即安静模式)完全关闭显示。
使用这两个不可或缺的选项,以下命令会抑制提示并提供详细的日志记录:
xcopy C:\Source\File2Copy.txt C:\Target\ /Y /F
这个命令有太多可用选项,无法在此逐一介绍;请使用 help 命令查看完整列表。不过,作为一小部分示例,/U 选项仅复制目标中已经存在的文件;/S 选项复制文件夹及子文件夹;/J 选项使用无缓冲 I/O,适用于非常大的文件。
尽管 copy 命令已被弃用,xcopy 命令取而代之,微软实际上也认为 xcopy 本身已被弃用,取而代之的是更新的 robocopy 命令。尽管如此,xcopy 命令的广泛使用意味着它仍将在可预见的操作系统中可用,正如我稍后会详细说明的那样,在许多情况下它仍然是更好的选择。事实上,我在第一章中使用的就是这个命令,也许是你第一个 bat 文件的一部分。
robocopy
删除命令名中的最后一个字符,能揭示一部老科幻电影的标题,但 robocopy 实际上代表的是强大复制,这并非夸张。尽管 xcopy 拥有许多有用的选项,robocopy 提供了令人印象深刻的日志记录和令人目不暇接的选项。
尽管 robocopy 命令比 xcopy 强大得多,但它同样非常易于使用。参数略有不同:首先提供源目录(不带文件名),然后是目标目录,最后是要复制的文件、文件或文件掩码:
robocopy C:\Source\ C:\Target\ File2Copy.txt
这个示例在功能上与前一节中的 xcopy 命令等效,并在此重现:
xcopy C:\Source\File2Copy.txt C:\Target\
由于 robocopy 命令的前两个参数被认为是路径,因此你可以省略结尾的斜杠,这相比 xcopy 是一个很大的优势,因为在 xcopy 中,漏掉斜杠会把目标目录误当作文件名。
robocopy 日志记录
要了解 robocopy 的强大功能,不妨看看它的日志记录能力。如果你是 xcopy 的爱好者,最好先坐下再继续往下看。Verbose(详细)是唯一能形容它的词,甚至连这个词也不够准确。除了复制的文件列表,日志还包括一个漂亮的头部、开始和结束时间、文件大小、复制速度统计、源路径和目标路径、复制和跳过的文件和目录总数、任何失败的复制、使用的命令行选项列表等等。它的格式也很不错,有很多空白区域,便于阅读。
前面展示的简单 robocopy 命令用于复制一个文件,它会生成以下日志:
-----------------------------------------------------------------------------
ROBOCOPY :: Robust File Copy for Windows
-----------------------------------------------------------------------------
Started : Tuesday, January 30, 2007 12:18:44 PM
Source : C:\Source\
Dest : C:\Target\
Files : File2Copy.txt
Options : /DCOPY:DA /COPY:DAT /R:1000000 /W:30
-----------------------------------------------------------------------------
New Dir 1 C:\Source\
New File 83146 File2Copy.txt
100%
-----------------------------------------------------------------------------
Total Copied Skipped Mismatch FAILED Extras
Dirs : 1 1 0 0 0 0
Files : 1 1 0 0 0 0
Bytes : 81.1 k 81.1 k 0 0 0 0
Times : 0:00:00 0:00:00 0:00:00 0:00:00
Speed : 20786500 Bytes/sec.
Speed : 1189.413 MegaBytes/min.
Ended : Tuesday, January 30, 2007 12:18:44 PM
一些结果不言自明,比如 Total(总数)、Copied(已复制)和 FAILED(失败),但其他结果则不太明显。如果要复制的文件已经存在于目标路径中并且完全相同,robocopy 会聪明地避免浪费时间覆盖它,而是将其视为合理的 Skipped(跳过)。当复制被阻止,因为源文件和目标文件夹(或源文件夹和目标文件)同名时,会发生 Mismatch(不匹配)。Extras(额外项)是指不在源目录中,但已经存在于目标位置的文件和目录。显然,这些文件并没有被复制,但解释器觉得有必要记录它们的存在。对于仅复制一个小文件的任务来说,日志记录可能显得有些过多。一个更有趣的任务可能会持续几页,但对于那个更有趣的任务,日志记录可能是不可或缺的。
默认情况下,所有这些信息都会写入控制台(在第十二章中,我会讨论如何将任何命令甚至整个批处理文件的日志重定向到日志文件)。但 robocopy 命令的独特之处在于,你可以轻松地通过选项创建日志文件。日志文件的路径和名称紧随其后,通过冒号与 /LOG 选项分隔。
这可能是你第一次看到选项后有多个字符的情况,而且这个命令还有更多类似的选项。将加号添加到选项中,如/LOG+,会导致信息附加到日志文件中(如果文件已存在)。为了演示,考虑以下内容:
set roboLog=C:\Batch\Robocopy.log
robocopy C:\Source\ C:\Target\ File2Copy.txt /LOG:%roboLog% /NP
robocopy C:\Source\ C:\Target\ AnotherFile2Copy.txt /LOG+:%roboLog% /NP
在定义 roboLog 为日志文件路径和名称后,它会在两个 robocopy 命令中使用。第一个命令创建日志文件并写入复制信息,如果文件已存在,则会覆盖;第二个 robocopy 命令则将复制结果追加到该日志文件中。
最后两个命令都使用了/NP 选项,代表无进度,因为在创建日志文件时我认为它是必需的。如果信息被写入控制台,复制的进度会实时显示为百分比,并且每秒更新多次。较大的文件可能会显示几十个进度值,直到最终显示 100%。这种状态在控制台上查看时非常好,但在日志文件中,每次更新都会成为一个新记录。即使是几个大文件,也能让日志文件变得一团糟。即使是小文件,也可能导致日志文件中出现一些不需要的额外记录。/NP 选项非常好地解决了这个问题,它只会在每次复制完成后更新日志。
有用的 robocopy 选项
robocopy 有许多选项,所以我将快速介绍其中最有用的几个。但首先,给出一个命令作为选项的基础:
robocopy C:\Source\ C:\Target\
这个简单的命令(没有文件的第三个参数)会将源目录中的所有文件复制到目标目录。
这个命令会自动重试失败的复制操作,功能非常棒,但默认的重试次数高达一百万次,每次之间间隔 30 秒。这可能会导致在进行有致命错误的复制尝试时,程序挂起几个月。/R 和/W 选项分别覆盖了默认的重试次数和等待时间(秒):
robocopy C:\Source\ C:\Target\ /R:20 /W:5
使用这些选项时,解释器会在失败的复制操作中最多重试 20 次,每次尝试之间间隔 5 秒,之后才会中止。选择适合你的硬件和需求的值,但默认值不能继续使用。
如果添加了/S 选项,它还会复制所有的子目录,不过会排除空子目录:
robocopy C:\Source\ C:\Target\ /S
/E 选项会复制所有的子目录,包括空目录和非空目录:
robocopy C:\Source\ C:\Target\ /E
你可以精细化此选项,只复制源目录(C:\Source*)及其直接子目录中的文件,但不包括更低层级的子目录。/LEV 选项将子目录的层级*(包括根目录)定义为 2,以此来处理:
robocopy C:\Source\ C:\Target\ /E /LEV:2
无缓冲 I/O 对于非常大的文件来说效率更高,而且通过/ J 选项调用,奇怪的是,它的实现方式与 xcopy 命令中的/U 选项有关(/U 已经被占用了)。/MIN 和/MAX 选项设置了文件复制时的最小和最大字节限制。以下两个命令会复制文件夹中的所有文件,只对至少 1GB 的文件使用无缓冲 I/O:
robocopy C:\Source\ C:\Target\ /MIN:1000000000 /J
robocopy C:\Source\ C:\Target\ /MAX:999999999 /MT
我偷偷将/MT 选项(代表多线程)加到了小文件的命令中。默认情况下,robocopy 命令是串行复制文件的,但这个选项会并行复制八个文件。你甚至可以定义线程数;/MT:128 是最大值,但根据我的经验,这并不会比默认的八个线程快太多。我们可以讨论阈值,但对于大文件的无缓冲 I/O 和小文件的多线程操作,都会优化任何复制过程。
/MINAGE 和 /MAXAGE 选项(最小和最大年龄)定义了基于最后修改日期的复制规则。你可以单独使用它们,也可以结合使用以创建日期范围。以下命令只复制自 Microsoft 在 2000 年 9 月 14 日发布 Windows Me 以来修改过的文件,并排除过去七天内发生变化的文件:
robocopy C:\Source\ C:\Target /MAXAGE:20000914 /MINAGE:7
你可以将两个选项的值定义为日期(格式为 CCYYMMDD)或天数。解释器足够聪明,可以识别八字节数字表示的是日期,而不是超过 50,000 年的时间跨度。批处理可能不是一种新语言,但它既没有被丹尼索瓦人使用,也没有被我们的猎人-采集者祖先使用。任何小于 1,900 的数字都会被解释器认为是天数。
/PURGE 选项会删除目标位置的额外文件和目录,显然你应该谨慎使用,但它是创建备份时非常实用的工具。如果你在某一天备份了一个文件夹,第二天更改了源目录中某个文件的名称,然后再次备份该文件夹,除非使用 /PURGE 选项,否则备份中会多出一个不必要的文件。更好的做法是使用 /MIR 选项,它会镜像整个目录树。/MIR 选项基本上做的事情和 /PURGE 选项相同,但它还包括子目录。
/XF 选项会将一个或多个文件从复制中排除,而 /XD 选项则类似,排除一个或多个目录。/L 选项不会复制任何内容;它会生成一个列表,列出如果没有使用该选项,所有本应复制的内容。
你还可以使用 robocopy 命令来移动文件(参见第 80 页的“移动文件”)。像往常一样,使用帮助命令可以获得完整的可用选项列表。
robocopy 返回代码
xcopy 命令与大多数批处理命令类似,成功执行时返回错误级别 0,而失败时返回一个非 0 的数字。相比之下,robocopy 命令是独特的——如果不理解这一点,可能会感到非常困惑。返回代码 0 表示没有复制任何内容,但如果至少有一个文件成功复制,解释器会返回一个介于 1 到 15 之间的奇数,但即便是这些代码中,也并非都是良好的返回代码。以下是 robocopy 命令生成的六个基本返回代码:
0 没有错误,但没有文件被复制;换句话说,所有文件都被跳过了。
1 一个或多个文件复制成功。
2 发现了一个或多个额外的文件或目录,但没有任何文件被复制。
4 发现了一个或多个不匹配项;没有文件被复制。
8 某些文件或目录无法复制。
16 没有文件被复制;出现了严重错误。
哦,二的幂的优雅;一定是某个数学家想出了这些返回代码。难道用 0 到 5 这几个代码不更简单吗?不,中间的四个代码并不互相排斥;也就是说,它们中的多个(甚至全部四个)可以同时为真。解释器会将所有为真的代码加起来,并返回其总和作为错误级别。
举个例子,设想一个 robocopy 命令,将源文件夹中的所有文件复制到目标文件夹,其中一些文件复制成功,但目标文件夹也有额外的副本。返回代码 1 和 2 都为真,因此返回 3(它们的总和)作为错误级别。也可能在复制文件时,找到额外的文件,并且发生不匹配,另一个文件因为被某个进程或人占用而未能复制。做个数学运算(1 + 2 + 4 + 8),解释器返回 15。
这些返回代码为精明的编码员提供了细化错误处理的机会。返回代码 1 和 3 显然是好的,我通常认为 3 或更少也是好的。任何 4 到 7 的返回代码涉及不匹配,任何高于这个范围的返回代码至少包含一个显式的失败。根据具体情况,不匹配可能是完全可以接受的,因此只有返回代码为 8 或更大时才被视为不良。
为了明确验证至少有一个文件被复制,我们只需要查找一个奇数返回代码,这可以通过 第六章中的模运算函数来实现。
xcopy 与 robocopy
尽管“更好”这一词具有主观性质,但大多数批处理编码员一致认为 robocopy 比 xcopy 更好,因为它提供了丰富的选项、多线程、自动重试、强大的日志功能,以及许多后台操作,使得它更高效。但有一个主要的警告。当尝试复制单个文件时,xcopy 的错误处理显然更好。这两个命令都试图复制一个不存在的文件:
xcopy C:\Source\NonExistentFile.txt C:\Target\
robocopy C:\Source\ C:\Target\ NonExistentFile.txt
xcopy 命令报告没有复制任何文件,并返回错误级别 4,而 robocopy 命令则简单地返回 0,表示没有错误也没有文件被复制。我认为这应该算作一个错误。当尝试复制多个文件时,如果没有源文件,两个命令都会返回 0——这很合理,但这两个命令都要求显式地复制一个文件,而 robocopy 的返回代码并未区分文件未找到和文件因为已存在于目标目录而被跳过。跳过未更新的文件是 robocopy 的一个很棒的功能,但它与文件未找到完全不同。强大的日志功能使这一点变得清晰,但返回代码并没有体现。
返回代码未能报告为何文件未被复制(未找到或被跳过)是一个缺陷,但我们可以克服它。使用 robocopy 命令复制特定文件时,你可以在 errorlevel 为 0 时对目标文件执行 if exist 检查。如果文件存在,说明文件已被正确跳过;如果不存在,则表示出现了错误。或者,像我一样,在这种情况下直接使用 xcopy。
还有一些情况下,robocopy 命令复杂的返回代码超过了实际需要。你可能不关心是否有额外或不匹配的文件,而只需要一个简单的结果:好与坏,零与非零。这样做并没有错,说实话,有时候我仍然因为习惯和使用方便而在新代码中使用它。
也就是说,robocopy 命令正如其名,具有更强的健壮性。它的配置更加灵活,速度更快,失败的可能性更小。如果需要复制非常大的文件、数量庞大的文件,或者因网络连接问题可能会失败的文件,robocopy 无疑是最佳选择,而它复杂的返回代码在某些时候也是一大优势。此外,xcopy 命令对路径和文件名的长度有 254 字节的限制。我从未接近过这个限制,但如果你真的碰到这个问题,robocopy 可以应对。
我对这个话题最明确的声明是,你永远不应该使用 copy 命令来复制文件,但可以将它保留在工具箱中,用于创建空文件或合并文件。
文件掩码和通配符
之前的所有 xcopy 示例都是复制单个文件,而 robocopy 示例要么复制单个文件,要么复制目录中的所有文件。但使用文件掩码和通配符后,你可以创建更具针对性的命令,只复制目录中的部分文件;实际上,它们每次执行时可能会复制不同数量的文件。文件掩码替代这两个命令中的文件名,并由一个或多个通配符字符组成,可能还包含一些硬编码文本或已解析的变量。然后,当命令执行时,它会复制所有符合文件掩码条件的文件。
文件掩码并非仅限于复制文件的命令。接下来本章中的移动、删除甚至重命名文件的命令同样接受文件掩码来代替文件名,这样你就可以一次性移动、删除或重命名多个文件。for 命令充分利用了文件掩码,我将在第二部分中展示如何使用。任何对文件执行操作的命令可能都支持通配符;如果不确定,可以尝试一下。
Batch 识别两个字符作为通配符:星号()和问号(?)。它们的行为截然不同,我将在使用 xcopy 命令时详细说明这两者,但首先让我们看一组需要复制的文件。一个精心组织的人可能会保持包含预算信息的电子表格,每个月一个文件。为了演示,C:\Budget* 文件夹中包含按年份和月份命名的电子表格。这里列出三个:
Budget.January2008.xlsx
Budget.February2008.xlsx
Budget.March2008.xlsx
这些文件来自金融危机的年份和大萧条的开始,文件夹中还包含前后年份命名相似的文件以及其他类型的文件。
星号通配符字符
星号是 Batch 中最常见的通配符字符,代表零个或多个字符。为了演示,以下的 xcopy 命令将复制所有 2008 年的文件,或者更具体地说,复制*C:\Budget* 中满足 Budget.2008.xlsx 通配符的文件:
xcopy C:\Budget\Budget.*2008.xlsx C:\Target /F /Y
星号是通配符,意味着该命令复制所有以 Budget. 开头,2008.xlsx 结尾,且中间可以有任何内容,甚至没有内容的文件。如果文件夹中有一个名为 Budget.2008.xlsx 的文件,它也满足这个通配符并将被复制。
到年底时,应该有十二个这样的文件满足此通配符,一个对应每个月,这样命令就会复制所有 12 个文件。但该命令不会复制 2007 年或 2009 年的文件,也不会复制具有相同名称但扩展名不同的 Word 文档,例如 Budget.June2008.docx。如果你将其中一个文件的文件名中的第一个点删除,像 BudgetAugust2008.xlsx,解释器也不会复制它,因为它不满足通配符。
以下的细微变化——在通配符前插入字母 J——导致命令只复制三份文件,分别是 1 月、6 月和 7 月的文件:
xcopy C:\Budget\Budget.J*2008.xlsx C:\Target /F /Y
在通配符前再加一个字符,Budget.Ju*2008.xlsx 的通配符将排除 1 月的文件。
你不只限于在一个通配符中使用单一的星号。这里是一个例子,使用了两个星号,其中最后一个代表文件扩展名:
xcopy C:\Budget\Budget.*2008.* C:\Target /F /Y
前面提到的 Word 文档,Budget.June2008.docx,现在也被拖网捕获。
你现在知道星号(*)通配符可以表示任何长度的文本,包括不包含任何文本的情况,但有时你可能希望更具限制性。Batch(批处理)有一个鲜为人知且更少被理解的通配符字符,专门用于这个目的。
问号通配符字符
虽然星号是零个或多个字符的通配符,但问号通常是精确匹配一个字符的通配符。(是的,斜体表示有个小陷阱。)为了只复制包含四个字符月份的文件,我将在原本是星号的位置使用四个问号:
xcopy C:\Budget\Budget.????2008.xlsx C:\Target /F /Y
该命令复制了 6 月和 7 月的文件,但没有复制 3 月、4 月、5 月以及所有其他月份的文件,这些月份的文件名通常较长。
这看起来很简单,但我之前承诺会告诉你一个注意事项。如果一个或多个问号通配符出现在文件掩码的末尾,或者这些问号紧接着一个点号,Batch 也会认为空字符是每个问号的有效替代值。
举个例子,我会稍微调整文件命名规则,在月份和年份之间插入一个句点,从而得到如下的文件名:
Budget.April.2008.xlsx
Budget.May.2008.xlsx
Budget.June.2008.xlsx
Budget.July.2008.xlsx
接下来,我会将句点插入到文件掩码中的四个问号和年份之间,如下所示:
xcopy C:\Budget\Budget.????.2008.xlsx C:\Target /F /Y
即便是一些经验丰富的 Batch 程序员,可能也只会认为 6 月和 7 月的文件符合这个通配符条件。事实上,他们是对的,3 月、4 月以及所有其他月份(用超过四个字母表示的月份)的文件不会被复制,但 5 月的文件也会符合这个条件。前面三个通配符与“May”中的每个字母匹配,但第四个问号则与一个空字符或不存在的字符匹配。即使是命名奇怪的Budget..2008.xlsx也符合这个条件。
关键是,当n个问号通配符出现在文件掩码末尾或紧跟一个点号时,该掩码可以由零到n个字符满足。否则,n个问号通配符则要求正好n个字符。这是一个非常微妙的特性,理解它可能会节省你几个小时的麻烦。
为了更明确地说明这两个 Batch 通配符字符之间的区别,“shot”、“shoot”、“shut”、“shunt”、“shallot”甚至“sht”都符合 sh*t 文件掩码。对于这些单词(以及一个非单词),只有“shot”和“shut”符合 sh?t 文件掩码。
移动文件
移动文件类似于复制文件;唯一的区别是,复制文件后,它会存在于两个地方,而移动文件后,原始文件就不复存在了。移动命令可以轻松完成这一任务。它只需将源文件和目标文件作为参数,通常你会看到它带有/Y 选项,用于抑制覆盖目标文件时的确认提示:
move C:\Source\File2Move.txt C:\Target\ /Y
许多旧代码仍然包含移动命令,但它已被大部分弃用,转而使用 robocopy 命令。带有/MOV 选项的以下命令在功能上等同于之前的移动命令:
robocopy C:\Source\ C:\Target\ File2Move.txt /MOV
从这个 robocopy 命令中去掉文件名会移动源文件夹中的所有文件,但不会移动子文件夹中的任何文件。
添加/S 选项会导致移动所有子文件夹中的内容,即使目标子文件夹需要创建:
robocopy C:\Source\ C:\Target\ /MOV /S
该命令移动所有文件。文件不再位于源位置,但源文件夹结构保持不变。现在,这里有点奇怪,值得注意。我们一直在讨论 /MOV 选项,这显然是 move(移动)的缩写,但一个类似的选项 /MOVE 是指……我猜是 带 E 的移动。这两个选项有微妙的不同。带有 /MOVE 选项的 robocopy 命令真正地移动了文件和目录。
在选项中添加 E 会在将所有文件复制到目标位置后删除源目录结构:
robocopy C:\Source\ C:\Target\ /MOVE /S
我们有一个 /MOV 选项,它在复制子目录的同时移动文件,还有一个 /MOVE 选项,它移动文件和子目录。这个区别并不特别直观。
在考虑使用哪条命令来移动文件时,robocopy 命令是最有效的选择,原因和“xcopy 与 robocopy”中第 76 页提到的完全一样。但像 xcopy 命令一样,move 命令返回的代码更直观,仍然在批处理环境中有它的位置。
删除文件
当文件不再需要时,清理它们是很有道理的。del 命令可以轻松删除一个或多个文件。/Q 选项,代表 quiet 模式,可以防止解释器询问是否删除文件。该命令接受多个要删除的文件参数,既可以使用显式文件名,也可以使用文件掩码。以下命令删除名为 Junk.txt 的特定文件以及文件夹中所有扩展名为 .OLD 的文件:
del /Q C:\Source\Junk.txt C:\Source\*.OLD
使用 /A 选项根据文件的属性选择文件进行删除。例如,使用 /AH 选项仅删除隐藏文件。取反逻辑,/A-H 选项仅删除 非 隐藏文件。和往常一样,使用 help 命令查看完整的选项列表。
只需将目录作为参数传递给 del 命令,即可删除该文件夹中的所有文件,但目录本身仍然存在。要删除目录本身,你需要使用不同的命令,我会在第十三章中分享它。
重命名文件
ren 和 rename 命令是批处理命令的同义词;也就是说,它们是相同的命令。第一个参数是要重命名的文件,第二个参数是新的文件名:
ren C:\Batch\File2Rename.txt NewFileName.txt
如果目标文件已经存在,解释器会返回错误级别 1。如果有可能同名文件已经存在,我会在重命名之前悄悄删除它:
del C:\Batch\NewFileName.txt /Q
ren C:\Batch\File2Rename.txt NewFileName.txt
即使对于此命令也支持通配符,但我只使用 ren 命令来显式指定文件名,主要是因为该命令不会在控制台上写出重命名文件的列表。(如果我要重命名多个文件,我会使用 dir 命令作为 for 命令的输入,然后一个一个地重命名。我们将在第十三章和第十七章进一步探讨这些命令。)
ren 命令并不复杂,但我见过一个常见的陷阱太多次了。虽然在两个参数中都使用路径很简单,但如果稍微思考一下,解释器已经从第一个参数中知道了路径。文件并没有被移动或复制到其他地方;根据命令的性质,它只是在当前位置被重命名。如果第一个参数没有路径,系统会默认当前目录(更多内容将在下一章介绍),但第二个参数仅是新文件名,不应该包含路径。
总结
本章可能会成为你在本书中最常引用的一章。如果没有创建、复制、移动、合并和删除文件的能力,Batch 就什么都不是,我在这里介绍了许多执行这些任务的命令。我讨论的一些命令非常简单,但我也覆盖了不止一个常见的陷阱,并提供了解决方案。你还学习了如何利用文件掩码和通配符在多个文件上同时执行这些命令。
复制文件听起来是一个简单的任务,但我详细介绍了可用的多种技术和需要考虑的因素。我希望我能向你展示 robocopy 命令的强大与实用,同时也让你认识到 xcopy 命令的简单性和实用性。
在下一章,我将描述如何执行在其他语言中编译的程序,这将涉及到更深入的讨论,探讨当你没有提供路径时,解释器是如何找到要执行的程序的。
第八章:8 执行编译程序

本章名义上是关于一个 bat 文件执行或调用用其他语言编写并编译的程序。实际上,执行这些操作的语法非常简单。本章最有趣的部分是,有时候被执行的程序在 bat 文件中并没有定义路径。那么,bat 文件是如何找到可执行文件的呢?
本章的主要内容将集中在寻找此类程序的两个重要机制上:当前目录 和路径变量。这个话题不仅仅局限于执行程序。当你调用其他 bat 文件时,也会用到这些机制,而且它影响到很多其他资源未在 bat 文件中定义路径时的情况。例如,在第七章中,我讨论了许多用于复制、移动、删除和重命名文件的命令。当这些命令中的文件或文件未在 bat 文件中定义路径时,它们仍然能在你的 bat 文件中完美运行,只要你理解这些概念。当然,你还将学习不同的方式来调用程序并传递参数。
调用可执行文件
bat 文件通常仅仅是调用编译程序的载体或包装器,也就是所谓的 可执行文件。bat 文件会设置一些程序所需的变量,调用可执行文件,并在后台执行一些错误处理。更复杂的 bat 文件可能会调用数十个不同的程序,甚至在某些调用上使用条件逻辑。无论简单还是复杂,Batch 的一个特点是能够调用用其他语言编写的可执行文件。
call 命令接受可执行文件作为它的第一个参数,可能也是唯一的参数。以下命令调用或执行位于 *C:\Executables* 目录下的程序 MyProg.exe:
call C:\Executables\MyProg.exe
call 命令用于调用程序;这应该不会让人感到惊讶,但接下来要说的会有些奇怪。这是 Batch 中唯一一个,在命令名本身被省略时仍然能正常工作的命令,可能其他语言中也没有类似的情况。以下命令虽然在技术上不是 call 命令,但它执行的功能与前面的例子中的 call 命令相同:
C:\Executables\MyProg.exe
想一想这个问题。命令 set x=1 设置了一个变量,但语句 x=1 只是让解释器感到困惑。如果 robocopy 命令前面没有 robocopy 这个文本,没有理智的人会期望剩下的文本能复制一个文件。(如果这还不够奇怪,调用其他 bat 文件时,call 命令的有无会变得更加怪异,在第十章中有所讨论。)
这几乎看起来像魔法,但从解释器的角度来看。当它解释一行新代码时,通常期望第一项是一个命令。当它找到 set 时,它会预期一个变量、一个等号和一个值;当它找到 robocopy 时,它接下来会寻找不同的参数。当它遇到完全出乎意料的东西时,解释器不会退缩;它会给你,程序员,一个怀疑的好处,假设无论它是什么,都可以执行,并执行它——就像 call 命令那样。
一些批处理程序员使用 call 命令来执行可执行文件;有些则不使用。我属于后者,更喜欢程序仅由可执行文件本身或只是一个解决的变量在单行代码中呈现的干净样式,但对于那些明确拼写出命令的人,我没有异议。更重要的是,一致性是关键;坚持你选择的约定。
我还更倾向于将程序名保存在一个带有完整路径的变量中,只有在它尚未定义时才进行设置。这确保了所需的程序默认存储在变量中,同时也允许其他人将其设置为备用程序,以增加灵活性:
if not defined pgmMyProg set pgmMyProg=C:\Executables\MyProg.exe
然后,当执行程序时,这个简单的命令,如果我可以称它为命令,将会调用期望的程序:
%pgmMyProg%
这个变量包含可执行文件的路径,但让我们回到一个仅由硬编码路径和文件名组成的代码行的概念。
你可以通过移除路径,保留程序名和可能的扩展名来简化它:
myProg.exe
这看起来更简单,但当你停下来思考解释器该如何在机器或网络的某个位置找到程序时,复杂性就增加了。在深入这些细节之前,我需要稍微插开话题,讲讲两个命令/变量。
cd 命令和变量
cd 命令也是一个变量,是少数几个批处理伪环境变量之一。在第二十一章中,我将有更多内容讨论这些变量。目前,只需将它们视为解释器最初设置的变量,具有一些独特的特性。
这个变量代表当前目录。该命令稍微有些模糊,因为它也可以表示更改目录,因为它被用来……嗯,改变当前目录。
当你双击或打开一个 bat 文件时,当前目录就是 bat 文件所在的目录或文件夹。如果从另一个进程调用相同的 bat 文件,当前目录将从该进程继承。仅仅在不同目录中调用一个 bat 文件或可执行文件并不会改变当前目录,但 cd 命令会。
接下来的第一行和最后一行使用 cd 变量来显示当前目录。中间部分是 cd 命令,巧妙地将当前目录更改为其参数,假设该目录存在:
> con echo Current Directory is: %cd%
cd C:\NewDir\
> con echo Current Directory is: %cd%
如果一个包含这三行代码的 bat 文件位于 *C:\Batch* 目录下,执行它会在控制台上显示原始当前目录和新分配的当前目录:
Current Directory is: C:\Batch
Current Directory is: C:\NewDir
你还可以相对现有的当前目录设置当前目录。一个点表示现有的值,因此这将把 cd 变量分配到一个子目录:
cd .\Child\
两个点表示当前目录的父目录,因此以下命令会将当前目录上移一个级别:
cd ..
(.... 参数查找祖父目录。)
你甚至可以通过先使用两个点“..”上移一个级别,再重新分配 cd 变量到同级目录:
cd ..\Sibling\
我甚至不愿提及这一点,但 chdir 是 cd 命令的批处理同义词。也就是说,前一个示例中的命令在功能上等同于 chdir ..\Sibling\。然而,cd 变量没有同义词,所以你可以使用 chdir 或 cd 来更改当前目录,但在解析当前目录时,你需要使用 cd。我发现最简单的做法是始终使用 cd 来完成这两个目的。
在讲解当前目录的用途之前,我需要介绍另一个命令,它也是一个变量。
path 命令和变量
类似于 cd,path 也是一个命令和伪环境变量。该变量在 Windows 机器上预定义,包含一个以分号分隔的目录列表,这些目录对计算机是必需的,例如 Java 和 Windows 可执行文件的路径。(要查看当前在 Windows 机器上设置的 path 变量,可以打开命令提示符,使用我们在第二章中学到的知识,输入命令 set path。)
就像 cd 命令设置当前目录一样,path 命令设置 path 变量。在以下代码行中,现有的值被两个其他目录添加到前后;请注意,在每个附加目录的末尾插入了分号作为分隔符:
path C:\PrependDir\;%path%C:\AppendDir\;
你可以完全重新分配 path 变量——甚至可以完全清除它,如果参数仅是一个分号的话。此变量中的各个目录是有目的的,可能是为了允许某些必要的进程运行。对于在机器上持久更改该变量(例如使用 setx 命令)要非常小心,但前面显示的 path 命令只会更改 bat 文件执行时的路径。最坏的情况是你可能会破坏 bat 文件,但不会破坏计算机上的其他任何内容。在下一节中,我将解释为什么你可能想要更改 path 变量。
警告
set 命令提供了重置 cd 和 path 变量的另一种方法,但出于一致性考虑,我抵制这种方法,因为一些其他伪环境变量不能或不应该使用此命令重置——而且它需要更多的按键操作。
查找可执行文件
让我们回到通过仅调用程序的名称和扩展名来执行程序,如下所示:
myProg.exe
解释器在哪里找到可执行文件?它首先会在当前目录查找。如果在那里找到了,它会执行那个文件。否则,解释器会按顺序在路径变量中定义的每个目录中查找,并执行第一个找到的可执行文件。如果在这些目录中找不到该文件,解释器只会将错误级别设置为 9009 的值。(奇怪的是,如果call命令出现在可执行文件名之前,错误代码是 1。)
假设myProg.exe位于*C:\Executables*,我们执行相同的代码行。如果该目录是当前目录,程序将被找到并执行。否则,如果该目录在路径变量中,程序可能会被找到并执行。这假设程序没有被当前目录中或路径变量更高位置中的同名、同扩展名的其他程序所覆盖。
如果以上都不成立,程序将无法找到,但有多种方法可以确保解释器找到可执行文件。首先,我们可以使用cd命令在执行程序之前更改当前目录:
cd C:\Executables\
或者,我们可以通过两种方式来修改路径变量,使其包含目录。这里我是在路径前添加目录:
path C:\Executables\;%path%
这里我是在路径后追加目录:
path %path%C:\Executables\;
如果目录被追加,并且路径变量中较早定义的目录中存在另一个名为myProg.exe的文件,那么将会执行那个程序。将目录添加到前面确保了我的可执行文件在任何其他文件之前被选中,但这也有一定的风险。它可能会将某些内容引入到路径变量中,覆盖其他进程使用的资源。
这绝不是一种不好的技巧;事实上,当合理管理时,它非常有用。使用当前目录或路径变量来查找可执行文件的一个很好的应用是使代码具有可移植性。你可以将一个批处理文件保存在单个文件夹中,或者一个更复杂的文件夹结构中,里面包含其他批处理文件、可执行文件、配置文件和其他资源。然后,你可以将这个文件夹复制到具有不同根目录结构的其他计算机和网络中。由于当前目录本质上跟随高层批处理文件,它会在这些不同的位置工作,只要使用当前目录来查找它的其他组件。
你可以将默认可执行文件与批处理文件放在同一个文件夹中。如果单独运行,它将使用这个可执行文件。如果从另一个批处理文件调用,且当前目录不同,它可能会找到一个不同的程序,从而允许其他人使用你的批处理文件来调用他们自己的可执行文件。简而言之,你可以创建一组同名的程序,在不同的实例中执行不同的程序。
进一步说,我之前提到过,实际上甚至不需要扩展名就可以调用一个程序。也就是说,如果 myProg.exe 存在于当前目录,它很可能会通过以下代码行被调用:
myProg
解释器通过另一个伪环境变量 pathext 来找到没有扩展名的可执行文件,pathext 包含一个由分号分隔的扩展名列表,类似于 path 变量包含的目录层级。解释器仍然会在当前目录中查找可执行文件,然后是 path 变量中的各个目录,但在每个文件夹中,它现在会查找第一个可以找到的,文件名为 myProg 且扩展名位于给定层级中的可执行文件。
如果 pathext 变量没有被其他人或其他程序修改,它通常包含大约十几种文件扩展名,按顺序包括:.com、.exe、.bat 和 .cmd。因此,唯一会阻止先前的命令在当前目录执行 myProg.exe 的实体,就是当前目录中的 myProg.com。 (如果你需要重置这个变量,可以使用 set 命令。pathext 变量只是一个变量,而不是一个命令。)
推送和弹出当前目录
cd 命令非常有效地更改当前目录,但先前的当前目录会消失在空中,再也无法找回。通常这完全没有问题,但在某些情况下,你可能希望在更改当前目录后暂时恢复原来的状态。也许有一个实用的批处理文件(bat 文件)是为了被其他许多批处理文件调用而编写的。稍后我将详细讨论如何从另一个批处理文件调用一个批处理文件,但现在,我们只需要理解被调用的批处理文件的角度。
被调用的批处理文件可能会在某个文件夹中创建或使用资源,因此在批处理文件开始时更改当前目录是有意义的。然而,当被调用的批处理文件完成并将控制权交还给调用它的批处理文件时,应该恢复先前的当前目录。这是基本的礼貌,因为调用的批处理文件可能正在不同的目录中工作,改变它的当前目录可能会给它带来问题。一个更自私的动机是,被调用的批处理文件可能希望保持它自己的目录。如果被调用的批处理文件不恢复当前目录,调用的批处理文件可能会在现在的当前目录中丢下不必要的文件。被调用的批处理文件可以在不让外部干扰的情况下保护它的目录,同时也表现得非常有礼貌。
为了解决这个问题,你可以在执行 cd 命令之前将先前的当前目录存储在一个变量中,然后在批处理文件的末尾执行另一个 cd 命令来恢复它。但批处理提供了两个命令,结合使用可以更优雅地完成这个任务,它们就是 pushd 和 popd 命令。
pushd 命令像 cd 命令一样改变当前目录,但它还会将先前的当前目录 推送 到堆栈中,供以后使用。有时它被称为 推送目录 命令,尽管为了简便,通常按字面发音,即“push-d”命令。在 bat 文件的开头附近,此命令将简洁地执行这两个任务:
pushd C:\NewDir\
在 bat 文件的末尾或接近末尾的位置,以下简短的命令将删除 *C:\NewDir* 作为当前目录,并从堆栈中获取或 弹出 之前的当前目录,用它来恢复当前目录:
popd
这有时被称为 弹出目录 命令,但更常见的是“pop-d”命令。
注意,没有参数;popd 是一个极少接受任何参数的命令。当多个 pushd 命令执行时,每个命令都会将另一个先前的当前目录推送到堆栈中,而每个随后的 popd 命令都会恢复最近添加的目录。
另需注意的是,如果传递给 pushd 命令的参数是网络路径,则该路径会分配给最高的未使用驱动器字母,popd 命令将取消该分配。最后,未带参数的 pushd 命令会显示堆栈中目录的完整列表,从最近添加的目录开始。
警告
pushd 和 popd 命令必须平衡使用。 如果一个 pushd 分配了网络路径,则应始终执行相应的 popd,即使有错误被处理。如果没有,任何映射的驱动器字母将保持映射,即使 bat 文件已经完成。如果这种情况发生得足够频繁,计算机会用完可用的驱动器字母。
使用当前目录查找其他资源
当前目录用于的不仅仅是查找可执行程序。对于任何资源,例如文件,如果路径没有被定义,当前目录将被假定为其路径。例如,在第七章中,以下命令删除了一个显式文件和所有以特定扩展名结尾的文件:
del /Q C:\Source\Junk.txt C:\Source\*.OLD
以下命令在按下的键数更少的情况下完成相同的任务 如果——if 是一个关键限定词——当前目录是 *C:\Source*,即前一个命令的路径移除两次:
del /Q Junk.txt *.OLD
xcopy 命令的源参数和任何其他接受路径和文件名作为参数的命令相同。我通常更倾向于使用显式路径,以避免任何歧义,但这种技巧赋予了本章中描述的同类型的灵活性,适用于大量命令。再翻阅一遍第七章,想象所有用于复制、移动和重命名文件的命令都没有显式路径。如果解释器能够在当时的当前目录中找到特定的文件或文件,它们都会是有效的命令。
向可执行文件传递参数
在本章开始时,我演示了如何调用一个已编译的程序。在继续之前,我有一个关于这个语法的最后观察要分享。
可执行文件通常在执行时接受一个或多个参数。这些参数通过简单地将它们列在程序后面作为参数传递给程序。为了便于阅读,我将这三个参数放入了变量中:
set inFile=C:\Batch\Input.dat
set outFile=C:\Batch\Output.dat
set logFile=C:\Batch\Log.dat
%pgmMyProg% %inFile% %outFile% %logFile%
输入文件是传递给程序的第一个参数;在许多语言中,这通常被视为程序中的 args[0]。同样,输出文件是第二个参数 args[1],日志是第三个参数 args[2]。你也可以使用硬编码的值,参数可以是任何你想要的,它们不一定是文件。
摘要
执行已编译的程序一开始看起来非常基础。毕竟,你甚至不需要一个命令。但如果不了解当前目录和我在这里详细介绍的路径变量,你就无法真正理解它是如何工作的。你已经学会了解释器如何使用它们来查找可执行文件、文件和其他资源,以及管理这些重要变量内容的多种方法。
执行另一个 bat 文件类似于但不完全相同于执行已编译的程序,你将在第十章中了解这些区别。但在我深入讨论之前,你将学习标签及其在下一章中的多种重要用途,主要是它们对命令执行时间和频率的影响。
第九章:9 标签与非顺序执行

在政治领域,标签常常被误解,但在最基本的层面上,标签是一个标识符,它以尽可能少的字词简明地定义一个产品或对象。如果没有标签,商业就会停滞不前;超市会堆满满满的神秘罐头产品。晚餐吃什么?可能是豆子,也可能是南瓜派混合粉;我们打开之前无法知道。
没有标签,Batch 文件不会完全陷入这种混乱,但作为创建更复杂批处理文件的重要工具,它将缺失在你的编程工具箱中。到目前为止,本书中的每个批处理文件、代码片段和列表都是按顺序执行的。解释器逐行解释,每次先执行第一条命令,然后是第二条。这一过程会一直继续,直到发生以下两种情况之一:要么是批处理文件的最后一条命令被解释,要么是语法错误导致批处理文件崩溃。标签允许你以非顺序的方式执行 Batch 命令。在本章中,我将介绍如何在代码中进行前后分支、根据数据条件重复某些代码段,甚至创建一些 Batch 本身不包含的命令。
标签还为我提供了一个很好的机会,来讨论一个至关重要的话题:编码规范,尤其是缩进。
标签
在 Batch 中,标签就是你可能预期的那样,一个定义代码块的标签。更具体地说,批处理文件中的某个位置或点被标记为标签。标签本身不是命令,虽然它从未被执行,但你很快会发现它对于执行流程至关重要。
标签可以包含字母、数字和一些特殊字符,最重要的是,它们必须以冒号开头。奇怪的是,标签名可以包含额外的冒号,但绝不能出现在第二个位置。例如,这里有一些代码,按照它的作用进行了定义或标记,用于检查特定变量的状态:
:CheckStatus
if /i "%status%" equ "fail" > con echo Failure
if /i "%status%" equ "good" > con echo Success
类似地,这段代码处理一个非常基本的中止过程,并被标记为这样:
:Abort
echo The Process is aborting
exit /B 1
我将在第十章中讨论退出命令。现在,它仅用于退出批处理文件。
定义标签非常简单,但在深入了解标签的影响及如何使用它们之前,请允许我稍微偏离一下,甚至有点愤怒地谈谈编码规范。
缩进
许多批处理编码员拒绝给他们的代码添加缩进。我不太清楚原因,因为我所熟悉的每种编程语言都有某种约定,即使不是强制要求,也有对缩进的规范。我的最佳猜测是,批处理语言本质上缺乏对其本身的尊重,认为批处理只是一个必须尽快完成的工具性麻烦,根本不考虑可读性,更别提美学了。为了让你的批处理代码赢得应有的尊重,我建议所有命令前加上两个空格的缩进。在 if 命令的代码块(以及其他类似结构,后面会讨论)内部,缩进再加三个空格,嵌套结构则应缩进更多。
这个话题可能在讲解标签的章节中看起来有些不合时宜,但实际上,这是最理想的位置。标签应该稍微突出一点,甚至更突出。任何格式良好的文档都有部分、章节、节和/或小节,每个部分通常都有某种标题或提示——或者我敢说是 标签——通过不同的字体、字号、加粗、下划线、着色或上述几种方式的组合,使其在视觉上与其余文本区别开来。然而,在编写 bat 文件时,这些选项都不可用。我们的工具库只剩下一个重要的项目——缩进,同时也可以借助大写字母和空格。
由于标签的第一个字符总是冒号,因此我通常将冒号放在行的第二个字符位置,这样我的典型缩进就减少为一个字符。因此,当任何人,包括我自己,查看我编写的 bat 文件时,所有标签都会非常明显。我将行的第一个字符保留给 rem 命令的开始。例如,下面是一个基本的备注、标签和两个简单的命令:
rem - This code does something.
:DoSomething
set do=something
set doMore=somethingElse
标签中的冒号后的大写字母也增加了其显著性。
希望我不是把自己当作批处理编码规范的斯大林。这只是一个编码员的个人意见,实际上还有许多经过深思熟虑、与我不同的规范。重要的是代码应该易于阅读。实现这一点有很多方法,但完全不缩进肯定无法通过测试,即使这个话题让我显得有些专制。
goto 命令
既然我们已经有了一个定义代码片段的标签,那它有什么用呢?一些编码员实际上将标签当作临时备注(我想这也没问题),但标签的真正功能是指引程序流向标签下方的代码。这就是 goto 命令的作用,它按字面意思指示解释器跳转到由标签定义的代码位置。考虑以下两个命令:
goto :Abort
goto :DoSomething
goto 命令将控制转到本章前面定义的 :Abort 和 :DoSomething 标签。
好吧,这不完全正确;第一条命令将执行跳转到 abort 例程,第二个 goto 命令永远不会执行。在 bat 文件中,标签本身可以出现在 goto 命令之前或之后,但重要的是要明白,一旦执行了 goto 命令,执行不会返回到 goto 命令后的那条命令。执行一旦跳转,我们就完全受制于标签下的代码。
要跳转到定义为:Abort 的标签,你也可以使用以下命令:
goto ABORT
这里有两件事。首先,goto 命令中标签名称的冒号被省略了。我怀疑这是一个早期的错误,微软为了保持向后兼容性可能不会修复。其次,实际的标签只有 A 是大写的,但 goto 命令显示的是整个标签名称都大写。
这个例子演示了标签名称是大小写不敏感的,就像 Batch 通常一样,并且在 goto 命令中冒号是可选的。虽然解释器允许这样做,但我认为没有理由让两个标签名称有所不同,因为这只会引起混乱。保持一致性是关键。
在第八章中介绍的 call 命令也与标签一起使用,但其行为与 goto 命令非常不同。我将在第十章中回到 call 命令和这些区别。
前进分支
goto 命令将控制权或流程转向两个方向之一;一个是跳过代码的前进分支。在这个例子中,三个 echo 命令将文本输出到控制台,但只有第一个和第三个会被执行:
> con echo Before GOTO
goto :MyLabel
> con echo After GOTO
:MyLabel
> con echo After LABEL
很容易想象使用这种技术的更复杂代码。一个 goto 命令可能基于 if 命令的结果有条件地执行,而不仅仅是跳过单个 echo 命令,它可能跳过更大的一段代码。例如,如果某个文件存在或不存在,或者检测到失败,你可以跳转到将中止 bat 文件执行的代码,跳过其他所有内容。
goto 也可以作为跳出循环的工具。不幸的是,我还没有讨论循环;在第二部分中,我将详细讨论 for 命令和循环。但现在,为了理解这个逻辑,你只需要知道这个循环会根据 listOfNames 变量中列出的每个名称执行一次,不管它包含多少个名称:
for %%n in (%listOfNames%) do (
if /i "%%n" equ "Waldo" goto :FoundName
)
> con echo ** Name Not Found **
:FoundName
if 命令在搜索特定名称。如果找到了,它会让 goto 命令跳出循环,跳转到最后一行的标签。
这很重要,原因有两个。首先,它是高效的——如果名字出现在列表的前面,CPU 不会浪费周期去无谓地搜索列表的其余部分。更重要的是,如果名字被找到,echo 命令就不会被执行。请注意,逻辑不仅提前跳出了循环,而且还跳过了写出未找到名字的消息的部分。
向后跳转
上一节中的示例使用了 goto 命令来跳过代码的前进。接下来,我将展示一些 goto 命令反向跳转的示例。但首先,我已经讨论了如何构建一些更现代语言的组件,这些组件并不是批处理语言的一部分(比如布尔值和浮点数),但很多其他组件还未出现。批处理没有 while 命令,也不支持 do...while 命令。在其他语言中,while 命令会根据条件执行一段代码零次或多次,直到条件满足。do...while 命令也非常类似;唯一的区别是,在评估条件之前,代码块会执行一次。让我们在批处理中创建这两种命令。
while “命令”
为了演示批处理 while 命令的有用性,我们将编写一些代码,去除值的所有前导零,这对于任何不想意外执行八进制算术的程序员来说是必要的(如果你错过了,可以在第六章找到详细解释)。while 命令会执行一段代码,只要——或者 当——第一个字节是 0,并且这段代码将仅仅做一件事:去除一个前导字节。
以下代码完美地执行了任务:
:StripLead0s
if "%nbr:~0,1%" equ "0" (
if "%nbr:~1,2%" neq "" (
set nbr=%nbr:~1%
goto :StripLead0s
) )
解释器在第一次遇到标签时,基本上会忽略它,并检查 nbr 的第一个字符。如果是 0,接下来代码会验证是否有第二个字节——也就是说,0 是否确实位于某个值的前面。如果两者都成立,代码块会被执行,它会移除前导 0,然后 goto 命令将控制权传回到 if 命令之前的标签。
让我们通过三个不同的数字逐步查看代码,真正理解其逻辑。如果变量没有前导 0,则代码块永远不会执行。如果有一个前导 0,则代码块执行一次。然后,再次检查前导字节,由于它不再是 0,执行流程将继续执行后续的内容。如果 nbr 有 17 个前导 0,移除 0 的代码块会执行 17 次,在前导字节第 18 次检查后,执行流程继续向下进行。
在这个列表中并没有出现 while 这个词,但它做的事情和一个标准的 while 命令一样。就我而言,它就是一个批处理中的 while 命令。
注意
前面的代码片段是我第一次展示一个 if 命令嵌套在另一个命令中的例子,但在接下来的章节中,你将看到更多的嵌套命令。关于编码规范的另一个说明,我在那个列表中将两个尾随的右括号堆叠在同一行。这使得代码更紧凑,尤其是在嵌套多个层级时,也使得关注点集中在有趣的逻辑上。不过我承认,我可能是少数派。大多数 Batch 程序员会将每个右括号与其各自的 if 命令对齐。这需要更多的代码行,但以下代码在功能上等同于我之前的代码:
:StripLead0s
if "%nbr:~0,1%" equ "0" (
if "%nbr:~1,2%" neq "" (
set nbr=%nbr:~1%
goto :StripLead0s
)
)
做自己觉得对的事情,并坚持去做。另外,请注意标签名称包含了一个数字值。如前所述,我们并不局限于字母。顺便说一下,那个缩进看起来不错吧?
do...while “命令”
Batch 的 do...while 命令看起来与 while 命令非常相似;唯一的区别在于主逻辑必须至少执行一次。在具有内建 do...while 命令的语言中,条件子句通常位于结构的尾部(可以理解为在主逻辑执行一次后),Batch 也不例外。与 while 命令相比,主逻辑从 if 命令代码块内部移到了标签之后、if 命令之前的位置。
为了演示,让我们以一个例子为例,其中 textStr 变量需要右填充至少一个空格,以便将其扩展到最少 25 字节的长度。如果原始字符串的长度小于 25 字节,结果将是 25 字节;如果它本身已经至少 25 字节长,则会在结果中附加一个空格。(这个字符串可能是某些连接文本的一部分,用于在控制台上显示,其中空格填充将对齐列。当然,我们希望它和接下来的内容之间有一个空格,即使这需要额外的一个字节。)
右填充必须至少执行一次,这使得 do...while 命令非常适用:
:PadRight
set textStr=%textStr% &
if "%textStr:~24,1%" equ "" goto :PadRight
与 while 命令一样,标签位于大部分代码之前,但核心逻辑紧随其后,在这种情况下是一个单独的 set 命令,用一个空格填充字符串。然后检查第 25 个字节。(记住,它是零偏移。)如果不存在,goto 命令会将执行返回到标签处,以便可以向字符串中添加另一个空格。这个过程会一直重复,直到第 25 个字节被填充,确保字符串至少为 25 字节长,并且无论长度如何,都至少添加了一个空格。
在第二十六章中,我将详细介绍如何执行自动重启失败的进程,通常只需使用一个标签和 goto 命令—也就是 do...while 命令—就能使进程成功重启。
:eof 标签
一个不是由程序员创建但对所有 bat 文件内在存在的特殊标签是 :eof,它代表 文件结束。当以下的 goto :eof 命令在被调用的 bat 文件的主逻辑中执行时,控制权会返回到调用的 bat 文件:
> con echo We are about to exit the bat file.
goto :eof
> con echo This command will never be executed.
在高级 bat 文件中执行相同的命令会完全停止过程,即使 bat 文件中没有定义 :eof 标签。
如果你本性反其道而决定定义自己的 :eof 标签,解释器将简单地忽略它,仿佛它是一个无意义的备注。在第十章中,我将进一步探讨这个独特的标签,特别是解释器如何在可调用例程内部处理 goto :eof 命令。
变量标签
在没有编译器的语言中工作确实有一些缺点,但我已经向你展示了一些“银 lining”(例如延迟扩展)。另一个优点是能够在执行时在 goto 命令中定义标签名,尽管标签本身必须是硬编码的。为了设置这个,设想为每个月定义一个不同的标签。这里显示了前三个标签,但它们下方没有与月份相关的代码:
:MonthJanuary
:MonthFebruary
:MonthMarch
显然,以下命令将把执行指向前面代码片段中的某个特定标签:
goto :MonthFebruary
但现在这已经是陈旧的消息了。更有趣的是,如果变量 month 被设置为二月,以下命令将调用相同的标签:
goto :Month%month%
这个 goto 命令的参数是硬编码的 :Month 和 month 变量的值的拼接。在变量被解析后,命令将执行转向标签 :MonthFebruary。同样的道理也适用于其他有效的月份,这意味着如果 month 被设置为三月,这行代码也会跳转到 :MonthMarch。
但这确实引出了一个问题,那就是当生成的标签名在 bat 文件中不存在时会发生什么,比如,如果 month 被设置为 Erele(在约鲁巴语中表示二月)。解释器会向控制台写入以下消息:
The system cannot find the batch label specified - MonthErele
不幸的是,你永远不会看到此消息,因为过程会立即崩溃。
在第十章中,你将看到当与 call 命令一起使用时,Batch 可以更好地处理错误的标签名,但如果你使用这种技术与 goto 命令,确保该参数解析为有效的标签。
总结
在本章中,我介绍了标签的概念以及如何通过 goto 命令导航到它们。你学习了如何创建标签,探索了它们的使用技巧,并了解了它们在构建 while 和 do...while 命令中的重要作用。我还介绍了不可或缺的 :eof 标签。
但是你可以通过两种不同的方式导航到标签。下一章的大部分内容也将集中在标签上,探讨如何使用它们在 bat 文件内部创建可调用的例程。我还将详细说明如何从一个 bat 文件调用另一个 bat 文件,这是在你开始创建过于复杂的项目时一个至关重要的话题。
第十章:10 调用例程和批处理文件

在上一章中,我介绍了标签和非顺序执行,它们在本章中也发挥了重要作用。我将很快介绍一种已经讨论过的命令的新变化,使你能够创建并调用由标签定义的例程。控制权不会仅仅交给标签后面的代码,而是在例程执行后返回到调用它的地方。当你编写更复杂、更有趣的批处理文件时,你将希望充分理解例程。
在第八章中,我介绍了调用用其他语言编译的可执行文件的概念。这里我将扩展这一讨论,描述一个批处理文件调用另一个批处理文件的不同技巧。显然,你将了解最典型的调用方式,即将控制权返回给调用批处理文件的方式。但你也会学到将控制权交给被调用批处理文件的技巧,以及如何生成一个并行的第二个批处理进程。此外,你还将探索不同的退出例程或批处理文件的方式,无论是否带有返回代码。
调用命令,再探讨
在你创建可调用的内部例程之前,你必须了解两个与标签相关的命令的相似性和差异性。一个是第八章中首次介绍的 call 命令,我们用它来调用用其他语言编译的程序。另一个是第九章中介绍的 goto 命令,用于改变批处理文件的执行流程。
为了对比这两个命令,回顾第九章中的这段代码:
> con echo Before GOTO
goto :MyLabel
> con echo After GOTO
:MyLabel
> con echo After LABEL
goto 命令跳过了中间的 echo 命令,导致输出结果如下:
Before GOTO
After LABEL
为了演示两者的对比,清单 10-1 将代码中的每个 goto 实例替换为 call,包括 goto 命令和 echo 命令中的文本,同时保持此非常简洁的批处理文件中的其他内容不变。
> con echo Before CALL
call :MyLabel
> con echo After CALL
:MyLabel
> con echo After LABEL
清单 10-1:演示 call 命令的简短批处理文件
执行清单 10-1 中的批处理文件,你会在控制台上看到清单 10-2 中显示的四行内容,而不是某些人预期的三行。
Before CALL
After LABEL
After CALL
After LABEL
清单 10-2:执行清单 10-1 时写入控制台的结果
“Before CALL”的显示显然会立即执行(双关含义)。call 命令暂时将控制权交给标签后面的代码,导致显示“After LABEL”。当时如果是 goto 命令,那就结束了;批处理文件在那次显示之后就终止了。但是使用 call 命令时,在执行完:MyLabel 和批处理文件结尾之间的所有内容后,控制权会返回到 call 命令后面紧接的命令。因此,显示“After CALL”。
有些人可能会认为执行到此为止,但解释器接下来再次遇到 :MyLabel。我们没有调用它,也没有跳转到它;它只是代码的下一行。请注意,我没有称它为命令或语句。它只是代码的一行,占位符,在这个上下文中,除了是通向下一个命令的微妙“减速带”外,几乎没有其他意义。解释器继续执行 bat 文件中的最后一行,并且 After LABEL 文本第二次显示。解释器没有找到其他命令需要解释,bat 文件执行完毕。
虽然 goto 命令放弃了控制权,但 call 命令会记住它来自何处,并在完成任务后返回到该位置。现在我们已经具备了可调用的内部例程,我们将通过 call 命令调用该例程。
调用内部例程
当你的 Batch 代码变得更加复杂时,你可能会想从 bat 文件中的不同位置多次执行某段代码。例如,你可能想多次调用一个可执行文件,或者定期检查某个目录中是否有需要复制的文件。当我们进入交互式 Batch 时,你可能会想多次询问用户问题并获取响应。
面对需要多次调用某段代码的需求,一位新手程序员可能会 resort to cut and paste——在我极为评判(但准确)的看法中,这是一种令人厌恶的选择。一个更好的解决方案是创建一个内部例程,并从多个位置调用它。你甚至可以将一些只调用一次的代码放入一个例程中,便于更好地组织你的 bat 文件。有时直接通过标签运行是完全可以的,但更多时候,你可能希望创建一个只能通过调用才会执行的例程。
对于接下来的练习,我将以清单 10-1 为例,重新配置它,使得标签定义了一个可调用的例程。也就是说,执行流程会调用该例程,执行完毕后返回,再退出 bat 文件,避免再次执行该例程。为此,我需要一种方法来终止例程和 bat 文件。在清单 10-2 中,After LABEL 显示的内容将不再出现。相反,我们将期待以下三行输出:
Before CALL
After LABEL
After CALL
以下代码,尽管看起来与之前的不同,但正是做了这件事:
> con echo Before CALL
❶ call :MyLabel
> con echo After CALL
❷ goto :eof & rem End of TestCall.bat
❸ :MyLabel
> con echo After LABEL
❹ goto :eof & rem End of :MyLabel
❺ :AnotherLabel
> con echo This is Never Executed
❻ goto :eof & rem End of :AnotherLabel
在逐步执行代码之前,请注意这三条 goto :eof 命令。正如你所预料的,第一个 ❷ 跳转到文件末尾,终止 bat 文件。其他两条 ❹ ❻ 则是完全不同的——是一些新的内容。
在初始的 echo 命令之后,call 命令 ❶ 调用了 :MyLabel ❸ 定义的例程,该例程仅包含两条命令。第一条是熟悉的将 “After LABEL” 打印到控制台,第二条是一个 goto :eof 命令 ❹。因为该命令在标签调用后执行,所以它结束的不是文件而是例程,控制会返回到 call 命令 ❶ 之后的命令,打印 “After CALL” 到控制台。最后,主 goto :eof 命令 ❷ 退出了 bat 文件,因为解释器知道它不在例程中。
当在 :MyLabel ❸ 例程内部时,跳转到 :eof(或 文件结束)是一个误称;它实际上更像是 例程结束,但我们不必在语义上纠缠。如果去掉这个 goto :eof 命令 ❹,控制会继续执行到 :AnotherLabel ❺ 下的代码,然后再返回主线逻辑。但有了这个命令 ❹,则 :AnotherLabel 下的代码永远不会执行。
由于 goto :eof 命令有两种不同的用途,我通常会在此类命令后添加注释,明确指出它正在终止的内容,可能是例程的名称或是 bat 文件本身。我只是将 rem 命令放在一个 & 符号后面,& 符号将两条命令分开,写在同一行代码中。从程序角度看,这并不是必须的,但这种做法确实大大提高了代码的可读性,尤其是在例程变得比之前的示例更长、更复杂时。
调用 Bat 文件
短小或重复的代码片段非常适合放入内部例程中;你可以在 bat 文件的末尾添加一个或多个例程,创建一个结构良好的模块,你可以为此感到自豪。但有时这些短小的代码片段并不那么简短,或者它们非常有用,以至于你希望将它们提供给其他由你编写的,甚至是其他人编写的 bat 文件。在这种情况下,应该用一个 bat 文件调用另一个 bat 文件,而不是创建一个例程。例如,你可以创建一个单独的 bat 文件来处理日志记录,并从多个其他 bat 文件中调用它。
从另一个 bat 文件执行一个 bat 文件的方式与执行内部例程有些不同。但首先,让我们回到 第八章 中如何执行编译程序。当解释器遇到一行仅包含可执行文件名的代码时,它会调用该可执行文件。因此,这条“命令”执行了程序:
C:\Executables\CompiledProg.exe
程序完成任务后,控制会返回到 bat 文件。你可能期望调用一个 bat 文件的方式与此相同,但实际上并非如此。然而,以下这一行代码确实执行了被调用的 bat 文件,但存在一个巨大的警告:
C:\Batch\CalledBat.bat
那个 bat 文件的特点很简单:控制永远不会返回到调用它的 bat 文件。整个过程在被调用的 bat 文件结束时就结束了。绝大多数情况下,你都希望控制能够返回;否则,在调用 bat 文件后继续编码就没有什么意义了。为了看到控制返回,你可以在被调用的 bat 文件之前插入 call 命令:
call C:\Batch\CalledBat.bat
总结一下,无论是调用 bat 文件还是调用其他语言编译的可执行文件,你都可以使用 call 命令或者省略它,但它们是有区别的。调用可执行文件时,这两种方法几乎是相同的。而调用另一个 bat 文件时,call 命令确保控制能够返回给调用者。如果没有这个命令,控制将永远不会返回。
由于我从未找到过不返回的 bat 文件调用的用途,我的偏好一直是对于可执行文件省略 call 命令,对于 bat 文件使用它。一个优点是,一眼就能看出被调用的是哪种类型的文件。
在我的职业生涯初期,我通过一次艰难的经历学到了调用命令(call command)在 bat 文件中的必要性,那时我无法理解为什么我的 bat 文件停止执行。没有挂起或中止的消息,它就是停止了。更复杂的是,我的故障排除自然集中在被调用的 bat 文件上。过了很久,我才注意到缺少了 call 命令,更重要的是,才理解了它的重要性。但这并不是关于 call 命令的唯一特性。
关于调用标签的备注
在上一章中,我提到过,在 goto 命令的参数中,标签名称后面的冒号是可以省略的,尽管强烈建议包含它。而在 call 命令中,调用定义内部例程的标签时,冒号是始终需要的。
这个明显的不一致可能无法理解,直到你考虑到 goto 命令只关心跳转到它自己 bat 文件中的标签,而 call 命令则能调用它自己 bat 文件内外的实体。结果就是,当没有冒号的情况下尝试调用 :MyLabel 时,会发生一些非常意外的事情:
call MyLabel
冒号本应告诉解释器调用一个内部例程,但解释器却试图调用一个外部文件。首先,它会在当前目录中查找一个可执行文件,例如 MyLabel.com 或 MyLabel.exe。接着它会查找 MyLabel.bat 以及一些其他类型的可执行文件,仍然在当前目录中。然后它会在路径变量中的所有目录中查找任何名为 MyLabel 的可执行文件。如果没有找到这样的文件,解释器就不会再查找该名称的标签,即使 :MyLabel 是 bat 文件中的有效标签;相反,它会产生一个错误。
在使用 goto 或 call 命令跳转到标签时,始终使用冒号,至少为了保持一致性。
注意
在第九章中,我提到过,当找不到标签时,goto 命令会中止进程。call 命令则宽容一些。当其参数是无效标签时,二者都会输出错误信息,但 call 命令还会将 errorlevel 设置为 1。如果你选择不检查返回码,进程会继续执行,就像什么都没发生一样。(有关如何处理失败的 call 命令的更多细节,请参见第二十八章。)
启动 Bat 文件
有时你可能希望将 bat 文件作为新进程启动或生成。也就是说,你可能希望启动另一个 bat 文件,但不希望解释器在继续之前等待它完成。例如,你可以并行执行多个进程,从而加速整体处理时间。你还可以启动一个非关键但耗时的任务,可能是一个日志记录过程,让它自行执行。在第二十六章中,我将讨论如何自动终止和重启一个挂起的进程。为了实现这一点,我将把容易挂起的进程生成一个独立的 bat 文件,并从主 bat 文件中监控它。
要启动或生成 bat 文件,只需使用 start 命令替代 call 命令:
start C:\Batch\LaunchedBat.bat
该命令创建了第二个命令或 DOS 窗口,LaunchedBat.bat文件与启动它的 bat 文件同时执行。
exit 命令
如你所料,exit 命令会退出例程、bat 文件或整个执行过程,甚至可以设置返回码。它的功能与 goto :eof 命令重叠,但我很快会展示出一个显著的区别。
不带参数的 exit 命令会突然结束整个进程。遗憾的是,第二个 echo 命令将不会被执行:
> con echo The Meaning of Life is...
exit
> con echo ... %meaningOfLife%
第一个 echo 命令将消息输出到控制台,但 exit 命令会在你阅读之前关闭窗口。无论 exit 命令在哪里调用——无论是在高层 bat 文件中、在调用的 bat 文件中,还是在任何类型的 bat 文件中的例程——都会发生这种情况。此命令的变体类似于使用大锤。
然而,/B 选项将 exit 命令变成了更像一只珠宝锤。文档中没有明确说明 B 代表什么,但对我来说,它代表break,即后续命令仅中断被调用的代码,无论是调用的 bat 文件,还是 bat 文件内部的例程:
exit /B
该命令仅在高层 bat 文件的主逻辑中调用时才会退出整个进程。它不会更改 errorlevel,逻辑上等同于 goto :eof。两个命令都是有效的,使用哪个通常取决于个人偏好。我的偏好是 goto :eof 命令,但仅在不需要返回码的情况下。
在第九章的开头,我提到了清单 10-3 中复现的基本中止逻辑,但将解释留到后面,现在就是解释的时机。
:Abort
echo The Process is aborting
exit /B 1
清单 10-3:一个标记为:Abort 的中止例程
这个exit命令的行为与exit /B非常相似,唯一的例外是,当控制权返回到代码被调用的位置时,跟随选项的命令数字参数将成为errorlevel中的新值。简而言之,这个命令会中断批处理文件或例程,并返回退出或返回代码。在之前的示例中,返回代码是 1。如果没有检测到错误,批处理文件的主逻辑可能通过将返回代码设置为 0 来结束:
exit /B 0
如果检测到致命错误,主逻辑中的goto :Abort命令将引导解释器进入清单 10-3 中显示的终止逻辑。必须使用goto命令,因为call命令会将终止逻辑当作被调用的例程;错误级别会被设置,但控制权会返回到致命错误发生的地方。而当通过goto命令导航到标签时,并不会调用例程;它仍然被认为是在主逻辑中,exit命令会结束批处理文件,而不是调用一个例程。
为了增加灵活性,你可以为退出代码创建一个变量,并为不同的失败设置不同的值:
:Abort
echo The Process is Aborting
exit /B %exitCode%
然后,这段逻辑可以通过多个goto命令在批处理文件中进行访问。
(一个真实的终止例程会比这个简单的回显命令更有趣。错误信息可能包含多行内容并包含变量,还会被写入日志文件和控制台,但为了保持对退出命令的关注,我在这里进行了简化。)
总结
在本章中,我详细介绍了调用内部例程和其他批处理文件的不同方法。你已经学会了如何带或不带返回代码从这些调用中返回,或者如何从任何地方直接中止整个过程。你还学会了如何启动或生成另一个与第一个批处理文件完全独立的批处理文件。最重要的是,你现在理解了goto和call命令之间重要而微妙的差异。简而言之,call会返回控制权并可以访问其外部,而goto则没有这种能力。
这个谜题中仍然有一个大块未解。一个调用的批处理文件可以将多个参数传递给被调用的批处理文件,而且被调用的批处理文件甚至可以设置并返回参数。这个过程比人们预期的要复杂,我将在接下来的章节中详细讲解所有的细节。
第十一章:11 参数和参数传递

在前一章中,我演示了如何通过 bat 文件调用内部例程和其他 bat 文件,但我没有讨论如何在调用逻辑和被调用逻辑之间传递数据。默认情况下,所有在调用代码中设置的变量都对被调用代码可见,反之亦然。如果代码紧密耦合,从技术上讲,不需要传递参数和接受参数,但要使其生效,两个代码集必须达成一致并使用相同的变量集。
如果您只是创建一个第二个 bat 文件来拆分一个大型项目,并且被调用的 bat 文件永远不会从其他地方调用,那么这样做是可以接受的。但为了创建更通用的代码,使得其他过程和程序员可以重复使用,参数化传递给 bat 文件和例程的数据是至关重要的。
在本章中,我将详细介绍您需要了解的所有关于参数和参数传递的知识,包括 Batch 独有的晦涩语法。您将学习如何将参数传递给 bat 文件或例程,以及如何接受返回的参数。您甚至会了解隐藏参数、如何以及为什么要调整参数,以及如何通过几次鼠标点击将参数传递给 bat 文件。
传递参数
为了演示如何传递参数和接受参数,我将编写一个简短的 bat 文件,构建一个简单的 Mad Libs 示例,这是一个在便携式电子设备普及之前,让孩子们在长时间车程中保持耐心的游戏。这个 bat 文件接受三个顺序参数(一个形容词,一个动词和一个名词),并将它们插入以下文本中,然后显示结果给用户:
蝙蝠是 _______(形容词)哺乳动物。它们在洞穴中 _______(动词)飞来飞去,但如果你站在它们下方,你可能会被 _______(名词)砸中。
我还不打算与您分享这个调用的 bat 文件,因为我首先会集中讲解如何在传递这三个参数时调用MadLibs.bat。毕竟,调用的 bat 文件并不关心香肠是如何做出来的;它只需要一个绞肉机。调用的 bat 文件只需要知道要传递的参数和预期的结果,仅此而已。(在下一部分,我将从被调用的 bat 文件 MadLibs.bat 的角度来分析这个问题。)
在第十章中,我演示了如何使用 call 命令调用另一个 bat 文件。接下来的命令正是如此操作的,不过现在,bat 文件名后面跟着三个参数,分别是形容词、动词和名词,按照这个顺序,且每个参数之间用空格分隔:
call C:\Batch\MadLibs.bat adorable fly guano
显然有人在作弊,因为写入控制台的结果对这个游戏来说太合理了:
Bats are adorable mammals. They fly around in caves,
but if you stand under them, you might get hit with guano.
更典型的情况是,如果一个 11 岁的男孩玩这个游戏,他可能会想到以下一组代表形容词、动词和名词的词:
call C:\Batch\MadLibs.bat stinky fart poop
结果是
Bats are stinky mammals. They fart around in caves,
but if you stand under them, you might get hit with poop.
这种方式不知为何仍然有效——至少对于一个青少年男孩来说是这样。
参数分隔符
在前面的示例中,传递的参数通过空格彼此分隔,并且与被调用的批处理文件名分开。空格无疑是最常见的分隔符,逗号是第二常用的分隔符,但分号、等号和制表符也可以作为分隔符。考虑以下两个调用命令:
call C:\Batch\MadLibs.bat adorable fly guano
call C:\Batch\MadLibs.bat,adorable;fly=guano
两个命令在功能上是等效的,但第二个命令看起来像是故意混淆其意图的练习。
逗号分隔的数据是相当常见的(例如.csv文件的内容)。因为逗号是批处理参数分隔符的一部分,你可以将逗号分隔的数据存储在一个变量中,然后将该变量作为命令的一部分传递,像这样:
set myArgs=adorable,fly,guano
call C:\Batch\MadLibs.bat %myArgs%
解释器将每一段由逗号分隔的文本视为一个独立的参数(假设数据中没有其他分隔符)。这看起来可能像是一个参数,但实际上命令传递了三个参数。
参数封装
查看允许的参数分隔符列表会引发一个有趣的问题:是否可以将空格和其他分隔符作为实际的参数数据传递?是的,这完全可能,但首先要考虑以下这些(三个?)参数所带来的问题:
call C:\Batch\MadLibs.bat ad hominem took off ice cream
显然有人在故意制造麻烦,使用三个参数的双词版本。(而任何把“ad hominem”作为《疯狂填字游戏》形容词使用的人,其实比制造麻烦还更为做作。他可能是办公室里那个纠正每个人使用“参数”和“论点”错误的人。)结果是,解释器需要处理六个参数,而不是三个。
由于空格是分隔参数的字符之一,解释器将“ad”视为形容词参数,而动词参数是“hominem”,名词参数是“took”。这导致了一个毫无意义的词组,即便是按照《疯狂填字游戏》的标准来看也显得荒谬。命令的其余部分“off ice cream”变成了一个接收最多三个参数的批处理文件的第四、第五和第六个参数;它们被适当忽略,不会造成进一步的影响。
解决这个问题的方法是将每个参数用双引号括起来,这样做还有一个额外的好处,就是可读性大大提高:
call C:\Batch\MadLibs.bat "ad hominem" "took off" "ice cream"
被调用的批处理文件需要处理可能被双引号包裹的参数,稍后我会详细讲解。
通过这些设置,输出至少在语法上是有意义的,大致正确:
Bats are ad hominem mammals. They took off around in caves,
but if you stand under them, you might get hit with ice cream.
包裹的双引号提供了另一个巨大优势;它们能够占据任何缺失参数或设置为空格的参数的位置。例如,作为一种反向思维,某人可能拒绝提供第一个参数。(毕竟,省略形容词仍然能保持语法正确,但省略名词或动词必定会破坏句子结构。)如果没有双引号,在以下示例中,第二个参数会偏移成为第一个,第三个则变成第二个。相反,即使第一个参数为空,依然传递了三个参数:
call C:\Batch\MadLibs.bat "" "hop" "fudge"
hop 和 fudge 参数正确映射到 verb 和 noun,而没有 adjective,因此输出如下:
Bats are mammals. They hop around in caves,
but if you stand under them, you might get hit with fudge.
双引号的包围还允许你将逗号、分号、等号甚至制表符作为参数或参数的一部分使用。
参数变量
到目前为止,我只展示了硬编码的参数,但实际上,参数通常是变量。如果你不能百分百确定没有(并且永远不会有)嵌入的空格,建议你在解析后的变量周围加上双引号:
call C:\Batch\MadLibs.bat "%arg0%" "%arg1%" "%arg2%"
当调用命令执行时,三个参数变量会被解析并传递给被调用的 bat 文件,每个参数都被双引号包围。
注意
“参数与论据”的争论:关于这两个术语之间的区别有一些争议。我见过不同的定义,但我在写作时使用的是这样一个区分: 论据 是从 调用 代码传递的, 参数 是被 被调用 代码接受的。但也有灰色地带,程序员常常谈论“传递参数”和“接收论据”。我们都知道什么意思。我不想成为那个在办公室里纠正每个人的刻板讨厌家伙,但我会尽量保持这些词的一致使用。
接受参数
让我们通过 180 度转变视角,来看一下示例 11-1,在这个示例中,被调用的 bat 文件接受三个参数,这意味着最终共享 MadLibs.bat 文件,该文件在前面的示例中生成了多轮输出。
set adjective=%~1
set verb=%~2
set noun=%~3
> con echo.
> con echo Bats are %adjective% mammals. They %verb% around in caves,
> con echo but if you stand under them, you might get hit with %noun%.
goto :eof
示例 11-1:MadLibs.bat 文件接受三个参数并显示一个 Mad Lib。
任何接受参数的 bat 文件应该以两种方式之一开始。它应该包含说明 bat 文件接受哪些参数的注释,或者它应该在文件顶部使用 set 命令定义这些参数,并使用明确命名的变量。对于这个 bat 文件,我选择了后一种方式。
第一个参数被描述性地命名为 adjective,而 %~1 是解析传递给 bat 文件的第一个参数的最佳语法。递增该整数可以得到第二个参数的值 %~2,并将其赋值给 verb。最后,另一个明确命名的变量 noun 被赋值为第三个参数 %~3。
解析带波浪号的参数
下一个要点既重要又微妙;如果你从 bat 文件的第一行中去掉波浪号,%1 会解析为接收到的第一个参数,无论是否有双引号。然而,%~1 会解析为去掉双引号的第一个参数——如果没有双引号需要去掉,解析后的参数保持不变,因此波浪号不会带来任何问题。其他参数也一样:%~2 解析为去掉双引号的第二个参数,%2 则解析为传递的原始参数。
波浪号的使用正是我在前一节中提到的内容,指的是被调用的批处理文件需要做些什么,以便处理可能被或可能不被双引号括起来的参数。MadLibs.bat文件显然使用了波浪号的语法,使它能够同时兼容两种情况,从而给调用批处理文件的开发者提供了便利。
我唯一不会使用波浪号来解析参数的情况是当我明确希望保留双引号时,而这种情况极为罕见。使用波浪号可以让调用的批处理文件灵活地选择是否使用双引号。请考虑以下这两个功能上等价的命令:
call C:\Batch\MadLibs.bat ugly running "cell phone"
call C:\Batch\MadLibs.bat "ugly" "running" "cell phone"
调用的批处理文件必须使用双引号来处理任何包含空格的参数,例如上面提到的第三个参数;否则,双引号不是必需的,但使用它们不会有害。我提到这两个命令在功能上是等价的,但如果MadLibs.bat没有在前面三个命令中使用波浪号,那就不成立了。
如果你觉得解析参数的语法看起来很奇怪,那你并不孤单。变量通常通过两个百分号来解析,但在这里,单个百分号后面跟着一个一位数(或波浪号和数字),别无他物。以数字开头的变量不能通过百分号解析,因此当解释器看到百分号后跟着一个数字(可能中间有一个波浪号)时,就会认为这是一个参数。一旦理解了,它确实提供了一个非常简洁的语法来解析参数。
解析整个参数列表
你可以通过在参数列表前加上百分号和星号,轻松解析整个参数列表,无论值的数量如何,这在被调用的批处理文件需要调用另一个批处理文件或例程并传递相同的参数列表时非常方便。考虑以下命令,特别是末尾的%*字符:
call C:\Batch\SecondCalledBat.bat "%arg0%" %*
该命令传递了一个由简单变量组成的参数列表,后面跟着传递到批处理文件或例程的参数列表。我已将第一个参数,即 arg0 变量,用双引号括起来,以防它包含任何嵌入的分隔符字符,如空格;这确保了解释器将该变量视为单一参数,而不是多个参数。其余的参数是传递到被调用的批处理文件的完整参数集,无论数量如何。最终,传递到批处理文件的第一个参数是调用中的第二个参数,以此类推。(剧透:当我们在第三十二章讲解面向对象设计时,我们将广泛使用这项技术。)
内部例程参数
到目前为止,我只讨论了与调用另一个 bat 文件相关的参数。幸运的是,在调用 bat 文件内部的例程时,实际上并没有什么不同。我们可以轻松地将列表 11-1 中的完整 bat 文件重写为一个内部例程。请注意,在列表 11-2 中,唯一的区别是添加了标签。
:MadLibs
set adjective=%~1
set verb=%~2
set noun=%~3
> con echo.
> con echo Bats are %adjective% mammals. They %verb% around in caves,
> con echo but if you stand under them, you might get hit with %noun%.
goto :eof
列表 11-2::MadLibs 例程接受三个参数并显示一个 Mad Lib。
调用例程的 call 命令具有相同的参数,并且双引号的处理方式也相同:
call :MadLibs "ad hominem" "took off" "ice cream"
唯一的区别是调用了一个标签,而不是另一个 bat 文件。其他一切都完全一样。
隐藏参数
许多现代语言将传入的参数列表视为一个数组,特别是一个零偏移的数组。再加上批处理文件意外地使用零偏移来进行子字符串提取,许多程序员曾经尝试在 %~0 中寻找一个参数(包括我自己)。结果可能让人困惑,正如在列表 11-2 中展示的,添加以下 echo 命令来说明这一点:
:MadLibs
> con echo Parm 0 is %~0
set adjective=%~1
写入控制台的文本包含正在执行的例程的名称:
Parm 0 is :MadLibs
在我解释到底发生了什么之前,章节前面提到过,Mad Libs 的逻辑在一个外部文件中,并像这样被调用:
call C:\Batch\MadLibs.bat "adorable" "fly" "guano"
将之前添加到 :MadLibs 例程中的相同 echo 命令,添加到列表 11-1 中的被调用 bat 文件中,结果如下:
Parm 0 is C:\Batch\MadLibs.bat
我不太愿意称 %0 为第一个参数。它更像是一个隐藏参数,位于第一个参数之前。从这个例子来看,很明显隐藏的参数是正在执行的例程或 bat 文件的名称,但它来自哪里呢?
一个 bat 文件通常是 call 命令的第一个参数,并且这个参数会作为 %0 传递给被调用的 bat 文件本身。事实上,%0 匹配的是 call 命令中路径和文件名(或标签名)的大小写,而不是实际路径和文件名的大小写。如果路径不是参数的一部分,那么它就不是 %0 的一部分。此外,如果 call 命令中路径和文件名被双引号括起来,那么 %0 也会被双引号括起来。因此,通常最好像处理其他参数一样使用波浪符号来解析它,即 %~0。
隐藏的参数在所有 bat 文件中都无处不在,不仅仅是被调用的 bat 文件或例程。即使是一个高层次的 bat 文件,那个你可能通过双击打开的文件,其路径和文件名也包含在隐藏的参数中。我们通常不会考虑打开 bat 文件时发生了什么,但这并不是魔法。Windows 执行一个 call 命令,并将 bat 文件作为唯一参数传递给你。结果是 %0 被解析为 bat 文件的路径和名称,并被双引号括起来。
这个隐藏参数有很多用途。对于错误处理,它提供了一种简单的方式来记录错误发生的例程。如果 bat 文件被移动到不同的目录、计算机或域中,隐藏参数可以帮助 bat 文件知道它的位置。当我们介绍到 for 命令时,有一种相对简单的方法可以从一个包含路径和文件名的变量中仅获取路径。然后,你可以以多种方式使用它。例如,你可以将输出文件放入 bat 文件所在目录下的子文件夹,或者 bat 文件可以根据它所处的位置执行不同的操作。它是 Batch 中一个非常有用且隐藏的特性。
移动参数
如果第一个参数通过%~1 解析,第九个参数通过%~9 解析,那么第 10 个参数通过%~10 解析似乎是合理的。但实际上不行,解释器只识别参数 0 到 9,或者单个数字的序数。要是编译器能够温和地提醒你%~10 不是有效参数那该多好,但同样的,这就是 Batch。为了演示在解析这个参数时可能出现的问题,可以参考示例 11-3 中的一个双数字参数列表。例程传递给它的是字母表的前一半,目的是将五个选定的字母写入控制台。
call :Alphabet A B C D E F G H I J K L M
goto :eof
:Alphabet
> con echo Parm 1 is "%~1"
> con echo Parm 2 is "%~2"
> con echo Parm 9 is "%~9"
> con echo Parm 10 is "%~10"
> con echo Parm 13 is "%~13"
goto :eof
示例 11-3:第一次尝试编写例程:Alphabet,以显示五个参数
前三个看起来没问题,但第 10 和第 13 个参数似乎是无效的:
Parm 1 is "A"
Parm 2 is "B"
Parm 9 is "I"
Parm 10 is "A0"
Parm 13 is "A3"
当我们人类看到%~13 时,我们看到的是数字 13,可能会期待第 13 个参数 M 被解析出来。但解释器从来没有被误认为是人工智能,更不可能是人类智慧。当它遇到%~13 时,它看到的是第一个参数%~1,解析为 A,后面跟着硬编码的值 3,结果是 A3。类似地,%~10 会解析为第一个参数,但会附加一个零,结果是 A0。
然而,Batch 并不限于只有九个参数。事实上,我曾见过传递了几十个参数,因为没有实际的限制(除了任何命令中最大字符数为 8,191 的限制)。要访问第 10 个及以后的参数,你需要使用 shift 命令。为了演示,让我们修正示例 11-3 中的:Alphabet 例程:
:Alphabet
> con echo Parm 1 is "%~1"
> con echo Parm 2 is "%~2"
> con echo Parm 9 is "%~9"
shift
> con echo Parm 10 is "%~9"
shift & shift & shift
> con echo Parm 13 is "%~9"
goto :eof
在 shift 命令执行之前,第 1、2 和 9 号参数的解析结果没有变化,shift 命令会将每个参数向左移动一个位置。第二个参数变成第一个,第三个变成第二个,第 10 个参数变成第九个。虽然这可能直觉上让人不太明白,但在 shift 命令之后,%~9 会解析为第 10 个参数。再经过三次 shift 命令,%~9 会解析为 M,第 13 个参数,从而产生期望的输出:
Parm 1 is "A"
Parm 2 is "B"
Parm 9 is "I"
Parm 10 is "J"
Parm 13 is "M"
在第二章中介绍命令分隔符(&)时,我提到过应该谨慎使用它。在其他情况下,它可能会弄乱代码,但 shift 是一个非常简单和简洁的命令,将三个命令写在一行实际上可以清理代码。
请注意,shift 命令不会影响%*的解析。这种奇怪的语法无论执行了多少个 shift 命令,仍然会解析为完整且原始的参数列表。
移动参数引发了一个有趣的问题。在执行 shift 命令后,使用%0 解析的路径和文件名会发生什么?简短的回答是,它会被清除并替换为第一个参数,至少在默认情况下是这样。但是 shift 命令有一个独立的选项,这个选项定义了在 shift 时哪个参数会被丢弃;所有在该参数之前的参数都会被保留。它的格式与我们之前描述的选项有所不同。/n 选项丢弃第n个参数,因此以下命令丢弃第一个参数并保留%0:
shift /1
参数 0,或者说隐藏参数,保持不变。参数 1 被丢弃,而参数 2 向前滑动成为参数 1,第三个参数变成第二个,以此类推。通过一个小改动,以下命令保留了前四个参数(以及隐藏参数),同时丢弃了第五个参数:
shift /5
参数 6 被移动到参数 5,其他参数也相应地移动。
这个选项接受参数 0 到 8——尽管没有必要使用/0,因为这是默认行为。由于某些未知原因,/9 是无效的。别问。
返回参数
你现在知道如何将参数传递给例程和其他批处理文件,并将接收到的参数写入控制台,但在真实的编码世界中,许多例程接受一些参数并将其他参数传回调用者。由于在被调用进程中的批处理变量是全局可用的,一些程序员干脆在例程中设置一个硬编码的变量名,并在其他地方使用它。真是些庸俗的人!一种更优雅、灵活的解决方案是允许调用者定义返回变量的名称。
注意
我使用庸俗一词作为贬义词,但并非没有犹豫。今天,这个词是指对美学和艺术无感的粗暴个体,而我正是以此含义使用它。但是,如果他们有现代代表性,我们不会诋毁整个民族。甚至连“吉普赛蛾”也正在被重新命名,正确地说是为了不冒犯某一群体,尽管这只蛾仍然可以被肆意诽谤。我相信当时有好有坏的庸俗人,但由于他们在两千五百多年前被完全消灭,并且在三大世界宗教的经文中被视为不祥之物,他们的名字被轻蔑地使用,而几乎没有任何悔意。近几十年来,甚至我们的远亲——尼安德特人,形象的恢复比庸俗人更加正面。
以下示例包含三个参数;前两个是要相加的数字,第三个是加法的结果。这个例程可能很短,但由于其复杂性,确实需要注释:
rem - Parm 3 is the sum of Parms 1 and 2
:Add
set /A %~3 = %~1 + %~2
goto :eof
在 set /A 命令的等号右侧,前两个参数被解析并相加。等号左侧——即被设置的部分——是神秘的文本%~3。这里本应是一个变量名,但实际上解析的是第三个参数。set /A 命令实际上是将一个命名变量设置为两个数字的和。最重要的是,这个变量名就是传入此例程的第三个参数。这种技术不难理解,但却不直观,并且在其他语言中并不常见。我见过很多程序员被这个弄得一头雾水。
一个对该例程的调用示例可以澄清发生了什么。以下调用命令传递了两个数字和一个变量名,其中双引号是可选的:
call :Add "7" "8" "sum"
在此调用之后,sum 变量的值为 15。我不能过分强调的是,在这种技术中,调用代码定义了要返回的变量名。为了进一步说明这一点,本例中的三个调用都是对同一个:Add 例程的调用:
call :Add "5" "8" "sum1"
call :Add "9" "11" "sum2"
call :Add "%sum1%" "%sum2%" "sum4Nbrs"
> con echo The sum of all four numbers is %sum4Nbrs%.
前两个调用使用了硬编码的数字,分别返回 sum1 和 sum2 变量。这两个变量都被解析并作为参数传递到第三个调用中,返回 sum4Nbrs。
请注意,在前两个调用中,sum1 和 sum2 没有带有分隔符的百分号,因为变量名被传递。但是在第三个调用中,我正在解析它们,因为它们的值被传递了——这些值是在前两个调用中赋值的。最后,echo 命令将这些内容写入控制台:
The sum of all four numbers is 33.
一个调用中的输出使用的同一个变量,在另一个调用中作为输入,但这个变量是否可以在单个调用中同时用于这两种用途呢?
一个变量作为输入和输出
一个单独的变量可以同时作为例程或批处理文件的输入和输出。为了设置这个,想象一个求整数平方的例程。第一个参数是输入,第二个参数是输出——简单来说,就是输入乘以它本身:
:Square
set /A %~2 = %~1 * %~1
goto :eof
该例程没有展示我们尚未讨论的内容,尽管%~1 的输入被使用了两次。调用代码定义了输出变量,并在例程中将其解析为%~2。
调用代码可以使用两个不同的变量来作为输入和输出。但是假设你想用一个变量的平方来替换它的值。为了实现这一点,你可以将它的值作为第一个参数传递——请注意,值两侧有百分号——并将变量名作为第二个参数传递,而不加百分号:
call :Square %nbr% nbr
如果在调用之前,nbr 被设置为 5,那么调用之后它会被设置为 25。:Square 例程提供了一些真正的灵活性,可以与两个不同的变量或相同的变量一起使用。
让我们再对这段代码做一次小改动,写一个只有一个参数的例程,该参数既是输入也是输出。该参数是一个包含数字值的变量,例程将该值替换为其平方,这使得调用例程变得更加简便:
call :SquareMe nbr
> con echo The squared number is %nbr%.
goto :eof
:SquareMe
set /A %~1 = !%~1! * !%~1!
goto :eof
首先,注意到新例程(或标签)的名称。更重要的是,注意到该例程接受一个单一的参数,即整数的未解析变量名——而不是整数值。
将:SquareMe 与:Square 例程进行比较,set /A 命令有两个关键更新。首先,不再将%~1 自乘,而是将!%1!作为每个操作数。在之前的例子中,接受的是一个值,而现在输入的是一个变量名,因此%1 解析为变量名,并且通过延迟扩展,感叹号将该名称解析为整数值。(我是否提到过延迟扩展的真正强大之处?它的应用仅受限于你的想象力。)
第二个变化是积被分配给第一个参数%1,而不是第二个参数%2。记住,在:SquareMe 中,唯一的参数现在是变量名。结果是,调用此例程会更改变量的值,即使该变量在例程中没有明确提到。
注意
我已经通过例程演示了返回参数,但从被调用的 bat 文件返回参数的方式几乎是一样的。实际上,如果被调用的 bat 文件没有通过 setlocal 和 endlocal 命令限制作用域,那么它的工作方式完全相同。如果它限制了作用域,那么有一种特殊技巧可以让一个或多个变量在 endlocal 之后存活,我将在第十六章中详细说明。
输入参数列表的长度变化
让我们构建一个更接近实际的例程,使用我们目前为止在本章中学到的内容。以下例程接受一系列一对多的数字(没有合理的限制),并返回两个变量,一个填充为输入数字的和,另一个填充为输入数字的积:
rem - Parm 1 = Sum of multiple numbers, returned parm
rem - Parm 2 = Product of multiple numbers, returned parm
rem - Parms 3+ = Set of numbers to add and multiply
:Arithmetic
set %~1=0
set %~2=1
:NextParm
set /A %~1 += %~3
set /A %~2 *= %~3
shift /3
if "%~3" neq "" goto :NextParm
goto :eof
由于我们不知道在给定调用中会有多少输入参数,因此将这些参数放在参数列表的末尾,输出参数占据前两个位置。(没人说输入必须在输出之前。)由于其复杂性,:Arithmetic 例程包含了许多必要的注释,用于定义参数列表。
和,表示为%~1,因为它还没有实际的变量名,初始化为 0。同样,积,%~2,初始化为 1。(对于我的数学迷朋友们来说,这分别被称为加法和乘法的单位。)暂时忽略第二个标签:NextParm,集中注意力在接下来的两个 set /A 命令上。第一个命令将第一个参数,即和,设置为它本身加上列表中的第一个输入数字%3,它是第三个参数。类似地,接下来的命令将第二个参数,即积,设置为它本身乘以列表中的第一个数字,同样是%3。
接下来的 shift 命令非常关键。输入参数的数量是未知的,所以我们希望丢弃刚刚使用过的第三个参数,并将其后面的参数向左移动,而不干扰前两个参数,这两个参数在本例中也是返回参数。/3 选项能够无缝地实现这一点。接下来,我们查看新生成的第三个参数,看看它是否已被填充。请记住,这个参数在原始列表中是第二个输入数字,或者说在 shift 之前是第四个总体参数。如果它已经被填充,我们将返回之前忽略过的 :NextParm 标签。现在,这两个 set /A 命令分别将第二个输入参数加到和中,并乘到积中。
这个过程会一直重复,直到数字列表(无论其长度如何)用完为止,此时 if 命令的条件语句为假,控制权将转移到后续的 goto :eof 命令,例程将返回前两个参数。
以下代码测试了这个新例程:
call :Arithmetic sum product 5 8 9 11
> con echo The sum is %sum%.
> con echo The product is %product%.
结果文本将写入控制台:
The sum is 33.
The product is 3960.
顺便说一下,如果这个结构看起来很熟悉,那是因为它是来自第九章的 Batch do...while 命令的示例。至少假设有一个输入参数,并且只要存在更多的参数,例程就会执行算术运算,或者说是 只要。
拖放参数
通过几个鼠标点击,你可以将任何文件的路径和名称,或者多个文件,传递给 bat 文件。这是当我学习 Batch 时让我和其他许多人都感到惊讶的事情。只需右键单击任何文件并选择 复制;然后右键单击 bat 文件进行执行并选择 粘贴。bat 文件执行时,唯一的参数是复制文件的路径和文件名(如果路径或文件名包含空格,则会用双引号括起来)。另外,你还可以选择文件,拖动并将其放到 bat 文件上,结果是一样的。
这种技术适用于任何数量的文件。如果你将 n 个文件复制到 bat 文件上,n 个以空格分隔的参数将被传递——每个参数都是一个路径和文件名。如果你将 n 个文件拖放到 bat 文件上,情况也是一样,甚至适用于目录。
起初,将文件拖放到 bat 文件上可能看起来像是一个简单的 Batch 小把戏,但它的用途非常广泛。你可以设计一个包装的 bat 文件来处理单个文件。也许一个程序正在执行,并以一个文件作为输入;该程序可能会将文件转换为不同的格式或添加尾部记录。也许 Batch 代码只是简单地重命名文件或给现有的文件名添加扩展名。重要的是,bat 文件正在对输入文件执行某些操作。
一种可能的操作是简单地将输入文件复制到另一个目录。考虑以下两个命令,它们构成了 BackUpOneFile.bat 的全部内容:
xcopy %1 D:\Some\Deep\Hard\To\Reach\Folder\ /F /Y
pause
xcopy 命令的第一个参数 %1 解析为 bat 文件的第一个参数,该参数将是任何拖放到 BackUpOneFile.bat 上的文件的路径和文件名。该命令将输入文件复制到目标路径,即 *D:* 驱动器中的一个深层、难以触及的文件夹。(我故意保留了 %1 语法中的双引号,而不是 %~1,因为如果路径或文件名包含空格,xcopy 命令需要双引号。)最后,pause 命令仅仅是让窗口保持打开状态,以便用户查看复制结果。
最终,如果你将 BackUpOneFile.bat 放在 Windows 桌面上,你可以快速将任何一个文件拖放到它上面,bat 文件将把输入文件复制到所需的目录,而无需你导航到该目录。
为了在没有这种拖放技术的情况下将参数传递给 bat 文件,你必须输入一个调用命令(到另一个 bat 文件或命令提示符中),并将输入文件作为参数键入。但通过这种技术,任何用户,甚至是非程序员,都可以轻松地运行 bat 文件,而无需键盘输入。你可以为位于网络上任何位置的 bat 文件创建一个 Windows 快捷方式,从而隐藏源 Batch 代码,使用户更难意外删除或修改它。
当路径和文件名作为参数传递给 bat 文件时,Batch 提供了一种简便的方法来获取关于文件的各种信息,例如最后修改日期和时间、大小、属性、路径、扩展名、文件名等。我将在探索 第十七章 中的 for 命令后详细讨论这一点。for 命令还将解锁循环功能,这样如果我们将 n 个文件拖放到 bat 文件中,系统就可以依次处理每个文件。
总结
本章以及 第八章 到 第十章 希望能够让你对 Batch 的理解超越单一的无规律顺序执行的 bat 文件。在不久的过去,你的 bat 文件无法调用可执行文件或其他 bat 文件,解释器只会按顺序执行每个命令,直到遇到 bat 文件的结尾。但是现在,你有了开始构建有趣且复杂的 bat 文件的工具。
在本章中,你学习了如何将参数传递给 bat 文件和程序,并在调用的 bat 文件中接收它们作为参数。我详细介绍了如何分隔参数,甚至如何将分隔符本身作为参数传递。你还初步了解了隐藏参数——Batch 世界中的“雪人”。我展示了如何传递回调用者定义的变量,其中包含返回值。你甚至可能学会了如何玩疯狂填字游戏。
在下一章,我将通过介绍各种由解释器和你自己创建的输出,进一步拓展这一领域。我将向你展示如何捕获 Batch 命令的输出,以及如何创建自己的文件。
第十二章:12 输出、重定向和管道

现在你可以使用 Batch 做很多事情,比如设置、重置和查询各种数据类型的变量,调用例程和其他 bat 文件,执行算术运算;然而,你学到的大部分内容不会产生持久的影响(除了文件移动和能够持久设置变量)。在 bat 文件的短暂执行结束时,所有被操作的位和字节可能会消失在空气中,就好像这个 bat 文件从未存在过一样。
我将留待以后再讨论 Batch 的形而上学问题,但每当任何类型的代码被执行时,目的就是产生某种变化。一些编码人员仅将 Batch 用作一个包装器,在调用可执行文件之前设置一些变量来实现这种变化,但 Batch 能做的远不止这些。在本章中,我将讨论两种输出类型:解释器输出(stdout 和 stderr)和你,编码人员的输出。了解了这一区别后,你将能够将不同类型的输出写入控制台、新文件和现有文件。通过这种方式,你可以将所有那些变量、调用和算术运算的结果存储在你的计算机上。
讨论这两种输出类型会引出几个相关且有趣的话题。其中一个是将任何 Batch 命令的输出重定向到文件,另一个是管道技术,将一个命令的输出传递到另一个命令。也许最重要的是,你将学会如何管理快速滚动的控制台内容——即要么保存它,要么抑制它。
解释器生成的输出与编码器生成的输出
当你打开或执行一个 bat 文件时,它会生成两种类型的输出:解释器生成的输出和编码器生成的输出。为了完全清楚,解释器技术上生成了所有的输出,但其中一部分输出是你,编码员,通过命令写入控制台或文件所产生的。这就是编码员生成的输出。作为运行的副产品,解释器还会生成你没有明确要求的输出,这就是解释器生成的输出。
所有 Batch 命令都会生成解释器输出;少数命令还会生成编码器输出。默认情况下,Batch 会将两种类型的输出写入控制台,如果 bat 文件稍微复杂一些,文本滚动得太快,以至于无法阅读。
echo 命令就是一个很好的例子,它可以生成两种类型的输出。在最简单的表现形式中,它会将所有的参数写入控制台。考虑以下命令:
echo Greetings, Earthlings.
执行此命令会将 清单 12-1 中所示的输出写入控制台。
C:\Batch>echo Greetings, Earthlings.
Greetings, Earthlings.
清单 12-1:解释器和编码器生成的输出
所需的文本“Greetings, Earthlings.”会被输出,但它并不孤单,会出现两次,且由于某种原因,当前目录会被附加到第一行之前。(假设本章中当前目录为*C:\Batch*。)
Listing 12-1 中的两行表示完全不同类型的输出。第二行来自编码者——通过 echo 命令写入控制台的消息。第一行由解释器生成。它不是 echo 命令的输出;它是 echo 命令的执行报告。
这种区别既微妙又重要。至少,解释器会通过写入每个命令并附加提示符来记录每个命令的执行。默认情况下,提示符是当前目录,后跟一个大于符号。许多命令会产生额外的解释器生成的输出,比如 xcopy 命令,它通常会列出已复制的文件。
显然,交织的输出很乱,如果这个例子看起来不乱,那是因为我只展示了一个命令的输出。很快,我将向你展示如何通过将这些不同的输出发送到不同的目标来清理控制台,但首先你需要更好地理解由解释器生成的输出,实际上它是两个不同的输出。
stdout 和 stderr
批处理将每一部分由解释器生成的输出写入两个数据流之一。数据流是从源(在本例中是解释器)到目标的传输信息,默认情况下目标是控制台。每个数据流由文件描述符表示,而最大的数据流是被描述为stdout的流,发音为标准输出(或较少使用的标准输出)。事实上,唯一不在 stdout 中的解释器生成的输出是错误消息,它们被写入文件描述符stderr,发音为标准错误。
stdout 数据流可能会变得复杂且难以理解,但它通常对你帮助极大,能帮助你确定批处理文件执行过程中到底发生了什么。你通常可以看到 if 命令的结果、哪些文件被创建等等。为了演示,以下 del 命令删除一个文件,接着是一个 set 命令,它捕获 errorlevel 作为返回代码:
del C:\Batch\DeleteMe.txt
set rc=%errorlevel%
这里没有产生由编码器生成的输出;所有输出都来自解释器。
如果存在DeleteMe.txt文件,前面的代码会删除它并将以下内容写入 stdout:
C:\Batch>del C:\Batch\DeleteMe.txt
C:\Batch>set rc=0
大多数变量在 stdout 中解析,比如在这个例子中,errorlevel 被解析为 0。(令人沮丧的是,解释器在某些情况下无法完全解析变量,比如在使用延迟扩展时。更多细节见第三十一章。)
标准输出(stdout)中间可以夹杂第二种解释器生成的输出,即写入到 stderr 数据流的输出,但仅在发生错误时才会有输出。例如,如果 del 命令中的文件不存在,解释器会输出如下内容:
C:\Batch>del C:\Batch\DeleteMe.txt
Could Not Find C:\Batch\DeleteMe.txt
C:\Batch>set rc=0
第一行和最后一行被写入标准输出(stdout),但中间一行表示文件找不到的信息则写入标准错误输出(stderr)。理解这一点非常重要:错误消息被写入 stderr,而所有其他解释器生成的输出都会写入 stdout。为了全面了解批处理文件执行过程中发生的情况,两个输出都需要,并且除非我们采取措施干预,否则它们都会写入控制台。
(顺便说一下,别纠结于为什么即使生成了错误消息,返回代码仍然是 0。但如果非要解释的话,del 命令完成后文件不存在,从某种平凡的意义上来说,它是成功的,或者这算是个 bug。)
写入文件
创建、写入和追加文件是大多数编程语言的基本功能,而批处理(Batch)允许你构建包含编码器和解释器生成输出的文件。直到现在,你所看到的所有输出都被写入到控制台,并且当批处理文件完成时,窗口会关闭。
通常,你可能希望创建在批处理文件执行完毕后仍然存在的文件。你可以将数据写入文件,以便作为可执行文件或另一个批处理文件的输入。如果我想记录其他人运行我的批处理文件的频率,我可以设置它将一条记录写入到一个中央日志文件中,记录何时以及在哪个服务器上运行了该文件。你甚至可以生成报告,并将解释器生成的输出捕获到文件中,这样可以为你提供关于批处理文件执行过程中发生的事情的良好审计跟踪。
来自编码器生成的输出
我已经展示了如何使用> con 语法将文本写入控制台。通过类似的语法,我们可以通过两个命令写入文件:熟悉的 echo 命令写入一条记录,而不那么熟悉的 type 命令将整个文件写入另一个文件。
写入记录到文件
让我们回到外星人。与其通过控制台向我们问好,它们可能想把问候语写入一个简单的文本文件中,它们可以通过一行代码来实现:
echo Greetings, Earthlings.> C:\Batch\ET.txt
这行代码有四个不同的元素。第一个是 echo 命令,第二个是命令的参数:Greetings, Earthlings. 文本。第三个元素是大于符号(>),即重定向字符。这个字符将前面的命令输出重定向到目标,目标是第四个元素:C:\Batch\ET.txt 文件。将它们结合起来,前面的语句将问候语写入文本文件。
以下的替代语法执行相同的任务,但因为信息不再紧贴着大于符号,所以更容易阅读。我将重定向符号移到了前面,后面跟着目标:
> C:\Batch\ET.txt echo Greetings, Earthlings.
如果你将此示例中的路径和文件名替换为 con,你将会认出这种常见的语法来写入控制台。
以下示例演示了这种语法的优势。如果外星人有多行信息需要传递,他们可以将目标文件路径和名称设置为变量,并在后续的 echo 命令中使用它。这种语法允许将多个重定向、目标和 echo 命令按顺序排列,使得更容易阅读写入文件的内容:
set alienFile=C:\Batch\ET.txt
> %alienFile% echo Greetings, Earthlings.
>> %alienFile% echo.
>> %alienFile% echo Take us to your leader.
在我们继续之前,我在之前的代码中悄悄加入了几个微妙但重要的功能。第一个 echo 命令使用了单一的大于符号来进行重定向,而后续的命令则每个使用了两个大于符号。单字符操作符会创建一个新的文件并写入内容,如果该文件已存在,则会删除它。两个字符操作符则会将内容追加到现有文件中。一个新手常犯的错误是对多个命令使用单一的大于符号,结果让初学者困惑,为什么只有最后一个命令有效,实际上每个命令都有效,只不过每次都会清空文件并写入一行文本。
另一个重要特性是 echo 命令后面紧跟一个点;它会写入一个空行,而不是一个点。执行完此代码后,ET.txt 的完整内容包括以下三行:
Greetings, Earthlings.
Take us to your leader.
如果你需要在一行中写入一个单独的点,记得在 echo 和点之间留一个空格。
写入文件到文件
另一个你可以与重定向结合使用的有用命令是 type 命令。单独使用时,该命令会将文件的完整内容写入标准输出和控制台。结合重定向时,它可以将该文件的完整内容插入到另一个文件中。以下示例将 DetailRecs.txt 的内容写入 OutFile.txt,并通过两个 echo 命令在前面添加一个头记录,在后面添加一个尾记录:
set outFil=C:\Batch\OutFile.txt
> %outFil% echo This is a Header Record
>> %outFil% type C:\Batch\DetailRecs.txt
>> %outFil% echo This is a Trailer Record
请注意,只有第一个 echo 命令使用了单一的重定向符号,从而确保它是真正的头记录。
来自解释器生成的输出
你现在知道,控制台上滚动的大多数杂乱信息是标准输出(stdout),可能还包含一些标准错误(stderr)和程序生成的输出。你也可以控制程序生成的输出的目标(控制台或文件)。缺失的一部分是如何处理解释器生成的输出。默认情况下,它会被发送到控制台,但通过一些努力,你可以将解释器生成的所有内容写入一个文件,通常称为 跟踪文件。
在上一节中,你学会了如何将 echo 和 type 命令的输出重定向到文件,但你可以将任何命令的输出重定向,尤其是 call 命令。在第十章中,我介绍了在 bat 文件中调用例程的概念,因此你可以创建一个包含 bat 文件主要逻辑的例程,并在文件的顶部调用它。这里的新内容是重定向操作符和在下面示例中追加到第一行的追踪文件:
@call :GetTrace > C:\Batch\Trace.txt
pause
goto :eof
:GetTrace
> con echo Greetings, Earthlings.
> con echo.
> con echo Take us to your leader.
goto :eof
这是一个完整的 bat 文件,它将问候语写入控制台并捕获追踪文件。
在第一个命令前加上@符号可以抑制 call 命令本身在控制台的显示,但不会抑制 call 命令的输出。关键的是,call 命令的输出是从被调用的例程中出来的 stdout,而该输出通过第一个命令中的大于符号被重定向,避免显示在控制台并转到追踪文件。注意,文件底部的问候语明确被发送到控制台,每个 echo 命令前都有> con 前缀。如果没有重定向,问候语将与其余的 stdout 一起写入追踪文件。
每个数据流都有一个引用或数字句柄,你可以用最少的按键引用其中之一。stdout 的引用是 1,将该引用号放在重定向符号之前,可以明确地将 stdout 重定向到追踪文件。但默认情况下,stdout 是通过大于符号(>)单独重定向的,因此以下两个命令在功能上是等效的:
@call :GetTrace > C:\Batch\Trace.txt
@call :GetTrace 1> C:\Batch\Trace.txt
在这两个命令中,仅将 stdout 写入追踪文件,stderr 中的任何错误消息将显示在控制台上。
stderr 数据流由 2 引用,因此你可以在重定向符号之前通过修改一个字节来重定向这些错误消息:
@call :GetTrace 2> C:\Batch\Trace.txt
你甚至可以将每个数据流,stdout 和 stderr,同时重定向到完全不同的文件:
@call :GetTrace 1> C:\Batch\stdout.txt 2> C:\Batch\stderr.txt
在实际操作中,分离数据流很少有用,因为任何错误消息都不会与生成它们的命令关联。
一个远远优于此的解决方案,如 Listing 12-2 所示,是将两个输出都写入追踪文件,且通过在命令末尾使用特别晦涩的语法,其中 2 和 1 分别代表 stderr 和 stdout,来实现这一点。
@call :GetTrace > C:\Batch\Trace.txt 2>&1
Listing 12-2:将 stdout 和 stderr 重定向到追踪文件的理想技巧
如你所知,&符号是用于在单行中执行两个命令的命令分隔符。但在 Listing 12-2 中所示的方式使用时,解释器将&符号视为重定向语法的一部分。记住这一点作为另一个 bat 的警告,不要问为什么,但这是捕获追踪文件的最佳技巧。
抑制 stdout 和 stderr
你可以观察到 stdout 和 stderr 在控制台上滚动,或者你可以将这两个数据流保存到跟踪文件中。在其他情况下,这些数据根本不需要。有时为每次执行一个稳定且频繁运行的过程创建日志可能不值得占用磁盘空间,而且你可能不希望 stdout 和 stderr 输出到控制台,因为你希望控制台对任何程序员生成的输出保持整洁。在这种情况下,你将希望彻底抑制解释器生成的输出。有两种技巧可以做到;其中一种简单但只适用于 stdout,而稍微复杂的技巧也适用于 stderr。
@echo off 技巧
抑制 stdout 的简单技巧是使用 @echo off 命令。根据你对 echo 命令的了解,你可能会预期这个特定的命令会将内容输出到 stdout。毕竟,echo Hello 会将 Hello 输出到 stdout。通常,echo 会输出它的参数,但有两个例外。
off 参数指示解释器抑制(或关闭)stdout,on 参数则将其重新打开,但有一个限制。用于抑制 stdout 的 echo 命令本身是写入 stdout 的。幸运的是,Batch 允许通过在命令前加上 at 符号 (@) 来抑制单个命令对 stdout 的贡献。因此,抑制 stdout 的命令本身被抑制写入 stdout 为 @echo off。我在第二章中悄悄介绍了这个技巧来清理控制台,但并未做深入解释,今天来讲解(现在是时候了)。
如果外星人要通过控制台与我们沟通,这个简单的 bat 文件就能派上用场:
@echo off
echo Greetings, Earthlings.
echo.
pause
第一个 echo 命令抑制了 stdout,以保持控制台的整洁和可读性。尝试去掉这一行,看看差异。效果很差;每个 echo 命令都会将两种类型的输出都发送到控制台。使用这个命令后,第二个 echo 命令仅将它的参数写入控制台。紧跟着点号的 echo 命令会写出一个空行。最后,pause 命令保持窗口打开,避免它迅速消失。最终的结果是:
Greetings, Earthlings.
Press any key to continue ...
按下任意键,pause 命令允许 bat 文件继续执行并关闭窗口。在第三章中,我提到我在每个高级 bat 文件的开头都会使用 setlocal 命令,以启用命令扩展和延迟扩展。这个 echo 命令是唯一可能出现在 setlocal 之前的命令,从而保持控制台的整洁。不过,你也可以在 setlocal 命令前面加上一个 at 符号,以抑制其从 stdout 的执行。
作为附带说明,每个初学者 Batch 编程者可能会试图在 echo 命令后仅通过空格写出一行空白,即便这只是个意外。但这个命令只是写出 echo 的状态,而状态只有开或关。例如,在 bat 文件的开头执行以下两行,会将 ECHO is off. 输出到控制台:
echo off
> con echo
同样,用 echo on 替换第一行会激活 stdout,输出为 ECHO is on。
@echo off 技巧还有一个注意点。尽管 stdout 被抑制,stderr 不受影响,这意味着任何未重定向的错误信息会在控制台上显示,且几乎没有上下文。
重定向到 nul 的技巧
抑制所有解释器生成的输出的最佳技巧是将 stdout 和 stderr 重定向到第七章中介绍的nul文件。无论写入其中什么内容,这个文件始终为空,因此它有点像是一个 Batch 垃圾接收器。例如,将清单 12-2 中的 call 命令重新编写为将 stdout 和 stderr 发送到nul而不是跟踪文件,有效地抑制了所有解释器生成的输出:
@call :GetTrace > nul 2>&1
然而,例程的名称现在已经不准确了。(如果需要,重新命名标签为:SuppressTrace。)
这个技巧确实需要你为主线逻辑创建一个例程,但它非常有效,且相对简单,你可以用它来抑制所有被调用的 bat 文件和例程生成的输出。前导的@符号甚至可以将 call 命令本身从 stdout 中抑制。
你可以使用这种技巧来抑制任何命令的输出。例如,以下代码执行编译后的程序,同时简单地丢弃其命令行输出:
> nul SomeProgramWithUnwantedOutput.exe
在程序执行前添加(或甚至附加)重定向到nul文件,可以很好地解决这个问题。
stdout 中的备注
我对 stdout 还有最后一点说明。在第二章中,我介绍了 rem 命令,它是将备注或注释简单地写入代码中的一种方式。还有另一种语法,它对 stdout 有影响。
使用 rem 命令生成的备注会被写入 stdout。然而,代码中任何以两个冒号(::)开头的行也会被视为备注,但它是一个隐藏的备注,且会从 stdout 中被抑制。请看这两个有效的备注:
rem This is a Remark shared to stdout.
::This is a Top Secret Remark not meant for the Hoi Polloi.
你只会在 stdout 中看到第一个备注。还要注意,双冒号前不需要空格。 在第九章中,我提到标签必须以冒号开头,但第二个字符必须是不同的字符。这是因为双冒号表示隐藏的备注。
隐藏的备注并没有什么不当之处。解释代码的备注在 stdout 中可能很有用,但其他备注可能会让它变得杂乱。对于程序员来说,这是一个很好的技巧,可以在代码中仅为自己保留注释。例如,始终保持源代码修订的详细历史是件好事,但这些细节可能会把跟踪文件弄得一团糟。如果是这样,可以为此类备注使用双冒号。
任何命令的重定向
我已经展示过如何通过重定向 echo、type 和 call 命令将输出写入文件,但这只是三个例子。你可以重定向任何 Batch 命令的输出。举个例子,你可以将以下 xcopy 命令的输出发送到一个日志文件:
set copyLog=C:\Batch\Copy.log
>> %copyLog% xcopy C:\Batch\*.dat D:\Backup\ /Y /F
无论是否重定向,.dat 文件都会被复制到 *D:\Backup* 目录,但解释器会将列出所有刚刚复制的文件及其总数的文本附加到 Copy.log 文件中。它之所以是附加的,是因为使用了两个大于号;如果是单个字符运算符,则会创建一个新的文件来存储输出。
虽然你可以对任何命令进行重定向,但许多命令没有输出,或者其输出非常乏味,不值得捕捉。在下一章中,我将介绍 dir 命令,它将目录中所有文件和子文件夹的详细信息写入标准输出。这种信息通常很容易且经常被重定向到文件。编译程序的命令行输出是另一种值得捕捉的数据。
管道
重定向通常将输出发送到文件,而管道则将输出发送到完全不同的目标。管道 是将两个不同命令连接起来的概念。解释器通过某种方式将第一个命令的输出作为输入传递给第二个命令,像通过管子、软管、管道等 ... 其实有一个更合适的物理比喻,就是管道。用来建立这种连接的字符恰如其分地被称为管道字符,也叫竖线或直杠。在大多数键盘上,它位于回车键上方,按下 SHIFT-\ 就能输入。
到目前为止,你已经看到 echo 命令的结果被重定向到控制台、标准输出或某个特定文件,但你同样可以将这些结果通过管道传递给其他 Batch 命令。回想一下 第七章 中提到的 xcopy 命令,它在复制过程中将目标文件命名为与源文件不同的名字。当时我警告过,类似的命令有时会失败,并承诺在这一章提供解决方案,那个解决方案就是管道。以下命令显然是一个直接的复制操作,并将目标文件重新命名:
xcopy C:\Batch\OldName.txt C:\Target\NewName.txt /Y /F
如果目标路径下已经存在 NewName.txt 文件,这个命令会简单地覆盖该文件并继续执行。但由于 Batch 的不确定性(或者更直白地说,可能是一个 bug),如果 NewName.txt 不存在,解释器会有些困惑。源文件显然是 OldName.txt,但是 NewName.txt 是目标文件的名字,还是目标文件夹的名字呢?.txt 扩展名应该能让解释器明白,但目录名中也可以包含点(.)。(不过,给文件夹命名时加上常见的文件扩展名真是个让人羞愧的做法。)当解释器感到困惑时,它会像迷路的人一样寻求帮助:
C:\Batch>xcopy C:\Batch\OldName.txt C:\Target\NewName.txt /Y /F
Does C:\Target\NewName.txt specify a file name
or directory name on the target
(F = file, D = directory)?
如果你在命令提示符下输入了 xcopy 命令并看到了该信息被写入控制台,你无疑会直接输入 F 然后完成操作。如果这个 stdout 输出来自一个批处理文件,也会是同样的情况,但当这个命令在一个有重定向 stdout 的批处理文件中时,解释器本质上会向追踪文件请求响应,并会一直等待下去。某个时候,会有人调查这个长时间运行的进程,滚动到追踪文件的底部,找到前面示例中的文本。这就是程序员所说的挂起;它比中止更糟糕,因为执行永远不会结束。原因可能是一个无限循环,但在这种情况下,原因是解释器向一个无法响应的实体请求反馈。唯一的人工回应就是终止命令窗口,找到并修正问题,然后重新运行。
在程序中实时响应并给出答案的唯一方式是预测问题并在批处理文件执行之前编写相应代码。为此,我将在 xcopy 命令前加上 echo 命令的响应。以下的 echo F 命令只是写入 F,这是针对文件的响应,并且这个响应被作为输入传递给 xcopy 命令:
echo F | xcopy C:\Batch\OldName.txt C:\Target\NewName.txt /Y /F
现在标准输出显示 F 作为对问题的回答,尽管这个回应并不是来自一个人类:
C:\Batch>echo F | xcopy C:\Batch\OldName.txt C:\Target\NewName.txt /Y /F
Does C:\Target\NewName.txt specify a file name
or directory name on the target
(F = file, D = directory)? F
C:\Batch\OldName.txt -> C:\Target\NewName.txt
1 File(s) copied
(是的,解释器和标准输出在管道符号之前的空格上做了一些处理。)
最终,解释器使用新名称将文件复制到目标文件夹。如果在管道符之前是 echo D,那么目标文件会是C:\Target\NewName.txt\OldName.txt。显然,这不是这里的意图,但在不同的情况下,你可以使用管道技术将目标定义为一个目录。
为了使这一过程更具通用性,你可以设置一个变量,根据目标的格式将其设置为 F 或 D。如果目标以句点和扩展名结尾,你可以假设它是一个文件;如果不是,它就是一个目录。然后你可以将解析后的变量通过回显命令传递给 xcopy 命令。
但是,当 xcopy 命令不要求反馈时,这种技术有什么效果呢?它就像试图向某人传授智慧,也许是一个不想听的青少年。就像父母的话语消失在空洞中一样,如果没有提出问题,管道传递给 xcopy 命令的信息会被完全忽略。就好像 echo 命令从未执行过一样。因此,如果需要回应,管道就会给出反馈;如果不需要回应,那它就是无害的。
管道有许多应用。在第二十四章中,你将学习如何通过将 echo 和 type 命令通过管道传递给 findstr 命令来执行一些相当复杂的文本搜索,从而允许你在字符串中找到特定文本,或者在文件中找到包含该文本的所有记录。你甚至可以通过将命令的输出传递给尚未讨论的 sort 命令来对其进行排序。
标准输入(stdin)
尽管本章讲的是输出,但任何关于 stdout 和 stderr 的讨论,如果不提及输入数据流 stdin(发音为 standard in 或不太常见的 standard input),以及它通过 0 来引用来自控制台的输入,都会是不完整的。不过,仅需简单提及即可。在大多数相关文献中,通常会将三者一起提及,仿佛它们同等重要,但尽管 stdout 和 stderr 是无处不在的,stdin 的使用却相对偶尔。
在下面的例子中,第一个命令通过 echo 将消息重定向到控制台。第二个命令则相对较新,可以视为对第一个命令的反转:
@> con echo Enter some data to be saved in a file:
@type con > C:\Batch\FromTheConsole.txt
stdin 数据流是来自键盘或控制台的输入,在此上下文中通过保留字 con 来表示。直到现在,我只将 con 用作输出,特别是输出到控制台。最终,type 命令将 stdin 重定向到文本文件中。
该命令会暂时暂停处理。用户可以输入一行文本并按 ENTER 键将该行文本写入文件。文件可以接受多行文本,直到用户通过按 CTRL-Z 后再按 ENTER 键(当光标位于行首时)来终止命令。(我可没说过它是用户友好的。)
许多时候,你可能会在控制台请求用户的基本响应——通常是简单的“是”或“否”——我将在第十五章讨论这种交互式 bat 文件如何工作。在那些你请求用户提供更复杂输入的罕见情况下,重定向 stdin 会将数据保存到文件中,以便稍后使用。
总结
在本章中,我介绍了三个相关的主题:输出、重定向和管道。stdout 和 stderr 数据流是重要且有用的由解释器生成的输出,它们为你提供有关 bat 文件执行的详细信息。这些输出不同于由编码人员生成的输出——你显式创建的输出。你学会了如何通过重定向创建新文件并附加到现有文件。我展示了如何将解释器生成的输出捕获到跟踪文件中或完全抑制它。各个命令也有输出,你学会了如何将其重定向到文件并将其管道传输到其他命令。
在本书的后续章节中,我将讨论这些新工具的许多应用。在第二十二章中,使用批处理格式化简单报告将充分利用重定向。我已经提到过使用管道进行文本搜索以及通过 dir 命令进行重定向。在下一章中,我将讨论这个极为有用的命令以及你需要了解的有关目录的所有内容。
第十三章:13 使用目录

Batch 是一个理想的工具,用于查询 Windows 目录。目录中有哪些文件或文件类型?是否有某些目录快满了?丢失的文件在哪里?你可以通过一些 Batch 代码回答这些问题,甚至更多。
在本章中,你将学习如何创建目录、删除目录,以及如何获取关于现有目录的大量信息。很快,你将能够快速生成一个报告,详细列出目录的内容,包括文件名、子目录以及任何子目录的所有内容。信息可以包括所有文件的大小、最后修改日期和属性,或者仅选择某些文件。最终,你将学习如何在 bat 文件中逐一处理这些文件和目录,但我将从分享如何获取这些有用的数据开始。我还将探讨如何轻松确定特定文件或文件掩码是否存在。
最后,你将学习如何将本地和网络目录映射到驱动器字母。几乎所有使用 Windows 计算机的人都可以通过将本章的一些小技巧编入一个简单的 bat 文件,从而减轻一些日常的繁琐工作,提高工作效率。
目录基础
目录 是计算机磁盘驱动器上的一个映射位置,可以容纳文件和其他目录或子目录。在 Windows 计算机上,目录由文件夹表示;事实上,目录 和 文件夹 这两个术语经常可以互换使用。在 Windows 资源管理器中,你可以通过几次鼠标点击在目录中创建和删除子目录及文件。使用 bat 文件,你也可以做到这一点,可能还更简单。
创建目录
要创建一个目录,Batch 使用 md 和 mkdir 命令。这两个命令都代表 创建目录,实际上它们是相同的命令(批处理同义词)。
md 命令不接受任何选项,唯一的参数是要创建的目录:
md C:\Batch\MakeMe\
这个既有用又简单的命令可以接受多个目录进行创建,但当你为每个目录使用单个命令时,每个命令的返回代码清楚地告诉你哪些目录已成功创建,哪些没有。md 命令可以在有无斜杠的情况下工作,但我建议使用它,理由仅仅是它的存在让参数看起来像一个目录。
删除目录
md 命令的对等命令是 rd 命令,用于 删除目录,它会删除一个目录及其中的任何文件。它还有一个批处理同义词 rmdir,并且只有两个选项,我总是使用这两个选项:/Q 启用 安静 模式,/S 删除任何 子目录 及其内容:
rd /Q /S C:\DeleteMe\
此命令还接受一个到多个目录作为参数,带或不带尾部斜杠。如果没有子目录,该命令可以使用或不使用 /S 选项,但如果有一个或多个子目录,则没有任何东西会在没有选项的情况下被删除。我还无法概念化删除一个目录而不删除其子目录意味着什么,因此我总是使用 /S 选项。
检索目录信息
批处理有两个命令用于检索关于目录及其包含文件的详细信息,其中一个比另一个更有用。我将从不可或缺的命令开始。
dir 命令
一个极其有用的工具是 dir 命令,它是 directory 的缩写。许多人会在命令提示符下使用它来在控制台上显示信息。当在批处理文件中单独使用时,该信息只是简单地输出到标准输出流(stdout),通常意味着它被 stdout 中包含的所有其他内容所包含,因此并不是很有用。但是,通常您会以以下两种方式之一使用 dir。
首先,根据您在第十二章中学到的知识,您可以将命令的输出重定向到文件中供程序、人类或最终批处理文件读取。其次,更令人印象深刻的是,我将展示如何将 dir 命令输入到 for 命令中,跳过创建文件的步骤。这将在第二部分中讨论,但在您使用 dir 命令输入 for 命令之前,您需要了解 dir 命令本身的复杂性。
没有选项
最简单的 dir 命令接受一个单一的参数:目录或文件夹。如果路径中没有嵌入空格,则双引号是可选的:
dir "C:\Important Stuff\"
为了演示目的,假设该目录包含一些重要个人文档的电子副本和几个其他重要材料的子目录。命令的结果可能是向 stdout 显示的列表 13-1 中显示的格式良好的报告。
Volume in drive C is OS
Volume Serial Number is 2E7D-DB30
Directory of C:\Important Stuff
07/25/2020 05:19 PM <DIR> .
07/25/2020 05:19 PM <DIR> ..
10/05/2019 06:44 PM 280,643 Birth Certificate.jpg
04/01/2014 08:28 PM 120,542 Car Title.pdf
07/25/2020 05:18 PM 61,124 Passport.png
07/25/2020 05:20 PM <DIR> Retirement
07/25/2020 10:51 PM 64,760 SSI Card.png
07/18/2020 02:26 PM <DIR> Taxes
4 File(s) 527,069 bytes
4 Dir(s) 173,275,090,944 bytes free
列表 13-1:无选项 dir 命令的示例输出
经过三行标题后,前两个
注意,文件和子目录是交错显示的,默认按字母顺序排序,但正如您很快将看到的那样,该命令非常可定制,具有其选项,可以极大地控制输出。
您甚至可以在单个命令中列出多个目录:
dir "C:\Important Stuff\" C:\Batch\
此命令显示第一个文件夹下的所有文件和子目录,然后是另一个标题下的第二个文件夹的类似信息:C:\Batch 目录。
一些有用的选项
dir 命令有很多有用的选项;其中有几个选项甚至还有自己的选项。我将讨论我经常使用的那些选项,但如常,完整的选项列表可以在帮助中找到,命令为 dir /?
/O 选项控制排序顺序,但它的工作方式与到目前为止你所见的大多数选项略有不同。附加字符定义了排序顺序。例如,/OG 将目录排在文件前面,而减号则反转排序顺序,/O-G 将文件排在目录前面。(令人痛苦的是,G 代表先显示目录。)/OEN 选项按扩展名和名称排序,而/O-E-N 则反转顺序。有些人可能觉得它讨厌,但是/ODS 按修改日期和时间排序,然后按文件大小排序:
dir "C:\Important Stuff\" /ODS
属性选项,/A,限制显示内容。/AH 选项仅列出隐藏文件,而/A-H 则从列表中省略隐藏文件;/AD 仅显示目录,/A-D 则不显示目录。实际上应该有一个仅针对文件的选项,但“无目录”选项能够完成任务,就像只有批处理能做到的一样。
默认情况下,文件大小以逗号分隔,方便阅读(对于人类来说),但如果我们想对这些数字进行任何算术操作,/ -C 选项会去掉逗号。编码者经常希望生成一个没有任何杂乱的简单文件名列表。/B 选项,即裸选项,能够很好地完成这个任务。
将这些选项组合在一起,以下命令跳过目录(/A-D),按大小从小到大排序(/OS),并仅显示文件名而不显示其路径(/B):
dir "C:\Important Stuff\" /B /A-D /OS
这条没有任何选项的命令生成了列表 13-1 中的报告。使用这些选项后,结果简洁得多,仅仅是一个简单的文件名列表:
Passport.png
SSI Card.png
Car Title.pdf
Birth Certificate.jpg
从列表 13-1 中可以看到,护照是最小的文件,出生证明是最大的,这表明尽管没有显示字节数,文件仍然按大小排序。简洁的输出可能看起来像是一种降级;确实,它对人类不如之前那么有信息量,但在第二部分中,这将是理想的数据,可以输入到一个 for 命令中逐个处理文件。
另一个有用的选项是/S,代表子目录。它本质上是先对一个目录运行 dir 命令,然后对其所有子目录再次运行此命令,返回一个格式良好的报告,每个子目录都有小标题。dir C:\ /S 命令提供了你计算机上每个文件夹的报告,但速度不快,结果可能比本书还长。当与/B 选项一起使用时,每个纯文件名都会加上其路径——这可能看起来像是一个矛盾,但在考虑到每个文件可能位于多个目录中时,就能理解了。
默认选项集
如果你打算运行多个 dir 命令,而且每个命令的选项都相同,那么你不需要为每个命令都重复这些选项。相反,你可以将一个或多个选项加载到 dircmd 伪环境变量中。一旦设置,所有后续的 dir 命令将默认使用 dircmd 变量中的选项。
例如,以下代码执行两个 dir 命令,显示裸文件名 (/B),不显示目录 (/A-D),并按扩展名排序 (/OE):
set dircmd=/B /A-D /OE
dir "C:\Important Stuff\"
dir "C:\Some Other Folder\"
你可以为特定的 dir 命令覆盖 dircmd 变量中的一个或多个选项。假设接下来的 dir 命令遵循了前面的代码,其中 dircmd 已经设置:
dir "C:\Important Stuff\" /O-E
这仍然使用了仅适用于文件裸格式的选项,但排序顺序被反转了。
任何时候,你都可以通过将 dircmd 设置为空或完全不设置来关闭此功能。
where 命令
where 命令类似于 dir 命令;它搜索一个或多个目录来查找文件 所在的位置。如果你在读这段话时耸了耸肩,心想:“dir 命令不也能做这个,而且还能做更多吗?”那么我的回答肯定是肯定的。大多数你能用 where 做的事情,dir 也能做,只是做得更好。
然而,where 命令在执行某些任务时,比 dir 命令更高效。使用 /Q 选项时,where 命令返回一个退出代码,表示成功或失败,而不是返回找到的文件列表,这使得判断某个文件是否存在或是否有至少一个与文件掩码匹配的文件变得更加容易。(/Q 选项代表 安静 模式,像你见过的其他模式一样,但在此命令中,"安静"的含义稍有不同。对于其他命令,提示被抑制,但在这里,输出被抑制。)
以下命令会在文件夹 *C:\Batch* 中查找至少一个以 FindMe 开头的文件,忽略大小写,并根据结果设置错误级别:
where /Q C:\Batch\:FindMe*
如果至少有一个文件符合掩码,返回代码将为 0;如果没有,则为 1;如果语法不正确,则为 2。
如果你仔细查看,会发现路径和文件掩码之间似乎有一个多余的冒号。dir 命令正确地将路径和文件名(或掩码)视为一个参数一起处理。而 where 命令将它们视为由冒号分隔的两个参数。这确实允许你用一个文件名或掩码输入多个由分号分隔的路径,但这并没有多少安慰。更令人困惑的是,当使用 /R 选项时——表示 递归,即它还会搜索子目录——冒号会被空格替代:
where /Q /R C:\Batch\ FindMe*
我勉强包括了这个命令。语法本身就是错误的,但它确实有一个值得使用的功能。可以将其视为带有返回代码的 if exist 命令。仅在执行这个特定任务时使用它,其他情况请继续使用 dir。
映射驱动器字母
批处理有两个非常有用的命令用于映射驱动器字母。一个是将本地路径映射到驱动器字母,另一个是将网络路径和共享映射到驱动器字母。如果您不是编程人员——首先,给您慢拍手,恭喜您已经阅读到第十三章——但更重要的是,如果这听起来像是只有程序员才能使用的东西,那其实并不是这样的。
映射路径是一个很好的工具,特别是如果您在一天中经常在计算机或网络的多个特定路径下工作。导航到这些路径可能会花费一些时间,特别是当这些路径深层嵌套时。另一个挑战,尤其是如果您在家工作,可能是您在连接到虚拟私人网络(VPN)之前无法看到网络路径。为了简化这一切,编写一个简短的批处理文件,每天早上运行,可能是在连接到网络后,这样您就可以轻松访问这些路径了。映射完成后,通过点击 Windows 资源管理器中的驱动器字母即可访问每个路径。
subst 命令
subst 命令将本地目录或您 Windows 计算机上的任何文件夹映射到驱动器字母。命令名称是 substitute 的缩写,因为使用它后,您可以用驱动器字母替代目录。(不,这不是用来做子字符串操作的。)以下命令将 *Z:* 映射到所示路径,尽管如果路径不存在或驱动器字母已映射到另一个路径,则会失败:
subst Z: C:\ParentFolder\ChildFolder\GrandchildFolder\
执行此命令后,您将在 Windows 资源管理器中找到 *Z:*,它作为 *C:\ParentFolder\ChildFolder\GrandchildFolder* 路径的别名。现在,批处理文件可以仅通过调用驱动器字母来访问该路径中的任何内容。例如,在执行前述命令后,以下命令将在 *GrandchildFolder* 目录中创建一个空文件,从而减少输入量:
copy nul Z:\EmptyFile.txt
subst 命令如果没有参数或选项,将显示所有当前通过之前的 subst 命令映射的文件夹。如果在之前的 subst 命令之前,已经有其他文件夹被映射,简洁命令将...
subst
可能会生成以下输出:
Y:\: => C:\Certain\Other\Folder\
Z:\: => C:\ParentFolder\ChildFolder\GrandchildFolder\
*Z:* 的映射现在将在计算机上生效,甚至对于其他进程(无论是批处理还是人工操作),直到计算机注销或运行以下命令通过 /D 选项 删除 或断开该映射:
subst Z: /D
此命令仅映射本地目录;它不会映射其他计算机上的目录,但还有一个其他命令可以完成这个任务。
net use 命令
net use 命令将网络目录和共享映射为 subst 命令映射本地目录。以下命令将 *Y:* 映射到远程服务器上的共享:
net use Y: \\RemoteServer\ShareName\
您现在可以使用驱动器字母 *Y:* 来访问另一台计算机上的此路径,再次适用于批处理和人工操作,直到映射被删除或计算机关闭。与 subst 命令类似,net use 命令也有一个选项来断开或删除映射,但它更为冗长:
net use Y: /DELETE
/D 和/DELETE 选项的创始人至少在潜意识里是在尊重那句著名的名言:“愚蠢的一致性是狭隘心智的鬼怪。”拉尔夫·沃尔多·爱默生可能会不赞同,但为了致敬这种愚蠢的一致性,我像对待其他选项一样将这个冗长的选项大写化,因为这是我的惯例,但并非没有保留。我的这一惯例背后的主要动机是尽量减少大写字母的使用,但当一个通常是单个字符的选项变为六个字符时,它自然应该使用小写字母。做你觉得对的事情。
与 subst 命令类似,net use 命令如果没有附加参数,将会列出通过早期 net use 命令映射的所有目录和驱动器。
注意
我需要先预防一些恶评。这实际上是 net 命令,其中 use 是它的第一个参数。该命令有十多个其他的第一个参数——例如,share 用来创建文件共享。但是由于 use 的广泛使用,程序员通常称其为 net use 命令。事实上,help net use /?会像任何其他命令一样提供相关信息。
总结
在这一章中,你学习了如何创建和删除目录。我详细介绍了极为重要的 dir 命令,稍后你还会再见到它,还有 where 命令,你在本书中不会再见到它,因为之前提到的原因。现在,你可以获取关于目录及其所有内容的惊人信息,在第二部分中,我将演示如何遍历这些数据,以便你可以对每个文件或目录执行任务。你还学会了如何映射本地和网络目录,这对于任何人来说都是极其有用的技能,不仅仅是程序员,尤其是那些经常在 Windows 计算机上工作的用户。
转换话题,我接下来将深入探讨逃逸的概念。对某些人来说,这可能是本书中最让人困惑的标题;什么是逃逸,逃逸的是什麼,我们又在试图逃避什么?这些问题和更多的疑问将在下一章中解答。
第十四章:14 转义

本章讨论的是一个令人头疼的问题及其批处理解决方案。问题在于,有时你想将某个字符作为普通文本使用,但该字符在编码语言中有特定的功能。解决方案是转义。
在本章中,我将解释如何在批处理语言中转义字符的所有复杂细节。大多数时候会使用某种特定的语法,除了在需要时会有不同。你将了解多轮转义、语法,以及为什么你可能需要多次转义一个字符。我还会回到“续行符”,它用于将命令延续到多行代码,因为当你揭开它的面纱时,你会发现它原来是一个转义字符。然而,在讨论如何解决这个问题之前,你首先需要理解并欣赏这个问题的本质。
问题陈述
你可能希望在某些代码中使用特定字符,但如果该字符是编码语言中具有某种预定义功能的特殊字符怎么办?例如,假设你尝试在文本字符串中使用该特殊字符。这种情况在所有语言中都会发生,但在批处理语言中比较常见,因为该语言具有独特的深奥语法。
正如你在本书中反复看到的那样,百分号用于限定变量;百分号位于变量的两边时,变量会被解析为它的值。但是,在批处理语言出现之前,百分号被用来表示百分比——即 100 的比例。因此,批处理中的文本字符串不能简单地表示 50%,否则百分号就会被当作分隔符来解析。这个问题的隐秘之处在于,没有编译器能捕捉到这个问题,解释器甚至可能不会失败,而是产生意外的结果。
举个例子,考虑一下这个命令,它将看似简单的语句写入控制台:
> con echo Between 60% and 80% of Americans don't understand percents!
解释器将两个百分号之间的内容(即后面跟着 80 的空格)视为一个变量。假设该变量没有设置,这是几乎可以肯定的,那么它(连同百分号)会解析为空值。结果就是在控制台上写出这样一个无意义的语句:
Between 60 of Americans don't understand percents
如果该命令只使用了一个百分号,它本身就会被从输出中去掉。顺便问一下,结尾处的感叹号去哪了?先记住这个问题。
解决这个难题的方法是转义任何特殊字符。转义字符可能很棘手,但在许多情况下,它们非常有用且不可或缺。很快,我会回到前面的 echo 命令,向你展示如何让它输出所需的文本。
插入符号转义字符
主要的 Batch 转义字符是插入符号 (^)。在其他语境中,它被称为帽子符号或用来表示指数,但在 Batch 领域,它就是插入符号。在大多数键盘上,你可以通过按 SHIFT-6 来输入它。关键是,解释器会把插入符号后面的大多数字符视为普通文本。
以下 echo 命令试图向控制台输出一些陈词滥调,那些你可能在由不理解 办公室空间 是一部喜剧的办公室里的坏激励海报上看到的内容,但令人尴尬的内容只是问题的一部分。它根本无法工作:
> con echo Together We Are > You & Me Alone
解释器将第二个大于号视为第二个重定向符号,在当前目录中创建一个名为 You 的无扩展名文件,而 & 符号则结束一个命令并开始另一个命令。显然,带有 Alone 参数的 Me 命令会直接失败。
这个命令显然很乱,但通过插入符号可以修复。我在之前被阻碍的两个字符前插入了主要的 Batch 转义字符:
> con echo Together We Are ^> You ^& Me Alone
你可以将这个命令中的每个插入符号看作一个特殊的信使。每当解释器遇到转义字符时,它就会接收到这个清晰的信号:
紧接着我的下一个字符将被视为普通文本。不要像通常那样解析它。哦,顺便说一下,迅速丢弃我,因为我不过是一个数字版的斐迪比德斯,一个在完成任务后就消失的简单信使。
结果是这个或许具有激励性和鼓舞人心的信息写入了控制台:
Together We Are > You & Me Alone
另一种让解释器将特殊字符视为文本的方式是将字符串用双引号括起来:
> con echo "Together We Are > You & Me Alone"
需要注意的是,虽然这个命令没有转义字符,但它也会将双引号输出到控制台。
我很快会揭示一些例外情况,但插入符号是最常用的 Batch 转义字符,你可以用它来转义小于符号 (<)、管道符号 (|) 和圆括号 (()),以及其他特殊字符。但 Batch 并不平等对待所有字符。
转义插入符号
由于解释器将插入符号视为转义字符并将其丢弃,你可能会思考一个插入符号希望仅作为文本来处理的困境。例如,如果你要在控制台上写出毕达哥拉斯定理,我希望你不会惊讶地发现 Batch 不支持上标:A² + B² = C²。相反,如果我们能够让它起作用,插入符号表示指数运算会足够:A² + B² = C²。(毕达哥拉斯定理假设 A 和 B 是直角三角形的两条直角边,C 是斜边。)这可能是解决方案的第一次尝试:
> con echo The Pythagorean Theorem: A² + B² = C²
不幸的是,每个插入符号都会告诉解释器将随后的字符(每个实例中的 2)视为普通文本,而这本来它就会如此处理:
The Pythagorean Theorem: A2 + B2 = C2
解释器只是将插入符号丢弃,好像它们从未存在过。
解决方案在于插入符号是自转义的;一个插入符号用另一个插入符号进行转义。我已将以下代码中的每个插入符号替换为双插入符号。在每个实例中,第一个插入符号是转义字符,后面是文本插入符号:
> con echo The Pythagorean Theorem: A^² + B^² = C^²
现在你会得到期望的结果:
The Pythagorean Theorem: A² + B² = C²
我们仍然无法管理上标,但写入控制台的结果是最接近的,并且很符合数学家的喜好。
转义百分号和感叹号
在学习了勾股定理后,你可能会在数学考试中获得满分,但这个庆祝性批处理命令无法产生预期的结果,因为两个特殊字符从写入控制台的文本中被丢弃了:
> con echo I Scored 100% on my Math Test!
哎呀!我们忘记了插入符号。你可能会认为这个快速修复会显示百分号和感叹号:
> con echo I Scored 100^% on my Math Test^!
但输出没有改变。不幸的是,正如批处理中的常见情况那样,存在一些限制。插入符号不能作为百分号或感叹号的转义字符。
百分号的转义字符是另一个百分号,而感叹号的转义字符——实际上是转义字符(复数)——是两个插入符号。如果这对你没有任何意义,你并不孤单。我从未找到过一个合理的解释来说明这个异常,但以下命令会将“I Scored 100% on my Math Test!”写入控制台:
> con echo I Scored 100%% on my Math Test^^!
思考一下;与勾股定理示例相比,Batch 如何处理双插入符号似乎存在矛盾。文本^² 会解析为²,但与数学测试相关时,文本^^!会解析为!,完全没有留下插入符号。是的,就是这样工作。解释器处理双插入符号的方式取决于它后面跟着感叹号或其他任何字符。可以把它看作是一个限制的限制(或者是一个元限制)。
回到本章开头的题目,这条命令写入了期望的文本:
> con echo Between 60%% and 80%% of Americans don't understand percents^^!
解释器通过每一对百分号和前置于感叹号前的两个插入符号将适当的文本写入控制台。
注意
正如在第三章中提到的,我写这本书时假设始终启用了延迟扩展,但如果它被禁用,Batch 会将感叹号当作任何其他字符来处理,在 Batch 中没有特殊意义,也不需要转义。
多层转义
之前的示例演示了如何通过单层转义将硬编码文本写入控制台,同样的技巧也成功地设置了一个简单的变量,但有一个陷阱。例如,以下的 set 命令解析了两个转义字符并将“Together We Are > You & Me Alone”存储到变量中:
set pureDrivel=Together We Are ^> You ^& Me Alone
不幸的是,这个变量的用途非常有限。该变量确实包含了两个特殊字符,但如果你尝试将它写入控制台或文件,或者尝试将它传递给另一个命令,它将无法按预期工作。因为在将该文本赋值给变量时,转义字符被移除了,所以当该变量稍后被解析时,解释器认为具有特殊意义的字符又会引发原本转义解决的问题。
解决方案是转义转义字符——是的,双重转义。以下两行代码将期望的文本写入控制台,我所说的期望文本是指它包含了一个大于号和一个和号,并且没有转义字符:
set pureDrivel=Together We Are ^^^> You ^^^& Me Alone
> con echo %pureDrivel%
为了看清发生了什么,让我们专注于^^^&。第一个插入符号是第二个插入符号的转义字符,第三个插入符号是和号的转义字符。解析后,set 命令将^&作为变量值的一部分进行存储。当 echo 命令解析该变量时,剩下的插入符号——它刚才被当作文本处理——现在是和号的转义字符,最终只有和号被写入控制台。
让我们看一下整个文本字符串。第一个命令将pureDrivel设置为Together We Are ^> You ^& Me Alone的值;然后第二个命令将文本Together We Are > You & Me Alone写入控制台。
多级转义可能会变得更复杂。例如,如果你在将两个变量拼接成一个更大的变量后再将该第二个变量写入文件,你将需要三轮转义。
至于三重转义的机制,考虑一下这个:由于^^^^^^^&中有四个转义字符(即七个插入符号),它解析为^^^&。第二轮转义将其解析为^&,最终在第三轮解析时变成&。转义字符的数量是2*^n* - 1,其中n是转义的次数。我说它很棘手,但它也挺酷的。
连续字符
我不止一次听到程序员将插入符号称为批处理的连续字符,我甚至在第五章中介绍过这种用法,举了一个例子,展示了它在一个跨越四行代码的 set 命令中的应用。严格来说,这不正确,但在实践中,它确实执行了这个功能。让我来解释一下。
每个程序员的目标都应该是写出不需要读者左右滚动的代码。(当然,它还应该是高效的、文档完善的、结构清晰的,甚至是优雅的,但这也许只是我的个人看法,所以我离题了。)在大多数编译语言中,当命令过长不容易阅读时,你只需按 ENTER 键,然后在下一行继续输入。编译器足够聪明,知道该命令包含了两行、三行,甚至更多行。
Batch 解释器没有那么宽容(或聪明),但当你在代码行的最后添加一个插入符号时,语句会继续到下一行。即使一行代码不特别长,我有时也会使用这种技术将传递给可执行文件的参数对齐,以提高可读性:
C:\Batch\SomeExecutable.exe %Arg1% ^
%Arg2% ^
%AnotherArg% ^
%YetAnotherArg% ^
%AndAnother%
在文本文件的大多数行末尾,两个字节表示回车换行符(carriage return line feed)。在十六进制中,这两个字节是 x'0D' x'0A',它们通常合在一起称为 CRLF,但在文本编辑器中通常不可见。(如果使用 Notepad++,可以选择查看 ▶ 显示符号 ▶ 显示行尾,以便让 CRLF 可见。其他编辑器也有类似功能。)
实际上,插入符号仍然只是一个转义字符,它是在转义 CRLF。遵循转义字符的作用,当解释器看到插入符号时,它不会像通常那样将后面的 CRLF 视为行末,而是将其视为其他空白字符并忽略它,实际上是“换行”了。从这个角度来看,插入符号就是“续行符”。(但我仍然感到不太舒服。)
一个常见的错误是,在行尾看似结束的插入符号后加上一两个空格,从而使文本包装失效。由于插入符号转义的是紧随其后的字符,这样做不过是转义了一个空格,这几乎等于什么都没做,并且不会打乱 CRLF。对于那些仅将插入符号视为续行符而不是转义 CRLF 的用户来说,这种疏忽可能很难排查。知识就是力量。
你已经学到,单个字符通过一个转义字符来转义,除了感叹号,它需要两个转义字符。CRLF 是另一个例外,但原因正好相反。CRLF 实际上是两个字符,回车字符和换行字符,它是 Batch 中唯一一个由一个字符转义的两个字符示例。
总结
在本章中,你学习了许多使用插入符号(caret)以及有时使用百分号(percent sign)来转义特殊字符的方法,但讨论才刚刚开始。这项技术是一个不可或缺的工具,稍后在本书中你将看到它的多个应用。
如果没有别的,我希望本章能让你认识到转义是多么棘手。当我还是一个初学者时,我从一位更有经验的同事那里得到了一个简单却富有智慧的建议:要认真测试;测试代码可能遇到的所有字符。由于有许多警告、特殊情况和例外,你不应该在看到转义在某个特定场景下有效后,就假设它在所有环境下都会有效。在你的测试计划中,加入所有可能遇到的特殊字符,来测试那些进行转义的代码。
为了完全不同的内容,下一个章节将讨论如何使批处理文件与人类进行交互,提问、获取回答,并根据这些回答执行条件逻辑。
第十五章:15 互动批处理

如果你心中有任何疑虑,让我来告诉你,Batch 没有图形用户界面(GUI),但它有功能性的用户界面(UI)。在本章中,我将讨论如何在 bat 文件执行时获取用户输入的不同方式,比如从列表中选择一个选项或输入对某个问题的回答。我还会描述如何更改控制台的视觉显示或外观,包括清除屏幕、改变颜色以及更新标题。最后,我将把所有内容整合在一起,构建一个完全可操作的 Batch UI(BUI)准备执行。
用户界面、图形用户界面和批处理用户界面
用户界面本质上是用户与计算机之间进行沟通的一种方式,用户输入信息并获取反馈。每次你进行在线购买时,你都在使用图形用户界面,它是一种更复杂的用户界面,允许通过不仅仅是键盘的图形化用户输入。视频游戏是一个被美化的用户界面,每次你在智能手机上点击一个图标来打开应用时,你都在使用一个用户界面。《星际迷航:下一代》中的指挥官数据是一个安卓人,拥有一个极其先进的用户界面,能够通过五种感官与人类互动。
继续沿用科幻主题,Batch UI 更像是 1983 年的电影战争游戏。在 Batch UI 或 BUI(发音为 boo-ē)中,没有面板、下拉菜单、图标、菜单或单选按钮,更没有触摸屏或语音命令。请注意,如果你跟程序员提到这个词,可能会看到他们一脸茫然或扬起眉毛。我曾尝试过,但到目前为止未能将 BUI 纳入编程术语,但我仍然抱有希望,它会被接受并流行起来。
BUI 可能并不性感,但它能够向用户提问,用户可以通过输入一串文本或按一个键从列表中选择一个选项来回应。若是程序员用 Batch 来为大量用户构建一个复杂的用户界面,那简直是个施虐狂,更别说这位程序员很快就会失业了。但在许多情况下,bat 文件确实需要从用户那里获取一两个数据,尤其是当用户也是程序员时。我编写了很多 BUI,但每个 BUI 我都能数得出有多少人曾经使用过它。
BUI 可能有很多需求。你的 bat 文件可能需要将文件从或向服务器复制,并要求用户指定服务器。例如,你可能想创建一个报告,但能够根据用户的偏好从测试数据或生产数据中生成它。或者,你可能要求用户输入一个文件备份的日期范围。
从列表中选择一个选项
有两个命令允许用户输入数据到 BUI 中。一个要求用户从多个可能的选项中选择一个,另一个要求输入自由格式的响应。第一个是选择命令,顾名思义,它允许用户从两个或更多选项中做出选择。
为了开始,让我们问用户一个问题——你想要笑话、双关语还是谜语?——并允许他们输入 J、P 或 R,分别对应三种选择。以下选择命令正是这样做的:
choice /C:JPR /M:"Do you want a Joke, Pun, or Riddle"
/C 选项列出选择,/C:JPR,双引号括起来的文本与/M 选项相关联,是展示给用户的消息。这两个选项在没有冒号分隔符的情况下也能工作。也就是说,/C JPR 在功能上等同于/C:JPR,但我更喜欢使用冒号,因为它巧妙地将选项与其值或消息联系在一起,就像一块有价值的地毯能把房间融为一体。另外,注意问题末尾没有问号。解释器在向用户展示可选择项列表后会自动添加标点符号。
上一个命令在控制台上向用户显示以下内容:
Do you want a Joke, Pun, or Riddle [J,P,R]?
执行批处理文件时,命令会在选择命令处暂停,直到用户按下三个键中的一个(或退出命令窗口)。如果用户按下列表中没有的键,计算机会发出哔声并继续等待,但当用户选择其中一个选项时会发生什么呢?
到目前为止,errorlevel 仅仅是一个返回代码,通常为 0 表示命令成功执行,0 以外的数字表示执行失败。但在执行选择命令后,errorlevel 会被设置为用户的选择;更具体地说,它会被设置为与用户选择在列表中的位置相对应的整数值。更简单地说,如果选择是由/C:JPR 定义的,选择 J 会返回 1,P 返回 2,R 返回 3。
我觉得返回有效选择的变量名字中包含error一词有点误导,但在克服了语义问题以及这个保留字承担双重职责的事实后,检查变量以确定用户的选择并执行接下来的逻辑并不困难。
这是另一个选择命令的示例,似乎也缺少了一些内容(除了问号之外):
choice /M:"Do you want to try again"
缺少了/C 选项,但这是一个是或否的问题,当省略此选项时,隐含的默认值是/C:YN,Y 返回 1,N 返回 2。
另外两个选项总是配合使用。/T 选项设置超时,即给定的秒数,在超时之前,命令会等待,解释器会选择/D 选项定义的默认选择。以下命令给用户 20 秒的时间,/T:20,在此之后会强制执行一个双关语,/D:P:
choice /C:JPR /M:"Do you want a Joke, Pun, or Riddle" /T:20 /D:P
有时列出选择项并不合适。例如,在询问用户给某项打分时,从 1 到 5 之间的评分,可能更倾向于通过文字来解释评分系统。假设你想向用户提出以下问题,要求其做出回应:
Rate your agreement to the statement on a scale of 1 to 5.
I love bat comedy. 1 (Agree) to 5 (Disagree)
以下的 echo 命令和 choice 命令一起生成期望的文本,并等待响应:
echo Rate your agreement to the statement on a scale of 1 to 5.
choice /C:12345 /M:"I love bat comedy. 1 (Agree) to 5 (Disagree)" /N
你将需要 /C:12345 选项,以便解释器能够知道所有可能的选择,但你不希望显示 [1,2,3,4,5],因为它会与前一行的指示产生冲突。/N 选项(无选择键)会抑制选择的显示,只向用户显示期望的消息。问号也会随着 /N 选项被抑制,但如果消息是以问题的形式提问,你可以将问号包含在消息字符串中。
如果一个 bat 文件要在选定的服务器上执行任务,可以使用一系列的 echo 命令列出任意数量的服务器及其相关的键盘键,作为选择命令的前奏。对于预定义的服务器列表,这样做效果很好,但如果列表特别长或在编码时无法预知,可以改为要求用户输入服务器名称,作为下一个命令的输入。
自由格式的用户输入
另一个允许用户输入数据的命令是 set 命令(见第二章),当它与 /P 或 提示字符串 选项一起使用时。与没有选项的 set 命令类似,set /P 命令将一个值赋给变量,这个值可以是任何合理长度的字符串,甚至为空。不同之处在于等号后的文本不是赋给变量的值,而是显示给用户的提示字符串。用户输入的内容将在按下 ENTER 键后赋值给变量。
为了演示,等号后的问题将显示在控制台上:
set /P yourAns=How are bats like false teeth? &
执行会被暂停,直到用户响应,此时解释器会将该响应赋值给 yourAns 变量。
如果忘记使用 /P 选项,变量将直接赋值为等号后面的文本,而不会提示用户。注意三个重要的细节。首先,这个命令没有在消息字符串后附加问号,所以我加上了。其次,我在问号后添加了一些空格,以便将响应的开始与问题分开。最后,我在行末加了一个 & 符号,让这些空格一目了然。这些细微的调整会让你的用户和任何阅读你代码的人都感到满意。
每个笑话都需要一个结尾,而这个结尾会在用户有机会思考问题并输入猜测后,出现在以下代码的第二行:
set /P yourAns=How are bats like false teeth? &
echo ** They both come out at night.
echo ** You said: "%yourAns%"
第三行和最后一行显示用户的答案,答案被双引号括起来。这是一个笑话。我并没有说这是一个好笑话。
修改界面外观和感觉
Batch 提供了三个更多的命令来改变控制台的外观和感觉,其中有一些命令还有其他用途:
更新标题
当一个 bat 文件执行时,会打开一个命令窗口,其顶部的白色条上的标题很可能是 C:\WINDOWS\system32\cmd.exe,即执行 bat 文件的程序。title 命令将这个标题重置为更具辨识度且不那么通用的标题。以下命令会将标题更改为紧随其后的文本:
title Batch Improv Theater
参数列表中的嵌入空格不是问题,如果你将文本括在双引号中,它们也将成为标题的一部分。
使用这个命令不仅限于交互式 bat 文件,在其值显而易见的情况下也可以使用。任何在其他 bat 文件可能也在运行的机器上运行的 bat 文件,都可以通过标题得到增强。如果其中一个 bat 文件挂起或陷入死循环,应该终止哪个呢?如果它们没有标题,它们可能看起来一模一样,导致你无法知道是哪一个。title 命令解决了这个问题。实际上,我将在第二十六章中使用这个命令,给一个可能会挂起的进程添加标题,以便另一个 bat 文件能找到它并终止它,如果它确实挂起的话。你甚至可以在运行期间多次重置标题,可能显示当前状态或正在执行的步骤。
清除屏幕
cls 命令是 clear screen 的简写。当这个不带选项的命令执行时,屏幕或控制台会被清除,显示一个空白(目前是黑色)的画布。为了减少干扰,你可以在向用户提问之前执行这个命令。
更改颜色
打开或执行 bat 文件时,会弹出一个带有白色文本和黑色背景的命令窗口,这与人类自古埃及纸莎草和墨水出现以来的阅读方式完全相反。想象一下你现在正在阅读的文本,如果它是白色的,背景是漆黑的纸张。现在看起来似乎有些过时,但在 Batch 早期,它一定是前卫的。
color 命令提供了 16 种不同的颜色,供用户选择用于前景文本和背景,总共有 240 种组合,尽管某些组合几乎不可读,甚至会让眼睛感到不适。
进入帮助菜单,输入 color /? 来查看完整的颜色列表,颜色由十六进制数字 0 到 F 表示,但流行的颜色包括黑色(0)、蓝色(1)、红色(4)和白色(7)。color 命令接受一个两字符的颜色属性作为参数,其中第一个字符表示背景色,第二个字符表示前景色或文本色。顺便提一下,解释器足够智能,能够拒绝将相同颜色同时赋给背景和前景的命令。
黑色背景和白色文本是默认的 Batch 颜色方案,你可以通过以下命令显式调用它:color 07。将属性反转为 70 会创建一个白色背景和黑色文本的组合,但黑色文本配上明亮的白色(F)背景更加吸引人:
color F0
我个人偏好的是在蓝色背景上使用亮白色文字,颜色 1F,但撇开美学不谈,这个命令最主要的用途是标记问题。你可能每天运行某些 bat 文件以执行一些日常或重复的任务。你可能每天早晨在登录时运行这样的 bat 文件,然后忽略它,但在文件无法复制或进程中止的罕见情况下,颜色命令提供了一种很好的方式,通过"红旗"(字面意义上)来警告用户。如果使用默认的颜色设置,写完错误信息后,这行命令会立即将屏幕从黑色(默认)变为红色(4),稍微加亮白色文字(F),并保持窗口打开:
@color 4F & pause
即使用户已经转向其他任务并且命令窗口被置于一旁,这应该能够吸引他们的注意。如果没有,你也可以使用 cls 命令在写出错误信息之前清除屏幕。为了增强对比度,当过程成功完成时,你可以将背景设置为绿色,使用颜色 2F。
如果 stdout 已被重定向到追踪文件或 nul 文件,那么 cls 和 color 命令将不起作用。如果只需要清除屏幕一次,在重定向 stdout 到追踪文件之前执行 cls 命令;否则,echo off 是唯一能够关闭控制台输出的现实选择。同样,你可以在重定向前或后执行 color 命令。提前执行,它将为整个重定向过程设置颜色。你也可以在执行结束后返回重定向时,将屏幕变为红色,以表示中止。title 命令是其中最受欢迎的,因为它在 bat 文件中的任何位置都能正常工作,无论是否有重定向。
完全功能的批处理用户界面
让我们将这一切组合成一个完全功能的 bat 文件,可以反复互动地分享笑话、双关语或谜语。接下来的两个代码片段包含了完整的 bat 文件。这是BatchImprov.bat的第一部分:
❶ @setlocal EnableExtensions EnableDelayedExpansion
@echo off
color 1F
title Batch Improv Theater
❷ :Again
cls
> con echo.
❸ > con choice /C:JPR /M:"Do you want a Joke, Pun, or Riddle"
> con echo.
❹ if %errorlevel% equ 1 (
call :Joke
) else if %errorlevel% equ 2 (
call :Pun
) else if %errorlevel% equ 3 (
call :Riddle
)
> con echo.
❺ > con choice /M:"Do you want to try again"
if %errorlevel% equ 1 goto :Again
goto :eof
在 setlocal 命令❶(我常用的开头命令)之后,echo off 命令抑制 stdout,使得只有我生成的输出能显示在控制台上。注意,由于前面的 at 符号(@),这两个命令都不会写入控制台。然后,color 命令将背景设置为蓝色,文本设置为亮白色,目的是为了更好的可读性。接下来,title 命令定义命令窗口的标题。在:Again 标签❷之后,cls 命令清除屏幕,完成设置。
三个 echo.命令被策略性地放置在代码中,用于显示空白行,提升可读性。与之前相同的 choice 命令❸询问用户他们喜欢哪种幽默类型。由于有三种选择,if 命令配合 else if 结构根据 errorlevel❹的值判断用户的选择。根据用户的回答,分别调用笑话、双关语或谜语的不同 call 命令,分别对应 1、2 或 3 的值。
第二个选择命令❺询问用户是否希望继续欣赏这些幽默,默认选择为 Y 和 N。如果用户选择 Y,解释器返回 1 并且我们回到:Again 标签❷,在这里清屏并重新开始。选择 N 表示用户已经看够了,我们退出 bat 文件。
在前面的代码列表中调用的三个例程在BatchImprov.bat的最后部分定义如下:
:Joke
> con echo Please give an answer to the joke:
> con set /P yourAns=How are bats like false teeth? &
> con echo ** They both come out at night.
> con echo ** You said: "%yourAns%"
goto :eof
:Pun
> con echo We hope you find this punny:
> con echo Crossing a vampire bat with a computer means love at first byte.
goto :eof
:Riddle
> con echo Please give an answer to the riddle:
> con set /P yourAns=This type of bat is silly. &
> con echo ** A Dingbat.
> con echo ** You said: "%yourAns%"
goto :eof
:Joke 和:Riddle 例程结构相似。set /P 命令在揭示笑点和用户答案之前要求输入。:Pun 例程则仅仅输出机智的双关语,不需要用户输入。
这个 bat 文件并没有捕捉 stdout 和 stderr,因为这么做会阻止在每次选择之间使用 cls 命令。如果在开发过程中这成为问题,你可以暂时注释掉 echo off 命令,但要准备好面对一个凌乱的控制台,影响你的测试。
在BatchImprov.bat中的每个 choice 和 set /P 命令与本章前面展示的有所不同。除了 echo 命令外,每个命令都使用重定向,通过> con 语法显式地将提示或消息写入控制台。如果 stdout 被重定向到跟踪文件,这个重定向就显得必要;否则,它会将无法回答的提示字符串写入跟踪文件。但由于 stdout 只是被抑制了,控制台的重定向在这种情况下是多余的。我在本章早些地方没有包含重定向,是因为我想专注于介绍新命令,但在实际使用中,最好总是显式地定义目标,如我在这里所做的,即使它不是必需的。
BatchImprov.bat bat 文件现在已经完全功能化。运行它,你可以回答问题并查看结果,直到你在"Do you want to try again?"提示下选择 N。毫无疑问,由于内容有限,你很快就会感到厌倦,但我们仍然有许多增强功能要讨论。在附录 A 中,你会找到一个更加动态的 bat 文件版本,能够读取包含笑话、双关语和谜语的文件;将它们存储到数组中;并在单次执行中随机访问这些数组以获得独特的内容。
总结
在本章中,我创建了迄今为止最重要的 bat 文件,目的是演示如何与用户进行交互式通信。你学习了如何提供一个选择列表供用户选择,以及如何将用户输入的自由格式响应(无论长度如何)存储到一个变量中。我还介绍了用于清屏、更新标题、改变背景和文本颜色的有用命令,包括这些命令的其他非交互式用途。
在下一章,即第一部分的最后一章,我将讨论代码块,这是一个在你深入学习过程中不可或缺的话题。代码块不仅仅是“代码块”那么简单。我将在接下来的几页中解释它是什么、为什么它很重要以及它如何有用。
第十六章:16 代码块

代码块 有时是一个相当通用的术语,指的是程序中的某个模糊部分或几行代码。在 Batch 中,它是一个明确定义的实体:一组在一对圆括号之间的命令。一个显著的例子是当 if 命令为真时经常执行的代码。
这看起来很简单,但实际上,正确且明智地使用代码块远比定义它要困难得多。Batch 的一个非常强大的功能是延迟扩展,它允许你在代码块内部以两种不同的方式解析变量,但初学者往往将这种功能误解为一个 bug。在本章中,我将详细介绍所有涉及的复杂问题,另外你还会学到如何使用代码块,特别是裸代码块,提供一种有趣的技术,使变量能够在有限范围的代码中生存。如果这听起来还不够令人印象深刻,我相信很快你会觉得它非常有用。
在代码块中解析变量
我在第二章中详细讨论了设置和解析变量的过程,但这些规则在代码块内部会有所不同。正如我很快将展示的那样,这是 Batch 的一个伟大功能,但它经常被误解,并可能导致程序员感到困惑和烦躁。即使是在多年的 Batch 编码经验之后,偶尔我也会碰到这个问题。虽然我通常能够相对迅速地找到并修复这个问题(在发出一声“哎呀!”后),但对于初学者来说,这可能会导致数小时的沮丧。这种警告最好通过一个示例来说明。
在许多情况下,同事们向我展示了一些看似简单的代码片段,比如列表 16-1 中的代码。
set price=$450
if %bSale% (
set price=$350
> con echo The sale price of a 50-inch TV is %price%.
)
列表 16-1:在代码块中设置并解析的变量...以及一个谜团
代码块旁边跟着一条令人沮丧的提问:
一个变量有一个初始值,但我将其重设为另一个值,然而它似乎并没有“生效”。我将一台 50 英寸电视的价格设置为 450 美元,且我知道 bSale 布尔值为真,因为控制台确实输出了信息,但是变量的值并没有被重设为 350 美元。回显命令输出了 450 美元。这就像是代码块中的第一条命令没有执行,而第二条却执行了。疯狂吧?为了验证这个理论,我将回显命令从 if 命令代码块内部移动到其后面。突然,我得到了我想要的销售价格显示,但这并不是解决方法,因为我只想在有促销的情况下执行命令。我甚至尝试在 if 命令之前不设置变量,但结果它什么都没有。唉!这太不合逻辑了。到底发生了什么?
简短且过于简略的回答是:“将百分号替换为感叹号。”对列表 16-1 所需的唯一更改就是在回显命令的末尾解析价格:
set price=$450
if %bSale% (
set price=$350
> con echo The sale price of a 50-inch TV is !price!.
)
结果正是那位困惑的程序员一直预料的:
The sale price of a 50-inch TV is $350.
当程序员难以置信地尝试并看到感叹号生效时,他们通常会感到更加恼火而非宽慰,并带着另一个问题和新的抱怨返回:“有时候你用百分号来解析一个变量,而有时又用感叹号。什么样的编程 sadist 会想到这个?难道 Batch 还不够深奥吗?当我设置一个变量时,我希望它被设置好。就这样。它有什么用?”这种抨击的唯一变化就是其强度和粗俗程度。这些评论来自一位非常温和且虔诚的同事。在回答这个功能用途的问题之前,我得先给你更好的解释,说明价格变量发生了什么。
一个变量同时拥有两个值是延迟展开的另一个应用,延迟展开首次在第三章中介绍,它允许在解析时或执行时解析变量。当一个变量在代码块内被设置时,你可以认为它此时拥有两个值。一个是它在代码块中设置时的当前值,在执行时解析。另一个是它进入代码块时被赋予的值,在解析时解析。
如果一个变量同时有两个不同的值,我们就需要两种不同的方式来解析这个变量。为此,百分号是用于揭示其进入代码块时的值的分隔符,而感叹号是用于获取其在代码块内当前值的分隔符。代码可以多次重设一个变量,而百分号分隔符仍然会将它解析为进入代码块之前的状态。
结果是,在列表 16-1 中的 echo 命令时,%price% 被解析为 $450,而 !price! 被解析为 $350。
尽管我的同事表示抗议,这根本不是什么虐待狂行为;它实际上代表了一个在大多数编程语言中缺乏的有趣特性。一个变量能够持有两个值可能很难理解,但一旦理解,它会带来许多可能性。为了演示,我将修改在列表 16-1 中写入控制台的消息。与其仅仅给出销售价格,展示原价和销售价格,显示节省的金额会更容易理解。我在列表 16-2 中为这两个值使用了相同的价格变量——一次使用百分号解析,一次使用感叹号解析。
set price=$450
if %bSale% (
set price=$350
> con echo A 50-inch TV has been marked down from %price% to !price!.
)
列表 16-2:一个变量解析为两个不同的值
这一点从本质上来说是有意义的,因为这两个值实际上都是一个价格;一个是原始价格,另一个是销售价格。你本可以使用两个不同的变量,也许是 origPrice 和 salePrice,但具有敏锐眼光的程序员可能会称列表 16-2 为优雅,这是对作者的最高赞誉,尤其是在看到输出结果之后:
A 50-inch TV has been marked down from $450 to $350.
这个特性为富有想象力的编码者提供了许多可能性。你可能有一个计数器或一个变量在循环内部累计数字(在接下来的几章中,我将最终介绍 for 命令和循环)。在该循环内部,你可能需要访问原始的计数器或累计值以进行比较。循环的某些迭代中的数据条件可能会提醒你该循环本不该被处理。如果没有这个特性,你需要先执行一次循环进行验证,再执行一次处理核心逻辑。而有了延迟扩展,你只需一个循环,在任何时刻,你可以将所有变量恢复到原始值并退出循环。
这就引出了一个问题:嵌套代码块中会发生什么?在一个 if 命令的代码块内嵌套另一个 if 命令代码块时,是否会有三个活动的变量值?不会。只有两个值;一个是进入最外层代码块之前的值,另一个是代码块内部的当前值,无论嵌套层次如何。
F. Scott Fitzgerald 曾著名写道:“一流的智慧的考验是能够同时持有两个相反的观点,并且仍然保持功能。”在之前的章节中,我曾侮辱过解释器的智力,但 Batch 在这种二元性中的功能能力确实表明我可能过于苛刻了。也许解释器能处理更高级的话题,甚至是理论物理学。SchrodingersCat 变量可以同时保存两个值:生与死。
裸代码块
在之前的例子中,我仅处理了 if 命令代码块中的变量,但本章之前讨论的内容适用于任何代码块。请记住,代码块实际上只是一个或多个命令放在一对圆括号内。
另一个代码块的例子是 if 命令的 else 关键字后面的代码。我之前已经提到过,for 命令使用代码块,这些代码块可能会非常复杂,包含嵌套和频繁赋值与重新赋值的多个变量。这就是为什么这一章是接下来的讨论最重要 Batch 命令的最后先决条件。但是代码块不一定非得与命令关联。
一个 裸代码块 是作为其自身的实体创建的,而不是与 if 或 for 等命令相关联。例如,我们可以在没有 if 和条件子句的情况下重写 列表 16-2 中的 if 命令。列表 16-3 中的裸代码块刚开始看起来有些奇怪,但请注意,除了删除了文本 if %bSale% 外,其他一切都与之前相同。
set price=$450
(
set price=$350
> con echo A 50-inch TV has been marked down from %price% to !price!.
)
列表 16-3:一个裸代码块,其中价格有两个值
这段代码仍然在进入代码块之前将价格变量设置为原始价格,而在代码块内,我们将该变量重置为销售价格。
在输出中,我们看到相同的文本,包括两个价格,唯一的区别是代码总是将以下内容写入控制台,因为原本是条件逻辑的部分现在无条件执行:
A 50-inch TV has been marked down from $450 to $350.
要真正展示这些括号的威力,只需移除它们并检查效果。这正是我在这里所做的,而且我甚至没有重新对齐缩进,尽管这么做对结果没有任何影响。将其与 清单 16-3 进行对比:
set price=$450
set price=$350
> con echo A 50-inch TV has been marked down from %price% to !price!.
实际上,这段代码没有意义。我们在一行中设置变量,并在接下来的紧接一行中重置它,这完全废除了第一个 set 命令,那个命令不如注释掉或删除。现在,price 变量只有一个值,百分号符号和感叹号都将变量解析为其唯一值 $350,导致输出不合逻辑:
A 50-inch TV has been marked down from $350 to $350.
这组括号对代码清单产生了显著影响。它们创建了一个裸代码块,使得变量可以具有两个值,每个值可以通过不同的分隔符访问。没有括号时,代码就是废话。
使用裸代码块时,保持打开和关闭括号在同一列,并确保代码块中的代码按需缩进,就像它跟随一个 if 命令一样,这是一种良好的编码风格。你可以将 清单 16-3 中的第二个 set 命令与打开括号放在同一行,并将关闭括号放在 echo 命令后面,但这样做会让代码变得非常难以阅读。(我甚至不想展示它。)如果你在编写裸代码块时,可能有充分的理由这么做,如果你隐藏了它的存在,那么一个优雅的解决方案突然就变得难以理解。
使用裸代码块的一个很好的理由是交换两个变量的值,而不需要中介变量。这个代码将 fact 转换为 fiction,将 fiction 转换为 fact,做得比任何政治家都好:
(
set fact=%fiction%
set fiction=%fact%
)
第一个 set 命令只是重置了 fact 变量,但第二个 set 命令在重置 fiction 时并没有使用这个更新后的值。相反,百分号符号在进入裸代码块之前将 fact 解析为其值。解释器在设置任何变量之前会读取并解析这两个 set 命令——并解析两个变量。如果你删除了括号,两个变量都会采用最初定义为 fiction 的值,从而完全失败了值交换。
生存于一个 endlocal 命令
任何代码块,尤其是裸代码块,还有一个非常有用的功能:让变量在 endlocal 命令之后依然生存。在第三章中,你学到了所有在 setlocal 命令和 endlocal 命令之间的变量都会在 endlocal 执行后恢复到它们的先前状态。这个很棒的批处理功能确保被调用的例程不会覆盖可能由调用者使用的变量,但它也提出了一个非常关键的问题。如果 endlocal 命令后什么都不能存活,那被调用的例程如何返回结果呢?
endlocal 命令的“问题”
为了演示这个问题,清单 16-4 中的例程接受一个以美元和美分表示的货币金额作为第一个参数,并尝试将加上 6% 销售税后的金额作为第二个参数返回。
:AddTax
setlocal
set factor=106
set inAmt=%~1
set amtNoDec=%inAmt:.=%
set /A wTaxNoDec = amtNoDec * factor + 50
set wTaxDec=%wTaxNoDec:~0,-4%.%wTaxNoDec:~-4,2%
❶ set %2=%wTaxDec%
❷ endlocal
goto :eof
清单 16-4:浪费的好数学
现在,不要被数学问题困住。(有关详细信息,请参阅“给我数学极客朋友的算术插曲”框。)与此讨论相关的是,:AddTax 例程以 setlocal 命令开始,随后是六个 set 命令。最后一个 set 命令 ❶ 将算术结果分配给第二个参数,但 endlocal 命令 ❷ 紧接着就清除了它。没有任何返回。我试图保护或隐藏对前五个变量的更改,使其不被例程外的代码看到,但我也希望让这个最后一个变量通过。到目前为止,我没有成功。
经过一番深思熟虑,倒退命令可能在 goto :eof 之前有意义。
endlocal
set %2=%wTaxDec%
goto :eof
可惜,这个方法也不行。现在,wTaxDec 变量在 endlocal 后消失了,所以这个逻辑很可能将返回参数设置为“无”(或者 wTaxDec 在例程之前设置的值)。这是同一问题的不同表现;在 setlocal 和 endlocal 之间设置的内容都不会保存。
裸代码块解决方案
只需简单地添加两个括号(并加上一些缩进以提高可读性),就可以创建一个以 endlocal 命令开始的代码块——并解决问题。与清单 16-4 相比:
:AddTax
setlocal
set factor=106
set inAmt=%~1
set amtNoDec=%inAmt:.=%
set /A wTaxNoDec = amtNoDec * factor + 50
set wTaxDec=%wTaxNoDec:~0,-4%.%wTaxNoDec:~-4,2%
(
endlocal
set %2=%wTaxDec%
)
goto :eof
左括号开始了代码块。endlocal 命令清除了五个变量的当前状态,将它们恢复到 setlocal 之前的状态。现在,事情变得有趣了。感叹号会将变量解析为代码块内部的当前状态,而百分号会将变量解析为代码块开始前的状态,即 endlocal 执行之前的值。因此,!wTaxDec! 解析为无(或垃圾),但 %wTaxDec% 解析为代码块前赋值的那个值,而那正是我在离开例程之前分配给第二个参数的值。
关键点是,在裸代码块内有一个狭窄的窗口——从 endlocal 到右括号之间——我们可以通过百分号解析这五个变量。我利用这个窗口,通过百分号解析出我需要的唯一一个变量,并将其值赋给返回参数。
现在我们只需要调用例程来查看它的效果:
call :AddTax 25.75 result
> con echo The amount with tax is $%result%.
以下输出展示了成功将 6% 的销售税添加到原始金额中:
The amount with tax is $27.30.
这个例子设置了一个由例程返回的参数,但例程并不是这种技术的必需部分。在批处理文件的任何位置,你都可以通过调用 setlocal 命令来隐藏变量。在以下示例中,两个变量 survive 和 persist 在代码块中的 endlocal 之后依然存在,但 extinct 变量则不会:
setlocal
set survive=This variable will survive the endlocal
set persist=Multiple variables can survive and persist past the endlocal
set extinct=Time is very short for this variable
(
endlocal
set survive=%survive%
set persist=%persist%
)
这个代码块类似于之前的示例,但有两个相关的不同之处。首先,它保留了多个变量。其次,set 命令看起来是多余的——每个变量都被设置为其自身的解析值。每个变量的当前值在 endlocal 后是空的,但最后两个 set 命令将变量在代码块之前的值恢复过来。
这个技巧简单却不直观。endlocal 命令开始了一个裸代码块,接着是一条或多条 set 命令赋值变量,通常是赋值给它们自己。裸代码块外部的世界现在可以使用共享的变量,但不能使用任何未共享的变量。如果你希望有条件地进行赋值,只需在裸代码块中的 set 命令周围加上 if 命令和你选择的条件子句。
我必须承认,对于这个任务,还有一种不涉及裸代码块的替代方案。我的一部分心情有些后悔分享它,但我还是会分享,因为你可能某天会遇到它。你也可以通过将之前的裸代码块替换为这行非常丑陋的三条命令,使这两个变量在 endlocal 之后仍然生效:
endlocal&set survive=%survive%&set persist=%persist%
在每个命令分隔符(&)后添加一个或两个空格可能会使其更具可读性,但远远不够。请使用裸代码块。
总结
在本章中,你学习了如何在代码块中解析变量。下次当你听到有人说,“在代码块内使用感叹号,外部使用百分号”时,我希望你能有足够的知识为这场对话增添一些深度。现在你已经学会了延迟扩展和变量解析在代码块中的细微差别,你不仅能让某些东西工作,还能在适当的地方使用变量中的两个值。我还介绍了裸代码块,并展示了它在允许变量在 endlocal 命令后依然存在方面的重要作用。
接下来是久违的命令。现在我们已经准备好深入探讨这个非常重要的批处理命令,在第二部分中。
第二部分 for命令
for命令无疑是整个 Batch 编码世界中最不可或缺和最基本的命令。如果没有它,循环几乎不可能实现,而且它有许多形式,提供了许多选项和关键字,用于执行各种各样的任务。这个命令拥有本书的一个专门章节,而你现在正拿着这本书。
如果for命令没有任何选项,它仍然是 Batch 中最强大的命令,我将在第十七章中详细介绍这个没有选项的变体。但这个命令还提供了四个令人印象深刻的选项。在第十八章中,我将介绍处理目录、递归和迭代循环的选项。在第十九章中,你将了解文件读取选项,实际上它是个误称,因为它解锁了远超文件读取的强大功能。我们将在第二十章中讨论关于for命令的高级技巧。
第十七章:17 for命令的基础

在本章中,我将介绍for命令,特别是没有任何选项的for命令,它仅仅展示了该命令的整体功能。这个没有选项的命令创建了循环,其中输入可以是零个到多个值,既可以是简单的文本值,也可以是文件名。有些人称之为基本的for命令,但我不太喜欢这个修饰词,因为即使没有选项(我将在后续章节中讨论),for命令也并不“基本”。
说到修饰符,在完全不同的上下文中,这个命令的语法允许使用多个修饰符,你将学习如何使用这些修饰符提取关于任何文件的丰富信息,比如它的大小、最后修改的日期和时间、属性、以及路径和文件名的部分内容。你还会看到几个没有选项的for命令在实际中的应用示例,作为它强大功能的一个小展示,我将从如何构建有关这个重要命令的个性化文档开始。
创建个性化文档
在正式开始之前,我强烈建议将for命令的帮助文档导出到一个文本文件中以备将来参考。正如你在第十二章中学到的,你可以将任何命令的输出重定向到文件,这对于help命令也是适用的:
for /? > %userprofile%\OneDrive\Desktop\ForCommand.txt
使用这个路径,命令会在我的 Windows 桌面上创建ForCommand.txt文件。你可以尝试删除*OneDrive*节点,或者将其写入你选择的文件夹,但桌面是一个很方便存放这个文件的地方。(你将在第二十一章中了解 userprofile 伪环境变量。)
我没有建议你对任何其他命令这样做,而且你总是可以在命令提示符下找到帮助信息,所以你可能会好奇为什么我在这里建议这样做。随着你探索和实验for命令,这最终将变得更多是自我文档(就像一个数字笔记本)而不是帮助文档。部分文档并不十分清晰,正如你很快会看到的,确实需要一些注解。通过这个文件,你可以添加自己的评论并修改语法,使其更符合你的理解。你还可以添加命令可以采用的不同形式的示例,并包括你以后可以提取和使用的模板。
for 命令的语法细微变化会极大影响其功能,甚至可能导致其无法工作。因此,程序员常常会尝试编写 for 命令。如果第一次不行,他们会继续尝试,可能会添加某些关键字,给输入加上双引号,或者尝试单引号。最终,他们会找到某个有效的或者看似有效的方案。更好的方法是理解这个命令多种形式的复杂性。创建个性化文档,你将能将关于这个命令的所有信息集中在一个地方(当然除了这本书)。
无选项 for 命令
让我们从没有选项的 for 命令开始。for 命令可以与(恰如其分地)四个选项一起使用,我将在接下来的两章中详细讲解这些选项,但即便不带选项,它依然是个强力工具。以下不是一个实际的命令,而是用于执行循环零次或多次的通用语法, loosely 基于帮助文档中找到的内容:
for `%%variable` in (`input`) do `command`
关键词 for、in 和 do 是保留字,在你的 for 命令中将按此方式出现。括号也会如示例所示出现,但括号中的内容是无选项 for 命令的三个主要组成部分之一。这些组成部分分别是 for 变量(%%variable)、你传递给命令的输入(input),以及在循环中执行的核心逻辑(command)。
for 变量
for 变量是这个命令的核心。如果循环执行多次,它的值会在每次迭代时发生变化。你通过两个百分号符号加上一个字符来定义 for 变量。数字和许多特殊字符都是有效的,但大多数程序员普遍使用字母。通常,%%i 是最常用的变量,其中 i 代表 索引。
有些程序员专门使用 %%i,好像它是唯一允许的 for 变量,但不要局限于此。一个单字符的变量名没有太多的描述潜力,但可以利用手头的工具。我常用 %%f 表示 文件,%%c 表示 计数,%%n 表示 名称 或 数字,虽然 %%# 也可以表示 数字。与其他变量名一样,很多程序员使用大写字母,但我使用小写字母。做你认为合理的,并且保持一致。
输入
for 命令的第二个组成部分是括号内的输入,或者说你传递给命令的输入。它可以是一组文件名、单个文件名、文件掩码、多个文件掩码、硬编码文本或已解析的变量。要等一段时间才能给你展示所有这些,因此目前先将其视为输入,从单个文件开始。(帮助文档使用 set 代替输入,像是文件的 集合,但这个术语不完整,且容易与同名的命令混淆。)
命令
最后,命令是每次循环执行的核心逻辑,可以执行零次或多次。它可以是单个命令,也可以是跨越多行代码的多个命令,在该逻辑中的任何地方,你都可以将 for 变量解析为它的当前值,但这个变量与之前见过的变量完全不同。通常,你使用百分号符号(前后都有)来解析变量,但你按照定义的方式解析 for 变量:前缀是两个百分号符号。更简单地说,如果你将 for 变量定义为 %%n,你可以在命令中将其解析为 %%n。
类似于批处理接受参数的方式, strategically 放置的波浪号(tilde)可以移除围绕变量的双引号。对于 for 变量,波浪号出现在第二个百分号符号之后和变量名之前——例如,%%~n。与参数类似,如果没有双引号,波浪号对解析没有任何影响。
在一个非常令人惊讶的转折中,for 变量是区分大小写的,这是批处理世界中的一个显著异常。因此,%%i 和 %%I 不是同一个实体。在另一个转折中,在第十四章你学会了如何用另一个百分号转义百分号符号。这个变量也带有双百分号符号,但解释器足够聪明,能够区分它们。例如,如果 %%i 结果是一个 for 变量,解释器会将其解析为其值;如果不是,解释器会将第一个字符视为转义字符,并将其解析为文本 %i。让我们将这些内容组合成一些实际可执行的示例。
以下 for 命令将路径和文件名写入控制台:
for %%f in (C:\Batch\MyInputFile.txt) do > con echo Filename is %%f
对于其三个组件,我选择 %%f 作为 for 变量的简写,表示文件,因为输入是一个文件,C:\Batch\MyInputFile.txt。命令组件是一个简单的 echo 命令,用于将文本输出到控制台。
不久你就会看到 for 命令如何创建一个执行多次的循环,但这个例子只执行一次,变量被设置为括号内的路径和文件名。接下来是 do 保留字后的命令,echo 命令中的 %%f 会解析为输入文本,并将以下内容写入控制台:
Filename is C:\Batch\MyInputFile.txt
注意代码行中两个 %%f 实例,一个在行首附近,另一个在行末。第一个定义了 for 变量,第二个实例使用了该变量;也就是说,命令组件解析它并使用其值。
如果你在 echo 命令周围加上括号,会稍微更容易理解,这也是合法的语法:
for %%f in (C:\Batch\MyInputFile.txt) do (> con echo Filename is %%f)
如果命令组件包含的逻辑稍微复杂一点,最好将其重写为多行代码,以便于阅读。
以下示例在功能上等同于前两个示例,但 echo 命令现在独立成一行。使用多行语法时,括号是必需的:
for %%f in (C:\Batch\MyInputFile.txt) do (
> con echo Filename is %%f
)
开括号必须紧跟在 do 保留字后面,且与其位于同一行。这是一个重要的规定,因为其他语言允许,甚至鼓励,开括号单独位于新的一行,并与闭括号对齐。
下一个例子展示了关于无选项 for 命令的三个额外要点:
for %%F in ("C:\Program Files (x86)\Notepad++\notepad++.exe") do (
> con echo Filename is %%F
> con echo Filename is %%~F
)
首先,注意到 for 命令的代码块中可以包含多个命令。其次,我将 for 变量改为 %%F,只是为了展示我可以将它设为任何我想要的字符,只要在代码块中一致使用 %%F 或 %%~F(尽管你不会再看到我用大写字母来表示这种变量)。第三,我将输入路径和文件名括在双引号中。即使路径和文件名中包含空格、括号,甚至加号,解释器也不会受到影响,因为使用了双引号。
为了演示波浪号与参数的作用,注意到在第一次使用 %%F 的 echo 命令输出中有双引号,而在第二次使用 %%~F 时没有双引号:
Filename is "C:\Program Files (x86)\Notepad++\notepad++.exe"
Filename is C:\Program Files (x86)\Notepad++\notepad++.exe
对于单个文件名作为输入,这个 for 命令看起来可能有些过度,因为它的确是过度的。你完全可以用两个 echo 命令更轻松地将这两行文本输出到控制台。这个逻辑的真正优势在于你可以使用文件集和文件掩码来多次执行循环,每个文件执行一次。
注意
“for”命令和for循环在很大程度上可以互换使用,但存在一个细微的差别。从技术上讲,循环可以执行零次到多次,但我通常在确定输入的性质使得循环会执行多次时,使用循环这个术语。相反,我会在知道代码块中的逻辑只会执行一次时使用命令,比如在所有之前的例子中,但如果存在歧义,我也会使用这个术语,因为它是更一般且包含的术语。
文件集、文件掩码和循环
文件集,正如你可能预期的那样,是一组文件,而 for 命令接受文件集作为输入,像接受单个文件一样简单。这里的输入文件集包含两个由逗号和空格分隔的文件,但你可以包含任意数量的文件:
for %%f in (C:\Batch\MyInputFile.txt, C:\AnotherFolder\AnotherFile.dat) do (
> con echo Filename is %%f
)
如果没有空格,仅使用逗号,也可以分隔这两个文件,空格而不使用逗号也是如此,但两者都能使代码更具可读性。
这是输出结果:
Filename is C:\Batch\MyInputFile.txt
Filename is C:\AnotherFolder\AnotherFile.dat
这是第一个例子,展示了一个 for 命令如何转变为执行多次的操作——也就是更常见的所谓的 for 循环。解释器执行代码块两次,因为文件集包含正好两个文件。如果文件集中的文件数为三个,将会生成第三行输出。
在第七章中,我介绍了在 xcopy 和 robocopy 命令中使用通配符的文件掩码,创建文件掩码作为 for 命令输入时,适用相同的规则(以及注意事项)。星号代表任意数量的字符,甚至没有字符,问号通常表示恰好一个字符。然而,问号位于掩码末尾或后面跟着点时,也可以表示没有字符。
为了演示,我将从一个熟悉的 for 命令开始,替换文件名为最简单的掩码,即一个孤立的星号:
for %%f in (C:\Batch\*) do (
> con echo Filename is %%f
)
现在,解释器不会再将单个文件名写入控制台,而是会将文件夹中的每个文件逐一输出,因为每个文件都符合掩码。如果*C:\Batch*中有 17 个文件,所有 17 个文件都符合掩码,解释器会为所有 17 个文件写一条消息到控制台。如果文件夹中只有一个文件,echo 命令只会执行一次,如果没有文件,echo 命令则完全不执行。
将文件名输出到控制台并不是很令人满足,但你可以使用文件掩码执行更有趣的任务。你可以重命名满足掩码条件的每个文件,或者可能为每个文件调用一次编译程序。无论任务是什么,for 循环的基本结构都不会改变,如果逻辑会比较复杂,最好将其设置为内部例程。这里是一个调用命令,它为每个文件调用这样的例程,并将每个文件的路径和文件名作为唯一参数传递:
for %%f in (C:\Batch\*) do (
call :SomeComplexTask "%%~f"
)
如果有十几个文件符合此掩码,call 命令会调用例程 12 次,每次针对一个文件。
Batch 甚至接受以逗号分隔的文件掩码列表作为 for 命令的输入。也就是说,你可以创建一个包含多个文件和/或文件掩码的文件集。
简单文本作为输入
另一个有趣的无选项 for 命令的用法在帮助文档中找不到。你可以通过将值逐一放入括号内作为输入,处理一个值的列表。Batch 用于分隔传递参数的字符集也用于分隔此列表,所以你可以使用逗号、分号、等号和制表符,但空格分隔的列表是常见的做法。语法与之前几个示例类似,但分隔的文本替代了文件名或掩码。例如,这个 for 循环会依次将括号中的五个单词传递给代码块:
for %%i in (Individual line for each word) do (
> con echo %%i
)
结果是每个单词都会被逐行写入控制台:
Individual
line
for
each
word
注意,Batch 将括号内的 for 视为简单文本,而不是作为启动命令的保留字。我曾对解释器提出过一些质疑,但它足够聪明,能够根据上下文区分这些差异。
先前的输入列表是以空格分隔的,但解释器会将双引号包围的内容视为一个单独的值,这意味着以下 for 命令会执行两次其代码块:
for %%i in ("Just two lines for" "these seven words") do (
> con echo %%~i
)
结果是这两行输出到控制台,而不是七行:
Just two lines for
these seven words
注意,使用波浪符号 ~ 解析 for 变量时,%%~i 会去掉每个字符串中的双引号。
这些简单的示例掩盖了该技巧的巨大实用性。为了说明这一点,我将从一个接受单个参数进行处理的 bat 文件开始。这里的处理过程不重要;也许传递的值会被添加到数据结构中,或者它是一个传递给可执行文件的文件名。无论是什么处理过程,关键在于,如果我有 17 项需要处理,我必须调用 bat 文件 17 次。但使用这个将列表作为 for 命令输入的技巧,我可以增强 bat 文件,使其能够接受任意数量的参数,并在一次执行中逐一处理它们。
我将首先把处理单个参数的逻辑放入一个可调用的例程中,并通过 :ProcessParm 标签进行定义。现在,我可以通过在 bat 文件顶部加入以下代码,调用该例程零次或多次:
for %%i in (%*) do (
call :ProcessParm %%i
)
goto :eof
我在括号中输入了 %* 作为 for 命令的输入。(记住在第十一章中,%* 会扩展为 bat 文件接收到的完整参数列表。)
如果有 99 个参数,所有 99 个值都会作为 for 命令的输入,解释器会执行循环体 99 次。在第一次执行时,%%i 解析为列表中的第一个参数,并作为参数传递给调用命令中的例程。第一次执行完后,第二个参数成为第二次执行的参数,以此类推,直到解释器处理完所有 99 个参数。
这种技巧的其他用途也很多。例如,你可以轻松地对一个空格分隔的数字列表求和。我还没有介绍数组,但这提供了一种很好的方法来将多个值添加到数组中。
检索文件信息
无选项的 for 命令还有一个更棒的功能。它可以检索关于文件的大量信息和数据,比如文件大小、最后修改日期/时间等等。在本章前面,我讨论了将文件名传递给 for 循环的技巧,目的是简单地将路径和文件名写入控制台。例如:
for %%f in ("C:\Program Files (x86)\Notepad++\notepad++.exe") do (
> con echo Filename is %%~f
)
让我们把这个变得更有意义。代码不再只是简单地回显路径和文件名,而是检索并输出关于文件的大量信息。如果文件有什么需要隐藏的,我们就把它暴露出来。我最终对前面示例中的代码所做的唯一修改是与 for 命令相关的代码块,但在此之前,我必须先介绍修饰符。
修饰符
提取文件有用数据的工具被称为 修饰符,其中 9 个修饰符都是单个字母字符,巧妙地插入到 for 变量中进行解析。(我很快就会介绍第十个修饰符。)
要使用修饰符,请从一个 for 变量(例如 %%f)开始。然后在两个百分号之间和一个字符变量名之前插入波浪号和修饰符字符。例如,X 是用于检索文件扩展名的修饰符,这意味着对于 for 变量 %%f,解释器将 %%~Xf 解析为扩展名。
接下来的代码块现在有 11 个 echo 命令。前两个没有什么新意;它们都显示路径和文件名,第一个带双引号,第二个去除了双引号。其他九个 echo 命令使用特定的修饰符来修改 %%f 变量,这一点很容易理解:
for %%f in ("C:\Program Files (x86)\Notepad++\notepad++.exe") do (
> con echo Filename = %%f
> con echo Filename Without Quotes = %%~f
> con echo Fully Qualified Path Name = %%~Ff
> con echo Drive Letter Only = %%~Df
> con echo Path Only = %%~Pf
> con echo Filename Only = %%~Nf
> con echo File Extension Only = %%~Xf
> con echo Short Name Only = %%~Sf
> con echo File Attributes = %%~Af
> con echo File Date and Time = %%~Tf
> con echo File Size = %%~Zf
)
我在代码中已记录了这九个修饰符。
执行此代码可能会将以下文本写入控制台。仔细检查每个修饰符对输出的影响:
Filename = "C:\Program Files (x86)\Notepad++\notepad++.exe"
Filename Without Quotes = C:\Program Files (x86)\Notepad++\notepad++.exe
Fully Qualified Path Name = C:\Program Files (x86)\Notepad++\notepad++.exe
Drive Letter Only = C:
Path Only = \Program Files (x86)\Notepad++\
Filename Only = notepad++
File Extension Only = .exe
Short Name Only = C:\PROGRA~2\NOTEPA~1\NOTEPA~1.EXE
File Attributes = --a--------
File Date and Time = 03/11/2010 01:23 PM
File Size = 2958480
这确实是相当多的数据。从第三行开始,F 修饰符为我们提供了完全限定的路径名。通常,%%~Ff 解析出的值与 %%~f 相同,正如这里所示,但并非总是如此。如果输入仅由文件名和扩展名组成,没有路径,并且解释器在当前目录中找到了该文件,那么 %%~f 会模拟输入,省略路径,而 %%~Ff 会解析为完整的路径和文件名。
接下来,你可以在接下来的四个修饰符中看到这个完全限定路径名的各个组成部分:D(带冒号的驱动器字母)、P(没有驱动器字母的路径或目录)、N(裸文件名——即没有扩展名)、以及前面提到的 X(文件扩展名,包括前面的点)。
S 修饰符生成操作系统定义的短文件名(我答应过会在第七章中展示如何找到这个文件名),而 A 修饰符生成文件属性的列表。一个文件有 11 种可能的属性,如果文件没有某个特定属性,相应的字节会显示为破折号。此列表中唯一显示的属性是 a,表示文件被归档,但缺少其他值意味着文件不是隐藏的、压缩的、只读的,也不具有其他任何可能的属性。(我将在第三十章中进一步讨论属性。)
最后,T 修饰符提供了文件最后修改的日期和时间,而 Z 修饰符返回文件大小(以字节为单位)。(记住,S 已被占用。)缺乏逗号使得输出不易读取,但从输出中可以看到文件接近 3MB。
注意
除非你直接跳到本书的这一部分,否则你应该知道我并不喜欢过度使用大写字母,但每种方式都有其适用的场合。修饰符不区分大小写,但我使用大写字母来使它们更加显眼。小写字母的变量,在这种情况下是 f 代表文件,终结了需要解析的内容,但我知道我是少数派。请注意,大多数程序员会做相反的操作,所以你更可能看到 %%~zI 来获取 %%I 的文件大小,而不是我用来获取 %%f 文件大小的 %%~Zf。
修饰符的应用几乎是无限的,但一个简单的用途是对文件执行某种复杂的任务,但前提是文件中有数据。假设批处理代码是处理来自其他来源的数据文件。即使来源没有数据可报告,最好也让它创建一个空文件,因为根本没有文件将意味着一个失败的进程。为了使其生效,bat 文件需要判断文件是空的还是已填充数据。考虑以下内容:
for %%f in ("C:\Batch\IntermediateFile.dat") do (
if %%~Zf gtr 0 call :SomeComplexTask "%%~Ff"
)
for 命令内部的 if 命令有效地(甚至可能优雅地)验证了中间数据文件%%~Zf 的大小是否大于 0 字节,只有在通过验证后,才会调用一个例程并传递文件的路径和名称。
路径修饰符
我欠你一个第十个修饰符,万一你还没有觉得这个语法足够复杂,路径修饰符的语法与其他九个完全不同。但这并不是问题,因为它的功能也完全不同。我会稍后解释它的作用,但首先要理解的是,这个修饰符实际上是一个奇特的分隔变量。其他修饰符都是单一字符,而路径修饰符是一个以美元符号开头并以冒号结尾的变量名。
为了设置这个,且为了马上就能明白的原因,让我们在修饰符中使用路径变量(与第八章中介绍的包含由分号分隔的目录的连接的相同变量)。这个变量应该已经在你的机器上设置好了,但你可以向其中添加或前置额外的目录:
path C:\Batch\SubDir\;C:\Batch\;%path%C:\Budget\;
为了看到这个修饰符的实际效果,让我们使用%%~\(path:f 语法修改%%f 变量。注意路径变量夹在\)和:字符之间;这三个部分组成了修饰符。现在我们可以在 for 命令中使用它:
for %%f in (FourBrits.txt) do (
> con echo File Found: %%~$path:f
)
这个修饰符的实际功能是什么?它指示解释器遍历路径变量,从其中列出的第一个目录开始,寻找第一个(并且只有第一个)名为FourBrits.txt的文件。如果该文件存在于路径变量的第二个目录中,但不在第一个目录中,结果输出将包含完整的路径和文件名:
File Found: C:\Batch\FourBrits.txt
如果解释器在路径变量定义的任何目录中找不到名为FourBrits.txt的文件,它会简单地将%%~$path:f 解析为 null。
这个功能显然是为路径变量设计的,帮助文档在示例中明确使用了路径变量;只有在文档底部才提到,你可以用任何有效的变量替换它,意味着任何包含目录列表的变量。尽管如此,它通常只是被称为路径修饰符,并几乎专门用于路径变量。
堆叠修饰符
单独使用时,这些修饰符非常有用,但它们的真正威力在于将多个修饰符叠加使用时展现出来——也就是说,当它们一起使用时,可以一次解决多个文件特性。例如,你可以分别解析文件名和扩展名,然后将它们拼接为%%Nf%%Xf,但这有点混乱。相反,%%~NXf 巧妙地以更优雅的方式产生相同的结果。以下是三个典型的例子:
for %%f in ("C:\Batch\FourBrits.txt") do (
> con echo Drive Letter and Path = %%~DPf
> con echo Filename and Extension = %%~NXf
> con echo File Date/Time/Size = %%~TZf bytes
)
这段代码可能会向控制台输出以下内容:
Drive Letter and Path = C:\Batch\
Filename and Extension = FourBrits.txt
File Date/Time/Size = 04/20/2018 01:14 PM 518 bytes
使用叠加修饰符,你可以轻松获取文件的完整路径而不带文件名,或者仅获取文件名和扩展名而不带路径。最后一个示例可能会在报告中显示,并在结尾添加硬编码的文本“bytes”。
叠加修饰符甚至可以与路径修饰符一起使用:
for %%f in (FourBrits.txt) do (
> con echo Path Found: %%~DP$path:f
)
这段代码输出在路径层级中找到的第一个文件的完整路径,不包括文件名和扩展名,该文件名为FourBrits.txt。
带修饰符的参数
在进入几个实际示例之前,我将回到第十一章关于参数的讨论。事实证明,提取文件信息的修饰符在 for 命令的上下文中同样适用于参数。
我曾提到过,你可以将带或不带路径的文件名作为参数传递给批处理文件,并且被调用的批处理文件可以通过%1、%2 等解析它们。此外,你还可以将%~0 解析为正在执行的批处理文件的完整路径和文件名。好吧,前面讨论的相同修饰符(以及叠加修饰符)也适用于任何代表文件的参数。假设以下代码接收两个文件名作为前两个参数:
> con echo Size of File #1 = %~Z1
> con echo Date and Time of File #2 = %~T2
第一个命令输出第一个文件的字节大小,第二个命令输出第二个文件的最后修改日期和时间。
将两个特定的叠加修饰符应用于隐藏参数,%~DP0 解析为正在执行的批处理文件的驱动器字母和路径。凭借这些信息,你可以创建子目录,将其他文件存放在此目录或兄弟目录中,或者更新与批处理文件相关的日志文件,而且不需要知道批处理文件最终将安装在哪里。也许它会部署在多台服务器上。
修饰符允许你通过最少的键盘操作检索大量的文件信息。
实际应用
让我们在两个实际示例中应用你刚刚在本章中学到的所有有用工具。
备份中的文件重命名
使用文件掩码,无需选项的 for 命令可以生成文件列表,而修饰符可以提取每个文件路径和文件名的各个组件。结合这两种功能,你可以将一组文件复制到另一个驱动器上的镜像文件夹结构中,同时调整目标文件名。
假设我有一个名为C:\Budget*的文件夹,顾名思义,它包含预算信息。这个文件夹的名称显然表示应该进行备份,可能是备份到外部D:*驱动器,但保持相同的文件夹结构,以便文件容易找到和比较。更复杂的是,我希望在每个文件名前添加 Bkup_,因为当两个文件夹同时打开时,很容易在错误的文件夹中工作,但如果备份目录中的每个文件都以这个独特且具描述性的文本开头,就不容易出错。完成这个任务的一种方法是在for循环中使用单个命令(尽管在实际的实际应用中,你可能需要一些错误处理):
for %%f in ("C:\Budget\*.*") do (
echo f | xcopy "%%~Ff" "D:%%~PfBkup_%%~NXf" /Y /F
)
我已经将源路径和目标路径都用双引号括起来,以处理文件名中可能包含空格的情况。源路径是完全限定的路径和文件名,%%~Ff,但目标路径则稍微复杂一些。尽管它看起来像是随机的按键输入,实际上它是四个项目的拼接,当我加粗了两个带修饰符的for变量与常量的对比时,它变得更易读:
D:**%%~Pf**Bkup_**%%~NXf**
目标路径以硬编码的驱动器字母和冒号 D:开头,后跟源文件变量的路径,去掉了驱动器字母%%~Pf;这模仿了源驱动器中的源文件夹。路径的开始和结束都有反斜杠,因此接下来的是文件名的开始部分 Bkup_。为了完成目标文件名,解释器找到第二个for变量,%%~NXf,这个变量包含了源文件名(N)和文件扩展名(X)的堆叠修饰符。
综合起来,如果解释器找到一个名为C:\Budget\Budget.January2023.xlsx的文件,结果目标字符串是:
D:**\Budget\**Bkup_**Budget.January2023.xlsx**
如果你习惯了由百分号分隔的批处理变量,变量的起始和结束位置可能会让你感到困惑。当编译器看到%%~时,它知道接下来将解析一个for变量——那是开始的位置。由于由这个for命令定义的变量是%%f,解释器会在看到小写字母 f 时结束该变量——那是结束的位置。在开始和结束之间,解释器会查找零个或几个有效的修饰符。终止后的部分可能是常量,一个百分号符号开始解析一个更传统的批处理变量,或者是另一个for变量。
源目录中每个符合条件的文件都会被复制到*D:*驱动器的备份路径,并且目标文件的名称会经过调整。这个工具非常强大,虽然看起来只是一个简单的for命令,但要充分利用它的能力,必须对语法有深入的理解。
处理可变数量的文件
在第十一章的末尾,我介绍了包装器批处理文件的概念——即一个批处理文件,它所做的几乎只是执行一个处理单个输入文件的程序。这个批处理文件是可执行文件的包装器。在那一章中,我还演示了如何将多个文件拖放到一个批处理文件上,从而使得批处理文件执行一次并带有多个参数。尽管这很令人印象深刻,但如果没有一个能够处理所有这些参数的批处理文件,或者能够处理不同数量参数的批处理文件,这个技巧就几乎没有用处。
在本章中,我讨论了两个对于构建此类包装器批处理文件非常重要的概念。一个是使用修饰符提取文件信息的非常有用的技巧,另一个是 for 命令处理值列表的能力。
现在,来介绍一下设置。我创建了一个已编译的程序,用于将 Java 代码转换为 C#代码,并且它接受两个参数:输入文件和输出文件。代码转换程序可以帮助减少将旧代码更新为新语言时的麻烦。一个语言的模块作为输入传递给程序,该程序将原始语法的大部分转换为另一种语言,并在相同的文件夹中输出一个同名但扩展名不同的文件。从批处理文件的角度来看,它接受一个或多个.java文件作为参数,确定相应的.cs输出文件的路径和文件名,然后使用这两个参数调用已编译的代码——而这些.java文件可以在任何文件夹中。
以下是一个批处理文件,去除了所有错误处理和注释,它完成了所有期望的功能。for 命令接受整个参数列表作为%*,将它们一个个传递到代码块中作为%%f,后者则是传递给:ConvOneFile 例程的唯一参数:
for %%f in (%*) do (
call :ConvOneFile %%f
)
goto :eof
:ConvOneFile
ConvJava2CS.exe %~F1 %~DPN1.cs
goto :eof
在调用ConvJava2CS.exe时,这段批处理代码将输入文件和输出文件作为两个参数传递给它。我使用%~F1 获取输入文件的完整文件名,这是第一个参数加上 F 修饰符。输出路径和文件名%~DPN1.cs 则更为复杂。我使用相同的参数,也就是%1,但加上了驱动器(D)、路径(P)和文件名(N)的修饰符——也就是说,%F1 去掉扩展名(通过 X 修饰符表示)。然后,我再加上硬编码的.cs 扩展名,形成输出文件名。注意,这些变量前面只有一个百分号,而不是两个,因为这些是参数,不是变量;我在一个例程中调用可执行文件,而不是在 for 命令中。
一个评论家(或者可能是一个不熟悉批处理的人)可能会对此提出异议,认为接受编译代码中的单个参数并在程序中进行文件名操作更好或更容易。但这种方法缺乏灵活性;如果其他人希望在许多文件甚至数百个文件上运行此过程,则可能希望将输出放入子文件夹,甚至放在另一个服务器上的文件夹中。通过在批处理代码中操作文件名,将输出放入子目录无需更改编译代码;而是简单地将%~DPN1.cs 更改为%DP1%subDir%%N1.cs。您可以定义一个硬编码的子目录,但在这里我使用 subDir 作为子目录节点的变量。如果子目录不存在,甚至可以执行带有%~DP1%subDir%\作为其参数的 md 命令来创建子目录。
即使是对编译代码的简单更改,也总会有额外开销。您必须运行编译器并注意保持源代码和可执行文件同步。在可能的情况下,编码人员应该进行简单的更改,例如在批处理代码中获取文件连接器或文件名,至少在我看来是如此。
现在,您可以使用此批处理文件处理多个文件或单个文件,甚至不处理任何文件。如果没有传递参数,for 命令就没有输入,并且代码块永远不会执行。再次强调,几行代码正在执行比眼前看到的更多的工作。
在代码块中解析变量
在继续解锁 for 命令选项提供的所有功能和强大功能之前,我必须提到,到目前为止,我已经解析了与 for 命令相关联的代码块内部的单一用途变量,但这太过简单和幼稚。更常见的做法是将数据分配给一个变量,然后在复杂的代码块内使用它,甚至可能修改它。这就是第十六章详细描述的同一类型的代码块,解析变量的规则也适用于此。
例如,您可能希望为文件的日期和时间分别使用两个不同的字段,但 T 修饰符将它们解析为单个值,而在这段代码中这根本不是问题:
for %%f in ("C:\Program Files (x86)\Notepad++\notepad++.exe") do (
set filDtTm=%%~Tf
> con echo File Date = !filDtTm:~0,10!
> con echo File Time = !filDtTm:~11!
)
在代码块顶部,我将整个字符串分配给 filDtTm 变量,然后在接下来的两行分别截取日期和时间。这是一个微妙但至关重要的点:我使用感叹号和延迟扩展来解析刚在代码块内设置的变量。百分号分隔符将在代码块之前解析为 filDtTm 的值,而且由于这个值很可能根本没有被设置,结果可能会是垃圾(~0,10 和~11)。
最终,如果你在代码块内为变量赋值,你必须使用感叹号来获取其当前值。如果逻辑变得过于复杂,还有其他技巧,而在第二十章中,我将演示如何在这些更复杂的代码块中充分利用延迟扩展。但通常来说,要理解这些变量有两种可能的值,并尽量避免让代码过于复杂。
总结
在这一章中,我详细介绍了不带选项的for命令、它的组成部分、如何与文件集和文件掩码一起使用,以及如何通过修饰符检索大量文件信息。你了解到这个命令通常接受文件或多个文件作为输入,但它也可以接受一串文本。我希望你觉得这些实际应用既有趣又富有启发性,并且激发你思考这个重要命令的其他相关用途。我还提供了一些关于如何解析for命令代码块中定义的变量的提示。
第二部分的剩余章节将揭示更多内容,所有这些内容将有助于全面理解for命令的全貌。在下一章中,我将讨论一些通过选项开启的功能;其中一个列举目录而不是文件,另一个遍历子目录寻找文件,最后一个实现了一个至关重要的功能:迭代循环。
第十八章:18 目录、递归和迭代循环

许多批处理命令都有选项,但大多数提供的只是命令的稍微不同的变种或调整。for 命令的选项则是一个完全不同的故事。四个可用的选项以四种不同的方式影响 for 命令。在本章中,我将详细介绍其中的三个,第四个将在下一章中讲解,并且需要专门的一章来描述。
一个选项将命令的焦点从文件转向目录,另一个则使用递归遍历子目录以查找文件。你还将学到,如何将这两个选项结合使用来遍历一个目录树,查找子目录。
本章中的最后一个选项将 for 命令变成了一个我尚未讨论过的全新东西。它的功能与没有选项的 for 命令或带有其他选项的命令有很大的不同。它创建了一个迭代循环,在从一个数字到另一个数字的过程中,按照固定的增量或减量执行逻辑。这项工具对于任何程序员来说都是绝对必要的。
目录选项
不是所有选项字母都能准确描述它们的功能,但 /D 选项代表 directory(目录)。虽然没有选项的 for 命令会枚举文件名列表,但 /D 选项使得命令可以枚举目录或文件夹的列表。通用语法显示,除了插入该选项外,语法与没有选项的 for 命令完全相同:
for /D %%`variable` in (`input`) do `command`
在使用该选项之前,这里有一个类似于上一章的示例,一个没有选项的 for 命令,在路径后使用了一个唯一的通配符字符作为文件名:
for %%f in (C:\Budget\*) do (
> con echo Filename is %%f
)
结果输出包括文件夹中每个文件的路径和文件名,并写入控制台。
在下面的示例中,我对没有选项的 for 循环做了两个修改:一个是重要的,另一个是外观上的修改。重要的修改是将 /D 选项插入到 for 变量之前。注意,for 命令的其余部分保持完全不变。外观上的修改是我将 echo 命令中的“File”替换成了“Directory”:
for /D %%f in (C:\Budget\*) do (
> con echo Directory Name is %%f
)
对这段代码的另一个可能修改是将 %%f 替换为 %%d 来代表 directory(目录)(但我不打算使这个示例过于复杂)。
在没有 /D 选项的情况下,这个文件夹中的文件不再是输出的一部分。现在,解释器将立即将每个位于 *C:\Budget* 下的目录写入控制台,可能是:
Directory Name is C:\Budget\BankStatements
Directory Name is C:\Budget\CreditCardStatements
该输出假定这是唯一的两个子目录,但那些子目录的子目录并未出现在输出中。
递归选项
另一个有用的选项是/R,它方便地代表递归。这个选项使得for命令能够递归地在一个目录及其所有子目录(以及它们的子目录,依此类推)中搜索符合某个模式的文件。与没有选项的命令语法相比,它的通用语法与仅有选项的命令语法有所不同:
for /R [[`drive`:]`path`] %%`variable` in (`input`) do `command`
最显著的区别在于要搜索的路径现在出现在for变量之前。如果省略尾部斜杠,解释器不会受到影响,但最好加上斜杠,以明确表示它是一个路径。路径可以仅是驱动器字母后跟冒号,并且对于包含空格的路径,你需要用双引号括起来。在括号内,输入将是一个或多个没有路径的文件名,文件名之间用空格或逗号分隔。例如,以下命令会在*C:\Budget*目录及其所有子目录中搜索 Word 文档,并将找到的文件写入控制台:
for /R C:\Budget\ %%w in (*.docx) do (
> con echo Word Document Name is %%w
)
以下for命令会找到你在意大利旅行中错放的照片,或者至少是任何扩展名为*.jpg*或*.bmp*的文件,且文件名以Italy开头。由于使用了/R选项,命令不仅会在根目录C:*文件夹中查找,还会在你整个C:*驱动器中查找:
for /R C:\ %%p in (Italy*.jpg, Italy*.bmp) do (
> con echo Photo from the Italy Trip is %%p
)
(多个文件模式在所有for命令中都可以使用,无论是否有选项。)
for /R 命令有其他几种变体。在一般语法中,围绕驱动器和路径的方括号意味着它们是可选的,如果省略,则假定当前目录。因此,前面的命令在功能上等同于以下命令,因为*C:*是当前目录:
cd C:\
for /R %%p in (Italy*.jpg, Italy*.bmp) do (
> con echo Photo from the Italy Trip is %%p
)
在这两个例子中,%%p将解析为每个找到的照片的完整路径和文件名。
在一个微妙的警告中,所有传递给for /R命令的输入必须至少包含一个通配符字符。如果你在之前的命令中使用了显式的文件名(或没有星号或问号的任何文本)作为输入,它会返回*C:*及其所有子目录,并附带你的文件名,即使文件在目录中不存在。要找到显式的文件名,你必须在文件名输入中添加一个通配符字符。我建议使用尾部星号,因为它通常最不容易意外捕获其他文件。
注意
无论好坏,解释器自行处理递归,即进入每个子文件夹,隐藏它给你。在第二十三章中,我将重新讲解递归并解释如何定义实际的递归调用,从而开启更多可能性。
目录递归
如果/D选项允许目录搜索,而/R选项允许递归文件搜索,你可能会认为它们可以一起用于递归目录搜索,确实如此。格式遵循/R命令的通用语法,/D在/R之前,并且括号中的输入仅为一个星号:
for /D /R C:\Budget\ %%d in (*) do (
> con echo Directory is %%d
)
运行此代码会显示 *C:\Budget* 所有子目录及其所有子目录,依此类推。例如,假设存在两个特定的子目录和一个子子目录,输出会是:
Directory is C:\Budget\SubDir
Directory is C:\Budget\Taxes
Directory is C:\Budget\SubDir\SubSubDir
在一个有趣的怪异情况下,单独使用 for /R 命令也可以执行相同的功能,而不需要 /D 选项,或者至少执行的是类似的功能。为了演示这一点,我对之前的 for 命令做了两个小调整。我去掉了 /D 选项,并将输入从星号改为点:
for /R C:\Budget\ %%d in (.) do (
> con echo Directory is %%d
)
这并不直观,但括号中的点会指示 for /R 命令列举目录而不是文件。(目录和点都以 D 开头,如果这有帮助的话。)这也会产生子目录的列表,但请注意三点不同:
Directory is C:\Budget\.
Directory is C:\Budget\SubDir\.
Directory is C:\Budget\SubDir\SubSubDir\.
Directory is C:\Budget\Taxes\.
当你使用 /R 选项并将输入改为点时,第一个不同之处是解释器现在会列举根目录,在这个例子中是 *C:\Budget*,以及所有子目录。第二,输出的每个目录后面会跟着一个反斜杠和一个点,第三,输出排序方式也不同,解释器会先处理一个目录的子目录,再处理其兄弟目录。请注意,当同时使用 /D 和 /R 选项,并且输入为星号时,解释器会先处理 *C:\Budget\Taxes*,再处理 *C:\Budget\SubDir\SubSubDir*,这与使用 /R 选项和点时的结果正好相反。
警告
我所详细说明的关于点作为输入传递给 for /R 命令的内容,正是帮助文档所说的那样,但这并不完全正确。每行输出末尾的点只是输入的再现。这是前一节中提到的 batveat 的另一种表现形式。任何没有通配符的输入都会告诉命令逐步遍历所有子目录,并将每个子目录附加上该输入。点在上面的例子中很好地结束了每个目录,但如果你改为使用波浪号作为输入,输出中的目录都会以波浪号结尾,而不是点。
输出中的差异很细微,根据你的应用程序的不同,这些差异可能并不重要,但无论如何,它们依然存在。
迭代循环选项
/L 选项将 for 命令转变为一个迭代循环,这是任何编码者工具箱中的必备项目,并且可能是最常用的命令变体。我至今讨论的循环都在遍历一系列文件、目录或某种文本。然而,这个选项将 for 命令变成一个通过某个数值或步长从一个数字递增或递减到另一个数字,并且有一个固定结束值的循环。大多数编程语言都会以某种方式实现迭代循环。事实上,许多语言也有一个专门用于此目的的 for 命令。批处理脚本的独特之处在于,for 命令做了这么多其他事情。
将命令转变为迭代循环的一般语法是:
for /L %%`variable` in (`start`, `step`, `end`) do `command`
除了添加 /L 选项外,这与没有选项的 for 命令之间唯一的区别是括号内的数据,在这里三个用逗号分隔的数字组成了输入。第一个是起始值或起始索引;第二个是步长,即每次循环迭代中索引递增的量;最后一个是结束值,即索引可以达到的最后一个值。
例如,这个 /L 循环从值 1 开始,每次迭代步进 2,直到 3 结束:
for /L %%i in (1, 2, 3) do > con echo Index is %%i
这个命令在循环的第一次迭代中将 for 变量 %%i 设置为 1;然后,它以步长 2 递增,使得 %%i 第二次通过时变为 3。这与结束值匹配,因此循环不再执行。以下是输出到控制台的内容:
Index is 1
Index is 3
以下 for 命令从变量设置为 10 开始,然后每次递增 1,直到达到 12:
for /L %%i in (10, 1, 12) do > con echo Index is %%i
这会遍历索引 10、11 和 12。
要递减索引,请将步长赋值为负数:
for /L %%i in (2, -1, 0) do > con echo Index is %%i
这会导致索引从 2 降到 0:
Index is 2
Index is 1
Index is 0
所有三个数值输入都可以是负数:
for /L %%i in (-1, -3, -7) do > con echo Index is %%i
这个 for 循环会对这三个索引执行:
Index is -1
Index is -4
Index is -7
给定以下的起始值和步长,显然这个 for 命令生成了一个从 10 开始的递增的正数 10 的倍数序列:
for /L %%i in (10,10,35) do > con echo Index is %%i
更不清楚的是序列到底在哪里结束。35 的结束值不是 10 的倍数,因此不在序列中,但一旦索引大于 35,循环就会结束,所以 30 是序列中的最后一个数字。如果结束值是 30、39 或任何介于它们之间的整数,命令在功能上是等效的,但为了清晰起见,30 会是最好的选择。此外,请注意,我在输入中省略了逗号后的空格。通常,我会为了可读性添加空格,但这个例子展示了它们并不是必需的(并且很容易被忽略)。
幂函数例程
不幸的是,Batch 不支持幂函数。在第六章中,我提到我们可以为此任务编写一个简短的例程,这里是承诺中的例程,它使用了一个迭代循环。它接受三个参数:指数的基数、指数值以及包含结果的返回变量的名称:
:Pow
set %3=1
for /L %%i in (1, 1, %2) do set /A %3 *= %1
goto :eof
(如果返回参数让你困惑,可以回到第十一章。)
我将返回参数初始化为 1。然后,循环从 1 开始,每次递增 1,直到达到指数或第二个参数的值。因此,如果指数是 n,循环执行 n 次。循环内的命令将返回参数乘以指数的基数或第一个参数。因此,如果基数是 b,这个 for 循环将 n 个 b 相乘。当循环完成时,返回参数包含 b**^n,例程结束。
要找到 53,请调用例程并传递这三个参数:
call :Pow 5 3 pow
> con echo Five cubed = %pow%
代码将 3 个 5 相乘,并将结果 Five cubed = 125 输出到控制台。
案例研究
作为 18 世纪末的七岁数学神童,伟大的数学家卡尔·弗里德里希·高斯和全班其他同学一起,从老师那里接到了一项繁重的任务。学生们需要将 1 到 100 的所有数字加起来。过了一会儿,老师抬起头,看到所有的孩子都在忙着用粉笔和石板做加法,只有一个人没有。他走近年轻的高斯,准备严厉训斥,结果发现高斯的石板上写着正确答案 5050。
高斯意识到有 50 对数字加起来是 101(100 + 1,99 + 2,...,51 + 50)。他迅速将 50 乘以 101,写下答案,然后坐回去等待同学们完成,可能在想他们怎么还没做完。
这个故事有其他不同的版本,虽然它可能是传说,但如果那些其他的学生能够使用一台 Windows 电脑,他们也许能在年轻天才之前就完成任务——前提是他们能够快速键入以下内容:
set sum=0
for /L %%i in (1, 1, 100) do (
set /A sum += %%i
)
> con echo The sum is %sum%.
这个循环从 1 到 100 迭代%%i 索引,其中 set /A 命令会累加所有索引的值。在循环之前,set 命令明确地将 sum 初始化为 0,从而保证最终结果是所需的值。高斯的技术娴熟的同学们或许能在与高斯本人相当的时间内,把输出“总和是 5050”写到他们的石板上。
总结
在本章中,你学习了 for 命令的三个选项。/D 选项允许命令枚举目录而不是文件,/R 选项则使用递归来遍历子目录。我甚至演示了递归枚举目录的两种方法。
你还学习了如何使用/L 选项创建一个迭代循环,并掌握了它的所有方面。我使用了带此选项的 for 命令来创建一个将一个数字提升为另一个数字的例程,甚至还顺便讲了一下快速的数学历史课程以及一点批处理算术。
现在只剩下一个选项了,你将在下一章学习它。它允许读取文件以及更多操作。
第十九章:19 读取文件和其他输入

到目前为止,功能强大的 for 命令的最复杂语法和最强大的功能是通过 /F 选项实现的,它允许进行文件读取。没有选项的 for 命令已经能够处理文件,比如获取文件的名称、路径、驱动器、大小和属性,几乎涵盖了与文件相关的所有内容,除了文件的实际内容。接下来就是 /F 选项。
在介绍语法后,我将演示如何逐条记录地读取文件。你将学习如何操作输入数据,将记录的全部内容放入变量中,或将其拆分为批处理所称的 tokens。通过对数据进行标记化处理,你将能够提取你需要的特定数据部分。
尽管 for /F 命令表面上是用于读取文件的,但输入也可以采用其他形式。这个命令可以将字符串视为单个记录文件,甚至可以将另一个批处理命令的输出作为其输入。所有这些将允许你以多种不同的方式处理和操作不同形式的信息。
含 /F 选项的 for 命令
/F 选项将 for 命令转变为一个多功能的文件读取工具。带有此选项的命令有多种形式,但我将首先考虑其中一种。以下是读取文件或文件集的 for /F 命令的通用语法:
for /F ["`suboptions`"] %%`variable` in (`file-set`) do `command`
与没有选项的 for 命令相比,这种语法有很大不同。最明显的是,/F 选项已经到位。我还将括号内的通用输入占位符更改为更具体的术语 file-set,但这仅仅是命名上的不同。与没有选项的命令版本相比,这个变体只接受一个文件、文件掩码或多个文件或文件掩码。
最显著的区别是增加了["suboptions"]子句。方括号表示这些子选项本身是可选的,但如果包含它们,必须用双引号括起来。每个可能的子选项都有自己的特定关键字。我将在本章稍后讨论其中一个子选项 usebackq,但其他子选项会直接影响你如何读取和操作数据。
读取文件内容
关键字 tokens、delims、skip 和 eol 可能不容易说出口,但它们是子选项子句的关键元素,控制着文件的读取。了解它们的最佳方法是探索没有选项的 for /F 命令的行为,然后逐一添加与这四个关键字相关的子句,以展示它们的用途。
让我们回到第十七章中 for 命令的介绍部分,在那里我简要处理了一个名为FourBrits.txt的文件,但没有提及其内容。对于这个演示,假设这是一个包含四条记录的小文件。每条记录有五个元素,第一个是语言,其余四个是该语言中写出的四个特定男性名字。元素之间由空格或制表符分隔,下面是文件的完整内容:
English John Paul George Richard
Spanish Juan Pablo Jorge Ricardo
French Jean Pol Georges Richard
Italian Giovanni Paolo Giorgio Riccardo
考虑这个 for 命令:
for %%a in (C:\Batch\FourBrits.txt) do (
> con echo %%a
)
输出正是你现在期望的:一行数据,显示输入文件的路径和文件名:
C:\Batch\FourBrits.txt
为了演示新选项,我将在逻辑中添加/F:
for /F %%a in (C:\Batch\FourBrits.txt) do (
> con echo %%a
)
现在是输出:
English
Spanish
French
Italian
这样一个小小的变化导致了截然不同的结果。现在,代码不是以文件名的形式向控制台写入单条信息,而是输出文件中每条记录的第一个单词。由于/F 选项,命令正在打开并读取文件的内容。还有很多工作要做,但命令实际上是在读取文件中的每一条记录,并将%%a 设置为记录中的第一个单词。如果文件包含 64 条记录,那么之前的逻辑将在控制台上写入 64 个单词,每个单词占一行。
这个输出引发了更多问题,而不是解答了它们。为什么这不是写入整个记录?我们可以只解析记录的一部分吗?我们必须读取每条记录吗?这就是通用语法中的 suboptions 子句的关键字发挥作用的地方。
标记化数据
在 Batch 中,一个标记是记录的一个元素,默认情况下,每个标记由空格或制表符分隔。检查文件中的第一条记录,它正好包含五个标记:
English John Paul George Richard
默认情况下,Batch 将标记数量设置为 1。这解释了为什么在之前的示例中,解释器将%%a 解析为每条记录中的第一个单词(或标记),并丢弃了记录中的其余部分。
要更改这个默认值,你可以使用 tokens 子句;它是 tokens 关键字,后面跟着一个等号和设置,在这种情况下是 2:
for /F "tokens=2" %%a in (C:\Batch\FourBrits.txt) do (
> con echo %%a
)
tokens 子句告诉解释器将哪个标记或哪些标记作为 for 变量传递到循环中。执行此代码会产生以下输出,写入控制台:
John
Juan
Jean
Giovanni
由于 tokens 子句,解释器现在将第二个单词或标记传递到循环中,将 John 及其翻译写入控制台。将其设置为 5 将检索到 Richard 的名字。通过这个子句,你可以相对轻松地提取单个标记。
提取多个标记
能够提取单个标记很不错,但能够提取多个并选择标记会更有用。要提取所有五个标记,将文本 1-5 分配给 tokens 关键字。但这引出了一个有趣的问题。如何仅通过定义单个变量%%a 来解析这五个标记呢?
额外的变量以简单的字母顺序优雅地表示。如果%%a 是 for /F 命令中定义的变量,它解析为标记子句中定义的第一个标记,接着%%b 解析为第二个,%%c 解析为第三个,%%d 解析为第四个,最后%%e 解析为第五个。考虑以下内容:
for /F "tokens=1-5" %%a in (C:\Batch\FourBrits.txt) do (
> con echo %%a: %%b, %%c, %%d, and %%e
)
这段代码将记录格式化,使语言后跟冒号和两个空格。然后,在输出中,名称之间用逗号和空格分隔,最后一个名字前有一个连接词:
English: John, Paul, George, and Richard
Spanish: Juan, Pablo, Jorge, and Ricardo
French: Jean, Pol, Georges, and Richard
Italian: Giovanni, Paolo, Giorgio, and Riccardo
你也可以选择提取特定的标记,而不是一个范围。在标记子句中,用逗号分隔所需的标记。此代码仅将第二个和第四个标记传入循环:
for /F "tokens=2,4" %%a in (C:\Batch\FourBrits.txt) do (
> con echo %%a and %%b played guitar.
)
结果将写入控制台:
John and George played guitar.
Juan and Jorge played guitar.
Jean and Georges played guitar.
Giovanni and Giorgio played guitar.
现在,数据记录中的第二个标记是标记子句定义的第一个标记,因此 Batch 将其分配给%%a,而不是%%b。同样,%%b 现在解析为记录中的第四个标记,也就是第二个选中的标记。简而言之,变量字母模仿的是标记子句中定义的标记位置,而不是它在记录中的位置。
到目前为止,我使用了%%a 作为 for 变量。无论你选择哪个字母作为变量,解释器都会将后续标记分配给字母表中的相应字母。之前的代码在功能上等价于以下代码:
for /F "tokens=2,4" %%x in (C:\Batch\FourBrits.txt) do (
> con echo %%x and %%y played guitar.
)
唯一的区别是我将%%a 改为了%%x,这意味着%%y 现在解析为第二个标记,而不是%%b。
需要注意的是,选择%%x 作为 for 变量限制了你只能处理三个可能的标记,因为在%%z 之后没有更多的标记可以分配(尽管《超越斑马》(On Beyond Zebra!)中的 Dr. Seuss 努力过)。你不会经常看到这种情况,但数字也有效。例如,如果你提取三个标记并将 for 变量定义为%%7,解释器将使用%%8 和%%9 作为隐式标记。
该关键字还接受数字、逗号和短横线的组合。例如,tokens=1,3-5 子句处理除 John(第二个标记)之外的所有标记。
提取记录的其余部分
关于标记关键字,还有另一个变化。星号表示输入记录的其余部分,包括嵌入的空格。考虑这段使用 2* 的代码在标记子句中的情况:
for /F "tokens=2*" %%a in (C:\Batch\FourBrits.txt) do (
> con echo %%a, Rest of the Band: "%%b"
)
这个命令将第二个标记分配给%%a,因为子句中的 2;然后解释器将记录的其余部分,即第三、第四和第五个标记,包括任何嵌入的空格和制表符,分配给%%b,因为星号(*)。语言,即每个记录中的第一个标记,在输出中找不到:
John, Rest of the Band: "Paul George Richard"
Juan, Rest of the Band: "Pablo Jorge Ricardo"
Jean, Rest of the Band: "Pol Georges Richard"
Giovanni, Rest of the Band: "Paolo Giorgio Riccardo"
为了让这个讨论圆满结束,当我们在本章开始时仅仅为 for 命令添加了 /F 选项时,默认的标记子句只将第一个标记传递到代码块。在许多情况下,你会希望将整个记录完整地提取出来,正如它在输入文件中所呈现的那样作为一个整体。由于星号代表输入记录的其余部分,单独将它分配给标记就会选择整个记录:
for /F "tokens=*" %%r in (C:\Batch\FourBrits.txt) do (
> con echo Entire Record: "%%r"
)
同时请注意,我将变量改为 %%r。这里是输出结果:
Entire Record: "English John Paul George Richard"
Entire Record: "Spanish Juan Pablo Jorge Ricardo"
Entire Record: "French Jean Pol Georges Richard"
Entire Record: "Italian Giovanni Paolo Giorgio Riccardo"
之前使用 %%a 完全可以正常工作,但我在这里做了修改,原因有两个:首先,r 代表记录,其次,作为一个简单的提醒,任何字母字符都可以使用。
注意
如第十七章中所提到,for 变量实际上是区分大小写的,并且允许一些非字母数字字符。这意味着 %%a 和 %%b 仅能解析为定义为 %%a 的变量的前两个标记;%%A 和 %%B 不会生效,除非你将 for 变量改为 %%A。我可能不需要再提,但那些奇怪的字符(比如 %%#)根本不适合用来提取多个标记。
定义数据分隔符集
能够将输入记录分解为由空格和制表符等分隔符定义的标记确实非常强大,但在许多情况下,你无法控制正在读取的文件中的数据,而不同的分隔符,甚至是多个分隔符,会更加适用。delims 关键字定义了用于标记数据的一种或多种分隔符。
解析逗号分隔数据
逗号分隔格式是一种流行且简便的存储数据方式。例如,.csv 文件包含任意数量的逗号分隔记录,你可以使用 Excel 打开并查看数据。每个标记由逗号分隔,每个标记可以包含空格。在展示 Batch 如何读取 CSV 数据之前,我需要做一些设置。
FourBrits.txt 文件可能包含带有嵌入空格的名字,比如 Richard aka Ringo。在之前讨论的空格分隔示例中,这段文本包含三个不同的标记。我现在将重新格式化数据,并以逗号分隔的文件 FourBrits.csv 提供。该文件的内容将与其同名的 .txt 文件相同,只是分隔符和 Richard 的别名有所不同:
English,John,Paul,George,Richard aka Ringo
Spanish,Juan,Pablo,Jorge,Ricardo aka Ringo
French,Jean,Pol,Georges,Richard aka Ringo
Italian,Giovanni,Paolo,Giorgio,Riccardo aka Ringo
下一步任务是修改 for /F 命令,使其将逗号作为分隔符而不是空格,我将通过 delims 关键字实现这一点。当关键字不存在时,解释器会默认为只包含空格和制表符字符的分隔符集。在以下示例中,delims=, 将分隔符集定义为单个字符,即逗号:
for /F "tokens=1-5 delims=," %%a in (C:\Batch\FourBrits.csv) do (
> con echo %%a: %%b, %%c, %%d, and %%e
)
当解释器遇到第五个也是最后一个标记——一个名字后跟别名 Ringo 时——它会将文本写入控制台,保留其中的空格:
English: John, Paul, George, and Richard aka Ringo
Spanish: Juan, Pablo, Jorge, and Ricardo aka Ringo
French: Jean, Pol, Georges, and Richard aka Ringo
Italian: Giovanni, Paolo, Giorgio, and Riccardo aka Ringo
Ringo 是不可翻译的。
使用管道分隔的数据文件也很流行;delims=|子句解析输入数据中的管道,这样内嵌的空格和逗号就不会创建额外的标记。等号甚至可以作为分隔符,使用 delims==子句。(你很快会看到相关示例。)
定义多个分隔符
你甚至可以通过一个 delims 子句定义多个分隔符。以下代码将分隔符集合定义为逗号和空格。虽然很微妙,但与之前的示例相比,唯一的变化是我在 delims=和后续的双引号之间添加了一个空格:
for /F "tokens=1-5 delims=, " %%a in (C:\Batch\FourBrits.csv) do (
> con echo %%a: %%b, %%c, %%d, and %%e
)
现在,解释器每次遇到空格或逗号时,都会看到一个新的标记。第五个标记将仅包含名字 Richard,而 aka 和 Ringo 分别被分配到未分配的第六个和第七个标记。
为了展示更大范围的分隔符,我将使用一个名为Alphabets.txt的单记录文件,其中包含大写和小写的拉丁字母,中间用一个空格隔开:
ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz
完全省略 delims 子句将调用默认的分隔符集合,并仅生成两个标记:大写字符集和小写字符集,除此之外没有更多内容。相反,在以下代码中,我将 delims 关键字设置为 Delims 文本和一个尾随空格:
for /F "tokens=1-8 delims=Delims " %%a in (C:\Batch\Alphabets.txt) do (
> con echo Token 1 = %%a
> con echo Token 2 = %%b
> con echo Token 3 = %%c
> con echo Token 4 = %%d
> con echo Token 5 = %%e
> con echo Token 6 = %%f
> con echo Token 7 = %%g
> con echo Token 8 = %%h
)
如果数据中每个分隔符仅出现一次,似乎逻辑上会产生八个标记。虽然这不一定是正确的,但我已经通过 tokens=1-8 子句定义了这么多个标记,并添加了相同数量的 echo 命令。让我们关注一下生成的标记数量。以下是结果输出:
Token 1 = ABC
Token 2 = EFGHIJKLMNOPQRSTUVWXYZ
Token 3 = abcd
Token 4 = fgh
Token 5 = jk
Token 6 = nopqr
Token 7 = tuvwxyz
Token 8 =
这里有许多需要注意的点,包括分隔符集合是区分大小写的事实。大写字母 D 是一个分隔符,但小写字母 d 不是。小写字母 e、l、i、m 和 s,以及空格,组成了完整的分隔符集合。当查看原始数据,并将除了空格以外的分隔符加粗时,更容易理解这七个标记的来源:
ABC**D**EFGHIJKLMNOPQRSTUVWXYZ abcd**e**fgh**i**jk**lm**nopqr**s**tuvwxyz
注意,这些数据中列出了两个分隔符(l 和 m)按顺序排列,并且由于它们之间没有任何内容,你可能会预期解释器将为第六个标记分配空值或空白,并将其后面的文本 nopqr 移到第七个标记。然而,Batch 将连续的分隔符(lm)视为一个单一的分隔符,因此第六个标记是文本 nopqr。因此,tuvwxyz 是第七个标记,且由于它后面没有内容,第八个标记的值为空。
注意只使用你已定义的标记。如果子句是 tokens=1-7,最终的 echo 命令将导致 Token 8 = %h,因为%%h 不会是第八个标记;它将是一个百分号转义另一个百分号,后面跟着一个硬编码的字符。
在解析逗号分隔(或管道分隔)数据时,许多编译语言将连续的分隔符视为两个分隔符,并尊重它们之间的空值,这完全是合理的。然而,Batch 通常以空格作为分隔符,只需看看原始的FourBrits.txt 数据,其中每个单词之间的空格数量不一,就可以清楚地看到,视多个空格为单一分隔符也是非常合乎逻辑的。
不幸的是,这确实会带来一些挫败感,尤其是当数据不是以空格,而是逗号分隔时,但在第二十章中,我会向你展示如何让 for 循环尊重连续分隔符之间的空值。然而,如果我能够控制仅以逗号分隔的输入数据,我会总是在两个逗号之间写入一个空元素作为空格。
我将空格放在分隔符集合中的最后,因为它必须排在最后。我不想再次贬低解释器,但如果空格出现在其他位置,它会感到困惑。因此,在将空格包含在分隔符集合中时,delims 子句必须放在子选项的最后,紧接在结尾的双引号之前。
跳过头部记录
如你刚刚看到的,tokens 和 delims 关键字有很多微妙之处。接下来的几个关键字则简单得多,特别是 skip 关键字,它可以让你在读取文件时跳过任意数量的头部记录。
让我们回到原始的FourBrits.txt 文件,在接下来的四条记录之前添加一个头部记录:
Four Liverpudlians in Four Languages:
English John Paul George Richard
Spanish Juan Pablo Jorge Ricardo
French Jean Pol Georges Richard
Italian Giovanni Paolo Giorgio Riccardo
让我们运行来自 tokens 关键字讨论的相同的 for /F 循环:
for /F "tokens=1-5" %%a in (C:\Batch\FourBrits.txt) do (
> con echo %%a: %%b, %%c, %%d, and %%e
)
它将头部行作为其他行一样处理,输出以下垃圾数据,后面跟着四行有效输出(我只展示其中一行):
Four: Liverpudlians, in, Four, and Languages:
English: John, Paul, George, and Richard
幸运的是,skip 关键字定义了在文件开头要跳过的记录数。这样就会跳过第一条记录,并完美处理数据记录:
for /F "tokens=1-5 skip=1" %%a in (C:\Batch\FourBrits.txt) do (
> con echo %%a: %%b, %%c, %%d, and %%e
)
如果有三条头部记录,skip=3 子句会使 for 命令从第四条记录开始。
抑制注释记录
虽然 skip 关键字仅跳过文件开头的记录,但 eol 关键字(表示行尾注释字符)跳过文件中所有以定义字符开头的记录。如果我要宽容一点的话,我会说,当解释器在记录的第一个字节中看到定义的字符时,它会将该字符视为行尾,并将剩余部分视为注释。
为了演示,我们再次编辑输入文件。这一次,我添加了第二条注释记录,更重要的是,每条注释记录现在都以句点开头:
.Four Liverpudlian in Four Languages:
English John Paul George Richard
Spanish Juan Pablo Jorge Ricardo
French Jean Pol Georges Richard
.This is a random and annoying mid-file comment with no discernable purpose.
Italian Giovanni Paolo Giorgio Riccardo
向相同的 for 命令中添加 eol=. 子句指示解释器跳过所有注释行,无论它们在数据中出现的位置如何,并只处理其余的记录:
for /F "tokens=1-5 eol=." %%a in (C:\Batch\FourBrits.txt) do (
> con echo %%a: %%b, %%c, %%d, and %%e
)
不幸的是,这个简单明了的关键字也有它自己的限制。鉴于 delims 子句接受一组分隔符,你可能期望 eol 子句接受一个或多个字符。但实际上,它只接受一个字符。
术语 tokens、delims、skip 和 eol 可能不像 John、Paul、George 和 Ringo 那样朗朗上口,但这四个精彩的关键字提供了极大的灵活性,本章及其他章节中还会有更多与这些关键字相关的子句示例。
定义输入
你现在知道如何使用 for /F 命令读取一个文件,但你可能还不知道如何读取一个文件——这之间是有区别的。传统的文件是由存储在驱动器上的一个可引用位置中的记录组成的字节集合,这些记录通过目录和文件名进行定义。而文件是某种可以被 for /F 命令读取的输入;它可以采取不同的形式,并不一定是编辑器能够打开的东西。
除了传统的文件,这个命令还可以接受字符串作为输入,也可以接受另一个批处理命令的输出。正如你将很快看到的那样,语法中的微小差别决定了输入的类型。
文件输入
文件集输入是传统的文件或一组文件。我在本章中已经展示了几次这种形式的示例。每个示例中括号内都包含一个文件,现在我们知道这些输入是文件,因为没有任何围绕的引号,无论是单引号还是双引号。你还没有看到的是一组文件。以下命令按文件集中的列出顺序读取两个文件的内容,文件之间由逗号、空格或两者的组合作为分隔符,方便阅读:
for /F "tokens=1-5" %%a in (FourBrits.txt, MoreBrits.txt) do (
> con echo %%a: %%b, %%c, %%d, and %%e
)
假设第一个文件 FourBrits.txt 有四条记录,for /F 命令按顺序处理每一条记录,向控制台写入四行。解释器接着关闭该文件并打开 MoreBrits.txt。此文件中的每一条记录都会触发一次代码块内逻辑的执行,并向控制台再写入一行。我还删除了文件路径,因此解释器将在当前目录以及路径变量中定义的目录中查找这些文件。
字符串输入
以下是使用字符串作为输入的 for /F 命令的一般语法。请注意括号内的输入:
for /F ["`suboptions`"] %%`variable` in ("`string`") do `command`
这种形式将输入文本替换为“字符串”。双引号告诉解释器它们包围的是某种文本,而完全没有引号则告诉解释器考虑输入为文件或一组文件。其他部分的工作方式完全相同。
以下 for /F 命令与本章前面使用的其他示例类似,唯一不同的是输入:
for /F "tokens=1-5" %%a in ("Italian Giovanni Paolo Giorgio Riccardo") do (
> con echo %%a: %%b, %%c, %%d, and %%e
)
唯一的区别是,我已将 FourBrits.txt 的最后一行文本移除了一些额外的空格以提高可读性,并将其作为被双引号包围的字符串输入。
代码块中的 echo 命令并不知道这五个标记的来源,是否来自硬编码的字符串或从文件读取的记录。这里是我们熟悉的结果:
Italian: Giovanni, Paolo, Giorgio, and Riccardo
在这个例子中,括号内的文本是硬编码的,但你也可以使用包含文本的变量,只要它被双引号括起来。你可以使用任何字符串作为输入,解释器会将其视为一个单一记录文件。
这个特性使得复杂的字符串解析成为可能。实际上,我在 第五章 中使用了这个功能,却完全没有解释它是如何工作的,承诺稍后会解释。for /F 命令将布尔值转换为布尔字符串,其中 bGod 布尔值包含两个值中的一个,true==true 或 false==x。当时我提到,尽管将其归结为不过是个魔术技巧,这个命令会去掉两个等号及其后的所有内容,从而将 bStrGod 的值设为 true 或 false:
for /F "delims==" %%b in ("%bGod%") do set bStrGod=%%b
通过本章所学,你现在可以弄清楚它是如何工作的。
for /F 命令将括号内双引号中的解析布尔值视为字符串或文本输入。delims 子句将等号定义为唯一的分隔符,并且由于我没有定义 tokens 关键字,所以隐式地采用了默认子句 tokens=1。结果是,等号前的词是第一个标记,解释器将其解析为 %%b(当然是为 boolean),同时丢弃等号后面的第二个标记。set 命令然后在第一次也是唯一一次执行时将第一个标记赋值给布尔字符串。
所以,这根本不是什么魔术,而仅仅是一种简洁的方法,通过一行代码从更大的字符串中提取一个单一的分隔值。请看 for /F 命令及其多个输入的强大功能。
命令输入
括号内的内容决定了任何 for /F 命令的输入。你知道没有引号表示一个文件,双引号表示一个字符串。单引号表示另一个 Batch 命令是输入,下面是它的一般语法:
for /F ["`suboptions`"] %%`variable` in ('`command`') do `command`
在单引号之间输入的任何 Batch 命令会先执行,其结果的标准输出(stdout)将作为 for /F 命令的输入。解释器随后会逐行处理它认为是输入的内容,如果有多行,就像处理一个文件一样。
为了展示命令作为输入的有用性,考虑在 第十三章 中介绍的 dir 命令。以下命令列出给定目录中的所有文本文件:
dir C:\Batch\*.txt /B
/B 选项只会生成文件名和扩展名的列表——没有文件日期和大小,也没有头部和尾部行。命令只会将这些信息写入标准输出供你查看,仅此而已。很多时候,你会想将这个文件列表拿来对每个文件执行某些逻辑。例如,让我们编写一些代码,将目录中所有 *.txt 文件的扩展名改为 *.bak,以表示它们是备份文件。
单引号告诉 for /F 命令输入的是一个命令。dir 命令生成一个文件名列表,每个文件名都带有扩展名但没有路径:
for /F "tokens=*" %%f in ('dir C:\Batch\*.txt /B') do (
ren "C:\Batch\%%f" "%%~Nf.bak"
)
tokens=* 子句确保 %%f 在遍历文件列表时将整个字符串作为其值。默认子句会丢弃名称中第一个嵌入空格后的所有内容。ren 命令重命名文件,使用文件名的修改符(N),并附加上硬编码的扩展名。
我希望你能理解这一点有多么令人印象深刻。三个命令被绑定在一起,只需几行代码就可以完成相当多的操作。对于经常使用它的人来说,这可能变得司空见惯,而对于其他人来说,可能会觉得有些压倒性,但在许多语言中,类似的操作没有中间文件是做不到的。使用这种技术,dir 命令不仅仅是将信息输出到控制台或文件中(虽然这也很有用)。输出的原始格式显然是为此目的设计的。你可以对任意数量的文件执行许多处理,无论是简单的还是复杂的。我会在深入一些实际应用时进一步展开。
输入类型的替代语法
对于某些输入,刚才讨论的语法根本不起作用。有时文件名包含空格,但将其用双引号括起来会使其变成字符串输入。如果字符串中包含双引号,你就无法将其用一对双引号括起来。同样,命令有时会包含单引号。这些情况就需要不同的语法,而 usebackq 关键字,作为 for /F 命令的最后一个子选项,是解决方案。
在深入之前,有些批处理缩写是显而易见的;我大概不需要告诉你 delims 是 delimiters 的缩写,tokens 是 tokens 的缩写。但你可能会对这个加密的文本 usebackq 感到困惑。这个关键字代表 use back quote,虽然这可能没什么帮助,但至少现在你知道看到它时该怎么发音了。
文件输入
展示 usebackq 关键字有用性的最佳方法是重新创建一种无疑促使其诞生的情境。到目前为止,本章中的许多示例都读取了一个名为 FourBrits.txt 的文件。我很少在文件名中嵌入空格,但别人可能会将这个文件命名为 Four Brits.txt。
为了展示空格可能带来的问题,我从之前的内容中直接复制了以下 for /F 命令,只是文件名现在包含了一个嵌入的空格:
for /F "tokens=1-5" %%a in (C:\Batch\Four Brits.txt) do (
> con echo %%a: %%b, %%c, %%d, and %%e
)
批处理将此视为两个文件(而不是一个),可能会失败,因为找不到名为 Four 且没有扩展名的文件。显而易见的解决方案是将路径和文件名用双引号括起来,但 for /F 命令会将其视为文本字符串而不是文件名,因此这种方法行不通。
解决方案是使用 usebackq 关键字 并 在输入周围加上双引号:
for /F "usebackq tokens=1-5" %%a in ("C:\Batch\Four Brits.txt") do (
> con echo %%a: %%b, %%c, %%d, and %%e
)
这个关键字还使处理包含特殊字符(如括号)的路径和文件名变得更容易。
for /F 命令成功地读取文件并产生与之前相同的输出。你甚至可以读取多个文件,每个文件的名字中都有空格:
for /F "usebackq tokens=1-5" %%a in ("Four Brits.txt", "More Brits.txt") do (
> con echo %%a: %%b, %%c, %%d, and %%e
)
注意,文件名由逗号分隔,并且每个文件名都被一对双引号括起来。
这是使用文件集作为输入并带有 usebackq 关键字的命令的一般语法:
for /F "usebackq [`suboptions`]" %%`variable` in ("`file-set`") do `command`
这个关键字改变了规则。双引号可以包围每个文件名,尽管它在没有双引号的情况下仍然与关键字一起工作,前提是文件名中没有嵌入空格。
字符串输入
usebackq 关键字解决了一个问题,但又创造了另一个问题。它允许在文件名周围使用双引号,但这似乎禁止了处理字符串的能力。解决方案是将文本用不同的字符括起来,这个字符就是单引号。以下是一般语法:
for /F "usebackq [`suboptions`]" %%`variable` in ('`string`') do `command`
因此,以下两个 for 命令在功能上是等价的:
for /F "tokens=1-4" %%a in ("Larry Moe Curly Iggy") do break
for /F "usebackq tokens=1-4" %%a in ('Larry Moe Curly Iggy') do break
如果没有 usebackq,我将 Stooges 的简短列表括在双引号中,而当关键字存在时,单引号会将相同的文本括起来。(我还使用了 break 或 无操作 命令,使这些命令在关注关键字和引号类型的同时有效,但这些命令不会产生任何输出。)
为什么 Batch 提供两种执行相同任务的方法?为了回答这个问题,我将使用一串芝加哥机场的名称,每个机场名出于某种未知原因都被括在括号中,作为输入。以下两个命令在功能上是等价的:
for /F "tokens=1-2" %%a in ("(O'Hare) (Midway)") do break
for /F "usebackq tokens=1-2" %%a in ('^(O'Hare^) ^(Midway^)') do break
第一个示例,没有使用 usebackq,要更加易于操作。你必须转义特殊字符,如括号,因为缺少双引号。但如果没有关键字,这些字符只是出现在双引号中的一部分。
但是,在双引号字符串中,唯一永远不起作用的字符就是孤立的双引号本身——即使它被转义。回到 Stooges,虽然 Iggy Pop 是 The Stooges 的主唱,但他显然不是 The Three Stooges 的成员,所以我可能想用引号或者双引号把他的名字括起来,以表示讽刺:
for /F "usebackq tokens=1-4" %%a in ('Larry Moe Curly "Iggy"') do (
> con echo The Four Stooges are %%a, %%b, %%c, and %%d.
)
(解释器足够智能,可以处理双引号字符串中的双引号个数是偶数的情况,但为了清晰起见,当字符串中包含任何双引号时,我总是使用关键字并将整个输入用单引号括起来。)
这个 for /F 命令将以下内容写入控制台:
The Four Stooges are Larry, Moe, Curly, and "Iggy".
(可怜的 Shemp,大家都忘了 Shemp。)
命令输入
你刚刚学会了在使用 usebackq 和字符串输入时使用单引号,因此你不能再用它们来括起命令。但是,这种通用语法允许你将关键字与命令作为输入一起使用:
for /F "usebackq [`suboptions`]" %%`variable` in (``command``) do `command`
在某些字体中,括住命令输入的两个字符可能看起来像单引号,但它们是全新的字符。
这让我们回到那个悬而未决的问题,关于文本 usebackq 或 use back quote 的含义。这个名称源于那些弯曲的单引号,它们把命令括起来,实际上这些单引号更准确地说是反引号(或称为反撇号)。反引号的键在大多数键盘上位于 TAB 键的上方和数字 1 键的左侧。这个键有双重功能,按住 Shift 键可以输入波浪号。它被 relegated 到键盘的底层区域,因为大多数人几乎从不使用它,但它是 Batch 编程者的一个重要键。
以下 for /F 命令是功能等效的;第一个模仿了前一节的一个命令,第二个使用了关键字和反引号:
for /F "tokens=*" %%f in ('dir C:\Batch\*.txt /B') do break
for /F "usebackq tokens=*" %%f in (`dir C:\Batch\*.txt /B`) do break
第一行支持命令中可能包含反引号的情况,而你会使用第二行—带有 usebackq 关键字的语法—当命令可能包含单引号时。
何时使用 usebackq
你可以在有或没有 usebackq 的情况下实现每种类型的输入,那么应该什么时候使用这个关键字呢?简单来说:无论何时可能,我都会对文件集和命令输入使用 usebackq,对于字符串输入则不使用。
用双引号括起输入能够提供最大的灵活性,并且应该在可能的情况下使用。它支持输入中嵌入空格和许多可能需要转义的字符。因此,在处理文件集时,我使用双引号和 usebackq,而在处理字符串时,我使用双引号并省略该关键字。
当输入是一个命令时,双引号不会参与计算。有些人可能会跳过关键词来节省一些按键,但我使用 usebackq 关键字和反引号将输入括起来,原因有两个。首先,虽然单引号和反引号在命令中都不太常见,但反引号更不常见。更重要的是,当你看到单引号时,你会立刻问自己输入是字符串还是命令;而反引号消除了所有的歧义,它就是一个命令。
如果你实施我的建议,最终结果是任何查看你 for /F 命令的人都能够从输入中推断出很多信息。如果它被反引号括起来,那就是一个命令;如果它被双引号括起来,焦点就转移到了 usebackq 上。如果存在,那输入就是一个文件集;如果没有,那它就是一个字符串。还有三种形式在“蓝月亮”时可用,但你会比看到两次满月还要少见到包含双引号的输入字符串或包含反引号的输入命令。
关于 usebackq 的最后一个提示,如果你觉得这个关键字还不够难懂,q 是可选的。你可以用它的同义词 useback 替换本章代码示例中出现的每一个 usebackq 实例。我通常支持减少按键次数,但这样做会在已经对许多人来说难以理解的东西上加上一层面纱。
实际应用
读取文件的能力,以及将字符串和命令输出当作文件来处理的能力,具有许多应用。真正理解 for /F 命令可以接受的不同类型的输入,将使你能够提出富有创意的解决方案,解决一些曾经看似难以克服的问题。
仅处理文件夹中的大文件
在第十七章中,我探讨了一个应用程序,该程序将多个文件传递到一个 bat 文件中逐个处理。在这个例子中,我们仍然会逐个处理文件,但我们不再使用参数列表,而是处理某个特定文件夹中的所有.txt文件。为了让它更有趣一点,我们只处理大文件,定义为至少 100KB 大小的文件。
你可以将 dir 命令作为输入传递给 for /F 命令,前提是使用 usebackq 关键字并将命令放入反引号中。裸格式选项/B 会生成一个没有头部的干净文件列表,非常适合这个目的,但使用这个选项时会丢失文件大小,因此我们暂时放弃这个想法。
然而,不使用/B 选项会带来其他挑战。文件的日期和时间会被列出,并且还会有头部和尾部记录。你需要使用每个文件的大小来评估它是否足够大以进行处理,但显示的文件大小中的恼人逗号会成为一个麻烦。
在第十三章中,我提到过 dir 命令的/-C 选项会抑制逗号,但要弄清楚如何解析其输出中的内容,你需要查看一个示例。除非你拥有比我更为详细的记忆,否则构建任何复杂的 for /F 命令时,内嵌命令的执行是一个必不可少的步骤,该内嵌命令位于反引号中,你需要执行它并检查其输出。以下是命令,其中 workDir 是相关文件夹:
dir %workDir%\*.txt /-C
它可能生成如下内容:
Volume in drive C is OS
Volume Serial Number is 2E6D-DBF0
Directory of C:\Batch
12/25/2011 12:42 PM 538346 Big.txt
11/24/2011 05:22 PM 17234 Little.txt
09/05/2011 10:43 AM 10 Wee Tiny.txt
07/04/2011 07:22 PM 1864408 Wicked Big.txt
4 File(s) 2419998 bytes
0 Dir(s) 146181152768 bytes free
这里有很多内容需要解析,但通过这些数据,你可以构建剩余的 for /F 命令。你关注的是四个详细记录,但包括空记录在内的五个头部记录,你需要通过 skip=5 子句跳过它们。
接下来,你需要找出找到与文件大小和名称对应的标记的最佳方法。空格是默认的分隔符之一,在这里效果很好,但请注意,如果你想从数据中提取月份、日期、年份、小时或分钟,你会使用一个定义了正斜杠、冒号和空格的分隔符集的 delims 子句。由于空格是分隔符,你必须戴上解释器的帽子,寻找以空格为分隔符的标记。日期是第一个标记,时间由接下来的两个标记组成;由于内嵌的空格,上午/下午(AM/PM)成为第三个标记。因此,第四个标记是文件大小,其余数据是文件名。
为了表示大小,你将使用 %%s 作为 for 变量,这意味着 %%t 将是文件名及扩展名。(如果你使用 %%e 作为大小,那么 %%f 就变成了文件。)
可能会有诱惑认为文件名是第五个令牌,但文件名中可能包含空格,任何空格都会将其分隔成另一个令牌。幸运的是,文件名出现在数据的末尾,这意味着 tokens=4* 子句会将其分配为两个令牌。第四个令牌是文件大小,由于有星号,其余数据即文件名则成为第五个令牌。
你还没有完成,但可以将 for /F 命令的主要结构组合起来。这里讨论的所有内容都在这里,尤其是反引号中的 dir 命令:
for /F "usebackq tokens=4* skip=5" %%s in (`dir %workDir%\*.txt /-C`) do (
在调用处理大文件的逻辑之前,你需要处理两个 if 命令。第一个简单地验证文件大小 %%s 是否大于或等于 100000。for /F 命令允许你跳过头部记录,但尾部记录仍然会影响结果,因此你必须灵活处理。在 dir 命令的输出中,注意到两个尾部记录的第四个令牌是字节数,这肯定不适用于你感兴趣的详细记录。因此,你需要一个第二个 if 命令,通过检查同一个 %%s 变量来过滤掉这两条不需要的记录。
现在你可以将它们组合起来了:
for /F "usebackq tokens=4* skip=5" %%s in (`dir %workDir%\*.txt /-C`) do (
if %%s geq 100000 (
if /i "%%s" neq "bytes" (
call :ProcessBigTxtFile "%workDir%\%%t"
) ) )
如果 workDir 变量已填充为文本文件的工作目录,那么此代码就可以处理由 %%t 表示的大型文本文件了。此外,可能会觉得很奇怪,%%s 被用于数值和字母比较。在第四章中,我提到过,数值和字母值是有顺序的;字母被认为大于数字,因此字节数大于等于 100000 的条件子句总是为真。如果它一直为假,那么你本可以省略第二个 if 命令。
总是存在多个解决方案,看到以不同方式思考问题是一件很棒的事情。我打算再次尝试 dir 命令的 /B 选项。我知道它不会提供文件大小,但也许有其他方法可以获取它。我在第十七章中展示了如何使用修饰符来获取文件大小,使用不带选项的 for 命令。也许两个 for 命令会比一个更好。
我将从这个解决方案开始,它实现了嵌套的 for 命令:
for /F "usebackq tokens=*" %%f in (`dir %workDir%\*.txt /B`) do (
for %%g in ("%workDir%\%%f") do (
if %%~Zg geq 100000 (
call :ProcessBigTxtFile %%g
) ) )
外部 for 命令具有 /F 选项,并使用 usebackq 关键字,同时它将 %%f 用作变量。它的输入是一个带有 /B 选项的 dir 命令,并用反引号包裹。tokens=* 子句确保无论文件名中是否有空格,代码都会将每个文件名传入代码块,在这里我会立即为其添加路径,用双引号括起来,并将其作为输入传递给内部 for 命令。
这个内部命令没有使用 /F 选项,它使用%%g 作为变量,但除了%%f 之外,任何其他变量名都可以是良好的格式。我通过使用修饰符%%~Zg 来推导文件大小;如果文件足够大,call 命令会调用处理大文件的例程。
嵌套的 for 命令与非嵌套命令的行为差别不大,但一个重要的点是每个命令所选的 for 变量的字母字符。我本可以为两个命令都使用%%f,但我总是使用唯一的值,因为这样更清晰、更易读。此外,外部变量在内部命令中仍然可用,但前提是它没有被内部变量覆盖。
为了防止 for 变量冲突,它们不仅要不同,而且我还建议错开它们,以确保任何隐含的变量不会发生冲突。例如,如果外部命令从输入数据中提取三个标记并使用%%f 作为其 for 变量,那么%%g 和%%h 也应当避免在内部命令中使用。
你已经看到了两个解决同一问题的方法。一个使用了更复杂的命令来解析数据,另一个则使用了嵌套命令。哪种方式更好是有争议的,但无可争议的是,和其他语言一样,所有问题都有多种可能的解决方案。永远不要满足于一个解决方案。
全局文本替换
想象一下,你维护一个旧系统,运行着许多可执行文件,其中许多文件存放在一台旧服务器上。团队正在退役这台服务器\OldServer,并用恰如其分地命名为\NewServer的新服务器替换它。计划是将旧服务器上的每个文件迁移到新服务器,并保持相同的目录结构。
幸运的是,你的团队明智地实施并遵循了一致的命名规范,定义每个程序的变量都以 _pgm 开头。你控制其中一些变量的创建,但不是所有变量。你永远无法追踪到所有变量,但为了顺利切换到新服务器,你需要实时调整所有这些变量——也就是说,你需要将旧服务器的文本更改为新服务器的文本,而且你甚至无法获取所有变量名的列表。
第一个需要解决的问题是检索给定执行中的变量名列表。在第二章中,你学习了以下命令会生成所有当前活动的变量列表,这些变量名以 _pgm 开头:
set _pgm
如果仅设置了四个这样的变量,以下可能是命令的结果:
_pgmDBMrge=\\OldServer\Executables\DatabaseMerge.exe
_pgmSTElse=\\NewServer\Executables\SomethingElse.exe
_pgmTtlDiff=\\ThirdServer\Progs\TotallyDifferent.exe
_pgmVldtn=\\OldServer\Executables\Validation.exe
这些中的第一项和最后一项需要调整,第二项已经使用了新文本更新,第三项有一个完全不同的目录结构。你需要更新其中两个,而不改变另外两个。解决方案包括将前一个 set 命令作为 for /F 命令的输入。代码块将接受变量名和对应的值,并用更新后的文本重置该变量。
以下 for /F 命令是其中一种解决方案:
for /F "usebackq tokens=1,2 delims==" %%p in (`set _pgm`) do (
set tempPgm=%%q
set %%p=!tempPgm:\\OldServer\=\\NewServer\!
)
你将命令 set _pgm 放在反引号之间,并在 for /F 命令中使用了 usebackq 关键字。使用 dir 命令作为输入时,我强调了理解其输出的重要性,同样的道理也适用于 set 命令。查看四行示例输出,它们的共同点是变量名后面跟着一个值,两者之间用等号隔开。因此,你通过 delims==子句按等号进行分隔,并使用 tokens=1,2 子句提取两个令牌。for 变量%%p 是变量名,意味着%%q 是它的值。
这段代码仅用两个命令就能重置代码块中的变量。你将输入变量%%q 的原始值赋给临时变量 tempPgm。第二个命令将变量名%%p 重置为该值,并将\OldServer替换为\NewServer。注意,由于 tempPgm 在代码块中被设置并使用,因此必须使用感叹号来解析该变量的当前值。
这个逻辑会更新所有具有特定前缀的变量,同时忽略没有该前缀的变量。基于这一原则还有很多其他应用。你可以通过用一个命令 set %%p=替换代码块中的代码,并使用默认的 tokens 子句,来将所有类似名称的变量重置为 null。
文档说明
我对不同形式的 for /F 命令的语法做了一些调整,所以我将把你在这里看到的与帮助文档中的内容进行比较,并解释我的理由。首先,这是你通过运行 for /?命令可以看到的内容:
FOR /F ["`options`"] %`variable` IN (`file-set`) DO `command` [`command-parameters`]
FOR /F ["`options`"] %`variable` IN ("`string`") DO `command` [`command-parameters`]
FOR /F ["`options`"] %`variable` IN ('`command`') DO `command` [`command-parameters`]
or, if usebackq option present:
FOR /F ["`options`"] %`variable` IN (`file-set`) DO `command` [`command-parameters`]
FOR /F ["`options`"] %`variable` IN ('`string`') DO `command` [`command-parameters`]
FOR /F ["`options`"] %`variable` IN (``command``) DO `command` [`command-parameters`]
我列出了我几乎专门使用的三种形式,然后是我不常使用的几种:
for /F "usebackq [`suboptions`]" %%`variable` in ("`file-set`") do `command`
for /F ["`suboptions`"] %%`variable` in ("`string`") do `command`
for /F "usebackq [`suboptions`]" %%`variable` in (``command``) do `command`
for /F ["`suboptions`"] %%`variable` in (`file-set`) do `command`
for /F "usebackq [`suboptions`]" %%`variable` in ('`string`') do `command`
for /F ["`suboptions`"] %%`variable` in ('`command`') do `command`
首先,我比大多数程序员更喜欢小写字母。而且,我将["options"]子句替换为["suboptions"]。这个 for 命令已经有一个选项/F,我认为没有必要再引入另一种类型的选项,所以我选择了suboptions这个术语,但这两个调整都是表面上的。
我去掉了每行末尾的[command-parameters],因为这只是说明命令可能有参数,但这显而易见,不值得浪费额外的篇幅。为了真正正确地展示,我本可以显示额外的可选命令以及定义可能代码块的可选括号。为了让文档更具可读性,它必须保持一定的模糊性,因此我决定让它简洁明了。
另一个区别是,我对 for 变量%%variable 使用了两个百分号,而不是一个。帮助文档显示了如何从命令提示符执行命令,唯一的区别是它调用的是单个百分号。这是一本关于 bat 文件的书,而不是命令提示符,而且命令通常过于复杂,不适合在提示符下输入。
我明确地在使用 usebackq 关键字的形式中展示它,而帮助文档将其放在一个奇怪的标题后面。为了适应这一变化,我还将其他子选项放入方括号中,因为它们仍然是可选的。
最后的区别在于第一个形式,这也是最关键的一点。在研究这本书时,我感到非常震惊,发现两个使用文件集作为输入的形式在帮助文档中是相同的。添加 usebackq 关键字意味着现在双引号可以包含输入内容。从技术上讲,它们并不一定需要存在,但这不正是使用 usebackq 的意义所在吗?我很难想出在使用该关键字时我不想使用双引号的情况,因此我在文档中包含了它们,但请理解它们是可选的。
使用对你有意义的形式。更好的是,选择其中一个形式开始(我希望是我的形式),然后在你自己的 for 命令文档中进行构建。
拆解任何 for 命令
for 命令,尤其是跟随/F 选项时,可能会让人感到压倒性。你可以在仅几行代码中加入相当多的逻辑。当你遇到这样的构造时,可能很难准确知道从哪里开始分析。我多年来已经拆解了许多这样的命令(其中不少是我自己写的),每当我看到一个以 for 开头的命令时,我都会使用这些问题和步骤来弄清楚代码在做什么:
1. 什么是选项?是没有选项、/D、/R、/L 还是/F?
2. 根据选项,它使用的是什么类型的输入?如果没有选项,那么输入是文件、文件集还是值的列表?如果是/D 选项,期望输入为一个目录。如果是/R 选项,期望对一个或多个文件进行递归处理。如果是/L 选项,查看定义迭代循环的三个数字。在这四种情况下,按照输入的逻辑步骤通过代码块。
但是,如果选项是/F,那么你才刚刚开始拆解。命令使用的是什么类型的输入?如果没有引号,输入是文件集;如果是反引号,则是一个命令。如果是双引号,usebackq 将其转换为文件集;否则是一个字符串。如果是单引号,usebackq 将其转换为字符串;否则是一个命令。
3. 确定输入贡献了什么。文件集是否包含通配符?大概处理了多少个文件?文件或文件中的数据是什么类型?该字符串是硬编码的还是变量?命令做了什么,输出是什么?大致创建了多少行输出?数据的稳定性如何——也就是说,会有头部或尾部数据吗?
4. 检查子选项,并确定输入是如何被处理的。根据 tokens 和 delims 子句(或它们的默认值),会创建多少个标记,它们将如何从输入中填充?是否有记录通过 skip 或 eol 子句被丢弃?
5. 识别 for 变量,可能是 %%i,并确定与之相关的输入数据及其后续变量,可能是 %%j 和 %%k。
6. 检查代码块。确定 for 变量以及任何隐含变量的引用方式。是否有文件被复制、重命名、删除或以其他方式处理?
也许这些问题和步骤看起来令人难以应对,但为了展示它们的简单性,让我们拆解一些代码。某个刚认识的在线用户给了你这个命令,告诉你它会备份一些文件。只需要运行它:
for /F "usebackq tokens=1,3" %%x in (`xcopy * %userprofile%\%random%\`) do (
if "%%y" neq "copied" > %%x echo This file has been randomly moved.
)
但在执行之前,你应该先拆解它。使用之前的六个步骤:
1. 选项是/F。
2. 反引号包裹输入,所以它是一个命令。
3. xcopy 的源参数是星号,这意味着该命令将当前目录中的所有文件复制到一个奇怪命名的文件夹中,使用了两个伪环境变量,我还没有讨论(但会在第二十一章中讨论)。如果此命令成功复制五个文件,输出将是五个源文件的列表,后面跟着一条记录,写着:5 个文件已复制。
4. 基于 tokens 子句和缺少 delims 子句,该代码提取了从空格分隔输入中提取的第一个和第三个标记。对于大部分输出,第一个标记是源文件的完整路径和名称,假设文件名中没有嵌入空格,第三个标记为空。对于输入的最后一行,或者说记录的结尾,第一个标记解析为已复制的文件数量,而第三个标记是“copied”这个词。
5. %%x 是路径和文件名,直到输出的最后一行,当它变成已复制的文件数量时。%%y 完全为空(假设没有嵌入的空格),在处理输出的最后一行时解析为“copied”。到目前为止,一切正常。
6. 切换到代码块,if 命令过滤掉最后一条记录,其中 %%y 解析为特定文本,这意味着每个被复制的文件,echo 命令都会将其整个内容替换成看似模糊的威胁。
这看起来有点可疑。代码将当前目录中的所有文件隐藏起来,包括假设它在当前目录中的 bat 文件本身,放进一个随机命名的文件夹中。然后它擦除并替换每个源文件的内容,替换成一些文本。即使带有嵌入空格的文件名可能会被保留下来也无济于事。
因为你现在知道如何拆解一个 for 命令,所以你没有被这个 bat 文件欺骗。你没有执行代码,但已经推断出它是恶意代码——一个 bat 病毒。
概要
在这一章中,你了解了 for 命令的文件读取选项,并且学到了它不仅仅是读取文件。输入可以是文件、字符串,或者是命令的输出,每种都有不同的语法。我详细介绍了如何操作输入数据、创建标记,并在命令的代码块中使用这些标记的语法。我还通过一些现实世界的问题展示了这些功能,最后讨论了如何拆解这个重要的命令。
在下一章中,我将通过考察一些高级技巧来结束第二部分,这些技巧要么解决与使用 for 命令相关的问题,要么解决 for 命令使用中的问题。
第二十章:20 个高级for技巧

过去的三章展示了for命令的强大功能,但也演示了它使用起来的复杂性,甚至揭示了一些限制。在本章中,我将探讨一些高级主题,包括如何绕过这些限制的技巧。
在本章中,我将解释如何执行以下任务:
-
让解释器在
for /F命令查询的数据中,尊重连续分隔符字符之间的空值。 -
使用传统的 Batch,或者通过将 PowerShell 或 Python 命令嵌入 Batch 代码中,强制将字符串转换为大写(或小写),这是一种可以扩展到其他命令、语言和应用程序的技巧。
-
在
for命令的代码块中实现两个级别的延迟扩展 -
在
for命令中处理转义,尤其是使用delims子句中的双引号。
这些技巧将使你能够处理更多类型的数据和更复杂的变量。如果你编写批处理文件的时间足够长,至少其中的几个话题对你来说会变得相关。更重要的是,它们应该展示了一种解决问题的方法,这将激发你在遇到其他未知问题时的想象力。
尊重空值
在第十九章中,我提到过,当使用delims子句将字符串分割为独立标记时,解释器不会尊重空值。考虑一个名为staff.csv的文件,其中包含每个员工记录的五个项目:名字、中间名、姓氏,后跟职位和政府 ID:
Amy,Amanda,Andersen,Architect,111-11-1111
Colin,,Clark,Coder,222-22-2222
Mai,Maria,McManus,Manager,333-33-3333
数据组织得很好,但可读性不强。然而,以下代码会将来自前四个标记的数据以基础报告的形式显示到控制台,同时不显示记录末尾的敏感数据——或者至少是这样的意图:
set cnt=0
for /F "usebackq tokens=1-4 delims=," %%a in ("C:\Batch\staff.csv") do (
set /A cnt += 1
> con echo Employee !cnt!:
> con echo First Name: %%a
> con echo Middle Name: %%b
> con echo Last Name: %%c
> con echo Job Title: %%d
)
前两条记录生成如下内容,但请注意,这里存在一个重大问题:
Employee 1:
First Name: Amy
Middle Name: Amanda
Last Name: Andersen
Job Title: Architect
Employee 2:
First Name: Colin
Middle Name: Clark
Last Name: Coder
Job Title: 222-22-2222
正如你在数据Colin,,Clark中看到的,Colin 没有中间名,但for /F命令会跳过两个逗号之间的空值,导致后续数据出现偏差,并暴露了 Colin 的政府 ID。在许多情况下,比如在按多个空格分隔时,作为程序员,你可能希望这种行为,但在这里就不行。当遇到这样的数据时,你需要一种方法来强制 Batch 尊重空值。
这个解决方案通过使用嵌套的for /F命令,在连续的逗号分隔符之间插入一个空格:
set cnt=0
❶ for /F "usebackq tokens=*" %%r in ("C:\Batch\staff.csv") do (
set inRec=%%r
❷ for /F "tokens=1-4 delims=," %%a in ("!inRec:,,=, ,!") do (
set /A cnt += 1
> con echo Employee !cnt!:
> con echo First Name: %%a
❸ > con echo Middle Name: %%b
> con echo Last Name: %%c
> con echo Job Title: %%d
) )
在外层 for /F 命令❶的开始,我将整个记录存储在 inRec 变量中,然后将它作为内层 for /F 命令❷的文本输入,但请仔细查看这个输入:!inRec:,,=, ,!。首先,我通过感叹号解析 inRec,因为我已经在代码块中为它赋值。更重要的是,这个语法将双逗号替换为由空格分隔的两个逗号,实际上插入了一个空格。内层 for /F 命令❷将四个以逗号分隔的标记传递到它的代码块中,当处理科林的数据时,插入的空格变成了第二个标记%%b,并将其作为中间名❸与其他信息一起写入控制台。
艾米的报告没有变化,科林的条目看起来好多了:
Employee 2:
First Name: Colin
Middle Name:
Last Name: Clark
Job Title: Coder
他的中间名正确地显示为空,而他的政府身份证明被隐藏起来。
尽管这些嵌套命令对于该特定数据有效,但逻辑远非万无一失。当记录中的第一个标记是空值时,它就无法正常工作,因为这样的空值不会表现为双逗号;空值作为第一个标记时,只表现为一个逗号引领记录,后面跟着第二个标记。它也无法捕捉到两个连续的空值,或是连续三个逗号,在第二个逗号前插入一个空格,但在它后面却没有插入空格。最后,这些嵌套的 for 命令将空值更改为空格,因此它被改变了。根据你如何使用数据,这通常不是问题,但有时你会希望保持数据的完整性。
所有这些规定可能看起来会使这种方法不适用,但如果你了解你的数据,它可能是完全可以接受的。例如,在这个示例数据中,如果你确信中间名是唯一可能缺失的标记,并且你可以接受在中间名标签后写入一个额外的空格,那么这个语法就完全没问题。但总是有改进的空间。
对之前解决方案做一些调整,下面的代码修正了刚才讨论的所有局限性:
set cnt=0
❶ for /F "usebackq tokens=*" %%r in ("C:\Batch\staff.csv") do (
set inRec=%%r
❷ for /F "tokens=1-4 delims=," %%a in ("_!inRec:,=,_!") do (
❸ set a=%%a& set b=%%b& set c=%%c& set d=%%d
set /A cnt += 1
> con echo Employee !cnt!:
❹ > con echo First Name: !a:~1!
> con echo Middle Name: !b:~1!
> con echo Last Name: !c:~1!
> con echo Job Title: !d:~1!
) )
外层 for /F 命令❶保持不变。内层 for /F 命令❷仍然传递四个以逗号分隔的标记,但它的输入(!inRec:,=,!)在每个标记前加上了一个下划线。这个前置的下划线实际上是对整个记录进行前置操作,只是将第一个标记前加了一个下划线。然后,替换语法将每个逗号改为一个逗号后跟下划线,这会在每个逗号后插入下划线,从而有效地使每个剩余的标记前加上下划线。空值标记现在是一个下划线(,_),所以解释器会尊重它。
虽然这解决了一个问题,但也引发了另一个问题;我们必须在使用数据项或令牌之前,去除每个数据项或令牌前面新添加的下划线。我不常在单行代码中合并多个命令,但在这里我使用了四个 set 命令,因为它们简短、简单且重复❸。第一个命令,set a=%%a,将令牌 %%a 赋值给变量 a,之后我可以在代码块中通过 !a:~1! ❹ 截取第一个字符,留下只有去除前导下划线后的数据,即原始内容。接着,我对其他三个令牌使用相同的技巧,因为我确信每个令牌前都有下划线。
这种在每个令牌前添加一个字符然后去除它的技巧要更加通用。
强制将字符串转换为大写
如果你还没有意识到,我非常喜欢 Batch 编程,但有时其他编程语言能提供与我在 Batch 中构思的不同,甚至更好的解决方案。例如,强制将文本转换为大写在现代语言中是一个微不足道的操作,这些语言有某种可调用的方法,比如 PowerShell 中的 .toUpper() 或 Python 中的 .upper()。不幸的是,Batch 并没有类似的命令或任何内建机制来将文本转换为大写,但我会展示一些不同的方法来完成这个任务。
到现在为止,你已经看到我们如何从头开始构建一些东西,而在这个解决方案中,我们将利用 Batch 中固有的不区分大小写的特性,创建一个实际改变文本大小写的例程。以下例程接受一个字符串变量名作为输入,并返回该变量,其中内容被强制转换为大写:
:ToUpper
for %%u in (A B C D E F G H I J K L M N O P Q R S T U V W X Y Z) do (
set %~1=!%~1:%%u=%%u!
)
goto :eof
这个不带选项的 for 命令执行 set 命令,以延迟扩展的方式更新变量 26 次,每次处理字母表中的一个字符,每次传递都将 %%u 解析为一个大写字符。(也许我应该打破惯例,在这里使用 %%U。)
第一次传递将 A 改为 A,这看起来有些多余,直到你记得批处理文本替换语法是不区分大小写的,这意味着这个技巧同时将 A 和 a 都改为 A。接下来,我们将 B 改为 B,C 改为 C,依此类推,直到整个字母表都被替换完。
当 for 循环完成时,它将所有小写字母转换为大写字母,而不会影响现有的大写字母和非字母字符。传递给这个逻辑的变量作为唯一参数,现在包含的是转换为大写的文本。
你可以创建一个类似的例程,将字符串强制转换为小写,只需将输入的值列表更改为所有小写字符的集合。唯一可能需要更改的是标签,你可能需要将其更新为类似 :ToLower 的名称。
嵌入 PowerShell 命令
另一种解决方案是将前面提到的 PowerShell 命令作为输入传递给 Batch 的 for /F 命令。没错,你没看错;你可以将其他编程语言的逻辑嵌入到 Batch 代码中。但是在将 PowerShell 命令作为 for /F 命令的一部分之前,我们先来看一下它在命令提示符下的运行情况。
如果你的 Windows 电脑中预装了 PowerShell,你可以在命令提示符下执行 PowerShell 命令,除非你在第一代 iPhone 发布之前购买了电脑,否则自 XP SP2(大约 2006 年)以来,每个 Windows 操作系统都会自带 PowerShell。
在命令提示符下输入此命令:
**powershell 'Set this to Upper-Case'.toUpper^(^)**
解释器将单引号内的文本输出到控制台,所有字母字符都会被转为大写。
一眼看去,这看起来像是一个名为 powershell 的 Batch 命令,但并没有这样的命令。实际上,这是程序 powershell.exe,解释器应该能够在路径层级中找到它(大多数电脑上的路径为 *C:\Windows\System32\WindowsPowerShell\v1.0*)。该程序的参数是 PowerShell 命令,用于将文本转换为大写,这个命令恰好使用了点表示法,正好与传统 Batch 命令相反。被强制转换为大写的文本位于单引号内,后面跟着点和适当的 PowerShell 方法,其括号已转义。
你还可以将此命令放入 bat 文件中,它在你希望看到“SET THIS TO UPPER-CASE”写入标准输出时表现良好,但如果你希望将此文本赋值给一个变量,那么就需要使用 for /F 命令:
:ToUpper
for /F "usebackq tokens=*" %%u in (`powershell '!%~1!'.toUpper^(^)`) do (
set %~1=%%u
)
goto :eof
与第一个解决方案一样,这个例程有一个参数,作为输入和输出使用——也就是包含文本的变量的名称。
包围在反引号中的 for /F 命令的输入告诉解释器这是一个命令输入,在本例中是嵌入的 PowerShell 命令。请注意,这个命令看起来非常像我们之前在命令提示符下输入的命令。唯一的区别是现在 !%~1! 替代了硬编码的文本。%~1 参数解析为输入变量名;然后,感叹号和延迟扩展会将变量解析为其值。PowerShell 命令的输出是该文本的大写版本,解释器将其作为变量 %%u 发送到代码块中,最终将其重新赋值给相同的输入参数:%~1。
嵌入 Python 命令
你不必止步于 PowerShell 命令。任何你可以在命令提示符下输入的内容,都可以作为 for /F 命令的有效输入,包括调用任何程序,而不仅仅是 Batch 命令。在这个示例中,我将调用 Python 运行时,但只要你能够在命令提示符下执行它,你就可以使用任何语言的运行时。你甚至可以执行用其他语言编写并编译的程序,将它的输出作为 for /F 命令的输入。
与 PowerShell 不同,你的 Windows 计算机可能没有预装 Python 运行时,但你可以从 <wbr>www<wbr>.python<wbr>.org<wbr>/downloads<wbr>/ 下载。这里不是一本关于 Python 的书(虽然 No Starch Press 的确有一些很棒的书籍),所以我不会详细探讨语法,但只要你的计算机上安装了 Python,下面的例程在功能上等同于前两个具有相同标签的例程:
:ToUpper
set subopts=usebackq tokens=*
for /F "%subopts%" %%u in (`python -c ^"print^('!%~1!'.upper^(^)^)^"`) do (
set %~1=%%u
)
goto :eof
我答应不深入探讨,但简而言之,python 是没有扩展名的可执行文件或运行时,-c 告诉运行时接下来是一个 Python 命令。print() 函数输出的是括号内内容的结果,该内容是解析后的输入变量,包含单引号、一个点和将字符串转换为大写的 Python 方法。去掉六个转义字符后,Python 命令更容易阅读:print('!%~1!'.upper())。
代码块中的 set 命令再次将嵌入命令的输出赋值给它的唯一参数。但还要注意,我设置了一个包含 usebackq 关键字和 tokens 子句的文本变量,然后将该文本作为 for /F 命令的一部分进行解析。这样做有两个原因。第一个也是最直接的原因是,否则命令就放不下在页面上(尽管它是完全有效的)。第二,这是另一个机会,让我提醒你,我们如何在一个未编译的脚本语言中将命令拼接在一起。
实际的 PowerShell 和 Python 命令本身当然很简单,但使用它们所需的 Batch 机制确实增加了一些复杂性。这通常不是解决问题时首先想到的技巧,但当你能够真正利用其他语言的命令时,for /F 命令为你提供了一种有效的方式来分配其输出。
注意
在第十九章中,为了保持一致性,我建议养成在命令作为 for /F 命令输入时使用 usebackq 的习惯。我推荐的部分原因是,虽然在这样的命令中通常找不到反引号或单引号,但单引号是两者中更可能出现的。对于这最后两个例子,关键字和包含反引号不仅是好的实践;它们是必须的,因为嵌入的 PowerShell 和 Python 命令都包含单引号。
延迟扩展的两个层次
延迟扩展是 Batch 的一大特点,其他大多数语言中都没有。当你积累经验并开始构建一个更有趣且复杂的代码库时,你会遇到一个相当常见的问题,即如何在 for 命令的代码块中处理两个层次的延迟扩展。
在第三章中,我通过定义包含五个城市美食的变量来演示了延迟扩展。这里有两个例子:
set foodNash=Hot Chicken
set foodNYC=Thin Crust Pizza
使用相同的缩写,我还设置了每个城市完整名称的变量:
set NashFull=Nashville
set NYCFull=New York City
最终,以下的 echo 命令包含了两个延迟扩展的示例,这些示例解析了这些变量,前提是 city 变量被分配了有效的城市缩写:
> con echo The best !food%city%! can be found only in !%city%Full!.
现在,让我们基于这个例子,列出美国所有 50 个州,并为每个州指定一个烹饪之都。例如,虽然杰斐逊市是密苏里州的首府,但圣路易斯以其冷冻卡士达闻名,许多人认为它是该州的烹饪之都。在我成长的州——康涅狄格州,纽黑文独特的薄脆比萨和米德尔顿的蒸奶酪汉堡之间长期以来一直存在争论。我并不想卷入其中,但为了本练习的需要,我(某个权威机构)被赋予了定义每个州烹饪之都的权力,并且明确了每个城市,进而确定该州的代表性菜肴。给定一个州的邮政缩写代码列表,我可以这样定义每个州的烹饪之都:
set culCapTN=Nash
set culCapNY=NYC
set culCapIL=Chic
set culCapLA=NO
set culCapMO=STL
例如,TN(田纳西州)的烹饪之都 culCapTN 是 Nash(纳什维尔)。现在,我们可以使用两位数的州代码作为输入,进行 for 命令,其中延迟扩展将烹饪之都变量解析为城市缩写。然后,第二级的延迟扩展将这些缩写解析为食物和完整的城市名称。
这些州的代码可能来自文件中的数据,但为了让这个练习尽可能简单,我使用了硬编码的州代码列表作为没有选项的 for 命令的输入:
for %%s in (TN NY IL LA MO) do (
set city=!culCap%%s!
> con echo The best !food%city%! can be found only in !%city%Full!.
)
%%s 变量会在五次迭代中解决到州的代码。在数据的第一次迭代中,!culCap%%s! 会解决为 !culCapTN!,然后它会解决为 Nash(纳什维尔),并且我们将其分配给 city 变量。然后,echo 命令从第三章中按字面意思提取,解决了两个延迟扩展的示例,就像在上一章中那样,对吧?
错了!如果这种情况有效,我就不会称其为“高级技巧”了。虽然解释器成功地为 city 变量分配了城市缩写,但当代码处于像这样的代码块内时,变量会有两个不同的值:感叹号定界符将其解决为代码块中已取得的值,而百分号定界符则将其解决为代码块执行之前的值。
因此,%city% 解决为无(或它在先前代码中设置的任何值),然后 !food%city%! 解决为 !food!,而 !food! 很可能也解决为无。代码不会挂起或抛出中止错误——更糟糕的是,它只会显示一个不完整的句子。
我们陷入了困境。由于 city 是在代码块内部设置的,解决它当前值的唯一方法涉及感叹号,但延迟展开要求我们用百分号来解析内部变量。(当首次遇到这个困境时,大多数程序员,如果不是所有程序员,都会尝试交换定界符。你可以试试,但它就是不行。)以某种方式,你必须通过百分号让 city 可解析。
一种解决方案涉及一个隐藏的例程。这个技巧并不会将第一次延迟展开的结果赋值给像 city 这样的变量;相反,它将值传递给一个例程:
for %%s in (TN NY IL LA MO) do (
call :WriteFoodText "!culCap%%s!"
)
if 0 equ 1 (
:WriteFoodText
> con echo The best !food%~1! can be found only in !%~1Full!.
goto :eof
)
例程中的代码随后使用 %~1 语法将城市解析为一个简单的参数。由于例程在 for 命令的代码块外部执行解析,变量 !food%~1! 会很好地解析为 !foodTN!,再次解析为热鸡肉。
然而,这种技巧也不是没有问题的。它不是那种通常适合放在 bat 文件末尾的复杂例程,而且绝对不会单独出现在一个 bat 文件中。它其实只是一个单一的命令(加上标签和终止的 goto :eof 命令),所以最好将其靠近 call 命令。但如果我将标签紧接在这个 for 命令后面,解释器会对每个州执行一次例程,然后在 for 命令完成并控制流转到下一个命令时,再执行一次。为了解决这个问题,我使用了一个永远不会为真的笨拙条件语句 0 equ 1,它作为例程的门卫。
这种技巧很容易让任何阅读代码的人感到困惑,但它成功地隐藏了一个放在 call 命令附近的隐藏例程,而这是访问它的唯一方法。它有效,我经常使用它,但这不是你会想挂在冰箱上和孩子的艺术作品一起展示的那种逻辑。
更为优雅的解决方案是将第二个 for 命令嵌套在第一个命令中:
for %%s in (TN NY IL LA MO) do (
for /F "tokens=*" %%c in ("!culCap%%s!") do (
> con echo The best !food%%c! can be found only in !%%cFull!.
) )
首先,%%s 解析为州代码;然后 !culCap%%s!,解析为相应的城市缩写,是传入内部 for 命令的字符串,我们将其赋值给 %%c。因此,内部的 for 命令只不过是将 city 赋值给一个变量,我们可以通过百分号来解析它。注意,我已经将问题陈述中解析为 null 的 %city% 替换为 %%c 变量。同样,以田纳西州为例,%%c 解析为 Nash,然后 !food%%c! 变为 !foodNash!,进一步解析为美味的热鸡肉。类似地,!%%cFull! 解析为 !NashFull!,并解析为 Nashville。现在,这可是值得贴在冰箱上的代码!
我使用/F选项与内部for命令配合,并用双引号将文本输入括起来。将 tokens 关键字设置为星号确保%%c解析为整个文本字符串,即使它包含空格,这意味着代码块只会执行一次。(一个更简单的不带选项的for命令对于这个特定数据也能作为内部for命令使用,但for /F命令是一个更通用的解决方案,因为它可以处理包含空格和不包含空格的输入。)
本节中详细介绍的两种解决方案会将以下输出写入控制台:
The best Hot Chicken can be found only in Nashville.
The best Thin Crust Pizza can be found only in New York City.
The best Deep Dish Pizza can be found only in Chicago.
The best Muffuletta Sandwich can be found only in New Orleans.
The best Frozen Custard can be found only in St Louis.
我提供的这个问题陈述看起来可能有些牵强,但这种技术的需求常常出现。更现实的例子通常涉及更复杂的情况,涉及文件、数组、哈希表等更多我尚未深入探讨的主题。例如,若要为robocopy命令获取目标路径,你可能需要从文件中读取目标城市,然后在哈希表中查找该城市以获得路径。但这些情况归根结底都是同一个问题:你需要在代码块中使用延迟扩展来处理之前设置的变量。要时刻留意类似的情况。
使用for命令进行转义
在for命令中转义特殊字符是一个挑战,可能以多种不同方式表现出来,其中一个例子就是在子选项子句中。输入数据通常完全不在我们程序员的掌控之中。如果所有数据文件都是以逗号或管道符分隔的那该多好,但很多时候你不得不使用更复杂的分隔符。最糟糕的情况是文本以双引号分隔。如果你试图提取六个标记,你可能会考虑这个子选项子句,但它是无效的:
"tokens=1-6 delims=""
虽然解释器正确地将第一个双引号识别为子句的开始,但第二个双引号终止了子句,第三个双引号则处于上下文之外。但这个看起来晦涩的子句完成了任务:
tokens^=1-6^ delims^=^"
没有双引号开启子句,且结尾的双引号并没有关闭子句——它是分隔符集中的唯一项。包围子句的双引号不再存在。这允许你在delims子句中包含双引号,只要你用前导插入符号进行转义。但是,缺少包围选项子句的双引号会引发其他问题。等号甚至空格不再受到包围双引号的保护,现在也需要转义。
我在两个等号前面都加了插入符号(^)。这两个关键字子句之间的空格会终止较大的子选项子句,因此这个空格也需要转义。这个插入符号看起来似乎跟在 6 后面,但实际上它是引导空格的。在批处理的一个不太文档化的特性中,只要你转义了特殊字符和空格,你就可以省略包围的双引号。
现在让我们将这一切结合起来并进行测试。考虑这个非常小的文件,UglyData.txt,其中仅有一行数据,用五个双引号分隔六个单词:
This"is"some"messed"up"data
这个 for /F 命令包含了前面讨论过的选项子句:
for /F tokens^=1-6^ delims^=^" %%a in (C:\Batch\UglyData.txt) do (
> con echo %%a %%f %%b no longer %%d %%e.
)
这是输出到控制台的内容:
This data is no longer messed up.
在 第十九章 中,我提到过我通常在读取文件时使用 usebackq。这里没有使用,因为子选项已经变得很混乱,但如果你把双引号放在输入文件周围,你需要在 tokens 子句前添加 usebackq^ 和一个空格。(你稍后会看到类似的内容。)
当存在多个分隔符时,相同的原理也适用。让我们通过添加插入符号、管道符、百分号和与符号分隔符,使UglyData.txt变得更加混乱:
This^is|some%messed"up&data
现在,以下的子选项子句使得这个操作得以实现:
tokens^=1-6^ delims^=^"^|%%^^^&
一个插入符号转义所有这些特殊字符(甚至是插入符号分隔符),除了一个;我们只能通过另一个百分号来转义百分号。
在分隔符集里可能很少会出现双引号的需求,但一旦需要,这项技术就显得不可或缺。
我们还没有完全完成转义。在接下来的示例中,我将使用相同的数据并将相同的输出写入控制台,但数据的来源会不同。我将从一个文件中提取混乱数据,并将其设置为一个变量,这将要求对数据和 for /F 命令进行更改。
我将从数据开始。在 第十四章 中,你学到了设置变量时必须转义所有特殊字符,但即便如此,你得到的变量仍然包含无法稍后解析的特殊字符,除非将它们暴露出来。解决方法是进行两层转义:
set uglyData=This^^^^is^^^|some%%%%messed^^^"up^^^&data
使用这个 set 命令,所有的双插入符号都会被解析为单插入符号。解释器会丢弃所有其他插入符号,因为它们是在转义其他字符;双百分号会解析为一个,从而使得四个变成两个。最后,解释器将 uglyData 设置为 This^is|some%%messed"up&data 文本。下一次我们解析这个变量时,现有的转义字符将被丢弃。
对 for /F 命令也做了更改:
for /F usebackq^ tokens^=1-6^ delims^=^"^|%%^^^& %%a in ('%uglyData%') do (
> con echo %%a %%f %%b no longer %%d %%e.
)
该命令包含了更复杂的 delims 子句,包含五个已转义的分隔符。输入到 for 命令中的 %uglyData% 文本会解析为 This^is|some%messed"up&data,且被单引号包围,并且 usebackq 关键字也已就位,并带有其后缀转义字符。
由于双引号是输入的一部分,因此此 for /F 命令必须在输入字符串周围使用单引号,这就需要使用 usebackq 关键字。(这正是 第十九章 中提到的那种罕见情况。)注意,当双引号位于正在读取的文件中时,Batch 更容易处理它;通常,解析字符串比解析文件数据要复杂。最后,我们看到 This data is no longer messed up. 被写入控制台。
从这次讨论中,最大的收获是你应该始终弄清楚需要预期多少级别的转义,并确保代码能够支持输入中所有预期的特殊字符。
总结
在本章中,我介绍了一些我在作为批处理编码员的职业生涯中遇到的高级技巧。这并不是关于for命令的所有有趣难题的详尽列表,但它代表了许多典型情况。要充分发挥批处理的能力,你必须理解for命令,而这需要实验和探索。不要害怕。如果你在批处理中编程足够长时间,你会遇到一些这里没有涉及的内容,但如果你遵循这些示例中的思考过程,你应该能够通过实验找到解决方案。
这部分内容结束了第二部分,但这绝不是你在接下来的章节中最后一次见到for命令。这个命令的更多应用还在后面,但在下一章,我会稍微退一步,探索一些重要的伪环境变量。
第三部分 高级主题
第一部分介绍了批处理文件的基础知识,第二部分探讨了至关重要的 for 命令。第三部分将涵盖需要使用这一关键命令的技巧,以及一些实用的高级主题。
在本书的这一部分,你将学习伪环境变量、编写报告、递归、文本搜索、自动重启,以及构建其他批处理文件的批处理文件。其他主题将深入探讨条件执行(再一次)、数组、哈希表以及测试与故障排除。我将最后讲解面向对象设计,并通过数据结构——栈和队列的应用来进行说明。这些主题中的许多并不是批处理的创始者所设想的。但这从未阻止过我们,也不会在接下来的章节中阻止我们。
第二十一章:21 伪环境变量

我在第二章中介绍了环境变量及其 set 命令,它们从那时起出现在了几乎每一页中。你可以定义和解析一个简单的变量,然后可能会重置并以不同方式再次使用它。伪环境变量与其类似,但也有显著不同。你可以像常规环境变量一样解析它们,但它们的来源或设置方式有本质的不同。
我们已经讨论了一些伪环境变量,包括 path、pathext、cd 和 errorlevel。所有伪环境变量都有一些共同特点,但许多都有独特的属性。许多伪环境变量有不同的设置方式,有些你根本不应设置。某些变量在运行 bat 文件之前就已激活,而解释器会在执行过程中反复更新其他变量。
每个伪环境变量在批处理世界中都有一个固有的特性,在正确使用之前你必须理解这一点。在本章中,我将解释一些已经提到的伪环境变量的复杂性,并探索一些我们将在未来章节中使用的有用的伪环境变量。我还将提供一直承诺的关于从编码者角度看,bat 文件和 cmd 文件的主要区别的解释。
日期和时间
你可以轻松地通过分别使用恰如其分命名的日期和时间伪环境变量来获取当前的日期和时间:
> con echo Date = "%date%"; Time = "%time%"
如果在波士顿红袜队结束了 86 年的冠军荒之后不久执行该命令,它将产生如下输出:
Date = "Wed 10/27/2004"; Time = "23:39:12.34"
日期变量包含当前日期,格式为星期几、月份、日期和年份。星期几总是以三个字符的大写和小写混合形式呈现,后跟一个空格、一个两字节的月份、一个斜杠、一个两字节的日期、另一个斜杠和一个四字节的年份。例如,Sun 06/08/1986 是批处理时代早期的一个例子。
时间变量包含当前时间,格式为小时、分钟、秒和百分之一秒。它使用 24 小时制,因此晚上 11:39 转为 23:39:00.00。如果小时数是单个数字,批处理会在其前面加上一个空格,而不是前导零;例如,在上午 10:00 临近时,时间将解析为一个空格,后跟 9:59:59.99,而午夜则显示为空格加上 0:00:00.00。
这确实有些奇怪,但请记住,时间在前面有一个空格代替零,而日期对于所有单数字的月份和日期则有一个前导零。
由于日期和时间格式良好,你可以轻松地将它们用来增强报告和日志文件,而且因为日期格式一致,所以可以轻松地提取一个日期戳,格式为 CCYYMMDD,如 Listing 21-1 所示。
set #=%date%
set datestamp=%#:~10,4%%#:~4,2%%#:~7,2%
Listing 21-1: 在 Datestamp.bat 文件中使用的两个命令来构建日期戳
(为了节省按键次数,我喜欢使用井号作为一个非常简短的变量名,但仅限于非常简洁和有限的使用。在 bat 文件中稍后使用这个变量会造成困扰,但在接下来的一两行中立即使用它则是一个很好的方式来简化代码。虽然我必须承认,%#:~7,2%是深奥的,并非每个人都喜欢。)
使用不同的技术,以下是构建按 HHMMSSss 格式的时间戳的代码:
set timestamp=%time: =0%
set timestamp=%timestamp::=%
set timestamp=%timestamp:.=%
第一个命令将替换前导空格(如果存在的话),用 0 代替。接下来的两个命令将移除两个冒号和小数点。
除非在上午 10 点之前进行编码和测试,否则很容易忘记前导空格,但它是至关重要的。如果在下午测试时忘记了它,变量将会在午夜时刻突然包含一个空格,而根据你计划如何使用它,可能会在非常不合时宜的时刻发生失败。这可以看作是另一个警告。
日期戳和时间戳有许多用途。你可以在文件名中使用它们来表示创建日期和时间。在 if 命令中,你可以将它们与其他日期戳或时间戳进行比较,在特定日期和时间启用逻辑。你甚至可以在某些过程之前和之后捕获它们来衡量经过的时间。
Prompt
提示符伪环境变量同时也作为命令使用。就像 path 是用来更改路径变量的命令一样,prompt 是用来更改提示符变量的命令。
在第十二章中,当讨论解释器生成的输出(stdout)时,我提到了解释器会在每个命令的输出前加上提示符,无论输出最终是显示在控制台上,还是重定向到跟踪文件中。默认情况下,提示符是当前目录后跟一个大于符号。例如,如果从* C:\Batch* 目录运行清单 21-1 中生成日期戳的两行代码,它可能会生成以下输出到跟踪文件:
C:\Batch>set #=Wed 10/27/2004
C:\Batch>set datestamp=20041027
但默认设置是可以更改的。提示符变量包含硬编码文本和/或特殊代码,定义了在 stdout 中看到的提示符内容,而提示符命令是更新提示符变量的工具。该命令的唯一参数是新的提示符变量,它将改变我们在 stdout 中看到的内容。
为了演示,如果我特别自恋并希望每行执行的代码上都带有我的签名,这个简单的命令可以满足我的虚荣心:
prompt Jack's$SCode$G
参数中有两段硬编码文本,分别是 Jack's 和 Code。此外,还有两个特殊代码,在单词之间插入一个空格(\(S),在末尾插入一个大于符号(\)G)。运行相同的两行代码,在同一天现在会生成如下输出:
Jack's Code>set #=Wed 10/27/2004
Jack's Code>set datestamp=20041027
但是这个命令和变量并不是为了虚荣而创建的;它们是为了在提示符中填充自定义信息而创建的。考虑这个更为复杂和深奥的示例:
prompt %~NX0$A$N:$$$C$D$B$T$F$G
这个提示符命令并不容易阅读,但这里我将其分解如下:
%~NX0 Bat 文件名和扩展名;可能解析为 Datestamp.bat
$A 和符号;解析为 &
$N 驱动器字母;可能解析为 C
: 硬编码文本;显示为 :


浙公网安备 33010602011771号