Skip to content

Commit 57163a9

Browse files
joschiGitHub CopilotGPT-5
committed
CLI: migrate to Cobra with persistent global flags
- Replace flag parsing with github.com/spf13/cobra - Add persistent flags: --log-level, --verbose, --quiet - Add subcommands: update and validate with their flags - Configure logging in PersistentPreRunE - Preserve context & signal cancellation - Keep existing runUpdate/runValidate implementations Benefits: - Robust CLI with clear help and extensibility - Global options apply to all commands consistently - Cleaner structure matching common Go CLI practices All tests pass. Co-authored-by: GitHub Copilot <github-copilot@github.com> Co-authored-by: GPT-5 <gpt-5@openai.com>
1 parent ebe9f7b commit 57163a9

3 files changed

Lines changed: 117 additions & 90 deletions

File tree

cmd/java-metadata/main.go

Lines changed: 104 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"context"
66
"encoding/json"
77
"errors"
8-
"flag"
98
"fmt"
109
"log/slog"
1110
"os"
@@ -22,6 +21,7 @@ import (
2221
"github.com/joschi/java-metadata/internal/output"
2322
"github.com/joschi/java-metadata/internal/providers"
2423
"github.com/joschi/java-metadata/internal/providers/allproviders"
24+
"github.com/spf13/cobra"
2525

2626
"go.opentelemetry.io/otel"
2727
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp"
@@ -36,83 +36,10 @@ import (
3636
)
3737

3838
func main() {
39-
// Global flags
40-
logLevel := flag.String("log-level", "info", "Log level (debug, info, warn, error)")
41-
verbose := flag.Bool("verbose", false, "Enable verbose output (same as --log-level=debug)")
42-
quiet := flag.Bool("quiet", false, "Quiet mode (same as --log-level=error)")
43-
44-
updateCmd := flag.NewFlagSet("update", flag.ExitOnError)
45-
metadataDir := updateCmd.String("metadata-dir", "./docs/metadata", "Output directory for metadata")
46-
checksumDir := updateCmd.String("checksum-dir", "./docs/checksums", "Output directory for checksums")
47-
concurrency := updateCmd.Int("concurrency", 4, "Number of concurrent provider fetches")
48-
downloadConcurrency := updateCmd.Int("download-concurrency", 3, "Number of concurrent downloads")
49-
maxRetries := updateCmd.Int("max-retries", 3, "Maximum number of retry attempts for downloads")
50-
providerTimeout := updateCmd.Duration("provider-timeout", 5*time.Minute, "Per-provider timeout (e.g. 2m, 30s)")
51-
52-
validateCmd := flag.NewFlagSet("validate", flag.ExitOnError)
53-
validateMetadataDir := validateCmd.String("metadata-dir", "./docs/metadata", "Directory containing metadata files")
54-
validateConcurrency := validateCmd.Int("concurrency", 10, "Number of concurrent URL checks")
55-
validateDelete := validateCmd.Bool("delete", false, "Delete files that fail validation")
56-
57-
if len(os.Args) < 2 {
58-
fmt.Println("Usage: java-metadata [global-options] <command> [options]")
59-
fmt.Println("\nGlobal Options:")
60-
flag.PrintDefaults()
61-
fmt.Println("\nCommands:")
62-
fmt.Println(" update Fetch and update metadata for all vendors")
63-
fmt.Println(" validate Validate URLs in metadata files")
64-
os.Exit(1)
65-
}
66-
67-
// Parse global flags
68-
flag.Parse()
69-
70-
// Configure logging
71-
level := logger.ParseLevel(*logLevel)
72-
if *verbose {
73-
level = logger.LevelDebug
74-
}
75-
if *quiet {
76-
level = logger.LevelError
77-
}
78-
logger.SetLevel(level)
79-
39+
// Root context and signal handling
8040
rootCtx := context.Background()
81-
82-
// Initialize OpenTelemetry with standard environment variable configuration
83-
res, err := initResource(rootCtx)
84-
if err != nil {
85-
logger.Error(rootCtx, "failed to initialize resource", "error", err)
86-
os.Exit(1)
87-
}
88-
tp, err := initTracer(rootCtx, res)
89-
if err != nil {
90-
logger.Error(rootCtx, "failed to initialize tracer", "error", err)
91-
// Continue without tracing rather than failing
92-
} else if tp != nil {
93-
defer func() {
94-
if err := tp.Shutdown(rootCtx); err != nil {
95-
logger.Error(rootCtx, "error shutting down tracer provider", "error", err)
96-
}
97-
}()
98-
}
99-
lp, err := initLogger(rootCtx, res)
100-
if err != nil {
101-
logger.Error(rootCtx, "failed to initialize logger", "error", err)
102-
// Continue without logging rather than failing
103-
} else if lp != nil {
104-
defer func() {
105-
if err := lp.Shutdown(rootCtx); err != nil {
106-
logger.Error(rootCtx, "error shutting down logger provider", "error", err)
107-
}
108-
}()
109-
}
110-
111-
// Root context to allow coordinated cancellation (e.g., future signal handling)
11241
ctx, cancel := context.WithCancel(rootCtx)
11342
defer cancel()
114-
115-
// Handle SIGINT/SIGTERM to cancel in-flight work
11643
sigs := make(chan os.Signal, 1)
11744
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
11845
go func() {
@@ -121,21 +48,108 @@ func main() {
12148
cancel()
12249
}()
12350

124-
switch os.Args[1] {
125-
case "update":
126-
updateCmd.Parse(os.Args[2:])
127-
if err := runUpdate(ctx, *metadataDir, *checksumDir, *concurrency, *downloadConcurrency, *maxRetries, *providerTimeout); err != nil {
128-
logger.Error(ctx, "update failed", "error", err)
129-
os.Exit(1)
130-
}
131-
case "validate":
132-
validateCmd.Parse(os.Args[2:])
133-
if err := runValidate(ctx, *validateMetadataDir, *validateConcurrency, *validateDelete); err != nil {
134-
logger.Error(ctx, "validation failed", "error", err)
135-
os.Exit(1)
136-
}
137-
default:
138-
fmt.Printf("Unknown command: %s\n", os.Args[1])
51+
// Global options
52+
var (
53+
optLogLevel string
54+
optVerbose bool
55+
optQuiet bool
56+
)
57+
58+
rootCmd := &cobra.Command{
59+
Use: "java-metadata",
60+
Short: "Collect and validate Java JDK/JRE metadata",
61+
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
62+
// Configure logging from persistent flags
63+
level := logger.ParseLevel(optLogLevel)
64+
if optVerbose {
65+
level = logger.LevelDebug
66+
}
67+
if optQuiet {
68+
level = logger.LevelError
69+
}
70+
logger.SetLevel(level)
71+
72+
// Initialize OpenTelemetry
73+
res, err := initResource(cmd.Context())
74+
if err != nil {
75+
logger.Error(cmd.Context(), "failed to initialize resource", "error", err)
76+
return nil // continue without resource
77+
}
78+
tp, err := initTracer(cmd.Context(), res)
79+
if err == nil && tp != nil {
80+
// Ensure shutdown on exit
81+
defer func() {
82+
if err := tp.Shutdown(rootCtx); err != nil {
83+
logger.Error(rootCtx, "error shutting down tracer provider", "error", err)
84+
}
85+
}()
86+
}
87+
lp, err := initLogger(cmd.Context(), res)
88+
if err == nil && lp != nil {
89+
defer func() {
90+
if err := lp.Shutdown(rootCtx); err != nil {
91+
logger.Error(rootCtx, "error shutting down logger provider", "error", err)
92+
}
93+
}()
94+
}
95+
return nil
96+
},
97+
Run: func(cmd *cobra.Command, args []string) {
98+
// Show help when no subcommand is provided
99+
_ = cmd.Help()
100+
},
101+
}
102+
103+
// Persistent flags
104+
rootCmd.PersistentFlags().StringVar(&optLogLevel, "log-level", "info", "Log level (debug, info, warn, error)")
105+
rootCmd.PersistentFlags().BoolVar(&optVerbose, "verbose", false, "Enable verbose output (same as --log-level=debug)")
106+
rootCmd.PersistentFlags().BoolVar(&optQuiet, "quiet", false, "Quiet mode (same as --log-level=error)")
107+
108+
// Update command flags
109+
var (
110+
metadataDir string
111+
checksumDir string
112+
concurrency int
113+
downloadConcurrency int
114+
maxRetries int
115+
providerTimeout time.Duration
116+
)
117+
updateCmd := &cobra.Command{
118+
Use: "update",
119+
Short: "Fetch and update metadata for all vendors",
120+
RunE: func(cmd *cobra.Command, args []string) error {
121+
return runUpdate(cmd.Context(), metadataDir, checksumDir, concurrency, downloadConcurrency, maxRetries, providerTimeout)
122+
},
123+
}
124+
updateCmd.Flags().StringVar(&metadataDir, "metadata-dir", "./docs/metadata", "Output directory for metadata")
125+
updateCmd.Flags().StringVar(&checksumDir, "checksum-dir", "./docs/checksums", "Output directory for checksums")
126+
updateCmd.Flags().IntVar(&concurrency, "concurrency", 4, "Number of concurrent provider fetches")
127+
updateCmd.Flags().IntVar(&downloadConcurrency, "download-concurrency", 3, "Number of concurrent downloads")
128+
updateCmd.Flags().IntVar(&maxRetries, "max-retries", 3, "Maximum number of retry attempts for downloads")
129+
updateCmd.Flags().DurationVar(&providerTimeout, "provider-timeout", 5*time.Minute, "Per-provider timeout (e.g. 2m, 30s)")
130+
131+
// Validate command flags
132+
var (
133+
validateMetadataDir string
134+
validateConcurrency int
135+
validateDelete bool
136+
)
137+
validateCmd := &cobra.Command{
138+
Use: "validate",
139+
Short: "Validate URLs in metadata files",
140+
RunE: func(cmd *cobra.Command, args []string) error {
141+
return runValidate(cmd.Context(), validateMetadataDir, validateConcurrency, validateDelete)
142+
},
143+
}
144+
validateCmd.Flags().StringVar(&validateMetadataDir, "metadata-dir", "./docs/metadata", "Directory containing metadata files")
145+
validateCmd.Flags().IntVar(&validateConcurrency, "concurrency", 10, "Number of concurrent URL checks")
146+
validateCmd.Flags().BoolVar(&validateDelete, "delete", false, "Delete files that fail validation")
147+
148+
rootCmd.AddCommand(updateCmd, validateCmd)
149+
rootCmd.SetContext(ctx)
150+
151+
if err := rootCmd.Execute(); err != nil {
152+
logger.Error(ctx, "command failed", "error", err)
139153
os.Exit(1)
140154
}
141155
}

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,12 @@ require (
2323
github.com/go-logr/stdr v1.2.2 // indirect
2424
github.com/google/uuid v1.6.0 // indirect
2525
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
26+
github.com/inconshreveable/mousetrap v1.1.0 // indirect
2627
github.com/samber/lo v1.52.0 // indirect
2728
github.com/samber/slog-common v0.19.0 // indirect
2829
github.com/samber/slog-multi v1.6.0 // indirect
30+
github.com/spf13/cobra v1.10.2 // indirect
31+
github.com/spf13/pflag v1.0.9 // indirect
2932
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
3033
go.opentelemetry.io/contrib/bridges/otelslog v0.14.0 // indirect
3134
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect

go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x
22
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
33
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
44
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
5+
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
56
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
67
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
78
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
@@ -21,14 +22,21 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
2122
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
2223
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
2324
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
25+
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
26+
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
2427
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
2528
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
29+
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
2630
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
2731
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
2832
github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI=
2933
github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M=
3034
github.com/samber/slog-multi v1.6.0 h1:i1uBY+aaln6ljwdf7Nrt4Sys8Kk6htuYuXDHWJsHtZg=
3135
github.com/samber/slog-multi v1.6.0/go.mod h1:qTqzmKdPpT0h4PFsTN5rYRgLwom1v+fNGuIrl1Xnnts=
36+
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
37+
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
38+
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
39+
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
3240
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
3341
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
3442
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
@@ -69,6 +77,7 @@ go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjce
6977
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
7078
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
7179
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
80+
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
7281
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
7382
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
7483
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
@@ -85,5 +94,6 @@ google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
8594
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
8695
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
8796
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
97+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
8898
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
8999
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 commit comments

Comments
 (0)