【Windows】 テキストベースシェル使いのためのPowershellメモ

【Windows】 テキストベースシェル使いのためのPowershellメモ

Windows Powershell

最近、何かとWindows Powershellを触る機会が多い。

Powershellといえばオブジェクト指向のシェル。
巨大な.NETライブラリを利用できる上、そのオブジェクトをパイプラインで渡すことができるので非常に便利。

ただ、Bashとかの従来のテキストベースシェルに比べると扱い方が特殊。
もともと.NETerな方々ならともかく、それ以外の人にとっては結構学習コストが高いのではないかと思う。

そんなわけで、テキストベースシェルを使っている人向けにPowershellの使い方の簡単なメモを書いてみる。

標準出力とパイプで渡されるものは別物

テキストベースシェルを使っていた人がPowershellを使って一番戸惑うのが、
コンソールへの出力とパイプで渡されるデータが一致しないことだと思う。

こんな感じ。
PS C:\work> Get-Childitem

    ディレクトリ: C:\work

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----       2018/04/04     20:31                testfolder
-a----       2018/04/04     20:31             70 test.txt
-a----       2018/04/04     20:32             70 test_.txt
-a----       2018/04/04     20:33             86 test_1.txt
-a----       2018/04/04     20:33            164 test_2.txt
# コンソールに出力される項目は、[Mode]、[LastWriteTime]、[Length]、[Name]
# テキストベースシェルだと、パイプで渡せる情報はこれだけのはず

# ところが、Powershellの場合…
PS C:\work> Get-Childitem | Select-Object Directory,Name,CreationTime,LastAccessTime

Directory Name       CreationTime        LastAccessTime
--------- ----       ------------        --------------
          testfolder 2018/04/04 20:31:32 2018/04/04 20:31:32
C:\work   test.txt   2018/04/04 20:30:48 2018/04/04 20:30:48
C:\work   test_.txt  2018/04/04 20:32:48 2018/04/04 20:32:48
C:\work   test_1.txt 2018/04/04 20:33:13 2018/04/04 20:33:13
C:\work   test_2.txt 2018/04/04 20:32:54 2018/04/04 20:32:54
# コンソールに表示されてない[Directory]、[CreationTime]、[LastAccessTime]を出力することができる!

Powershellでは、標準出力(に相当する「Standard output stream」)とコンソール上の表示は別物なのだ。
これが一番分かりやすいのが、[Write-Output]と[Write-Host]の両コマンドレットの比較。
PS C:\work> Write-Output "hoge"
hoge

PS C:\work> Write-Host "hoge"
hoge
# [Write-Output] も [Write-Host]も、コンソール上で実行するだけなら結果は同じ

PS C:\work> $a = Write-Output "hoge"
PS C:\work> $a
hoge
PS C:\work> Write-Output "C:\work\hoge" | Split-Path -Leaf
hoge
# [Write-Output]は[Standard output stream]への出力なので、
# 結果を変数に格納したりパイプで渡したりできるが…

PS C:\work> $b = Write-Host "hoge"
hoge
PS C:\work> $b
PS C:\work> Write-Host "C:\work\hoge" | Split-Path -Leaf
C:\work\hoge
# [Write-Host]はあくまでホスト(コンソール)に表示するだけなので、
# 結果を変数に格納したりパイプで渡したりできない!

パイプで渡されているものの正体は?

テキストベースのシェルに慣れていると、「パイプで何が渡されているのか分からない」
というのは気持ちが悪く感じるかもしれない(私は凄く気持ち悪かった)。

「オブジェクトが渡される」とはいうけど、具体的にはどんなものが渡されているのか。

これの確認には、[Get-Member]と[Select-Object *]を使用すると良い。
  ※単純に型名が欲しいだけなら、GetType()も便利
PS C:\work> $list = Get-Childitem
PS C:\work> $list[1]

    ディレクトリ: C:\work

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a----       2018/04/04     20:31             70 test.txt

PS C:\work> $list[1] | Get-Member

   TypeName: System.IO.FileInfo # 型名

Name                      MemberType     Definition # プロパティとメソッド

----                      ----------     ----------
LinkType                  CodeProperty   System.String LinkType{get=GetLinkType;}
Mode                      CodeProperty   System.String Mode{get=Mode;}
Target                    CodeProperty   System.Collections.Generic.IEnumerable`1[[System.String, mscorlib, Version=...
AppendText                Method         System.IO.StreamWriter AppendText()
CopyTo                    Method         System.IO.FileInfo CopyTo(string destFileName), System.IO.FileInfo CopyTo(s...
Create                    Method         System.IO.FileStream Create()
# …(中略)
Attributes                Property       System.IO.FileAttributes Attributes {get;
CreationTime              Property       datetime CreationTime {get;set;}
CreationTimeUtc           Property       datetime CreationTimeUtc {get;set;}
Directory                 Property       System.IO.DirectoryInfo Directory {get;}
# …(長いので以下省略)
# [Get-Member]は、オブジェクトの型とメソッド・プロパティをまとめて確認できる!

PS C:\work> $list[1] | Select-Object *

PSPath            : Microsoft.PowerShell.Core\FileSystem::C:\work\test.txt
PSParentPath      : Microsoft.PowerShell.Core\FileSystem::C:\work
PSChildName       : test.txt
PSDrive           : C
PSProvider        : Microsoft.PowerShell.Core\FileSystem
PSIsContainer     : False
Mode              : -a----
VersionInfo       : File:             C:\work\test.txt
                    InternalName:
                    OriginalFilename:
                    FileVersion:
                    FileDescription:
                    Product:
                    ProductVersion:
                    Debug:            False
                    Patched:          False
                    PreRelease:       False
                    PrivateBuild:     False
                    SpecialBuild:     False
                    Language:

BaseName          : test
Target            : {}
LinkType          :
Name              : test.txt
Length            : 70
DirectoryName     : C:\work
Directory         : C:\work
IsReadOnly        : False
Exists            : True
FullName          : C:\work\test.txt
Extension         : .txt
CreationTime      : 2018/04/04 20:30:48
CreationTimeUtc   : 2018/04/04 11:30:48
LastAccessTime    : 2018/04/04 20:30:48
LastAccessTimeUtc : 2018/04/04 11:30:48
LastWriteTime     : 2018/04/04 20:31:17
LastWriteTimeUtc  : 2018/04/04 11:31:17

Attributes        : Archive
# [Select-Object *]は、各プロパティの内容を確認できる!

型を意識しよう

単純な文字列や数値ではなくオブジェクトを扱う以上、
何よりも大切になってくるのが「型」。

Powershellは自動的にかなり自由な型変換を行ってくれるが、
やっぱりちゃんと型を意識しないと変なことになる。
PS C:\work> $array_1 = @("C:\work\hoge","C:\work\fuga","D:\work\piyo")
PS C:\work> $array_1
C:\work\hoge
C:\work\fuga
D:\work\piyo
PS C:\work> $array_1[1] | Split-Path -Leaf
fuga
# 配列$array_1にパス情報を格納し、2つ目の要素のパスの末端を表示
# 何も問題なし

PS C:\work> $array_2 = $array_1 | Select-String -Pattern "^C:"
PS C:\work> $array_2

C:\work\hoge
C:\work\fuga

PS C:\work> $array_2[1] | Split-Path -Leaf
InputStream
# $array_1からSelect-Stringで"C:"で始まる文字列だけを抽出し、$array_2とする
# 同じようにSplit-Path -Leafすると、InputStreamという謎の文字列が
さて、何が起こったのか。
実はこの[Select-String]コマンドレット、「文字列を検索する」という機能だけど
戻り値は文字列ではなく[MatchInfo]型。
  ※なんだそりゃ。ややこしい。
grepっぽいのは確かだけど、完全にgrep感覚で使うのは危険かも。

詳しく見て行くと次のような感じ。
PS C:\work> $array_1[1].GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     String                                   System.Object
# $array_1[1]は[String]型

PS C:\work> $array_2[1].GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     False    MatchInfo                                System.Object
# $array_2[1]は[MatchInfo]型になっている!!
# プロパティを確認してみよう。

PS C:\work> $array_2[1] | Select-Object *

IgnoreCase : True
LineNumber : 2
Line       : C:\work\fuga
Filename   : InputStream # 謎の文字列InputStreamの正体はこれだ!
Path       : InputStream
Pattern    : ^C:
Context    :
Matches    : {0}
# 余談だけど、MatchInfoのプロパティ FileNameとPathには、
# Select-Stringに引数渡ししたファイルの情報が格納される
# ファイルを渡してないと、このようにInputStreamという文字列が入る

PS C:\work> [String]$array_2[1] | Split-Path -Leaf
fuga
# 型不一致で上手く動かないのならキャストしてしまえ

PS C:\work> Split-Path $array_2[1] -Leaf
fuga
# 理由は分からないけど、[Split-Path]への渡し方がパイプではなく引数だと
# 自動的に[MatchInfo] → [String]に変換されるよう
# 変なの

まとめ

テキストベースのシェルに慣れている人がPowershellを触ると、
パイプラインの挙動の違いに気持ち悪くなって挫折してしまうケースが多い気がする。
でも、落ち着いてパイプの中身を確認していくようにすると、
すぐに使いこなせるようになる…かもしれない。

0 件のコメント :

コメントを投稿