home ~ projects ~ socials

Run An External Command Synchronously In Neovim

Getting A Line Of Output

This is how I pull text from external commands to use in Neovim:

local print_response = function()
    local response = vim.system(
        {'echo', 'hello'}, 
        { text = true }):wait()
    local output = response['stdout']:gsub(
        "\n", ""
    )
    print(output)
end

The process is synchronous (i.e. it blocks you from being able to interact with Neovim while it runs). I only use it for commands that return quickly and/or commands that do something that I need to happen before continuing to work on a file.

The key for getting this to work like expected is stripping the newline (\n) character. Some functions break without that (e.g. if you try to use vim.api.nvim_buf_set_lines without stripping the newline it'll throw an error).

The response Table

The value of response in the example above is a table that looks like this:

{ 
    code = 0, 
    signal = 0, 
    stdout = 'hello', 
    stderr = '' 
}

Outputting A Single Line To A Buffer

You can use this approach to write a single line to the end of the current buffer with:

function output_line_to_buffer()
    local bufnr = 0
    local start_line = -1
    local end_line = -1
    local strict_bounds = false
    local response = vim.system(
        {'echo', 'hello world'}, 
        { text = true }):wait()
    local output = response['stdout']:gsub(
        "\n", 
        ""
    )
    local lines =  { output } 
    vim.api.nvim_buf_set_lines(
        bufnr, 
        start_line, 
        end_line, 
        strict_bounds, 
        lines
    )
end

Don't Do This

Writing to a buffer fails if you don't string the \n newline character. For example, this doesn't work:

local response = vim.system(
    {'echo', 'hello world'}, 
    { text = true }):wait()

local bufnr = 0
local start_line = 0
local end_line = -1
local strict_bounds = false
local lines =  { response['stdout'] } 

vim.api.nvim_buf_set_lines(
    bufnr, start_line, end_line, strict_bounds, lines
)

You'll get an error message that contains something like:

'replacement string' item contains newlines

Output Multiple Lines To A Buffer

In order to output multiple lines the response from the command has to be split into a table of individual lines. This can be done by using gmatch to look for newlines. For example, this outputs multiple lines from ls -la to the current buffer:

function output_multiple_lines_to_buffer()
    local bufnr = 0
    local start_line = -1
    local end_line = -1
    local strict_bounds = false
    local response = vim.system(
        {'ls', '-la'}, 
        { text = true }):wait()
    local lines =  {} 
    for line in response['stdout']:gmatch '[^\n]+' do
        table.insert(lines, line)
    end
    vim.api.nvim_buf_set_lines(
        bufnr, 
        start_line, 
        end_line, 
        strict_bounds, 
        lines
    )
end
-- end of line --

Endnotes

This took forever to figure out. Like, hours. I was shocked that it took so long to find a clear example.

Hopefully, this will help other folks who want to pull stuff from external commands.

The \n newline stripping I'm doing works on my Mac. It should work on most linux installs too.

For windows, I think you'd need to do something like \r\n, but I'm not sure. You'll have to figure that out.

References