[英]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) 。
便捷函數ConvertFrom-CsvTyped
(下面的源代碼)克服了ConvertFrom-Csv
始終只創建字符串類型屬性的限制,方法是啟用自定義標題符號,該符號支持在標題行中的每個列名前加上類型文字; 例如[int] ID
(有關 PowerShell 類型文字的系統概述,請參閱此答案,它可以引用任何 .NET 類型)。
這使您能夠從輸入 CSV創建(非字符串)類型的屬性,只要目標類型的值可以表示為數字或字符串文字,其中包括:
[int]
、 [long]
、 [double]
、 [decimal]
……)[datetime]
、 [datetimeoffset]
和[timespan]
[bool]
(使用0
和1
作為列值)[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.