監視結果の一部を抽出し、記録用エクセルへ転記する処理

## 状況：
・サーバーごとの空き容量を含む、様々な監視の結果が記入されている文章が生成される
・現在は毎日一回、その文章を参照し、サーバーごとの空き容量を指定のエクセルファイルに記入している
・エクセルファイルの記入は、縦軸に日付、横軸にサーバー名が設定されており、対応する欄へ一日分につき一行記入することとなっている

<br>

## 処理概要：
#### １．入力用ファイルの確認
監視結果文章内にはそのファイルが監視した日付の記載がなかったため、
文章を「yyyymmdd.txt」という名前で所定のフォルダへ配置してもらうことで、入力用テキストファイルとして対象とするようにしています。
#### ２．出力用ファイルの確認
出力用エクセルファイルが存在するか、実行時にエクセルが実行中だった場合は当該ファイルを開いていないか、いずれも問題なかった場合は当該ファイル内に正しく処理対象のシートが存在しているか、を確認しています。
#### ３．処理
条件に合致した入力用テキストファイルの数だけ、処理を繰り返します。具体的には、下記の通りです。
ファイル名から記入したい日付を取得
 → その日付が処理対象のシートの何行目を記入欄としているかを把握
 → テキストファイル内の文書で目的の内容（サーバーごとの空き容量）を示す部分を抽出
 → 一緒に抽出したサーバー名が処理対象のシートの何列目を記入欄としているかを把握
 → 行と列の把握により特定できたセルに記入

<br>

## main
```powershell:main
function main(){
    #初期化
    initialize
    #実行前必要要素チェック
    if(-not (preCheck)){
        wh "処理を中止します。"
        return
    }
    #処理実行
    execute
    #処理終了を通知
    showNotify -msg "処理が終了しました。"
}
```

## Initialize
```powershell:initialize
function initialize(){
    #エイリアス設定
    Set-Alias wh Write-Host -Scope Script
    Set-Alias tp Test-Path -Scope Script
    
    #入力用テキストファイル配置場所のフォルダパス
    Set-Variable -Name "INPUT_DIR" `
        -Value "（任意のフォルダパス）" `
        -Scope Script
    #出力用エクセルファイルパス
    Set-Variable -Name "OUTPUT_FILEPATH" `
        -Value "（任意のパス）" `
        -Scope Script
    #出力用エクセルシート名
    Set-Variable -Name "OUTPUT_SHEETNAME" `
        -Value "（任意のシート名）" `
        -Scope Script

    #通知用ライブラリ読み込み
    Add-Type -AssemblyName System.Windows.Forms
}
```

## preCheck
```powershell:preCheck
function preCheck(){
    
    #入力用テキストファイルのチェック
    function isInputFileAvailable(){
        #指定場所に存在する、条件に合うテキストファイルのみを配列化
        $script:inputFilesArray = @(Get-ChildItem -Path $script:INPUT_DIR -Filter *.txt -ErrorAction SilentlyContinue | Where-Object {
            #条件：yyyymmdd.txt（ただしyyyymmddは日付として成立するものに限る）
            $_.BaseName -match '^\d{8}' `
            -and `
            [datetime]::TryParseExact($_.Name.Substring(0, 8), 'yyyyMMdd', $null, [System.Globalization.DateTimeStyles]::None, [ref]$null)
        })
        #配列の要素数が0かどうかで判定
        if($script:inputFilesArray.Count -eq 0){
            wh "エラー：入力用テキストファイルが存在しません。"
            return $false
        }
        return $true
    }
    
    #出力用エクセルファイルのチェック
    function isOutputFileAvailable(){
        #存在チェック
        if(-not (tp $script:OUTPUT_FILEPATH)){
            wh "エラー：出力用エクセルファイルが存在しません。"
            return $false
        }
        #既にエクセルが実行中だった場合、対象ファイルを開いていないかチェック
        if(Get-Process -Name EXCEL -ErrorAction SilentlyContinue){
            try{
                $oldExcel = [Runtime.InteropServices.Marshal]::GetActiveObject("Excel.Application")
                foreach($wbTemp in $oldExcel.Workbooks){
                    if($wbTemp.Name -eq (Split-Path -Path $script:OUTPUT_FILEPATH -Leaf)){
                        wh "エラー：処理対象のブックが既に開かれています。閉じてから再実行してください。"
                        return $false
                    }
                }
            } catch {
                wh "エラー：起動中エクセルのオブジェクト取得に失敗しました。"
                return $false
            }
        }
        #対象ファイルにおける、対象シート存在チェック
        try{
            $script:newExcel = New-Object -ComObject Excel.Application
            $script:newExcel.Visible = $false
            $script:wb = $script:newExcel.Workbooks.Open($script:OUTPUT_FILEPATH)
            $sheetNamesArray = @($script:wb.Sheets | forEach-Object {$_.Name})
            if(-not ($sheetNamesArray -contains $script:OUTPUT_SHEETNAME)){
                wh "エラー：処理対象のブックに、処理対象のシートが存在しません。"
                return $false
            }
            $script:ws = $script:wb.Sheets.Item($script:OUTPUT_SHEETNAME)
        } catch {
            wh "エラー：処理対象のブック読み取りに失敗しました。"
            return $false
        }
        return $true
    }

    #入力用テキストファイル・出力用エクセルファイル、両方のチェックが問題なければtrueを返す
    if(isInputFileAvailable -and isOutputFileAvailable){
        return $true
    }
    wh "処理を中断します。"
    return $false
    
}
```

## execute
```powershell:execute
function execute(){
    #出力用エクセルファイルに記入したことを示す結果変数を用意
    $isWritten = $false
    #入力用テキストファイルの数だけ、処理を繰り返す
    foreach($inputFile in $script:inputFilesArray){
        #現在処理中のテキストファイルが、ファイル名で指定している日付を取得
        $inputfileDate = [datetime]::ParseExact($inputFile.BaseName, "yyyyMMdd", $null)
        $inputFileDate = $inputFileDate.ToString("yyyy/M/d")
        #取得した日付を、出力用シート上で検索
        $dateCell = $script:ws.UsedRange.Find(
            $inputFileDate,
            [Type]::Missing,
            [Type]::Missing,
            [Type]::Missing,
            1,
            1,
            $false,
            [Type]::Missing,
            [Type]::Missing,
        )
        #取得した日付でセルがヒットした場合、処理を継続
        if($dateCell -ne $null){
            #取得した日付に対応する入力範囲を取得
            $row = $dateCell.Row
            $entryRange = $script:ws.Range(
                $script:ws.Cells.Item($row, 2),
                $script:ws.Cells.Item($row, 17)
            )
            #取得した日付に対応する入力範囲が全てブランクであることを確認
            $isAllEntryRangeBlank = $true
            foreach($cellTemp in $entryRange.Cells){
                if($cellTemp.Text -ne ""){
                    $isAllEntryRangeBlank = $false
                    break
                }
            }
            if(-not ($isAllEntryRangeBlank)){
                wh "${inputFileDate} の行に値が記入済みなため、処理をスキップしました。"
            } else {
                #記入したセルの数を数えるための変数を用意
                $numOfWroteCells = 0
                #ログファイルから「サーバ名」「空き容量」「最大容量」を取得
                Get-Content "$script:INPUT_DIR\$inputFile" | forEach-Object {
                    $line = $_
                    if($line -match 'サーバ「(?<serverName>[^ 」]+)」.*?\((?<freeSpace>\d+)/(?<totalSpace>\d+)G\)'){
                        #取得した日付の行 * サーバ名でヒットしたセルの列 に、空き容量を記入
                        $serverNameCell = $script:ws.UsedRange.Find(
                            $($matches['serverName']),
                            [Type]::Missing,
                            [Type]::Missing,
                            [Type]::Missing,
                            1,
                            1,
                            $false,
                            [Type]::Missing,
                            [Type]::Missing
                        )
                        if($serverNameCell -ne $null){
                            $col = $serverNameCell.Column
                            $script:ws.Cells.Item($row, $col).Value2 = $($matches['freeSpace'])
                            $numOfWroteCells++
                        } else {
                            wh "${$matches['serverName']} の記入列が見つからなかったため、処理をスキップしました。"
                            break
                        }
                    }
                }
                if($numOfWroteCells -eq （サーバー数）){
                    wh "${inputFileDate} は、正しく記入されました。"
                    $isWritten = $true
                } elseif($numOfWroteCells -eq 0) {
                    wh "${inputFileDate} は、記載できませんでした。入力用ファイルを確認してください。"
                } else {
                    wh "${inputFileDate} は、不十分に記入されました。確認してください。"
                    $isWritten = $true
                }
            }
        } else {
            wh "${inputFileDate} の記入行が見つからなかったため、記入をスキップしました。"
        }
    }
    #後始末
    if($isWritten){
        $script:wb.Save()
    }
    $script:wb.Close()
    $script:newExcel.Quit()
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($script:ws) | Out-Null
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($script:wb) | Out-Null
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($script:newExcel) | Out-Null
    [GC]::Collect()
    [GC]::WaitForPendingFinalizers()
}
```

## showNotify
```powershell:showNotify
function showNotify(){
    param(
        [string]$msg,
        [string]$title = "（任意のタイトル）",
        [System.Windows.Forms.ToolTipIcon]$iconType = "Info"
    )
    $notify = New-Object Sustem.Windows.Forms.NotifyIcon
    $notify.Icon = [System.Drawing.SystemIcons]::Information
    $notify.BalloonTipText = $msg
    $notify.BalloonTipTitle = $title
    $notify.BalloonTipIcon = $iconType
    $notify.Visible = $true
    $notify.ShowBalloonTip(3000)

    Start-Sleep -Seconds 5

    $notify.Dispose()
}
```