Hatena::ブログ(Diary)

mooz deceives you

(about 'mooz) ; => "See http://mooz.github.com/index-ja.html"

 

September 11 (Sun), 2011

atool を使い dired のサポートする圧縮形式を増やす

atool を使い dired のサポートする圧縮形式を増やす設定を紹介する.

atool

atool <http://www.nongnu.org/atool/> というツールがある.

これは様々な圧縮形式・ツールに対するフロントエンドツールで,ファイルの拡張子から適切な圧縮形式を推測し,内部で適切なコマンドを呼び出してくれるという優れもの.

標準で搭載しているシステムは少ないが,たいていはソフトウェアリポジトリに入っている.例えば Ubuntu なら apt でインストールすることができる.

sudo apt-get install atool

使い方は簡単で,例えばファイル FILE を解凍する場合は次のようにすれば良い.

aunpack FILE

tar xvfgunzip など圧縮形式毎にコマンドを記憶する時代は終わった.こうして人間は退化していく.

dired の圧縮・解凍機能

dired というツールがある.

これは Emacs 上で動くファイラで,とりたてて「ここが優れている」という部分もないが,標準で付属してくるため未だ利用する者も多い.

さて,そんな dired にはファイルを圧縮・解凍するコマンドが存在する.デフォルトの状態ではファイルを選択した(ポイントを上に持ってきた)状態で Z キーを押すと,選択されたファイルが圧縮されている場合は解凍され,未圧縮の場合は gzip 形式で圧縮される*1

圧縮ファイルの場合は解凍され,未圧縮ファイルの場合は圧縮されると書いた.では「そのファイルが圧縮されているかどうか」という判断はどのようにして行なっているのだろうか.dired は拡張子による判断方式を採用しており,内部に「このパターンにマッチする場合は圧縮ファイルなので,このプログラムを使って解凍すること」というルールを保持している.

そのルールの中身を覗いてみよう.

(defvar dired-compress-file-suffixes
  '(("\\.gz\\'" "" "gunzip")
    ("\\.tgz\\'" ".tar" "gunzip")
    ("\\.Z\\'" "" "uncompress")
    ;; For .z, try gunzip.  It might be an old gzip file,
    ;; or it might be from compact? pack? (which?) but gunzip handles both.
    ("\\.z\\'" "" "gunzip")
    ("\\.dz\\'" "" "dictunzip")
    ("\\.tbz\\'" ".tar" "bunzip2")
    ("\\.bz2\\'" "" "bunzip2")
    ;; This item controls naming for compression.
    ("\\.tar\\'" ".tgz" nil))
; snip

見ると,非常に限られたルールしか用意されていないことが分かる.広く使われている zip に対するルールすら見当たらない.

しかしながら,そこは拡張性の高い Emacs.この dired-compress-file-suffixes へは後から自由にルールを追加することができる.必要なものがあれば,自分で追加すれば良い.

といっても ("\\.zip\\'" "" "unzip") という具合に適切なプログラムを拡張子毎に追加していくのはさすがに骨が折れる.今回は,ここで先ほどの atool を利用することにした.とりあえず様々な圧縮形式を登録しておき,適切なプログラムは全て atool に判断してもらおうというわけだ.

dired の圧縮・解凍機能で atool を使う

というわけで dired の圧縮・解凍機能で atool を使うような設定を行なった.コードを以下に示す.

(defvar my-dired-additional-compression-suffixes
  '(".7z" ".Z" ".a" ".ace" ".alz" ".arc" ".arj" ".bz" ".bz2" ".cab" ".cpio"
    ".deb" ".gz" ".jar" ".lha" ".lrz" ".lz" ".lzh" ".lzma" ".lzo" ".rar"
    ".rpm" ".rz" ".t7z" ".tZ" ".tar" ".tbz" ".tbz2" ".tgz" ".tlz" ".txz"
    ".tzo" ".war" ".xz" ".zip"))

(eval-after-load "dired-aux"
  '(progn
     (require 'cl)
     (loop for suffix in my-dired-additional-compression-suffixes
           do (add-to-list 'dired-compress-file-suffixes
                           `(,(concat "\\" suffix "\\'") "" "aunpack")))))

この設定により,大抵の圧縮ファイルは dired で Z キーを押せば解凍できるようになる.

atool がサポートする圧縮形式のリスト *2 は man ページに記載されているため,以下のようなスクリプトを使って抽出した.

#!/usr/bin/env ruby
# Usage: man atool |& ./extract.rb

manpage = $stdin.read
manpage.sub!(/.*^ARCHIVE TYPES/, "")
manpage.sub!(/^CONFIGURATION.*/, "")

exts = manpage.
  scan(/\((\.(?:\.|[a-z0-9A-Z])+)(?:[ ,]+(\.(?:\.|[a-z0-9A-Z])+)\))*/).
  flatten.
  reject { |elem| elem.nil? }.
  map { |ext| ext.split(".")[-1] }.
  uniq

puts exts.join("\n")

まとめ

atool を使い dired のサポートする圧縮形式を増やす設定を紹介した.この圧縮・解凍機能は最低限の実装という印象で,実際に使ってみると様々な改善点*3が見えてくる.それらに関しては,気が向いたときにでも改善を図っていきたい.

*1dired-do-compress という関数が呼ばれている

*2my-dired-additional-compression-suffixes

*3:そのファイルが圧縮されるのか解凍されるのかが分からない.圧縮・解凍が同期的に行われブロックする.など.

sr10sr10 2011/10/03 04:05 unpってのもありますん。
でもatoolの方が高機能臭い。

moozmooz 2011/10/04 11:40 >sr10さん

おお,unp なんてものもあるのですね.そちらは unpack 専用と.

May 04 (Wed), 2011

Shadow.el - Emacs に Shadow.vim を

Shadow.vim

Vim 用のプラグインに Shadow.vim というものがあります.

これは,

  1. ファイルを保存するたびに
  2. 指定されたコマンドを実行し
  3. 実行結果を保存する

という作業を行なう非常にシンプルなプラグインで,作者である ujihisa さんは,以下のように説明されています.

これは任意の言語で任意の事前処理を行なうための薄いフレームワークです。コマンドや関数は提供されず、以下のようなファイル読み込み時と保存時のフックのみを提供します。

シンプルでこそあれ,小粒でもピリリと辛いプラグインです.その使い方,そして応用例に関しては no title を見ると良いでしょう.

Shadow.el

さて,先述の Shadow.vim ですが,残念ながら Emacs では動作しません.当たり前ですね.

話は変わりますが,先日開催された Yokohama.vim #1 で「Vim も Emacs も,お互い良いところはパクり合えば良い」という話を耳にしました.Shadow.vim は明らかに「良いところ」に該当します.

こうした理由から,自然な流れで Emacs で動く Shadow.vim,もとい Shadow.el を作成するに至ったわけです.

インストール

インストールは shadow.el をダウンロード し,このファイルを Emacs のロードパス内にあるディレクトリに配置した上で .emacs などの設定ファイルへ以下の一行を記述することで完了します.

(require 'shadow)

また,説明は後述しますが,次のような設定を行なっておくと,foo.txt というファイルを開く際に foo.txt.shd というファイルが存在した場合 foo.txt.shd が自動的に開かれ,foo.txt 側で auto-revert-mode が有効となります.

(add-hook 'find-file-hooks 'shadow-on-find-file)
(add-hook 'shadow-find-unshadow-hook
          (lambda () (auto-revert-mode 1)))
動作

Shadow.el は,ファイルの拡張子が .shd であった場合,そのファイルが保存されるたびに以下の作業を行ないます.

  1. .shd ファイル内からコマンドを取得
    • Emacs のファイルローカル変数で shadow-command に値が設定されていれば,これを用いる (Emacs スタイル)
    • shadow-command に値が設定されていなければ,ファイル一行目からコマンドを取得 (Shadow.vim スタイル)
  2. 取得したコマンドの標準入力へ .shd ファイルの内容を渡す
  3. コマンドの実行結果を保存
    • この際,先程のファイル名から .shd を取り除いたものが保存されるファイル名として用いられる

例えばファイル名が foo.js.shd であった場合,foo.js.shd の内容を指定されたコマンドへ渡した結果が foo.js として保存されます.このときコマンドに CoffeeScript のコンパイラを指定しておくことにより,ファイルが保存されるたびにコンパイルを透過的に行なうことが可能となるわけです.

こうした自動コンパイルはファイルシステムのイベントを監視するというアプローチ(OMake など)によっても実現できますが,これに対して Shadow.el には「foo.js.shd ファイルを一つ用意するだけで良い」という利点があると言えます.

また,余談ではありますがコンパイルして生成される foo.js を開いておき M-x auto-revert-mode としておくと,ファイルが更新されるたびにバッファの内容も更新してくれるため,大変作業効率があがります.

Shadow.vim スタイルのコマンド指定

Shadow.vim スタイルのコマンド指定は非常にシンプルです..shd ファイルの一行目,先頭から 3 文字を除いた残りがコマンドとして解釈されます.

以下にいくつか(全く実用的ではない)例を掲載します.

まずは,C プログラムで DATE という文字列をを全て date コマンドの実行結果に置き換えるもの.

// sed -e "s/DATE/`date`/g"
static const char *last_modified = "DATE";

次に,Python プログラムでマクロを利用する例.大変うれしいですね……

#  cpp -P -
#define MAX(x, y) ((x) if (x) > (y) else (y))

import random

if __name__ == "__main__":
    a = random.random()
    b = random.random()
    c = random.random()
    print(a, b, c)
    print(MAX(MAX(a, b), c))

さて,お気づきかもしれませんが Shadow.vim スタイルは /* sed -e "s/DATE/`date`/g" */ のような C スタイルのコメントと相性が良くありません.この場合,先頭 3 文字が取り除かれた残りの sed -e "s/DATE/`date`/g" */ がコマンドとして利用されますが,末尾に */ というゴミが入っています.このように「囲うタイプ」のコメントを持つ言語では,以下のファイルローカル変数による指定を利用する必要があります*1

Emacs のファイルローカル変数によるコマンド指定

Shadow.el は shadow-command というバッファローカル変数に文字列が指定されていた場合,その内容をコマンドとして解釈します.ゆえに Emacs のファイルローカル変数指定機能を利用してこの変数へ値を設定することにより,ファイル毎にコマンドを指定することが可能です.

ファイルローカル変数の詳細に関しては 404 Not Found を参照してください.

まず,ファイル冒頭のファイルローカル変数指定による例を示します.

# -*- mode: coffee; shadow-command: "coffee -csb"; -*-
f = (x) -> x + 1
print f 10

次に,ファイル末尾のファイルローカル変数指定による例を示します.

f = (x) -> x + 1
print f 10

# Local Variables:
# mode: coffee
# shadow-command: "coffee -csb"
# End:

以上は伝統的な Emacs の記法ですが,Shadow.vim の記法と比較するとやや煩雑な印象もあります.

まとめ

Shadow.vim の Emacs 版と言える Shadow.el の紹介を行ないました.

非常にシンプルな仕組みですが,応用例は非常に幅広く,使いでのあるプラグインです.Shebang との兼ね合いなど色々と課題も見えたため,少しずつ改善を図っていきたいと考えています.

*1:# */ のようにしてシェル側でのコメントアウトを期待しても,内部的には (sed '1d' ./foo.c.shd | sed -e \"s/DATE/`date`/g\" # */) > ./foo.c のように展開されるためこれまたマズいのです.この辺りは手抜きから来る問題です……

susu 2011/06/20 11:57 10個前の履歴を貼り付けようとすると、全てのアイテムを貼り付けてしまいます
20個前、30個前なども全てのアイテムを貼り付けてしまいます
対応よろしくお願いします

April 03 (Sun), 2011

scratch バッファなどでも inf-ruby.el の ruby-send-region を動かす

Emacs の scratch バッファで inf-ruby.el の ruby-send-region を呼ぶと,次のようなエラーが出る.

irb(main):001:0> 
TypeError: can't convert nil into String
	from (irb):1:in `eval'
	from (irb):1

ここで,該当バッファの内容を適当なファイルへ保存した上でもう一度 ruby-send-region を呼んでやると,今度は上手くリージョン内のコードが評価される.

原因は何だろうと思い inf-ruby.el の中身を覗いてみたところ,ruby-send-region は次のような文字列を Ruby インタプリタへ送信している,ということが分かった.

eval <<'コード', nil, ファイル名, 行番号

ここでのファイル名は (buffer-file-name) から取得されているが,これは scratch バッファなど「ファイルと関連付けられていないバッファ」に対して nil を返す.つまり scratch バッファから ruby-send-region を呼ぶと,以下のようなコードが Ruby 側で評価されることとなる.

eval <<'コード', nil, nil, 行番号

偶然にも Ruby には Lisp と同様 nil が存在するため構文エラーにはならないが,eval はファイル名に文字列を期待するために,冒頭のエラーが出てしまうというわけだ.

そこで,次のようなパッチを inf-ruby.el へ当て,ファイル名が必ず文字列となるように修正してやる.

--- inf-ruby.el.org	2009-10-02 21:04:37.000000000 +0900
+++ inf-ruby.el	2011-04-03 13:34:31.377009424 +0900
@@ -311,7 +311,7 @@
         (goto-char m)
         (insert ruby-eval-separator "\n")
         (set-marker m (point))))
-    (comint-send-string (ruby-proc) (format "eval <<'%s', nil, %S, %d\n" term file line))
+    (comint-send-string (ruby-proc) (format "eval <<'%s', nil, %S, %d\n" term (or file "") line))
     (comint-send-region (ruby-proc) start end)
     (comint-send-string (ruby-proc) (concat "\n" term "\n"))))
 

これで scratch バッファから ruby-send-region を呼び,コードの評価が行えるようになった.

March 20 (Sun), 2011

anything-font-families で Emacs のフォント一覧を便利に表示

フォントの表示確認

Emacs におけるフォントの設定では「どのフォントがどのように表示されるか」ということを確認するのがとても面倒です.私は毎回 M-x customize-face としてから family へフォント名を設定し表示を確認する,という手段をとっていたのですが,非効率的にもほどがありました.

何か良いインタフェースは無いものだろうか.そう考えていたときにふと思い浮かんだのが anything-colors でした.これは anything.el を用いて Emacs 組み込みの色や face の一覧表示を行なう関数で,次のような特徴を持ちます.

  • 色や face が実際にどのように表示されるかを確認可能
  • anything.el インタフェースにより,インクリメンタルな絞り込みが可能
  • anything.el インタフェースにより,色や face の名前をコピーや挿入することが可能

このインタフェースはフォントにも適用できるかもしれない.そう思って作成したものが,今回紹介する anything-font-families となります.

anything-font-families

f:id:mooz:20110320182622p:image

anything-font-families は Emacs で利用可能なフォントを anything.el で表示するもので,anything-colors と同様に次のような特徴を持ちます.

  • フォントが実際にどのように表示されるかを確認可能
  • anything.el インタフェースにより,インクリメンタルな絞り込みが可能
  • anything.el インタフェースにより,フォント名をコピーや挿入することが可能

導入は,以下のソースコードを設定ファイルへ記述することにより行なってください.

(require 'cl)  ; loop, delete-duplicates

(defun anything-font-families ()
  "Preconfigured `anything' for font family."
  (interactive)
  (flet ((anything-mp-highlight-match () nil))
    (anything-other-buffer
     '(anything-c-source-font-families)
     "*anything font families*")))

(defun anything-font-families-create-buffer ()
  (with-current-buffer
      (get-buffer-create "*Fonts*")
    (loop for family in (sort (delete-duplicates (font-family-list)) 'string<)
          do (insert
              (propertize (concat family "\n")
                          'font-lock-face
                          (list :family family :height 2.0 :weight 'bold))))
    (font-lock-mode 1)))

(defvar anything-c-source-font-families
      '((name . "Fonts")
        (init lambda ()
              (unless (anything-candidate-buffer)
                (save-window-excursion
                  (anything-font-families-create-buffer))
                (anything-candidate-buffer
                 (get-buffer "*Fonts*"))))
        (candidates-in-buffer)
        (get-line . buffer-substring)
        (action
         ("Copy Name" lambda
          (candidate)
          (kill-new candidate))
         ("Insert Name" lambda
          (candidate)
          (with-current-buffer anything-current-buffer
            (insert candidate))))))

内部

(font-family-list) で Emacs において利用可能なフォント名一覧が得られるようなので,そこで得たフォントを font-lock-face での :family に指定することにより,そのフォントを実際に用いて表示を行なっています.

自分の環境では :weightbold にしないと一部のフォントがうまく表示されなかったため bold 指定を行なっていますが,この辺りは好みに応じて変更して下さい.

また,anything-match-plugin を利用している場合,絞り込みを行なった際にマッチ部分がハイライトされますが,このハイライトによって表示が崩れてしまうという問題が生じてしまったために flet を用いて anything-mp-highlight-match-internal を無効にしています.本当は anything-match の定義を局所的に変更したかったのですが,方法が分からなかったために断念しました.

(require 'cl) に関しては,見なかったことにして下さい.

まとめ

anything-font-families を使い,より自由なフォント設定を楽しみましょう.

January 30 (Sun), 2011

python.el の run-python がもたつく理由,その対策

Emacs 上で Python をいじりたいという時には M-x run-python として Python インタプリタを立ち上げるのだが,この run-python の起動がもたつくように感じられ,長らく気になっていた.

何度も run-python とするたびにストレスがつのり,つい先ほど我慢が出来なくなりソースコードを覗いたところ,原因が判明.以下に該当部分のソースコードを示す.

(defun run-python (&optional cmd noshow new)
  ;; <省略>
  (if (derived-mode-p 'python-mode)
      (setq python-buffer (default-value 'python-buffer))) ; buffer-local
  ;; Without this, help output goes into the inferior python buffer if
  ;; the process isn't already running.
  (sit-for 1 t)        ;Should we use accept-process-output instead?  --Stef
  (unless noshow (pop-to-buffer python-buffer t)))

下から二行目に sit-for とあるが,これは指定時間の sleep を行う関数だ.ここでは 1 が指定されているため run-python を呼ぶと「必ず 1 秒は待たされる」ということになる.

何でまたこんなことになっているのかというと,丁寧にコメントがそえられてあった.意訳すると,次のようになる.

Python は起動時にヘルプメッセージを出力する.このため,すぐに Python のインタプリタが走っているバッファ (*Python* のこと) へ移動してしまうと,Python が起動時に出力したヘルプメッセージがそのバッファへ挿入されてしまうことになる.

そのため,とりあえず 1 秒ほど待ち Python がヘルプメッセージを出力し終えたのではないか,という時点になったら *Python* バッファに移動することにした.

「とりあえず 1 秒ほど待っておけ」という楽観的なスタンスが全くもって気に食わない.Python の起動に 1 秒以上かかる環境ではどうするというのだろうか.

また,この run-python を使っている限り,既に *Python* バッファが作成されてプロセスが起動している場合,単純なバッファ移動にも「必ず 1 秒待たされる」こととなる.これは本当に馬鹿げた話だ.

これが Emacs に標準添付されているものだと思うと涙が出てくるが,されてしまったものは仕方がない.結局,次のような設定により対処することにした.

(defadvice run-python (around run-python-no-sit activate)
  "Suppress absurd sit-for in run-python of python.el"
  (let ((process-launched (or (ad-get-arg 2) ; corresponds to `new`
                              (not (comint-check-proc python-buffer)))))
    (flet ((sit-for (seconds &optional nodisp)
                    (when process-launched
                      (accept-process-output (get-buffer-process python-buffer)))))
      ad-do-it)))

sit-for 関数を上書きし,Python インタプリタが新しく立ち上げられた場合に,ヘルプの出力が完了するのを待つようにしている.これで理不尽に 1 秒待たされることもなくなった.