たいちょーの雑記

ぼくが3日に一度くらい雑記をかくところ

ワンライナー中にC#を書きたかったのでocsというコマンドを作った

作りました

github.com

経緯

自分はC#が好きなので、シェルでワンライナーを描いているときもふと、C#が使いたくなります。そういう時はMonoのREPLを使ったりしていたんですが、各行に対してC#を適応したいとき、書くコードが増えてしまうのでイマイチでした。

$ seq 10 | csharp -e 'string s;while((s=Console.ReadLine())!=null)Console.WriteLine(int.Parse(s) * 10)'

こんなの書いているうちに日が暮れてしまうので、大体awkとかですますのですが、やはりワンライナー中にC#を書きたいんですよね。ええ。え!?PowerShell!?なにそれ!!!

というわけでawkっぽく使えるC#ラッパーとしてocsを作りました。アイデアrbopyと同じです。

インストール

dotnetがインストールされているならクローンしてdotnet publishしてもいいですが、ビルド済みのバイナリをReleaseにアップロードしているのでそれをダウンロードするのがいいかと思います

それでもビルドしたい人

$ git clone https://github.com/xztaityozx/ocs
$ cd ocs
# win向け、linuxならlinux-x64, macOSならosx-x64
$ dotnet publish --self-contained true -c Release -r win-x64 -p:PublishSingleFile=true -p:PublishReadyToRun=true -o ./bin

$ ./bin/ocs --help #バイナリはこれ

zinitで管理する

zinit ice from"gh-r" as"program" pick"*/ocs"
zinit light xztaityozx/ocs

使い方

まぁawkと似た感じです

echo a b c | ocs '{println(F[1], F[2])}'
a b

ブロックに対するパターンも使えます

seq 10 | ocs 'int.Parse(F0)%2==0{ println(F0) }'
2
4
6
8
10

本当はパターンだけを書けるようにしたかったんですけど、パーサーを書くのに疲れてしまいました。Issueにはするので、そのうち追加するかもしれないです。

# こうしたかった
seq 10 | ocs 'int.Parse(F0)%2==0'
2
4
6
8
10

パターンにはboolを返す式であればどんな式でも書けます(パーサーが壊れてなければですが)

$ seq 10 | awk '{print $1%2 ? "" : $1}' | ocs 'F0.Any(){println(F0)}'
2
4
6
8
10

BEGINENDも使えます

seq 100 | ocs 'BEGIN{var sum=0}{sum+=i(F0)}END{println(sum)}'
5050

usingしたいなら-I, --importsを使います。

ocs -ISystem.IO '{ ... }'
ocs -ISystem.IO,System.Text '{ ... }'

フィールドセパレータも指定できます。

$ cat csv | ocs -F, '{...}'

# 正規表現もいける
$ echo 1abc2ebc3dbc | ocs -F'.bc' '{println((F[1], F[2], F[3]))}'
(1, 2, 3)

技術的な話

今回はC#コードの実行にRoslynのScripting APIを使いました

github.com

ocsがやっていることはC#内でC#のソースを生成して、をコンパイルして、実行している。だけですね。生成されたコードを出力するオプションも用意しているので、試しにocsが生成するコードを見てみます

$ seq 100 | ocs --show 'BEGIN{var sum=0}{sum+=i(F0)}END{println(sum)}'
[ 14:36:14 | Information ] Generated Code
var sum=0;
using(Reader) while(Reader.Peek() > 0) {
F0 = Reader.ReadLine();
sum+=i(F0);
}
println(sum);

5050

こんな感じです。内部で処理するだけなのでインデントもくそもないですが、読みやすくするとこうですね

var sum=0;
using(Reader) while(Reader.Peek() > 0) {
  F0 = Reader.ReadLine();
  sum+=i(F0);
}
println(sum);

それぞれwhileの前か後か中かにコードが展開されるだけですね。F0やReaderなんかは予約されている変数です。この辺はREADMEを読んでもらうことにします。

パターン付きのアクションはどうなるんでしょう。

$ seq 100 | ocs --show 'BEGIN{int a,b}{a+=i(F0)}F0.Length==2{b+=i(F0)}END{println((a,b))}'
[ 14:39:12 | Information ] Generated Code
int a,b;
using(Reader) while(Reader.Peek() > 0) {
F0 = Reader.ReadLine();
a+=i(F0);
if(F0.Length==2){b+=i(F0);};
}
println((a,b));

(5050, 4905)
int a,b;
using(Reader) while(Reader.Peek() > 0) {
  F0 = Reader.ReadLine();
  a+=i(F0);
  if(F0.Length==2){b+=i(F0);};
}
println((a,b));

こうですね。パターンを条件としたifが生成されます。とっても単純ですね。

おわり

とりあえずやりたかったことはできたのでうれしいです。ひとつ難点として挙げられるのはバイナリサイズがめちゃめちゃデカいことですね。Releaseにアップロードしているものは40MBぐらいなのでwgetするのに若干時間がかかるんですよね。まぁ仕方ないんですが・・・。バイナリサイズを落とすコンパイルとかも試したんですが、ocsが動かなくなるのであきらめました。悲しいのだ

まぁ良かったら使ってやってください