Powershell ForEach-Object column variables - json

Kind of have a strange problem. I have a large JSON file that needs to be processed. Based on another question I need to stream the file since it will otherwise gets me problems because of memory: JSON Powershell memory issue
What I have is this:
get-content -Path largefile.json | ForEach-Object {
$row = $_ = $_.TrimStart('[').TrimEnd(']')
if ($_) { $_ | Out-String | ConvertFrom-Json }
New-Item -Path $($Row.Id).txt
Set-Content -Path $($Row.Id).txt -Value ($row.Body)
}
I can easily do $row to publish the last processed row in the Largefile.json. I want to create a file with the name of the Id in the row that is currently processed and add the body column to the file. But when I want to show a specific column using $row.Id, unfortunately this shows up empty.
The structure of the Largefile.json is as followed:
[{"Id":"1","ParentId":"parent","Name":"filename","OwnerId":"owner","CreatedDate":"date","Body":"data1"}
{"Id":"2","ParentId":"parent","Name":"filename","OwnerId":"owner","CreatedDate":"date","Body":"data2"}
{"Id":"3","ParentId":"parent","Name":"filename","OwnerId":"owner","CreatedDate":"date","Body":"data3"}
{"Id":"4","ParentId":"parent","Name":"filename","OwnerId":"owner","CreatedDate":"date","Body":"data4"}
{"Id":"5","ParentId":"parent","Name":"filename","OwnerId":"owner","CreatedDate":"date","Body":"data5"}
]
The end result should be that I have 5 files:
1.txt - Value inside the file should be: data1
2.txt - Value inside the file should be: data2
3.txt - Value inside the file should be: data3
4.txt - Value inside the file should be: data4
5.txt - Value inside the file should be: data5
I use Powershell 7.1.3
Is there any way that I can use $row.Id and $row.ParentId just like a regular ForEach would do?
thanks for your help.

It seems to me that this is what you're looking for:
Get-Content largefile.json | ForEach-Object {
$row = $_.TrimStart('[').TrimEnd(']') | ConvertFrom-Json
if ($null -ne $row) {
Set-Content -Path ($row.Id) -Value ($row.Body)
}
}

I am still not sure what you expect as an outcome.
But I think you want to do this:
#'
[{"Id":"1","ParentId":"parent1","Name":"1.txt","OwnerId":"owner","CreatedDate":"date","Body":"Data1"}
{"Id":"2","ParentId":"parent2","Name":"2.txt","OwnerId":"owner","CreatedDate":"date","Body":"Data2"}
{"Id":"3","ParentId":"parent3","Name":"3.txt","OwnerId":"owner","CreatedDate":"date","Body":"Data3"}
{"Id":"4","ParentId":"parent4","Name":"4.txt","OwnerId":"owner","CreatedDate":"date","Body":"Data4"}
{"Id":"5","ParentId":"parent5","Name":"5.txt","OwnerId":"owner","CreatedDate":"date","Body":"Data5"}
]
'# | Set-Content .\largefile.json
Get-Content .\largefile.json | ForEach-Object {
$_ = $_.TrimStart('[').TrimEnd(']')
If ($_) {
$Row = ConvertFrom-Json $_
Set-Content -Path ".\$($Row.Name)" -Value $Row.Body
}
}

The question has many errors. Assuming the json has the missing commas in, I would do it this way, if I understand the question. This should work with the new updates to the question. I also have a more unusual solution involving streaming json with jq here: Iterate though huge JSON in powershell Json streaming support may be added later: ConvertFrom-JSON high memory consumption #7698
[{"Id":"ID","ParentId":"parent","Name":"filename","OwnerId":"owner","CreatedDate":"date","Body":"*******"},
{"Id":"ID","ParentId":"parent","Name":"filename","OwnerId":"owner","CreatedDate":"date","Body":"*******"},
{"Id":"ID","ParentId":"parent","Name":"filename","OwnerId":"owner","CreatedDate":"date","Body":"*******"},
{"Id":"ID","ParentId":"parent","Name":"filename","OwnerId":"owner","CreatedDate":"date","Body":"*******"},
{"Id":"ID","ParentId":"parent","Name":"filename","OwnerId":"owner","CreatedDate":"date","Body":"*******"}
]
get-content -Path largefile.json | ForEach-Object {
$_ = $_.TrimStart('[').TrimEnd(']').TrimEnd(',')
if ($_) {
$row = $_ | ConvertFrom-Json
Set-Content -Path ($Row.Id + '.txt') -Value $row.Body
}
}
get-content ID.txt
*******

As others already explained, your json example is invalid.
However, since this is a huge file to process, you can use switch for this.
switch -Regex -File D:\Test\largefile.json {
'"Id":"(\d+)".*"Body":"(\w+)"' {
Set-Content -Path ('D:\Test\{0}.txt' -f $matches[1]) -Value $matches[2]
}
}
Results using your example would be 5 files called 1.txt .. 5.txt, each having a single line data1 .. data5

Related

Powershell - Reading multiple json files in a directory and output the data

I'm having to whip up a process that will read multiple json files created by another process.
I have code that can read a single file, but we're needing to process these results in bulk.
Here's my current code:
$json = Get-ChildItem $filePath -recurse | Where-Object { $_.LastWriteTime -gt [DateTime] $filesNewerThan } | ConvertFrom-Json
$json.delegates | foreach-Object {
foreach ($File in $_.files)
{
[PSCustomObject]#{
LastName = $_.lastName
ZipCode = $File.zipCode
BirthDate = $File.birthdate
Address = $File.Address}
}
}
Right now I'm getting an error about an "invalid JSON primitive" which what I'm guessing is an issue where I don't have "Get-Content" specified in my code.
Wondering what my issue is with my code.
ConvertFrom-Json currently (as of PowerShell 7.0) doesn't support file-path input, only file-content input (the actual JSON string), which means that you need to involve Get-Content:
$json = Get-ChildItem -File $filePath -Recurse |
Where-Object { $_.LastWriteTime -gt [DateTime] $filesNewerThan } |
ForEach-Object { Get-Content -Raw -LiteralPath $_.FullName | ConvertFrom-Json }

Find and Replace Nested JSON Values with Powershell

I have an appsettings.json file that I would like to transform with a PowerShell script in a VSTS release pipeline PowerShell task. (BTW I'm deploying a netstandard 2 Api to IIS). The JSON is structured like the following:
{
"Foo": {
"BaseUrl": "http://foo.url.com",
"UrlKey": "12345"
},
"Bar": {
"BaseUrl": "http://bar.url.com"
},
"Blee": {
"BaseUrl": "http://blee.url.com"
}
}
I want to replace BaseUrl and, if it exists, the UrlKey values in each section which are Foo, Bar and Blee. (Foo:BaseUrl, Foo:UrlKey, Bar:BaseUrl, etc.)
I'm using the following JSON structure to hold the new values:
{
"##{FooUrl}":"$(FooUrl)",
"##{FooUrlKey}":"$(FooUrlKey)",
"##{BarUrl}":"$(BarUrl)",
"##{BleeUrl}":"$(BleeUrl)"
}
So far I have the following script:
# Get file path
$filePath = "C:\mywebsite\appsettings.json"
# Parse JSON object from string
$jsonString = "$(MyReplacementVariablesJson)"
$jsonObject = ConvertFrom-Json $jsonString
# Convert JSON replacement variables object to HashTable
$hashTable = #{}
foreach ($property in $jsonObject.PSObject.Properties) {
$hashTable[$property.Name] = $property.Value
}
# Here's where I need some help
# Perform variable replacements
foreach ($key in $hashTable.Keys) {
$sourceFile = Get-Content $filePath
$sourceFile -replace $key, $hashTable[$key] | Set-Content $filePath
Write-Host 'Replaced key' $key 'with value' $hashTable[$key] 'in' $filePath
}
Why are you defining your replacement values as a JSON string? That's just going to make your life more miserable. If you're defining the values in your script anyway just define them as hashtables right away:
$newUrls = #{
'Foo' = 'http://newfoo.example.com'
'Bar' = 'http://newbaz.example.com'
'Blee' = 'http://newblee.example.com'
}
$newKeys = #{
'Foo' = '67890'
}
Even if you wanted to read them from a file you could make that file a PowerShell script containing those hashtables and dot-source it. Or at least define the values as lists of key=value lines in text files, which can easily be turned into hashtables:
$newUrls = Get-Content 'new_urls.txt' | Out-String | ConvertFrom-StringData
$newKeys = Get-Content 'new_keys.txt' | Out-String | ConvertFrom-StringData
Then iterate over the top-level properties of your input JSON data and replace the nested properties with the new values:
$json = Get-Content $filePath | Out-String | ConvertFrom-Json
foreach ($name in $json.PSObject.Properties) {
$json.$name.BaseUrl = $newUrls[$name]
if ($newKeys.ContainsKey($name)) {
$json.$name.UrlKey = $newKeys[$name]
}
}
$json | ConvertTo-Json | Set-Content $filePath
Note that if your actual JSON data has more than 2 levels of hierarchy you'll need to tell ConvertTo-Json via the parameter -Depth how many levels it's supposed to convert.
Side note: piping the Get-Content output through Out-String is required because ConvertFrom-Json expects JSON input as a single string, and using Out-String makes the code work with all PowerShell versions. If you have PowerShell v3 or newer you can simplify the code a little by replacing Get-Content | Out-String with Get-Content -Raw.
Thank you, Ansgar for your detailed answer, which helped me a great deal. Ultimately, after having no luck iterating over the top-level properties of my input JSON data, I settled on the following code:
$json = (Get-Content -Path $filePath) | ConvertFrom-Json
$json.Foo.BaseUrl = $newUrls["Foo"]
$json.Bar.BaseUrl = $newUrls["Bar"]
$json.Blee.BaseUrl = $newUrls["Blee"]
$json.Foo.Key = $newKeys["Foo"]
$json | ConvertTo-Json | Set-Content $filePath
I hope this can help someone else.
To update values of keys at varying depth in the json/config file, you can pass in the key name using "." between the levels, e.g. AppSettings.Setting.Third to represent:
{
AppSettings = {
Setting = {
Third = "value I want to update"
}
}
}
To set the value for multiple settings, you can do something like this:
$file = "c:\temp\appSettings.json"
# define keys and values in hash table
$settings = #{
"AppSettings.SettingOne" = "1st value"
"AppSettings.SettingTwo" = "2nd value"
"AppSettings.SettingThree" = "3rd value"
"AppSettings.SettingThree.A" = "A under 3rd"
"AppSettings.SettingThree.B" = "B under 3rd"
"AppSettings.SettingThree.B.X" = "Z under B under 3rd"
"AppSettings.SettingThree.B.Y" = "Y under B under 3rd"
}
# read config file
$data = Get-Content $file -Raw | ConvertFrom-Json
# loop through settings
$settings.GetEnumerator() | ForEach-Object {
$key = $_.Key
$value = $_.Value
$command = "`$data.$key = $value"
Write-Verbose $command
# update value of object property
Invoke-Expression -Command $command
}
$data | ConvertTo-Json -Depth 10 | Out-File $file -Encoding "UTF8"

Access Object From JSON File in Powershell

I have a JSON file that I am reading in Powershell. The structure of the file is below.
[
["computer1", ["program1", versionX]],
["computer2", ["program2", versionY]],
["computer3", ["program3", "versionX"],
["program1", "versionZ"]
],
]
What I want in the program is use $env:computername and compare it with the computerX in the JSON file. If found a match, then iterate through and get the values of programName and ProgramVersion.
However, I don't know how to search through the objects and find ALL items under that.
This is what I have so far.
$rawData = Get-Content -Raw -Path "file.json" | ConvertFrom-Json
$computername=$env:computername
$data = $rawData -match $computername
This gives me objects under it. But how do I iterate through and get individual values?
But don't know what I do after that.
To start you need to be using a valid JSON file
{
"computer1": {
"program1": "versionX"
},
"computer2": {
"program2": "versionY"
},
"computer3": {
"program3": "versionX",
"program1": "versionZ"
}
}
Then you can access the PSObject Properties
$rawData = Get-Content -Raw -Path "file.json" | ConvertFrom-Json
$rawData.PsObject.Properties |
Select-Object -ExpandProperty Name |
ForEach-Object { IF ($_ -eq $env:COMPUTERNAME) {
Write-Host "Computer Name : " $_
Write-Host "Value : " $rawData."$_"
}
}
EDIT for Computer, Program, and Version as separate values
psobject.Properties.Name will give all the program names.
psobject.Properties.Name[0] will give the first program name.
psobject.Properties.value[0] will give the first program version value.
You need to increment the value to get second value, you can also use -1 as a shortcut for the last value.
$rawData = Get-Content -Raw -Path "file.json" | ConvertFrom-Json
$rawData.PsObject.Properties |
Select-Object -ExpandProperty Name |
ForEach-Object { IF ($_ -eq $env:COMPUTERNAME) {
$Computer = $_
$Values = $rawData.$_
}
}
$Computer
$Values.psobject.Properties
$Values.psobject.Properties.Name
$Values.psobject.Properties.Name[0]
$Values.psobject.Properties.value[0]
$Values.psobject.Properties.Name[1]
$Values.psobject.Properties.value[1]
You could also use the program name
$Values.program1
$Values.program2
$Values.program3

Replace only first occurrence

I have a PowerShell scripts which replaces
"version" : "xxx"
with
"version" : "myBuildNumber"
Now I encountered that I have multiple of these in my file.
I only want to replace the first occurrence.
I tried already Powershell - Replace first occurrences of String but it does not work with my regex.
Here's my script:
(Get-Content myFile.txt) -replace '(?<pre>"version"[\s]*:[\s]*)(?<V>"[^\"]*")', "`$1`"$Env:BUILD_VERSION`"" | Out-File myFile.txt
Since you are patching a JSON file, regex isn't the way to go. Instead you should parse the JSON, access and change the property you want and write it back:
$filePath = 'your_Path_To_project.json'
$json = (Get-Content $filePath -raw | ConvertFrom-Json)
$json.version = $Env:BUILD_VERSION
$json | ConvertTo-Json -Depth 10 | Set-Content $filePath

Import-Csv TrimEnd Column Header

I need import a CSV and run it through a foreach loop. I want to trim the end on the column header DeviceName to avoid any potential issues. I have tried the following but it is not working as expected.
$Import = Import-CSV $csv
foreach ($i in ($import.DeviceName).TrimEnd())
{do something}
Any help? Thank you!
If you need to change both the header and the content in the column for devicename which has spaces I have come up with this forgiving code.
$csvData = import-csv $csv
$properties = $csvData[0].psobject.Properties.name
$csvHeader = "`"$(($properties | ForEach-Object{$_.Trim()}) -join '","')`""
$deviceHeader = $properties -match "DeviceName"
$csvHeader
$csvHeader | Set-Content $file
$csvData | ForEach-Object{
$_.$deviceHeader = ($_.$deviceHeader).trim()
$_
} | ConvertTo-Csv -NoTypeInformation | Select-Object -Skip 1 | Add-Content $file
What this does is read in the CSV like normal. Parse the property names of the object in the order they appear. We find the one that has DeviceName no matter how many spaces (if there is more that one you could have a problem). Keep that so we can use it to call the correct property of each "row".
Export the new cleaned header to the file. Then we go through each "row" removing all the leading and trailing space from the DeviceName. Once that is done write back the CSV to the original file.
The best solution would be to tell the other team to fix their generation procedure. However, if for some reason that's not an option, I'd recommend pre-processing the file before you import it as a CSV.
$filename = 'C:\path\to\your.csv'
(Get-Content $filename -Raw) -replace '^(.*DeviceName)[ ]*(.*)', '$1$2' |
Set-Content $filename
Reading the file as a single string (-Raw) and anchoring the expression at the beginning of the string (^) ensures that only the column title is replaced.
For large input files you may want to consider a different approach, though, since the above reads the entire file into memory before replacing the first line.
$infile = 'C:\path\to\input.csv'
$outfile = 'C:\path\to\output.csv'
$firstLine = $true
Get-Content $infile | % {
if ($firstLine) {
$_ -replace '(DeviceName)[ ]*', '$1'
$firstLine = $false
} else {
$_
}
} | Set-Content $outfile
Thinking about it some more and taking inspiration from a comment to #Zeek's answer, you could also extract the headers first and then convert the rest of the file.
$infile = 'C:\path\to\input.csv'
$outfile = 'C:\path\to\output.csv'
$header = (Get-Content $infile -First 1) -split '\s*,\s*'
Get-Content $infile |
select -Skip 1 |
ConvertFrom-Csv -Header $header |
Export-Csv $outfile -NoType
Is this all you're trying to do? This will give you a collection of objects imported from your csv file but trim the end of the DeviceName property on each object.
$items = Import-CSV -Path $csv
$items.ForEach({ $_.DeviceName = $_.DeviceName.TrimEnd() })