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.
This commit is contained in:
parent
69c3165fc1
commit
cee274318d
6 changed files with 250 additions and 149 deletions
152
generate.go
Normal file
152
generate.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
package cv
|
package cv
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"fmt"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewCV returns a new CV value from the given JSON file.
|
// NewCV returns a new CV value from the given JSON file.
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package templates
|
package templates
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
|
||||||
"codeflow.dananglin.me.uk/apollo/spruce/internal/cv"
|
"codeflow.dananglin.me.uk/apollo/spruce/internal/cv"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WithinTimePeriod returns true if the employment's end date is within
|
// WithinTimePeriod returns true if the employment's end date is within
|
||||||
|
|
|
@ -6,6 +6,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/magefile/mage/mg"
|
"github.com/magefile/mage/mg"
|
||||||
"github.com/magefile/mage/sh"
|
"github.com/magefile/mage/sh"
|
||||||
|
@ -20,7 +22,7 @@ var (
|
||||||
// Build builds the binary.
|
// Build builds the binary.
|
||||||
func Build() error {
|
func Build() error {
|
||||||
flags := ldflags()
|
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.
|
// Install installs the binary to the execution path.
|
||||||
|
@ -55,9 +57,11 @@ func Clean() error {
|
||||||
|
|
||||||
// ldflags returns the build flags.
|
// ldflags returns the build flags.
|
||||||
func ldflags() string {
|
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.
|
// version returns the latest git tag using git describe.
|
||||||
|
@ -69,3 +73,13 @@ func version() string {
|
||||||
|
|
||||||
return version
|
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
|
||||||
|
}
|
||||||
|
|
158
main.go
158
main.go
|
@ -2,158 +2,38 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"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/*
|
//go:embed templates/tex/*
|
||||||
var templates embed.FS
|
var templates embed.FS
|
||||||
|
|
||||||
var version string
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var (
|
commandMap := map[string]Runner{
|
||||||
input string
|
"version": NewVersionCommand(),
|
||||||
output string
|
"generate": NewGenerateCommand(),
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := run(input, output, employmentHistory); err != nil {
|
subcommand := os.Args[1]
|
||||||
log.Fatalf("ERROR: %v", err)
|
|
||||||
}
|
runner, ok := commandMap[subcommand]
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
log.Fatalf("ERROR: unknown subcommand '%s'.", subcommand)
|
||||||
}
|
}
|
||||||
|
|
||||||
func run(input, output string, employmentHistory int) error {
|
if err := runner.Parse(os.Args[2:]); err != nil {
|
||||||
historyLimit := time.Now().AddDate(-1*employmentHistory, 0, 0)
|
log.Fatalf("ERROR: unable to parse the command line flags; %v.", err)
|
||||||
|
|
||||||
tempDir, err := os.MkdirTemp("/tmp", "cv-builder-")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to create a temporary directory; %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
if err := runner.Run(); err != nil {
|
||||||
err := os.RemoveAll(tempDir)
|
log.Fatalf("ERROR: unable to run '%s'; %v.", runner.Name(), err)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
55
version.go
Normal file
55
version.go
Normal file
|
@ -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
|
||||||
|
}
|
Loading…
Reference in a new issue