03 Windows批处理的作用域和延迟扩展

在前文中,我们学习了变量、如何设置它们以及如何解析它们的值。在本文中,我将重点介绍setlocal命令,它是批处理中非常重要的、具有不同特性的核心内容,它可以何时、何地以及如何处理变量。首先,它定义了作用域:在何处以及何时可以访问和操作这些变量。其次,它启用了一个称为延迟扩展的特性,该特性改变了变量解析的方式,其结果之一是允许将一个变量存储在另一个变量中。

所有语言都以某种方式处理作用域,但延迟扩展或类似的东西远不常见,您将看到它的一些令人惊讶的用途。最后,setlocal命令启用命令扩展,用于描述为许多其他批处理命令打开的大量附加功能。

作用域

作用域定义变量的生命周期。全局变量可以在任何地方设置、解析、删除和修改,这对于大多数简单的bat文件都很有效。局部变量具有有限的保存期限,意味着可以在其作用域内的单个代码段中访问。如果这些修改不能被识别,那么变量就超出了作用域。

在批处理中,setlocal命令之后开始一段代码,其中变量在作用域中,endlocal命令结束该部分,使这些变量不在作用域中。这两个命令之间定义或操作的所有内容在该空间中都是活动的,但是在执行endlocal命令之后,这些变量将恢复到先前的状态。

为了演示,下面的代码将三个变量的状态分别写入setlocal命令作用域内和作用域外的控制台。一个仅在setlocal作用域内定义,一个仅在setlocal作用域外定义,还有一个既在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

这里有很多东西要拆解。让我们看看定义的第一个变量:inAndOut在setlocal命令执行之前被设置为OUT,这意味着它被设置在命令的作用域之外。setlocal执行后,该变量会被设置为IN。当inAndOut第一次被询问时,它会被解析为IN,因为它在作用域中。但是在endlocal程序执行之后,它就不在作用域范围内,并恢复到之前的状态,也就是OUT。

现在考虑内部变量,它在作用域中只定义一次。也就是说,执行完setlocal命令的时候,它被设置为IN。在endlocal语句执行之前,这个变量会被解析成IN,但有趣的是;在endlocal语句之后,它恢复到之前没有定义的状态,即null或empty。

最后一个变量是外部的,它也只定义一次,但是当它超出作用域时。在setlocal执行之前,它被设置为OUT。正如你所料,在endlocal程序执行后,当它超出作用域时,这个变量仍然是OUT。但你可能没有预料到,它的值在setlocal 的范围内也是可用的,因为它的值在endlocal执行之前也是OUT !

这个例子表明setlocal命令并没有阻止我们使用已经在作用域中的变量。在此之前存在的所有东西仍然可用。它的作用是这样的:在setlocal执行的那一刻对环境进行快照,并在endlocal执行时返回该快照。

使用setlocal和endlocal命令定义作用域只有一个用途,但它很重要:在代码的一部分中隐藏或分割变量以防止冲突。默认情况下,批处理变量是全局的;在一个bat文件中设置的变量可以在bat的文件中解析或重置。默认情况下,许多其他语言使用相反的方法,限制被调用程序和例程内部使用的变量的范围。有时全局变量非常好,但在其他情况下,限制作用域是更好的选择。定义范围的能力使我们能够使用最适合我们的应用程序的功能。

如果您正在编写一个将被许多其他进程调用的实用程序bat文件,那么您可能不知道调用进程正在使用哪些变量。在bat文件的顶部放置一个setlocal,在末尾或接近末尾放置一个endlocal,可以定义和限制作用域。这样做的结果是,如果您碰巧使用了与调用bat文件相同的变量名,那么您就不会踩到它的变量,这就允许调用者调用您的bat文件,并保证不会产生不良副作用。对于所谓的内部例程也是如此。

定义作用域提出了一个有趣的问题。如果调用实用程序bat文件来执行特定任务,那么该任务的至少一部分很可能是设置和返回某个变量。有一种方法允许一个或多个变量在endlocal命令中存活,我将在后面进行介绍。

延迟扩展

setlocal命令是一个多管齐下的工具。除了定义作用域外,当与描述性参数一起使用时,它还支持延迟扩展:

setlocal EnableDelayedExpansion

延迟扩展实现了两轮变量解析:初始解析和延迟解析或扩展。当解释器执行一个bat文件时,它逐个处理每一行代码,首先读入或解析一行,然后执行该行。初始解析发生在解释器解析该行时,延迟扩展发生在它执行该行时。

这个特性允许一些在大多数语言中没有的有趣行为。例如,可以将变量的值视为变量本身,也可以将其值视为另一个变量名的一部分。在下面的代码中,Toyota既是一个变量名又是一个值;当然这不仅仅是巧合。

setlocal EnableDelayedExpansion
set Car=Toyota
set Toyota=Prius

设置启用延迟扩展的Car和Toyota

首先,我们需要使用带有参数的setlocal命令来启用延迟扩展。接下来,我们将Car设置为汽车的品牌,在本例中是Toyota。但是丰田生产几个车型,如果我们想捕获一个特定的车型,我们可以将定义为Toyota的变量设置为值Prius。

变量及值

正如前面提到的,丰田既是一个值也是一个变量。它是Car变量的值,也是一个包含Prius值的变量。现在我们可以执行三条语句,将三个变量写入控制台,如下所示。

...
> con echo               Car = %Car%
> con echo         Car Again = !Car!
> con echo Delayed Expansion = !%Car%!

通过三种不同的方式解析Car;输出结果如下:

              Car = Toyota
        Car Again = Toyota
Delayed Expansion = Prius

到目前为止,Car的第一种方式是常规解析。在变量周围加上百分号(%)将其解析为其值Toyota。第二个命令引入了一些新东西:感叹号(!)用作分隔符来解析变量!Car!,而不是百分号。用感叹号包围的变量也解析为Toyota,但是为什么要用两个不同的字符来执行相同的功能呢?在我们检查最后的命令之后,答案就会出现。

第三个命令真正显示了延迟扩展的功能。变量被百分号包围,百分号被感叹号包围。解释器首先将%Car%解析为Toyota,该值现在被感叹号包围,这将导致它再次被解析,因此!Toyota!变为Prius。把所有这些放在一起,变量解析如下:

!%Car%! → !Toyota! → Prius

为了回答关于两个不同字符执行相同功能的问题,解释器需要同时执行此解析,因为我们现在有两轮解析:百分号用于内部解析,感叹号用于外部解析。(那么这里我们可以使用两个百分号或者两个感叹号或者%!Car!%来解析吗?)

同理,我们也可以关闭延迟扩展(删除setlocal EnableDelayedExpansion)来观察控制台输出:

              Car = Toyota
        Car Again = !Car!
Delayed Expansion = !Toyota!

如果没有延迟扩展,感叹号将被解析为普通文本,对批处理没有意义。!Car!变量根本没有被解析;解释器甚至不认为这三个字母是一个变量。!%Car%!变量经历了一轮百分号转换,但是再次转换,感叹号只是直接输出。

在前一篇文章关于变量和值中,我们巧妙地回避了一个问题,即变量名不应该以数字开头。从技术上讲,你可以设置这样一个变量,但你不能用百分号来解析它;您只能使用感叹号和启用延迟扩展来执行此操作。处理这个小问题的最好方法是永远不要以数字作为变量名的开头。

现在我们有了一个变量,它可以被解析为一个值,而这个值在第二次被解析为另一个值。在那些花哨的现代编译语言中,这通常不容易做到,甚至根本做不到。说实话,尽管整个词既是变量又是值可能很酷,但它在现实世界中并不常用,但部分变量名有很多应用。

部分变量名

当解析的值仅用作变量名的一部分时,这种技术变得更加有趣和有用。为了证明这一点,请考虑以下五个城市的标志性烹饪杰作:

set foodNash=Hot Chicken
set foodNYC=Thin Crust Pizza
set foodChic=Deep Dash 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! → Muffuletta Sandwich

同样,城市的全名也分两步解析。这里唯一的区别是变量名的硬编码部分位于要解析的部分后面:

!%city%Full! → !NOFull! → New Orleans

对于不同的city值,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命令将在很大程度上依赖于延迟扩展,一个变量将能够同时保存两个值也是值得我们讨论的。

命令扩展

setlocal命令还接受一个参数,用于打开命令扩展。与延迟扩展不同,命令扩展默认情况下应该是开启的,但您也可以使用以下命令显式地打开它们:

setlocal EnableExtensions

开启命令扩展可以为几个批处理命令解锁大量附加功能和可用选项。例如,for命令对于任何批处理程序员都是必不可少的。我们还没有讨论它,但是当命令扩展被禁用时,批处理有一个for命令的变体。甚至在前文中讨论的set命令也有附加的功能和可用的选项。具体的特性因命令而异,您可以通过help命令在命令提示符处检索它们的详细信息。

为了演示通过启用命令扩展为一个命令解锁的额外功能,返回到命令提示符并从前面输入相同的命令来检索set命令的文档:

help set

在简短的几行文本之后,详细说明命令扩展未启用时命令的作用,解释器显示以下行:

如果命令扩展被启用,SET 会如下改变:

下面是所有已解锁的扩展功能。有太多的信息要展示,但在这个小的案例中,两个以前不可用的选项是共享的:

在 SET 命令中添加了两个新命令行开关:  

SET /A expression
SET /P variable=[promptString]

我在前面的文章中提到了这些选项,但是没有提到命令扩展可以打开它们。在启用命令扩展时,help命令提供的关于set命令功能的信息是禁用命令扩展时的,对于许多其他命令也是如此。随着我们介绍更多命令,我鼓励您使用help命令进一步研究它们,以查看更多的用法和选项列表,并查看命令扩展名启用了哪些功能。

setlocal和endlocal的结论

在编写了几年的bat文件之后,我对setlocal和endlocal命令的使用有一些强烈的看法,并且我并不羞于分享它们。我编写的每个高级bat文件在代码的第一行或第一行附近都有这个命令:

setlocal EnableExtensions EnableDelayedExpansion

我将高级bat文件定义为不能从另一个bat文件调用的bat文件。我很少遇到不希望启用命令扩展和延迟扩展的情况。这些额外的功能几乎不需要任何成本。就好像你可以把你的丰田变成兰博基尼,没有任何缺点,比如成本和汽油里程。但在这种罕见的情况下,您可以使用DisableExtensions和DisableDelayedExpansion参数禁用这些特性。

此外,每当编写一些可能对其他代码产生不利影响的逻辑时,我都会在该逻辑之前使用不带参数的简单setlocal命令,并用相应的endlocal命令结束它。别担心;延迟扩展仍然从原来的setlocal命令启用。您甚至可以嵌套多个setlocal和endlocal命令,在子节中创建具有定义范围的代码子节,但深度不超过32层。我从来没有接近过这个限制,但是如果您这样做,您可以在调用的例程或另一个bat文件中进一步嵌套。

为了完整起见,最好在bat文件的末尾设置一个相应的endlocal,但如果省略,解释器将在退出高级bat文件之前执行一个隐含的endlocal。

至关重要的是,本系列文章是在假设命令扩展和延迟扩展都启用的情况下书写的。如果文章中的示例在您的测试中不起作用,请确保您已经运行了带有两个启用参数的命令。

重要:

对于前面提到的使用特定的setlocal命令启动所有高级bat文件的规则,只有一个例外。在后面的文章中,我将提供一些非常短的bat文件的例子,可能只有两到三行。这些简单的示例可能不需要这个命令,并且它的使用可能会将重点从手头的主题转移开。在这些情况下,我将不包括该命令,但要理解它一直都在那里。

总结

本文的主要内容是setlocal命令,它定义了作用域并启用命令扩展。最重要的是,它支持延迟扩展,为定义和使用变量提供了巨大的可能性。

启用延迟扩展后,您看到了如何根据定义城市的变量的值,仅用一个命令就可以写出五个句子中的一个。但是,如果延迟展开被禁用,您可能需要使用5个if命令来查询该变量。在本文给出的示例中,这可能是一个不优雅的解决方案,但通常情况下,if命令在任何语言中都是一个重要的工具,批处理也不例外。在下一篇文章中,我们将详细讨论if——因为这是批处理的一种特性。

本文由博客一文多发平台 OpenWrite 发布!