mirror of
https://github.com/neovim/neovim.git
synced 2025-10-17 15:21:47 +00:00

These are not needed after #35129 but making uncrustify still play nice with them was a bit tricky. Unfortunately `uncrustify --update-config-with-doc` breaks strings with backslashes. This issue has been reported upstream, and in the meanwhile auto-update on every single run has been disabled.
1503 lines
42 KiB
C
1503 lines
42 KiB
C
// eval/fs.c: Filesystem related builtin functions
|
|
|
|
#include <assert.h>
|
|
#include <limits.h>
|
|
#include <stdbool.h>
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
|
|
#include "auto/config.h"
|
|
#include "nvim/ascii_defs.h"
|
|
#include "nvim/buffer_defs.h"
|
|
#include "nvim/cmdexpand.h"
|
|
#include "nvim/cmdexpand_defs.h"
|
|
#include "nvim/errors.h"
|
|
#include "nvim/eval.h"
|
|
#include "nvim/eval/fs.h"
|
|
#include "nvim/eval/typval.h"
|
|
#include "nvim/eval/userfunc.h"
|
|
#include "nvim/eval/window.h"
|
|
#include "nvim/ex_cmds.h"
|
|
#include "nvim/ex_docmd.h"
|
|
#include "nvim/file_search.h"
|
|
#include "nvim/fileio.h"
|
|
#include "nvim/garray.h"
|
|
#include "nvim/garray_defs.h"
|
|
#include "nvim/gettext_defs.h"
|
|
#include "nvim/globals.h"
|
|
#include "nvim/macros_defs.h"
|
|
#include "nvim/memory.h"
|
|
#include "nvim/message.h"
|
|
#include "nvim/option_vars.h"
|
|
#include "nvim/os/fileio.h"
|
|
#include "nvim/os/fileio_defs.h"
|
|
#include "nvim/os/fs.h"
|
|
#include "nvim/os/fs_defs.h"
|
|
#include "nvim/os/os_defs.h"
|
|
#include "nvim/path.h"
|
|
#include "nvim/pos_defs.h"
|
|
#include "nvim/strings.h"
|
|
#include "nvim/types_defs.h"
|
|
#include "nvim/vim_defs.h"
|
|
#include "nvim/window.h"
|
|
|
|
#include "eval/fs.c.generated.h"
|
|
|
|
static const char e_error_while_writing_str[] = N_("E80: Error while writing: %s");
|
|
|
|
/// "chdir(dir)" function
|
|
void f_chdir(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
|
{
|
|
rettv->v_type = VAR_STRING;
|
|
rettv->vval.v_string = NULL;
|
|
|
|
if (argvars[0].v_type != VAR_STRING) {
|
|
// Returning an empty string means it failed.
|
|
// No error message, for historic reasons.
|
|
return;
|
|
}
|
|
|
|
// Return the current directory
|
|
char *cwd = xmalloc(MAXPATHL);
|
|
if (os_dirname(cwd, MAXPATHL) != FAIL) {
|
|
#ifdef BACKSLASH_IN_FILENAME
|
|
slash_adjust(cwd);
|
|
#endif
|
|
rettv->vval.v_string = xstrdup(cwd);
|
|
}
|
|
xfree(cwd);
|
|
|
|
CdScope scope = kCdScopeGlobal;
|
|
if (argvars[1].v_type != VAR_UNKNOWN) {
|
|
const char *s = tv_get_string(&argvars[1]);
|
|
if (strcmp(s, "global") == 0) {
|
|
scope = kCdScopeGlobal;
|
|
} else if (strcmp(s, "tabpage") == 0) {
|
|
scope = kCdScopeTabpage;
|
|
} else if (strcmp(s, "window") == 0) {
|
|
scope = kCdScopeWindow;
|
|
} else {
|
|
semsg(_(e_invargNval), "scope", s);
|
|
return;
|
|
}
|
|
} else if (curwin->w_localdir != NULL) {
|
|
scope = kCdScopeWindow;
|
|
} else if (curtab->tp_localdir != NULL) {
|
|
scope = kCdScopeTabpage;
|
|
}
|
|
|
|
if (!changedir_func(argvars[0].vval.v_string, scope)) {
|
|
// Directory change failed
|
|
XFREE_CLEAR(rettv->vval.v_string);
|
|
}
|
|
}
|
|
|
|
/// "delete()" function
|
|
void f_delete(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
|
{
|
|
rettv->vval.v_number = -1;
|
|
if (check_secure()) {
|
|
return;
|
|
}
|
|
|
|
const char *const name = tv_get_string(&argvars[0]);
|
|
if (*name == NUL) {
|
|
emsg(_(e_invarg));
|
|
return;
|
|
}
|
|
|
|
char nbuf[NUMBUFLEN];
|
|
const char *flags;
|
|
if (argvars[1].v_type != VAR_UNKNOWN) {
|
|
flags = tv_get_string_buf(&argvars[1], nbuf);
|
|
} else {
|
|
flags = "";
|
|
}
|
|
|
|
if (*flags == NUL) {
|
|
// delete a file
|
|
rettv->vval.v_number = os_remove(name) == 0 ? 0 : -1;
|
|
} else if (strcmp(flags, "d") == 0) {
|
|
// delete an empty directory
|
|
rettv->vval.v_number = os_rmdir(name) == 0 ? 0 : -1;
|
|
} else if (strcmp(flags, "rf") == 0) {
|
|
// delete a directory recursively
|
|
rettv->vval.v_number = delete_recursive(name);
|
|
} else {
|
|
semsg(_(e_invexpr2), flags);
|
|
}
|
|
}
|
|
|
|
/// "executable()" function
|
|
void f_executable(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
|
{
|
|
if (tv_check_for_string_arg(argvars, 0) == FAIL) {
|
|
return;
|
|
}
|
|
|
|
// Check in $PATH and also check directly if there is a directory name
|
|
rettv->vval.v_number = os_can_exe(tv_get_string(&argvars[0]), NULL, true);
|
|
}
|
|
|
|
/// "exepath()" function
|
|
void f_exepath(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
|
{
|
|
if (tv_check_for_nonempty_string_arg(argvars, 0) == FAIL) {
|
|
return;
|
|
}
|
|
|
|
char *path = NULL;
|
|
|
|
os_can_exe(tv_get_string(&argvars[0]), &path, true);
|
|
|
|
#ifdef BACKSLASH_IN_FILENAME
|
|
if (path != NULL) {
|
|
slash_adjust(path);
|
|
}
|
|
#endif
|
|
|
|
rettv->v_type = VAR_STRING;
|
|
rettv->vval.v_string = path;
|
|
}
|
|
|
|
/// "filecopy()" function
|
|
void f_filecopy(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
|
{
|
|
rettv->vval.v_number = false;
|
|
|
|
if (check_secure()
|
|
|| tv_check_for_string_arg(argvars, 0) == FAIL
|
|
|| tv_check_for_string_arg(argvars, 1) == FAIL) {
|
|
return;
|
|
}
|
|
|
|
const char *from = tv_get_string(&argvars[0]);
|
|
|
|
FileInfo from_info;
|
|
if (os_fileinfo_link(from, &from_info)
|
|
&& (S_ISREG(from_info.stat.st_mode) || S_ISLNK(from_info.stat.st_mode))) {
|
|
rettv->vval.v_number
|
|
= vim_copyfile(tv_get_string(&argvars[0]), tv_get_string(&argvars[1])) == OK;
|
|
}
|
|
}
|
|
|
|
/// "filereadable()" function
|
|
void f_filereadable(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
|
{
|
|
const char *const p = tv_get_string(&argvars[0]);
|
|
rettv->vval.v_number = (*p && !os_isdir(p) && os_file_is_readable(p));
|
|
}
|
|
|
|
/// @return 0 for not writable
|
|
/// 1 for writable file
|
|
/// 2 for a dir which we have rights to write into.
|
|
void f_filewritable(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
|
{
|
|
const char *filename = tv_get_string(&argvars[0]);
|
|
rettv->vval.v_number = os_file_is_writable(filename);
|
|
}
|
|
|
|
static void findfilendir(typval_T *argvars, typval_T *rettv, int find_what)
|
|
{
|
|
char *fresult = NULL;
|
|
char *path = *curbuf->b_p_path == NUL ? p_path : curbuf->b_p_path;
|
|
int count = 1;
|
|
bool first = true;
|
|
bool error = false;
|
|
|
|
rettv->vval.v_string = NULL;
|
|
rettv->v_type = VAR_STRING;
|
|
|
|
const char *fname = tv_get_string(&argvars[0]);
|
|
|
|
char pathbuf[NUMBUFLEN];
|
|
if (argvars[1].v_type != VAR_UNKNOWN) {
|
|
const char *p = tv_get_string_buf_chk(&argvars[1], pathbuf);
|
|
if (p == NULL) {
|
|
error = true;
|
|
} else {
|
|
if (*p != NUL) {
|
|
path = (char *)p;
|
|
}
|
|
|
|
if (argvars[2].v_type != VAR_UNKNOWN) {
|
|
count = (int)tv_get_number_chk(&argvars[2], &error);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (count < 0) {
|
|
tv_list_alloc_ret(rettv, kListLenUnknown);
|
|
}
|
|
|
|
if (*fname != NUL && !error) {
|
|
char *file_to_find = NULL;
|
|
char *search_ctx = NULL;
|
|
|
|
do {
|
|
if (rettv->v_type == VAR_STRING || rettv->v_type == VAR_LIST) {
|
|
xfree(fresult);
|
|
}
|
|
fresult = find_file_in_path_option(first ? (char *)fname : NULL,
|
|
first ? strlen(fname) : 0,
|
|
0, first, path,
|
|
find_what, curbuf->b_ffname,
|
|
(find_what == FINDFILE_DIR
|
|
? ""
|
|
: curbuf->b_p_sua),
|
|
&file_to_find, &search_ctx);
|
|
first = false;
|
|
|
|
if (fresult != NULL && rettv->v_type == VAR_LIST) {
|
|
tv_list_append_string(rettv->vval.v_list, fresult, -1);
|
|
}
|
|
} while ((rettv->v_type == VAR_LIST || --count > 0) && fresult != NULL);
|
|
|
|
xfree(file_to_find);
|
|
vim_findfile_cleanup(search_ctx);
|
|
}
|
|
|
|
if (rettv->v_type == VAR_STRING) {
|
|
rettv->vval.v_string = fresult;
|
|
}
|
|
}
|
|
|
|
/// "finddir({fname}[, {path}[, {count}]])" function
|
|
void f_finddir(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
|
{
|
|
findfilendir(argvars, rettv, FINDFILE_DIR);
|
|
}
|
|
|
|
/// "findfile({fname}[, {path}[, {count}]])" function
|
|
void f_findfile(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
|
{
|
|
findfilendir(argvars, rettv, FINDFILE_FILE);
|
|
}
|
|
|
|
/// `getcwd([{win}[, {tab}]])` function
|
|
///
|
|
/// Every scope not specified implies the currently selected scope object.
|
|
///
|
|
/// @pre The arguments must be of type number.
|
|
/// @pre There may not be more than two arguments.
|
|
/// @pre An argument may not be -1 if preceding arguments are not all -1.
|
|
///
|
|
/// @post The return value will be a string.
|
|
void f_getcwd(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
|
{
|
|
// Possible scope of working directory to return.
|
|
CdScope scope = kCdScopeInvalid;
|
|
|
|
// Numbers of the scope objects (window, tab) we want the working directory
|
|
// of. A `-1` means to skip this scope, a `0` means the current object.
|
|
int scope_number[] = {
|
|
[kCdScopeWindow] = 0, // Number of window to look at.
|
|
[kCdScopeTabpage] = 0, // Number of tab to look at.
|
|
};
|
|
|
|
char *cwd = NULL; // Current working directory to print
|
|
char *from = NULL; // The original string to copy
|
|
|
|
tabpage_T *tp = curtab; // The tabpage to look at.
|
|
win_T *win = curwin; // The window to look at.
|
|
|
|
rettv->v_type = VAR_STRING;
|
|
rettv->vval.v_string = NULL;
|
|
|
|
// Pre-conditions and scope extraction together
|
|
for (int i = MIN_CD_SCOPE; i < MAX_CD_SCOPE; i++) {
|
|
// If there is no argument there are no more scopes after it, break out.
|
|
if (argvars[i].v_type == VAR_UNKNOWN) {
|
|
break;
|
|
}
|
|
if (argvars[i].v_type != VAR_NUMBER) {
|
|
emsg(_(e_invarg));
|
|
return;
|
|
}
|
|
scope_number[i] = (int)argvars[i].vval.v_number;
|
|
// It is an error for the scope number to be less than `-1`.
|
|
if (scope_number[i] < -1) {
|
|
emsg(_(e_invarg));
|
|
return;
|
|
}
|
|
// Use the narrowest scope the user requested
|
|
if (scope_number[i] >= 0 && scope == kCdScopeInvalid) {
|
|
// The scope is the current iteration step.
|
|
scope = i;
|
|
} else if (scope_number[i] < 0) {
|
|
scope = i + 1;
|
|
}
|
|
}
|
|
|
|
// Find the tabpage by number
|
|
if (scope_number[kCdScopeTabpage] > 0) {
|
|
tp = find_tabpage(scope_number[kCdScopeTabpage]);
|
|
if (!tp) {
|
|
emsg(_("E5000: Cannot find tab number."));
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Find the window in `tp` by number, `NULL` if none.
|
|
if (scope_number[kCdScopeWindow] >= 0) {
|
|
if (scope_number[kCdScopeTabpage] < 0) {
|
|
emsg(_("E5001: Higher scope cannot be -1 if lower scope is >= 0."));
|
|
return;
|
|
}
|
|
|
|
if (scope_number[kCdScopeWindow] > 0) {
|
|
win = find_win_by_nr(&argvars[0], tp);
|
|
if (!win) {
|
|
emsg(_("E5002: Cannot find window number."));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
cwd = xmalloc(MAXPATHL);
|
|
|
|
switch (scope) {
|
|
case kCdScopeWindow:
|
|
assert(win);
|
|
from = win->w_localdir;
|
|
if (from) {
|
|
break;
|
|
}
|
|
FALLTHROUGH;
|
|
case kCdScopeTabpage:
|
|
assert(tp);
|
|
from = tp->tp_localdir;
|
|
if (from) {
|
|
break;
|
|
}
|
|
FALLTHROUGH;
|
|
case kCdScopeGlobal:
|
|
if (globaldir) { // `globaldir` is not always set.
|
|
from = globaldir;
|
|
break;
|
|
}
|
|
FALLTHROUGH; // In global directory, just need to get OS CWD.
|
|
case kCdScopeInvalid: // If called without any arguments, get OS CWD.
|
|
if (os_dirname(cwd, MAXPATHL) == FAIL) {
|
|
from = ""; // Return empty string on failure.
|
|
}
|
|
}
|
|
|
|
if (from) {
|
|
xstrlcpy(cwd, from, MAXPATHL);
|
|
}
|
|
|
|
rettv->vval.v_string = xstrdup(cwd);
|
|
#ifdef BACKSLASH_IN_FILENAME
|
|
slash_adjust(rettv->vval.v_string);
|
|
#endif
|
|
|
|
xfree(cwd);
|
|
}
|
|
|
|
/// "getfperm({fname})" function
|
|
void f_getfperm(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
|
{
|
|
char *perm = NULL;
|
|
char flags[] = "rwx";
|
|
|
|
const char *filename = tv_get_string(&argvars[0]);
|
|
int32_t file_perm = os_getperm(filename);
|
|
if (file_perm >= 0) {
|
|
perm = xstrdup("---------");
|
|
for (int i = 0; i < 9; i++) {
|
|
if (file_perm & (1 << (8 - i))) {
|
|
perm[i] = flags[i % 3];
|
|
}
|
|
}
|
|
}
|
|
rettv->v_type = VAR_STRING;
|
|
rettv->vval.v_string = perm;
|
|
}
|
|
|
|
/// "getfsize({fname})" function
|
|
void f_getfsize(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
|
{
|
|
const char *fname = tv_get_string(&argvars[0]);
|
|
|
|
rettv->v_type = VAR_NUMBER;
|
|
|
|
FileInfo file_info;
|
|
if (os_fileinfo(fname, &file_info)) {
|
|
uint64_t filesize = os_fileinfo_size(&file_info);
|
|
if (os_isdir(fname)) {
|
|
rettv->vval.v_number = 0;
|
|
} else {
|
|
rettv->vval.v_number = (varnumber_T)filesize;
|
|
|
|
// non-perfect check for overflow
|
|
if ((uint64_t)rettv->vval.v_number != filesize) {
|
|
rettv->vval.v_number = -2;
|
|
}
|
|
}
|
|
} else {
|
|
rettv->vval.v_number = -1;
|
|
}
|
|
}
|
|
|
|
/// "getftime({fname})" function
|
|
void f_getftime(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
|
{
|
|
const char *fname = tv_get_string(&argvars[0]);
|
|
|
|
FileInfo file_info;
|
|
if (os_fileinfo(fname, &file_info)) {
|
|
rettv->vval.v_number = (varnumber_T)file_info.stat.st_mtim.tv_sec;
|
|
} else {
|
|
rettv->vval.v_number = -1;
|
|
}
|
|
}
|
|
|
|
/// "getftype({fname})" function
|
|
void f_getftype(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
|
{
|
|
char *type = NULL;
|
|
char *t;
|
|
|
|
const char *fname = tv_get_string(&argvars[0]);
|
|
|
|
rettv->v_type = VAR_STRING;
|
|
FileInfo file_info;
|
|
if (os_fileinfo_link(fname, &file_info)) {
|
|
uint64_t mode = file_info.stat.st_mode;
|
|
if (S_ISREG(mode)) {
|
|
t = "file";
|
|
} else if (S_ISDIR(mode)) {
|
|
t = "dir";
|
|
} else if (S_ISLNK(mode)) {
|
|
t = "link";
|
|
} else if (S_ISBLK(mode)) {
|
|
t = "bdev";
|
|
} else if (S_ISCHR(mode)) {
|
|
t = "cdev";
|
|
} else if (S_ISFIFO(mode)) {
|
|
t = "fifo";
|
|
} else if (S_ISSOCK(mode)) {
|
|
t = "socket";
|
|
} else {
|
|
t = "other";
|
|
}
|
|
type = xstrdup(t);
|
|
}
|
|
rettv->vval.v_string = type;
|
|
}
|
|
|
|
/// "glob()" function
|
|
void f_glob(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
|
{
|
|
int options = WILD_SILENT|WILD_USE_NL;
|
|
expand_T xpc;
|
|
bool error = false;
|
|
|
|
// When the optional second argument is non-zero, don't remove matches
|
|
// for 'wildignore' and don't put matches for 'suffixes' at the end.
|
|
rettv->v_type = VAR_STRING;
|
|
if (argvars[1].v_type != VAR_UNKNOWN) {
|
|
if (tv_get_number_chk(&argvars[1], &error)) {
|
|
options |= WILD_KEEP_ALL;
|
|
}
|
|
if (argvars[2].v_type != VAR_UNKNOWN) {
|
|
if (tv_get_number_chk(&argvars[2], &error)) {
|
|
tv_list_set_ret(rettv, NULL);
|
|
}
|
|
if (argvars[3].v_type != VAR_UNKNOWN
|
|
&& tv_get_number_chk(&argvars[3], &error)) {
|
|
options |= WILD_ALLLINKS;
|
|
}
|
|
}
|
|
}
|
|
if (!error) {
|
|
ExpandInit(&xpc);
|
|
xpc.xp_context = EXPAND_FILES;
|
|
if (p_wic) {
|
|
options += WILD_ICASE;
|
|
}
|
|
if (rettv->v_type == VAR_STRING) {
|
|
rettv->vval.v_string = ExpandOne(&xpc, (char *)
|
|
tv_get_string(&argvars[0]), NULL, options,
|
|
WILD_ALL);
|
|
} else {
|
|
ExpandOne(&xpc, (char *)tv_get_string(&argvars[0]), NULL, options,
|
|
WILD_ALL_KEEP);
|
|
tv_list_alloc_ret(rettv, xpc.xp_numfiles);
|
|
for (int i = 0; i < xpc.xp_numfiles; i++) {
|
|
tv_list_append_string(rettv->vval.v_list, xpc.xp_files[i], -1);
|
|
}
|
|
ExpandCleanup(&xpc);
|
|
}
|
|
} else {
|
|
rettv->vval.v_string = NULL;
|
|
}
|
|
}
|
|
|
|
/// "globpath()" function
|
|
void f_globpath(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
|
{
|
|
int flags = WILD_IGNORE_COMPLETESLASH; // Flags for globpath.
|
|
bool error = false;
|
|
|
|
// Return a string, or a list if the optional third argument is non-zero.
|
|
rettv->v_type = VAR_STRING;
|
|
|
|
if (argvars[2].v_type != VAR_UNKNOWN) {
|
|
// When the optional second argument is non-zero, don't remove matches
|
|
// for 'wildignore' and don't put matches for 'suffixes' at the end.
|
|
if (tv_get_number_chk(&argvars[2], &error)) {
|
|
flags |= WILD_KEEP_ALL;
|
|
}
|
|
|
|
if (argvars[3].v_type != VAR_UNKNOWN) {
|
|
if (tv_get_number_chk(&argvars[3], &error)) {
|
|
tv_list_set_ret(rettv, NULL);
|
|
}
|
|
if (argvars[4].v_type != VAR_UNKNOWN
|
|
&& tv_get_number_chk(&argvars[4], &error)) {
|
|
flags |= WILD_ALLLINKS;
|
|
}
|
|
}
|
|
}
|
|
|
|
char buf1[NUMBUFLEN];
|
|
const char *const file = tv_get_string_buf_chk(&argvars[1], buf1);
|
|
if (file != NULL && !error) {
|
|
garray_T ga;
|
|
ga_init(&ga, (int)sizeof(char *), 10);
|
|
globpath((char *)tv_get_string(&argvars[0]), (char *)file, &ga, flags, false);
|
|
|
|
if (rettv->v_type == VAR_STRING) {
|
|
rettv->vval.v_string = ga_concat_strings_sep(&ga, "\n");
|
|
} else {
|
|
tv_list_alloc_ret(rettv, ga.ga_len);
|
|
for (int i = 0; i < ga.ga_len; i++) {
|
|
tv_list_append_string(rettv->vval.v_list,
|
|
((const char **)(ga.ga_data))[i], -1);
|
|
}
|
|
}
|
|
|
|
ga_clear_strings(&ga);
|
|
} else {
|
|
rettv->vval.v_string = NULL;
|
|
}
|
|
}
|
|
|
|
/// "glob2regpat()" function
|
|
void f_glob2regpat(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
|
{
|
|
const char *const pat = tv_get_string_chk(&argvars[0]); // NULL on type error
|
|
|
|
rettv->v_type = VAR_STRING;
|
|
rettv->vval.v_string = pat == NULL ? NULL : file_pat_to_reg_pat(pat, NULL, NULL, false);
|
|
}
|
|
|
|
/// `haslocaldir([{win}[, {tab}]])` function
|
|
///
|
|
/// Returns `1` if the scope object has a local directory, `0` otherwise. If a
|
|
/// scope object is not specified the current one is implied. This function
|
|
/// share a lot of code with `f_getcwd`.
|
|
///
|
|
/// @pre The arguments must be of type number.
|
|
/// @pre There may not be more than two arguments.
|
|
/// @pre An argument may not be -1 if preceding arguments are not all -1.
|
|
///
|
|
/// @post The return value will be either the number `1` or `0`.
|
|
void f_haslocaldir(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
|
{
|
|
// Possible scope of working directory to return.
|
|
CdScope scope = kCdScopeInvalid;
|
|
|
|
// Numbers of the scope objects (window, tab) we want the working directory
|
|
// of. A `-1` means to skip this scope, a `0` means the current object.
|
|
int scope_number[] = {
|
|
[kCdScopeWindow] = 0, // Number of window to look at.
|
|
[kCdScopeTabpage] = 0, // Number of tab to look at.
|
|
};
|
|
|
|
tabpage_T *tp = curtab; // The tabpage to look at.
|
|
win_T *win = curwin; // The window to look at.
|
|
|
|
rettv->v_type = VAR_NUMBER;
|
|
rettv->vval.v_number = 0;
|
|
|
|
// Pre-conditions and scope extraction together
|
|
for (int i = MIN_CD_SCOPE; i < MAX_CD_SCOPE; i++) {
|
|
if (argvars[i].v_type == VAR_UNKNOWN) {
|
|
break;
|
|
}
|
|
if (argvars[i].v_type != VAR_NUMBER) {
|
|
emsg(_(e_invarg));
|
|
return;
|
|
}
|
|
scope_number[i] = (int)argvars[i].vval.v_number;
|
|
if (scope_number[i] < -1) {
|
|
emsg(_(e_invarg));
|
|
return;
|
|
}
|
|
// Use the narrowest scope the user requested
|
|
if (scope_number[i] >= 0 && scope == kCdScopeInvalid) {
|
|
// The scope is the current iteration step.
|
|
scope = i;
|
|
} else if (scope_number[i] < 0) {
|
|
scope = i + 1;
|
|
}
|
|
}
|
|
|
|
// If the user didn't specify anything, default to window scope
|
|
if (scope == kCdScopeInvalid) {
|
|
scope = MIN_CD_SCOPE;
|
|
}
|
|
|
|
// Find the tabpage by number
|
|
if (scope_number[kCdScopeTabpage] > 0) {
|
|
tp = find_tabpage(scope_number[kCdScopeTabpage]);
|
|
if (!tp) {
|
|
emsg(_("E5000: Cannot find tab number."));
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Find the window in `tp` by number, `NULL` if none.
|
|
if (scope_number[kCdScopeWindow] >= 0) {
|
|
if (scope_number[kCdScopeTabpage] < 0) {
|
|
emsg(_("E5001: Higher scope cannot be -1 if lower scope is >= 0."));
|
|
return;
|
|
}
|
|
|
|
if (scope_number[kCdScopeWindow] > 0) {
|
|
win = find_win_by_nr(&argvars[0], tp);
|
|
if (!win) {
|
|
emsg(_("E5002: Cannot find window number."));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
switch (scope) {
|
|
case kCdScopeWindow:
|
|
assert(win);
|
|
rettv->vval.v_number = win->w_localdir ? 1 : 0;
|
|
break;
|
|
case kCdScopeTabpage:
|
|
assert(tp);
|
|
rettv->vval.v_number = tp->tp_localdir ? 1 : 0;
|
|
break;
|
|
case kCdScopeGlobal:
|
|
// The global scope never has a local directory
|
|
break;
|
|
case kCdScopeInvalid:
|
|
// We should never get here
|
|
abort();
|
|
}
|
|
}
|
|
|
|
/// "isabsolutepath()" function
|
|
void f_isabsolutepath(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
|
{
|
|
rettv->vval.v_number = path_is_absolute(tv_get_string(&argvars[0]));
|
|
}
|
|
|
|
/// "isdirectory()" function
|
|
void f_isdirectory(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
|
{
|
|
rettv->vval.v_number = os_isdir(tv_get_string(&argvars[0]));
|
|
}
|
|
|
|
/// "mkdir()" function
|
|
void f_mkdir(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
|
{
|
|
int prot = 0755;
|
|
|
|
rettv->vval.v_number = FAIL;
|
|
if (check_secure()) {
|
|
return;
|
|
}
|
|
|
|
char buf[NUMBUFLEN];
|
|
const char *const dir = tv_get_string_buf(&argvars[0], buf);
|
|
if (*dir == NUL) {
|
|
return;
|
|
}
|
|
|
|
if (*path_tail(dir) == NUL) {
|
|
// Remove trailing slashes.
|
|
*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);
|
|
if (prot == -1) {
|
|
return;
|
|
}
|
|
}
|
|
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,
|
|
defer || defer_recurse ? &created : NULL);
|
|
if (ret != 0) {
|
|
semsg(_(e_mkdir), failed_dir, os_strerror(ret));
|
|
xfree(failed_dir);
|
|
rettv->vval.v_number = FAIL;
|
|
return;
|
|
}
|
|
rettv->vval.v_number = OK;
|
|
}
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// "pathshorten()" function
|
|
void f_pathshorten(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
|
{
|
|
int trim_len = 1;
|
|
|
|
if (argvars[1].v_type != VAR_UNKNOWN) {
|
|
trim_len = (int)tv_get_number(&argvars[1]);
|
|
if (trim_len < 1) {
|
|
trim_len = 1;
|
|
}
|
|
}
|
|
|
|
rettv->v_type = VAR_STRING;
|
|
const char *p = tv_get_string_chk(&argvars[0]);
|
|
if (p == NULL) {
|
|
rettv->vval.v_string = NULL;
|
|
} else {
|
|
rettv->vval.v_string = xstrdup(p);
|
|
shorten_dir_len(rettv->vval.v_string, trim_len);
|
|
}
|
|
}
|
|
|
|
/// Evaluate "expr" (= "context") for readdir().
|
|
static varnumber_T readdir_checkitem(void *context, const char *name)
|
|
FUNC_ATTR_NONNULL_ALL
|
|
{
|
|
typval_T *expr = (typval_T *)context;
|
|
typval_T argv[2];
|
|
varnumber_T retval = 0;
|
|
bool error = false;
|
|
|
|
if (expr->v_type == VAR_UNKNOWN) {
|
|
return 1;
|
|
}
|
|
|
|
typval_T save_val;
|
|
prepare_vimvar(VV_VAL, &save_val);
|
|
set_vim_var_string(VV_VAL, name, -1);
|
|
argv[0].v_type = VAR_STRING;
|
|
argv[0].vval.v_string = (char *)name;
|
|
|
|
typval_T rettv;
|
|
if (eval_expr_typval(expr, false, argv, 1, &rettv) == FAIL) {
|
|
goto theend;
|
|
}
|
|
|
|
retval = tv_get_number_chk(&rettv, &error);
|
|
if (error) {
|
|
retval = -1;
|
|
}
|
|
|
|
tv_clear(&rettv);
|
|
|
|
theend:
|
|
set_vim_var_string(VV_VAL, NULL, 0);
|
|
restore_vimvar(VV_VAL, &save_val);
|
|
return retval;
|
|
}
|
|
|
|
/// "readdir()" function
|
|
void f_readdir(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
|
{
|
|
tv_list_alloc_ret(rettv, kListLenUnknown);
|
|
|
|
const char *path = tv_get_string(&argvars[0]);
|
|
typval_T *expr = &argvars[1];
|
|
garray_T ga;
|
|
int ret = readdir_core(&ga, path, (void *)expr, readdir_checkitem);
|
|
if (ret == OK && ga.ga_len > 0) {
|
|
for (int i = 0; i < ga.ga_len; i++) {
|
|
const char *p = ((const char **)ga.ga_data)[i];
|
|
tv_list_append_string(rettv->vval.v_list, p, -1);
|
|
}
|
|
}
|
|
ga_clear_strings(&ga);
|
|
}
|
|
|
|
/// Read blob from file "fd".
|
|
/// Caller has allocated a blob in "rettv".
|
|
///
|
|
/// @param[in] fd File to read from.
|
|
/// @param[in,out] rettv Blob to write to.
|
|
/// @param[in] offset Read the file from the specified offset.
|
|
/// @param[in] size Read the specified size, or -1 if no limit.
|
|
///
|
|
/// @return OK on success, or FAIL on failure.
|
|
static int read_blob(FILE *const fd, typval_T *rettv, off_T offset, off_T size_arg)
|
|
FUNC_ATTR_NONNULL_ALL
|
|
{
|
|
blob_T *const blob = rettv->vval.v_blob;
|
|
FileInfo file_info;
|
|
if (!os_fileinfo_fd(fileno(fd), &file_info)) {
|
|
return FAIL; // can't read the file, error
|
|
}
|
|
|
|
int whence;
|
|
off_T size = size_arg;
|
|
const off_T file_size = (off_T)os_fileinfo_size(&file_info);
|
|
if (offset >= 0) {
|
|
// The size defaults to the whole file. If a size is given it is
|
|
// limited to not go past the end of the file.
|
|
if (size == -1 || (size > file_size - offset && !S_ISCHR(file_info.stat.st_mode))) {
|
|
// size may become negative, checked below
|
|
size = (off_T)os_fileinfo_size(&file_info) - offset;
|
|
}
|
|
whence = SEEK_SET;
|
|
} else {
|
|
// limit the offset to not go before the start of the file
|
|
if (-offset > file_size && !S_ISCHR(file_info.stat.st_mode)) {
|
|
offset = -file_size;
|
|
}
|
|
// Size defaults to reading until the end of the file.
|
|
if (size == -1 || size > -offset) {
|
|
size = -offset;
|
|
}
|
|
whence = SEEK_END;
|
|
}
|
|
if (size <= 0) {
|
|
return OK;
|
|
}
|
|
if (offset != 0 && vim_fseek(fd, offset, whence) != 0) {
|
|
return OK;
|
|
}
|
|
|
|
ga_grow(&blob->bv_ga, (int)size);
|
|
blob->bv_ga.ga_len = (int)size;
|
|
if (fread(blob->bv_ga.ga_data, 1, (size_t)blob->bv_ga.ga_len, fd)
|
|
< (size_t)blob->bv_ga.ga_len) {
|
|
// An empty blob is returned on error.
|
|
tv_blob_free(rettv->vval.v_blob);
|
|
rettv->vval.v_blob = NULL;
|
|
return FAIL;
|
|
}
|
|
return OK;
|
|
}
|
|
|
|
/// "readfile()" or "readblob()" function
|
|
static void read_file_or_blob(typval_T *argvars, typval_T *rettv, bool always_blob)
|
|
{
|
|
bool binary = false;
|
|
bool blob = always_blob;
|
|
FILE *fd;
|
|
char buf[(IOSIZE/256) * 256]; // rounded to avoid odd + 1
|
|
int io_size = sizeof(buf);
|
|
char *prev = NULL; // previously read bytes, if any
|
|
ptrdiff_t prevlen = 0; // length of data in prev
|
|
ptrdiff_t prevsize = 0; // size of prev buffer
|
|
int64_t maxline = MAXLNUM;
|
|
off_T offset = 0;
|
|
off_T size = -1;
|
|
|
|
if (argvars[1].v_type != VAR_UNKNOWN) {
|
|
if (always_blob) {
|
|
offset = (off_T)tv_get_number(&argvars[1]);
|
|
if (argvars[2].v_type != VAR_UNKNOWN) {
|
|
size = (off_T)tv_get_number(&argvars[2]);
|
|
}
|
|
} else {
|
|
if (strcmp(tv_get_string(&argvars[1]), "b") == 0) {
|
|
binary = true;
|
|
} else if (strcmp(tv_get_string(&argvars[1]), "B") == 0) {
|
|
blob = true;
|
|
}
|
|
if (argvars[2].v_type != VAR_UNKNOWN) {
|
|
maxline = tv_get_number(&argvars[2]);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (blob) {
|
|
tv_blob_alloc_ret(rettv);
|
|
} else {
|
|
tv_list_alloc_ret(rettv, kListLenUnknown);
|
|
}
|
|
|
|
// Always open the file in binary mode, library functions have a mind of
|
|
// their own about CR-LF conversion.
|
|
const char *const fname = tv_get_string(&argvars[0]);
|
|
|
|
if (os_isdir(fname)) {
|
|
semsg(_(e_isadir2), fname);
|
|
return;
|
|
}
|
|
if (*fname == NUL || (fd = os_fopen(fname, READBIN)) == NULL) {
|
|
semsg(_(e_notopen), *fname == NUL ? _("<empty>") : fname);
|
|
return;
|
|
}
|
|
|
|
if (blob) {
|
|
if (read_blob(fd, rettv, offset, size) == FAIL) {
|
|
semsg(_(e_notread), fname);
|
|
}
|
|
fclose(fd);
|
|
return;
|
|
}
|
|
|
|
list_T *const l = rettv->vval.v_list;
|
|
|
|
while (maxline < 0 || tv_list_len(l) < maxline) {
|
|
int readlen = (int)fread(buf, 1, (size_t)io_size, fd);
|
|
|
|
// This for loop processes what was read, but is also entered at end
|
|
// of file so that either:
|
|
// - an incomplete line gets written
|
|
// - a "binary" file gets an empty line at the end if it ends in a
|
|
// newline.
|
|
char *p; // Position in buf.
|
|
char *start; // Start of current line.
|
|
for (p = buf, start = buf;
|
|
p < buf + readlen || (readlen <= 0 && (prevlen > 0 || binary));
|
|
p++) {
|
|
if (readlen <= 0 || *p == '\n') {
|
|
char *s = NULL;
|
|
size_t len = (size_t)(p - start);
|
|
|
|
// Finished a line. Remove CRs before NL.
|
|
if (readlen > 0 && !binary) {
|
|
while (len > 0 && start[len - 1] == '\r') {
|
|
len--;
|
|
}
|
|
// removal may cross back to the "prev" string
|
|
if (len == 0) {
|
|
while (prevlen > 0 && prev[prevlen - 1] == '\r') {
|
|
prevlen--;
|
|
}
|
|
}
|
|
}
|
|
if (prevlen == 0) {
|
|
assert(len < INT_MAX);
|
|
s = xmemdupz(start, len);
|
|
} else {
|
|
// Change "prev" buffer to be the right size. This way
|
|
// the bytes are only copied once, and very long lines are
|
|
// allocated only once.
|
|
s = xrealloc(prev, (size_t)prevlen + len + 1);
|
|
memcpy(s + prevlen, start, len);
|
|
s[(size_t)prevlen + len] = NUL;
|
|
prev = NULL; // the list will own the string
|
|
prevlen = prevsize = 0;
|
|
}
|
|
|
|
tv_list_append_owned_tv(l, (typval_T) {
|
|
.v_type = VAR_STRING,
|
|
.v_lock = VAR_UNLOCKED,
|
|
.vval.v_string = s,
|
|
});
|
|
|
|
start = p + 1; // Step over newline.
|
|
if (maxline < 0) {
|
|
if (tv_list_len(l) > -maxline) {
|
|
assert(tv_list_len(l) == 1 + (-maxline));
|
|
tv_list_item_remove(l, tv_list_first(l));
|
|
}
|
|
} else if (tv_list_len(l) >= maxline) {
|
|
assert(tv_list_len(l) == maxline);
|
|
break;
|
|
}
|
|
if (readlen <= 0) {
|
|
break;
|
|
}
|
|
} else if (*p == NUL) {
|
|
*p = '\n';
|
|
// Check for utf8 "bom"; U+FEFF is encoded as EF BB BF. Do this
|
|
// when finding the BF and check the previous two bytes.
|
|
} else if ((uint8_t)(*p) == 0xbf && !binary) {
|
|
// Find the two bytes before the 0xbf. If p is at buf, or buf + 1,
|
|
// these may be in the "prev" string.
|
|
char back1 = p >= buf + 1 ? p[-1]
|
|
: prevlen >= 1 ? prev[prevlen - 1] : NUL;
|
|
char back2 = p >= buf + 2 ? p[-2]
|
|
: (p == buf + 1 && prevlen >= 1
|
|
? prev[prevlen - 1]
|
|
: prevlen >= 2 ? prev[prevlen - 2] : NUL);
|
|
|
|
if ((uint8_t)back2 == 0xef && (uint8_t)back1 == 0xbb) {
|
|
char *dest = p - 2;
|
|
|
|
// Usually a BOM is at the beginning of a file, and so at
|
|
// the beginning of a line; then we can just step over it.
|
|
if (start == dest) {
|
|
start = p + 1;
|
|
} else {
|
|
// have to shuffle buf to close gap
|
|
int adjust_prevlen = 0;
|
|
|
|
if (dest < buf) {
|
|
// adjust_prevlen must be 1 or 2.
|
|
adjust_prevlen = (int)(buf - dest);
|
|
dest = buf;
|
|
}
|
|
if (readlen > p - buf + 1) {
|
|
memmove(dest, p + 1, (size_t)readlen - (size_t)(p - buf) - 1);
|
|
}
|
|
readlen -= 3 - adjust_prevlen;
|
|
prevlen -= adjust_prevlen;
|
|
p = dest - 1;
|
|
}
|
|
}
|
|
}
|
|
} // for
|
|
|
|
if ((maxline >= 0 && tv_list_len(l) >= maxline) || readlen <= 0) {
|
|
break;
|
|
}
|
|
if (start < p) {
|
|
// There's part of a line in buf, store it in "prev".
|
|
if (p - start + prevlen >= prevsize) {
|
|
// A common use case is ordinary text files and "prev" gets a
|
|
// fragment of a line, so the first allocation is made
|
|
// small, to avoid repeatedly 'allocing' large and
|
|
// 'reallocing' small.
|
|
if (prevsize == 0) {
|
|
prevsize = p - start;
|
|
} else {
|
|
ptrdiff_t grow50pc = (prevsize * 3) / 2;
|
|
ptrdiff_t growmin = (p - start) * 2 + prevlen;
|
|
prevsize = grow50pc > growmin ? grow50pc : growmin;
|
|
}
|
|
prev = xrealloc(prev, (size_t)prevsize);
|
|
}
|
|
// Add the line part to end of "prev".
|
|
memmove(prev + prevlen, start, (size_t)(p - start));
|
|
prevlen += p - start;
|
|
}
|
|
} // while
|
|
|
|
xfree(prev);
|
|
fclose(fd);
|
|
}
|
|
|
|
/// "readblob()" function
|
|
void f_readblob(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
|
{
|
|
read_file_or_blob(argvars, rettv, true);
|
|
}
|
|
|
|
/// "readfile()" function
|
|
void f_readfile(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
|
{
|
|
read_file_or_blob(argvars, rettv, false);
|
|
}
|
|
|
|
/// "rename({from}, {to})" function
|
|
void f_rename(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
|
{
|
|
if (check_secure()) {
|
|
rettv->vval.v_number = -1;
|
|
} else {
|
|
char buf[NUMBUFLEN];
|
|
rettv->vval.v_number = vim_rename(tv_get_string(&argvars[0]),
|
|
tv_get_string_buf(&argvars[1], buf));
|
|
}
|
|
}
|
|
|
|
/// "resolve()" function
|
|
void f_resolve(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
|
{
|
|
rettv->v_type = VAR_STRING;
|
|
const char *fname = tv_get_string(&argvars[0]);
|
|
#ifdef MSWIN
|
|
char *v = os_resolve_shortcut(fname);
|
|
if (v == NULL) {
|
|
if (os_is_reparse_point_include(fname)) {
|
|
v = os_realpath(fname, NULL, MAXPATHL + 1);
|
|
}
|
|
}
|
|
rettv->vval.v_string = (v == NULL ? xstrdup(fname) : v);
|
|
#else
|
|
# ifdef HAVE_READLINK
|
|
{
|
|
bool is_relative_to_current = false;
|
|
bool has_trailing_pathsep = false;
|
|
int limit = 100;
|
|
|
|
char *p = xstrdup(fname);
|
|
|
|
if (p[0] == '.' && (vim_ispathsep(p[1])
|
|
|| (p[1] == '.' && (vim_ispathsep(p[2]))))) {
|
|
is_relative_to_current = true;
|
|
}
|
|
|
|
ptrdiff_t len = (ptrdiff_t)strlen(p);
|
|
if (len > 1 && after_pathsep(p, p + len)) {
|
|
has_trailing_pathsep = true;
|
|
p[len - 1] = NUL; // The trailing slash breaks readlink().
|
|
}
|
|
|
|
char *q = (char *)path_next_component(p);
|
|
char *remain = NULL;
|
|
if (*q != NUL) {
|
|
// Separate the first path component in "p", and keep the
|
|
// remainder (beginning with the path separator).
|
|
remain = xstrdup(q - 1);
|
|
q[-1] = NUL;
|
|
}
|
|
|
|
char *const buf = xmallocz(MAXPATHL);
|
|
|
|
char *cpy;
|
|
while (true) {
|
|
while (true) {
|
|
len = readlink(p, buf, MAXPATHL);
|
|
if (len <= 0) {
|
|
break;
|
|
}
|
|
buf[len] = NUL;
|
|
|
|
if (limit-- == 0) {
|
|
xfree(p);
|
|
xfree(remain);
|
|
emsg(_("E655: Too many symbolic links (cycle?)"));
|
|
rettv->vval.v_string = NULL;
|
|
xfree(buf);
|
|
return;
|
|
}
|
|
|
|
// Ensure that the result will have a trailing path separator
|
|
// if the argument has one.
|
|
if (remain == NULL && has_trailing_pathsep) {
|
|
add_pathsep(buf);
|
|
}
|
|
|
|
// Separate the first path component in the link value and
|
|
// concatenate the remainders.
|
|
q = (char *)path_next_component(vim_ispathsep(*buf) ? buf + 1 : buf);
|
|
if (*q != NUL) {
|
|
cpy = remain;
|
|
remain = remain != NULL ? concat_str(q - 1, remain) : xstrdup(q - 1);
|
|
xfree(cpy);
|
|
q[-1] = NUL;
|
|
}
|
|
|
|
q = path_tail(p);
|
|
if (q > p && *q == NUL) {
|
|
// Ignore trailing path separator.
|
|
p[q - p - 1] = NUL;
|
|
q = path_tail(p);
|
|
}
|
|
if (q > p && !path_is_absolute(buf)) {
|
|
// Symlink is relative to directory of argument. Replace the
|
|
// symlink with the resolved name in the same directory.
|
|
const size_t p_len = strlen(p);
|
|
const size_t buf_len = strlen(buf);
|
|
p = xrealloc(p, p_len + buf_len + 1);
|
|
memcpy(path_tail(p), buf, buf_len + 1);
|
|
} else {
|
|
xfree(p);
|
|
p = xstrdup(buf);
|
|
}
|
|
}
|
|
|
|
if (remain == NULL) {
|
|
break;
|
|
}
|
|
|
|
// Append the first path component of "remain" to "p".
|
|
q = (char *)path_next_component(remain + 1);
|
|
len = q - remain - (*q != NUL);
|
|
const size_t p_len = strlen(p);
|
|
cpy = xmallocz(p_len + (size_t)len);
|
|
memcpy(cpy, p, p_len + 1);
|
|
xstrlcat(cpy + p_len, remain, (size_t)len + 1);
|
|
xfree(p);
|
|
p = cpy;
|
|
|
|
// Shorten "remain".
|
|
if (*q != NUL) {
|
|
STRMOVE(remain, q - 1);
|
|
} else {
|
|
XFREE_CLEAR(remain);
|
|
}
|
|
}
|
|
|
|
// If the result is a relative path name, make it explicitly relative to
|
|
// the current directory if and only if the argument had this form.
|
|
if (!vim_ispathsep(*p)) {
|
|
if (is_relative_to_current
|
|
&& *p != NUL
|
|
&& !(p[0] == '.'
|
|
&& (p[1] == NUL
|
|
|| vim_ispathsep(p[1])
|
|
|| (p[1] == '.'
|
|
&& (p[2] == NUL
|
|
|| vim_ispathsep(p[2])))))) {
|
|
// Prepend "./".
|
|
cpy = concat_str("./", p);
|
|
xfree(p);
|
|
p = cpy;
|
|
} else if (!is_relative_to_current) {
|
|
// Strip leading "./".
|
|
q = p;
|
|
while (q[0] == '.' && vim_ispathsep(q[1])) {
|
|
q += 2;
|
|
}
|
|
if (q > p) {
|
|
STRMOVE(p, p + 2);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ensure that the result will have no trailing path separator
|
|
// if the argument had none. But keep "/" or "//".
|
|
if (!has_trailing_pathsep) {
|
|
q = p + strlen(p);
|
|
if (after_pathsep(p, q)) {
|
|
*path_tail_with_sep(p) = NUL;
|
|
}
|
|
}
|
|
|
|
rettv->vval.v_string = p;
|
|
xfree(buf);
|
|
}
|
|
# else
|
|
char *v = os_realpath(fname, NULL, MAXPATHL + 1);
|
|
rettv->vval.v_string = v == NULL ? xstrdup(fname) : v;
|
|
# endif
|
|
#endif
|
|
|
|
simplify_filename(rettv->vval.v_string);
|
|
}
|
|
|
|
/// "simplify()" function
|
|
void f_simplify(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
|
{
|
|
const char *const p = tv_get_string(&argvars[0]);
|
|
rettv->vval.v_string = xstrdup(p);
|
|
simplify_filename(rettv->vval.v_string); // Simplify in place.
|
|
rettv->v_type = VAR_STRING;
|
|
}
|
|
|
|
/// "tempname()" function
|
|
void f_tempname(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
|
{
|
|
rettv->v_type = VAR_STRING;
|
|
rettv->vval.v_string = vim_tempname();
|
|
}
|
|
|
|
/// Write "list" of strings to file "fd".
|
|
///
|
|
/// @param fp File to write to.
|
|
/// @param[in] list List to write.
|
|
/// @param[in] binary Whether to write in binary mode.
|
|
///
|
|
/// @return true in case of success, false otherwise.
|
|
static bool write_list(FileDescriptor *const fp, const list_T *const list, const bool binary)
|
|
FUNC_ATTR_NONNULL_ARG(1)
|
|
{
|
|
int error = 0;
|
|
TV_LIST_ITER_CONST(list, li, {
|
|
const char *const s = tv_get_string_chk(TV_LIST_ITEM_TV(li));
|
|
if (s == NULL) {
|
|
return false;
|
|
}
|
|
const char *hunk_start = s;
|
|
for (const char *p = hunk_start;; p++) {
|
|
if (*p == NUL || *p == NL) {
|
|
if (p != hunk_start) {
|
|
const ptrdiff_t written = file_write(fp, hunk_start,
|
|
(size_t)(p - hunk_start));
|
|
if (written < 0) {
|
|
error = (int)written;
|
|
goto write_list_error;
|
|
}
|
|
}
|
|
if (*p == NUL) {
|
|
break;
|
|
} else {
|
|
hunk_start = p + 1;
|
|
const ptrdiff_t written = file_write(fp, (char[]){ NUL }, 1);
|
|
if (written < 0) {
|
|
error = (int)written;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!binary || TV_LIST_ITEM_NEXT(list, li) != NULL) {
|
|
const ptrdiff_t written = file_write(fp, "\n", 1);
|
|
if (written < 0) {
|
|
error = (int)written;
|
|
goto write_list_error;
|
|
}
|
|
}
|
|
});
|
|
if ((error = file_flush(fp)) != 0) {
|
|
goto write_list_error;
|
|
}
|
|
return true;
|
|
write_list_error:
|
|
semsg(_(e_error_while_writing_str), os_strerror(error));
|
|
return false;
|
|
}
|
|
|
|
/// Write a blob to file with descriptor `fp`.
|
|
///
|
|
/// @param[in] fp File to write to.
|
|
/// @param[in] blob Blob to write.
|
|
///
|
|
/// @return true on success, or false on failure.
|
|
static bool write_blob(FileDescriptor *const fp, const blob_T *const blob)
|
|
FUNC_ATTR_NONNULL_ARG(1)
|
|
{
|
|
int error = 0;
|
|
const int len = tv_blob_len(blob);
|
|
if (len > 0) {
|
|
const ptrdiff_t written = file_write(fp, blob->bv_ga.ga_data, (size_t)len);
|
|
if (written < (ptrdiff_t)len) {
|
|
error = (int)written;
|
|
goto write_blob_error;
|
|
}
|
|
}
|
|
error = file_flush(fp);
|
|
if (error != 0) {
|
|
goto write_blob_error;
|
|
}
|
|
return true;
|
|
write_blob_error:
|
|
semsg(_(e_error_while_writing_str), os_strerror(error));
|
|
return false;
|
|
}
|
|
|
|
/// "writefile()" function
|
|
void f_writefile(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
|
|
{
|
|
rettv->vval.v_number = -1;
|
|
|
|
if (check_secure()) {
|
|
return;
|
|
}
|
|
|
|
if (argvars[0].v_type == VAR_LIST) {
|
|
TV_LIST_ITER_CONST(argvars[0].vval.v_list, li, {
|
|
if (!tv_check_str_or_nr(TV_LIST_ITEM_TV(li))) {
|
|
return;
|
|
}
|
|
});
|
|
} else if (argvars[0].v_type != VAR_BLOB) {
|
|
semsg(_(e_invarg2),
|
|
_("writefile() first argument must be a List or a Blob"));
|
|
return;
|
|
}
|
|
|
|
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) {
|
|
const char *const flags = tv_get_string_chk(&argvars[2]);
|
|
if (flags == NULL) {
|
|
return;
|
|
}
|
|
for (const char *p = flags; *p; p++) {
|
|
switch (*p) {
|
|
case 'b':
|
|
binary = true; break;
|
|
case 'a':
|
|
append = true; break;
|
|
case 'D':
|
|
defer = true; break;
|
|
case 's':
|
|
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);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
char buf[NUMBUFLEN];
|
|
const char *const fname = tv_get_string_buf_chk(&argvars[1], buf);
|
|
if (fname == NULL) {
|
|
return;
|
|
}
|
|
|
|
if (defer && !can_add_defer()) {
|
|
return;
|
|
}
|
|
|
|
FileDescriptor fp;
|
|
int error;
|
|
if (*fname == NUL) {
|
|
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));
|
|
} 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);
|
|
} else {
|
|
write_ok = write_list(&fp, argvars[0].vval.v_list, binary);
|
|
}
|
|
if (write_ok) {
|
|
rettv->vval.v_number = 0;
|
|
}
|
|
if ((error = file_close(&fp, do_fsync)) != 0) {
|
|
semsg(_("E80: Error when closing file %s: %s"),
|
|
fname, os_strerror(error));
|
|
}
|
|
}
|
|
}
|