From cee274318d02e3b729549f586055e335b56d6fa5 Mon Sep 17 00:00:00 2001 From: Dan Anglin Date: Fri, 11 Aug 2023 18:33:26 +0100 Subject: [PATCH] feat: add FlagSets to create new subcommands Create new FlagSets to create new subcommands. - The version subcommand prints the version and build info. - The generate subcommand generates the CV PDF documentation. --- generate.go | 152 ++++++++++++++++++++ internal/cv/cv.go | 4 +- internal/templateFuncs/withindaterange.go | 2 +- magefiles/mage.go | 20 ++- main.go | 166 +++------------------- version.go | 55 +++++++ 6 files changed, 250 insertions(+), 149 deletions(-) create mode 100644 generate.go create mode 100644 version.go diff --git a/generate.go b/generate.go new file mode 100644 index 0000000..2e82731 --- /dev/null +++ b/generate.go @@ -0,0 +1,152 @@ +package main + +import ( + "flag" + "fmt" + "io" + "log" + "os" + "os/exec" + "path/filepath" + "text/template" + "time" + + "codeflow.dananglin.me.uk/apollo/spruce/internal/cv" + tf "codeflow.dananglin.me.uk/apollo/spruce/internal/templateFuncs" +) + +type GenerateCommand struct { + fs *flag.FlagSet + input string + output string + employmentHistory int +} + +func NewGenerateCommand() *GenerateCommand { + gc := GenerateCommand{ + fs: flag.NewFlagSet("generate", flag.ExitOnError), + } + + gc.fs.StringVar(&gc.input, "input", "", "specify the CV JSON file that you want to input to the builder.") + gc.fs.StringVar(&gc.output, "output", "", "specify the name of the output CV file.") + gc.fs.IntVar(&gc.employmentHistory, "employment-history", 10, "show employment history within these number of years.") + + return &gc +} + +func (g *GenerateCommand) Parse(args []string) error { + return g.fs.Parse(args) +} + +func (g *GenerateCommand) Name() string { + return g.fs.Name() +} + +func (g *GenerateCommand) Run() error { + historyLimit := time.Now().AddDate(-1*g.employmentHistory, 0, 0) + + tempDir, err := os.MkdirTemp("/tmp", "cv-builder-") + if err != nil { + return fmt.Errorf("unable to create a temporary directory; %w", err) + } + + defer func() { + err := os.RemoveAll(tempDir) + if err != nil { + log.Printf("WARN: An error occurred when removing the temporary directory; %v", err) + } + }() + + texFile, err := tex(g.input, tempDir, historyLimit) + if err != nil { + return fmt.Errorf("unable to create the tex file; %w", err) + } + + if err := pdf(tempDir, texFile, g.output); err != nil { + return fmt.Errorf("unable to create the PDF file; %w", err) + } + + return nil +} + +// tex generates the CV document as a Tex file. +func tex(input, tempDir string, historyLimit time.Time) (string, error) { + c, err := cv.NewCV(input) + if err != nil { + return "", fmt.Errorf("unable to create a new CV value from %s; %w", input, err) + } + + output := filepath.Join(tempDir, "cv.tex") + + file, err := os.Create(output) + if err != nil { + return "", fmt.Errorf("unable to create output file %s; %w", output, err) + } + defer file.Close() + + fmap := template.FuncMap{ + "notLastElement": tf.NotLastElement, + "join": tf.JoinSentences, + "durationToString": tf.FormatDuration, + "withinTimePeriod": tf.WithinTimePeriod(historyLimit), + } + + t := template.Must(template.New("cv.tmpl.tex"). + Funcs(fmap). + Delims("<<", ">>"). + ParseFS(templates, "templates/tex/*.tmpl.tex"), + ) + + if err = t.Execute(file, c); err != nil { + return "", fmt.Errorf("unable to execute the CV template. %w", err) + } + + log.Printf("INFO: Tex file %s was successfully created.", output) + + return output, nil +} + +// pdf generates the CV document as a PDF file from the tex file. +func pdf(tempDir, texFile, output string) error { + pathArg := "--path=" + tempDir + + command := exec.Command("mtxrun", pathArg, "--script", "context", texFile) + + command.Stderr = os.Stderr + command.Stdout = os.Stdout + + if err := command.Run(); err != nil { + return err + } + + if output == "" { + output = "./cv.pdf" + } + + if err := copyfile(filepath.Join(tempDir, "cv.pdf"), output); err != nil { + return err + } + + return nil +} + +func copyfile(input, output string) error { + inputFile, err := os.Open(input) + if err != nil { + return fmt.Errorf("unable to open %s; %w", input, err) + } + defer inputFile.Close() + + outputFile, err := os.Create(output) + if err != nil { + return fmt.Errorf("unable to create %s; %w", output, err) + } + defer outputFile.Close() + + _, err = io.Copy(outputFile, inputFile) + if err != nil { + return fmt.Errorf("unable to copy %s to %s; %w", input, output, err) + } + + return nil +} diff --git a/internal/cv/cv.go b/internal/cv/cv.go index 1e3d1aa..92db668 100644 --- a/internal/cv/cv.go +++ b/internal/cv/cv.go @@ -1,9 +1,9 @@ package cv import ( - "os" - "fmt" "encoding/json" + "fmt" + "os" ) // NewCV returns a new CV value from the given JSON file. diff --git a/internal/templateFuncs/withindaterange.go b/internal/templateFuncs/withindaterange.go index 4c4a91b..c0f1b29 100644 --- a/internal/templateFuncs/withindaterange.go +++ b/internal/templateFuncs/withindaterange.go @@ -1,8 +1,8 @@ package templates import ( - "time" "codeflow.dananglin.me.uk/apollo/spruce/internal/cv" + "time" ) // WithinTimePeriod returns true if the employment's end date is within diff --git a/magefiles/mage.go b/magefiles/mage.go index d63aa1d..9b555e9 100644 --- a/magefiles/mage.go +++ b/magefiles/mage.go @@ -6,6 +6,8 @@ import ( "fmt" "os" "path/filepath" + "runtime" + "time" "github.com/magefile/mage/mg" "github.com/magefile/mage/sh" @@ -20,7 +22,7 @@ var ( // Build builds the binary. func Build() error { flags := ldflags() - return sh.Run("go", "build", "-ldflags="+flags, "-v", "-a", "-o", binary, ".") + return sh.Run("go", "build", "-ldflags="+flags, "-a", "-o", binary, ".") } // Install installs the binary to the execution path. @@ -55,9 +57,11 @@ func Clean() error { // ldflags returns the build flags. func ldflags() string { - ldflagsfmt := "-s -w -X main.version=%s" - return fmt.Sprintf(ldflagsfmt, version()) + ldflagsfmt := "-s -w -X main.binaryVersion=%s -X main.gitCommit=%s -X main.goVersion=%s -X main.buildTime=%s" + buildTime := time.Now().UTC().Format(time.RFC3339) + + return fmt.Sprintf(ldflagsfmt, version(), gitCommit(), runtime.Version(), buildTime) } // version returns the latest git tag using git describe. @@ -69,3 +73,13 @@ func version() string { return version } + +// gitCommit returns the current git commit +func gitCommit() string { + commit, err := sh.Output("git", "rev-parse", "--short", "HEAD") + if err != nil { + commit = "N/A" + } + + return commit +} diff --git a/main.go b/main.go index 03e058f..4822b51 100644 --- a/main.go +++ b/main.go @@ -2,158 +2,38 @@ package main import ( "embed" - "flag" - "fmt" - "io" "log" "os" - "os/exec" - "path/filepath" - "text/template" - "time" - - "codeflow.dananglin.me.uk/apollo/spruce/internal/cv" - tf "codeflow.dananglin.me.uk/apollo/spruce/internal/templateFuncs" ) +type Runner interface { + Parse([]string) error + Name() string + Run() error +} + //go:embed templates/tex/* var templates embed.FS -var version string - func main() { - var ( - input string - output string - employmentHistory int - printVersion bool - ) - - flag.StringVar(&input, "input", "", "specify the CV JSON file that you want to input to the builder.") - flag.StringVar(&output, "output", "", "specify the name of the output CV file.") - flag.IntVar(&employmentHistory, "employment-history", 10, "show employment history within these number of years.") - flag.BoolVar(&printVersion, "version", false, "print the application version and exit.") - flag.Parse() - - if printVersion { - Version() - os.Exit(0) + commandMap := map[string]Runner{ + "version": NewVersionCommand(), + "generate": NewGenerateCommand(), } - if err := run(input, output, employmentHistory); err != nil { - log.Fatalf("ERROR: %v", err) + subcommand := os.Args[1] + + runner, ok := commandMap[subcommand] + + if !ok { + log.Fatalf("ERROR: unknown subcommand '%s'.", subcommand) + } + + if err := runner.Parse(os.Args[2:]); err != nil { + log.Fatalf("ERROR: unable to parse the command line flags; %v.", err) + } + + if err := runner.Run(); err != nil { + log.Fatalf("ERROR: unable to run '%s'; %v.", runner.Name(), err) } } - -func run(input, output string, employmentHistory int) error { - historyLimit := time.Now().AddDate(-1*employmentHistory, 0, 0) - - tempDir, err := os.MkdirTemp("/tmp", "cv-builder-") - if err != nil { - return fmt.Errorf("unable to create a temporary directory; %w", err) - } - - defer func() { - err := os.RemoveAll(tempDir) - if err != nil { - log.Printf("WARN: An error occurred when removing the temporary directory; %v", err) - } - }() - - texFile, err := tex(input, tempDir, historyLimit) - if err != nil { - return fmt.Errorf("unable to create the tex file; %w", err) - } - - if err := pdf(tempDir, texFile, output); err != nil { - return fmt.Errorf("unable to create the PDF file; %w", err) - } - - return nil -} - -// tex generates the CV document as a Tex file. -func tex(input, tempDir string, historyLimit time.Time) (string, error) { - c, err := cv.NewCV(input) - if err != nil { - return "", fmt.Errorf("unable to create a new CV value from %s; %w", input, err) - } - - output := filepath.Join(tempDir, "cv.tex") - - file, err := os.Create(output) - if err != nil { - return "", fmt.Errorf("unable to create output file %s; %w", output, err) - } - defer file.Close() - - fmap := template.FuncMap{ - "notLastElement": tf.NotLastElement, - "join": tf.JoinSentences, - "durationToString": tf.FormatDuration, - "withinTimePeriod": tf.WithinTimePeriod(historyLimit), - } - - t := template.Must(template.New("cv.tmpl.tex"). - Funcs(fmap). - Delims("<<", ">>"). - ParseFS(templates, "templates/tex/*.tmpl.tex"), - ) - - if err = t.Execute(file, c); err != nil { - return "", fmt.Errorf("unable to execute the CV template. %w", err) - } - - log.Printf("INFO: Tex file %s was successfully created.", output) - - return output, nil -} - -// pdf generates the CV document as a PDF file from the tex file. -func pdf(tempDir, texFile, output string) error { - pathArg := "--path=" + tempDir - - command := exec.Command("mtxrun", pathArg, "--script", "context", texFile) - - command.Stderr = os.Stderr - command.Stdout = os.Stdout - - if err := command.Run(); err != nil { - return err - } - - if output == "" { - output = "./cv.pdf" - } - - if err := copyfile(filepath.Join(tempDir, "cv.pdf"), output); err != nil { - return err - } - - return nil -} - -func copyfile(input, output string) error { - inputFile, err := os.Open(input) - if err != nil { - return fmt.Errorf("unable to open %s; %w", input, err) - } - defer inputFile.Close() - - outputFile, err := os.Create(output) - if err != nil { - return fmt.Errorf("unable to create %s; %w", output, err) - } - defer outputFile.Close() - - _, err = io.Copy(outputFile, inputFile) - if err != nil { - return fmt.Errorf("unable to copy %s to %s; %w", input, output, err) - } - - return nil -} - -func Version() { - fmt.Printf("Spruce version %s\n", version) -} diff --git a/version.go b/version.go new file mode 100644 index 0000000..45367bb --- /dev/null +++ b/version.go @@ -0,0 +1,55 @@ +package main + +import ( + "flag" + "fmt" +) + +type VersionCommand struct { + fs *flag.FlagSet + fullVersion bool +} + +var ( + binaryVersion string + buildTime string + goVersion string + gitCommit string +) + +func NewVersionCommand() *VersionCommand { + vc := VersionCommand{ + fs: flag.NewFlagSet("version", flag.ExitOnError), + } + + vc.fs.BoolVar(&vc.fullVersion, "full", false, "prints the full version") + + return &vc +} + +func (v *VersionCommand) Parse(args []string) error { + return v.fs.Parse(args) +} + +func (v *VersionCommand) Name() string { + return v.fs.Name() +} + +func (v *VersionCommand) Run() error { + var version string + if v.fullVersion { + fullVersionFmt := `Spruce + Version: %s + Git commit: %s + Go version: %s + Build date: %s +` + + version = fmt.Sprintf(fullVersionFmt, binaryVersion, gitCommit, goVersion, buildTime) + } else { + version = binaryVersion + "\n" + } + + fmt.Print(version) + return nil +}