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

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.

Related

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: if loop, while the first position of arraylist in in process

I have an arraylist, in which I am going to save some values. I am doing a foreach loop and if a value is going to be saved at the first position of the ArrayList, I want to output a "First position" line and otherwise, nothing should be done.
The if block that I wrote below, doesn't work.
<# Don't consider this code from here
[System.Collections.ArrayList]$alist = #()
$al ="keeranpc01,www.google.ch,192.168.0.25"
$al = $al.Split(",")
foreach($h in $al){
# Abstand vor dem Hostnamen löschen
if($h.contains(' ')) {
$h = $h -replace '\s', ''
}
$alist.Add($h)
}
to here
#>
#### Start here
[System.Collections.ArrayList]$rear = #()
foreach($ha in $alist) {
$PingConnetion = Test-Connection $ha -Count 1 -ErrorAction SilentlyContinue
$pcre = $ha
$pire = if($PingConnetion.ResponseTime -ne $null) {Write-output 'Erreichbar'}else{Write-Output 'Nicht Erreichbar'}
$zure = $PingConnetion.ResponseTime
$zeit = Get-Date -Format HH:mm:ss
if($alist[$_] -eq $alist[0]) {Write-Host 'First position'}
[void]$rear.Add([PSCustomObject]#{Zeit = $zeit; Host = $pcre; IPv4 = $PingConnetion.IPV4Address.IPAddressToString; Ping = $pire; Zugriffszeit = $zure; })
}
how should I write the if statement so that it is possible? I expect if-statement to work, Only when the first position of ArrayList is in process
thx
What you are tying to do does work except you are tying to compare element zero of your alist to a ordinal position of your alist which is invalid. You would need to compare the following:
if($ha -eq $alist[0]) {Write-Host 'First position'}
Below is a worked example that might be clearer.
$input = 1..10
foreach($x in $input){
if($input[0] -eq $x){
write-host "First Position"
}
$x
}

How can I pass dynamic parameters to powershell script and iterate over the list?

I want to create a powershell script that accepts dynamic parameters and I also want to iterate through them.
eg:
I call the powershell script in the following manner.
ParametersTest.ps1 -param1 value1 -param2 value2 -param3 value3
And I should be able to access my params inside the script as follows:
for($key in DynamicParams) {
$paramValue = DynamicParams[$key];
}
Is there anyway to do this in powershell? Thanks in advance.
There is nothing built-in like that (essentially you're asking for PowerShell parameter parsing in the absence of any definition of those parameters). You can emulate it, though. With $args you can get at all arguments of the function as an array. You can then iterate that and decompose it into names and values:
$DynamicParams = #{}
switch -Regex ($args) {
'^-' {
# Parameter name
if ($name) {
$DynamicParams[$name] = $value
$name = $value = $null
}
$name = $_ -replace '^-'
}
'^[^-]' {
# Value
$value = $_
}
}
if ($name) {
$DynamicParams[$name] = $value
$name = $value = $null
}
To iterate over dynamic parameters you can either do something like you wrote
foreach ($key in $DynamicParams.Keys) {
$value = $DynamicParams[$key]
}
(note the foreach, not for, the latter of which cannot work like you wrote it) or just iterate normally over the hash table:
$DynamicParams.GetEnumerator() | ForEach-Object {
$name = $_.Key
$value = $_.Value
}

How can I alternate/switch parts of a string in PowerShell without using an intermediate value?

I'm trying to alternate a setting in a config file using PowerShell. For example, if a certain value is true, I'd like to switch it to false. If it's false, I'd like to switch it to true. I'd also like to change a path from \\servername\folder\ to \\servername\. Is there a way I can perform this in PowerShell without using an intermediate value?
If I do this:
$foo = 'aaa'
$foo -replace 'aaa', 'bbb' -replace 'bbb', 'aaa'
$foo will always be 'aaa'. I realize I could make an intermittent change ('aaa' becomes 'ccc' and then changes to 'bbb') but that's messy to read.
How can I alternate values without using an intermediate value?
I'm not quite sure what you're trying to achieve. Do you want to switch two values without a buffer variable? Or do you want to toggle a value between two states? The latter can be achieved like this:
function Toggle($s, $v1, $v2) {
$e1 = [regex]::Escape($v1)
$e2 = [regex]::Escape($v2)
$r = $s
if ($s -match $e1) {
$r = $s -replace $e1, $v2
} elseif ($s -match $e2) {
$r = $s -replace $e2, $v1
}
return ($r)
}
$foo = "..."
Toggle $foo "\\servername\folder\" "\\servername\"
Using a match evaluator. Not sure if this is any better or not.
Begin{
[regex]$ValueRegex = 'aaa|bbb'
$ValueToggles = #{
aaa='bbb'
bbb='aaa'
}
$toggleValue = {$ValueToggles[$args[0].groups[0].value]}
}
Process{
$foo = 'Value: aaa'
$ValueRegex.replace($foo,$toggleValue)
}
Value: bbb
You can alternate a boolean value ($true or $false) simply by using the -not operator; e.g.:
PS C:\> $value = $true
PS C:\> $value = -not $value
PS C:\> $value
False
Regular expression replacement:
PS C:\> '\\servername\folder\' -replace '^(\\\\[^\\]+)\\[^\\]+', '$1'
\\servername\
Bill

Is it possible to include functions only without executing the script?

Say I have MyScript.ps1:
[cmdletbinding()]
param (
[Parameter(Mandatory=$true)]
[string] $MyInput
)
function Show-Input {
param ([string] $Incoming)
Write-Output $Incoming
}
function Save-TheWorld {
#ToDo
}
Write-Host (Show-Input $MyInput)
Is it possible to dot source the functions only somehow? The problem is that if the script above is dot sourced, it executes the whole thing...
Is my best option to use Get-Content and parse out the functions and use Invoke-Expression...? Or is there a way to access PowerShell's parser programmatically? I see this might be possible with PSv3 using [System.Management.Automation.Language.Parser]::ParseInput but this isn't an option because it has to work on PSv2.
The reason why I'm asking is that i'm trying out the Pester PowerShell unit testing framework and the way it runs tests on functions is by dot sourcing the file with the functions in the test fixture. The test fixture looks like this:
MyScript.Tests.ps1
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path).Replace(".Tests.", ".")
. "$here\$sut"
Describe "Show-Input" {
It "Verifies input 'Hello' is equal to output 'Hello'" {
$output = Show-Input "Hello"
$output.should.be("Hello")
}
}
Using Doug's Get-Function function you could include the functions this way:
$script = get-item .\myscript.ps1
foreach ($function in (get-function $script))
{
$startline = $function.line - 1
$endline = $startline
$successful = $false
while (! $successful)
{
try {
$partialfunction = ((get-content $script)[$startline..$endline]) -join [environment]::newline
invoke-expression $partialfunction
$successful = $true
}
catch [Exception] { $endline++ }
}
}
Edit: [System.Management.Automation.IncompleteParseException] can be used instead of [Exception] in Powershell V2.
Note -- if you find this answer helpful please upvote jonZ's answer as I wouldn't of been able to come up with this if it weren't for his helpful answer.
I created this function extractor function based on the script #jonZ linked to. This uses [System.Management.Automation.PsParser]::Tokenize to traverse all tokens in the input script and parses out functions into function info objects and returns all function info objects as an array. Each object looks like this:
Start : 99
Stop : 182
StartLine : 7
Name : Show-Input
StopLine : 10
StartColumn : 5
StopColumn : 1
Text : {function Show-Input {, param ([string] $Incoming), Write-Output $Incoming, }}
The text property is a string array and can be written to temporary file and dot sourced in or combined into a string using a newline and imported using Invoke-Expression.
Only the function text is extracted so if a line has multiple statements such as: Get-Process ; function foo () { only the part relevant to the function will be extracted.
function Get-Functions {
param (
[Parameter(Mandatory=$true)]
[System.IO.FileInfo] $File
)
try {
$content = Get-Content $File
$PSTokens = [System.Management.Automation.PsParser]::Tokenize($content, [ref] $null)
$functions = #()
#Traverse tokens.
for ($i = 0; $i -lt $PSTokens.Count; $i++) {
if($PSTokens[$i].Type -eq 'Keyword' -and $PSTokens[$i].Content -eq 'Function' ) {
$fxStart = $PSTokens[$i].Start
$fxStartLine = $PSTokens[$i].StartLine
$fxStartCol = $PSTokens[$i].StartColumn
#Skip to the function name.
while (-not ($PSTokens[$i].Type -eq 'CommandArgument')) {$i++}
$functionName = $PSTokens[$i].Content
#Skip to the start of the function body.
while (-not ($PSTokens[$i].Type -eq 'GroupStart') -and -not ($PSTokens[$i].Content -eq '{')) {$i++ }
#Skip to the closing brace.
$startCount = 1
while ($startCount -gt 0) { $i++
if ($PSTokens[$i].Type -eq 'GroupStart' -and $PSTokens[$i].Content -eq '{') {$startCount++}
if ($PSTokens[$i].Type -eq 'GroupEnd' -and $PSTokens[$i].Content -eq '}') {$startCount--}
}
$fxStop = $PSTokens[$i].Start
$fxStopLine = $PSTokens[$i].StartLine
$fxStopCol = $PSTokens[$i].StartColumn
#Extract function text. Handle 1 line functions.
$fxText = $content[($fxStartLine -1)..($fxStopLine -1)]
$origLine = $fxText[0]
$fxText[0] = $fxText[0].Substring(($fxStartCol -1), $fxText[0].Length - ($fxStartCol -1))
if ($fxText[0] -eq $fxText[-1]) {
$fxText[-1] = $fxText[-1].Substring(0, ($fxStopCol - ($origLine.Length - $fxText[0].Length)))
} else {
$fxText[-1] = $fxText[-1].Substring(0, ($fxStopCol))
}
$fxInfo = New-Object -TypeName PsObject -Property #{
Name = $functionName
Start = $fxStart
StartLine = $fxStartLine
StartColumn = $fxStartCol
Stop = $fxStop
StopLine = $fxStopLine
StopColumn = $fxStopCol
Text = $fxText
}
$functions += $fxInfo
}
}
return $functions
} catch {
throw "Failed in parse file '{0}'. The error was '{1}'." -f $File, $_
}
}
# Dumping to file and dot sourcing:
Get-Functions -File C:\MyScript.ps1 | Select -ExpandProperty Text | Out-File C:\fxs.ps1
. C:\fxs.ps1
Show-Input "hi"
#Or import without dumping to file:
Get-Functions -File C:\MyScript.ps1 | % {
$_.Text -join [Environment]::NewLine | Invoke-Expression
}
Show-Input "hi"