03 Windows批處理的作用域和延遲擴充套件

公子奇的博客發表於2024-09-03

在前文中,我們學習了變數、如何設定它們以及如何解析它們的值。在本文中,我將重點介紹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 釋出!

相關文章