changeset 12043:2796a2c9fc17 v8.0.0902

patch 8.0.0902: cannot specify directory or environment for a job commit https://github.com/vim/vim/commit/05aafed54b50b602315ae55d83a7d089804cecb0 Author: Bram Moolenaar <Bram@vim.org> Date: Fri Aug 11 19:12:11 2017 +0200 patch 8.0.0902: cannot specify directory or environment for a job Problem: Cannot specify directory or environment for a job. Solution: Add the "cwd" and "env" arguments to job options. (Yasuhiro Matsumoto, closes #1160)
author Christian Brabandt <cb@256bit.org>
date Fri, 11 Aug 2017 19:15:04 +0200
parents 3b09a43a3830
children f07a8b5428f9
files runtime/doc/channel.txt src/channel.c src/os_unix.c src/os_win32.c src/structs.h src/terminal.c src/testdir/test_channel.vim src/testdir/test_terminal.vim src/version.c
diffstat 9 files changed, 254 insertions(+), 45 deletions(-) [+]
line wrap: on
line diff
--- a/runtime/doc/channel.txt
+++ b/runtime/doc/channel.txt
@@ -427,8 +427,8 @@ When no message was available then the r
 channels, an empty string for a RAW or NL channel.  You can use |ch_canread()|
 to check if there is something to read.
 
-Note that when there is no callback message are dropped.  To avoid that add a
-close callback to the channel.
+Note that when there is no callback, messages are dropped.  To avoid that add
+a close callback to the channel.
 
 To read all output from a RAW channel that is available: >
 	let output = ch_readraw(channel)
@@ -475,11 +475,6 @@ it like this: >
 Without the handler you need to read the output with |ch_read()| or
 |ch_readraw()|. You can do this in the close callback, see |read-in-close-cb|.
 
-Note that if the job exits before you read the output, the output may be lost.
-This depends on the system (on Unix this happens because closing the write end
-of a pipe causes the read end to get EOF).  To avoid this make the job sleep
-for a short while before it exits.
-
 The handler defined for "out_cb" will not receive stderr.  If you want to
 handle that separately, add an "err_cb" handler: >
     let job = job_start(command, {"out_cb": "MyHandler",
@@ -494,6 +489,11 @@ started job gets the focus.  To avoid th
 This might not always work when called early, put in the callback handler or
 use a timer to call it after the job has started.
 
+Depending on the system, starting a job can put Vim in the background, the
+started job gets the focus.  To avoid that, use the `foreground()` function.
+This might not always work when called early, put in the callback handler or
+use a timer to call it after the job has started.
+
 You can send a message to the command with ch_evalraw().  If the channel is in
 JSON or JS mode you can use ch_evalexpr().
 
@@ -696,6 +696,10 @@ See |job_setoptions()| and |ch_setoption
 "block_write": number	only for testing: pretend every other write to stdin
 			will block
 
+"env": dict		environment variables for the new process
+"cwd": "/path/to/dir"	current working directory for the new process;
+			if the directory does not exist an error is given
+
 
 Writing to a buffer ~
 							*out_io-buffer*
@@ -731,10 +735,6 @@ The "out_msg" option can be used to spec
 first line set to "Reading from channel output...".  The default is to add the
 message.  "err_msg" does the same for channel error.
 
-'modifiable' option off, or write to a buffer that has 'modifiable' off.  That
-means that lines will be appended to the buffer, but the user can't easily
-change the buffer.
-
 When an existing buffer is to be written where 'modifiable' is off and the
 "out_modifiable" or "err_modifiable" options is not zero, an error is given
 and the buffer will not be written to.
--- a/src/channel.c
+++ b/src/channel.c
@@ -4153,6 +4153,8 @@ free_job_options(jobopt_T *opt)
 	partial_unref(opt->jo_exit_partial);
     else if (opt->jo_exit_cb != NULL)
 	func_unref(opt->jo_exit_cb);
+    if (opt->jo_env != NULL)
+	dict_unref(opt->jo_env);
 }
 
 /*
@@ -4433,6 +4435,26 @@ get_job_options(typval_T *tv, jobopt_T *
 		opt->jo_term_finish = *val;
 	    }
 #endif
+	    else if (STRCMP(hi->hi_key, "env") == 0)
+	    {
+		if (!(supported & JO2_ENV))
+		    break;
+		opt->jo_set |= JO2_ENV;
+		opt->jo_env = item->vval.v_dict;
+		++item->vval.v_dict->dv_refcount;
+	    }
+	    else if (STRCMP(hi->hi_key, "cwd") == 0)
+	    {
+		if (!(supported & JO2_CWD))
+		    break;
+		opt->jo_cwd = get_tv_string_buf_chk(item, opt->jo_cwd_buf);
+		if (opt->jo_cwd == NULL || !mch_isdir(opt->jo_cwd))
+		{
+		    EMSG2(_(e_invarg2), "cwd");
+		    return FAIL;
+		}
+		opt->jo_set |= JO2_CWD;
+	    }
 	    else if (STRCMP(hi->hi_key, "waittime") == 0)
 	    {
 		if (!(supported & JO_WAITTIME))
--- a/src/os_unix.c
+++ b/src/os_unix.c
@@ -5320,6 +5320,22 @@ mch_job_start(char **argv, job_T *job, j
 # endif
 	    set_default_child_environment();
 
+	if (options->jo_env != NULL)
+	{
+	    dict_T	*dict = options->jo_env;
+	    hashitem_T	*hi;
+	    int		todo = (int)dict->dv_hashtab.ht_used;
+
+	    for (hi = dict->dv_hashtab.ht_array; todo > 0; ++hi)
+		if (!HASHITEM_EMPTY(hi))
+		{
+		    typval_T *item = &dict_lookup(hi)->di_tv;
+
+		    vim_setenv((char_u*)hi->hi_key, get_tv_string(item));
+		    --todo;
+		}
+	}
+
 	if (use_null_for_in || use_null_for_out || use_null_for_err)
 	    null_fd = open("/dev/null", O_RDWR | O_EXTRA, 0);
 
@@ -5387,6 +5403,9 @@ mch_job_start(char **argv, job_T *job, j
 	if (null_fd >= 0)
 	    close(null_fd);
 
+	if (options->jo_cwd != NULL && mch_chdir((char *)options->jo_cwd) != 0)
+	    _exit(EXEC_FAILED);
+
 	/* See above for type of argv. */
 	execvp(argv[0], argv);
 
--- a/src/os_win32.c
+++ b/src/os_win32.c
@@ -3981,31 +3981,46 @@ vim_create_process(
     BOOL		inherit_handles,
     DWORD		flags,
     STARTUPINFO		*si,
-    PROCESS_INFORMATION *pi)
+    PROCESS_INFORMATION *pi,
+    LPVOID		*env,
+    char		*cwd)
 {
 #ifdef FEAT_MBYTE
     if (enc_codepage >= 0 && (int)GetACP() != enc_codepage)
     {
-	WCHAR	*wcmd = enc_to_utf16((char_u *)cmd, NULL);
-
-	if (wcmd != NULL)
+	BOOL	ret;
+	WCHAR	*wcmd, *wcwd = NULL;
+
+	wcmd = enc_to_utf16((char_u *)cmd, NULL);
+	if (wcmd == NULL)
+	    goto fallback;
+	if (cwd != NULL)
 	{
-	    BOOL ret;
-	    ret = CreateProcessW(
-		NULL,			/* Executable name */
-		wcmd,			/* Command to execute */
-		NULL,			/* Process security attributes */
-		NULL,			/* Thread security attributes */
-		inherit_handles,	/* Inherit handles */
-		flags,			/* Creation flags */
-		NULL,			/* Environment */
-		NULL,			/* Current directory */
-		(LPSTARTUPINFOW)si,	/* Startup information */
-		pi);			/* Process information */
-	    vim_free(wcmd);
-	    return ret;
+	    wcwd = enc_to_utf16((char_u *)cwd, NULL);
+	    if (wcwd == NULL)
+	    {
+		vim_free(wcmd);
+		goto fallback;
+	    }
 	}
-    }
+
+	ret = CreateProcessW(
+	    NULL,			/* Executable name */
+	    wcmd,			/* Command to execute */
+	    NULL,			/* Process security attributes */
+	    NULL,			/* Thread security attributes */
+	    inherit_handles,	/* Inherit handles */
+	    flags,			/* Creation flags */
+	    env,			/* Environment */
+	    wcwd,			/* Current directory */
+	    (LPSTARTUPINFOW)si,	/* Startup information */
+	    pi);			/* Process information */
+	vim_free(wcmd);
+	if (wcwd != NULL)
+	    vim_free(wcwd);
+	return ret;
+    }
+fallback:
 #endif
     return CreateProcess(
 	NULL,			/* Executable name */
@@ -4014,8 +4029,8 @@ vim_create_process(
 	NULL,			/* Thread security attributes */
 	inherit_handles,	/* Inherit handles */
 	flags,			/* Creation flags */
-	NULL,			/* Environment */
-	NULL,			/* Current directory */
+	env,			/* Environment */
+	cwd,			/* Current directory */
 	si,			/* Startup information */
 	pi);			/* Process information */
 }
@@ -4079,7 +4094,8 @@ mch_system_classic(char *cmd, int option
 
     /* Now, run the command */
     vim_create_process(cmd, FALSE,
-	    CREATE_DEFAULT_ERROR_MODE |	CREATE_NEW_CONSOLE, &si, &pi);
+	    CREATE_DEFAULT_ERROR_MODE |	CREATE_NEW_CONSOLE,
+	    &si, &pi, NULL, NULL);
 
     /* Wait for the command to terminate before continuing */
     {
@@ -4398,7 +4414,8 @@ mch_system_piped(char *cmd, int options)
      * About "Inherit handles" being TRUE: this command can be litigious,
      * handle inheritance was deactivated for pending temp file, but, if we
      * deactivate it, the pipes don't work for some reason. */
-     vim_create_process(p, TRUE, CREATE_DEFAULT_ERROR_MODE, &si, &pi);
+     vim_create_process(p, TRUE, CREATE_DEFAULT_ERROR_MODE,
+	     &si, &pi, NULL, NULL);
 
     if (p != cmd)
 	vim_free(p);
@@ -4835,7 +4852,8 @@ mch_call_shell(
 	     * inherit our handles which causes unpleasant dangling swap
 	     * files if we exit before the spawned process
 	     */
-	    if (vim_create_process((char *)newcmd, FALSE, flags, &si, &pi))
+	    if (vim_create_process((char *)newcmd, FALSE, flags,
+			&si, &pi, NULL, NULL))
 		x = 0;
 	    else if (vim_shell_execute((char *)newcmd, n_show_cmd)
 							       > (HINSTANCE)32)
@@ -4976,6 +4994,67 @@ job_io_file_open(
     return h;
 }
 
+/*
+ * Turn the dictionary "env" into a NUL separated list that can be used as the
+ * environment argument of vim_create_process().
+ */
+    static void
+make_job_env(garray_T *gap, dict_T *env)
+{
+    hashitem_T	*hi;
+    int		todo = (int)env->dv_hashtab.ht_used;
+    LPVOID	base = GetEnvironmentStringsW();
+
+    /* for last \0 */
+    if (ga_grow(gap, 1) == FAIL)
+	return;
+
+    if (base)
+    {
+	WCHAR	*p = (WCHAR*) base;
+
+	/* for last \0 */
+	if (ga_grow(gap, 1) == FAIL)
+	    return;
+
+	while (*p != 0 || *(p + 1) != 0)
+	{
+	    if (ga_grow(gap, 1) == OK)
+		*((WCHAR*)gap->ga_data + gap->ga_len++) = *p;
+	    p++;
+	}
+	FreeEnvironmentStrings(base);
+	*((WCHAR*)gap->ga_data + gap->ga_len++) = L'\0';
+    }
+
+    for (hi = env->dv_hashtab.ht_array; todo > 0; ++hi)
+    {
+	if (!HASHITEM_EMPTY(hi))
+	{
+	    typval_T *item = &dict_lookup(hi)->di_tv;
+	    WCHAR   *wkey = enc_to_utf16((char_u *)hi->hi_key, NULL);
+	    WCHAR   *wval = enc_to_utf16(get_tv_string(item), NULL);
+	    --todo;
+	    if (wkey != NULL && wval != NULL)
+	    {
+		int n, lkey = wcslen(wkey), lval = wcslen(wval);
+		if (ga_grow(gap, lkey + lval + 2) != OK)
+		    continue;
+		for (n = 0; n < lkey; n++)
+		    *((WCHAR*)gap->ga_data + gap->ga_len++) = wkey[n];
+		*((WCHAR*)gap->ga_data + gap->ga_len++) = L'=';
+		for (n = 0; n < lval; n++)
+		    *((WCHAR*)gap->ga_data + gap->ga_len++) = wval[n];
+		*((WCHAR*)gap->ga_data + gap->ga_len++) = L'\0';
+	    }
+	    if (wkey != NULL) vim_free(wkey);
+	    if (wval != NULL) vim_free(wval);
+	}
+    }
+
+    *((WCHAR*)gap->ga_data + gap->ga_len++) = L'\0';
+}
+
     void
 mch_job_start(char *cmd, job_T *job, jobopt_T *options)
 {
@@ -4987,6 +5066,7 @@ mch_job_start(char *cmd, job_T *job, job
     HANDLE		ifd[2];
     HANDLE		ofd[2];
     HANDLE		efd[2];
+    garray_T		ga;
 
     int		use_null_for_in = options->jo_io[PART_IN] == JIO_NULL;
     int		use_null_for_out = options->jo_io[PART_OUT] == JIO_NULL;
@@ -5005,6 +5085,7 @@ mch_job_start(char *cmd, job_T *job, job
     ofd[1] = INVALID_HANDLE_VALUE;
     efd[0] = INVALID_HANDLE_VALUE;
     efd[1] = INVALID_HANDLE_VALUE;
+    ga_init2(&ga, (int)sizeof(wchar_t), 500);
 
     jo = CreateJobObject(NULL, NULL);
     if (jo == NULL)
@@ -5013,6 +5094,9 @@ mch_job_start(char *cmd, job_T *job, job
 	goto failed;
     }
 
+    if (options->jo_env != NULL)
+	make_job_env(&ga, options->jo_env);
+
     ZeroMemory(&pi, sizeof(pi));
     ZeroMemory(&si, sizeof(si));
     si.cb = sizeof(si);
@@ -5100,14 +5184,19 @@ mch_job_start(char *cmd, job_T *job, job
 	    CREATE_SUSPENDED |
 	    CREATE_DEFAULT_ERROR_MODE |
 	    CREATE_NEW_PROCESS_GROUP |
+	    CREATE_UNICODE_ENVIRONMENT |
 	    CREATE_NEW_CONSOLE,
-	    &si, &pi))
+	    &si, &pi,
+	    ga.ga_data,
+	    (char *)options->jo_cwd))
     {
 	CloseHandle(jo);
 	job->jv_status = JOB_FAILED;
 	goto failed;
     }
 
+    ga_clear(&ga);
+
     if (!AssignProcessToJobObject(jo, pi.hProcess))
     {
 	/* if failing, switch the way to terminate
@@ -5148,6 +5237,7 @@ failed:
     CloseHandle(ofd[1]);
     CloseHandle(efd[1]);
     channel_unref(channel);
+    ga_clear(&ga);
 }
 
     char *
--- a/src/structs.h
+++ b/src/structs.h
@@ -1686,7 +1686,9 @@ struct channel_S {
 #define JO2_ERR_MSG	    0x0002	/* "err_msg" (JO_OUT_ << 1) */
 #define JO2_TERM_NAME	    0x0004	/* "term_name" */
 #define JO2_TERM_FINISH	    0x0008	/* "term_finish" */
-#define JO2_ALL		    0x000F
+#define JO2_ENV		    0x0010	/* "env" */
+#define JO2_CWD		    0x0020	/* "cwd" */
+#define JO2_ALL		    0x003F
 
 #define JO_MODE_ALL	(JO_MODE + JO_IN_MODE + JO_OUT_MODE + JO_ERR_MODE)
 #define JO_CB_ALL \
@@ -1738,6 +1740,9 @@ typedef struct
     int		jo_id;
     char_u	jo_soe_buf[NUMBUFLEN];
     char_u	*jo_stoponexit;
+    dict_T	*jo_env;	/* environment variables */
+    char_u	jo_cwd_buf[NUMBUFLEN];
+    char_u	*jo_cwd;
 
 #ifdef FEAT_TERMINAL
     /* when non-zero run the job in a terminal window of this size */
--- a/src/terminal.c
+++ b/src/terminal.c
@@ -2362,7 +2362,8 @@ f_term_start(typval_T *argvars, typval_T
 	    && get_job_options(&argvars[1], &opt,
 		JO_TIMEOUT_ALL + JO_STOPONEXIT
 		+ JO_EXIT_CB + JO_CLOSE_CALLBACK
-		+ JO2_TERM_NAME + JO2_TERM_FINISH) == FAIL)
+		+ JO2_TERM_NAME + JO2_TERM_FINISH
+		+ JO2_CWD + JO2_ENV) == FAIL)
 	return;
 
     term_start(cmd, &opt);
--- a/src/testdir/test_channel.vim
+++ b/src/testdir/test_channel.vim
@@ -1664,6 +1664,45 @@ func Test_read_from_terminated_job()
   call assert_equal(1, g:linecount)
 endfunc
 
+func Test_env()
+  if !has('job')
+    return
+  endif
+
+  let s:envstr = ''
+  if has('win32')
+    call job_start(['cmd', '/c', 'echo %FOO%'], {'callback': {ch,msg->execute(":let s:envstr .= msg")}, 'env':{'FOO': 'bar'}})
+  else
+    call job_start([&shell, &shellcmdflag, 'echo $FOO'], {'callback': {ch,msg->execute(":let s:envstr .= msg")}, 'env':{'FOO': 'bar'}})
+  endif
+  call WaitFor('"" != s:envstr')
+  call assert_equal("bar", s:envstr)
+  unlet s:envstr
+endfunc
+
+func Test_cwd()
+  if !has('job')
+    return
+  endif
+
+  let s:envstr = ''
+  if has('win32')
+    let expect = $TEMP
+    call job_start(['cmd', '/c', 'echo %CD%'], {'callback': {ch,msg->execute(":let s:envstr .= msg")}, 'cwd': expect})
+  else
+    let expect = $HOME
+    call job_start(['pwd'], {'callback': {ch,msg->execute(":let s:envstr .= msg")}, 'cwd': expect})
+  endif
+  call WaitFor('"" != s:envstr')
+  let expect = substitute(expect, '[/\\]$', '', '')
+  let s:envstr = substitute(s:envstr, '[/\\]$', '', '')
+  if $CI != '' && stridx(s:envstr, '/private/') == 0
+    let s:envstr = s:envstr[8:]
+  endif
+  call assert_equal(expect, s:envstr)
+  unlet s:envstr
+endfunc
+
 function Ch_test_close_lambda(port)
   let handle = ch_open('localhost:' . a:port, s:chopt)
   if ch_status(handle) == "fail"
--- a/src/testdir/test_terminal.vim
+++ b/src/testdir/test_terminal.vim
@@ -8,8 +8,8 @@ source shared.vim
 
 " Open a terminal with a shell, assign the job to g:job and return the buffer
 " number.
-func Run_shell_in_terminal()
-  let buf = term_start(&shell)
+func Run_shell_in_terminal(options)
+  let buf = term_start(&shell, a:options)
 
   let termlist = term_list()
   call assert_equal(1, len(termlist))
@@ -32,7 +32,7 @@ func Stop_shell_in_terminal(buf)
 endfunc
 
 func Test_terminal_basic()
-  let buf = Run_shell_in_terminal()
+  let buf = Run_shell_in_terminal({})
   if has("unix")
     call assert_match("^/dev/", job_info(g:job).tty)
     call assert_match("^/dev/", term_gettty(''))
@@ -51,7 +51,7 @@ func Test_terminal_basic()
 endfunc
 
 func Test_terminal_make_change()
-  let buf = Run_shell_in_terminal()
+  let buf = Run_shell_in_terminal({})
   call Stop_shell_in_terminal(buf)
   call term_wait(buf)
 
@@ -65,7 +65,7 @@ func Test_terminal_make_change()
 endfunc
 
 func Test_terminal_wipe_buffer()
-  let buf = Run_shell_in_terminal()
+  let buf = Run_shell_in_terminal({})
   call assert_fails(buf . 'bwipe', 'E517')
   exe buf . 'bwipe!'
   call WaitFor('job_status(g:job) == "dead"')
@@ -76,7 +76,7 @@ func Test_terminal_wipe_buffer()
 endfunc
 
 func Test_terminal_hide_buffer()
-  let buf = Run_shell_in_terminal()
+  let buf = Run_shell_in_terminal({})
   quit
   for nr in range(1, winnr('$'))
     call assert_notequal(winbufnr(nr), buf)
@@ -266,9 +266,11 @@ func Test_terminal_size()
 endfunc
 
 func Test_finish_close()
+  return
+  " TODO: use something that takes much less than a whole second
+  echo 'This will take five seconds...'
   call assert_equal(1, winnr('$'))
 
-  " TODO: use something that takes much less than a whole second
   if has('win32')
     let cmd = $windir . '\system32\timeout.exe 1'
   else
@@ -304,3 +306,32 @@ func Test_finish_close()
 
   bwipe
 endfunc
+
+func Test_terminal_cwd()
+  if !has('unix')
+    return
+  endif
+  call mkdir('Xdir')
+  let buf = term_start('pwd', {'cwd': 'Xdir'})
+  sleep 100m
+  call term_wait(buf)
+  call assert_equal(getcwd() . '/Xdir', getline(1))
+
+  exe buf . 'bwipe'
+  call delete('Xdir', 'rf')
+endfunc
+
+func Test_terminal_env()
+  if !has('unix')
+    return
+  endif
+  let buf = Run_shell_in_terminal({'env': {'TESTENV': 'correct'}})
+  call term_wait(buf)
+  call term_sendkeys(buf, "echo $TESTENV\r")
+  call term_wait(buf)
+  call Stop_shell_in_terminal(buf)
+  call term_wait(buf)
+  call assert_equal('correct', getline(2))
+
+  exe buf . 'bwipe'
+endfunc
--- a/src/version.c
+++ b/src/version.c
@@ -770,6 +770,8 @@ static char *(features[]) =
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    902,
+/**/
     901,
 /**/
     900,