この記事は、Emacs Advent Calendar 2021の12日目の記事です。
Emacsからほかのアプリを開く試みは古今東西さまざま行われてきたようです。
しかし、最近のEmacsの初期設定ではほかのアプリを開くのはできないらしい、、、ということでやり方をちょっと調べてみました。
シェルからアプリを開く
Emacsに限らず、ターミナルやコマンドプロンプト、およびシェルから指定されたファイルをアプリを開くにはどうしたらよいでしょうか?
最近のMacOSやWindows、Linuxなどでは、ターミナルおよびシェルから指定されたファイルをアプリを開くコマンドが用意されています。
ファイルを開くときのアプリは、OS側でファイルの拡張子に応じて設定していることが前提です。
MacOSの場合
MacOSでは、openコマンドを使ってターミナルおよびシェルから指定されたファイルをアプリを開けます。
たとえばカレントディレクトリーにあるsample.pdfファイルは、ターミナルから次のコマンドで起動できます。覚えておくと便利。
$ open sample.pdf
もちろんカレントディレクトリー以外にあるファイルも、開けます。絶対参照と相対参照、どちらでも指定可能です。
$ open ../sample.pdf
$ open /Users/foo/sample.pdf
ただし、ちょっと困るのがスペース(空白文字)などの特殊文字を含むファイル名の場合。
たとえばカレントディレクトリーにあるsample 1.pdfファイルをターミナルから次のコマンドで起動しようとするとエラーになります。
$ open sample 1.pdf
The files /Users/foo/Documents/- and /Users/foo/Documents/株式会社ほげほげ/sample.pdf do not exist.
最近ではMicrosoftのOneDriveを使うと、「OneDrive - 株式会社ほげほげ」といったスペースを含む名前のディレクトリーが作られるので、こうしたファイル名を使わざるをえません。このままだと、openコマンドでこうしたファイルは開けません。
open ../OneDrive - 株式会社ほげほげ/sample.pdf
The files /Users/foo/Documents/- and /Users/foo/Documents/株式会社ほげほげ/sample.pdf do not exist.
こういう場合は、空白などの特殊文字の前に \ をつけてエスケープするか、ファイル名を""(ダブルクォテーション)で囲うとうまく開けます。
$ open sample\ 1.pdf
$ open ../OneDrive\ -\ 株式会社ほげほげ/sample.pdf
Windowsの場合
Windowsでは、startコマンドを使ってコマンドプロンプトからファイルをアプリで開けます。
たとえばカレントディレクトリーにあるsample.pdfファイルは、コマンドプロンプトから次のコマンドで起動できます。
> start sample.pdf
ところが、スペース(空白文字)などの特殊文字を含むファイル名の場合、次のコマンドを実行すると、うまくいきません。
> start "sample 1.pdf"
startコマンドでは最初の引数として指定された""で囲まれた文字列は、ファイル名ではなく、「ウィンドウのタイトルバーに表示されるタイトル」と解釈されるのです。そのため start "sample.pdf" では、sample.pdfというタイトルのコマンドプロンプトが新たに起動し、sample.pdfは開けないのです。うーん、わかりにくい。
""で囲まれたファイル名のファイルを開きたいときは、次のようなコマンドを使います。
> start "" "sample.pdf"
> start "" "sample 1.pdf"
> start "" "..\OneDrive - 株式会社ほげほげ\sample.pdf"
つまり、Windowsでは次のコマンドで名前が[ファイル名]のファイルを開けます。
> start "" "[ファイル名]"
Linuxなどの場合
Linuxでは、xdg-openコマンドを使ってターミナルおよびシェルから指定されたファイルをアプリを開けます。FreeBSDでもxdg-openコマンドは使えるようです。GNU Hurdでも使えそうですが、詳細は不明です。
Linuxなどの場合、MacOSやWindowsとは異なり、さまざまなデスクトップ環境を選択できます。基本的には、デスクトップ環境ごとにファイルをアプリで開くコマンドも変わります。しかし、xdg-openコマンドはそうしたデスクトップ環境の違いを吸収します。
また、MacOSのopenコマンドやWindowsのstartコマンドは常に使えるのに対し、Linuxなどではxdg-openコマンドを含むxdg-utilsがインストールされていることが前提条件になります。
たとえば、ターミナルから次のコマンドでPDFファイルを開けます。
$ xdg-open sample.pdf
$ xdg-open ../sample.pdf
$ xdg-open /Users/foo/sample.pdf
MacOSのopenコマンドと同様、空白などの特殊文字があるときはその前に \ をつけてエスケープするか、ファイル名を""(ダブルクォテーション)で囲うとうまく開けます。
$ xdg-open sample \1.pdf
$ xdg-open ..\OneDrive\ -\ 株式会社ほげほげ\sample.pdf
Emacsでの実装
start-process-shell-command関数
Emacsでは、外部コマンドを呼び出す関数が様々用意されています。詳細は、Emacs Lisp Manualの38 Processesを参照するといいでしょう。ここでは、指定したシェルコマンドをサブプロセスを作成して非同期に実行させるstart-process-shell-command関数を使います。
EmacsからOSごとに異なるコマンドを呼び出す
EmacsでOSごとに異なるコマンドを呼び出すときは、system-type関数を使えます。詳しくは、init.elの設定をコンピューターごとに分岐させるを参照してください。
OSコマンドインジェクション対策を施す
Emacsからシェルコマンドを実行するときには、OSコマンド・インジェクション対策が必要です。一般的な対策については、IPA 情報セキュリティにある安全なウェブサイトの作り方 - 1.2 OSコマンド・インジェクションが参考になります。Emacsでの対策についてはEmacsからの安全なシェルコマンド実行が参考になります。
今回は、次の対策を施すことにしました。
- ファイルをアプリで開くOSごとのコマンドとファイル名以外の引数を関数内に記述する
- ファイル名は実在するかどうかチェックする。実在しない場合はエラー終了
- ファイル名の中に特別な文字が含まれる文字はエスケープする
特別な文字が含まれる文字をエスケープするために、file-quote-argument関数を使います。file-quote-argument関数により、Windows(とMS-DOS)の場合は文字列が""で囲まれ、MacOSやLinuxなどそのほかのOSの場合は特別な文字がエスケープされます。
process-connection-type変数の設定
Linuxなどのxdg-openコマンドは、process-connection-type変数をnilにしないと動作しません。
例えば、次の関数をEmacs上で実行させてもsample.pdfファイルは開けません。
(start-process-shell-command "emacs-exopen" nil "xdg-open sample.pdf"))
次のように一時的に変数process-connection-typeをnilにすることで、sample.pdfファイルをxdg-openコマンドで開けます。
(let ((process-connection-type nil))
(start-process-shell-command "emacs-exopen" nil "xdg-open sample.pdf"))
情報源は Emacs Stack Exchange の Why does xdg-open not work in eshell? というやりとり。Qiitaのこの記事やこの記事を見ても、なぜこうなるかは理解できませんでしたが。
なお、MacOSのopenやWindowsのstartでは、process-connection-typeの値に関わらずEmacsからファイルを開けます。
exopen-file関数
以上の点を考慮して作成したのが次のexopen-file関数です。実行時、日時と実行するコマンド文字列をメッセージとして(Messageバッファに)出力するようにしました。
;;; ファイルを外部プログラムでオープン
(defun exopen-file (file)
"Open a file with external program."
;;; 指定したファイルが実在しない場合はエラー終了
(unless (file-exists-p file)
(error "%s: File does not exist." file))
(let (
;; システム別の外部プログラムオープンコマンド
(opencmd-alist
'(
(gnu/linux . ("xdg-open"))
(gnu/kfreebsd . ("xdg-open"))
(darwin . ("open"))
(windows-nt . ("start" "\"\""))))
;; xdg-open用の設定
;; 参照: https://emacs.stackexchange.com/questions/19344/why-does-xdg-open-not-work-in-eshell
(process-connection-type nil)
opencmd cmdstr)
(unless (car (setq opencmd (cdr (assoc system-type opencmd-alist))))
(error "There is no external open command on this `%s' system." system-type))
(setq opencmd (append opencmd (list (shell-quote-argument file))))
(setq cmdstr (mapconcat 'identity opencmd " "))
(message "exopen at %s: %s" (format-time-string "%Y/%m/%d %H:%M:%S") cmdstr)
(start-process-shell-command "emacs-exopen" nil cmdstr)))
exopen-mode.el
ファイルを外部プログラムで開くexopen-file関数があれば、次のようなプログラムは比較的かんたんに作れます。
- バッファで開いているファイルを外部プログラムで開く
- 指定したファイルと同名で拡張子が異なるファイルを外部プログラムで開く。たとえば
aaa.texファイルを編集しているときに、aaa.pdfファイルをプレビューやAdobe Readerで開く - 現在のディレクトリー(フォルダー)をFinderやエクスプローラーで開く
- diredからファイルを外部プログラムで開く
自作の exopen-mode.el は、exopen-file関数に加えてこうした関数たちを定義しています。
こうして定義した関数に、init.elファイルなどでキーを割り付けるとなかなか便利です。たとえば私は、diredではrキーでファイルを外部プログラムで開けるようにしています。
まとめ
MacOS、Windows、Linuxなどでは、MacOSのopenのようなコマンドが用意されていて、こうしたコマンドをEmacsから呼び出す関数を作ると重宝します。ただし、OSインジェクションなどのセキュリティには気をつけるようにしましょう。
それでは、良いクリスマスを!