fish-shell/src/env.cpp

1224 lines
44 KiB
C++
Raw Normal View History

// Functions for setting and getting environment variables.
#include "config.h" // IWYU pragma: keep
#include <errno.h>
#include <pwd.h>
2015-07-25 15:14:25 +00:00
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
#include <algorithm>
#include <iterator>
#include <mutex>
#include <set>
2015-07-25 15:14:25 +00:00
#include <utility>
#include <vector>
#include "builtin_bind.h"
#include "common.h"
#include "env.h"
#include "env_dispatch.h"
#include "env_universal_common.h"
#include "event.h"
#include "fallback.h" // IWYU pragma: keep
#include "fish_version.h"
#include "history.h"
#include "input.h"
#include "path.h"
#include "proc.h"
#include "reader.h"
#include "wutil.h" // IWYU pragma: keep
#define DEFAULT_TERM1 "ansi"
#define DEFAULT_TERM2 "dumb"
/// Some configuration path environment variables.
#define FISH_DATADIR_VAR L"__fish_data_dir"
#define FISH_SYSCONFDIR_VAR L"__fish_sysconf_dir"
#define FISH_HELPDIR_VAR L"__fish_help_dir"
#define FISH_BIN_DIR L"__fish_bin_dir"
#define FISH_CONFIG_DIR L"__fish_config_dir"
#define FISH_USER_DATA_DIR L"__fish_user_data_dir"
/// At init, we read all the environment variables from this array.
extern char **environ;
/// The character used to delimit path and non-path variables in exporting and in string expansion.
static const wchar_t PATH_ARRAY_SEP = L':';
static const wchar_t NONPATH_ARRAY_SEP = L' ';
bool curses_initialized = false;
/// Does the terminal have the "eat_newline_glitch".
bool term_has_xn = false;
// Struct representing one level in the function variable stack.
// Only our variable stack should create and destroy these
class env_node_t {
public:
/// Variable table.
var_table_t env;
/// Does this node imply a new variable scope? If yes, all non-global variables below this one
/// in the stack are invisible. If new_scope is set for the global variable node, the universe
/// will explode.
2019-04-08 19:22:08 +00:00
const bool new_scope;
/// Does this node contain any variables which are exported to subshells
/// or does it redefine any variables to not be exported?
bool exportv = false;
/// Pointer to next level.
const std::shared_ptr<env_node_t> next;
env_node_t(bool is_new_scope, std::shared_ptr<env_node_t> next_scope)
: new_scope(is_new_scope), next(std::move(next_scope)) {}
2019-04-08 19:22:08 +00:00
maybe_t<env_var_t> find_entry(const wcstring &key) {
auto it = env.find(key);
if (it != env.end()) return it->second;
return none();
}
};
using env_node_ref_t = std::shared_ptr<env_node_t>;
// This is a big dorky lock we take around everything that might modify an env_node_t. Fine grained
// locking is annoying here because env_nodes may be shared between env_stacks, so each node would
// need its own lock.
static std::mutex env_lock;
// A class wrapping up a variable stack
// Currently there is only one variable stack in fish,
// but we can imagine having separate (linked) stacks
// if we introduce multiple threads of execution
struct var_stack_t {
var_stack_t(var_stack_t &&) = default;
// Top node on the function stack.
env_node_ref_t top;
// Bottom node on the function stack.
const env_node_ref_t global_env;
// Exported variable array used by execv.
maybe_t<null_terminated_array_t<char>> export_array;
/// Flag for checking if we need to regenerate the exported variable array.
void mark_changed_exported() { export_array.reset(); }
bool has_changed_exported() const { return !export_array; }
void update_export_array_if_necessary();
2018-09-11 01:59:57 +00:00
var_stack_t() : top(globals()), global_env(globals()) {
// Add a toplevel local scope on top of the global scope. This local scope will persist
// throughout the lifetime of the fish process, and it will ensure that `set -l` commands
// run at the command-line don't affect the global scope.
push(false);
}
// Pushes a new node onto our stack
// Optionally creates a new scope for the node
2019-03-25 07:49:52 +00:00
void push(bool new_scope) {
auto node = std::make_shared<env_node_t>(new_scope, this->top);
2019-03-25 07:49:52 +00:00
// Copy local-exported variables.
auto top_node = top;
// Only if we introduce a new shadowing scope; i.e. not if it's just `begin; end` or
// "--no-scope-shadowing".
if (new_scope && top_node != this->global_env) {
for (const auto &var : top_node->env) {
if (var.second.exports()) node->env.insert(var);
}
}
this->top = std::move(node);
2019-03-25 07:49:52 +00:00
if (new_scope && local_scope_exports(this->top)) {
this->mark_changed_exported();
}
}
// Pops the top node, asserting it's not global.
// \return the popped node.
env_node_ref_t pop() {
assert(top != this->global_env && "Cannot pop global node");
2019-03-25 07:49:52 +00:00
env_node_ref_t old_top = this->top;
this->top = old_top->next;
return old_top;
2019-03-25 07:49:52 +00:00
}
// Returns the next scope to search for a given node, respecting the new_scope flag.
// Returns an empty pointer if we're done.
2019-03-25 07:49:52 +00:00
env_node_ref_t next_scope_to_search(const env_node_ref_t &node) const {
assert(node != NULL);
if (node == this->global_env) {
return nullptr;
}
return node->new_scope ? this->global_env : node->next;
}
// Returns the scope used for unspecified scopes. An unspecified scope is either the topmost
// shadowing scope, or the global scope if none. This implements the default behavior of `set`.
2019-03-25 07:49:52 +00:00
env_node_ref_t resolve_unspecified_scope() {
env_node_ref_t node = this->top;
while (node && !node->new_scope) {
node = node->next;
}
return node ? node : this->global_env;
}
/// Copy this vars_stack.
var_stack_t clone() const {
return var_stack_t(*this);
}
/// return true if the topomst local scope exports a variable.
bool local_scope_exports(const env_node_ref_t &n) const;
private:
/// Copy constructor. This does not copy the export array; it just allows it to be regenerated.
var_stack_t(const var_stack_t &rhs) : top(rhs.top), global_env(rhs.global_env) {}
void get_exported(const env_node_t *n, var_table_t &h) const;
/// Returns the global variable set.
2019-03-25 07:49:52 +00:00
static env_node_ref_t globals() {
static env_node_ref_t s_globals{std::make_shared<env_node_t>(false, nullptr)};
2019-03-25 07:49:52 +00:00
return s_globals;
}
2019-03-25 07:49:52 +00:00
};
/// Universal variables global instance. Initialized in env_init.
static env_universal_t *s_universal_variables = NULL;
/// Getter for universal variables.
static env_universal_t *uvars() { return s_universal_variables; }
// A typedef for a set of constant strings. Note our sets are typically on the order of 6 elements,
// so we don't bother to sort them.
using string_set_t = const wchar_t *const[];
/// Check if a variable may not be set using the set command.
2019-04-08 20:56:00 +00:00
static bool is_read_only(const wcstring &key) {
static const string_set_t env_read_only = {
L"PWD", L"SHLVL", L"history", L"pipestatus", L"status", L"version",
2019-02-25 05:45:59 +00:00
L"FISH_VERSION", L"fish_pid", L"hostname", L"_", L"fish_private_mode"};
2019-04-08 20:56:00 +00:00
return contains(env_read_only, key) || (in_private_mode() && key == L"fish_history");
}
/// Return true if a variable should become a path variable by default. See #436.
static bool variable_should_auto_pathvar(const wcstring &name) {
return string_suffixes_string(L"PATH", name);
}
/// Table of variables whose value is dynamically calculated, such as umask, status, etc.
2019-02-25 05:45:59 +00:00
static const string_set_t env_electric = {L"history", L"pipestatus", L"status", L"umask"};
static bool is_electric(const wcstring &key) { return contains(env_electric, key); }
env_scoped_t::env_scoped_t() : env_scoped_t(make_unique<var_stack_t>()) {}
env_scoped_t::env_scoped_t(std::unique_ptr<var_stack_t> vars) : vars_(std::move(vars)) {}
env_scoped_t::env_scoped_t(env_scoped_t &&) = default;
env_scoped_t::~env_scoped_t() = default;
2019-03-25 07:49:52 +00:00
void env_stack_t::universal_barrier() {
ASSERT_IS_MAIN_THREAD();
if (!uvars()) return;
callback_data_list_t callbacks;
bool changed = uvars()->sync(callbacks);
if (changed) {
universal_notifier_t::default_notifier().post_notification();
}
env_universal_callbacks(this, callbacks);
}
2019-03-25 07:49:52 +00:00
// Get the variable stack
var_stack_t &env_scoped_t::vars_stack() { return *vars_; }
2019-03-25 07:49:52 +00:00
const var_stack_t &env_scoped_t::vars_stack() const { return *vars_; }
2019-03-25 07:49:52 +00:00
/// Return the current umask value.
static mode_t get_umask() {
mode_t res;
res = umask(0);
umask(res);
return res;
}
/// Some env vars contain a list of paths where an empty path element is equivalent to ".".
/// Unfortunately that convention causes problems for fish scripts. So this function replaces the
/// empty path element with an explicit ".". See issue #3914.
void fix_colon_delimited_var(const wcstring &var_name, env_stack_t &vars) {
const auto paths = vars.get(var_name);
if (paths.missing_or_empty()) return;
// See if there's any empties.
const wcstring empty = wcstring();
const wcstring_list_t &strs = paths->as_list();
if (contains(strs, empty)) {
// Copy the list and replace empties with L"."
wcstring_list_t newstrs = strs;
std::replace(newstrs.begin(), newstrs.end(), empty, wcstring(L"."));
int retval = vars.set(var_name, ENV_DEFAULT | ENV_USER, std::move(newstrs));
if (retval != ENV_OK) {
debug(0, L"fix_colon_delimited_var failed unexpectedly with retval %d", retval);
}
}
}
/// Ensure the content of the magic path env vars is reasonable. Specifically, that empty path
/// elements are converted to explicit "." to make the vars easier to use in fish scripts.
static void init_path_vars() {
// Do not replace empties in MATHPATH - see #4158.
fix_colon_delimited_var(L"PATH", env_stack_t::globals());
fix_colon_delimited_var(L"CDPATH", env_stack_t::globals());
}
/// Make sure the PATH variable contains something.
static void setup_path() {
auto &vars = env_stack_t::globals();
const auto path = vars.get(L"PATH");
if (path.missing_or_empty()) {
#if defined(_CS_PATH)
// _CS_PATH: colon-separated paths to find POSIX utilities
std::string cspath;
cspath.resize(confstr(_CS_PATH, nullptr, 0));
confstr(_CS_PATH, &cspath[0], cspath.length());
#else
std::string cspath = "/usr/bin:/bin"; // I doubt this is even necessary
#endif
vars.set_one(L"PATH", ENV_GLOBAL | ENV_EXPORT, str2wcstring(cspath));
}
}
/// If they don't already exist initialize the `COLUMNS` and `LINES` env vars to reasonable
/// defaults. They will be updated later by the `get_current_winsize()` function if they need to be
/// adjusted.
2018-09-11 02:17:44 +00:00
void env_stack_t::set_termsize() {
auto &vars = env_stack_t::globals();
2018-09-11 02:17:44 +00:00
auto cols = get(L"COLUMNS");
if (cols.missing_or_empty()) vars.set_one(L"COLUMNS", ENV_GLOBAL, DFLT_TERM_COL_STR);
2018-09-11 02:17:44 +00:00
auto rows = get(L"LINES");
if (rows.missing_or_empty()) vars.set_one(L"LINES", ENV_GLOBAL, DFLT_TERM_ROW_STR);
}
/// Update the PWD variable directory from the result of getcwd().
void env_stack_t::set_pwd_from_getcwd() {
wcstring cwd = wgetcwd();
if (cwd.empty()) {
debug(0,
_(L"Could not determine current working directory. Is your locale set correctly?"));
return;
}
set_one(L"PWD", ENV_EXPORT | ENV_GLOBAL, cwd);
}
void env_stack_t::mark_changed_exported() { vars_stack().mark_changed_exported(); }
wcstring environment_t::get_pwd_slash() const {
// Return "/" if PWD is missing.
// See https://github.com/fish-shell/fish-shell/issues/5080
auto pwd_var = get(L"PWD");
wcstring pwd;
if (!pwd_var.missing_or_empty()) {
pwd = pwd_var->as_string();
}
if (!string_suffixes_string(L"/", pwd)) {
pwd.push_back(L'/');
}
return pwd;
}
/// Set up the USER variable.
static void setup_user(bool force) {
auto &vars = env_stack_t::globals();
if (force || vars.get(L"USER").missing_or_empty()) {
2017-05-18 06:07:47 +00:00
struct passwd userinfo;
struct passwd *result;
char buf[8192];
int retval = getpwuid_r(getuid(), &userinfo, buf, sizeof(buf), &result);
if (!retval && result) {
const wcstring uname = str2wcstring(userinfo.pw_name);
vars.set_one(L"USER", ENV_GLOBAL | ENV_EXPORT, uname);
}
}
}
/// Various things we need to initialize at run-time that don't really fit any of the other init
/// routines.
void misc_init() {
// If stdout is open on a tty ensure stdio is unbuffered. That's because those functions might
// be intermixed with `write()` calls and we need to ensure the writes are not reordered. See
// issue #3748.
if (isatty(STDOUT_FILENO)) {
fflush(stdout);
setvbuf(stdout, NULL, _IONBF, 0);
}
}
void env_init(const struct config_paths_t *paths /* or NULL */) {
env_stack_t &vars = env_stack_t::globals();
// Import environment variables. Walk backwards so that the first one out of any duplicates wins
// (See issue #2784).
wcstring key, val;
const char *const *envp = environ;
size_t i = 0;
while (envp && envp[i]) i++;
while (i--) {
const wcstring key_and_val = str2wcstring(envp[i]); // like foo=bar
2012-12-19 21:31:06 +00:00
size_t eql = key_and_val.find(L'=');
if (eql == wcstring::npos) {
// No equal-sign found so treat it as a defined var that has no value(s).
if (is_read_only(key_and_val) || is_electric(key_and_val)) continue;
2018-09-11 04:27:25 +00:00
vars.set_empty(key_and_val, ENV_EXPORT | ENV_GLOBAL);
} else {
key.assign(key_and_val, 0, eql);
val.assign(key_and_val, eql+1, wcstring::npos);
if (is_read_only(key) || is_electric(key)) continue;
vars.set(key, ENV_EXPORT | ENV_GLOBAL, {val});
}
}
// Set the given paths in the environment, if we have any.
if (paths != NULL) {
vars.set_one(FISH_DATADIR_VAR, ENV_GLOBAL, paths->data);
vars.set_one(FISH_SYSCONFDIR_VAR, ENV_GLOBAL, paths->sysconf);
vars.set_one(FISH_HELPDIR_VAR, ENV_GLOBAL, paths->doc);
vars.set_one(FISH_BIN_DIR, ENV_GLOBAL, paths->bin);
}
wcstring user_config_dir;
path_get_config(user_config_dir);
vars.set_one(FISH_CONFIG_DIR, ENV_GLOBAL, user_config_dir);
wcstring user_data_dir;
path_get_data(user_data_dir);
vars.set_one(FISH_USER_DATA_DIR, ENV_GLOBAL, user_data_dir);
init_path_vars();
// Set up the USER and PATH variables
setup_path();
// Some `su`s keep $USER when changing to root.
// This leads to issues later on (and e.g. in prompts),
// so we work around it by resetting $USER.
// TODO: Figure out if that su actually checks if username == "root"(as the man page says) or
// UID == 0.
uid_t uid = getuid();
setup_user(uid == 0);
// Set up $IFS - this used to be in share/config.fish, but really breaks if it isn't done.
vars.set_one(L"IFS", ENV_GLOBAL, L"\n \t");
// Set up the version variable.
wcstring version = str2wcstring(get_fish_version());
vars.set_one(L"version", ENV_GLOBAL, version);
vars.set_one(L"FISH_VERSION", ENV_GLOBAL, version);
// Set the $fish_pid variable.
vars.set_one(L"fish_pid", ENV_GLOBAL, to_string(getpid()));
// Set the $hostname variable
wcstring hostname = L"fish";
get_hostname_identifier(hostname);
vars.set_one(L"hostname", ENV_GLOBAL, hostname);
// Set up SHLVL variable. Not we can't use vars.get() because SHLVL is read-only, and therefore
// was not inherited from the environment.
2012-05-09 10:23:31 +00:00
wcstring nshlvl_str = L"1";
if (const char *shlvl_var = getenv("SHLVL")) {
const wchar_t *end;
// TODO: Figure out how to handle invalid numbers better. Shouldn't we issue a diagnostic?
long shlvl_i = fish_wcstol(str2wcstring(shlvl_var).c_str(), &end);
if (!errno && shlvl_i >= 0) {
nshlvl_str = to_string(shlvl_i + 1);
2012-05-09 10:23:31 +00:00
}
}
vars.set_one(L"SHLVL", ENV_GLOBAL | ENV_EXPORT, nshlvl_str);
// Set up the HOME variable.
// Unlike $USER, it doesn't seem that `su`s pass this along
// if the target user is root, unless "--preserve-environment" is used.
// Since that is an explicit choice, we should allow it to enable e.g.
// env HOME=(mktemp -d) su --preserve-environment fish
if (vars.get(L"HOME").missing_or_empty()) {
auto user_var = vars.get(L"USER");
if (!user_var.missing_or_empty()) {
char *unam_narrow = wcs2str(user_var->as_string());
struct passwd userinfo;
struct passwd *result;
char buf[8192];
int retval = getpwnam_r(unam_narrow, &userinfo, buf, sizeof(buf), &result);
if (retval || !result) {
// Maybe USER is set but it's bogus. Reset USER from the db and try again.
setup_user(true);
user_var = vars.get(L"USER");
if (!user_var.missing_or_empty()) {
unam_narrow = wcs2str(user_var->as_string());
retval = getpwnam_r(unam_narrow, &userinfo, buf, sizeof(buf), &result);
}
}
if (!retval && result && userinfo.pw_dir) {
const wcstring dir = str2wcstring(userinfo.pw_dir);
vars.set_one(L"HOME", ENV_GLOBAL | ENV_EXPORT, dir);
} else {
// We cannot get $HOME. This triggers warnings for history and config.fish already,
// so it isn't necessary to warn here as well.
2018-09-11 04:27:25 +00:00
vars.set_empty(L"HOME", ENV_GLOBAL | ENV_EXPORT);
}
free(unam_narrow);
} else {
// If $USER is empty as well (which we tried to set above), we can't get $HOME.
2018-09-11 04:27:25 +00:00
vars.set_empty(L"HOME", ENV_GLOBAL | ENV_EXPORT);
}
}
// initialize the PWD variable if necessary
// Note we may inherit a virtual PWD that doesn't match what getcwd would return; respect that
// if and only if it matches getcwd (#5647). Note we treat PWD as read-only so it was not set in
// vars.
const char *incoming_pwd_cstr = getenv("PWD");
wcstring incoming_pwd = incoming_pwd_cstr ? str2wcstring(incoming_pwd_cstr) : wcstring{};
if (!incoming_pwd.empty() && paths_are_same_file(incoming_pwd, L".")) {
vars.set_one(L"PWD", ENV_EXPORT | ENV_GLOBAL, incoming_pwd);
} else {
2018-09-11 02:17:44 +00:00
vars.set_pwd_from_getcwd();
}
2018-09-11 02:17:44 +00:00
vars.set_termsize(); // initialize the terminal size variables
// Set fish_bind_mode to "default".
2018-09-11 02:17:44 +00:00
vars.set_one(FISH_BIND_MODE_VAR, ENV_GLOBAL, DEFAULT_BIND_MODE);
// Allow changes to variables to produce events.
env_dispatch_init(vars);
init_input();
// Set up universal variables. The empty string means to use the default path.
assert(s_universal_variables == NULL);
s_universal_variables = new env_universal_t(L"");
callback_data_list_t callbacks;
s_universal_variables->initialize(callbacks);
env_universal_callbacks(&env_stack_t::principal(), callbacks);
}
/// Search all visible scopes in order for the specified key. Return the first scope in which it was
/// found.
env_node_ref_t env_stack_t::get_node(const wcstring &key) {
env_node_ref_t env = vars_stack().top;
while (env) {
if (env->find_entry(key)) break;
env = vars_stack().next_scope_to_search(env);
}
return env;
}
static int set_umask(const wcstring_list_t &list_val) {
long mask = -1;
if (list_val.size() == 1 && !list_val.front().empty()) {
mask = fish_wcstol(list_val.front().c_str(), NULL, 8);
}
if (errno || mask > 0777 || mask < 0) return ENV_INVALID;
// Do not actually create a umask variable. On env_stack_t::get() it will be calculated.
umask(mask);
return ENV_OK;
}
/// Set a universal variable, inheriting as applicable from the given old variable.
static void env_set_internal_universal(const wcstring &key, wcstring_list_t val,
env_mode_flags_t input_var_mode, env_stack_t *stack) {
ASSERT_IS_MAIN_THREAD();
if (!uvars()) return;
env_mode_flags_t var_mode = input_var_mode;
auto oldvar = uvars()->get(key);
// Resolve whether or not to export.
if ((var_mode & (ENV_EXPORT | ENV_UNEXPORT)) == 0) {
bool doexport = oldvar ? oldvar->exports() : false;
var_mode |= (doexport ? ENV_EXPORT : ENV_UNEXPORT);
}
// Resolve whether to be a path variable.
// Here we fall back to the auto-pathvar behavior.
if ((var_mode & (ENV_PATHVAR | ENV_UNPATHVAR)) == 0) {
bool dopathvar = oldvar ? oldvar->is_pathvar() : variable_should_auto_pathvar(key);
var_mode |= (dopathvar ? ENV_PATHVAR : ENV_UNPATHVAR);
}
// Split about ':' if it's a path variable.
if (var_mode & ENV_PATHVAR) {
wcstring_list_t split_val;
for (const wcstring &str : val) {
vec_append(split_val, split_string(str, PATH_ARRAY_SEP));
}
val = std::move(split_val);
}
// Construct and set the new variable.
env_var_t::env_var_flags_t varflags = 0;
if (var_mode & ENV_EXPORT) varflags |= env_var_t::flag_export;
if (var_mode & ENV_PATHVAR) varflags |= env_var_t::flag_pathvar;
env_var_t new_var{val, varflags};
uvars()->set(key, new_var);
env_universal_barrier();
if (new_var.exports() || (oldvar && oldvar->exports())) {
stack->mark_changed_exported();
}
}
/// Set the value of the environment variable whose name matches key to val.
///
/// \param key The key
/// \param val The value to set.
/// \param var_mode The type of the variable. Can be any combination of ENV_GLOBAL, ENV_LOCAL,
/// ENV_EXPORT and ENV_USER. If mode is ENV_DEFAULT, the current variable space is searched and the
/// current mode is used. If no current variable with the same name is found, ENV_LOCAL is assumed.
///
/// Returns:
///
/// * ENV_OK on success.
/// * ENV_PERM, can only be returned when setting as a user, e.g. ENV_USER is set. This means that
/// the user tried to change a read-only variable.
/// * ENV_SCOPE, the variable cannot be set in the given scope. This applies to readonly/electric
/// variables set from the local or universal scopes, or set as exported.
/// * ENV_INVALID, the variable value was invalid. This applies only to special variables.
int env_stack_t::set_internal(const wcstring &key, env_mode_flags_t input_var_mode, wcstring_list_t val) {
ASSERT_IS_MAIN_THREAD();
env_mode_flags_t var_mode = input_var_mode;
bool has_changed_old = vars_stack().has_changed_exported();
int done = 0;
2013-01-21 02:34:18 +00:00
if (val.size() == 1 && (key == L"PWD" || key == L"HOME")) {
// Canonicalize our path; if it changes, recurse and try again.
wcstring val_canonical = val.front();
2012-02-08 05:23:12 +00:00
path_make_canonical(val_canonical);
if (val.front() != val_canonical) {
return set_internal(key, var_mode, {val_canonical});
2012-02-08 05:23:12 +00:00
}
}
2013-01-21 02:34:18 +00:00
if ((var_mode & ENV_LOCAL || var_mode & ENV_UNIVERSAL) &&
(is_read_only(key) || is_electric(key))) {
return ENV_SCOPE;
}
if ((var_mode & ENV_EXPORT) && is_electric(key)) {
return ENV_SCOPE;
}
if ((var_mode & ENV_USER) && is_read_only(key)) {
return ENV_PERM;
}
2013-01-21 02:34:18 +00:00
if (key == L"umask") { // set new umask
return set_umask(val);
}
2013-01-21 02:34:18 +00:00
if (var_mode & ENV_UNIVERSAL) {
if (uvars()) {
env_set_internal_universal(key, std::move(val), var_mode, this);
}
} else {
scoped_lock locker(env_lock);
// Determine the node.
bool has_changed_new = false;
env_node_ref_t preexisting_node = get_node(key);
maybe_t<env_var_t::env_var_flags_t> preexisting_flags{};
if (preexisting_node != nullptr) {
var_table_t::const_iterator result = preexisting_node->env.find(key);
assert(result != preexisting_node->env.end());
preexisting_flags = result->second.get_flags();
if (*preexisting_flags & env_var_t::flag_export) {
has_changed_new = true;
}
}
2013-01-21 02:34:18 +00:00
env_node_ref_t node = nullptr;
if (var_mode & ENV_GLOBAL) {
node = vars_stack().global_env;
} else if (var_mode & ENV_LOCAL) {
node = vars_stack().top;
} else if (preexisting_node != nullptr) {
node = std::move(preexisting_node);
if ((var_mode & (ENV_EXPORT | ENV_UNEXPORT)) == 0) {
// Use existing entry's exportv status.
if (preexisting_flags && (*preexisting_flags & env_var_t::flag_export)) {
var_mode |= ENV_EXPORT;
}
}
} else {
if (uvars() && uvars()->get(key)) {
// Modifying an existing universal variable.
env_set_internal_universal(key, std::move(val), var_mode, this);
done = true;
} else {
// New variable with unspecified scope
node = vars_stack().resolve_unspecified_scope();
}
}
2013-01-21 02:34:18 +00:00
if (!done) {
// Resolve if we should mark ourselves as a path variable or not.
// If there's an existing variable, use its path flag; otherwise infer it.
if ((var_mode & (ENV_PATHVAR | ENV_UNPATHVAR)) == 0) {
bool should_pathvar = false;
if (preexisting_flags) {
should_pathvar = *preexisting_flags & env_var_t::flag_pathvar;
} else {
should_pathvar = variable_should_auto_pathvar(key);
}
var_mode |= should_pathvar ? ENV_PATHVAR : ENV_UNPATHVAR;
}
// Split about ':' if it's a path variable.
if (var_mode & ENV_PATHVAR) {
wcstring_list_t split_val;
for (const wcstring &str : val) {
vec_append(split_val, split_string(str, PATH_ARRAY_SEP));
}
val = std::move(split_val);
}
// Set the entry in the node. Note that operator[] accesses the existing entry, or
// creates a new one.
env_var_t &var = node->env[key];
if (var.exports()) {
// This variable already existed, and was exported.
has_changed_new = true;
}
var = var.setting_vals(std::move(val))
.setting_exports(var_mode & ENV_EXPORT)
.setting_pathvar(var_mode & ENV_PATHVAR)
.setting_read_only(is_read_only(key));
if (var_mode & ENV_EXPORT) {
// The new variable is exported.
node->exportv = true;
has_changed_new = true;
} else {
// Set the node's exported when it changes something about exports
// (also when it redefines a variable to not be exported).
node->exportv = has_changed_old != has_changed_new;
}
2013-01-21 02:34:18 +00:00
if (has_changed_old || has_changed_new) vars_stack().mark_changed_exported();
}
}
2013-01-21 02:34:18 +00:00
event_fire(event_t::variable(key, {L"VARIABLE", L"SET", key}));
env_dispatch_var_change(key, *this);
return ENV_OK;
}
/// Sets the variable with the specified name to the given values.
int env_stack_t::set(const wcstring &key, env_mode_flags_t mode, wcstring_list_t vals) {
return set_internal(key, mode, std::move(vals));
}
/// Sets the variable with the specified name to a single value.
int env_stack_t::set_one(const wcstring &key, env_mode_flags_t mode, wcstring val) {
wcstring_list_t vals;
vals.push_back(std::move(val));
return set_internal(key, mode, std::move(vals));
}
/// Sets the variable with the specified name without any (i.e., zero) values.
int env_stack_t::set_empty(const wcstring &key, env_mode_flags_t mode) {
return set_internal(key, mode, {});
}
/// Attempt to remove/free the specified key/value pair from the specified map.
///
/// \return zero if the variable was not found, non-zero otherwise
bool env_stack_t::try_remove(env_node_ref_t n, const wchar_t *key, int var_mode) {
if (n == nullptr) {
return false;
}
var_table_t::iterator result = n->env.find(key);
if (result != n->env.end()) {
if (result->second.exports()) {
mark_changed_exported();
}
n->env.erase(result);
return true;
}
if (var_mode & ENV_LOCAL) {
return false;
}
if (n->new_scope) {
return try_remove(vars_stack().global_env, key, var_mode);
}
return try_remove(n->next, key, var_mode);
}
int env_stack_t::remove(const wcstring &key, int var_mode) {
ASSERT_IS_MAIN_THREAD();
env_node_ref_t first_node{};
int erased = 0;
if ((var_mode & ENV_USER) && is_read_only(key)) {
return ENV_SCOPE;
}
first_node = vars_stack().top;
if (!(var_mode & ENV_UNIVERSAL)) {
if (var_mode & ENV_GLOBAL) {
first_node = vars_stack().global_env;
}
if (try_remove(first_node, key.c_str(), var_mode)) {
event_fire(event_t::variable(key, {L"VARIABLE", L"ERASE", key}));
erased = 1;
}
}
if (!erased && !(var_mode & ENV_GLOBAL) && !(var_mode & ENV_LOCAL)) {
bool is_exported = false;
if (uvars()) {
if (auto old_flags = uvars()->get_flags(key)) {
is_exported = *old_flags & env_var_t::flag_export;
}
erased = uvars()->remove(key);
}
if (erased) {
env_universal_barrier();
event_fire(event_t::variable(key, {L"VARIABLE", L"ERASE", key}));
}
if (is_exported) vars_stack().mark_changed_exported();
}
env_dispatch_var_change(key, *this);
return erased ? ENV_OK : ENV_NOT_FOUND;
}
const wcstring_list_t &env_var_t::as_list() const { return *vals_; }
wchar_t env_var_t::get_delimiter() const {
return is_pathvar() ? PATH_ARRAY_SEP : NONPATH_ARRAY_SEP;
}
/// Return a string representation of the var.
wcstring env_var_t::as_string() const { return join_strings(*vals_, get_delimiter()); }
void env_var_t::to_list(wcstring_list_t &out) const { out = *vals_; }
env_var_t::env_var_flags_t env_var_t::flags_for(const wchar_t *name) {
env_var_flags_t result = 0;
if (is_read_only(name)) result |= flag_read_only;
return result;
}
/// \return a singleton empty list, to avoid unnecessary allocations in env_var_t.
std::shared_ptr<const wcstring_list_t> env_var_t::empty_list() {
static const auto result = std::make_shared<const wcstring_list_t>();
return result;
}
/// \return a the value of a variable for \p key, which must be electric (computed).
static maybe_t<env_var_t> get_electric(const wcstring &key, const environment_t &vars) {
if (key == L"history") {
// Big hack. We only allow getting the history on the main thread. Note that history_t
// may ask for an environment variable, so don't take the lock here (we don't need it).
if (!is_main_thread()) {
return none();
}
history_t *history = reader_get_history();
if (!history) {
history = &history_t::history_with_name(history_session_id(vars));
}
wcstring_list_t result;
if (history) history->get_history(result);
return env_var_t(L"history", result);
} else if (key == L"pipestatus") {
const auto js = proc_get_last_statuses();
wcstring_list_t result;
result.reserve(js.pipestatus.size());
for (int i : js.pipestatus) {
result.push_back(to_string(i));
}
return env_var_t(L"pipestatus", std::move(result));
} else if (key == L"status") {
return env_var_t(L"status", to_string(proc_get_last_status()));
} else if (key == L"umask") {
return env_var_t(L"umask", format_string(L"0%0.3o", get_umask()));
}
// We should never get here unless the electric var list is out of sync with the above code.
DIE("unrecognized electric var name");
}
maybe_t<env_var_t> env_scoped_t::get(const wcstring &key, env_mode_flags_t mode) const {
const bool has_scope = mode & (ENV_LOCAL | ENV_GLOBAL | ENV_UNIVERSAL);
const bool search_local = !has_scope || (mode & ENV_LOCAL);
const bool search_global = !has_scope || (mode & ENV_GLOBAL);
const bool search_universal = !has_scope || (mode & ENV_UNIVERSAL);
const bool search_exported = (mode & ENV_EXPORT) || !(mode & ENV_UNEXPORT);
const bool search_unexported = (mode & ENV_UNEXPORT) || !(mode & ENV_EXPORT);
// Make the assumption that electric keys can't be shadowed elsewhere, since we currently block
2018-09-25 03:59:55 +00:00
// that in env_stack_t::set().
if (is_electric(key)) {
if (!search_global) return none();
return get_electric(key, *this);
}
if (search_local || search_global) {
scoped_lock locker(env_lock); // lock around a local region
env_node_ref_t env = search_local ? vars_stack().top : vars_stack().global_env;
while (env != NULL) {
if (env == vars_stack().global_env && !search_global) {
break;
}
var_table_t::const_iterator result = env->env.find(key);
if (result != env->env.end()) {
const env_var_t &var = result->second;
if (var.exports() ? search_exported : search_unexported) {
return var;
}
}
env = vars_stack().next_scope_to_search(env);
}
}
// Okay, we couldn't find a local or global var given the requirements. If there is a matching
// universal var return that.
if (search_universal && uvars()) {
auto var = uvars()->get(key);
if (var && (var->exports() ? search_exported : search_unexported)) {
return var;
}
}
return none();
}
void env_universal_barrier() {env_stack_t::principal().universal_barrier(); }
/// Returns true if the specified scope or any non-shadowed non-global subscopes contain an exported
/// variable.
bool var_stack_t::local_scope_exports(const env_node_ref_t &n) const {
assert(n != nullptr);
if (n == global_env) return false;
if (n->exportv) return true;
if (n->new_scope) return false;
return local_scope_exports(n->next);
}
void env_stack_t::push(bool new_scope) { vars_stack().push(new_scope); }
void env_stack_t::pop() {
auto &vars = vars_stack();
auto old_node = vars.pop();
// Maybe exported variables have changed.
if (old_node->exportv) {
// This node exported or unexported a variable.
vars.mark_changed_exported();
} else if (old_node->new_scope && vars.local_scope_exports(old_node->next)) {
// This node was a local scope, so it shadowed exports from its parent.
vars.mark_changed_exported();
}
// TODO: we would like to coalesce locale / curses changes, so that we only re-initialize once.
for (const auto &kv : old_node->env) {
env_dispatch_var_change(kv.first, *this);
}
}
/// Function used with to insert keys of one table into a set::set<wcstring>.
static void add_key_to_string_set(const var_table_t &envs, std::set<wcstring> *str_set,
bool show_exported, bool show_unexported) {
var_table_t::const_iterator iter;
for (iter = envs.begin(); iter != envs.end(); ++iter) {
const env_var_t &var = iter->second;
if ((var.exports() && show_exported) || (!var.exports() && show_unexported)) {
// Insert this key.
str_set->insert(iter->first);
}
}
}
wcstring_list_t env_scoped_t::get_names(int flags) const {
scoped_lock locker(env_lock);
wcstring_list_t result;
std::set<wcstring> names;
int show_local = flags & ENV_LOCAL;
int show_global = flags & ENV_GLOBAL;
int show_universal = flags & ENV_UNIVERSAL;
env_node_ref_t n = vars_stack().top;
const bool show_exported = (flags & ENV_EXPORT) || !(flags & ENV_UNEXPORT);
const bool show_unexported = (flags & ENV_UNEXPORT) || !(flags & ENV_EXPORT);
if (!show_local && !show_global && !show_universal) {
show_local = show_universal = show_global = 1;
}
if (show_local) {
while (n) {
if (n == vars_stack().global_env) break;
add_key_to_string_set(n->env, &names, show_exported, show_unexported);
if (n->new_scope)
break;
else
n = n->next;
}
}
if (show_global) {
add_key_to_string_set(vars_stack().global_env->env, &names, show_exported, show_unexported);
if (show_unexported) {
result.insert(result.end(), std::begin(env_electric), std::end(env_electric));
}
}
if (show_universal && uvars()) {
const wcstring_list_t uni_list = uvars()->get_names(show_exported, show_unexported);
names.insert(uni_list.begin(), uni_list.end());
}
result.insert(result.end(), names.begin(), names.end());
return result;
}
/// Get list of all exported variables.
void var_stack_t::get_exported(const env_node_t *n, var_table_t &h) const {
if (!n) return;
if (n->new_scope) {
get_exported(global_env.get(), h);
} else {
get_exported(n->next.get(), h);
}
var_table_t::const_iterator iter;
for (iter = n->env.begin(); iter != n->env.end(); ++iter) {
const wcstring &key = iter->first;
const env_var_t var = iter->second;
if (var.exports()) {
// Export the variable. Don't use std::map::insert here, since we need to overwrite
// existing values from previous scopes.
h[key] = var;
} else {
// We need to erase from the map if we are not exporting, since a lower scope may have
// exported. See #2132.
h.erase(key);
}
}
}
// Given a map from key to value, return a vector of strings of the form key=value
static std::vector<std::string> get_export_list(const var_table_t &envs) {
std::vector<std::string> result;
result.reserve(envs.size());
for (const auto &kv : envs) {
std::string ks = wcs2string(kv.first);
std::string vs = wcs2string(kv.second.as_string());
// Create and append a string of the form ks=vs
std::string str;
2013-02-12 08:14:27 +00:00
str.reserve(ks.size() + 1 + vs.size());
str.append(ks);
str.append("=");
str.append(vs);
result.push_back(std::move(str));
}
return result;
}
void var_stack_t::update_export_array_if_necessary() {
if (!this->has_changed_exported()) {
return;
}
debug(4, L"export_arr() recalc");
var_table_t vals;
get_exported(this->top.get(), vals);
if (uvars()) {
const wcstring_list_t uni = uvars()->get_names(true, false);
for (const wcstring &key : uni) {
auto var = uvars()->get(key);
if (!var.missing_or_empty()) {
// Note that std::map::insert does NOT overwrite a value already in the map,
// which we depend on here.
vals.insert(std::pair<wcstring, env_var_t>(key, *var));
}
}
}
export_array.emplace(get_export_list(vals));
}
const char *const *env_stack_t::export_arr() {
ASSERT_IS_MAIN_THREAD();
ASSERT_IS_NOT_FORKED_CHILD();
vars_stack().update_export_array_if_necessary();
assert(vars_stack().export_array && "Should have export array");
return vars_stack().export_array->get();
}
void env_stack_t::set_argv(const wchar_t *const *argv) {
if (argv && *argv) {
wcstring_list_t list;
for (auto arg = argv; *arg; arg++) {
2018-09-11 04:27:25 +00:00
list.emplace_back(*arg);
}
2018-09-11 04:27:25 +00:00
set(L"argv", ENV_LOCAL, std::move(list));
} else {
2018-09-11 04:27:25 +00:00
set_empty(L"argv", ENV_LOCAL);
}
}
environment_t::~environment_t() = default;
env_stack_t::~env_stack_t() = default;
env_stack_t::env_stack_t(env_stack_t &&) = default;
null_environment_t::null_environment_t() = default;
null_environment_t::~null_environment_t() = default;
maybe_t<env_var_t> null_environment_t::get(const wcstring &key, env_mode_flags_t mode) const {
UNUSED(key);
UNUSED(mode);
return none();
}
wcstring_list_t null_environment_t::get_names(int flags) const {
UNUSED(flags);
return {};
}
env_stack_t env_stack_t::make_principal() {
const env_stack_t &gl = env_stack_t::globals();
std::unique_ptr<var_stack_t> dup_stack = make_unique<var_stack_t>(gl.vars_stack().clone());
return env_stack_t{std::move(dup_stack)};
}
env_stack_t &env_stack_t::principal() {
static env_stack_t s_principal = make_principal();
return s_principal;
}
env_stack_t &env_stack_t::globals() {
static env_stack_t s_global;
return s_global;
}
env_vars_snapshot_t::env_vars_snapshot_t(const environment_t &source, const wchar_t *const *keys) {
ASSERT_IS_MAIN_THREAD();
wcstring key;
for (size_t i = 0; keys[i]; i++) {
key.assign(keys[i]);
const auto var = source.get(key);
if (var) {
vars[key] = std::move(*var);
}
}
names = source.get_names(0);
}
env_vars_snapshot_t::~env_vars_snapshot_t() = default;
maybe_t<env_var_t> env_vars_snapshot_t::get(const wcstring &key, env_mode_flags_t mode) const {
UNUSED(mode);
auto iter = vars.find(key);
if (iter == vars.end()) return none();
return iter->second;
}
2012-05-06 21:53:19 +00:00
wcstring_list_t env_vars_snapshot_t::get_names(int flags) const {
UNUSED(flags);
return names;
}
const wchar_t *const env_vars_snapshot_t::highlighting_keys[] = {
L"PATH", L"CDPATH", L"fish_function_path", L"PWD", L"HOME", NULL};
const wchar_t *const env_vars_snapshot_t::completing_keys[] = {
L"PATH", L"CDPATH", L"fish_function_path", L"PWD", L"HOME", NULL};
#if defined(__APPLE__) || defined(__CYGWIN__)
static int check_runtime_path(const char *path) {
UNUSED(path);
return 0;
}
#else
/// Check, and create if necessary, a secure runtime path. Derived from tmux.c in tmux
/// (http://tmux.sourceforge.net/).
static int check_runtime_path(const char *path) {
// Copyright (c) 2007 Nicholas Marriott <nicm@users.sourceforge.net>
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
// IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
// OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
struct stat statpath;
uid_t uid = geteuid();
if (mkdir(path, S_IRWXU) != 0 && errno != EEXIST) return errno;
if (lstat(path, &statpath) != 0) return errno;
if (!S_ISDIR(statpath.st_mode) || statpath.st_uid != uid ||
(statpath.st_mode & (S_IRWXG | S_IRWXO)) != 0)
return EACCES;
return 0;
}
#endif
/// Return the path of an appropriate runtime data directory.
wcstring env_get_runtime_path() {
wcstring result;
const char *dir = getenv("XDG_RUNTIME_DIR");
// Check that the path is actually usable. Technically this is guaranteed by the fdo spec but in
// practice it is not always the case: see #1828 and #2222.
int mode = R_OK | W_OK | X_OK;
if (dir != NULL && access(dir, mode) == 0 && check_runtime_path(dir) == 0) {
result = str2wcstring(dir);
} else {
// Don't rely on $USER being set, as setup_user() has not yet been called.
// See https://github.com/fish-shell/fish-shell/issues/5180
// getpeuid() can't fail, but getpwuid sure can.
auto pwuid = getpwuid(geteuid());
const char *uname = pwuid ? pwuid->pw_name : NULL;
// /tmp/fish.user
std::string tmpdir = get_path_to_tmp_dir() + "/fish.";
if (uname) {
tmpdir.append(uname);
}
if (!uname || check_runtime_path(tmpdir.c_str()) != 0) {
debug(0, L"Runtime path not available.");
debug(0, L"Try deleting the directory %s and restarting fish.", tmpdir.c_str());
return result;
}
result = str2wcstring(tmpdir);
}
return result;
}