鷲ノ巣

C# とか PowerShell とか。当ブログの記事は(特に公開直後は)頻繁に改定される場合があることをご了承ください。

ドキュメント フォルダを OneDrive にバックアップしていると PowerShell Core 6 の Get-InstalledModule が機能しない件

皆さん、PC のバックアップはしてますか?
何が起こるかわからないので、定期的なバックアップは大切です。

Microsoft としては、OneDrive を使ったバックアップを推しているように思われます。
新しく PC を買ってきて立ち上げると、Windows 10 の初期セットアップ中に、OneDrive バックアップを構成することを勧められますし。

今回は、そんな OneDrive バックアップと、PowerShell の相性のお話。

TL;DR

GitHub に Issue 立ててるんで、そっちを見てください。*1
github.com

この Issue が修正されたら、このブログ記事の内容は時代遅れです。

つまり?

PowerShellGet を使っている場合、Get-InstalledModule で、インストール済みのモジュールの一覧が取得できますね。
ところが、PowerShell Core 6 を使っていて、ドキュメント フォルダーを OneDrive にバックアップしていると、このコマンドは何も結果を返しません。

何故なの。

OneDrive フォルダはリパース ポイント

「シンボリック リンク」とか「ジャンクション」とか、聞いたことありますか。
平たく言うと、ファイルやフォルダーの実体を、そのパスが指し示す場所とは違う場所に置くという NTFS の機能です。

これらは、より包括的な「リパース ポイント」という仕組みで実現されています。
リパース ポイントという仕組みは汎用的なものであり、シンボリック リンクやジャンクション以外にも、様々なところで使われています。
OneDrive もその一つです。

あるフォルダがリパース ポイントかどうかは、fsutil reparsepoint query というコマンドで調べられます。

f:id:aetos382:20190708114127p:plain

ここで表示される「再解析タグ値」というのが、リパースポイントの種類を示します。
シンボリック リンクとジャンクションが代表的な例ですが、(レガシーなものも含めれば)30 種類以上が定義されています。*2

Get-InstalledModule は内部で Get-ChildItem を使っている

PowerShellGet でモジュールをインストールできる場所は 2 つです。
Install-Module の -Scope パラメーターで間接的に指定でき、PowerShell Core 6 の場合は

  • AllUsers → Program Files 下の PowerShell\Modules
  • CurrentUser → ドキュメント フォルダ下の PowerShell\Modules

となります。

上記のリンクにある Install-Module コマンドの説明では、CurrentUser の場合は $env:HOME から取るように見えますが、これは嘘で、実際は Environment.GetFolderPath("MyDocument") で取得されます。
このメソッドが返すパスは、ドキュメント フォルダを OneDrive にバックアップしている場合、OneDrive 内のパスになります。
つまり、Install-Module がモジュールをインストールするパスも、OneDrive 内になるわけです。

Get-InstalledModule コマンドは、これらのフォルダ内を、Get-ChildItem コマンドを使って再帰的に検索し、PowerShellGet でインストールされたモジュールを探します。*3

PowerShell Core 6 からの仕様変更

PowerShell Core 6 から、Get-ChildItem に -FollowSymLink というパラメーターが追加されています。
再帰的にフォルダー階層を検索していくときに、リパース ポイントを見つけると、-FollowSymLink パラメーターが指定されていない限り、Get-ChildItem はそこより下の階層を見に行きません。

そして、現状、Get-InstalledModule は、その内部で Get-ChildItem を呼び出すときに、このパラメーターをつけていないのです。

Windows PowerShell 5.1 にはこのパラメーターはなく、既定でリパース ポイントを辿るようになっているので、この問題は起きません。

まとめると

  • OneDrive フォルダはリパース ポイント
  • OneDrive バックアップを構成していると、PowerShellGet がモジュールをインストールしたり探索したりするフォルダは OneDrive 下、つまり、リパース ポイントの先になる
  • Get-InstalledModule は Get-ChildItem を使ってモジュールを探索する
  • PowerShell Core 6 から、Get-ChildItem は -FollowSymLink パラメーターをつけないと、リパース ポイントの先を見ない仕様になった
  • Get-InstalledModule は、その内部で Get-ChildItem を呼ぶ際に、-FollowSymLink パラメーターをつけていない

というのが、この問題の原因です。
そのため、Get-InstalledModule が、内部で Get-ChildItem を呼ぶ際に、-FollowSymLink パラメーターをつけるようにしてくれれば解決するはずです。

Windows 10 が推奨する構成でやるとちゃんと機能しないということで、早めに修正して欲しいなあ…と思っています。

*1:英語がアレなのはぐぐる先生のせいです

*2:他には、Windows ストア アプリとか、Git Virtual File System とかで、それぞれ種類の違うリパース ポイントが使われています。それぞれが IReparsePoint インターフェイスを実装した異なるクラスみたいなイメージでよいと思います(そんなインターフェイスは実在しませんが)。

*3:PSGetModuleInfo.xml という隠しファイルを探しています