tech.guitarrapc.cóm

Technical updates

PowerShell の Pipeline と C# の LINQ の簡単な比較とかなんとか

てきとーです。ふぇぇ、まさかりこわいです。

PowerShell といえば Pipeline 処理。 C# なら LINQ がわかりやすい対比になると思ったのでてきとーな記事をでっち上げてみます。

今回は文字列(string) を題材にします。

わかりやすい例 : フィルタ

とりあえず Where ですよねー。

PowerShell

Pipeline 処理といえばてけとーにプロセス取得してフィルタしてとか

Get-Process | where Name -eq "powershell"

あるいは、v4からの .Whereオペレータを使って Pipeline使わずつなげると

[System.Diagnostics.Process]::GetProcesses().Where{$_.Name -eq "powershell"}

で、こう

Handles NPM(K) PM(K) WS(K) VM(M) CPU(s)   Id ProcessName
------- ------ ----- ----- ----- ------   -- -----------
    429     38 67576 78732   610   5.31 7576 powershell
LINQ

簡単に LINQ でも 書けますよね。

System.Diagnostics.Process.GetProcesses().Where(x => x.ProcessName == "powershell");

あとはてきとーなプロパティを表示すればいいでしょう。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApplication3
{
    class Program
    {
        static void Main(string[] args)
        {
            System.Diagnostics.Process.GetProcesses()
                .Where(x => x.ProcessName == "powershell")
                .Select(x => x.ProcessName)
                .ToList()
                .ForEach(Console.WriteLine);

            Console.ReadLine();
        }
    }
}

Redis の info 結果で試す

あまりやりたくないのですが、string を同様に処理してみましょう。

たとえば、RespClient を使って、テキトーにサーバーから info コマンドの結果を取得します。

Connect-RedisServer
Send-RedisCommand -Command "info"
Disconnect-RedisServer

結果は、このような string です。

はい、string[] ではなく string です。行ごとに分かれてませんからねー。ほぇぇ。

# Server
redis_version:2.8.13
redis_git_sha1:00000000
redis_git_dirty:0
redis_mode:standalone
os:Linux 3.10.35-43.137.amzn1.x86_64 x86_64
arch_bits:64
multiplexing_api:epoll
gcc_version:4.8.2
process_id:6490
run_id:0023285cc87f5b4f686ec0b438d8e3f884e4e77c
tcp_port:6379
uptime_in_seconds:9455054
uptime_in_days:109
hz:10
lru_clock:458978

# Clients
connected_clients:0
client_longest_output_list:0
client_biggest_input_buf:0
blocked_clients:0

# Memory
used_memory:878480
used_memory_human:857.89K
used_memory_rss:7557120
used_memory_peak:935456
used_memory_peak_human:913.53K
used_memory_lua:31744
mem_fragmentation_ratio:8.60
mem_allocator:jemalloc-3.2.0

# Persistence
loading:0
rdb_changes_since_last_save:7025
rdb_bgsave_in_progress:0
rdb_last_save_time:1409681336
rdb_last_bgsave_status:ok
rdb_last_bgsave_time_sec:0
rdb_current_bgsave_time_sec:-1
aof_enabled:0
aof_rewrite_in_progress:0
aof_rewrite_scheduled:0
aof_last_rewrite_time_sec:-1
aof_current_rewrite_time_sec:-1
aof_last_bgrewrite_status:ok

# Stats
total_connections_received:82988
total_commands_processed:674668037
instantaneous_ops_per_sec:16
rejected_connections:0
expired_keys:0
evicted_keys:0
keyspace_hits:0
keyspace_misses:0
pubsub_channels:0
pubsub_patterns:0
latest_fork_usec:656

# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:0
master_sync_in_progress:0
slave_priority:100
slave_read_only:1
connected_slaves:0

# CPU
used_cpu_sys:7664.74
used_cpu_user:4442.53
used_cpu_sys_children:0.00
used_cpu_user_children:0.00

# Keyspace
db0:keys=16,expires=0

このままだと Linuxで シェルを使うならいいのですが(よくない)、PowerShell や C# からは使いにくいです。

そこで、オブジェクトにしてみましょう。

最終的には、このような形で出力されることを目指します。

Name                         Value
----                         -----
redis_version                2.8.13
redis_git_sha1               00000000
redis_git_dirty              0
redis_mode                   standalone
os                           Linux 3.10.35-43.137.amzn1.x86_64 x86_64
arch_bits                    64
multiplexing_api             epoll
gcc_version                  4.8.2
process_id                   6490
run_id                       0023285cc87f5b4f686ec0b438d8e3f884e4e77c
tcp_port                     6379
uptime_in_seconds            9457077
uptime_in_days               109
hz                           10
lru_clock                    459181
connected_clients            2
client_longest_output_list   0
client_biggest_input_buf     0
blocked_clients              0
used_memory                  878480
used_memory_human            857.89K
used_memory_rss              7553024
used_memory_peak             935456
used_memory_peak_human       913.53K
used_memory_lua              31744
mem_fragmentation_ratio      8.60
mem_allocator                jemalloc-3.2.0
loading                      0
rdb_changes_since_last_save  4495
rdb_bgsave_in_progress       0
rdb_last_save_time           1409683443
rdb_last_bgsave_status       ok
rdb_last_bgsave_time_sec     0
rdb_current_bgsave_time_sec  -1
aof_enabled                  0
aof_rewrite_in_progress      0
aof_rewrite_scheduled        0
aof_last_rewrite_time_sec    -1
aof_current_rewrite_time_sec -1
aof_last_bgrewrite_status    ok
total_connections_received   83118
total_commands_processed     674712373
instantaneous_ops_per_sec    27
rejected_connections         0
expired_keys                 0
evicted_keys                 0
keyspace_hits                0
keyspace_misses              0
pubsub_channels              0
pubsub_patterns              0
latest_fork_usec             686
role                         slave
master_host                  127.0.0.1
master_link_status           up
master_last_io_seconds_ago   0
master_sync_in_progress      0
slave_priority               100
slave_read_only              1
connected_slaves             0
used_cpu_sys                 7664.74
used_cpu_user                4444.35
used_cpu_sys_children        0.00
used_cpu_user_children       0.00
db0                          {(keys, 16), (expires, 0)}
文字列のデリミタを探す

返ってみたデータを見ると、 infoの結果は、表題が # から始まり、区分、データは : で区切られています。

# Replication
role:slave

さらに、データの中身に複数の結果がある場合は、 , でデータは区切られて、KVは =で区切られています。

# Keyspace
db0:keys=16,expires=0

ということで、デリミタは確定しました。

PowerShell

さくっと書いてみましょう。HashTable使ってるのは見逃してください。

function ParseInfo ([object]$source)
{
    $source -split "`r`n" `
    | where {$_.contains(':')} `
    | %{
        $split = $_.Split(':')
        $value = $split[1].split(',')
        @{
            $split[0] = if ($value.length -eq 1){ $split[1] }
                else
                {
                $value `
                | %{
                    $inside = $_.('=')
                    [Tuple]::Create($inside[0], $inside[1])
                }
            }
        }
    }
}

結果は、先ほどの通りです。

LINQ

先ほどのPowerShell のパイプライン処理を LINQ でやってみます。

public Dictionary<string, object> ParseInfo(object source)
{
    var item = source.ToString().Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries);

    var dic = item
    .Where(x => x.Contains(':'))
    .Select(x => x.Split(':'))
    .ToDictionary(kv => kv[0], kv =>
    {
        var split = kv[1].Split(',');
        if (split.Length == 1) return (object)split[0];
        return split.Select(x => x.Split('='))
            .Select(xs => Tuple.Create(xs[0], xs[1]))
            .ToArray();
        });
        return dic;
    }
}
なにができるようになったの

ちなみに、文字列をこのように変換したことで、RespClientで次のような書き方ができるようになります。わーい。

Connect-RedisServer
Get-RedisInfo | where Key -eq db0 | select -ExpandProperty Value

まとめ

なんとなく、 LINQ で書いてたのが PowerShell だとこう、あるいはその逆。という例になれば幸いです。

LINQ 便利メソッド多い以上に、ラムダ式が書きやすくていいですよね。PowerShell には、匿名コンストラクタである ScriptBlock がありますが、アロー演算子がなかったりと書きにくいのです。

PowerShell は、何気に動的に型変換かけたりしているのは意図的です。

おまけ

LINQ での書き方は、neuecctanaka_733 に教えてもらったりして幸せでした。はい。ありがとうございました!