コマンド実行

I/Oストリームを完全に制御して外部コマンドとシェルスクリプトを実行します。

エグゼキュータの設定についてはエグゼキュータを参照。

ロード

local exec = require("exec")

エグゼキュータの取得

IDでプロセスエグゼキュータリソースを取得します:

local executor, err = exec.get("app:exec")
if err then
    return nil, err
end

-- エグゼキュータを使用
local proc = executor:exec("ls -la")
-- ...

-- 完了時に解放
executor:release()
パラメータ 型 説明
id string リソースID

戻り値: Executor, error

プロセスの作成

指定されたコマンドで新しいプロセスを作成します:

-- シンプルなコマンド
local proc, err = executor:exec("echo 'Hello, World!'")

-- 作業ディレクトリ付き
local proc = executor:exec("npm install", {
    work_dir = "/app/project"
})

-- 環境変数付き
local proc = executor:exec("python script.py", {
    work_dir = "/scripts",
    env = {
        PYTHONPATH = "/app/lib",
        DEBUG = "true",
        API_KEY = api_key
    }
})

-- シェルスクリプトを実行
local proc = executor:exec("./deploy.sh production", {
    work_dir = "/app/scripts",
    env = {
        DEPLOY_ENV = "production"
    }
})
パラメータ 型 説明
cmd string 実行ファイルとリテラルの引数
options.work_dir string 作業ディレクトリ
options.env table 環境変数
options.pty table 子プロセス用の疑似ターミナルを割り当てる
options.process_group boolean 子プロセスを独自のプロセスグループで開始し、シグナルが子孫にも届くようにする。Windowsでは非対応

戻り値: Process, error

プロセスは作成されますが、開始はされません。

コマンドの解析

cmdは、シェル風のクォート規則で実行ファイルとリテラルの引数に分割されます。シングルクォートとダブルクォートは単語をまとめ、バックスラッシュは後続の1文字をエスケープします。シェルは介在しないため、変数展開、グロブ、パイプ、リダイレクトは行われません。閉じられていないクォートはerrors.INVALIDを返します。

-- スペースを含む1つの引数がリテラルとして渡される
local proc = executor:exec("grep 'hello world' notes.txt")

-- $HOMEは展開されず、$HOMEという5文字として渡される
local proc = executor:exec("echo $HOME")

シェルの機能を使うには、シェルを明示的に呼び出します:

local proc = executor:exec("/bin/sh -c 'ls *.log | wc -l'")

PTYオプション

PTYを割り当てると、子プロセスは実際のターミナルを得ます。行編集、ジョブ制御、フルスクリーンプログラムがシェル上と同じように動作します。

local proc = executor:exec("/bin/bash --noprofile --norc", {
    pty = {width = 100, height = 30, term = "xterm-256color"},
})
フィールド 型 デフォルト 説明
width number 80 PTYの初期カラム数(1〜65535)
height number 24 PTYの初期行数(1〜65535)
term string なし 子プロセスのTERM値

幅×高さは262,144セルを超えられません。PTYを持つプロセスは子プロセスの出力を単一のターミナルストリームにまとめます。stdin/stdoutのパイプメソッドではなく、resizeとattach_terminalで操作してください。

start / wait

プロセスを開始して完了を待機します。

local proc = executor:exec("./build.sh")

local ok, err = proc:start()
if err then
    return nil, err
end

local exit_code, err = proc:wait()
if err then
    return nil, err
end

if exit_code ~= 0 then
    return nil, errors.new({ kind = errors.INTERNAL, message = "Build failed with exit code: " .. exit_code })
end

stdout_stream / stderr_stream

プロセス出力を読み取るストリームを取得します。

local proc = executor:exec("./process-data.sh")

local stdout = proc:stdout_stream()
local stderr = proc:stderr_stream()

proc:start()

-- すべてのstdoutを読み取り
local output = {}
while true do
    local chunk = stdout:read(4096)
    if not chunk then break end
    table.insert(output, chunk)
end
local result = table.concat(output)

-- エラーをチェック
local err_output = {}
while true do
    local chunk = stderr:read(4096)
    if not chunk then break end
    table.insert(err_output, chunk)
end

local exit_code = proc:wait()

stdout:close()
stderr:close()

if exit_code ~= 0 then
    return nil, errors.new({ kind = errors.INTERNAL, message = table.concat(err_output) })
end

return result

write_stdin

プロセスのstdinにデータを書き込みます。

local proc = executor:exec("head -n 3")
local stdout = proc:stdout_stream()

proc:start()

proc:write_stdin("banana\napple\ncherry\n")

local lines = stdout:read()

proc:wait()
stdout:close()

各呼び出しは指定されたバイト列を書き込んで戻ります。子プロセスにEOFを認識させる必要がある場合は、close_stdin()を呼び出します:

local proc = assert(executor:exec("sort"))
local stdout = assert(proc:stdout_stream())
assert(proc:start())
assert(proc:write_stdin("banana\napple\n"))
assert(proc:close_stdin())
local sorted = assert(stdout:read())

close_stdin()は冪等です。入力側を閉じた後の書き込みは失敗します。PTYベースのプロセスではこのパイプ操作を利用できません。

done

プロセスハンドルを消費せずに終了を監視するには、done()を使用します:

local proc = assert(executor:exec("./worker"))
assert(proc:start())
local exits = assert(proc:done())

local status, open = exits:receive()
if open then
    print(status.code, status.signal, status.error)
end

返されるチャネルは終了レコードを1件送信した後に閉じます。繰り返し呼び出すと同じチャネルが返されます。レコードにはcode、任意のsignalが含まれ、ランタイムが終了を監視できなかった場合のみerrorが含まれます。シグナルによる終了では、コードは128 + signalになります。wait()と異なり、done()はハンドルを引き続き使用可能にするため、ストリーム、signal()、close()を利用できます。通知後にwait()を呼び出すと、記録されたコードを返します。

signal / close

シグナルを送信またはプロセスを解放します。

local proc = executor:exec("./long-running-server.sh")
proc:start()

-- ... 後でそれを停止する必要がある ...

-- SIGTERMを送信してハンドルを解放
proc:close()

-- SIGKILLを送信してハンドルを解放
proc:close(true)

-- または特定のシグナルを送信し、ハンドルは保持
local SIGINT = 2
proc:signal(SIGINT)

close(force?)は、開始済みの子プロセスにSIGTERMを(forceがtrueの場合はSIGKILLを)送信し、その後バックグラウンドで回収するため、呼び出しはブロックしません。猶予期間を過ぎても実行中の子プロセスはkillされ、回収が必ず完了します。開始されていないハンドルは単に無効化され、二重にクローズしてもエラーにはなりません。process_groupを有効にすると、シグナルはグループを対象とし、リーダーの終了後も子孫に届きます。

回収前に取得したストリームは、最後の書き込み側が閉じるまで読み取り可能です。パイプを継承した子孫プロセスも含まれます。close()後はプロセスのメソッドがprocess closedを報告します。終了が重要でハンドルを引き続き使用する必要がある場合は、done()を使用してください。

resize

PTYを持つプロセスのPTYをリサイズします。パイプベースのプロセスではエラーを返します。

local ok, err = proc:resize(120, 40)
パラメータ 型 説明
width number カラム数(1〜65535)
height number 行数(1〜65535)

戻り値: boolean, error

プロセスをターミナルセッションに渡す前に、初期ジオメトリを設定するために使用します。セッションがプロセスを所有した後は、代わりにresizeイベントをセッションへ送信してください。

attach_terminal

開始されていないPTYベースのプロセスを呼び出し元プロセスのターミナルにアタッチし、TerminalSessionを返します。

local exec = require("exec")
local tty = require("tty")

local executor = assert(exec.get("app:exec"))
local proc = assert(executor:exec("/bin/bash --noprofile --norc", {
    pty = {term = "xterm-256color"},
}))
local session = assert(proc:attach_terminal())

戻り値: TerminalSession, error

この呼び出しはプロセスを消費します。セッションが唯一のライフサイクル所有者となり、元のハンドルは使用できなくなります。セッションは現在のターミナルポート上にサーフェスを開き、PTYエミュレーション、入力エンコーディング、リサイズ、グレースフルおよび強制終了、回収を所有します。セッションにはターミナルポートが必要で(ターミナルホスト上のプロセス、またはビューポートグラント付きでスポーンされたプロセス)、ポートに入力コントローラがない場合や既にサーフェスが開かれている場合は失敗します。

TerminalSession

メソッド 戻り値 説明
send(event) boolean, error 正規化されたTTYイベントを1つ子プロセスへ転送する
done() channel 子プロセスの終了時に一度だけ発火するチャネル
status() string, error "running"または"done"。失敗した場合はその失敗エラーを伴う
close() boolean, error 実行中の子プロセスの終了を要求する

sendはTTYで説明されているキー、マウス、リサイズ、フォーカス、ペーストの各レコードを受け付けます。子プロセスの終了後に送信するとエラーを返します。

local channel = require("channel")

local events = assert(tty.events())
assert(tty.start())
local done = session:done()

while true do
    local selected = channel.select({
        events:case_receive(),
        done:case_receive(),
    })
    if not selected.ok or selected.channel == done then break end
    if selected.value.type == "close" then break end
    assert(session:send(selected.value))
end

assert(session:close())

権限

Exec操作はセキュリティポリシー評価の対象です。

アクション リソース 説明
exec.get エグゼキュータID エグゼキュータリソースを取得
exec.run コマンド 特定のコマンドを実行

exec.runは生のコマンド文字列に対して評価され、要求されたオプションがメタデータとして渡されます:

キー 型 説明
work_dir string 要求された作業ディレクトリ。未設定の場合は空
env_names string[] 渡された環境変数の名前(ソート済み)。値は公開されない
pty.requested boolean PTYが要求されたかどうか
pty.width number 解決されたPTYのカラム数。要求された場合に存在
pty.height number 解決されたPTYの行数。要求された場合に存在
pty.term string 要求されたTERM値。要求された場合に存在

したがってポリシーは、通常のコマンドは許可しつつ、ターミナルや特定の作業ディレクトリを要求するコマンドだけを制限できます。

エラー

条件 種別 再試行可能
無効なID errors.INVALID no
権限拒否 errors.INVALID no
プロセスがクローズ済み errors.INVALID no
プロセスが開始されていない errors.INVALID no
既に開始済み errors.INVALID no
コマンド内のクォートが閉じられていない errors.INVALID no
プロセスにPTYがない errors.INVALID no
ターミナルポートが利用できない errors.UNAVAILABLE no

エラーの処理についてはエラー処理を参照。

関連項目

  • エグゼキュータ — エグゼキュータの設定
  • TTY — ターミナルイベント、サーフェス、ビューポート
  • ターミナルUI — ビューポートでPTY子プロセスをホストするシェル