How to pipe results into output array - powershell

After playing around with some powershell script for a while i was wondering if there is a version of this without using c#. It feels like i am missing some information on how to pipe things properly.
$packages = Get-ChildItem "C:\Users\A\Downloads" -Filter "*.nupkg" |
%{ $_.Name }
# Select-String -Pattern "(?<packageId>[^\d]+)\.(?<version>[\w\d\.-]+)(?=.nupkg)" |
# %{ #($_.Matches[0].Groups["packageId"].Value, $_.Matches[0].Groups["version"].Value) }
foreach ($package in $packages){
$match = [System.Text.RegularExpressions.Regex]::Match($package, "(?<packageId>[^\d]+)\.(?<version>[\w\d\.-]+)(?=.nupkg)")
Write-Host "$($match.Groups["packageId"].Value) - $($match.Groups["version"].Value)"
}
Originally i tried to do this with powershell only and thought that with #(1,2,3) you could create an array.
I ended up bypassing the issue by doing the regex with c# instead of powershell, which works, but i am curious how this would have been done with powershell only.
While there are 4 packages, doing just the powershell version produced 8 lines. So accessing my data like $packages[0][0] to get a package id never worked because the 8 lines were strings while i expected 4 arrays to be returned

Terminology note re without using c#: You mean without direct use of .NET APIs. By contrast, C# is just another .NET-based language that can make use of such APIs, just like PowerShell itself.
Note:
The next section answers the following question: How can I avoid direct calls to .NET APIs for my regex-matching code in favor of using PowerShell-native commands (operators, automatic variables)?
See the bottom section for the Select-String solution that was your true objective; the tl;dr is:
# Note the `, `, which ensures that the array is output *as a single object*
%{ , #($_.Matches[0].Groups["packageId"].Value, $_.Matches[0].Groups["version"].Value) }
The PowerShell-native (near-)equivalent of your code is (note tha the assumption is that $package contains the content of the input file):
# Caveat: -match is case-INSENSITIVE; use -cmatch for case-sensitive matching.
if ($package -match '(?<packageId>[^\d]+)\.(?<version>[\w\d\.-]+)(?=.nupkg)') {
"$($Matches['packageId']) - $($Matches['Version'])"
}
-match, the regular-expression matching operator, is the equivalent of [System.Text.RegularExpressions.Regex]::Match() (which you can shorten to [regex]::Match()) in that it only looks for (at most) one match.
Caveat re case-sensitivity: -match (and its rarely used alias -imatch) is case-insensitive by default, as all PowerShell operators are; for case-sensitive matching, use the c-prefixed variant, -cmatch.
By contrast, .NET APIs are case-sensitive by default; you'd have to pass the [System.Text.RegularExpressions.RegexOptions]::IgnoreCase flag to [regex]::Match() for case-insensitive matching (you may use 'IgnoreCase', which PowerShell auto-converts for you).
As of PowerShell 7.2.x, there is no operator that is the equivalent of the related return-ALL-matches .NET API, [regex]::Matches(). See GitHub issue #7867 for a green-lit but yet-to-be-implemented proposal to introduce one, named -matchall.
However, instead of directly returning an object describing what was (or wasn't) matched, -match returns a Boolean, i.e. $true or $false, to indicate whether matching succeeded.
Only if -match returns $true does information about a match become available, namely via the automatic $Matches variable, which is a hashtable reflecting the matching parts of the input string: entry 0 is always the full match, with optional additional entries reflecting what any capture groups ((...)) captured, either by index, if they're anonymous (starting with 1) or, as in your case, for named capture groups ((?<name>...)) by name.
Syntax note: Given that PowerShell allows use of dot notation (property-access syntax) even with hashtables, the above command could have used $Matches.packageId instead of $Matches['packageId'], for instance, which also works with the numeric (index-based) entries, e.g., $Matches.0 instead of $Matches[0]
Caveat: If an array (enumerable) is used as the LHS operand, -match' behavior changes:
$Matches is not populated.
filtering is performed; that is, instead of returning a Boolean indicating whether matching succeeded, the subarray of matching input strings is returned.
Note that the $Matches hashtable only provides the matched strings, not also metadata such as index and length, as found in [regex]::Match()'s return object, which is of type [System.Text.RegularExpressions.Match].
Select-String solution:
$packages |
Select-String '(?<packageId>[^\d]+)\.(?<version>[\w\d\.-]+)(?=.nupkg)' |
ForEach-Object {
"$($_.Matches[0].Groups['packageId'].Value) - $($_.Matches[0].Groups['version'].Value)"
}
Select-String outputs Microsoft.PowerShell.Commands.MatchInfo instances, whose .Matches collection contains one or more [System.Text.RegularExpressions.Match] instances, i.e. instances of the same type as returned by [regex]::Match()
Unless -AllMatches is also passed, .Matches only ever has one entry, hence the use of [0] to target that entry above.
As you can see, working with Select-Object's output objects requires you to ultimately work with the same .NET type as when you call [regex]::Match() directly.
However, no method calls are required, and discovering the properties of the output objects is made easy in PowerShell via the Get-Member cmdlet.
If you want to capture the matches in a jagged array:
$capturedStrings = #(
$packages |
Select-String '(?<packageId>[^\d]+)\.(?<version>[\w\d\.-]+)(?=.nupkg)' |
ForEach-Object {
# Output an array of all capture-group matches,
# *as a single object* (note the `, `)
, $_.Matches[0].Groups.Where({ $_.Name -ne '0' }).Value
}
)
This returns an array of arrays, each element of which is the array of capture-group matches for a given package, so that $capturedStrings[0][0] returns the packageId value for the first package, for instance.
Note:
$_.Matches[0].Groups.Where({ $_.Name -ne '0' }).Value programmatically enumerates all capture-group matches and returns an their .Value property values as an array, using member-access enumeration; note how name '0' must be excluded, as it represents the whole match.
With the capture groups in your specific regex, the above is equivalent to the following, as shown in a commented-out line in your question:
#($_.Matches[0].Groups['packageId'].Value, $_.Matches[0].Groups['version'].Value)
, ..., the unary form of the array-construction operator, is used as a shortcut for outputting the array (symbolized by ... here) as a whole, as a single object. By default, enumeration would occur and the elements would be emitted one by one. , ... is in effect a shortcut to the conceptually clearer Write-Output -NoEnumerate ... - see this answer for an explanation of the technique.
Additionally, #(...), the array subexpression operator is needed in order to ensure that a jagged array (nested array) is returned even in the event that only one array is returned across all $packages.

Related

PowerShell Hashtable - how to select property

I need to get the value of an environment variable from a kubernetes pod. I have my values listed in a hash table.
I call
$hash["service-testurl"].spec.template.spec.containers.env
And it returns a table:
name value
---- -----
ADDR https://test.com
TOKEN 123456789
CERT_PATH public-certs/test
ENVIRONMENT dev
I need to get https://test.com into a variable in my ps1 script, but i'm not sure how to get this value. (consider that for each deployment the url will be different, like abc.com, def.com, ghj.com... so i can't filter by the name test.com)
I was looking for something like $hash["service-testurl"].spec.template.spec.containers.env.name["ADDR"].value
Running $hash["service-testurl"].spec.template.spec.containers.env.PSTypeNames returns
System.Object[]
System.Array
System.Object
To complement your own effective solution:
Even though your display output of $hash["service-testurl"].spec.template.spec.containers.env looks like the representation of a (single) hashtable, the value is actually:
(a) an array, as your diagnostic output with .pstypenames demonstrates,
(b) whose elements are (presumably) [pscustomobject] instances that each have a a .name and a .value property (an easy way to tell is that the display output's column headers are name and value, whereas with hashtables they would be Name and Value).
Leaving aside that the identifier ADDR is a property value rather than a property / key name in your case, you fundamentally cannot use key-based index notation (['ADDR']) on an array - that generally only works on a (single) hashtable (or, more generally, dictionary).[1]
In your case, you need to find the array element whose .name property value is 'ADDR', which then allows you to return its .value property value.
For collections already in memory, the intrinsic .Where() method (as used in your own solution) is a more efficient - and more flexible - alternative to filtering a collection via the Where-Object cmdlet.
It will often not matter in practice, but you can optimize a .Where() call to stop filtering once the first match has been found, if you expect or are only interested in one match:
$hash["service-testurl"].spec.template.spec.containers.env.Where(
{ $_.name -eq 'ADDR' },
'First'
).value
Note that .Where() always returns an array-like collection, even if only a single value is matched - see this answer for details. As such, the .value property access is attempted on that collection, which, however, still works as intended, courtesy of the PowerShell feature known as member-access enumeration.
Note how using (...) around the arguments is now a syntactic necessity.
While with only a single argument - the filter script block ({ ... }) - you can get away with not using (...) - .Where{ $_.name -eq 'ADDR' } as shorthand for .Where({ $_.name -eq 'ADDR' }) - omitting the (...) is problematic for two reasons:
Given that the Where-Object cmdlet can also be referred to as Where (via a built-in alias), the two command forms could be confused, and given that Where-Object allows and is typically used with a space separating the command name from its script-block argument (e.g, 1..3 | Where { $_ -eq 2 }, it is tempting to also try to use a space with the .Where() method, which does not work:
# !! BROKEN, due to space before "{"
(1..3).Where { $_ -eq 2 }
If you add another argument later, you need to remember to use (...)
[1] The fact that key-based index notation does not work with member-access enumeration, i.e. doesn't work on an array of hashtables (only dot notation does, which PowerShell supports for hashtables too) could be considered an inconsistency; e.g. #( #{ foo=1 } ).foo works (dot notation), but #( #{ foo=1 } )['foo'] does not, due to the array wrapper.
However, this inconsistency was declared to be by design - see GitHub issue #17514.
I was able to do it with something similar that #iRon proposed:
$hash["service-testurl"].spec.template.spec.containers.env.where{$_.name -eq 'ADDR'}.value
Thanks!

Having problem with split method using powershell

I have an xml file where i have line some
<!--<__AMAZONSITE id="-123456780" instance ="CATZ00124"__/>-->
and i need the id and instance values from that particular line.
where i need have -123456780 as well as CATZ00124 in 2 different variables.
Below is the sample code which i have tried
$xmlfile = 'D:\Test\sample.xml'
$find_string = '__AMAZONSITE'
$array = #((Get-Content $xmlfile) | select-string $find_string)
Write-Host $array.Length
foreach ($commentedline in $array)
{
Write-Host $commentedline.Line.Split('id=')
}
I am getting below result:
<!--<__AMAZONSITE
"-123456780"
nstance
"CATZ00124"__/>
The preferred way still is to use XML tools for XML files.
As long a line with AMAZONSITE and instance is unique in the file this could do:
## Q:\Test\2019\09\13\SO_57923292.ps1
$xmlfile = 'D:\Test\sample.xml' # '.\sample.xml' #
## see following RegEx live and with explanation on https://regex101.com/r/w34ieh/1
$RE = '(?<=AMAZONSITE id=")(?<id>[\d-]+)" instance ="(?<instance>[^"]+)"'
if((Get-Content $xmlfile -raw) -match $RE){
$AmazonSiteID = $Matches.id
$Instance = $Matches.instance
}
LotPings' answer sensibly recommends using a regular expression with capture groups to extract the substrings of interest from each matching line.
You can incorporate that into your Select-String call for a single-pipeline solution (the assumption is that the XML comments of interest are all on a single line each):
# Define the regex to use with Select-String, which both
# matches the lines of interest and captures the substrings of interest
# ('id' an 'instance' attributes) via capture groups, (...)
$regex = '<!--<__AMAZONSITE id="(.+?)" instance ="(.+?)"__/>-->'
Select-String -LiteralPath $xmlfile -Pattern $regex | ForEach-Object {
# Output a custom object with properties reflecting
# the substrings of interest reported by the capture groups.
[pscustomobject] #{
id = $_.Matches.Groups[1].Value
instance = $_.Matches.Groups[2].Value
}
}
The result is an array of custom objects that each have an .id and .instance property with the values of interest (which is preferable to setting individual variables); in the console, the output would look something like this:
id instance
-- --------
-123456780 CATZ00124
-123456781 CATZ00125
-123456782 CATZ00126
As for what you tried:
Note: I'm discussing your use of .Split(), though for extracting a substring, as is your intent, .Split() is not the best tool, given that it is only the first step toward isolating the substring of interest.
As LotPings notes in a comment, in Windows PowerShell, $commentedline.Line.Split('id=') causes the String.Split() method to split the input string by any of the individual characters in split string 'id=', because the method overload that Windows PowerShell selects takes a char[] value, i.e. an array of characters, which is not your intent.
You could rectify this as follows, by forcing use of the overload that accepts string[] (even though you're only passing one string), which also requires passing an options argument:
$commentedline.Line.Split([string[] 'id=', 'None') # OK, splits by whole string
Note that in PowerShell Core the logic is reversed, because .NET Core introduced a new overload with just [string] (with an optional options argument), which PowerShell Core selects by default. Conversely, this means that if you do want by-any-character splitting in PowerShell Core, you must cast the split string to [char[]].
On a general note, PowerShell has the -split operator, which is regex-based and offers much more flexibility than String.Split() - see this answer.
Applied to your case:
$commentedline.Line -split 'id='
While id= is interpreted a regex by -split, that makes no difference here, given that the string contains no regex metacharacters (characters with special meaning); if you do want to safely split by a literal substring, use [regex]::Escape('...') as the RHS.
Note that -split is case-insensitive by default, as PowerShell generally is; however, you can use the -csplit variant for case-sensitive matching.

What does mean % , $_ and # in Powershell?

What does this mean: $_ and % in Powershell?
1..10 | Foreach {if($_%2){"$_ is odd number"}}
%
In your case, it is the modulus operator. It will return the remainder of dividing the left-hand side value by the right-hand side value.
It defaults as a PowerShell alias for Foreach-Object. You can execute the Get-Alias command to see other potential aliases that may contain special characters like Where-Object's alias ?.
$_
Synonymous with $PSItem
Contains the current object in the pipeline object
In your case, it represents the current object passed into your Foreach-Object script block ({}).
It will commonly show up in the Where-Object {} script block and Select-Object hash tables.
#
A literal # character
Denotes splatting
The syntax is #VariableName. The variable can be an array or hash table. It is commonly used with a hash table or dictionary where the Name property represents a parameter name and the value property is the value for that parameter. Then that variable is splatted into another command. An example is Get-Process #Params.
Used for declaring and initializing arrays via the array sub-expression operator #().
Examples are $myArray = #() and $myArray = #("value1","value2").
Used to create and/or initialize a hash table
The syntax is $variable = #{} or $variable = #{Property=Value}.
Used in here-strings
Here-strings are special case strings that can expand multiple lines and contain special characters
Denoted by beginning a string value with #' or #" and closing the string value with a corresponding '# or "#.
The here-string open and close characters should be isolated on their respective lines of the right-hand side (RHS).
Common At symbol
Used in email address construction, i.e. user#domain.com.
Used in external program remote logon syntax, i.e. user#hostname.
Extra Reading and Notable Links:
See About Arithmetic Operators for information on modulus among other arithmetic operators.
See Foreach-Object for more information about Foreach-Object and how objects are processed.
See About Splatting for more information and usage of splatting.
Another good resource is About Automatic Variables, which will list PowerShell's reserved/automatic variables. They are created and maintained by PowerShell. You will notice there are some variables that have non-alpha and non-numeric characters. You should only use these variables for their intended purposes and not use their names when you create your own custom variables.
See About Arrays for details on the array sub-expression operator.
See About Hash Tables for details on creating and manipulating hash table objects.
See About Quoting Rules to see more information and examples of using here-strings.

PowerShell to check the string in the array [duplicate]

I am trying to filter out users that are in a specific group.
I got the following output in a variable:
Group1
Group2
etc...
One group for each line saved in an array. Im trying to filter out only one specific group. But when I use -contains it always says $false, even tho the group is there.
My Code:
$group = get-aduser -identity name -properties memberof |
select-object -expandproperty memberof | %{ (get-adgroup $_).name }
$contains = $group -contains "string"
$contains is $false even if the array has elements that contain the string...
What am I missing?
It looks like your misconception was that you expected PowerShell's -contains operator to perform substring matching against the elements of the LHS array.
Instead, it performs equality tests - as -eq would - against the array's elements - see this answer for details.
In order to perform literal substring matching against the elements of an array, use:
# With non-literal search strings:
[bool] $contains = $group -match ([regex]::Escape($someString))
# With a string literal that doesn't contain regex metachars.,
# escaping isn't needed.
[bool] $contains = $group -match 'foo'
# With a string literal with metachars., you must individually \-escape them.
[bool] $contains = $group -match 'foo\.bar'
Note:
The above shows a robust, generic way of ensuring that your search string is treated as a literal value using [regex]::Escape(), which is necessary because -match expects a regex (regular expression) as its RHS (the search pattern).
Escaping isn't always necessary; specifically, only the presence of so-called metacharacters (those with special meaning in a regex, such as .) requires it, and when you're using a string literal, you can opt to directly \-escape them; e.g., to search for literal substring a.b, you can pass 'a\.b'.
Chances are that AD group names do not require escaping, but it's important to be aware of the need for it in general.
As with all operators in PowerShell, by default the matching is case-insensitive; use the -cmatch variant for case-sensitive matching.
The [bool] type constrained above is used to ensure that the result of the -match operation is converted to a Boolean:
While -match directly returns a Boolean with a scalar (non-array) LHS, with an array LHS it acts as a filter, and returns the matching array elements instead; interpreted in a Boolean context, such as in an if conditional, that usually still gives the expected result, because a non-empty array is interpreted as $true, whereas an empty one as $false; again, however it's important to know the difference.
This will rarely be a performance concern in practice, but it is worth noting that -match, due to acting as a filter with arrays, always matches against all array elements - it doesn't stop once the first match is found, the way that the -contains and -in operators do.
On the plus side, you can use -match to obtain the matching elements themselves.
The mistaken expectation of -contains performing substring matching may have arisen from confusion with the similarly named, but unrelated String.Contains() method, which indeed performs literal substring matching; e.g., 'foo'.Contains('o') yields $true.
Also note that .Contains() is case-sensitive - invariably in Windows PowerShell, by default in PowerShell (Core) 7+.
PowerShell has no operator for literal substring matching.
However, you could combine PowerShell's generic array-filtering features with the .Contains() string method - but note that this will typically perform (potentially much) worse than the -match approach.
A reasonably performant alternative is to use the PSv4+ .Where() array method as follows:
# Note: Substring search is case-sensitive here.
[bool] $contains = $group.Where({ $_.Contains("string") }, 'First')
On the plus side, this approach stops matching once the first match is found.
The answer was -match insted of contains. Now the output is true.

Arrays and -contains - test for substrings in the elements of an array

I am trying to filter out users that are in a specific group.
I got the following output in a variable:
Group1
Group2
etc...
One group for each line saved in an array. Im trying to filter out only one specific group. But when I use -contains it always says $false, even tho the group is there.
My Code:
$group = get-aduser -identity name -properties memberof |
select-object -expandproperty memberof | %{ (get-adgroup $_).name }
$contains = $group -contains "string"
$contains is $false even if the array has elements that contain the string...
What am I missing?
It looks like your misconception was that you expected PowerShell's -contains operator to perform substring matching against the elements of the LHS array.
Instead, it performs equality tests - as -eq would - against the array's elements - see this answer for details.
In order to perform literal substring matching against the elements of an array, use:
# With non-literal search strings:
[bool] $contains = $group -match ([regex]::Escape($someString))
# With a string literal that doesn't contain regex metachars.,
# escaping isn't needed.
[bool] $contains = $group -match 'foo'
# With a string literal with metachars., you must individually \-escape them.
[bool] $contains = $group -match 'foo\.bar'
Note:
The above shows a robust, generic way of ensuring that your search string is treated as a literal value using [regex]::Escape(), which is necessary because -match expects a regex (regular expression) as its RHS (the search pattern).
Escaping isn't always necessary; specifically, only the presence of so-called metacharacters (those with special meaning in a regex, such as .) requires it, and when you're using a string literal, you can opt to directly \-escape them; e.g., to search for literal substring a.b, you can pass 'a\.b'.
Chances are that AD group names do not require escaping, but it's important to be aware of the need for it in general.
As with all operators in PowerShell, by default the matching is case-insensitive; use the -cmatch variant for case-sensitive matching.
The [bool] type constrained above is used to ensure that the result of the -match operation is converted to a Boolean:
While -match directly returns a Boolean with a scalar (non-array) LHS, with an array LHS it acts as a filter, and returns the matching array elements instead; interpreted in a Boolean context, such as in an if conditional, that usually still gives the expected result, because a non-empty array is interpreted as $true, whereas an empty one as $false; again, however it's important to know the difference.
This will rarely be a performance concern in practice, but it is worth noting that -match, due to acting as a filter with arrays, always matches against all array elements - it doesn't stop once the first match is found, the way that the -contains and -in operators do.
On the plus side, you can use -match to obtain the matching elements themselves.
The mistaken expectation of -contains performing substring matching may have arisen from confusion with the similarly named, but unrelated String.Contains() method, which indeed performs literal substring matching; e.g., 'foo'.Contains('o') yields $true.
Also note that .Contains() is case-sensitive - invariably in Windows PowerShell, by default in PowerShell (Core) 7+.
PowerShell has no operator for literal substring matching.
However, you could combine PowerShell's generic array-filtering features with the .Contains() string method - but note that this will typically perform (potentially much) worse than the -match approach.
A reasonably performant alternative is to use the PSv4+ .Where() array method as follows:
# Note: Substring search is case-sensitive here.
[bool] $contains = $group.Where({ $_.Contains("string") }, 'First')
On the plus side, this approach stops matching once the first match is found.
The answer was -match insted of contains. Now the output is true.