diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index 584a12ce1c..ca9c2e8b35 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -5620,7 +5620,8 @@ A jump table for the options with a short description can be found at |Q_op|. < *'shellcmdflag'* *'shcf'* -'shellcmdflag' 'shcf' string (default "-c"; Windows: "/s /c") +'shellcmdflag' 'shcf' string (default "-c"; Windows, when 'shell' + contains "cmd" somewhere: "/s /c") global Disallowed in |modeline|. |no-modeline-option| Flag passed to the shell to execute "!" and ":!" commands; e.g., @@ -5646,12 +5647,12 @@ A jump table for the options with a short description can be found at |Q_op|. For MS-Windows the default is "2>&1| tee". The stdout and stderr are saved in a file and echoed to the screen. For Unix the default is "| tee". The stdout of the compiler is saved - in a file and echoed to the screen. If the 'shell' option is "csh" or - "tcsh" after initializations, the default becomes "|& tee". If the - 'shell' option is "sh", "ksh", "mksh", "pdksh", "zsh", "zsh-beta", - "bash", "fish", "ash" or "dash" the default becomes "2>&1| tee". This - means that stderr is also included. Before using the 'shell' option a - path is removed, thus "/bin/sh" uses "sh". + in a file and echoed to the screen. If the 'shell' option contains + "csh" (e.g. "tcsh") after initializations, the default becomes + "|& tee". Otherwise, if it contains "sh" (e.g. "bash", "zsh"), the + default becomes "2>&1| tee". This means that stderr is also included. + Before using the 'shell' option a path is removed, thus "/bin/sh" uses + "sh". The initialization of this option is done after reading the vimrc and the other initializations, so that when the 'shell' option is set there, the 'shellpipe' option changes automatically, unless it was @@ -5693,12 +5694,12 @@ A jump table for the options with a short description can be found at |Q_op|. The name of the temporary file can be represented by "%s" if necessary (the file name is appended automatically if no %s appears in the value of this option). - The default is ">". For Unix, if the 'shell' option is "csh" or - "tcsh" during initializations, the default becomes ">&". If the - 'shell' option is "sh", "ksh", "mksh", "pdksh", "zsh", "zsh-beta", - "bash" or "fish", the default becomes ">%s 2>&1". This means that - stderr is also included. For Win32, the Unix checks are done and - additionally "cmd" is checked for, which makes the default ">%s 2>&1". + The default is ">". For Unix, if the 'shell' option contains "csh" + (e.g. "tcsh") during initializations, the default becomes ">&". + Otherwise, if it contains "sh" (e.g. "bash", "zsh"), the default + becomes ">%s 2>&1". This means that stderr is also included. For + Win32, the Unix checks are done and additionally "cmd" is checked + for, which makes the default ">%s 2>&1". Also, the same names with ".exe" appended are checked for. The initialization of this option is done after reading the vimrc and the other initializations, so that when the 'shell' option is set @@ -5710,7 +5711,8 @@ A jump table for the options with a short description can be found at |Q_op|. Only a single "%s" item is allowed in the option value. *'shellslash'* *'ssl'* *'noshellslash'* *'nossl'* -'shellslash' 'ssl' boolean (default on, Windows: off) +'shellslash' 'ssl' boolean (default on; Windows: off, except when 'shell' + contains "sh" somewhere) global only modifiable in MS-Windows When set, a forward slash is used when expanding file names. This is @@ -5748,7 +5750,8 @@ A jump table for the options with a short description can be found at |Q_op|. to execute most external commands with cmd.exe. *'shellxquote'* *'sxq'* -'shellxquote' 'sxq' string (default "", Windows: "\"") +'shellxquote' 'sxq' string (default ""; Windows, when 'shell' + contains "cmd" somewhere: "\"") global Disallowed in |modeline|. |no-modeline-option| Quoting character(s), put around the command passed to the shell, for diff --git a/runtime/lua/vim/_meta/options.gen.lua b/runtime/lua/vim/_meta/options.gen.lua index 3e2e24e7fc..0895207bd2 100644 --- a/runtime/lua/vim/_meta/options.gen.lua +++ b/runtime/lua/vim/_meta/options.gen.lua @@ -5914,12 +5914,12 @@ vim.go.shcf = vim.go.shellcmdflag --- For MS-Windows the default is "2>&1| tee". The stdout and stderr are --- saved in a file and echoed to the screen. --- For Unix the default is "| tee". The stdout of the compiler is saved ---- in a file and echoed to the screen. If the 'shell' option is "csh" or ---- "tcsh" after initializations, the default becomes "|& tee". If the ---- 'shell' option is "sh", "ksh", "mksh", "pdksh", "zsh", "zsh-beta", ---- "bash", "fish", "ash" or "dash" the default becomes "2>&1| tee". This ---- means that stderr is also included. Before using the 'shell' option a ---- path is removed, thus "/bin/sh" uses "sh". +--- in a file and echoed to the screen. If the 'shell' option contains +--- "csh" (e.g. "tcsh") after initializations, the default becomes +--- "|& tee". Otherwise, if it contains "sh" (e.g. "bash", "zsh"), the +--- default becomes "2>&1| tee". This means that stderr is also included. +--- Before using the 'shell' option a path is removed, thus "/bin/sh" uses +--- "sh". --- The initialization of this option is done after reading the vimrc --- and the other initializations, so that when the 'shell' option is set --- there, the 'shellpipe' option changes automatically, unless it was @@ -5964,12 +5964,12 @@ vim.go.shq = vim.go.shellquote --- The name of the temporary file can be represented by "%s" if necessary --- (the file name is appended automatically if no %s appears in the value --- of this option). ---- The default is ">". For Unix, if the 'shell' option is "csh" or ---- "tcsh" during initializations, the default becomes ">&". If the ---- 'shell' option is "sh", "ksh", "mksh", "pdksh", "zsh", "zsh-beta", ---- "bash" or "fish", the default becomes ">%s 2>&1". This means that ---- stderr is also included. For Win32, the Unix checks are done and ---- additionally "cmd" is checked for, which makes the default ">%s 2>&1". +--- The default is ">". For Unix, if the 'shell' option contains "csh" +--- (e.g. "tcsh") during initializations, the default becomes ">&". +--- Otherwise, if it contains "sh" (e.g. "bash", "zsh"), the default +--- becomes ">%s 2>&1". This means that stderr is also included. For +--- Win32, the Unix checks are done and additionally "cmd" is checked +--- for, which makes the default ">%s 2>&1". --- Also, the same names with ".exe" appended are checked for. --- The initialization of this option is done after reading the vimrc --- and the other initializations, so that when the 'shell' option is set diff --git a/src/nvim/option.c b/src/nvim/option.c index b568ec2530..246abecec0 100644 --- a/src/nvim/option.c +++ b/src/nvim/option.c @@ -626,44 +626,57 @@ void set_init_2(bool headless) change_option_default(kOptWindow, NUMBER_OPTVAL(Rows - 1)); } +static const struct { + const char *pat; + const char *shcf; + const char *sp; + const char *srr; + const char *sxq; +} shell_rules[] = { +#ifdef MSWIN + { "cmd", "/s /c", NULL, NULL, "\"" }, + { "powershell", "-Command", NULL, NULL, NULL }, +#endif + { "csh", NULL, "|& tee", ">&", NULL }, + { "sh", NULL, "2>&1| tee", ">%s 2>&1", NULL }, +}; + +static void change_option_and_default_if_unset(OptIndex idx, const char *val) +{ + if (val == NULL || options[idx].flags & kOptFlagWasSet) { + return; + } + OptVal optval = CSTR_AS_OPTVAL(val); + set_option_direct(idx, optval, 0, SID_NONE); + change_option_default(idx, optval_copy(optval)); +} + /// Initialize the options, part three: After reading the .vimrc void set_init_3(void) { parse_shape_opt(SHAPE_CURSOR); // set cursor shapes from 'guicursor' - // Set 'shellpipe' and 'shellredir', depending on the 'shell' option. - // This is done after other initializations, where 'shell' might have been - // set, but only if they have not been set before. - bool do_srr = !(options[kOptShellredir].flags & kOptFlagWasSet); - bool do_sp = !(options[kOptShellpipe].flags & kOptFlagWasSet); - - size_t len = 0; - char *p = (char *)invocation_path_tail(p_sh, &len); - p = xmemdupz(p, len); - - bool is_csh = path_fnamecmp(p, "csh") == 0 || path_fnamecmp(p, "tcsh") == 0; - bool is_known_shell = path_fnamecmp(p, "sh") == 0 || path_fnamecmp(p, "ksh") == 0 - || path_fnamecmp(p, "mksh") == 0 || path_fnamecmp(p, "pdksh") == 0 - || path_fnamecmp(p, "zsh") == 0 || path_fnamecmp(p, "zsh-beta") == 0 - || path_fnamecmp(p, "bash") == 0 || path_fnamecmp(p, "fish") == 0 - || path_fnamecmp(p, "ash") == 0 || path_fnamecmp(p, "dash") == 0; - - // Default for p_sp is "| tee", for p_srr is ">". - // For known shells it is changed here to include stderr. - if (is_csh || is_known_shell) { - if (do_sp) { - const OptVal sp = - is_csh ? STATIC_CSTR_AS_OPTVAL("|& tee") : STATIC_CSTR_AS_OPTVAL("2>&1| tee"); - set_option_direct(kOptShellpipe, sp, 0, SID_NONE); - change_option_default(kOptShellpipe, optval_copy(sp)); + size_t len; + char name[MAXPATHL]; + const char *p = invocation_path_tail(p_sh, &len); + xmemcpyz(name, p, len); + for (size_t i = 0; i < ARRAY_SIZE(shell_rules); i++) { + if (strstr(name, shell_rules[i].pat) == NULL) { + continue; } - if (do_srr) { - const OptVal srr = is_csh ? STATIC_CSTR_AS_OPTVAL(">&") : STATIC_CSTR_AS_OPTVAL(">%s 2>&1"); - set_option_direct(kOptShellredir, srr, 0, SID_NONE); - change_option_default(kOptShellredir, optval_copy(srr)); + change_option_and_default_if_unset(kOptShellcmdflag, shell_rules[i].shcf); + change_option_and_default_if_unset(kOptShellpipe, shell_rules[i].sp); + change_option_and_default_if_unset(kOptShellredir, shell_rules[i].srr); + change_option_and_default_if_unset(kOptShellxquote, shell_rules[i].sxq); +#ifdef MSWIN + if (i > 0 && !(options[kOptShellslash].flags & kOptFlagWasSet)) { + // Use `/` as path separator on Unix-like shells or powershell on Windows + set_option_direct(kOptShellslash, BOOLEAN_OPTVAL(true), 0, SID_NONE); + change_option_default(kOptShellslash, BOOLEAN_OPTVAL(true)); } +#endif + break; } - xfree(p); if (buf_is_empty(curbuf)) { // Apply the first entry of 'fileformats' to the initial buffer. diff --git a/src/nvim/options.lua b/src/nvim/options.lua index e4b826251a..b0bdfaa21f 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -7709,10 +7709,9 @@ local options = { { abbreviation = 'shcf', defaults = { - condition = 'MSWIN', - if_false = '-c', - if_true = '/s /c', - doc = '"-c"; Windows: "/s /c"', + if_true = '-c', + doc = [["-c"; Windows, when 'shell' + contains "cmd" somewhere: "/s /c"]], }, desc = [=[ Flag passed to the shell to execute "!" and ":!" commands; e.g., @@ -7751,12 +7750,12 @@ local options = { For MS-Windows the default is "2>&1| tee". The stdout and stderr are saved in a file and echoed to the screen. For Unix the default is "| tee". The stdout of the compiler is saved - in a file and echoed to the screen. If the 'shell' option is "csh" or - "tcsh" after initializations, the default becomes "|& tee". If the - 'shell' option is "sh", "ksh", "mksh", "pdksh", "zsh", "zsh-beta", - "bash", "fish", "ash" or "dash" the default becomes "2>&1| tee". This - means that stderr is also included. Before using the 'shell' option a - path is removed, thus "/bin/sh" uses "sh". + in a file and echoed to the screen. If the 'shell' option contains + "csh" (e.g. "tcsh") after initializations, the default becomes + "|& tee". Otherwise, if it contains "sh" (e.g. "bash", "zsh"), the + default becomes "2>&1| tee". This means that stderr is also included. + Before using the 'shell' option a path is removed, thus "/bin/sh" uses + "sh". The initialization of this option is done after reading the vimrc and the other initializations, so that when the 'shell' option is set there, the 'shellpipe' option changes automatically, unless it was @@ -7821,12 +7820,12 @@ local options = { The name of the temporary file can be represented by "%s" if necessary (the file name is appended automatically if no %s appears in the value of this option). - The default is ">". For Unix, if the 'shell' option is "csh" or - "tcsh" during initializations, the default becomes ">&". If the - 'shell' option is "sh", "ksh", "mksh", "pdksh", "zsh", "zsh-beta", - "bash" or "fish", the default becomes ">%s 2>&1". This means that - stderr is also included. For Win32, the Unix checks are done and - additionally "cmd" is checked for, which makes the default ">%s 2>&1". + The default is ">". For Unix, if the 'shell' option contains "csh" + (e.g. "tcsh") during initializations, the default becomes ">&". + Otherwise, if it contains "sh" (e.g. "bash", "zsh"), the default + becomes ">%s 2>&1". This means that stderr is also included. For + Win32, the Unix checks are done and additionally "cmd" is checked + for, which makes the default ">%s 2>&1". Also, the same names with ".exe" appended are checked for. The initialization of this option is done after reading the vimrc and the other initializations, so that when the 'shell' option is set @@ -7851,7 +7850,8 @@ local options = { condition = 'MSWIN', if_true = false, if_false = true, - doc = 'on, Windows: off', + doc = [[on; Windows: off, except when 'shell' + contains "sh" somewhere]], }, desc = [=[ only modifiable in MS-Windows @@ -7913,10 +7913,9 @@ local options = { { abbreviation = 'sxq', defaults = { - condition = 'MSWIN', - if_false = '', - if_true = '"', - doc = '"", Windows: "\\""', + if_true = '', + doc = [[""; Windows, when 'shell' + contains "cmd" somewhere: "\""]], }, desc = [=[ Quoting character(s), put around the command passed to the shell, for diff --git a/src/nvim/path.c b/src/nvim/path.c index 4bc0b3682b..93abe8945d 100644 --- a/src/nvim/path.c +++ b/src/nvim/path.c @@ -139,10 +139,24 @@ char *path_tail_with_sep(char *fname) return tail; } -/// Finds the path tail (or executable) in an invocation. +/// Finds the executable name (path tail) in a program invocation. /// -/// @param[in] invocation A program invocation in the form: -/// "path/to/exe [args]". +/// The invocation starts with an executable path, optionally followed +/// by arguments. +/// +/// Parsing rules: +/// - A space outside double quotes ends the executable path. +/// - Within quoted segments, a backslash skips the following character. +/// Note: on Windows, `\` is treated firstly as a path separator. In +/// practice, this rule should be rarely needed anyway. +/// +/// Examples: +/// - "path/foo/bash --login" => "bash" +/// - "path/foo bar/bash --login" => "foo" +/// - "\"path/foo bar/bash\" --login" => "bash" +/// - "\"path/foo\\\" bar/bash\" --login" => "bash" +/// +/// @param[in] invocation Program invocation string. /// @param[out] len Stores the length of the executable name. /// /// @post if `len` is not null, stores the length of the executable name. @@ -152,17 +166,25 @@ const char *invocation_path_tail(const char *invocation, size_t *len) FUNC_ATTR_NONNULL_RET FUNC_ATTR_NONNULL_ARG(1) { const char *tail = get_past_head(invocation); + const char *tail_end = tail; const char *p = tail; - while (*p != NUL && *p != ' ') { - bool was_sep = vim_ispathsep_nocolon(*p); - MB_PTR_ADV(p); - if (was_sep) { - tail = p; // Now tail points one past the separator. + bool inquote = false; + while (*p != NUL && (inquote || *p != ' ')) { + int l = utfc_ptr2len(p); + if (vim_ispathsep_nocolon(*p)) { + tail = p + 1; // Now tail points one past the separator. + } else if (*p == '\\' && inquote) { + p++; + } else if (*p == '"') { + inquote ^= 1; + } else { + tail_end = p + l; } + p += l; } if (len != NULL) { - *len = (size_t)(p - tail); + *len = (size_t)(tail_end - tail); } return tail; diff --git a/test/functional/options/shell_spec.lua b/test/functional/options/shell_spec.lua new file mode 100644 index 0000000000..dd6b8c054c --- /dev/null +++ b/test/functional/options/shell_spec.lua @@ -0,0 +1,78 @@ +local t = require('test.testutil') +local n = require('test.functional.testnvim')() + +local eq = t.eq +local is_os = t.is_os +local skip = t.skip +local api = n.api +local clear = n.clear + +describe('applies sensible default options for different shells #28384', function() + ---@param sh string + ---@param shcf string + ---@param sp string + ---@param srr string + ---@param sxq string + ---@param ssl boolean + local function expect(sh, shcf, sp, srr, sxq, ssl) + local opt = { sh = sh, shcf = shcf, sp = sp, srr = srr, sxq = sxq, ssl = ssl } + for k, v in pairs(opt) do + eq(v, api.nvim_get_option_info2(k, {}).default) + end + end + it('cmd.exe', function() + skip(not is_os('win'), 'N/A: only works on Windows') + clear() + expect('cmd.exe', '/s /c', '2>&1| tee', '>%s 2>&1', '"', false) + end) + + it('powershell(PowerShell 5.x)', function() + t.skip(not is_os('win'), 'N/A: only works on Windows') + clear { + env = { SHELL = 'powershell' }, + } + expect('powershell', '-Command', '2>&1| tee', '>%s 2>&1', '', true) + end) + + it('pwsh(PowerShell 7.x)', function() + clear { + env = { SHELL = 'pwsh' }, + } + expect('pwsh', '-c', '2>&1| tee', '>%s 2>&1', '', true) + end) + + it('csh', function() + clear { + env = { SHELL = 'csh' }, + } + expect('csh', '-c', '|& tee', '>&', '', true) + end) + + it('bash', function() + clear { + env = { SHELL = 'bash' }, + } + expect('bash', '-c', '2>&1| tee', '>%s 2>&1', '', true) + end) + + it('unknown', function() + clear { + env = { SHELL = 'unknown' }, + } + expect( + 'unknown', + '-c', + not is_os('win') and '| tee' or '2>&1| tee', + not is_os('win') and '>' or '>%s 2>&1', + '', + not is_os('win') and true or false + ) + end) + + it('even if the path contains spaces', function() + clear { + env = { SHELL = ('%s/foo bar/bash'):format(n.nvim_dir) }, + } + expect(('"%s/foo bar/bash"'):format(n.nvim_dir), '-c', '2>&1| tee', '>%s 2>&1', '', true) + end) +end) diff --git a/test/old/testdir/test_plugin_netrw.vim b/test/old/testdir/test_plugin_netrw.vim index 5e7304fc64..ba9a2778eb 100644 --- a/test/old/testdir/test_plugin_netrw.vim +++ b/test/old/testdir/test_plugin_netrw.vim @@ -230,7 +230,7 @@ func SetShell(shell) if has("win32") " Nvim: default 'shell' is "sh" due to $SHELL being set in Makefile, " but here 'shell' should be cmd.exe. - set shell=cmd.exe + set shell=cmd.exe shellcmdflag=/s\ /c endif elseif a:shell == "powershell" " help dos-powershell " powershell desktop is windows only