fix: use default FlagSet for spruce help message

Use the default FlagSet to parse all the arguments and to set the
default help message for spruce. Arguments set after the subcommand are
still parsed by the subcommand's FlagSet.

The summaries for all subcommand are defined in one place in the main
function for consistency.

The internal/cmd.SpruceUsage function is replaced with the
spruceUsageFunc function in the main package which returns the usage
function which is set as the default usage function.

The format of the help message for spruce and the subcommands have been
updated with the inspiration of the help message from gopass.
This commit is contained in:
Dan Anglin 2023-08-20 06:14:10 +01:00
parent 8a530551c2
commit 4f90a4c8bb
Signed by: dananglin
GPG key ID: 0C1D44CFBEE68638
5 changed files with 111 additions and 76 deletions

View file

@ -1,13 +1,22 @@
package main
import (
"flag"
"fmt"
"log/slog"
"os"
"slices"
"strings"
"codeflow.dananglin.me.uk/apollo/spruce/internal/cmd"
)
const (
create string = "create"
generate string = "generate"
version string = "version"
)
var (
binaryVersion string
buildTime string
@ -16,13 +25,24 @@ var (
)
func main() {
args := os.Args[1:]
summaries := map[string]string{
create: "creates a new CV JSON file",
generate: "generates a PDF file from an existing CV JSON file",
version: "print the application's version and build information",
}
if len(args) < 1 || args[0] == "--help" || args[0] == "-h" || args[0] == "help" || args[0] == "-help" {
cmd.SpruceUsage()
flag.Usage = spruceUsageFunc(summaries)
flag.Parse()
if flag.NArg() < 1 {
flag.Usage()
os.Exit(0)
}
subcommand := flag.Arg(0)
args := flag.Args()[1:]
logOptions := slog.HandlerOptions{
AddSource: false,
}
@ -31,35 +51,70 @@ func main() {
slog.SetDefault(logger)
subcommand := args[0]
var runner cmd.Runner
switch subcommand {
case "version":
case create:
runner = cmd.NewCreateCommand(create, summaries[create])
case generate:
runner = cmd.NewGenerateCommand(generate, summaries[generate])
case version:
runner = cmd.NewVersionCommand(
binaryVersion,
buildTime,
goVersion,
gitCommit,
version,
summaries[version],
)
case "generate":
runner = cmd.NewGenerateCommand()
case "create":
runner = cmd.NewCreateCommand()
default:
slog.Error("unknown subcommand", "subcommand", subcommand)
cmd.SpruceUsage()
flag.Usage()
os.Exit(1)
}
if err := runner.Parse(os.Args[2:]); err != nil {
if err := runner.Parse(args); err != nil {
slog.Error(fmt.Sprintf("unable to parse the command line flags; %v.", err))
os.Exit(1)
}
if err := runner.Run(); err != nil {
slog.Error(fmt.Sprintf("unable to run '%s'; %v.", runner.Name(), err))
slog.Error(fmt.Sprintf("unable to run %q; %v.", runner.Name(), err))
os.Exit(1)
}
}
func spruceUsageFunc(summaries map[string]string) func() {
cmds := make([]string, len(summaries))
ind := 0
for k := range summaries {
cmds[ind] = k
ind++
}
slices.Sort(cmds)
return func() {
var b strings.Builder
b.WriteString("SUMMARY:\n spruce - A command-line tool for building CVs\n\n")
if binaryVersion != "" {
b.WriteString("VERSION:\n " + binaryVersion + "\n\n")
}
b.WriteString("USAGE:\n spruce [flags]\n spruce [command]\n\nCOMMANDS:")
for _, cmd := range cmds {
fmt.Fprintf(&b, "\n %s\t%s", cmd, summaries[cmd])
}
b.WriteString("\n\nFLAGS:\n -help, --help\n print the help message\n")
flag.VisitAll(func(f *flag.Flag) {
fmt.Fprintf(&b, "\n -%s, --%s\n %s\n", f.Name, f.Name, f.Usage)
})
b.WriteString("\nUse \"spruce [command] --help\" for more information about a command.\n")
w := flag.CommandLine.Output()
fmt.Fprint(w, b.String())
}
}

View file

@ -12,40 +12,20 @@ type Runner interface {
Run() error
}
func SpruceUsage() {
usage := `A tool for building CVs
Usage:
spruce [flags]
spruce [command]
Available Commands:
create create a new CV JSON file
generate generate a PDF file from an existing CV JSON file
version print the application's build information
Flags:
-h, --help
print the help message for spruce
Use "spruce [command] --help" for more information about a command.
`
fmt.Print(usage)
}
func usageFunc(name, summary string, flagset *flag.FlagSet) func() {
return func() {
var b strings.Builder
w := flag.CommandLine.Output()
fmt.Fprintf(&b, "%s\n\nUsage:\n spruce %s [flags]\n\nFlags:", summary, name)
fmt.Fprintf(&b, "SUMMARY:\n %s - %s\n\nUSAGE:\n spruce %s [flags]\n\nFLAGS:", name, summary, name)
flagset.VisitAll(func(f *flag.Flag) {
fmt.Fprintf(&b, "\n --%s\n %s\n", f.Name, f.Usage)
fmt.Fprintf(&b, "\n -%s, --%s\n %s", f.Name, f.Name, f.Usage)
})
b.WriteString("\n")
w := flag.CommandLine.Output()
fmt.Fprint(w, b.String())
}
}

View file

@ -20,20 +20,20 @@ type CreateCommand struct {
filename string
}
func NewCreateCommand() *CreateCommand {
cc := CreateCommand{
FlagSet: flag.NewFlagSet("create", flag.ExitOnError),
summary: "Create a new CV JSON file.",
func NewCreateCommand(name, summary string) *CreateCommand {
command := CreateCommand{
FlagSet: flag.NewFlagSet(name, flag.ExitOnError),
summary: summary,
}
cc.StringVar(&cc.filename, "filepath", "cv.json", "specify the path where the CV JSON document should be created.")
cc.StringVar(&cc.firstName, "first-name", "", "specify your first name.")
cc.StringVar(&cc.jobTitle, "job-title", "", "specify your current job title.")
cc.StringVar(&cc.lastName, "last-name", "", "specify your last name.")
command.StringVar(&command.filename, "filepath", "cv.json", "specify the output path of the CV JSON file.")
command.StringVar(&command.firstName, "first-name", "", "specify your first name.")
command.StringVar(&command.jobTitle, "job-title", "", "specify your current job title.")
command.StringVar(&command.lastName, "last-name", "", "specify your last name.")
cc.Usage = usageFunc(cc.Name(), cc.summary, cc.FlagSet)
command.Usage = usageFunc(command.Name(), command.summary, command.FlagSet)
return &cc
return &command
}
func (c *CreateCommand) Run() error {

View file

@ -27,28 +27,28 @@ func (e noInputSpecifiedError) Error() string {
return "no input file specified, please set the --input field"
}
func NewGenerateCommand() *GenerateCommand {
gc := GenerateCommand{
FlagSet: flag.NewFlagSet("generate", flag.ExitOnError),
summary: "Generate a PDF file from an existing CV JSON file.",
func NewGenerateCommand(name, summary string) *GenerateCommand {
command := GenerateCommand{
FlagSet: flag.NewFlagSet(name, flag.ExitOnError),
summary: summary,
}
gc.StringVar(&gc.input, "input", "", "specify the CV JSON file that you want to input to the builder.")
gc.StringVar(&gc.output, "output", "cv.pdf", "specify the name of the output CV file.")
gc.IntVar(&gc.employmentHistory, "employment-history", 10, "show employment history within these number of years.")
gc.BoolVar(&gc.verbose, "verbose", false, "set to true to enable verbose logging.")
command.StringVar(&command.input, "input", "", "specify the CV JSON file that you want to input to the builder.")
command.StringVar(&command.output, "output", "cv.pdf", "specify the name of the output CV file.")
command.IntVar(&command.employmentHistory, "employment-history", 10, "show employment history within these number of years.")
command.BoolVar(&command.verbose, "verbose", false, "set to true to enable verbose logging.")
gc.Usage = usageFunc(gc.Name(), gc.summary, gc.FlagSet)
command.Usage = usageFunc(command.Name(), command.summary, command.FlagSet)
return &gc
return &command
}
func (g *GenerateCommand) Run() error {
if g.input == "" {
func (c *GenerateCommand) Run() error {
if c.input == "" {
return noInputSpecifiedError{}
}
historyLimit := time.Now().AddDate(-1*g.employmentHistory, 0, 0)
historyLimit := time.Now().AddDate(-1*c.employmentHistory, 0, 0)
tempDir, err := os.MkdirTemp("/tmp", "cv-builder-")
if err != nil {
@ -62,13 +62,13 @@ func (g *GenerateCommand) Run() error {
}
}()
pdfFileName, err := pdf.Generate(tempDir, g.input, historyLimit, g.verbose)
pdfFileName, err := pdf.Generate(tempDir, c.input, historyLimit, c.verbose)
if err != nil {
return fmt.Errorf("unable to create the PDF file; %w", err)
}
if err := copyfile(filepath.Join(tempDir, "cv.pdf"), g.output); err != nil {
return fmt.Errorf("unable to copy %s to %s; %w", pdfFileName, g.output, err)
if err := copyfile(filepath.Join(tempDir, "cv.pdf"), c.output); err != nil {
return fmt.Errorf("unable to copy %s to %s; %w", pdfFileName, c.output, err)
}
return nil

View file

@ -16,29 +16,29 @@ type VersionCommand struct {
gitCommit string
}
func NewVersionCommand(binaryVersion, buildTime, goVersion, gitCommit string) *VersionCommand {
vc := VersionCommand{
FlagSet: flag.NewFlagSet("version", flag.ExitOnError),
func NewVersionCommand(binaryVersion, buildTime, goVersion, gitCommit, name, summary string) *VersionCommand {
command := VersionCommand{
FlagSet: flag.NewFlagSet(name, flag.ExitOnError),
binaryVersion: binaryVersion,
buildTime: buildTime,
goVersion: goVersion,
gitCommit: gitCommit,
summary: "Print the application's build information.",
summary: summary,
}
vc.BoolVar(&vc.fullVersion, "full", false, "prints the full build information")
command.BoolVar(&command.fullVersion, "full", false, "prints the full build information")
vc.Usage = usageFunc(vc.Name(), vc.summary, vc.FlagSet)
command.Usage = usageFunc(command.Name(), command.summary, command.FlagSet)
return &vc
return &command
}
func (v *VersionCommand) Run() error {
func (c *VersionCommand) Run() error {
var b strings.Builder
if v.fullVersion {
fmt.Fprintf(&b, "Spruce\n Version: %s\n Git commit: %s\n Go version: %s\n Build date: %s\n", v.binaryVersion, v.gitCommit, v.goVersion, v.buildTime)
if c.fullVersion {
fmt.Fprintf(&b, "Spruce\n Version: %s\n Git commit: %s\n Go version: %s\n Build date: %s\n", c.binaryVersion, c.gitCommit, c.goVersion, c.buildTime)
} else {
fmt.Fprintln(&b, v.binaryVersion)
fmt.Fprintln(&b, c.binaryVersion)
}
fmt.Print(b.String())