changeset 30083:a542dfb1c1a2 v9.0.0379

patch 9.0.0379: cleaning up after writefile() is a hassle Commit: https://github.com/vim/vim/commit/806a273f3c84ecd475913d901890bb1929be9a0a Author: Bram Moolenaar <Bram@vim.org> Date: Sun Sep 4 15:40:36 2022 +0100 patch 9.0.0379: cleaning up after writefile() is a hassle Problem: Cleaning up after writefile() is a hassle. Solution: Add the 'D' flag to defer deleting the written file. Very useful in tests.
author Bram Moolenaar <Bram@vim.org>
date Sun, 04 Sep 2022 17:00:03 +0200
parents 3dc03a877e86
children f4a27718e608
files runtime/doc/builtin.txt src/filepath.c src/proto/userfunc.pro src/proto/vim9cmds.pro src/proto/vim9execute.pro src/testdir/test_writefile.vim src/userfunc.c src/version.c src/vim9cmds.c src/vim9execute.c src/vim9expr.c
diffstat 11 files changed, 268 insertions(+), 99 deletions(-) [+]
line wrap: on
line diff
--- a/runtime/doc/builtin.txt
+++ b/runtime/doc/builtin.txt
@@ -10448,34 +10448,43 @@ writefile({object}, {fname} [, {flags}])
 		When {object} is a |List| write it to file {fname}.  Each list
 		item is separated with a NL.  Each list item must be a String
 		or Number.
-		When {flags} contains "b" then binary mode is used: There will
-		not be a NL after the last list item.  An empty item at the
-		end does cause the last line in the file to end in a NL.
-
-		When {object} is a |Blob| write the bytes to file {fname}
-		unmodified.
-
-		When {flags} contains "a" then append mode is used, lines are
-		appended to the file: >
-			:call writefile(["foo"], "event.log", "a")
-			:call writefile(["bar"], "event.log", "a")
-<
-		When {flags} contains "s" then fsync() is called after writing
-		the file.  This flushes the file to disk, if possible.  This
-		takes more time but avoids losing the file if the system
-		crashes.
-		When {flags} does not contain "S" or "s" then fsync() is
-		called if the 'fsync' option is set.
-		When {flags} contains "S" then fsync() is not called, even
-		when 'fsync' is set.
-
 		All NL characters are replaced with a NUL character.
 		Inserting CR characters needs to be done before passing {list}
 		to writefile().
+
+		When {object} is a |Blob| write the bytes to file {fname}
+		unmodified, also when binary mode is not specified.
+
+		{flags} must be a String.  These characters are recognized:
+
+		'b'  Binary mode is used: There will not be a NL after the
+		     last list item.  An empty item at the end does cause the
+		     last line in the file to end in a NL.
+
+		'a'  Append mode is used, lines are appended to the file: >
+			:call writefile(["foo"], "event.log", "a")
+			:call writefile(["bar"], "event.log", "a")
+<
+		'D'  Delete the file when the current function ends.  This
+		     works like: >
+		     	:defer delete({fname})
+<		     Fails when not in a function.  Also see |:defer|.
+
+		's'  fsync() is called after writing the file.  This flushes
+		     the file to disk, if possible.  This takes more time but
+		     avoids losing the file if the system crashes.
+
+		'S'  fsync() is not called, even when 'fsync' is set.
+
+		     When {flags} does not contain "S" or "s" then fsync() is
+		     called if the 'fsync' option is set.
+
 		An existing file is overwritten, if possible.
+
 		When the write fails -1 is returned, otherwise 0.  There is an
 		error message if the file can't be created or when writing
 		fails.
+
 		Also see |readfile()|.
 		To copy a file byte for byte: >
 			:let fl = readfile("foo", "b")
--- a/src/filepath.c
+++ b/src/filepath.c
@@ -2232,6 +2232,7 @@ f_writefile(typval_T *argvars, typval_T 
 {
     int		binary = FALSE;
     int		append = FALSE;
+    int		defer = FALSE;
 #ifdef HAVE_FSYNC
     int		do_fsync = p_fs;
 #endif
@@ -2285,6 +2286,8 @@ f_writefile(typval_T *argvars, typval_T 
 	    binary = TRUE;
 	if (vim_strchr(arg2, 'a') != NULL)
 	    append = TRUE;
+	if (vim_strchr(arg2, 'D') != NULL)
+	    defer = TRUE;
 #ifdef HAVE_FSYNC
 	if (vim_strchr(arg2, 's') != NULL)
 	    do_fsync = TRUE;
@@ -2297,37 +2300,59 @@ f_writefile(typval_T *argvars, typval_T 
     if (fname == NULL)
 	return;
 
+    if (defer && !in_def_function() && get_current_funccal() == NULL)
+    {
+	semsg(_(e_str_not_inside_function), "defer");
+	return;
+    }
+
     // Always open the file in binary mode, library functions have a mind of
     // their own about CR-LF conversion.
     if (*fname == NUL || (fd = mch_fopen((char *)fname,
 				      append ? APPENDBIN : WRITEBIN)) == NULL)
     {
-	semsg(_(e_cant_create_file_str), *fname == NUL ? (char_u *)_("<empty>") : fname);
+	semsg(_(e_cant_create_file_str),
+			       *fname == NUL ? (char_u *)_("<empty>") : fname);
 	ret = -1;
     }
-    else if (blob)
-    {
-	if (write_blob(fd, blob) == FAIL)
-	    ret = -1;
-#ifdef HAVE_FSYNC
-	else if (do_fsync)
-	    // Ignore the error, the user wouldn't know what to do about it.
-	    // May happen for a device.
-	    vim_ignored = vim_fsync(fileno(fd));
-#endif
-	fclose(fd);
-    }
     else
     {
-	if (write_list(fd, list, binary) == FAIL)
-	    ret = -1;
+	if (defer)
+	{
+	    typval_T tv;
+
+	    tv.v_type = VAR_STRING;
+	    tv.v_lock = 0;
+	    tv.vval.v_string = vim_strsave(fname);
+	    if (tv.vval.v_string == NULL
+		    || add_defer((char_u *)"delete", 1, &tv) == FAIL)
+	    {
+		ret = -1;
+		fclose(fd);
+		(void)mch_remove(fname);
+	    }
+	}
+
+	if (ret == 0)
+	{
+	    if (blob)
+	    {
+		if (write_blob(fd, blob) == FAIL)
+		    ret = -1;
+	    }
+	    else
+	    {
+		if (write_list(fd, list, binary) == FAIL)
+		    ret = -1;
+	    }
 #ifdef HAVE_FSYNC
-	else if (do_fsync)
-	    // Ignore the error, the user wouldn't know what to do about it.
-	    // May happen for a device.
-	    vim_ignored = vim_fsync(fileno(fd));
+	    if (ret == 0 && do_fsync)
+		// Ignore the error, the user wouldn't know what to do about
+		// it.  May happen for a device.
+		vim_ignored = vim_fsync(fileno(fd));
 #endif
-	fclose(fd);
+	    fclose(fd);
+	}
     }
 
     rettv->vval.v_number = ret;
--- a/src/proto/userfunc.pro
+++ b/src/proto/userfunc.pro
@@ -58,6 +58,7 @@ void func_ptr_unref(ufunc_T *fp);
 void func_ref(char_u *name);
 void func_ptr_ref(ufunc_T *fp);
 void ex_return(exarg_T *eap);
+int add_defer(char_u *name, int argcount_arg, typval_T *argvars);
 void handle_defer(void);
 void ex_call(exarg_T *eap);
 int do_return(exarg_T *eap, int reanimate, int is_cmd, void *rettv);
--- a/src/proto/vim9cmds.pro
+++ b/src/proto/vim9cmds.pro
@@ -21,6 +21,7 @@ char_u *compile_finally(char_u *arg, cct
 char_u *compile_endtry(char_u *arg, cctx_T *cctx);
 char_u *compile_throw(char_u *arg, cctx_T *cctx);
 char_u *compile_eval(char_u *arg, cctx_T *cctx);
+int get_defer_var_idx(cctx_T *cctx);
 char_u *compile_defer(char_u *arg_start, cctx_T *cctx);
 char_u *compile_mult_expr(char_u *arg, int cmdidx, cctx_T *cctx);
 char_u *compile_put(char_u *arg, exarg_T *eap, cctx_T *cctx);
--- a/src/proto/vim9execute.pro
+++ b/src/proto/vim9execute.pro
@@ -3,6 +3,8 @@ void to_string_error(vartype_T vartype);
 void update_has_breakpoint(ufunc_T *ufunc);
 void funcstack_check_refcount(funcstack_T *funcstack);
 int set_ref_in_funcstacks(int copyID);
+int in_def_function(void);
+int add_defer_function(char_u *name, int argcount, typval_T *argvars);
 char_u *char_from_string(char_u *str, varnumber_T index);
 char_u *string_slice(char_u *str, varnumber_T first, varnumber_T last, int exclusive);
 int fill_partial_and_closure(partial_T *pt, ufunc_T *ufunc, ectx_T *ectx);
--- a/src/testdir/test_writefile.vim
+++ b/src/testdir/test_writefile.vim
@@ -933,6 +933,23 @@ func Test_write_binary_file()
   call delete('Xwbfile3')
 endfunc
 
+func DoWriteDefer()
+  call writefile(['some text'], 'XdeferDelete', 'D')
+  call assert_equal(['some text'], readfile('XdeferDelete'))
+endfunc
+
+def DefWriteDefer()
+  writefile(['some text'], 'XdefdeferDelete', 'D')
+  assert_equal(['some text'], readfile('XdefdeferDelete'))
+enddef
+
+func Test_write_with_deferred_delete()
+  call DoWriteDefer()
+  call assert_equal('', glob('XdeferDelete'))
+  call DefWriteDefer()
+  call assert_equal('', glob('XdefdeferDelete'))
+endfunc
+
 " Check that buffer is written before triggering QuitPre
 func Test_wq_quitpre_autocommand()
   edit Xsomefile
--- a/src/userfunc.c
+++ b/src/userfunc.c
@@ -1728,6 +1728,7 @@ emsg_funcname(char *ermsg, char_u *name)
 /*
  * Get function arguments at "*arg" and advance it.
  * Return them in "*argvars[MAX_FUNC_ARGS + 1]" and the count in "argcount".
+ * On failure FAIL is returned but the "argvars[argcount]" are still set.
  */
     static int
 get_func_arguments(
@@ -5570,9 +5571,6 @@ ex_defer_inner(char_u *name, char_u **ar
 {
     typval_T	argvars[MAX_FUNC_ARGS + 1];	// vars for arguments
     int		argcount = 0;			// number of arguments found
-    defer_T	*dr;
-    int		ret = FAIL;
-    char_u	*saved_name;
 
     if (current_funccal == NULL)
     {
@@ -5580,23 +5578,51 @@ ex_defer_inner(char_u *name, char_u **ar
 	return FAIL;
     }
     if (get_func_arguments(arg, evalarg, FALSE, argvars, &argcount) == FAIL)
-	goto theend;
-    saved_name = vim_strsave(name);
+    {
+	while (--argcount >= 0)
+	    clear_tv(&argvars[argcount]);
+	return FAIL;
+    }
+    return add_defer(name, argcount, argvars);
+}
+
+/*
+ * Add a deferred call for "name" with arguments "argvars[argcount]".
+ * Consumes "argvars[]".
+ * Caller must check that in_def_function() returns TRUE or current_funccal is
+ * not NULL.
+ * Returns OK or FAIL.
+ */
+    int
+add_defer(char_u *name, int argcount_arg, typval_T *argvars)
+{
+    char_u	*saved_name = vim_strsave(name);
+    int		argcount = argcount_arg;
+    defer_T	*dr;
+    int		ret = FAIL;
+
     if (saved_name == NULL)
 	goto theend;
-
-    if (current_funccal->fc_defer.ga_itemsize == 0)
-	ga_init2(&current_funccal->fc_defer, sizeof(defer_T), 10);
-    if (ga_grow(&current_funccal->fc_defer, 1) == FAIL)
-	goto theend;
-    dr = ((defer_T *)current_funccal->fc_defer.ga_data)
+    if (in_def_function())
+    {
+	if (add_defer_function(saved_name, argcount, argvars) == OK)
+	    argcount = 0;
+    }
+    else
+    {
+	if (current_funccal->fc_defer.ga_itemsize == 0)
+	    ga_init2(&current_funccal->fc_defer, sizeof(defer_T), 10);
+	if (ga_grow(&current_funccal->fc_defer, 1) == FAIL)
+	    goto theend;
+	dr = ((defer_T *)current_funccal->fc_defer.ga_data)
 					  + current_funccal->fc_defer.ga_len++;
-    dr->dr_name = saved_name;
-    dr->dr_argcount = argcount;
-    while (argcount > 0)
-    {
-	--argcount;
-	dr->dr_argvars[argcount] = argvars[argcount];
+	dr->dr_name = saved_name;
+	dr->dr_argcount = argcount;
+	while (argcount > 0)
+	{
+	    --argcount;
+	    dr->dr_argvars[argcount] = argvars[argcount];
+	}
     }
     ret = OK;
 
--- a/src/version.c
+++ b/src/version.c
@@ -704,6 +704,8 @@ static char *(features[]) =
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    379,
+/**/
     378,
 /**/
     377,
--- a/src/vim9cmds.c
+++ b/src/vim9cmds.c
@@ -1685,6 +1685,27 @@ compile_eval(char_u *arg, cctx_T *cctx)
 }
 
 /*
+ * Get the local variable index for deferred function calls.
+ * Reserve it when not done already.
+ * Returns zero for failure.
+ */
+    int
+get_defer_var_idx(cctx_T *cctx)
+{
+    dfunc_T	*dfunc = ((dfunc_T *)def_functions.ga_data)
+					       + cctx->ctx_ufunc->uf_dfunc_idx;
+    if (dfunc->df_defer_var_idx == 0)
+    {
+	lvar_T *lvar = reserve_local(cctx, (char_u *)"@defer@", 7,
+							    TRUE, &t_list_any);
+	if (lvar == NULL)
+	    return 0;
+	dfunc->df_defer_var_idx = lvar->lv_idx + 1;
+    }
+    return dfunc->df_defer_var_idx;
+}
+
+/*
  * Compile "defer func(arg)".
  */
     char_u *
@@ -1693,7 +1714,7 @@ compile_defer(char_u *arg_start, cctx_T 
     char_u	*p;
     char_u	*arg = arg_start;
     int		argcount = 0;
-    dfunc_T	*dfunc;
+    int		defer_var_idx;
     type_T	*type;
     int		func_idx;
 
@@ -1730,16 +1751,10 @@ compile_defer(char_u *arg_start, cctx_T 
 
     // TODO: check argument count with "type"
 
-    dfunc = ((dfunc_T *)def_functions.ga_data) + cctx->ctx_ufunc->uf_dfunc_idx;
-    if (dfunc->df_defer_var_idx == 0)
-    {
-	lvar_T *lvar = reserve_local(cctx, (char_u *)"@defer@", 7,
-							    TRUE, &t_list_any);
-	if (lvar == NULL)
-	    return NULL;
-	dfunc->df_defer_var_idx = lvar->lv_idx + 1;
-    }
-    if (generate_DEFER(cctx, dfunc->df_defer_var_idx - 1, argcount) == FAIL)
+    defer_var_idx = get_defer_var_idx(cctx);
+    if (defer_var_idx == 0)
+	return NULL;
+    if (generate_DEFER(cctx, defer_var_idx - 1, argcount) == FAIL)
 	return NULL;
 
     return skipwhite(arg);
--- a/src/vim9execute.c
+++ b/src/vim9execute.c
@@ -845,40 +845,70 @@ set_ref_in_funcstacks(int copyID)
     return FALSE;
 }
 
+// Ugly static to avoid passing the execution context around through many
+// layers.
+static ectx_T *current_ectx = NULL;
+
+/*
+ * Return TRUE if currently executing a :def function.
+ * Can be used by builtin functions only.
+ */
+    int
+in_def_function(void)
+{
+    return current_ectx != NULL;
+}
+
+/*
+ * Add an entry for a deferred function call to the currently executing
+ * function.
+ * Return the list or NULL when failed.
+ */
+    static list_T *
+add_defer_item(int var_idx, int argcount, ectx_T *ectx)
+{
+    typval_T	*defer_tv = STACK_TV_VAR(var_idx);
+    list_T	*defer_l;
+    list_T	*l;
+    typval_T	listval;
+
+    if (defer_tv->v_type != VAR_LIST)
+    {
+	// first time, allocate the list
+	if (rettv_list_alloc(defer_tv) == FAIL)
+	    return NULL;
+    }
+    defer_l = defer_tv->vval.v_list;
+
+    l = list_alloc_with_items(argcount + 1);
+    if (l == NULL)
+	return NULL;
+    listval.v_type = VAR_LIST;
+    listval.vval.v_list = l;
+    listval.v_lock = 0;
+    if (list_insert_tv(defer_l, &listval, defer_l->lv_first) == FAIL)
+    {
+	vim_free(l);
+	return NULL;
+    }
+
+    return l;
+}
+
 /*
  * Handle ISN_DEFER.  Stack has a function reference and "argcount" arguments.
  * The local variable that lists deferred functions is "var_idx".
  * Returns OK or FAIL.
  */
     static int
-add_defer_func(int var_idx, int argcount, ectx_T *ectx)
+defer_command(int var_idx, int argcount, ectx_T *ectx)
 {
-    typval_T	*defer_tv = STACK_TV_VAR(var_idx);
-    list_T	*defer_l;
-    typval_T	*func_tv;
-    list_T	*l;
+    list_T	*l = add_defer_item(var_idx, argcount, ectx);
     int		i;
-    typval_T	listval;
-
-    if (defer_tv->v_type != VAR_LIST)
-    {
-	// first time, allocate the list
-	if (rettv_list_alloc(defer_tv) == FAIL)
-	    return FAIL;
-    }
-    defer_l = defer_tv->vval.v_list;
-
-    l = list_alloc_with_items(argcount + 1);
+    typval_T	*func_tv;
+
     if (l == NULL)
 	return FAIL;
-    listval.v_type = VAR_LIST;
-    listval.vval.v_list = l;
-    listval.v_lock = 0;
-    if (list_insert_tv(defer_l, &listval, defer_l->lv_first) == FAIL)
-    {
-	vim_free(l);
-	return FAIL;
-    }
 
     func_tv = STACK_TV_BOT(-argcount - 1);
     // TODO: check type is a funcref
@@ -891,6 +921,43 @@ add_defer_func(int var_idx, int argcount
 }
 
 /*
+ * Add a deferred function "name" with one argument "arg_tv".
+ * Consumes "name", also on failure.
+ * Only to be called when in_def_function() returns TRUE.
+ */
+    int
+add_defer_function(char_u *name, int argcount, typval_T *argvars)
+{
+    dfunc_T	*dfunc = ((dfunc_T *)def_functions.ga_data)
+						  + current_ectx->ec_dfunc_idx;
+    list_T	*l;
+    typval_T	func_tv;
+    int		i;
+
+    if (dfunc->df_defer_var_idx == 0)
+    {
+	iemsg("df_defer_var_idx is zero");
+	vim_free(func_tv.vval.v_string);
+	return FAIL;
+    }
+    func_tv.v_type = VAR_FUNC;
+    func_tv.v_lock = 0;
+    func_tv.vval.v_string = name;
+
+    l = add_defer_item(dfunc->df_defer_var_idx - 1, 1, current_ectx);
+    if (l == NULL)
+    {
+	vim_free(func_tv.vval.v_string);
+	return FAIL;
+    }
+
+    list_set_item(l, 0, &func_tv);
+    for (i = 0; i < argcount; ++i)
+	list_set_item(l, i + 1, argvars + i);
+    return OK;
+}
+
+/*
  * Invoked when returning from a function: Invoke any deferred calls.
  */
     static void
@@ -1068,10 +1135,6 @@ call_prepare(int argcount, typval_T *arg
     return OK;
 }
 
-// Ugly global to avoid passing the execution context around through many
-// layers.
-static ectx_T *current_ectx = NULL;
-
 /*
  * Call a builtin function by index.
  */
@@ -3748,7 +3811,7 @@ exec_instructions(ectx_T *ectx)
 
 	    // :defer func(arg)
 	    case ISN_DEFER:
-		if (add_defer_func(iptr->isn_arg.defer.defer_var_idx,
+		if (defer_command(iptr->isn_arg.defer.defer_var_idx,
 			     iptr->isn_arg.defer.defer_argcount, ectx) == FAIL)
 		    goto on_error;
 		break;
@@ -5121,7 +5184,7 @@ theend:
 }
 
 /*
- * Execute the instructions from a VAR_INSTR typeval and put the result in
+ * Execute the instructions from a VAR_INSTR typval and put the result in
  * "rettv".
  * Return OK or FAIL.
  */
--- a/src/vim9expr.c
+++ b/src/vim9expr.c
@@ -833,6 +833,14 @@ compile_call(
 		}
 	    }
 
+	    if (STRCMP(name, "writefile") == 0 && argcount > 2)
+	    {
+		// May have the "D" flag, reserve a variable for a deferred
+		// function call.
+		if (get_defer_var_idx(cctx) == 0)
+		    idx = -1;
+	    }
+
 	    if (idx >= 0)
 		res = generate_BCALL(cctx, idx, argcount, argcount_init == 1);
 	}