patch 9.2.0250: system() does not support bypassing the shell

Commit: 
https://github.com/vim/vim/commit/30f012d8bcf3d1cb19410ab8ca20523b1716539d
Author: Yasuhiro Matsumoto <[email protected]>
Date:   Wed Mar 25 21:48:36 2026 +0000

    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 <[email protected]>
    Signed-off-by: Christian Brabandt <[email protected]>

diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt
index 250c07b62..30894b96d 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 77b19ec7f..f460c3b1f 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 ed58dae14..8045698a3 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 016b917bd..fba5b93f1 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 d9f0017e6..de6975a0d 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 ca913f930..8a951298b 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('
', 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 <CR><NL> into <NL>
        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 91bfd63d0..1d382f7f3 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(_("
Cannot fork
"));
+       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 e24c40cf3..3d5b095c3 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] == '        ' || 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 99fecb432..029a19449 100644
--- a/src/po/vim.pot
+++ b/src/po/vim.pot
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: Vim
"
 "Report-Msgid-Bugs-To: [email protected]
"
-"POT-Creation-Date: 2026-03-20 20:44+0800
"
+"POT-Creation-Date: 2026-03-25 21:51+0000
"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE
"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>
"
 "Language-Team: LANGUAGE <[email protected]>
"
@@ -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 5abaae621..7513e400f 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 c84e308ac..3f26388b2 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 3eb950860..1b1d02400 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])', 
'<foo>'])
+  call assert_match('^<foo>', 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 d3effd834..4cc565628 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<any>', 'E1224: String, Number or 
List required for argument 2'])
   assert_equal("123
", 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<any>', 'E1224: String, Number or 
List required for argument 2'])
   if has('win32')
     call assert_equal(["123
"], systemlist('echo 123'))
diff --git a/src/version.c b/src/version.c
index f25d3cbb6..f761cbd30 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,
 /**/

-- 
-- 
You received this message from the "vim_dev" maillist.
Do not top-post! Type your reply below the text you are replying to.
For more information, visit http://www.vim.org/maillist.php

--- 
You received this message because you are subscribed to the Google Groups 
"vim_dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion visit 
https://groups.google.com/d/msgid/vim_dev/E1w5WVE-005vlv-8R%40256bit.org.

Raspunde prin e-mail lui