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/12/20

この記事はPowerShell Advent Calendar 2016の20日目です。

はじめに

前々回はASTの概要について述べ、最後にAST.FindAllメソッドを使って、ASTから指定のASTノードを検索する方法について説明しました。

前回はASTを再帰的に検索して、木構造を視覚化してみました。

今回もASTを検索する話なのですが、静的解析機能を実装するためのAST Visitorを用いる方法について説明します。が、あらかじめお断りしておきますが、静的解析の実装までは今回はたどり着きません。静的解析ツールをどう作るかorどう作られているか、ということを雰囲気で味わっていただければと。

Visitorパターン

AST Visitorの説明をする前に、まず、Visitorパターンについて簡単に。

Visitorパターン[Wikipedia]というのは、オブジェクト指向言語におけるデザインパターンの1つで、対象オブジェクトを巡回する「訪問者」クラスを定義するものです。Visitorクラスでは、対象クラスごとに行う処理を、個別にvisitメソッドをオーバーロードさせることで定義します。共通のVisitor抽象クラスを継承することで、異なる機能を持ったVisitorクラスを作ることができます。

一方、処理対象クラスには、Visitorオブジェクトを引数に受け取る、acceptメソッドを定義します。acceptメソッドでは、引数として受け取ったVisitorオブジェクトのvisitメソッドを呼ぶことで、処理を実行させます。

なお、処理対象クラスが子要素クラスを持つ場合には、acceptメソッド内で、子要素クラスのacceptメソッドを呼ぶようにします。こうしておくことで、Visitorは処理対象を再帰的に巡回できるようになります。

このように処理対象クラスから、実際に処理を行う機能をVisitorクラスとして分離することで、処理対象クラスに手を加えることなく、Visitorクラスを追加して、処理内容を増やしたりすることが可能になります。

AST Visitorの呼び出し

Visitorパターンを念頭において、AST Visitorの呼び出し方を見ていきましょう。Ast抽象クラスには、以下の2つのVisitメソッドが定義されています。

説明に入る前に注意点。メソッド名は"Visit"となっていますが、Visitorパターンでいうところの"accept"メソッドのことです。なぜメソッド名がAcceptじゃないのかは不明ですが…。

ともかく、AstクラスのVisitメソッドは、AstVisitor抽象クラスを継承したクラスのオブジェクトか、ICustomAstVisitorインターフェースを実装したクラスのいずれかを引数に取ることで、ASTに対する処理を実施します。

AstVisitor抽象クラスを継承、もしくはICustomAstVisitorインターフェースを実装することで、ASTの種類に応じた巡回処理を行うクラスを、自分で定義していきます。

AstVisitor抽象クラス

AstVisitor抽象クラスは、Visitorとしての基本的な機能があらかじめ実装されています。具体的には既に以下の機能は用意されています。

  • ASTの種類に応じたVisitメソッドの定義
    すべての種類のASTに対応するVisitメソッド(50個以上)がVirtualメソッドとして定義されています(※)。例えば、IfStatementAstに対する処理を行うための、VisitIfStatementメソッドがあります。

    ※一般的なVisitorパターンでは、Visitメソッドを対象クラス分オーバーロードさせますが、PowerShellのAstVisitorは対象クラスに応じた別名のメソッドを定義する方式です。これも理由は分かりませんが、オーバーロードにするには多すぎるからかもしれません。

  • 子ノードの再帰的な巡回
    各Visitメソッドには、ASTの子ノードに対し、再帰的にVisitメソッドを呼ぶ仕掛けがあらかじめ備わっています。
  • ノード巡回の停止
    各VisitメソッドはAstVisitAction列挙型を返却します。以下のように返却する値によって、ノード巡回の継続、停止を制御できます。
    • Continue:ノード巡回を継続(デフォルト)
    • SkipChildren:子ノードの巡回を行わない
    • StopVisit:巡回を終了する
カスタムAstVisitorクラスを作成する

以上の基本的な機能を踏まえて、AstVisitor抽象クラスを実装したカスタムVisitorクラスを作ります。C#で書くのが一般的ですが、せっかくなのでPowerShell v5で追加された、クラス構文を使って書いてみましょう。

例えば、「利用しているコマンドのリストを取得する。ただし、コマンドのパラメータ内で別コマンドを呼び出している場合は除く。」というお題を解くことを考えます。

ASTのFindAllメソッドだと、配下に含まれるすべてのCommandAstを取得してしまうので、単純にはいきません。そこでカスタムAstVisitorクラスの出番です。

このお題を実現するVisitorクラスは以下のようになるでしょう。

using namespace System.Management.Automation.Language

class GetCommandNamesVisitor : AstVisitor
{
    [string[]]$CommandNames = @()

    [AstVisitAction]VisitCommand([CommandAst]$commandAst)
    {
        $this.CommandNames += $commandAst.CommandElements[0].Extent.Text
        return [AstVisitAction]::SkipChildren
    }
}

PowerShellのクラス構文において、Virtualメソッドのオーバーライドは、単に同名のメソッドを定義するだけですので、ここではVisitCommandメソッドをオーバーライドします。

プロパティとフィールドの区別はないので、コマンド名の一覧を格納するCommandNamesプロパティは上記のような定義になります。メソッド内でクラスメンバを参照する際には$thisを用います。

作成したGetCommandNamesVisitorクラスをインスタンス化し、解析対象スクリプトブロックのASTのVisitメソッドに引数として渡します。

$scriptBlock = {
    $files = Get-ChildItem -Path (Get-Location | Split-Path -Parent) -File
    $files | 
        Sort-Object -Property LastWriteTime -Descending | 
        Select-Object -First 5
}

$visitor = New-Object GetCommandNamesVisitor
$scriptBlock.Ast.Visit($visitor)
$visitor.CommandNames

実行すると、結果は

Get-ChildItem
Sort-Object
Select-Object

のようになるかと思います。

AstVisitorクラスの具体的な実装については、PSReadLinePowerShellEditorServicesにありますので、参考にしてみてください。

ICustomAstVisitorの実装

前項で述べた、AstVisitor抽象クラスを継承したカスタムAstVisitorクラスの場合、基本的な処理を実装する必要はないですし、目的とするASTクラスに対するVisitメソッドだけオーバーライドすればいいので、非常に簡便です。

ただ、本格的にPowerShellの構文解析を行いたい場合、ノードの巡回順だとか、その他もろもろをもっと細かく自分で実装したいケースが出てきます。

そういった場合にはICustomAstVisitorインターフェースを実装したクラスを作って対応します。ICustomAstVisitorインターフェースも、AstVisitor抽象クラス同様、各ASTクラスに応じたVisitメソッドが定義されているのですが、各VisitメソッドはAstVisitAction列挙体ではなく、object型のオブジェクトを返します。つまり、自分で好きなオブジェクトを返すように定義できるわけです。

Ast.Visit(ICustomAstVisitor)はAstVisitor抽象クラスを引数に取る場合と異なり、objectを返却するのですが、このとき返却されるのは、最初に実行されたVisitメソッドの戻り値になります。

ICustomAstVisitorはインターフェースですので、処理はすべて自分で定義しなくてはなりません(※)。ノードの再帰的探索も、必要ならもちろん自前で実装する必要があります(前回紹介した、JSON化スクリプトのような処理になるかと思います)。

※ISEだとインターフェースの実装を一発で行うリファクタリング機能はないので、今回みたく実装すべきメンバがたくさんある場合は、こんな感じのひな形を作るスクリプトを使うと良いでしょう。

今回はICustomAstVisitorインターフェースを実装したクラスの実例まではご紹介できませんでしたが、興味のある方は、PSScriptAnalyzerで用いられているので参考にしてみてください。

まとめ

PowerShellのASTについてきちんと解説している記事が英語圏を含めてもあまりないようでしたので、3回に渡って、一通りの基礎知識をまとめてみました。

普通にPowerShellを使っている分には、滅多に使うことはないと思いますが、たとえばPSScriptAnalyzerカスタムルールを自分で作る場合には、ASTの知識は必須になってきますので、必要に応じて参考にしていただければ幸いです。

2016/09/18

デモ用スクリプトダウンロード

8/27、コンピュータの舞台裏 (第7回) で行った、「PowerShellで始めるWindows 自動化入門」というセッションの資料を公開します。とても遅くなってすみません。

コンピュータの舞台裏は、コンピュータの仕組み、基礎の部分から扱う勉強会ですので、私の方からは、自動化ってそもそも何がうれしいの?というところから話をさせていただきました。

コンピュータの自動化手法として、プログラム言語やスクリプト言語を利用する方法がまず挙げられると思いますが、昨今は無料で使える言語がたくさんあります。その中でも、「システム管理用」を銘打つPowerShellは、システムの構成要素として重要な「ファイル」を扱うことにかけては、強力かつ簡単だと思います。というわけで、目の前のコンピュータの自動化の第一歩として、PowerShellを触ってみるのは悪くないんじゃないかと思っています。

2015/02/01

昨日のMVP Community Campにはたくさんの方にお越しいただき、まことにありがとうございました。

私の行った「PowerShellスクリプトを書いてラクしよう」のセッション資料を公開します。

セッション中、デモで用いたサンプルスクリプトも公開します。

今回は私の前にJPPOSH大阪代表のwakaさんのPowerShellセッションがあり、そちらでサーバー管理の本筋の話をしていただいたので、私の方はクライアント側の話でまとめてみました。

どちらかというと遊びに近いネタも多かったと思いますが、PowerShellの基本的なスクリプトを書くための文法等の知識を身に着ける題材として、参考にしていただければ幸いです。

2014/12/11

MMLとは

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

突然ですが、MMLってご存知ですか? MMLとはMusic Macro Languageの略で、その名の通り音楽演奏データを記述するための言語です。BASICのPLAY文で使うあれです。MIDIファイルに変換するコンパイラもありましたよね。

たとえば、「ねこふんじゃった」の最初の部分をMMLで書くとこんな感じです。MMLはいろんな記法があるのですが、よくあるのはこういうのです。

e-d-<g-4>g-rg-4e-d-<g-4>g-rg-4e-d-<g-4>g-4<e-4>g-4<d-4>frf4
e-d-<d-4>frf4e-d-<d-4>frf4e-d-<d-4>f4<e-4>f4<g-4>g-rg-4

ドの音をc、レの音をd、のように以下シの音をbまでアルファベット音階で割り当てます。半音上げる(シャープ)場合は音名のあとに+、半音下げる(フラット)場合は-を付与します。音の長さは4分音符なら4、8分音符なら8と書きます。付点は数字のあとに.を付けます。数字省略時は8分音符と解釈するのが普通のようです。オクターブを上げる場合は>、下げる場合は<と書きます(コンパイラによっては逆の解釈をする)。休符はrです。

本物のMMLでは、音色の指定だとかトラックの割り当てだとか、もっとたくさん命令があるんですが、基本はだいたいこんなもんです。

PowerShellで音を鳴らすには

PowerShellで音を鳴らすには、.NETのSystem.Console.Beepメソッドを使うのが一番手っ取り早いです。ビープ音ですね。比較的古いWindowsではハードウェアのビープスピーカーから、比較的新しいWindowsではサウンドデバイス上で鳴ります。今回のMMLシーケンサーも結局はBeepメソッドです。

そしてぎたぱそ先生がずっと以前に書いておられます。なので今回のはまあ、MMLシーケンサー部分を除けば二番煎じなんですけどもね。(そしてSmallBasicLibraryを使えば実はMML演奏もできるので、車輪の再発明でもあるんですが)

ちなみにwavやmp3なんかを鳴らすには、Windows Media PlayerをCOM経由で実行する方法や.NET(WPF)のSystem.Windows.Media.MediaPlayerクラスを使う方法などがあります。

音程の決め方

Beepメソッドでは第1引数にビープ音の周波数をHzで、第2引数に再生時間をミリ秒で指定します。つまりは音程(音の高さ)はHzで指定する必要があります。ここでドの音は何ヘルツで、みたいな対応表をどこかから探してきてそのデータを使ってもいいんですが、今回はせっかくなので、PowerShellで計算して求めてみます。

まず、ある音程の一つ上のオクターブの音程の間の周波数比は、1:2です。

ピアノ等の鍵盤楽器が近くにあれば、それを見てもらえば分かると思いますが、1オクターブには12の音程が含まれています。1オクターブの周波数比を12個均等に分割します(この周波数の決め方を平均律といいます)。つまり、隣り合う音程の周波数比は、1:12√2となるわけです。

さて、音程の周波数比はこれでわかりました。あとは基準となる音の周波数が分かれば、全ての音の周波数が計算できるわけです。時代や地域、演奏する楽器等によって微妙に違ってきますが、現代日本においては普通は、A(ラ)=440Hzが基準周波数です。

あとは単純にかけ算して周波数を求めて連想配列に入れておくだけです。コードにすればこんな感じですね。

$baseFrequency = 440.0
$pitches = @{
            "c" = $baseFrequency * [math]::Pow(2, 3/12)
            "d" = $baseFrequency * [math]::Pow(2, 5/12)
...
}

あと、+で半音上がる場合は[math]::Pow(2, 1/12)をかけ、-で半音下がる場合は割ればいいだけです。(別に$pitchesテーブルで最初から定義してもいいかもですが)

音価の決め方

音価というのは音の長さです。Beepメソッドの第2引数にはミリ秒で実際の時間を指定する必要があるので、これも計算で求めてやります。

4分音符というのは、1小節(全音符)を4分割、つまり1/4した音価を持ちます。同様に8分音符は1/8ですね。付点がつくと音価は1.5倍になります。

ではたとえば4分音符の具体的な音価はどうやって定めるのでしょうか。それを決めるのがテンポと拍子です。

テンポは1分間(つまり60000ミリ秒)の拍数(BPM、Bit Per Minuteともいう)で定義されます。今回作るシーケンサーは4/4拍子、すなわち1拍=4分音符とする4拍子固定とするので、1分間に4分音符がBPM回含まれることになります。

つまり、4分音符の音価=60000ミリ秒/BPMの値、全音符の音価=60000*4ミリ秒/BPMの値 となるわけです。

具体的なコードは…略します。単なるかけ算と割り算だけなんで。

MMLのパース

えっと時間がなくなってきたので、駆け足で説明します。

MMLのパースとは、要するに最初の例のようなMML(テキストデータ)から、実際に演奏する音程と音価の組み合わせのデータとして組み立てる作業になります。

今回のシーケンサーでは、繰り返し記号等はサポートしないので、単にMMLを一文字ずつ読み込んで、音名(cなど)を見つけたら、その後の文字を読んで、+があれば半音上げるなどして周波数を求め、その後に数字があればn分音符とみなし、先ほどの計算式から実際の音価を求めてやる。というのを全部の音でくり返すだけです。

この処理をやっているのがConvertFrom-MMLという関数です。MMLテキストを入力してやると、周波数(Frequency)や発音時間(Duration)などをプロパティとして含んだpscustomobject(Noteオブジェクト)を出力します。(Noteとは音符のことです)

MMLの演奏

パースしたMMLを実際に演奏するのがInvoke-Beep関数です。入力は、ConvertFrom-MMLで生成したNoteオブジェクトです。実際の処理は至って単純で、 [System.Console]::Beep($note.Frequency, $note.Duration)するだけです(えー)。あ、あと休符の時はDuration分だけSleepを入れてやっています。

MMLの>と<の意味を逆転する-Reverseスイッチ、BPMを指定する-Bpmパラメータ(デフォルト120)なんかも用意してます。

実際にはMMLから直接演奏できるように、Invoke-Mmlというラッパー関数を用意しています。

冒頭のねこふんじゃったのMMLを再生するにはこんな感じにします。

$s=@"
e-d-<g-4>g-rg-4e-d-<g-4>g-rg-4e-d-<g-4>g-4<e-4>g-4<d-4>frf4
e-d-<d-4>frf4e-d-<d-4>frf4e-d-<d-4>f4<e-4>f4<g-4>g-rg-4
"@
Invoke-Mml $s
コード
function ConvertFrom-MML
{
    [CmdletBinding()]
    param(
        [parameter(ValueFromPipeline=$true)][string[]]$mml,
        [int]$bpm = 120,
        [switch]$reverse = $false
    )
    begin
    {
        $baseFrequency = 440.0
        $scaleRatio = [math]::Pow(2, 1/12)
        $baseDuration = 60000 * 4 / $bpm
        $pitches = @{
            "c" = $baseFrequency * [math]::Pow(2, 3/12)
            "d" = $baseFrequency * [math]::Pow(2, 5/12)
            "e" = $baseFrequency * [math]::Pow(2, 7/12)
            "f" = $baseFrequency * [math]::Pow(2, 8/12)
            "g" = $baseFrequency * [math]::Pow(2, 10/12)
            "a" = $baseFrequency * [math]::Pow(2, 12/12)
            "b" = $baseFrequency * [math]::Pow(2, 14/12)
            "r" = -1
        }
        $currentOctave = 0
    }

    process
    {
        $chars = [string[]]$mml.ToCharArray()

        0..($chars.Length - 1)|%{
            if($null -ne $pitches[$chars[$_]])
            {
                $frequency = $pitches[$chars[$_]]
                $suffix = ""
            
                $denominator = ""
                $dot = $false

                if($_ -lt $chars.Length-1)
                {
                    switch($chars[($_ + 1)..($chars.Length - 1)])
                    {
                        {$pitches.Contains($_)}
                            {break}
                        "+"
                            {$frequency *= $scaleRatio; $suffix += $_}
                        "-"
                            {$frequency /= $scaleRatio; $suffix += $_}
                        {$_ -match "\d"}
                            {$denominator += $_}
                        "."
                            {$dot = $true}
                    }
                }

                $frequency *= [math]::Pow(2, $currentOctave)

                $denominator = [int]$denominator
                if($denominator -eq 0){$denominator = 8}
                $multiplier = 1 / $denominator
                
                if($dot){$multiplier *= 1.5}
                $duration = $baseDuration * $multiplier

                if($frequency -le 0){$on = $false}else{$on = $true}
                [pscustomobject]@{
                    Frequency = [int]$frequency
                    Duration = $duration
                    Note = "$($chars[$_])$suffix$denominator$(if($dot){"."})"
                    Octave = $currentOctave
                    Pitch = $chars[$_]
                    Suffix = $suffix
                    Denominator = "$denominator$(if($dot){"."})"
                    On = $on
                }
            }
            elseif($chars[$_] -eq ">")
            {
                if($reverse)
                {
                    $currentOctave--
                }
                else
                {
                    $currentOctave++
                }
            }
            elseif($chars[$_] -eq "<")
            {
                if($reverse)
                {
                    $currentOctave++
                }
                else
                {
                    $currentOctave--
                }
            }
        }
    }
}

function Invoke-Beep
{
    [CmdletBinding()]
    param([parameter(ValueFromPipeline = $true)][psobject[]]$note)
    
    process
    {
        if($note.On)
        {
            [System.Console]::Beep($note.Frequency, $note.Duration)
        }
        else
        {
            Start-Sleep -Milliseconds $note.Duration
        }
    }
}

function Invoke-Mml
{
    [CmdletBinding()]
    param(
        [string]$mml,
        [int]$bpm,
        [switch]$reverse
    )
    
    ConvertFrom-Mml @PSBoundParameters|Invoke-Beep
}
おわりに

今回はMMLをパースしてBeepを演奏するスクリプトをPowerShellで作ってみました。これをシーケンサーというのはおこがましいと思いますが、まあたまにはこういう柔らかいネタもいいんじゃないでしょうか。ドレミの音がどうやって決められてるのかというのも、もしかして話のネタくらいにはなるかも。

そしてこれ、シーケンサーといいつつ、入力機能がないですよね。それについては次回やります。

PowerShellアドベントカレンダー2014はまだまだ残席ございます。ぜひ、あなたのPowerShell話を聞かせて下さい。

2014/09/09

8/23わんくま横浜勉強会で、PowerShellコマンドの書き方というセッションをしたのですが、その際、株式会社Codeerさんが公開されているFriendlyというライブラリを使ったコマンドレットを動作させるデモを行いました。

準備の時間がなくて、突貫工事で作ったサンプルで恐縮ですが、公開することにします。(一応動かしてみたら動いた、レベルのものなのであしからず…)

コード
using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Management.Automation;
using Codeer.Friendly.Windows;
using Codeer.Friendly.Dynamic;
using System.Windows.Forms;

namespace Winscript
{
    [Cmdlet(VerbsCommon.Get, "FormControlText")]
    public class GetFormControlTextCommand : Cmdlet
    {
        [Parameter(Mandatory = false, ValueFromPipeline = false, Position = 1)]
        public string[] ControlName { get; set; }

        [Parameter(Mandatory = true, ValueFromPipeline = true, Position = 0)]
        public Process Process { get; set; }

        protected override void ProcessRecord()
        {
            var _app = new WindowsAppFriend(this.Process);
            dynamic form = _app.Type<Control>().FromHandle(this.Process.MainWindowHandle);
            foreach (var c in form.Controls)
            {
                if (ControlName == null || ControlName.Contains((string)c.Name))
                {
                    WriteObject((string)c.Text);
                }
            }
        }
    }

    [Cmdlet(VerbsCommon.Set, "FormControlText")]
    public class SetFormControlTextCommand : Cmdlet
    {
        [Parameter(Mandatory = true, ValueFromPipeline = false, Position = 1)]
        public string ControlName { get; set; }

        [Parameter(Mandatory = true, ValueFromPipeline = false, Position = 2)]
        public string Text { get; set; }

        [Parameter(Mandatory = true, ValueFromPipeline = true, Position = 0)]
        public Process Process { get; set; }

        protected override void ProcessRecord()
        {
            var _app = new WindowsAppFriend(this.Process);
            dynamic form = _app.Type<Control>().FromHandle(this.Process.MainWindowHandle);
            foreach (var c in form.Controls)
            {
                if (ControlName == (string)c.Name)
                {
                    c.Text = Text;
                }
            }
        }
    }
}
ビルド方法
  1. Windows 8.1 SDK をインストールする。
  2. Visual Studio 2010以降でC#のクラスライブラリを新規作成する。
  3. C:\Program Files (x86)\Reference Assemblies\Microsoft\WindowsPowerShell\3.0 にある.dllを参照設定する。
  4. 対象フレームワークを.NET 4.5、対象プラットフォームをx64にする。
  5. 上記のコードを貼り付ける。
  6. NuGetでFriendlyおよびFriendly.Windowsを追加する。
  7. ビルドする。
使用方法

上記をビルドして生成したDLLにはGet-FormControlTextと、Set-FormControlTextの2つのコマンドレットが含まれます。

# コマンドレットのインポート
Import-Module "dllのフルパス"

# 操作対象のプロセスオブジェクト取得
$p = Get-Process WindowsFormsApplication1 

# プロセスを指定して、すべてのコントロールのテキストを取得
Get-FormControlText -Process $p

# プロセスとコントロール名を指定して、テキストを取得
Get-FormControlText -Process $p -ControlName textBox1

# 位置パラメータなので以下のようにも書ける
Get-FormControlText $p textBox1

# パイプラインからプロセスオブジェクトを入力することもできる
$p | Get-FormControlText -ControlName textBox1 

# プロセスとコントロール名を指定して、テキストを変更する
Set-FormControlText -Process $p -ControlName textBox1 -Text Wankuma
Set-FormControlText $p textBox1 Yokohama
$p | Set-FormControlText -ControlName textBox1 -Text 6
制限事項

フォーム直下に配置されたコントロールしか取得できない(と思います)。

Windows Formアプリケーションにしか対応していない(と思います)。

x64アプリしか操作できない(と思います。x86用はアセンブリを分ける必要がある??)。(←9/9追記)

コントロール名指定はCase sensitiveでワイルドカード不可です。(この辺ただの手抜きですが)

今後の方針?

上のコードをみてもらえればわかると思いますが、ちょっと触ってみたら一応動くものができるくらい、Friendlyは分かりやすいです。皆さんもぜひ使ってみてください。

本来は、テキストじゃなくてコントロールそのものをGetしたりSetしたりInvoke(クリックとか)したりできるようにしたかったんですが、Controlはシリアル化できないオブジェクトなので、Friendlyで実物を持ってきてコマンドレットに出力する、というのは無理でした。何らかのプロキシオブジェクトみたいなのでラップしたりすれば良いかと思います。

それにしてもPowerShellとFriendlyの組み合わせはものすごい可能性を秘めている予感がします。

システム管理方面では…

セッションでもやりましたが、PowerShellコマンドレットベースのアプリケーションを作ると、GUIとCUIのいいところどりが出来る、とはいうものの、コマンドインターフェースがなくGUIオンリーのアプリケーションはまだまだたくさんあるのが現実かと思います。そういうアプリケーションも、PowerShellデフォルトの機能のみではつらいですが、上記のようにFriendlyを併用すれば、GUI操作の自動化が容易になると思います。

開発方面では…

C#でFriendlyを使ったGUIのテストコードを書く以外に、上記のようなFriendlyの機能をラップしたコマンドレットを用意することで、PowerShellスクリプトでもテストコードを書くことができるようになると思います。その際、Pester等のPowerShell用テストフレームワークを併用すると、より一貫したテストコードが記述できるようになるんじゃないかと思います。

いずれは(いつ?)、Friendlyの機能をラップしたコマンドレット群をきちんと設計、実装して、モジュールとして公開したいですね!

2013/07/02

というわけで、10年目に突入しました。これも私のアウトプットをインプットしてくださる皆様、私のインプットとなるアウトプットをしていただいている皆様のおかげです。いつもありがとうございます。

10周年なので軽く歴史を振り返ります。

2004年にWSH Lab.というサイトを中心としたWSH / VBScript関係の活動が評価されてfor Developer - Scriptingという分野でMSMVPを受賞したのがはじまりです。

実はWSH Lab.を始めたのが1999年のことで、それ以前も1997年頃からWindows系のニュースグループに出入りして、オンラインコミュニティではMVP受賞前も知る人ぞ知る存在ではあったように思います。(なんせ、WSHは相当ニッチな分野だったもので、まともに取り扱おうと思う人は当時はほとんどいなかったもので)

当時は私はまだ大学生(しかも分野は化学系でIT関係なし)でして、体調崩したり色々あって卒業後も就職せず迷走してた時期でした。なのでIT系の方々とも交流はなく、オンラインのみで何故かWSHというこれまたあんまり誰も手を付けないマイナー分野で突っ走ってる謎の存在だったんじゃないかなーと思います。

MVP受賞後はひょこひょこオフラインにも出没するようになり、謎めいた存在から確かに実在する存在として認識されていったのではないかと思います。2006年にはわんくま同盟に加入し、IT系の方々と交流し、また自らセッションをする機会にも恵まれました。

MSMVP受賞のおかげで2006年からは@ITさんの方でチェック式 WSH入門という連載をさせていただくことができ、商業デビュー(?)を果たしました。

だんだんWSHがフェードアウトしていくと同時にPowerShellというものが登場したのも2006年のことでした。当時は.NETのことは全然知らなかったのですが、このままでは取り残される!と思い、それなりに勉強を始めたものでした。受賞カテゴリも2007年からはfor Data Center Management- Admin Frameworksというよく分からない名前に代わり、開発系からサーバー系への移籍となりました。

もちろんWindows Serverなんてものもまともに触ったこともなく、今思えばあの頃は色々と憶えることがたくさんあったなーと思いますね。そんなこんなで2008年にはMVPカテゴリもfor Data Center Management- PowerShellとなり、PowerShell専門となりました。

2008年には技術評論社さんからWindows PowerShell ポケットリファレンスという書籍を書かせてもらうことができました。まさか自分が本を書くなんて思ってもいなかったですし、あれはいい経験になったと思っています。

それからも体調はずっとよくなくて迷走しまくりで随分多くの方々にご迷惑をかけ続けていましたが、なんとかMVPという繋がりを武器に社会と繋がっていようとあがいた感じです。書籍や記事執筆、スピーカー、そして密かにプログラマーとして会社に勤めたりしてた時期もありました。

私のあがきとは関係なく、PowerShellはどんどん重要性をましていって、特にWindows Server 2012とPowerShell 3.0がリリースされた2012年は全国各地で声がかかって(自分で志願したのもありますが)、計8回もセッションを担当しました。中でもMicrosoft Windows Developer DaysでMicrosoft社員さんに交じってセッションを担当したのは貴重な経験でした。そして今年はPowerShellポケットリファレンスの改訂版を世に出すことができました。

こうやって10年目にして振り返ると、確かにWindows Scripting / PowerShell周りで要所要所でいろいろやってきたなーという感じですね。今年もPowerShell 4.0 / Windows Server 2012 R2がリリースされる予定で、PowerShellは今後も着実に発展、浸透していくのだと思います。私も陰ながらと言わず割と表立って、そのお手伝いをしていければいいなーと思っています。

さて余談ですが、結局あがいた結果今はどうなん?という話なんですが、結果としてはITの世界では今まで通り、PowerShellの分野ではこういう感じの活動を続けてますが、メインのおしごとは全然ITとも化学とも違う別なことをやっていたりします。ちゃんとおしごとしてるのでそんな目で見ないでくださいね。あと、体調に関しては2年前くらいからほぼ完全に回復してるので、遠慮せず遊んでやってください。

こんなことを書いてると、なんか「お前…消えるのか…?」と思われてしまうかもしれませんがそんなことは全然ない(と思う)ので、これからも変わらず、お付き合いいただければと思います。

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さんの登場です。よろしくお願いします。

2010/02/22

PowerShell 2.0では標準コマンドレットの数が1.0の129個から236個へと、107個増えています。また、既存のコマンドレットの一部にパラメータが増えています。そこで、2.0で新しく加わったコマンドレットと、新しく追加されたパラメータを列挙するスクリプトを作ってみました。

まず、1.0がインストールされている環境で、コマンドレットのリストをXMLに書き出します。次のコマンドを実行してください。

 get-command -type cmdlet|export-clixml cmdlets1.xml -depth 3

同様に、2.0の環境でも実行してください。

 get-command -type cmdlet|export-clixml cmdlets2.xml -depth 3

出来上がった二つのxmlファイルをカレントディレクトリに置いて、次のスクリプトを実行すると、新しく追加されたコマンドレットとパラメータが列挙されます。(必要なら適宜リダイレクトするなどしてファイルに落とし込んでください)

function Get-CmdletHash
{
    param([string]$path)
    $cmdlets = Import-Clixml $path
    $cmdletsHash = @{}
    $cmdlets|
        %{
            $parameters = @()
            $_.ParameterSets|
                %{
                    $_.Parameters|
                        %{
                            $parameters += $_.Name
                        }
                }
            $parameters = $parameters|Sort-Object|Get-Unique|?{"WarningAction","WarningVariable" -notcontains $_}
            $cmdletsHash.Add($_.Name,$parameters)
        }
    return $cmdletsHash
}

$ver1 = Get-CmdletHash cmdlets1.xml
$ver2 = Get-CmdletHash cmdlets2.xml

$ver2.Keys|
    %{
        if($ver1.ContainsKey($_))
        {
            $result = Compare-Object $ver1[$_] $ver2[$_]
            if($result)
            {
                "[Update] $_"  
                $result | Format-Table
            }
        }
        else
        {
            "[New] $_  `r`n" 
        }
    }

出力結果を置いておきます。こちら

このスクリプトではGet-CommandコマンドレットがSystem.Management.Automation.CmdletInfoというコマンドレットの情報を格納したオブジェクトを返すことを利用し、Export-Clixmlコマンドレットでシリアライズ化してXMLファイルに落とし込むことにより異なるバージョンのPowerShell同士を比較しています。ここで-depthパラメータを3にしているのは、パラメータ情報を格納するParametersプロパティがルートから3階層下にあるためです。(デフォルトは2階層下までを出力。PS2.0ではCmdletInfoにParametersプロパティというのがあって2階層でたどれるのですが1.0にこのプロパティはないのでParameterSetsプロパティからたどる必要があります)

また、WarningActionとWarningVariableという共通パラメータが増えているので、これらはすべてのコマンドレットに共通して追加されているパラメータなので除外しておきます。

あとはパラメータをコマンドレットごとに配列化して、Compare-Objectで比較しています。新しく追加されたコマンドレットは[New]のマークをつけ、既存のコマンドレットでパラメータに変化があるものは[Update]をつけて追加されたパラメータを列挙するようにしています。

かなりたくさんのコマンドレットでパラメータが増えたことが分かりますね。また、Get-CommandコマンドレットのPSSnapinパラメータが廃止され、Moduleパラメータに変更されたことなどが分かったりします。

1.0ユーザーだった方にお勧めのスクリプトです。

元記事:http://blogs.wankuma.com/mutaguchi/archive/2010/02/22/186334.aspx


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

Books

Twitter