Archive for the “Function” Category

Jeffrey Snover just published a great article on using the  capabilities of Advanced Functions. They have changed the name from Script Cmdlets to Advanced Functions and completely gotten rid of the Cmdlet keyword.

This actually makes a lot of sense IMHO. One of the beauties of this is that there is a smooth path from a basic function to an advanced function

   1: PS C:\> function foo {"foo $args"}
   2: PS C:\> foo bar
   3: foo bar
   4: PS C:\> function foo ($a) {"foo $a"}
   5: PS C:\> foo -a bar
   6: foo bar
   7: PS C:\> function foo {param($a) "foo $a"}
   8: PS C:\> foo -a bar
   9: foo bar
  10: PS C:\> function foo {param([Parameter(ValueFromPipeline=$true)]$a) "foo $a"}
  11: PS C:\> "bar" | foo
  12: foo bar
  13: PS C:\> foo bar
  14: foo bar
  15: PS C:\> foo -a bar
  16: foo bar
  17: PS C:\>

Just by setting some attributes on a parameter, you can get all kinds of great functionality for free.

Jeffrey’s function “Test-LeapYear” is a great example or template to start with. I really like how he used multi-line comments for all the Help documentation. Very slick.

Comments No Comments »

In my last post I had a function called Get-TfsWorkItem and really needed to be able to easily search based on one property of the [WorkItem] object, in this case it was title. What I needed was some kind of wildcard support in my function. Sure I could pipe it to Where-Object but that gets old, especially if I am going to be using this thing every day. So I was thinking, why not put the where directly in the function and make the back half of the where clause a parameter.

Here’s the function again:

   1: function Get-TfsWorkItem {
   2: param($title = "*",
   3:       $user="Andy Schneider",
   4:       $Project='InfrastructureAutomation'
   5:       )  
   6:         
   7: $WIQL = @"
   8: SELECT STUFF
   9: FROM THING
  10: "@
  11:  
  12: $tfs = Get-TfsServer 
  13: $workItems = $tfs.wit.query($WIQL)
  14: return $workItems | where {$_.Title -like $title}
  15: return $tfs
  16: }

Notice on line 14, rather than returning $workItems, I return $workitems piped to a where-object cmdlet that puts some condition on $workitem.Title.

That condition is specified as a parameter and defaults to * making the where-object cmdlet let everything pass. But if you wan to use it for something liket Get-TfsItem –title *PowerShell* you can, rather than doing get-tfsworkitem | where {$_.Title –like *powershell*}

You can obviously still filter on stuff later on in the pipeline, but it just gives a nice user experience, in my humble opinion.

Comments 1 Comment »

UPDATE:

/\/\o\/\/ and Joel Bennet both chimed in and provided a much more elegant solution, just override out-default. This is really what I had wanted to do, but didn’t know how. Thanks to both /\/\o\/\/ and Joel for your input. Please do check out their comments on this post. But here is the code they provided:

   1: # From /\/\o\/\/
   2:  
   3: function out-default {
   4:     $input | Tee-Object -var global:lastobject | 
   5:     Microsoft.PowerShell.Utility\out-default
   6: }
   7:  
   8: # And from Joel 
   9: # In case you are using custom formatting
  10: # You will need to override the format-* cmdlets and then
  11: # add this to your prompt function
  12:  
  13: if($LastFormat){$LastOut=$LastFormat; $LastFormat=$Null }
  14:  
  15:  
  16:  

A couple of days ago, an intern that is working for us, was helping me with a Powershell script to manage our Hyper V Cluster. The script ran fine but we were querying 7 different computers and then rolling up all the output into a custom object, so the thing took a while to run. It was just long enough to be annoying. During this process, he asked if there was a way to have PowerShell automatically store the output of the last command in a variable automatically.

I first went down the road of using Tee-object,  According to the built-in help,

The Tee-Object cmdlet send the output of a command in two directions (like the letter T). It stores the output in a file or variable, and also sends it down the pipeline. If Tee-Object is the last command in the pipeline, the command output is displayed in the console.

Here’s Tee-Object in action

PS C:\> get-process notepad | tee-object -variable note

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
     50       2     1264       6596    60     0.06   6984 notepad

PS C:\> $note

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
     50       2     1264       6596    60     0.06   6984 notepad

PS C:\>

But of course, my Intern (rightfully so) declares this to be unsatisfactory. He wants it to just happen automagically. Picky intern, huh?

Then on the bus ride home this afternoon, I was thinking about Jeffrey’s post on Push-Noun. He basically shows us how to set up a loop that goes forever, taking input and executing it only for a specific noun in Powershell.

Considering this, I realized I could do something similar for my issue. So here’s the code, stolen from Push-Noun.

function Set-LastObjectAvailable {
while ($TRUE)
{
    Write-Host "[LASTOBJECT]> " -NoNewLine
    $line = $Host.UI.ReadLine().trim()
    switch ($line)
    {
    "exit"   {return}
    "quit"   {return}
    "?"      {"Just type a command and the output will be displayed and stored in `$lastobject" }
    {$_.StartsWith("!")}
             {
                $Cmd = $_.SubString(1)
                Invoke-Expression $line |Tee-Object -varialbe lastobject | Out-Host
             }
    default  {

                Invoke-Expression $line |Tee-Object -Variable lastobject | out-host
             }
    }
}
}

And here it is in action:

PS C:\> Set-LastObjectAvailable
[LASTOBJECT]> get-process notepad

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
     50       2     1264       6600    60            6984 notepad
     52       2     1256       4412    58     0.06  10152 notepad

[LASTOBJECT]> $lastobject

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
     50       2     1264       6600    60            6984 notepad
     52       2     1256       4412    58     0.06  10152 notepad

[LASTOBJECT]> ?
Just type a command and the output will be displayed and stored in $lastobject
[LASTOBJECT]> gps notepad

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
     50       2     1264       6600    60            6984 notepad
     52       2     1256       4412    58     0.06  10152 notepad

[LASTOBJECT]> $lastobject.count
2
[LASTOBJECT]> exit
PS C:\>

This isn’t exactly bulletproof. I think I would like to have an automatic variable that stores the output of the last command that was executed, just in case you want to mess with it and you don’t want to have to run it again. Basically just override out-host to use a Tee-Object -variable lastcommandoutput or something along those lines.

Comments 5 Comments »

Anybody that has been using Powershell has created a function at some point. A function is simply a named ScriptBlock that can accept parameters. Also, you very likely know that there is a Function provider in Powershell. But I am willing to bet a lot of folks haven’t used the provider all that much. If I am wrong, I would love to hear some feedback on the topic.

So here I am going to create a basic function and then we will look at what we can do with the function provider to manipulate it.

   1: 6 >  function foo {"Hello $args"}
   2: 7 >  foo world
   3: Hello world

So now that we have a new function we can cd into the function drive as follows. Notice that now we can get some cool info about the function we just created.

   1: 11 >  cd function:
   2: 12 >  ls foo | fl
   3:  
   4:  
   5: Name        : foo
   6: CommandType : Function
   7: Definition  : "Hello $args"

Since we are in the function directory, we can rename a function quite easily.

   1: 15 >  rename-item foo bar
   2: 16 >  bar world
   3: Hello world
   4:  
   5: 17 >  ls foo
   6: Get-ChildItem : Cannot find path 'foo' because it does not exist.
   7: At line:1 char:3
   8: + ls <<<<  foo
   9: 18 >  ls bar | fl
  10:  
  11:  
  12: Name        : bar
  13: CommandType : Function
  14: Definition  : "Hello $args"

There is no longer a function called foo but we do have a function named bar with the exact same definition that foo had originally.

Armed with this information, occasionally I like to take a look at what one of my functions looks like.

To do this all I need is to run get-content on a function, or use the alias cat.

   1: 28 >  cat Function:\Add-Assembly
   2: param($name) return [System.Reflection.Assembly]::LoadWithPartialName($name)

Comments No Comments »

I have had a custom prompt for quite some time now. I am pretty sure I got started with by using the custom prompt from the PowerShell Community Extensions.

I have tweaked it here and there and apparently at some point I dropped the part of the default prompt that used $NestedPromptLevel.

This wasn’t a problem until I was reading about some great debugging techniques in PowerShell in Action by Bruce Payette which used the Nested Prompt feature.

This was quickly fixed with the following:

   1: if ($NestedPromptLevel -gt 0) {$myPrompt = "PS-nested >>"}
   2: Write-Host ($myPrompt)  

Now my nested prompt works quite well. When I hit S to suspend, I get dropped into my custom nested prompt.

   1: PS 10 >  notepad
   2: PS 11 >  gps notepad | kill -confirm
   3:  
   4: Confirm
   5: Are you sure you want to perform this action?
   6: Performing operation "Stop-Process" on Target "notepad (4512)".
   7: [Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend  [?] Help (default is "Y"): S
   8: PS-nested >> gps notepad
   9:  
  10: Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
  11: -------  ------    -----      ----- -----   ------     -- -----------
  12:      57       6     2000       9884    78     0.08   4512 notepad
  13:  
  14:  
  15: PS-nested >>
  16: PS-nested >>
  17: PS-nested >>
  18: PS-nested >> exit
  19:  
  20: Confirm
  21: Are you sure you want to perform this action?
  22: Performing operation "Stop-Process" on Target "notepad (4512)".
  23: [Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend  [?] Help (default is "Y"): Y
  24: PS 14 >

Note to self: when messing with defaults, don’t delete stuff when you don’t know what they are for.

 

Andy

Comments No Comments »

Event six was a classic programming math problem. Find all the prime numbers in a given range. For this particular problem, the range was between 1 and 200.

Here’s my answer

   1: filter select-prime 
   2: { 
   3:     if ($_ -eq 1) {return $null}
   4:     for ($i=2;$i -le ([int][Math]::Sqrt($_));$i++) 
   5:         { 
   6:         if ($_ % $i -eq 0 ) {return}
   7:         }
   8:     $_
   9: } 
  10: 1..200 | select-prime

It turns out that in order to find if a prime number we can use the modulus operator. This math operator simply returns the remainder when one number is divided by another

If a number n % x  = 0 where x is not 1 or n, then the number will not be prime.

   1: PS U:> 6 % 2
   2: 0
   3: PS U:> 5 % 3
   4: 2
   5: PS U:> 5 % 2.5
   6: 0
   7: PS U:> 9 % 2
   8: 1
   9: PS U:> 9 % 3
  10: 0

So this will work if we just went from 2 to N. But it turns out that is way more work than necessary. We only need to go up to the Square Root of N and we can call it prime.

I really thought I was being super cool and iterated up the square root of the number. Turns out the calls into .NET to calculate the square root were to costly to make my efficiency efficient.

   1: PS C:usersandysDesktop> cat C:usersandysDesktopprimes.ps1
   2: filter select-primeQuickly
   3: {
   4:         if ($_ -eq 1) {return $null}
   5:         for ($i=2;$i -le ([int][Math]::Sqrt($_));$i++)
   6:                 {
   7:                 if ($_ % $i -eq 0 ) {return}
   8:                 }
   9:         $_
  10: }
  11:  
  12: filter select-primeSlowly
  13: {
  14:         if ($_ -eq 1) {return $null}
  15:         for ($i=2;$i -le $_;$i++)
  16:                 {
  17:                 if ($_ % $i -eq 0 ) {return}
  18:                 }
  19:         $_
  20: }
  21:  
  22: "Quickly"
  23: ""
  24: measure-command {1..200 | select-primeQuickly}
  25: "Slowly"
  26: ""
  27: measure-command {1..200 | select-primeSlowly}
  28: PS C:usersandysDesktop> C:usersandysDesktopprimes.ps1
  29: Quickly
  30:  
  31: Days              : 0
  32: Hours             : 0
  33: Minutes           : 0
  34: Seconds           : 0
  35: Milliseconds      : 734
  36: Ticks             : 7346876
  37: TotalDays         : 8.5033287037037E-06
  38: TotalHours        : 0.000204079888888889
  39: TotalMinutes      : 0.0122447933333333
  40: TotalSeconds      : 0.7346876
  41: TotalMilliseconds : 734.6876
  42:  
  43: Slowly
  44:  
  45: Days              : 0
  46: Hours             : 0
  47: Minutes           : 0
  48: Seconds           : 0
  49: Milliseconds      : 346
  50: Ticks             : 3469459
  51: TotalDays         : 4.0155775462963E-06
  52: TotalHours        : 9.63738611111111E-05
  53: TotalMinutes      : 0.00578243166666667
  54: TotalSeconds      : 0.3469459
  55: TotalMilliseconds : 346.9459

Looking at the results, what I thought was going to be quick was actually a lot slower! The version that calls into .NET ran in 736 milliseconds and the version that did not ran in 346 milliseconds.

Goes to show that when you are programming or scripting, what we think may be a gain in efficiency, may very well not be, as I learned in this exercise.

Comments 2 Comments »

The beauty of Powershell is that everything in Powershell is an object. You take objects, pass them to objects, and get objects back. One of the other great features of Powershell is compatibility with older CMD commands. The problems arises though when we want to get the two working together.

A big issue with regular commands is the each one has its own way of displaying output. Interpreting and using this output programmatically can be tricky at best. Mostly for the sake of an example, I’d like to walk through the process of objectifying the output of ipconfig.

To begin, we will need to run ipconfig and store the output in a variable. However, when we run it, lets filter down as much as possible with some broad parsing and then we can filter it down and fine tune our parsing as we get closer to the actual information we want.

So here we go:

   1: PS C:\> $ip = ipconfig | Select-String "IPv4|Subnet|Gateway" | select-object -first 3
   2: PS C:\> $ip
   3:  
   4:    IPv4 Address. . . . . . . . . . . : 192.168.15.220
   5:    Subnet Mask . . . . . . . . . . . : 255.255.255.0
   6:    Default Gateway . . . . . . . . . : 192.168.15.1
   7:  
   8:  
   9: PS C:\>

Using select-string we can get all the lines that have “IPv4 OR Subnet OR Gateway.” Next we pipe this to select-object and select the first 3 lines that match our criteria. This way we are only getting the first adapter.

Now that we have this info store in the variable $ip we can start looking at parsing out the different lines. Lets just look at working with the first line as a demonstration.

   1: PS C:\> $ip[0]
   2:  
   3:    IPv4 Address. . . . . . . . . . . : 192.168.15.220
   4:  
   5: # Use multi variable assignment to get the output of the split on the string
   6: # Note that we split on ":" so we IPv4 Address set to $prop and the IP Address 
   7: # gets set to $value
   8:  
   9: PS C:\> $prop,$value = $ip[0].ToString().Split(":")
  10: PS C:\> $prop
  11:    IPv4 Address. . . . . . . . . . .
  12: PS C:\> $value
  13:  192.168.15.220
  14:  
  15: PS C:\> # Now lets clean it up a bit
  16:  
  17: PS C:\> $prop.Trim(" :")
  18: IPv4 Address. . . . . . . . . . .
  19:  
  20: # Trim off the spaces and periods.
  21: PS C:\> $prop.Trim(". :")
  22: IPv4 Address
  23:  
  24: # for this we can just trim off the leading space
  25:  
  26: PS C:\> $value
  27:  192.168.15.220
  28: PS C:\> $value.Trim()
  29: 192.168.15.220

We use the split method to extract the property and value, essentially cutting each line in half at the “:”

Once we get the individual property and value entries we can clean them up with a little haircut… using the Trim method.

The last thing we will need to do is add these properties and their values to an object. This is accomplished using the add-member cmdlet.

   1: PS C:\> $prop = $prop.Trim(". :")
   2: PS C:\> $prop
   3: IPv4 Address
   4: PS C:\> $value = $value.Trim()
   5: PS C:\> $value
   6: 192.168.15.220
   7: PS C:\> $ipo = New-Object psobject
   8: PS C:\> Add-Member -InputObject $ipo noteproperty -Name $prop -Value $value
   9: PS C:\> $ipo
  10:  
  11: IPv4 Address
  12: ------------
  13: 192.168.15.220
  14:  
  15:  
  16: PS C:\>

Now if we wrap all this up into a foreach-object command.

   1: PS C:\> $ip = ipconfig | Select-String "IPv4|Subnet|Gateway" | select-object -first 3
   2: PS C:\> $ip | % {$ipo = New-Object psobject} {$prop,$value = $_.ToString().Split(":");
   3: >> Add-member -InputObject $ipo noteproperty -Name $prop.Trim(". :") -Value $value.Trim()}
   4: >>
   5: PS C:\> $ipo
   6:  
   7: IPv4 Address                                              Subnet Mask                                               Default Gateway
   8: ------------                                              -----------                                               ---------------
   9: 192.168.15.220                                            255.255.255.0                                             192.168.15.1
  10:  
  11:  
  12: PS C:\> $ipo | fl *
  13:  
  14:  
  15: IPv4 Address    : 192.168.15.220
  16: Subnet Mask     : 255.255.255.0
  17: Default Gateway : 192.168.15.1
  18:  
  19:  
  20:  
  21: PS C:\>

Really the only tricky thing here is that we changed $ip[0] to $_ so we could process the current object each time using the “%” which is the alias for the ForEach-Object cmdlet..

This is all sweet and nice but still its a lot of work for one command and doing all the string manipulation is  a bit tough, and not repeatable for other commands. So this brings up a question for which I do not have an answer. in the long run,  do we adopt commands and coerce them into the world of objects or do we build functions/Cmdlets to replace them.

I am sure the answer is the same as what my grandpa said when I asked him if he wears briefs of boxers… “Welll.. Depends”

What say you ?

Andy

Comments 2 Comments »

Powershell can slice and dice arrays all day long, and we use them all the time. However, when we are testing stuff and creating dummy arrays the syntax can be a bit awkward. Its quite straightforward and understandable, but its just a pain to type

For example

   1: PS 39 >  $i = "one","two","three"
   2: PS 40 >  $i
   3: one
   4: two
   5: three

Now for me that is just way too many quotes and commas. This can easily be addressed with a new function called New-Array.  Are we going to have to parse and add quotes and do all kinds of crazy stuff to get this working ? Not really. Here’s the function and how to use it.

   1: PS 41 >  function new-array {$args}
   2: PS 42 >  $i = new-array one two three four five six
   3: PS 43 >  $i
   4: one
   5: two
   6: three
   7: four
   8: five
   9: six

Ah, now that is much better. All this does is return the $args array which is an array that you get in every function be default. $args[0] is the first argument. $args[1] is the second etc etc.

That being said we are not quite there.  Lets say you want to be really sure you create an Array. In the case where you originally have only 1 $arg it will return a scalar. Let me demonstrate.

   1: PS 58 >  function new-array {$args}
   2: PS 59 >  $i = new-array 1
   3: PS 60 >  $i += 2
   4: PS 61 >  $i
   5: 3

 

In this case, $i ends up being a scalar with a value of 1 and then when you add 2 to it, you get $i = 3.

What we want is to be able to use the += operator to add a value to the array. There are multiple ways to force a scalar to be an array when you declare it but i think the comma operator is the shortest.  So we make one quick addition to our new-array function.

   1: PS 62 >  function new-array {,$args}
   2: PS 63 >  $i = new-array 1
   3: PS 64 >  $i +=2
   4: PS 65 >  $i
   5: 1
   6: 2

Pretty cool!

Comments 3 Comments »