Web検索すると s
キーや C-u s
でソート順を変更できるという情報が見つかりますがそういった話ではありません。悪しからず。
Diredではlsやls-lispが認識するオプションによってファイルをソートできますが、ソートのキーに出来るのはあくまでファイルシステムが認識できるもののみです。ファイルシステムの外にある何らかの情報に基づいてファイルをソートするにはどうしたら良いでしょうか。
テキストバッファレベルでのソート
基本的にDiredというのは(良くも悪くも)ファイル情報を並べただけのテキストバッファに過ぎないので、read-onlyを一時的に解除して好きなように並べ替えてしまえば良いのです。ただし、次のことに注意する必要があります。
C-x C-q
(dired-toggle-read-only
)はwdiredを起動してしまうので使えません。M-x read-only-mode
するか、Emacs Lispから(let ((inhibit-read-only t)) ...書き替え... )
しましょう。- ファイル部分には見えないテキストプロパティが設定されているのでそれが失われないようにしましょう。
- ディレクトリの範囲は
dired-subdir-alist
変数とマーカーで管理されているので、それが壊れないようにしましょう。 - Diredの一部の機能や拡張はファイル部分にオーバーレイを設定するので、それに配慮しましょう。いったん解除してから編集するか、または、それらが機能する前に割り込んで処理してしまうかです。
試しに M-x read-only-mode
の後にファイルの範囲を正確にリージョンで囲って、 M-x sort-lines
してみればソートされることが確認できると思います。
外部の情報を元にテキストバッファをソート
例えば何かファイルに対するスコアを列挙したファイル(.file-score.csv)があるとしましょう。
muscat.html,230 melon.html,140 grape.html,185 cherry.html,210 strawberry.html,153
これを読み込むには次のようなEmacs Lispの関数を作れば良いでしょう。
(defun my-dired-sort-read-file-score (dir) (ignore-errors (with-temp-buffer (insert-file-contents (file-name-concat dir ".file-score.csv")) (goto-char (point-min)) (cl-loop until (eobp) for (file score) = (split-string (buffer-substring (point) (line-end-position)) ",") collect (cons (expand-file-name file dir) (string-to-number score)) do (forward-line)))))
試しに実行してみると次のようになります。
(my-dired-sort-read-file-score "/products/")
(("/products/muscat.html" . 230) ("/products/melon.html" . 140) ("/products/grape.html" . 185) ("/products/cherry.html" . 210) ("/products/strawberry.html" . 153))
これを元にDired上でファイルをソートするには、sort-subr
関数を使用すると次のように書けます。
(defun my-dired-sort-region-by-file-score (beg end) (when-let ((file-score-alist (my-dired-sort-read-file-score (dired-current-directory)))) (save-restriction (narrow-to-region beg end) (goto-char beg) (sort-subr nil ; REVERSE #'forward-line ; NEXTRECFUN #'end-of-line ; ENDRECFUN ;; STARTKEYFUN (注意:決してnilを返さないこと) (lambda () (alist-get (dired-get-filename nil t) file-score-alist 0 nil #'string=)) nil ; ENDKEYFUN #'< ; PREDICATE ))))
sort-subr
はバッファ内のテキストをソートする汎用的な関数です(Sorting (GNU Emacs Lisp Reference Manual))。引数STARTKEYFUNには通常キーとなるテキストの先頭へ移動する関数を指定しますが、単にキーとなる値を返しても構いません。ここではファイル名に対応するスコアの数値を返しています。キーを数値にしたのでPREDICATEには #'<
を指定していますが、デフォルトのままでも大丈夫かもしれません。
試しにテスト用のファイルを用意して、Diredバッファ内でファイルをリージョンで囲い、 M-: (my-dired-sort-region-by-file-score (region-beginning) (region-end))
などとしてみるとちゃんとソートされることが分かります。
一つのディレクトリのファイルリストの範囲
ここまでソートする範囲を手動で指定していましたが、ディレクトリの先頭のファイルと最後のファイルの位置を自動的に求めるにはどうしたら良いでしょうか。
ここで注意する必要があるのが、Diredは一つのバッファ内に複数のディレクトリを表示できるという点です(dired-insert-subdir
等)。なのでその一つ一つのディレクトリ毎にファイルリストの範囲を求める必要があります。
最も単純なのはバッファ内の全行をスキャンしてファイルがある所とファイルが無いところを判別していくことです。
(defun my-dired-next-dir-files-region () ;; ファイル名が無いところをスキップ (while (and (null (dired-move-to-filename)) (= (forward-line) 0))) (unless (eobp) (let ((beg (line-beginning-position))) ;; ファイル名があるところをスキップ (while (and (dired-move-to-filename) (= (forward-line) 0))) (cons beg (line-beginning-position)))))
行にファイル情報があるかどうかはdired-move-to-filename
が非nilを返すかどうかで判別できます。この方法はdired.el内で広く使われています。
そしてファイル情報が連続している範囲を見つけて返すのが上のコードになります。
全行をスキャンするのが嫌ならばdired-subdir-alist
を利用することもできます。
(defun my-dired-next-dir-files-region () ;; ファイル名が無いところをスキップ (while (and (null (dired-move-to-filename)) (= (forward-line) 0))) (unless (eobp) (let ((beg (line-beginning-position))) ;; 次のディレクトリの先頭またはEOBへジャンプ (goto-char (dired-subdir-max)) ;; ファイルがある所まで戻る (while (and (= (forward-line -1) 0) (not (dired-move-to-filename)))) ;; その次の行の先頭 (forward-line) (cons beg (point)))))
dired-subdir-max
関数は現在のポイントが指し示すディレクトリのバッファ内での末尾位置を返します。dired-subdir-alist
には各ディレクトリの末尾の位置がマーカーとして保存されているので、全行をスキャンせずとも高速に割り出すことが出来ます。
ただ、その位置は正確に最後のファイルを指し示していないので、若干の後戻りスキャンが必要になります。
Diredバッファ内の全てのファイルをソートする
以上を組み合わせると次のようなコマンドを仕立てることが出来ます。
(defun my-dired-sort-by-file-score () (interactive nil dired-mode) (widen) (goto-char (point-min)) (let ((inhibit-read-only t)) (while-let ((files-region (my-dired-next-dir-files-region))) (my-dired-sort-region-by-file-score (car files-region) (cdr files-region)) (goto-char (cdr files-region)))))
どのタイミングでソートするか
手動でコマンドを実行したときだけソートされれば良いのであればこれで十分です。しかし何かのコマンドを実行したら常にソートされていて欲しい場合はどうしたら良いでしょうか。
当然Diredバッファが更新されるたびにどこかで自動的にソートする必要があります。
Diredのバッファ更新処理は大ざっぱに言って次のような流れを辿ります。
dired-noselect等 → dired-revert → dired-readin (あるいはdired-insert-old-subdirs) → dired-readin-insert → dired-insert-directory → insert-directory → file-name-handler(insert-directory) or ls-lisp--insert-directory or call-process(sh -c ls) → run-hook 'dired-after-readin-hook
最も無難なのがdired-after-readin-hookのタイミングです。このタイミングではバッファの更新が一通り終わって、上のコードで使用している全てのdired関数(dired-move-to-filename
やdired-subdir-max
、dired-get-filename
等)が正常に機能します。ただし、他の拡張もこのタイミングで様々な処理を行うことが多いため、競合には注意した方が良いでしょう。
他のタイミングとしては、一つのディレクトリを挿入するdired-insert-directory
やinsert-directory
の直後が考えられるでしょう。ただし、このタイミングでソートをするにはいくつか注意が必要です。
- 全てのdired関数が使用できない、あるいは正しく動作しないケースがある可能性がある。例えば
dired-insert-directory
の直後ではdired-subdir-alist
が構築されていないため、ディレクトリのファイルリストの範囲は全行スキャンしなければなりません。dired-move-to-filename
はおそらく大丈夫ですが、dired-get-filename
は問題がありそうです。 - ファイル名のエスケープ処理に注意。バッファに挿入されるファイル名にはある種のエスケープ処理が施されています。なので、バッファからファイル名を取得するときにはエスケープを解除する処理が必要になります。
dired-get-filename
はその辺りの処理をちゃんと行ってくれます。
ls-lisp
の中でソートするのは魅力的なアイデアでしょう。ls-lisp
はlsコマンドを使用せずEmacsの機能だけでlsをエミュレートするライブラリです。Windowsや最近ではAndroidでもデフォルトではls-lisp
が使われるようになっています。ls-lisp
は内部でdirectory-files-and-attributes
関数を呼び出してファイルリストを取得しています。この関数は結果をLispのリストで返すため、うまく介入すればテキストに挿入される前にリストのレベルでソート処理を行えます。具体的にはls-lisp-handle-switches
関数の直前はどうでしょうか。file-alistを独自にソートした上でswitchesにUオプションを追加してしまえばls-lisp
はそれ以上ソートを行いません。欠点はls-lisp
は使われない場合があるという点でしょう。UNIX環境でlsを使っている場合は当然として、trampなどでリモートディレクトリを参照している場合もls-lisp
が使われない可能性があります。
el-xmpでXMPプロパティに基づいてソート
というわけで、先日から作っているel-xmpでは、任意のXMPプロパティに基づいてdiredやimage-dired内でファイルやサムネイルをソートできるようになりました。
misohena/el-xmp: Emacs XMP (Extensible Metadata Platform) Library
xmp-setup.elを使用すれば ' S p
でプロパティ名を指定してソート、 ' S -
で解除できます。私は exif:DateTimeOriginal (撮影日)でソートしたいがためにこの機能を追加しました。
dired上でのソートは概ね上で説明したようなやり口で対処しています。
image-dired上では一つの画像がテキストプロパティが付いた1文字に対応するので、sort-subr
を使用して1文字単位でソートしています。
あ、そうそう、結局キャッシュのためにSQLiteでデータベースを作成しました。ついでにサイドカーファイル無しでもプロパティを保存できるようにもしました(設定次第)。ただし、サイドカーファイルが無い分データベースのメンテナンスのために様々なコマンドを駆使する必要が出てきてしまっています。プロパティを付けた後にその対象ファイルを移動したり削除したりした場合、データベース内に記録されたプロパティは対象ファイルが存在しないいわば「はぐれ」状態になってしまいます。それを修正するためのコマンドを色々追加してあります。サイドカーファイルでもはぐれファイルになることはありますが比較的気がつきやすいですし対処もしやすいです。