Powershell: Get the default PSModulePath at runtime?

Created on 15 Jun 2018  Â·  15Comments  Â·  Source: PowerShell/PowerShell

Two questions on the PSModulePath in PowerShell Core:

  1. Is the the default setting of PSModulePath in PS Core documented anywhere (e.g. online) in a way that is general and cross platform? Like a table of what it is on each supported platform, or a string with unexpanded platform-specific variables in it? (e.g. $PSHOME/Modules:$AnotherPlace/SecondModuleDir)
  2. Is it possible to access the default PSModulePath at runtime? (Or to reset the current one?)
Area-Maintainers-Documentation Issue-Question WG-Engine

Most helpful comment

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
}

All 15 comments

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

Was this page helpful?
0 / 5 - 0 ratings