• :%!
  • exe "!..."
  • exe "!bash -c" shellescape(...)

I'm getting quite confused as to what exactly Vim invokes. And I suspect at some point it invokes bash -c bash .... Is there a way to see exactly what shell command is being called out? Does Vim ever invoke directly the command after !, or does it always wrap the command in bash -c ...?

I'm really trying to invoke a shell command, stored in g:cmd, which has a % in it, with as little overhead as possible (i.e., no bash -c bash). It's supposed to act as a filter on the visual selection. But

exe "'<,'>!" g:cmd  # Substitutes '%'
exe "'<,'>! bash -c" shellescape( g:cmd, 1 )  # Maybe wraps 2x bash around
exe "'<,'>!" shellescape( g:cmd, 1/0 )  # Bash calls g:cmd as executable -> error
  • 3
    You know you can write a wrapper and set 'shell' to be that. It can be a script that does whatever you want...prints the parameters passed to it, for instance.
    – B Layer
    Apr 30, 2021 at 9:57
  • Aha, that's the answer to the debugging question
    – usretc
    Apr 30, 2021 at 9:59

3 Answers 3


When you use ! or :! Vim builds the invoking command based on whatever is specified in the various 'shell...' settings (i.e. 'shell', 'shellcmdflag', 'shellquote', to name three). On Unix systems the defaults for the first two are usually the default shell ($SHELL) and -c so if I'm using Bash and I do :!foo bar the call Vim makes will look like:

/bin/bash -c "foo bar"

So, to answer your question: No, Vim never directly calls the command after !.

The above answers the one concrete question I saw in the question's text but as we sussed out in the comments there was also a question about how to debug things in this situation. For the benefit of other readers I'll repeat here what I suggested:

A good way to get insight into what exactly gets passed by Vim to the 'shell' program is to write your own "shell". By which I mean write a wrapper script which contains whatever you want. The most obvious thing to do is just have it print all the params/args received. Now set 'shell' to point to that instead of /bin/bash or whatever it's currently pointing to, make a :! call and start debugging.


Is there a way to see exactly what shell command is being called out?

To see what exactly is run,

:echo getpid()

will show vim's PID, e.g. 1234, with which you can then in another terminal run:

sudo strace -fe trace=execve -p 1234

then in vim you can use any shell-invoking command like:

:!echo foo

and in the terminal running strace see something like:

[pid 1737876] execve("/usr/bin/zsh", ["zsh", "-c", "echo foo"], 0x561845a10070 /* 48 vars */) = 0

zsh -c is used because I have :set shell=zsh, but that's exactly what's being run. You can use this to see how vim processes characters like %, #, \, or |.

For example,

:!echo \%\|

results in

[pid 1747484] execve("/usr/bin/zsh", ["zsh", "-c", "echo %\\|"], 0x561845a10070 /* 48 vars */) = 0

strace might show much more output than you might be interested in. You can use grep to filter what you want, but note that the output is through stderr instead of stdout, so instead of a simple |, you should use something like |&. e.g.

sudo strace -fe trace=execve -p 1234 |& grep echo

This works for stuff beyond vim.

  • A bit overkill, but it's a nice one! I believe you typically don't need sudo for this, but that actually might depend on some settings in /proc and might depend on which O.S. you're in... Would you try it without sudo to see if it still works as expected?
    – filbranden
    Apr 30, 2021 at 23:26
  • @filbranden You don't need sudo when having strace start a new process, but you do need it to attach to an existing one. I get strace: attach: ptrace(PTRACE_SEIZE, 1791897): Operation not permitted.
    – JoL
    Apr 30, 2021 at 23:41
  • There's also ltrace -fp, though I'm not getting anything useful in a quick test
    – usretc
    May 1, 2021 at 1:10
  • strace has -o, which could probably be made a fifo if necessary. Or I'm sure it's possible to attach gdb. But we're getting really overkill
    – usretc
    May 1, 2021 at 1:12
  • @usretc Yes, there's different ways to avoid using sudo, but I figured that the process you're trying to debug could include important context that's not simple to replicate in a new vim instance. That's why being able to attach to a pre-existing process rather than invoking a new one seemed more valuable from a debugging perspective. I didn't think people would have many qualms of running strace with root privileges.
    – JoL
    May 1, 2021 at 20:56

An easy check is :!foo that (probably) will print something like bash: line 1: foo: command not found. Also see :h job_start() (or :h jobstart() for Neovim) and so on.

To summarize:

  • :! always uses &shell (as well as &shellcmdflag, &shellquote, &shellxqoute and so on);
  • :h system() and :h systemlist() use &shell in Vim; in Neovim it depends on the first parameter (String = shell, List = direct);
  • :h job_start() executes directly (Vim specific)
  • :h jobstart() works like system(), i.e. String = shell, List = direct (Neovim specific)
  • :h :term in Vim executes command directly (&shell is the default);
  • :h :term in Neovim uses &shell (so :term sh results in two shells running one inside another).

Your Answer

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

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