package main import ( "fmt" "os" "os/exec" "strings" "github.com/chzyer/readline" ) // yesno asks a y/n question with a default and reads a single keypress. It is a // variable because it guards the destructive operations: tests replace it to // drive those paths without a terminal, and to assert that the question was // asked at all. var yesno = func(prompt string, def bool) bool { suffix := " y/N ? " if def { suffix = " Y/n ? " } ans := strings.ToLower(strings.TrimSpace(getkey(prompt + suffix))) if ans == "" { return def } return ans == "y" } // getkey reads a single keypress from the terminal without echo. It reads a // single byte directly from stdin; readline is not reading at this point (we // are inside command execution), so there is no reader to desync with. // // Anything else already typed on the same line is discarded: answering "yes" // to a y/n prompt must not leave "es\n" queued for the next readline call, // where it would come back as a bogus command. func getkey(prompt string) string { fmt.Print(prompt) tty := readline.IsTerminal(int(os.Stdin.Fd())) if tty { stty("-icanon", "-echo") } var buf [1]byte n, err := os.Stdin.Read(buf[:]) if tty { drainTTY() stty("icanon", "echo") } key := "" if err == nil && n > 0 { key = strings.Trim(string(buf[:n]), "\r\n\t") } fmt.Println(key) return key } // drainTTY discards input already queued on the terminal. `min 0 time 0` makes // a read return whatever is buffered without waiting, so this cannot block when // nothing is pending. func drainTTY() { stty("-icanon", "-echo", "min", "0", "time", "0") buf := make([]byte, 256) for { n, err := os.Stdin.Read(buf) if err != nil || n == 0 { return } } } func stty(args ...string) { c := exec.Command("stty", args...) c.Stdin = os.Stdin c.Stdout = os.Stdout c.Stderr = os.Stderr c.Run() }