eshell

香港・マカオの旅が台風で中止になって残念な思いをした義姉さん。リベンジで2度目のベトナムにチャレンジしてきたとか。土産を頂いた。餞別もあげなかったのに、気を使って頂いてありがとう。

ベトナムコーヒーのセット。挽いた豆が250g、アルミ製のフィルターがセットになったやつ。もう、これだけでエスニックですよ。所で、どうやってコーヒーを淹れるの?

ベトナムコーヒーの淹れ方って、聞いてみた。

なる程、2重フィルター式なのね。半永久的に使えるってのがエコでいいな。たっぷりの豆を入れるのがこつらしいけど、初回はちょっと薄かった。とか言ってたら、女房が新手法を編み出した。

そんなの麦茶と一緒で、鍋で煮出しちゃえばいいのよ。茶漉しでフィルターして、美味しいのが入りましたよ。あんたは、コーヒーバリスタか? 既成概念に捕らわれないのが女房流。何事もチャレンジが大事ってか?

eshell

カスタマブルな Emacs Lisp 製シェル「eshell」これ、るびきちさんの記事。るびきちさんって、あのruby大好きさんだよねとか、過去を懐かしみながら読んでます。

起動するには、M-x eshell ってするんだな。

Welcome to the Emacs shell

/tmp $ pwd
/tmp
/tmp $ (+ 1 2 3 4)
10
/tmp $ + 1 2 3 4
10

カッコを省ける所が、かっこいいな。まて、shellのコマンドでも、括弧を省けるぞ。

/tmp $ bc
bc 1.06.95
Copyright 1991-1994, 1997, 1998, 2000, 2004, 2006 Free Software Foundation, Inc.
This is free software with ABSOLUTELY NO WARRANTY.
For details type `warranty'.
1 + 2 + 3 + 4
10                   ;; EOFを送って終了するには、C-c C-d なんだな。要注意の事
/tmp $

幾つも+を入力させられるって、苦痛ですわ。ああ、eshellの終了は普通にexitね。

/tmp $ (dotimes (i (length obarray))
  (prin1 (aref obarray i))
  (terpri))

調子に乗ってobarrayの結果を表示しようとした。アレー形式になってるんだけど、リストと同じような扱いをしてくれると思って、最初は一つ覚えのdolistを使ったんだ。そしたらアレーって事になっちゃたんで、subr.elを漁ってdotimesを見つけ出したよ。

配列へのアクセスなんで、obarray[i]と書きたい所だけど、elispの作法に従って、aref関数を使ったよ。脳内変換が強化される事を希望する。

0
ert-run-tests-interactively
pre-redisplay-functions
cl-format
eshell-needs-pipe-p
separator-frame
Info-check-pointer
boron-theme

これが結果なんだな。後は、これをリダイレクトすればいいのか。 ちゃんとファイルは作られるものの、中身は空だなあ。もしやと思ってエラー出力も一緒にしてみたけど、やはり空だった。

/tmp $ echo Hello Elisp World >ew
/tmp $ cat ew
("Hello" "Elisp" "World")/tmp $ echo and goodby java >>ew
/tmp $ cat ew
("Hello" "Elisp" "World")("and" "goodby" "java")/tmp $

こういうのは、普通に出来ているんで、eshellの秘孔を突いたって事ですかな。

macro

上で使った、dotimesなマクロ、どんなコソコソをやってるか調べてみる。調べる対象は、quoteしておくのが肝心。

(macroexpand  '(dotimes (i 10)(print i)))

(let ((--dotimes-limit-- 10) (i 0))
  (while (< i --dotimes-limit--)
    (print i)
    (setq i (1+ i))))

結果は、人様が読む必要無しって事で、最密表示される。んな訳で、オイラーが少々整理してあげたよ。名前が衝突しないように、ひねくれた変数名(--dotimes-limit-- なんて言う、知らない人からすると卒倒するような名前。lispは名前に関しては、制限が緩いので、これもりっぱな名前として扱える)を採用してるな。 特に変な事をやってる訳ではないので、安心した。

ついでに、dolistも展開してみるか。

(macroexpand '(dolist (e (garbage-collect))
  (prin1 e)
  (terpri)))
  
(let ((--dolist-tail-- (garbage-collect))
      e)
  (while --dolist-tail--
    (setq e (car --dolist-tail--))
    (prin1 e)
    (terpri)
    (setq --dolist-tail-- (cdr --dolist-tail--))))

whileの条件句は、nilか否も使えるのね。

これらのマクロは、共にコレクション(配列、リスト)のイテレーターを実現してる。使うユーザー側からすれば、あたかもelispの文法?が拡張されたように見える。これぞ、マクロが持つ威力だ。久しぶりに、マクロの魔導書 On Lisp を眺めてみるかな。

macroじゃないけど、過去の資料を見ていたら、obarrayに登録されてる個数を正確にカウントする例が載ってた。

(let ((cnt 0))
  (mapatoms '(lambda (x) (setq cnt (1+ cnt))))
  cnt)
23800
(length obarray)
15121

随分違いが有るね。この違いって、ハッシュの衝突の頻度を表しているのかな? keyのhashを計算。衝突してたら、keyをリストに保存ってのが、よく使われる手法。

obarrayで出て来るのは、衝突していない数。これなら、直ぐに求まる。衝突してたら、リストを辿って、正確な数を求める。こちらがmapatomsで出て来る数(って予想)。詳しい事は、ソース嫁。

eshellの二枚舌

上で出て来た、obarrayの出力がリダイレクトされない件(勿論パイプも駄目)を、秘孔と考えないで、深堀してみる。

実行時に、画面が割れて、下側に結果の一部が出て来た。次のコマンドを入力すると、その画面は自動消滅。これが、観察結果である。

emacsでは、何かを表示する場合、必ずbufferに書き込むのが約束。ああ、bufferってのは、表示窓ってぐらいの意味だ。だから、一時的であれ、結果が出て来るって事は、何かのbufferに書き込んでいるはず。

こう思って、C-x C-b して、bufferのリストを出してみた所、*Messages* buffer が、異常に膨らんでいた。*Messages*を覗いてみると、oblistの結果が収録されてた。

そうか、そういう事か。実行結果は、*Messages* に書き込まれ、そのtail部分を、親切にeshell君が見せてくれてるのね。

そうなると、S式の実行結果が、eshellの画面(buffer)に出て来たり、*Messages*に出て来たりの使い分けが知りたい。

過去のLispの知識を総動員して、オイラーのAIに聞いてみた。して、その答えは?

副作用の結果は、*Messages* に行くんじゃよ。S式が返すやつは、eshell画面に出て来るんじゃよ。

何の事か? と思わる人向けに実験。

/tmp/emacs-26.1/src $ (concat "Hello" "Emacs")
HelloEmacs

concat関数の結果(文字列の連結)が、eshell画面に表示された。

/tmp/emacs-26.1/src $ (dotimes (i 20) (print "Hello elisp"))
/tmp/emacs-26.1/src $

dotimesの結果はnilなんで、表示無し。dotimesの副作用(で、print)の結果は、黙って、*Messages*に書き込まれた。そして、親切に、eshellが一部を表示してくれた。

"Hello elisp"

"Hello elisp"

"Hello elisp"
/tmp/emacs-26.1/src $ (let ((res ""))
(dotimes (i 3)
(setq res (concat res "Hello Elisp" "\n")))
res)
Hello Elisp
Hello Elisp
Hello Elisp
/tmp/emacs-26.1/src $

letでローカル変数resを作り、dotimesで文字列をresに連結。dotimesを抜けたら、結果としてresを返す。これで目出度く、eshell画面に結果が出てきた。

/tmp/emacs-26.1/src $ (let ((res ""))
(dotimes (i 3)
(setq res (concat res "Hello Elisp" "\n")))
res) | wc
       3       6      36

結果がeshellの画面に出てくれば、しめたもの。上のように、パイプに繋いだり、リダイレクトも、おちゃのこさいさい、であーる。

秘孔と勝手に決めて、うっちゃっておかなくて良かったわーい。

このeshell、TABで補完が効くし、評価でエラーになっても、M-pで前のコードを呼び出して、C-nとかで、コード内を飛び回って、修正実行(が、RETで)出来るんで、気にいった。

スクラッチバッファーで、補完しようとすると、C-M-i なんて叩かないいけないので、少々うんざりしてましたからね。まあ、こういうキーバインドは即変更しちゃうって手もありますけど。

etag

gdbを使ってemacs内を観光するのもいいけど、もっとダイナミックに飛び回りたい。それには、やっぱりetagsでTAGファイルを作っておくのが良い。

emacsはetagsの本家。試用する為の機構が用意されているに違いない。

cent:emacs-26.1$ make tags
cent:emacs-26.1$ find . -name TAGS
./lisp/TAGS
./src/TAGS
./lwlib/TAGS
cent:emacs-26.1$ sudo cp lisp/TAGS /usr/local/share/emacs/26.1/lisp/

Makefileを点検する事なくターゲットを指定したら、出来上がっちゃったぞ。elisp用のものは、実環境にも配備しておいた。これで、ソース系を一式消してしまっても、安心かな?

Emacs C source dir: /usr/local/share/emacs/26.1/lisp/

と、思っていたら、elisp内をすいすい飛び回っていて、知らないでプリミティブ関数を指定しちゃうと、上記のようにソースは何処やって質問が出て来る。(ソースが入っていると、いつの間にか、C語の世界に切り替わっている。シームレスは有り難いんだけど、ちょいと案内が欲しいぞ。)

バイリンガルな人は、言語が違っても同時に聞き分けられるんですかね? 今のは日本語だな、今度は英語が聞こえてきたぞ、こういう経験は余りない。翻って、昔青森にスキーに行った時、骨休めで酸ヶ湯温泉に行った。 古老に話しかけられたけど、さっぱりでしたよ。日本語と思って聞いても、意味不。片言でいいから、英語でお願いしますってマジで思ったものだ。

少々脱線したけど、tagを作る時のは、コンパイルが終了した直後が良い。真っ新なソースが ある所でtagを作ろうとするとエラー、若しくは再コンパイルが走っちゃうから。 再コンパイルったって、1分少々で済むから、大した手間ではないけどね。

edebug

折角TAGSが出来たんで、eshell問題を解いてみたい。ええ、上で出て来た、副作用は*Messages*バッファーへ行っちゃうというやつね。何処かをごにょごにょしたら、表舞台のeshell画面に出てこないかなって、期待を込めて。

eshell関係のソース一式は、

/tmp/emacs-26.1/lisp/eshell $ ls *.el
em-alias.el   em-hist.el    em-smart.el  esh-cmd.el     esh-opt.el
em-banner.el  em-ls.el      em-term.el   esh-ext.el     esh-proc.el
em-basic.el   em-pred.el    em-tramp.el  esh-groups.el  esh-util.el
em-cmpl.el    em-prompt.el  em-unix.el   esh-io.el      esh-var.el
em-dirs.el    em-rebind.el  em-xtra.el   esh-mode.el    eshell.el
em-glob.el    em-script.el  esh-arg.el   esh-module.el

総行数は13740行有った。docと称する説明文とかが大量に内包されてるから、実コードは、半分ぐらいでしょうかね。

何処から手を付けたらいいものかと気ままにzappingしてたら、面白いコメントを発見。

(defmacro eshell-trap-errors (object)
  "Trap any errors that occur, so they are not entirely fatal.
     :
Someday, when Scheme will become the dominant Emacs language, all of
this grossness will be made to disappear by using `call/cc'..."

これが、公式なdocの一部になってるって、どうよ? Schemeが優勢になるemacs。オイラーも切に願っておりますだ。

でも、 Emacs、Guile、Emacs Lispの未来を見ると、現実は厳しいような。UTFな文字コード一択に反対を唱えた殉教者のあの人みたいな心境だな。まあ、日本は漢字、ひらがな、カタカナ、ローマ字、英語と、屈託がない国ですからね。

沢山あるファイルをじっと見ると、頭にem-が付いたやつと、esh-が付いたものに分かれている。前者は多分elispで書かれたモジュールだろう。eshellの本体は後者と思われる。

そこで、後者に的を絞って、一番怪しそうなesh-cmd.elあたりを見る。unixライクなコマンドとS式の切り分けをやっているに違いない。

M-x occur RET (defun RET して、定義名をリストしてみる。

    :
   1223:(defun eshell-plain-command (command args)
   1235:(defun eshell-exec-lisp (printer errprint func-or-form args form-p)
   1303:(defun eshell-lisp-command (object &optional args)

lispと名前が付くやつが2つ見つかった。そのうちのeshell-exec-lispを眺めると、いかにも怪しそうな関数と思えてきた。

/tmp/emacs-26.1/lisp/eshell $ (progn (prin1 "HOGE") "lastval")
lastval

最後の式と言うか文字列が出て来た。この時に*Messages*は、下記のようになってた。em- モジュール説はビンゴでしたね。良く観察して考察するが大事だな。

Making completion list...
Mark set
Loading em-alias...done
  :
Loading em-unix...done
"HOGE"

esh-cmd.elを表示させて、edebugを使うように関数を登録。eshell-exec-lispの所にカーソルを持って行って、C-u C-M-x するんだった。

それから、eshell画面に戻って、progn... を再実行。果たしてヒットしてくれるかな。

(defun eshell-exec-lisp (printer errprint func-or-form args form-p)
  "Execute a lisp FUNC-OR-FORM, maybe passing ARGS.
PRINTER and ERRPRINT are functions to use for printing regular
messages, and errors.  FORM-P should be non-nil if FUNC-OR-FORM
represent a lisp form; ARGS will be ignored in that case."
  (eshell-condition-case err
      (let ((result
             (save-current-buffer
               (if form-p
=>                 (eval func-or-form)★
                 (apply func-or-form args)))))
        (and result (funcall printer result))
        result)
    (error
     :

カーソルが★の所での結果は、mini-bufferに Result: "lastval" って出たた。オイラーが与えたS式は評価が終わったって事だ。メッセージバッファーは

Result: (progn (prin1 "HOGE") "lastval")

"HOGE"
Result: "lastval"

このようにHOGEが送りこまれていた。他のmsgはedebugが出したものだろう。

はて、この後どうする?

debug

別のdebug方法も試してみる。 debugモードでお手軽にやるなら、止めたい関数を指定してから実行。

/usr/pkg/share/emacs/26.1/lisp/eshell $ (debug-on-entry 'eshell-exec-lisp)
/usr/pkg/share/emacs/26.1/lisp/eshell $ (progn (prin1 "hoge") "LAST")

画面が割れて、バックトレース画面が出て来る。

Debugger entered--entering a function:
* eshell-exec-lisp(eshell-print eshell-error (progn (prin1 "hoge") "LAST") nil $
  eshell-lisp-command((progn (prin1 "hoge") "LAST"))
  eval((eshell-lisp-command '(progn (prin1 "hoge") "LAST")))
    :
  command-execute(eshell-send-input)

dでステップ実行、cで継続するとな。(cancel-debug-on-entry)で、キャンセル出来る。

これで追っても、やはりevalした時点で、メッセージバッファに出力されちゃうんだよな。

gdb

prin1の主力がメッセージバッファに行っちゃうなら、その設定が何処かでなされているはず。 grep Message *el しても、引っかからず。

ならばCのソースレベルまで立ち返る必要が有るな。やおら、 prin1の説明を見る

(prin1 OBJECT &optional PRINTCHARFUN)

Optional argument PRINTCHARFUN is the output stream, which can be one
of these:

   - a buffer, in which case output is inserted into that buffer at point;
   - a marker, in which case output is inserted at marker's position;
   - a function, in which case that function is called once for each
     character of OBJECT's printed representation;
   - a symbol, in which case that symbol's function definition is called; or
   - t, in which case the output is displayed in the echo area.

optionで、schemeのport相当が使えるんだな。これがセットされていないか?

(gdb) b Fprin1
Breakpoint 3 at 0x8168050: file print.c, line 625.
(gdb) c
Continuing.

Breakpoint 3, Fprin1 (object=XIL(0x99fc0fc), printcharfun=XIL(0)) at print.c:625
625     {
(gdb) p object
$1 = XIL(0x99fc0fc)
(gdb) xpr
Lisp_String
$2 = (struct Lisp_String *) 0x99fc0f8
"HELLO"
(gdb) p printcharfun
$3 = XIL(0)
(gdb) xpr
Lisp_Symbol
$4 = (struct Lisp_Symbol *) 0x846d8a0 <lispsym>
"nil"

Uum 予想が外れたわい。説明によるとprintcharfunがnilの場合、出力はstdout送りになるそうな。

諦めて、eshellのelispレベルのバックトレースをgdbのbtの最後から拾ってみた。

Lisp Backtrace:
"prin1" (0xbfdda030)
"progn" (0xbfdda0cc)
"eval" (0xbfdda19c)
"eshell-exec-lisp" (0xbfdda3c0)
"eshell-lisp-command" (0xbfdda5d0)
"eval" (0xbfdda73c)
"eshell-do-eval" (0xbfddab48)
"eshell-do-eval" (0xbfddaf00)
"condition-case" (0xbfddb0ac)
"eval" (0xbfddb174)
"eshell-do-eval" (0xbfddb530)
"let" (0xbfddb6cc)
"eval" (0xbfddb794)
"eshell-do-eval" (0xbfddbba8)
"eshell-do-eval" (0xbfddbf60)
"catch" (0xbfddc0ec)
"eval" (0xbfddc1b4)
"eshell-do-eval" (0xbfddc5c8)
"eshell-do-eval" (0xbfddc980)
"let" (0xbfddcb1c)
"eval" (0xbfddcbe4)
"eshell-do-eval" (0xbfddcff0)
"eshell-resume-eval" (0xbfddd1e0)
"eshell-eval-command" (0xbfddd414)
"eshell-send-input" (0xbfddd788)
"funcall-interactively" (0xbfddd784)
"call-interactively" (0xbfddd900)
"command-execute" (0xbfdddb6c)

こちらは、参考までに、スクラッチでの実行時バックトレース結果。物の資料の説明を余す事なく再現してるな。

Lisp Backtrace:
"prin1" (0xbfddd030)
"progn" (0xbfddd0cc)
"eval" (0xbfddd1b8)
"elisp--eval-last-sexp" (0xbfddd39c)
"eval-last-sexp" (0xbfddd56c)
"eval-print-last-sexp" (0xbfddd788)
"funcall-interactively" (0xbfddd784)
"call-interactively" (0xbfddd900)
"command-execute" (0xbfdddb6c)

scsh

eshellばかりがshellじゃ無い。記憶を頼りに探したら、見つかった。

nb8$ cat DESCR
Scsh is a Unix shell in that is has significant syntax extensions to
make writing Unix shell scripts easy (constructing pipelines, setting
I/O redirection, conditional execution etc.). It also offers access to
lower-level functionality like all Posix system calls, TCP/IP sockets
and a full-featured regular expression library. This is embedded into
a general-purpose programming language with real data types,
extensive, syntactically clean control constructs and "real" quoting
rules.

Scsh is also a full implementation of R4RS Scheme with some
non-standard behavior (required for scripting). As a result, a wide
variety of existing Scheme code can be used.

The underlying Scheme implementation is a virtual machine for compact
byte code.

NetBSDに有った案内。以外に不人気みたいでFreeBSDにもウブにも収録されていなかった。 本山は、 scshだよ。 そして、使用例は SCSH(Scheme Shell)スクリプト入門に出てた。

後でDebianで試してみるか。