Reimplement the CLI with flags first package.

CLI configuration may now be managed with env vars.

improved CLI help output
This commit is contained in:
onyx-and-iris 2026-02-17 16:09:53 +00:00
parent 0b9546ee0e
commit fce6fa43fc
4 changed files with 80 additions and 46 deletions

1
.gitignore vendored
View File

@ -24,6 +24,7 @@ go.work.sum
# env file
.env
.envrc
# Added by goreleaser init:
dist/

View File

@ -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] <vban commands>"))
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 {

1
go.mod
View File

@ -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
)

6
go.sum
View File

@ -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=