共用方式為


Windows PowerShell一次編寫一行指令碼

Don Jones

我在前面幾個專欄中一直強調 Windows PowerShell 是個殼層。它的使用是互動式的 – 這和您可能早已熟悉的 cmd.exe (或命令提示字元) 殼層差不多,但是 Windows PowerShell 還可以支援指令碼語言,這種語言比 cmd.exe 的批次語言還要強,而且

與 VBScript 等語言一樣強,或甚至更強。不過,Windows PowerShell™ 屬於互動式殼層的事實,使得指令碼非常容易學習。事實上,您可以在殼層中互動式地開發指令碼,因此能夠一次編寫一行指令碼,並立即看到創作的成果。

這種反覆式的指令碼編寫技巧也更容易偵錯。由於您馬上能看到指令碼的結果,因此如果出現非預期的結果,可以很快地修正。

我將在本專欄中逐步解說 Windows PowerShell 中之互動式指令碼的範例。我會建立一個從文字檔讀取服務名稱,並將每個服務的啟動模式設定為停用 (Disabled) 的指令碼。

我希望您能從逐步解說中得到的概念,就是一次只要建置一小段 Windows PowerShell 指令碼,不要想一次完成一大堆指令碼。您可以將任何需要完成的管理工作分成多個片段,然後思考如何讓每個片段能夠獨立完成作業即可。因為 Windows PowerShell 會提供將這些片段繫結在一起的極佳方式,您很快就會看到。而且您也會發現,一次處理一個小片段,會使整個指令碼的開發更容易。

從檔案讀取名稱

在 Windows PowerShell 中,要想出如何讀取文字檔會很令人沮喪。如果我執行 Help *file*,得到的只是 Out-File cmdlet 的說明,這會傳送文字至檔案,而非從檔案讀取文字。一點用處都沒有!不過,Windows PowerShell cmdlet 名稱確實遵循一定的邏輯,讓我可以有發揮的空間。Windows PowerShell 擷取資訊時,cmdlet 名稱通常會以 Get 開頭。因此我執行 Help Get* 以顯示這些 cmdlet,再捲動清單以找到 Get-Content。似乎有點希望了!於是我執行 Help Get-Content,以查看此功能的詳細資訊 (請參閱 [圖 1]),看起來似乎能夠滿足我的需求。

圖 1 執行 Help Get-Content 以取得詳細資訊

圖 1** 執行 Help Get-Content 以取得詳細資訊 **(按影像可放大)

Windows PowerShell 幾乎將一切都當成物件處理,就連文字檔也不例外。文字檔在技術上是許多行的集合,檔案中的每一行都可算是獨立的物件。因此,如果我建立一個名為 C:\services.txt 的文字檔,並在檔案中填入服務名稱 (每個名稱都放在檔案中單獨的一行),Windows PowerShell 就可以使用 Get-Content cmdlet 個別讀取這些名稱。由於此逐步解說的構想是要示範如何互動式地開發指令碼,因此讓我只先執行 Get-Content,然後提供我的文字檔名稱,看看會有什麼結果:

PS C:\> get-content c:\services.txt
messenger
alerter
PS C:\>

和預期的一樣,Windows PowerShell 會讀取檔案,並顯示名稱。當然,我要的並非只顯示名稱,不過我現在知道 Get-Content 的運作方式正是我要的。

變更服務

下一步就是要變更服務的啟動模式。我同樣先試著找出正確的 cmdlet。所以我執行 Help *Service*。結果傳回一份簡短的清單,而唯一能符合我需求的,似乎只有 Set-Service cmdlet。我想要測試這個 cmdlet,以確定將其加入到指令碼之前,我已了解其運作方式。執行 Help Set-Service 顯示 cmdlet 應有的運作方式,而我所執行的小測試隨之確認:

PS C:\> set-service messenger -startuptype
    disabled
PS C:\>

將各部分結合

現在我必須將可從檔案讀取服務名稱的能力和 Set-Service cmdlet 結合,這時就是 Windows PowerShell 強大的管線功能發揮作用的時候了。透過管線即可傳遞第一個 cmdlet 的輸出做為第二個 cmdlet 的輸入。管線會傳遞完整的物件。如果將多個物件的集合放入管線中,每個物件會透過管線個別傳遞。這表示 Get-Content 的輸出 (記住,這是多個物件的集合) 可以透過管線傳遞至 Set-Service。因為 Get-Content 傳遞的是集合,所以集合中每個物件 (或文字行) 會個別透過管線傳遞至 Set-Service。結果就是系統會針對我文字檔案中的每一行,執行一次 Set-Service。命令的內容如下:

PS C:\> get-content c:\services.txt | 
 set-service -startuptype disabled
PS C:\>

以下是執行過程:

  1. Get-Content cmdlet 執行,讀取整個檔案。檔案中每一行都當成唯一的物件處理,全部一起構成物件集合。
  2. 物件集合透過管線傳遞至 Set-Service。
  3. 管線針對每個輸入物件,執行一次 Set-Service cmdlet。在每次執行時,輸入物件 (亦即服務名稱) 都會當成 cmdlet 的第一個參數傳遞至 Set-Service。
  4. Set-Service 會執行,使用輸入物件做為其第一個參數,以及其他任何指定的參數,在此例中是 -startuptype 參數。

有趣的是,我的工作至此實際上已經完成,但是我連指令碼都還沒編寫。要在 Cmd.exe 殼層中完成同樣的動作會很困難,而若是使用 VBScript,則要編寫幾十行的程式碼。但是 Windows PowerShell 在一行中就處理了一切動作。不過我還沒全部完成。您可以看到,我的命令並未提供許多狀態輸出或回應。雖然看得出來沒有發生什麼錯誤,卻也很難看到是否真的有執行任何作業。即然我已經掌握完成工作所需的功能,即可開始編寫指令碼,讓它好好發揮一下。

Windows PowerShell Prompt Here,作者 Michael Murgolo

最普及 (也是我最愛之一) 的 Microsoft® PowerToys for Windows® 之一,就是 [在此處開啟命令視窗 (Open Command Window Here)] 工具。[在此處開啟命令視窗] 包含在 Microsoft PowerToys for Windows XP 或 Windows Server® 2003 Resource Kit 工具中,可讓您在 Windows 檔案總管中的資料夾或磁碟機上按一下滑鼠右鍵,即開啟指向該資料夾的命令視窗。

我在學習 Windows PowerShell 時,發現自己希望它也有相同的功能。因此我從 Windows Server 2003 Resource Kit 工具中擷取了 [在此處開啟命令視窗] 的安裝 .inf 檔案 cmdhere.inf,並進行修改以建立 [在此處開啟 Windows PowerShell 提示 (Windows PowerShell Prompt Here)] 的內容功能表。這個 .inf 檔案包含在此資訊看板所在之原始部落格貼文中 (可從 leeholmes.com/blog/PowerShellPromptHerePowerToy.aspx 存取)。若要安裝工具,只要在 .inf 檔案上按滑鼠右鍵,再選取 [安裝] 即可。

我在建立此工具的 Windows PowerShell 版本時,發現原始版有錯誤 -- 如果解除安裝 [在此處開啟命令視窗] 工具,會留下一個無作用的內容功能表項目。因此,我提供了更新版的 cmdhere.inf,也可從原始部落格貼文下載。

這兩種 PowerToy 都利用一個現象,那就是這些內容功能表項目都是在與 Directory 和 Drive 物件型別關聯的登錄機碼下設定。其方式和內容功能表與檔案類型關聯的方式一樣。例如,您若在 Windows 檔案總管中的 .txt 檔案上按滑鼠右鍵,清單上方提供您許多動作 (例如開啟、列印及編輯)。為了要了解如何設定這些項目,讓我們來看看 HKEY_CLASSES_ROOT 登錄區。

如果您開啟登錄編輯程式並展開 HKEY_CLASSES_ROOT 分支,會看到針對檔案類型命名的機碼,例如 .doc、.txt 等等。如果您按一下 .txt 機碼,會看到 (預設值) 是 txtfile (請參閱 [圖 A])。這是與 .txt 檔案關聯的物件型別。如果您捲動並展開 txtfile 機碼,再展開其下的 shell 機碼,會看到針對 .txt 檔案之內容功能表項目命名的機碼 (您不會看到全部的項目,因為還有其他方法可以建立內容功能表)。每個項目底下會有一個 command 機碼。command 機碼下的 (預設值) 就是您選取該內容功能表項目時,Windows 會執行的命令列。

cmd 提示和 Windows PowerShell 提示都使用此技巧來設定 [在此處開啟命令視窗] 和 [在此處開啟 Windows PowerShell 提示 (Windows PowerShell Prompt Here] 內容功能表項目。磁碟機和目錄沒有關聯的檔案類型,但是 HKEY_CLASSES_ROOT 底下有與這些物件關聯的 Drive 和 Directory 機碼。

圖 A 內容功能表項目是在登錄中設定

圖 A** 內容功能表項目是在登錄中設定 **(按影像可放大)

Michael Murgolo 是 Microsoft Consulting Services 的資深基礎結構顧問。他專精的領域包括作業系統、部署、網路服務、Active Directory、系統管理、自動化及修補檔案管理。

互動式地編寫指令碼

我要做的一件事,是針對 C:\services.txt 中列出的每個服務,執行多次 cmdlet。這樣我就可以輸出服務名稱和我要的其他任何資訊,以便追蹤指令碼的進度。

我在前面使用管線從某個 cmdlet 傳遞物件到另一個 cmdlet。不過,這次我要使用更像指令碼的技巧,稱為 Foreach 建構 (我在上個月的專欄介紹過)。Foreach 建構可以接受物件集合,並且會針對集合中的每個物件執行多重 cmdlet。我指定一個變數,此變數在每次迴圈時代表目前的物件。例如,建構的開頭可能是這樣的:

foreach ($service in get-content c:\services.txt)

我還是執行相同的 Get-Content cmdlet 以擷取文字檔案的內容。但這次我要求 Foreach 建構針對 Get-Content 傳回的物件集合執行迴圈。於是建構針對每個物件執行一次迴圈,並將目前的物件放在變數 $service 中。現在我只需指定要在迴圈中執行的程式碼。我會先嘗試複製原先的單行命令 (這樣可減少複雜性,並保證不會漏掉任何功能):

PS C:\> foreach ($service in get-content c:\services.txt) {
>> set-service $service -startuptype disabled
>> }
>>
PS C:\>

這段程式碼值得討論一下。請注意,我在 Foreach 建構第一行的結尾使用的是左大括弧 ({)。左大括弧和右大括弧之內的任何內容都視為在 Foreach 迴圈內,會針對輸入集合中的每個物件執行一次。請注意,我輸入 { 再按下 Enter 之後,Windows PowerShell 的提示字元即變成 >> (請參閱 [圖 2])。這表示它知道我已開始某種建構,而它正在等候我完成。接著我輸入我的 Set-Service cmdlet。這次我使用 $service 做為第一個參數,因為 $service 代表從我的文字檔案讀取的目前服務名稱。我在下一行使用右大括弧 (}) 結束建構。連按兩次 Enter 之後,Windows PowerShell 馬上執行我的程式碼。

圖 2 Windows Powershell 知道建構已開始

圖 2** Windows Powershell 知道建構已開始 **(按影像可放大)

沒錯,就是馬上。我輸入的內容很像指令碼,但是 Windows PowerShell 實際上是立即執行,並未儲存在任何文字檔案中。現在我要重新輸入全部內容,並新增一行程式碼以輸出目前的服務名稱:

PS C:\> foreach ($service in get-content c:\services.txt) {
>> set-service $service -startuptype disabled
>> "Disabling $service"
>> }
>>
Disabling messenger
Disabling alerter
PS C:\>

請注意,我要求它顯示一些內容,並使用雙引號括住要顯示的內容。雙引號告訴 Windows PowerShell 這是字串或文字,而非其他命令。但是,當您使用雙引號 (相對於單引號) 時,Windows PowerShell 會掃描文字字串,判斷是否有任何變數。如果找到任何變數,會以變數的實際值取代變數名稱。因此,它在執行此程式碼時,您可以看到顯示的是目前的服務名稱。

不過這仍然不是指令碼!

我到目前為止都互動式地在使用 Windows PowerShell,這是立即看到我編寫內容結果的一個很好的方式。不過,每次需要重新輸入這些程式碼是很煩的。這就是 Windows PowerShell 也可以執行指令碼的原因。事實上,Windows PowerShell 指令碼的行為就是很單純地在「輸入」指令碼:Windows PowerShell 基本上只是讀取指令碼文字檔案,並「輸入」它找到的每一行,完全和您手動輸入各行一樣。這表示我到目前為止所做的一切都能貼入指令碼檔案中。因此,我要使用筆記本建立一個名為 disableservices.ps1 的檔案,並貼入下列內容:

foreach ($service in get-content c:\services.txt) {
 set-service $service -startuptype disabled
 "Disabling $service"
 }

我將此檔案放在名為 C:\test 的資料夾中。接下來我要試著從 Windows PowerShell 中執行此檔案:

PS C:\test> disableservices
'disableservices' is not recognized as a cmdlet, function, operable program, or
<script file.
At line:1 char:15
+ disableservices <<<<
PS C:\test>

噢喔。哪裡出問題了?Windows PowerShell 搜尋 C:\test 資料夾,但是找不到我的指令檔。原因何在?由於安全性的限制,Windows PowerShell 的設計是不會從目前資料夾執行任何指令碼,藉此防止任何指令碼劫持作業系統命令。例如,我不能建立名為 dir.ps1 的指令碼,讓它覆寫正常的 dir 命令。如果我需要從目前資料夾執行指令碼,必須指定相對路徑:

PS C:\test> ./disableservices
The file C:\test\disableservices.ps1 cannot be loaded. 
The execution of scripts is disabled on this system. 
Please see "get-help about_signing" for more details.
At line:1 char:17
+ ./disableservices <<<<
PS C:\test>

接下來該怎麼辦?它還是不能正常動作。我的路徑正確,但是 Windows PowerShell 表示它無法執行指令碼。這是因為,Windows PowerShell 預設是無法執行指令碼的。這也是安全防護措施,它的設計是要防止我們在 VBScript 所遇到的問題。根據預設,惡意指令碼無法在 Windows PowerShell 中執行,因為依預設是無法執行任何指令碼的。若要執行我的指令碼,我必須明確變更執行原則:

PS C:\test> set-executionpolicy remotesigned

RemoteSigned 執行原則允許未經簽署的指令碼從本機電腦執行,下載的指令碼還是需要經過簽署才能執行。AllSigned 是一個更好的原則,它只會執行已經由受信任的發行者發行之憑證數位簽署過的指令碼。但是我手邊並沒有憑證,因此無法簽署我的指令碼,所以 RemoteSigned 是合理的選擇。接下來要再嘗試執行我的指令碼:

PS C:\test> ./disableservices
Disabling messenger
Disabling alerter
PS C:\test>

我要指出,我們所使用的 RemoteSigned 執行原則並不是最理想的選擇,只是合理的選擇而已。因為還有其他更好的解決方案。取得程式碼簽署憑證、使用 Windows PowerShell Set-AuthenticodeSignature cmdlet 簽署我的指令碼,以及將執行原則設定為更安全的 AllSigned 原則,才是比較安全的做法。

還有另一個原則 - Unrestricted,您一定要避免使用這個原則。此原則允許所有指令碼 (甚至來自遠端位置的惡意指令碼) 毫無限制地在您的電腦上執行,可能讓您冒著極大的風險。因此,不論是任何原因,建議您都不要使用 Unrestricted 原則。

立即結果

Windows PowerShell 中的互動式指令碼處理功能可讓您很快地建立指令碼原型,甚至只是指令碼的一小段。您可以立即得到結果,因此能夠很容易修改指令碼,得到您真正想要的結果。當您完成後,可以將程式碼移到 .ps1 檔案中使其成為永久可用的指令碼,以便在未來隨時存取。另外請記住:最理想的是,您應該數位簽署這些 .ps1 檔案,以保持 Windows PowerShell 的 AllSigned 執行原則設定,這是允許指令碼執行的最安全原則。

Don Jones 是 SAPIEN Technologies 的指令碼語言大師,也是 Windows PowerShell:TFM 的共同執筆者 (請參閱 www.SAPIENPress.com (英文))。Don 的連絡方式為:don@sapien.com

© 2008 Microsoft Corporation and CMP Media, LLC. 保留所有權利;未經允許,嚴禁部分或全部複製.