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