Dukeyu
Dukeyu

Reputation: 59

How to get a specific string from xml in powershell

I am facing a problem. I want to get a specific idstring of product node from XML content.

Example:
Run $xml.group.product[0].id it returns 1234/5678
But I just want a $variable="1234" to match id = "1234/5678" to get or return a index of array ofproduct node

How can I do that?

$xml=[xml]@"
<?xml version="1.0" encoding="UTF-8"?>
<group>
  <product description="phone" id="1234/5678">
     <item name="apple" version="50" />
     <item name="banana" version="100" />
  </product>
  <product description="notebook" id="6666/7777">
     <item name="orange" version="150" />
  </product>
</group>
"@

Upvotes: 2

Views: 4868

Answers (4)

mklement0
mklement0

Reputation: 437042

The simplest (but case-sensitive) way to perform literal prefix matching is to use the [string] type's .StartsWith() method.

You can combine it with the .Where() array method (PSv4+) to find the first matching element whose id attribute value starts with a given prefix, along with its (zero-based) index among its siblings.

# The prefix to match.
$variable = '1234'

# Initialize the matched element's index to -1
# which signals that no element was found.
$ndx = -1

# Use the .Where() method to find the (first) matching element.
$el = $xml.group.product.Where({ $_.id.StartsWith($variable); ++$ndx }, 'First')

Note: If you're using Windows PowerShell (as opposed to PowerShell 6+), it is better to use @($xml.group.product).Where(...) to ensure that the .Where() method call works even if there happens to be just one product element (thanks, Steven).

With your sample XML,

  • $ndx now contains 0 (indicating that the first product element matched),
  • and $el contains the matching element,
    <product description="phone" id="1234/5678">...</product>.

An aside re PowerShell's unified handling of scalars and collections:

PowerShell [Core] commendably consistently supports the .Where() and .ForEach() array methods even on scalars.

This unified handling also extends to:

  • scalars having an implied .Count property that reports 1 (e.g., $scalar = 42; $scalar.Count reporting 1), sensibly treating a scalar as a one-element collection.

  • supporting indexing (e.g., $scalar[0] and $scalar[-1] being the same as $scalar).

    • However, scalars (from a PowerShell perspective) that themselves support indexing, preempt this PowerShell-provided indexing, such as in the case of XmlElement instances, whose own indexing provides access to child elements by name. This GitHub issue suggests that the PowerShell-provided positional indexing should still work in such cases, as long as it doesn't conflict with the type-native indexing.

Upvotes: 1

Steven
Steven

Reputation: 7057

Considering the discussion:

$Index = $null
$Products = @($xml.group.product)
$Product = 
:loop For($i = 0; $i -lt $Products.Length; ++$i)
{
    If($Products[$i].id -match "^1234/") {
        $Products[$i]
        $Index = $i
        break loop
    }
}

Write-Host "Last index : $Index"
$Product

Recognizing the -1 trick from mklement0, but still playing around. I did have the same issue I mentioned, if there happens to be only 1 product there's no length property, so the loop won't even run. I had to wrap in @() to get around that. I can't flip to a string because 0 will always be -lt .length. So I thought I was being smart by spitting out $i after the break, but it will incorrectly be 0 too often.

So I had to use $Index. In concept this isn't very different if $Index is $null you didn't get a hit versus if $ndx is -1 you didn't get a hit. It also forced me to introduce $Products as wrapping in @() right in the For statement also showed some issues.

So this is a lot less concise... Just an exercise mklement0's answer is likely to be faster. Forgive me for going on & on...

Upvotes: 0

Steven
Steven

Reputation: 7057

There are a couple of ways I can think to quickly do this.

$xml.group.product | Where-Object{$_.id -match "^1234/"}

This basically means starts with 1234 and should output something like:

description id


phone 1234/5678

If you only want the first part you are going may have to split it off :

($xml.group.product | Where-Object{$_.id -match "^1234/"}).id.Split("/")[0]

It might help to know what the 2 parts of the number mean. if there's an intention on using that first part throughout the program, it might be worth adding it as it's own property.

Oh if you really want to return a Boolean the RegEx match will still work. but it would look more like:

'1234/5678' -match '^1234/'

This would return true, but given you have multiple products the If logic will have to be in a loop somewhere. In the answers so far the loop and the if logic are implied by the Where clause.

Let me know if that helps...

Upvotes: 0

Martin Brandl
Martin Brandl

Reputation: 58931

If I understand it correctly, you want to retrieve a product based on a specific id?

You can do this using the Where-Object cmdlet. Example:

$xml.group.product | Where-Object { $_.id -eq '1234/5678'}

Upvotes: 0

Related Questions