Get the path of the importing script from within a module? - powershell

Is there a way to get the path of a script that imported a module from within that module?
The script module I'm writing is meant to load settings from files relative to the importing script. I plan on reusing the module for a number of projects, so I would prefer if the module could make no assumptions about where its being imported from.
This is a nice to have, it would be great the module could be as implicit as possible. If all else fails though, I can just have the caller pass in its location.
Unfortunately everything I've attempted so far returns the path to the module (not what imported it). Here's a simple demonstration:
Test-RelativeModule.ps1, Stored at: c:\test\
import-module "$PSScriptRoot\mod\Test.psm1"
Test.psm1, Stored at: c:\test\mod\
# returns 'c:\test\mod'
write-host "`$PSScriptRoot: $PSScriptRoot"
# returns 'c:\test\mod'
# The value of $MyInvocation.MyCommand.Path is 'c:\test\mod\Test.psm1'
write-host "Split Invoation: $(Split-Path $MyInvocation.MyCommand.Path)"
# returns whatever path the console is currently residing
write-host "Resolve Path: $((resolve-path '.\').path)"
# what I'm looking for is something to return 'c:\test' from within the module
# without making any assumptions about the folder structure

Try this:
Write-Host "My invoker's PSScriptRoot: $($MyInvocation.PSScriptRoot)"

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.

is start-bitstransfer compatible with pester

When running start-bitstransfer in a script I'm trying to apply pester to... it doesn't seem to work...
I just tried downloading some random http page (while preparing to do some tropico 5 ;) ... to the pester testdrive, but this doesnt work...
do .net things not work with pester or such ?
set-content "testdrive:\bla.txt" -value "bla"
Start-BitsTransfer -source "http://www.tropicostrategy.com/p/blog-page.html" -destination 'testdrive:\' -Description "Downloading test"
Get-ChildItem 'TestDrive:\' | out-file c:\temp\b.txt
output:
Describing get-wallhavens
[-] downloads a file 591ms
ArgumentException: Value does not fall within the expected range.
ArgumentException: An incorrect value is specified in the Source parameter or in the Destination parameter. Verify that the directory and file names in the Source and Destination parameters are correct.
at <ScriptBlock>, D:\stack\Projects\Personal\wallchange\download-wallpaper.Tests.ps1: line 7
The documentation on GitHub indicates that you need to use a variable that holds the real location of the temporary storage:
Working with .NET Objects
When working directly with .NET objects, it's not possible to use the convenient TestDrive:\ PSDrive. Instead you need to use the $TestDrive variable which holds the actual path in a format that .NET understands. For example instead of using TestDrive:\somefile.txt use $TestDrive\somefile.txt instead.

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

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

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