Cmdlets are high level, task-oriented abstractions.  The implementation of cmdlets can talk to anything:  Web services, COM objects, WMI objects, .NET objects – anything.   Depending upon how developers design their API, they can make it easier or harder to write cmdlets.  Now that you can write cmdlets in PowerShell itself, here are some tips for designing your API so that it can be easily scripted against and turned into a set of cmdlets using PowerShell

  • Strong, String-Friendly, Types
    • Taking types like PSTokenType (an enum), Color, KeyGesture is great because they have validation on them, but scripters can directly pass strings like “F7” making them easy to use in PowerShell
  • Single root object
    • With a single root object ($psISE) the user can tab-complete to any part of the object model, and use Get-Member to discover it. Having many variables would make it harder to find
  • No Constructors
    • Runspaces, files and custom menus are not constructed by the user. Instead, we have Add() functions on collections. Constructors require the user to know New-Object, to know the full name of the type, and to somehow discover the parameters for the constructor, which is very difficult
      • Example: [string].GetConstructors() | %{$_.ToString()} is how you do it, but that’s NOT discoverable
    • Having Add() also means that we prevent the creation of unused objects
  • ScriptBlock, not strings for Commands
    • ScriptBlock was used instead of strings, because the parsing would be done earlier and validate the command. With ScriptBlocks, the user also gets syntax coloring. For example, $psISE.CustomMenu.Submenus.Add("dir", {dir}, "f7")
  • Handle all threading for the user
    • Modifying text, colors etc. has to be done in the UI thread. All public calls to the Object Model have wrappers so that the user does not have to do the following:
      • Invoke-HypotheticalUIDispatcher {$psise.CurrentOpenedFile.Editor.Text = “Hello”}
  • Prevent state changes external to the current command/pipeline
    • We exposed Current, instead of Selected
    • When you have multiple tabs, it is tempting to expose the Selected File, or Selected Runspace. In scripts this can be dangerous, because the user could change the Selected Runspace external to the current command/pipeline
      • For example, you define, function clear-output {$psISE.OpenedRunspaces.SelectedRunspace.Output.Editor.Text = “”}
        Then, you execute “RunSlowCommand; Clear-Output; Write-Host Complete”
        By the time, Clear-Output is run, the user might be looking at a different runspace, and be surprised when the output is cleared
    • The $psISE.CurrentOpenedRunspace, and $psISE.CurrentOpenedFile always refer to the runspace and file first seen when the command started to run
    • ‘Locking’ a property value makes the API more predictable. The Value does not change within the command

Just because the Object Model lives in .NET land, doesn’t mean we can’t make it easier to use for scripters in PowerShell land

Hope this helps,