mirror of
https://github.com/neovim/neovim.git
synced 2025-02-25 18:55:25 -06:00
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
This commit is contained in:
parent
10fbda508c
commit
d337814906
@ -438,6 +438,8 @@ Where {optname} is one of: *++ff* *++enc* *++bin* *++nobin* *++edit*
|
|||||||
bad specifies behavior for bad characters
|
bad specifies behavior for bad characters
|
||||||
edit for |:read| only: keep option values as if editing
|
edit for |:read| only: keep option values as if editing
|
||||||
a file
|
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
|
{value} cannot contain white space. It can be any valid value for these
|
||||||
options. Examples: >
|
options. Examples: >
|
||||||
|
@ -7067,6 +7067,9 @@ char *set_cmdarg(exarg_T *eap, char *oldarg)
|
|||||||
if (eap->bad_char != 0) {
|
if (eap->bad_char != 0) {
|
||||||
len += 7 + 4; // " ++bad=" + "keep" or "drop"
|
len += 7 + 4; // " ++bad=" + "keep" or "drop"
|
||||||
}
|
}
|
||||||
|
if (eap->mkdir_p != 0) {
|
||||||
|
len += 4;
|
||||||
|
}
|
||||||
|
|
||||||
const size_t newval_len = len + 1;
|
const size_t newval_len = len + 1;
|
||||||
char *newval = xmalloc(newval_len);
|
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",
|
snprintf(newval + strlen(newval), newval_len, " ++bad=%c",
|
||||||
eap->bad_char);
|
eap->bad_char);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (eap->mkdir_p) {
|
||||||
|
snprintf(newval, newval_len, " ++p");
|
||||||
|
}
|
||||||
|
|
||||||
vimvars[VV_CMDARG].vv_str = newval;
|
vimvars[VV_CMDARG].vv_str = newval;
|
||||||
return oldval;
|
return oldval;
|
||||||
}
|
}
|
||||||
|
@ -9970,6 +9970,7 @@ static void f_writefile(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
|||||||
bool binary = false;
|
bool binary = false;
|
||||||
bool append = false;
|
bool append = false;
|
||||||
bool do_fsync = !!p_fs;
|
bool do_fsync = !!p_fs;
|
||||||
|
bool mkdir_p = false;
|
||||||
if (argvars[2].v_type != VAR_UNKNOWN) {
|
if (argvars[2].v_type != VAR_UNKNOWN) {
|
||||||
const char *const flags = tv_get_string_chk(&argvars[2]);
|
const char *const flags = tv_get_string_chk(&argvars[2]);
|
||||||
if (flags == NULL) {
|
if (flags == NULL) {
|
||||||
@ -9985,6 +9986,8 @@ static void f_writefile(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
|||||||
do_fsync = true; break;
|
do_fsync = true; break;
|
||||||
case 'S':
|
case 'S':
|
||||||
do_fsync = false; break;
|
do_fsync = false; break;
|
||||||
|
case 'p':
|
||||||
|
mkdir_p = true; break;
|
||||||
default:
|
default:
|
||||||
// Using %s, p and not %c, *p to preserve multibyte characters
|
// Using %s, p and not %c, *p to preserve multibyte characters
|
||||||
semsg(_("E5060: Unknown flag: %s"), p);
|
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"));
|
emsg(_("E482: Can't open file with an empty name"));
|
||||||
} else if ((error = file_open(&fp, fname,
|
} else if ((error = file_open(&fp, fname,
|
||||||
((append ? kFileAppend : kFileTruncate)
|
((append ? kFileAppend : kFileTruncate)
|
||||||
|
| (mkdir_p ? kFileMkDir : kFileCreate)
|
||||||
| kFileCreate), 0666)) != 0) {
|
| kFileCreate), 0666)) != 0) {
|
||||||
semsg(_("E482: Can't open file %s for writing: %s"),
|
semsg(_("E482: Can't open file %s for writing: %s"),
|
||||||
fname, os_strerror(error));
|
fname, os_strerror(error));
|
||||||
|
@ -1922,6 +1922,13 @@ int do_write(exarg_T *eap)
|
|||||||
fname = curbuf->b_sfname;
|
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;
|
name_was_missing = curbuf->b_ffname == NULL;
|
||||||
retval = buf_write(curbuf, ffname, fname, eap->line1, eap->line2,
|
retval = buf_write(curbuf, ffname, fname, eap->line1, eap->line2,
|
||||||
eap, eap->append, eap->forceit, true, false);
|
eap, eap->append, eap->forceit, true, false);
|
||||||
|
@ -202,6 +202,7 @@ struct exarg {
|
|||||||
int regname; ///< register name (NUL if none)
|
int regname; ///< register name (NUL if none)
|
||||||
int force_bin; ///< 0, FORCE_BIN or FORCE_NOBIN
|
int force_bin; ///< 0, FORCE_BIN or FORCE_NOBIN
|
||||||
int read_edit; ///< ++edit argument
|
int read_edit; ///< ++edit argument
|
||||||
|
int mkdir_p; ///< ++p argument
|
||||||
int force_ff; ///< ++ff= argument (first char of argument)
|
int force_ff; ///< ++ff= argument (first char of argument)
|
||||||
int force_enc; ///< ++enc= argument (index in cmd[])
|
int force_enc; ///< ++enc= argument (index in cmd[])
|
||||||
int bad_char; ///< BAD_KEEP, BAD_DROP or replacement byte
|
int bad_char; ///< BAD_KEEP, BAD_DROP or replacement byte
|
||||||
|
@ -4074,6 +4074,13 @@ static int getargopt(exarg_T *eap)
|
|||||||
return OK;
|
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) {
|
if (STRNCMP(arg, "ff", 2) == 0) {
|
||||||
arg += 2;
|
arg += 2;
|
||||||
pp = &eap->force_ff;
|
pp = &eap->force_ff;
|
||||||
|
@ -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);
|
FLAG(flags, kFileReadOnly, O_RDONLY, kFalse, wr != kTrue);
|
||||||
#ifdef O_NOFOLLOW
|
#ifdef O_NOFOLLOW
|
||||||
FLAG(flags, kFileNoSymlink, O_NOFOLLOW, kNone, true);
|
FLAG(flags, kFileNoSymlink, O_NOFOLLOW, kNone, true);
|
||||||
|
FLAG(flags, kFileMkDir, O_CREAT|O_WRONLY, kTrue, !(flags & kFileCreateOnly));
|
||||||
#endif
|
#endif
|
||||||
#undef FLAG
|
#undef FLAG
|
||||||
// wr is used for kFileReadOnly flag, but on
|
// 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]`
|
// `error: variable ‘wr’ set but not used [-Werror=unused-but-set-variable]`
|
||||||
(void)wr;
|
(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);
|
const int fd = os_open(fname, os_open_flags, mode);
|
||||||
|
|
||||||
if (fd < 0) {
|
if (fd < 0) {
|
||||||
|
@ -35,6 +35,7 @@ typedef enum {
|
|||||||
///< be used with kFileCreateOnly.
|
///< be used with kFileCreateOnly.
|
||||||
kFileNonBlocking = 128, ///< Do not restart read() or write() syscall if
|
kFileNonBlocking = 128, ///< Do not restart read() or write() syscall if
|
||||||
///< EAGAIN was encountered.
|
///< EAGAIN was encountered.
|
||||||
|
kFileMkDir = 256,
|
||||||
} FileOpenFlags;
|
} FileOpenFlags;
|
||||||
|
|
||||||
static inline bool file_eof(const FileDescriptor *fp)
|
static inline bool file_eof(const FileDescriptor *fp)
|
||||||
|
@ -946,6 +946,37 @@ int os_mkdir_recurse(const char *const dir, int32_t mode, char **const failed_di
|
|||||||
return 0;
|
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.
|
/// Create a unique temporary directory.
|
||||||
///
|
///
|
||||||
/// @param[in] template Template of the path to the directory with XXXXXX
|
/// @param[in] template Template of the path to the directory with XXXXXX
|
||||||
|
@ -20,6 +20,9 @@ describe(':write', function()
|
|||||||
os.remove('test_bkc_file.txt')
|
os.remove('test_bkc_file.txt')
|
||||||
os.remove('test_bkc_link.txt')
|
os.remove('test_bkc_link.txt')
|
||||||
os.remove('test_fifo')
|
os.remove('test_fifo')
|
||||||
|
os.remove('test/write/p_opt.txt')
|
||||||
|
os.remove('test/write')
|
||||||
|
os.remove('test')
|
||||||
os.remove(fname)
|
os.remove(fname)
|
||||||
os.remove(fname_bak)
|
os.remove(fname_bak)
|
||||||
os.remove(fname_broken)
|
os.remove(fname_broken)
|
||||||
@ -94,6 +97,30 @@ describe(':write', function()
|
|||||||
fifo:close()
|
fifo:close()
|
||||||
end)
|
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()
|
it('errors out correctly', function()
|
||||||
if isCI('cirrus') then
|
if isCI('cirrus') then
|
||||||
pending('FIXME: cirrus')
|
pending('FIXME: cirrus')
|
||||||
|
@ -111,6 +111,26 @@ describe('writefile()', function()
|
|||||||
pcall_err(command, ('call writefile([42], %s)'):format(ddname_tail)))
|
pcall_err(command, ('call writefile([42], %s)'):format(ddname_tail)))
|
||||||
end)
|
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()
|
it('errors out with invalid arguments', function()
|
||||||
write_file(fname, 'TEST')
|
write_file(fname, 'TEST')
|
||||||
eq('Vim(call):E119: Not enough arguments for function: writefile',
|
eq('Vim(call):E119: Not enough arguments for function: writefile',
|
||||||
|
Loading…
Reference in New Issue
Block a user