4

I'm trying to filter specific lines through external command. I think knowing how to do this would be extremely beneficial to me, so I'm trying to find a way to do this. As an example, I wanted to count the occurrence of a specific word or line in a file. I was trying to do this with a filter, using the following command: :g/http/w !wc -l

However, this ran w !wc -l for every line matching the file, so I got the total number of lines in the file, a number of times equal to the number of matches.

What I was expecting, was something akin to this line: :%!grep http | wc -l

Is something like this possible in vim using filters or commands? If not, I'm fine using the grep wc solution, but it would be nice having a clean way to do this from vim.

I've also noticed that :g/http/p prints all the matching lines, but I don't have the option of piping the output to an external command using the print command. Is there anything else I could use?

4 Answers 4

3

but I don't have the option of piping the output to an external command

Actually you do:

echo system('wc -l', execute('g/http/'))

However, note that execute() prepends a newline to the resulting string, while not appending a newline after it. Hence the result of wc -l remains the same, but it could matter under different circumstances.

4
  • Thank you for your answer, this is what I'm looking for. However, this has a lot of extra boilerplate. Is there a shorter way of doing this? If not, I think I'll stick to the grep and other external solutions.
    – Dafta
    Feb 11, 2020 at 21:27
  • @Dafta You may also want to read this answer.
    – Matt
    Feb 12, 2020 at 5:09
  • Thanks, that answer helps a lot. It's not perfect, but combined with the answer @B Layer gave above, I think I can come up with a good solution for this.
    – Dafta
    Feb 12, 2020 at 20:48
  • Thanks again for the help. I've written a function that does exactly what I need and posted it as an answer.
    – Dafta
    Feb 13, 2020 at 14:22
3

I'm not aware of any builtin, compact functionality that let's you filter the output of any vim command through any external command. With a little one-time scripting, though, you could get something like this...

:MyFilt g/http/ | wc -l

To start, put this in your vimrc...

func! MyFilt(cmd)
    let parts = split(a:cmd, '|')
    let vimcmd = trim(parts[0])
    let shcmd = trim(parts[1])
    echom trim(system(shcmd, execute(vimcmd)))
endfunc

(The base command here is pretty much what Matt showed in his answer plus a bunch of junk trimming.)

We could use this like...

:call MyFilt('g/http/ | wc -l')

But we can distill it down further using a :command (also goes in your vimrc)...

:command -nargs=1 MyFilt call MyFilt(<f-args>)

And now your call can be of the form :MyFilt <vim-command> | <sh-filter> as show above...

:MyFilt g/http/ | wc -l

(The uniqueness of a one-arg :command's param processing is to thank here. It just takes everything after the command name and passes it as a single string param to the designated function.)

5
  • Thank you, this helps a lot. It's not perfect, but I'll try and create a better solution using this and the answer that @Matt linked in the comment to his answer. If I come up with something good I'll add it as an answer here.
    – Dafta
    Feb 12, 2020 at 20:47
  • The thing I'm fighting with at the current moment is that vim uses null bytes instead of newlines and translates that to newlines under certain circumstances, and that the lines are output with line numbers. So if I have a file with a bunch of lines containing just the letter g, and I do :MyFilt g/g/ | cat -, the output is this: ^@ 2 g^@ 4 g^@ 5 g^@ 7 g^@ 9 g^@ 11 g^@ 13 g. Do you know how I can replace the null bytes with newlines and how to remove the line numbers? This is the only thing that's wrong with the current solution.
    – Dafta
    Feb 12, 2020 at 21:37
  • I got it to work by using substitute to replace the null bytes with a newline, and the line numbers were caused by me setting the number option, so temporarily unsetting the option in the function works. I'd still like to hear if you've got a better solution than substitute for the null byte replacement.
    – Dafta
    Feb 12, 2020 at 22:21
  • That's the nice thing about using a function like this; you can shape the data however you'd like and in one location. Offhand, I don't have a better solution to the null bytes, substitution sounds reasonable, but I'll play with it and let you know if something occurs to me.
    – B Layer
    Feb 12, 2020 at 22:52
  • Thanks for your help. I've written a function and posted it as an answer to this question for future reference.
    – Dafta
    Feb 13, 2020 at 14:21
1

You can use :%s/pattern/gn The n tells it to count the g to count every occurrence.

1
  • This works for the example I've given, but like I said, I'm not looking for a specific solution to this answer but a general solution for piping output of vim commands to external commands.
    – Dafta
    Feb 11, 2020 at 21:27
1

Thanks to @B Layers and @Matts answers, I've written this function which serves my purposes perfectly. I'm sharing it here for future reference, if anyone needs the same functionality.

" Function to pipe output of vim command to external command.
" The 'trim' argument determines whether the vim command
" output will be trimmed before piping it to the external
" command. The 'cmd' argument is the full command, including
" the vim command. The vim command is separated from the
" external command with a '|' character, and everything from
" the first '|' character to the end of the line is considered
" the external command, so external pipes work as well.
function! Pipe(trim, cmd)
    " Split the command
    let l:cmds = split(a:cmd, '|')

    " Save the state of the number and relativenumber option
    let l:number = &number
    let l:relativenumber = &relativenumber

    " Disable the two options, so that the vim output doesn't have line numbers
    setlocal nonumber norelativenumber

    " Run the vim command
    let l:vimresult = execute(trim(l:cmds[0]))

    " Restore the two saved options
    let &l:number = l:number
    let &l:relativenumber = l:relativenumber

    " Trim the vim command if necessary
    if a:trim
        let l:vimresult = trim(l:vimresult)
    endif

    " If there was no external command, return the output of the vim command
    if len(l:cmds) <= 1
        return l:vimresult
    endif

    " Run the external command and return the output
    return trim(system(join(l:cmds[1:], '|'), l:vimresult))
endfunction

" Define a command called Pipe for calling the Pipe() function
command! -nargs=1 -bang -complete=command Pipe echo Pipe(<bang>1, <f-args>)

Using it is simple, just do this:

:Pipe[!] g/test/ | wc -l

If the bang is present, the function won't trim the vim command output, which is, for example, needed for wc to correctly output the number of lines, since it counts all the newlines found in the text.

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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