diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt index cb05718ab9..adb3164429 100644 --- a/runtime/doc/builtin.txt +++ b/runtime/doc/builtin.txt @@ -9507,31 +9507,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" fsync() call is not used, with "s" - it is used, 'fsync' option applies by default. No fsync() - means that writefile() will finish faster, but writes may be - left in OS buffers and not yet written to disk. Such changes - will disappear if system crashes before OS does writing. - 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") diff --git a/src/nvim/eval/funcs.c b/src/nvim/eval/funcs.c index 99e511a7a4..66fd663e5e 100644 --- a/src/nvim/eval/funcs.c +++ b/src/nvim/eval/funcs.c @@ -9296,6 +9296,7 @@ static void f_writefile(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) bool binary = false; bool append = false; + bool defer = false; bool do_fsync = !!p_fs; bool mkdir_p = false; if (argvars[2].v_type != VAR_UNKNOWN) { @@ -9309,6 +9310,8 @@ static void f_writefile(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) binary = true; break; case 'a': append = true; break; + case 'D': + defer = true; break; case 's': do_fsync = true; break; case 'S': @@ -9328,6 +9331,12 @@ static void f_writefile(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) if (fname == NULL) { return; } + + if (defer && get_current_funccal() == NULL) { + semsg(_(e_str_not_inside_function), "defer"); + return; + } + FileDescriptor fp; int error; if (*fname == NUL) { @@ -9336,9 +9345,17 @@ static void f_writefile(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) ((append ? kFileAppend : kFileTruncate) | (mkdir_p ? kFileMkDir : kFileCreate) | kFileCreate), 0666)) != 0) { - semsg(_("E482: Can't open file %s for writing: %s"), - fname, os_strerror(error)); + semsg(_("E482: Can't open file %s for writing: %s"), fname, os_strerror(error)); } else { + if (defer) { + typval_T tv = { + .v_type = VAR_STRING, + .v_lock = VAR_UNLOCKED, + .vval.v_string = xstrdup(fname), + }; + add_defer("delete", 1, &tv); + } + bool write_ok; if (argvars[0].v_type == VAR_BLOB) { write_ok = write_blob(&fp, argvars[0].vval.v_blob); diff --git a/src/nvim/eval/userfunc.c b/src/nvim/eval/userfunc.c index 9853622ee0..4b9bc1fdec 100644 --- a/src/nvim/eval/userfunc.c +++ b/src/nvim/eval/userfunc.c @@ -478,6 +478,7 @@ void emsg_funcname(const char *errmsg, const char *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(char **arg, evalarg_T *const evalarg, int partial_argc, typval_T *argvars, int *argcount) { @@ -3119,16 +3120,28 @@ static int ex_defer_inner(char *name, char **arg, evalarg_T *const evalarg) { typval_T argvars[MAX_FUNC_ARGS + 1]; // vars for arguments int argcount = 0; // number of arguments found - int ret = FAIL; if (current_funccal == NULL) { semsg(_(e_str_not_inside_function), "defer"); return FAIL; } if (get_func_arguments(arg, evalarg, false, argvars, &argcount) == FAIL) { - goto theend; + while (--argcount >= 0) { + tv_clear(&argvars[argcount]); + } + return FAIL; } + add_defer(name, argcount, argvars); + return OK; +} + +/// Add a deferred call for "name" with arguments "argvars[argcount]". +/// Consumes "argvars[]". +/// Caller must check that current_funccal is not NULL. +void add_defer(char *name, int argcount_arg, typval_T *argvars) +{ char *saved_name = xstrdup(name); + int argcount = argcount_arg; if (current_funccal->fc_defer.ga_itemsize == 0) { ga_init(¤t_funccal->fc_defer, sizeof(defer_T), 10); @@ -3140,13 +3153,6 @@ static int ex_defer_inner(char *name, char **arg, evalarg_T *const evalarg) argcount--; dr->dr_argvars[argcount] = argvars[argcount]; } - ret = OK; - -theend: - while (--argcount >= 0) { - tv_clear(&argvars[argcount]); - } - return ret; } /// Invoked after a function has finished: invoke ":defer" functions. diff --git a/test/old/testdir/test_quickfix.vim b/test/old/testdir/test_quickfix.vim index 838c4b1c15..f720b6e42d 100644 --- a/test/old/testdir/test_quickfix.vim +++ b/test/old/testdir/test_quickfix.vim @@ -6250,28 +6250,6 @@ func Test_very_long_error_line() call setqflist([], 'f') endfunc -" The test depends on deferred delete and string interpolation, which haven't -" been ported, so override it with a rewrite that doesn't use these features. -func! Test_very_long_error_line() - let msg = repeat('abcdefghijklmn', 146) - let emsg = 'Xlonglines.c:1:' . msg - call writefile([msg, emsg], 'Xerror') - cfile Xerror - call delete('Xerror') - cwindow - call assert_equal('|| ' .. msg, getline(1)) - call assert_equal('Xlonglines.c|1| ' .. msg, getline(2)) - cclose - - let l = execute('clist!')->split("\n") - call assert_equal([' 1: ' .. msg, ' 2 Xlonglines.c:1: ' .. msg], l) - - let l = execute('cc')->split("\n") - call assert_equal(['(2 of 2): ' .. msg], l) - - call setqflist([], 'f') -endfunc - " In the quickfix window, spaces at the beginning of an informational line " should not be removed but should be removed from an error line. func Test_info_line_with_space() diff --git a/test/old/testdir/test_writefile.vim b/test/old/testdir/test_writefile.vim index 6019cee193..5e6428ded8 100644 --- a/test/old/testdir/test_writefile.vim +++ b/test/old/testdir/test_writefile.vim @@ -924,19 +924,36 @@ endfunc " Test for ':write ++bin' and ':write ++nobin' func Test_write_binary_file() " create a file without an eol/eof character - call writefile(0z616161, 'Xfile1', 'b') - new Xfile1 - write ++bin Xfile2 - write ++nobin Xfile3 - call assert_equal(0z616161, readblob('Xfile2')) + call writefile(0z616161, 'Xwbfile1', 'b') + new Xwbfile1 + write ++bin Xwbfile2 + write ++nobin Xwbfile3 + call assert_equal(0z616161, readblob('Xwbfile2')) if has('win32') - call assert_equal(0z6161610D.0A, readblob('Xfile3')) + call assert_equal(0z6161610D.0A, readblob('Xwbfile3')) else - call assert_equal(0z6161610A, readblob('Xfile3')) + call assert_equal(0z6161610A, readblob('Xwbfile3')) endif - call delete('Xfile1') - call delete('Xfile2') - call delete('Xfile3') + call delete('Xwbfile1') + call delete('Xwbfile2') + 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