github.com/jfrog/jfrog-cli-go@v1.22.1-0.20200318093948-4826ef344ffd/artifactory/commands/utils/questionnaire.go (about) 1 package utils 2 3 import ( 4 "fmt" 5 "regexp" 6 "strconv" 7 "strings" 8 9 "github.com/c-bata/go-prompt" 10 ) 11 12 const ( 13 InsertValuePromptMsg = "Insert the value for " 14 ) 15 16 // The interactive questionnaire works as follows: 17 // We have to provide a map of QuestionInfo which include all possible questions may be asked. 18 // 1. Mandatory Questions: 19 // * We will ask all the questions in MandatoryQuestionsKeys list one after the other. 20 // 2. Optional questions: 21 // * We have to provide a slice of prompt.Suggest, in which each suggest.Text is a key of a question in the map. 22 // * After a suggest was chosen from the list, the corresponding question from the map will be asked. 23 // * Each answer is written to to the configMap using its writer, under the MapKey specified in the questionInfo. 24 // * We will execute the previous step until the SaveAndExit string was inserted. 25 type InteractiveQuestionnaire struct { 26 QuestionsMap map[string]QuestionInfo 27 MandatoryQuestionsKeys []string 28 OptionalKeysSuggests []prompt.Suggest 29 AnswersMap map[string]interface{} 30 } 31 32 // Each question can have the following properties: 33 // * Msg - will be printed in separate line 34 // * PromptPrefix - will be printed before the input cursor in the answer line 35 // * Options - In case the answer must be selected from a predefined list 36 // * AllowVars - a flag indicates whether a variable (in form of ${var}) is an acceptable answer despite the predefined list 37 // * Writer - how to write the answer to the final config map 38 // * MapKey - the key under which the answer will be written to the configMap 39 // * Callback - optional function can be executed after the answer was inserted. Can be used to implement some dependencies between questions. 40 type AnswerWriter func(resultMap *map[string]interface{}, key, value string) error 41 type questionCallback func(*InteractiveQuestionnaire, string) (string, error) 42 43 type QuestionInfo struct { 44 Msg string 45 PromptPrefix string 46 Options []prompt.Suggest 47 AllowVars bool 48 Writer AnswerWriter 49 MapKey string 50 Callback questionCallback 51 } 52 53 const ( 54 PressTabMsg = " (press Tab for options):" 55 InvalidAnswerMsg = "Invalid answer. Please select value from the suggestions list." 56 VariableUseMsg = " You may use dynamic variable in the form of ${key}." 57 EmptyValueMsg = "The value cannot be empty. Please enter a valid value:" 58 OptionalKey = "OptionalKey" 59 SaveAndExit = ":x" 60 61 // Boolean answers 62 True = "true" 63 False = "false" 64 65 CommaSeparatedListMsg = "The value should be a comma separated list" 66 ) 67 68 // Var can be inserted in the form of ${key} 69 var VarPattern = regexp.MustCompile(`^\$\{\w+\}+$`) 70 71 func prefixCompleter(options []prompt.Suggest) prompt.Completer { 72 return func(document prompt.Document) []prompt.Suggest { 73 return prompt.FilterHasPrefix(options, document.GetWordBeforeCursor(), true) 74 } 75 } 76 77 // Bind ctrl+c key to interrupt the command 78 func interruptKeyBind() prompt.Option { 79 interrupt := prompt.KeyBind{ 80 Key: prompt.ControlC, 81 Fn: func(buf *prompt.Buffer) { 82 panic("Interrupted") 83 }, 84 } 85 return prompt.OptionAddKeyBind(interrupt) 86 } 87 88 // Ask question with free string answer, answer cannot be empty. 89 // Variable aren't check and can be part of the answer 90 func AskString(msg, promptPrefix string) string { 91 if msg != "" { 92 fmt.Println(msg + ":") 93 } 94 for { 95 answer := prompt.Input(promptPrefix+" ", prefixCompleter(nil), interruptKeyBind()) 96 if answer != "" { 97 return answer 98 } 99 fmt.Println(EmptyValueMsg) 100 } 101 } 102 103 // Ask question with list of possible answers. 104 // The answer must be chosen from the list, but can be a variable if allowVars set to true. 105 func AskFromList(msg, promptPrefix string, allowVars bool, options []prompt.Suggest) string { 106 if msg != "" { 107 fmt.Println(msg + PressTabMsg) 108 } 109 errMsg := InvalidAnswerMsg 110 if allowVars { 111 errMsg += VariableUseMsg 112 } 113 for { 114 answer := prompt.Input(promptPrefix+" ", prefixCompleter(options), interruptKeyBind()) 115 if validateAnswer(answer, options, allowVars) { 116 return answer 117 } 118 fmt.Println(errMsg) 119 } 120 } 121 122 func validateAnswer(answer string, options []prompt.Suggest, allowVars bool) bool { 123 if allowVars { 124 if regexMatch := VarPattern.FindStringSubmatch(answer); regexMatch != nil { 125 return true 126 } 127 } 128 for _, option := range options { 129 if answer == option.Text { 130 return true 131 } 132 } 133 return false 134 } 135 136 // Ask question steps: 137 // 1. Ask for string/from list 138 // 2. Write the answer to answersMap (if writer provided) 139 // 3. Run callback (if provided)q 140 func (iq *InteractiveQuestionnaire) AskQuestion(question QuestionInfo) (value string, err error) { 141 142 var answer string 143 if question.Options != nil { 144 answer = AskFromList(question.Msg, question.PromptPrefix, question.AllowVars, question.Options) 145 } else { 146 answer = AskString(question.Msg, question.PromptPrefix) 147 } 148 if question.Writer != nil { 149 err = question.Writer(&iq.AnswersMap, question.MapKey, answer) 150 if err != nil { 151 return "", err 152 } 153 } 154 if question.Callback != nil { 155 _, err = question.Callback(iq, answer) 156 if err != nil { 157 return "", err 158 } 159 } 160 return answer, nil 161 } 162 163 // The main function to perform the questionnaire 164 func (iq *InteractiveQuestionnaire) Perform() error { 165 iq.AnswersMap = make(map[string]interface{}) 166 for i := 0; i < len(iq.MandatoryQuestionsKeys); i++ { 167 iq.AskQuestion(iq.QuestionsMap[iq.MandatoryQuestionsKeys[i]]) 168 } 169 fmt.Println("You can type \":x\" at any time to save and exit.") 170 OptionalKeyQuestion := iq.QuestionsMap[OptionalKey] 171 OptionalKeyQuestion.Options = iq.OptionalKeysSuggests 172 for { 173 key, err := iq.AskQuestion(OptionalKeyQuestion) 174 if err != nil { 175 return err 176 } 177 if key == SaveAndExit { 178 break 179 } 180 } 181 return nil 182 } 183 184 // Common questions 185 var FreeStringQuestionInfo = QuestionInfo{ 186 Options: nil, 187 AllowVars: false, 188 Writer: WriteStringAnswer, 189 } 190 191 func GetBoolSuggests() []prompt.Suggest { 192 return []prompt.Suggest{ 193 {Text: True}, 194 {Text: False}, 195 } 196 } 197 198 var BoolQuestionInfo = QuestionInfo{ 199 Options: GetBoolSuggests(), 200 AllowVars: true, 201 Writer: WriteBoolAnswer, 202 } 203 204 var IntQuestionInfo = QuestionInfo{ 205 Options: nil, 206 AllowVars: true, 207 Writer: WriteIntAnswer, 208 } 209 210 var StringListQuestionInfo = QuestionInfo{ 211 Msg: CommaSeparatedListMsg, 212 Options: nil, 213 AllowVars: true, 214 Writer: WriteStringArrayAnswer, 215 } 216 217 // Common writers 218 func WriteStringAnswer(resultMap *map[string]interface{}, key, value string) error { 219 (*resultMap)[key] = value 220 return nil 221 } 222 223 func WriteBoolAnswer(resultMap *map[string]interface{}, key, value string) error { 224 if regexMatch := VarPattern.FindStringSubmatch(value); regexMatch != nil { 225 return WriteStringAnswer(resultMap, key, value) 226 } 227 boolValue, err := strconv.ParseBool(value) 228 if err != nil { 229 return err 230 } 231 (*resultMap)[key] = boolValue 232 return nil 233 } 234 235 func WriteIntAnswer(resultMap *map[string]interface{}, key, value string) error { 236 if regexMatch := VarPattern.FindStringSubmatch(value); regexMatch != nil { 237 return WriteStringAnswer(resultMap, key, value) 238 } 239 intValue, err := strconv.Atoi(value) 240 if err != nil { 241 return err 242 } 243 (*resultMap)[key] = intValue 244 return nil 245 } 246 247 func WriteStringArrayAnswer(resultMap *map[string]interface{}, key, value string) error { 248 if regexMatch := VarPattern.FindStringSubmatch(value); regexMatch != nil { 249 return WriteStringAnswer(resultMap, key, value) 250 } 251 arrValue := strings.Split(value, ",") 252 (*resultMap)[key] = arrValue 253 return nil 254 } 255 256 func GetSuggestsFromKeys(keys []string, SuggestionMap map[string]prompt.Suggest) []prompt.Suggest { 257 var suggests []prompt.Suggest 258 for _, key := range keys { 259 suggests = append(suggests, SuggestionMap[key]) 260 } 261 return suggests 262 } 263 264 // After an optional value was chosen we'll ask for its value. 265 func OptionalKeyCallback(iq *InteractiveQuestionnaire, key string) (value string, err error) { 266 if key != SaveAndExit { 267 valueQuestion := iq.QuestionsMap[key] 268 // Since we are using default question in most of the cases we set the map key here. 269 valueQuestion.MapKey = key 270 valueQuestion.PromptPrefix = InsertValuePromptMsg + key 271 if valueQuestion.Options != nil { 272 valueQuestion.PromptPrefix += PressTabMsg 273 } 274 valueQuestion.PromptPrefix += " >" 275 value, err = iq.AskQuestion(valueQuestion) 276 } 277 return value, err 278 }