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


【お知らせ】


<2018/10/14 追記>
X11 forwarding を設定した接続先に ssh で接続した際、「No xauth data; using fake authentication data for X11 forwarding」と表示される場合は、接続元の環境で次のコマンドを実行してみてください。
xauth generate :0 .
これが原因で comint-read-input-ring 関数が上手く動作しない場合があるようです。設定を行った後は、M-x tramp-cleanup-all-connection で tramp のコネクションをクリアし、再度本設定の操作を実行してみてください。

<2018/09/23 追記>
PROMPT_COMMAND 変数での コマンドの .bash_history への逐次書き込みでは、コマンドの重複削除は機能しないようです。このため、bash 終了時に .bash_history の重複コマンドを削除する機能を追加しました。

<2018/08/21 追記>
helm の設定については、次のページを参照してください。

<2018/07/26 追記>
comint-run は、make-comint を使うと専用コマンドを作ることもできます。本設定と組み合わせてお使いください。
;; sample
(switch-to-buffer (make-comint "bash" "bash")) ; like shell-mode
(switch-to-buffer (make-comint "python" "python3")) ; like run-python

<2018/06/21 追記>
tramp の接続時に接続先の .bash_history が正しく読み込めない状況が発生した場合は、Emacs を終了し、~/.emacs.d/tramp ファイルにエラーメッセージが書き込まれていないか確認してください。エラーメッセージが記録されている場合は、その内容から対処方法を調査して対策し、~/.emacs.d/tramp を消してから Emacs を起動して、動作を確認してください。
次を評価したとき、t が帰ってくれば、正常な状態であると思います。
(file-readable-p (format "/%s:<username>@<hostname>:~/.bash_history" tramp-default-method)) 
※ <user> と <host> は tramp の接続先の値に置き換えてください。

<2018/05/07 追記>
tramp の挙動がおかしい時は、Emacs を終了後、~/.emacs.d/tramp というファイルを削除してから Emacs を再起動してください。

<2015/06/30 追記>
gnupack-12.00 以降に添付される .bashrc には、以下で設定を行っている HISTCONTROL や PROMPT_COMMAND の設定が含まれています。gnupack-12.00 以降を利用する場合には、.bashrc の中のこの設定を無効としてご利用ください。

【本題】



helm から comint の入力履歴を検索するための設定です。

comint で管理されている入力履歴 comint-input-ring を helm で表示し、選択実行することができます。
comint の入力履歴を検索しているので、comint-mode を使っているコマンド(comint-run、shell、run-python、run-ruby、sql-* など)で利用することができます。
shell-mode で利用する場合は、接続する shell(但し、bash 限定)のヒストリファイルと直接連携し、次の点を改善するように調整しています。
  • helm-command-prefix-key + C-p の入力で for文などの複数行に渡るコマンドを一行のコマンドとして検索できる
  • 同じマシン上で複数のshellバッファを起動した場合でもヒストリを共有できる (zsh の share_history みたいな感じ?)

参考としたページは次のとおりとなります。

(require 'shell)
(require 'tramp)
(require 'tramp-sh)

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

;; 情報源を設定する
(setq helm-source-comint-history
      (helm-build-sync-source "Comint History"
        :candidates (lambda ()
                      (let ((ring (buffer-local-value 'comint-input-ring helm-current-buffer)))
                        (when (ring-p ring)
                          (ring-elements ring))))
        :candidate-number-limit 1000 ; helm-candidate-number-limit の値を置き換える
        :multiline t
        :action (helm-make-actions "Insert"  (lambda (candidate)
                                               (comint-goto-process-mark)
                                               (comint-delete-input)
                                               (insert (mapconcat 'identity (helm-marked-candidates) "\n")))
                                   "Execute" (lambda (candidate)
                                               (comint-goto-process-mark)
                                               (comint-delete-input)
                                               (insert (mapconcat 'identity (helm-marked-candidates) "\n"))
                                               (comint-send-input)))
        :migemo t))

;; helm コマンドを作成する
(defun helm-comint-history ()
  (interactive)
  (helm :sources 'helm-source-comint-history
        :prompt "[MIGEMO] pattern: "
        :input (thing-at-point 'line)
        :buffer "*helm comint history*"))

;; comint-input-ring のサイズを指定する
(setq comint-input-ring-size 10000)

;; 連続する同一の入力を履歴に格納しない
(setq-default comint-input-ignoredups t)

;; キーバインドを設定する
;; コマンドの一部を入力してからキーを打つと、コマンド履歴からのマッチング検索を行う
;; helm-comint-history で空白文字を検索対象としたい場合は、空白文字をエスケープするか2文字入力する
(define-key comint-mode-map (kbd "M-p") 'comint-previous-matching-input-from-input)
(define-key comint-mode-map (kbd "M-n") 'comint-next-matching-input-from-input)
;; (define-key comint-mode-map (kbd "M-r") 'helm-comint-history)
(define-key comint-mode-map (kbd (concat helm-command-prefix-key " C-p")) 'helm-comint-history)

;; ヒストリファイルを変更しない
(setq tramp-histfile-override nil)

(defun shell-mode-setup ()
  ;; ヒストリファイル名を設定する
  (setq comint-input-ring-file-name (concat (file-remote-p default-directory) "~/.bash_history"))

  ;; 設定コマンドを発行する
  (process-send-string
   nil
   (concat "\\echo;"
           "\\shopt -u histappend;" ; セッションクローズ時にヒストリファイルへの追記をしない
           "TRAP=$(\\trap -p 0 | \\sed -r -e \"s/.*?'(.*)'.*/\\\\1/\" -e 's/([^; ] *$)/\\1;/');"
           "\\trap " ; bash 終了時に .bash_history の重複行を削除する(以降8行目まで)
           "    \"$TRAP\"'"
           "    TMP=$(\\mktemp);"
           "    \\tac ~/.bash_history | \\awk \"!a[\\$0]++\" | \\tac > $TMP;"
           "    if [ -s $TMP ]; then"
           "        \\cp $TMP ~/.bash_history;"
           "    fi;"
           "    \\rm $TMP' 0;"
           "\\unset TRAP;"
           "if [ -f ~/.bash_history ]; then"
           "    (TMP=$(\\mktemp);" ; セッション確立時に追加される不要なコマンド行を削除する(以降6行目まで)
           "     \\sed -e '/^exec env /d' -e '/^exec ssh /d' -e '/&& exit || exit$/d' ~/.bash_history > $TMP;"
           "     if [ -s $TMP ]; then"
           "         \\cp $TMP ~/.bash_history;"
           "     fi;"
           "     \\rm $TMP);"
           "fi;"
           "if [ ! -s ~/.bash_history ]; then"
           "    \\echo history > ~/.bash_history;" ; history コマンドが正常に機能するように1行追加する
           "fi;"
           "PROMPT_COMMAND=\"\\history -a; \\history -c; \\history -r; $PROMPT_COMMAND\";"
           "\\history -c; \\history -r;" ; ヒストリを初期化する
           "\n"))

  ;; ヒストリファイルを読み込む
  (comint-read-input-ring t)
  (message ""))

;; shell-mode-hook を設定する
(add-hook 'shell-mode-hook 'shell-mode-setup)

;; キーバインドを設定する
;; (define-key shell-mode-map (kbd "M-r")
(define-key shell-mode-map (kbd (concat helm-command-prefix-key " C-p"))
  (lambda ()
    (interactive)
    ;; ヒストリファイルを再読み込みする
    (comint-read-input-ring t)
    (helm-comint-history)))

;; bash の環境変数を設定する
;; ・LC_ALL と LC_CTYPE の設定は、リモートサーバで shell を開いた時に日本語が文字化けしないようにするため
;; ・HISTFILE の設定は、tramp での接続時に .bash_history に不要な書き込みをしないようにするため
;; ・HISTIGNORE の設定はなくても良いが、一応設定してみた(.bashrc で設定してあるかも)

;; for remote
(let ((process-environment tramp-remote-process-environment))
  (setenv "LC_ALL" nil)
  (setenv "LC_CTYPE" nil)
  (setenv "HISTFILE" nil)
  (setenv "HISTIGNORE" "rm*:mv*:kill*")
  (setq tramp-remote-process-environment process-environment))

;; for local
(setenv "HISTIGNORE" "rm*:mv*:kill*")

sql-mode を使う場合は、次の設定もしておくと良いようです。
;; sql-mode が他の comint を使うコマンドに悪さをしないようにするおまじない
(advice-add 'sql-interactive-mode
            :around (lambda (orig-fun &rest args)
                      (let (comint-input-ring-separator)
                        (apply orig-fun args))))


<変更履歴>
  • 2013/09/17 このページを作成した。
  • 2013/10/15 全面的に内容の見直しを行った。
  • 2013/10/16 表示するヒストリサイズが小さくなってしまっていたのを修正した。
  • 2013/11/25 .bash_historyファイル が存在しなかったり 0サイズ だった場合に正常に機能しなかったのを対策した。
  • 2014/09/04 helm 情報源のデフォルトアクションを Execute から Insert に変更した。
  • 2014/11/03 NTEmacs64 で使った際、リモートで shell を起動する際にエラーが出るので対策した。
  • 2015/09/10 advice を Emacs-24.4 以降の書式に見直した。
  • 2015/09/18 コマンドヒストリーの検索キーを M-r から C-; C-p に変更した。この対応により、M-r に本来割り当てられていたコマンドを利用可能。
  • 2015/10/30 helm-migemo-mode に対応した helm source の書き方に変更した。
  • 2018/05/08 shell-mode-hook で実行している process-send-string の送信コマンドに "shopt -u histappend;" を追加した。
  • 2018/06/21 Emacs-26系で動作しなかったので対応した。
  • 2018/07/25 comint-run など comint-mode を利用するコマンド全てで動作するように、設定を汎用化した。
  • 2018/09/18 候補の複数選択に対応した。
  • 2018/09/23 bash 終了時に .bash_history の重複コマンドを削除する機能を追加した。
  • 2018/09/23 helm の情報源の :multiline を OFF とした。
  • 2018/10/08 comint-input-ring-file-name の指定方法を変更した。
  • 2018/10/17 signal 0 の時の trap 処理を追加する仕様に見直した。
  • 2019/04/17 キーバインドの設定方法、comint-input-ignoredups バッファローカル変数の設定方法を変更した。