簡體   English   中英

如何在 powershell 腳本中處理內聯 CSV?

[英]How to process inline CSV in powershell script?

我試圖避免使用 powershell 中常用的極其冗長的哈希映射和數組。 為什么? 因為我有 100 行,當我只需要 CSV 類型時,將每一行都包裝在@(name='foo; id='bar')等中是沒有任何意義的的陣列。

$header = @('name', 'id', 'type', 'loc')

$mycsv = @(
    # name, id, type, loc
    'Brave', 'Brave.Brave', 1, 'winget'
    'Adobe Acrobat (64-bit)', '{AC76BA86-1033-1033-7760-BC15014EA700}', 2, ''
    'GitHub CLI', 'GitHub.cli', 3, 'C:\portable'
)

# Do some magic here to set the CSV / hash headers so I can use them as shown below

Foreach ($app in $mycsv) {
    Write-Host "App Name: $app.name"
    Write-Host "App Type: $app.type"
    Write-Host "App id  : $app.id"
    Write-Host "App Loc : $app.type"
    Write-Host ("-"*40)
}

我相信你知道我要去哪里。

那么如何使用標題名稱逐行處理內聯 CSV?

預期輸出:

App Name: Brave
App Type: 1
App id  : Brave.Brave
App Loc : winget
----------------------------------------
...

更新: 2022-12-03

最終的解決方案是以下非常簡短且不冗長的代碼:

$my = @'
    name,id,type,loc
    Brave, Brave.Brave,1,winget
    "Adobe Acrobat (64-bit)",{AC76BA86-1033-1033-7760-BC15014EA700},2,
    GitHub CLI,GitHub.cli,,C:\portable
'@ 

ConvertFrom-Csv $my | % {
    Write-Host "App Name: $($_.name)"
    Write-Host "App Type: $($_.type)"
    Write-Host "App id  : $($_.id)"
    Write-Host "App Loc : $($_.loc)"
    Write-Host $("-"*40)
}

您可以使用內存中的 CSV 數據的字符串表示形式,使用here-string並使用ConvertFrom-Csv將其解析為對象

# This creates objects ([pscustomobject] instances) with properties
# named for the fields in the header line (the first line), i.e: 
#  .name, .id. .type, and .loc
# NOTE: 
# * The whitespace around the fields is purely for *readability*.
# * If any field values contain "," themselves, enclose them in "..."
$mycsv =
@'
  name,                   id,                                       type, loc
  Brave,                  Brave.Brave,                              1,    winget
  Adobe Acrobat (64-bit), {AC76BA86-1033-1033-7760-BC15014EA700},   2,
  GitHub CLI,             GitHub.cli,                               3,    C:\portable
'@ | ConvertFrom-Csv

$mycsv | Format-List $mycsv | Format-List然后提供所需的輸出(沒有Format-List ,您將獲得隱式Format-Table格式,因為對象的屬性不超過 4 個)。

  • 順便說一句: Format-List本質上提供了您在Write-Host調用循環中嘗試過的用於顯示的格式; 如果您確實需要后一種方法,請注意,正如Walter Mitty 的回答中指出的那樣,您需要將屬性訪問表達式(例如$_.name包含在$(...)中,以便在可擴展的 (...雙引號) PowerShell string ( "..." ) - 請參閱此答案以系統概述 PowerShell 的可擴展字符串(字符串插值)的語法。

筆記:

  • 這種方法很方便

    • 允許您省略引號,除非需要,即僅當字段值恰好包含,本身時。

      • 在本身包含的字段值周圍使用"..."引號) ,'...' ,即引號在 CSV 數據中沒有句法含義,任何'字符都被逐字保留)。

        • 如果這樣的字段另外包含"字符,則將它們轉義為""
    • 允許您使用附帶的空格提高可讀性,如上所示。

  • 還可以在輸入中使用 ,以外,分隔符(例如| ,並通過-Delimiter參數將其傳遞給ConvertFrom-Csv

  • 注意: CSV 數據通常是無類型的,這意味着ConvertFrom-Csv (以及Import-Csv )創建的對象的屬性都是字符串[string ]-typed)


可選閱讀:支持創建類型化屬性的自定義 CSV 表示法:

便捷函數ConvertFrom-CsvTyped (下面的源代碼)克服了ConvertFrom-Csv始終只創建字符串類型屬性的限制,方法是啟用自定義標題符號,該符號支持在標題行中的每個列名前加上類型文字 例如[int] ID (有關 PowerShell 類型文字的系統概述,請參閱此答案,它可以引用任何 .NET 類型)。

這使您能夠從輸入 CSV創建(非字符串)類型的屬性只要目標類型的值可以表示為數字或字符串文字,其中包括:

  • 數字類型( [int][long][double][decimal] ……)
  • 日期和時間相關類型[datetime][datetimeoffset][timespan]
  • [bool] (使用01作為列值)
  • 要測試給定類型是否可以使用,請從示例編號或字符串中轉換它,例如: [timespan] '01:00'[byte] 0x40

示例 - 請注意第二和第三列名稱[int][datetime]之前的類型文字:

@'
  Name,        [int] ID, [datetime] Timestamp
  Forty-two,   0x2a,     1970-01-01
  Forty-three, 0x2b,     1970-01-02
'@ | ConvertFrom-CsvTyped

輸出 - 注意十六進制。 數字被這樣識別(默認情況下格式化為小數),以及數據字符串如何被識別為[datetime]實例:

Name        ID Timestamp
----        -- ---------
Forty-two   42 1/1/1970 12:00:00 AM
Forty-three 43 1/2/1970 12:00:00 AM

在上面的調用中添加-AsSourceCode允許您將解析的對象輸出為 PowerShell 源代碼字符串,即作為[pscustomobject]文字數組:

@'
  Name,        [int] ID, [datetime] Timestamp
  Forty-two,   0x2a,     1970-01-01
  Forty-three, 0x2b,     1970-01-02
'@ | ConvertFrom-CsvTyped -AsSourceCode

輸出 - 請注意,如果您要在腳本中使用它或將其用作Invoke-Expression的輸入(僅用於測試),您將獲得與上述相同的對象和用於顯示的輸出:

@(
  [pscustomobject] @{ Name = 'Forty-two'; ID = [int] 0x2a; Timestamp = [datetime] '1970-01-01' }
  [pscustomobject] @{ Name = 'Forty-three'; ID = [int] 0x2b; Timestamp = [datetime] '1970-01-02' }
)

ConvertFrom-CsvTyped源碼:
function ConvertFrom-CsvTyped {
  <#
.SYNOPSIS
  Converts CSV data to objects with typed properties;
.DESCRIPTION
  This command enhances ConvertFrom-Csv as follows:
   * Header fields (column names) may be preceded by type literals in order
     to specify a type for the properties of the resulting objects, e.g. "[int] Id"
   * With -AsSourceCode, the data can be transformed to an array of 
    [pscustomobject] literals.

.PARAMETER Delimiter
  The single-character delimiter (separator) that separates the column values.
  "," is  the (culture-invariant) default.

.PARAMETER AsSourceCode
  Instead of outputting the parsed CSV data as objects, output them as
  as source-code representations in the form of an array of [pscustomobject] literals.

.EXAMPLE
  "Name, [int] ID, [datetime] Timestamp`nForty-two, 0x40, 1970-01-01Z" | ConvertFrom-CsvTyped
  
  Parses the CSV input into an object with typed properties, resulting in the following for-display output:
    Name      ID Timestamp
    ----      -- ---------
    Forty-two 64 12/31/1969 7:00:00 PM  

  .EXAMPLE
  "Name, [int] ID, [datetime] Timestamp`nForty-two, 0x40, 1970-01-01Z" | ConvertFrom-CsvTyped -AsSourceCode
  
  Transforms the CSV input into an equivalent source-code representation, expressed
  as an array of [pscustomobject] literals:
    @(
      [pscustomobject] @{ Name = 'Forty-two'; ID = [int] 0x40; Timestamp = [datetime] '1970-01-01Z' }
    )
#>

  [CmdletBinding(PositionalBinding = $false)]
  param(
    [Parameter(Mandatory, ValueFromPipeline)]
    [string[]] $InputObject,
    [char] $Delimiter = ',',
    [switch] $AsSourceCode
  )
  begin {
    $allLines = ''
  }
  process {
    if (-not $allLines) {
      $allLines = $InputObject -join "`n"
    }
    else {
      $allLines += "`n" + ($InputObject -join "`n")
    }
  }
  end {

    $header, $dataLines = $allLines -split '\r?\n'

    # Parse the header line in order to derive the column (property) names.
    [string[]] $colNames = ($header, $header | ConvertFrom-Csv -ErrorAction Stop -Delimiter $Delimiter)[0].psobject.Properties.Name
    [string[]] $colTypeNames = , 'string' * $colNames.Count
    [type[]] $colTypes = , $null * $colNames.Count
    $mustReType = $false; $mustRebuildHeader = $false

    if (-not $dataLines) { throw "No data found after the header line; input must be valid CSV data." }

    foreach ($i in 0..($colNames.Count - 1)) {
      if ($colNames[$i] -match '^\[([^]]+)\]\s*(.*)$') {
        if ('' -eq $Matches[2]) { throw "Missing column name after type specifier '[$($Matches[1])]'" }
        if ($Matches[1] -notin 'string', 'System.String') {
          $mustReType = $true
          $colTypeNames[$i] = $Matches[1]
          try {
            $colTypes[$i] = [type] $Matches[1]
          }
          catch { throw }
        }
        $mustRebuildHeader = $true
        $colNames[$i] = $Matches[2]
      }
    }
    if ($mustRebuildHeader) {
      $header = $(foreach ($colName in $colNames) { if ($colName -match [regex]::Escape($Delimiter)) { '"{0}"' -f $colName.Replace('"', '""') } else { $colName } }) -join $Delimiter
    }

    if ($AsSourceCode) {
      # Note: To make the output suitable for direct piping to Invoke-Expression (which is helpful for testing),
      #       a *single* string mut be output.
    (& {
        "@("
        & { $header; $dataLines } | ConvertFrom-Csv -Delimiter $Delimiter | ForEach-Object {
          @"
    [pscustomobject] @{ $(
    $(foreach ($i in 0..($colNames.Count-1)) {
      if (($propName = $colNames[$i]) -match '\W') {
        $propName = "'{0}'" -f $propName.Replace("'", "''")
      }
      $isString = $colTypes[$i] -in $null, [string]
      $cast = if (-not $isString) { '[{0}] ' -f $colTypeNames[$i] }
      $value = $_.($colNames[$i])
      if ($colTypes[$i] -in [bool] -and ($value -as [int]) -notin 0, 1) { Write-Warning "'$value' is interpreted as `$true - use 0 or 1 to represent [bool] values."  }
      if ($isString -or $null -eq ($value -as [double])) { $value = "'{0}'" -f $(if ($null -ne $value) { $value.Replace("'", "''") }) }
      '{0} = {1}{2}' -f $colNames[$i], $cast, $value
    }) -join '; ') }
"@
        }
        ")"
      }) -join "`n"
    }
    else {
      if (-not $mustReType) {
        # No type-casting needed - just pass the data through to ConvertFrom-Csv
        & { $header; $dataLines } | ConvertFrom-Csv -ErrorAction Stop -Delimiter $Delimiter
      }
      else {
        # Construct a class with typed properties matching the CSV input dynamically
        $i = 0
        @"
class __ConvertFromCsvTypedHelper {
$(
  $(foreach ($i in 0..($colNames.Count-1)) {
    '  [{0}] ${{{1}}}' -f $colTypeNames[$i], $colNames[$i]
  }) -join "`n"
)
}
"@ | Invoke-Expression

        # Pass the data through to ConvertFrom-Csv and cast the results to the helper type.
        try {
          [__ConvertFromCsvTypedHelper[]] (& { $header; $dataLines } | ConvertFrom-Csv -ErrorAction Stop -Delimiter $Delimiter)
        }
        catch { $_ }
      }
    }
  }
}

以下是一些可以幫助您使用 CSV 格式數據的技巧。 我稍微改變了你的輸入。 我沒有定義單獨的標題,而是將標題記錄作為 CSV 數據的第一行。 這就是 ConvertFrom-CSV 所期望的。 我還把單引號改成了雙引號。 我完全省略了一個字段。

第一個輸出顯示如果將 ConvertFrom-CSV 的輸出提供給 format-List 會發生什么。 如果您計划使用變量中的數據,我不建議您這樣做。 format-list 適合顯示,但不適合進一步處理。

第二個輸出模仿您的示例輸出。 here 字符串包含各種子表達式,每個子表達式都可以通過自動變量 $_ 訪問當前數據。

最后,我向您展示管道流的成員。 請注意從您的字段名稱中獲取名稱的四個屬性。

$mycsv = @"
name, id, type, loc
"Brave", "Brave.Brave", 1, "winget"
"Adobe Acrobat (64-bit)", "{AC76BA86-1033-1033-7760-BC15014EA700}", 2,
"GitHub CLI", "GitHub.cli", 3, "C:\portable"
"@

ConvertFrom-CSV $mycsv | Format-List

ConvertFrom-Csv $mycsv | % {@"
App Name: $($_.name)
App Type: $($_.type)
App id  : $($_.id)
App Loc : $($_.loc)
$("-"*40)
"@
}

ConvertFrom-CSV $mycsv | gm

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM