4 January 2013

I was building a more involved function the past couple of days for gathering data from a PC in the enterprise should troubleshooting be necessary.  The type of company I work for doesn’t always allow us to stay on the PC and troubleshoot an item (unless it’s a total blocker) so being able to gather a broad range of data is necessary to really troubleshoot since we can’t be on the machine.  Part of that function is retrieving hard drive information.  I’m only gathering a few pieces of info, but since PC’s can have multiple drives I needed a good way to manage each disk separately.  I could have done it with the following line of code, but as you can see from the screenshot, it’s messy and the numbers output are in bytes which isn’t useful at first glance.

gwmi -class win32_logicaldisk | Where {$_.DriveType -eq 3 -OR $_.DriveType -eq 4} | select -property DeviceID,Size,FreeSpace

gwmi

 

The big thing is to convert those numbers into something more meaningful.  I also don’t like the clumped output and wanted to be able to handle each disk separately and call any of it’s properties without additional formatting (to see what I mean, run that code and then try pulling just $var.DeviceID and you’ll see all the hashtable @ and{} characters… it’s a mess).  I decided to create the hashtable myself.  Since there could be multiple disks I also needed it to really be more like an object.  That’s where nesting my hashtables came in.  My other requirement, dynamically creating variables, comes from wanting to return all of my unknown-beforehand-number-of-disks within the same variable, but needing a way to make unique hashtable key names ($return.Disk1… , $return.Disk2… , etc).  It sounds confusing, so here’s the code:

Function getHD($strComputer) {
	# Retrieves hard drive information
	$return = @{}  # declare our return variable as a hashtable so it doesn't get created as a string
	$i = 0  # counter and dynamic part of variable for disks
	$hd = gwmi -comp $strComputer -class win32_logicaldisk
	ForEach ($disk in $hd) {
		If ($disk.Drivetype -eq 3 -OR $disk.DriveType -eq 4) {   # only return physical hard drive and network drive types (5 is CD)
			$i++
			$return.numDrives = $i # where I store the number of drives found so I can use that to iterate through them all later
			$return += @{
				"Disk$i" = @{ # new hashname must be in quotes so variable will be expanded
				DriveLetter = $disk.DeviceID
				Size = $disk.Size/1048576   # Convert to MB
				FreeSpace = $disk.FreeSpace/1048576   # Convert to MB
				}
			}
		}
	}
	return $return
}

 

  1. First, I declare my return variable as a hashtable so that if I wanted to store a certain bit of info at the root of it, it wouldn’t be a string.
  2. Next I declare the variable (a counter) that will be the dynamic part of my nested hashtable key name.
  3. Get the actual info, as objects with properties.
  4. Start my loop
  5. I only want physical hard disks and network drives to return, so I’m limiting what objects I actually look at based on their DriveType.  3 is physical, 4 is network.
  6. Note I’m not starting to count up on my counter until I have a match of a disk type I want.  This keeps my numbers continuous, with no gaps in the numbering for the final output.
  7. Next I’m setting my hashtable key that stores the value of the number of drives returned
  8. Now I’m adding a new (nested) hashtable to my $return hashtbale.  I do this by using the += to say I’m adding to and not replacing my hashtable, and then saying that what I’m adding is actually a hashtable itself by folling the += with the @{
  9. Now, inside the hashtable declaration, I’m creating the dynamic hashtable itself.  Note that the hashtable name is in quotes, if I didn’t use the quotes powershell would error.  Putting it in quotes causes the $i to expand to whatever it’s value is.  Thus, for each iteration it changes:  Disk1 = @{…}, Disk2=@{…}, etc
  10. Then the next few lines are me setting the key names for each disk, example:  Disk1.DriveLetter = $disk.DeviceID
    1. You can’t do this symply by typing out Disk$i.DriveLetter = $disk.DeviceID. Reason being the Disk$i is not in quotes and you can’t use . notation if it is in quotes.  (This does not work:  “Disk$i”.DriveLetter = whatever)
    2. The math in there is just me converting bytes to MB… I know, nowadays what drives aren’t in GB but that’s easy to see from MB.
  11. And then finally I return the entire $return hashtable.

So now what?  I call my function by:  $hdinfo = getHD $pc      Then I run through that returned hashtable “object” and output the data to a file.  That is just a few simple lines:

For ($i=1; $i -le $hdInfo.numDrives;$i++) {
     $hdvar = "`$hdinfo.disk$i"
     invoke-expression $hdvar | Out-file $global:consolePath\PCData\$pc\PCVitals.txt -append -noclobber
}

 

  1. I’m starting a for loop that starts at 1 (Disk1) and will keep adding up until I get to the the number of drives that I stored in that key numDrives.
  2. Next I have to dynamically retrieve that dynamically named variable.  To do that I have to set a variable to be the name of the variable since I have to expand again.  Putting the ` infromt of $hdinfo keeps it from expanding that part.  I want to keep that part of the name literal since I dont want it’s value yet.  Since I know the exact number and how the naming convention goes, I just use my $i counter and put it in the part where it belongs:  disk$i.  The resulting value of $hdvar ends up being “$hdinfo.disk1” (2,3,4, etc).  But it’s a string value, not the object/hashtable itself.  that’s why….
  3. In the last line I’m calling invoke-expression because it’s a cmdlet that tells powershell to take the contents of the variable passed to it and treat it like it’s a command that was typed at the prompt (i.e. it will convert the string to a command).  And I’m pipping it out to a file.
    1. The other thing that wont work is piping your hashtable directly to the out-file.  Your resulting file will end up just having System.Whatever.Hashtable or something like that as it can’t figure out how to convert the output so it just outputs the object type.

And what does it all look like?

Name                           Value                                                                                   
----                           -----                                                                                   
FreeSpace                      1171586.5703125                                                                         
DriveLetter                    C:                                                                                      
Size                           1437513.9921875                                                                         

Name                           Value                                                                                   
----                           -----                                                                                   
FreeSpace                      1143174.359375                                                                          
DriveLetter                    D:                                                                                      
Size                           1423843.99609375                                                                        

Name                           Value                                                                                   
----                           -----                                                                                   
FreeSpace                      1034453.83984375                                                                        
DriveLetter                    E:                                                                                      
Size                           1430727.99609375

I could do a little more formatting if I wanted, but this is good enough for me!  And if I wanted to further use that data in a function it’s easy to reference an individual item with just $hdinfo.Disk1.FreeSpace or $hdinfo.Disk2.Size


2 Responses to “Powershell: Dynamically Creating Variable Names; Nested Hashtables”

  • aardvark
    September 23rd, 2013 at 6:14 am     

    Interesting post!

    I’ve doing something similar but I have trouble outputting my data properly because format-table doesn’t understand nested arrays or hashtables. To take your example, is it possible to output your data as follows (sorry for the lousy spacing):

    Freespace DriveLetter Size
    ——— ———– —-
    1171586.5703125 C: 1437513.9921875
    1143174.359375 D: 1423843.99609375
    1034453.83984375 E: 1430727.99609375

    Thanks!

  • thegeek
    September 23rd, 2013 at 7:48 am     

    Yes it is possible, but it’s kind of confusing (at least for me). You have to build your own custom format for a table, and then pass that as the -property parameter to the format-table cmdlet. That’s a topic I’ve been wanting to post about but life keeps getting in the way of blogging here. You can check out this page: http://technet.microsoft.com/en-us/library/ee692794.aspx

    Basically, you take a variable and you assign it the columns and value that should go in each column. @{} defines the format/display of each column. (although it’s not really a hashtable as it has more properties. The properties each has are Expression, Label, and width. There may be more but I haven’t used them. The Label is just the column name. In your example it would be Freespace, DriveLetter, and Size. So you would have three @{}.

    I’m not sure how you’re creating your object but I would assume your custom table format would look like:

    $customTable = @{Expression={$_.Freespace};Label=”Freespace”;width=30},@{Expression={$_.DriveLetter};Label=”Drive Letter”;width=30},@{Expression={$_.Size};Label=”Total Size”;width=30}

    Then, you would pass your returned object to the format-table cmdlet but tell the cmdlet to use your custom format:

    $returnedObject | format-table -property $customTable

You must be logged in to post a comment.