Add set --function (#8145)

* Add `set --function`

This makes the function's scope available, even inside of blocks. Outside of blocks it's the toplevel local scope.

This removes the need to declare variables locally before use, and will probably end up being the main way variables get set.

E.g.:

```fish
set -l thing
if condition
    set thing one
else
    set thing two
end
```

could be written as

```fish
if condition
    set -f thing one
else
    set -f thing two
end
```

Note: Many scripts shipped with fish use workarounds like `and`/`or`
instead of `if`, so it isn't easy to find good examples.

Also, if there isn't an else-branch in that above, just with

```fish
if condition
    set -f thing one
end
```

that means something different from setting it before! Now, if
`condition` isn't true, it would use a global (or universal) variable of
te same name!

Some more interesting parts:

Because it *is* a local scope, setting a variable `-f` and
`-l` in the toplevel of a function ends up the same:

```fish
function foo2
    set -l foo bar
    set -f foo baz # modifies the *same* variable!
end
```

but setting it locally inside a block creates a new local variable
that shadows the function-scoped variable:

```fish
function foo3
    set -f foo bar
    begin
        set -l foo banana
        # $foo is banana
    end
    # $foo is bar again
end
```

This is how local variables already work. "Local" is actually "block-scoped".

Also `set --show` will only show the closest local scope, so it won't
show a shadowed function-level variable. Again, this is how local
variables already work, and could be done as a separate change.

As a fun tidbit, functions with --no-scope-shadowing can now use this to set variables in the calling function. That's probably okay given that it's already an escape hatch (but to be clear: if it turns out to problematic I reserve the right to remove it).

Fixes #565
This commit is contained in:
Fabian Homborg 2021-08-01 20:08:12 +02:00 committed by GitHub
parent 66709571ed
commit 733114fefb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 149 additions and 28 deletions

View File

@ -33,9 +33,11 @@ With ``--show``, ``set`` will describe the given variable names, explaining how
The following options control variable scope:
- ``-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
- ``-f`` or ``--function`` scopes the variable to the currently executing function. It is erased when the function ends.
- ``-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
- ``-l`` or ``--local`` scopes the variable to the currently executing block. It is erased when the block ends. Outside of a block, this is the same as ``--function``.
- ``-g`` or ``--global`` causes the specified shell variable to be given a global scope. Global variables don't disappear and are available to all functions running in the same shell. They can even be modified.
- ``-U`` or ``--universal`` causes the specified shell variable to be given a universal scope. If this option is supplied, the variable will be shared between all the current user's fish instances on the current computer, and will be preserved across restarts of the shell.

View File

@ -878,19 +878,20 @@ So you set a variable with ``set``, and use it with a ``$`` and the name.
Variable scope
^^^^^^^^^^^^^^
There are three kinds of variables in fish: universal, global and local variables.
There are four kinds of variables in fish: universal, global, function and local variables.
- Universal variables are shared between all fish sessions a user is running on one computer.
- Global variables are specific to the current fish session, and will never be erased unless explicitly requested by using ``set -e``.
- Local variables are specific to the current fish session, and associated with a specific block of commands, and automatically erased when a specific block goes out of scope. A block of commands is a series of commands that begins with one of the commands ``for``, ``while`` , ``if``, ``function``, ``begin`` or ``switch``, and ends with the command ``end``.
- Function variables are specific to the currently executing function. They are erased ("go out of scope") when the current function ends.
- Local variables are specific to the current block of commands, and automatically erased when a specific block goes out of scope. A block of commands is a series of commands that begins with one of the commands ``for``, ``while`` , ``if``, ``function``, ``begin`` or ``switch``, and ends with the command ``end``. Outside of a block, this is the same as the function scope.
Variables can be explicitly set to be universal with the ``-U`` or ``--universal`` switch, global with the ``-g`` or ``--global`` switch, or local with the ``-l`` or ``--local`` switch. The scoping rules when creating or updating a variable are:
Variables can be explicitly set to be universal with the ``-U`` or ``--universal`` switch, global with ``-g`` or ``--global``, function-scoped with ``-f`` or ``--function`` and local to the current block with ``-l`` or ``--local``. The scoping rules when creating or updating a variable are:
- When a scope is explicitly given, it will be used. If a variable of the same name exists in a different scope, that variable will not be changed.
- When no scope is given, but a variable of that name exists, the variable of the smallest scope will be modified. The scope will not be changed.
- As a special case, when no scope is given and no variable has been defined the variable will belong to the scope of the currently executing *function*. This is different from the ``--local`` flag, which would make the variable local to the current *block*.
- When no scope is given and no variable of that name exists, the variable is created in function scope if inside a function, or global scope if no function is executing.
There can be many variables with the same name, but different scopes. When you :ref:`use a variable <expand-variable>`, the smallest scoped variable of that name will be used. If a local variable exists, it will be used instead of the global or universal variable of the same name.
@ -908,6 +909,16 @@ Typically inside funcions you should use local scope::
end
end
# or
function something
if test -e /path/to/my/file
set -f file /path/to/my/file
else
set -f file /path/to/my/otherfile
end
end
If you want to set something in config.fish, or set something in a function and have it available for the rest of the session, global scope is a good choice::
# Don't shorten the working directory in the prompt
@ -930,16 +941,24 @@ If you want to set some personal customization, universal variables are nice::
Here is an example of local vs function-scoped variables::
begin
# This is a nice local scope where all variables will die
set -l pirate 'There be treasure in them thar hills'
set captain Space, the final frontier
end
function test-scopes
begin
# This is a nice local scope where all variables will die
set -l pirate 'There be treasure in them thar hills'
set -f captain Space, the final frontier
# If no variable of that name was defined, it is function-local.
set gnu "In the beginning there was nothing, which exploded"
end
echo $pirate
# This will not output anything, since the pirate was local
echo $captain
# This will output the good Captain's speech since $captain had function-scope.
echo $pirate
# This will not output anything, since the pirate was local
echo $captain
# This will output the good Captain's speech since $captain had function-scope.
echo $gnu
# Will output Sir Terry's wisdom.
end
When in doubt, use function-scoped variables. When you need to make a variable accessible everywhere, make it global. When you need to persistently store configuration, make it universal. When you want to use a variable only in a short block, make it local.
.. _variables-override:
@ -1029,7 +1048,7 @@ Variables can be explicitly set to be exported with the ``-x`` or ``--export`` s
- Otherwise, by default, the variable will not be exported.
- If a variable has local scope and is exported, any function called receives a *copy* of it, so any changes it makes to the variable disappear once the function returns.
- If a variable has function or local scope and is exported, any function called receives a *copy* of it, so any changes it makes to the variable disappear once the function returns.
- Global variables are accessible to functions whether they are exported or not.

View File

@ -33,6 +33,7 @@ struct set_cmd_opts_t {
bool print_help = false;
bool show = false;
bool local = false;
bool function = false;
bool global = false;
bool exportv = false;
bool erase = false;
@ -57,9 +58,10 @@ enum {
// 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 wchar_t *const short_options = L"+:LSUaefghlnpqux";
static const struct woption long_options[] = {
{L"export", no_argument, nullptr, 'x'}, {L"global", no_argument, nullptr, 'g'},
{L"function", no_argument, nullptr, 'f'},
{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'},
@ -94,6 +96,10 @@ static int parse_cmd_opts(set_cmd_opts_t &opts, int *optind, //!OCLINT(high ncs
opts.preserve_failure_exit_status = false;
break;
}
case 'f': {
opts.function = true;
break;
}
case 'g': {
opts.global = true;
break;
@ -185,7 +191,7 @@ static int validate_cmd_opts(const wchar_t *cmd,
}
// Variables can only have one scope.
if (opts.local + opts.global + opts.universal > 1) {
if (opts.local + opts.function + 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;
@ -214,7 +220,7 @@ static int validate_cmd_opts(const wchar_t *cmd,
// 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)) {
(opts.local || opts.function || 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;
@ -393,6 +399,7 @@ static wcstring_list_t erased_at_indexes(wcstring_list_t input, std::vector<long
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.function) scope |= ENV_FUNCTION;
if (opts.global) scope |= ENV_GLOBAL;
if (opts.exportv) scope |= ENV_EXPORT;
if (opts.unexport) scope |= ENV_UNEXPORT;

View File

@ -472,8 +472,9 @@ struct query_t {
// Whether any scopes were specified.
bool has_scope;
// Whether to search local, global, universal scopes.
// Whether to search local, function, global, universal scopes.
bool local;
bool function;
bool global;
bool universal;
@ -493,8 +494,9 @@ struct query_t {
bool user;
explicit query_t(env_mode_flags_t mode) {
has_scope = mode & (ENV_LOCAL | ENV_GLOBAL | ENV_UNIVERSAL);
has_scope = mode & (ENV_LOCAL | ENV_FUNCTION | ENV_GLOBAL | ENV_UNIVERSAL);
local = !has_scope || (mode & ENV_LOCAL);
function = !has_scope || (mode & ENV_FUNCTION);
global = !has_scope || (mode & ENV_GLOBAL);
universal = !has_scope || (mode & ENV_UNIVERSAL);
@ -1190,6 +1192,22 @@ mod_result_t env_stack_impl_t::set(const wcstring &key, env_mode_flags_t mode,
} else if (query.local) {
assert(locals_ != globals_ && "Locals should not be globals");
set_in_node(locals_, key, std::move(val), flags);
} else if (query.function) {
// "Function" scope is:
// Either the topmost local scope of the nearest function,
// or the top-level local scope if no function exists.
//
// This is distinct from the unspecified scope,
// which is the global scope if no function exists.
auto node = locals_;
while (node->next) {
node = node->next;
// The first node that introduces a new scope is ours.
// If this doesn't happen, we go on until we've reached the
// topmost local scope.
if (node->new_scope) break;
}
set_in_node(node, key, std::move(val), flags);
} else {
DIE("Unknown scope");
}

View File

@ -26,23 +26,24 @@ enum {
ENV_DEFAULT = 0,
/// Flag for local (to the current block) variable.
ENV_LOCAL = 1 << 0,
ENV_FUNCTION = 1 << 1,
/// Flag for global variable.
ENV_GLOBAL = 1 << 1,
ENV_GLOBAL = 1 << 2,
/// Flag for universal variable.
ENV_UNIVERSAL = 1 << 2,
ENV_UNIVERSAL = 1 << 3,
/// Flag for exported (to commands) variable.
ENV_EXPORT = 1 << 3,
ENV_EXPORT = 1 << 4,
/// Flag for unexported variable.
ENV_UNEXPORT = 1 << 4,
ENV_UNEXPORT = 1 << 5,
/// Flag to mark a variable as a path variable.
ENV_PATHVAR = 1 << 5,
ENV_PATHVAR = 1 << 6,
/// Flag to unmark a variable as a path variable.
ENV_UNPATHVAR = 1 << 6,
ENV_UNPATHVAR = 1 << 7,
/// Flag for variable update request from the user. All variable changes that are made directly
/// by the user, such as those from the `read` and `set` builtin must have this flag set. It
/// serves one purpose: to indicate that an error should be returned if the user is attempting
/// to modify a var that should not be modified by direct user action; e.g., a read-only var.
ENV_USER = 1 << 7,
ENV_USER = 1 << 8,
};
typedef uint32_t env_mode_flags_t;

View File

@ -732,3 +732,77 @@ begin
echo $CDPATH
# CHECK: . /usr
end
# Function scope:
set -f actuallystilllocal "this one is still local"
set -ql actuallystilllocal
and echo "Yep, it's local"
# CHECK: Yep, it's local
set -S actuallystilllocal
#CHECK: $actuallystilllocal: set in local scope, unexported, with 1 elements
#CHECK: $actuallystilllocal[1]: |this one is still local|
# Blocks aren't functions, "function" scope is still top-level local:
begin
set -f stilllocal "as local as the moon is wet"
echo $stilllocal
# CHECK: as local as the moon is wet
end
set -S stilllocal
#CHECK: $stilllocal: set in local scope, unexported, with 1 elements
#CHECK: $stilllocal[1]: |as local as the moon is wet|
set -g globalvar global
function test-function-scope
set -f funcvar "function"
echo $funcvar
# CHECK: function
set -S funcvar
#CHECK: $funcvar: set in local scope, unexported, with 1 elements
#CHECK: $funcvar[1]: |function|
begin
set -l funcvar "block"
echo $funcvar
# CHECK: block
set -S funcvar
#CHECK: $funcvar: set in local scope, unexported, with 1 elements
#CHECK: $funcvar[1]: |block|
end
echo $funcvar
# CHECK: function
begin
set -f funcvar2 "function from block"
echo $funcvar2
# CHECK: function from block
set -S funcvar2
#CHECK: $funcvar2: set in local scope, unexported, with 1 elements
#CHECK: $funcvar2[1]: |function from block|
end
echo $funcvar2
# CHECK: function from block
set -S funcvar2
#CHECK: $funcvar2: set in local scope, unexported, with 1 elements
#CHECK: $funcvar2[1]: |function from block|
set -l fruit banana
if true
set -f fruit orange
end
echo $fruit #orange
# function scope *is* the outermost local scope,
# so that `set -f` altered the same funcvariable as that `set -l` outside!
# CHECK: orange
set -f globalvar function
set -S globalvar
#CHECK: $globalvar: set in local scope, unexported, with 1 elements
#CHECK: $globalvar[1]: |function|
#CHECK: $globalvar: set in global scope, unexported, with 1 elements
#CHECK: $globalvar[1]: |global|
end
test-function-scope
echo $funcvar $funcvar2
# CHECK: