mvdan / sh

A shell parser, formatter, and interpreter with bash support; includes shfmt
https://pkg.go.dev/mvdan.cc/sh/v3
BSD 3-Clause "New" or "Revised" License
7.35k stars 346 forks source link

shell buitins have no effect when running an interactive parser with a custom readline implementation #1100

Closed sweetbbak closed 1 month ago

sweetbbak commented 1 month ago

for example, if I alias a command to something else:

alias ls='echo this is an alias'

and then if I run ls the alias isn't honored, it just runs the ls binary.

$ alias ls='echo nothing'                                                                                           18:27
$ ls                                                                                                                                18:27
ash_history  bin  cmd  go.mod  go.sum  justfile  pkg
$ alias                                                                                                                           18:27
alias ls='eza'
$                                                                                                                                    18:27

This could be a result of the way I'm using this library, but I've tried every documented way to parse and run shell statements with my custom implementation of readline. This implementation is probably 3500 sloc at minimum and I can't fit it into the given parser.Interactive method.

here is an example of the implementation:

    r, err := interp.New(interp.StdIO(os.Stdin, os.Stdout, os.Stderr))
    if err != nil {
        log.Fatal(err)
    }

    parser := syntax.NewParser(syntax.Variant(syntax.LangBash))

    for {
        line, err := shell.Readline() // for this example this is a black box that returns a string
        if err == io.EOF {
            break
        } else if err != nil {
            return err
        }

        if err := parser.Stmts(strings.NewReader(line), func(stmt *syntax.Stmt) bool {
            if parser.Incomplete() {
                return true
            }

                        // pass Ctrl+C to the child process and ignore it
            ctx, cancel := context.WithCancel(context.Background())
            go func() {
                select {
                case <-signals:
                    cancel()
                    return
                case <-ctx.Done():
                    return
                }
            }()

            r.Run(ctx, stmt)
            if r.Exited() {
                return false
            }

            return true
        }); err != nil {
            log.Println(err)
        }
    }

is there some part of the API I am missing here, or is what is necessary to allow this not exposed and what could possibly be causing shell builtins not to work in this context?

exit, read -r VARNAME also doesn't work, but cd , export, pushd popd dirs work just fine.

edit: upon doing some testing I see that alias also doesn't work with gosh either, so maybe this is a known issue.

mvdan commented 1 month ago

Thanks for the report. Indeed cmd/gosh did not expand aliases by default, even though it should behave like an interactive shell. The commits above should have fixed that, although note that you will need to use the option like cmd/gosh does now.

Can you clarify what you mean by exit and read not working properly in an interactive shell? They seem to work just fine in cmd/gosh at master, at least:

$ echo $0
/usr/bin/bash
$ gosh
$ echo $0
gosh
$ read -r FOO <<EOF
> here is some text
> EOF
$ echo $FOO
here is some text
$ exit 34
$ echo $0
/usr/bin/bash
sweetbbak commented 1 month ago

I'm glad it was a simple fix. My problem is that the parser.Interactive method only allows for very rudimentary forms of line editing. So I, and the two other gosh implementations I found in the wild, were trying to re-implement parser.Interactive but with better readline capabilities:

(u-root) https://github.com/u-root/u-root/tree/main/cmds/core/gosh (gonix) https://git.mills.io/prologic/gonix/src/branch/main/internal/applets/sh/sh.go

but the problem all of these implementations run into the issues I outlined above: aliases don't expand, read -r VAR doesn't read into a variable, exit also doesn't actually exit the interactive shell and asfaik there is no real way to propagate that exit code upwards.

I made a little test repo that shows the problem and gives a way clearer idea of what I am trying to do you can check it out here https://github.com/sweetbbak/ash/blob/main/cmd/readline/main.go, or you could quick install it with this if you want to see it first hand:

go install github.com/sweetbbak/ash/cmd/readline@6615b6b5e7588118ecb081ceed14c8cc0c3aa864

but ultimately, I would like to implement sh with a way more capable version of readline that allows for ^L clearing the screen, vim + emacs mode, etc... and the given API of parser.Interactive isn't capable of doing that (as far as I know, I could be wrong here) and replicating that behavior externally causes the issues I outlined. Thanks you for the awesome library and tooling by the way, it is awe inspiring to me.

edit: my apologies, I realized that read does work and I just made a stupid mistake of not exporting the variable, I'm guessing exit can be implemented externally, and aliases are fixed so I'll just close the issue.