File: json2.go 1 /* 2 The MIT License (MIT) 3 4 Copyright © 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 // errNoMoreOutput is a generic dummy output-error, which is meant to be 50 // ultimately ignored, being just an excuse to quit the app immediately 51 // and successfully 52 var errNoMoreOutput = errors.New(`no more output`) 53 54 func main() { 55 if len(os.Args) > 1 { 56 switch os.Args[1] { 57 case `-h`, `--h`, `-help`, `--help`: 58 os.Stdout.WriteString(info[1:]) 59 return 60 } 61 } 62 63 if len(os.Args) > 2 { 64 os.Stderr.WriteString("multiple inputs not allowed\n") 65 os.Exit(1) 66 } 67 68 // figure out whether input should come from a named file or from stdin 69 path := `-` 70 if len(os.Args) > 1 { 71 path = os.Args[1] 72 } 73 74 err := handleInput(os.Stdout, path) 75 if err != nil && err != io.EOF && err != errNoMoreOutput { 76 os.Stderr.WriteString(err.Error()) 77 os.Stderr.WriteString("\n") 78 os.Exit(1) 79 } 80 } 81 82 // handleInput simplifies control-flow for func main 83 func handleInput(w io.Writer, path string) error { 84 if path == `-` { 85 return convert(w, os.Stdin) 86 } 87 88 // if f := strings.HasPrefix; f(path, `https://`) || f(path, `http://`) { 89 // resp, err := http.Get(path) 90 // if err != nil { 91 // return err 92 // } 93 // defer resp.Body.Close() 94 // return convert(w, resp.Body) 95 // } 96 97 f, err := os.Open(path) 98 if err != nil { 99 // on windows, file-not-found error messages may mention `CreateFile`, 100 // even when trying to open files in read-only mode 101 return errors.New(`can't open file named ` + path) 102 } 103 defer f.Close() 104 return convert(w, f) 105 } 106 107 // convert simplifies control-flow for func handleInput 108 func convert(w io.Writer, r io.Reader) error { 109 bw := bufio.NewWriter(w) 110 defer bw.Flush() 111 return json2(bw, r) 112 } 113 114 // escapedStringBytes helps func handleString treat all string bytes quickly 115 // and correctly, using their officially-supported JSON escape sequences 116 // 117 // https://www.rfc-editor.org/rfc/rfc8259#section-7 118 var escapedStringBytes = [256][]byte{ 119 {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'}, 120 {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'}, 121 {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'}, 122 {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'}, 123 {'\\', 'b'}, {'\\', 't'}, 124 {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'}, 125 {'\\', 'f'}, {'\\', 'r'}, 126 {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'}, 127 {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'}, 128 {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'}, 129 {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'}, 130 {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'}, 131 {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'}, 132 {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'}, 133 {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'}, 134 {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'}, 135 {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39}, 136 {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47}, 137 {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55}, 138 {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63}, 139 {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71}, 140 {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79}, 141 {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87}, 142 {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95}, 143 {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103}, 144 {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111}, 145 {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119}, 146 {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127}, 147 {128}, {129}, {130}, {131}, {132}, {133}, {134}, {135}, 148 {136}, {137}, {138}, {139}, {140}, {141}, {142}, {143}, 149 {144}, {145}, {146}, {147}, {148}, {149}, {150}, {151}, 150 {152}, {153}, {154}, {155}, {156}, {157}, {158}, {159}, 151 {160}, {161}, {162}, {163}, {164}, {165}, {166}, {167}, 152 {168}, {169}, {170}, {171}, {172}, {173}, {174}, {175}, 153 {176}, {177}, {178}, {179}, {180}, {181}, {182}, {183}, 154 {184}, {185}, {186}, {187}, {188}, {189}, {190}, {191}, 155 {192}, {193}, {194}, {195}, {196}, {197}, {198}, {199}, 156 {200}, {201}, {202}, {203}, {204}, {205}, {206}, {207}, 157 {208}, {209}, {210}, {211}, {212}, {213}, {214}, {215}, 158 {216}, {217}, {218}, {219}, {220}, {221}, {222}, {223}, 159 {224}, {225}, {226}, {227}, {228}, {229}, {230}, {231}, 160 {232}, {233}, {234}, {235}, {236}, {237}, {238}, {239}, 161 {240}, {241}, {242}, {243}, {244}, {245}, {246}, {247}, 162 {248}, {249}, {250}, {251}, {252}, {253}, {254}, {255}, 163 } 164 165 // writeSpaces does what it says, minimizing calls to write-like funcs 166 func writeSpaces(w *bufio.Writer, n int) { 167 const spaces = ` ` 168 if n < 1 { 169 return 170 } 171 172 for n >= len(spaces) { 173 w.WriteString(spaces) 174 n -= len(spaces) 175 } 176 w.WriteString(spaces[:n]) 177 } 178 179 // json2 does it all, given a reader and a writer 180 func json2(w *bufio.Writer, r io.Reader) error { 181 dec := json.NewDecoder(r) 182 // avoid parsing numbers, so unusually-long numbers are kept verbatim, 183 // even if JSON parsers aren't required to guarantee such input-fidelity 184 // for numbers 185 dec.UseNumber() 186 187 t, err := dec.Token() 188 if err == io.EOF { 189 return errors.New(`input has no JSON values`) 190 } 191 192 if err = handleToken(w, dec, t, 0, 0); err != nil { 193 return err 194 } 195 // don't forget ending the last line for the last value 196 w.WriteByte('\n') 197 198 _, err = dec.Token() 199 if err == io.EOF { 200 // input is over, so it's a success 201 return nil 202 } 203 204 if err == nil { 205 // a successful `read` is a failure, as it means there are 206 // trailing JSON tokens 207 return errors.New(`unexpected trailing data`) 208 } 209 210 // any other error, perhaps some invalid-JSON-syntax-type error 211 return err 212 } 213 214 // handleToken handles recursion for func json2 215 func handleToken(w *bufio.Writer, dec *json.Decoder, t json.Token, pre, level int) error { 216 switch t := t.(type) { 217 case json.Delim: 218 switch t { 219 case json.Delim('['): 220 return handleArray(w, dec, pre, level) 221 case json.Delim('{'): 222 return handleObject(w, dec, pre, level) 223 default: 224 return errors.New(`unsupported JSON syntax ` + string(t)) 225 } 226 227 case nil: 228 writeSpaces(w, 2*pre) 229 w.WriteString(`null`) 230 return nil 231 232 case bool: 233 writeSpaces(w, 2*pre) 234 if t { 235 w.WriteString(`true`) 236 } else { 237 w.WriteString(`false`) 238 } 239 return nil 240 241 case json.Number: 242 writeSpaces(w, 2*pre) 243 w.WriteString(t.String()) 244 return nil 245 246 case string: 247 return handleString(w, t, pre) 248 249 default: 250 // return fmt.Errorf(`unsupported token type %T`, t) 251 return errors.New(`invalid JSON token`) 252 } 253 } 254 255 // handleArray handles arrays for func handleToken 256 func handleArray(w *bufio.Writer, dec *json.Decoder, pre, level int) error { 257 for i := 0; true; i++ { 258 t, err := dec.Token() 259 if err != nil { 260 return err 261 } 262 263 if t == json.Delim(']') { 264 if i == 0 { 265 writeSpaces(w, 2*pre) 266 w.WriteByte('[') 267 w.WriteByte(']') 268 } else { 269 w.WriteByte('\n') 270 writeSpaces(w, 2*level) 271 w.WriteByte(']') 272 } 273 return nil 274 } 275 276 if i == 0 { 277 writeSpaces(w, 2*pre) 278 w.WriteByte('[') 279 w.WriteByte('\n') 280 } else { 281 w.WriteByte(',') 282 w.WriteByte('\n') 283 if err := w.Flush(); err != nil { 284 // a write error may be the consequence of stdout being closed, 285 // perhaps by another app along a pipe 286 return errNoMoreOutput 287 } 288 } 289 290 err = handleToken(w, dec, t, level+1, level+1) 291 if err != nil { 292 return err 293 } 294 } 295 296 // make the compiler happy 297 return nil 298 } 299 300 // handleObject handles objects for func handleToken 301 func handleObject(w *bufio.Writer, dec *json.Decoder, pre, level int) error { 302 for i := 0; true; i++ { 303 t, err := dec.Token() 304 if err != nil { 305 return err 306 } 307 308 if t == json.Delim('}') { 309 if i == 0 { 310 writeSpaces(w, 2*pre) 311 w.WriteByte('{') 312 w.WriteByte('}') 313 } else { 314 w.WriteByte('\n') 315 writeSpaces(w, 2*level) 316 w.WriteByte('}') 317 } 318 return nil 319 } 320 321 if i == 0 { 322 writeSpaces(w, 2*pre) 323 w.WriteByte('{') 324 w.WriteByte('\n') 325 } else { 326 w.WriteByte(',') 327 w.WriteByte('\n') 328 if err := w.Flush(); err != nil { 329 // a write error may be the consequence of stdout being closed, 330 // perhaps by another app along a pipe 331 return errNoMoreOutput 332 } 333 } 334 335 k, ok := t.(string) 336 if !ok { 337 return errors.New(`expected a string for a key-value pair`) 338 } 339 340 err = handleString(w, k, level+1) 341 if err != nil { 342 return err 343 } 344 345 w.WriteString(": ") 346 347 t, err = dec.Token() 348 if err == io.EOF { 349 return errors.New(`expected a value for a key-value pair`) 350 } 351 352 err = handleToken(w, dec, t, 0, level+1) 353 if err != nil { 354 return err 355 } 356 } 357 358 // make the compiler happy 359 return nil 360 } 361 362 // handleString handles strings for func handleToken, and keys for func 363 // handleObject 364 func handleString(w *bufio.Writer, s string, level int) error { 365 writeSpaces(w, 2*level) 366 w.WriteByte('"') 367 for i := range s { 368 w.Write(escapedStringBytes[s[i]]) 369 } 370 w.WriteByte('"') 371 return nil 372 }