﻿# PowerShell 的 Get-ChildItem 需注意 -Include, -Path 與 Reparse Points 地雷

我一直覺得 Windows PowerShell 是一個讓人又愛又恨的命令列執行環境，其強型別的優點確實是好的讓人無法拒絕，但其執行環境的複雜度、版本相容性與各種 Cmdlet 的奇葩設計，每次遇到也都是讓人~~心幹神疑~~心曠神怡，不免嘖嘖稱奇。今天我就來分享一個昨天寫文章時遇到的神奇狀況，也就是我們常用的 [Get-ChildItem](https://learn.microsoft.com/zh-tw/powershell/module/microsoft.powershell.management/get-childitem) cmdlet 需注意 `-Path` 與 `-Include` 與 `-Recurse` 的各種用法組合，以及一個 Reparse Points 的問題。

![PowerShell](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/78a13aafdb1d1b39a0a9-213847699-195c2255-afeb-43ad-9f69-b75afca46644.jpg)

### Get-ChildItem 的 Alias 與 `-Recurse` 用法

以下我就列出幾種常見的用法，與其注意事項：

1.  最常見的用法，就是用 `dir` 或 `ls` 來列出**當前資料夾**中的**檔案**與**資料夾**
    
    以下這三個命令都是相同的：
    
    ```powershell
    dir
    ```
    
    ```powershell
    ls
    ```
    
    ```powershell
    Get-ChildItem
    ```
    
2.  列出**當前資料夾**與其**子資料夾**中的**檔案**與**資料夾**
    
    ```powershell
    Get-ChildItem -Recurse
    ```
    
3.  篩選**當前資料夾**中的**特定檔案類型**，例如找出所有 `*.png` 檔案
    
    ```powershell
    dir *.png
    ```
    
4.  篩選**當前資料夾**與其**子資料夾**中的**特定檔案類型**，例如找出所有 `*.png` 檔案
    
    ```powershell
    dir *.png -Recurse
    ```
    

總之，你只要加上 `-Recurse` 參數，就可以列出當前目錄與其子目錄下所有檔案內容！

### Get-ChildItem 的 `-Path`、`-File` 或 `-Directory` 用法

你可以透過 `-Path` 來指定取得指定目錄下的檔案與資料夾：

1.  用 `Get-ChildItem` 列出**指定資料夾**中的**檔案**(不要顯示**資料夾**)
    
    ```powershell
    Get-ChildItem -Path "G:\" -File
    ```
    
2.  用 `Get-ChildItem` 列出**指定資料夾**中的**資料夾**(不要顯示**檔案**)
    
    ```powershell
    Get-ChildItem -Path "G:\" -Directory
    ```
    
3.  用 `Get-ChildItem` 列出**指定資料夾**中的**檔案**且僅顯示 `*.png` 檔案內容
    
    ```powershell
    Get-ChildItem -Path "G:\*.png" -File
    ```
    
4.  用 `Get-ChildItem` 列出**指定資料夾**與其**子資料夾**中的**檔案**且僅顯示 `*.png` 檔案內容
    
    ```powershell
    Get-ChildItem -Path "G:\*.png" -File -Recurse
    ```
    

上述幾種寫法，都是平時經常使用的用法與參數，其實沒太多地雷，但我在做 CI/CD 或批次作業時，就可能會用到一些進階技巧。

### Get-ChildItem 的 `-Include` 用法

這個 `-Include` 用法就真的有雷了，請讓我娓娓道來。

1.  列出**當前資料夾**與其**子資料夾**中所有 `*.png` 與 `*.jpg` 的**檔案**
    
    這語法沒問題：
    
    ```powershell
    Get-ChildItem -Include '*.png','*.jpg' -Recurse
    ```
    
2.  列出**指定資料夾**與其**子資料夾**中所有 `*.png` 與 `*.jpg` 的**檔案**
    
    這語法沒問題：
    
    ```powershell
    Get-ChildItem -Path 'G:\' -Include '*.png','*.jpg' -Recurse
    ```
    
3.  列出**當前資料夾**中所有 `*.png` 與 `*.jpg` 的**檔案**
    
    你如果用以下命令執行，很抱歉，什麼結果都不會有！💥
    
    ```powershell
    Get-ChildItem -Include '*.png','*.jpg'
    ```
    
    你如果用以下命令執行，很抱歉，什麼結果都不會有！💥
    
    ```powershell
    Get-ChildItem -Path . -Include '*.png','*.jpg'
    ```
    
    你如果用以下命令執行，很抱歉，什麼結果都不會有！💥
    
    ```powershell
    Get-ChildItem -Path $PWD -Include '*.png','*.jpg'
    ```
    
    > 這裡的 `$PWD` 是一個 PowerShell 內建的預設變數，代表目前資料夾路徑。
    
    如果加上 `-Recurse` 參數的話，就會有結果！🔥
    
    但我只想要在**當前資料夾**取得 `*.png` 與 `*.jpg` **檔案**要怎麼辦呢？你要這樣寫：
    
    ```powershell
    Get-ChildItem -Path "*" -Include '*.png','*.jpg'
    ```
    
    ```powershell
    Get-ChildItem -Path ".\*" -Include '*.png','*.jpg'
    ```
    
    ```powershell
    Get-ChildItem -Path "$PWD\*" -Include '*.png','*.jpg'
    ```
    
    ```powershell
    Get-ChildItem -Path "G:\*" -Include '*.png','*.jpg'
    ```
    
    這也太怪了吧，為啥 `-Path` 一定要加上 `\*` 才給我用 `-Include` 篩選檔案類型時才有結果呢？正常人應該只會覺得 `-Path` 應該放「資料夾路徑」吧？你說雷不雷！💥
    
    不過，其實[官網文件範例](https://learn.microsoft.com/zh-tw/powershell/module/microsoft.powershell.management/get-childitem?view=powershell-7.3&WT.mc_id=DT-MVP-4015686#4-include)寫的蠻清楚的，只是沒看到這段的人，肯定會跟我一樣，鬼打牆一段時間！😒
    
    ![範例 4：使用 Include 參數取得子專案](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/e4eb5ff167115889d434-213848178-87c9f5d0-72a1-45db-9712-cf522cf5df46.png)
    

### 避免 Get-ChildItem 在遇到 NTFS Reparse Points 引發的無窮迴圈問題

[NTFS reparse point](https://en.wikipedia.org/wiki/NTFS_reparse_point) 是一個相當特殊的設計，從 Windows 2000 (NTFS v3.0) 年代就有了，主要用來擴充 NTFS 檔案系統，你可以把他視為 Linux 的 Hard Link 來看待，但其實 Reparse Point 的功能更多些，而且檔案與資料夾都可以是 Reparse Point，可以讓你**重新解析**(連結)到另一個資料夾或檔案。

總之，如果你的檔案系統中有個 Reparse Point 目錄，很有可能在透過 Get-ChildItem 取得檔案清單時，遇到無窮迴圈的狀況，這個狀況不太好處理，要透過**遞迴**(Recursive)去避開 Reparse Point 資料夾才有辦法解決。

我從 [How to make Get-ChildItem not to follow links - Stack Overflow](https://stackoverflow.com/a/55544084/910074) 找到了一個不錯的解答：

1.  先建立一個 `Get-ChildItemNoFollowReparse` Function
    
    ```powershell
    Function Get-ChildItemNoFollowReparse
    {
        [Cmdletbinding(DefaultParameterSetName = 'Path')]
        Param(
            [Parameter(Mandatory=$true, ParameterSetName  = 'Path', Position = 0,
                      ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
            [ValidateNotNullOrEmpty()]
            [String[]] $Path
        ,
            [Parameter(Mandatory=$true, ParameterSetName  = 'LiteralPath', Position = 0,
                      ValueFromPipelineByPropertyName=$true)]
            [ValidateNotNullOrEmpty()]
            [Alias('PSPath')]
            [String[]] $LiteralPath
        ,
            [Parameter(ParameterSetName  = 'Path')]
            [Parameter(ParameterSetName  = 'LiteralPath')]
            [Switch] $Recurse
        ,
            [Parameter(ParameterSetName  = 'Path')]
            [Parameter(ParameterSetName  = 'LiteralPath')]
            [Switch] $Force
        )
        Begin {
            [IO.FileAttributes] $private:lattr = 'ReparsePoint'
            [IO.FileAttributes] $private:dattr = 'Directory'
        }
        Process {
            $private:rpaths = switch ($PSCmdlet.ParameterSetName) {
                                  'Path'        { Resolve-Path -Path $Path }
                                  'LiteralPath' { Resolve-Path -LiteralPath $LiteralPath }
                              }
            $rpaths | Select-Object -ExpandProperty Path `
                    | ForEach-Object {
                          Get-ChildItem -LiteralPath $_ -Force:$Force
                          if ($Recurse -and (($_.Attributes -band $lattr) -ne $lattr)) {
                              Get-ChildItem -LiteralPath $_ -Force:$Force `
                                | Where-Object { (($_.Attributes -band $dattr) -eq $dattr) -and `
                                                (($_.Attributes -band $lattr) -ne $lattr) } `
                                | Get-ChildItemNoFollowReparse -Recurse
                          }
                      }
        }
    }
    ```
    
2.  接著你可以用以下命令取得**指定資料夾**中所有**檔案**與**資料夾**，但排除 Reparse Point 的**檔案**與**資料夾**
    
    ```powershell
     Get-ChildItemNoFollowReparse G:\ -Recurse
    ```
    
3.  如果想過濾出只有 `*.png` 與 `*.jpg` 檔案的話，就要自己用 `Where-Object` 來過濾了
    
    ```powershell
    Get-ChildItemNoFollowReparse G:\ -Recurse |
      Where-Object { ($_.Extension -eq '.png') -or ($_.Extension -eq '.jpg') }
    ```
    

### 相關連結

-   [Get-ChildItem (Microsoft.PowerShell.Management) - PowerShell | Microsoft Learn](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/get-childitem)
-   [NTFS reparse point - Wikipedia](https://en.wikipedia.org/wiki/NTFS_reparse_point)
-   [Reparse Points - Win32 apps | Microsoft Learn](https://learn.microsoft.com/en-us/windows/win32/fileio/reparse-points?WT.mc_id=DT-MVP-4015686)
-   [powershell - How to make Get-ChildItem not to follow links - Stack Overflow](https://stackoverflow.com/a/55544084/910074)
