たいちょーの雑記

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

GoでGoogle Spreadsheetにデータを書き込みたい

面倒なことは機械にやらせよう

こんな記事は死ぬほどあるけど他に書くこともない

動機

研究でめっちゃデータ取ってる。もともとデータの集計も手作業でやる系だったんだけど、この部分はシェルスクリプトとかで自動化してMarkdownの表を生成するまではやってた。しかし報告するときにMarkdownの表は見づらいみたいなことをBOSSと同期に言われたので集めてるデータをGoogle Spreadsheetで管理しようと思った。
今見たらこのMarkdownの表、自分でみてもわかりにくい

要件

書き込みたいデータ

double
int
int
int
int
int
int
int
int
int

先頭が少数、その後に9つの整数が縦に続く。これを書き込みたい。
いろんなパラメータを書く欄があるのでカラムはEから始まる。行は4から。Zまで書き込んだあとはEへ戻って下の行へ移動する。

できれば次の行への移動とかも自動でやってほしい。

使う言語

最近GoをはじめたのでGoでやってみようと思った。SpreadSheetはAPIを公開してて、Go向けにもライブラリがあったのでこれでいいやみたいな

developers.google.comhttps://developers.google.com/sheets/api/quickstart/go

作ってみる

ためす

まずは試す。

qiita.com

この解説を一通りやってみると大体の感覚が解る。ちなみにAPIを試すだけならAPIのリファレンスの各ページからも試せる。API試せるのめっちゃありがたい

リファレンスだと自動入力が効いてるのでRequestのbodyとかの書き方をチェックするのにも使えて最高

メソッドを選ぶ

developers.google.comhttps://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/batchUpdate

今回はspreadsheets.values.batchUpdateを選んだUpdateをまとめてできるらしい。今の所1回につき1セットのデータしか書き込まないけど、そのうち拡張するかもしれないのでこっちにしておいた。

bodyをみていると、dataっていうところがあった。data便利な単語だと思う。
それで、data にはValueRange型を好きな数書くといいみたい。ValueRange型についてはリファレンスとか解説記事を見るとわかやすい

qiita.com
developers.google.comhttps://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values#ValueRange

今回最低限書き込みに必要な情報だけまとめるとこうなった

{
    "valueInputOption": "USER_ENTERED",
    "spreadsheetId": "xxxxxxxxxxxxxxxxxxxxxxxxxx",
    "responses": [
        {
          "range":"E4:E13",
          "majorDimension":"COLUMNS",
          "values":[
                double,
                int,
                int,
                int,
                int,
                int,
                int,
                int,
                int,
                int,
         ],
     },
 ],
}

これをライブラリを使って投げてやればいいだけですね

実装してみる

とりあえずリファレンスのQuickStartをコピペ。かいてあるとおりにやったらAPIにアクセスできるようになった。便利な世の中やでホンマ

ここからはgoを雰囲気でかいているところが散見するのでマサカリが有ったらブンブン投げてほしいところ

変更したところはgetClientを少し変えたぐらい

func getClient(ctx context.Context,credentialFile string) *http.Client {
    b, err := ioutil.ReadFile(credentialFile)
    if err != nil {
        log.Fatalf("Unable to read client secret file: %v", err)
    }

    config, err := google.ConfigFromJSON(b, "https://www.googleapis.com/auth/spreadsheets")
    if err != nil {
        log.Fatalf("Unable to parse client secret file to config: %v", err)
    }
    tokFile := "token.json"
    tok, err := tokenFromFile(tokFile)
    if err != nil {
        tok = getTokenFromWeb(config)
        saveToken(tokFile, tok)
    }
    return config.Client(ctx, tok)
}

contextを受け取れるようにしただけw

次にmain部分をかいていく

func main() {
    spreadsheetId := ""
    ctx := context.Background()
    client := getClient(ctx,"client_secret.json")

    sheetService, err := sheets.New(client)
    if err != nil{
        log.Fatal(err)
    }


    data := []*sheets.ValueRange{
        {
            Range: "E4:E13"
            Values: [][]interface{}{
                0.123,
                1,
                2,
                3,
                4,
                5,
                6,
                7,
                8,
                9,
            },
            MajorDimension: "COLUMNS",
        },
    }

    reqest := &sheets.BatchUpdateValuesRequest{
        ValueInputOption: "USER_ENTERED",
        Data: data,
    }

    res, err := sheetService.Spreadsheets.Values.BatchUpdate(spreadsheetId,reqest).Context(ctx).Do()
    if err != nil{
        log.Fatal(err)
    }
 

    fmt.Printf("%#v\n",res)
}

typescriptをかいてたときも思ったけど。こうやってソースの途中にJSONみたいな記述ができるってのはすごく良いと思う。どう良いかっていうのは説明できない

とりあえずこのままgo runすれば

0.123
1
2
3
4
5
6
7
8
9

みたいなデータがかけるようになったうれしい

もっと自動化する

書き込めて嬉しいけどこれだと書き込みたいデータが変わるごとにソースを更新しないといけない。しかも手作業
ぼくは人間なので手作業をN回すれば絶対1度はミスを犯すのでここも自動にしたいわけです

とりあえずbodyをJSONから読み込めるようにします。

type Body struct {
  Start  int            `json:"Start"`
  End    int            `json:"End"`
  Column string         `json:"Column"`
  Data   []interface{}  `json:"Data"`
}

これはencoding/json パッケージを使えば下みたいなJSONファイルと簡単にやり取りできます

{
  "Start":4,
  "End":13,
  "Column":"E",
  "Data":[
    1,2,3,4,4,5,6,7,8,9
  ]
}

goはこういうところが良い感じですね。すき

リクエストを送るときは書き込みたいデータが既にまとまっているとして、終了時にはリセットしておきます。書き込みたいデータはシェルスクリプトからjqを使って書き込むことにします
つまり

書き込みたいとき

{
  "Start":4,
  "End":13,
  "Column":"E",
  "Data":[
    1,2,3,4,4,5,6,7,8,9
  ]
}

終わるとき。Dataにはjqでデータを書き込むことにします

{
  "Start":4,
  "End":13,
  "Column":"F",
  "Data":[]
}

こうしたいので下みたいに書いてみたよ

func writeNext(b *Body) {
    cur := b.Column
    var next string
    st := b.Start
    ed := b.End

    if cur == "Z" {
        next = "E"
        st = ed + 2
        ed = st + 9
    } else {
        next = string([]byte(cur)[0]+1)
    }
    wd := Body{
        Column: next,
        Data: []interface{}{},
        Start: st,
        End: ed,
    }
    path := "next.json"
    jb, err := json.Marshal(wd)
    if err != nil{
        log.Fatal(err)
    }
    ioutil.WriteFile(path,jb,0644)
}

読み込み側はこうだな

func readNextJson() *Body{
    path := "next.json"
    b, err := ioutil.ReadFile(path)
    if err != nil {
        log.Fatal(err)
    }
    var readData Body
    if err := json.Unmarshal(b,&readData); err != nil{
        log.Fatal(err)
    }
    return &readData
}

こうやって修正したmainがこういう感じになった

func main() {
    spreadsheetId := "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    ctx := context.Background()
    client := getClient(ctx,"client_secret.json")

    sheetService, err := sheets.New(client)
    if err != nil{
        log.Fatal(err)
    }

    rj := readNextJson()

    data := []*sheets.ValueRange{
        {
            Range: fmt.Sprintf("%s%d:%s%d",rj.Column,rj.Start,rj.Column,rj.End),
            Values: [][]interface{}{
                rj.Data,
            },
            MajorDimension: "COLUMNS",
        },
    }

    reqest := &sheets.BatchUpdateValuesRequest{
        ValueInputOption: "USER_ENTERED",
        Data: data,
    }

    res, err := sheetService.Spreadsheets.Values.BatchUpdate(spreadsheetId,reqest).Context(ctx).Do()
    if err != nil{
        log.Fatal(err)
    }
 

    fmt.Printf("%#v\n",res)

    writeNext(rj)
}

あとは好きなタイミングでこれをgo runすればいいですね。はー楽になった

終わり

Vim向けのGoプラグインが優秀なこともあって、Goはなかなか楽に書けるな〜と思いました。そのうちデータの集計とかもGoにまとめたいですね

Surface3にUbuntu Budgie 17.10をインストールする

windowsしんどいっす

ってsurface3が言ってた

surface3にUbuntu Budgie 17.10をインストールする

2年ほど前に中古でsurface3を買いました。価格は5万円ぐらいだったと思います。
買った時にはすでに尿液晶で、白いUIだと尿まみれでした

最近、強いノートPCを支給され、winマシンは要らなくなったのでsurfaceUbuntuマシンにしようと思っていろいろ迷走したのでその備忘録です
先に行っておくと、なんでうごいてるのかわからないという内容の記事ですので注意です。

SurfaceLinux

www.reddit.com

超有益な情報が集まっているredditがありました。名前も超そのままだ・・・

ざっくりやったこと

  1. Ubuntu 17.10を普通にインストール。しばらく使っていたらWi-Fiが死んだので終了
  2. Ubuntu MATE 17.10をインストール。こんどはSurfaceLinuxのGUIDEに沿ってやってみるも不安定なWi-Fi
  3. Ubuntu Budgie 17.10をインストール。後述のppaで追加できるカスタムカーネルを試そうとしたら16.04までだった。GUIDEを試して再起動すると画面がつかなくなった
  4. LiveUSBつくるのがダルかったのでUbuntu Budgie 17.10をもう一度インストール。GUIDEはすっ飛ばした。なぜか安定しているのでここで終わり

採用したやつ

OSはUbuntu Budgie 17.10を使いました。

ubuntubudgie.org

PlankTilixがデフォでインストールされているとか、何もしなくても黒UIなところとかシンプルで綺麗とかもあるんですが、コイツでしかちゃんと動いてないっていうのもあります。

これの前にUbuntu,Ubuntu MATEを試したんですがダメでした。僕はどちらも17.10をインストールしたんですが、redditにある解決策やGUIDEを試してもうまく動きませんでした。

Budgie 17.10だとインストールした瞬間から安定して動作していました。というわけでそれについてです

LiveUSBを作る

Ubuntu Budgie 17.10のLiveUSBを作ります。作り方は解説記事がたくさんあるのでそちらに任せます。

ubuntubudgie.org

インストールする

LiveUSBからUbuntu Budgieを起動するにはSecureBootをOFFにする必要があります。
BIOSを出すときは、WindowsでShiftキーを押しながら再起動ボタンを押せば楽にいけて便利です

Ubuntu Budgieが起動したらインストールします。今回はWindowsとお別れするのでディスクを削除してインストールしました。
Ubuntuのアップデートを同時にインストールしましたし、MP3とかも一緒にやりました

インストールしたあと

IBus

なんかIBusが死んでいます。このへんのツールにこだわりはないので動きそうなFcitxに乗り換えます。

kledgeb.blogspot.jp

Ubuntu Budgieじゃないとき

やはり普通のPCではないのでいろいろ普通に問題が山積みです。例えばWi-Fiが突然死したり、画面がすごい勢いで明滅します。

www.reddit.com

16.04をインストールする場合は上のリンク先が非常に役に立ちます。ppaを追加してインストールできるkernalが超いい感じらしいです(試せてない)

サスペンドできない

これは解決策がGUIDEに書いてあります。試してないので試したら追記します

バッテリー情報が取得できない

できないです。SurfaceLinux内でも話題に上がっていることの一つでした。どうやらカーネルのバグらしく・・・
106231 – Call-By-Reference-Constant - battery and power adapter not working, MSHW0011 driver needed, even in windows - Surface 3

パッチはできてるみたいで、PRもmergeされたぜ!みたいなこと書いててカーネル4.13に入るかもな!やったー!カスタムカーネル無しで動くわね!って感じだったけど結局revertされたらしい。

ここで配布されているパッチ当てればなおるのかな?英語もパソコンもなんもわからん

第35回シェル芸勉強会に参加しました

めちゃくちゃだよお・・・

第35回またまためでたいシェル芸勉強会の大阪サテライトに参加してきました。
この日大阪は寒かったので久しぶりにウインドブレーカーを取り出したんですがストリーミング配信の先は半袖だったので驚きました・・・

今回もはちゃめちゃな問題が多くて勉強になりましたし、倒れました。

【問題と解答】jus共催 第35回またまためでたいシェル芸勉強会

せっかく参加して何問か解けたので残しておこうと思います。想定解のはTogetterなりを見てくださいね。

問1

curl parrot.liveから流れてくるアニメーションをファイルに保存して再生する問題。いきなりあたまおかC

ファイルの種類に制限がなかったのでスクリーンキャプチャしてgifにしました(反則)

想定解ではawksleepしながら1行ずつ出力するものでした。awkってsleepもできちゃうんですね知りませんでした。

ところでこのアニメーションめっちゃ面白いっすね・・・

問2

数値と文字がペアになっている複数行のファイルから数値の行に文字を表示する。ファイルに存在しない数値の行は空行。

cat herohero | sed 'y/1234567890/1234567890/' | sed -E 's/(へ|ろ)/ \1/g'| awk '{x[$1]=$2}END{for(i=1;i<=13;i++) print x[i]}'

awk連想配列を使えば楽だなーと思ったので整形してからawkへもっていきました。呟いた後で他の方の解答を見ていたんですがsed 'y/.../'のところはnkf -Zの方がスマートでいいですね。さらにsed -Eの部分もろ|へをキーにするんじゃなくて[0-9]+正規表現の方がいいですね。勉強になります。

あとはsedc-f-も初めて知ったのでこれから使えたらカッコいいですね

問3

また数値と文字がペアになっている複数行のファイルがあるので、これを今度は集計する。
集計自体は難しくないけど出力の成形が少し面倒らしい

cat data |sort -k 1|uniq -c|awk '{x[$2]=x[$2]" "$3":"$1}END{for(i in x) print i,x[i]}'

直前のことが頭に残ってたのでawk連想配列を使うパターンで解きました。

dataを集計すると

$ cat data | sort | uniq -c
2 1 A
3 1 B
1 2 A
3 2 C
3 3 B
1 3 C
1 3 D
1 4 C

となるので、2レコード目をキーにして出力を整形しました。これは簡単(基準がおかしくなっています)
別解ではyarrというコマンドがあるのでそれで一発だよ。というのがありました。

後で気づいたんですけですけどsort-k 1はいらないっすね・・・。

問4

ひらがなで名前っぽい単語をランダムに生成する問題。名前っぽいとはみたいな

zsh -c "echo {あ..ん}"|xargs -n1|shuf -n6|tr -d '\n'|sed 's/.../& /'|awk 1

最初に作ったのがこれでした。zshのブレース展開で平仮名を作って6個選んで3つずつに分けて名前っぽく・・・

なりませんでした(なるわけがない)

なので次は名前ランキングみたいなのからスクレイピングすることにしました。

curl https://st.benesse.ne.jp/ninshin/name/2017/name-ranking.html | grep -oP "[あ-ん]+"|awk 'length($1)==3'

日本語だけ取り出した後、3文字のやつは名前と決め打ちで抽出しました。けっこうそれっぽかった

ゆうま
ひなた
あおい
みなと
そうま
はると
はると
そうた
あさひ
そうた
ゆうと
ゆいと
あおい
やまと
...

fakerを使うのかなーとか思っていたら適当に列挙して、mecabに通した後人名だけ取り出すというパワーシェル芸が想定解だったみたいです。すごい

問5

echo 響け!ユーフォニアムからはじめて下みたいな出力を得る。ぼくはこのアニメ見てました

響け!ユーフォニアム
 響け!ユォニアム
  響け!ニアム
   響けアム
    響ム
     
     
    ム響
   ムアけ響
  ムアニ!け響
 ムアニォユ!け響
ムアニォフーユ!け響

響け!ユーフォニアム問題は様々なパターンでの出題がありますが一問も解けたことがないです。これもそうでした。あたまいたい
少し前のLTでもあったように少しずつ解に近づけていく方法で行こうと思いましたが時間切れ。
想定解はsubstrでゴリゴリ削ってゴリゴリ並べて上を作った後、下の三角形をrevtacで作るものでした。このときmoreutilsに含まれているpeeを使うと楽だよというのを知りました。teeの出力先がコマンド版みたいなやつです。前々から欲しかったコマンドなので知れてうれしかったです。どうでもいいんですけどpeeっておしっこですよね

この問題は他の方の解答を眺めていました。vimシェル芸とかホントすごい・・・

問6

素因数分解したときに23以下の数だけで構成される自然数を1985個だす問題。急に数学かよ!?と思ったら数学オリンピックからの引用だそうで。競プロにもこんな問題ありそう

これは正規表現を書いていたら頭おかしくなってました。前問のせいですかね。
想定解では、factorは小さい順に素因数が出力されるのでawk$NFを見て23以下かを見るだけでOKのようですね。かしこい

問7

文字列の素数番目を読むと意味を成すような文字列を作る問題。楽そう(基準がおかしくなっています)

join <(seq 50|factor|awk 'NF==2{print $2}'|cat -n) <(echo 響け!ユーフォニアム|grep -o .|cat -n)| awk '{x[$2]=$3}END{for(i=0;i<=29;i++)print x[i]==""?"ユ":x[i]}'|tr -d '\n'|awk 1

響け!ユーフォニアム問題が解けなかった腹いせに響け!ユーフォニアムでやってみました

ユユ響けユ!ユユユユユーユフユユユォユニユユユアユユユユユム

10番目の素数が29なのでそこまでの配列をawkで作って素数じゃないところはユで埋めます。いままでjoinでやってきましたがpasteのが便利なんじゃないかというね?
せっかくsedcを覚えたので使って解いたのがこっちです

seq 10000|factor|awk 'NF==2{print $2}'|paste - <(echo 響け!ユーフォニアム|grep -o .)|head -n10|awk '{print $1"c"$2}'|sed -f- <(yes)|head -n29|sed 's/y/ユ/g'|tr -d '\n' |awk 1

問8

問6で作った自然数リストから4つ選んで掛け算したときの値がある値の4乗になっている組み合わせを1つみつける問題。全部探せだと無理だなってなった

最初誤読していて、リストの中から4乗の数を探すのかと思って解いたのがこれです。

$ yes|awk '{print "echo $(("NR"*"NR"*"NR"*"NR"))"}'|head -n30|bash|while read L;do cat a|grep -w $L;done
1
16
81
256
625
1296
2401
4096
6561
10000
14641

これは本当の答えではないんですが、ここから4つ選んで掛け算すれば4乗の数が作れるのでそれでOKということですね。

yes|awk '{print "echo $(("NR"*"NR"*"NR"*"NR"))"}'|head -n12|bash|xargs -I@ grep -w @ a|shuf -n4|xargs|sed 's/ /*/g'|bc|factor

4つランダムに取り出して計算、factorにかけます。これも何とか解けました。

1から1985個の条件に合う自然数20,111が最後なので4乗の数は11個しかないです。これなら結局全列挙できそうですねこれ
ところで記事を書いてて思ったんですが、awkで計算式を作ってbashに計算させてますがこれawkで完結しますよね・・・

yes | awk '{print NR*NR*NR*NR}' | head -n12 | shuf -n4 | xargs | sed 's/ /*/g' | bc | factor

LT

今回も難読化シェル芸でLTさせていただきました!!!難読化シェル芸だいすき!時間があったら詳細を記事にしてみたいとかおもったりします!せかしてくれたら早めにやると思います!!

www.slideshare.net

はじめはkanataさんによるA,z,記号,1,2でやる難読化シェル芸をみて「ヤバすぎ」とおもったんですが、面白そうだったので少しやってみたらなんかすごいことになりました。

C#のラムダとかで

list.Select(_=>_....)

みたいに書くのでそれがずっと頭に残ってました。だからどうとかではないんですが、頭はおかしくなりました。

何人かの方に気に入っていただけたようでとっても嬉しいです。いつか__で会話する日が来るといいですね(来ない)

まとめ

今回もとても楽しかったです!いつも大阪会場を用意してくださる方、主催の方々、ありがとうございました!(名前を出していいのかわからないのでぼかしています)

競プロはじめてから1年ぐらいたった

楽しい

でもつらいときもある

1年ぐらい経ちました

去年のこれぐらいの時期にAtCoderに登録したような気がします。(今使っているアカウントは4月末のコンテストから参加してたので正確な日がわからん)

コンテストには合計で43回参加してるみたいで、レートは水色一歩手前で揺れてますね。こう見ると成長がえっちらおっちらですねこりゃ。
1

コンテスト用のリポジトリはもいい感じ(?)にcommitが増えてきました。
github.com

なので覚えてることとかをせっかくなので書いてみようと思いました。

出会い

同期にICPCへ誘われて始めました。今思えば自分にはちょっとハードルの高い始め方だったようにも感じます。
自分はC#が好きなのでそれでやろうと思っていたんですが、ICPCC#が使えないのでC++でやってました。(早く使えるようになって)

ICPCまでは同期にC++教えてもらったり、ABCに出ていたりしていました。このころはSTLや競プロ的な問題文や解説の言い回しに慣れなくて四苦八苦してた覚えがあります。弊学の競プロサークルでは毎週バチャコンを行い終わった後に解説し合う、というのをやっていましたがみんなの解説が解らなくて例の顔みたいになっていました。

このころはとけてもABCのBまで・・・とかで、ようやくC問題やICPCの過去問の300?問題を読んで長考して解けるぐらいになったころに本番が来てしまってました。

ICPC

自分はA問題を担当しました。Aを解いた後はB,Cをメンバーが解いてる間にDを読んだりしてました。ICPCはチーム戦で、ggれないしPC1台しか使えないしとかで勝手が解らずにぶっ倒れてたことしか覚えてません。

結果は5X位ぐらいだった気がします。

そのあと

ICPCの後はせっかく始めたし、ということで続けていくことにしました。ここからはC#を使いました。C++でやってる時にLINQがあればな~と思うことが多々あったので・・・それなりにC#も速いですし。Visual Studioのインテリセンスがはちゃめちゃ強いですし。

ただジャッジサーバがだいたいmonoなのでこちらが未来のC#を書いてることが結構あるのが辛いところでした。(とくにAOJのやつ)

とりあえずAtCoderのC問題ぐらいがコンテスト中に安定して解けるように目指して過去問埋めをやっていました。まあ色んな所に書いてあるんですが、C問題ともなると競プロ的な知識がないと解けない感じだったので躓いていました。これは9月ぐらいだったと思います(レートのグラフもそうなってますね)

Cがちょっとわかる

C問題がちょこちょこ解けるようになってきたころが10月ぐらいからでした。なにか切っ掛けになるようなことがあった・・・のかは思い出せませんが気づいたらチョットワカルになってました。あえていうなら問題に対して「考えようとする気持ち」が一番高かった気がします。

コンテスト中にCまでは解けるのでD・・・と思って問題をみて無言になるのを繰り返していました。

最近

全然できません。昔聞いていたラジオで、「成長は階段みたいなもんで、なかなか伸びないとき、それは踊り場にいるだけだからもう少し頑張ろうな」って言ってたのを思い出します。完全にそれを感じてます。

400点を安定して解けるようにと思って400点埋めをしたんですが、これがなかなか。
これぐらいになるとやはり必要になる知識の幅が広くなってきます。今ある400を埋めきったところで次のコンテストをクリアできるかっていうとうーん・・・と思ってしまいます。

before
twitter.com

after
twitter.com

これが10月ごろと同じようにまた伸びへ転換してくれればいいんですが・・・!

あと自分の苦手な問題もよくわかってきた気がします。例えば

これですね・・・・こういうなんか・・・算数?みたいな問題が苦手です。算数サボったからかな・・・

偉そうで申し訳ないんですが、AtCoderの最近の問題はまた難しくなった気がします(質が上がったともいう)。
DijkstraやるだけUnion-Findするだけみたいなのは減って、それらを使うためにどうやって状態を減らすかとかが必要な気がします。まあこれが上手く思いつかないんですが

あとは考察が終わっても書くのが遅いのでこれも何とかしないと・・・

これから

やっぱコンテスト中とかに解けないと普通に凹むんですよね。めちゃくちゃ悔しいんですよ。他の人は解いてるのに己は・・・とか
終わった後は解説とか他の人の提出を見て「天才~~~!」とか「次は倒す!」と思うようにしています。ゲームってすぐクリアできても面白くないですからね。

次の1年はとりあえず水色になって、それからはまあその時考えます。

VisualStudioから棒読みちゃんを喋らせるやつ

VisualStudioから棒読みちゃんを喋らせるやつ

VOICEROID買っちゃってました

前々からVOICEROID気になってて、ネット記事とか読み上げてほしいなあ~って思ってたんです
まあでも諭吉さんぐらいはかかっちゃうわけで。「悩む理由が値段なら買え」っていうのはたまに聞きますが、やっぱ考えちゃうんです買った後のこと
別に動画とか投稿しないだろうし(これは大変だって聞くし)、これといった用途も思いつかなかったので伸ばし伸ばしにしてたんです

ところがそんなときAHSからVOICEROID2 紲星あかりが発表されまして
www.ah-soft.comhttp://www.ah-soft.com/voiceroid/akari/

めっちゃかわいいじゃないですか

めっちゃかわいいんです

しかも発売日が誕生日と同じだったんです

買うしかないじゃないですか

で発売日にDL版を購入したんです。で、しゃべらせてみたらスゲー可愛いじゃないですか
毎日いろいろ喋らせてウフフって思ってました

でも、僕オタクなんで
このエディタにコリコリ文字入力させるだけじゃ物足りなくなってきたんですね
シェル芸・・・とかWLSでの重い処理やVisual Studioのビルドとかが終わったらあかりボイスでお知らせしてくれたら嬉しいじゃないですか
嬉しいんですよ
じゃあできるようにしたいじゃないですか

色々調べていたら棒読みちゃんプラグインでVOICEROIDに飛ばせる奴があると
chi.usamimi.info
ch.nicovideo.jp

天才か。危うくまた車輪の再開発をするところだった。
この棒読みちゃんにタスクを投げればプラグインを介してあかりちゃんがしゃべるわけですね

さらに棒読みちゃんにはコンソールから棒読みちゃんに発言タスクを投げるやつが同梱されてました。天才じゃん
なのでWLSの重い処理が終わったときにあかりボイスが聞ける最高の環境は出来たんです
ですがVisual Studioのビルド終了時にも喋ってほしいんですよ。けどそんなのないので(システム音を弄る方法はあるが)

自分で作るしかないんですよおおおおおお

VSBouyomi

作った拡張機能の名前はVSBouyomiです。何のひねりもない名前ですが

github.com
アイコンとかほしいんですけどなんも思いつかなかった・・・

Visual Studio Market上に公開しようと思ったんですがなんかエラーがでてできませんでした。泣こう

VSBouyomiの機能

VSBouyomiはタイトルに書いた通り、Visual Studioから棒読みちゃんを喋らせる拡張機能です
Visual Studioのいくつかのイベントが発生したときに任意の文字列を棒読みちゃんへ送ることができます。今のバージョンでは棒読みちゃんのパラメータを弄ることはできません VisualStudioから棒読みちゃんへは棒読みちゃん側が用意してくれているTCP通信機能を使っています

インストール

  1. GitHubからリポジトリをクローン。もしくはvsixをダウンロードしてくる
    1. クローンしたならお手持ちのVisual Studioでビルドして下さい
  2. ビルドしたvsixもしくは、ダウンロードしたvsixをダブルクリックしてVisual Studioにインストールする

使い方

インストールされたVisual Studioツールバーを右クリックします。すると
toolbar
VSBouyomi Activateという項目があると思うのでクリック

ツールバーにボタンが追加されます
button
このボタンが押されるとVSBouyomiが有効になり、もう一度押すと無効化されます。あとはVSBouyomiが勝手にやります

設定

初期設定ではビルドが成功したときに「ビルド成功です」とだけ言いますが、これを変えたいときもあると思います
そういう時は[ツール]=>[オプション]からVSBouyomiの設定を開きます

option1

各設定項目の詳細は以下です

ネットワーク設定

項目 説明
IPアドレス 棒読みちゃんIPアドレスです
ポート番号 棒読みちゃんのポート番号です

棒読みちゃんTCP通信するときに使うパラメータです。デフォルトでは127.0.0150001が入っています

発言内容設定

どれがどのときに発言する内容なのかは項目を見てもらえばわかると思うので割愛です。ここではその詳細設定を書きます

option2

発言内容はbool値とstringの組み合わせでできています

項目 説明
すぐに? イベントが発生したら棒読みちゃんにたまっているタスクを取り消して直ぐに読み上げるかどうかです
テキスト 読ませたいテキストを入力します。空白の場合は読み上げを行いません

この程度のシンプルな設定にしたのは 細かい設定はVOICEROIDとプラグイン側でやれよ と思ったからです
最初は拾えるイベントは拾えるだけ拾って発言できるタイミングを増やしたり、WPFを使ったイケイケな設定画面とかも作っていたんですがだんだん 「いるんかこれ・・・」 となってしまったので全部バイバイしました

これでVisual Studioがあかりちゃんの声でしゃべるようになったので他の痛IDE拡張と組み合わせれば

ide

コード書いてる時もワクワクですね!(立ち絵はMtU氏のものです

今後のうんぬん

この間TypeScriptを触ることがあったのでVSCodeへVSBouyomiを移植してもいいかなあと思っています。まあ気が向いたらですね

VisualStudioの設定ダイアログを作る

またVSIX

ほぼ備忘録になってる
まぁぼくは忘れやすいのでOKOK

それにしてもsurface3みたいなスペックだと実験インスタンス起動するのに時間かかってつらい

Visual Studioの設定ダイアログを作る

メニューバーの[ツール][オプション]で開くやつ正式名称はわからない・・・

d1
こういうやつ

これを自分の作ったVSIX用に作る

参考はここ

Creating an Options Page

プロジェクトを作る

今回は空のVSPackageに作っていく。自分のVSIXに実装するときは新しくVSPackageを追加する必要はない

まずは[ファイル]-[新規作成]-[プロジェクト]-[Visual C#]-[Extensibility]-[VSIX Project]を選んでプロジェクトを作る

できたらソリューションエクスプローラーで右クリックして[追加]-[新しい項目]
d2

Visual Studio Packageを選ぶ

d2

テンプレートが展開されるまでちょっと待つ

実装する

ダイアログページを作るにはSystem.ComponentModel名前空間が必要なのでusingする

using System.ComponentModel;

次にオプションで設定したい値を持ってるクラスを作る。今回はOptionDialogクラスにした。ネーミングセンス・・・・

public class OptionDialog : DialogPage {

}

このクラスにはDialogPageクラスを継承する必要があるのに気を付けよう

次にPackageクラスを継承してるクラスに属性を追加する。たぶんこのVSIXはオプションページがありますよ~~~ってことなんだと思うたぶん

/*他の属性は略*/
[ProvideOptionPage(typeof(OptionDialog),"Test Dialog","General",0,0,true)]
public sealed class Dialog : Package {
    /*略*/
}

d2

ちなみにこの属性の引数はこんな感じらしい。categoryNameはオプションウィンドウのサイドバーの項目で、pageNameはその下の全般とかのことっぽい

今のままだと設定したい項目が0なので追加していく今回はIntを1つもたせることにした

public class OptionDialog : DialogPage {
    [Category("かてごりー")]
    [DisplayName("ひょうじめい")]
    [Description("せつめい")]
    public int Num { get; set; }
}

DialogPageではプロパティとかにした値がそのまま設定項目として表示される。XmlとかJsonシリアライズしたりするのと似ていてちょっとうれしかった

実は実装はこれで終わりで、あとはビルドするだけでちゃんと動くのだ!最高!

d2

プロパティにつけていた属性の意味は上の画像と照らし合わせればわかると思う。これは僕も驚いたところだけど、これ値を変えてから再起動しても値が変更されたままになる。どこに書き出しているかはリファレンスをちゃんと読んでないのでわからない(読む気もない)。まあとにかく簡単である

xmlに書き出してみる

xmlファイルの準備

こういうのはjsonとかxmlに書き出しておきたいっていうのがあります(あります)
なので今回はxmlに書き出すのを目標にしてみる

まずはソリューションエクスプローラーからxmlを追加しておく。そして一度自力で書くか、適当なコンソールアプリケーションを書いてこのxmlを書いておく。書くのはOptionDialogクラスと同じようなプロパティを持ったクラスをシリアライズしたやつ。こういう時にC#インタラクティブが便利だと思うんだけどXmlSerializerがroslynでは死ぬのでダメだった・・・ざんねん・・・
なので自分はいつも以下みたいなのを書いてる。面倒

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Serialization;

namespace ConsoleApp2 {
    class Program {
        static void Main(string[] args) {
            var path = @"c:\users\user\source\repos\TestDialog\TestDialog\Resources\config.xml";
            var xml = new XmlSerializer(typeof(Store));
            using(var sw=new StreamWriter(path, false, Encoding.UTF8)) {
                xml.Serialize(sw, new Store { Num = 0 });
            }
        }

    }
        public class Store {
            public int Num { get; set; }
        }

}

追加したxmlはプロパティウィンドウでビルドアクションをコンテンツに、Include in VSIXをTrueにしておくこと。これが分からなくてINF分失った

Load/Saveイベント

またイベントの話だけどダイアログが開かれたとき、または閉じられたときにLoadとSaveが行われる。DialogPageにはこれをするLoad/SaveSettingsToStorageメソッドがあるのでオーバーライドしてやるといい

パスの取得

さっきビルドした拡張機能がどこにインストールされてるのか探してみたらC:\Users\USER\AppData\Local\Microsoft\VisualStudio\15.0_29065328Exp\Extensions\xztaityozx\TestDialog\1.0にあった。上の設定をしていればこの下にxmlも置かれるのでこのパスが分からないといけない。でもユーザー名とか変わるし決め打ちはできないのでどうにかして実行中に取得したい

ggっていたらドンピシャなstackoverflowを見つけた
stackoverflow.com

CodeBase?ってやつを取得すると実行中のファイルがわかるっぽい
そしてどうやらPackageクラスにも同じような値が入っているらしいけど何階層か上までしかわからなかったのでダメだった

なのでこんな風にかいた

public class OptionDialog : DialogPage {
    [Category("かてごりー")]
    [DisplayName("ひょうじめい")]
    [Description("せつめい")]
    public int Num { get; set; }

    public string Path {
        get {
            var codebase = typeof(Dialog).Assembly.CodeBase;
            var uri = new Uri(codebase, UriKind.Absolute);
            return System.IO.Path.GetDirectoryName(uri.LocalPath) + "\\Resources\\config.xml";
        }
    }

    public override void LoadSettingsFromStorage() {

        using (var sr = new StreamReader(Path, Encoding.UTF8)) {
            var xmlSerializer = new System.Xml.Serialization.XmlSerializer(typeof(Store));
            var config = (Store)xmlSerializer.Deserialize(sr);
            this.Num = config.Num;
        }
    }

    public override void SaveSettingsToStorage() {
        var config = new Store {
            Num = this.Num
        };
        using (var sw = new StreamWriter(Path, false, Encoding.UTF8)) {
            var xmlSerializer = new System.Xml.Serialization.XmlSerializer(typeof(Store));
            xmlSerializer.Serialize(sw, config);
        }
    }
}
public class Store {
    public int Num { get; set; }
}

これでxmlに書き出せるようになったのでVSIX中の任意の場所から同じようにxmlにアクセスすれば色々できてうれしい

それでは最後にソースコードをはって終わります

using System;
using System.ComponentModel.Design;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.OLE.Interop;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.Win32;
using System.ComponentModel;
using System.IO;
using System.Text;

namespace TestDialog {
    /// <summary>
    /// This is the class that implements the package exposed by this assembly.
    /// </summary>
    /// <remarks>
    /// <para>
    /// The minimum requirement for a class to be considered a valid package for Visual Studio
    /// is to implement the IVsPackage interface and register itself with the shell.
    /// This package uses the helper classes defined inside the Managed Package Framework (MPF)
    /// to do it: it derives from the Package class that provides the implementation of the
    /// IVsPackage interface and uses the registration attributes defined in the framework to
    /// register itself and its components with the shell. These attributes tell the pkgdef creation
    /// utility what data to put into .pkgdef file.
    /// </para>
    /// <para>
    /// To get loaded into VS, the package must be referred by &lt;Asset Type="Microsoft.VisualStudio.VsPackage" ...&gt; in .vsixmanifest file.
    /// </para>
    /// </remarks>
    [PackageRegistration(UseManagedResourcesOnly = true)]
    [InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)] // Info on this package for Help/About
    [Guid(Dialog.PackageGuidString)]
    [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1650:ElementDocumentationMustBeSpelledCorrectly", Justification = "pkgdef, VS and vsixmanifest are valid VS terms")]
    [ProvideOptionPage(typeof(OptionDialog),"Test Dialog","General",0,0,true)]
    public sealed class Dialog : Package {
        /// <summary>
        /// Dialog GUID string.
        /// </summary>
        public const string PackageGuidString = "3e22428e-3305-4a4b-9c3e-11c54c2bef28";

        /// <summary>
        /// Initializes a new instance of the <see cref="Dialog"/> class.
        /// </summary>
        public Dialog() {
            // Inside this method you can place any initialization code that does not require
            // any Visual Studio service because at this point the package object is created but
            // not sited yet inside Visual Studio environment. The place to do all the other
            // initialization is the Initialize method.
        }

       #region Package Members

        /// <summary>
        /// Initialization of the package; this method is called right after the package is sited, so this is the place
        /// where you can put all the initialization code that rely on services provided by VisualStudio.
        /// </summary>
        protected override void Initialize() {
            base.Initialize();
        }

       #endregion
    }

    public class OptionDialog : DialogPage {
        [Category("かてごりー")]
        [DisplayName("ひょうじめい")]
        [Description("せつめい")]
        public int Num { get; set; }

        public string Path {
            get {
                var codebase = typeof(Dialog).Assembly.CodeBase;
                var uri = new Uri(codebase, UriKind.Absolute);
                return System.IO.Path.GetDirectoryName(uri.LocalPath) + "\\Resources\\config.xml";
            }
        }

        public override void LoadSettingsFromStorage() {
            
            using (var sr = new StreamReader(Path, Encoding.UTF8)) {
                var xmlSerializer = new System.Xml.Serialization.XmlSerializer(typeof(Store));
                var config = (Store)xmlSerializer.Deserialize(sr);
                this.Num = config.Num;
            }
        }

        public override void SaveSettingsToStorage() {
            var config = new Store {
                Num = this.Num
            };
            using (var sw = new StreamWriter(Path, false, Encoding.UTF8)) {
                var xmlSerializer = new System.Xml.Serialization.XmlSerializer(typeof(Store));
                xmlSerializer.Serialize(sw, config);
            }
        }
    }
    public class Store {
        public int Num { get; set; }
    }
}

Visual Studioのビルド成功時にメモ帳を起動する

VisualStudioのビルド成功時にメモ帳を起動する

おいVSIX

Visual Studio拡張機能を少しずつ作ってる。なんもわからん

開発対象にしてるのはVS2017

Visual Studioのビルド成功時にメモ帳を起動する

今回はビルド成功時に発火するイベントを用いて色々するやつ。ビルド成功したときにメモ帳が出て何がうれしいんだ?って感じだけど分かりやすい例としてね

めちゃくちゃ分かりやすいリファレンス(怒)はここ
BuildEventsClass Class

OnBuildDone/Beginイベント

OnBuildDoneはビルドが終了したときに発火するイベントで、OnBuildBeginはビルドが始まったときに発火するイベント。まあそのままですね

この二つに登録できるメソッドは第一引数にvsBuildScope列挙型と第二引数にvsBuildAction列挙型を持ってるやつだけ。つまり以下みたいなやつ

private void OnBuildDoneEvent(vsBuildScope scope,vsBuildAction action){
    //なんかの処理
}

vsBuildScope 列挙型はリファレンスによると Represents the scope of the build. らしい。ふーんなるほどねすべて理解した。それとvsBuildAction列挙型は同リファレンスによるとRepresents the type of build action that is occurring, such as a build or a deploy action.らしい。なるほどなるほど

こういうのはインテリセンス見たほうがはやいんですよ

buildevent2.PNG

vsBuildScopeはどうやらバッチビルドなのかソリューションのビルドなのかプロジェクトのビルドなのかを示してるらしい。そのままだったw

buildevent3.PNG

vsBuildActionはどんなビルドだったのかが分かるらしい。これもそのままだったw ていうかよくみたらビルドメニューのドロップダウンと一緒だった

buildevent4.PNG

OnBuildProjConfigDone/Beginイベント

こいつらはOnBuildDone/Beginとほぼ同じタイミング(こっちのが先)で発生するが、ソースコードが変更されたなどでビルドし直さないといけないときにしか発生しない。つまり

buildevent1.PNG

こんな風にビルド時[出力ウインドウ]に正常終了が出てる時しか発火しない。1文字もソースを変更せずにビルドしたときは変更不要になるのでこのイベントは起きない

これに登録できるメソッドは引数が結構多いから下を見てくれ

private void OnBuildProjConfigDoneEvent(
                    string Project,
                    string ProjectConfig,
                    string Platform,
                    string SolutionConfig, 
                    bool   Success
) {
        //挟みこみたい処理
}

割と引数の名前が分かりやすいので何が入ってるかわかる。個人的に重要だと思ってるのは最後のbool値。変数名からわかると思うけどSuccessにはビルドが成功したかが入ってる。今回はこれがtrueの時にメモ帳を起動すればOKということ

というわけでビルド成功時にメモ帳を起動したいなら

private void OnBuildProjConfigDoneEvent(
                    string Project,
                    string ProjectConfig,
                    string Platform,
                    string SolutionConfig, 
                    bool   Success
) {
    if(!Success) return;
    new System.Diagnostics.Process { 
        StartInfo = { 
            FileName = "notepad.exe" 
        } 
    }.Start();
}

たぶんこんな感じになる

イベントハンドラを登録する

作ったメソッドをイベントハンドラとして登録して実際に動かしたい

上4つのイベントはEnvDTE.BuildEventsインターフェイスにあるのでそれをどこからか探してくる必要がある。これにはPackageクラスを継承しているクラスでGetService()メソッドをつかってEnvDTE.DTEインターフェイスを取得してそこから~~・・・・・文章書くのも難しいのでコードを張るね

protected override void Initialize() {
    var dte = (DTE)GetService(typeof(DTE));    //<--DTEインターフェイス取得
    //dteを辿って OnBuildProjConfigDoneにハンドラ登録する。
    dte.Events.BuildEvents.OnBuildProjConfigDone+=OnBuildProjConfigDoneEvent;
}

VSIXを作るとコンストラクタの代わりに上みたいなメソッドを書くのでそこで登録すればいいと思うんだけど、実はこれではダメなんですよ

BuildEventsを持っておかないとダメ

ぼくもこれで動くと思ってたがうんともすんとも言わなくて泣いた。でも色々調べてたら下のにたどり着いた

social.msdn.microsoft.comhttps://social.msdn.microsoft.com/Forums/vstudio/en-US/33ab218f-3e7b-4dd9-b2a2-d5df04fefa6f/why-is-onbuilddone-event-not-firing?forum=vsx

どうやらPackageクラスの継承先で参照を持ってないとダメらしい。VS2017のテンプレで作成したなら{ProjectName}Package.scってソースだ(たぶん)

なので

public BuildEvents BuildEvent {get;set;}
protected override void Initialize() {
    var dte = (DTE)GetService(typeof(DTE));    //<--DTEインターフェイス取得
    //dteを辿ってBuildEvents取得
    this.BuildEvents=dte.Events.BuildEvents;
    //はい
    this.BuildEvents.OnBuildProjConfigDone+=OnBuildProjConfigDoneEvent;
}

これでOK

img

おっ起動したな

OnBuildDoneのときもメモ帳起動したい

今のままだとソースが変更されていて、新たにビルドしたときしかメモ帳が起動しないのは簡単にわかると思う。
ぼくは変更不要の時もメモ帳起動してほしかったのでOnBuildDoneにも登録したかったけど二重起動しちゃうし、そもそもOnBuildDoneは失敗時にも発火するのでダメ!ぼくは成功時だけがいいの!

まあでもこれはboolをフィールドに持っていれば済む話で

private bool lastStatus = true;

private void OnBuildDoneEvent(vsBuildScope sc,vsBuildAction ac){
    if(lastStatus) WakeNotepad();
}

private void OnBuildProjConfigDoneEvent(/* 略 */){
    lastStatus=Success;
}

private void WakeNotepad() => new System.Diagnostics.Process { StartInfo = { FileName = "notepad.exe" } }.Start();

上にも少し書いたけどOnBuildDoneOnBuildProjConfigDoneより後に発火するのでこうやって書ける

これをそれぞれのイベントに登録すればおわり!
BuildEventsを持っておかないとだめっていう罠にどっぷりハマって死んでいたけど、これが分かってしまえば他のイベントでも同じようにすることになるのでやった~~~という感じですね

ちなみに他のイベントをまとめたクラスを作っておくとPackageクラスから参照を渡すだけでいいので楽です

今回はここまで