commit e644038f06b4e3401f9b496bbe4337bfe8b18146
parent 7f18811668708e7d0505596c0cdb7a15ba3a5fcf
Author: Lewis Russell <lewis6991@gmail.com>
Date: Thu, 10 Jul 2025 10:32:17 +0100
docs: move vim.system to own section
Diffstat:
7 files changed, 267 insertions(+), 143 deletions(-)
diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt
@@ -1754,6 +1754,101 @@ vim.str_utfindex({s}, {encoding}, {index}, {strict_indexing})
Return: ~
(`integer`)
+
+==============================================================================
+Lua module: vim.system *lua-vim-system*
+
+*vim.SystemCompleted*
+
+ Fields: ~
+ • {code} (`integer`)
+ • {signal} (`integer`)
+ • {stdout}? (`string`) `nil` if stdout is disabled or has a custom
+ handler.
+ • {stderr}? (`string`) `nil` if stderr is disabled or has a custom
+ handler.
+
+*vim.SystemObj*
+
+ Fields: ~
+ • {cmd} (`string[]`) Command name and args
+ • {pid} (`integer`) Process ID
+ • {kill} (`fun(self: vim.SystemObj, signal: integer|string)`) See
+ |SystemObj:kill()|.
+ • {wait} (`fun(self: vim.SystemObj, timeout: integer?): vim.SystemCompleted`)
+ See |SystemObj:wait()|.
+ • {write} (`fun(self: vim.SystemObj, data: string[]|string?)`) See
+ |SystemObj:write()|.
+ • {is_closing} (`fun(self: vim.SystemObj): boolean`) See
+ |SystemObj:is_closing()|.
+
+
+SystemObj:is_closing() *SystemObj:is_closing()*
+ Checks if the process handle is closing or already closed.
+
+ This method returns `true` if the underlying process handle is either
+ `nil` or is in the process of closing. It is useful for determining
+ whether it is safe to perform operations on the process handle.
+
+ Return: ~
+ (`boolean`)
+
+SystemObj:kill({signal}) *SystemObj:kill()*
+ Sends a signal to the process.
+
+ The signal can be specified as an integer or as a string.
+
+ Example: >lua
+ local obj = vim.system({'sleep', '10'})
+ obj:kill('TERM') -- sends SIGTERM to the process
+<
+
+ Parameters: ~
+ • {signal} (`integer|string`) Signal to send to the process.
+
+SystemObj:wait({timeout}) *SystemObj:wait()*
+ Waits for the process to complete or until the specified timeout elapses.
+
+ This method blocks execution until the associated process has exited or
+ the optional `timeout` (in milliseconds) has been reached. If the process
+ does not exit before the timeout, it is forcefully terminated with SIGKILL
+ (signal 9), and the exit code is set to 124.
+
+ If no `timeout` is provided, the method will wait indefinitely (or use the
+ timeout specified in the options when the process was started).
+
+ Example: >lua
+ local obj = vim.system({'echo', 'hello'}, { text = true })
+ local result = obj:wait(1000) -- waits up to 1000ms
+ print(result.code, result.signal, result.stdout, result.stderr)
+<
+
+ Parameters: ~
+ • {timeout} (`integer?`)
+
+ Return: ~
+ (`vim.SystemCompleted`) See |vim.SystemCompleted|.
+
+SystemObj:write({data}) *SystemObj:write()*
+ Writes data to the stdin of the process or closes stdin.
+
+ If `data` is a list of strings, each string is written followed by a
+ newline.
+
+ If `data` is a string, it is written as-is.
+
+ If `data` is `nil`, the write side of the stream is shut down and the pipe
+ is closed.
+
+ Example: >lua
+ local obj = vim.system({'cat'}, { stdin = true })
+ obj:write({'hello', 'world'}) -- writes 'hello\nworld\n' to stdin
+ obj:write(nil) -- closes stdin
+<
+
+ Parameters: ~
+ • {data} (`string[]|string?`)
+
vim.system({cmd}, {opts}, {on_exit}) *vim.system()*
Runs a system command or throws an error if {cmd} cannot be run.
@@ -1778,32 +1873,30 @@ vim.system({cmd}, {opts}, {on_exit}) *vim.system()*
Parameters: ~
• {cmd} (`string[]`) Command to execute
- • {opts} (`vim.SystemOpts?`) 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. Note: if
- `env` is `nil`, the current environment is used but
- without `NVIM` set.
- • stdin: (string|string[]|boolean) If `true`, then a pipe
+ • {opts} (`table?`) A table with the following fields:
+ • {cwd}? (`string`) Set the current working directory for
+ the sub-process.
+ • {env}? (`table<string,string|number>`) 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. Note: if `env` is `nil`, the current
+ environment is used but without `NVIM` set.
+ • {stdin}? (`string|string[]|true`) 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 stderr.
- 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) Run the command with a time limit.
- Upon timeout the process is sent the TERM signal (15) and
- the exit code is set to 124.
- • detach: (boolean) If true, spawn the child process in a
+ `write()` method to SystemObj. If `string` or `string[]`
+ then will be written to stdin and closed.
+ • {stdout}? (`fun(err:string?, data: string?)|boolean`,
+ default: `true`) Handle output from stdout.
+ • {stderr}? (`fun(err:string?, data: string?)|boolean`,
+ default: `true`) Handle output from stderr.
+ • {text}? (`boolean`) Handle stdout and stderr as text.
+ Normalizes line endings by replacing `\r\n` with `\n`.
+ • {timeout}? (`integer`) Run the command with a time limit
+ in ms. Upon timeout the process is sent the TERM signal
+ (15) and the exit code is set to 124.
+ • {detach}? (`boolean`) 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
@@ -1811,29 +1904,14 @@ vim.system({cmd}, {opts}, {on_exit}) *vim.system()*
unless the parent process calls |uv.unref()| on the
child's process handle.
• {on_exit} (`fun(out: vim.SystemCompleted)?`) Called when subprocess
- exits. When provided, the command runs asynchronously.
- Receives SystemCompleted object, see return of
- SystemObj:wait().
-
- Return: ~
- (`vim.SystemObj`) Object with the fields:
- • cmd (string[]) Command name and args
- • pid (integer) Process ID
- • wait (fun(timeout: integer|nil): SystemCompleted) Wait for the
- process to complete, including any open handles for background
- processes (e.g., `bash -c 'sleep 10 &'`). To avoid waiting for
- handles, set stdout=false and stderr=false. Upon timeout the process
- is sent the KILL signal (9) and the exit code is set to 124. Cannot
- be called in |api-fast|.
- • 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|string))
- • write (fun(data: string|nil)) Requires `stdin=true`. Pass `nil` to
- close the stream.
- • is_closing (fun(): boolean)
+ exits. When provided, the command runs asynchronously. See
+ return of SystemObj:wait().
+
+ Overloads: ~
+ • `fun(cmd: string, on_exit: fun(out: vim.SystemCompleted)): vim.SystemObj`
+
+ Return: ~
+ (`vim.SystemObj`) See |vim.SystemObj|.
==============================================================================
@@ -2827,7 +2905,8 @@ vim.ui.open({path}, {opt}) *vim.ui.open()*
• {cmd}? (`string[]`) Command used to open the path or URL.
Return (multiple): ~
- (`vim.SystemObj?`) Command object, or nil if not found.
+ (`vim.SystemObj?`) Command object, or nil if not found. See
+ |vim.SystemObj|.
(`string?`) Error message on failure, or nil on success.
See also: ~
diff --git a/runtime/lua/vim/_editor.lua b/runtime/lua/vim/_editor.lua
@@ -76,81 +76,6 @@ local utfs = {
['utf-32'] = true,
}
--- TODO(lewis6991): document that the signature is system({cmd}, [{opts},] {on_exit})
---- Runs a system command or throws an error if {cmd} cannot be run.
----
---- Examples:
----
---- ```lua
---- local on_exit = function(obj)
---- print(obj.code)
---- print(obj.signal)
---- print(obj.stdout)
---- print(obj.stderr)
---- end
----
---- -- Runs asynchronously:
---- vim.system({'echo', 'hello'}, { text = true }, on_exit)
----
---- -- Runs synchronously:
---- local obj = vim.system({'echo', 'hello'}, { text = true }):wait()
---- -- { code = 0, signal = 0, stdout = 'hello\n', stderr = '' }
----
---- ```
----
---- See |uv.spawn()| for more details. Note: unlike |uv.spawn()|, vim.system
---- throws an error if {cmd} cannot be run.
----
---- @param cmd (string[]) Command to execute
---- @param opts vim.SystemOpts? 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. Note: if `env` is `nil`, the current environment is used but without `NVIM` set.
---- - 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 stderr. 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) Run the command with a time limit. Upon timeout the process is sent the
---- TERM signal (15) and the exit code is set to 124.
---- - 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? fun(out: vim.SystemCompleted) Called when subprocess exits. When provided, the command runs
---- asynchronously. Receives SystemCompleted object, see return of SystemObj:wait().
----
---- @return vim.SystemObj Object with the fields:
---- - cmd (string[]) Command name and args
---- - pid (integer) Process ID
---- - wait (fun(timeout: integer|nil): SystemCompleted) Wait for the process to complete,
---- including any open handles for background processes (e.g., `bash -c 'sleep 10 &'`).
---- To avoid waiting for handles, set stdout=false and stderr=false. Upon timeout the process is
---- sent the KILL signal (9) and the exit code is set to 124. Cannot be called in |api-fast|.
---- - 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|string))
---- - 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.
-- Used by nvim_get_proc() as a fallback.
function vim._os_proc_info(pid)
diff --git a/runtime/lua/vim/_init_packages.lua b/runtime/lua/vim/_init_packages.lua
@@ -96,4 +96,5 @@ end
-- only on main thread: functions for interacting with editor state
if vim.api and not vim.is_thread() then
require('vim._editor')
+ require('vim._system')
end
diff --git a/runtime/lua/vim/_system.lua b/runtime/lua/vim/_system.lua
@@ -1,23 +1,51 @@
local uv = vim.uv
--- @class vim.SystemOpts
---- @field stdin? string|string[]|true
---- @field stdout? fun(err:string?, data: string?)|false
---- @field stderr? fun(err:string?, data: string?)|false
+--- @inlinedoc
+---
+--- Set the current working directory for the sub-process.
--- @field cwd? string
+---
+--- Set environment variables for the new process. Inherits the current environment with `NVIM` set
+--- to |v:servername|.
--- @field env? table<string,string|number>
+---
+--- `env` defines the job environment exactly, instead of merging current environment. Note: if
+--- `env` is `nil`, the current environment is used but without `NVIM` set.
--- @field clear_env? 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.
+--- @field stdin? string|string[]|true
+---
+--- Handle output from stdout.
+--- (Default: `true`)
+--- @field stdout? fun(err:string?, data: string?)|boolean
+---
+--- Handle output from stderr.
+--- (Default: `true`)
+--- @field stderr? fun(err:string?, data: string?)|boolean
+---
+--- Handle stdout and stderr as text. Normalizes line endings by replacing `\r\n` with `\n`.
--- @field text? boolean
---- @field timeout? integer Timeout in ms
+---
+--- Run the command with a time limit in ms. Upon timeout the process is sent the TERM signal (15)
+--- and the exit code is set to 124.
+--- @field timeout? integer
+---
+--- 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.
--- @field detach? boolean
--- @class vim.SystemCompleted
--- @field code integer
--- @field signal integer
---- @field stdout? string
---- @field stderr? string
+--- @field stdout? string `nil` if stdout is disabled or has a custom handler.
+--- @field stderr? string `nil` if stderr is disabled or has a custom handler.
---- @class vim.SystemState
+--- @class (package) vim.SystemState
--- @field cmd string[]
--- @field handle? uv.uv_process_t
--- @field timer? uv.uv_timer_t
@@ -48,13 +76,9 @@ local function close_handle(handle)
end
--- @class vim.SystemObj
---- @field cmd string[]
---- @field pid integer
+--- @field cmd string[] Command name and args
+--- @field pid integer Process ID
--- @field private _state vim.SystemState
---- @field wait fun(self: vim.SystemObj, timeout?: integer): vim.SystemCompleted
---- @field kill fun(self: vim.SystemObj, signal: integer|string)
---- @field write fun(self: vim.SystemObj, data?: string|string[])
---- @field is_closing fun(self: vim.SystemObj): boolean
local SystemObj = {}
--- @param state vim.SystemState
@@ -67,7 +91,17 @@ local function new_systemobj(state)
}, { __index = SystemObj })
end
---- @param signal integer|string
+--- Sends a signal to the process.
+---
+--- The signal can be specified as an integer or as a string.
+---
+--- Example:
+--- ```lua
+--- local obj = vim.system({'sleep', '10'})
+--- obj:kill('TERM') -- sends SIGTERM to the process
+--- ```
+---
+--- @param signal integer|string Signal to send to the process.
function SystemObj:kill(signal)
self._state.handle:kill(signal)
end
@@ -79,6 +113,23 @@ function SystemObj:_timeout(signal)
self:kill(signal or SIG.TERM)
end
+--- Waits for the process to complete or until the specified timeout elapses.
+---
+--- This method blocks execution until the associated process has exited or
+--- the optional `timeout` (in milliseconds) has been reached. If the process
+--- does not exit before the timeout, it is forcefully terminated with SIGKILL
+--- (signal 9), and the exit code is set to 124.
+---
+--- If no `timeout` is provided, the method will wait indefinitely (or use the
+--- timeout specified in the options when the process was started).
+---
+--- Example:
+--- ```lua
+--- local obj = vim.system({'echo', 'hello'}, { text = true })
+--- local result = obj:wait(1000) -- waits up to 1000ms
+--- print(result.code, result.signal, result.stdout, result.stderr)
+--- ```
+---
--- @param timeout? integer
--- @return vim.SystemCompleted
function SystemObj:wait(timeout)
@@ -99,6 +150,23 @@ function SystemObj:wait(timeout)
return state.result
end
+--- Writes data to the stdin of the process or closes stdin.
+---
+--- If `data` is a list of strings, each string is written followed by a
+--- newline.
+---
+--- If `data` is a string, it is written as-is.
+---
+--- If `data` is `nil`, the write side of the stream is shut down and the pipe
+--- is closed.
+---
+--- Example:
+--- ```lua
+--- local obj = vim.system({'cat'}, { stdin = true })
+--- obj:write({'hello', 'world'}) -- writes 'hello\nworld\n' to stdin
+--- obj:write(nil) -- closes stdin
+--- ```
+---
--- @param data string[]|string|nil
function SystemObj:write(data)
local stdin = self._state.stdin
@@ -127,6 +195,12 @@ function SystemObj:write(data)
end
end
+--- Checks if the process handle is closing or already closed.
+---
+--- This method returns `true` if the underlying process handle is either
+--- `nil` or is in the process of closing. It is useful for determining
+--- whether it is safe to perform operations on the process handle.
+---
--- @return boolean
function SystemObj:is_closing()
local handle = self._state.handle
@@ -228,8 +302,6 @@ end
local is_win = vim.fn.has('win32') == 1
-local M = {}
-
--- @param cmd string
--- @param opts uv.spawn.options
--- @param on_exit fun(code: integer, signal: integer)
@@ -282,7 +354,7 @@ local function _on_exit(state, code, signal, on_exit)
-- #30846: Do not close stdout/stderr here, as they may still have data to
-- read. They will be closed in uv.read_start on EOF.
- local check = assert(uv.new_check())
+ local check = uv.new_check()
check:start(function()
for _, pipe in pairs({ state.stdin, state.stdout, state.stderr }) do
if not pipe:is_closing() then
@@ -333,7 +405,7 @@ end
--- @param opts? vim.SystemOpts
--- @param on_exit? fun(out: vim.SystemCompleted)
--- @return vim.SystemObj
-function M.run(cmd, opts, on_exit)
+local function run(cmd, opts, on_exit)
vim.validate('cmd', cmd, 'table')
vim.validate('opts', opts, 'table', true)
vim.validate('on_exit', on_exit, 'function', true)
@@ -397,4 +469,41 @@ function M.run(cmd, opts, on_exit)
return obj
end
-return M
+--- Runs a system command or throws an error if {cmd} cannot be run.
+---
+--- Examples:
+---
+--- ```lua
+--- local on_exit = function(obj)
+--- print(obj.code)
+--- print(obj.signal)
+--- print(obj.stdout)
+--- print(obj.stderr)
+--- end
+---
+--- -- Runs asynchronously:
+--- vim.system({'echo', 'hello'}, { text = true }, on_exit)
+---
+--- -- Runs synchronously:
+--- local obj = vim.system({'echo', 'hello'}, { text = true }):wait()
+--- -- { code = 0, signal = 0, stdout = 'hello\n', stderr = '' }
+---
+--- ```
+---
+--- See |uv.spawn()| for more details. Note: unlike |uv.spawn()|, vim.system
+--- throws an error if {cmd} cannot be run.
+---
+--- @param cmd string[] Command to execute
+--- @param opts vim.SystemOpts?
+--- @param on_exit? fun(out: vim.SystemCompleted) Called when subprocess exits. When provided, the command runs
+--- asynchronously. See return of SystemObj:wait().
+---
+--- @return vim.SystemObj
+--- @overload fun(cmd: string, on_exit: fun(out: vim.SystemCompleted)): vim.SystemObj
+function vim.system(cmd, opts, on_exit)
+ if type(opts) == 'function' then
+ on_exit = opts
+ opts = nil
+ end
+ return run(cmd, opts, on_exit)
+end
diff --git a/src/gen/gen_steps.zig b/src/gen/gen_steps.zig
@@ -75,6 +75,7 @@ pub fn nvim_gen_sources(
"_init_packages",
"inspect",
"_editor",
+ "_system",
"filetype",
"fs",
"F",
diff --git a/src/gen/gen_vimdoc.lua b/src/gen/gen_vimdoc.lua
@@ -141,6 +141,7 @@ local config = {
'builtin.lua',
'_options.lua',
'_editor.lua',
+ '_system.lua',
'_inspector.lua',
'shared.lua',
'loader.lua',
@@ -172,6 +173,7 @@ local config = {
'runtime/lua/vim/uri.lua',
'runtime/lua/vim/ui.lua',
'runtime/lua/vim/_extui.lua',
+ 'runtime/lua/vim/_system.lua',
'runtime/lua/vim/filetype.lua',
'runtime/lua/vim/keymap.lua',
'runtime/lua/vim/fs.lua',
@@ -215,6 +217,8 @@ local config = {
name = name:lower()
if name == '_editor' then
return 'Lua module: vim'
+ elseif name == '_system' then
+ return 'Lua module: vim.system'
elseif name == '_options' then
return 'LUA-VIMSCRIPT BRIDGE'
elseif name == 'builtin' then
@@ -243,6 +247,8 @@ local config = {
helptag_fmt = function(name)
if name == '_editor' then
return 'lua-vim'
+ elseif name == '_system' then
+ return 'lua-vim-system'
elseif name == '_options' then
return 'lua-vimscript'
elseif name == 'tohtml' then
diff --git a/src/nvim/CMakeLists.txt b/src/nvim/CMakeLists.txt
@@ -335,6 +335,7 @@ set(VIM_MODULE_FILE ${GENERATED_DIR}/lua/vim_module.generated.h)
# NVIM_RUNTIME_DIR
set(LUA_DEFAULTS_MODULE_SOURCE ${NVIM_RUNTIME_DIR}/lua/vim/_defaults.lua)
set(LUA_EDITOR_MODULE_SOURCE ${NVIM_RUNTIME_DIR}/lua/vim/_editor.lua)
+set(LUA_SYSTEM_MODULE_SOURCE ${NVIM_RUNTIME_DIR}/lua/vim/_system.lua)
set(LUA_FILETYPE_MODULE_SOURCE ${NVIM_RUNTIME_DIR}/lua/vim/filetype.lua)
set(LUA_FS_MODULE_SOURCE ${NVIM_RUNTIME_DIR}/lua/vim/fs.lua)
set(LUA_F_MODULE_SOURCE ${NVIM_RUNTIME_DIR}/lua/vim/F.lua)
@@ -626,6 +627,7 @@ add_custom_command(
${LUA_INIT_PACKAGES_MODULE_SOURCE} "vim._init_packages"
${LUA_INSPECT_MODULE_SOURCE} "vim.inspect"
${LUA_EDITOR_MODULE_SOURCE} "vim._editor"
+ ${LUA_SYSTEM_MODULE_SOURCE} "vim._system"
${LUA_FILETYPE_MODULE_SOURCE} "vim.filetype"
${LUA_FS_MODULE_SOURCE} "vim.fs"
${LUA_F_MODULE_SOURCE} "vim.F"
@@ -640,6 +642,7 @@ add_custom_command(
${LUA_INIT_PACKAGES_MODULE_SOURCE}
${LUA_INSPECT_MODULE_SOURCE}
${LUA_EDITOR_MODULE_SOURCE}
+ ${LUA_SYSTEM_MODULE_SOURCE}
${LUA_FILETYPE_MODULE_SOURCE}
${LUA_FS_MODULE_SOURCE}
${LUA_F_MODULE_SOURCE}