■ Emacs 全般で利用できる設定


【お知らせ】


<2019/04/19 追記>
helm を利用している場合、次の設定も行うと shell-mode をさらに便利に利用できます。

<2015/10/12 追記>
Emacs-25系で、shell コマンドの挙動が変わった(other-window に shellバッファが表示されるようになった)ので、その対策を行いました。

【本題】


dired と連携して shell を利用している場合、tramp にによる ssh や docker の接続なども利用して複数の dired バッファを開き始めると、その dired バッファに対応する shell を選択したり cd したりするのが面倒になり、ついつい新規で shell を作成(C-u M-x shell)してしまいます。

以下は、開いているバッファの default-directory を確認し、同じロケーションの shellバッファ を探して表示した後、同じディレクトリに cd してしてくれる設定(shell-mode 用)です。

動作仕様は次のとおりです。
  • shellバッファ は other-window に表示します。
  • 該当の shellバッファ がない場合は、新たに shellバッファ を作成し、表示します。
  • 該当の shellバッファ がある場合は、そのバッファを表示し、必要であればコマンドを実行したバッファと同じディレクトリになるように cdコマンド を発行します。但し、フォアグラウンドプロセスが走っている shellバッファは、ポップアップの対象外とします。
  • 同じロケーションの shellバッファ が複数ある場合には、一番最近に表示した shellバッファ を選択します。
  • shellバッファ 上でコマンドを発行した場合でかつ複数ウィンドウが表示されている場合には、その shellバッファ を表示しているウィンドウを削除します。
  • universal-argument(C-u)付でコマンドを発行した場合には、常に新しい shellバッファ を作成し、表示します。
  • 以上の機能は、tramp による ssh や docker の接続先でも動作します。
(require 'cl-lib)
(require 'shell)
(require 'tramp)

(setq shell-file-name "/bin/bash") ; Mingw版 Emacs から Cygwin の bash を使う場合は、"bash" とすること
(setq shell-command-switch "-c")
(setq explicit-shell-file-name shell-file-name)

;; other-window がなければ開き、その window に移動する
;; http://d.hatena.ne.jp/rubikitch/20100210/emacs
(defun other-window-or-split ()
  (interactive)
  (when (one-window-p)
    (split-window-sensibly))
  (other-window 1))

;; ロケーションが同じ shellバッファ を other-window に表示する
(defun shell-popup ()
  (interactive)
  (cl-labels ((shell-quote-argument-2 (arg)
                                      (replace-regexp-in-string
                                       "[\x00-\x09\x0b-\x2c\x3b-\x40\x5b-\x5e\x60\x7b-\x7d\x7f]"
                                       "\\\\\\&"
                                       arg))
              (shell-other-window (buffer-name)
                                  (let ((current-directory default-directory))
                                    (cond ((>= emacs-version-number 25)
                                           (switch-to-buffer-other-window buffer-name)
                                           (shell buffer-name))
                                          (t
                                           (other-window-or-split)
                                           (let ((default-directory current-directory))
                                             (switch-to-buffer buffer-name)
                                             (shell buffer-name))))
                                    (comint-goto-process-mark)
                                    (unless (string= (expand-file-name default-directory)
                                                     (expand-file-name current-directory))
                                      (comint-delete-input)
                                      (insert (concat "cd " (shell-quote-argument-2 (file-local-name current-directory))))
                                      (comint-send-input)
                                      (sit-for 0))))
              ;; process-mark のある行に comint-prompt-regexp にマッチする文字列があるかだけの簡易な判定をしている
              (shell-prompt-p (buffer-name)
                              (save-excursion
                                (set-buffer buffer-name)
                                (comint-goto-process-mark)
                                (forward-line 0)
                                (looking-at comint-prompt-regexp))))
    (if current-prefix-arg
        (shell-other-window (generate-new-buffer-name "*shell*"))
      (if (and (string-match "^\\*shell\\*" (buffer-name))
               (not (one-window-p)))
          (delete-window)
        (cl-loop for buffer in (buffer-list)
                 if (and (string-match "^\\*shell\\*" (buffer-name buffer))
                         (let ((current-directory (buffer-local-value 'default-directory buffer)))
                           (or (and (not (tramp-tramp-file-p default-directory))
                                    (not (tramp-tramp-file-p current-directory)))
                               (tramp-equal-remote default-directory current-directory)))
                         (or (not (get-buffer-process buffer))
                             (shell-prompt-p (buffer-name buffer))))
                 return (shell-other-window (buffer-name buffer))
                 finally return (shell-other-window (generate-new-buffer-name "*shell*")))))))

;; キーバインドを設定する
(global-set-key (kbd "C-c s") 'shell-popup)

;; 次は、必要であれば設定してください
(global-set-key (kbd "C-x o") 'other-window-or-split)

MinGW版 Emacs を利用している場合は、次の設定も検討ください。
tramp で開いた shellバッファ 内で cd した際、そのバッファの default-directory 変数が適切に再設定されない不具合?を改善するための設定です。
ただし、他への影響があるかもしれませんので、その点をご理解のうえ利用してください。
設定しない場合でも、cd の動作が不安定にはなりますが、shellバッファ を表示する機能に影響はありません。
(advice-add 'comint-substitute-in-file-name
            :around (lambda (orig-fun &rest args)
                      (if (file-remote-p default-directory)
                          (cl-letf (((symbol-function 'substitute-in-file-name)
                                     (symbol-function 'identity)))
                            (apply orig-fun args))
                        (apply orig-fun args))))


<変更履歴>
  • 2013/09/17 このページを作成した。
  • 2013/09/24 cd する際に、もし shell バッファに入力途中の文字列があれば、削除するように対策した。
  • 2013/09/24 ローカル変数 default-directory の参照方法を変更した。
  • 2013/10/08 Emacs-24系でも動作するように見直しを行った。
  • 2013/10/13 shellバッファのカーソル位置がバッファの中間位置に移動してしまうことを対策した。
  • 2013/10/21 フォアグラウンドプロセスが走っている shellバッファは、ポップアップしないようにした。
  • 2013/11/13 cd するときにパスをシングルコーテーションで括っていたが、Linuxで動作しなかった(~ が展開されない)ので削除した。
  • 2013/11/14 cd コマンドを発行するかどうかの判定が Linux で正しく行われいなかったのを対策した。
  • 2014/11/05 cd する際のディレクトリに shell-quote-argument の処理を通すようにした。
  • 2015/03/05 flet系 の関数の利用を labels系 の関数の利用に変更しました。
  • 2015/03/05 cd のパラメータのクオート関数をローカルに定義して利用するようにした。
  • 2015/09/13 advice を Emacs-24.4 以降の書式に見直した。
  • 2015/10/12 Emacs-25系で、shell コマンドの挙動が変わったので、その対策を行った。
  • 2018/03/27 shell-file-name 変数等の設定を追加した。
  • 2018/06/19 Emacs-26系で動作しなかったので対応した。
  • 2018/10/09 shell-popup 関数の設定内容を見直した。機能に変更は無し。