Itsukaraの日記

最新IT技術を勉強・実践中。最近はDeep Learningに注力。

最近、PowerShellで遊んでます

会社でPowerShellを使う機会があり少し勉強しました。ちょっとWebを探しただけで結構いろいろな記事が見つかるので、本は購入せずに使うことができました。

最初の一歩

当面必要な目的に合わせ最初に読んだのが下記です。

正規表現を使ったファイル名変更が1行で書け、とても便利と感じました。

Get-ChildItem <対象ファイル> | Rename-Item -NewName { $_.Name -replace '旧文字列','新文字列' }

言語仕様などの基本理解

次に読んだのは下記で、PowerShellの基本を理解することができました。

最初は、PowerShellの文法にかなり違和感を感じました。
例えば、関数呼び出しを「()」や「,」無しで書いたり:

function f($p, $a) {
  return "$p -- $a"
}

f "pen" "apple"
# f("pen", "apple")は、fの第1引数の値が("pen", "apple")になる。
# f "pen", "apple" も、fの第1引数の値が("pen", "apple")になる。

比較演算子を「-eq」「-ne」などと書いたり:

if ($p -eq "pen") {
  echo "apple"
}

などです。しかし、PowerShellがコマンド実行系と考えると、コマンド引数は空白で区切るのが基本であり、「-eq」はコマンドに対するオプションと考えられ、「if ($p -eq "pen")」といった記述が自然に感じられるようになりました。

また、PowerShellのパイプでは文字列ではなくオブジェクトが渡されることも、便利と感じました。つまり、上でファイル名変更スクリプトを記載しましたが、対象ファイルとして「C:\TEMP\WORK」といったパスを指定しても、「Rename-Item」に渡されるのはオブジェクトで、「$_.name」は渡されたオブジェクトのファイル名(フルパスではない)を示すので、ファイルパスの文字列にパターンにマッチした文字列があっても、何の問題も起きません。

また、渡ってきたオブジェクトから、ファイル名以外にも色々な情報を取り出せます。例えば、下記のように書けば、"this"と"pen"と"apple"を含む全てのファイルに対して、その名前、長さ(バイト数)、フルパスを表示することができます。

PS C:\TEMP> Get-ChildItem . | Where-Object {$_.name -match ".*this.*pen.*.apple" } | Select-Object -Property name,length,fullname

Name                                  Length FullName                                              
----                                  ------ --------                                              
this-is-a-pen-apple.txt                   25 C:\TEMP\this-is-a-pen-apple.txt              
this-is-a-pen-pineapple-apple-pen.txt     39 C:\TEMP\this-is-a-pen-pineapple-apple-pen.txt
this-is-a-pen-pineapple-apple.txt         33 C:\TEMP\this-is-a-pen-pineapple-apple.txt    

開発環境がインストール済み

PowerShellの実行環境および開発環境は、殆どのWindowsには初めからこれがインストールされています。仕事上では、これが一番ありがたい点だったりします。下記(Wikipedia)を見ると、Windows XPWindows Server 2003などのレガシーでも使えるようです。ただ、PowerShellのバージョンは、Windowsのバージョンによって異なり、古いPowerShellでは使えない機能があったりするので、要注意ですね。

開発環境は、PowerShell ISE (Integrated Scripting Environment)というものがあり、PowerShellやISEで検索すると、すぐに見つかると思います。開発環境では、コマンドやオプションを途中まで入力してTABキーを押すと、コンプリーションしてくれたり候補を表示してくれたりするので、コマンド名やオプション名をうろ覚えでも簡単にプログラムを開発できます。デバッガもついており、Breakpointも設定できます。

GUIも使える

実は、GUIも使えるようで、試しにプログラムを作ってみました。参考にしたサイトは下記です。ちなみに、PowerShellからGUIを使うには各コンポーネントの座標位置までゴリゴリ細かく記述する方法(下記1つ目)と、HTMLのように構造を記述しレイアウトはコンテナに任せるやり方(下記2つ目)とあり、2つ目の方が良いと感じました。


下記は、$src_dirにあるファイルのうち、特定のパターンにマッチするファイルを、$dst_dirにコピーするスクリプトです。GUIでは、パターン中の2つの部分をlistboxで選択するようになっています。これらを選択したうえで、"Copy Files"ボタンを押すと、ファイルがコピーされます。

$ErrorActionPreference = "stop"
Set-PSDebug -Strict
$src_dir = "C:\TEMP\src"
$dst_dir = "C:\TEMP\dst"
Add-Type -AssemblyName PresentationFramework

[xml]$xaml = @'
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="xaml test #1"
    Width="300" Height="250">
  <StackPanel>
    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
      <StackPanel>
        <Label Content="AAAAAAAAA" />
        <ListBox x:Name="listboxA" />
      </StackPanel>
      <Label Content="  " />
      <StackPanel>
        <Label Content="BBBBBBBBB" />
        <ListBox x:Name="listboxB" />
      </StackPanel>
    </StackPanel>
    <Label Content="  " />
    <Button x:Name="btn" Content="Copy Files" />
  </StackPanel>
</Window>
'@

$reader = New-Object System.Xml.XmlNodeReader $xaml
$frm = [System.Windows.Markup.XamlReader]::Load($reader)
$AAA = "090 080 070 050 020".split(" ")
$listboxA = $frm.FindName("listboxA")
foreach ($a in $AAA) {
  [void]$listBoxA.Items.Add($a)
}
$listboxA.SelectedIndex = 0

$BBB = "107 110 116 117 119".split(" ")
$listboxB = $frm.FindName("listboxB")
foreach ($b in $BBB) {
  [void]$listBoxB.Items.Add($B)
}
$listboxB.SelectedIndex = 0

Remove-Item log.txt -ErrorAction:Ignore
function clicked {
    # delete files in $dst_dir before copy
    del $dst_dir\*.*

    $a = $listBoxA.SelectedItems
    $b = $listBoxB.SelectedItems
    $date = date
    echo "[$date] Selected: $a $b" >> log.txt
    $rexp = "^$a-(\d\d\d)-$b.txt$"
    $count = 0
    Get-ChildItem $src_dir | ?{$_.name -match $rexp} | %{$count += 1; echo "Copy $($_.name)" >> log.txt; copy $_.FullName $dst_dir\$($_.name) }
    $msg = New-Object -ComObject wscript.Shell
    $result = $msg.popup("$count files copied")
}

$btn = $frm.FindName("btn")
$btn.Add_Click({clicked})
$result = $frm.ShowDialog()

画面は、下記のような感じです。
f:id:Itsukara:20170128234046p:plain

コピーが完了すると、コピーファイル数を示すメッセージがポップアップします。
f:id:Itsukara:20170128204726p:plain

ちなみに、テストデータを作成するスクリプトも、一応載せておきます。

$AAA = "090 080 070 050 020".split(" ")
$BBB = "107 110 116 117 119".split(" ")
$src_dir = "C:\TEMP\src"
foreach ($i in 1..200) {
    $a  = $AAA[$(Get-Random $AAA.Length)]
    $b  = $BBB[$(Get-Random $BBB.Length)]
    $m  = "{0:D3}" -f (Get-Random 1000)
    $fname = "$a-$m-$b.txt"
    Set-Content $src_dir\$fname "Test Test"
}

追記(2/18追記)

PowerShell ISEでは、そのまま動きますが、PowerShellでは、下記で起動しないとエラーになることがあります。詳細は別記事参照のこと。

今後

今のところ、PowerShellは面白いと感じており、下記のサイトを中心に、もう少し勉強してみる予定です。

また、GUIも、もう少し試したいので、下記で勉強予定です。
www.atmarkit.co.jp

ちなみに、PowerShellは、Windowsだけでなく、他のプラットフォームでも使えるようです。HTML的な感じでGUIが簡単に書けることを考えると、bashよりも良いかもしれません。