I'm trying print to stdout from golang using $PAGER
or manually invoking more
or less
to allow the user to easily scroll through a lot of options. How can I achieve this?
You can use the os/exec package to start a process that runs less
(or whatever is in $PAGER
) and then pipe a string to its standard input. The following worked for me:
func main() {
// Could read $PAGER rather than hardcoding the path.
cmd := exec.Command("/usr/bin/less")
// Feed it with the string you want to display.
cmd.Stdin = strings.NewReader("The text you want to show.")
// This is crucial - otherwise it will write to a null device.
cmd.Stdout = os.Stdout
// Fork off a process and wait for it to terminate.
err := cmd.Run()
if err != nil {
log.Fatal(err)
}
}
Here is a somewhat naive cat example that uses $PAGER
when set.
package main
import (
"io"
"log"
"os"
"os/exec"
)
func main() {
var out io.WriteCloser
var cmd *exec.Cmd
if len(os.Args) != 2 {
log.Fatal("Wrong number of args: gcat <file>")
}
fileName := os.Args[1]
file, err := os.Open(fileName)
if err != nil {
log.Fatal("Error opening file: ", err)
}
pager := os.Getenv("PAGER")
if pager != "" {
cmd = exec.Command(pager)
var err error
out, err = cmd.StdinPipe()
if err != nil {
log.Fatal(err)
}
cmd.Stdout = os.Stdout
if err := cmd.Start(); err != nil {
log.Fatal("Unable to start $PAGER: ", err)
}
} else {
out = os.Stdout
}
_, err = io.Copy(out, file)
if err != nil {
log.Fatal(err)
}
file.Close()
out.Close()
if cmd != nil {
if err := cmd.Wait(); err != nil {
log.Fatal("Error waiting for cmd: ", err)
}
}
}
I assume you already have output being printed from your program to stdout that you want to have captured and sent to your pager, you don't want to rewrite the I/O to use another input stream like the other responses require.
You can create an os.Pipe which works the same as running a command with "|less" by attaching one side to your pager and the other side to stdout like this:
// Create a pipe for a pager to use
r, w, err := os.Pipe()
if err != nil {
panic("You probably want to fail more gracefully than this")
}
// Capture STDOUT for the Pager. Keep the old
// value so we can restore it later.
stdout := os.Stdout
os.Stdout = w
// Create the pager process to execute and attach
// the appropriate I/O streams.
pager := exec.Command("less")
pager.Stdin = r
pager.Stdout = stdout // the pager uses the original stdout, not the pipe
pager.Stderr = os.Stderr
// Defer a function that closes the pipe and invokes
// the pager, then restores os.Stdout after this function
// returns and we've finished capturing output.
//
// Note that it's very important that the pipe is closed,
// so that EOF is sent to the pager, otherwise weird things
// will happen.
defer func() {
// Close the pipe
w.Close()
// Run the pager
if err := pager.Run(); err != nil {
fmt.Fprintln(os.Stderr, err)
}
// restore stdout
os.Stdout = stdout
}()
This version creates an io.Writer
called pager
for all the output that you want paged (you can assign it to os.Stdout
if you like) and correctly closes that and waits for the $PAGER
when main()
returns.
import (
"fmt"
"io"
"log"
"os"
"os/exec"
)
var pager io.WriteCloser
func main() {
var cmd *exec.Cmd
cmd, pager = runPager()
defer func() {
pager.Close()
cmd.Wait()
}()
fmt.Fprintln(pager, "Hello, 世界")
}
func runPager() (*exec.Cmd, io.WriteCloser) {
pager := os.Getenv("PAGER")
if pager == "" {
pager = "more"
}
cmd := exec.Command(pager)
out, err := cmd.StdinPipe()
if err != nil {
log.Fatal(err)
}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
log.Fatal(err)
}
return cmd, out
}