diff options
-rw-r--r-- | runtime/doc/builtin.txt | 60 | ||||
-rw-r--r-- | runtime/doc/userfunc.txt | 60 | ||||
-rw-r--r-- | src/nvim/eval/funcs.c | 56 | ||||
-rw-r--r-- | src/nvim/eval/typval_defs.h | 1 | ||||
-rw-r--r-- | src/nvim/eval/userfunc.c | 272 | ||||
-rw-r--r-- | src/nvim/ex_cmds.lua | 12 | ||||
-rw-r--r-- | src/nvim/ex_eval.c | 2 | ||||
-rw-r--r-- | src/nvim/fileio.c | 4 | ||||
-rw-r--r-- | src/nvim/globals.h | 2 | ||||
-rw-r--r-- | src/nvim/log.c | 2 | ||||
-rw-r--r-- | src/nvim/main.c | 4 | ||||
-rw-r--r-- | src/nvim/memline.c | 2 | ||||
-rw-r--r-- | src/nvim/os/fs.c | 11 | ||||
-rw-r--r-- | src/nvim/shada.c | 2 | ||||
-rw-r--r-- | src/nvim/undo.c | 2 | ||||
-rw-r--r-- | test/old/testdir/test_autochdir.vim | 19 | ||||
-rw-r--r-- | test/old/testdir/test_autocmd.vim | 17 | ||||
-rw-r--r-- | test/old/testdir/test_eval_stuff.vim | 64 | ||||
-rw-r--r-- | test/old/testdir/test_quickfix.vim | 22 | ||||
-rw-r--r-- | test/old/testdir/test_user_func.vim | 102 | ||||
-rw-r--r-- | test/old/testdir/test_writefile.vim | 50 | ||||
-rw-r--r-- | test/unit/os/fs_spec.lua | 51 |
22 files changed, 643 insertions, 174 deletions
diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt index cb05718ab9..15ccfd9b92 100644 --- a/runtime/doc/builtin.txt +++ b/runtime/doc/builtin.txt @@ -5572,8 +5572,24 @@ mkdir({name} [, {flags} [, {prot}]]) When {flags} is present it must be a string. An empty string has no effect. - If {flags} is "p" then intermediate directories are created as - necessary. + If {flags} contains "p" then intermediate directories are + created as necessary. + + If {flags} contains "D" then {name} is deleted at the end of + the current function, as with: > + defer delete({name}, 'd') +< + If {flags} contains "R" then {name} is deleted recursively at + the end of the current function, as with: > + defer delete({name}, 'rf') +< Note that when {name} has more than one part and "p" is used + some directories may already exist. Only the first one that + is created and what it contains is scheduled to be deleted. + E.g. when using: > + call mkdir('subdir/tmp/autoload', 'pR') +< and "subdir" already exists then "subdir/tmp" will be + scheduled for deletion, like with: > + defer delete('subdir/tmp', 'rf') If {prot} is given it is used to set the protection bits of the new directory. The default is 0o755 (rwxr-xr-x: r/w for @@ -9507,31 +9523,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. + 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. + unmodified, also when binary mode is not specified. - When {flags} contains "a" then append mode is used, lines are - appended to the file: > + {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") < - 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. + '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. - All NL characters are replaced with a NUL character. - Inserting CR characters needs to be done before passing {list} - to writefile(). 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/runtime/doc/userfunc.txt b/runtime/doc/userfunc.txt index ce6f2fc2e9..db0127df95 100644 --- a/runtime/doc/userfunc.txt +++ b/runtime/doc/userfunc.txt @@ -350,10 +350,68 @@ A function can also be called as part of evaluating an expression or when it is used as a method: > let x = GetList() let y = GetList()->Filter() +< +============================================================================== +3. Cleaning up in a function ~ + *:defer* +:defer {func}({args}) Call {func} when the current function is done. + {args} are evaluated here. + +Quite often a command in a function has a global effect, which must be undone +when the function finishes. Handling this in all kinds of situations can be a +hassle. Especially when an unexpected error is encountered. This can be done +with `try` / `finally` blocks, but this gets complicated when there is more +than one. + +A much simpler solution is using `defer`. It schedules a function call when +the function is returning, no matter if there is an error. Example: > + func Filter(text) abort + call writefile(a:text, 'Tempfile') + call system('filter < Tempfile > Outfile') + call Handle('Outfile') + call delete('Tempfile') + call delete('Outfile') + endfunc + +Here 'Tempfile' and 'Outfile' will not be deleted if something causes the +function to abort. `:defer` can be used to avoid that: > + func Filter(text) abort + call writefile(a:text, 'Tempfile') + defer delete('Tempfile') + defer delete('Outfile') + call system('filter < Tempfile > Outfile') + call Handle('Outfile') + endfunc + +Note that deleting "Outfile" is scheduled before calling `system()`, since it +can be created even when `system()` fails. + +The deferred functions are called in reverse order, the last one added is +executed first. A useless example: > + func Useless() abort + for s in range(3) + defer execute('echomsg "number ' .. s .. '"') + endfor + endfunc + +Now `:messages` shows: + number 2 + number 1 + number 0 + +Any return value of the deferred function is discarded. The function cannot +be followed by anything, such as "->func" or ".member". Currently `:defer +GetArg()->TheFunc()` does not work, it may work in a later version. + +Errors are reported but do not cause aborting execution of deferred functions. + +No range is accepted. The function can be a partial with extra arguments, but +not with a dictionary. *E1300* ============================================================================== -3. Automatically loading functions ~ + +4. Automatically loading functions ~ *autoload-functions* When using many or large functions, it's possible to automatically define them only when they are used. There are two methods: with an autocommand and with diff --git a/src/nvim/eval/funcs.c b/src/nvim/eval/funcs.c index 99e511a7a4..f53b283c79 100644 --- a/src/nvim/eval/funcs.c +++ b/src/nvim/eval/funcs.c @@ -4892,6 +4892,9 @@ static void f_mkdir(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) *path_tail_with_sep((char *)dir) = NUL; } + bool defer = false; + bool defer_recurse = false; + char *created = NULL; if (argvars[1].v_type != VAR_UNKNOWN) { if (argvars[2].v_type != VAR_UNKNOWN) { prot = (int)tv_get_number_chk(&argvars[2], NULL); @@ -4899,9 +4902,17 @@ static void f_mkdir(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) return; } } - if (strcmp(tv_get_string(&argvars[1]), "p") == 0) { + const char *arg2 = tv_get_string(&argvars[1]); + defer = vim_strchr(arg2, 'D') != NULL; + defer_recurse = vim_strchr(arg2, 'R') != NULL; + if ((defer || defer_recurse) && !can_add_defer()) { + return; + } + + if (vim_strchr(arg2, 'p') != NULL) { char *failed_dir; - int ret = os_mkdir_recurse(dir, prot, &failed_dir); + int ret = os_mkdir_recurse(dir, prot, &failed_dir, + defer || defer_recurse ? &created : NULL); if (ret != 0) { semsg(_(e_mkdir), failed_dir, os_strerror(ret)); xfree(failed_dir); @@ -4909,10 +4920,27 @@ static void f_mkdir(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) return; } rettv->vval.v_number = OK; - return; } } - rettv->vval.v_number = vim_mkdir_emsg(dir, prot); + if (rettv->vval.v_number == FAIL) { + rettv->vval.v_number = vim_mkdir_emsg(dir, prot); + } + + // Handle "D" and "R": deferred deletion of the created directory. + if (rettv->vval.v_number == OK + && created == NULL && (defer || defer_recurse)) { + created = FullName_save(dir, false); + } + if (created != NULL) { + typval_T tv[2]; + tv[0].v_type = VAR_STRING; + tv[0].v_lock = VAR_UNLOCKED; + tv[0].vval.v_string = created; + tv[1].v_type = VAR_STRING; + tv[1].v_lock = VAR_UNLOCKED; + tv[1].vval.v_string = xstrdup(defer_recurse ? "rf" : "d"); + add_defer("delete", 2, tv); + } } /// "mode()" function @@ -9296,6 +9324,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 +9338,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 +9359,11 @@ static void f_writefile(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) if (fname == NULL) { return; } + + if (defer && !can_add_defer()) { + return; + } + FileDescriptor fp; int error; if (*fname == NUL) { @@ -9336,9 +9372,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 = FullName_save(fname, false), + }; + 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/typval_defs.h b/src/nvim/eval/typval_defs.h index 80432271b0..517e59f3c2 100644 --- a/src/nvim/eval/typval_defs.h +++ b/src/nvim/eval/typval_defs.h @@ -299,6 +299,7 @@ struct funccall_S { linenr_T breakpoint; ///< Next line with breakpoint or zero. int dbg_tick; ///< debug_tick when breakpoint was set. int level; ///< Top nesting level of executed function. + garray_T fc_defer; ///< Functions to be called on return. proftime_T prof_child; ///< Time spent in a child. funccall_T *caller; ///< Calling function or NULL; or next funccal in ///< list pointed to by previous_funccal. diff --git a/src/nvim/eval/userfunc.c b/src/nvim/eval/userfunc.c index a348588106..f91962ac09 100644 --- a/src/nvim/eval/userfunc.c +++ b/src/nvim/eval/userfunc.c @@ -53,6 +53,13 @@ # include "eval/userfunc.c.generated.h" #endif +/// structure used as item in "fc_defer" +typedef struct { + char *dr_name; ///< function name, allocated + typval_T dr_argvars[MAX_FUNC_ARGS + 1]; + int dr_argcount; +} defer_T; + static hashtab_T func_hashtab; // Used by get_func_tv() @@ -74,6 +81,8 @@ static const char e_no_white_space_allowed_before_str_str[] = N_("E1068: No white space allowed before '%s': %s"); static const char e_missing_heredoc_end_marker_str[] = N_("E1145: Missing heredoc end marker: %s"); +static const char e_cannot_use_partial_with_dictionary_for_defer[] + = N_("E1300: Cannot use a partial with dictionary for :defer"); void func_init(void) { @@ -469,45 +478,63 @@ void emsg_funcname(const char *errmsg, const char *name) } } -/// Allocate a variable for the result of a function. -/// -/// @param name name of the function -/// @param len length of "name" or -1 to use strlen() -/// @param arg argument, pointing to the '(' -/// @param funcexe various values -/// -/// @return OK or FAIL. -int get_func_tv(const char *name, int len, typval_T *rettv, char **arg, evalarg_T *const evalarg, - funcexe_T *funcexe) +/// 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) { - char *argp; + char *argp = *arg; int ret = OK; - typval_T argvars[MAX_FUNC_ARGS + 1]; // vars for arguments - int argcount = 0; // number of arguments found - const bool evaluate = evalarg == NULL ? false : (evalarg->eval_flags & EVAL_EVALUATE); // Get the arguments. - argp = *arg; - while (argcount < MAX_FUNC_ARGS - - (funcexe->fe_partial == NULL ? 0 : funcexe->fe_partial->pt_argc)) { + while (*argcount < MAX_FUNC_ARGS - partial_argc) { argp = skipwhite(argp + 1); // skip the '(' or ',' + if (*argp == ')' || *argp == ',' || *argp == NUL) { break; } - if (eval1(&argp, &argvars[argcount], evalarg) == FAIL) { + if (eval1(&argp, &argvars[*argcount], evalarg) == FAIL) { ret = FAIL; break; } - argcount++; + (*argcount)++; if (*argp != ',') { break; } } + + argp = skipwhite(argp); if (*argp == ')') { argp++; } else { ret = FAIL; } + *arg = argp; + return ret; +} + +/// Call a function and put the result in "rettv". +/// +/// @param name name of the function +/// @param len length of "name" or -1 to use strlen() +/// @param arg argument, pointing to the '(' +/// @param funcexe various values +/// +/// @return OK or FAIL. +int get_func_tv(const char *name, int len, typval_T *rettv, char **arg, evalarg_T *const evalarg, + funcexe_T *funcexe) +{ + typval_T argvars[MAX_FUNC_ARGS + 1]; // vars for arguments + int argcount = 0; // number of arguments found + const bool evaluate = evalarg == NULL ? false : (evalarg->eval_flags & EVAL_EVALUATE); + + char *argp = *arg; + int ret = get_func_arguments(&argp, evalarg, + (funcexe->fe_partial == NULL + ? 0 + : funcexe->fe_partial->pt_argc), + argvars, &argcount); if (ret == OK) { int i = 0; @@ -1148,6 +1175,9 @@ void call_user_func(ufunc_T *fp, int argcount, typval_T *argvars, typval_T *rett DOCMD_NOWAIT|DOCMD_VERBOSE|DOCMD_REPEAT); } + // Invoke functions added with ":defer". + handle_defer_one(current_funccal); + RedrawingDisabled--; // when the function was aborted because of an error, return -1 @@ -1544,7 +1574,7 @@ int call_func(const char *funcname, int len, typval_T *rettv, int argcount_in, t int argv_base = 0; partial_T *partial = funcexe->fe_partial; - // Initialize rettv so that it is safe for caller to invoke clear_tv(rettv) + // Initialize rettv so that it is safe for caller to invoke tv_clear(rettv) // even when call_func() returns FAIL. rettv->v_type = VAR_UNKNOWN; @@ -3033,7 +3063,156 @@ void ex_return(exarg_T *eap) clear_evalarg(&evalarg, eap); } +/// Lower level implementation of "call". Only called when not skipping. +static int ex_call_inner(exarg_T *eap, char *name, char **arg, char *startarg, + const funcexe_T *const funcexe_init, evalarg_T *const evalarg) +{ + bool doesrange; + bool failed = false; + + for (linenr_T lnum = eap->line1; lnum <= eap->line2; lnum++) { + if (eap->addr_count > 0) { + if (lnum > curbuf->b_ml.ml_line_count) { + // If the function deleted lines or switched to another buffer + // the line number may become invalid. + emsg(_(e_invrange)); + break; + } + curwin->w_cursor.lnum = lnum; + curwin->w_cursor.col = 0; + curwin->w_cursor.coladd = 0; + } + *arg = startarg; + + funcexe_T funcexe = *funcexe_init; + funcexe.fe_doesrange = &doesrange; + typval_T rettv; + rettv.v_type = VAR_UNKNOWN; // tv_clear() uses this + if (get_func_tv(name, -1, &rettv, arg, evalarg, &funcexe) == FAIL) { + failed = true; + break; + } + + // Handle a function returning a Funcref, Dictionary or List. + if (handle_subscript((const char **)arg, &rettv, &EVALARG_EVALUATE, true) == FAIL) { + failed = true; + break; + } + + tv_clear(&rettv); + if (doesrange) { + break; + } + + // Stop when immediately aborting on error, or when an interrupt + // occurred or an exception was thrown but not caught. + // get_func_tv() returned OK, so that the check for trailing + // characters below is executed. + if (aborting()) { + break; + } + } + + return failed; +} + +/// Core part of ":defer func(arg)". "arg" points to the "(" and is advanced. +/// +/// @return FAIL or OK. +static int ex_defer_inner(char *name, char **arg, const partial_T *const partial, + evalarg_T *const evalarg) +{ + typval_T argvars[MAX_FUNC_ARGS + 1]; // vars for arguments + int partial_argc = 0; // number of partial arguments + int argcount = 0; // number of arguments found + + if (current_funccal == NULL) { + semsg(_(e_str_not_inside_function), "defer"); + return FAIL; + } + if (partial != NULL) { + if (partial->pt_dict != NULL) { + emsg(_(e_cannot_use_partial_with_dictionary_for_defer)); + return FAIL; + } + if (partial->pt_argc > 0) { + partial_argc = partial->pt_argc; + for (int i = 0; i < partial_argc; i++) { + tv_copy(&partial->pt_argv[i], &argvars[i]); + } + } + } + int r = get_func_arguments(arg, evalarg, false, argvars + partial_argc, &argcount); + argcount += partial_argc; + if (r == FAIL) { + while (--argcount >= 0) { + tv_clear(&argvars[argcount]); + } + return FAIL; + } + add_defer(name, argcount, argvars); + return OK; +} + +/// Return true if currently inside a function call. +/// Give an error message and return FALSE when not. +bool can_add_defer(void) +{ + if (get_current_funccal() == NULL) { + semsg(_(e_str_not_inside_function), "defer"); + return false; + } + return true; +} + +/// 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); + } + defer_T *dr = GA_APPEND_VIA_PTR(defer_T, ¤t_funccal->fc_defer); + dr->dr_name = saved_name; + dr->dr_argcount = argcount; + while (argcount > 0) { + argcount--; + dr->dr_argvars[argcount] = argvars[argcount]; + } +} + +/// Invoked after a function has finished: invoke ":defer" functions. +static void handle_defer_one(funccall_T *funccal) +{ + for (int idx = funccal->fc_defer.ga_len - 1; idx >= 0; idx--) { + defer_T *dr = ((defer_T *)funccal->fc_defer.ga_data) + idx; + funcexe_T funcexe = { .fe_evaluate = true }; + typval_T rettv; + rettv.v_type = VAR_UNKNOWN; // tv_clear() uses this + call_func(dr->dr_name, -1, &rettv, dr->dr_argcount, dr->dr_argvars, &funcexe); + tv_clear(&rettv); + xfree(dr->dr_name); + for (int i = dr->dr_argcount - 1; i >= 0; i--) { + tv_clear(&dr->dr_argvars[i]); + } + } + ga_clear(&funccal->fc_defer); +} + +/// Called when exiting: call all defer functions. +void invoke_all_defer(void) +{ + for (funccall_T *funccal = current_funccal; funccal != NULL; funccal = funccal->caller) { + handle_defer_one(funccal); + } +} + /// ":1,25call func(arg1, arg2)" function call. +/// ":defer func(arg1, arg2)" deferred function call. void ex_call(exarg_T *eap) { char *arg = eap->arg; @@ -3041,9 +3220,6 @@ void ex_call(exarg_T *eap) char *name; char *tofree; int len; - typval_T rettv; - linenr_T lnum; - bool doesrange; bool failed = false; funcdict_T fudi; partial_T *partial = NULL; @@ -3051,6 +3227,7 @@ void ex_call(exarg_T *eap) fill_evalarg_from_eap(&evalarg, eap, eap->skip); if (eap->skip) { + typval_T rettv; // trans_function_name() doesn't work well when skipping, use eval0() // instead to skip to any following command, e.g. for: // :if 0 | call dict.foo().bar() | endif. @@ -3089,59 +3266,24 @@ void ex_call(exarg_T *eap) // Skip white space to allow ":call func ()". Not good, but required for // backward compatibility. startarg = skipwhite(arg); - rettv.v_type = VAR_UNKNOWN; // tv_clear() uses this. if (*startarg != '(') { semsg(_(e_missingparen), eap->arg); goto end; } - lnum = eap->line1; - for (; lnum <= eap->line2; lnum++) { - if (eap->addr_count > 0) { // -V560 - if (lnum > curbuf->b_ml.ml_line_count) { - // If the function deleted lines or switched to another buffer - // the line number may become invalid. - emsg(_(e_invrange)); - break; - } - curwin->w_cursor.lnum = lnum; - curwin->w_cursor.col = 0; - curwin->w_cursor.coladd = 0; - } + if (eap->cmdidx == CMD_defer) { arg = startarg; - + failed = ex_defer_inner(name, &arg, partial, &evalarg) == FAIL; + } else { funcexe_T funcexe = FUNCEXE_INIT; - funcexe.fe_firstline = eap->line1; - funcexe.fe_lastline = eap->line2; - funcexe.fe_doesrange = &doesrange; - funcexe.fe_evaluate = true; funcexe.fe_partial = partial; funcexe.fe_selfdict = fudi.fd_dict; + funcexe.fe_firstline = eap->line1; + funcexe.fe_lastline = eap->line2; funcexe.fe_found_var = found_var; - if (get_func_tv(name, -1, &rettv, &arg, &evalarg, &funcexe) == FAIL) { - failed = true; - break; - } - - // Handle a function returning a Funcref, Dictionary or List. - if (handle_subscript((const char **)&arg, &rettv, &EVALARG_EVALUATE, true) == FAIL) { - failed = true; - break; - } - - tv_clear(&rettv); - if (doesrange) { - break; - } - - // Stop when immediately aborting on error, or when an interrupt - // occurred or an exception was thrown but not caught. - // get_func_tv() returned OK, so that the check for trailing - // characters below is executed. - if (aborting()) { - break; - } + funcexe.fe_evaluate = true; + failed = ex_call_inner(eap, name, &arg, startarg, &funcexe, &evalarg); } // When inside :try we need to check for following "| catch" or "| endtry". diff --git a/src/nvim/ex_cmds.lua b/src/nvim/ex_cmds.lua index be6299db0e..845ea4bb15 100644 --- a/src/nvim/ex_cmds.lua +++ b/src/nvim/ex_cmds.lua @@ -715,6 +715,18 @@ module.cmds = { func='ex_debuggreedy', }, { + command='def', + flags=bit.bor(EXTRA, BANG, SBOXOK, CMDWIN, LOCK_OK), + addr_type='ADDR_NONE', + func='ex_ni', + }, + { + command='defer', + flags=bit.bor(NEEDARG, EXTRA, NOTRLCOM, CMDWIN, LOCK_OK), + addr_type='ADDR_NONE', + func='ex_call', + }, + { command='delcommand', flags=bit.bor(BANG, NEEDARG, WORD1, TRLBAR, CMDWIN, LOCK_OK), addr_type='ADDR_NONE', diff --git a/src/nvim/ex_eval.c b/src/nvim/ex_eval.c index 5404ae6731..12d1f3d9bd 100644 --- a/src/nvim/ex_eval.c +++ b/src/nvim/ex_eval.c @@ -1966,7 +1966,7 @@ void rewind_conditionals(cstack_T *cstack, int idx, int cond_type, int *cond_lev /// Handle ":endfunction" when not after a ":function" void ex_endfunction(exarg_T *eap) { - emsg(_("E193: :endfunction not inside a function")); + semsg(_(e_str_not_inside_function), ":endfunction"); } /// @return true if the string "p" looks like a ":while" or ":for" command. diff --git a/src/nvim/fileio.c b/src/nvim/fileio.c index d4725ccd86..fa2f72932f 100644 --- a/src/nvim/fileio.c +++ b/src/nvim/fileio.c @@ -2536,7 +2536,7 @@ static int buf_write_make_backup(char *fname, bool append, FileInfo *file_info_o if (*dirp == NUL && !os_isdir(IObuff)) { int ret; char *failed_dir; - if ((ret = os_mkdir_recurse(IObuff, 0755, &failed_dir)) != 0) { + if ((ret = os_mkdir_recurse(IObuff, 0755, &failed_dir, NULL)) != 0) { semsg(_("E303: Unable to create directory \"%s\" for backup file: %s"), failed_dir, os_strerror(ret)); xfree(failed_dir); @@ -2679,7 +2679,7 @@ nobackup: if (*dirp == NUL && !os_isdir(IObuff)) { int ret; char *failed_dir; - if ((ret = os_mkdir_recurse(IObuff, 0755, &failed_dir)) != 0) { + if ((ret = os_mkdir_recurse(IObuff, 0755, &failed_dir, NULL)) != 0) { semsg(_("E303: Unable to create directory \"%s\" for backup file: %s"), failed_dir, os_strerror(ret)); xfree(failed_dir); diff --git a/src/nvim/globals.h b/src/nvim/globals.h index 3c31d07e33..0d4f02eaee 100644 --- a/src/nvim/globals.h +++ b/src/nvim/globals.h @@ -986,6 +986,8 @@ EXTERN const char e_maxmempat[] INIT(= N_("E363: pattern uses more memory than ' EXTERN const char e_emptybuf[] INIT(= N_("E749: empty buffer")); EXTERN const char e_nobufnr[] INIT(= N_("E86: Buffer %" PRId64 " does not exist")); +EXTERN const char e_str_not_inside_function[] INIT(= N_("E193: %s not inside a function")); + EXTERN const char e_invalpat[] INIT(= N_("E682: Invalid search pattern or delimiter")); EXTERN const char e_bufloaded[] INIT(= N_("E139: File is loaded in another buffer")); EXTERN const char e_notset[] INIT(= N_("E764: Option '%s' is not set")); diff --git a/src/nvim/log.c b/src/nvim/log.c index 77eeb09fec..4de0c4d88c 100644 --- a/src/nvim/log.c +++ b/src/nvim/log.c @@ -76,7 +76,7 @@ static void log_path_init(void) char *failed_dir = NULL; bool log_dir_failure = false; if (!os_isdir(loghome)) { - log_dir_failure = (os_mkdir_recurse(loghome, 0700, &failed_dir) != 0); + log_dir_failure = (os_mkdir_recurse(loghome, 0700, &failed_dir, NULL) != 0); } XFREE_CLEAR(loghome); // Invalid $NVIM_LOG_FILE or failed to expand; fall back to default. diff --git a/src/nvim/main.c b/src/nvim/main.c index 0ef1fc9391..698c2dcc4f 100644 --- a/src/nvim/main.c +++ b/src/nvim/main.c @@ -30,6 +30,7 @@ #include "nvim/eval.h" #include "nvim/eval/typval.h" #include "nvim/eval/typval_defs.h" +#include "nvim/eval/userfunc.h" #include "nvim/event/multiqueue.h" #include "nvim/event/stream.h" #include "nvim/ex_cmds.h" @@ -693,6 +694,9 @@ void getout(int exitval) // Position the cursor on the last screen line, below all the text ui_cursor_goto(Rows - 1, 0); + // Invoked all deferred functions in the function stack. + invoke_all_defer(); + // Optionally print hashtable efficiency. hash_debug_results(); diff --git a/src/nvim/memline.c b/src/nvim/memline.c index 18d5e75a53..0c38f18739 100644 --- a/src/nvim/memline.c +++ b/src/nvim/memline.c @@ -3442,7 +3442,7 @@ static char *findswapname(buf_T *buf, char **dirp, char *old_fname, bool *found_ } else if (!*found_existing_dir && **dirp == NUL) { int ret; char *failed_dir; - if ((ret = os_mkdir_recurse(dir_name, 0755, &failed_dir)) != 0) { + if ((ret = os_mkdir_recurse(dir_name, 0755, &failed_dir, NULL)) != 0) { semsg(_("E303: Unable to create directory \"%s\" for swap file, " "recovery impossible: %s"), failed_dir, os_strerror(ret)); diff --git a/src/nvim/os/fs.c b/src/nvim/os/fs.c index cb51e81005..872d9c9314 100644 --- a/src/nvim/os/fs.c +++ b/src/nvim/os/fs.c @@ -937,10 +937,13 @@ int os_mkdir(const char *path, int32_t mode) /// the name of the directory which os_mkdir_recurse /// failed to create. I.e. it will contain dir or any /// of the higher level directories. +/// @param[out] created Set to the full name of the first created directory. +/// It will be NULL until that happens. /// /// @return `0` for success, libuv error code for failure. -int os_mkdir_recurse(const char *const dir, int32_t mode, char **const failed_dir) - FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +int os_mkdir_recurse(const char *const dir, int32_t mode, char **const failed_dir, + char **const created) + FUNC_ATTR_NONNULL_ARG(1, 3) FUNC_ATTR_WARN_UNUSED_RESULT { // Get end of directory name in "dir". // We're done when it's "/" or "c:/". @@ -975,6 +978,8 @@ int os_mkdir_recurse(const char *const dir, int32_t mode, char **const failed_di if ((ret = os_mkdir(curdir, mode)) != 0) { *failed_dir = curdir; return ret; + } else if (created != NULL && *created == NULL) { + *created = FullName_save(curdir, false); } } xfree(curdir); @@ -1002,7 +1007,7 @@ int os_file_mkdir(char *fname, int32_t mode) *tail = NUL; int r; char *failed_dir; - if (((r = os_mkdir_recurse(fname, mode, &failed_dir)) < 0)) { + if (((r = os_mkdir_recurse(fname, mode, &failed_dir, NULL)) < 0)) { semsg(_(e_mkdir), failed_dir, os_strerror(r)); xfree(failed_dir); } diff --git a/src/nvim/shada.c b/src/nvim/shada.c index 78499922bf..fcb8a15cde 100644 --- a/src/nvim/shada.c +++ b/src/nvim/shada.c @@ -3042,7 +3042,7 @@ shada_write_file_nomerge: {} if (!os_isdir(fname)) { int ret; char *failed_dir; - if ((ret = os_mkdir_recurse(fname, 0700, &failed_dir)) != 0) { + if ((ret = os_mkdir_recurse(fname, 0700, &failed_dir, NULL)) != 0) { semsg(_(SERR "Failed to create directory %s " "for writing ShaDa file: %s"), failed_dir, os_strerror(ret)); diff --git a/src/nvim/undo.c b/src/nvim/undo.c index 132c84231f..7eb0d390fc 100644 --- a/src/nvim/undo.c +++ b/src/nvim/undo.c @@ -705,7 +705,7 @@ char *u_get_undo_file_name(const char *const buf_ffname, const bool reading) // Last directory in the list does not exist, create it. int ret; char *failed_dir; - if ((ret = os_mkdir_recurse(dir_name, 0755, &failed_dir)) != 0) { + if ((ret = os_mkdir_recurse(dir_name, 0755, &failed_dir, NULL)) != 0) { semsg(_("E5003: Unable to create directory \"%s\" for undo file: %s"), failed_dir, os_strerror(ret)); xfree(failed_dir); diff --git a/test/old/testdir/test_autochdir.vim b/test/old/testdir/test_autochdir.vim index a8810047a0..652ce8b794 100644 --- a/test/old/testdir/test_autochdir.vim +++ b/test/old/testdir/test_autochdir.vim @@ -30,9 +30,9 @@ func Test_set_filename_other_window() CheckFunction test_autochdir let cwd = getcwd() call test_autochdir() - call mkdir('Xa') - call mkdir('Xb') - call mkdir('Xc') + call mkdir('Xa', 'R') + call mkdir('Xb', 'R') + call mkdir('Xc', 'R') try args Xa/aaa.txt Xb/bbb.txt set acd @@ -44,9 +44,6 @@ func Test_set_filename_other_window() finally set noacd call chdir(cwd) - call delete('Xa', 'rf') - call delete('Xb', 'rf') - call delete('Xc', 'rf') bwipe! aaa.txt bwipe! bbb.txt bwipe! ccc.txt @@ -59,10 +56,10 @@ func Test_acd_win_execute() set acd call test_autochdir() - call mkdir('Xfile') + call mkdir('XacdDir', 'R') let winid = win_getid() - new Xfile/file - call assert_match('testdir.Xfile$', getcwd()) + new XacdDir/file + call assert_match('testdir.XacdDir$', getcwd()) cd .. call assert_match('testdir$', getcwd()) call win_execute(winid, 'echo') @@ -71,7 +68,6 @@ func Test_acd_win_execute() bwipe! set noacd call chdir(cwd) - call delete('Xfile', 'rf') endfunc func Test_verbose_pwd() @@ -82,7 +78,7 @@ func Test_verbose_pwd() edit global.txt call assert_match('\[global\].*testdir$', execute('verbose pwd')) - call mkdir('Xautodir') + call mkdir('Xautodir', 'R') split Xautodir/local.txt lcd Xautodir call assert_match('\[window\].*testdir[/\\]Xautodir', execute('verbose pwd')) @@ -116,7 +112,6 @@ func Test_verbose_pwd() bwipe! call chdir(cwd) - call delete('Xautodir', 'rf') endfunc func Test_multibyte() diff --git a/test/old/testdir/test_autocmd.vim b/test/old/testdir/test_autocmd.vim index 6d7f1649b3..ec671369f5 100644 --- a/test/old/testdir/test_autocmd.vim +++ b/test/old/testdir/test_autocmd.vim @@ -918,14 +918,13 @@ func Test_BufEnter() call assert_equal('++', g:val) " Also get BufEnter when editing a directory - call mkdir('Xdir') - split Xdir + call mkdir('Xbufenterdir', 'D') + split Xbufenterdir call assert_equal('+++', g:val) " On MS-Windows we can't edit the directory, make sure we wipe the right " buffer. - bwipe! Xdir - call delete('Xdir', 'd') + bwipe! Xbufenterdir au! BufEnter " Editing a "nofile" buffer doesn't read the file but does trigger BufEnter @@ -2186,11 +2185,10 @@ func Test_BufWriteCmd() new file Xbufwritecmd set buftype=acwrite - call mkdir('Xbufwritecmd') + call mkdir('Xbufwritecmd', 'D') write " BufWriteCmd should be triggered even if a directory has the same name call assert_equal(1, g:written) - call delete('Xbufwritecmd', 'd') unlet g:written au! BufWriteCmd bwipe! @@ -2947,16 +2945,15 @@ func Test_throw_in_BufWritePre() endfunc func Test_autocmd_in_try_block() - call mkdir('Xdir') + call mkdir('Xintrydir', 'R') au BufEnter * let g:fname = expand('%') try - edit Xdir/ + edit Xintrydir/ endtry - call assert_match('Xdir', g:fname) + call assert_match('Xintrydir', g:fname) unlet g:fname au! BufEnter - call delete('Xdir', 'rf') endfunc func Test_autocmd_CmdWinEnter() diff --git a/test/old/testdir/test_eval_stuff.vim b/test/old/testdir/test_eval_stuff.vim index 7acc91c17b..20eb873326 100644 --- a/test/old/testdir/test_eval_stuff.vim +++ b/test/old/testdir/test_eval_stuff.vim @@ -36,12 +36,70 @@ func Test_mkdir_p() endtry " 'p' doesn't suppress real errors call writefile([], 'Xfile') - call assert_fails('call mkdir("Xfile", "p")', 'E739') + call assert_fails('call mkdir("Xfile", "p")', 'E739:') call delete('Xfile') call delete('Xmkdir', 'rf') call assert_equal(0, mkdir(v:_null_string)) - call assert_fails('call mkdir([])', 'E730') - call assert_fails('call mkdir("abc", [], [])', 'E745') + call assert_fails('call mkdir([])', 'E730:') + call assert_fails('call mkdir("abc", [], [])', 'E745:') +endfunc + +func DoMkdirDel(name) + call mkdir(a:name, 'pD') + call assert_true(isdirectory(a:name)) +endfunc + +func DoMkdirDelAddFile(name) + call mkdir(a:name, 'pD') + call assert_true(isdirectory(a:name)) + call writefile(['text'], a:name .. '/file') +endfunc + +func DoMkdirDelRec(name) + call mkdir(a:name, 'pR') + call assert_true(isdirectory(a:name)) +endfunc + +func DoMkdirDelRecAddFile(name) + call mkdir(a:name, 'pR') + call assert_true(isdirectory(a:name)) + call writefile(['text'], a:name .. '/file') +endfunc + +func Test_mkdir_defer_del() + " Xtopdir/tmp is created thus deleted, not Xtopdir itself + call mkdir('Xtopdir', 'R') + call DoMkdirDel('Xtopdir/tmp') + call assert_true(isdirectory('Xtopdir')) + call assert_false(isdirectory('Xtopdir/tmp')) + + " Deletion fails because "tmp" contains "sub" + call DoMkdirDel('Xtopdir/tmp/sub') + call assert_true(isdirectory('Xtopdir')) + call assert_true(isdirectory('Xtopdir/tmp')) + call delete('Xtopdir/tmp', 'rf') + + " Deletion fails because "tmp" contains "file" + call DoMkdirDelAddFile('Xtopdir/tmp') + call assert_true(isdirectory('Xtopdir')) + call assert_true(isdirectory('Xtopdir/tmp')) + call assert_true(filereadable('Xtopdir/tmp/file')) + call delete('Xtopdir/tmp', 'rf') + + " Xtopdir/tmp is created thus deleted, not Xtopdir itself + call DoMkdirDelRec('Xtopdir/tmp') + call assert_true(isdirectory('Xtopdir')) + call assert_false(isdirectory('Xtopdir/tmp')) + + " Deletion works even though "tmp" contains "sub" + call DoMkdirDelRec('Xtopdir/tmp/sub') + call assert_true(isdirectory('Xtopdir')) + call assert_false(isdirectory('Xtopdir/tmp')) + + " Deletion works even though "tmp" contains "file" + call DoMkdirDelRecAddFile('Xtopdir/tmp') + call assert_true(isdirectory('Xtopdir')) + call assert_false(isdirectory('Xtopdir/tmp')) endfunc func Test_line_continuation() 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_user_func.vim b/test/old/testdir/test_user_func.vim index f475803ce1..4bb4078a1c 100644 --- a/test/old/testdir/test_user_func.vim +++ b/test/old/testdir/test_user_func.vim @@ -532,4 +532,106 @@ func Test_funcdef_alloc_failure() bw! endfunc +func AddDefer(arg1, ...) + call extend(g:deferred, [a:arg1]) + if a:0 == 1 + call extend(g:deferred, [a:1]) + endif +endfunc + +func WithDeferTwo() + call extend(g:deferred, ['in Two']) + for nr in range(3) + defer AddDefer('Two' .. nr) + endfor + call extend(g:deferred, ['end Two']) +endfunc + +func WithDeferOne() + call extend(g:deferred, ['in One']) + call writefile(['text'], 'Xfuncdefer') + defer delete('Xfuncdefer') + defer AddDefer('One') + call WithDeferTwo() + call extend(g:deferred, ['end One']) +endfunc + +func WithPartialDefer() + call extend(g:deferred, ['in Partial']) + let Part = funcref('AddDefer', ['arg1']) + defer Part("arg2") + call extend(g:deferred, ['end Partial']) +endfunc + +func Test_defer() + let g:deferred = [] + call WithDeferOne() + + call assert_equal(['in One', 'in Two', 'end Two', 'Two2', 'Two1', 'Two0', 'end One', 'One'], g:deferred) + unlet g:deferred + + call assert_equal('', glob('Xfuncdefer')) + + call assert_fails('defer delete("Xfuncdefer")->Another()', 'E488:') + call assert_fails('defer delete("Xfuncdefer").member', 'E488:') + + let g:deferred = [] + call WithPartialDefer() + call assert_equal(['in Partial', 'end Partial', 'arg1', 'arg2'], g:deferred) + unlet g:deferred + + let Part = funcref('AddDefer', ['arg1'], {}) + call assert_fails('defer Part("arg2")', 'E1300:') +endfunc + +func DeferLevelTwo() + call writefile(['text'], 'XDeleteTwo', 'D') + throw 'someerror' +endfunc + +" def DeferLevelOne() +func DeferLevelOne() + call writefile(['text'], 'XDeleteOne', 'D') + call g:DeferLevelTwo() +" enddef +endfunc + +func Test_defer_throw() + let caught = 'no' + try + call DeferLevelOne() + catch /someerror/ + let caught = 'yes' + endtry + call assert_equal('yes', caught) + call assert_false(filereadable('XDeleteOne')) + call assert_false(filereadable('XDeleteTwo')) +endfunc + +func Test_defer_quitall() + let lines =<< trim END + " vim9script + func DeferLevelTwo() + call writefile(['text'], 'XQuitallTwo', 'D') + qa! + endfunc + + " def DeferLevelOne() + func DeferLevelOne() + call writefile(['text'], 'XQuitallOne', 'D') + call DeferLevelTwo() + " enddef + endfunc + + " DeferLevelOne() + call DeferLevelOne() + END + call writefile(lines, 'XdeferQuitall', 'D') + let res = system(GetVimCommandClean() .. ' -X -S XdeferQuitall') + call assert_equal(0, v:shell_error) + call assert_false(filereadable('XQuitallOne')) + call assert_false(filereadable('XQuitallTwo')) +endfunc + + " vim: shiftwidth=2 sts=2 expandtab diff --git a/test/old/testdir/test_writefile.vim b/test/old/testdir/test_writefile.vim index 6019cee193..312d45e18f 100644 --- a/test/old/testdir/test_writefile.vim +++ b/test/old/testdir/test_writefile.vim @@ -924,19 +924,49 @@ 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 + +func DoWriteFile() + call writefile(['text'], 'Xthefile', 'D') + cd .. +endfunc + +func Test_write_defer_delete_chdir() + let dir = getcwd() + call DoWriteFile() + call assert_notequal(dir, getcwd()) + call chdir(dir) + call assert_equal('', glob('Xthefile')) endfunc " Check that buffer is written before triggering QuitPre diff --git a/test/unit/os/fs_spec.lua b/test/unit/os/fs_spec.lua index 95a12f5b17..3a40e97755 100644 --- a/test/unit/os/fs_spec.lua +++ b/test/unit/os/fs_spec.lua @@ -17,6 +17,7 @@ local OK = helpers.OK local FAIL = helpers.FAIL local NULL = helpers.NULL local mkdir = helpers.mkdir +local endswith = helpers.endswith local NODE_NORMAL = 0 local NODE_WRITABLE = 1 @@ -748,12 +749,17 @@ describe('fs.c', function() local function os_mkdir_recurse(path, mode) local failed_str = ffi.new('char *[1]', {nil}) - local ret = fs.os_mkdir_recurse(path, mode, failed_str) - local str = failed_str[0] - if str ~= nil then - str = ffi.string(str) + local created_str = ffi.new('char *[1]', {nil}) + local ret = fs.os_mkdir_recurse(path, mode, failed_str, created_str) + local failed_dir = failed_str[0] + if failed_dir ~= nil then + failed_dir = ffi.string(failed_dir) end - return ret, str + local created_dir = created_str[0] + if created_dir ~= nil then + created_dir = ffi.string(created_dir) + end + return ret, failed_dir, created_dir end describe('os_mkdir', function() @@ -774,33 +780,37 @@ describe('fs.c', function() describe('os_mkdir_recurse', function() itp('returns zero when given an already existing directory', function() local mode = ffi.C.kS_IRUSR + ffi.C.kS_IWUSR + ffi.C.kS_IXUSR - local ret, failed_str = os_mkdir_recurse('unit-test-directory', mode) + local ret, failed_dir, created_dir = os_mkdir_recurse('unit-test-directory', mode) eq(0, ret) - eq(nil, failed_str) + eq(nil, failed_dir) + eq(nil, created_dir) end) itp('fails to create a directory where there is a file', function() local mode = ffi.C.kS_IRUSR + ffi.C.kS_IWUSR + ffi.C.kS_IXUSR - local ret, failed_str = os_mkdir_recurse( + local ret, failed_dir, created_dir = os_mkdir_recurse( 'unit-test-directory/test.file', mode) neq(0, ret) - eq('unit-test-directory/test.file', failed_str) + eq('unit-test-directory/test.file', failed_dir) + eq(nil, created_dir) end) itp('fails to create a directory where there is a file in path', function() local mode = ffi.C.kS_IRUSR + ffi.C.kS_IWUSR + ffi.C.kS_IXUSR - local ret, failed_str = os_mkdir_recurse( + local ret, failed_dir, created_dir = os_mkdir_recurse( 'unit-test-directory/test.file/test', mode) neq(0, ret) - eq('unit-test-directory/test.file', failed_str) + eq('unit-test-directory/test.file', failed_dir) + eq(nil, created_dir) end) itp('succeeds to create a directory', function() local mode = ffi.C.kS_IRUSR + ffi.C.kS_IWUSR + ffi.C.kS_IXUSR - local ret, failed_str = os_mkdir_recurse( + local ret, failed_dir, created_dir = os_mkdir_recurse( 'unit-test-directory/new-dir-recurse', mode) eq(0, ret) - eq(nil, failed_str) + eq(nil, failed_dir) + ok(endswith(created_dir, 'unit-test-directory/new-dir-recurse')) eq(true, os_isdir('unit-test-directory/new-dir-recurse')) luv.fs_rmdir('unit-test-directory/new-dir-recurse') eq(false, os_isdir('unit-test-directory/new-dir-recurse')) @@ -808,10 +818,11 @@ describe('fs.c', function() itp('succeeds to create a directory ending with ///', function() local mode = ffi.C.kS_IRUSR + ffi.C.kS_IWUSR + ffi.C.kS_IXUSR - local ret, failed_str = os_mkdir_recurse( + local ret, failed_dir, created_dir = os_mkdir_recurse( 'unit-test-directory/new-dir-recurse///', mode) eq(0, ret) - eq(nil, failed_str) + eq(nil, failed_dir) + ok(endswith(created_dir, 'unit-test-directory/new-dir-recurse')) eq(true, os_isdir('unit-test-directory/new-dir-recurse')) luv.fs_rmdir('unit-test-directory/new-dir-recurse') eq(false, os_isdir('unit-test-directory/new-dir-recurse')) @@ -819,10 +830,11 @@ describe('fs.c', function() itp('succeeds to create a directory ending with /', function() local mode = ffi.C.kS_IRUSR + ffi.C.kS_IWUSR + ffi.C.kS_IXUSR - local ret, failed_str = os_mkdir_recurse( + local ret, failed_dir, created_dir = os_mkdir_recurse( 'unit-test-directory/new-dir-recurse/', mode) eq(0, ret) - eq(nil, failed_str) + eq(nil, failed_dir) + ok(endswith(created_dir, 'unit-test-directory/new-dir-recurse')) eq(true, os_isdir('unit-test-directory/new-dir-recurse')) luv.fs_rmdir('unit-test-directory/new-dir-recurse') eq(false, os_isdir('unit-test-directory/new-dir-recurse')) @@ -830,10 +842,11 @@ describe('fs.c', function() itp('succeeds to create a directory tree', function() local mode = ffi.C.kS_IRUSR + ffi.C.kS_IWUSR + ffi.C.kS_IXUSR - local ret, failed_str = os_mkdir_recurse( + local ret, failed_dir, created_dir = os_mkdir_recurse( 'unit-test-directory/new-dir-recurse/1/2/3', mode) eq(0, ret) - eq(nil, failed_str) + eq(nil, failed_dir) + ok(endswith(created_dir, 'unit-test-directory/new-dir-recurse')) eq(true, os_isdir('unit-test-directory/new-dir-recurse')) eq(true, os_isdir('unit-test-directory/new-dir-recurse/1')) eq(true, os_isdir('unit-test-directory/new-dir-recurse/1/2')) |