LoginSignup
2
2

More than 5 years have passed since last update.

Lem エディタから OS コマンドを実行する

Last updated at Posted at 2018-10-21

はじめに

Lem エディタのバッファ上から OS のコマンドを実行し、結果をバッファに書き戻す処理を実装します。

実行例

今回実装するプログラムの実行例は以下の通りです。
Lem エディタ内から Bourne Shell を起動し、ls -l を実行させ、結果をバッファに書き戻しています。

;; load program
(load "lemsh")

;; create sub process
(lemsh:start "sh")

;; interact with it
(lemsh:pwrite "ls -l")
-rw-r--r--  1 me  staff  2197 10 21 15:35 lemsh.md
-rw-r--r--  1 me  staff  2111 10 21 15:32 lemsh.lisp
total 16

;; quit
(lemsh:stop)

Bourne Shell 以外にも、任意のコマンドを実行することが可能です。

OS コマンドの実行方法

Lem エディタから OS コマンドを呼び出す方法はいくつかありますが、今回は uioplaunch-program を使用します。

uiop:launch-program は OS コマンドを実行する関数のポータブルな実装です。

Lem から OS コマンドを実行する方法は他にも存在しますが、Lem 自身の実装に uiop:run-program が使用されていたため、類似の uiop:launch-program を使用しています。

Disclaimer

  • Common Lisp には詳しくないので、間違いがある可能性があります
  • Common Lisp として適切なプログラミングスタイルではない可能性があります
  • エラー処理は実装していません
  • Lem の実装が変わった場合に、実行できなくなる可能性があります
  • プログラムの実行は自己責任でお願いします

実装

以下、順番に実装を解説していきます。

パッケージ

パッケージ名は lemsh としました。

Lem の関数を使用するため、lemlem-baseuse しています。

startstoppwrite の 3 つの関数を export しています。

(defpackage lemsh
  (:use cl lem lem-base)
  (:export start stop pwrite))

(in-package lemsh)

変数

パッケージで使用する変数を定義します。

*proc* はサブプロセスの情報を保持する変数です。
*p-in* はサブプロセスへの入力用ストリーム、*p-out* はサブプロセスからの出力用ストリームです。
*timeout**interval* はサブプロセスへのポリングの間隔を設定します。

(defvar *proc*)
(defvar *p-in*)
(defvar *p-out*)

(defvar *timeout* 0.05)
(defvar *interval* 0.01)

OS コマンドの実行

uiop:launch-program に OS コマンド名を文字列で渡してサブプロセスを作成します。

続いて、*proc**p-in**p-out* を設定しています。
サブプロセスの入力ストリームと出力ストリームは uiop:process-info-inputuiop:process-info-output で取得できます。

最後に、export 用に関数をラップしています。

;;; start program

(defun create-process (command)
  (setq *proc* (uiop:launch-program command
                                    :input :stream
                                    :output :stream))
  (setq *p-in* (uiop:process-info-input *proc*))
  (setq *p-out* (uiop:process-info-output *proc*)))

(defun start (command)
  (create-process command))

サブプロセスの停止

uiop:terminate-process でサブプロセスを停止します。

その前に、uiop:close-streams で入出力ストリームを閉じています。
uiop の実装内のコメントによると、入出力ストリームを作成した場合は実行するようにとのことなので、ここで実行しています。

また、uiop:wait-process でプロセスの停止を待機しています。
こちらも uiop のコメントで実行するように指定されています。

最後に export 用に関数をラップしています。

;;; stop program

(defun stop-process ()
  (uiop:close-streams *proc*)
  (uiop:terminate-process *proc*)
  (uiop:wait-process *proc*))

(defun stop ()
  (stop-process))

サブプロセスへの入力と、出力の受け取り

サブプロセスへの入力は *p-in* に文字列を書き込みます。
サブプロセスの出力の受け取りは *p-out* を読み出します。

サブプロセスへの入力

以下がサブプロセスへ文字列を書き込む関数です。
呼び出す際は、out*p-in* を指定します。

write-line で書き込んで finish-output でフラッシュしています。

(defun write-and-flush (str out)
  (progn
    (write-line str out)
    (finish-output out)))

サブプロセスの出力の確認

サブプロセスが複数行の出力を返す場合は、出力データがまだ残っていないか確認をする必要があります。

C の場合は select を使うことが多いと思いますが、Common Lisp では listenread-char-no-hang を使用します。
ここでは listen を使用して、サブプロセスの出力が残っていないか確認をしています。

サブプロセスの出力が遅延することも考慮して、timeout の時間分は出力を待機するような実装にしています。

(defun listen-with-timeout (timeout interval in)
  (if (< timeout 0)
      nil
      (if (listen in)
          t
          (progn
            (sleep interval)
            (listen-with-timeout (- timeout interval) interval in)))))

サブプロセスの出力を読み出す

read-line を使用して、サブプロセスの出力を 1 行ずつ読み出しています。
サブプロセスの出力が残っているかを先ほどの listen を使用して判定しています。

(defun read-all (in)
  (labels ((%read-all (acc in)
             (let ((timeout *timeout*)
                   (interval *interval*))
               (if (listen-with-timeout timeout interval in)
                   (%read-all (concatenate 'string
                                           (read-line in)
                                           (string #\newline)
                                           acc)
                              in)
                   acc))))
    (%read-all nil in)))

サブプロセスの出力をエディタのバッファに書き出す

read-line で読み出したサブプロセスの出力を Lem エディタのバッファに書き出します。

lem-base:current-point で現在のカーソル位置を取り出しています。
lem-base:insert-character で、指定した場所に改行を書き込んでいます。
最後に lem-base:insert-string で文字列を出力します。

(defun write-buffer (str)
  (let ((point (current-point)))
    (insert-character point #\newline)
    (insert-string point str)))

上記の関数をまとめる

サブプロセスへの入力、サブプロセスからの出力の読み出し、エディタバッファへの書き込みを関数にまとめます。

(defun pwrite (str)
  (write-and-flush str *p-in*)
  (write-buffer (read-all *p-out*)))

プログラムをファイルにまとめる

以上のコードを lemsh.lisp という名前のファイルにまとめて、カレントディレクトリに配置します。
Lem からは (load "lemsh") でプログラムをロードすることができます。

実行テスト

以下は bc -lq を実行して計算結果を受け取っている例です。

;; load program
(load "lemsh")

;; create sub process
(lemsh:start "bc -lq")

;; interact with it
(lemsh:pwrite "1 * 2 * 3 * 4")
;; => 24

;; quit
(lemsh:pwrite "quit")
(lemsh:stop)

まとめ

以上、Lem エディタ内から OS コマンドを実行し、その結果をエディタのバッファに出力する方法をまとめました。

付記

プログラムの改善ポイント

上記の実装には以下のような改善ポイントがありますが、このドキュメントの範囲外とさせていただきます。

  • 複数のコマンドを同時に実行することを考慮していません
  • エラー処理を行なっていません
  • 入力データや出力データが大きい場合については検討していません
2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2