無精で短気で傲慢なプログラマ

UNIX や web やプログラムの技術的なことを中心に。

find には sh!

ウノウラボ Unoh Labs: コマンドラインで作業する上で知っておくといいテクニック
% find . -name '*~' -exec rm {} \;

404 Blog Not Found:勝手に添削 - find(1)
% find . -type f -name '*~' | xargs rm
% find . -type f -name '*~' | perl -nle unlink

わたしなら
% find . -type f -name '*~' | sed 's/^/rm /'
で確認し、OK なら
% find . -type f -name '*~' | sed 's/^/rm /' | sh
で消します。

いまどき fork/exec rm のオーバーヘッドがどれほどのものか。
xargs を使うとき、rm が 1回だけ実行されるときと、ARG_MAX を超えたときに
rm が複数回実行されるときの違いにどれほどの差があると言うのか。
誤差の範囲である。

コンピュータが速くなった今、重視すべきは確実性。
% find . -type f -name '*~' | sed 's/^/rm /'
として、
rm ./hoge~
rm ./dir/fuga~
rm ./foo/bar/baz~

という出力をじっくり眺め、問題がないことを確認する。OK なら、最後に "| sh" をつけて
% find . -type f -name '*~' | sed 's/^/rm /' | sh

とする。

削除してマズいファイルを見つけたら、grep -v で除外すればよい。

find コマンドは、人が使うには難しすぎる。なんだよ {} \; って。
xargs コマンド は、何をするコマンドなのかさえ直感的にわかりづらい。
わざわざ難しいことをする必要はないではないか。

単純なファイル削除ではあまり利点が見えないかもしれないが、便利なのは例えば
% find . -name \*.html | sed "s|\(.*\)|command < \1 > out/\1 | "
などというとき。出力が
command < hoge.html > out/hoge.html
となっていることをじっくり確認して、最後に "| sh" を付けて実行する。

何が起こるかは出力をじっくり観察すればわかる。見よ、この安心感。


…ってちょっと無理があるか? sed の基礎はおさえておかなきゃいけないし、
メタキャラクタ入りのファイルに弱いしなぁ。

sh をつけるやり方を流行らせたいんだが、無理かなー。いやほんと、便利よ。
最初の数行だけ試しに実行したかったら出力をマウスでコピペすればいいし、
コマンドラインだけでがんばるのが厳しかったら、ファイルにリダイレクトして
エディタで中身をいじって
% sh < command.txt
としてもいい。


ちなみに環境により挙動が異なる echo(1) は使わず、printf(1) を使うのが
POSIX 的な解かと思います。といっても、まわりの人が全然知らないので
仕事ではあえて使わないけど。

スポンサーサイト

PageTop

英単語トリビアと look コマンド

話のネタになるかもしれないトリビア50個
> * "Almost"は、全ての文字がアルファベット順になっている単語の中で最も長い単語。
>  A→l→m→o→s→t。他にどんな単語があるのでしょうか。
>
> * "Rhythm"は、母音の無い最も長い単語。
>  これも他の単語が気になります。

英単語トリビアが気になったので調べてみる。

UNIX には look というコマンドがある (1979年リリースの V7 から存在する)。引数で与えた文字列から始まる英単語を表示するだけのものだ。
% look fuga
fugacious
fugaciously
fugaciousness
fugacity
fugal
fugally

テキストファイル (FreeBSD だと /usr/share/dict/words) に英単語が羅列してあって、そこから grep しているようなものである。テキストファイルは最初からソートしてあり、二分探索で探しているので grep よりは速い。でもいまどきのマシンにおいては、ほとんど誤差程度の違いしかない。

> * "Rhythm"は、母音の無い最も長い単語。

まずこれ。若干の敗北感を感じつつも awk を使う。
% grep -iv '[aeiou]' /usr/share/dict/words | awk '{print length($1),$1}' | sort -nr | head -15
8 symphysy
7 nymphly
7 gypsyry
7 gypsyfy
6 tyddyn
6 thymyl
6 syzygy
6 sylphy
6 styryl
6 strych
6 spryly
6 rhythm
6 pyrryl
6 phytyl
6 myrrhy

「Rhythm」より長い英単語はあるし、「Rhythm」と同じ長さの英単語も結構あるので「最も長い」はちょっと言いすぎのようだ。

look で最長であった「symphysy」でググった結果、ほんとの母音なしの最長英単語は「TWYNDYLLYNG」のようだ。15世紀ごろの英古語で意味は「twin」だとか。ここまでくると、どの範囲を英単語と認めるかという話になりそうだ。

 - A Collection of Word Oddities and Trivia, Page 6
 - Longest word without vowels

> * "Almost"は、全ての文字がアルファベット順になっている単語の中で最も長い単語。

今度は perl を使う。コマンドラインでこんなことができるんですよ! ということを書きたかったはずなのに。堕落だ。
#!/usr/local/bin/perl
open(IN, '/usr/share/dict/words') || die "$!";
wordloop:
while (<IN>){
  chomp;
  my @chars = unpack('C*', lc($_));
  $max = 0;
  foreach my $c (@chars){
    if ( $max < $c ){
      $max = $c;
    } else {
      next wordloop;
    }
  }
  printf("%d %s\n", length($_), $_);
}
実行結果:
% perl hoge.pl | sort -rn | head -15
7 egilops
7 Adelops
6 ghosty
6 dimpsy
6 deinos
6 dehort
6 dehors
6 chintz
6 biopsy
6 bijoux
6 beknow
6 behint
6 befist
6 almost
6 agnosy

トップは 7文字の「egilops」と「Adelops」。almost は 6文字で、2番手グループ。上記の結果は同じ文字が連続している英単語は除外していたが、それも含むと以下のようになる。
% perl hoge.pl | sort -rn | head -10
7 egilops
7 billowy
7 begorry
7 beefily
7 alloquy
7 Adelops
6 knotty
6 knoppy
6 glossy
6 ghosty

A Collection of Word Oddities and Trivia, Page 1 によれば、全ての文字がアルファベット順になっている最長英単語は 8文字の「aegilops」。意味は look の最長である「egilops」と同じで、意味は an ulcer in a part of the eye (目の潰瘍?)。

とはいえ、
 * "TWYNDYLLYNG"は、母音の無い最も長い単語。
 * "aegilops"は、全ての文字がアルファベット順になっている単語の中で最も長い単語。
ではあんまり「へぇ」とは思わないので、まぁいいか。

look のデータは更新されてるのかな? と思って FreeBSD の cvsweb を見てみたら、ちまちまと手が入っているようだ。例えば Australia, Denmark, karaoke, karate などの英単語が追加されている。最新版 (2003年更新) は 235,882 単語が登録されており、4.4 BSD Lite 版 (1994年) と比べると 2632 単語 追加・修正・削除されていた。

ちなみに V7 版 は 24,001単語だった。
PageTop

「GCCプログラミング工房」の西田亙氏が「GNU開発ツール」を自費出版

西田亙氏が「GNU開発ツール」を自費出版 より。

UNIX USERでGCCプログラミング工房を連載されていた西田亙氏が、Computer Architecture Series 「GNU開発ツール」を自費出版します。価格は 4,500円。

久々に読みたい本が出てきた。早速予約。
PageTop

biff はなぜ動く?

web の referrer を見てたら、slashdot.jp から
 UNIX の部屋: biff
へリンクが張られている。

そこでふと気づいたが、わたしは biff の動作原理を説明できない。

思い起こせば、わたしが biff 的なものに触れたのは twm 上での
xbiff がはじめてで、biff コマンドは一度試しにつかってみたことが
ある程度だったような気がする。

/var/mail/$USER などのメールボックスにアクセスしているのは誰か?
まさか biff がシェルの内部コマンドなわけでもないだろう (シェル
変数の $mail と役割がかぶってるし)。

ということは、biff y とすると常駐して、メールボックスを定期的に
ポーリングし、メールが届くと端末に出力するのか? しかし biff
コマンドが登場した 4.0BSD な時代に、そんなリソースの無駄遣いは
許されないような気がする。


とりあえず使ってみよう。
 % biff y
 % echo hoge | mail 68user@localhost

…何も起こりません。


仕方がないので FreeBSD 6.0-RELEASE の biff.c を読む。

つまるところ biff y とすると
 char *name = ttyname(STDIN_FILENO);
  if ( argv[1][0] == 'y' )
  chmod(name, (sb.st_mode & ~(S_IXUSR | S_IXGRP)) | S_IXUSR);

となるようだ。

ぱっと見、パーミッションをどう変えたいのかさっぱりわからない。
そういえば C で chmod(2) を使ったことがないような気もする。

 % tty
 /dev/ttyp0


となるので、name はおそらくこの値が入っているのだろう。chmod(2) によると、
S_IXUSR は 0000100、S_IXGRP は 0000010 なので (0 から始まる数字は 8進数)、

 chmod(name, (sb.st_mode & ~(S_IXUSR | S_IXGRP)) | S_IXUSR)



 chmod("/dev/ttyp0", /dev/ttyp0 の現在のモード & ~(0000100 | 0000010)) | 0000100)

となり、0000100 と 0000010 の論理 OR は 0000110 なので

 chmod("/dev/ttyp0", /dev/ttyp0 の現在のモード & ~(0000110)) | 0000100)

となり、~ は否定なので 0000110 のビットが逆になり 7777667 となって (int が 32bit なら
01777777777667 とかが正しいか?)

 chmod("/dev/ttyp0", /dev/ttyp0 の現在のモード & 0777667) | 0000100)

となって、ログイン直後の /dev/ttyp0 は 0620 (rw--w----) なので

 chmod("/dev/ttyp0", 0000620 & 0777667) | 0000100)

となり、0620 と 0777667 の論理 AND を取ると 0620 なので

 chmod("/dev/ttyp0", 0000620 | 0000100)

となって、0620 と 0100 の論理 OR は 0720 なので

 chmod("/dev/ttyp0", 0000720)

となると。つまり u-x して g-x して u+x しているわけか。


というわけで実行。

 % biff y
 % ls -l `tty`
 crwx-w---- 1 68user tty 5, 0 Nov 23 00:59 /dev/ttyp0


確かに rw--w---- が rwx-w---- になった。

で、これでなぜメールが届くと端末にメッセージが出力されるのか? biff コマンドが
端末のパーミッションを開けているなら、誰かそこに出力しているプロセスがあるはず。

しかたがないので biff(1) を読む。
 SEE ALSO
  csh(1), mail(1), sh(1), comsat(8)

comsat(8) ってのが怪しい。

 % whatis comsat
 comsat(8) - biff server


おお、まさにこれか。comsat(8)
 SEE ALSO
  biff(1), inetd(8)

とあるので inetd 経由で起動されるのだろう。/etc/inetd.conf を見ると、
当然ながらコメントアウトしてある (いまどきの UNIX は、標準で talk
コマンドも write コマンドも使えないのでちょっと悲しい)。
  # run comsat as root to be able to print partial mailbox contents w/ biff,
# or use the safer tty:tty to just print that new mail has been received.
#comsat dgram udp wait tty:tty /usr/libexec/comsat comsat

この行を有効にして inetd 再起動。
 # kill -HUP `cat /var/run/inetd.conf`

再度メールを送信。
 % echo hoge | mail 68user@localhost
 New mail for 68user@localhost has arrived:
 ----


これでやっと biff が機能するようになった。しかしまだ comsat をつついて
いるプロセスがわからない。inetd を有効にして biff が機能するようになった
からには、UDP な comsat ポートをつついているプロセスがいるはずである。

まーどうせローカル配信エージェント (MDA) の mail.local なんでしょ?
と思いつつソースを探したが、マニュアルしか引っかからない。
 % cd /usr/src; find contrib/sendmail/mail.local/ -type f | xargs grep comsat
 contrib/sendmail/mail.local/mail.local.8:comsat(8),


はてと思いつつ /etc/services を見る。
 % grep comsat /etc/services
 biff 512/udp comsat #used by mail system to notify users

正式名称は biff で、comsat は別名だった。

というわけで、comsat でなく biff で mail.local のソースを grep。
 % cd /usr/src; find contrib/sendmail/mail.local/ -type f | xargs grep comsat
 contrib/sendmail/mail.local/mail.local.c:notifybiff(msg)
 contrib/sendmail/mail.local/mail.local.c: /* Be silent if biff service not available. */
 contrib/sendmail/mail.local/mail.local.c: if ((sp = getservbyname("biff", "udp")) == NULL ||


うむ、満足した。


苦労して計算したが、truss(1) なら一発でパーミッションがわかる。
 % truss biff y
 ....
 stat("/dev/ttyp0",0xbfbfeb64) = 0 (0x0)
 chmod("/dev/ttyp0",020720) = 0 (0x0)


0720 じゃなくて 020720 なのは、端末がキャラクタデバイスなため。
 /usr/include/sys/stat.h より:
  #define S_IFCHR 0020000 /* character special */


ところで、なぜ truss は chmod(2) の第二引数を 8進数で表示するのか?
と思ったらちゃんと truss のソース で定義してありました。
   struct syscall syscalls[] = {
...
{ "chmod", 0, 2,
{ { String, 0 }, { Octal, 1 }}},

PageTop

C のメモリ初期化

C で、
  while (1){
   char buf[256];
   memset(buf, 0, sizeof(buf));
   ...
 }
などと毎回メモリを 0 で塗りつぶす人がいるが、果たしてこれはよい習慣なのだろうか。

わたしは「適切に文字終端を管理していればこんな初期化は不要」派である。

しかし適切に文字終端を管理できない人はたくさんいる。そういう人にとっては、前回
ループの変数の内容が次ループにも引き継がれてしまう、という最悪の事態を避けるため
のよい方法なのだろう。実際、他人の書いたソースを眺めていて、memset がなかったら
落ちるなぁと思ったことは多々ある。

でもわたしには、ただのバグ隠蔽工作にしか見えない。VC++ でデバッグビルドをした
バイナリをリリースするようなものだ (VC++ のデバッグビルドでは、変数は 0xcdcdcdcd に
初期化される、らしい)。

というわけで、これまでは
 - わたしは memset による初期化はしない
 - 他人が memset するのは口出ししない (でも俺はしないけどね、くらいは言うかも)
 - 何も知らない初心者には memset で初期化しろとは言わない (適切な文字終端管理が
  できるようになることを願って)
というふうにしてきた。でもダブルスタンダードなので気持ち悪い。

世の中的にはどうなんでしょうね。
PageTop