Skip to content

Commit 8ecc109

Browse files
committed
Implementation of jsonfmt by GPT-5.3-Codex
1 parent 9f7fb84 commit 8ecc109

9 files changed

Lines changed: 346 additions & 1 deletion

File tree

.goreleaser.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,23 @@ builds:
126126
- -X main.COMMIT={{.ShortCommit}}
127127
- -X main.BUILDER=goreleaser
128128

129+
- id: jsonfmt
130+
main: ./cmd/jsonfmt/jsonfmt.go
131+
binary: jsonfmt
132+
env:
133+
- CGO_ENABLED=0
134+
goos:
135+
- linux
136+
- windows
137+
- darwin
138+
ldflags:
139+
- -s
140+
- -w
141+
- -X main.VERSION={{.Version}}
142+
- -X main.LASTMOD={{.CommitDate}}
143+
- -X main.COMMIT={{.ShortCommit}}
144+
- -X main.BUILDER=goreleaser
145+
129146
- id: unhexdump
130147
main: ./cmd/unhexdump/unhexdump.go
131148
binary: unhexdump
@@ -220,6 +237,7 @@ homebrew_casks:
220237
- ghash
221238
- hexdumpc
222239
- hosty
240+
- jsonfmt
223241
- unhexdump
224242
- unicount
225243
- uniwhat
@@ -247,6 +265,7 @@ homebrew_casks:
247265
system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/ghash"]
248266
system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/hexdumpc"]
249267
system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/hosty"]
268+
system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/jsonfmt"]
250269
system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/unhexdump"]
251270
system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/unicount"]
252271
system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/uniwhat"]

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Or download from [Releases](https://github.com/FileFormatInfo/fftools/releases)
2121
- [hexdumpc](cmd/hexdumpc/README.md): generate canonical hexdump (`hexdump -C`) output in case you don't have [hexdump`]
2222
(https://man7.org/linux/man-pages/man1/hexdump.1.html) installed
2323
- [hosty](cmd/hosty/README.md): manipulate hostnames
24+
- [jsonfmt](cmd/jsonfmt/README.md): format JSON (expanded, canonical, line, fractured)
2425
- [unhexdump](cmd/unhexdump/README.md): convert `hexdump -c` output back into binary
2526
- [unicount](cmd/unicount/README.md): count Unicode codepoints in a file
2627
- [uniwhat](cmd/uniwhat/README.md): print the names of each Unicode character in a file
@@ -43,7 +44,6 @@ Or download from [Releases](https://github.com/FileFormatInfo/fftools/releases)
4344

4445
- [ ] `body`: prints specific lines of a file (in between `head` and `tail`)
4546
- [ ] `bom-defuse`: remove byte-order-marks (BOMs) from files
46-
- [ ] `jsonfmt`: pretty print json (see [FracturedJson](https://github.com/j-brooke/FracturedJson/wiki))
4747
- [ ] `purify`: remove high bytes | non-UTF8 | non-ASCII | etc
4848
- [ ] `trilobyte`: translates bytes according to a map
4949
- [ ] `trune`: translates Unicode codepoints (runes) according to a map

cmd/jsonfmt/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
Format JSON files either expanded, compact or single-line
44

5+
Default mode is `--fractured` when no mode flag is provided.
6+
57
## Options
68

79
* `--canonical`: the same output as `jq . --sort-keys`

cmd/jsonfmt/jsonfmt.go

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
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+
}

cmd/jsonfmt/jsonfmt_script_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package main
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
"github.com/rogpeppe/go-internal/testscript"
8+
)
9+
10+
func TestMain(m *testing.M) {
11+
exitVal := testscript.RunMain(m, map[string]func() int{
12+
"jsonfmt": func() int {
13+
main()
14+
return 0
15+
},
16+
})
17+
os.Exit(exitVal)
18+
}
19+
20+
func TestJsonfmtScript(t *testing.T) {
21+
testscript.Run(t, testscript.Params{
22+
Files: []string{"../../testdata/jsonfmt.txtar"},
23+
})
24+
}

cmd/jsonfmt/jsonfmt_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package main
2+
3+
import (
4+
"strings"
5+
"testing"
6+
)
7+
8+
func TestResolveModes(t *testing.T) {
9+
canonical, line, fractured, err := resolveModes(false, false, false)
10+
if err != nil {
11+
t.Fatalf("resolveModes default error: %v", err)
12+
}
13+
if canonical || line || !fractured {
14+
t.Fatalf("resolveModes default = canonical:%v line:%v fractured:%v", canonical, line, fractured)
15+
}
16+
17+
if _, _, _, err := resolveModes(true, false, true); err == nil {
18+
t.Fatalf("resolveModes expected conflict error")
19+
}
20+
}
21+
22+
func TestApplyEOL(t *testing.T) {
23+
in := "a\r\nb\rc\n"
24+
25+
lf, err := applyEOL(in, "lf")
26+
if err != nil {
27+
t.Fatalf("applyEOL lf error: %v", err)
28+
}
29+
if lf != "a\nb\nc\n" {
30+
t.Fatalf("applyEOL lf = %q", lf)
31+
}
32+
33+
cr, err := applyEOL(in, "cr")
34+
if err != nil {
35+
t.Fatalf("applyEOL cr error: %v", err)
36+
}
37+
if cr != "a\rb\rc\r" {
38+
t.Fatalf("applyEOL cr = %q", cr)
39+
}
40+
41+
crlf, err := applyEOL(in, "crlf")
42+
if err != nil {
43+
t.Fatalf("applyEOL crlf error: %v", err)
44+
}
45+
if crlf != "a\r\nb\r\nc\r\n" {
46+
t.Fatalf("applyEOL crlf = %q", crlf)
47+
}
48+
49+
if _, err := applyEOL(in, "bad"); err == nil {
50+
t.Fatalf("applyEOL expected error for invalid mode")
51+
}
52+
}
53+
54+
func TestFormatJSONLine(t *testing.T) {
55+
out, err := formatJSON([]byte("{\n \"b\": 2,\n \"a\": 1\n}\n"), false, true, false)
56+
if err != nil {
57+
t.Fatalf("formatJSON line error: %v", err)
58+
}
59+
if out != "{\"b\":2,\"a\":1}" {
60+
t.Fatalf("line format output = %q", out)
61+
}
62+
}
63+
64+
func TestFormatJSONCanonicalSortsKeys(t *testing.T) {
65+
out, err := formatJSON([]byte("{\"b\":2,\"a\":1}"), true, false, false)
66+
if err != nil {
67+
t.Fatalf("formatJSON canonical error: %v", err)
68+
}
69+
if !strings.Contains(out, "\n \"a\": 1,") || !strings.Contains(out, "\n \"b\": 2") {
70+
t.Fatalf("canonical output did not contain expected key/value lines: %q", out)
71+
}
72+
if strings.Index(out, "\"a\"") > strings.Index(out, "\"b\"") {
73+
t.Fatalf("canonical output did not sort keys: %q", out)
74+
}
75+
}
76+
77+
func TestFormatJSONExpanded(t *testing.T) {
78+
out, err := formatJSON([]byte("{\"k\":\"v\"}"), false, false, false)
79+
if err != nil {
80+
t.Fatalf("formatJSON expanded error: %v", err)
81+
}
82+
if out != "{\n \"k\": \"v\"\n}" {
83+
t.Fatalf("expanded output = %q", out)
84+
}
85+
}
86+
87+
func TestFormatJSONFractured(t *testing.T) {
88+
out, err := formatJSON([]byte("{\"a\":1,\"b\":2}"), false, false, true)
89+
if err != nil {
90+
t.Fatalf("formatJSON fractured error: %v", err)
91+
}
92+
if !strings.Contains(out, "\"a\"") || !strings.Contains(out, "\"b\"") {
93+
t.Fatalf("fractured output missing keys: %q", out)
94+
}
95+
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.24.0
55
toolchain go1.24.2
66

77
require (
8+
github.com/FileFormatInfo/go-fractured-json v0.0.0-20260310165450-a56e3d30f067
89
github.com/anyascii/go v0.3.2
910
github.com/mattn/go-isatty v0.0.20
1011
github.com/olekukonko/tablewriter v1.0.7

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
github.com/FileFormatInfo/go-fractured-json v0.0.0-20260310165450-a56e3d30f067 h1:hUEtcfcbDhcnimttJSgIor4F5h15I0aXHgVFoa3BRmE=
2+
github.com/FileFormatInfo/go-fractured-json v0.0.0-20260310165450-a56e3d30f067/go.mod h1:vf0/8Lsb160Lag/yfTJ5MB/pAf/R4IYCKj/o8befaJs=
13
github.com/anyascii/go v0.3.2 h1:87uFISteh7vwofK02srrPKtAvG6Wx7ozRjNh8uhfa7w=
24
github.com/anyascii/go v0.3.2/go.mod h1:HDvbMmSpqJyIe+xtSkHmAYTjc8PzvO3l1Jmgx/IFUPs=
35
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=

0 commit comments

Comments
 (0)