コマンドの非同期呼び出しshell-command->string/async

python-ex.elを作成中。

自分の好みに合ったpythonの環境を提供するためのelisp。まだまだ開発途中。
その中で、非同期に外部プロセスを呼び出す方法が便利そうなので紹介します。

同期的な呼び出し

コマンドの出力結果を文字列として欲しい場合には、よくshell-command-to-stringなどを使いますね。以下のようなかんじです。末尾に改行が入ってしまうので取り除きます。

(defun shell-command->string (cmd)
  (let ((r (shell-command-to-string cmd)))
    (substring-no-properties r 0 (1- (length r)))))

(shell-command->string "seq 5 | tr '\n' ' '")

問題は、時間がかかる処理になると他の動作ができないことです。

(shell-command->string "sleep 10 && seq 5 | tr '\n' ' '") 
;; 終わんない><

こんなとき、非同期でコマンドを呼び出すことができると便利です。

非同期について

同期的な処理の場合はコマンドの結果を直接もらうことができますが、非同期処理の場合には結果を直接もらうことができません。

(sync-action args ...) ;; => <anything-value>

(async-action args ...) ;; => すぐに何か返ってくる。でも実行結果は (>_<)?

そんな実行結果をもらえない非同期の関数ではどうするかというと、結果を受け取る関数を渡すようにします。
そんな関数をcall-backとかいったりします。

(async-action args ... <call-back-function>) ;; async-action ---pass--> <call-back-function> 

コマンドの実行を非同期で

その考え方でshell-command->stringの非同期版を作ってみます。elispはdynamic-scopeなので少し分かりにくいところがあるかもしれませんが、大体上の非同期の説明と同様のことをしています。

(require 'cl)

(defun shell-command->string/async (cmd call-back)
  (lexical-let ((buf "*output buffer*")
                (call-back call-back))
    (with-current-buffer (get-buffer-create buf)
      (erase-buffer))
    (set-process-sentinel
     (start-process-shell-command "SH" buf cmd)
     (lambda (status process)
       ;; 出力結果はbufferに入っている。
       (let ((r (with-current-buffer buf
                  (buffer-string))))
         (funcall call-back r))))))

作成したshell-command->string/asyncは、コマンドの文字列とその結果を利用する関数を引数にとります。今回の関数は非同期で動くのでコマンドの実行中もカーソル移動などができます。便利ですね!!

;;; 10秒後にカーソル位置に1 2 3 4 5と挿入
(shell-command->string/async
 "sleep 10 && seq 5 | tr '\n' ' '"
 'insert)
;; でも、そのあいだ動ける!!!

まだ実は問題があります。それは、複数のコマンドを非同期で実行できないことです。

バッファを被らない名前にする。

gensymを使って被らないようにしましょう。

(require 'cl)

(defun shell-command->string/async (cmd call-back)
  (lexical-let ((buf (get-buffer-create (symbol-name (gensym))))
                (name (symbol-name (gensym)))
                (call-back call-back))
    (set-process-sentinel
     (start-process-shell-command name buf cmd)
     (lambda (status process)
       (let ((r (with-current-buffer buf
                  (buffer-string))))
         (kill-buffer buf)
         (funcall call-back r))))))

今度はたくさん呼んでも大丈夫です。便利ですね!!!