PowerShellのメモ
基本文法はこちらの例で説明しています。


コマンドプロンプトから実行する
PowerShellのスクリプトは、セキュリティのため、 エクスプローラーからダブルクリックするだけでは実行されないようになっています。 コマンドラインで実行するために、次のような簡易なバッチファイルを用意して実行しています。

@echo off
powershell -ExecutionPolicy RemoteSigned -File %1

またPC環境によっては、%1はカレントディレクトリがパスに含まれないことがあります。 つまり、カレントディレクトリの「sample.ps1」を実行するには 「./sample.ps1」または「.\sample.ps1」と指定する必要があります。


キー入力を待つコマンドラインループのサンプル

while($true){
    $input = Read-Host "入力>"
    if($input -eq "exit"){
        Write-Host "終了します。"
        exit 0
    } elseif($input -eq "quit" -or $input -eq "QUIT"){
        Write-Host "終了します.."
        break
    } else {
        Write-Host "入力されました:${input}"
    }
}
echo "Good bye."

PowerShellでは真偽値は$true、$falseと書きます。またElse Ifはelseifです。


再帰的ファイル検索のサンプル

$items = (Get-ChildItem D:\usr\work\the\path `
  -Recurse -Force -File -ErrorAction "SilentlyContinue")
foreach ($e in $items){
    $fullName = $e.FullName
    $size     = $e.Length
    $name     = $e.Name
    echo $fullName
}

簡単には上のようになります。本題と関係ありませんが、 2行に分けて記述する場合は、行の末尾にバッククォートを入れます。 (他の言語のように下線やバックスラッシュではないので注意)

$items = (Get-ChildItem D:\usr\work `
  -Recurse -Force -Directory -ErrorAction "SilentlyContinue")
foreach ($e in $items){
    $path = $e.FullName

    # 存在チェックはTest-Pathで行う
    if((Test-Path "${path}.zip") -Or `
       (Test-Path "${path}.tar.gz")){
        echo $path
    }
}

この例では、指定したディレクトリ以下を再帰的に検索し、 同じ位置にディレクトリと、主ファイル名がそれと同じ.zipや.tar.gzが共にある場合、 「ディスクの無駄では?」ということで表示します。


ファイル編集日時の一括変更
起点ディレクトリから再帰的に指定した拡張子(ここでは*.java、*.class)のファイルを検索し、 全ての最終編集日時を指定した値に変更する処理です。 なお、最終編集日時ではなく最終アクセス日時を変更したい場合は、 LastWriteTimeの代わりにLastAccessTimeを指定すればOKです。

Get-ChildItem C:\the\path\of\basedir -Recurse -Include *.java,*.class -ErrorAction SilentlyContinue | ForEach-Object {
    Set-ItemProperty $_.FullName -Name LastWriteTime -Value "2021/11/28 12:00:00"
}


文字列の置き換えと分割

$str1 = "Hello Power Shell"
$str2 = $str1 -replace "Hello","Good Morning"
echo $str2
# Good Morning Power Shell となるはず

$str1 = "Hello,Power,Shell"
$arr = $str1 -split ","
echo $arr
# Hello(改行) Power(改行) Shell(改行)となるはず

$arr  = ("Hello", "Power", "Shell")
$str3 = $arr -join "☆"
echo $str3
# Hello☆Power☆Shell となるはず

PowerShellでは、文字列の置き換えや分割も関数ではなく、演算子のような記法になっています。


Webコンテンツの取得

$res = $null
$errcode = $null
$url = "http://www.nsnhnkmmkk.co.jp/sub/sub2/"
try {
    $res = Invoke-WebRequest -Uri $url -Method GET
} catch {
    $errcode = $_.Exception.Response.StatusCode.Value__
    $errdesc = $_.Exception.Response.StatusDescription
}
Write-Host "----------"
if([string]::IsNullOrEmpty($errcode)){
    Write-Host "Status:" $res.StatusCode
    # ヘッダの内容が表示できる
    #$res.Headers
    $links = $res.ParsedHtml.getElementsByTagName("a")
    foreach($link in $links){
        # 意味はない処理だが、リンク内の文字を出力してみる
        Write-Host "linktext[" $link.innerText "]"
        # リンク先を出力する。こちらはスクレイピングみたいで意味はあるか
        Write-Host "href url[" $link.href "]"
    }
} else {
    Write-Host "Status:"  $errcode
    Write-Host "Message:" $errdesc
}
Write-Host "----------"

Webコンテンツの取得、いわゆるWebスクレイピングのようなことができます。


ディレクトリ全体をZip圧縮するサンプル

$indir = "D:\usr\work\the\path\of\target\dir"
$outZipPath = "C:\work\output.zip"
Compress-Archive -Path $indir -Update -DestinationPath $outZipPath

ファイル名に非ASCII文字(日本語など)を含む場合、 Compress-Archiveは常にUTF-8として圧縮します。 エクスプローラーの「すべて展開...」を使えば正しい名前で展開されますが、 その他のツールで展開すると、ファイル名が化けてしまうことがあります。


ディレクトリの作成とファイルのコピー

$zengetsu = (Get-Date).AddMonths(-1).ToString("yyyyMM")
New-Item ".\${zengetsu}" -ItemType Directory
Copy-Item ".\jisseki.txt" "${zengetsu}\jisseki${zengetsu}.txt"

前月(YYYYMM)の名前のディレクトリを作成し、 現在のディレクトリにある「jisseki.txt」というファイルをその中にコピーするサンプルです。


Excelファイルを処理する
PowerShellの強力な機能の一つが、Excelファイルを自由に読み書きできることです。 Excelのマクロ(VBA)以外にも、Excelを読み書きできるプログラミング言語とフレームワークとしては、 Java(Apache POI)、Python(openpyxl)、PHP(PhpSpreadsheet)などがありますが、 処理系のインストールが必要で、またバージョンアップによって仕様が大きく変わる物が多いです。 Excelファイルを手軽に扱う目的なら、 Windows PCなら最初から入っていて使えるPowerShellには大きな利点があります。

Excelファイルの読み書きの基本形

$excel = New-Object -ComObject Excel.Application
# ↓Excelを画面に出して処理する
$excel.Visible = $true
$wb = $excel.Workbooks.Open("C:\work\input_data.xlsx")
$sheet = $wb.Worksheets.Item(1)

$v = $sheet.Cells.Item(4, 3).Text
Write-Output "値は${v}"

$sheet.Cells.Item(5, 4) = "こんにちは"

# ↓上書き保存時に警告ダイアログを出さない
$excel.DisplayAlerts = $false
# ファイルを名前を付けて保存
$wb.SaveAs("C:\work\output_data.xlsx")
# Excelを終了
$excel.Quit()
$excel = $null
[GC]::Collect()

この例では、入力Excelファイルの4行目3列目(R4C3)のセルの値を表示し、 代わりに5行目4列目(R5C4)セルに「こんにちは」という文字列を設定しています。

セルのフォントを設定する

$sheet.Cells.Item(8,7).Font.Name = "Meiryo UI"

CSV形式で出力する

$wb.SaveAs("C:\work\output_data.csv", 6)

「6」がCSV形式を表します。他の値は、こちらの XlFileFormat 列挙体 (Excel) に記載されています。

セルにリンクを張る

$sheet.Hyperlinks.Add($sheet.Cells.Item(7,6), "\\file_srv\path\abc.docx", "", "", "abc.docx")

引数はリンクを張るセル、リンク先のアドレス(URL)、サブアドレス、 コメント、リンクに表示する文字列です。

セルのリンク先URLを抽出する
Excelの複数のセルに設定したリンクの、リンク先アドレス(URL) を一括して抽出することはExcelの標準操作ではできないそうです。 VBA(マクロ)ならできるそうですが、たったそれだけのことがなぜ標準機能でできないの? と文句を言っても仕方ありません。マクロの代わりにPowerShellで抽出するなら次のようにできます。
この例では、指定したシートに含まれる全てのリンクを順番に検索し、 リンクのあるセルと同じ行の、13列目にリンク先アドレス(URL)を転記しています。

$sheet.Hyperlinks | ForEach-Object {
    $col  = $_.Range.Column
    $row  = $_.Range.Row
    $text = $_.TextToDisplay
    $url  = $_.Address

    # リンクのセルと同じ行の、13列目に抽出したURLを転記する
    $sheet.Cells.Item($row,13) = $url
}


テキストデータの読み取り
CSVファイルからExcelへのインポート
CSVファイルはUTF-8で記述し、1行目は見出し行です。

$rowno = 2
Import-Csv "C:\work\input.csv" | ForEach {
    $sheet.Cells.Item($rowno, 2) = $_.SHITEN_CODE
    $sheet.Cells.Item($rowno, 3) = $_.SHITEN_NAME
    $sheet.Cells.Item($rowno, 4) = $_.AREA_CODE
    $sheet.Cells.Item($rowno, 5) = $_.ADDRESS
    $sheet.Cells.Item($rowno, 6) = $_.EMPLOYEE_NUM
    $rowno++
}

この例では、CSVファイルの1行目が

SHITEN_CODE,SHITEN_NAME,AREA_CODE,ADDRESS,EMPLOYEE_NUM

となっていることを前提にしています。Excelの2行目から開始して、 2列目、3列目…にSHITEN_CODE、SHITEN_NAME…つまりCSVファイルの1列目、2列目… の値を移送しています。

タブ区切り、シフトJIS、ヘッダ行がない場合
上の例の逆をやってみましょう。 今度は、区切り文字がタブ(`t)、文字コードがシフトJIS(Default)のファイルを読んで、 HTMLのTABLE要素(の一部)を生成しています。
この例では、1行目を見出し行ではなくデータ行として扱うため、 代わりに-Headerで各列の見出しを指定しています。 つまり1列目が「0」、2列目が「1」…という列見出しを付けています。
したがってこの例だと、データの列の数は事前に知っておく必要があります。

Import-Csv .\data.txt -Delimiter "`t" -Encoding Default -Header 0,1,2,3,4 | ForEach {
    $s = "<tr>"
    for($i=0; $i -Lt 5; $i++){
        $s += "<td>" + $_.$i + "</td>"
    }
    Write-Host "$s</tr>"
}

-Delimiterに限らず、PowerShellではエスケープシーケンスをバックスラッシュではなく、 バッククォートで表すのが特徴です。 例えば文字列の中で改行文字を表現するには「\n」ではなく「`n」とします。
また連想配列の部分が 「$_.$i」 でないと私の環境では動きませんでしたが、 新しい版だと「$_[$i]」のように指定もできるらしいです。

Misc. Topics Top

(first uploaded 2020/05/06 last updated 2022/08/01, URANO398)