2017/12/24
PowerShellにおける「文」と「式」についての考察
この記事には「 独自研究 」に基づいた記述が含まれているおそれがあります。
この記事は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/11/09
JapanesePhoneticAnalyzerを使ってPowerShellで形態素解析(後編)
分かち書きとは
中編で作ったGet-JpYomi関数は、JapanesePhoneticAnalyzerクラスの読み仮名取得機能にフォーカスを当てたラッパー関数でした。
今回は、JapanesePhoneticAnalyzerクラスの最大の使用目的と思われる、「分かち書き」を目的とした関数を作成します。分かち書きとは、文章を文節単位で分割することと考えて頂いて良いかと思います。
前々回作ったGet-JpWordは単語単位の分割を行うものでしたが、読みやすさや発音のしやすさを目的として文章を分割表記する場合は、単語単位では細かすぎると言えます。
よって単語単位ではなく、文を意味のあるまとまりとして区切ることのできる最小の単位である、文節単位で分割する方法を考えてみます。
Split-JpText関数
前編で作ったGet-JpWord関数をラップし、分かち書きに特化した関数Split-JpTextを作成しました。まずは以下にコードを示します。
function Split-JpText { param( [parameter(ValueFromPipeline=$true)] [PSObject[]] $InputObject, [ValidateSet("Text", "Yomi", "Detail")] [string] $Format = "Text", [ValidateSet("ByWord", "ByPhrase")] [string] $SplitMode = "ByPhrase", [string] $Separator = " ", [switch] $ToArray ) begin { if($Format -eq "Detail"){$ToArray = $true} } process { foreach($o in $InputObject) { $o.ToString() | Get-JpWord | foreach -Begin { $phrases = @() $phrase = $null } -Process { if($_.IsPhraseStart) { if($phrase){$phrases += $phrase} $phrase = New-Object psobject | Add-Member -MemberType ScriptProperty -Name Text -Value { -join $this.Words.DisplayText} -PassThru | Add-Member -MemberType ScriptProperty -Name Yomi -Value { -join $this.Words.YomiText} -PassThru | Add-Member -MemberType NoteProperty -Name Words -Value @() -PassThru } $phrase.Words += $_ } -End { if($phrase){$phrases += $phrase} if($SplitMode -eq "ByPhrase") { $out = switch($Format) { "Text" {$phrases.Text} "Yomi" {$phrases.Yomi} "Detail" {$phrases} } } else { $out = switch($Format) { "Text" {$phrases.Words.DisplayText} "Yomi" {$phrases.Words.YomiText} "Detail" {$phrases.Words} } } if($ToArray) { $out } else { $out -join $Separator } } } } }
パラメータの説明
パラメータ名 | 型 | 説明 |
InputObject | 任意の型 | 入力テキスト。文字列以外の型の場合は文字列に変換して評価される。パイプライン入力可能。 |
Format | string | Text(デフォルト):文字列のみ出力する。 Yomi:文字列ではなく読みを出力する。 Detail:文節の文字列、読み、各文節に含まれる単語の配列を含んだオブジェクトの配列を出力する。(SplitMode=ByPhraseの時のみ) |
SplitMode | string | ByPhrase(デフォルト):文を文節単位で分割する。 ByWord:文を単語単位で分割する。 |
Separator | string | 分割文字を指定。デフォルトは" "(半角スペース)。(Format=DetailもしくはToArray指定時には無効) |
ToArray | switch | 指定すると、単一の文字列ではなく、文字列の配列を出力する。 |
使用法
- 分かち書き(文節)
例:Split-JpText "今日はいい天気ですね。"
出力:今日は いい 天気ですね 。 - 分割文字指定
例:Split-JpText "今日はいい天気ですね。" -Separator /
出力:今日は/いい/天気ですね/。 - 分かち書き(単語)
例:Split-JpText "今日はいい天気ですね。" -SplitMode ByWord
出力:今日 は いい 天気 です ね 。 - 文節単位で読み仮名を表示
例: Split-JpText "今日はいい天気ですね。" -Separator / -Format Yomi
出力:きょうは/いい/てんきですね/。 - 分かち書きした文節を文字列配列として変数に格納
例:$phrases = Split-JpText "今日はいい天気ですね。" -ToArray
解説
ちょっと長めの関数ですが、ポイントはJapanesePhoneticAnalyzerクラスのGetWordsメソッドが返すJapanesePhonemeオブジェクトのIsPhraseStartプロパティです。
IsPhraseStartプロパティは、当該単語(Phoneme)が文節(Phrase)の開始部分にあたる単語であればTrueを返します。すなわち、JapanesePhonemeコレクションを文頭から文末まで列挙していったとき、IsPhraseStartプロパティがFalseからTrueに変わる部分が文節の境界になるわけです。
Split-JpText関数では、単語を列挙していき、文頭もしくは文節の境界に遭遇すると、文節に含まれる文字列(Textプロパティ)とその読み(Yomiプロパティ)と単語の配列(Wordsプロパティ)を格納するオブジェクトを新たに作成し、$phrase変数に代入します。一方で$phrase変数に元々入っていたオブジェクトは、$phrases配列に追加します。
$phraseオブジェクトのWordsプロパティには、列挙中の単語を都度、追加していきます。
なお、$phraseオブジェクトのTextプロパティとYomiプロパティはスクリプトプロパティとして定義しておき、必要時に値を取得するようにしてあります。
まとめ
3回に渡って、JapanesePhoneticAnalyzerクラスの使用法を具体的なラッパー関数を作成して紹介しました。
個人的には、PowerShellからなら中編で挙げた、読みの取得が一番使いでがあるかな?と思いました。今回取り上げた分かち書きは、意外と応用例が思いつきませんでした。
前編のGet-JpWord関数を使って、何らかの文書群の単語リストをあらかじめインデックスとして出力しておき、単語検索コマンドを実装するのも面白そうですね。
ただ、残念ながら品詞情報が取れないので、JapanesePhoneticAnalyzerをmecabとかの形態素解析エンジンの代替にするのはちょっと厳しいかもしれないです。まあ、標準機能のみでちょっとしたものを作れるのは大きいかと思います。何か日本語の文章を解析する必要があるときには使ってみてはいかがでしょうか。
2014/04/15
本当は怖いPowerShell その1 暗黙の型変換の闇
PowerShellはゆるふわな言語ですが、そのゆるふわさがたまによく牙を剥きます。今日はそんなお話。
あえとすさんがこんなツイートをされていました。
$x = 'A' + @('B', 'C') ってしたとき、何故 $x.Length -eq 4 で $x[2] -eq ' ' なのだろうか。 #PowerShell
? アエトス・トリスメギストス (@aetos382) April 14, 2014
直観的には、$xには'A', 'B', 'C'の3要素が格納された配列となるのでLengthは3、$x[2]は最後の要素である'C'が入っていそうです。
さて、何故だかわかりますか。シンキングタイム3分。
…では解説です。
まず、'A' + @('B', 'C')というのは実は3要素の配列を返さず、単一の文字列を返します。というのもPowerShellは+, -等の二項演算子を利用する際、左辺と右辺の型が異なる場合は、まず右辺の型を左辺の型に暗黙の型変換を行ってから演算を行います。この場合だと右辺は@('B', 'C')なので文字列配列(厳密にはobject[])、左辺は文字列型なので、文字列配列が文字列に型変換されるわけです。
さて、ここで配列→文字列の型変換がどうやって行われるかという話なのですが、まず配列要素がそれぞれ文字列型に変換されます。この変換は型によってそれぞれ挙動が違いますが、特にPowerShell上で定義がない場合はToString()されたものが返されます。今回のは配列要素が元々文字列なので変換はありません。
次に、文字列同士がユーザー定義$OFSに格納されている文字列で連結されます。$OFSはデフォルトではnull(定義なし)なのですが、nullの場合は" "(半角スペース)として扱われます。
※ちなみにOFSとはOutput Field Separatorの略です。awkとかPerlとかにも同様の変数があり、PowerShellのはそれらを参考にしたものと思います。
よって、@('B', 'C')が文字列に変換されると、'B'と'C'が$OFSのデフォルトの" "で連結され、'B C'となります。変換の後+演算子が実行されて、'A'と'B C'が連結されるので、'AB C'となります。この値が$xに格納されるわけです。
$xには配列ではなく単一の文字列が格納されているので、Lengthプロパティはstringクラスのものが参照されるので、文字数を返却します。$xの中身はA,B,半角スペース,Cの4文字なので$x.Lengthは4になります。
また文字列変数に数値でのインデックスアクセスをすると、該当文字位置に格納されたchar型の文字が返されるので、$x[2]は$xに格納された3番目の文字(インデックスは0から始まるので)、' '(半角スペース)を返すわけですね。
これであえとすさんの疑問は解消したわけですが、じゃあ本来の目的である、「単一の値と配列を連結して配列を得る」にはどうするか、というと…
@('A', 'B', 'C') が欲しければ @('A') + @('B', 'C') とすればよい。 ('A`)ヴァー
? アエトス・トリスメギストス (@aetos382) April 14, 2014
となるわけです。こうやって非配列値をあらかじめ@()により要素数1の配列にしておくと、+演算子の左辺と右辺がどちらも配列型となるため型変換は行われず、配列同士の+演算、すなわち配列の連結処理が行われるわけですね。
その1とありますがその2があるかは不明。なお、闇が沢山あるのは事実です。('A`)ヴァー
2011/12/13
バックグラウンドジョブとの通信 [PS Advent Calendar '11]
はじめに
この記事はPowerShell Advent Calendar 2011の13日目、そして私の2回目の記事となります。
今日のテーマは前回の続きで、PowerShellのバックグラウンドジョブの結果を読み取ったり、バックグラウンドジョブに値を与えたりして、ジョブと通信を行う方法を解説します。
ジョブから呼び出し元に値を返却する
ジョブの結果を取得するにはReceive-Jobコマンドレットを使用すれば良いと前回書きましたが、今回はジョブ側から結果を返す実際の方法を示します。
基本的にPowerShellのスクリプトやスクリプトブロックが呼び出し元に返却する値というのは、そのスクリプト(or ブロック)でパイプラインを通じて最終的にデフォルト出力に渡されたすべての値です。複数行に渡って出力されている場合は、呼び出し元にはその配列(object[])として返却されます。
ジョブにおいてもそれは同様で、基本的にStart-Jobなどで生成したスクリプトやスクリプトブロックが出力したすべての値がジョブの出力となり、呼び出し元からはReceive-Jobコマンドレットで受け取ることができます。
以下に現在の日付時刻を出力するサンプルを示します。サンプルなのでジョブなのに同期的な処理になってますがご了承を。
$job=Start-Job { Start-Sleep -sec 5 Get-Date } Wait-Job $job|Receive-Job
複数だと以下のようになります。
$job=Start-Job { Start-Sleep -sec 1 "Give me job." Get-Date 1+1 } Wait-Job $job|Receive-Job
この場合だと文字列、日付時刻、数値の3種類のオブジェクトが出力されますので、結果は長さ3のobject配列になります。そのためこれらの値を個別に取り出す場合は次のようにします。
$job=Start-Job { Start-Sleep -sec 1 "Give me job." Get-Date 1+1 } $result=Wait-Job $job|Receive-Job Write-Host $result[0] Write-Host $result[1].ToString("yyyyMMdd") Write-Host $result[2]
このように配列のインデックスで各値にアクセスできますが、これだと受け取り側での処理が分かりにくいと思われるかもしれませんね。
そこでお勧めなのが、このように複数値を返却するのではなく、カスタムオブジェクトを1つだけ返却するようにする方法です。
$job = Start-Job { Start-Sleep -sec 1 $ret = New-Object PSObject -property @{ String = "Give me job."; Date = Get-Date; Number = 1+1 } $ret } $result = Wait-Job $job|Receive-Job Write-Host $result.String Write-Host $result.Date.ToString("yyyyMMdd") Write-Host $result.Number
この方法ではジョブの中でNew-Objectコマンドレットでカスタムオブジェクトを作成し、それを返却しています。返却値は1つのオブジェクトでそのプロパティに値が格納されているのでドット演算子で値を参照できるようになりました。
ただしこの方法にも欠点があって、Receive-Objectで結果を参照するとき、ジョブが終了するまですべての値が参照できません。実はジョブが完了してない段階でも、Receive-Objectを実行するとジョブがそこまで出力した値を逐次取得することができるのです。よって
$job=Start-Job { Start-Sleep -sec 3 "Give me job." Start-Sleep -sec 3 Get-Date Start-Sleep -sec 3 1+1 }
のようにしてジョブを走らせた後、適当な間隔で
$job|Receive-Job
を実行すると、それまでに出力した部分までを取得して書き出します。先程の例のように出力をカスタムオブジェクトでまとめてしまうとこの手法が使えなくなってしまいます。
どちらもメリット、デメリットがあるのでうまく使い分けると良いかと思います。具体的にはジョブの実行途中では結果を取得せず、ジョブ完了後の最終的な結果のみまとめて参照したい場合はカスタムオブジェクトで返却し、それ以外はそのまま随時値を返却するようにすればいいと思います。
さて、ジョブの結果を受け取る際にもう一点注意しなければならないことがあります。それはジョブが返すオブジェクトの型です。PowerShellのジョブ機能はリモーティング機構の上に構築されているというのは前回も書きましたが、その関係上、呼び出し元とジョブとの間でオブジェクトを受け渡しする場合は一度シリアル化され、受け取り側でデシリアライズされます。
オブジェクトのクラスもしくは構造体がシリアライズ可能(Serializable属性がついている)なら、PowerShellによりシリアル化→デシリアライズされたオブジェクトはシリアル化される前のオブジェクトと同一のものです。しかしそうではないオブジェクトの場合だと完全に元と同じオブジェクトには復元されません。
たとえば(Get-Process)[0]をジョブで実行するとSystem.Diagnostics.Processオブジェクトが得られますが、それをジョブの呼び出し元に返却するとDeserialized.System.Diagnostics.Processというカスタムオブジェクトに変換されます。このオブジェクトは各プロパティ値は(シリアル化可能なものだけ)保持しているものの、メソッド定義などは消失しているのでこのオブジェクトのメソッドを実行することはできません。
ちなみにSystem.StringクラスやSystem.Int32やSystem.DateTime構造体はSerializable属性がついているのでジョブの結果として取得しても元のオブジェクトと同一なので、メソッドなどが呼び出し可能です。
ジョブに呼び出し元の値を渡す
今度は逆の場合です。ジョブを走らせるとき、呼び出し元からジョブに値を渡す方法です。
$job = Start-Job { param($date,$value) Start-Sleep -sec 1 "${date}の${value}日後の日付は" + $date.AddDays($value).ToString("yyyy/MM/dd") + "です。" } -argumentList @((Get-Date),1) Wait-Job $job|Receive-Job
このようにStart-Jobコマンドレットの-argumentListパラメータに、ジョブに渡したい値を指定すればOKです。複数ある場合はこのように配列指定も可能です。
ジョブ側ではparamキーワードで仮引数を指定しておけば、スクリプトブロック内で呼び出し元の値が格納された変数を使用できます。ここではparamを使いましたが、paramを使用しない場合は$argsに実引数が配列として格納されているので、これを利用するのでもOKです。
値を渡す場合でもシリアライズとデシリアライズが行われるので、その点だけは注意が必要です。
ジョブは呼び出し元と別インスタンスなので、呼び出し元に読み込まれた関数を参照することはできません。よってジョブでも呼び出し元で定義した関数を実行したい場合は同様に-argumentListで関数の実体であるスクリプトブロックを送ってやる必要があります。
function Get-Test { "テスト!" + (1+1) } $job = Start-Job { param($sb) &([scriptblock]::Create($sb)) } -argumentList (Get-Item Function:\Get-Test).ScriptBlock Wait-Job $job|Receive-Job
-argumentListでスクリプトブロックを渡すとStringにキャストされてしまうので、ジョブ内でそれをCreateメソッドでスクリプトブロックに戻してから実行演算子&で実行するという回りくどいことになってしまいました。関数にこだわらなければ呼び出し側でスクリプトブロックを作って変数に入れ、それを-argumentListに入れてやると少しだけ記述がシンプルになりますが、ジョブ内でスクリプトブロックを復元しなければならないのは同様です。
いずれにせよあんまり美しくないのでお勧めしません。こんなことをやるくらいならジョブの中あるいは -InitializationScriptパラメータの中で関数やスクリプトブロックを定義してやるか、関数を別スクリプトファイルに切り出して、そのスクリプトファイルをジョブ内で読み込むほうが良いかと思います。前者の場合だと呼び出し元とジョブ内で関数を共有することはできませんが、後者の方法だとファイルとしては分割してしまいますが可能です。
おわりに
今回はジョブと通信する方法として、ジョブから結果を出力したり、ジョブに値を渡したりする方法をまとめました。意外と落とし穴が多いので注意してください。
このシリーズはあと1回だけ続く予定です。お楽しみに。
2011/10/09
JScriptで2次元のSafeArrayを作る
JScriptは言語単体ではSafeArrayを作ることができません。
そこでJScriptでSafeArrayが必要な場合、VBScriptを併用しVBScriptの配列(これはSafeArrayです)をJScriptに取り込む方法や、Scripting.DictionaryのItems()メソッドを使う方法などが使われているようです。
しかしこれらの方法で多次元のSafeArrayを作るサンプルをあまり見かけませんでした。Dictionaryの方法ではそもそも1次元しか無理ですしね。そんな中、この記事を発見しました→JScriptの配列とVBScriptの配列(SafeArray)を相互変換する方法(2次元編) - プログラマとSEのあいだ
この記事の二つ目の例ではExcelを使用しRegionオブジェクトが二次元配列を返す点を利用しています。これはなかなか盲点というかアイデアものではありますが、実行速度にやや難があるかな?と思いました。
注: ただしこの記事の方法は、もともとExcelで二次元配列が必要な場合があったから考案されたもののようで、その用途においてはExcelを起動するコストは考慮しなくてよいのかもしれません。
一つ目の方法ではVBScriptを併用していますが、.wsfファイルを使用してJScriptとVBScriptを混在させる形式をとっています。この方法はWSHでは問題ありませんが、複数のスクリプトエンジンを混在できないホスト環境では問題があります。
注:そんな環境ってあるのか?と聞かれそうですが、たしかにHTML/HTA/Windowsデスクトップ(サイドバー)ガジェット/WSH/classic ASPなどほとんどの環境では大丈夫そうです。ただ私が最近はまっているJScript実行環境であるところのAzureaでは無理ですね。WSHでも.jsファイルにこだわるのであれば。
そこで考えたのが、ScriptControlを使用してJScriptのコードの中でVBScriptのコードを実行させる方法です。以下のような感じになります。
function array2dToSafeArray2d(jsArray2d) { var sc = new ActiveXObject("ScriptControl"); sc.Language = "VBScript"; var code = 'Function ConvertArray(jsArray)\n' + ' ReDim arr(jsArray.length - 1, jsArray.[0].length - 1)\n' + ' outerCount = 0\n' + ' For Each outer In jsArray\n' + ' innerCount = 0\n' + ' For Each inner In outer\n' + ' arr(outerCount, innerCount) = inner\n' + ' innerCount = innerCount + 1\n' + ' Next\n' + ' outerCount = outerCount + 1\n' + ' Next\n' + ' ConvertArray = arr\n' + 'End Function\n'; sc.AddCode(code); return sc.Run("ConvertArray",jsArray2d); }
まあやっていることは本当にJScriptの配列をバラしてVBScriptの二次元配列に詰め直しているだけです。
ただいくつかポイントがあって、まずVBScriptからはJScriptのオブジェクトメンバーにドット演算子でアクセスができます。JScriptの配列はオブジェクトと同一であり、配列はオブジェクトに0,1,2...という名前のプロパティが存在することになります。しかしVBScriptで数字のメンバ名はそのままではドット演算子でアクセスできないので、[]でくくる必要があります(これ、予約語なんかもそうですね。あとVB6でもVB.NETでも同じなので覚えておくといいかも)。なので次元数2の配列の長さを調べるのにjsArray.[0]でまず内側の配列オブジェクトを取得しているわけです。
さらにポイントとして、VBScriptでJScriptの配列を含むオブジェクトメンバを列挙するのにコード例のようにFor Each Next構文が使えます。ただしFor Nextを使ってインデックスアクセスはできません。というのもjsArray.[3]とかはあくまでjsArrayオブジェクトの3プロパティの値を参照しているにすぎず、jsArray.[I]という書き方ができないからです(これだと単にIプロパティの値を見てることになる)。Eval関数を併用すれば可能ではありますが、コードの中にコードを含ませさらにその中にまたコードを含ませるのも微妙なのでここでは使ってません。
あとは紹介した記事の関数部分だけ置き換えればJScriptのVBArrayオブジェクトを用いたテストもできるかと思います。注意点はExcelオブジェクトは配列添え字が1から始まるのに対し、VBScriptの配列は0から始まる点です。LBound関数を使えばその差違は吸収できるかな、と思います。
最初は多次元配列というかn次元配列に拡張した関数を書いてやろうと企んでましたが挫折しました。ネストしたループではなく再帰呼び出しである必要がありますし、ReDimは次元数を動的に指定することができないので実行するVBScript自体を動的生成しなければいけません。興味がある方はチャレンジしてみてください。
元記事:http://blogs.wankuma.com/mutaguchi/archive/2011/10/09/204198.aspx2011/05/14
PowerShellでJScript.NETを利用してJSONをパースする
PowerShellでJSONをパースする方法はいくつかあると思います。
1. System.Runtime.Serialization.Json.JsonReaderWriterFactoryクラスを用いる
これは.NET Framework 3.5から追加されたクラスで、JSONデータを読み書きするXMLReader/Writerを提供します。すなわちJSONをパースしてXMLに変換することが可能です。XMLはPowerShellから簡単に扱えるので有用な方法と言えるでしょう。
PowerShellからの使用方法についてはこちらの記事が参考になります。:JSON Serialization/Deserialization in PowerShell | Keith Hill's Blog
2. 頑張って自力でパースする
.NET 3.5が入っていない環境では1の方法が使えないので別の方法を考える必要があります。JSONはテキストデータなので、頑張って自力でパースすることもできなくはないでしょう。
PowerShellでやっている例はこちらになります。:Convert between PowerShell and JSON - Home("Source Code"のリンクをたどっていくとソースがあります)
3. ScriptControl+JScriptを用いる
もう少し簡便な方法はないかなと思っていろいろ考えたんですが、PowerShellではScriptControlを用いるとJScriptやVBScriptを実行することができます。そしてJSONはJavaScriptで扱うことを想定しているだけあって、JScriptではeval()するだけでJSONをオブジェクトに変換することができます。そこで実際にやってみたのですが…
$json=@' {"items": [ { "code":25, "name":"ハードディスク2TB", "price":7000 }, { "code":56, "name":"メモリ8GB", "price":8000 }, { "code":137, "name":"23インチ液晶ディスプレイ", "price":35000 } ] } '@ $sc=new-object -com ScriptControl $sc.Language = "JScript" $jscode="function parseJSON(json){return eval('(' +json + ')').toString();}" $sc.AddCode($jscode) $jsobj=$sc.CodeObject.parseJSON($json) $jsobj
このコードを実行すると、確かにJSONがパースされ、結果が$jsobjという変数に格納されるのですが、残念ながらPowerShellはJScriptのオブジェクト(JScriptTypeInfo)を展開することができないようなのです。
JScriptTypeInfoオブジェクトはVBScriptでは扱うことができるので、まずJScriptでパースし、その結果オブジェクトをVBScriptに渡し、オブジェクトはScripting.Dictionaryオブジェクトに変換し、配列はVBScriptの配列(Safe Array)に変換し、その結果オブジェクトをPowerShellに戻すという方法を考えました。PowerShellはCOMオブジェクトやSafe Arrayは扱えるので理屈の上ではうまくいきます。(参考までに、ASPでこの方法を実際にコードにしてる方がいらっしゃいました。:ASPでJSONパーサーを書いてみた - ゆるゆると)
しかしこの方法は当初の目的「簡便にJSONをパースする」からだいぶ離れてしまっています。
4. JScript.NETを用いる
そうだ、JScriptが駄目ならJScript.NETを使えばいいじゃない。JScript.NETなら結果は.NETのオブジェクトで返るしPowerShellでも読めるだろう、ということで、この前このブログで紹介したAdd-Typeコマンドレットを使ってJScript.NETのコードを実行する方法を利用してやってみました。
($jsonの値は先ほどのスクリプトのを使います)
$code=@" static function parseJSON(json) { return eval('(' +json + ')'); } "@ $JSONUtil = (Add-Type -Language JScript -MemberDefinition $code -Name "JSONUtil" -PassThru)[1] $jsobj = $JSONUtil::parseJSON($json) # $jsobjはJSObject $jsobj["items"][1]["name"] #「メモリ8GB」と表示される $items=$jsobj["items"] # $itemsはJSArrayObject $items|%{$items[$_]["name"]} # 名前が列挙される
という感じでうまくいきました。
ここで$jsobjに格納されているのはMicrosoft.JScript.JSObjectクラスのオブジェクトです。このクラスのItemプロパティ(引数付きプロパティ、PowerShellではParameterizedPropertyと呼ばれる)にプロパティ名を引数として渡すと、その値が返却されます。PowerShellでは引数付きの既定プロパティはC#のインデクサと同様の構文で値が参照できるので、$jsobj[“items”]のように[]でアクセス可能です。これは$jsobj.Item(“items”)としても同様の結果が得られます(プロパティなのに()で値を取るところはVB風味?)。
配列の列挙ですが、JSObjectと、オブジェクトが配列の場合はその派生クラスであるJSArrayObjectクラスになりますが、これらはIEnumerableインターフェースを実装しているのでforeachで列挙が可能です。しかしここで列挙されるのはあくまでkey、すなわちプロパティ名の方です。値が列挙されるわけではありません。ご存じのとおり、JavaScriptの配列、連想配列、オブジェクトは同じものであり、配列の場合はkeyが配列インデックスの数字に相当します。そのため配列をforeachしても「0,1,2…」という数字が列挙されるだけです。
なので配列を列挙する場合は、この例のように、一旦JSArrayObjectを変数で受けて、それに対してforeachし、列挙した要素(インデックスの数字)をJSArrayObject.Itemプロパティの引数に与えることで、JS配列要素の値を取得してやる必要があると思います。
1のXMLを経由する方法のように、JSONをドット演算子でプロパティアクセスできないのは残念ですが、.NET 3.5が入っていない(がPowerShell 2.0は入ってる)環境では、それほど手間をかけずJSONを扱えるという点でそれなりに有用ではないでしょうか。
JSObjectもXMLみたいに型アダプタがあればプロパティアクセスできるようになるでしょうし、Add-Memberコマンドレットを駆使してJSObjectに動的にプロパティを追加する関数を書くのもいいかもしれません。が、そこまでいくとやはりお手軽からはかけ離れてしまうので今回はこの辺にとどめておきましょう。
元記事:http://blogs.wankuma.com/mutaguchi/archive/2011/05/14/199047.aspx2011/04/26
PowerShellでJScript.NETのコードを実行する
PowerShell 2.0ではAdd-Typeコマンドレットを用いてC#など他言語のコードをコンパイルし実行することが可能です。(P/Invokeも可能です)
ほとんど使っている方はいないと思われますが、JScript.NETのコードもコンパイルして実行できます。以下、コード例です。
$code=@" static function writeHello() { System.Console.WriteLine("Hello JScript.NET World!"); } "@ $c = Add-Type -Language JScript -MemberDefinition $code -Name "JSTest" -PassThru $c[1]::writeHello()
JScript.NETのスタティックメソッドを用意してやると、Add-Typeコマンドレットによりそのメソッドを持ったクラス(ここではMicrosoft.PowerShell.Commands.AddType.AutoGeneratedTypes.JSTest)が生成されます。-passThruパラメータを指定することでその型情報が変数に代入できます。あとはJSTestクラスのスタティックメソッドを::演算子で呼び出すのですが、なぜかAdd-Typeが目的のクラスの型以外にJScript.NETのグローバルクラス?の型情報も一緒に出力するので、型情報が配列になっています。よってインデックスを指定してからスタティックメソッドを呼ぶようにします。
ここではスタティックメソッドを実行する例を挙げましたが、インスタンスメソッドも実行できるか試してみました。
$code=@" import System; public class JSTest { public function writeHello() { Console.WriteLine("Hello JScript.NET World!"); } } "@ Add-Type -Language JScript -TypeDefinition $code $o = new-object JSTest $o.writeHello()
ところがこれはNew-Objectのところで「New-Object : "0" 個の引数を指定して ".ctor" を呼び出し中に例外が発生しました: "アプリケーションでエラーが発生しました。"」というエラーになってしまいます。コンストラクタの実行でしくっているようですが…。ちなみに明示的にfunction JSTest()というコンストラクタを定義してやってもだめでした。なんででしょうね?
元記事:http://blogs.wankuma.com/mutaguchi/archive/2011/04/26/198645.aspx2011/03/25
ジャグ配列も、多次元配列も、あるんだよ
PowerShellは一次元のobject配列を多用しますが、実は他言語と同様に、多次元配列やジャグ配列(配列の配列)もちゃんとあります。
ジャグ配列
ジャグ配列の作成
PS > $array = @(@("a","b"),@("c","d","e"))
配列の参照
PS > $array a b c d e PS > $array.Length 2 PS > $array[0] a b PS > $array[0][1] b
ジャグ配列の作成は、配列をまず作って、その配列を要素に持つ配列を作成するとできます。とても直感的かと思います。ただしここで注意しなければならない点は、
PS > $array = @(@("a","b") + @("c","d","e"))
のように、配列を+演算子で連結するのは駄目であるという点です。こうしてしまうと単に一次元配列同士が連結された要素数5の一次元配列が生成されてしまいます。
さて、このようにジャグ配列に含まれる配列とその数があらかじめ分かっている場合はこのように,演算子を使えば問題ありませんが、そうではない場合はちょっと工夫が必要です。たとえば「テキストファイルの一行ごとにchar[]配列を作り、それらを要素に持つジャグ配列を作成する」ことを考えてみましょう。まず最初に駄目な例。
$lines = @(Get-Content file.txt) $array = @() foreach($line in $lines) { $array += [char[]]$line }
これは一見うまくいきそうですが、先ほどと同じパターンで$arrayは単なるchar[]の一次元配列になってしまいます。目的のジャグ配列を得るには次のようにします。
$lines = @(Get-Content file.txt) $array = @() foreach($line in $lines) { $array += ,[char[]]$line }
ここでは配列要素の追加処理の際、配列に「,」を前置することによって「配列要素を展開することなく、配列そのものとして扱う」ようにしています。こうすることで$arrayにはchar[]配列そのものが要素として追加され、結果としてジャグ配列が格納されます。
多次元配列
2x2の多次元配列を作成
PS > $array = New-Object "object[,]" 2,2
要素に値を代入
PS > $array[0,0] = "a" PS > $array[0,1] = "b" PS > $array[1,0] = "c" PS > $array[1,1] = "d"
配列の参照
PS > $array a b c d
配列のスライス
PS > $array[@(0,0)] a PS > $array[@(0,0),@(0,1)] a b PS > $array[@(0,0),@(1,0)] a c
配列の作成の仕方がちょっと気持ち悪いですが、これはサイズ固定の一次元配列を作るときと同じ要領です。
配列のスライスも可能で、切り出したい要素のインデックスを「要素のインデックスを表す配列」で指定します。たとえば$array[0,0]を取り出したい場合は$array[@(0,0)]になるわけですね。複数の要素を切り出したい場合は「要素のインデックスを表す配列」の配列(つまりはジャグ配列)を指定することになります。
おまけ:一次元配列のちょっとしたtips
変数、プロパティ、コマンドレットの戻り値などで、その値が配列かそうでないかが事前に分からない場合でも、必ず配列として処理したい場合には@を用います。
$lines = Get-Content file.txt
Get-Contentコマンドレットはテキストファイルが1行の場合は、$linesには文字列の配列ではなく文字列が格納されます(複数行なら行ごとの文字列が格納された配列になる)。よってこの場合$linesが配列かどうかは事前には分からないことになります。テキストファイルの1行目を取得するつもりで $lines[0] とやっても、もしテキストファイルが1行だった場合は、その1行の一文字目が返ってきてしまいます。このような事態を防ぐには、
$lines = @(Get-Content file.txt)
のようにすると、$linesには必ず配列が格納されます。たとえテキストファイルが1行でも、要素数1の配列になります。
これとは逆のケースで、「配列か非配列かわからないが、必ず非配列として扱いたい」場合は次のようにするのも手でしょう。
$value = @($unknown)[0]
この場合だともし$unknownが配列だったとしても、その1要素目をとってきて$valueに格納してくれます。本来なら$unknownが配列かどうか確かめて、その要素数がいくつであるかなどを確認すべきなんですが、「非配列か要素数1の配列どちらかが返されるパターン」というのはADSIやXMLなど扱う際に意外と多くて、そういう場合に有用かと思います。
JavaScriptの配列操作と同じようなことをする方法
$array = @(1..5) # push(配列末尾に値を追加) $array += 6 # unshift(配列先頭に値を追加) $array = @(0) + $array # shift(配列先頭の値を削除) $array[0]; $array = $array[1..($array.length-1)] # pop(配列末尾の値を削除) $array[$array.length-1]; $array=$array[0..($array.length-2)]元記事:http://blogs.wankuma.com/mutaguchi/archive/2011/03/25/197857.aspx
2009/09/23
[Win7]リモートにあるフォルダをライブラリに追加する際の注意点
こんにちは、むたぐちです。Windows 7とWindows Server 2008 R2を使っていろいろと遊ん…研究しています。
Win7はVistaのマイナーチェンジ版とよく言われますが、カーネルからかなりのチューニングが入っており、またシェルもかなり使い勝手が良くなっていて、単にVistaは重いからWin7は軽くしてみました、以上のものがあります。
今回はWin7で加わった新しい機能である、ライブラリについて注意点を書いてみます。
ライブラリは、エクスプローラを開くとデフォルトで表示される、仮想ディレクトリです。デフォルトでは、ミュージック、ビデオ、ピクチャ、ドキュメントの4つがあります。それぞれ、対応する実フォルダと関連付けられていて、複数のフォルダをまるで一つのフォルダのように扱えます。もちろんフォルダを追加することも、ライブラリ自体を新しく作ることもできます。ライブラリはデフォルトで挙げられたものと汎用のものの計5タイプがあり、それぞれ少しずつ機能が違います。たとえば、ミュージックタイプはアーティストごとにグループ化できたりします。
さて、ローカルフォルダをライブラリに追加するのは特に何の問題もありませんが、リモートフォルダをライブラリに追加するにはいくつかの要件があります。
ライブラリは、Windows Search
サービスが作成するインデックスをもとに構築されます。そのため、リモートフォルダはインデックスが構築されている必要があります。Windows Search
サービスが動作しているOSはWindows Vista / Server 2008 以上です。なので、残念ながらXPや2003
Serverなどのリモートフォルダはライブラリに追加できません。2009/10/11訂正。Windows Search
4.0を入れれば可能です。渋木宏明(ひどり) さんありがとうございます。
注:ただし、Windows Media Player 12を使った場合、グループ化などはできないものの、未対応のフォルダでも強引にライブラリに追加できる。ちなみにWMP12のライブラリはエクスプローラのライブラリと同一のものです。
さて、Windows Vista/7ではWindows Search サービスはデフォルトで稼働していますが、Server 2008 /Server 2008 R2 ではデフォルトオフです。なので、これを有効化してやる必要があります。その方法は、役割の追加で「ファイルサービス」を追加し、役割サービスの追加で「Windows Search サービス」を追加します。(この場合、「Windows Server 2003ファイルサービス」は使用不可能となる)
これで、一応リモートサーバーにあるフォルダをライブラリに追加できますが、このままではミュージックライブラリでグループ化ができません。なぜかというと、Server 2008にはWMP11 or 12がデフォルトでは有効化されていないため、インデックスサービスがmp3ファイルなどのアーティスト名等のプロパティ(ID3タグ)を参照できないためです。
WMPを有効化するには、機能の追加で、「デスクトップ エクスペリエンス」を追加します。
これで、晴れて2008/R2にあるフォルダを完全な形でライブラリに追加できます。
ライブラリは非常に面白い機能なのでぜひ使ってみてください。過去の思わぬファイルなんかがみつかったりしますよ。
元記事:http://blogs.wankuma.com/mutaguchi/archive/2009/09/23/181554.aspx2006/12/08
コマンドレットオンラインヘルプ作成 ver2
コマンドレットオンラインヘルプ作成
http://blogs.wankuma.com/mutaguchi/archive/2006/11/07/43965.aspx
の続編です。本文中のコマンドレットにリンクを張るようにしました。その他いろいろ改善。
旧バージョンをお使いの方は一度*.htmlを削除してください。ファイル名が変わりました。
function Sanitize{ #サニタイズ処理 param ([string]$strSource) return ($strSource.Replace("&","&").Replace("<","<").Replace(">",">")) } function MakeLink{ #用語にリンクを張る param ([string]$strSource) foreach ($key in $keys){ $strSource = $strSource -replace "(\s)($key)(\s)", ("`$1`$2`$3") } return ($strSource); } #キーワードを格納するArrayList $keys = new-object System.Collections.Arraylist get-help -category all | ?{"Cmdlet","Provider","HelpFile" -contains $_.category}| %{if ($_.Name -ne $null) {[void] $keys.Add($_.Name)}} # "Cmdlet","Provider","HelpFile"のカテゴリを持つヘルプをHTML化 get-help -category all | ?{"Cmdlet","Provider","HelpFile" -contains $_.category}| %{"" + $(MakeLink $(Sanitize (get-help $_.Name -detail|out-string))).Replace("`r`n",""| out-file($_.name + ".html")} $temp="
") + "戻る
名前 | 種類 | 簡易説明 |
---|---|---|
" + $_.name + " | " + $_.category + " | " + $_.synopsis + " | "} # Cmdlet,Provider,HelpFileの各ヘルプ get-help -category all | ?{"Cmdlet","Provider","HelpFile" -contains $_.category} | sort category,name| %{$temp+="
" + $_.name + " | " + $_.category + " | " + $_.synopsis + " | "} $temp+="
コマンドレットとパイプを多用するスクリプトの見やすい記法ってなんかないですかね?自分で読んでもよくわかりませんなこれw
あと気づいたこと。メソッドを呼び出すときはmethodname
(parameter)
のようにメソッド名と引数の間にスペースを入れてはいけない!VBSとかになれているとはまります。
関数の呼び出しはfuncname param1
param2
のようにする(引数は,で区切るのではない!)。関数の戻り値を読むときは$(funcname param1
param2)
のようにする。関数中で何か値が返されるとそれがそのまま関数の戻り値になる(returnは明示しなくてもいいということ。複数の値を返すと戻り値は[object[]]の配列になる。逆に値を返したくない場合は[void]にキャスト)
-replace演算子でサブマッチ文字列は$1,$2..に格納される。$は変数の頭文字なので ` (アクサン グラーブ)でエスケープし、`$1とする。もしくは''(シングルクォーテーション)でくくり'$1'のようにする。
そうそう、
get-help -category all | ?{"Cmdlet","Provider","HelpFile" -contains
$_.category}|
は、なんで
get-help -category Cmdlet,Provider,HelpFile
にしなかったかというと、このようにしてもなぜかAliasが含まれてしまうからです。Cmdletを指定するとAliasが含まれる仕様のようです。
でも-containsの使い方がわかってよかったです(ちょっとトリッキー?)
Copyright © 2005-2018 Daisuke Mutaguchi All rights reserved
mailto: mutaguchi at roy.hi-ho.ne.jp
プライバシーポリシー