Musings of a PC

Thoughts about Windows, TV and technology in general

My first PowerShell script

Not one to try the eternal "Hello, World!" approach, I wanted to try to put PS to some practical use. As a fairly simple (!) start, I decided to take a fairly short VBScript I’d written that looks for empty groups and lists them. Here is the code:
 
  Const ADS_SCOPE_SUBTREE = 2
 
  ‘ Define an ADO connection so that we can perform a SELECT against
  ‘ Active Directory
  Set objConnection = CreateObject("ADODB.Connection")
  Set objCommand =   CreateObject("ADODB.Command")
  objConnection.Provider = "ADsDSOObject"
  objConnection.Open "Active Directory Provider"
  Set objCommand.ActiveConnection = objConnection
 
  ‘ Set a couple of properties on the command so that it searches the
  ‘ way we want it to
  objCommand.Properties("Page Size") = 1000
  objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE
 
  ‘ Specify the SELECT statement.
  objCommand.CommandText = _
    "SELECT Name,ADsPath FROM ‘LDAP://dc=contoso,dc=com’ WHERE objectCategory=’group’ ORDER BY Name" 
  Set objRecordSet = objCommand.Execute
 
  ‘ Make sure we are at the start of the result set
  objRecordSet.MoveFirst
  ‘ Turn on error trapping
  On Error Resume Next
  ‘ Loop through the records in the result set
  Do Until objRecordSet.EOF
    ‘ Get the group object specified by the LDAP string in ADsPath
    Set objGroup = GetObject(objRecordSet.Fields("ADsPath"))
    ‘ Get the information for that object
    objGroup.GetInfo
    ‘ Make sure we clear any pre-existing error information
    Err.Clear
    ‘ because if the group is empty, the next line generates
    ‘ an error, which we then test against in order to find the
    ‘ empty groups. Nice!
    arrMemberOf = objGroup.GetEx("member")
    If Err.Number<>0 Then
      WScript.Echo objRecordSet.Fields("Name")
    End If
    ‘ Move on to the next record
    objRecordSet.MoveNext
    ‘ and loop
  Loop
 
Hopefully the comments I’ve put in will explain how the VBScript works. If you need some more info about the ADO side of things, there is a great article in the Script Center:
 
 
In fact, they use an additional tweak that I haven’t used: setting the "Sort On" property, while I’ve added that to the SELECT statement.
 
Let’s now have a look at what I came up with in terms of PS code:
 
function Get-EmptyGroups()
{
   # Define an empty hash table of empty groups
   $egs = @()
 
   # Set up a new search
   $ds = new-object system.directoryservices.directorySearcher
 
   # Default page size is 1000
   # Default SearchRoot is the current domain
 
   # Specify the properties we want to retrieve
   [void]$ds.PropertiesToLoad.Add("Name")
 
   # Sort on that property
   $ds.Sort.Propertyname = "name"
 
   # Limit it to just groups
   $ds.Filter="(objectClass=group)"
 
   # and go do the search, putting the results into an object
   $groups = $ds.FindAll()
 
   # Now go through the groups, looking for empty ones
   foreach ($g in @($groups))
   {
      $group = new-object DirectoryServices.DirectoryEntry($g.path)
      if (!$group.member)
      {
         $egs += $g.get_Properties()[‘name’]
      }
   }
   $egs
}
 
Again, hopefully the comments help with the understanding, but here are few key points that I picked up and are worth passing on in terms of how PS works and some of the language structures …
 
The reason for the hash table ($egs = @{}) is to store the results of the function. In this iteration of the code, I am just returning the group name so that the function does the same thing as the original VBScript code. However, if the line that reads
 
$egs += $g.get_Properties()[‘name’]
 
is changed to
 
$egs += $g
 
then the function will actually output an array of property values, namely the group name and its LDAP path. This can then easily be consumed by some more PS code in a useful manner.
 
We then replace 10 lines of VBScript with 5 lines of PS (ignoring comments). In PS, some properties get default values which helps significantly. For example, you’ll notice that I don’t need to know the domain I’m running under in order for the PS code to work. If you do want to get the domain and add some OUs, for example, under it, it is only a matter of a line or two to get that information (see http://mow001.blogspot.com/2005/10/ad-infastructure-exploring-with-msh.html for examples). The fact that PS allows you to directly work with the .NET framework delivers a level of power that, until now, meant you needed to work with a .NET language like C#. As we’ll see below, the fact that you can drive it interactively means that you can query the structures themselves to get information that Visual Studio sometimes struggles to deliver.
 
One difference between the two examples that might need explaining is that in the VBScript, I’m looking for objectCategory matching group, while in PS, I’m looking for objectClass matching group. I’m not an AD guru, so I don’t know if the difference is important . The reason why the PS code using objectClass is because that is what other sample scripts I found did.
 
Finally, we get to the foreach loop. This iterates through each of the entries in the returned array. It gets the group information using a DirectoryServices call. Note how I check for whether or not there are any members though! This is a big improvement over VBScript and one of the areas where the PS designers have gone to a lot of trouble to ensure consistency when the underlying OS might not do that!
 
So what is happening here? If there aren’t any members, $group.member is NULL – simple as that. No errors raised, no messy code, nothing. A simple if test.
 
Another little language point: $g contains an array of properties, called ‘name’ and ‘adspath’. The line that starts $egs += shows how you extract a particular index from the array. Note that the property names are case sensitive. If you don’t match the string exactly, you don’t get anything back, including an error.
 
Another little tip: one of the really fantastic and powerful features of PS is that you can drive it interactively. This is a very very useful way for building up your code and testing it as you go. Try doing that with any of the other programming languages that MS provide!
 
If you want to try this for yourself, copy and paste the function a line at a time, starting from the line that starts $ds =  and finish with $groups = $ds.FindAll(). At this point, $groups will contain all of the groups in your AD domain. Now type this:
 
$g = $groups[1]
 
This will get the second entry in the array. Yes, PS counts like C does – it starts arrays at zero.
 
Now type this:
 
$g
 
You should see something like this:
 
Path                                    Properties
—-                                    ———-
LDAP://CN=Administrators,CN=Builtin,… {name, adspath}
 
Don’t worry if the LDAP path doesn’t match. So what is this output telling us? That $g is an object with a Path value and a set of Properties. What can we do with this object? Try this:
 
$g | get-member
 
You should now see this:
 
   TypeName: System.DirectoryServices.SearchResult
 
Name              MemberType Definition
—-              ———- ———-
Equals            Method     System.Boolean Equals(Object obj)
get_Path          Method     System.String get_Path()
get_Properties    Method     System.DirectoryServices.ResultPropertyCollecti…
GetDirectoryEntry Method     System.DirectoryServices.DirectoryEntry GetDire…
GetHashCode       Method     System.Int32 GetHashCode()
GetType           Method     System.Type GetType()
ToString          Method     System.String ToString()
Path              Property   System.String Path {get;}
Properties        Property   System.DirectoryServices.ResultPropertyCollecti…
 
Note the very first line – the type name. PS is very good on types. This is helpful to us as newbies because it means that if you need more information than is given here, you can go to MSDN and do a search for System.DirectoryServices.SearchResult and get lots more juicy info about directory searches:
 
 
Finally, the $egs at the end of the function simply outputs the results. If you call the function interactively, this will be output on the console, otherwise you can pipe it to another cmdlet.
 
Wow – quite a long post! There was a lot learnt in this example. There are some questions in there (like what is the difference between objectCategory and objectClass). There are also some next steps for me to take like looking at parameters to allow me to specify whether I want the group name or the group attributes returned.
 
Until next time …
Advertisements

4 responses to “My first PowerShell script

  1. MOW June 23, 2006 at 11:09 pm

    Great Post !
     
    about the  difference between objectCategory and objectClass,
    objectClass is an arry that does list all parent classes.
     
    objectcategory is only the actual "class"
     
    try a search with User as  objectcategory and then one with objectClass user.
    you will see the difference.
     
    I also started a series about AD and Powershell, I will post some more info and links there also.
     
    great to see you getting started on PowerShell .
     
    Greetings /\/\o\/\/

  2. MOW June 23, 2006 at 11:25 pm

    Another thing I did note.
     
    in the list of members :
     
    $g | get-member
     
    You should now see this:
     
       TypeName: System.DirectoryServices.SearchResult
    ……
    GetDirectoryEntry Method     System.DirectoryServices.DirectoryEntry GetDire…
    …..
    did you see the Method GetDirectoryEntry(), you can use that also to get the DirectoryEntry, like this .
     
    $ds.FindAll() | foreach {$_.GetDirectoryEntry()}
     
    Greetings /\/\o\/\/

  3. Jeffrey June 23, 2006 at 11:47 pm

    You might find the following useful – it shows you how to extend your types so you can call the method MSDN() on them to get documentation.
     
    http://blogs.msdn.com/powershell/archive/2006/06/24/644987.aspx
     
    Jeffrey Snover [MSFT]Windows PowerShell Architect

  4. Unknown July 6, 2006 at 3:08 am

    Excellent article.
     
    To improve the performance of your function, you can move the check for member from the foreach to the LDAP query. Try
     
    $ds.Filter="(&(objectClass=group)(!member=*))"
     
    This should let your searcher object do the work of identifying groups that have no members. And leveraging /\/\o\/\/’s point about the getdirectoryentry method, your pipeline can then collapse to the following:
     
    $ds.FindAll() | foreach {$egs += $_.getdirectoryentry()}
     

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: