PowerShell初心者が忘れやすそうな独特のお作法などをメモしたもの。
Windowsで運用者向けにスクリプトを書くとなったらPowerShellしかないことに気づき勉強。
運用者向けであるということで、必然的にPowerShell 5.1を前提としています。(Win10のプリインストールされたバージョンと同じ)
目次
OSとバージョンに関して
Windows10にはPowerShell 5.1がプリインストールされており、インストールすればv7.xが使える。(2025/01現在)
そのため他人に配るスクリプトはv5.1を前提にした方がいいが、三項演算子が使えない等機能面で不便もある。
とりわけChatGPT等のAIはこのへんの区別ができておらず、コードを書かせたときに一番多いミスが三項演算子である。
開発環境
デフォルトでPowerShell ISEがインストールされているが更新が止まっている。
VS CodeにはMS純正のPowerShell用拡張機能が提供されているため、どちらかを使う。
VSCodeでデバッグするときの注意点
VSCodeでデバッグするときは実行毎にターミナルを変えないと古いコードのまま実行されることがある。
だから毎回ターミナルを削除&再起動するとより安全。
逆に、ターミナルが以前に読み込んだアセンブリ等を覚えていると更新後のスクリプト単体では実行できないコードが実行できてしまったりもするので注意が必要。
関数の読み込み順序
実行する関数は事前に定義されている必要があり、定義が重複したときは上書きされる。
func # error
function func(){
"1"
}
func # 1
function func(){
"2"
}
func # 2
クラス
クラスの読み込み順序
クラスは関数と異なり多重定義はできない。
class Test {
}
class Test { # error
}
クラスのひな型
class Test {
# private等はサポートされていない
[string]$Property
static [string]$StaticProperty = "static prop value"
Test() {
$this.Property = "default value"
}
Test([string]$arg) { # オーバーロードが可能
$this.Property = $arg
}
[void] ShowProperty() {
Write-Host $this.Property # thisは省略不可
}
static [void] StaticMethod() {
Write-Host ([Test]::StaticProperty) # 静的プロパティは()で括らないと展開されない
}
}
# インスタンス生成とメソッド呼び出し
[Test]::new().ShowProperty() # default value
[Test]::new(123).ShowProperty() # 123
[Test]::new($false).ShowProperty() # False
[Test]::new($null).ShowProperty() # 空
# New-Object Test で生成する方法もあり、そっちが推奨らしい?
# 静的メソッド
[Test]::StaticMethod() # static prop value
同一スクリプト内で外部アセンブリを使用した際の注意点
PowerShellではアセンブリのロードに先立ってクラスの構文解析が行われる。
このため、外部アセンブリに依存するクラス名などをクラス内で型名に指定すると解決できずエラーとなる。
これを解決するためには
- アセンブリのロードとクラスファイルを別々にする (構文解析はファイル単位で行われるため)
- 静的(明示的)な型指定はクラスでは行わない (Newするときにも文字列でクラスを指定しないといけないので気持ち悪いが…)
などの方法がある。
エラー例を以下に示す。
test.ps1
# 1
Add-Type -AssemblyName "System.Windows.Forms"
# 2 ※1より先にここの構文解析が行われる
class MyForm {
[void] ShowForm() {
# 1が読み込まれていないため、[System.Windows.Forms.Form]がこの時点では存在せずエラーとなる
$form = [System.Windows.Forms.Form]::new()
$form.ShowDialog() | Out-Null
}
}
$myForm = [MyForm]::new()
$myForm.ShowForm()
実行結果
PS C:\Users\a> . 'R:\test.ps1'
発生場所 R:\test.ps1:8 文字:18
+ $form = [System.Windows.Forms.Form]::new()
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
型 [System.Windows.Forms.Form] が見つかりません。
+ CategoryInfo : ParserError: (:) [], ParseException
+ FullyQualifiedErrorId : TypeNotFound
解決策として以下のように動的な手段で型指定を行う。
test.ps1
# 1
Add-Type -AssemblyName "System.Windows.Forms"
# 2 ※1より先にここの構文解析が行われる
class MyForm {
[void] ShowForm() {
# 型名を文字列で指定しているため実行時に型の有無が判定される
$FormType = 'System.Windows.Forms.Form' -as [type]
$form = $FormType::new()
$form.ShowDialog() | Out-Null
}
}
$myForm = [MyForm]::new()
$myForm.ShowForm()
インポート方法が多い
TL;DR: ライブラリ開発者とかでなければ制約のないドットソースで全部間に合う。
ドットソース
ドットソースではあたかもそのファイル内容がそこに書かれているかのように実行される。
関数もクラスも読み込むことができる。カレントディレクトリは呼び出し元のものになる。
main.ps1
Write-Host "main.ps1 Get-Location:" (Get-Location) # main.ps1の実行元のカレントディレクトリ
Write-Host "main.ps1 PSScriptRoot: $PSScriptRoot" # main.ps1が格納されているディレクトリ
. "$PSScriptRoot\class.ps1"
#. "$PSScriptRoot\class.psm1" # .psm1はドットソースで読み込めない
$test = [Test]::new() # Test (.psm1では "型 [Test] が見つかりません。" となる)
$test.Method() # Method
class.ps1
class Test {
Test() {
Write-Host "Test"
}
Method() {
Write-Host "Method"
}
}
Write-Host "class.ps1 Get-Location:" (Get-Location)
Write-Host "class.ps1 PSScriptRoot: $PSScriptRoot"
実行結果
PS C:\Users\a> C:\Users\a\main.ps1
main.ps1 Get-Location: C:\Users\a
main.ps1 PSScriptRoot: C:\Users\a\scripts
class.ps1 Get-Location: C:\Users\a
class.ps1 PSScriptRoot: C:\Users\a\scripts
Test
Method
Import-Module
モジュールによって定義されているモジュール関数、エイリアス、変数のみをインポートする。
.ps1を読み込む
main.ps1
Import-Module "$PSScriptRoot\class.ps1"
$test = [Test]::new() # Test
$test.Method() # Method
結果は同様で、.ps1を読み込んだ場合、ドットソースと変わらない動きに見える。ただし以下の注意書きがある。
スクリプト (.ps1) ファイルをモジュールとしてインポートすることは可能ですが、スクリプト ファイルは通常、スクリプト モジュール ファイル (.psm1) ファイルのように構造化されていません。 スクリプト ファイルをインポートしても、モジュールとして使用できる保証はありません。 詳細については、「 about_Modules」を参照してください。
.psm1を読み込む
ファイル単位で読み込む
クラスは対応しておらず、関数が読み込める。使う関数についてはExport-ModuleMemberで個別に指定ができる。
何も指定しない場合は全ての関数が実行できる。
class.psm1
class Test {
Test() {
Write-Host "Test"
}
Method() {
Write-Host "Method"
}
}
function Test2() {
Write-Host "Test2"
}
function Test3() {
Write-Host "Test3"
}
Export-ModuleMember -Function Test2 # Test3はインポートしない
main.ps1
Import-Module "$PSScriptRoot\module\class.psm1"
$test = [Test]::new() # エラー
$test.Method() # エラー
Test2 # Test2
Test3 # エラー
実行結果
PS C:\Users\a> . 'C:\Users\a\main.ps1'
型 [Test] が見つかりません。
発生場所 C:\Users\a\main.ps1:5 文字:9
+ $test = [Test]::new()
+ ~~~~~~
+ CategoryInfo : InvalidOperation: (Test:TypeName) []、RuntimeException
+ FullyQualifiedErrorId : TypeNotFound
null 値の式ではメソッドを呼び出せません。
発生場所 C:\Users\a\main.ps1:6 文字:1
+ $test.Method()
+ ~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) []、RuntimeException
+ FullyQualifiedErrorId : InvokeMethodOnNull
Test2
Test3 : 用語 'Test3' は、コマンドレット、関数、スクリプト ファイル、または操作可能なプログラムの名前として認識されません。名前が正しく記述されていることを確認し、パスが含まれている場合はそのパスが正しいことを確認してから、再試行し
てください。
発生場所 C:\Users\a\main.ps1:9 文字:1
+ Test3
+ ~~~~~
+ CategoryInfo : ObjectNotFound: (Test3:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
ディレクトリ単位で読み込む
ディレクトリを指定することで中のモジュールが使えるようになるそうだが、失敗した。何か足りないのか。
class.psm1
class Test {
Test() {
Write-Host "Test"
}
Method() {
Write-Host "Method"
}
}
function Test2() {
Write-Host "Test2"
}
function Test3() {
Write-Host "Test3"
}
Export-ModuleMember -Function Test2 # Test3はインポートしない
main.ps1
Import-Module "$PSScriptRoot\module" -Verbose
$test = [Test]::new() # エラー
$test.Method() # エラー
Test2 # エラー
Test3 # エラー
実行結果
PS C:\Users\dire> . 'C:\Users\a\main.ps1'
Import-Module : モジュール ディレクトリに有効なモジュール ファイルが見つからなかったため、指定されたモジュール 'C:\Users\a\module' は読み込まれませんでした。
発生場所 C:\Users\a\main.ps1:4 文字:1
+ Import-Module "$PSScriptRoot\module" -Verbose
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<略>
using module
モジュールをインポートしクラス定義も読み込み可能。
ファイルの指定に変数が使えないことと、ファイルの先頭に記述する必要がある必要がある。
.ps1を読み込む
結論から言うと.ps1から関数やクラスを読み込むことはできない。
class.ps1
class Test {
Test() {
Write-Host "Test"
}
Method() {
Write-Host "Method"
}
}
function Test2() {
Write-Host "Test2"
}
function Test3() {
Write-Host "Test3"
}
main.ps1
using module ".\class.ps1" # 変数を指定することはできない
$test = [Test]::new()
$test.Method()
Test2
Test3
実行結果
PS C:\Users\a> . 'C:\Users\a\main.ps1'
型 [Test] が見つかりません。
発生場所 C:\Users\a\main.ps1:5 文字:9
+ class Test {
+ ~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (class Test {
...thod"
}
}:TypeDefinitionAst) []、RuntimeException
+ FullyQualifiedErrorId : TypeNotFound
+
ちなみにclass.ps1にExport-ModuleMemberを含めると以下のエラーが出た。.psmと.ps1はただの拡張子かと思いきや、明確にバリデーションに関係してくるようだ。
. : Export-ModuleMember コマンドレットは、モジュール内からしか呼び出せません。
.psm1を読み込む
class.psm1
class Test {
Test() {
Write-Host "Test"
}
Method() {
Write-Host "Method"
}
}
function Test2() {
Write-Host "Test2"
}
function Test3() {
Write-Host "Test3"
}
Export-ModuleMember -Function Test2 # Test3はインポートしない
main.ps1
using module ".\module\class.psm1" # 変数を指定することはできない
$test = [Test]::new() # Test
$test.Method() # Method
Test2 # Test2
Test3 # エラー
実行結果
PS C:\Users\a> . 'C:\Users\a\main.ps1'
Test
Method
Test2
Test3 : 用語 'Test3' は、コマンドレット、関数、スクリプト ファイル、または操作可能なプログラムの名前として認識されません。名前が正しく記述されていることを確認し、パスが含まれている場合はそのパスが正しいことを確認してから、再試行し
てください。
#Requires
インポートというより特定のモジュールが入っていることをアサーションするためのものらしい?
未検証
スクリプトブロック
jsのように関数を変数に代入する場合、PowerShellでは関数名を文字列型変数に代入してそこから呼び出すか、スクリプトブロックというものを変数に代入しそれを呼び出す方式を取る。
スクリプトブロックとは、処理の一連を変数に代入したり関数に渡したりできるラムダ式のようなもの。
PowerShellはレキシカルスコープでなくダイナミックスコープを採用している都合上、jsとは異なりスクリプトブロックが呼ばれたときの変数の値が異なる。
たとえばthisを親ブロックからthis_等のローカル変数に保存してそれを読み出すようなことはデフォルトではしない。
これを解決するには以下のようにGetNewClosureを使ってブロックを代入する。
$this_ = $this
$f = {
$this_.someProperty
}.GetNewClosure()
外部プロセス実行
- Start-Processに-WindowStyleと-NoNewWindowを同時に指定することはできない
式と演算子
フォーマットを使うときはリテラルを()で囲む
Write-Host ("誕生日は {0}月 {1}日" -f 4, 1)
大文字小文字が区別されない
PS C:\Users\a> "aa" -eq "aA"
True
PS C:\Users\a> "aa" -match "aA"
True
ほか
- -not と ! は同じ (細かい優先順位は異なるかもしれない)
配列の種類
- 構文上の配列と、C#等で使われるリッチな配列とでは種類が違う
- ArrayListは固定長配列であり、追加もできるがコストが掛かる
[int32[]]$array = 1, 2, 3
$array2 = @(4, 5, 6)
$windowHandles = New-Object System.Collections.ArrayList
$array.Gettype()
$array2.Gettype()
$windowHandles.Gettype()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32[] System.Array
True True Object[] System.Array
True True ArrayList System.Object
カレントディレクトリ
ps1内のカレントディレクトリは、呼び出し元のcdに依るようだ。
以下であればaの下の aaa.png を相対パスで処理することができる。
cd C:\Users\a
powershell -File main.ps1 ./aaa.png
例外
finnaly句はreturnの直後にも実行される
function Test-ReturnFinally {
try {
"In try block"
return "Returning from try block"
} finally {
"In finally block"
}
}
Test-ReturnFinally
# In try block
# Returning from try block
# In finally block