2015/12/21

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

PowerShellの属性

PowerShellには2.0から言語機能として「属性」機能が追加されました。PowerShellの属性はC#の属性とほぼ同じものですが、2.0〜4.0の時点では、高度な関数(Advanced Function)を作成するために関数に付与するCmdletBinding属性(記述上ではparamブロックに付与する形になる)、関数のパラメータに付与するParameter属性やAlias属性等、関数のパラメータや変数に付与する各種検証属性(Validate〜、Allow〜系)くらいしか使うことはなかったかと思います。

このうちパラメータ検証属性については、あえとす氏が以前詳しく解説しておられます。:パラメーターの検証属性について/前編 - 鷲ノ巣

PowerShellで属性クラスを作る

さて話は変わって、つい先日、WMF 5.0 / PowerShell 5.0がRTMしました。Windows 7/8.1、Windows Server 2008R2/2012/2012R2用のインストーラーが公開されたので、もう使っておられる方もいると思います。ぎたぱそ氏が一晩で解説記事を書いてくれてますね。

PowerShell 5.0では言語機能としてclass構文がついに追加されました。これでついにPowerShell言語も真の意味でオブジェクト指向言語の要素を完備したと言えるかと思います。

このclass構文、.NET Frameworkの既存のクラスを基底クラスとして継承するなんてことも、普通にできてしまいます。そこで私が考えたのは、もしかしてこれで属性も作れるんじゃない?ということでした。

v5のclass構文の基本的な部分の解説は、また別の機会にということにしまして、今回はいきなり、属性を作る話をしていきます。

ちなみにC#の属性については、属性 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C が参考になります。PowerShellで属性を作る時も基本はだいたい同じかと思います。

属性クラスの例

属性パラメータ(コンストラクタ引数)にDescription、名前付きパラメータとしてNoを指定でき、classに適用可能なTest属性はこんな感じです。

[AttributeUsage([AttributeTargets]::Class)]
class TestAttribute : Attribute
{
    [string]$Description

    [int]$No

    TestAttribute($Description)
    {
        $this.Description = $Description
    }
}

これを利用する際は、まずこのclassを含むスクリプトをドットソースで実行することで、グローバルに読みこむ必要があるようです(同一スクリプト内に属性定義と属性利用をまとめて書くとうまく動作しない)。

呼び出し側では以下のように指定します。

[TestAttribute("クラスの説明", No = 1)]
class Foo
{
    $X = 1
}

通常なら[Test()]のように"Attribute"の部分は省略できるはずなんですが、これも何故かフルネームで指定しないとダメなようでした。

クラスに属性が正しく指定されているかどうかはリフレクションで調べます。

[Attribute]::GetCustomAttributes([Foo])

結果は

Description  No TypeId
-----------  -- ------
クラスの説明  1 TestAttribute

こんな感じです。

パラメータ検証属性を作る

これだけでは面白くないので、少し実用になるかもしれない属性を書いてみましょう。

ところで前述のあえとす氏の記事の後編では、C#でPowerShellのパラメータ検証属性を作る方法について解説されています。ただ、C#でPowerShellの実行空間にアクセスして、操作を行ったり情報を取得したりするのは、少し知識が必要な部分かと思います。

そこで、PowerShellで使う属性なんだからPowerShellで書いたら楽になるんじゃない?という発想で、パラメータ検証属性をクラス構文で書いてみました。

このサンプルは、パラメータ(または変数)値の型が、指定の型セットに含まれているかどうかを検証するものです。複数型を取るようにするためにパラメータの型を[psobject]にすることが良くありますが、この属性を利用して、指定可能な型を絞り込めるようになります。

using namespace System.Management.Automation

[AttributeUsage(([AttributeTargets]::Property) -bor ([AttributeTargets]::Field))]
class ValidateTypeSetAttribute : ValidateEnumeratedArgumentsAttribute
{
    [Type[]]$Types

    ValidateTypeSetAttribute($Types)
    {
        $this.Types = $Types
    }

    [void]ValidateElement([object]$element)
    {
        if(!($element.GetType() -in $this.Types))
        {
            throw New-Object ValidationMetadataException `
                "$($element.GetType().FullName) は許容されない型です。値は $($this.Types) のいずれかの型である必要があります。"
        }
    }
}

使い方は以下の通り。パラメータ検証属性の場合は、定義と同一スクリプト内で呼び出しても、"Attribute"を省略しても問題ないようです。

function test
{
    param(
        [ValidateTypeSet(,([int],[string],[double]))]
        [psobject]
        $obj
    )
}

test "a" #OK
test (Get-Process)[0] #NG

class構文のコンストラクタで可変長引数を定義する方法が分からなかったので、呼び出しがちょっと不格好ですが、そこはご容赦を。

2011/04/26

PowerShell 2.0ではAdd-Typeコマンドレットを用いてC#など他言語のコードをコンパイルし実行することが可能です。(P/Invokeも可能です)

ほとんど使っている方はいないと思われますが、JScript.NETのコードもコンパイルして実行できます。以下、コード例です。

$code=@"
static function writeHello()
{
    System.Console.WriteLine("Hello JScript.NET World!");
}
"@

$c = Add-Type -Language JScript -MemberDefinition $code -Name "JSTest" -PassThru
$c[1]::writeHello()

JScript.NETのスタティックメソッドを用意してやると、Add-Typeコマンドレットによりそのメソッドを持ったクラス(ここではMicrosoft.PowerShell.Commands.AddType.AutoGeneratedTypes.JSTest)が生成されます。-passThruパラメータを指定することでその型情報が変数に代入できます。あとはJSTestクラスのスタティックメソッドを::演算子で呼び出すのですが、なぜかAdd-Typeが目的のクラスの型以外にJScript.NETのグローバルクラス?の型情報も一緒に出力するので、型情報が配列になっています。よってインデックスを指定してからスタティックメソッドを呼ぶようにします。

ここではスタティックメソッドを実行する例を挙げましたが、インスタンスメソッドも実行できるか試してみました。

$code=@"
import System;
public class JSTest
{
    public function writeHello()
    {
        Console.WriteLine("Hello JScript.NET World!");
    }
}
"@

Add-Type -Language JScript -TypeDefinition $code
$o = new-object JSTest
$o.writeHello()

ところがこれはNew-Objectのところで「New-Object : "0" 個の引数を指定して ".ctor" を呼び出し中に例外が発生しました: "アプリケーションでエラーが発生しました。"」というエラーになってしまいます。コンストラクタの実行でしくっているようですが…。ちなみに明示的にfunction JSTest()というコンストラクタを定義してやってもだめでした。なんででしょうね?

元記事:http://blogs.wankuma.com/mutaguchi/archive/2011/04/26/198645.aspx

2007/01/15

スクリプト センターの新着記事にこんなのが。

VBScript からも .NET Framework のクラスを利用できる?!
NET Framework のクラスは VBScript からアクセスできないと皆さん思っていませんか? 後悔する前にこの記事を読みましょう!

System.Collections.ArrayListなんかを普通にCreateObjectして使えるんですねー。

しらんかったー!すげー!

たしかにProgIDがレジストリに登録されてるから、使えるような気はしてましたが、まさか本当に使えるとは。

これってどういう仕組みなんだろう?どのクラスが使えてどのクラスがだめかは筆者も調査中とのことですが、相当いろんなことができるんじゃないでしょうか。

少なくともCOMコンポーネントにはコンストラクタがない(ですよね?)のでコンストラクタが必須のクラスは駄目でしょうね。

まだまだWSHいけるんじゃないですか?PowerShellもいいですけどWSHも使いましょう。

元記事:http://blogs.wankuma.com/mutaguchi/archive/2007/01/15/56349.aspx

2006/12/28

PowerShellの配列は基本的に値の入った固定長の配列が作成できます。

たとえば

$a=1,2,3,4,5
$b=6..10
[system.diagnostics.process[]]$proc=get-process

等々。でも空の固定長の配列も実は作成可能であるという話をこの前ある方から聞きました。

PS C:\> [Int32[]]$ar = new-object System.Int32[] 5
PS C:\> $ar.IsFixedSize
True
PS C:\> $ar.Length
5
PS C:\> $ar
0
0
0
0
0

こんな感じです。5が気持ち悪い方は(5)でもいいです。要はnew-objectコマンドレットでSystem.Int32[]の配列を作り、コンストラクタに配列のサイズを指定しているのですね。0で初期化されてしまうのはどうにかならないかな。まあめったに使うことはないと思いますが一応ここでも取り上げておきます。

元記事:http://blogs.wankuma.com/mutaguchi/archive/2006/12/28/53984.aspx

2006/11/29

WMIクラスのインスタンスは簡単にGet-WMIObjectコマンドレットで取れますが、WMIのスタティックなメソッドを実行するにはどうすればいいのでしょう?

結論は.NETクラスのSystem.Management.ManagementClassをnew-objectしたインスタンスから呼べます。

(new-object System.Management.ManagementClass Win32_Process).create("not
epad")

このように、コンストラクタにWMIクラス名を指定します。例はnotepad.exeのプロセスを作成するというものですが、PowerShellではnotepadと入力するだけで実行できるのであまり適切な例とは言えませんが…

ついでに後片付け。notepad.exeを終了します。

gwmi Win32_Process|?{$_.processname -eq "notepad.exe"}|%{$_.terminate()}

betaの段階ではInvokeMethodメソッドを呼ばなければいけなかったようですが、正式版では直で呼べるようですね。ありがたや。

これもstop-process -name notepadでできるのであまり適切な例とは言えませんが…

元記事:http://blogs.wankuma.com/mutaguchi/archive/2006/11/29/47498.aspx

2006/07/14

.NET Framework 2.0にはタスクトレイにアイコンを表示させるためのクラス、NotifyIconクラスがあります。これを使ってみましょう。

function CreateNotifyIconMenu()
{
 # menuItem1オブジェクトの作成
 $menuItem1 = new-object System.Windows.Forms.MenuItem("終了(&X)")
 
 # Clickイベント
 $menuItem1.Add_Click({Form_Closing})
 
 # contextMenuオブジェクトの作成
 $contextMenu = new-object System.Windows.Forms.ContextMenu
 
 # menuItemオブジェクトをcontextMenuオブジェクトのMenuItemsコレクションに追加
 [void]$contextMenu.MenuItems.Add($menuItem1)
 #↑ここの[void]を忘れるとAddメソッドの戻り値がreturnされてしまう。
 
 return ($contextMenu)
}
function Form_Closing()
{
 # フォームとシステムトレイアイコンを非表示に
 $form.Visible = $false
 $notifyIcon.Visible = $false
 
 # PowerShellの終了
 [System.Environment]::Exit(0)
}
[void] [Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") 
[void] [System.Windows.Forms.Application]::EnableVisualStyles()
# notifyIconオブジェクトの作成とプロパティの設定
$notifyIcon = new-object System.Windows.Forms.NotifyIcon
# System.Drawing.Iconクラスのコンストラクタにはicoファイルのパスを指定する
$notifyIcon.Icon = new-object System.Drawing.Icon C:\script\test\a.ico
$notifyIcon.Text = "PowerShell実行中"
$notifyIcon.ContextMenu = CreateNotifyIconMenu
$notifyIcon.Visible = $true
# formオブジェクトの作成
$form = new-object System.Windows.Forms.Form
# Closingイベント
$form.Add_Closing({Form_Closing})
# フォームの表示
[void]$form.showDialog()

理論上はフォームを表示させなくても良いはずなんですが、ループをまわすいい方法が思いつきませんでした(start-sleepを使うとその間イベントが実行されない)。あと、showDialogで表示したフォームを閉じるいい方法も思いつかなかったので[System.Environment]::Exit(0)
としてPowerShell自体のプロセスを終了させています(exitとやるとエラーが出るんですよね。なんでしょうこれは)。これらに対する良い解決策をお持ちの方は教えてください。

PowerShellのfunctionって引数がなしのとき、呼び出す際()を使うとエラーになるんですよね。あと、文頭に持ってこないといけないのも気に入りません。何とかならなかったのでしょうか…。

せっかくタスクトレイが使えるのに、フォームとコンソールまで表示されてしまいます。そこでWSHを併用してごまかす方法を。

Set WshShell=CreateObject("WScript.Shell")
WshShell.Run "powershell test.ps1",0

このようなvbsファイルを作ってps1ファイルをキックしてやります。Runメソッドの第二引数に0を指定すると、コンソールおよびフォームが非表示になりますが、タスクトレイは表示されます。この手を使うとまるでタスクトレイだけが表示されているかのような状態を作り出せます。ちなみに、MessageBoxも同じようにそれだけを表示させることができます。

元記事:http://blogs.wankuma.com/mutaguchi/archive/2006/07/14/32470.aspx


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

Twitter

Books