elvish
elvish copied to clipboard
More explicit syntax for temporary assignments
The current syntax for temporary assignments is borrowed from POSIX shell, and requires there to be no spaces surrounding a =
, e.g.
foo=bar put $foo
pwd=/tmp touch x
The syntax is quite subtle, and is a source of syntactical complexity - for example, the syntax for lvalues have to support braced lists, in order for code like this to work (suppose that f
outputs two values):
{a,b}=(f) echo $a $b
It may be a good idea to make the syntax for temporary assignments more explicit, for example using a special command with
. The examples above can be written as:
with foo = bar { put $foo }
with pwd = /tmp { touch x }
with a b = (f) { echo $a $b }
However, it's not straightforward to generalize this to multiple temporary assignments, which can be done easily using the current syntax, such as a=foo b=bar put $a $b
. Nesting with
certainly works, but is quite verbose. One possibility is to use lists to surround the individual assignments:
with [a = foo] [b = bar] { put $a $b }
In any case, the with
command is going to be more verbose than the current syntax, hence the "maybe" tag.
FWIW, the POSIX semantics for temporary assignments work by instantiating environment variables. Except when it doesn't. That is, x=y ...
is (mostly) equivalent to env x=y ...
. It's an example of a rather silly optimization to eliminate four characters from a statement. The "mostly" is parenthesized because the behavior depends on whether the command is a builtin or external. Even that distinction has subtleties. Consider the following:
bash-5.0$ WTF=wtf eval 'echo $WTF'
wtf
bash-5.0$ WTF=wtf echo $WTF
bash-5.0$ WTF=wtf typeset | grep WTF
bash-5.0$ WTF=wtf typeset -x | grep WTF
bash-5.0$ WTF=wtf eval 'typeset -x' | grep WTF
declare -x WTF="wtf"
bash-5.0$ WTF=wtf env | grep WTF
WTF=wtf
bash-5.0$ type env
env is hashed (/usr/bin/env)
Which is a long winded way of saying that any change to the syntax or behavior of temporary assignments requires careful thought so as not to repeat the mistakes of the POSIX standard.
See also, issue #705. Consider the unexpected behavior of this example:
> x = 1
> put $x
▶ 1
> x=2
> put $x
▶ 2
> x=3 true
▶ 2
The second and third "put" should have output 1
rather than 2
. If this issue were implemented that would probably resolve issue #705.
This comment is indirectly related to this issue since the problem I'm describing involves permanent, rather than temporary, assignments. Consider this pipeline:
echo a=b | cut -d = -f 1
In a POSIX shell (e.g., bash) that will output a
. In Elvish it outputs nothing because the RHS of the pipeline performs two assignments: cut = -f
and -d = 1
. That pipeline is a simplified version of a more complex pipeline. I am not arguing that the current Elvish semantics are wrong and should be changed. I'm simply pointing out that the surprising behavior of the above pipeline should be considered in resolving this issue.
Getting back to this. I'm considering stealing a design from Raku.
Introduce a special command (say tmp
) that does temporary assignment within the current scope. An example:
var x = foo
{
tmp x = bar
# $x is "bar" for the remainder of the scope
command using $x
}
# $x reverts to "foo"
The original original with
proposal had the downside that it was tricky to do multiple temporary assignments. With tmp
, just have multiple tmp
commands:
var x = foo
var y = lorem
{
tmp x = (some command)
tmp y = (some other command)
command using $x and $y
}
One-liner length comparison:
x=foo some command
{ tmp x = foo; some-command }
with x = foo { some-command }
Still more verbose than the current syntax, but same as the original with
proposal.
Also, if there is already a scope, using tmp
is actually more concise.
For example, to and run something using each subdirectory as the working directory:
for d [*/] {
tmp pwd = $d
# do things in $d
}
While the current syntax will require an additional level of nesting, unless there's only one command in the body:
for d [*/] {
pwd=$d {
# do things in $d
}
}
Another nice thing about tmp
is that it has exactly the same syntax with var
and set
(and the same length; this is why I shortened Raku's temp
to tmp
).
I like the idea, if only I could suggest let
in lieu of tmp
. It's javascript's syntax for the same construct, and I prefer it for purely ergonomic reasons. Another option is the even shorter my
from perl, again with the same behavior.
This new construct has no corresponding construct in JavaScript or Perl. It does correspond to Bash's local
declaration.
Both JavaScript's let
and Perl's my
are lexical declarations and correspond to Elvish's var
.
I don't think referencing Bash's local
keyword clarifies the matter and should be avoided. See, for example, https://stackoverflow.com/questions/4419704/differences-between-declare-typeset-and-local-variable-in-bash. The distinction between typeset
, declare
and local
is a muddle in Bash and other POSIX shells. Having said that, a focus on how the proposed tmp
keyword differs from var
for magic vars like pwd
would help. I'm basically a +1 on the proposal to introduce a tmp
keyword from a functionality perspective. The challenge will be documenting the behavior (I found the Raku document linked to above borderline incomprehensible).
I've done a little more research, and did originally misunderstand. Would tmp be dynamically scoped, like local
in perl?
var x = 1
fn foo []{
echo $x
}
fn bar []{
tmp x = 2
foo
}
foo
bar
foo
Would this print 1, 2, 1?
Would this print 1, 2, 1?
That's how I read it. But I wasn't sure, until I saw the pwd
example. And otherwise, it would be just like var
. In fact, many of the examples given would produce the same result using var
, except for what's under the hood.
@fennewald You're right.
Indeed Perl's local
does this too - it is confusingly named and Raku's name temp
better reflects what it does.
A +1 for tmp
0.18.0 will deprecate the legacy syntax.
After 0.18.0 is released, the legacy syntax will be removed. Moving this to the 0.19.0 milestone.
I think variables should be limited to their scope, unless specified otherwise, like Python's global or nonlocal.
If we support a simple scope with { ... }
which is like ( ... )
except it doesn't return the value, just like Go, then instead of
with [a = foo] [b = bar] { put $a $b }
we write
{ var a = foo; var b = bar; put $a $b }
Also maybe a new command to set multiple variables like
{ vars &a = foo &b = bar; put $a $b }
Doesn't make much different in verbosity though.
@ilius Variables in Elvish are already lexically scoped, so in { var a = foo; var b = bar; put $a $b }
, the variables $a
and $b
are not visible outside the closure. This issue was about giving an already existing variable a temporary value for a certain scope.
Anyway this has been implemented; I'll leave this open until the legacy syntax is fully removed.
I've decided to implement the originally proposed with
command as well: tmp
is technically fine, but it can sometimes feel counter-intuitive (https://github.com/elves/elvish/issues/1776#issuecomment-2245282976)