PowerShellの痒い所メモ

PowerShell初心者が忘れやすそうな独特のお作法などをメモしたもの。
Windowsで運用者向けにスクリプトを書くとなったらPowerShellしかないことに気づき勉強。
運用者向けであるということで、必然的にPowerShell 5.1を前提としています。(Win10のプリインストールされたバージョンと同じ)

## 目次 <!-- omit in toc -->

<!--
  - 目次は Ctrl+Shift+P -> "mark table" など入力すると自動生成される (VSCode)
  - 目次は保存時に自動更新される
  - 見出しの後ろに omit in toc とコメントを入れると目次から除外される
-->

- [OSとバージョン](#osとバージョン)
- [開発環境](#開発環境)
  - [VSCodeでデバッグするときの注意点](#vscodeでデバッグするときの注意点)
- [関数の読み込み順序](#関数の読み込み順序)
- [クラス](#クラス)
  - [クラスの読み込み順序](#クラスの読み込み順序)
  - [クラスのひな型](#クラスのひな型)
  - [同一スクリプト内で外部アセンブリを使用した際の注意点](#同一スクリプト内で外部アセンブリを使用した際の注意点)
  - [インポート方法が多い](#インポート方法が多い)
    - [ドットソース](#ドットソース)
    - [Import-Module](#import-module)
      - [.ps1を読み込む](#ps1を読み込む)
      - [.psm1を読み込む](#psm1を読み込む)
        - [ファイル単位で読み込む](#ファイル単位で読み込む)
        - [ディレクトリ単位で読み込む](#ディレクトリ単位で読み込む)
    - [using module](#using-module)
      - [.ps1を読み込む](#ps1を読み込む-1)
      - [.psm1を読み込む](#psm1を読み込む-1)
    - [#Requires](#requires)
- [スクリプトブロック](#スクリプトブロック)
- [外部プロセス実行](#外部プロセス実行)
- [式と演算子](#式と演算子)
  - [フォーマットを使うときはリテラルを()で囲む](#フォーマットを使うときはリテラルをで囲む)
  - [大文字小文字が区別されない](#大文字小文字が区別されない)
  - [ほか](#ほか)
- [配列の種類](#配列の種類)
- [カレントディレクトリ](#カレントディレクトリ)
- [例外](#例外)
  - [finnaly句はreturnの直後にも実行される](#finnaly句はreturnの直後にも実行される)
  - [スコープ全体のエラーをキャッチ](#スコープ全体のエラーをキャッチ)

## OSとバージョンに関して
Windows10にはPowerShell 5.1がプリインストールされており、インストールすればv7.xが使える。(2025/01現在)  
そのため他人に配るスクリプトはv5.1を前提にした方がいいが、三項演算子が使えない等機能面で不便もある。  
とりわけChatGPT等のAIはこのへんの区別ができておらず、コードを書かせたときに一番多いミスが三項演算子である。

## 開発環境
デフォルトでPowerShell ISEがインストールされているが更新が止まっている。  
VS CodeにはMS純正のPowerShell用拡張機能が提供されているため、どちらかを使う。

### VSCodeでデバッグするときの注意点
VSCodeでデバッグするときは実行毎にターミナルを変えないと古いコードのまま実行されることがある。  
だから毎回ターミナルを削除&再起動するとより安全。  
逆に、ターミナルが以前に読み込んだアセンブリ等を覚えていると更新後のスクリプト単体では実行できないコードが実行できてしまったりもするので注意が必要。

## 関数の読み込み順序
実行する関数は事前に定義されている必要があり、定義が重複したときは上書きされる。
```powershell
func  # error

function func(){
    "1"
}

func  # 1

function func(){
    "2"
}

func  # 2
```

## クラス

### クラスの読み込み順序
クラスは関数と異なり多重定義はできない。
```powershell
class Test {
}

class Test {  # error
}
```

### クラスのひな型
```powershell
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. アセンブリのロードとクラスファイルを別々にする (構文解析はファイル単位で行われるため)
1. 静的(明示的)な型指定はクラスでは行わない (Newするときにも文字列でクラスを指定しないといけないので気持ち悪いが…)

などの方法がある。

エラー例を以下に示す。

test.ps1
```powershell
# 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()
```

実行結果
```powershell
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
```powershell
# 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
```powershell
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
```powershell
class Test {
    Test() {
        Write-Host "Test"
    }

    Method() {
        Write-Host "Method"
    }
}

Write-Host "class.ps1 Get-Location:" (Get-Location)
Write-Host "class.ps1 PSScriptRoot: $PSScriptRoot"
```

実行結果
```powershell
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
```powershell
Import-Module "$PSScriptRoot\class.ps1"

$test = [Test]::new()   # Test
$test.Method()          # Method
```

結果は同様で、.ps1を読み込んだ場合、ドットソースと変わらない動きに見える。ただし以下の注意書きがある。

> スクリプト (.ps1) ファイルをモジュールとしてインポートすることは可能ですが、スクリプト ファイルは通常、スクリプト モジュール ファイル (.psm1) ファイルのように構造化されていません。 スクリプト ファイルをインポートしても、モジュールとして使用できる保証はありません。 詳細については、「 about_Modules」を参照してください。

##### .psm1を読み込む

###### ファイル単位で読み込む
クラスは対応しておらず、関数が読み込める。使う関数についてはExport-ModuleMemberで個別に指定ができる。  
何も指定しない場合は全ての関数が実行できる。

class.psm1
```powershell
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
```powershell
Import-Module "$PSScriptRoot\module\class.psm1"

$test = [Test]::new()   # エラー
$test.Method()          # エラー

Test2   # Test2
Test3   # エラー
```

実行結果
```powershell
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
```powershell
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
```powershell
Import-Module "$PSScriptRoot\module" -Verbose

$test = [Test]::new()   # エラー
$test.Method()          # エラー

Test2   # エラー
Test3   # エラー
```

実行結果
```powershell
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
```powershell
class Test {
    Test() {
        Write-Host "Test"
    }

    Method() {
        Write-Host "Method"
    }
}

function Test2() {
    Write-Host "Test2"
}

function Test3() {
    Write-Host "Test3"
}

```

main.ps1
```powershell
using module ".\class.ps1"    # 変数を指定することはできない

$test = [Test]::new()
$test.Method()

Test2
Test3
```

実行結果
```powershell
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
```powershell
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
```powershell
using module ".\module\class.psm1"  # 変数を指定することはできない

$test = [Test]::new()   # Test
$test.Method()          # Method

Test2   # Test2
Test3   # エラー
```

実行結果
```powershell
PS C:\Users\a> . 'C:\Users\a\main.ps1'
Test
Method
Test2 
Test3 : 用語 'Test3' は、コマンドレット、関数、スクリプト ファイル、または操作可能なプログラムの名前として認識されません。名前が正しく記述されていることを確認し、パスが含まれている場合はそのパスが正しいことを確認してから、再試行し
てください。
```

#### #Requires
インポートというより特定のモジュールが入っていることをアサーションするためのものらしい？  
未検証

## スクリプトブロック
jsのように関数を変数に代入する場合、PowerShellでは関数名を文字列型変数に代入してそこから呼び出すか、スクリプトブロックというものを変数に代入しそれを呼び出す方式を取る。  

スクリプトブロックとは、処理の一連を変数に代入したり関数に渡したりできるラムダ式のようなもの。

PowerShellはレキシカルスコープでなくダイナミックスコープを採用している都合上、jsとは異なりスクリプトブロックが呼ばれたときの変数の値が異なる。  
たとえばthisを親ブロックからthis_等のローカル変数に保存してそれを読み出すようなことはデフォルトではしない。

これを解決するには以下のようにGetNewClosureを使ってブロックを代入する。

```powershell
$this_ = $this
$f = {
    $this_.someProperty
}.GetNewClosure()
```

## 外部プロセス実行
- Start-Processに-WindowStyleと-NoNewWindowを同時に指定することはできない

## 式と演算子

### フォーマットを使うときはリテラルを()で囲む
```powershell
Write-Host ("誕生日は {0}月 {1}日" -f 4, 1)
```

### 大文字小文字が区別されない
```powershell
PS C:\Users\a> "aa" -eq "aA"
True
PS C:\Users\a> "aa" -match "aA"
True
```

### ほか
- **-not** と **!** は同じ (細かい優先順位は異なるかもしれない)


## 配列の種類
- 構文上の配列と、C#等で使われるリッチな配列とでは種類が違う
- ArrayListは固定長配列であり、追加もできるがコストが掛かる

```powershell
[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* を相対パスで処理することができる。

```powershell
cd C:\Users\a
powershell -File main.ps1 ./aaa.png
```

## 例外

### finnaly句はreturnの直後にも実行される
```powershell
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
```