Saturday, 25 April 2015

Returning Only Matching Text With Select-String

Let’s face it - Select-String can be a maddeningly frustrating cmdlet.  I think a lot of people with a DOS or VbScript background expect it to work like findstr which just returns the matching text you were looking for.  Select-String, on the other hand, is ever so helpful and returns the file name, the line number of the match and THEN the matching text all in one line.  I remember banging my head against the wall when I first tried to figure it out so here it is, recorded for posterity.

You’re Giving Me Too Much Information

Let’s take a look at the overly helpful information returned by default.  We’re looking for the string “PickMe” in a file called testfile.txt which has the following lines in it:

PickMeImTheStringYouWant
IgnoreMe
IgnoreMeToo
PickMeImTheStringYouWantAsWell

This is what we get returned when we run Select-String looking for the pattern “PickMe”:

image

Goodness, that’s a lot of info.  Problem is, I already know the file name since I used it in the command and I don’t really care about what line my matches are on – I just want to know if there were any matches and what they were!

This is how we get only the information we’re after:

image

That’s all there is to it - the Line property is the key!  There are other properties you can use as well although I find Line is the one I use almost all the time.  To see the other methods and properties, run the command shown below:

image

Monday, 20 April 2015

Efficient AD Queries

The cmdlets Get-AdUser and Get-ADComputer have a Filter parameter which is very powerful and can make a significant difference in script execution time.  Let’s take a look at how to use this parameter and what happens if you don’t (hint – it’s not good).

Spot the Difference

Take a look at these two lines of code.  They look quite similar at first glance but one is a beautiful one-liner and the other is a nightmare of inefficiency as you can see by their execution times.  The efficient one took 212 milliseconds while the inefficient one took 58 seconds!  In other words, the blink of an eye vs almost a full minute to retrieve the exact same information.  I like this example because it demonstrates how, armed with the right knowledge, subtle differences in how you write your scripts can make a massive difference in how efficient they are.

image

image

So why the big difference?  In the first example, we initially retrieve every single user account from AD (signified by –Filter *) and then we pass it through the pipeline and get our target user identified.  The Active Directory database that I ran this against had about 20,000 users in it so you can see why the query took so long – the script first retrieved all of those accounts and then had to go through each one, one at a time, to find the matching account.

Compare this to the second example where, instead of using an asterisk for the filter parameter, we specify the parameters of our search without piping the results to anything else for further processing.  By doing this, we allow AD to do the filtering for us and only return the result we require.  Since Active Directory is a database built for efficiently handling queries, we get our result back in a fraction of a second.

Another thing to keep in mind with this kind of thing is the difference in load you’re placing on the domain controller executing the query.  The first script places a much greater load on the poor DC handling the request because it has to retrieve and pass back every single account.  In the second script, the DC handles the query in the way it was born to do and the result is only a fraction of a second of work.  The filter parameter is your friend – use it!

Sunday, 19 April 2015

The Definitive Page File Sizing Guide

How big should I make my page file?  It’s a question which has haunted humanity throughout the ages and for many of us the answer remains elusive.  Well fear not – in this post we will cover exactly what you need to know to figure out how big you should make your page file.

Why Should I Care?

Good question.  Let’s see why.  Back in the days of x86 computers with 4 GB of RAM, the standard practice was to make your page file some multiple of RAM in size.  For example, in your 4 GB server you could make it 1.5x the amount of RAM and set it to 6 GB.  But that formula becomes questionable when you start dealing with x64 servers with large amounts of RAM.  Does your SharePoint vm with 32 GB of RAM really need a 48 GB page file?  What if you have six of these SharePoint vm’s and they really only need a 4 GB page file but they each have a 48 GB page file?  That’s 264 GB of valuable SAN disk space you’re potentially wasting.  And besides, wouldn’t you just feel better knowing that you, the page file sizing wizard, used a logical and organized process to set the size of your page file rather than just using a random formula?

There’s one other benefit I should mention before we move on.  If you understand how to correctly size the page file you are well on your way to understanding the internals of Windows memory management and this is something you want to know well if your goal is to be the best Windows engineer/administrator out there.

Virtual and Physical memory – know the difference

The key to understanding page file sizing is to know the difference between physical and virtual memory.  Let’s get something important out of the way first – virtual memory has nothing to do with virtual machines.  Whether Windows is running on a physical hardware or a virtual machine, virtual memory is still present.  So keep that in mind when reading through this post because we’ll be talking a lot about virtual memory.

Also, when I talk about physical memory I’m not talking exclusively about RAM.  As we’ll see in this post, the page file is also included as part of physical memory.  So when I use the term physical memory I’m referring to the memory presented by the page file and RAM.

The first definition we need to cover is page.  A page is simply a chunk of memory.  Every process running in your system has a certain number of memory pages assigned to it, inside which its data is stored.  In addition, every page has an address and this address is used to identify and access each page.  An example of an address would be 0x03FF.

With that out the way, let’s look first look at a world without virtual memory.  In the figure below, we see a system with six pages of memory whose addresses range from A – F and whose size is 1 MB, for a total of 6 MB of RAM.  This is drastically simplified since most systems will have more RAM, much smaller pages sizes (e.g. 4 KB)  and the addresses would not be so simple.  However, for the purposes of illustrating these concepts this simplification is beneficial.

 
image

As you can see, Word has pages A, B and C, Excel has pages D and E, and Notepad has page F.  In this example, the processes are seeing the physical memory directly and being assigned pages directly as well.  In other words, when Excel attempts to write data to what it sees as page D, it really is modifying the contents of the page in RAM with address D.  Now that we’ve seen a physical memory-only example, let’s see how the introduction of virtual memory changes the picture.

Virtual Memory – It’s an Illusion

What virtual memory does is present a completely “fake” memory address space to each process.  It looks and acts like the RAM in our first example but it’s not!  For example, Word may think that it’s using page F but it may very well actually be using page A in physical memory.  Behind the scenes, Windows tracks this in a table so it knows which pages a process thinks it’s using and which pages it is actually using.  This is known as a page table. 

In combination with paging, we’re now able to present the memory provided by the page file as part of our available memory.  Remember that in our very simplified example above, each memory page is 1 MB in size and we have six of them for a total of 6 MB.  Now let’s say we add a 6 MB page file.  Now, we have an additional six pages of memory we can assign to our processes.  Without installing any extra RAM, we’ve increased the capacity of our system using significantly cheaper disk!

Now, you may be wondering – why go through this hassle?  There are some good reasons for using virtual memory and paging:

  1. Each process can get its own, large contiguous virtual address space.  This way, the memory pages that Word is accessing are isolated from the pages that Excel is accessing and there is less memory fragmentation. 
  2. You can run more processes concurrently thanks to the inclusion of the page file as available memory.  In our sad world without virtual memory, we were limited to the installed amount of RAM.  Once virtual memory and the page file came into the mix, we doubled our available memory without adding any RAM.

Let’s take a look at our address space with virtual memory in the mix.

image

There are some important things to note in this diagram:

  1. The number of usable pages in our virtual memory space is equivalent to the number of pages in our RAM and page file combined.  In other words, our total virtual memory available for processes to use is equal to the installed RAM plus the size of the page file.  Please read this statement over and over until you fully understand it because it’s central to what will come later on when we figure out how big to make our page file.
  2. The blue line indicates a barrier through which the processes on the system cannot see.  In other words, the actual physical memory represented by the page file and RAM is not visible to processes like it was in the first example – they can only see and interact with their own virtual address space with Windows handling the subsequent interaction with the other side of the barrier.

Putting It All Together

Now that we have virtual memory defined, let’s revisit our Word/Excel/Notepad scenario from above and see how it would look with virtual memory in play.

image

Let’s go over the important points in this diagram.

  • Notice how Word, Excel and Notepad have their own contiguous memory spaces.  Each of them has been presented with their own private virtual address space for their own use
  • There is no correlation between what pages a process is using in its virtual address space and what pages it is using in physical memory.  For example, Word is using pages A, B and C in its virtual space but pages B, F, and C in physical memory.
  • Two of Excel’s pages (E and F) are backed by the page file and not by RAM.  Remember, as far as the process is concerned, it’s just accessing memory – it has no idea that the physical pages backing its virtual pages are in the page file.
  • Remember the barrier represented by the blue line – the processes cannot see the physical memory they’re ultimately using and the mapping of what physical pages their virtual pages correspond to is handled by the OS in a page table.

 

This Is All Very Interesting, But How Does It Relate To Page File Sizing?

We finally have enough knowledge now to move onto the whole point of this post – how big does my page file need to be?  You could sum up the role of page file in the context of our discussion so far like this: it acts as the physical backing for the virtual memory which Windows has committed to make available to a process.  In other words, if Windows told Word that it could have 3 MB of memory, the following must happen:

  • Windows must give Word 3 MB of virtual memory
  • Windows must guarantee that there will be 3 MB of physical memory available to back the virtual memory, either in the page file or in RAM

Which brings us to our critical definitions.  These two terms are vital to understanding how to correctly size a page file.

  • Commit Limit – The total amount of virtual memory available for processes.  Equal to approximately the sum of the total amount of RAM and the size of the page file(s).
  • Commit Charge – The total amount of virtual memory which Windows has given to processes.  This number cannot exceed the Commit Limit – if it does, you have run out of virtual memory and you will start experiencing application crashes and general system instability.

Let’s relate these terms to our sample system from above.  Our Commit Limit would be 12 MB.  This is the sum of 6 MB of RAM and 6 MB of page file.  Our Commit Charge would be 7 MB, the sum of the 1 MB pages which are in use by Word, Excel and Notepad.

To solidify this important concept further, let’s look at a few more examples:

  • 12 GB RAM + 4 GB Page File = 16 GB Commit Limit
  • 64 GB RAM + 4 GB Page File = 68 GB Commit Limit
  • 48 GB RAM + No Page File = 48 GB Commit Limit

The Commit Charge and Commit Limit can be viewed in Task Manager under the System section.  In this screenshot, the 4 represents the Commit Charge and the 23 represents the Commit Limit.  In other words, on this particular system there was 4 GB of virtual memory assigned to processes out of a possible total of 23 GB (RAM + Page File).  How many times have you seen those numbers in Task Manager and wondered what they really mean?  Now you know!

image

So how do these terms relate to page file sizing?  Remember that the page file and RAM are the physical backing for committed virtual memory.  This means that for every single MB of committed virtual memory, there must be an equivalent amount present in either the RAM or page file.  Therefore, if at any point Windows has committed more virtual memory than there is installed RAM, the backing for any subsequent virtual memory must come from the page file.  However much your committed memory exceeds your installed RAM under peak load is the minimum size your page file needs to be.  Crazy, isn’t it?  It’s not related at all to how much RAM you have multiplied by some magic number and furthermore, you can’t know beforehand how big your page file should be – the figure can only come from performance monitoring of the system under peak load. 

Let’s look at some examples to illustrate how the minimum size of the page file should be calculated.  For these hypothetical scenarios, you’ve set up performance monitor to record Memory\Committed Bytes which is your Commit Charge.  You’ve set it to run for long enough to capture the value under the peak load this system will face.  You’ve set the page file size to System Managed to allow Windows to automatically size it since you don’t yet know how big you should make it (we’ll cover later why I don’t recommend leaving this option enabled). 

Scenario 1

After collecting your data under peak load you establish that the highest value of your Commit Charge was 16 GB (orange arrow).  Gigabytes 1 through 12 were covered by your physical RAM but notice that 13 through 16 had no physical RAM to back them (represented by the dotted blue box).  Remember that every single MB of committed virtual memory must be backed by RAM or the page file therefore the 4 GB excess of committed memory on this system must be backed by the page file.  Which in turn means that your page file must be a minimum of 4 GB.

image

Scenario 2

In this scenario, your captured performance data indicated that your Commit Charge was 100 GB (orange arrow).  Your server has 128 GB of RAM in it so now we have the opposite situation to our first scenario – you had less committed virtual memory than your total RAM.  Notice how there is no deficit blue dotted box representing virtual memory that couldn’t be backed by your RAM.  So what does this mean?  Technically it means you don’t need a page file on this server because you never committed more virtual memory than your total RAM could support.  Having said that, Microsoft’s recommendation is to always have a page file so in cases like this where your performance monitoring shows you don’t need a page file I usually set it to 4 GB (see the Some Conditions Apply section below for details on why I recommend this number).

image

We’ve covered what the minimum size should be in our examples above, but what about the maximum?  Over time, the load on your server might increase beyond the peak load that you used for your initial calculation which would cause your minimum page file size to not be big enough.  To account for this, you can set your maximum to double your minimum (8 GB in our example).

Can’t I Just Set It To System Managed And Call It A Day?

You’ve probably looked at the option to allow Windows to “Automatically manage paging file size for all volumes” and wondered what exactly it means.

image

Armed with the knowledge you’ve gained so far in this post you might be able to take an educated guess as to what this option does.  It tells Windows to watch your Commit Charge and when it reaches 90% of the Commit Limit it increases the size of the page file to accommodate.  When and if the Commit Charge drops again it will shrink the page file.  There are two drawbacks to this option:

  1. While Windows is increasing the size of the page file, requests to commit virtual memory can fail.  This is a BAD THING that you do not want happening.  The error message in the “Virtual Memory Minimum Too Low” dialog which Windows displays while the page file is expanding warns you of this:image
  2. If you are running Windows 2008 R2 or lower, choosing this option will set the minimum page file size to whatever your RAM size is.  If you were the owner of the server in our imaginary scenario 2 above, your page file size would be set to 128 GB when in fact you could get away with a minimal page file of 4 GB.  In other words, an immense amount of disk space wasted for nothing!

To summarize, I don’t recommend selecting this option – especially now that you have the knowledge to properly size your page file.

Some Conditions Apply, See Store For Details

Sometimes the minimum size of your page file is dictated by other requirements even if your performance monitoring shows that you need a very small page file.  The primary role of the page file which we’ve covered so far is to act as the physical backing for committed virtual memory.  One of the other roles of the page file is to capture memory dumps in the event of a blue screen.  If your IT department's policy is to always capture a full memory dump then your page file’s minimum size must be the size of your RAM + 257 MB (see https://support.microsoft.com/en-us/kb/2860880 for details).  If there is no such policy and you find yourself in a situation similar to scenario 2 above where your performance monitor results show that you don’t need a page file, I recommend setting it to 4 GB.  This should be enough to capture a kernel memory dump in virtually all cases.

Conclusion

Congratulations on making it this far!  This was a pretty lengthy post but trust me, if you can understand the concepts presented here you are way ahead of the game and well on your way to a solid understanding of the intricacies of memory management in Windows.  I want to end this post with a brief summary of the main points we covered:

  1. Virtual memory is presented as a per-process address space which is abstracted from the actual physical memory backing it
  2. One of the roles of the page file is to act as the physical backing for committed virtual memory
  3. Windows will not commit more virtual memory than the total size of your RAM and page file(s).  In other words, the Commit Charge cannot exceed the Commit Limit
  4. To calculate the size of your page file, you need the highest value of your Commit Charge under peak load.  However much this value exceeds your total RAM is the minimum size your page file should be.
  5. Set your maximum page file size to some ratio of your minimum (double is a good starting point) and you’re done.
  6. If your peak Commit Charge doesn’t exceed your total RAM then you just need a nominal sized page file (e.g. 4GB) unless you need to capture full memory dumps.

Wednesday, 15 April 2015

Next-Level Scripting - Using Parameters in Your Own Scripts, Part III

Welcome to the next post in my Next-Level Scripting series where I show you the PowerShell scripting techniques that take your scripts to the next level – a level defined by concise, well-written code that conforms to best practices and uses the built-in tools in PowerShell whenever possible to handle common scripting tasks.

In this post we’ll be taking a look at a slightly more advanced feature of parameters in PowerShell which allows you to create mutually exclusive parameters.  If you are not familiar with parameters yet, I suggest you take a look at Part I and Part II in this series first before proceeding.

What are mutually exclusive parameters?

If you’ve done some basic scripting with PowerShell you’ve probably come across mutually exclusive parameters.  Take, for example, Get-Process.  It has two parameters, –Name and  –Id, to retrieve processes by name or process id respectively.  If you stop and think about it for a second, you only want to allow the Name or Id parameter to be used each time the cmdlet is run.  If you allowed both at the same time then you would in essence be allowing someone to retrieve a process by Name and Id, something which is bound to lead to problems.  And this is the concept of mutually exclusive parameters – when you select one parameter, one or more other parameters become unavailable for use.  Let’s take a look at how we do this.

Complicated Copy

Let’s imagine you’re writing a script to copy some files on an ad-hoc basis to random servers.  Some of the servers you will target are inside your network and accessible via name while others are behind a firewall and only accessible via IP address.  Furthermore, you have multiple firewalls and depending on which zone the target computer is in, you want to copy a different set of files.  These are the required parameters for our script:

  1. Computer name
  2. IP Address
  3. Firewall zone name

There’s more to it though – we only want to allow the following parameter combinations:

  • Computer name only
  • IP Address and firewall zone name only

What we don’t want is to allow the computer name and IP address to be specified at the same time.  So the challenge is: how do we set up our parameters so that once the computer name parameter has been selected, IP address and firewall zone name are no longer available and vice versa?

Game, ParameterSet and Match

The key to achieving this is using Parameter sets.  This feature allows you to categorize your parameters into different groups such that once a parameter from one group is selected, any parameter which belongs to a different group is no longer available.  This is the code to set up the parameters according to our requirements for our fictitious file copy script:

001
002
003
004
005
006
007
008
009
010
011

param
(
   
[Parameter(ParameterSetName = "NameCopy")]
    $computerName,

    [Parameter(ParameterSetName = "IPCopy")]
    $IPAddress,

    [Parameter(ParameterSetName = "IPCopy")]
    $FwZone
)

As you have probably guessed by now, the ParameterSetName is the key here.  We have created two groups of parameters:

Group 1 Name:  NameCopy

Group 1 Parameter:  $computerName

Group 2 Name: IPCopy

Group 2 Parameters: $IPAddress, $FwZone

We have now prevented anyone running our script from selecting both the computer name and IP address parameter at the same time.  In addition, the firewall zone parameter will only be available with the IP address parameter.  Let’s take a look at the output if we run help Copy-MyFiles.ps1, the name of our hypothetical script.

image

Isn’t that a beautiful sight?  Notice how the two possible combinations shown in the help output adhere to how we structured out parameter sets.  PowerShell has taken care of documenting our parameters for us and all someone has to do to find out what parameter combinations are acceptable is to run help on our script.

NOTE:  To keep things simple, I did not make these parameters mandatory as shown in the previous posts in this series on parameters.  As a best practice, you should include the Mandatory attribute for these parameters since none of them are optional.  This is how our param block would look with this addition:

001
002
003
004
005
006
007
008
009
010
011

param
(
   
[Parameter(Mandatory = $true, ParameterSetName = "NameCopy")]
    $computerName,

    [Parameter(Mandatory = $true, ParameterSetName = "IPCopy")]
    $IPAddress,

    [Parameter(Mandatory = $true, ParameterSetName = "IPCopy")]
    $FwZone
)

 

Conclusion

We’ve taken our file copy script parameters to the next level in this post by using Parameter sets to group our parameters.  This approach prevents the person running our script from accidentally executing it with a parameter combination which will cause our script to behave unpredictably – something you definitely want to avoid at all costs.

Saturday, 4 April 2015

Next-Level Scripting – Using Parameters in Your Own Scripts, Part II

Welcome to the next post in my Next-Level Scripting series where I show you the PowerShell scripting techniques that take your scripts to the next level – a level defined by concise, well-written code that conforms to best practices and uses the built-in tools in PowerShell whenever possible to handle common scripting tasks.

In today’s post we’re going to look at the switch parameter in PowerShell.  In Part I of the parameters series, we looked at how parameters can help us with obtaining information from the person running our script without using Read-Host.  In Part II we’re going to look at a special type of parameter called the switch parameter.

What Does It Do?

The switch parameter allows you to deal with true/false situations in a far more elegant way than the traditional approach of asking the user a question.  Let’s say you are writing a script to copy files to a group of computers.  The script will be run against a wide variety of computers in your organization, from servers to laptops.  When you’re copying to servers you are confident that they will be online but when you copy to laptops you know that some of them will be offline.  You would like the option of doing a ping test before starting the copy to avoid waiting for the long timeout before the copy fails if the target computer is offline.  But you also don’t want the ping test done every time you run the script.  So you need a way to turn off the ping test depending on what group of computers each run of the script is targeting.

Ask and Ye Shall Receive

One approach which you might use is to ask the user a yes/no question as shown below.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015

$choice = Read-Host "Do a ping test before each copy? Enter Yes or No"

if ($choice -eq "Yes"
)
{
   
#insert code for ping test
}
elseif ($choice -eq "No"
)
{
   
#skip ping test
}
else
{
   
Write-Host -ForegroundColor Red "Error - you must enter Yes or No"
    exit
}

Hopefully the problem with this approach is evident here.  You can’t really control what the user types in so you have to check for “Yes”, “No”, and something that doesn’t match either of those.  While this approach would work, it’s not the approach that a Next-Level Scripter would use.

Don’t Ask

The more elegant approach to a situation like this is to just not ask.  You might be wondering, “So if we don’t ask, how will we know what to do?”  The answer is to use the switch parameter as shown below.

001
002
003
004

Param
(
   
[switch]$pingTest
)

If the param block above is making you go “Huh?” I recommend you take a look at Part I of this series.  If you’re still with me, let’s move on to the switch parameter inside our param block.  (You would need more parameters than this to get things like which computers to target but to keep things simple I’m only including the switch parameter.)

What we have done here is allow the script, called New-FileCopy.ps1 to be run in two ways:

001
002
003
004
005

# Method 1 - Skip the ping test
C:\Temp\New-FileCopy.ps1

# Method 2 - Do the ping test
C:\Temp\New-FileCopy.ps1 -pingTest

If we run the script as shown in method 1, the variable pingTest will be set to $false.  On the other hand, if we run it as shown in method 2 the variable pingTest will be set to $true.  By now you are hopefully looking with disdain on the first way I demonstrated for getting this information using Read-Host and looking with admiration at the beauty and simplicity of achieving the same result with the switch parameter.

Now all we need to do is check if pingTest exists and act accordingly.  EFFICIENCY TIP – you want to write the most elegant code possible, right?  When it comes to true/false variables you don’t need to check if the variable is set to True or False, you just need to check it with a simple if statement.  Compare the two ways of doing it shown below to see what I mean.  The result is the same but I will always think to myself, “This person is a Next-Level Scripter” when I see the check done the second way. 

001
002
003
004
005
006
007
008
009
010

Param
(
   
[switch]$pingTest
)

#Waste of space to do it this way
if ($pingTest -eq $true) {Write-Host "You chose to do the ping test"}

#Do it this way and you save having to type 9 extra characters!
if ($pingTest) {Write-Host "You chose to do the ping test"}

And that’s all there is to it…with one simple line of code we’ve created our pingTest variable and can easily control whether it is set to True or False.  What’s more, it can’t be tripped up by someone typing “Yess” by mistake!

Conclusion

We’ve taken our ping yes/no check to the next level in this post by using the built-in switch parameter to control whether the ping test is performed.  When compared to the first method I demonstrated which used Read-Host and prompted the user for a response, the Next-Level method is significantly more simple and more robust as there is minimal code involved and no reliance on a user typing the correct response.

Friday, 3 April 2015

Next-Level Scripting - Using Parameters in Your Own Scripts, Part I

Welcome to the first post in my Next-Level Scripting series where I show you the PowerShell scripting techniques that take your scripts to the next level - a level defined by concise, well-written code that conforms to best practices and uses the built-in tools in PowerShell whenever possible to handle common scripting tasks.

In today's post we're going to look at how to use parameters in PowerShell to tackle a very common scenario - getting input from a user. 

Being the hater of inefficiency that you are, you're writing a script to create the home drive folder for new users which you're going to give to another employee to run who is not overly technical.  The script needs to take the username and the user's department as input and then create the folder, share it, set permissions etc.  The question we will tackle is - how do you get the information from the person running the script?

One way to get the username and department would be this:

001
002

$userName = Read-Host "Enter the username"
$department = Read-Host "Enter the user's department"

So far, so good - when you run the script you'll get a prompt for the username and department.  Now let's say you wanted to do some basic checking to verify that the username entered is valid, based on your company's standards for usernames and departments:

  1. Usernames are a maximum of 8 characters long
  2. Department must be either FIN, HRD, ITD, PAY or MKT
  3. You also want to verify that the person running the script didn't just press Enter to your prompts, leaving you with a blank username.

You might do something like this to perform these checks:

001
002
003
004
005
006
007
008
009
010
011
012

if ($userName.Length -gt 8 -or $UserName.Length -lt 1)
{
   
Write-Host -ForegroundColor Red "Error in username, please try again. Usernames must be 1 to 8 characters long."
    exit
}

if ($department -ne "FIN" -or $department -ne "HRD" -or $department -ne "ITD" -or $department -ne "PAY" -or $department -ne "MKT"
) 
{
   
Write-Host -ForegroundColor Red "Error in department, please try again. Department must be either FIN, HRD, ITD, PAY, or MKT "
    exit
}

Looking good so far - we're prompting for a username and department and if the user gets either of those wrong we warn them and exit the script so they can try again.  So what's the problem?  After all, we've achieved our goals for getting the information we needed for our beautiful script to run!  Yes, this approach works, but it's not how a well-written script should ask for this type of information and validate it.  If I was interviewing someone who said they had some good PowerShell skillz I would have serious doubts about their knowledge of PowerShell if they used the above approach.  So what's the right way to do it?  Enter the mighty PARAMETER…

The param Block

With the proper use of parameters, this is how our script (called New-HomeDrive.ps1) would be run:  New-HomeDrive.ps1 -Username bpitt -Department FIN

So how do we get our script to the next level?  We use what's known as a param block at the beginning of our script.  Before I show you the parameter block, let's recap our requirements:

  1. 1 to 8-character username,  must be present
  2. Three-letter department, must be from set FIN, HRD, ITD, PAY or MKT, must be present

This is how we would define the parameters for our script:

001
002
003
004
005
006
007
008
009
010
011

Param
(
    
[Parameter(Mandatory=$true)]
     [ValidateNotNullOrEmpty()]
     [ValidateLength(1,8)]
     $UserName,
 
    
[Parameter(Mandatory=$True)]
     [ValidateSet("FIN","HRD", "PAY", "MKT", "ITD")]
     $Department
)

Let's see how this takes care of our requirements by looking at each parameter line-by-line.

Parameter 1 – UserName

Parameter 1 is defined by the following lines of code:

001
002
003
004

[Parameter(Mandatory=$true)]
[
ValidateNotNullOrEmpty()
]
[
ValidateLength(1,8)]
$UserName,

 

Requirement 1 – Parameter must be used

The use of Parameter(Mandatory = $true) tells PowerShell that this parameter must be used.  How is this enforced if we don’t specify it? 

This is what happens if we run the script without specifying the parameter:image

We've now ensured that the script cannot run without this parameter being present.  If you run the script without specifying the parameter, PowerShell will do all the heavy lifting for you and ask the user for it.  What's not to like about that?

We're still not in the clear yet.  What if the user running the script just presses Enter to the prompt without typing anything in?  That is the second part of meeting our requirement and is achieved with this line of code:  [ValidateNotNullOrEmpty()]

What we are doing here is telling PowerShell that this parameter cannot be empty.  So if we were to just press Enter to the prompt shown above, we would get this message:

Cannot validate argument on parameter 'UserName'. The character length (0) of the argument is too short.  Specify an argument with a length that is greater than or equal to "1", and then try the command again.

Requirement 2 – Parameter must be 1 to 8 characters long

This line of code – [ValidateLength(1,8)] - tells PowerShell that this argument must be 1 to 8 characters long.  Anything below or above that threshold will trigger an error message, such as this one:

Cannot validate argument on parameter 'UserName'. The character length (9) of the argument is too long.  Shorten the character length of the argument so it is fewer than or equal to "8" characters, and then try the command again.

So with this one simple line of code we've taken care of the length requirement and as a bonus you get the error message handled for you, without having to type any extra code!

Parameter 2 – Department

Parameter 2 is defined by the following lines:

001
002
003

[Parameter(Mandatory=$True)]
[
ValidateSet("FIN","HRD", "PAY", "MKT", "ITD")]
$Department

Let’s see how this parameter takes care of our requirements.  We’ve already covered how to make a parameter mandatory with our first parameter so let’s look at the second requirement

Requirement - Must be one of the following:  MKT, FIN, HRD, PAY, MKT, ITD

This is one of my favourite features of parameters in Powershell - the ability to define a list of possible choices.  We do this with the following line of code:

[ValidateSet("FIN","HRD", "PAY", "MKT", "ITD")]

If the user enters anything other than one of those six choices, their input will be rejected with this error message:
Cannot validate argument on parameter 'Department'. The argument "Finance" does not belong to the set "FIN,HRD,PAY,MKT,ITD" specified by the ValidateSet attribute. Supply an argument that is in the set and then try the command again.

In the example above, our user typed Finance instead of FIN which is a perfectly reasonable thing to do, except it's not what our script is expecting!  Which leads me to what I think is the most powerful feature of ValidateSet - documentation.  Imagine that you were handing this script over to another user to run and you had done your department check the old-fashioned way like I did at the beginning of this post.  You would need to provide the user with a list of what the choices are when they get prompted for the department.  If you ever added support for another department to your script you would need to update the documentation and make sure the user is aware of the change.

Not so when you're using ValidateSet!  After the user types -Department in the PowerShell prompt, all they need to do is hit TAB and PowerShell will cycle through all the available choices.  This really is an incredibly powerful tool because now the choices are self-documented and if you add another department to your script it will show up as the user is cycling through the choices you have programmed in.   No need to update documentation and hope the user gets it right.

 

Conclusion

We've taken our home drive creation script to the next level in this post by using the built-in features of PowerShell to handle our parameters rather than writing our own code to do it.  When compared to the first method I demonstrated, the param block method is more efficient, scalable and robust and is the one that a "Next Level" scripter would use.  Stay tuned for the next post in the Next Level Scripting Series – the use of the switch parameter.