getclip、putclip 「再考」nなGo!式あんす

awesome go巡りてたら思い出して、なネタである。

「getclip, putclip」というペアはこれは、「UNIX に基づきつつも Windows らしいコマンドも少しだけある cygwin」に「追加」されていたコマンド、だったと記憶している。あくまでも「記憶」であって、正確性を求めないで欲しい。ワタシにとっては「リアル UNIX ユーザだった頃には存在は認識したことがなく、cygwin ユーザになって初めて使うようになったもの」であり、なので「cygwin 固有なのだろう」と判断した、てこと。cygpath みたいにはっきりと cygwin 固有のものであるのかどうかは知らない。そして cygwin ユーザであることをやめて MSYS を愛するようになってしばらくのちに「そういえば getclip, putclip なんてあったよな」と思い返してみたのがこのネタだったわけね。

そのときの話の趣旨としては、「ないなら(python で)作っちまえばええやんけ」を「する必要がない」ということだった。putclip については、Windows XP 以降「clip」が標準付属になったことが理由で、そして getclip の方はそもそも使ってなかったよなと気付いた上に当面それをするユースケースを思いつかんてことね。

そしてここ何日かで続けてる MSYS, Go の話の中で awesome go を眺めていて…、クリップボードを扱うパッケージが目に入り、そういえば、と。「ここ何日かで続けてる MSYS, Go の話」で強調してきたのが「持ち運びやすさ」なわけよ、なので「Go で作られたもの」もしくは「Go であたいが作るもの」は特殊なランタイムを必要としない「ハンディな」道具たりうるわけよ、そこが「python で」実現しようとするのとの違いね。どうかしら、と。

実は最近になってから「標準出力に utf-8 で吐き出しちゃってからの clip」で困るようになったのも、まさに「Go で作ったプログラム」のせいだったりする。python だとデフォルトでロケールに従おうとする、つまり Windows 版 CPython を、Windows 10 の Japanese Edition な「デフォルト状態のコンソール」で実行しようとすれば、sys.stdout への出力は必然的に cp932 となる。これを clip で受け取るのは何も困らない。けれども Go で作るものは「cp932 固定」とか「ロケールに従って」とかってことは自動ではやろうとしないので、つまりは「素直に utf-8 で出力してしまう」ことが必然的に多くなる。これを Windows 標準の clip に流し込むと…、ほらいわんこっちゃない、てことだわな。

しかるに、「文字コード変換も担っちゃうような clip が欲しかね」てのを考えたわけだわ。python (の tkinter) でもすぐに作れるがそれをやろうとしなかった理由はいいよね、今言ったとおり。python プログラムばかり使ってる分にはそんなに困らなかった、てことと、「EXE 一個なので持ち運びやすい」てことね。

もちろん UNIX コマンドラインのノリで考えるなら nkf みたいなフィルタを考えるのもありなんだけれど、あんまり使う必要性に迫られないんだよね、今は。だって「utf-8 or cp932」の二択という形でしかほとんど文字コード問題って日常に現れなくなってるから。ので clip に組み込んじゃうのは便利かろうと。

「クリップボードにアクセスする」部分以外についての必要部品は全て「不完全過ぎる zip2tar、あるいは、zip なんかなくなってしまえばいいのに」(の追記部分)と「束にしてくりんなGo! (bundle, CLI)」で見つけてある。github.com/ogier/pflaggithub.com/axgle/mahonia ね。

で、本日の本題の「クリップボードにアクセスする部分」は、おーさんごーにはいくつか見つかるが、github.com/golang-design/clipboard でひとまず良いのではないかと判断した。これの「dependancies」はちょっと注意深く読んで欲しいのだが、「Windows では、cgo 未使用、依存物ナシ」ということよ、プラットフォームによって違うの。けれども、「オレにはそれでいい、それがいい」てことだよ。それと github.com/golang-design/clipboard はプリ定義の main を含んでいるので、「プログラミングを伴わずに使うことが出来る(gclip)」けれど、「文字コード変換」は範疇外なので、「ライブラリとして使っちゃるぜ」てことだよ。簡単だっ:

 1 package main
 2 
 3 import (
 4     "os"
 5     "io"
 6     "strings"
 7     "github.com/ogier/pflag"
 8     "github.com/axgle/mahonia"
 9     "golang.design/x/clipboard"  // github.com/golang-design/clipboard
10 )
11 
12 func init() {
13     err := clipboard.Init()
14     if err != nil {
15         panic(err)
16     }
17 }
18 
19 func main() {
20     ie := pflag.StringP(
21         "input_encoding", "i", "utf-8", "specify input encoding")
22     ousage := pflag.Usage
23     pflag.Usage = func() {
24         ousage()
25         os.Exit(1)
26     }
27     pflag.Parse()
28     icont, err := io.ReadAll(os.Stdin)  // require Go 1.16+
29     if err != nil {
30         panic(err)
31     }
32     if strings.ToLower(*ie) == "png" {
33         clipboard.Write(clipboard.FmtImage, icont)
34     } else {
35         dec := mahonia.NewDecoder(*ie)
36         ocont := []byte(dec.ConvertString(string(icont)))
37         clipboard.Write(clipboard.FmtText, ocont)
38     }
39 }

上流コマンドが cp932 を出力しちゃう方がノーマルのケースの方が Windows 生活なら多いと思うけど、それは Windows なら標準搭載の clip を使えばいいよね、てことで。

ちなみにこの話、正確に言えば「簡単だったはずなのに無駄な苦労をした」。ほんとは github.com/hellflame/argparse を使おうとしたの。したらこやつ、「positional 引数を持たないパターン」に対応してないんでやんの。つまり今作った「コマンドライン引数なしでいい」ケースで使えない。なんだそれ…。

ついでなので少し機能追加しとく。時々入力の改行コードを取り除いたりとかしたいことがあってね。プログラムの実例をクリップボードにコピーして、それを「bash コマンドラインに」入れたいときに、ときとして改行コードが邪魔なわけだ。あるいは html の「<br/>」に変えたいとかもありうるよね。そんなのにこたえるヤツ:

 1 package main
 2 
 3 import (
 4     "os"
 5     "io"
 6     "strings"
 7     "regexp"
 8     "github.com/ogier/pflag"
 9     "github.com/axgle/mahonia"
10     "golang.design/x/clipboard"  // github.com/golang-design/clipboard
11 )
12 
13 func init() {
14     err := clipboard.Init()
15     if err != nil {
16         panic(err)
17     }
18 }
19 
20 func main() {
21     ie := pflag.StringP(
22         "input_encoding", "i", "utf-8", "specify input encoding")
23     nl := pflag.StringP(
24         "replace_newline", "n", "\n", "replace newline to this")
25     ousage := pflag.Usage
26     pflag.Usage = func() {
27         ousage()
28         os.Exit(1)
29     }
30     pflag.Parse()
31     icont, err := io.ReadAll(os.Stdin)  // require Go 1.16+
32     if err != nil {
33         panic(err)
34     }
35     if strings.ToLower(*ie) == "png" {
36         clipboard.Write(clipboard.FmtImage, icont)
37     } else {
38         dec := mahonia.NewDecoder(*ie)
39         oconttxt := dec.ConvertString(string(icont))
40         oconttxt = regexp.MustCompile(`\r?\n`).ReplaceAllString(oconttxt, *nl)
41         ocont := []byte(oconttxt)
42         clipboard.Write(clipboard.FmtText, ocont)
43     }
44 }

うん、まぁ結構使いやすいかもしれんね。

ちぅかこれあれだよな、tee みたく標準出力にも吐き出しちゃう手もあるんだよなぁ、そうすると嬉しい場合ってあるだろうか? あるかもしれんなぁ。