mirror of
https://github.com/neovim/neovim.git
synced 2025-02-25 18:55:25 -06:00
Merge pull request #7202 from teto/jobstart_env
[RFC] override environment for jobstart
This commit is contained in:
commit
9f3d483c79
@ -5451,6 +5451,9 @@ jobstart({cmd}[, {opts}]) *jobstart()*
|
||||
|on_exit| : exit event handler (function name or |Funcref|)
|
||||
cwd : Working directory of the job; defaults to
|
||||
|current-directory|.
|
||||
env : A dict of strings to append (or replace see
|
||||
|clear_env|) to the current environment.
|
||||
clear_env: If set, use the exact values passed in |env|
|
||||
rpc : If set, |msgpack-rpc| will be used to communicate
|
||||
with the job over stdin and stdout. "on_stdout" is
|
||||
then ignored, but "on_stderr" can still be used.
|
||||
|
@ -272,11 +272,35 @@ static void close_cb(Stream *stream, void *data)
|
||||
channel_decref(data);
|
||||
}
|
||||
|
||||
|
||||
/// Starts a job and returns the associated channel
|
||||
///
|
||||
/// @param[in] argv Arguments vector specifying the command to run,
|
||||
/// NULL-terminated
|
||||
/// @param[in] on_stdout Callback to read the job's stdout
|
||||
/// @param[in] on_stderr Callback to read the job's stderr
|
||||
/// @param[in] on_exit Callback to receive the job's exit status
|
||||
/// @param[in] pty True if the job should run attached to a pty
|
||||
/// @param[in] rpc True to communicate with the job using msgpack-rpc,
|
||||
/// `on_stdout` is ignored
|
||||
/// @param[in] detach True if the job should not be killed when nvim exits,
|
||||
/// ignored if `pty` is true
|
||||
/// @param[in] cwd Initial working directory for the job. Nvim's working
|
||||
/// directory if `cwd` is NULL
|
||||
/// @param[in] pty_width Width of the pty, ignored if `pty` is false
|
||||
/// @param[in] pty_height Height of the pty, ignored if `pty` is false
|
||||
/// @param[in] term_name `$TERM` for the pty
|
||||
/// @param[in] env Nvim's configured environment is used if this is NULL,
|
||||
/// otherwise defines all environment variables
|
||||
/// @param[out] status_out 0 for invalid arguments, > 0 for the channel id,
|
||||
/// < 0 if the job can't start
|
||||
///
|
||||
/// @returns [allocated] channel
|
||||
Channel *channel_job_start(char **argv, CallbackReader on_stdout,
|
||||
CallbackReader on_stderr, Callback on_exit,
|
||||
bool pty, bool rpc, bool detach, const char *cwd,
|
||||
uint16_t pty_width, uint16_t pty_height,
|
||||
char *term_name, varnumber_T *status_out)
|
||||
char *term_name, char **env, varnumber_T *status_out)
|
||||
{
|
||||
assert(cwd == NULL || os_isdir_executable(cwd));
|
||||
|
||||
@ -314,6 +338,7 @@ Channel *channel_job_start(char **argv, CallbackReader on_stdout,
|
||||
proc->events = chan->events;
|
||||
proc->detach = detach;
|
||||
proc->cwd = cwd;
|
||||
proc->env = env;
|
||||
|
||||
char *cmd = xstrdup(proc->argv[0]);
|
||||
bool has_out, has_err;
|
||||
@ -328,6 +353,7 @@ Channel *channel_job_start(char **argv, CallbackReader on_stdout,
|
||||
if (status) {
|
||||
EMSG3(_(e_jobspawn), os_strerror(status), cmd);
|
||||
xfree(cmd);
|
||||
os_free_fullenv(proc->env);
|
||||
if (proc->type == kProcessTypePty) {
|
||||
xfree(chan->stream.pty.term_name);
|
||||
}
|
||||
@ -336,6 +362,8 @@ Channel *channel_job_start(char **argv, CallbackReader on_stdout,
|
||||
return NULL;
|
||||
}
|
||||
xfree(cmd);
|
||||
os_free_fullenv(proc->env);
|
||||
|
||||
|
||||
wstream_init(&proc->in, 0);
|
||||
if (has_out) {
|
||||
|
@ -8716,18 +8716,25 @@ static void f_environ(typval_T *argvars, typval_T *rettv, FunPtr fptr)
|
||||
{
|
||||
tv_dict_alloc_ret(rettv);
|
||||
|
||||
for (int i = 0; ; i++) {
|
||||
// TODO(justinmk): use os_copyfullenv from #7202 ?
|
||||
char *envname = os_getenvname_at_index((size_t)i);
|
||||
if (envname == NULL) {
|
||||
break;
|
||||
}
|
||||
const char *value = os_getenv(envname);
|
||||
size_t env_size = os_get_fullenv_size();
|
||||
char **env = xmalloc(sizeof(*env) * (env_size + 1));
|
||||
env[env_size] = NULL;
|
||||
|
||||
os_copy_fullenv(env, env_size);
|
||||
|
||||
for (size_t i = 0; i < env_size; i++) {
|
||||
const char * str = env[i];
|
||||
const char * const end = strchr(str + (str[0] == '=' ? 1 : 0),
|
||||
'=');
|
||||
assert(end != NULL);
|
||||
ptrdiff_t len = end - str;
|
||||
assert(len > 0);
|
||||
const char * value = str + len + 1;
|
||||
tv_dict_add_str(rettv->vval.v_dict,
|
||||
(char *)envname, STRLEN((char *)envname),
|
||||
value == NULL ? "" : value);
|
||||
xfree(envname);
|
||||
str, len,
|
||||
value);
|
||||
}
|
||||
os_free_fullenv(env);
|
||||
}
|
||||
|
||||
/*
|
||||
@ -12577,6 +12584,7 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv, FunPtr fptr)
|
||||
|
||||
bool executable = true;
|
||||
char **argv = tv_to_argv(&argvars[0], NULL, &executable);
|
||||
char **env = NULL;
|
||||
if (!argv) {
|
||||
rettv->vval.v_number = executable ? 0 : -1;
|
||||
return; // Did error message in tv_to_argv.
|
||||
@ -12594,6 +12602,7 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv, FunPtr fptr)
|
||||
bool detach = false;
|
||||
bool rpc = false;
|
||||
bool pty = false;
|
||||
bool clear_env = false;
|
||||
CallbackReader on_stdout = CALLBACK_READER_INIT,
|
||||
on_stderr = CALLBACK_READER_INIT;
|
||||
Callback on_exit = CALLBACK_NONE;
|
||||
@ -12604,6 +12613,7 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv, FunPtr fptr)
|
||||
detach = tv_dict_get_number(job_opts, "detach") != 0;
|
||||
rpc = tv_dict_get_number(job_opts, "rpc") != 0;
|
||||
pty = tv_dict_get_number(job_opts, "pty") != 0;
|
||||
clear_env = tv_dict_get_number(job_opts, "clear_env") != 0;
|
||||
if (pty && rpc) {
|
||||
EMSG2(_(e_invarg2), "job cannot have both 'pty' and 'rpc' options set");
|
||||
shell_free_argv(argv);
|
||||
@ -12620,6 +12630,45 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv, FunPtr fptr)
|
||||
return;
|
||||
}
|
||||
}
|
||||
dictitem_T *job_env = tv_dict_find(job_opts, S_LEN("env"));
|
||||
if (job_env) {
|
||||
if (job_env->di_tv.v_type != VAR_DICT) {
|
||||
EMSG2(_(e_invarg2), "env");
|
||||
shell_free_argv(argv);
|
||||
return;
|
||||
}
|
||||
|
||||
size_t custom_env_size = (size_t)tv_dict_len(job_env->di_tv.vval.v_dict);
|
||||
size_t i = 0;
|
||||
size_t env_size = 0;
|
||||
|
||||
if (clear_env) {
|
||||
// + 1 for last null entry
|
||||
env = xmalloc((custom_env_size + 1) * sizeof(*env));
|
||||
env_size = 0;
|
||||
} else {
|
||||
env_size = os_get_fullenv_size();
|
||||
|
||||
env = xmalloc((custom_env_size + env_size + 1) * sizeof(*env));
|
||||
|
||||
os_copy_fullenv(env, env_size);
|
||||
i = env_size;
|
||||
}
|
||||
assert(env); // env must be allocated at this point
|
||||
|
||||
TV_DICT_ITER(job_env->di_tv.vval.v_dict, var, {
|
||||
const char *str = tv_get_string(&var->di_tv);
|
||||
assert(str);
|
||||
size_t len = STRLEN(var->di_key) + strlen(str) + strlen("=") + 1;
|
||||
env[i] = xmalloc(len);
|
||||
snprintf(env[i], len, "%s=%s", (char *)var->di_key, str);
|
||||
i++;
|
||||
});
|
||||
|
||||
// must be null terminated
|
||||
env[env_size + custom_env_size] = NULL;
|
||||
}
|
||||
|
||||
|
||||
if (!common_job_callbacks(job_opts, &on_stdout, &on_stderr, &on_exit)) {
|
||||
shell_free_argv(argv);
|
||||
@ -12637,8 +12686,8 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv, FunPtr fptr)
|
||||
}
|
||||
|
||||
Channel *chan = channel_job_start(argv, on_stdout, on_stderr, on_exit, pty,
|
||||
rpc, detach, cwd, width, height, term_name,
|
||||
&rettv->vval.v_number);
|
||||
rpc, detach, cwd, width, height,
|
||||
term_name, env, &rettv->vval.v_number);
|
||||
if (chan) {
|
||||
channel_create_event(chan, NULL);
|
||||
}
|
||||
@ -14933,7 +14982,7 @@ static void f_rpcstart(typval_T *argvars, typval_T *rettv, FunPtr fptr)
|
||||
|
||||
Channel *chan = channel_job_start(argv, CALLBACK_READER_INIT,
|
||||
CALLBACK_READER_INIT, CALLBACK_NONE,
|
||||
false, true, false, NULL, 0, 0, NULL,
|
||||
false, true, false, NULL, 0, 0, NULL, NULL,
|
||||
&rettv->vval.v_number);
|
||||
if (chan) {
|
||||
channel_create_event(chan, NULL);
|
||||
@ -18320,7 +18369,7 @@ static void f_termopen(typval_T *argvars, typval_T *rettv, FunPtr fptr)
|
||||
Channel *chan = channel_job_start(argv, on_stdout, on_stderr, on_exit,
|
||||
true, false, false, cwd,
|
||||
term_width, curwin->w_height_inner,
|
||||
xstrdup("xterm-256color"),
|
||||
xstrdup("xterm-256color"), NULL,
|
||||
&rettv->vval.v_number);
|
||||
if (rettv->vval.v_number <= 0) {
|
||||
return;
|
||||
|
@ -41,7 +41,7 @@ int libuv_process_spawn(LibuvProcess *uvproc)
|
||||
#endif
|
||||
uvproc->uvopts.exit_cb = exit_cb;
|
||||
uvproc->uvopts.cwd = proc->cwd;
|
||||
uvproc->uvopts.env = NULL; // Inherits the parent (nvim) env.
|
||||
uvproc->uvopts.env = proc->env;
|
||||
uvproc->uvopts.stdio = uvproc->uvstdio;
|
||||
uvproc->uvopts.stdio_count = 3;
|
||||
uvproc->uvstdio[0].flags = UV_IGNORE;
|
||||
|
@ -23,6 +23,7 @@ struct process {
|
||||
uint64_t stopped_time; // process_stop() timestamp
|
||||
const char *cwd;
|
||||
char **argv;
|
||||
char **env;
|
||||
Stream in, out, err;
|
||||
process_exit_cb cb;
|
||||
internal_process_cb internal_exit_cb, internal_close_cb;
|
||||
|
@ -184,39 +184,142 @@ int os_unsetenv(const char *name)
|
||||
return r == 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
char *os_getenvname_at_index(size_t index)
|
||||
/// Returns number of variables in the current environment variables block
|
||||
size_t os_get_fullenv_size(void)
|
||||
{
|
||||
size_t len = 0;
|
||||
#ifdef _WIN32
|
||||
wchar_t *env = GetEnvironmentStringsW();
|
||||
if (!env) {
|
||||
return NULL;
|
||||
wchar_t *envstrings = GetEnvironmentStringsW();
|
||||
wchar_t *p = envstrings;
|
||||
size_t l;
|
||||
if (!envstrings) {
|
||||
return len;
|
||||
}
|
||||
char *name = NULL;
|
||||
size_t current_index = 0;
|
||||
// GetEnvironmentStringsW() result has this format:
|
||||
// var1=value1\0var2=value2\0...varN=valueN\0\0
|
||||
for (wchar_t *it = env; *it != L'\0' || *(it + 1) != L'\0'; it++) {
|
||||
if (index == current_index) {
|
||||
while ((l = wcslen(p)) != 0) {
|
||||
p += l + 1;
|
||||
len++;
|
||||
}
|
||||
|
||||
FreeEnvironmentStringsW(envstrings);
|
||||
#else
|
||||
# if defined(HAVE__NSGETENVIRON)
|
||||
char **environ = *_NSGetEnviron();
|
||||
# else
|
||||
extern char **environ;
|
||||
# endif
|
||||
|
||||
while (environ[len] != NULL) {
|
||||
len++;
|
||||
}
|
||||
|
||||
#endif
|
||||
return len;
|
||||
}
|
||||
|
||||
void os_free_fullenv(char **env)
|
||||
{
|
||||
if (!env) { return; }
|
||||
for (char **it = env; *it; it++) {
|
||||
XFREE_CLEAR(*it);
|
||||
}
|
||||
xfree(env);
|
||||
}
|
||||
|
||||
/// Copies the current environment variables into the given array, `env`. Each
|
||||
/// array element is of the form "NAME=VALUE".
|
||||
/// Result must be freed by the caller.
|
||||
///
|
||||
/// @param[out] env array to populate with environment variables
|
||||
/// @param env_size size of `env`, @see os_fullenv_size
|
||||
void os_copy_fullenv(char **env, size_t env_size)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
wchar_t *envstrings = GetEnvironmentStringsW();
|
||||
if (!envstrings) {
|
||||
return;
|
||||
}
|
||||
wchar_t *p = envstrings;
|
||||
size_t i = 0;
|
||||
size_t l;
|
||||
// GetEnvironmentStringsW() result has this format:
|
||||
// var1=value1\0var2=value2\0...varN=valueN\0\0
|
||||
while ((l = wcslen(p)) != 0 && i < env_size) {
|
||||
char *utf8_str;
|
||||
int conversion_result = utf16_to_utf8(it, -1, &utf8_str);
|
||||
int conversion_result = utf16_to_utf8(p, -1, &utf8_str);
|
||||
if (conversion_result != 0) {
|
||||
EMSG2("utf16_to_utf8 failed: %d", conversion_result);
|
||||
break;
|
||||
}
|
||||
size_t namesize = 0;
|
||||
while (utf8_str[namesize] != '=' && utf8_str[namesize] != NUL) {
|
||||
namesize++;
|
||||
p += l + 1;
|
||||
|
||||
env[i] = utf8_str;
|
||||
i++;
|
||||
}
|
||||
name = (char *)vim_strnsave((char_u *)utf8_str, namesize);
|
||||
|
||||
FreeEnvironmentStringsW(envstrings);
|
||||
#else
|
||||
# if defined(HAVE__NSGETENVIRON)
|
||||
char **environ = *_NSGetEnviron();
|
||||
# else
|
||||
extern char **environ;
|
||||
# endif
|
||||
|
||||
size_t i = 0;
|
||||
while (environ[i] != NULL && i < env_size) {
|
||||
env[i] = xstrdup(environ[i]);
|
||||
i++;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Copy value of the environment variable at `index` in the current
|
||||
/// environment variables block.
|
||||
/// Result must be freed by the caller.
|
||||
///
|
||||
/// @param index nth item in environment variables block
|
||||
/// @return [allocated] environment variable's value, or NULL
|
||||
char *os_getenvname_at_index(size_t index)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
wchar_t *envstrings = GetEnvironmentStringsW();
|
||||
if (!envstrings) {
|
||||
return NULL;
|
||||
}
|
||||
wchar_t *p = envstrings;
|
||||
char *name = NULL;
|
||||
size_t i = 0;
|
||||
size_t l;
|
||||
// GetEnvironmentStringsW() result has this format:
|
||||
// var1=value1\0var2=value2\0...varN=valueN\0\0
|
||||
while ((l = wcslen(p)) != 0 && i <= index) {
|
||||
if (i == index) {
|
||||
char *utf8_str;
|
||||
int conversion_result = utf16_to_utf8(p, -1, &utf8_str);
|
||||
if (conversion_result != 0) {
|
||||
EMSG2("utf16_to_utf8 failed: %d", conversion_result);
|
||||
break;
|
||||
}
|
||||
|
||||
// Some Windows env vars start with =, so skip over that to find the
|
||||
// separator between name/value
|
||||
const char * const end = strchr(utf8_str + (utf8_str[0] == '=' ? 1 : 0),
|
||||
'=');
|
||||
assert(end != NULL);
|
||||
ptrdiff_t len = end - utf8_str;
|
||||
assert(len > 0);
|
||||
name = xstrndup(utf8_str, (size_t)len);
|
||||
xfree(utf8_str);
|
||||
break;
|
||||
}
|
||||
if (*it == L'\0') {
|
||||
current_index++;
|
||||
}
|
||||
|
||||
// Advance past the name and NUL
|
||||
p += l + 1;
|
||||
i++;
|
||||
}
|
||||
|
||||
FreeEnvironmentStringsW(env);
|
||||
FreeEnvironmentStringsW(envstrings);
|
||||
return name;
|
||||
#else
|
||||
# if defined(HAVE__NSGETENVIRON)
|
||||
@ -224,19 +327,20 @@ char *os_getenvname_at_index(size_t index)
|
||||
# else
|
||||
extern char **environ;
|
||||
# endif
|
||||
// Check if index is inside the environ array and is not the last element.
|
||||
|
||||
// check if index is inside the environ array
|
||||
for (size_t i = 0; i <= index; i++) {
|
||||
if (environ[i] == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
char *str = environ[index];
|
||||
size_t namesize = 0;
|
||||
while (str[namesize] != '=' && str[namesize] != NUL) {
|
||||
namesize++;
|
||||
}
|
||||
char *name = (char *)vim_strnsave((char_u *)str, namesize);
|
||||
return name;
|
||||
assert(str != NULL);
|
||||
const char * const end = strchr(str, '=');
|
||||
assert(end != NULL);
|
||||
ptrdiff_t len = end - str;
|
||||
assert(len > 0);
|
||||
return xstrndup(str, (size_t)len);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -26,6 +26,7 @@ describe('jobs', function()
|
||||
|
||||
before_each(function()
|
||||
clear()
|
||||
|
||||
channel = nvim('get_api_info')[1]
|
||||
nvim('set_var', 'channel', channel)
|
||||
source([[
|
||||
@ -48,6 +49,57 @@ describe('jobs', function()
|
||||
]])
|
||||
end)
|
||||
|
||||
it('must specify env option as a dict', function()
|
||||
command("let g:job_opts.env = v:true")
|
||||
local _, err = pcall(function()
|
||||
if iswin() then
|
||||
nvim('command', "let j = jobstart('set', g:job_opts)")
|
||||
else
|
||||
nvim('command', "let j = jobstart('env', g:job_opts)")
|
||||
end
|
||||
end)
|
||||
ok(string.find(err, "E475: Invalid argument: env") ~= nil)
|
||||
end)
|
||||
|
||||
it('append environment #env', function()
|
||||
nvim('command', "let $VAR = 'abc'")
|
||||
nvim('command', "let g:job_opts.env = {'TOTO': 'hello world'}")
|
||||
if iswin() then
|
||||
nvim('command', [[call jobstart('echo %TOTO% %VAR%', g:job_opts)]])
|
||||
else
|
||||
nvim('command', [[call jobstart('echo $TOTO $VAR', g:job_opts)]])
|
||||
end
|
||||
|
||||
expect_msg_seq({
|
||||
{'notification', 'stdout', {0, {'hello world abc', ''}}},
|
||||
})
|
||||
end)
|
||||
|
||||
it('replace environment #env', function()
|
||||
nvim('command', "let $VAR = 'abc'")
|
||||
nvim('command', "let g:job_opts.env = {'TOTO': 'hello world'}")
|
||||
nvim('command', "let g:job_opts.clear_env = 1")
|
||||
|
||||
-- libuv ensures that certain "required" environment variables are
|
||||
-- preserved if the user doesn't provide them in a custom environment
|
||||
-- https://github.com/libuv/libuv/blob/635e0ce6073c5fbc96040e336b364c061441b54b/src/win/process.c#L672
|
||||
-- https://github.com/libuv/libuv/blob/635e0ce6073c5fbc96040e336b364c061441b54b/src/win/process.c#L48-L60
|
||||
--
|
||||
-- Rather than expecting a completely empty environment, ensure that $VAR
|
||||
-- is *not* in the environment but $TOTO is.
|
||||
if iswin() then
|
||||
nvim('command', [[call jobstart('echo %TOTO% %VAR%', g:job_opts)]])
|
||||
expect_msg_seq({
|
||||
{'notification', 'stdout', {0, {'hello world %VAR%', ''}}}
|
||||
})
|
||||
else
|
||||
nvim('command', [[call jobstart('echo $TOTO $VAR', g:job_opts)]])
|
||||
expect_msg_seq({
|
||||
{'notification', 'stdout', {0, {'hello world', ''}}}
|
||||
})
|
||||
end
|
||||
end)
|
||||
|
||||
it('uses &shell and &shellcmdflag if passed a string', function()
|
||||
nvim('command', "let $VAR = 'abc'")
|
||||
if iswin() then
|
||||
|
@ -186,7 +186,12 @@ function module.expect_msg_seq(...)
|
||||
if status then
|
||||
return result
|
||||
end
|
||||
final_error = cat_err(final_error, result)
|
||||
local message = result
|
||||
if type(result) == "table" then
|
||||
-- 'eq' returns several things
|
||||
message = result.message
|
||||
end
|
||||
final_error = cat_err(final_error, message)
|
||||
end
|
||||
error(final_error)
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user