Quickstart #2: Dynamic Parameters 1 of 3 – Statically Defined Parameters

Normally parameters for a Cmdlet are defined directly as properties on the Cmdlet class itself. There are a few other ways for a Cmdlet to define parameters, and in this short series of three parts, I’ll cover the three variants you’ll likely to encounter as you become a more experienced PowerShell developer. First thing you need to do is to implement the System.Management.Automation.IDynamicParameters interface on your Cmdlet. This interface has one member, GetDynamicParameters. This interface can be implemented on either a Provider or a Cmdlet. In the former case, a provider is able to add new parameters at runtime to certain built-in Cmdlets like Get-ChildItem, Get-Item & Get-ItemProperty. It is typically used in a path context-aware manner – e.g. depending on whether the current argument is a folder or item, and what Type such a folder or item might be. We’re just to cover the Cmdlet variant in this post.

image

My example is just a simple Cmdlet named Get-OSVersion. It writes out Environment.OSVersion to the pipeline, and will dynamically add a parameter. It adds –EnsureElevated if you’re running Vista or higher, otherwise it will add –EnsureAdministrator. Statically defined dynamic parameter sets are defined by creating a simple class and decorating that class with parameter/properties you want to surface as if that class was the Cmdlet itself, then returning it from the GetDynamicProperties method at runtime. Simple!

  1. [Cmdlet(VerbsCommon.Get, "OSVersion")]   
  2. public class GetOSVersionCommand : PSCmdlet, IDynamicParameters   
  3. {   
  4.     private const string SWITCH_VISTA = "EnsureElevated";   
  5.     private const string SWITCH_WINXP = "EnsureAdministrator";   
  6.   
  7.     protected override void EndProcessing()   
  8.     {   
  9.         WriteObject(Environment.OSVersion);   
  10.            
  11.         string switchName = IsVistaOrHigher() ?   
  12.             SWITCH_VISTA : SWITCH_WINXP;   
  13.   
  14.         // not really a warning, just nice yellow text.   
  15.         WriteWarning(String.Format("{0}.IsPresent {1}",   
  16.             switchName, IsSwitchPresent(switchName)));   
  17.     }   
  18.   
  19.     // IDynamicParameters.GetDynamicParameters   
  20.     public object GetDynamicParameters()   
  21.     {   
  22.         if (IsVistaOrHigher())   
  23.         {   
  24.             return new VistaParameters();   
  25.         }   
  26.         else  
  27.         {   
  28.             return new WinXPParameters();   
  29.         }   
  30.     }   
  31.        
  32.     private bool IsSwitchPresent(string name)   
  33.     {   
  34.         // parameters bound at runtime   
  35.         Dictionary<stringobject> parameters = MyInvocation.BoundParameters;   
  36.   
  37.         // determine whether switch is set   
  38.         if (parameters.ContainsKey(name))   
  39.         {   
  40.             return ((SwitchParameter) parameters[name]).IsPresent;   
  41.         }   
  42.         return false;   
  43.     }   
  44.   
  45.     private static bool IsVistaOrHigher()   
  46.     {   
  47.         return (Environment.OSVersion.Version.Major >= 6);   
  48.     }   
  49.   
  50.     // dynamic parameters for Vista   
  51.     internal class VistaParameters   
  52.     {   
  53.         [Parameter]   
  54.         public SwitchParameter EnsureElevated   
  55.         {   
  56.             getset;   
  57.         }   
  58.     }   
  59.   
  60.     // dynamic parameters for Windows XP   
  61.     // (powershell won't run on windows 2000 ;-))   
  62.     internal class WinXPParameters   
  63.     {   
  64.         [Parameter]   
  65.         public SwitchParameter EnsureAdministrator   
  66.         {   
  67.             getset;   
  68.         }   
  69.     }   
  70. }  

Of course, adding Dynamic Parameters is no good if you don’t know how to read them back. In the code, you can see I’m using MyInvocation.BoundParameters to check if the Switches have been set.

Quickstart #1 - A Cmdlet That Processes Files and Directories

It seems lots of people are catching the PowerShell bug these days. People who previously considered themselves as administrators have learned enough of the magic from using PowerShell from day to day over the last few months that they feel comfortable enough to tinker around with C#.

This is an example Cmdlet skeleton that is designed to work with Files and Directories. It looks after resolving wildcards, supports –WhatIf and –Confirm, accepting files and/or directories from the pipeline or accepting strings of paths from the pipeine. It also ensures that the only types of paths it can accept lie on the File System. Here’s an example of the invocation styles it supports:

cmdlet

Here’s the source code for this. It doesn’t really do a lot. It spits out two different types of custom objects: one for a file, and another for a directory. Feel free to do what you like with it; it’s yours. Have fun.

using System;
using System.Collections.Generic;
using System.IO;
using System.Management.Automation;
using Microsoft.PowerShell.Commands;
namespace PSQuickStart
{
    [Cmdlet(VerbsCommon.Get, Noun,
        DefaultParameterSetName = ParmamSetPath,
        SupportsShouldProcess = true)
    ]
    public class GetFileMetadataCommand : PSCmdlet
    {
        private const string Noun = "FileMetadata";
        private const string ParamSetLiteral = "Literal";
        private const string ParmamSetPath = "Path";
        private string[] _paths;
        private bool _shouldExpandWildcards;
        [Parameter(
            Position = 0,
            Mandatory = true,
            ValueFromPipeline = false,
            ValueFromPipelineByPropertyName = true,
            ParameterSetName = ParamSetLiteral)
        ]
        [Alias("PSPath")]
        [ValidateNotNullOrEmpty]
        public string[] LiteralPath
        {
            get { return _paths; }
            set { _paths = value; }
        }
        [Parameter(
            Position = 0,
            Mandatory = true,
            ValueFromPipeline = true,
            ValueFromPipelineByPropertyName = true,
            ParameterSetName = ParmamSetPath)
        ]
        [ValidateNotNullOrEmpty]
        public string[] Path
        {
            get { return _paths; }
            set
            {
                _shouldExpandWildcards = true;
                _paths = value;
            }
        }
        protected override void ProcessRecord()
        {
            foreach (string path in _paths)
            {
                // This will hold information about the provider containing
                // the items that this path string might resolve to.                
                ProviderInfo provider;
                // This will be used by the method that processes literal paths
                PSDriveInfo drive;
                // this contains the paths to process for this iteration of the
                // loop to resolve and optionally expand wildcards.
                List<string> filePaths = new List<string>();
                if (_shouldExpandWildcards)
                {
                    // Turn *.txt into foo.txt,foo2.txt etc.
                    // if path is just "foo.txt," it will return unchanged.
                    filePaths.AddRange(this.GetResolvedProviderPathFromPSPath(path, out provider));
                }
                else
                {
                    // no wildcards, so don't try to expand any * or ? symbols.                    
                    filePaths.Add(this.SessionState.Path.GetUnresolvedProviderPathFromPSPath(
                        path, out provider, out drive));
                }
                // ensure that this path (or set of paths after wildcard expansion)
                // is on the filesystem. A wildcard can never expand to span multiple
                // providers.
                if (IsFileSystemPath(provider, path) == false)
                {
                    // no, so skip to next path in _paths.
                    continue;
                }
                // at this point, we have a list of paths on the filesystem.
                foreach (string filePath in filePaths)
                {
                    PSObject custom;
                    // If -whatif was supplied, do not perform the actions
                    // inside this "if" statement; only show the message.
                    //
                    // This block also supports the -confirm switch, where
                    // you will be asked if you want to perform the action
                    // "get metadata" on target: foo.txt
                    if (ShouldProcess(filePath, "Get Metadata"))
                    {
                        if (Directory.Exists(filePath))
                        {
                            custom = GetDirectoryCustomObject(new DirectoryInfo(filePath));
                        }
                        else
                        {
                            custom = GetFileCustomObject(new FileInfo(filePath));
                        }
                        WriteObject(custom);
                    }
                }
            }
        }
        private PSObject GetFileCustomObject(FileInfo file)
        {
            // this message will be shown if the -verbose switch is given
            WriteVerbose("GetFileCustomObject " + file);
            // create a custom object with a few properties
            PSObject custom = new PSObject();
            custom.Properties.Add(new PSNoteProperty("Size", file.Length));
            custom.Properties.Add(new PSNoteProperty("Name", file.Name));
            custom.Properties.Add(new PSNoteProperty("Extension", file.Extension));
            return custom;
        }
        private PSObject GetDirectoryCustomObject(DirectoryInfo dir)
        {
            // this message will be shown if the -verbose switch is given
            WriteVerbose("GetDirectoryCustomObject " + dir);
            // create a custom object with a few properties
            PSObject custom = new PSObject();
            int files = dir.GetFiles().Length;
            int subdirs = dir.GetDirectories().Length;
            custom.Properties.Add(new PSNoteProperty("Files", files));
            custom.Properties.Add(new PSNoteProperty("Subdirectories", subdirs));
            custom.Properties.Add(new PSNoteProperty("Name", dir.Name));
            return custom;
        }
        private bool IsFileSystemPath(ProviderInfo provider, string path)
        {
            bool isFileSystem = true;
            // check that this provider is the filesystem
            if (provider.ImplementingType != typeof(FileSystemProvider))
            {
                // create a .NET exception wrapping our error text
                ArgumentException ex = new ArgumentException(path +
                    " does not resolve to a path on the FileSystem provider.");
                // wrap this in a powershell errorrecord
                ErrorRecord error = new ErrorRecord(ex, "InvalidProvider",
                    ErrorCategory.InvalidArgument, path);
                // write a non-terminating error to pipeline
                this.WriteError(error);
                // tell our caller that the item was not on the filesystem
                isFileSystem = false;
            }
            return isFileSystem;
        }
    }
}

Changes from CTP2 in PowerShell 2.0 in the Windows 7 M3 “PDC” Build

Here’s just a quick rundown of the changes in PowerShell 2.0 in comparison to the last public release, CTP2. This may be helpful to those of you who don’t have access to the Windows 7 PDC bits. Remember, things are still subject to change - it’s not in beta yet.

Unchanged cmdlets are not listed here.

Added (or renamed*) Cmdlets

Enter-PSSession
Remove-PSSession
Get-PSSession
Exit-PSSession
Get-PSSessionConfiguration
Unregister-PSSessionConfiguration
Register-PSSessionConfiguration
Debug-Process
Test-ModuleManifest
New-ModuleManifest
New-WSManSessionOption
New-PSSession
Import-PSSession
Export-PSSession
Set-PSSessionConfiguration
Get-WSManInstance
Get-WSManCredSSP
Enable-WSManCredSSP
Set-WSManInstance
Set-WSManQuickConfig
New-WSManInstance
Remove-WSManInstance
Ping-WSMan
New-WebServiceProxy
Get-PSTransaction
Connect-WSMan
Disable-WSManCredSSP
Invoke-WSManAction
Disconnect-WSMan
Get-ComputerRestorePoint
Disable-ComputerRestore
Enable-ComputerRestore
Get-Counter
Clear-EventLog
Export-Counter
Import-Counter
Checkpoint-Computer
Restart-Computer
Stop-Computer
Ping-Computer
Resume-BitsTransfer
Set-BitsTransfer
Suspend-BitsTransfer
Add-BitsFile
Clear-BitsTransfer
Remove-EventLog
Restore-Computer
New-Module
Get-HotFix
Complete-BitsTransfer
Write-EventLog
Show-EventLog
Limit-EventLog
Get-BitsTransfer
New-BitsTransfer
New-EventLog

Changed Cmdlets (parameter or parameterset differences)

Invoke-Command
Receive-PSJob
Set-WmiInstance
Stop-PSJob
Wait-PSJob
Remove-PSJob
Export-ModuleMember
Start-PSJob
Get-PSJob
Add-Module
Set-Service

   

Removed (or renamed*) Cmdlets

Pop-Runspace
New-Runspace
Push-Runspace
Get-Runspace
Remove-Runspace

   

* e.g. The *-Runspace Cmdlets have been renamed to *-PSSession

PowerShellCommunity.org Convention Survey

Fellow MVP Hal Rottenberg asked me to highlight this short and painless survey we’re trying to get out to y’all.

Think of the following in terms of impact on the greatest number of people at a tech convention such as Microsoft TechEd or MMS. Your opinions will help shape our plans for future shows.

Thanks!

Hal Rottenberg
Community Director, PowershellCommunity.org

It shouldn’t take you too long ;-)

PowerShell v2.0 Timeline Announced

So it looks like the High King of PowerShell has spoken over at TechED Europe today and answered the question that sits on every child’s lips in the back of a moving car: “Are we there yet?”

“Nope. Not Yet.”

“So when, dad?”

Dmitry Sotnikov (who’s  not my dad, btw) of PowerGUI fame broke the news to the web at large, and I’m just shamelessly grabbing the HTML directly from his post:

  • December 2008 - CTP 3 (Community Technology Preview) or Beta 1 if it meets the internal criteria and all names/features are finalized.
  • RTM - end of 2009/early 2010 as part of Windows 7 and Windows Server 2008 R2.
  • RTM for XP, 2003, Vista, and 2008 - as a downloadable package (with new WinRM bundled in there) only a few months later.

Yay!

Sapien PrimalForms GUI Editor for PowerShell Released

I’d say a lot of folks will be chomping at the bit for this. It’s the Tk to PowerShell’s Tcl and is really the final piece of the puzzle where the shell’s position as the de-facto Windows Shell stands anyway. Well, maybe Remoting support for XP and Windows Server 2003 is ahead there just a bit, but a forms designer must be a close second.

Check it out: http://blog.sapien.com/index.php/2008/11/03/free-primalforms-tool-for-powershell-released/

Congratulations guys on shipping such a fine tool, for FREE. All it costs you is an email address. Yah, sell your soul, it’ll be worth it.

Managed Code in InfoPath Workflow Task Forms - Why isn’t this categorized as a bug?

I’ve been performing the “resolution” listed in this article since I first discovered the problem, but never ever did I think it was a “feature?” I was sure that I was just the unluckiest person alive and was always working with some dodgy permissions configuration of the 12 hive, but lo and behold, no. This is just the way it’s meant to be.

Anyone else think this is just pure madness? By default any forms you create in Visual Studio have a managed component. Hitting F5 to run deploy/debug won’t ever do the work for you either. Why isn’t this mentioned anywhere in any sort of official capacity? Anybody?


Managed code may not be executed if an InfoPath 2007 workflow form is deployed as a feature for a workflow

SYMPTOMS

Consider the following scenario. A Microsoft Office InfoPath 2007 workflow form contains managed code. The form is deployed as a feature for a workflow. In this scenario, the managed code may not be executed.

CAUSE

This issue occurs because a compiled DLL becomes part of the InfoPath form template (XSN) file when you create managed code behind an InfoPath form. Microsoft Office Forms Services does not extract a DLL by expanding the XSN file when the XSN file is installed by using the features functionality.

RESOLUTION

To resolve this issue, copy the compiled DLL to the same folder to which your feature XSN file was deployed.

http://support.microsoft.com/kb/930894/en-us

Tips on SharePoint Workflow, SharePoint Activities and MethodInvoking callbacks

One thing I’ve noticed about the SharePoint callback-based Activities in Workflow is that they are extremely flakey if you try to reference the member variable reference for the owning Activity directly instead of casting the “sender” argument in the event handler itself. You have a SharePoint SetState Activity, for exmaple, along with the corresponding MethodInvoking hook and I don’t mean the State Machine transition Activity, but rather the SharePoint-specific Activity that lets you override the Workflow’s overall state string using the <ExtendedStatusColumnValues> metadata in Workflow.xml. In my case, I use the MySetState_MethodInvoking hook to set the state at runtime depending on several conditions. If I try to set properties on the workflow's MySetState member variable directly like this:

private void MySetState_MethodInvoking(object sender, EventArgs e) {
    this.MySetState.State = SPWorkflowStatus.Max + 2;
}

This just plainly doesn’t work reliably. It doesn’t throw any exceptions, in fact it looks to work just fine, except the state is not changed. The same seems to go for any other of the SharePoint interfaces that involve callbacks. In my experience, they just don’t work most of the time especially if the workflow dehydrates before the callback is invoked. This leads me to think that there are some quirky interactions going on internally that somehow cause the workflow to ignore your attempts to change anything, perhaps due to the workflow getting rehydrated after you have made the changes, or who knows. All I know is that if you want this stuff to work reliably, ALWAYS CAST THE SENDER to your target Activity before attempting to manipulate the owning Activity. Using the member variable is just flakey. If anyone has any information on this, let me know. This works reliably:

private vid MySetState_MethodInvoking(object sender, EventArgs e) {
    ((SetState)sender).State = SPWorkflowStatus.Max + 2;
}

Anyway, have fun.

A trick to jump directly to a Cmdlet’s implementation in Reflector

Fellow PowerShell MVP and developer all-round expert administrator Brandon Shell asked me how he could navigate to a Cmdlet’s implementation in Lutz Roeder’s Reflector -- which has now been acquired by Red-Gate Software btw. At first I was going to explain to him how snap-ins work and how to figure out where things are, and then a really simple trick hit me that uses some fairly secret command-line parameter that Reflector can accept, namely the /select parameter:

  1. function Reflect-Cmdlet {  
  2.     param([Management.Automation.CommandInfo]$command)  
  3.     if ($input) {  
  4.         trap { $_; break }  
  5.         $command = $input | select -first 1  
  6.     }      
  7.          
  8.     # resolve to command if this is an alias  
  9.     while ($command.CommandType -eq "Alias") {  
  10.         $command = Get-Command ($command.definition)  
  11.     }  
  12.       
  13.     $name = $command.ImplementingType      
  14.     $DLL = $command.DLL  
  15.       
  16.     if (-not (gcm reflector.exe -ea silentlycontinue)) {  
  17.         throw "I can't find Reflector.exe in your path." 
  18.     }  
  19.        
  20.     reflector /select:$name $DLL 

Just pipe the output of get-command to it, like: gcm dir | reflect-cmdlet and Reflector will open up with the class selected (it takes a few seconds).

Update: Doug pointed out in a comment that the gcm reflector.exe line could benefit from an erroraction to keep it silent on failure, so only the throw message shows. Thanks Doug!

Naming your Cmdlets and Functions: Verbs and Nouns

The verbs list for naming your Cmdlets is pretty strict. You should definitely try to pick one of the known verbs. You can see them here:

function Get-Verb {
    [psobject].assembly.getexportedtypes() | `
        ? {$_.name -like "Verbs*"} | gm -static -membertype property | `
        sort Name | select Name
}

ps> Get-Verb

The noun end of the spectrum is pretty open-ended. Single nouns are better, and try to use the singular if you can. Cmdlets typically return one or more objects. so the singular form is the convention - better than trying to name your Cmdlet "Get-Noun(s)" which is just plain silly.

To prefix or not to prefix: this is a sticky one. The Religious Right, aka the Microsoft Powershell team, dictate that you should not prefix. The reality is more complicated and is under constant debate. If you pick a verb-noun pair that already exists and is currently loaded into powershell, you must disambiguate the command with the snapin name (snapinname\verb-noun) or it won't run. The snapin name is typically long and ugly. A noun prefix is typically short and sweet. The problem with the latter is that your commands are now harder to discover for someone who doesn't know they are there (and as such doesn't know your magic prefix). Ultimately though, people load your snapin explicitly and should know the prefix, so I tend to lean towards this although I feel a strong connection with the "One Noun Way."

(taken from one of my newgroup posts)

About the author

Irish, PowerShell MVP, .NET/ASP.NET/SharePoint Developer, Budding Architect. Developer. Montrealer. Opinionated. Montreal, Quebec.

Month List

Page List