aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/builtin_set.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/builtin_set.cpp')
-rw-r--r--src/builtin_set.cpp827
1 files changed, 827 insertions, 0 deletions
diff --git a/src/builtin_set.cpp b/src/builtin_set.cpp
new file mode 100644
index 00000000..b1b6b0ea
--- /dev/null
+++ b/src/builtin_set.cpp
@@ -0,0 +1,827 @@
+/** \file builtin_set.c Functions defining the set builtin
+
+Functions used for implementing the set builtin.
+
+*/
+#include "config.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <wchar.h>
+#include <wctype.h>
+#include <sys/types.h>
+#include <termios.h>
+#include <signal.h>
+#include <vector>
+#include <algorithm>
+#include "fallback.h"
+#include "util.h"
+
+#include "wutil.h"
+#include "builtin.h"
+#include "env.h"
+#include "expand.h"
+#include "common.h"
+#include "wgetopt.h"
+#include "proc.h"
+#include "parser.h"
+
+/* We know about these buffers */
+extern wcstring stdout_buffer, stderr_buffer;
+
+/**
+ Error message for invalid path operations
+*/
+#define BUILTIN_SET_PATH_ERROR L"%ls: Warning: path component %ls may not be valid in %ls.\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 int is_path_variable(const wchar_t *env)
+{
+ return contains(env, L"PATH", L"CDPATH");
+}
+
+/**
+ 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)
+{
+ 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 */
+ wcstring_list_t existing_values;
+ const env_var_t existing_variable = env_get_string(key, scope);
+ 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 (list_contains_string(existing_values, dir))
+ {
+ any_success = true;
+ continue;
+ }
+
+ bool show_perror = false;
+ int show_hint = 0;
+ bool error = false;
+
+ struct stat buff;
+ if (wstat(dir, &buff))
+ {
+ error = true;
+ show_perror = true;
+ }
+
+ if (!(S_ISDIR(buff.st_mode)))
+ {
+ error = true;
+ }
+
+ if (!error)
+ {
+ any_success = true;
+ }
+ else
+ {
+ append_format(stderr_buffer, _(BUILTIN_SET_PATH_ERROR), L"set", dir.c_str(), key);
+ const wchar_t *colon = wcschr(dir.c_str(), L':');
+
+ if (colon && *(colon+1))
+ {
+ show_hint = 1;
+ }
+
+ }
+
+ if (show_perror)
+ {
+ builtin_wperror(L"set");
+ }
+
+ if (show_hint)
+ {
+ append_format(stderr_buffer, _(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++)
+ {
+ 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_PERM:
+ {
+ append_format(stderr_buffer, _(L"%ls: Tried to change the read-only variable '%ls'\n"), L"set", key);
+ retcode=1;
+ break;
+ }
+
+ case ENV_SCOPE:
+ {
+ append_format(stderr_buffer, _(L"%ls: Tried to set the special variable '%ls' with the wrong scope\n"), L"set", key);
+ retcode=1;
+ break;
+ }
+
+ case ENV_INVALID:
+ {
+ append_format(stderr_buffer, _(L"%ls: Tried to set the special variable '%ls' to an invalid value\n"), L"set", key);
+ retcode=1;
+ 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)
+{
+ size_t len;
+
+ int count = 0;
+ const wchar_t *src_orig = src;
+
+ if (src == 0)
+ {
+ return 0;
+ }
+
+ while (*src != L'\0' && (iswalnum(*src) || *src == L'_'))
+ {
+ src++;
+ }
+
+ if (*src != L'[')
+ {
+ append_format(stderr_buffer, _(BUILTIN_SET_ARG_COUNT), L"set");
+ return 0;
+ }
+
+ len = src-src_orig;
+
+ if ((wcsncmp(src_orig, name, len)!=0) || (wcslen(name) != (len)))
+ {
+ append_format(stderr_buffer,
+ _(L"%ls: Multiple variable names specified in single call (%ls and %.*ls)\n"),
+ L"set",
+ name,
+ len,
+ src_orig);
+ return 0;
+ }
+
+ src++;
+
+ while (iswspace(*src))
+ {
+ src++;
+ }
+
+ while (*src != L']')
+ {
+ wchar_t *end;
+
+ long l_ind;
+
+ errno = 0;
+
+ l_ind = wcstol(src, &end, 10);
+
+ if (end==src || errno)
+ {
+ append_format(stderr_buffer, _(L"%ls: Invalid index starting at '%ls'\n"), L"set", src);
+ return 0;
+ }
+
+ if (l_ind < 0)
+ {
+ l_ind = var_count+l_ind+1;
+ }
+
+ src = end;
+ if (*src==L'.' && *(src+1)==L'.')
+ {
+ src+=2;
+ long l_ind2 = wcstol(src, &end, 10);
+ if (end==src || errno)
+ {
+ return 1;
+ }
+ src = end;
+
+ 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++;
+ }
+ 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);
+ }
+
+// 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)
+{
+ 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);
+
+ stdout_buffer.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;
+
+ stdout_buffer.append(L" ");
+ stdout_buffer.append(e_value);
+
+ if (shorten)
+ {
+ stdout_buffer.push_back(ellipsis_char);
+ }
+
+ }
+ }
+
+ stdout_buffer.append(L"\n");
+ }
+}
+
+
+
+/**
+ The set builtin. Creates, updates and erases environment variables
+ and environemnt variable arrays.
+*/
+static int builtin_set(parser_t &parser, wchar_t **argv)
+{
+ /** Variables used for parsing the argument list */
+ const struct woption long_options[] =
+ {
+ { L"export", no_argument, 0, 'x' },
+ { L"global", no_argument, 0, 'g' },
+ { L"local", no_argument, 0, 'l' },
+ { L"erase", no_argument, 0, 'e' },
+ { L"names", no_argument, 0, 'n' },
+ { L"unexport", no_argument, 0, 'u' },
+ { L"universal", no_argument, 0, 'U' },
+ { L"long", no_argument, 0, 'L' },
+ { L"query", no_argument, 0, 'q' },
+ { L"help", no_argument, 0, 'h' },
+ { 0, 0, 0, 0 }
+ } ;
+
+ const wchar_t *short_options = L"+xglenuULqh";
+
+ 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;
+ bool preserve_incoming_failure_exit_status = true;
+ const int incoming_exit_status = proc_get_last_status();
+
+ /*
+ Variables used for performing the actual work
+ */
+ wchar_t *dest = 0;
+ int retcode=0;
+ int scope;
+ int slice=0;
+ int i;
+
+ const wchar_t *bad_char = NULL;
+
+
+ /* Parse options to obtain the requested operation and the modifiers */
+ woptind = 0;
+ while (1)
+ {
+ int c = wgetopt_long(argc, argv, short_options, long_options, 0);
+
+ if (c == -1)
+ {
+ break;
+ }
+
+ switch (c)
+ {
+ case 0:
+ break;
+
+ case 'e':
+ erase = 1;
+ preserve_incoming_failure_exit_status = false;
+ break;
+
+ case 'n':
+ list = 1;
+ preserve_incoming_failure_exit_status = false;
+ break;
+
+ case 'x':
+ exportv = 1;
+ break;
+
+ case 'l':
+ local = 1;
+ break;
+
+ case 'g':
+ global = 1;
+ break;
+
+ case 'u':
+ unexport = 1;
+ break;
+
+ case 'U':
+ universal = 1;
+ break;
+
+ case 'L':
+ shorten_ok = false;
+ break;
+
+ case 'q':
+ query = 1;
+ preserve_incoming_failure_exit_status = false;
+ break;
+
+ case 'h':
+ builtin_print_help(parser, argv[0], stdout_buffer);
+ return 0;
+
+ case '?':
+ builtin_unknown_option(parser, argv[0], argv[woptind-1]);
+ return 1;
+
+ default:
+ 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))
+ {
+ append_format(stderr_buffer,
+ BUILTIN_ERR_COMBO,
+ argv[0]);
+
+ builtin_print_help(parser, argv[0], stderr_buffer);
+ return 1;
+ }
+
+
+ /* We can't both list and erase variables */
+ if (erase && list)
+ {
+ append_format(stderr_buffer,
+ BUILTIN_ERR_COMBO,
+ argv[0]);
+
+ builtin_print_help(parser, argv[0], stderr_buffer);
+ return 1;
+ }
+
+ /*
+ Variables can only have one scope
+ */
+ if (local + global + universal > 1)
+ {
+ append_format(stderr_buffer,
+ BUILTIN_ERR_GLOCAL,
+ argv[0]);
+ builtin_print_help(parser, argv[0], stderr_buffer);
+ return 1;
+ }
+
+ /*
+ Variables can only have one export status
+ */
+ if (exportv && unexport)
+ {
+ append_format(stderr_buffer,
+ BUILTIN_ERR_EXPUNEXP,
+ argv[0]);
+ builtin_print_help(parser, argv[0], stderr_buffer);
+ return 1;
+ }
+
+ /*
+ 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=woptind; i<argc; i++)
+ {
+ wchar_t *arg = argv[i];
+ int slice=0;
+
+ if (!(dest = wcsdup(arg)))
+ {
+ DIE_MEM();
+ }
+
+ 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()))
+ {
+ builtin_print_help(parser, argv[0], stderr_buffer);
+ 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);
+ return 0;
+ }
+
+ if (woptind == argc)
+ {
+ /*
+ Print values of variables
+ */
+
+ if (erase)
+ {
+ append_format(stderr_buffer,
+ _(L"%ls: Erase needs a variable name\n"),
+ argv[0]);
+
+ builtin_print_help(parser, argv[0], stderr_buffer);
+ retcode = 1;
+ }
+ else
+ {
+ print_variables(1, 1, shorten_ok, scope);
+ }
+
+ return retcode;
+ }
+
+ if (!(dest = wcsdup(argv[woptind])))
+ {
+ DIE_MEM();
+ }
+
+ if (wcschr(dest, L'['))
+ {
+ slice = 1;
+ *wcschr(dest, L'[')=0;
+ }
+
+ if (!wcslen(dest))
+ {
+ free(dest);
+ append_format(stderr_buffer, BUILTIN_ERR_VARNAME_ZERO, argv[0]);
+ builtin_print_help(parser, argv[0], stderr_buffer);
+ return 1;
+ }
+
+ if ((bad_char = wcsvarname(dest)))
+ {
+ append_format(stderr_buffer, BUILTIN_ERR_VARCHAR, argv[0], *bad_char);
+ builtin_print_help(parser, argv[0], stderr_buffer);
+ free(dest);
+ return 1;
+ }
+
+ /*
+ 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 (; woptind<argc; woptind++)
+ {
+ if (!parse_index(indexes, argv[woptind], dest, result.size()))
+ {
+ builtin_print_help(parser, argv[0], stderr_buffer);
+ retcode = 1;
+ break;
+ }
+
+ size_t idx_count = indexes.size();
+ size_t val_count = argc-woptind-1;
+
+ if (!erase)
+ {
+ if (val_count < idx_count)
+ {
+ append_format(stderr_buffer, _(BUILTIN_SET_ARG_COUNT), argv[0]);
+ builtin_print_help(parser, argv[0], stderr_buffer);
+ retcode=1;
+ break;
+ }
+ if (val_count == idx_count)
+ {
+ 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);
+ }
+ else
+ {
+ wcstring_list_t value;
+
+ while (woptind < argc)
+ {
+ value.push_back(argv[woptind++]);
+ }
+
+ if (update_values(result,
+ indexes,
+ value))
+ {
+ append_format(stderr_buffer, L"%ls: ", argv[0]);
+ append_format(stderr_buffer, ARRAY_BOUNDS_ERR);
+ stderr_buffer.push_back(L'\n');
+ }
+
+ my_env_set(dest, result, scope);
+
+
+ }
+ }
+ }
+ else
+ {
+ woptind++;
+
+ /*
+ No slicing
+ */
+ if (erase)
+ {
+ if (woptind != argc)
+ {
+ append_format(stderr_buffer,
+ _(L"%ls: Values cannot be specfied with erase\n"),
+ argv[0]);
+ builtin_print_help(parser, argv[0], stderr_buffer);
+ retcode=1;
+ }
+ else
+ {
+ retcode = env_remove(dest, scope);
+ }
+ }
+ else
+ {
+ wcstring_list_t val;
+ for (i=woptind; i<argc; i++)
+ val.push_back(argv[i]);
+ retcode = my_env_set(dest, val, scope);
+ }
+ }
+
+ /* 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())
+ {
+ append_format(stderr_buffer, _(L"%ls: Warning: universal scope selected, but a global variable '%ls' exists.\n"), L"set", dest);
+ }
+
+ free(dest);
+
+ if (retcode == STATUS_BUILTIN_OK && preserve_incoming_failure_exit_status)
+ retcode = incoming_exit_status;
+ return retcode;
+
+}
+