PowerShellでExcelにファイル一覧を出力させる

# 背景
PowerShellスクリプトでファイル一覧をExcelに出力させること自体はできていたのですが、出力内容をメンテナンスしたい事情（出力項目の位置調整、空白列の挿入など）があり、リファクタリングしてみたので投稿してみました。

# PowerShellで実装したこと
ざっくりいえば、コマンドプロンプトでいうところのtreeコマンドっぽい出力結果をExcelに転記します。（実用するのはスクリプトの成果物（ファイル一覧Excel）の方です。）
<strong>ファイル一覧をExcelに出力</strong>しておけば、Windows10標準ファイルマネージャーの検索機能よりも<font color='blue'>一覧性に優れる</font>し、<font color='blue'>柔軟かつ高速な検索（<strong>ファイル発掘</strong>）</font>ができるようになります。
もちろん、Excelなので<font color='red'>検索結果の行数が多いと重たい</font>し、<font color='red'>リアルタイム性がない</font>といった欠点もありますが、ユースケース次第では結構実用性があるかと思います。

　　※前提1：Windowsのファイルシステムでみえるディレクトリが対象です。Web（ファイルストレージ等）のディレクトリには非対応です(-_-;)
　　※前提2：バージョンはPowerShell 5.1

# 使い方
　1.コードをスクリプトファイル（.ps1）として保存する
　2.ショートカットをつくって、[プロパティをごにょごにょ](https://qiita.com/tomoko523/items/df8e384d32a377381ef9#%E3%82%B7%E3%83%A7%E3%83%BC%E3%83%88%E3%82%AB%E3%83%83%E3%83%88%E4%BD%9C%E6%88%90%E7%B7%A8)する
　3.ショートカット経由でスクリプトを起動する
　4.コンソール画面で検索対象<strong>"ディレクトリ"</strong>のパスを入力しEnterする（"ファイル"のパスだとエラーになる仕様です）

　※3.の後に ↓ な感じなコンソール画面が起動します
![demo1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602161/d7f8f848-2f9f-24a3-3ffb-2ee5a761c21e.png)

検索したいディレクトリのパスを入力（コピペ）&Enter後、しばし待てば・・・
![demo3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602161/d126c799-af9d-1319-262b-b9744684f5da.png)

ファイル一覧Excelが完成！（検索対象ディレクトリ自体は出力されない仕様です）
![demo5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602161/ba3bcb34-27b7-b2b2-3cce-44aa8970a4ed.png)


　※参考情報：自宅環境（Wifi経由でNASストレージを参照）にて、ディレクトリエントリが多いディレクトリに対して実行したところ...
　　<strong>約5.17万件のエントリ　→　約15分で完了</strong>（= エントリ取得 約4分 + Excel出力 約11分、ファイルサイズは3733KB）しました。
　-----
　　21:52:00 ディレクトリからエントリを取得中...
　　21:56:09 51738件のエントリをExcelに書き込み中...
　　22:06:54 以下にファイル出力済み！
　-----
　が、<strong>リードタイムは環境依存要素が強かったハズ</strong>なことをお含めおきください。（ボトルネックはストレージ？）
　職場環境（Wifi経由で共有ストレージ）だと、2～3万件のエントリ取得だけで1時間位かかるので、半年に1回位の頻度で休憩中に実行！～みたいな使い方をしています。（リアルタイム性がないのは妥協）


# コード
```PowerShell:ファイル一覧をExcelに出力する.ps1
# ディレクトリエントリを取得する
function StoreEntrysInHashArray
{
    param(
        $root_directory
    )

    $entrys = @()
    Get-ChildItem $root_directory -Recurse | 
    %{
        # 項目名の順序は保持される。取得不要なら#でコメントアウト （カスタマイズ可）
        $tmp = 
        [ordered]@{
            Name          = $_.Name
            IsDirectory   = $_.PSiscontainer
            Size_KB       = [Math]::Round(($_.Length / 1KB),2)
            Size_MB       = [Math]::Round(($_.Length / 1MB),2)
            #Dummy         = $null
            LastWriteTime = (Get-Date $_.LastWriteTime).ToString("yyyy/MM/dd HH:mm:ss")
            DirectoryName = $_.DirectoryName
            FullName      = $_.FullName
            FullName_lnk  = $_.FullName
        }
        $entrys += $tmp
    }
    
    Write-Output $entrys
    Return
}


# ファイル一覧をExcelで作る
function MakeFileListToExcel
{
    param(
        $entrys
       ,$dn_output = $PSScriptRoot
    )
    
    $fp_output = Join-Path $dn_output ("filelist_{0}.xlsx" -f (Get-Date).ToString("yyyyMMddhhmmss"))

    $xl = New-Object -ComObject Excel.application
    $wb = $xl.Workbooks.add()
    $ws = $wb.WorkSheets.item(1)
    $ktn = $ws.cells(1,1)


    # 連想配列の項目名を書き込む。ヘッダ行として
    $row_offset = $col_offset = 0
    [Array]$header_row = $entrys[0].keys

    $header_row |
    %{
        $ktn.offset($row_offset, $col_offset) = $_
        $col_offset += 1
    }

    $row_offset = 1


    # エントリを書き込む。項目名の末尾が"_lnk" かつ 実在パスの場合はハイパーリンク化
    $entrys | 
    %{
        $entry = $_
        $col_offset = 0

        foreach($col_name in $header_row) 
        {
            $ktn.offset($row_offset,$col_offset) = $entry.$col_name 

            if(($col_name -match ".*_lnk$") -and ($entry.$col_name -notin @("",$null)) -and (Test-Path($entry.$col_name)))
            {
                $ktn.offset($row_offset,$col_offset) = '=Hyperlink("{0}")' -f $entry.$col_name
            } 

            $col_offset += 1
        }

        $row_offset += 1
    }

    [void]$ktn.AutoFilter()

    $wb.saveas($fp_output)
    $xl.quit()
    $ws = $wb = $xl = $null
    [GC]::Collect()

    Write-Output $fp_output
    Return
}


# メイン処理はココから
$root_directory = Read-Host "検索対象のディレクトリを入力したらEnter [中断したい場合は Ctrl+C] `n"
if(-not ((Test-Path $root_directory) -and ((Get-Item $root_directory) -is [System.IO.DirectoryInfo])))
{
    Write-Output "実在するディレクトリを入力してください（>_<）"
    Exit
}


Write-Output ("`n{0} ディレクトリからエントリを取得中..." -f (Get-Date).ToString("HH:mm:ss"))
$entrys = @()
$entrys = StoreEntrysInHashArray -root_directory $root_directory
if($entrys.count -eq 0)
{
    Write-Output "エントリがありませんでした...┐(-ω-;)┌"
    Exit
}

Write-Output ("{0} {1}件のエントリをExcelに書き込み中..." -f (Get-Date).ToString("HH:mm:ss"),$entrys.count)
#$fp_filelist = MakeFileListToExcel -entrys $entrys -dn_output ("C:\Users\hoge\Desktop")
$fp_filelist = MakeFileListToExcel -entrys $entrys


Write-Output ("{0} 以下にファイル出力済み！`n{1}`n" -f (Get-Date).ToString("HH:mm:ss"),$fp_filelist)
Read-Host "出力ファイルを開きますか？[y/n]" | 
%{
    if($_ -eq "y"){Start-Process $fp_filelist}
}


Write-Output "オシマイ!! (/・ω・)/ =3"

```

# 参考にしたWebサイト
[PowerShell で与えられたパスがフォルダかどうか判定する方法](https://qiita.com/hitsumabushi845/items/7132ff880a86f83bab8e)
[PowerShell: orderedによる順序付けされた連想配列](https://step-learn.com/article/powershell/064-hash-ordered.html)
[PowerShellのHashtableの並び替えを制御したい](https://tech.guitarrapc.com/entry/2013/03/20/200351)
[PowerShellにおける"戻り値"と"Return"について](https://blog.shibata.tech/entry/2015/07/05/114903)

# 参考図書
・PowerShell 5.1のお勉強にオススメです　※細かい誤字脱字を気にしない度量のある人向けですが...(・人－)
[PowerShell実践ガイドブック クロスプラットフォーム対応の次世代シェルを徹底解説](https://book.mynavi.jp/ec/products/detail/id=90597)

以上