Chris
Chris

Reputation: 1009

How to expand path in binary PowerShell module?

I'm writing a simple sample binary PowerShell module:

using System;
using System.Management.Automation;
using System.Management.Automation.Runspaces;

namespace MyModule
{
    [Cmdlet(VerbsDiagnostic.Test,"SampleCmdlet")]
    [OutputType(typeof(System.String))]
    public class TestSampleCmdletCommand : PSCmdlet
    {
        [Parameter(
            Mandatory = true,
            Position = 0,
            ValueFromPipeline = true,
            ValueFromPipelineByPropertyName = true)]
        public string Path { get; set; }

        // This method gets called once for each cmdlet in the pipeline when the pipeline starts executing
        protected override void BeginProcessing()
        {
            WriteVerbose("Begin!");
        }

        // This method will be called for each input received from the pipeline to this cmdlet; if no input is received, this method is not called
        protected override void ProcessRecord()
        {
            WriteObject( Path );
            WriteObject( System.IO.Path.GetFullPath(Path) );
        }

        // This method will be called once at the end of pipeline execution; if no input is received, this method is not called
        protected override void EndProcessing()
        {
            WriteVerbose("End!");
        }
    }

}

When I import the module and run the code, GetFullPath() always returns a path relative to the user profile and not the real full path:

PS C:\Users\User\GitHub\MyModule\source\MyModule> Import-Module -Name .\bin\Debug\netstandard2.0\MyModule.dll
PS C:\Users\User\GitHub\MyModule\source\MyModule> cd \
PS C:\> Test-SampleCmdlet -Path .\Temp\
.\Temp\
C:\Users\User\Temp\
PS C:\>                                                                                                                                                                                                                                                                     

How can I expand the path parameter properly?

Upvotes: 2

Views: 331

Answers (1)

mklement0
mklement0

Reputation: 437248

.NET methods use the process-wide current directory as reflected in Environment.CurrentDirectory, which is not the same as PowerShell's runspace-specific (thread-specific) current location.[1]

You cannot directly rely on System.IO.Path.GetFullPath(), at least not without also specifying the runspace's current file-system location as the optional 2nd basePath argument - which is only available in .NET Core / PowerShell [Core] 6+.

To robustly refer to the calling runspace's current file-system location, even when a different provider's location happens to be the current one[2], you can use .CurrentProviderLocation("FileSystem").ProviderPath as follows in PowerShell [Core] 6+ / .NET Core (Path is the property representing the cmdlet's -Path parameter, i.e. the input path):

// Get the runspace's file-system location.
string currentDir = CurrentProviderLocation("FileSystem").ProviderPath;

// .NET Core ONLY: Resolve the given path to a full path as a native path.
//                 relative to the given base path.
string fullPath = System.IO.Path.GetFullPath(Path, currentDir);

When hosting Windows PowerShell more work is needed:

// Get the runspace's file-system location as a native path.
string currentDir = CurrentProviderLocation("FileSystem").ProviderPath;

// Requires `using System.Text.RegularExpressions;`
// Strips a ".\" or "./" prefix from a relative path.
string relativePathWithoutPrefix = Regex.Replace(Path, @"^.[\\/]", "")

// Caveat: Combine() will not resolve any additional, *interior* . 
//         or .. components, should they be present in relativePathWithoutPrefix.
string fullPath = System.IO.Path.Combine(currentDir, relativePathWithoutPrefix);

Both methods will pass a Path value that already is a full path through.

Note:

  • The resulting path is by design a native file-system path, even if the runspace's current file-system location is based on a PowerShell-only drive (created with New-PSDrive).

  • The approach implies that your cmdlet supports file-system paths only.

For information about how to work with PS provider locations more generally, including wildcard resolution, see this answer.


[1] This discrepancy is an unfortunate side effect of PowerShell supporting multiple runspaces (threads) inside a single process, all of which need to have their own current location - see this GitHub issue for background information.

[2] For instance, the runspace's current location may be a registry location such as HKCU:\Console.

Upvotes: 4

Related Questions