|
| 1 | +package main |
| 2 | + |
| 3 | +import ( |
| 4 | + "bytes" |
| 5 | + _ "embed" |
| 6 | + "encoding/json" |
| 7 | + "fmt" |
| 8 | + "io" |
| 9 | + "os" |
| 10 | + "strings" |
| 11 | + |
| 12 | + fracturedjson "github.com/FileFormatInfo/go-fractured-json" |
| 13 | + "github.com/spf13/pflag" |
| 14 | +) |
| 15 | + |
| 16 | +var ( |
| 17 | + BUILDER = "unknown" |
| 18 | + COMMIT = "(local)" |
| 19 | + LASTMOD = "(local)" |
| 20 | + VERSION = "internal" |
| 21 | +) |
| 22 | + |
| 23 | +//go:embed README.md |
| 24 | +var helpText string |
| 25 | + |
| 26 | +func normalizeLF(s string) string { |
| 27 | + s = strings.ReplaceAll(s, "\r\n", "\n") |
| 28 | + s = strings.ReplaceAll(s, "\r", "\n") |
| 29 | + return s |
| 30 | +} |
| 31 | + |
| 32 | +func applyEOL(s string, eol string) (string, error) { |
| 33 | + normalized := normalizeLF(s) |
| 34 | + switch eol { |
| 35 | + case "lf": |
| 36 | + return normalized, nil |
| 37 | + case "cr": |
| 38 | + return strings.ReplaceAll(normalized, "\n", "\r"), nil |
| 39 | + case "crlf": |
| 40 | + return strings.ReplaceAll(normalized, "\n", "\r\n"), nil |
| 41 | + default: |
| 42 | + return "", fmt.Errorf("invalid --eol value %q (expected: lf, cr, crlf)", eol) |
| 43 | + } |
| 44 | +} |
| 45 | + |
| 46 | +func decodeJSON(input []byte) (any, error) { |
| 47 | + dec := json.NewDecoder(bytes.NewReader(input)) |
| 48 | + dec.UseNumber() |
| 49 | + |
| 50 | + var v any |
| 51 | + if err := dec.Decode(&v); err != nil { |
| 52 | + return nil, err |
| 53 | + } |
| 54 | + if err := dec.Decode(&struct{}{}); err != io.EOF { |
| 55 | + return nil, fmt.Errorf("extra JSON content after first value") |
| 56 | + } |
| 57 | + return v, nil |
| 58 | +} |
| 59 | + |
| 60 | +func formatJSON(input []byte, canonical bool, line bool, fractured bool) (string, error) { |
| 61 | + if fractured { |
| 62 | + return fracturedjson.Reformat(string(input)) |
| 63 | + } |
| 64 | + |
| 65 | + if line { |
| 66 | + var out bytes.Buffer |
| 67 | + if err := json.Compact(&out, input); err != nil { |
| 68 | + return "", err |
| 69 | + } |
| 70 | + return out.String(), nil |
| 71 | + } |
| 72 | + |
| 73 | + if canonical { |
| 74 | + decoded, err := decodeJSON(input) |
| 75 | + if err != nil { |
| 76 | + return "", err |
| 77 | + } |
| 78 | + out, err := json.MarshalIndent(decoded, "", " ") |
| 79 | + if err != nil { |
| 80 | + return "", err |
| 81 | + } |
| 82 | + return string(out), nil |
| 83 | + } |
| 84 | + |
| 85 | + var out bytes.Buffer |
| 86 | + if err := json.Indent(&out, input, "", " "); err != nil { |
| 87 | + return "", err |
| 88 | + } |
| 89 | + return out.String(), nil |
| 90 | +} |
| 91 | + |
| 92 | +func readInput(arg string) ([]byte, error) { |
| 93 | + if arg == "-" { |
| 94 | + return os.ReadFile("/dev/stdin") |
| 95 | + } |
| 96 | + return os.ReadFile(arg) |
| 97 | +} |
| 98 | + |
| 99 | +func resolveModes(canonical bool, line bool, fractured bool) (bool, bool, bool, error) { |
| 100 | + modeCount := 0 |
| 101 | + for _, v := range []bool{canonical, line, fractured} { |
| 102 | + if v { |
| 103 | + modeCount++ |
| 104 | + } |
| 105 | + } |
| 106 | + if modeCount > 1 { |
| 107 | + return false, false, false, fmt.Errorf("use at most one of --canonical, --line, --fractured") |
| 108 | + } |
| 109 | + if modeCount == 0 { |
| 110 | + fractured = true |
| 111 | + } |
| 112 | + return canonical, line, fractured, nil |
| 113 | +} |
| 114 | + |
| 115 | +func main() { |
| 116 | + var canonical = pflag.Bool("canonical", false, "Output canonical JSON (same as jq . --sort-keys)") |
| 117 | + var line = pflag.Bool("line", false, "Output JSON on a single line") |
| 118 | + var fractured = pflag.Bool("fractured", false, "Use fractured JSON formatting") |
| 119 | + var trailingNewline = pflag.Bool("trailing-newline", false, "Emit a trailing newline") |
| 120 | + var eol = pflag.String("eol", "lf", "End-of-line style: lf, cr, or crlf") |
| 121 | + |
| 122 | + var help = pflag.BoolP("help", "h", false, "Show help message") |
| 123 | + var version = pflag.Bool("version", false, "Print version information") |
| 124 | + |
| 125 | + pflag.Parse() |
| 126 | + |
| 127 | + if *version { |
| 128 | + fmt.Fprintf(os.Stdout, "jsonfmt version %s (built by %s on %s, commit %s)\n", VERSION, BUILDER, LASTMOD, COMMIT) |
| 129 | + return |
| 130 | + } |
| 131 | + |
| 132 | + if *help { |
| 133 | + fmt.Printf("Usage: jsonfmt [options] [file|-]\n\n") |
| 134 | + fmt.Printf("Options:\n") |
| 135 | + pflag.PrintDefaults() |
| 136 | + fmt.Printf("%s\n", helpText) |
| 137 | + return |
| 138 | + } |
| 139 | + |
| 140 | + canonicalMode, lineMode, fracturedMode, err := resolveModes(*canonical, *line, *fractured) |
| 141 | + if err != nil { |
| 142 | + fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) |
| 143 | + os.Exit(1) |
| 144 | + } |
| 145 | + |
| 146 | + args := pflag.Args() |
| 147 | + if len(args) == 0 { |
| 148 | + args = []string{"-"} |
| 149 | + } |
| 150 | + if len(args) > 1 { |
| 151 | + fmt.Fprintf(os.Stderr, "WARNING: ignoring extra arguments (count=%d)\n", len(args)-1) |
| 152 | + } |
| 153 | + arg := args[0] |
| 154 | + |
| 155 | + input, err := readInput(arg) |
| 156 | + if err != nil { |
| 157 | + fmt.Fprintf(os.Stderr, "ERROR: unable to read input: %v\n", err) |
| 158 | + os.Exit(1) |
| 159 | + } |
| 160 | + |
| 161 | + formatted, err := formatJSON(input, canonicalMode, lineMode, fracturedMode) |
| 162 | + if err != nil { |
| 163 | + fmt.Fprintf(os.Stderr, "ERROR: unable to format JSON: %v\n", err) |
| 164 | + os.Exit(1) |
| 165 | + } |
| 166 | + |
| 167 | + formatted = strings.TrimRight(normalizeLF(formatted), "\n") |
| 168 | + if *trailingNewline { |
| 169 | + formatted += "\n" |
| 170 | + } |
| 171 | + formatted, err = applyEOL(formatted, strings.ToLower(*eol)) |
| 172 | + if err != nil { |
| 173 | + fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) |
| 174 | + os.Exit(1) |
| 175 | + } |
| 176 | + |
| 177 | + fmt.Fprint(os.Stdout, formatted) |
| 178 | +} |
0 commit comments