2017/12/24

この記事には「 独自研究 」に基づいた記述が含まれているおそれがあります。

この記事はPowerShell Advent Calendar 2017の24日目です。

一般的なプログラミング言語では、文(statement)と式(expression)の違いは、値を返すのが式で、返さないのが文、という説明がされることが多いと思います。しかし、PowerShellではこの説明は成り立たたず、文が値を返したりしてるように見えて良く分かりません。そこでPowerShellにおける文と式とはそもそも何なのかということを、仕様書(PowerShell 3.0のものですが)やAST(ShowPSAstモジュールが便利!)を眺めながら考えてみたので軽くまとめようと思います。

文(statement)と式(expression)の定義

PowerShellでは言語要素として、文(statement)と式(expression)が明確に定義されています。すなわち、言語要素の何が文であって、何が式であるかという定義は仕様できちんと決まっていて、ある言語要素が、状況によって文になったり式になったりと変化する、ということはありません。

パイプライン、代入、ifやforやfunctionなどは文です。

変数、数値/文字リテラル、オブジェクトのメンバ呼び出し、スクリプトブロック、単項または二項算術演算子で構成される式、カンマ演算子で構成される配列などは式です。

ただ、仕様書での定義と、実際に構築されるASTに齟齬があることはあります。

例えば「$a=1」のような代入については、ASTではAssignmentStatementAstとなります。一方、言語仕様上はassignment-expressionと書かれています。厳密には、言語仕様書のgrammer節によれば、assignment-expressionはexpressionではなくpipelineであるということになっています(お前は何を言っているんだ)。いずれにせよパイプラインは文であるので、AST通り、代入は文であるという解釈で良いと思います。

※仕様書にはassignment expressionとはっきり書いてあるんだから代入は式だろ!という意見を否定するものではないです。が、代入は文であると考えたほうが他の文法と整合性を取りやすいので、そういう立場をとりました。

しばたさんが9日目に書かれた記事で取り上げられているように、配列に関しても同様の齟齬があります。いずれにせよ「1,2,3」のような配列は、式と考えて良いと思います。

文と式の構造

PowerShellには文と式が存在することは分かりました。では文と式は何が違うのか? それを考察するために、具体的にいくつかの文と式の構造を取り上げて見ていきます。

パイプライン(文)

パイプラインといえば「Get-Process | Where-Object Handles -gt 100 | Select-Object ProcessName」みたいな|で繋ぐやつのことでしょ、と思われがちですが、言語仕様上は以下のようなものはすべてパイプラインです。

Get-Process -Name PowerShell # @コマンド1つだけ
1 # A数値リテラルだけ
1 + 1  # B算術演算子で構成される算術式
$a = 1 # C代入
gps | where Handles -gt 100 | select ProcessName # Dパイプ記号でコマンドを連結したもの
1 + 1 | Write-Host # E算術式をパイプラインでコマンドと繋げたもの

要はパイプラインというのは、一般的なプログラミング言語で、;で終わる一文に相当するものと考えればだいたい間違いないと思います。ただしPowerShellだと文末の;は必須ではなく、改行でもOKです。

パイプラインは以下のような構造を取ります。[]は省略可能を意味します。

パイプライン要素1 [ | パイプライン要素2] [ | パイプライン要素3] ...

または、

代入文

すなわち、代入文(後述)を除くパイプラインは1個以上のパイプライン要素から構成されており、複数のパイプライン要素が存在する場合はパイプ演算子|で連結されます。

パイプライン要素には式とコマンド(Get-Processとか)が存在します。ただし式は1つ目のパイプライン要素にのみ許可されます。

以上を踏まえると、@、Dはコマンドのみで構成されるパイプライン、A、Bは単独の式のみで構成されるパイプライン、Eは1つの式とコマンドで構成されるパイプライン、Cは代入文であることが分かります。

代入文

代入文はパイプラインなので、文です。代入文は以下のような構造を取ります。(ここでは+=などの複合代入演算子については省略)

式 = 文

ただし、左辺の式は、変数やプロパティなど、代入が可能な式である必要があります。

よって以下のような記述が可能です。

$a = $b # @変数
$a = 1 + 1 # A算術式
$a = Get-Process # Bパイプライン(コマンド1つ)
$a = gps | where Handles -gt 100 # Cパイプライン(コマンド複数)
$a = if($true){"a"}else{"b"} # Dif文
$a = $b = 1 # E代入文の結果を更に代入

言語仕様上、代入文の右辺には文であれば何でも書けるのですが、実際に代入が行われるのは、パイプライン、if文、for文、switch文といった、パイプラインに値を出力する文と代入文に限られます。ちなみにDのようにパイプラインと代入文以外の文を右辺に指定できるようになったのは、PowerShell2.0からです。

ところで上記@やAは右辺が式です。代入文の右辺は文じゃなかったの?と思われると思いますが、パイプラインの節で述べた通り、単独の式もパイプラインであり、パイプラインは文なので、三段論法でいくと式は文として扱われることになります。

※AST上では$a = $bの右辺はPipelineAst/CommandExpressionAst/VariableExpressionAstではなく、いきなりCommandExpressionAst/VariableExpressionAstとなっているので、この説明はASTの実装とはかみ合わないかもしれません。AssignmentStatementAst.Rightは確かにStatementAstを取るのですが、CommandExpressionAstはStatementAstから派生しているクラスなので、式の代入は問題なく行えます。

代入文は上記Eのようなことができることから分かる通り、値を返す文ですが、パイプラインには値を出力しません。値は返すがパイプライン出力がないものは、インクリメント演算子で構成される式($a++等)も同様です。

if文

パイプラインと代入文以外の文は色々あるわけですが、代表的なものとしてif文を取り上げます。一番シンプルなif文はこういう構造です。

if (パイプライン) {
    文1
    文2
....}

おそらく多くの人が誤解しているのではないかと思いますが、条件節に書くのは式ではなくパイプラインです。パイプラインを実行した結果、出力値がtrue、またはboolに型変換してtrueになる場合に、ブロック内の複数の文が実行されます。

よって以下のような記述が可能です。

if ($true) {} # @変数
if ($a -eq 1) {} # A論理演算子で構成された式
if ("a.txt" | Test-Path) {} # Bパイプライン
if ($a = 1) {} # C代入文

@とAは普通の書き方ですが、実際には、1つの式のみ有するパイプラインを実行し、出力される値が判定されています。

条件節はパイプラインなので、当然Bのような書き方もできるわけです。また、代入文もパイプラインであるので、Cの書き方もできてしまい、注意を要します。

条件節に指定できるのはパイプラインだけで他の文は許容されないので、
if (if($true){}){}
というような書き方はできません。

※といっても実はこう書くと文法上はvalidであり、条件節内は「ifコマンド、パラメータ値1($true)、パラメータ値2(スクリプトブロック)」という解釈になってしまいます。

また、条件節にはパイプラインは1つのみ指定可能で、複数文を書くことはできないので、
if ($a;$b) {}
という書き方はできません。(パーサーもエラーを出す)

丸括弧式

さて、普通のプログラミング言語だと、()はグループ化や演算子の優先順を変更するのに用いられるものの、別に文法そのものに影響を与えるものではないと思います。多くの場合、ASTでも()の情報はそぎ落とされます。

ところがPowerShellでの丸括弧()は文法的な意味を有しており、ASTにもParenExpressionAstとして存在する、立派な式です。丸括弧式の構造は以下の通りです。

(パイプライン)

これは要するに、「パイプラインに()を付けると式になる」、ということです。()内のパイプラインで出力された値が返される式となります。具体的にどういうところで使うのかを示します。

2 * (1 + 3) # @数値演算の優先順を変更する
($a = 1) # A値を返すがパイプラインには出力しない代入文の値を出力させる
$a[(Get-Hoge)] # B式は許容するがパイプラインは許容しない構文で、式に変換する
Get-Process -Name (Get-Hoge) # Cコマンドのパラメータにコマンド実行結果を指定する

@の使い方は普通です。ただし、()はパイプラインを生成するので、「(1+3)」は「1つの式のみ有するパイプラインを実行し、パイプラインに値を出力し、その値を返す」という見た目より複雑な処理になります。

※少なくともAST上はそうなりますが、実際は何らかの最適化処理が入ってる可能性はあります。

代入を重ねる場合にはAのような書き方は必要ないのですが、代入した結果をパイプラインの出力としたい場合は()を付ける必要があります。この場合、$aに1が代入され、コンソールにも1が出力されます。

Bで挙げている、式を許容するがパイプラインは許容しない言語要素というのは実はあまりないです。前述した通りif文の条件節は式じゃなくて、パイプラインを取るといった案配です。ただ、たとえば配列や連想配列の要素を取得するインデックス演算子[]は、式のみ許容されます。なのでコマンドなどのパイプラインの出力値を指定したい場合は()が必須となるわけです。

ちなみに、()内にはパイプライン以外の文(if文等)は指定できません。また、複数のパイプラインも指定できず、あくまで1つだけです。

※任意の文あるいは複数の文を式としたい場合には、部分式演算子$()または@()を用います。両者とも内部の文がパイプラインに出力した値を返す、「部分式」となります。両者とも複数値が出力されると配列になりますが、@()は出力値が1つでも要素数1の配列を返す点が異なります。

まとめ?

PowerShellの文と式は厳密に定義されています。文は複数の文と式で構成されるし、式は複数の文と式で構成されています。文や式の構成要素が取る文や式の種類についても、各々、きちんと定義されています。

ただし、PowerShellにおいて「値を返すか返さないか」、「パイプラインに出力されるかされないか」、「式であるか文であるか」という概念はすべて独立しています。そのため、PowerShellの文とは何である、式とは何である、ということを一言で説明することは難しいんじゃないかと思います。

なので、本記事でこれまで述べてきたとおり、「パイプラインは文で、要素として式やコマンドを取りますよ」とか、「ifは文で、条件節にはパイプラインを取りますよ」みたいな、各論でしか表現できないのではないかなぁと、私はそういう結論に至りました。

しかしここまで書いてちゃぶ台をひっくり返すようなことを言いますが、ある言語要素が文であるか式であるか、ここまで仕様書を読んだりASTを追ったりして把握するのは、まあ楽しくなくはないですが、知らなくても別に大丈夫だと思われます。別に、ifの条件節には条件式を書くのだと理解していても不都合は特にないかと。

効用があるとすれば、例えば「if ((Test-Path a.txt)) {}」とか「foreach ($i in (1..5)) {}」とかの、余分な()を取り除くのには文法の知識が役立ちます。それもまぁ、心配だから怪しい所には常に()付けておく or 何か変だったら()付けてみる とかでもそれ程問題にはならないかもしれません。

2015/12/04

この記事はPowerShell Advent Calendar 2015の4日目の記事です。

はじめに

今回はPowerShellでWebページのスクレイピングをする際の、ちょっとしたノウハウ集を前後編に分けて紹介したいと思います。

スクレイピングというのは、Webページから文字列を取ってきて、スクリプトから利用可能な形に加工する処理です。昨今は多くのWebサイトやサービスでWeb APIが公開されていて、スクレイピングをせずとも比較的簡単にデータを取得できます。PowerShellだとInvoke-RestMethodコマンドレット等が使えます(その話はまた次回とかにやります)。

しかし現実には、APIが公開されていない等の理由で、HTMLを取ってきて自前で解釈せざるを得ないケースが多々あります。さて、PowerShellではどうやりましょうか、というのが今回の話。様々な方々によってもう色々と語られている分野ではあるのですが、結構細かいハマりどころがあるのでちょっとまとめてみようと思いました。

前編ではまず、Webページからの文字列の取得方法ついてまとめます。

なお、スクレイピングには技術的な問題以外の、微妙な問題(著作権の問題とか、Webサイトへの攻撃と見なされる可能性とか)を含むものなので、その辺りは各自どうかご留意ください。この辺りの話はPowerShellに限った問題ではないので、ここでは詳説いたしません。参考記事:Webスクレイピングの注意事項一覧 - Qiita

Invoke-WebRequestコマンドレットで文字列を取得する

PowerShellでのスクレイピング、基本は何はなくともInvoke-WebRequestコマンドレットです。ただしこのコマンドレットはPowerShell 3.0で追加されたものなので、2.0環境にはないことに注意です。その場合は.NETのWebClientクラス等を使う方法があり、後で述べます。

基本は、

$response = Invoke-WebRequest -Uri "http://winscript.jp/"

のように、Invoke-WebRequestコマンドレットを実行する、だけです。「-Uri」は省略可能です。

このとき$responseにはHtmlWebResponseObjectオブジェクトが格納されています。このうち、指定URLのWebページに含まれているHTMLなどの文字列データは、Contentプロパティに格納されます。つまり、$response.Content に欲しいデータが格納されているので、あとはそれをよしなに利用すればいいわけです。

実はInvoke-WebRequestは、文字列データを取得すると同時に、HTMLの場合はパースしてタグの構造をオブジェクト化までしてくれます。が、それについては次回。

なお、Invoke-WebRequestコマンドレットでは文字列を取得する他、バイナリデータをダウンロードしてファイルとして保存する機能もあります。それについては過去記事をご参照ください。

リクエストにパラメータを付与する(GET)

GETメソッドを用いてクエリを指定する場合、要はhttps://www.google.co.jp/search?q=PowerShell のようなURLのデータを取得する場合は、Invoke-WebRequest "https://www.google.co.jp/search?q=PowerShell" のようにQueryStringを含んだURLをそのまま指定するだけでOKです。

ただし、動的にクエリを組み立てる場合は、URIエンコード(URIエスケープ)を考慮する必要があります。もっとも簡単なのは

$searchWord = "PowerShell 配列"
$response = Invoke-WebRequest "https://www.google.co.jp/search?q=$([Uri]::EscapeDataString($searchWord))"

のように、Uri.EscapeDataStringメソッドを使う方法かと思います。

リクエストにパラメータを付与する(POST)

POSTメソッドでリクエストボディにパラメータを付与するには、Invoke-WebRequestコマンドレットの-Methodパラメータに"Post"を指定し、-Bodyパラメータにリクエストボディに付与するデータを連想配列で指定します。

たとえばブログのトラックバックを手動で撃つにはこんな感じでいけます。

$body = @{title="テスト";url="http://example.com/";excerpt="テスト";blog_name="test"}
Invoke-WebRequest http://ご自分のブログのトラックバックpingURL -Method POST -Body $body

なお、リクエストボディに含めるパラメータの各値(連想配列の値)は、自動でURIエンコードしてくれます。

(12/16追記)
また、-Bodyには連想配列のみならず、任意の文字列(URIエンコード要)やバイト配列(バイナリを送信する場合)を指定することも可能です。

標準認証が必要なページを取得する

ページの取得に標準認証が必要な場合は、-Credentialパラメータにユーザー名とパスワードを指定したPSCredentialオブジェクトを指定すればOKです。

セキュリティのことは取りあえず置いておき、簡易的にスクリプトに生パスワードを直書きしてもいいかな、という場合には以下のように書くことができます。

$userName = "user"
$password = "pass"
$credential = New-Object PSCredential $userName, (ConvertTo-SecureString $password -AsPlainText -Force)
Invoke-WebRequest 認証が必要なページのURL -Credential $credential

しかしこの方法はもちろんお勧めできないので、スクリプトとして保存する場合は通常はパスワードを暗号化しておきます。

まず、Get-Credential ユーザー名 | Export-Clixml cred.xmlを、スクリプトを実行するコンピュータ上で、スクリプトを実行するアカウントと同じアカウントで実行します。パスワードを入力するダイアログが出るので、Webサイトにログオンする際のパスワードを入力します。すると、ユーザー名と暗号化されたパスワードがcred.xmlに出力されます。

スクリプトからは

$credential = Import-Clixml cred.xml
Invoke-WebRequest 認証が必要なページのURL -Credential $credential

のようにすると、cred.xmlからユーザー名と復号したパスワードを、そのまま-Credentialパラメータに渡すことが可能です。

なおcred.xmlに含まれる暗号化パスワードは、ConvertFrom-SecureStringコマンドレットと同様、Windows Data Protection API(DPAPI)を用いてWindowsアカウントのパスワードをキーに利用して暗号化されているので、他のユーザーが復号することはできません。

ちなみに同一スクリプトファイルに暗号化パスワードを含めておくこともできなくはないです。過去記事参照。あと本当は資格情報マネージャーを使うのがいいんですが、…略。参考:PowerShell で Windows の 資格情報マネージャー を利用する (Jenkins などでの Git Credentialなど) - tech.guitarrapc.com

セッション情報を引き継ぐ

多くのWebアプリケーションは、同一クライアントからの連続したアクセスを、セッションという単位で管理します。

サーバーはクライアント(普通はWebブラウザ)の初回アクセス時にセッションIDを含むcookieを返し、クライアントからの2回目のアクセス時に、サーバーはcookieにセッションIDが含まれているかどうかを確認し、同一クライアントからのアクセスかどうかを判断するわけです。(ざっくりした説明ですが)

WebブラウザではなくInvoke-WebRequestを使ったアクセスでも同様に、以下のようにすれば受けとったcookie等のセッション情報を次回アクセスに引き継ぐことができます。

$url = "https://ログオンが必要なサイト"
$body = @{リクエストボディ(例えばユーザー名とかパスワードとか)}
$response = Invoke-WebRequest $url -SessionVariable sv -Method POST -Body $body
Invoke-WebRequest $url -WebSession $sv

初回アクセス時に-SessionVariableパラメータに指定した変数名(sv)の変数($sv)にはWebRequestSessionオブジェクトが格納されます。この中に、サーバーから受け取ったcookie等の情報が格納されています。

次回アクセス時には、-WebSessionパラメータに、初回アクセス時に得られたWebRequestSessionオブジェクト($sv)を指定します。

さて、実際のWebアプリケーションではcookie以外にも、Formのhiddenフィールドの値などもセッション管理に用いていることがあります。その場合は、初回アクセスのレスポンスからFormに含まれるinput type="hidden"なフィールドを抽出し、次回アクセスのリクエストボディに含ませる必要が出てきます。この辺りの話は後編で述べるパースが必須になってくる(し、長くなる)ので今回は詳説しません。Invoke-WebRequestコマンドレットのリファレンスのExample2に、Facebookにログオンする例なんてのがあるので、そちらで雰囲気をつかんでください。(今でも動作するかは確認してないですが)

エラートラップ

さて、Invoke-WebRequestは、タイムアウトになった、名前解決ができなかった、ページが無かった(404エラー)等々、正常にWebページを取得できなかった場合は、System.Net.WebExceptionというエラーを出します。

コマンドレットの出すエラー(Errorストリームに出力されるErrorRecord)は、try...catchステートメントでは捕捉できない、というのが原則ですが、Invoke-WebRequestコマンドレットのエラーは一般的なコマンドレットと異なり、普通の.NETの例外(System.Net.WebException)なので、try...catchステートメントでエラートラップを行います。

とは言え、Invoke-WebRequestコマンドレットの仕様上、エラートラップをして適切な処理を行うのは非常にめんどいです。何故かというと、Invoke-WebRequestがエラーを出した時点で、HtmlWebResponseObjectオブジェクトの出力は行われないので、このオブジェクトから得られる様々な情報(レスポンス文字列、ステータスコード等々)が取得できないからです。

じゃあどうすればいいのかという話なんですけど、どうもWebExceptionオブジェクトのResponseプロパティを見るしかないようです。具体的にはこんな感じ。

try
{
    $response = Invoke-WebRequest http://存在しないページなど
}
catch [System.Net.WebException]
{
    # HTTPステータスコード取得
    $statusCode = $_.Exception.Response.StatusCode.value__

    # レスポンス文字列取得
    $stream = $_.Exception.Response.GetResponseStream()
    $reader = New-Object System.IO.StreamReader $stream
    $reader.BaseStream.Position = 0
    $reader.DiscardBufferedData()
    $responseBody = $reader.ReadToEnd()
}

せっかくInvoke-WebRequestコマンドレットは、生のレスポンスを利用しやすくHtmlWebResponseObjectという形で返してくれるのに、エラー発生時はその恩恵を受けることができず、泥臭い処理が必要になります。これはかなりいけてないですし、どうせここまで書かないといけないのであれば最初からWebClientクラスを使った方がいいと思います。

httpsで無効な証明書が使われている場合

(12/16追記)
Invoke-WebRequestコマンドレット(およびWebClient)では、httpsで始まるURLからもダウンロード可能ですが、サイトで用いられている証明書に問題がある場合(期限が切れている、暗号化形式に問題がある、いわゆるオレオレ証明書である等)には、「要求は中止されました。SSL/TLSセキュリティで保護されているチャネルを作成できませんでした」というエラーが出てしまいます。

これを回避するには、Invoke-WebRequestコマンドレット実行前に、

[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}

という1文を記述しておきます。

ただし、証明書に問題があるということは、その通信相手が正当かどうか、通信内容が正しく秘匿されているかどうか、保証がされなくなるということですから、その点は念頭においてください。

文字化けの問題

Invoke-WebRequestコマンドレットのもう一つの悩ましい問題、それは文字コードです。実はInvoke-WebRequestコマンドレットには、Webページの文字コードを指定する方法がありません。(多分)

ではレスポンス文字列の文字コードがどのように決まるかというと、サーバーが返すレスポンスヘッダのContent-Typeフィールドで指定されているcharsetです。具体的には、$response.Headers["Content-Type"]の値が例えば"text/html; charset=UTF-8"であれば、$response.Contentの文字コードはUTF-8になります。

このときページ(HTML)を記述している文字コードと、レスポンスヘッダで指定されている文字コードが一致すれば全く問題はないのですが、異なる場合は容赦なく文字化けします。

異なる場合だけでなく、レスポンスヘッダのContent-Typeフィールドに文字コードの指定がない場合はASCIIと見なされるので、日本語のページの場合はやはり文字化けします。

この問題を回避する方法は、私はまだ見つけていません。よって文字化けが起きる場合は、諦めてWebClientを使って文字コードを指定するようにしています…。

WebClientを用いる

以上で述べてきたとおり、Invoke-WebRequestコマンドレットは、ページをさくっと取得して、さくっとパースするのには重宝するのですが、細かい所で融通が利かない印象があります。

そこで細かい処理が必要な場合(と、PowerShell 2.0環境)は、素直にWebClientクラスを用いるのがいいと思います。今回WebClientの使い方も入れようかと思いましたが、長くなったので詳しくは省略します。

基本は以下のような感じでDownloadStringメソッドを使って文字列を取得します。文字コードも指定できます。

$client = New-Object System.Net.WebClient
$client.Encoding = [System.Text.Encoding]::UTF8
$content = $client.DownloadString("http://アドレス")

なお、WebClientを用いた場合でも、Invoke-WebRequestと同等のHTMLパースを行う方法は存在するので、それは次回に。

おわりに

今回はまず、Webページから文字列データを取得する部分にフォーカスしてみました。といっても、Invoke-WebRequestの機能を全部網羅したわけではなく、使用頻度が高そうなものと個人的ハマリポイントがあるところだけです。なので詳しくはリファレンスを見て下さい。というかハマリポイントたぶんまだまだ一杯あると思います。

後編では、とってきた文字列データを「パース」して、扱いやすいデータ形式に変換する方法についてまとめようかと思います。

2014/05/02

また続くかどうか不明の新シリーズ。今日書いたワンライナーを記録していきます。

今回は、配列に含まれる要素のうち、もっとも出現頻度の多いものを調べる方法です。たとえば、0, 0, 0, 1, 0, 3, 0, 2, 0, 2という配列がある場合、「0」は6個含まれており最も要素数が多いので、この場合「0」を出力します。

ワンライナーは以下のようになります。(配列変数の宣言を合わせると2ラインですけど)

$array = 0, 0, 0, 1, 0, 3, 0, 2, 0, 2
$array | group | sort Count -Descending | select -First 1 | select -Expand Name 

今回のケースのように、「配列内に同一の要素が含まれており、要素ごとに纏める」という手順が必要なときはGroup-Objectコマンドレット(エイリアス:group)の出番です。

$array | group とすると、

Count Name                      Group
----- ----                      -----
    6 0                         {0, 0, 0, 0...}
    1 1                         {1}
    1 3                         {3}
    2 2                         {2, 2}

のような出力が得られます。Count=出現回数、Name=要素、Group=該当する要素のリスト です。なのでこの出力だけでお題の解答である「0」が確認できます。あとはこの出力から求める値を抽出するだけです。

Group-Objectコマンドレットの出力はGroupInfoというオブジェクトです。そう、これもオブジェクトなので、他のオブジェクト同様、プロパティ情報を保ったまま後続パイプラインに渡すことができます。

まずSort-Objectコマンドレット(エイリアス:sort)を用いてGroupInfoのCountプロパティの値で降順ソート(-Descendingパラメータ使用)をかけます。するとCountプロパティが一番大きいGroupInfoオブジェクトが一番最初の要素となります。(今回の場合はたまたまソート前後で最初の要素が同じでしたが)

次にSelect-Objectコマンドレット(エイリアス:select)に-First 1と指定することで一番最初のGroupInfoオブジェクトのみ取得します。最後に、GroupInfoオブジェクトのNameプロパティの値だけを取りだすのにSelect-Objectコマンドレットに -ExpandPropertyパラメータを指定します。

ところでGroupInfoオブジェクトのGroupプロパティ、どうせNameと同じものがCount分列挙されるだけじゃないか何の意味が?と思われるかも知れないので補足です。Group-Objectコマンドレットには-Propertyパラメータ(位置パラメータなのでパラメータ名は省略可能)が定義されており、指定すると任意のプロパティ値に基づいてグループ化してくれます。

たとえば

dir | group Extension

とすると、カレントディレクトリに含まれるファイルが拡張子別にグループ化され、以下のような出力が得られます。

Count Name                      Group
----- ----                      -----
    1 .gadget                   {twitterpost.gadget}
   59 .vbs                      {7zip_fix_archive.vbs, 7zip_store_each.vbs...}
   72 .ps1                      {cddrive.ps1, clipboard.ps1, cmdlets.ps1...}

このようにGroupプロパティの中身は、指定プロパティ値を持つ要素のグループとなっていることが分かると思います。

-Propertyパラメータには集計プロパティを指定することもできるので、

1..10 | group {if($_ % 2 -eq 0){"偶数"}else{"奇数"}}
Count Name                      Group
----- ----                      -----
    5 奇数                      {1, 3, 5, 7...}
    5 偶数                      {2, 4, 6, 8...}

みたいなこともできたりします。

ちなみにGroup情報が不要であるときは、Group-Objectコマンドレットに -NoElementパラメータを付与すると出力を抑制できます。(この場合、出力はGroupInfoオブジェクトではなく、GroupInfoNoElementオブジェクトとなる)

なんか後半はGroup-Objectコマンドレット特集みたくなってしまいました。ではまた次回。

2010/02/13

PowerShellは.NET Framework 2.0を利用するWindowsのシステム管理用シェルである。シェルであるためコンソールで対話的にコマンドを実行することができるのはもちろん、スクリプトファイル(*.ps1)を記述しバッチ的に実行することも可能である。ここではPowerShellスクリプトで(コンソールでも使用は可能だが)用いることのできる基礎文法を紹介する。なお、PowerShellでは文法上、大文字小文字を区別しない。

※(★2.0)の注釈があるものはPowerShell 2.0で新たに追加された要素である。

1.基礎

表示

コンソールに文字列を表示。

"Hello world" 

コマンドレット(後述)を使用した場合。

Write-Host "Hello world" 

コマンドレット

PowerShellはコマンドレットと呼ばれる100種類以上のコマンドライン・ツール群を単独で、あるいはパイプライン(後述)で連結して使用するのが基本となる。コマンドレットは原則verb-nounという命名規則にしたがっている。パラメータをつける場合は「-パラメータ名」あるいは「-パラメータ名 パラメータ値」を指定する。

# コマンドレットの一覧表示
Get-Command 

# サービスの一覧を表示
Get-Service 

# アプリケーション イベントログの最新15個のエントリを表示
Get-EventLog -logName Application -newest 15

パイプライン

コマンドレットが値を返却する場合、.NET Frameworkのオブジェクトが含まれる配列であることが多い。このオブジェクト配列がパイプラインを渡って後続のコマンドレットに入力される。

# プロセスのリスト(System.Diagnostics.Processオブジェクトの配列)を取得し、
# Where-Objectコマンドレットでハンドル数(handlesプロパティ)の値が500より大きいものだけを取り出し
# Select-Objectコマンドレットで最初の5つのオブジェクトだけを切りだして表示
Get-Process | Where-Object {$_.handles -gt 500} | Select-Object -first 5

# C:\Windows 配下のフォルダ、ファイルの一覧(System.IO.DirectoryInfo,System.IO.FileInfoオブジェクトの配列)を取得し、
# ForEach-Objectコマンドレットで配列を列挙しすべてのオブジェクトのFullNameプロパティ(フルパス)の値を表示
Get-ChildItem C:\Windows | ForEach-Object {$_.FullName} 

# 通常の配列に関してもパイプラインを使用可能。
# 重複を取り除き、ソートをかける
@(3,5,10,1,2,1,1,1,2,6,4,4)|Sort-Object|Get-Unique 

コメント

# コメント 

マルチラインコメント(★2.0)

<#
複数行に渡る
コメントです
#> 

変数の宣言

PowerShellは変数の宣言をしなくても変数を使用可能。以下のようにするとどのような型でも代入可能な変数が作られる。

$a = 1
$a = $b = $c = 1 #複数変数に一度に同じ値を代入する場合
$items = Get-ChildItem # コマンドレットの戻り値を格納 

変数の型を指定することは可能。以下のようにするとint型のみ格納可能な変数が作られる。

[int]$a = 1 

あるいは、コマンドレットを用いて$aという変数を宣言することもできる。この場合変数の型は指定できない。

New-Variable -name a 

変数のスコープ

# どのスコープからも読み書き可能
$global:a = 1

# 現在のスコープからのみ読み書き可能
$private:a = 1 

# 現在のスクリプトからのみ読み書き可能
$script:a = 1 

文法チェック

以下を実行することで未定義の変数を参照するとエラーが出るようになる。

Set-PSDebug -strict 

スクリプトの実行

デフォルトの実行ポリシーではスクリプトの実行は不許可であるため、以下のようにポリシーを変更しておく。(RemoteSignedはローカルにあるスクリプトファイルは無条件で実行可、リモートにあるスクリプトファイルは署名付きのもののみ実行可)

Set-ExecutionPolicy RemoteSigned 

スクリプト/コマンドを実行するにはコマンドラインで次のようにする。

コマンドを実行する

powershell -command {Get-ChildItem C:\} 

ファイルを実行する

powershell  .\script.ps1 

ドットソース(スクリプトの内容をグローバルスコープに読み込む)

powershell  . .\script.ps1 

ファイルを実行する(★2.0)

powershell -file script.ps1 

PowerShellスクリプトから別のスクリプトを実行する場合(関数のインクルードにも用いられる)

.\script.ps1
. .\script.ps1 # ドットソース 

デバッガの起動

Set-PSDebug -trace 2 

ステップ実行

Set-PSDebug -step 

2.数値

数値の表現

PowerShellにおける数値は.NET Frameworkの数値を表す構造体のインスタンスである。数値には整数、浮動小数点があり、変数に代入した段階で適切な型が設定される。

# int型(System.Int32型)
$int = 1

# System.Double型
$double = 1.001

四則演算

# 足し算
$i = 1 + 1

# 引き算
$i = 1 - 1 

# 掛け算
$i = 1 * 1 

# 割り算
$i = 1 / 1 

余りと商の求め方

# 割り算の余り
$mod = 7 % 3 

# 上記の場合の商
$div = (7 - 7 % 3) / 3 

べき乗

# 2の8乗
$i = [math]::Pow(2,8) 

インクリメントとデクリメント

# インクリメント
$i++ 

# デクリメント
$i-- 

3.文字列

PowerShellにおける文字列は.NET Frameworkの System.Stringクラスのインスタンスである。

文字列の表現

文字列はシングルクォーテーションかダブルクォーテーションで囲む。ダブルクォーテーションの中では`t(タブ)や`r`n(改行)などの特殊文字が使用でき、変数が展開される。

$str1 = 'abc'
$str2 = "def"
$str3 = "a`tbc`r`n" 

#変数展開(結果は abc def)
$str4 = "$str1 def" 

文字列操作

各種文字列操作

# 結合
$join1 = "aaa" + "bbb"
$join2 = [string]::Join(",",@("aaa","bbb","ccc") )

# 結合(★2.0)
$join2 = @("aaa","bbb","ccc") -join "," 

# 分割
$record1 = "aaa,bbb,ccc".Split(",") 

# 分割(★2.0)
$record2 = "aaa,bbb,ccc" -split "," 

# 長さ
$length = "abcdef".Length 

# 切り出し
$substr = "abcd".SubString(0,2) # ab

正規表現検索

# hitした場合はTrue,しなかった場合はFalse
$result = "abcd" -match "cd"

# 最初に見つかった文字列。添え字の1,2…には()内のサブ式にhitした文字列が格納。
$matches[0] 

正規表現置換

$result = "abc" -replace "c","d" 

4.配列

PowerShellにおける配列は.NET Frameworkの System.Arrayクラスのインスタンスである。

配列の参照と代入

# 5個の要素を持つ配列宣言と代入
$arr1 = @(1,3,5,7,9)
 
# 以下のようにも記述できる
$arr1 = 1,3,5,7,9 

# 型指定する場合
[int[]]$arr1 = @(1,3,5,7,9) 

# 1〜10までの要素を持つ配列宣言と代入
$arr2 = @(1..10) 

# 1要素の配列宣言と代入
$arr3 = @(1) 
$arr3 = ,1 

# 空の配列宣言と代入
$arr4 = @() 

配列の要素の参照と代入

# 4番目の要素を参照 
$ret = $arr2[3] 

# 6〜9番目の要素を含んだ配列を参照
$ret = $arr2[5..8] 

# 1〜4番目と8番目の要素を含んだ配列を参照
$ret = $arr2[0..3+7] 

# 配列の末尾の要素を取り出す
$ret = $arr2[-1] 

# 5番目の要素に値を代入
$arr2[4] = 11 

# 3より小さな要素を含んだ配列を返す
$ret = $arr2 -lt 3 

配列の個数

$arr1_num = $arr1.Length 

配列の操作

$arr1 = @(1,3,5,7,9) 
$arr2 = @(1..10) 

# 配列の末尾に要素を加える(push)
$arr2 += 50 

# 配列を結合し新しい配列を作成
$arr5 = $arr1 + $arr2 

# 配列にある要素が含まれるかどうか(ここではTrue)
$arr2 -contains 2 

5.ハッシュ

PowerShellにおけるハッシュは.NET Frameworkの System.Collections.Hashtableクラスのインスタンスである。

ハッシュ変数の宣言と代入

# 3つの要素を持つハッシュの宣言と代入
$hash1 = @{a=1;b=2;c=3}
 
# 空のハッシュの宣言と代入
$hash2 = @{} 

ハッシュの要素の参照と代入

# 要素の参照
$hash1.a 
$hash1["a"] 

#要素の代入
$hash1.b = 5
$hash1["b"] = 5 

ハッシュの操作

# ハッシュに要素を追加
$hash1.d = 4 
$hash1.Add("e",5)
 
# ハッシュの要素の削除
$hash1.Remove("a") 

# ハッシュのキーの取得
$keys = $hash1.Keys 

# ハッシュの値の取得
$values = $hash1.Values 

# ハッシュの要素を列挙
foreach ($key in $hash1.Keys)
{
    $key + ":" + $hash1[$key]
} 

# キーの存在確認
$hash1.Contains("b") 

6.制御文

if文

if (条件) {

}

if 〜 else文

if (条件) {

}
else{

}

if 〜 elsif 文

if (条件) {

}
elseif (条件) { 

} 

while/do文

while (条件) {

}

do {

} while (条件)

for文

for ($i = 0; $i -lt 5; $i++) {

} 

foreach文

foreach ($item in $items) {

} 

switch文

case を書かないのが特徴的。またスクリプトブロックを条件文に記述できる。

switch ($i) {
    1 {"1";break}
    2 {"2";break}
    {$_ -lt 5} {"5より小さい";break}
    default {"default句";break}
}
# ここで$iに配列を指定すると配列要素すべてに対してswitch文が実行される。 

比較演算子

比較演算子の一覧。PowerShellではPerlの文字列比較演算子のような記述をおこなうが、Perlとは異なり文字列も数値も同じ書式である。

$num1 -eq $num2 # $num1は$num2と等しい
$num1 -ne $num2 # $num1は$num2は等しくない
$num1 -lt $num2 # $num1は$num2より小さい
$num1 -gt $num2 # $num1は$num2より大きい
$num1 -le $num2 # $num1は$num2以下
$num1 -ge $num2 # $num1は$num2以上 

論理演算子

# 論理否定
$ret = -not $true
$ret = !$true

# 論理積
$ret = $true -and $false 

# 論理和
$ret = $true -or $false 

# 排他的論理和
$ret = $true -xor $false 

ビット演算子

# ビット単位の否定
$ret = -bnot 0x14F4

# ビット単位の積
$ret = 0x14F4 -band 0xFF00 

# 上記結果を16進数で表示する場合
$ret = (0x14F4 -band 0xFF00).ToString("X") 

# ビット単位の和
$ret = 0x14F4 -bor 0xFF00 

# ビット単位の排他的論理和
$ret = 0x14F4 -bxor 0xFF00 

7.サブルーチン

PowerShellのサブルーチンには関数とフィルタがある。関数とフィルタは呼び出し行の前で宣言する必要がある。 filter構文もfunction構文と並んで独自関数を記述するものだが、filter構文はパイプラインに渡されたオブジェクトをフィルタするのに用いる。 functionとの違いは、パイプラインに渡した配列を一度に処理するか(function)個別に処理するか(filter)

# 関数宣言の基本
function Get-Test {
    return "test"
}
# 注:returnを付けなくても関数内で出力された値はすべて呼び出し元に返却される。返却したくない場合は出力値をを[void]にキャストするか|Out-Nullに渡す。

# 引数を指定する場合
function Get-Test {
    param($param1,$param2)
    return $param1 + $param2
}
 
# 引数を指定する場合の簡易的な記述法
function Get-Test($param1,$param2) {
    return $param1 + $param2
} 

# 引数の型を指定する場合
function Get-Test {
    param([string]$param1,[string]$param2)
    return $param1 + $param2
} 

# 関数の呼び出し方(,区切りではなくスペース区切りであることに注意)
Get-Test "引数1" "引数2"

# 引数の順序はパラメータ名(引数名)を指定すると自由に指定可能
Get-Test -param2 "引数2" -param1 "引数1" 

# フィルタ宣言の基本
filter Get-Odd {
    if($_ % 2 -eq 1){
        return $_ 
    }else{
        return
    }
} 

# フィルタの使用
@(1..10) | Get-Odd

8.テキストファイル入出力

コマンドレットで可能。エンコーディングは日本語環境のデフォルトではShift-JIS。コマンドレット出力のテキストファイルへの書き出しに関してはリダイレクトも可能。この場合エンコーディングはUnicode。

$str1 = "testテスト"
Set-Content test.txt $str1 # 書き込み
Add-Content test.txt "追記" # 追記
$str2 = Get-Content test.txt # 読み込み

Set-Content test.txt $str1 -encoding UTF8 # UTF-8で書き込み

# リダイレクト
Get-Process > test.txt # 書き込み
Get-Process >> test.txt # 追記
Get-Process | Out-File test.txt -encoding UTF8 # エンコーディングを指定する場合

9.例外

PowerShellで例外が発生すると、デフォルトではエラーメッセージを表示し次の行を実行する(シェル変数$ErrorActionPreferenceの設定により挙動の変更可能)。VBでいうとOn Error Resume Nextに近い。エラーが発生すると$Errorにエラー情報の配列が格納され、$?にFalseが格納される。エラーをトラップするには次の構文を使用する。VBでいうとOn Error Goto lineに近い。

# すべてのエラーをトラップ
trap {

}

# エラーの型名を指定してトラップ
trap [System.Management.Automation.CommandNotFoundException] {

} 

# エラーを発生させる
throw "エラー"
throw New-Object NullReferenceException 

構造化例外処理(★2.0)

# 基本
try{

}
catch{

}
finally{

} 

# エラーの型を指定してcatch
try{

}
catch [System.Net.WebException],[System.IO.IOException]{

}

10.知っておいたほうがよい文法

行継続文字

1行にすると長いコードを複数行に書くには行継続文字`を用いる。VBの_。

$items = Get-ChildItem a*,b*,c*,d*,e* `
-force -recurce 

ただし以下のような場合は`を使用しなくてもよい

$items =
    Get-ChildItem a*,b*,c*,d*,e* -force –recurse
    
Get-Process | 
    Where-Object {$_.handles -gt 500} |
    Select-Object -first 5 

ステートメント分割

ステートメントを分割するには改行コードもしくは;を使用する。VBの:。JavaScriptと同様、文末に;はつけてもつけなくてもよい。

$i = 1; $j = 5; $k = $i + $j 

ヒア文字列

複数行の文字列を記述する方法。

$str = @"
aaaaaa
bbbbb
cccc
ddd
ee
"@ 

.NET Frameworkクラスの利用

.NET Frameworkに含まれているクラスのプロパティやメソッドを使用できる。基本的に完全修飾名を指定しなければいけないが、"System."は省略可能。また、intなど型エイリアスがいくつか定義されている。

# スタティックメンバの使用
[System.Math]::Pow(2,8) 

# インスタンスの生成とメソッドの実行
$arrayList = New-Object System.Collections.ArrayList
$arrayList.Add("a") 

# コンストラクタがある場合。複数ある場合は配列として指定
$message = New-Object System.Net.Mail.Message from@example.com,to@example.com

# COMオブジェクトの生成
$wshShell = New-Object -com WScript.Shell 

# デフォルトで読み込まれていないアセンブリを読み込む
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
[System.Windows.Forms.MessageBox]::Show("hello!") 

# クラスにどんなメンバがあるかの確認
# インスタンスメンバ
Get-ChildItem | Get-Member 

# スタティックメンバ
[math] | Get-Member -static 

キャスト

-asを使った場合はキャスト失敗時もエラーにならずNullが格納される。

$dt = [System.DateTime]"2010/02/13"
$dt = "2010/02/13" -as [System.DateTime] 

ユーザー定義オブジェクト

PowerShellにはクラスを定義する構文はないが、空のオブジェクト(PSObject)を生成し、任意のプロパティ(ノートプロパティ)を付加することができる。

$obj = New-Object PSObject
$property = New-Object System.Management.Automation.PSNoteProperty "Name","名前"
$obj.PSObject.Members.Add($property) 

シェル変数

あらかじめ定義されている変数。シェル変数には自動変数(変更不可能)とユーザー定義変数(変更すると挙動を変更することができる)がある。自動変数の例を挙げる。

$_ :現在パイプラインにわたっているオブジェクト
$args :関数やスクリプトに与えられたパラメータの配列
$pshome :PowerShellがインストールされているフォルダのフルパス
$MyInvocation :スクリプトの実行情報。$myInvocation.ScriptNameでスクリプトのフルパス取得(★2.0)。$myInvocation.MyCommand.Path(1.0の場合)
$true :true。
$false :false。
$null :null。

 

サブ式

$()内には複数行のコードが記述できる。

$arr = $(1;2;1+4)

式モードとコマンドモード

PowerShellの構文解析は式モードとコマンドモードがある。式モードは通常のモード。コマンドモードは引用符がなくても文字列を文字列として扱う。コマンドレットのパラメータなどはコマンドモードで扱われる。ただしコマンドモードになるところでも()もしくは$()もしくは@()をつけるとその中身は式モードとして解釈、実行される。

$i = 1 + 1 # 式モード
Write-Host aaa # コマンドモード(表示:aaa)
Write-Host aaa bbb # コマンドモード(表示:aaa bbb)
Write-Host 1+1 # コマンドモード(表示:1+1)
Write-Host (1+1) # 式モード(表示:2)
$itemCount = @(Get-ChildItem).Length # 式モード

実行演算子とスクリプトブロック

&演算子を用いるとスクリプトブロック{}の内容を実行できる。この場合、スクリプトブロック内のコードは別スコープになる。

$script = {$i = 1+6; Write-Host $i}
&$script
& 'C:\Program Files\Internet Explorer\iexplore.exe' # パスにスペースの含まれるファイルを実行したりするのにも使える

フォーマット演算子

-f演算子を使うと、.NET Frameworkのカスタム書式が使用可能。

"{0:#,##0}Bytes" -f 38731362 # 表示:38,731,362Bytes

バイト数の簡易表記

$i = 1KB # 1024が代入される
$i = 1MB # 1048576が代入される
$i = 1GB # 1073741824が代入される

そのほかの基礎文法最速マスターへのリンク

プログラミング基礎文法最速マスターまとめ - ネットサービス研究室
http://d.hatena.ne.jp/seikenn/20100203/programmingMaster

PowerShellの詳しい機能解説についてはこちらの記事を参照してください。
PowerShell的システム管理入門 ―― PowerShell 2.0で始める、これからのWindowsシステム管理術 ―― ─ @IT
進化したPowerShell 2.0 ─ @IT

文法や機能について詳しく学びたい方には書籍もあります
Windows PowerShellポケットリファレンス
PowerShellによるWindowsサーバ管理術

元記事:http://blogs.wankuma.com/mutaguchi/archive/2010/02/13/186034.aspx


Copyright © 2005-2018 Daisuke Mutaguchi All rights reserved
mailto: mutaguchi at roy.hi-ho.ne.jp
プライバシーポリシー

Books

Twitter