マウントしたネットワークドライブのパスからUNCパスを取得する

# 概要

マウントしたネットワークドライブ(割り当てられたネットワークドライブ)の情報は、WMI(`Win32_MappedLogicalDisk`)から取得できる。

Windows PowerShell では WMI 関連コマンドレットが用意されているため、そちらから情報を取得し、パスの判定・UNC パスへの変換を行う関数。

# PowerShell 関数

`using namespace`や`Get-CimInstance`を使用しているため、Windows PowerShell 5.1 以降用。

入力された文字列がネットワークドライブのパスのようだったら、UNCパスに変換して出力する関数。
コメントベースヘルプも設定しているのでそちらも参考に。

```powershell
using namespace System.Collections.Generic
using namespace System.Management.Automation

function Resolve-MapNetworkDrivePathToUnc {
<#
.SYNOPSIS
割り当てられたネットワークドライブのパスをUNCパスに解決します。
.DESCRIPTION
Resolve-MapNetworkDrivePathToUnc 関数は、割り当てられたネットワークドライブのパスをUNCパスに解決します。
指定されたパスが相対パスの場合は、絶対パスに変換した上で解決が行われます。
すでにUNCパスの場合、あるいはローカルパスの場合は絶対パスを返します。
存在しているパスを指定する必要があります。
.INPUTS
System.String
.OUTPUTS
System.String
#>
    [CmdletBinding(
        DefaultParameterSetName = 'LiteralPath',
        HelpUri = 'https://qiita.com/nukie_53/items/673e9581624fc8f201bf'
    )]
    [OutputType([string])]
    param (
        # 解決するパスを指定します。LiteralPath の値は、入力した内容のまま使用されます。ワイルドカードとして解釈される文字はありません。
        [Parameter(
            Mandatory = $true, 
            Position = 0,
            ValueFromPipeline = $true, 
            ValueFromPipelineByPropertyName = $true,
            ParameterSetName = 'LiteralPath'
        )]
        [ValidateNotNullOrEmpty()]
        [Alias('PSPath', 'ProviderPath', 'FullName', 'LocalPath')]
        [string[]]$LiteralPath
        ,
        # 解決するパスを指定します。ワイルドカードを使用できます。
        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'Path'
        )]
        [ValidateNotNullOrEmpty()]
        [SupportsWildcards()]
        [string[]]$Path
    )
    begin {
        # 割り当てられたネットワークドライブの一覧をDictionaryに格納。
        [Dictionary[string, string]]$driveUncDic = [Dictionary[string, string]]::new()
        
        # WMI 経由でドライブ情報を取得。
        # DeviceID が ドライブレター、ProviderName が 実際のパスとなる。
        [string]$Wql = 'SELECT DeviceID, ProviderName FROM Win32_MappedLogicalDisk'
        Get-CimInstance -Query $Wql | 
            ForEach-Object -Process {
                $driveUncDic.Add($_.DeviceID, $_.ProviderName)
                $_.Dispose()
            }
    }
    process {
        # Resolve-Path コマンドレットに、
        # LiteralPath が指定されたときには $LiteralPath、Path が指定されたときは $Path を渡したいため、スプラッティングを使用する。
        [hashtable]$resolvePathParam = @{
            $($PSCmdlet.ParameterSetName) = $PSBoundParameters[$PSCmdlet.ParameterSetName]
        }
        [PathInfo]$p = $null
        foreach ($p in Resolve-Path @resolvePathParam) {
            if ($p.Provider.Name -ne 'FileSystem') {
                # ファイル以外のものが指定された場合はエラーを出してスキップ。
                $PSCmdlet.WriteError([ErrorRecord]::new(
                    [System.ArgumentException]::new(('${0} には実際に存在するファイルのパスを指定してください。' -f $PSCmdlet.ParameterSetName)),
                    'InvalidArgument',
                    [ErrorCategory]::InvalidArgument,
                    $p.ProviderPath
                ))
                continue
            }

            [uri]$u = [uri]($p.ProviderPath)
            [string]$localPath = $u.LocalPath # この後よく使うので一旦変数に格納。

            if ($u.IsUnc) {
                # すでにUNCパスならそのままでOK。
                $PSCmdlet.WriteObject($localPath)
                continue
            }
            
            # continue の対象がわかりにくくになるため、foreach ではなく、ForEach-Object を使用。
            $driveUncDic.GetEnumerator() |
                ForEach-Object -Process {
                    # Resolve-Path によりパスの大文字小文字は正規化されているので、そのまま比較してOK。
                    if ($localPath.StartsWith($_.Key)) {
                        # パスの先頭がドライブレターなら、ドライブレター以降を切り出して実際のパスと結合。
                        [string]$resolvedPath = $_.Value + $localPath.Substring($_.Key.Length)
                        $PSCmdlet.WriteObject($resolvedPath)
                        continue
                    }
                }
            
            # ローカルのパス(C ドライブとか)
            $PSCmdlet.WriteObject($localPath)
        }
    }
}
```

# 使用例

## 概要
batファイルから上記関数を含むPSスクリプトを呼び出し、UNCパスに変換されたパスをクリップボードへ送る。

## batファイル

```text:Clip-UNCPath.bat
@PowerShell.exe -Sta -NoProfile -ExecutionPolicy RemoteSigned -File "%~dpn0.ps1" %* -Prefix "" -Suffix ""
```

## ps1スクリプト

```powershell:Clip-UNCPath.ps1
using namespace System.Collections.Generic
using namespace System.Management.Automation

using assembly  System.Windows.Forms
using namespace System.Windows.Forms

[CmdletBinding(PositionalBinding = $false)]
Param(
    [Parameter(Position = 0, ValueFromRemainingArguments = $true)]
    [string[]]$InputObject,
    [string]$Separator = [System.Environment]::NewLine,
    [string]$Prefix = '<"',
    [string]$Suffix = '">'
)

function Resolve-MapNetworkDrivePathToUnc {
<#
.SYNOPSIS
割り当てられたネットワークドライブのパスをUNCパスに解決します。
.DESCRIPTION
Resolve-MapNetworkDrivePathToUnc 関数は、割り当てられたネットワークドライブのパスをUNCパスに解決します。
指定されたパスが相対パスの場合は、絶対パスに変換した上で解決が行われます。
すでにUNCパスの場合、あるいはローカルパスの場合は絶対パスを返します。
存在しているパスを指定する必要があります。
.INPUTS
System.String
.OUTPUTS
System.String
#>
    [CmdletBinding(
        DefaultParameterSetName = 'LiteralPath',
        HelpUri = 'https://qiita.com/nukie_53/items/673e9581624fc8f201bf'
    )]
    [OutputType([string])]
    param (
        # 解決するパスを指定します。LiteralPath の値は、入力した内容のまま使用されます。ワイルドカードとして解釈される文字はありません。
        [Parameter(
            Mandatory = $true, 
            Position = 0,
            ValueFromPipeline = $true, 
            ValueFromPipelineByPropertyName = $true,
            ParameterSetName = 'LiteralPath'
        )]
        [ValidateNotNullOrEmpty()]
        [Alias('PSPath', 'ProviderPath', 'FullName', 'LocalPath')]
        [string[]]$LiteralPath
        ,
        # 解決するパスを指定します。ワイルドカードを使用できます。
        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'Path'
        )]
        [ValidateNotNullOrEmpty()]
        [SupportsWildcards()]
        [string[]]$Path
    )
    begin {
        # 割り当てられたネットワークドライブの一覧をDictionaryに格納。
        [Dictionary[string, string]]$driveUncDic = [Dictionary[string, string]]::new()
        
        # WMI 経由でドライブ情報を取得。
        # DeviceID が ドライブレター、ProviderName が 実際のパスとなる。
        [string]$Wql = 'SELECT DeviceID, ProviderName FROM Win32_MappedLogicalDisk'
        Get-CimInstance -Query $Wql | 
            ForEach-Object -Process {
                $driveUncDic.Add($_.DeviceID, $_.ProviderName)
                $_.Dispose()
            }
    }
    process {
        # Resolve-Path コマンドレットに、
        # LiteralPath が指定されたときには $LiteralPath、Path が指定されたときは $Path を渡したいため、スプラッティングを使用する。
        [hashtable]$resolvePathParam = @{
            $($PSCmdlet.ParameterSetName) = $PSBoundParameters[$PSCmdlet.ParameterSetName]
        }
        [PathInfo]$p = $null
        foreach ($p in Resolve-Path @resolvePathParam) {
            if ($p.Provider.Name -ne 'FileSystem') {
                # ファイル以外のものが指定された場合はエラーを出してスキップ。
                $PSCmdlet.WriteError([ErrorRecord]::new(
                    [System.ArgumentException]::new(('${0} には実際に存在するファイルのパスを指定してください。' -f $PSCmdlet.ParameterSetName)),
                    'InvalidArgument',
                    [ErrorCategory]::InvalidArgument,
                    $p.ProviderPath
                ))
                continue
            }

            [uri]$u = [uri]($p.ProviderPath)
            [string]$localPath = $u.LocalPath # この後よく使うので一旦変数に格納。

            if ($u.IsUnc) {
                # すでにUNCパスならそのままでOK。
                $PSCmdlet.WriteObject($localPath)
                continue
            }
            
            # continue の対象がわかりにくくになるため、foreach ではなく、ForEach-Object を使用。
            $driveUncDic.GetEnumerator() |
                ForEach-Object -Process {
                    # Resolve-Path によりパスの大文字小文字は正規化されているので、そのまま比較してOK。
                    if ($localPath.StartsWith($_.Key)) {
                        # パスの先頭がドライブレターなら、ドライブレター以降を切り出して実際のパスと結合。
                        [string]$resolvedPath = $_.Value + $localPath.Substring($_.Key.Length)
                        $PSCmdlet.WriteObject($resolvedPath)
                        continue
                    }
                }
            
            # ローカルのパス(C ドライブとか)
            $PSCmdlet.WriteObject($localPath)
        }
    }
}

[string[]]$resolvedPath = 
    $InputObject | Resolve-MapNetworkDrivePathToUnc

[string]$clipTxt =
    $Prefix +
    ($resolvedPath -join ($Suffix + $Separator + $Prefix)) +
    $Suffix

[Clipboard]::SetText($clipTxt) # Copy
```

# 参考ほか

### 他のアプローチによる取得
[【備忘録】ネットワークドライブとUNCの変換と、それに伴うハック](http://qiita.com/Real_analysis/items/ab85b8901f9428861ae2 "【備忘録】ネットワークドライブとUNCの変換と、それに伴うハック")

### 最後の無駄なコメントの意味
[Markdown のコードブロックの中で PowerShell で .NET の静的メソッドを呼び出すコードを書いたら表示されないのを回避するバッドノウハウ](http://qiita.com/cd01/items/3ec546214fbbd743b737 "Markdown のコードブロックの中で PowerShell で .NET の静的メソッドを呼び出すコードを書いたら表示されないのを回避するバッドノウハウ")