Friday, August 29, 2008

Functions in PowerShell


What's a function?

A function is a script block with a name.  Most functions also take parameters and/or return a value, but that's not required.  The PSDrive called function: has a list of all defined functions, and there will be quite a few in there from the first time you install PowerShell.  You can use Get-Item (or gi, for short) cmdlet to get the Definition property of the function to see how they're made, as shown below.

Function Parameters and the param() Method

Let's make a test function and try this bad boy out.  I'm going to make a function that adds two numbers.  You can use the return statement to explicitly return a value, but any expression that is not assigned to a variable will be sent as output.

function Add-Numbers ($x, $y) {
  $x + $y
}

PS Function:\>  Add-Numbers 23 77
100

Okay, that worked, so let's look at the Definition property:

Function:\>  (gi Add-Numbers).Definition
param($x, $y) $x + $y

That's funny, I don't remember using the param() method in my function.

The param() method is implied when you put your parameters in parentheses before the script block.  This code would do the same thing:

function Add-Numbers {
  param($x, $y)
  $x + $y
}

Command-Line Switches and Named Parameters

So how do we set up named parameters?  We already did.  In PowerShell every variable name that you add to the param() method also is automatically created as a named parameter that you can use at the command-line.  Observe:

Function:\>  Add-Numbers -y 10 -x 20
30

Default Parameter Values

What if you have an optional variable?  Something that usually has the same value, that you'd rather not have to type in every time?   Easy.  Just assign it a value within the param method and you're done.  If you use the parameter when you call the function, then it will take on the value you assign, but if you don't, then it will keep the default value.

function Add-Numbers {
  param($x, $y=20)
  $x + $y
}

Function:\>  Add-Numbers 203
223

A Final Note about Parameters

There are two caveats when calling a function that you should be aware of:
  1. Unnamed parameters are assumed to be in the order that they were declared in param().  If you call Add-Numbers with only one number then it will be assigned to $x because that variable came first.  
  2. Any parameters left over after being assigned to parameters get dumped into an array called $args.

An Example Incorporating What We've Learned

# Get-SmsWmi
# A wrapper for Get-WmiObject that makes it easy to get objects from SMS.
#
# Args:
#   $Class: the WMI class to retrieve
#   $Filter: the where clause of the query
#   $Computername: the SMS server hosting the SMS Provider
#   $Site: the SMS Site Code of the target site
# Returns:
#   An array of WMI objects

function Get-SmsWmi {
  param([string]$Class = $(throw "ERROR: You must enter a class name.`n"), 
        [string]$Filter = $null, 
      [string]$ComputerName = 'sms-server', 
      [string]$Site = 'S00')
  
  [string]$query = "Select * from $Class"
  
  if ($Filter) {
    $query += " Where $Filter"
  }
  
  # Now that we have our parameters, let's execute the command.
  gwmi -ComputerName $ComputerName -Namespace ('root\sms\site_' + $Site) -Query $query
}

Get All Shared Printers with WMI and PowerShell

I whipped up this handy dandy script yesterday for work, and I think it makes a good simple example script, so I'm posting it here:

# Finds all servers that are hosting printers.
#
# Author: Tojo2000 
#
# Note: Requires a text file with one server per line named 'winserv.txt'.

$credential = Get-Credential
$servers = gc winserv.txt | sort

echo "ServerName`tShareName`tName`tDriverName`tStatus" > .\printers.txt

foreach ($server in $servers) {
  echo $server
  $ping = gwmi Win32_PingStatus -Filter "address = `"$server`""

  if ((!$ping.StatusCode) -and (!$ping.PrimaryAddressResolutionStatus)) {
    $printers = gwmi -Credential $credential -ComputerName $server -class Win32_Printer -Filter 'Shared = "True"'

    foreach ($printer in $printers) {
      if ($printer) {
        $output = $server, $printer.ShareName, $printer.Name, $printer.DriverName, $printer.Status
        echo ($output | Join-String2 "`t") >> .\printers.txt
      }
    }
  }
}

__________________________________________

# Join-String2
# Args:
#   $join_string: the string to paste between each element of the string array
#
# Returns:
#   The resulting string to the pipeline

function global:Join-String2 ([string]$join_string) {
  $output = '';
  $input_array = @($input);
  
  foreach ($string in $input_array[0..($input_array.Length - 2)]) {
    $output += $string;
  $output += $join_string;
  }
  $output += $input_array[-1];
  
  Write-Output $output;
}


There are a couple of things worthy of note here.  The Get-Credential cmdlet pops up a prompt to enter a username and password and stores it as a SecureString This is vastly more secure than storing the string in your script or typing it in to the console window.  Get-WMIObject, like many cmdlets, has a -Credential option that takes a credential as an input and runs the cmdlet under that account.  You can also specify a username here, and Get-Credential will be invoked to prompt you for the password.

The Join-String2 function is because there is already a cmdlet that is part of the PowerShell Comunity Extensions called Join-String, but apparently it freaks out if any of the values being printed are null, so I created a function that does a basic join.

The -Filter option of Get-WMIObject is basically just the "where clause" of the wmi query, which is to say that

gwmi Win32_Printer -Filter 'Shared = "True"'

is the same thing as

gwmi -Query 'Select * From Win32_Printer Where Shared = "True"'

It's just a lot more PowerShell-ish and a little less verbose.

Thursday, August 21, 2008

PowerShell and COM

I mentioned to a few people today that you can use PowerShell to create COM objects, but I think that warrants a little explaining.  VBScript automation examples use COM a lot, and if you're going to want to use PowerShell to automate Word documents or any of that other neat stuff, you'll need to use it as well.  

Consider the code below:

PS C:\> $sms_client = New-Object -COM 'CPApplet.CPAppletMgr'             #1
PS C:\> $sms_client | gm                                                 #2


   TypeName: System.__ComObject#{279463bb-1034-4fb5-878e-4a330a08beab}

Name                MemberType Definition
----                ---------- ----------
GetClientActions    Method     IClientActions GetClientActions ()
GetClientComponents Method     IClientComponents GetClientComponents ()
GetClientProperties Method     IClientProperties GetClientProperties ()

PS C:\> $actions = $sms_client.GetClientActions()                        #3
PS C:\> $actions | select Name                                           #4

Name
----
Software Inventory Collection Cycle
MSI Product Source Update Cycle
Hardware Inventory Collection Cycle
Standard File Collection Cycle
Discovery Data Collection Cycle
Request & Evaluate User Policy
Request & Evaluate Machine Policy
Software Metering Usage Report Cycle

PS C:\> $machine_policy = $actions | ?{$_.Name -like "*Machine Policy"}  #5
PS C:\> $machine_policy.PerformAction()                                  #6

In the code above, "CPApplet.CPappletMgr" is the name of the COM type that is equivalent tot he Systems Management applet in the control panel, so here's what the code does:

  1. Creates a new COM object of type 'CPApplet.CPAppletMgr'.  This is the equivalent of "sms_client = CreateObject('CPApplet.CPAppletMgr')" in VBScript.
  2. Runs Get-Member (gm) on the variable I just set to see what methods are available.
  3. Calls the GetClientActions method and stores the resulting array in $actions.
  4. Uses Select-Object (select) to get just the names of each action
  5. Uses Where-Object (?) to get just the action whose name is like "*Machine Policy" and assign it to $machine_policy
  6. Invokes the PerformAction() method of $machine_policy.

Worthy of note is that piping to where technically creates a one-element array, but PowerShell automatically converts that to a scalar by default, so I didn't have to use $machine_policy[0].  If I had actually wanted it to be treated as an array (let's say I'll be using the .Length() method of an array to see how many items were returned, I would have to use the @() syntax to force it:

PS C:\> $machine_policy = @($actions | ?{$_.Name -like "*Machine Policy"}) 

Friday, August 15, 2008

Getting Network Adapter Information Using Get-WMIObject and GetRelated()

WMI stores network adapter information in two classes: Win32_NetworkAdapter and Win32_NetworkAdapterConfiguration.  The former has basic information about the network adapter, and the latter has the IP configuration, etc.

The last time I had to do this, had to look up both objects and compare them to see that the property that linked the two classes was called Index and then write this to output the Win32_NetworkAdapter object, the associated Win32_NetworkAdapterConfiguration object, and then a line of dashes to separate the adapters.

gwmi Win32_NetworkAdapterConfiguration| %{gwmi -query "Select * from Win32_NetworkAdapter Where Index = $($_.Index)"; $_;  '------------------'}

As I discoverd from another post by /\/\o\/\/ (aka The PowerShell Guy), it turns out that the objects returned by Get-WmiObject have a nifty method called GetRelated that will do that work for you, so you can do this instead:

gwmi Win32_NetworkAdapterConfiguration | %{$_.GetRelated('Win32_NetworkAdapter'); $_;  '------------------'}

Tuesday, August 12, 2008

Recursion in PowerShell

Recursion is one of those things that you'll probably find that you don't need to use all that often, but it's a really powerful concept, and where it's useful it can save you a lot of time, and not just in PowerShell.  Simply put, a recursive function has the ability to call itself, usually to perform a task with an unknown depth.

I remember the first time I hit this problem, and I had just started scripting.  I wanted to see who had permissions to a folder.  I walked the ACL on the folder and found a group, so wrote a function to grab the members of the group.  That's when I realized that some of the group's members were groups.  My first solution was horribly unwieldy.  I wrote a bunch of if/else statements that were able to grab group memberships up to three levels deep, and as I was looking unhappily at my hard-to-follow code, a spark of a memory started nagging at me, and I remembered an email thread about recursion, and that's the first time I really "got it".

Let's cut to the chase with an easy example: walking a directory tree.  The function below will use Get-ChildItem (dir) to grab all child items in the current PSPath and print their FullName.  In order to get all files in all child directories, every time it hits a directory it will call itself on the directory.  

# Recurse($path, $fileglob)
# Recurses through a psdrive and prints all items that match.
#
# Args:
#   [string]$path: The starting path
#   [string]$fileglob(optional): The search string for matching files
#
function Recurse ([string]$path, [string]$fileglob){
  if (-not (Test-Path $path)) {
    Write-Error "$path is an invalid path."
    return $false
  }

  $files = @(dir -Path $path -Include $fileglob)

  foreach ($file in $files) {
    if ($file.GetType().FullName -eq 'System.IO.FileInfo') {
      Write-Output $file.FullName
    }elseif ($file.GetType().FullName -eq 'System.IO.DirectoryInfo') {
      Recurse $file.FullName
    }
  }
}

In my mind I always picture a cat's cradle-style string looping through itself over and over until the final loop is met and a final tug unravels the whole thing back into a string.

Wednesday, August 6, 2008

Fish Don't Fry in the Kitchen

We're moving on up...to blogspot. I initially posted Taste of PowerShell to my personal domain, just because I wanted to watch the traffic, etc, but ultimately I just couldn't stand it anymore. I really like using the labels links on the side, and you can't do that if you use blogger to post your blog to another domain via FTP.


The new URL is http://tasteofpowershell.blogspot.com.

Friday, August 1, 2008

Fun with PowerShell and the Speech API

I don't know why, but listening to a computer-generated voice reading things you type is just a lot of fun, and the potential for annoying your friends, neighbors, pets, and co-workers is endless.  Personally I blame my parents for buying me a Speak 'n' Spell when I was a kid.

Anyway, to that end, I present the following little ditty, a function I like to call Speak-String (which I usually alias to "say"), inspired by a few posts using the same trick that I've come across on the Internet.

function Speak-String([string]$message) {
  $speaker = new-object -com SAPI.SpVoice
  $speaker.Speak($message, 1)
}

Set-Alias say 'Speak-String'

PS C:\> Say "Would you like to play a game?"
PS C:\> Say "How about Global Thermonuclear War?"



Formatting Strings in PowerShell

If you're a recovering Perl user, Pythonista, or C junkie, pretty soon you'll be sniffing around in PowerShell looking for a way to format strings.

Does this look familiar to you?

sprintf("%02.2f percent of %04d in %s", $percent, $from, $some_string); # Perl

"%02.2f percent of %04d of %s" % (percent, from, some_string) # Python


Never mind if it doesn't look familiar, but keep this in mind: the ability to choose the format that a variable will be printed in becomes very useful when scripting, especially when dealing with numbers.  Let me give you an example:

You do a dir on the current directory to get the size of a file, and the size is 16493253, but you want to output the number of MB,  so:

PS C:\> 16493253/1MB
15.7291917800903

Now that's a long, nasty number.  Really, you just want to show numbers to two places past the decimal.  PowerShell's way of doing it looks a bit obtuse at first, but it is actually very efficient.

PS C:\> "{0:#.00}" -f (16493253/1MB)
15.73

Note that the .NET string formatting rounds up the number when it sets the precision.  Anyway, let's look at what I just did.  According to Microsoft, Composite Formatting follows this syntax:

{ index[,alignment][:formatString]}

So for starters, any paired curly braces within the format string will be substituted with whatever strings are passed to it via the -f operator.  Consider the following example:

PS C:\> "I have a {0}.  My {0} is {1}." -f "name", "Tojo"
I have a name.  My name is Tojo.

As you can see, the "index" value inserts whatever string is in that position that is passed to the -f operator.  

If you want the string to be placed within a particular column width, you can optionally use the next value, the "alignment" value.  A positive number indicates a right-justified string at least n spaces wide, and a negative number indicates a left-justified string.

PS C:\> ":{0, 20}:" -f "Tojo"
:                Tojo:
PS C:\> ":{0, -20}:" -f "Tojo"
:Tojo                :

The last section of the Composite Format is the actual formatString value.  This is the part of the string that tells PowerShell how to format the string for output.  Let's take a look at the earlier example again:

PS C:\> "{0:#.00}" -f (.16493253/1MB)
15.73

What we have is a format that tells PowerShell that we want to replace the string between the braces with the string after the -f operator and put it into the following format: zero or more digits followed by the decimal with exactly two digits after the decimal.

I won't bother going through all of the possible variations for now, I'll just point you to some useful resources on the Internet, but there is one more thing I should mention.  For any object that supports the ToString() method, you have the option of calling ToString() and passing it the format string directly rather than using the "formatString -f Variable" syntax.

Here are some invaluable resources for when you can't remember the format you're looking for, whether it's a date, percent, currency, or whatever:

MSDN Resources

Other Internet Resources
Under The Stairs: Formatting with PowerShell -- another great writeup on formatting
Hey Scripting Guy!  -- an article on how to format a number as a phone number