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  }