Category Archives: Quick Learn

Practical examples of PowerShell concepts gathered from specific projects, forum replies, or general PowerShell use.

Checking a Variable for a Path

I helped someone on a forum recently that needed to determine if a path existed or not. Providing the path did exist, they would need to perform an action. The problem wasn’t what to do (in the end), it was determining if the path existed (to begin with). Here’s a rough draft of their code:

$Path = 'C:\Scripts\TestFolder'

If ($Path) {
    Remove-Item -Path $Path
}

If you’ve been doing this a little while, then it’s quite easy to spot the problem with the “logic” used in the example above. The If statement’s condition is only evaluating if $Path has a value (if it contains something). The fact that the value may, or may not, be a path, and whether or not it exists (if it is a path), is not being evaluated. I recommended they use the Test-Path cmdlet, to ensure the value stored in $Path, was a path, and that it existed.

$Path = 'C:\Scripts\TestFolder'

If (Test-Path -Path $Path) {
    Remove-Item -Path $Path
}

There are times when all you will want to know is if a variable contains a value. As an example, we can use a variable to record if a specific process is running (at the time the variable is created), and take action depending on if it is, or isn’t.

The example below checks for a process called pn (Programmer’s Notepad) and stores the results in the $PNProcess variable. We can then check and determine if $PNProcess has been set, or assigned a value, and make a determination of what to do based on the results. Since we’re only looking to see if our variable contains a value, then this is a perfectly suitable solution.

$PNProcess = Get-Process -ProcessName pn -ErrorAction SilentlyContinue

If ($PNProcess) {
    Write-Output -Verbose "Programmer's Notepad is running."
} Else {
    Write-Output -Verbose "Programmer's Notepad is not running."
}

Note: While I would normally wrap a string in single quotes, double quotes are being used because single quotes will not work when there’s a single quote in the string.

Determine if the Alias, or Function Name was Used

As a Windows PowerShell enthusiast, I am often thinking about PowerShell. Seems logical. Can I do this? What will happen if I do that? Why doesn’t this work!? Well, I had one of those thoughts recently. Can I determine if a function was called using an alias, or if it was called using the function name?

This thought occurred to me because of a change I considered adding to one of my functions. The idea was, that I wanted to prompt the user for confirmation to run a function (within the function), if an alias was used, and not prompt the user when the full function name was used. This lined up with the assumption that an alias could be accidentally entered more easily than the function name, and that we may want to take precautions when the alias was used.

My first consideration was to get the last command entered by the user, by using: (Get-History -Count 1).CommandLine. The problem with that “option,” is that the history isn’t updated until after the function has ended. This means that I wouldn’t know what was entered by the user while the function was running. Take a look at the example below. When I run the function Get-TMHistory the first time, nothing is displayed (in a fresh console), but something is displayed after the second run.

Function Get-TMHistory {
	Get-History -Count 1
}

PS C:\> Get-TMHistory
PS C:\> Get-TMHistory

  Id CommandLine
  -- -----------
   1 Get-TMHistory

I came up with a another idea. Let’s start by creating a new function called Get-TMHowCalled. In addition, we’ll need to create an alias for the function – let’s use ghc. I’ve included the New-Alias cmdlet’s -Force parameter in case there’s already an alias with that name. If there is, then New-Alias acts like Set-Alias, and updates the alias (instead of throwing an error). Note: This option could be problematic if it replaced ghc, and ghc was used for a different purpose.

New-Alias -Name ghc -Value Get-TMHowCalled -Force

Function Get-TMHowCalled {

}

The next thing to do is to construct the logic that will check and determine if the alias was used, or if the full function name was used. This is simple function: check, and echo what was entered by the user.

Function Get-TMHowCalled {
    If () {
        Write-Output -Verbose 'Alias was used.'
    } ElseIf () {
        Write-Output -Verbose 'Function name was used.'
    }
}

The secret here is the $MyInvocation automatic variable and its Line property. This stores what was entered, and so it can be used to determine how the function was called – by alias or by name. The best part, is that the variable is populated when the function is called and can, therefore, be queried inside the function. This variable can only be used in “scripts, functions, and script blocks.” You can read more about it using this command: Get-Help about_Automatic_Variables.

Function Get-TMHowCalled {
    If ($MyInvocation.Line -eq 'ghc') {
        Write-Output -Verbose 'Alias was used.'
    } ElseIf ($MyInvocation.Line -eq 'Get-TMHowCalled') {
        Write-Output -Verbose 'Function name was used.'
    }
}

Hope this proves to be helpful for someone. It might be for me.

Get the Version of PowerShell (Not its Host)

I’ve seen it over and over again: people telling people to use $Host, or the Get-Host cmdlet, to determine the version of Windows PowerShell. Please stop. Get-Host and $Host return a version, sure, but that’s the version of the host – the application that is hosting PowerShell. It’s not the version of PowerShell.

For many of us, the host is the PowerShell console, or the ISE. The name of the console host is ConsoleHost, and the name of the ISE host is ‘Windows PowerShell ISE Host.’ You can return these names by entering $Host or Get-Host, in the respective host, and looking at the Name property. These hosting applications are made by our friends at Microsoft, and so often, if not always, the version of the host will match the version of PowerShell. That said, you’d still be better served to get your results from the proper variable, especially in certain situations.

In order to ensure you are returning the version of PowerShell, use the automatic variable $PSVersionTable. The examples below show how to return information using this variable. Notice how we further isolate the version property – a suitable thing to do when you’re using the correct variable.

PS C:\> $PSVersionTable

Name                           Value
----                           -----
PSVersion                      4.0
WSManStackVersion              3.0
SerializationVersion           1.1.0.1
CLRVersion                     4.0.30319.34014
BuildVersion                   6.3.9600.17090
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0}
PSRemotingProtocolVersion      2.2

PS C:\> $PSVersionTable.PSVersion

Major  Minor  Build  Revision
-----  -----  -----  --------
4      0      -1     -1

PS C:\> $PSVersionTable.PSVersion.Major
4
PS C:\>

We can do the same type of property isolation with $Host and Get-Host. However, to get in this habit while using $Host, or Get-Host, is a bad idea, and may lead to wasted time and confusion if you’re attempting to determine the version of PowerShell.

PS C:\> $Host

Name             : ConsoleHost
Version          : 4.0
InstanceId       : 48ae3046-45ed-46d4-bf6a-0b7e289856db
UI               : System.Management.Automation.Internal.Host.InternalHostUserInterface
CurrentCulture   : en-US
CurrentUICulture : en-US
PrivateData      : Microsoft.PowerShell.ConsoleHost+ConsoleColorProxy
IsRunspacePushed : False
Runspace         : System.Management.Automation.Runspaces.LocalRunspace

PS C:\> $Host.Version

Major  Minor  Build  Revision
-----  -----  -----  --------
4      0      -1     -1

PS C:\> $Host.Version.Major
4
PS C:\>

The problem occurs when we’re using other hosting applications and under the belief $Host, or Get-Host, returns the version of PowerShell. The first instance that comes to mind, where we can see this problem, is when you’re using PSRemoting. If you’re not using the $PSVersionTable variable in a remote session, then you’re likely going to end up with incorrect results, when you try and get the version of PowerShell.

The example below uses an interactive session on a remote computer. $Host says we’re using version 1, but again, it’s reporting the version of the host (this time it’s named ServerRemoteHost). $PSVersionTable correctly indicates we’re using PowerShell 4.0. I’ve also returned the version of the operating system. Microsoft Server 2012 R2, by default, comes with PowerShell 4.0.

PS C:\> Enter-PSSession -ComputerName dc01
[dc01]: PS C:\Users\tommymaynard\Documents> Set-Location \
[dc01]: PS C:\> $Host.Version

Major  Minor  Build  Revision
-----  -----  -----  --------
1      0      0      0

[dc01]: PS C:\> $PSVersionTable.PSVersion

Major  Minor  Build  Revision
-----  -----  -----  --------
4      0      -1     -1

[dc01]: PS C:\> $Host

Name             : ServerRemoteHost
Version          : 1.0.0.0
InstanceId       : aed243b7-73ee-45c8-9032-2f2a5679f6ed
UI               : System.Management.Automation.Internal.Host.InternalHostUserInterface
CurrentCulture   : en-US
CurrentUICulture : en-US
PrivateData      :
IsRunspacePushed :
Runspace         : System.Management.Automation.Runspaces.LocalRunspace

[dc01]: PS C:\> (Get-CimInstance -ClassName Win32_OperatingSystem).Caption
Microsoft Windows Server 2012 R2 Standard
[dc01]: PS C:\> Exit-PSSession
PS C:\>

Use $PSVersionTable to make sure you capture the proper version of PowerShell – not the version of the host, that’s hosting PowerShell. If this is one of your bad habits, then change it now, before you wish you had.

Add CMD’s ver to PowerShell

One of the great things about Windows PowerShell is that it can run Windows native command line tools, such as ping, ipconfig, and others. There are times, however, when there are exceptions. While recently working in the console, I brainlessly entered ‘ver’ (without the quotes), and it didn’t return what I expected. Instead of printing ‘Microsoft Windows [Version 6.3.9600]’ to the console, it reported that ver wasn’t recognized.

PS C:\> ver
ver : The term 'ver' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the
spelling of the name, or if a path was included, verify that the path is correct and try again.
At line:1 char:1
+ ver
+ ~~~
    + CategoryInfo          : ObjectNotFound: (ver:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException

While I wouldn’t recommend opening CMD to run the command, because seriously get out of that habit already, you can switch to CMD from inside the PowerShell console, as demonstrated below.

PS C:\> cmd
Microsoft Windows [Version 6.3.9600]
(c) 2013 Microsoft Corporation. All rights reserved.

C:\>ver

Microsoft Windows [Version 6.3.9600]

C:\>exit
PS C:\>

Still, this wasn’t quite as native as I wanted. For whatever reason, I wanted to type ver, and get the identical output I was used to seeing,… regardless of the fact that the ver output doesn’t really tell me much.

The first thing I did was launch the ISE and create an alias for ver. Keep in mind, that you may need to add -ErrorAction SilentlyContinue to the New-Alias cmdlet, if you run the script more than once inside the ISE. The problem here is that you’ll receive an error if you try and create an alias that is already being used. Either that, or you can add the -Force parameter and make New-Alias act like Set-Alias (and overwrite the alias even though it’s not really changing it).

New-Alias -Name ver -Value Get-TMVersion

Next, I constructed a function called Get-TMVersion that the alias ver would use. Inside the function, I created a single variable, $OS, and assigned it the results of a Get-CimInstance command. If for some reason you’re still using PowerShell 2.0, this can be replaced by the Get-WmiObject equivalent: Get-WmiObject -Class Win32_OperatingSystem.

Function Get-TMVersion {
    $OS = Get-CimInstance -ClassName Win32_OperatingSystem
}

Once I have this data stored in a variable, I can begin checking for a value in the variable, and then, building out my results to match the native Windows command. The If statement checks to see if the Name property of $OS begins with the string Microsoft Windows. Providing it does, it sets a second variable, $Version, as seen in the example below. Once that’s complete, it then echos a blank line, the $Version variable, and then echos a second blank line. At this point, $Version should be identical to the ver command’s standard output.

Function Get-TMVersion {
    $OS = Get-CimInstance -ClassName Win32_OperatingSystem
    If ($OS.Name -like 'Microsoft Windows*') {
        $Version = "Microsoft Windows [Version $($OS.Version)]"
    }
    Write-Output -Verbose `r`n$Version`r`n
}

Here’s the function in action.

PS C:\> ver

Microsoft Windows [Version 6.3.9600]

PS C:\>

Boring, but it works. I should note that this function would probably be best served to include some error checking, in case setting the $OS variable errors out. In addition, I suspect there are probably other ways to produce the same resul–… (pause, keyboard keys clicking) …ugh, here’s a couple variations of yet another, simpler way.

PS C:\> cmd /c ver

Microsoft Windows [Version 6.3.9600]
PS C:\> cmd /c ver;echo ''

Microsoft Windows [Version 6.3.9600]

PS C:\>

It seems I could have just dropped a tiny bit of text into my function and called it a day. If you’ve ever read anything else I’ve written and posted, then you may have noticed a pattern. I seem to do things the hard way, long before I figure out a simpler way. I like it that way, though. If anything, it keeps me thinking, and therefore, improving my PowerShell skills overall.

Function Get-TMVersion {
    cmd /c ver
    Write-Output -Verbose ''
}

Get All 13 Months in a Year

I started a little, pet project recently and it’s all based around how I store my pictures and video files. I have a base directory – C:\Users\tommymaynard\Documents\Media – that contains a subdirectory for each year: 2012, 2013, etc. Inside each of those, I have 12 other directories – one for each month. These each use the naming convention 01 – January, 02 – February, etc. Inside each of those directories, I have at least two other directories: pictures and videos. Here’s an image that may help this structure make more sense.

Get All 13 Months in a Year01

I don’t always sort my photos and videos right away. Instead, I’ll often procrastinate and just drop them into a ‘Pics to Sort’ folder on my desktop – that’s literally the directory’s name. I wondered if I would procrastinate any less, if I had Windows PowerShell build out the desired directory structure for me each year. As an aside, if you consider that I always add my pictures and videos to the same I’ll-sort-you-later directory, then I could easily build a solution to do the sorting, based on file extension and creation date, from this directory into the proper subdirectories in the media directory. I’ll save this one for another day, but thanks for thinking of it.

For this Quick Learn, let’s only concern ourselves with capturing the month’s names. While I could have created a hard-coded array of the month names, I decided to see what I could get return using Get-Date – not what I wanted. I could return the current month’s name using Get-Date -Format MMMM, or the current month’s numeric representation using Get-Date | Select-Object Month, but neither of these return all of the months. The system knows the names of the months and so I opted to look for a .NET way to get them.

So, here it is, the System.Globalization.DateTimeFormatInfo .NET class. When you enter this as the value of the New-Object’s -TypeName parameter, it will return a great deal of information, to include a property called MonthNames. In the example below, you can see the code used to store the month names in a variable. You would think I would be done here, but I wasn’t.

PS C:\> $MonthNames = (New-Object System.Globalization.DateTimeFormatInfo).MonthNames
PS C:\> $MonthNames
January
February
March
April
May
June
July
August
September
October
November
December

PS C:\>

Instead of creating directories, like my project does, the example below simply echos the month’s numeric value, a space-dash-space, and then the month’s name. I’ve added a for statement example as well, just in case someone were to wonder why I didn’t include one, since I was incrementing a counter variable and knew how many looping iterations I had to do.

PS C:\> $i = 1
PS C:\> $MonthNames | foreach {Write-Output "$i - $_";$i++}
1 - January
2 - February
3 - March
4 - April
5 - May
6 - June
7 - July
8 - August
9 - September
10 - October
11 - November
12 - December
13 -
PS C:\>
PS C:\> for ($i=1; $i -le $MonthNames.Count; $i++) {echo "$i - $($MonthNames[$i-1])"}
1 - January
2 - February
3 - March
4 - April
5 - May
6 - June
7 - July
8 - August
9 - September
10 - October
11 - November
12 - December
13 -
PS C:\>

Based on these results, the MonthNames property has a thirteenth entry, and that’s a problem, since there’s no thirteenth month. I proved to myself that there was a problem a couple different ways, as seen below. Remember that the first index, or element, of an array is index zero, making January $MonthNames[0] and December $MonthNames[11].

PS C:\> $MonthNames.Count
13
PS C:\> $MonthNames[0]
January
PS C:\> $MonthNames[11]
December
PS C:\> $MonthNames[12]

PS C:\> $MonthNames[13]
PS C:\>

I needed to ensure this extra, blank month wasn’t making its way into my code. While I could have done my filtering when I was actually creating new directories, it is a better practice to only store what I need, and to only store what’s correct. As can be seen below, I only returned twelve months by adding a pipeline and some filtering.

PS C:\> $MonthNames = (New-Object System.Globalization.DateTimeFormatInfo).MonthNames | Where-Object -FilterScript {$_ -notlike ''}
PS C:\> $MonthNames
January
February
March
April
May
June
July
August
September
October
November
December
PS C:\> $MonthNames.Count
12

Since I was already here, I checked out another property. DayNames didn’t have a blank entry, and had I needed to use it, I wouldn’t have had to do any filtering on the returned results – something I incorrectly assumed about MonthNames. Hope this is helpful to someone, and thank you for reading this post.

PS C:\> $DayNames = (New-Object System.Globalization.DateTimeFormatInfo).DayNames
PS C:\> $DayNames
Sunday
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
PS C:\> $DayNames.Count
7
PS C:\>

Converting to UTC Time (Thanks, SharePoint)

Earlier this year, I created and implemented a constrained, PowerShell endpoint on two SharePoint Central Admin servers. The purpose was to allow non-administrative users the ability to remotely connect via PSRemoting, or PowerShell Web Access, to create new SharePoint Site Collections (SCs). By default, I provided a few Get- SharePoint cmdlets, as well as, the New-SPSite SharePoint cmdlet. A month or so after requesting and then receiving the Set-SPSite cmdlet, the team using the endpoint, requested another cmdlet: Remove-SPSite – a cmdlet that made me a little leery. While I trusted the people who would use the cmdlet, I needed to sleep at night, and so I needed to add some precautions.

So, what to do? I created a “proxy” function in place of the SharePoint Remove-SPSite cmdlet. While the cmdlet wouldn’t be available, the function would be. The best part about writing your own function, for inclusion in a constrained endpoint, has to be the ability to make use of cmdlets in the function, that are not available in the endpoint. The Remove-SPSite cmdlet is used in my function, even when it isn’t available to the people that use the endpoint. Simply, badass.

While I’m not going to share the complete function, I will explain the logic I decided to use. I allowed people using the endpoint, the ability to delete SCs under one condition – that the SC had been created within the last six hours. This helped protect any SCs that had been created prior to six hours ago. To do this, I decided I would simply get the current date and compare it to the created date of the SC – small problem.

First, let’s return the SC that I created.

PS C:\> Get-SPSite -Identity https://teamsite.sp.mydomain.com

Url                                                     CompatibilityLevel
---                                                     ------------------
https:/teamsite.sp.mydomain.com                         15

Now, let’s return the time when the SC was created. The problem, in the example below, is that I didn’t create the site at 2 a.m. From my point in time, that was 7 hours in the future. With the help of a coworker, I realized that the time being used by SharePoint on the back end, was Coordinated Universal Time, or UTC, regardless that all the other SharePoint times were set for my time zone.

PS C:\> (Get-SPSite -Identity https://teamsite.sp.mydomain.com).RootWeb.Created

Monday, November 17, 2014 2:05:10 AM

My first thought was that this wasn’t going to be an effective way to protect the SCs, until I remembered, I can convert the current time. The easiest way to do this was to use the DateTime object’s method, .ToUniversalTime(). That’s right, take the current time, convert it to Universal Time, and then use that for comparison.

In the example below, we do several things. The first three lines set some variables. In line 1, we capture the SC to a variable. In line 2, we create the variable that holds the numeric value of hours we are willing to allow, and in line 3 we create the DateTime object in UTC that we use to compare against the SharePoint SC.

$SPSite = Get-SPSite -Identity https://teamsite.sp.mydomain.com
$Hours = 6
$TimeHoursAgo = ((Get-Date).ToUniversalTime()).AddHours(-$Hours)

If ($SPSite.RootWeb.Created -gt $TimeHoursAgo) {
    Remove-SPSite -Identity $SPSite -Confirm:$True
} Else {
    Write-Warning -Verbose "Unable to delete Site Collection: $($SPSite.Url) (Over $Hours hours old))."
}

The rest of the example does the comparison between the DateTime object we’ve created and the created date of the SC. If the SC was created after our $TimeHoursAgo variable (meaning that it is greater than $TimeHoursAgo), then it can be deleted, and if it wasn’t, then it cannot be deleted.

Notice the .AddHours() method, above, is using a negative sign before the $Hours variable – this is used to move back in time. Also, pay attention to the embedded parenthesis as the value $TimeHoursAgo is being assigned. They indicate the order in which the command is executing, beginning with the most embedded set. You can ignore the parenthesis that are used as a part of the methods, such as .ToUniversalTime(), even if they contain something between the parenthesis. I’ve included an example of what that line would look like had it been written procedurally.

PS C:\> $Hours = 6
PS C:\> $TimeHoursAgo = Get-Date
PS C:\> $TimeHoursAgo = $TimeHoursAgo.ToUniversalTime()
PS C:\> $TimeHoursAgo = $TimeHoursAgo.AddHours(-$Hours)

Keep in mind that various Microsoft technologies may use UTC on the back end, even if the GUI shows your proper time zone.

about_Profiles

This post is the help rewrite for about_Profiles. While the help files for Windows PowerShell are invaluable, the idea behind a rewrite is so true beginners might even better understand the help file concepts. At times, some things discussed in the Windows PowerShell help file will not be included in a help rewrite. Therefore, it is always best to read the actual help file after reading this post. (PS3.0)

Once you start using your profile ($PROFILE), you’ll have a hard time not using it and not adding new things to it. When you have a profile (script), it runs each time you open a new Windows PowerShell session. My current profile does a number of things – it sets my location to the C:\ drive, sets various variables, set aliases, modifies the console’s window title, and creates several functions. Some of those functions allow me to connect remotely to Exchange servers, load the VMWare PowerCLI PSSnapin (without having to remember its name), use Wake-on-LAN to wake my home computer, and several others.

There are several different profiles based on the host and the user. To see your profile, type $PROFILE and press Enter in the console or Integrated-Scripting Environment (ISE). As seen in the example below, this will display the path to the profile. Line 1 is typed in the standard PowerShell console and Line 3 in the ISE.

PS C:\> $PROFILE
C:\Users\tommymaynard\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1
PS C:\> $PROFILE
C:\Users\tommymaynard\Documents\WindowsPowerShell\Microsoft.PowerShellISE_profile.ps1

There are more profiles than these two and they are active under different circumstances. To see all the profiles, pipe the $PROFILE variable to the Select-Object cmdlet with the wildcard character.

PS C:\> $PROFILE | Select-Object -Property *

AllUsersAllHosts       : C:\Windows\System32\WindowsPowerShell\v1.0\profile.ps1
AllUsersCurrentHost    : C:\Windows\System32\WindowsPowerShell\v1.0\Microsoft.PowerShell_profile.ps1
CurrentUserAllHosts    : C:\Users\tommymaynard\Documents\WindowsPowerShell\profile.ps1
CurrentUserCurrentHost : C:\Users\tommymaynard\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1
Length                 : 82

Based on the information on the left hand side, there are different profiles based on who is using what host. While it’s not recommended to modify the profiles in the System32 directory, you should be able to tell what these do. The one on line1 is run for any user, regardless of what host (console/ISE) they use. The second one on line 2 is for any user in the current host. The third is for the current user, me, in all the hosts, and the final one is for me in the current host. These last two are the one’s you can modify.

But just by having a path stored in $PROFILE, doesn’t actually mean the file, or profile, actually exists. Use the Test-Path cmdlet to determine if the file/profile exists.

PS C:\> Test-Path $PROFILE
False

If this returns False, then you do not have a profile and so it will need to be created. You can create this file but using the New-Item cmdlet.

PS C:\> New-Item -Type File -Path $PROFILE -Force

    Directory: C:\Users\tommymaynard\Documents\WindowsPowerShell

Mode                LastWriteTime     Length Name
----                -------------     ------ ----
-a---         8/20/2014  12:39 PM          0 Microsoft.PowerShell_profile.ps1

These two commands can be put together in an If-Else statement that will test for the file, and if not found, will create the file. Once you have a profile file created, you can open it up and start adding to it. Remember that every time you open the matching console, the profile script will run.

If (-not(Test-Path $PROFILE)) {
    New-Item -Type File -Path $PROFILE -Force
}

Here’s some quick examples of things that could be added to a profile. The first example  changes the prompt location from its default to the root of the C:\ drive.

Set-Location \

The next example sets aliases for c and gh. The first example, in line 1, will allow me to use the letter c to run the Clear-Host cmdlet and in line 2, will allow me to use the alias gh in place of typing out Get-Help.

Set-Alias -Name c -Value Clear-Host
Set-Alias -Name gh -Value Get-Help

These next example allow me to set variables inside my profile. This will allow me to use $DCs to return DC01, DC02, DC03, allow me to use $hosts to return the path of my hosts file, and allow me to return all my web and data servers by using the $AppServers variable. Noticed that $AppSevers is a mulit-dimentional array. Use $AppServers[0] to return the web servers and $AppServers[1] to return the data servers.

Set-Variable -Name DCs -Value 'DC01','DC02','DC03'
Set-Variable -Name hosts -Value "$env:SystemRoot\System32\drivers\etc\hosts"
Set-Variable -Name AppServers -Value @(('web01','web02','web03'),('data01','data02','data03'))

You can also create functions. This function allows me to type Add-VMC to load the VMware PSSnapin. For me, it’s easier to remember this short function name than remembering the PSSnapin name or having to type out Get-PSSnapin -Registered to find the name.

Function Add-VMC {
	Add-PSSnapin VMware.VimAutomation.Core
}

Start using your $PROFILE today and everything you can to help personalize your PowerShell experience. Keep in mind that profiles do not exist in remote sessions.

Learn More

This information, and more, is stored in the help file about_Profile that comes with Windows PowerShell. This information can be read by typing any of the commands below. The first example will display the help file in the Windows PowerShell console, the second example will open the full help in it’s own window, the third example will send the contents of the help file to the clipboard (so it can be pasted into Word, Notepad, etc.), and the fourth example will open the help file in Notepad.

PS C:\> Get-Help about_variables
PS C:\> Get-Help about_variables -ShowWindow
PS C:\> Get-Help about_variables| clip
PS C:\> Notepad $PSHOME\en-us\about_Variables.help.txt

Using Replace() to Fix Split() (and Convert-Path)

I was working on a recent project that required scripting ACLs, and so I had a Windows PowerShell console open in addition to the ISE. This allowed me to quickly check the owner of a directory (or folder). I could press the up arrow to rerun my command, and I could quickly see if the owner had changed according to my script. Let’s say my directory is called ‘TestFolder’ and is located at the root of the C:\ drive.

PS C:\> New-Item -Path TestFolder -ItemType Directory

    Directory: C:\

Mode                LastWriteTime     Length Name
----                -------------     ------ ----
d----        10/21/2014   9:19 PM            TestFolder

To get the ACL information for the folder, you can use the Get-Acl cmdlet, as in the example below.

PS C:\> Get-Acl -Path .\TestFolder

    Directory: C:\

Path                                    Owner                                   Access
----                                    -----                                   ------
TestFolder                              BUILTIN\Administrators                  BUILTIN\Administrators Allow  FullCo...

Being the PowerShell enthusiast that I am, I modified my command so that only the information I wanted (the path and the owner) was being returned. This is done using the Select-Object cmdlet. Unfortunately, when the command returned those two properties, the Path property was no longer what I was expecting – take a look below. While the example above only returned the name of the folder (TestFolder), I thought I would change this to show the full path (C:\TestFolder) since I was now dealing with a string that included it.

PS C:\> Get-Acl -Path .\TestFolder | Select-Object -Property Path,Owner

Path                                                        Owner
----                                                        -----
Microsoft.PowerShell.Core\FileSystem::C:\TestFolder         BUILTIN\Administrators

The first thing I thought to do was to split the path at the two colons (::) and grab the second element, which I thought would end up being C:\TestFolder. The split method didn’t work so well; here’s what I ended up with.

PS C:\> Get-Acl -Path .\TestFolder | Select-Object -Property @{N='Path';E={($PSItem.Path).Split('::')[-1]}},Owner

Path                                                        Owner
----                                                        -----
\TestFolder                                                 BUILTIN\Administrators

The reason this didn’t work is because the Split() method doesn’t split on each occurrence of two, back-to-back colons like you might expect, it splits on every colon. Since C:\ has a colon, it split there as well. FYI: The use of [-1] returns the last element in an array – good to know, I know. Here’s an example that may help better explain. In this example below, the string is split on every exclamation point (!) and every question mark (?) – not only on the combination of both (!?).

PS C:\> $String = 'Today is the 21st! That is great news, right?'
PS C:\> $String
Today is the 21st! That is great news, right?
PS C:\> $String.Split('!?')
Today is the 21st
 That is great news, right

PS C:\>

What I then decided to do to get this the way I wanted it, was to first replace the two, back-to-back colons with a single character (that was not a part of the string), and then split on that single character. It worked, and here’s what that looks like.

PS C:\> Get-Acl -Path .\TestFolder | Select-Object -Property @{N='Path';E={(($PSItem.Path).Replace('::','@')).Split('@')[-1]}},Owner

Path                                                        Owner
----                                                        -----
C:\TestFolder                                               BUILTIN\Administrators

PS C:\>

It was about this point, that I wanted to see if the -split operator would have handled this the same way and required the additional work that the Replace()/Split() methods did. Of course, after all I did to get this to work how I wanted, I determined I should have started with the -split operator. The -split operator isn’t looking at the characters individually, but instead of, as a whole – two, back-to-back colons is two back-to-back colons.

PS C:\> Get-Acl -Path .\TestFolder | Select-Object -Property @{N='Path';E={(($PSItem.Path) -split '::')[-1]}},Owner

Path                                                        Owner
----                                                        -----
C:\TestFolder                                               BUILTIN\Administrators

A note, if the @{N=…;E={…}} syntax is confusing, or new to you, then spend some time reading this: http://technet.microsoft.com/en-us/library/ff730948.aspx, and then run Get-Help about_Hash_Tables.

This happens just about every time I get to ready to publish a new post. I discovered a better way to handle the problem – much like I did when I considered the -split operator.  It turns out that I could have used a built-in cmdlet to convert the path for me. That’s right, there’s a cmdlet that would have handled everything. Using the Convert-Path cmdlet will convert something like this: Microsoft.PowerShell.Core\FileSystem::C:\TestFolder to this: C:\TestFolder. Here’s the example.

PS C:\> Get-Acl -Path .\TestFolder | Select-Object -Property @{N='Path';E={Convert-Path $PSItem.Path}},Owner

Path                                                        Owner
----                                                        -----
C:\TestFolder                                               BUILTIN\Administrators

PS C:\>

Well, that’s it for this one. As much as I might seem irritated about how I did this three different ways – from the most work to the least – I understand how important this learning process is, and that one day I will be grateful for having gone down this path… (pun intended).

Clear-Host, Without Clearing the Host

After you read this, read part 2 (and download the TMModule)

I use the Clear-Host cmdlet alias, cls, throughout the day to clear out whatever typing I have inside my Windows PowerShell console. It does its job well, but recently I’ve wanted it to work differently. I wanted it to appear that the console host has been cleared, but still allow me to scroll back up to see what was on the screen before it was cleared. I started playing around with the console class, [Console]. While not necessary, this can also be written using the namespace, System, such as [System.Console]. I like the idea of being as complete as possible and so you’ll see me use the namespace even though it’s not necessary.

Before I could write something reusable, such as a function, I had to figure out if what I wanted to accomplish, was even possible. I knew I was working with [System.Console] and so I piped that to Get-Member, but it returned the methods and properties of System.RuntimeType, seen below.

PS C:\> [System.Console] | Get-Member

    TypeName: System.RuntimeType

I struggled for a moment until I remembered an article I had read on using static classes. I found that page again, http://technet.microsoft.com/en-us/library/dd347632.aspx, and was quickly reminded that using the -Static parameter of the Get-Member cmdlet would get me the correct results.

PS C:\> [System.Console] | Get-Member -Static

    TypeName: System.Console

Running the command above produces the TypeName as shown, but it also produces all the methods and properties. I started looking over the properties and a couple about the cursor quickly caught my eye, especially the CursorTop property. After the Get-Member command from above, and based on the results of returning the CursorTop property, my cursor was positioned at line 59 inside my console, as can been seen in the example below on line 2. I cleared the screen, and beginning on line 4 below, I reran the command three more times. Each time, it gave me the location where the cursor was last positioned.

PS C:\> [System.Console]::CursorTop
59
PS C:\> cls
PS C:\> [System.Console]::CursorTop
1
PS C:\> [System.Console]::CursorTop
3
PS C:\> [System.Console]::CursorTop
5
PS C:\>

I decided I would assign the value, 0, to the CursorTop property and suddenly I was writing over the text on the top line. Take a close look at line 1 below.

PS C:\> blahblahConsole]::CursorTop
59
PS C:\> cls
PS C:\> [System.Console]::CursorTop
1
PS C:\> [System.Console]::CursorTop
3
PS C:\> [System.Console]::CursorTop
5
PS C:\> [System.Console]::CursorTop = 0

I could move my cursor, great, but this wasn’t exactly what I wanted. What I wanted was to push that scroll bar down so that anything that was already on the screen was pushed off the top of my console, and all that was left was my PowerShell prompt. I still believed there was a way to do this and so I spent a little more time looking over the properties. I found four that began with Window – WindowHeight, WindowLeft, WindowTop, and WindowWidth – and began to experiment with them. I didn’t suspect I’d be doing anything with the height and width but I thought I would check out their values anyway – 50 and 120, respectively.

PS C:\> [System.Console]::WindowHeight
50
PS C:\> [System.Console]::WindowWidth
120
PS C:\>

WindowLeft didn’t seem to be that important, because no matter how much was typed before I entered [System.Console]::WindowLeft, the property value was still set to 0. Then I entered in [System.Console]::WindowTop and it was also 0 every time. Then it dawned on me, what if I changed its value like I did with CursorTop. I tried it and my scroll bar started jumping all over. I’m getting close!

PS C:\> [System.Console]::WindowTop = 10
PS C:\> [System.Console]::WindowTop = 200
PS C:\> [System.Console]::WindowTop = 2000
PS C:\> [System.Console]::WindowTop = 0

We know the CursorTop value changes, so what would happen if we set the value of WindowTop to CursorTop? I tried it, and it worked!

PS C:\> [System.Console]::WindowTop = [System.Console]::CursorTop
PS C:\>

I thought I was done when I took another moment and scanned over the methods. I found one called SetWindowPosition. Instead of simply assigning a new value to the property WindowTop, I decided I would use the method to do the work for me. I eventually ran both of these options through the Measure-Command cmdlet and determined that there was no gain in speed by using one option over the other.

So, once I knew what to do, I opened my profile ($PROFILE) and created an empty function. For whatever reason, I called it clx thinking that this would be a good option for me. Turns out that while clx has little meaning, I was able to quickly remember it and start using it right away. Now, every time I want it to appear that I’ve cleared my host, but didn’t really, I type clx and press Enter.

Function clx {
    [System.Console]::SetWindowPosition(0,[System.Console]::CursorTop)
}

I added one additional feature to this function as is seen in the example below. This option allowed me to run the clx function and leave the last n number of rows on the screen. Try it out by ensuring your have some output in your console and then entering clx 2. This will “clear” the console screen but still allow you to view the last two rows without scrolling back up. Try it and it may make more sense.

Function clx($SaveRows) {
    If ($SaveRows) {
        [System.Console]::SetWindowPosition(0,[System.Console]::CursorTop-($SaveRows+1))
    } Else {
        [System.Console]::SetWindowPosition(0,[System.Console]::CursorTop)
   }
}

Here’s a video of the function in action. The first thing we do is return 5 processes and then 5 services. Then we use cls and notice that we cannot scroll back up to see what was cleared. This is the typical behavior. When we add the processes and services back, and then use the clx function, we can see that we have the option to scroll back up and see what was on the screen, before we “cleared” it.

Cmdlets of the Same Name (VMware & Hyper-V)

Update: This post was submitted to PowerShell.org for their PowerShell TechLetter. For the most update to date version of this post, please read it here: http://powershell.org/techletter/issues/2015-01-january.php#Article1

One of the first things I did when I moved to Windows 8.1 (and Windows PowerShell 4.0, by default), was to add the Hyper-V feature. If you didn’t already know, yes, you can run virtual machines in Windows 8.1 without the need for third-party software. I can’t be 100% certain, but I’m fairly certain I installed this feature a year ago on Windows 8, as well. In both operating systems, you will need to meet some hardware requirements to install Hyper-V, otherwise the feature will be grayed out. In addition, it may not even be listed in non-Pro and non-Enterprise versions of Windows 8.1.

Adding Hyper-V was twofold. One, I wanted to gain experience with the Hyper-V cmdlets and two, I wanted to add at least a couple virtual machines to my computer – Windows 8.1 to test PowerShell 5.0, and Windows 7 with PowerShell 3.0. After a day or so of playing with Hyper-V, it occurred to me that my future work with PowerCLI, VMware’s vSphere PowerShell PSSnapin, meant there would be name overlap between cmdlets in Hyper-V and cmdlets in VMware. This includes popular cmdlets, such as Get-VM and Get-VMHost.

In previous experience, I had learned about command precedence (Get-Help about_Command_Precedence). These are the precedence rules that are used when a command runs. If we have an alias, a function, a cmdlet, and a native Windows command, all with the same name, it will run the alias first when the name is entered into the PowerShell console. If we don’t have an alias with that name, but do have a function, a cmdlet, and native Windows command, then it will run the function. If the name is the same, but the command type is different, then they will always run in order from alias to function to cmdlet to native Windows command.

If we have two of the same command type and they have the same name, such as the cmdlet Get-VM, it will run the one that was added most recently. This is unless we provide the cmdlet a path. For instance, if the Hyper-V module was loaded and then the VMware PSSnapin was loaded, when we run Get-VM it will run the cmdlet from VMware. If we wanted to use the Get-VM cmdlet from the Hyper-V module, we would need to enter the full path that includes the module name: Hyper-V\Get-VM. Just because one cmdlet was loaded more recently, doesn’t mean the other one is gone.

In my mind, there’s a couple ways to fix the problem, or rather, make it easier to use cmdlets with the same name. The first option I tried, was to determine if the Add-PSSnapin cmdlet, that is used by VMware, had a -Prefix parameter. It didn’t, but had it, I could have added a prefix to all the VMware cmdlets with an option like the example below.

Add-PSSnapin -Name VMware.VimAutomation.Core -Prefix VMware

This doesn’t actually work. Read the post.

An option like this wouldn’t have clobbered my Hyper-V cmdlets, and therefore, Hyper-V’s Get-VM cmdlet would still work without a path, and VMware’s Get-VM cmdlet, for example, would have been Get-VMwareVM. The Import-Module cmdlet does have a -Prefix parameter so I could have changed the Hyper-V cmdlets to use a prefix, but I didn’t think the Hyper-V cmdlets should be the ones to suffer. I’m a PowerShell enthusiast, and I didn’t want to change the naming of Microsoft-built cmdlets, and therefore, learn them incorrectly.

Here’s what I did. I added a function to my profile ($PROFILE) that would allow me to choose which set of cmdlets would be loaded first, and which would be loaded second. The cmdlets I loaded second wouldn’t need the path to use them. This meant I could set the Hyper-V cmdlets to not need a path when I work with those, or I could set VMware cmdlets to not need a path when I work with those.

I started by declaring an empty function called Add-VMCs (Virtual Machine Cmdlets) that included a single parameter called Default.

Function Add-VMCs($Default) {

}

I started the function with an If statement. On line 2, below, it checks the parameter that has been provided when calling the function. If it matches the letter h or the letter v it will continue to line 3. If it does not, it will jump to line 7 and run the Else portion – displaying a message that nothing was changed and what parameters can be used. Line 3 and 4 remove the Hyper-V module and VMware PSSnapin, whether or not they are already loaded. While there could have been some logic to first check if they are loaded, I decided I was fine with hiding any errors that might occur (-ErrorAction SilentlyContinue) if I tried to unload a module or PSSnapin that wasn’t already loaded. I usually handle errors better than this, but I didn’t think it was necessary for this function.

The reason this is required is because if we try to load an already loaded module or PSSnapin, it won’t actually bother doing it, or at least that’s what I think is going on. This would prevent the function from ensuring the module and PSSnapin were loaded in my preferred order. Remember, if cmdlet names are the same, the most recently loaded cmdlet will be the one that is used.

Function Add-VMCs($Default) {
    If ($Default -eq 'h' -or $Default -eq 'v') {
        Remove-Module -Name Hyper-V -ErrorAction SilentlyContinue
        Remove-PSSnapin -Name VMware.VimAutomation.Core -ErrorAction SilentlyContinue

    } Else {
        Write-Output -Verbose "INFO: No changes made`r`nUse H to set Hyper-V as the default (Add-VMCs H) or use V to set VMware as the default (Add-VMCs V)."
    }
 }

Like we said previously, if the parameter is equal to the letter v or the letter h, it will remove the module and PSSnapin. The function continues to an If-ElseIf statement that begins on line 5. This bit of logic takes different actions depending on the value of the parameter. If the parameter is equal to the letter h, it will load the VMware PSSnapin and then import the Hyper-V module. This means that Get-VM would be the cmdlet associated with Hyper-V. If it is not equal to the letter h, and instead it is equal to the letter v, then it will load the Hyper-V module and then the VMware PSSnapin. This means that Get-VM would be the cmdlet associated with VMware.

Function Add-VMCs($Default) {
    If ($Default -eq 'h' -or $Default -eq 'v') {
        Remove-Module -Name Hyper-V -ErrorAction SilentlyContinue
        Remove-PSSnapin -Name VMware.VimAutomation.Core -ErrorAction SilentlyContinue
        If ($Default -eq 'h') {
            Add-PSSnapin -Name VMware.VimAutomation.Core
            Import-Module -Name Hyper-V
        } ElseIf ($Default -eq 'v') {
            Import-Module -Name Hyper-V
            Add-PSSnapin -Name VMware.VimAutomation.Core
        }
    } Else {
        Write-Output -Verbose "INFO: No changes made`r`nUse H to set Hyper-V as the default (Add-VMCs H) or use V to set VMware as the default (Add-VMCs V)."
    }
}

Here’s the function in action. At first I set the Hyper-V cmdlets to be the default on line 1. I then verify that my cmdlet is from the correct module by using the Get-Command cmdlet. Once that’s been verified, I run the Get-VM cmdlet on line 8. Then, I do it all again after I change the default to VMware. Note: If you’ve ever used VMware’s PowerCLI then you know I had to use the Connect-VIServer cmdlet to connect to a vCenter system before it would allow me to run the VMWare Get-VM cmdlet.

PS C:\> Add-VMCs -Default H
PS C:\> Get-Command Get-VM

CommandType     Name                                               ModuleName
-----------     ----                                               ----------
Cmdlet          Get-VM                                             Hyper-V

PS C:\> Get-VM -Name Win8.1*

Name        State   CPUUsage(%) MemoryAssigned(M) Uptime   Status
----        -----   ----------- ----------------- ------   ------
Win8.1PS5.0 Running 23          1536              00:26:20 Operating normally

PS C:\> Add-VMCs V
PS C:\> Get-Command Get-VM

CommandType     Name                                               ModuleName
-----------     ----                                               ----------
Cmdlet          Get-VM                                             VMware.VimAutomation.Core

PS C:\> Get-VM -Name Windows*

Name                 PowerState Num CPUs MemoryGB
----                 ---------- -------- --------
Windows 2003 R2 S... PoweredOff 1        1.000

Linked from here:
http://blogs.technet.com/b/heyscriptingguy/archive/2014/09/04/powertip-use-complete-name-for-powershell-cmdlet.aspx