mirror of
https://github.com/neovim/neovim.git
synced 2026-06-15 16:23:48 +00:00
fix(option): set 'shell…' options based on detected shell #40031
Problem: * 'shellcmdflag' states that its default value is set according to the value of 'shell', but this behavior is not yet implemented on Windows. The same applies to 'shellpipe', 'shellredir', and 'shellxquote'. * On Windows, Git is often installed in paths containing spaces, and we still do not correctly resolve the sh executable name as described in 'shell'. * On Windows, the default value of 'shellslash' is always `false`, which causes Unix-like shells to interpret `\` in paths returned by some functions as escape charaters. Solution: Use a simple rule table to detect common shells (e.g. `cmd`, `powershell`, shells whose names contain `csh` or `sh`) and apply best-effort defaults, while leaving more complex scenarios to user configuration.
This commit is contained in:
@@ -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
|
||||
|
||||
24
runtime/lua/vim/_meta/options.gen.lua
generated
24
runtime/lua/vim/_meta/options.gen.lua
generated
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
78
test/functional/options/shell_spec.lua
Normal file
78
test/functional/options/shell_spec.lua
Normal file
@@ -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)
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user