Powershell Equivalent of Linq Statement - powershell

I have the following function in a library that is being converted to Powershell from C# and I havn't the first clue how to go about translation this statement. I realize SO is not for do a task for me type questions but anyone have any thoughts on where to begin?
IEnumerable<string[]> ReadI3TableString(string sFileData)
{
var records = sFileData.Split(new[] { (char)05, (char)00 }, StringSplitOptions.RemoveEmptyEntries).Skip(1)
.Select(line => line.Split(line.ToArray().Where(c => ((Int16)c) >= 128 || ((Int16)c) < 32).ToArray(), StringSplitOptions.RemoveEmptyEntries)).Where(p => p.Length > 1);
return records;
}

It's not really a "Linq-ish" implementation, rather using Powershell operators and the Powershell pipeline to the same effect:
$records = $fileData -split { $_ -in #([char]0, [char]5) } |
Select -Skip 1 |
Where-Object {
($_ -split { $_ -ge [char] 128 -or $_ -lt 32 }).Count -gt 1
}
Note: with some sample data I could validate this, but this works with mocked up data I had set up.

Related

PowerShell Isn't Parsing Data as Expected // Basic PowerShell Operator Question

I have a pretty nooby question regarding the proper usage of certain operators.
I am writing a script that pulls data out of a CSV. The script asks the user to enter a location # and then is supposed to output the IP of their server.
The script works as expected except on rows where the location number also has other text in it like numbers or special characters.
Here is an example CSV to illustrate my problem:
Loc#,State,Server IP
1,NY,10.0.0.1
2,CA,10.0.0.2
3,WA,10.0.0.3
4 (inp),KY,10.0.0.4
My script looks something like this:
$CSV = import-csv C:\users\Self\MyProject.csv
$location = read-host "enter the location #"
foreach( $row in $CSV){
if($row.loc# -eq $location)
{
write-host $row.'Server IP'
}
Now, this script works as expected unless the user chooses location 4. If the users chooses location 4, then the $location variable is left blank.
Ok, this makes a little bit of sense since I'm using the -eq operator. But even if I use the -contains operator I get the same results.
Here is another way of showing my problem:
$number = "10 ten"
if ($number -contains "10"){ (Write-Host "true")} else{ write-host "false"}
false
Now, why isn't the output showing as "true" since that $number variable does indeed contain "10"?
Any help is much appreciated,
Thanks
As it seems, Location can have a random value and not only digits, normally you could use -match as Mahmoud Moawad pointed out in his comment, however this could also bring you problems since there is no specific pattern you can follow there is also no clear way of how you can filter the specific value. What I would personally do is give the user a list where he can choose the Location by Index:
$csv = Import-Csv C:\users\Self\MyProject.csv
$csv.foreach({
begin
{
'- Choose a location:'
$i = 0
$map = #{}
}
process
{
$map[(++$i)] = $_
"[$i] - {0}" -f $_.'Loc#'
}
})
$question = { Read-Host 'Index' }
while($true)
{
$index = (& $question) -as [int]
if($index -ge 1 -and $index -le $csv.Count)
{
break
}
'Input must be between 1 and {0}!!' -f $csv.Count
}
$map[$index].'Server IP'
As for why -contains is not showing you $true on your condition, the Containment Operators will look for an exact match of an element:
'10 ten' -contains '10' # => False
'10 ten' -contains '10 ten' # => True
'9 nine', '10 ten' -contains '10 ten' # => True

IndexOf() or .FindIndex() case-insensitive

I am trying to validate some XML with verbose logging of issues, including required order of attributes and miscapitalization. If the required order of attributes is one, two, three and the XML in question has one, three, two I want to log it. And if an attributes is simply miscapitalized, say TWO instead of two I want to log that as well.
Currently I have two arrays, $ordered with the names of the attributes as they should be (correct capitalization) and $miscapitalized with the names of the miscapitalized attributes.
So, given attributes of one, three, TWO and required order of one, two, three
$ordered = one, two, three
$miscapitalized = TWO
From here I want to append the miscapitalizion, so a new variable
$logged = one, two (TWO), three
I can get the index of $ordered where the miscapitalization occurs with
foreach ($attribute in $ordered) {
if ($attribute -iin $miscapitalized) {
$indexOrdered = [array]::IndexOf($ordered, $attribute)
}
}
However, I can't get the index in $miscapitalized based on the (correctly capitalized) $attribute. I tried
$miscapitalized = #('one', 'two', 'three')
$miscapitalized.IndexOf('TWO')
which doesn't work because .IndexOf() is case sensitive. I found this that says [Collections.Generic.List[Object]] will work, so I thought perhaps Generic.List was where the functionality came from. So I tried
$miscapitalized = [System.Collections.Generic.List[String]]#('one', 'two', 'three')
$miscapitalized.FindIndex('TWO')
Which throws
Cannot find an overload for "FindIndex" and the argument count: "1".
That led me to this that says I need an actual predicate type, not just a string. At which point I am in WAY over my head, and the only thing that I could come up with is $miscapitalized.FindIndex([System.Predicate]::new('TWO')) which doesn't work. I suspect a Predicate could/should be a regex somehow, but I can't seem to find anything that points me in the right direction, or at least that I can understand and recognize that it is pointing me in the right direction. I also found https://www.powershellstation.com/2010/05/18/passing-predicates-as-parameters-in-powershell/ that talks about a code block as predicate, but I am not clear that it's the same usage of the term predicate (it is a widely used term) nor can I grok how to even make a code block that would be helpful here.
I did come up with this approach, which uses the same foreach search in $miscapitalized as in $ordered and it does work. But I wonder if there is a more graceful approach that doesn't require nested loops. Plus, understanding Predicate as it applies here seems useful, as well as (possibly) how a codeblock might be used.
$ordered = #('one', 'two', 'three')
$miscapitalized = #('TWO')
$replacements = [System.Collections.Specialized.OrderedDictionary]::new()
foreach ($orderedAttribute in $ordered) {
if ($orderedAttribute -iin $miscapitalized) {
$indexOrdered = [array]::IndexOf($ordered, $orderedAttribute)
foreach ($miscapitalizedAttribute in $miscapitalized) {
if (($miscapitalizedAttribute -iin $ordered) -and ($miscapitalizedAttribute -ieq $orderedAttribute) -and ($miscapitalizedAttribute -cne $orderedAttribute)) {
#$indexMiscapitalized = [array]::IndexOf($miscapitalized, $miscapitalizedAttribute)
$replacements.Add($indexOrdered, "$orderedAttribute ($miscapitalizedAttribute)")
}
}
}
}
if ($replacements.Count -gt 0) {
foreach ($index in $replacements.Keys) {
$ordered[$index] = $replacements.$index
}
}
$ordered
EDIT: Based on comments below, I have tried this
$ordered = #('one', 'two', 'three')
$miscapitalized = #('TWO', 'Three')
$replacements = [System.Collections.Specialized.OrderedDictionary]::new()
foreach ($orderedAttribute in $ordered) {
if ($orderedAttribute -iin $miscapitalized) {
$indexOrdered = [array]::IndexOf($ordered, $orderedAttribute)
if ($indexMiscapitalized = $miscapitalized.FindIndex({param($s) $s -eq $orderedAttribute})) {
$replacements.Add($indexOrdered, "$orderedAttribute ($($miscapitalized[$indexMiscapitalized]))")
}
}
}
if ($replacements.Count -gt 0) {
foreach ($index in $replacements.Keys) {
$ordered[$index] = $replacements.$index
}
}
$ordered
Which gets the last one (three/Three) but is missing two/TWO. But lots of possible solutions to try tomorrow, since there will be something to learn from each one.
You can substitute a scriptblock for the predicate required by FindIndex():
PS ~> $miscapitalized = [System.Collections.Generic.List[String]]#('one', 'two', 'three')
PS ~> $predicate = {param($s) $s -eq 'TWO'}
PS ~> $miscapitalized.FindIndex($predicate)
1
This will work as expected since PowerShell's -eq operator is case-insensitive by default.
Perhaps, you're overthinking this. You could use Compare-Object to do all the hard work and then you can inspect results and log them accordingly:
# Reference array for attributes order and capitalization
[array]$reference = #(
'one'
'two'
'three'
'four'
)
# Example XML
[xml]$xml = '<foo one="1" oNe="oNe" thrEE="thrEE" two="2">dummy</foo>'
# Compare XML attributes to refrerence array
# -SyncWindow 0 - Order of items in the array matters
# https://stackoverflow.com/questions/40507552/powershell-order-sensitive-compare-objects-diff
Compare-Object -ReferenceObject $reference -DifferenceObject $xml.foo.Attributes.Name -SyncWindow 0 -CaseSensitive -includeEqual
This will produce:
InputObject SideIndicator
----------- -------------
one ==
oNe =>
two <=
thrEE =>
three <=
two =>
four <=
As you can see, the one attribute is in at the correct index (==) and properly cased. We also have additional oNe attribute, that is out of place.
You could also group the Compare-Object result and produce hashtable, which you can use for advanced logging. You could do all kinds of lookups and comparisons using SideIndicator and InputObject properties.
$group = Compare-Object -ReferenceObject $reference -DifferenceObject $xml.foo.Attributes.Name -SyncWindow 0 -CaseSensitive -includeEqual |
Group-Object -Property InputObject -AsHashTable -AsString
$group
Result
four {#{InputObject=four; SideIndicator=<=}}
one {#{InputObject=one; SideIndicator===}, #{InputObject=oNe; SideIndicator==>}}
thrEE {#{InputObject=thrEE; SideIndicator==>}, #{InputObject=three; SideIndicator=<=}}
two {#{InputObject=two; SideIndicator=<=}, #{InputObject=two; SideIndicator==>}}
In this case hashtable keys will be case-insensitive, so you can do stuff like this:
foreach ($r in $reference) {
$ret = $group.$r | Where-Object {
$_.SideIndicator -ne '==' -and $_.InputObject -cne $r
} | Select-Object -ExpandProperty InputObject |
ForEach-Object {
'Index of {0}: {1}' -f $_, $xml.foo.Attributes.Name.IndexOf($_)
}
if ($ret) {
#{ $r = $ret }
}
}
Name Value
---- -----
one Index of "oNe": 1
three Index of "thrEE": 2
You could add a small helper function that finds the index case-insensitive:
function Find-Index {
param (
[Parameter(Mandatory = $true, Position = 0)]
[string[]]$Array,
[Parameter(Mandatory = $true, Position = 1)]
[string]$Value
)
for ($i = 0; $i -lt $Array.Count; $i++) {
if ($Array[$i] -eq $Value) { return $i }
}
-1
# or combine the elements with some unlikely string
# convert that to lowercase and split on the same unlikely string
# then use regular IndexOf() against the value which is also lower-cased:
# (($Array -join '~#~').ToLowerInvariant() -split '~#~').IndexOf($Value.ToLowerInvariant())
}
Then below that, use it like this:
# if any of the below arrays has only one item, wrap it inside #()
$ordered = 'one','two','three'
$miscapitalized = 'One','TWO'
$logged = foreach ($item in $ordered) {
$index = Find-Index $miscapitalized $item
if ($index -ge 0) {
'{0} ({1})' -f $item, $miscapitalized[$index]
}
else { $item }
}
$logged -join ','
Output
one (One),two (TWO),three

Powershell studio datagridview if statement

I have a cellpainting event and am trying to correct/clean this IF statement up. I think I'm getting lost in my parenthess. Is someone able to take a second look at this? Thanks for your time.
My end goal is the IF statemant to be: Column 1 date older than 42 days or not $null and column 4 value = "SEP"
$datagridview1_CellPainting=[System.Windows.Forms.DataGridViewCellPaintingEventHandler]{
$SEPreturnlimit = "{0:MM/dd/yyyy}" -f (get-date).AddDays(-42)
if ((($_.ColumnIndex -eq 1 -and ([datetime]$_.Value -le $SEPreturnlimit -and [datetime]$_.Value.ToString() -ne $null))) -and ($datagridview1.rows .Cells[4].Value -eq "SEP")) #Column 1 date older than 42 days or not $null **and** column 4 value = "SEP"
{
$this.Rows[$_.RowIndex] | %{ $_.DefaultCellStyle.BackColor = 'crimson' } #Color Row
}
I would split the if conditions into separate nested ifs and also add a try..catch
$datagridview1_CellPainting = [System.Windows.Forms.DataGridViewCellPaintingEventHandler] {
if ($_.ColumnIndex -eq 1 -and $datagridview1.rows.Cells[4].Value -eq 'SEP') {
$SEPreturnlimit = (Get-Date).AddDays(-42).Date # set to 42 days back at midnight
try {
# if we succeed in parsing out a datetime object
$date = [datetime]$_.Value
# test if we have a DateTime object and if that date is older than the reference date
if (($date) -and $date-le $SEPreturnlimit) {
# cannot check this myself, but shouldn't that simply be
# $this.Rows[$_.RowIndex].DefaultCellStyle.BackColor = 'crimson'
$this.Rows[$_.RowIndex] | ForEach-Object{ $_.DefaultCellStyle.BackColor = 'crimson' } #Color Row
}
}
catch { <# do nothing #> }
}
}
I think I fixed the placement, but check the logic. Have a look at it this way
if
(
(
$_.ColumnIndex -eq 1 -and
(
[datetime]$_.Value -le
$SEPreturnlimit -and
[datetime]$_.Value.ToString() -ne
$null
)
) -and
(
$datagridview1.rows .Cells[4].Value -eq
'SEP'
)
) #Column 1 date older than 42 days or not $null **and** column 4 value = "SEP"

continue statement confusing in powershell, looks like break statement

My code looks like:
1..10 | % {
$i=$_
10..20 | % {
if ($_ -gt 12) {
continue
}
Write-Host $i $_
}
}
Why the output is:
1 10
1 11
1 12
It seems the continue statement in PoweShell is not different with other language, why PowerShell is designed like this?
If I change the continue to return, then I get the expect result.
As PeterSerAI pointed out in his comment, you don't use a loop in your code, you are instead using the Foreach-Object cmdlet which is different.
Just use a foreach loop instead:
foreach($obj in 1.. 10)
{
$i = $obj
foreach($obj2 in 10 ..20) {
if ($obj2 -gt 12) {
continue
}
Write-Host $obj $obj2
}
}

Converting strings to timespans, $PSItem in 'switch'?

I have a bunch of strings, in the form of:
'3m 36s', '24m 38s', '59s'
, to be converted to timespans. My current "solution" is:
'3m 36s', '24m 38s', '59s' |ForEach-Object {
$s = 0
$m = 0
$h = 0
$PSItem.Split(' ') |ForEach-Object {
$item = $PSItem
switch ($PSItem[-1])
{
's'
{
$s = $item.TrimEnd('s')
}
'm'
{
$m = $item.TrimEnd('m')
}
'h'
{
$h = $item.TrimEnd('h')
}
Default
{
Write-Error 'Ooops...' -ErrorAction Stop
}
}
}
$timespan = New-TimeSpan -Hours $h -Minutes $m -Seconds $s
# ToString() is used just to get some easy to read output
$timespan.ToString()
}
While it seems to work for me, I have two issues with the above:
Is the general approach
ForEach -> Split(' ') -> ForEach -> switch
OK-ish? Are there any alternative/better ways of doing the conversion?
I tried using $PSItem in the switch
It seems that the switch construct has it's "own pipeline"
# $item = $PSItem
switch ($PSItem[-1])
{
's'
{
$PSItem
}
}
-- in the above $PSItem evaluates to 's'(, 'm', the value matched). What is actually going on? (internaly?)
I would take one ForEach loop out of things by performing that loop with the Switch command. Here's what I'd end up with:
'3m 36s', '59s', '24m 38s' |%{
$TSParams = #{}
Switch($_.Split()){
{$_[-1] -eq 's'}{$TSParams.Add('Seconds', ([int]$_.trim('s')))}
{$_[-1] -eq 'm'}{$TSParams.Add('Minutes', ([int]$_.trim('m')))}
{$_[-1] -eq 'h'}{$TSParams.Add('Hours', ([int]$_.trim('h')))}
}
New-TimeSpan #TSParams
}
For each string it creates an empty hashtable, then loops through each item of the Split() method, adding the appropriate time to the hashtable. Then it splats that to the New-TimeSpan command, and moves to the next item in the ForEach loop. I tried it locally and had some issues initially when the numbers did not cast as an int, and it tried to convert them to a DateTime, which is why I type cast them in the above code.