diff --git a/doc_src/cmds/set.rst b/doc_src/cmds/set.rst index f92232fa2..8101e60d1 100644 --- a/doc_src/cmds/set.rst +++ b/doc_src/cmds/set.rst @@ -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. diff --git a/doc_src/language.rst b/doc_src/language.rst index 6070d42e5..02ed4e82e 100644 --- a/doc_src/language.rst +++ b/doc_src/language.rst @@ -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 `, 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. diff --git a/src/builtin_set.cpp b/src/builtin_set.cpp index b74783f18..286cd29dd 100644 --- a/src/builtin_set.cpp +++ b/src/builtin_set.cpp @@ -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::vectornext) { + 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"); } diff --git a/src/env.h b/src/env.h index bf24cbe9c..208dda14a 100644 --- a/src/env.h +++ b/src/env.h @@ -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; diff --git a/tests/checks/set.fish b/tests/checks/set.fish index e76693964..2ebf91896 100644 --- a/tests/checks/set.fish +++ b/tests/checks/set.fish @@ -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: