この記事は、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インジェクションなどのセキュリティには気をつけるようにしましょう。
それでは、良いクリスマスを!