Do you like CLI applications? We love them! At Tidal Migrations we use full-featured GUI IDEs and editors like VS Code and Emacs but also vim and git running in our terminals. Every day we use bash, awk, sed and lots of other CLI tools and apps for work and fun. Also, we like to develop CLI apps and with this post, we're going to show you how to implement different interactive prompts for your CLI apps written in Go.
Passing data to CLI apps
Oftentimes CLI applications don't just work by themselves, but some process or operation is required on the information or data.
There are different ways to pass data to command line applications. Using flags, environment variables, file names as CLI arguments or reading from standard input is quite common and is pretty easy to implement using just the standard Go library. Using interactive prompts can spice up your CLI application and improve the overall UX.
Let's get started!
How to implement text input prompt
The basic text input prompt is easy to implement. Just read from standard input until the new line character (\n):
packagemainimport("bufio""fmt""os""strings")// StringPrompt asks for a string value using the labelfuncStringPrompt(labelstring)string{varsstringr:=bufio.NewReader(os.Stdin)for{fmt.Fprint(os.Stderr,label+" ")s,_=r.ReadString('\n')ifs!=""{break}}returnstrings.TrimSpace(s)}funcmain(){name:=StringPrompt("What is your name?")fmt.Printf("Hello, %s!\n",name)}
How to implement password input prompt
Password prompts are similar to text input prompts, except the user's typed input should be hidden:
packagemainimport("fmt""os""syscall""golang.org/x/term")// PasswordPrompt asks for a string value using the label.// The entered value will not be displayed on the screen// while typing.funcPasswordPrompt(labelstring)string{varsstringfor{fmt.Fprint(os.Stderr,label+" ")b,_:=term.ReadPassword(int(syscall.Stdin))s=string(b)ifs!=""{break}}fmt.Println()returns}funcmain(){password:=PasswordPrompt("What is your password?")fmt.Printf("Oh, I see! Your password is %q\n",password)}
How to implement Yes/No prompt
For Yes/No prompts we're going to create an infinite loop to keep asking until the user answers yes or no:
packagemainimport("bufio""fmt""os""strings")// YesNoPrompt asks yes/no questions using the label.funcYesNoPrompt(labelstring,defbool)bool{choices:="Y/n"if!def{choices="y/N"}r:=bufio.NewReader(os.Stdin)varsstringfor{fmt.Fprintf(os.Stderr,"%s (%s) ",label,choices)s,_=r.ReadString('\n')s=strings.TrimSpace(s)ifs==""{returndef}s=strings.ToLower(s)ifs=="y"||s=="yes"{returntrue}ifs=="n"||s=="no"{returnfalse}}}funcmain(){ok:=YesNoPrompt("Dev.to is awesome!",true)ifok{fmt.Println("Agree!")}else{fmt.Println("Huh?")}}
How to implement interactive checkboxes
To create an interactive multi-select prompt we're going to use an awesome survey package:
packagemainimport("fmt""strings""github.com/AlecAivazis/survey/v2")funcCheckboxes(labelstring,opts[]string)[]string{res:=[]string{}prompt:=&survey.MultiSelect{Message:label,Options:opts,}survey.AskOne(prompt,&res)returnres}funcmain(){answers:=Checkboxes("Which are your favourite programming languages?",[]string{"C","Python","Java","C++","C#","Visual Basic","JavaScript","PHP","Assembly Language","SQL","Groovy","Classic Visual Basic","Fortran","R","Ruby","Swift","MATLAB","Go","Prolog","Perl",},)s:=strings.Join(answers,", ")fmt.Println("Oh, I see! You like",s)}
Caveats and workarounds
If you pipe some input data to your interactive CLI app, the prompts will read that data:
$echo"Petr" | go run main.go
What is your name? Hello, Petr!
Sometimes such behavior is acceptable, but sometimes not. To check if the terminal is interactive let's use term.IsTerminal function:
packagemainimport("fmt""syscall""golang.org/x/term")funcmain(){ifterm.IsTerminal(int(syscall.Stdin)){fmt.Println("Terminal is interactive! You're good to use prompts!")}else{fmt.Println("Terminal is not interactive! Consider using flags or environment variables!")}}
$echo"Hello" | go run main.go
Terminal is not interactive! Consider using flags or environment variables!
$go run main.go
Terminal is interactive! You're good to use prompts!
Libraries
As you can see, it's pretty easy to implement basic interactive prompts, but for complex ones it's better to use some Go packages from the community:
Hey everyone! I finally came to terms with the fact that I can no longer dedicate enough time to keep this library alive
This project outgrew my wildest expectations and was such a great experience. If someone else wants to take over maintainence
please reach out
package main
import (
"fmt""github.com/AlecAivazis/survey/v2"
)
// the questions to askvarqs= []*survey.Question{
{
Name: "name",
Prompt: &survey.Input{Message: "What is your name?"},
Validate: survey.Required,
Transform: survey.Title,
},
{
Name: "color",
Prompt: &survey.Select{
Message: "Choose a color:",
Options: []string{"red"
twitterID:=prompter.Prompt("Enter your twitter ID", "")
lang:=prompter.Choose("Which language do you like the most?", []string{"Perl", "Golang", "Scala", "Ruby"}, "Perl")
passwd:=prompter.Password("Enter your password")
varlikeSushibool=prompter.YN("Do you like sushi?", true)
varlikeBeerbool=prompter.YesNo("Do you like beer?", false)
Features
Easy to use
Care non-interactive (not a tty) environment
Default is used and the process is not blocked
No howeyc/gopass (which uses cgo) dependency
cross build friendly
Customizable prompt setting by using &prompter.Prompter{} directly
Promptui is a library providing a simple interface to create command-line
prompts for go. It can be easily integrated into
spf13/cobraurfave/cli or any cli go application.
Promptui has two main input modes:
Prompt provides a single line for user input. Prompt supports
optional live validation, confirmation and masking the input.
Select provides a list of options to choose from. Select supports
pagination, search, detailed view and custom templates.