🌳

[PowerShell]フォルダーをツリー形式で表示するFunction(CLIとGUIの2通り)

2024/08/09に公開

概要

PowerShellでツリー形式(構造)を出力する場合、以前より「TREE /F "対象フォルダー"」というコマンドプロンプトのコマンドで実行していました。

ただ、TREEコマンドは昔からある古いコマンドでいくつかの課題があると感じていました。わたしが感じていた課題は以下の4つです。

  • 課題1:デフォルトの調査対象がフォルダーのみでファイルも対象にする場合はオプション「/F」の指定が必要
  • 課題2:フォルダー と ファイル を見分けにくい
  • 課題3:オブジェクト数が多いとコマンド出力後にコピーしたい場合、マウスで範囲選択が必要でコピーしにくい
  • 課題4:文字のみの表示となるため、視認しにくい

これら課題を解消するため、今回もPowerShellで2つのFunctionを自作してみました。

この記事のターゲット

  • PowerShellユーザーの方
  • TREEコマンドの課題をクリアしたいと感じている方

対応方法

CLIで出力可能なFunctionGUIで表示可能なFunction の2つを作成。

  • 課題1:デフォルトの調査対象がフォルダーのみでファイルも対象にする場合はオプション「/F」の指定が必要
    双方のFunctionとも規定でフォルダーとファイルすべてをリストアップする仕様に。
  • 課題2:フォルダー と ファイル を見分けにくい
    CLIで出力するFunctionではフォルダーに小なり(<)と大なり(>)で囲むように。
      (例:< フォルダー名 >
      GUIで表示するFunctionではフォルダーは背景色を青色で文字色を白色に変更。
  • 課題3:オブジェクト数が多いとコマンド出力後にコピーしたい場合、マウスで範囲選択が必要でコピーしにくい
    CLIで出力するFunctionで論理値の引数($CopyToClipboard)を設けることで出力結果をクリップボードにコピーできるように。
  • 課題4:文字のみの表示となるため、視認しにくい
    GUIで表示するFunctionでより視覚的にわかりやすく。

対応方法1:CLIで出力可能なFunction

CLIで出力可能なFunctionのコード
function Write-TreeView {
    param (
        [Parameter(Mandatory=$true)][System.String]$Path,
        [System.Boolean]$AsciiMode = $False,
        [System.Boolean]$CopyToClipboard = $False,
        [ValidateSet(1, 2)][System.Int32]$NewlineCode = 1
    )

    # フォルダーがない場合は処理中断
    if (-not (Test-Path -Path $Path)) {
        Write-Warning "指定されたパスは存在しません: $Path"
        return
    }
    
    # 改行コードを設定
    $newlineTable = @{
        1 = "`r`n"  # CRLF
        2 = "`n"    # LF
    }
    
    # 出力形式を設定
    # * 日本語OSではバックスラッシュ(\)が円マーク(¥)となる。
    $indentTable = @{}
    if ($AsciiMode) {
        $indentTable["Pattern1"] = '     '
        $indentTable["Pattern2"] = '|     '
        $indentTable["Pattern3"] = '\---- '
        $indentTable["Pattern4"] = '+---- '
    }
    else {
        $indentTable["Pattern1"] = '   '
        $indentTable["Pattern2"] = '│  '
        $indentTable["Pattern3"] = '└── '
        $indentTable["Pattern4"] = '├── '
    }
    
    # 内部関数:フォルダーのツリー構造を取得する関数
    function Write-FolderTree {
        param (
            [System.String]$CurrentPath,
            [System.Collections.Hashtable]$Levels
        )

        # ファイルが先頭に表示されるよう並び替え
        $items = (Get-ChildItem -Path $CurrentPath | Sort-Object { -not $_.PSIsContainer }, Name)
        $count = $items.Count
        $index = 0
        
        $ouputFoldertree = ""

        foreach ($item in $items) {
            $index++
            $isLastItem = ($index -eq $count)
            $level = $Levels.Count
            $Levels[$level] = $isLastItem
            $indent = ""

            for ($i = 0; $i -lt $level; $i++) {
                $indent += if ($Levels[$i]) { "$($indentTable["Pattern1"])" } else { "$($indentTable["Pattern2"])" }
            }

            $line = if ($isLastItem) { "$($indentTable["Pattern3"])" } else { "$($indentTable["Pattern4"])" }

            if ($item.PSIsContainer) {
                # フォルダーの場合
                $ouputFoldertree += "$($indent)$($line)< $($item) >$($newlineTable[$NewlineCode])"
                $ouputFoldertree += (Write-FolderTree -CurrentPath $item.FullName -Levels $Levels)
            } else {
                # ファイルの場合
                $ouputFoldertree += "$($indent)$($line)$($item)$($newlineTable[$NewlineCode])"
            }
            
            # フォルダー内で最終アイテムの場合に次のアイテムと間隔を空ける
            if ($isLastItem) {
                $ouputFoldertree += "$($indent)$($newlineTable[$NewlineCode])"
            }

            $Levels.Remove($level)
        }
        
        return $ouputFoldertree
    }

    $Levels = @{}
    $outputTreeview =  "$($Path)$($newlineTable[$NewlineCode])"
    $outputTreeview += (Write-FolderTree -CurrentPath $Path -Levels $Levels)
    
    if ($CopyToClipboard) {
        $outputTreeview | Set-Clipboard
        Write-Output "出力結果をクリップボードにコピーしました!$($newlineTable[$NewlineCode])"
    }
    else {
        Write-Output $outputTreeview
    }
}
コピー用
Write-TreeView -Path "対象フォルダーのパス"
実際に実行した結果パターン1 - 標準出力する場合
PS C:\WINDOWS\system32> Write-TreeView -Path "C:\Program Files\WindowsPowerShell"
C:\Program Files\WindowsPowerShell
├── < Configuration >
│  ├── < Registration >
│  └── < Schema >
│  
├── < Modules >
│  ├── < IconExport >
│  │  └── < 2.0.0 >
│  │     ├── IconExport.psd1
│  │     └── IconExport.psm1
│  │     
│  │  
│  ├── < ImportExcel >
│  │  └── < 7.8.9 >
│  │     ├── < .github >
│  │     │  ├── < workflows >
│  │     │  │  └── ci.yml
│  │     │  │  
│  │     │  └── stale.yml
│  │     │  
│  │     ├── < .vscode >
│  │     │  └── spellright.dict
│  │     │  
│  │     ├── < __tests__ >
│  │     │  ├── < ImportExcelTests >
│  │     │  │  ├── construction.xlsx
│  │     │  │  ├── DataInDiffRowCol.xlsx

~ 省略 ~

│  
└── < Scripts >
   ├── < InstalledScriptInfos >
   │  └── Install-AboutHelp_InstalledScriptInfo.xml
   │  
   └── Install-AboutHelp.ps1
   


PS C:\WINDOWS\system32>
実際に実行した結果パターン2 - クリップボードにコピーする場合
PS C:\WINDOWS\system32> Write-TreeView -Path "C:\Program Files\WindowsPowerShell" -CopyToClipboard $true
出力結果をクリップボードにコピーしました!

PS C:\WINDOWS\system32>

対応方法2:GUIで表示可能なFunction

"自作Function「Out-TreeView」の画面"
実際の画面:自作Function「Out-TreeView」

Functionを実行し表示されるWindowsフォームの画面では下記のショートカットキーが機能します。

  • ツリー上のファイルを ダブルクリック
    拡張子に紐づいている既定のソフトで開く
  • ツリー上のフォルダーまたはファイルを選択中に Enterキー
    ファイルの場合は拡張子に紐づいている既定のソフトで開く。
    フォルダーの場合はエクスプローラーで開く。
  • ツリー上のフォルダーまたはファイルを選択中に CtrlC
    選択中のオブジェクトの名前のみクリップボードにコピーする。
  • ツリー上のフォルダーまたはファイルを選択中に CtrlShiftC
    選択中のオブジェクトの絶対パスをクリップボードにコピーする。
GUIで表示可能なFunctionのコード
function Out-TreeView {
    param (
        [Parameter(Mandatory=$true)][System.String]$Path,
        [System.String[]]$WindowSize = @(600, 450),
        [System.Single]$FontSize = 9.5
    )

    # フォルダーがない場合は処理中断
    if (-not (Test-Path -Path $Path)) {
        Write-Warning "指定されたパスは存在しません: $Path"
        return
    }

    # 内部関数:フォルダーのツリー構造を取得する関数
    function Get-FolderTree {
        param (
            [System.String]$Path,
            [Windows.Forms.TreeNode]$ParentNode
        )
        $items = (Get-ChildItem -Path $Path)
        foreach ($item in $items) {
            $node = New-Object Windows.Forms.TreeNode
            $node.Text = $item.Name
            $node.Tag = $item.FullName
            if ($item.PSIsContainer) {
                $node.BackColor = [System.Drawing.Color]::DarkBlue
                $node.ForeColor = [System.Drawing.Color]::AliceBlue
                Get-FolderTree -Path $item.FullName -ParentNode $node
            } else {
                $node.ForeColor = [System.Drawing.Color]::Black
            }
            $ParentNode.Nodes.Add($node) > $null
        }
    }

    # フォームの作成
    Add-Type -AssemblyName System.Windows.Forms
    Add-Type -AssemblyName System.Drawing

    $mainForm = New-Object Windows.Forms.Form
    $mainForm.Text = "$($Path) - $($MyInvocation.MyCommand.Name)"
    $mainForm.Size = New-Object Drawing.Size($WindowSize[0], $WindowSize[1])
    $mainForm.Icon = [System.Drawing.Icon]::ExtractAssociatedIcon("$PSHOME\powershell.exe")
    
    # ステータスバーの作成
    $statusBar = New-Object Windows.Forms.StatusBar
    $mainForm.Controls.Add($statusBar)

    # ツリービューの作成
    $treeView = New-Object Windows.Forms.TreeView
    [System.Int32]$witdhSize = [System.Int32]$WindowSize[0] * 0.957
    [System.Int32]$heightSize = [System.Int32]$WindowSize[1] * 0.845
    $treeView.Size = New-Object Drawing.Size($witdhSize, $heightSize)
    $treeView.Location = New-Object Drawing.Point(5, 5)
    $treeView.Anchor = `
    [System.Windows.Forms.AnchorStyles]::Top -bor `
    [System.Windows.Forms.AnchorStyles]::Bottom -bor `
    [System.Windows.Forms.AnchorStyles]::Left -bor `
    [System.Windows.Forms.AnchorStyles]::Right
    $treeView.Font = New-Object System.Drawing.Font("Microsoft Sans Serif", $FontSize)

    # ルートノードの作成
    $root = New-Object Windows.Forms.TreeNode
    $root.Text = $Path
    $root.Tag = $Path
    $root.BackColor = [System.Drawing.Color]::DarkBlue
    $root.ForeColor = [System.Drawing.Color]::AliceBlue
    $treeView.Nodes.Add($root) > $null

    # ツリー構造の取得
    Get-FolderTree -Path $root.Tag -ParentNode $root

    # ノード選択イベントの設定
    $treeView.add_AfterSelect({
        param ($sender, $e)
        $item = Get-Item -Path $e.Node.Tag
        $statusBar.Text = "$($item.Name) - Last Modified: $($item.LastWriteTime)"
    })

    # ノードダブルクリックイベントの設定
    $treeView.add_NodeMouseDoubleClick({
        param ($sender, $e)
        $item = Get-Item -Path $e.Node.Tag
        if (-not ($item.PSIsContainer)) {
            Invoke-Item -Path $item.FullName
        }
    })
    
    # ショートカットキー
    $treeView.add_KeyDown({
        param ($sender, $e)
        # Enterキーが押された場合
        if ($e.KeyCode -eq [System.Windows.Forms.Keys]::Enter) {
            $selectedNode = $treeView.SelectedNode
            if ($selectedNode -ne $null) {
                $item = Get-Item -Path $selectedNode.Tag
                if ($item.PSIsContainer) {
                    Start-Process explorer.exe -ArgumentList $item.FullName
                } else {
                    Invoke-Item -Path $item.FullName
                }
            }
        }
        # Ctrl + Shift + C:ノード名の絶対パスをクリップボードにコピー
        elseif ($e.Control -and $e.Shift -and $e.KeyCode -eq [System.Windows.Forms.Keys]::C) {
            $selectedNode = $treeView.SelectedNode
            if ($selectedNode -ne $null) {
                [System.Windows.Forms.Clipboard]::SetText($selectedNode.Tag)
            }
        }
        # Ctrl + C:ノード名をクリップボードにコピー
        elseif ($e.Control -and $e.KeyCode -eq [System.Windows.Forms.Keys]::C) {
            $selectedNode = $treeView.SelectedNode
            if ($selectedNode -ne $null) {
                [System.Windows.Forms.Clipboard]::SetText($selectedNode.Text)
            }
        }
    })

    # フォームにツリービューを追加
    $mainForm.Controls.Add($treeView)

    # フォームの表示
    $mainForm.Add_Shown({$mainForm.Activate()})
    $mainForm.ShowDialog() > $null
}
コピー用
Out-TreeView '対象フォルダーのパス'

実際に実行した際の画面は前述したため、割愛。

まとめ

  • コマンドベース(CLI)と画面ベース(GUI)でツリー形式で表示するFunctionを作成できた
  • 個人的に感じていたコマンドプロンプト「TREEコマンド」の課題をクリアできた

ここまで書いておいてなんですが、ツリービューは視覚的に埋もれたり隠れたりしてしまうことが多く操作も独特です。
また、結果を表示させてから内容を変更したい場合、テキストベースで編集しにくいという点あります。

わたし個人としてツリー形式は苦手であまり操作したくない表示形式。ツリー形式以外で表現できるのであれば、それを採用しちゃってますね。

関連記事

https://haretokidoki-blog.com/pasocon_powershell-startup/
https://zenn.dev/haretokidoki/articles/7e6924ff0cc960
https://zenn.dev/haretokidoki/articles/fb6830f9155de5

GitHubで編集を提案

Discussion