Goシェル芸リターンズ

# 概要

シェル芸勉強会でLTをしてみたかったのでGoをワンライナーで書くコマンドを作りなおしました。
[Gowk](https://github.com/kuwa72/gowk)というものです。

# スライド

[![thumbnail](https://image.slidesharecdn.com/go1liner-181222092502/95/writing-one-liner-by-golangjapanese-1-638.jpg?cb=1545470736)](https://www.slideshare.net/yuichirokuwashima/writing-one-liner-by-golangjapanese)

# 説明

内容としては3つありました。

1. Goシェル芸自体は既出でした。昔[g1](https://github.com/kuwa72/g1)というコマンドをシェルスクリプトで作りましたがいくつか不都合がありました。（ラッパーさえ書けばコンパイルするタイプの処理系でもシェル芸できるといえばできる説もある）
2. 作り直す際にGoのコードを動的に実行しようとしましたが調べたところ無理そうなので諦めました。一時ファイルにコードを出力しコンパイルして実行する形式になりました。
3. Golangでawk風のワンライナーが書ける[gowk](https://github.com/kuwa72/gowk)というコマンドを作ってデモを行いました。

# まとめについての補足

会場で質問をいただきましたが、Go言語がシェル芸に向いていないと考える理由はいくつかあります。

* トップレベルに直接コードが書けません。
* 設計思想的に短く書くことをあまり想定していないのである程度長めになります。
* awk等と比べると文字列と数値の扱いが厳格なので型変換が面倒です。とはいえGoの場合は代入時の型推論があるので一般的な静的型言語より短く書ける場合もあるかと思います。
* ワンライナーで書くには多値で帰ってくるエラーがちょっと邪魔かと思います。また多値で帰ってくるエラーのため関数の引数の中で関数を実行する書き方ができない場合があり、ショートコーディングが辛いということになります。
    * ちょっと思ったけど[エラーを握りつぶしつつ値変換するひと](https://goplay.space/#BJ_ejw_gb7z)がいればよいかも。行儀は悪いがワンライナーなのでよしとする。

# 実際に使ってみる

LT準備のため会場で問題を解けなかったので解いていきたいと思います。

問題等の詳細は[39回シェル芸勉強会リンク集](https://b.ueda.tech/?post=20181223_shellgei_39_links)を参照してください。

fishなのでBash/Zshとエスケープが違うかもしれません。

gowkのオプションは以下のような感じです。

* -n: 1行読んで処理するモード
* -v: verbose(コンパイル前のコードを表示する)
* -i [pkg]: パッケージをインポートする。複数指定可。
* [-d, -b, -e] [script]: 定義類と開始処理と終了処理
* -r [script]: メインの処理

## Q1

[regexp.Expand](https://goplay.space/#BJ_ejw_gb7z)かregexp.ReplaceAllFuncでいけそうだけど後方参照がやたら弱い。
正規表現まわりやたら不便なので便利ライブラリを作りたいですねこれ…。
特殊パターンのとこがつらい。

```shell-session
➜  vol.39 git:(master) ✗ cat wrong.md | gowk -v -i os -i io/ioutil -i regexp -r 's,_:=ioutil.ReadAll(os.Stdin);reg:=regexp.MustCompile(`[\[\(](?P<url>[^\]\)]+?)[\]\)][\[\(](?P<tex
t>[^\]\)]+?)[\]\)]`);bs:=reg.ReplaceAllFunc(s,func(s []byte)[]byte{sm:=reg.FindStringSubmatch(string(s));if strings.HasPrefix(sm[2],"群") || strings.HasPrefix(sm[2],"h") {_s:=sm[2
];sm[2]=sm[1];sm[1]=_s};return []byte(fmt.Sprintf("[%s](%s)",sm[2],sm[1]))});fmt.Println(string(bs))'
2018/12/24 20:18:07 package main

import (
        "fmt"
        "io/ioutil"
        "os"
        "regexp"
        "strings"
)

func main() {
        // begin

        // main
        s, _ := ioutil.ReadAll(os.Stdin)
        reg := regexp.MustCompile(`[\[\(](?P<url>[^\]\)]+?)[\]\)][\[\(](?P<text>[^\]\)]+?)[\]\)]`)
        bs := reg.ReplaceAllFunc(s, func(s []byte) []byte {
                sm := reg.FindStringSubmatch(string(s))
                if strings.HasPrefix(sm[2], "群") || strings.HasPrefix(sm[2], "h") {
                        _s := sm[2]
                        sm[2] = sm[1]
                        sm[1] = _s
                }
                return []byte(fmt.Sprintf("[%s](%s)", sm[2], sm[1]))
        })
        fmt.Println(string(bs))

        //end

}

# わたしはマークダウソちょっとできる

## 軍馬県高崎市

[軍魔県](https://ja.wikipedia.org/wiki/%E7%BE%A4%E9%A6%AC%E7%9C%8C)は、日本の県庁所在地の一つ。県庁所在地は[高崎市](https://ja.wikipedia.org/wiki/%E9%AB%98%E5%B4%8E%E5%B8%82)

* [松井常松](https://ja.wikipedia.org/wiki/%E6%9D%BE%E4%BA%95%E5%B8%B8%E6%9D%BE)
* [高崎ハム](http://takasakiham.com/?transactionid=8e5164a76108c8411e7547d69e0dd0fd443f072a)


![たかさきしししょう](群馬県高崎市市章.svg)

```

最終的なGoのソースコード。なんかもう少し短くなりそうな気もする。
正規表現中に後方参照用のラベルが残ってるけど不要。
配列のインデックスの入れ替えは`a[1],a[2] = a[2],a[1]`とか書ける。
strings.HasPrefixを無理に使う必要はないかも。

```golang
package main

import (
        "fmt"
        "io/ioutil"
        "os"
        "regexp"
        "strings"
)

func main() {
        // begin

        // main
        s, _ := ioutil.ReadAll(os.Stdin)
        reg := regexp.MustCompile(`[\[\(](?P<url>[^\]\)]+?)[\]\)][\[\(](?P<text>[^\]\)]+?)[\]\)]`)
        bs := reg.ReplaceAllFunc(s, func(s []byte) []byte {
                sm := reg.FindStringSubmatch(string(s))
                if strings.HasPrefix(sm[2], "群") || strings.HasPrefix(sm[2], "h") {
                        _s := sm[2]
                        sm[2] = sm[1]
                        sm[1] = _s
                }
                return []byte(fmt.Sprintf("[%s](%s)", sm[2], sm[1]))
        })
        fmt.Println(string(bs))

        //end

}
```

## Q2

ソートパッケージもワンライナーで使うには冗長。各会場の行はMapで持つことにした。

```shell-session
➜  vol.39 git:(master) ✗ cat attendee.md | gowk -v -i sort -n -d 'type sg struct{n string;v map[string]string};func(s *sg)add(v string){s.v[v[6:9]]=v};type ss []sg;func(s ss)Len()int{return len(s)};func(s ss)S
wap(i,j int){s[i],s[j]=s[j],s[i]};func(s ss)Less(i,j int)bool{return s[i].n[5:7]<s[j].n[5:7]}' -b 'i:=-1;var ts ss = []sg{}' -r 'if s[0][0] == \'*\' {i=i+1;ts=append(ts,sg{n:s[0],v:map[string]string{}});contin
ue};ts[i].add(s[0])' -e 'sort.Sort(ts);for _,s1:=range ts{fmt.Println(s1.n);for _,s2:=range []string{"福","大","東"}{if _,ok:=s1.v[s2];ok{fmt.Println(s1.v[s2])}}}'
2019/01/07 00:28:06 fixImports(filename=""), abs="/Users/yuichiro/go/src/github.com/ryuichiueda/ShellGeiData/vol.39", srcDir="/Users/yuichiro/go/src/github.com/ryuichiueda/ShellGeiData" ...
2019/01/07 00:28:06 package main

import (
        "bufio"
        "fmt"
        "os"
        "sort"
        "strings"
)

type sg struct {
        n string
        v map[string]string
}

func (s *sg) add(v string) { s.v[v[6:9]] = v }

type ss []sg

func (s ss) Len() int           { return len(s) }
func (s ss) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
func (s ss) Less(i, j int) bool { return s[i].n[5:7] < s[j].n[5:7] }

func main() {

        i := -1
        var ts ss = []sg{}

        r := os.Stdin
        scanner := bufio.NewScanner(r)

        for scanner.Scan() {
                s := []string{scanner.Text()}
                s = append(s, strings.Fields(s[0])...)

                if s[0][0] == '*' {
                        i = i + 1
                        ts = append(ts, sg{n: s[0], v: map[string]string{}})
                        continue
                }
                ts[i].add(s[0])
        }
        if err := scanner.Err(); err != nil {
                fmt.Fprintln(os.Stderr, "reading standard input:", err)
        }

        sort.Sort(ts)
        for _, s1 := range ts {
                fmt.Println(s1.n)
                for _, s2 := range []string{"福", "大", "東"} {
                        if _, ok := s1.v[s2]; ok {
                                fmt.Println(s1.v[s2])
                        }
                }
        }
}

* 第34回シェル芸勉強会
    * 大阪: 16
    * 東京: 19
* 第35回シェル芸勉強会
    * 大阪: 10
    * 東京: 27
* 第36回シェル芸勉強会
    * 東京: 38
* 第37回シェル芸勉強会
    * 福岡: 8
    * 大阪: 10
    * 東京: 21
* 第38回シェル芸勉強会
    * 福岡: 3
    * 大阪: 8
    * 東京: 26
```

最終的なGoのソースコード。選択ソートを自力で書いたほうが短いかもしれない。

```golang
package main

import (
        "bufio"
        "fmt"
        "os"
        "sort"
        "strings"
)

type sg struct {
        n string
        v map[string]string
}

func (s *sg) add(v string) { s.v[v[6:9]] = v }

type ss []sg

func (s ss) Len() int           { return len(s) }
func (s ss) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
func (s ss) Less(i, j int) bool { return s[i].n[5:7] < s[j].n[5:7] }

func main() {

        i := -1
        var ts ss = []sg{}

        r := os.Stdin
        scanner := bufio.NewScanner(r)

        for scanner.Scan() {
                s := []string{scanner.Text()}
                s = append(s, strings.Fields(s[0])...)

                if s[0][0] == '*' {
                        i = i + 1
                        ts = append(ts, sg{n: s[0], v: map[string]string{}})
                        continue
                }
                ts[i].add(s[0])
        }
        if err := scanner.Err(); err != nil {
                fmt.Fprintln(os.Stderr, "reading standard input:", err)
        }

        sort.Sort(ts)
        for _, s1 := range ts {
                fmt.Println(s1.n)
                for _, s2 := range []string{"福", "大", "東"} {
                        if _, ok := s1.v[s2]; ok {
                                fmt.Println(s1.v[s2])
                        }
                }
        }
}
```

## Q3

標準ライブラリにパーサーがあるのでこれは割と楽。見るだけならこんな感じで。

```shell-session
➜  vol.39 git:(master) ✗ cat index.html | gowk -v -i golang.org/x/net/html -r 'z := html.NewTokenizer(os.Stdin);for{tt := z.Next();switch tt {case html.ErrorToken:return;case html.StartTagToken:t:=z.Token();tagName := strings.ToLower(t.Data);if tagName == "meta" {fmt.Println(t)}}}'
2019/01/07 01:13:05 fixImports(filename=""), abs="/Users/yuichiro/go/src/github.com/ryuichiueda/ShellGeiData/vol.39", srcDir="/Users/yuichiro/go/src/github.com/ryuichiueda/ShellGeiData" ...
2019/01/07 01:13:05 package main

import (
        "fmt"
        "os"
        "strings"

        "golang.org/x/net/html"
)

func main() {

        z := html.NewTokenizer(os.Stdin)
        for {
                tt := z.Next()
                switch tt {
                case html.ErrorToken:
                        return
                case html.StartTagToken:
                        t := z.Token()
                        tagName := strings.ToLower(t.Data)
                        if tagName == "meta" {
                                fmt.Println(t)
                        }
                }
        }

}

<meta content="世界中のあらゆる情報を検索するためのツールを提供しています。さまざまな検索機能を活用して、お探しの情報を見つけてください。" name="description">
<meta content="noodp" name="robots">
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
<meta content="/images/branding/googleg/1x/googleg_standard_color_128dp.png" itemprop="image">
```

最終的なGoのソースコード。

```golang
package main

import (
        "fmt"
        "os"
        "strings"

        "golang.org/x/net/html"
)

func main() {

        z := html.NewTokenizer(os.Stdin)
        for {
                tt := z.Next()
                switch tt {
                case html.ErrorToken:
                        return
                case html.StartTagToken:
                        t := z.Token()
                        tagName := strings.ToLower(t.Data)
                        if tagName == "meta" {
                                fmt.Println(t)
                        }
                }
        }

}
```

## Q4

これもHTMLのパースの問題なので同様。めんどくさいので2回叩いてしまうが…。

```
➜  vol.39 git:(master) ✗ cat index.html | gowk -v -i golang.org/x/net/html -r 'z := html.NewTokenizer(os.Stdin);for{tt := z.Next();switch tt {case html.ErrorToken:return;case html.StartTagToken:t:=z.Token();tagName := strings.ToLower(t.Data);if tagName == "script" {z.Next();fmt.Println(string(z.Text()))}}}'>index.js
2019/01/07 02:49:00 fixImports(filename=""), abs="/Users/yuichiro/go/src/github.com/ryuichiueda/ShellGeiData/vol.39", srcDir="/Users/yuichiro/go/src/github.com/ryuichiueda/ShellGeiData" ...
2019/01/07 02:49:00 package main

import (
        "fmt"
        "os"
        "strings"

        "golang.org/x/net/html"
)

func main() {

        z := html.NewTokenizer(os.Stdin)
        for {
                tt := z.Next()
                switch tt {
                case html.ErrorToken:
                        return
                case html.StartTagToken:
                        t := z.Token()
                        tagName := strings.ToLower(t.Data)
                        if tagName == "script" {
                                z.Next()
                                fmt.Println(string(z.Text()))
                        }
                }
        }

}

➜  vol.39 git:(master) ✗ cat index.html | gowk -v -i golang.org/x/net/html -r 'z := html.NewTokenizer(os.Stdin);for{tt := z.Next();switch tt {case html.ErrorToken:return;case html.StartTagToken:t:=z.Token();tagName := strings.ToLower(t.Data);if tagName == "style" {z.Next();fmt.Println(string(z.Text()))}}}'>index.css
2019/01/07 02:49:21 fixImports(filename=""), abs="/Users/yuichiro/go/src/github.com/ryuichiueda/ShellGeiData/vol.39", srcDir="/Users/yuichiro/go/src/github.com/ryuichiueda/ShellGeiData" ...
2019/01/07 02:49:21 package main

import (
        "fmt"
        "os"
        "strings"

        "golang.org/x/net/html"
)

func main() {

        z := html.NewTokenizer(os.Stdin)
        for {
                tt := z.Next()
                switch tt {
                case html.ErrorToken:
                        return
                case html.StartTagToken:
                        t := z.Token()
                        tagName := strings.ToLower(t.Data)
                        if tagName == "style" {
                                z.Next()
                                fmt.Println(string(z.Text()))
                        }
                }
        }

}

```

Q4とほぼ同じなのでGoのソースコードは省略。

## Q5

これもHTML分解なのだがだいぶ端折っている。外部参照してるscript/styleはないようなのでとりあえず飛ばせばいける。

```shell-session
➜  vol.39 git:(master) ✗ cat index.html | gowk -v -i golang.org/x/net/html -r 'z := html.NewTokenizer(os.Stdin);for{tt := z.Next();switch tt {case html.ErrorToken:return;case html.StartTagToken:t:=z.Token();tagName := strings.ToLower(t.Data);if tagName == "script" || tagName=="style" {z.Next();z.Next()};default: fmt.Print(string(z.Raw()))}}'
2019/01/07 09:56:35 fixImports(filename=""), abs="/Users/yuichiro/go/src/github.com/ryuichiueda/ShellGeiData/vol.39", srcDir="/Users/yuichiro/go/src/github.com/ryuichiueda/ShellGeiData" ...2019/01/07 09:56:35 package main

import (
        "fmt"
        "os"
        "strings"

        "golang.org/x/net/html"
)

func main() {

        z := html.NewTokenizer(os.Stdin)
        for {
                tt := z.Next()
                switch tt {
                case html.ErrorToken:
                        return
                case html.StartTagToken:
                        t := z.Token()
                        tagName := strings.ToLower(t.Data)
                        if tagName == "script" || tagName == "style" {
                                z.Next()
                                z.Next()
                        }
                default:
                        fmt.Print(string(z.Raw()))
                }
        }

}

<!doctype html>Google</title> &#26908;&#32034;</b> &#30011;&#20687;</a> &#12510;&#12483;&#12503;</a> Play</a> YouTube</a> &#12491;&#12517;&#12540;&#12473;</a> Gmail</a> &#12489;&#12521;&#12452;&#12502;</a> &#12418;&#12387;&#12392;&#35211;&#12427;</u> &raquo;</a></nobr></div></span></span></span>&#12454;&#12455;&#12502;&#23653;&#27508;</a> | &#35373;&#23450;</a> | &#12525;&#12464;&#12452;&#12531;</a></nobr></div></div></div> </div></div>&nbsp;</td></div></span></span></span></span></td>&#26908;&#32034;&#12458;&#12503;&#12471;&#12519;&#12531;</a>&#35328;&#35486;&#12484;&#12540;&#12523;</a></td></tr></table></form></div></div>&#24195;&#21578;&#25522;&#36617;</a>&#12499;&#12472;&#12493;&#12473; &#12477;&#12522;&#12517;&#12540;&#12471;&#12519;&#12531;</a>+Google</a>Google &#12395;&#12388;&#12356;&#12390;</a>Google.co.jp</a></div></div>&copy; 2019 - &#12503;&#12521;&#12452;&#12496;&#12471;&#12540;</a> - &#35215;&#32004;</a></p></span></center>     </body></html>⏎
```

Q4とほぼ同じなのでGoのソースコードは省略。

体力と時間が尽きた…。

# 感想
使うのがたいへんつらいので、短く簡潔に書くためのイディオム/ライブラリの考察とかには意外と役に立つのかもしれない。