3

In my script I am attempting to create PowerShell objects from a text file. Here is a sample of a text file.

junk data at the 

beginning of

the file


Item: Car
In Stock: YES
Make: Ford
Model: Taurus
Color: Blue
Miles: 0
Item: Car
Miles: 0
Item: Truck
Item: SUV
Item: Car
In Stock: YES
Make: Honda
Model: Civic
Color: Red
Miles: 0

junk
data at the 

end

Sometimes these files are generated with junk data at the beginning and end. I try to account for this with my Select-String statement.

Here is my script:

[CmdletBinding()]
param(
    [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
    [string]$File
)

class Item {
    [string] $Item = ''
    [string] $Found = 'NO'
    [string] ${In Stock}  = 'N/A'
    [string] $Make = 'N/A'
    [string] $Model = 'N/A'
    [string] $Color = 'N/A'
    [string] $Miles = 'N/A'
  }


[string[]]$Keep = @("Item:", "In Stock:", "Make:", "Model:", "Color:", "Miles:")
  
(Get-Content $File | Select-String -Pattern ($Keep -join "|") |
    Out-String) -split '(?m)(?=^Item:)' -ne '' -replace '(?m)^(.+?):', '$1=' |
        ConvertFrom-StringData | ForEach-Object {
            $Obj = [Item] $_
            if ($Obj.'In Stock' -eq 'Yes') {
                $Obj.Found = 'YES'
            }
            Write-Output $Obj 
        }
  

The script works a lot better than it did. Many thanks to another user that helped me get this far.

When I run the script, I get an EXTRA object at the top with all the default values defined in the class. Why is that?

Here is the output:

Item:
Found: NO
In Stock: N/A
Make: N/A
Model: N/A
Color: N/A
Miles: N/A

Item: Car
Found: YES
In Stock: YES
Make: Ford
Model: Taurus
Color: Blue
Miles: 0

Item: Car
Found: NO
In Stock: N/A
Make: N/A
Model: N/A
Color: N/A
Miles: 0

Item: Truck
Found: NO
In Stock: N/A
Make: N/A
Model: N/A
Color: N/A
Miles: N/A

Item: SUV
Found: NO
In Stock: N/A
Make: N/A
Model: N/A
Color: N/A
Miles: N/A

Item: Car
Found: YES
In Stock: YES
Make: Honda
Model: Civic
Color: Red
Miles: 0
okkv1747vm
  • 91
  • 1
  • 7

1 Answers1

2

The input to your ForEach-Object call in which the [Item] cast happens (see explanation below) starts with an empty line, which results in an extra output object (with all the class' default values), because it is in effect like [Item] @{} (because something like "`n" | ConvertFrom-StringData outputs an empty hashtable), which in turn is like simply calling the (parameterless default) constructor, [Item]::new().

You can avoids this empty line as follows (note the use of .Line and avoiding a Get-Content call in favor of passing the file path directly to Select-String):

(
  (Select-String -Pattern ($Keep -join "|") -LiteralPath $File).Line | Out-String
) -split '(?m)(?=^Item:)' -ne '' -replace '(?m)^(.+?):', '$1=' |
  ConvertFrom-StringData | ForEach-Object {
    $Obj = [Item] $_
    if ($Obj.'In Stock' -eq 'Yes') {
      $Obj.Found = 'YES'
    }
    Write-Output $Obj
  }

The reason for the empty line (which appears both at the start and and at the end) is that you're directly sending Select-String's output objects to Out-String, which results in the same for-display string formatting that you would see in the console. Accessing the .Line property on Select-String's output objects avoids that, as it outputs just strings (rather than the Microsoft.PowerShell.Commands.MatchInfo instances emitted by default), which adds no formatting.

On a side note: Out-String unexpectedly always appends a trailing newline to its output string; this problematic behavior is discussed in GitHub issue #14444.


Explanation of the [Item] casting technique:

On instantiation of class [Item], the properties have the default values as specified in the class definition, unless you override them later.

By casting a hashtable - as output by ConvertFrom-StringData - to [Item], those of its entries whose keys match the names of properties of the class implicitly set those properties; all others keep their defaults (and no entries with keys that do not match property names are allowed).

This construction by casting from a hashtable is explained in detail in this answer.


A simplified example with a hashtable literal @{ ... }:

class Item {
  $A = 'A_default'
  $B = 'B_default'
  $C = 'C_default'
}

# Construct an [Item] instance and assign to its .B property
[Item] @{ B = 'B_NEW' }

The above yields:

A         B     C
-         -     -
A_default B_NEW C_default
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • @AbrahamZinala A sample text file is posted above. The actual files have possible sensitive info at the top and bottom. As I am looking into into this more, it seems my script is picking up on a blank line or something that is causing the loop to output an "extra" object. Which is strange because I thought that I am grabbing only the lines I need with the Select-String statement. – okkv1747vm Jun 27 '21 at 01:12