PowerShell

powershell チートシート

自分用のチートシート

たまにしか使わないので少しずつ記述を増やしていく

動作確認はバージョン2および4で行っている。バージョン3は環境がないため未確認である

(自宅のPCをWindows10にアップグレードしたので、今後バージョン5も確認予定。逆に今後はバージョン4の確認はできない)


コマンド名

powershell


実行


バージョン確認方法

powershell -Command "$Host"

powershell -command "$PSVersionTable"


コマンドライン実行

powershell -Command "式"

powershell -c "式"


スクリプトファイル実行

powershell .\hoge.ps1

# スクリプトを実行するには、事前に管理者にてSet-ExecutionPolicyの設定が必要
# 後述のHello, World!の例を参照


インタラクティブ実行

powershell


統合環境(powershell Integrated Scripting Environment (ISE))

powershell_ise

ps1ファイルを右クリックから「編集」でもISEが起動する


ヘルプ

Get-Help コマンド名など


コメント


  • 行コメント: # から改行

  • ブロックコメント:  <# ~ #>


コメントベースドヘルプ

<#

.SYNOPSIS
.DESCRIPTION
.PARAMETER name
.EXAMPLE
.NOTES
.LINK
#>


言語の特性

特性
振る舞い

実行形式
インタプリタ型

スコープ
ダイナミックスコープ

識別子の大文字、小文字
区別しない

文の区切り

; (省略可)

偽の値

$null, $false, @(), "", 0 など (詳しくは後述の「分岐」を参照)

行の継続
行末のバッククォート(`)は次の行に式が続くことを示す。パイプ(|) や 開き括弧({, ()などで終わる行はクォートは不要

else ifの形式
elseif


とりあえず覚えること

Set-ExecutionPolicy

Set-StrictMode

$profile


Hello, World!

# 事前にコマンドプロンプトを管理者で起動して以下を実行

C:\Windows\system32> powershell

PS C:\windows\system32> Get-ExecutionPolicy
Restricted

PS C:\windows\system32> Set-ExecutionPolicy RemoteSigned

PS C:\windows\system32> Get-ExecutionPolicy
RemoteSigned


hello.ps1

"Hello, World!"   


C:\> powershell .\hello.ps1

Hello, World!

管理者になれない人や、ExecutionPolicy を変更したくない人は、「PowerShellのExecutionPolicyのスコープとかについて詳しく」を読むと良い。


スクリプトファイル雛形

$ErrorActionPreference = "Stop"  # コマンド異常時にスクリプトを終了する

Set-StrictMode -Version 2.0 # 未定義変数、未定義プロパティの参照をエラーにするなど

上記は最低限と考えるもの。もう少しいろいろやっていると以下のようなものが必要になってくる。

なお、powershell v2 (未だに現場に存在する)を前提としている。

# スクリプトと同じフォルダのスクリプトを読む(モジュールを作るほどでもない場合に利用)

. (Join-Path $MyInvocation.MyCommand.Path ../hogehoge.ps1)

# 例外が発生したときにpowershell自身の終了ステータスを1として異常終了して欲しい場合
# Jenkins などでコマンドの異常を検知させたい場合に利用する
trap {
$ErrorActionPreference = "Continue";
Write-Error $_
exit 1
}

# テストを実行したい場合やISE内での変数の変更を局所化したい場合など
# main処理を関数にして直接そのスクリプトを実行した場合だけmainを実行したい
function main {
}

if ($MyInvocation.InvocationName -notin '&','.') {
main
}

# ISE ではないとき終了後に入力待ちにして画面が消えてほしくない場合がある
if ($Host.Name -notmatch "ISE") {
Read-Host "終了するにはEnterキーを押してください"
}


パイプライン

パイプラインでよく使うコマンド

コマンド
略記
補足

ForEach-Object
%

Select-Object

Out-Null
>$null

Out-File
>
リダイレクト出力と同じ。エンコーディングの指定などパラメータで動作を変えることができる

Where-Object
?

Format-List, Format-Table, Format-Wide, Format-Custom

Sort-Object

Group-Object

はまるところを参照

Out-String

入力オブジェクトを文字列にする

Export-CSV

Tee-Object

Get-Unique

式の値は出力であり、パイプラインの入力になる

ループ用の変数として、 $_ が使える

$var = 1..3

$var | ForEach-Object { $_ * 2 } | ForEach-Object { $_ + 1 }
# => 3
# 5
# 7

パイプラインの各コマンドは、(Unixコマンドと同様に)並列に動作する。

ただし、マルチスレッドではないらしい。

(ここでは、画面出力に Write-Host を利用。この出力は直接画面に出てパイプには伝搬しない)

$var = 1..3

$var | ForEach-Object { Write-Host $_; $_} |
ForEach-Object { Write-Host $_ }
# => 1
# 1
# 2
# 2
# 3
# 3


値の確認

$obj

$obj.GetType().FullName # 型の名前を返す
$obj | Get-Member


変数

$var

$env:path # 環境変数
# 変数名に大文字、小文字の区別はない


代入

$a = 1

$a, $b = 1, 2
$foo, $bar = 1..5 # $foo = 1; $bar = 2..5
$a += 1
$a++ # 前置、後置あり。後置は、式の評価の後に演算


スコープ

スクリプトファイル、関数、& { ... } (Call Operator によるスクリプトブロック実行)でスコープができる。

以下ラベルをつける($global:foo = 1 など)ことで別の変数のスコープを参照したり、定義するスコープを明示したりできる


  • グローバル global

  • スクリプト script

  • private 現在のスコープのみ

  • local スコープを付けない変数と同じ?


自動変数(Automatic Variables)

$_

$input
$foreach
$args
$switch
$matches
$? ($true/$false)
$LastExitCode
$Error
$StackTrace
$HOME
$PsHome
$ConsoleFileName
$Profile
$PWD
$ShellId
$PID
$HOST
$true
$false
$null 未初期化の変数の値
$OFS


設定変数(Preference Variables)

Variable                             Default Value

-------- -------------
$ConfirmPreference High
$DebugPreference SilentlyContinue
$ErrorActionPreference Continue
$ProgressPreference Continue
$VerbosePreference SilentlyContinue
$WarningPreference Continue
$WhatIfPreference False
$OFS (Space character (" "))
$OutputEncoding ASCIIEncoding object


リテラル


数値

1

0x10 # 16進数 なお、8進数の表記はない
1KB, 1MB, 1GB # 1024 単位


文字列

"abc"  変数展開あり

'abc' 変数展開なし

# コマンドレットのパラメータに渡す文字列は空白が含まれないか、"-"(ハイフン)で始まらなければクォートはいらない
Write-Host abc # Write-Host "abc" と同じ
Write-Host "a b c" # クォートが必須
Write-Host "-abc" # クォートが必須(クォートがなければ
Write-Host a$var # ダブルクォートで括られているかのように$varは変数展開される

# 以下はすべて1文字の文字列 " を表す
"`"" # ` はエスケープ文字
'"'
""""

# 変数展開
"$foo"

# 式展開
"abc$(式)def"

# プロパティの値を文字列に埋め込むには、以下のように式展開を利用するしかない
# `$($...` と少し冗長で汚い表記になる
"abc$($var.property)"

# ヒアストリング(@' '@ なら変数展開しない)
@"
~
"@


配列

# 定義

$array = @(1,2,3,4)
$array = 1,2,3,4
$array = 1..4
$array = @(1) # 1要素の配列
$array = ,1 # 1要素の配列
$array = @() # 空の配列

# 参照
$array[0]
$array[-1] # 末尾の要素
$array[0,1,2] # インデックス 0,1,2 の要素
$array[0..2] # インデックス 0,1,2 の要素
$array[0..1+2] # インデックス 0,1,2 の要素

$index = 0,1,2
$array[$index] # インデックス 0,1,2 の要素

# 要素の置換
$array[1] = 2

# 要素の追加
$array += 5

# 要素の削除
$array[1] = $null
# ただし、配列の要素に$nullが入っているだけで、$array.Count のサイズは変わらない(後述)

# 範囲外参照 => $null が返る
$array[10] -eq $null # => True
# ただし、Set-StrictMode -Version 3 の場合は、例外 IndexOutOfRangeException が発生

# インデックス 1から末尾までの要素
$array[1..($array.Length-1)]
# ただし、Set-StrictMode -Version 3の場合、要素数が0または1のときに範囲外参照のエラーになるので事前に長さチェックが必要になる

# 以下は、上と同じ意味にはならない。(1..-1は、1,0,-1を生成するため)
$array[1..-1]


配列の$null要素と削除について

配列の要素$nullの挿入は、要素の削除ではなく$nullでの上書き

$array = @(1,2,3)

$array[1] = $null
$array.Count # => 3

$array | ForEach-Object { $_ -eq $null }
# => False
# True
# False

Select-Object を通せば $null を除くことができる

$array | Select-Object | ForEach-Object { $_ -eq $null }

# => False
# False

Powershellの配列System.Arrayは、配列要素数の動的な変更ができない(たぶん)。

本当にこのようなオブジェクトが必要なら.Net の、System.Collections.ArrayList オブジェクトにする方法がある

$array = @(1,2,3)

$list = [Collections.ArrayList]$array
$list.RemoveAt(1)
$list.Count # => 2

配列要素を縦に書くことができる。(カンマは不要。出力のある式の値が配列要素になる)

@(

"a"
"b"
).Length # => 2

あるいはカンマ付きで列挙する場合には、追加削除がしやすいよう各要素の前にカンマを書くことができる。最後の要素の後ろにカンマを書くと構文エラーになる。

@(

,"a"
,"b"
).Length # => 2

@()の詳細については、powershellの@()の謎を解くを参照。


ハッシュ

# 定義

$hash = @{one = 1; two = 2; three = 3}

# 参照
$hash["two"]
$hash.two

# キーの配列(順不同)
$hash.Keys

# 値の配列(順不同)
$hash.Values

# ハッシュでループ(順不同)
$hash.Keys | ForEach-Object {
$key = $_
$value = $hash.$key
}

# ハッシュでループ(順不同)
# 各要素はDictionaryEntry
$hash.GetEnumerator() | ForEach-Object {
$key = $_.Key
$value = $_.Value
}

# ハッシュをpowershellのオブジェクト化
$obj = New-Object PSObject -Property $hash
# オブジェクトにするとFormat-Tableとかで扱いやすくなる
$obj | Format-Table -AutoSize -Property one,three


PSObject

# 定義

$obj = New-Object PSObject -Property @{
one = 1
two = 2
three = 3
}

# プロパティ参照
$obj.one
$obj."one" # 文字列でプロパティ名を指定できる。プロパティ名が数字始まりなどの場合に使える。
$propname = "one" # 変数でプロパティ名を指定できる。動的なプロパティの指定に使える。
$obj.$propname

# プロパティ代入
$obj.two = 2

# プロパティ追加
$obj | Add-Member -NotePropertyName four -NotePropertyValue 4
$obj | Add-Member @{five = 5; six = 6}
$obj | Add-Member seven 7
# エイリアス追加
$obj | Add-Member -MemberType AliasProperty -Name seven2 -Value seven
# メソッド追加
$obj | Add-Member -MemberType ScriptMethod -Name Show -Value { $this | Select-Object one,two,three,four,five,six,seven,seven2}
# メソッド呼び出し
$obj.Show()

# プロパティ名の配列(順不同)
$obj | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name

# プロパティ値の配列(順不同)
$obj | Get-Member -MemberType NoteProperty | ForEach-Object { $obj.($_.Name) }


PSCustomObject

powershell v3 から利用できる。PSObjectよりも以下の利点がある。(主観: 3番目の特徴が使いやすい)


  • 生成の構文が簡潔(キャストするだけ)

  • 生成が速い

  • プロパティの順序を定義順で保持する

以下の例は、PSObjectの例と比べて、定義方法とShowメソッドの実装が異なる。(Select-Object でプロパティの順序を指定する必要がない)

    # 定義

$obj = [PSCustomObject]@{
one = 1
two = 2
three = 3
}

# プロパティ参照
$obj.one

# プロパティ代入
$obj.two = 2

# プロパティ追加
$obj | Add-Member -NotePropertyName four -NotePropertyValue 4
$obj | Add-Member @{five = 5; six = 6}
$obj | Add-Member seven 7
# エイリアス追加
$obj | Add-Member -MemberType AliasProperty -Name seven2 -Value seven
# メソッド追加
$obj | Add-Member -MemberType ScriptMethod -Name Show -Value { $this }
# メソッド呼び出し
$obj.Show()

powershell v2 では、[PSCustomObject]@{a = 1; b = 2} としても、ハッシュが返る。

上記のPSCustomObjectを使う場合は、スクリプト冒頭に#Requires -Version 3を記述してバージョンを限定した方がよい。

(参考)キャストによる生成の方が速いことの確認

$hash = @{

one = 1
two = 2
three = 3
}

Measure-Command { 1..10000 | ForEach-Object { New-Object psobject -Property $hash } } | Format-Table TotalMilliseconds -AutoSize
Measure-Command { 1..10000 | ForEach-Object { [PSCustomObject]$hash } } | Format-Table TotalMilliseconds -AutoSize

結果

TotalMilliseconds

-----------------
1211.9604
TotalMilliseconds
-----------------
360.6347


正規表現

-match 演算子の右辺の文字列、-split 演算子のデリミタ、-replace 演算子の置換対象

は正規表現にコンパイルされる。

-match-notmatch 演算子で正規表現がマッチした場合のみ $Matches にキャプチャがハッシュで格納される。

マッチしなかった場合は、$Matchesには前の値が残っているので、必ずマッチしたことを判断した後に参照する必要がある。

if ("abcdef" -match "bc(de)") {

$Matches
}

Name                           Value

---- -----
0 bcde
1 de

正規表現のデフォルトのオプションは


  • 大文字・小文字の違いを無視する

  • "." は改行にマッチしない(LFにマッチしない。CRにはマッチする)

  • "^", "$" は改行の後、改行の前にはマッチしない

それぞれ、"(?ims:...)" "(?-ims:...)" で切り替える。


名前付きキャプチャ

名前付きキャプチャは (?<name>...)または(?'name'...) を使う。

$Matches のキーがその名前に変わる

if ("abcdef" -match "bc(?<xxx>de)") {

$Matches
}

Name                           Value

---- -----
xxx de
0 bcde


制御構造


分岐

 if (式) { 

...
} elseif (式) {
...
} else {
...
}

真偽値の判定は以下の通り

    function cond {

Param($v)
if ($v) {
"true"
} else {
"false"
}
}

cond $null # => false
cond $false # => false
cond @() # => false
cond "" # => false
cond 0 # => false
cond 0.0 # => false

# 以下は意外にも偽になる(1要素の配列はその要素がfalseならfalseとなる)
cond @($null) # => false
cond @(0) # => false

# 2要素の配列なら真になる
cond @($null,$null) # => true

# 空のハッシュは真
cond @{} # => true


switch 文

switch (式) {

値 { ... ; break }
値 { ... ; break }
{ 条件式 } { ... ; break }
default { ... }
}

# 条件式では、$_ で式の値を参照できる `{ $_ -eq 5 }` など


ループ

# while 文

while (式) {
...
}

# do while 文
do {
...
} while (式)

# for 文
for ( 初期値; ループ条件; 式) {
...
}
# 例
for ($i = 0; $i -lt 10; $i++) { Write-Host $i }

# foreach 文
foreach (変数 in 式) {
...
}
# foreachループを抜けた後、変数は残っている(foreach 内のスコープではない)
foreach ($v in 1,2,3) { }
$v # => 3


脱出

break

ループやswitch を抜ける
continue
次のループ
return
関数やスクリプトブロックを抜ける

# スクリプトブロック(`{ }`)では、continue の代わりに return を使う
(1..3) | % {
if ($_ -gt 2) {
return
}
$_
}

break, continue は、引数に :ラベル を指定できる。:ラベルは、for, while, foreach などの前に指定できる

  :ONE

foreach ($one in @(1,2,3)) {
:TWO
foreach ($two in @(4,5,6)) {
[String]$one + ":" + $two
break :ONE
}
}


演算子


算術演算子



  • + - * / %

加算、減算、乗算、除算、剰余算

整数の割り算は割り切れない場合は浮動小数を返す。(整数に丸めるには[Int]でキャストする。丸め方は偶数丸めになる)

    4 / 2  # => 2

10 / 3 # => 3.33333333333333
[Int](10 / 3) # => 3

+ は、文字列、配列、ハッシュの結合に、* は、文字列、配列の繰り返しに使える。

ハッシュの結合はキー重複があればエラーになる。

べき乗の演算子はない。代わりにMathクラスの静的メソッドPowを使う

[Math]::Pow(2, 3) # 2の3乗 => 8


代入演算子



  • = += -= *= /= %=


比較演算子


  • -eq 等しい

  • -ne 等しくない

  • -gt より大きい

  • -lt より小さい

  • -ge 以上

  • -le 以下


  • -like ワイルドカードによる比較


  • -notlike ワイルドカードによる比較


  • -match 正規表現による比較


  • -notmatch 正規表現による比較



論理演算子


  • -and 論理積

  • -or 論理和

  • -xor 排他的論理和

  • -not 論理否定

  • ! 論理否定


リダイレクト演算子


  • >

  • >>

  • 2>

  • 2>>

  • 2>&1


split, join 演算子


  • -split

  • -join

-split string

string -split delimiter
string -split delimiter,maxsubstrings

-join strings

strings -join delimiter


型演算子


  • -is 指定された型である。

  • -isnot 指定された型ではない。

  • -as 型キャスト


フォーマット演算子


  • -f

"{0} {1,-10} {2:N}" -f 1,"hello",[math]::pi

=> 1 hello 3.14


範囲演算子

範囲の開始値..範囲の終了値

1..5 # => 1,2,3,4,5 と同じ

5..1 # => 5,4,3,2,1 と同じ

1..100000 としても、配列を生成するのではなく、整数ジェネレータとして動作する。

従って、1..10000 | ForEach-Object { $_ } などとしても大きな配列を生成せずにループする。

$x = 1..100000 のように変数に代入するとその時点で配列を生成する。


置換演算子


  • -replace 半角英字の大文字/小文字を区別しないで置換する。

  • -ireplace 同上。

  • -creplace 半角英字の大文字/小文字を区別して置換する。

    "置換対象文字列" -replace "置換したい文字(列)", "置換後の文字(列)"

置換後の文字列に$1, $2を書けば対応する括弧にマッチした文字列となる。$0はマッチした部分全体を表す。ダブルクォート文字列中ではバッククォート(`)によるエスケープが必要となることに注意が必要。

 "foobarbaz" -replace "foo(.*)baz", "FOO`$1BAZ"

# => FOObarBAZ

置換文字列を文字列ではなく式で指定する方法はpowershellにはない(powershell v4 調べ。たぶん)

これを行うには、.Net のメソッドを使う必要がある。

$x = [Regex]::Replace("foobarbaz", "(b..)", {Param($m); $m.Groups[1].Value.ToUpper() })

$x
=> fooBARBAZ

# 以下の様なことができても良さそうだが -replace 演算子はスクリプトブロックを実行しない
"foobarbaz" -replace "bar", { "foo" + "bar" }
# => foo "foo" + "bar" baz

# スクリプトブロックの文字列表現は、そのソースなので上記は以下と同じ動作をしていると思われる
"foobarbaz" -replace "bar", { "foo" + "bar" }.ToString()
# => foo "foo" + "bar" baz

置換演算子は括弧なしに繋げられる

"foo" -replace "foo", "bar" `

-replace "bar", "baz" `
-replace "baz", "FOO"
=> FOO


ビット演算子


  • -band ビットAnd

  • -bor ビットOr

  • -bxor ビットXor

  • -bnot ビットNot

  • -shl 左ビットシフト (powershell v3)

  • -shr 右ビットシフト (powershell v3)


包含演算子


  • -in

  • -notin

  • -contains コレクションに指定した値が含まれている

  • -notcontains コレクションに指定した値が含まれていない

-in-contains は左辺と右辺が逆


関数定義

function foo {

param($arg1, $arg2)

return $value
}

式の値(出力)または、return の引数が戻り値になる。以下の関数は文字列"a"を返す

function foo {

"a"
}

foo
# => "a"

以下の関数は、配列"a","b"を返す(複数の値を出力すれば配列になる)

function bar {

"a"
"b"
}

bar
# => "a"
# "b"

引数のない return は関数を終了するだけで値を返さない。

function foo {

"a"
return
"b"
}

(foo).Length # => 1

例えば、ファイルにパターンを含むかどうか判定する grep という関数を作るとする

function grep

{
param($pattern, $file)

Get-Content $file | Select-String -Pattern $pattern
}

となる。呼び出し側は、以下のパターンがある。


  • 1. そのまま、出力する。

    grep "^xx" file


  • 2. 変数に結果を代入する

    $ret = grep "^xx" file


  • 3. 常に配列として結果を代入する。@()で囲まない上の例では、出力が0または、1要素の場合配列にならない。(結果によって配列かどうか変わる)

    $ret = @(grep "^xx" file)


  • 4. コマンドの引数に結果を渡すには、括弧で囲む

    Write-Host (grep "^xx" file)

特に、この例でパターンを含むかどうかを判定させたい場合には、以下のようにする。@()によって、常に配列として受けることでそのままCountプロパティが使える。(空配列は偽となることから、短く書くなら .Count -gt 0 は不要)

    if (@(grep "^xx" file | Select-Object -First 1).Count -gt 0) {

"Found"
}
else {
"Not found"
}


二値を返す関数の例

powershellの関数には終了ステータスがない(プロセス実行にはある)。Unixのシェルの場合は行が見つかったかどうか終了ステータスで判定ができるが、powershellではあくまでも出力(式の値)が関数の戻り値である

    function grep

{
param($pattern, $file)

@(Get-Content $file | Select-String -Pattern $pattern | Select-Object -First 1).Count -gt 0
}

if (grep "^xxx" file) {
"Found"
}
else {
"Not found"
}

    Unixシェルなら以下のように書くところを

if grep "^xxx" file >/dev/null; then
echo "Found"
else
echo "Not found"
fi

以下のような書き方をしたことになる
if [[ $(grep "^xxx" file | head -1 | wc -l) -gt 0 ]]; then
echo "Found"
else
echo "Not found"
fi


スクリプトブロックからの脱出

二値を返すgrepの実装例を手続的に作りたい場合、以下のようなことがしたくなるができない。

スクリプトブロックからのreturnはスクリプトブロック自体(ここでは、ForEach-Object)からのreturn

であり、関数をreturnしない。

    function grep

{
param($pattern, $file)

@(Get-Content $file | Select-String -Pattern $pattern | ForEach-Object {
"found"

return # ここで、関数を抜けたいが出来ない。ForEach-Object の一回のループを抜けるだけ
}).Count -gt 0
}

PowerShell バージョン2以降なら例外を使って以下のようにすることはできる

    function grep

{
param($pattern, $file)

@(try {
Get-Content $file | Select-String -Pattern $pattern | ForEach-Object {
"found"

throw
# 例外で無駄なループをスキップする苦肉の策
}
}
catch {}).Count -gt 0
}

if (grep "^xx" file) {
"Found"
}
else {
"Not found"
}

Select-Object -First 1 は、入力の処理を中断する。結局、大域脱出より関数型的にこのことを利用した方が筋がよい。

※ powershell v2 では中断されなかったので注意!例えば、以下の例は無限ループが中断されないので、処理が終わらない。

function foo {

while ($true) {
$global:i
$global:i++
}
}

$i = 0; foo | Select-Object -First 2
# => 0
# 1
$i
# => 1

# 条件に一致した場合に、中断するにはWhere-Objectと組み合わせれば良い
$i = 0; foo | Where-Object { $_ -eq 50 } | Select-Object -First 1
# => 50
$i
# => 50

ファイルのクローズなど、必ず後処理を行わせたい場合は、try .. finally を使う。

function foo {

$global:i = 0
try {
for (;;) {
$global:i
$global:i += 1
}
}
finally {
Write-Host "END" # 中断した場合でも実行したい処理
}
}

foo | Select-Object -First 2
# => 0
# 1
# END

finally 節が出力を行うとfinally節も中断されてしまうので注意

function foo {

$global:i = 0
try {
for (;;) {
$global:i
$global:i += 1
}
}
finally {
9999 # <-------- 出力
Write-Host "END"
}
}

foo | Select-Object -First 2
# => 0
# 1


引数のパターン

    function foo

{
param($arg1, $arg2, $arg3 = 3)
"arg1: " + $arg1
"arg2: " + $arg2
"args: " + ($args -join ",")
}

# 可変長引数($args)
# param() で受けていない残りの引数は、$args に配列で格納される
foo a b c d
# => arg1: a
# arg2: b
# args: c,d

# 引数の位置指定(コマンドラインスイッチ)
# コマンドラインスイッチの形式で名前を指定して引数を渡せる
foo -arg2 b c d
# => arg1: c
# arg2: b
# args: d

# 引数のデフォルト値
function bar
{
param($arg1 = 1, $arg2 = 2)
"arg1: " + $arg1
"arg2: " + $arg2
"args: " + ($args -join ",")
}

bar -arg2 b
# => arg1: 1
# arg2: b
# args:

# スクリプトブロックはそのまま引数として渡される
function baz
{
param($block)
"block: " + $block.GetType()
& $block
}

baz { Write-Host "baz" }
# => block: scriptblock
# baz


関数定義(filter)

Begin, Process, End のキーワードを使った以下の構文で関数を定義すると、その関数はフィルタとして動作する。

Beginブロックは、入力を処理する前に一度だけ実行され、Processブロックは入力の都度($_ に入力が渡される)実行される。Endブロックは最後の入力を処理した後に一度だけ実行される。

Begin, Process, End ブロックはそれぞれ省略できる。

function foo {

Param($arg1, $arg2)
Begin {
..
}
Process {
..
}
End {
..
}
}

function flatten {

param($block)

Process {
$_ | ForEach-Object $block
}
}

@(1,2),@(3,4) | ForEach-Object { ">" + $_ }
# => 1 2
# 3 4
@(1,2),@(3,4) | flatten { ">" + $_ }
# => 1
# 2
# 3
# 4

パイプの入力元のオブジェクトを表す$input変数を使えば通常の関数定義の構文でフィルタを作成することもできる。

ただし、この場合は入力をすべて受け付けた後に処理が動作するので、パイプラインが並列で動作しない。

# 悪い例

function flatten {
param($block)

$input | ForEach-Object {
$_ | ForEach-Object $block
}
}

1..1000000 | flatten { ">" + $_ } | Select-Object -First 1
# => >1

# この場合、1000000要素の配列が生成された後に、各ブロックが実行されるので遅い
# 最初のフィルタ定義の構文であれば、すぐに処理が終了する

フィルタ構文のENDブロックは、フィルタで中断された場合には実行されない(!)。この問題をスマートに解決する手段は不明。

function foo {

Begin { $i = 0 }
Process {
$i
$i++
}
End {
Write-Host "END" # <--- パイプラインによって、実行されないかもしれない
}
}

1..10 | foo | Select-Object -First 2
# => 0
# 1
# (END は表示されない)


インデックス付ループ

例1

"foo","bar","baz" | ForEach-Object { $i = 0 } { $i, $_ -join ","; $i++ }

# => 0,foo
1,bar
2,baz

例2

function With-Index {

Begin { $i = 0 }
Process { ,@($i, $_); $i++ }
}

"foo","bar","baz" | With-Index | ForEach-Object { $_ -join "," }
# => 0,foo
1,bar
2,baz

# With-Index 関数で 「, @(1,2)」 っと余分な 前置の "," が必要になる点がわかりにくい
# @(@(1,2)) は、@(1,2)と同じになってしまう

例3

function With-Index {

Begin { $i = 0 }
Process { $_ | Add-Member -MemberType NoteProperty Index $i; $_; $i++ }
}

"foo","bar","baz" | With-Index | ForEach-Object { $_.Index, $_ -join "," }


flatMap

例1

function flatMap {

param($block)

Process {
& $block | ForEach-Object { $_ }
}
}

1,3 | flatMap { $_, ($_ + 1) } | ForEach-Object { ">" + $_ }
# => >1
# >2
# >3
# >4
1,3 | flatMap { , @($_, ($_ + 1)) } | ForEach-Object { ">" + $_ }
# => >1
# >2
# >3
# >4

例2

例1のようなことをしなくても、ForEach-Objectのままでも一段階なら展開される。

1,3 | ForEach-Object { $_, ($_ + 1) } | ForEach-Object { ">" + $_ }

# => >1
# >2
# >3
# >4


その他


日付

現在時刻を返す

$date = Get-Date

型は System.DateTime

ミリ秒の精度を持つ

日付の演算

$date = $date.AddMonths(1)

$date = $date.AddDays(-1)
$date = $date.AddHours(1)

など(演算結果のDateTimeを返す)

文字列→日付

$date = [DateTime]"2014/2/28 12:34:56"

日付→文字列

Get-Date $date -Format "yyyy/MM/dd hh:mm:ss"


パス操作

ファイル名部分取得

Split-Path -Leaf パス文字列

親ディレクトリ名部分取得

Split-Path パス文字列

ドライブ名取得

Split-Path -Quolifier パス文字列

パス連結

Join-Path C:\Windows php.ini

カレントフォルダ取得

Get-Location

フルパス取得

#ファイルが存在しないとダメ

Resolve-Path 相対パス # => PathInfoオブジェクト
(Get-Item 相対パス).FullName

#ファイルが存在しなくてもいい
#Set-Locationによるカレントフォルダの変更は無視される
[IO.Path]::GetFullPath(相対パス)

# 相対パスは存在しなくて良い(後者は外部のコマンドに渡したいファイル名として使う)
Join-Path (Get-Location) 相対パス
Join-Path (Convert-Path .) 相対パス


sleep

Start-Sleep 秒


OSコマンド実行(Call Operator)

powershell ではコマンドレットなどと同じ方法でOSコマンドを実行できる

# コマンド(や関数、コマンドレット)実行

notepad

# & を付けても同じ
& notepad

# コマンドに空白を含む場合は、& を使う必要がある
& "c:\Program Files\Internet Explorer\iexplore.exe"

# & を使えば、文字列をコマンド(や関数、コマンドレットなど)として実行することができる
& "Get-Date"

$cmd = "Get-Date"
& $cmd

&(Call Operator) を使えば、文字列をコマンド(エイリアス、関数、コマンドレット、OSコマンド)として実行できる。パスに空白を含むコマンドを実行する場合や変数に入ったコマンドを実行する場合に利用する。

OSコマンドとコマンドレットを区別して実行するには、フルパスで書く、.exe等拡張子を指定、オブジェクトを取得しそれを&で実行するなどがある。

  function notepad { "dummy" }

notepad # => dummy
notepad.exe # => notepad.exe 起動
C:\Windows\notepad.exe # => notepad.exe 起動

# CommandInfo オブジェクト
# Path に同名のコマンドが複数あることを考慮して最初の1 個に限定している)
& (Get-Command notepad -CommandType Application | Select-Object -First 1)

# FileInfo オブジェクト
& (Get-ChildItem c:\Windows\notepad.exe)

dos のstartコマンドのように操作対象のファイルを指定してコマンドを実行できる。

# 関連付けされたコマンド(notepadやブラウザなど)を使ってファイルを開く。

# ファイルは存在していなければならない。

c:\foo\bar.txt
c:\foo\bar.html

# url を直接書いてブラウザを起動するようなことはできない
http://www.google.com # => CommandNotFoundException


GUIプログラムの終了を待つ

notepad | Out-Null


powershell_ise から実行できないコンソールコマンド

https://operationslab.wordpress.com/2013/02/16/powershell-ise-から呼び出せないコマンド/


文字列を式として評価

Invoke-Expression "Get-Date"

Invoke-Expression "'a' -eq 'a'" # => True

& と Invoke-Expression は似ているようで違う(&はスコープを作る)

    $foo = 1

& { $foo += 1 }
$foo # => 1

$bar = 1
Invoke-Expression ' $bar += 1 '
$bar # => 2


よく使うコマンドレット

コマンドレット
エイリアス
補足

Get-ChildItem
ls

Get-Content
cat

Set-Content

Add-Content

Clear-Content

Set-Location
cd

Push-Location
pushd

Pop-Location
popd

Get-PSDrive

Get-PSProvider

Select-String

grep のようなもの

New-Item

Remove-Item

Copy-Item

Set-Item

Move-Item

Rename-Item

Clear-Item


サンプルスクリプト


空フォルダを出力

    Get-ChildItem . -Recurse |

if ($_.Attributes -band [System.IO.FileAttributes]::Directory) {
if (@($_ | Get-ChildItem).Count -eq 0) {
Write-Output $_.FullName >> list.txt
Write-Host $_.FullName
}
}
}

別解

    Get-ChildItem . -Recurse | 

?{ $_.Attributes -band [System.IO.FileAttributes]::Directory} |
?{ @($_ | Get-ChildItem).Count -eq 0 } |
%{ Write-Host $_.FullName }

Read-Host -Prompt "完了しました"


空フォルダを削除

    Get-ChildItem . -Recurse | %{

if ($_.Attributes -band [System.IO.FileAttributes]::Directory}) {
if (@($_ | Get-ChildItem).Count -eq 0) {
Write-Host $_.FullName
Remove-Item $_.FullName
}
}
}


漢字コード変換

    $top = "."

$src = "$top\utf8\*.csv"
$dest = New-Item -Path "$top\unicode" -ItemType "directory"

Get-Item $src | %{
$name = $_.Name
Get-Content -Path $_.FullName -Encoding UTF8 |
Set-Content -Path "$top\unicode\$name" -Encoding Unicode
}
Read-Host -Prompt "完了しました"


tail -f

ただし、ファイルすべて読み込むので小さいファイルでないと...

    Get-Content -path $args[0] -wait


BOMありUTF-8にする

    Get-Content -Path test.txt -Encoding UTF8 |

Out-File -Encoding UTF8 -FilePath test2.txt


BOMなしUTF-8にする

    $Content = Get-Content -Path test.txt -Encoding UTF8

$Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding($False)
[System.IO.File]::WriteAllLines('test2.txt', $Content, $Utf8NoBomEncoding)


マイナーな言語機能

powershellは、表現方法が豊か(?)すぎて分かりにくい機能が多々ある。ここでは、個人的に利用頻度が低いであろう機能やマニアックな機能、使うと混乱する機能について補足する。

以降は、慣れないうちは利用自体を控えた方がよいと考える。


部分式 $( ) (subexpression)

複数の式を書けることを除けば括弧( )と同じ。普通の用途では()を使えばいい。式の結果によってスカラー値または配列を返す。

   $( "a"; "b" )[1] # => b

以下のように複数の式の結果をブロックで受けたり、ファイルにリダイレクトできる

    $(

Get-Date
Start-SLeep 2
Get-Date
) | ForEach-Object { $_ }

しかし、$()@()は、中の式がすべて実行されるまで結果をパイプに伝搬しない。上記例では、2秒後に2つのGet-Dateの結果が表示される。

このような用途では、& { }(Call operator)や . { }(Dot sourcing operator)によるスクリプトブロックの実行の方が並列性が保ててよい。

    & {

Get-Date # この Get-Date の結果がすぐに表示される
Start-SLeep 2
Get-Date
} | ForEach-Object { $_ }


& (call operator) によるスクリプトブロックの実行とスコープ

& は、ブロックを渡すと子スコープで式を評価する。無名関数として利用できる。

  $foo = 1

& { $foo++; $foo } # => 2 (子スコープの変数 $foo を参照)
$foo # => 1 # 上のスコープの変数は変わらない

&が子スコープを作る。スクリプトブロックが必ず子スコープを作るわけではない。

  $foo = 1

$bar = { $foo++; $foo }
& $bar # => 2 (子スコープの変数 $foo を参照)
$foo # => 1 # 親のスコープの変数は変わらない
ForEach-Object $bar # => 2 親のスコープを参照
$foo # => 2 # 親のスコープの変数が変化

# 注意: ForEach-Object は、入力がない場合、$null を入力に一回だけ現在のスコープでブロックを実行する

スクリプトブロックは実行場所のスコープの変数を参照する。

スクリプトブロックが外のスコープを保存しないので、クロージャではない

  function foo

{
$block = { Write-Host $foo }

& $block # => global

$foo = "function"

& $block # => function

$block
}

$foo = "global"

& (foo) # => global
$foo # => global

スクリプトブロックはそのスコープも保存しないので、クロージャではない

  $block = {

$bar++
$bar
}

$bar = 5
& $block # => 6
& $block # => 6 実行の都度変数が定義されているので値を引き継がない
$bar # => 5

上記例の注意:

以下はpowershellのバージョン毎に異なる結果となる

   $block = {

$bar += 1
$bar
}

$bar = 5
& $block
# version2 は 6
# version4, 5 は 1

version2 では、$bar += 1 は、親スコープの値に+1した結果となる。しかし、version4および5では親スコープの値に関係なく\$barを1にする。

$bar += 1の代わりに $bar = $bar + 1$bar++ と書けばどのバージョンでも親スコープの値に+1した結果となる。$bar += 1という書き方にのみ注意が必要となる。

この挙動はいずれかがバグだと思う(バージョン4, 5 のバグだと思う)。


. (Dot sourcing operator)

新たなスコープを作らないことを除けば、& と同じ。(ように思えるが、確証はない)

通常の用途としては、Unixシェルと同じように引数に指定したファイルを読み込む。

(だから、決してマイナーな機能ではないが、&との関連からここに記載する)


foo.ps1

$a++

$a

$a = 1

. .\foo.ps1 # => 2
. .\foo.ps1 # => 3
$a # => 3

& の場合は、新たなスコープで読み込む(実行する)ため、親スコープの値が変わらない。

$a = 1

& .\foo.ps1 # => 2
& .\foo.ps1 # => 2
$a # => 1


クロージャ

powershell v2 から本物のクロージャが使える。

スクリプトブロックに対して、GetNewClosure() メソッドを呼び出すとその時点のスコープをもとにクロージャを生成することができる。(より詳細な情報は「powershellのクロージャを理解する」にまとめた)

function foo {

$a = 0
$block = {
$script:a++
Write-Host $a
}.GetNewClosure()

$block
}

$a = "global"
(foo).GetType().FullName # => System.Management.Automation.ScriptBlock

$b = foo
& $b # => 1 (関数のスコープを参照している)
& $b # => 2 (関数のスコープの値を保持している)
& (foo) # => 1 呼び出し毎に新しいクロージャが生成される
& $b # => 3 元のクロージャはそのまま


型キャスト []-as

(主観) 賛否両論あると思うが、型のキャストをしたり型を明示したりするとスクリプトが途端に見にくくなるので、出来るだけ型キャストしなくて済むようなコーディングを心がけたいと思っている。


参照渡し [ref]

(主観) 副作用反対


スコープの規則


  1. 内側のスコープ(child scope)は、外側のスコープ(parent scope)を参照できる。

  2. 外側のスコープの値を変えることはできない


  • 1の例外: privateを明示した変数は内側のスコープから参照できない。

$g = 1

$private:p = 2
& {
$g # => 1
$p # => $null
}


  • 2の例外:スコープを明示すれば(privateでない)外側のスコープの値を変更できる。

$a = 1

$b = 2
& {
$a = $a + 1
$global:b = $b + 1
}
$a # => 1
$b # => 3


コマンドの異常時に終了する

デフォルトではコマンドに異常があっても、次の式が継続して実行される。

これは、設定変数 $ErrorActionPreference のデフォルト値が "Continue" であるためである。

デフォルトがこうなっているのは、Unix シェルに合わせているものと思われる。

(主観:インタラクティブな動作の時は "Continue"、スクリプト実行のときは "Stop" だったらよかったのに)

# Unix シェル

[ $x -gt 0 ] # => エラー ($x が空のため)

echo $? # => 2 # 実行は継続する最終ステータスのエラー値を表示
echo $? # => 0 # 直前のechoコマンドのエラー値を表示、結果スクリプト自体は正常終了

$x = @{}

$x -gt 0 # => エラー: NotIcomparable
Write-Host $? # => False 実行は継続する
Write-Host $? # => True スクリプト自体は正常終了する

それでも異常時に即座にスクリプトを止めて、異常終了したい場合は、スクリプト冒頭に $ErrorActionPreference = "Stop" を書く。(Unix シェルの set -e と同じ)

$ErrorActionPreference = "Stop" 

$x = @{}
$x -gt 0 # => エラー: NotIcomparable この時点でスクリプトは、異常ステータス 1 で終了する
Write-Host $? # 実行されない
Write-Host $? # 実行されない

ファイルの存在チェックなどで異常終了してほしくない場合がある。異常終了して欲しくないコマンドに対しては共通のオプション -ErrorAction SilentlyContinue を利用する。

(主観:見通しが悪すぎる)

$ErrorActionPreference = "Stop"

$x = Get-ChildItem non-existent-path -ErrorAction SilentlyContinue
if (-not $?) {
Write-Host "File not found"
}

ダイナミックスコープの特性から、$ErrorActionPreference の変更の影響はその子スコープに限定される。

$ErrorActionPreference = "Continue"

function foo {
$ErrorActionPreference
}

function bar {
$ErrorActionPreference = "Stop"
foo
}

# 関数の中だけStopになる。
foo # => Continue
bar # => Stop
foo # => Continue


Splat

配列を展開してコマンドの各引数として渡すには、@配列変数名とする。

# lisp のapply。rubyのsplat記法

# コマンドを表示して実行する関数

function echo-and-invoke {
Write-Host ">>>" $args
$cmd = $args[0]
$params = $args[1..($args.Length)]
& $cmd @params
}

echo-and-invoke Write-Host test
echo-and-invoke Write-Host


コマンドレットが実行するスクリプトブロックのスコープ

例えば、以下の様な挙動をするコマンドレットについて、

これを再現する関数を作ることはできない。

$i = 0

1..3 | ForEach-Object { $i += $_ }
$i # => 6 (ブロックの実行結果が反映される)

# ForEach-Object を模倣

function PseudoEach {
Param($block)
Process {
. $block
# Write-Host "> $i"
}
}
$i = 0
1..3 | PseudoEach { $i += $_ }
$i # => 0 (ブロックの実行結果が親スコープに反映されない)

コマンドレットのスクリプトブロックは親スコープの変数を変更するが、関数はそれができない。

コマンドレットは、(C#など)別言語で実装した世界でスクリプトブロックを実行しているために、新たなスコープが生成されていないのではないかと予想する。

苦しいが、同じようなことをするには以下の様にするしかなさそう。(retをiにしてもいいが、恐らくより混乱する)

$i = 0

1..3 | PseudoEach { $i += $_
Set-Variable ret -Scope 1 -Value $i
}
$ret # => 6


はまるところ


  • 式の結果が出力されること、関数の出力はすべて戻り値になることを理解すること


  • @() で、大きな配列を作ってしまって、powershellは遅いと勘違いする


  • @()で囲むかどうかで配列かそうでないかが変わる。バージョン3.0は、$null や 1要素のオブジェクトにLengthプロパティや[]が付いたが、これはこれで混乱する。

  • batで実行したときは普通なのに、powershellから実行したコマンドの出力に余分な空行が出力される場面に遭遇した(要調査)

  • オブジェクトからプロパティを除くために、Select-Object -ExcludeProperty するときは * が必要

New-Object PSObject -Property @{foo = 1; bar = 2; baz = 3} |

Select-Object * -ExcludeProperty foo,bar
# ^^^ 指定したものから foo,bar を除外する指定となるため「*」が必要
=> baz
---
3


  • $ErrorActionPreference のデフォルト値が "Continue" なため、エラーがあってもスクリプトは継続して実行され、最後に実行されたコマンドの終了ステータスでスクリプトは終了する。

  • Group-Object の使い方が難しい

@(

New-Object PSObject -Property @{foo = 1; bar = 1; baz = 1}
New-Object PSObject -Property @{foo = 1; bar = 2; baz = 2}
New-Object PSObject -Property @{foo = 2; bar = 1; baz = 3}
New-Object PSObject -Property @{foo = 2; bar = 2; baz = 4}
New-Object PSObject -Property @{foo = 2; bar = 2; baz = 5}
) | Group-Object foo,bar | % {
# ここでの $_ は、GroupInfoオブジェクト
$_.Name # グループ化キーの文字列表現("1, 1" など)
$_.Values # グループ化キーの配列(@(1, 1)など)
"---"
# グループ化した値の配列
$_.Group | % {
$_ | Select-Object foo,bar,baz | Format-Table -AutoSize
}
}

-AsHashTable -AsString により、GroupInfoオブジェクトではなくハッシュでグループをループすることができる。下の例では、一見記述が短く扱いやすそうだが、powershellでは、ハッシュが扱いにくいのでこの -AsHashTable を覚える必要はないのではないか?と思う

(ハッシュが扱いにくいというのは、例えば下の例でSelect-Objectが使えないところなど)

@(

New-Object PSObject -Property @{foo = 1; bar = 1; baz = 1}
New-Object PSObject -Property @{foo = 1; bar = 2; baz = 2}
New-Object PSObject -Property @{foo = 2; bar = 1; baz = 3}
New-Object PSObject -Property @{foo = 2; bar = 2; baz = 4}
New-Object PSObject -Property @{foo = 2; bar = 2; baz = 5}
) | Group-Object foo,bar -AsHashTable -AsString | % {
$hash = $_
$hash.Keys | % {
$_
"---"
$hash.$_
}
}

グループのキー毎にループしたい場合は、-AsHashTableなし、グループ分けしたひとつのオブジェクトが必要な場合は、-AsHashTableありにすればいい?

ハッシュの配列をグループ化しようと思うと(ハッシュのキーはプロパティではないので)Group-Object foo,bazとしてもグループ化できない(エラーにもならないのでハマる)。

どうしても必要なら以下のように Group-Info { } で任意の式でのグループ化できるので、これを利用する。

@(

@{foo = 1; bar = 1; baz = 1}
@{foo = 1; bar = 2; baz = 2}
@{foo = 2; bar = 1; baz = 3}
@{foo = 2; bar = 2; baz = 4}
@{foo = 2; bar = 2; baz = 5}
) | Group-Object {$_.foo},{$_.bar} | % {
$_.Name
"---"
$_.Group | % {
$_.baz
}
}


  • 配列の妙(version 4 で確認)

関数のパラメータには配列がそのまま渡る

(このあたりの検証をするとき、オブジェクトに対する[0]の参照は自身を返すギミックがあることに注意しなければならない。(1)[0] # => 1)

Set-StrictMode -Version 3

function foo {
param($x)

$x[1] # => System.IndexOutOfRangeException が起こるので配列が渡っている
}

foo @(1)

パイプラインに1要素の配列を1つだけ渡すには、前置の,が必要

@(1).GetType().FullName # => System.Object[]

,@(1) | Get-Member # => System.Object[] (配列)
@(@(1)) | Get-Member # => System.Int32 (1が渡っている)

2要素以上なら配列が渡る

Set-StrictMode -Version 3

@(@(1),@(2)) | % { $_[1] } # (@(1)、@(2)が順に渡っているので2回System.IndexOutOfRangeExceptionが起こる)


  • $input は配列ではない

以下のようなことをしようとすると、意図しない空白が挟まれてしまう。これは、$inputが配列ではない何か特殊なオブジェクトであるためのようだ。

"a","b","c" | . { -join $input }

# => a b c

以下のように@()で配列にすることで意図通りの結果になる。

"a","b","c" | . { -join @($input) }

# => abc


powershellの良いところ(主観)


  • bat を使わなくて済む

  • OS(Windows)に標準でインストールされている

  • .Net のオブジェクトを扱える

  • Unixっぽい書き方(パイプ)ができる


powershellの嫌なところ(主観)


  • スクリプトの字面が汚い

  • コマンド名やオプション名が長い

  • 文字の大小の区別がないので、表記が揺れる

  • OSに標準で入っていることがpowershellの魅力なのにバージョンの変遷が大きいため、結局個別にインストールするか、過去のバージョンに合わせてスクリプトを修正するはめになる

  • UTF-8出力でBOMが付く

  • 配列の解釈がご都合主義(詳しく書く)

  • パス操作のコマンドレットがややこしく、使いにくい