github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/interact/pollster.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package interact 5 6 import ( 7 "bufio" 8 "bytes" 9 "fmt" 10 "html/template" 11 "io" 12 "net/url" 13 "os" 14 "sort" 15 "strconv" 16 "strings" 17 18 "github.com/juju/collections/set" 19 "github.com/juju/errors" 20 "github.com/juju/jsonschema" 21 "golang.org/x/crypto/ssh/terminal" 22 ) 23 24 // Non standard json Format 25 const FormatCertFilename jsonschema.Format = "cert-filename" 26 27 // Pollster is used to ask multiple questions of the user using a standard 28 // formatting. 29 type Pollster struct { 30 VerifyURLs VerifyFunc 31 VerifyCertFile VerifyFunc 32 scanner *bufio.Scanner 33 out io.Writer 34 errOut io.Writer 35 in io.Reader 36 } 37 38 // New returns a Pollster that wraps the given reader and writer. 39 func New(in io.Reader, out, errOut io.Writer) *Pollster { 40 return &Pollster{ 41 scanner: bufio.NewScanner(byteAtATimeReader{in}), 42 out: out, 43 errOut: errOut, 44 in: in, 45 } 46 } 47 48 // List contains the information necessary to ask the user to select one item 49 // from a list of options. 50 type List struct { 51 Singular string 52 Plural string 53 Options []string 54 Default string 55 } 56 57 // MultiList contains the information necessary to ask the user to select from a 58 // list of options. 59 type MultiList struct { 60 Singular string 61 Plural string 62 Options []string 63 Default []string 64 } 65 66 var listTmpl = template.Must(template.New("").Funcs(map[string]interface{}{"title": strings.Title}).Parse(` 67 {{title .Plural}} 68 {{range .Options}} {{.}} 69 {{end}} 70 `[1:])) 71 72 var selectTmpl = template.Must(template.New("").Parse(` 73 Select {{.Singular}}{{if .Default}} [{{.Default}}]{{end}}: `[1:])) 74 75 // Select queries the user to select from the given list of options. 76 func (p *Pollster) Select(l List) (string, error) { 77 return p.SelectVerify(l, VerifyOptions(l.Singular, l.Options, l.Default != "")) 78 } 79 80 // SelectVerify queries the user to select from the given list of options, 81 // verifying the choice by passing responses through verify. 82 func (p *Pollster) SelectVerify(l List, verify VerifyFunc) (string, error) { 83 if err := listTmpl.Execute(p.out, l); err != nil { 84 return "", err 85 } 86 87 question, err := sprint(selectTmpl, l) 88 if err != nil { 89 return "", errors.Trace(err) 90 } 91 val, err := QueryVerify(question, p.scanner, p.out, p.errOut, verify) 92 if err != nil { 93 return "", errors.Trace(err) 94 } 95 if val == "" { 96 return l.Default, nil 97 } 98 return val, nil 99 } 100 101 var multiSelectTmpl = template.Must(template.New("").Funcs( 102 map[string]interface{}{"join": strings.Join}).Parse(` 103 Select one or more {{.Plural}} separated by commas{{if .Default}} [{{join .Default ", "}}]{{end}}: `[1:])) 104 105 // MultiSelect queries the user to select one more answers from the given list of 106 // options by entering values delimited by commas (and thus options must not 107 // contain commas). 108 func (p *Pollster) MultiSelect(l MultiList) ([]string, error) { 109 var bad []string 110 for _, s := range l.Options { 111 if strings.Contains(s, ",") { 112 bad = append(bad, s) 113 } 114 } 115 if len(bad) > 0 { 116 return nil, errors.Errorf("options may not contain commas: %q", bad) 117 } 118 if err := listTmpl.Execute(p.out, l); err != nil { 119 return nil, err 120 } 121 122 // If there is only ever one option and that option also equals the default 123 // option, then just echo out what that option is (above), then return that 124 // option back. 125 if len(l.Default) == 1 && len(l.Options) == 1 && l.Options[0] == l.Default[0] { 126 return l.Default, nil 127 } 128 129 question, err := sprint(multiSelectTmpl, l) 130 if err != nil { 131 return nil, errors.Trace(err) 132 } 133 verify := multiVerify(l.Singular, l.Plural, l.Options, l.Default != nil) 134 a, err := QueryVerify(question, p.scanner, p.out, p.errOut, verify) 135 if err != nil { 136 return nil, errors.Trace(err) 137 } 138 if a == "" { 139 return l.Default, nil 140 } 141 return multiSplit(a), nil 142 } 143 144 // Enter requests that the user enter a value. Any value except an empty string 145 // is accepted. 146 func (p *Pollster) Enter(valueName string) (string, error) { 147 return p.EnterVerify(valueName, func(s string) (ok bool, msg string, err error) { 148 return s != "", "", nil 149 }) 150 } 151 152 // EnterPassword works like Enter except that if the pollster's input wraps a 153 // terminal, the user's input will be read without local echo. 154 func (p *Pollster) EnterPassword(valueName string) (string, error) { 155 if f, ok := p.in.(*os.File); ok && terminal.IsTerminal(int(f.Fd())) { 156 defer fmt.Fprint(p.out, "\n\n") 157 if _, err := fmt.Fprintf(p.out, "Enter "+valueName+": "); err != nil { 158 return "", errors.Trace(err) 159 } 160 value, err := terminal.ReadPassword(int(f.Fd())) 161 if err != nil { 162 return "", errors.Trace(err) 163 } 164 return string(value), nil 165 } 166 return p.Enter(valueName) 167 } 168 169 // EnterDefault requests that the user enter a value. Any value is accepted. 170 // An empty string is treated as defVal. 171 func (p *Pollster) EnterDefault(valueName, defVal string) (string, error) { 172 return p.EnterVerifyDefault(valueName, nil, defVal) 173 } 174 175 // VerifyFunc is a type that determines whether a value entered by the user is 176 // acceptable or not. If it returns an error, the calling func will return an 177 // error, and the other return values are ignored. If ok is true, the value is 178 // acceptable, and that value will be returned by the calling function. If ok 179 // is false, the user will be asked to enter a new value for query. If ok is 180 // false. if errmsg is not empty, it will be printed out as an error to te the 181 // user. 182 type VerifyFunc func(s string) (ok bool, errmsg string, err error) 183 184 // EnterVerify requests that the user enter a value. Values failing to verify 185 // will be rejected with the error message returned by verify. A nil verify 186 // function will accept any value (even an empty string). 187 func (p *Pollster) EnterVerify(valueName string, verify VerifyFunc) (string, error) { 188 return QueryVerify("Enter "+valueName+": ", p.scanner, p.out, p.errOut, verify) 189 } 190 191 // EnterOptional requests that the user enter a value. It accepts any value, 192 // even an empty string. 193 func (p *Pollster) EnterOptional(valueName string) (string, error) { 194 return QueryVerify("Enter "+valueName+" (optional): ", p.scanner, p.out, p.errOut, nil) 195 } 196 197 // EnterVerifyDefault requests that the user enter a value. Values failing to 198 // verify will be rejected with the error message returned by verify. An empty 199 // string will be accepted as the default value even if it would fail 200 // verification. 201 func (p *Pollster) EnterVerifyDefault(valueName string, verify VerifyFunc, defVal string) (string, error) { 202 var verifyDefault VerifyFunc 203 if verify != nil { 204 verifyDefault = func(s string) (ok bool, errmsg string, err error) { 205 if s == "" { 206 return true, "", nil 207 } 208 return verify(s) 209 } 210 } 211 s, err := QueryVerify("Enter "+valueName+" ["+defVal+"]: ", p.scanner, p.out, p.errOut, verifyDefault) 212 if err != nil { 213 return "", errors.Trace(err) 214 } 215 if s == "" { 216 return defVal, nil 217 } 218 return s, nil 219 } 220 221 // YN queries the user with a yes no question q (which should not include a 222 // question mark at the end). It uses defVal as the default answer. 223 func (p *Pollster) YN(q string, defVal bool) (bool, error) { 224 defaultStr := "(y/N)" 225 if defVal { 226 defaultStr = "(Y/n)" 227 } 228 verify := func(s string) (ok bool, errmsg string, err error) { 229 _, err = yesNoConvert(s, defVal) 230 if err != nil { 231 return false, err.Error(), nil 232 } 233 return true, "", nil 234 } 235 a, err := QueryVerify(q+"? "+defaultStr+": ", p.scanner, p.out, p.errOut, verify) 236 if err != nil { 237 return false, errors.Trace(err) 238 } 239 return yesNoConvert(a, defVal) 240 } 241 242 func yesNoConvert(s string, defVal bool) (bool, error) { 243 if s == "" { 244 return defVal, nil 245 } 246 switch strings.ToLower(s) { 247 case "y", "yes": 248 return true, nil 249 case "n", "no": 250 return false, nil 251 default: 252 return false, errors.Errorf("Invalid entry: %q, please choose y or n.", s) 253 } 254 } 255 256 // VerifyOptions is the verifier used by pollster.Select. 257 func VerifyOptions(singular string, options []string, hasDefault bool) VerifyFunc { 258 return func(s string) (ok bool, errmsg string, err error) { 259 if s == "" { 260 return hasDefault, "", nil 261 } 262 for _, opt := range options { 263 if strings.ToLower(opt) == strings.ToLower(s) { 264 return true, "", nil 265 } 266 } 267 return false, fmt.Sprintf("Invalid %s: %q", singular, s), nil 268 } 269 } 270 271 func multiVerify(singular, plural string, options []string, hasDefault bool) VerifyFunc { 272 return func(s string) (ok bool, errmsg string, err error) { 273 if s == "" { 274 return hasDefault, "", nil 275 } 276 vals := set.NewStrings(multiSplit(s)...) 277 opts := set.NewStrings(options...) 278 unknowns := vals.Difference(opts) 279 if len(unknowns) > 1 { 280 list := `"` + strings.Join(unknowns.SortedValues(), `", "`) + `"` 281 return false, fmt.Sprintf("Invalid %s: %s", plural, list), nil 282 } 283 if len(unknowns) > 0 { 284 return false, fmt.Sprintf("Invalid %s: %q", singular, unknowns.Values()[0]), nil 285 } 286 return true, "", nil 287 } 288 } 289 290 func multiSplit(s string) []string { 291 chosen := strings.Split(s, ",") 292 for i := range chosen { 293 chosen[i] = strings.TrimSpace(chosen[i]) 294 } 295 return chosen 296 } 297 298 func sprint(t *template.Template, data interface{}) (string, error) { 299 b := &bytes.Buffer{} 300 if err := t.Execute(b, data); err != nil { 301 return "", err 302 } 303 return b.String(), nil 304 } 305 306 // QuerySchema takes a jsonschema and queries the user to input value(s) for the 307 // schema. It returns an object as defined by the schema (generally a 308 // map[string]interface{} for objects, etc). 309 func (p *Pollster) QuerySchema(schema *jsonschema.Schema) (interface{}, error) { 310 if len(schema.Type) == 0 { 311 return nil, errors.Errorf("invalid schema, no type specified") 312 } 313 if len(schema.Type) > 1 { 314 return nil, errors.Errorf("don't know how to query for a value with multiple types") 315 } 316 var v interface{} 317 var err error 318 if schema.Type[0] == jsonschema.ObjectType { 319 v, err = p.queryObjectSchema(schema) 320 } else { 321 v, err = p.queryOneSchema(schema) 322 } 323 if err != nil { 324 return nil, errors.Trace(err) 325 } 326 return v, nil 327 } 328 329 func (p *Pollster) queryObjectSchema(schema *jsonschema.Schema) (map[string]interface{}, error) { 330 // TODO(natefinch): support for optional values. 331 vals := map[string]interface{}{} 332 if len(schema.Order) != 0 { 333 m, err := p.queryOrder(schema) 334 if err != nil { 335 return nil, errors.Trace(err) 336 } 337 vals = m 338 } else { 339 // traverse alphabetically 340 for _, name := range names(schema.Properties) { 341 v, err := p.queryProp(schema.Properties[name]) 342 if err != nil { 343 return nil, errors.Trace(err) 344 } 345 vals[name] = v 346 } 347 } 348 349 if schema.AdditionalProperties != nil { 350 if err := p.queryAdditionalProps(vals, schema); err != nil { 351 return nil, errors.Trace(err) 352 } 353 } 354 355 return vals, nil 356 } 357 358 // names returns the list of names of schema in alphabetical order. 359 func names(m map[string]*jsonschema.Schema) []string { 360 ret := make([]string, 0, len(m)) 361 for n := range m { 362 ret = append(ret, n) 363 } 364 sort.Strings(ret) 365 return ret 366 } 367 368 func (p *Pollster) queryOrder(schema *jsonschema.Schema) (map[string]interface{}, error) { 369 vals := map[string]interface{}{} 370 for _, name := range schema.Order { 371 prop, ok := schema.Properties[name] 372 if !ok { 373 return nil, errors.Errorf("property %q from Order not in schema", name) 374 } 375 v, err := p.queryProp(prop) 376 if err != nil { 377 return nil, errors.Trace(err) 378 } 379 vals[name] = v 380 } 381 return vals, nil 382 } 383 384 func (p *Pollster) queryProp(prop *jsonschema.Schema) (interface{}, error) { 385 if isObject(prop) { 386 return p.queryObjectSchema(prop) 387 } 388 return p.queryOneSchema(prop) 389 } 390 391 func (p *Pollster) queryAdditionalProps(vals map[string]interface{}, schema *jsonschema.Schema) error { 392 if schema.AdditionalProperties.Type[0] != jsonschema.ObjectType { 393 return errors.Errorf("don't know how to query for additional properties of type %q", schema.AdditionalProperties.Type[0]) 394 } 395 396 verifyName := func(s string) (ok bool, errmsg string, err error) { 397 if s == "" { 398 return false, "", nil 399 } 400 if _, ok := vals[s]; ok { 401 return false, fmt.Sprintf("%s %q already exists", strings.Title(schema.Singular), s), nil 402 } 403 return true, "", nil 404 } 405 406 localEnvVars := func(envVars []string) string { 407 for _, envVar := range envVars { 408 if value, ok := os.LookupEnv(envVar); ok && value != "" { 409 return value 410 } 411 } 412 return "" 413 } 414 415 // Currently we assume we always prompt for at least one value for 416 // additional properties, but we may want to change this to ask if they want 417 // to enter any at all. 418 for { 419 // We assume that the name of the schema is the name of the object the 420 // schema describes, and for additional properties the property name 421 // (i.e. map key) is the "name" of the thing. 422 var name string 423 var err error 424 425 // Note: here we check that schema.Default is empty as well. 426 defFromEnvVar := localEnvVars(schema.EnvVars) 427 if (schema.Default == nil || schema.Default == "") && defFromEnvVar == "" { 428 name, err = p.EnterVerify(schema.Singular+" name", verifyName) 429 if err != nil { 430 return errors.Trace(err) 431 } 432 } else { 433 // If we set a prompt default, that'll get returned as the value, 434 // but it's not the actual value that is the default, so fix that, 435 // if an environment variable wasn't used. 436 var def string 437 if schema.PromptDefault != nil { 438 def = fmt.Sprintf("%v", schema.PromptDefault) 439 } 440 if defFromEnvVar != "" { 441 def = defFromEnvVar 442 } 443 if def == "" { 444 def = fmt.Sprintf("%v", schema.Default) 445 } 446 447 name, err = p.EnterVerifyDefault(schema.Singular, verifyName, def) 448 if err != nil { 449 return errors.Trace(err) 450 } 451 452 if name == def && schema.PromptDefault != nil && name != defFromEnvVar { 453 name = fmt.Sprintf("%v", schema.Default) 454 } 455 } 456 457 v, err := p.queryObjectSchema(schema.AdditionalProperties) 458 if err != nil { 459 return errors.Trace(err) 460 } 461 vals[name] = v 462 more, err := p.YN("Enter another "+schema.Singular, false) 463 if err != nil { 464 return errors.Trace(err) 465 } 466 if !more { 467 break 468 } 469 } 470 471 return nil 472 } 473 474 func (p *Pollster) queryOneSchema(schema *jsonschema.Schema) (interface{}, error) { 475 if len(schema.Type) == 0 { 476 return nil, errors.Errorf("invalid schema, no type specified") 477 } 478 if len(schema.Type) > 1 { 479 return nil, errors.Errorf("don't know how to query for a value with multiple types") 480 } 481 if len(schema.Enum) == 1 { 482 // if there's only one possible value, don't bother prompting, just 483 // return that value. 484 return schema.Enum[0], nil 485 } 486 if schema.Type[0] == jsonschema.ArrayType { 487 return p.queryArray(schema) 488 } 489 if len(schema.Enum) > 1 { 490 return p.selectOne(schema) 491 } 492 var verify VerifyFunc 493 switch schema.Format { 494 case "": 495 // verify stays nil 496 case jsonschema.FormatURI: 497 if p.VerifyURLs != nil { 498 verify = p.VerifyURLs 499 } else { 500 verify = uriVerify 501 } 502 case FormatCertFilename: 503 if p.VerifyCertFile != nil { 504 verify = p.VerifyCertFile 505 } 506 default: 507 // TODO(natefinch): support more formats 508 return nil, errors.Errorf("unsupported format type: %q", schema.Format) 509 } 510 511 if schema.Default == nil { 512 a, err := p.EnterVerify(schema.Singular, verify) 513 if err != nil { 514 return nil, errors.Trace(err) 515 } 516 return convert(a, schema.Type[0]) 517 } 518 519 var def string 520 if schema.PromptDefault != nil { 521 def = fmt.Sprintf("%v", schema.PromptDefault) 522 } 523 var defFromEnvVar string 524 if len(schema.EnvVars) > 0 { 525 for _, envVar := range schema.EnvVars { 526 value := os.Getenv(envVar) 527 if value != "" { 528 defFromEnvVar = value 529 def = defFromEnvVar 530 break 531 } 532 } 533 } 534 if def == "" { 535 def = fmt.Sprintf("%v", schema.Default) 536 } 537 538 a, err := p.EnterVerifyDefault(schema.Singular, verify, def) 539 if err != nil { 540 return nil, errors.Trace(err) 541 } 542 543 // If we set a prompt default, that'll get returned as the value, 544 // but it's not the actual value that is the default, so fix that, 545 // if an environment variable wasn't used. 546 if a == def && schema.PromptDefault != nil && a != defFromEnvVar { 547 a = fmt.Sprintf("%v", schema.Default) 548 } 549 550 return convert(a, schema.Type[0]) 551 } 552 553 func (p *Pollster) queryArray(schema *jsonschema.Schema) (interface{}, error) { 554 if !supportedArraySchema(schema) { 555 b, err := schema.MarshalJSON() 556 if err != nil { 557 // shouldn't ever happen 558 return nil, errors.Errorf("unsupported schema for an array") 559 } 560 return nil, errors.Errorf("unsupported schema for an array: %s", b) 561 } 562 var def string 563 if schema.Default != nil { 564 def = schema.Default.(string) 565 } 566 if schema.PromptDefault != nil { 567 def = schema.PromptDefault.(string) 568 } 569 var array []string 570 if def != "" { 571 array = []string{def} 572 } 573 return p.MultiSelect(MultiList{ 574 Singular: schema.Singular, 575 Plural: schema.Plural, 576 Options: optFromEnum(schema.Items.Schemas[0]), 577 Default: array, 578 }) 579 } 580 581 func uriVerify(s string) (ok bool, errMsg string, err error) { 582 if s == "" { 583 return false, "", nil 584 } 585 _, err = url.Parse(s) 586 if err != nil { 587 return false, fmt.Sprintf("Invalid URI: %q", s), nil 588 } 589 return true, "", nil 590 } 591 592 func supportedArraySchema(schema *jsonschema.Schema) bool { 593 // TODO(natefinch): support arrays without schemas. 594 // TODO(natefinch): support arrays with multiple schemas. 595 // TODO(natefinch): support arrays without Enums. 596 if schema.Items == nil || 597 len(schema.Items.Schemas) != 1 || 598 len(schema.Items.Schemas[0].Enum) == 0 || 599 len(schema.Items.Schemas[0].Type) != 1 { 600 return false 601 } 602 switch schema.Items.Schemas[0].Type[0] { 603 case jsonschema.IntegerType, 604 jsonschema.StringType, 605 jsonschema.BooleanType, 606 jsonschema.NumberType: 607 return true 608 default: 609 return false 610 } 611 } 612 613 func optFromEnum(schema *jsonschema.Schema) []string { 614 ret := make([]string, len(schema.Enum)) 615 for i := range schema.Enum { 616 ret[i] = fmt.Sprint(schema.Enum[i]) 617 } 618 return ret 619 } 620 621 func (p *Pollster) selectOne(schema *jsonschema.Schema) (interface{}, error) { 622 options := make([]string, len(schema.Enum)) 623 for i := range schema.Enum { 624 options[i] = fmt.Sprint(schema.Enum[i]) 625 } 626 def := "" 627 if schema.Default != nil { 628 def = fmt.Sprint(schema.Default) 629 } 630 a, err := p.Select(List{ 631 Singular: schema.Singular, 632 Plural: schema.Plural, 633 Options: options, 634 Default: def, 635 }) 636 if err != nil { 637 return nil, errors.Trace(err) 638 } 639 if schema.Default != nil && a == "" { 640 return schema.Default, nil 641 } 642 return convert(a, schema.Type[0]) 643 } 644 645 func isObject(schema *jsonschema.Schema) bool { 646 for _, t := range schema.Type { 647 if t == jsonschema.ObjectType { 648 return true 649 } 650 } 651 return false 652 } 653 654 // convert converts the given string to a specific value based on the schema 655 // type that validated it. 656 func convert(s string, t jsonschema.Type) (interface{}, error) { 657 switch t { 658 case jsonschema.IntegerType: 659 return strconv.Atoi(s) 660 case jsonschema.NumberType: 661 return strconv.ParseFloat(s, 64) 662 case jsonschema.StringType: 663 return s, nil 664 case jsonschema.BooleanType: 665 switch strings.ToLower(s) { 666 case "y", "yes", "true", "t": 667 return true, nil 668 case "n", "no", "false", "f": 669 return false, nil 670 default: 671 return nil, errors.Errorf("unknown value for boolean type: %q", s) 672 } 673 default: 674 return nil, errors.Errorf("don't know how to convert value %q of type %q", s, t) 675 } 676 } 677 678 // byteAtATimeReader causes all reads to return a single byte. This prevents 679 // things line bufio.scanner from reading past the end of a line, which can 680 // cause problems when we do wacky things like reading directly from the 681 // terminal for password style prompts. 682 type byteAtATimeReader struct { 683 io.Reader 684 } 685 686 func (r byteAtATimeReader) Read(out []byte) (int, error) { 687 return r.Reader.Read(out[:1]) 688 }