しばたテックブログ

気分で書いている技術ブログです。

Select-Stringは具体的に何を検索しているのか?

PowerShellの基本的なことがわかってなかったシリーズ第5弾です。

PowerShell版のGrepともいえるSelect-Stringコマンドレットですが、ヘルプには、

文字列とファイル内のテキストを検索します。

としか記載されていません。

PowerShellでは基本オブジェクトを扱うのにもかかわらず"文字列"とだけ言われても何を指しているのかさっぱりわかりません。
そこで本エントリではこの"文字列"が具体的に何を指すのかを調べてみました。

調査した環境はWindows 8.1、PowerShell 4.0になります。
PowerShellのバージョンによっては結果が違うかもしれませんが多分同じか大差ないと思います。(未検証です。すいません。)

Select-Stringコマンドレットの実体

Select-Stringコマンドレットの実体はMicrosoft.PowerShell.Commands.Utility.dllにあるSelectStringCommandクラスになります。

ILSpy等でこのクラスの中身を見て具体的に何を検索しているのか調べることができるので、以降はその結果について書いていきます。

ファイルを検索する場合

Select-Stringコマンドレットは-Pathおよび-LiteralPathパラメータを指定してファイルを検索する場合と-InputObjectを指定してオブジェクトを検索する場合にパターン分けされます。
はじめにファイルを検索する場合について調べてみます。

この場合はわかりやすく、-Pathおよび-LiteralPathで指定されたファイル内部のテキストが検索対象になります。
-Encodingパラメーターで指定されたエンコーディング(デフォルトはUTF8)でファイルが開かれ中のテキストが1行ずつ検索されていきます。

ちなみに-Path-LiteralPathの違いはファイル名にワイルドカードを許可するかしないかになります。

オブジェクトを検索する場合

-InputObjectを指定した場合はオブジェクトが検索対象になります。
この場合は指定されるオブジェクトによって内部動作が変わってきます。

1. System.IO.FileInfoの場合

-InputObjectSystem.IO.FileInfoが来た場合は前項同様にファイルの中身を検索します。
FileInfoを特別扱いしている理由はあまりよくわかりません...

例として、あるディレクトリにHello.txtというファイルがあり、その中身が、

Hello.txt

Hello World.

な場合を想定します。

Get-ChildItem(ls)コマンドレットを実行した結果が

PS C:\temp\HelloDir> ls


    ディレクトリ: C:\temp\HelloDir


Mode                LastWriteTime     Length Name
----                -------------     ------ ----
-a---        2015/08/20     21:36         12 Hello.txt

な場合、戻り値はHello.txtに対するFileInfoクラスになります。

PS C:\temp\HelloDir> (ls).GetType().FullName
System.IO.FileInfo

PS C:\temp\HelloDir> (ls).FullName
C:\temp\HelloDir\Hello.txt

ここでこの結果に対してSelect-Stringを実行すると、

PS C:\temp\HelloDir> ls | Select-String "hello"

Hello.txt:1:Hello World.

となりHello.txtの中身を検索し1行目の"Hello"がヒットしていることがわかります。

ちなみに、ファイルの中身でなくHello.txtのファイル名を検索したい場合は、

PS C:\temp\HelloDir> ls | Out-String -Stream | Select-String "hello"

    ディレクトリ: C:\temp\HelloDir
-a---        2015/08/20     21:36         12 Hello.txt

の様に一度Out-String -Streamを通してやると良いです。

尚、System.IO.DirectoryInfoはディレクトリ名が検索対象になります。

PS C:\temp\HelloDir> (ls ..\ -Directory).GetType().FullName
System.IO.DirectoryInfo

PS C:\temp\HelloDir> ls ..\ -Directory | Select-String "hello"

HelloDir

2. Microsoft.PowerShell.Commands.MatchInfoの場合

Microsoft.PowerShell.Commands.MatchInfoSelect-Stringの戻り値の型です。
検索されてヒットされた結果を格納するオブジェクトになります。

このクラスを特別扱いしているのはSelect-Stringされた結果をさらにパイプラインでつないだ場合を想定しているのだと思われます。

-InputObjectMatchInfoが来た場合はMatchInfo.Lineプロパティ(String型)の内容が検索対象になります。

先ほどの例を使うと、

PS C:\temp\HelloDir> ls | Select-String "hello"

Hello.txt:1:Hello World.

PS C:\temp\HelloDir> (ls | Select-String "hello").GetType().FullName
Microsoft.PowerShell.Commands.MatchInfo

PS C:\temp\HelloDir> (ls | Select-String "hello").Line
Hello World.

となり、MatchInfo.Lineプロパティは"Hello World."であることがわかります。
これに対して、

PS C:\temp\HelloDir> ls | Select-String "hello" | Select-String "txt"
PS C:\temp\HelloDir>

Select-String "txt"でパイプラインを繋げた場合、コンソールには"Hello.txt"が表示されいるもののMatchInfo.Lineには"txt"は含まれていないため何もヒットしません。
Select-String "world"であればヒットします。

PS C:\temp\HelloDir> ls | Select-String "hello" | Select-String "world"

Hello.txt:1:Hello World.

ちなみに先ほどと同様にOut-String -Streamを通した場合はSelect-String "txt"で検索がヒットします。

PS C:\temp\HelloDir> ls | Select-String "hello" | Out-String -Stream | Select-String "txt"

Hello.txt:1:Hello World.

3. その他の型の場合

-InputObjectにその他の型のオブジェクトが来た場合はその型をSystem.Management.Automation.LanguagePrimitives.ConvertTo()メソッドでString型に変換した結果が検索対象になります。

名前から察するにこのメソッドはPowerShellの型変換機構そのものだと思われます。なので-InputObjectにその他の型のオブジェクトが来た場合はPowerShellの型変換機構によってString型に変換された結果が検索対象になるといっても良いと思います。

流石にこのSystem.Management.Automation.LanguagePrimitives.ConvertTo()メソッドの詳細を読み解くことはできませんでした...
ただ、いろいろなクラスで挙動を見た限り、おおむねToString()メソッドの結果と近いのかなぁと思いました。

これまでの例でGet-ChildItem(ls)した結果をFormat-List(fl)した場合、以下の様な結果になります。

PS C:\temp\HelloDir> ls | fl


    ディレクトリ: C:\temp\HelloDir



Name           : Hello.txt
Length         : 12
CreationTime   : 2015/08/20 21:36:47
LastWriteTime  : 2015/08/20 21:36:59
LastAccessTime : 2015/08/20 21:36:47
VersionInfo    : File:             C:\temp\HelloDir\Hello.txt
                 InternalName:
                 OriginalFilename:
                 FileVersion:
                 FileDescription:
                 Product:
                 ProductVersion:
                 Debug:            False
                 Patched:          False
                 PreRelease:       False
                 PrivateBuild:     False
                 SpecialBuild:     False
                 Language:

ls | flした結果、コンソールにはフォーマットされた内容が表示されますが、戻り値はMicrosoft.PowerShell.Commands.Internal.Format.ほげほげなクラスの配列になります。

PS C:\temp\HelloDir> ls | fl | select { $_.GetType().FullName }

 $_.GetType().FullName
-----------------------
Microsoft.PowerShell.Commands.Internal.Format.FormatStartData
Microsoft.PowerShell.Commands.Internal.Format.GroupStartData
Microsoft.PowerShell.Commands.Internal.Format.FormatEntryData
Microsoft.PowerShell.Commands.Internal.Format.GroupEndData
Microsoft.PowerShell.Commands.Internal.Format.FormatEndData

このため単純にSelect-Stringを実行しても何もヒットしません。

PS C:\temp\HelloDir> ls | fl | Select-String "hello"
PS C:\temp\HelloDir>

ここでなんでもありありな検索条件.*を渡してやると

PS C:\temp\HelloDir> ls | fl | Select-String ".*"

Microsoft.PowerShell.Commands.Internal.Format.FormatStartData
Microsoft.PowerShell.Commands.Internal.Format.GroupStartData
Microsoft.PowerShell.Commands.Internal.Format.FormatEntryData
Microsoft.PowerShell.Commands.Internal.Format.GroupEndData
Microsoft.PowerShell.Commands.Internal.Format.FormatEndData

となり、文字列型に変換(≒ToString())された後の文字列が検索対象となっていることがわかります。

PS C:\temp\HelloDir> ls | fl | select { $_.ToString() }

 $_.ToString()
---------------
Microsoft.PowerShell.Commands.Internal.Format.FormatStartData
Microsoft.PowerShell.Commands.Internal.Format.GroupStartData
Microsoft.PowerShell.Commands.Internal.Format.FormatEntryData
Microsoft.PowerShell.Commands.Internal.Format.GroupEndData
Microsoft.PowerShell.Commands.Internal.Format.FormatEndData

また、これまでと同様にOut-String -Streamを通してやるといい感じになってくれます。

PS C:\temp\HelloDir> ls | fl | Out-String -Stream | Select-String "hello"

    ディレクトリ: C:\temp\HelloDir
Name           : Hello.txt
VersionInfo    : File:             C:\temp\HelloDir\Hello.txt

追記

2015/08/25追記

以下の補足記事を書きましたのでこちらも併せてご覧ください。

stknohg.hatenablog.jp