Unengineered Weblog

PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND

入力ソースを切り替えるキーバインドを ctrl + space に変えよう(と思ったが…)

入力モードを切り替えるキーバインドは何を設定していますか?

私はこれまで ctrl + \ で入力ソースを切り替えていたが、ターミナルで STOP シグナルを送るキーバインドとかぶっていることに気づいた。 普段 STOP シグナルを送ることはあまりないが、このような被りは誤操作を引き起こしそうだと考えてキーバインドをタイトルのように変更した。

そもそも ctrl + spaceMac のデフォルト設定なのだから、それに戻しただけである。 わざわざ変えていた理由は Emacs だ。 Emacs では ctrl + space は範囲選択に、ctrl + \Emacs の 入力ソースを切り替えになっていたのだ。 Emacs に干渉しないように設定したというわけである。

でも最近は Emacs を使わないから Mac のデフォルト設定に戻してしまおう。 Ubuntu もそれにあわせよう。めでたしめでたし

と思ったんだが…

この続きを読むには
購入して全文を読む

環境変数$SHELLの使い方を間違っていませんか?

要約

いま動いているインタラクティブシェルを知る方法に環境変数$SHELLを読むのが広く知られているが、これはこの環境変数の誤った使い方である。 $SHELLはユーザーのお気に入りのシェルを指定するものである($EDITORに近い)。 シェルは起動時に$SHELLの値をいまのシェルにセットするわけではない。 いま動いているシェルを知るには

ps -o comm= -p $$

とするのが良さそうだ。

Googleで「現在のシェル 確認」と検索すると

google:現在のシェル 確認

echo $SHELLでわかると書いてあるページが多い。たまにecho $SHELLではなくecho $0とやれと書いてある記事もある。

POSIXの説明

SHELL

This variable shall represent a pathname of the user's preferred command language interpreter. If this interpreter does not conform to the Shell Command Language in XCU Shell Command Language, utilities may behave differently from those described in POSIX.1-2017.

pubs.opengroup.org

この説明からわかるように$SHELLは現在の動いているシェルを表すものでは無い。

試してみる

簡単な実験で$SHELLが現在走っているシェルを示さないことがわかる。

# いまは zsh です。
% uname -a
Darwin Ryuichis-MacBook-Pro.local 22.4.0 Darwin Kernel Version 22.4.0: Mon Mar  6 20:59:58 PST 2023; root:xnu-8796.101.5~3/RELEASE_ARM64_T6020 arm64
% echo $SHELL
/bin/zsh
%
% bash # bashを起動するも、
$ echo $SHELL # $SHELLの値は変わらない。
/bin/zsh
$
$ ksh
$ echo $SHELL
/bin/zsh

このようにbashksh環境変数$SHELL/bin/bashに上書きしてくれるわけでは無い。 このようにいま動いているシェルと$SHELLが異なる状態で$SHELLからいま動いているシェルを知ろうとする(行儀の悪い)スクリプトsource.で読むとおかしい挙動をしたりする。

だったらどうやっていまのシェルを調べればよいのだろう。

$0は罠がある

$0C言語でのargv[0]を示すので、インタラクティブシェルでは$0は現在のシェルのパスを表す(シェルスクリプトでは起動したファイルの名前を表す)。

# bash
$ echo $0
bash

# ksh
$ echo $0
ksh

# sh
$ echo $0
sh

# dash
$ echo $0
dash

# zsh
% echo $0
-zsh

(最後のzshのようにログインシェルはprefixに-がついている。)

「これでいまのシェルを知ればいいじゃん」と思うじゃん? zshだけは起動ファイル~/.zshrc.でファイルを読んだ時にそのファイル名を表してしまう。

# echo0.source
echo $0
# bash
$ . ./echo0.source 
bash

# ksh
$ . ./echo0.source
ksh

# sh
$ . ./echo0.source
sh

# dash
$ . ./echo0.source
dash

# zsh だけ違う出力をする!
% . ./echo0.source
./echo0.source

こういった挙動の違いは.bashrc.zshrcを共通のファイルにしている人(そんなにいないかも🤔)にとっては困る。

どんなシェルでも同じ方法でいまのシェルを知る方法が欲しい。

いまのシェル名はpsから取得しよう

僕が思いついた方法はpsを使うことだ。 インターネットで調べると同じようにpsを使っていまのシェルを知る方法を紹介しているページがいくつか見つかる。

ps -o comm= -p $$
# or
ps -o args= -p $$
$ bash -i
$ ps -o comm= -p $$
bash
$ ps -o args= -p $$
bash -i

-o args=の場合は起動時の引数も表示するという違いがある。

これはPOSIXに規定されているコマンドオプションのみなのでLinuxmacOS、確認してないがほかのUnixでも動くだろう。 細かいオプションの説明はしないのでPOSIXのページでも見てほしい。

pubs.opengroup.org

ちなみに$$はシェルが展開する変数だ。だから例えばGoのos/execパッケージはシェルを経由しないで実行するので

exec.Command("ps", "-o", "comm=", "-p", "$$")

とやっても動かない(シェルが展開するグロブや変数を知らない人を稀によく観察する)。

実はps -o comm= -p $$が動かない環境がある。

busyboxのpsは-pオプションがない。 だから上の方法はAlpine Linuxでは動かない。 だから

ps -o pid=,comm= | awk -v pid=$$ '$1 == pid { print $2 }'

となるかなぁ。ちょっとめんどくさいね。

Goでスライスなどを無理やり比較する

Goには比較可能ではない型がある
  • スライス
  • 関数値
  • 上記のものを含んだ構造体など
package main

import (
	"fmt"
)

func main() {
	a := []int{1, 2, 3}
	b := []int{1, 2, 3}

	fmt.Println(a == b)
}
$ go run x.go
# command-line-arguments
./x.go:11:14: invalid operation: a == b (slice can only be compared to nil)

雑に使い捨てコードを書いているときにいちいちスライスの要素を一つ一つ比較する関数を書くのはめんどくさい。

無理やり比較する
package main

import (
	"fmt"
)

func main() {
	a := []int{1, 2, 3}
	b := []int{1, 2, 3}

	fmt.Printf("# %q == %q\n", a, b)
	fmt.Println(fmt.Sprintf("%q", a) == fmt.Sprintf("%q", b))
}
 go run x.go
# ['\x01' '\x02' '\x03'] == ['\x01' '\x02' '\x03']
true

比較したい値をSprintfによって文字列にする。その文字列の比較によってその値が等しいかを調べてしまう。

注意点

ただのハックなのでちゃんとしたコードを書く場面では使わないほうがいいだろう。

株式会社はてなに入社しました

株式会社はてなに入社しました

株式会社はてなに入社しました - hitode909の日記









本当です。23年度の新卒エンジニアとして入社します。

Plan 9にハマったら株式会社はてなに入社した話

Plan 9を調べていくうちに

blog.lufia.org

というブログを見つけて、そのブログの作者 id:lufiabb さんが株式会社はてなのエンジニアだったので株式会社はてなにエントリーしたら株式会社はてなに入社しました。

株式会社はてなではPlan 9に興味があるエンジニアを募集しています。

go generateでクオート、標準出入力を扱う。

まとめ

//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である。

参考文献

pkg.go.dev

Goのファイルシステムfs.FSを実装するときのヒント

pkg.go.dev

fs.FSを実装するには次のインターフェースを満たす具象型を実装しなければならない。

多いね。それぞれのメソッドとインターフェースの関係は次の図で表すことができる。 例えば、FSのメソッドOpenはFileを返す。

ファイルパスのルール

FSのファイルパスのルールはfsパッケージの関数ValidPathに書いてある。 ざっと次のとおり。

  • UTF-8エンコード
  • パスの区切りは/(例えば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}
    }
    ...
}

fs.ReadDirFileとfs.ReadDirFSの違い

ReadDirFileはFileにReadDirメソッドが追加されたインターフェース。 ReadDirFSはFSにReadDirメソッドが追加されたインターフェースである。 実装したファイルシステムにどちらかのインターフェースが備わっていればディレクトリの中身を読むことができる。 しかしながら好きな方どっちかを実装すればよいという訳ではない。 Goのドキュメントでは全てのディレクトリファイルはReadDirFileとして実装するべきと書いてある。 ReadDirFSはfsパッケージの関数ReadDir(上のメソッドとは区別せよ)を最適化するための補助的なインターフェースであって、ReadDirFSだからReadDirFileを実装しなくていいよねという話にはならない。

ちなみにnet/httpパッケージを使うとFSの静的ファイルサーバーを作れるが、ディレクトリファイルをReadDirFileとして実装しないとディレクトリの中身を読んでくれない。

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の違い

DirEntryもFileInfoもファイルの情報を保持するインターフェースである。 しかしなぜ同じようなものが2つあるのか。 ReadDirもDirEntryという劣化版ではなくて、FileInfoを返せばよいではないか。と疑問を感じるかもしれない。

これはUnixファイルシステムの設計が原因である。 Unixファイルシステムにおいてディレクトリの中身(DirEntry)とファイルの情報(FileInfo)は別々の領域に格納されている。 ディレクトリの中身を読み(readdir システムコール)、中身のファイルの情報を取得する(stat システムコール)という処理をすると2回システムコールを呼ぶ必要がある。 しかしながらディレクトリの中身を探索する処理ではたいてい DirEntry を読むだけで十分なことが多く、FileInfo を毎回呼ぶのは無駄である。 だからReadDirはFileInfoではなくDirEntryを返す実装になっている。

この経緯はGoのissueを読むとわかる。

github.com

ただオリジナルのファイルシステムを実装する際にこのような背景は知ったこっちゃないだろう。 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 }

fs.FileInfoを実装するときに注意すること

意味のない値の扱い

/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) }

POST RequestをハンドルするシンプルすぎるHTTPサーバー

github.com

Usage

postsrv [-flag] cmd [arg ...]

postsrvはとてもシンプルなHTTPサーバー。 カレントディレクトリをサーブするファイルサーバーとほとんど同じだが、/uploadはPOSTリクエストをハンドルする。 POSTリクエストのBodyが標準入力を通してcmdへ送られ、cmdの標準出力がレスポンスとなる。

Example

postsrv cat

とすれば、/uploadへPOSTしたフォームがそのままレスポンスとして返ってくるサーバーになる。 基本的にはカレントディレクトリに<form action='/upload' ...>タグを含んだindex.htmlをおいて使う。

また-mフラッグによってmultipart/form-data形式でPOSTされたファイルを扱える。ファイルが複数あるときはconcatenateされ標準入力へ送られる。

postsrv -m cat

なぜ作ったか

いま僕はJavaScriptのお勉強をしている。 このときPOSTリクエストをハンドルするサーバーをサクッと作れると便利だった。 毎回サーバーをイチから実装するのは面倒くさいので、シェルスクリプトの要領で簡単に作れるようにした。 このUnixコマンドへ入出力させるという単純だが強力な手法は応用が効きそうだなっていつも考えている。

応用

POSTされたデータをターミナルへ出力

postsrv sh -c 'cat > /dev/tty'

文字列操作

postsrv tr a-z A-Z

POSTされたテキストデータを大文字化して返す。

画像を操作する

postsrv -m convert - -negate -

画像ファイルをPOSTするとImageMagickのconvert(1)によって色が反転された画像が返ってくる。

POSTされたデータを保存して、レスポンスを返す

postsrv sh -c 'cat > $(mktemp); cat response'

POSTされたデータをランダムな名前のファイルに保存し、ファイルresponseに書いてあるレスポンスを返す。