feat(lua): add vim.system()

feat(lua): add vim.system()

Problem:

  Handling system commands in Lua is tedious and error-prone:
  - vim.fn.jobstart() is vimscript and comes with all limitations attached to typval.
  - vim.loop.spawn is too low level

Solution:

  Add vim.system().
  Partly inspired by Python's subprocess module
  Does not expose any libuv objects.
This commit is contained in:
Lewis Russell 2023-06-07 13:52:23 +01:00 committed by GitHub
parent 4ecc71f6fc
commit c0952e62fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 611 additions and 167 deletions

View File

@ -1566,6 +1566,77 @@ schedule_wrap({cb}) *vim.schedule_wrap()*
• |vim.schedule()|
• |vim.in_fast_event()|
system({cmd}, {opts}, {on_exit}) *vim.system()*
Run a system command
Examples: >lua
local on_exit = function(obj)
print(obj.code)
print(obj.signal)
print(obj.stdout)
print(obj.stderr)
end
-- Run asynchronously
vim.system({'echo', 'hello'}, { text = true }, on_exit)
-- Run synchronously
local obj = vim.system({'echo', 'hello'}, { text = true }):wait()
-- { code = 0, signal = 0, stdout = 'hello', stderr = '' }
<
See |uv.spawn()| for more details.
Parameters: ~
• {cmd} (string[]) Command to execute
• {opts} (SystemOpts|nil) Options:
• cwd: (string) Set the current working directory for the
sub-process.
• env: table<string,string> Set environment variables for
the new process. Inherits the current environment with
`NVIM` set to |v:servername|.
• clear_env: (boolean) `env` defines the job environment
exactly, instead of merging current environment.
• stdin: (string|string[]|boolean) If `true`, then a pipe
to stdin is opened and can be written to via the
`write()` method to SystemObj. If string or string[] then
will be written to stdin and closed. Defaults to `false`.
• stdout: (boolean|function) Handle output from stdout.
When passed as a function must have the signature
`fun(err: string, data: string)`. Defaults to `true`
• stderr: (boolean|function) Handle output from stdout.
When passed as a function must have the signature
`fun(err: string, data: string)`. Defaults to `true`.
• text: (boolean) Handle stdout and stderr as text.
Replaces `\r\n` with `\n`.
• timeout: (integer)
• detach: (boolean) If true, spawn the child process in a
detached state - this will make it a process group
leader, and will effectively enable the child to keep
running after the parent exits. Note that the child
process will still keep the parent's event loop alive
unless the parent process calls |uv.unref()| on the
child's process handle.
• {on_exit} (function|nil) Called when subprocess exits. When provided,
the command runs asynchronously. Receives SystemCompleted
object, see return of SystemObj:wait().
Return: ~
SystemObj Object with the fields:
• pid (integer) Process ID
• wait (fun(timeout: integer|nil): SystemCompleted)
• SystemCompleted is an object with the fields:
• code: (integer)
• signal: (integer)
• stdout: (string), nil if stdout argument is passed
• stderr: (string), nil if stderr argument is passed
• kill (fun(signal: integer))
• write (fun(data: string|nil)) Requires `stdin=true`. Pass `nil` to
close the stream.
• is_closing (fun(): boolean)
==============================================================================
Lua module: inspector *lua-inspector*

View File

@ -76,6 +76,8 @@ The following new APIs or features were added.
is resized horizontally). Note: Lines that are not visible and kept in
|'scrollback'| are not reflown.
• |vim.system()| for running system commands.
==============================================================================
CHANGED FEATURES *news-changed*

View File

@ -14,79 +14,19 @@ local function man_error(msg)
end
-- Run a system command and timeout after 30 seconds.
---@param cmd_ string[]
---@param cmd string[]
---@param silent boolean?
---@param env string[]
---@param env? table<string,string|number>
---@return string
local function system(cmd_, silent, env)
local stdout_data = {} ---@type string[]
local stderr_data = {} ---@type string[]
local stdout = assert(vim.uv.new_pipe(false))
local stderr = assert(vim.uv.new_pipe(false))
local function system(cmd, silent, env)
local r = vim.system(cmd, { env = env, timeout = 10000 }):wait()
local done = false
local exit_code ---@type integer?
-- We use the `env` command here rather than the env option to vim.uv.spawn since spawn will
-- completely overwrite the environment when we just want to modify the existing one.
--
-- Overwriting mainly causes problems NixOS which relies heavily on a non-standard environment.
local cmd = cmd_
if env then
cmd = { 'env' }
vim.list_extend(cmd, env)
vim.list_extend(cmd, cmd_)
end
local handle
handle = vim.uv.spawn(cmd[1], {
args = vim.list_slice(cmd, 2),
stdio = { nil, stdout, stderr },
}, function(code)
exit_code = code
stdout:close()
stderr:close()
handle:close()
done = true
end)
if handle then
stdout:read_start(function(_, data)
stdout_data[#stdout_data + 1] = data
end)
stderr:read_start(function(_, data)
stderr_data[#stderr_data + 1] = data
end)
else
stdout:close()
stderr:close()
if not silent then
local cmd_str = table.concat(cmd, ' ')
man_error(string.format('command error: %s', cmd_str))
end
return ''
end
vim.wait(30000, function()
return done
end)
if not done then
if handle then
handle:close()
stdout:close()
stderr:close()
end
if r.code ~= 0 and not silent then
local cmd_str = table.concat(cmd, ' ')
man_error(string.format('command timed out: %s', cmd_str))
man_error(string.format("command error '%s': %s", cmd_str, r.stderr))
end
if exit_code ~= 0 and not silent then
local cmd_str = table.concat(cmd, ' ')
man_error(string.format("command error '%s': %s", cmd_str, table.concat(stderr_data)))
end
return table.concat(stdout_data)
return assert(r.stdout)
end
---@param line string
@ -312,7 +252,7 @@ local function get_path(sect, name, silent)
end
local lines = system(cmd, silent)
local results = vim.split(lines or {}, '\n', { trimempty = true })
local results = vim.split(lines, '\n', { trimempty = true })
if #results == 0 then
return
@ -505,9 +445,9 @@ local function get_page(path, silent)
-- http://comments.gmane.org/gmane.editors.vim.devel/29085
-- Set MAN_KEEP_FORMATTING so Debian man doesn't discard backspaces.
return system(cmd, silent, {
'MANPAGER=cat',
'MANWIDTH=' .. manwidth,
'MAN_KEEP_FORMATTING=1',
MANPAGER = 'cat',
MANWIDTH = manwidth,
MAN_KEEP_FORMATTING = 1,
})
end

View File

@ -42,10 +42,6 @@ for k, v in pairs({
vim._submodules[k] = v
end
-- Remove at Nvim 1.0
---@deprecated
vim.loop = vim.uv
-- There are things which have special rules in vim._init_packages
-- for legacy reasons (uri) or for performance (_inspector).
-- most new things should go into a submodule namespace ( vim.foobar.do_thing() )
@ -69,13 +65,73 @@ vim.log = {
},
}
-- Internal-only until comments in #8107 are addressed.
-- Returns:
-- {errcode}, {output}
function vim._system(cmd)
local out = vim.fn.system(cmd)
local err = vim.v.shell_error
return err, out
-- TODO(lewis6991): document that the signature is system({cmd}, [{opts},] {on_exit})
--- Run a system command
---
--- Examples:
--- <pre>lua
---
--- local on_exit = function(obj)
--- print(obj.code)
--- print(obj.signal)
--- print(obj.stdout)
--- print(obj.stderr)
--- end
---
--- -- Run asynchronously
--- vim.system({'echo', 'hello'}, { text = true }, on_exit)
---
--- -- Run synchronously
--- local obj = vim.system({'echo', 'hello'}, { text = true }):wait()
--- -- { code = 0, signal = 0, stdout = 'hello', stderr = '' }
---
--- </pre>
---
--- See |uv.spawn()| for more details.
---
--- @param cmd (string[]) Command to execute
--- @param opts (SystemOpts|nil) Options:
--- - cwd: (string) Set the current working directory for the sub-process.
--- - env: table<string,string> Set environment variables for the new process. Inherits the
--- current environment with `NVIM` set to |v:servername|.
--- - clear_env: (boolean) `env` defines the job environment exactly, instead of merging current
--- environment.
--- - stdin: (string|string[]|boolean) If `true`, then a pipe to stdin is opened and can be written
--- to via the `write()` method to SystemObj. If string or string[] then will be written to stdin
--- and closed. Defaults to `false`.
--- - stdout: (boolean|function)
--- Handle output from stdout. When passed as a function must have the signature `fun(err: string, data: string)`.
--- Defaults to `true`
--- - stderr: (boolean|function)
--- Handle output from stdout. When passed as a function must have the signature `fun(err: string, data: string)`.
--- Defaults to `true`.
--- - text: (boolean) Handle stdout and stderr as text. Replaces `\r\n` with `\n`.
--- - timeout: (integer)
--- - detach: (boolean) If true, spawn the child process in a detached state - this will make it
--- a process group leader, and will effectively enable the child to keep running after the
--- parent exits. Note that the child process will still keep the parent's event loop alive
--- unless the parent process calls |uv.unref()| on the child's process handle.
---
--- @param on_exit (function|nil) Called when subprocess exits. When provided, the command runs
--- asynchronously. Receives SystemCompleted object, see return of SystemObj:wait().
---
--- @return SystemObj Object with the fields:
--- - pid (integer) Process ID
--- - wait (fun(timeout: integer|nil): SystemCompleted)
--- - SystemCompleted is an object with the fields:
--- - code: (integer)
--- - signal: (integer)
--- - stdout: (string), nil if stdout argument is passed
--- - stderr: (string), nil if stderr argument is passed
--- - kill (fun(signal: integer))
--- - write (fun(data: string|nil)) Requires `stdin=true`. Pass `nil` to close the stream.
--- - is_closing (fun(): boolean)
function vim.system(cmd, opts, on_exit)
if type(opts) == 'function' then
on_exit = opts
opts = nil
end
return require('vim._system').run(cmd, opts, on_exit)
end
-- Gets process info from the `ps` command.
@ -85,13 +141,14 @@ function vim._os_proc_info(pid)
error('invalid pid')
end
local cmd = { 'ps', '-p', pid, '-o', 'comm=' }
local err, name = vim._system(cmd)
if 1 == err and vim.trim(name) == '' then
local r = vim.system(cmd):wait()
local name = assert(r.stdout)
if r.code == 1 and vim.trim(name) == '' then
return {} -- Process not found.
elseif 0 ~= err then
elseif r.code ~= 0 then
error('command failed: ' .. vim.fn.string(cmd))
end
local _, ppid = vim._system({ 'ps', '-p', pid, '-o', 'ppid=' })
local ppid = assert(vim.system({ 'ps', '-p', pid, '-o', 'ppid=' }):wait().stdout)
-- Remove trailing whitespace.
name = vim.trim(name):gsub('^.*/', '')
ppid = tonumber(ppid) or -1
@ -109,14 +166,14 @@ function vim._os_proc_children(ppid)
error('invalid ppid')
end
local cmd = { 'pgrep', '-P', ppid }
local err, rv = vim._system(cmd)
if 1 == err and vim.trim(rv) == '' then
local r = vim.system(cmd):wait()
if r.code == 1 and vim.trim(r.stdout) == '' then
return {} -- Process not found.
elseif 0 ~= err then
elseif r.code ~= 0 then
error('command failed: ' .. vim.fn.string(cmd))
end
local children = {}
for s in rv:gmatch('%S+') do
for s in r.stdout:gmatch('%S+') do
local i = tonumber(s)
if i ~= nil then
table.insert(children, i)
@ -1006,4 +1063,8 @@ end
require('vim._meta')
-- Remove at Nvim 1.0
---@deprecated
vim.loop = vim.uv
return vim

342
runtime/lua/vim/_system.lua Normal file
View File

@ -0,0 +1,342 @@
local uv = vim.uv
--- @class SystemOpts
--- @field cmd string[]
--- @field stdin string|string[]|true
--- @field stdout fun(err:string, data: string)|false
--- @field stderr fun(err:string, data: string)|false
--- @field cwd? string
--- @field env? table<string,string|number>
--- @field clear_env? boolean
--- @field text boolean?
--- @field timeout? integer Timeout in ms
--- @field detach? boolean
--- @class SystemCompleted
--- @field code integer
--- @field signal integer
--- @field stdout? string
--- @field stderr? string
--- @class SystemState
--- @field handle uv_process_t
--- @field timer uv_timer_t
--- @field pid integer
--- @field timeout? integer
--- @field done boolean
--- @field stdin uv_stream_t?
--- @field stdout uv_stream_t?
--- @field stderr uv_stream_t?
--- @field cmd string[]
--- @field result? SystemCompleted
---@private
---@param state SystemState
local function close_handles(state)
for _, handle in pairs({ state.handle, state.stdin, state.stdout, state.stderr }) do
if not handle:is_closing() then
handle:close()
end
end
end
--- @param cmd string[]
--- @return SystemCompleted
local function timeout_result(cmd)
local cmd_str = table.concat(cmd, ' ')
local err = string.format("Command timed out: '%s'", cmd_str)
return { code = 0, signal = 2, stdout = '', stderr = err }
end
--- @class SystemObj
--- @field pid integer
--- @field private _state SystemState
--- @field wait fun(self: SystemObj, timeout?: integer): SystemCompleted
--- @field kill fun(self: SystemObj, signal: integer)
--- @field write fun(self: SystemObj, data?: string|string[])
--- @field is_closing fun(self: SystemObj): boolean?
local SystemObj = {}
--- @param state SystemState
--- @return SystemObj
local function new_systemobj(state)
return setmetatable({
pid = state.pid,
_state = state,
}, { __index = SystemObj })
end
--- @param signal integer
function SystemObj:kill(signal)
local state = self._state
state.handle:kill(signal)
close_handles(state)
end
local MAX_TIMEOUT = 2 ^ 31
--- @param timeout? integer
--- @return SystemCompleted
function SystemObj:wait(timeout)
local state = self._state
vim.wait(timeout or state.timeout or MAX_TIMEOUT, function()
return state.done
end)
if not state.done then
self:kill(6) -- 'sigint'
state.result = timeout_result(state.cmd)
end
return state.result
end
--- @param data string[]|string|nil
function SystemObj:write(data)
local stdin = self._state.stdin
if not stdin then
error('stdin has not been opened on this object')
end
if type(data) == 'table' then
for _, v in ipairs(data) do
stdin:write(v)
stdin:write('\n')
end
elseif type(data) == 'string' then
stdin:write(data)
elseif data == nil then
-- Shutdown the write side of the duplex stream and then close the pipe.
-- Note shutdown will wait for all the pending write requests to complete
-- TODO(lewis6991): apparently shutdown doesn't behave this way.
-- (https://github.com/neovim/neovim/pull/17620#discussion_r820775616)
stdin:write('', function()
stdin:shutdown(function()
if stdin then
stdin:close()
end
end)
end)
end
end
--- @return boolean
function SystemObj:is_closing()
local handle = self._state.handle
return handle == nil or handle:is_closing()
end
---@private
---@param output function|'false'
---@return uv_stream_t?
---@return function? Handler
local function setup_output(output)
if output == nil then
return assert(uv.new_pipe(false)), nil
end
if type(output) == 'function' then
return assert(uv.new_pipe(false)), output
end
assert(output == false)
return nil, nil
end
---@private
---@param input string|string[]|true|nil
---@return uv_stream_t?
---@return string|string[]?
local function setup_input(input)
if not input then
return
end
local towrite --- @type string|string[]?
if type(input) == 'string' or type(input) == 'table' then
towrite = input
end
return assert(uv.new_pipe(false)), towrite
end
--- @return table<string,string>
local function base_env()
local env = vim.fn.environ()
env['NVIM'] = vim.v.servername
env['NVIM_LISTEN_ADDRESS'] = nil
return env
end
--- uv.spawn will completely overwrite the environment
--- when we just want to modify the existing one, so
--- make sure to prepopulate it with the current env.
--- @param env? table<string,string|number>
--- @param clear_env? boolean
--- @return string[]?
local function setup_env(env, clear_env)
if clear_env then
return env
end
--- @type table<string,string|number>
env = vim.tbl_extend('force', base_env(), env or {})
local renv = {} --- @type string[]
for k, v in pairs(env) do
renv[#renv + 1] = string.format('%s=%s', k, tostring(v))
end
return renv
end
--- @param stream uv_stream_t
--- @param text? boolean
--- @param bucket string[]
--- @return fun(err: string?, data: string?)
local function default_handler(stream, text, bucket)
return function(err, data)
if err then
error(err)
end
if data ~= nil then
if text then
bucket[#bucket + 1] = data:gsub('\r\n', '\n')
else
bucket[#bucket + 1] = data
end
else
stream:read_stop()
stream:close()
end
end
end
local M = {}
--- @param cmd string
--- @param opts uv.aliases.spawn_options
--- @param on_exit fun(code: integer, signal: integer)
--- @param on_error fun()
--- @return uv_process_t, integer
local function spawn(cmd, opts, on_exit, on_error)
local handle, pid_or_err = uv.spawn(cmd, opts, on_exit)
if not handle then
on_error()
error(pid_or_err)
end
return handle, pid_or_err --[[@as integer]]
end
--- Run a system command
---
--- @param cmd string[]
--- @param opts? SystemOpts
--- @param on_exit? fun(out: SystemCompleted)
--- @return SystemObj
function M.run(cmd, opts, on_exit)
vim.validate({
cmd = { cmd, 'table' },
opts = { opts, 'table', true },
on_exit = { on_exit, 'function', true },
})
opts = opts or {}
local stdout, stdout_handler = setup_output(opts.stdout)
local stderr, stderr_handler = setup_output(opts.stderr)
local stdin, towrite = setup_input(opts.stdin)
--- @type SystemState
local state = {
done = false,
cmd = cmd,
timeout = opts.timeout,
stdin = stdin,
stdout = stdout,
stderr = stderr,
}
-- Define data buckets as tables and concatenate the elements at the end as
-- one operation.
--- @type string[], string[]
local stdout_data, stderr_data
state.handle, state.pid = spawn(cmd[1], {
args = vim.list_slice(cmd, 2),
stdio = { stdin, stdout, stderr },
cwd = opts.cwd,
env = setup_env(opts.env, opts.clear_env),
detached = opts.detach,
hide = true,
}, function(code, signal)
close_handles(state)
if state.timer then
state.timer:stop()
state.timer:close()
end
local check = assert(uv.new_check())
check:start(function()
for _, pipe in pairs({ state.stdin, state.stdout, state.stderr }) do
if not pipe:is_closing() then
return
end
end
check:stop()
state.done = true
state.result = {
code = code,
signal = signal,
stdout = stdout_data and table.concat(stdout_data) or nil,
stderr = stderr_data and table.concat(stderr_data) or nil,
}
if on_exit then
on_exit(state.result)
end
end)
end, function()
close_handles(state)
end)
if stdout then
stdout_data = {}
stdout:read_start(stdout_handler or default_handler(stdout, opts.text, stdout_data))
end
if stderr then
stderr_data = {}
stderr:read_start(stderr_handler or default_handler(stderr, opts.text, stderr_data))
end
local obj = new_systemobj(state)
if towrite then
obj:write(towrite)
obj:write(nil) -- close the stream
end
if opts.timeout then
state.timer = assert(uv.new_timer())
state.timer:start(opts.timeout, 0, function()
state.timer:stop()
state.timer:close()
if state.handle and state.handle:is_active() then
obj:kill(6) --- 'sigint'
state.result = timeout_result(state.cmd)
if on_exit then
on_exit(state.result)
end
end
end)
end
return obj
end
return M

View File

@ -14,32 +14,6 @@ local function is_dir(filename)
return stat and stat.type == 'directory' or false
end
---@private
--- Merges current process env with the given env and returns the result as
--- a list of "k=v" strings.
---
--- <pre>
--- Example:
---
--- in: { PRODUCTION="false", PATH="/usr/bin/", PORT=123, HOST="0.0.0.0", }
--- out: { "PRODUCTION=false", "PATH=/usr/bin/", "PORT=123", "HOST=0.0.0.0", }
--- </pre>
---@param env (table) table of environment variable assignments
---@returns (table) list of `"k=v"` strings
local function env_merge(env)
if env == nil then
return env
end
-- Merge.
env = vim.tbl_extend('force', vim.fn.environ(), env)
local final_env = {}
for k, v in pairs(env) do
assert(type(k) == 'string', 'env must be a dict')
table.insert(final_env, k .. '=' .. tostring(v))
end
return final_env
end
---@private
--- Embeds the given string into a table and correctly computes `Content-Length`.
---
@ -658,89 +632,85 @@ end
--- - `is_closing()` returns a boolean indicating if the RPC is closing.
--- - `terminate()` terminates the RPC client.
local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
local _ = log.info()
and log.info('Starting RPC client', { cmd = cmd, args = cmd_args, extra = extra_spawn_params })
if log.info() then
log.info('Starting RPC client', { cmd = cmd, args = cmd_args, extra = extra_spawn_params })
end
validate({
cmd = { cmd, 's' },
cmd_args = { cmd_args, 't' },
dispatchers = { dispatchers, 't', true },
})
if extra_spawn_params and extra_spawn_params.cwd then
extra_spawn_params = extra_spawn_params or {}
if extra_spawn_params.cwd then
assert(is_dir(extra_spawn_params.cwd), 'cwd must be a directory')
end
dispatchers = merge_dispatchers(dispatchers)
local stdin = uv.new_pipe(false)
local stdout = uv.new_pipe(false)
local stderr = uv.new_pipe(false)
local handle, pid
local sysobj ---@type SystemObj
local client = new_client(dispatchers, {
write = function(msg)
stdin:write(msg)
sysobj:write(msg)
end,
is_closing = function()
return handle == nil or handle:is_closing()
return sysobj == nil or sysobj:is_closing()
end,
terminate = function()
if handle then
handle:kill(15)
end
sysobj:kill(15)
end,
})
---@private
--- Callback for |vim.uv.spawn()| Closes all streams and runs the `on_exit` dispatcher.
---@param code (integer) Exit code
---@param signal (integer) Signal that was used to terminate (if any)
local function onexit(code, signal)
stdin:close()
stdout:close()
stderr:close()
handle:close()
dispatchers.on_exit(code, signal)
local handle_body = function(body)
client:handle_body(body)
end
local spawn_params = {
args = cmd_args,
stdio = { stdin, stdout, stderr },
detached = not is_win,
}
if extra_spawn_params then
spawn_params.cwd = extra_spawn_params.cwd
spawn_params.env = env_merge(extra_spawn_params.env)
if extra_spawn_params.detached ~= nil then
spawn_params.detached = extra_spawn_params.detached
local stdout_handler = create_read_loop(handle_body, nil, function(err)
client:on_error(client_errors.READ_ERROR, err)
end)
local stderr_handler = function(_, chunk)
if chunk and log.error() then
log.error('rpc', cmd, 'stderr', chunk)
end
end
handle, pid = uv.spawn(cmd, spawn_params, onexit)
if handle == nil then
stdin:close()
stdout:close()
stderr:close()
local detached = not is_win
if extra_spawn_params.detached ~= nil then
detached = extra_spawn_params.detached
end
local cmd1 = { cmd }
vim.list_extend(cmd1, cmd_args)
local ok, sysobj_or_err = pcall(vim.system, cmd1, {
stdin = true,
stdout = stdout_handler,
stderr = stderr_handler,
cwd = extra_spawn_params.cwd,
env = extra_spawn_params.env,
detach = detached,
}, function(obj)
dispatchers.on_exit(obj.code, obj.signal)
end)
if not ok then
local err = sysobj_or_err --[[@as string]]
local msg = string.format('Spawning language server with cmd: `%s` failed', cmd)
if string.match(pid, 'ENOENT') then
if string.match(err, 'ENOENT') then
msg = msg
.. '. The language server is either not installed, missing from PATH, or not executable.'
else
msg = msg .. string.format(' with error message: %s', pid)
msg = msg .. string.format(' with error message: %s', err)
end
vim.notify(msg, vim.log.levels.WARN)
return
end
stderr:read_start(function(_, chunk)
if chunk then
local _ = log.error() and log.error('rpc', cmd, 'stderr', chunk)
end
end)
local handle_body = function(body)
client:handle_body(body)
end
stdout:read_start(create_read_loop(handle_body, nil, function(err)
client:on_error(client_errors.READ_ERROR, err)
end))
sysobj = sysobj_or_err --[[@as SystemObj]]
return public_client(client)
end

View File

@ -340,6 +340,7 @@ function TLua2DoX_filter.filter(this, AppStamp, Filename)
if vim.startswith(line, '---@cast')
or vim.startswith(line, '---@diagnostic')
or vim.startswith(line, '---@overload')
or vim.startswith(line, '---@type') then
-- Ignore LSP directives
outStream:writeln('// gg:"' .. line .. '"')

View File

@ -0,0 +1,57 @@
local helpers = require('test.functional.helpers')(after_each)
local clear = helpers.clear
local exec_lua = helpers.exec_lua
local eq = helpers.eq
local function system_sync(cmd, opts)
return exec_lua([[
return vim.system(...):wait()
]], cmd, opts)
end
local function system_async(cmd, opts)
exec_lua([[
local cmd, opts = ...
_G.done = false
vim.system(cmd, opts, function(obj)
_G.done = true
_G.ret = obj
end)
]], cmd, opts)
while true do
if exec_lua[[return _G.done]] then
break
end
end
return exec_lua[[return _G.ret]]
end
describe('vim.system', function()
before_each(function()
clear()
end)
for name, system in pairs{ sync = system_sync, async = system_async, } do
describe('('..name..')', function()
it('can run simple commands', function()
eq('hello\n', system({'echo', 'hello' }, { text = true }).stdout)
end)
it('handle input', function()
eq('hellocat', system({ 'cat' }, { stdin = 'hellocat', text = true }).stdout)
end)
it ('supports timeout', function()
eq({
code = 0,
signal = 2,
stdout = '',
stderr = "Command timed out: 'sleep 10'"
}, system({ 'sleep', '10' }, { timeout = 1 }))
end)
end)
end
end)