admin管理员组

文章数量:1418017

I am writing a Go wrapper that does various things and then executes an interactive bash shell. I would like to usurp FD2 of this child process with my own thing that is fed in from the Go wrapper. That part I can do fine. I can even execute bash fine. The problem is that readline refuses to use FD1 for its stream so it ends up such that my thing on FD2 kind of 'eats' the readline output. Meaning, nothing I type comes up on the screen (no it is not terminal echo mode if that's what you're thinking) and the last line of my prompt (I have a complex two line PS1 for this) gets eaten. Clearly, it is readline. However, as I understand it, bash+readline should only ever use FD2 for its stream if stdout is detected to not be a tty (if that is incorrect, please inform me).

I am going to paste some basic code from the Go wrapper that illustrates what I am doing and this is a working example as it does not actually do the FD2 thing. This just executes bash, works great.

I've tried several different iterations on this basic theme to get my idea to work (usurping FD2). This one below, it is obvious that the process' stdout is not a TTY, yes. But even when I pass in the 'tty' directly on both stdin and stdout, same thing happens. I've also used a PTY (creack/pty package) and the same thing happens. I know the PTY is the way to do this but I am trying to keep the surface area of external dependencies as narrow as possible.

func executeScript(scriptPath string, cfg *config.Config) error {
    msgSock, err := service_util.OpenSocket("messages")
    if err != nil {
        return err
    }
    defer msgSock.Close()

    cmd := exec.Command(
        "/usr/bin/env",
        "bash",
        "-c",
        buildScriptCommand(scriptPath, cfg),
    )

    cmd.ExtraFiles = append(make([]*os.File, 31334), msgSock)

    if tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0); err == nil {
        defer tty.Close()
        cmd.SysProcAttr = &syscall.SysProcAttr{
            Setpgid:    true,
            Ctty:       int(tty.Fd()),
            Foreground: true,
        }
    }

    if len(cfg.Args) > 2 {
        cmd.Args = append(cmd.Args, cfg.Args...)
    }

    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr

    return cmd.Run()
}

Works great until I either try to set cmd.Stderr to be the msgSock, or if I try to even collapse fd1 and 2 in a bash init file while moving a different FD into place as FD2. It all has the same behavior, readline outputs to FD2 so my thing on FD2 eats it. I just want readline to output on FD1/STDOUT.

The version with the tty passed in just changed up the tty variable initialization properly in-scope and passed tty to cmd.Stdin and Stdout.

This post would be 10 pages long if I showed you everything I've tried, some of those include:

  • Executing bash differently

    If I execute bash with --noediting to disable readline it proves that readline is the culprit here, as what I type gets echo'd back. But I want readline to work for this application. I also tried non-interactive which has a similar effect. I've also stripped down the command to just be bash without the extra script stuff I am passing in. Basically no matter how I slice this pie, I'm still eating sh**.

  • PTY

    As I mentioned I have also used PTY and yes I know direct tty access is bad, please don't lecture me on this point because as far as I can tell in this instance, it's irrelevant as readline's behavior is the same. If however, you know of some magic incantation with the creack/pty library (the de facto standard afaict for go) then by all means, i am listening!

  • Fed FD2 in directly

    As mentioned before I have tried passing msgSock in directly to FD2. This is another reason I know readline itself is trying to stream to FD2- in my go code I can see the last line of the prompt and what i type coming in, which I have began printing back just to prove that.

  • Put msgSock on high FD then set it up in bash with exec

    I put msgSock on FD 31337 then did:

    exec 2>&1 2<&
    

    same difference.

  • Took a shot in the dark with .inputrc settings

    I even threw some longshot settings in inputrc trying to see if I could alter this behavior. I got nowhere with that, just like I thought I would.

I am writing a Go wrapper that does various things and then executes an interactive bash shell. I would like to usurp FD2 of this child process with my own thing that is fed in from the Go wrapper. That part I can do fine. I can even execute bash fine. The problem is that readline refuses to use FD1 for its stream so it ends up such that my thing on FD2 kind of 'eats' the readline output. Meaning, nothing I type comes up on the screen (no it is not terminal echo mode if that's what you're thinking) and the last line of my prompt (I have a complex two line PS1 for this) gets eaten. Clearly, it is readline. However, as I understand it, bash+readline should only ever use FD2 for its stream if stdout is detected to not be a tty (if that is incorrect, please inform me).

I am going to paste some basic code from the Go wrapper that illustrates what I am doing and this is a working example as it does not actually do the FD2 thing. This just executes bash, works great.

I've tried several different iterations on this basic theme to get my idea to work (usurping FD2). This one below, it is obvious that the process' stdout is not a TTY, yes. But even when I pass in the 'tty' directly on both stdin and stdout, same thing happens. I've also used a PTY (creack/pty package) and the same thing happens. I know the PTY is the way to do this but I am trying to keep the surface area of external dependencies as narrow as possible.

func executeScript(scriptPath string, cfg *config.Config) error {
    msgSock, err := service_util.OpenSocket("messages")
    if err != nil {
        return err
    }
    defer msgSock.Close()

    cmd := exec.Command(
        "/usr/bin/env",
        "bash",
        "-c",
        buildScriptCommand(scriptPath, cfg),
    )

    cmd.ExtraFiles = append(make([]*os.File, 31334), msgSock)

    if tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0); err == nil {
        defer tty.Close()
        cmd.SysProcAttr = &syscall.SysProcAttr{
            Setpgid:    true,
            Ctty:       int(tty.Fd()),
            Foreground: true,
        }
    }

    if len(cfg.Args) > 2 {
        cmd.Args = append(cmd.Args, cfg.Args...)
    }

    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr

    return cmd.Run()
}

Works great until I either try to set cmd.Stderr to be the msgSock, or if I try to even collapse fd1 and 2 in a bash init file while moving a different FD into place as FD2. It all has the same behavior, readline outputs to FD2 so my thing on FD2 eats it. I just want readline to output on FD1/STDOUT.

The version with the tty passed in just changed up the tty variable initialization properly in-scope and passed tty to cmd.Stdin and Stdout.

This post would be 10 pages long if I showed you everything I've tried, some of those include:

  • Executing bash differently

    If I execute bash with --noediting to disable readline it proves that readline is the culprit here, as what I type gets echo'd back. But I want readline to work for this application. I also tried non-interactive which has a similar effect. I've also stripped down the command to just be bash without the extra script stuff I am passing in. Basically no matter how I slice this pie, I'm still eating sh**.

  • PTY

    As I mentioned I have also used PTY and yes I know direct tty access is bad, please don't lecture me on this point because as far as I can tell in this instance, it's irrelevant as readline's behavior is the same. If however, you know of some magic incantation with the creack/pty library (the de facto standard afaict for go) then by all means, i am listening!

  • Fed FD2 in directly

    As mentioned before I have tried passing msgSock in directly to FD2. This is another reason I know readline itself is trying to stream to FD2- in my go code I can see the last line of the prompt and what i type coming in, which I have began printing back just to prove that.

  • Put msgSock on high FD then set it up in bash with exec

    I put msgSock on FD 31337 then did:

    exec 2>&1 2<&
    

    same difference.

  • Took a shot in the dark with .inputrc settings

    I even threw some longshot settings in inputrc trying to see if I could alter this behavior. I got nowhere with that, just like I thought I would.

Share Improve this question edited Jan 29 at 19:21 jonrsharpe 122k30 gold badges268 silver badges476 bronze badges asked Jan 29 at 19:11 J MJ M 2031 silver badge7 bronze badges 11
  • 2 readline should only ever use FD2 for its stream if stdout is detected to not be a tty who told you that? readline always uses stderr – oguz ismail Commented Jan 29 at 19:32
  • 3 As above -- it's 100% normal and correct to use stderr for prompts. They're "diagnostic" in nature (informing the operator on the status of the program they're running -- particularly, that it's ready for more input), and stderr is per black-letter POSIX standard where diagnostic content belongs. This is also a practical choice, because stderr is unbuffered by default whereas even when it's a TTY, stdout is line-buffered – Charles Duffy Commented Jan 29 at 19:35
  • 2 Bluntly, if you want to usurp an FD for your own purposes, I'd keep it out of the "holy trio" of 0/1/2. Take over FD 3. Take over FD 10. Take over any other FD you like. Leave the standard ones alone. – Charles Duffy Commented Jan 29 at 19:41
  • (granted, it looks like Go's os/exec isn't built to make that easy -- poor API design, that; if you compare to, say, the Python subprocess module, the latter lets one set close_fds=False to pass anything opened without the O_CLOEXEC flag through to the child)). – Charles Duffy Commented Jan 29 at 19:45
  • 2 ...well, wait -- it's there, just not as well-documented as one might hope. See ExtraFiles in cs.opensource.google/go/go/+/refs/tags/go1.23.5:src/os/exec/… to let you specify file handles to pass in additional/nonstandard descriptors. – Charles Duffy Commented Jan 29 at 19:49
 |  Show 6 more comments

1 Answer 1

Reset to default 2

This is achievable with a one-line patch to bashline.c in bash's source code:

diff --git a/bashline.c b/bashline.c
index c85b05b6..e2289e6c 100644
--- a/bashline.c
+++ b/bashline.c
@@ -456,7 +456,7 @@ initialize_readline ()

   rl_terminal_name = get_string_value ("TERM");
   rl_instream = stdin;
-  rl_outstream = stderr;
+  rl_outstream = stdout;

   /* Allow conditional parsing of the ~/.inputrc file. */
   rl_readline_name = "Bash";

of course with a few more lines this could be controllable by an environment variable. For now I am going to vendor this in my project and just make it part of my build. When I have time I will try to make something a little more robust and reach out to the maintainers to see about getting a toggle included for this.

NOTE- I have NO EARTHLY CLUE what the larger implications of doing this are!! Please do not take this as advice. I am doing something wacky that most people would never want to do.

本文标签: linuxCan39t get GNU readline with bash to use STDOUTFD1 as rloutstreamStack Overflow