1

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
Michele
  • 3,617
  • 12
  • 47
  • 81
  • 4
    Also, `{$XML_OmMap_Names[$i]=$errorMap}` is a script block, not a hashtable - use `@{$XML_OmMap_Names[$i]=$errorMap}` – mklement0 Jan 03 '22 at 23:01
  • Also, in addition to what Santiago said. How are you using this function? You are trying to return to something. – dcaz Jan 03 '22 at 23:01
  • 1
    @dcaz - It returns the info to the main code. I use another returned map from a function, and foreach item, I do successive lookups in several maps returned similarly from different filetypes (but those maps have different regex). So, look up first alarm id and it's value...then that value gets looked up in another map, then that value gets looked up in another map. It's being put together in a spreadsheet to show the end-to-end alarms across the system at different times and then what the instructions are for fixing them, for quick lookup by a tech/etc. – Michele Jan 04 '22 at 14:01

2 Answers2

3

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.

By Left or Right hand side, that means which side of the assignment = operator it is.

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
    }   
)

Links

ninMonkey
  • 7,211
  • 8
  • 37
  • 66
  • 1
    `[System.Collections]` isn't `$null` - because no such type exists, a statement-terminating error is triggered. – mklement0 Jan 04 '22 at 03:09
  • 1
    While `@(...)` is indeed not an array _constructor_, it is an array _guarantor_, if you will. As such, a type constraint of `[object[]]` is superfluous if the RHS of an assignment is a `@(...)` expresssion. – mklement0 Jan 04 '22 at 03:10
  • While it makes sense to avoid `[System.Collections.ArrayList]`, `[System.Collections.Generic[object]]::new()` should be used _instead_ (replace `object` with a specific type, as needed). (Your "1]" point makes it sound like the latter type is to be avoided too). – mklement0 Jan 04 '22 at 03:13
  • @ninMonkey - So when I get an item out of config1 in the list in my main code, what would that look like? For the map alone, it used to look like: $omAlarm = $($omAlarmMap[$alarmIdDef.trim()]). Would it be $omAlarm = $($omAlarmList['Config1']['12-7']) to get it out of the list returned? – Michele Jan 04 '22 at 14:48
  • @ninMonkey - How do I take them out of the list like my comment above? I need to control which I'm pulling out of the list so I can place it in my csv file in the right spot, instead of the Get-ChildItem -depth etc thing you do above. – Michele Jan 04 '22 at 15:01
  • Sorry if I wasn't clear. What I meant was `@()` enumerates the expressions, then returns type `[object[]]` Which can have 0 elements. This doesn't type your variable -- even though it's currently type of an array. ex: `$x = @(); $x += 'a'; $x = 3; $x.GetType()`. It didn't lose the type `[object[]]` when appending a value. But the final assignment caused the array to be type `integer` (not an integer array). Strongly typing would prevent that. – ninMonkey Feb 11 '22 at 01:03
  • Just edited my answer. Thx for being willing to undo the down-vote, but I'm baffled by its rationale: my answer doesn't mention type-constraining at all (it isn't necessary to answer the question). My original point with respect to _your_ answer was that it seemed to suggest that _both_ type-constraining and `@(...)` are _always_ necessary, but that is only true if you want to _type-constrain_ - and if you do the latter with `[object[]]`, you generally do _not_ need the `@(...)` Turning-into-a-scalar of an unconstrained var. initialized with `@(...)` can only happen during _later reassignment_ – mklement0 Feb 17 '22 at 15:53
  • And while your answer contains good general information (except for the contradiction of seemingly first listing `[System.Collections.Generic[Type]]::new()` as to _avoid_; I presume what you actually meant to say is to use `[System.Collections.Generic[Type]]::new()` _instead_ of `[System.Collections.ArrayList]::new()`, but the wording needs to change), the top part that tries to address the OP's actual problem is misinterpreting his intent: despite using `.Add({$XML_OmMap_Names[$i]=$errorMap})` in the question, the intent was never to add a _script block_ (`{ ... }`) to the collection. – mklement0 Feb 17 '22 at 16:10
2

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

# Initialize an ordered hashtable.
$OMs = [ordered] @{ } # This will later store 3 nested hashtables.

# 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 mistakenly passed a single argument as a script block ({ ... }).)

Alternatively, index syntax - $OMS[$XML_OmMap_Names[$i]] = $errorMap - could be used to add the entries; note that this method quietly replaces any preexisting value with the same key, whereas the .Add() method would throw an exception.

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'
mklement0
  • 382,024
  • 64
  • 607
  • 775