Use PowerShell to Combine Multiple Commands for Ease of Use

Use PowerShell to Combine Multiple Commands for Ease of Use

  • Comments 1
  • Likes

 

Summary: Learn how to use Windows PowerShell 2.0 to combine multiple commands into new custom functions and simplify your scripting.

 

Hey, Scripting Guy! QuestionHey, Scripting Guy! I do a lot of work from the Windows PowerShell command line. In fact, I write very few scripts, but I run lots of commands. I would like to create a custom Windows PowerShell function that contains the capability of several commands. This would allow me to be able to type a single command, and have it perform multiple actions. I used to be able to do things like this in other languages. Can it be accomplished in Windows PowerShell?

-- EJ

 

Hey, Scripting Guy! Answer Hello EJ,

Microsoft Scripting Guy Ed Wilson here. This is going to entail quite a bit of detail to answer, so I am going to get out of the way and let James get to work on your answer.

James Brundage is the founder of Start-Automating (http://www.start-automating.com), a company dedicated to saving people time and money by helping them automate. Start-Automating offers Windows PowerShell training and custom Windows PowerShell and .NET Framework development. James formerly worked on the Windows PowerShell team at Microsoft, and he is the author of the PowerShellPack, a collection of Windows PowerShell scripts to enable building user interfaces, interact with RSS feeds, get system information, and more.

 

A Series on Splatting (and Stuff)

Part 3 – Bridging Commands Together with Splatting and CommandMetaData

In the previous post, I covered how to use splatting to create commands that wrap other commands. Wrapper commands allowed us to take something unwieldy (such as Get-Counter ‘\Processor(_total)\% Processor Time’) into something convenient and memorable (such as Watch-ProcessorPerformance). Today, we learn how to build bridges.

Specifically, we learn how to bridge the functionality of two complicated commands to make one convenient one. Creating wrappers and bridges can help make a mess of long command lines into a neat pile or organized commands. We’re also going to learn how to handle wrapping commands with default parameters.

We’ll do this by writing a function called Watch-File. Watch-File will merge two commands, Register-ObjectEvent and Get-ChildItem. It will also add one parameter that will let us choose which events to watch for, which will effectively map to the –EventName parameter of Register-ObjectEvent.

Watch-File is a great candidate because:

  • There are a lot of valuable parameters we'll probably want to use in Get-ChildItem (-Filter, -Recurse, -Include, and -Exclude come to mind).
  • There are a lot of parameters in Register-Object event that might prove useful as well (such as -SourceIdentifier and -Action).
  • Register-ObjectEvent has only one parameter set (this makes this exercise simpler).

To write this command, we’ll need to understand a little more about the stuff that surrounds splatting, CommandMetaData and ProxyCommands. You used both of these things in the last article, but New-ScriptCmdlet hid them so that you didn’t have to deal with them. Now that we’re writing a more complex command, we won’t be able to avoid the inner workings.

Remember how proxy commands were designed to wrap other commands in Windows PowerShell so that they can be called remotely? Well, in order for this to work, the ProxyCommands had to be able to get a lot more information about the command than was available in the CommandInfo object (what comes back from Get-Command). In Windows PowerShell 1.0, there were a couple of internal classes that had this extra information, CommandMetaData and ParameterMetaData. In Windows PowerShell 2.0, these classes were made public and had a little bit added onto them. The ProxyCommand class takes this information and generates a parameter block and cmdlet binding.

You can add or remove items to the command and parameter metadata, so if you’re looking at wrapping commands with a few parameters, you can often approach writing it in terms of manipulating this metadata.

To get the right parameter block for these commands, we'll use some ParameterMetaData magic. Every command in Windows PowerShell has a CommandMetaData object, and that in turn contains several ParameterMetaData objects. To make this function, we need to collect all the parameters from Register-Object event, omit the two parameters we won't be wanting anymore, and then add one for the type of file change we want.

When you're using a programming language to write other programs, this is called metaprogramming. While this walkthrough covers a lot of ground about how to use splatting to fuse two functions together, it also contains a lot of very useful information about techniques you can use anytime you want to write scripts that write other scripts in Windows PowerShell.

Let's start by getting all of the parameters from Register-Object event. The command to do this is shown here:

$registerObjectEventParameters =([Management.Automation.CommandMetaData](Get-Command Register-ObjectEvent)).Parameters.Values

Now let’s filter out the parameters we don’t want:

$registerObjectEventParameters = $registerObjectEventParameters | 
   
Where-Object { 
       
$_.Name -ne "InputObject" -and $_.Name -ne "EventName"
   
}

To get the right parameter block for these commands, we'll use some ParameterMetaData magic.

In order to use the same underlying tricks that the New-ScriptCmdlet does (the New-ScriptCmdlet was discussed in yesterday’s Hey, Scripting Guy! post), we need to add these new parameters to a command metadata object. The command to do this is shown here:

$getChildItemMetaData = [Management.Automation.CommandMetaData](Get-Command Get-ChildItem)

Now let's add the parameters from Register-ObjectEvent. To do this, use the add method:

foreach ($parameter in $registerObjectEventParameters ) {
   
$null = $getChildItemMetaData.Parameters.Add($parameter.Name, $parameter)
}

We'll need to add one more parameter to the combined list, but that parameter is a lot harder to build from scratch with command metadata than it is to type, so let’s turn what we've got now into a parameter block. We can do this with a nifty method on a class that Windows PowerShell uses to make Import/Export-PSSession work: [Management.Automation.ProxyCommand]::GetParamBlock(), as shown here:

$combinedParameters = [Management.Automation.ProxyCommand]::GetParamBlock($getChildItemMetaData)

Now to add our new parameter. Our new parameter will be called -For, and it will accept as input any valid event name on the class IO.FileSystemWather. By default, it will watch for the events: Deleted, Changed, and Renamed. To make sure the arbitrary string that we give the function is actually an event name, we'll use the ValidateScript attribute. ValidateScript is a really great tool to have in your scripting repertoire, especially when you want to constrain strings to be a particular sort of information. Here's what the parameter would look like on its own:

$newParameter = '
    [ValidateScript({
        if (-not ([IO.FileSystemWatcher].GetEvent($_))) {
            $possibleEvents = [IO.FileSystemWatcher].GetEvents() | 
                ForEach-Object { $_.Name } 
            throw "$_ is not an event on [IO.FileSystemWatcher].  Possible values are: $possibleEvents"
        }
        return $true
    })]
    [string[]]
    $For = ("Deleted", "Changed", "Renamed")
'

Making one combined parameter block is now just one little string with a couple of variables:

$paramBlock = "$combinedParameters,

$newParameter"

In order to use splatting to join the commands and the default values, we'll need to keep creating hashtables to provide the input. The technique I've learned to use for this is pretty simple. Find all of the variables that have a name that matches the input for a command, and put them into a hashtable. To make sure we do this efficiently, we'll collect the parameter names from Get-ChildItem and Register-ObjectEvent in the begin block. This technique is illustrated here:

$beginBlock = {
   
$getChildItemParameterNames = 
       
(Get-Command Get-ChildItem).Parameters.Keys
   
$registerObjectEventParameterNames =
       
(Get-Command Register-ObjectEvent).Parameters.Keys    
}

You might notice that the $paramblock variable I created earlier holds a string. The $beginBlock I created above holds a script block. This is done purely for the ease of authoring. When writing scripts that will in turn write other scripts, I find that it is helpful to put any portion of the code that will not change into a script block. As script block, as you may recall, is delineated by braces (curly brackets). This does a number of things for us, such as the ones listed here:

  • It gives you nice syntax highlighting for the portion of the script you're working on.
  • It shows you any syntax errors you have in the script block, instead of seeing it in the finished script.
  • It makes sure you don't accidentally try to evaluate the variable within the string, which can be the source of pretty insidious bugs.

We're almost done. The Process block is the last piece of the function about which we need to worry. After this is completed, stringing the whole thing together is pretty trivial. The Process block needs to collect the parameters for Get-ChildItem and Register-ObjectEvent cmdlets. To do this, it will look for any variable that is declared that also has a value. Then it needs to loop through the results of Get-ChildItem, create a FileSystemWatcher for each item, and run Register-ObjectEvent once for each –For provided. The following code accomplishes all these tasks:

$processBlock = {
   
# Declare a hashtable to store each set of parameters
   
$getChildItemParameters = @{}
   
$registerObjectEventParameters = @{}
    
   
# Walk over the list of parameter names from Get-ChildItem.  Any variable
   
# that is found will be put into the hashtable
   
foreach ($parameterName in $getChildItemParameterNames) {
       
$variable = Get-Variable -Name $parameterName -ErrorAction SilentlyContinue
       
if ($variable -and $variable.Value) {
           
$getChildItemParameters[$parameterName] = $variable.Value
       
}
   
}
    
   
# Do the same thing for Register-ObjectEvent
   
foreach ($parameterName in $registerObjectEventParameterNames) {
       
$variable = Get-Variable -Name $parameterName -ErrorAction SilentlyContinue
       
if ($variable -and $variable.Value) {
           
$registerObjectEventParameters[$parameterName] = $variable.Value
       
}
   
}
    
    
   
Get-ChildItem @GetChildItemParameters |
       
ForEach-Object {
           
# Store away $_ into its own variable
           
$file = $_
            
           
# Create a FileSystemWatcherObject to watch for the event
           
$watcher = New-Object IO.FileSystemWatcher -Property @{
               
Path = $file.Directory
               
Filter = $file.Name
           
}
            
           
# Pack the $watcher into the hashtable of parameters for Register-ObjectEvent
           
$RegisterObjectEventParameters["InputObject"] = $Watcher
           
foreach ($f in $for) {
               
$RegisterObjectEventParameters["EventName"] = $f
               
Register-ObjectEvent @RegisterObjectEventParameters
           
}                       
       
}

}

To combine it all, we need to simply add the param, begin, and process blocks together. We also need to add the CmdletBindingBlock from whatever command we're primarily wrapping (in this case, Get-ChildItem):

$watchFileFunction = "
function Watch-File {    
    $([Management.Automation.ProxyCommand]::GetCmdletBindingAttribute($getChildItemMetaData))
    param(  
    $paramBlock
    )
    
    begin {
        $beginBlock
    }
    
    process {
        $processBlock
    }
}
"

Let’s take it for a quick spin. The command to do this is really easy, as shown here:

. ([ScriptBLock]::Create($watchFileFunction))

Let’s create 10 test files. These new files will be used to test the file system watcher:

1..10 | 
   
ForEach-Object {
      
$_ > "${_}.test.txt" 
   
}

Now let’s watch for the deletion of those files, with an Action that should tell us which file was deleted. The code to do this is shown here:

Watch-File -Filter *.test.txt -For "Deleted" -Action {
   
Write-Host "File Deleted - $($eventArgs.FullPath)" -ForegroundColor "Red" 
}

To make things more interesting, let's pick a random file to delete.

There's a nifty Windows PowerShell trick used here: -Path is a parameter that can be supplied from the pipeline by a property name. This means that if a magic object came along with a path property, Get-ChildItem would use this property for -Path. It also means that you can supply a script block to figure out the path each time, which is what we do here.

When you run this pipeline, a random file will be deleted and you should see a message in red telling you which one was deleted. The code to delete the random file is shown here:

1..10  | 
   
Get-Random | 
   
Remove-Item -Path  { "$_.test.txt" }

The message from running the previous command that deletes the random file is shown in the following image.

Image of message from running command

Great! Now you know how to make wrapper functions that wrap a single command, and bridge functions to wrap more than one command. This enables you to create quickly, complicated functions for things that already have a command. In the next article, we’ll see how you can use splatting and private commands to take something messy in .NET Framework and turn it into something simpler in Windows PowerShell.

EJ, that is all there is to creating wrapper functions that wrap a single command and bridge functions that wrap more than one command. Guest Blogger Week will continue tomorrow when James talks about using splatting to simplify messy .NET Framework code.

We invite you to follow us on Twitter and Facebook. If you have any questions, send email to us at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

 

Ed Wilson and Craig Liebendorfer, Scripting Guys

Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment
  • I am new to powershell. I wanted to invoke multiple powershell commands from command prompt.How can I do this?