package main import ( "errors" "fmt" "io" "os" "os/exec" "os/signal" "sync" "syscall" "github.com/mattn/go-isatty" ) func isTerminal() bool { return isatty.IsTerminal(os.Stdout.Fd()) && isatty.IsTerminal(os.Stderr.Fd()) } func readWithLess(r io.Reader, wrapLongLines bool) error { if !isTerminal() { // Just write everything to stdout if no terminal is available. _, err := io.Copy(os.Stdout, r) if err != nil && !isErrPipe(err) { return fmt.Errorf("error when forwarding data to stdout: %w", err) } if err := os.Stdout.Sync(); err != nil { return fmt.Errorf("cannot sync data to stdout: %w", err) } return nil } pr, pw, err := os.Pipe() if err != nil { return fmt.Errorf("cannot create pipe: %w", err) } defer func() { _ = pr.Close() _ = pw.Close() }() // Ignore Ctrl+C in the current process, so 'less' could handle it properly cancel := ignoreSignals(os.Interrupt) defer cancel() // Start 'less' process path, err := exec.LookPath("less") if err != nil { return fmt.Errorf("cannot find 'less' command: %w", err) } opts := []string{"less", "-F", "-X"} if !wrapLongLines { opts = append(opts, "-S") } p, err := os.StartProcess(path, opts, &os.ProcAttr{ Env: append(os.Environ(), "LESSCHARSET=utf-8"), Files: []*os.File{pr, os.Stdout, os.Stderr}, }) if err != nil { return fmt.Errorf("cannot start 'less' process: %w", err) } // Close pr after 'less' finishes in a parallel goroutine // in order to unblock forwarding data to stopped 'less' below. waitch := make(chan *os.ProcessState) go func() { // Wait for 'less' process to finish. ps, err := p.Wait() if err != nil { fatalf("unexpected error when waiting for 'less' process: %w", err) } _ = pr.Close() waitch <- ps }() // Forward data from r to 'less' _, err = io.Copy(pw, r) _ = pw.Sync() _ = pw.Close() // Wait until 'less' finished ps := <-waitch // Verify 'less' status. if !ps.Success() { return fmt.Errorf("'less' finished with unexpected code %d", ps.ExitCode()) } if err != nil && !isErrPipe(err) { return fmt.Errorf("error when forwarding data to 'less': %w", err) } return nil } func isErrPipe(err error) bool { return errors.Is(err, syscall.EPIPE) || errors.Is(err, io.ErrClosedPipe) } func ignoreSignals(sigs ...os.Signal) func() { ch := make(chan os.Signal, 1) signal.Notify(ch, sigs...) var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() for { _, ok := <-ch if !ok { return } } }() return func() { signal.Stop(ch) close(ch) wg.Wait() } }