Skip to content

Commit

Permalink
feat(lua): add vim.system()
Browse files Browse the repository at this point in the history
  • Loading branch information
lewis6991 committed May 31, 2023
1 parent 55f6a1c commit e30a6c7
Show file tree
Hide file tree
Showing 7 changed files with 480 additions and 149 deletions.
57 changes: 57 additions & 0 deletions runtime/doc/lua.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1571,6 +1571,63 @@ schedule_wrap({cb}) *vim.schedule_wrap()*
|vim.schedule()|
|vim.in_fast_event()|

system({spec}, {on_exit}) *vim.system()*
Run a system command

Examples: >lua

local on_exit = function(code, signal, stdout, stderr)
print(code)
print(signal)
print(stdout)
print(stderr)
end

-- Run asynchronously
vim.system({'echo', 'hello'}, on_exit)
vim.system({cmd = {'echo', 'hello'}}, on_exit)
vim.system({cmd = 'echo', args = {'hello'}}, on_exit)

-- Run synchronously
local code, signal, stdout, stderr = vim.system({'echo', 'hello'})
<

See |uv.spawn()| for more details.

Parameters: ~
{spec} (string[]|SystemSpec) When passed as a string array, the
argument is interpreted as a command. Accepts the options:
• cmd: (string|string[]) command to execute
• args: (string[]) Command line arguments. Ignored when cmd
is a string[]
• cwd: (string) Set the current working directory for the
sub-process.
• env: table<string,string> Set environment variables for
the new process.
• stdin: (string|string[]|uv_stream_t)
• stdout: (uv_stream_t|function) when passed as a function
must have the signature fun(err: string, data: string)
• stderr: (uv_stream_t|function) when passed as a function
must have the signature fun(err: string, data: string)
• timeout: (integer) Ignored when on_exit is provided.
Defaults to 10000ms.
• detached: (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. Arguments:
• code: (integer)
• signal: (integer)
• stdout: (string|nil), nil if stdout argument is passed
• stderr: (string|nil), nil if stderr argument is passed

Return: ~
uv_process_t|nil, integer|string process handle and PID


==============================================================================
Lua module: inspector *lua-inspector*
Expand Down
2 changes: 2 additions & 0 deletions runtime/doc/news.txt
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,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*

Expand Down
82 changes: 11 additions & 71 deletions runtime/lua/man.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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.loop.new_pipe(false))
local stderr = assert(vim.loop.new_pipe(false))
local function system(cmd, silent, env)
local code, _, stdout, stderr = vim.system({ cmd = cmd, env = env })

local done = false
local exit_code ---@type integer?

-- We use the `env` command here rather than the env option to vim.loop.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.loop.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
local cmd_str = table.concat(cmd, ' ')
man_error(string.format('command timed out: %s', cmd_str))
end

if exit_code ~= 0 and not silent then
if 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)))
man_error(string.format("command error '%s': %s", cmd_str, stderr))
end

return table.concat(stdout_data)
return assert(stdout)
end

---@param line string
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
70 changes: 60 additions & 10 deletions runtime/lua/vim/_editor.lua
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,60 @@ 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
--- Run a system command
---
--- Examples:
--- <pre>lua
---
--- local on_exit = function(code, signal, stdout, stderr)
--- print(code)
--- print(signal)
--- print(stdout)
--- print(stderr)
--- end
---
--- -- Run asynchronously
--- vim.system({'echo', 'hello'}, on_exit)
--- vim.system({cmd = {'echo', 'hello'}}, on_exit)
--- vim.system({cmd = 'echo', args = {'hello'}}, on_exit)
---
--- -- Run synchronously
--- local code, signal, stdout, stderr = vim.system({'echo', 'hello'})
---
--- </pre>
---
--- See |uv.spawn()| for more details.
---
--- @param spec (string[]|SystemSpec) When passed as a string array, the argument is interpreted
--- as a command. Accepts the options:
--- - cmd: (string|string[]) command to execute
--- - args: (string[]) Command line arguments. Ignored when cmd is a string[]
--- - cwd: (string) Set the current working directory for the sub-process.
--- - env: table<string,string> Set environment variables for the new
--- process.
--- - stdin: (string|string[]|uv_stream_t)
--- - stdout: (uv_stream_t|function)
--- when passed as a function must have the signature fun(err: string, data: string)
--- - stderr: (uv_stream_t|function)
--- when passed as a function must have the signature fun(err: string, data: string)
--- - timeout: (integer) Ignored when on_exit is provided. Defaults to 10000ms.
--- - detached: (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. Arguments:
--- - code: (integer)
--- - signal: (integer)
--- - stdout: (string|nil), nil if stdout argument is passed
--- - stderr: (string|nil), nil if stderr argument is passed
---
--- @return uv_process_t|nil, integer|string process handle and PID
function vim.system(spec, on_exit)
return require('vim._system').run(spec, on_exit)
end

-- Gets process info from the `ps` command.
Expand All @@ -81,13 +128,15 @@ function vim._os_proc_info(pid)
error('invalid pid')
end
local cmd = { 'ps', '-p', pid, '-o', 'comm=' }
local err, name = vim._system(cmd)
local err, _, name = vim.system(cmd)
assert(name)
if 1 == err and vim.trim(name) == '' then
return {} -- Process not found.
elseif 0 ~= err then
error('command failed: ' .. vim.fn.string(cmd))
end
local _, ppid = vim._system({ 'ps', '-p', pid, '-o', 'ppid=' })
local _, _, ppid = vim.system({ 'ps', '-p', pid, '-o', 'ppid=' })
assert(ppid)
-- Remove trailing whitespace.
name = vim.trim(name):gsub('^.*/', '')
ppid = tonumber(ppid) or -1
Expand All @@ -105,7 +154,8 @@ function vim._os_proc_children(ppid)
error('invalid ppid')
end
local cmd = { 'pgrep', '-P', ppid }
local err, rv = vim._system(cmd)
local err, _, rv = vim.system(cmd)
assert(rv)
if 1 == err and vim.trim(rv) == '' then
return {} -- Process not found.
elseif 0 ~= err then
Expand Down

0 comments on commit e30a6c7

Please sign in to comment.