Jan 302013
 

If you’re like me, sometimes you just feel like counting. Well, in PowerShell you can count easier than ever before! PowerShell gives you ability to not only easily increment, but also run code for each increment because you can pipe! First, let’s take a look at the basic code:

And why limit yourself to counting up?! You can even decrement those numbers:

So, how can you use this? Well, remember the wonderful thing called a pipeline. You can potentially ping an entire subnet with the following command:

Remember, if that command takes too long, you could consider multithreading it with one of the scripts on this site!

Aug 302012
 

Converting arrays into strings can cause a couple different problems, and I am starting to see more and more people struggling. The problem is that it doesn’t always act like you think it should. By default, converting an array into a string will place every item on one line of text separated by a space. This is particularly true with arrays made by get-content that are typecast into a string.

The answer is very simple. When you type cast an array into a string every field is separated by a secret value (not so secret anymore!) called the Object Field Separator, or OFS. This value is keep in a variable called $OFS and you can change it! For instance, if you want there to be a new line when you typecast the array then set $OFS = "`r`n" which will make a newline between every field. If you want to comma separate the values, then set $OFS = ", "

Just remember to clean up when you are done! You can set the value back to it’s default (a single space) by removing the variable: Remove-Variable OFS Don’t worry, PowerShell will remake it.

NOTE: The ‘ that you are seeing should be the Backtick key found to the left of your numbers on a US keyboard.

Enjoy!

Aug 302012
 

I really don’t like using tools like Quest for several reasons, primary among them being that it isn’t portable, or installed by default. I started the AD without Quest segment some time ago and I wanted to continue it again, but in a new direction. I think making scripts that can work in the pipeline is a better approach over making functions that port to each new script. That said, I wanted to make a generic AD searcher that could be used without modification for most queries. After I created it, I found that I prefer it over the UI based tools for information gathering purposes, it really is pretty neat.

Let’s get started!

First, we need to create our variables. As with all of my recent scripts, I have placed all of my variables in the Param block so that I can modify them at run time. I really like this practice since I can now change the way the script works at run time and I can even use “tab complete” to remember what I was thinking if I run the script 7 months from now. Notice that I setup a “default” value for each of these parameters so that it doesn’t need to be provided for the script to run, you are simply modifying the way the script runs with each.

Notice that the only value that I have defined as “from the pipeline” is the search term. This is because I think it’s the only thing that might change if it is coming from the pipeline. I don’t think, for instance that you would have a single search string, but then search on different properties. As well, I have forced the “PropertiesToLoad” to be an array. This is because I will use the .count property later, and if only one property is provided then PowerShell will make the variable a string instead of an array of strings. Strings do not have the .count property.

Now, I wanted to script to work in the pipeline, and there are several semantics that must be used to do this “properly” the first is that you have a “begin”, “process” and “end” block. The begin block will be run once, it is where you can setup any persistent objects or other complete environment items. In my case, I don’t want to create a new .NET AD searcher for every single item in the pipeline, the one will serve for everything, so I create it in the “begin” block.

The page size of the searcher will break the search into chunks returning 1000 objects at a time. The end result of a search with 8000 users is the same, but AD actually limits the number of items that can be returned in a search (by default 1000) so in the background we will perform eight inquiries to get our one return.

Now, I wanted a way get back only certain data. What if I only want to send the name down the pipeline? What if I want a quick way to see users with a logon count of greater than 1000 and their user names? I decided to let the user pass an array of attribute names to be loaded and sent down the pipeline. More on this later, but for now I am going to load them all into the searcher. Again, this is only done once, not for every single item since it’s in the begin block.

Now, we can move to the “process” portion of the script. This will get executed for each item that enters the pipeline, or a single time if executing outside of the pipeline.

The first thing that we need to do is setup the search string for the AD object. I am going to base this whole thing off of the parameters we created at the start. Those familiar with AD search string will recognize this. This is also where the heart of the script resides.

All this does is make a string that makes a typical, quick search. The default value of the parameters would may the string "(&(objectcategory=*)(cn=*))" and return every single AD object. Any modification of the parameters narrows it down from there. If the script is executed with ObjectType user then the string would become "(&(objectcategory=user)(cn=*))" and return every single user. I think you probably get the idea. The second line simply executes the query and places the results in our variable with the same name.

For the last part of the script, I wanted to format the data for use in the most intelligent way. Basically, depending on what the user provides for the “PropertiesToLoad” parameter we will have the script give a slightly different output. I decided that if the user gave nothing, then we would send the native .NET object that the searcher returns, unmodified. That would allow the most open-ended use of the return down the pipeline because all of the properties are included. If the user gives a single property, then they are probably trying to send a single piece of information down the pipeline, and so we can return a basic array of strings. Finally, if they are providing several, specific pieces, then perhaps they are trying to gather information, and we can make an array of objects with a property for each item requested and provide it, this makes the output compatible with cmdlets like Out-Gridview.

To start off we will see if the user provided input for “PropertiesToLoad” and either show the direct results, or start some post processing. If we are going to start post processing we need to make our array of objects and all associated properties. If there was only one object loaded, we’ll convert the array of objects into an array of strings.

To break it down more in case that’s a bit confusing. The first step is to make an object called Return and give it the properties that the user wants. This is a quick, easy way to do just that.

Next we will loop through all of the properties that they would like to have and move it from our results to our return object. We have to do this so that we can pick and choose only the items the user wanted. Even though the native .NET result was limited to the properties we told it above, it will always contain certain things like the distinguished name. Making our own object removes those items and allows PowerShell to display it in a pretty manner.

Here we are checking to see if only one property was provided. If so then we will quickly convert the array of objects into an array of strings. This is the best way I know how to do that. We are simply sending the object down the pipeline and running a ForEach-Object (% is a built-in alias) and printing the property.

If there is more than one property then we will print the whole object.

That’s it. Once you start to play around with the script you will start to see all of the possibilities. The following code block has the entire thing as one with a help block for usage information. This block will make the script compatible with Get-Help as well!

Feb 162012
 

I have had problems in my very long scripts with Write-Progress. When writing the script you add in several Write-Progress statements and give them a percentage, the problem is that the percentage you give it is likely a number that you are typing in and hard coding. This is a problem because if you add to the code or move the code around you have to go back and change every one of these items.

In a recent script I wrote that was just over 300 lines I had to repeat this process 5 times which was really frustrating, so I thought up a way to make sure I don’t have to do it again.

Now, I should state beforehand that this process only works if each write-progress is executed once and only once. Any changes to that means you may need to add some other intelligence, but in general I think this is a good starting point.

The first thing you want to do is read the script file that you are executing to get a count of the number of lines with a “Write-Progress” in them. Then store a tracking number as a global variable so that all functions can see it. Do this at the start of the script.

Here is my single line to do all of the above:

1
$ProgressIndicators = $($Global:I=0; $(GC $MyInvocation.InvocationName | ? {$_ -Match "Write-Progress"}).count -1)

So, there is a whole lot of learning packed into a small area here. Let me cover the confusing sections. First naming a variable $Global:I will create a global variable called “I” that is accessible from all PowerShell scopes. As well, there is a “;” after this portion to execute this as a separate line of code. Since there is no output, this will not become part of the variable when run like this. For more information on scopes visit http://technet.microsoft.com/en-us/library/dd315289.aspx.

The command GC is a built-in alias for Get-Content.

The object property “$MyInvocation.InvocationName will provide the file path of the script that is currently being executed. So, in effect, we are reading the entire script that is currently running.

Then we pipe the script contents to ? which is a built-in alias for Where-Object. Then we look for a match on “Write-Progress”. You’ll notice that this whole section is surrounded in $() which “Instantiates” a variable for this this output. Then we can find the number of lines with the .count property. After that, I subtract 1 so it doesn’t count itself.

All of this means that the variable $ProgressIndicators will now contain the number of times “Write-Progress” appears in the script, no matter what script you place it in.

So now all we need is for Write-Progress to read these two values when it run and increment our global tracking variable.

1
2
3
4
5
6
Write-Progress -Activity "Making Progress" -Status "Test 1" -PercentComplete $($($Global:I++;$Global:I) / $ProgressIndicators * 100)
Sleep 1
Write-Progress -Activity "Making Progress" -Status "Test 2" -PercentComplete $($($Global:I++;$Global:I) / $ProgressIndicators * 100)
Sleep 1
Write-Progress -Activity "Making Progress" -Status "Test 3" -PercentComplete $($($Global:I++;$Global:I) / $ProgressIndicators * 100)
Sleep 1

So, as you can see I use the same techniques described above to increment the variable and then do some basic match to get a percentage of total progress.

You can throw this code into any script to give you a dynamically updating progress indicator. There are two things to keep in mind. If you have a loop that runs a write-progress command over and over then it will continue to increment and break this. You could remove the $Global:I++ portion or make a sub progress bar to correctly display the progress of the loop. Also, you might skip a write-progress line due to an IF statement. If so maybe you could increment the value of $Global:I in an ELSE statement.

Jul 032011
 

This is the second post in my AD without Quest series. I am covering individual functions that can be combine to produce a wide variety of scripts. Today I am going to be covering how to connect to AD to read an object ADSPath. The ADSPath is basically the LDAP string to connect to that object. Once you have an object’s LDAP path it is very easy to work with the object.

When searching in AD all you need to do is use the directory searcher object and continue to narrow down the filter. There are some slight changes between searching for a user, computer and group, so I’ll cover each.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 Function Get-UserADSPathbyName{
    Param([STRING]$User)
   
    $Searcher=New-Object System.DirectoryServices.DirectorySearcher
    $Searcher.Filter="(&(SamAccountName=$User)(objectcategory=user))"
    $Searcher.PageSize = 1000
    $Results=$Searcher.FindAll()
    If(!$?){"AD searcher failed on $User"}

    If ($Results.Count -eq 1){
        $Results[0].properties.adspath
    }Else{
        Return $false
    }
}

On the line with $Results[0].properties.adspath it is important to note that the “adspath” portion is case sensitive. I really have no clue why that is the case. If you know, drop me a comment to help me out!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 Function Get-ComputerADSPathbyName{
    Param([STRING]$Computer)
   
    $Searcher=New-Object System.DirectoryServices.DirectorySearcher
    $Searcher.Filter=(&(CN=$Computer)(objectcategory=computer))
    $Searcher.PageSize = 1000
    $Results=$Searcher.FindAll()
    If(!$?){"AD searcher failed on $Computer"}

    If ($Results.Count -eq 1){
        $Results[0].properties.adspath
    }Else{
        Return $false
    }
}

So as you can see, a couple of small changes will convert the function for use with computers instead of users. One final function converts this again for use with groups.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 Function Get-GroupADSPathbyName{
    Param([STRING]$Group)
   
    $Searcher=New-Object System.DirectoryServices.DirectorySearcher
    $Searcher.Filter=(&(CN=$Group)(objectcategory=group))
    $Searcher.PageSize = 1000
    $Results=$Searcher.FindAll()
    If(!$?){"AD searcher failed on $Group"}

    If ($Results.Count -eq 1){
        $Results[0].properties.adspath
    }Else{
        Return $false
    }
}

So there they are, but what can you do with them? There is a great variable type in PowerShell defined with the ADSI tag. Once a LDAP string is defined as and ADSI variable you can interact directly with that object by reading any property you want, and even changing them.

1
2
3
4
5
6
7
$UserLDAP = [ADSI]$(Get-UserADSPathbyName “MyUserName”)
$UserLDAP.extensionAttribute1
>MyOldValue
$UserLDAP.Put(“extensionAttribute1”,”MyNewValue”)
$UserLDAP.SetInfo()
$UserLDAP.extensionAttribute1
>MyNewValue
Jul 022011
 

I’ve decided to start an Active Directory series. I have discovered that when you search online for any AD related code everything points back to the Quest tools. I prefer to do everything natively and not rely on a third party tool kits that have to be installed. As an admin, I understand that we do rely on our workstations, but when you start needing to install third party tools on server you are entering dangerous ground. I wouldn’t want to explain that Exchange went down because I installed some tool that locked the server.

I have been using PowerShell since its inception and I still haven’t found an instance where I need to use the Quest tools. Sometimes it might be easier, but once you’ve created a well written function, it’s easy to copy and paste it into your next script. So I am going to base this series off of individual functions that can be added to scripts to work with AD.

We’ll start with the basic ones. The following functions can be used to pull all the server names and user name out of Active Directory. This can be modified to pull whatever property you’d like, but I find that either account name or ADSPath (the LDAP string) are the most useful. I use some variation of these in almost every script I make.

First, let’s look at pulling all AD servers. Now these are only for the domain that you are running it from. There is some extra work to do to change domains, but I’ll cover that in a later blog post. The basic idea is to create a directory searcher, then set a filter (or you will return every object in AD). Then you need to set the page size. That, basically, tells the searcher how many objects it can return in a single search, so if you set it to 1000 and you have 8000 total objects, it will run a total of 8 searches. This step can be skipped if you have a very small domain, but AD has a built in limit to how many objects can be returned in a single search. This value can be set by the admin, but 1000 works with default domains. Finally, you run the search and then return the property you want from the function.

1
2
3
4
5
6
7
8
 Function Get-ServerList{
    $Searcher=New-Object System.DirectoryServices.DirectorySearcher
    $Searcher.Filter="(&(objectcategory=computer)(operatingSystem=*Server*))"
    $Searcher.PageSize = 1000
    $Results=$Searcher.FindAll()

    $Results | % {$_.Properties.name}
}

So, that’s it. You can drop this into your script and then run it with $servers = Get-ServerList to get a list of all your server names. This function runs very quickly and can be put into any script to make things really easily.

However, part of what I am trying to get across is that these functions can be quickly changed to suite your needs. So this is the function very slightly changed to pull all the users instead. In this case the PageSize parameter really comes into use.

1
2
3
4
5
6
7
8
 Function Get-UserList{
    $Searcher=New-Object System.DirectoryServices.DirectorySearcher
    $Searcher.Filter="(objectcategory=user)"
    $Searcher.PageSize = 1000
    $Results=$Searcher.FindAll()

    $Results | % {$_.Properties.samaccountname}
}

More AD entries to come! You can find them via my blogs tags to the right.

Mar 152011
 

With the scripting games coming up, I noticed that they will be adding stars to your work based on the addition of script help blocks that are compatible with the PowerShell “Comment Based Help”. Basically, this is a form of writing a comment block that works in conjunction with the get-help cmdlet that is included with PowerShell.

With this post I am hoping spread the information that this is possible just as much as the how to do it. I think that most script writers don’t even know it’s possible, and I’ll be honest that while I’ve know that you can do it, I don’t include in all of my own scripts.

So, you can add comment based help either to a script or to a function. It is my opinion that you do not need to add the help to a function within a script as no one will be able to run a get-help against it; however, using the format provided to label your functions is probably a good idea.

So, there are two ways to make a comment block and three places to put them.

You can start all the lines in a block with # or you can surround all the lines with <# ...comments... #>. Either form is fine, but the later might be easier for large block; however, the former might be easier to follow if browsing the code. I typically like to start my lines with # as I can quickly see where all the comments start and stop.

As far as where to place them, you can put them at the start of a function or script, at the end of a function or script,or just before the start of a function. Line breaks become very important as spaces between comments and their attached objects will detach them.

Now the comments need to follow the format:

1
2
3
4
5
# .KEYWORD
#  Content
#
# .SYNOPSIS
# This is an example of comment based help.

So what kind of keywords are there? This is taken from the help file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
    .SYNOPSIS
        A brief description of the function or script. This keyword can be used
        only once in each topic.

    .DESCRIPTION
        A detailed description of the function or script. This keyword can be
        used only once in each topic.

    .PARAMETER  <Parameter-Name>
        The description of a parameter. You can include a Parameter keyword for
        each parameter in the function or script syntax.

        The Parameter keywords can appear in any order in the comment block,but
        the function or script syntax determines the order in which the parameters
        (and their descriptions) appear in Help topic. To change the order,
        change the syntax.
 
        You can also specify a parameter description by placing a comment in the
        function or script syntax immediately before the parameter variable name.
        If you use both a syntax comment and a Parameter keyword, the description
        associated with the Parameter keyword is used, and the syntax comment is
        ignored.

    .EXAMPLE
        A sample command that uses the function or script, optionally followed
        by sample output and a description. Repeat this keyword for each example.

    .INPUTS
        The Microsoft .NET Framework types of objects that can be piped to the
        function or script. You can also include a description of the input
        objects.

    .OUTPUTS
        The .NET Framework type of the objects that the cmdlet returns. You can
        also include a description of the returned objects.

    .NOTES
        Additional information about the function or script.

    .LINK
        The name of a related topic. Repeat this keyword for each related topic.

        This content appears in the Related Links section of the Help topic.

        The Link keyword content can also include a Uniform Resource Identifier
        (URI) to an online version of the same Help topic. The online version
        opens when you use the Online parameter of Get-Help. The URI must begin
        with "http" or "https".

There are even more past that, but I think that covers the main group. If you want to see them all and get more detailed information go to PowerShell and run Get-Help about_Comment_Based_Help.

As well, there are several built in sections that are generated automatically: Name, Syntax, Parameter List, Common Parameters, Parameter Attribute Table, Remarks.

To get more information and some good examples go to PowerShell and run Get-Help about_Comment_Based_Help.

Mar 112011
 

Just though I’d spread some information about the Microsoft hosted scripting games for 2011. So far I think this year it’s PowerShell only. Microsoft wants everyone of all skills to get involved, they promise that you will learn new stuff no matter your skill level, so go try it out, you might even win something!

http://blogs.technet.com/b/heyscriptingguy/archive/2011/02/19/2011-scripting-games-all-links-on-one-page.aspx

Feb 042011
 

My most popular article has thus far been the post over multithreading found here. As such I felt that my audience might like to see some more entries covering that topic. After all, it is a vast topic with a lot of very advanced things you can do. In this post I would like to talk about tracking the status of your various threads. In other words, how can you see a progress of what your child script is doing?

In exploring this myself, I worked with passing custom objects back to the parent which held various status messages that the parent could read in with the “receive-job” command. That actually worked, but it took a lot of code in both the parent and the child to generate and read the status from the child threads.

While working with this, though, I found that the Job object from “get-job”, a System.Management.Automation.Job, has a property called “Progress”. I then decided to look into how this actually gets filled out. And the answer eluded my research until I managed to do it, completely by accident. As it turns out, this very useful property is filled in automatically by a “write-progress” command run within the child thread. It is actually quite logical that the progress field is filled in by “write-progress”.

So, that is enough of a background on how I came about it. Let’s actually look at some code. First,I want to create a script block to run within my child job.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
## Write some code for the child threads to execute
$ThreadCode = {
    $x = 0
    $time = 200
    While ($x -lt 100){
        $x++

        Write-Progress  -Activity "Testing"
            -Status "Testing"
            -PercentComplete $X
            -completed
        Start-Sleep -Milliseconds $time
    }
    Write-Progress  -Activity "Testing"
            -Status "Testing"
            -PercentComplete 100
            -Completed
    "Annnd we're done."
}

So here you’ll notice that I am basically creating a loop that will count 1 to 100 and then close. Along the way it will fill out its progress with the “write-progress” cmdlet. The one important note is that I am adding the “-completed” parameter to the “write-progress” cmdlet. That may look like a mistake in the code,but it isn’t. If you play around with this manner of getting progress from a child you will notice that when you run a “receive-job” your host thread will read all output from the child, including flashing all of the “write-progress” bars that were in the child. If you actually read the help files for “write-progress”, you’ll see that actual description of the functionality of “-complete” states that is simply hides the output bar. We need to do this so that when the parent thread runs a “receive-job” there is no odd behavior for the end user.

1
2
3
4
5
6
## Kill any current jobs in the sessions and get rid of them
Get-Job | Stop-Job
Get-Job | Remove-Job

## Start the child job and assign to a variable to save some code later
$Job = Start-Job -ScriptBlock $ThreadCode

So for this quick snippet we are just closing an remove any jobs our current session may have, and then starting our job. I assign it to a variable so we don’t have to track it down later, not that it’s hard.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
While ($Job.State -eq "Running"){
    ## Get the Child job object
    $Child = $Job.ChildJobs[0]
   
    ## Get the newest Progress Object in the child job
    $Progress = $Child.Progress[$Child.Progress.Count - 1]

    ## If the progress object is not null
    If ($Progress.Activity -ne $Null){
        Write-Progress  -Activity $Job.Name
                        -Status $Progress.StatusDescription
                        -PercentComplete $Progress.PercentComplete
                        -ID $Job.ID
    }
   
    Start-Sleep -Milliseconds 200
}

So this is the bread and butter of the parent script. It simply loops around as long as our job is running. First we need to find the child job to our parent job. I am not sure why Microsoft chose to design it like this, but each job you start actually runs commands in a child job. I have a feeling it has something to do with functionality with Windows HPC, but that is neither here nor there. Once we have our child job, we want to find the latest progress object that has been passed. After that I just check to see if the object is empty, because the first check may not have given the child thread enough time to fill this out. If there is data, then we can fill out a “write-progress” bar on our host thread. From there we just repeat until the job is done.

1
Get-Job | Receive-Job

At the end we just want to read anything the child script may actually have sent. So that covers it. It is really easy to get custom progress bars for a child process because there is a good, built in method to do it.

Following is a full example of a script which will launch three child threads at a time until there have been 10 opened total. It will show you the output of each thread as it completes and the progress bars produced by each. I even included a random speed that each child will complete so you can see some bells and whistles going.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
## Write some code for the child threads to execute
$ThreadCode = {
    $x = 0
    $time = Get-Random -Maximum 200 -SetSeed $(Get-Date).Millisecond
    While ($x -lt 100){
        $x++

        Write-Progress -Activity "Testing" -Status "Testing" -PercentComplete $X -completed
        Start-Sleep -Milliseconds $time
    }
    Write-Progress -Activity "Testing" -Status "Testing" -PercentComplete 100 -Completed
    "Annnd we're done."
}

## Kill any current jobs in the sessions and get rid of them
Get-Job | Stop-Job
Get-Job | Remove-Job

## Loop control
$x = 0

## While we are still launching threads or wait for them to close
While (($x -lt 10) -or ($(Get-Job -State "Running").count -gt 0)){
    ## While there are less than three jobs running and less than 10 have started
    While (($(Get-Job -State "Running").count -lt 3) -and ($x -lt 10)){
        Start-Job -ScriptBlock $ThreadCode | Out-Null
        $x++
    }
   
    ## Main portion - Read all jobs
    ForEach ($Job in Get-Job) {
        ## Read all children of all jobs
        ForEach ($Child in $Job.ChildJobs){
            ## Get the latest progress object of the job
            $Progress = $Child.Progress[$Child.Progress.Count - 1]
           
            ## If there is a progress object returned write progress
            If ($Progress.Activity -ne $Null){
                Write-Progress  -Activity $Job.Name
                                -Status $Progress.StatusDescription
                                -PercentComplete $Progress.PercentComplete
                                -ID $Job.ID
            }
           
            ## If this child is complete then stop writing progress
            If ($Progress.PercentComplete -eq 100){
                Write-Progress  -Activity $Job.Name
                                -Status $Progress.StatusDescription
                                -PercentComplete $Progress.PercentComplete
                                -ID $Job.ID
                                -Complete
                ## Clear all progress entries so we don't process it again
                $Child.Progress.Clear()
            }
        }
    }
   
    Get-Job -State "Completed" | Receive-Job
   
    ## Setting for loop processing speed
    Start-Sleep -Milliseconds 200
}

Get-Job | Wait-Job | Out-Null
Get-Job | Receive-Job
Feb 022011
 

As you are typing in any given PowerShell console or in PowerShell ISE you can finish typing many things simply by hitting the “Tab” key while you are halfway through a word. This practice is refered to as “Tab Complete”. If you aren’t in the practice of using this all the time, then you really need to start utilizing it heavily for your own sake. Not only can it help you type and create code faster, but it can be useful in remembering cmdlet names or parameters when you are typing.

Things I know you can tab complete are: cmdlet names, file and directory names, third party programs, custom scripts, cmdlet parameters, variable names, object members, functions, and many more.

Another cool thing is that if you hit it too early in the word, simply hit it again to keep going through all possible entries. If you type “Get-” and then start hitting tab,you will see the names of all the cmdlets until you get the one you want. Or after you’ve typed the name of a cmdlet,type “-” and then tab to see all possible parameter names, and it even works with your own scripts!

Skipped by the command you wanted? Shift-Tab will start you back in the other direction.