github.com/richardwilkes/toolbox@v1.121.0/cmdline/usage.go (about) 1 // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 // 3 // This Source Code Form is subject to the terms of the Mozilla Public 4 // License, version 2.0. If a copy of the MPL was not distributed with 5 // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 // 7 // This Source Code Form is "Incompatible With Secondary Licenses", as 8 // defined by the Mozilla Public License, version 2.0. 9 10 package cmdline 11 12 import ( 13 "fmt" 14 "os" 15 "path/filepath" 16 "runtime/debug" 17 "sort" 18 "strconv" 19 "strings" 20 "time" 21 22 "github.com/richardwilkes/toolbox/i18n" 23 "github.com/richardwilkes/toolbox/xio/term" 24 ) 25 26 var ( 27 // AppCmdName holds the application's name as specified on the command line. 28 AppCmdName string 29 // AppName holds the name of the application. By default, this is the same as AppCmdName. 30 AppName string 31 // CopyrightYears holds the years to place in the copyright banner. Instead of setting this explicitly, consider 32 // using CopyrightStartYear and CopyrightEndYear instead. For example, setting CopyrightStartYear early in your 33 // main() method, and allowing the build system to populate CopyrightEndYear for you. 34 CopyrightYears string 35 // CopyrightStartYear holds the starting year to place in the copyright banner. Will not be used if CopyrightYears 36 // is already set. If not set explicitly, will be set to the year of the "vcs.time" build tag, if available. 37 CopyrightStartYear string 38 // CopyrightEndYear holds the ending year to place in the copyright banner. Will not be used if CopyrightYears is 39 // already set. If not set explicitly, will be set to the year of the "vcs.time" build tag, if available. 40 CopyrightEndYear string 41 // CopyrightHolder holds the name of the copyright holder. 42 CopyrightHolder string 43 // License holds the license the software is being distributed under. This is intended to be a simple one line 44 // description, such as "Mozilla Public License 2.0" and not the full license itself. 45 License string 46 // AppVersion holds the application's version information. If not set explicitly, will be the version of the main 47 // module. Unfortunately, this automatic setting only works for binaries created using 48 // "go install <package>@<version>". 49 AppVersion string 50 // GitVersion holds the vcs revision and clean/dirty status. If not set explicitly, will be generated from the value 51 // of the build tags "vcs.revision" and "vcs.modified". 52 GitVersion string 53 // VCSModified is true if the "vcs.modified" build tag is true. 54 VCSModified bool 55 // BuildNumber holds the build number. If not set explicitly, will be generated from the value of the build tag 56 // "vcs.time". 57 BuildNumber string 58 // AppIdentifier holds the uniform type identifier (UTI) for the application. This should contain only alphanumeric 59 // (A-Z,a-z,0-9), hyphen (-), and period (.) characters. The string should also be in reverse-DNS format. For 60 // example, if your company’s domain is ajax.com and you create an application named Hello, you could assign the 61 // string com.ajax.Hello as your AppIdentifier. 62 AppIdentifier string 63 vcs = "git" 64 ) 65 66 func init() { 67 if path, err := os.Executable(); err == nil { 68 path = filepath.Base(path) 69 if path != "." { 70 AppCmdName = path 71 } 72 } 73 if AppCmdName == "" { 74 AppCmdName = "<unknown>" 75 } 76 if AppName == "" { 77 AppName = AppCmdName 78 } 79 var vcsRevision string 80 var vcsTime time.Time 81 if info, ok := debug.ReadBuildInfo(); ok { 82 if AppVersion == "" && info.Main.Version != "(devel)" { 83 AppVersion = strings.TrimLeft(info.Main.Version, "v") 84 } 85 for _, setting := range info.Settings { 86 switch setting.Key { 87 case "vcs": 88 vcs = setting.Value 89 case "vcs.revision": 90 vcsRevision = setting.Value 91 case "vcs.time": 92 if t, err := time.Parse(time.RFC3339, setting.Value); err == nil { 93 vcsTime = t 94 } 95 case "vcs.modified": 96 if setting.Value == "true" { 97 VCSModified = true 98 } 99 } 100 } 101 } 102 if AppVersion == "" { 103 AppVersion = "0.0" 104 } 105 if GitVersion == "" && vcsRevision != "" { 106 GitVersion = vcsRevision 107 } 108 if vcsTime.IsZero() { 109 vcsTime = time.Now() 110 } 111 if BuildNumber == "" { 112 BuildNumber = vcsTime.Format("20060102150405") 113 } 114 year := strconv.Itoa(vcsTime.Year()) 115 if CopyrightStartYear == "" { 116 CopyrightStartYear = year 117 } 118 if CopyrightEndYear == "" { 119 CopyrightEndYear = year 120 } 121 } 122 123 // ResolveCopyrightYears resolves the copyright years. If the CopyrightYears has been explicitly set, that will be 124 // returned unmodified. Otherwise, it will be generated based on the values of CopyrightStartYear and CopyrightEndYear. 125 func ResolveCopyrightYears() string { 126 if CopyrightYears != "" { 127 return CopyrightYears 128 } 129 years := CopyrightStartYear 130 if CopyrightEndYear != "" && CopyrightEndYear != CopyrightStartYear { 131 if years == "" { 132 years = CopyrightEndYear 133 } else { 134 years += "-" + CopyrightEndYear 135 } 136 } 137 return years 138 } 139 140 // Copyright returns the copyright notice. 141 func Copyright() string { 142 var dot string 143 if !strings.HasSuffix(CopyrightHolder, ".") { 144 dot = "." 145 } 146 return fmt.Sprintf(i18n.Text("Copyright © %[1]s by %[2]s%[3]s All rights reserved."), ResolveCopyrightYears(), 147 CopyrightHolder, dot) 148 } 149 150 // DisplayUsage displays the program usage information. 151 func (cl *CmdLine) DisplayUsage() { 152 term.WrapText(cl, "", AppName) 153 buildInfo := fmt.Sprintf(i18n.Text("Version %s"), ShortVersion()) 154 if BuildNumber != "" { 155 buildInfo = fmt.Sprintf(i18n.Text("%s, Build %s"), buildInfo, BuildNumber) 156 } 157 term.WrapText(cl, " ", buildInfo) 158 if GitVersion != "" { 159 str := vcs + ": " + GitVersion 160 if VCSModified { 161 str += "-modified" 162 } 163 term.WrapText(cl, " ", str) 164 } 165 term.WrapText(cl, " ", Copyright()) 166 if License != "" { 167 term.WrapText(cl, " ", fmt.Sprintf(i18n.Text("License: %s"), License)) 168 } 169 fmt.Fprintln(cl) 170 if cl.Description != "" { 171 term.WrapText(cl, "", cl.Description) 172 fmt.Fprintln(cl) 173 } 174 usage := fmt.Sprintf(i18n.Text("%s [options]"), AppCmdName) 175 opts := cl 176 var stack []*CmdLine 177 for opts != nil { 178 stack = append(stack, opts) 179 opts = opts.parent 180 } 181 for i := len(stack) - 1; i >= 0; i-- { 182 one := stack[i] 183 if one.cmd == nil { 184 if i == 0 && len(cl.cmds) > 0 { 185 usage += i18n.Text(" <command> [command options]") 186 } 187 } else { 188 usage += fmt.Sprintf(i18n.Text(" %[1]s [%[1]s options]"), one.cmd.Name()) 189 } 190 } 191 if cl.UsageSuffix != "" { 192 usage += " " + cl.UsageSuffix 193 } 194 term.WrapText(cl, i18n.Text("Usage: "), usage) 195 for i := len(stack) - 1; i >= 0; i-- { 196 one := stack[i] 197 fmt.Fprintln(one) 198 if one.cmd == nil { 199 if i == 0 { 200 usage += i18n.Text(" <command> [command options]") 201 } 202 fmt.Fprintln(one, i18n.Text("Options:")) 203 } else { 204 fmt.Fprintf(one, i18n.Text("%s options:\n"), one.cmd.Name()) 205 } 206 fmt.Fprintln(one) 207 one.displayOptions() 208 } 209 cl.displayCommands(2) 210 if cl.UsageTrailer != "" { 211 fmt.Fprintln(cl) 212 term.WrapText(cl, "", cl.UsageTrailer) 213 } 214 } 215 216 func (cl *CmdLine) displayOptions() { 217 sort.Sort(cl.options) 218 hasShort := false 219 largest := 0 220 for _, option := range cl.options { 221 if option.usage == "" { 222 continue 223 } 224 if option.single != 0 { 225 hasShort = true 226 } 227 length := len([]rune(option.name)) 228 if length > 0 { 229 length += 2 230 } 231 if !option.isBool() { 232 if length > 0 { 233 length++ 234 } 235 length += 2 + len([]rune(option.arg)) 236 } 237 if length > largest { 238 largest = length 239 } 240 } 241 largest += 2 242 for _, option := range cl.options { 243 if option.usage == "" { 244 continue 245 } 246 var sn string 247 if hasShort { 248 if option.single != 0 { 249 sn = "-" + string(option.single) 250 if option.name != "" { 251 sn += ", " 252 } else { 253 sn += " " 254 } 255 } else { 256 sn = " " 257 } 258 } 259 var ln string 260 if option.name != "" { 261 ln = "--" + option.name 262 } 263 if !option.isBool() { 264 if ln != "" { 265 ln += " " 266 } 267 ln += "<" + option.arg + ">" 268 } 269 prefix := " " + sn + ln + strings.Repeat(" ", largest-len([]rune(ln))) 270 usage := option.usage 271 if !strings.HasSuffix(usage, ".") { 272 usage += "." 273 } 274 if !option.isBool() && option.def != "" { 275 usage += i18n.Text(" Default: ") 276 usage += option.def 277 } 278 term.WrapText(cl, prefix, usage) 279 } 280 } 281 282 func (cl *CmdLine) displayCommands(indent int) { 283 if len(cl.cmds) > 0 { 284 fmt.Fprintln(cl) 285 term.WrapText(cl, "", i18n.Text("Available commands:")) 286 fmt.Fprintln(cl) 287 var all []string 288 largest := 0 289 for key := range cl.cmds { 290 all = append(all, key) 291 length := len(key) 292 if length > largest { 293 largest = length 294 } 295 } 296 sort.Strings(all) 297 format := fmt.Sprintf("%s%%-%ds ", strings.Repeat(" ", indent), largest) 298 for _, cmd := range all { 299 term.WrapText(cl, fmt.Sprintf(format, cmd), cl.cmds[cmd].Usage()) 300 } 301 fmt.Fprintln(cl) 302 term.WrapText(cl, "", fmt.Sprintf(i18n.Text("Use '%s help <command>' to see command options"), AppCmdName)) 303 } 304 }