How to make sure PowerShell module manifest (.psd1) is used - powershell

I created a PowerShell module MyUtil.psm1 and a manifest file MyUtil.psd1 for it. In the psd1 file it has the prefix defined to prevent name conflicts for exported functions:
DefaultCommandPrefix = 'MyToolbox'
This way, after running Import-Module .\MyUtil.psd1, a function like Get-Command in the psm1 file will be Get-MyToolboxCommand, everything is fine. But if someone runs Import-Module .\MyUtil.psm1 to import the psm1 file directly, the psd1 file is simply not used and the prefix I want won't be applied.
If I want to prevent this (importing MyUtil.psm1 directly), is there a way to ONLY allow importing the corresponding psd1 manifest file instead of the psm1 file? Or a programmatic way to detect that this module was not imported through psd1 so I can warn the user to use psd1?

Ok, this is a bit annoying, but it works. You can use the Export-ModuleMember cmdlet with no args to stop the psm1 from exporting anything, but you need to send a value from the psd1 to the psm1 during the import. That way we know when we're being called with the psd1. So first off, add a value to the PrivateData hashtable in your PSD:
PrivateData = #{
FromPSD = $true
PSData = #{
Then you need to access it in the psm1 file. You can in a function, but not inline, so we have to stick it into a function.
function Get-PD
{
[CmdletBinding()]
$MyInvocation.MyCommand.Module.PrivateData
}
(I totally stole this from this SO answer Accessing PrivateData during Import-Module).
Then you wrap it all up by calling this code in the module which will get run as the module is loaded.
$MyPD = Get-PD
if($MyPD.Count -eq 0)
{
Export-ModuleMember
}
Now, if you don't want this pesky extra function to be referenced when the module is loaded, you'll need to populate the FunctionsToExport in the psd1 file with the list of functions you want the users to have access to.

You can use: #Requires
For example:
Require that Hyper-V (version 1.1 or greater) is installed.
#Requires -Modules #{ ModuleName="Hyper-V"; ModuleVersion="1.1" }
Requires that Hyper-V (only version 1.1) is installed.
#Requires -Modules #{ ModuleName="Hyper-V"; RequiredVersion="1.1" }
Requires that any version of PSScheduledJob and PSWorkflow, is installed.
#Requires -Modules PSWorkflow, PSScheduledJob
About Requires

Related

I created a custom powershell .psm1 module but it won't update after an edit

I created a custom powershell module in the
C:\Program Files\WindowsPowerShell\Modules\PennoniAppManagement directory. Whenever I make changes to a function in the module, then import the module into a script, the updated code won't take effect. Any solutions?
Make sure you remove the already-loaded version of the module from the session before re-importing it:
Remove-Module PennoniAppManagement -Force
Import-Module PennoniAppManagement
Normally, Import-Module-Force - by itself - is enough to force reloading of an updated module into the current session.
Import-Module -Force implicitly performs Remove-Module before reloading the module (if the module isn't currently loaded, -Force just loads the module normally).
Also note that force-reloading a module is not an option if you're loading it via a using module statement (at least as of PowerShell 7.1.2). Notably the using module method of importing is required if a module exports custom class definitions that the caller should see - see this answer for details.
Mathias' two-step approach - Remove-Module -Force, followed by Import-Module - is apparently needed in some cases, and seems to be required in yours.
It would be good to understand when the two-step approach is needed. Mathias thinks it is related to cached versions of custom class definitions (used module-internally) lingering instead of getting reloaded and redefined when Import-Module -Force is called. That is, while the module overall may get reloaded, it may be operating on stale classes. At least in the simple scenario below I was not able to reproduce this problem, neither in Windows PowerShell 5.1, nor in PowerShell (Core) 7.2.1, but there may be scenarios where the problem does surface.
The Remove-Module documentation describes the -Force parameter solely as relating to the - rarely used - .AccessMode property available on a loaded module's module-information object (you can inspect it with (Get-Module ...).AccessMode). The default value is ReadWrite, which allows unloading (removal) of the module anytime. If the property value is ReadOnly, Remove-Module -Force is needed to unload; if it is Constant, the module cannot be removed from the session at all, once loaded - at least not with Remove-Module.
Notably, the implicit unloading that happens with Import-Module -Force is not subject to these restrictions and implicitly unloads a module even if its .AccessMode is Constant (as of PowerShell 7.1.2; I am unclear on whether that is by design).
Test code involving reloading a module with a modified class definition, to see if Import-Module -Force is enough:
# Create a template for the content of a sample script module.
# Note: The doubled { and } are needed for use of the string with
# with the -f operator later.
$moduleContent = #'
class MyClass {{
[string] $Foo{0}
}}
function Get-Foo {{
# Print the property names of custom class [MyClass]
[MyClass]::new().psobject.Properties.Name
}}
'#
# Create the module with property name .Foo1 in the [MyClass] class.
$moduleContent -f 1 > .\Foo.psm1
# Import the module and call Get-Foo to echo the property name.
Import-Module .\Foo.psm1; Get-Foo
# Now update the module on disk by changing the property name
# to .Foo2
$moduleContent -f 2 > .\Foo.psm1
# Force-import (reload) the module and
# see if the property name changed.
Import-Module -Force .\Foo.psm1; Get-Foo
# Clean up.
Remove-Item .\Foo.psm1
In both Windows PowerShell (whose latest and last version is v5.1) and PowerShell (Core) 7.2.1 (current as of this writing), the above yields, as expected:
Foo1 # Original import.
Foo2 # After modifying the class and force-reloading

Using Module and path

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.

Error while doing Get-Module in powershell 2.0

I'm new to powershell.I'm facing an error while working on an existing script.
In C:/Scripts I have 3 files . One .ps1 file and 2 .psm1 file.
ps1 file Name : RunScript.ps1
This imports .psm1 files like below.
$modules = #(
"$PsscriptRoot\Modules\Module1.psm1",
"$PsscriptRoot\Modules\Module2.psm1"
)
foreach ($module in $modules) {
if (-not $(Get-Module $module)) {
Import-Module $module -Force -WarningAction SilentlyContinue
}
}
This above code is giving me error as below
Get-Module : Running the Get-Module cmdlet without ListAvailable
parameter is not supported for module names that include a path. Name
parameter has this element 'C:\Scripts\Modules\Module1.psm1' which
resolves to a path. Update the Name parameter to not have path
elements, and then try again.
It is expecting to not give path of the file. But how do I do Get-Module in this scenario. Any help is very helpful.Thanks.
Get-Module returns modules imported into the current session or modules installed that are available for you to import. You should install your module then it will be available with the get-module cmdlet by using the name of your module.
I'm not quite sure what you're trying to do with your script? You only want to import it if its not already imported?
I would have added this as a comment but I cant yet...

PowerShell: Unable to find type when using PS 5 classes

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()

Dot source a "script" within a function

My PowerShell profile is getting a bit cumbersome and I find that I don't always use everything in it. I want to reduce the size of my profile and speed up the startup time that's been getting slower and slower, but I still want to be able to access those functions relatively quickly when I need them.
Is there a way to "dot source" a set of PowerShell functions and aliases from within a separate function such that the sourced functions will be available outside of that function call?
sourcing
After a great deal of effort (and to make sure I won't lose it later), here's what I've found works.
I've placed the following function in my profile:
function extras {. C:\...\WindowsPowerShell\extrafunctions.ps1}
And then I "dot source" the function within the PowerShell window.
. extras
And all my extra functions are available to the session without slowing down the startup time.
As others have already pointed out, the proper way to go about this would be to put those extra functions into a module. In its simplest form a module is a subfolder in one of the folders listed in $env:PSModulePath with a PowerShell script of the same name (but with the extension .psm1 instead of .ps1):
$env:USERPROFILE
`-Documents
`-WindowsPowerShell
`-Modules
`-ExtraFunctions
`-ExtraFunctions.psm1
ExtraFunctions.psm1 contains all your functions and ends with an Export-ModuleMember statement exporting the functions/aliases/… you want to publish:
function Get-Foo {
...
}
function New-Bar {
...
}
...
New-Alias -Name gf -Value Get-Foo
...
Export-ModuleMember -Function Get-Foo, New-Bar, ... -Alias gf, ...
That way you can import specific members:
PS C:\> Import-Module ExtraFunctions -Function Get-Foo
or everything at once:
PS C:\> Import-Module ExtraFunctions
A module can be unloaded via the Remove-Module cmdlet:
PS C:\> Remove-Module ExtraFunctions
With PowerShell v3 and newer you don't even need to import your module manually, because modules are loaded automatically when one of their exported functions/cmdlets is called.
If you want to put in some extra work you can add a module manifest:
#{
# Script module or binary module file associated with this manifest
ModuleToProcess = 'ExtraFunctions.psm1'
# Version number of this module.
ModuleVersion = '1.0'
# ID used to uniquely identify this module
GUID = 'dbf5a7ca-683a-4f18-a090-0700ecccf6ff'
# Author of this module
Author = 'Ansgar Wiechers'
# Company or vendor of this module
CompanyName = ''
# Copyright statement for this module
Copyright = ''
# Description of the functionality provided by this module
Description = 'Extra functions.'
# Minimum version of the Windows PowerShell engine required by this module
PowerShellVersion = ''
# Name of the Windows PowerShell host required by this module
PowerShellHostName = ''
# Minimum version of the Windows PowerShell host required by this module
PowerShellHostVersion = ''
# Minimum version of the .NET Framework required by this module
DotNetFrameworkVersion = ''
# Minimum version of the common language runtime (CLR) required by this
# module
CLRVersion = ''
# Processor architecture (None, X86, Amd64, IA64) required by this module
ProcessorArchitecture = ''
# Modules that must be imported into the global environment prior to
# importing this module
RequiredModules = #()
# Assemblies that must be loaded prior to importing this module
RequiredAssemblies = #()
# Script files (.ps1) that are run in the caller's environment prior to
# importing this module
ScriptsToProcess = #()
# Type files (.ps1xml) to be loaded when importing this module
TypesToProcess = #()
# Format files (.ps1xml) to be loaded when importing this module
FormatsToProcess = #()
# Modules to import as nested modules of the module specified in
# ModuleToProcess
NestedModules = #()
# Functions to export from this module
FunctionsToExport = 'Get-Foo', 'New-Bar'
# Cmdlets to export from this module
CmdletsToExport = ''
# Variables to export from this module
VariablesToExport = ''
# Aliases to export from this module
AliasesToExport = 'gf'
# List of all modules packaged with this module
ModuleList = #()
# List of all files packaged with this module
FileList = 'ExtraFunctions.psm1'
# Private data to pass to the module specified in ModuleToProcess
PrivateData = ''
}
Manifests allow you for instance to define dependencies, or to split your module implementation into multiple files. They can be created manually or via the New-ModuleManifest cmdlet. Put the manifest into the root folder of the module:
$env:USERPROFILE
`-Documents
`-WindowsPowerShell
`-Modules
`-ExtraFunctions
+-ExtraFunctions.psd1
`-ExtraFunctions.psm1