Two questions on the PSModulePath in PowerShell Core:
$PSHOME/Modules:$AnotherPlace/SecondModuleDir
)This is probably a Documentation-Needed
situation. Currently, you'll probably have to look at the code
It would also be nice to have it as an API endpoint perhaps (like a variable, static property, etc) -- like [ModuleIntrinsics]::GetModulePath()
except without requiring arguments. Curious if anyone else has felt a need for that?
I think PSModulePath being an env var means you should get and set it like an env var
What I feel is absent is not necessarily the built-in default value, but a way to access the various well-known module locations by _scope_.
By analogy, let's take the example of $PROFILE
, which contains the well-known profile locations as named-for-the-scope NoteProperty
members:
# Sample output when run on macOS
PS> $PROFILE | Get-Member -type noteproperty | Select Name, Definition
Name Definition
---- ----------
AllUsersAllHosts string AllUsersAllHosts=/usr/local/microsoft/powershell/6-preview/profile.ps1
AllUsersCurrentHost string AllUsersCurrentHost=/usr/local/microsoft/powershell/6-preview/Microsoft.PowerShell_profile.ps1
CurrentUserAllHosts string CurrentUserAllHosts=/Users/jdoe/.config/powershell/profile.ps1
CurrentUserCurrentHost string CurrentUserCurrentHost=/Users/jdoe/.config/powershell/Microsoft.PowerShell_profile.ps1
I think we should provide something similar for module locations, using scopes such as AllUsers
, CurrentUser
, System
, and Custom
(the latter comprising all $env:PSModulePath
entries in addition to the well-known locations).
I'm unclear on how best to surface this functionality, however.
In theory, we could use the same approach as with $PROFILE
: decorate the $env:PSModulePath
[string]
value with NoteProperty
members, but that is somewhat obscure.
Then again, if properly documented, perhaps that's the way to go.
@BrucePay you should take @mklement0's suggestion as part of your PSModulePath6
RFC
I have submitted https://github.com/PowerShell/PowerShell-Docs/pull/3214 to add the *nix locations to the existing about_Modules and about_Environment_Variables docs. I grant that this is only a partial solution.
Want to add that this may be of use in hosted PowerShell contexts where the hosting implementation might have reasons for using different paths for each module scope. (I would say "or possibly define its own", but I think we're coupled to the existing scopes now.)
It might merit being able to set the scoped module paths in the powershell.config.json
and exposing their location through some PowerShell API. (Like @mklement0, I'm not sure what shape that API should be -- but I'd opt for either a variable or a method call, with the latter being a more direct fit to the current implementation).
In case anyone wants this btw, here's a quick function to get the defaults:
function Get-DefaultModulePath
{
param(
[Parameter()]
[ValidateSet('AllUsers', 'CurrentUser', 'System', 'WindowsPSSystem')]
[string[]]
$Scope
)
if ($Scope -eq 'WindowsPSSystem')
{
if ($IsLinux -or $IsMacOs)
{
throw "The scope '$Scope' does not exist on non-Windows platforms"
}
if ($PSVersionTable.PSVersion -le '6.0')
{
throw "The scope '$Scope' is only valid in PowerShell versions 6.1 and above"
}
}
if (-not $Scope)
{
$Scope = 'AllUsers','CurrentUser','System'
if (-not ($IsLinux -or $IsMacOS))
{
$Scope += 'WindowsPSSystem'
}
}
$invokeModuleMethod = {
$bindingFlags = [System.Reflection.BindingFlags]'static,nonpublic'
$m = [System.Management.Automation.ModuleIntrinsics].GetMethod($args[0], $bindingFlags)
$m.Invoke($null, @())
}
$paths = @{}
switch ($Scope)
{
'CurrentUser'
{
$p = & $invokeModuleMethod 'GetPersonalModulePath'
$paths.CurrentUser = $p
}
'AllUsers'
{
$p = & $invokeModuleMethod 'GetSharedModulePath'
$paths.AllUsers = $p
}
'System'
{
$p = & $invokeModuleMethod 'GetPSHomeModulePath'
$paths.System = $p
}
'WindowsPSSystem'
{
$p = & $invokeModuleMethod 'GetWindowsPowerShellPSHomeModulePath'
$paths.WindowsPSSystem = $p
}
}
return $paths
}
@rjmholt thanks very much for this, I'm using it in a module and, if I can get it working nicely, I'll publish it as a standalone script.
I note that on 5.1 and 6.0.1 on WIndows, and 6.1 on Linux, there is no GetWindowsPowerShellPSHomeModulePath
method; it is only available on 6.1 on WIndows, AFAICS.
Oh good point. Yes, I wrote that method for 6.1. Windows PowerShell should have some method like that in it, but I don’t know what it is. The method I wrote is equivalent to just ”${env:windir}\System32\WindowsPowerShell\v1.0\Modules”
Thanks for the handy function, @rjmholt (also interesting to see the use of reflection to call the non-public methods - I hope we won't need that in the long run).
It seems that in Windows PowerShell only the method that returns the CurrentUser
scope exists, so I've amended your function to add fallback values for Windows PowerShell.
(I've also changed the name to Get-StandardModulePath
, shortened WindowsPSSystem
to WinPSSystem
and I'm using an _ordered_ hashtable for the result, which makes the function v3+; btw, you forgot to rename a couple of instances of WindowsSystem
to WindowsPSSystem
in your recent edit).
#requires -version 3
function Get-StandardModulePath {
param(
[Parameter()]
[ValidateSet('AllUsers', 'CurrentUser', 'System', 'WinPSSystem')]
[string[]]
$Scope
)
if ($Scope -eq 'WinPSSystem' -and ($IsLinux -or $IsMacOs)) {
throw "The scope '$Scope' does not exist on non-Windows platforms."
}
if (-not $Scope) {
$Scope = 'CurrentUser', 'AllUsers', 'System'
if (-not ($IsLinux -or $IsMacOS)) {
$Scope += 'WinPSSystem'
}
}
$invokeModuleMethod = {
$bindingFlags = [System.Reflection.BindingFlags]'static,nonpublic'
$m = [System.Management.Automation.ModuleIntrinsics].GetMethod($args[0], $bindingFlags)
$m.Invoke($null, @())
}
$paths = [ordered] @{}
switch ($Scope) {
'CurrentUser' {
$paths.CurrentUser = try {
& $invokeModuleMethod 'GetPersonalModulePath'
}
catch {
# WinPS fallback.
"$(Split-Path $PROFILE)\Modules"
}
}
'AllUsers' {
$paths.AllUsers = try {
& $invokeModuleMethod 'GetSharedModulePath'
}
catch {
# WinPS fallback.
"$env:ProgramFiles\WindowsPowerShell\Modules"
}
}
'System' {
$paths.System = try {
& $invokeModuleMethod 'GetPSHomeModulePath'
}
catch {
# WinPS fallback.
"$PSHOME\Modules"
}
}
'WinPSSystem' {
$paths.WinPSSystem = try {
& $invokeModuleMethod 'GetWindowsPowerShellPSHomeModulePath'
}
catch {
# WinPS fallback.
"$PSHOME\Modules"
}
}
}
return $paths
}
@mklement0 Yeah I opened this issue because I think those methods (or something like them) should really be a public API, since they are a contract with PSGet
Ah, of course, @rjmholt; so another thank you for opening this issue.
:tada:This issue was addressed in #11057, which has now been successfully released as v7.0.0-preview.6
.:tada:
Handy links:
@SteveL-MSFT is there a reason #11057 is marked as closing this issue? I couldn't find any new mechanism to solve feature request in that PR
Most helpful comment
In case anyone wants this btw, here's a quick function to get the defaults: