たいちょーの雑記

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

第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=""
香桂銀金王金銀桂香         
×飛×××××角          
歩×歩歩歩歩歩歩歩         
 歩   〇     
    〇    
歩 歩〇        
〇歩〇歩歩歩歩歩歩         
 角 銀   飛         
香桂〇金玉金銀桂香         

sedawkで〇になりうるところ決め打ちで書き換えています。ほとんど解答例として解説されたものと同じですね。

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 -Fn=1000000000000
xztaity+   20150  0.0  0.0   8756  2816 pts/4    TN   15:46   0:00 awk -Fn=1000000000000000
xztaity+   20151  0.0  0.0   8756  3040 pts/4    TN   15:46   0:00 awk -Fn=1000000000000000000

ほかの環境でも再現性があるかはわからないんですが、大きな数値を代入するawkコードをを実行することでRSSを使わせ、メッセージを仕込むことができました。ほかの文字列でもできるかは謎です

終わり

stty tostopやプロンプトの展開など、全然知らないことを知れたのが嬉しかったです。 今回も企画・開催ありがとうございました!

| ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄|
|        終        |
|    制作・著作    |
|   ̄ ̄ ̄ ̄ ̄ ̄ ̄  |
|    xztaityozx    |
|_________|
  ∧∧   ||         
  ( ゚д゚)||           
  /    づΦ         

sel の csv/tsv オプションと template オプションの話

selコマンドを知っていますか?カラムを取り出すことに注目したコマンドで、cutawkのちょうど間ぐらいの使い心地を目指して私がちまちま作っているものですね。

github.com

他の似たツールとして selfchoose があります。

基本的な使い方

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

これに対応するために、awkFPATのようなものを実装しようと考えましたが、面倒だしバグらせる自信があるし、なにより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のように数値計算したりはできません。

正直この機能を実装するかどうか結構悩みました。機能過多だと思ったからです。機能が増えるとそれだけメンテするコストが上がってしまうので面倒です。 でも結局実装しました。便利だし、モチベであるcutawkのちょうど間ぐらいの機能として違和感はなかったからです。

まとめ

今回は最近selに追加したcsv/tsvオプションとtemplateオプションについて書きました。

個人的に結構気に入ってるコマンドなので、ぜひ使ってみて貰えると嬉しいです。Goがインストールされていれば go installを通じてインストールできます。Goがない場合でも GitHub Releasesからお好みのバイナリをダウンロードすることができます。

# install
$ go install github.com/xztaityozx/sel@latest

以上です。

| ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄|
|        終        |
|    制作・著作    |
|   ̄ ̄ ̄ ̄ ̄ ̄ ̄  |
|    xztaityozx    |
|_________|
  ∧∧   ||         
  ( ゚д゚)||           
  /    づΦ         

付録

SourceGeneratorで自作例外クラスのコンストラクタを自動生成してみる

自作の例外クラス書いてますか?自分は時々書いてます。

で、雑に以下のように書きますよね。

public class FailedToHogeException : Exception {
    public FailedToHogeException(string message) : base(message) {}
}

するとSonarLintなどからこんな感じの警告もらいませんか?自分はもらいます。

よくみるやつ

これってちゃんと実装しようね!ってことなだけなんですけど、結構実装量多い…ような気がするので自動生成されると嬉しいですね。というわけでSourceGeneratorの素振りの題材として作ってみます。

実装したやつ

実装したものは以下のリポジトリにあります。

github.com

以下のように自動生成してもらいたい例外クラスに 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を書いた

  • オプションで生成するコンストラクタを制御できるようにした

  • フラグのenumをどこに書くべきかはいいアイデアが出なかった

正直テンプレートでもいいので使うかは謎…ですね。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のコマンドです。

github.com

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などのツールやawkforを使わずにというのがあったのでそれにも挑戦してみます。

$ 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すればいいってことに気づいたので、PowerShellGroup-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 -j9awkですべての組み合わせを列挙し、/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を複数個にすることで誤認識を減らそうというアイデアを実装したものになります。
最後のライブコーディングでも正しく読み取れたので良かったです!
聞いてくださった方ありがとうございました!

おわり

最近はjuliaegzactといった便利なツールを学んだことで、複雑なことを短いコマンドでできるようになりました。それもいいんですが、コマンドを組み合わせることと情報を足して処理しやすくすることも大切だなと感じる問題が多かったです。どっちもできるようになりたいですね。

今回も準備・運営ありがとうございました!

| ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄|
|        終        |
|    制作・著作    |
|   ̄ ̄ ̄ ̄ ̄ ̄ ̄  |
|    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にして、時間の差分を取るって感じにしました。dateutilsdatediffだともうちょっと簡単に書けたかもです。

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がありました

juliamath.github.io

$ 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しませんでしたが、面白い話を聞いたので紹介します。

qiita.com

ジョークRFCのrfc9402の実装をしたっていう内容でした。めっちゃよかった。

感想

以前juliaをオススメされてから少しずつ使ってはいたのですが、今回の問題だと使える場面があって良かったです。これで数学系の問題がきても平気ですね。

今回もとても楽しかったです!企画、運営などありがとうございました!

App::Prove::Plugin::の素振りをした話

多分初めてのPerlネタです。

proveのプラグイン書いてみたかった

metacpanでApp::Proveのドキュメントを読んでいたら、プラグイン機構があることを知ったので素振りをしてみました。ネタはなんでもよかったんですが、今回は Jestの--shardオプションみたいなのを作ってみます。

この記事で実装したやつを丁寧に書いたやつは以下のリポジトリにあります。

github.com

ドキュメントを読んでみる

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_testsaroundでえいするだけですね。

# 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つ起動し

example(1/5 ~ 5/5)

それぞれに指定されたシャードのテストを実行させることができるということです。うれしいですね。

うれしい


Perlのセットアップはactions-setup-perlがめちゃめちゃ便利でした。cache機構もあるので、actions/cacheでなんかいろいろ書いたりしなくていいところが嬉しいポイントでしたね。

その他、地味にハマった点としては、PERL5LIBApp::Prove::Plugin::Shardがある場所へのパスが渡されていなければならないことでした。ずっと「prove --liblibは読み込んでるはず…なのになんでなの????」ってなってました。

まとめ

  1. proveプラグイン機構があることを知ったので素振りしたかった
  2. 題材として、Jestの--shardみたいなのをやってみることにした
  3. 割とあっさりできた
  4. GitHub Actionsでそれぞれのシャードを別々に動かすことができた

おわり

本当は Type::TinyAPIがよくわからずめちゃめちゃハマった話とか、windows-latestVMstricturesがビルドできなかったりしたなど、割とつらいことが色々ありました。でも本質じゃなかったので書きませんでした…。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の出番です!

github.com

先頭からずらしながら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させていただきました!聞いてくださった方ありがとうございました!

speakerdeck.com

音声合成やってみたいな…と思ったのでやってみたというお話です。筋トレも頑張ります。

まとめ

今回は2問目をサッと解けるような回答が出せなくて悔しかったですね。最後2問も難しそうなんですが、パターンに気づけるかどうかだったので頭の体操って感じでいいなって思いました。楽しんで解くことができました!

途中、もなかを頂いたんですが、これが糖分補給にめちゃめちゃ良かったです。次回から必須アイテムかもですね(?)

今回も企画・開催・準備などありがとうございました!