I am trying to make my way through the PowerShell documentation and have hit a point of confusion.
I have creating a cmdlet assembly using the sample code located here.
I can load the module by issuing the command:
Import-Module -Name *NameOfAssembly*
This is, of course, if the assembly is located in a folder where PowerShell can find it.
If I create a module manifest, the only way I have been able to get the module manifest to load the assembly is to add the assembly on the RequiredModules line of the manifest. The documentation (located here) states that this doesn't actually load any modules. From what I have observed this is contradictory to what actually happens. Am I reading / understanding this incorrectly? If not, what am I missing? Is there a better way to get a cmdlet assembly (or assemblies) (I think they are called binary modules) deployed?
First of all - same as it works with script modules, you can get loading of NameOfModule.dll by simply putting it in NameOfModule subfolder of any folders listed in $env:PSModulePath
$dir = mkdir $profile\..\Modules\Greetings -Force
$dllPath = Join-Path -Path $dir.FullName -ChildPath Greetings.dll
Add-Type #'
using System.Management.Automation;
namespace SendGreeting
{
[Cmdlet(VerbsCommunications.Send, "Greeting")]
public class SendGreetingCommand : Cmdlet
{
[Parameter(Mandatory=true)]
public string Name
{
get { return name; }
set { name = value; }
}
private string name;
protected override void ProcessRecord()
{
WriteObject("Hello " + name + "!");
}
}
}
'# -OutputAssembly $dllPath
Import-Module Greetings -PassThru
In case you really need manifest (e.g. for some metadata, or external files) you have two options, depending on PowerShell version:
v2 - ModulesToProcess (works with newer versions, but causes warning)
v3+ - RootModule (fails on v2)
The key you've used, RequiredModules, is there to provide a way to name your dependencies. E.g. modules that your code depends on. RequiredAssemblies kind-of works, because adding any assembly that contains PowerShell cmdlets "just works" - but that approach kind of "hides" from future user where you defined your commands.
Related
I am trying to write a PowerShell module that relies on a small assembly. The assembly is created from a C# file (add-type -typeDefinition (get-content -raw xyz.cs) -outputAssembly xyz.dll -outputType library). Because the module requires this assembly, I have to create the assembly manually when I install the module. I am now wondering if it is possible to have PowerShell execute this add-type ... step (or any other module initialization step) automatically when it is first used
Note:
As Mathias R. Jessen points out, there is no strict need to write a helper assembly to disk - creating it in memory is sufficient for your use case (omit the -OutputType and -OutputAssembly arguments, and skip the Add-Type -LiteralPath call in the code below).
The reason that this suffices is that the existence of types used to declare parameters in a function isn't enforced at the time the function is defined (and exported), but rather at the time it is called (or its help is invoked or its name is passed to Get-Command -Syntax).
However, you may still want to use the disk-based approach, shown below, so as to incur the performance penalty that the compilation incurs only once per machine, the very first time you import the module.
Consider also implementing a versioning mechanism so that updates to your module that require an updated type definition recreate the helper assembly when needed.
The following proof of concept uses a stand-alone script module (./foo.psm1) whose top-level code tests for the existence of helper assembly foo.dll in the same location as the module, and, if not found, creates it with Add-Type.
The assembly defines sample type [demo.Foo], which the Use-Foo module function exported by the module uses as a parameter type.
In real-world usage, with a directory-based module, you'd simply point your module manifest's (*.psd1 file's) RootModule entry to the equivalent of the foo.psm1 file.
# Create demo module ./foo.psm1
#'
# Deactivate this to silence the verbose messages.
$VerbosePreference = 'Continue'
$dllPath = "$PSScriptRoot/foo.dll"
if (-not (Test-Path $dllPath)) {
Write-Verbose "Creating assembly $dllPath..."
Add-Type -ErrorAction Stop -OutputType Library -OutputAssembly $dllPath '
namespace demo {
public class Foo {
public string Bar { get { return "I am a Foo instance."; } }
}
}
'
}
else {
Write-Verbose "Using preexisting $dllPath assembly."
}
Write-Verbose "Loading assembly $dllPath..."
Add-Type -ErrorAction Stop -LiteralPath $dllPath
# Define the function to be exported, whose parameter
# uses the type defined by the helper assembly.
function Use-Foo {
param(
[demo.Foo] $foo
)
$foo.Bar
}
'# > ./foo.psm1
# Import the module, at which point the top-level code runs,
# which either creates the helper assembly or loads a previously
# created copy.
Import-Module -Force -Verbose ./foo.psm1
Write-Verbose -vb 'Calling Use-Foo...'
Use-Foo ([demo.Foo]::new())
Running the above for the first time yields the following, proving that the assembly was created on demand, loaded, and that use of the assembly-defined type as the parameter type of the exported Use-Foo function succeeded:
VERBOSE: Loading module from path '/Users/jdoe/demo/foo.psm1'.
VERBOSE: Creating assembly /Users/jdoe/demo/foo.dll...
VERBOSE: Loading assembly /Users/jdoe/demo/foo.dll...
VERBOSE: Exporting function 'Use-Foo'.
VERBOSE: Importing function 'Use-Foo'.
VERBOSE: Calling Use-Foo...
I am a Foo instance.
I am running into some issues with loading a PSM1 file. The PSM1 is always in the same folder as the PS1, but that folder can change. It works if I use a literal path like this...
Using module '\\Mac\iCloud Drive\Px Tools 4.#\Dev 4.0\#Spikes\Windows7\library.psm1'
But that's useless since the code could be installed anywhere. And the code is signed, so it can't change.
It also doesn't work to use the new (in PS3.0) $PSScriptRoot automatic variable...
Using module "$PSScriptRoot\library.psm1"
Nor does a relative path or simple file name, as in...
Using module ".\library.psm1"
or...
Using module "library.psm1"
What am I missing, other than perhaps it's time to call it a day?
Also, note that the library contains classes, so other module loading options like Import-Module don't work. This has me wondering if perhaps classes are better supported in a later version, and I should really be targeting PS 6.0, rather than 5.1?
Looking for more info on your problem, I came around this blog post. It says relative paths work, so have you tried using relative path with single quotes or without quotes?
Since values inside double-quotes get evaluated before passing them to the cmdlet, using them might not work.
you should run import-module first.
in PS1, I have add code to call import-module
like this:
ForEach($_ in Get-ChildItem "$env:TEMP\*.psm1") {
$checkModuleName = $_.Name -replace ".psm1"
$importModule = $_.Name
if (Get-Module $checkModuleName) {
Write-Host "Update Module $importModule" -ForegroundColor Green
Remove-Module $checkModuleName
Import-Module "$env:TEMP\$importModule"
}
else {
Write-Host "Import Module $importModule" -ForegroundColor Green
Import-Module "$env:TEMP\$importModule"
}
}
when import done, I can used all module.
I'm using classes in PS with WinSCP PowerShell Assembly. In one of the methods I'm using various types from WinSCP.
This works fine as long as I already have the assembly added - however, because of the way PowerShell reads the script when using classes (I assume?), an error is thrown before the assembly could be loaded.
In fact, even if I put a Write-Host at the top, it will not load.
Is there any way of forcing something to run before the rest of the file is parsed?
Transfer() {
$this.Logger = [Logger]::new()
try {
Add-Type -Path $this.Paths.WinSCP
$ConnectionType = $this.FtpSettings.Protocol.ToString()
$SessionOptions = New-Object WinSCP.SessionOptions -Property #{
Protocol = [WinSCP.Protocol]::$ConnectionType
HostName = $this.FtpSettings.Server
UserName = $this.FtpSettings.Username
Password = $this.FtpSettings.Password
}
Results in an error like this:
Protocol = [WinSCP.Protocol]::$ConnectionType
Unable to find type [WinSCP.Protocol].
But it doesn't matter where I load the assembly. Even if I put the Add-Type cmdlet on the topmost line with a direct path to WinSCPnet.dll, it won't load - it detects the missing types before running anything, it seems.
As you've discovered, PowerShell refuses to run scripts that contains class definitions that reference then-unavailable (not-yet-loaded) types - the script-parsing stage fails.
As of PSv5.1, even a using assembly statement at the top of a script does not help in this case, because in your case the type is referenced in the context of a PS class definition - this may get fixed in PowerShell Core, however; the required work, along with other class-related issues, is being tracked in GitHub issue #6652.
The proper solution is to create a script module (*.psm1) whose associated manifest (*.psd1) declares the assembly containing the referenced types a prerequisite, via the RequiredAssemblies key.
See alternative solution at the bottom if using modules is not an option.
Here's a simplified walk-through:
Create test module tm as follows:
Create module folder ./tm and manifest (*.psd1) in it:
# Create module folder (remove a preexisting ./tm folder if this fails).
$null = New-Item -Type Directory -ErrorAction Stop ./tm
# Create manifest file that declares the WinSCP assembly a prerequisite.
# Modify the path to the assembly as needed; you may specify a relative path, but
# note that the path must not contain variable references (e.g., $HOME).
New-ModuleManifest ./tm/tm.psd1 -RootModule tm.psm1 `
-RequiredAssemblies C:\path\to\WinSCPnet.dll
Create the script module file (*.psm1) in the module folder:
Create file ./tm/tm.psm1 with your class definition; e.g.:
class Foo {
# As a simple example, return the full name of the WinSCP type.
[string] Bar() {
return [WinSCP.Protocol].FullName
}
}
Note: In the real world, modules are usually placed in one of the standard locations defined in $env:PSMODULEPATH, so that the module can be referenced by name only, without needing to specify a (relative) path.
Use the module:
PS> using module ./tm; [Foo]::new().Bar()
WinSCP.Protocol
The using module statement imports the module and - unlike Import-Module -
also makes the class defined in the module available to the current session.
Since importing the module implicitly loaded the WinSCP assembly thanks to the RequiredAssemblies key in the module manifest, instantiating class Foo, which references the assembly's types, succeeded.
If you need to determine the path to the dependent assembly dynamically in order to load it or even to ad-hoc-compile one (in which case use of a RequiredAssemblies manifest entry isn't an option), you should be able to use the approach recommended in Justin Grote's helpful answer - i.e., to use a ScriptsToProcess manifest entry that points to a *.ps1 script that calls Add-Type to dynamically load dependent assemblies before the script module (*.psm1) is loaded - but this doesn't actually work as of PowerShell 7.2.0-preview.9: while the definition of the class in the *.psm1 file relying on the dependent assembly's types succeeds, the caller doesn't see the class until a script with a using module ./tm statement is executed a second time:
Create a sample module:
# Create module folder (remove a preexisting ./tm folder if this fails).
$null = New-Item -Type Directory -ErrorAction Stop ./tm
# Create a helper script that loads the dependent
# assembly.
# In this simple example, the assembly is created dynamically,
# with a type [demo.FooHelper]
#'
Add-Type #"
namespace demo {
public class FooHelper {
}
}
"#
'# > ./tm/loadAssemblies.ps1
# Create the root script module.
# Note how the [Foo] class definition references the
# [demo.FooHelper] type created in the loadAssemblies.ps1 script.
#'
class Foo {
# Simply return the full name of the dependent type.
[string] Bar() {
return [demo.FooHelper].FullName
}
}
'# > ./tm/tm.psm1
# Create the manifest file, designating loadAssemblies.ps1
# as the script to run (in the caller's scope) before the
# root module is parsed.
New-ModuleManifest ./tm/tm.psd1 -RootModule tm.psm1 -ScriptsToProcess loadAssemblies.ps1
Now, still as of PowerShell 7.2.0-preview.9, trying to use the module's [Foo] class inexplicably succeeds only after calling using module ./tm twice - which you cannot do in a single script, rendering this approach useless for now:
# As of PowerShell 7.2.0-preview.9:
# !! First attempt FAILS:
PS> using module ./tm; [Foo]::new().Bar()
InvalidOperation: Unable to find type [Foo]
# Second attempt: OK
PS> using module ./tm; [Foo]::new().Bar()
demo.FooHelper
The problem is a known one, as it turns out, and dates back to 2017 - see GitHub issue #2962
If your use case doesn't allow the use of modules:
In a pinch, you can use Invoke-Expression, but note that it's generally better to avoid Invoke-Expression in the interest of robustness and so as to avoid security risks[1]
.
# Adjust this path as needed.
Add-Type -LiteralPath C:\path\to\WinSCPnet.dll
# By placing the class definition in a string that is invoked at *runtime*
# via Invoke-Expression, *after* the WinSCP assembly has been loaded, the
# class definition succeeds.
Invoke-Expression #'
class Foo {
# Simply return the full name of the WinSCP type.
[string] Bar() {
return [WinSCP.Protocol].FullName
}
}
'#
[Foo]::new().Bar()
Alternatively, use a two-script approach:
A main script that loads the dependent assemblies,
which then dot-sources a second script that contains the class definitions relying on the types from the dependent assemblies.
This approach is demonstrated in Takophiliac's helpful answer.
[1] It's not a concern in this case, but generally, given that Invoke-Expression can invoke any command stored in a string, applying it to strings not fully under your control can result in the execution of malicious commands - see this answer for more information.
This caveat applies to other language analogously, such as to Bash's built-in eval command.
An additional solution is to put your Add-Type logic into a separate .ps1 file (name it AssemblyBootStrap.ps1 or something) and then add it to the ScriptsToProcess section of your module manifest. ScriptsToProcess runs before the root script module (*.psm1), and the assemblies will be loaded at the time the class definitions are looking for them.
Although it's not the solution per se, I worked around it. However, I'll leave the question open as it still stands
Instead of using WinSCP-types, I just use strings. Seeing as I already have enumerals that are identical to WinSCP.Protocol
Enum Protocols {
Sftp
Ftp
Ftps
}
And have set Protocol in FtpSettings
$FtpSettings.Protocol = [Protocols]::Sftp
I can set the protocol like this
$SessionOptions = New-Object WinSCP.SessionOptions -Property #{
Protocol = $this.FtpSettings.Protocol.ToString()
HostName = $this.FtpSettings.Server
UserName = $this.FtpSettings.Username
Password = $this.FtpSettings.Password
}
I used similar on [WinSCP.TransferMode]
$TransferOptions.TransferMode = "Binary" #[WinSCP.TransferMode]::Binary
First, I would recommend mklement0's answer.
However, there is a bit of running around you can do to get much the same effect with a bit less work, which can be helpful in smaller projects or in the early stages.
It's possible to merely . source another ps1 file in your code which contains your classes referencing a not yet loaded library after you load the referenced assembly.
##########
MyClasses.ps1
Class myClass
{
[3rdParty.Fancy.Object] $MyFancyObject
}
Then you can call your custom class library from your main script with a .
#######
MyMainScriptFile.ps1
#Load fancy object's library
Import-Module Fancy.Module #If it's in a module
Add-Type -Path "c:\Path\To\FancyLibrary.dll" #if it's in a dll you have to reference
. C:\Path\to\MyClasses.ps1
The original parsing will pass muster, the script will start, your reference will be added, and then as the script continues, the . sourced file will be read and parsed, adding your custom classes without issue as their reference library is in memory by the time the code is parsed.
It's still very much better to make and use a module with the proper manifest, but this will get by easy and is very easy to remember and use.
Add-Type -LiteralPath C:\Windows\Microsoft.NET\Framework\v4.0.30319\System.IO.Compression.FileSystem.dll
$psclasses = GC "C:\Windows\Temp\foobarclass.ps1" -Raw
Invoke-Expression $psclasses
[Foo]::new().Bar()
I'm writing a module, and I have some helper functions that I don't want exposed, but I do want available to module functions internally. I have set up my directory structure like:
root\
..Private
....Invoke-PrivateTest.ps1
....private.psd1
....private.psm1
..Public
....Get-Something.ps1
....Public.psd1
....Public.psm1
..test.psd1
I've setup a repository on github https://github.com/jpbruckler/test that has all the module files in it.
The behavior that I'm expecting is that Get-Something is a public function. When running Get-Command -Module Test it should be listed. On the contrary, Invoke-PrivateTest should not be in the output of that command.
When calling Get-Something it should output the text Invoke-PrivateTest called. Instead, I get an error stating that the command Invoke-PrivateTest doesn't exist.
I am explicitly saying in test.psd1 that only the Get-Something function is to be exported.
Both the Private module and the public module are being called via the NestedModules property in test.psd1. Any help or pointers would be appreciated.
Unless you have other reasons to put the code into separate (sub)modules I'd keep it in one folder and control what's exported via the function names. Use "official" notation (<Verb>-<Noun>) for the names of public functions and omit the hyphen in the names of private function (<Verb><Noun>). That way you can export public functions in your global .psd1 like this:
FunctionsToExport = '*-*'
#PetSerAl pointed me in the right direction. Ultimately this came down to a scoping issue. The way I had the module arranged, each sub-module would need to make a call to load the private module, which is a bunch of code duplication - and also what I was hoping to avoid by splitting out some helper functions.
To get it all to work, instead of multiple sub modules, I just broke up the Public folder into sub folders that will hold scripts that do similar things, basically removing all the .psd1 and .psm1 files from the Public directory. I did the same thing for the Private directory. This left me with a bunch of loose .ps1 files that I load in test.psm1 with the following code:
$Private = (Get-ChildItem -Path (Join-Path $PSScriptRoot 'Private') -Filter *.ps1)
$Public = (Get-ChildItem -Path (Join-Path $PSScriptRoot 'Public') -Filter *.ps1 -Recurse)
foreach ($Script in $Public) {
. $Script.FullName
Export-ModuleMember $Script.BaseName
}
foreach ($Script in $Private) {
. $Script.FullName
}
I've modified the test module at https://github.com/jpbruckler/test to reflect the changes I made.
I'm looking to implement a PowerShell Provider in PowerShell.
I keep thinking that if I just define the types, then import them into my session (import-module), I should be able to have them available.
For example, this does not work but its along the path of what I'd like to implement.
I'm obviously missing quite a bit...anyone know if this is possible?
# EnvironmentProvider.ps1
$reference_assemblies = (
"System.Management.Automation, Version=1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
# "System.Configuration.Install, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
)
$source = #"
namespace Providers
{
using System.Management.Automation;
using System.Management.Automation.Provider;
[CmdletProvider("Environments", ProviderCapabilities.None)]
public class EnvironmentProvider : DriveCmdletProvider
{
protected override PSDriveInfo NewDrive(PSDriveInfo drive)
{
return new EnvironmentDriveInfo(drive);
}
protected override object NewDriveDynamicParameters()
{
return base.NewDriveDynamicParameters();
}
}
public class EnvironmentDriveInfo : PSDriveInfo
{
public EnvironmentDriveInfo(PSDriveInfo driveInfo) : base(driveInfo)
{
}
}
}
"#
# -ea silentlycontinue in case its already loaded
#
add-type -referencedassemblies $referenced_assemblies -typedefinition $source -language CSharp -erroraction silentlycontinue
After import-module, I try to create the drive "environments":
new-psdrive -psprovider Environments -name "Environments" -root ""
errors with:
New-PSDrive : Cannot find a provider with the name 'Environments'.
Assuming the provider actually worked, maybe have it return a list of environments: dev, qa, staging, production.
Then I'd like to be able to re-use this through:
c:\adminlib>import-module .\EnvironmentProvider.ps1
c:\adminlib>environments:
environments:>ls
dev
qa
staging
production
environments:> cd production
environments\production> [execute actions against production]
environments\production:> cd dev
environments\dev:> [execute actions against dev, etc]
I would strongly recommend looking at the stuff Oisin wrote, suspect for people like you, who can grab their head around it, that could be very good reference on how-to. Or maybe what to avoid? ;)
You can find it on codeplex: http://psprovider.codeplex.com/
I know it's been some time since you asked the question, but I've been searching for that same answer myself. As it happens, re-reading the Samples in msdn finally got me my answer, and given the frustration quotient I thought I'd share:
The assembly containing the provider needs to be imported using Import-Module (not merely the module containing the add-type declaration). This can be done using two ways:
Option 1:
Use the parameter of Add-Type that builds the runtime assembly as a .dll file and import the file.
Option 2:
Import the runtime assembly from memory. This is how I did that with the standard msdn samples:
[appdomain]::CurrentDomain.GetAssemblies() | Where {$_.ExportedTypes -ne $null} | Where {($_.ExportedTypes | Select -ExpandProperty "Name") -contains "AccessDBProvider"} | Import-Module
Replace the Provider name in the where filter with your own.
Cheers,
Fred