Vim Tips Wiki
Advertisement
Tip 1549 Printable Monobook Previous Next

created March 20, 2008 · complexity basic · author Don Reba · version 7.0


Under Windows, Vim provides two commands for running external programs: ! and !start. The first suspends Vim, runs the program, waits for a keypress, then returns to Vim. The second runs the program without suspending Vim or waiting for a keypress.

The first method has the drawback of not letting you work while the program is running, but with the second you do not see the result of the program's execution. Here is how to get the best of both worlds:

:!start cmd /c "my app.exe" && pause

The following is an alternative that will leave the command window open until you close it manually:

:!start cmd /k "my app.exe"

Using this knowledge, the following mapping allows you to press F5 to run the compiled program corresponding to the current source file:

:nnoremap <silent> <buffer> <F5> :!start cmd /c "%<.exe" && pause<CR>

Explanation

Under Windows, if the Vim command begins with "!start ", Vim creates a process to run the program without invoking a shell, so the "Hit any key to close this window..." message does not appear. :help :!start :help win32-vimrun

This tip assumes that cmd.exe is your shell (Windows command prompt :help 'shell'), and that you are running a console program (not GUI). Most console programs (and batch scripts) will not pause at the end of the output, so just calling the command by itself with :!start does not work. But, Vim's :!start command does not accept multiple commands, and additionally does not accept built-in shell commands like pause or echo which are not executables or scripts (you will get a "command not found" error). To work around these limitations, we invoke the shell directly and tell it to run the command we pass it, in this case the command sequence "my app.exe" && pause.

The alternate method works in the same way, but uses the /k cmd.exe switch, which executes a command and then leaves the window open, as opposed to the /c switch, which runs a command and exits.

GUI applications do not need to use this method. For example, if you enter a command like :!calc (to run calc.exe for the Windows calculator), Vim will open a shell window, then launch a new calc window from that shell. When calc is closed, the prompt Hit any key to close this window... appears, and Vim is frozen until a key is pressed. But Calculator is a GUI program so it runs in a new process, in a new window. So, if you want to asynchronously run a GUI program like Calculator, you can simply enter:

:!start calc

!start vs. Windows start command

The following mapping launches the currently edited file with the external application that is associated with it, using the file type's default handler. This is like entering the filename in the Start > Run... dialog, or double-clicking in Windows Explorer. For example, example.html will be opened in your default browser.

:nmap <Leader>x :silent ! start "1" "%:p"<CR>

Explanation

In the Vim command, ':silent' avoids the Hit-Enter prompt, ':p' makes this launch independent from the CWD, the surrounding "" make it handle spaces. The Windows start command opens the passed file in a separate window, so that Vim doesn't block; the Windows shell locates the application that is associated with the type of file. The "1" is the optional "title" argument to 'start'; without this dummy, 'start' will take the passed file as the title, because it is enclosed in double quotes.

Getting results back into Vim

In many cases, you may want to run a process in a shell, but then do something with the results in Vim. Normally you could just do this with the system() function or by reading in the output with :r!. But, if you want to do this while continuing to use Vim, you can use the methods from above to launch the process asynchonously, and then use Vim's clientserver features to send the results back into Vim. For example, here is a simple "hello world" sort of command, that will just echo what is passed into it, but do so by first sending it through an asynchronous shell.

command! -nargs=+ AsyncHello call SendAsyncText(<q-args>)

function! SendAsyncText(hello_text)
  let temp_file = tempname()
  exec 'silent !start cmd /c "echo '.a:hello_text.' > '.temp_file
        \ ' && vim --servername '.v:servername.' --remote-expr "GetAsyncText('."'".temp_file."')\"".
        \ ' && pause"'
endfunction

function! GetAsyncText(temp_file_name)
  echomsg readfile(a:temp_file_name)[0]
  call delete(a:temp_file_name)
endfunction

Explanation

This command and associated functions works by invoking an asynchronous external process that writes to a temporary file that is then passed back to Vim. If you passed the text, "hello Vim" into AsyncHello, and the current Vim server is named GVIM3, the command that is actually executed in the shell window is:

  echo hello Vim > some_temporary_file_name && vim --servername GVIM3 --remote-expr "GetAsyncText('some_temporary_file_name')" && pause

Note how our use of use of --servername v:servername ensures that the results will be sent back to the correct Vim. We use --remote-expr to make it easy to write and invoke a function to process the results of the command in any way we desire. In our case it is a simple readfile call, but it could be pretty much anything, including use of a command like cgetfile to run an asynchronous search, for example. The basic idea is the same, however: call a shell command that writes to a known file name before invoking a Vim function on the results.

You may have realized that the pause is unnecessary in this case, but it serves to demonstrate the fact that the newly spawned window is opened in the foreground. If your goal is to continue working in Vim, sending the new console window to the background would be useful. In most versions of Windows, this can be done using :!start /min instead of just :!start in the command.

One final thing to note is the final call to the delete() function in our processing function. This is important, otherwise every time we invoke our command, we will leave behind the temporary file.

Extended example: asynchronous grep

Here is an extended example of getting the asynchronous process to communicate back to Vim. The following will perform a grep (well, a findstr) asynchronously on Windows. This example uses all the same ideas in our "hello world" example, to do something that is actually useful. Something extra to note in this function is the use of escape() to make sure the user can pass in a search pattern containing a '!' character. According to :help :!, any such characters in a shell command will be expanded to the previously used shell command unless escaped with a backslash. Note that if your search does not need to be asynchronous, it is easy to grep from within Vim without any tricks like this.

if has('win32')
  " basic async search
  command! -bar -complete=file -nargs=+ GrepAsync call AsyncGrep('',<f-args>)
  " same, but recursive
  command! -bar -complete=file -nargs=+ RGrepAsync call AsyncGrep('/S',<f-args>)

  function! AsyncGrep(flags, pattern, ...)
    " find a place to store the results to allow Vim to read them in
    let grep_temp_file = tempname()

    " get the file list into a format findstr can read, or search all files if
    " no file list was passed in
    if a:0 > 0
      let grep_file_list = substitute(join(a:000, '" "'),'/','\','g')
    else
      let grep_file_list = '*'
    endif

    " Need to escape '!' characters according to :help :!
    let flags = escape(a:flags, '!')
    let pattern = escape(a:pattern, '!')
    let grep_file_list = escape(grep_file_list, '!')

    " execute the search in a new process and redirect to a temporary file, then
    " send the result file back to this Vim instance
    exec 'silent !start /min cmd /c "echo Searching for "'.pattern.'" in '.grep_file_list.', do not close this window... && '.
          \ 'findstr /R /N '.flags.' "'.pattern.'" "'.grep_file_list.'" > "'.grep_temp_file.'" && '.
          \ 'vim --servername '.v:servername.' --remote-expr "ParseAsyncGrep('."'".grep_temp_file."')".'""'
  endfunction

  function! ParseAsyncGrep(tempfile)
    " set up the errorformat so Vim can parse the output
    let oldefm = &errorformat
    let &errorformat = &grepformat

    " parse the results into the quickfix window, but don't jump to the first
    " search hit
    exec 'cgetfile '.a:tempfile

    " restore the previous state
    let &errorformat = oldefm
    call delete(a:tempfile)

    " the echomsg is mostly debug, but could be used instead of the botright
    " copen to alert the user without interfering
    echomsg "got grep results file ".a:tempfile

    " open the quickfix window but don't interfere with the user; jump back to
    " the current window after opening the quickfix window
    botright copen
    wincmd p
    redraw
  endfunction
endif

See also

Comments

Is there a way to do this on Linux or Mac OS? Thanks!


I do not know. :help :!start seems to indicate that it is for Windows only. If there is a way to start a command asynchronously on *nix systems, the same strategy could be used, invoking the shell directly with a command that will communicate to Vim through a file.

--Fritzophrenic 05:30, January 29, 2010 (UTC)

Advertisement