Windows PowerShell

Last-modified: 2016-03-11 (金) 12:15:22

覚え書き。

自作スクリプト

hwiki2chm.ps1

ひとりWikiで出力したHTMLファイルから、HTML Helpのプロジェクトファイル(.hhp, hhc)を出力する。要HTML Help Workshop。

事前準備

  • ひとりWikiの[ファイル‐HTML出力]で必要なファイルをHTMLファイルに出力する
    • 出力先フォルダは任意の空フォルダを指定する。ここでは仮にC:\temp\workとする
    • [index.htmlを出力する]はチェック解除
    • 出力文字コードは[SJIS]
    • ファイル名は[標準]を推奨

プロジェクトファイル出力

PowerShellコンソールから以下のコマンドラインを入力する。

.\hwiki2chm.ps1 出力先フォルダ

先ほどの例では次のようになる。

.\hwiki2chm.ps1 C:\temp\work

出力先フォルダに出来た.hhpファイルをHTML Help Workshopで読み込み、コンパイルする。

  • 作られるプロジェクトファイル(.hhp)、コンテンツファイル(.hhc)、HTML Helpファイル(.chm)の名前は出力先フォルダから取られる。先ほどの例では work.hhp、work.hhc、work.chm になる
  • トピックはHTMLファイルのタイムスタンプ順(新しいHTMLファイルが上)で並ぶ
  • 索引はサポートしていない
  • ひとりWikiが出力したwiki.cssは、そのままだと閲覧しにくいので修正した方が良い

ダウンロード

割とハマったこと

カレントフォルダにあるスクリプト(.ps1)の実行

以下のコマンドラインだとエラーになる場合あり(環境変数PATHの設定次第だが)。

file.ps1

相対または絶対パス指定すると確実に実行できる。

.\file.ps1

環境変数PATHにカレントフォルダを加えれば、そのまま実行できる。

$Env:PATH = '.;' + $Env:PATH
file.ps1

ドライブの変更

ディスクドライブなら

d:

とドライブレターだけで変更できるのだが、環境変数やレジストリなどは

cd Env:
cd HKLM:

と明示的にcd(Set-Location)を使わないと移動できない。

配列に要素を追加

# 空の配列を作る
$arr = @()
# 配列要素として整数3を追加(+=演算子を使う)
$arr += 3

余談だが、要素を削除するには、

$len = $arr.Length - 2
$arr = $arr[0..$len]

のように、部分集合を使って配列を作り直すしかない模様。

引数の参照渡し

関数を呼び出す場合で、引数を参照渡しする時は、 [ref]$x ではなく ([ref]$x) と記述する。

function swap([ref]$a, [ref]$b)
{
    $a.value, $b.value = $b.value, $a.value
}
$x = 10
$y = 20
swap ([ref]$x) ([ref]$y)
$x
$y

引数を伴うアプリケーション実行

エイリアスが意外に使える。

$file = 'foo.txt'
Set-Alias editor 'C:\Program Files\Hidemaru\Hidemaru.exe'
editor /j50 $file

Invoke-Expressionでも引数を伴うアプリケーション実行が可能だが、フルパスに空白が含まれている場合は、(バッククォートではなく)シングルクォーテーションでエスケープする(にしても&ならOKでInvoke-ExpressionだとNGなのは何故だろう?)。

# OK(パスが通っている)
Invoke-Expression 'notepad foo.txt'
Invoke-Expression 'javac foo.java'
# OK(パスが通っておらず、パスに空白が含まれていない)
Invoke-Expression 'C:\BATCH\foo.bat foo.txt'
# OK(パスが通っておらず、パスに空白が含まれている)
Invoke-Expression "C:\Program' Files\Hidemaru\Hidemaru.exe' foo.txt"
& "C:\Program Files\Hidemaru\Hidemaru.exe" foo.txt
C:\Program' Files\Hidemaru\Hidemaru.exe' foo.txt
C:\Program" Files\Hidemaru\Hidemaru.exe" foo.txt

以下だとNG。

# NG(パスが通っておらず、パスに空白が含まれている)
Invoke-Expression "C:\Program Files\Hidemaru\Hidemaru.exe" foo.txt
Invoke-Expression 'C:\Program Files\Hidemaru\Hidemaru.exe foo.txt'
Invoke-Expression '"C:\Program Files\Hidemaru\Hidemaru.exe" foo.txt'
Invoke-Expression "`"C:\Program Files\Hidemaru\Hidemaru.exe`" foo.txt"
  :

日付

英語表記を得るには以下のようにする。

$culture = New-Object System.Globalization.CultureInfo('en-US')
$today = Get-Date
$today.ToString('D', $culture)
$today.ToString('dd MMM yyyy', $culture)

(日本の)元号表記を得るには以下のようにする。

$culture = New-Object System.Globalization.CultureInfo('ja-JP')
$calendar = New-Object System.Globalization.JapaneseCalendar
$culture.DateTimeFormat.Calendar = $calendar
$today = Get-Date
$today.ToString('D', $culture)
$today.ToString('ggyy年MM月dd日', $culture)

desktop.iniとThumbs.dbの削除

確認

Get-ChildItem -force -recurse | Where-Object { $_.Name -eq 'desktop.ini' -or $_.Name -eq 'Thumbs.db' }

削除

Get-ChildItem -force -recurse | Where-Object { $_.Name -eq 'desktop.ini' -or $_.Name -eq 'Thumbs.db' } | Remove-Item -force

検索・置換

# 検索
Get-Content foo.txt | Where-Object { $_ -match "bar" }
# 置換
Get-Content foo.txt | ForEach-Object { $_ -replace "bar", "goo" }
# 以下でも同じ(エイリアスを使用)
type foo.txt | ? { $_ -match "bar" }
type foo.txt | % { $_ -replace "bar", "goo" }

プロンプトを一時的に変更→復帰

$p = $function:prompt
$function:prompt = { "$PWD> " }
# (中略)
$function:prompt = $p

スクリプトファイル名を獲得

# このスクリプトファイル名のみ
$MyInvocation.MyCommand.Name
# このスクリプトファイルのフルパス(結果は同じ)
$MyInvocation.MyCommand.Path
$MyInvocation.MyCommand.Definition
# このスクリプトファイルがあるフォルダ
Split-Path $MyInvocation.MyCommand.Path -parent

設定ファイルの読み込み

変数などを別ファイルに追い出せる。

# config.txtの例
$folder = 'c:\tmp'
# スクリプト本体
Get-Content .\config.txt | Invoke-Expression
# 変数$folderにc:\tmpが代入されている

スクリプト二重起動の防止

Mutexを使う*1

$mt = New-Object System.Threading.Mutex($false, "MutexName")
if (-not $mt.WaitOne(0, $false)) {
    Write-Warning "スクリプトが二重起動されたため終了します"
    exit
}
# 本来の処理
$mt.ReleaseMutex()
$mt.Close()
trap [System.Threading.AbandonedMutexException] {
    Write-Warning "前回、エラー終了/強制終了した形跡があります"
    continue
}

ロックファイルを使う

確実ではないと思うが昔懐かしのロックファイルを使う方法。ロックファイルにプロセスIDを書き込み、その値と比較する(実行中のスクリプトファイル名も一緒に書き込むと良いかも知れない)。

$lockfile = '.\___lockfile.txt'
# ロックファイルがあったら…
if (Test-Path $lockfile -pathType Leaf) {
    # ロックファイルの内容(PID)を獲得
    $l = Get-Content $lockfile | Invoke-Expression
    # PIDがpowershellなら終了。そうでなければ以前、何らかの理由で中断されたものと見なす
    $p = Get-Process | Where-Object {$_.Id -eq $l}
    if ($p -ne $null -and $p.Name -imatch "powershell") {
        Write-Output "ロックファイルがあるため処理を終了します..."
        exit
    } else {
        Write-Warning "前回、強制終了された形跡があります。処理は続行します"
    }
}
# プロセスIDをロックファイルに出力する
$PID | Out-File $lockfile -encoding OEM
# 以下、本来の処理

FTP

$username = "********"
$password = "********"
$base_uri = "ftp://*****************/"
$base_dir = $PWD
$dl_file = "foo.html"
$up_file = "bar.html"
$dl_filepath = Join-Path $base_dir $dl_file
$up_filepath = Join-Path $base_dir $up_file
# ファイルのダウンロード
$wc = New-Object System.Net.WebClient
$nc = New-Object System.Net.NetworkCredential($username, $password)
$wc.Credentials = $nc
$wc.DownloadFile($base_uri+$dl_file, $dl_filepath)
Copy-Item $dl_filepath $up_filepath
# ファイルのアップロード
$wc.UploadFile($base_uri+$up_file, $up_filepath)
function ftp_request($cred, $uri, $method)
{
  $ec = [System.Text.Encoding]::GetEncoding("Shift_JIS")
  $wr = [System.Net.WebRequest]::Create($uri)
  $wr.Credentials = $cred
  $wr.Method = $method
  $res = $wr.GetResponse()  # System.Net.WebResponseオブジェクト
  $sr = New-Object System.IO.StreamReader($res.GetResponseStream(), $ec)
  $result = $sr.ReadToEnd()
  $sr.Close()
  $result
}
# ファイルの削除
$cm = [System.Net.WebRequestMethods+Ftp]::DeleteFile  # "+Ftp"となっていることに注意
ftp_request $nc ($base_uri+$up_file) $cm
# ファイル一覧
$cm = [System.Net.WebRequestMethods+Ftp]::ListDirectory  # "+Ftp"となっていることに注意
ftp_request $nc $base_uri $cm
# 子クラスのフィールドを関数に直接渡す方法が良く判らない…(参照渡しでもダメ)

WMI

# Win32_Productクラスを使う
$pr = Get-WmiObject Win32_Product
# インストールされている全アプリケーション(コンポーネント)名をソートして表示
$pr | Sort-Object Name | ForEach-Object { $_.Name }
# インストールされている.NET Frameworkを表示
$pr | Where-Object { $_.Name -match 'NET Framework' } | ForEach-Object { $_.Name }
# Microsoftの全アプリケーション(コンポーネント)名をソートして表示
$pr | Where-Object { $_.Vendor -match 'Microsoft' } | Sort-Object Name | ForEach-Object { $_.Name }
# MS-Officeのインストール先フォルダを表示
$pr | Where-Object { $_.Name -match 'Microsoft Office' } | ForEach-Object { $_.InstallLocation }

Win32_ProductはVista/2003では標準でインストールされていない模様。

GUIDの獲得

$g = [Guid]::NewGuid()
# そのまま出力→0e51d205-184e-4a84-bfa3-f0e51e991012
$g.ToString()
# ブレースで囲む&大文字化して出力→{0E51D205-184E-4A84-BFA3-F0E51E991012}
"{{{0}}}" -f $g.ToString().ToUpper()

以下のように書くと末尾にゴミ文字列(?)がつく。

$t = New-Object -ComObject Scriptlet.TypeLib
# {4467D015-E912-4A21-A871-28C4E124EEEF} a などと出力される(a?)
$t.GUID

ヘルプ

カテゴリーごとに項目名と概要を表示。

Get-Help -category 'Alias' | Select-Object Name, Synopsis
Get-Help -category 'Cmdlet' | Select-Object Name, Synopsis
Get-Help -category 'Provider' | Select-Object Name, Synopsis
Get-Help -category 'HelpFile' | Select-Object Name, Synopsis

ブックマーク

Windows PowerShell関連

IDE、ツールなど

その他


*1 実行中のスクリプトをCtrl-Cで中断→再度実行すると、まれに例外が発生しないことがある。試しにtrapステートメントを削除して同じ事を試すと、必ず例外が発生するのだが、こういうモノなんだろうか…。