Friday, December 19, 2008

Writing PowerShell cmdlets in F#

Note: This issue was resolved by the May 2009 CTP. The cmdlet code is unchanged in the Feb 2010 (v 1.9.9.9) CTP.

There's a catch to this, as I discovered today. You can't -- or to be accurate, you can't inherit PSSnapIn in F# yet. As also noted here, the F# compiler gets its knickers in a twist about the internal abstract members higher up the inheritance chain, culminating in an error stating that PSInstaller.get_RegKey() and PSInstaller.get_RegValues() are not implemented.

You can't fix this with cunning reflection tricks (if you make an override method to call via reflection into the base class, you get told that there's nothing public to override; if you make it a member, you get back to the same error as before). And you can't fix it by delegating the install to a PSSnapIn subclass in a different assembly, because that just registers the delegated assembly (which probably contains no cmdlets at all).

But you can use a CustomPSSnapIn subclass in an assembly that references the F# code. For a very simple "Hello World", based on the code samples from the Wrox PowerShell Programming book we do something like

// Specify the cmdlets that belong to this custom PowerShell snap-in.
private Collection<CmdletConfigurationEntry> cmdlets;
public override Collection<CmdletConfigurationEntry> Cmdlets
{
get
{
if (cmdlets == null)
{
string name = "fsharp-cmdlets, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null";
Assembly fsharp = Assembly.Load(name);
Type cmdlet = typeof(Cmdlet);
Type attribute = typeof(CmdletAttribute);
var have =
fsharp.GetExportedTypes().Where(t => t.IsSubclassOf(cmdlet)).Select(
t => new {Type = t, Attribs = t.GetCustomAttributes(attribute, false)}).
Where(t => t.Attribs.Length > 0).
Select(t =>
{
var attrib = t.Attribs[0] as CmdletAttribute;
return new CmdletConfigurationEntry(
// construct cmdlet name
attrib.VerbName + "-" + attrib.NounName,
t.Type, // cmdlet class type
null // help filename for the cmdlet
);
}
).ToList();
cmdlets = new Collection<CmdletConfigurationEntry>(have);
}
return cmdlets;
}
view raw gistfile1.cs hosted with ❤ by GitHub

in the custom snap-in subclass, which is the only class needed in a C# project, which generates the assembly that you actually register via installutil. This project references your F# module for the cmdlet classes that do the real work, and just does the work of publishing them to the PoSh infrastructure. The essentials of the F# code are (in the .fsi file):

type TouchFileCmdlet = class
inherit PSCmdlet
new: unit -> TouchFileCmdlet
member Path : String with set
member Date : DateTime with set
member FileInfo : FileInfo with set
end
view raw gistfile1.fs hosted with ❤ by GitHub

where the constructor declaration is essential for the cmdlet to work, and in the .fs file:

// Ignore the spaces before the attribute names
// -- it's an artefact of the syntax highlighting script
[< Cmdlet("Touch", "File", DefaultParameterSetName = "Path")>]
type TouchFileCmdlet = class
inherit PSCmdlet
val mutable path : String
val mutable fileInfo : FileInfo
val mutable date : DateTime
new() = {path = null; fileInfo = null; date = DateTime.Now}
[<Parameter(ParameterSetName = "Path", Mandatory = true, Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)>]
[<ValidateNotNullOrEmpty>]
[<Alias([|"FullName"|])>]
member x.Path
with get () = x.path
and set p = x.path <- p
[<Parameter(ParameterSetName = "FileInfo", Mandatory = true, Position = 1, ValueFromPipeline = true)>]
member x.FileInfo
with get () = x.fileInfo
and set p = x.fileInfo <- p
[<Parameter>]
member x.Date
with get () = x.date
and set p = x.date <- p
// perform the cmdlet function
override x.ProcessRecord () =…
view raw gistfile1.fs hosted with ❤ by GitHub

And then we can write F# cmdlets as we wish...

No comments :