3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PowerShellの痒い所メモ

Last updated at Posted at 2025-01-13

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ではアセンブリのロードに先立ってクラスの構文解析が行われる
このため、外部アセンブリに依存するクラス名などをクラス内で型名に指定すると解決できずエラーとなる。

これを解決するためには

  1. アセンブリのロードとクラスファイルを別々にする (構文解析はファイル単位で行われるため)
  2. 静的(明示的)な型指定はクラスでは行わない (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
3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?