如何在powershell脚本中处理内联CSV



我正在努力避免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)
}

您可以在内存中使用,即使用here字符串来表示CSV数据,并使用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然后提供所需的输出(如果没有Format-List,您将获得隐含的Format-Table格式,因为对象的属性不超过4个)。

  • 顺便说一句:Format-List本质上提供了您在Write-Host调用循环中尝试的显示格式;如果您真的需要后一种方法,请注意,正如Walter Mitty的回答中所指出的,您需要将属性访问表达式(如$_.name)包含在$(...)中,以便在可扩展(双引号)PowerShell字符串("...")中进行扩展-请参阅此回答以系统地概述PowerShell可扩展字符串的语法(字符串插值)

注意:

  • 这种方法非常方便:

    • 允许您省略引用,除非需要,即仅当字段值恰好包含,本身时。

      • 在本身包含,的字段值周围使用"..."(-引用)('...',即-引用在CSV数据中不具有语法意义,并且任何'字符都会逐字保留)。

        • 如果这样的字段另外包含CCD_ 17字符。,以""的形式逃离它们
    • 允许您使用附带的空白来实现更可读的格式,如上所示。

  • 您还可以在输入中使用除,(例如|)之外的分隔符,并通过-Delimiter参数将其传递给ConvertFrom-Csv

  • 注意:CSV数据通常是非类型化的,这意味着ConvertFrom-Csv(以及Import-Csv)创建的对象的属性都是字符串([string]-类型化)


可选阅读:自定义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的输出馈送到格式化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

相关内容

  • 没有找到相关文章

最新更新