fish-shell/src/builtin_set.cpp

620 lines
21 KiB
C++
Raw Normal View History

// Functions used for implementing the set builtin.
#include "config.h" // IWYU pragma: keep
#include <errno.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <wchar.h>
#include <wctype.h>
#include <algorithm>
#include <iterator>
#include <memory>
#include <set>
#include <string>
#include <vector>
#include "builtin.h"
#include "common.h"
#include "env.h"
#include "expand.h"
#include "fallback.h" // IWYU pragma: keep
#include "io.h"
#include "proc.h"
#include "wgetopt.h"
#include "wutil.h" // IWYU pragma: keep
class parser_t;
// Error message for invalid path operations.
#define BUILTIN_SET_PATH_ERROR L"%ls: Warning: $%ls entry \"%ls\" is not valid (%s)\n"
// Hint for invalid path operation with a colon.
#define BUILTIN_SET_PATH_HINT L"%ls: Did you mean 'set %ls $%ls %ls'?\n"
// Error for mismatch between index count and elements.
#define BUILTIN_SET_ARG_COUNT \
L"%ls: The number of variable indexes does not match the number of values\n"
// Test if the specified variable should be subject to path validation.
static const wcstring_list_t path_variables({L"PATH", L"CDPATH"});
static int is_path_variable(const wchar_t *env) { return contains(path_variables, env); }
/// Call env_set. If this is a path variable, e.g. PATH, validate the elements. On error, print a
/// description of the problem to stderr.
static int my_env_set(const wchar_t *key, const wcstring_list_t &val, int scope,
io_streams_t &streams) {
size_t i;
int retcode = 0;
const wchar_t *val_str = NULL;
if (is_path_variable(key)) {
// Fix for https://github.com/fish-shell/fish-shell/issues/199 . Return success if any path
// setting succeeds.
bool any_success = false;
// Don't bother validating (or complaining about) values that are already present. When
// determining already-present values, use ENV_DEFAULT instead of the passed-in scope
// because in:
//
// set -l PATH stuff $PATH
//
// where we are temporarily shadowing a variable, we want to compare against the shadowed
// value, not the (missing) local value. Also don't bother to complain about relative paths,
// which don't start with /.
wcstring_list_t existing_values;
const env_var_t existing_variable = env_get_string(key, ENV_DEFAULT);
if (!existing_variable.missing_or_empty())
tokenize_variable_array(existing_variable, existing_values);
for (i = 0; i < val.size(); i++) {
const wcstring &dir = val.at(i);
if (!string_prefixes_string(L"/", dir) || contains(existing_values, dir)) {
any_success = true;
continue;
}
int show_hint = 0;
bool error = false;
struct stat buff;
if (wstat(dir, &buff) == -1) {
error = true;
2016-11-03 12:54:57 +08:00
} else if (!S_ISDIR(buff.st_mode)) {
error = true;
errno = ENOTDIR;
2016-11-03 12:54:57 +08:00
} else if (waccess(dir, X_OK) == -1) {
error = true;
}
if (!error) {
any_success = true;
} else {
streams.err.append_format(_(BUILTIN_SET_PATH_ERROR), L"set", key, dir.c_str(),
2016-11-03 12:54:57 +08:00
strerror(errno));
const wchar_t *colon = wcschr(dir.c_str(), L':');
if (colon && *(colon + 1)) {
show_hint = 1;
}
}
if (show_hint) {
streams.err.append_format(_(BUILTIN_SET_PATH_HINT), L"set", key, key,
wcschr(dir.c_str(), L':') + 1);
}
}
// Fail at setting the path if we tried to set it to something non-empty, but it wound up
// empty.
if (!val.empty() && !any_success) {
return 1;
}
}
wcstring sb;
if (!val.empty()) {
for (i = 0; i < val.size(); i++) {
2012-02-23 04:00:02 +08:00
sb.append(val[i]);
if (i < val.size() - 1) {
sb.append(ARRAY_SEP_STR);
}
}
val_str = sb.c_str();
}
switch (env_set(key, val_str, scope | ENV_USER)) {
case ENV_OK: {
break;
}
case ENV_PERM: {
streams.err.append_format(_(L"%ls: Tried to change the read-only variable '%ls'\n"),
L"set", key);
retcode = 1;
2012-11-19 16:31:03 +08:00
break;
}
case ENV_SCOPE: {
streams.err.append_format(
_(L"%ls: Tried to set the special variable '%ls' with the wrong scope\n"), L"set",
key);
retcode = 1;
break;
}
case ENV_INVALID: {
streams.err.append_format(
_(L"%ls: Tried to set the special variable '%ls' to an invalid value\n"), L"set",
key);
retcode = 1;
2012-11-19 16:31:03 +08:00
break;
}
default: {
DIE("unexpected env_set() ret val");
break;
}
}
return retcode;
}
/// Extract indexes from a destination argument of the form name[index1 index2...]
///
/// \param indexes the list to insert the new indexes into
/// \param src the source string to parse
/// \param name the name of the element. Return null if the name in \c src does not match this name
/// \param var_count the number of elements in the array to parse.
///
/// \return the total number of indexes parsed, or -1 on error
static int parse_index(std::vector<long> &indexes, const wchar_t *src, const wchar_t *name,
size_t var_count, io_streams_t &streams) {
size_t len;
int count = 0;
const wchar_t *src_orig = src;
if (src == 0) {
return 0;
}
2016-10-21 09:53:31 +08:00
while (*src != L'\0' && (iswalnum(*src) || *src == L'_')) src++;
if (*src != L'[') {
streams.err.append_format(_(BUILTIN_SET_ARG_COUNT), L"set");
return 0;
}
len = src - src_orig;
if ((wcsncmp(src_orig, name, len) != 0) || (wcslen(name) != (len))) {
streams.err.append_format(
_(L"%ls: Multiple variable names specified in single call (%ls and %.*ls)\n"), L"set",
name, len, src_orig);
return 0;
}
src++;
2016-10-21 09:53:31 +08:00
while (iswspace(*src)) src++;
while (*src != L']') {
const wchar_t *end;
long l_ind = fish_wcstol(src, &end);
if (errno > 0) { // ignore errno == -1 meaning the int did not end with a '\0'
streams.err.append_format(_(L"%ls: Invalid index starting at '%ls'\n"), L"set", src);
return 0;
}
2016-10-21 09:53:31 +08:00
if (l_ind < 0) l_ind = var_count + l_ind + 1;
2016-10-21 09:53:31 +08:00
src = end; //!OCLINT(parameter reassignment)
if (*src == L'.' && *(src + 1) == L'.') {
src += 2;
long l_ind2 = fish_wcstol(src, &end);
if (errno > 0) { // ignore errno == -1 meaning the int did not end with a '\0'
return 1;
}
2016-10-21 09:53:31 +08:00
src = end; //!OCLINT(parameter reassignment)
if (l_ind2 < 0) {
l_ind2 = var_count + l_ind2 + 1;
}
int direction = l_ind2 < l_ind ? -1 : 1;
for (long jjj = l_ind; jjj * direction <= l_ind2 * direction; jjj += direction) {
// debug(0, L"Expand range [set]: %i\n", jjj);
indexes.push_back(jjj);
count++;
}
} else {
indexes.push_back(l_ind);
count++;
}
2016-10-21 09:53:31 +08:00
while (iswspace(*src)) src++;
}
return count;
}
static int update_values(wcstring_list_t &list, std::vector<long> &indexes,
wcstring_list_t &values) {
size_t i;
// Replace values where needed.
for (i = 0; i < indexes.size(); i++) {
// The '- 1' below is because the indices in fish are one-based, but the vector uses
// zero-based indices.
long ind = indexes[i] - 1;
const wcstring newv = values[i];
if (ind < 0) {
return 1;
}
if ((size_t)ind >= list.size()) {
list.resize(ind + 1);
Squashed commit of the following: commit 5b7659ec3d5e67b8dad8d3543d87a0169dc9a9e9 Merge: 57f3df3 22a4cd6 Author: ridiculousfish <corydoras@ridiculousfish.com> Date: Thu Jun 21 10:15:41 2012 -0700 Merge branch 'master' of https://github.com/maxfl/fish-shell into maxfl-master-base commit 22a4cd686fbe4a6730859aa1a84b21bc9c832203 Author: maxfl <gmaxfl@gmail.com> Date: Tue Jun 19 15:51:43 2012 +0400 set now expands the variable size, if index is outside it commit 9b0ffa83157ce0cfa36d246fa2e3179d6b790dea Author: maxfl <gmaxfl@gmail.com> Date: Mon Jun 18 21:30:44 2012 +0400 fixes #78 commit 78387fb3915b01342a981059780a164eedc0f8eb Merge: c0e6096 93dc7d4 Author: maxfl <gmaxfl@gmail.com> Date: Mon Jun 18 21:27:47 2012 +0400 Merge remote-tracking branch 'fishfish/master' commit c0e60963c179f80cb1d5d6a18b44510e55c95e10 Merge: 32a98e7 1bead8a Author: maxfl <gmaxfl@gmail.com> Date: Mon Jun 18 10:29:42 2012 +0400 Merge remote-tracking branch 'fishfish/master' commit 32a98e799eb2f016f8bad5287851f6353b835014 Merge: 6e71021 f2b5292 Author: maxfl <gmaxfl@gmail.com> Date: Sat Jun 16 18:42:07 2012 +0400 Merge remote-tracking branch 'fishfish/master' commit 6e710211bca0bca73d738d71e22d20e700db2a63 Author: maxfl <gmaxfl@gmail.com> Date: Thu Jun 14 11:01:13 2012 +0400 revert fish_pager commit 731a29f35bdbaa4dfaad78c7428ab2e5edb45a6c Author: maxfl <gmaxfl@gmail.com> Date: Thu Jun 14 10:57:41 2012 +0400 revert fish_pager.cpp commit 72c1bfc7bfa77bb723d6df3f030a1918db2aca8a Merge: ea74ffa 9b781c4 Author: maxfl <gmaxfl@gmail.com> Date: Wed Jun 13 17:54:11 2012 +0400 Merge branch 'master' into maxfl_completions commit ea74ffa08689a35fd08bc3520a0d52cf30365568 Author: maxfl <gmaxfl@gmail.com> Date: Wed Jun 13 17:35:20 2012 +0400 __fish_complete_command now can understand '--arg=option' tokens latexmk completion is updated commit 45b667826f3f181972e4987a3609d4ccac16a675 Author: maxfl <gmaxfl@gmail.com> Date: Wed Jun 13 16:46:47 2012 +0400 . completion commit 1c9f8ffc9e7ea45eee8aedba5a48e21c75a08882 Author: maxfl <gmaxfl@gmail.com> Date: Wed Jun 13 16:46:13 2012 +0400 a lot of new completions commit 8224d9f984a678fd49cdf78f76770977e0ae257b Author: Maxim Gonchar <gonchar@myhost.localdomain> Date: Tue Jun 12 20:19:31 2012 +0400 A lot of new completions. Some small updates and fixes of old functions and completions. commit 234ed8f5dab209fd657ccaf0dd23d636d2a06355 Author: Maxim Gonchar <gonchar@myhost.localdomain> Date: Tue Jun 12 20:03:44 2012 +0400 step-coloring initial set_color correction
2012-06-22 01:24:49 +08:00
}
// free((void *) al_get(list, ind));
list[ind] = newv;
}
return 0;
}
/// Erase from a list of wcstring values at specified indexes.
static void erase_values(wcstring_list_t &list, const std::vector<long> &indexes) {
// Make a set of indexes.
// This both sorts them into ascending order and removes duplicates.
const std::set<long> indexes_set(indexes.begin(), indexes.end());
// Now walk the set backwards, so we encounter larger indexes first, and remove elements at the
// given (1-based) indexes.
std::set<long>::const_reverse_iterator iter;
for (iter = indexes_set.rbegin(); iter != indexes_set.rend(); ++iter) {
long val = *iter;
if (val > 0 && (size_t)val <= list.size()) {
// One-based indexing!
list.erase(list.begin() + val - 1);
}
}
}
/// Print the names of all environment variables in the scope, with or without shortening, with or
/// without values, with or without escaping
static void print_variables(int include_values, int esc, bool shorten_ok, int scope,
io_streams_t &streams) {
wcstring_list_t names = env_get_names(scope);
sort(names.begin(), names.end());
for (size_t i = 0; i < names.size(); i++) {
const wcstring key = names.at(i);
const wcstring e_key = escape_string(key, 0);
streams.out.append(e_key);
if (include_values) {
env_var_t value = env_get_string(key, scope);
if (!value.missing()) {
int shorten = 0;
if (shorten_ok && value.length() > 64) {
shorten = 1;
value.resize(60);
}
wcstring e_value = esc ? expand_escape_variable(value) : value;
streams.out.append(L" ");
streams.out.append(e_value);
if (shorten) {
streams.out.push_back(ellipsis_char);
}
}
}
streams.out.append(L"\n");
}
}
/// The set builtin creates, updates, and erases (removes, deletes) variables.
int builtin_set(parser_t &parser, io_streams_t &streams, wchar_t **argv) {
wchar_t *cmd = argv[0];
int argc = builtin_count_args(argv);
// Flags to set the work mode.
int local = 0, global = 0, exportv = 0;
int erase = 0, list = 0, unexport = 0;
int universal = 0, query = 0;
bool shorten_ok = true;
2016-11-03 12:54:57 +08:00
bool preserve_failure_exit_status = true;
const int incoming_exit_status = proc_get_last_status();
// Variables used for performing the actual work.
2017-06-10 04:29:16 +08:00
wchar_t *dest = NULL;
int retcode = STATUS_CMD_OK;
int scope;
int slice = 0;
2017-06-10 04:29:16 +08:00
// Variables used for parsing the argument list. This command is atypical in using the "+"
// (REQUIRE_ORDER) option for flag parsing. This is not typical of most fish commands. It means
// we stop scanning for flags when the first non-flag argument is seen.
2017-06-30 12:49:57 +08:00
static const wchar_t *short_options = L"+:LUeghlnqux";
2017-06-10 04:29:16 +08:00
static const struct woption long_options[] = {{L"export", no_argument, NULL, 'x'},
{L"global", no_argument, NULL, 'g'},
{L"local", no_argument, NULL, 'l'},
{L"erase", no_argument, NULL, 'e'},
{L"names", no_argument, NULL, 'n'},
{L"unexport", no_argument, NULL, 'u'},
{L"universal", no_argument, NULL, 'U'},
{L"long", no_argument, NULL, 'L'},
{L"query", no_argument, NULL, 'q'},
{L"help", no_argument, NULL, 'h'},
{NULL, 0, NULL, 0}};
2017-06-10 04:29:16 +08:00
// Parse options to obtain the requested operation and the modifiers.
int opt;
wgetopter_t w;
while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, NULL)) != -1) {
switch (opt) {
case 'e': {
2012-11-19 16:31:03 +08:00
erase = 1;
2016-11-03 12:54:57 +08:00
preserve_failure_exit_status = false;
2012-11-19 16:31:03 +08:00
break;
}
case 'n': {
2012-11-19 16:31:03 +08:00
list = 1;
2016-11-03 12:54:57 +08:00
preserve_failure_exit_status = false;
2012-11-19 16:31:03 +08:00
break;
}
case 'x': {
2012-11-19 16:31:03 +08:00
exportv = 1;
break;
}
case 'l': {
2012-11-19 16:31:03 +08:00
local = 1;
break;
}
case 'g': {
2012-11-19 16:31:03 +08:00
global = 1;
break;
}
case 'u': {
2012-11-19 16:31:03 +08:00
unexport = 1;
break;
}
case 'U': {
2012-11-19 16:31:03 +08:00
universal = 1;
break;
}
case 'L': {
2012-11-19 16:31:03 +08:00
shorten_ok = false;
break;
}
case 'q': {
2012-11-19 16:31:03 +08:00
query = 1;
2016-11-03 12:54:57 +08:00
preserve_failure_exit_status = false;
2012-11-19 16:31:03 +08:00
break;
}
case 'h': {
builtin_print_help(parser, streams, cmd, streams.out);
return STATUS_CMD_OK;
}
2017-06-30 12:49:57 +08:00
case ':': {
builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]);
2017-06-30 12:49:57 +08:00
return STATUS_INVALID_ARGS;
}
case '?': {
builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]);
return STATUS_INVALID_ARGS;
}
2017-06-10 04:29:16 +08:00
default: {
DIE("unexpected retval from wgetopt_long");
break;
}
}
}
// Ok, all arguments have been parsed, let's validate them. If we are checking the existance of
// a variable (-q) we can not also specify scope.
if (query && (erase || list)) {
streams.err.append_format(BUILTIN_ERR_COMBO, cmd);
builtin_print_help(parser, streams, cmd, streams.err);
return STATUS_INVALID_ARGS;
}
// We can't both list and erase variables.
if (erase && list) {
streams.err.append_format(BUILTIN_ERR_COMBO, cmd);
builtin_print_help(parser, streams, cmd, streams.err);
return STATUS_INVALID_ARGS;
}
// Variables can only have one scope.
if (local + global + universal > 1) {
streams.err.append_format(BUILTIN_ERR_GLOCAL, cmd);
builtin_print_help(parser, streams, cmd, streams.err);
return STATUS_INVALID_ARGS;
}
// Variables can only have one export status.
if (exportv && unexport) {
streams.err.append_format(BUILTIN_ERR_EXPUNEXP, argv[0]);
builtin_print_help(parser, streams, cmd, streams.err);
return STATUS_INVALID_ARGS;
}
// Calculate the scope value for variable assignement.
scope = (local ? ENV_LOCAL : 0) | (global ? ENV_GLOBAL : 0) | (exportv ? ENV_EXPORT : 0) |
(unexport ? ENV_UNEXPORT : 0) | (universal ? ENV_UNIVERSAL : 0) | ENV_USER;
if (query) {
// Query mode. Return the number of variables that do not exist out of the specified
// variables.
int i;
for (i = w.woptind; i < argc; i++) {
wchar_t *arg = argv[i];
int slice = 0;
dest = wcsdup(arg);
assert(dest);
if (wcschr(dest, L'[')) {
slice = 1;
*wcschr(dest, L'[') = 0;
}
if (slice) {
std::vector<long> indexes;
wcstring_list_t result;
size_t j;
env_var_t dest_str = env_get_string(dest, scope);
if (!dest_str.missing()) tokenize_variable_array(dest_str, result);
if (!parse_index(indexes, arg, dest, result.size(), streams)) {
builtin_print_help(parser, streams, cmd, streams.err);
retcode = 1;
break;
}
for (j = 0; j < indexes.size(); j++) {
long idx = indexes[j];
if (idx < 1 || (size_t)idx > result.size()) {
retcode++;
}
}
} else {
if (!env_exist(arg, scope)) {
retcode++;
}
}
free(dest);
}
return retcode;
}
if (list) {
// Maybe we should issue an error if there are any other arguments?
print_variables(0, 0, shorten_ok, scope, streams);
return STATUS_CMD_OK;
}
if (w.woptind == argc) {
// Print values of variables.
if (erase) {
streams.err.append_format(_(L"%ls: Erase needs a variable name\n"), cmd);
builtin_print_help(parser, streams, cmd, streams.err);
retcode = STATUS_INVALID_ARGS;
} else {
print_variables(1, 1, shorten_ok, scope, streams);
}
return retcode;
}
dest = wcsdup(argv[w.woptind]);
assert(dest);
if (wcschr(dest, L'[')) {
slice = 1;
*wcschr(dest, L'[') = 0;
}
if (!valid_var_name(dest)) {
streams.err.append_format(BUILTIN_ERR_VARNAME, cmd, dest);
builtin_print_help(parser, streams, argv[0], streams.err);
return STATUS_INVALID_ARGS;
}
// Set assignment can work in two modes, either using slices or using the whole array. We detect
// which mode is used here.
if (slice) {
// Slice mode.
std::vector<long> indexes;
wcstring_list_t result;
const env_var_t dest_str = env_get_string(dest, scope);
if (!dest_str.missing()) {
tokenize_variable_array(dest_str, result);
} else if (erase) {
retcode = 1;
}
if (!retcode) {
for (; w.woptind < argc; w.woptind++) {
if (!parse_index(indexes, argv[w.woptind], dest, result.size(), streams)) {
builtin_print_help(parser, streams, argv[0], streams.err);
retcode = 1;
break;
}
size_t idx_count = indexes.size();
size_t val_count = argc - w.woptind - 1;
if (!erase) {
if (val_count < idx_count) {
streams.err.append_format(_(BUILTIN_SET_ARG_COUNT), argv[0]);
builtin_print_help(parser, streams, argv[0], streams.err);
retcode = 1;
break;
}
if (val_count == idx_count) {
w.woptind++;
break;
}
}
}
}
if (!retcode) {
// Slice indexes have been calculated, do the actual work.
if (erase) {
erase_values(result, indexes);
my_env_set(dest, result, scope, streams);
} else {
wcstring_list_t value;
while (w.woptind < argc) {
value.push_back(argv[w.woptind++]);
}
if (update_values(result, indexes, value)) {
streams.err.append_format(L"%ls: ", argv[0]);
streams.err.append_format(ARRAY_BOUNDS_ERR);
streams.err.push_back(L'\n');
}
my_env_set(dest, result, scope, streams);
}
}
} else {
w.woptind++;
// No slicing.
if (erase) {
if (w.woptind != argc) {
streams.err.append_format(_(L"%ls: Values cannot be specfied with erase\n"),
argv[0]);
builtin_print_help(parser, streams, argv[0], streams.err);
retcode = 1;
} else {
retcode = env_remove(dest, scope);
}
} else {
wcstring_list_t val;
for (int i = w.woptind; i < argc; i++) val.push_back(argv[i]);
retcode = my_env_set(dest, val, scope, streams);
}
}
// Check if we are setting variables above the effective scope. See
// https://github.com/fish-shell/fish-shell/issues/806
env_var_t global_dest = env_get_string(dest, ENV_GLOBAL);
if (universal && !global_dest.missing()) {
streams.err.append_format(
_(L"%ls: Warning: universal scope selected, but a global variable '%ls' exists.\n"),
L"set", dest);
}
free(dest);
2017-05-05 13:42:42 +08:00
if (retcode == STATUS_CMD_OK && preserve_failure_exit_status) retcode = incoming_exit_status;
return retcode;
}