ARTICLE AD BOX
I am trying to spawn a process so that I can do the following:
Send text to its standard input and specify EOF by closing the stdin pipe.
Connect the process' stdout and stderr to a TTY device so its behavior is identical to when it is run inside a TTY.
Initially, I launched the process in a pty using creack/pty in Go, but I soon realized that there's no way to 'send' EOF. I know that EOF is neither a signal nor a character, so it can't be sent.
Take for example, the highlighting behavior of grep. I want grep to behave as if it's being run in a TTY (without specifying the --color flag). For that, I want its stdou to be connected to a TTY device, not a pipe. But at the same time, I wish to send input to grep using a Pipe so that i can close the pipe and specify EOF to grep.

Edit: I was able to achieve this in C using the following piece of code:
#define _XOPEN_SOURCE 600 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/wait.h> #include <string.h> #include <pty.h> int main() { int pipe_fd[2]; int master_fd, slave_fd; pid_t pid; // Create pipe for stdin if (pipe(pipe_fd) == -1) { perror("pipe"); exit(1); } // Create PTY for stdout/stderr // This makes grep think it's running in a terminal, enabling color output if (openpty(&master_fd, &slave_fd, NULL, NULL, NULL) == -1) { perror("openpty"); exit(1); } pid = fork(); if (pid == -1) { perror("fork"); exit(1); } if (pid == 0) { // Child process // Close unused pipe end (write end) close(pipe_fd[1]); // Redirect stdin to pipe read end dup2(pipe_fd[0], STDIN_FILENO); close(pipe_fd[0]); // Close master PTY (parent uses this) close(master_fd); // Redirect stdout and stderr to slave PTY dup2(slave_fd, STDOUT_FILENO); dup2(slave_fd, STDERR_FILENO); close(slave_fd); // Execute grep with -P flag for Perl regex execlp("grep", "grep", "--color=auto" , "-P", "hello", NULL); // If exec fails perror("execlp"); exit(1); } else { // Parent process // Close unused pipe end (read end) close(pipe_fd[0]); // Close slave PTY (child uses this) close(slave_fd); // Write "hello world" to grep's stdin (emulating: echo "hello world" | grep -P "hello") const char *test_data = "hello world\n"; write(pipe_fd[1], test_data, strlen(test_data)); close(pipe_fd[1]); // Close pipe to signal EOF - grep will exit cleanly // Read grep's output from master PTY // This will include ANSI color codes if grep detects a TTY char buffer[1024]; ssize_t n; printf("=== Grep output (with color codes if terminal detected) ===\n"); while ((n = read(master_fd, buffer, sizeof(buffer) - 1)) > 0) { buffer[n] = '\0'; printf("%s", buffer); } close(master_fd); // Wait for child to finish int status; waitpid(pid, &status, 0); if (WIFEXITED(status)) { printf("=== grep exited with status %d ===\n", WEXITSTATUS(status)); } } return 0; }But, I was unable to replicate the behavior using Go. The parent just hangs on Read() function
package main /* #include <stdlib.h> #include <util.h> // Wrapper function to call openpty int open_pty(int *master_fd, int *slave_fd) { return openpty(master_fd, slave_fd, NULL, NULL, NULL); } */ import "C" import ( "fmt" "log" "os" "syscall" ) func main() { // Create pipe for stdin pipeFd := make([]int, 2) if err := syscall.Pipe(pipeFd); err != nil { fmt.Fprintf(os.Stderr, "pipe: %v\n", err) os.Exit(1) } // Create PTY for stdout/stderr using openpty(3) var masterFd, slaveFd C.int if C.open_pty(&masterFd, &slaveFd) == -1 { fmt.Fprintf(os.Stderr, "openpty failed\n") os.Exit(1) } pid, err := syscall.ForkExec( "/opt/homebrew/bin/ggrep", []string{"ggrep", "--color=auto", "-P", "hello"}, &syscall.ProcAttr{ Files: []uintptr{ uintptr(pipeFd[0]), // stdin from pipe uintptr(slaveFd), // stdout to PTY uintptr(slaveFd), // stderr to PTY }, Sys: &syscall.SysProcAttr{ Setsid: true, }, }, ) if err != nil { fmt.Fprintf(os.Stderr, "ForkExec: %v\n", err) os.Exit(1) } // Close unused descriptors syscall.Close(pipeFd[0]) // read end (child uses this) syscall.Close(int(slaveFd)) // slave PTY (child uses this) // Write test data to grep's stdin testData := []byte("hello world\n\n") syscall.Write(pipeFd[1], testData) if err := syscall.Close(pipeFd[1]); err != nil { log.Fatal(err.Error()) } // Close to signal EOF // Read grep's output from master PTY fmt.Println("=== Grep output (with color codes if terminal detected) ===") buffer := make([]byte, 1024) for { fmt.Println("Read...") n, err := syscall.Read(int(masterFd), buffer) if err != nil || n == 0 { break } fmt.Print(string(buffer[:n])) } syscall.Close(int(masterFd)) // Wait for child to finish var status syscall.WaitStatus _, err = syscall.Wait4(pid, &status, 0, nil) if err != nil { fmt.Fprintf(os.Stderr, "wait4: %v\n", err) os.Exit(1) } if status.Exited() { fmt.Printf("=== grep exited with status %d ===\n", status.ExitStatus()) } }Please clarify me on changes I need to make in my Go program.
