Access PSObject property indirectly with variable

前端 未结 1 1732
有刺的猬
有刺的猬 2020-12-03 19:57

Say I have JSON like:

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

Now ConvertTo-Json will happily cr

相关标签:
1条回答
  • 2020-12-03 20:45

    No, there is no special syntax.

    While the simplest solution is to use Invoke-Expression, as shown next, Invoke-Expression should generally be avoided; it is safe in this particular scenario, because you fully control the input string, but it is better to form a habit of not using Invoke-Expression, especially given that in most situations there are alternatives that are both more robust and more secure:

    $json = @'
    {
      "a" : {
          "b" : 1,
          "c" : 2,
          }
    }
    '@
    
    $obj = ConvertFrom-Json $json
    
    # The path to the target property.
    $propertyPath = 'a.b'
    
    # NOTE: In general, AVOID Invoke-Expression
    # Construct the expression and pass it to Invoke-Expression.
    # Note the need to `-escape the `$` in `$obj` to prevent premature expansion.
    Invoke-Expression "`$obj.$propertyPath"
    

    The above is the equivalent of executing $obj.a.b. directly and yields 1.


    Alternatively, you could write a simple helper function:

    function propByPath($obj, $propertyPath) {
      foreach ($prop in $propertyPath -split '\.')  { $obj = $obj.$prop }
      $obj # output
    }
    

    Instead of the Invoke-Expression call you would then use:

    propByPath $obj $propertyPath
    

    You could even use PowerShell's ETS (extended type system) to attach a .GetPropByPath() method to all [pscustomobject] instances (PSv3+ syntax; in PSv2 you'd have to create a *.types.ps1xml file and load it with Update-TypeData -PrependPath):

    'System.Management.Automation.PSCustomObject',
    'Deserialized.System.Management.Automation.PSCustomObject' |
      Update-TypeData -TypeName { $_ } `
                      -MemberType ScriptMethod -MemberName GetPropByPath -Value {                  #`
        param($propPath)
        $obj = $this
        foreach ($prop in $propPath -split '\.')  { $obj = $obj.$prop }
        $obj # output
      }
    

    You could then call $obj.GetPropByPath('a.b').

    Note: Type Deserialized.System.Management.Automation.PSCustomObject is targeted in addition to System.Management.Automation.PSCustomObject in order to also cover deserialized custom objects, which are returned in a number of scenarios, such as using Import-CliXml, receiving output from background jobs, and using remoting.

    .GetPropByPath() will be available on any [pscustomobject] instance in the remainder of the session (even on instances created prior to the Update-TypeData call [1]); place the Update-TypeData call in your $PROFILE (profile file) to make the method available by default.


    A more robust solution that supports indexing and preserves array-valued properties as such

    The above solution:

    • doesn't support indices as part of the property path (e.g., 'a.b[2]')
    • unwraps array-valued properties using pipeline logic, which means that a single-element array is unwrapped to its one and only element.

    The following solution fixes these limitations, but note that:

    • Only literal, scalar indices are supported (that is, you can use 'a.b[2]', but not 'a.b[1..2]' or 'a.b[1, 2]', for instance)

    • For properties that are hashtables, specify the (literal) key name without embedded quoting (e.g., 'a.ht[bar]'); note that you won't be able to access numeric hashtable keys in general, and, additionally, you won't be able to access an ordered hashtable's entries by index.

    'System.Management.Automation.PSCustomObject',
    'Deserialized.System.Management.Automation.PSCustomObject' |
      Update-TypeData -TypeName { $_ } `
                      -MemberType ScriptMethod -MemberName GetPropByPath -Value {                  #`
        param($propPath)
        $obj = $this
        foreach ($prop in $propPath -split '\.')  {
          # See if the property spec has an index (e.g., 'foo[3]')
          if ($prop -match '(.+?)\[(.+?)\]$') {
            $obj = $obj.($Matches.1)[$Matches.2]
          } else {
            $obj = $obj.$prop
          }
        }
        # Output: If the value is a collection (array), output it as a
        #         *single* object.
        if ($obj.Count) {
          , $obj
        } else {
          $obj
        }
      }
    

    [1] Verify with (all on one line) $co = New-Object PSCustomObject; Update-TypeData -TypeName System.Management.Automation.PSCustomObject -MemberType ScriptMethod -MemberName GetFoo -Value { 'foo' }; $co.GetFoo(), which outputs foo even though $co was created before Update-TypeData was called.

    0 讨论(0)
提交回复
热议问题