mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 11:01:25 +01:00
fix: report unknown --json fields with the fields a command outputs
tea issue list --json number,titel failed with "accepts 0 arg(s), received 1": a field list with an unknown name stayed a positional argument. A field-shaped word after --json now binds to it unless the command takes the word as an argument, and an unknown field lists the available fields. --json= is a usage error instead of printing text. --jq alone now implies --json, except on commands annotated tea.rawjson that filter raw JSON themselves; -q is short for --jq and -y for --yes. Fields are listed alphabetically, and terminal.JSONOutput can indent and color JSON for a terminal. Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
4 files changed
+216
-42
No files matched your search
+100
-13
@@ -4,17 +4,36 @@
|
||||
package terminal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/itchyny/gojq"
|
||||
)
|
||||
|
||||
// JSONOutput selects, filters, and formats command results as JSON.
|
||||
type JSONOutput struct {
|
||||
// Fields holds the comma-separated fields to keep; "" and "*" keep every field.
|
||||
Fields string
|
||||
// Query is a jq expression applied after field selection.
|
||||
Query string
|
||||
// Pretty indents the JSON for a terminal and strips control characters from raw jq strings.
|
||||
Pretty bool
|
||||
// Color highlights pretty JSON keys and values.
|
||||
Color bool
|
||||
}
|
||||
|
||||
// WriteJSON projects explicit result fields and applies an in-process jq filter.
|
||||
func WriteJSON(w io.Writer, value any, fields, query string) error {
|
||||
return JSONOutput{Fields: fields, Query: query}.Write(w, value)
|
||||
}
|
||||
|
||||
// Write projects explicit result fields of value, applies the jq filter, and writes the results.
|
||||
func (o JSONOutput) Write(w io.Writer, value any) error {
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -23,7 +42,7 @@ func WriteJSON(w io.Writer, value any, fields, query string) error {
|
||||
if err := json.Unmarshal(data, &normalized); err != nil {
|
||||
return err
|
||||
}
|
||||
if names := fieldNames(fields); names != nil {
|
||||
if names := fieldNames(o.Fields); names != nil {
|
||||
selectFields := func(item any) (any, error) {
|
||||
object, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
@@ -54,12 +73,10 @@ func WriteJSON(w io.Writer, value any, fields, query string) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
encoder := json.NewEncoder(w)
|
||||
encoder.SetEscapeHTML(false)
|
||||
if query == "" {
|
||||
return encoder.Encode(normalized)
|
||||
if o.Query == "" {
|
||||
return o.encode(w, normalized)
|
||||
}
|
||||
parsed, err := parseQuery(query)
|
||||
parsed, err := parseQuery(o.Query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -73,25 +90,95 @@ func WriteJSON(w io.Writer, value any, fields, query string) error {
|
||||
return fmt.Errorf("evaluate --jq: %w", err)
|
||||
}
|
||||
if str, ok := value.(string); ok {
|
||||
if o.Pretty {
|
||||
str = SafeText(str)
|
||||
}
|
||||
if _, err := fmt.Fprintln(w, str); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := encoder.Encode(value); err != nil {
|
||||
} else if err := o.encode(w, value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// encode writes one JSON value followed by a newline.
|
||||
func (o JSONOutput) encode(w io.Writer, value any) error {
|
||||
var buf bytes.Buffer
|
||||
encoder := json.NewEncoder(&buf)
|
||||
encoder.SetEscapeHTML(false)
|
||||
if o.Pretty {
|
||||
encoder.SetIndent("", " ")
|
||||
}
|
||||
if err := encoder.Encode(value); err != nil {
|
||||
return err
|
||||
}
|
||||
text := buf.String()
|
||||
if o.Pretty {
|
||||
// The encoder escapes control characters but not format characters such as bidirectional overrides.
|
||||
text = SafeText(text)
|
||||
}
|
||||
if o.Color {
|
||||
text = colorJSON(text)
|
||||
}
|
||||
_, err := io.WriteString(w, text)
|
||||
return err
|
||||
}
|
||||
|
||||
// JSON colors, following gh: bold blue keys, green strings, yellow booleans, cyan null.
|
||||
const (
|
||||
jsonKeyColor = "\x1b[1;34m"
|
||||
jsonStringColor = "\x1b[32m"
|
||||
jsonBoolColor = "\x1b[33m"
|
||||
jsonNullColor = "\x1b[36m"
|
||||
jsonResetColor = "\x1b[0m"
|
||||
)
|
||||
|
||||
// colorJSON highlights the keys and literal values of encoded JSON text.
|
||||
func colorJSON(text string) string {
|
||||
var b strings.Builder
|
||||
for i := 0; i < len(text); {
|
||||
color, end := "", i+1
|
||||
switch c := text[i]; {
|
||||
case c == '"':
|
||||
for end < len(text) && text[end] != '"' {
|
||||
if text[end] == '\\' {
|
||||
end++
|
||||
}
|
||||
end++
|
||||
}
|
||||
end++
|
||||
color = jsonStringColor
|
||||
if strings.HasPrefix(strings.TrimLeft(text[end:], " "), ":") {
|
||||
color = jsonKeyColor
|
||||
}
|
||||
case strings.HasPrefix(text[i:], "true"):
|
||||
color, end = jsonBoolColor, i+len("true")
|
||||
case strings.HasPrefix(text[i:], "false"):
|
||||
color, end = jsonBoolColor, i+len("false")
|
||||
case strings.HasPrefix(text[i:], "null"):
|
||||
color, end = jsonNullColor, i+len("null")
|
||||
}
|
||||
if color == "" {
|
||||
b.WriteString(text[i:end])
|
||||
} else {
|
||||
b.WriteString(color + text[i:end] + jsonResetColor)
|
||||
}
|
||||
i = end
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// ValidateOutput catches invalid projection/filter syntax before a service mutates data.
|
||||
// An unknown field error lists the allowed fields.
|
||||
func ValidateOutput(fields, query string, allowed []string) error {
|
||||
if names := fieldNames(fields); names != nil && allowed != nil {
|
||||
known := map[string]bool{}
|
||||
for _, name := range allowed {
|
||||
known[name] = true
|
||||
}
|
||||
for _, name := range names {
|
||||
if !known[name] {
|
||||
return fmt.Errorf("unknown JSON field %q", name)
|
||||
if name == "" {
|
||||
return errors.New("--json field names cannot be empty; use --json alone for every field")
|
||||
}
|
||||
if !slices.Contains(allowed, name) {
|
||||
return fmt.Errorf("unknown JSON field %q\navailable fields: %s", name, strings.Join(allowed, ", "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user