Go · Golang

Create a Boolean Yes or No prompt in Go


In this post, I will create a simple function that will allow you to ask the user a question, receive a valid response, and return it as a Boolean type. And if needed, the user will be able to tap Ctrl+C at any time to exit the program. Also importantly, despite using the operating system input, we will create unit tests for os.stdin.

go mod init devtidbits.com/yn

Firstly create a new function in the main.go file called YesNo that takes a string as an argument and returns a boolean value.

package main

func main() {}

func YesNo(s string) bool {
	return false
}

Now let’s call the YesNo function by asking a question and printing the returned boolean value. At the moment, this will always be false.

func main() {
	b := YesNo("Is the weather good today?")
	fmt.Println(b)
}

func YesNo(s string) bool {
	fmt.Printf("%s ", s)
	return false
}

Create a function named Read that will take an io.Reader argument and return a rune and error value. This function is to read the user’s keyboard input as the first Unicode rune rather than the first byte.

// Read and return the first rune from the reader.
func Read(r io.Reader) (rune, error) {
	reader := bufio.NewReader(r)
	ru, _, err := reader.ReadRune()
	return ru, err
}

Create a new Parse function to read the rune and return the appropriate boolean or an error.

var ErrParse = errors.New("invalid keypress, it does not match a boolean")

// Parse the rune as a bool.
// Characters y or Y return true, n or N return false.
// Everything else returns an error.
func Parse(r rune) (bool, error) {
	switch unicode.ToLower(r) {
	case rune('y'):
		return true, nil
	case rune('n'):
		return false, nil
	}
	return false, ErrParse
}

Update the YesNo function to read and parse the standard input. It also now prints the expected keyboard input.

// YesNo prints s requesting a boolean yes-or-no answer from the stardard input.
func YesNo(s string) bool {
	fmt.Printf("%s [y/n] ", s)
	input, err := Read(os.Stdin)
	if err != nil {
		log.Fatalln(err)
	}
	r, _ := Parse(input)
	return r
}
go run .
Is the weather good today? [y/n] y
true

go run .
Is the weather good today? [y/n] n
false

Instead of manually inputting various keys to test the results, let’s automate it with unit tests. Create a main_test.go file with a package main_test.

package main_test

import (
	"io"
	"testing"

	main "devtidbits.com/yn"
)

func TestRead(t *testing.T) {
	var stdin bytes.Buffer
	tests := []struct {
		name     string
		keypress string
		want     rune
		wantErr  bool
	}{
		// TODO: Add test cases.
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			stdin.WriteString(tt.keypress)
			got, err := main.Read(&stdin)
			if (err != nil) != tt.wantErr {
				t.Errorf("Read() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if got != tt.want {
				t.Errorf("Read() = %v, want %v", got, tt.want)
			}
		})
	}
}

The “devtidbits.com/yn” module in the go.mod file is imported as main. The TestRead function is a boilerplate created by VSCode, and I’ve then modified it to simulate a standard input keypress.

Replace // TODO: Add test cases. with our test arguments.

	}{
		{"empty", "", 0, true},
		{"newline", "\n", rune('\n'), false},
		{"space", " ", rune(' '), false},
		{"upper y", "Y", rune('Y'), false},
		{"lower y", "y", rune('y'), false},
		{"multibyte", "πŸ˜ƒ", rune('πŸ˜ƒ'), false},
		{"Yes", "Yes", rune('Y'), false},
	}

The “empty” test returns an error because stdin cannot be null and always returns a value.

The “newline” and “space” tests confirm whitespace inputs work as expected. A newline control is the return value for the Enter or Return keypresses.

The “upper y” and “lower y” tests confirm case matching.

The “multibyte” test confirms characters larger than a single byte work correctly.

And the “Yes” test shows that only the first character of the standard input is ever read and returned.

Create a TestParse for the Parse function, mainly using the same TestRead cases. This test will be different though as incorrect inputs return errors.

func TestParse(t *testing.T) {
	tests := []struct {
		name    string
		r       rune
		want    bool
		wantErr bool
	}{
		// incorrect values
		{"empty", 0, false, true},
		{"newline", rune('\n'), false, true},
		{"space", rune(' '), false, true},
		{"multibyte", rune('πŸ˜ƒ'), false, true},
		// correct values
		{"Yes", rune('Y'), true, false},
		{"yes", rune('y'), true, false},
		{"No", rune('N'), false, false},
		{"no", rune('n'), false, false},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := main.Parse(tt.r)
			if (err != nil) != tt.wantErr {
				t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if got != tt.want {
				t.Errorf("Parse() = %v, want %v", got, tt.want)
			}
		})
	}
}

Don’t forget to run the tests.

go test .
ok      devtidbits.com/yn       0.002s

Bonus, limit the keypresses.

Currently, YesNo returns false for all keypresses except for Y and y. Assuming no for all other keys is generally bad for usability and accidental keypresses. Instead, YesNo should only work with the n, N, y, Y, keys. Otherwise, remind the user of the question.

// YesNo prints s requesting a boolean yes-or-no answer from the stardard input.
func YesNo(s string) bool {
	for {
		fmt.Printf("%s [y/n] ", s)
		input, err := Read(os.Stdin)
		if err != nil {
			log.Fatalln(err)
		}
		if r, err := Parse(input); err == nil {
			return r
		}
	}
}
go run .
Is the weather good today? [y/n] 
Is the weather good today? [y/n] x
Is the weather good today? [y/n] ?
Is the weather good today? [y/n] y
true

Bonus, intercept Ctrl+C with a graceful exit

A user can tap Ctrl+C to quit during the Parse loop, which causes an ugly signal interrupt exit.

go run .
Is the weather good today? [y/n] ^Csignal: interrupt

Create a new CtrlC function that will intercept the signal and gracefully exit with an emoji handwave. We must also call this function in YesNo before its input loop.

// CtrlC intercepts any Ctrl+C keyboard input and exits to the shell.
func CtrlC() {
	c := make(chan os.Signal, 1)
	signal.Notify(c, os.Interrupt, syscall.SIGTERM)
	go func() {
		<-c
		fmt.Fprintf(os.Stdout, " πŸ‘‹\n")
		os.Exit(0)
	}()
}
func YesNo(s string) bool {
	CtrlC()
	for {
Is the weather good today? [y/n] ^C πŸ‘‹

That’s it, nice work, and thank you for reading. The complete source code is on my GitHub repo.

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s