【问题标题】:Access PSObject property indirectly with variable使用变量间接访问 PSObject 属性
【发布时间】:2021-06-20 14:45:28
【问题描述】:

假设我有类似 JSON:

  {
    "a" : {
        "b" : 1,
        "c" : 2
        }
  }

现在ConvertTo-Json 将愉快地从中创建PSObjects。我想访问一个我可以做的项目$json.a.b 并获得 1 - 很好的嵌套属性。

现在,如果我有字符串"a.b",问题是如何使用该字符串访问该结构中的相同项目?似乎应该有一些我缺少的特殊语法,例如用于动态函数调用的&,否则您必须自己使用Get-Member 重复我期望的解释字符串。

【问题讨论】:

    标签: powershell


    【解决方案1】:

    不,有没有特殊语法,但有一个简单的解决方法,使用内置别名iex[1] 对于Invoke-Expression cmdlet:

    $propertyPath = 'a.b'
    
    # Note the ` (backtick) before $json, to prevent premature expansion.
    iex "`$json.$propertyPath" # Same as: $json.a.b
    
    # You can use the same approach for *setting* a property value:
    $newValue = 'foo'
    iex "`$json.$propertyPath = `$newValue" # Same as: $json.a.b = $newValue
    

    警告只有在您完全控制或隐含信任$propertyPath 的值时才这样做
    只有在极少数情况下才真正需要Invoke-Expression,而should generally be avoided,因为它可能存在安全风险。

    请注意,如果目标属性包含特定集合类型的实例,并且您希望按原样保留它(这并不常见)(例如,如果属性值是强类型数组,例如[int[]],或列表类型的实例,例如[System.Collections.Generic.List`1]),请使用以下内容:

    # "," constructs an aux., transient array that is enumerated by
    # Invoke-Expression and therefore returns the original property value as-is.
    iex ", `$json.$propertyPath"
    

    如果没有, 技术,Invoke-Expression 枚举集合值属性的元素,您最终会得到一个常规 PowerShell 数组,即[object[]] 类型 - 通常,但是,这种区别并不重要。

    注意:如果您要通过管道直接发送, 技术的结果,则集合值属性值将作为单个对象发送而不是像往常一样被枚举。 (相比之下,如果您首先将结果保存在 variable 中,然后通过管道发送 it,则会发生通常的枚举)。虽然您可以强制枚举,只需将Invoke-Expression 调用包含在(...) 中,在这种情况下没有理由使用, 技术,因为枚举总是需要丢失有关正在枚举其元素的集合类型的信息。

    继续阅读打包解决方案。


    注意:

    • 以下打包的解决方案 最初使用Invoke-Expression结合清理指定的属性路径,以防止无意/恶意注入命令.但是,这些解决方案现在使用了不同的方法,即将属性路径拆分为单独的属性名称并迭代深入到对象中,如Gyula Kokas's helpful answer 所示。这不仅消除了消毒的需要,而且比使用Invoke-Expression更快(后者仍然值得考虑一次性使用)。

      • 这种技术的简洁、仅获取、始终枚举版本将是以下函数:

        # Sample call: propByPath $json 'a.b'
        function propByPath { param($obj, $propPath) foreach ($prop in $propPath.Split('.')) { $obj = $obj.$prop }; $obj }
        
      • 下面更详细的解决方案提供了什么:参数验证、通过路径设置属性值的能力,以及 - 对于propByPath 函数 - 防止枚举作为集合的属性值的选项(见下一点)。

    • propByPath 函数提供了一个-NoEnumerate 开关,可以选择性地请求保留属性值的特定集合类型。

    • 相比之下,.PropByPath()方法中省略了此功能,因为没有语法上方便的方法来请求它(方法仅支持位置参数)。一种可能的解决方案是创建第二种方法,例如 .PropByPathNoEnumerate(),它应用上面讨论的 , 技术。

    辅助函数 propByPath:

    function propByPath {
    
      param(
        [Parameter(Mandatory)] $Object,
        [Parameter(Mandatory)] [string] $PropertyPath,
        $Value,               # optional value to SET
        [switch] $NoEnumerate # only applies to GET
      )
    
      Set-StrictMode -Version 1
    
      # Note: Iteratively drilling down into the object turns out to be *faster*
      #       than using Invoke-Expression; it also obviates the need to sanitize
      #       the property-path string.
      
      $props = $PropertyPath.Split('.') # Split the path into an array of property names.
      if ($PSBoundParameters.ContainsKey('Value')) { # SET
        $parentObject = $Object
        if ($props.Count -gt 1) {
          foreach ($prop in $props[0..($props.Count-2)]) { $parentObject = $parentObject.$prop }
        }
        $parentObject.($props[-1]) = $Value
      }
      else { # GET
        $value = $Object
        foreach ($prop in $props) { $value = $value.$prop }
        if ($NoEnumerate) {
          , $value
        } else {
          $value
        }
      }
    
    }
    

    然后您将使用 Invoke-Expression 调用:

    # GET
    propByPath $obj $propertyPath
    
    # GET, with preservation of the property value's specific collection type.
    propByPath $obj $propertyPath -NoEnumerate
    
    
    # SET
    propByPath $obj $propertyPath 'new value'
    

    您甚至可以使用 PowerShell 的 ETS(扩展类型系统).PropByPath() 方法附加到所有 [pscustomobject] 实例PSv3+语法;在 PSv2 中,您必须创建一个 *.types.ps1xml 文件并使用 Update-TypeData -PrependPath 加载它):

    'System.Management.Automation.PSCustomObject',
    'Deserialized.System.Management.Automation.PSCustomObject' |
      Update-TypeData -TypeName { $_ } `
                      -MemberType ScriptMethod -MemberName PropByPath -Value  {                  #`
    
                        param(
                          [Parameter(Mandatory)] [string] $PropertyPath,
                          $Value
                        )
                        Set-StrictMode -Version 1
    
                        
                        $props = $PropertyPath.Split('.') # Split the path into an array of property names.
                        if ($PSBoundParameters.ContainsKey('Value')) { # SET
                            $parentObject = $this
                            if ($props.Count -gt 1) {
                              foreach ($prop in $props[0..($props.Count-2)]) { $parentObject = $parentObject.$prop }
                            }
                            $parentObject.($props[-1]) = $Value
                        }
                        else { # GET
                          # Note: Iteratively drilling down into the object turns out to be *faster*
                          #       than using Invoke-Expression; it also obviates the need to sanitize
                          #       the property-path string.
                          $value = $this
                          foreach ($prop in $PropertyPath.Split('.')) { $value = $value.$prop }
                          $value
                        }
    
                      }
    

    然后您可以致电$obj.PropByPath('a.b')$obj.PropByPath('a.b', 'new value')

    注意:类型 Deserialized.System.Management.Automation.PSCustomObject 是除 System.Management.Automation.PSCustomObject 之外的目标,以便还涵盖 反序列化 自定义对象,这些对象在许多场景中返回,例如就像使用Import-CliXml,从后台作业接收输出,以及使用远程处理。

    .PropByPath() 将在会话剩余的任何 [pscustomobject] 实例上可用(即使是在 Update-TypeData 调用之前创建的实例 [2]);将Update-TypeData 调用放在您的$PROFILE(配置文件)中,以使该方法默认可用。


    [1] 注意:虽然通常建议将别名限制为交互式使用和在脚本中使用完整的cmdlet名称,对我来说,使用iex 是可以接受的,因为它是一个内置 别名,可以实现简洁 解决方案。

    [2] 使用(全部在一行上)$co = New-Object PSCustomObject; Update-TypeData -TypeName System.Management.Automation.PSCustomObject -MemberType ScriptMethod -MemberName GetFoo -Value { 'foo' }; $co.GetFoo() 进行验证,即使 $co 是在调用 Update-TypeData 之前创建的,也会输出 foo

    【讨论】:

    • 你不需要在脚本块中转义换行符,我不确定你在评论后面的勾号是否真的任何事情。
    • @TheIncorrigible1:屏幕外#` 的唯一原因是为了减轻损坏的 SO 语法突出显示。我本可以使用 splatting 来避免这种情况,但我不想通过引入不相关的概念来混淆问题。
    • 不知道 ETS,这对于我正在尝试做的许多事情来说似乎非常有用。本质上,我试图找到一种方法来指向结构的特定点。对于 XPath 并且已经是 API 的一部分的 XML。
    • @cyborg:它不适用于 deserialized 自定义对象的原因是它们具有不同的类型名称:Deserialized.System.Management.Automation.PSCustomObject - 我已将答案更新为也为该类型名称添加一个 ETS 方法。
    • @mklement0 我会尝试一下 - 表格不是问题,因为属性名称结构本质上是解析 JSON 方式的关键。
    【解决方案2】:

    这种解决方法可能对某人有用。

    结果总是更深,直到它碰到正确的对象。

    $json=(Get-Content ./json.json | ConvertFrom-Json)
    
    $result=$json
    $search="a.c"
    $search.split(".")|% {$result=$result.($_) }
    $result
    
    
    

    【讨论】:

    • 做得很好,虽然重复使用有点笨拙。您可以使用foreach 循环来加速它,您可以将其打包为一个函数,如下所示:function propByPath { param($obj, $propPath) foreach ($prop in $propPath.Split('.')) { $obj = $obj.$prop }; $obj }。事实证明,尽管使用了循环,但这种技术比 Invoke-Expression 更快(尽管这只会在大量重复应用该技术时才有意义),并且默认情况下它也是安全的。
    【解决方案3】:

    你可以有 2 个变量。

    $json = '{
        "a" : {
            "b" : 1,
            "c" : 2
            }
      }' | convertfrom-json
    $a,$b = 'a','b'
    $json.$a.$b
    
    1
    

    【讨论】:

    • 我已经从问题中删除了无关的,
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-06-10
    • 1970-01-01
    • 2010-12-17
    • 2021-10-25
    相关资源
    最近更新 更多