しばたテックブログ

気分で書いている技術ブログです。

PowerShellのロケーションについて

PowerShellの基本的なところがわかってなかった恥を晒すエントリです。
本エントリの内容はWindows 8.1上で動作させ確認しています。

[2015/05/11追記] 内容に間違いがあったので修正。
さらに恥を晒すことになってしまいました。

はじめに

PowerShellを起動してGet-Location(またはpwdコマンド)を実行すると以下の様な感じでカレントディレクトリのパスを取得することができます。

Windows PowerShell
Copyright (C) 2014 Microsoft Corporation. All rights reserved.

PS C:\Users\stknohg> Get-Location

Path
----
C:\Users\stknohg

また、System.IO.Directory.GetCurrentDirectoryメソッドを実行しても同様の結果を得ることができます。

PS C:\Users\stknohg> [System.IO.Directory]::GetCurrentDirectory()

C:\Users\stknohg

ここでSet-Location(またはcdコマンド)で適当なディレクトリに移動して再度カレントディレクトリのパスを取得してみると、

PS C:\Users\stknohg> cd C:\Tmp

PS C:\Tmp> Get-Location

Path
----
C:\Tmp

PS C:\Tmp> [System.IO.Directory]::GetCurrentDirectory()

C:\Users\stknohg

となり、System.IO.Directory.GetCurrentDirectoryメソッドを実行した場合にカレントディレクトリが更新されない事象に遭遇しました。

この事象によってPowerShellから.NET Frameworkの機能を呼んで相対パスを使用する場合(例えばSystem.IO.Compressionあたりの機能を使う場合など)に予期しない結果になることがあります。

PowerShellのドライブとロケーション

個人的にはGet-LocationSystem.IO.Directory.GetCurrentDirectoryメソッドの結果は常に同期するものと思ってて何故こうなるのかわかっていませんでした。

結論から先に書くと、この様な結果になるのはPowerShellで扱う"ロケーション"がディレクトリだけではないためになります。
上記の内容は間違ってて、[System.IO.Directory]::GetCurrentDirectory()メソッドは現在のプロセス(PowerShell.exe)のカレントディレクトリを取得するものであり、PowerShellのシェル内でのロケーションとは無関係であるのが理由の様です。

MSDNフォーラムの以下のスレッドでこの件と類似の現象について触れられていました。

[IO.Directory]::GetCurrentDirectory() gives different results on different PCs

いちおう、以下のドライブとロケーションの関係については間違ってはいないのでそのままにしておきます...

PowerShellのドライブ

Managing Windows PowerShell Drives | Microsoft Docs で細かい説明がされていますが、PowerShellにおいてドライブはファイルシステム以外も管理対象としています。
Get-PSDriveコマンドを使うとドライブの一覧を取得することができます。

実行例

PS C:\Tmp> Get-PSDrive

Name           Used (GB)     Free (GB) Provider      Root
----           ---------     --------- --------      ----
Alias                                  Alias
C                  12.58         51.08 FileSystem    C:\
Cert                                   Certificate   \
D                                      FileSystem    D:\
Env                                    Environment
Function                               Function
HKCU                                   Registry      HKEY_CURRENT_USER
HKLM                                   Registry      HKEY_LOCAL_MACHINE
Variable                               Variable
WSMan                                  WSMan

この結果のProviderの部分がドライブの種類とみなすことができ、ファイルシステム以外のものがあることがわかります。
主なプロバイダーには、

  • Alias - エイリアス
  • Certificate - 証明書
  • Environment - 環境変数
  • FileSytem - ファイルシステム
  • Function - ファンクション
  • Registry - レジストリ
  • WSMan - WS-Management構成情報
  • Variable - システム定義、独自定義の変数

等があります。各プロバイダーの詳細はこの辺この辺に載っています。

PowerShellのロケーション

そして、これらのプロバイダーの中で階層構造を持つもの、例えばレジストリについてもディレクトリと同様にSet-Locationでカレントのキーを移動することができる様になっています。*1

実行例

PS C:\Tmp> cd HKCU:\Software\Microsoft
PS HKCU:\Software\Microsoft> ls


    Hive: HKEY_CURRENT_USER\Software\Microsoft


Name                           Property
----                           --------
Active Setup
ActiveMovie
Assistance
AuthCookies
Command Processor              PathCompletionChar : 9
                               EnableExtensions   : 1
                               CompletionChar     : 9
                               DefaultColor       : 0
・・・(後略)・・・

当然、上記の例を実行した時点でGet-Locationした場合に得られるのはディレクトリではなくレジストリキーのパスになります。

PS HKCU:\Software\Microsoft> Get-Location

Path
----
HKCU:\Software\Microsoft


PS HKCU:\Software\Microsoft> Get-Location | fl

Drive        : HKCU
Provider     : Microsoft.PowerShell.Core\Registry
ProviderPath : HKEY_CURRENT_USER\Software\Microsoft
Path         : HKCU:\Software\Microsoft

この様にPowerShellにおいて"ロケーション"は単純なディレクトリを指すだけではなくより抽象度の高いものであるため、Get-LocationSystem.IO.Directory.GetCurrentDirectoryメソッドの結果が同期しない様になっている様です。
PowerShellにおいて"ロケーション"は単純なディレクトリを指すだけではなくより抽象度の高いものでありましたが今回の件とはあまり関係ありませんでした...

カレントディレクトリを同期させるには

とはいえGet-LocationSystem.IO.Directory.GetCurrentDirectoryメソッドの結果が同期してないと困る場合もあったりするので、同期させたい場合は以下の様にロケーションのProviderFileSystemの場合にSystem.IO.Directory.SetCurrentDirectoryメソッドでカレントディレクトリを更新してやればOKです。

$Current = Get-Location
if( $Current.Provider.Name -eq "FileSystem" ){
    [System.IO.Directory]::SetCurrentDirectory($Current.Path)
}

【2016/01/08追記】

[System.IO.Directory]::SetCurrentDirectory((Get-Location -PSProvider FileSystem).Path)

と、Providerを指定したGet-Location(pwd)でも良いですね。

*1:一応補足しておくと、階層構造をもたないプロバイダーに対してもドライブのルートに対してはSet-Locationを行うことができます。