elves / elvish

Powerful scripting language & versatile interactive shell
https://elv.sh/
BSD 2-Clause "Simplified" License
5.65k stars 299 forks source link

Add a script file executed when Elvish is invoked as login shell #1841

Open mkalinski opened 1 week ago

mkalinski commented 1 week ago

What new feature should Elvish have?

I think Elvish should provide a script file, let's call it ~/.config/elvish/login.elv, that's executed when Elvish is invoked as login shell.

This was touched on in issues #316 and #1726, but I decided to open a new issue to point to what exactly I think is the problem, since the older issues are kind of muddled.

The reason why I think this is necessary, is because Linux is still, unfortunately, lacking a good way to set up environment variables for user session, that doesn't depend on the concept of a login shell (which is also an answer for this comment).

There is environment.d for systems that utilize systemd, but the wording in the man page seems purposefully vague on whether it will always set up the environment for all programs when you log in. It doesn't set up environment for the user session, but for "any services started by [the] service manager". Besides, the syntax supported is very limited compared to shells that provide utilities to ex. more easily handle $PATH.

Other than that, using a login shell that executes a script like ~/.profile seems to be the only reliable way to set up the environment of the user session in Linux land.

And by "environment of the user session" I don't mean "all shells started by the user", but "all programs of any kind started by the user".

One simple example: consider that a graphical environment may provide an option to set up a global keyboard shortcut to launch some command. Without being able to extend $PATH during login, so that the graphical environment inherits the value, all paths to custom user scripts would need to be typed out in full.

Elvish says that it can be set up as login shell, but as it is now it's pretty pointless, since Elvish doesn't perform the primary function of a login shell, which is to set up the environment for all programs launched within the user session.

Output of "elvish -version"

0.21.0

Code of Conduct

krader1961 commented 1 week ago

FWIW, I am a strong thumbs down on adding this feature. See my comments in #316 and #1726. Mostly because the concept of a "login" shell is fuzzy. In the distant past, when I started programming in the 1970's, a "login" shell was the program launched by the getty process after you entered your user ID and password to login to the computer. In the 1980's that was extended to the first shell launched by a X11 display manager such as XDM. Which was essentially a GUI replacement for the TUI getty program.

The main problem is that the concept of a "login" shell is now far too vague to be useful. Even when its meaning was clearer its use by shells like the Korn shell and Bash was a constant source of problems. Which caused most users to put everything in their ~/.kshrc or ~/.bashrc and never bother with ~/.bash_login, ~/.profile, or equivalent. Which is a good reason not to add that "feature" to Elvish.

I have a ~/.config/elvish/lib/env.elv module that I use env in my ~/.config/elvish/rc.elv and any non-interactive Elvish program where I want a predictable set of environment variables. That module for the most part just sets my preferred env vars regardless of the inherited environment. I do have a mechanism for telling my env.elv module not to override the existing environment but have never used it outside of explicitly testing that it works.

If you are relying on env vars being set by a file such as /etc/environment or /etc/environment.d/.conf those are already set by whatever programs launched your Elvish shell. All you need to do is test whether the relevant env var is already set before setting a default value in your ~/.config/elvish/lib/env.elv* module.

mkalinski commented 1 week ago

The main problem is that the concept of a "login" shell is now far too vague to be useful.

Respectfully, I think I described one case where it is currently useful: setting up environment variables for all programs launched in user session. Including non-terminal programs, so I can't just use env (I would have agreed it's preferable where possible).

I'm not saying a "login shell" is a good concept, but it's the one Linux world is stuck with, because it's been around forever, and no one can agree on a replacement standard, so it is what it is.

Regarding usage of environment.d, I'm not sure how much of a replacement it really is, since the manual doesn't seem to want to commit to it, instead saying "environment depends on where the program was launched from". And I also remember few years ago when ~/.pam_environment was the new place to put your user environment setup in, but it was soon deprecated. Who knows what happens with environment.d. While login shells will probably always continue to work, because they're too deep embedded in Linux legacy.

If Elvish makes an opinionated decision to not support the login shell concept, that's fine, but I think then it should not advertise setting itself as the login shell in https://elv.sh/get/default-shell.html#changing-your-login-shell

hanche commented 1 week ago

I tend to agree that this is a bad idea, but I am willing to admit that the option of knowing whether you have a “login” shell could be useful to some. So I suggest a compromise:

Make the shell's argv[0] available as a variable, safely semi-hidden in some module (os?). So now you can start rc.elv with a test for a - prefix (using str:has-prefix of course) and optionally run some code appropriate to the occasion.

Document it with a minimum of fanfare, not to lead users astray. And who knows, some intrepid users might find other uses for this?

mkalinski commented 1 week ago

I want to re-clarify that this issue isn't about detecting if you're in a login shell (#1726).

Detecting if you're in a login shell is pointless, because elvish does not execute rc.elv for non-interactive login shells.

This issue is about possibly introducing a script file separate from rc.elv that's executed in all login shells.

hanche commented 1 week ago

Oh, I see. Or rather, I don't. I had no idea such a thing as a non-interactive login shell could even exist.

mkalinski commented 1 week ago

Non-interactive login shells generally happen when you do a graphical login.

While it's certainly possible to do a graphical login without a login shell (in Wayland at least), they usually are invoked, since login shells have been the only way to set up user environment variables sice forever, and there's no agreed-on alternative so far.

krader1961 commented 1 week ago

Oh, I see. Or rather, I don't. I had no idea such a thing as a non-interactive login shell could even exist.

From the Bash man page:

   When bash is invoked as an interactive login shell, or as a non-interactive shell with the --login option, it first reads and executes
   commands from the file /etc/profile, if that file exists.  After reading that file, it looks for ~/.bash_profile, ~/.bash_login, and
   ~/.profile, in that order, and reads and executes commands from the first one that exists and is readable.  The --noprofile option may be
   used when the shell is started to inhibit this behavior.

Legacy shells like Bash and Ksh have extremely problematic behavior with regard to the login option. If you have a ~/.profile and at a later date create a ~/.bash_login your ~/.profile is silently ignored. Elvish doesn't have to suffer the same problem but adding more files that are magically sourced when Elvish starts is likely to cause problems sooner or later. Sooner if you ask what happens if you start an interactive "login" Elvish shell. Does it source both ~/.config/elvish/rc.elv and ~/.config/elvish/login.elv? In which order does it source those files? What happens when they perform incompatible initializations? Support for --login is also unnecessary. The reason ~/.config/elvish/rc.elv is useful, and therefore supported, is to support starting an interactive Elvish session when Elvish is run with no options. When running Elvish non-interactively all you have to do is tell Elvish to source ("use") the desired Elvish program.

Note that Elvish does not support the -l short option or the --login long option. So this proposal would require adding support for those options in addition to sourcing a relevant startup script. The O.P. wrote: "Elvish says that it can be set up as login shell, but as it is now it's pretty pointless". But that link refers to using Elvish as an interactive login shell and does not imply Elvish supports the --login option that might be used by programs like XDM (the X Display Manager) to start a non-interactive, login, shell. Adding support for that option is, by itself, insufficient since Elvish can't source files like /etc/profile since Elvish is not a POSIX 1003.1 compatible shell.

@mkalinski You wrote:

One simple example: consider that a graphical environment may provide an option to set up a global keyboard shortcut to launch some command. Without being able to extend $PATH during login, so that the graphical environment inherits the value, all paths to custom user scripts would need to be typed out in full.

That is not a simple example; or, at least, lacks sufficient detail to understand what you attempted. If you provide more details about your system and how you attempted to replace a POSIX shell (e.g., Bash) with Elvish in your "graphical environment" we can provide better feedback. Including possibly agreeing that Elvish should support the -l and --login options which trigger implicit sourcing of a new Elvish startup file.

mkalinski commented 6 days ago

But that link refers to using Elvish as an interactive login shell and does not imply Elvish supports the --login option that might be used by programs

The docs only say:

On Unix systems, you can also use Elvish as your login shell

It doesn't say anything about "interactive", nor does it define what a "login shell" is, but all I can say is that all other Unix-y shells that I know of support some way of executing a script at login, so I think this is a natural expectation, when the docs lack any further explanation.

That is not a simple example; or, at least, lacks sufficient detail to understand what you attempted. If you provide more details about your system and how you attempted to replace a POSIX shell (e.g., Bash) with Elvish in your "graphical environment" we can provide better feedback. Including possibly agreeing that Elvish should support the -l and --login options which trigger implicit sourcing of a new Elvish startup file.

I'm not sure if any concrete details I can provide will be very useful, but sure.

I'm normally logging in via GNOME desktop environment on Wayland. It does start the session within a login shell.

I have a directory, ~/.local/bin, where I put personal scripts that I don't want to install for the whole system. This directory isn't on $PATH by default, so I want to add it. I'm using fish as my login shell, so in config.fish I have:

if status is-login
   fish_add_path -gP ~/.local/bin
end

This prepends the directory to $PATH. Now it's available in all programs.

I have a script in ~/.local/bin that puts current date into my clipboard. I use it via a global keyboard shortcut, because it's convenient. It's set up in GNOME Settings application like this:

Screenshot from 2024-09-25 21-10-34

If I didn't add ~/.local/bin to $PATH in login shell, I would have to write /home/<username>/.local/bin/my-clipboard-put-date.

This is just one case. I have more scripts in that directory, that I have to reference in other places in GUI applications.

I also use other exported variables from login shell, but I wanted to focus in this example on what I think is the most common reason for people to have login scripts: extending $PATH in a way that's detectable for all programs.

xiaq commented 5 days ago

@krader1961 Please don't be combative in the issue tracker. The Elvish community is about helping people do what they want to do with Elvish, not about proving that one way is better than another. (This is a formal warning from me as the moderator.)

xiaq commented 5 days ago

@mkalinski It would be useful to know how exactly the GNOME session invokes Elvish. Since it's not an interactive session, it must be either passing commands via -c or the path to a script. Since Elvish is non-POSIX, either the literal commands or the script must be simple enough to fall in the small intersection between POSIX and Elvish...

Can you find the full command line of Elvish invocation made by the GNOME session? For example, if you run pstree in a terminal, do you see an Elvish invocation fairly close to the root?

krader1961 commented 5 days ago

See https://www.reddit.com/r/linuxquestions/comments/1dd9xwi/why_does_gnomesession_wayland_start_a_login_shell/ for an explanation why Elvish is not a good fit as a "login" shell In the context of the Gnome Wayland ecosystem. The short version is the Gnome Wayland desktop environment assumes it can reinvoke the users default shell as a "login" shell (by prefacing the path to the user's default shell with a hyphen in the argv[0] value passed to the shell) for the side-effect of causing the shell to evaluate the relevant startup scripts (e.g., ~/.bash_login) before the shell enters its non-interactive REPL loop.

This caused me to notice that Elvish has a related, but unexpected, behavior. Compare the behavior of Bash and Elvish:

elvish> echo 'echo yes' | bash
yes
elvish> echo 'echo yes' | elvish
Multiple compilation errors:
  no variable $edit:insert:binding
    /Users/krader/.config/elvish/rc.elv:105:9-37:     del edit:insert:binding[$pair[0]]
... [lots of errors elided]
  cannot find variable $edit:location:pinned
    /Users/krader/.config/elvish/rc.elv:226:9-28:     set edit:location:pinned = [$pwd $@prev]
/Users/krader> yes
/Users/krader> ⏎

I don't see any alternative but to implement support, more or less as proposed by @mkalinski, if Elvish is intended to be usable as the users "default" shell as defined by their user profile. I never noticed this shortcoming because I either did not set Elvish as my default shell, or when I did so I didn't use it in a manner that depended on side-effects of it being run as a "login" shell. I would also recommend exposing the argv[0] string via an Elvish builtin var.

krader1961 commented 5 days ago

There are obviously many questions related to adding support for Elvish as a "login" shell. Starting with whether it should support the --login and -l options. More importantly legacy POSIX 1003 shells evaluate /etc/profile when invoked as a "login"shell but Elvish can't evaluate /etc/profile since it isn't a POSIX 1003 shell. Does that mean Elvish can't be a login shell? Is it good enough to document that Elvish users have to copy whatever initialization they care about that is done by a "login" shell from /etc/profile to a private Elvish config script?

P.S., This is the /etc/profile content on my primary macOS server:

# System-wide .profile for sh(1)

if [ -x /usr/libexec/path_helper ]; then
    eval `/usr/libexec/path_helper -s`
fi

if [ "${BASH-no}" != "no" ]; then
    [ -r /etc/bashrc ] && . /etc/bashrc
fi

Using Elvish as a "login" shell would mean it has to duplicate whatever /usr/libexec/path_helper -s does for full fidelity with whatever a legacy POSIX 1003 shell does. Which is probably unreasonable. This is a situation where, due to breaking with the past, it may not be possible to substitute Elvish for a legacy POSIX 1003 shell like Bash. Not unless users are willing to deal with incompatibilities like the aforementioned inability to execute the /etc/profile script.

krader1961 commented 5 days ago

I'll also note that my "Mostly because the concept of a "login" shell is fuzzy." comment was ill advised. In the context of this issue a "login" shell is not fuzzy. Specifically, it means a program invoked with a argv[0] value whose first byte is a hyphen (-); e.g., -/bin/bash. Without regard for why the parent process (e.g., getty) invoked the shell as a "login" shell. My "fuzzy" comment was mostly directed at humans who don't understand the difference between a "login" shell and a user's default shell.

mkalinski commented 5 days ago

Can you find the full command line of Elvish invocation made by the GNOME session? For example, if you run pstree in a terminal, do you see an Elvish invocation fairly close to the root?

I can show you how exactly it looks in gnome-session code:

exec bash -c "exec -l '$SHELL' -c '$0 -l $*'"

This convoluted invocation means that, if $SHELL == /usr/bin/elvish, it's executed like:

/usr/bin/elvish -c 'gnome-session -l (whatever other args gnome passes to itself)'

Bash's exec -l prepends a - to the child process's argv[0], so elvish would see itself as -/usr/bin/elvish. The leading hyphen in argv[0] is the standard way to to detect if the application is the login shell, like @krader1961 said, supporting -l / --login is irrelevant for this purpose (though most shells do, for user convenience). Curiously, GNOME assumes that the shell will always support the -c option, which elvish fortunately does.

More importantly legacy POSIX 1003 shells evaluate /etc/profile when invoked as a "login"shell but Elvish can't evaluate /etc/profile since it isn't a POSIX 1003 shell. Does that mean Elvish can't be a login shell? Is it good enough to document that Elvish users have to copy whatever initialization they care about that is done by a "login" shell from /etc/profile to a private Elvish config script?

I concede that setting a non-POSIX shell as the login shell can lead to unexpected problems, since there can always be applications that assume that $SHELL must be POSIX (https://elv.sh/get/default-shell.html#changing-your-login-shell lists a few). But obviously, if a shell bills itself as non-POSIX, I don't think anyone reasonable would expect it to also source POSIX initialization files.

This is a situation where, due to breaking with the past, it may not be possible to substitute Elvish for a legacy POSIX 1003 shell like Bash. Not unless users are willing to deal with incompatibilities

This ship has already sailed, when Elvish docs already show how to set it as the login shell, which already leads to incompatibilities.

My primary inspiration for this proposal is the fish shell, which has a completely non-POSIX configuration files hierarchy and seems to get away with it. I've been using it as a login shell for years and never had any issue.

Regarding files like /etc/profile: if elvish provides an alternative configuration path, ex. /etc/elvish/login.elv, then I think it's the responsibility of maintainers of elvish packages in various distros to setup default configuration that works in their system as well as bash.

Out of curiosity I checked, and on my own system, /etc/profile is even more complex than @krader1961's. And /etc/fish/config.fish is empty. I never noticed any issues because of /etc/profile never being sourced during my session.

xiaq commented 1 day ago

Hmm OK. Elvish does support -l, but it's currently a no-op.

I took a look at that script from gnome-session - this is the full source I believe and it's doing some quite convoluted things... But it is what it is, @mkalinski thanks for the analysis. I see that there's no way to make this work short of implementing your original proposal of executing a login.elv somewhere when Elvish is the login shell.

I'm thinking doing this in a slightly unusual way: execute login.elv, but throw away its namespace at the end. That is, if you have a login.elv that contains:

set E:FOO = bar
fn lorem { ... }

And if you run elvish -l, the environment variable $E:FOO will be set, but you don't get access to the lorem function.

This is to ensure that script executed as elvish -c code or elvish script.elv still have access to a namespace that doesn't depend on any user customization when you add -l.