Skip to content

Commit d780f0a

Browse files
committed
Make external command execution work properly without vim-shell!
In vim-easytags/issues#58 it became apparent that external command execution on Windows using vim-misc without vim-shell was completely broken :-( I never noticed before because I always have vim-shell installed and honestly just never gave the interaction any thought... Crap. This commit fixes xolox#misc#os#exec() on Windows when vim-shell is not available. It also adds testing infrastructure to run all tests involving xolox#misc#os#exec() twice, once with vim-shell disabled and then again with vim-shell enabled (assuming it is available). For more details, see the issue page on GitHub: xolox/vim-easytags#58
1 parent 473381c commit d780f0a

File tree

5 files changed

+191
-73
lines changed

5 files changed

+191
-73
lines changed

README.md

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ from the source code of the miscellaneous scripts using the Python module
3737

3838
<!-- Start of generated documentation -->
3939

40-
The documentation of the 78 functions below was extracted from
41-
15 Vim scripts on June 25, 2013 at 23:45.
40+
The documentation of the 80 functions below was extracted from
41+
15 Vim scripts on June 26, 2013 at 00:09.
4242

4343
### Handling of special buffers
4444

@@ -337,6 +337,13 @@ Returns a dictionary with one or more of the following key/value pairs:
337337

338338
[vim-shell]: http://peterodding.com/code/vim/shell/
339339

340+
#### The `xolox#misc#os#can_use_dll()` function
341+
342+
If a) we're on Microsoft Windows, b) the vim-shell plug-in is installed
343+
and c) the compiled DLL included in vim-shell works, we can use the
344+
vim-shell plug-in to execute external commands! Returns 1 (true)
345+
if we can use the DLL, 0 (false) otherwise.
346+
340347
### Pathname manipulation functions
341348

342349
#### The `xolox#misc#path#which()` function
@@ -553,6 +560,12 @@ with `xolox#misc#os#find_vim()`.
553560
Test basic functionality of synchronous command execution with
554561
`xolox#misc#os#exec()`.
555562

563+
#### The `xolox#misc#tests#synchronous_command_execution_with_stderr()` function
564+
565+
Test basic functionality of synchronous command execution with
566+
`xolox#misc#os#exec()` including the standard error stream (not available
567+
on Windows when vim-shell is not installed).
568+
556569
#### The `xolox#misc#tests#synchronous_command_execution_with_raising_of_errors()` function
557570

558571
Test raising of errors during synchronous command execution with
@@ -565,8 +578,11 @@ Test synchronous command execution without raising of errors with
565578

566579
#### The `xolox#misc#tests#asynchronous_command_execution()` function
567580

568-
Test basic functionality of asynchronous command execution with
569-
`xolox#misc#os#exec()`.
581+
Test the basic functionality of asynchronous command execution with
582+
`xolox#misc#os#exec()`. This runs the external command `mkdir` and tests
583+
that the side effect of creating the directory takes place. This might
584+
seem like a peculiar choice, but it's one of the few 100% portable
585+
commands (Windows + UNIX) that doesn't involve input/output streams.
570586

571587
#### The `xolox#misc#tests#string_case_transformation()` function
572588

autoload/xolox/misc.vim

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
" Last Change: June 25, 2013
55
" URL: http://peterodding.com/code/vim/misc/
66

7-
let g:xolox#misc#version = '1.8.2'
7+
let g:xolox#misc#version = '1.8.3'

autoload/xolox/misc/os.vim

Lines changed: 77 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,24 @@ function! xolox#misc#os#exec(options) " {{{1
113113
let cmd = a:options['command']
114114
let async = get(a:options, 'async', 0)
115115

116+
" We need to know in a couple of places whether we are on Windows.
117+
let is_win = xolox#misc#os#is_win()
118+
119+
" Use vim-shell so we don't pop up a console window on Windows? If the
120+
" caller specifically asks us *not* to use vim-shell, we'll respect that
121+
" choice; this is very useful for automated tests :-).
122+
if get(a:options, 'use_dll', 1) == 0
123+
let use_dll = 0
124+
else
125+
let use_dll = xolox#misc#os#can_use_dll()
126+
endif
127+
128+
" Decide whether to redirect the standard output and standard error
129+
" streams to temporary files.
130+
let redirect_output = !async && (use_dll || !is_win)
131+
116132
" Write the input for the external command to a temporary file?
117-
if has_key(a:options, 'stdin')
133+
if has_key(a:options, 'stdin') && use_dll
118134
let tempin = tempname()
119135
if type(a:options['stdin']) == type([])
120136
let lines = a:options['stdin']
@@ -125,68 +141,82 @@ function! xolox#misc#os#exec(options) " {{{1
125141
let cmd .= ' < ' . xolox#misc#escape#shell(tempin)
126142
endif
127143

128-
" Redirect the standard output and standard error streams of the external
129-
" process to temporary files? (only in synchronous mode, which is the
130-
" default).
131-
if !async
144+
" Redirect the standard output and/or standard error streams of the
145+
" external process to temporary files? (only in synchronous mode)
146+
if redirect_output
132147
let tempout = tempname()
133148
let temperr = tempname()
134149
let cmd = printf('(%s) 1>%s 2>%s', cmd, xolox#misc#escape#shell(tempout), xolox#misc#escape#shell(temperr))
135150
endif
136151

137-
" If A) we're on Windows, B) the vim-shell plug-in is installed and C) the
138-
" compiled DLL works, we'll use that because it's the most user friendly
139-
" method. If the plug-in is not installed Vim will raise the exception
140-
" "E117: Unknown function" which is caught and handled below.
141-
try
142-
if xolox#shell#can_use_dll()
143-
" Let the user know what's happening (in case they're interested).
144-
call xolox#misc#msg#debug("vim-misc %s: Executing external command using compiled DLL: %s", g:xolox#misc#version, cmd)
145-
let exit_code = xolox#shell#execute_with_dll(cmd, async)
146-
endif
147-
catch /^Vim\%((\a\+)\)\=:E117/
148-
call xolox#misc#msg#debug("vim-misc %s: The vim-shell plug-in is not installed, falling back to system() function.", g:xolox#misc#version)
149-
endtry
150-
151-
" If we cannot use the DLL, we fall back to the default and generic
152-
" implementation using Vim's system() function.
153-
if !exists('exit_code')
152+
" Use vim-shell or system() to execute the external command?
153+
if use_dll
154+
call xolox#misc#msg#debug("vim-misc %s: Executing external command using compiled DLL: %s", g:xolox#misc#version, cmd)
155+
let exit_code = xolox#shell#execute_with_dll(cmd, async)
156+
else
154157

155158
" Enable asynchronous mode (very platform specific).
156159
if async
157-
if xolox#misc#os#is_win()
158-
let cmd = 'start /b ' . cmd
160+
if is_win
161+
let cmd = printf('start /b %s', cmd)
159162
elseif has('unix')
160-
let cmd = '(' . cmd . ') &'
163+
let cmd = printf('(%s) &', cmd)
161164
else
162-
call xolox#misc#msg#warn("vim-misc %s: I don't know how to run commands asynchronously on your platform! Falling back to synchronous mode.", g:xolox#misc#version)
165+
call xolox#misc#msg#warn("vim-misc %s: I don't know how to execute the command %s asynchronously on your platform! Falling back to synchronous mode...", g:xolox#misc#version, cmd)
163166
endif
164167
endif
165168

166-
" Execute the command line using 'sh' instead of the default shell,
167-
" because we assume that standard output and standard error can be
168-
" redirected separately, but (t)csh does not support this.
169+
" On UNIX we explicitly execute the command line using 'sh' instead of
170+
" the default shell, because we assume that standard output and standard
171+
" error can be redirected separately, but (t)csh does not support this
172+
" (and it might be the default shell).
169173
if has('unix')
170174
call xolox#misc#msg#debug("vim-misc %s: Generated shell expression: %s", g:xolox#misc#version, cmd)
171175
let cmd = printf('sh -c %s', xolox#misc#escape#shell(cmd))
172176
endif
173177

174178
" Let the user know what's happening (in case they're interested).
175-
call xolox#misc#msg#debug("vim-misc %s: Executing external command using system() function: %s", g:xolox#misc#version, cmd)
176-
call system(cmd)
177-
let exit_code = v:shell_error
179+
if async && is_win
180+
call xolox#misc#msg#debug("vim-misc %s: Executing external command using !start command: %s", g:xolox#misc#version, cmd)
181+
silent execute '!' . cmd
182+
else
183+
call xolox#misc#msg#debug("vim-misc %s: Executing external command using system() function: %s", g:xolox#misc#version, cmd)
184+
let arguments = [cmd]
185+
if has_key(a:options, 'stdin')
186+
if type(a:options['stdin']) == type([])
187+
call add(arguments, join(a:options['stdin'], "\n"))
188+
else
189+
call add(arguments, a:options['stdin'])
190+
endif
191+
endif
192+
let stdout = call('system', arguments)
193+
let exit_code = v:shell_error
194+
endif
178195

179196
endif
180197

181198
" Return the results as a dictionary with one or more key/value pairs.
182199
let result = {'command': cmd}
183200
if !async
184201
let result['exit_code'] = exit_code
185-
let result['stdout'] = s:readfile(tempout, 'standard output', a:options['command'])
186-
let result['stderr'] = s:readfile(temperr, 'standard error', a:options['command'])
202+
" Get the standard output of the command.
203+
if redirect_output
204+
let result['stdout'] = s:readfile(tempout, 'standard output', a:options['command'])
205+
elseif exists('stdout')
206+
let result['stdout'] = split(stdout, "\n")
207+
else
208+
let result['stdout'] = []
209+
endif
210+
" Get the standard error of the command.
211+
if exists('temperr')
212+
let result['stderr'] = s:readfile(temperr, 'standard error', a:options['command'])
213+
else
214+
let result['stderr'] = []
215+
endif
187216
" If we just executed a synchronous command and the caller didn't
188217
" specifically ask us *not* to check the exit code of the external
189-
" command, we'll do so now.
218+
" command, we'll do so now. The idea here is that it should be easy
219+
" to 'do the right thing'.
190220
if get(a:options, 'check', 1) && exit_code != 0
191221
" Prepare an error message with enough details so the user can investigate.
192222
let msg = printf("vim-misc %s: External command failed with exit code %d!", g:xolox#misc#version, result['exit_code'])
@@ -215,6 +245,18 @@ function! xolox#misc#os#exec(options) " {{{1
215245

216246
endfunction
217247

248+
function! xolox#misc#os#can_use_dll() " {{{1
249+
" If a) we're on Microsoft Windows, b) the vim-shell plug-in is installed
250+
" and c) the compiled DLL included in vim-shell works, we can use the
251+
" vim-shell plug-in to execute external commands! Returns 1 (true)
252+
" if we can use the DLL, 0 (false) otherwise.
253+
try
254+
return xolox#shell#can_use_dll()
255+
catch /^Vim\%((\a\+)\)\=:E117/
256+
return 0
257+
endtry
258+
endfunction
259+
218260
function! s:readfile(fname, label, cmd) " {{{1
219261
try
220262
return readfile(a:fname)

autoload/xolox/misc/tests.vim

Lines changed: 60 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
" automated test suite of the miscellaneous Vim scripts. Right now the
99
" coverage is not very high yet, but this will improve over time.
1010

11+
let s:use_dll = 0
12+
let s:can_use_dll = xolox#misc#os#can_use_dll()
13+
1114
function! xolox#misc#tests#run() " {{{1
1215
" Run the automated test suite of the miscellaneous Vim scripts. To be used
1316
" interactively. Intended to be safe to execute irrespective of context.
@@ -23,12 +26,27 @@ function! xolox#misc#tests#run() " {{{1
2326
call xolox#misc#test#summarize()
2427
endfunction
2528

29+
function! s:wrap_exec_test(function)
30+
" Wrapper for tests that use xolox#misc#os#exec(). If we're on Windows and
31+
" the vim-shell plug-in is installed, the test will be run twice: Once with
32+
" vim-shell disabled and once with vim-shell enabled. This makes sure that
33+
" all code paths are tested as much as possible.
34+
call xolox#misc#msg#debug("vim-misc %s: Temporarily disabling vim-shell so we can test vim-misc ..", g:xolox#misc#version)
35+
let s:use_dll = 0
36+
call xolox#misc#test#wrap(a:function)
37+
if s:can_use_dll
38+
call xolox#misc#msg#debug("vim-misc %s: Re-enabling vim-shell so we can test that as well ..", g:xolox#misc#version)
39+
let s:use_dll = 1
40+
call xolox#misc#test#wrap(a:function)
41+
endif
42+
endfunction
43+
2644
" Tests for autoload/xolox/misc/escape.vim {{{1
2745

2846
function! s:test_string_escaping()
2947
call xolox#misc#test#wrap('xolox#misc#tests#pattern_escaping')
3048
call xolox#misc#test#wrap('xolox#misc#tests#substitute_escaping')
31-
call xolox#misc#test#wrap('xolox#misc#tests#shell_escaping')
49+
call s:wrap_exec_test('xolox#misc#tests#shell_escaping')
3250
endfunction
3351

3452
function! xolox#misc#tests#pattern_escaping() " {{{2
@@ -47,9 +65,15 @@ endfunction
4765
function! xolox#misc#tests#shell_escaping() " {{{2
4866
" Test escaping of shell arguments with `xolox#misc#escape#shell()`.
4967
let expected_value = 'this < is > a | very " scary ^ string '' indeed'
50-
let result = xolox#misc#os#exec({'command': g:xolox#misc#test#echo . ' ' . xolox#misc#escape#shell(expected_value)})
68+
let result = xolox#misc#os#exec({'command': g:xolox#misc#test#echo . ' ' . xolox#misc#escape#shell(expected_value), 'use_dll': s:use_dll})
69+
call xolox#misc#test#assert_equals(0, result['exit_code'])
5170
call xolox#misc#test#assert_equals(0, result['exit_code'])
52-
call xolox#misc#test#assert_equals([expected_value], result['stdout'])
71+
call xolox#misc#test#assert_same_type([], result['stdout'])
72+
call xolox#misc#test#assert_equals(1, len(result['stdout']))
73+
" XXX On Windows using system() there's a trailing space I can't explain.
74+
" However the point of this test was to show that all characters pass
75+
" through unharmed, so for now I'll just ignore the space :-)
76+
call xolox#misc#test#assert_equals(expected_value, xolox#misc#str#trim(result['stdout'][0]))
5377
endfunction
5478

5579
" Tests for autoload/xolox/misc/list.vim {{{1
@@ -136,10 +160,11 @@ endfunction
136160

137161
function! s:test_command_execution()
138162
call xolox#misc#test#wrap('xolox#misc#tests#finding_vim_on_the_search_path')
139-
call xolox#misc#test#wrap('xolox#misc#tests#synchronous_command_execution')
140-
call xolox#misc#test#wrap('xolox#misc#tests#synchronous_command_execution_with_raising_of_errors')
141-
call xolox#misc#test#wrap('xolox#misc#tests#synchronous_command_execution_without_raising_errors')
142-
call xolox#misc#test#wrap('xolox#misc#tests#asynchronous_command_execution')
163+
call s:wrap_exec_test('xolox#misc#tests#synchronous_command_execution')
164+
call s:wrap_exec_test('xolox#misc#tests#synchronous_command_execution_with_stderr')
165+
call s:wrap_exec_test('xolox#misc#tests#synchronous_command_execution_with_raising_of_errors')
166+
call s:wrap_exec_test('xolox#misc#tests#synchronous_command_execution_without_raising_errors')
167+
call s:wrap_exec_test('xolox#misc#tests#asynchronous_command_execution')
143168
endfunction
144169

145170
function! xolox#misc#tests#finding_vim_on_the_search_path() " {{{2
@@ -155,18 +180,30 @@ endfunction
155180
function! xolox#misc#tests#synchronous_command_execution() " {{{2
156181
" Test basic functionality of synchronous command execution with
157182
" `xolox#misc#os#exec()`.
158-
let result = xolox#misc#os#exec({'command': printf('%s output && %s errors >&2', g:xolox#misc#test#echo, g:xolox#misc#test#echo)})
183+
let result = xolox#misc#os#exec({'command': printf('%s output', g:xolox#misc#test#echo), 'use_dll': s:use_dll})
159184
call xolox#misc#test#assert_same_type({}, result)
160185
call xolox#misc#test#assert_equals(0, result['exit_code'])
161186
call xolox#misc#test#assert_equals(['output'], result['stdout'])
162-
call xolox#misc#test#assert_equals(['errors'], result['stderr'])
187+
endfunction
188+
189+
function! xolox#misc#tests#synchronous_command_execution_with_stderr() " {{{2
190+
" Test basic functionality of synchronous command execution with
191+
" `xolox#misc#os#exec()` including the standard error stream (not available
192+
" on Windows when vim-shell is not installed).
193+
if !(xolox#misc#os#is_win() && !s:use_dll)
194+
let result = xolox#misc#os#exec({'command': printf('%s output && %s errors >&2', g:xolox#misc#test#echo, g:xolox#misc#test#echo), 'use_dll': s:use_dll})
195+
call xolox#misc#test#assert_same_type({}, result)
196+
call xolox#misc#test#assert_equals(0, result['exit_code'])
197+
call xolox#misc#test#assert_equals(['output'], result['stdout'])
198+
call xolox#misc#test#assert_equals(['errors'], result['stderr'])
199+
endif
163200
endfunction
164201

165202
function! xolox#misc#tests#synchronous_command_execution_with_raising_of_errors() " {{{2
166203
" Test raising of errors during synchronous command execution with
167204
" `xolox#misc#os#exec()`.
168205
try
169-
call xolox#misc#os#exec({'command': 'exit 1'})
206+
call xolox#misc#os#exec({'command': 'exit 1', 'use_dll': s:use_dll})
170207
call xolox#misc#test#assert_true(0)
171208
catch
172209
call xolox#misc#test#assert_true(1)
@@ -177,7 +214,7 @@ function! xolox#misc#tests#synchronous_command_execution_without_raising_errors(
177214
" Test synchronous command execution without raising of errors with
178215
" `xolox#misc#os#exec()`.
179216
try
180-
let result = xolox#misc#os#exec({'command': 'exit 42', 'check': 0})
217+
let result = xolox#misc#os#exec({'command': 'exit 42', 'check': 0, 'use_dll': s:use_dll})
181218
call xolox#misc#test#assert_true(1)
182219
call xolox#misc#test#assert_equals(42, result['exit_code'])
183220
catch
@@ -186,20 +223,23 @@ function! xolox#misc#tests#synchronous_command_execution_without_raising_errors(
186223
endfunction
187224

188225
function! xolox#misc#tests#asynchronous_command_execution() " {{{2
189-
" Test basic functionality of asynchronous command execution with
190-
" `xolox#misc#os#exec()`.
191-
let tempfile = tempname()
192-
let expected_value = string(localtime())
193-
let command = g:xolox#misc#test#echo . ' ' . xolox#misc#escape#shell(expected_value) . ' > ' . tempfile
194-
let result = xolox#misc#os#exec({'command': command, 'async': 1})
226+
" Test the basic functionality of asynchronous command execution with
227+
" `xolox#misc#os#exec()`. This runs the external command `mkdir` and tests
228+
" that the side effect of creating the directory takes place. This might
229+
" seem like a peculiar choice, but it's one of the few 100% portable
230+
" commands (Windows + UNIX) that doesn't involve input/output streams.
231+
let temporary_directory = xolox#misc#path#tempdir()
232+
let random_name = printf('%i', localtime())
233+
let expected_directory = xolox#misc#path#merge(temporary_directory, random_name)
234+
let command = 'mkdir ' . xolox#misc#escape#shell(expected_directory)
235+
let result = xolox#misc#os#exec({'command': command, 'async': 1, 'use_dll': s:use_dll})
195236
call xolox#misc#test#assert_same_type({}, result)
196237
" Make sure the command is really executed.
197238
let timeout = localtime() + 30
198-
while !filereadable(tempfile) && localtime() < timeout
239+
while !isdirectory(expected_directory) && localtime() < timeout
199240
sleep 500 m
200241
endwhile
201-
call xolox#misc#test#assert_true(filereadable(tempfile))
202-
call xolox#misc#test#assert_equals([expected_value], readfile(tempfile))
242+
call xolox#misc#test#assert_true(isdirectory(expected_directory))
203243
endfunction
204244

205245
" Tests for autoload/xolox/misc/str.vim {{{1

0 commit comments

Comments
 (0)