嗨,Scripting Guy!不太辛苦的工作照樣也有回報

The Microsoft Scripting Guys

下載本文程式碼: HeyScriptingGuy2008_06.exe (150KB)

數百年來, 人們總是相信辛勤工作本身就是一種回報;經過一日的努力勞動就會獲得真正的滿足:而幸福真諦來自目的地而非過程;還有 — 算了,您能夠意會就好。任勞任怨的幹活 (就像灰姑娘一樣),那麼總有一天您會獲得回報。

備註:這是真的嗎,辛苦耕耘不但讓您更快樂,而且總有一天會獲得回報嗎?為什麼要問我們這種問題?

當然,灰姑娘很幸運;畢竟,如果您跟邪惡的繼母和兩個壞心眼的姐姐一起住,就不愁沒有辛勤工作的機會了。但是,萬一您不像她這麼幸運呢?萬一您不是跟邪惡的繼母和兩個壞心眼的姐姐一起住怎麼辦?您要如何經歷漫長、精疲力盡又吃力不討好的工作日呢?您該如何尋覓幸福真諦?

如果您下定決心要拼死拼活的工作,那麼 Scripting Guy 建議您試著撰寫指令碼,以精緻、簡潔的表格格式輸出資料,如同 [圖 1] 的輸出所示。

[圖 1] 表格式輸出

[圖 1]** 表格式輸出 **(按一下影像可放大檢視)

就像我們說過的,灰姑娘實在太幸運了:她必須把房子打掃的一塵不染,工作結束後,還得要坐在充滿灰燼和煤煙的壁爐中。但是一說到要撰寫以表格格式顯示資料的指令碼,即使是灰姑娘也會知難而退。坐在灰燼和煤煙中是一件事;撰寫以表格格式輸出資料的指令碼又是另一回事。

備註:喔,除非您必須坐在壁爐裡撰寫指令碼;若是如此,那就差不多一樣了。

為何這項工作會讓灰姑娘心生膽怯?因為它難斃了。要在 VBScript 中使用表格格式顯示資料只有一個辦法,就是運用下列幾行:

  • 首先是確定每一欄的最大值。比方說,您可能希望服務的顯示名稱最多可填入 52 個字元。
  • 如此一來,您要計算資料片段的字元數。例如,顯示名稱 Adobe LM Service 有 16 個字元。
  • 如果顯示名稱超過 52 個字元的限制,確定必須從字串結尾砍斷的字元數,好讓顯示名稱能放入所分配的空間中。如果顯示名稱少於 52 個字元,確定必須加入字串結尾的空格數,好讓長度剛好等於 52 個字元。
  • 對下一組資料重複執行相同的動作。再下一個也是,然後依此類推。

如果您很熟悉灰姑娘的故事,那麼您應該知道神仙教母可能會突然現身,把老鼠變成系統管理員 (至於這個戲法有多容易,我們就不予置評了),然後那隻老鼠就會替您撰寫這個指令碼。不過,您大概不應該奢望會發生這種奇蹟。關於這件苦差事,您只能全靠自己了。

不過往好的方面想,您將是全世界最歡喜的人了。因為您同時也將是全世界最辛苦工作的人。至今還沒有其他工作比它更艱困。

當然,有些人可能願意拿一點點快樂來交換不必那麼辛苦工作的機會。若您也是其中之一,您可能會想:「感謝老天,我們有 Scripting Guy;他們會教我如何將老鼠變成系統管理員,然後教我如何讓老鼠將我的輸出格式化成表格」。可惜,恐怕我們要告訴您一個壞消息:Scripting Guy 不曉得如何將老鼠變成系統管理員 (不過我們可以將系統管理員變成老鼠,如果這樣對您有幫助的話)。我們不知道如何讓別人幫您寫指令碼,更別說是撰寫指令碼來使用表格格式顯示輸出資料了。

可是沒關係,只要您是執行 Windows® XP 或 Windows Server® 2003 (很多人都是),您就不需要老鼠來為您撰寫指令碼 (抱歉了,Windows Vista® 恐怕並不適用)。您可以自己完成這項工作,而且不用多花多少力氣,這都要感謝 Microsoft.CmdLib 物件。請看一下 [圖 2] 中的範例指令碼。

Figure 2 建立表格式顯示

Dim arrResultsArray()
i = 0

Set objCmdLib = _
  CreateObject("Microsoft.CmdLib")
Set objCmdLib.ScriptingHost = _
  WScript.Application

arrHeader = Array("Display Name", _
  "State", "Start Mode")
arrMaxLength = Array(52, 12, 12)
strFormat = "Table"
blnPrintHeader = True
arrBlnHide = Array(False, False, False)

strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" _
  & strComputer & "\root\cimv2")

Set colServices = objWMIService.ExecQuery _
  ("Select * FROM Win32_Service")

For Each objService In colServices
  ReDim Preserve arrResultsArray(i)
  arrResultsArray(i) = _
  Array(objService.DisplayName, _
    objService.State,objService.StartMode)
  i = i + 1
Next
objCmdLib.ShowResults arrHeader, _
  arrResultsArray, arrMaxLength, _
  strFormat, blnPrintHeader, arrBlnHide

稍微解釋一下,Microsoft.CmdLib 是 Windows XP 和 Windows Server 2003 隨附的 COM 物件。這個物件有幾項不同的功能;如需詳細資訊,請在命令提示字元中輸入 cmdlib.wsc /?,並閱讀螢幕上所顯示的指令碼檔案中的命令。不過今天我們只關心 Microsoft.CmdLib 的一項功能:以表格格式顯示資料的能力。

這顯然會產生一個問題:如何以表格格式顯示資料?讓我們看看能不能找到方法。如您所見,我們的範例指令碼一開始會定義名為 arrResultsArray 的動態陣列:

Dim arrResultsArray()

在典型的指令碼中,資料會在一被擷取時就立即傳回螢幕。但是您怎麼能期望 Scripting Guy 採取這麼平凡的作法?在此指令碼中,我們不會在資料被擷取時立即傳回資料。相反地,我們要將所有傳回的資料儲存在陣列中,然後讓 Microsoft.CmdLib 格式化並顯示資料。

換句話說,這也是為什麼一開始要建立動態陣列 (也就是可以在指令碼的執行過程中調整大小的陣列) 的原因。定義陣列之後,我們將名為 i 的計數器變數值設為 0;我們將使用這個值來追蹤目前的陣列大小。

如果您想知道,我們將 i 值設為 0 的原因是,陣列中的第一個項目永遠是指定為索引編號 0。因此,將第一個項目新增到陣列時,所新增的是項目 0 而非項目 1。

接下來我們必須初始化 Microsoft.CmdLib 物件的執行個體;以下兩行程式碼就是負責這項工作:

Set objCmdLib = _
CreateObject("Microsoft.CmdLib")
Set objCmdLib.ScriptingHost = WScript.Application

接著就到下面這一小段程式碼區塊:

arrHeader = Array("Display Name", "State", "Start Mode")
arrMaxLength = Array(52, 12, 12)
strFormat = "Table"
blnPrintHeader = True
arrBlnHide = Array(False, False, False)

在此我們要設定幾個參數來設定輸出。在第一行中,我們將值指派到名為 arrHeader 的陣列;您大概已經猜到,這些是輸出表格中各欄的標題。在此範例指令碼中,我們要擷取有關電腦上執行的服務資訊,接著顯示各個服務的 DisplayName、State 及 StartMode 屬性值。

不用說,我們為陣列指派了欄名稱 Display Name、State 和 Start Mode (當然我們也可以將欄名稱簡單指定為 A、B 和 C;Larry、Moe 和 Curly;或是任何我們想使用的名稱。欄名稱不一定要跟屬性名稱一樣)。

備註:灰姑娘的故事有好幾個版本,後母生的姐姐也有不同的名字版本。在迪士尼版本的灰姑娘故事中,兩個姐姐的名字是 Drizzella 和 Anastasia,這剛好也是我們的 Scripting Editor 的名字和中間名!

在第二行程式碼中,我們將值指派給另一個名為 arrMaxLength 的陣列。這個陣列將每一欄的大小存放在表格中。在我們的輸出中,我們要配置 52 個字元空間到欄 1 (服務顯示名稱);接著我們要分別配置 12 個空格到服務狀態和啟動模式中 (Microsoft.CmdLib 會自動在各欄之間插入空格)。如果我們希望欄大小是 52、12,還有 12 個字元空間 (實際上也是如此),便要使用如下所示的程式碼:

arrMaxLength = Array(52, 12, 12)

第三行指定資料的輸出格式。我們想要將資料顯示成表格;因此將 strFormat (存放輸出類型的變數) 的值設為 Table。假設我們是要將資料顯示成以逗號分隔的值清單。此時,我們要將格式設為 CSV,如下所示:

strFormat = "CSV"

如此一來,會得到類似下面的輸出結果:

"Display Name","State","Start Mode"
"Adobe LM Service","Stopped","Manual"
"Adobe Active File Monitor V4","Stopped","Manual"
"Alerter","Stopped","Manual"
"Application Layer Gateway Service","Running","Manual"
"Apple Mobile Device","Running","Auto"
"Application Management","Stopped","Manual"

要注意的是,Microsoft.CmdLib 不僅在每個項目之間插入逗號,而且還將各個值以雙引號括住。這個功能比您一開始想的還棒。畢竟,在手動執行的情況下,您必須使用如下的程式碼:

Wscript.Echo Chr(34) & objService.DisplayName & Chr(34) & "," & Chr(34) & objService.State & Chr(34) & "," & Chr(34) & objService.StartMode & Chr(34)

真討厭。

您說什麼?您不喜歡表格,也不喜歡 CSV 格式?如果是這樣的話,不妨試著將格式設為 List,您會得到類似下面的輸出:

Display Name: Adobe LM Service
State:        Stopped
Start Mode:   Manual
Display Name: Adobe Active File Monitor V4
State:        Stopped
Start Mode:   Manual

不過這已經離題了 (我們有時容易離題)。定義輸出格式之後,我們將名為 blnPrintHeader 的變數值設為 True:

blnPrintHeader = True

我們要使用 blnPrintHeader 來告訴 Microsoft.CmdLib 列印欄標題。如果我們不想列印欄標題呢?沒關係;在這種情況下,將 blnPrintHeader 設為 False:

blnPrintHeader = False

最後是以下這行程式碼:

arrBlnHide = Array(False, False, False)

要顯示資料時,Microsoft.CmdLib 可讓您選擇顯示或隱藏欄。若要隱藏欄 (也就是隱藏特定屬性的資料),請將該屬性值設為 True;若要顯示欄,請將值設為 False。

在我們的輸出中,我們要依序顯示 DisplayName、State 和 StartMode 屬性的值;因此陣列中使用的值分別是 False、False、False。萬一我們要顯示 DisplayName、隱藏 State,並顯示 StartMode 呢?若是這樣,我們要使用下列這行程式碼:

arrBlnHide = Array(False, True, False)

請記住,使用 False 會顯示欄,True 則是隱藏欄。

此時,我們已經準備好輸出一些資料 — 如果真的有資料的話,就會是這樣 (Scripting Guy 可曾撰寫無法輸出資料的指令碼,然後花費過多時間來偵錯該指令碼,結果卻發現一開始根本不必費事擷取任何資料?呃,當然沒有這種事;您怎麼會這麼想?)

知道這一點之後,我們的下一步是繫結到本機電腦上的 Windows Management Instrumentation (WMI) 服務,然後使用 ExecQuery 方法來擷取關於該電腦上安裝的所有服務資訊:

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")

Set colServices = objWMIService.ExecQuery ("Select * FROM Win32_Service")

我們應該要澄清一點,在此您不止能使用 WMI 資料。也許您已經猜到,Microsoft.CmdLib 適用於任何資料類型。WMI 只是方便我們擷取一堆資料的手段。

收到服務相關資料集合之後,我們要設定 For Each 迴圈,對集合中的所有服務重複執行同樣的動作。不過,在此並非傳回每個服務的屬性值,而是要執行下列程式碼:

ReDim Preserve arrResultsArray(i)
arrResultsArray(i) = Array(objService.DisplayName, objService.State,objService.StartMode)
i = i + 1

這個程式碼區塊的功用到底是什麼?在第 1 行中,我們要調整陣列大小,所使用的命令 ReDim Preserve 不僅能調整陣列大小,還可以保留該陣列中現有的資料 (若少了 Preserve 關鍵字,雖然能調整陣列大小,但是該陣列中現存的資料全都會刪除清除)。

我們要將陣列調整成什麼大小?第一次執行迴圈時,我們要讓陣列大小變成 0;換言之,我們要建立包含一個項目的陣列。那麼我們如何確知已經將陣列大小變成 0 了呢?因為我們使用了計數器變數 i,而且該變數等於 0。

因此,如果我們使用變數 i 來代表陣列大小,不就表示我們永遠會將大小設為 0 嗎?的確如此,只不過每執行一次迴圈,我們將會將 i 值加 1;如您所見,我們在程式碼區塊的第 3 行就是這麼做。

現在就剩下一行程式碼要操心了:

arrResultsArray(i) = Array(objService.DisplayName, objService.State,objService.StartMode)

在此我們只是取得感興趣的屬性值 — DisplayName、State 和 StartMode — 然後將它們新增到陣列 arrResultsArray 中。請注意,我們不會個別新增屬性值;而是以值陣列的方式加入。的確,這有點不尋常:意思是陣列 arrResultsArray 中的每個項目都是另一個陣列;而這正是 Microsoft.CmdLib 運作的方式。

當然,您可能又要擔心了。「陣列的陣列?我到底要怎麼做才能在充滿陣列的陣列中存取所有個別值?」別慌張,其實很簡單:讓 Microsoft.CmdLib 幫您做就行了。

備註:很抱歉,不過存取陣列的陣列中的所有個別值大概是 Microsoft.CmdLib 唯一能為您做的事。可是下一版物件可能會考慮加入打掃壞後母的房間、洗碗,還有幫討厭的姐姐縫衣服等功能。

事實是,我們只要呼叫 ShowResults 方法,然後將之前在指令碼中設定的所有陣列及變數都傳送到此方法,就可以使用簡潔的表格格式輸出資料:

objCmdLib.ShowResults arrHeader, arrResultsArray, arrMaxLength, strFormat, blnPrintHeader, arrBlnHide

然後會產生什麼結果呢?結果就是我們所要的正確格式化輸出,如 [圖 1] 所示。

不賴吧?您現在有了美觀的輸出,而且幾乎不費吹灰之力 (您的下一個指令碼甚至更簡單,因為您只需要對此處的第一個指令碼做一點小更動)。

當然,您不像灰姑娘那樣日夜操勞,才能使用表格顯示指令碼輸出,所以您也無法享受像灰姑娘一樣的滿足感。而且老實說,您大概也不可能嫁給王子;這並非本文的用意。不過,為了從 VBScript 指令碼獲得這麼棒的輸出,付出一點小代價也是值得的,尤其是一想到當今王子的水準。

而且誰曉得:或許您自己也能找到一位王子也說不定。畢竟,就連皇室成員也需要了解他們電腦上安裝的所有服務資訊,不是嗎?

Dr. Scripto 的指令碼謎題 (Scripting Perplexer)

這個每月一次的挑戰不僅測試您的解謎功力,更要測試您的指令碼技巧。

2008 年 6 月PowerShell 途徑

在這些謎題中,從水平、垂直和對角方向連結字母,藉此構成某 Windows PowerShell 指令程式的名稱。每個字母都只能使用一次。例子如下:

在這個謎題中建構的指令程式是 New-Alias。現在換您了。以下還有三個謎題:

ANSWER:

Dr. Scripto 的指令碼謎題 (Scripting Perplexer)

答案是:PowerShell Pathway,2008 年 6 月

Read-Host

Set-AuthenticodeSignature

The Microsoft Scripting Guys 為 Microsoft 做事,也就是受雇於 Microsoft。他們在不玩、不教或不看棒球 (以及其他各種活動) 的時候,就負責管理 TechNet 指令碼中心。請造訪他們的網站:https://www.microsoft.com/taiwan/technet/scriptcenter/default.mspx

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