aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVictor Blanchard <48864055+Viblanc@users.noreply.github.com>2022-11-07 04:31:50 +0100
committerGitHub <noreply@github.com>2022-11-06 19:31:50 -0800
commitd337814906b1377e34aa2c2dfd8aa16285328692 (patch)
treede1ae115fe6b4266dabef3aa59e2d62d4fede6b7
parent10fbda508cc9fad931e55000d4434e71701ddeab (diff)
downloadrneovim-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.txt2
-rw-r--r--src/nvim/eval.c8
-rw-r--r--src/nvim/eval/funcs.c4
-rw-r--r--src/nvim/ex_cmds.c7
-rw-r--r--src/nvim/ex_cmds_defs.h1
-rw-r--r--src/nvim/ex_docmd.c7
-rw-r--r--src/nvim/os/fileio.c8
-rw-r--r--src/nvim/os/fileio.h1
-rw-r--r--src/nvim/os/fs.c31
-rw-r--r--test/functional/ex_cmds/write_spec.lua27
-rw-r--r--test/functional/vimscript/writefile_spec.lua20
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',