【PowerShell】PowerShellでC#っぽいスクリプトを組むときの覚書

前書き

こう、「超単純だしC#なら10分ぐらいで書けるんだけど手作業でやると恐ろしく時間がかかる」みたいな作業ってあるじゃないですか。

そう言うのは最近ScriptCSで書いてるんですけど、あれの弱点って「どんな端末でも実行できるわけではない」なんですよね。.NET 4.5以上が必須だし、そもそもScriptCS自体がない。

Windows Server上での作業だとか、メインで使っているのとは別の開発端末での作業とか、「こいつ叩けば一発っすよ。」と鼻をほじりながら誰かに渡したりするようなスクリプトを書きたいときにはどうしても別の方法を考えなきゃいけません。

ってなると、じゃあbatでも書く?ってなるんですが、あれを書くエネルギーって本当にもったいないじゃないですか。デバッグは大変だし、文法はクソだし、文法はクソだし、文法はクソだし。

System.IO.PathとかSystem.IO.Directoryとかが使えればこんなの一発なのに…!ついでにSystem.DateTimeとかもあったら完璧なのに…!と思ったら、さっさとPowerShellで書くことにしました。

この文章の対象読者

PowerShellの文法を覚えるのは面倒だけどC#っぽいことができるスクリプトを書きたい人

下準備

PowerShellの環境を準備する

準備と言っても、Windows 7Windows Server 2008 R2からは標準搭載です。XPは標準搭載ではないですが、Windows Updateで取得できます。まぁ、XPなんて流石にもう使ってないと思うんですが…。使ってないですよね?

そんなわけで、今ならbatと同じぐらいの感覚で「これ使って」とばらまくことが可能です。実際に書いてる人を全然見たことがありませんが。

IDEの準備をする

PowerShellをインストールすると勝手にPowerShell ISEなる統合開発環境がくっついてきます。

…が、ver.1~2は「PowerShellシンタックスを綺麗に色分けしてくれるちょっと重いエディタ」でしかないです。(デバッグ機能はあるけども)

ver.3以降はインテリセンスが使えるので、可能であればアップデートしてきましょう。可能でなければ、頑張るだけです。

実際に書いてみる

オブジェクトを作る

New-Objectと言うコマンドレットがコンストラクタのかわりです。

ドキュメントには色んな引数が書いてありますが、基本的にはこんな感じでOKです。

$l = New-Object System.Collections.Generic.List[string]

New-Object 名前空間.型名って感じですね。ジェネリクス<T>ではなく[T]になります。

引数を渡すことも当然可能です。複数ある場合は,でOK。

$l = New-Object System.Collections.Generic.List[string](20)

プロパティをセットすることもできますが、インテリセンスが効かないのでオススメしません。

#意味不明なコードだ…。
$l = New-Object System.Collections.Generic.List[string](20) -Property @{Capacity=30}
$l.Capacity
# => 30

静的メソッドを呼び出す

[名前空間.型名]::メソッドで呼び出せます。

$tuple = [System.Tuple]::Create("hoge", 0)
$tuple.Item1
# => hoge
$tuple.Item2
# => 0

usingっぽいことをする

今までの例を見てもらえればわかるんですが、いちいち名前空間を書くのが結構面倒くさいです。(ISE ver.3以降であればインテリセンスである程度補完できますが…。)

PowerShellの文法にはC#のusingに該当するシンタックスはありませんが、近いことはできます。

# 名前空間を文字列で保持しておく
$nGeneric = "System.Collections.Generic"

# New-Objectの際に型名を文字列で渡す
$l = New-Object "$nGeneric.List[string]"
$l.Add("hoge")
$l.Add("fuga")
$l

# => hoge
# => fuga

静的メソッドを呼ぶ場合はもう一手間必要です。

$nIO = "System.IO"

# デフォルトで読み込まれていないアセンブリならロードしておく必要がある
#[Reflection.Assembly]::LoadWithPartialName($nIo)

# Typeにキャストする
$cPath = "$nIO.Path" -as [type]

$cPath::GetFileName("C:\test.txt")
# => test.txt

どちらもインテリセンスが効かなくなってしまうんですが、F8で選択した文字列だけを実行できるので、usingっぽいところだけを適宜実行すればOKです。

もうちょっと実践的なことをする

ファイルの退避処理

例えば、C:\TMP配下の.txtファイルをC:\TMP\backにタイムスタンプ(yyyymmdd)をつけて格納する、みたいな処理。

C:\TMP
│  fuga.txt
│  hoge.txt
│  piyo.png
│
└─back

これだけの処理ですがbatでやろうとすると結構面倒です。dirでtxtだけとってきて…batのforってどう書くんだっけ…日付の書式ってどう書くんだっけ…と嫌になってきます。

ここは.NETの力を借りましょう。どんどん借りましょう。

$baseDir = "C:\tmp"
$today = [datetime]::Today.ToString("yyyyMMdd")

foreach($file in (dir ($baseDir + "\*.txt"))) {
    
    # バッククオートを使うと改行できる
    $destFile = $baseDir + "\back\" + [System.IO.Path]::GetFileNameWithoutExtension($file) `
                    + "_" + $today + ".txt"

    copy $file $destFile
}

インテリセンスを使いつつ大体3分ぐらいで書けました。

また、当然のことですがPowerShellではパスが通ってるコマンドならいくらでも使用可能です。(dirとかcopyとかmoveとかはPowerShellのaliasで色々構文が変わってしまっていますが…。)コマンドの構文が思い出せなかったら.NETのAPIでやっちゃえばいいです。

それに加えて制御構文としてforeachswitchwhileなんかも当然のように使えます。もうbatを使う理由なんて一つもないですね。

もういっそ全処理をAdd-Typeでぶちこむ

ここまで読んでも「いやでもやっぱPowerShellクソ面倒でしょ。C#最高でしょ。」と思った人にオススメの方法があります。

PowerShell ver. 2から追加されたAdd-Typeと言うコマンドレットがあるんですが、.NETのDLLやソースをPowerShellで使えるようにしてくれます。コードをインラインで書いてもOKです。

# 文字列を@でくくるとヒアドキュメントになる
Add-Type @"
using System;

public class Test {
    public static void Exec(string arg) {
        Console.WriteLine(arg);
    }
}
"@

[Test]::Exec("hello world")
# => hello world

「やりたいことを全部C#で書く⇒PowerShellでAdd-Typeする⇒適当に呼び出す」とするだけでPowerShellの記述は、まぁおおよそ3行ぐらいですみます。やりましたね!

ちなみにこれ、-Languageを指定することでVB.NETでもいけます。VBおじさんにも優しい!やりましたね!

Add-Type -Language VisualBasic @"
Public Class Test
    Public Shared Sub Exec(ByVal args As String)
        System.Console.WriteLine(args)
    End Sub
End Class
"@

[Test]::Exec("hello world")
# => hello world

また、なぜかJScriptも読み込めます。何の因果かJavaScriptを習得してしまったWebデザイナーにも優しい!やりましたね!

$obj = (Add-Type -Language JScript -MemberDefinition @"
    static function exec(args) {
        // 流石にJScriptでConsole.WriteLineは無理っす…。
        return args
    }
"@ -Name "Test" -PassThru)[1]

Write-Host $obj::exec("hello world")
# => hello world

JScriptの例で-MemberDefinitionなるものを使いましたが、これを使えばC#でもいちいちクラスの宣言等が不要になります。

$obj = Add-Type -MemberDefinition @"
public static void Exec(string arg) {
    Console.WriteLine(arg);
}
"@ -Name "Test" -PassThru

$obj::Exec("hello world")
# => hello world

更にコードが短くなりました!もう怖いものなしですね!どんどんC#書きましょう!

まとめ

中々PowerShellおじさんに怒られそうな内容になりました。っていうか内容がペラッペラなので、何か思いついたらどんどん追記していきます…。

ともかく、PowerShellの文法をほとんど知らなくてもここまでやりたい放題できるので、ちょっとしたスクリプトをbatなんてゴミで作るぐらいならどんどん活用していきましょう。

参考