Iterate through XML nodes / objects and pass to variables in Powershell - powershell

I'd be wanting to iterate through a set of XML and then pass those to variables which can be printed.
Here is an example of the data:
<applications>
<size>75</size>
<application>
<name>Applications 1</name>
<path>/Applications/Utilities/Application 1</path>
<version>10.14</version>
</application>
<application>
<name>Application 2</name>
<path>/Applications/Utilities/Application 2</path>
<version>6.3.9</version>
</application>
</applications
I've looked at using ForEach-Object when trying to output it but to no avail.
[string]$applicationProperties = $API.applications.application| ForEach-Object {
$_.name
$_.path
$_.version
}
This works but puts them all on one line, I'd like them so they print on individual lines but I couldn't prefix the $_ variable. I'm new to POSH as you can tell.
e.g. so I'd like to have name/path/version data saved to variables
[string]$applicationProperties = $API.applications.application | ForEach-Object {
[string]$name_var = $_.name
[string]$path_var = $_.path
[string]$version_var = $_.variable
}
This gives me one "application", but not all the possible objects. Also mentions that even when I'm putting down $name_var it's not accessing that variable? Do I need to do something to access that variable?
Any advice would be appreciated.

When you assign the output from ForEach-Object to [string]$applicationProperties, you're forcing PowerShell to convert all the strings into a single string because of the cast to [string].
What you'll want to do is create a new object for each application node that you're iterating over:
$appInformation = $API.applications.application | ForEach-Object {
# Create a new custom objects with the values from the XML nodes
[pscustomobject]#{
Name = $_.name
Path = $_.path
Version = $_.version
}
}
Now $appInformation will contain an array of objects each with a Name, Path and Version property. You can then further use and/or manipulate these objects in your scripts rather than just having a bunch of strings:
$appInformation |ForEach-Object {
Write-Host "Here is the version of application '$($_.Name)': $($_.Version)"
}
If you want to see them printed in the console with each property value on a separate line just pipe the array to Format-List:
$appInformation |Format-List

Related

Powershell - randomize same string in huge file using all random strings from array

I am looking for a way to randomize a specific string in a huge file by using predefined strings from array, without having to write temporary file on disk.
There is a file which contains the same string, e.g. "ABC123456789" at many places:
<Id>ABC123456789</Id><tag1>some data</tag1><Id>ABC123456789</Id><Id>ABC123456789</Id><tag2>some data</tag2><Id>ABC123456789</Id><tag1>some data</tag1><tag3>some data</tag3><Id>ABC123456789</Id><Id>ABC123456789</Id>
I am trying to randomize that "ABC123456789" string using array, or list of defined strings, e.g. "#('foo','bar','baz','foo-1','bar-1')". Each ABC123456789 should be replaced by randomly picked string from the array/list.
I have ended up with following solution, which is working "fine". But it definitely is not the right approach, as it do many savings on disk - one for each replaced string and therefore is very slow:
$inputFile = Get-Content 'c:\temp\randomize.xml' -raw
$checkString = Get-Content -Path 'c:\temp\randomize.xml' -Raw | Select-String -Pattern '<Id>ABC123456789'
[regex]$pattern = "<Id>ABC123456789"
while($checkString -ne $null) {
$pattern.replace($inputFile, "<Id>$(Get-Random -InputObject #('foo','bar','baz','foo-1','bar-1'))", 1) | Set-Content 'c:\temp\randomize.xml' -NoNewline
$inputFile = Get-Content 'c:\temp\randomize.xml' -raw
$checkString = Get-Content -Path 'c:\temp\randomize.xml' -Raw | Select-String -Pattern '<Id>ABC123456789'
}
Write-Host All finished
The output is randomized, e.g.:
<Id>foo
<Id>bar
<Id>foo
<Id>foo-1
However, I would like to achieve this kind of output without having to write file to disk in each step. For thousands of the string occurrences it takes a lot of time. Any idea how to do it?
=========================
Edit 2023-02-16
I tried the solution from zett42 and it works fine with simple XML structure. In my case there is some complication which was not important in my text processing approach.
Root and some other elements names in the structure of processed XML file contain colon and there must be some special setting for "-XPath" for this situation. Or, maybe the solution is outside of Powershell scope.
<?xml version='1.0' encoding='UTF-8'?>
<C23A:SC777a xmlns="urn:C23A:xsd:$SC777a" xmlns:C23A="urn:C23A:xsd:$SC777a" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:C23A:xsd:$SC777a SC777a.xsd">
<C23A:FIToDDD xmlns="urn:iso:std:iso:20022:tech:xsd:pacs.008.001.02">
<CxAAA>
<DxBBB>
<ABC>
<Id>ZZZZZZ999999</Id>
</ABC>
</DxBBB>
<CxxCCC>
<ABC>
<Id>ABC123456789</Id>
</ABC>
</CxxCCC>
</CxAAA>
<CxAAA>
<DxBBB>
<ABC>
<Id>ZZZZZZ999999</Id>
</ABC>
</DxBBB>
<CxxCCC>
<ABC>
<Id>ABC123456789</Id>
</ABC>
</CxxCCC>
</CxAAA>
</C23A:FIToDDD>
<C23A:PmtRtr xmlns="urn:iso:std:iso:20022:tech:xsd:pacs.004.001.02">
<GrpHdr>
<TtREEE Abc="XV">123.45</TtREEE>
<SttlmInf>
<STTm>ABCA</STTm>
<CLss>
<PRta>SIII</PRta>
</CLss>
</SttlmInf>
</GrpHdr>
<TxInf>
<OrgnlTxRef>
<DxBBB>
<ABC>
<Id>YYYYYY888888</Id>
</ABC>
</DxBBB>
<CxxCCC>
<ABC>
<Id>ABC123456789</Id>
</ABC>
</CxxCCC>
</OrgnlTxRef>
</TxInf>
</C23A:PmtRtr>
</C23A:SC777a>
As commented, it is not recommended to process XML like a text file. This is a brittle approach that depends too much on the formatting of the XML. Instead, use a proper XML parser to load the XML and then process its elements in an object-oriented way.
# Use XmlDocument (alias [xml]) to load the XML
$xml = [xml]::new(); $xml.Load(( Convert-Path -LiteralPath input.xml ))
# Define the ID replacements
$searchString = 'ABC123456789'
$replacements = 'foo','bar','baz','foo-1','bar-1'
# Process the text of all ID elements that match the search string, regardless how deeply nested they are.
$xml | Select-Xml -XPath '//Id/text()' | ForEach-Object Node |
Where-Object Value -eq $searchString | ForEach-Object {
# Replace the text of the current element by a randomly choosen string
$_.Value = Get-Random $replacements
}
# Save the modified document to a file
$xml.Save( (New-Item output.xml -Force).Fullname )
$xml | Select-Xml -XPath '//Id/text()' selects the text nodes of all Id elements, regardless how deeply nested they are in the XML DOM, using the versatile Select-Xml command. The XML nodes are selected by specifying an XPath expression.
Regarding your edit, when you have to deal with XML namespaces, use the parameter -Namespace to specify a namespace prefix to use in the XPath expression for the given namespace URI. In this example I've simply choosen a as the namespace prefix:
$xml | Select-Xml -XPath '//a:Id/text()' -Namespace #{a = 'urn:iso:std:iso:20022:tech:xsd:pacs.008.001.02'}
ForEach-Object Node selects the Node property from each result of Select-Xml. This simplifies the following code.
Where-Object Value -eq $searchString selects the text nodes that match the search string.
Within ForEach-Object, the variable $_ stands for the current text node. Assign to its Value property to change the text.
The Convert-Path and New-Item calls make it possible to use a relative PowerShell path (PSPath) with the .NET XmlDocument class. In general .NET APIs don't know anything about the current directory of PowerShell, so we have to convert the paths before passing to .NET API.

Powershell Array Output to html

Apologies if this is irrelevant but I'm new to powershell and I've been scratching my head on this for a few days on and off now. I'm trying to write a script that will output two columns of data to a html document. I've achieved most of it by learning through forums and testing different combinations.
The problem is although it gives me the result I need within powershell itself; it will not properly display the second column results for Net Log Level.
So the script looks at some folders and pulls the * value which is always three digits (this is the Site array). It then looks within each of these folders to the Output folder and grabs a Net Log Level node from a file inside there. The script is correctly listing the Sites but is only showing the last value for Net Log Level which is 2. You can see this in the screenshot above. I need this to take every value for each Site and display as appropriate. The image of the incorrect result is below. I need the result to be 1,4,2,2,2. Any help would be greatly appreciated!
function getSite {
Get-ChildItem C:\Scripts\ServiceInstalls\*\Output\'Config.exe.config' | foreach {
$Site = $_.fullname.substring(27, 3)
[xml]$xmlRead = Get-Content $_
$NetLogLevel = $xmlRead.SelectSingleNode("//add[#key='Net Log Level']")
$NetLogLevel = $NetLogLevel.value
New-Object -TypeName System.Collections.ArrayList
$List1 += #([System.Collections.ArrayList]#($Site))
New-Object -TypeName System.Collections.ArrayList
$List2 += #([System.Collections.ArrayList]#($NetLogLevel))
}
$Results = #()
ForEach($Site in $List1){
$Results += [pscustomobject]#{
"Site ID" = $Site
"Net Log Level" = $NetLogLevel
}
}
$Results | ConvertTo-HTML -Property 'Site','Net Log Level' | Set-Content Output.html
Invoke-Item "Output.html"
}
getSite
Restructure your code as follows:
Get-ChildItem 'C:\Scripts\ServiceInstalls\*\Output\Config.exe.config' |
ForEach-Object {
$site = $_.fullname.substring(27, 3)
[xml]$xmlRead = Get-Content -Raw $_.FullName
$netLogLevel = $xmlRead.SelectSingleNode("//add[#key='Net Log Level']").InnerText
# Construct *and output* a custom object for the file at hand.
[pscustomobject] #{
'Site ID' = $site
'Net Log Level' = $netLogLevel
}
} | # Pipe the stream of custom objects directly to ConvertTo-Html
ConvertTo-Html | # No need to specify -Property if you want to use all properties.
Set-Content Output.html
As for what you tried:
New-Object -TypeName System.Collections.ArrayList in effect does nothing: it creates an array-list instance but doesn't save it in a variable, causing it to be enumerated to the pipeline, and since there is nothing to enumerate, nothing happens.
There is no point in wrapping a [System.Collections.ArrayList] instance in #(...): its elements are enumerated and then collected in a regular [object[]] array - just use #(...) by itself.
Using += to "grow" an array is quite inefficient, because a new array must be allocated behind the scenes every time; often there is no need to explicitly create an array - e.g. if you can simply stream objects to another command via the pipeline, as shown above, or you can let PowerShell itself implicitly create an array for you by assigning the result of a pipeline or foreach loop as a whole to a variable - see this answer.
Also note that when you use +=, the result is invariably a regular [object[] array, even if the RHS is a different collection type such as ArrayList.
There are still cases where iteratively creating an array-like collection is necessary, but you then need to use the .Add() method of such a collection type in order to grow the collection efficiently - see this answer.
Instead of populating two separate lists, simply create the resulting objects in the first loop:
function getSite {
$Results = Get-ChildItem C:\Scripts\ServiceInstalls\*\Output\'Config.exe.config' | ForEach-Object {
$Site = $_.fullname.substring(27, 3)
[xml]$xmlRead = Get-Content $_
$NetLogLevel = $xmlRead.SelectSingleNode("//add[#key='Net Log Level']")
$NetLogLevel = $NetLogLevel.value
[pscustomobject]#{
"Site ID" = $Site
"Net Log Level" = $NetLogLevel
}
}
$Results | ConvertTo-HTML -Property 'Site', 'Net Log Level' | Set-Content Output.html
Invoke-Item "Output.html"
}
getSite

Trying to combine replace and new-item in powershell

I have a task to make changes to some Config files in a Directory, and the files that need changing are 7, all starting with "Monitoring_Tran_xx".
Within these files there are certain values (TransactionID="01" AgreedResponseTime="500" SearchProfileID="216") that need changing but not present across all 7, and will need to check if they are present before replace or creating them.
Also I am trying to insert a new parameter(using new-item) if a certain parameter is equal to a certain value e.g. if TemplateType = "4152" then create a new parameter next to it "DirectoryPoolID = '3' "
I will appreciate your help on this please.
Thanks
Example of Config file below:
<?xml *version="1.0"* ?>
<Monitor>
<Configuration OperatingMode="Transaction" LogFileName="C:\Program Files\*******s\MonitoringOMTR\MonitorLog_01.log" WriteFileInterval="120" ConnectionKey="**********" />
<TransactionMonitoringConfig TransactionID="01" AgreedResponseTime="500" SearchProfileID="216" />
<ShowMessages>
<Message Name="MOISearchStart" />
<Message Name="MOOSearchFound" />
<Message Name="MOOSearchEnd" />
<Message Name="MOOAlert" />
</ShowMessages>
<PerformanceCounters TransactionCount="191" TransactionBreaches="0" />
</Monitor>
Powershell script that i tried but it didn't quite result well:
(Get-Content -Path '***FS2072\UserData$\aroyet01\Desktop\HostPS.txt') |
if ([bool]((Get-Content -Path "***FS2072\UserData$\aroyet01\Desktop\HostPS.txt") -like '*DirectoryPool*', '*SearchProfile*')) {
write-host "Found it"
}
else {
write-host "Did not find it"
}
ForEach-Object {$_ -replace 'TransactionCount="191"', 'TransactionCount="196"'} |
Set-Content -Path '***FS2072\UserData$\aroyet01\Desktop\HostPS.txt'
Get-Content -Path '***FS2072\UserData$\aroyet01\Desktop\HostPS.txt'
I have a task to make changes to some Config files in a Directory, and the files that need changing are 7, all starting with "Monitoring_Tran_xx".
Stop treating your XML files as raw text - they can be parsed and treated with much better precision :)
First up, we need to parse the document(s) as XML, the easiest way probably being:
$xmlDocument = [xml](Get-Content C:\Path\To\)
Since you have multiple documents with a common naming pattern, we might use Get-ChildItem to discover the files on disk and loop over them to get the paths:
Get-ChildItem -Path C:\path\to\config\files\Monitoring_Tran_*.ps1 |ForEach-Object {
# read each document from disk with `Get-Content`, convert to [xml]
$xmlDocument = [xml]($_ |Get-Content)
}
Next, we need to be able to navigate our XML document. PowerShell has native syntax bindings for this:
$tmConfig = $xmlDocument.Monitor.TransactionMonitoringConfig
or, you can use XPath expressions to navigate the document as well:
$tmConfig = $xmlDocument.SelectSingleNode("/Monitor/TransactionMonitoringConfig")
Within these files there are certain values (TransactionID="01" AgreedResponseTime="500" SearchProfileID="216") that need changing but not present across all 7, and will need to check if they are present before replace or creating them
To check whether a named attribute exists on a node we can use the HasAttribute() method:
if($tmConfig.HasAttribute('TransactionID')){
# attribute exists, let's update it!
$tmConfig.SetAttribute('TransactionID', 123) # replace 123 with whatever ID you need
}
Also I am trying to insert a new parameter(using new-item) if a certain parameter is equal to a certain value e.g. if TemplateType = "4152" then create a new parameter next to it "DirectoryPoolID = '3' "
In the case where you want to add a new attribute to a node only if another attribute has a specific value, you can use GetAttribute() and SetAttribute():
if($xmlNode.GetAttribute('TemplateType') -eq "4152"){
$xmlNode.SetAttribute("DirectoryPoolID", 3)
}
Whenever you're done, save the document back to file:
Get-ChildItem -Path C:\path\to\config\files\Monitoring_Tran_*.ps1 |ForEach-Object {
# read each document from disk with `Get-Content`, convert to [xml]
$xmlDocument = [xml]($_ |Get-Content)
<# write all the code to inspect and update the attributes here #>
# write document back to disk
$xmlDocument.Save($_.FullName)
}
Looking at your code snippet i can only assist by giving you the advice to start with basic Powershell syntax documentation.
help about_If
help about_Pipelines
To give you an idea... Your if is placed in a pipeline. Your ForEach-Object is not placed in a pipeline. Both of these don't work the way you posted it. You can start from something like this:
$Content = '***FS2072\UserData$\aroyet01\Desktop\HostPS.txt'
if ($Content -condition 'Yourcondition') {
$Content | ForEach-Object {
$_ -replace 'Regex', 'Replacement'
}
} else {
'Your else here'
}
For editing the xml file you can take a look at this post as hinted by vonPryz in this question today.

Use a for loop for list and set element to environment variable then get value

I'm not familiar with powershell at all and only need it for a few commands. I was hoping that someone could assist me. I have a json file named optionsConfig.json that reads like so,
{
"test1": ["options_size", "options_connection", "options_object"],
"test2":["options_customArgs", "options_noUDP", "options_noName"]
}
In my powershell file, I have one line so far and this is to get the content of the json file.
$rawData = Get-Content -Raw -Path "optionsConfig.json" | ConvertFrom-Json
I had planned on having an environment variable on my system named test that would have the value of test1 or test2 and from that, I would look at the elements of the list in the associated value in the json file. With these elements from the list, I would assume that they are also environment variables and I would want to get their values. ( a list comprehension in python would be perfect here).I'm a python guy so I am not quite certain how to show this in powershell but say I did env = getEnvironmentVariable(test) and that equals test1. I would then say something like for i in $rawData.env
return getEnvironmentVariable(i)
I pretty much want another list or new json object with the values for the assumed environment variables that we get from the original json object.
Is there a way that I can do this in powershell? It would help a lot if anyone could assist. I apologize if I wasn't clear with anything. Thanks
(edit) I see that $rawData.Get-ChildItem Env:test does not work. Is there someone to write something like this in order to get the correct list from json file?
Also setting a new variable like so $var1 = Get-ChildItem Env.tool and doing $rawData.$test.value has no effect either.
# Sample values: make environment variable 'test' ($env:test)
# point to property 'test1'
$env:test = 'test1'
# Provide sample values for the environment-variable names listed
# in the 'test1' property.
$env:options_size = 'size1'
$env:options_connection = 'conn1'
$env:options_object = 'obj1'
# Read the JSON file into a custom object.
$configObj = Get-Content -Raw optionsConfig.json | ConvertFrom-Json
# Retrieve the environment variables whose
# names are listed in the $env:test property ('test1' in this example),
# as name-value pairs.
Get-Item -Path env:* -Include $configObj.$env:test
The above yields an array of [System.Collections.DictionaryEntry] instances each representing an environment variable's name and value:
Name Value
---- -----
options_connection conn1
options_object obj1
options_size size1
To convert the above to JSON:
Get-Item -Path env:* -Include $configObj.$env:test |
Select-Object Name, Value | ConvertTo-Json
Note: The seemingly redundant Select-Object call is necessary to strip additional properties that PowerShell adds behind the scenes, which would otherwise show in the resulting JSON.
If you wanted to rename the properties in the resulting JSON, you'd need Select-Object anyway, using calculated properties.
This yields:
[
{
"Name": "options_connection",
"Value": "conn1"
},
{
"Name": "options_object",
"Value": "obj1"
},
{
"Name": "options_size",
"Value": "size1"
}
]
I don't see a problem, just set and use $ENV:test
$rawdata = Get-Content .\optionsconfig.json | convertfrom-json
$Env:Test="test1"
$rawdata.$ENV:Test
$Env:Test="test2"
$rawdata.$ENV:Test
Sample output
options_size
options_connection
options_object
options_customArgs
options_noUDP
options_noName

How to export mixed type objects to csv file?

I'm writing a script that returns a list of objects that most of them have different number of properties. When I print it in the console everything is OK, but when I try to export to CSV only those fields that are common in all objects get exported. All others are cropped.
I use the Add-Member cmdlet to add more properties but not all of the objects get the same number of properties.
For example I try to export 2 objects where one is like this:
FirstObject:{
Network0:nic1,
Network1:nic2,
Network2:nic3,
Network3:nic4,
Name:VirtualMachine1
}
SecondObject:{
Network0:nic1,
Network1:nic2,
Name:VirtualMachine1
}
The Network property is added with Add-Member cmdlet. The problem I get when exporting to CSV is that Network2 and Network3 properties from the first object are cropped and all the columns I get is Network0, Network1, and Name.
What I would like to know is there a way to export all the properties and if one of the objects doesn't have the property, just assign $null?
P.S. I have a solution just to add those fields manually with a loop, but I was wondering maybe there is a cleaner solution built in PowerShell which I missed?
Update:
I found out that it provides the same columns to the file that are in the first object. All other fields are ignored. So to be more exact I need all columns in all objects. If some objects do not have the field, then it should be printed empty.
Just a few lines of code that add missing properties.
#sample setup
$one = [pscustomobject]#{
Network0='nic1'
Network1='nic2'
Network2='nic3'
Network3='nic4'
Name='VirtualMachine1'
}
$two = [pscustomobject]#{
Network0='nic1'
Network1='nic2'
Name='VirtualMachine2'
}
$three = [pscustomobject]#{
Network0='nic1'
Name='VirtualMachine3'
}
$export = ($one,$two,$three)
#build list of all properties available to $allProps
$export | % -begin { $allProps = #() } -process { $allProps = [linq.enumerable]::union([object[]](($_.psobject.Properties).Name), [object[]]$allProps) }
#convert each object in $export to new custom object having all properties and put to $result
$export | % -begin { $result = #() } -process { $__ = $_; $o = #{ }; $allProps | %{ $o += #{ $_ = $__.$_ } }; $result+=[pscustomobject]$o }
#export $result to csv
$result | Export-Csv $env:TEMP\export.csv -NoTypeInformation -Force
Get-Content $env:TEMP\export.csv
"Network1", "Network3", "Network0", "Name", "Network2"
"nic2", "nic4", "nic1", "VirtualMachine1", "nic3"
"nic2",, "nic1", "VirtualMachine2",
,, "nic1", "VirtualMachine3",
>> Script Ended
Things to note:
[linq.enumerable]::union is used to easy build list of all available properties across all objects.
($_.psobject.Properties).Name is shortcut to #($_.psobject.Properties | select -ExpandProperty Name), it contains array of property names
$__ = $_ is a trick for nested loop
$o += #{ $_ = $__.$_ } adds key-value pairs to output object; trick here is that even if property $_='nic4' does not exists in $__ export object, powershell does not throw error and returns $null. Note that this will not work when Set-StrictMode is set -Version 2 or later.