The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

PowerShell 如何判斷 Cmdlet 或 Function 回傳資料的筆數 (0, 1, >1)

老實說,我在寫 PowerShell 的時候,偶爾會遇到一些非常葩的設計,很多時候不深入探究,根本就無法理解。以我今天要寫的這篇文章為例,當你執行一個 Cmdlet 並回傳資料時,當你可能拿到 0 筆、1 筆、超過 1 筆的情況時,正常人應該會覺得我們應該會得到一個「陣列」,但是你知道嗎,在 PowerShell 竟然可能會用到三種不同的處理方式,超怪的。因為這個問題實在遇到太多次了,這次我終於有空寫成文章,希望可以給遇到相同問題的人一些指引。

首先,你必須理解,這樣的邏輯是內建在 PowerShell Core 的語言特性中的,所以預設所有的 Cmdlet 都是這樣回傳的!

再來,你必須理解 PowerShell 的陣列表示法如下:

  1. 空陣列

    @()
    
  2. 只有一個元素的陣列

    @('Hello')
    
  3. 包含兩個元素的陣列

    @('Hello', 'World')
    

PowerShell Array

奇葩特性一:System.ValueType

接著,我先設計三個不同的 function 讓你瞭解這個問題:

# 回傳 0 筆資料
function Get0 { return @() }
# 回傳 1 筆資料
function Get1 { return @(1) }
# 回傳 2 筆資料
function Get2 { return @(1,2) }
  1. 判斷一個 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
    

    image

    各位觀眾,你說奇葩不奇葩,一個空值(null-valued expression)無法執行 GetType() 但卻可以取得 CountLength 屬性值為 0! 😅

  2. 判斷一個 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
    

    image

    各位觀眾,你說奇葩不奇葩,一個感覺回傳 1 筆的陣列,其實不是陣列,但卻可以取得 CountLength 屬性值為 1! 😅

  3. 判斷一個 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     # (這裡回傳一個空陣列喔,超怪的)
    

    image

    只有超過 1 筆的情況,才是個還算正常一點的「陣列」,不過陣列跟 $null 比較的結果,竟然是個陣列,還有沒有個正常一點的語言特性啊!

如果你覺得這樣已經很奇葩,那你就錯了,請繼續看下去! 🔥

奇葩特性二:System.Object

我再設計三個不同的 function 讓你瞭解另一種奇葩情況,這次我用字串來當成陣列元素,字串在 PowerShell 裡面使用 System.Object 型別:

# 回傳 0 筆資料
function Get0 { return @() }
# 回傳 1 筆資料
function Get1 { return @('Hello') }
# 回傳 2 筆資料
function Get2 { return @('Hello', 'World') }
  1. 判斷一個 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
    

    image

    這個例子跟上一個例子一樣,沒有奇葩!

  2. 判斷一個 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
    

    image

    各位觀眾,請看上述註解部分,你說有沒有奇葩了! 😅

  3. 判斷一個 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     # (這裡回傳一個空陣列喔,超怪的)
    

    image

    這個例子跟上一個例子一樣,沒有奇葩!但我還是覺得 (Get2) -eq $null 出現一個空陣列真的超怪的!

實戰演練比對資料筆數的方法

經由上述的各種情境分析,你應該可以想像的到,網路上可能找到的解決方案,能有多奇葩,就有多奇葩,各種各式各樣的判斷式寫法都找的到,有些會複雜到讓你覺得懷疑人生。

最後,我想要總結一下在 PowerShell 之中判斷回傳筆數的標準寫法,那就是:

一律使用 (Cmdlet).Count 來取得筆數,超簡單!

不過,當你知道筆數之後,還要適度的修改你存取物件的方式,才能讓你的 PowerShell 程式如預期的執行。

  1. 取回 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 }
    
  2. 取回 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 }
    
  3. 取回超過 1 筆的情況

    這個 Get-Process 命令在我的回傳為 411 筆,所以我們其實可以預期他會回傳一個 Array 陣列。

    $items = Get-Process
    

    判斷方法:

    if ($items.Count -gt 1) { $items | foreach { $_.ProcessName } }
    

    注意: 不需要判斷結果是否為 $null,但此時 $items 的值為一個 Process 陣列!

    你可以直接用 ForEach-Object 跑迴圈!

    $items | foreach { $_.Name }
    

再補充幾個冷知識

網路上常會有人建議透過封裝在一個陣列的方式,解決回傳值可能不是陣列的問題,但是你有可能會遇到以下狀況。

  1. 取回 0 筆的情況

    $item = $null
    @($item).Length -eq 1
    

    這種把 $null 包在「陣列」的寫法,陣列數量不會為 0,而是 1,所以放到迴圈跑,還是會跑一次!

  2. 取回 1 筆的情況

    $item = 'Hello'
    @($item).Length -eq 1
    
  3. 取回超過 1 筆的情況

    $item = @('Hello', 'World')
    @($item).Length -eq 2
    

    這種「陣列」包「陣列」的寫法,在 PowerShell 並不會變成「二維陣列」喔,要注意!

所以網路上看到的這種建議,會在 0 筆的時候,額外跑一遍迴圈,所以不是一個真正完美的解法。

以下則是我的解法:

# 在取得物件之後,透過以下方法轉換成絕對的陣列
if ($item -eq $null) { $item = @() } else { $item = @($item) }
  1. 取回 0 筆的情況

    $item = $null
    
    # 在取得物件之後,透過以下方法轉換成絕對的陣列
    if ($item -eq $null) { $item = @() } else { $item = @($item) }
    
    @($item).Length -eq 0
    

    這種把 $null 包在「陣列」的寫法,陣列數量不會為 0,而是 1,所以放到迴圈跑,還是會跑一次!

  2. 取回 1 筆的情況

    $item = 'Hello'
    
    # 在取得物件之後,透過以下方法轉換成絕對的陣列
    if ($item -eq $null) { $item = @() } else { $item = @($item) }
    
    @($item).Length -eq 1
    
  3. 取回超過 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 是有原因的! 😆

所以我最終的標準寫法,可以歸納出兩個結論:

  1. 判斷筆數用 .Count,千萬不要用 .Length

    $item.Count
    

    此情境適用於 0, 1, >1 筆資料,但僅限於 PowerShell Core 環境下。

  2. 讀取資料不要直接存取,而是強制轉成陣列來處理,然後直接用 foreach 跑迴圈就對了,萬無一失!

    # 在取得物件之後,透過以下方法轉換成絕對的陣列
    if ($item -eq $null) { $item = @() } else { $item = @($item) }
    
    $item | foreach { $_ }
    

網路上各種奇葩的寫法,還有各位之前寫過的程式碼,翻出來看可能都會覺得莫名其妙,原來這麼簡單就可以寫好了! 😍

相關連結