Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to export a class in a PowerShell v5 module

I've got a module setup to be like a library for a few other scripts. I can't figure out how to get a class declaration into the script scope calling Import-Module. I tried to arrange Export-Module with a -class argument, like the -function, but there isn't a -class available. Do I just have to declare the class in every script?

The setup:

  • holidays.psm1 in ~\documents\windows\powershell\modules\holidays\
  • active script calls import-module holidays
  • there is another function in holidays.psm1 that returns a class object correctly, but I don't know how to create new members of the class from the active script after importing

Here is what the class looks like:

Class data_block {     $array     $rows     $cols     data_block($a, $r, $c)     {         $this.array = $a         $this.rows = $r         $this.cols = $c     } } 
like image 450
jason Avatar asked Jun 25 '15 12:06

jason


People also ask

How do I export a PowerShell module?

The Export-ModuleMember cmdlet specifies the module members that are exported from a script module ( . psm1 ) file, or from a dynamic module created by using the New-Module cmdlet. Module members include cmdlets, functions, variables, and aliases.

How do I import a class in PowerShell?

Import Classes using Using Module Statement The assembly-importing feature is a convenience; the module-importing feature is a requirement, because there's no other way to import classes from script modules. The Using statement has a module parameter that takes a module name string or a ModuleSpecification object.

How do I create a psd1 file?

The New-ModuleManifest cmdlet creates a new module manifest ( . psd1 ) file, populates its values, and saves the manifest file in the specified path. Module authors can use this cmdlet to create a manifest for their module.

What is $using in PowerShell?

The using statement allows you to specify which namespaces are used in the session. Adding namespaces simplifies usage of . NET classes and member and allows you to import classes from script modules and assemblies. The using statements must come before any other statements in a script or module.


2 Answers

PSA: There is a known issue that keeps old copies of classes in memory. It makes working with classes really confusing if you don't know about it. You can read about it here.


using is Prone to Pitfalls

The using keyword is prone to various pitfalls as follows:

  • The using statement does not work for modules not in PSModulePath unless you specify the module's full path in the using statement. This is rather surprising because although a module is available via Get-Module the using statement may not work depending on how the module was loaded.
  • The using statement can only be used at the very beginning of a "script". No combination of [scriptblock]::Create() or New-Module seems overcome this. A string passed to Invoke-Expression seems to act as a sort of standalone script; a using statement at the beginning of such a string sort of works. That is, Invoke-Expression "using module $path" can succeed but the scope into which the contents of the module are made available seems rather inscrutable. For example, if Invoke-Expression "using module $path" is used inside a Pester scriptblock, the classes inside the module are not available from the same Pester scriptblock.

The above statements are based on this set of tests.

ScriptsToProcess Prevents Access to Private Module Functions

Defining a class in a script referred to by the module manifest's ScriptsToProcess seems at first glance to export the class from the module. However, instead of exporting the class, it "creates the class in the global SessionState instead of the module's, so it...can't access private functions". As far as I can tell, using ScriptsToProcess is like defining the class outside the module in the following manner:

#  this is like defining c in class.ps1 and referring to it in ScriptsToProcess class c {     [string] priv () { return priv }     [string] pub  () { return pub  } }  # this is like defining priv and pub in module.psm1 and referring to it in RootModule New-Module {     function priv { 'private function' }     function pub  { 'public function' }     Export-ModuleMember 'pub' } | Import-Module  [c]::new().pub()  # succeeds [c]::new().priv() # fails 

Invoking this results in

public function priv : The term 'priv' is not recognized ... +         [string] priv () { return priv } ... 

The module function priv is inaccessible from the class even though priv is called from a class that was defined when that module was imported. This might be what you want, but I haven't found a use for it because I have found that class methods usually need access to some function in the module that I want to keep private.

.NewBoundScriptBlock() Seems to Work Reliably

Invoking a scriptblock bound to the module containing the class seems to work reliably to export instances of a class and does not suffer from the pitfalls that using does. Consider this module which contains a class and has been imported:

New-Module 'ModuleName' { class c {$p = 'some value'} } |     Import-Module 

Invoking [c]::new() inside a scriptblock bound to the module produces an object of type [c]:

PS C:\> $c = & (Get-Module 'ModuleName').NewBoundScriptBlock({[c]::new()}) PS C:\> $c.p some value 

Idiomatic Alternative to .NewBoundScriptBlock()

It seems that there is a shorter, idiomatic alternative to .NewBoundScriptBlock(). The following two lines each invoke the scriptblock in the session state of the module output by Get-Module:

& (Get-Module 'ModuleName').NewBoundScriptBlock({[c]::new()}) & (Get-Module 'ModuleName') {[c]::new()}} 

The latter has the advantage that it will yield flow of control to the pipeline mid-scriptblock when an object is written to the pipeline. .NewBoundScriptBlock() on the other hand collects all objects written to the pipeline and only yields once execution of the entire scriptblock has completed.

like image 92
alx9r Avatar answered Oct 03 '22 05:10

alx9r


I found a way to load the classes without the need of "using module". In your MyModule.psd1 file use the line:

ScriptsToProcess = @('Class.ps1') 

And then put your classes in the Class.ps1 file:

class MyClass {} 

Update: Although you don't have to use "using module MyModule" with this method you still have to either:

  • Run "using module MyModule"
  • Or run "Import-Module MyModule"
  • Or call any function in your module (so it will auto import your module on the way)

Update2: This will load the Class to the current scope so if you import the Module from within a function for example the Class will not be accessible outside of the function. Sadly the only reliable method I see is to write your Class in C# and load it with Add-Type -Language CSharp -TypeDefinition 'MyClass...'.

like image 40
ili Avatar answered Oct 03 '22 03:10

ili