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; | |
} |
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 |
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 () =… |
And then we can write F# cmdlets as we wish...
No comments :
Post a Comment