我正在努力避免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字符。,以
""
的形式逃离它们
- 如果这样的字段另外包含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]
(使用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的输出馈送到格式化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