2017/11/05


11/04、#vimconf2017 に参加してきました。

Vim の国際会議として開催される VimConf2017 に kaoriya さんからお誘いを受けたので参加してきました。電源は各席に付いていて、WiFi も完備、LAN ケーブルも繋がるし、会場も綺麗、弁当付き、コーヒー飲み放題、白い恋人、懇親会、LT、本当に本当に盛沢山で素晴らしい国際会議だったと思います。

Vim, Me and Community

haya14busa さんの Vim 人生を振り返り。haya14busa さんがコントリビュートしてきた EasyMotion、そして incsearch.vim というプラグインの機能紹介をしつつ、Vim 本体への incsearch の実装を分かりやすく説明してくれました。

僕ら古株 Vimmer には思いつかないアイデアだったと思います。Vim 初心者の方にも伝わる発表でした。

The Past and Future of Vim-go

vim-go の作者、fatih 氏による発表。僕は普段 Go言語をよく書いていますが、正直 vim-go が無いと Go を書くのが辛いくらい vim-go に依存してしまっているので、その vim-go の生い立ちを作者から聞けたのは良い体験でした。

Vim と Go の相性の良さが、vim-go を有名にしたと言って過言ではないでしょう。

Talk Show

kaoriya さん、k-takata さん、僕でトークショーをやってきました。僕と k-takata さんの Vim 使用歴の話から始まり、ここ最近の vim-jp の立ち位置、なぜ vim-dev でなく vim-jp で議論するのかといった話を皆さんにお伝えする事が出来て良かったと思います。

ちなみにこのトークショー前に壇上で皆さんの前で、すき焼き弁当を食べるという滅多にない体験をしてきました。貴重な体験でした。お弁当美味しかったです。

Creating your lovely color scheme

実は僕もカラースキームを作った事があります。Vim に同梱されている shine.vim (シャイン) というカラースキームです(「社員」でも「死ね」でもない)。ただその頃はカラーパレットを使って、色の色調を考えるといった事をしていなかったので、単にベース色の上に主張し過ぎない程度の色を選んだ程度の物でした。ちゃんと考えて作れば iceberg の様に綺麗なカラースキームを作れるんだなーと、良い勉強になりました。この発表は Vim に限らず色テーマを作る方であれば参考になる良い発表だっと思います。

vim-mode-plus: The most ambitious vim emulator in the world

Atom 上でありながら Vim という変則的な UI を作り出し、さらにその上でどの様に機能を足すかで新しい UX を作り出す事を考えていらっしゃる t9md さんらしい発表でした。僕たち開発者からは出て来ないかもしれないアイデアに目を見張る物がありました。0 から 1 を生み出そうとしている感が出ていて良かったです。

Vim and Compatibility

POSIX の在り方と Vim の在り方を照らし合わせた面白い発表でした。has() や exists() といった、Vim がバージョン相違問題の回避に使っている関数が出てきて、分かる人にはニヤニヤする発表でした。正直 Vim はバージョン毎に機能追加されたりデグレを起こしたりするソフトウェアです。ツギハギしながら成長した証が垣間見れます。

Shougo -Neosnippet.vim + Deoppet.nvim-

暗黒美夢王の発表。Shougo ware の近況報告と neocomplete.vim 終了のお知らせ。また neosnippet、deoppet について構想している内容について。個人的には UltiSnips や snipMate 等は、Vim の UX 上では開発しづらいと思っていて、僕が作っている sonictemplate.vim というプラグインはマーカーのリアルタイム編集は行っていません。ただ、僕らよりも補完に関する知見を多く持っている暗黒美夢王であれば、もしかするとまた新しい UX を作りだしてくれるかもしれません。

How ordinary Vim user contributed to Vim

dice_zu さんが Vim 本体に contribute した際の話。パッチは小さい物とご本人が仰っておられましたが、だれも直せなかったバグを直せた貢献度は、パッチの大きさには依存しないと思っています。とても良い発表で、僕の中ではこの日のベストトークでした。haya14busa さんの発表もそうでしたが、Vim のコミュニティは本体にコントリビュートしたいと思っておられる方が多く、vim-jp の志す所とマッチしていて本当に良い雰囲気となっているなと思いました。

The new syntax highlighter for Vim

Vim の syntax highlight を独自実装した話。言語のシンタックスを言語自身で AST に落とし、それを JSON で引き取って Vim の matchaddpos を使ってレンダリングするという方法。これも僕には思いつかなかった面白い方法だと思います。何か本体にフィードバック出来る物があれば、アイデアを持ち込んでみても良いのではないでしょうか。

You've been Super Viman. After this talk, you could say you are Super Viman 2 -- Life with gina.vim

alisue さんの gina.vim に関する発表。Vim の Git 関連のプラグインは数多く存在しますが、gina.vim の特徴的な部分を皆に伝える事が出来る良い発表だったと思います。僕は Git 関連のプラグインを使っていませんが、ぜひ今度試してみたいと思います。

懇親会

色々な方に喋りかけて頂けて頂き、本当に楽しめました。ありがとうございました。LT 飛び入りも楽しかったです。メルカリさんから配られた Vim バッジと Emacs バッジのうち、Emacs バッジの方が早く無くなったというのがこの日のハイライトでした。あとメルカリさんから Gopher のステッカーが配られており、さっそくノート PC に貼りました。

総評

実は今回、直接的なスタッフでは無かったのですが、サイトの更新等をお手伝いさせて頂いておりました。運営に関するお手伝いは出来なかったのですが、同じ slack で皆さんが色々な起きうるシーンを想定しながら、issue を書いて議論したり、名刺を作ったり、翻訳の為の打ち合わせをしたり、機材の準備をしたり、業者と連絡を取ったり、いろいろな準備をしているのを見て、本当に感謝の言葉しか出ない思いです。皆さん、本当にご苦労様でした。ありがとうございました。

おまけ

実は fatih を迎えるにあたり、当初外国人の渡航難しくなるのではという話が出ていたので、もしもの事を考えてスライドを作らせて頂いていました。

VimConf 2017 当日では運良くこのスライドが使われる事はありませんでしたが、せっかく作ったので発表形式ではありませんが公開したいと思います。


2017/10/26


printf デバッグは便利だ。技術の後退と言われようと printf でないと解決できない事はまだまだたくさんあります。

今日は net/http でクライアントが得たレスポンスの JSON を確認したいといった場合に、どうデバッグしたらいいかを書いてみたいと思う。

Go のインタフェースは大よそ io.Reader もしくは io.Writer を使う様に設計されている。こうする事でプログラムがメモリを一度に沢山確保してしまわない様にしています。

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
)

type Foo struct {
    ID  string `json:"id"`
    Content string `json:"content"`
}

func main() {
    resp, err := http.Get("http://example.com")
    if err != nil {
        log.Fatal(err)
    }
    defer resp.Body.Close()

    var foo Foo
    err = json.NewDecoder(resp.Body).Decode(&foo)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(foo.Content)
}

例えばこういうコードの、resp.Body に何が流れているのか確認したい場合、デバッグ出力する為に一旦 ioutil.ReadAll で全て読み取ったりしていないでしょうか。

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
)

type Foo struct {
    ID  string `json:"id"`
    Content string `json:"content"`
}

func main() {
    resp, err := http.Get("http://example.com")
    if err != nil {
        log.Fatal(err)
    }
    defer resp.Body.Close()

    b, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(string(b))

    var foo Foo
    err = json.NewDecoder(bytes.NewReader(b)).Decode(&foo)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(foo.Content)
}

デバッグ表示したいだけなのに、ちょっとコードが増えてしまった感じがしますよね。デバッグを無効にしたいときに消すコードも多い。しかも json.NewDecoder の部分にも手を入れてしまわないといけなくてなんだか嫌な感じもします。元のコードは json.NewDecoder の箇所に手を入れられるから良いですが、時には io.Reader を引数に持つ関数に渡す必要があったり、ioutil.ReadAll で全て読み取る事が出来ないストリームデータの場合には使えません。こういった場合は io.TeeReader を使います。

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"
    "os"
)

type Foo struct {
    ID  string `json:"id"`
    Content string `json:"content"`
}

func main() {
    resp, err := http.Get("http://example.com")
    if err != nil {
        log.Fatal(err)
    }
    defer resp.Body.Close()

    var r io.Reader = resp.Body
    r = io.TeeReader(r, os.Stderr)

    var foo Foo
    err = json.NewDecoder(r).Decode(&foo)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(foo.Content)
}

こうしておき、必要に応じて r = io.TeeReader(r, os.Stderr) の行をコメントアウトすれば良いのです。コメントアウトを外せばデバッグ表示になります。メモリも節約出来てお得感ありますね。


2017/10/24


たぶん逆引きが無いから探せないのかなと思ったので path/filepath にどういう機能があるのか書いておく。

パスからファイル名を得る

filepath.Base を使う。

package main

import (
    "path/filepath"
)

func main() {
    println(filepath.Base("C:/foo/bar"))
}

この場合 bar が表示される。

パスからディレクトリ名を得る

filepath.Dir を使う。

package main

import (
    "path/filepath"
)

func main() {
    println(filepath.Dir(filepath.Clean(`../foo\bar`)))
}

この場合、..\foo が表示される。

パスからボリューム名を得る

filepath.VolumeName を使う。

package main

import (
    "path/filepath"
)

func main() {
    println(filepath.VolumeName(`c:/windows/notepad.exe`))
}

この場合 c: が表示される。UNIX の場合は空文字列が返る。

相対パスから絶対パスに変換する

filepath.Abs を使う。

package main

import (
    "log"
    "path/filepath"
)

func main() {
    p, err := filepath.Abs("./testdata")
    if err != nil {
        log.Fatal(err)
    }
    println(p)
}

絶対パスから相対パスに変換する

filepath.Rel を使う。

package main

import (
    "log"
    "os"
    "path/filepath"
)

func main() {
    cwd, err := os.Getwd()
    if err != nil {
        log.Fatal(err)
    }
    p, err := filepath.Rel(cwd, `c:/dev`)
    if err != nil {
        log.Fatal(err)
    }
    println(p)
}

ディレクトリ配下であれば、それ以下の部分が。ディレクトリ配下でなければ .. で上昇した結果が返る。

パスを綺麗にする

../foo\bar\baz といった汚いパスを綺麗にするには filepath.Clean を使う。

package main

import (
    "path/filepath"
)

func main() {
    println(filepath.Clean(`../foo\bar`))
}

この場合、..\foo\bar が表示される。

シンボリックリンクのリンク元を得る

filepath.EvalSymlinks を使う。

package main

import (
    "log"
    "path/filepath"
)

func main() {
    p, err := filepath.EvalSymlinks(`c:/dirlink`)
    if err != nil {
        log.Fatal(err)
    }
    println(p)
}

Windows でも動作する。(ショートカットファイルではなくジャンクション)

パスから拡張子を得る

filepath.Ext を使う。

package main

import (
    "path/filepath"
)

func main() {
    println(filepath.Ext(`C:\Windows\Notepad.exe`))
}

.bashrc の様にドットで始まるファイル名を渡すと、ファイル名のまま返る。

スラッシュで区切られたパスを OS のパスセパレータに直す

filepath.FromSlash を使う。僕が path/filepath で一番好きな関数。

package main

import (
    "path/filepath"
)

func main() {
    println(filepath.FromSlash(`c:/users/mattn/.bashrc`))
}

この場合 c:\users\mattn\.bashrc が表示される。UNIX では何もしていない。Windows だけスラッシュがバックスラッシュに変換される。なので例えばファイルパスから URL のパスを作る時にこれを使ってくれると Windows ユーザが幸せになれる。

OS のパスセパレータで区切られたパスをスラッシュに直す

filepath.ToSlash を使う。filepath.FromSlash の逆。

package main

import (
    "path/filepath"
)

func main() {
    println(filepath.ToSlash(`c:\users\mattn\.bashrc`))
}

c:/users/mattn/.bashrc が表示される。

ファイルをマスクで検索する

filepath.Glob を使う。

package main

import (
    "log"
    "path/filepath"
)

func main() {
    files, err := filepath.Glob(`c:\Windows\*`)
    if err != nil {
        log.Fatal(err)
    }
    for _, f := range files {
        println(f)
    }
}

Windows の場合、バックスラッシュはエスケープ文字として扱われない。また ** は使えない。使いたい場合は zglob を使う。

パスの先頭に特定のディレクトリが含まれるか確認する

filepath.HasPrefix を使う。

package main

import (
    "path/filepath"
)

func main() {
    println(filepath.HasPrefix(`c:\Windows\Notepad.exe``c:\windows`))
}

気を付けないといけないのは、この動作は strings.HasPrefix でしかない事。この関数は deprecated として扱われている。

パスが絶対パスかを確認する

filepath.IsAbs を使う。

package main

import (
    "path/filepath"
)

func main() {
    println(filepath.IsAbs(`..\Notepad.exe`))
}

パスを結合する

filepath.Join を使う。

package main

import (
    "path/filepath"
)

func main() {
    println(filepath.Join(`c:\windows``system32``drivers``etc``hosts`))
}

OS のパスセパレータで結合される。可変個引数なので複数渡せる。

パスをディレクトリ名とファイル名に分解する

filepath.Split を使う。

package main

import (
    "path/filepath"
)

func main() {
    dir, filename := filepath.Split(`c:\windows\notepad.exe`)
    println(dir, filename)
}

c:\windows\notepad.exe に分けられる。

パスリストを分解する

PATH 環境変数の様に OS のパスリストセパレータで結合された物を分解する。filepath.SplitList を使う。

package main

import (
    "os"
    "path/filepath"
)

func main() {
    for _, p := range filepath.SplitList(os.Getenv("PATH")) {
        println(p)
    }
}

パスがパターンにマッチするか確認する

filepath.Match を使う。Glob が中で使っている物に過ぎない。

package main

import (
    "log"
    "path/filepath"
)

func main() {
    ok, err := filepath.Match(`*.exe``c:/windows/notepad.exe`)
    if err != nil {
        log.Fatal(err)
    }
    println(ok)
}

ディレクトリを下ってファイルを探索する

filepath.Walk を使う。例えば特定パス配下のディレクトリだけを探すのであれば以下の様に実行する。

package main

import (
    "log"
    "os"
    "path/filepath"
)

func main() {
    root := `c:\windows\system32\drivers\`
    err := filepath.Walk(root, func(p string, info os.FileInfo, err errorerror {
        if info.IsDir()  {
            println(p)
        }
        return nil
    })
    if err != nil {
        log.Fatal(err)
    }
}

探索を中断するには関数内でエラーを返す。もし特定のディレクトリ配下の探索をやめたいのであれば filepath.SkipDir を return で返してあげる。

なお物理ファイルの操作に path/filepath ではなく path を使うと爆発します。ちなみに、なぜここまで口をすっぱく言っているのかと言うと、UNIX で実装した物を Windows に持ってくると動かないからです。それどころかセキュリティ issue にもなり得る。

package main

import (
    "io"
    "net/http"
    "os"
    "path"
)

func main() {
    cwd, _ := os.Getwd()

    http.HandleFunc("/"func(w http.ResponseWriter, r *http.Request) {
        if ok, err := path.Match("/download/*", r.URL.Path); err != nil || !ok {
            http.NotFound(w, r)
            return
        }
        name := path.Join(cwd, r.URL.Path)
        f, err := os.Open(name)
        if err != nil {
            http.NotFound(w, r)
            return
        }
        defer f.Close()
        io.Copy(w, f)
    })
    http.ListenAndServe(":8080"nil)
}

何かをダウンロードさせるのにこういったコードを書いてしまうと、以下の様なリクエストでディレクトリトラバーサルが発生する。(正しくは http.ServeFile を使ってね)

http://localhost:8080/download/..%5cmain.go

これは Go のライブラリが悪い訳じゃない。こんなコードを書いた人が悪い。この辺は「みんなの Go 言語」にも書かれている。

みんなのGo言語[現場で使える実践テクニック] みんなのGo言語[現場で使える実践テクニック]
松木雅幸, mattn, 藤原俊一郎, 中島大一, 牧大輔, 鈴木健太
技術評論社 / (2016-09-09)
 
発送可能時間: