Getting to know Powershell (Part 2 - More scripting!)

Welcome back to the wonderful world of Powershell. In our last article,  we went through a little bit of the basics of Powershell, went over some of the key additions and changes with PS 3.0, explained how PS deals with objects, and started exploring the help system. Now, we're going to start actually using Powershell.

Ok, enough for the front page, read on after the jump...

The Pipeline

In powershell, we refer to our series of commands as the pipeline. Sure, single commands are useful, but Powershell becomes a lot more fun when we start stringing commands together. As you've noticed in the previous article, when we did Get-Process | Get-Member, we used the pipe character (the "|")...so hence: we have the pipeline. 

The pipeline becomes very useful when we want to do things like sorting, comparisons, modifying output, sending output somewhere, accepting input, and doing programmatic things like loops. We can string a whole bunch of commands together, and the output of each command gets sent through the pipe, to the next command. In Get-Process | Get-Member what is actually happening is that Get-Process is being run first, and then the output of Process objects is being sent (piped) to Get-Member. 

This is all well and good, but it has limitations: for example, you have to be piping useful output to the next command, so it knows what to do with it. Something like Get-Process | Stop-Process makes sense, but Get-Process | Stop-Service does not. The caveat is that the object type going out, has to be understood by the next cmdlet.  So Powershell is smart enough to yell at us when we give it some incorrect inputs, but how do WE know what commands can accept the inputs of other commands?  Again, our good friend of Get-Help on a cmdlet will tell us what we need to know.

Let's start off with a simple example: in powershell type Get-Help Stop-Process -Full and look through the section called "PARAMETERS". Actually, you know what, instead of that, do Get-Help Stop-Process -ShowWindow and make it easier to read.

Notice, that some of the parameters have "Accept pipeline input? as TRUE. That means that this particular cmdlet will accept those objects as input (for now, don't worry about byPropertyName, byValue, and all that). So, for Stop-Process, the parameters that accept pipeline input are "Id", "InputObject", and  "Name". Now we can worry about the byPropertyName and byValue; if something accepts pipeline input byValue, that means that it is the same object type. If you recall, our other friend "Get-Member" tells us object types.

Let's take a look at the above example. Let's see what Get-Process returns.
  TypeName: System.Diagnostics.Process
Let's now look at what Stop-process accepts byValue:
 -InputObject <Process[]>

Hmm...those two things look eerily alike, so it looks like that will work. 

However, if we go to our failed example of  trying to stop a service we notice that Stop-Service accepts the following byValue
-InputObject <ServiceController[]> 

These object types are different, so they won't work. But what of "byPropertyName"? Is that ever going to be useful? It very well might be, if you want to string together cmdlets that aren't REALLY related, but have some common properties. It's used less often, but there are situations where the object that gets returned from a cmdlet isnt of the same type as what you need to pipe it into, but it has a property with the same name as something accepting byPropertyName.

Let's take a theoretical cmdlet called Get-MyListofThings, and if we run it through get-member, it is a Luke.Custom.Object has the properties of "name", and "description". Let's say that our cmdlet returns "notepad" and "this is a notepad process". We want to stop the notepad process that is contained in Get-MyListofThings, but, obviously Luke.Custom.Object is not the same as System.Diagnostic.Process. However, the help section of Stop-Process tells me that it will accept "Name" in the pipeline. We can therefore select the name property, and pass it to Stop-Process, and it will work. It should be noted that byPropertyValue has the ability to have some very strange effects, so it's definitely one of those things to be careful with.


Selections, ScriptBlocks, and the Order of Operations

Powershell can be very powerful, especially when used in scripts. One liners are fun, but what if we want to do a very complex script that has blocks of code that do different things, and we need to execute some things first, or only work with certain properties of objects?

Luckily, Powershell lets us do exactly that. Here are a few things to keep in mind as we go forward:

{ } is called a script block. We will enclose sections of PS code in this block, and it will stay together. This is useful in everything from simple selections to being the basis for loops.

( ) is stuff that needs to be executed first. Just like middle school math, anything that is in parenthesis gets executed first.

$_ or $PSObject refers to the current object going through the pipeline. For example, if you do Get-Process, it's not just one process that comes across, it will be a list of every process running on your system. When you pass that across the pipeline, you can do things to the individual rows of processes that come across.

Let's combine a few of these together and get information about a notepad process we have running: Get-Process | Where-Object ($_.ProcessName -eq "notepad")
Now, before you yell at me, yes I know you can do "Get-Process notepad" for the same effect.

So let's take a look at what's going on here. We are getting a list of Processes with Get-Process and sending them to Where-Object. The "-eq" stands for "equals" so we want to match something that IS "notepad" (more on comparison later). We know we only want to return information about the notepad process, so we need to find some ProcessName property that has notepad in it. We need to do this comparison first, before Where-Object gets run, so that we can get a true/false answer.

So here's kind of what is happening:
Get-Process is run and has the following rows of data:

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName                           
-------  ------    -----      ----- -----   ------     -- -----------                           
    266      26     9456      15540   127     4.38   1840 AAM Updates Notifier
 69      10     4600      10848    89     0.02  40428 Notepad

We send those rows of data to Where-Object, and we look at the ProcessName by getting the current object's ProcessName property ... The first object that comes across is the AAM Updates Notifier so $_.ProcessName is AAM Updates Notifier, $_.Id is 1840, and so on. Does that ProcessName equal "notepad", nope, so it returns "false" and where-object ignores it.

It goes to the next row that is coming across, it happens to be notepad, it returns true, so it returns it to us, and then it goes along its merry way for the dozens of other processes that our computers run.

Variables

Powershell also lets us work with variables. In PS, our variables are strongly typed, which means that internally, they are all a certain kind of object, whether it's a string, an integer, a Process object, or whatever else that Powershell understands.

We can put any type of object into a variable. We can get rows of hundreds of processes into a variable, or we can get something as simple as a number or letter into one.

Our variables can be named pretty much anything. If you want to have a variable with spaces, special characters, international characters, or whatever you can think of; powershell lets you do that.

All variables in powershell start with a $. All variables that have spaces or special characters have to be enclosed in a script block.

Examples of valid variables are:
$a
$ilikevariables
$var123
${this variables has spaces!}

Examples of invalid variables:
$this has spaces too but won't work.

When we work with variables inside scripts or in the pipeline, we have to make a determination of whether we want to evaluate them or not. So if we do $a = 5 (which, by the way, if you do $a|gm you'll see that PS is smart enough to assign it a type of Integer), we now have the number 5 in our variable. Now let's see what the differences are in output:

" " will evaluate the value of the variable
PS C:\Users\lkwiecinski> "the value of $a is $a"
the value of 5 is 5


' ' does not evaluate the variables
PS C:\Users\lkwiecinski> 'the value of $a is $a'
the value of $a is $a



The ` (which is not the same as single quote...this is the "tick" character), allows us to stop evaluating only when we choose to.
PS C:\Users\lkwiecinski> "the value of $a is `$a"
the value of 5 is $a




Comparisons

There are lots of comparisons we can do in Powershell. Instead of listing them all out here, the best way to get information about them is to do Get-Help About_comparison and see the syntax for equals, not equals, greater than, less than, like, and the various other comparisons you can do. Most of them make sense, and are just shorthand for the comparison we want to do (eg: eq for equals, lt for less than).

Why don't we simply do = and != for equals and not equals instead, you ask? Ok, maybe you didnt ask, but I'll tell you anyway. Simply put, it's because that's how we assign variables. Instead of making a big deal about when = is assigning and when it is a comparison, we have our list of comparisons that we can use.

Along the lines of comparing, we can also do sorting with the Sort-Object cmdlet.

Let's say we want to organize our Processlist alphabetically, we can do that by piping the output of Get-Process to Sort-Object and tell it how we want to sort it.

For example, we can sort it alphabetically by processname: Get-Process | Sort-Object -Property Processname

If we want, we can sort it reverse alphabetically Get-Process | Sort-Object -Property Processname -Descending


So from here, go on...start playing with commands, look at other people's scripts, read through powershell.org and the powershell subreddit on reddit.com. There's only so much you can learn from reading a few posts, but hopefully you get a better understanding of how powershell works, how it passes data across the pipeline, what kind of data it can work with, and the almost limitless of combinations that you can do. 

I plan to use this series of posts as my own post-it notes; there are a lot of things in powershell to try to remember, but once you start working with it, all you'll need is a few reminders here and there. 

Programming

Now that we know a bit about comparisons and how to work with objects in the pipeline, let's see what else we can do with powershell scripting. This will tie in comparison operators, pipelines, script blocks, and introduce some new elements of loops and conditional commands.

Programming scripts assumes that you have some knowledge of the structures of loops and what you want to do with conditional statements, so I'll simply show you the structure of how loops work in Powershell.


Let the pseudo-code begin!

if (something -eq something else) {
    do this set of commands 
}

or we can go further and

if(something -eq something else) {
   do this set of commands
}
else {
  do another set of commands
}

Otherwise, we can do

if (something -eq something else) {
   do stuff
}
elseif (something -eq completely different) {
   do different stuff
}
else {
  do something completely different
}

We can do while loops as well:

while (something -eq something) {
 do some stuff
}

We can do for loops as well

for ($var = 0; $var -lt 10; $var++) {
 loop through 10 times and do some code here
}

foreach ($var in $collection) {
 do stuff to each line in your object
}

I notice that foreach tends to confuse people, so let's use Get-Process as an example. When you run Get-Process, you get a bunch of processes that are returned. If we do $collection = Get-Process, we will have a bunch of rows of data. If we run the above foreach....each row will be put in $var, where you can do stuff to it, then the next row gets put in $var, and so on.

switch ($var) {
  value { do stuff }
  value2 {do other stuff}
  default { if nothing else matches - this is optional }
}



Labels: