Files
neovim/src/nvim/os/env.c
Justin M. Keyes 497db001df test: expand_env_esc()
Test expand_env_esc() using the same parameters reported in #3725.

Closes #3725
2017-01-21 18:17:44 +01:00

835 lines
24 KiB
C

// env.c -- environment variable access
#include <assert.h>
#include <uv.h>
// vim.h must be included before charset.h (and possibly others) or things
// blow up
#include "nvim/vim.h"
#include "nvim/ascii.h"
#include "nvim/charset.h"
#include "nvim/os/os.h"
#include "nvim/memory.h"
#include "nvim/message.h"
#include "nvim/path.h"
#include "nvim/strings.h"
#include "nvim/eval.h"
#include "nvim/ex_getln.h"
#include "nvim/version.h"
#ifdef HAVE__NSGETENVIRON
#include <crt_externs.h>
#endif
#ifdef HAVE_SYS_UTSNAME_H
#include <sys/utsname.h>
#endif
/// Like getenv(), but returns NULL if the variable is empty.
const char *os_getenv(const char *name)
FUNC_ATTR_NONNULL_ALL
{
const char *e = getenv(name);
return e == NULL || *e == NUL ? NULL : e;
}
/// Returns `true` if the environment variable, `name`, has been defined
/// (even if empty).
bool os_env_exists(const char *name)
FUNC_ATTR_NONNULL_ALL
{
return getenv(name) != NULL;
}
int os_setenv(const char *name, const char *value, int overwrite)
FUNC_ATTR_NONNULL_ALL
{
#ifdef HAVE_SETENV
return setenv(name, value, overwrite);
#elif defined(HAVE_PUTENV_S)
if (!overwrite && os_getenv(name) != NULL) {
return 0;
}
if (_putenv_s(name, value) == 0) {
return 0;
}
return -1;
#else
# error "This system has no implementation available for os_setenv()"
#endif
}
/// Unset environment variable
///
/// For systems where unsetenv() is not available the value will be set as an
/// empty string
int os_unsetenv(const char *name)
{
#ifdef HAVE_UNSETENV
return unsetenv(name);
#else
return os_setenv(name, "", 1);
#endif
}
char *os_getenvname_at_index(size_t index)
{
# if defined(HAVE__NSGETENVIRON)
char **environ = *_NSGetEnviron();
# elif !defined(__WIN32__)
// Borland C++ 5.2 has this in a header file.
extern char **environ;
# endif
// check if index is inside the environ array
for (size_t i = 0; i < index; i++) {
if (environ[i] == NULL) {
return NULL;
}
}
char *str = environ[index];
if (str == NULL) {
return NULL;
}
size_t namesize = 0;
while (str[namesize] != '=' && str[namesize] != NUL) {
namesize++;
}
char *name = (char *)vim_strnsave((char_u *)str, namesize);
return name;
}
/// Get the process ID of the Neovim process.
///
/// @return the process ID.
int64_t os_get_pid(void)
{
#ifdef _WIN32
return (int64_t)GetCurrentProcessId();
#else
return (int64_t)getpid();
#endif
}
/// Get the hostname of the machine running Neovim.
///
/// @param hostname Buffer to store the hostname.
/// @param len Length of `hostname`.
void os_get_hostname(char *hostname, size_t len)
{
#ifdef HAVE_SYS_UTSNAME_H
struct utsname vutsname;
if (uname(&vutsname) < 0) {
*hostname = '\0';
} else {
strncpy(hostname, vutsname.nodename, len - 1);
hostname[len - 1] = '\0';
}
#else
// TODO(unknown): Implement this for windows.
// See the implementation used in vim:
// https://code.google.com/p/vim/source/browse/src/os_win32.c?r=6b69d8dde19e32909f4ee3a6337e6a2ecfbb6f72#2899
*hostname = '\0';
#endif
}
/// To get the "real" home directory:
/// - get value of $HOME
/// For Unix:
/// - go to that directory
/// - do os_dirname() to get the real name of that directory.
/// This also works with mounts and links.
/// Don't do this for Windows, it will change the "current dir" for a drive.
static char_u *homedir = NULL;
void init_homedir(void)
{
// In case we are called a second time (when 'encoding' changes).
xfree(homedir);
homedir = NULL;
char_u *var = (char_u *)os_getenv("HOME");
#ifdef WIN32
// Typically, $HOME is not defined on Windows, unless the user has
// specifically defined it for Vim's sake. However, on Windows NT
// platforms, $HOMEDRIVE and $HOMEPATH are automatically defined for
// each user. Try constructing $HOME from these.
if (var == NULL) {
const char *homedrive = os_getenv("HOMEDRIVE");
const char *homepath = os_getenv("HOMEPATH");
if (homepath == NULL) {
homepath = "\\";
}
if (homedrive != NULL && strlen(homedrive) + strlen(homepath) < MAXPATHL) {
snprintf((char *)NameBuff, MAXPATHL, "%s%s", homedrive, homepath);
if (NameBuff[0] != NUL) {
var = NameBuff;
vim_setenv("HOME", (char *)NameBuff);
}
}
}
#endif
if (var != NULL) {
#ifdef UNIX
// Change to the directory and get the actual path. This resolves
// links. Don't do it when we can't return.
if (os_dirname(NameBuff, MAXPATHL) == OK
&& os_chdir((char *)NameBuff) == 0) {
if (!os_chdir((char *)var) && os_dirname(IObuff, IOSIZE) == OK) {
var = IObuff;
}
if (os_chdir((char *)NameBuff) != 0) {
EMSG(_(e_prev_dir));
}
}
#endif
homedir = vim_strsave(var);
}
}
#if defined(EXITFREE)
void free_homedir(void)
{
xfree(homedir);
}
#endif
/// Call expand_env() and store the result in an allocated string.
/// This is not very memory efficient, this expects the result to be freed
/// again soon.
/// @param src String containing environment variables to expand
/// @see {expand_env}
char_u *expand_env_save(char_u *src)
{
return expand_env_save_opt(src, false);
}
/// Similar to expand_env_save() but when "one" is `true` handle the string as
/// one file name, i.e. only expand "~" at the start.
/// @param src String containing environment variables to expand
/// @param one Should treat as only one file name
/// @see {expand_env}
char_u *expand_env_save_opt(char_u *src, bool one)
{
char_u *p = xmalloc(MAXPATHL);
expand_env_esc(src, p, MAXPATHL, false, one, NULL);
return p;
}
/// Expand environment variable with path name.
/// "~/" is also expanded, using $HOME. For Unix "~user/" is expanded.
/// Skips over "\ ", "\~" and "\$" (not for Win32 though).
/// If anything fails no expansion is done and dst equals src.
///
/// @param src Input string e.g. "$HOME/vim.hlp"
/// @param dst[out] Where to put the result
/// @param dstlen Maximum length of the result
void expand_env(char_u *src, char_u *dst, int dstlen)
{
expand_env_esc(src, dst, dstlen, false, false, NULL);
}
/// Expand environment variable with path name and escaping.
/// @see expand_env
///
/// @param srcp Input string e.g. "$HOME/vim.hlp"
/// @param dst[out] Where to put the result
/// @param dstlen Maximum length of the result
/// @param esc Escape spaces in expanded variables
/// @param one `srcp` is a single filename
/// @param prefix Start again after this (can be NULL)
void expand_env_esc(char_u *restrict srcp,
char_u *restrict dst,
int dstlen,
bool esc,
bool one,
char_u *prefix)
{
char_u *tail;
char_u *var;
bool copy_char;
bool mustfree; // var was allocated, need to free it later
bool at_start = true; // at start of a name
int prefix_len = (prefix == NULL) ? 0 : (int)STRLEN(prefix);
char_u *src = skipwhite(srcp);
dstlen--; // leave one char space for "\,"
while (*src && dstlen > 0) {
// Skip over `=expr`.
if (src[0] == '`' && src[1] == '=') {
var = src;
src += 2;
(void)skip_expr(&src);
if (*src == '`') {
src++;
}
size_t len = (size_t)(src - var);
if (len > (size_t)dstlen) {
len = (size_t)dstlen;
}
memcpy((char *)dst, (char *)var, len);
dst += len;
dstlen -= (int)len;
continue;
}
copy_char = true;
if ((*src == '$') || (*src == '~' && at_start)) {
mustfree = false;
// The variable name is copied into dst temporarily, because it may
// be a string in read-only memory and a NUL needs to be appended.
if (*src != '~') { // environment var
tail = src + 1;
var = dst;
int c = dstlen - 1;
#ifdef UNIX
// Unix has ${var-name} type environment vars
if (*tail == '{' && !vim_isIDc('{')) {
tail++; // ignore '{'
while (c-- > 0 && *tail != NUL && *tail != '}') {
*var++ = *tail++;
}
} else // NOLINT
#endif
{
while (c-- > 0 && *tail != NUL && vim_isIDc(*tail)) {
*var++ = *tail++;
}
}
#if defined(UNIX)
// Verify that we have found the end of a Unix ${VAR} style variable
if (src[1] == '{' && *tail != '}') {
var = NULL;
} else {
if (src[1] == '{') {
++tail;
}
#endif
*var = NUL;
var = (char_u *)vim_getenv((char *)dst);
mustfree = true;
#if defined(UNIX)
}
#endif
} else if (src[1] == NUL // home directory
|| vim_ispathsep(src[1])
|| vim_strchr((char_u *)" ,\t\n", src[1]) != NULL) {
var = homedir;
tail = src + 1;
} else { // user directory
#if defined(UNIX)
// Copy ~user to dst[], so we can put a NUL after it.
tail = src;
var = dst;
int c = dstlen - 1;
while (c-- > 0
&& *tail
&& vim_isfilec(*tail)
&& !vim_ispathsep(*tail)) {
*var++ = *tail++;
}
*var = NUL;
// Use os_get_user_directory() to get the user directory.
// If this function fails, the shell is used to
// expand ~user. This is slower and may fail if the shell
// does not support ~user (old versions of /bin/sh).
var = (char_u *)os_get_user_directory((char *)dst + 1);
mustfree = true;
if (var == NULL) {
expand_T xpc;
ExpandInit(&xpc);
xpc.xp_context = EXPAND_FILES;
var = ExpandOne(&xpc, dst, NULL,
WILD_ADD_SLASH|WILD_SILENT, WILD_EXPAND_FREE);
mustfree = true;
}
#else
// cannot expand user's home directory, so don't try
var = NULL;
tail = (char_u *)""; // for gcc
#endif // UNIX
}
#ifdef BACKSLASH_IN_FILENAME
// If 'shellslash' is set change backslashes to forward slashes.
// Can't use slash_adjust(), p_ssl may be set temporarily.
if (p_ssl && var != NULL && vim_strchr(var, '\\') != NULL) {
char_u *p = vim_strsave(var);
if (mustfree) {
xfree(var);
}
var = p;
mustfree = true;
forward_slash(var);
}
#endif
// If "var" contains white space, escape it with a backslash.
// Required for ":e ~/tt" when $HOME includes a space.
if (esc && var != NULL && vim_strpbrk(var, (char_u *)" \t") != NULL) {
char_u *p = vim_strsave_escaped(var, (char_u *)" \t");
if (mustfree) {
xfree(var);
}
var = p;
mustfree = true;
}
if (var != NULL && *var != NUL
&& (STRLEN(var) + STRLEN(tail) + 1 < (unsigned)dstlen)) {
STRCPY(dst, var);
dstlen -= (int)STRLEN(var);
int c = (int)STRLEN(var);
// if var[] ends in a path separator and tail[] starts
// with it, skip a character
if (*var != NUL && after_pathsep((char *)dst, (char *)dst + c)
#if defined(BACKSLASH_IN_FILENAME)
&& dst[-1] != ':'
#endif
&& vim_ispathsep(*tail))
++tail;
dst += c;
src = tail;
copy_char = false;
}
if (mustfree) {
xfree(var);
}
}
if (copy_char) { // copy at least one char
// Recognize the start of a new name, for '~'.
// Don't do this when "one" is true, to avoid expanding "~" in
// ":edit foo ~ foo".
at_start = false;
if (src[0] == '\\' && src[1] != NUL) {
*dst++ = *src++;
--dstlen;
} else if ((src[0] == ' ' || src[0] == ',') && !one) {
at_start = true;
}
*dst++ = *src++;
--dstlen;
if (prefix != NULL && src - prefix_len >= srcp
&& STRNCMP(src - prefix_len, prefix, prefix_len) == 0) {
at_start = true;
}
}
}
*dst = NUL;
}
/// Check if the directory "vimdir/<version>" or "vimdir/runtime" exists.
/// Return NULL if not, return its name in allocated memory otherwise.
/// @param vimdir directory to test
static char *vim_version_dir(const char *vimdir)
{
if (vimdir == NULL || *vimdir == NUL) {
return NULL;
}
char *p = concat_fnames(vimdir, VIM_VERSION_NODOT, true);
if (os_isdir((char_u *)p)) {
return p;
}
xfree(p);
p = concat_fnames(vimdir, RUNTIME_DIRNAME, true);
if (os_isdir((char_u *)p)) {
return p;
}
xfree(p);
return NULL;
}
/// If `dirname + "/"` precedes `pend` in the path, return the pointer to
/// `dirname + "/" + pend`. Otherwise return `pend`.
///
/// Examples (path = /usr/local/share/nvim/runtime/doc/help.txt):
///
/// pend = help.txt
/// dirname = doc
/// -> doc/help.txt
///
/// pend = doc/help.txt
/// dirname = runtime
/// -> runtime/doc/help.txt
///
/// pend = runtime/doc/help.txt
/// dirname = vim74
/// -> runtime/doc/help.txt
///
/// @param path Path to a file
/// @param pend A suffix of the path
/// @param dirname The immediate path fragment before the pend
/// @return The new pend including dirname or just pend
static char *remove_tail(char *path, char *pend, char *dirname)
{
size_t len = STRLEN(dirname);
char *new_tail = pend - len - 1;
if (new_tail >= path
&& fnamencmp((char_u *)new_tail, (char_u *)dirname, len) == 0
&& (new_tail == path || after_pathsep(path, new_tail))) {
return new_tail;
}
return pend;
}
/// Iterate over colon-separated list
///
/// @note Environment variables must not be modified during iteration.
///
/// @param[in] val Value of the environment variable to iterate over.
/// @param[in] iter Pointer used for iteration. Must be NULL on first
/// iteration.
/// @param[out] dir Location where pointer to the start of the current
/// directory name should be saved. May be set to NULL.
/// @param[out] len Location where current directory length should be saved.
///
/// @return Next iter argument value or NULL when iteration should stop.
const void *vim_colon_env_iter(const char *const val,
const void *const iter,
const char **const dir,
size_t *const len)
FUNC_ATTR_NONNULL_ARG(1, 3, 4) FUNC_ATTR_WARN_UNUSED_RESULT
{
const char *varval = (const char *) iter;
if (varval == NULL) {
varval = val;
}
*dir = varval;
const char *const dirend = strchr(varval, ':');
if (dirend == NULL) {
*len = strlen(varval);
return NULL;
} else {
*len = (size_t) (dirend - varval);
return dirend + 1;
}
}
/// Iterate over colon-separated list in reverse order
///
/// @note Environment variables must not be modified during iteration.
///
/// @param[in] val Value of the environment variable to iterate over.
/// @param[in] iter Pointer used for iteration. Must be NULL on first
/// iteration.
/// @param[out] dir Location where pointer to the start of the current
/// directory name should be saved. May be set to NULL.
/// @param[out] len Location where current directory length should be saved.
///
/// @return Next iter argument value or NULL when iteration should stop.
const void *vim_colon_env_iter_rev(const char *const val,
const void *const iter,
const char **const dir,
size_t *const len)
FUNC_ATTR_NONNULL_ARG(1, 3, 4) FUNC_ATTR_WARN_UNUSED_RESULT
{
const char *varend = (const char *) iter;
if (varend == NULL) {
varend = val + strlen(val) - 1;
}
const size_t varlen = (size_t) (varend - val) + 1;
const char *const colon = xmemrchr(val, ':', varlen);
if (colon == NULL) {
*len = varlen;
*dir = val;
return NULL;
} else {
*dir = colon + 1;
*len = (size_t) (varend - colon);
return colon - 1;
}
}
/// Vim's version of getenv().
/// Special handling of $HOME, $VIM and $VIMRUNTIME, allowing the user to
/// override the vim runtime directory at runtime. Also does ACP to 'enc'
/// conversion for Win32. Result must be freed by the caller.
/// @param name Environment variable to expand
char *vim_getenv(const char *name)
{
const char *kos_env_path = os_getenv(name);
if (kos_env_path != NULL) {
return xstrdup(kos_env_path);
}
bool vimruntime = (strcmp(name, "VIMRUNTIME") == 0);
if (!vimruntime && strcmp(name, "VIM") != 0) {
return NULL;
}
// When expanding $VIMRUNTIME fails, try using $VIM/vim<version> or $VIM.
// Don't do this when default_vimruntime_dir is non-empty.
char *vim_path = NULL;
if (vimruntime
#ifdef HAVE_PATHDEF
&& *default_vimruntime_dir == NUL
#endif
) {
kos_env_path = os_getenv("VIM");
if (kos_env_path != NULL) {
vim_path = vim_version_dir(kos_env_path);
if (vim_path == NULL) {
vim_path = xstrdup(kos_env_path);
}
}
}
// When expanding $VIM or $VIMRUNTIME fails, try using:
// - the directory name from 'helpfile' (unless it contains '$')
// - the executable name from argv[0]
if (vim_path == NULL) {
if (p_hf != NULL && vim_strchr(p_hf, '$') == NULL) {
vim_path = (char *)p_hf;
}
#ifdef WIN32
// Find runtime path relative to the nvim binary i.e. ../share/runtime
if (vim_path == NULL) {
char exe_name[MAXPATHL];
size_t exe_name_len = MAXPATHL;
if (os_exepath(exe_name, &exe_name_len) == 0) {
char *path_end = (char *)path_tail_with_sep((char_u *)exe_name);
*path_end = '\0'; // remove the trailing "nvim.exe"
path_end = (char *)path_tail((char_u *)exe_name);
*path_end = '\0'; // remove the trailing "bin/"
if (append_path(
exe_name,
"share" _PATHSEPSTR "nvim" _PATHSEPSTR "runtime" _PATHSEPSTR,
MAXPATHL) == OK) {
vim_path = exe_name;
}
}
}
#endif
if (vim_path != NULL) {
// remove the file name
char *vim_path_end = (char *)path_tail((char_u *)vim_path);
// remove "doc/" from 'helpfile', if present
if (vim_path == (char *)p_hf) {
vim_path_end = remove_tail(vim_path, vim_path_end, "doc");
}
// for $VIM, remove "runtime/" or "vim54/", if present
if (!vimruntime) {
vim_path_end = remove_tail(vim_path, vim_path_end, RUNTIME_DIRNAME);
vim_path_end = remove_tail(vim_path, vim_path_end, VIM_VERSION_NODOT);
}
// remove trailing path separator
if (vim_path_end > vim_path && after_pathsep(vim_path, vim_path_end)) {
vim_path_end--;
}
// check that the result is a directory name
assert(vim_path_end >= vim_path);
vim_path = xstrndup(vim_path, (size_t)(vim_path_end - vim_path));
if (!os_isdir((char_u *)vim_path)) {
xfree(vim_path);
vim_path = NULL;
}
}
}
#ifdef HAVE_PATHDEF
// When there is a pathdef.c file we can use default_vim_dir and
// default_vimruntime_dir
if (vim_path == NULL) {
// Only use default_vimruntime_dir when it is not empty
if (vimruntime && *default_vimruntime_dir != NUL) {
vim_path = xstrdup(default_vimruntime_dir);
} else if (*default_vim_dir != NUL) {
if (vimruntime
&& (vim_path = vim_version_dir(default_vim_dir)) == NULL) {
vim_path = xstrdup(default_vim_dir);
}
}
}
#endif
// Set the environment variable, so that the new value can be found fast
// next time, and others can also use it (e.g. Perl).
if (vim_path != NULL) {
if (vimruntime) {
vim_setenv("VIMRUNTIME", vim_path);
didset_vimruntime = true;
} else {
vim_setenv("VIM", vim_path);
didset_vim = true;
}
}
return vim_path;
}
/// Replace home directory by "~" in each space or comma separated file name in
/// 'src'.
/// If anything fails (except when out of space) dst equals src.
/// @param buf When not NULL, check for help files
/// @param src Input file name
/// @param dst Where to put the result
/// @param dstlen Maximum length of the result
/// @param one If true, only replace one file name, including spaces and commas
/// in the file name
void home_replace(buf_T *buf, char_u *src, char_u *dst, int dstlen, bool one)
{
size_t dirlen = 0, envlen = 0;
size_t len;
if (src == NULL) {
*dst = NUL;
return;
}
/*
* If the file is a help file, remove the path completely.
*/
if (buf != NULL && buf->b_help) {
STRCPY(dst, path_tail(src));
return;
}
/*
* We check both the value of the $HOME environment variable and the
* "real" home directory.
*/
if (homedir != NULL)
dirlen = STRLEN(homedir);
char_u *homedir_env = (char_u *)os_getenv("HOME");
bool must_free = false;
if (homedir_env != NULL && vim_strchr(homedir_env, '~') != NULL) {
must_free = true;
size_t usedlen = 0;
size_t flen = STRLEN(homedir_env);
char_u *fbuf = NULL;
(void)modify_fname((char_u *)":p", &usedlen, &homedir_env, &fbuf, &flen);
flen = STRLEN(homedir_env);
if (flen > 0 && vim_ispathsep(homedir_env[flen - 1]))
/* Remove the trailing / that is added to a directory. */
homedir_env[flen - 1] = NUL;
}
if (homedir_env != NULL)
envlen = STRLEN(homedir_env);
if (!one)
src = skipwhite(src);
while (*src && dstlen > 0) {
/*
* Here we are at the beginning of a file name.
* First, check to see if the beginning of the file name matches
* $HOME or the "real" home directory. Check that there is a '/'
* after the match (so that if e.g. the file is "/home/pieter/bla",
* and the home directory is "/home/piet", the file does not end up
* as "~er/bla" (which would seem to indicate the file "bla" in user
* er's home directory)).
*/
char_u *p = homedir;
len = dirlen;
for (;; ) {
if ( len
&& fnamencmp(src, p, len) == 0
&& (vim_ispathsep(src[len])
|| (!one && (src[len] == ',' || src[len] == ' '))
|| src[len] == NUL)) {
src += len;
if (--dstlen > 0)
*dst++ = '~';
/*
* If it's just the home directory, add "/".
*/
if (!vim_ispathsep(src[0]) && --dstlen > 0)
*dst++ = '/';
break;
}
if (p == homedir_env)
break;
p = homedir_env;
len = envlen;
}
/* if (!one) skip to separator: space or comma */
while (*src && (one || (*src != ',' && *src != ' ')) && --dstlen > 0)
*dst++ = *src++;
/* skip separator */
while ((*src == ' ' || *src == ',') && --dstlen > 0)
*dst++ = *src++;
}
/* if (dstlen == 0) out of space, what to do??? */
*dst = NUL;
if (must_free) {
xfree(homedir_env);
}
}
/// Like home_replace, store the replaced string in allocated memory.
/// @param buf When not NULL, check for help files
/// @param src Input file name
char_u * home_replace_save(buf_T *buf, char_u *src) FUNC_ATTR_NONNULL_RET
{
size_t len = 3; // space for "~/" and trailing NUL
if (src != NULL) { // just in case
len += STRLEN(src);
}
char_u *dst = xmalloc(len);
home_replace(buf, src, dst, (int)len, true);
return dst;
}
/// Our portable version of setenv.
/// Has special handling for $VIMRUNTIME to keep the localization machinery
/// sane.
void vim_setenv(const char *name, const char *val)
{
os_setenv(name, val, 1);
#ifndef LOCALE_INSTALL_DIR
// When setting $VIMRUNTIME adjust the directory to find message
// translations to $VIMRUNTIME/lang.
if (*val != NUL && STRICMP(name, "VIMRUNTIME") == 0) {
char *buf = (char *)concat_str((char_u *)val, (char_u *)"/lang");
bindtextdomain(PROJECT_NAME, buf);
xfree(buf);
}
#endif
}
/// Function given to ExpandGeneric() to obtain an environment variable name.
char_u *get_env_name(expand_T *xp, int idx)
{
# define ENVNAMELEN 100
// this static buffer is needed to avoid a memory leak in ExpandGeneric
static char_u name[ENVNAMELEN];
assert(idx >= 0);
char *envname = os_getenvname_at_index((size_t)idx);
if (envname) {
STRLCPY(name, envname, ENVNAMELEN);
xfree(envname);
return name;
}
return NULL;
}