system_spec.lua (21485B)
1 -- Tests for system() and :! shell. 2 3 local t = require('test.testutil') 4 local n = require('test.functional.testnvim')() 5 local Screen = require('test.functional.ui.screen') 6 7 local assert_alive = n.assert_alive 8 local testprg = n.testprg 9 local eq, call, clear, eval, feed_command, feed, api = 10 t.eq, n.call, n.clear, n.eval, n.feed_command, n.feed, n.api 11 local command = n.command 12 local insert = n.insert 13 local expect = n.expect 14 local exc_exec = n.exc_exec 15 local pcall_err = t.pcall_err 16 local is_os = t.is_os 17 18 local function create_file_with_nuls(name) 19 return function() 20 feed('ipart1<C-V>000part2<C-V>000part3<ESC>:w ' .. name .. '<CR>') 21 eval('1') -- wait for the file to be created 22 end 23 end 24 25 local function delete_file(name) 26 return function() 27 eval("delete('" .. name .. "')") 28 end 29 end 30 31 describe('system()', function() 32 before_each(clear) 33 34 describe('command passed as a List', function() 35 it('throws error if cmd[0] is not executable', function() 36 eq( 37 "Vim:E475: Invalid value for argument cmd: 'this-should-not-exist' is not executable", 38 pcall_err(call, 'system', { 'this-should-not-exist' }) 39 ) 40 eq(-1, eval('v:shell_error')) 41 end) 42 43 it('parameter validation does NOT modify v:shell_error', function() 44 -- 1. Call system() with invalid parameters. 45 -- 2. Assert that v:shell_error was NOT set. 46 feed_command('call system({})') 47 eq('E475: Invalid argument: expected String or List', eval('v:errmsg')) 48 eq(0, eval('v:shell_error')) 49 feed_command('call system([])') 50 eq('E474: Invalid argument', eval('v:errmsg')) 51 eq(0, eval('v:shell_error')) 52 53 -- Provoke a non-zero v:shell_error. 54 eq( 55 "Vim:E475: Invalid value for argument cmd: 'this-should-not-exist' is not executable", 56 pcall_err(call, 'system', { 'this-should-not-exist' }) 57 ) 58 local old_val = eval('v:shell_error') 59 eq(-1, old_val) 60 61 -- 1. Call system() with invalid parameters. 62 -- 2. Assert that v:shell_error was NOT modified. 63 feed_command('call system({})') 64 eq(old_val, eval('v:shell_error')) 65 feed_command('call system([])') 66 eq(old_val, eval('v:shell_error')) 67 end) 68 69 it('quotes arguments correctly #5280', function() 70 local out = 71 call('system', { testprg('printargs-test'), [[1]], [[2 "3]], [[4 ' 5]], [[6 ' 7']] }) 72 73 eq(0, eval('v:shell_error')) 74 eq([[arg1=1;arg2=2 "3;arg3=4 ' 5;arg4=6 ' 7';]], out) 75 76 out = call('system', { testprg('printargs-test'), [['1]], [[2 "3]] }) 77 eq(0, eval('v:shell_error')) 78 eq([[arg1='1;arg2=2 "3;]], out) 79 80 out = call('system', { testprg('printargs-test'), 'A\nB' }) 81 eq(0, eval('v:shell_error')) 82 eq('arg1=A\nB;', out) 83 end) 84 85 it('calls executable in $PATH', function() 86 if 0 == eval("executable('python3')") then 87 pending('missing `python3`') 88 end 89 eq('foo\n', eval([[system(['python3', '-c', 'print("foo")'])]])) 90 eq(0, eval('v:shell_error')) 91 end) 92 93 it('does NOT run in shell', function() 94 if is_os('win') then 95 eq( 96 '%PATH%\n', 97 eval( 98 "system(['powershell', '-NoProfile', '-NoLogo', '-ExecutionPolicy', 'RemoteSigned', '-Command', 'Write-Output', '%PATH%'])" 99 ) 100 ) 101 else 102 eq('* $PATH %PATH%\n', eval("system(['echo', '*', '$PATH', '%PATH%'])")) 103 end 104 end) 105 end) 106 107 it('sets v:shell_error', function() 108 if is_os('win') then 109 eval([[system("cmd.exe /c exit")]]) 110 eq(0, eval('v:shell_error')) 111 eval([[system("cmd.exe /c exit 1")]]) 112 eq(1, eval('v:shell_error')) 113 eval([[system("cmd.exe /c exit 5")]]) 114 eq(5, eval('v:shell_error')) 115 eval([[system('this-should-not-exist')]]) 116 eq(1, eval('v:shell_error')) 117 else 118 eval([[system("sh -c 'exit'")]]) 119 eq(0, eval('v:shell_error')) 120 eval([[system("sh -c 'exit 1'")]]) 121 eq(1, eval('v:shell_error')) 122 eval([[system("sh -c 'exit 5'")]]) 123 eq(5, eval('v:shell_error')) 124 eval([[system('this-should-not-exist')]]) 125 eq(127, eval('v:shell_error')) 126 end 127 end) 128 129 describe('executes shell function', function() 130 local screen 131 132 before_each(function() 133 screen = Screen.new() 134 t.write_file('Xmorefile', ('line1\nline2\nline3\n'):rep(10)) 135 end) 136 137 after_each(function() 138 os.remove('Xmorefile') 139 end) 140 141 if is_os('win') then 142 local function test_more() 143 eq('line1', eval([[get(split(system('"more" "Xmorefile"'), "\n"), 0, '')]])) 144 end 145 local function test_shell_unquoting() 146 eval([[system('"ping" "-n" "1" "127.0.0.1"')]]) 147 eq(0, eval('v:shell_error')) 148 eq('"a b"\n', eval([[system('cmd /s/c "cmd /s/c "cmd /s/c "echo "a b""""')]])) 149 eq( 150 '"a b"\n', 151 eval( 152 [[system('powershell -NoProfile -NoLogo -ExecutionPolicy RemoteSigned -Command Write-Output ''\^"a b\^"''')]] 153 ) 154 ) 155 end 156 157 it('with shell=cmd.exe', function() 158 command('set shell=cmd.exe') 159 eq('""\n', eval([[system('echo ""')]])) 160 eq('"a b"\n', eval([[system('echo "a b"')]])) 161 eq('a \nb\n', eval([[system('echo a & echo b')]])) 162 eq('a \n', eval([[system('echo a 2>&1')]])) 163 test_more() 164 eval([[system('cd "C:\Program Files"')]]) 165 eq(0, eval('v:shell_error')) 166 test_shell_unquoting() 167 end) 168 169 it('with shell=cmd', function() 170 command('set shell=cmd') 171 eq('"a b"\n', eval([[system('echo "a b"')]])) 172 test_more() 173 test_shell_unquoting() 174 end) 175 176 it('with shell=$COMSPEC', function() 177 local comspecshell = eval("fnamemodify($COMSPEC, ':t')") 178 if comspecshell == 'cmd.exe' then 179 command('set shell=$COMSPEC') 180 eq('"a b"\n', eval([[system('echo "a b"')]])) 181 test_more() 182 test_shell_unquoting() 183 else 184 pending('$COMSPEC is not cmd.exe: ' .. comspecshell) 185 end 186 end) 187 188 it('with powershell', function() 189 n.set_shell_powershell() 190 eq('a\nb\n', eval([[system('Write-Output a b')]])) 191 eq('C:\\\n', eval([[system('cd c:\; (Get-Location).Path')]])) 192 eq('a b\n', eval([[system('Write-Output "a b"')]])) 193 end) 194 end 195 196 it('powershell w/ UTF-8 text #13713', function() 197 if not n.has_powershell() then 198 pending('powershell not found', function() end) 199 return 200 end 201 n.set_shell_powershell() 202 eq('ああ\n', eval([[system('Write-Output "ああ"')]])) 203 -- Sanity test w/ default encoding 204 -- * on Windows, UTF-8 still works. 205 -- * on Linux, expected to default to UTF8 206 command([[let &shellcmdflag = '-NoLogo -NoProfile -ExecutionPolicy RemoteSigned -Command ']]) 207 eq('ああ\n', eval([[system('Write-Output "ああ"')]])) 208 end) 209 210 it('`echo` and waits for its return', function() 211 feed(':call system("echo")<cr>') 212 screen:expect([[ 213 ^ | 214 {1:~ }|*12 215 :call system("echo") | 216 ]]) 217 end) 218 219 it('prints verbose information', function() 220 api.nvim_set_option_value('shell', 'fake_shell', {}) 221 api.nvim_set_option_value('shellcmdflag', 'cmdflag', {}) 222 223 screen:try_resize(72, 14) 224 feed(':4verbose echo system("echo hi")<cr>') 225 if is_os('win') then 226 screen:expect { any = [[Executing command: "'fake_shell' 'cmdflag' '"echo hi"'"]] } 227 else 228 screen:expect { any = [[Executing command: "'fake_shell' 'cmdflag' 'echo hi'"]] } 229 end 230 feed('<cr>') 231 end) 232 233 it('self and total time recorded separately', function() 234 local tempfile = t.tmpname() 235 236 feed(':function! AlmostNoSelfTime()<cr>') 237 feed('echo system("echo hi")<cr>') 238 feed('endfunction<cr>') 239 240 feed(':profile start ' .. tempfile .. '<cr>') 241 feed(':profile func AlmostNoSelfTime<cr>') 242 feed(':call AlmostNoSelfTime()<cr>') 243 feed(':profile dump<cr>') 244 245 feed(':edit ' .. tempfile .. '<cr>') 246 247 local command_total_time = tonumber(n.fn.split(n.fn.getline(7))[2]) 248 local command_self_time = tonumber(n.fn.split(n.fn.getline(7))[3]) 249 250 t.neq(nil, command_total_time) 251 t.neq(nil, command_self_time) 252 end) 253 254 it('`yes` interrupted with CTRL-C', function() 255 feed( 256 ':call system("' 257 .. (is_os('win') and 'for /L %I in (1,0,2) do @echo y' or 'yes') 258 .. '")<cr>' 259 ) 260 screen:expect([[ 261 | 262 {1:~ }|*12 263 ]] .. (is_os('win') and [[ 264 :call system("for /L %I in (1,0,2) do @echo y") |]] or [[ 265 :call system("yes") |]])) 266 feed('foo<c-c>') 267 screen:expect([[ 268 ^ | 269 {1:~ }|*12 270 Type :qa and press <Enter> to exit Nvim | 271 ]]) 272 end) 273 274 it('`yes` interrupted with mapped CTRL-C', function() 275 command('nnoremap <C-C> i') 276 feed( 277 ':call system("' 278 .. (is_os('win') and 'for /L %I in (1,0,2) do @echo y' or 'yes') 279 .. '")<cr>' 280 ) 281 screen:expect([[ 282 | 283 {1:~ }|*12 284 ]] .. (is_os('win') and [[ 285 :call system("for /L %I in (1,0,2) do @echo y") |]] or [[ 286 :call system("yes") |]])) 287 feed('foo<c-c>') 288 screen:expect([[ 289 ^ | 290 {1:~ }|*12 291 {5:-- INSERT --} | 292 ]]) 293 end) 294 end) 295 296 describe('passing no input', function() 297 it('returns the program output', function() 298 if is_os('win') then 299 eq('echoed\n', eval('system("echo echoed")')) 300 else 301 eq('echoed', eval('system("printf echoed")')) 302 end 303 end) 304 it('to backgrounded command does not crash', function() 305 -- This is indeterminate, just exercise the codepath. May get E5677. 306 feed_command( 307 'call system(has("win32") ? "start /b /wait cmd /c echo echoed" : "printf echoed &")' 308 ) 309 local v_errnum = string.match(eval('v:errmsg'), '^E%d*:') 310 if v_errnum then 311 eq('E5677:', v_errnum) 312 end 313 assert_alive() 314 end) 315 end) 316 317 describe('passing input', function() 318 it('returns the program output', function() 319 eq('input', eval('system("cat -", "input")')) 320 end) 321 it('to backgrounded command does not crash', function() 322 -- This is indeterminate, just exercise the codepath. May get E5677. 323 feed_command('call system(has("win32") ? "start /b /wait more" : "cat - &", "input")') 324 local v_errnum = string.match(eval('v:errmsg'), '^E%d*:') 325 if v_errnum then 326 eq('E5677:', v_errnum) 327 end 328 assert_alive() 329 end) 330 it('works with an empty string', function() 331 eq('test\n', eval('system("echo test", "")')) 332 assert_alive() 333 end) 334 end) 335 336 describe('passing a lot of input', function() 337 it('returns the program output', function() 338 local input = {} 339 -- write more than 1mb of data, which should be enough to overcome 340 -- the os buffer limit and force multiple event loop iterations to write 341 -- everything 342 for _ = 1, 0xffff do 343 input[#input + 1] = '01234567890ABCDEFabcdef' 344 end 345 input = table.concat(input, '\n') 346 api.nvim_set_var('input', input) 347 eq(input, eval('system("cat -", g:input)')) 348 end) 349 end) 350 351 describe('Number input', function() 352 it('is treated as a buffer id', function() 353 command("put ='text in buffer 1'") 354 eq('\ntext in buffer 1\n', eval('system("cat", 1)')) 355 eq('Vim(echo):E86: Buffer 42 does not exist', exc_exec('echo system("cat", 42)')) 356 end) 357 end) 358 359 describe('with output containing NULs', function() 360 local fname = 'Xtest_functional_vimscript_system_nuls' 361 362 before_each(create_file_with_nuls(fname)) 363 after_each(delete_file(fname)) 364 365 it('replaces NULs by SOH characters', function() 366 eq('part1\001part2\001part3\n', eval([[system('"cat" "]] .. fname .. [["')]])) 367 end) 368 end) 369 370 describe('input passed as List', function() 371 it('joins List items with linefeed characters', function() 372 eq('line1\nline2\nline3', eval("system('cat -', ['line1', 'line2', 'line3'])")) 373 end) 374 375 -- Notice that NULs are converted to SOH when the data is read back. This 376 -- is inconsistent and is a good reason for the existence of the 377 -- `systemlist()` function, where input and output map to the same 378 -- characters(see the following tests with `systemlist()` below) 379 describe('with linefeed characters inside List items', function() 380 it('converts linefeed characters to NULs', function() 381 eq( 382 '\001l1\001p2\nline2\001a\001b\nl3', 383 eval([[system('cat -', ["\nl1\np2", "line2\na\nb", 'l3'])]]) 384 ) 385 end) 386 end) 387 388 describe('with leading/trailing whitespace characters on items', function() 389 it('preserves whitespace, replacing linefeeds by NULs', function() 390 eq( 391 'line \nline2\001\n\001line3', 392 eval([[system('cat -', ['line ', "line2\n", "\nline3"])]]) 393 ) 394 end) 395 end) 396 end) 397 398 it("with a program that doesn't close stdout will exit properly after passing input", function() 399 local out = eval(string.format("system('%s', 'clip-data')", testprg('streams-test'))) 400 assert(out:sub(0, 5) == 'pid: ', out) 401 eq(0, vim.uv.kill(assert(tonumber(out:match('%d+'))), 'sigkill')) 402 end) 403 end) 404 405 describe('systemlist()', function() 406 -- Similar to `system()`, but returns List instead of String. 407 before_each(clear) 408 409 it('sets v:shell_error', function() 410 if is_os('win') then 411 eval([[systemlist("cmd.exe /c exit")]]) 412 eq(0, eval('v:shell_error')) 413 eval([[systemlist("cmd.exe /c exit 1")]]) 414 eq(1, eval('v:shell_error')) 415 eval([[systemlist("cmd.exe /c exit 5")]]) 416 eq(5, eval('v:shell_error')) 417 eval([[systemlist('this-should-not-exist')]]) 418 eq(1, eval('v:shell_error')) 419 else 420 eval([[systemlist("sh -c 'exit'")]]) 421 eq(0, eval('v:shell_error')) 422 eval([[systemlist("sh -c 'exit 1'")]]) 423 eq(1, eval('v:shell_error')) 424 eval([[systemlist("sh -c 'exit 5'")]]) 425 eq(5, eval('v:shell_error')) 426 eval([[systemlist('this-should-not-exist')]]) 427 eq(127, eval('v:shell_error')) 428 end 429 end) 430 431 describe('executes shell function', function() 432 local screen 433 434 before_each(function() 435 screen = Screen.new() 436 end) 437 438 it('`echo` and waits for its return', function() 439 feed(':call systemlist("echo")<cr>') 440 screen:expect([[ 441 ^ | 442 {1:~ }|*12 443 :call systemlist("echo") | 444 ]]) 445 end) 446 447 it('`yes` interrupted with CTRL-C', function() 448 feed(':call systemlist("yes | xargs")<cr>') 449 screen:expect([[ 450 | 451 {1:~ }|*12 452 :call systemlist("yes | xargs") | 453 ]]) 454 feed('<c-c>') 455 screen:expect([[ 456 ^ | 457 {1:~ }|*12 458 Type :qa and press <Enter> to exit Nvim | 459 ]]) 460 end) 461 end) 462 463 describe('passing string with linefeed characters as input', function() 464 it('splits the output on linefeed characters', function() 465 eq({ 'abc', 'def', 'ghi' }, eval([[systemlist("cat -", "abc\ndef\nghi")]])) 466 end) 467 end) 468 469 describe('passing a lot of input', function() 470 it('returns the program output', function() 471 local input = {} 472 for _ = 1, 0xffff do 473 input[#input + 1] = '01234567890ABCDEFabcdef' 474 end 475 api.nvim_set_var('input', input) 476 eq(input, eval('systemlist("cat -", g:input)')) 477 end) 478 end) 479 480 describe('with output containing NULs', function() 481 local fname = 'Xtest_functional_vimscript_systemlist_nuls' 482 483 before_each(function() 484 command('set ff=unix') 485 create_file_with_nuls(fname)() 486 end) 487 after_each(delete_file(fname)) 488 489 it('replaces NULs by newline characters', function() 490 eq({ 'part1\npart2\npart3' }, eval([[systemlist('"cat" "]] .. fname .. [["')]])) 491 end) 492 end) 493 494 describe('input passed as List', function() 495 it('joins list items with linefeed characters', function() 496 eq({ 'line1', 'line2', 'line3' }, eval("systemlist('cat -', ['line1', 'line2', 'line3'])")) 497 end) 498 499 -- Unlike `system()` which uses SOH to represent NULs, with `systemlist()` 500 -- input and output are the same. 501 describe('with linefeed characters inside list items', function() 502 it('converts linefeed characters to NULs', function() 503 eq( 504 { '\nl1\np2', 'line2\na\nb', 'l3' }, 505 eval([[systemlist('cat -', ["\nl1\np2", "line2\na\nb", 'l3'])]]) 506 ) 507 end) 508 end) 509 510 describe('with leading/trailing whitespace characters on items', function() 511 it('preserves whitespace, replacing linefeeds by NULs', function() 512 eq( 513 { 'line ', 'line2\n', '\nline3' }, 514 eval([[systemlist('cat -', ['line ', "line2\n", "\nline3"])]]) 515 ) 516 end) 517 end) 518 end) 519 520 describe('handles empty lines', function() 521 it('in the middle', function() 522 eq({ 'line one', '', 'line two' }, eval("systemlist('cat',['line one','','line two'])")) 523 end) 524 525 it('in the beginning', function() 526 eq({ '', 'line one', 'line two' }, eval("systemlist('cat',['','line one','line two'])")) 527 end) 528 end) 529 530 describe('when keepempty option is', function() 531 it('0, ignores trailing newline', function() 532 eq({ 'aa', 'bb' }, eval("systemlist('cat',['aa','bb'],0)")) 533 eq({ 'aa', 'bb' }, eval("systemlist('cat',['aa','bb',''],0)")) 534 end) 535 536 it('1, preserves trailing newline', function() 537 eq({ 'aa', 'bb' }, eval("systemlist('cat',['aa','bb'],1)")) 538 eq({ 'aa', 'bb', '' }, eval("systemlist('cat',['aa','bb',''],2)")) 539 end) 540 end) 541 542 it("with a program that doesn't close stdout will exit properly after passing input", function() 543 local out = eval(string.format("systemlist('%s', 'clip-data')", testprg('streams-test'))) 544 assert(out[1]:sub(0, 5) == 'pid: ', out) 545 eq(0, vim.uv.kill(assert(tonumber(out[1]:match('%d+'))), 'sigkill')) 546 end) 547 548 it('powershell w/ UTF-8 text #13713', function() 549 if not n.has_powershell() then 550 pending('powershell not found', function() end) 551 return 552 end 553 n.set_shell_powershell() 554 eq({ is_os('win') and 'あ\r' or 'あ' }, eval([[systemlist('Write-Output あ')]])) 555 -- Sanity test w/ default encoding 556 -- * on Windows, UTF-8 still works. 557 -- * on Linux, expected to default to UTF8 558 command([[let &shellcmdflag = '-NoLogo -NoProfile -ExecutionPolicy RemoteSigned -Command ']]) 559 eq({ is_os('win') and 'あ\r' or 'あ' }, eval([[systemlist('Write-Output あ')]])) 560 end) 561 end) 562 563 describe('shell :!', function() 564 before_each(clear) 565 566 it(':{range}! works when the first char is NUL #34163', function() 567 api.nvim_buf_set_lines(0, 0, -1, true, { '\0hello', 'hello' }) 568 command('%!cat') 569 eq({ '\0hello', 'hello' }, api.nvim_buf_get_lines(0, 0, -1, true)) 570 end) 571 572 it(':{range}! with powershell using "commands" filter/redirect #16271 #19250', function() 573 if not n.has_powershell() then 574 return 575 end 576 local screen = Screen.new(500, 8) 577 n.set_shell_powershell() 578 insert([[ 579 3 580 1 581 4 582 2]]) 583 if is_os('win') then 584 feed(':4verbose %!sort /R<cr>') 585 screen:expect { 586 any = [[Executing command: " $input | sort /R".*]], 587 } 588 else 589 feed(':4verbose %!sort -r<cr>') 590 screen:expect { 591 any = [[Executing command: " $input | sort %-r".*]], 592 } 593 end 594 feed('<CR>') 595 expect([[ 596 4 597 3 598 2 599 1]]) 600 end) 601 602 it(':{range}! with powershell using "cmdlets" filter/redirect #16271 #19250', function() 603 if not n.has_powershell() then 604 pending('powershell not found', function() end) 605 return 606 end 607 local screen = Screen.new(500, 8) 608 n.set_shell_powershell() 609 insert([[ 610 3 611 1 612 4 613 2]]) 614 feed(':4verbose %!Sort-Object -Descending<cr>') 615 screen:expect { 616 any = [[Executing command: " $input | Sort%-Object %-Descending".*]], 617 } 618 feed('<CR>') 619 expect([[ 620 4 621 3 622 2 623 1]]) 624 end) 625 626 it(':{range}! without redirecting to buffer', function() 627 local screen = Screen.new(500, 10) 628 insert([[ 629 3 630 1 631 4 632 2]]) 633 feed(':4verbose %w !sort<cr>') 634 screen:expect { 635 any = [[Executing command: "sort".*]], 636 } 637 feed('<CR>') 638 639 if not n.has_powershell() then 640 return 641 end 642 643 n.set_shell_powershell(true) 644 feed(':4verbose %w !sort<cr>') 645 screen:expect { 646 any = [[Executing command: " $input | sort".*]], 647 } 648 feed('<CR>') 649 n.expect_exit(command, 'qall!') 650 end) 651 end)