diff --git a/.gitignore b/.gitignore index 070c410..0a55fde 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ go.work.sum # env file .env +.envrc # Added by goreleaser init: dist/ diff --git a/cmd/q3rcon/main.go b/cmd/q3rcon/main.go index 952f620..64b109b 100644 --- a/cmd/q3rcon/main.go +++ b/cmd/q3rcon/main.go @@ -2,6 +2,7 @@ package main import ( "bufio" + "errors" "flag" "fmt" "io" @@ -11,12 +12,41 @@ import ( "time" "github.com/charmbracelet/log" + "github.com/peterbourgon/ff/v4" + "github.com/peterbourgon/ff/v4/ffhelp" "github.com/onyx-and-iris/q3rcon" ) var version string // Version will be set at build time +type Flags struct { + Host string + Port int + Rconpass string + Interactive bool + LogLevel string + Version bool +} + +func (f Flags) Validate() error { + if f.Port < 1024 || f.Port > 65535 { + return fmt.Errorf( + "invalid port value, got: (%d) expected: in range 1024-65535", + f.Port, + ) + } + + if len(f.Rconpass) < 8 { + return fmt.Errorf( + "invalid rcon password, got: (%s) expected: at least 8 characters", + f.Rconpass, + ) + } + + return nil +} + func main() { var exitCode int @@ -39,71 +69,67 @@ func main() { // run executes the main logic of the application and returns a cleanup function and an error if any. func run() (func(), error) { - var ( - host string - port int - rconpass string - interactive bool - loglevel string - versionFlag bool - ) + var flags Flags - flag.StringVar(&host, "host", "localhost", "hostname of the gameserver") - flag.StringVar(&host, "H", "localhost", "hostname of the gameserver (shorthand)") - flag.IntVar(&port, "port", 28960, "port on which the gameserver resides, default is 28960") - flag.IntVar( - &port, - "p", + fs := ff.NewFlagSet("q3rcon - A command-line RCON client for Quake 3 Arena") + fs.StringVar(&flags.Host, 'H', "host", "localhost", "hostname of the gameserver") + fs.IntVar( + &flags.Port, + 'p', + "port", 28960, - "port on which the gameserver resides, default is 28960 (shorthand)", + "port on which the gameserver resides, default is 28960", ) - flag.StringVar(&rconpass, "rconpass", os.Getenv("RCON_PASS"), "rcon password of the gameserver") - flag.StringVar( - &rconpass, - "r", - os.Getenv("RCON_PASS"), - "rcon password of the gameserver (shorthand)", + fs.StringVar( + &flags.Rconpass, + 'r', + "rconpass", + "", + "rcon password of the gameserver", ) - flag.BoolVar(&interactive, "interactive", false, "run in interactive mode") - flag.BoolVar(&interactive, "i", false, "run in interactive mode (shorthand)") + fs.BoolVar(&flags.Interactive, 'i', "interactive", "run in interactive mode") + fs.StringVar( + &flags.LogLevel, + 'l', + "loglevel", + "info", + "Log level (debug, info, warn, error, fatal, panic)", + ) + fs.BoolVar(&flags.Version, 'v', "version", "print version information and exit") - flag.StringVar(&loglevel, "loglevel", "warn", "log level") - flag.StringVar(&loglevel, "l", "warn", "log level (shorthand)") + err := ff.Parse(fs, os.Args[1:], + ff.WithEnvVarPrefix("Q3RCON"), + ) + switch { + case errors.Is(err, ff.ErrHelp): + fmt.Fprintf(os.Stderr, "%s\n", ffhelp.Flags(fs, "q3rcon [flags] ")) + return nil, nil + case err != nil: + return nil, fmt.Errorf("failed to parse flags: %w", err) + } - flag.BoolVar(&versionFlag, "version", false, "print version information and exit") - flag.BoolVar(&versionFlag, "v", false, "print version information and exit (shorthand)") - - flag.Parse() - - if versionFlag { + if flags.Version { fmt.Printf("q3rcon version: %s\n", versionFromBuild()) return nil, nil } - level, err := log.ParseLevel(loglevel) + if err := flags.Validate(); err != nil { + return nil, err + } + + level, err := log.ParseLevel(flags.LogLevel) if err != nil { - return nil, fmt.Errorf("invalid log level: %s", loglevel) + return nil, fmt.Errorf("invalid log level: %s", flags.LogLevel) } log.SetLevel(level) - if port < 1024 || port > 65535 { - return nil, fmt.Errorf("invalid port value, got: (%d) expected: in range 1024-65535", port) - } - - if len(rconpass) < 8 { - return nil, fmt.Errorf( - "invalid rcon password, got: (%s) expected: at least 8 characters", - rconpass, - ) - } - - client, closer, err := connectRcon(host, port, rconpass) + client, closer, err := connectRcon(flags.Host, flags.Port, flags.Rconpass) if err != nil { return nil, fmt.Errorf("failed to connect to rcon: %w", err) } - if interactive { + if flags.Interactive { fmt.Printf("Enter 'Q' to exit.\n>> ") err := interactiveMode(client, os.Stdin) if err != nil { diff --git a/go.mod b/go.mod index 5181159..a1624c3 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.25.0 require ( github.com/charmbracelet/log v0.4.2 + github.com/peterbourgon/ff/v4 v4.0.0-beta.1 github.com/sirupsen/logrus v1.9.3 ) diff --git a/go.sum b/go.sum index 4fd7d4e..c1c8696 100644 --- a/go.sum +++ b/go.sum @@ -25,6 +25,10 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= +github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/peterbourgon/ff/v4 v4.0.0-beta.1 h1:hV8qRu3V7YfiSMsBSfPfdcznAvPQd3jI5zDddSrDoUc= +github.com/peterbourgon/ff/v4 v4.0.0-beta.1/go.mod h1:onQJUKipvCyFmZ1rIYwFAh1BhPOvftb1uhvSI7krNLc= 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/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -45,6 +49,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=