Search code examples
goterminal-emulator

How to correctly emulate terminal in linux/macOS using exex(go)?


I need to emulate a terminal in go. I try to do it like this:

lsCmd := exec.Command("bash", "-c", "ls")
lsOut, err := lsCmd.Output()
if err != nil {
    panic(err)
}
fmt.Println(string(lsOut))

And it seems to work correctly (the native ubuntu terminal displays a horizontal list, and the result of this function goes vertically).

But if I specifically call the wrong command, for example exec.Command ("bash", "-c", "lss"), I get:

panic: exit status 127

And in the native ubuntu terminal I get the following result:

Command 'lss' not found, did you mean:

and enumeration of commands.

I need to communicate with the native terminal, and get the same thing as the result of the command if I wrote the command in the standard ubuntu terminal.

What is the best way to do this? Maybe the exec library is not suitable for this? All this is necessary for front-end communication with the OS terminal. On a simple html/css/js page, the user enters a command, after go it sends it to the native terminal of the operating system and returns the result to the front-end.

How I can get the same result of executing commands as if I were working in a native terminal?


Solution

  • The problem

    But if I specifically call the wrong command, for example exec.Command ("bash", "-c", "lss"), I get:

    panic: exit status 127
    

    And in the native ubuntu terminal I get the following result:

    Command 'lss' not found, did you mean:
    

    and enumeration of commands.

    This has nothing to do with Go, and the problem is actually two-fold:

    • Ubuntu ships with a special package, command-not-found, which is usually preinstalled, which tries make terminal more friently for mere mortals by employing two techniques:

      • It tries to suggest corrections for misspellings (your case).
      • It tries to suggest packages to install when the user tries to execute a program which would have been be available if the user had a specific package installed.
    • When the command is not found, "plain" (see below) shell fails the attempt by returning a non-zero exit code.
      This is absolutely expected and normal. I mean, panicking on it is absolutely unwise.

    • There's a historical difference on how a shell is run on a Unix system.

      When a user logs into the system (remember that back in the days the concept of the shell was invented you'd be logging in via a hardware computer terminal which was basically what your GNOME Terminal window is but in hardware, and connected over a wire), the so-called login shell is started.
      The primary idea of a logic shell is to provide interactive environment for the user.

      But as you surely know, shells are also capable of executing scripts. When a shell executes a script, it's running in a non-interactive mode.

    The modes a Unix shell can work in

    Now let's dig deeper into that thing about interactive vs non-interactive shells.

    • In the interactive mode:

      • The shell is usually connected to a real terminal (either hadrware or a terminal emulator; your GNOME Terminal window is a terminal emulator).
        "Connected" means that the shell's standard I/O streams are connected to the terminal, so that what the shell prints is displayed by the terminal.
      • It enables certain bells and whistles for the user, usually providind limited means for editing what is being input (bash, for instance, engages GNU readline.
    • In the non-interactive mode:

      • The shell's standard I/O streams are connected to some files (or to "nowhere" — like /dev/null).
      • No bells and whistles are enabled — as there is nobody to make use of them.

    GNU bash is able to run in both modes, and which mode it runs in depends on how it was invoked.

    When initializing in different modes, bash reads different initialization scripts, and this explains why the machinery provided by the command-not-found package gets engaged in the interactive mode and does not when bash is run otherwise — like in your invocation from Go.

    What do do about the problem

    The simplest thing to try is to run bash with the --login command-line option or otherwise make it think it runs as an interactive shell.

    This might solve the problem for your case but not necessarily.
    The next possible problem is that some programs do really check whether they're running at a terminal — usually these are programs which insist on real interaction with the user, usually for security purposes, and there are programs which simply cannot run when not connected to a real terminal — these are "full-screen" text UI programs such as GNU Midnight Commander, Vim, Emacs, GNU Nano and anything like this.

    To solve this problem, the only solution is to run the shell in a pseudo-terminal environment, and that's what @eudore hinted at in their comment.
    The github.com/creack/pty might be a package to start looking at; golang.org/x/crypto/ssh also provides some means to wrangle PTYs.