PowerShell: using module does not resolve "~" home folder correctly

Prerequisites

Steps to reproduce

  • Obtain a MacOS / nix machine
  • Create a .psm1 module and place it in a subfolder under your home ~ directly
  • Create a .ps1 script and attempt to reference the .psm1 module using the using module statement
  • Run the script

Example

#!/usr/local/bin/pwsh
using module "~/.nuget/packages/<package-name>/1.0.1/scripts/k8s/module.psm1"

Expected behavior

I expected the `using module` statement to successfully load classes from the `.psm1` module

Actual behavior

- I'm receiving an error stating:


The specified module '/Users/<username>/<calling ps1 path>/~/.nuget/packages/<package-name>/1.0.1/scripts/k8s/module.psm1' was not loaded because no valid module file was found in any module directory.
  • I’ve verified that the file exists under the ~/.nuget/packages/<package-name>/1.0.1/scripts/k8s/module.psm1' path
  • For some reason the calling script’s path and the module’s path are getting merged


### Error details

```console
Exception             : 
    Type        : System.Management.Automation.RuntimeException
    ErrorRecord : 
        Exception             : 
            Type    : System.Management.Automation.ParentContainsErrorRecordException
            Message : The specified module '/Users/maciej.misztal/Projects/@devops/ops-k8s-argocd/content/scripts/~/.nuget/packages/allegropay.devops.
k8s.automation/1.0.1/scripts/k8s/HELM.psm1' was not loaded because no valid module file was found in any module directory.
            HResult : -2146233087
        TargetObject          : /Users/maciej.misztal/Projects/@devops/ops-k8s-argocd/content/scripts/ArgoCD.psm1
        CategoryInfo          : InvalidOperation: (/Users/maciej.miszt…scripts/ArgoCD.psm1:String) [], ParentContainsErrorRecordException
        FullyQualifiedErrorId : Modules_ModuleNotFound,Microsoft.PowerShell.Commands.ImportModuleCommand
        InvocationInfo        : 
            ScriptLineNumber : 1
            OffsetInLine     : 3
            HistoryId        : 1
            Line             : . "/Users/maciej.misztal/Projects/@devops/ops-k8s-argocd/content/scripts/Deploy.ArgoCD.ps1"
            PositionMessage  : At line:1 char:3
                               + . "/Users/maciej.misztal/Projects/@devops/ops-k8s-argocd/content/scri …
                               +   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
            InvocationName   : .
            CommandOrigin    : Internal
        ScriptStackTrace      : at <ScriptBlock>, <No file>: line 1
    TargetSite  : 
        Name          : LoadModule
        DeclaringType : System.Management.Automation.Language.Compiler, System.Management.Automation, Version=7.1.2.0, Culture=neutral, 
PublicKeyToken=31bf3856ad364e35
        MemberType    : Method
        Module        : System.Management.Automation.dll
    StackTrace  : 
   at System.Management.Automation.Language.Compiler.LoadModule(PSModuleInfo originalModuleInfo)
   at System.Management.Automation.Language.Compiler.LoadUsingsImpl(IEnumerable`1 usingAsts, Assembly[]& assemblies)
   at System.Management.Automation.Language.Compiler.GenerateLoadUsings(IEnumerable`1 usingStatements, Boolean allUsingsAreNamespaces, List`1 exprs)
   at System.Management.Automation.Language.Compiler.GenerateTypesAndUsings(ScriptBlockAst rootForDefiningTypesAndUsings, List`1 exprs)
   at System.Management.Automation.Language.Compiler.CompileSingleLambda(ReadOnlyCollection`1 statements, ReadOnlyCollection`1 traps, String 
funcName, IScriptExtent entryExtent, IScriptExtent exitExtent, ScriptBlockAst rootForDefiningTypesAndUsings)
   at System.Management.Automation.Language.Compiler.CompileNamedBlock(NamedBlockAst namedBlockAst, String funcName, ScriptBlockAst 
rootForDefiningTypes)
   at System.Management.Automation.Language.Compiler.VisitScriptBlock(ScriptBlockAst scriptBlockAst)
   at System.Management.Automation.Language.ScriptBlockAst.Accept(ICustomAstVisitor visitor)
   at System.Management.Automation.Language.Compiler.Compile(CompiledScriptBlockData scriptBlock, Boolean optimize)
   at System.Management.Automation.CompiledScriptBlockData.ReallyCompile(Boolean optimize)
   at System.Management.Automation.CompiledScriptBlockData.CompileUnoptimized()
   at System.Management.Automation.CompiledScriptBlockData.Compile(Boolean optimized)
   at System.Management.Automation.ScriptBlock.Compile(Boolean optimized)
   at System.Management.Automation.PSScriptCmdlet..ctor(ScriptBlock scriptBlock, Boolean useNewScope, Boolean fromScriptFile, ExecutionContext 
context)
   at System.Management.Automation.CommandProcessor.Init(IScriptCommandInfo scriptCommandInfo)
   at System.Management.Automation.CommandProcessor..ctor(IScriptCommandInfo scriptCommandInfo, ExecutionContext context, Boolean useLocalScope, 
Boolean fromScriptFile, SessionStateInternal sessionState)
   at System.Management.Automation.CommandDiscovery.GetScriptAsCmdletProcessor(IScriptCommandInfo scriptCommandInfo, ExecutionContext context, 
Boolean useNewScope, Boolean fromScriptFile, SessionStateInternal sessionState)
   at System.Management.Automation.CommandDiscovery.CreateCommandProcessorForScript(ExternalScriptInfo scriptInfo, ExecutionContext context, Boolean 
useNewScope, SessionStateInternal sessionState)
   at System.Management.Automation.CommandDiscovery.CreateScriptProcessorForSingleShell(ExternalScriptInfo scriptInfo, ExecutionContext context, 
Boolean useLocalScope, SessionStateInternal sessionState)
   at System.Management.Automation.CommandDiscovery.LookupCommandProcessor(CommandInfo commandInfo, CommandOrigin commandOrigin, Nullable`1 
useLocalScope, SessionStateInternal sessionState)
   at System.Management.Automation.CommandDiscovery.LookupCommandProcessor(String commandName, CommandOrigin commandOrigin, Nullable`1 useLocalScope)
   at System.Management.Automation.ExecutionContext.CreateCommand(String command, Boolean dotSource)
   at System.Management.Automation.PipelineOps.AddCommand(PipelineProcessor pipe, CommandParameterInternal[] commandElements, CommandBaseAst 
commandBaseAst, CommandRedirection[] redirections, ExecutionContext context)
   at System.Management.Automation.PipelineOps.InvokePipeline(Object input, Boolean ignoreInput, CommandParameterInternal[][] pipeElements, 
CommandBaseAst[] pipeElementAsts, CommandRedirection[][] commandRedirections, FunctionContext funcContext)
   at System.Management.Automation.Interpreter.ActionCallInstruction`6.Run(InterpretedFrame frame)
   at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
    Message     : The specified module '/Users/maciej.misztal/Projects/@devops/ops-k8s-argocd/content/scripts/~/.nuget/packages/allegropay.devops.k8s.
automation/1.0.1/scripts/k8s/HELM.psm1' was not loaded because no valid module file was found in any module directory.
    Data        : System.Collections.ListDictionaryInternal
    Source      : System.Management.Automation
    HResult     : -2146233087
TargetObject          : /Users/maciej.misztal/Projects/@devops/ops-k8s-argocd/content/scripts/ArgoCD.psm1
CategoryInfo          : InvalidOperation: (/Users/maciej.miszt…scripts/ArgoCD.psm1:String) [], RuntimeException
FullyQualifiedErrorId : Modules_ModuleNotFound,Microsoft.PowerShell.Commands.ImportModuleCommand
InvocationInfo        : 
    ScriptLineNumber : 1
    OffsetInLine     : 3
    HistoryId        : 1
    Line             : . "/Users/maciej.misztal/Projects/@devops/ops-k8s-argocd/content/scripts/Deploy.ArgoCD.ps1"
    PositionMessage  : At line:1 char:3
                       + . "/Users/maciej.misztal/Projects/@devops/ops-k8s-argocd/content/scri …
                       +   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    InvocationName   : .
    CommandOrigin    : Internal
ScriptStackTrace      : at <ScriptBlock>, <No file>: line 1

Environment data

Name                           Value
----                           -----
PSVersion                      7.1.2
PSEdition                      Core
GitCommitId                    7.1.2
OS                             Darwin 21.4.0 Darwin Kernel Version 21.4.0: Mon Feb 21 20:34:37 PST 2022; root:xnu-8020.101.4~2/RELEASE_X86_64
Platform                       Unix
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

Visuals

No response

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 27 (4 by maintainers)

Most upvoted comments

Considering that relative paths work like: using module ..\..\TestModule I don’t see why ~ shouldn’t also work.

Interestingly, relative paths work differently in using statements. They work relative to the file rather than the working directory. Basically the using version of $PSScriptRoot

WG-Engine discussed this today. We agree it’s definitely a bug, but given that ~ is technically a PSProvider concept it is somewhat uniquely ill-suited to use in using module which is a parse-time concept. We feel it would be unwise to involve the rest of the engine and PSProvider subsystem in the parsing code.

As a “mostly good” fix, we feel the better option might be to take the “most common” definition of ~ rather than the “strictly PowerShell-correct” one during parsing, where using module with a path like this would evaluate ~ purely based on the appropriate environment variables instead of involving the PSProvider subsystem in the parsing code. This would enable the vast majority of use cases here without being liable to cause odd edge cases where suddenly parsing this kind of code does strange things in unexpected circumstances.

Marking this as a bug for now. Thanks for the report! 💖 😊

.Net syntax is not friendly with ~ as part of the path

@237dmitry Our using statement is not a dotnet concept. We have full control over how it acts.

The question is whether we can consider ~ to be usable in what is a parse time construct, not whether we have the ability to change it.

I’d like to see this. The biggest potential disconnect is that ~ is configurable, so it’s not particularly well suited for a parse time concept:

PS C:\> (Get-PSProvider FileSystem).Home = 'C:\Program Files\'
PS C:\> Get-Item ~

    Directory: C:\

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-r--           7/27/2022 12:18 PM                Program Files 

Opening up to the Engine WG for discussion.


Also note that using in this context is a PowerShell concept. There’s already plenty of custom handling in the path resolution.

Import-Module is a no-go. I need to import the classes out of the psm1

There is work-round for that too, but it looks like resolving the path before the using statement is easier - especially if it is not your module to modify.

Can you elaborate?

Sure. Because when I write a module for others to use I don’t know if they will load it with USING or Import-Module, if I want classes to be available I now put them in their own file. In the PSD1 file I then have

ScriptsToProcess = @(‘Classes.ps1’)

https://github.com/jhoneill/OctopusTools is an example, psm1 loads files from public (exported) and private (not exported). PSD1 loads the Psm1 file, the classes, the types and the formats.

. If it is your module you can move the classes out of the PSM1 file and change the PSD1 .But that’s harder to do with someone else’s code - their next update will reverse your change 😃

<div> GitHub</div><div>GitHub - jhoneill/OctopusTools: Work in progress: PowerShell bits for Octopus deploy</div><div>Work in progress: PowerShell bits for Octopus deploy - GitHub - jhoneill/OctopusTools: Work in progress: PowerShell bits for Octopus deploy</div>

Considering that relative paths work like: using module ..\..\TestModule I don’t see why ~ shouldn’t also work.

Import-Module is a no-go. I need to import the classes out of the psm1

Won’t work. It needs to be a top-level statement.

Because using is the .Net statement and it does not know about powershell’s aliases or variables.

not at all sure that using module <path to ps module> is a .net statement and even if it were, somewhere between the PowerShell parser and the .net internals the path should be resolved.

@kasini3000 's work round or using Import-Module instead should avoid the error, but it does look like a bug to me.