v0.26.4 cli enhancements

This commit is contained in:
lixen committed 2026-10-05 15:12:19 -04:00
1 parent c00c076d9b
commit b2f11f6104
11 files changed
+173 -81

No files matched your search

+41 -16
View File
@@ -6,6 +6,7 @@ import (
"flag" "flag"
"fmt" "fmt"
"io" "io"
"regexp"
"slices" "slices"
"strings" "strings"
"unicode" "unicode"
@@ -72,8 +73,10 @@ func (c *command) run(args []string, stdout, stderr io.Writer) int {
} }
s := c.subcommands[i] s := c.subcommands[i]
fs := flag.NewFlagSet("lw "+c.name+" "+s.name, flag.ContinueOnError) fs := flag.NewFlagSet("lw "+c.name+" "+s.name, flag.ContinueOnError)
fs.SetOutput(stderr) // flag names a long flag with one dash: its errors, and the usage, print below
fs.Usage = func() { 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) fmt.Fprintf(stderr, "Usage: %s %s\n\n%s.\n\n", fs.Name(), s.synopsis, s.summary)
printFlags(stderr, fs) 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 fs.Var(f.Value, letter, "") // no usage: printFlags shows it with its long flag
} }
} }
if err := fs.Parse(args[1:]); err != nil { err := fs.Parse(args[1:])
if errors.Is(err, flag.ErrHelp) { switch {
case errors.Is(err, flag.ErrHelp):
usage()
return 0 return 0
case err != nil:
err = usageError(longFlag.ReplaceAllString(err.Error(), "$1--$2"))
default:
err = checkArgs(fs, s.required)
} }
return 2 // already reported by flag, with the usage
}
err := checkArgs(fs, s.required)
if err == nil { if err == nil {
err = work(stdout, stderr) 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) fmt.Fprintf(stderr, "%s: %v\n", fs.Name(), err)
if _, ok := errors.AsType[usageError](err); ok { if _, ok := errors.AsType[usageError](err); ok {
fs.Usage() usage()
return 2 return 2
} }
return 1 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) { func (c *command) usage(w io.Writer) {
fmt.Fprintf(w, "Usage: lw %s COMMAND [flags]\n\n", c.name) fmt.Fprintf(w, "Usage: lw %s COMMAND [flags]\n\n", c.name)
for _, s := range c.subcommands { 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 // 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 // other --flag is a setting, passed to lixenwraith/config as --path=value or a
// what it does not know. A pipeline flag takes the next argument unless it // bare switch so it never guesses. A flag's value is the next argument unless
// starts with '-'. A help request wins over a malformed flag. // that starts with '-'. Every error is a usage error; a help request wins.
func parseCommandLine(argv []string) (inv invocation, err error) { func parseCommandLine(argv []string) (inv invocation, err error) {
defer func() { defer func() {
if inv.help { 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 { 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 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 { next := func(i *int) string {
if *i+1 < len(argv) && !strings.HasPrefix(argv[*i+1], "-") { if *i+1 < len(argv) && !strings.HasPrefix(argv[*i+1], "-") {
*i++ *i++
@@ -194,9 +205,11 @@ func parseCommandLine(argv []string) (inv invocation, err error) {
} }
switch { switch {
case arg == "--": 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 return inv, err
case arg == "--help": case arg == "--help" || i == 0 && arg == "help":
inv.help = true inv.help = true
case name == "--config": case name == "--config":
if !inline { 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}) 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])): 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)) 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: 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) inv.load.Overrides = append(inv.load.Overrides, arg)
} }
} }
+68 -10
View File
@@ -7,6 +7,7 @@ import (
"path/filepath" "path/filepath"
"reflect" "reflect"
"regexp" "regexp"
"strconv"
"strings" "strings"
"testing" "testing"
@@ -16,9 +17,9 @@ import (
) )
// lw's own flags: a short is its long flag, an unknown single-dash one fails, // 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 // the last -c wins, a flag takes the next argument unless it starts with '-',
// it starts with '-', -- ends lw's flags, and a command is only the first // a switch takes none, and a command is only the first argument. A setting
// argument. Everything else is a setting for config. // reaches config as --path=value or a bare switch.
func TestCommandLineGrammar(t *testing.T) { func TestCommandLineGrammar(t *testing.T) {
for _, c := range []struct { for _, c := range []struct {
argv []string 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"}}}}}, 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{"--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{"--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{"--quiet", "-h"}, invocation{help: true, load: config.Args{Overrides: []string{"--quiet"}}}},
{[]string{"-c", "-h"}, invocation{help: true}}, {[]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{"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.level", "debug", "--logging.file.retention_hours=-1", "--dump=false"},
{[]string{"-q", "-t", "-V", "-p", "tail,path=x", "-c=a.toml", "--logging.file.retention_hours", "-1"},
invocation{load: config.Args{File: "a.toml", Specs: []config.Spec{{Flag: "preset", Value: "tail,path=x"}}, 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) got, err := parseCommandLine(c.argv)
if err != nil || !reflect.DeepEqual(got, c.want) { 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"}, 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 { if _, err := parseCommandLine(argv); err == nil {
t.Errorf("%q accepted", argv) 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 // Every subcommand's -h lists each flag once, GNU style, with its short form
// beside it: " -u, --user NAME" or " --credentials FILE" // beside it: " -u, --user NAME" or " --credentials FILE"
func TestUsageListsEachFlagOnce(t *testing.T) { func TestUsageListsEachFlagOnce(t *testing.T) {
@@ -63,6 +106,9 @@ func TestUsageListsEachFlagOnce(t *testing.T) {
s.define(fs) s.define(fs)
want := 0 want := 0
fs.VisitAll(func(f *flag.Flag) { fs.VisitAll(func(f *flag.Flag) {
if f.Usage == "" {
return // an earlier name, unlisted
}
want++ want++
head := " --" + f.Name head := " --" + f.Name
for letter, short := range shorts { 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, // lw tls ca and lw tls cert write a chain a dialer verifies by the CA file,
// keys private, and never replace a file // keys private, and never replace a file
func TestTLSCommandsIssueAVerifiableChain(t *testing.T) { func TestTLSCommandsIssueAVerifiableChain(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
steps := [][]string{ steps := [][]string{
{"ca", "-dir", dir, "-name", "test CA"}, {"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 { for _, args := range steps {
if code, _, stderr := runCommand(t, "tls", args...); code != 0 { if code, _, stderr := runCommand(t, "tls", args...); code != 0 {
+4 -5
View File
@@ -23,8 +23,8 @@ Usage:
Commands (lw COMMAND -h lists its subcommands): Commands (lw COMMAND -h lists its subcommands):
%s %s
Options: a short option is its long one; a value follows as the next argument 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 '='; or after '=' (-c FILE, --config=FILE), one that starts with '-' or a switch's
short options do not combine. after '='; short options do not combine. lw takes options only.
-c, --config FILE Configuration file (default: -c, --config FILE Configuration file (default:
~/.config/logwisp/logwisp.toml if it exists, ~/.config/logwisp/logwisp.toml if it exists,
else ./logwisp.toml) else ./logwisp.toml)
@@ -94,9 +94,8 @@ Signals:
Exit codes: Exit codes:
0 success, including the end of input 0 success, including the end of input
1 general error, including an error in lw's own options (lw COMMAND 1 general error, including a configuration that does not load
exits 2 on a usage error) 2 usage error, or a named configuration file not found
2 configuration file not found
` `
func printHelp() { func printHelp() {
+1 -1
View File
@@ -24,7 +24,7 @@ func main() {
switch { switch {
case err != nil: case err != nil:
fmt.Fprintf(os.Stderr, "Error: %v\n", err) 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: case inv.command != nil:
os.Exit(inv.command.run(inv.args, os.Stdout, os.Stderr)) os.Exit(inv.command.run(inv.args, os.Stdout, os.Stderr))
case inv.help: case inv.help:
+3 -2
View File
@@ -23,7 +23,7 @@ var tlsCommands = []subcommand{
{"ca", "--dir DIR [--name NAME] [--days N]", {"ca", "--dir DIR [--name NAME] [--days N]",
"Create a certificate authority, DIR/ca.crt and DIR/ca.key, that signs certificates only", "Create a certificate authority, DIR/ca.crt and DIR/ca.key, that signs certificates only",
[]string{"dir"}, defineCA}, []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", "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}, []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 { 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)") 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") 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)") server := fs.Bool("server", false, "for listeners (TLS server authentication)")
client := fs.Bool("client", false, "for dialers presenting a certificate (TLS client 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") days := fs.Int("days", int(tlsx.LeafValidity/(24*time.Hour)), "validity in `n` days, at most the CA's")
+17 -11
View File
@@ -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`, - A value follows as the next argument or after `=` (`-c FILE`, `-c=FILE`,
`--config FILE`, `--config=FILE`); a value that starts with `-` follows `=`, `--config FILE`, `--config=FILE`); a value that starts with `-` follows `=`,
and `--` ends the options. 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 - 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`, - lw's multiword options join words with `-` (`--rate-limit`,
`--password-file`); configuration keys, spec keys and preset keys keep `--password-file`); configuration keys, spec keys and preset keys keep
TOML's `_` and `.` (`--status_reporter`, `--sink http,tls.cert_file=F`, TOML's `_` and `.` (`--status_reporter`, `--sink http,tls.cert_file=F`,
@@ -48,7 +52,7 @@ pipelines have [their own flags](#pipelines):
``` ```
--<path>=<value> e.g. --logging.level=debug --<path>=<value> e.g. --logging.level=debug
--<path> <value> e.g. --logging.level debug --<path> <value> e.g. --logging.level debug
--<path> bare flag: true, for a boolean path only --<path> a switch: true, for a boolean path only
``` ```
### Common ### Common
@@ -309,14 +313,16 @@ service untouched; see [Configuration](configuration.md#hot-reload).
- `0`: the end of input, a clean shutdown (`SIGINT`, `SIGTERM`), `--version` / - `0`: the end of input, a clean shutdown (`SIGINT`, `SIGTERM`), `--version` /
`--help`, `--dump`, or a valid `--check` `--help`, `--dump`, or a valid `--check`
- `1`: general error: a command-line error (an unknown option, a missing - `1`: general error: a configuration load or validation failure (`--check`
`-c` path), a configuration load or validation failure (`--check` included, and a bad value on the command line), a logger init failure, a
included), a logger init failure, a service bootstrap failure service bootstrap failure
- `2`: an explicitly requested configuration file is not found - `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 - killed by `SIGPIPE` (shell status `141`): the reader of stdout went away, as
in `lw | head`; `cat` ends the same way 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 `--config=`, or the `LOGWISP_CONFIG_*` variables). A missing discovered default
is not an error, and LogWisp starts on built-in defaults. 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 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] [--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.client_ca_file`; with `ca.key` it is a listener's
`tls.issuer_cert_file` and `tls.issuer_key_file`. `tls.issuer_cert_file` and `tls.issuer_key_file`.
- `cert`: `NAME.crt` and `NAME.key`, in `--out` (default: `--ca-dir`). - `cert`: `NAME.crt` and `NAME.key`, in `--out` (default: `--ca-dir`).
- `--server` for a listener: `--host` lists the DNS names and IP addresses it - `--server` for a listener: `--hosts` lists the DNS names and IP addresses
carries, default `NAME`; `--client` for a dialer presenting a certificate; it carries, default `NAME` (`--host`, its earlier name, still works);
both may be given. `--client` for a dialer presenting a certificate; both may be given.
- `NAME` is the subject CN, the identity `auth.type = "mtls"` matches. - `NAME` is the subject CN, the identity `auth.type = "mtls"` matches.
- Valid 397 days (the longest browsers accept), never past the CA. It prints - Valid 397 days (the longest browsers accept), never past the CA. It prints
the certificate's `pin_sha256`. the certificate's `pin_sha256`.
+6 -11
View File
@@ -234,17 +234,12 @@ lw --logging.level debug # space form also works
lw --quiet # bare flag means true lw --quiet # bare flag means true
``` ```
A bare flag is accepted only for a boolean key: `--logging.file.directory A bare flag is accepted only for a boolean key, which takes its value only
--quiet` fails with `--logging.file.directory needs a value`. Unrecognized `--` after `=`: `--logging.file.directory --quiet` fails with
flags and positional words are reported on stderr before the logger exists and `--logging.file.directory needs a value`, and `--quiet false` with `unexpected
are then ignored: 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)).
Warning: unrecognized flags ignored: [nosuch.key]
```
Single-dash options are lw's own ([CLI](cli.md#options)); an unknown one is
an error.
An empty `LOGWISP_<PATH>` variable counts as unset, as the pipeline variables An empty `LOGWISP_<PATH>` variable counts as unset, as the pipeline variables
do. do.
+12 -8
View File
@@ -112,10 +112,13 @@ that stands for it in every command that has it.
A value follows as the next argument or after A value follows as the next argument or after
.Ql = ; .Ql = ;
a value that starts with a value that starts with
.Ql - .Ql - ,
follows and a switch's, follows
.Ql = . .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 The options of
.Nm .Nm
join words with join words with
@@ -375,13 +378,14 @@ Create a certificate authority,
and and
.Pa dir/ca.key , .Pa dir/ca.key ,
valid 3650 days. 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 Issue
.Pa name.crt .Pa name.crt
and and
.Pa name.key .Pa name.key
from that authority, valid 397 days and never past it. 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 lists the DNS names and addresses a server certificate carries
.Pq default: Ar name ; .Pq default: Ar name ;
.Ar name .Ar name
@@ -547,8 +551,8 @@ The service's working directory on Linux and on
.Nm .Nm
exits 0 at the end of input, after a clean shutdown, exits 0 at the end of input, after a clean shutdown,
.Fl \-version .Fl \-version
or help; 1 on a command-line, configuration, logger or startup error; and 2 or help; 1 on a configuration, logger or startup error; and 2 on a usage
when a configuration file named by error or when a configuration file named by
.Fl c .Fl c
or or
.Ev LOGWISP_CONFIG_* .Ev LOGWISP_CONFIG_*
@@ -568,7 +572,7 @@ the shell reports status 141.
.Cm tls .Cm tls
and and
.Cm preset .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 .Sh SIGNALS
.Bl -tag -width "SIGINT, SIGTERM" .Bl -tag -width "SIGINT, SIGTERM"
.It Dv SIGINT , SIGTERM .It Dv SIGINT , SIGTERM
+2 -12
View File
@@ -65,19 +65,9 @@ identity, and both skeletons download the tagged source.
Smaller items found along the way, each independent of the others. Smaller items found along the way, each independent of the others.
1. Command line: 1. TLS: the generated-certificate key is one per process, so a pin taken from a
- 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
`self_signed` listener also matches an `issuer` listener of the same `self_signed` listener also matches an `issuer` listener of the same
process; document it or key per listener. Pins are checked in process; document it or key per listener. Pins are checked in
`VerifyPeerCertificate`, which Go skips on resumption: guard it if a dialer `VerifyPeerCertificate`, which Go skips on resumption: guard it if a dialer
ever keeps a `ClientSessionCache`. ever keeps a `ClientSessionCache`.
3. `core.ShutdownTimeout` is unused. 2. `core.ShutdownTimeout` is unused.
+15 -4
View File
@@ -29,7 +29,21 @@ type Manager struct {
type Args struct { type Args struct {
File string // -c: the configuration file, "" for the environment or discovery File string // -c: the configuration file, "" for the environment or discovery
Specs []Spec // pipeline flags, in order 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. // 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. // A missing discovered default still permits valid CLI/env/default values.
} }
m.read = err == nil 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 m.config = cfg
return m, nil return m, nil
} }
+4 -1
View File
@@ -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" lw preset edge -to 127.0.0.1:1 >/dev/null 2>"$LOG/edge-noauth.err"
rc=$? 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)" 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 </dev/null 2>"$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" section "Certificates"
lw tls ca -dir "$RUN/pki" 2>"$LOG/tls.out" && 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" \ 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)" "$(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 ")" check "keys are private" "$(is "$(stat -c %a "$RUN/pki/ca.key" "$RUN/pki/logs.example.key" | tr '\n' ' ')" "600 600 ")"