Switch handling with global arrays - powershell

Say I have 2 global arrays:
$Global:Values = #(1..100)
$Global:Test = #(75, 50, 25, 101)
Then I create a Switch by piping $Global:Test into a ForEach loop by using different conditions which I tried troubleshooting to get any kind of response:
$Global:Test | ForEach-Object ($_) {
$_
Switch ($_) {
($_ -contains $Global:Values) {Write-Host "Excellent"}
($_ -in $Global:Values) {Write-Host "Satisfactory"}
($_ -eq "25") {Write-Host "Unsatisfactory"}
Default {Write-Host "Invalid Grade"}
}
}
Output:
75
Invalid Grade
50
Invalid Grade
25
Invalid Grade
101
Invalid Grade
All of these switch statements do not work except the default. I don't know what I'm missing, but I'm sure it's a simple mistake I'm overlooking. Can anyone help me spot this mistake?

The foreach() statement is not the same as the ForEach-Object cmdlet, and you seem to be combining the syntax of both.
Additionally, you're using the switch statement incorrectly.
The part you're trying to match against needs to be a value:
switch ($something)
{
5 { "5 thing" }
(10 + 10) { "20 thing" }
}
or a scriptblock expression that returns a truthy or falsey value:
switch ($something)
{
{ $_ -gt 5 } { "do > 5 thing" }
{ Test-SomeCondition -Value $_ } { "do other thing" }
}
So you want the scriptblock form, but you're using parentheses.
So here's the ForEach-Object form:
$Global:Test | ForEach-Object {
Switch ($_) {
{$_ -contains $Global:Values} {Write-Host "Excellent"}
{$_ -in $Global:Values} {Write-Host "Satisfactory"}
"25" {Write-Host "Unsatisfactory"}
Default {Write-Host "Invalid Grade"}
}
}
or the foreach form:
foreach($value in $Global:Test) {
Switch ($value) {
{$_ -contains $Global:Values} {Write-Host "Excellent"}
{$_ -in $Global:Values} {Write-Host "Satisfactory"}
"25" {Write-Host "Unsatisfactory"}
Default {Write-Host "Invalid Grade"}
}
}
Also of note, the switch statement can actually work directly with arrays, so you don't have to iterate first. Basic example:
switch (1..10)
{
{ $_ -gt 5 } { "Second Half"}
default: { "First Half" }
}
So with yours, maybe:
Switch ($Global:Test) {
{$_ -contains $Global:Values} {Write-Host "Excellent"}
{$_ -in $Global:Values} {Write-Host "Satisfactory"}
"25" {Write-Host "Unsatisfactory"}
Default {Write-Host "Invalid Grade"}
}
Small addendum: stop using Global variables!

Related

How do I preserve Enums as 'Strings' in ConvertTo-Json cmdlet? [duplicate]

For "Get-Msoldomain" powershell command-let I get the below output (lets call it Output#1) where Name, Status and Authentication are the property names and below are their respective values.
Name Status Authentication
myemail.onmicrosoft.com Verified Managed
When I use the command with "ConvertTo-Json" like below
GetMsolDomain |ConvertTo-Json
I get the below output (lets call it Output#2) in Json Format.
{
"ExtensionData": {
},
"Authentication": 0,
"Capabilities": 5,
"IsDefault": true,
"IsInitial": true,
"Name": "myemail.onmicrosoft.com",
"RootDomain": null,
"Status": 1,
"VerificationMethod": 1
}
However, the problem is, that if you notice the Status property in both the outputs, it's different. Same happens for VerificationMethod property. Without using the ConvertTo-JSon Powershell gives the Text, and with using ConvertTo-Json it gives the integer.
When I give the below command
get-msoldomain |Select-object #{Name='Status';Expression={"$($_.Status)"}}|ConvertTo-json
I get the output as
{
"Status": "Verified"
}
However, I want something so that I don't have to specify any specific property name for it to be converted , the way I am specifying above as
Select-object #{Name='Status';Expression={"$($_.Status)"}}
This line is transforming only the Status Property and not the VerificationMethod property because that is what I am providing as input .
Question: Is there something generic that I can give to the "ConvertTo-Json" commandlet, so that It returns ALL the Enum properties as Texts and not Integers, without explicitly naming them, so that I get something like below as the output:
{
"ExtensionData": {
},
"Authentication": 0,
"Capabilities": 5,
"IsDefault": true,
"IsInitial": true,
"Name": "myemail.onmicrosoft.com",
"RootDomain": null,
"Status": "Verified",
"VerificationMethod": "DnsRecord"
}
Well, if you don't mind to take a little trip :) you can convert it to CSV which will force the string output, then re-convert it back from CSV to PS Object, then finally back to Json.
Like this:
Get-MsolDomain | ConvertTo-Csv | ConvertFrom-Csv | ConvertTo-Json
If you need to keep the original Types instead of converting it all to string see mklement0 helpful answer...
PowerShell Core (PowerShell versions 6 and above) offers a simple solution via ConvertTo-Json's -EnumsAsStrings switch.
GetMsolDomain | ConvertTo-Json -EnumsAsStrings # PS *Core* (v6+) only
Unfortunately, this switch isn't supported in Windows PowerShell.
Avshalom's answer provides a quick workaround that comes with a big caveat, however: All property values are invariably converted to strings in the process, which is generally undesirable (e.g., the Authentication property's numeric value of 0 would turn into string '0').
Here's a more generic workaround based on a filter function that recursively introspects the input objects and outputs ordered hashtables that reflect the input properties with enumeration values converted to strings and all other values passed through, which you can then pass to ConvertTo-Json:
Filter ConvertTo-EnumsAsStrings ([int] $Depth = 2, [int] $CurrDepth = 0) {
if ($_ -is [enum]) { # enum value -> convert to symbolic name as string
$_.ToString()
} elseif ($null -eq $_ -or $_.GetType().IsPrimitive -or $_ -is [string] -or $_ -is [decimal] -or $_ -is [datetime] -or $_ -is [datetimeoffset]) {
$_
} elseif ($_ -is [Collections.IEnumerable] -and $_ -isnot [Collections.IDictionary]) { # enumerable (other than a dictionary)
, ($_ | ConvertTo-EnumsAsStrings -Depth $Depth -CurrDepth ($CurrDepth+1))
} else { # non-primitive type or dictionary (hashtable) -> recurse on properties / entries
if ($CurrDepth -gt $Depth) { # depth exceeded -> return .ToString() representation
Write-Warning "Recursion depth $Depth exceeded - reverting to .ToString() representations."
"$_"
} else {
$oht = [ordered] #{}
foreach ($prop in $(if ($_ -is [Collections.IDictionary]) { $_.GetEnumerator() } else { $_.psobject.properties })) {
if ($prop.Value -is [Collections.IEnumerable] -and $prop.Value -isnot [Collections.IDictionary] -and $prop.Value -isnot [string]) {
$oht[$prop.Name] = #($prop.Value | ConvertTo-EnumsAsStrings -Depth $Depth -CurrDepth ($CurrDepth+1))
} else {
$oht[$prop.Name] = $prop.Value | ConvertTo-EnumsAsStrings -Depth $Depth -CurrDepth ($CurrDepth+1)
}
}
$oht
}
}
}
Caveat: As with ConvertTo-Json, the recursion depth (-Depth) is limited to 2 by default, to prevent infinite recursion / excessively large output (as you would get with types such as [System.IO.FileInfo] via Get-ChildItem, for instance). Similarly, values that exceed the implied or specified depth are represented by their .ToString() value. Use -Depth explicitly to control the recursion depth.
Example call:
PS> [pscustomobject] #{ p1 = [platformId]::Unix; p2 = 'hi'; p3 = 1; p4 = $true } |
ConvertTo-EnumsAsStrings -Depth 2 |
ConvertTo-Json
{
"p1": "Unix", # Enum value [platformId]::Unix represented as string.
"p2": "hi", # Other types of values were left as-is.
"p3": 1,
"p4": true
}
Note: -Depth 2 isn't necessary here, given that 2 is the default value (and given that the input has depth 0), but it is shown here as a reminder that you may want to control it explicitly.
If you want to implement custom representations for additional types, such as [datetime], [datetimoffset] (using the ISO 8601-compatible .NET round-trip date-time string format, o, as PowerShell (Core) v6+ automatically does), as well as [timespan], [version], [guid] and [ipaddress], see Brett's helpful variation of this answer.
I needed to serialize pwsh objects to JSON, and was not able to use the -EnumsAsStrings parameter of ConvertTo-Json, as my code is running on psv5. As I encountered infinite loops while using #mklement0's code Editor's note: since fixed., I rewrote it. My revised code also deals with the serialization of some other types such as dates, serializing them into the ISO 8601 format, which is generally the accepted way to represent dates in JSON. Feel free to use this, and let me know if you encounter any issues.
Filter ConvertTo-EnumsAsStrings ([int] $Depth = 10, [int] $CurrDepth = 0) {
if ($CurrDepth -gt $Depth) {
Write-Error "Recursion exceeded depth limit of $Depth"
return $null
}
Switch ($_) {
{ $_ -is [enum] -or $_ -is [version] -or $_ -is [IPAddress] -or $_ -is [Guid] } {
$_.ToString()
}
{ $_ -is [datetimeoffset] } {
$_.UtcDateTime.ToString('o')
}
{ $_ -is [datetime] } {
$_.ToUniversalTime().ToString('o')
}
{ $_ -is [timespan] } {
$_.TotalSeconds
}
{ $null -eq $_ -or $_.GetType().IsPrimitive -or $_ -is [string] -or $_ -is [decimal] } {
$_
}
{ $_ -is [hashtable] } {
$ht = [ordered]#{}
$_.GetEnumerator() | ForEach-Object {
$ht[$_.Key] = ($_.Value | ConvertTo-EnumsAsStrings -Depth $Depth -CurrDepth ($CurrDepth + 1))
}
if ($ht.Keys.Count) {
$ht
}
}
{ $_ -is [pscustomobject] } {
$ht = [ordered]#{}
$_.PSObject.Properties | ForEach-Object {
if ($_.MemberType -eq 'NoteProperty') {
Switch ($_) {
{ $_.Value -is [array] -and $_.Value.Count -eq 0 } {
$ht[$_.Name] = #()
}
{ $_.Value -is [hashtable] -and $_.Value.Keys.Count -eq 0 } {
$ht[$_.Name] = #{}
}
Default {
$ht[$_.Name] = ($_.Value | ConvertTo-EnumsAsStrings -Depth $Depth -CurrDepth ($CurrDepth + 1))
}
}
}
}
if ($ht.Keys.Count) {
$ht
}
}
Default {
Write-Error "Type not supported: $($_.GetType().ToString())"
}
}
}

Replacing multiple if statements

I am using PS version 5.0 and I have quite a few if statements which might grow over time.
if ($hostname -like "**12*") {
Write-Output "DC1"
} elseif ($Hostname -like "**23*") {
Write-Output "DC2"
} elseif ($Hostname -like "**34*") {
Write-Output "DC3"
} elseif ($Hostname -like "**45*") {
Write-Output "DC4"
}
Can you suggest some better way of writing the same code?
You could use a switch statement. Here is an example using the -Regex flag since it looks like you are doing just a simple match and then could cut out the * wildcards.
$hostname = 'asdf12asdf'
switch -Regex ($hostname) {
"12" {Write-Output "DC1"}
"23" {Write-Output "DC2"}
"34" {Write-Output "DC3"}
"45" {Write-Output "DC4"}
Default {Write-Error "No Match Found"}
}
If you didn't want multiple matches add a ; Break after each case. For example if you had a host name such as asdf12asdf34 a statement "12" {Write-Output "DC1"; Break} would prevent the output both of 12 and 34

Pipe a string through multiple if-statements with an else for each

Essentially I have an If statement that has two conditions using -and, which a string is then run through. However, what I really want is the string to be run through the first condition, then if that is true, it checks for the second, and if that is true it does one thing, and if it is false do something else.
I currently have:
if(($_ -match "/cls") -and ($env:UserName -eq $Name)){cls}
I know I can do what I want with:
if(($_ -match "/cls") -and ($env:UserName -eq $Name)){cls}
elseif(($_ -match "/cls") -and ($env:UserName -ne $Name)){OTHER COMMAND}
But I would like to know if there was a more simple way.
(The result is already being piped in from somewhere else, hence the $_)
Move the first match to the outer scope.
if($_ -match "/cls")
{
if ($env:UserName -eq $Name))
{
cls
}
else
{
other command
}
}
I would write two if statements to improve readabilty. This way, you also need the pipeline value only once::
if($_ -match "/cls")
{
if ($env:UserName -eq $Name)
{
cls
}
else
{
#OTHER COMMAND
}
}

How can I get out of the loop once and for all?

I have this file:
function t {
"abcd" -split "" |%{ if ($_ -eq "b") { return; } write-host $_; }
}
$o = #{}
$o |add-member -name t -membertype scriptmethod -value {
"abcd" -split "" |%{ if ($_ -eq "b") { return; } write-host $_; }
}
write-host '-function-'
t;
write-host '-method-'
$o.t();
and if I run it I'll get:
-function-
a
c
d
-method-
a
c
d
as expected. but if what I wanted was 'a', I could replace the return with break. if I do so in the function what I get is:
-function-
a
so the method never even gets called. if I replace it in the method, it won't compile (and complains about calling it). what is going on here?
and what's the proper way to exit the foreach-object loop once and for all?
The simplest solution is to rewrite it as a foreach statement, instead of using the ForEach-Object command, which is particularly hard to stop:
$o | add-member -name t -membertype scriptmethod -value {
foreach($e in "abcd" -split "") {
if ($_ -eq "b") { break }
write-host $_;
}
}
As a side note, the foreach(){} keyword/construct runs a lot faster than piping through ForEach-Object.
For the sake of completeness ...
To stop a pipeline, throw PipelineStoppedException
"abcd" -split "" | % {
if($_ -eq "b") {
throw [System.Management.Automation.PipelineStoppedException]::new()
}
$_
}
You don't use break to stop the ForEach, you use return since it's executed as a lambda on each member of the object anyway.

Break out of inner loop only in nested loop

I'm trying to implement a break so I don't have to continue to loop when I got the result xxxxx times.
$baseFileCsvContents | ForEach-Object {
# Do stuff
$fileToBeMergedCsvContents | ForEach-Object {
If ($_.SamAccountName -eq $baseSameAccountName) {
# Do something
break
}
# Stop doing stuff in this For Loop
}
# Continue doing stuff in this For Loop
}
The problem is, that break is exiting both ForEach-Object loops, and I just want it to exit the inner loop. I have tried reading and setting flags like :outer, however all I get is syntax errors.
Anyone know how to do this?
You won't be able to use a named loop with ForEach-Object, but can do it using the ForEach keyword instead like so:
$OuterLoop = 1..10
$InnerLoop = 25..50
$OuterLoop | ForEach-Object {
Write-Verbose "[OuterLoop] $($_)" -Verbose
:inner
ForEach ($Item in $InnerLoop) {
Write-Verbose "[InnerLoop] $($Item)" -Verbose
If ($Item -eq 30) {
Write-Warning 'BREAKING INNER LOOP!'
BREAK inner
}
}
}
Now whenever it gets to 30 on each innerloop, it will break out to the outer loop and continue on.
Using the return keyword works, as "ForEach-Object" takes a scriptblock as it's parameter and than invokes that scriptblock for every element in the pipe.
1..10 | ForEach-Object {
$a = $_
1..2 | ForEach-Object {
if($a -eq 5 -and $_ -eq 1) {return}
"$a $_"
}
"---"
}
Will skip "5 1"