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 何か変だったら()付けてみる とかでもそれ程問題にはならないかもしれません。

2017/12/10

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

PowerShellはオブジェクトを扱うシェルですが、別にテキストデータを扱えない訳ではありません。むしろ、PowerShellで取得したデータをテキストファイルとして保存したり、スクリプトで用いるデータをテキストファイルで保存しておくことは日常的に行われることだと思います。

ただし、PowerShellで扱うデータはオブジェクトであり、テキストファイルは文字通り文字列であることから、コマンドレットを用いる等、何らかの手段で変換が必要になります。また、テキストデータ形式にも様々な種類があり、それぞれメリット、デメリットが存在します。今回の記事では、PowerShellで用いるデータを保持しておく際のテキストデータ形式について比較をしてみます。

プレーンテキスト

プレーンテキスト、すなわち書式なしのテキストファイルです。もっともシンプルな使い方をする場合、文字列配列の各要素に含まれる文字列が、テキストファイルの1行と対応します。

書き出し
$lines | Set-Content -LiteralPath file.txt -Encoding UTF8

$linesは文字列変数です。

特に理由がなければ文字コードはUTF-8で良いと思います。

追記
$lines | Add-Content -LiteralPath file.txt -Encoding UTF8

Add-Contentは実行のたびにファイルを開いて、書き込んでから閉じるという動作をするので、1行ずつforeachで実行するのはNGです。

読み込み
$lines = @(Get-Content -LiteralPath file.txt -Encoding UTF8)
メリット
  • 文字列配列をテキストファイルに書き出すのは多分これが一番楽だと思います。
  • 書き出したデータは人間にも読みやすい。 編集もしやすい。
デメリット
  • 文字列だけを保存しておきたいというケースがそもそも少ない。
CSV

コンマ等の特別な文字で区切り、1行あたりに複数のデータを保存できる形式です。

PowerShellのコマンドレットで扱う場合、オブジェクトが持つプロパティがヘッダ列名に対応します。各行にオブジェクト配列1要素のプロパティ値が、カンマ区切りで保持されます。

書き出し
$objects | Export-Csv -LiteralPath file.csv -NoTypeInformation -Encoding Default

$objectsは任意のオブジェクト配列です。必要であればSelect-Objectコマンドレットを併用して、プロパティを絞り込みます。

文字コードはExcelでそのまま読み込み/書き出しができるDefault(日本語環境ではShift_JIS)がお勧めです。(最近のExcel2016ならUTF8も一応読めますが)

追記
$objects | Export-Csv -LiteralPath file.csv -Append -NoTypeInformation -Encoding Default
読み込み
$objects = Import-Csv -LiteralPath file.csv -Encoding Default
メリット
  • オブジェクトのプロパティ値が、すべて数値あるいは文字列で表現できる値を持つ場合に最も適合する。
  • 人間にも読みやすく、ある程度は編集もできる。
  • Excelで開ける。
デメリット
  • オブジェクトのプロパティが、数値と文字列以外のオブジェクトである場合、すなわち、階層構造を持つデータの保存には適さない。
  • 数値も文字列として読み込まれてしまうので、数値として扱いたい場合は変換が必要になる。
  • Export-CsvとImport-Csvで扱うCSVファイルはヘッダが必須。つまり、ヘッダなしのCSVファイルが既にあって、それを読み書きするという用途には適さない。(できなくはないが)
  • 書き出し時の列順を制御することができない。つまり、PowerShellで書き出したCSVを、列順が固定であるとの想定である他のプログラムで読み込むことは基本NG。
  • 書き出し時、1つ目の要素に存在しないプロパティは、2つ目以降では存在しないものとして扱われる。同種のオブジェクトで構成される配列なら通常は問題ないのだが、要素によって動的に追加されるプロパティがあったりなかったりすると厄介。(ADでありがち)
JSON

JavaScriptのような表記でデータを保持するデータ形式です。データの受け渡しに様々な言語で利用できます。Web APIでもよく利用されます。

PowerShellではv3からJSONを扱うコマンドレットが提供されています。

書き出し
$objects | ConvertTo-Json | Set-Content -LiteralPath file.json -Encoding UTF8
読み込み
$objects = Get-Content -LiteralPath file.json -Encoding UTF8 -Raw | ConvertFrom-Json
メリット
  • CSVと異なり、階層構造を持ったデータでも扱える。
  • CSVと異なり、数値は数値型のまま読み書き可能。 (整数値はint、小数値はdecimal)
  • 人間にもまぁまぁ読めるし、頑張れば編集できなくもない。
デメリット
  • -Depthパラメータによりプロパティを展開する階層の深さを指定はできるが、プロパティに応じて深さ指定を変化させるというようなことはできない。基本的には、自分で構築したPSCustomObjectを使うか、JSON化する前に自分で元オブジェクトを整形しておく必要がある。
  • 直接ファイルに書き出し、追記、ファイルから読み込みするコマンドレットはない。
  • 実は細かい話をしだすと色々と罠があります…。
CLIXML

PowerShellではPSリモーティング等、プロセス間でオブジェクトのやり取りを行う際に、CLIXML形式を介してシリアライズ/デシリアライズが実行されます。シリアライズ対象によっては、完全に元のクラスのオブジェクトに復元されます。(復元されないオブジェクトにはクラス名にDeserialized.との接頭辞が付与され、プロパティ値のみ復元される)

ユーザーもコマンドレットを用いて、任意のデータをCLIXML形式でシリアライズし、XMLファイルとして保存することができます。

書き出し
$objects | Export-Clixml -LiteralPath file.xml
読み込み
$objects = Import-Clixml -LiteralPath file.xml
メリット
  • 元のオブジェクトの構造、プロパティ値と型情報を含めてほぼ完全にテキストファイルに保存できる。
  • 復元したオブジェクトはプロパティ値を参照できるのはもちろん、オブジェクト全体が完全にデシリアライズされ、元の型に戻った場合には、メソッドを実行することも可能。
  • 例え元の型に戻らず、Deserialized.との接頭辞が付いた状態でも、コンソールに表示する場合は元の型のフォーマットが使われるので見やすい。
デメリット
  • すべてのオブジェクトが元の型に戻せるわけではない。戻せるかどうかは確認が必要。
  • 人間が読み書きするようなものではない。

ちなみに、ConvertTo-Xmlという似たようなコマンドレットがありますが、出力形式はCLIXMLではない上、復元の手段もなく、かといって別に読みやすいXMLというわけでもなく、正直何のために使うのかよく分かりません(適切なxsltでも用意すればいいのかな?)。まだConvertTo-Htmlの方が使えそうです。

psd1

psd1は「PowerShellデータファイル」で、モジュールマニフェストやローカライズデータに使われるファイル形式です。スクリプトファイルの1種ですが、数値や文字列リテラル、配列、連想配列、コメントなど基本的な言語要素のみ使用可能です。PowerShell 5.0以降ではImport-PowerShellDataFileコマンドレットを用いて、任意のpsd1ファイルのデータを読み込み、変数に格納することが可能です。

書き出し

書き出し用のコマンドレットはありません。

読み込み

例えば以下のような内容をbackup_setting.psd1として保存しておきます。ルート要素は必ず連想配列にします。

@{
	Directories = @(
		@{
			From = "C:\test1"       # コピー元
			To = "D:\backup\test1"  # コピー先
			Exclude = @("*.exe", "*.dll")
			Recurse = $true
		},
		@{
			From = "C:\test2"
			To = "D:\backup\test2"
			Exclude = @("*.exe")
			LimitSize = 50MB
		},
		@{
			From = "C:\test3"
			To = "D:\backup\test4"
		}
	)
	Start = "0:00"
}

なお、dataセクションで全体を括ってもいいですが、psd1で許容される言語要素はdataセクションより更に制限がきついので、敢えてしなくてもいいんじゃないかと思います。

このファイルは以下のように読み込めます。

$setting = Import-PowerShellDataFile -LiteralPath backup_setting.psd1

$settingには連想配列が格納され、以下のように値が参照できます。

$setting.Directories | foreach {Copy-Item -Path $_.From -Destination $_.To}
メリット
  • PowerShellの構文でデータを記述できる。
  • 通常のps1ファイルを呼び出すのとは異なり、式の評価やコマンド実行などはされない分、セキュアである。
  • 配列と連想配列の組み合わせにより、JSONライクな階層構造を持てる。型情報も保持される。
  • JSONとは違い、コメントが入れられる。
デメリット
まとめ

PowerShellで扱うデータをテキストファイルとして保存する際には、各テキストデータ形式の特性を理解し、メリット、デメリットを踏まえて選定する必要があります。

また、当然ながらテキストファイルに保持することが不適切なデータもありますので、そこは注意してください。(画像データを敢えてBase64とかでエンコードしてテキストファイル化する意味があるのか、とかですね)

個人的には…

ちょっとした作業ログ等を記録しておきたい→プレーンテキスト

.NETオブジェクトの一部のプロパティだけ抜き出してファイル化したい→CSV

自分で構築したPSCustomObjectをファイル化したい→JSON

.NETオブジェクト全体をファイル化したい→CLIXML

スクリプトで使う設定データを用意したい→psd1

みたいな感じでなんとなく使い分けていると思います。psd1はまだ採用例はないですが…。

今回はビルトインのコマンドレットで扱えるもののみ取り上げましたが、他にもyaml等のテキストデータ形式が存在し、有志によるモジュールを用いて扱うことが可能です。

2017/12/01

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

毎年恒例のPowerShell Advent Calendar、今年も始まりました。ここ数年は私がトップバッターを務めさせていただいて、1年間のPowerShell界隈の出来事をさくっとまとめてみています。→2016年2015年

昨年2016年はPowerShell 10周年の年であり、PowerShell 5.1、Windows Server 2016、Nano ServerとPowerShell Core Editionが各々正式版としてリリースされ、さらにはPowerShellがオープンソース化、マルチプラットフォーム展開を始めるという大きな変革があった年でした。

今年2017年は昨年ほど大きな変化はないとはいえ、昨年のOSS化からのマルチプラットフォーム展開を着実に進行させた年だと言えると思います。

以下、いくつかトピックを紹介します。

WMF 5.1インストーラーの登場

PowerShell 5.1を含むWMF (Windows Management Framework) 5.1は、Windows2016に同梱され、昨年8月にリリースされたWindows 10 Anniversary Updateにも同梱されました。今年1月に公開されたWMF5.1のインストーラーは、下位OS(Windows 7/8.1/Server 2008 R2/2012/2012 R2)のためのものです。

なお、Win10/2016に同梱のPester(テストフレームワーク)やPSReadline(コンソール入力支援)についてはWMF5.1には含まれていないので、別途PowerShellGetでインストールするのがお勧めです。

Azure Cloud ShellでのPowerShell サポート

Webブラウザ上で動作するAzureの管理用シェルである、Azure Cloud ShellではまずBashがサポートされていましたが、今年9月にPowerShellもサポート(まだプレビューですが)されました。

自動的に認証された状態で最新のAzure PowerShellのコマンドが使え、AzureのリソースにAzure:ドライブを介してアクセスすることが可能です。

注意点があって、このPowerShell版Azure Cloud Shell、どうも現バージョンでは(Nano Serverではなく)Windows Server Coreのコンテナ上で動作しているらしく、Bashに比べ起動が若干遅いのと、実体はPowerShell CoreではなくWindows PowerShell 5.1であることはちょっと念頭においておいたほうがいいかもしれません。

これ、PowerShell CoreではまだAzure PowerShellの全機能がサポートされてないからだと思うんですが、今後に期待ですね。

PowerShell for Visual Studio Code 正式版リリース

先だってオープンソース化された、マルチプラットフォーム対応のコードエディタであるVisual Studio CodeでPowerShellスクリプトの開発を行うためのExtension、PowerShell for Visual Studio Codeの正式版(1.0)が5月に公開されました。なお、現時点での最新バージョンは1.5.1となっています。

当初は、PowerShellに付属の標準スクリプト開発環境、PowerShell ISEの方が多機能だったようにも思いますが、今はもう完全にISEの機能を追い越したんじゃないかと思います。シンタックスハイライト、インテリセンス、デバッグ、コンソールといった基本機能はもちろん、Gitによるバージョン管理もVSCode自体でサポートされていることに加え、静的解析機能を提供するPowerShell Script Analyzer、テストフレームワークのPester、プロジェクト管理機能を提供するPlasterなどが統合されており、本格的な開発環境となっています。

また当然ではありますが、マルチプラットフォーム対応なので、WindowsではWindows PowerShell 、LinuxやMacではPowerShell Coreの開発が各々可能です。

公式ブログでのアナウンスによれば、今後ISEがなくなることはありませんが、ISEに新機能が追加されることはなくなり、PowerShell for VSCodeの開発に注力されることになります。ISEはとにかく標準添付である(GUI有効ならサーバーOSでも動く!)という強みがあり、シンプルなスクリプト記述であればそこそこ便利に使えるので、これからもシチュエーションに応じて使い分けて行けば良いのかなと思います。

PowerShell Core RCのリリース

昨年OSS化したPowerShell Core 6はα版として開発が続いていましたが、今年5月にはβ版となり、先月(11月)、ついにRC(Release Candidate)となりました。6.0.0のGAリリースは来年1月になるそうです。

OSS化直後からRCに至るまでの変更点は多岐に渡り、とても一言で説明できるものではないですが、ポイントとしては以下の3点に集約されるんじゃないかと思います。

  1. PowerShellが長年抱えていた問題点の洗い出しと修正

    PowerShellがOSS化した当初は、ほとんどがWindows PowerShell 5.1のコードそのままであったと言ってよいかと思います。10年以上増改築が繰り返されたコードが突如、全世界に公開されたわけです。コミュニティの力でバグや変な仕様といった問題点が洗い出され、どんどん修正されていきました。
    また、不足していると思われる機能はどんどん追加されました。既存コマンドレットのパラメータが増えるというパターンが多かったように思います。

    特筆すべきは、破壊的変更であっても妥当性があれば躊躇せずに取り入れていったことかと思います。これは英断ではありますが、一方でWindows PowerShell 5.1とPowerShell Coreでは細かいところで非互換性が色々出ていますので、移行の際には注意を要します。

  2. マルチプラットフォーム対応

    前述の通り、OSS化した当初のPowerShell 6.0は、ほぼWindows PowerShell 5.1なので、Windowsでしか動作しない部分が多々ありました。それをLinuxやMac環境でも動作するように多くの修正が加えられました。

    ところで、PowerShell 6.0は当初、条件付きコンパイルにより、Windows用に.NET Framework(Full CLR)をターゲットにして、Desktop Edition相当のPowerShellをビルドすることが可能でした。

    しかしβ版になったタイミングで、OSS版PowerShell 6.0は、「PowerShell Core 6.0」すなわち、「.NET Core上で動作するPowerShell Core」であることが明確にされました。よってFull CLRターゲットのビルドはできなくなり、β6ではついにFull CLR対応のコードはすべて削除され、Core CLR対応のコードのみとなりました。

  3. Windows PowerShell用コマンドレットの呼び出し

    PowerShell Core 6.0にはいくつかのコマンドレットが同梱されていますが、Windows PowerShell 5.1に含まれているすべてのコマンドレットを網羅しているわけではありません。また、WindowsやWindows Serverの管理のために提供されている、OS付属のモジュール群もCore 6.0には含まれておらず、α版の段階では実行も不可能(だったはず)でした。

    β1からターゲットが.NET Core 2.0に移行したことにより、.NET Standard 2.0がサポートされました。このことによって、Windowsに付属の数千ものコマンドレットを初めとするWindows PowerShell用コマンドレット(要はFull CLRをターゲットとしてビルドされたもの)のうち、.NET Standard 2.0に含まれるAPIしか使われていないものであれば、原理的にはPowerShell Coreでも実行可能になりました。

Windows PowerShellの今後

さて、PowerShell Core 6.0がまもなく正式版リリースということですが、では従来のWindows PowerShellはどうなるのか、という話について。

公式ブログのアナウンスによれば、Windows 10やWindows Server 2016に付属のWindows PowerShell 5.1については、今後もサポートライフサイクルに則り、重大なバグフィックスやセキュリティパッチ提供等のサポートは継続されます。もちろん下位バージョンのOS/Windows PowerShellも同様です。

しかしながら、Windows PowerShellに新機能が追加されることは今後はなく、開発のメインはPowerShell Coreへと移行します。つまりは、PowerShell Coreの開発の中で追加された新機能、変更点、バグフィックスについては、基本的にはWindows PowerShellとは無関係ということです。

また、現状ではPowerShell CoreはWindows PowerShell環境に追加インストールし、サイドバイサイド実行が可能となっていますが、将来的にPowerShell CoreがWindowsに同梱されるかどうかについては言及されておらず、今のところは不明です。

以下は私見になります。

このような状況で、Windows PowerShellユーザー、とりわけWindows Serverの管理はするが、Linuxとかは特に…というユーザーはこれからどうすべきか?という点は割と悩ましいところだと思います。個人的には、WindowsやWindows Serverを管理するスクリプトが現時点であるなら、それを無理に今すぐCore対応にする必要はないと思います。現時点で今すぐCoreに移行すべき理由というのはとくに無いと感じます。Coreで追加、改善された機能はあるものの、Coreには無い機能もたくさんあるからです。
また新規にスクリプトを作る場合でも、対象がWindowsに限定されるのであれば、Windows PowerShell用に作れば良いのではないかと。OS付属のコマンドレットの動作は確実に保証されているわけですから。

ただし、ご存じの通りWindows10とServer 2016は半期に一度の大型アップデートで新機能が次々追加されていきます。その過程でPowerShell Coreが含まれるようになったり、Coreのみ対応のコマンドレットが追加される可能性は無きにしもあらずなのではないかとも思います。なので、Coreの状況をチラ見しつつ、未来に備えておく必要はあると思います。Windows10/Server2016の「次」も見据えて。

それとPowerShellでWindowsもLinuxも面倒みていきたい、という野心がある方は、Coreを採用していくのがいいのではないかと思います。ただし、現状しばらくは茨の道ではあるとは思います。

あとはスクリプトやモジュールを作成し公開する方は、より多くの環境で使われるように、可能であればCore対応を進めるのは悪くないんじゃないかと思います。

おわりに

他にもWin10/Server2016におけるPowerShell 2.0の非推奨化の話とか、DSC Core構想とか、なにげに結構いろいろ話題はありました。

さて、Windows PowerShellとしては一端落ち着いた感もある界隈ですが、PowerShell Coreとしてはこれからも活発に動いていくものと思います。注目していきたいですね。

そんな2017年の締めくくり、今年はどんな記事が集まるでしょうか。PowerShell Advent Calendar 2017の参加、お待ちしております。

2016/01/03

PowerShellのパイプライン処理

まず、PowerShellのパイプライン処理について軽くおさらいします。

例えば、@、A、Bをそれぞれ何らかのコマンドとしたとき、

@|A|B

というパイプラインがあったら、処理の流れは、

@begin→Abegin→Bbegin→(@process→Aprocess→Bprocess→)×n→@end→Aend→Bend

の順に実行されます。(processブロックで「1入力に対し1出力する」場合以外は必ずしもこうならないですが)

さて、AかBのprocessブロック実行中に、何らかの条件を満たした時、パイプラインのprocessの後続処理を打ち切りたい場合はどうすれば良いでしょうか。

まずはbreakを使った駄目な例

ネットでよく見かける以下のようなコード、すなわち「パイプラインはbreakで処理を打ち切ることができる」というのは実は正しくないのです。

function Select-WhileTest
{
    [CmdletBinding()]             
    param (             
        [parameter(ValueFromPipeline=$true)] 
        [psobject[]]$InputObject,
        [parameter(Position=0)]           
        [scriptblock]$Predicate
    )

    process
    {
        if(!(&$Predicate))
        {
            break
        }
        $InputObject
    }
}

このコードはv2までではそもそも正しく動作しませんが、v3以降では条件によっては正しく動作しているように見えるのが、誤解の元なのかと思います。(というか私も誤解してました。)

例えば、

$result = "初期値"
$result = &{end{foreach($i in (1..5)){$i}}} | Select-WhileTest {$_ -lt 3}
Write-Host "`$resultは $result です。"

のようにすると、

$resultは 1 2 です。

のように、想定した通りの結果が得られます。このように、上流のスクリプトブロックのendブロック内にforeachなどループブロックが存在し、そのループブロック内で下流に値を出力している場合はうまくいきます。(ちなみに、スクリプトブロック直下に記述するのとendブロック内に記述するのは等価。)

しかし、上流にループブロックがない場合、例えば

$result = "初期値"
$result = 1..5 | Select-WhileTest {$_ -lt 3}
Write-Host "`$resultは $result です。"

とすると、コンソールに1と2が改行区切りで表示されますが、ホストに表示されるだけで$resultには値は格納されません。そしてスクリプト化して実行した場合は、Write-Hostが実行されることすらなく、スクリプトが終了してしまいます。

breakだとなぜうまくいかないのか

結局どういうことかというと、パイプライン下流のbreakは、パイプラインを打ち切る処理をするのではなく、単に一つ上流のブロックをbreakする処理に過ぎないのです。

パイプライン上流にループブロックがある場合は、そのループブロックをbreakしますが、それ以外の場合はスクリプトのbegin, process, endのいずれかのブロックがbreakされてしまい、結果としてスクリプトが終了してしまうわけですね。

そして、このSelect-WhileTest関数だと大丈夫ですが、processブロックの中にループブロックを記述し、その中でbreakを書くのは当然ダメです。単にそのループを抜けるだけなので、上流の出力は止まってくれません。

breakではなくcontinueを使う場合も基本は同じ結果です。しかもcontinueは所詮、その名の通り継続処理なので、上流に以下のような無限リストがあると無限ループに陥ってしまいます。

&{begin{$i = 0} process{while($true){$i++; $i}}}|Select-WhileTest {$_ -lt 3}

breakの代わりに、

throw (New-Object System.Management.Automation.PipelineStoppedException)

を実行する方法も見かけますが、これはループブロックがあっても強制的にスクリプトが終了するので余計ダメです。try...catchでエラートラップすればスクリプトの終了は回避できますが、「パイプラインが正常終了せずエラーを出した」扱いであることには代わりないので、やはり出力を変数に格納することができません。

ダミーループを用いる、取りあえずの解決策

前述のbreakを使った方法の問題点のうち、上流にループブロックがないとスクリプトが終了してしまい、出力を変数に代入することもできない問題は、とりあえず解決する方法があります。

以下のように、呼び出す時にパイプライン全体をダミーのループブロックでラップすれば良いのです。

$result = "初期値"
$result = do{
    1..5 | Select-WhileTest {$_ -lt 3}
}until($true)
Write-Host "`$resultは $result です。"

このようにしておけば、breakはパイプラインの外側のdo...untilを抜ける効果になるので、スクリプトが終了する心配も、値を出力しない問題も起こりません。

元々、パイプライン上流にループブロックが存在する場合でも、単にdoループ内の処理が1回走るだけなので、特に問題は起きません。1回だけ処理を実行するダミーループなら、for($i=0; $i -lt 1; $i++){}とかでも良いです。

ただ…この記述を美しいと思う人は多分いないですね。事情を知らないと意味不明ですし。そして、breakを記述する側の関数には、前述の通りのループブロック内では値を出力できないという制限は残ったままになります。

やはりbreakでパイプラインを打ち切るのは、本来想定された動作かと言われるとかなり微妙なところだと思います。(v3で一応動くようになったとはいえ)

この方法についての参考記事:Cancelling a Pipeline - Dreaming in PowerShell - PowerShell.com ? PowerShell Scripts, Tips, Forums, and Resources (ただしv2準拠の内容であることに注意)

ところで、Select-Object -Firstは…

さて、話は変わって、PowerShell 3.0からはSelect-Object -First の処理が変わったことについては、ご存知の方も多いかと思います。

具体的には、v2までは単にパイプライン処理をすべて終了してから、最初のn件を抽出する処理であったn件のパイプライン出力がされた後は、入力をすべてフィルタし出力に流さなくなる動作であったところが、v3からはn件のパイプライン出力がされた時点で、パイプラインの処理を打ち切るようになりました。(1/5修正)

つまり、

$result = 1..5| &{process{Write-Host "Process:$_"; $_}}| Select-Object -First 2
Write-Host "`$resultは $result です。"

というスクリプトは、v2までは

Process:1
Process:2
Process:3
Process:4
Process:5
$resultは 1 2 です。

のようにパイプライン出力は指示通り2件であるものの、上流の処理は結局、全部実行されてしまっています。

一方v3以降では、

Process:1
Process:2
$resultは 1 2 です。

のように、きちんと上流の処理を打ち切ってくれています。

つまり、ここまで述べてきたパイプライン処理の打ち切りは、実はv3以降のSelect-Object -Firstでは実現できているということです。これと同じことを我々も自作関数の中でやりたいわけです。

ではSelect-Object -Firstは具体的にどういう処理をしているかというと、StopUpstreamCommandsExceptionという例外をthrowすることでパイプライン処理の打ち切りを実現しています。この例外はまさに名前の通り、パイプライン上流の処理を中止するための例外です。この例外を自作関数でthrowしてやればうまくいきそうです。

しかし、この例外は非publicな例外クラスであることから、New-Objectコマンドレットなどでインスタンス化することはできません。リフレクションを駆使する必要がでてきます。
参考:PowerShell 3.0からはじめるTakeWhile - めらんこーど地階

(1/5追記)参考2:パイプラインの処理を途中で打ち切る方法のPowerShell版 - tech.guitarrapc.cóm(Add-TypeでC#経由でリフレクションしてます。)

頑張ればできなくはないですが、もっと楽な方法はないものか…と思ってあきらめかけたところ、いい方法を思いついたので紹介します。

Select-Object -Firstを利用する方法

Select-Object -Firstでできることが我々には(簡単には)できない。ならばどうするか。Select-Object -Firstを使えばいいじゃない。という発想です。

function Select-While
{
    [CmdletBinding()]             
    param (             
        [parameter(ValueFromPipeline=$true)] 
        [psobject[]]$InputObject,
        [parameter(Position=0)]           
        [scriptblock]$Predicate
    )

    begin
    {
        $steppablePipeline = {Select-Object -First 1}.GetSteppablePipeline()
        $steppablePipeline.Begin($true)
    }

    process
    {
        if(!(&$Predicate))
        {
            $steppablePipeline.Process($InputObject)
        }
        $InputObject
    }

    end
    {
        $steppablePipeline.End()
    }
}

scriptblockにはGetSteppablePipelineというメソッドが存在し、このメソッドによりSteppablePipelineオブジェクトが取得できます。これは何かというと、要は「スクリプトブロック内のbegin, process, endを個別に実行する」ための機能です。
参考:PowerShell: ◆パイプライン入力・パラメータ入力対応のGridView出力関数を作る(私自身も以前この記事で知りました。)

{Select-Object -First 1}というスクリプトブロックは、1回目に実行するprocessブロック内でStopUpstreamCommandsExceptionを出してくれます。

よって、自作関数のprocessブロック内のパイプライン処理を打ち切りたい箇所で、SteppablePipelineオブジェクトのProcessメソッドを使うことで、{Select-Object -First 1}のprocessブロックの処理を呼び出してやればいいわけです。

このようにして作成したSelect-While関数を以下のように実行してみると、

# 上流にループあり
$result1 = &{end{foreach($i in (1..5)){$i}}} | Select-WhileTest {$_ -lt 2}
Write-Host "`$result1は $result1 です。"

# 上流にループなし
$result2 =  1..5 | Select-While {$_ -lt 3}
Write-Host "`$result2は $result2 です。"

# 上流に無限リスト
$result3 = &{begin{$i = 0} process{while($true){$i++; $i}}} | Select-While {$_ -lt 4}
Write-Host "`$result3は $result3 です。"

結果は

$result1は 1 です。
$result2は 1 2 です。
$result3は 1 2 3 です。

となり、少なくとも今まで述べてきた諸問題はすべて解消していることが分かると思います。

このSelect-While関数は、スクリプトブロックで指定した条件を満たさなくなったときに、パイプライン処理を打ち切ってくれるものですが、この「Steppable Select -First 方式」を使えば他の自作関数でも、割と気楽に呼べるんじゃないかなと思います。ループブロック内で呼び出すことももちろん可能です。

ただし問題点はある

これはSelect-Object -FirstというかStopUpstreamCommandsExceptionあるいはPowerShellのパイプライン機構の仕様に由来する問題であると思われるので、どうにもならないことではあるんですが、一点だけ注意事項があります。

$result = 1..5| &{
    begin
    {
        Write-Host "Begin"
    }
    process
    {
        Write-Host "Process:$_"
        $_
    }
    end
    {
        Write-Host "End"
    }
}| Select-While {$_ -lt 2}

Write-Host "`$resultは $result です。"

これの結果は

Begin
Process:1
Process:2
$resultは 1 です。

となり、なんとendブロックが実行されていません。Select-While {$_ -lt 2} の代わりに Select-Object -Firstを使っても、同様にendは実行されません。

つまり、StopUpstreamCommandsExceptionというのはパイプライン処理を打ち切って、そこまでの出力値を正しくパイプライン出力として出してくれますが、やってくれるのはそこまでで、最後のendブロック処理はしてくれません。

これは十分注意が必要な点で、自作関数内でbeginブロックで確保したリソースをprocessブロックで利用して、endブロックで解放する…という、いかにも書いてしまいそうなパターンは、実はNGなんですね。何も上のようにマニアックなことをしなくても、単に下流でSelect-Object -Firstを使うだけでアウトです。

じゃあ、リソースの取り回しはどうするのが良いの、って話もありますが、それはまたの機会にしましょう。

(1/5追記)あえとすさんの記事が参考になります。:パイプライン処理の後始末をしよう - 鷲ノ巣 ただ、この方法ではパイプライン下流でthrowされた場合はトラップできないぽいですね。コマンドレットクラスの場合はIDisposable実装で良さそうです。

ここからは私見ですが、StopUpstreamCommandsExceptionが後付けかつ非パブリックなところとか、パイプラインを合法的に脱出するステートメントが今に至るまで用意されていないところとか、パイプラインを何とかして途中で打ち切っても、endは実行されないところとかを見ていると、そもそもPowerShellではパイプライン処理の中断というのは、あまり想定してない操作なのかなぁ、という気がしてきています。

上記のような裏技を使って回避するのも一案ではあるとは思いますが、そもそも「パイプライン処理の中断はイレギュラー」と考えて、そういう処理は避けて、必要に応じて別のアプローチを取ることも考えた方がいいのかもしれません。

2015/12/01

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

アドベントカレンダーは今年で5回目ですが、例年よりだいぶ参加者が少ないので、敢えて完走を目指さずまったりいきましょう。

とはいえ今年から来年にかけては、PowerShellの変革の年といっても過言ではないかと思います。

今年7月にはWindows 10のリリースとともに、WMF (Windows Management Framework) 5.0 / PowerShell 5.0 (2012R2/8.1向けにはProduction Preview)が登場しました。(私の書いたPS5.0新機能のセッション資料はこちら。)

PowerShell 5.0では特に、"Infrastructure as Code"、すなわちインフラの構成をコードで記述可能にし自動化するための機能である、PowerShell DSC周りが格段にパワーアップしています。DSCの機能増強により、Microsoft Azure等のクラウド、オンプレミスのサーバーはともに大きな恩恵を受けることが期待されます。Azure DSC Extensionも追従する形で凄まじい勢いでバージョンアップしてますね。

PowerShell 5.0の登場に合わせて、各種のPowerShell関係のモジュールやアプリケーションが新登場していますが、これらはいずれもOSS(オープンソースソフトウェア)となりました。

一例を挙げると、DSCで用いるロジック本体となる「DSCリソース」を作製する際に有用なDSCリソースキット、対応リポジトリをプロバイダという形で拡張可能であるパッケージ管理システムPackageManagement、PowerShellモジュールを専用リポジトリであるPowerShell Galleryからコマンド1発で取得可能となるPowerShellGet、PowerShellスクリプトの静的解析を行うスクリプトアナライザー、Windowsのみならず他プラットフォームの一括管理を目指すDSC for Linux等々です。

先日OSS化したばかりの、マルチプラットフォーム対応のコードエディタであるVisual Studio Codeと、PowerShellスクリプトが記述可能となるPowerShell Extensionあたりもトピックとして熱いですね。

Visual Studio 2015には、VSでPowerShellスクリプト開発を行うためのOSSなプラグインであるPowerShell Tools for Visual Studioが標準搭載されたことも記憶に新しいですね。(12/1追記)

逆に、PowerShellのテストスクリプトを記述するためのフレームワーク(DSL)であるPesterや、コンソールの入出力をパワーアップさせるPSReadlineといった、OSSで開発されている既存のPowerShellモジュールが、Windows 10に標準機能として取り込まれるなど、OSSとの関係性については大きく変化したと言えるでしょう。

そして次期Windows Serverである、Windows Server 2016の足音も聞こえてきました。現在はTP4が公開されており、試すことができます。Windows Server 2016の目玉は何といっても、Windows ContainersNano Serverでしょう。

アプリケーションをコンテナという単位で配置し、自由なすげ替え、あるいは使い捨てが可能な環境を構築するツールであるDockerというOSSに対し、インターフェースの互換性を持たせたWindows版コンテナがWindows Containersです。

そして、コンテナ機能を最大限に活用するためにフットプリントの最小化を目指し、GUIどころかコンソールすら廃した新しいWindows Serverの形態が、Nano Serverです。

Windows ContainersとNano Serverの管理は当然、PowerShellがメインとなります。また、ようやくWindowsにやってくる、SSHクライアントとサーバーはPowerShell上で動くようです。

PowerShellは5.0に進化することで、足回りを強化しました。そして各種OSSと連携して、クラウドとオンプレミスのサーバー群の基礎を支える存在として、ますます重要性を帯びていくことでしょう。

昨今のPowerShellを取り巻く状況は、私の理解ではざっとこんな感じです。この中に興味を持たれたテーマはありませんか? もちろん、新機能以外にも、まだまだ知られていない機能や利用法も埋もれていると思います。

もしそんなテーマがあったら、PowerShell Advent Calendar 2015で共有していただければ嬉しいなあと思います。

というわけで、例年にない感じの初日記事を書いてみました。今回のアドベントカレンダーもどうぞよろしくおねがいします。

2015/11/14

既出ですが、まとめておきます。

今回登場する関数は、elevate.ps1という名前で一つにまとめて保存しておくと便利です。

起動中のスクリプトが管理者権限で実行されているかどうかの確認
function Test-Admin
{
     (
        [Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::
        GetCurrent()
     ).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}

スクリプト中でこのTest-Admin関数を実行してTrueが返れば、スクリプト(もしくはコンソール)は管理者権限で実行されている。

管理者権限で任意のスクリプトを起動
function Start-ScriptAsAdmin
{
    param(
        [string]
        $ScriptPath,
        [object[]]
        $ArgumentList
    )
    if(!(Test-Admin))
    {
        $list = @($ScriptPath)
        if($null -ne $ArgumentList)
        {
             $list += @($ArgumentList)
        }
        Start-Process powershell -ArgumentList $list -Verb RunAs -Wait
    }
}

Start-ScriptAsAdmin "任意のps1ファイルパス" を実行すると、指定のスクリプトが管理者権限で実行される。(実行前にUACダイアログが表示される)

なお、"powershell"の部分を、今起動しているホストの実行ファイルと同じにするには、(Get-Process -Id $pid).Path としても良い。ただ、ISEの場合だと単にスクリプトファイルが開かれた状態になるだけで実行まではしてくれない。

管理者権限がない場合、昇格してスクリプトを再実行する

管理者権限を要する処理の直前で、以下のようにStart-ScriptAsAdmin関数を実行すると、仮に管理者権限がない場合はスクリプトを再起動し、昇格して再実行する。(管理者権限がある場合は再実行せずそのまま継続する。)

. .\elevate.ps1
Start-ScriptAsAdmin -ScriptPath $PSCommandPath
if(Test-Admin)
{
    "昇格を要する処理"
}

v3未満の場合は、実行中のスクリプトパスを示すシェル変数$PSCommandPath の代わりに &{$MyInvocation.ScriptName}が使える

再起動するまでに実行した処理は再実行しないようにするには、以下のようにスクリプトファイルにパラメータを定義すれば良い。

param([switch]$SkipNormalTask)
. .\elevate.ps1

if(!$SkipNormalTask)
{
    "昇格不要な処理1"
}

Start-ScriptAsAdmin -ScriptPath $PSCommandPath -ArgumentList "-SkipNormalTask"

if(Test-Admin)
{
    "昇格を要する処理"
}

if(!$SkipNormalTask)
{
    "昇格不要な処理2"
}
UACダイアログを出さずに管理者権限でスクリプトを実行する

一般的な方法と同様、タスクスケジューラを利用する。

PowerShellからタスクスケジューラにスクリプトを登録するには、PowerShell3.0以上に同梱されているPSScheduledJobモジュールが必要。

function Register-ScriptAsAdmin
{
    param(
        [string]
        $ScriptPath,
        [object[]]
        $ArgumentList
    )

    $jobArgs=@{
        FilePath = $ScriptPath
        ScheduledJobOption = New-ScheduledJobOption -RunElevated
        Name = "RunAsAdmin $(Split-Path -Leaf $ScriptPath)"
    }
    if($null -ne $ArgumentList){$jobArgs+=@{ArgumentList = $ArgumentList}}

    Register-ScheduledJob @jobArgs 
}

function Invoke-ScriptAsAdmin
{
    param(
        [string]
        $ScriptPath
    )
    $job = Get-ScheduledJob -Name "RunAsAdmin $(Split-Path -Leaf $ScriptPath)"
    $job.RunAsTask()
}

function Unregister-ScriptAsAdmin
{
    param(
        [string]
        $ScriptPath
    )
    Unregister-ScheduledJob -Name "RunAsAdmin $(Split-Path -Leaf $ScriptPath)"   
}

まず、管理者権限で実行したいスクリプトをRegister-ScriptAsAdmin "スクリプトのフルパス"としてタスクスケジューラに登録。

この操作には当然ながら管理者権限を要するので、もし登録用スクリプトから登録をするには前述のStart-ScriptAsAdminを併用しても良い。(さすがにUACダイアログを1回も表示させない方法はなさげ…)

登録が済んだら後は、Invoke-ScriptAsAdmin "スクリプトのフルパス"とすれば、UACダイアログを表示させずに管理者権限でスクリプトを実行できる。

スクリプトの登録を解除するには、Unregister-ScriptAsAdmin "スクリプトのフルパス"を実行する。(この操作には管理者権限不要)

管理者権限がない場合はスクリプトを実行しない

逆に、管理者権限がない時は、一切処理を実施させたくない場合。

v4以上の場合は、以下をスクリプトの一行目に定義しておくだけでOK。管理者権限がない場合はエラーになってそのままスクリプトは終了する。

#Requires -RunAsAdministrator

v4未満の場合は、前掲のTest-Admin関数を以下のようにスクリプト先頭で呼び出せば良い。

if(!(Test-Admin))
{
    throw "管理者権限がありません"
}

2014/12/22

はじめに

この記事はPowerShell Advent Calendar 2014の22日目の記事です。

前回はMMLをパースし、音楽をBeepで再生するところまで作りました。

MMLはテキストデータなので、テキストエディタで入力しても良いのですが、どうせなら、PowerShellでエディタも作ってしまいましょう。

コード

いきなりですが、エディタの本体コードを。

function Enter-MusicEditor
{
    $keyMap=@{
        A="<G+4>"
        Z="<A4>"
        S="<A+4>"
        X="<B4>"
        C="C4"
        F="C+4"
        V="D4"
        G="D+4"
        B="E4"
        N="F4"
        J="F+4"
        M="G4"
        K="G+4"
        ","="A4"
        L="A+4"
        "."="B4"
        "/"=">C4<"
        ":"=">C+4<"
        "\"=">D4<"
        "]"=">D+4<"
        "R"="R4"
    }

    $mml=@()
    cls
    while($true)
    {  
        $k = [System.Console]::ReadKey($true)

        if($k.Key -eq [System.ConsoleKey]::Escape -or $k.Key -eq [System.ConsoleKey]::Enter)
        {
            cls
            break
        }
        elseif($k.Key -eq [System.ConsoleKey]::Backspace)
        {
            if($mml.length -eq 1)
            {
                $mml=@()
            }
            elseif($mml.length -ge 2)
            {
                $mml=$mml[0..($mml.length-2)]
            }
            cls
            [console]::write((-join $mml))
        }
        else
        {
            $key=$k.KeyChar.ToString().ToUpper()
        
            if($keyMap.Contains($key))
            {
                $mml+=$keyMap[$key]
                [console]::write($keyMap[$key])
                Invoke-Mml $keyMap[$key]
            }
        }
    }
    -join $mml
}
使い方

PowerShellコンソール上(ISEは不可)で、前回公開した、ConvertFrom-MML、Invoke-Beep、Invoke-Mmlの3つの関数をまず読み込み、続いて上記のEnter-MusicEditor関数を読み込みます。

この状態でコンソール上でEnter-MusicEditorを実行すると、MMLエディタモードに入ります。

MMLエディタモードでは、PCのキーボードを鍵盤代わりにして入力できます。

たとえば、Cキーを押下すると、「ド」の音がBeepで鳴り、コンソールに"C4"と出力されます。同様にFキー押下で「ド♯」が鳴り"C+4"と出力されます。どのキーがどの音に対応しているかは、上記コードの$keyMap変数に格納された連想配列を参照してください。

(なんとなくですけど、楽器の鍵盤の配置と合わせてあります)

Backspaceキーを押下すると、直前の入力を削除できます。

EnterキーかEscapeキーを押下すると、エディタモードを終了します。

出力したMMLは、コンソール上に表示が残るので、あとはコピーしてInvoke-Mml関数に渡してやると演奏することができます。

また、$mml=Enter-MusicEditorとしてやると、入力したMMLをそのまま変数に格納できます。この場合だとInvoke-Mml $mmlと実行すれば演奏できます。

解説

技術的には全然大したことをしてないですが、一応解説。

まず、[System.Console]::ReadKey()で入力したキーコードを判別します。引数に$trueを指定すると、入力したキー名をそのままコンソールに出力するのを抑制できます。

ReadKeyメソッドはConsoleKeyInfoオブジェクトを返します。アルファベットキーについてはKeyCharプロパティの値、特殊キーについてはConsoleKey列挙体を返すKeyプロパティの値で調べることができます。

あとは入力キーから対応する音名を連想配列$keyMapから取ってきて、その音名を[System.Console]::Write()でコンソール出力すると同時に、Invoke-Mml関数でその音をBeepとして鳴らしているだけです。

まとめ

入力も再生もできるようになったので、これはもう完全にシーケンサーですね! いや、やはり無理があるか…。

しかし、他のMMLコンパイラと併用して、ちょっと演奏しながらMML入力したいな、というときにもしかすると役に立つかもしれません。

ちょっと機能が少なすぎるんで、せめて↑で半音上げ↓で半音下げ、→で伸ばす←で短く、くらいはそのうち実装してみたいですね。

まあ、MML作成にはあんまり役に立たないかもしれませんが、PowerShellでコンソールの入出力を制御するのは、こういう単純なものなら意外と簡単にできるという、サンプルにはなるかと思います。

2012/03/06

次期Windows Server OSであるWindows Server “8”のベータ版が3/1よりWindows 8 Consumer Preview版と同時に提供が開始されました。今回のリリースでは日本語版も提供されています。

それと同時にPowerShell 3.0 betaを含むWindows Management Framework (WMF) 3.0 betaの提供も始まりました。こちらは英語版のみの提供で、日本語環境にインストールするには英語言語パッケージをダウンロードし適用する必要があります。

まだ評価を初めて間もない段階ですが、きづいた点をいくつか書いてみます。

PowerShell ISEがDeveloper Preview/CTP版から大きく変わっており、コマンドを入力する「コマンドペイン」と結果が表示される「出力ペイン」が統合され、「コンソールペイン」となりました。image

こんな感じでコンソールペインはその名の通りコンソール版のPowerShellと見た目や使用感が似ている上にISEならではの機能(インテリセンスなど)が使用可能になっており、ISEはスクリプト編集のみならずインタラクティブシェルとしてpowershell.exeの代わりに使用する場合でもより便利になったと言えるでしょう。なおISEのキーワード色分け設定は細かく指定できたりします。

Server8ではActive Directory管理センターの大幅な機能増強が目に留まります。Active Directory管理センターには「Windows PowerShell 履歴」ペインが追加され、GUIで操作した履歴がそのまま、再実行可能なPowerShellスクリプトとして表示されるようになりました。

たとえばユーザーを作成する作業をGUIでやります。

image

するとその作業内容がこのように履歴ペインに表示されます。

image

履歴はまさにPowerShellスクリプトそのままなので、これを「コピー」してISEに貼りつけるともうスクリプトファイルが完成します。

image

あとは必要に応じてパラメータを変更したり、ループを設けて繰り返し処理をしたり、自由自在に再利用ができます。ここではユーザー名を”testuser2”に変えて実行してみました。最初にGUIで作ったユーザーと同じ設定を用いて複数のユーザーを作成することが簡単に行えます。ユーザー名を記載したCSVファイルを読み込んでそのユーザーを一括作成することなどもできますね(PowerShellにはImport-CsvというCSVファイルを扱うコマンドレットもあります)。

スクリプトファイルにして保存すれば何回でも実行可能ですし、PSScheduledJobモジュールを使ってタスクスケジューラに登録すれば定期実行も可能です。

履歴スクリプトのうち、どこからどこまでが「今やった作業」なのか分かりにくいこともあると思いますが、そういうときには履歴ペインの「タスクの開始」をクリックし、今からやりたい作業名をまずメモします。

image

そしてGUIで作業を行い、終わったら「タスクの終了」をクリックします。

image

作業単位でこれを繰り返します。すべてが終わったら履歴をすべてコピーしてISEに貼りつけてみます。

image

するとこのようにタスク名がコメント行として挿入され、スクリプトのどこからどこまでが、どの作業に対応しているかが明確になるわけです。

このように管理GUIがPowerShellモジュール(コマンドレット)のフロントエンド的存在となり、GUIでの操作によりPowerShellコマンドレットが実行され、その履歴はスクリプトとして再利用が可能というMicrosoftが提唱する新しい管理方式が、ついにWindows Serverの本丸的存在Active Directoryに採用されたということになります。2008R2のActive Directory管理センターも内部的にはそういう構造だったのですが、履歴をスクリプトとして取り出す方法がなかったのですが、Server8からは可能になったということになります。

これまではこの構造が完全に組み込まれていたのはExchange Serverなど限られた製品のみでしたが、今後はこちらの構造が主流になると思われます。GUIとCUIの「いいとこどり」ができるこの構造はなかなか理想的なシステムなんじゃないかと思います。

PowerShellは3.0になってますますWindows OS管理の中核として重要度が高まっており、それに答えるように様々な機能増強が行われています。今回紹介したのはその一面ですが、特にServer8においてPowerShellがいかに重要かはWindows Server "8" に関するテクニカル プレビューの各項目の記述内容の多くにPowerShellに関する記載があることでも分かるかと思います。

2012/02/14

2/25(土)に開催されるわんくま同盟 大阪勉強会 #47で、「PowerShell 3.0 概要」と題してPowerShell 3.0のセッションをします。

PowerShell3.0はWindows 8とWindows Server 8に搭載され、従来のコンソールexeの大半がOS付属モジュールのコマンドレットに置きかわります。その数Win8では800以上、Server8では2000を超える量になります。これまでのexeも互換性のために残されますが、今後の主流は間違いなくPowerShellコマンドレットにシフトしていきます。

Windows Server 8では「役割」と「機能」に対応するPowerShellモジュールがそれぞれ用意され、管理用GUIがPowerShellモジュールの機能を呼び出す構造への移行がより一段と進みます。

Windows Server本体のみならず、サーバーアプリケーションでもMicrosoft製品はもちろん、サードパーティ製のものでもPowerShellモジュールが付属しているのが珍しくなくなりました。またOffice365やWindows AzureといったクラウドサービスもPowerShellで管理できるようになってきました。

PowerShellの利用場面の増加に答えるように、PowerShell3.0ではさまざまな改善と機能追加が行われています。今回のセッションではそれらをご紹介します。Windowsを管理する上でPowerShellはもはや必須といえる段階になってきたと思います。この機会にPowerShellを始めてみませんか?

今回は先日MVPを受賞されたちゅきさんのActiveDirectoryの話やmoririringさんによるC#開発中級編の話、TWorksさんによるスマフォやタブレットの開発をHTML5対応JSフレームワークで行う話など、もりだくさんです。お時間がございまいたら2/25はぜひ、わんくま勉強会へお越しください!

2011/12/19

はじめに

PowerShell Advent Calendar 2011の19日目の記事、そしてこれが私の記事では3回目となります。今回も前々回前回からの引き続きでバックグラウンドジョブについての話題です。前回までは現行バージョンであるPowerShell 2.0におけるバックグラウンドジョブの機能の使い方を解説してきましたが、今回はPowerShellの次期バージョンである3.0に追加される予定の機能のうち、ジョブ関係のものをピックアップしてみます。現在PowerShell 3.0を含むWindows Management Framework(WMF)3.0のCTP2が公開されています。またWindows 8 Developer Preview / Windows Server 8 Developer PreviewにはWMF3.0 CTP1相当のPowerShell 3.0が含まれています。

注意:本記事で取り上げた内容は製品のプレビュー版をもとに記述しています。そのためリリース版では内容が一致しない可能性があることをご承知おきください。

using:ラベル

前回、ジョブに値を渡す方法について解説しましたが、-argumentListに引数として渡すというのは正直めんどうです。呼び出し元のグローバル変数を直接ジョブ側から参照したいですよね。そこでPowerShell v3では新たに変数に付けるusing:ラベルというのが追加されました。このラベルをジョブのスクリプトブロック内で使うと、呼び出し元の変数を参照することができます。具体例。

$test="PowerShell 3.0"
Start-Job {$using:test}|Wait-Job|Receive-Job

とすると、「PowerShell 3.0」と表示され、たしかにジョブのスクリプトブロックから呼び出し元の変数を参照できていることがわかります。これは便利ですね。ただし残念ながらこの方法を使ってもスクリプトブロックをジョブに渡すことはできないようです。相変わらず文字列にキャストされてしまいました。

Receive-Jobコマンドレットの変更点

前々回に、Invoke-Command -asJobで複数リモートコンピュータに対してジョブを走らせた場合、そのジョブに対して$job|Receive-Jobがなぜか機能しない、と書きましたがこの問題が解決されています。そもそもなんでこの問題が発生していたのか、面白いのでちょっと解説します。

実はReceive-Jobコマンドレットの-locationパラメータに「パイプライン入力を許可する   true (ByPropertyName)」フラグがついていたのが原因でした。複数コンピュータに対して実行したジョブは子ジョブを複数持ちますが、親ジョブ自体は配列ではありません。そしてそのLocationプロパティには子ジョブが実行されているコンピュータ名が"remote01,remote02,remote03"のようなカンマ区切りの文字列として格納されています。よってこのジョブオブジェクトをパイプラインを通じてReceive-Jobコマンドレットに渡すと、ValueFromPipelineByPropertyName属性が付いている-locationパラメータにジョブオブジェクトのLocationプロパティの値が渡されますが、その値はカンマ区切りの文字列なので正しく解釈されず、結果として期待の動作をしなかったわけです。

v3ではReceive-Job -locationのValueFromPipelineByPropertyName属性が取り除かれ、問題なく動作するようになりました。

他の変更点としてはReceive-Jobにジョブが完了するまで待つための-waitパラメータが追加されました。が、$job|Wait-Job|Receive-Jobと違いが分からないかも…。

Get-Jobコマンドレットの変更点

Get-Jobに-filterパラメータが追加されました。連想配列でジョブにフィルタをかけられるものです。

Get-Job -filter @{State="Completed";Location="localhost"}

where-objectを使わずともフィルタできるので便利、かも。しかし個人的には-filterパラメータはいろんなコマンドレットで定義されているものの、使い方がそれぞれ異なるのがとてもとてもイヤです。まず覚えられないのでヘルプを引くところから始まっちゃいますので。パフォーマンスの関係上、Where-Objectを使うよりコマンドレット内部でフィルタしたほうが速くなるというのはわかるのですが、もう少しフィルタ方式に統一性を持たせられなかったんだろうかとか思いますね。

Get-Jobにはほかに-afterと-beforeというパラメータが追加されています。これは後述するPSScheduledJobの完了時刻をDateTimeで範囲指定し、フィルタするものです。

PowerShell Workflow

PowerShell3.0というかWMF3.0のおそらく目玉機能の一つがPowerShell Workflowです。文字通り、PowerShellでワークフローが記述できるようになります。

Workflowは関数の一種なのですが、長時間を要するタスクやリモート実行や並列実行などで使うことを主目的としているようです。functionキーワードの代わりにworkflowキーワードでワークフローを定義すると、自動的に実行対象コンピュータ名や資格情報といったパラメータが複数定義されるので、これらのパラメータを特に定義なしで利用することができます。またworkflow内ではparallelブロックを定義でき、その中に記述された各行は並列に実行されます。またfor/foreachステートメントで-parallelパラメータが利用可能になり、繰り返し処理やコレクションの列挙を並列して行うことができるようになります。

自動定義されるパラメータに-asJobがあり、これを利用するとworkflowをジョブとして実行できます。このジョブは通常のジョブとは違い、新たに追加されたSuspend-JobコマンドレットとResume-Jobを使うことによって、ジョブの一時中断と再開ができます。このジョブの中断と再開は、リモートコンピュータ上でワークフローを走らせてるときでも可能ですし、中断後リモートセッションが切断されたあとに再開することもできますし、リモートコンピュータがシャットダウンしても再起動後にジョブを再開することまでできてしまいます。これらはWMFにおけるリモート基盤を支えているWinRMの最新バージョン、WinRM3.0が実現している機能です。このようにセッションを再接続してもタスクを継続できるような接続をrobust(堅牢な), resilient(弾力性のある、障害から容易に回復する) connectionと称しているようです。

PowerShell WorkflowはWindows Workflow Foundation(WF)と密接な関係があり、WFのデザイナで作ったxamlをPS Workflowに変換したり(逆もできる?)、Invoke-Expressionでxamlを実行したりできるらしいです。WF側でもPowerShellの多くの機能がアクティビティとして使用できたりして、WFとPowerShellがWMFというシステム管理フレームワークの主要なパーツとして密に連携していくようです。このあたりの話はWFの専門家であるAhfさんがPSアドベントカレンダーの23日目にしてくださる予定なので、楽しみですね!

なおPS Workflowは従来のPSスクリプトとは異なった利用状況を想定しているため、あるいはWFの機能と合わせるため、PSスクリプトではできるのにPS Workflowではできないことがとてもたくさんあります。forの中でbreakやcontinueステートメントが使えないとかStart-Sleepは-Secondパラメータしか指定できない(ミリ秒単位でスリープかけられない)とか色々あります。そのうちPS WorkflowとPSスクリプトの違いというドキュメントが公開されるんじゃないかと思います。

ちなみにWinRM3.0のおかげでワークフローではない通常のリモートジョブでも、New-PSSessionで作成したセッションの中でジョブを実行した場合、そのジョブが動作しているコンピュータへのセッションを切断(Disconnect-PSSession)したあと、セッションに再接続(Connect-PSSessionやReceive-PSSession)すればジョブの結果を取得したりすることができます。またセッションを作製したインスタンス(powershell.exe)でそのセッションを切断すると、それ以降は別のインスタンスやコンピュータからそのセッションにConnect-PSSessionで接続することができます。

ScheduledTasksモジュール

PowerShell3.0が含まれる次期Windowsでは大量のモジュールが追加され、それらのモジュールに含まれるコマンドレットの総数はWindows 8でも2000を超える膨大な量になります。これはWindows 8やWindows Server 8では従来のコマンドプロンプトから実行するコンソールexeコマンドのほとんどすべてをPowerShellコマンドレットに置き換える措置のためです。もちろん従来のコマンドは互換性のために残されますが、netsh.exeなど一部のコマンドではPowerShellへの移行を促すメッセージが表示されたりするようになるようです。参考:Window 8の機能の概要 − @IT

ScheduledTasksモジュールというタスクスケジューラを扱うモジュールもWindows 8 / Windows Server 8に新しく追加されるモジュールの一つで、schtasks.exeを置き換えるものとなります。これまでPowerShellでタスクスケジューラを扱うにはschtasks.exeを使うか、WMIのWin32_ScheduledJobを使う必要があり面倒でしたが、このモジュールに含まれるコマンドレットを用いるとそれが容易に行えるようになります。たとえば「notepad.exeを毎日朝10:00に起動する。バッテリ駆動のときでも実行」というタスクを「test」という名前で登録するには、

$action = New-ScheduledTaskAction -Execute "notepad.exe"
$trigger = New-ScheduledTaskTrigger -At "10AM" -Daily
$setting = New-ScheduledTaskSettings -AllowStartIfOnBatteries 
New-ScheduledTask -action $action -trigger $trigger -setting $setting|Register-ScheduledTask -TaskName test

とすれば可能であるはずです。実はServer 8 Developer Preview版ではこのコードは機能しません。タスクのトリガを作成するNew-ScheduledTaskTriggerコマンドレットが正しいオブジェクトを作ってくれないのです。これは将来のバージョンできっと修正されるかと思います。ただトリガを定義する部分をはずせば(あんまり意味はないですが)このコードは動作するので、やり方はたぶんあってると思います。

Register-ScheduledTaskコマンドレットには-asJobパラメータがあり、タスクスケジューラへの登録をジョブとしてバックグラウンドで行うことができます。ScheduledTasksモジュールはWMIを利用してタスクスケジューラを操作するので、ほかのWMI関係のコマンドレットと同様ですね。

なおScheduledTasksモジュールはデフォルトでは読み込まれていないので、使用するには本来Import-Moduleコマンドレットを使用しなければならないところですが、PowerShell3.0のCmdlet Discoveryという機能によりImport-Moduleは実行しなくてもScheduledTasksモジュールに含まれるコマンドレットを利用することができます。Cmdlet Discoveryとは現在読み込まれていて実行可能なコマンドレットの中にない、未知のコマンドレットを実行しようとしたとき、Modulesフォルダに存在するモジュールから同名のコマンドレットが定義されているものを探し出し、発見できたらそのモジュールを読み込んだうえでコマンドレットを実行するという優れた機能です。初回だけモジュールの検索とロードの手順が実行されるので待たされますが、一度Cmdlet Discoveryによってモジュールがシェルに読み込まれればあとは快適にコマンドレットを実行できるようになります。

PSScheduledJobモジュール

ScheduledTasksモジュールは-asJobパラメータが定義されているくらいで実はそれほどPowerShellのジョブとは関係ないのですが、ScheduledTasksモジュールが内包しているPSScheduledJobモジュールはPowerShellのジョブ機能と大いに関係があります。

従来PowerShellスクリプトをタスクスケジューラに登録するにはコマンドラインに"powershell.exe"を、引数に"-file hoge.ps1"を指定して、みたいなまわりくどいことをする必要がありました。しかし新しく追加されるPSScheduledJobモジュールに含まれるコマンドレット群はこの問題を解消します。PowerShellスクリプト(.ps1)あるいはスクリプトブロックをPSScheduledJobとして直接タスクスケジューラに登録できるようになり、PowerShellとタスクスケジューラのシームレスな連携を実現します。こちらはWindows 8/Server 8に付属のモジュールではなく、PowerShell 3.0に付属のモジュールなので、Win7などでも使用可能になる予定です。

使用例を見ていきましょう。

$triggers = @()
$triggers += New-JobTrigger -at "2012/01/01 11:11:10" -Once
$triggers += New-JobTrigger -at "10:00" -Daily

$sb = {
    "This is Scheduled Job."
    Get-Date
}

Register-ScheduledJob -ScriptBlock $sb -Trigger $triggers -Name ScheduledJobTest1

まずNew-JobTriggerコマンドレットによってトリガー(具体的には実行時刻など)を定義します。ここでは決められた時刻に1回実行するものと、毎日同じ時刻に実行するものの2つを定義してみました。そしてこれらの時刻に実行したい内容をスクリプトブロックに記述し、これらをRegister-ScheduledJobコマンドレットで登録してやります。

するとこのスクリプトブロックはタスクスケジューラに登録され、指定時刻になると指定したスクリプトブロックの内容が実行されます。このタスクは「タスクスケジューラ― ライブラリ\Microsoft\Windows\PowerShell\ScheduledJobs」に登録されています。

このタスクのアクションは具体的には次のようになっています。

powershell.exe -NoLogo -NonInteractive -WindowStyle Hidden -Command "Import-Module PSScheduledJob; Start-Job -DefinitionName 'ScheduledJobTest2' -DefinitionPath 'C:\Users\Administrator\AppData\Local\WindowsPowerShell\ScheduledJobs' -WriteToStore | Wait-Job"

これによると、指定時刻に実際にタスクスケジューラによって実行されるのはpowershell.exeであり、Start-Jobコマンドレットを使って登録したスケジュールをPowerShellのジョブとして実行していることがわかります。Start-Jobコマンドレットの-DefinitionNameパラメータなどはPSScheduledJobのために追加されたもので、これによりRegister-ScheduledJobが出力したPSScheduledJob定義をファイルから読み込んでジョブとして実行できるようになっています。PSScheduledJob定義とジョブの出力は-DefinitionPathで指定されているフォルダの下にxmlファイルとして保存されているので興味がある方は覗いてみるといいかもしれません。

さて、スケジュールしたジョブの実行結果はどうやって受け取ればいいのでしょうか。実はこれはすごく簡単で、PSScheduledJob(ここではScheduledJobTest1という名前で定義しました)がタスクスケジューラによって一度以上実行された後は、

$job=Get-Job -name ScheduledJobTest1

とすることでJobオブジェクトとして取得することができるようになります。あとは通常のジョブと同じ取り扱いができるので、

$job|Receive-Job

などで実行結果を取得できます。

ちなみにPSScheduledJobはそれを定義したインスタンス以外でも参照することができます。具体的にはpowershell.exeでジョブをスケジューリングして終了→また別のpowershell.exeを立ち上げてimport-module PSScheduledJobしたあとGet-Job|Receive-JobしてPSScheduledJobの結果を参照、みたいなことができます。

ここで紹介した一連の操作ではスクリプトブロックをPSScheduledJobにしましたが、Register-ScheduledJobコマンドレットの-FilePathパラメータを用いれば.ps1ファイルをPSScheduledJobとして登録することも可能です。

現行バージョンのPowerShellはとにかく起動が遅いため、タスクスケジューラにスクリプトを登録しても実行が始まるまで何十秒も待たされるなどはざらでしたが、PSv3は起動がずいぶん速くなり、スペックや状況にもよるとは思いますがpowershell.exeの起動後ほんの数秒でスクリプトが走り始めます。この速度のおかげもあってPSScheduledJobはきっととても有効に機能するんじゃないかと思います。

おわりに

今回はPowerShell 3.0で増強されるバックグラウンドジョブ関係の機能をまとめてみました。これらの新機能のおかげで、時間のかかる処理や定期実行する処理を扱うのが飛躍的にやりやすくなりそうです。PowerShell 3.0で追加される機能は他にもたくさんあって、このブログでもいつか全部紹介したいと思ってるのですが、今回取り上げたジョブ関係はその中でもかなり重要な機能増加を多く含んでいると言えるでしょう。PowerShell 3.0やWindows 8/Server 8のリリースに備えてジョブ関係から予習しておくのは悪くないと思いますよ。

なんか25日のアドベントカレンダーのうち3回もバックグラウンドジョブネタをやって、PSアドベントカレンダーというより私だけ一人でPSジョブアドベントカレンダーをやってる感じでちょっと申し訳ないんですが、どうか許してください。そして前回は今回で終了するって言ってたんですが、実はまだジョブ関係の小ネタが残ってるので最終日25日にさせてください。では今日のところはこのへんで。明日はwaritohutsuさんの登場です。よろしくお願いします。


古い記事のページへ |


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

Twitter

Books