ห้องสมุดสำหรับการสร้างพรอมต์แบบโต้ตอบและเข้าถึงได้บนเทอร์มินัลที่รองรับลำดับ ANSI Escape
เฮ้ทุกคน! ในที่สุดฉันก็ตกลงกับความจริงที่ว่าฉันไม่สามารถอุทิศเวลาพอที่จะทำให้ห้องสมุดนี้มีชีวิตอยู่ได้อีกต่อไป โครงการนี้เติบโตเกินความคาดหวังของฉันและเป็นประสบการณ์ที่ยอดเยี่ยม หากมีคนอื่นต้องการที่จะดูแลรักษาโปรดเอื้อมมือออกไป
package main
import (
"fmt"
"github.com/AlecAivazis/survey/v2"
)
// the questions to ask
var qs = [] * 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" , "blue" , "green" },
Default : "red" ,
},
},
{
Name : "age" ,
Prompt : & survey. Input { Message : "How old are you?" },
},
}
func main () {
// the answers will be written to this struct
answers := struct {
Name string // survey will match the question and field names
FavoriteColor string `survey:"color"` // or you can tag fields to match a specific name
Age int // if the types don't match, survey will convert it
}{}
// perform the questions
err := survey . Ask ( qs , & answers )
if err != nil {
fmt . Println ( err . Error ())
return
}
fmt . Printf ( "%s chose %s." , answers . Name , answers . FavoriteColor )
} ตัวอย่างสามารถพบได้ใน examples/ ไดเรกทอรี เรียกใช้พวกเขาเพื่อดูพฤติกรรมพื้นฐาน:
go run examples/simple.go
go run examples/validation.go มีสองวิธีหลักในการดำเนินการแจ้งเตือนและเริ่มรวบรวมข้อมูลจากผู้ใช้ของคุณ: Ask และ AskOne ความแตกต่างหลักคือไม่ว่าคุณจะสนใจรวบรวมข้อมูลชิ้นเดียวหรือหากคุณมีรายการคำถามที่จะถามว่าควรรวบรวมคำตอบในโครงสร้างเดียว สำหรับ Usecases ขั้นพื้นฐานส่วนใหญ่ Ask ควรจะเพียงพอ อย่างไรก็ตามสำหรับการสำรวจที่มีตรรกะการแตกแขนงที่ซับซ้อนเราขอแนะนำให้คุณแยกคำถามของคุณออกเป็นหลายการโทรไปยังฟังก์ชั่นทั้งสองนี้เพื่อให้เหมาะกับความต้องการของคุณ
พรอมต์ส่วนใหญ่ใช้การกำหนดค่าที่ละเอียดผ่านฟิลด์บนโครงสร้างที่คุณสร้างอินสแตนซ์ นอกจากนี้ยังเป็นไปได้ที่จะเปลี่ยนพฤติกรรมเริ่มต้นของการสำรวจโดยผ่าน AskOpts ไปยัง Ask หรือ AskOne ตัวอย่างในเอกสารนี้จะทำทั้งสองแทนกันได้:
prompt := & Select {
Message : "Choose a color:" ,
Options : [] string { "red" , "blue" , "green" },
// can pass a validator directly
Validate : survey . Required ,
}
// or define a default for the single call to `AskOne`
// the answer will get written to the color variable
survey . AskOne ( prompt , & color , survey . WithValidator ( survey . Required ))
// or define a default for every entry in a list of questions
// the answer will get copied into the matching field of the struct as shown above
survey . Ask ( questions , & answers , survey . WithValidator ( survey . Required )) name := ""
prompt := & survey. Input {
Message : "ping" ,
}
survey . AskOne ( prompt , & name ) file := ""
prompt := & survey. Input {
Message : "inform a file to save:" ,
Suggest : func ( toComplete string ) [] string {
files , _ := filepath . Glob ( toComplete + "*" )
return files
},
}
}
survey . AskOne ( prompt , & file ) text := ""
prompt := & survey. Multiline {
Message : "ping" ,
}
survey . AskOne ( prompt , & text ) password := ""
prompt := & survey. Password {
Message : "Please type your password" ,
}
survey . AskOne ( prompt , & password ) name := false
prompt := & survey. Confirm {
Message : "Do you like pie?" ,
}
survey . AskOne ( prompt , & name ) color := ""
prompt := & survey. Select {
Message : "Choose a color:" ,
Options : [] string { "red" , "blue" , "green" },
}
survey . AskOne ( prompt , & color ) ฟิลด์และค่าที่มาจากพรอมต์ Select อาจเป็นหนึ่งในสองสิ่งที่แตกต่างกัน หากคุณผ่าน int ฟิลด์จะมีค่าของดัชนีที่เลือก หากคุณส่งสตริงแทนค่าสตริงที่เลือกจะถูกเขียนไปยังฟิลด์
ผู้ใช้ยังสามารถกด esc เพื่อสลับวงจรความสามารถผ่านตัวเลือกด้วยปุ่ม J และ K เพื่อทำตามลำดับ
โดยค่าเริ่มต้นพรอมต์เลือกจะถูก จำกัด ให้แสดง 7 ตัวเลือกในแต่ละครั้งและจะแบ่งรายชื่อตัวเลือกนานกว่านั้น สามารถเปลี่ยนแปลงได้หลายวิธี:
// as a field on a single select
prompt := & survey. MultiSelect { ... , PageSize : 10 }
// or as an option to Ask or AskOne
survey . AskOne ( prompt , & days , survey . WithPageSize ( 10 ))ข้อความคำอธิบายเพิ่มเติมสามารถใช้เพื่อเพิ่มข้อมูลเพิ่มเติมให้กับแต่ละตัวเลือกที่แสดงในพรอมต์เลือก:
color := ""
prompt := & survey. Select {
Message : "Choose a color:" ,
Options : [] string { "red" , "blue" , "green" },
Description : func ( value string , index int ) string {
if value == "red" {
return "My favorite color"
}
return ""
},
}
survey . AskOne ( prompt , & color )
// Assuming that the user chose "red - My favorite color":
fmt . Println ( color ) //=> "red"
days := [] string {}
prompt := & survey. MultiSelect {
Message : "What days do you prefer:" ,
Options : [] string { "Sunday" , "Monday" , "Tuesday" , "Wednesday" , "Thursday" , "Friday" , "Saturday" },
}
survey . AskOne ( prompt , & days ) ฟิลด์และค่าที่มาจากพรอมต์ MultiSelect อาจเป็นหนึ่งในสองสิ่งที่แตกต่างกัน หากคุณผ่าน int ฟิลด์จะมีชิ้นส่วนของดัชนีที่เลือก หากคุณส่งสตริงแทนชิ้นส่วนของค่าสตริงที่เลือกจะถูกเขียนลงในฟิลด์
ผู้ใช้ยังสามารถกด esc เพื่อสลับวงจรความสามารถผ่านตัวเลือกด้วยปุ่ม J และ K เพื่อทำตามลำดับ
โดยค่าเริ่มต้นพรอมต์ MultiSelect จะถูก จำกัด ให้แสดง 7 ตัวเลือกในแต่ละครั้งและจะแบ่งรายชื่อตัวเลือกนานกว่านั้น สามารถเปลี่ยนแปลงได้หลายวิธี:
// as a field on a single select
prompt := & survey. MultiSelect { ... , PageSize : 10 }
// or as an option to Ask or AskOne
survey . AskOne ( prompt , & days , survey . WithPageSize ( 10 ))เปิดตัวตัวแก้ไขที่ผู้ใช้ต้องการ (กำหนดโดยตัวแปรสภาพแวดล้อม $ visual หรือ $ editor) บนไฟล์ชั่วคราว เมื่อผู้ใช้ออกจากตัวแก้ไขเนื้อหาของไฟล์ชั่วคราวจะถูกอ่านเป็นผลลัพธ์ หากไม่มีสิ่งใดที่มีอยู่ให้ใช้ Notepad (บน Windows) หรือ Vim (Linux หรือ Mac)
คุณยังสามารถระบุรูปแบบสำหรับชื่อของไฟล์ชั่วคราว สิ่งนี้มีประโยชน์สำหรับการสร้างความมั่นใจในการไฮไลต์ไวยากรณ์ที่ตรงกับการใช้ USECASE ของคุณ
prompt := & survey. Editor {
Message : "Shell code snippet" ,
FileName : "*.sh" ,
}
survey . AskOne ( prompt , & content )โดยค่าเริ่มต้นผู้ใช้สามารถกรองตัวเลือกในการเลือกและ multiSelects โดยการพิมพ์ในขณะที่พรอมต์ทำงานอยู่ สิ่งนี้จะกรองตัวเลือกทั้งหมดที่ไม่มีสตริงพิมพ์ที่ใดก็ได้ในชื่อของพวกเขาโดยไม่สนใจเคส
นอกจากนี้ยังสามารถให้ฟังก์ชั่นตัวกรองที่กำหนดเองเพื่อเปลี่ยนพฤติกรรมนี้:
func myFilter ( filterValue string , optValue string , optIndex int ) bool {
// only include the option if it includes the filter and has length greater than 5
return strings . Contains ( optValue , filterValue ) && len ( optValue ) >= 5
}
// configure it for a specific prompt
& Select {
Message : "Choose a color:" ,
Options : [] string { "red" , "blue" , "green" },
Filter : myFilter ,
}
// or define a default for all of the questions
survey . AskOne ( prompt , & color , survey . WithFilter ( myFilter ))โดยค่าเริ่มต้นตัวกรองจะหายไปหากผู้ใช้เลือกหนึ่งในองค์ประกอบที่กรอง เมื่อผู้ใช้เลือกองค์ประกอบหนึ่งการตั้งค่าตัวกรองจะหายไป
อย่างไรก็ตามผู้ใช้สามารถป้องกันไม่ให้สิ่งนี้เกิดขึ้นและทำให้ตัวกรองใช้งานได้หลายรายการในการเลือกแบบหลายทางเลือก:
// configure it for a specific prompt
& Select {
Message : "Choose a color:" ,
Options : [] string { "light-green" , "green" , "dark-green" , "red" },
KeepFilter : true ,
}
// or define a default for all of the questions
survey . AskOne ( prompt , & color , survey . WithKeepFilter ( true )) การตรวจสอบการตอบสนองของแต่ละบุคคลสำหรับคำถามเฉพาะสามารถทำได้โดยการกำหนดฟิลด์ Validate ใน survey.Question คำถามที่จะตรวจสอบความถูกต้อง ฟังก์ชั่นนี้ใช้ interface{} ประเภทและส่งคืนข้อผิดพลาดเพื่อแสดงให้กับผู้ใช้พร้อมแจ้งการตอบกลับอื่น เช่น survey.WithValidator
q := & survey. Question {
Prompt : & survey. Input { Message : "Hello world validation" },
Validate : func ( val interface {}) error {
// since we are validating an Input, the assertion will always succeed
if str , ok := val .( string ) ; ! ok || len ( str ) > 10 {
return errors . New ( "This response cannot be longer than 10 characters." )
}
return nil
},
}
color := ""
prompt := & survey. Input { Message : "Whats your name?" }
// you can pass multiple validators here and survey will make sure each one passes
survey . AskOne ( prompt , & color , survey . WithValidator ( survey . Required )) survey มาพร้อมกับผู้ตรวจสอบความถูกต้องสองสามตัวเพื่อให้พอดีกับสถานการณ์ทั่วไป ปัจจุบันผู้ตรวจสอบเหล่านี้รวมถึง:
| ชื่อ | ประเภทที่ถูกต้อง | คำอธิบาย | หมายเหตุ |
|---|---|---|---|
| ที่จำเป็น | ใดๆ | ปฏิเสธค่าศูนย์ของประเภทการตอบสนอง | ค่าบูลีนผ่านตรงผ่านเนื่องจากค่าศูนย์ (เท็จ) เป็นการตอบสนองที่ถูกต้อง |
| minlength (n) | สาย | บังคับใช้ว่าการตอบสนองอย่างน้อยก็มีความยาวที่กำหนด | |
| MaxLength (N) | สาย | บังคับใช้ว่าการตอบสนองไม่เกินความยาวที่กำหนด | |
| MaxItems (n) | [] OptionAsanswer | บังคับใช้ว่าการตอบสนองไม่มีการเลือกที่ระบุอีกต่อไป | |
| minitems (n) | [] OptionAsanswer | บังคับใช้ว่าการตอบสนองมีการเลือกที่ระบุไม่น้อยกว่า |
พรอมต์ทั้งหมดมีฟิลด์ Help ซึ่งสามารถกำหนดเพื่อให้ข้อมูลเพิ่มเติมแก่ผู้ใช้ของคุณ:
& survey. Input {
Message : "What is your phone number:" ,
Help : "Phone number should include the area code" ,
} โดยค่าเริ่มต้นผู้ใช้สามารถเลือกตัวเลือกหลายตัวเลือกทั้งหมดโดยใช้ปุ่มลูกศรขวา เพื่อป้องกันไม่ให้ผู้ใช้สามารถทำได้ (และลบ <right> to all จากพรอมต์) ให้ใช้ตัวเลือก WithRemoveSelectAll :
import (
"github.com/AlecAivazis/survey/v2"
)
number := ""
prompt := & survey. Input {
Message : "This question has the select all option removed" ,
}
survey . AskOne ( prompt , & number , survey . WithRemoveSelectAll ()) นอกจากนี้โดยค่าเริ่มต้นผู้ใช้สามารถใช้ปุ่มลูกศรซ้ายเพื่อยกเลิกการเลือกตัวเลือกทั้งหมด เพื่อป้องกันไม่ให้ผู้ใช้สามารถทำได้ (และลบข้อความ <left> to none ข้อความออกจากพรอมต์) ให้ใช้ตัวเลือก WithRemoveSelectNone :
import (
"github.com/AlecAivazis/survey/v2"
)
number := ""
prompt := & survey. Input {
Message : "This question has the select all option removed" ,
}
survey . AskOne ( prompt , & number , survey . WithRemoveSelectNone ()) ในบางสถานการณ์ ? เป็นการตอบสนองที่ถูกต้องอย่างสมบูรณ์แบบ ในการจัดการกับสิ่งนี้คุณสามารถเปลี่ยนรูนที่สำรวจได้ด้วย WithHelpInput :
import (
"github.com/AlecAivazis/survey/v2"
)
number := ""
prompt := & survey. Input {
Message : "If you have this need, please give me a reasonable message." ,
Help : "I couldn't come up with one." ,
}
survey . AskOne ( prompt , & number , survey . WithHelpInput ( '^' )) การเปลี่ยนไอคอนและสี/รูปแบบของพวกเขาสามารถทำได้โดยผ่านตัวเลือก WithIcons รูปแบบเป็นไปตามรูปแบบที่ระบุไว้ที่นี่ ตัวอย่างเช่น:
import (
"github.com/AlecAivazis/survey/v2"
)
number := ""
prompt := & survey. Input {
Message : "If you have this need, please give me a reasonable message." ,
Help : "I couldn't come up with one." ,
}
survey . AskOne ( prompt , & number , survey . WithIcons ( func ( icons * survey. IconSet ) {
// you can set any icons
icons . Question . Text = "⁇"
// for more information on formatting the icons, see here: https://github.com/mgutz/ansi#style-format
icons . Question . Format = "yellow+hb"
}))ไอคอนและข้อความและรูปแบบเริ่มต้นของพวกเขาสรุปไว้ด้านล่าง:
| ชื่อ | ข้อความ | รูปแบบ | คำอธิบาย |
|---|---|---|---|
| ข้อผิดพลาด | x | สีแดง | ก่อนเกิดข้อผิดพลาด |
| ช่วย | ฉัน | สีฟ้า | ก่อนความช่วยเหลือข้อความ |
| คำถาม | - | สีเขียว+HB | ก่อนข้อความของพรอมต์ |
| SelectFocus | - | สีเขียว | ทำเครื่องหมายโฟกัสปัจจุบันใน Select และ MultiSelect |
| การเปิดบัญชี | - | ค่าเริ่มต้น+hb | ทำเครื่องหมายตัวเลือกที่ไม่ได้เลือกในพรอมต์ MultiSelect |
| การทำเครื่องหมาย | [x] | สีฟ้า+b | ทำเครื่องหมายตัวเลือกที่เลือกในพรอมต์ MultiSelect |
แบบสำรวจจะกำหนดคำตอบที่รวดเร็วให้กับประเภทที่กำหนดเองของคุณหากพวกเขาใช้อินเทอร์เฟซนี้:
type Settable interface {
WriteAnswer ( field string , value interface {}) error
}นี่คือตัวอย่างวิธีการใช้:
type MyValue struct {
value string
}
func ( my * MyValue ) WriteAnswer ( name string , value interface {}) error {
my . value = value .( string )
}
myval := MyValue {}
survey . AskOne (
& survey. Input {
Message : "Enter something:" ,
},
& myval
) คุณสามารถทดสอบพรอมต์แบบโต้ตอบของโปรแกรมโดยใช้ Go-Expect ห้องสมุดสามารถใช้เพื่อคาดหวังการจับคู่ใน stdout และตอบสนองใน stdin เนื่องจาก os.Stdout ในกระบวนการ go test ไม่ใช่ TTY หากคุณจัดการเคอร์เซอร์หรือใช้ survey คุณจะต้องมีวิธีตีความลำดับการหลบหนีของเทอร์มินัล / ANSI สำหรับสิ่งต่าง ๆ เช่น CursorLocation vt10x.NewVT10XConsole จะสร้างคอนโซล go-expect ซึ่งยังมัลติเพล็กซ์ stdio ไปยังเทอร์มินัลเสมือนในหน่วยความจำในหน่วยความจำ
สำหรับตัวอย่างบางส่วนคุณสามารถเห็นการทดสอบใด ๆ ใน repo นี้
survey ประเภทใดที่ได้รับการสนับสนุนจาก IOการสำรวจมีวัตถุประสงค์เพื่อสนับสนุนอีมูเลเตอร์เทอร์มินัลส่วนใหญ่ คาดว่าจะสนับสนุนลำดับการหลบหนีของ ANSI ซึ่งหมายความว่าการอ่านจาก piped stdin หรือการเขียนไปยัง piped stdout ไม่ได้รับการสนับสนุน และมีแนวโน้มที่จะทำลายแอปพลิเคชันของคุณในสถานการณ์เหล่านี้ ดู #337
โดยปกติเมื่อคุณพิมพ์ CTRL-C เทอร์มินัลจะรับรู้สิ่งนี้เป็นปุ่มเลิกและส่งสัญญาณ sigint ไปยังกระบวนการซึ่งสิ้นสุดลง อย่างไรก็ตามการสำรวจกำหนดค่าเทอร์มินัลชั่วคราวเพื่อส่งรหัสควบคุมเป็นไบต์อินพุตธรรมดา เมื่อการสำรวจอ่าน A ^C BYTE (ASCII X03, "End of Text") มันขัดจังหวะการสำรวจปัจจุบันและส่งคืน github.com/AlecAivazis/survey/v2/terminal.InterruptErr จาก Ask หรือ AskOne หากคุณต้องการหยุดกระบวนการจัดการข้อผิดพลาดที่ส่งคืนในรหัสของคุณ:
err := survey . AskOne ( prompt , & myVar )
if err != nil {
if err == terminal. InterruptErr {
log . Fatal ( "interrupted" )
}
...
}