Home > Enterprise >  You cannot call a method on a null-valued expression when adding to array of System.Collections
You cannot call a method on a null-valued expression when adding to array of System.Collections

Time:01-05

I have a Function (powershell 5.1) that is using regular expressions to get info out of 3 separate files/strings using the same regex. I want to process all 3 in the same Function and return it as an array of System.Collections(maybe Hashtable ...not sure if that's needed). But I'm getting the above error. It makes sense for me to do it this way because otherwise I will have separate functions doing the exact same thing, with the same regex. So I'm trying to re-use functionality to do the same regex on 3 different strings/files.

I looked up the error and it says something isn't assigned. not assigned I'm not seeing what isn't assigned in my case.

Function ProcessOmAlarmXml{
[cmdletbinding()]
  Param () 
  Process
  {
   $OMs = [System.Collections]::new() #this will store the 3 hashtables from the regex

   $pathViewBase = 'C:\EndToEnd_view\' 
   $XML_OmMap_Dirs = @('\OmAlarmMap.xml')   #will add the other 2 later
   $XML_OmMap_Names = @('Config1','Config2','Config3')
   $i = 0
   
   #get each one from array
   foreach($omFile in $XML_OmMap_Dirs)
   {
      $pathToXml = Join-Path -Path $pathViewBase -ChildPath $omFile
      if(Test-Path $pathToXml)
      {
          #get file contents to parse as string
          $fileContent = Get-MethodContents -codePath $pathToXml -methodNameToReturn "<Configuration>" -followingMethodName "</Configuration>"

          #get the Mapping from omMap xml file
          $errorMap = @{}
          # create array to collect keys that are grouped together
          $keys = @()

          #regex works perfectly
          switch -Regex ($fileContent -split '\r?\n') {  #test regex with https://regex101.com/
            'Name=\"(\w -\w )' { #12-5704
                # add relevant key to key collection
                $keys = $Matches[1] } #only match once
                '^[\s] <Value Name="MsgTag[">] (\w [.\w]*)<\/Value' { # gets the word after MsgTag, before closing tag      (\w )<\/Value                MsgTag[">] (\w )<\/Value    ([?:<!\s\S]?"MsgTag[">] )(\w [.\w]*)<\/Value #fails if line commented out..still captures line
                # we've reached the relevant error, set it for all relevant keys
                foreach($key in $keys){
                    #Write-Host "om key: $key"
                    $errorMap[$key] = $Matches[1]
                    Write-Host "om key: $key ... value: $($errorMap[$key])"
                }
            }
            'break' {
                # reset/clear key collection
                $keys = @()
            }    
          }#switch
          
          #I'm trying to add each $errorMap with the name in the array $XML_OmMap_Names so I can pull them out and use them later meaningfully
          [void]$OMs.Add({$XML_OmMap_Names[$i]=$errorMap})  #gives error message
          Write-Host $OMs.Count
          Write-Host $OMs -ForegroundColor Cyan
          $i  
       }#test-Path
       else
       {
          Write-Host "No such path $pathToXml"
       }
       
      } #foreach
   return $errorMap #will return $OMs later instead
  } #end Process
}# End of Function

I'm also trying to store my objects in the array like this seems to be using: arrays.

Note that I'm trying to provide a minimal amount of info here, and want to show enough of what the function does so it makes sense of why I need to store the hashtables in an array to return without re-writing complex code which would still be pretty complex. It would be great if someone has more info on how to store the hashtables in the array to return all 3 in the array.

More info on the error:

You cannot call a method on a null-valued expression.
At C:\Users\2021\temp endToEnd folder while network down\EndToEndParser.ps1:329 char:11
            [void]$OMs.Add({$XML_OmMap_Names[$i]=$errorMap})  #[System. ...
            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      CategoryInfo          : InvalidOperation: (:) [], RuntimeException
      FullyQualifiedErrorId : InvokeMethodOnNull

CodePudding user response:

Answer

You want

using namespace system.collections.generic
$OMs = [list[object]]::new() #this will store the 3 hashtables from the regex

[object[]]$XML_OmMap_Names = @('Config1','Config2','Config3')

$OMs.Add({$XML_OmMap_Names[$i]=$errorMap})
$OMs.Add( $somethingElse )

[System.Collections] isn't a valid type. That's why this is null.

$OMs = [System.Collections]::new()
$null -eq $OMs # returns True

Note, @() is not an array constructor

 @('\OmAlarmMap.xml')

To declare those as lists, and not accidentally become scalars, what you want is to give it a type.

$XML_OmMap_Names = @('Config1','Config2','Config3')

# becomes
[object[]]$XML_OmMap_Names = @('Config1','Config2','Config3')

Types on Left Hand Side will declare types, they are strongly typed. Types on the Right Hand Side coerce values, but doesn't declare them to be any type.

using namespace system.collections.generic
$OMs = [list[object]]::new() #this will store the 3 hashtables from the regex

$OMs.Add({$XML_OmMap_Names[$i]=$errorMap})
$OMs.Add( $somethingElse )

Creating arrays: What to Avoid

What you want to avoid is

1] [System.Collections.ArrayList] and [System.Collections.Generic[Type]]::new()

This is explained in why generics are better? : microsoft docs

2] addition like $s = @(), the $s = stuff, because that allocates new memory every addition. It gets exponentially worse as the size increases.

What you should do

1] The docs say for objects that have mixed types, use [list[object]]

using namespace System.Collections.Generic

$items = [list[object]]::new()
$items.Add( '1' )
$items.Add( (Get-Item . ))

2] for objects of one specific type use [list[type]]

$nums = [list[Int64]]::new()
$nums.add( '1' )

$nums[0] -is [Int64]  # True
$nums[0] -is [string] # False

3] Implicit arrays are okay if you're not = ing

# now the type is an array, regardless if nothing, or single value return
[object[]]$ArrayOfAnything = FunctionMightReturnNothing # 0-tomany items


[object[]]$AlwaysArray = Get-ChildItem . | Select -first 1 
$AlwaysArray.GetType() # type is object[] arrray

$sometimesArray = Get-ChildItem . | select -first 1 
$sometimesArray.GetType() # type is a scalar of type [filesysteminfo]

Implicit arrays are fine.

# often in powershell it's more natural to emit output to the pipeline, instead of manually adding arrays. 

$results = @(
    Get-ChildItem c:\ -depth 3 | Sort-Object LastWriteTime | Select-Object -top 3    
    if($IncludeProfile) { 
         Get-ChildItem -file "$Env:UserProfile" -depth 3 | Sort-Object Length | Select-Object -top 3
    }   
)

CodePudding user response:

ninMonkey's answer provides good general pointers regarding lists and arrays.

However, it sounds like what you're looking for is a (potentially ordered) nested hashtable:

$OMs = [ordered] @{ } #this will store the 3 hashtables from the regex

# The (top-level) keys
$XML_OmMap_Names = @('Config1','Config2','Config3')

$i = 0
foreach($someFile in 'foo', 'bar', 'baz') {
  # Construct the nested hashtable
  $errorMap = [ordered] @{ nested = $someFile }
  # Add it to the top-level hashtable.
  $OMS.Add($XML_OmMap_Names[$i], $errorMap)
    $i
}

Note how the key, $XML_OmMap_Names[$i], and the value, $errorMap are passed as separate arguments to the .Add() method.
(By contrast, in your attempt, [void]$OMs.Add({$XML_OmMap_Names[$i]=$errorMap}), you passed a single argument as a script block ({ ... }).)

Now you can get entries by name; e.g.:

$OMs.Config1  # Same as: $OMs['Config1']

Note: Since [ordered] was used, you can also use positional indices:

$OMs[0] # Same as : $OMs.Config1

Direct access to the nested hashtable's entries is also possible:

$OMs.Config1.nested  # Same as: $OMs['Config1']['nested'] -> 'foo'
  •  Tags:  
  • Related