This site uses advanced css techniques
When I first started building cmdlets to serve an important business need at a customer, one of my early wishes was to have some concept of an ongoing session, where if I made a connection or made something current, that setting would "stick" and be available for cmdlets that followed.
For years I never dug in enough to really sort this out, but recently have found the right way, and it's easy!
The key is the SessionState.PSVariable property that's available to all cmdlets derived from PSCmdlet (but not from Cmdlet), and we can fetch/store arbitrary objects by name.
These objects can be individual items, but my approach is to create a state object that goes into a single named variable; it simplifies my use case, but others might want to group things differently.
With this, we'll be able to set a configuration at the start of doing our work, not needing to provide them to the cmdlets that follow.
The first step is to create the class that will hold the state, and we're intentionally using a really simple example. These items will be presumably configured by the user at the start of some session, and future cmdlet invocations can pull this saved information if not provided explicitly on the command line:
public class MyState { public string URL { get; set; } public string Username { get; set; } }
I typically define the state class itself inside the cmdlet source area, but the object can contain other objects defined (and more widely used) elsewhere.
The PSCmdlet.SessionState.PSVariable object has Set() and GetValue() methods, both of which take a name and an arbitrary object: the former is obvious, while the latter either fetches the object if found by name in session state, or it returns the default value provided in the second argument (which is often null).
Though it's possible to do the variable manipulation inside each cmdlet's parameter processing, it's far easier to put this in the base interface class all your cmdlets were derived from (which in turns derives from PSCmdlet; see my previous post for how I usually do this).
public abstract class MyCmdlet : PSCmdlet { ... private const string VarName = "_MyState"; protected MyState getState() { var state = SessionState.PSVariable.GetValue(VarName, null) as MyState; if (state == null) SessionState.PSVariable.Set(VarName, state = new MyState()); return state; } ... }
This creates a new (and empty) MyState object the first time it's called and stores it into session state, returning that same value every time thereafter. This doesn't know anything about the specific values of items within the state object, that's for the caller to fool with.
I don't see any way to enumerate the variable names stored in session state, so it appears that you have to know the name of anything you want to work with.
Because the GetVariable()
We still have to populate our session state, and this requires an explicit first step. There are a number of ways, but I typically use an explicit Initialize-MyState cmdlet:
[Cmdlet(VerbsData.Initialize, "MyState")] public class Initialize_MyState : MyCmdlet { [Parameter(Mandatory = false)] public string URL {get; set;} [Parameter(Mandatory = false)] public string Username {get; set;} protected override void ProcessRecord() { base.ProcessRecord(); bool any = false; if (URL != null) { getState().URL = URL; any = true; } if (Username != null) { getState().Username = Username; any = true; } if ( !any) { // ERROR: you need to provide *something* } else { WriteVerbose("Session state variables initialized"); } } }
With this cmdlet in place, we can load the assembly into our PowerShell session and tell it to use a certain URL for the remainder of the session:
PS> Import-Module Blog.Cmdlet.dll PS> Initialize-MyState -URL http://unixwiz.net
Once initialized, this information hangs around and is available for future cmdlets.
Now that session state is available and possibly initialized, it's time to use it. There are lots of possible use cases, but in mine, each cmdlet supports all parameters on the command line, but will pull from session state if the user didn't provide something.
[Cmdlet(VerbsCommunications.Connect, "Server")] public class Connect_Server : MyCmdlet { [Parameter(Mandatory = false)] public string URL {get; set;} protected override void BeginProcessing() { base.BeginProcessing(); if (URL == null) URL = getState().URL; if (URL == null) { // Throw error; missing URL parameter } } protected override void ProcessRecord() { // DO THE REAL WORK } }
We use BeginProcessing() to pull URL from session state only if the -URL parameter was not provided by the caller, and it's an error if the URL is not found one way or the other.
First published: 2019/07/05