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の知識は必須になってきますので、必要に応じて参考にしていただければ幸いです。

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 "管理者権限がありません"
}

2015/08/10

C#6.0のnameof演算子(じんぐるさんによる解説岩永さんによる解説)が羨ましかったので、PowerShellでも似たようなことができるようにしてみました。

function nameof
{
    param([scriptblock]$s)
    $element=@($s.Ast.EndBlock.Statements.PipelineElements)[0]
    if($element -is [System.Management.Automation.Language.CommandExpressionAst])
    {
        switch($element.Expression)
        {
            {$_ -is [System.Management.Automation.Language.TypeExpressionAst]}
                {$_.TypeName.Name}
            {$_ -is [System.Management.Automation.Language.MemberExpressionAst]}
                {$_.Member.Value}
            {$_ -is [System.Management.Automation.Language.VariableExpressionAst]}
                {$_.VariablePath.UserPath}
        }
    }
    elseif($element -is [System.Management.Automation.Language.CommandAst])
    {
        $element.CommandElements[0].Value
    }
}

nameof{$PSHOME}                      # 変数名 : PSHOME
nameof{$PSHOME.Length}               # プロパティ名 : Length 
nameof{[System.Diagnostics.Process]} # クラス名 : System.Diagnostics.Process
nameof{[string]::Empty}              # フィールド名 : Empty
nameof{[DayOfWeek]::Friday}          # 列挙体メンバー名 : Friday
nameof{Get-Command}                  # コマンド名 : Get-Command

原理的には、変数やプロパティ等をスクリプトブロックに格納し、生成されるAST(抽象構文木、abstract syntax tree)を解析して、含まれる変数名やプロパティ名を抽出しています。(なので、PowerShell 3.0以上でないと動作しないと思います)

そもそも、どういうシチュエーションで使うの?という話ですが、実はあえとすさんのPowerShell コマンドを C# で書くときに便利な拡張メソッド - 鷲ノ巣という記事を見て、じゃあPSでコマンド(高度な関数)を書く時にも同じことが出来るといいかな?と思ったのがきっかけです。

function Get-Test
{
    [CmdletBinding()]
    param([int]$Number)
    if($PSBoundParameters.ContainsKey((nameof{$Number})))
    {
        "-$(nameof{$Number})パラメータが指定された"
    }
}

こういう風に、"Number"という文字列をコード中に書かずに、-Numberパラメータ指定の有無を確認できるようになる、というわけです。

(この例の場合だと、Get-Test -Number 12 のようにすると、if文の中身が実行されます。)

ただ作ってはみたものの、使う意味がどれほどあるのか疑問に思えてきました。一応、ISEでは変数名やメンバ名に入力補完が効くので、実際の変数名を文字列で手打ちしなくて済むというメリットはなきにしもあらず、ですか。

しかし所詮は動的言語なので、存在しない変数名やメンバ名を入れても実行前にエラーは出ないですからね。(Set-StrictModeによるストリクトモードは編集時ではなくあくまで実行時(正確には変数やメンバを参照した瞬間)にエラーを出すためのもの)

それとISEのリファクタリング機能は弱い(というか無い)ので、リファクタリングに追従できるという本家nameof演算子に存在するメリットは、現状のところISEを使っている限りは享受できません。

PowerShell 5.0からの新要素、Script Analyzerによる静的解析を組み合わせればあるいは意味が出てくるのかもしれないですが、まだ確認できてないです。

というわけで、書いてはみたもののなんか微妙ですが、せっかくなんで公開しときます。

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

本記事はPowerShell Advent Calendar 2012、最終日の記事です。

前回はAdd-Typeコマンドレットを使って独自のクラスを作成し、そのクラスを入力あるいは出力型に取る関数をどのように記述すれば良いのか、というお話でした。

今回は前回に残した課題である、ユーザー定義の独自型の出力にちゃんとした書式を設定する方法について説明していきます。

型データと書式設定データ

PowerShellは.NETオブジェクト(に限らず、型アダプタが存在するCOMやXMLなども、ですが…)をラッピングした型システムを有しているわけですが、このラッピング時にオブジェクトに対してPowerShell独自のデータを付与することができます。それが型データと書式設定データと呼ばれるものです。

型データはクラスにPowerShellエンジンが付加する独自のメンバ(プロパティ、メソッド)です。代表的なものにNoteProperty(静的な値をもつプロパティ), ScriptProperty(スクリプトで記述されたgetterとsetterをもつプロパティ), AliasProperty(既存プロパティのエイリアス), CodeProperty(.NETのスタティックプロパティ), CodeMethod(.NETのスタティックメソッド), ScriptMethod(スクリプトで記述されたメソッド)があります。

型データは .types.ps1xmlファイルにその定義を記述し、モジュールならモジュールマニフェスト(.psd1)のTypesToProcessプロパティに型データファイルパスを指定することで、インポート時に型データを反映させることができます。

Update-TypeDataコマンドレットで後から型データファイルを読み込んで反映することもできます。PowerShell 3.0ではUpdate-TypeDataコマンドレットで.types.ps1xmlファイルを読むのではなく、直接任意のメンバを任意の型に追加することも可能になっています。

また、

$obj | Get-Member -View Extended

とすることで$objに追加されたメンバがどれなのかが分かります。(ちなみに型アダプタによって追加されたメンバはAdapted指定で分かります)

今回の記事では型データについてはこれくらいにして(またいつか改めて取り上げたいですが)、以下、本題の書式設定データの話をしていきます。

書式設定データとは

書式設定データも型データと同様、クラスに付加するデータなのですが、これはオブジェクトを出力する際のデフォルトの表示フォーマットを定義するものとなります。

たとえばGet-Processコマンドレットを実行すると

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
    138      13    18456       7104    62            2052 aaHMSvc
     88       8     2168       1268    55            2112 AdminService
...

のようにProcessオブジェクトが表形式で表示されます。

ここで表に含まれるプロパティ、IdとProcessNameは.NETのProcessクラスが持つオリジナルのプロパティで、HandlesはHandleCountプロパティのAliasPropertyです。NPMやWSなども対応するAliasPropertyやScriptPropertyが定義されているのですが、たとえばNPMというAliasPropertyはあってもNPM(K)というメンバはありません。これを定義しているのが書式設定データになるわけです。そもそもこの表に含まれるプロパティの種類であるとか、もっというとProcessオブジェクトは特に指定がない場合は表形式で表示する、といった定義も書式設定データに含まれます。

書式設定データも型データと同様にXMLファイルに定義されるのですが、その拡張子は.format.ps1xmlです。モジュールならマニフェストのFormatsToProcessプロパティに.format.ps1xmlファイルのパスを指定することで表示に反映されますし、Update-FormatDataコマンドレットによって後から反映させることも可能です。

この書式設定ファイルはユーザー定義型に関しても定義を記述できます。つまり、ユーザー定義型に対応する.format.ps1xmlファイルを記述し、それを読み込むことで、自分がAdd-Typeで作った型に対しても書式を設定できるわけです。次の節でそのやり方を見ていきましょう。

書式設定データの作り方

書式設定データ.format.ps1xmlの書式についてはMSDNにリファレンスがあるので、これを読めば自分で一から作成することは可能です。ですがそれはちょっと面倒くさいので、既存の書式設定データをベースに、独自型用に修正していくのがお勧めです。

書式設定データはGet-FormatDataコマンドレットで取得でき、Export-FormatDataコマンドレットでファイルとして出力できます。なお、Export-FormatDataコマンドレットの出力XMLファイルは改行コードが入っていなくて見づらいので、XmlDocumentとして再度読み込んでSave()するという小細工を施すのがお勧めです。先ほどのProcessクラス(System.Diagnostics.Process)の書式設定データをファイル化するには以下のようなスクリプトを実行します。

$ps1xml="process.format.ps1xml"
Get-FormatData System.Diagnostics.Process | Export-FormatData -Path $ps1xml -IncludeScriptBlock
([xml](Get-Content $ps1xml)).Save((Join-Path (Get-Location) $ps1xml))

このスクリプトを実行すると、Processクラスの書式設定データをprocess.format.ps1xmlファイルとして出力できます。

出力した.format.ps1xmlはPowerShell ISE(ただしv3の)で開くのがお勧めです。ちゃんとXMLノードを折りたたみできるので。

さて、出力した.format.ps1xmlファイルをつらつらと眺めると、実際の出力書式の定義はビュー(View)という単位で行われていることがわかります。View要素の下にはName要素(ビューの名前)、ViewSelectedBy要素(ビューを反映する対象の型)、TableControl要素(表の書式)があります。

TableControl要素の下にはTableHeaders要素とTableRowEntries要素が含まれており、前者は表のヘッダーに記載するラベルやその幅などをTableColumnHeader要素に一つ一つ定義し、後者は表の本体に表示するオブジェクトのプロパティ値をTableColumnItem要素に一つ一つ定義しています。

TableColumnItem要素は単純にプロパティ値を表示させるならPropertyName要素にプロパティ名を書くだけでOKです。スクリプトの結果を表示させるならScriptBlock要素内にスクリプトを書きます。ScriptBlock要素内で自動変数$_に1オブジェクトが格納されています。

結局のところ、表に表示したいプロパティの分だけ、TableColumnHeader要素(プロパティ名のラベル)とTableColumnItem要素(プロパティ値)を1:1で定義していけばOKです。

以上を踏まえてprocess.format.ps1xmlを改変して、前回作成したWinscript.Driveクラスの書式設定ファイルdrive.format.ps1xmlを作成してみました。

<?xml version="1.0" encoding="utf-8"?>
<Configuration>
  <ViewDefinitions>
    <View>
      <Name>drive</Name>
      <ViewSelectedBy>
        <TypeName>Winscript.Drive</TypeName>
      </ViewSelectedBy>
      <TableControl>
        <TableHeaders>
          <TableColumnHeader>
            <Label>Name</Label>
            <Width>4</Width>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>VolumeName</Label>
            <Width>15</Width>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>Type</Label>
            <Width>15</Width>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>RootPath</Label>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>Size(GB)</Label>
            <Width>25</Width>
            <Alignment>Right</Alignment>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>Used(%)</Label>
            <Width>7</Width>
            <Alignment>Right</Alignment>
          </TableColumnHeader>
        </TableHeaders>
        <TableRowEntries>
          <TableRowEntry>
            <TableColumnItems>
              <TableColumnItem>
                <PropertyName>Name</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>VolumeName</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>Type</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>RootPath</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <ScriptBlock>[int]($_.Size/1GB)</ScriptBlock>
                <FormatString>{0:#,#}</FormatString>
              </TableColumnItem>
              <TableColumnItem>
                <ScriptBlock>[int]($_.UsedSpace*100/$_.Size)</ScriptBlock>
              </TableColumnItem>
            </TableColumnItems>
          </TableRowEntry>
        </TableRowEntries>
      </TableControl>
    </View>
  </ViewDefinitions>
</Configuration>

前回作成したスクリプトを実行後、このdrive.format.ps1xmlファイルを

Update-FormatData -AppendPath .\drive.format.ps1xml

のようにして現在のセッションに読み込んでやることで、以降は定義した関数を実行すると、

PS> Get-Drive
Name VolumeName      Type            RootPath                  Size(GB) Used(%)
---- ----------      ----            --------                  -------- -------
C:                   LocalDisk       C:\                            112      88
D:                   LocalDisk       D:\                            466      63
Q:                   CompactDisc     Q:\                                       
V:                   NetworkDrive    \\server\D                   1,397      64

このように定義した型のオブジェクトに対しても、綺麗な書式で出力することができるようになるわけです。

まとめ

ここまで全三回にわたって、「関数の定義」「型の定義」「出力書式の定義」の基本のきについて説明してきました。基本とはいえ、PowerShellでがっつりとちゃんとした関数を書く上で真っ先に押さえておかないといけないことばかりですし、逆にここまで必要最小限に絞った記事もあまりないかなと思い、まとめてみました。参考にしていただければ幸いです。

さて、PSアドベントカレンダー2012もこれで終わりです。皆様、よいクリスマス…はもう終わりなので、よいお年を!

※終わりと言っておきながら実は明日以降、ロスタイムとしてもうひとかたご登場の予定です。ご期待ください。

2012/12/14

本記事はPowerShell Advent Calendar 2012の14日目の記事になります。

前回(アドベントカレンダー1日目)は「PowerShellらしい関数の書き方」と題して、パイプライン内でうまく他のコマンドと連携させるための関数をどう書けばいいのか、ということについて書きました。前回の関数の例では入力型と出力型がstringだったのですが、実際は自分で定義した型を入力、出力値に取るように書くのが普通かと思います。今回は、それをするためにどうやって型を定義するのか、そしてその型を関数にどう指定するのか、という話をします。

PowerShellにはクラス定義構文がない

そもそもの話になるんですが、型を定義する、つまりはクラスを記述するためのPowerShellのステートメントやコマンドレットが無いため、PowerShell単独ではできません。なので無理です以上おしまい。…というわけにはいかないので、実際はどうするのがいいのかという話をしていきます。

方法としては大きく分けて二つあると思います。

1.C#など他の.NET言語を用いてクラスを記述する

2.ユーザー定義オブジェクトを作成する

今回は1の方法を説明します。

C#を用いてクラスを記述する

つまりはPowerShellでクラスを定義できないなら、C#を使えばいいじゃない。ということです。幸いPowerShell 2.0からはAdd-Typeというコマンドレットを用いると、C#やVBなど.NET言語のソースをその場でコンパイルしてアセンブリとして現在のセッションに読み込むことが可能です。

たとえば、論理ドライブを表すDriveというクラスを定義してみます。

Add-Type -TypeDefinition @"
    namespace Winscript
    {
        public enum DriveType
        {
            Unknown, NoRootDirectory, RemovableDisk, LocalDisk, NetworkDrive, CompactDisc, RAMDisk
        }

        public class Drive
        {
            public string Name {get;set;}
            public string VolumeName {get;set;}
            public DriveType Type {get;set;}
            public long Size  {get;set;}
            public long FreeSpace  {get;set;}
            public long UsedSpace  {get;set;}
            public string RootPath {get;set;}
        }
    }
"@ -Language CSharpVersion3

このようにC#のコードを文字列として-TypeDefinitionパラメータに与えると、コンパイルされて指定のクラス(ここではWinscript.Drive)がロードされます。

ここで-Language CSharpVersion3というパラメータは指定コードをC# 3.0としてコンパイルすることを指定するため、今回使用している自動実装プロパティなどC# 3.0の構文が利用できます。なおこのパラメータはPowerShell 3.0では不要です。ただし明示しておくとPowerShell 2.0でも正しく動作します。というのも-Languageパラメータ省略時はPowerShell 2.0ではC# 2.0でコンパイルされるのですが、PowerShell 3.0ではC# 3.0でコンパイルするためです(逆にPSv3でC#2.0でコンパイルするには”CSharpVersion2”という新しく追加されたパラメータ値を指定します)

なお、ここでは-TypeDefinitionパラメータを用いてクラス全体を記述しましたが、この例のように列挙体も定義してそれをプロパティの型にするなどせず、すべて基本型のプロパティで完結するのならば、-MemberDefinitionパラメータを使ってメンバ定義だけを行う方が記述が短くなります。以下はWinscript.Manというクラスを定義する例です。

Add-Type -Namespace Winscript -Name Man -MemberDefinition @"
    public int Age {get;set;}
    public string Name {get;set;}
"@ -Language CSharpVersion3

例のようにC#のコード内には特にロジックを記述せず、単にデータの入れ物となるクラスにとどめておくのが良いかと思います。別にロジックを書いてもいいのですが、ISEで記述する限りはC#の編集に関してはただのテキストエディタレベルの恩恵しか受けないですし、それなら最初からVisual Studio使ってC#で全部コマンドレットとして書けばいいのに、ともなりかねないので。PowerShellでは実現困難な処理などがあればそれをメソッドとして書く程度ならいいかもしれません。ただしメソッドを記述してもそれをユーザーに直接使わせるというよりも、関数でラップして使わせる形が望ましいでしょう。

さて、次はこのクラスのオブジェクトを扱う関数を記述していきます。

定義した型のオブジェクトを扱う関数の記述

ここでは3つの関数を定義しています。Get-Drive関数はシステムに含まれるすべての論理ドライブを取得、Show-Drive関数は指定のDriveオブジェクトをエクスプローラで開く、Set-Drive関数は指定のDriveオブジェクトのボリューム名(VolumeNameプロパティ)を変更するものです。

ちなみに関数の動詞部分(ここではGet, Show, Set)は、Get-Verb関数で取得できるリスト以外のものは基本的に使わないようにします。モジュールに組み込んだ場合、インポートのたびに警告が出てしまうので。

関数の基本については前回に書いているので、今回のコードはそれを踏まえて読んでみてください。

function Get-Drive
{
    [OutputType([Winscript.Drive])]
    param(
        [string[]]$Name,
        [Winscript.DriveType]$Type
    )

    Get-WmiObject -Class Win32_LogicalDisk | ForEach-Object {
        if($null -ne $Name -and $Name -notcontains $_.Name)
        {
        }
        elseif($Type -ne $null -and $_.DriveType -ne $Type)
        {   
        }
        else
        {
            New-Object Winscript.Drive -Property @{
                Name = $_.Name
                VolumeName = $_.VolumeName
                Type = [enum]::Parse([Winscript.DriveType],$_.DriveType)
                RootPath = if($_.ProviderName -ne $null){$_.ProviderName}else{$_.Name + "\"}
                Size = $_.Size
                FreeSpace = $_.FreeSpace
                UsedSpace = $_.Size - $_.FreeSpace
            }
        }
    }
}

(↑10:33 foreachステートメントではなくForEach-Objectコマンドレットを使うように修正。Get-*な関数のようにパイプラインの先頭で実行する関数でも、内部でPowerShellのコマンドレットや関数の出力を利用する場合は、配列化してforeachするよりも、ForEach-Objectで出力を逐次処理した方が良いですね。内部関数の出力がすべて完了してから一気に出力するのではなく、内部関数が1個オブジェクトを出力するたびに出力するようにできるので。)

function Show-Drive
{
    [OutputType([Winscript.Drive])]
    param(
        [Parameter(ValueFromPipeline=$true,Mandatory=$true)]
        [Winscript.Drive[]]
        $Drive,
        
        [switch]
        $PassThru
    )

    process
    {
        foreach($d in $Drive)
        {
            Start-Process $d.Name
            if($PassThru)
            {
                $d
            }
        }
    }
}
function Set-Drive
{
      param(
        [Parameter(ValueFromPipeline=$true,Mandatory=$true)]
        [Winscript.Drive]
        $Drive,
        
        [Parameter(Mandatory=$true)] 
        [string]
        $VolumeName
    )

    process
    {
        Get-WmiObject Win32_LogicalDisk -Filter "DeviceID='$($Drive.Name)'" |
            Set-WmiInstance -Arguments @{VolumeName=$VolumeName} | Out-Null
    }
}

細かい説明は省きますが、前回説明した関数の基本フォーマットに、自分で定義した型を適用してロジックを書くとこうなる、という参考例としてとらえてください。

一つだけ前回に説明し忘れてたことがあります。それは[OutputType]属性です。これは文字通り、関数の出力型を指定するものです。この属性を指定しておくと何が嬉しいかというと、関数の出力を変数に代入したりWhere-Objectコマンドレットでフィルタをかけるコードを記述する際、関数の実行「前」にもプロパティ名をちゃんとタブ補完してくれるようになります。残念ながらこの静的解析機能はPowerShell 3.0からのものなので2.0だとできませんが、OutputType属性自体は2.0でも定義可能なので、定義しておくことを推奨します。

さて、型の定義と関数の定義をしたので実際に関数を実行してみます。

PS> Get-Drive # 全ドライブ取得

Name       : C:
VolumeName :
Type       : LocalDisk
Size       : 119926681600
FreeSpace  : 12262494208
UsedSpace  : 107664187392
RootPath   : C:\

Name       : D:
VolumeName :
Type       : LocalDisk
Size       : 500086886400
FreeSpace  : 198589583360
UsedSpace  : 301497303040
RootPath   : D:\

Name       : Q:
VolumeName :
Type       : CompactDisc
Size       : 0
FreeSpace  : 0
UsedSpace  : 0
RootPath   : Q:\

Name       : V:
VolumeName : 
Type       : NetworkDrive
Size       : 1500299390976
FreeSpace  : 571001868288
UsedSpace  : 929297522688
RootPath   : \\server\D

PS> Get-Drive | where {$_.Size -gt 1TB} # Where-Objectでフィルタ

Name       : V:
VolumeName : 
Type       : NetworkDrive
Size       : 1500299390976
FreeSpace  : 571001868288
UsedSpace  : 929297522688
RootPath   : \\server\D

PS> Get-Drive -Type NetworkDrive | Show-Drive -PassThru | ConvertTo-Csv #ネットワークドライブのみエクスプローラーで開く。取得結果はCSVとして出力。
#TYPE Winscript.Drive
"Name","VolumeName","Type","Size","FreeSpace","UsedSpace","RootPath"
"V:","","NetworkDrive","1500299390976","571001868288","929297522688","\\server\D"
PS> Get-Drive -Name D: | Set-Drive -VolumeName 新しいドライブ # D:ドライブのボリューム名を指定。(管理者権限で)

関数をきちんとPowerShellの流儀に従って記述したおかげで、このようにPowerShellの他の標準コマンドレットと同様の呼び出し方ができ、自作関数やそれ以外のコマンド同士をうまくパイプラインで繋げて実行することができています。

さて、おそらく一つ気になる点があるとすれば、ドライブの容量表示が見づらいということでしょう。容量であればGBとかの単位で表示してほしいですし、大きい数字は,で桁を区切ってほしいですよね。じゃあそういう値を文字列で返すプロパティを定義してやる必要があるというかと言えばそんなことはなく、PowerShellには型に応じた表示フォーマットを指定する方法が用意されています。次回はそのあたりを解説しようと思います。

また、C#とかめんどくさいしもうちょっと楽な方法はないのか?ということで、最初の方でちょっと触れた、ユーザー定義オブジェクトを利用する方法も、余裕があれば次回に。

さて、PSアドベントカレンダー、明日はsunnyoneさんです。よろしくお願いします!

2012/03/14

2/25のわんくま大阪勉強会#47でおこなったセッション「PowerShell 3.0 概要」のスライドを公開します。

この資料はCTP版やDeveloper Preview版をもとに作成していますが、セッションの数日後にWindows Server 8 beta / WMF3.0 betaがリリースされたので、内容は若干古くなっています(特にISEの見た目は大幅に変更された)。しかし大筋は問題ないですので、参考にしていただければ幸いです。PowerShell 3.0の日本語で書かれた資料としてはかなり充実してる方だと思いますよ!

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に関する記載があることでも分かるかと思います。

2011/11/14

TechNet マガジン October 2011内のWindows PowerShell: 空白文字を入れてくださいという記事に対する意見です。

PowerShellスクリプトを記述するときは適宜空白文字やインデントを入れて見やすくしましょう、という趣旨の記事なんですが、その趣旨には同意するものの、どうも実際にインデントを入れたスクリプトの例がいけてない気がしました。

ここでインデントや改行などを適切に追加して体裁を整えたとされるコードは次のようなものです。

(ブログのスタイルに起因する見え方の違いが考えられるので、比較のためにオリジナルも再掲させていただきます)

function Get-PCInfo {
[CmdletBinding()]
param(
        [Parameter(Mandatory=$True,
ValueFromPipeline=$True,
ValueFromPipelineByPropertyName=$True)]
        [string[]]$computername
    )
    PROCESS {
        Write-Verbose "Beginning PROCESS block"
foreach ($computer in $computername) {
            Write-Verbose "Connecting to $computer"
            try {
                $continue = $true
                $cs = Get-WmiObject -EV mybad -EA Stop `
-Class Win32_computersystem `
-ComputerName $computer
            } catch {
                $continue = $false
                $computer | 
Out-File -FilePath oops.txt -append
Write-Verbose "$computer failed"
                $mybad | ForEach-Object { Write-Verbose $_ }
            }
            if ($continue) {
                $proc = Get-WmiObject win32_processor `
-ComputerName $computer | 
                    select -first 1
                $obj = new-object -TypeName PSObject
             $obj | add-member NoteProperty ComputerName $computer
             $obj | add-member NoteProperty ProcArchitecture $proc.addresswidth
             $obj | add-member NoteProperty Domain $cs.domain
             $obj | add-member NoteProperty PCModel $cs.model
                $obj.psobject.typenames.insert(0,'MyPCInfo')
                write-output $obj
            }
        }
    }
}

最初見た時、全体的に何がなんだか分からなかったのですが、よくよく見てみると一応ルールはあるようで、一つの文を改行して複数行に記述する場合は、二行目以降は文頭から詰めて記述する、というルールに従っているようです。

こういう書き方はかえって読みづらいと感じてしまうのは私だけでしょうか? 確かに、Webページや書籍などで一行がスペースに収まらないときに強制的に改行して表示する場合にこのような体裁になることはありますが、これははっきり言って読みづらいです。

せっかく自分で改行を入れるわけですから、改行したときも読みやすくしたほうがいいでしょう(そもそも強制改行されて読みづらくなるというのを防ぐというのも、自分で改行を入れる意義の一つなわけですから)。

あとparam()やforeach{}のインデント位置はそもそもなぜそうなのかよくわかりませんね(単なる編集ミスだろうか?)。

この辺を踏まえて、私流にインデントを入れるとこんな感じです。

function Get-PCInfo
{
    [CmdletBinding()]
    param
    (
        [Parameter(
            Mandatory=$True,
            ValueFromPipeline=$True,
            ValueFromPipelineByPropertyName=$True
        )]
        [string[]]$computername
    )
    
    process
    {
        Write-Verbose "Beginning PROCESS block"
        foreach ($computer in $computername)
        {
            Write-Verbose "Connecting to $computer"
            try 
            {
                $continue = $true
                $cs = Get-WmiObject -EV mybad -EA Stop `
                      -Class Win32_computersystem -ComputerName $computer
            }
            catch
            {
                $continue = $false
                $computer | 
                    Out-File -FilePath oops.txt -append
                Write-Verbose "$computer failed"
                $mybad | ForEach-Object { Write-Verbose $_ }
            }
            if ($continue)
            {
                $proc = Get-WmiObject win32_processor `
                        -ComputerName $computer |
                            select -first 1
                $obj = new-object -TypeName PSObject
                $obj | add-member NoteProperty ComputerName $computer
                $obj | add-member NoteProperty ProcArchitecture $proc.addresswidth
                $obj | add-member NoteProperty Domain $cs.domain
                $obj | add-member NoteProperty PCModel $cs.model
                $obj.psobject.typenames.insert(0,'MyPCInfo')
                write-output $obj
            }
        }
    }
}

こんな感じでPowerShellも基本的には他の言語のコード整形の流儀に準じればいいと思います。唯一PowerShellで特徴的なのは複数行に渡るパイプラインの記述方法になると思いますが、そこも特に深く考えずに一段インデントを深くする程度でいいのではないかと思います。

あと「一貫性」の節で、「{」を改行の前に入れるか後に入れるかは統一させよとありますが、基本的にはそれでいいと思います。ただ改行の後に「{」を入れるスタイル(私の書いた例)でも、改行後に「{」や「(」を入れるとコードの意味が変わったりエラーになったりする場合が出てくるので、その場合はわざわざ継続文字「`」を入れてやらずとも、その部分だけは改行前に入れてやってもいいかなと思います。

具体的にはこのコードでいうと「[Parameter( 」のところなどですね。あとスクリプトブロックをパラメータに取るコマンドレットの場合なんかもそうです。

@(1..12)|ForEach-Object {
    Write-Host $_
    Write-Host ($_ * $_)
}

こういうのですね。この場合ForEach-Objectコマンドレットの-processパラメータ(ここではパラメータ名は省略されている)にスクリプトブロックを指定しているのですが、「{」はスクリプトブロックリテラルの開始文字なので、改行後に入れると正しく動作しません。

PowerShell ISEはまだVisual Studioなどに比べると編集機能が貧弱で、構文を元に自動的に整形してくれたりしないので、ユーザーが自分ルールで整形していかないといけないです。しかし基本的には他の言語と同様な、素直な整形を施してやれば特に読みづらくなるということもないかなと思います。

元記事:http://blogs.wankuma.com/mutaguchi/archive/2011/11/14/212009.aspx


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

Books

Twitter