株式会社はてなに入社しました
株式会社はてなに入社しました - hitode909の日記
本当です。23年度の新卒エンジニアとして入社します。
Plan 9にハマったら株式会社はてなに入社した話
Plan 9を調べていくうちに
というブログを見つけて、そのブログの作者 id:lufiabb さんが株式会社はてなのエンジニアだったので株式会社はてなにエントリーしたら株式会社はてなに入社しました。
株式会社はてなに入社しました
株式会社はてなに入社しました - hitode909の日記
本当です。23年度の新卒エンジニアとして入社します。
Plan 9を調べていくうちに
というブログを見つけて、そのブログの作者 id:lufiabb さんが株式会社はてなのエンジニアだったので株式会社はてなにエントリーしたら株式会社はてなに入社しました。
まとめ
//go:generate sh -c "m4 -DVAL=\"hello world\" hello.go.m4 > hello.go"
//go:generate
ディレクティブ以降に指定されたコマンドはシェルで実行されるわけではない。
だからシェルが提供する機能であるクオート、標準出入力は使えない。
ただgo generateはクオートの機能を提供している。シェルとは挙動が違う。
"
(ダブルクオート)で囲む。ちなみこのクオートされた文字列はgoの文字列リテラルと同じ働きをする。
シェルとは違う動作なので注意。
例えば次のようにすると、
//go:generate echo "\"hello\nworld\""
次のような出力をする。
"hello world"
この場合出力をするだけでgoコードは生成しない。意味のないコードである。
標準出入力をあつかうのはshを使えばよい。
//go:generate sh -c "m4 -DVAL=\"hello world\" hello.go.m4 > hello.go"
goのクオートとシェルのクオートのどちらにも気をつける必要がある。
VAL
の値は何だろうか?
-DVAL=\"hello world\"
の\
はgoによって取り除かれて、"
はシェルによって取り除かれる。
だからVAL
の値はhello world
である。
シングルクオートを使った方がいいかもしれない。
//go:generate sh -c "m4 -DVAL='hello world' hello.go.m4 > hello.go"
この場合もVAL
の値はhello world
である。
fs.FSを実装するには次のインターフェースを満たす具象型を実装しなければならない。
多いね。それぞれのメソッドとインターフェースの関係は次の図で表すことができる。 例えば、FSのメソッドOpenはFileを返す。
FSのファイルパスのルールはfsパッケージの関数ValidPathに書いてある。 ざっと次のとおり。
/
(例えばpath/to/file
)/
をつけてはいけない(/aaa
, aaa/
は許されない).
や..
は許されない(path//to/./dir/../or/file
は許されない).
で示す。これらはFSを実装しやすくするためのルールだ。 Openメソッドを実装するときには入力されたファイルパスが上のルールに従っているかVaildPathを使ってチェックすると良いだろう。
func (fsys *myfs) Open(name string) (fs.File, error) { if !fs.ValidPath(name) { return nil, &fs.PathError{"open", name, fs.ErrInvalid} } ... }
ReadDirFileはFileにReadDirメソッドが追加されたインターフェース。 ReadDirFSはFSにReadDirメソッドが追加されたインターフェースである。 実装したファイルシステムにどちらかのインターフェースが備わっていればディレクトリの中身を読むことができる。 しかしながら好きな方どっちかを実装すればよいという訳ではない。 Goのドキュメントでは全てのディレクトリファイルはReadDirFileとして実装するべきと書いてある。 ReadDirFSはfsパッケージの関数ReadDir(上のメソッドとは区別せよ)を最適化するための補助的なインターフェースであって、ReadDirFSだからReadDirFileを実装しなくていいよねという話にはならない。
ちなみにnet/httpパッケージを使うとFSの静的ファイルサーバーを作れるが、ディレクトリファイルをReadDirFileとして実装しないとディレクトリの中身を読んでくれない。
仕様がちょっと複雑だ。 archive/zipパッケージにはfs.FSが実装されていてzipファイルを解凍せずに中身をファイルシステムとして読むことができる。 そこのReadDirメソッドのコードは実装するときに参考になるかもしれない。
https://cs.opensource.google/go/go/+/refs/tags/go1.19.3:src/archive/zip/reader.go;l=884
DirEntryもFileInfoもファイルの情報を保持するインターフェースである。 しかしなぜ同じようなものが2つあるのか。 ReadDirもDirEntryという劣化版ではなくて、FileInfoを返せばよいではないか。と疑問を感じるかもしれない。
これはUnixのファイルシステムの設計が原因である。 Unixのファイルシステムにおいてディレクトリの中身(DirEntry)とファイルの情報(FileInfo)は別々の領域に格納されている。 ディレクトリの中身を読み(readdir システムコール)、中身のファイルの情報を取得する(stat システムコール)という処理をすると2回システムコールを呼ぶ必要がある。 しかしながらディレクトリの中身を探索する処理ではたいてい DirEntry を読むだけで十分なことが多く、FileInfo を毎回呼ぶのは無駄である。 だからReadDirはFileInfoではなくDirEntryを返す実装になっている。
この経緯はGoのissueを読むとわかる。
ただオリジナルのファイルシステムを実装する際にこのような背景は知ったこっちゃないだろう。 DirEntryとFileInfoは同じ構造体で実装してしまえばよい。
type info struct { name string size int64 mode fs.FileMode modTime time.Time } var _ fs.FileInfo = &info{} var _ fs.DirEntry = &info{} func (i *info) Name() string { return i.name } func (i *info) Size() int64 { return i.size } func (i *info) Mode() fs.FileMode { return i.mode } func (i *info) Type() fs.FileMode { return i.mode.Type() } func (i *info) ModTime() time.Time { return i.modTime } func (i *info) IsDir() bool { return i.mode.IsDir() } func (i *info) Info() (fs.FileInfo, error) { return i, nil } func (i *info) Sys() any { return nil }
/dev/urandom
のようなファイルでは大きさ(Size)や更新日時(ModTime)の情報は意味をなさない。
これは個人的な意見であるが、もし作ったファイルシステムを既存の Unix ソフトウェアと組み合わせるなら、更新日時をtime.Timeのゼロ値(つまりJanuary 1, year 1, 00:00:00.000000000 UTC)にしないほうがよいかもしれない。
Unixソフトウェアを組み合わせたときにバグを引き起こしてしまうかもしれないからだ。この場合はUnixエポックタイムでのゼロ秒を返すかな。
func (info) ModTime() time.Time { return time.Unix(0,0) }
postsrv [-flag] cmd [arg ...]
postsrvはとてもシンプルなHTTPサーバー。
カレントディレクトリをサーブするファイルサーバーとほとんど同じだが、/upload
はPOSTリクエストをハンドルする。
POSTリクエストのBodyが標準入力を通してcmdへ送られ、cmdの標準出力がレスポンスとなる。
postsrv cat
とすれば、/upload
へPOSTしたフォームがそのままレスポンスとして返ってくるサーバーになる。
基本的にはカレントディレクトリに<form action='/upload' ...>
タグを含んだindex.htmlをおいて使う。
また-m
フラッグによってmultipart/form-data形式でPOSTされたファイルを扱える。ファイルが複数あるときはconcatenateされ標準入力へ送られる。
postsrv -m cat
いま僕はJavaScriptのお勉強をしている。 このときPOSTリクエストをハンドルするサーバーをサクッと作れると便利だった。 毎回サーバーをイチから実装するのは面倒くさいので、シェルスクリプトの要領で簡単に作れるようにした。 このUnixコマンドへ入出力させるという単純だが強力な手法は応用が効きそうだなっていつも考えている。
postsrv sh -c 'cat > /dev/tty'
postsrv tr a-z A-Z
POSTされたテキストデータを大文字化して返す。
postsrv -m convert - -negate -
画像ファイルをPOSTするとImageMagickのconvert(1)によって色が反転された画像が返ってくる。
postsrv sh -c 'cat > $(mktemp); cat response'
POSTされたデータをランダムな名前のファイルに保存し、ファイルresponseに書いてあるレスポンスを返す。
Unixにはシステムコールやライブラリ(C言語)、コマンド(シェルスクリプト)などのインターフェースを定義したPOSIXという規格がある。移植性の高いソフトウェアの開発を容易にすることを目的としており、GNU/LinuxやmacOS, BSDなどはPOSIXにだいたい準拠するように実装されている。POSIXを意識してシェルスクリプトを書けば、GNU拡張やBSD拡張に混乱しないですむ。POSIXは下のサイトから参照できる。
手元のターミナルから参照できるようにすると便利だ。2つの方法でインストールできる。
次のサイトからPOSIXのmanのtroffファイルをダウンロードできる。
付属しているMakefileはLinux向けだ。makeでインストールしたPOSIXのman pagesはセクション0p, 1p, 3p下に置かれる。つまり
$ man 1p grep
とすればPOSIXのmanページが参照できる。
macOSのmanはこの方法では上手く行かない。理由はよくわからないが0p, 1p, 3pというセクションを認識してくれない。付属のMakefileを使わずに手動でやるか、Makefileを書き換えよう。ファイル名をgrep.1p
からposix:grep.1p
などと変え、/usr/local/share/man/man{1,3,7}
にインストールすればよいだろう(man0
はないので変わりにman7
)。そのときは
$ man 1 posix:grep
で参照できる。
パッケージマネージャーからインストールすれば楽だ。
aptからmanpages-posix
とmanpages-posix-dev
の2つのパッケージがあり両方インストールすれば良い。
$ man 1p grep # or $ man 1posix grep
で参照できる。
筆者はMacPortsを使っている。posix-manpages
のパッケージをインストールすればよい。
$ man 1 posix:grep
で参照できる。
探せばあると思う。
u9fsはPlan 9に付属している(のに)Unix向けのソフトウェアである。これはUnixのファイルシステムをPlan 9の通信プロトコル9Pでサーブし、Plan 9でマウントすることを目的としている。私は初め使い方が全く分からなかったので、私と同じ人のためにメモを残す。
u9fsのソースコードにはソケットを操作する部分が見当たらなかったため、初めて読んだとき(こいつはどうやって通信するんだ)と悩んだ。 答えをいうとこのソフトウェアはinetdを使って通信するのだ。 私は"若い" Unixユーザーなので最近あまり使われていないinetdを知らなかったのである。 inetdはsuffixのdが示すとおり、デーモンである。 使い方はWikipediaのページを参照してほしい。
要するにinetdは接続がacceptされた際に登録されたプログラムを起動し、そのプログラムの標準出入力をソケットにフックするのだ。 つまりinetdを使って通信するu9fsは標準出入力を通して9Pをサーブしているのである。 これがわかればinetdを使わなくても通信できる。
u9fsの使い方を示した日本語の記事を紹介しよう。
この記事の本題である。ちゃんと使うにはinetdを設定する必要があるが、面倒である。 ちょっと使うだけなら次の方法できるだろう。
一番簡単なのはnetcat (nc)を使う方法である。
fifo=/tmp/tmpfifo.$$ mkfifo $fifo ./u9fs -a none -l /dev/null -n -u $(id -un) < $fifo | nc -l localhost 5640 > $fifo
あまり用いられないnamed pipeの格好の利用例でもある(ちなみにmktemp(1)に似た一時的なnamed pipeを作るコマンドmktempfifoがある)。
設定は接続時の認証は無し(-a none
)、ログファイルは残さず(-l /dev/null
)、u9fsを通して作ったファイルのオーナーは自分(-u $(id -un)
)としている。
オプションについて詳しくはmanページを参照してほしい。
そして別のターミナルから接続してみよう(9pはplan9portにあるコマンドで、9Pのクライアントである)。
$ 9p -a 'tcp!localhost!5640' ls -l d-rwxr-xr-x M 0 root root 106496 Jul 23 12:45 bin d-rwxr-xr-x M 0 root root 4096 Jul 23 12:36 boot d-rwxrwxr-x M 0 root root 4096 Mar 2 2020 cdrom d-rwxr-xr-x M 0 root root 5120 Jul 23 15:13 dev d-rwxr-xr-x M 0 root root 12288 Jul 23 12:45 etc ...
この方法は一度しか接続できないという問題点がある。
これは私が作成した簡易版inetdである。とても単純なプログラムだ(Plan 9のユーザープログラムにこれに似たlisten1がある)。
$ listen -a 'localhost:5640' ./u9fs -a none -l /dev/null -n -u $(id -un)
と起動する。これは接続ごとにu9fsをexecするので同時に何度も接続することが可能である。
これは技術ブログでない。ただの日記だ。
皆様はPythonでUnixのコマンドcatを実装できますでしょうか?それっぽいのではなく、ちゃんと標準の/bin/catと同じ動作するものを作ろうとすると意外と引っかかる部分が多いと思います。
まず最も単純な実装(cat1.py)を見てみましょう。
#!/usr/bin/env python3 import sys BUFSIZE=1024 def cat(f): while True: b = f.read(BUFSIZE) if len(b) == 0: return print(b, end='') def main(): if len(sys.argv) == 1: cat(sys.stdin) else: for fname in sys.argv[1:]: with open(fname) as f: cat(f) main()
これは一応catっぽいことをします。引数にテキストファイルがあればそれらを、なければ標準入力をを標準出力に書き出します(標準的なcatは引数に-
を指定すると標準入力から読み取るという仕様がありますが、今回それは無視します。そもそも現代のUnixでは/dev/fd/1
を指定すればいいし)。
BUFSIZE
の大きさは適当に決めてしまいました。『詳細UNIXプログラミング』という書籍が参考になるかも。
これには問題点があって、Pythonは面倒なことにバイナリファイルとテキストファイルを区別します。
だからバイナリファイルを入力すると例外を起こしてしまいます。
$ ./cat1.py /bin/sh Traceback (most recent call last): File "./cat1.py", line 21, in <module> main() File "./cat1.py", line 19, in main cat(f) File "./cat1.py", line 8, in cat b = f.read(BUFSIZE) File "/usr/lib/python3.10/codecs.py", line 322, in decode (result, consumed) = self._buffer_decode(data, self.errors, final) UnicodeDecodeError: 'utf-8' codec can't decode byte 0xf0 in position 24: invalid continuation byte
ファイルをバイナリモードでオープンしないといけませんね。次のプログラム(cat2.py)に書き換えます。
#!/usr/bin/env python3 import sys BUFSIZE=1024 def cat(f): while True: b = f.read(BUFSIZE) if len(b) == 0: return sys.stdout.buffer.write(b) def main(): if len(sys.argv) == 1: cat(sys.stdin.buffer) else: for fname in sys.argv[1:]: with open(fname, "rb") as f: cat(f) main()
バイナリはsys.std{in,out}.buffer
を通して標準出入力します。また関数open
に"rb"
と引数を追加してバイナリモードでopenすることを指示しなければなりません。
これでバイナリファイルも扱えるようになりました。
$ ./cat2.py /bin/sh | sha256sum
4f291296e89b784cd35479fca606f228126e3641f5bcaee68dee36583d7c9483 -
$ ./cat2.py < /bin/sh | sha256sum
4f291296e89b784cd35479fca606f228126e3641f5bcaee68dee36583d7c9483 -
$ sha256sum /bin/sh
4f291296e89b784cd35479fca606f228126e3641f5bcaee68dee36583d7c9483 /bin/sh
catが実装できた。めでたしめでたし。
と、思いきやこれで終わりではありません。 Pythonはデフォルトで標準出入力やファイルをバッファリングします。このプログラムもバッファリングしているでしょう。しかしながらGNUやらPlan 9やらのcatは出入力をバッファリングしません。Python版でもバッファリングしないように実装したいですね。
バッファリングについて解説するのは省いて、バッファリングによって/bin/catとcat2.pyが異なる動作をする例を確認してみましょう。つぎのGoのプログラム(coproc)を使います。
package main import ( "fmt" "log" "os" "os/exec" ) func main() { if len(os.Args) < 2 { log.Fatal("no args") } c := exec.Command(os.Args[1], os.Args[2:]...) w, _ := c.StdinPipe() r, _ := c.StdoutPipe() if err := c.Start(); err != nil { log.Fatal(err) } nbuf := 100 buf := make([]byte, nbuf) greeting := []byte("hello\n") w.Write(greeting) fmt.Printf("input: %q (%d bytes)\n", greeting, len(greeting)) n, _ := r.Read(buf) fmt.Printf("output: %q (%d bytes)\n", buf[:n], n) greeting = []byte("bye\n") w.Write(greeting) fmt.Printf("input: %q (%d bytes)\n", greeting, len(greeting)) n, _ = r.Read(buf) fmt.Printf("output: %q (%d bytes)\n", buf[:n], n) w.Close() if err := c.Wait(); err != nil { log.Fatal(err) } }
このプログラムは引数に与えられたプログラムをfork/execで起動します。
coprocは起動したプロセスへパイプを通してhello\n
をwriteしたら100bytesぶんreadし、またbye\n
をwriteしたら100bytesぶんreadするといった単純なものです。
/bin/catと./cat2.pyで試してみましょう。
$ ./coproc /bin/cat input: "hello\n" (6 bytes) output: "hello\n" (6 bytes) input: "bye\n" (4 bytes) output: "bye\n" (4 bytes) $ ./coproc ./cat2.py input: "hello\n" (6 bytes)
/bin/catでは予想通りの結果が出力されプログラムが終了しました。 しかしながらcat2.pyは出力がされずプログラムが固まってしまいました。 なぜでしょう?
writeシステムコールは重いシステムコールと呼ばれ、実行が遅いです。
ちょっとの文字列しか出力しないprintf
のような関数で毎回writeシステムコールを呼んでいたらプログラムは遅くなってしまいます。
だからPythonのsys.std{in,out}.buffer
やCのstdio.hは出力するデータをバッファに一時的に貯め、まとまった量を一気にwriteします。
これをバッファリングといいます。
ちなみにCのstdio.hではデータを貯める量(バッファーサイズ)をBUFSIZ
として定義してあります。
readシステムコールも同様です。
すると上記の./coproc ./cat2.py
が固まってしまった理由がわかるでしょう。
coprocからパイプを通して書き込まれた6bytesのhello\n
はcat2.pyのバッファーサイズよりも小さいですから、cat2.pyはwriteせずにバッファに貯めます。
その状態でcoprocがパイプをreadしても読み込むものはありません。
通常この状態になるとreadシステムコールはプログラムをブロックします。
これによってcoprocは固まってしまったのです。
一方通常の/bin/catはバッファリングしていないですから、readしたらすぐにwriteします。 catはデータの加工をしませんから、まとまった量を一気にreadして、そのままwriteしちゃえばいいんです。 この点においてcatは一行ずつreadして加工をして一行ずつwriteする他のプログラム(grepやらsedやら)と異なります。
話をもとに戻しましょう。バッファリングしない完成版Python版cat(cat3.py)は次のようになります。
#!/usr/bin/env python3 import sys BUFSIZE=1024 def cat(f): while True: b = f.read(BUFSIZE) if len(b) == 0: return sys.stdout.buffer.raw.write(b) def main(): if len(sys.argv) == 1: cat(sys.stdin.buffer.raw) else: for fname in sys.argv[1:]: with open(fname, "rb", buffering=0) as f: cat(f) main()
sys.std{in,out}.buffer
の代わりにsys.std{in,out}.buffer.raw
を通して入出力します。
またopen
の引数にbuffering=0
が追加しました。
これをcoprocで起動してみましょう。
$ ./coproc ./cat3.py input: "hello\n" (6 bytes) output: "hello\n" (6 bytes) input: "bye\n" (4 bytes) output: "bye\n" (4 bytes)
ようやく/bin/catと同じ動作になりました。
Pythonでcatを実装するのも簡単ではないですね。
バッファリングやらread/writeシステムコールの知識も必要だし、これまでsys.std{in,out}.buffer.raw
なんか知らなかったし、Pythonも難しいですね。