I'm writing a function to execute an external command using visual selection. I've noticed that if by mistake the entire line is selected, including the new line char the extra ^@ character makes the shell command to fail:

Error detected while processing function <SNR>20_BitwiseOperator:
line   18:
E485: Can't read file /tmp/v5rPHeq/9

I know I can just trim the last character if it matches ^@, but I'm wondering if I am missing something.

My BitwiseOperation function looks this:

function! s:BitwiseOperator(type)
  let saved_unnamed_register = @@
  if a:type ==# 'v'
        normal! `<v`>y
    elseif a:type ==# 'char'
        normal! `[v`]y

  botright new
  setlocal buftype=nofile bufhidden=wipe nobuflisted noswapfile nowrap
  silent execute '$read !'. "bitwise --no-color ". shellescape(@@)
  setlocal nomodifiable
  let @@ = saved_unnamed_register

The ^@ you're referring to is actually a newline character, so the problem you describe will happen when your visual selection includes the newline (e.g. v$ to select until the end of the line) or when it spawns multiple lines.

Assuming your external tool you're shelling out to is able to handle multiline strings appropriately, Vim's shellescape() will be able to handle it in a way that the shell will interpret it correctly, by turning it into a single quoted string that can embed newlines.

So multiline\n becomes 'multiline\n' through shellescape() (where the \n here represents an actual newline character), and the shell is fine with it.

The problem you're having is that :execute is now seeing two separate commands, in this case:

$read !bitwise --no-color 'multiline



Or, more geneally, for first line\nsecond line:

$read !bitwise --no-color 'first line
second line'

There are two possible solutions to this problem. The first is to pass a non-zero second argument to shellescape(), which will make it do some extra magic to handle cases like this one with the newline. From :help shellescape():

With a non-zero-arg {special} the <NL> character is also escaped. [...]

Example of use with a :! command:

:exe '!dir ' . shellescape(expand('<cfile>'), 1)

The example itself is using : execute, so it's actually a good illustration of your issue.

So one possible solution is:

silent execute '$read !'. "bitwise --no-color ". shellescape(@@, 1)

Another, actually better solution, is to use the system() function to shell out instead, in which case you don't need to worry about escaping the string from :execute.

In order to put the contents of the system() result into the new buffer, you can use the :put command together with the @= expression register.

Putting it all together:

silent put =system('bitwise --no-color '.shellescape(@@))

You can also use setline() instead of :put, which makes it even easier to get rid of the empty line at the start of the buffer. With setline(), use systemlist(), to get the shell output as a list of lines:

call setline(1, systemlist('bitwise --no-color '.shellescape(@@)))
  • 1
    Thanks for the answer. shellescape(@@, 1) just introduces an escape `` character before the ^@
    – stdcall
    Jul 28 '19 at 18:15
  • It actually adds a backslash before the newline... It works, but the other solutions using system() (or systemlist()) are better, since you don't need that extra level of escaping...
    – filbranden
    Jul 28 '19 at 18:21

In the end, I ended up trimming the newline charachter using search/replace.

substitute(a:string, '\n\+$', '', '')

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.