Convert String from XML to Array - powershell

I am setting up a unattended installation of a software with PowerShell.
If I use the following code to set an argument ($Servers) for the installer, the installation is OK:
$Servers = "Server1.$($env:USERDNSDOMAIN)","Server2.$($env:USERDNSDOMAIN)"
But I want to get the settings from an external XML file like this one:
<?xml version="1.0" encoding="utf-8"?>
<Settings>
<Servers>"Server1.$($env:USERDNSDOMAIN)","Server2.$($env:USERDNSDOMAIN)"</Servers>
</Settings>
I use the following code to retrieve the data in Powershell:
[Xml]$xmlConfigFile = Get-Content -Path "C:\Temp\Settings.xml"
$Servers = $ExecutionContext.InvokeCommand.ExpandString($xmlConfigFile.Settings.Servers)
This does NOT work. The install fails.
I think this is because I get a string back from the XML and not an array like in the first example above.
What is the best method to workaround the issue?
I thought about .Split(","), but the quotes are a problem. I also tried ConvertFrom-Csv, but this didn't work either.
EDIT:
The output of the variable $Servers when it's working is:
Server1.domain.local
Server2.domain.local
The ouput of the variable $Server when coming from XML is:
"Server1.domain.local","Server2.domain.local"

Yes, the value you get from the XML data is a single string, not an array, so you need to split it to go from string to array. You could remove the double quotes before splitting the string (PowerShell allows you to daisy-chain method calls that return a value):
$Servers = $Servers.Replace('"', '').Split(',')
However, a much better approach would be to fix your input data by putting the values in separate nodes:
<?xml version="1.0" encoding="utf-8"?>
<Settings>
<Servers>
<Server>Server1.$($env:USERDNSDOMAIN)</Server>
<Server>Server2.$($env:USERDNSDOMAIN)</Server>
</Servers>
</Settings>
so you can process the data like this:
$Servers = $xmlConfigFile.Settings.Servers.Server | ForEach-Object {
$ExecutionContext.InvokeCommand.ExpandString($_)
}
or like this:
$Servers = $xmlConfigFile.SelectNodes('//Server') | ForEach-Object {
$ExecutionContext.InvokeCommand.ExpandString($_.'#text')
}

Related

Powershell reading from a XML

I am attempting to read from an XML file and make an output of the information.
<?xml version="1.0" encoding="utf-8"?>
<ModelList>
<Model name="ThinkCentre M715Q">
<Types>
<Type>10M4</Type>
<Type>10RA</Type>
<Type>10RB</Type>
<Type>10M5</Type>
<Type>10RC</Type>
<Type>10M2</Type>
<Type>10RD</Type>
<Type>10M3</Type>
</Types>
</Model>
</ModelList>
I managed to get the output of the model attribute with the following powershell code.
[xml]$xmlFile = Get-Content -Path C:\Temp\data.xml
$xmlFile.GetType().Attributes
$xmlFile.ModelList.Model | Format-Table
Output information with the current code above:
name Types
---- -----
ThinkCentre M715Q Types
But... As you can see, the Types attribute is just types. I also want to be able to read the nested information inside of the ModelList. I want the output to be more like this:
name Types
---- -----
ThinkCentre M715Q 10M4, 10RA, 10RB, 10M5, 10RC...
I am stuck here. I need guidance to just simply bind the attributes. That Types attribute knows it is associated with the Model of ThinkCentre. Any help is appreciated! Thanks.
Since your XML could contain more models and types, you would need to loop over the nodes.
Also, there is a better way to load the xml than using Get-Content that automatically takes the documents encoding into account:
# load the xml file. This way, you are ensured to get the file encoding correct
$xml = [System.Xml.XmlDocument]::new()
$xml.Load('C:\Temp\data.xml')
foreach ($model in $xml.ModelList.Model) {
[PsCustomObject]#{
Model = $model.Name
Types = $model.Types.Type -join ', '
}
}
Output:
Model Types
----- -----
ThinkCentre M715Q 10M4, 10RA, 10RB, 10M5, 10RC, 10M2, 10RD, 10M3

Passing values to a PowerShell script in a TeamCity metarunner

I've got a metarunner defined like so:
<?xml version="1.0" encoding="UTF-8"?>
<meta-runner name="Fancy Pancy">
[...]
<build-runners>
<runner name="Fancy Pancy" type="jetbrains_powershell">
<parameters>
[...]
<param name="jetbrains_powershell_scriptArguments"><![CDATA-Optional:%SomeOptMetarunnerParam%
-Required:%SomeReqMetarunnerParam%]]></param>
<param name="jetbrains_powershell_script_code"><![CDATA[#Requires -Version 7
[CmdletBinding()]
param (
[string] $Optional,
[string] $Required
)
[...]
My problem is the following: if the user keeps the optional parameter empty, the PowerShell execution thinks that "-Required:%SomeReqMetarunnerParam" is the argument value for the "Optional" parameter and finally fails because a value for the "Required" parameter is missing. If the user does give a value for the optional parameter, everything works as intended.
I've worked around it by redefining the parameters as follows (note the quotes):
<param name="jetbrains_powershell_scriptArguments"><![CDATA-Optional:'%SomeOptMetarunnerParam%'
-Required:'%SomeReqMetarunnerParam%']]></param>
This works, however, the quotes become part of the value. So I have to trim them at the beginning of the PowerShell script which is clearly bad style and not clean code at all.
Is there a trick on how to overcome this problem?
I found a way to overcome my problem. I removed the scriptArguments XML, the CmdletBinding and the param declaration completely. Instead I simply access the variables at the beginning of the jetbrains_powershell_script_code XML:
$Optional = "%Optional%"
$Required = "%Required%"
The required check is done by TeamCity's validationMode property further up the XML.

Powershell not displaying rss feed

I am trying to parse an RSS feed using powershell, however when i use Invoke-RestMethod this is the only output that i get:
xml RDF
--- ---
version="1.0" encoding="UTF-8" RDF
I have had this issue with multiple rss feeds where nothing gets displayed, how can i get it working so that the RSS feed is actually displayed when I use Invoke-RestMethod?
$url = 'http://www.aero-news.net/news/rssfeed.xml'
Invoke-RestMethod -Uri $url
Thanks.
The output you're getting implies that Invoke-RestMethod worked as intended: it returned a [xml] (System.Xml.XmlDocument) instance that is an XML DOM (document object model) of the XML text returned from the site.
Unfortunately, the default display formatting for [xml] instances, as shown in your question, isn't very helpful[1], but all the information is there, which you can simply verify by accessing the .OuterXml property:
# Get the XML DOM object parsed from the website's XML text output.
$xml = Invoke-RestMethod 'http://www.aero-news.net/news/rssfeed.xml'
# Output its text representation.
$xml.OuterXml
The above prints a string such as:
<?xml version="1.0" encoding="iso-8859-1"?>
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns="http://purl.org/rss/1.0/">
<channel rdf:about="http://www.aero-news.net">
<title>Aero-News Network</title>
<description>Daily, Real-Time news and information critical to aviation and aerospace personnel the world over. Aero-News provides daily newsletter summaries, RSS feeds, and numerous personal and professional syndication and news distribution options to insure that aviators, the world over, are kept up to date on information of critical concern.</description>
<link>http://www.aero-news.net</link>
...
You can therefore work with the [xml] (XmlDocument) instance as usual:
Using PowerShell's convenient adaptation of the XML DOM via property-based dot-notation; e.g., $xml.RDF.channel.about returns string http://www.aero-news.net, which is the text content of the about attribute of the element whose path (from the document root) is /RDF/channel, irrespective of namespaces[2]; see this answer for more information.
Using the [xml] type's native properties and methods, such as the XPath-based .SelectNodes() method for extracting information from the XML document; however, this is less convenient if XML namespaces are involved (such as in your case), because they require explicit management; see this answer for more information.
If you want to pretty-print the XML text:
The [xml] (System.Xml.XmlDocument) type doesn't have built-in support for pretty-printing its text content.
While it's possibly to use a System.Xml.XmlWriter instance, doing so is verbose and cumbersome; however, it does offer you control over the specifics of the pretty-printing format.
A pragmatic, much simpler solution is to use the System.Xml.Linq.XDocument type instead (for which PowerShell does not provide dot notation, unfortunately), whose .ToString() method pretty-prints by default, using indentation with two space characters, as the following example demonstrates:
# Create a sample XmlDocument instance, as would be returned
# from an Invoke-RestMethod call (from a site returning XML text):
$xml = [xml] ('<?xml version="1.0"?><catalog><book id="bk101"><title>De Profundis</title></book></catalog>')
# Cast to [System.Xml.Linq.XDocument] via .OuterXml; the former's
# .ToString() method then pretty-prints automatically.
([System.Xml.Linq.XDocument] $xml.OuterXml).ToString()
The above yields the following string:
<catalog>
<book id="bk101">
<title>De Profundis</title>
</book>
</catalog>
Note that the XML declaration is not included, but you can easily prepend it yourself:
$xd = [System.Xml.Linq.XDocument] $xml.OuterXml
$xd.Declaration.ToString() + "`n" + $xd.ToString()
The following Format-Xml convenience function wraps this functionality:
function Format-Xml {
param(
[Parameter(ValueFromPipeline)]
[xml] $Xml
)
process {
$xd = [System.Xml.Linq.XDocument] $Xml.OuterXml
if ($xd.Declaration) {
$str = $xd.ToString()
$newline = ("`n", "`r`n")[$str.Contains("`r`n")]
$xd.Declaration.ToString() + $newline + $str
}
else {
$xd.ToString()
}
}
}
Now you can use the following to pretty-print the original $xml variable (obtained via Invoke-RestMethod):
# Outputs a pretty-printed version of the document's XML text.
$xml | Format-Xml
[1] What is being shown is the content of the document's XML declaration as property .xml, and the name of the document (root) element as a property named for itself. Printing any given element in the document works as follows: if the element has neither attributes nor child elements, its text content (text child node), if any, is printed. Otherwise, its attributes and their values are printed, followed by properties named for the child elements, each represented by their name as the property value too, if they have attributes and/or child elements themselves, otherwise by their text content, if any.
[2] An example command that processes all feed items whose title contains a given word and transforms them into custom objects.
$userTerm = 'Quote'
$xml.RDF.Item | ? Title -like "*$userTerm*" | % {
[PSCustomObject]#{
Source = "aero"
Title = $_.Title
Link = $_.Link
Description = $_.description
}
}
Invoke-WebRequest parses the RSS feed data as xml. You can just access the data like an ordinary object. A demo:
$feed = [xml]( invoke-webrequest "https://arminreiter.com/feed/" )
$feed.rss.channel.item | Select-Object #{Name="Id";Expression={$_."post-id".InnerText}}, title, link, pubDate

HTMLDocumentClass and getElementsByClassName not working

Last year I had powershell (v3) script that parsed HTML of one festival page (and generate XML for my Windows Phone app).
I also was asking a question about it here and it worked like a charm.
But when I run the script this year, it is not working. To be specific - the method getElemntsByClassName is not returning anything. I tried that method also on other web pages with no luck.
Here is my code from last year, that is not working now:
$tmpFile_bandInfo = "C:\band.txt"
Write-Host "Stahuji kapelu $($kap.Nazev) ..." -NoNewline
Invoke-WebRequest http://www.colours.cz/ucinkujici/the-asteroids-galaxy-tour/ -OutFile $tmpFile_bandInfo
$content = gc $tmpFile_bandInfo -Encoding utf8 -raw
$ParsedHtml = New-Object -com "HTMLFILE"
$ParsedHtml.IHTMLDocument2_write($content)
$ParsedHtml.Close()
$bodyK = $ParsedHtml.body
$bodyK.getElementsByClassName("body four column page") # this returns NULL
$page = $page.item(0)
$aside = $page.getElementsByTagName("aside").item(0)
$img = $aside.getElementsByTagName("img").item(0)
$imgPath = $img.src
this is code I used to workaround this:
$sec = $bodyK.getElementsByTagName("section") | ? ClassName -eq "body four column page"
# but now I have no innerHTML, only the lonely tag SECTION
# so I am walking through siblings
$img = $sec.nextSibling.nextSibling.nextSibling.getElementsByTagName("img").item(0)
$imgPath = $img.src
This works, but this seems silly solution to me.
Anyone knows what I am doing wrong?
I actually solved this problem by abandoning Invoke-WebRequest cmdlet and by adopting HtmlAgilityPack.
I transformed my former sequential HTML parsing into few XPath queries (everything stayed in powershell script). This solution is much more elegant and HtmlAgilityPack is real badass ;) It is really honour to work with project like this!
The issue is not a bug but rather that the return where you're seeing NULL is because it's actually a reference to a proxy HTMLFile COM call to the DOM model.
You can force this to operate and return the underlying strings by boxing it into an array #() as such:
#($mybody.getElementsByClassName("body four column page")).textContent
If you do a Select-Object on it, that also automatically happens and it will unravel it via COM and return it as a string
$mybody.getElementsByClassName("body four column page") | Select-Object -Property TextContent

Export-csv formatting. Powershell

Let's start off by saying that I'm quite new to Powershell and not the greatest one working with it's code and scripts but trying to learn. And now to the problem!
I'm working on a script that fetches information from computers in the network. I've got some code that works quite well for my purposes. But I'm having some problem when it comes to some information, mostly information that contains multiple objects, like service.
#This application will pull information from a list of devices. The devices list is
sent with C:\Users\test.txt and output is pushed to a file C:\Users\devices.csv
function kopiera
{
param
(
[Parameter(ValueFromPipeline=$true)]
[string[]]$Computer=$env:computername
)
Process
{
$computer | ForEach-Object{
$service=Get-WmiObject -Class Win32_Service -Computername $_
$prop= [ordered]#{
Service =$service.caption
}
New-Object PSCustomObject -Property $prop
}
}
}
Get-Content C:\Users\test.txt | kopiera | Export-csv c:\Users\devices.csv
When I export the csv file it looks like this:
TYPE System.Management.Automation.PSCustomObject
"Service"
"System.Object[]"
So it doesn't fetch the service.caption (Because there are too many?).
But If I replace the export-csv C:\Users\device.csv with out-file C:\Users\devices.txt it looks like this instead:
{Adobe Acrobat Update Service, Adobe Flash Player Update Service, Andrea ADI Filters Service, Application Experience...}
So it's starting to look better, but it doesn't get them all (Still because there are too many services?). What I'd like to do with this export/out-file is to get the information to appear vertically instead of horizontal.
(Wanted result)
Adobe Acrobat Service
Adobe Flash Player Update Service
and so on..
instead of:
(Actual result)
Adobe Acrobat Update Service, Adobe Flash Player Update Service, and so on...
Is there a way to make this possible, been trying for a while and can't wrap my brain around this.
Any help is appreciated!
CSV is not usually a good choice for exporting objects that contain multi-valued or complex properties. The object properties are going to be converted to a single string value. The only way to store an array of values is to convert it to a delimited string.
$prop= [ordered]#{
Service =($service.caption) -join ';'
}
will create a semi-colon delimited string, and you'll have to deal with splitting it back out in whatever appication is using the csv later.
If you want to save and re-import the original object with the property as an array, you can switch to Export-CLIXML instead of Export-CSV.