PowerShellのswitch文についてまとめました。
動作環境
Windows 10
PowerShell 5.1
Visual Studio Code (拡張機能として ms-vscode.powershell を使用)
$PSVersionTable
Name Value
---- -----
PSVersion 5.1.15063.502
PSEdition Desktop
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0...}
BuildVersion 10.0.15063.502
CLRVersion 4.0.30319.42000
WSManStackVersion 3.0
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
用語の定義
switch文の解説を書いていくにあたり、用語を定義しておかないと伝わる自信がなかったので、
この記事内で使用する用語を下記の通りとします。
※ 正式な呼び方ではないです。
switch -Exact <# ここを「~オプション」 #> (<# switchに渡す値を「制御式」 #>) {
# この {} 内を「switch本体」
value <# ここのvalueを「case式」 #> {
# この {} 内を「case句」
}
default {
# この {} 内を「default句」
}
}
文法
まずは公式のシンタックスの解説( Get-Help about_Language_Keywords
)を引用。
※補完機能で入力するとオプション名はアッパーキャメルケースになるので、
この記事内でもそれに統一させました。
switch文には制御式をどう取得するかで2つのパターンがあります。
まずは、普通のパターン。
普通って何?
switch [-Regex|-Wildcard|-Exact][-CaseSensitive] ( pipeline )
{
<string>|<number>|<variable>|{ <expression> } {<statement list>}
<string>|<number>|<variable>|{ <expression> } {<statement list>}
...
default {<statement list>}
}
pipelineには変数、リテラル、そして、式が入ります。
foreachみないにコレクションを制御式として渡せるのもPowerShellならではの特徴ですね。
なぜ、valueではなく、pipelineと書くのかというと、
pipelineで指定した変数はswitch文内では$_という自動変数から参照できるようになるので、
その特性がpipelineと同様であることからこの表記を採用しているのではないかと思っています。(想定)
続きまして、ファイルから制御式を取得するパターン。
switch [-Regex|-Wildcard|-Exact][-CaseSensitive] -File filename
{
<string>|<number>|<variable>|{ <expression> } {<statement list>}
<string>|<number>|<variable>|{ <expression> } {<statement list>}
...
default {<statement list>}
}
こちらはファイルの内容を一行単位に制御式としてswitch本体に流していくようになります。
実際にはファイルの文字コードとかが指定できないので、
Get-Content
などで1行ごとの配列にしてからswitch文に渡した方が柔軟なので、
使用するケースはまずないのかと。
以上2つが、switch文のシンタックスとなります。
ただ、シンタックスを見ただけで「あぁーなるほど。」と理解できないのが、PowerShellの面白いところです。
では、実際にどう書けばいいのか、サンプルを通して解説していきます。
CASE句
基本
まずは、基本的な書き方から。
switch (2) {
1 {"One."}
2 {"Two."}
3 {"Three."}
default {"Not matched."}
}
# Two.
公式のシンタックスで見たとおり、他の言語によく見られるcaseキーワードは不要です。
また、breakを書いていませんが次のcase句は実行されていません。
ただ、これは場合によっては次のcase句も実行される可能性があります。
私もPowerShellを学びたての頃は
「え、じゃぁ自動でcase句の最後にbreakが付いてくれるってこと?(フォールスルー対策が不要?)」
と思っていましたが、これは間違いです。
一言でいうと、制御式はcase式の判定結果を問わず、全てのcase式に対して評価が働き、
評価がtrueとなった全てのcase句が実行されるようになります。
複数のcase式でtrueとなる場合
switch (2) {
1 {"One."}
2 {"Two."}
3 {"Three."}
2 {"Two. Two."}
default {"Not matched."}
}
# Two.
# Two. Two.
なので、フォールスルーの心配がいらないかと言われると、
必ずしもそうではない、ということになります。
default句が実行される場合
switch (2) {
1 {"One."}
3 {"Three."}
default {"Not matched."}
}
# Not matched.
どのcase式もtrueにならなければ、default句が実行されます。
なお、default句は必須ではないので、書いていなければ何も実行されずにswitch文が終了します。
breakによる中断
switch (2) {
1 {"One."}
2 {"Two."; break}
2 {"Two. Two."}
default {"Not matched."}
}
# Two.
breakキーワードを書くとそれ以降のcase式は評価されません。
case式がお互いに排他的(どの制御式であっても2つ以上のcase句が実行されるケースがない状態)であれば、
breakを書かなくても大丈夫そうですが、
どのみちbreakを書くことで短絡評価となりますので、パフォーマンスとしてはよくなります。
continueによる中断
switch (2) {
1 {"One."}
2 {"Two."; continue}
2 {"Two. Two."}
default {"Not matched."}
}
# Two.
continueキーワードを書いても制御式がスカラー型(単一のデータを取る値)であれば、
breakキーワードで書いた場合と挙動は変わりません。
foreach文で1要素しかないコレクションを回した場合と一緒、と考えてもらえればと思います。
ちょっと、文字の解説だと難しいので、詳細は以降のサンプルコードで。
制御式がコレクション型の場合
switch (2, 3) {
1 {"One."}
2 {"Two."}
3 {"Three."}
default {"Not matched."}
}
# Two.
# Three.
制御式がコレクションの場合の実行結果です。
処理の流れ的にはswitch本体が2回ループで実行された感じになります。
可読性はよくないですが、処理速度は速いので使い道はあります。
breakの挙動
switch (2, 3) {
1 {"One."}
2 {"Two."; break}
3 {"Three."}
default {"Not matched."}
}
# Two.
これは、制御式が2の時にbreakが実行されたので、
その後の制御式がswitch本体に渡されずに終了した結果となります。
break, continueはそもそもループを終了するか、今のループを終了し、次のループを実行するか、といった
制御になるので、switch文の制御式がコレクションの場合、ループでswitch本体が実行されると理解しておけば、
自然と頭に入るのかと思います。
continueの挙動
switch (2, 1) {
1 {"One."}
2 {"Two."; continue}
2 {"Two.Two."}
default {"Not matched."}
}
# Two.
# One.
2番目のcase句でcontinueしているため、3番目のcase句は実行されずに、
次のループが実行されます。
ラベルを使った制御
PowerShellではfor, foreach, while, do (while|until)でラベルが使用できますが、
switch文でも利用可能です。
公式ドキュメントには書いてないですが。。。
# ラベルは :ラベル名 で指定できます。
# ラベルとswitch文の最初の行は一行で書く必要があります。
:myLabel switch (1..5) {
default {
$i = $_
switch ("hello") {
"hello" {
Write-Output "${i}: $_"
if ($i -gt 3) {
continue myLabel
}
}
}
Write-Output "3以下はここも実行される。"
}
}
# 1: hello
# 3以下はここも実行される。
# 2: hello
# 3以下はここも実行される。
# 3: hello
# 3以下はここも実行される。
# 4: hello
# 5: hello
Qiitaのシンタックスハイライトで:(コロン)がエラー表示みたいになってますが、
ちゃんと動作することは検証済みです。
実際に使用されるケースはないと思うので、解説はここまで。
制御式の型
文字列
switch ("banana") {
"apple" {"リンゴ"}
"banana" {"バナナ"}
"Pineapple" {"パイナップル"}
default {"Not matched."}
}
# バナナ
string型を制御式に投入した例です。
大文字・小文字による判定
switch ("Banana") {
"banana" {"小文字バナナ"}
"Banana" {"最初だけ大文字バナナ"}
"BANANA" {"大文字バナナ"}
default {"Not matched."}
}
# 小文字バナナ
# 最初だけ大文字バナナ
# 大文字バナナ
デフォルトでは大文字・小文字の違いは考慮されません。
PowerShellでは基本ケースインセンシティブなので、お決まりの挙動となります。
配列
配列を配列(2次元配列)で制御式として渡した場合、
$_
には一次元配列が入ります。
switch (@(1, 2, 3), @(4, 5, 6)) {
{$_ -contains 1} {
$_ -join ", "
}
{$_ -contains 4} {
$_ -join "- "
}
}
# 1, 2, 3
# 4- 5- 6
サンプルとしては紹介しませんが、3次元配列以降も同じような挙動で展開されます。
$null
他の言語ではnullを指定するとヌルポが投げられることもありますが、
PowerShellの場合はエラーとはならず、しっかしとcase式で評価されます。
switch ($null) {
$null {
"Nullでは落ちない!"
}
}
# Nullでは落ちない!
case式が条件式の場合
$_自動変数を使うことで、PowerShellの比較演算子で制御することができます。
switch (2) {
{$_ -gt 2} {"2より大きい。"}
{$_ -eq 2} {"2である。"}
{$_ -in (1, 2, 3)} {"1, 2, 3のどれかである。"}
default {"Not matched."}
}
# 2である
# 1, 2, 3のどれかである。
case式が変数の場合
case式の値はリテラルだけではなく、変数でも指定できます。
$random = Get-Random -Minimum 0 -Maximum 2
0..2 | % {
switch ($_) {
$random {
"Matched!!"
}
default {
"Not matched."
}
}
}
# 3回に1回は「Matched!!」が出力されます。
# Not matched.
# Not matched.
# Matched!!
もっといえば、case式には式であれば何でも指定できます。
0..2 | % {
switch ($_) {
(Get-Random -Minimum 0 -Maximum 2) {
"Matched!!"
}
default {
"Not matched."
}
}
}
# 3分の1の確率で「Matched!!」が出力されます。
# Not matched.
# Matched!!
# Not matched.
試してみればもっといろんな書き方が出来ると思いますが、
不毛な気がするのでここまで。
各ループごとに実行される処理
敢えて紹介するほどでもないですが、
{$true}
のようにcase式を書けば、
その直前にbreak, continueが実行されない限り、
どの制御式でも実行される文を書くことができます。
switch (1, 2) {
{$true} {
"絶対に実行される。"
}
}
# 絶対に実行される。
# 絶対に実行される。
これにより、本来switch文ではcase句かdefault句にしか処理が書けませんが、
switch本体で各ループごとに絶対に実行したい文があれば、
こういう書き方もやろうと思えばできます。
オプションによる挙動
一般的な方法(Exact)で評価する
# RegexとWildcardオプションと一緒には使用できない
switch -Exact ("Banana") {
"banana" {"小文字バナナ"}
"Banana" {"最初だけ大文字バナナ"}
"BANANA" {"大文字バナナ"}
default {"Not matched."}
}
# 小文字バナナ
# 最初だけ大文字バナナ
# 大文字バナナ
「大文字・小文字による判定」と同じような結果になりましたが、
これはswitch文はデフォルトでExactオプションが付くようになるためです。
また、Regex、Wildcard、Exactの3つのオプションは複数指定してもエラーとはなりませんが、
適用されるのは一番最後に指定したオプションとなります。
オプションの省略記法
ちなみにこれから紹介するオプションもそうですが、これらは省略して書くことが可能です。
# Exactはe, Wildcardはw, Regexはr, CaseSensitiveはc, Fileはfに省略可能。
switch -e ("Banana") {
"banana" {"小文字バナナ"}
"Banana" {"最初だけ大文字バナナ"}
"BANANA" {"大文字バナナ"}
default {"Not matched."}
}
# 小文字バナナ
# 最初だけ大文字バナナ
# 大文字バナナ
ただ、PowerShellを慣れてないと分かりにくくなってしまうので、
できれば省略せずに書いたほうがいいというのが個人的な意見です。
なお、この省略ルールはswitch文特有のルールではなく、
PowerShell全体でオプションを解釈する際に先頭一致で見るようになっているためです。
なので、-e, -ex, -exa, -exac, -exactのどれでも使用可能となります。
ケースセンシティブ(CaseSensitive)で判定する
-CaseSensitiveを付けると、大文字・小文字を区別します。
制御式が文字列でない場合は無視されます。
なお、以降に紹介するワイルドカード、正規表現内でも、
-CaseSensitiveを指定しないと、大文字・小文字の区別をしてくれません。
# -c でも同様
# -Exact, -Wildcard, -Regexと併用して用いることが可能。
switch -CaseSensitive ("Banana") {
"banana" {"小文字バナナ"}
"Banana" {"最初だけ大文字バナナ"}
"BANANA" {"大文字バナナ"}
default {"Not matched."}
}
# 最初だけ大文字バナナ
ワイルドカード(Wildcard)で判定する
ワイルドカードで一致させるように指定します。
-like演算子と間違わないように、注意してください。
# -w でも同様
switch -Wildcard ("banana") {
"ba?a?a" {"任意の一文字"}
"ba*" {"0文字以上の任意の文字列"}
"[ab][ab][an]a[a-z]a" {"括弧内に含まれる一文字"}
default {"Not matched."}
}
# 任意の一文字
# 0文字以上の任意の文字列
# 括弧内に含まれる一文字
ワイルドカードの詳しい書き方はこちら。
正規表現(Regex)で判定する
正規表現で一致させるように指定します。
これも-match演算子と間違わないように、注意してください。
ちなみに、$Matches自動変数には-match演算子を使用した場合と同様、
正規表現で一致した値が入ります。
# -r でも同様
switch -Regex -CaseSensitive ("baNANA") {
"NA" {"「NA」が含まれる。"}
"BAnana" {"大文字・小文字が逆のため、不一致。"}
"^[a-z]{2}NA[A-Z].$" {"paNAMAでも一致する。"}
default {"Not matched."}
}
# 「NA」が含まれる。
# paNAMAでも一致する。
$Matches
# Name Value
# ---- -----
# 0 baNANA
正規表現の詳しい書き方はこちら。
ファイルから制御式を指定する
Get-Content -Encoding UTF8 C:\tmp\switch_banana.txt
# バナナ
# バナナマン
# バナー
# -f でも同様
switch -File C:\tmp\switch_banana.txt {
"バナナ" {"banana"}
"バナナマン" {"bananaman"}
"バナー" {"banner"}
default {"Not matched."}
}
# banana
# bananaman
# banner
-Fileに渡すファイルパスの文字列は"(ダブルクォーテーション)で囲まなくても実行できます。
もっと言うと、$ @ ' " (
のいずれかで始まる文字でなければ "
で囲まなくても文字列として解釈されます。
これもswitch文特有ではなく、PowerShellのパース仕様と同じです。
また、制御式として2バイト文字を判定に用いる場合、UTF-8でエンコードする必要があります。
仮に、エンコードがSJISとかで実行すると、文字化けして3行とも Not matched. となります。
switch文のFileオプションで読み込むファイルはエンコードの種類が柔軟に対応できないので、
回避策としては以下のように Get-Content
で取得した結果を制御式として渡す方法があります。
# SJISのテキストファイルを読み込む
switch (Get-Content -Encoding String C:\tmp\switch_banana.txt) {
"バナナ" {"banana"}
"バナナマン" {"bananaman"}
"バナー" {"banner"}
default {"Not matched."}
}
# banana
# bananaman
# banner
ワークフロー内で実行する
もともと制約の多いワークフローですが、
switch文においてもワークフロー内で書く場合は制約があります。
ただ、全てのパターンの制約は把握しきれなかったのですが、
オプションだとCaseSensitiveのみしかサポートされてないようです。
In a Windows PowerShell Workflow, the switch statement supports only the 'CaseSensitive' flag.
ちなみに上記の制約はオプション付けるとしたらではなく、どうやら必須オプションとして
CaseSensitiveをつけてください、という意味だそうです。
[PowerShell] Case-insensitive switch statements are not supported in a Windows PowerShell Workflow. Supply the -CaseSensitive flag, and ensure that case clauses are written appropriately. To write a case-insensitive case statement, first convert the input to either uppercase or lowercase, and update the case clauses to match.
また、ワークフローと言えば、Parallelオプションの指定によって出来る並列処理が魅力ですが、
switch文の場合、上記の制約によりエラーとなってしまいました。
ただ、補完機能で候補として出てくるので非常に紛らわしいです。
さらに、戻り値もスカラーである必要があります。
ただ、breakもcontinueもワークフロー内では使用できないので、
複数のcase句が実行されないように注意する必要があります。
In a Windows PowerShell Workflow, switch statements support only expressions that return a single element.
workflow Invoke-Switch {
param(
$value
)
switch -CaseSensitive ($value) {
"test" {
Write-Output $value
}
default {
Write-Output "Not matched."
}
}
}
Invoke-Switch -value test
# test
変数のスコープ
switch文では、基本スコープの境界はないと思ってもらって大丈夫です。
{}が多い構文なので、スコープの制御があやふやになりがちですが、
しっかりと抑えていきましょう。
case句内で変数宣言
switch (1) {
0 {}
1 {$a = "hello"}
}
$a
# hello
PowerShellではif文やfor文も同様ですが、制御文によるブロックでは
新たにスコープが分けられることはありません。
ですので、case句内で新たに宣言した変数であってもswitch文の外でも使用することができます。
case句内で変数の上書き
$a = "hello"
switch (1) {
0 {}
1 {$a = "world"}
}
$a
# world
また、上書きも可能です。
入れ子によるスコープ
$a = "hello"
switch (1) {
0 {}
1 {
switch ($_) {
0 {}
1 {$a = "world"}
}
}
}
$a
# world
switch文は入れ子にしてもスコープが分けられることはないようです。
入れ子の上限とかは調べても載ってなかったのですが、
おそらく、実行環境のリソースに依存とかだと思います。
$_ 自動変数
制御式の値を参照する際に使用する $_
ですが
実は上書き可能でしかも、上書きされた状態はswitch本体全てに反映されます。
よって、以下のような書き方が可能です。
switch (1) {
1 {
$_++
}
2 {
$_++
}
3{
$_
}
}
# 3
もちろん、使用非推奨です。
戻り値
switch文の戻り値は関数と同じような挙動となり、
戻り値がない場合は $null、
1つの場合は スカラ値、
2つ以上の場合は 配列型 となります。
変数に代入する
$ret = switch (2) {
1 {"One."}
2 {"Two."}
2 {"Two. Two."}
default {"Not matched."}
}
$ret
# Two.
# Two. Two.
$ret.GetType()
# IsPublic IsSerial Name BaseType
# -------- -------- ---- --------
# True True Object[] System.Array
なお、サンプルのように、switch文から直接変数に代入することが可能です。
return文で返す
ただ、上のサンプルを見て
「PowerShellのswitch文は実はswitch式なのでは?」
と思ってしまいそうですが、これは間違いで、
PowerShellのswitch文は式ではなく、歴とした文です。
例えば、switch式であれば、以下のように書けそうですが、これはエラーとなります。
# return文の後には書けない。
function Do-Switch() {
return switch (2) {
1 {"One."}
2 {"Two."}
3 {"Three."}
default {"Not matched."}
}
}
# 式またはステートメントのトークン '{' を使用できません。
よって、switch文全体を部分式演算子である$()で囲む必要があります。
一見、()だけでもよさそうですが、()だと中で文を指定することができないので、
やはり、文もまとめて値に変換できる部分式演算子でないと通りません。
# $()で囲うと正常に実行できる。()だけだとエラー。
function Do-Switch() {
return $(switch (2) {
1 {"One."}
2 {"Two."}
3 {"Three."}
default {"Not matched."}
})
}
Do-Switch
# Two.
ちょっと不気味な書き方ですが、これまでの解説を理解していただければ、
ご納得いただけるかと思います。
エディターの補完入力
PowerShell ISEやVisual Studio Codeでスクリプティングをする場合、
エディタが備えるスニペットを利用して、switch文を書くことが出来ます。
※Visual Studio Codeの場合、拡張機能としてPowerShellを利用することを前提とします。
PowerShell ISEの場合は[Ctrl + J] -> [s] -> Enterでswitch文のスニペットが書き込まれます。
switch ($x)
{
'value1' {}
{$_ -in 'A','B','C'} {}
'value3' {}
Default {}
}
Visual Studio Codeの場合は「swi」まで入力して[Ctrl + Space]、もしくは、[Tab]で書き込まれます。
switch ($x) {
condition { }
Default {}
}
また、「ex-sw」で、ISEと同じスニペットも利用できるようです。
switch ($x)
{
'value1' { }
{$_ -in 'A','B','C'} {}
'value3' {}
Default {}
}
まだ、switch文の書き方に慣れてない方には便利ですが、
この記事を熟読していただいた方は一から書いた方が速そうです。
サンプル集
以下、switch文を使わない書き方と使った書き方を比べるためのサンプルコードです。
FizzBuzz
コードを整形しているので、switch文で書いた方が僅かに簡潔そうに見えますが、
整形していないと、case式とcase句が混同しそうです。
1..100 | % {
if ($_ % 3 -eq 0) {
$tmp += "Fizz"
}
if ($_ % 5 -eq 0) {
$tmp += "Buzz"
}
if ($tmp -ne $null) {
$tmp
$tmp = $null
}
}
switch (1..100) {
{$_ % 3 -eq 0} {
$tmp += "Fizz"
}
{$_ % 5 -eq 0} {
$tmp += "Buzz"
}
{$tmp -ne $null} {
$tmp
$tmp = $null
}
}
九九表
これは2重ループになるので、無理してswitch文で書くメリットはなさそうです。
switch_99.ps1では番号付きスコープで取得できるよう、内側のswitch文をスクリプトブロックで囲み、
Get-Variableで外側のループ変数$_
を取得しています。
単に内側のswitch文の前に$i = $_
と書けば済む話なんですが、
サンプルなので、敢えてこういう書き方にしました。
foreach ($i in 1..9) {
foreach ($j in 1..9) {
Write-Host -NoNewline ("{0, 3}" -f ($i * $j))
}
Write-Host
}
switch (1..9) {
default {
& {
switch (1..9) {
default {
Write-Host -NoNewline ("{0, 3}" -f ((Get-Variable -Name _ -Scope 1).Value * $_))
}
}
Write-Host
}
}
}