Unengineered Weblog

PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND

最近の macOS の /usr/bin/diff は色をつけることができる

いつからかは分からないが、最近の macOS の /usr/bin/diff は色をつけるオプションが生えた。 記憶が曖昧だが過去のバージョンではこのオプションはなかった気がする。

使い方は簡単。 --color オプションをつけること。

下は macOS (Ventura 13.5.2) の diff の man ページから引っ張ってきたもの。

--color=[when]
       Color the additions green, and removals red, or the value in the DIFFCOLORS environment variable.  The
       possible values of when are “never”, “always” and “auto”.  auto will use color if the output is a tty
       and the COLORTERM environment variable is set to a non-empty string.

GNU diff と同じ文法である。

ちなみに macOSgrep や ls も色をつけることができる。同様に --color をつければよい。 以前のバージョンの macOS では ls に色をつけるのは -G しかなかった記憶。

毎回 --color をつけるのは面倒臭い。こういうときは alias を使う。 私の ~/.zhrc には

alias grep='grep --color=auto'
alias ls='ls -G'
alias diff='diff --color=auto'

と書いてある。

Linux でファイルがどのファイルシステムにあるのか調べるのは簡単ではなかった

GNU/Linux で df の挙動を見てみよう。引数に(デバイスファイルではなく、普通の)ファイル名を渡すと、それが保存してあるファイルシステムを探してきて、デバイスファイルやマウントポイントなどそのファイルシステムの情報を表示する。

$ df Makefile
Filesystem      1K-blocks       Used  Available Use% Mounted on
/dev/sda1       477514616  106381680  346803108  24%  /

実はこの挙動「ファイルが保存されてあるファイルシステムを返す」をするシステムコールは無い。 df はマウントしているファイルシステムの一覧がある /etc/mtab から探しているのである。

/etc/mtab は次のようなファイルだ

sysfs /sys sysfs rw,nosuid,nodev,noexec,relatime 0 0
proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0
...
/dev/sdb2 / ext4 rw,relatime 0 0
/dev/sda1 /home/ ext4 rw,relatime 0 0
...

マウントしているファイルシステムが一行ごとに書いてある。一行には左からスペース区切りで、ファイルシステムの名前(デバイスファイル)、マウントしてあるディレクトリ(マウントポイント)、あとは細かい情報(いまからの説明に関係がないので端折る)が書いてある。 ちなみに文法は /etc/fstab と同じである。

それでは df がどのようにファイルシステムを探しているのか見てみよう。 GNU のコードを読むのは大変なので、 busybox のコードを読んでみよう。まさにファイルがどのファイルシステムにあるのか調べる関数 find_mount_point という関数がある。これはファイル名を渡すと、それがあるファイルシステムの情報を構造体 のポインタ struct mntent* を返す。この構造体は /etc/mtab の一行をパースしただけである。

github.com

アルゴリズムを端折って説明すると、ファイルの stat から得られるデバイス ID (st_dev) と一致するマウントポイントのディレクトリを /etc/mtab から一行一行読んで探し、そこからファイルシステムを特定している。

なんて泥臭いんだ! 「ファイルが保存されてあるファイルシステムを探す」ためのシステムコールがあると思っていた僕は面食らってしまった。タイトルどおり簡単ではなかった。まあ難しくもないが。

ちなみに macOS では statfs システムコールを使えば一発でわかる。

Bash が環境変数を勝手に変えてくる!

ネタバレ

犯人は BASH_ENV

私がシェルスクリプトを書いていたとき、 bash が勝手に環境変数を変えてくるという謎の挙動をした。なぜなのか調べた。

どんなことが起こったのか

私が書いていたシェルスクリプトは複雑で本題とは関係ない。話を簡単にするために、次のシェルスクリプト hello を書いていたとしよう。 環境変数 NAME へ挨拶するスクリプトだ。

#!/bin/bash
echo "hello, $NAME"

そして次のように実行した。すると意図しない挙動をする。

$ NAME=alice ./hello
hello, bob
🤔

環境変数 NAME は alice だったはず! なぜ bash環境変数を勝手に変えてくるのだろうか。

状況はどうであったか

このシェルの環境変数を見てみると怪しいものがある。

$ printenv
...
BASH_ENV=/path/to/name
....

そして気になる /path/to/name の中身は次の通り。

NAME=bob

BASH_ENV とは

BASH_ENV

If this variable is set when Bash is invoked to execute a shell script, its value is expanded and used as the name of a startup file to read before executing the script.

www.gnu.org

環境変数 BASH_ENV がセットしていたとき、bash スクリプトが実行されると、スクリプトが実行される前にファイル BASH_ENV が読まれるそうだ。

つまり、上の name スクリプトは /path/to/name によって環境変数 NAME が上書きされてしまったのである。

BASH_ENV を unset したら意図した挙動になった。

$ unset BASH_ENV
$ NAME=alice ./hello
hello, alice

なぜ BASH_ENV がセットされていたのか

ある GitHub リポジトリにあった Dockerfile で BASH_ENV が使われていたのをよくわからずにコピペして動かしていたから。よくわからないものをコピペして動かすのはよくないね。

入力ソースを切り替えるキーバインドを 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に興味があるエンジニアを募集しています。