第67回シェル芸勉強会に参加しました
参加しました。今回も参加記として解きなおしを残しておこうと思います。
PowerShell解も余裕があればやります。
当日の配信はこちらです。
Q1
shogi
ファイルは以下のような内容です
香桂銀金王金銀桂香 飛 角 歩 歩歩歩歩歩歩歩 歩 歩 歩 歩 歩歩歩歩歩歩 角 銀 飛 香桂 金玉金銀桂香
このファイルの2行目の飛が動ける範囲に×
を、8行目の角が動ける範囲に〇
を書く問題です。sed
のリハビリ問題とのこと。
$ cat shogi | sed '2s/ /×/g;2s/.$/ /;3s/ /×/' | sed '7~2s/ /〇/g;s/$/ /' | awk -F '' '{if(4<=NR&&NR<=6)$(10-NR)="〇"; print}' OFS="" 香桂銀金王金銀桂香 ×飛×××××角 歩×歩歩歩歩歩歩歩 歩 〇 〇 歩 歩〇 〇歩〇歩歩歩歩歩歩 角 銀 飛 香桂〇金玉金銀桂香
sed
とawk
で〇になりうるところ決め打ちで書き換えています。ほとんど解答例として解説されたものと同じですね。
PowerShell解もやってみます。
$ cat .\shogi |%{ if($idx -eq 2 -or $idx -eq 3) { $_ -replace ' $','' -replace ' ','×' } else { $_ };$idx++} -Begin {$idx=1} |%{if($idx -eq 7 -or $idx -eq 9) { $_ -replace ' ','〇' } else { $_ }} | %{ $_ -replace '$',' '} | % { if(4 -le $idx -and $idx -le 6) { $hoge="$_".ToCharArray();$hoge[9-$idx]="〇"; $_=$hoge -join '' }; $_ }
なが~い。方針は同じなので解説はしません!
Q2
32ビット符号付整数の上限値をggらずにどうやって知るか?という問題。いろいろありそうですね。
$ echo 'typemax(Int32)' | julia 2147483647
はい。数値系の問題とかは最近juliaを使うようになりました。便利!
PowerShell解は以下の通りですね
$ [System.Int32]::MaxValue 2147483647
やはり型があると便利な場面はありますね。
Q3
hoge.c
は以下のような内容です。
#include <stdio.h> #include <limits.h> #include <stdlib.h> int main(){ printf("%d", INT_MAX); exit(0); }
このファイルをコピペしただけではコンパイルできないようにしてください。という問題。学校の先生も大変だ。
cat hoge.c | sed 's/$/'"$(echo 💩| zws|tr -d \')"/
zwsを使って💩をゼロ幅スペースにエンコードし、それらを各行の末尾に追加しました。便利なコマンドですねzws
。
Q4
grep a / -R &
が実行されたとたん、コマンドの実行を止めるようにシェルなどに細工をする問題。想定解はstty tostop
のようです。知らんかった。
さて自分の解答は以下の通りです。
$ function a() { [[ "$1" == "grep a / -R &" ]] && kill -9 $$ } $ add-zsh-hook preexec a
該当のコマンドが実行されたとき、そのシェルごとkill
するフックをシェルに仕込みました。ヤバイ
Q5
PID=1な topコマンドを作る問題。
$ sudo unshare --fork --pid --mount-proc top
昔やった問題の応用ですね。docker run --rm ubuntu top
とかでもよいと思います
Q6
端末のプロンプト部分をファイルに書き出す問題。つまり、$PS1
をファイルに書き出すってことのようです。むずかしそう
# bash >= 4.4 $ echo ${PS1@P}
BashのParameter Expansionの機能で再現できました。なんでもあるもんだなあ
ちなみに zsh
にもありました
$ echo ${(%)PS1}
そしてPowerShellではprompt
で出力できますね
$ prompt
Q7
ps
コマンドのVSZかRSSで並び替えたとき、COMMAND
の列にメッセージが出るように細工をする問題。むずかしそう
$ echo うんこ | grep -o . | nl | shuf | awk '{print "awk -F "$2" n="1000**($1+3)" &"}' > a $ source a $ ps u | sort -nk6,6 USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND xztaity+ 20149 0.0 0.0 8756 2812 pts/4 TN 15:46 0:00 awk -F う n=1000000000000 xztaity+ 20150 0.0 0.0 8756 2816 pts/4 TN 15:46 0:00 awk -F ん n=1000000000000000 xztaity+ 20151 0.0 0.0 8756 3040 pts/4 TN 15:46 0:00 awk -F こ n=1000000000000000000
ほかの環境でも再現性があるかはわからないんですが、大きな数値を代入するawk
コードをを実行することでRSSを使わせ、メッセージを仕込むことができました。ほかの文字列でもできるかは謎です
終わり
stty tostop
やプロンプトの展開など、全然知らないことを知れたのが嬉しかったです。
今回も企画・開催ありがとうございました!
| ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄| | 終 | | 制作・著作 | |  ̄ ̄ ̄ ̄ ̄ ̄ ̄ | | xztaityozx | |_________| ∧∧ || ( ゚д゚)|| / づΦ
sel の csv/tsv オプションと template オプションの話
sel
コマンドを知っていますか?カラムを取り出すことに注目したコマンドで、cut
とawk
のちょうど間ぐらいの使い心地を目指して私がちまちま作っているものですね。
他の似たツールとして self や choose があります。
基本的な使い方
sel
はカラムを選択するだけのプログラムです。カラムは1始まりの数値で選択します。0は行全体を表します。
$ cat ./file 1 2 3 4 5 6 a b c d e f # 1 ~ 3カラム目を取り出す $ cat ./file | sel 1 2 3 1 2 3 a b c # 0は行全体 $ cat ./file | sel 0 1 2 3 4 5 6 a b c d e f
負のインデックスを指定することで「後ろから何番目」を表すこともできます。
# 後ろから1番目のカラムを取り出す $ cat ./file | sel -- -1 6 f
また、単一のカラムだけでなく、範囲指定することができます。(これがこのコマンドで一番やりたかったことでもあります)
# 1 ~ 3カラム目を取り出す $ cat ./file | sel 1:3 1 2 3 a b c
基本的な使い方は以上です。もっと詳しく使い方を説明したいところですが、それはREADMEを読んでいただくこととし、この記事では最近追加した新機能2種について書こうと思います。
csv/tsv オプション
sel v1.1.9でちゃんとしたCSVとTSVがサポートされました。
今までも入力文字列のデリミターを-d, --input-delimiter
オプションを通じて変更できたので、-d,
とすれば使えたのですが hoge,"fuga,piyo",bar
というようなデータをちゃんと処理できない状態でした。
# CSVの2カラム目としては `fuga,piyo` が正しいが、`-d,`では `"fuga` になってしまう $ echo 'hoge,"fuga,piyo",bar' | sel -d, 2 "fuga
これに対応するために、awk
のFPAT
のようなものを実装しようと考えましたが、面倒だしバグらせる自信があるし、なによりFPAT
は気軽に書けないので Goのencoding/csv
パッケージを使って入力を読み取るオプションを搭載することにしました。それが--csv
オプションです。
$ echo 'hoge,"fuga,piyo",bar' | sel --csv 2 fuga,piyo
ついでにTSVもサポートしました。--tsv
オプションを通じて利用することができます。
ところで、最近awk
でもcsv
がサポートされました。便利ですね。
template オプション
sel v1.1.10で、出力文字列のフォーマットを指定できる -t, --template
オプションを追加しました。
いままではシンプルに選択されたカラムが出力されるだけでしたが、--template
オプションの追加で以下のような出力ができるようになりました。
$ echo AAA BBB CCC | sel --template '1: {} 2: {} 3: {}' 1:3 1: AAA 2: BBB 3: CCC
テンプレート内にプレースホルダー{}
を書くと、そこへ選択したカラムを順番に割り当てることができます。それだけです。awk
のように数値計算したりはできません。
正直この機能を実装するかどうか結構悩みました。機能過多だと思ったからです。機能が増えるとそれだけメンテするコストが上がってしまうので面倒です。
でも結局実装しました。便利だし、モチベであるcut
とawk
のちょうど間ぐらいの機能として違和感はなかったからです。
まとめ
今回は最近sel
に追加したcsv/tsv
オプションとtemplate
オプションについて書きました。
個人的に結構気に入ってるコマンドなので、ぜひ使ってみて貰えると嬉しいです。Goがインストールされていれば go install
を通じてインストールできます。Goがない場合でも GitHub Releasesからお好みのバイナリをダウンロードすることができます。
# install
$ go install github.com/xztaityozx/sel@latest
以上です。
| ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄| | 終 | | 制作・著作 | |  ̄ ̄ ̄ ̄ ̄ ̄ ̄ | | xztaityozx | |_________| ∧∧ || ( ゚д゚)|| / づΦ
付録
- https://www.slideshare.net/xztaityozx/ocssel
- はじめて話したときのスライド
self
という先行研究があることを知りその場で膝から崩れ落ちた
- https://www.slideshare.net/xztaityozx/sel-250560885
- 2回目の挑戦
- 先行研究を研究し、速度と機能の面から攻めたときの話
SourceGeneratorで自作例外クラスのコンストラクタを自動生成してみる
自作の例外クラス書いてますか?自分は時々書いてます。
で、雑に以下のように書きますよね。
public class FailedToHogeException : Exception { public FailedToHogeException(string message) : base(message) {} }
するとSonarLintなどからこんな感じの警告もらいませんか?自分はもらいます。
これってちゃんと実装しようね!ってことなだけなんですけど、結構実装量多い…ような気がするので自動生成されると嬉しいですね。というわけでSourceGeneratorの素振りの題材として作ってみます。
実装したやつ
実装したものは以下のリポジトリにあります。
以下のように自動生成してもらいたい例外クラスに SerializableException
属性を付けるだけで
namespace Example; [SerializableException] public partial class FailedToHogeException : Exception { }
次のようなファイルを生成してくれます
// <auto-generated> // THIS FILE IS GENERATED BY SerializableExceptionGenerator. DO NOT EDIT IT. // </auto-generated> namespace Example; [global::System.Serializable] public partial class FailedToHogeException : global::System.Exception { public FailedToHogeException() { } public FailedToHogeException(string message) : base(message) { } public FailedToHogeException(string message, global::System.Exception inner) : base(message, inner) { } protected FailedToHogeException( global::System.Runtime.Serialization.SerializationInfo info, global::System.Runtime.Serialization.StreamingContext context ) : base(info, context) { } }
引数にオプション値を渡すことで、生成したいコンストラクタを自由に選ぶこともできます。例えば以下のように GenerateTarget.StringMessageExceptionConstructor
だけ指定すると
using SerializableExceptionGenerator; namespace Example; [SerializableException(GenerateTarget.StringMessageExceptionConstructor)] public partial class FailedToException : Exception { }
次のようなものが生成されます。
// <auto-generated> // THIS FILE IS GENERATED BY SerializableExceptionGenerator. DO NOT EDIT IT. // </auto-generated> namespace Example; [global::System.Serializable] public partial class FailedToException : global::System.Exception { public FailedToException(string message, global::System.Exception inner) : base(message, inner) { } }
自前実装した残りは自動実装してくれよな!ってときに便利かもしれません
実現方法
最初に言った通りSourceGeneratorを使っています。SerializableException
属性を探して実装を出力するIIncrementalGenerator
を書いたって感じです。
using System.Text; using Microsoft.CodeAnalysis; namespace SerializableExceptionGenerator; [Generator(LanguageNames.CSharp)] public class SerializableExceptionGenerator : IIncrementalGenerator { // 名前は結構繰り返し使うので変数に入れとくと楽だったり楽じゃなかったりする private const string GeneratorAttributeName = "SerializableExceptionAttribute"; private const string GeneratorNamespace = "SerializableExceptionGenerator"; public void Initialize(IncrementalGeneratorInitializationContext context) { var source = context.SyntaxProvider.ForAttributeWithMetadataName( $"{GeneratorNamespace}.{GeneratorAttributeName}", static (_, _) => true, static (context, _) => context // ここでGenerateTargetの値とれそうなきがしなくもない ); // コード生成は別のメソッドにしてる context.RegisterSourceOutput(source, Emit); context.RegisterPostInitializationOutput(static postInitializationContext => { // 引数用のEnumってどこに実装するのがいいんだろう // 生成するコードに書くと、二重管理になってめんどくさい気がするんだよな… postInitializationContext.AddSource( $"{GeneratorAttributeName}.cs", $$""" // <auto-generated> // THIS FILE IS GENERATED BY SerializableExceptionGenerator. DO NOT EDIT IT. // </auto-generated> namespace {{GeneratorNamespace}} { [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = false, Inherited = false)] internal sealed class {{GeneratorAttributeName}} : global::System.Attribute { public GenerateTarget Target { get; set; } public {{GeneratorAttributeName}}(GenerateTarget target = GenerateTarget.All) { this.Target = target; } } [global::System.Flags] internal enum GenerateTarget { None = 0, DefaultConstructor = 1, StringMessageConstructor = 1 << 1, StringMessageExceptionConstructor = 1 << 2, SerializationInfoStreamingContextConstructor = 1 << 3, All = DefaultConstructor | StringMessageConstructor | StringMessageExceptionConstructor | SerializationInfoStreamingContextConstructor } } """ ); }); } private static void Emit(SourceProductionContext context, GeneratorAttributeSyntaxContext source) { <省略だよ> } // さっきも書いたけど生成コード中に同じものがあるのつらいお気持ちがなくはない [Flags] internal enum GenerateTarget { <省略だよ> } }
コメント内にも書いていますが、オプション値として使うenum
の定義が2か所に生えてしまうのをどうにかしたいなというお気持ちです。なんかいいアイデアがあれば教えてください。
まとめ
自作例外クラスを書くときによく見るRSPEC-3925に対応できるSourceGeneratorが欲しかった
なので指定したコンストラクタを自動生成してくれるSourceGeneratorを書いた
オプションで生成するコンストラクタを制御できるようにした
正直テンプレートでもいいので使うかは謎…ですね。Copilotが勝手に埋めてくれたりもしますしね…。
とはいえ良いSourceGeneratorの素振りにはなりました。皆さんも何か書いてみてはいかがでしょうか。
| ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄| | 終 | | 制作・著作 | |  ̄ ̄ ̄ ̄ ̄ ̄ ̄ | | xztaityozx | |_________| ∧∧ || ( ゚д゚)|| / づΦ
第65回シェル芸勉強会に参加しました
参加しました。今回は自宅からリモート参戦でした。
新生シェル芸Botがめちゃいい感じでしたね。整備ありがとうございます!!
この参加記では時間中の自分の解答と、解きなおしのPowerShell解をしたりしなかったりします。
Q1
以下のようなinput
ファイルがあります
a bb ccc dddd eeee fff gg h ii
このファイルから次のような出力を得るという問題
a bb a ccc a dddd eeee fff eeee gg h ii
ヤバそうな気がしますねえ。
cat Sh*/vol.65/input | stairl | awk 'NF!=1{print $1,$NF}'
stairl
はegzactのコマンドです。
stairl
を使うと以下のようになります
$ cat Sh*/vol.65/input | stairl a a bb a bb ccc a bb ccc dddd eeee eeee fff eeee fff gg h h ii
目的の出力得るためには、1列以上ある行について、各行の先頭と最後を選択すればよさそうです。なのであとはawk
でエイヤという感じにしました。
ところで、問題の制約としてtarr
などのツールやawk
のfor
を使わずにというのがあったのでそれにも挑戦してみます。
$ sel 1 2 1::2 1::3 -f ./Sh*/vol.65/input | \ juz 3 sed 's/ /\n/2' | \ awk 'NF!=1&&$1!=$2'
sel
の[n]:[m]:[k]
を使うと[n]
から[m]
まで[k]
個ごとに出力するクエリです。なので1::2
は1列目から2列ごとに出力するものです。この場合は以下のような出力になります
a bb a ccc a dddd eeee fff eeee gg eeee h ii h h
後は2個ずつ改行して必要なデータだけ取り出して終わりです。sed
は何回か繰り返す必要があるのですが、これにはjuzを使いました。便利です。
PowerShell解は↑で疲れたのでスキップです
Q2
input
内容を以下のように加工する問題
a dddd bb dddd ccc dddd eeee gg fff gg h ii
最後の列を毎列ごとに差し込むって感じですね。Q1で出た解答を参考にした感じです。解説は配信をご覧ください。
cat Sh*/vol.65/input | \ awk 'gsub(FS, FS$(NF)FS)' | \ sel :-2
こっちはPowerShell解を書きます
$ cat She*/*.65/input | %{ $x=($_ -split ' '); $_ -replace ' ',(" " + $x[-1] + " ") } | sel -- :-2 a dddd bb dddd ccc dddd eeee gg fff gg h ii
手法自体は同じです。書き方が違うというだけですね。awk
に慣れてると、ForEach-Object
では毎回split
しないといけないことがめんどくさく感じちゃいますね。なんかいい方法あったりするんでしょうか。
Q3
eeeeee
ファイルの内容をランレングス圧縮する問題です。
cat Sh*/vol.65/eeeeee | grep -oP '(.)\1*' | awk '$0=$1NF' FS= ORS=
grep
で文字ごとに行に分割します。あとはawk
で数えつつ連結するって感じです。awk
の部分はTLを見てて賢いな~と思ったのを参考にしています。
ところでjulia
にはランレングスするrle()
という関数があるようです。これでやってみます
cat ./Sh*/vol.65/e* | \ sel -d '' 0 | \ julia -E 'using StatsBase;rle(split(readline()))' | \ sel -d'[' 2: | \ tr '\]\)' \\n | \ tr -d ,\" | \ rs -T | \ tr -d \ \\n
出力の整形を結構頑張る必要がありますが、ロジックを自前実装する必要がないのがいいですね。
Q4
symbols
ファイルは以下のような内容です
a-> c-) g~> >-z-> )-d-) >~y~> )-e >-f >~i~> >~j
これを矢印の種類でまとめてくださいという問題。時間内に解けなかったので解説を参考にした解答を張っておきます。
cat Sh*/vol.65/sy* | \ sed -r 's/^([a-z])(.)(.)/\3\2\1\2\3/;s/../& /' | \ awk '{a[$1]=a[$1]$0}END{for(x in a) print a[x]}' | \ sel -D '' 2:
PowerShell解は以下です
$ cat She*/*.65/sy* | %{ @{k=$_ -replace '^[a-z](.)(.)','$2$1' -replace '^(..).+','$1';v=$_} } | group -Property k | %{ ($_.Group|%{$_.v}) -join '' } c-))-d-))-e a->>-z->>-f g~>>~y~>>~i~>>~j
左側の矢印をキーにGroup Byすればいいってことに気づいたので、PowerShellのGroup-Object
でやってみました。便利だ便利。こんな感じでGroup Byしてくれるコマンド、欲しくなりますね。
Q5
words
ファイルは以下のような内容です
ang ker ora ch pea eat spea nge rep le
1列目、2列目を組み替えて、意味のある単語にしてくださいという問題
$ join -j9 Sh*/vol.65/words{,} | awk '$0=$1$NF' | grep -x -f- /usr/share/dict/words
join -j9
とawk
ですべての組み合わせを列挙し、/usr/share/dict/words
に存在する単語を選ぶという感じです。
PowerShellはどうすればいいんだ…?
Q6
echo 焼肉定食
からはじめて総画数を計算するという問題。すごいぜ。
$ wget https://raw.githubusercontent.com/cjkvi/cjkvi-tables/master/joyo2010.txt $ echo 焼肉定食 | grep -o . | grep -f- ./joyo2010.txt | sel -a -- -3 | jq -s add
画数とかがまとまってるデータをwget
してきて、漢字をgrep
で検索。あとは該当のカラムを選んで足すという感じです。
PowerShell解は以下の通りです。
$ echo 焼肉定食 | %{ $_ -split '' } | ?{ $_ } | %{ ((sls $_ .\joyo2010.txt) -split '\s')[2] } | measure -Sum | %{$_.Sum}
echo 焼肉定食
で始めることを意識してこんな感じに仕上げてみました。大体のことは組み込みでできますね。
Q7
kim
ファイルの内容から総画数が素数な行を取り出す問題。
$ cat Sh*/vol.65/kim | \ while read N; do echo $N $(echo $N | grep -o . | grep -f- ./joyo2010.txt| sel -a -- -3 | jq -s add|factor); done | \ awk 'NF==3{print $1}'
Q6の解答を拡張しただけですね。
LT
今回はLTさせていただきました!
内容としては呪符式高速詠唱シェル芸の続きです。読み取りに使うOCRを複数個にすることで誤認識を減らそうというアイデアを実装したものになります。
最後のライブコーディングでも正しく読み取れたので良かったです!
聞いてくださった方ありがとうございました!
おわり
最近はjulia
やegzact
といった便利なツールを学んだことで、複雑なことを短いコマンドでできるようになりました。それもいいんですが、コマンドを組み合わせることと情報を足して処理しやすくすることも大切だなと感じる問題が多かったです。どっちもできるようになりたいですね。
今回も準備・運営ありがとうございました!
| ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄| | 終 | | 制作・著作 | |  ̄ ̄ ̄ ̄ ̄ ̄ ̄ | | xztaityozx | |_________| ∧∧ || ( ゚д゚)|| / づΦ
第64回2の6乗シェル芸勉強会に参加しました
参加しました。今回はタイミングよく本会場の近くにいたため、現地参加してきました。 いろんな人にお会いできてよかったなあと思いました。
今回も参加記として自分の解答と復習のPowerShell解を書きます。
問題と解説は当日の配信をご覧ください
問題1
replay.txt
はメールの件名、送信者、受信者、送信時刻が行区切りで書かれているデータです。このデータの中から、送信と返信のペアを見つけて、返信までに何時間かかったかを出力するという問題。
$ join -j9 ShellGeiData/vol.64/reply.txt{,} | awk '"Re:"$4==$NF{print $4, $3, $7, $1"T"$2, $5"T"$6}' | teip -f 4,5 -- date -f- +%s | awk '{print $1,$2,$3,($5-$4)/3600}' | sort -k4n やったー 太郎->花子 花子->太郎 3.18278 首記の件 花子->太郎 太郎->花子 22.8919 こんにちは 太郎->花子 花子->太郎 25.1964 げんき? 太郎->花子 花子->太郎 52.0769 毎々お世話になります 太郎->花子 花子->太郎 81.9994
join -j9
でクロスジョインすることでペアを全列挙します。そこから欲しいペアだけawk
で出力していくやり方を採用しました。awk
で状態を持たなくていいのでちょっと楽ですけど、カラムが多くて数えるのが面倒です。
ペアを出したあとは、teip
で日付をUnixTimeにして、時間の差分を取るって感じにしました。dateutils
のdatediff
だともうちょっと簡単に書けたかもです。
PowerShell解もやってみます。
$ cat .\ShellGeiData\vol.64\reply.txt | % -Begin {$hash=@{}} { $x=$_ -split ' '; $hash[$x[3]]=@{t=Get-Date -Date ($x[0..1] -join ' '); ft=$x[2]} } -End { $hash.Keys | ?{ $hash["Re:$_"] } | %{ @($_, $hash[$_].ft,$hash["Re:$_"].ft, $hash["Re:$_"].t.Subtract($hash[$_].t).TotalHours) -join ' ' }} | Sort-Object {[decimal]($_ -split ' ')[3]} やったー 太郎->花子 花子->太郎 3.18277777777778 首記の件 花子->太郎 太郎->花子 22.8919444444444 こんにちは 太郎->花子 花子->太郎 25.1963888888889 げんき? 太郎->花子 花子->太郎 52.0769444444444 毎々お世話になります 太郎->花子 花子->太郎 81.9994444444444
シュッとクロスジョインできなかったので、ForEach-Object
でゴリゴリしました。そんなにいうことはないんですけど、Hashじゃない単純なテキストファイルのソートでSort-Object
を使うとき、列の指定に {($_ -split ' ')[N]}
を使うってのを見てそうなんだ…。ってなりました。
問題2
平均値ゼロ、標準偏差1の正規分布に従う乱数を延々と出力する問題。よくわからない場合は0~1の乱数を12個足して6引くのでもOKとのこと。よくわかんないので後者で行きます。
$ yes | awk 'BEGIN{srand()}{print rand()}' | xargs -n12 | sel -D + 0 | addr -6 | bc | head -.5806370 .266194 -1.616049118 -.6380601 -.297515 -.5362764 -1.0671419 1.218777 .5491392 -.9868898
はい…。解説することがないぐらいシンプルにできました。ちなみにaddr
はegzactのやつで、右端に文字を追加するっていうコマンドです。sed
でもいいです。
PowerShell解もやってみます
$ while($true) {(Get-Random -SetSeed (Get-Date).Nanosecond -Maximum 1.0 -Count 12 | measure -Sum).Sum - 6}
こちらも解説することはあんまないです。
問題3
mat
ファイルには行列が書かれています。これが対称行列であることを示してください。という問題。
$ cat ShellGeiData/vol.64/mat | rs -T | sel -a 0 | diff - ShellGeiData/vol.64/mat && echo そうだよ || echo ちゃうよ そうだよ
そうらしいです。rs
の-T
で転置して、diff
が出なければOKって感じですね。
PowerShell解も大体同じになっちゃうんですが、こっちはjuliaでやってみます。
$ diff (("transpose([$((cat .\ShellGeiData\vol.64\mat) -join ';')])" | julia)[1..5] -replace ' +',' ' -replace '^ ') (cat .\ShellGeiData\vol.64\mat); echo $? True
julia
で転置を計算し、不要な部分を削ってから diff
を取ってます。それだけです
問題4
nums.0
ファイルは、一行に[value] [key]
という形式でデータが書かれたファイルです。
小問1
[key]
で集計し、[value]
の平均値を求めて下さい
小問2
nums.0
の[key]
を付け替えてください。さっき求めた平均値に最も近い[key]
を選ぶようにしてください。
という感じの問題。小問1はさっと行けたんですが…。
$ cat ShellGeiData/vol.64/nums.0 | awk '{c[$2]++;t[$2]+=$1}END{for(k in c) print k, t[k]/c[k]}' 0 5.33333 1 6.33333 2 6
小問2は時間切れになりました。解説によると、平均値を 5.33333 6.33333 6
みたいに1行にしてから、nums.0
の右に連結、awk
で一番近いのを計算しつつ、フィールド番号からキーを付け替えるということでした。むず
問題5
オイラーのファイ関数を実装してください。という問題。
$ echo 12 | factor | zniq | tr -d : | teip -f2- -- sed 's:.*:*(1-1/&):g' | julia 4.0
素因数をuniq
し、k番目の素因数をpk
としたとき、この式はN * (1-1/p1) * (1-1/p2) * ... * (1-1/pk)
となるので、これをシェルで再現すればよいということになりますね。素因数分解といえばfactor
コマンドです。この出力からユニークな素因数一覧を取り出したいです。fmt -1|uniq
みたいなことをすればいいですが、横方向のuniq
をしたいときはegzactのzinq
を使えばサッとできるのでオススメです。あとはteip
で1カラム目以降を(1-1/pk)
の形に整形してjulia
で計算してます。別にjulia
じゃなくてもよいです。
PowerShell解もやってみます。と思ったんですけど、julia
を調べてたらtotient
がありました
$ echo 12 | %{ julia -E "using Primes;totient($_)" } 4
PowerShell解じゃなくない?とはなってます
問題6
小問1
seq 100 |
から初めて、各行の数字に対して互いに素な自然数を列挙してください
小問2
列挙した自然数のリストが、ファイ関数の出力と一致することを確認してください
という問題。時間内にどちらも解けませんでした!
# 小問1 $ seq 100 | xargs -I@ julia -E '(@, [x for x in 1:@ if gcd(@,x) == 1])' | tr -d \(\) 1, [1] 2, [1] 3, [1, 2] 4, [1, 3] 5, [1, 2, 3, 4] 6, [1, 5] 7, [1, 2, 3, 4, 5, 6] 8, [1, 3, 5, 7] 9, [1, 2, 4, 5, 7, 8] 10, [1, 3, 7, 9] 11, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 12, [1, 5, 7, 11] ...
またjulia
です。Pythonみたいなリスト内包表記が使えるってのをみてめっちゃええやんってなりました。
小問2も、ここにちょっと足すだけで解けますね。
# 小問2 $ seq 100 | xargs -I@ julia -E 'using Primes; (@, totient(@), [x for x in 1:@ if gcd(@,x) == 1])' | tr -d \(\) 1, 1, [1] 2, 1, [1] 3, 2, [1, 2] 4, 2, [1, 3] 5, 4, [1, 2, 3, 4] 6, 2, [1, 5] 7, 6, [1, 2, 3, 4, 5, 6] 8, 4, [1, 3, 5, 7] 9, 6, [1, 2, 4, 5, 7, 8] 10, 4, [1, 3, 7, 9] 11, 10, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 12, 4, [1, 5, 7, 11] ...
2要素目にファイ関数の出力を置きました。あとは見比べればいいですね。
PowerShell解はほぼ同じになっちゃうので省略です。
LT
今回自分はLTしませんでしたが、面白い話を聞いたので紹介します。
ジョークRFCのrfc9402の実装をしたっていう内容でした。めっちゃよかった。
感想
以前julia
をオススメされてから少しずつ使ってはいたのですが、今回の問題だと使える場面があって良かったです。これで数学系の問題がきても平気ですね。
今回もとても楽しかったです!企画、運営などありがとうございました!
App::Prove::Plugin::の素振りをした話
多分初めてのPerlネタです。
proveのプラグイン書いてみたかった
metacpanでApp::Proveのドキュメントを読んでいたら、プラグイン機構があることを知ったので素振りをしてみました。ネタはなんでもよかったんですが、今回は Jestの--shard
オプションみたいなのを作ってみます。
この記事で実装したやつを丁寧に書いたやつは以下のリポジトリにあります。
ドキュメントを読んでみる
App::Prove::Plugin::[HOGE]
という感じのモジュールを作れば、prove
の呼び出し時に -PHOGE
と指定することでプラグインを読み込ませることができるようです。今回はShard
プラグインとして作りたいので、App::Prove::Plugin::Shard
モジュールを作ればよいですね。呼び出しは-PShard
になるはずです。
肝心の実装方法ですが、サンプルを読む感じ、まぁ割と直感的かなという印象です。最小は以下の通りになりそうです。これを読み込んでも何も起きないですが。
package App::Prove::Plugin::Shard; use strict; use warnings FATAL => 'all'; use utf8; sub load { # ここにやりたいことを書いたり書かなかったりする return 1; }; 1;
すでにあるApp::Prove::Plugin::*を読んでみる
すでに公開されているプラグインをいくつか読んでみます。今回はテストしたいファイルのリストに介入したいです。似た感じのプラグインを探したところ、App::Prove::Plugin::Countが見つかりました。実装を読んでみると、App::Prove::_get_tests
をラップしてやってるみたいですね。こういう柔軟さがPerlのいいところですよねえ。
今回のShard
プラグインでも同じように App::Prove::_get_tests
をラップして、テスト対象のファイルを絞り込んでいく感じにしていこうと思います。
ざっくり実装してみる
load
にはApp::Prove
のオブジェクトが渡されます。そこにはプラグインに対して渡された引数が含まれているようです。
例えば -PShard=1
と呼び出されたとき
sub load { my (undef, $prove) = @_; my @args = @{ $prove->{args} }; # [1] }
という感じに $prove->{args}
を通じて引数の値にアクセスできるようです。Jestの--shard
ではどのシャードか、と全部で何個のシャードがあるかを渡すようになっています。例えば以下のように指定します。
# シャード1/全体で3個のシャード $ jest --shard=1/3
なのでShard
プラグインでも同じように指定したいです。サンプルやドキュメントにもある通り、プラグインへの引数はカンマ区切りで複数個渡すことができ、先のargs
メンバーに配列として格納されます。というわけで -PShard=1,3
という感じの指定方法がとれそうです。
sub load { my (undef, $prove) = @_; my @args = @{ $prove->{args} }; # -PShard=1,3 としたとき [1, 3] }
これが取れたらあとは App::Prove::_get_tests
をaround
でえいするだけですね。
# useなどは省略してます sub load { my (undef, $prove) = @_; my @args = @{ $prove->{args} }; my $n = $args[0]; # 何番目のシャードを選ぶか my $m = $args[1]; # 全体で何個のシャードがあるか around 'App::Prove::_get_tests' => sub { my $orig = shift; # m個のグループに順番に割り振り -> n番目のグループを選ぶという感じの実装 # n, mともに1以上を想定 # 振り分けが毎回同じになるように並べ替えておく # でもたぶんProveが展開したときにすでに並び変わってる気がしなくもないですね my @test_files = sort $orig->(@_); # データの流れがパイプとは逆向きだからなんか違和感ある return map { $test_files[$_] } grep { ($_ % $m) == n - 1 } 0..$#test_files; }; return 1; }
ざっくり実装できたので、100個のテストファイルがシャードに分けられるか試してみましょう
# テストファイルを作って $ vim example_tests/1.t # 100個に複製 $ seq 2 100 | rargs cp example_tests/1.t example_tests/{1}.t # Shardプラグインを試してみる。5個中の1番目のシャード $ prove -PShard=1,5 ./example_tests example_tests/1.t ... ok example_tests/13.t .. ok example_tests/18.t .. ok example_tests/22.t .. ok example_tests/27.t .. ok example_tests/31.t .. ok example_tests/36.t .. ok example_tests/40.t .. ok example_tests/45.t .. ok example_tests/5.t ... ok example_tests/54.t .. ok example_tests/59.t .. ok example_tests/63.t .. ok example_tests/68.t .. ok example_tests/72.t .. ok example_tests/77.t .. ok example_tests/81.t .. ok example_tests/86.t .. ok example_tests/90.t .. ok example_tests/95.t .. ok All tests successful. Files=20, Tests=20, 1 wallclock secs ( 0.02 usr 0.02 sys + 0.67 cusr 0.06 csys = 0.77 CPU) Result: PASS
100個を5個に分割した内の1つを選んだので、20個のテストが実行されています!想定通りですね!やりました!
シャード化したテストを並列処理させてみよう
Jestの--shard
のユースケースとしては、巨大なテストスイートを小さくし、それぞれのシャードをいくつかのテストランナーへ同時に投げることだと思います。例えば GitHub Actionsでは以下のようなワークフローを書いて実現できますね。(いろいろ省略してます)
jobs: test: strategy: matrix: shard: ["1/3", "2/3", "3/3"] steps: - run: jest --shard ${{matrix.shard}}
今回 Shard
プラグインを作りましたから、これを使って同じことができそうです。実はこれが最初のモチベだったりします。
というわけで書いてみたのが以下ですね。
name: 'CI' on: push jobs: example: runs-on: ubuntu-latest strategy: matrix: shard: ["1,5", "2,5", "3,5", "4,5", "5,5"] steps: - uses: actions/checkout@v3 - uses: shogo82148/actions-setup-perl@v1 with: perl-version: 5.34.1 install-modules-with: cpm enable-modules-cache: true - run: echo PERL5LIB=$PERL5LIB:$PWD/lib:$PWD/local/lib/perl5 >> $GITHUB_ENV - run: prove -PShard=${{matrix.shard}} ./example_tests
このワークフローを開始すると、以下のようにランナーが5つ起動し
それぞれに指定されたシャードのテストを実行させることができるということです。うれしいですね。
Perlのセットアップはactions-setup-perlがめちゃめちゃ便利でした。cache機構もあるので、actions/cache
でなんかいろいろ書いたりしなくていいところが嬉しいポイントでしたね。
その他、地味にハマった点としては、PERL5LIB
へApp::Prove::Plugin::Shard
がある場所へのパスが渡されていなければならないことでした。ずっと「prove --lib
でlib
は読み込んでるはず…なのになんでなの????」ってなってました。
まとめ
prove
にプラグイン機構があることを知ったので素振りしたかった- 題材として、Jestの
--shard
みたいなのをやってみることにした - 割とあっさりできた
- GitHub Actionsでそれぞれのシャードを別々に動かすことができた
おわり
本当は Type::Tiny
のAPIがよくわからずめちゃめちゃハマった話とか、windows-latest
なVMでstrictures
がビルドできなかったりしたなど、割とつらいことが色々ありました。でも本質じゃなかったので書きませんでした…。Perl難しいですねえ。
第63回シェル芸勉強会に参加しました
第63回シェル芸勉強会
参加しました。今回は正規表現とか検索の回とのことでした。参加記として解答とPowerShell解を残しておきます。
当日の配信はこちらです。
問題1
九九の合計を出力する問題。えいでできそうですね。
$ echo {1..9}'*'{1..9} | fmt -1 | bc | jq -s add 2025 # 別解 $ seq 9 | join -j9 -t\* - <(seq 9) -o 1.1,2.1 | paste -sd+ | bc
組み合わせを生成して合計するだけですね。PowerShell解も考えてみます
$ (1..9 | %{ $x=$_; 1..9 | %{ $_ * $x } } | measure -Sum).Sum
普通に計算した感じですね。
問題2
9132円の支払いとして、10000円を出したとき、お釣りとしてあり得る効果の組み合わせを出力する問題。組み合わせは1種類でいいとのことなので、1円玉868枚と出力するだけでいいのですが、それだとつまらないのでできるだけ出力してみましょう。
$ join -j9 <(echo 500'*'{0,1} | fmt -1) <(echo 100'*'{0..8} | fmt -1) | join -j9 - <(echo 50'*'{0..17}|fmt -1) | sel -a -D+ 2: | sel 1 1 | teip -f2 -- bc | awk '$2<=868{for(ten=0;ten<=int((868-$2)/10);ten++) for(one=0;one<=int(868-ten*10-$2);one++)if($2+ten*10+one == 868) print $1"+10*"ten"+1*"one}' ... 500*1+100*2+50*2+10*1+1*58 500*1+100*2+50*2+10*2+1*48 500*1+100*2+50*2+10*3+1*38 500*1+100*2+50*2+10*4+1*28 500*1+100*2+50*2+10*5+1*18 500*1+100*2+50*2+10*6+1*8 500*1+100*2+50*3+10*0+1*18 500*1+100*2+50*3+10*1+1*8 500*1+100*3+50*0+10*0+1*68 500*1+100*3+50*0+10*1+1*58 500*1+100*3+50*0+10*2+1*48 500*1+100*3+50*0+10*3+1*38 500*1+100*3+50*0+10*4+1*28 500*1+100*3+50*0+10*5+1*18 500*1+100*3+50*0+10*6+1*8 500*1+100*3+50*1+10*0+1*18 500*1+100*3+50*1+10*1+1*8
500円玉、100円玉、50円玉の組み合わせを列挙、一度この時点で868円を超える組み合わせは除外します。最初から組み合わせを列挙すると、組み合わせの量が多すぎてえらいことになるのでこうしてます。
10円玉と1円玉の部分はawk
で組み合わせ列挙、868円となるものだけ出力してます。
同じ方針でのPowerShell解は以下です。
$ 0..1 | %{ @{ str="$_*500"; sum=$_*500 } } | %{for($i=0;$i -le 8;$i++){@{sum=$_.sum+$i*100;str=$_.str+"+$i*100"}}} | %{for($i=0;$i -le 17;$i++){@{sum=$_.sum+$i*50;str=$_.str+"+$i*50"}}} | ?{$_.sum -le 868} | %{for($i=0;$i -le 86;$i++){@{sum=$_.sum+$i*10;str=$_.str+"+$i*10"}}} | ?{$_.sum -le 868} | %{$d=868-$_.sum;$_.str+"+$d*1"}
問題3
genkou.tex
からペアになっていない\ref
、\label
を探す問題。160本ノックに類題がありました。という話もされましたね。
$ cat She*/*.63/gen* | grep -oPe '\\(label|ref)\{[^}]+}' | sel -gd '\\|\{|\}' 3 | sort | uniq -u eq:state_equation_linear eq:state_equation_nonlinear fig:typhoon
欲しいデータだけ削っていき、uniq -u
で重複部分だけ取り出すって感じですね。uniq
のオプションは覚えられんです。サッと出てくる人はすごいです
PowerShell解は以下です。
$ Get-Content -Encoding UTF-8 She*/*.63/gen* | sls '\\(ref|label)\{[^{]+}' | %{ $_.Matches.Value } | sort -Unique | %{ $x=($_ -split '{'); @{a=$x[0];b=$x[1]} } | group -Property b | ?{$_.Group.Count -eq 2 }|%{$_.Name} eq:state_transition_model} eq:state_transition_model2} fig:motion}
問題4
tonnan.txt
から東西南北
となっている部分文字列を抽出する問題。ただし、東西南北
の並び順は考えなくて良いとのこと。
$ cat She*/*.63/to* | conv fs='' 4 | zniq fs='' | awk 'length($1)==4{print NR"文字目から"$1}' 3文字目から西南北東 12文字目から南北西東 33文字目から南東西北 41文字目から西北南東 43文字目から南東北西 54文字目から東北南西 55文字目から北南西東 56文字目から南西東北 57文字目から西東北南 58文字目から東北南西 63文字目から南西東北 64文字目から西東北南 68文字目から東南西北 71文字目から北西南東 72文字目から西南東北 97文字目から西北東南
egzact
の出番です!
先頭からずらしながら4つずつ選んでいくのはconv
を使います。fs=''
とすることで一文字ずつをカラムにできるので楽です。
東西南北が揃っているかどうかは、横方向のuniq
をした後の文字列が4文字かどうかで判定できます。横方向のuniq
にはzniq
を使います。これもegzact
のコマンドです。便利~~~!
PowerShell解もやってみます。こちらはegzact
無しです
$ $TEXT=(Get-Content -Encoding utf8 Sh*/*.63/to*); 0..$TEXT.Length | %{ $x=$TEXT[$_..($_+3)]; if(($x | sort -Uniq).Length -eq 4) { "$_ "+($x -join '') } }
$TEXT
に文字列を取り出しておいて、スライスで切り出していきます。残りの部分はzniq
や、awk
でやってることと同じですね
問題5
reversi.txt
は以下のような内容のファイルです
12345678 A B C ⚪ D ⚪⚫ E ⚫⚫ F ⚪⚫ G H
次の手番は⚪
です。いくつかおける場所は何個かありますが、斜めにひっくりかえせる場所である、E6, G7に❌
をおいてくださいという問題。ただし、普通にやるだけだと面白くないので、同じsed
二回だけで完成させてくださいとのこと。どうして。
cat reversi.txt | sed '式' | sed '式'
難しそうですが、ひっくりかえせるかどうかを調べるべき座標は、⚪
から固定数倍個先であることが分かれば正規表現を書くのはやるだけです。
$ cat She*/*.63/re* | sed -zE 's/(⚪.{10}(⚫.{10})+) /\1❌/' | sed -zE 's/(⚪.{10}(⚫.{10})+) /\1❌/' 12345678 A B C ⚪ D ⚪⚫ E ⚫⚫❌ F ⚪⚫ G ❌ H
はい…。これがやるだけな正規表現かどうかは謎なんですが…。ここからの話はsed -zE
を前提にします。
盤面をななめ見るとき、駒と駒の間には改行を含めて10文字が存在します。最初は⚪
の次に⚫
が来なければなりません。これを正規表現で表すと⚪.{10}⚫
と書けます。
その先の駒も考えてみます。❌
の位置に⚪
を置くことで斜めにひっくりかえせることを考えると、⚫
の次は⚫
でなければなりません。正規表現で書くと⚫.{10}⚫
となりますが、⚫
は何個続いてもいいので、+
を使った繰り返しを用いて(⚫.{10})+
と表すことができます。
⚪
から⚫
の正規表現と⚫
から⚫
の正規表現を連結すると、⚪.{10}(⚫.{10})+
と表せます。最後、空白マスである
が登場したら、そこが❌
を置きたい場所になるので置換を行います。このとき、⚪
~⚫
~⚫
~の部分はそのまま使いたいので、キャプチャして後方参照します。
なんでこれが二回なのかというと、G7に置くときの始点であるD4は、E6の検索中にすでに通過している部分であり、sed
ではいったん戻って再検索ということができないからですね。
なんというか、信じられないほど読みづらい文章ですね。むずかしいね。
PowerShell解は…勘弁してください。
問題6
先ほど❌
を置いた位置と⚪
の間にある⚫
を⚪
にする問題。やっぱりねー!予想ついてましたよ!
この問題ではPerl
を使うと楽とのこと。先読みやら後読みやらが使えるからですね!最強!
$ cat She*/*.63/re* | sed -zE 's/(⚪.{10}(⚫.{10})+) /\1❌/' | sed -zE 's/(⚪.{10}(⚫.{10})+) /\1❌/' | tr -d \\n | perl -C -Mutf8 -pe 's/⚫(?=.{9}(⚫.{9})*❌)/⚪/g' | fold -b27 | awk 4 12345678 A B C ⚪ D ⚪⚪ E ⚫⚪❌ F ⚪⚪ G ❌ H
時間切れでした。正規表現を書くところまではよかったんですが、-Mutf8
にハマってしまいました。-C
が必要ってところまでは過去の午前の部で習ったので覚えていたんですが…。無念。
LT
今回もLTさせていただきました!聞いてくださった方ありがとうございました!
音声合成やってみたいな…と思ったのでやってみたというお話です。筋トレも頑張ります。
まとめ
今回は2問目をサッと解けるような回答が出せなくて悔しかったですね。最後2問も難しそうなんですが、パターンに気づけるかどうかだったので頭の体操って感じでいいなって思いました。楽しんで解くことができました!
途中、もなかを頂いたんですが、これが糖分補給にめちゃめちゃ良かったです。次回から必須アイテムかもですね(?)
今回も企画・開催・準備などありがとうございました!