fix(cmd): long-form flags, honest counts and safer inference

Assisted-by: GLM 5.3
This commit is contained in:
2026-09-22 21:15:07 +02:00
parent b7f39435e1
commit 4900367970
8 changed files with 791 additions and 224 deletions
+74 -32
View File
@@ -4,16 +4,16 @@
// Command interpres-decode is the toml-test harness adapter and a TOML
// validator. Without flags it reads a TOML document from standard input and
// writes the toml-test "tagged JSON" representation to standard output. With
// -encode it is the reverse: it reads tagged JSON and writes the TOML document
// it describes. With -validate it checks the named documents, or standard
// input when none are named, and exits non-zero on the first invalid one:
// --encode it is the reverse: it reads tagged JSON and writes the TOML document
// it describes. With --validate it checks the named documents, or standard
// input when none are named, and exits non-zero when one is invalid:
//
// interpres-decode -validate config.toml
// interpres-decode -encode < case.json
// interpres-decode --validate config.toml
// interpres-decode --encode < case.json
//
// Run the official suite in both directions against the adapter with:
//
// toml-test test -decoder=./interpres-decode -encoder='./interpres-decode -encode'
// toml-test test -decoder=./interpres-decode -encoder='./interpres-decode --encode'
package main
import (
@@ -39,11 +39,16 @@ func main() {
}
// Run runs the command line and returns the process exit code: 0 success,
// 1 an invalid document, 2 a usage, reading, encoding, or
// 1 an invalid document, 2 a usage, reading, writing, encoding, or
// unsupported-value error.
func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
fs := flag.NewFlagSet("interpres-decode", flag.ContinueOnError)
fs.SetOutput(stderr)
// The flag package's own diagnostics and default usage render flags
// with a single dash, while the command spells every flag in its
// two-dash long form, the form the manpage documents. Its output is
// therefore discarded and the usage below is the only one printed.
fs.SetOutput(io.Discard)
fs.Usage = func() {}
version := fs.Bool("version", false, "print the version and exit")
validate := fs.Bool("validate", false, "validate the documents instead of emitting tagged JSON")
encode := fs.Bool("encode", false, "read tagged JSON from stdin and write TOML instead")
@@ -52,12 +57,18 @@ func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
schemaType := fs.String("schema", "", "write a TOML template for the named struct type; the source file follows as the first argument")
if err := fs.Parse(args); err != nil {
if errors.Is(err, flag.ErrHelp) {
usage(stdout)
return 0
}
fmt.Fprintf(stderr, "interpres-decode: %v\n", err)
usage(stderr)
return 2
}
if *version {
fmt.Fprintf(stdout, "interpres-decode %s\n", versionString())
if _, err := fmt.Fprintf(stdout, "interpres-decode %s\n", versionString()); err != nil {
fmt.Fprintln(stderr, "interpres-decode: write stdout:", err)
return 2
}
return 0
}
modes := 0
@@ -70,13 +81,19 @@ func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
modes++
}
if modes > 1 {
fmt.Fprintln(stderr, "interpres-decode: -validate, -encode, -struct and -schema cannot be combined")
fmt.Fprintln(stderr, "interpres-decode: --validate, --encode, --struct and --schema cannot be combined")
return 2
}
// --json shapes the decoding output only, so it is rejected with every
// mode uniformly instead of being silently ignored by some of them.
if *plainJSON && modes > 0 {
fmt.Fprintln(stderr, "interpres-decode: --json shapes the decoder output and cannot be combined with --encode, --struct, --validate or --schema")
return 2
}
if *schemaType != "" {
rest := fs.Args()
if len(rest) != 1 {
fmt.Fprintln(stderr, "interpres-decode: -schema needs the type name and exactly one Go source file")
fmt.Fprintln(stderr, "interpres-decode: --schema needs the type name and exactly one Go source file")
return 2
}
if err := runSchema(*schemaType, rest[0], stdout); err != nil {
@@ -88,12 +105,8 @@ func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
if *validate {
return validatePaths(fs.Args(), stdin, stderr)
}
if *encode && *plainJSON {
fmt.Fprintln(stderr, "interpres-decode: -json shapes the decoder output and cannot be combined with -encode")
return 2
}
if fs.NArg() > 0 {
fmt.Fprintln(stderr, "interpres-decode: the adapter mode takes no arguments; name files with -validate")
fmt.Fprintln(stderr, "interpres-decode: the adapter mode takes no arguments; name files with --validate")
return 2
}
if *encode {
@@ -101,19 +114,25 @@ func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
}
data, err := io.ReadAll(stdin)
if err != nil {
fmt.Fprintln(stderr, "read stdin:", err)
fmt.Fprintln(stderr, "interpres-decode: read stdin:", err)
return 2
}
if *infer {
if err := inferStruct(data, stdout); err != nil {
fmt.Fprintf(stderr, "interpres-decode: %v\n", err)
return 1
// A document that fails to parse keeps the adapter's invalid
// exit; anything else, a failed write among them, is a tool
// failure.
if _, ok := errors.AsType[*interpres.SyntaxError](err); ok {
return 1
}
return 2
}
return 0
}
tree, err := interpres.ParseMap(data)
if err != nil {
fmt.Fprintln(stderr, err)
fmt.Fprintf(stderr, "interpres-decode: %v\n", err)
return 1
}
if *plainJSON {
@@ -121,25 +140,48 @@ func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
if err := enc.Encode(plainJSONValue(tree)); err != nil {
fmt.Fprintln(stderr, "encode:", err)
fmt.Fprintln(stderr, "interpres-decode: encode:", err)
return 2
}
return 0
}
tagged, err := tag(tree)
if err != nil {
fmt.Fprintln(stderr, err)
fmt.Fprintf(stderr, "interpres-decode: %v\n", err)
return 2
}
enc := json.NewEncoder(stdout)
enc.SetEscapeHTML(false)
if err := enc.Encode(tagged); err != nil {
fmt.Fprintln(stderr, "encode:", err)
fmt.Fprintln(stderr, "interpres-decode: encode:", err)
return 2
}
return 0
}
// usage prints the command line summary, with every flag in its two-dash
// long form: the flag package's default usage printer renders a single dash,
// and the manpage and docs/CLI.md spell the flags the way this text does.
func usage(w io.Writer) {
fmt.Fprint(w, `Usage: interpres-decode [flags]
Without a mode flag the command reads one TOML document from standard input
and writes the toml-test tagged JSON representation to standard output.
--encode read tagged JSON from standard input and write TOML
instead
--help print this usage
--json with the default mode, print plain indented JSON
instead of tagged JSON
--schema TYPE write a TOML template for the named struct type; the
Go source file follows as the first argument
--struct infer a Go struct definition from the document on
standard input and print it
--validate validate the documents instead of emitting tagged JSON
--version print the version and exit
`)
}
// versionString names the version the binary was built at: the module
// version the toolchain recorded, which is the tag when the release pipeline
// builds it, and (devel) for an ordinary build from a working tree.
@@ -226,8 +268,8 @@ func validatePaths(paths []string, stdin io.Reader, stderr io.Writer) int {
return 2
}
}
valid := true
checked := 0
invalid := 0
for _, p := range files {
name := p
var data []byte
@@ -244,17 +286,17 @@ func validatePaths(paths []string, stdin io.Reader, stderr io.Writer) int {
}
checked++
if _, err := interpres.ParseMap(data); err != nil {
fmt.Fprintf(stderr, "%s: %v\n", name, err)
valid = false
fmt.Fprintf(stderr, "interpres-decode: %s: %v\n", name, err)
invalid++
}
}
// The single-document run stays quiet on success, the contract the
// compliance tooling relies on; a directory walk closes with the
// summary that makes the sweep readable.
if dirs > 0 {
fmt.Fprintf(stderr, "checked %d documents, %d invalid\n", checked, map[bool]int{true: 0, false: 1}[valid])
fmt.Fprintf(stderr, "checked %d documents, %d invalid\n", checked, invalid)
}
if !valid {
if invalid > 0 {
return 1
}
return 0
@@ -265,17 +307,17 @@ func validatePaths(paths []string, stdin io.Reader, stderr io.Writer) int {
func encodeJSON(stdin io.Reader, stdout, stderr io.Writer) int {
data, err := io.ReadAll(stdin)
if err != nil {
fmt.Fprintln(stderr, "read stdin:", err)
fmt.Fprintln(stderr, "interpres-decode: read stdin:", err)
return 2
}
var desc any
if err := json.Unmarshal(data, &desc); err != nil {
fmt.Fprintln(stderr, "decode JSON:", err)
fmt.Fprintln(stderr, "interpres-decode: decode JSON:", err)
return 2
}
tree, err := untag(desc)
if err != nil {
fmt.Fprintln(stderr, err)
fmt.Fprintf(stderr, "interpres-decode: %v\n", err)
return 2
}
doc, ok := tree.(map[string]any)
@@ -285,11 +327,11 @@ func encodeJSON(stdin io.Reader, stdout, stderr io.Writer) int {
}
out, err := interpres.Marshal(doc)
if err != nil {
fmt.Fprintln(stderr, err)
fmt.Fprintf(stderr, "interpres-decode: %v\n", err)
return 2
}
if _, err := stdout.Write(out); err != nil {
fmt.Fprintln(stderr, "write stdout:", err)
fmt.Fprintln(stderr, "interpres-decode: write stdout:", err)
return 2
}
return 0