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

eoigrqb6  于 2022-12-06  发布在  Shell
关注(0)|答案(3)|浏览(125)

我试图避免使用powershell中常用的极其冗长的哈希Map和数组。为什么?因为我有100行,而且当我只需要一个CSV类型的数组时,必须将每一行都 Package 在@(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)
}

💖 👍

ztyzrc3y

ztyzrc3y1#

您可以使用内存中,即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然后提供所需的输出(如果没有Format-List,您将得到隐式的Format-Table,因为对象的属性不超过4个)。

  • 顺便说一句:Format-List * 本质上 * 提供了您在Write-Host调用循环中尝试过的for-display格式;如果您确实需要后一种方法,请注意,正如Walter Mitty's answer中所指出的,您需要将属性访问 * 表达式 *(如$_.name)括在$(...)中,以便在可扩展(双引号)PowerShell字符串("...")中进行扩展-有关PowerShell可扩展字符串(字符串插值)语法的系统概述,请参见this answer

注意事项:

  • 此方法方便
  • 它**允许您 * 省略引号 ,除非需要*,即仅当字段值恰好包含,本身时。
  • 在本身包含,的字段值周围使用**"..."(* 双 *-引号)**('...',即 * 单 *-引号在CSV数据中 * 不 * 具有语法意义,并且任何'字符 * 逐字 * 保留)。
  • 如果这样的字段还包含"字符,则将其转义为""
  • 允许您使用附带的 * 空格 使格式*更易读,如上所示。
  • 也可以在输入中使用 * ,以外的分隔符 *(例如|,并通过**-Delimiter**参数将其传递给ConvertFrom-Csv
  • 注意:CSV数据通常是 untyped,这意味着ConvertFrom-Csv(以及Import-Csv)创建的对象的属性都是 * string *([string]-typed)
可选阅读:一种自定义CSV表示法,允许创建 typed 属性:

便利函数**ConvertFrom-CsvTyped(下面的源代码)通过启用*自定义标头表示法 (支持在标头行中的每个列名前面加上 type literal),克服了ConvertFrom-Csv总是只创建 string 类型属性的限制;例如[int] ID(有关PowerShell类型文字的系统概述,请参见this answer,它可以引用任何.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

Output -请注意,如果您在脚本中使用它或将其作为Invoke-Expression的输入(仅用于测试),您将获得与上面相同的对象和for-display输出:

@(
  [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.
    $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 { $_ }
      }
    }
  }
}
hrirmatl

hrirmatl2#

这里有一些技巧可以帮助你使用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
lmvvr0a8

lmvvr0a83#

若要在PowerShell指令码中行程内嵌CSV,您可以使用ConvertFrom-Csv Cmdlet将CSV数据转换成具有可在指令码中使用之属性的对象。以下是如何使用此Cmdlet行程指令码中CSV数据的范例:

$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'
)

# Convert the CSV data into objects with properties
$apps = $mycsv | ConvertFrom-Csv -Header $header

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

此脚本使用ConvertFrom-Csv cmdlet将内联CSV数据转换为具有与$header变量中的值匹配的属性的对象。然后使用foreach循环遍历$apps变量中的对象,并打印每个对象的属性值。
注意:在此范例中,ConvertFrom-Csv Cmdlet假设CSV数据的第一列包含信头,这就是呼叫Cmdlet时需要指定-Header参数的原因。如果您的CSV数据没有信头,您可以改用-Property参数指定属性名称。例如:

$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'
)

# Convert the CSV data into objects with properties
$apps = $mycsv | ConvertFrom-Csv -Property @('name', 'id', 'type', 'loc')

此脚本使用ConvertFrom-Csv cmdlet将内联CSV数据转换为属性与-Property参数中指定的值匹配的对象。然后,它使用foreach循环迭代$apps变量中的对象,并打印每个对象的属性值。

相关问题