老實說,我在寫 PowerShell 的時候,偶爾會遇到一些非常葩的設計,很多時候不深入探究,根本就無法理解。以我今天要寫的這篇文章為例,當你執行一個 Cmdlet 並回傳資料時,當你可能拿到 0 筆、1 筆、超過 1 筆的情況時,正常人應該會覺得我們應該會得到一個「陣列」,但是你知道嗎,在 PowerShell 竟然可能會用到三種不同的處理方式,超怪的。因為這個問題實在遇到太多次了,這次我終於有空寫成文章,希望可以給遇到相同問題的人一些指引。
首先,你必須理解,這樣的邏輯是內建在 PowerShell Core 的語言特性中的,所以預設所有的 Cmdlet 都是這樣回傳的!
再來,你必須理解 PowerShell 的陣列表示法如下:
-
空陣列
@()
-
只有一個元素的陣列
@('Hello')
-
包含兩個元素的陣列
@('Hello', 'World')
奇葩特性一:System.ValueType
接著,我先設計三個不同的 function 讓你瞭解這個問題:
# 回傳 0 筆資料
function Get0 { return @() }
# 回傳 1 筆資料
function Get1 { return @(1) }
# 回傳 2 筆資料
function Get2 { return @(1,2) }
-
判斷一個 Cmdlet 或 Function 回傳為 0 筆的方法
(Get0)
(Get0).Count # 0
(Get0).Count -eq 0 # True
(Get0).Length # 0
(Get0).Length -eq 0 # True
(Get0)[0] # InvalidOperation: Cannot index into a null array.
(Get0).GetType() # InvalidOperation: You cannot call a method on a null-valued expression.
(Get0) -eq $null # True
各位觀眾,你說奇葩不奇葩,一個空值(null-valued expression)無法執行 GetType()
但卻可以取得 Count
或 Length
屬性值為 0
! 😅
-
判斷一個 Cmdlet 或 Function 回傳為 1 筆的方法
(Get1)
(Get1).Count # 1
(Get1).Count -eq 1 # True
(Get1).Length # 1
(Get1).Length -eq 1 # True
(Get1)[0] # 1
(Get1).GetType() # System.ValueType (這不是陣列類型)
(Get1) -eq $null # False
各位觀眾,你說奇葩不奇葩,一個感覺回傳 1
筆的陣列,其實不是陣列,但卻可以取得 Count
或 Length
屬性值為 1
! 😅
-
判斷一個 Cmdlet 或 Function 回傳為 0 筆的方法
(Get2)
(Get2).Count # 2
(Get2).Count -eq 2 # True
(Get2).Length # 2
(Get2).Length -eq 2 # True
(Get2)[0] # 2
(Get2).GetType() # System.Array (陣列類型)
(Get2) -eq $null # (這裡回傳一個空陣列喔,超怪的)
只有超過 1 筆的情況,才是個還算正常一點的「陣列」,不過陣列跟 $null
比較的結果,竟然是個陣列,還有沒有個正常一點的語言特性啊!
如果你覺得這樣已經很奇葩,那你就錯了,請繼續看下去! 🔥
奇葩特性二:System.Object
我再設計三個不同的 function 讓你瞭解另一種奇葩情況,這次我用字串來當成陣列元素,字串在 PowerShell 裡面使用 System.Object
型別:
# 回傳 0 筆資料
function Get0 { return @() }
# 回傳 1 筆資料
function Get1 { return @('Hello') }
# 回傳 2 筆資料
function Get2 { return @('Hello', 'World') }
-
判斷一個 Cmdlet 或 Function 回傳為 0 筆的方法
(Get0)
(Get0).Count # 0
(Get0).Count -eq 0 # True
(Get0).Length # 0
(Get0).Length -eq 0 # True
(Get0)[0] # InvalidOperation: Cannot index into a null array.
(Get0).GetType() # InvalidOperation: You cannot call a method on a null-valued expression.
(Get0) -eq $null # True
這個例子跟上一個例子一樣,沒有更奇葩!
-
判斷一個 Cmdlet 或 Function 回傳為 1 筆的方法
(Get1)
(Get1).Count # 1
(Get1).Count -eq 1 # True
(Get1).Length # 5 (喔喔喔,是 5 耶,因為 'Hello' 是 5 個字元)
(Get1).Length -eq 1 # False (事實證明 Get1 回傳的結果不是個陣列,而是個字串物件)
(Get1)[0] # H (回傳字串的第 1 個字元)
(Get1).GetType() # System.Object (這不是陣列類型)
(Get1) -eq $null # False
各位觀眾,請看上述註解部分,你說有沒有更奇葩了! 😅
-
判斷一個 Cmdlet 或 Function 回傳為 0 筆的方法
(Get2)
(Get2).Count # 2
(Get2).Count -eq 2 # True
(Get2).Length # 2
(Get2).Length -eq 2 # True
(Get2)[0] # Hello
(Get2).GetType() # System.Array (陣列類型)
(Get2) -eq $null # (這裡回傳一個空陣列喔,超怪的)
這個例子跟上一個例子一樣,沒有更奇葩!但我還是覺得 (Get2) -eq $null
出現一個空陣列真的超怪的!
實戰演練比對資料筆數的方法
經由上述的各種情境分析,你應該可以想像的到,網路上可能找到的解決方案,能有多奇葩,就有多奇葩,各種各式各樣的判斷式寫法都找的到,有些會複雜到讓你覺得懷疑人生。
最後,我想要總結一下在 PowerShell 之中判斷回傳筆數的標準寫法,那就是:
一律使用 (Cmdlet).Count 來取得筆數,超簡單!
不過,當你知道筆數之後,還要適度的修改你存取物件的方式,才能讓你的 PowerShell 程式如預期的執行。
-
取回 0 筆的情況
這個 Get-Job 命令在我的回傳為 0 筆,所以我們其實可以預期他會回傳一個 $null
空值。
$items = Get-Job
判斷方法:
if ($items.Count -eq 0) { echo 'No Data' }
注意: 不需要判斷結果是否為 $null
,但此時 $items
的值為 $null
喔!
有趣的地方就是,其實你可以用 ForEach-Object 在一個 $null
上,並不會出錯!
$items | foreach { $_.Id }
-
取回 1 筆的情況
這個 Get-Disk 命令在我的回傳為 1 筆,所以我們其實可以預期他會回傳一個 Object
物件 (非陣列)。
$items = Get-Disk
判斷方法:
if ($items.Count -eq 1) { $items | select Number,FriendlyName,HealthStatus }
注意: 不需要判斷結果是否為 $null
,但此時 $jobs
的值為一個 CimInstance
物件,不是一個陣列!
有趣的地方就是,其實你可以用 ForEach-Object 在一個不是陣列的物件上,並不會出錯!
$items | foreach { $_.FriendlyName }
-
取回超過 1 筆的情況
這個 Get-Process 命令在我的回傳為 411 筆,所以我們其實可以預期他會回傳一個 Array
陣列。
$items = Get-Process
判斷方法:
if ($items.Count -gt 1) { $items | foreach { $_.ProcessName } }
注意: 不需要判斷結果是否為 $null
,但此時 $items
的值為一個 Process
陣列!
你可以直接用 ForEach-Object 跑迴圈!
$items | foreach { $_.Name }
再補充幾個冷知識
網路上常會有人建議透過封裝在一個陣列的方式,解決回傳值可能不是陣列的問題,但是你有可能會遇到以下狀況。
-
取回 0 筆的情況
$item = $null
@($item).Length -eq 1
這種把 $null
包在「陣列」的寫法,陣列數量不會為 0,而是 1,所以放到迴圈跑,還是會跑一次!
-
取回 1 筆的情況
$item = 'Hello'
@($item).Length -eq 1
-
取回超過 1 筆的情況
$item = @('Hello', 'World')
@($item).Length -eq 2
這種「陣列」包「陣列」的寫法,在 PowerShell 並不會變成「二維陣列」喔,要注意!
所以網路上看到的這種建議,會在 0 筆的時候,額外跑一遍迴圈,所以不是一個真正完美的解法。
以下則是我的解法:
# 在取得物件之後,透過以下方法轉換成絕對的陣列
if ($item -eq $null) { $item = @() } else { $item = @($item) }
-
取回 0 筆的情況
$item = $null
# 在取得物件之後,透過以下方法轉換成絕對的陣列
if ($item -eq $null) { $item = @() } else { $item = @($item) }
@($item).Length -eq 0
這種把 $null
包在「陣列」的寫法,陣列數量不會為 0,而是 1,所以放到迴圈跑,還是會跑一次!
-
取回 1 筆的情況
$item = 'Hello'
# 在取得物件之後,透過以下方法轉換成絕對的陣列
if ($item -eq $null) { $item = @() } else { $item = @($item) }
@($item).Length -eq 1
-
取回超過 1 筆的情況
$item = @('Hello', 'World')
# 在取得物件之後,透過以下方法轉換成絕對的陣列
if ($item -eq $null) { $item = @() } else { $item = @($item) }
@($item).Length -eq 2
這種「陣列」包「陣列」的寫法,在 PowerShell 並不會變成「二維陣列」喔,要注意!
我後來又再次研究了一下,我發現只有 PowerShell Core 執行環境下,所有物件才有 .Count
屬性,而 Windows PowerShell 則只能說是「大部分」的物件都有 .Count
屬性,少部分物件類型還是沒有的! 🔥
總結
好吧,我要來總結了,綜合上述的的各種情境,我終於可以很自信的驗證一個觀念,那就是 PowerShell 雖然骨子裡以 .NET 為基礎,但他在語言特性上卻一點都不像 .NET 的強型別特性。我們在 .NET 經常需要判斷各種空值的情境,但是在 PowerShell 根本不用,畢竟這是給 IT 人員的腳本語言,因此開發人員絕對不能把 PowerShell 跟 .NET 混為一談。
我終於瞭解,原來開發人員寫不好 PowerShell 是有原因的! 😆
所以我最終的標準寫法,可以歸納出兩個結論:
-
判斷筆數用 .Count
,千萬不要用 .Length
!
$item.Count
此情境適用於 0, 1, >1 筆資料,但僅限於 PowerShell Core 環境下。
-
讀取資料不要直接存取,而是強制轉成陣列來處理,然後直接用 foreach 跑迴圈就對了,萬無一失!
# 在取得物件之後,透過以下方法轉換成絕對的陣列
if ($item -eq $null) { $item = @() } else { $item = @($item) }
$item | foreach { $_ }
網路上各種奇葩的寫法,還有各位之前寫過的程式碼,翻出來看可能都會覺得莫名其妙,原來這麼簡單就可以寫好了! 😍
相關連結