File: json2.go 1 /* 2 The MIT License (MIT) 3 4 Copyright (c) 2026 pacman64 5 6 Permission is hereby granted, free of charge, to any person obtaining a copy of 7 this software and associated documentation files (the "Software"), to deal 8 in the Software without restriction, including without limitation the rights to 9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 of the Software, and to permit persons to whom the Software is furnished to do 11 so, subject to the following conditions: 12 13 The above copyright notice and this permission notice shall be included in all 14 copies or substantial portions of the Software. 15 16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 SOFTWARE. 23 */ 24 25 /* 26 To compile a smaller-sized command-line app, you can use the `go` command as 27 follows: 28 29 go build -ldflags "-s -w" -trimpath json2.go 30 */ 31 32 package main 33 34 import ( 35 "bufio" 36 "encoding/json" 37 "errors" 38 "io" 39 "os" 40 ) 41 42 const info = ` 43 json2 [filepath...] 44 45 JSON-2 indents valid JSON input into multi-line JSON which uses 2 spaces for 46 each indentation level. 47 ` 48 49 func main() { 50 args := os.Args[1:] 51 52 if len(args) > 0 { 53 switch args[0] { 54 case `-h`, `--h`, `-help`, `--help`: 55 os.Stdout.WriteString(info[1:]) 56 return 57 } 58 } 59 60 if len(args) > 0 && args[0] == `--` { 61 args = args[1:] 62 } 63 64 if len(args) > 1 { 65 const msg = "multiple inputs aren't allowed\n" 66 os.Stderr.WriteString(msg) 67 os.Exit(1) 68 } 69 70 // figure out whether input should come from a named file or from stdin 71 name := `-` 72 if len(args) == 1 { 73 name = args[0] 74 } 75 76 if err := handleInput(os.Stdout, name); err != nil && err != io.EOF { 77 os.Stderr.WriteString(err.Error()) 78 os.Stderr.WriteString("\n") 79 os.Exit(1) 80 } 81 } 82 83 // handleInput simplifies control-flow for func main 84 func handleInput(w io.Writer, path string) error { 85 if path == `-` { 86 return convert(w, os.Stdin) 87 } 88 89 f, err := os.Open(path) 90 if err != nil { 91 // on windows, file-not-found error messages may mention `CreateFile`, 92 // even when trying to open files in read-only mode 93 return errors.New(`can't open file named ` + path) 94 } 95 defer f.Close() 96 return convert(w, f) 97 } 98 99 // convert simplifies control-flow for func handleInput 100 func convert(w io.Writer, r io.Reader) error { 101 bw := bufio.NewWriter(w) 102 defer bw.Flush() 103 return json2(bw, r) 104 } 105 106 // escapedStringBytes helps func handleString treat all string bytes quickly 107 // and correctly, using their officially-supported JSON escape sequences 108 // 109 // https://www.rfc-editor.org/rfc/rfc8259#section-7 110 var escapedStringBytes = [256][]byte{ 111 {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'}, 112 {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'}, 113 {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'}, 114 {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'}, 115 {'\\', 'b'}, {'\\', 't'}, 116 {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'}, 117 {'\\', 'f'}, {'\\', 'r'}, 118 {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'}, 119 {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'}, 120 {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'}, 121 {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'}, 122 {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'}, 123 {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'}, 124 {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'}, 125 {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'}, 126 {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'}, 127 {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39}, 128 {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47}, 129 {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55}, 130 {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63}, 131 {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71}, 132 {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79}, 133 {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87}, 134 {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95}, 135 {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103}, 136 {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111}, 137 {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119}, 138 {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127}, 139 {128}, {129}, {130}, {131}, {132}, {133}, {134}, {135}, 140 {136}, {137}, {138}, {139}, {140}, {141}, {142}, {143}, 141 {144}, {145}, {146}, {147}, {148}, {149}, {150}, {151}, 142 {152}, {153}, {154}, {155}, {156}, {157}, {158}, {159}, 143 {160}, {161}, {162}, {163}, {164}, {165}, {166}, {167}, 144 {168}, {169}, {170}, {171}, {172}, {173}, {174}, {175}, 145 {176}, {177}, {178}, {179}, {180}, {181}, {182}, {183}, 146 {184}, {185}, {186}, {187}, {188}, {189}, {190}, {191}, 147 {192}, {193}, {194}, {195}, {196}, {197}, {198}, {199}, 148 {200}, {201}, {202}, {203}, {204}, {205}, {206}, {207}, 149 {208}, {209}, {210}, {211}, {212}, {213}, {214}, {215}, 150 {216}, {217}, {218}, {219}, {220}, {221}, {222}, {223}, 151 {224}, {225}, {226}, {227}, {228}, {229}, {230}, {231}, 152 {232}, {233}, {234}, {235}, {236}, {237}, {238}, {239}, 153 {240}, {241}, {242}, {243}, {244}, {245}, {246}, {247}, 154 {248}, {249}, {250}, {251}, {252}, {253}, {254}, {255}, 155 } 156 157 // writeSpaces does what it says, minimizing calls to write-like funcs 158 func writeSpaces(w *bufio.Writer, n int) { 159 const spaces = ` ` 160 if n < 1 { 161 return 162 } 163 164 for n >= len(spaces) { 165 w.WriteString(spaces) 166 n -= len(spaces) 167 } 168 w.WriteString(spaces[:n]) 169 } 170 171 // json2 does it all, given a reader and a writer 172 func json2(w *bufio.Writer, r io.Reader) error { 173 dec := json.NewDecoder(r) 174 // avoid parsing numbers, so unusually-long numbers are kept verbatim, 175 // even if JSON parsers aren't required to guarantee such input-fidelity 176 // for numbers 177 dec.UseNumber() 178 179 t, err := dec.Token() 180 if err == io.EOF { 181 return errors.New(`input has no JSON values`) 182 } 183 184 if err = handleToken(w, dec, t, 0, 0); err != nil { 185 return err 186 } 187 // don't forget ending the last line for the last value 188 w.WriteByte('\n') 189 190 _, err = dec.Token() 191 if err == io.EOF { 192 // input is over, so it's a success 193 return nil 194 } 195 196 if err == nil { 197 // a successful `read` is a failure, as it means there are 198 // trailing JSON tokens 199 return errors.New(`unexpected trailing data`) 200 } 201 202 // any other error, perhaps some invalid-JSON-syntax-type error 203 return err 204 } 205 206 // handleToken handles recursion for func json2 207 func handleToken(w *bufio.Writer, dec *json.Decoder, t json.Token, pre, level int) error { 208 switch t := t.(type) { 209 case json.Delim: 210 switch t { 211 case json.Delim('['): 212 return handleArray(w, dec, pre, level) 213 case json.Delim('{'): 214 return handleObject(w, dec, pre, level) 215 default: 216 return errors.New(`unsupported JSON syntax ` + string(t)) 217 } 218 219 case nil: 220 writeSpaces(w, 2*pre) 221 w.WriteString(`null`) 222 return nil 223 224 case bool: 225 writeSpaces(w, 2*pre) 226 if t { 227 w.WriteString(`true`) 228 } else { 229 w.WriteString(`false`) 230 } 231 return nil 232 233 case json.Number: 234 writeSpaces(w, 2*pre) 235 w.WriteString(t.String()) 236 return nil 237 238 case string: 239 return handleString(w, t, pre) 240 241 default: 242 // return fmt.Errorf(`unsupported token type %T`, t) 243 return errors.New(`invalid JSON token`) 244 } 245 } 246 247 // handleArray handles arrays for func handleToken 248 func handleArray(w *bufio.Writer, dec *json.Decoder, pre, level int) error { 249 for i := 0; true; i++ { 250 t, err := dec.Token() 251 if err != nil { 252 return err 253 } 254 255 if t == json.Delim(']') { 256 if i == 0 { 257 writeSpaces(w, 2*pre) 258 w.WriteByte('[') 259 w.WriteByte(']') 260 } else { 261 w.WriteByte('\n') 262 writeSpaces(w, 2*level) 263 w.WriteByte(']') 264 } 265 return nil 266 } 267 268 if i == 0 { 269 writeSpaces(w, 2*pre) 270 w.WriteByte('[') 271 w.WriteByte('\n') 272 } else { 273 w.WriteByte(',') 274 w.WriteByte('\n') 275 if err := w.Flush(); err != nil { 276 // a write error may be the consequence of stdout being closed, 277 // perhaps by another app along a pipe 278 return io.EOF 279 } 280 } 281 282 err = handleToken(w, dec, t, level+1, level+1) 283 if err != nil { 284 return err 285 } 286 } 287 288 // make the compiler happy 289 return nil 290 } 291 292 // handleObject handles objects for func handleToken 293 func handleObject(w *bufio.Writer, dec *json.Decoder, pre, level int) error { 294 for i := 0; true; i++ { 295 t, err := dec.Token() 296 if err != nil { 297 return err 298 } 299 300 if t == json.Delim('}') { 301 if i == 0 { 302 writeSpaces(w, 2*pre) 303 w.WriteByte('{') 304 w.WriteByte('}') 305 } else { 306 w.WriteByte('\n') 307 writeSpaces(w, 2*level) 308 w.WriteByte('}') 309 } 310 return nil 311 } 312 313 if i == 0 { 314 writeSpaces(w, 2*pre) 315 w.WriteByte('{') 316 w.WriteByte('\n') 317 } else { 318 w.WriteByte(',') 319 w.WriteByte('\n') 320 } 321 322 k, ok := t.(string) 323 if !ok { 324 return errors.New(`expected a string for a key-value pair`) 325 } 326 327 err = handleString(w, k, level+1) 328 if err != nil { 329 return err 330 } 331 332 w.WriteString(": ") 333 334 t, err = dec.Token() 335 if err == io.EOF { 336 return errors.New(`expected a value for a key-value pair`) 337 } 338 339 err = handleToken(w, dec, t, 0, level+1) 340 if err != nil { 341 return err 342 } 343 } 344 345 // make the compiler happy 346 return nil 347 } 348 349 // handleString handles strings for func handleToken, and keys for func 350 // handleObject 351 func handleString(w *bufio.Writer, s string, level int) error { 352 writeSpaces(w, 2*level) 353 w.WriteByte('"') 354 for i := range s { 355 w.Write(escapedStringBytes[s[i]]) 356 } 357 w.WriteByte('"') 358 return nil 359 }