diff options
author | Victor Blanchard <48864055+Viblanc@users.noreply.github.com> | 2022-11-07 04:31:50 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-11-06 19:31:50 -0800 |
commit | d337814906b1377e34aa2c2dfd8aa16285328692 (patch) | |
tree | de1ae115fe6b4266dabef3aa59e2d62d4fede6b7 | |
parent | 10fbda508cc9fad931e55000d4434e71701ddeab (diff) | |
download | rneovim-d337814906b1377e34aa2c2dfd8aa16285328692.tar.gz rneovim-d337814906b1377e34aa2c2dfd8aa16285328692.tar.bz2 rneovim-d337814906b1377e34aa2c2dfd8aa16285328692.zip |
feat: ":write ++p" creates parent dirs #20835
- `:write ++p foo/bar/baz.txt` should create parent directories `foo/bar/` if
they do not exist
- Note: `:foo ++…` is usually for options. No existing options have
a single-char abbreviation (presumably by design), so it's safe to
special-case `++p` here.
- Same for `writefile(…, 'foo/bar/baz.txt', 'p')`
- `BufWriteCmd` can see the ++p flag via `v:cmdarg`.
closes #19884
-rw-r--r-- | runtime/doc/editing.txt | 2 | ||||
-rw-r--r-- | src/nvim/eval.c | 8 | ||||
-rw-r--r-- | src/nvim/eval/funcs.c | 4 | ||||
-rw-r--r-- | src/nvim/ex_cmds.c | 7 | ||||
-rw-r--r-- | src/nvim/ex_cmds_defs.h | 1 | ||||
-rw-r--r-- | src/nvim/ex_docmd.c | 7 | ||||
-rw-r--r-- | src/nvim/os/fileio.c | 8 | ||||
-rw-r--r-- | src/nvim/os/fileio.h | 1 | ||||
-rw-r--r-- | src/nvim/os/fs.c | 31 | ||||
-rw-r--r-- | test/functional/ex_cmds/write_spec.lua | 27 | ||||
-rw-r--r-- | test/functional/vimscript/writefile_spec.lua | 20 |
11 files changed, 116 insertions, 0 deletions
diff --git a/runtime/doc/editing.txt b/runtime/doc/editing.txt index 58a5cbc60c..76c528ef3c 100644 --- a/runtime/doc/editing.txt +++ b/runtime/doc/editing.txt @@ -438,6 +438,8 @@ Where {optname} is one of: *++ff* *++enc* *++bin* *++nobin* *++edit* bad specifies behavior for bad characters edit for |:read| only: keep option values as if editing a file + p creates the parent directory (or directories) of + a filename if they do not exist {value} cannot contain white space. It can be any valid value for these options. Examples: > diff --git a/src/nvim/eval.c b/src/nvim/eval.c index c578d9fd39..0848326d90 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -7067,6 +7067,9 @@ char *set_cmdarg(exarg_T *eap, char *oldarg) if (eap->bad_char != 0) { len += 7 + 4; // " ++bad=" + "keep" or "drop" } + if (eap->mkdir_p != 0) { + len += 4; + } const size_t newval_len = len + 1; char *newval = xmalloc(newval_len); @@ -7100,6 +7103,11 @@ char *set_cmdarg(exarg_T *eap, char *oldarg) snprintf(newval + strlen(newval), newval_len, " ++bad=%c", eap->bad_char); } + + if (eap->mkdir_p) { + snprintf(newval, newval_len, " ++p"); + } + vimvars[VV_CMDARG].vv_str = newval; return oldval; } diff --git a/src/nvim/eval/funcs.c b/src/nvim/eval/funcs.c index 1492a2d30d..5fcbc40623 100644 --- a/src/nvim/eval/funcs.c +++ b/src/nvim/eval/funcs.c @@ -9970,6 +9970,7 @@ static void f_writefile(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) bool binary = false; bool append = false; bool do_fsync = !!p_fs; + bool mkdir_p = false; if (argvars[2].v_type != VAR_UNKNOWN) { const char *const flags = tv_get_string_chk(&argvars[2]); if (flags == NULL) { @@ -9985,6 +9986,8 @@ static void f_writefile(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) do_fsync = true; break; case 'S': do_fsync = false; break; + case 'p': + mkdir_p = true; break; default: // Using %s, p and not %c, *p to preserve multibyte characters semsg(_("E5060: Unknown flag: %s"), p); @@ -10004,6 +10007,7 @@ static void f_writefile(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) emsg(_("E482: Can't open file with an empty name")); } else if ((error = file_open(&fp, fname, ((append ? kFileAppend : kFileTruncate) + | (mkdir_p ? kFileMkDir : kFileCreate) | kFileCreate), 0666)) != 0) { semsg(_("E482: Can't open file %s for writing: %s"), fname, os_strerror(error)); diff --git a/src/nvim/ex_cmds.c b/src/nvim/ex_cmds.c index d3a4b6c282..925ff3b8ea 100644 --- a/src/nvim/ex_cmds.c +++ b/src/nvim/ex_cmds.c @@ -1922,6 +1922,13 @@ int do_write(exarg_T *eap) fname = curbuf->b_sfname; } + if (eap->mkdir_p) { + if (os_file_mkdir(fname, 0755) < 0) { + retval = FAIL; + goto theend; + } + } + name_was_missing = curbuf->b_ffname == NULL; retval = buf_write(curbuf, ffname, fname, eap->line1, eap->line2, eap, eap->append, eap->forceit, true, false); diff --git a/src/nvim/ex_cmds_defs.h b/src/nvim/ex_cmds_defs.h index 0015a82880..378271c107 100644 --- a/src/nvim/ex_cmds_defs.h +++ b/src/nvim/ex_cmds_defs.h @@ -202,6 +202,7 @@ struct exarg { int regname; ///< register name (NUL if none) int force_bin; ///< 0, FORCE_BIN or FORCE_NOBIN int read_edit; ///< ++edit argument + int mkdir_p; ///< ++p argument int force_ff; ///< ++ff= argument (first char of argument) int force_enc; ///< ++enc= argument (index in cmd[]) int bad_char; ///< BAD_KEEP, BAD_DROP or replacement byte diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index dc2b7247f1..5bb7cb2da2 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -4074,6 +4074,13 @@ static int getargopt(exarg_T *eap) return OK; } + // ":write ++p foo/bar/file + if (strncmp(arg, "p", 1) == 0) { + eap->mkdir_p = true; + eap->arg = skipwhite(arg + 1); + return OK; + } + if (STRNCMP(arg, "ff", 2) == 0) { arg += 2; pp = &eap->force_ff; diff --git a/src/nvim/os/fileio.c b/src/nvim/os/fileio.c index b1710737d0..280a9c2bee 100644 --- a/src/nvim/os/fileio.c +++ b/src/nvim/os/fileio.c @@ -71,6 +71,7 @@ int file_open(FileDescriptor *const ret_fp, const char *const fname, const int f FLAG(flags, kFileReadOnly, O_RDONLY, kFalse, wr != kTrue); #ifdef O_NOFOLLOW FLAG(flags, kFileNoSymlink, O_NOFOLLOW, kNone, true); + FLAG(flags, kFileMkDir, O_CREAT|O_WRONLY, kTrue, !(flags & kFileCreateOnly)); #endif #undef FLAG // wr is used for kFileReadOnly flag, but on @@ -78,6 +79,13 @@ int file_open(FileDescriptor *const ret_fp, const char *const fname, const int f // `error: variable ‘wr’ set but not used [-Werror=unused-but-set-variable]` (void)wr; + if (flags & kFileMkDir) { + int mkdir_ret = os_file_mkdir((char *)fname, 0755); + if (mkdir_ret < 0) { + return mkdir_ret; + } + } + const int fd = os_open(fname, os_open_flags, mode); if (fd < 0) { diff --git a/src/nvim/os/fileio.h b/src/nvim/os/fileio.h index da23a54c4e..5e47bbf921 100644 --- a/src/nvim/os/fileio.h +++ b/src/nvim/os/fileio.h @@ -35,6 +35,7 @@ typedef enum { ///< be used with kFileCreateOnly. kFileNonBlocking = 128, ///< Do not restart read() or write() syscall if ///< EAGAIN was encountered. + kFileMkDir = 256, } FileOpenFlags; static inline bool file_eof(const FileDescriptor *fp) diff --git a/src/nvim/os/fs.c b/src/nvim/os/fs.c index 68e96eea6e..3c9578979e 100644 --- a/src/nvim/os/fs.c +++ b/src/nvim/os/fs.c @@ -946,6 +946,37 @@ int os_mkdir_recurse(const char *const dir, int32_t mode, char **const failed_di return 0; } +/// Create the parent directory of a file if it does not exist +/// +/// @param[in] fname Full path of the file name whose parent directories +/// we want to create +/// @param[in] mode Permissions for the newly-created directory. +/// +/// @return `0` for success, libuv error code for failure. +int os_file_mkdir(char *fname, int32_t mode) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + if (!dir_of_file_exists((char_u *)fname)) { + char *tail = path_tail_with_sep(fname); + char *last_char = tail + strlen(tail) - 1; + if (vim_ispathsep(*last_char)) { + emsg(_(e_noname)); + return -1; + } + char c = *tail; + *tail = NUL; + int r; + char *failed_dir; + if ((r = os_mkdir_recurse(fname, mode, &failed_dir) < 0)) { + semsg(_(e_mkdir), failed_dir, os_strerror(r)); + xfree(failed_dir); + } + *tail = c; + return r; + } + return 0; +} + /// Create a unique temporary directory. /// /// @param[in] template Template of the path to the directory with XXXXXX diff --git a/test/functional/ex_cmds/write_spec.lua b/test/functional/ex_cmds/write_spec.lua index 4045d13153..d6b5c54de9 100644 --- a/test/functional/ex_cmds/write_spec.lua +++ b/test/functional/ex_cmds/write_spec.lua @@ -20,6 +20,9 @@ describe(':write', function() os.remove('test_bkc_file.txt') os.remove('test_bkc_link.txt') os.remove('test_fifo') + os.remove('test/write/p_opt.txt') + os.remove('test/write') + os.remove('test') os.remove(fname) os.remove(fname_bak) os.remove(fname_broken) @@ -94,6 +97,30 @@ describe(':write', function() fifo:close() end) + it("++p creates missing parent directories", function() + eq(0, eval("filereadable('p_opt.txt')")) + command("write ++p p_opt.txt") + eq(1, eval("filereadable('p_opt.txt')")) + os.remove("p_opt.txt") + + eq(0, eval("filereadable('p_opt.txt')")) + command("write ++p ./p_opt.txt") + eq(1, eval("filereadable('p_opt.txt')")) + os.remove("p_opt.txt") + + eq(0, eval("filereadable('test/write/p_opt.txt')")) + command("write ++p test/write/p_opt.txt") + eq(1, eval("filereadable('test/write/p_opt.txt')")) + + eq(('Vim(write):E32: No file name'), pcall_err(command, 'write ++p test_write/')) + if not iswin() then + eq(('Vim(write):E17: "'..funcs.fnamemodify('.', ':p:h')..'" is a directory'), + pcall_err(command, 'write ++p .')) + eq(('Vim(write):E17: "'..funcs.fnamemodify('.', ':p:h')..'" is a directory'), + pcall_err(command, 'write ++p ./')) + end + end) + it('errors out correctly', function() if isCI('cirrus') then pending('FIXME: cirrus') diff --git a/test/functional/vimscript/writefile_spec.lua b/test/functional/vimscript/writefile_spec.lua index 5f693249a9..8c8da9dc88 100644 --- a/test/functional/vimscript/writefile_spec.lua +++ b/test/functional/vimscript/writefile_spec.lua @@ -111,6 +111,26 @@ describe('writefile()', function() pcall_err(command, ('call writefile([42], %s)'):format(ddname_tail))) end) + it('writefile(..., "p") creates missing parent directories', function() + os.remove(dname) + eq(nil, read_file(dfname)) + eq(0, funcs.writefile({'abc', 'def', 'ghi'}, dfname, 'p')) + eq('abc\ndef\nghi\n', read_file(dfname)) + os.remove(dfname) + os.remove(dname) + eq(nil, read_file(dfname)) + eq(0, funcs.writefile({'\na\nb\n'}, dfname, 'pb')) + eq('\0a\0b\0', read_file(dfname)) + os.remove(dfname) + os.remove(dname) + eq('Vim(call):E32: No file name', + pcall_err(command, ('call writefile([], "%s", "p")'):format(dfname .. '.d/'))) + eq(('Vim(call):E482: Can\'t open file ./ for writing: illegal operation on a directory'), + pcall_err(command, 'call writefile([], "./", "p")')) + eq(('Vim(call):E482: Can\'t open file . for writing: illegal operation on a directory'), + pcall_err(command, 'call writefile([], ".", "p")')) + end) + it('errors out with invalid arguments', function() write_file(fname, 'TEST') eq('Vim(call):E119: Not enough arguments for function: writefile', |