search and replace text in a file without foreach-object (get-content output?)

I was just wondering if there’s a way to search and replace text in a file without using foreach-object. I don’t understand what get-content returns. On the one hand, it’s an array of strings. But on the other hand, it’s an object array with 7 properties and no string. So you should be able to pass an altered string and PSPath down the pipe to set-content. How do you make your own object like that, with an object stream and a string stream?

PS C:\Users\me> get-content input | select *

PSPath       : C:\Users\me\input
PSParentPath : C:\Users\me
PSChildName  : input
PSDrive      : C
PSProvider   : Microsoft.PowerShell.Core\FileSystem
ReadCount    : 1
Length       : 3

I’m thinking something like:

(get-content input) | select @{n = '_'; e = {$_ -replace 111,222}}, PSPath | set-content # converts the hashtable to a string

or

(get-content input) | set-content -value { $_ -replace 111,222}  # takes the script block as a literal string

But neither of them work…

If I just do this, I lose the path:

(get-content input) -replace 111,222 | set-content

set-content : The input object cannot be bound because it did not contain the information required to bind all mandatory parameters:  Path

But this works:

(get-content input) | set-content

I think switch can do it.

switch -regex -file 'C:\temp\replace.txt'{
'111' {222}
default {$_}
}

I feel like [string] is actually an object, but there’s a custom view that makes it look like a string, similar to get-date or [datetime]. But which .format.ps1xml defines the custom view for string?

Did a search, But didn’t find anything for System.String

Get-ChildItem -Path C:\Windows\System32\WindowsPowerShell\v1.0 -Recurse -Filter *.format.ps1xml |
Foreach-Object -Process {[xml]$r = Get-Content -Path $_.FullName;$r.Configuration.ViewDefinitions.View.ViewSelectedBy.TypeName} |
Out-GridView

Yeah, Get-Content is completely cheating.

You can add NoteProperties to any object in PowerShell, even simple things like strings and numbers, thanks to its Extended Type System. In brief, every object you handle in PowerShell is always wrapped in a PSObject. These objects have a looser definition of what a property or a method is than, for example, C#, where the object’s class definition is unchangeable once the code is compiled. As a result, you can just create and add properties to any object in PowerShell via their hidden .PSObject member.

For example:

https://gist.github.com/vexx32/b5a4c16d22978c663333241cb8ca792e

Those are still strings you get from Get-Content… it just tacks on additional properties so that if you need it, you’ve a reference back to the file it came from.

In order to do this in a pipeline, you would need to use the Add-Member -PassThru command to add the property on and pass the resulting object through the pipeline.

So with the get-content output, how do I alter the string without losing the hidden properties like PSPath that it comes with? Only the Length property is left after using the replace operator.

PS C:\Users\js> (Get-Content input.txt) | Select-Object *

PSPath       : C:\Users\js\input.txt
PSParentPath : C:\Users\js
PSChildName  : input
PSDrive      : C
PSProvider   : Microsoft.PowerShell.Core\FileSystem
ReadCount    : 1
Length       : 3


PS C:\Users\js> (Get-Content input.txt) -replace 111, 222 | Select-Object *

Length
------
     3

You would need an extra step to keep track of the PSPath and any other properties you care about, really:

Get-Content -Path input.txt | ForEach-Object {
    $_ -replace 111,222 | Add-Member -Name PSPath -Value $_.PSPath -MemberType NoteProperty -PassThru
} | Set-Content

If needed, you could chain addition Add-Member calls to add additional properties, or you could refer to Get-Help Add-Member -Online and check out the examples where they add multiple properties in the one Add-Member call. :slight_smile:

 

I was hoping there was some hidden property that contained the string, like:

(get-content input) | foreach { $_.line = $_.line -replace 111,222 } | set-content input

I still don’t understand what property the string is “stored” in. How can something be both a string and a list of properties at the same time?

Within the foreach-object, $_ is the string value. It doesn’t matter to things that work with only strings that it has other secret properties, they won’t see them.

As I said… everything in PS is not simple data, it’s always a complex object. When you do $var = “string” what you’re actually doing behind the scenes is creating a string, then creating a PSObject to hold that string, and then storing that in your variable. So adding random properties to the PSObject that holds the string is done in about the same way PS adds properties to any PSObject, they’re basically a dictionary collection of property names and values and nothing more. PowerShell just behaves like those dictionary entries are real properties.

As for “which property” the string value is stored in, I guess you could say it’s technically stored in $object.PSObject.BaseObject, but at the end of the day accessing the object itself without targeting a specific property will generally cause the bare string to be used anyway.

Bit confusing, but that’s how they chose to do it. Pretty intuitive on the surface, till you hit weirdness like that, which doesn’t make a lot of sense coming from other programming languages.

Hmm, no error but only the last line ends up in the file.

(Get-Content input) | ForEach-Object {
    $_ -replace 111,222 |
    Add-Member -Name PSPath -Value $_.PSPath -MemberType NoteProperty -PassThru |
    Add-Member -Name PSParentPath -Value $_.PSParentPath -MemberType NoteProperty -PassThru |
    Add-Member -Name PSChildName -Value $_.PSChildName -MemberType NoteProperty -PassThru |
    Add-Member -Name PSDrive -Value $_.PSDrive -MemberType NoteProperty -PassThru |
    Add-Member -Name PSProvider -Value $_.PSProvider -MemberType NoteProperty -PassThru |
    Add-Member -Name ReadCount -Value $_.ReadCount -MemberType NoteProperty -PassThru
} | Set-Content 

Ah, yeah. Since it’s taking the file path over the pipeline, it’s not able to be sure it needs to keep the file open and write all the data. So it’s overwriting the file for each line.

Might be best to use Add-Content for this kind of usage.

The thing is, this works. So there’s some kind of magic I’m missing. EDKT: Oh not true, it is one line in the result!

(get-content input) | set-content

Same one line result:

(get-content input) | set-content -path {$_.pspath}

Yeah, it has to be provided the path via the -Path parameter directly to be able to properly utilise the pipeline for writing a whole file, really. :slight_smile:

You could opt to use Get-Content -Raw to retrieve the file as a single complete string and then do the replace on that before writing the file if you really want to use Set-Content over Add-Content for whatever reason. :slight_smile:

You’re right about -raw. I had just tried that. Except it adds a blank line at the bottom.

PS /Users/js> (get-content -raw input ) | set-content                
PS /Users/js> (get-content -raw input ) | set-content
PS /Users/js> (get-content -raw input ) | set-content
PS /Users/js> get-content input
111
111
111



PS /Users/js> 

As for…

You're right about -raw. I had just tried that. Except it adds a blank line at the bottom.

… you can just trim that.

Delete all blank lines from a text file using PowerShell https://www.madwithpowershell.com/2013/08/delete-all-blank-lines-from-text-file.html