mirror of
https://github.com/chenasraf/sofmani.git
synced 2026-05-17 17:28:04 +00:00
refactor: migrate args parsing to cobra
This commit is contained in:
@@ -1,9 +1,7 @@
|
||||
package appconfig
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -205,109 +203,6 @@ func SetVersion(v string) {
|
||||
AppVersion = v
|
||||
}
|
||||
|
||||
// boolPtr returns a pointer to a boolean value.
|
||||
func boolPtr(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
|
||||
// ParseCliConfig parses command-line arguments and returns an AppCliConfig.
|
||||
func ParseCliConfig() *AppCliConfig {
|
||||
args := os.Args[1:]
|
||||
config := &AppCliConfig{
|
||||
ConfigFile: "",
|
||||
Debug: nil,
|
||||
CheckUpdates: nil,
|
||||
Summary: nil,
|
||||
Filter: []string{},
|
||||
LogFile: nil,
|
||||
ShowLogFile: false,
|
||||
ShowMachineID: false,
|
||||
}
|
||||
file := FindConfigFile()
|
||||
for len(args) > 0 {
|
||||
switch args[0] {
|
||||
case "-d", "--debug":
|
||||
config.Debug = boolPtr(true)
|
||||
case "-D", "--no-debug":
|
||||
config.Debug = boolPtr(false)
|
||||
case "-u", "--update":
|
||||
config.CheckUpdates = boolPtr(true)
|
||||
case "-U", "--no-update":
|
||||
config.CheckUpdates = boolPtr(false)
|
||||
case "-s", "--summary":
|
||||
config.Summary = boolPtr(true)
|
||||
case "-S", "--no-summary":
|
||||
config.Summary = boolPtr(false)
|
||||
case "-f", "--filter":
|
||||
if len(args) > 1 {
|
||||
config.Filter = append(config.Filter, args[1])
|
||||
args = args[1:]
|
||||
}
|
||||
case "-l", "--log-file":
|
||||
if len(args) > 1 && !strings.HasPrefix(args[1], "-") {
|
||||
logFile := args[1]
|
||||
config.LogFile = &logFile
|
||||
args = args[1:]
|
||||
} else {
|
||||
// No value provided, just show log file path
|
||||
config.ShowLogFile = true
|
||||
}
|
||||
case "-m", "--machine-id":
|
||||
config.ShowMachineID = true
|
||||
case "-h", "--help":
|
||||
printHelp()
|
||||
os.Exit(0)
|
||||
case "-v", "--version":
|
||||
printVersion()
|
||||
os.Exit(0)
|
||||
default:
|
||||
if strings.HasPrefix(strings.TrimSpace(args[0]), "-test.") {
|
||||
break
|
||||
}
|
||||
_, err := os.Stat(file)
|
||||
exists := !errors.Is(err, fs.ErrNotExist)
|
||||
if exists {
|
||||
file = args[0]
|
||||
}
|
||||
}
|
||||
args = args[1:]
|
||||
}
|
||||
// If only showing log file or machine ID, don't require config file
|
||||
if config.ShowLogFile || config.ShowMachineID {
|
||||
return config
|
||||
}
|
||||
if file == "" {
|
||||
fmt.Fprintln(os.Stderr, "No config file found")
|
||||
os.Exit(1)
|
||||
}
|
||||
config.ConfigFile = file
|
||||
return config
|
||||
}
|
||||
|
||||
// printHelp prints the command-line help message.
|
||||
func printHelp() {
|
||||
fmt.Println("Usage: sofmani [options] [config_file]")
|
||||
fmt.Println("Options:")
|
||||
fmt.Println(" -d, --debug Enable debug mode")
|
||||
fmt.Println(" -D, --no-debug Disable debug mode")
|
||||
fmt.Println(" -u, --update Enable update checks")
|
||||
fmt.Println(" -U, --no-update Disable update checks")
|
||||
fmt.Println(" -s, --summary Enable installation summary (default)")
|
||||
fmt.Println(" -S, --no-summary Disable installation summary")
|
||||
fmt.Println(" -f, --filter Filter by installer name (can be used multiple times)")
|
||||
fmt.Println(" -l, --log-file Set log file path, or show current path if no value given")
|
||||
fmt.Println(" -m, --machine-id Show machine ID and exit")
|
||||
fmt.Println(" -h, --help Show this help message")
|
||||
fmt.Println(" -v, --version Show version")
|
||||
fmt.Println("")
|
||||
fmt.Println("For online documentation, see https://github.com/chenasraf/sofmani/tree/master/docs")
|
||||
}
|
||||
|
||||
// printVersion prints the application version.
|
||||
func printVersion() {
|
||||
fmt.Println(AppVersion)
|
||||
}
|
||||
|
||||
// NewAppConfig creates a new AppConfig with default values.
|
||||
func NewAppConfig() AppConfig {
|
||||
return AppConfig{
|
||||
|
||||
210
cmd/root.go
Normal file
210
cmd/root.go
Normal file
@@ -0,0 +1,210 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/chenasraf/sofmani/appconfig"
|
||||
"github.com/chenasraf/sofmani/logger"
|
||||
"github.com/chenasraf/sofmani/machine"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
// Flag variables
|
||||
debug bool
|
||||
noDebug bool
|
||||
update bool
|
||||
noUpdate bool
|
||||
summary bool
|
||||
noSummary bool
|
||||
filter []string
|
||||
logFile string
|
||||
machineID bool
|
||||
|
||||
// The parsed CLI config
|
||||
cliConfig *appconfig.AppCliConfig
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands
|
||||
rootCmd = &cobra.Command{
|
||||
Use: "sofmani [flags] [config_file]",
|
||||
Short: "Software manifest installer",
|
||||
Long: `Sofmani is a declarative software manifest installer.
|
||||
It reads a configuration file and installs software based on the manifest.
|
||||
|
||||
For online documentation, see https://github.com/chenasraf/sofmani/tree/master/docs`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
// Build AppCliConfig from parsed flags
|
||||
cliConfig = buildCliConfig(cmd, args)
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Handle --log-file without value: show log file path and exit
|
||||
if cliConfig.ShowLogFile {
|
||||
fmt.Println(logger.GetLogFile())
|
||||
return
|
||||
}
|
||||
|
||||
// Handle --machine-id: show machine ID and exit
|
||||
if cliConfig.ShowMachineID {
|
||||
fmt.Println(machine.GetMachineID())
|
||||
return
|
||||
}
|
||||
|
||||
// Run the main application logic
|
||||
RunMain(cliConfig)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
func Execute() {
|
||||
// Pre-process args to handle -l/--log-file with space-separated value
|
||||
// This maintains backward compatibility with the original CLI behavior
|
||||
os.Args = preprocessArgs(os.Args)
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// preprocessArgs handles the -l/--log-file flag which has an optional value:
|
||||
// - "-l" alone or "--log-file" alone -> show current log path
|
||||
// - "-l value" or "--log-file value" -> set log file to value
|
||||
// This transforms the args so Cobra can handle them properly.
|
||||
func preprocessArgs(args []string) []string {
|
||||
result := make([]string, 0, len(args))
|
||||
i := 0
|
||||
for i < len(args) {
|
||||
arg := args[i]
|
||||
|
||||
// Check if this is -l or --log-file without an = sign
|
||||
isLogFlag := arg == "-l" || arg == "--log-file"
|
||||
|
||||
if isLogFlag {
|
||||
// Check if there's a next argument that could be the value
|
||||
if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
|
||||
// Next arg is the value - combine into -l=value format
|
||||
result = append(result, "-l="+args[i+1])
|
||||
i += 2
|
||||
continue
|
||||
} else {
|
||||
// No value provided - use sentinel to indicate "show log path"
|
||||
result = append(result, "-l=:show:")
|
||||
i++
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, arg)
|
||||
i++
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetCliConfig returns the parsed CLI configuration.
|
||||
func GetCliConfig() *appconfig.AppCliConfig {
|
||||
return cliConfig
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Disable alphabetical sorting to control flag order in help output
|
||||
rootCmd.Flags().SortFlags = false
|
||||
|
||||
// Boolean flags with negation variants (grouped together)
|
||||
rootCmd.Flags().BoolVarP(&debug, "debug", "d", false, "Enable debug mode")
|
||||
rootCmd.Flags().BoolVarP(&noDebug, "no-debug", "D", false, "Disable debug mode")
|
||||
rootCmd.Flags().BoolVarP(&update, "update", "u", false, "Enable update checks")
|
||||
rootCmd.Flags().BoolVarP(&noUpdate, "no-update", "U", false, "Disable update checks")
|
||||
rootCmd.Flags().BoolVarP(&summary, "summary", "s", false, "Enable installation summary")
|
||||
rootCmd.Flags().BoolVarP(&noSummary, "no-summary", "S", false, "Disable installation summary")
|
||||
|
||||
// Filter flag (repeatable)
|
||||
rootCmd.Flags().StringArrayVarP(&filter, "filter", "f", nil, "Filter by installer name (can be used multiple times)")
|
||||
|
||||
// Log file flag - optional value handled via arg preprocessing
|
||||
rootCmd.Flags().StringVarP(&logFile, "log-file", "l", "", "Set log file path (use flag alone to show current path)")
|
||||
|
||||
// Machine ID flag
|
||||
rootCmd.Flags().BoolVarP(&machineID, "machine-id", "m", false, "Show machine ID and exit")
|
||||
}
|
||||
|
||||
// SetVersion sets the version for the root command.
|
||||
func SetVersion(version string) {
|
||||
rootCmd.Version = version
|
||||
// Use custom template to match original output format (just version number)
|
||||
rootCmd.SetVersionTemplate("{{.Version}}\n")
|
||||
appconfig.SetVersion(version)
|
||||
}
|
||||
|
||||
// buildCliConfig creates an AppCliConfig from the parsed Cobra flags.
|
||||
func buildCliConfig(cmd *cobra.Command, args []string) *appconfig.AppCliConfig {
|
||||
config := &appconfig.AppCliConfig{
|
||||
ConfigFile: "",
|
||||
Debug: nil,
|
||||
CheckUpdates: nil,
|
||||
Summary: nil,
|
||||
Filter: filter,
|
||||
LogFile: nil,
|
||||
ShowLogFile: false,
|
||||
ShowMachineID: machineID,
|
||||
}
|
||||
|
||||
// Handle debug flag
|
||||
if cmd.Flags().Changed("debug") {
|
||||
config.Debug = boolPtr(true)
|
||||
}
|
||||
if cmd.Flags().Changed("no-debug") {
|
||||
config.Debug = boolPtr(false)
|
||||
}
|
||||
|
||||
// Handle update flag
|
||||
if cmd.Flags().Changed("update") {
|
||||
config.CheckUpdates = boolPtr(true)
|
||||
}
|
||||
if cmd.Flags().Changed("no-update") {
|
||||
config.CheckUpdates = boolPtr(false)
|
||||
}
|
||||
|
||||
// Handle summary flag
|
||||
if cmd.Flags().Changed("summary") {
|
||||
config.Summary = boolPtr(true)
|
||||
}
|
||||
if cmd.Flags().Changed("no-summary") {
|
||||
config.Summary = boolPtr(false)
|
||||
}
|
||||
|
||||
// Handle log file flag
|
||||
if cmd.Flags().Changed("log-file") {
|
||||
if logFile == ":show:" {
|
||||
// Flag was provided without a value
|
||||
config.ShowLogFile = true
|
||||
} else {
|
||||
config.LogFile = &logFile
|
||||
}
|
||||
}
|
||||
|
||||
// Handle config file positional argument
|
||||
if len(args) > 0 {
|
||||
config.ConfigFile = args[0]
|
||||
} else if !config.ShowLogFile && !config.ShowMachineID {
|
||||
// Find config file if not showing log file or machine ID
|
||||
file := appconfig.FindConfigFile()
|
||||
if file == "" {
|
||||
fmt.Fprintln(os.Stderr, "No config file found")
|
||||
os.Exit(1)
|
||||
}
|
||||
config.ConfigFile = file
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// boolPtr returns a pointer to a boolean value.
|
||||
func boolPtr(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
|
||||
// RunMain is set by main.go to run the main application logic.
|
||||
var RunMain func(cliConfig *appconfig.AppCliConfig)
|
||||
@@ -4,13 +4,6 @@ import (
|
||||
"github.com/chenasraf/sofmani/appconfig"
|
||||
)
|
||||
|
||||
// LoadConfig loads the application configuration.
|
||||
// It parses command-line arguments and then parses the configuration file.
|
||||
func LoadConfig() (*appconfig.AppConfig, error) {
|
||||
overrides := appconfig.ParseCliConfig()
|
||||
return loadConfigFromCli(overrides)
|
||||
}
|
||||
|
||||
// loadConfigFromCli loads the application configuration from pre-parsed CLI config.
|
||||
func loadConfigFromCli(overrides *appconfig.AppCliConfig) (*appconfig.AppConfig, error) {
|
||||
cfg, err := appconfig.ParseConfig(overrides)
|
||||
|
||||
3
go.mod
3
go.mod
@@ -7,6 +7,7 @@ require (
|
||||
github.com/eschao/config v0.1.0
|
||||
github.com/mattn/go-runewidth v0.0.19
|
||||
github.com/samber/lo v1.47.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.9.0
|
||||
golang.org/x/term v0.39.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
@@ -14,7 +15,9 @@ require (
|
||||
|
||||
require (
|
||||
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/spf13/pflag v1.0.9 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
|
||||
9
go.sum
9
go.sum
@@ -1,20 +1,29 @@
|
||||
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
|
||||
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/eschao/config v0.1.0 h1:vtlNamzs6dC9pE0zyplqql16PFUUlst3VttQ+IT2/rk=
|
||||
github.com/eschao/config v0.1.0/go.mod h1:XMilcx0dPfk+tlJowGZPZdmdCRnd7AZuFhYA93tYBgA=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
|
||||
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||
|
||||
24
main.go
24
main.go
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/chenasraf/sofmani/appconfig"
|
||||
"github.com/chenasraf/sofmani/cmd"
|
||||
"github.com/chenasraf/sofmani/installer"
|
||||
"github.com/chenasraf/sofmani/logger"
|
||||
"github.com/chenasraf/sofmani/machine"
|
||||
@@ -18,25 +19,18 @@ import (
|
||||
//go:embed version.txt
|
||||
var appVersion []byte // appVersion is embedded from version.txt and contains the application version.
|
||||
|
||||
func init() {
|
||||
cmd.RunMain = runMain
|
||||
}
|
||||
|
||||
// main is the entry point of the application.
|
||||
func main() {
|
||||
appconfig.SetVersion(strings.TrimSpace(string(appVersion)))
|
||||
|
||||
// Parse CLI config first to check for --log-file flag
|
||||
cliConfig := appconfig.ParseCliConfig()
|
||||
|
||||
// Handle --log-file without value: show log file path and exit
|
||||
if cliConfig.ShowLogFile {
|
||||
fmt.Println(logger.GetLogFile())
|
||||
return
|
||||
}
|
||||
|
||||
// Handle --machine-id: show machine ID and exit
|
||||
if cliConfig.ShowMachineID {
|
||||
fmt.Println(machine.GetMachineID())
|
||||
return
|
||||
cmd.SetVersion(strings.TrimSpace(string(appVersion)))
|
||||
cmd.Execute()
|
||||
}
|
||||
|
||||
// runMain runs the main application logic with the given CLI config.
|
||||
func runMain(cliConfig *appconfig.AppCliConfig) {
|
||||
// Set custom log file if provided
|
||||
if cliConfig.LogFile != nil {
|
||||
logger.SetLogFile(*cliConfig.LogFile)
|
||||
|
||||
Reference in New Issue
Block a user