eshell の強化

# 背景

以前Emacs でなんでもやりたいと思ってから、zsh メインで使っていたのを eshell メインにした。eshell はデフォルトだと、補完などzsh に比べて劣るところ が多かったため、使えるレベルにするためにいくつか改良を加えた。

# 環境

-   Linux 4.19.2 (arch Linux)
-   GNU Emacs 26.1

# current buffer でeshell を起動

## eshell-on-current-dir

現在のバッファのディレクトリでeshell を起動する関数を作成した。 [Eshell - Caiorss.GitHub.io by caiorss](https://caiorss.github.io/Emacs-Elisp-Programming/Eshell.html) を参照にした。

```emacs-lisp
(defun eshell-on-current-dir (&optional arg)
  "invoke eshell and cd to current directory"
  (interactive "P")
  (let ((dir default-directory))
    (eshell arg)
    (cd dir))
  (eshell-emit-prompt)
  (goto-char (point-max)))
```

## popwin でopen

上記関数を実行する度に、eshell が1バッファ埋めてしまうのは不便のため、 eshell バッファはpopwin で開くことにした。設定は下記。

```emacs-lisp
(add-to-list 'popwin:special-display-config
             '("\\`\\*eshell" :regexp t :dedicated t :position bottom
               :height 0.3))
```

# 補完強化

デフォルトでは、パスの通っているコマンド、カレントディレクトリ内の ファイル、ディレクトリしか補完できない。そのため、サブコマンドが必要なコマンド、 それ以外の補完候補を取るコマンドの補完をpcomplete で強化した。 下記3つのコマンドの補完関数を作成した。

-   `sudo`
-   `systemctl`
-   `man`

また、pcomplete の使い方に関しては、

-   [pcomplete (programmable completion) の書き方](http://d.hatena.ne.jp/a666666/20091204/1259857382)
-   [PComplete: Context-Sensitive Completion in Emacs - Mastering Emacs](https://www.masteringemacs.org/article/pcomplete-context-sensitive-completion-emacs)

を参照した。

```emacs-lisp
;;;; sudo completion
(defun pcomplete/sudo ()
  "Completion rules for the `sudo' command."
  (let ((pcomplete-ignore-case t))
    (pcomplete-here (funcall pcomplete-command-completion-function))
    (while (pcomplete-here (pcomplete-entries)))))

;;;; systemctl completion
(defcustom pcomplete-systemctl-commands
  '("disable" "enable" "status" "start" "restart" "stop" "reenable"
    "list-units" "list-unit-files")
  "p-completion candidates for `systemctl' main commands"
  :type '(repeat (string :tag "systemctl command"))
  :group 'pcomplete)

(defvar pcomplete-systemd-units
  (split-string
   (shell-command-to-string
    "(systemctl list-units --all --full --no-legend;systemctl list-unit-files --full --no-legend)|while read -r a b; do echo \" $a\";done;"))
  "p-completion candidates for all `systemd' units")

(defvar pcomplete-systemd-user-units
  (split-string
   (shell-command-to-string
    "(systemctl list-units --user --all --full --no-legend;systemctl list-unit-files --user --full --no-legend)|while read -r a b;do echo \" $a\";done;"))
  "p-completion candidates for all `systemd' user units")

(defun pcomplete/systemctl ()
  "Completion rules for the `systemctl' command."
  (pcomplete-here (append pcomplete-systemctl-commands '("--user")))
  (cond ((pcomplete-test "--user")
         (pcomplete-here pcomplete-systemctl-commands)
         (pcomplete-here pcomplete-systemd-user-units))
        (t (pcomplete-here pcomplete-systemd-units))))

;;;; man completion
(defvar pcomplete-man-user-commands
  (split-string
   (shell-command-to-string
    "apropos -s 1 .|while read -r a b; do echo \" $a\";done;"))
  "p-completion candidates for `man' command")

(defun pcomplete/man ()
  "Completion rules for the `man' command."
  (pcomplete-here pcomplete-man-user-commands))
```

# bookmarks でお気に入りのディレクトリ登録

zsh を使っていたときは、お気に入りのディレクトリをブックマークして すぐに移動できるプラグインを使用していた。 私はeshell でも同じことがしたく、ディレクトリにも使用できるということで、 emacs 標準のbookmarks 機能を利用することにした。そうしてeshell 上で動く `bmk` というコマンドを作った。機能としては

-   登録済みのブックマークにジャンプ
-   カレントディレクトリのブックマーク登録
-   特定のブックマークの削除
-   ブックマーク一覧の表示

である。使い方は `bmk -h` を入力して表示されるヘルプメッセージを参照してほしい。

```emacs-lisp
;;; eshell bookmark command
(defun pcomplete/eshell-mode/bmk ()
  "Completion for `bmk'"
  (pcomplete-opt "adl")
  (pcomplete-here (bookmark-all-names)))

(defun show-bmk-list ()
  "Show the bookmark list and each corresponding path"
  (let (output-string bmk-name bmk-path (each-format "%-15s|%-5s|%s
"))
    (setq output-string (concat (format each-format "Bookmarks" "Type"
                                        "Path") (format "%s
" (make-string 80 ?-))))
    (dolist (tmp bookmark-alist)
      (setq bmk-name (car tmp))
      (setq bmk-path (abbreviate-file-name (assoc-default 'filename tmp)))
      (if (file-directory-p bmk-path)
          (setq output-string
                (concat output-string (format each-format bmk-name "D" bmk-path)))
        (setq output-string (concat output-string (format each-format bmk-name "F" bmk-path)))))
    output-string))

(defun add-bmk (&optional args)
  "add the current path to the bookmark as the name of the first argument"
  (let ((name (or (car args)
                  (file-name-nondirectory
                   (directory-file-name default-directory)))))
    (if name
        (progn
          (bookmark-set name)
          (bookmark-set-filename name (eshell/pwd))
          (bookmark-save)
          (format "Saved current directory in bookmark %s" name))
      (error "You must enter a bookmark name"))))

(defun delete-bmk (args)
  "delete bookmark arguments."
  (let (output-string)
    (if args
        (progn (dolist (tmp args)
                 (bookmark-delete tmp)
                 (bookmark-save)
                 (setq output-string
                       (concat output-string (format "Deleted the bookmark %s
" tmp))))
               output-string)
      (error "You must enter a bookmark name"))))

(defun cd-bmk (args)
  "cd/open the first bookmark argument"
  (let ((bookmark (car args)) filename)
    (if (setq filename (bookmark-get-filename bookmark))
        ;; If it points to a directory, change to it.
        (if (file-directory-p filename)
            (eshell/cd filename)
          ;; otherwise, just jump to the bookmark
          (bookmark-jump bookmark))
      (error "%s is not a bookmark" bookmark))))

;; to add bookmark on eshell
(defun bookmark-alternate-buffer-file-name ()
  "Return the current buffer's file in a way useful for bookmarks."
  ;; Abbreviate the path, both so it's shorter and so it's more
  ;; portable.  E.g., the user's home dir might be a different
  ;; path on different machines, but "~/" will still reach it.
  (abbreviate-file-name
   (cond
    (buffer-file-name buffer-file-name)
    ((and (boundp 'dired-directory) dired-directory)
     (if (stringp dired-directory)
         dired-directory
       (car dired-directory)))
    (default-directory)
    (t (error "Buffer not visiting a file or directory")))))

(advice-add 'bookmark-buffer-file-name :override
            'bookmark-alternate-buffer-file-name)


(defun eshell/bmk (&rest args)
  "Eshell bookmark command to show/add/delete bookmarks.
For usage, execute without arguments."
  (eshell-eval-using-options
   "bmk" args
   '((?a "add" t add
         "add current directory as the directory name or BOOKMARK")
     (?d "delete" t del
         "delete BOOKMARK entry")
     (nil "list" nil show
          "show the bookmark list")
     (?h "help" nil nil "show this usage screen")
     ;; :show-usage
     :usage "BOOKMARK
   or: bmk -a [BOOKMARK]
   or: bmk -d BOOKMARK ...
   or: bmk [OPTION]
Change directory pointed to by BOOKMARK
or bookmark-jump to the BOOKMARK if it is not a directory.
With an option, this can create a bookmark, delete bookmarks
or show the bookmark list.
Without any BOOKMARK and option, this shows the bookmark list.")
   (bookmark-maybe-load-default-file)
   (setq args (eshell-flatten-list args))
   (cond
    (show
     (show-bmk-list))
    (add
     (add-bmk args))
    (del
     (delete-bmk args))
    (args
     (cd-bmk args))
    (t (show-bmk-list)))))
```

# まとめ

eshell の強化方法について載せた。補完に関してはpcomplete で強化はしているが マイナーコマンドの補完ができないといった不便なところもまだまだある。 使ったことはないが、fish の補完を実装できるという、 [GitHub - Ambrevar/emacs-fish-completion](https://github.com/Ambrevar/emacs-fish-completion) をいずれ使ってみたい。 また最近は、dired でディレクトリ移動を行っているが、階層の遠いディレクトリ はbookmarks 機能のジャンプを使っている。ブックマークの登録は、そのディレクトリに 行ってから `eshell-on-current-dir` でeshell 上で `bmk` を使うと便利だ。