コンピュータの方がうまくできることに人間を使うな。 -- Tom Duff
私の見る限り、プログラマという人種は決まって事務作業が嫌いな ようである。同じような書類を何枚も書かされたり、1つ誤字があ るから書き直せと言われたりすると、異常にストレスがたまる。
同様に、計算機を使っていても、同じような作業を何度も繰り返し たり、ちょっとでも間違えたら最初からやり直し、というのは耐え られない。そこで、今回は履歴を活用して作業の再利用をするノウ ハウを取り上げる。
シェルの履歴
Unixを使い始めて間もない初心者が作業しているところを見ると、 打ち間違えたコマンドを律儀に最初から打ち直しているのに気づく ことがある。そんなときに、シェルの履歴を使えば最初から打ち直 さなくてもいいよ、と教えてあげると、なるほどと感心される。プ ログラミングにおいて部品の再利用が重要なのと同様に、Unixで作 業する上では履歴の再利用が重要である。
大学などの環境では csh や tcsh が標準であることが多いようだ が、私はシェルに zsh*2 を使っている ので、ここでは zsh を取り上げる。zsh は他のシェルにはない強 力な機能を多く装備しているので重宝している。
インストールした直後に zsh を実行すると、Emacs と同じ感覚で キー操作が行える。ただし、環境変数 EDITOR に vi が設定されて いる場合は ~/.zshrc *3 に
bindkey -e # Emacsと同じキー操作を行う
と設定を加えないと Emacs のキー操作が有効にならないので注意 が必要である。*4
1つ前の履歴に戻るには C-p (up-line-or-history) という キー操作を行う。たとえば、cp コマンドを xp と打ち間違えて*5
% xp foo.txt /tmp zsh: command not found: xp %
とコマンドの実行に失敗した場合、C-p を押せば、再び
% xp foo.txt /tmp
の状態に戻ることができる。ここで、xp を cp に修正すれば、全 体を最初から打ち直す代わりに、1つ前の履歴をわずかに修正する だけで、目的のコマンドを実行できる。
履歴をインクリメンタル検索
C-p での操作では、10個前の履歴を再利用するのに C-p を10回も打つ必要があり面倒である。そこで、 C-r (history-incremental-search-backward) を使えば、目 的のコマンドをインクリメンタル検索で簡単に探しだせる。たとえ ば以前に実行した diff コマンドを再実行したい場合は文字列 ``diff'' を含む履歴をインクリメンタル検索すればいい。次の例 では C-r d i f の 4打鍵で検索が完了 している。C-p を10 回打つよりも楽である。
zshのインクリメンタル検索1
zshのインクリメンタル検索2
zshのインクリメンタル検索3
zshのインクリメンタル検索4
上の例では diff unimag.rd~ unimag.rd というコマンドが見つかっ たが、この結果が気に入らない場合は、続けて C-r を打て ば ``dif'' を含む次の候補を検索できる。
履歴をファイルに保存
何も設定しない状態では zsh を終了すると履歴が消えてしまうた め、再び zsh を実行したときに、以前の履歴を再利用することが できない。履歴の再利用を可能にするためには、次の設定を ~/.zshrc に加えればいい。これにより、~/.zsh-history ファイル に履歴を保存され、zshを再実行したときに 以前の履歴を再利用で きるようになる。
HISTFILE=$HOME/.zsh-history # 履歴をファイルに保存する HISTSIZE=100000 # メモリ内の履歴の数 SAVEHIST=100000 # 保存される履歴の数 setopt extended_history # 履歴ファイルに時刻を記録 function history-all { history -E 1 } # 全履歴の一覧を出力する
上の設定では最大 10万行の履歴が保存される。私の場合は、約8か 月で58,000行の履歴が保存された。1日あたり約 240行の増加であ る。ファイルサイズは約 1.9MB である。
10万行も履歴を残しても意味がないと思われるかもしれないが、履 歴を検索すれば大昔に実行した複雑なワンライナーを再利用できて 便利である*6。簡単な検索ならC-r のインクリメンタル検索で行えるが、ちょっと複雑な場合は grep を使えばいい。たとえば、「カレントディレクトリ以下の大文字の ファイル名をすべて小文字にする」という複雑なワンライナーを再 利用したいときに、コマンドラインに "find" と "tr" という文字 列が含まれていたのを覚えているなら、
% history-all | grep find | grep tr
と実行すれば、
5603 13.6.2001 07:11 16:54 find . -depth -print | while read i; do mv $i `dirname $i`/`basename $i | tr A-Z a-z`; done
という半年前の履歴が得られる。今後も使いそうなワンライナーな らシェルスクリプトとして実行ファイルを作っておくと便利である。 私の場合は上のワンライナーを downcase-rename という名前のシェ ルスクリプトとして ~/bin に保存した。
なお、上のワンライナーは、ある CD-ROM を Unix から読んだとき に、ファイル名がすべて大文字なのに、収録されている HTML 文書 内のリンクが小文字で書かれているため HTMLのリンクがまったく たどれない、という問題に遭遇したときに作ったものである。
履歴の共有
Unix での作業中に kterm や Tera Term*7 などの端末エミュレータ (以下、端末) をたくさん開く人は多い。 私もその 1人だが、以前にシェルの履歴を再利用しようとしたとき に、どの端末に目的の履歴が残っているかわからずずいぶん不便を 感じた。
この問題は、シェルのプロセスごとに履歴が別々に管理されている ことが原因である。zsh では次の設定を ~/.zshrc に加えれば、同 一ホストで自分が動かしているすべての zsh のプロセスで履歴を 共有できる。
setopt share_history
履歴の共有は、コマンドを実行した直後に ~/.zsh-history ファイ ルに履歴を保存することによって実現されている。ただし、端末間 の履歴は完全に同期しているわけではなく、コマンドラインから、 ls などのコマンドを 1回実行するか、Enter キーを打ったタイミ ングで履歴の同期が行われる。このため端末Aで実行したコマンド は、その直後に端末Bで C-p キーで 1つ前の履歴をたどって も出てこない。端末Bで Enter キーを打って履歴を同期すれば端末 Aで実行したコマンドの履歴が得られる。
アンドゥ
zsh では C-/ キーでアンドゥ (編集のやり直し) が行え る。たとえばコマンドラインの編集中に誤って M-d (kill-word) でカーソル位置の単語を消してしまった場合 *8、 C-/ で単語を元に戻すことができる。C-/を何度も打 てば、どんどん過去に遡ることができる。
履歴とアンドゥを使いこなせば、コマンドを最初から打ち直すと いう苦痛を味わわずに生活が送れる。
補完
zshといえば強力な補完が有名である。補完はファイル名やコマン ド名などを入力を省略するための機能である。zshの補完機能を詳 しく紹介すると長くなるので、ここでは軽く紹介するにとどめる。
ファイル名の補完
コマンドラインでファイル名を途中まで入力して <tab> を 打つと、残りのファイル名が補完される。
補完候補が複数あるときはコマンドラインの下に候補が一覧表示さ れる。
zstyle ':completion:*:default' menu select=1
という設定を ~/.zshrc に入れておけば、一覧表示された補完候補 を C-n C-p C-f C-b のカーソルで選択 することができる。補完候補を一覧表示させた時点で <tab> をもう 1度打つとカーソルが表示される。 zstyleコマンドは、zshの補完などの挙動を変更するための zshの 内部コマンドである。
コマンド名の補完
Unixの頻繁に使うコマンドは ls, cp, mv など極端に短い名前がつ けられているため、キーボードからすばやく入力できるが、さきほ ど紹介したシェルスクリプト downcase-rename のような 15文字も の長い名前のコマンはいちいち入力するのが面倒である。そこで、 コマンド名の補完機能を使えば d o w n <tab> の 5打鍵で済む。コマンド名を途中まで入力して <tab> を打つと、残りが補完される。
コマンド特有の補完
zsh ではコマンドごとに補完の挙動を定義できる。gcc や cvs な どの一般的なコマンドについては、zshのパッケージに標準で補完 の定義ファイルが付属している。それらの定義ファイルを有効にす るには ~/.zshrc に次の設定を加えればいい。
autoload -U compinit compinit
上の設定を行うと、たとえば gcc -funroll-loops とコマンドを入 力するのに補完を使ってg c c <space> - f u n <tab> <tab> l <tab> のように行える。
zshのgcc補完1
zshのgcc補完2
zshのgcc補完3
zshのgcc補完4
zshのgcc補完5
その他
zsh には、この他にも rm **/*(x.)
でカレントディレクト
リ以下のすべての実行ファイルを削除したり、M-q で入力中
のコマンドラインをスタックに一時待避したりといった、入力の手
間を減らす強力な機能が備わっている。zsh の日本語の情報源はい
くつかあるが、手始めには ``zsh for the working researcher''
*9
が参考になる。
Emacsの履歴
プログラムにしても文章にしても、テキストを編集する際にはファ イルを開いたり、コピーアンドペーストしたりといった操作が頻繁 に繰り返される。履歴を活用すれば、そういったテキスト編集を効 率よく行える。ここではテキストエディタ Emacs の履歴を活用す る方法を紹介する。
コピーアンドペースト
Emacs では C-<space> で範囲選択を開始してカーソルを移 動した後に C-w (kill-region) で選択範囲を切り取るか、 あるいは M-w (kill-ring-save) で選択範囲をコピーすると、 選択範囲の文字列が kill-ring と呼ばれる変数に保存される。 kill-ring はリストの構造をしているため、切り取り・コピーされ た文字列は過去に遡れる履歴として記録される。
kill-ring に保存された文字列は、C-y (yank) を打てば取 り出すことができる。
C-y に続いて M-y (yank-pop) を打つと、1つ前の文 字列を取り出せる。M-y を連続して打てば kill-ring に格 納された文字列を過去に遡って取り出すこともできる。しかし、 zshで C-p で履歴を遡るときと同様に、古い履歴を取り出す には M-y を何度も打つ必要があるため面倒である。
そこで、 大城尚紀氏による kill-summary.el *10 を使えば、 M-x kill-summary で kill-ring の内容を一覧 表示してその中から選ぶことができるため、たいへん便利である *11。kill-summary を使うには kill-summary.el をインストー ル*12 して、 ~/.emacs に次の設定を加える。
(autoload 'kill-summary "kill-summary" nil t)
複数行にまたがる候補は 1行に圧縮されて表示されるため、巨大な 文字列を kill-ring に格納しても一覧性が低下することはない。 また、一覧表示の中で C-s でインクリメンタル検索を行う こともできる。
ミニバッファの履歴
C-x C-f (find-file) でファイルを開くと、そのファ イル名は file-name-history という変数に履歴が記録される。ミ ニバッファ*13でファイル名 を入力するときに M-p を打つと 1 つ前の履歴を取り出すこ とができる。
M-p を繰り返して打てば古い履歴を取り出せるが、何度も打 つのはやはり面倒である。そこで、永野圭一郎氏の minibuf-isearch *14 を 使えば、C-r でミニバッファ内で履歴をインクリメンタル検 索できるので、目的の履歴をすばやく取り出すことができる。ちょ うど zsh の C-r で履歴をインクリメンタル検索するのと同 様である。
minibuf-isearch
minibuf-isearch
minibuf-isearch
minibuf-isearch を使うには、 minibuf-isearch.el をインストー ルして ~/.emacs に次の設定を加える。
(require 'minibuf-isearch)
ファイル名に限らず M-% (query-replace) で置換に使った 文字列なども履歴が記録されているため、minibuf-isearch で同様 にインクリメンタル検索が行える。
履歴の保存
Christoph Wedler氏による session.el *15 を使う と、Emacsを終了するときに、kill-ring や file-name-history と いった変数の内容をファイルに保存して、次回に Emacs を立ち上 げたときに、それらの履歴を再利用できる。session.el をインス トールして~/.emacs に次の設定を加えれば履歴の保存が行われる ようになる。
(require 'session) (add-hook 'after-init-hook 'session-initialize)
アンドゥ
Emacs でも zsh と同様に C-/ キーでアンドゥ (編集のやり 直し) が行える。アンドゥは無制限に行えるので、C-/ を押 しっぱなしにすれば、すばやく過去に遡ることができる。アンドゥ しすぎてしまった場合は C-g で一度アンドゥを止めてから、 再び C-/ でアンドゥを行うと、アンドゥしすぎてしまった 分を取り戻せる。
最近使ったファイル
Windows のスタートメニューには「最近使ったファイル」という項 目がある。その名が示す通り、ここには最近使ったファイルの一覧 が登録されている。なかなか気の利いた機能である。
Emacs でも David Ponce氏の recentf.el*16 を使えば、File メニューに Open Recent が追加され、最近使った ファイルにすばやくアクセスできる。
recentf.el は Emacs 21 には標準で付属しているので、~/.emacs に次の設定を加えるだけですぐに使える。
(recentf-mode)
メニューをマウスで操作するのが嫌いな人は M-x recentf-open-files を実行すればいい。
動的略称展開
動的略称展開は、以前に入力したことのある単語を少ない打鍵数で 入力するための機構である。Emacs では dabbrev として実装され ている。たとえば、C言語のプログラムを書いているときに ``real_pathname'' という変数名を再び入力する際にr e a l M-/ (dabbrev-expand) と打てば ``real'' が ``real_pathname'' に展開される。動的略称展開は打 鍵数を減らすだけでなく、打ち間違いを減らすという点でも有効で ある。プログラミングにおいては長い変数名や関数名を入力するの にたいへん役立つ。
動的略称展開は、カーソル位置からテキストを前後に検索して補完 候補を見つけるという処理によって行われる。通常の dabbrev で は日本語の扱いに難があるが、前回の記事で紹介した日本語のイン クリメンタル検索 Migemo の技術を応用して、小松弘幸氏は日本語 の動的略称展開を実現している*17。
Rubyの履歴
オブジェクト指向スクリプト言語 Ruby*18 には irb (interactive ruby) という対話型のインタプリタが付属している。irbを使うと、 コードを書いて実行するという過程を対話的に行えるので、Rubyの ライブラリの使い方を調べたり、プログラムの一部分をテストした りするのに便利である。たとえば、バージョン 1.7 から追加され た Enumerable#inject というメソッドの使い方を確認するにはコ マンドラインから irb を実行して Enumerable#inject を使った小 さなコードを動かしてみればいい。
% irb irb(main):002:0> (1..10).inject(0) {|x,y| x + y} 55
このように irb は Rubyプログラミングに必須と言える便利なツー ルだが、履歴をファイルに保存しないため、irbを再実行したとき に前回に irbを実行したときのコードは再利用できない。そこで、 やまだあきら氏のirb-history.rb *19 を使えば、irb の履歴をファイルに保存できる。irb-history.rb をインストール *20 して ~/.irbrc に次の設定を加えれば、履歴が ~/.irb-history ファイルに保存されるようになる。
require "irb-history"
ついでに
require "irb/completion"
という設定を加えると、Rubyの言語仕様に沿った補完が行えるので 便利である。たとえば [1,2,3].rev まで入力して <tab> を 打つとメソッド名が補完されて [1,2,3].reverse が得られる。irb は readline ライブラリを利用しているため、 C-p で 1 つ 前の履歴を取り出す、C-r でインクリメンタル検索、 C-/ でアンドゥといった編集操作が行える。
実験: Unixで「最近使ったファイル」
Unix で「最近使ったファイル」を実現する方法を考えた。ここで は、開いたファイルの履歴をシステムコール open を横取りして記 録する、という強引な方法を紹介する。次のコード recentf.c は Red Hat Linux 7.2 (glibc 2.2.4 + gcc 2.96) で動作を確認して いる。他の OS では動かないが、アイディアを試す実験ということ で容赦していただきたい。
/* * システムコール open(2) を横取りして「最近使ったファイル」 * の履歴を取る実験 (Red Hat Linux 7.2 で動作確認) */ #include <time.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <limits.h> #include <sys/stat.h> #include <sys/syscall.h> static char * date (void) { static char datestr[BUFSIZ]; time_t t; struct tm *tm; time(&t); tm = localtime(&t); strftime(datestr, BUFSIZ, "%Y-%m-%d %H:%M:%S", tm); return datestr; } static void do_logging (const char *pathname) { FILE *fp; char log_filename[PATH_MAX]; char *user; user = getenv("USER"); if (user) { sprintf(log_filename, "/tmp/%s-recentf", user); fp = fopen(log_filename, "a"); if (fp) { fprintf(fp, "%s: %s\n", date(), pathname); fclose(fp); chmod(log_filename, 0600); } } } static int under_home_p (const char *pathname) { char *home, real_home[PATH_MAX]; int homelen, pathlen; home = getenv("HOME"); if (home) { realpath(home, real_home); homelen= strlen(real_home); pathlen = strlen(pathname); if (pathlen > homelen && strncmp(real_home, pathname, homelen) == 0) { return 1; } else { return 0; } } else { return 0; } } int open (const char *pathname, int flags, mode_t mode) { char real_pathname[PATH_MAX]; realpath(pathname, real_pathname); if (under_home_p(real_pathname)) { do_logging(real_pathname); } return syscall(SYS_open, pathname, flags, mode); } int __open(const char *, int, mode_t) __attribute__((weak, alias("open"))); int __libc_open(const char *, int, mode_t) __attribute__((weak, alias("open"))); int open64(const char *, int, mode_t) __attribute__((weak, alias("open"))); int __libc_open64(const char *, int, mode_t) __attribute__((weak, alias("open")));
recentf.c のコンパイル方法は次の通りである。
% gcc -shared -o recentf.so recentf.c
コンパイルが成功したら zsh から次のように実行して「最近使っ たファイル」の共有ライブラリを有効にする。具体的には、環境変 数LD_PRELOAD を利用して、openの関数定義を recentf.so で定義 した代替品で上書きしている。
% export LD_PRELOAD=$PWD/recentf.so
あとは、普通に作業を行っていれば /tmp/$USER-recentf*21 に、「最 近使ったファイル」の一覧が次のようなフォーマットで保存される。 このファイルの末尾を tail で閲覧すれば、最近使った10個のファ イルを知ることができる。
2002-01-17 17:20:17: /home/satoru/tmp/recentf.c 2002-01-17 17:20:50: /home/satoru/tmp/Makefile
問題点
recentf.c は ~/ 以下の open したファイルの履歴を記録する、と いう安直な方針で「最近使ったファイル」を実現しているため、意 図しないファイルまで履歴に記録されるという問題を抱えている。
たとえば、お気に入りの emacs lisp プログラムを ~/share/emacs/site-lisp に集めている場合、emacs を立ち上げる だけで ~/share/emacs/site-lisp 以下のファイルが大量に「最近 使ったファイル」に記録されてしまう。日常的に使うにはまだまだ 改良が必要である。
おわりに
以前に私は「横着をするための労力を惜しんではいけない」という 信条に基づいて、Unixの環境整備や作業のノウハウの蓄積にいそし んでいたのだが、ある日「横着をするための労力を惜しんではいけ ない、という口実で現実逃避してしまう」という法則を発見してか らは左記の信条は撤回することにした。環境整備ばかりしていて肝 心の作業がちっとも進まないという現実を目の当たりにしたのであ る。横着をするための労力もほどほどにしておきたい。
なお、本連載で作成したプログラムのソースコードは筆者のページ *22 から入手可能である。
謝辞
今回の原稿を執筆するにあたって、zsh の技術内容の確認のため田 中哲氏のお世話になった。ここに記して感謝する。
余談: 恐怖の教えたがり人間
今回の記事は、打ち間違えたコマンドを律儀に最初から打ち直した り、アンドゥを知らずに作業を最初からやり直したりして損をして いる初心者の手助けになればと思って書いた。しかしながら、私は、 初心者を手助けしているつもりが実際には相手に迷惑をかけている だけだったという失敗をしたことがある。
親切のつもりで「シェルの履歴を使うといいよ」と助言しているう ちに、ついつい調子に乗って「こうすればもっと簡単にできるよ」 などと言いながらキーボードを奪い取り、ややこしいワンライナー をごちゃごちゃと打ち込んで「ほら、どうだ」としたり顔をしてみ たり、「そんなやり方はダメだー!!」と絶叫しつつ目にも止まらぬ Emacs さばきを披露して「ほら、こうやれば一瞬でしょう」と自慢 を始めたりすると、もはや親切ではなく、ただの迷惑である。
私の場合、これをさんざんやった挙げ句に嫌われてしまった。よか れと思って教えてあげているつもりが、相手にとっては余計なお世 話だったのである。せっかく教えてあげているのに迷惑がられるの も困ったものだと疑問に感じていたのだが、ある日、ワインバーグ の『コンサルタントの秘密』*23 を読んでいると、 訳者の木村泉氏の前書きに次のような記述を見つけた。教えたがり で失敗していたのは私だけではなかったのである。
専門知識はあるとないでは大変な違いである。専門知識がないばか りにとんでもない損をしている人々はたくさんいる。だからといっ て、そういう人たちのところへ行って、ストレートにそれじゃだめ です、こうやるべきなんですよ、といってみたところで何にもなら ない。これはコンピュータのような技術の進歩の速い分野の知識・ 技能を身につけた若者が陥りやすい落とし穴である。訳者自身過去 には、そこのところを完全に誤解していて、いろいろ失敗をした。
ノウハウを相手に強引に詰め込もうとしても意味がなく、相手が自 分で学ぶのを助けるような助言をしたり質問をしたりするのがよろ しい、ということのようである。みなさんも、教えたがり症候群で 初心者に嫌われないように気をつけていただきたい。
参考文献
- 小松 弘幸, 高林 哲, 増井 俊之: 日本語動的単語補完方式Nanashikiを活用した予測入力, インタラクティブシステムとソフトウェアIX: 日本ソフトウェア科学会 WISS2001, pp. 67-74, December, 2001. <http://0xcc.net/pub/wiss2001.pdf>
- G. M. ワインバーグ, 『スーパーエンジニアへの道 - 技術リーダーシップの人間学』, 共立出版, 1991