ed

今年もそろそろ終わりモードなので、今年の科学界の事を振り返ってみる事にした。資料は ニュートン誌1年分。と言っても、この雑誌は結構人気があるんで、貸し出し中が多いんよ。

そんな訳で、5,6月号を借りてきた。これだけではあれなんで、副読本として 『人類が生まれるための12の偶然』(岩波ジュニア新書)もね。この本、オイラーにとっては 当り本でしたよ。宇宙誕生の偶然、太陽系、地球を変えた月、地球に起きたさまざまな偶然、 不思議な液体『水』、生命におきたさまざまな事、文明誕生を後押しした気候。

これだけで、ニュートン誌1年分ぐらいな範囲があるな。かの誌には必ず、宇宙の写真や 恐竜の図が載ってるけど、余り興味が無かった。これからはそういう物も興味を持って 眺められるだろう。それが高じて、ハワイ島にある天文台へ行ったり、恐竜の里、福井県へ 旅行に出かけたりしてね。

昔行った、ハワイ4島めぐりの旅行で、同天文台へ行く予定だったんだけど、火山が 臨界状態って事で、山の上へ行くのは禁止されてた。もう一度、そのためだけに行ってみたいな。

話題を戻して、上記本で特に面白かったのは、不思議な液体、水の所。通販で売っている 水素水の事では無い。水素水が体に良いなんて、大嘘。もし、効くとしたら偽薬効果かな。

4度で最も重くなる、多くの物質を溶かす、ユニークな分子結合(104.5度結合)、大きな比熱、 これらが生命誕生に大きく寄与した。日本だと水が溢れているけど、よその国にこういう 所は少ない。中国の富豪あたりが水を求めて土地を買うってのは賢いね。水が金を産むんだ もの。そうか、最近は金を生み出す道具って訳か。水防衛に必死になれよ。政府、自治体の みなさん。

ed事始め

ちょっと前に、OpenBSDをvboxに入れようとして四苦八苦してた時、インストールスクリプトを 変更してみようと言う気になった。けど、emacsも入ってないし、viも無いし、有るのはedだけ。 けど、使い方を忘れてる。もしもの為に非常訓練しておくか。

歴史ある編集師なんで、資料には事かかない。

なぜedはクソなのか

edは、国民的辞書にも解説有りですよ

結構使える ed の使い方

どんな環境でも使えるライン・エディタ - edエディタ -

ed メモ

edを使ってみる

FreeBSD ed(1)

ed(1) 好きなあなたに 53 の質問(V0.8086)

そして、何と言ってもソース嫁。

[ob: ed]$ wc *.[ch]
     250     780    4825 buf.c
     203     760    5250 ed.h
     173     595    4125 glbl.c
     315    1086    6602 io.c
    1383    4343   29995 main.c
      99     339    2091 re.c
     222     818    5180 sub.c
     112     351    2480 undo.c
    2757    9072   60548 total

勿論、置いてある場所は/binの中です。

復活の呪文 u

色々追いかけてもきりが無いので、undoコマンドがどうなっているか、追ってみたい。 まず、コマンドは何処で受け取るか?

 144                if (prompt) {
 145                        fputs(prompt, stdout);
 146                        fflush(stdout);
 147                }
 148=>              if ((n = get_tty_line()) < 0) {
 149                        status = ERR;
 150                        continue;
   :
 174                isglobal = 0;
 175                if ((status = extract_addr_range()) >= 0 &&
 176                    (status = exec_command()) >= 0)

148行目で、入力を受け取り、各種チェックの後174行目に飛んで来る。次の行で、コマンドに 前置されるアドレス範囲を解析した後、176行目で、コマンド実行。exec_command関数の中で、 コマンドを振り分けている。

 759        case 'u':
 760=>              if (addr_cnt > 0) {
 761                        seterrmsg("unexpected address");
 762                        return ERR;
 763                }
 764                GET_COMMAND_SUFFIX();
 765                if (pop_undo_stack() < 0)
 766                        return ERR;
 767                break;

どうやらundo機能は、スタックによって実現されてるようだ。popの先を追ってみる。

 60        for (n = u_p; n-- > 0;) {
 61                switch(ustack[n].type) {
 62                case UADD:
 63                        REQUE(ustack[n].h->q_back, ustack[n].t->q_forw);
 64                        break;
 65                case UDEL:
 66=>                      REQUE(ustack[n].h->q_back, ustack[n].h);
 67                        REQUE(ustack[n].t, ustack[n].t->q_forw);
 68                        break;
 69                case UMOV:
 70                case VMOV:

コマンドのタイプによって、戻し方を使い分けているようだ。そんじゃ、代表的なやつで、 3,5dとかするとどうなるか?

 455        case 'd':
 456                if (check_addr_range(current_addr, current_addr) < 0)
 457                        return ERR;
 458                GET_COMMAND_SUFFIX();
 459=>              if (!isglobal) clear_undo_stack();
 460                if (delete_lines(first_addr, second_addr) < 0)
 461                        return ERR;
 462                else if ((addr = INC_MOD(current_addr, addr_last)) != 0)
 463                        current_addr = addr;
 464                break;

clear_undo_stackでスタックをクリアするんか。そうすると、2回続けて消すと、最初のやつは 復帰出来ないんだな。で、delの範囲を指定したので、460行目が実行される。

(gdb) bt
#0  push_undo_stack (type=1, from=3, to=5) at undo.c:15
#1  0x19ffd9ee in delete_lines (from=3, to=5) at main.c:1178
#2  0x19ff93be in exec_command () at main.c:460
#3  0x19ff840e in main (argc=1, argv=0xcfbec3f0) at main.c:175
  9/* push_undo_stack: return pointer to initialized undo node */
 10undo_t *
 11push_undo_stack(int type, int from, int to)
 12{
 13        undo_t *t;
 14
 15=>      t = ustack;
 16        if (u_p < usize ||
 17            (t = reallocarray(ustack, (usize += USIZE), sizeof(undo_t))) != NUL   L) {
 18                ustack = t;
 19                ustack[u_p].type = type;
 20                ustack[u_p].t = get_addressed_line_node(to);
 21                ustack[u_p].h = get_addressed_line_node(from);
 22                return ustack + u_p++;
 23        }

このまま、追ってもいいんだけど、やはり基本事項を押さえておかないとね。

舞台の移動

OpenBSDには、gdbserverが入っていない。(無ければ入れるのが正しい対処方法ですが、 面倒なのでPASSします)そこで、ソースをそのままfedoraに移してやってみたい。

が、コンパイルするとエラーのオンパレード。エラー内容を見ると、OpenBSDの自慢の種、 安全な文字列操作関数がfedoraと言うかリナには無い事が判明。駄目じゃん、リナ。

憤慨していてもしょうがないので、FreeBSDからソースを持ってくる事にしました。 持ってきたソースには、FreeBSDの烙印が押されていて、やはりエラー。しょうがないので 烙印を削除しましたよ。その他、微小な修正(strlcpy -> strcpy)を施しました。 コンパイルは普通Makefileがあるので、make一発のはずですが、やはりFreeBSD用に調教 されてますんで、手作業です。

[sakae@fedora ed]$ for f in *.c
> do
> cc -gdwarf-2 -g3 -c $f
> done
[sakae@fedora z]$ cc -gdwarf-2 -g3 *.o -o ed

こんな具合に、マクロ関係もgdbから確認出来るように、コンパイルしておきました。 上記はちょっと上品にコンパイル過程とリンク過程を分けてみたけど、いきなり全部の Cファイルを指定してコンパイルでも良い。

これで、BSD系がリナに殴り込みですよ。えっ、GNU製のedを使えば楽でしょって声が 聞こえそう。そういうのは却下。本家の伝統を守らなければ。。

edの構造

editorの基本って何だろう? それは、編集対象となる(時には数100Mにも及ぶであろう) 文書をどうやって管理するかにかかって来ると思う。簡単なのは、対象の文書を、全て メモリーに載せてしまえば楽だろう。 でも、文書の切り貼りとか移動とかにどうやって対応する? 行単位で管理するとしても、 短い文字数と行有り、とっても長い行ありで、収拾がつかなくなりそう。

edは、管理情報だけ、メモリーに置き、実際の文書は、一時ファイルに保存してる。 行の増加や修正は、一時ファイルに書き込み、その位置とサイズを、メモリー上で管理。 行の削除は、メモリー上の管理情報だけで行う。一時ファイル内の該当行は放置。 どうせ、一時ファイルですから。

で、管理情報は、切り貼りが容易な線形リストを使っている。逆にも辿れるように、 双方向性リストにしてる。

で、構造体を使った、リストの扱いを復習しましょ。

アルゴリズム-リスト構造-

TAILQ のソースを読んで C のポインタをマスターする。

適当にファイルを読み込ませて、それを印字させようとすると、リストを辿って、 頭出しするルーチンが呼ばれる。その関数は、buf.cの中に有った。

128/* get_addressed_line_node: return pointer to a line node in the editor buffer */
129line_t *
130get_addressed_line_node(long n)
131{
132        static line_t *lp = &buffer_head;
133        static long on = 0;
134
135=>      SPL1();
136        if (n > on)
137                if (n <= (on + addr_last) >> 1)
138                        for (; on < n; on++)
139                                lp = lp->q_forw;
140                else {
141                        lp = buffer_head.q_back;
142                        for (on = addr_last; on > n; on--)
143                                lp = lp->q_back;
144                }
 :
154        SPL0();
155        return lp;
156}

SPL1()とかのマクロは、割り込み(但し、sigunalね)の禁止、有効を制御しています。 まるで、カーネルのあれみたい。

で、大事な line_t って構造体、これが、編集文書の1行に一つ用意されて、 それが数珠繋ぎになってる。

/* Line node */
typedef struct  line {
        struct line     *q_forw;
        struct line     *q_back;
        off_t           seek;           /* address of line in scratch buffer */
        int             len;            /* length of line */
} line_t;

ed(1)にも書いてあるけど、一行あたりのオーバーヘッドは整数4つ分ですってのが、 この構造体から見て取れる。(バイト数にするとオイラーのぼろいマシンでは16byteです)

こういう、構造体を使った線形リストはソースに頻出するけど、効率的にgdbで追いかけ られないの? そんなのきっとあるはず。調べたら、データを検査するの章の 値ヒストリって所に、説明が有ったぞ。

printコマンドにより表示された値は、 GDBの 値ヒストリに保存されます。 
これによりユーザは、 これらの値をほかの式の中で参照することができます。

表示される値はヒストリ番号を与えられ、 この番号によって参照することができます。 
この番号は1から始まる連続した整数です。 printコマンドは、 値に割り当てられた
ヒストリ番号を、 値の前に`$num = 'という形で表示します。 
ここで、 numがそのヒストリ番号です。 以前の任意の値を参照するには、
 `$'に続けてヒストリ番号を指定します。 printコマンドが出力に付加するラベルは、 
ユーザにこのことを知らせるためのものです。 

$単体では、 ヒストリ内の最も新しい値を参照し、 $$はその1つ前の値を参照します。 
$$nは、 最新のものから数えてn番目の値を参照します。 $$2は$$の1つ前の値を参照し、
 $$1は$$と同一、 $$0は$と同一です。 

例えば、 ユーザがたった今、 構造体へのポインタを表示し、 今度はその構造体の内容を
見たいと考えているとしましょう。 この場合は、

p *$

を実行すれば十分です。 また、 連結された構造体があり、 そのメンバのnextが
次の構造体を指すポインタであるとすると、 次の構造体の内容を表示するには、

p *$.next

それでは、早速応用。上の get_addressed_line_node に飛び込んできた所で、 試験運用してみる。

(gdb) p lp
$1 = (line_t *) 0x8059320
(gdb) p *$
$2 = {q_forw = 0x805518c <buffer_head>, q_back = 0x8059308, seek = 611, len = 51}
(gdb) p *$.q_back
$3 = {q_forw = 0x8059320, q_back = 0x80592f0, seek = 561, len = 50}
(gdb)
$4 = {q_forw = 0x8059308, q_back = 0x80592d8, seek = 511, len = 50}
  :
(gdb)
$14 = {q_forw = 0x8059218, q_back = 0x8058d30, seek = 9, len = 53}
(gdb)
$15 = {q_forw = 0x8059200, q_back = 0x805518c <buffer_head>, seek = 0, len = 9}

これ、尻尾から先頭に向かって、リストを辿っています。 一度、辿る方向を決めたなら、後はリターンを叩くだけで、次から次と構造体を検査 出来ます。 構造体のメンバー、seekが 段々小さくなっているのが確認出来満足です。これでDDDも不用かな。

FreeBSD特注機能

OpenBSDとFreeBSDのファイル構成を比べてみると、FreeBSDの方に余分なファイルが一つ 存在してる。cbc.cってのがそれ。ファイル名から想像するに、暗号関係っぽい。

ファイルを開いてみると、#ifdef DES でほとんどが囲まれていました。DESを有効にして コンパイルしてみましたが、openssl/des.hが必要と言われ、なんでそんな弱い暗号使うねん? と、あざけりを受けましたよ。

editor内で暗号を扱えるようにするって、unixの哲学から完全に逸脱してると思いませんか。 どうやら、FreeBSDの勇み足ですな。

GNU ed

上記で色々やってる時、/tmpの下にどんなファイルが出来るんか確認を入れてみた。すると、 /tmp/ed.1SZAmz こんな名前のものが出来るんだけど、サイズがゼロになってたりする。 何故?

大きなファイルを編集しようとすると、一時ファイルにちゃんと値がつく事が判明。そうか 、サイズが小さいうちはOSの機能によってバッファリングされるんだな、と勝手に納得。

それはいいんだけど、ある時、tmpの下にこの一時ファイルが出来ない事が有った。 なんの事は無い。./ed として起動しなければいけない所を、普通に ed とやったもんだから、 fedoraに備え付けの ed が起動しちゃったんだ。

でも、GNU edは一時ファイルを作らないの? それでどうやって文書を管理してんの? どうやらGNU ed を紐解く時が来たようです。

適当にGNUのミラーサイトから取ってきます。 で取ってきたのはいいんですが、ed-1.12.tar.lz こんな名前ですよ。圧縮方法が初遭遇 です。こういう時は、相手に聞いてみるに限る。

[sakae@fedora z]$ file ed-1.12.tar.lz
ed-1.12.tar.lz: lzip compressed data, version: 1

lzipって初めて聞く名前。我らの友に聞いてみたぞ。lzip

古い(仕様の)ソフトを新しい器に盛って出すとは味な事をしおるわい。感心してる場合じゃ 無いですよ。どうやって展開する?

野生の感で、lzipってアプリが有るに違いない。fedoraには入っていたぞ。 どうやってtarと組み合わせる? これも野生の感で

[sakae@fedora z]$ tar lxf ed-1.12.tar.lz

感が冴えてる。64kサイズのものが490kのtarファイルになりましたから、いい線行ってるかな。

取りあえずgdbで扱えるようにコンパイルして、gdbで追跡。buffer.c内

   |381     /* open scratch file */                                            |
   |382     bool open_sbuf( void )                                             |
   |383       {                                                                |
   |384       isbinary_ = newline_added_ = false;                              |
   |385       sfp = tmpfile();                                                 |
  >|386       if( !sfp )                                                       |
   |387         {                                                              |
   |388         show_strerror( 0, errno );                                     |
   |389         set_error_msg( "Cannot open temp file" );                      |
   |390         return false;                                                  |
   |391         }                                                              |
   |392       return true;                                                     |
   |393       }                                                                |

一時ファイルの作成はtmpfile()に任せている。

(gdb) p sfp
$3 = (FILE *) 0x8055880
(gdb) p *$
$4 = {_flags = -72539008, _IO_read_ptr = 0x0, _IO_read_end = 0x0,
  _IO_read_base = 0x0, _IO_write_base = 0x0, _IO_write_ptr = 0x0,
  _IO_write_end = 0x0, _IO_buf_base = 0x0, _IO_buf_end = 0x0,
  _IO_save_base = 0x0, _IO_backup_base = 0x0, _IO_save_end = 0x0,
  _markers = 0x0, _chain = 0xb7fc1cc0 <_IO_2_1_stderr_>, _fileno = 3,
  _flags2 = 0, _old_offset = 0, _cur_column = 0, _vtable_offset = 0 '\000',
  _shortbuf = "", _lock = 0x8055918, _offset = -1, __pad1 = 0x0,
  __pad2 = 0x8055924, __pad3 = 0x0, __pad4 = 0x0, __pad5 = 0, _mode = 0,
  _unused2 = '\000' <repeats 39 times>}

何のこっちゃ? 素直に、マンセーよ。

NOTES
       The  standard  does  not specify the directory that tmpfile() will use.
       Glibc will try the path prefix P_tmpdir defined in  <stdio.h>,  and  if
       that fails the directory /tmp.

で、stdio.h

#if defined __USE_MISC || defined __USE_XOPEN
/* Default path prefix for `tempnam' and `tmpnam'.  */
# define P_tmpdir       "/tmp"
#endif

#if defined __USE_MISC || defined __USE_XOPEN
/* Generate a unique temporary filename using up to five characters of PFX
   if it is not NULL.  The directory to put this file in is searched for
   as follows: First the environment variable "TMPDIR" is checked.
   If it contains the name of a writable directory, that directory is used.
   If not and if DIR is not NULL, that value is checked.  If that fails,
   P_tmpdir is tried and finally "/tmp".  The storage for the filename
   is allocated by `malloc'.  */
extern char *tempnam (const char *__dir, const char *__pfx)
     __THROW __attribute_malloc__ __wur;
#endif

結局の所、/tmpを使うはず。なんだけど、それらしいのが出来ていない。ひょっとして 小さい文書ファイルだとキャシュされちゃって、表に表れないかと思い、大きなファイルで 試すも、出てこない。

manで関連を調べると、mktempなんてのが有ったので試すと

[sakae@fedora ed-1.12]$ mktemp
/tmp/tmp.dEqZYAK39R

ちゃんとそれらいのが出来ている。オイラー、何か勘違いしてる?????

折角、お取り寄せしたんで、ちと観賞。ファイル構成すっきりした。大文字のマクロが 関数に置き換えられて、読みやすくなった。コメントが丁寧に付いたって感想です。 例えば、コマンドループ内

    case 'd': if( !check_current_addr( addr_cnt ) ||
                  !get_command_suffix( ibufpp, &gflags ) ) return ERR;
              if( !isglobal ) clear_undo_stack();
              if( !delete_lines( first_addr, second_addr, isglobal ) ) return ERR;
              inc_current_addr();
              break;

論理がすっきりして読み易いです。

uv6のed

昔のunixのedも見ておくか。ウブンツにpdp11で動くやつが入ってた。ソースの在り処は、/usr/s1/ed.c だった。 それはいいんだけど、ウブ側にどうやってソースを持ってくる?

こういう時は、screenのログ機能が活躍します。Hコマンドでログ開始。こうしておいてから、 cat ed.c して、画面にソースを流します。終了したら、もう一度Hコマンド実施。 これで、flushって名前のログがhome-dirに出来上がりました。

1337行有りました。どんなになってるか干渉じゃなかった、観賞しましょ。 観賞だけじゃなくて、古代の道具が現代に蘇るか試してみると、、、

a.c:54:17: error: expected ‘=’, ‘,’, ‘;’, ‘asm’ or ‘__attribute__’ before numeric constant
 int     vflag   1;

a.c:304:28: warning: assignment makes pointer from integer without a cast
                         a1 =+ n;
                            ^
a.c:370:28: warning: assignment makes pointer from integer without a cast
                         a1 =+ minus;                          ^

こういう古代語が話されていたようで、現代に通用させるには、それなりの変換が多数必要と 判明しました。修復は学芸員にでもお願いしますかね。

ちょっとコードを見ていたら、考え方は現代まで継承されてるようでした。一時ファイルを 作成する部分。自分でやってるんですねぇ。

        tfname = "/tmp/exxxxx";
        ichanged = 0;
        pid = getpid();
        for (p = &tfname[11]; p > &tfname[6];) {
                *--p = (pid&07) + '0';
                pid =>> 3;
        }
        close(creat(tfname, 0600));
        tfile = open(tfname, 2);

init()関数の中の一部でした。

謎解き

GNU edで、一時ファイルが見えない謎が気になるんで、資料がしっかりしてるOpenBSDで 確認。やっぱり見えない。マンセーすると、tmpfile()は、mkstemp()を使ってるとな。 gdbで追って、tmpfile.cを紐解いてみると

45tmpfile(void)
46{
47        sigset_t set, oset;
48        FILE *fp;
49        int fd, sverrno;
50#define TRAILER "tmp.XXXXXXXXXX"
51        char buf[sizeof(_PATH_TMP) + sizeof(TRAILER)];
52
53=>      (void)memcpy(buf, _PATH_TMP, sizeof(_PATH_TMP) - 1);
54        (void)memcpy(buf + sizeof(_PATH_TMP) - 1, TRAILER, sizeof(TRAILER));
55
56        sigfillset(&set);
57        (void)sigprocmask(SIG_BLOCK, &set, &oset);
58
59        fd = mkstemp(buf);
60        if (fd != -1) {
61                mode_t u;
62
63                (void)unlink(buf);
64                u = umask(0);
65                (void)umask(u);
66                (void)fchmod(fd, 0666 & ~u);
67        }
68
69        (void)sigprocmask(SIG_SETMASK, &oset, NULL);
70
71        if (fd == -1)
72                return (NULL);
73
74        if ((fp = fdopen(fd, "w+")) == NULL) {
75                sverrno = errno;
76                (void)close(fd);
77                errno = sverrno;
78                return (NULL);
79        }
80        return (fp);
81}

ファイル名を隠すような実装がしてあるんだな。納得。

なお、edのソースをちゃんと見るなら、現代風訳の GNU ed が良いですよ。これは、 源氏物語を現代訳で観賞するのと同じ。あらすじが分かってから、古典に取り組むのが 正解と思います。

と、こんな事で思わずページが一杯になってしまった為、囲碁の章はきっと次回にでも。。。

今日の読み物

一日では読みきれない。まあ、ゲストブログあたりをお散歩

サイオスさんのOSS