Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add function scope option to the set command #565

Closed
xiaq opened this issue Feb 7, 2013 · 27 comments · Fixed by #8145
Closed

add function scope option to the set command #565

xiaq opened this issue Feb 7, 2013 · 27 comments · Fixed by #8145

Comments

@xiaq
Copy link
Contributor

xiaq commented Feb 7, 2013

Doc of set builtin says:

The scoping rules when creating or updating a variable are:

  1. If a variable is explicitly set to either universal, global or local, that setting will be honored. If a variable of the same name exists in a different scope, that variable will not be changed.
  2. If a variable is not explicitly set to be either universal, global or local, but has been previously defined, the previous variable scope is used.
  3. If a variable is not explicitly set to be either universal, global or local and has never before been defined, the variable will be local to the currently executing function. Note that this is different from using the -l or –local flag. If one of those flags is used, the variable will be local to the most inner currently executing block, while without these the variable will be local to the function. If no function is executing, the variable will be global.

The upside is that well-known global variables ($PATH, $EDITOR, etc.) can be set within functions without an explicit -g. However, the behavior of the following function can be confusing:

function f
    set var val
end

If the user has a global variable named var, this function alters the global environment. Otherwise it has no impact on global environment. To make the function safe one has to always write

function f
    set -l var val
end

which is quite tedious. Therefore I propose that set should always imply function scope within functions.

@xiaq
Copy link
Contributor Author

xiaq commented Feb 7, 2013

Oops. Things are actually more confusing here. -l doesn't always put the variable in the function's scope, it puts it in the innermost scope.

@xiaq
Copy link
Contributor Author

xiaq commented Feb 7, 2013

Description updated. (imply -l -> imply function scope)

@JanKanis
Copy link
Contributor

JanKanis commented Feb 7, 2013

I came across this apparent inconsistency as well (see bug #482), and I considered proposing to change the scoping rules. But after thinking everything through I decided against it as I could not come up with better rules.

It also has to do with whether the shell language is optimized for writing scripts or for interactive use. Optimizing for writing scripts, I also think that the default for set should be some scope within the function rather than global or universal. But optimizing for interactive use, I think the default for a bare set within a function should be the same as set outside of a function. It should be easy to take a piece of shell code that was previously bare and wrap it in a function. I suspect that the large majority of fish functions written are small functions written by shell users to modify their environment in some way, rather than the complex functions that are part of the shell distribution.

@cben
Copy link
Contributor

cben commented May 9, 2013

I think we can still do a tiny bit better while retaining this 2 axioms:

  1. set PATH ... within a function should modify global PATH.
  2. set foo ... within a function should make foo local.

First, is block-local useful for anything?
Python only does function scope, which is more convenient because it allows stuff like:

if ...
    set foo bar
else
    set foo baz
end
use $foo ...

The only use I see for block scope is temporarily changing an env var with set -l -x but env covers 90% of that use.
So I'd just change set -l to mean function scope.
If not, let's expose function scope with set -f. People writing robust functions should have access to it without the risk of the default set foo behavior that you'd modify a global/exported foo if one exists.

Second, this dilemma where just set foo mostly works but is a little bit unsafe reminds me of bash's $foo-vs-"$foo" dilemma. Could we have a safe well-defined default?
So here is a radical idea: case sensitive default! (Somewhat inspired by Ruby)

  • set lowercase sets a function-local var.
  • set UPPERCASE sets a global [exported?] var.
  • Don't think we want a third meaning for set MixedCase. Could be a global non-exported var but I've never seen mixed case in shell scripts.
    Of course you'll still be able force any scope with a flag.

@dag
Copy link
Contributor

dag commented May 11, 2013

This is a tough one. Some observations:

  • It could be confusing if set inside a function behaved differently from outside a function.
  • But it can also be confusing that set inside a function can alter global variables.
  • Having to "declare" all your variables with set -l (one line for each!) in every function is... perfectly sane for a programming language, but a bit obnoxious for shell scripting.
  • That lengthy and involved description of the scoping rules is an indicator that the design is overly complex.
  • A special function scope that you can't ask for explicitly is troublesome.
  • Block-local scope (set -l) may or may not actually be useful in a shell.
  • Case sensitivity doesn't really work in Ruby and wouldn't really work well in a shell either, and would only make things more confusing and complex I think.

Straw man proposal: Only have one scope, "local to execution context". In a function this is function scope, outside it is global scope. set -g can then be explained as "set in the outermost execution context".

Potential problem: Arguably "context" and "scope" is the same thing and the description above is circular.

@ridiculousfish
Copy link
Member

I observe that most non-scripting programming languages solve this by separating variable assignment from variable definition. That is, separate variable creation from variable assignment. I'm not arguing that's right for fish, just that it's another possible solution.

@dag
Copy link
Contributor

dag commented May 12, 2013

Perhaps if we could set -l a,b,c, it would be bearable. That still leaves all my bullet points above except the third, though. It also means we have to decide what set -l a,b,c val does: error or set all? Probably set all would be most consistent and also useful in some cases.

Hm, actually this could be useful in general:

set -gx EDITOR,VISUAL vim

@JanKanis
Copy link
Contributor

I think looking at scripting languages is a better comparison for fish. In non-scripting languages it is generally also an error to use an undefined variable, in fish and other shells that is fine. Also, in many scripting languages assigning to an undeclared variable sets the global variable. Python is an exception to that, where an undeclared variable is by default function local, but that is arguably a better choice than what other scripting languages do.

The main reason for scoping is to allow using a variable in a function without accidentally overwriting a global variable. Block scope is very sensible for a low level language where you want to conserve stack memory usage, and there is something to say from the point of consistency with function blocks, but I don't think it adds a lot of value in a scripting language. (An exception is when closures are involved such as in Python, there block scope would imo make a lot of sense, but that doesn't matter for fish.)

My preference if I were redesigning the scoping mechanism would be to have a single scope for functions (less scopes = less confusion), and also allow top level block structures on the global level to have their own scope. That is slightly less consistent but I think it is a win for interactive usability. If you want to use a variable in a loop you type interactively on the command line you can use local variables without worrying about leaking them, and you can safely copy-paste snippets. Within a single function accidental variable clashes are not a problem.

@ridiculousfish
Copy link
Member

Block scopes interact nicely with "temporarily exporting" variables, e.g.

begin
set -lx SHELL /bin/sh
vim
end

The environment variable is automatically cleared when the scope ends.

@alphapapa
Copy link

I'm converting another Bash script to Fish, and ran into this. I used set -l in a function, and I expected it to define the variable in the function's scope. But the set -l was also inside a while loop, so it actually defined it locally to that loop.

This is very confusing. I'm no expert, but I can't think of any other languages that have block- or loop-local variables (that are defined inside the loop, I mean; of course there is e.g. for (( i=1; i<=steps; i++ )).

This is so unusual that I think it should be considered a bug and changed. Defining a variable with -l inside a function should use the function's scope. If the block-local behavior is desired, there should be a separate switch for that.

@martin-g
Copy link

Java/Scala has block level variables. JavaScript/ES2015 (aka ES6/Harmony) also supports them with let someVar = someValue.

@alphapapa
Copy link

Ok. And sure, there are lisps with things like (let .... But I think it is very unusual for a shell scripting language to behave this way. I don't think it's worth the extra confusion (and the resulting bugs). The unusual behavior should be enabled by a separate option.

@ridiculousfish
Copy link
Member

Note that the default behavior of set is function scoped. You have to opt in to block scoping with -l

@alphapapa
Copy link

Hm, I didn't realize that. That's another way that Fish somewhat confusingly differs from Bash:

#!/bin/bash

function foo {
    bar=baz
}

echo $bar
foo
echo $bar
#!/usr/bin/env fish

function foo
    set bar baz
end

echo $bar
foo
echo $bar

I'm sure you've given all this much more thought than I have, and I'm late to the party, I know--but I just wonder if these subtle-yet-significant differences are worth it. It seems like every time I convert a little Bash script to Fish, I run into some of these gotchas, and I'm not sure how the differences help me. :)

@faho
Copy link
Member

faho commented Sep 11, 2015

Hm, I didn't realize that. That's another way that Fish somewhat confusingly differs from Bash:

For those reading along at home, that code prints "" and "baz" for bash and "" and "" for fish. Essentially, both allow for a few different levels of scoping (global and local at least), only bash is global by default (change the assignment to local bar=baz to get local).

And quite frankly, basically every other programming language behaves like fish does, and the only advantage bash has is familiarity. But if we'd see familiarity as important, we wouldn't be doing fish.

Now, there's a few things to change here - I'd argue for a switch to set to force function-level scope, since now code like this:

function foo
   if true
     set bar bie
   else
     set bar drink
   end
  echo $bar
end

can't help but change the global variable bar if it is defined. This needs an additional set -l bar before the if, and it's ugly and a real stumbling block.

Either that or abolish block-level scope or always shadow variables (which seems overly restrictive) or make "capturing" global variables explicit.

@alphapapa
Copy link

Thanks for clarifying; I should have listed the output myself. :)

And quite frankly, basically every other programming language behaves like fish does, and the only advantage bash has is familiarity. But if we'd see familiarity as important, we wouldn't be doing fish.

Well, for "programming" languages you're probably right, but for shell scripting, I think the opposite is true. For example, not only Bash behaves that way, but POSIX sh does, as well as zsh. So that behavior isn't just a Bashism--it's the standard behavior for shell scripting.

Now, as you said, that doesn't necessarily mean Fish should do so as well. However, in this case, I think that Fish should follow their example.

My reasoning is this: For major, obvious differences in syntax (like using end instead of fi/done/esac/}, using set var (command) instead of var=$(command)), a new user can't help but be aware of it. If you get it wrong, the script will probably give an error as soon as you try to run it. But for variable scoping and other more subtle issues, the difference is not immediately obvious; and even if someone reads about it, it's likely he'll forget about it until he experiences the difference by falling back on old habits and causing a bug.

Now I may be wrong. :) And if someone can show me an example of why the way Fish currently does scoping is actually beneficial and outweighs the benefit of familiarity, I'd be glad. But so far, I see this current behavior as more of a problem than a benefit.

I agree with you that set -l should behave as Bash local does and define a variable local to function scope. That would be more consistent and intuitive. And then, a set -b for block-local scope would complement it nicely, similar to a let in lisps, but by being explicit it would avoid causing unintended bugs, especially for new Fish users.

I would argue against making capturing global variables explicit, because I think one of the benefits of shell scripting--especially Fish--is that it's lightweight and less verbose than "real" programming languages. For me, I have a different mental mode when doing shell scripting, and I'm used to functions having access to global scope, even though I try to avoid doing that when possible as a best practice. I think that most people who are familiar with shell scripting expect functions to have access to global scope implicitly, and I think that this is one of the kinds of behavior that Fish should emulate rather than redefine.

Thanks for all the great discussion and your work on Fish! :)

@faho
Copy link
Member

faho commented Sep 12, 2015

Now I may be wrong. :) And if someone can show me an example of why the way Fish currently does scoping is actually beneficial and outweighs the benefit of familiarity, I'd be glad. But so far, I see this current behavior as more of a problem than a benefit.

Basically, it all goes back to "structured programming" from the seventies sixties (you know, the "GOTO considered harmful" crowd, Dijkstra et al). If variables are global by default, people will use tons of global variables. And global variables are like "spaghetti code", except regarding data and not code flow. If you use them, it can be hard to see where the data stored in them comes from, especially when you're using functions written by other people - if they happen to have a global variable of the same name as one of yours (or, and this I think is a real problem, one of you does set VAR on a variable the other declared global to try to get function-level scope), unrelated functions change your data.

This is one of those things that should never have been that way in bash/sh to begin with (like word-splitting), and fish has the opportunity to change it. I think it should, even if at the expense of a bit more unfamiliarity.

I agree with you that set -l should behave as Bash local does and define a variable local to function scope. That would be more consistent and intuitive.

That might cause some breakage in existing fish code and would need a flag-day transition. That's why I advocated for a "switch" to set (though my wording wasn't great). Keep set as-is - either create a new variable with function-level scope or set the existing variable and keep the scope - and add a "-f" option to force function-level scope, shadowing other variables by the same name if necessary.

Thanks for all the great discussion and your work on Fish! :)

I believe the canonical way to express that is "So long and thanks for all the Fish!" 😄.

Edit: The part I forgot:
Of course I'm not saying that global variables are always bad, just that you should have to think about using them so you know to be careful not to step on those variables. Which is why opt-in globals are okay.

@chamini2
Copy link

Completely agree with @faho, there should be a -f option to specify a var as function scoped. As I said in #3323

... it makes sense to me that if there's a default scoping style, it should be also offered as an option in the flags. It seems weird that we get half the behaviour: looking for vars in each scope and if none were found create a new one scoped to the function; this introduces the idea of "function scoped variables" but then doesn't allow for fully using that scoping style through a flag.

Would a PR for this flag be accepted?

@alphapapa
Copy link

I agree with @chamini2 about this:

It seems weird that we get half the behaviour: looking for vars in each scope and if none were found create a new one scoped to the function;

This is a strange inconsistency: for set to implicitly set a variable in global scope, but only if that variable is already defined--otherwise it sets block-local scope. It would be much better if it were consistent and always followed the same behavior by default, requiring a flag to change it.

This is the kind of behavior that can lead to bugs that are very hard to find, because since it depends on global state (which tends to be "invisible" when coding and debugging), it's hard to reproduce in a test case unless you already know what the problem is--and then why would you need a test case? :)

Anyway, here's what I'm thinking right now. The question seems to be about variables' default scope: should it be block-local or function-local? My vote is for function-local by default, with something like set -b for explicit block-local scope. When called outside of a function, set should be global by default. Inside a function, set -g should be required to set global scope.

Then there's the matter of setting a variable within a function that is already a global variable: if set is used without -g, should the existing global variable be set, or should a function-local variable be set? I'm going to go out on a bit of a limb here and say that I think Fish should disallow this: if a user tries to set a variable inside a function that has the name of an existing global variable, Fish should throw an error. The user should use a different name for the function-local variable. The reason is to avoid a situation like this:

function fry
  echo "Frying thing: $thing"
  set thing "$thing fries"
end

set thing potato
fry
echo "Now it's $thing"

Now does fry change the value of the global thing or does it set a function-local thing, in which case the last line of the script would print "Now it's potato" instead of "Now it's potato fries"? I think the best thing to do here would be to force the user to be explicit by calling set -g inside the function, and throw an error if a bare set is used, since thing is already a global variable.

Accessing variables inside a function is another issue. Shadowing global scope by default seems to make sense since it is consistent with other shell scripting. Another option would be to explicitly capture or shadow global variables from within a function, but that seems cumbersome to me; if I'm writing a shell script, it's because I don't want to have the rigor of a "real" programming language. :)

@alphapapa
Copy link

alphapapa commented Sep 24, 2016

Ugh, I just got bitten by this again when converting a Bash script to Fish:

function convert_time_to_seconds
    set -l times $argv
    set -l fields (echo $times | tr ' ' '\n' | tr ':' ' ')

    switch (count $fields)
        case 2
            set -l hours 0
            set -l minutes $fields[1]
            set -l seconds $fields[2]
        case 3
            set -l hours $fields[1]
            set -l minutes $fields[2]
            set -l seconds $fields[3]
        case '*'
            die "Weird time from MPC:$times  Fields:$fields  Numfields:"(count $fields)
    end

    # Remove leading 0 from fields
    for field in hours minutes seconds
        echo "DEBUG: FIELD:$field VALUE:$$field"
        set -l $$field (echo $$field | sed -r 's/^0//')
    end

    echo (math "($hours * 60 * 60) + ($minutes * 60) + $seconds")
end

The error:

DEBUG: FIELD:minutes VALUE:2 18
in command substitution
        called on line 15 of file ~/.bin/wct.functions.fish

in function “convert_time_to_seconds”
        called on standard input
        with parameter list “1:21 3:53”

set: Variable name can not be the empty string
~/.bin/wct.functions.fish (line 36):         set -l $$field (echo $$field | sed -r 's/^0//')

It was non-obvious how the variable name could have been the empty string, since in the DEBUG line, $$field was not empty! (Note: This code actually contains a bug, in that set -l $$field should be set -l $field--this was my mistake that I fixed shortly afterwards, but this still illustrates how counter-intuitive the scoping issue is.)

After several minutes it hit me that the set -l statements in the switch were scoping to only within the switch block.

So to fix this, I first have to put this above the switch:

set -l hours
set -l minutes
set -l seconds

Then I can use plain set in the switch and get function-local scoping.

On top of that, in the process of debugging another issue, I found this:

$ set minutes 6
$ echo $minutes
0

This was bewildering, until I thought again of this issue. I grepped through my Fish config files and found that my prompt function was calling another function that converted seconds to hours:minutes:seconds, and that function was calling set instead of set -l, so it was polluting my session scope.

This is incredibly counter-intuitive. It really needs to be fixed.

@ridiculousfish
Copy link
Member

Thanks for the great write-ups @alphapapa

The existing default behavior where set modifies a global if one exists, otherwise it introduces a new function-scoped variable, is definitely problematic. Changing this default is difficult since it has compatibility implications. However introducing a new flag for function-scoped variables seems like something we all agree on.

@faho
Copy link
Member

faho commented Oct 23, 2016

From @krader1961 in #3465:

make it so set -f behaves like set -l outside of a function

I'm ambivalent. I agree that throwing errors is not friendly. On the other hand letting people shoot themselves in the foot is also not friendly. The -l and -g flags have well defined, fairly obvious, meanings outside of a function block. The same is not true for -f. Also, what does it mean to do set -qf outside of a function? Will it return all locally scoped vars? And if so then it's not consistent with the behavior we expect inside a function. And if not then why would should set -f be equivalent to set -l outside of a function body?

These are tough questions because they have subtle implications.

So let's first try to answer the behavior inside of a function.

The main issue for me there is that set -f inside of a function is the same as set -l unless you are also inside of a block. I.e.

function banana
    set -l flounder 0
    set -f arglebargle 1
end

Here $flounder and $arglebargle have exactly the same visibility. It's only when you do

function apple
    if true
        set -f haberdashery 2
        set -l humpty 3
     end
    # $humpty won't be defined here anymore
end

The question here is how much we want "function-scope" to be its own thing, versus just a shortcut for another scope.

  • Do local variables shadow function ones?

They probably should, so you can do easy temporary variables. Also, it's already what happens:

function a
    set -l a 1
    if true
       set -l a 0
       echo $a # will output 0
    end
    echo $a # will output 1
end
  • Do function variables count as local ones?

This is obviously a new thing because we don't currently have a way to query for function-scope.

To me the answer is no, because if you had a function var and defined a local one, it would shadow, not overwrite it. Because it is effectively its own scope, it should really count as that.

I've said before that a set -qfl might be nice here, to see if a variable is defined either in local or function scope.

With that in mind, let's try to tackle the behavior outside of a function:

The -l and -g flags have well defined, fairly obvious, meanings outside of a function block.

True.

The same is not true for -f.

Let's make one up, then. As we've seen above, we'd effectively understand function scope to be "a scope above the highest local scope up to a function". The only "change" needed here is that that function does not need to exist. So "function scope" outside of a function would be the highest local scope. It would shadow global and universal, and be shadowed by any local vars. So if you did

if true
   set -f samovar 1
end
echo $samovar

it would output 1. Which seems kind of useful.

Also, what does it mean to do set -qf outside of a function? Will it return all locally scoped vars?

Like inside of functions, it would only return true for variables defined with set -f, since they could be shadowed by locally scoped ones.

And if so then it's not consistent with the behavior we expect inside a function.

I believe it would be.

And if not then why would should set -f be equivalent to set -l outside of a function body?

That's the trick - it wouldn't be.


Now of course there's some weirdness here, since

set -l fruit banana
if true
    set -f fruit orange
end
echo $fruit

would print "banana".

Personally, I can't think of any way to make it more consistent.

Does that make sense?

@faho faho added the RFC label Oct 23, 2016
@chamini2
Copy link

I have no objections to @faho's proposal, makes sense to me. So we would be creating a new scope instead of the having an alias for a local scope.

Since with @faho's proposal this scope would be included outside of a function too, maybe the name of the scope should not be function scope but outer scope (bike-shedding, I know); apart from that, seems a good solution.

@ridiculousfish
Copy link
Member

Thanks for the great and detailed proposal faho. Also see 7b12fd2 for a relevant commit. This introduced the "top level local scope."

Let's make one up, then. As we've seen above, we'd effectively understand function scope to be "a scope above the highest local scope up to a function". The only "change" needed here is that that function does not need to exist. So "function scope" outside of a function would be the highest local scope. It would shadow global and universal, and be shadowed by any local vars

I wonder about the behavior of unadorned (no-scope-specified) set. Today it first attempts to modify an in-scope existing variable, then if it does not exist, introduces a new variable in function scope, or global scope if not in a function. This global scope behavior is very useful, since it allows users to directly set variables without worrying about scope, e.g. by typing set -x LD_LIBRARY_PATH ~/libs and this will be global.

A new "top level function scope" would force us to qualify this further: unadorned set introduces a new variable in the nearest function scope, except for the top level one. However maybe this isn't so bad if nobody will encounter it: users may never know the top level function scope exists.

Is there any good reason to run set -f at top level? I can't think of any. With that in mind perhaps it's better to make set -f error at top level. We wouldn't be preventing any legitimate use cases, and we might be saving the user from themself.

@chamini2
Copy link

chamini2 commented Nov 7, 2016

I wonder about the behavior of unadorned (no-scope-specified) set...

This was the reason this flag is even being discussed, the unadorned set
does this scoping rule not achievable through a flag, that's why we are
introducing this one.

Is there any good reason to run set -f at top level? I can't think of any. With that in mind perhaps it's better to make set -f error at top level. We wouldn't be preventing any legitimate use cases, and we might be saving the user from themself.

This makes sense to me now that I think about it. The docs could say that an unadorned set searches an existent variable in all scopes and if none are found, it creates a variable with -f option if in a function and with -g option otherwise. Keeping -f specific for functions.

If we defined a function scope outside of functions, it would be a new global scope that sometimes is in functions, adding more complexity to the scoping rules outside of a function, IMHO.

Please give your opinion so we can move forward with this! :)


Some examples

In the top level

set -f a 0  # error
set -qf     # error

In a function

function f
  set var 0
  if true
    set -f var 1
  end
  echo $var   # 1
end
function f
  set -l var 0
  if true
    set -f var 1
  end
  echo $var   # 0
end
function f
  set -l vl 0
  set -f vf 1
  set -qf     # vf
  set -ql     # vl
end

@ridiculousfish
Copy link
Member

Revisiting this case:

function f
    set -l fruit banana
    if true
        set -f fruit orange
    end
    echo $fruit #banana
end

Now the first top-level set -l looks like a footgun, forever shadowing the function-scoped variable.

If set -f only works inside functions, is there any harm in collapsing the function scope with its outermost local scope? So that set -f just means the same as set -l at top level inside the function.

There's also a question of --no-scope-shadowing functions, in particular eval. It's annoying today that eval 'set -l foo bar' doesn't work (it ends up setting foo inside the eval function). Here's an opportunity to rectify that.

@alphapapa
Copy link

alphapapa commented Nov 11, 2016

If set -f only works inside functions, is there any harm in collapsing the function scope with its outermost local scope? So that set -f just means the same as set -l at top level inside the function.

IMO it should definitely be collapsed. Having set -f within an if within a function not set the variable in the function-level scope would be extremely confusing.

And that makes me question the whole notion of set -f in the first place. If -l and -f both exist, that would imply that code written like this would somehow be sensible:

function peel_if
    set -f fruit apple
    if test (today) = tuesday
        set -l fruit banana
        if test $fruit = banana
            peel $fruit
        end
    end
end

But clearly it's not. What languages allow shadowing variables within a function's own shadowed scope, other than, e.g. Lisps, which do it explicitly with let? It's a bad idea. Just use separate variables.

Just like it's a bad idea in most languages to give variables in a for loop the same name as variables outside of the loop, it's a bad idea to shadow variables inside control blocks. They aren't anonymous functions.

Block-level-scope/shadowing is just asking for trouble. There should only be two scopes: global and function, so there should only be bare set and set -l (or set -l could be changed to set -f, which would be more descriptive). At least, I think this should be the long-term goal.

@krader1961 krader1961 changed the title set in functions should always imply function scope add function scope option to the set command Mar 13, 2017
@mqudsi mqudsi mentioned this issue Oct 3, 2017
faho added a commit to faho/fish-shell that referenced this issue Jul 15, 2021
This makes the "unspecified" scope available - the one that is used
when no variable exists.

It's either the enclosing function's topmost local scope, or global
scope if there is no function.

This removes the need to declare variables locally before use. 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.

Fixes fish-shell#565
@faho faho mentioned this issue Jul 15, 2021
3 tasks
faho added a commit to faho/fish-shell that referenced this issue Jul 23, 2021
This makes the "unspecified" scope available - the one that is used
when no variable exists.

It's either the enclosing function's topmost local scope, or global
scope if there is no function.

This removes the need to declare variables locally before use. 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.

Fixes fish-shell#565
@faho faho closed this as completed in #8145 Aug 1, 2021
faho added a commit that referenced this issue Aug 1, 2021
* 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
@zanchey zanchey modified the milestones: fish-future, fish 3.4.0 Aug 2, 2021
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 8, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging a pull request may close this issue.