📝
Neovimのvim.systemで起動したプロセスの完了を待たずに標準出力を1行ずつ処理する
vim.system()
はNeovim Luaで外部プロセスを扱うのに便利。
しかし、タイトルのケースでは注意が必要なため対応例を示す。
プロセスの完了を待って処理する場合
vim.system()
で外部プロセスを起動し完了後に標準出力を処理する場合は以下でいい。
vim.system({ "ls" }, {
text = true,
}, function(o)
if o.code ~= 0 then
error(o.stderr)
end
vim.print(o.stdout)
end)
プロセスの完了を待たずに処理する場合
完了を待ちたくない、かつ、大きめの出力を行ごとに処理する場合には工夫が必要になる。
以下はパイプの容量を超える長さの行が分割されている。
vim.system({ "echo", "-n", "test1\n" .. ("a"):rep(65536) .. [[a
test2
test3]] }, {
text = true,
stdout = function(err, str)
assert(not err, err)
if not str then
return
end
vim.print(vim.split(str, "\n", { plain = true }))
end,
})
出力
{ "test1", "aaa...(省略)" }
{ "aaaaaaa", "test2", "test3" }
📝 参考: https://man7.org/linux/man-pages/man7/pipe.7.html のPipe capacity
参考: 容量を変更したパイプを使ってみる
vim.system()
の内部ではvim.uv.spawn()
を使っている。
以下のコードではvim.uv.spawn()
で容量を変えたパイプを使ってみて変化を確認している。
local ffi = require("ffi")
ffi.cdef([[
int fcntl(int fd, int cmd, ...);
]])
local F_SETPIPE_SZ = 1031
local fds = assert(vim.uv.pipe({ nonblock = true }, { nonblock = true }))
ffi.C.fcntl(fds.read, F_SETPIPE_SZ, 4096)
local write_pipe = assert(vim.uv.new_pipe(false))
write_pipe:open(fds.write)
local read_pipe = assert(vim.uv.new_pipe(false))
read_pipe:open(fds.read)
local opts = {
args = { ("a"):rep(4097) },
stdio = { nil, write_pipe, nil },
}
local handle = vim.uv.spawn("echo", opts, function() end)
local count = 0
read_pipe:read_start(function(err, data)
assert(not err, err)
count = count + 1
vim.print({
count = count,
data = data,
})
end)
vim.wait(1000, function()
return handle:is_active() == false
end, nil, true)
出力
{
count = 1,
data = "aaa...(省略)"
}
{
count = 2,
data = "a\n"
}
変更後のパイプの容量を超える出力のため1行が分割されている。
参考: パイプの容量を確認する
local ffi = require("ffi")
ffi.cdef([[
int fcntl(int fd, int cmd, ...);
]])
local F_GETPIPE_SZ = 1032
local fds = assert(vim.uv.pipe({ nonblock = true }, { nonblock = true }))
local size = ffi.C.fcntl(fds.read, F_GETPIPE_SZ)
print(size)
出力
65536
jobstart()
に関する補足
Vim Scriptの関数のjobstart()
にも似たような挙動が存在し、
以下のヘルプで対応方法が説明されている。
こちらはstring
でなくstring[]
として出力を受け取る。
行ごとに処理する実装例
出力を受け取るごとに改行で分割して、最後の要素は不完全な行の可能性があるため次の出力と結合する。
これを繰り返す。
末尾の空行をトリムしていい場合はvim.split()
のtrimempty
オプションを使って少し簡略化できる。
local new_line_parser = function()
local incomplete
local finish = function()
if incomplete == nil then
return {}
end
return vim.split(incomplete, "\n", { plain = true })
end
return function(data)
if not data then
return finish()
end
local joined = (incomplete or "") .. data
local lines = vim.split(joined, "\n", { plain = true })
incomplete = table.remove(lines)
return lines
end
end
local to_lines = new_line_parser()
vim.system({ "echo", "-n", "test1\n" .. ("a"):rep(65536) .. [[a
test2
test3]] }, {
text = true,
stdout = function(err, data)
assert(not err, err)
vim.print(to_lines(data))
end,
})
{ "test1" }
{ "aaa...(省略)", "test2" }
{ "test3" }
感想
パイプを使っていれば言語に依存しないよくある話なんだと思うが、
大きめの出力でしか起きないバグを生むので気をつけたい。
Discussion