pkg.re/essentialkaos/ek.10@v12.41.0+incompatible/usage/usage.go (about) 1 // Package usage provides methods and structs for generating usage info for 2 // command-line tools 3 package usage 4 5 // ////////////////////////////////////////////////////////////////////////////////// // 6 // // 7 // Copyright (c) 2022 ESSENTIAL KAOS // 8 // Apache License, Version 2.0 <https://www.apache.org/licenses/LICENSE-2.0> // 9 // // 10 // ////////////////////////////////////////////////////////////////////////////////// // 11 12 import ( 13 "os" 14 "path/filepath" 15 "strings" 16 "time" 17 18 "pkg.re/essentialkaos/ek.v12/fmtc" 19 "pkg.re/essentialkaos/ek.v12/mathutil" 20 "pkg.re/essentialkaos/ek.v12/strutil" 21 "pkg.re/essentialkaos/ek.v12/version" 22 ) 23 24 // ////////////////////////////////////////////////////////////////////////////////// // 25 26 const ( 27 _SPACES = " " 28 _DOTS = "................................................................" 29 ) 30 31 const _BREADCRUMBS_MIN_SIZE = 8 32 33 // ////////////////////////////////////////////////////////////////////////////////// // 34 35 const ( 36 DEFAULT_COMMANDS_COLOR_TAG = "{y}" 37 DEFAULT_OPTIONS_COLOR_TAG = "{g}" 38 DEFAULT_APP_NAME_COLOR_TAG = "{c*}" 39 DEFAULT_APP_VER_COLOR_TAG = "{c}" 40 ) 41 42 // ////////////////////////////////////////////////////////////////////////////////// // 43 44 // About contains info about application 45 type About struct { 46 App string // App is application name 47 Version string // Version is current application version in semver notation 48 Release string // Release is current application release 49 Build string // Build is current application build 50 Desc string // Desc is short info about application 51 Year int // Year is year when owner company was founded 52 License string // License is name of license 53 Owner string // Owner is name of owner (company/developer) 54 BugTracker string // BugTracker is URL of bug tracker 55 56 AppNameColorTag string // AppNameColorTag contains default app name color tag 57 VersionColorTag string // VersionColorTag contains default app version color tag 58 59 // Function for checking application updates 60 UpdateChecker UpdateChecker 61 } 62 63 // Info contains info about commands, options, and examples 64 type Info struct { 65 AppNameColorTag string // AppNameColorTag contains default app name color tag 66 CommandsColorTag string // CommandsColorTag contains default commands color tag 67 OptionsColorTag string // OptionsColorTag contains default options color tag 68 Breadcrumbs bool // Breadcrumbs is flag for using bread crumbs for commands and options output 69 70 Name string // Name is app name 71 Args []string // Args is slice with app arguments 72 Spoiler string // Spoiler contains additional info 73 74 Commands []*Command // Commands is list of supported commands 75 Options []*Option // Options is list of supported options 76 Examples []*Example // Examples is list of usage examples 77 78 curGroup string 79 } 80 81 // UpdateChecker is a base for all update checkers 82 type UpdateChecker struct { 83 Payload string 84 CheckFunc func(app, version, data string) (string, time.Time, bool) 85 } 86 87 // ////////////////////////////////////////////////////////////////////////////////// // 88 89 // Command contains info about supported command 90 type Command struct { 91 Name string // Name is command name 92 Desc string // Desc is command description 93 Group string // Group is group name 94 Args []string // Args is slice with arguments 95 BoundOptions []string // BoundOptions is slice with long names of related options 96 97 ColorTag string // ColorTag contains default color tag 98 99 info *Info 100 } 101 102 // Option contains info about supported option 103 type Option struct { 104 Short string // Short is short option name (with one minus prefix) 105 Long string // Long is long option name (with two minuses prefix) 106 Desc string // Desc is option description 107 Arg string // Arg is option argument 108 109 ColorTag string // ColorTag contains default color tag 110 111 info *Info 112 } 113 114 // Example contains usage example 115 type Example struct { 116 Cmd string // Cmd is command usage example 117 Desc string // Desc is usage description 118 Raw bool // Raw is raw example flag (without automatic binary name appending) 119 120 info *Info 121 } 122 123 // ////////////////////////////////////////////////////////////////////////////////// // 124 125 // NewInfo creates new info struct 126 func NewInfo(args ...string) *Info { 127 var name string 128 129 if len(args) != 0 { 130 name = args[0] 131 args = args[1:] 132 } 133 134 name = strutil.Q(name, filepath.Base(os.Args[0])) 135 136 info := &Info{ 137 Name: name, 138 Args: args, 139 140 CommandsColorTag: DEFAULT_COMMANDS_COLOR_TAG, 141 OptionsColorTag: DEFAULT_OPTIONS_COLOR_TAG, 142 Breadcrumbs: true, 143 } 144 145 return info 146 } 147 148 // AddGroup adds new command group 149 func (i *Info) AddGroup(group string) { 150 i.curGroup = group 151 } 152 153 // AddCommand adds command (name, description, args) 154 func (i *Info) AddCommand(a ...string) { 155 group := "Commands" 156 157 if i.curGroup != "" { 158 group = i.curGroup 159 } 160 161 if len(a) < 2 { 162 return 163 } 164 165 i.Commands = append( 166 i.Commands, 167 &Command{ 168 Name: a[0], 169 Desc: a[1], 170 Args: a[2:], 171 Group: group, 172 info: i, 173 }, 174 ) 175 } 176 177 // AddOption adds option (name, description, args) 178 func (i *Info) AddOption(a ...string) { 179 if len(a) < 2 { 180 return 181 } 182 183 long, short := parseOptionName(a[0]) 184 185 i.Options = append( 186 i.Options, 187 &Option{ 188 Long: long, 189 Short: short, 190 Desc: a[1], 191 Arg: strings.Join(a[2:], " "), 192 info: i, 193 }, 194 ) 195 } 196 197 // AddExample adds example of application usage 198 func (i *Info) AddExample(a ...string) { 199 if len(a) == 0 { 200 return 201 } 202 203 a = append(a, "") 204 205 i.Examples = append(i.Examples, &Example{a[0], a[1], false, i}) 206 } 207 208 // AddRawExample adds example of application usage without command prefix 209 func (i *Info) AddRawExample(a ...string) { 210 if len(a) == 0 { 211 return 212 } 213 214 a = append(a, "") 215 216 i.Examples = append(i.Examples, &Example{a[0], a[1], true, i}) 217 } 218 219 // AddSpoiler adds spoiler 220 func (i *Info) AddSpoiler(spoiler string) { 221 i.Spoiler = spoiler 222 } 223 224 // BoundOptions bounds command with options 225 func (i *Info) BoundOptions(cmd string, options ...string) { 226 for _, command := range i.Commands { 227 if command.Name == cmd { 228 for _, opt := range options { 229 longOption, _ := parseOptionName(opt) 230 command.BoundOptions = append(command.BoundOptions, longOption) 231 } 232 233 return 234 } 235 } 236 } 237 238 // GetCommand tries to find command with given name 239 func (i *Info) GetCommand(name string) *Command { 240 for _, command := range i.Commands { 241 if command.Name == name { 242 return command 243 } 244 } 245 246 return nil 247 } 248 249 // GetOption tries to find option with given name 250 func (i *Info) GetOption(name string) *Option { 251 name, _ = parseOptionName(name) 252 253 for _, option := range i.Options { 254 if option.Long == name { 255 return option 256 } 257 } 258 259 return nil 260 } 261 262 // Render prints usage info to console 263 func (i *Info) Render() { 264 usageMessage := "\n{*}Usage:{!} " + i.AppNameColorTag + i.Name + "{!}" 265 266 if len(i.Options) != 0 { 267 usageMessage += " " + i.OptionsColorTag + "{options}{!}" 268 } 269 270 if len(i.Commands) != 0 { 271 usageMessage += " " + i.CommandsColorTag + "{command}{!}" 272 } 273 274 if len(i.Args) != 0 { 275 usageMessage += " " + strings.Join(i.Args, " ") 276 } 277 278 fmtc.Println(usageMessage) 279 280 if i.Spoiler != "" { 281 fmtc.NewLine() 282 fmtc.Println(i.Spoiler) 283 } 284 285 if len(i.Commands) != 0 { 286 renderCommands(i) 287 } 288 289 if len(i.Options) != 0 { 290 renderOptions(i) 291 } 292 293 if len(i.Examples) != 0 { 294 renderExamples(i) 295 } 296 297 fmtc.NewLine() 298 } 299 300 // ////////////////////////////////////////////////////////////////////////////////// // 301 302 // String returns a string representation of the command 303 func (c *Command) String() string { 304 if c == nil { 305 return "" 306 } 307 308 return c.Name 309 } 310 311 // String returns a string representation of the option 312 func (o *Option) String() string { 313 if o == nil { 314 return "" 315 } 316 317 return "--" + o.Long 318 } 319 320 // ////////////////////////////////////////////////////////////////////////////////// // 321 322 // Render renders info about command 323 func (c *Command) Render() { 324 colorTag := strutil.Q(DEFAULT_COMMANDS_COLOR_TAG, c.ColorTag) 325 size := getCommandSize(c) 326 useBreadcrumbs := true 327 maxSize := size 328 329 if c.info != nil { 330 colorTag = c.info.CommandsColorTag 331 maxSize = getMaxCommandSize(c.info.Commands) 332 useBreadcrumbs = c.info.Breadcrumbs 333 } 334 335 fmtc.Printf(" "+colorTag+"%s{!}", c.Name) 336 337 if len(c.Args) != 0 { 338 fmtc.Printf(" " + renderArgs(c.Args...)) 339 } 340 341 fmtc.Printf(getSeparator(size, maxSize, useBreadcrumbs)) 342 fmtc.Printf(c.Desc) 343 344 fmtc.NewLine() 345 } 346 347 // Render renders info about option 348 func (o *Option) Render() { 349 colorTag := strutil.Q(DEFAULT_OPTIONS_COLOR_TAG, o.ColorTag) 350 size := getOptionSize(o) 351 useBreadcrumbs := true 352 maxSize := size 353 354 if o.info != nil { 355 colorTag = o.info.OptionsColorTag 356 maxSize = getMaxOptionSize(o.info.Options) 357 useBreadcrumbs = o.info.Breadcrumbs 358 } 359 360 fmtc.Printf(" "+colorTag+"%s{!}", formatOptionName(o)) 361 362 if o.Arg != "" { 363 fmtc.Printf(" " + renderArgs(o.Arg)) 364 } 365 366 fmtc.Printf(getSeparator(size, maxSize, useBreadcrumbs)) 367 fmtc.Printf(o.Desc) 368 369 fmtc.NewLine() 370 } 371 372 // Render renders usage example 373 func (e *Example) Render() { 374 appName := os.Args[0] 375 376 if e.info != nil { 377 appName = e.info.Name 378 } 379 380 if e.Raw { 381 fmtc.Printf(" %s\n", e.Cmd) 382 } else { 383 fmtc.Printf(" %s %s\n", appName, e.Cmd) 384 } 385 386 if e.Desc != "" { 387 fmtc.Printf(" {s-}%s{!}\n", e.Desc) 388 } 389 } 390 391 // ////////////////////////////////////////////////////////////////////////////////// // 392 393 // Render prints version info to console 394 func (a *About) Render() { 395 nc := strutil.Q(a.AppNameColorTag, DEFAULT_APP_NAME_COLOR_TAG) 396 vc := strutil.Q(a.VersionColorTag, DEFAULT_APP_VER_COLOR_TAG) 397 398 switch { 399 case a.Build != "": 400 fmtc.Printf( 401 "\n"+nc+"%s{!} "+vc+"%s{!}{s}%s{!} {s-}(%s){!} - %s\n\n", 402 a.App, a.Version, 403 a.Release, a.Build, a.Desc, 404 ) 405 default: 406 fmtc.Printf( 407 "\n"+nc+"%s{!} "+vc+"%s{!}{s}%s{!} - %s\n\n", 408 a.App, a.Version, 409 a.Release, a.Desc, 410 ) 411 } 412 413 if a.Owner != "" { 414 if a.Year == 0 { 415 fmtc.Printf( 416 "{s-}Copyright (C) %d %s{!}\n", 417 time.Now().Year(), a.Owner, 418 ) 419 } else { 420 fmtc.Printf( 421 "{s-}Copyright (C) %d-%d %s{!}\n", 422 a.Year, time.Now().Year(), a.Owner, 423 ) 424 } 425 } 426 427 if a.License != "" { 428 fmtc.Printf("{s-}%s{!}\n", a.License) 429 } 430 431 if a.UpdateChecker.CheckFunc != nil && a.UpdateChecker.Payload != "" { 432 newVersion, releaseDate, hasUpdate := a.UpdateChecker.CheckFunc( 433 a.App, 434 a.Version, 435 a.UpdateChecker.Payload, 436 ) 437 438 if hasUpdate && isNewerVersion(a.Version, newVersion) { 439 printNewVersionInfo(a.Version, newVersion, releaseDate) 440 } 441 } 442 443 fmtc.NewLine() 444 } 445 446 // ////////////////////////////////////////////////////////////////////////////////// // 447 448 // renderCommands renders all supported commands 449 func renderCommands(info *Info) { 450 var curGroup string 451 452 for _, command := range info.Commands { 453 if curGroup != command.Group { 454 printGroupHeader(command.Group) 455 curGroup = command.Group 456 } 457 458 command.Render() 459 } 460 } 461 462 // renderOptions renders all supported options 463 func renderOptions(info *Info) { 464 printGroupHeader("Options") 465 466 for _, option := range info.Options { 467 option.Render() 468 } 469 } 470 471 // renderExamples renders all usage examples 472 func renderExamples(info *Info) { 473 printGroupHeader("Examples") 474 475 total := len(info.Examples) 476 477 for index, example := range info.Examples { 478 example.Render() 479 480 if index < total-1 { 481 fmtc.NewLine() 482 } 483 } 484 } 485 486 // renderArgs renders args with colors 487 func renderArgs(args ...string) string { 488 var result string 489 490 for _, a := range args { 491 if strings.HasPrefix(a, "?") { 492 result += "{s-}" + a[1:] + "{!} " 493 } else { 494 result += "{s}" + a + "{!} " 495 } 496 } 497 498 return fmtc.Sprintf(strings.TrimRight(result, " ")) 499 } 500 501 // formatOptionName formats option name 502 func formatOptionName(opt *Option) string { 503 if opt.Short != "" { 504 return "--" + opt.Long + ", -" + opt.Short 505 } 506 507 return "--" + opt.Long 508 } 509 510 // parseOptionName parses option name 511 func parseOptionName(name string) (string, string) { 512 if strings.Contains(name, ":") { 513 return strutil.ReadField(name, 1, false, ":"), 514 strutil.ReadField(name, 0, false, ":") 515 } 516 517 return name, "" 518 } 519 520 // getSeparator return bread crumbs (or spaces if colors are disabled) for 521 // item name aligning 522 func getSeparator(size, maxSize int, breadcrumbs bool) string { 523 if breadcrumbs && !fmtc.DisableColors && maxSize > _BREADCRUMBS_MIN_SIZE { 524 return " {s-}" + _DOTS[:maxSize-size] + "{!} " 525 } 526 527 return " " + _SPACES[:maxSize-size] + " " 528 } 529 530 // getMaxCommandSize returns the biggest command size 531 func getMaxCommandSize(commands []*Command) int { 532 var size int 533 534 for _, command := range commands { 535 size = mathutil.Max(size, getCommandSize(command)+2) 536 } 537 538 return size 539 } 540 541 // getMaxOptionSize returns the biggest option size 542 func getMaxOptionSize(options []*Option) int { 543 var size int 544 545 for _, option := range options { 546 size = mathutil.Max(size, getOptionSize(option)+2) 547 } 548 549 return size 550 } 551 552 // getOptionSize calculate rendered command size 553 func getCommandSize(cmd *Command) int { 554 size := strutil.Len(cmd.Name) + 2 555 556 for _, arg := range cmd.Args { 557 if strings.HasPrefix(arg, "?") { 558 size += strutil.Len(arg) 559 } else { 560 size += strutil.Len(arg) + 1 561 } 562 } 563 564 return size 565 } 566 567 // getOptionSize calculate rendered option size 568 func getOptionSize(opt *Option) int { 569 var size int 570 571 if opt.Short != "" { 572 size += strutil.Len(opt.Long) + strutil.Len(opt.Short) + 4 573 } else { 574 size += strutil.Len(opt.Long) + 1 575 } 576 577 if opt.Arg != "" { 578 size += strutil.Len(opt.Arg) 579 580 if !strings.HasPrefix(opt.Arg, "?") { 581 size++ 582 } 583 } 584 585 return size 586 } 587 588 // printGroupHeader print category header 589 func printGroupHeader(name string) { 590 fmtc.Printf("\n{*}%s{!}\n\n", name) 591 } 592 593 // isNewerVersion return true if latest version is greater than current 594 func isNewerVersion(current, latest string) bool { 595 v1, err := version.Parse(current) 596 597 if err != nil { 598 return false 599 } 600 601 v2, err := version.Parse(latest) 602 603 if err != nil { 604 return false 605 } 606 607 return v2.Greater(v1) 608 } 609 610 // printNewVersionInfo print info about latest release 611 func printNewVersionInfo(curVersion, newVersion string, releaseDate time.Time) { 612 cv, err := version.Parse(curVersion) 613 614 if err != nil { 615 return 616 } 617 618 nv, err := version.Parse(newVersion) 619 620 if err != nil { 621 return 622 } 623 624 days := int(time.Since(releaseDate) / (time.Hour * 24)) 625 626 colorTag := "{s}" 627 628 switch { 629 case cv.Major() != nv.Major(): 630 colorTag = "{r}" 631 case cv.Minor() != nv.Minor(): 632 colorTag = "{y}" 633 } 634 635 fmtc.NewLine() 636 fmtc.Printf(colorTag+"Latest version is %s{!} ", newVersion) 637 638 switch days { 639 case 0: 640 fmtc.Println("{s-}(released today){!}") 641 case 1: 642 fmtc.Println("{s-}(released 1 day ago){!}") 643 default: 644 fmtc.Printf("{s-}(released %d days ago){!}\n", days) 645 } 646 }