PowerShell のお勉強

by

in

 コマンドプロンプトで出来ないことをするために、PowerShell のお世話になることがある。Windows の標準機能なのでユーザーを選ばないのが利点。
 ただ、こういったニーズが頻繁にある訳でないのと、結構「クセ強」な言語なので、しばらく使わないとすっかり忘れてしまう。
 という訳で備忘のため、Excel のマクロのインポート、エキスポートを行うスクリプトを書いてみた。こうすると、Git でマクロのバージョン管理が出来るようになる。

 先ずはエキスポート。こちらは比較的楽に完成。

function Usage() {
    Write-Host "`n Export VBComponents`n"
    Write-Host "  Usage : export (-h | foo.xlsm)`n"
    exit 0
}

if ($($Args.Count) -eq 1) {
    $arg = $Args[0]
    if ($arg -eq "-h") { Usage } 
    else {
        if ($arg -notlike "*.xlsm") {
            Write-Host "`n ERROR : invalid file type"
            exit 1
        }
        if ((Test-Path -Path $arg)) {
            $workbookPath = Resolve-Path -Path $arg
        } else {
            Write-Host "`n ERROR : $arg not found"
            exit 1

        }
    }
} else { Usage }

$excel = New-Object -ComObject Excel.Application
$excel.Visible = $false
$excel.DisplayAlerts = $false
$excel.AutomationSecurity = 1 # msoAutomationSecurityLow

$workbook = $excel.Workbooks.Open($workbookPath)

foreach ($VBComponent in $workbook.VBProject.VBComponents) {
    switch ($VBComponent.Type) {
        1 {$ext = ".bas"}
        2 {$ext = ".cls"}
        3 {$ext = ".frm"}
        default {$ext = ".cls"}
    }
    $VBComponent.Export($PWD.Path + '\' + $VBComponent.Name + $ext)
}

$workbook.Close($false)

$excel.Quit()

[System.Runtime.InteropServices.Marshal]::ReleaseComObject($workbook) | Out-Null
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($excel) | Out-Null
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()

 次いで、インポート。Excel のエキスポートの仕様として、ワークシート(Thisworkbook を含む)の VBA は拡張子に「.cls」を持つファイルに保存される。これは「クラス」に属するファイルとバッティングする。しかも、これをインポートする時は「クラス」のメタデータを除いて取り込まなければならない。
 この辺を考慮して書いたコードが以下。ちょっと苦労。

function Usage() {
    Write-Host "`n Import VBComponents`n"
    Write-Host "  Usage : import (-h | foo.xlsm)`n"
    exit 0
}

function is_include($test, $items) {
    foreach ($item in $items) {
        if ($item -eq $test) {
            return $True
        }
    }
    return $False
}

function Import($item_name, $codeFilePath, $is_sheet) {

    try {
        if ($is_sheet) {
            $lines = Get-Content -Path $codeFilePath
            $match_1st = $false
            $codeLines = @()
            foreach ($line in $lines) {
                $match = $line.Trim() -like "Attribute*"
                if ((-not $match_1st) -and $match) {
                    $match_1st = $True
                }
                if ((-not $match_1st) -or  $match) {
                    continue
                }
                $codeLines += $line
            }
            $codeContent = $codeLines | Out-String
            $item = $workbook.VBProject.VBComponents.Item($item_name)
            $code = $item.CodeModule
            $code.DeleteLines(1, $code.CountOfLines)
            $code.AddFromString($codeContent)
        } else {
            $components = $workbook.VBProject.VBComponents
            $item = $components.Item($item_name)
            $components.Remove($item)
            $components.Import($codeFilePath)
        }
        return $True
    }
    catch {
        return $False
    }
}

if ($($Args.Count) -eq 1) {
    $arg = $Args[0]
    if ($arg -eq "-h") { Usage } 
    else {
        if ($arg -notlike "*.xlsm") {
            Write-Host "`n ERROR : invalid file type"
            exit 1
        }
        if ((Test-Path -Path $arg)) {
            $workbookPath = Resolve-Path -Path $arg
            $exist_wbk = $True
        } else {
            $currentPath = Get-Location
            $workbookPath = Join-Path -Path $currentPath -ChildPath $arg
            $exist_wbk = $False
        }
    }
} else { Usage }

$excel = New-Object -ComObject Excel.Application
$excel.Visible = $false
$excel.DisplayAlerts = $false
$excel.AutomationSecurity = 1 # msoAutomationSecurityLow
if ($exist_wbk) {
    $workbook = $excel.Workbooks.Open($workbookPath)
} else {
    $workbook = $excel.Workbooks.Add()
}

$sheets = @()
foreach ($sheet in $workbook.Sheets) {
    $sheets += $sheet.CodeName
}
$sheets += "Thisworkbook"

foreach ($file in (Get-ChildItem -Path ".\*" `
                                 -Include *.cls, *.bas, *.frm)) {
    $module_name = $file.BaseName
    if ($file.Extension -eq ".cls") {
        $include = is_include -test $module_name -items $sheets
    } else {
        $include = $False
    }
    $success = Import -item_name $module_name `
                      -codeFilePath $file.FullName `
                      -is_sheet $include
    if (-not $success) {
        Write-Host "fail to import $module_name"
    }
}

if ($exist_wbk) {
    $workbook.Save()
} else {
    $fileFormat = 52 # 52 : xlOpenXMLWorkbookMacroEnabled
    $workbook.SaveAs($workbookPath, $fileFormat)
}

$workbook.Close($false)

$excel.Quit()

[System.Runtime.InteropServices.Marshal]::ReleaseComObject($workbook) | Out-Null
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($excel) | Out-Null
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()

 それぞれ、PowerShell を開くのが面倒なので、バッチファイルから起動できるようにしておく。

@echo off
powershell %~dp0export_vba.ps1 %*
@echo off
powershell %~dp0import_vba.ps1 %*

 途中、AI の助けも借りたが、当方の聞き方が悪いのか、先方の知識ベースが少ないのか、微妙な所ではあまりアテにならなかった。