From: Yasuhiro Matsumoto Date: Wed, 25 Mar 2026 21:48:36 +0000 (+0000) Subject: patch 9.2.0250: system() does not support bypassing the shell X-Git-Tag: v9.2.0250^0 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=30f012d8bcf3d1cb19410ab8ca20523b1716539d;p=thirdparty%2Fvim.git patch 9.2.0250: system() does not support bypassing the shell Problem: system() and systemlist() only accept a String, requiring manual shell escaping for arguments with special characters. Solution: Accept a List as the first argument and execute the command bypassing the shell (Yasuhiro Matsumoto). fixes: #19789 closes: #19791 Signed-off-by: Yasuhiro Matsumoto Signed-off-by: Christian Brabandt --- diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt index 250c07b627..30894b96dc 100644 --- a/runtime/doc/builtin.txt +++ b/runtime/doc/builtin.txt @@ -699,7 +699,8 @@ synconcealed({lnum}, {col}) List info about concealing synstack({lnum}, {col}) List stack of syntax IDs at {lnum} and {col} system({expr} [, {input}]) String output of shell command/filter {expr} -systemlist({expr} [, {input}]) List output of shell command/filter {expr} +systemlist({expr} [, {input}]) + List output of shell command/filter {expr} tabpagebuflist([{arg}]) List list of buffer numbers in tab page tabpagenr([{arg}]) Number number of current or last tab page tabpagewinnr({tabarg} [, {arg}]) @@ -11694,6 +11695,30 @@ system({expr} [, {input}]) *system()* *E677* Get the output of the shell command {expr} as a |String|. See |systemlist()| to get the output as a |List|. + {expr} can be a |String| or a |List|. + When {expr} is a |String|, the command is executed through the + shell (see below for how the command is constructed). + + *E1575* + When {expr} is a |List|, the first item is the executable and + the remaining items are passed as arguments directly. The + command is executed without using a shell, similar to + |job_start()|. Since no shell is involved, shell features + such as redirection, piping, globbing, environment variable + expansion and backtick expansion will not work. Characters + like ">" are passed as literal arguments to the command, not + interpreted as redirection. Use this form when arguments may + contain special characters that should not be interpreted by + the shell. Example: > + :let out = system(['grep', '-r', 'pattern', '.']) +< With the String form ">" would be shell redirection, but + with a List it is passed as a literal argument: > + :let out = system(['echo', 'hello', '>', 'world']) +< This outputs "hello > world", not redirect to a file. + + To use the shell explicitly with a List: > + :let out = system(['/bin/sh', '-c', 'echo $HOME']) +< When {input} is given and is a |String| this string is written to a file and passed as stdin to the command. The string is written as-is, you need to take care of using the correct line @@ -11719,11 +11744,11 @@ system({expr} [, {input}]) *system()* *E677* being echoed on the screen. > :silent let f = system('ls *.vim') < - Note: Use |shellescape()| or |::S| with |expand()| or - |fnamemodify()| to escape special characters in a command - argument. Newlines in {expr} may cause the command to fail. - The characters in 'shellquote' and 'shellxquote' may also - cause trouble. + Note: When {expr} is a String, use |shellescape()| or |::S| + with |expand()| or |fnamemodify()| to escape special + characters in a command argument. Newlines in {expr} may + cause the command to fail. The characters in 'shellquote' + and 'shellxquote' may also cause trouble. This is not to be used for interactive commands. The result is a String. Example: > @@ -11736,7 +11761,8 @@ system({expr} [, {input}]) *system()* *E677* To avoid the string being truncated at a NUL, all NUL characters are replaced with SOH (0x01). - The command executed is constructed using several options: + When {expr} is a String, the command executed is constructed + using several options: 'shell' 'shellcmdflag' 'shellxquote' {expr} 'shellredir' {tmp} 'shellxquote' ({tmp} is an automatically generated file name). For Unix, braces are put around {expr} to allow for @@ -11763,6 +11789,9 @@ system({expr} [, {input}]) *system()* *E677* systemlist({expr} [, {input}]) *systemlist()* Same as |system()|, but returns a |List| with lines (parts of output separated by NL) with NULs transformed into NLs. + Like |system()|, {expr} can be a |String| (executed through + the shell) or a |List| (executed directly without a shell). + See |system()| for details. Output is the same as |readfile()| will output with {binary} argument set to "b", except that there is no extra empty item when the result ends in a NL. diff --git a/runtime/doc/tags b/runtime/doc/tags index 77b19ec7ff..f460c3b1f0 100644 --- a/runtime/doc/tags +++ b/runtime/doc/tags @@ -4771,6 +4771,7 @@ E1571 builtin.txt /*E1571* E1572 options.txt /*E1572* E1573 channel.txt /*E1573* E1574 channel.txt /*E1574* +E1575 builtin.txt /*E1575* E158 sign.txt /*E158* E159 sign.txt /*E159* E16 cmdline.txt /*E16* diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt index ed58dae145..8045698a38 100644 --- a/runtime/doc/version9.txt +++ b/runtime/doc/version9.txt @@ -52611,6 +52611,8 @@ Other ~ - New "leadtab" value for the 'listchars' setting. - Improved |:set+=|, |:set^=| and |:set-=| handling of comma-separated "key:value" pairs individually (e.g. 'listchars', 'fillchars', 'diffopt'). +- |system()| and |systemlist()| functions accept a list as first argument, + bypassing the shell completely. xxd ~ --- diff --git a/src/errors.h b/src/errors.h index 016b917bdf..fba5b93f19 100644 --- a/src/errors.h +++ b/src/errors.h @@ -3795,8 +3795,6 @@ EXTERN char e_osc_response_timed_out[] #ifdef FEAT_EVAL EXTERN char e_cannot_add_listener_in_listener_callback[] INIT(= N_("E1569: Cannot use listener_add in a listener callback")); -#endif -#ifdef FEAT_EVAL EXTERN char e_cannot_add_redraw_listener_in_listener_callback[] INIT(= N_("E1570: Cannot use redraw_listener_add in a redraw listener callback")); EXTERN char e_no_redraw_listener_callbacks_defined[] @@ -3810,3 +3808,7 @@ EXTERN char e_cannot_listen_on_port[] EXTERN char e_gethostbyname_in_channel_listen[] INIT(= N_("E1574: gethostbyname(): cannot resolve hostname in channel_listen()")); #endif +#ifdef FEAT_EVAL +EXTERN char e_cannot_create_pipes[] + INIT(= N_("E1575: Cannot create pipes")); +#endif diff --git a/src/evalfunc.c b/src/evalfunc.c index d9f0017e68..de6975a0d4 100644 --- a/src/evalfunc.c +++ b/src/evalfunc.c @@ -1380,7 +1380,7 @@ static argcheck_T arg45_sign_place[] = {arg_number, arg_string, arg_string, arg_ static argcheck_T arg23_slice[] = {arg_slice1, arg_number, arg_number}; static argcheck_T arg13_sortuniq[] = {arg_list_any_mod, arg_sort_how, arg_dict_any}; static argcheck_T arg24_strpart[] = {arg_string, arg_number, arg_number, arg_bool}; -static argcheck_T arg12_system[] = {arg_string, arg_str_or_nr_or_list}; +static argcheck_T arg12_system[] = {arg_string_or_list_any, arg_str_or_nr_or_list}; static argcheck_T arg23_win_execute[] = {arg_number, arg_string_or_list_string, arg_string}; static argcheck_T arg23_writefile[] = {arg_list_or_blob, arg_string, arg_string}; static argcheck_T arg24_match_func[] = {arg_string_or_list_any, arg_string, arg_number, arg_number}; diff --git a/src/misc1.c b/src/misc1.c index ca913f930a..8a951298b9 100644 --- a/src/misc1.c +++ b/src/misc1.c @@ -2511,6 +2511,9 @@ get_cmd_output_as_rettv( FILE *fd; list_T *list = NULL; int flags = SHELL_SILENT; + int use_argv = FALSE; + char **argv = NULL; + int argc = 0; rettv->v_type = VAR_STRING; rettv->vval.v_string = NULL; @@ -2518,7 +2521,7 @@ get_cmd_output_as_rettv( goto errret; if (in_vim9script() - && (check_for_string_arg(argvars, 0) == FAIL + && (check_for_string_or_list_arg(argvars, 0) == FAIL || check_for_opt_string_or_number_or_list_arg(argvars, 1) == FAIL)) return; @@ -2598,6 +2601,47 @@ get_cmd_output_as_rettv( } } + // When the command is a List, execute directly without the shell. + if (argvars[0].v_type == VAR_LIST) + { + list_T *l = argvars[0].vval.v_list; + + if (l == NULL || l->lv_len < 1) + { + emsg(_(e_invalid_argument)); + goto errret; + } + if (build_argv_from_list(l, &argv, &argc) == FAIL) + goto errret; + if (argc == 0 || *skipwhite((char_u *)argv[0]) == NUL) + { + emsg(_(e_invalid_argument)); + goto errret; + } + use_argv = TRUE; + + if (p_verbose > 3) + { + int i; + garray_T ga; + + verbose_enter(); + ga_init2(&ga, 1, 200); + for (i = 0; i < argc; ++i) + { + if (i > 0) + ga_append(&ga, ' '); + ga_concat(&ga, (char_u *)argv[i]); + } + ga_append(&ga, NUL); + smsg(_("Executing directly: \"%s\""), (char *)ga.ga_data); + msg_putchar_attr('\n', 0); + cursor_on(); + verbose_leave(); + ga_clear(&ga); + } + } + // Omit SHELL_COOKED when invoked with ":silent". Avoids that the shell // echoes typeahead, that messes up the display. if (!msg_silent) @@ -2612,7 +2656,10 @@ get_cmd_output_as_rettv( char_u *end; int i; - res = get_cmd_output(tv_get_string(&argvars[0]), infile, flags, &len); + if (use_argv) + res = mch_get_cmd_output_direct(argv, infile, flags, &len); + else + res = get_cmd_output(tv_get_string(&argvars[0]), infile, flags, &len); if (res == NULL) goto errret; @@ -2652,7 +2699,10 @@ get_cmd_output_as_rettv( } else { - res = get_cmd_output(tv_get_string(&argvars[0]), infile, flags, NULL); + if (use_argv) + res = mch_get_cmd_output_direct(argv, infile, flags, NULL); + else + res = get_cmd_output(tv_get_string(&argvars[0]), infile, flags, NULL); # ifdef USE_CRNL // translate into if (res != NULL) @@ -2674,6 +2724,13 @@ get_cmd_output_as_rettv( } errret: + if (argv != NULL) + { + int i; + for (i = 0; argv[i] != NULL; i++) + vim_free(argv[i]); + vim_free(argv); + } if (infile != NULL) { mch_remove(infile); diff --git a/src/os_unix.c b/src/os_unix.c index 91bfd63d0d..1d382f7f33 100644 --- a/src/os_unix.c +++ b/src/os_unix.c @@ -5915,6 +5915,154 @@ mch_call_shell( #endif } +#if defined(FEAT_EVAL) +/* + * Execute "argv" directly without the shell and return the output. + * Used by system() and systemlist() when the command is a List. + * "infile" is an optional temp file for stdin input. + * "flags" is SHELL_SILENT etc. + * When "ret_len" is not NULL, set it to the length of the output. + * Returns the output in allocated memory (or NULL on error). + * Sets v:shell_error to the exit status. + */ + char_u * +mch_get_cmd_output_direct( + char **argv, + char_u *infile, + int flags UNUSED, + int *ret_len) +{ + pid_t pid; + int fd_out[2] = {-1, -1}; + int status = -1; + char_u *buffer = NULL; + garray_T ga; + SIGSET_DECL(curset) + + ga_init2(&ga, 1, 4096); + + ch_log(NULL, "directly executing: %s", argv[0]); + + if (pipe(fd_out) < 0) + { + emsg(_(e_cannot_create_pipes)); + return NULL; + } + + BLOCK_SIGNALS(&curset); + pid = fork(); + if (pid == -1) + { + UNBLOCK_SIGNALS(&curset); + close(fd_out[0]); + close(fd_out[1]); + emsg(_("\nCannot fork\n")); + return NULL; + } + + if (pid == 0) + { + // child process + reset_signals(); + UNBLOCK_SIGNALS(&curset); + + if (ch_log_active()) + { + ch_log(NULL, "closing channel log in the child process"); + ch_logfile((char_u *)"", (char_u *)""); + } + + // Set up stdin. + if (infile != NULL) + { + int fd_in = open((char *)infile, O_RDONLY); + if (fd_in >= 0) + { + close(0); + vim_ignored = dup(fd_in); + close(fd_in); + } + } + else + { + int nullfd = open("/dev/null", O_RDONLY); + if (nullfd >= 0) + { + close(0); + vim_ignored = dup(nullfd); + close(nullfd); + } + } + + // Set up stdout: write end of pipe. + close(fd_out[0]); + close(1); + vim_ignored = dup(fd_out[1]); + // Also redirect stderr to the pipe. + close(2); + vim_ignored = dup(fd_out[1]); + close(fd_out[1]); + + execvp(argv[0], argv); + _exit(127); + // NOTREACHED + } + + // parent process + UNBLOCK_SIGNALS(&curset); + close(fd_out[1]); + + // Read output from child. + for (;;) + { + char buf[4096]; + int n; + + n = (int)read(fd_out[0], buf, sizeof(buf)); + if (n <= 0) + break; + ga_grow(&ga, n); + mch_memmove((char *)ga.ga_data + ga.ga_len, buf, n); + ga.ga_len += n; + } + close(fd_out[0]); + + // Wait for child to finish. + (void)waitpid(pid, &status, 0); + if (WIFEXITED(status)) + status = WEXITSTATUS(status); + else + status = -1; + set_vim_var_nr(VV_SHELL_ERROR, (long)status); + + if (ga.ga_len > 0) + { + buffer = alloc(ga.ga_len + 1); + if (buffer != NULL) + { + mch_memmove(buffer, ga.ga_data, ga.ga_len); + if (ret_len == NULL) + { + int i; + + // Change NUL into SOH, otherwise the string is truncated. + for (i = 0; i < ga.ga_len; ++i) + if (buffer[i] == NUL) + buffer[i] = 1; + buffer[ga.ga_len] = NUL; + } + else + *ret_len = ga.ga_len; + } + } + else if (ret_len != NULL) + *ret_len = 0; + + ga_clear(&ga); + return buffer; +} +#endif + #if defined(FEAT_JOB_CHANNEL) void mch_job_start(char **argv, job_T *job, jobopt_T *options, int is_terminal) diff --git a/src/os_win32.c b/src/os_win32.c index e24c40cf31..3d5b095c3c 100644 --- a/src/os_win32.c +++ b/src/os_win32.c @@ -5960,6 +5960,219 @@ create_pipe_pair(HANDLE handles[2]) return TRUE; } +# if defined(FEAT_EVAL) +/* + * Execute "argv" directly without the shell and return the output. + * Used by system() and systemlist() when the command is a List. + * "infile" is an optional temp file for stdin input. + * When "ret_len" is not NULL, set it to the length of the output. + * Returns the output in allocated memory (or NULL on error). + * Sets v:shell_error to the exit status. + */ + char_u * +mch_get_cmd_output_direct( + char **argv, + char_u *infile, + int flags UNUSED, + int *ret_len) +{ + STARTUPINFO si; + PROCESS_INFORMATION pi; + SECURITY_ATTRIBUTES saAttr; + HANDLE hChildStdoutRd = INVALID_HANDLE_VALUE; + HANDLE hChildStdoutWr = INVALID_HANDLE_VALUE; + HANDLE hChildStdinRd = INVALID_HANDLE_VALUE; + garray_T cmd_ga; + garray_T out_ga; + char_u *buffer = NULL; + DWORD exit_code = (DWORD)-1; + int i; + + // Build a command string from argv. + ga_init2(&cmd_ga, 1, 256); + for (i = 0; argv[i] != NULL; i++) + { + char_u *arg = (char_u *)argv[i]; + char_u *s = arg; + int has_spaces = FALSE; + int j; + + for (j = 0; s[j] != NUL; j++) + if (s[j] == ' ' || s[j] == '\t' || s[j] == '"') + { + has_spaces = TRUE; + break; + } + + if (i > 0) + ga_append(&cmd_ga, ' '); + + if (has_spaces) + { + int num_bs; + + ga_append(&cmd_ga, '"'); + for (j = 0; arg[j] != NUL; j++) + { + num_bs = 0; + while (arg[j] == '\\') + { + num_bs++; + j++; + } + + if (arg[j] == NUL) + { + // Backslashes before closing quote must be doubled. + while (num_bs-- > 0) + { + ga_append(&cmd_ga, '\\'); + ga_append(&cmd_ga, '\\'); + } + break; + } + else if (arg[j] == '"') + { + // Backslashes before a double quote must be doubled, + // and the double quote must be escaped. + while (num_bs-- > 0) + { + ga_append(&cmd_ga, '\\'); + ga_append(&cmd_ga, '\\'); + } + ga_append(&cmd_ga, '\\'); + ga_append(&cmd_ga, '"'); + } + else + { + while (num_bs-- > 0) + ga_append(&cmd_ga, '\\'); + ga_append(&cmd_ga, arg[j]); + } + } + ga_append(&cmd_ga, '"'); + } + else + ga_concat(&cmd_ga, arg); + } + ga_append(&cmd_ga, NUL); + + ga_init2(&out_ga, 1, 4096); + + saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); + saAttr.bInheritHandle = TRUE; + saAttr.lpSecurityDescriptor = NULL; + + // Create a pipe for the child's stdout. + if (!CreatePipe(&hChildStdoutRd, &hChildStdoutWr, &saAttr, 0) + || !SetHandleInformation(hChildStdoutRd, HANDLE_FLAG_INHERIT, 0)) + { + emsg(_(e_cannot_create_pipes)); + goto done; + } + + // Set up stdin from infile if provided. + if (infile != NULL) + { + WCHAR *winfile = enc_to_utf16(infile, NULL); + + if (winfile != NULL) + { + hChildStdinRd = CreateFileW(winfile, GENERIC_READ, + FILE_SHARE_READ, &saAttr, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, NULL); + vim_free(winfile); + } + } + + ZeroMemory(&pi, sizeof(pi)); + ZeroMemory(&si, sizeof(si)); + si.cb = sizeof(si); + si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW; + si.wShowWindow = SW_HIDE; + si.hStdOutput = hChildStdoutWr; + si.hStdError = hChildStdoutWr; + si.hStdInput = (hChildStdinRd != INVALID_HANDLE_VALUE) + ? hChildStdinRd : INVALID_HANDLE_VALUE; + + ch_log(NULL, "directly executing: %s", (char *)cmd_ga.ga_data); + + // Create the child process directly, without going through the shell. + if (!vim_create_process((char *)cmd_ga.ga_data, TRUE, + CREATE_DEFAULT_ERROR_MODE | CREATE_NEW_PROCESS_GROUP, + &si, &pi, NULL, NULL)) + { + semsg(_(e_invalid_argument_str), cmd_ga.ga_data); + goto done; + } + + // Close the write end of stdout pipe and stdin in the parent so that + // ReadFile() will get EOF when the child process exits. + CloseHandle(hChildStdoutWr); + hChildStdoutWr = INVALID_HANDLE_VALUE; + if (hChildStdinRd != INVALID_HANDLE_VALUE) + { + CloseHandle(hChildStdinRd); + hChildStdinRd = INVALID_HANDLE_VALUE; + } + + // Read output from child process. + for (;;) + { + char buf[4096]; + DWORD n; + + if (!ReadFile(hChildStdoutRd, buf, sizeof(buf), &n, NULL) || n == 0) + break; + if (ga_grow(&out_ga, (int)n) == OK) + { + mch_memmove((char *)out_ga.ga_data + out_ga.ga_len, buf, n); + out_ga.ga_len += (int)n; + } + } + + // Wait for child to finish and get exit code. + WaitForSingleObject(pi.hProcess, INFINITE); + GetExitCodeProcess(pi.hProcess, &exit_code); + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + + set_vim_var_nr(VV_SHELL_ERROR, (long)exit_code); + + if (out_ga.ga_len > 0) + { + buffer = alloc(out_ga.ga_len + 1); + if (buffer != NULL) + { + mch_memmove(buffer, out_ga.ga_data, out_ga.ga_len); + if (ret_len == NULL) + { + // Change NUL into SOH, otherwise the string is truncated. + for (i = 0; i < out_ga.ga_len; ++i) + if (buffer[i] == NUL) + buffer[i] = 1; + buffer[out_ga.ga_len] = NUL; + } + else + *ret_len = out_ga.ga_len; + } + } + else if (ret_len != NULL) + *ret_len = 0; + +done: + ga_clear(&cmd_ga); + ga_clear(&out_ga); + if (hChildStdoutRd != INVALID_HANDLE_VALUE) + CloseHandle(hChildStdoutRd); + if (hChildStdoutWr != INVALID_HANDLE_VALUE) + CloseHandle(hChildStdoutWr); + if (hChildStdinRd != INVALID_HANDLE_VALUE) + CloseHandle(hChildStdinRd); + return buffer; +} +# endif + void mch_job_start(char *cmd, job_T *job, jobopt_T *options) { diff --git a/src/po/vim.pot b/src/po/vim.pot index 99fecb4329..029a19449e 100644 --- a/src/po/vim.pot +++ b/src/po/vim.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Vim\n" "Report-Msgid-Bugs-To: vim-dev@vim.org\n" -"POT-Creation-Date: 2026-03-20 20:44+0800\n" +"POT-Creation-Date: 2026-03-25 21:51+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -2269,6 +2269,10 @@ msgstr "" msgid "Beep!" msgstr "" +#, c-format +msgid "Executing directly: \"%s\"" +msgstr "" + #, c-format msgid "Calling shell to execute: \"%s\"" msgstr "" @@ -8849,6 +8853,9 @@ msgstr "" msgid "E1574: gethostbyname(): cannot resolve hostname in channel_listen()" msgstr "" +msgid "E1575: Cannot create pipes" +msgstr "" + #. type of cmdline window or 0 #. result of cmdline window or 0 #. buffer of cmdline window or NULL diff --git a/src/proto/os_unix.pro b/src/proto/os_unix.pro index 5abaae6216..7513e400f0 100644 --- a/src/proto/os_unix.pro +++ b/src/proto/os_unix.pro @@ -63,6 +63,7 @@ void mch_set_shellsize(void); void mch_new_shellsize(void); int unix_build_argv(char_u *cmd, char ***argvp, char_u **sh_tofree, char_u **shcf_tofree); int mch_call_shell(char_u *cmd, int options); +char_u *mch_get_cmd_output_direct(char **argv, char_u *infile, int flags, int *ret_len); void mch_job_start(char **argv, job_T *job, jobopt_T *options, int is_terminal); char *mch_job_status(job_T *job); job_T *mch_detect_ended_job(job_T *job_list); diff --git a/src/proto/os_win32.pro b/src/proto/os_win32.pro index c84e308acb..3f26388b25 100644 --- a/src/proto/os_win32.pro +++ b/src/proto/os_win32.pro @@ -50,6 +50,7 @@ void mch_new_shellsize(void); void mch_set_winsize_now(void); int mch_call_shell(char_u *cmd, int options); void win32_build_env(dict_T *env, garray_T *gap, int is_terminal); +char_u *mch_get_cmd_output_direct(char **argv, char_u *infile, int flags, int *ret_len); void mch_job_start(char *cmd, job_T *job, jobopt_T *options); char *mch_job_status(job_T *job); job_T *mch_detect_ended_job(job_T *job_list); diff --git a/src/testdir/test_system.vim b/src/testdir/test_system.vim index 3eb9508608..1b1d024006 100644 --- a/src/testdir/test_system.vim +++ b/src/testdir/test_system.vim @@ -210,4 +210,47 @@ func Test_system_with_powershell() endtry endfunc +func Test_system_list_arg() + CheckExecutable python3 + + " When the command is a List, it is executed directly without the shell. + " Shell meta characters should not be interpreted but passed as-is. + + " Redirect characters should be passed literally. + let out = system(['python3', '-c', 'import sys; print(sys.argv[1])', '']) + call assert_match('^', out) + + " Environment variable syntax should not be expanded. + if has('win32') + let out = system(['python3', '-c', 'import sys; print(sys.argv[1])', '%USERPROFILE%']) + call assert_match('^%USERPROFILE%', out) + else + let out = system(['python3', '-c', 'import sys; print(sys.argv[1])', '$HOME']) + call assert_match('^\$HOME', out) + endif + + " Spaces in arguments should be preserved without shell word splitting. + let out = system(['python3', '-c', 'import sys; print(sys.argv[1])', 'hello world']) + call assert_match('^hello world', out) + + " Pipe and ampersand should be passed literally. + let out = system(['python3', '-c', 'import sys; print(sys.argv[1])', 'a&b|c']) + call assert_match('^a&b|c', out) + + " systemlist() should work too. + let out = systemlist(['python3', '-c', 'print("line1"); print("line2")']) + call assert_match('^line1', out[0]) + call assert_match('^line2', out[1]) + + " v:shell_error should be set. + call system(['python3', '-c', 'import sys; sys.exit(42)']) + call assert_equal(42, v:shell_error) + call system(['python3', '-c', 'import sys; sys.exit(0)']) + call assert_equal(0, v:shell_error) + + " Invalid arguments. + call assert_fails('call system([])', 'E474:') + call assert_fails('call systemlist([])', 'E474:') +endfunc + " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/testdir/test_vim9_builtin.vim b/src/testdir/test_vim9_builtin.vim index d3effd8342..4cc5656289 100644 --- a/src/testdir/test_vim9_builtin.vim +++ b/src/testdir/test_vim9_builtin.vim @@ -4658,13 +4658,13 @@ def Test_synstack() enddef def Test_system() - v9.CheckSourceDefAndScriptFailure(['system(1)'], ['E1013: Argument 1: type mismatch, expected string but got number', 'E1174: String required for argument 1']) + v9.CheckSourceDefAndScriptFailure(['system(1)'], ['E1013: Argument 1: type mismatch, expected string but got number', 'E1222: String or List required for argument 1']) v9.CheckSourceDefAndScriptFailure(['system("a", {})'], ['E1013: Argument 2: type mismatch, expected string but got dict', 'E1224: String, Number or List required for argument 2']) assert_equal("123\n", system('echo 123')) enddef def Test_systemlist() - v9.CheckSourceDefAndScriptFailure(['systemlist(1)'], ['E1013: Argument 1: type mismatch, expected string but got number', 'E1174: String required for argument 1']) + v9.CheckSourceDefAndScriptFailure(['systemlist(1)'], ['E1013: Argument 1: type mismatch, expected string but got number', 'E1222: String or List required for argument 1']) v9.CheckSourceDefAndScriptFailure(['systemlist("a", {})'], ['E1013: Argument 2: type mismatch, expected string but got dict', 'E1224: String, Number or List required for argument 2']) if has('win32') call assert_equal(["123\r"], systemlist('echo 123')) diff --git a/src/version.c b/src/version.c index f25d3cbb69..f761cbd30c 100644 --- a/src/version.c +++ b/src/version.c @@ -734,6 +734,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 250, /**/ 249, /**/