rakitzis / rc

rc shell -- independent re-implementation for Unix of the Plan 9 shell (from circa 1992)
Other
250 stars 23 forks source link

rc and the POSIX environment #106

Closed BourgeoisBear closed 2 weeks ago

BourgeoisBear commented 3 weeks ago

I'm not sure if these are actual issues, or user-errors, but they are issues I've encountered when using rc as my login shell:

Scenario 1: Interactive Shell Under X11

X sources a number of scripts (/etc/profile, /etc/profile.d/*, $HOME/.profile, /etc/X11/Xsession, /etc/X11/Xsession.d/*) under the /bin/sh interpreter. These generate an environment that rc inherits when run interactively. However, since rc only autoloads a script (.rcrc) when run as a login shell, rc-specific definitions have to be sourced manually.

Result: System & user environments initialized, rc-specific definitions missing.

Possible solution: Just as bash has .bashrc and .bash_profile, and zsh has .zshrc and .zprofile, have a non-login shell autoscript for rc (.rc_interactive perhaps?).

Scenario 2: Login Shell

Logging in via SSH or Linux console invokes rc as a login shell. .rcrc is sourced, but system and user init scripts from the aforementioned paths are not sourced, and are not source-able from rc due to language difference.

Result: Outside of PAM/auth vars, system environment is largely missing, but rc-specific definitions are sourced.

Possible Solution: First snag & import environment vars from /bin/sh -l when running as a login shell. This is what I've done in my config, but having to copy paste 34 lines of rc script to preserve your POSIX environment doesn't feel right.

Here is my amateur version of this solution:

# get login shell env vars first
nullchar = `{/usr/bin/printf '\000'}
lstEnv = `` $nullchar { \
    env - HOME=`pwd /usr/bin/sh -l -c '/usr/bin/env --null' \
}

fn _join {
    joiner=$1
    shift
    while () {
        echo -n $1; shift
        ~ $#* 0 && break
        echo -n $joiner
    }
    joiner=()
}

fn _envAssign {
    vname = $1
    shift
    vval = `{_join '=' $*}
    # only import vars where name starts with uppercase alpha
    if ( echo -n $vname | /usr/bin/grep --no-ignore-case --quiet '^[A-Z]' ) {
        eval $vname'=$vval'
    }
    vname = ()
    vval = ()
}

# import bin/sh login vars into rc environment
for(line in $lstEnv) {
    _envAssign `` ('=') { echo -n $line }
}
line = ()
rakitzis commented 3 weeks ago

A couple of questions: did you try starting rc as "rc -l" for each xterm? Not a perfect solution, but an adequate workaround. I do something similar on MacOS Terminal.

As for inheriting variables, did you try this one-liner? It may just work:

eval `{bash -l -c 'rc -c whatis'}

rc reports its environment variables quote-escaped, so it should be able to inherit from bash and then re-evaluate the assignments from backquote expansion. Try it?

BourgeoisBear commented 3 weeks ago

xterm has the XTerm*loginShell option for automatically forcing a login term, but I use st, which lacks that option.

The one-liner is almost there, but seems to drop newlines. That was why I went through the trouble of parsing env --null.

rakitzis commented 3 weeks ago

Yes, ifs will get in your way.

BourgeoisBear commented 3 weeks ago

Would you consider a pull request for an .rc_interactive feature, or is this a not broke / don't fix?

rakitzis commented 3 weeks ago

One more suggestion: you can overload fn prompt to run stuff in an interactive context. Something like (untested)

fn prompt {
  if (!~ $interactive_init $pid) {
    # initialize stuff
    . $HOME/.rcrc
    # synchronize state
    interactive_init=$pid
  }
}
BourgeoisBear commented 3 weeks ago

But how do you get fn prompt automatically sourced in the interactive context?

rakitzis commented 3 weeks ago

Shell functions are exported in the environment. You should be able to paste the quoted form into your bash profile.

export fn_prompt='{rc stuff}'
BourgeoisBear commented 3 weeks ago

Relying upon bash to source rc funcs seems a bit wacky to me. fn prompt will also have to be redefined in .rcrc to configure the prompt under a login shell, which does not source the sh/bash environment.

rakitzis commented 2 weeks ago

I think it's actually the same shell function regardless. You can redefine it harmlessly. Exported to the environment from sh it doesn't really look wacky:

$ export fn_prompt='{if(!~ $interactive_init $pid){. $HOME^/.rcinit;interactive_init=$pid}}'

Closing as "won't fix" -- there's a super high bar for new features.

BourgeoisBear commented 2 weeks ago

Thanks for the troubleshooting. As someone who has already been to this rodeo, do you think something like Go would be a decent language for writing a shell, or would the runtime just get in the way?

rakitzis commented 2 weeks ago

It may be worth experimenting with Go. It's a powerful language. But it doesn't support fork(2) out of the box (only fork+exec) so the runtime in that sense will get in the way. But maybe a new shell language which doesn't have the same sub-shell semantics of the common Unix shells would be doable in Go. Worth a try for sure.

rakitzis commented 2 weeks ago

Start here: https://github.com/mvdan/sh. I haven't tried it, but it's a POSIX shell written in Go.

BourgeoisBear commented 2 weeks ago

I've looked at that one. It's very clean. Though it seems more like a testbed for the parser than a production shell.

I guess it's safe to assume that fork + exec is going to be more expensive than fork + twiddle a few bits + continue? They expose the syscall, but that is probably asking for trouble when the runtime gets partially-forked alongside everything else. exec.Cmd might be good enough though, and would be somewhat cross-platform for free. Haven't been a unix user long enough to imagine cases where the fork() behavior is indispensable, so may end up painting myself into a corner on that one if there is some common case where it is.

Here are the built-ins he's implemented so far if you're interested:

https://github.com/mvdan/sh/blob/7bd422f92937020ddf4519e47f8ea1bbf815a6d5/interp/builtin.go#L55

rakitzis commented 2 weeks ago

Fork+exec is the standard pattern for running a new command in the Unix environment. The shell is an exception. It allows fine control over subshells, which as you say are "fork + continue". The difference is not performance but rather things like the semantics of signal delivery. I'm afraid the shell is very tightly coupled to the syscall API surface area. By "emulating" that with a Go program using fork+exec and threads for everything else, you may indeed paint yourself into a corner.

Or perhaps not. If you forgo POSIX compliance, maybe you end up with a useful and usable shell. I have my doubts (without having tried to make anything like this myself) about making a true POSIX-compliant shell with Go.