Monthly Archives: April 2016

PowerShell Nested Switch Statements

There was a Reddit post recently that gave me an opportunity to try something I’m not sure I’ve tried before: nested switch statements—-switch statements inside switch statements. So we’re on the same page, I’ve written an example of a basic, non-nested switch. It provides some of the same functionality as an If-ElseIf. I’ll typically use a switch statement instead of an If-ElseIf when there’s three or more outcomes. It’s easier to read, and it looks cleaner.

In the example below, a specific phrase will be written if the value in the $Fruit variable is apple, banana, or pear. If it’s not one of those, it’ll take the default action. In the case below, the default action indicates that an apple, banana, or pear wasn’t chosen. Switch statements will sometimes be called case statements in other languages.

$Fruit = 'banana'
Switch ($Fruit) {
    'apple' {'You chose apple.'; break}
    'banana' {'You chose banana.'; break}
    'pear' {'You chose pear.'; break}
    default {'You didn''t choose an apple, banana, or pear.'}
}

As a part of my nested switch statement testing, I wrote this example using automobile makes and models. The outermost switch statement makes a determination of which nested switch statement to execute.

$Make = 'Ford'
$Model = 'Escape'
 
Switch ($Make) {
    'Ford' {
        Switch ($Model) {
            'Mustang' {Write-Output -InputObject 'You selected the Ford Mustang.'; break}
            'Escape' {Write-Output -InputObject 'You selected the Ford Escape.'; break}
            default {Write-Output -InputObject "You selected Ford, however, the $Model isn't a valid model for this make."; break}
        }
    }
    'Chevy' {
        Switch ($Model) {
            'Corvette' {Write-Output -InputObject 'You selected the Chevy Corvette.'; break}
            'Camaro' {Write-Output -InputObject 'You selected the Chevy Camaro.'; break}
            default {Write-Output -InputObject "You selected Chevy, however, the $Model isn't a valid model for this make."; break}
        }
    }
    'Dodge' {
        Switch ($Model) {
            'Charger' {Write-Output -InputObject 'You selected the Dodge Charger.'; break}
            'Viper' {Write-Output -InputObject 'You selected the Dodge Viper.'; break}
            default {Write-Output -InputObject "You selected Dodge, however, the $Model isn't a valid model for this make."; break}
        }
    }
}

Should we take this further—-a switch statement inside a switch statement inside a switch statement? I’m not going to lie, it started to get confusing and so my first recommendation would be to try and steer clear of the multi-nesting this deep (3 levels). I’ve removed some of the example above—-Chevy and Dodge—-in my example below and focused on the Ford (pun, absolutely intended). To help, I’ve included comments on the closing brackets. This multi-nested switch statement will return different values based on the make, the model, and the year, too.

$Make = 'Ford'
$Model = 'Escape'
$Year = '2016'
  
Switch ($Make) {
    'Ford' {
        Switch ($Model) {
            'Mustang' {
                Switch ($Year) {
                    '2015' {Write-Output -InputObject "You selected the 2015 Ford Mustang."; break}
                    '2016' {Write-Output -InputObject "You selected the 2016 Ford Mustang"; break}
                    default {Write-Output -InputObject "You selected the Ford Mustang with a non-matching year."; break}
                } # End Mustang Year Switch.
            } # End Mustang Switch.
            'Escape' {
                Switch ($Year) {
                    '2015' {Write-Output -InputObject "You selected the 2015 Ford Escape."; break}
                    '2016' {Write-Output -InputObject "You selected the 2016 Ford Escape."; break}
                    default {Write-Output -InputObject "You selected the Ford Escape with a non-matching year."; break}
                } # End Escape Year Switch.
            } # End Escape Switch.
            default {Write-Output -InputObject "You selected a Ford; however, the $Model isn't a valid model for this make."}
        } # End Ford Model Switch.
    } # End Ford Switch.
    default {Write-Output -InputObject 'Sorry, we''re only dealing with Fords (at the moment).'}
} # End Make Switch.

So I can track it down later, here’s the post I read on Reddit that influenced this post. It includes the link to my possible, partial solution; however, that direct link is here: http://pastebin.com/KRxRb1VM.

Update: It has occurred to me that it might be easier to follow this thrice nested switch without all the comments. I’ve removed those below.

$Make = 'Ford'
$Model = 'Escape'
$Year = '2016'
  
Switch ($Make) {
    'Ford' {
        Switch ($Model) {
            'Mustang' {
                Switch ($Year) {
                    '2015' {Write-Output -InputObject "You selected the 2015 Ford Mustang."; break}
                    '2016' {Write-Output -InputObject "You selected the 2016 Ford Mustang"; break}
                    default {Write-Output -InputObject "You selected the Ford Mustang with a non-matching year."; break}
                }
            }
            'Escape' {
                Switch ($Year) {
                    '2015' {Write-Output -InputObject "You selected the 2015 Ford Escape."; break}
                    '2016' {Write-Output -InputObject "You selected the 2016 Ford Escape."; break}
                    default {Write-Output -InputObject "You selected the Ford Escape with a non-matching year."; break}
                }
            }
            default {Write-Output -InputObject "You selected a Ford; however, the $Model isn't a valid model for this make."}
        }
    }
    default {Write-Output -InputObject 'Sorry, we''re only dealing with Fords (at the moment).'}
}

Linux Prompt on Windows – Part I

I already have an answer to the question, “Why?” Because, I can. At some point in the last few days, I decided I would attempt to replicate the Linux prompt in PowerShell. So I did it, and I thought I would share it. I’ve broken this up a little with the full function toward the bottom of this evening’s post.

In this first section, we’ve created the prompt function’s basic structure and added our first bit of code. This code determines if the user that started the Windows PowerShell session is an administrator or not. If they are, they get the # symbol as part of the prompt, and if they’re not, they get the $ symbol.

Function Prompt {
    If ([bool](([System.Security.Principal.WindowsIdentity]::GetCurrent()).Groups -match 'S-1-5-32-544')) {
        $Symbol = '#'
    } Else {
        $Symbol = '$'
    }
}

The next addition includes the logic to determine how to write the path inside the prompt. If the current path is C:\Users\<CurrentUser>, it enters a tilde (~). If the path includes the C:\Users\<CurrentUser> path, in addition to one or more directories, it will enter a tilde and the other directory or directories, such as ~/Desktop, or ~/Favorites/Links. In the last case, it’ll simply list the current path after replacing the backslashes with forward slashes and dumping the drive letter and colon. In all instances, in fact, all (Windows) backslashes, will be replaced with (Unix/Linux) forward slashes.

Function Prompt {
    If ([bool](([System.Security.Principal.WindowsIdentity]::GetCurrent()).Groups -match 'S-1-5-32-544')) {
        $Symbol = '#'
    } Else {
        $Symbol = '$'
    }

    If ($PWD.Path -eq $env:USERPROFILE) {
        $Location = '~'
    } ElseIf ($PWD.Path -like "*$env:USERPROFILE*") {
        $Location = $PWD.Path -replace ($env:USERPROFILE -replace '\\','\\'),'~' -replace '\\','/'
    } Else {
        $Location = "$(($PWD.Path -replace '\\','/' -split ':')[-1])"
    }
}

The last section is where the prompt function actually writes the prompt to the screen. It’s the simple combination of the current user, an @ character, the computer name, the path, and the proper symbol (# or $). As you may notice, I’ve written the code to force the case of the current user and computer to lowercase.

Function Prompt {
    If ([bool](([System.Security.Principal.WindowsIdentity]::GetCurrent()).Groups -match 'S-1-5-32-544')) {
        $Symbol = '#'
    } Else {
        $Symbol = '$'
    }

    If ($PWD.Path -eq $env:USERPROFILE) {
        $Location = '~'
    } ElseIf ($PWD.Path -like "*$env:USERPROFILE*") {
        $Location = $PWD.Path -replace ($env:USERPROFILE -replace '\\','\\'),'~' -replace '\\','/'
    } Else {
        $Location = "$(($PWD.Path -replace '\\','/' -split ':')[-1])"
    }

    "$($env:USERNAME.ToLower())@$($env:COMPUTERNAME.ToLower()) $Location $Symbol "
}

In closing, I’ve included a gif of the prompt and an example of moving though the directory structure. While I typically only use full cmdlet names in my posts, this example does include aliases for the Set-Location cmdlet (cd, sl, and even chdir). Due to this function being saved in my profile script, this prompt displays immediately when a new console is opened, and every time after that. As someone that’s written a few different prompt functions, this one’s a keeper. I already can’t imaging switching back. On that note, you might not want to test it out.

duplicate-the-linux-prompt01

tommymaynard@srv01 / # $PWD.Path
C:\
tommymaynard@srv01 / # cd C:\Windows\
tommymaynard@srv01 /Windows # cd .\System32\
tommymaynard@srv01 /Windows/System32 # chdir ..
tommymaynard@srv01 /Windows # chdir ..
tommymaynard@srv01 / # sl /users
tommymaynard@srv01 /users # sl /users/tommymaynard2 # Not my current user.
tommymaynard@srv01 /users/tommymaynard2 # cd ..
tommymaynard@srv01 /users # cd C:\Users\tommymaynard\ # My current user.
tommymaynard@srv01 ~ # cd .\Desktop\
tommymaynard@srv01 ~/Desktop # cd ..
tommymaynard@srv01 ~ # cd .\Favorites\Links\
tommymaynard@srv01 ~/Favorites/Links # sl /
tommymaynard@srv01 / # cd /users
tommymaynard@srv01 /users # chdir \
tommymaynard@srv01 / #

Update1: A day later and I still love my new PowerShell prompt. I’ve made a minor change, however. The last line in the function is now the following three lines. It stores the prompt in a variable called $Prompt, modifies the title of the window to the $Prompt value, and then writes the prompt. In the past my window’s title indicate the computer name. Now it indicates that information, as well as the current user, the location on the current drive, and whether or not I’m running the session as an administrator.

...
	$Prompt = "$($env:USERNAME.ToLower())@$($env:COMPUTERNAME.ToLower()) $Location $Symbol "
	$Host.UI.RawUI.WindowTitle = $Prompt
	$Prompt
}

Update2: It occurred to me today that by entering the tilde character, such as Set-Location -Path ~, it moves you to the location stored in the Home property of the FileSystem PSProvider. Because of this, I opted to add another line, just before the beginning of the prompt function. Setting this property to $env:USERPROFILE, allows me to quickly move to C:\Users\<CurrentUser>. I should note that this property is sometimes already set. In those cases, this is just a precaution that the ~ character will move you back home.

(Get-PSProvider -PSProvider FileSystem).Home = $env:USERPROFILE
Function Prompt {
...
}

In case you want to take this prompt function for a spin, and you do, here’s everything. Copy and paste it to your console and hit Enter a couple times and try it out. If you hate it, close and reopen the PowerShell console and it never existed. Cheers.

(Get-PSProvider -PSProvider FileSystem).Home = $env:USERPROFILE
Function Prompt {
    If ([bool](([System.Security.Principal.WindowsIdentity]::GetCurrent()).Groups -match 'S-1-5-32-544')) {
        $Symbol = '#'
    } Else {
        $Symbol = '$'
    }
 
    If ($PWD.Path -eq $env:USERPROFILE) {
        $Location = '~'
    } ElseIf ($PWD.Path -like "*$env:USERPROFILE*") {
        $Location = $PWD.Path -replace ($env:USERPROFILE -replace '\\','\\'),'~' -replace '\\','/'
    } Else {
        $Location = "$(($PWD.Path -replace '\\','/' -split ':')[-1])"
    }

	$Prompt = "$($env:USERNAME.ToLower())@$($env:COMPUTERNAME.ToLower()) $Location $Symbol "
	$Host.UI.RawUI.WindowTitle = $Prompt
	$Prompt
}

 

Check for Active Directory User in Function Parameters

I’m not sure what it is, but this might’ve been the longest I’ve gone without writing. It’s not that I’ve stopping thinking PowerShell, it’s that I’m so consumed, I’ve had a hard time focusing. My wife calls it my adult ADD. It was just last week that I attended my second PowerShell + DevOps Global Summit (that’s the new name). I learned a great deal and I had a great time. Thanks again to PowerShell.org for the contest and free 4-day pass, my employer for picking up the flight, lodging, and food, my parents for their help, and of course to my wife! The weather in Bellevue, WA was perfect and the summit was a huge success. Much gratitude to all those involved in the planning and execution.

It wasn’t long ago that I had a post about comparing Active Directory group memberships. I wanted a pleasant way to visualize and compare the group memberships of two AD users. We had a ticket earlier today, at the office, where I went back to that post and gathered the commands, so I could use them again. I knew I’d need it, I just didn’t know I’d need it so soon. I sent the output results as an image to a coworker, and it wasn’t but a few seconds later that he was interested in what was used to gather that output: it was my modified compare. Here’s an edited version of the image I sent. This image differs from the original post in that I added the users’ SamAccountNames as the property names in my collection. This is why they’re blurred out this time around.

check-for-active-directory-user-in-function-parameters01

I mentioned to him that I’d write the commands into a wrapper function. While that’s nearly done, I asked myself this question: When should I check if the users exist in Active Directory? Do I do it as part of the function (inside the Begin block, for instance), or do I check at the time the parameter is accepted, as part of a ValidateScript validation?

I’m here today to say that making it a part of the ValidateScript validation attribute is acceptable. While it has the potential to cause an error condition and dump red text to the console, that’s fine. People good at Windows PowerShell read these errors — they’re PowerShell’s errors, and we’re just allowing them to occur. While I can catch these in the function, PowerShell doesn’t always do this for us natively, so I as I see it, it’s not always necessary. Knowing the audience for your tool can help you with this determination. Do you want them to learn PowerShell and get comfortable with the native errors, or does it make sense to protect them from seeing these? It’s up to you. I’m okay with allowing my tools to display some of the native errors, especially since we’re talking about my team using these tools, and not a lower-tiered group. Again, you have to consider who will use your tools, and how they’ll handle the red text.

The test function I wrote, and some error results, can been seen below. In my mind, there were two error possibilities: the computer where the function is invoked doesn’t have the ActiveDirectory module, or one, or both, of the users don’t exist in Active Directory. Again, in many cases, these standard PowerShell errors can be delivered without the need to hide them from the user of the function. Here’s a test function now.

Function Test-ADUser {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$true,Position=0)]
        [ValidateScript({Get-ADUser -Identity $_})]
        [string]$User1,

        [Parameter(Mandatory=$true,Position=1)]
        [ValidateScript({Get-ADUser -Identity $_})]
        [string]$User2
    )

    Write-Output -InputObject "Success on both parameters."
}

This first image below, shows the error condition when the ActiveDirectory module isn’t available on the system where the function is invoked. It displays the standard, I-don’t-recognize-what-you-entered error: “Get-ADUser is not recognized…”

check-for-active-directory-user-in-function-parameters02

In this image, we can see the error message that occurs when one of the supplied users doesn’t exist in Active Directory. It won’t get this far unless the ActiveDirectory module is on the system where this function is invoked.

check-for-active-directory-user-in-function-parameters03

So in the end, consider when writing your tools, if a standard PowerShell error is acceptable or not. I think that there’s times it’s suitable, depending on the error and the group using the tool.