1

Summary

In [Neo]Vim, if I call an external command on the current visual selection (expecting that command to transform the selected text, which Vim then updates in the buffer), and the command fails with a nonzero exit value, then the error message on the command's stderr isn't displayed. How can I get it to display automatically?

Background

It's easy to manually make a visual selection, then call an external command and have Vim use the stdout from that to replace the selected text.

xnoremap <Leader>d :!mycommand<CR>

This defines a key mapping, so that pressing leader key then 'd' runs external script 'mycommand' on the visual selection, as described above. The use of 'xnoremap', as opposed to other '-remap' commands, means this only happens when a visual selection currently exists, automatically passing the selected text to the command, etc.

Problem

But if mycommand fails (for example, perhaps the text in the visual selection is not formatted as mycommand expects), then mycommand will print nothing on stdout, an error message on stderr, and give a nonzero exit value. In this case, my Neovim correctly leaves the buffer unmodified, but does not display the error message. Instead, it just shows:

:'<,'>!mycommand                                                                                        
shell returned 1

Current kludgy workaround

I've fallen back on what seems like a bit of a kludge:

function! s:MyFunc() range
    silent '<,'>!mycommand 2>/tmp/vim.err
    if v:shell_error
        echo join(readfile("/tmp/vim.err"), "\n")
    endif
endfunction

xnoremap <Leader>d :call <sid>MyFunc()<CR>

No doubt there are more sensible temp storage locations I could use (proper tempfiles, variables, registers, etc?)

But, is this even necessary? Am I going to have to do it for every external program I call as a filter while wanting to see the error message? Am I going to end up generalizing the above function to operate on arbitrary external commands, and arbitrary ranges?

This seems like such a predictable and common need that I feel as though I must be missing something obvious that would automatically do this for me, but I haven't figure it from my web searches, other stackoverflow answers, nor the Vim docs around "help !".

Jonathan Hartley
  • 1,006
  • 13
  • 25

2 Answers2

1

The use of 'xnoremap', as opposed to other '-remap' commands, means this only happens when a visual selection currently exists, automatically passing the selected text to the command, etc.

Note that there is no such thing as "'-remap' commands". All you have are "'-map' commands", with xnoremap being read like this:

 |    |
+|----|------- visual mode
 |++++|------- non-recursive
 |    |+++---- mapping
x|nore|map
 |    |

The re is part of nore, not of an imaginary remap.

Now, maybe there is a difference between Vim and Neovim regarding error handling, because Vim has always replaced the filtered lines with both stdout and stderr, which, I agree, is far from elegant:

example

  • First attempt replaces the text with the "nothing" that came out of stdout and stderr, therefore effectively only with stderr,
  • second attempt suppresses stderr and replaces the text with the "nothing" that came out of stdout,
  • third attempt replaces the text with both stdout and stderr.

In Vim, the usual simplistic workaround is to let the filter do its thing and, if there is an error (as par v:shell_error), undo and handle the error how one sees fit.

A slightly better approach, still in Vim, would be to:

  1. write the current buffer to a temporary file,
  2. run your external command on that temporary file,
  3. if there is an error, handle it
  4. decide whether the lines of the buffer should be replaced or not.
function! Filter() range
    " generate a temporary file name
    let tempfile = tempname()

    " write the lines in the given range to that temporary file
    call getline(a:firstline, a:lastline)->writefile(tempfile)

    " pass the temporary file to the external command and grab the output
    let output = systemlist('jskdfsdsg ' .. tempfile)

    if v:shell_error
        " if there is an error, prit the error and leave the buffer alone
        echomsg v:shell_error
    else
        " if there is none, delete the given range
        execute a:firstline .. ',' .. a:lastline .. 'd_'

        " and replace it with the output of the external command
        execute a:firstline - 1 .. 'put=output'
    endif
endfunction

See :help tempname(), :help getline(), :help writefile(), :help systemlist(), :help :d, :help registers, and :help :put.

romainl
  • 22,554
  • 2
  • 49
  • 59
  • This is very useful, thanks for posting. It both confirms that I'm not *totally* crazy (ie. there is a real problem here without a trivial solution) and it demonstrates the details of a real solution so I don't have to iterate my way to one from scratch. Thank you! – Jonathan Hartley May 15 '23 at 18:44
  • As far as I'm aware, everything you've said is also correct in NeoVim. I'll report back for future readers if I find that's wrong. – Jonathan Hartley May 17 '23 at 15:25
0

I found the following snippet in my config, which seems to partially solve the problem, but I don't remember where it came from:

augroup FILTER_ERROR
    au!
    autocmd ShellFilterPost * if v:shell_error | silent undo | endif
augroup END

So at least any external commands which fail won't clobber the text in the buffer.

Jonathan Hartley
  • 1,006
  • 13
  • 25