From 4598e489c797dd3e287b51711ac1924cf6bded1f Mon Sep 17 00:00:00 2001 From: Matthieu Coudron Date: Fri, 9 Nov 2018 23:52:57 +0900 Subject: [PATCH 1/6] test: always pass a string to expect_msg_seq Seems like pcall doesn't always return a string as a 2nd element of the tuple. --- test/functional/helpers.lua | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/functional/helpers.lua b/test/functional/helpers.lua index 0f2b04ea00..77d4b573fc 100644 --- a/test/functional/helpers.lua +++ b/test/functional/helpers.lua @@ -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 From f3fcaedfad59684e6f57ff0bb131b75a3c7fdcaa Mon Sep 17 00:00:00 2001 From: Matthieu Coudron Date: Fri, 9 Nov 2018 23:55:47 +0900 Subject: [PATCH 2/6] test: new test for setting environment --- test/functional/core/job_spec.lua | 40 +++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/test/functional/core/job_spec.lua b/test/functional/core/job_spec.lua index d285008a33..af2299945e 100644 --- a/test/functional/core/job_spec.lua +++ b/test/functional/core/job_spec.lua @@ -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,45 @@ describe('jobs', function() ]]) 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 From 19b6237087ebcf45427ceb6943d23ce33b39567f Mon Sep 17 00:00:00 2001 From: Matthieu Coudron Date: Fri, 9 Nov 2018 23:57:00 +0900 Subject: [PATCH 3/6] jobstart now supports env/clear_env to modify the environment of the launched job. --- runtime/doc/eval.txt | 3 ++ src/nvim/channel.c | 39 ++++++++++++++++++++++++- src/nvim/eval.c | 52 +++++++++++++++++++++++++++++++--- src/nvim/event/libuv_process.c | 2 +- src/nvim/event/process.h | 1 + src/nvim/os/env.c | 15 +++++++--- 6 files changed, 102 insertions(+), 10 deletions(-) diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt index ab65cc4560..597175b5e5 100644 --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -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. diff --git a/src/nvim/channel.c b/src/nvim/channel.c index f9102fa0e2..f8b480adcb 100644 --- a/src/nvim/channel.c +++ b/src/nvim/channel.c @@ -272,11 +272,44 @@ static void close_cb(Stream *stream, void *data) channel_decref(data); } +static inline void free_env(char **env) +{ + if (!env) { return; } + for (char **it = env; *it; it++) { + XFREE_CLEAR(*it); + } + xfree(env); +} + + +/// 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 +347,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 +362,7 @@ Channel *channel_job_start(char **argv, CallbackReader on_stdout, if (status) { EMSG3(_(e_jobspawn), os_strerror(status), cmd); xfree(cmd); + free_env(proc->env); if (proc->type == kProcessTypePty) { xfree(chan->stream.pty.term_name); } @@ -336,6 +371,8 @@ Channel *channel_job_start(char **argv, CallbackReader on_stdout, return NULL; } xfree(cmd); + free_env(proc->env); + wstream_init(&proc->in, 0); if (has_out) { diff --git a/src/nvim/eval.c b/src/nvim/eval.c index e99c41a915..00a0aecf4b 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -12577,6 +12577,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 +12595,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 +12606,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 +12623,47 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv, FunPtr fptr) return; } } + dictitem_T *item; + item = tv_dict_find(job_opts, S_LEN("env")); + if (item) { + size_t custom_env_size = (size_t)tv_dict_len(item->di_tv.vval.v_dict); + size_t i = 0; + size_t env_size = 0; + if (item->di_tv.v_type != VAR_DICT) { + EMSG2(_(e_invarg2), "env"); + return; + } + + if (clear_env) { + // + 1 for last null entry + env = xmalloc((custom_env_size + 1) * sizeof(*env)); + env_size = 0; + } else { + char **genv = os_getfullenv(); + for (env = genv; *env; env++) { + env_size++; + } + env = xmalloc((custom_env_size + env_size + 1) * sizeof(*env)); + + for (i = 0; i < env_size; i++) { + env[i] = xstrdup(genv[i]); + } + } + assert(env); // env must be allocated at this point + + TV_DICT_ITER(item->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 +12681,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 +14977,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 +18364,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; diff --git a/src/nvim/event/libuv_process.c b/src/nvim/event/libuv_process.c index 63efee59a8..37b9f73ba4 100644 --- a/src/nvim/event/libuv_process.c +++ b/src/nvim/event/libuv_process.c @@ -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; diff --git a/src/nvim/event/process.h b/src/nvim/event/process.h index ef9d953ab7..b677b80bfe 100644 --- a/src/nvim/event/process.h +++ b/src/nvim/event/process.h @@ -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; diff --git a/src/nvim/os/env.c b/src/nvim/os/env.c index eb86cb8ac7..15153e9bd7 100644 --- a/src/nvim/os/env.c +++ b/src/nvim/os/env.c @@ -184,7 +184,7 @@ int os_unsetenv(const char *name) return r == 0 ? 0 : -1; } -char *os_getenvname_at_index(size_t index) +char **os_getfullenv(void) { #ifdef _WIN32 wchar_t *env = GetEnvironmentStringsW(); @@ -224,13 +224,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. + return environ; +} + +char *os_getenvname_at_index(size_t index) +{ + char **env = os_getfullenv(); + // check if index is inside the environ array for (size_t i = 0; i <= index; i++) { - if (environ[i] == NULL) { + if (env[i] == NULL) { return NULL; } } - char *str = environ[index]; + char *str = env[index]; + assert(str != NULL); size_t namesize = 0; while (str[namesize] != '=' && str[namesize] != NUL) { namesize++; From 6dc10057876d1bf75c5cf1ea45cb4312160f13f0 Mon Sep 17 00:00:00 2001 From: James McCoy Date: Sun, 2 Jun 2019 14:36:17 -0400 Subject: [PATCH 4/6] Add os_getfullenv_size/os_copyfullenv --- src/nvim/channel.c | 13 +--- src/nvim/eval.c | 38 ++++++----- src/nvim/os/env.c | 156 ++++++++++++++++++++++++++++++++++++--------- 3 files changed, 148 insertions(+), 59 deletions(-) diff --git a/src/nvim/channel.c b/src/nvim/channel.c index f8b480adcb..2bb568f025 100644 --- a/src/nvim/channel.c +++ b/src/nvim/channel.c @@ -272,15 +272,6 @@ static void close_cb(Stream *stream, void *data) channel_decref(data); } -static inline void free_env(char **env) -{ - if (!env) { return; } - for (char **it = env; *it; it++) { - XFREE_CLEAR(*it); - } - xfree(env); -} - /// Starts a job and returns the associated channel /// @@ -362,7 +353,7 @@ Channel *channel_job_start(char **argv, CallbackReader on_stdout, if (status) { EMSG3(_(e_jobspawn), os_strerror(status), cmd); xfree(cmd); - free_env(proc->env); + os_free_fullenv(proc->env); if (proc->type == kProcessTypePty) { xfree(chan->stream.pty.term_name); } @@ -371,7 +362,7 @@ Channel *channel_job_start(char **argv, CallbackReader on_stdout, return NULL; } xfree(cmd); - free_env(proc->env); + os_free_fullenv(proc->env); wstream_init(&proc->in, 0); diff --git a/src/nvim/eval.c b/src/nvim/eval.c index 00a0aecf4b..e498253db1 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -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); } /* @@ -12639,15 +12646,12 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv, FunPtr fptr) env = xmalloc((custom_env_size + 1) * sizeof(*env)); env_size = 0; } else { - char **genv = os_getfullenv(); - for (env = genv; *env; env++) { - env_size++; - } + env_size = os_get_fullenv_size(); + env = xmalloc((custom_env_size + env_size + 1) * sizeof(*env)); - for (i = 0; i < env_size; i++) { - env[i] = xstrdup(genv[i]); - } + os_copy_fullenv(env, env_size); + i = env_size; } assert(env); // env must be allocated at this point diff --git a/src/nvim/os/env.c b/src/nvim/os/env.c index 15153e9bd7..ac442ee2e8 100644 --- a/src/nvim/os/env.c +++ b/src/nvim/os/env.c @@ -184,39 +184,139 @@ int os_unsetenv(const char *name) return r == 0 ? 0 : -1; } -char **os_getfullenv(void) +/// 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(p, -1, &utf8_str); + if (conversion_result != 0) { + EMSG2("utf16_to_utf8 failed: %d", conversion_result); + break; + } + p += l + 1; + + env[i] = utf8_str; + i++; + } + + 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(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++; - } - name = (char *)vim_strnsave((char_u *)utf8_str, namesize); + + const char * const end = strchr(utf8_str, '='); + 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,26 +324,20 @@ char **os_getfullenv(void) # else extern char **environ; # endif - return environ; -} -char *os_getenvname_at_index(size_t index) -{ - char **env = os_getfullenv(); // check if index is inside the environ array for (size_t i = 0; i <= index; i++) { - if (env[i] == NULL) { + if (environ[i] == NULL) { return NULL; } } - char *str = env[index]; + char *str = environ[index]; assert(str != NULL); - size_t namesize = 0; - while (str[namesize] != '=' && str[namesize] != NUL) { - namesize++; - } - char *name = (char *)vim_strnsave((char_u *)str, namesize); - return name; + const char * const end = strchr(str, '='); + assert(end != NULL); + ptrdiff_t len = end - str; + assert(len > 0); + return xstrndup(str, (size_t)len); #endif } From 39963c6a04afc417a821b2255a5caea4b7955d7d Mon Sep 17 00:00:00 2001 From: James McCoy Date: Wed, 4 Dec 2019 08:01:05 -0500 Subject: [PATCH 5/6] os_getenvname_at_index: Handle Windows env vars whose name starts with = --- src/nvim/os/env.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/nvim/os/env.c b/src/nvim/os/env.c index ac442ee2e8..360609c50d 100644 --- a/src/nvim/os/env.c +++ b/src/nvim/os/env.c @@ -302,7 +302,10 @@ char *os_getenvname_at_index(size_t index) break; } - const char * const end = strchr(utf8_str, '='); + // 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); From 91b313a904c039a9b6a53a7afc9f66e67a1e12fc Mon Sep 17 00:00:00 2001 From: James McCoy Date: Thu, 12 Dec 2019 07:26:39 -0500 Subject: [PATCH 6/6] Add negative test for type of job's env option --- src/nvim/eval.c | 17 +++++++++-------- test/functional/core/job_spec.lua | 12 ++++++++++++ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/nvim/eval.c b/src/nvim/eval.c index e498253db1..c14b7f513d 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -12630,17 +12630,18 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv, FunPtr fptr) return; } } - dictitem_T *item; - item = tv_dict_find(job_opts, S_LEN("env")); - if (item) { - size_t custom_env_size = (size_t)tv_dict_len(item->di_tv.vval.v_dict); - size_t i = 0; - size_t env_size = 0; - if (item->di_tv.v_type != VAR_DICT) { + 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)); @@ -12655,7 +12656,7 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv, FunPtr fptr) } assert(env); // env must be allocated at this point - TV_DICT_ITER(item->di_tv.vval.v_dict, var, { + 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; diff --git a/test/functional/core/job_spec.lua b/test/functional/core/job_spec.lua index af2299945e..e5d4444b92 100644 --- a/test/functional/core/job_spec.lua +++ b/test/functional/core/job_spec.lua @@ -49,6 +49,18 @@ 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'}")