implement set --append and set --prepend

Fixes #1326
This commit is contained in:
Kurtis Rader 2017-08-03 23:57:36 -07:00
parent cddc4cfb1d
commit 67de733b9b
9 changed files with 270 additions and 75 deletions

View file

@ -5,16 +5,17 @@ This section is for changes merged to the `major` branch that are not also merge
- The `IFS` variable is deprecated and will be removed in fish 4.0 (#4156).
## Notable fixes and improvements
- Local exported (`set -lx`) vars are now visible to functions (#1091).
- `set x[1] x[2] a b` is no longer valid syntax (#4236).
- `complete` now has a `-k`/`--keep-order` flag to keep the order of the OPTION_ARGUMENTS (#361).
- A "--delimiter" option to `read` as a better alternative to the `IFS` variable (#4256).
- `read` has a new `--delimiter` option as a better alternative to the `IFS` variable (#4256).
- `set` has a new `--append` and `--prepend` option (#1326).
- `set` has a new `--show` option to show lots of information about variables (#4265).
- Command substitution output is now limited to 10 MB by default (#3822).
- `set x[1] x[2] a b` is no longer valid syntax (#4236).
- `complete` now has a `-k` and `--keep-order` option to keep the order of the OPTION_ARGUMENTS (#361).
- Local exported (`set -lx`) vars are now visible to functions (#1091).
## Other significant changes
- `read` now requires at least one var name (#4220).
- `abbr` has been reimplemented to be faster. This means the old `fish_user_abbreviations` variable is ignored (#4048).
- Command substitution output is now limited to 10 MB by default (#3822).
# fish 2.7b1

View file

@ -21,6 +21,10 @@ With both variable names and values provided, `set` assigns the variable `VARIAB
The following options control variable scope:
- `-a` or `--append` causes the values to be appended to the current set of values for the variable. This can be used with `--prepend` to both append and prepend at the same time. This cannot be used when assigning to a variable slice.
- `-p` or `--prepend` causes the values to be prepended to the current set of values for the variable. This can be used with `--append` to both append and prepend at the same time. This cannot be used when assigning to a variable slice.
- `-l` or `--local` forces the specified shell variable to be given a scope that is local to the current block, even if a variable with the given name exists and is non-local
- `-g` or `--global` causes the specified shell variable to be given a global scope. Non-global variables disappear when the block they belong to ends
@ -76,24 +80,31 @@ In erase mode, if variable indices are specified, only the specified slices of t
In assignment mode, `set` does not modify the exit status. This allows simultaneous capture of the output and exit status of a subcommand, e.g. `if set output (command)`. In query mode, the exit status is the number of variables that were not found. In erase mode, `set` exits with a zero exit status in case of success, with a non-zero exit status if the commandline was invalid, if the variable was write-protected or if the variable did not exist.
\subsection set-example Example
\subsection set-examples Examples
\fish
set -xg
# Prints all global, exported variables.
set -xg
set foo hi
# Sets the value of the variable $foo to be 'hi'.
set foo hi
# Appends the value "there" to the variable $foo.
set -a foo there
# Does the same thing as the previous two commands the way it would be done pre-fish 3.0.
set foo hi
set foo $foo there
set -e smurf
# Removes the variable $smurf
set -e smurf
set PATH[4] ~/bin
# Changes the fourth element of the $PATH array to ~/bin
set PATH[4] ~/bin
# Outputs the path to Python if `type -p` returns true.
if set python_path (type -p python)
echo "Python is at $python_path"
end
# Outputs the path to Python if `type -p` returns true.
\endfish
\subsection set-notes Notes

View file

@ -40,19 +40,22 @@ struct set_cmd_opts_t {
bool universal = false;
bool query = false;
bool shorten_ok = true;
bool append = false;
bool prepend = false;
bool preserve_failure_exit_status = true;
};
// 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 *short_options = L"+:LSUeghlnqux";
static const wchar_t *short_options = L"+:LSUaeghlnpqux";
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"show", no_argument, NULL, 'S'},
{L"append", no_argument, NULL, 'a'}, {L"prepend", no_argument, NULL, 'p'},
{L"help", no_argument, NULL, 'h'}, {NULL, 0, NULL, 0}};
// Error message for invalid path operations.
@ -75,28 +78,45 @@ static int parse_cmd_opts(set_cmd_opts_t &opts, int *optind, //!OCLINT(high ncs
wgetopter_t w;
while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, NULL)) != -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 'l': {
opts.local = true;
break;
}
case 'g': {
opts.global = true;
break;
}
case 'u': {
opts.unexport = true;
break;
@ -109,20 +129,11 @@ static int parse_cmd_opts(set_cmd_opts_t &opts, int *optind, //!OCLINT(high ncs
opts.shorten_ok = false;
break;
}
case 'q': {
opts.query = true;
opts.preserve_failure_exit_status = false;
break;
}
case 'S': {
opts.show = true;
opts.preserve_failure_exit_status = false;
break;
}
case 'h': {
opts.print_help = true;
break;
}
case ':': {
builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]);
return STATUS_INVALID_ARGS;
@ -429,8 +440,8 @@ static int compute_scope(set_cmd_opts_t &opts) {
/// Print the names of all environment variables in the scope. It will include the values unless the
/// `set --list` flag was used.
static int builtin_set_list(const wchar_t *cmd, set_cmd_opts_t &opts, int argc,
wchar_t **argv, parser_t &parser, io_streams_t &streams) {
static int builtin_set_list(const wchar_t *cmd, set_cmd_opts_t &opts, int argc, wchar_t **argv,
parser_t &parser, io_streams_t &streams) {
UNUSED(cmd);
UNUSED(argc);
UNUSED(argv);
@ -469,8 +480,8 @@ static int builtin_set_list(const wchar_t *cmd, set_cmd_opts_t &opts, int argc,
}
// 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,
wchar_t **argv, parser_t &parser, io_streams_t &streams) {
static int builtin_set_query(const wchar_t *cmd, set_cmd_opts_t &opts, int argc, wchar_t **argv,
parser_t &parser, io_streams_t &streams) {
int retval = 0;
int scope = compute_scope(opts);
@ -593,8 +604,8 @@ static int builtin_set_show(const wchar_t *cmd, set_cmd_opts_t &opts, int argc,
}
/// Erase a variable.
static int builtin_set_erase(const wchar_t *cmd, set_cmd_opts_t &opts, int argc,
wchar_t **argv, parser_t &parser, io_streams_t &streams) {
static int builtin_set_erase(const wchar_t *cmd, set_cmd_opts_t &opts, int argc, wchar_t **argv,
parser_t &parser, io_streams_t &streams) {
if (argc != 1) {
streams.err.append_format(BUILTIN_ERR_ARG_COUNT2, cmd, L"--erase", 1, argc);
builtin_print_help(parser, streams, cmd, streams.err);
@ -633,75 +644,111 @@ static int builtin_set_erase(const wchar_t *cmd, set_cmd_opts_t &opts, int argc,
return check_global_scope_exists(cmd, opts, dest, streams);
}
/// This handles the more difficult case of setting individual slices of a var.
static int set_slices(const wchar_t *cmd, set_cmd_opts_t &opts, const wchar_t *dest,
std::vector<long> &indexes, int argc, wchar_t **argv, parser_t &parser,
io_streams_t &streams) {
/// This handles the common case of setting the entire var to a set of values.
static int set_var_array(const wchar_t *cmd, set_cmd_opts_t &opts, const wchar_t *varname,
wcstring_list_t &new_values, int argc, wchar_t **argv, parser_t &parser,
io_streams_t &streams) {
UNUSED(cmd);
UNUSED(parser);
UNUSED(streams);
if (opts.prepend || opts.append) {
int scope = compute_scope(opts);
if (opts.prepend) {
for (int i = 0; i < argc; i++) new_values.push_back(argv[i]);
}
env_var_t var_str = env_get_string(varname, scope);
wcstring_list_t var_array;
if (!var_str.missing()) tokenize_variable_array(var_str, var_array);
new_values.insert(new_values.end(), var_array.begin(), var_array.end());
if (opts.append) {
for (int i = 0; i < argc; i++) new_values.push_back(argv[i]);
}
} else {
for (int i = 0; i < argc; i++) new_values.push_back(argv[i]);
}
return STATUS_CMD_OK;
}
/// This handles the more difficult case of setting individual slices of a var.
static int set_var_slices(const wchar_t *cmd, set_cmd_opts_t &opts, const wchar_t *varname,
wcstring_list_t &new_values, std::vector<long> &indexes, int argc,
wchar_t **argv, parser_t &parser, io_streams_t &streams) {
UNUSED(parser);
if (opts.append || opts.prepend) {
streams.err.append_format(
L"%ls: Cannot use --append or --prepend when assigning to a slice", cmd);
builtin_print_help(parser, streams, cmd, streams.err);
return STATUS_INVALID_ARGS;
}
if (indexes.size() != static_cast<size_t>(argc)) {
streams.err.append_format(BUILTIN_SET_MISMATCHED_ARGS, cmd, indexes.size(), argc);
}
int scope = compute_scope(opts); // calculate the variable scope based on the provided options
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);
const env_var_t var_str = env_get_string(varname, scope);
if (!var_str.missing()) tokenize_variable_array(var_str, new_values);
// Slice indexes have been calculated, do the actual work.
wcstring_list_t new_values;
for (int i = 0; i < argc; i++) new_values.push_back(argv[i]);
wcstring_list_t result;
for (int i = 0; i < argc; i++) result.push_back(argv[i]);
int retval = update_values(result, indexes, new_values);
int retval = update_values(new_values, indexes, result);
if (retval != STATUS_CMD_OK) {
streams.err.append_format(BUILTIN_SET_ARRAY_BOUNDS_ERR, cmd);
return retval;
}
retval = my_env_set(cmd, dest, result, scope, streams);
if (retval != STATUS_CMD_OK) return retval;
return check_global_scope_exists(cmd, opts, dest, streams);
return STATUS_CMD_OK;
}
/// Set a variable.
static int builtin_set_set(const wchar_t *cmd, set_cmd_opts_t &opts, int argc,
wchar_t **argv, parser_t &parser, io_streams_t &streams) {
static int builtin_set_set(const wchar_t *cmd, set_cmd_opts_t &opts, int argc, 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_help(parser, streams, cmd, streams.err);
return STATUS_INVALID_ARGS;
}
int retval;
int scope = compute_scope(opts); // calculate the variable scope based on the provided options
wchar_t *dest = argv[0];
wchar_t *varname = argv[0];
argv++;
argc--;
std::vector<long> indexes;
int idx_count = parse_index(indexes, dest, scope, streams);
int idx_count = parse_index(indexes, varname, scope, streams);
if (idx_count == -1) {
builtin_print_help(parser, streams, cmd, streams.err);
return STATUS_INVALID_ARGS;
}
if (!valid_var_name(dest)) {
streams.err.append_format(BUILTIN_ERR_VARNAME, cmd, dest);
if (!valid_var_name(varname)) {
streams.err.append_format(BUILTIN_ERR_VARNAME, cmd, varname);
builtin_print_help(parser, streams, cmd, streams.err);
return STATUS_INVALID_ARGS;
}
if (idx_count != 0) {
int retval;
wcstring_list_t new_values;
if (idx_count == 0) {
// Handle the simple, common, case. Set the var to the specified values.
retval = set_var_array(cmd, opts, varname, new_values, argc, argv, parser, streams);
} else {
// Handle the uncommon case of setting specific slices of a var.
return set_slices(cmd, opts, dest, indexes, argc, argv, parser, streams);
retval =
set_var_slices(cmd, opts, varname, new_values, indexes, argc, argv, parser, streams);
}
// This is the simple, common, case. Set the var to the specified values.
wcstring_list_t values;
for (int i = 0; i < argc; i++) values.push_back(argv[i]);
retval = my_env_set(cmd, dest, values, scope, streams);
if (retval != STATUS_CMD_OK) return retval;
return check_global_scope_exists(cmd, opts, dest, streams);
retval = my_env_set(cmd, varname, new_values, scope, streams);
if (retval != STATUS_CMD_OK) return retval;
return check_global_scope_exists(cmd, opts, varname, streams);
}
/// The set builtin creates, updates, and erases (removes, deletes) variables.

View file

@ -5,3 +5,12 @@ $argle bargle: invalid var name
####################
# Verify behavior of `set --show`
####################
# Appending works
####################
# Prepending works
####################
# Appending and prepending at same time works

View file

@ -13,3 +13,34 @@ set --show var1
set -g var2
set --show _unset_var var2
logmsg Appending works
set -g var3a a b c
set -a var3a
set -a var3a d
set -a var3a e f
set --show var3a
set -g var3b
set -a var3b
set --show var3b
set -g var3c
set -a var3c 'one string'
set --show var3c
logmsg Prepending works
set -g var4a a b c
set -p var4a
set -p var4a d
set -p var4a e f
set --show var4a
set -g var4b
set -p var4b
set --show var4b
set -g var4c
set -p var4c 'one string'
set --show var4c
logmsg Appending and prepending at same time works
set -g var5 abc def
set -a -p var5 0 x 0
set --show var5

View file

@ -24,3 +24,62 @@ $var2: not set in local scope
$var2: set in global scope, unexported, with 0 elements
$var2: not set in universal scope
####################
# Appending works
$var3a: not set in local scope
$var3a: set in global scope, unexported, with 6 elements
$var3a[1]: length=1 value=|a|
$var3a[2]: length=1 value=|b|
$var3a[3]: length=1 value=|c|
$var3a[4]: length=1 value=|d|
$var3a[5]: length=1 value=|e|
$var3a[6]: length=1 value=|f|
$var3a: not set in universal scope
$var3b: not set in local scope
$var3b: set in global scope, unexported, with 0 elements
$var3b: not set in universal scope
$var3c: not set in local scope
$var3c: set in global scope, unexported, with 1 elements
$var3c[1]: length=10 value=|one string|
$var3c: not set in universal scope
####################
# Prepending works
$var4a: not set in local scope
$var4a: set in global scope, unexported, with 6 elements
$var4a[1]: length=1 value=|e|
$var4a[2]: length=1 value=|f|
$var4a[3]: length=1 value=|d|
$var4a[4]: length=1 value=|a|
$var4a[5]: length=1 value=|b|
$var4a[6]: length=1 value=|c|
$var4a: not set in universal scope
$var4b: not set in local scope
$var4b: set in global scope, unexported, with 0 elements
$var4b: not set in universal scope
$var4c: not set in local scope
$var4c: set in global scope, unexported, with 1 elements
$var4c[1]: length=10 value=|one string|
$var4c: not set in universal scope
####################
# Appending and prepending at same time works
$var5: not set in local scope
$var5: set in global scope, unexported, with 8 elements
$var5[1]: length=1 value=|0|
$var5[2]: length=1 value=|x|
$var5[3]: length=1 value=|0|
$var5[4]: length=3 value=|abc|
$var5[5]: length=3 value=|def|
$var5[6]: length=1 value=|0|
$var5[7]: length=1 value=|x|
$var5[8]: length=1 value=|0|
$var5: not set in universal scope

View file

@ -0,0 +1,21 @@
####################
# Test variable expand
####################
# Test variable set
####################
# Test using slices of command substitution
####################
# Test more
####################
# Verify that if statements swallow failure
####################
# Verify and/or behavior with if and while statements
####################
# Catch this corner case, which should produce an error

View file

@ -1,6 +1,6 @@
# Test index ranges
echo Test variable expand
logmsg Test variable expand
set n 10
set test (seq $n)
echo $test[1..$n] # normal range
@ -9,26 +9,26 @@ echo $test[2..5 8..6] # several ranges
echo $test[-1..-2] # range with negative limits
echo $test[-1..1] # range with mixed limits
echo Test variable set
logmsg Test variable set
set test1 $test
set test1[-1..1] $test; echo $test1
set test1[1..$n] $test; echo $test1
set test1[$n..1] $test; echo $test1
set test1[2..4 -2..-4] $test1[4..2 -4..-2]; echo $test1
echo Test using slices of command substitution
logmsg Test using slices of command substitution
echo (seq 5)[-1..1]
echo (seq $n)[3..5 -2..2]
echo Test more
logmsg Test more
echo $test[(count $test)..1]
echo $test[1..(count $test)]
# See issue 1061
echo "Verify that if statements swallow failure"
logmsg Verify that if statements swallow failure
if false ; end ; echo $status
# Verify and/or behavior with if and while statements
logmsg Verify and/or behavior with if and while statements
if false ; or true ; echo "success1" ; end
if false ; and false ; echo "failure1" ; end
while false ; and false ; or true ; echo "success2"; break ; end
@ -37,5 +37,5 @@ if false ; else if false ; and true ; else if false ; and false ; else if false;
if false ; else if false ; and true ; else if false ; or false ; else if false; echo "failure 4"; end
if false ; or true | false ; echo "failure5" ; end
# Catch this corner case, which should produce an error
logmsg Catch this corner case, which should produce an error
if false ; or --help ; end

View file

@ -1,23 +1,39 @@
Test variable expand
####################
# Test variable expand
1 2 3 4 5 6 7 8 9 10
10 9 8 7 6 5 4 3 2 1
2 3 4 5 8 7 6
10 9
10 9 8 7 6 5 4 3 2 1
Test variable set
####################
# Test variable set
10 9 8 7 6 5 4 3 2 1
1 2 3 4 5 6 7 8 9 10
10 9 8 7 6 5 4 3 2 1
10 7 8 9 6 5 2 3 4 1
Test using slices of command substitution
####################
# Test using slices of command substitution
5 4 3 2 1
3 4 5 9 8 7 6 5 4 3 2
Test more
####################
# Test more
10 9 8 7 6 5 4 3 2 1
1 2 3 4 5 6 7 8 9 10
Verify that if statements swallow failure
####################
# Verify that if statements swallow failure
0
####################
# Verify and/or behavior with if and while statements
success1
success2
success3
success4
####################
# Catch this corner case, which should produce an error