From b2f11f61043328d94a96346ee7a6f26ec65accb89a19f41c1fb00cda8bd719da Mon Sep 17 00:00:00 2001 From: Lixen Wraith Date: Mon, 5 Oct 2026 04:19:46 -0400 Subject: [PATCH] v0.26.4 cli enhancements --- cmd/lw/cli.go | 59 ++++++++++++++++++++--------- cmd/lw/cli_test.go | 78 ++++++++++++++++++++++++++++++++++----- cmd/lw/help.go | 9 ++--- cmd/lw/main.go | 2 +- cmd/lw/tls.go | 5 ++- doc/cli.md | 28 ++++++++------ doc/configuration.md | 17 +++------ doc/lw.1 | 20 ++++++---- doc/todo.md | 14 +------ internal/config/loader.go | 19 ++++++++-- test/preset-test.sh | 5 ++- 11 files changed, 174 insertions(+), 82 deletions(-) diff --git a/cmd/lw/cli.go b/cmd/lw/cli.go index 8745c1b..858e73f 100644 --- a/cmd/lw/cli.go +++ b/cmd/lw/cli.go @@ -6,6 +6,7 @@ import ( "flag" "fmt" "io" + "regexp" "slices" "strings" "unicode" @@ -72,8 +73,10 @@ func (c *command) run(args []string, stdout, stderr io.Writer) int { } s := c.subcommands[i] fs := flag.NewFlagSet("lw "+c.name+" "+s.name, flag.ContinueOnError) - fs.SetOutput(stderr) - fs.Usage = func() { + // flag names a long flag with one dash: its errors, and the usage, print below + fs.SetOutput(io.Discard) + fs.Usage = func() {} + usage := func() { fmt.Fprintf(stderr, "Usage: %s %s\n\n%s.\n\n", fs.Name(), s.synopsis, s.summary) printFlags(stderr, fs) } @@ -83,13 +86,16 @@ func (c *command) run(args []string, stdout, stderr io.Writer) int { fs.Var(f.Value, letter, "") // no usage: printFlags shows it with its long flag } } - if err := fs.Parse(args[1:]); err != nil { - if errors.Is(err, flag.ErrHelp) { - return 0 - } - return 2 // already reported by flag, with the usage + err := fs.Parse(args[1:]) + switch { + case errors.Is(err, flag.ErrHelp): + usage() + return 0 + case err != nil: + err = usageError(longFlag.ReplaceAllString(err.Error(), "$1--$2")) + default: + err = checkArgs(fs, s.required) } - err := checkArgs(fs, s.required) if err == nil { err = work(stdout, stderr) } @@ -98,12 +104,15 @@ func (c *command) run(args []string, stdout, stderr io.Writer) int { } fmt.Fprintf(stderr, "%s: %v\n", fs.Name(), err) if _, ok := errors.AsType[usageError](err); ok { - fs.Usage() + usage() return 2 } return 1 } +// longFlag is a flag name of two or more letters in flag's errors +var longFlag = regexp.MustCompile(`(^|\s)-(\w[\w-]+)`) + func (c *command) usage(w io.Writer) { fmt.Fprintf(w, "Usage: lw %s COMMAND [flags]\n\n", c.name) for _, s := range c.subcommands { @@ -162,9 +171,9 @@ type invocation struct { } // parseCommandLine reads lw's own flags, a short one as its long one; any -// other --flag is a --path=value setting for lixenwraith/config, which reports -// what it does not know. A pipeline flag takes the next argument unless it -// starts with '-'. A help request wins over a malformed flag. +// other --flag is a setting, passed to lixenwraith/config as --path=value or a +// bare switch so it never guesses. A flag's value is the next argument unless +// that starts with '-'. Every error is a usage error; a help request wins. func parseCommandLine(argv []string) (inv invocation, err error) { defer func() { if inv.help { @@ -175,9 +184,11 @@ func parseCommandLine(argv []string) (inv invocation, err error) { if i := slices.IndexFunc(commands, func(c command) bool { return c.name == argv[0] }); i >= 0 { return invocation{command: &commands[i], args: argv[1:]}, nil } - inv.help = argv[0] == "help" } - specFlags := config.SpecFlags() + specFlags, settings := config.SpecFlags(), config.Settings() + unexpected := func(word string) error { + return fmt.Errorf("unexpected argument %q: lw takes options only (a configuration file is -c FILE, a switch's value follows '=')", word) + } next := func(i *int) string { if *i+1 < len(argv) && !strings.HasPrefix(argv[*i+1], "-") { *i++ @@ -194,9 +205,11 @@ func parseCommandLine(argv []string) (inv invocation, err error) { } switch { case arg == "--": - inv.load.Overrides = append(inv.load.Overrides, argv[i:]...) + if i+1 < len(argv) { + err = cmp.Or(err, unexpected(argv[i+1])) + } return inv, err - case arg == "--help": + case arg == "--help" || i == 0 && arg == "help": inv.help = true case name == "--config": if !inline { @@ -215,9 +228,21 @@ func parseCommandLine(argv []string) (inv invocation, err error) { } inv.load.Specs = append(inv.load.Specs, config.Spec{Flag: name[2:], Value: value}) case len(name) > 1 && name[0] == '-' && unicode.IsLetter(rune(name[1])): - // config reads only --flags; a negative number stays a value + // lw's only single-dash options are the shorts err = cmp.Or(err, fmt.Errorf("unknown option %s: long options take two dashes, and a value that starts with '-' follows '=' (lw --help lists them)", typed)) + case !strings.HasPrefix(name, "--"): + err = cmp.Or(err, unexpected(arg)) default: + isSwitch, known := settings[name[2:]] + if !known { + err = cmp.Or(err, fmt.Errorf("unknown option %s (lw --help lists them)", typed)) + } + if !inline && !isSwitch { + if value = next(&i); value == "" && known { + err = cmp.Or(err, fmt.Errorf("%s needs a value; one that starts with '-' follows '='", typed)) + } + arg += "=" + value + } inv.load.Overrides = append(inv.load.Overrides, arg) } } diff --git a/cmd/lw/cli_test.go b/cmd/lw/cli_test.go index 675819d..98f4c98 100644 --- a/cmd/lw/cli_test.go +++ b/cmd/lw/cli_test.go @@ -7,6 +7,7 @@ import ( "path/filepath" "reflect" "regexp" + "strconv" "strings" "testing" @@ -16,9 +17,9 @@ import ( ) // lw's own flags: a short is its long flag, an unknown single-dash one fails, -// the last -c wins, a pipeline flag or --color takes the next argument unless -// it starts with '-', -- ends lw's flags, and a command is only the first -// argument. Everything else is a setting for config. +// the last -c wins, a flag takes the next argument unless it starts with '-', +// a switch takes none, and a command is only the first argument. A setting +// reaches config as --path=value or a bare switch. func TestCommandLineGrammar(t *testing.T) { for _, c := range []struct { argv []string @@ -30,15 +31,14 @@ func TestCommandLineGrammar(t *testing.T) { invocation{load: config.Args{Specs: []config.Spec{{Flag: "source", Value: "null"}, {Flag: "sink", Value: "http,port=1"}, {Flag: "filter"}, {Flag: "sink"}}}}}, {[]string{"--preset", "tail,path=x", "--pipeline", "b"}, invocation{load: config.Args{Specs: []config.Spec{{Flag: "preset", Value: "tail,path=x"}, {Flag: "pipeline", Value: "b"}}}}}, {[]string{"--color", "--color", "never", "--color=auto"}, invocation{load: config.Args{Overrides: []string{"--color=always", "--color=never", "--color=auto"}}}}, - {[]string{"--quiet", "--", "-c", "x", "--source", "null"}, invocation{load: config.Args{Overrides: []string{"--quiet", "--", "-c", "x", "--source", "null"}}}}, + {[]string{"--quiet", "--"}, invocation{load: config.Args{Overrides: []string{"--quiet"}}}}, {[]string{"--quiet", "-h"}, invocation{help: true, load: config.Args{Overrides: []string{"--quiet"}}}}, {[]string{"-c", "-h"}, invocation{help: true}}, - {[]string{"help"}, invocation{help: true, load: config.Args{Overrides: []string{"help"}}}}, + {[]string{"help"}, invocation{help: true}}, {[]string{"tls", "ca", "-h"}, invocation{command: &commands[1], args: []string{"ca", "-h"}}}, - {[]string{"--quiet", "tls"}, invocation{load: config.Args{Overrides: []string{"--quiet", "tls"}}}}, - {[]string{"-q", "-t", "-V", "-p", "tail,path=x", "-c=a.toml", "--logging.file.retention_hours", "-1"}, + {[]string{"-q", "-t", "-V", "-p", "tail,path=x", "-c=a.toml", "--logging.level", "debug", "--logging.file.retention_hours=-1", "--dump=false"}, invocation{load: config.Args{File: "a.toml", Specs: []config.Spec{{Flag: "preset", Value: "tail,path=x"}}, - Overrides: []string{"--quiet", "--check", "--version", "--logging.file.retention_hours", "-1"}}}}, + Overrides: []string{"--quiet", "--check", "--version", "--logging.level=debug", "--logging.file.retention_hours=-1", "--dump=false"}}}}, } { got, err := parseCommandLine(c.argv) if err != nil || !reflect.DeepEqual(got, c.want) { @@ -46,13 +46,56 @@ func TestCommandLineGrammar(t *testing.T) { } } for _, argv := range [][]string{{"-c"}, {"--config"}, {"--config="}, {"-c="}, {"-c", "--quiet"}, - {"-v"}, {"-qt"}, {"-u", "x"}, {"-config", "x"}} { + {"-v"}, {"-qt"}, {"-u", "x"}, {"-config", "x"}, {"--logging.level"}, {"--logging.file.retention_hours", "-1"}} { if _, err := parseCommandLine(argv); err == nil { t.Errorf("%q accepted", argv) } } } +// lw takes no positional argument: a word no flag takes is named, not passed +// to config, whose bool would take it as its value +func TestAWordNoFlagTakesIsAnError(t *testing.T) { + for _, c := range []struct { + argv []string + word string + }{ + {[]string{"tail"}, "tail"}, {[]string{"-t", "x.toml"}, "x.toml"}, {[]string{"--quiet", "tls"}, "tls"}, + {[]string{"--status_reporter", "false"}, "false"}, {[]string{"--logging.level", "debug", "x"}, "x"}, + {[]string{"--color=never", "x"}, "x"}, {[]string{"--", "-c"}, "-c"}, {[]string{"-1"}, "-1"}, + } { + _, err := parseCommandLine(c.argv) + if err == nil || !strings.Contains(err.Error(), "unexpected argument "+strconv.Quote(c.word)) { + t.Errorf("%q: %v", c.argv, err) + } + } +} + +// An option lw does not know is an error, whatever follows it +func TestAnUnknownOptionIsAnError(t *testing.T) { + for _, argv := range [][]string{{"--nosuch"}, {"--nosuch=1"}, {"--nosuch", "x"}, {"--check", "--nosuch"}} { + _, err := parseCommandLine(argv) + if err == nil || err.Error() != "unknown option --nosuch (lw --help lists them)" { + t.Errorf("%q: %v", argv, err) + } + } +} + +// flag writes a long flag with one dash; lw names it with two, then the usage +func TestFlagErrorsNameLongFlagsWithTwoDashes(t *testing.T) { + for args, want := range map[string]string{ + "--user": "lw auth add-user: flag needs an argument: --user\nUsage: ", + "-u": "lw auth add-user: flag needs an argument: -u\nUsage: ", + "-nosuch": "lw auth add-user: flag provided but not defined: --nosuch\nUsage: ", + "--user=x": "lw auth add-user: --credentials is required\nUsage: ", + } { + code, _, stderr := runCommand(t, "auth", "add-user", args) + if code != 2 || !strings.HasPrefix(stderr, want) || strings.Count(stderr, "Usage: ") != 1 { + t.Errorf("lw auth add-user %s: exit %d:\n%s", args, code, stderr) + } + } +} + // Every subcommand's -h lists each flag once, GNU style, with its short form // beside it: " -u, --user NAME" or " --credentials FILE" func TestUsageListsEachFlagOnce(t *testing.T) { @@ -63,6 +106,9 @@ func TestUsageListsEachFlagOnce(t *testing.T) { s.define(fs) want := 0 fs.VisitAll(func(f *flag.Flag) { + if f.Usage == "" { + return // an earlier name, unlisted + } want++ head := " --" + f.Name for letter, short := range shorts { @@ -139,13 +185,25 @@ func TestEveryPresetBuilds(t *testing.T) { } } +// --host, the earlier name of lw tls cert --hosts, still sets the hosts +func TestTLSCertHostIsHosts(t *testing.T) { + dir := t.TempDir() + if code, _, stderr := runCommand(t, "tls", "ca", "--dir", dir); code != 0 { + t.Fatalf("ca: exit %d: %s", code, stderr) + } + code, _, stderr := runCommand(t, "tls", "cert", "--ca-dir", dir, "--name", "agg", "--server", "--host", "a.example") + if code != 0 || !strings.Contains(stderr, "for [a.example CN=agg]") { + t.Fatalf("exit %d: %s", code, stderr) + } +} + // lw tls ca and lw tls cert write a chain a dialer verifies by the CA file, // keys private, and never replace a file func TestTLSCommandsIssueAVerifiableChain(t *testing.T) { dir := t.TempDir() steps := [][]string{ {"ca", "-dir", dir, "-name", "test CA"}, - {"cert", "-ca-dir", dir, "-name", "agg", "-server", "-host", "127.0.0.1"}, + {"cert", "-ca-dir", dir, "-name", "agg", "-server", "-hosts", "127.0.0.1"}, } for _, args := range steps { if code, _, stderr := runCommand(t, "tls", args...); code != 0 { diff --git a/cmd/lw/help.go b/cmd/lw/help.go index 8fb7319..76cda61 100644 --- a/cmd/lw/help.go +++ b/cmd/lw/help.go @@ -23,8 +23,8 @@ Usage: Commands (lw COMMAND -h lists its subcommands): %s Options: a short option is its long one; a value follows as the next argument -or after '=' (-c FILE, --config=FILE), one that starts with '-' after '='; -short options do not combine. +or after '=' (-c FILE, --config=FILE), one that starts with '-' or a switch's +after '='; short options do not combine. lw takes options only. -c, --config FILE Configuration file (default: ~/.config/logwisp/logwisp.toml if it exists, else ./logwisp.toml) @@ -94,9 +94,8 @@ Signals: Exit codes: 0 success, including the end of input - 1 general error, including an error in lw's own options (lw COMMAND - exits 2 on a usage error) - 2 configuration file not found + 1 general error, including a configuration that does not load + 2 usage error, or a named configuration file not found ` func printHelp() { diff --git a/cmd/lw/main.go b/cmd/lw/main.go index 1284958..4c1ae65 100644 --- a/cmd/lw/main.go +++ b/cmd/lw/main.go @@ -24,7 +24,7 @@ func main() { switch { case err != nil: fmt.Fprintf(os.Stderr, "Error: %v\n", err) - os.Exit(1) + os.Exit(2) // every command-line error is a usage error case inv.command != nil: os.Exit(inv.command.run(inv.args, os.Stdout, os.Stderr)) case inv.help: diff --git a/cmd/lw/tls.go b/cmd/lw/tls.go index 7d7b359..664cccd 100644 --- a/cmd/lw/tls.go +++ b/cmd/lw/tls.go @@ -23,7 +23,7 @@ var tlsCommands = []subcommand{ {"ca", "--dir DIR [--name NAME] [--days N]", "Create a certificate authority, DIR/ca.crt and DIR/ca.key, that signs certificates only", []string{"dir"}, defineCA}, - {"cert", "--ca-dir DIR --name NAME [--host NAME,...] [--server] [--client] [--days N] [--out DIR]", + {"cert", "--ca-dir DIR --name NAME [--hosts NAME,...] [--server] [--client] [--days N] [--out DIR]", "Issue a certificate and key, OUT/NAME.crt and OUT/NAME.key, from DIR/ca.crt and DIR/ca.key", []string{"ca-dir", "name"}, defineCert}, } @@ -53,7 +53,8 @@ func defineCA(fs *flag.FlagSet) func(stdout, stderr io.Writer) error { func defineCert(fs *flag.FlagSet) func(stdout, stderr io.Writer) error { caDir := fs.String("ca-dir", "", "`dir`ectory holding ca.crt and ca.key (lw tls ca)") name := fs.String("name", "", "subject common `name`, the mtls identity, and the file names") - hosts := fs.String("host", "", "DNS `names` and IP addresses, ',' between them (default with --server: --name)") + hosts := fs.String("hosts", "", "DNS `names` and IP addresses, ',' between them (default with --server: --name)") + fs.Var(fs.Lookup("hosts").Value, "host", "") // its earlier name, unlisted like a short server := fs.Bool("server", false, "for listeners (TLS server authentication)") client := fs.Bool("client", false, "for dialers presenting a certificate (TLS client authentication)") days := fs.Int("days", int(tlsx.LeafValidity/(24*time.Hour)), "validity in `n` days, at most the CA's") diff --git a/doc/cli.md b/doc/cli.md index 8a6951e..9af1e4d 100644 --- a/doc/cli.md +++ b/doc/cli.md @@ -33,8 +33,12 @@ manages SCRAM credentials and logs in to `scram` listeners - A value follows as the next argument or after `=` (`-c FILE`, `-c=FILE`, `--config FILE`, `--config=FILE`); a value that starts with `-` follows `=`, and `--` ends the options. +- A switch (`--check`, `--quiet`, any boolean key) takes no separate value: + `--quiet=false`, not `--quiet false`. - Short options do not combine (`-q -t`, not `-qt`) or take an attached value - (`-cFILE`); an unknown single-dash option is an error. + (`-cFILE`). +- lw takes options only. An unknown option and a word no option takes (`lw + tail`, `lw -t FILE`) are usage errors, named on stderr. - lw's multiword options join words with `-` (`--rate-limit`, `--password-file`); configuration keys, spec keys and preset keys keep TOML's `_` and `.` (`--status_reporter`, `--sink http,tls.cert_file=F`, @@ -48,7 +52,7 @@ pipelines have [their own flags](#pipelines): ``` --= e.g. --logging.level=debug -- e.g. --logging.level debug --- bare flag: true, for a boolean path only +-- a switch: true, for a boolean path only ``` ### Common @@ -309,14 +313,16 @@ service untouched; see [Configuration](configuration.md#hot-reload). - `0`: the end of input, a clean shutdown (`SIGINT`, `SIGTERM`), `--version` / `--help`, `--dump`, or a valid `--check` -- `1`: general error: a command-line error (an unknown option, a missing - `-c` path), a configuration load or validation failure (`--check` - included), a logger init failure, a service bootstrap failure -- `2`: an explicitly requested configuration file is not found +- `1`: general error: a configuration load or validation failure (`--check` + included, and a bad value on the command line), a logger init failure, a + service bootstrap failure +- `2`: a usage error (an unknown option, a word no option takes, a missing + value or `-c` path), or an explicitly requested configuration file that is + not found - killed by `SIGPIPE` (shell status `141`): the reader of stdout went away, as in `lw | head`; `cat` ends the same way -Exit code 2 applies only when the file was named explicitly (`-c`, +A missing file is exit code 2 only when it was named explicitly (`-c`, `--config=`, or the `LOGWISP_CONFIG_*` variables). A missing discovered default is not an error, and LogWisp starts on built-in defaults. @@ -511,7 +517,7 @@ Makes ECDSA P-256 certificates for `tls` blocks. It never replaces a file ``` lw tls ca --dir DIR [--name NAME] [--days 3650] -lw tls cert --ca-dir DIR --name NAME [--host NAME,...] [--server] [--client] +lw tls cert --ca-dir DIR --name NAME [--hosts NAME,...] [--server] [--client] [--days 397] [--out DIR] ``` @@ -520,9 +526,9 @@ lw tls cert --ca-dir DIR --name NAME [--host NAME,...] [--server] [--client] `tls.client_ca_file`; with `ca.key` it is a listener's `tls.issuer_cert_file` and `tls.issuer_key_file`. - `cert`: `NAME.crt` and `NAME.key`, in `--out` (default: `--ca-dir`). - - `--server` for a listener: `--host` lists the DNS names and IP addresses it - carries, default `NAME`; `--client` for a dialer presenting a certificate; - both may be given. + - `--server` for a listener: `--hosts` lists the DNS names and IP addresses + it carries, default `NAME` (`--host`, its earlier name, still works); + `--client` for a dialer presenting a certificate; both may be given. - `NAME` is the subject CN, the identity `auth.type = "mtls"` matches. - Valid 397 days (the longest browsers accept), never past the CA. It prints the certificate's `pin_sha256`. diff --git a/doc/configuration.md b/doc/configuration.md index a4ad270..1bdfb2c 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -234,17 +234,12 @@ lw --logging.level debug # space form also works lw --quiet # bare flag means true ``` -A bare flag is accepted only for a boolean key: `--logging.file.directory ---quiet` fails with `--logging.file.directory needs a value`. Unrecognized `--` -flags and positional words are reported on stderr before the logger exists and -are then ignored: - -``` -Warning: unrecognized flags ignored: [nosuch.key] -``` - -Single-dash options are lw's own ([CLI](cli.md#options)); an unknown one is -an error. +A bare flag is accepted only for a boolean key, which takes its value only +after `=`: `--logging.file.directory --quiet` fails with +`--logging.file.directory needs a value`, and `--quiet false` with `unexpected +argument "false"`. An unknown key is an error too (`unknown option +--nosuch.key`); each is a usage error, exit code 2. Single-dash options are +lw's own ([CLI](cli.md#options)). An empty `LOGWISP_` variable counts as unset, as the pipeline variables do. diff --git a/doc/lw.1 b/doc/lw.1 index a910583..81e85be 100644 --- a/doc/lw.1 +++ b/doc/lw.1 @@ -112,10 +112,13 @@ that stands for it in every command that has it. A value follows as the next argument or after .Ql = ; a value that starts with -.Ql - -follows +.Ql - , +and a switch's, follows .Ql = . -Short options do not combine, and an unknown one is an error. +Short options do not combine. +.Nm +takes options only: an unknown option, or a word no option takes, is a usage +error. The options of .Nm join words with @@ -375,13 +378,14 @@ Create a certificate authority, and .Pa dir/ca.key , valid 3650 days. -.It Cm cert Fl \-ca\-dir Ar dir Fl \-name Ar name Oo Fl \-host Ar names Oc Oo Fl \-server Oc Oo Fl \-client Oc Oo Fl \-days Ar n Oc Op Fl \-out Ar dir +.It Cm cert Fl \-ca\-dir Ar dir Fl \-name Ar name Oo Fl \-hosts Ar names Oc Oo Fl \-server Oc Oo Fl \-client Oc Oo Fl \-days Ar n Oc Op Fl \-out Ar dir Issue .Pa name.crt and .Pa name.key from that authority, valid 397 days and never past it. -.Fl \-host +.Fl \-hosts +.Pq or Fl \-host lists the DNS names and addresses a server certificate carries .Pq default: Ar name ; .Ar name @@ -547,8 +551,8 @@ The service's working directory on Linux and on .Nm exits 0 at the end of input, after a clean shutdown, .Fl \-version -or help; 1 on a command-line, configuration, logger or startup error; and 2 -when a configuration file named by +or help; 1 on a configuration, logger or startup error; and 2 on a usage +error or when a configuration file named by .Fl c or .Ev LOGWISP_CONFIG_* @@ -568,7 +572,7 @@ the shell reports status 141. .Cm tls and .Cm preset -exit 0 on success or help, 1 on failure, and 2 on a usage error. +do the same: 0 on success or help, 1 on failure, and 2 on a usage error. .Sh SIGNALS .Bl -tag -width "SIGINT, SIGTERM" .It Dv SIGINT , SIGTERM diff --git a/doc/todo.md b/doc/todo.md index 1e37d9b..83c3d1d 100644 --- a/doc/todo.md +++ b/doc/todo.md @@ -65,19 +65,9 @@ identity, and both skeletons download the tagged source. Smaller items found along the way, each independent of the others. -1. Command line: - - a positional word at the top level (`lw tail`, `lw -t FILE`) still reaches - config with an unclear message; name it ("unexpected argument X; the file - is -c FILE"); - - Go's flag errors keep single dashes ("flag needs an argument: -u"); - - an unknown `--key` only warns and then runs the stdin pipe; consider an - error when stdin is a terminal; - - `lw tls cert --host` could become `--hosts` (keeping `--host`), to match - `tls.hosts` and the preset key; - - top-level usage errors exit 1, subcommand ones 2. -2. TLS: the generated-certificate key is one per process, so a pin taken from a +1. TLS: the generated-certificate key is one per process, so a pin taken from a `self_signed` listener also matches an `issuer` listener of the same process; document it or key per listener. Pins are checked in `VerifyPeerCertificate`, which Go skips on resumption: guard it if a dialer ever keeps a `ClientSessionCache`. -3. `core.ShutdownTimeout` is unused. +2. `core.ShutdownTimeout` is unused. diff --git a/internal/config/loader.go b/internal/config/loader.go index e1cd145..a578458 100644 --- a/internal/config/loader.go +++ b/internal/config/loader.go @@ -29,7 +29,21 @@ type Manager struct { type Args struct { File string // -c: the configuration file, "" for the environment or discovery Specs []Spec // pipeline flags, in order - Overrides []string // --path=value settings, for lixenwraith/config + Overrides []string // --path=value settings and bare switches, for lixenwraith/config +} + +// Settings maps each --path setting Load takes to whether it is a switch, which +// takes no separate value; the parser must not leave config to guess. +func Settings() map[string]bool { + c := lconfig.New() + if err := c.RegisterStructWithTags("", defaults(), lconfig.FormatTOML); err != nil { + panic(err) // Config is static: every run fails or none does + } + settings := map[string]bool{} + for path, value := range c.GetRegisteredPathsWithDefaults() { + _, settings[path] = value.(bool) + } + return settings } // Load reads the startup sources and validates the initial configuration. @@ -80,9 +94,6 @@ func Load(args Args) (*Manager, error) { // A missing discovered default still permits valid CLI/env/default values. } m.read = err == nil - if unknown := cfg.UnknownCLIKeys(); len(unknown) > 0 && !initial.Quiet { - fmt.Fprintf(os.Stderr, "Warning: unrecognized flags ignored: %v\n", unknown) - } m.config = cfg return m, nil } diff --git a/test/preset-test.sh b/test/preset-test.sh index a1938ce..7690eb2 100755 --- a/test/preset-test.sh +++ b/test/preset-test.sh @@ -46,10 +46,13 @@ check "lw --check builds the dumped preset" "$(lw --check -c "$RUN/dump.toml" 2> lw preset edge -to 127.0.0.1:1 >/dev/null 2>"$LOG/edge-noauth.err" rc=$? check "an edge without credentials is a usage error ($rc)" "$( ((rc == 2)) && grep -q 'never sends unauthenticated' "$LOG/edge-noauth.err" && echo 1 || echo 0)" +lw tail "$LOG/stray.err" +rc=$? +check "a word no option takes is a usage error ($rc)" "$( ((rc == 2)) && grep -q 'unexpected argument "tail"' "$LOG/stray.err" && echo 1 || echo 0)" section "Certificates" lw tls ca -dir "$RUN/pki" 2>"$LOG/tls.out" && - lw tls cert -ca-dir "$RUN/pki" -name logs.example -server -host 127.0.0.1,logs.example 2>>"$LOG/tls.out" + lw tls cert -ca-dir "$RUN/pki" -name logs.example -server -hosts 127.0.0.1,logs.example 2>>"$LOG/tls.out" check "lw tls ca and cert issue a chain openssl verifies" \ "$(openssl verify -CAfile "$RUN/pki/ca.crt" "$RUN/pki/logs.example.crt" 2>/dev/null | grep -q ': OK$' && echo 1 || echo 0)" check "keys are private" "$(is "$(stat -c %a "$RUN/pki/ca.key" "$RUN/pki/logs.example.key" | tr '\n' ' ')" "600 600 ")"