PowerShellでFileSystemObjectのGetFolder(‘.’)がホームディレクトリを返す原因とフォルダーサイズ取得の正しい方法

PowerShell でフォルダーサイズを取得しようとして FileSystemObject の GetFolder(".") を使ったところ、なぜかカレントディレクトリではなくユーザープロファイル(ホームディレクトリ)を指してしまう――この現象は、PowerShell 特有の「現在の場所」と Windows プロセスの「作業ディレクトリ」のズレが原因です。本記事では、その仕組みを図解レベルで整理しつつ、安全な対処方法と、PowerShell ネイティブでフォルダーサイズを求める実践的なサンプルまでまとめて解説します。

目次

PowerShell で GetFolder(“.”) がホームを返す現象とは

まずは問題の全体像を整理します。環境としては次のようなイメージです。

  • OS:Windows 10 / Windows 11
  • PowerShell:5.1 または 7 系(7.5.2 など)
  • 目的:フォルダーのサイズを簡単に取得したい
  • 手段:COM の Scripting.FileSystemObject を利用(Folder.Size プロパティが便利)

ところが、次のようなスクリプトを実行すると、期待した動きになりません。

Set-Location 'C:\Work\Logs'

$fso = New-Object -ComObject Scripting.FileSystemObject
$folder = $fso.GetFolder('.')

$folder.Path   # ← C:\Work\Logs になると思いきや…

多くの人が期待する結果は C:\Work\Logs ですが、実際には次のようにユーザープロファイル(ホームディレクトリ)になってしまう場合があります。

C:\Users\ユーザー名

同じ Scripting.FileSystemObject を VBScript(WSH)から呼ぶと "." は期待どおり実行中のスクリプトフォルダーとして解決されるのに、PowerShell ではホームを指してしまうことがある――これが今回のテーマです。

原因:PowerShell の「現在の場所」とプロセスの「作業ディレクトリ」の違い

この挙動のカギになっているのが、次の 2 つの概念の違いです。

  • PowerShell の現在の場所:$pwd / Get-Location
  • Windows プロセスの作業ディレクトリ:[System.Environment]::CurrentDirectory

両者は「カレントディレクトリ」という意味では似ていますが、管理している主体が違います。

観点PowerShell の現在の場所
$pwd / Get-Location
プロセスの作業ディレクトリ
[Environment]::CurrentDirectory
管理しているものPowerShell ランタイム(プロバイダー含む)Windows プロセス全体(.NET / COM / Win32 API)
変更方法Set-Location / cd[Environment]::CurrentDirectory = 'パス'
影響する対象PowerShell のコマンドレットやプロバイダー.NET クラス・COM コンポーネント・多くの外部コマンド
例Get-ChildItem での相対パス解決Scripting.FileSystemObject.GetFolder(".") の解決基準

PowerShell では、Set-Location を実行しても [Environment]::CurrentDirectory は自動では変わりません。そのため、次のような状況が発生します。

Set-Location 'C:\Work\Logs'

$pwd.Path                           # C:\Work\Logs
[System.Environment]::CurrentDirectory  # C:\Users\ユーザー名 (例)

このズレがある状態で Scripting.FileSystemObject を使うと、"." は $pwd ではなく プロセスの作業ディレクトリ(多くの場合、起動時のホームディレクトリ)として解釈されます。これが「GetFolder(".") がホームを返してしまう」直接の原因です。

なぜ VBScript では問題になりにくいのか

同じ Scripting.FileSystemObject を VBScript(WSH)から使うときには、一般的にこの問題が表面化しません。その理由は、次のような点にあります。

  • VBScript は WScript.exe / CScript.exe から起動される
  • 起動された時点の 作業ディレクトリ がそのまま "." の基準になる
  • 多くの場合、「スクリプトを置いたフォルダー」から実行されるため、意図と実際が一致しやすい

つまり、VBScript では「スクリプト実行コンテキスト」と「プロセスの作業ディレクトリ」が一致していることが多く、意識しなくても GetFolder(".") が「スクリプトのある場所」として動いてくれる、というわけです。

一方で PowerShell は、「シェルとしての現在位置($pwd)」と「プロセスの作業ディレクトリ」を分けて管理しているため、この差が問題として現れてきます。

解決策 A:絶対パスを渡す(副作用なし・最も安全)

まず紹介するのは、最も安全で副作用のない方法です。"." を使わずに、絶対パスを明示的に渡すやり方です。

$fso = New-Object -ComObject Scripting.FileSystemObject

# "." ではなく $pwd.Path を渡す
$folder = $fso.GetFolder($pwd.Path)

$folder.Path  # 期待どおり、現在の PowerShell の場所
$folder.Size  # フォルダーサイズも取得可能

ポイントは次のとおりです。

  • プロセスの作業ディレクトリに依存しないため、どのホストから起動しても挙動が安定する
  • Set-Location で $pwd を変えても、その時点のパスを直接指定するのでズレが起きない
  • COM オブジェクトの生成タイミングにも依存しない

一方で、既存コードで GetFolder(".") を多用している場合、すべて GetFolder($pwd.Path) などに書き換える必要があるというデメリットがあります。

観点評価
安全性・予測可能性非常に高い(推奨)
既存コードとの互換性要修正("." を絶対パスに置き換え)
他のライブラリへの影響なし(作業ディレクトリを変更しない)

「これから書くスクリプト」「自分だけがメンテナンスするコード」であれば、基本的にはこの解決策 A を選ぶのが最善です。

解決策 B:「.」を使いたい場合に作業ディレクトリを同期する

次に、既存の VBScript 互換コードなどで GetFolder(".") という書き方を変えたくない場合の対処です。ポイントは、COM オブジェクトを作る前に、プロセスの作業ディレクトリを PowerShell の現在の場所に合わせることです。

# 1. PowerShell の現在地を変更
Set-Location 'C:\New\Vbs'

# 2. プロセス作業ディレクトリを $pwd に同期
[System.Environment]::CurrentDirectory = (Get-Location).Path

# 3. その後で FSO を生成
$fso = New-Object -ComObject Scripting.FileSystemObject

# 4. "." を使っても期待どおり C:\New\Vbs を指す
$folder = $fso.GetFolder(".")

$folder.Path  # C:\New\Vbs
$folder.Size  # サイズ取得も OK

この方法の長所・短所は次のとおりです。

項目内容
メリット既存コードの "." をそのまま活かせる VBScript からの移植コードをほとんど変更せずに使える
デメリットプロセス全体の作業ディレクトリを変更するため、副作用の可能性がある 外部コマンドや一部の .NET ライブラリが相対パスを解決する基準も変わる 他のモジュールやスクリプトと同じプロセスで動いている場合、予期せぬ衝突が起こることも

特に注意したいのは、作業ディレクトリを変えたあとに

  • robocopy や git などの外部コマンドを相対パス付きで呼び出す
  • .NET の API が内部で [Environment]::CurrentDirectory を使っている

といった場合です。思わぬところで「パスがおかしい」「ファイルが見つからない」といった問題に発展することがあります。

現在の状態を確認するチェックコード

挙動がおかしいと感じたときは、まず次の 2 行で状況を確認するのがおすすめです。

$pwd.Path
[System.Environment]::CurrentDirectory

この 2 つの値が違っていれば、GetFolder(".") などの動作が変になる可能性があります。特に、長時間起動しっぱなしの PowerShell セッションや、プロファイルスクリプトで何らかの初期化処理をしている場合は、一度確認しておくと安心です。

セッション開始時に自動で同期してしまう運用

「常に "." を現在の PowerShell の場所として扱いたい」という場合は、PowerShell のプロファイルに次のような 1 行を追加しておく方法もあります。

[System.Environment]::CurrentDirectory = (Get-Location).Path

ただし、この方法はセッション全体の挙動を変えるため、チームで共通の環境を使っている場合などは事前に合意をとった方が安全です。また、プロファイル内で何度も Set-Location を行っている場合は、最終的な状態を確認した上で設定するようにしましょう。

解決策 C:COM(FSO)を使わず PowerShell ネイティブでフォルダーサイズを取得する

そもそもの目的が「フォルダーサイズの取得」であれば、COM の Scripting.FileSystemObject にこだわる必要はありません。PowerShell 標準コマンドレットだけで、十分実用的なフォルダーサイズ集計が行えます。

基本形:隠しファイルも含めて合計サイズを求める

$bytes = (
    Get-ChildItem -LiteralPath . -Recurse -File -Force -ErrorAction SilentlyContinue |
    Measure-Object -Property Length -Sum
).Sum

"{0:N2} GB" -f ($bytes / 1GB)

このコードのポイントは次のとおりです。

  • -LiteralPath .:現在の PowerShell の場所($pwd)を起点にする
  • -Recurse:フォルダー配下を再帰的に走査
  • -File:ファイルだけを対象にする(サブフォルダー自体のエントリは集計しない)
  • -Force:隠しファイルやシステム属性ファイルも含める
  • -ErrorAction SilentlyContinue:アクセス拒否などを無視して続行
  • Measure-Object Length -Sum:ファイルサイズ(バイト)を合計

これで、GetFolder().Size と同等以上の情報が得られます。さらに、必要に応じて GB / MB などに変換したり、テーブル形式で表示するのも簡単です。

ジャンクション / シンボリックリンクを除外したい場合

ユーザープロファイル配下には Application Data などアクセス制限付きのジャンクションが多く存在します。これらを辿ると、

  • アクセス拒否エラーが大量に出る
  • 無限ループに近い構造を辿ってしまい、処理時間が伸びる

といった問題が起こりがちです。そこで、再解析ポイント(ReparsePoint)を除外するフィルターを追加した例がこちらです。

$bytes = (
    Get-ChildItem -LiteralPath . -Recurse -Force -ErrorAction SilentlyContinue |
    Where-Object {
        -not $_.PSIsContainer -and
        -not ($_.Attributes -band [IO.FileAttributes]::ReparsePoint)
    } |
    Measure-Object Length -Sum
).Sum

"{0:N2} GB" -f ($bytes / 1GB)

ここでは

  • -not $_.PSIsContainer – フォルダー(コンテナ)そのものを除外
  • -not ($_.Attributes -band [IO.FileAttributes]::ReparsePoint) – ジャンクションやシンボリックリンクを除外

という 2 段階のフィルターで、実ファイルだけを対象にしています。

よく使うなら関数化しておくと便利

何度も同じロジックを書く場合は、PowerShell プロファイルなどに関数として登録しておくと便利です。

function Get-FolderSize {
    [CmdletBinding()]
    param(
        [Parameter(Position = 0)]
        [string]$Path = '.',

        [switch]$ExcludeReparsePoint
    )

    $items = Get-ChildItem -LiteralPath $Path -Recurse -Force -ErrorAction SilentlyContinue

    if ($ExcludeReparsePoint) {
        $items = $items | Where-Object {
            -not $_.PSIsContainer -and
            -not ($_.Attributes -band [IO.FileAttributes]::ReparsePoint)
        }
    }
    else {
        $items = $items | Where-Object { -not $_.PSIsContainer }
    }

    $bytes = ($items | Measure-Object Length -Sum).Sum

    [PSCustomObject]@{
        Path  = (Resolve-Path -LiteralPath $Path).Path
        Bytes = $bytes
        MB    = [math]::Round($bytes / 1MB, 2)
        GB    = [math]::Round($bytes / 1GB, 2)
    }
}

使用例:

Get-FolderSize -Path . -ExcludeReparsePoint

Get-FolderSize -Path 'C:\Work\Logs' | Format-Table

このようにしておけば、FSO に依存せず、PowerShell だけでフォルダーサイズを扱えるようになります。PowerShell 7 であれば Linux / macOS でも同じコードが動くので、クロスプラットフォームなスクリプトを目指す場合にも有利です。

FSO の Folder.Size を使う場合の注意点

それでも「既存の VBScript 互換コードが大量にあり、FSO を使った方が楽」というケースもあります。その場合に押さえておきたいポイントを整理します。

ジャンクション/アクセス制限フォルダーでのエラー

ユーザープロファイル直下の例:

  • C:\Users\ユーザー名\Documents
  • C:\Users\ユーザー名\AppData
  • C:\Users\ユーザー名\Application Data(ジャンクション)

これらを GetFolder().Size で集計しようとすると、アクセス権の問題やジャンクションのループ構造の影響で時間がかかる、あるいはエラーになることがあります。FSO を使う場合でも、次のような対策を併用すると安定します。

  • 管理者権限の PowerShell を使う
  • アクセス拒否は try-catch で捕まえてログに記録し、処理自体は続ける
  • ジャンクションに対してはそもそも GetFolder を呼ばない設計にする

ただし、ここまで工夫するのであれば、前述の PowerShell ネイティブ集計に切り替える方が、制御しやすく保守もしやすい、というのが実務的な感想です。

パターン別:どの解決策を選ぶべきか

ここまで紹介した 3 つの解決策を、よくあるシナリオ別に整理してみます。

シナリオ推奨する解決策理由
新規に PowerShell スクリプトを書く解決策 C(PowerShell ネイティブ集計)
または 解決策 A(絶対パスを渡す)
COM 依存を避けることでクロスプラットフォームになり、保守性も高い
VBScript から移植した FSO ベースのコードがあり、なるべく変えたくない解決策 B(作業ディレクトリを同期)GetFolder(".") をほぼそのまま使い回せる。ただし副作用の理解が必須
小規模なスクリプトで局所的に FSO を使いたい解決策 A(絶対パス)作業ディレクトリをグローバルに変えず、安全に FSO を利用できる
チームで共有する汎用スクリプト解決策 C(PowerShell ネイティブ集計)環境依存を減らし、他メンバーの開発環境に左右されにくくできる

実務でハマりやすいポイントとチェックリスト

最後に、フォルダーサイズ取得や FSO 利用時に実務でハマりやすいポイントをチェックリストとしてまとめます。

  • $pwd.Path と [Environment]::CurrentDirectory は一致しているか?
  • FSO を使っている理由は本当にあるか? – PowerShell ネイティブで代替できないか?
  • ジャンクション / シンボリックリンクをどう扱うか? – 辿るのか、除外するのか方針を決める
  • アクセス拒否が出るフォルダーを処理対象に含めるか? – ログだけ残してスキップする運用も検討
  • 作業ディレクトリの変更が他の処理に影響しないか? – 外部コマンドや .NET を多用するスクリプトでは特に注意

このあたりを意識しておくと、「昨日は動いていたのに今日は動かない」「このマシンだけ挙動が違う」といったトラブルをかなり減らせます。

まとめ:原因を理解して、用途に合わせて選ぶ

今回の「PowerShell で GetFolder(".") がホームを返す」現象の本質は、次の 1 行に集約できます。

「"." の基準が $pwd ではなく [Environment]::CurrentDirectory である」

これを踏まえた上で、実務的な結論を整理すると次のようになります。

  • 手っ取り早く確実に直すなら → GetFolder($pwd.Path) を使う(解決策 A)
  • "." をどうしても維持したいなら → COM 生成前に [Environment]::CurrentDirectory を同期する(解決策 B)
  • フォルダーサイズ取得が目的なら → PowerShell ネイティブで集計する(解決策 C) 方が長期的に楽

PowerShell は「現在の場所」という概念をプロバイダーで拡張できる柔軟なシェルですが、その反面、Windows の従来 API(COM / Win32)との間にこうしたギャップが生まれやすくなります。今回のように「何がどの値を基準にしているのか」を一度きちんと整理しておくと、ファイルシステム周りのスクリプトトラブルを大きく減らすことができるはずです。

今後、PowerShell でフォルダーサイズを取得したり、FileSystemObject を使うことがあれば、ここで紹介した 3 つのパターンを思い出して、用途に合った方法を選んでみてください。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次