fish-shell/src/builtin_set.cpp
Fabian Homborg 980365735a If no uvars are available, fall back to global when setting
Otherwise `set -U foo bar` if uvars aren't available would simply not
set *anything*.
2021-05-01 18:59:25 +02:00

857 lines
32 KiB
C++

// Functions used for implementing the set builtin.
#include "config.h" // IWYU pragma: keep
#include <sys/stat.h>
#include <unistd.h>
#include <algorithm>
#include <cerrno>
#include <cstddef>
#include <cstdlib>
#include <cstring>
#include <cwchar>
#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 "history.h"
#include "io.h"
#include "parser.h"
#include "proc.h"
#include "wcstringutil.h"
#include "wgetopt.h"
#include "wutil.h" // IWYU pragma: keep
struct set_cmd_opts_t {
bool print_help = false;
bool show = false;
bool local = false;
bool global = false;
bool exportv = false;
bool erase = false;
bool list = false;
bool unexport = false;
bool pathvar = false;
bool unpathvar = false;
bool universal = false;
bool query = false;
bool shorten_ok = true;
bool append = false;
bool prepend = false;
bool preserve_failure_exit_status = true;
};
/// Values used for long-only options.
enum {
opt_path = 1,
opt_unpath = 2,
};
// 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.
static const wchar_t *const short_options = L"+:LSUaeghlnpqux";
static const struct woption long_options[] = {
{L"export", no_argument, nullptr, 'x'}, {L"global", no_argument, nullptr, 'g'},
{L"local", no_argument, nullptr, 'l'}, {L"erase", no_argument, nullptr, 'e'},
{L"names", no_argument, nullptr, 'n'}, {L"unexport", no_argument, nullptr, 'u'},
{L"universal", no_argument, nullptr, 'U'}, {L"long", no_argument, nullptr, 'L'},
{L"query", no_argument, nullptr, 'q'}, {L"show", no_argument, nullptr, 'S'},
{L"append", no_argument, nullptr, 'a'}, {L"prepend", no_argument, nullptr, 'p'},
{L"path", no_argument, nullptr, opt_path}, {L"unpath", no_argument, nullptr, opt_unpath},
{L"help", no_argument, nullptr, 'h'}, {nullptr, 0, nullptr, 0}};
// Hint for invalid path operation with a colon.
#define BUILTIN_SET_PATH_ERROR _(L"%ls: Warning: $%ls entry \"%ls\" is not valid (%s)\n")
#define BUILTIN_SET_PATH_HINT _(L"%ls: Did you mean 'set %ls $%ls %ls'?\n")
#define BUILTIN_SET_MISMATCHED_ARGS _(L"%ls: You provided %d indexes but %d values\n")
#define BUILTIN_SET_ERASE_NO_VAR _(L"%ls: Erase needs a variable name\n")
#define BUILTIN_SET_ARRAY_BOUNDS_ERR _(L"%ls: Array index out of bounds\n")
#define BUILTIN_SET_UVAR_ERR \
_(L"%ls: Universal variable '%ls' is shadowed by the global variable of the same name.\n")
// Test if the specified variable should be subject to path validation.
static int is_path_variable(const wcstring &env) { return env == L"PATH" || env == L"CDPATH"; }
static int parse_cmd_opts(set_cmd_opts_t &opts, int *optind, //!OCLINT(high ncss method)
int argc, const wchar_t **argv, parser_t &parser, io_streams_t &streams) {
const wchar_t *cmd = argv[0];
int opt;
wgetopter_t w;
while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, nullptr)) != -1) {
switch (opt) {
case 'a': {
opts.append = true;
break;
}
case 'e': {
opts.erase = true;
opts.preserve_failure_exit_status = false;
break;
}
case 'g': {
opts.global = true;
break;
}
case 'h': {
opts.print_help = true;
break;
}
case 'l': {
opts.local = true;
break;
}
case 'n': {
opts.list = true;
opts.preserve_failure_exit_status = false;
break;
}
case 'p': {
opts.prepend = true;
break;
}
case 'q': {
opts.query = true;
opts.preserve_failure_exit_status = false;
break;
}
case 'x': {
opts.exportv = true;
break;
}
case 'u': {
opts.unexport = true;
break;
}
case opt_path: {
opts.pathvar = true;
break;
}
case opt_unpath: {
opts.unpathvar = true;
break;
}
case 'U': {
opts.universal = true;
break;
}
case 'L': {
opts.shorten_ok = false;
break;
}
case 'S': {
opts.show = true;
opts.preserve_failure_exit_status = false;
break;
}
case ':': {
builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]);
return STATUS_INVALID_ARGS;
}
case '?': {
builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]);
return STATUS_INVALID_ARGS;
}
default: {
DIE("unexpected retval from wgetopt_long");
}
}
}
*optind = w.woptind;
return STATUS_CMD_OK;
}
static int validate_cmd_opts(const wchar_t *cmd,
const set_cmd_opts_t &opts, //!OCLINT(npath complexity)
int argc, parser_t &parser, io_streams_t &streams) {
// Can't query and erase or list.
if (opts.query && (opts.erase || opts.list)) {
streams.err.append_format(BUILTIN_ERR_COMBO, cmd);
builtin_print_error_trailer(parser, streams.err, cmd);
return STATUS_INVALID_ARGS;
}
// We can't both list and erase variables.
if (opts.erase && opts.list) {
streams.err.append_format(BUILTIN_ERR_COMBO, cmd);
builtin_print_error_trailer(parser, streams.err, cmd);
return STATUS_INVALID_ARGS;
}
// Variables can only have one scope.
if (opts.local + opts.global + opts.universal > 1) {
streams.err.append_format(BUILTIN_ERR_GLOCAL, cmd);
builtin_print_error_trailer(parser, streams.err, cmd);
return STATUS_INVALID_ARGS;
}
// Variables can only have one export status.
if (opts.exportv && opts.unexport) {
streams.err.append_format(BUILTIN_ERR_EXPUNEXP, cmd);
builtin_print_error_trailer(parser, streams.err, cmd);
return STATUS_INVALID_ARGS;
}
// Variables can only have one path status.
if (opts.pathvar && opts.unpathvar) {
streams.err.append_format(BUILTIN_ERR_EXPUNEXP, cmd);
builtin_print_error_trailer(parser, streams.err, cmd);
return STATUS_INVALID_ARGS;
}
// Trying to erase and (un)export at the same time doesn't make sense.
if (opts.erase && (opts.exportv || opts.unexport)) {
streams.err.append_format(BUILTIN_ERR_COMBO, cmd);
builtin_print_error_trailer(parser, streams.err, cmd);
return STATUS_INVALID_ARGS;
}
// The --show flag cannot be combined with any other flag.
if (opts.show &&
(opts.local || opts.global || opts.erase || opts.list || opts.exportv || opts.universal)) {
streams.err.append_format(BUILTIN_ERR_COMBO, cmd);
builtin_print_error_trailer(parser, streams.err, cmd);
return STATUS_INVALID_ARGS;
}
if (argc == 0 && opts.erase) {
streams.err.append_format(BUILTIN_SET_ERASE_NO_VAR, cmd);
builtin_print_error_trailer(parser, streams.err, cmd);
return STATUS_INVALID_ARGS;
}
return STATUS_CMD_OK;
}
// Check if we are setting a uvar and a global of the same name exists. See
// https://github.com/fish-shell/fish-shell/issues/806
static int check_global_scope_exists(const wchar_t *cmd, const set_cmd_opts_t &opts,
const wcstring &dest, io_streams_t &streams,
const parser_t &parser) {
if (opts.universal) {
auto global_dest = parser.vars().get(dest, ENV_GLOBAL);
if (global_dest && parser.is_interactive()) {
streams.err.append_format(BUILTIN_SET_UVAR_ERR, cmd, dest.c_str());
}
}
return STATUS_CMD_OK;
}
// Validate the given path `list`. If there are any entries referring to invalid directories which
// contain a colon, then complain. Return true if any path element was valid, false if not.
static bool validate_path_warning_on_colons(const wchar_t *cmd,
const wcstring &key, //!OCLINT(npath complexity)
const wcstring_list_t &list, io_streams_t &streams,
const environment_t &vars) {
// Always allow setting an empty value.
if (list.empty()) return true;
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 /.
const auto existing_variable = vars.get(key, ENV_DEFAULT);
const wcstring_list_t &existing_values =
existing_variable ? existing_variable->as_list() : wcstring_list_t{};
for (const wcstring &dir : list) {
if (!string_prefixes_string(L"/", dir) || contains(existing_values, dir)) {
any_success = true;
continue;
}
const wchar_t *colon = std::wcschr(dir.c_str(), L':');
bool looks_like_colon_sep = colon && colon[1];
if (!looks_like_colon_sep && any_success) {
// Once we have one valid entry, skip the remaining ones unless we might warn.
continue;
}
struct stat buff;
bool valid = true;
if (wstat(dir, &buff) == -1) {
valid = false;
} else if (!S_ISDIR(buff.st_mode)) {
errno = ENOTDIR;
valid = false;
} else if (waccess(dir, X_OK) == -1) {
valid = false;
}
if (valid) {
any_success = true;
} else if (looks_like_colon_sep) {
streams.err.append_format(BUILTIN_SET_PATH_ERROR, cmd, key.c_str(), dir.c_str(),
std::strerror(errno));
streams.err.append_format(BUILTIN_SET_PATH_HINT, cmd, key.c_str(), key.c_str(),
std::wcschr(dir.c_str(), L':') + 1);
}
}
return any_success;
}
static void handle_env_return(int retval, const wchar_t *cmd, const wcstring &key,
io_streams_t &streams) {
switch (retval) {
case ENV_OK: {
break;
}
case ENV_PERM: {
streams.err.append_format(_(L"%ls: Tried to change the read-only variable '%ls'\n"),
cmd, key.c_str());
break;
}
case ENV_SCOPE: {
streams.err.append_format(
_(L"%ls: Tried to modify the special variable '%ls' with the wrong scope\n"), cmd,
key.c_str());
break;
}
case ENV_INVALID: {
streams.err.append_format(
_(L"%ls: Tried to modify the special variable '%ls' to an invalid value\n"), cmd,
key.c_str());
break;
}
case ENV_NOT_FOUND: {
streams.err.append_format(_(L"%ls: The variable '%ls' does not exist\n"), cmd,
key.c_str());
break;
}
default: {
DIE("unexpected vars.set() ret val");
}
}
}
/// Call vars.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 env_set_reporting_errors(const wchar_t *cmd, const wcstring &key, int scope,
wcstring_list_t list, io_streams_t &streams, env_stack_t &vars,
std::vector<event_t> *evts) {
if (is_path_variable(key) && !validate_path_warning_on_colons(cmd, key, list, streams, vars)) {
return STATUS_CMD_ERROR;
}
int retval = vars.set(key, scope | ENV_USER, std::move(list), evts);
handle_env_return(retval, cmd, key, streams);
return retval;
}
/// A helper type returned by split_var_and_indexes.
struct split_var_t {
wcstring varname; // name of the variable
maybe_t<env_var_t> var{}; // value of the variable, or none if missing
std::vector<long> indexes{}; // list of requested indexes
/// \return the number of elements in our variable, or 0 if missing.
long varsize() const { return var ? static_cast<long>(var->as_list().size()) : 0L; }
};
/// Extract indexes from an argument of the form `var_name[index1 index2...]`.
/// The argument \p arg is split into a variable name and list of indexes, which is returned by
/// reference. Indexes are "expanded" in the sense that range expressions .. and negative values are
/// handled.
///
/// Returns:
/// a split var on success, none() on error, in which case an error will have been printed.
/// If no index is found, this leaves indexes empty.
maybe_t<split_var_t> split_var_and_indexes(const wchar_t *arg, env_mode_flags_t mode,
const environment_t &vars, io_streams_t &streams) {
split_var_t res{};
const wchar_t *open_bracket = std::wcschr(arg, L'[');
size_t varname_len = open_bracket ? open_bracket - arg : wcslen(arg);
res.varname.assign(arg, varname_len);
res.var = vars.get(res.varname, mode);
if (!open_bracket) {
// Common case of no bracket.
return res;
}
long varsize = res.varsize();
const wchar_t *p = open_bracket + 1;
while (*p != L']') {
const wchar_t *end;
long l_ind;
if (res.indexes.empty() && p[0] == L'.' && p[1] == L'.') {
// If we are at the first index expression, a missing start-index means the range starts
// at the first item.
l_ind = 1; // first index
} else {
const wchar_t *end = nullptr;
l_ind = fish_wcstol(p, &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",
res.varname.c_str());
return none();
}
p = end;
}
// Convert negative index to a positive index.
if (l_ind < 0) l_ind = varsize + l_ind + 1;
if (p[0] == L'.' && p[1] == L'.') {
p += 2;
long l_ind2;
// If we are at the last index range expression, a missing end-index means the range
// spans until the last item.
if (res.indexes.empty() && *p == L']') {
l_ind2 = -1;
} else {
l_ind2 = fish_wcstol(p, &end);
if (errno > 0) { // ignore errno == -1 meaning there was text after the int
return none();
}
p = end;
}
// Convert negative index to a positive index.
if (l_ind2 < 0) l_ind2 = varsize + l_ind2 + 1;
int direction = l_ind2 < l_ind ? -1 : 1;
for (long jjj = l_ind; jjj * direction <= l_ind2 * direction; jjj += direction) {
res.indexes.push_back(jjj);
}
} else {
res.indexes.push_back(l_ind);
}
}
return res;
}
/// Given a list of values and 1-based indexes, return a new list, with those elements removed.
/// Note this deliberately accepts both args by value, as it modifies them both.
static wcstring_list_t erased_at_indexes(wcstring_list_t input, std::vector<long> indexes) {
// Sort our indexes into *descending* order.
std::sort(indexes.begin(), indexes.end(), std::greater<long>());
// Remove duplicates.
indexes.erase(std::unique(indexes.begin(), indexes.end()), indexes.end());
// Now when we walk indexes, we encounter larger indexes first.
for (long idx : indexes) {
if (idx > 0 && static_cast<size_t>(idx) <= input.size()) {
// One-based indexing!
input.erase(input.begin() + idx - 1);
}
}
return input;
}
static env_mode_flags_t compute_scope(const set_cmd_opts_t &opts) {
int scope = ENV_USER;
if (opts.local) scope |= ENV_LOCAL;
if (opts.global) scope |= ENV_GLOBAL;
if (opts.exportv) scope |= ENV_EXPORT;
if (opts.unexport) scope |= ENV_UNEXPORT;
if (opts.universal) scope |= ENV_UNIVERSAL;
if (opts.pathvar) scope |= ENV_PATHVAR;
if (opts.unpathvar) scope |= ENV_UNPATHVAR;
return scope;
}
/// Print the names of all environment variables in the scope. It will include the values unless the
/// `set --names` flag was used.
static int builtin_set_list(const wchar_t *cmd, set_cmd_opts_t &opts, int argc,
const wchar_t **argv, parser_t &parser, io_streams_t &streams) {
UNUSED(cmd);
UNUSED(argc);
UNUSED(argv);
UNUSED(parser);
bool names_only = opts.list;
wcstring_list_t names = parser.vars().get_names(compute_scope(opts));
sort(names.begin(), names.end());
for (const auto &key : names) {
const wcstring e_key = escape_string(key, 0);
streams.out.append(e_key);
if (!names_only) {
wcstring val;
if (opts.shorten_ok && key == L"history") {
std::shared_ptr<history_t> history =
history_t::with_name(history_session_id(parser.vars()));
for (size_t i = 1; i < history->size() && val.size() < 64; i++) {
if (i > 1) val += L' ';
val += expand_escape_string(history->item_at_index(i).str());
}
} else {
auto var = parser.vars().get(key, compute_scope(opts));
if (!var.missing_or_empty()) {
val = expand_escape_variable(*var);
}
}
if (!val.empty()) {
bool shorten = false;
if (opts.shorten_ok && val.length() > 64) {
shorten = true;
val.resize(60);
}
streams.out.append(L" ");
streams.out.append(val);
if (shorten) streams.out.push_back(get_ellipsis_char());
}
}
streams.out.append(L"\n");
}
return STATUS_CMD_OK;
}
// Query mode. Return the number of variables that do NOT exist out of the specified variables.
static int builtin_set_query(const wchar_t *cmd, set_cmd_opts_t &opts, int argc,
const wchar_t **argv, parser_t &parser, io_streams_t &streams) {
int retval = 0;
env_mode_flags_t scope = compute_scope(opts);
for (int i = 0; i < argc; i++) {
auto split = split_var_and_indexes(argv[i], scope, parser.vars(), streams);
if (!split) {
builtin_print_error_trailer(parser, streams.err, cmd);
return STATUS_CMD_ERROR;
}
if (split->indexes.empty()) {
// No indexes, just increment if our variable is missing.
if (!split->var) retval++;
} else {
// Increment for every index out of range.
long varsize = split->varsize();
for (long idx : split->indexes) {
if (idx < 1 || idx > varsize) retval++;
}
}
}
return retval;
}
static void show_scope(const wchar_t *var_name, int scope, io_streams_t &streams,
const environment_t &vars) {
const wchar_t *scope_name;
switch (scope) {
case ENV_LOCAL: {
scope_name = L"local";
break;
}
case ENV_GLOBAL: {
scope_name = L"global";
break;
}
case ENV_UNIVERSAL: {
scope_name = L"universal";
break;
}
default: {
DIE("invalid scope");
}
}
const auto var = vars.get(var_name, scope);
if (!var) {
return;
}
const wchar_t *exportv = var->exports() ? _(L"exported") : _(L"unexported");
const wchar_t *pathvarv = var->is_pathvar() ? _(L" a path variable") : L"";
wcstring_list_t vals = var->as_list();
streams.out.append_format(_(L"$%ls: set in %ls scope, %ls,%ls with %d elements\n"), var_name,
scope_name, exportv, pathvarv, vals.size());
for (size_t i = 0; i < vals.size(); i++) {
if (vals.size() > 100) {
if (i == 50) {
// try to print a mid-line ellipsis because we are eliding lines not words
streams.out.append(get_ellipsis_char() > 256 ? L"\u22EF" : get_ellipsis_str());
streams.out.push_back(L'\n');
}
if (i >= 50 && i < vals.size() - 50) continue;
}
const wcstring value = vals[i];
const wcstring escaped_val = escape_string(value, ESCAPE_NO_QUOTED, STRING_STYLE_SCRIPT);
streams.out.append_format(_(L"$%ls[%d]: |%ls|\n"), var_name, i + 1, escaped_val.c_str());
}
}
/// Show mode. Show information about the named variable(s).
static int builtin_set_show(const wchar_t *cmd, const set_cmd_opts_t &opts, int argc,
const wchar_t **argv, parser_t &parser, io_streams_t &streams) {
UNUSED(opts);
const auto &vars = parser.vars();
if (argc == 0) { // show all vars
wcstring_list_t names = parser.vars().get_names(ENV_USER);
sort(names.begin(), names.end());
for (const auto &name : names) {
if (name == L"history") continue;
show_scope(name.c_str(), ENV_LOCAL, streams, vars);
show_scope(name.c_str(), ENV_GLOBAL, streams, vars);
show_scope(name.c_str(), ENV_UNIVERSAL, streams, vars);
}
} else {
for (int i = 0; i < argc; i++) {
const wchar_t *arg = argv[i];
if (!valid_var_name(arg)) {
streams.err.append_format(BUILTIN_ERR_VARNAME, cmd, arg);
builtin_print_error_trailer(parser, streams.err, cmd);
return STATUS_INVALID_ARGS;
}
if (std::wcschr(arg, L'[')) {
streams.err.append_format(
_(L"%ls: `set --show` does not allow slices with the var names\n"), cmd);
builtin_print_error_trailer(parser, streams.err, cmd);
return STATUS_CMD_ERROR;
}
show_scope(arg, ENV_LOCAL, streams, vars);
show_scope(arg, ENV_GLOBAL, streams, vars);
show_scope(arg, ENV_UNIVERSAL, streams, vars);
}
}
return STATUS_CMD_OK;
}
/// Erase a variable.
static int builtin_set_erase(const wchar_t *cmd, set_cmd_opts_t &opts, int argc,
const wchar_t **argv, parser_t &parser, io_streams_t &streams) {
int ret = STATUS_CMD_OK;
env_mode_flags_t scope = compute_scope(opts);
for (int i = 0; i < argc; i++) {
auto split = split_var_and_indexes(argv[i], scope, parser.vars(), streams);
if (!split) {
builtin_print_error_trailer(parser, streams.err, cmd);
return STATUS_CMD_ERROR;
}
if (!valid_var_name(split->varname)) {
streams.err.append_format(BUILTIN_ERR_VARNAME, cmd, split->varname.c_str());
builtin_print_error_trailer(parser, streams.err, cmd);
return STATUS_INVALID_ARGS;
}
int retval = STATUS_CMD_OK;
std::vector<event_t> evts;
if (split->indexes.empty()) { // unset the var
retval = parser.vars().remove(split->varname, scope, &evts);
// When a non-existent-variable is unset, return ENV_NOT_FOUND as $status
// but do not emit any errors at the console as a compromise between user
// friendliness and correctness.
if (retval != ENV_NOT_FOUND) {
handle_env_return(retval, cmd, split->varname, streams);
}
} else { // remove just the specified indexes of the var
if (!split->var) return STATUS_CMD_ERROR;
wcstring_list_t result = erased_at_indexes(split->var->as_list(), split->indexes);
retval = env_set_reporting_errors(cmd, split->varname, scope, std::move(result),
streams, parser.vars(), &evts);
}
// Fire any events.
for (const auto &evt : evts) {
event_fire(parser, evt);
}
// Set $status to the last error value.
// This is cheesy, but I don't expect this to be checked often.
if (retval != STATUS_CMD_OK) {
ret = retval;
} else {
retval = check_global_scope_exists(cmd, opts, split->varname, streams, parser);
if (retval != STATUS_CMD_OK) ret = retval;
}
}
return ret;
}
/// Return a list of new values for the variable \p varname, respecting the \p opts.
/// The arguments are given as the argc, argv pair.
/// This handles the simple case where there are no indexes.
static wcstring_list_t new_var_values(const wcstring &varname, const set_cmd_opts_t &opts, int argc,
const wchar_t *const *argv, const environment_t &vars) {
wcstring_list_t result;
if (!opts.prepend && !opts.append) {
// Not prepending or appending.
result.assign(argv, argv + argc);
} else {
// Note: when prepending or appending, we always use default scoping when fetching existing
// values. For example:
// set --global var 1 2
// set --local --append var 3 4
// This starts with the existing global variable, appends to it, and sets it locally.
// So do not use the given variable: we must re-fetch it.
// TODO: this races under concurrent execution.
if (auto existing = vars.get(varname, ENV_DEFAULT)) {
result = existing->as_list();
}
if (opts.prepend) {
result.insert(result.begin(), argv, argv + argc);
}
if (opts.append) {
result.insert(result.end(), argv, argv + argc);
}
}
return result;
}
/// This handles the more difficult case of setting individual slices of a var.
static wcstring_list_t new_var_values_by_index(const split_var_t &split, int argc,
const wchar_t *const *argv) {
assert(static_cast<size_t>(argc) == split.indexes.size() &&
"Must have the same number of indexes as arguments");
// Inherit any existing values.
// Note unlike the append/prepend case, we start with a variable in the same scope as we are
// setting.
wcstring_list_t result;
if (split.var) result = split.var->as_list();
// For each (index, argument) pair, set the element in our \p result to the replacement string.
// Extend the list with empty strings as needed. The indexes are 1-based.
for (int i = 0; i < argc; i++) {
long lidx = split.indexes.at(i);
assert(lidx >= 1 && "idx should have been verified in range already");
// Convert from 1-based to 0-based.
size_t idx = static_cast<size_t>(lidx - 1);
// Extend as needed with empty strings.
if (idx >= result.size()) result.resize(idx + 1);
result.at(idx) = argv[i];
}
return result;
}
/// Set a variable.
static int builtin_set_set(const wchar_t *cmd, set_cmd_opts_t &opts, int argc, const wchar_t **argv,
parser_t &parser, io_streams_t &streams) {
if (argc == 0) {
streams.err.append_format(BUILTIN_ERR_MIN_ARG_COUNT1, cmd, 1);
builtin_print_error_trailer(parser, streams.err, cmd);
return STATUS_INVALID_ARGS;
}
env_mode_flags_t scope = compute_scope(opts);
const wchar_t *var_expr = argv[0];
argv++;
argc--;
auto split = split_var_and_indexes(var_expr, scope, parser.vars(), streams);
if (!split) {
builtin_print_error_trailer(parser, streams.err, cmd);
return STATUS_INVALID_ARGS;
}
// Is the variable valid?
if (!valid_var_name(split->varname)) {
streams.err.append_format(BUILTIN_ERR_VARNAME, cmd, split->varname.c_str());
builtin_print_error_trailer(parser, streams.err, cmd);
return STATUS_INVALID_ARGS;
}
// Setting with explicit indexes like `set foo[3] ...` has additional error handling.
if (!split->indexes.empty()) {
// Indexes must be > 0. (Note split_var_and_indexes negates negative values).
for (long v : split->indexes) {
if (v <= 0) {
streams.err.append_format(BUILTIN_SET_ARRAY_BOUNDS_ERR, cmd);
return STATUS_INVALID_ARGS;
}
}
// Append and prepend are disallowed.
if (opts.append || opts.prepend) {
streams.err.append_format(
L"%ls: Cannot use --append or --prepend when assigning to a slice", cmd);
builtin_print_error_trailer(parser, streams.err, cmd);
return STATUS_INVALID_ARGS;
}
// Argument count and index count must agree.
if (split->indexes.size() != static_cast<size_t>(argc)) {
streams.err.append_format(BUILTIN_SET_MISMATCHED_ARGS, cmd, split->indexes.size(),
argc);
return STATUS_INVALID_ARGS;
}
}
wcstring_list_t new_values;
if (split->indexes.empty()) {
// Handle the simple, common, case. Set the var to the specified values.
new_values = new_var_values(split->varname, opts, argc, argv, parser.vars());
} else {
// Handle the uncommon case of setting specific slices of a var.
new_values = new_var_values_by_index(*split, argc, argv);
}
bool have_shadowing_global = check_global_scope_exists(cmd, opts, split->varname, streams, parser);
// Set the value back in the variable stack and fire any events.
std::vector<event_t> evts;
int retval = env_set_reporting_errors(cmd, split->varname, scope, std::move(new_values),
streams, parser.vars(), &evts);
for (const auto &evt : evts) {
event_fire(parser, evt);
}
if (retval != STATUS_CMD_OK) return retval;
return have_shadowing_global;
}
/// The set builtin creates, updates, and erases (removes, deletes) variables.
maybe_t<int> builtin_set(parser_t &parser, io_streams_t &streams, const wchar_t **argv) {
const wchar_t *cmd = argv[0];
int argc = builtin_count_args(argv);
set_cmd_opts_t opts;
int optind;
int retval = parse_cmd_opts(opts, &optind, argc, argv, parser, streams);
if (retval != STATUS_CMD_OK) return retval;
argv += optind;
argc -= optind;
if (opts.print_help) {
builtin_print_help(parser, streams, cmd);
return STATUS_CMD_OK;
}
retval = validate_cmd_opts(cmd, opts, argc, parser, streams);
if (retval != STATUS_CMD_OK) return retval;
if (opts.query) {
retval = builtin_set_query(cmd, opts, argc, argv, parser, streams);
} else if (opts.erase) {
retval = builtin_set_erase(cmd, opts, argc, argv, parser, streams);
} else if (opts.list) { // explicit list the vars we know about
retval = builtin_set_list(cmd, opts, argc, argv, parser, streams);
} else if (opts.show) {
retval = builtin_set_show(cmd, opts, argc, argv, parser, streams);
} else if (argc == 0) { // implicit list the vars we know about
retval = builtin_set_list(cmd, opts, argc, argv, parser, streams);
} else {
retval = builtin_set_set(cmd, opts, argc, argv, parser, streams);
}
if (retval == STATUS_CMD_OK && opts.preserve_failure_exit_status) return none();
return retval;
}